Templating
Volant renders Jinja2 on the controller with MiniJinja and adds Ansible’s filters, tests and lookups on top.
Strict by default
Section titled “Strict by default”Templates render in strict mode. An undefined variable is an error, not an empty string, unless you guard it:
- debug: msg: "{{ release | default('latest') }}" when: release is defined or use_latest | boolA when whose value is not a boolean is rejected as well.
Conditions
Section titled “Conditions”when, changed_when, failed_when and until take one expression or a list of them. With a list, every expression must hold.
- command: /opt/app/migrate.sh register: migrate changed_when: "'applied' in migrate.stdout" failed_when: - migrate.rc != 0 - "'already up to date' not in migrate.stderr"A condition wrapped in {{ }} that renders to a string is evaluated once more as a condition, as ansible-core 2.19 does. With healthy: "true" in the play’s vars, when: "{{ healthy }}" runs the task. This second evaluation never happens when the first render read a value from a managed host: the task fails with Encountered untrusted template or expression. instead. See Trusted and untrusted values.
What is available
Section titled “What is available”On top of MiniJinja’s own Jinja2 builtins, Volant adds these from Ansible:
| Kind | Names |
|---|---|
| Filters | default / d, bool, int, float, mandatory, ternary, combine, dict2items, items2dict, to_json, to_nice_json, from_json, to_yaml, to_nice_yaml, from_yaml, basename, dirname, split, regex_replace, regex_search, regex_findall, regex_escape, b64encode, b64decode, comment, difference, intersect, union, flatten, to_uuid, type_debug, quote |
| Tests | truthy, falsy, match, search, regex, contains, changed / change, failed / failure, succeeded / success / successful, skipped / skip, version (and version_compare) |
| Lookups | env, file, vars, pipe, first_found, fileglob, template |
From ansible.utils |
the ansible.utils.ipwrap filter, under that name only |
Every filter, test and lookup in this list also answers under its ansible.builtin.* name, so {{ data | ansible.builtin.to_json }} works. A filter, test or lookup that is not in this list fails with its own name, never silently. That includes the filters of any other collection.
first_found, fileglob and template look for files along ansible_search_path. In a role’s task that is the role’s directory, the directory of the task’s file, then the playbook’s directory. Outside a role it is the playbook’s directory. role_path is set in a role’s task and is undefined outside one. fileglob tries a relative pattern under files/ of each entry, then in the entry itself, and the first entry with a match wins. It returns files only, sorted, and a single match comes back as a string. Each one is checked against ansible-core in the golden test corpus under crates/volant/tests/golden.
A value also answers a handful of Python methods: .split(), .startswith() and .find() on a string, .keys() on a mapping, and the others that minijinja-contrib’s Python compatibility layer provides. A method neither that layer nor the engine knows fails with its own name rather than silently doing nothing.
difference, intersect and union build a Python set in ansible-core, whose order is its own: small integers happen to come out sorted, and strings come out in an order that changes with PYTHONHASHSEED from one run to the next. Volant sorts the result when every element is an integer, which matches every integer case measured, and otherwise keeps the order in which the elements first appeared. On strings, that order is not guaranteed to match a given run of ansible-core.
Rendering a template file
Section titled “Rendering a template file”The template module and lookup('template') share one render, and it differs from a task argument’s render in one way: it renders the file’s text once. A task argument goes through further passes, which is how a value that still holds {{ ... }} after rendering can be read as a template again. A template file does not get that second pass, so a value a managed host contributed lands in the written file as plain text and never runs as a template of its own.
It takes the options the template module reads:
| Option | Default | Effect |
|---|---|---|
trim_blocks |
on | Remove the first newline after a block tag. |
lstrip_blocks |
off | Strip the whitespace before a block tag on its line. |
newline_sequence |
\n |
Line endings of the written file: \n, \r or \r\n, escaped or literal. |
The file’s own trailing newline survives the render. Action plugins covers the rest of what the template module does.
Not there yet
Section titled “Not there yet”- The
!vaultand!unsafeYAML tags are detected and not supported yet. There is no decryption and no unsafe marking yet. - Other Ansible filters, tests and lookups, such as
hash,password_hash,ipaddr, theversionfilter andjson_query, are not supported yet. - In a template file,
template_host,template_uid,template_run_dateandtemplate_mtimeare not set, unliketemplate_path,template_fullpathandansible_managed. - In a template file,
{% include %}and{% import %}fail: nothing loads a second file during a render. - The six delimiter options (
variable_start_stringand its five relatives) and anoutput_encodingother than UTF-8 are not supported yet.
Performance on large inventories
Section titled “Performance on large inventories”Resolving variables costs more than linear time in the number of hosts. Two changes have removed most of that cost: every host reads one shared hostvars map, and inventory-wide values such as groups are built once per batch instead of once per host and task. Nothing in a playbook, an inventory or ansible.cfg changes this cost.
- 50 hosts
- 100 hosts
- 200 hosts
Show as a table
| Run time by inventory size | |
|---|---|
| 50 hosts | 0.16 s |
| 100 hosts | 0.50 s |
| 200 hosts | 1.45 s |