diff --git a/changelogs/fragments/72944-include_vars-add-hash_behaviour-option.yml b/changelogs/fragments/72944-include_vars-add-hash_behaviour-option.yml new file mode 100644 index 00000000000..8d00487434b --- /dev/null +++ b/changelogs/fragments/72944-include_vars-add-hash_behaviour-option.yml @@ -0,0 +1,2 @@ +minor_changes: + - include_vars - add ``hash_behaviour`` option (https://github.com/ansible/ansible/pull/72944). diff --git a/lib/ansible/modules/include_vars.py b/lib/ansible/modules/include_vars.py index 019d74680af..b1df96413c7 100644 --- a/lib/ansible/modules/include_vars.py +++ b/lib/ansible/modules/include_vars.py @@ -70,6 +70,14 @@ options: type: bool default: no version_added: "2.7" + hash_behaviour: + description: + - If set to C(merge), merges existing hash variables instead of overwriting them. + - If omitted C(null), the behavior falls back to the global I(hash_behaviour) configuration. + default: null + type: str + choices: ["replace", "merge"] + version_added: "2.12" free-form: description: - This module allows you to specify the 'file' option directly without any other options. diff --git a/lib/ansible/plugins/action/include_vars.py b/lib/ansible/plugins/action/include_vars.py index 07234537ac8..aec0f004437 100644 --- a/lib/ansible/plugins/action/include_vars.py +++ b/lib/ansible/plugins/action/include_vars.py @@ -7,10 +7,12 @@ __metaclass__ = type from os import path, walk import re +import ansible.constants as C from ansible.errors import AnsibleError from ansible.module_utils.six import string_types from ansible.module_utils._text import to_native, to_text from ansible.plugins.action import ActionBase +from ansible.utils.vars import combine_vars class ActionModule(ActionBase): @@ -20,7 +22,7 @@ class ActionModule(ActionBase): VALID_FILE_EXTENSIONS = ['yaml', 'yml', 'json'] VALID_DIR_ARGUMENTS = ['dir', 'depth', 'files_matching', 'ignore_files', 'extensions', 'ignore_unknown_extensions'] VALID_FILE_ARGUMENTS = ['file', '_raw_params'] - VALID_ALL = ['name'] + VALID_ALL = ['name', 'hash_behaviour'] def _set_dir_defaults(self): if not self.depth: @@ -46,6 +48,7 @@ class ActionModule(ActionBase): def _set_args(self): """ Set instance variables based on the arguments that were passed """ + self.hash_behaviour = self._task.args.get('hash_behaviour', None) self.return_results_as_name = self._task.args.get('name', None) self.source_dir = self._task.args.get('dir', None) self.source_file = self._task.args.get('file', None) @@ -135,6 +138,11 @@ class ActionModule(ActionBase): if failed: result['failed'] = failed result['message'] = err_msg + elif self.hash_behaviour is not None and self.hash_behaviour != C.DEFAULT_HASH_BEHAVIOUR: + merge_hashes = self.hash_behaviour == 'merge' + for key, value in results.items(): + old_value = task_vars.get(key, None) + results[key] = combine_vars(old_value, value, merge=merge_hashes) result['ansible_included_var_files'] = self.included_files result['ansible_facts'] = results diff --git a/lib/ansible/utils/vars.py b/lib/ansible/utils/vars.py index 17ef2df9de8..7545f1d4358 100644 --- a/lib/ansible/utils/vars.py +++ b/lib/ansible/utils/vars.py @@ -79,12 +79,12 @@ def _validate_mutable_mappings(a, b): ) -def combine_vars(a, b): +def combine_vars(a, b, merge=None): """ Return a copy of dictionaries of variables based on configured hash behavior """ - if C.DEFAULT_HASH_BEHAVIOUR == "merge": + if merge or merge is None and C.DEFAULT_HASH_BEHAVIOUR == "merge": return merge_hash(a, b) else: # HASH_BEHAVIOUR == 'replace' diff --git a/test/integration/targets/include_vars/tasks/main.yml b/test/integration/targets/include_vars/tasks/main.yml index 79f03d6e76b..db15ba3c5db 100644 --- a/test/integration/targets/include_vars/tasks/main.yml +++ b/test/integration/targets/include_vars/tasks/main.yml @@ -166,6 +166,48 @@ - "'my_custom_service' == service_name_fqcn" - "'my_custom_service' == service_name_tmpl_fqcn" +- name: Include a vars file with a hash variable + include_vars: + file: vars2/hashes/hash1.yml + +- name: Verify the hash variable + assert: + that: + - "{{ config | length }} == 3" + - "config.key0 == 0" + - "config.key1 == 0" + - "{{ config.key2 | length }} == 1" + - "config.key2.a == 21" + +- name: Include the second file to merge the hash variable + include_vars: + file: vars2/hashes/hash2.yml + hash_behaviour: merge + +- name: Verify that the hash is merged + assert: + that: + - "{{ config | length }} == 4" + - "config.key0 == 0" + - "config.key1 == 1" + - "{{ config.key2 | length }} == 2" + - "config.key2.a == 21" + - "config.key2.b == 22" + - "config.key3 == 3" + +- name: Include the second file again without hash_behaviour option + include_vars: + file: vars2/hashes/hash2.yml + +- name: Verify that the properties from the first file is cleared + assert: + that: + - "{{ config | length }} == 3" + - "config.key1 == 1" + - "{{ config.key2 | length }} == 1" + - "config.key2.b == 22" + - "config.key3 == 3" + - include_vars: file: no_auto_unsafe.yml register: baz diff --git a/test/integration/targets/include_vars/vars2/hashes/hash1.yml b/test/integration/targets/include_vars/vars2/hashes/hash1.yml new file mode 100644 index 00000000000..b0706f8f70f --- /dev/null +++ b/test/integration/targets/include_vars/vars2/hashes/hash1.yml @@ -0,0 +1,5 @@ +--- +config: + key0: 0 + key1: 0 + key2: { a: 21 } diff --git a/test/integration/targets/include_vars/vars2/hashes/hash2.yml b/test/integration/targets/include_vars/vars2/hashes/hash2.yml new file mode 100644 index 00000000000..1f2a9636626 --- /dev/null +++ b/test/integration/targets/include_vars/vars2/hashes/hash2.yml @@ -0,0 +1,5 @@ +--- +config: + key1: 1 + key2: { b: 22 } + key3: 3