Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Template module

The module supports the following template engines:

  • MiniJinja (which is based on Jinja2)
  • Mustache
  • Jinja2 (available only on Linux)

Minijinja is the default and preferred engine, Jinja2 and Mustache are provided for backward compatibility. The Minijinja and Mustache engines are native and fast, Jinja2 is very slow, requires a Python interpreter, but allows dynamic extensions.

The module takes the following arguments:

NameDescriptionPossible values
pathOutput file pathA path on the filesystem
template_pathSource template path (mutually exclusive with template_src)A path on the filesystem that points to a template to be rendered
template_srcInlined source template (mutually exclusive with template_path)A template in the form of a string (for example: {{%-top-}})
engineTemplate enginemustache, minijinja, jinja2 (default: minijinja)
dataJSON data used for templatingA valid JSON
show_contentControls output of diffs in the reporttrue/false (default: true)
modeTrust level for the template contentsandboxed, unrestricted (default: sandboxed)

Security

Engine sandboxing

The mode argument sets the trust level for the template content:

  • sandboxed (the default) restricts what a template can do — no reading of arbitrary files, no cross-file includes, and bounded compute — to limit the impact of rendering untrusted content.
  • unrestricted lifts those restrictions for trusted templates that need them (reading files, cross-file includes, or Jinja2 dynamic features).

Set mode: unrestricted only for templates you trust. Note that sandboxing limits what a template can do, not what it can see.

Template data and the agent state

⚠️ When `data` is empty, the `file_from_template_*` methods pass the **entire agent datastate** to the template.

By default these methods render with no explicit data, in which case the whole agent internal state — every variable and class of the current run, across all directives, including secrets stored in variables and node properties — is handed to the template as data. A sandboxed template cannot touch the filesystem, but it can still read that entire state and write it out.

So mode: sandboxed alone does not make it safe to render a fully untrusted template. To render untrusted content safely, also pass an explicit, minimal data object so the template only ever sees what you intend — never the implicit datastate.

Reporting and sensitive content

⚠️ With show_content: true (the default), the rendered file content is included in the agent report.

The diff (and, for a new file, the full content) is sent to the server and stored as compliance data. If the template renders secrets — values from data, or a file read with lookup('file', …) in unrestricted mode — those secrets end up in the reports.

Set show_content: false when rendering files that contain sensitive data (passwords, private keys, tokens, …).

CLI

The module provides a CLI to help debug the rendering and audit of templates.

$ /opt/rudder/bin/rudder-module-template --help
Usage: rudder-module-template [OPTIONS] --template <TEMPLATE> --data <DATA> --out <OUT>

Options:
  -e, --engine <ENGINE>      Template engine [default: minijinja] [possible values: mustache, minijinja, jinja2]
  -t, --template <TEMPLATE>  Template file
  -d, --data <DATA>          JSON data file
  -o, --out <OUT>            Output file
  -a, --audit                Audit mode
      --mode <MODE>          Trust level for the template content [default: sandboxed] [possible values: sandboxed, unrestricted]
  -h, --help                 Print help
  -V, --version              Print version

Minijinja

MiniJinja is based on the Jinja2 templating language. For a complete reference on MiniJinja syntax, see the Jinja2 documentation and the MiniJinja compatibility.

Syntax

This section presents some simple cases that cover what can be done with MiniJinja templating.

Note: You can add comments in the template, that will not be rendered in the output file with:

