diff --git a/jenkins_jobs/formatter.py b/jenkins_jobs/formatter.py
index 075e5a25c..9f47a71b2 100644
--- a/jenkins_jobs/formatter.py
+++ b/jenkins_jobs/formatter.py
@@ -82,9 +82,12 @@ def deep_format(obj, paramdict, allow_empty=False):
else:
ret = obj
if isinstance(ret, CustomLoader):
- # If we have a CustomLoader here, we've lazily-loaded a template;
+ # If we have a CustomLoader here, we've lazily-loaded a template
+ # or rendered a template to a piece of YAML;
# attempt to format it.
- ret = deep_format(ret, paramdict, allow_empty=allow_empty)
+ ret = deep_format(
+ ret.get_object_to_format(), paramdict, allow_empty=allow_empty
+ )
return ret
diff --git a/jenkins_jobs/local_yaml.py b/jenkins_jobs/local_yaml.py
index 9aeeb9afc..cece2eadb 100644
--- a/jenkins_jobs/local_yaml.py
+++ b/jenkins_jobs/local_yaml.py
@@ -196,6 +196,24 @@ construct.
Examples:
.. literalinclude:: /../../tests/yamlparser/fixtures/jinja-string01.yaml
+
+The tag ``!j2-yaml:`` is similar to the ``!j2:`` tag, just that it loads the
+Jinja-rendered string as YAML and embeds it in the calling YAML construct. This
+provides a very flexible and convenient way of generating pieces of YAML
+structures. One of use cases is defining complex YAML structures with much
+simpler configuration, without any duplication.
+
+Examples:
+
+ .. literalinclude:: /../../tests/yamlparser/fixtures/jinja-yaml01.yaml
+
+Another use case is controlling lists dynamically, like conditionally adding
+list elements based on project configuration.
+
+Examples:
+
+ .. literalinclude:: /../../tests/yamlparser/fixtures/jinja-yaml02.yaml
+
"""
import functools
@@ -369,6 +387,14 @@ class BaseYAMLObject(YAMLObject):
yaml_dumper = LocalDumper
+class J2Yaml(BaseYAMLObject):
+ yaml_tag = u"!j2-yaml:"
+
+ @classmethod
+ def from_yaml(cls, loader, node):
+ return Jinja2YamlLoader(node.value, loader.search_path)
+
+
class J2String(BaseYAMLObject):
yaml_tag = u"!j2:"
@@ -572,6 +598,26 @@ class Jinja2Loader(CustomLoader):
self._template.environment.loader = self._loader
return self._template.render(kwargs)
+ def get_object_to_format(self):
+ return self
+
+
+class LateYamlLoader(CustomLoader):
+ """A loader for data rendered via Jinja2, to be loaded as YAML and then deep formatted."""
+
+ def __init__(self, yaml_str, loader):
+ self._yaml_str = yaml_str
+ self._loader = loader
+
+ def get_object_to_format(self):
+ return load(self._yaml_str, search_path=self._loader._search_path)
+
+
+class Jinja2YamlLoader(Jinja2Loader):
+ def format(self, **kwargs):
+ yaml_str = super(Jinja2YamlLoader, self).format(**kwargs)
+ return LateYamlLoader(yaml_str, self)
+
class CustomLoaderCollection(object):
"""Helper class to format a collection of CustomLoader objects"""
diff --git a/tests/yamlparser/fixtures/jinja-yaml01.xml b/tests/yamlparser/fixtures/jinja-yaml01.xml
new file mode 100644
index 000000000..ec26a8078
--- /dev/null
+++ b/tests/yamlparser/fixtures/jinja-yaml01.xml
@@ -0,0 +1,72 @@
+
+
+
+ <!-- Managed by Jenkins Job Builder -->
+ false
+ false
+ false
+ false
+ true
+
+
+
+
+
+
+
+ REG_EXP
+ a|b|c
+
+
+ PLAIN
+ master
+
+
+ false
+
+
+ REG_EXP
+ d|e|f
+
+
+ PLAIN
+ stable
+
+
+ false
+
+
+
+ false
+ false
+ false
+ false
+
+ false
+ false
+ true
+
+ BASE64
+ PLAIN
+ PLAIN
+ BASE64
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+ __ANY__
+
+
+
+
+
+
diff --git a/tests/yamlparser/fixtures/jinja-yaml01.yaml b/tests/yamlparser/fixtures/jinja-yaml01.yaml
new file mode 100644
index 000000000..252c93a5f
--- /dev/null
+++ b/tests/yamlparser/fixtures/jinja-yaml01.yaml
@@ -0,0 +1,30 @@
+- job-template:
+ name: test-job-template
+ triggers:
+ - gerrit:
+ projects:
+ !j2-yaml: |
+ {% for item in triggers %}
+ - branches:
+ - branch-compare-type: PLAIN
+ branch-pattern: '{{ item.branch }}'
+ project-compare-type: REG_EXP
+ project-pattern: '{{ item.repositories|join("|") }}'
+ {% endfor %}
+
+- project:
+ name: test-job-project
+
+ jobs:
+ - test-job-template:
+ triggers:
+ - repositories:
+ - a
+ - b
+ - c
+ branch: master
+ - repositories:
+ - d
+ - e
+ - f
+ branch: stable
diff --git a/tests/yamlparser/fixtures/jinja-yaml02.xml b/tests/yamlparser/fixtures/jinja-yaml02.xml
new file mode 100644
index 000000000..4c6f687a9
--- /dev/null
+++ b/tests/yamlparser/fixtures/jinja-yaml02.xml
@@ -0,0 +1,48 @@
+
+
+
+ <!-- Managed by Jenkins Job Builder -->
+ false
+ false
+ false
+ false
+ true
+
+
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+ <!-- Managed by Jenkins Job Builder -->
+ false
+ false
+ false
+ false
+ true
+
+
+ false
+ false
+
+
+
+ 7
+ -1
+ -1
+ -1
+
+
+
+
+
+
+
+
diff --git a/tests/yamlparser/fixtures/jinja-yaml02.yaml b/tests/yamlparser/fixtures/jinja-yaml02.yaml
new file mode 100644
index 000000000..7477279c5
--- /dev/null
+++ b/tests/yamlparser/fixtures/jinja-yaml02.yaml
@@ -0,0 +1,19 @@
+- job-template:
+ name: 'test-job-{variant}'
+ properties: !j2-yaml: |
+ - rebuild
+ {% if discard_old_builds|default %}
+ - build-discarder:
+ days-to-keep: 7
+ {% endif %}
+
+- project:
+ name: test-project
+
+ jobs:
+ - 'test-job-{variant}':
+ variant: abc
+
+ - 'test-job-{variant}':
+ variant: def
+ discard_old_builds: true
diff --git a/tests/yamlparser/fixtures/jinja-yaml03.groovy b/tests/yamlparser/fixtures/jinja-yaml03.groovy
new file mode 100644
index 000000000..bc5b62c94
--- /dev/null
+++ b/tests/yamlparser/fixtures/jinja-yaml03.groovy
@@ -0,0 +1,3 @@
+if (manager.logContains(".*no_jenkins.*")) {
+ manager.build.result = hudson.model.Result.NOT_BUILT
+}
diff --git a/tests/yamlparser/fixtures/jinja-yaml03.xml b/tests/yamlparser/fixtures/jinja-yaml03.xml
new file mode 100644
index 000000000..052f28641
--- /dev/null
+++ b/tests/yamlparser/fixtures/jinja-yaml03.xml
@@ -0,0 +1,27 @@
+
+
+
+ <!-- Managed by Jenkins Job Builder -->
+ false
+ false
+ false
+ false
+ true
+
+
+
+
+
+ 0
+ false
+
+ false
+
+
+
+
+
diff --git a/tests/yamlparser/fixtures/jinja-yaml03.yaml b/tests/yamlparser/fixtures/jinja-yaml03.yaml
new file mode 100644
index 000000000..06a32b5be
--- /dev/null
+++ b/tests/yamlparser/fixtures/jinja-yaml03.yaml
@@ -0,0 +1,15 @@
+# the purpose of this test is to check if the piece of YAML generated by
+# !j2-yaml is deep-formatted properly; if not then double quotes introduced by
+# !include-raw-escape would be left untouched and passed down to the output XML
+# file, which would simply be wrong...
+
+- job-template:
+ name: 'test-job-template'
+ publishers: !j2-yaml: |
+ - groovy-postbuild:
+ script: !include-raw-escape: ./jinja-yaml03.groovy
+
+- project:
+ name: test-project
+ jobs:
+ - 'test-job-template'