diff --git a/changelogs/fragments/conditionals_fix.yml b/changelogs/fragments/conditionals_fix.yml new file mode 100644 index 00000000000..377a5219c4d --- /dev/null +++ b/changelogs/fragments/conditionals_fix.yml @@ -0,0 +1,3 @@ +bugfixes: + - remove bare var handling from conditionals (not needed since we removed bare vars from `with_` loops) to normalize handling of + variable values, no matter if the string value comes from a top level variable or from a dictionary key or subkey diff --git a/lib/ansible/config/base.yml b/lib/ansible/config/base.yml index 93288b5ea9e..393bc2c7846 100644 --- a/lib/ansible/config/base.yml +++ b/lib/ansible/config/base.yml @@ -318,6 +318,19 @@ COLOR_WARN: env: [{name: ANSIBLE_COLOR_WARN}] ini: - {key: warn, section: colors} +CONDITINAL_BARE_VARS: + name: Allow bare variable evaluation in conditionals + default: True + type: boolean + description: + - With this setting on (True), runing conditional evaluation 'var' is treated differently 'var.subkey' as the first is evaluted + directly while the second goes though the Jinja2 parser. But 'false' strings in 'var' get evaluated as booleans. + - With this settting off they both evalutate the same but in cases in which 'var' was 'false' (a string) it won't get evaluated as a boolean anymore. + - Currently this setting defaults to 'True' but will soon change to 'False' and the setting itself will be removed in the future. + - Expect the default to change in version 2.10 and that this setting eventually will be deprecated after 2.12 + env: [{name: ANSIBLE_CONDITIONAL_BARE_VARS}] + ini: + - {key: conditional_bare_variables, section: defaults} ACTION_WARNINGS: name: Toggle action warnings default: True diff --git a/lib/ansible/playbook/conditional.py b/lib/ansible/playbook/conditional.py index 3f86368d42a..eb8aa5155c6 100644 --- a/lib/ansible/playbook/conditional.py +++ b/lib/ansible/playbook/conditional.py @@ -25,6 +25,7 @@ import re from jinja2.compiler import generate from jinja2.exceptions import UndefinedError +from ansible import constants as C from ansible.errors import AnsibleError, AnsibleUndefinedVariable from ansible.module_utils.six import text_type from ansible.module_utils._text import to_native @@ -113,21 +114,16 @@ class Conditional: if isinstance(conditional, bool): return conditional + if C.CONDITINAL_BARE_VARS: + if conditional in all_vars and VALID_VAR_REGEX.match(conditional): + display.deprecated('evaluating %s as a bare variable, this behaviour will go away and you might need to add |bool' + ' to the expression in the future. Also see CONDITIONAL_BARE_VARS configuration toggle.' % conditional, "2.12") + conditional = all_vars[conditional] + if templar.is_template(conditional): - display.warning('when statements should not include jinja2 ' + display.warning('conditional statements should not include jinja2 ' 'templating delimiters such as {{ }} or {%% %%}. ' 'Found: %s' % conditional) - - # pull the "bare" var out, which allows for nested conditionals - # and things like: - # - assert: - # that: - # - item - # with_items: - # - 1 == 1 - if conditional in all_vars and VALID_VAR_REGEX.match(conditional): - conditional = all_vars[conditional] - # make sure the templar is using the variables specified with this method templar.set_available_variables(variables=all_vars) @@ -219,5 +215,5 @@ class Conditional: # as nothing above matched the failed var name, re-raise here to # trigger the AnsibleUndefinedVariable exception again below raise - except Exception as new_e: + except Exception: raise AnsibleUndefinedVariable("error while evaluating conditional (%s): %s" % (original, e)) diff --git a/test/integration/targets/conditionals/aliases b/test/integration/targets/conditionals/aliases index 765b70da796..3581b8a7ede 100644 --- a/test/integration/targets/conditionals/aliases +++ b/test/integration/targets/conditionals/aliases @@ -1 +1,3 @@ +shippable/posix/group1 shippable/posix/group2 +shippable/posix/group3 diff --git a/test/integration/targets/conditionals/inventory b/test/integration/targets/conditionals/inventory new file mode 100644 index 00000000000..4c2e0d1e86c --- /dev/null +++ b/test/integration/targets/conditionals/inventory @@ -0,0 +1,5 @@ +# Do not put test specific entries in this inventory file. +# For script based test targets (using runme.sh) put the inventory file in the test's directory instead. + +[testgroup] +testhost ansible_connection=local diff --git a/test/integration/targets/conditionals/play.yml b/test/integration/targets/conditionals/play.yml new file mode 100644 index 00000000000..e69e477ebe3 --- /dev/null +++ b/test/integration/targets/conditionals/play.yml @@ -0,0 +1,551 @@ +# (c) 2014, James Cammarata +# (c) 2019, Ansible Project + +- hosts: testhost + gather_facts: false + vars_files: + - vars/main.yml + tasks: + - name: set conditial bare vars status + set_fact: + bare: "{{lookup('config', 'CONDITINAL_BARE_VARS')|bool}}" + + - name: test conditional '==' + shell: echo 'testing' + when: 1 == 1 + register: result + + - name: assert conditional '==' ran + assert: + that: + - result is changed + - "result.stdout == 'testing'" + - "result.rc == 0" + + - name: test bad conditional '==' + shell: echo 'testing' + when: 0 == 1 + register: result + + - name: assert bad conditional '==' did NOT run + assert: + that: + - result is skipped + + - name: test conditional '!=' + shell: echo 'testing' + when: 0 != 1 + register: result + + - name: assert conditional '!=' ran + assert: + that: + - result is changed + - "result.stdout == 'testing'" + - "result.rc == 0" + + - name: test bad conditional '!=' + shell: echo 'testing' + when: 1 != 1 + register: result + + - name: assert bad conditional '!=' did NOT run + assert: + that: + - result is skipped + + - name: test conditional 'in' + shell: echo 'testing' + when: 1 in [1,2,3] + register: result + + - name: assert conditional 'in' ran + assert: + that: + - result is changed + - "result.stdout == 'testing'" + - "result.rc == 0" + + - name: test bad conditional 'in' + shell: echo 'testing' + when: 1 in [7,8,9] + register: result + + - name: assert bad conditional 'in' did NOT run + assert: + that: + - result is skipped + + - name: test conditional 'not in' + shell: echo 'testing' + when: 0 not in [1,2,3] + register: result + + - name: assert conditional 'not in' ran + assert: + that: + - result is changed + - "result.stdout == 'testing'" + - "result.rc == 0" + + - name: test bad conditional 'not in' + shell: echo 'testing' + when: 1 not in [1,2,3] + register: result + + - name: assert bad conditional 'not in' did NOT run + assert: + that: + - result is skipped + + - name: test conditional 'is defined' + shell: echo 'testing' + when: test_bare is defined + register: result + + - name: assert conditional 'is defined' ran + assert: + that: + - result is changed + - "result.stdout == 'testing'" + - "result.rc == 0" + + - name: test bad conditional 'is defined' + shell: echo 'testing' + when: foo_asdf_xyz is defined + register: result + + - name: assert bad conditional 'is defined' did NOT run + assert: + that: + - result is skipped + + - name: test conditional 'is not defined' + shell: echo 'testing' + when: foo_asdf_xyz is not defined + register: result + + - name: assert conditional 'is not defined' ran + assert: + that: + - result is changed + - "result.stdout == 'testing'" + - "result.rc == 0" + + - name: test bad conditional 'is not defined' + shell: echo 'testing' + when: test_bare is not defined + register: result + + - name: assert bad conditional 'is not defined' did NOT run + assert: + that: + - result is skipped + + - name: test bad conditional 'is undefined' + shell: echo 'testing' + when: test_bare is undefined + register: result + + - name: assert bad conditional 'is undefined' did NOT run + assert: + that: + - result is skipped + + - name: test bare conditional + shell: echo 'testing' + when: test_bare + register: result + + - name: assert bare conditional ran + assert: + that: + - result is changed + - "result.stdout == 'testing'" + - "result.rc == 0" + + - name: test conditional using a variable + shell: echo 'testing' + when: test_bare_var == 123 + register: result + + - name: assert conditional using a variable ran + assert: + that: + - result is changed + - "result.stdout == 'testing'" + - "result.rc == 0" + + - name: test good conditional based on nested variables + shell: echo 'testing' + when: test_bare_nested_good + register: result + + - name: assert good conditional based on nested var ran + assert: + that: + - result is changed + - "result.stdout == 'testing'" + - "result.rc == 0" + + - name: test bad conditional based on nested variables + shell: echo 'testing' + when: test_bare_nested_bad + register: result + + - debug: var={{item}} + loop: + - bare + - result + - test_bare_nested_bad + + - name: assert that the bad nested conditional is skipped since 'bare' since 'string' template is resolved to 'false' + assert: + that: + - result is skipped + + when: bare|bool + + - name: assert that the bad nested conditional did run since non bare 'string' is untempalted but 'trueish' + assert: + that: + - result is skipped + when: not bare|bool + - result is changed + + - name: test bad conditional based on nested variables with bool filter + shell: echo 'testing' + when: test_bare_nested_bad|bool + register: result + + - name: assert that the bad nested conditional did NOT run as bool forces evaluation + assert: + that: + - result is skipped + + #----------------------------------------------------------------------- + # proper booleanification tests (issue #8629) + + - name: set fact to string 'false' + set_fact: bool_test1=false + + - name: set fact to string 'False' + set_fact: bool_test2=False + + - name: set fact to a proper boolean using complex args + set_fact: + bool_test3: false + + - name: "test boolean value 'false' string using 'when: var'" + command: echo 'hi' + when: bool_test1 + register: result + + - name: assert that the task did not run for 'false' + assert: + that: + - result is skipped + + - name: "test boolean value 'false' string using 'when: not var'" + command: echo 'hi' + when: not bool_test1 + register: result + + - name: assert that the task DID run for not 'false' + assert: + that: + - result is changed + + - name: "test boolean value of 'False' string using 'when: var'" + command: echo 'hi' + when: bool_test2 + register: result + + - name: assert that the task did not run for 'False' + assert: + that: + - result is skipped + + - name: "test boolean value 'False' string using 'when: not var'" + command: echo 'hi' + when: not bool_test2 + register: result + + - name: assert that the task DID run for not 'False' + assert: + that: + - result is changed + + - name: "test proper boolean value of complex arg using 'when: var'" + command: echo 'hi' + when: bool_test3 + register: result + + - name: assert that the task did not run for proper boolean false + assert: + that: + - result is skipped + + - name: "test proper boolean value of complex arg using 'when: not var'" + command: echo 'hi' + when: not bool_test3 + register: result + + - name: assert that the task DID run for not false + assert: + that: + - result is changed + + - set_fact: skipped_bad_attribute=True + - block: + - name: test a with_items loop using a variable with a missing attribute + debug: var=item + with_items: "{{cond_bad_attribute.results | default('')}}" + register: result + - set_fact: skipped_bad_attribute=False + - name: assert the task was skipped + assert: + that: + - skipped_bad_attribute + when: cond_bad_attribute is defined and 'results' in cond_bad_attribute + + - name: test a with_items loop skipping a single item + debug: var=item + with_items: "{{cond_list_of_items.results}}" + when: item != 'b' + register: result + + - debug: var=result + + - name: assert only a single item was skipped + assert: + that: + - result.results|length == 3 + - result.results[1].skipped + + - name: test complex templated condition + debug: msg="it works" + when: vars_file_var in things1|union([vars_file_var]) + + - name: test dict with invalid key is undefined + vars: + mydict: + a: foo + b: bar + debug: var=mydict['c'] + register: result + when: mydict['c'] is undefined + + - name: assert the task did not fail + assert: + that: + - result is success + + - name: test dict with invalid key does not run with conditional is defined + vars: + mydict: + a: foo + b: bar + debug: var=mydict['c'] + when: mydict['c'] is defined + register: result + + - name: assert the task was skipped + assert: + that: + - result is skipped + + - name: test list with invalid element does not run with conditional is defined + vars: + mylist: [] + debug: var=mylist[0] + when: mylist[0] is defined + register: result + + - name: assert the task was skipped + assert: + that: + - result is skipped + + - name: test list with invalid element is undefined + vars: + mylist: [] + debug: var=mylist[0] + when: mylist[0] is undefined + register: result + + - name: assert the task did not fail + assert: + that: + - result is success + + + - name: Deal with multivar equality + tags: ['leveldiff'] + when: not bare|bool + vars: + toplevel_hash: + hash_var_one: justastring + hash_var_two: something.with.dots + hash_var_three: something:with:colons + hash_var_four: something/with/slashes + hash_var_five: something with spaces + hash_var_six: yes + hash_var_seven: no + toplevel_var_one: justastring + toplevel_var_two: something.with.dots + toplevel_var_three: something:with:colons + toplevel_var_four: something/with/slashes + toplevel_var_five: something with spaces + toplevel_var_six: yes + toplevel_var_seven: no + block: + + - name: var subkey simple string + debug: + var: toplevel_hash.hash_var_one + register: sub + when: toplevel_hash.hash_var_one + + - name: toplevel simple string + debug: + var: toplevel_var_one + when: toplevel_var_one + register: top + ignore_errors: yes + + - name: ensure top and multi work same + assert: + that: + - top is not skipped + - sub is not skipped + - top is not failed + - sub is not failed + + - name: var subkey string with dots + debug: + var: toplevel_hash.hash_var_two + register: sub + when: toplevel_hash.hash_var_two + + - debug: + var: toplevel_var_two + when: toplevel_var_two + register: top + ignore_errors: yes + + - name: ensure top and multi work same + assert: + that: + - top is not skipped + - sub is not skipped + - top is not failed + - sub is not failed + + - name: var subkey string with dots + debug: + var: toplevel_hash.hash_var_three + register: sub + when: toplevel_hash.hash_var_three + + - debug: + var: toplevel_var_three + when: toplevel_var_three + register: top + ignore_errors: yes + + - name: ensure top and multi work same + assert: + that: + - top is not skipped + - sub is not skipped + - top is not failed + - sub is not failed + + - name: var subkey string with colon + debug: + var: toplevel_hash.hash_var_four + register: sub + when: toplevel_hash.hash_var_four + + - debug: + var: toplevel_var_four + when: toplevel_var_four + register: top + ignore_errors: yes + + - name: ensure top and multi work same + assert: + that: + - top is not skipped + - sub is not skipped + - top is not failed + - sub is not failed + + - name: var subkey string with spaces + debug: + var: toplevel_hash.hash_var_five + register: sub + when: toplevel_hash.hash_var_five + + - debug: + var: toplevel_var_five + when: toplevel_var_five + register: top + ignore_errors: yes + + - name: ensure top and multi work same + assert: + that: + - top is not skipped + - sub is not skipped + - top is not failed + - sub is not failed + + - name: var subkey with 'yes' value + debug: + var: toplevel_hash.hash_var_six + register: sub + when: toplevel_hash.hash_var_six + + - debug: + var: toplevel_var_six + register: top + when: toplevel_var_six + + - name: ensure top and multi work same + assert: + that: + - top is not skipped + - sub is not skipped + + - name: var subkey with 'no' value + debug: + var: toplevel_hash.hash_var_seven + register: sub + when: toplevel_hash.hash_var_seven + + - debug: + var: toplevel_var_seven + register: top + when: toplevel_var_seven + + - name: ensure top and multi work same + assert: + that: + - top is skipped + - sub is skipped + + - name: test that 'comparisson expression' item works with_items + assert: + that: + - item + with_items: + - 1 == 1 + + - name: test that 'comparisson expression' item works in loop + assert: + that: + - item + loop: + - 1 == 1 diff --git a/test/integration/targets/conditionals/runme.sh b/test/integration/targets/conditionals/runme.sh new file mode 100755 index 00000000000..c47bace549a --- /dev/null +++ b/test/integration/targets/conditionals/runme.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -eux + +ANSIBLE_CONDITIONAL_BARE_VARS=1 ansible-playbook -i inventory play.yml "$@" +ANSIBLE_CONDITIONAL_BARE_VARS=0 ansible-playbook -i inventory play.yml "$@" diff --git a/test/integration/targets/conditionals/tasks/main.yml b/test/integration/targets/conditionals/tasks/main.yml deleted file mode 100644 index 868cffe2afa..00000000000 --- a/test/integration/targets/conditionals/tasks/main.yml +++ /dev/null @@ -1,361 +0,0 @@ -# test code for conditional statements -# (c) 2014, James Cammarata - -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -- name: test conditional '==' - shell: echo 'testing' - when: 1 == 1 - register: result - -- name: assert conditional '==' ran - assert: - that: - - "result.changed == true" - - "result.stdout == 'testing'" - - "result.rc == 0" - -- name: test bad conditional '==' - shell: echo 'testing' - when: 0 == 1 - register: result - -- name: assert bad conditional '==' did NOT run - assert: - that: - - "result.skipped == true" - -- name: test conditional '!=' - shell: echo 'testing' - when: 0 != 1 - register: result - -- name: assert conditional '!=' ran - assert: - that: - - "result.changed == true" - - "result.stdout == 'testing'" - - "result.rc == 0" - -- name: test bad conditional '!=' - shell: echo 'testing' - when: 1 != 1 - register: result - -- name: assert bad conditional '!=' did NOT run - assert: - that: - - "result.skipped == true" - -- name: test conditional 'in' - shell: echo 'testing' - when: 1 in [1,2,3] - register: result - -- name: assert conditional 'in' ran - assert: - that: - - "result.changed == true" - - "result.stdout == 'testing'" - - "result.rc == 0" - -- name: test bad conditional 'in' - shell: echo 'testing' - when: 1 in [7,8,9] - register: result - -- name: assert bad conditional 'in' did NOT run - assert: - that: - - "result.skipped == true" - -- name: test conditional 'not in' - shell: echo 'testing' - when: 0 not in [1,2,3] - register: result - -- name: assert conditional 'not in' ran - assert: - that: - - "result.changed == true" - - "result.stdout == 'testing'" - - "result.rc == 0" - -- name: test bad conditional 'not in' - shell: echo 'testing' - when: 1 not in [1,2,3] - register: result - -- name: assert bad conditional 'not in' did NOT run - assert: - that: - - "result.skipped == true" - -- name: test conditional 'is defined' - shell: echo 'testing' - when: test_bare is defined - register: result - -- name: assert conditional 'is defined' ran - assert: - that: - - "result.changed == true" - - "result.stdout == 'testing'" - - "result.rc == 0" - -- name: test bad conditional 'is defined' - shell: echo 'testing' - when: foo_asdf_xyz is defined - register: result - -- name: assert bad conditional 'is defined' did NOT run - assert: - that: - - "result.skipped == true" - -- name: test conditional 'is not defined' - shell: echo 'testing' - when: foo_asdf_xyz is not defined - register: result - -- name: assert conditional 'is not defined' ran - assert: - that: - - "result.changed == true" - - "result.stdout == 'testing'" - - "result.rc == 0" - -- name: test bad conditional 'is not defined' - shell: echo 'testing' - when: test_bare is not defined - register: result - -- name: assert bad conditional 'is not defined' did NOT run - assert: - that: - - "result.skipped == true" - -- name: test bad conditional 'is undefined' - shell: echo 'testing' - when: test_bare is undefined - register: result - -- name: assert bad conditional 'is undefined' did NOT run - assert: - that: - - "result.skipped == true" - -- name: test bare conditional - shell: echo 'testing' - when: test_bare - register: result - -- name: assert bare conditional ran - assert: - that: - - "result.changed == true" - - "result.stdout == 'testing'" - - "result.rc == 0" - -- name: test conditional using a variable - shell: echo 'testing' - when: test_bare_var == 123 - register: result - -- name: assert conditional using a variable ran - assert: - that: - - "result.changed == true" - - "result.stdout == 'testing'" - - "result.rc == 0" - -- name: test good conditional based on nested variables - shell: echo 'testing' - when: test_bare_nested_good - register: result - -- name: assert good conditional based on nested var ran - assert: - that: - - "result.changed == true" - - "result.stdout == 'testing'" - - "result.rc == 0" - -- name: test bad conditional based on nested variables - shell: echo 'testing' - when: test_bare_nested_bad - register: result - -- name: assert that the bad nested conditional did NOT run - assert: - that: - - "result.skipped == true" - -#----------------------------------------------------------------------- -# proper booleanification tests (issue #8629) - -- name: set fact to string 'false' - set_fact: bool_test1=false - -- name: set fact to string 'False' - set_fact: bool_test2=False - -- name: set fact to a proper boolean using complex args - set_fact: - bool_test3: false - -- name: "test boolean value 'false' string using 'when: var'" - command: echo 'hi' - when: bool_test1 - register: result - -- name: assert that the task did not run for 'false' - assert: - that: - - "result.skipped == true" - -- name: "test boolean value 'false' string using 'when: not var'" - command: echo 'hi' - when: not bool_test1 - register: result - -- name: assert that the task DID run for not 'false' - assert: - that: - - "result.changed" - -- name: "test boolean value of 'False' string using 'when: var'" - command: echo 'hi' - when: bool_test2 - register: result - -- name: assert that the task did not run for 'False' - assert: - that: - - "result.skipped == true" - -- name: "test boolean value 'False' string using 'when: not var'" - command: echo 'hi' - when: not bool_test2 - register: result - -- name: assert that the task DID run for not 'False' - assert: - that: - - "result.changed" - -- name: "test proper boolean value of complex arg using 'when: var'" - command: echo 'hi' - when: bool_test3 - register: result - -- name: assert that the task did not run for proper boolean false - assert: - that: - - "result.skipped == true" - -- name: "test proper boolean value of complex arg using 'when: not var'" - command: echo 'hi' - when: not bool_test3 - register: result - -- name: assert that the task DID run for not false - assert: - that: - - "result.changed" - -- set_fact: skipped_bad_attribute=True -- block: - - name: test a with_items loop using a variable with a missing attribute - debug: var=item - with_items: "{{cond_bad_attribute.results | default('')}}" - register: result - - set_fact: skipped_bad_attribute=False - - name: assert the task was skipped - assert: - that: - - skipped_bad_attribute - when: cond_bad_attribute is defined and 'results' in cond_bad_attribute - -- name: test a with_items loop skipping a single item - debug: var=item - with_items: "{{cond_list_of_items.results}}" - when: item != 'b' - register: result - -- debug: var=result - -- name: assert only a single item was skipped - assert: - that: - - result.results|length == 3 - - result.results[1].skipped - -- name: test complex templated condition - debug: msg="it works" - when: vars_file_var in things1|union([vars_file_var]) - -- name: test dict with invalid key is undefined - vars: - mydict: - a: foo - b: bar - debug: var=mydict['c'] - register: result - when: mydict['c'] is undefined - -- name: assert the task did not fail - assert: - that: - - "result.failed == false" - -- name: test dict with invalid key does not run with conditional is defined - vars: - mydict: - a: foo - b: bar - debug: var=mydict['c'] - when: mydict['c'] is defined - register: result - -- name: assert the task was skipped - assert: - that: - - "result.skipped == true" - -- name: test list with invalid element does not run with conditional is defined - vars: - mylist: [] - debug: var=mylist[0] - when: mylist[0] is defined - register: result - -- name: assert the task was skipped - assert: - that: - - "result.skipped == true" - -- name: test list with invalid element is undefined - vars: - mylist: [] - debug: var=mylist[0] - when: mylist[0] is undefined - register: result - -- name: assert the task did not fail - assert: - that: - - "result.failed == false"