blob: 70a880df0c3608969befb202975d07bb29f09101 [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. Blair83005782015-12-11 14:46:03 -080040class JobParser(object):
41 @staticmethod
42 def getSchema():
43 # TODOv3(jeblair, jhesketh): move to auth
44 swift = {vs.Required('name'): str,
45 'container': str,
46 'expiry': int,
47 'max_file_size': int,
48 'max-file-size': int,
49 'max_file_count': int,
50 'max-file-count': int,
51 'logserver_prefix': str,
52 'logserver-prefix': str,
53 }
54
James E. Blair8d692392016-04-08 17:47:58 -070055 node = {vs.Required('name'): str,
56 vs.Required('image'): str,
57 }
58
James E. Blair83005782015-12-11 14:46:03 -080059 job = {vs.Required('name'): str,
60 'parent': str,
61 'queue-name': str,
62 'failure-message': str,
63 'success-message': str,
64 'failure-url': str,
65 'success-url': str,
66 'voting': bool,
Joshua Hesketh89b67f62016-02-11 21:22:14 +110067 'mutex': str,
Joshua Heskethdc7820c2016-03-11 13:14:28 +110068 'tags': to_list(str),
James E. Blair83005782015-12-11 14:46:03 -080069 'branches': to_list(str),
70 'files': to_list(str),
71 'swift': to_list(swift),
72 'irrelevant-files': to_list(str),
James E. Blair8d692392016-04-08 17:47:58 -070073 'nodes': [node],
James E. Blair83005782015-12-11 14:46:03 -080074 'timeout': int,
James E. Blair4317e9f2016-07-15 10:05:47 -070075 '_source_project': model.Project,
James E. Blair83005782015-12-11 14:46:03 -080076 }
77
78 return vs.Schema(job)
79
80 @staticmethod
81 def fromYaml(layout, conf):
82 JobParser.getSchema()(conf)
83 job = model.Job(conf['name'])
84 if 'parent' in conf:
85 parent = layout.getJob(conf['parent'])
86 job.inheritFrom(parent)
87 job.timeout = conf.get('timeout', job.timeout)
88 job.workspace = conf.get('workspace', job.workspace)
89 job.pre_run = as_list(conf.get('pre-run', job.pre_run))
90 job.post_run = as_list(conf.get('post-run', job.post_run))
91 job.voting = conf.get('voting', True)
Joshua Hesketh89b67f62016-02-11 21:22:14 +110092 job.mutex = conf.get('mutex', None)
Joshua Heskethdc7820c2016-03-11 13:14:28 +110093 tags = conf.get('tags')
94 if tags:
95 # Tags are merged via a union rather than a
96 # destructive copy because they are intended to
97 # accumulate onto any previously applied tags from
98 # metajobs.
99 job.tags = job.tags.union(set(tags))
James E. Blair4317e9f2016-07-15 10:05:47 -0700100 # This attribute may not be overridden -- it is always
101 # supplied by the config loader and is the Project instance of
102 # the repo where it originated.
103 job.source_project = conf.get('_source_project')
James E. Blair83005782015-12-11 14:46:03 -0800104 job.failure_message = conf.get('failure-message', job.failure_message)
105 job.success_message = conf.get('success-message', job.success_message)
106 job.failure_url = conf.get('failure-url', job.failure_url)
107 job.success_url = conf.get('success-url', job.success_url)
James E. Blair96c6bf82016-01-15 16:20:40 -0800108
James E. Blair83005782015-12-11 14:46:03 -0800109 if 'branches' in conf:
110 matchers = []
111 for branch in as_list(conf['branches']):
112 matchers.append(change_matcher.BranchMatcher(branch))
113 job.branch_matcher = change_matcher.MatchAny(matchers)
114 if 'files' in conf:
115 matchers = []
116 for fn in as_list(conf['files']):
117 matchers.append(change_matcher.FileMatcher(fn))
118 job.file_matcher = change_matcher.MatchAny(matchers)
119 if 'irrelevant-files' in conf:
120 matchers = []
121 for fn in as_list(conf['irrelevant-files']):
122 matchers.append(change_matcher.FileMatcher(fn))
123 job.irrelevant_file_matcher = change_matcher.MatchAllFiles(
124 matchers)
125 return job
126
127
James E. Blairb97ed802015-12-21 15:55:35 -0800128class ProjectTemplateParser(object):
129 log = logging.getLogger("zuul.ProjectTemplateParser")
130
131 @staticmethod
132 def getSchema(layout):
133 project_template = {vs.Required('name'): str}
134 for p in layout.pipelines.values():
135 project_template[p.name] = {'queue': str,
136 'jobs': [vs.Any(str, dict)]}
137 return vs.Schema(project_template)
138
139 @staticmethod
140 def fromYaml(layout, conf):
141 ProjectTemplateParser.getSchema(layout)(conf)
142 project_template = model.ProjectConfig(conf['name'])
143 for pipeline in layout.pipelines.values():
144 conf_pipeline = conf.get(pipeline.name)
145 if not conf_pipeline:
146 continue
147 project_pipeline = model.ProjectPipelineConfig()
148 project_template.pipelines[pipeline.name] = project_pipeline
149 project_pipeline.queue_name = conf.get('queue')
150 project_pipeline.job_tree = ProjectTemplateParser._parseJobTree(
151 layout, conf_pipeline.get('jobs'))
152 return project_template
153
154 @staticmethod
155 def _parseJobTree(layout, conf, tree=None):
156 if not tree:
157 tree = model.JobTree(None)
158 for conf_job in conf:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +1000159 if isinstance(conf_job, six.string_types):
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700160 tree.addJob(model.Job(conf_job))
James E. Blairb97ed802015-12-21 15:55:35 -0800161 elif isinstance(conf_job, dict):
162 # A dictionary in a job tree may override params, or
163 # be the root of a sub job tree, or both.
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700164 jobname, attrs = conf_job.items()[0]
165 jobs = attrs.pop('jobs', None)
James E. Blairb97ed802015-12-21 15:55:35 -0800166 if attrs:
167 # We are overriding params, so make a new job def
168 attrs['name'] = jobname
169 subtree = tree.addJob(JobParser.fromYaml(layout, attrs))
170 else:
171 # Not overriding, so get existing job
172 subtree = tree.addJob(layout.getJob(jobname))
173
174 if jobs:
175 # This is the root of a sub tree
176 ProjectTemplateParser._parseJobTree(layout, jobs, subtree)
177 else:
178 raise Exception("Job must be a string or dictionary")
179 return tree
180
181
182class ProjectParser(object):
183 log = logging.getLogger("zuul.ProjectParser")
184
185 @staticmethod
186 def getSchema(layout):
187 project = {vs.Required('name'): str,
188 'templates': [str]}
189 for p in layout.pipelines.values():
190 project[p.name] = {'queue': str,
191 'jobs': [vs.Any(str, dict)]}
192 return vs.Schema(project)
193
194 @staticmethod
195 def fromYaml(layout, conf):
196 ProjectParser.getSchema(layout)(conf)
197 conf_templates = conf.pop('templates', [])
198 # The way we construct a project definition is by parsing the
199 # definition as a template, then applying all of the
200 # templates, including the newly parsed one, in order.
201 project_template = ProjectTemplateParser.fromYaml(layout, conf)
202 configs = [layout.project_templates[name] for name in conf_templates]
203 configs.append(project_template)
204 project = model.ProjectConfig(conf['name'])
205 for pipeline in layout.pipelines.values():
206 project_pipeline = model.ProjectPipelineConfig()
207 project_pipeline.job_tree = model.JobTree(None)
208 queue_name = None
209 # For every template, iterate over the job tree and replace or
210 # create the jobs in the final definition as needed.
211 pipeline_defined = False
212 for template in configs:
213 ProjectParser.log.debug("Applying template %s to pipeline %s" %
214 (template.name, pipeline.name))
215 if pipeline.name in template.pipelines:
216 pipeline_defined = True
217 template_pipeline = template.pipelines[pipeline.name]
218 project_pipeline.job_tree.inheritFrom(
219 template_pipeline.job_tree)
220 if template_pipeline.queue_name:
221 queue_name = template_pipeline.queue_name
222 if queue_name:
223 project_pipeline.queue_name = queue_name
224 if pipeline_defined:
225 project.pipelines[pipeline.name] = project_pipeline
226 return project
227
228
James E. Blairfb610ce2015-12-22 10:24:40 -0800229class PipelineParser(object):
230 log = logging.getLogger("zuul.PipelineParser")
231
232 # A set of reporter configuration keys to action mapping
233 reporter_actions = {
234 'start': 'start_actions',
235 'success': 'success_actions',
236 'failure': 'failure_actions',
237 'merge-failure': 'merge_failure_actions',
238 'disabled': 'disabled_actions',
239 }
240
241 @staticmethod
242 def getDriverSchema(dtype, connections):
243 # TODO(jhesketh): Make the driver discovery dynamic
244 connection_drivers = {
245 'trigger': {
246 'gerrit': 'zuul.trigger.gerrit',
247 },
248 'reporter': {
249 'gerrit': 'zuul.reporter.gerrit',
250 'smtp': 'zuul.reporter.smtp',
251 },
252 }
253 standard_drivers = {
254 'trigger': {
255 'timer': 'zuul.trigger.timer',
256 'zuul': 'zuul.trigger.zuultrigger',
257 }
258 }
259
260 schema = {}
261 # Add the configured connections as available layout options
262 for connection_name, connection in connections.connections.items():
263 for dname, dmod in connection_drivers.get(dtype, {}).items():
264 if connection.driver_name == dname:
265 schema[connection_name] = to_list(__import__(
266 connection_drivers[dtype][dname],
267 fromlist=['']).getSchema())
268
269 # Standard drivers are always available and don't require a unique
270 # (connection) name
271 for dname, dmod in standard_drivers.get(dtype, {}).items():
272 schema[dname] = to_list(__import__(
273 standard_drivers[dtype][dname], fromlist=['']).getSchema())
274
275 return schema
276
277 @staticmethod
278 def getSchema(layout, connections):
279 manager = vs.Any('independent',
280 'dependent')
281
282 precedence = vs.Any('normal', 'low', 'high')
283
284 approval = vs.Schema({'username': str,
285 'email-filter': str,
286 'email': str,
287 'older-than': str,
288 'newer-than': str,
289 }, extra=True)
290
291 require = {'approval': to_list(approval),
292 'open': bool,
293 'current-patchset': bool,
294 'status': to_list(str)}
295
296 reject = {'approval': to_list(approval)}
297
298 window = vs.All(int, vs.Range(min=0))
299 window_floor = vs.All(int, vs.Range(min=1))
300 window_type = vs.Any('linear', 'exponential')
301 window_factor = vs.All(int, vs.Range(min=1))
302
303 pipeline = {vs.Required('name'): str,
304 vs.Required('manager'): manager,
305 'source': str,
306 'precedence': precedence,
307 'description': str,
308 'require': require,
309 'reject': reject,
310 'success-message': str,
311 'failure-message': str,
312 'merge-failure-message': str,
313 'footer-message': str,
314 'dequeue-on-new-patchset': bool,
315 'ignore-dependencies': bool,
316 'disable-after-consecutive-failures':
317 vs.All(int, vs.Range(min=1)),
318 'window': window,
319 'window-floor': window_floor,
320 'window-increase-type': window_type,
321 'window-increase-factor': window_factor,
322 'window-decrease-type': window_type,
323 'window-decrease-factor': window_factor,
324 }
325 pipeline['trigger'] = vs.Required(
326 PipelineParser.getDriverSchema('trigger', connections))
327 for action in ['start', 'success', 'failure', 'merge-failure',
328 'disabled']:
329 pipeline[action] = PipelineParser.getDriverSchema('reporter',
330 connections)
331 return vs.Schema(pipeline)
332
333 @staticmethod
334 def fromYaml(layout, connections, scheduler, conf):
335 PipelineParser.getSchema(layout, connections)(conf)
336 pipeline = model.Pipeline(conf['name'], layout)
337 pipeline.description = conf.get('description')
338
339 pipeline.source = connections.getSource(conf['source'])
340
341 precedence = model.PRECEDENCE_MAP[conf.get('precedence')]
342 pipeline.precedence = precedence
343 pipeline.failure_message = conf.get('failure-message',
344 "Build failed.")
345 pipeline.merge_failure_message = conf.get(
346 'merge-failure-message', "Merge Failed.\n\nThis change or one "
347 "of its cross-repo dependencies was unable to be "
348 "automatically merged with the current state of its "
349 "repository. Please rebase the change and upload a new "
350 "patchset.")
351 pipeline.success_message = conf.get('success-message',
352 "Build succeeded.")
353 pipeline.footer_message = conf.get('footer-message', "")
James E. Blair60af7f42016-03-11 16:11:06 -0800354 pipeline.start_message = conf.get('start-message',
355 "Starting {pipeline.name} jobs.")
James E. Blairfb610ce2015-12-22 10:24:40 -0800356 pipeline.dequeue_on_new_patchset = conf.get(
357 'dequeue-on-new-patchset', True)
358 pipeline.ignore_dependencies = conf.get(
359 'ignore-dependencies', False)
360
361 for conf_key, action in PipelineParser.reporter_actions.items():
362 reporter_set = []
363 if conf.get(conf_key):
364 for reporter_name, params \
365 in conf.get(conf_key).items():
366 reporter = connections.getReporter(reporter_name,
367 params)
368 reporter.setAction(conf_key)
369 reporter_set.append(reporter)
370 setattr(pipeline, action, reporter_set)
371
372 # If merge-failure actions aren't explicit, use the failure actions
373 if not pipeline.merge_failure_actions:
374 pipeline.merge_failure_actions = pipeline.failure_actions
375
376 pipeline.disable_at = conf.get(
377 'disable-after-consecutive-failures', None)
378
379 pipeline.window = conf.get('window', 20)
380 pipeline.window_floor = conf.get('window-floor', 3)
381 pipeline.window_increase_type = conf.get(
382 'window-increase-type', 'linear')
383 pipeline.window_increase_factor = conf.get(
384 'window-increase-factor', 1)
385 pipeline.window_decrease_type = conf.get(
386 'window-decrease-type', 'exponential')
387 pipeline.window_decrease_factor = conf.get(
388 'window-decrease-factor', 2)
389
390 manager_name = conf['manager']
391 if manager_name == 'dependent':
392 manager = zuul.manager.dependent.DependentPipelineManager(
393 scheduler, pipeline)
394 elif manager_name == 'independent':
395 manager = zuul.manager.independent.IndependentPipelineManager(
396 scheduler, pipeline)
397
398 pipeline.setManager(manager)
399 layout.pipelines[conf['name']] = pipeline
400
401 if 'require' in conf or 'reject' in conf:
402 require = conf.get('require', {})
403 reject = conf.get('reject', {})
404 f = model.ChangeishFilter(
405 open=require.get('open'),
406 current_patchset=require.get('current-patchset'),
407 statuses=to_list(require.get('status')),
408 required_approvals=to_list(require.get('approval')),
409 reject_approvals=to_list(reject.get('approval'))
410 )
411 manager.changeish_filters.append(f)
412
413 for trigger_name, trigger_config\
414 in conf.get('trigger').items():
415 trigger = connections.getTrigger(trigger_name, trigger_config)
416 pipeline.triggers.append(trigger)
417
418 # TODO: move
419 manager.event_filters += trigger.getEventFilters(
420 conf['trigger'][trigger_name])
421
422 return pipeline
423
424
James E. Blaird8e778f2015-12-22 14:09:20 -0800425class TenantParser(object):
426 log = logging.getLogger("zuul.TenantParser")
427
James E. Blair96c6bf82016-01-15 16:20:40 -0800428 tenant_source = vs.Schema({'config-repos': [str],
429 'project-repos': [str]})
James E. Blair83005782015-12-11 14:46:03 -0800430
James E. Blaird8e778f2015-12-22 14:09:20 -0800431 @staticmethod
432 def validateTenantSources(connections):
James E. Blair83005782015-12-11 14:46:03 -0800433 def v(value, path=[]):
434 if isinstance(value, dict):
435 for k, val in value.items():
436 connections.getSource(k)
James E. Blaird8e778f2015-12-22 14:09:20 -0800437 TenantParser.validateTenantSource(val, path + [k])
James E. Blair83005782015-12-11 14:46:03 -0800438 else:
439 raise vs.Invalid("Invalid tenant source", path)
440 return v
441
James E. Blaird8e778f2015-12-22 14:09:20 -0800442 @staticmethod
443 def validateTenantSource(value, path=[]):
444 TenantParser.tenant_source(value)
James E. Blair83005782015-12-11 14:46:03 -0800445
James E. Blaird8e778f2015-12-22 14:09:20 -0800446 @staticmethod
447 def getSchema(connections=None):
James E. Blair83005782015-12-11 14:46:03 -0800448 tenant = {vs.Required('name'): str,
James E. Blaird8e778f2015-12-22 14:09:20 -0800449 'source': TenantParser.validateTenantSources(connections)}
450 return vs.Schema(tenant)
James E. Blair83005782015-12-11 14:46:03 -0800451
James E. Blaird8e778f2015-12-22 14:09:20 -0800452 @staticmethod
453 def fromYaml(base, connections, scheduler, merger, conf):
454 TenantParser.getSchema(connections)(conf)
455 tenant = model.Tenant(conf['name'])
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700456 unparsed_config = model.UnparsedTenantConfig()
457 tenant.config_repos, tenant.project_repos = \
458 TenantParser._loadTenantConfigRepos(connections, conf)
459 tenant.config_repos_config, tenant.project_repos_config = \
460 TenantParser._loadTenantInRepoLayouts(
461 merger, connections, tenant.config_repos, tenant.project_repos)
462 unparsed_config.extend(tenant.config_repos_config)
463 unparsed_config.extend(tenant.project_repos_config)
464 tenant.layout = TenantParser._parseLayout(base, unparsed_config,
James E. Blaird8e778f2015-12-22 14:09:20 -0800465 scheduler, connections)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700466 tenant.layout.tenant = tenant
James E. Blaird8e778f2015-12-22 14:09:20 -0800467 return tenant
James E. Blair83005782015-12-11 14:46:03 -0800468
James E. Blaird8e778f2015-12-22 14:09:20 -0800469 @staticmethod
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700470 def _loadTenantConfigRepos(connections, conf_tenant):
471 config_repos = []
472 project_repos = []
473
James E. Blaird8e778f2015-12-22 14:09:20 -0800474 for source_name, conf_source in conf_tenant.get('source', {}).items():
475 source = connections.getSource(source_name)
James E. Blair96c6bf82016-01-15 16:20:40 -0800476
James E. Blair96c6bf82016-01-15 16:20:40 -0800477 for conf_repo in conf_source.get('config-repos', []):
478 project = source.getProject(conf_repo)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700479 config_repos.append((source, project))
James E. Blair96c6bf82016-01-15 16:20:40 -0800480
James E. Blair96c6bf82016-01-15 16:20:40 -0800481 for conf_repo in conf_source.get('project-repos', []):
James E. Blaird8e778f2015-12-22 14:09:20 -0800482 project = source.getProject(conf_repo)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700483 project_repos.append((source, project))
484
485 return config_repos, project_repos
486
487 @staticmethod
488 def _loadTenantInRepoLayouts(merger, connections, config_repos,
489 project_repos):
490 config_repos_config = model.UnparsedTenantConfig()
491 project_repos_config = model.UnparsedTenantConfig()
492 jobs = []
493
494 for (source, project) in config_repos:
495 # Get main config files. These files are permitted the
496 # full range of configuration.
497 url = source.getGitUrl(project)
498 job = merger.getFiles(project.name, url, 'master',
499 files=['zuul.yaml', '.zuul.yaml'])
500 job.project = project
501 job.config_repo = True
502 jobs.append(job)
503
504 for (source, project) in project_repos:
505 # Get in-project-repo config files which have a restricted
506 # set of options.
507 url = source.getGitUrl(project)
508 # TODOv3(jeblair): config should be branch specific
509 job = merger.getFiles(project.name, url, 'master',
510 files=['.zuul.yaml'])
511 job.project = project
512 job.config_repo = False
513 jobs.append(job)
James E. Blair96c6bf82016-01-15 16:20:40 -0800514
James E. Blaird8e778f2015-12-22 14:09:20 -0800515 for job in jobs:
James E. Blair96c6bf82016-01-15 16:20:40 -0800516 # Note: this is an ordered list -- we wait for cat jobs to
517 # complete in the order they were launched which is the
518 # same order they were defined in the main config file.
James E. Blair8d692392016-04-08 17:47:58 -0700519 # This is important for correct inheritance.
James E. Blaird8e778f2015-12-22 14:09:20 -0800520 TenantParser.log.debug("Waiting for cat job %s" % (job,))
521 job.wait()
James E. Blair96c6bf82016-01-15 16:20:40 -0800522 for fn in ['zuul.yaml', '.zuul.yaml']:
523 if job.files.get(fn):
524 TenantParser.log.info(
525 "Loading configuration from %s/%s" %
526 (job.project, fn))
527 if job.config_repo:
528 incdata = TenantParser._parseConfigRepoLayout(
James E. Blair4317e9f2016-07-15 10:05:47 -0700529 job.files[fn], job.project)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700530 config_repos_config.extend(incdata)
James E. Blair96c6bf82016-01-15 16:20:40 -0800531 else:
532 incdata = TenantParser._parseProjectRepoLayout(
James E. Blair4317e9f2016-07-15 10:05:47 -0700533 job.files[fn], job.project)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700534 project_repos_config.extend(incdata)
535 job.project.unparsed_config = incdata
536 return config_repos_config, project_repos_config
James E. Blair83005782015-12-11 14:46:03 -0800537
James E. Blaird8e778f2015-12-22 14:09:20 -0800538 @staticmethod
James E. Blair4317e9f2016-07-15 10:05:47 -0700539 def _parseConfigRepoLayout(data, project):
James E. Blair96c6bf82016-01-15 16:20:40 -0800540 # This is the top-level configuration for a tenant.
541 config = model.UnparsedTenantConfig()
James E. Blair4317e9f2016-07-15 10:05:47 -0700542 config.extend(yaml.load(data), project)
James E. Blair96c6bf82016-01-15 16:20:40 -0800543
James E. Blair96c6bf82016-01-15 16:20:40 -0800544 return config
545
546 @staticmethod
James E. Blair4317e9f2016-07-15 10:05:47 -0700547 def _parseProjectRepoLayout(data, project):
James E. Blaird8e778f2015-12-22 14:09:20 -0800548 # TODOv3(jeblair): this should implement some rules to protect
549 # aspects of the config that should not be changed in-repo
James E. Blair96c6bf82016-01-15 16:20:40 -0800550 config = model.UnparsedTenantConfig()
James E. Blair4317e9f2016-07-15 10:05:47 -0700551 config.extend(yaml.load(data), project)
James E. Blair96c6bf82016-01-15 16:20:40 -0800552
James E. Blair96c6bf82016-01-15 16:20:40 -0800553 return config
James E. Blaird8e778f2015-12-22 14:09:20 -0800554
555 @staticmethod
556 def _parseLayout(base, data, scheduler, connections):
557 layout = model.Layout()
558
559 for config_pipeline in data.pipelines:
560 layout.addPipeline(PipelineParser.fromYaml(layout, connections,
561 scheduler,
562 config_pipeline))
563
564 for config_job in data.jobs:
565 layout.addJob(JobParser.fromYaml(layout, config_job))
566
567 for config_template in data.project_templates:
568 layout.addProjectTemplate(ProjectTemplateParser.fromYaml(
569 layout, config_template))
570
571 for config_project in data.projects:
572 layout.addProjectConfig(ProjectParser.fromYaml(
573 layout, config_project))
574
575 for pipeline in layout.pipelines.values():
576 pipeline.manager._postConfig(layout)
577
578 return layout
James E. Blair83005782015-12-11 14:46:03 -0800579
580
581class ConfigLoader(object):
582 log = logging.getLogger("zuul.ConfigLoader")
583
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700584 def expandConfigPath(self, config_path):
585 if config_path:
586 config_path = os.path.expanduser(config_path)
587 if not os.path.exists(config_path):
588 raise Exception("Unable to read tenant config file at %s" %
589 config_path)
590 return config_path
591
James E. Blair83005782015-12-11 14:46:03 -0800592 def loadConfig(self, config_path, scheduler, merger, connections):
593 abide = model.Abide()
594
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700595 config_path = self.expandConfigPath(config_path)
James E. Blair83005782015-12-11 14:46:03 -0800596 with open(config_path) as config_file:
597 self.log.info("Loading configuration from %s" % (config_path,))
598 data = yaml.load(config_file)
James E. Blaird8e778f2015-12-22 14:09:20 -0800599 config = model.UnparsedAbideConfig()
600 config.extend(data)
James E. Blair83005782015-12-11 14:46:03 -0800601 base = os.path.dirname(os.path.realpath(config_path))
602
James E. Blaird8e778f2015-12-22 14:09:20 -0800603 for conf_tenant in config.tenants:
604 tenant = TenantParser.fromYaml(base, connections, scheduler,
605 merger, conf_tenant)
James E. Blair83005782015-12-11 14:46:03 -0800606 abide.tenants[tenant.name] = tenant
James E. Blair83005782015-12-11 14:46:03 -0800607 return abide
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700608
609 def createDynamicLayout(self, tenant, files):
610 config = tenant.config_repos_config.copy()
611 for source, project in tenant.project_repos:
612 # TODOv3(jeblair): config should be branch specific
613 data = files.getFile(project.name, 'master', '.zuul.yaml')
614 if not data:
615 data = project.unparsed_config
616 if not data:
617 continue
James E. Blair4317e9f2016-07-15 10:05:47 -0700618 incdata = TenantParser._parseProjectRepoLayout(data, project)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700619 config.extend(incdata)
620
621 layout = model.Layout()
622 # TODOv3(jeblair): copying the pipelines could be dangerous/confusing.
623 layout.pipelines = tenant.layout.pipelines
624
625 for config_job in config.jobs:
626 layout.addJob(JobParser.fromYaml(layout, config_job))
627
628 for config_template in config.project_templates:
629 layout.addProjectTemplate(ProjectTemplateParser.fromYaml(
630 layout, config_template))
631
632 for config_project in config.projects:
633 layout.addProjectConfig(ProjectParser.fromYaml(
634 layout, config_project), update_pipeline=False)
635
636 return layout