Add !include-jinja2 for rendering templates with Jinja2

This template included using !include-jinja2:

"""
{{ my_var }}
"""

is rendered the same as an existing template that looks like this:

"""
{my_var}
"""

This also allows the use of Jinja2's richer syntax:

"""
{% for test_environment in configuration.get("envs", ["py35"]) %}
tox -e {{ test_environment }}
{% endfor %}
"""

Story: 2001135

Change-Id: Ia3ee21822d6e9237f5ea46796bc8810ecac61e2c
This commit is contained in:
Daniel Watkins 2017-07-31 17:46:57 -04:00
parent 2c60aff806
commit b95f194612
11 changed files with 163 additions and 5 deletions

View File

@ -21,6 +21,7 @@ import re
from string import Formatter from string import Formatter
from jenkins_jobs.errors import JenkinsJobsException from jenkins_jobs.errors import JenkinsJobsException
from jenkins_jobs.local_yaml import CustomLoader
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -68,6 +69,10 @@ def deep_format(obj, paramdict, allow_empty=False):
raise raise
else: else:
ret = obj ret = obj
if isinstance(ret, CustomLoader):
# If we have a CustomLoader here, we've lazily-loaded a template;
# attempt to format it.
ret = deep_format(ret, paramdict, allow_empty=allow_empty)
return ret return ret

View File

@ -127,6 +127,20 @@ Example:
on any filename passed via ``!include-raw-escape:`` the tag will be on any filename passed via ``!include-raw-escape:`` the tag will be
automatically converted to ``!include-raw:`` and no escaping will be automatically converted to ``!include-raw:`` and no escaping will be
performed. performed.
The tag ``!include-jinja2:`` will treat the given string or list of strings as
filenames to be opened as Jinja2 templates, which should be rendered to a
string and included in the calling YAML construct. (This is analogous to the
templating that will happen with ``!include-raw``.)
Examples:
.. literalinclude:: /../../tests/yamlparser/fixtures/jinja01.yaml
contents of jinja01.yaml.inc:
.. literalinclude:: /../../tests/yamlparser/fixtures/jinja01.yaml.inc
""" """
import functools import functools
@ -135,6 +149,7 @@ import logging
import os import os
import re import re
import jinja2
import yaml import yaml
from yaml.constructor import BaseConstructor from yaml.constructor import BaseConstructor
from yaml.representer import BaseRepresenter from yaml.representer import BaseRepresenter
@ -349,8 +364,8 @@ class YamlInclude(BaseYAMLObject):
elif isinstance(node, yaml.SequenceNode): elif isinstance(node, yaml.SequenceNode):
contents = [cls._from_file(loader, scalar_node) contents = [cls._from_file(loader, scalar_node)
for scalar_node in node.value] for scalar_node in node.value]
if any(isinstance(s, LazyLoader) for s in contents): if any(isinstance(s, CustomLoader) for s in contents):
return LazyLoaderCollection(contents) return CustomLoaderCollection(contents)
return u'\n'.join(contents) return u'\n'.join(contents)
else: else:
@ -383,6 +398,17 @@ class YamlIncludeRawEscape(YamlIncludeRaw):
return loader.escape_callback(data) return loader.escape_callback(data)
class YamlIncludeJinja2(YamlIncludeRaw):
yaml_tag = u'!include-jinja2:'
@classmethod
def _from_file(cls, loader, node):
contents = cls._open_file(loader, node)
if isinstance(contents, LazyLoader):
return contents
return Jinja2Loader(contents)
class DeprecatedTag(BaseYAMLObject): class DeprecatedTag(BaseYAMLObject):
@classmethod @classmethod
@ -407,8 +433,22 @@ class YamlIncludeRawEscapeDeprecated(DeprecatedTag):
_new = YamlIncludeRawEscape _new = YamlIncludeRawEscape
class LazyLoaderCollection(object): class CustomLoader(object):
"""Helper class to format a collection of LazyLoader objects""" """Parent class for non-standard loaders."""
class Jinja2Loader(CustomLoader):
"""A loader for Jinja2-templated files."""
def __init__(self, contents):
self._template = jinja2.Template(contents)
self._template.environment.undefined = jinja2.StrictUndefined
def format(self, **kwargs):
return self._template.render(kwargs)
class CustomLoaderCollection(object):
"""Helper class to format a collection of CustomLoader objects"""
def __init__(self, sequence): def __init__(self, sequence):
self._data = sequence self._data = sequence
@ -416,7 +456,7 @@ class LazyLoaderCollection(object):
return u'\n'.join(item.format(*args, **kwargs) for item in self._data) return u'\n'.join(item.format(*args, **kwargs) for item in self._data)
class LazyLoader(object): class LazyLoader(CustomLoader):
"""Helper class to provide lazy loading of files included using !include* """Helper class to provide lazy loading of files included using !include*
tags where the path to the given file contains unresolved placeholders. tags where the path to the given file contains unresolved placeholders.
""" """

