diff --git a/doc/source/definition.rst b/doc/source/definition.rst index 2361a504e..83c975ae9 100644 --- a/doc/source/definition.rst +++ b/doc/source/definition.rst @@ -201,6 +201,33 @@ create a view, you must define a view in a YAML file and have a variable called Views are processed differently than Jobs and therefore will not work within a `Project`_ or a `Job Template`_. +.. _view-template: + +View Template +^^^^^^^^^^^^^ + +Allow views to also be configured via templates similar to job-templates. This +is useful when you have multiple views defined that have similar configuration +except for a few variables. View Templates can be passed variables to fill in +sections automatically via a project configuration using the new 'views' key. + +Minimal Example:: + + - view-template: + name: '{name}-template-{seq}' + description: 'testing view templates feature' + view-type: list + regex: 'test-view-.*' + + - project: + name: 'test-view' + views: + - '{name}-template-{seq}' + seq: + - a + - b + - c + .. _macro: Macro diff --git a/jenkins_jobs/parser.py b/jenkins_jobs/parser.py index 656af17b9..69f1115ad 100644 --- a/jenkins_jobs/parser.py +++ b/jenkins_jobs/parser.py @@ -316,6 +316,69 @@ class YamlParser(object): raise JenkinsJobsException("Failed to find suitable " "template named '{0}'" .format(jobname)) + + for viewspec in project.get('views', []): + if isinstance(viewspec, dict): + # Singleton dict containing dict of view-specific params + viewname, viewparams = next(iter(viewspec.items())) + if not isinstance(viewparams, dict): + viewparams = {} + else: + viewname = viewspec + viewparams = {} + view = self._getView(viewname) + if view: + # Just naming an existing defined view + if viewname in seen: + self._handle_dups("Duplicate view '{0}' specified " + "for project '{1}'" + .format(viewname, project['name'])) + seen.add(viewname) + continue + # see if it's a view group + group = self._getViewGroup(viewname) + if group: + for group_viewspec in group['views']: + if isinstance(group_viewspec, dict): + group_viewname, group_viewparams = \ + next(iter(group_viewspec.items())) + if not isinstance(group_viewparams, dict): + group_viewparams = {} + else: + group_viewname = group_viewspec + group_viewparams = {} + view = self._getView(group_viewname) + if view: + if group_viewname in seen: + self._handle_dups( + "Duplicate view '{0}' specified for " + "project '{1}'".format(group_viewname, + project['name'])) + seen.add(group_viewname) + continue + template = self._getViewTemplate(group_viewname) + # Allow a group to override parameters set by a project + d = type(project)(project) + d.update(viewparams) + d.update(group) + d.update(group_viewparams) + # Except name, since the group's name is not useful + d['name'] = project['name'] + if template: + self._expandYamlForTemplateView( + d, template, jobs_glob) + continue + # see if it's a template + template = self._getViewTemplate(viewname) + if template: + d = type(project)(project) + d.update(viewparams) + self._expandYamlForTemplateView(d, template, jobs_glob) + else: + raise JenkinsJobsException("Failed to find suitable " + "template named '{0}'" + .format(viewname)) + # check for duplicate generated jobs seen = set() # walk the list in reverse so that last definition wins @@ -325,6 +388,17 @@ class YamlParser(object): "specified".format(job['name'])) self.jobs.remove(job) seen.add(job['name']) + + # check for duplicate generated views + seen_views = set() + # walk the list in reverse so that last definition wins + for view in self.views[::-1]: + if view['name'] in seen_views: + self._handle_dups("Duplicate definitions for view '{0}' " + "specified".format(view['name'])) + self.views.remove(view) + seen_views.add(view['name']) + return self.jobs, self.views def _expandYamlForTemplateJob(self, project, template, jobs_glob=None): @@ -411,3 +485,73 @@ class YamlParser(object): # The \n\n is not hard coded, because they get stripped if the # project does not otherwise have a description. return "\n\n" + MAGIC_MANAGE_STRING + + # Views related + def _getView(self, name): + view = self.data.get('view', {}).get(name, None) + if not view: + return view + return self._applyDefaults(view) + + def _getViewGroup(self, name): + return self.data.get('view-group', {}).get(name, None) + + def _getViewTemplate(self, name): + print(name) + view = self.data.get('view-template', {}).get(name, None) + if not view: + return view + return self._applyDefaults(view) + + def _expandYamlForTemplateView(self, project, template, views_glob=None): + dimensions = [] + template_name = template['name'] + # reject keys that are not useful during yaml expansion + for k in ['views']: + project.pop(k) + excludes = project.pop('exclude', []) + for (k, v) in project.items(): + tmpk = '{{{0}}}'.format(k) + if tmpk not in template_name: + continue + if type(v) == list: + dimensions.append(zip([k] * len(v), v)) + # XXX somewhat hackish to ensure we actually have a single + # pass through the loop + if len(dimensions) == 0: + dimensions = [(("", ""),)] + + for values in itertools.product(*dimensions): + params = copy.deepcopy(project) + params = self._applyDefaults(params, template) + + expanded_values = {} + for (k, v) in values: + if isinstance(v, dict): + inner_key = next(iter(v)) + expanded_values[k] = inner_key + expanded_values.update(v[inner_key]) + else: + expanded_values[k] = v + + params.update(expanded_values) + params = deep_format(params, params) + if combination_matches(params, excludes): + logger.debug('Excluding combination %s', str(params)) + continue + + for key in template.keys(): + if key not in params: + params[key] = template[key] + + params['template-name'] = template_name + expanded = deep_format( + template, params, + self.jjb_config.yamlparser['allow_empty_variables']) + + view_name = expanded.get('name') + if views_glob and not matches(view_name, views_glob): + continue + + self._formatDescription(expanded) + self.views.append(expanded)