diff --git a/changelogs/fragments/12031-xml-predicate-xpath-nomatch.yml b/changelogs/fragments/12031-xml-predicate-xpath-nomatch.yml new file mode 100644 index 0000000000..51eddd4da8 --- /dev/null +++ b/changelogs/fragments/12031-xml-predicate-xpath-nomatch.yml @@ -0,0 +1,5 @@ +minor_changes: + - xml - add ``create_if_missing`` option to control whether a node is created when + ``value`` is set and ``xpath`` finds no match + (https://github.com/ansible-collections/community.general/issues/8730, + https://github.com/ansible-collections/community.general/pull/12031). diff --git a/plugins/modules/xml.py b/plugins/modules/xml.py index 8624135aeb..545246a3cf 100644 --- a/plugins/modules/xml.py +++ b/plugins/modules/xml.py @@ -144,6 +144,13 @@ options: - This parameter requires O(xpath) to be set. type: bool default: false + create_if_missing: + description: + - When using O(value) and the O(xpath) matches no nodes, create the node. + - When set to V(false), a no-match is silently ignored instead of creating a new node. + type: bool + default: true + version_added: "13.0.0" requirements: - lxml >= 2.3.0 notes: @@ -673,11 +680,13 @@ def ensure_xpath_exists(module, tree, xpath, namespaces): finish(module, tree, xpath, namespaces, changed) -def set_target_inner(module, tree, xpath, namespaces, attribute, value): +def set_target_inner(module, tree, xpath, namespaces, attribute, value, create_if_missing=True): changed = False try: if not is_node(tree, xpath, namespaces): + if not create_if_missing: + return changed changed = check_or_make_target(module, tree, xpath, namespaces) except Exception as e: missing_namespace = "" @@ -722,8 +731,8 @@ def set_target_inner(module, tree, xpath, namespaces, attribute, value): return changed -def set_target(module, tree, xpath, namespaces, attribute, value): - changed = set_target_inner(module, tree, xpath, namespaces, attribute, value) +def set_target(module, tree, xpath, namespaces, attribute, value, create_if_missing): + changed = set_target_inner(module, tree, xpath, namespaces, attribute, value, create_if_missing) finish(module, tree, xpath, namespaces, changed) @@ -895,6 +904,7 @@ def main(): huge_tree=dict(type="bool", default=False), insertbefore=dict(type="bool", default=False), insertafter=dict(type="bool", default=False), + create_if_missing=dict(type="bool", default=True), ), supports_check_mode=True, required_by=dict( @@ -939,6 +949,7 @@ def main(): huge_tree = module.params["huge_tree"] insertbefore = module.params["insertbefore"] insertafter = module.params["insertafter"] + create_if_missing = module.params["create_if_missing"] # Check if we have lxml 2.3.0 or newer installed if not HAS_LXML: @@ -1014,7 +1025,7 @@ def main(): # Is the xpath target an attribute selector? if value is not None: - set_target(module, doc, xpath, namespaces, attribute, value) + set_target(module, doc, xpath, namespaces, attribute, value, create_if_missing) # If an xpath was provided, we need to do something with the data if xpath is not None: diff --git a/tests/integration/targets/xml/fixtures/ansible-xml-items.xml b/tests/integration/targets/xml/fixtures/ansible-xml-items.xml new file mode 100644 index 0000000000..f0d698d8f0 --- /dev/null +++ b/tests/integration/targets/xml/fixtures/ansible-xml-items.xml @@ -0,0 +1,8 @@ + + + + value_a + value_a + value_b + + diff --git a/tests/integration/targets/xml/fixtures/ansible-xml-items.xml.license b/tests/integration/targets/xml/fixtures/ansible-xml-items.xml.license new file mode 100644 index 0000000000..edff8c7685 --- /dev/null +++ b/tests/integration/targets/xml/fixtures/ansible-xml-items.xml.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/integration/targets/xml/results/test-set-element-value-predicate.xml b/tests/integration/targets/xml/results/test-set-element-value-predicate.xml new file mode 100644 index 0000000000..ae4ae99a75 --- /dev/null +++ b/tests/integration/targets/xml/results/test-set-element-value-predicate.xml @@ -0,0 +1,8 @@ + + + + value_c + value_c + value_b + + diff --git a/tests/integration/targets/xml/results/test-set-element-value-predicate.xml.license b/tests/integration/targets/xml/results/test-set-element-value-predicate.xml.license new file mode 100644 index 0000000000..edff8c7685 --- /dev/null +++ b/tests/integration/targets/xml/results/test-set-element-value-predicate.xml.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/integration/targets/xml/tasks/main.yml b/tests/integration/targets/xml/tasks/main.yml index 2dc633a25d..e69a28e578 100644 --- a/tests/integration/targets/xml/tasks/main.yml +++ b/tests/integration/targets/xml/tasks/main.yml @@ -66,6 +66,7 @@ - include_tasks: test-set-children-elements-value.yml - include_tasks: test-set-element-value.yml - include_tasks: test-set-element-value-empty.yml + - include_tasks: test-set-element-value-predicate.yml - include_tasks: test-pretty-print.yml - include_tasks: test-pretty-print-only.yml - include_tasks: test-add-namespaced-children-elements.yml diff --git a/tests/integration/targets/xml/tasks/test-set-element-value-predicate.yml b/tests/integration/targets/xml/tasks/test-set-element-value-predicate.yml new file mode 100644 index 0000000000..90f2bdbc5c --- /dev/null +++ b/tests/integration/targets/xml/tasks/test-set-element-value-predicate.yml @@ -0,0 +1,96 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# Tests for https://github.com/ansible-collections/community.general/issues/8730 + +# --- create_if_missing=false: predicate no-match must be a silent no-op --- + +- name: Setup test fixture + copy: + src: fixtures/ansible-xml-items.xml + dest: /tmp/ansible-xml-items.xml + +- name: Update items via predicate (create_if_missing=false, first run - matches) + xml: + path: /tmp/ansible-xml-items.xml + xpath: /business/items/item[text()='value_a'] + value: value_c + create_if_missing: false + register: set_predicate_first_run + +- name: Update items via predicate (create_if_missing=false, second run - predicate no longer matches) + xml: + path: /tmp/ansible-xml-items.xml + xpath: /business/items/item[text()='value_a'] + value: value_c + create_if_missing: false + register: set_predicate_second_run + +- name: Add trailing newline + shell: echo "" >> /tmp/ansible-xml-items.xml + +- name: Compare to expected result + copy: + src: results/test-set-element-value-predicate.xml + dest: /tmp/ansible-xml-items.xml + check_mode: true + diff: true + register: comparison + +- name: Assert create_if_missing=false with predicate no-match is a no-op + assert: + that: + - set_predicate_first_run is changed + - set_predicate_second_run is not changed + - comparison is not changed # no spurious elements created + +# --- create_if_missing=false: non-existent simple path must also be a no-op --- + +- name: Setup test fixture + copy: + src: fixtures/ansible-xml-items.xml + dest: /tmp/ansible-xml-items.xml + +- name: Set value on non-existent element (create_if_missing=false) + xml: + path: /tmp/ansible-xml-items.xml + xpath: /business/items/nonexistent + value: something + create_if_missing: false + register: set_nonexistent + +- name: Assert create_if_missing=false with missing simple path is a no-op + assert: + that: + - set_nonexistent is not changed + +# --- create_if_missing=true (default): no-match must create the node --- + +- name: Setup test fixture + copy: + src: fixtures/ansible-xml-items.xml + dest: /tmp/ansible-xml-items.xml + +- name: Set value on non-existent element (create_if_missing=true, should create) + xml: + path: /tmp/ansible-xml-items.xml + xpath: /business/items/item[text()='value_z'] + value: value_z + create_if_missing: true + register: set_predicate_create + +- name: Set same value again (create_if_missing=true, should be idempotent) + xml: + path: /tmp/ansible-xml-items.xml + xpath: /business/items/item[text()='value_z'] + value: value_z + create_if_missing: true + register: set_predicate_create_again + +- name: Assert create_if_missing=true creates the node and is then idempotent + assert: + that: + - set_predicate_create is changed + - set_predicate_create_again is not changed