View File

@ -7,3 +7,4 @@ pbr>=1.8 # Apache-2.0
stevedore>=1.17.1 # Apache-2.0 stevedore>=1.17.1 # Apache-2.0
python-jenkins>=0.4.8 python-jenkins>=0.4.8
fasteners fasteners
Jinja2

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<project>
<actions/>
<description>&lt;!-- Managed by Jenkins Job Builder --&gt;</description>
<keepDependencies>false</keepDependencies>
<blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
<blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
<concurrentBuild>false</concurrentBuild>
<canRoam>true</canRoam>
<properties/>
<scm class="hudson.scm.NullSCM"/>
<builders>
<hudson.tasks.Shell>
<command>test variable
a
b
c
</command>
</hudson.tasks.Shell>
</builders>
<publishers/>
<buildWrappers/>
</project>

View File

@ -0,0 +1,15 @@
- builder:
name: test-builder
builders:
- shell:
!include-jinja2: jinja01.yaml.inc
- job:
name: test-job
builders:
- test-builder:
var: "test variable"
test_list:
- a
- b
- c

View File

@ -0,0 +1,4 @@
{{ var }}
{% for item in test_list -%}
{{ item }}
{% endfor %}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<project>
<actions/>
<description>&lt;!-- Managed by Jenkins Job Builder --&gt;</description>
<keepDependencies>false</keepDependencies>
<blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
<blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
<concurrentBuild>false</concurrentBuild>
<canRoam>true</canRoam>
<properties/>
<scm class="hudson.scm.NullSCM"/>
<builders>
<hudson.tasks.Shell>
<command>1</command>
</hudson.tasks.Shell>
</builders>
<publishers/>
<buildWrappers/>
</project>

View File

@ -0,0 +1,11 @@
- job-template:
name: 'test-job-{num}'
builders:
- shell:
!include-jinja2: jinja02.yaml.inc
- project:
name: test-job-template-1
num: 1
jobs:
- 'test-job-{num}'

View File

@ -0,0 +1 @@
{{ num }}

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<project>
<actions/>
<description>&lt;!-- Managed by Jenkins Job Builder --&gt;</description>
<keepDependencies>false</keepDependencies>
<blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
<blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
<concurrentBuild>false</concurrentBuild>
<canRoam>true</canRoam>
<properties/>
<scm class="hudson.scm.NullSCM"/>
<builders>
<hudson.tasks.Shell>
<command>test variable
a
b
c
</command>
</hudson.tasks.Shell>
</builders>
<publishers/>
<buildWrappers/>
</project>

View File

@ -0,0 +1,16 @@
- builder:
name: test-builder
builders:
- shell:
!include-jinja2: jinja{include-number}.yaml.inc
- job:
name: test-job
builders:
- test-builder:
var: "test variable"
test_list:
- a
- b
- c
include-number: "01"