{# ... #}.

Conditions

To display content based on conditions definition:

{% if classes.my_condition is defined  %}
   display this if defined
{% endif %}
{% if not classes.my_condition is defined %}
   display this if not defined
{% endif %}

Note: You cannot use condition expressions here.

You can also use other tests, for example other built-in ones:

{% if vars.variable_prefix.my_number is odd  %}
   display if my_number is odd
{% endif %}

Scalar variables

Here is how to display a scalar variable value (integer, string, …), if you have defined variable_string("variable_prefix", "my_variable", "my_value"):

{{ vars.variable_prefix.my_variable }}

You can also modify what is displayed by using filters.

{{ vars.variable_prefix.my_variable | uppercase }}

Will display the variable in uppercase.

Iteration

Loops support {% break %} and {% continue %} to exit early or skip an item.

To iterate over a list, for example defined with:

variable_iterator("variable_prefix", "iterator_name", "a,b,c", ",")

Use the following file:

{% for item in vars.variable_prefix.iterator_name %}
{{ item }} is the current iterator_name value
{% endfor %}

Which will be expanded as:

a is the current iterator_name value
b is the current iterator_name value
c is the current iterator_name value

To iterate over a container defined by the following json file, loaded with variable_dict_from_file("variable_prefix", "dict_name", "path"):

{
   "hosts": [
       "host1",
       "host2"
   ],
   "files": [
       {"name": "file1", "path": "/path1", "users": [ "user1", "user11" ] },
       {"name": "file2", "path": "/path2", "users": [ "user2" ] }
   ],
   "properties": {
       "prop1": "value1",
       "prop2": "value2"
   }
}

Use the following template:

{% for item in vars.variable_prefix.dict_name.hosts %}
{{ item }} is the current hosts value
{% endfor %}

# will display the name and path of the current file
{% for file in vars.variable_prefix.dict_name.files %}
{{ file.name }}: {{ file.path }}
{% endfor %}

# will display the users list of each file
{% for file in vars.variable_prefix.dict_name.files %}
{{ file.name }}: {{ file.users|join(' ') }}
{% endfor %}


# will display the current properties key/value pair
{% for key, value in vars.variable_prefix.dict_name.properties.items() %}
{{ key }} -> {{ value }}
{% endfor %}

Which will be expanded as:

host1 is the current hosts value
host2 is the current hosts value

# will display the name and path of the current file
file1: /path1
file2: /path2

# will display the users list of each file
file1: user1 user11
file2: user2

# will display the current properties key/value pair
prop1 -> value1
prop2 -> value2
Key order

When iterating a dict (.items(), .keys(), .values()) or serializing it (tojson, {{%-top-}}), keys keep the order they have in the input data — they are not sorted alphabetically. As a result, changing the order of keys in the source data changes the rendered output (and so triggers a file update).

Filters

NameDescriptionRequired ArgsOptional ArgsExample
absReturns the absolute value of a number.NoneNone|a - b| = {{ (a - b)|abs }} -> |2 - 4| = 2
attrLooks up an attribute.keyNone{{ value['key'] == value|attr('key') }} -> true
batchBatch items.countfill_with{% for row in items|batch(3, '&nbsp;') %}
boolConverts the value into a boolean value.NoneNone{{ 42|bool }} -> true
capitalizeConvert the string with all its characters lowercased apart from the first char which is uppercased.NoneNone{{ chapter.title|capitalize }}
defaultIf the value is undefined it will return the passed default value, otherwise the value of the variable:Noneother{{ my_variable|default("my_variable was not defined") }}
dictsortDict sorting functionality.Noneby, case_sensitive, reverse{{ dict|sort('reverse') }}
escapeEscapes a string. By default to HTML.NoneNone{{ value|escape }}
firstReturns the first item from an iterable.NoneNone{{ user.email_addresses|first|default('no user') }}
floatConverts a value into a float.NoneNone{{ "42.5"|float == 42.5 }} -> true
groupbyGroup a sequence of objects by an attribute.Noneattribute, default, case_sensitive{% for city, items in users|groupby("city") %}
indentIndents Value with spaceswidthindent_first_line, indent_blank_lines{{ global_config|indent(2) }}
intConverts a value into an integer.NoneNone{{ "42"|int == 42 }} -> true
itemsReturns an iterable of pairs (items) from a mapping.NoneNone{% for key, value in my_dict|items %}
joinJoins a sequence by a characterNonejoiner{{ "Foo Bar Baz" | join(", ") }} -> foo, bar, baz
lastReturns the last item from an iterable.NoneNone{% with update = updates|last %}
lengthReturns the “length” of the valueNoneNone {{ results|length }}
linesSplits a string into lines.NoneNone{{ "foo\nbar\nbaz"|lines }} -> ["foo", "bar", "baz"]
listConverts the input value into a list.NoneNone{{ value|list }}
lowerConverts a value to lowercase.NoneNone{{ chapter.title|lower }}
mapApplies a filter to a sequence of objects or looks up an attribute.attributeNone{{ users|map(attribute='username')|join(', ') }}
maxReturns the largest item from an iterable.NoneNone{{ [1, 2, 3, 4]|max }} -> 4
minReturns the smallest item from an iterable.NoneNone{{ [1, 2, 3, 4]|min }} -> 1
pprintPretty print a variable.NoneNone{{ value|pprint }}
rejectCreates a new sequence of values that don’t pass a test.test_nameNone[1, 2, 3, 4]|reject("odd")
rejectattrCreates a new sequence of values of which an attribute does not pass a test.attrtest_name{{ users|rejectattr("id", "even") }}
replaceDoes a string replace.from, toNone{{ "Hello World"|replace("Hello", "Goodbye") }} -> Goodbye World
reverseReverses an iterable or stringNoneNone{% for user in users|reverse %}
roundRound the number to a given precision.Noneprecision{{ 42.55|round }} -> 43.0
safeMarks a value as safe. This converts it into a string.NoneNone{{ value|safe }}
selectCreates a new sequence of values that pass a test.Nonetest_name{{ [1, 2, 3, 4]|select("odd") }} -> [1, 3]
selectattrCreates a new sequence of values of which an attribute passes a test.attrtest_name{{ users|selectattr("id", "even") }} -> returns all users with an even id
sliceSlice an iterable and return a list of lists containing those items.countfill_with{% for column in items|slice(3) %}
sortReturns the sorted version of the given list.Nonetest_name{{ [1, 3, 2, 4]|sort }} -> [4, 3, 2, 1]
splitSplit a string into its substrings, using split as the separator string.Nonesplit, maxsplit{{ "hello world"|split|list }} -> ["hello", "world"]
stringConverts a value into a string if it’s not one already.NoneNone{{ value|string }}
sumSums up all the values in a sequence.NoneNone{{ range(10)|sum }} -> 45
titleConverts a value to title case.NoneNone{{ chapter.title|title }}
tojsonDumps a value to JSON.Noneident, ident{{ global_config|tojson }}
trimTrims a valueNonechars{{ value|trim(' ') }}
uniqueReturns a list of unique items from the given iterable.Noneattribute, case_sensitive{{ ['foo', 'bar', 'foobar', 'foobar']|unique|list }} -> ['foo', 'bar', 'foobar']
upperConverts a value to uppercase.NoneNone{{ chapter.title|upper }}
urlencodeURL encodes a value.NoneNone<a href="/search?{{ {"q": "my search", "lang": "fr"}|urlencode }}">Search</a>
dateformatFormats a timestamp as date.Noneformat, tz{{ value|dateformat(format="short", tz="Europe/Vienna") }}
datetimeformatFormats a timestamp as date and time.Noneformat, tz{{ value|datetimeformat(format="short", tz="Europe/Vienna") }}
filesizeformatFormats the value like a “human-readable” file size.Nonebinary{{ value|filesizeformat }}
pluralizeReturns a plural suffix if the value is not 1, ‘1’, or an object of length 1.Nonesingular, plural{{ entities|pluralize("y", "ies") }}
random :warning: Usage in a Rudder template will cause reparation at each run of the agent.Chooses a random element from a sequence or string.NoneNone{{ [1, 2, 3, 4]|random }}
timeformatFormats a timestamp as time.Noneformat, tz{{ value|timeformat(format="short") }}
truncateReturns a truncated copy of the string.Nonelength, killwords, end, leeway{{ "Hello World"|truncate(length=5) }}
wordcountCounts the words in a string.NoneNone{{ "Hello world!"|wordcount }}
wordwrapWrap a string to the given width.Nonewidth, break_long_words, break_on_hyphens, wrapstring{{ value|wordwrap(width=12) }}
b64encodeEncode a string as Base64.NoneNone{{ plain|b64encode }}
b64decodeDecode a Base64 string.NoneNone{{ encoded|b64decode }}
basenameGet a path’s base name.NoneNone{{ path|basename }}
dirnameGet a path’s directory name.NoneNone{{ path|dirname }}
urldecodeDecode percent-encoded sequences.NoneNone{{ encoded_url|urldecode }}
hashHash of input data (SHA-1, SHA-256, SHA-512, Uses SHA-1 by default).Nonealgorithm (sha-1, sha-256, sha-512){{ value|hash('sha-256') }}
quoteShell quoting.NoneNone{{ value|quote }}
regex_escapeEscape regex chars.NoneNone{{ re|regex_escape }}
regex_replaceReplace a string via regex.Nonecount{{ re|regex_replace }}

Reading files

Reading files is only available with mode: unrestricted.

The lookup('file', path) function reads a file’s raw UTF-8 content at render time, on the node:

{{ lookup('file', "/etc/issue") }}

A missing file, or one that is not valid UTF-8, makes the rendering fail. lookup currently only supports the file kind.

Use absolute paths, and only use trusted template contents.

Including other template files

Including files is only available with mode: unrestricted.

{% include %}, {% import %} and {% extends %} load other templates relative to the rendered template file’s directory.

{% include "header.j2" %}
{% import "macros.j2" as macros %}

Paths are resolved relative to the rendered template, so a header.j2 sitting next to your template_path is included as "header.j2".

This requires template_path (inline templates have no base directory).

Additional methods for Python compatibility

The following methods on basic types are provided:

  • dict.get
  • dict.items
  • dict.keys
  • dict.values
  • list.count
  • str.capitalize
  • str.count
  • str.endswith
  • str.find
  • str.isalnum
  • str.isalpha
  • str.isascii
  • str.isdigit
  • str.islower
  • str.isnumeric
  • str.isupper
  • str.join
  • str.lower
  • str.lstrip
  • str.replace
  • str.rfind
  • str.rstrip
  • str.split
  • str.splitlines
  • str.startswith
  • str.strip
  • str.title

Sandboxing

With mode: sandboxed (the default):

  • the lookup('file', …) function (which reads arbitrary files) is not registered, and
  • no template loader is configured, so include/import/extends cannot read files.

A fuel limit caps the total number of operations a render may perform in both modes (higher when unrestricted), so a runaway template — even an accidental infinite loop in a trusted one — fails instead of hanging.

Jinja2

Jinja2 behaves for the most part the same way as MiniJinja. For a complete reference of features, see the official Jinja2 documentation.

You can extend the Jinja2 templating engine by adding custom FILTERS and TESTS in the script /var/rudder/configuration-repository/ncf/10_ncf_internals/modules/extensions/jinja2_custom.py

For instance, to add a filter to uppercase a string and a test if a number is odd, you can create the file /var/rudder/configuration-repository/ncf/10_ncf_internals/modules/extensions/jinja2_custom.py on your Rudder server with the following content:

def uppercase(input):
    return input.upper()

def odd(value):
    return True if (value % 2) else False

FILTERS = {'uppercase': uppercase}
TESTS = {'odd': odd}

These filters and tests will be usable in your jinja2 templates automatically.

Sandboxing

⚠️ Important: `mode: unrestricted` disables all protections on the Jinja2 engine.

With mode: unrestricted, Jinja2 executes within the full Python runtime context of the process, without Jinja2’s sandboxed environment enabled.

Only set mode: unrestricted for trusted templates. With the default mode: sandboxed, Jinja2 uses its SandboxedEnvironment (which restricts access to Python internals), has no filesystem loader (so include/import/extends cannot read files), and each render is bounded by a timeout (3s sandboxed, longer when unrestricted).

The Jinja2 sandbox is best-effort, not a hard security boundary. For untrusted templates, prefer the MiniJinja engine.

Mustache

Mustache is a logic-less templating language. For a complete reference of features, see the official mustache specification.

Syntax

This section presents some simple cases that cover what can be done with Mustache templating.

The main specificity compared to standard mustache syntax of prefixes in all expanded values:

  • classes to access conditions
  • vars to access all variables

Classes

Here is how to display content depending on conditions definition:

{{#classes.my_condition}}
   content when my_condition is defined
{{/classes.my_condition}}

{{^classes.my_condition}}
   content when my_condition is *not* defined
{{/classes.my_condition}}

Note: You cannot use condition expressions here.

Scalar variable

Here is how to display a scalar variable value (integer, string, …), if you have defined variable_string("variable_prefix", "my_variable", "my_value"):

{{{vars.variable_prefix.my_variable}}}

We use the triple {{{ }}} to avoid escaping html entities.

Use {{#vars.container}} content {{/vars.container}} to iterate Use {{{.}}} for the current element value in iteration Use {{{key}}} for the key value in current element Use {{{@}}} for the current element key in iteration

To iterate over a list, for example defined with:

variable_iterator("variable_prefix", "iterator_name", "a,b,c", ",")

Use the following file:

{{#vars.variable_prefix.iterator_name}}
{{{.}}} is the current iterator_name value
{{/vars.variable_prefix.iterator_name}}

Which will be expanded as:

a is the current iterator_name value
b is the current iterator_name value
c is the current iterator_name value

To iterate over a container defined by the following json file, loaded with variable_dict_from_file("variable_prefix", "dict_name", "path"):

{
   "hosts": [
       "host1",
       "host2"
   ],
   "files": [
       {"name": "file1", "path": "/path1", "users": [ "user1", "user11" ] },
       {"name": "file2", "path": "/path2", "users": [ "user2" ] }
   ],
   "properties": {
       "prop1": "value1",
       "prop2": "value2"
   }
}

Use the following template:

{{#vars.variable_prefix.dict_name.hosts}}
{{{.}}} is the current hosts value
{{/vars.variable_prefix.dict_name.hosts}}

# will display the name and path of the current file
{{#vars.variable_prefix.dict_name.files}}
{{{name}}}: {{{path}}}
{{/vars.variable_prefix.dict_name.files}}
# Lines below will only be properly rendered in unix Nodes
# will display the users list of each file
{{#vars.variable_prefix.dict_name.files}}
{{{name}}}:{{#users}} {{{.}}}{{/users}}
{{/vars.variable_prefix.dict_name.files}}


# will display the current properties key/value pair
{{#vars.variable_prefix.dict_name.properties}}
{{{@}}} -> {{{.}}}
{{/vars.variable_prefix.dict_name.properties}}

Which will be expanded as:

host1 is the current hosts value
host2 is the current hosts value

# will display the name and path of the current file
file1: /path1
file2: /path2

# Lines below will only be properly rendered in unix Nodes
# will display the users list of each file
file1: user1 user11
file2: user2

# will display the current properties key/value pair
prop1 -> value1
prop2 -> value2

Note: You can use {{#-top-}} ... {{/-top-}} to iterate over the top level container.

System variables

Some sys dict variables (like sys.ipv4) are also accessible as string, for example:

${sys.ipv4} gives 54.32.12.4
$[sys.ipv4[ethO]} gives 54.32.12.4
$[sys.ipv4[eth1]} gives 10.45.3.2

These variables are not accessible as dict in the templating data, but are represented as string:

ipv4 is a string variable in the sys dict with value 54.32.12.4
ipv4[ethO] is a string variable in the sys dict with value 54.32.12.4
ipv4 is not accessible as a dict in the template

To access these value, use the following syntax in your mustache templates:

{{{vars.sys.ipv4[eth0]}}}

Top level container

The top level container can be iterated over:

{{#-top-}} ... {{/-top-}}

It can also be rendered as a compact JSON representation

{{$-top-}}

Or rendered as a multi-line JSON representation

{{%-top-}}

Sandboxing

Mustache does not currently enforce mode. It is logic-less, but {{> partial}} partials are always evaluated and read files from disk, so do not rely on sandboxing for untrusted Mustache templates — use MiniJinja instead.