mirror of
https://github.com/ansible/ansible.git
synced 2026-08-02 08:03:18 +02:00
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:
co-authored by
Matt Davis
Matt Clay
parent
6fc592df9b
commit
35750ed321
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user