blob: 3f8577146086ae1d12b59d83838bc651ab6564a5 [file] [log] [blame]
James E. Blair83005782015-12-11 14:46:03 -08001# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13import os
14import logging
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +100015import six
James E. Blair83005782015-12-11 14:46:03 -080016import yaml
17
18import voluptuous as vs
19
Morgan Fainberg78c301a2016-07-14 13:47:01 -070020from zuul import model
James E. Blair83005782015-12-11 14:46:03 -080021import zuul.manager.dependent
22import zuul.manager.independent
23from zuul import change_matcher
24
25
26# Several forms accept either a single item or a list, this makes
27# specifying that in the schema easy (and explicit).
28def to_list(x):
29 return vs.Any([x], x)
30
31
32def as_list(item):
33 if not item:
34 return []
35 if isinstance(item, list):
36 return item
37 return [item]
38
39
James E. Blaira98340f2016-09-02 11:33:49 -070040class NodeSetParser(object):
41 @staticmethod
42 def getSchema():
43 node = {vs.Required('name'): str,
44 vs.Required('image'): str,
45 }
46
47 nodeset = {vs.Required('name'): str,
48 vs.Required('nodes'): [node],
49 }
50
51 return vs.Schema(nodeset)
52
53 @staticmethod
54 def fromYaml(layout, conf):
55 NodeSetParser.getSchema()(conf)
56 ns = model.NodeSet(conf['name'])
57 for conf_node in as_list(conf['nodes']):
58 node = model.Node(conf_node['name'], conf_node['image'])
59 ns.addNode(node)
60 return ns
61
62
James E. Blair83005782015-12-11 14:46:03 -080063class JobParser(object):
64 @staticmethod
65 def getSchema():
Ricardo Carrillo Cruz4e94f612016-07-25 16:11:56 +000066 swift_tmpurl = {vs.Required('name'): str,
67 'container': str,
68 'expiry': int,
69 'max_file_size': int,
70 'max-file-size': int,
71 'max_file_count': int,
72 'max-file-count': int,
73 'logserver_prefix': str,
74 'logserver-prefix': str,
75 }
76
Ricardo Carrillo Cruz12c892b2016-11-18 15:35:49 +000077 auth = {'secrets': to_list(str),
Ricardo Carrillo Cruz4e94f612016-07-25 16:11:56 +000078 'inherit': bool,
79 'swift-tmpurl': to_list(swift_tmpurl),
80 }
James E. Blair83005782015-12-11 14:46:03 -080081
James E. Blair8d692392016-04-08 17:47:58 -070082 node = {vs.Required('name'): str,
83 vs.Required('image'): str,
84 }
85
James E. Blair83005782015-12-11 14:46:03 -080086 job = {vs.Required('name'): str,
87 'parent': str,
88 'queue-name': str,
89 'failure-message': str,
90 'success-message': str,
91 'failure-url': str,
92 'success-url': str,
James E. Blaire89c4902016-08-03 11:20:32 -070093 'hold-following-changes': bool,
James E. Blair83005782015-12-11 14:46:03 -080094 'voting': bool,
Joshua Hesketh89b67f62016-02-11 21:22:14 +110095 'mutex': str,
Joshua Heskethdc7820c2016-03-11 13:14:28 +110096 'tags': to_list(str),
James E. Blair83005782015-12-11 14:46:03 -080097 'branches': to_list(str),
98 'files': to_list(str),
Ricardo Carrillo Cruz4e94f612016-07-25 16:11:56 +000099 'auth': to_list(auth),
James E. Blair83005782015-12-11 14:46:03 -0800100 'irrelevant-files': to_list(str),
James E. Blair0eaad552016-09-02 12:09:54 -0700101 'nodes': vs.Any([node], str),
James E. Blair83005782015-12-11 14:46:03 -0800102 'timeout': int,
James E. Blair4317e9f2016-07-15 10:05:47 -0700103 '_source_project': model.Project,
James E. Blaire208c482016-10-04 14:35:30 -0700104 '_source_branch': vs.Any(str, None),
James E. Blair83005782015-12-11 14:46:03 -0800105 }
106
107 return vs.Schema(job)
108
109 @staticmethod
110 def fromYaml(layout, conf):
111 JobParser.getSchema()(conf)
112 job = model.Job(conf['name'])
Ricardo Carrillo Cruz4e94f612016-07-25 16:11:56 +0000113 if 'auth' in conf:
114 job.auth = conf.get('auth')
James E. Blair83005782015-12-11 14:46:03 -0800115 if 'parent' in conf:
116 parent = layout.getJob(conf['parent'])
117 job.inheritFrom(parent)
118 job.timeout = conf.get('timeout', job.timeout)
119 job.workspace = conf.get('workspace', job.workspace)
120 job.pre_run = as_list(conf.get('pre-run', job.pre_run))
121 job.post_run = as_list(conf.get('post-run', job.post_run))
122 job.voting = conf.get('voting', True)
James E. Blaire89c4902016-08-03 11:20:32 -0700123 job.hold_following_changes = conf.get('hold-following-changes', False)
Joshua Hesketh89b67f62016-02-11 21:22:14 +1100124 job.mutex = conf.get('mutex', None)
Joshua Hesketh3f7def32016-11-21 17:36:44 +1100125 job.attempts = conf.get('attempts', 3)
James E. Blair34776ee2016-08-25 13:53:54 -0700126 if 'nodes' in conf:
James E. Blair0eaad552016-09-02 12:09:54 -0700127 conf_nodes = conf['nodes']
128 if isinstance(conf_nodes, six.string_types):
129 # This references an existing named nodeset in the layout.
130 ns = layout.nodesets[conf_nodes]
131 else:
132 ns = model.NodeSet()
133 for conf_node in conf_nodes:
134 node = model.Node(conf_node['name'], conf_node['image'])
135 ns.addNode(node)
136 job.nodeset = ns
James E. Blair34776ee2016-08-25 13:53:54 -0700137
Joshua Heskethdc7820c2016-03-11 13:14:28 +1100138 tags = conf.get('tags')
139 if tags:
140 # Tags are merged via a union rather than a
141 # destructive copy because they are intended to
142 # accumulate onto any previously applied tags from
143 # metajobs.
144 job.tags = job.tags.union(set(tags))
James E. Blaire208c482016-10-04 14:35:30 -0700145 # The source attributes may not be overridden -- they are
146 # always supplied by the config loader. They correspond to
147 # the Project instance of the repo where it originated, and
148 # the branch name.
James E. Blair4317e9f2016-07-15 10:05:47 -0700149 job.source_project = conf.get('_source_project')
James E. Blaire208c482016-10-04 14:35:30 -0700150 job.source_branch = conf.get('_source_branch')
James E. Blair83005782015-12-11 14:46:03 -0800151 job.failure_message = conf.get('failure-message', job.failure_message)
152 job.success_message = conf.get('success-message', job.success_message)
153 job.failure_url = conf.get('failure-url', job.failure_url)
154 job.success_url = conf.get('success-url', job.success_url)
James E. Blair96c6bf82016-01-15 16:20:40 -0800155
James E. Blaire208c482016-10-04 14:35:30 -0700156 # If the definition for this job came from a project repo,
157 # implicitly apply a branch matcher for the branch it was on.
158 if job.source_branch:
159 branches = [job.source_branch]
160 elif 'branches' in conf:
161 branches = as_list(conf['branches'])
162 else:
163 branches = None
164 if branches:
James E. Blair83005782015-12-11 14:46:03 -0800165 matchers = []
James E. Blaire208c482016-10-04 14:35:30 -0700166 for branch in branches:
James E. Blair83005782015-12-11 14:46:03 -0800167 matchers.append(change_matcher.BranchMatcher(branch))
168 job.branch_matcher = change_matcher.MatchAny(matchers)
169 if 'files' in conf:
170 matchers = []
171 for fn in as_list(conf['files']):
172 matchers.append(change_matcher.FileMatcher(fn))
173 job.file_matcher = change_matcher.MatchAny(matchers)
174 if 'irrelevant-files' in conf:
175 matchers = []
176 for fn in as_list(conf['irrelevant-files']):
177 matchers.append(change_matcher.FileMatcher(fn))
178 job.irrelevant_file_matcher = change_matcher.MatchAllFiles(
179 matchers)
180 return job
181
182
James E. Blairb97ed802015-12-21 15:55:35 -0800183class ProjectTemplateParser(object):
184 log = logging.getLogger("zuul.ProjectTemplateParser")
185
186 @staticmethod
187 def getSchema(layout):
Adam Gandelman8bd57102016-12-02 12:58:42 -0800188 project_template = {
189 vs.Required('name'): str,
190 'merge-mode': vs.Any(
191 'merge', 'merge-resolve',
192 'cherry-pick')}
193
James E. Blairb97ed802015-12-21 15:55:35 -0800194 for p in layout.pipelines.values():
195 project_template[p.name] = {'queue': str,
196 'jobs': [vs.Any(str, dict)]}
197 return vs.Schema(project_template)
198
199 @staticmethod
200 def fromYaml(layout, conf):
201 ProjectTemplateParser.getSchema(layout)(conf)
202 project_template = model.ProjectConfig(conf['name'])
203 for pipeline in layout.pipelines.values():
204 conf_pipeline = conf.get(pipeline.name)
205 if not conf_pipeline:
206 continue
207 project_pipeline = model.ProjectPipelineConfig()
208 project_template.pipelines[pipeline.name] = project_pipeline
James E. Blair0dcef7a2016-08-19 09:35:17 -0700209 project_pipeline.queue_name = conf_pipeline.get('queue')
James E. Blairb97ed802015-12-21 15:55:35 -0800210 project_pipeline.job_tree = ProjectTemplateParser._parseJobTree(
James E. Blairf42035b2016-08-24 09:37:16 -0700211 layout, conf_pipeline.get('jobs', []))
James E. Blairb97ed802015-12-21 15:55:35 -0800212 return project_template
213
214 @staticmethod
215 def _parseJobTree(layout, conf, tree=None):
216 if not tree:
217 tree = model.JobTree(None)
218 for conf_job in conf:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +1000219 if isinstance(conf_job, six.string_types):
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700220 tree.addJob(model.Job(conf_job))
James E. Blairb97ed802015-12-21 15:55:35 -0800221 elif isinstance(conf_job, dict):
222 # A dictionary in a job tree may override params, or
223 # be the root of a sub job tree, or both.
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700224 jobname, attrs = conf_job.items()[0]
225 jobs = attrs.pop('jobs', None)
James E. Blairb97ed802015-12-21 15:55:35 -0800226 if attrs:
227 # We are overriding params, so make a new job def
228 attrs['name'] = jobname
229 subtree = tree.addJob(JobParser.fromYaml(layout, attrs))
230 else:
231 # Not overriding, so get existing job
232 subtree = tree.addJob(layout.getJob(jobname))
233
234 if jobs:
235 # This is the root of a sub tree
236 ProjectTemplateParser._parseJobTree(layout, jobs, subtree)
237 else:
238 raise Exception("Job must be a string or dictionary")
239 return tree
240
241
242class ProjectParser(object):
243 log = logging.getLogger("zuul.ProjectParser")
244
245 @staticmethod
246 def getSchema(layout):
247 project = {vs.Required('name'): str,
Adam Gandelman8bd57102016-12-02 12:58:42 -0800248 'templates': [str],
249 'merge-mode': vs.Any('merge', 'merge-resolve',
250 'cherry-pick')}
James E. Blairb97ed802015-12-21 15:55:35 -0800251 for p in layout.pipelines.values():
252 project[p.name] = {'queue': str,
253 'jobs': [vs.Any(str, dict)]}
254 return vs.Schema(project)
255
256 @staticmethod
257 def fromYaml(layout, conf):
James E. Blaire208c482016-10-04 14:35:30 -0700258 # TODOv3(jeblair): This may need some branch-specific
259 # configuration for in-repo configs.
James E. Blairb97ed802015-12-21 15:55:35 -0800260 ProjectParser.getSchema(layout)(conf)
261 conf_templates = conf.pop('templates', [])
262 # The way we construct a project definition is by parsing the
263 # definition as a template, then applying all of the
264 # templates, including the newly parsed one, in order.
265 project_template = ProjectTemplateParser.fromYaml(layout, conf)
266 configs = [layout.project_templates[name] for name in conf_templates]
267 configs.append(project_template)
268 project = model.ProjectConfig(conf['name'])
Adam Gandelman8bd57102016-12-02 12:58:42 -0800269 mode = conf.get('merge-mode', 'merge-resolve')
270 project.merge_mode = model.MERGER_MAP[mode]
James E. Blairb97ed802015-12-21 15:55:35 -0800271 for pipeline in layout.pipelines.values():
272 project_pipeline = model.ProjectPipelineConfig()
273 project_pipeline.job_tree = model.JobTree(None)
274 queue_name = None
275 # For every template, iterate over the job tree and replace or
276 # create the jobs in the final definition as needed.
277 pipeline_defined = False
278 for template in configs:
James E. Blairb97ed802015-12-21 15:55:35 -0800279 if pipeline.name in template.pipelines:
Paul Belangera466b662016-10-15 10:52:56 -0400280 ProjectParser.log.debug(
281 "Applying template %s to pipeline %s" %
282 (template.name, pipeline.name))
James E. Blairb97ed802015-12-21 15:55:35 -0800283 pipeline_defined = True
284 template_pipeline = template.pipelines[pipeline.name]
285 project_pipeline.job_tree.inheritFrom(
286 template_pipeline.job_tree)
287 if template_pipeline.queue_name:
288 queue_name = template_pipeline.queue_name
289 if queue_name:
290 project_pipeline.queue_name = queue_name
291 if pipeline_defined:
292 project.pipelines[pipeline.name] = project_pipeline
293 return project
294
295
James E. Blairfb610ce2015-12-22 10:24:40 -0800296class PipelineParser(object):
297 log = logging.getLogger("zuul.PipelineParser")
298
299 # A set of reporter configuration keys to action mapping
300 reporter_actions = {
301 'start': 'start_actions',
302 'success': 'success_actions',
303 'failure': 'failure_actions',
304 'merge-failure': 'merge_failure_actions',
305 'disabled': 'disabled_actions',
306 }
307
308 @staticmethod
309 def getDriverSchema(dtype, connections):
310 # TODO(jhesketh): Make the driver discovery dynamic
311 connection_drivers = {
312 'trigger': {
313 'gerrit': 'zuul.trigger.gerrit',
314 },
315 'reporter': {
316 'gerrit': 'zuul.reporter.gerrit',
317 'smtp': 'zuul.reporter.smtp',
318 },
319 }
320 standard_drivers = {
321 'trigger': {
322 'timer': 'zuul.trigger.timer',
323 'zuul': 'zuul.trigger.zuultrigger',
324 }
325 }
326
327 schema = {}
328 # Add the configured connections as available layout options
329 for connection_name, connection in connections.connections.items():
330 for dname, dmod in connection_drivers.get(dtype, {}).items():
331 if connection.driver_name == dname:
332 schema[connection_name] = to_list(__import__(
333 connection_drivers[dtype][dname],
334 fromlist=['']).getSchema())
335
336 # Standard drivers are always available and don't require a unique
337 # (connection) name
338 for dname, dmod in standard_drivers.get(dtype, {}).items():
339 schema[dname] = to_list(__import__(
340 standard_drivers[dtype][dname], fromlist=['']).getSchema())
341
342 return schema
343
344 @staticmethod
345 def getSchema(layout, connections):
346 manager = vs.Any('independent',
347 'dependent')
348
349 precedence = vs.Any('normal', 'low', 'high')
350
351 approval = vs.Schema({'username': str,
352 'email-filter': str,
353 'email': str,
354 'older-than': str,
355 'newer-than': str,
356 }, extra=True)
357
358 require = {'approval': to_list(approval),
359 'open': bool,
360 'current-patchset': bool,
361 'status': to_list(str)}
362
363 reject = {'approval': to_list(approval)}
364
365 window = vs.All(int, vs.Range(min=0))
366 window_floor = vs.All(int, vs.Range(min=1))
367 window_type = vs.Any('linear', 'exponential')
368 window_factor = vs.All(int, vs.Range(min=1))
369
370 pipeline = {vs.Required('name'): str,
371 vs.Required('manager'): manager,
372 'source': str,
373 'precedence': precedence,
374 'description': str,
375 'require': require,
376 'reject': reject,
377 'success-message': str,
378 'failure-message': str,
379 'merge-failure-message': str,
380 'footer-message': str,
381 'dequeue-on-new-patchset': bool,
382 'ignore-dependencies': bool,
383 'disable-after-consecutive-failures':
384 vs.All(int, vs.Range(min=1)),
385 'window': window,
386 'window-floor': window_floor,
387 'window-increase-type': window_type,
388 'window-increase-factor': window_factor,
389 'window-decrease-type': window_type,
390 'window-decrease-factor': window_factor,
391 }
392 pipeline['trigger'] = vs.Required(
393 PipelineParser.getDriverSchema('trigger', connections))
394 for action in ['start', 'success', 'failure', 'merge-failure',
395 'disabled']:
396 pipeline[action] = PipelineParser.getDriverSchema('reporter',
397 connections)
398 return vs.Schema(pipeline)
399
400 @staticmethod
401 def fromYaml(layout, connections, scheduler, conf):
402 PipelineParser.getSchema(layout, connections)(conf)
403 pipeline = model.Pipeline(conf['name'], layout)
404 pipeline.description = conf.get('description')
405
406 pipeline.source = connections.getSource(conf['source'])
407
408 precedence = model.PRECEDENCE_MAP[conf.get('precedence')]
409 pipeline.precedence = precedence
410 pipeline.failure_message = conf.get('failure-message',
411 "Build failed.")
412 pipeline.merge_failure_message = conf.get(
413 'merge-failure-message', "Merge Failed.\n\nThis change or one "
414 "of its cross-repo dependencies was unable to be "
415 "automatically merged with the current state of its "
416 "repository. Please rebase the change and upload a new "
417 "patchset.")
418 pipeline.success_message = conf.get('success-message',
419 "Build succeeded.")
420 pipeline.footer_message = conf.get('footer-message', "")
James E. Blair60af7f42016-03-11 16:11:06 -0800421 pipeline.start_message = conf.get('start-message',
422 "Starting {pipeline.name} jobs.")
James E. Blairfb610ce2015-12-22 10:24:40 -0800423 pipeline.dequeue_on_new_patchset = conf.get(
424 'dequeue-on-new-patchset', True)
425 pipeline.ignore_dependencies = conf.get(
426 'ignore-dependencies', False)
427
428 for conf_key, action in PipelineParser.reporter_actions.items():
429 reporter_set = []
430 if conf.get(conf_key):
431 for reporter_name, params \
432 in conf.get(conf_key).items():
433 reporter = connections.getReporter(reporter_name,
434 params)
435 reporter.setAction(conf_key)
436 reporter_set.append(reporter)
437 setattr(pipeline, action, reporter_set)
438
439 # If merge-failure actions aren't explicit, use the failure actions
440 if not pipeline.merge_failure_actions:
441 pipeline.merge_failure_actions = pipeline.failure_actions
442
443 pipeline.disable_at = conf.get(
444 'disable-after-consecutive-failures', None)
445
446 pipeline.window = conf.get('window', 20)
447 pipeline.window_floor = conf.get('window-floor', 3)
448 pipeline.window_increase_type = conf.get(
449 'window-increase-type', 'linear')
450 pipeline.window_increase_factor = conf.get(
451 'window-increase-factor', 1)
452 pipeline.window_decrease_type = conf.get(
453 'window-decrease-type', 'exponential')
454 pipeline.window_decrease_factor = conf.get(
455 'window-decrease-factor', 2)
456
457 manager_name = conf['manager']
458 if manager_name == 'dependent':
459 manager = zuul.manager.dependent.DependentPipelineManager(
460 scheduler, pipeline)
461 elif manager_name == 'independent':
462 manager = zuul.manager.independent.IndependentPipelineManager(
463 scheduler, pipeline)
464
465 pipeline.setManager(manager)
466 layout.pipelines[conf['name']] = pipeline
467
468 if 'require' in conf or 'reject' in conf:
469 require = conf.get('require', {})
470 reject = conf.get('reject', {})
471 f = model.ChangeishFilter(
472 open=require.get('open'),
473 current_patchset=require.get('current-patchset'),
Jamie Lennoxb59a73f2016-11-23 14:27:18 +1100474 statuses=as_list(require.get('status')),
475 required_approvals=as_list(require.get('approval')),
476 reject_approvals=as_list(reject.get('approval'))
James E. Blairfb610ce2015-12-22 10:24:40 -0800477 )
478 manager.changeish_filters.append(f)
479
480 for trigger_name, trigger_config\
481 in conf.get('trigger').items():
482 trigger = connections.getTrigger(trigger_name, trigger_config)
483 pipeline.triggers.append(trigger)
484
485 # TODO: move
486 manager.event_filters += trigger.getEventFilters(
487 conf['trigger'][trigger_name])
488
489 return pipeline
490
491
James E. Blaird8e778f2015-12-22 14:09:20 -0800492class TenantParser(object):
493 log = logging.getLogger("zuul.TenantParser")
494
James E. Blair96c6bf82016-01-15 16:20:40 -0800495 tenant_source = vs.Schema({'config-repos': [str],
496 'project-repos': [str]})
James E. Blair83005782015-12-11 14:46:03 -0800497
James E. Blaird8e778f2015-12-22 14:09:20 -0800498 @staticmethod
499 def validateTenantSources(connections):
James E. Blair83005782015-12-11 14:46:03 -0800500 def v(value, path=[]):
501 if isinstance(value, dict):
502 for k, val in value.items():
503 connections.getSource(k)
James E. Blaird8e778f2015-12-22 14:09:20 -0800504 TenantParser.validateTenantSource(val, path + [k])
James E. Blair83005782015-12-11 14:46:03 -0800505 else:
506 raise vs.Invalid("Invalid tenant source", path)
507 return v
508
James E. Blaird8e778f2015-12-22 14:09:20 -0800509 @staticmethod
510 def validateTenantSource(value, path=[]):
511 TenantParser.tenant_source(value)
James E. Blair83005782015-12-11 14:46:03 -0800512
James E. Blaird8e778f2015-12-22 14:09:20 -0800513 @staticmethod
514 def getSchema(connections=None):
James E. Blair83005782015-12-11 14:46:03 -0800515 tenant = {vs.Required('name'): str,
James E. Blaird8e778f2015-12-22 14:09:20 -0800516 'source': TenantParser.validateTenantSources(connections)}
517 return vs.Schema(tenant)
James E. Blair83005782015-12-11 14:46:03 -0800518
James E. Blaird8e778f2015-12-22 14:09:20 -0800519 @staticmethod
520 def fromYaml(base, connections, scheduler, merger, conf):
521 TenantParser.getSchema(connections)(conf)
522 tenant = model.Tenant(conf['name'])
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700523 unparsed_config = model.UnparsedTenantConfig()
524 tenant.config_repos, tenant.project_repos = \
525 TenantParser._loadTenantConfigRepos(connections, conf)
526 tenant.config_repos_config, tenant.project_repos_config = \
527 TenantParser._loadTenantInRepoLayouts(
528 merger, connections, tenant.config_repos, tenant.project_repos)
529 unparsed_config.extend(tenant.config_repos_config)
530 unparsed_config.extend(tenant.project_repos_config)
531 tenant.layout = TenantParser._parseLayout(base, unparsed_config,
James E. Blaird8e778f2015-12-22 14:09:20 -0800532 scheduler, connections)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700533 tenant.layout.tenant = tenant
James E. Blaird8e778f2015-12-22 14:09:20 -0800534 return tenant
James E. Blair83005782015-12-11 14:46:03 -0800535
James E. Blaird8e778f2015-12-22 14:09:20 -0800536 @staticmethod
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700537 def _loadTenantConfigRepos(connections, conf_tenant):
538 config_repos = []
539 project_repos = []
540
James E. Blaird8e778f2015-12-22 14:09:20 -0800541 for source_name, conf_source in conf_tenant.get('source', {}).items():
542 source = connections.getSource(source_name)
James E. Blair96c6bf82016-01-15 16:20:40 -0800543
James E. Blair96c6bf82016-01-15 16:20:40 -0800544 for conf_repo in conf_source.get('config-repos', []):
545 project = source.getProject(conf_repo)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700546 config_repos.append((source, project))
James E. Blair96c6bf82016-01-15 16:20:40 -0800547
James E. Blair96c6bf82016-01-15 16:20:40 -0800548 for conf_repo in conf_source.get('project-repos', []):
James E. Blaird8e778f2015-12-22 14:09:20 -0800549 project = source.getProject(conf_repo)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700550 project_repos.append((source, project))
551
552 return config_repos, project_repos
553
554 @staticmethod
555 def _loadTenantInRepoLayouts(merger, connections, config_repos,
556 project_repos):
557 config_repos_config = model.UnparsedTenantConfig()
558 project_repos_config = model.UnparsedTenantConfig()
559 jobs = []
560
561 for (source, project) in config_repos:
562 # Get main config files. These files are permitted the
563 # full range of configuration.
564 url = source.getGitUrl(project)
565 job = merger.getFiles(project.name, url, 'master',
566 files=['zuul.yaml', '.zuul.yaml'])
567 job.project = project
568 job.config_repo = True
569 jobs.append(job)
570
571 for (source, project) in project_repos:
572 # Get in-project-repo config files which have a restricted
573 # set of options.
574 url = source.getGitUrl(project)
James E. Blaire208c482016-10-04 14:35:30 -0700575 # For each branch in the repo, get the zuul.yaml for that
James E. Blair51b74922016-10-04 14:19:57 -0700576 # branch. Remember the branch and then implicitly add a
James E. Blaire208c482016-10-04 14:35:30 -0700577 # branch selector to each job there. This makes the
578 # in-repo configuration apply only to that branch.
579 for branch in source.getProjectBranches(project):
580 job = merger.getFiles(project.name, url, branch,
581 files=['.zuul.yaml'])
582 job.project = project
583 job.branch = branch
584 job.config_repo = False
585 jobs.append(job)
James E. Blair96c6bf82016-01-15 16:20:40 -0800586
James E. Blaird8e778f2015-12-22 14:09:20 -0800587 for job in jobs:
James E. Blair96c6bf82016-01-15 16:20:40 -0800588 # Note: this is an ordered list -- we wait for cat jobs to
589 # complete in the order they were launched which is the
590 # same order they were defined in the main config file.
James E. Blair8d692392016-04-08 17:47:58 -0700591 # This is important for correct inheritance.
James E. Blaird8e778f2015-12-22 14:09:20 -0800592 TenantParser.log.debug("Waiting for cat job %s" % (job,))
593 job.wait()
James E. Blair96c6bf82016-01-15 16:20:40 -0800594 for fn in ['zuul.yaml', '.zuul.yaml']:
595 if job.files.get(fn):
596 TenantParser.log.info(
597 "Loading configuration from %s/%s" %
598 (job.project, fn))
599 if job.config_repo:
600 incdata = TenantParser._parseConfigRepoLayout(
James E. Blair4317e9f2016-07-15 10:05:47 -0700601 job.files[fn], job.project)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700602 config_repos_config.extend(incdata)
James E. Blair96c6bf82016-01-15 16:20:40 -0800603 else:
604 incdata = TenantParser._parseProjectRepoLayout(
James E. Blaire208c482016-10-04 14:35:30 -0700605 job.files[fn], job.project, job.branch)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700606 project_repos_config.extend(incdata)
607 job.project.unparsed_config = incdata
608 return config_repos_config, project_repos_config
James E. Blair83005782015-12-11 14:46:03 -0800609
James E. Blaird8e778f2015-12-22 14:09:20 -0800610 @staticmethod
James E. Blair4317e9f2016-07-15 10:05:47 -0700611 def _parseConfigRepoLayout(data, project):
James E. Blair96c6bf82016-01-15 16:20:40 -0800612 # This is the top-level configuration for a tenant.
613 config = model.UnparsedTenantConfig()
James E. Blair4317e9f2016-07-15 10:05:47 -0700614 config.extend(yaml.load(data), project)
James E. Blair96c6bf82016-01-15 16:20:40 -0800615
James E. Blair96c6bf82016-01-15 16:20:40 -0800616 return config
617
618 @staticmethod
James E. Blaire208c482016-10-04 14:35:30 -0700619 def _parseProjectRepoLayout(data, project, branch):
James E. Blaird8e778f2015-12-22 14:09:20 -0800620 # TODOv3(jeblair): this should implement some rules to protect
621 # aspects of the config that should not be changed in-repo
James E. Blair96c6bf82016-01-15 16:20:40 -0800622 config = model.UnparsedTenantConfig()
James E. Blaire208c482016-10-04 14:35:30 -0700623 config.extend(yaml.load(data), project, branch)
James E. Blair96c6bf82016-01-15 16:20:40 -0800624
James E. Blair96c6bf82016-01-15 16:20:40 -0800625 return config
James E. Blaird8e778f2015-12-22 14:09:20 -0800626
627 @staticmethod
628 def _parseLayout(base, data, scheduler, connections):
629 layout = model.Layout()
630
631 for config_pipeline in data.pipelines:
632 layout.addPipeline(PipelineParser.fromYaml(layout, connections,
633 scheduler,
634 config_pipeline))
635
James E. Blaira98340f2016-09-02 11:33:49 -0700636 for config_nodeset in data.nodesets:
637 layout.addNodeSet(NodeSetParser.fromYaml(layout, config_nodeset))
638
James E. Blaird8e778f2015-12-22 14:09:20 -0800639 for config_job in data.jobs:
640 layout.addJob(JobParser.fromYaml(layout, config_job))
641
642 for config_template in data.project_templates:
643 layout.addProjectTemplate(ProjectTemplateParser.fromYaml(
644 layout, config_template))
645
646 for config_project in data.projects:
647 layout.addProjectConfig(ProjectParser.fromYaml(
648 layout, config_project))
649
650 for pipeline in layout.pipelines.values():
651 pipeline.manager._postConfig(layout)
652
653 return layout
James E. Blair83005782015-12-11 14:46:03 -0800654
655
656class ConfigLoader(object):
657 log = logging.getLogger("zuul.ConfigLoader")
658
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700659 def expandConfigPath(self, config_path):
660 if config_path:
661 config_path = os.path.expanduser(config_path)
662 if not os.path.exists(config_path):
663 raise Exception("Unable to read tenant config file at %s" %
664 config_path)
665 return config_path
666
James E. Blair83005782015-12-11 14:46:03 -0800667 def loadConfig(self, config_path, scheduler, merger, connections):
668 abide = model.Abide()
669
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700670 config_path = self.expandConfigPath(config_path)
James E. Blair83005782015-12-11 14:46:03 -0800671 with open(config_path) as config_file:
672 self.log.info("Loading configuration from %s" % (config_path,))
673 data = yaml.load(config_file)
James E. Blaird8e778f2015-12-22 14:09:20 -0800674 config = model.UnparsedAbideConfig()
675 config.extend(data)
James E. Blair83005782015-12-11 14:46:03 -0800676 base = os.path.dirname(os.path.realpath(config_path))
677
James E. Blaird8e778f2015-12-22 14:09:20 -0800678 for conf_tenant in config.tenants:
679 tenant = TenantParser.fromYaml(base, connections, scheduler,
680 merger, conf_tenant)
James E. Blair83005782015-12-11 14:46:03 -0800681 abide.tenants[tenant.name] = tenant
James E. Blair83005782015-12-11 14:46:03 -0800682 return abide
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700683
684 def createDynamicLayout(self, tenant, files):
685 config = tenant.config_repos_config.copy()
686 for source, project in tenant.project_repos:
687 # TODOv3(jeblair): config should be branch specific
James E. Blaire208c482016-10-04 14:35:30 -0700688 for branch in source.getProjectBranches(project):
689 data = files.getFile(project.name, branch, '.zuul.yaml')
690 if not data:
691 data = project.unparsed_config
692 if not data:
693 continue
694 incdata = TenantParser._parseProjectRepoLayout(
695 data, project, branch)
696 config.extend(incdata)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700697
698 layout = model.Layout()
699 # TODOv3(jeblair): copying the pipelines could be dangerous/confusing.
700 layout.pipelines = tenant.layout.pipelines
701
702 for config_job in config.jobs:
703 layout.addJob(JobParser.fromYaml(layout, config_job))
704
705 for config_template in config.project_templates:
706 layout.addProjectTemplate(ProjectTemplateParser.fromYaml(
707 layout, config_template))
708
709 for config_project in config.projects:
710 layout.addProjectConfig(ProjectParser.fromYaml(
711 layout, config_project), update_pipeline=False)
712
713 return layout