Templating overhaul, implement Data Tagging (#84621)

Co-authored-by: Matt Davis <mrd@redhat.com>
Co-authored-by: Matt Clay <matt@mystile.com>
This commit is contained in:
Matt Davis
2025-04-14 08:54:36 -07:00
committed by GitHub
co-authored by Matt Davis Matt Clay
parent 6fc592df9b
commit 35750ed321
793 changed files with 29627 additions and 12505 deletions
@@ -1,2 +0,0 @@
+ ansible-playbook -i localhost, -c local quiet.yml
++ set +x
@@ -1,17 +0,0 @@
PLAY [localhost] ***************************************************************
TASK [assert] ******************************************************************
ok: [localhost] => (item=item_A)
TASK [assert] ******************************************************************
ok: [localhost] => (item=item_A) => {
"ansible_loop_var": "item",
"changed": false,
"item": "item_A",
"msg": "All assertions passed"
}
PLAY RECAP *********************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
@@ -1,3 +0,0 @@
[all]
localhost
@@ -0,0 +1,10 @@
from __future__ import annotations
from ansible.plugins.lookup import LookupBase
class LookupModule(LookupBase):
accept_args_markers = True
def run(self, terms, variables=None, **kwargs):
return terms
-16
View File
@@ -1,16 +0,0 @@
---
- hosts: localhost
gather_facts: False
vars:
item_A: yes
tasks:
- assert:
that: "{{ item }} is defined"
quiet: True
with_items:
- item_A
- assert:
that: "{{ item }} is defined"
quiet: False
with_items:
- item_A
-71
View File
@@ -1,71 +0,0 @@
#!/usr/bin/env bash
# This test compares "known good" output with various settings against output
# with the current code. It's brittle by nature, but this is probably the
# "best" approach possible.
#
# Notes:
# * options passed to this script (such as -v) are ignored, as they would change
# the output and break the test
# * the number of asterisks after a "banner" differs is forced to 79 by
# redirecting stdin from /dev/null
set -eux
run_test() {
# testname is playbook name
local testname=$1
# The shenanigans with redirection and 'tee' are to capture STDOUT and
# STDERR separately while still displaying both to the console
{ ansible-playbook -i 'localhost,' -c local "${testname}.yml" \
> >(set +x; tee "${OUTFILE}.${testname}.stdout"); } \
2> >(set +x; tee "${OUTFILE}.${testname}.stderr" >&2) 0</dev/null
sed -i -e 's/ *$//' "${OUTFILE}.${testname}.stdout"
sed -i -e 's/ *$//' "${OUTFILE}.${testname}.stderr"
# Scrub deprecation warning that shows up in Python 2.6 on CentOS 6
sed -i -e '/RandomPool_DeprecationWarning/d' "${OUTFILE}.${testname}.stderr"
diff -u "${ORIGFILE}.${testname}.stdout" "${OUTFILE}.${testname}.stdout" || diff_failure
diff -u "${ORIGFILE}.${testname}.stderr" "${OUTFILE}.${testname}.stderr" || diff_failure
}
diff_failure() {
if [[ $INIT = 0 ]]; then
echo "FAILURE...diff mismatch!"
exit 1
fi
}
cleanup() {
if [[ $INIT = 0 ]]; then
rm -f "${OUTFILE}."*
fi
}
BASEFILE=assert_quiet.out
ORIGFILE="${BASEFILE}"
OUTFILE="${BASEFILE}.new"
trap 'cleanup' EXIT
# The --init flag will (re)generate the "good" output files used by the tests
INIT=0
if [[ ${1:-} == "--init" ]]; then
shift
OUTFILE=$ORIGFILE
INIT=1
fi
# Force the 'default' callback plugin
export ANSIBLE_STDOUT_CALLBACK=default
# Disable color in output for consistency
export ANSIBLE_FORCE_COLOR=0
export ANSIBLE_NOCOLOR=1
# Disable retry files
export ANSIBLE_RETRY_FILES_ENABLED=0
run_test quiet
@@ -0,0 +1,230 @@
# validate initial simple `assert` behavior upfront via `failed_when` and `fail` so we can safely
# use `assert` itself for the rest of the test
- set_fact:
a_var: one
- name: simple scalar true conditional expression
assert:
that: a_var == "one"
register: result
failed_when: result is not success or result.msg != "All assertions passed"
- name: simple scalar false conditional expression
assert:
that: a_var != "one"
register: result
failed_when:
- result is not failed or result.msg is not contains("Assertion failed") or result.assertion is not contains('a_var != "one"')
- name: list of all true conditional expressions
assert:
that:
- a_var == "one"
- 1 == 1
register: result
failed_when: result is not success or result.msg != "All assertions passed"
- name: list of one true conditional expression and two false
assert:
that:
- a_var == "one"
- 1 == 2
- a_var == "two"
register: result
failed_when: result is not failed or result.msg is not contains("Assertion failed") or result.assertion is not contains('1 == 2')
# from here on, just use `assert` to validate behavior
- name: arg splat loopable success cases
assert: '{{ item }}'
args:
quiet: yes
loop:
- that: 1 == 1
- that: a_var == "one"
- that: '{{ a_var == "one" }}'
# DTFIX-RELEASE: loops are not lazily templated, so this is not possible today, but could be in the future
# - that: |
# "hi mom" == '{{ "hi mom" }}'
- that:
- 1 == 1
- 2 == 2
- a_var == "one"
- '{{ a_var == "one" }}'
- name: arg splat fail cases
assert: '{{ item }}'
args:
quiet: yes
loop:
- that: 1 == 2
- that: a_var == "two"
- that: '{{ a_var == "two" }}'
- that:
- 1 == 2
- 2 == 3
- a_var == "two"
- '{{ a_var == "two" }}'
ignore_errors: true
register: result
- assert:
that:
- result.results | select('success') | length == 0
- result.results | rejectattr('msg', 'contains', 'Assertion failed') | length == 0
- name: test misc other assert failures
assert: '{{ item.args }}'
loop:
- args: { }
checks:
- item is failed
- 'item.msg is contains("missing required arguments: that")'
- args: {that: }
checks:
- item is failed
- item.msg is contains("Empty conditional expression")
ignore_errors: true
register: result
- name: validate per-item assertions
assert:
that: '{{ item.item.checks }}'
loop: '{{ result.results }}'
- name: ensure deprecation warning from handlebarred `that`
assert:
that: '{{ true }}'
vars:
ansible_deprecation_warnings: true
register: res
- assert:
that:
- res.deprecations | map(attribute="msg") | select('contains', 'should not be surrounded by templating delimiters') | length == 1
- name: quoted string literal templates in conditional expressions are processed
# solution: enable visit_Const trust tagging when processing a conditional expression (not a conditional template)
assert:
that: |
"dude" == "du{{'de'}}"
- name: templated strings in function args are processed in conditional expressions
assert:
that: |
dict({"dude": "{{ 'dude' }}"})["dude"] == "dude"
- name: templated strings in function args are not processed in conditional templates
assert:
that: |
{{ dict({"dude": "{{ 'dude' }}"})["dude"] == "{" + "{ 'dude' }}" }}
- name: templated strings in templates are ignored
# solution: do *not* enable visit_Const trust tagging when processing a conditional template (expressions are OK)
assert:
that: |
{{ "du{" + "{'de'}}" == "du{{'de'}}" }}
- name: templated string literals in non-lookup function args are ignored
# solution: do not template values in visit_Const, do not apply trust tags to const tagged values unless the function is a lookup
debug:
msg: |-
{% set pipe = joiner('{{ "dude" }}') %}
x{{ pipe() }}{{ pipe() }}x
register: debug
failed_when: debug.msg is not contains('{')
# DTFIX-FUTURE: move to templating integration test
- name: templated strings in lookup/query args are processed in a conditional
assert:
that:
- |
{{ lookup('pipe', 'echo {{ "dude" }}') == "dude" }}
- lookup('pipe', 'echo {{ "dude" }}') == "dude"
- |
{{ query('pipe', 'echo {{ "dude" }}') == ["dude"] }}
- query('pipe', 'echo {{ "dude" }}') == ["dude"]
# DTFIX-FUTURE: move to templating integration test
- name: templated strings in lookup/query args are processed in a non-conditional
debug:
msg:
- |
{{ lookup('pipe', 'echo {{ "dude" }}') == "dude" }}
- |
{{ query('pipe', 'echo {{ "dude" }}') == ["dude"] }}
register: result
failed_when: result.msg | reject | length != 0
- name: templated strings in lookup names are ignored
# solution: exclude the lookup name from trust tagging and templating
debug:
msg: |-
{{ lookup('{{ "pipe" }}') }}
register: debug
ignore_errors: yes
- assert:
that:
- debug.msg is contains ('lookup plugin')
- debug.msg is contains ('not found')
- name: meta-templating expressions is not supported
assert:
that: |
{{ 'hello' }} == {{ 'world' }}
ignore_errors: true
register: result
- assert:
that:
- result is failed
- result.msg is contains("untrusted template or expression")
- name: validate template-indirected conditional expression success
assert:
that: '{{ item }}' # only allowed when the template result is a trusted string literal; no meta-expressions!
loop:
- 1 == 1
- 2 == 2
- name: validate template-indirected conditional expression failure
assert:
that: '{{ item }}'
loop:
- 1 == 1
- 1 == 42 # a false expression; ensure that we're actually evaluating
ignore_errors: true
register: cond_fail
- assert:
that:
- cond_fail is failed
- cond_fail.results[1].msg is contains "Assertion failed"
- cond_fail.results[1].item == "1 == 42"
- name: ensure that arg-splatted expressions that are template-wrapped provide a deprecation warning
assert:
that: '{{ thats }}'
vars:
ansible_deprecation_warnings: true
thats:
- '{{ 1 == 1 }}'
register: result
- assert:
that:
- result.deprecations | length == 1
- result.deprecations[0].msg is contains "should not be surrounded by templating delimiters"
- name: ensure that expressions with embedded lookup arg templates are not early-resolved by args templating
assert:
that: lookup('yield_terms', '{{ bang }}', 42)[1] == 42 # this would blow up if it were arg-templated
# DTFIX-FUTURE: restore tests for `quiet` arg with a custom callback or output validation test
# see: https://github.com/ansible/ansible/tree/d2bdbadb0327d519c1c353bf3e28f83ba1b59485/test/integration/targets/assert