From c309abc02f54ce313c71fac111c070f21940eeb8 Mon Sep 17 00:00:00 2001 From: Robert Marklund Date: Mon, 17 Jun 2024 11:28:09 +0200 Subject: [PATCH 1/2] patch: make tests work again changed need to be returned for "add" and "move" also Signed-off-by: Robert Marklund --- json_patch.py | 3 +-- test_json_patch.py | 28 +++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/json_patch.py b/json_patch.py index 1e65f5b..f90ef55 100644 --- a/json_patch.py +++ b/json_patch.py @@ -4,7 +4,6 @@ # Copyright: (c) 2019, Joey Espinosa # Copyright: (c) 2019, Ansible Project # MIT License (https://opensource.org/licenses/MIT) - from __future__ import (absolute_import, division, print_function) __metaclass__ = type @@ -339,7 +338,7 @@ def patch(self): # attach object to patch operation (helpful for recursion) patch['obj'] = self.obj new_obj, changed, tested = getattr(self, op)(**patch) - if changed or op == "remove": # 'remove' will fail if we don't actually remove anything + if changed or op in ["remove", "add", "move"]: # 'remove', 'add' and 'move' will fail if we don't actually remove anything modified = bool(changed) if modified is True: self.obj = new_obj diff --git a/test_json_patch.py b/test_json_patch.py index 961bc35..70a058d 100644 --- a/test_json_patch.py +++ b/test_json_patch.py @@ -2,7 +2,11 @@ import json import pytest -from ansible.modules.files.json_patch import JSONPatcher, PathError +# Will work both if installed in ansible and in the local folder +try: + from ansible.modules.files.json_patch import JSONPatcher, PathError +except ImportError: + from json_patch import JSONPatcher, PathError __metaclass__ = type @@ -304,6 +308,28 @@ def test_op_test_wildcard(): assert tested is True +def test_op_test_wildcard_list(): + """Should find an element in the 'baz' list with the matching value.""" + patches = [ + {"op": "test", "path": "/2/baz/*/foo", "value": "grapes"} + ] + jp = JSONPatcher(sample_json, *patches) + changed, tested = jp.patch() + assert changed is None + assert tested is True + + +def test_op_test_wildcard_list_not_found(): + """Should find an element in the 'baz' list with the matching value.""" + patches = [ + {"op": "test", "path": "/2/baz/*/foo", "value": "grapes"} + ] + jp = JSONPatcher(sample_json, *patches) + changed, tested = jp.patch() + assert changed is None + assert tested is True + + def test_op_test_wildcard_not_found(): """Should return False on not finding an element with the given value.""" patches = [ From 209e18f91738d0c6015a02e73b696f80f27f2426 Mon Sep 17 00:00:00 2001 From: Robert Marklund Date: Thu, 20 Jun 2024 16:18:57 +0200 Subject: [PATCH 2/2] make test operator output object and result This so test can be used to append only if it does not exist from ansible Added example folder with examples and instructions Signed-off-by: Robert Marklund --- README.md | 13 +++++- examples/README.md | 21 ++++++++++ examples/append-if-not-exist.yaml | 48 ++++++++++++++++++++++ json_patch.py | 16 +++++++- test_json_patch.py | 67 +++++++++++++++++++++---------- 5 files changed, 140 insertions(+), 25 deletions(-) create mode 100644 examples/README.md create mode 100644 examples/append-if-not-exist.yaml diff --git a/README.md b/README.md index a0c8377..e81b58b 100644 --- a/README.md +++ b/README.md @@ -283,10 +283,19 @@ There are two special notations: value: "The Last Samurai" ``` +### Examples +For more examples of how to use this module see `exmaples` folder. + ## Testing -Testing is simple if you have pytest installed: + +### create virtual environment +``` bash +python3 -m venv .venv +.venv/bin/python3 -m pip install -U ansible pytest +``` +Testing is simple once you have pytest installed: ```bash -pytest +.venv/bin/py.test -v ``` Note you may have to copy the `json_patch.py` file into your Ansible installation's `lib/ansible/modules/files/` dir. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..5bc1d37 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,21 @@ +# Example playbooks + +## Setup virtual environment +``` bash +python3 -m venv .venv +.venv/bin/python3 -m pip install -U ansible pytest +``` +## Setup to run ansible +``` bash +mkdir -p library +ln -s ../json_patch.py library/json_patch.py +``` +## Examples + +### append-if-not-exist +Shows an example of how to test and add values to a list if they are not already there. + +``` bash +.venv/bin/ansible-playbook -vvv -i local, --connection=local examples/append-if-not-exist.yaml + +``` diff --git a/examples/append-if-not-exist.yaml b/examples/append-if-not-exist.yaml new file mode 100644 index 0000000..14fa9c5 --- /dev/null +++ b/examples/append-if-not-exist.yaml @@ -0,0 +1,48 @@ +--- +- name: Append value to list if it does not already exist + hosts: local + gather_facts: false + become: false + vars: + jsonfile: "/tmp/example-append-if-not-exist.json" + json_keys: + - key: "/foo/-" + value: "bar" + test: "/foo/*" + tasks: + - name: "Ensure json {{jsonfile}} file exists" + ansible.builtin.copy: + content: '{"foo": []}' + dest: "{{jsonfile}}" + force: true + + - name: Check if key exists + json_patch: + src: "{{jsonfile}}" + operations: + - op: test + path: "{{ item.test }}" + value: "{{ item.value }}" + loop: "{{json_keys}}" + register: foo_result + when: + - item.test is defined + + - name: debug msgs from the results of test + debug: + msg: "{{ foo_result.results | selectattr('item.key', 'equalto', item.key) | list | map(attribute='tested') | first }}" + loop: "{{json_keys}}" + when: + - item.test is defined + + - name: "append key/values to {{jsonfile}}" + json_patch: + src: "{{jsonfile}}" + pretty: true + operations: + - op: add + path: "{{ item.key }}" + value: "{{ item.value }}" + loop: "{{json_keys}}" + when: + - item.test is not defined or (item.test is defined and not foo_result.results | selectattr('item.key', 'equalto', item.key) | list | map(attribute='tested') | first) diff --git a/json_patch.py b/json_patch.py index f90ef55..1ad1137 100644 --- a/json_patch.py +++ b/json_patch.py @@ -245,7 +245,9 @@ def run(self): changed, tested = self.patcher.patch() result = {'changed': changed} if tested is not None: - result['tested'] = tested + print(tested) + result['tested'] = tested[0] + result['object'] = tested[1] if result['changed']: # let's write the changes dump_kwargs = {} if self.pretty_print: @@ -343,7 +345,11 @@ def patch(self): if modified is True: self.obj = new_obj if tested is not None: - test_result = False if test_result is False else tested # one false test fails everything + print(f"hej {tested}") + if test_result is None: + test_result = (tested, new_obj) # one false test fails everything + else: + test_result += (tested, new_obj) return modified, test_result def _get(self, path, obj, **discard): @@ -519,8 +525,14 @@ def test(self, path, value, obj, **discard): next_obj = obj for idx, elem in enumerate(elements): if elem == "*": # wildcard + # print(f"elem==*: {next_obj} {type(next_obj)}") if not isinstance(next_obj, list): return obj, None, False + # last element and test is '*' then just check if value is in list + if '/'.join(elements[(idx + 1):]) == '' and value in next_obj: + return obj, None, True + elif '/'.join(elements[(idx + 1):]) == '' and value not in next_obj: + return obj, None, False for sub_obj in next_obj: dummy, _, found = self.test('/'.join(elements[(idx + 1):]), value, sub_obj) if found: diff --git a/test_json_patch.py b/test_json_patch.py index 70a058d..b86607a 100644 --- a/test_json_patch.py +++ b/test_json_patch.py @@ -17,7 +17,9 @@ {"baz": [{"foo": "apples", "bar": "oranges"}, {"foo": "grapes", "bar": "oranges"}, {"foo": "bananas", "bar": "potatoes"}], - "enabled": False}]) + "enabled": False}, + {"bar": ["foo", "bar", "baz", "baza", "baza", "bazb"], "enabled": False} + ]) # OPERATION: ADD @@ -249,7 +251,7 @@ def test_op_test_string_equal(): jp = JSONPatcher(sample_json, *patches) changed, tested = jp.patch() assert changed is None - assert tested is True + assert tested[0] is True def test_op_test_string_unequal(): @@ -260,7 +262,7 @@ def test_op_test_string_unequal(): jp = JSONPatcher(sample_json, *patches) changed, tested = jp.patch() assert changed is None - assert tested is False + assert tested[0] is False def test_op_test_number_equal(): @@ -271,7 +273,7 @@ def test_op_test_number_equal(): jp = JSONPatcher(sample_json, *patches) changed, tested = jp.patch() assert changed is None - assert tested is True + assert tested[0] is True def test_op_test_number_unequal(): @@ -282,7 +284,7 @@ def test_op_test_number_unequal(): jp = JSONPatcher(sample_json, *patches) changed, tested = jp.patch() assert changed is None - assert tested is False + assert tested[0] is False def test_op_test_list_equal(): @@ -294,7 +296,7 @@ def test_op_test_list_equal(): jp = JSONPatcher(sample_json, *patches) changed, tested = jp.patch() assert changed is True - assert tested is True + assert tested[0] is True def test_op_test_wildcard(): @@ -305,40 +307,62 @@ def test_op_test_wildcard(): jp = JSONPatcher(sample_json, *patches) changed, tested = jp.patch() assert changed is None - assert tested is True + assert tested[0] is True -def test_op_test_wildcard_list(): - """Should find an element in the 'baz' list with the matching value.""" +def test_op_test_wildcard_not_found(): + """Should return False on not finding an element with the given value.""" patches = [ - {"op": "test", "path": "/2/baz/*/foo", "value": "grapes"} + {"op": "test", "path": "/2/baz/*/bar", "value": "rocks"} ] jp = JSONPatcher(sample_json, *patches) changed, tested = jp.patch() assert changed is None - assert tested is True + assert tested[0] is False -def test_op_test_wildcard_list_not_found(): - """Should find an element in the 'baz' list with the matching value.""" +def test_op_test_wildcard_list_first(): + """Should find an element in the 'bar' list with the matching value.""" patches = [ - {"op": "test", "path": "/2/baz/*/foo", "value": "grapes"} + {"op": "test", "path": "/3/bar/*", "value": "foo"} ] jp = JSONPatcher(sample_json, *patches) changed, tested = jp.patch() assert changed is None - assert tested is True + assert tested[0] is True -def test_op_test_wildcard_not_found(): - """Should return False on not finding an element with the given value.""" +def test_op_test_wildcard_list_middle(): + """Should find an element in the 'bar' list with the matching value.""" patches = [ - {"op": "test", "path": "/2/baz/*/bar", "value": "rocks"} + {"op": "test", "path": "/3/bar/*", "value": "baz"} + ] + jp = JSONPatcher(sample_json, *patches) + changed, tested = jp.patch() + assert changed is None + assert tested[0] is True + + +def test_op_test_wildcard_list_last(): + """Should find an element in the 'bar' list with the matching value.""" + patches = [ + {"op": "test", "path": "/3/bar/*", "value": "bazb"} + ] + jp = JSONPatcher(sample_json, *patches) + changed, tested = jp.patch() + assert changed is None + assert tested[0] is True + + +def test_op_test_wildcard_list_not_found(): + """Should not find an element in the 'bar' list with the matching value.""" + patches = [ + {"op": "test", "path": "/3/bar/*", "value": "no_foo"} ] jp = JSONPatcher(sample_json, *patches) changed, tested = jp.patch() assert changed is None - assert tested is False + assert tested[0] is False def test_op_test_multiple_tests(): @@ -350,7 +374,8 @@ def test_op_test_multiple_tests(): jp = JSONPatcher(sample_json, *patches) changed, tested = jp.patch() assert changed is None - assert tested is False + assert all(test for test in tested) is False + #assert tested is False def test_op_test_nonexistent_member(): @@ -361,4 +386,4 @@ def test_op_test_nonexistent_member(): jp = JSONPatcher(sample_json, *patches) changed, tested = jp.patch() assert changed is None - assert tested is False + assert tested[0] is False