From a096cd08c59f363289d53c384d7b588d2a622456 Mon Sep 17 00:00:00 2001 From: Michael Cassaniti Date: Sun, 24 Nov 2019 06:31:35 +1100 Subject: [PATCH] docker_swarm_service: Sort lists when checking for changes (#63887) * docker_swarm_service: Sort lists when checking for changes When two lists are checked for changes in this module, the lists are reported changed when the order of the items is different. This PR resolves this issue. * docker_swarm_service: Minor typo fix * docker_swarm_service: Another minor typo * docker_swarm_service: Should use sorted(), not sort() * docker_swarm_service: Sort lists of dictionaries * docker_swarm_service: Fix style issues in tests * docker_swarm_service: Updates to integration tests * docker_swarm_service: Casting string types within lists when comparing * docker_swarm_service: Special handling of unordered networks with ordered aliases * docker_swarm_service: Sorting network lists * docker_swarm_serivce: Better unit test code coverage for lists and networks * docker_swarm_service: Fixed coding style for sanity tests * docker_swarm_service: More coding style fixes * docker_swarm_service: Ignoring test for Python < 3 * docker_swarm_service: Update to version info check for backwards compatibility * docker_swarm_service: Added change fragment #63887 * docker_swarm_service: Better handling of missing sort key for dictionary of lists * docker_swarm_service: Preventing sorts from modifying in-place Co-Authored-By: Felix Fontein * docker_swarm_service: Removed spurious import in test * docker_swarm_service: Preventing sorts from modifying more data in-place Co-Authored-By: Felix Fontein --- ...rvice-sort-lists-when-checking-changes.yml | 2 + .../cloud/docker/docker_swarm_service.py | 103 ++++++-- .../tasks/tests/configs.yml | 23 +- .../tasks/tests/mounts.yml | 24 +- .../tasks/tests/networks.yml | 76 +++--- .../tasks/tests/options.yml | 70 +++-- .../tasks/tests/placement.yml | 32 ++- .../tasks/tests/secrets.yml | 23 +- .../cloud/docker/test_docker_swarm_service.py | 246 ++++++++++++++---- 9 files changed, 467 insertions(+), 132 deletions(-) create mode 100644 changelogs/fragments/63887-docker_swarm_service-sort-lists-when-checking-changes.yml diff --git a/changelogs/fragments/63887-docker_swarm_service-sort-lists-when-checking-changes.yml b/changelogs/fragments/63887-docker_swarm_service-sort-lists-when-checking-changes.yml new file mode 100644 index 00000000000..ebdf315b09f --- /dev/null +++ b/changelogs/fragments/63887-docker_swarm_service-sort-lists-when-checking-changes.yml @@ -0,0 +1,2 @@ +minor_changes: + - "docker_swarm_service - Sort lists when checking for changes." diff --git a/lib/ansible/modules/cloud/docker/docker_swarm_service.py b/lib/ansible/modules/cloud/docker/docker_swarm_service.py index 833d245bc17..7f0316019da 100644 --- a/lib/ansible/modules/cloud/docker/docker_swarm_service.py +++ b/lib/ansible/modules/cloud/docker/docker_swarm_service.py @@ -1244,19 +1244,59 @@ def has_dict_changed(new_dict, old_dict): return False -def has_list_changed(new_list, old_list): +def has_list_changed(new_list, old_list, sort_lists=True, sort_key=None): """ - Check two lists has differences. + Check two lists have differences. Sort lists by default. """ + + def sort_list(unsorted_list): + """ + Sort a given list. + The list may contain dictionaries, so use the sort key to handle them. + """ + + if unsorted_list and isinstance(unsorted_list[0], dict): + if not sort_key: + raise Exception( + 'A sort key was not specified when sorting list' + ) + else: + return sorted(unsorted_list, key=lambda k: k[sort_key]) + + # Either the list is empty or does not contain dictionaries + try: + return sorted(unsorted_list) + except TypeError: + return unsorted_list + if new_list is None: return False old_list = old_list or [] if len(new_list) != len(old_list): return True - for new_item, old_item in zip(new_list, old_list): + + if sort_lists: + zip_data = zip(sort_list(new_list), sort_list(old_list)) + else: + zip_data = zip(new_list, old_list) + for new_item, old_item in zip_data: is_same_type = type(new_item) == type(old_item) if not is_same_type: - return True + if isinstance(new_item, string_types) and isinstance(old_item, string_types): + # Even though the types are different between these items, + # they are both strings. Try matching on the same string type. + try: + new_item_type = type(new_item) + old_item_casted = new_item_type(old_item) + if new_item != old_item_casted: + return True + else: + continue + except UnicodeEncodeError: + # Fallback to assuming the strings are different + return True + else: + return True if isinstance(new_item, dict): if has_dict_changed(new_item, old_item): return True @@ -1266,6 +1306,35 @@ def has_list_changed(new_list, old_list): return False +def have_networks_changed(new_networks, old_networks): + """Special case list checking for networks to sort aliases""" + + if new_networks is None: + return False + old_networks = old_networks or [] + if len(new_networks) != len(old_networks): + return True + + zip_data = zip( + sorted(new_networks, key=lambda k: k['id']), + sorted(old_networks, key=lambda k: k['id']) + ) + + for new_item, old_item in zip_data: + new_item = dict(new_item) + old_item = dict(old_item) + # Sort the aliases + if 'aliases' in new_item: + new_item['aliases'] = sorted(new_item['aliases'] or []) + if 'aliases' in old_item: + old_item['aliases'] = sorted(old_item['aliases'] or []) + + if has_dict_changed(new_item, old_item): + return True + + return False + + class DockerService(DockerBaseClass): def __init__(self, docker_api_version, docker_py_version): super(DockerService, self).__init__() @@ -1761,7 +1830,7 @@ class DockerService(DockerBaseClass): force_update = False if self.endpoint_mode is not None and self.endpoint_mode != os.endpoint_mode: differences.add('endpoint_mode', parameter=self.endpoint_mode, active=os.endpoint_mode) - if self.env is not None and self.env != (os.env or []): + if has_list_changed(self.env, os.env): differences.add('env', parameter=self.env, active=os.env) if self.log_driver is not None and self.log_driver != os.log_driver: differences.add('log_driver', parameter=self.log_driver, active=os.log_driver) @@ -1770,26 +1839,26 @@ class DockerService(DockerBaseClass): if self.mode != os.mode: needs_rebuild = True differences.add('mode', parameter=self.mode, active=os.mode) - if has_list_changed(self.mounts, os.mounts): + if has_list_changed(self.mounts, os.mounts, sort_key='target'): differences.add('mounts', parameter=self.mounts, active=os.mounts) - if has_list_changed(self.configs, os.configs): + if has_list_changed(self.configs, os.configs, sort_key='config_name'): differences.add('configs', parameter=self.configs, active=os.configs) - if has_list_changed(self.secrets, os.secrets): + if has_list_changed(self.secrets, os.secrets, sort_key='secret_name'): differences.add('secrets', parameter=self.secrets, active=os.secrets) - if has_list_changed(self.networks, os.networks): + if have_networks_changed(self.networks, os.networks): differences.add('networks', parameter=self.networks, active=os.networks) needs_rebuild = not self.can_update_networks if self.replicas != os.replicas: differences.add('replicas', parameter=self.replicas, active=os.replicas) - if self.command is not None and self.command != (os.command or []): + if has_list_changed(self.command, os.command, sort_lists=False): differences.add('command', parameter=self.command, active=os.command) - if self.args is not None and self.args != (os.args or []): + if has_list_changed(self.args, os.args, sort_lists=False): differences.add('args', parameter=self.args, active=os.args) - if self.constraints is not None and self.constraints != (os.constraints or []): + if has_list_changed(self.constraints, os.constraints): differences.add('constraints', parameter=self.constraints, active=os.constraints) - if self.placement_preferences is not None and self.placement_preferences != (os.placement_preferences or []): + if has_list_changed(self.placement_preferences, os.placement_preferences, sort_lists=False): differences.add('placement_preferences', parameter=self.placement_preferences, active=os.placement_preferences) - if self.groups is not None and self.groups != (os.groups or []): + if has_list_changed(self.groups, os.groups): differences.add('groups', parameter=self.groups, active=os.groups) if self.labels is not None and self.labels != (os.labels or {}): differences.add('labels', parameter=self.labels, active=os.labels) @@ -1838,11 +1907,11 @@ class DockerService(DockerBaseClass): differences.add('image', parameter=self.image, active=change) if self.user and self.user != os.user: differences.add('user', parameter=self.user, active=os.user) - if self.dns is not None and self.dns != (os.dns or []): + if has_list_changed(self.dns, os.dns, sort_lists=False): differences.add('dns', parameter=self.dns, active=os.dns) - if self.dns_search is not None and self.dns_search != (os.dns_search or []): + if has_list_changed(self.dns_search, os.dns_search, sort_lists=False): differences.add('dns_search', parameter=self.dns_search, active=os.dns_search) - if self.dns_options is not None and self.dns_options != (os.dns_options or []): + if has_list_changed(self.dns_options, os.dns_options): differences.add('dns_options', parameter=self.dns_options, active=os.dns_options) if self.has_healthcheck_changed(os): differences.add('healthcheck', parameter=self.healthcheck, active=os.healthcheck) diff --git a/test/integration/targets/docker_swarm_service/tasks/tests/configs.yml b/test/integration/targets/docker_swarm_service/tasks/tests/configs.yml index 7962cf11b0a..db474ba0b73 100644 --- a/test/integration/targets/docker_swarm_service/tasks/tests/configs.yml +++ b/test/integration/targets/docker_swarm_service/tasks/tests/configs.yml @@ -97,6 +97,20 @@ register: configs_5 ignore_errors: yes +- name: configs (add idempotency no id and re-ordered) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + configs: + - config_name: "{{ config_name_2 }}" + filename: "/tmp/{{ config_name_2 }}.txt" + - config_name: "{{ config_name_1 }}" + filename: "/tmp/{{ config_name_1 }}.txt" + register: configs_6 + ignore_errors: yes + - name: configs (empty) docker_swarm_service: name: "{{ service_name }}" @@ -104,7 +118,7 @@ resolve_image: no command: '/bin/sh -v -c "sleep 10m"' configs: [] - register: configs_6 + register: configs_7 ignore_errors: yes - name: configs (empty idempotency) @@ -114,7 +128,7 @@ resolve_image: no command: '/bin/sh -v -c "sleep 10m"' configs: [] - register: configs_7 + register: configs_8 ignore_errors: yes - name: cleanup @@ -130,8 +144,9 @@ - configs_3 is changed - configs_4 is not changed - configs_5 is not changed - - configs_6 is changed - - configs_7 is not changed + - configs_6 is not changed + - configs_7 is changed + - configs_8 is not changed when: docker_api_version is version('1.30', '>=') and docker_py_version is version('2.6.0', '>=') - assert: diff --git a/test/integration/targets/docker_swarm_service/tasks/tests/mounts.yml b/test/integration/targets/docker_swarm_service/tasks/tests/mounts.yml index 0e204815dca..08ffc927583 100644 --- a/test/integration/targets/docker_swarm_service/tasks/tests/mounts.yml +++ b/test/integration/targets/docker_swarm_service/tasks/tests/mounts.yml @@ -61,6 +61,21 @@ type: "bind" register: mounts_3 +- name: mounts (order idempotency) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "/tmp/" + target: "/tmp/{{ volume_name_2 }}" + type: "bind" + - source: "{{ volume_name_1 }}" + target: "/tmp/{{ volume_name_1 }}" + type: "volume" + register: mounts_4 + - name: mounts (empty) docker_swarm_service: name: "{{ service_name }}" @@ -68,7 +83,7 @@ resolve_image: no command: '/bin/sh -v -c "sleep 10m"' mounts: [] - register: mounts_4 + register: mounts_5 - name: mounts (empty idempotency) docker_swarm_service: @@ -77,7 +92,7 @@ resolve_image: no command: '/bin/sh -v -c "sleep 10m"' mounts: [] - register: mounts_5 + register: mounts_6 - name: cleanup docker_swarm_service: @@ -90,8 +105,9 @@ - mounts_1 is changed - mounts_2 is not changed - mounts_3 is changed - - mounts_4 is changed - - mounts_5 is not changed + - mounts_4 is not changed + - mounts_5 is changed + - mounts_6 is not changed #################################################################### ## mounts.readonly ################################################# diff --git a/test/integration/targets/docker_swarm_service/tasks/tests/networks.yml b/test/integration/targets/docker_swarm_service/tasks/tests/networks.yml index c369c04f93f..9d1a0254b9e 100644 --- a/test/integration/targets/docker_swarm_service/tasks/tests/networks.yml +++ b/test/integration/targets/docker_swarm_service/tasks/tests/networks.yml @@ -99,7 +99,7 @@ - "{{ network_name_2 }}" register: networks_7 -- name: networks (change mixed order) +- name: networks (order idempotency) docker_swarm_service: name: "{{ service_name }}" image: alpine:3.8 @@ -110,17 +110,6 @@ - name: "{{ network_name_1 }}" register: networks_8 -- name: networks (change mixed order idempotency) - docker_swarm_service: - name: "{{ service_name }}" - image: alpine:3.8 - resolve_image: no - command: '/bin/sh -v -c "sleep 10m"' - networks: - - "{{ network_name_2 }}" - - name: "{{ network_name_1 }}" - register: networks_9 - - name: networks (change less) docker_swarm_service: name: "{{ service_name }}" @@ -129,7 +118,7 @@ command: '/bin/sh -v -c "sleep 10m"' networks: - "{{ network_name_2 }}" - register: networks_10 + register: networks_9 - name: networks (change less idempotency) docker_swarm_service: @@ -139,7 +128,7 @@ command: '/bin/sh -v -c "sleep 10m"' networks: - "{{ network_name_2 }}" - register: networks_11 + register: networks_10 - name: networks (empty) docker_swarm_service: @@ -148,7 +137,7 @@ resolve_image: no command: '/bin/sh -v -c "sleep 10m"' networks: [] - register: networks_12 + register: networks_11 - name: networks (empty idempotency) docker_swarm_service: @@ -157,7 +146,7 @@ resolve_image: no command: '/bin/sh -v -c "sleep 10m"' networks: [] - register: networks_13 + register: networks_12 - name: networks (unknown network) docker_swarm_service: @@ -167,7 +156,7 @@ command: '/bin/sh -v -c "sleep 10m"' networks: - "idonotexist" - register: networks_14 + register: networks_13 ignore_errors: yes - name: networks (missing dict key name) @@ -178,7 +167,7 @@ command: '/bin/sh -v -c "sleep 10m"' networks: - foo: "bar" - register: networks_15 + register: networks_14 ignore_errors: yes - name: networks (invalid list type) @@ -189,7 +178,7 @@ command: '/bin/sh -v -c "sleep 10m"' networks: - [1, 2, 3] - register: networks_16 + register: networks_15 ignore_errors: yes - name: cleanup @@ -207,18 +196,17 @@ - networks_5 is not changed - networks_6 is not changed - networks_7 is not changed - - networks_8 is changed - - networks_9 is not changed - - networks_10 is changed - - networks_11 is not changed - - networks_12 is changed - - networks_13 is not changed + - networks_8 is not changed + - networks_9 is changed + - networks_10 is not changed + - networks_11 is changed + - networks_12 is not changed + - networks_13 is failed + - '"Could not find a network named: ''idonotexist''" in networks_13.msg' - networks_14 is failed - - '"Could not find a network named: ''idonotexist''" in networks_14.msg' + - "'\"name\" is required when networks are passed as dictionaries.' in networks_14.msg" - networks_15 is failed - - "'\"name\" is required when networks are passed as dictionaries.' in networks_15.msg" - - networks_16 is failed - - "'Only a list of strings or dictionaries are allowed to be passed as networks' in networks_16.msg" + - "'Only a list of strings or dictionaries are allowed to be passed as networks' in networks_15.msg" - assert: that: @@ -262,7 +250,7 @@ - "alias2" register: networks_aliases_2 -- name: networks.aliases (change) +- name: networks.aliases (order idempotency) docker_swarm_service: name: "{{ service_name }}" image: alpine:3.8 @@ -271,9 +259,22 @@ networks: - name: "{{ network_name_1 }}" aliases: + - "alias2" - "alias1" register: networks_aliases_3 +- name: networks.aliases (change) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + networks: + - name: "{{ network_name_1 }}" + aliases: + - "alias1" + register: networks_aliases_4 + - name: networks.aliases (empty) docker_swarm_service: name: "{{ service_name }}" @@ -283,7 +284,7 @@ networks: - name: "{{ network_name_1 }}" aliases: [] - register: networks_aliases_4 + register: networks_aliases_5 - name: networks.aliases (empty idempotency) docker_swarm_service: @@ -294,7 +295,7 @@ networks: - name: "{{ network_name_1 }}" aliases: [] - register: networks_aliases_5 + register: networks_aliases_6 - name: networks.aliases (invalid type) docker_swarm_service: @@ -306,7 +307,7 @@ - name: "{{ network_name_1 }}" aliases: - [1, 2, 3] - register: networks_aliases_6 + register: networks_aliases_7 ignore_errors: yes - name: cleanup @@ -319,11 +320,12 @@ that: - networks_aliases_1 is changed - networks_aliases_2 is not changed - - networks_aliases_3 is changed + - networks_aliases_3 is not changed - networks_aliases_4 is changed - - networks_aliases_5 is not changed - - networks_aliases_6 is failed - - "'Only strings are allowed as network aliases' in networks_aliases_6.msg" + - networks_aliases_5 is changed + - networks_aliases_6 is not changed + - networks_aliases_7 is failed + - "'Only strings are allowed as network aliases' in networks_aliases_7.msg" #################################################################### ## networks.options ################################################ diff --git a/test/integration/targets/docker_swarm_service/tasks/tests/options.yml b/test/integration/targets/docker_swarm_service/tasks/tests/options.yml index 3ec3fbf11e2..035dc08d8e7 100644 --- a/test/integration/targets/docker_swarm_service/tasks/tests/options.yml +++ b/test/integration/targets/docker_swarm_service/tasks/tests/options.yml @@ -366,6 +366,18 @@ register: dns_options_3 ignore_errors: yes +- name: dns_options (order idempotency) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + dns_options: + - no-check-names + - "timeout:10" + register: dns_options_4 + ignore_errors: yes + - name: dns_options (empty) docker_swarm_service: name: "{{ service_name }}" @@ -373,7 +385,7 @@ resolve_image: no command: '/bin/sh -v -c "sleep 10m"' dns_options: [] - register: dns_options_4 + register: dns_options_5 ignore_errors: yes - name: dns_options (empty idempotency) @@ -383,7 +395,7 @@ resolve_image: no command: '/bin/sh -v -c "sleep 10m"' dns_options: [] - register: dns_options_5 + register: dns_options_6 ignore_errors: yes - name: cleanup @@ -397,8 +409,9 @@ - dns_options_1 is changed - dns_options_2 is not changed - dns_options_3 is changed - - dns_options_4 is changed - - dns_options_5 is not changed + - dns_options_4 is not changed + - dns_options_5 is changed + - dns_options_6 is not changed when: docker_api_version is version('1.25', '>=') and docker_py_version is version('2.6.0', '>=') - assert: that: @@ -588,6 +601,17 @@ - "TEST2=val3" register: env_3 +- name: env (order idempotency) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + env: + - "TEST2=val3" + - "TEST1=val1" + register: env_4 + - name: env (empty) docker_swarm_service: name: "{{ service_name }}" @@ -595,7 +619,7 @@ resolve_image: no command: '/bin/sh -v -c "sleep 10m"' env: [] - register: env_4 + register: env_5 - name: env (empty idempotency) docker_swarm_service: @@ -604,7 +628,7 @@ resolve_image: no command: '/bin/sh -v -c "sleep 10m"' env: [] - register: env_5 + register: env_6 - name: env (fail unwrapped values) docker_swarm_service: @@ -613,7 +637,7 @@ resolve_image: no env: TEST1: true - register: env_6 + register: env_7 ignore_errors: yes - name: env (fail invalid formatted string) @@ -624,7 +648,7 @@ env: - "TEST1=val3" - "TEST2" - register: env_7 + register: env_8 ignore_errors: yes - name: cleanup @@ -638,10 +662,11 @@ - env_1 is changed - env_2 is not changed - env_3 is changed - - env_4 is changed - - env_5 is not changed - - env_6 is failed + - env_4 is not changed + - env_5 is changed + - env_6 is not changed - env_7 is failed + - env_8 is failed #################################################################### ## env_files ####################################################### @@ -802,17 +827,29 @@ register: groups_2 ignore_errors: yes -- name: groups (change) +- name: groups (order idempotency) docker_swarm_service: name: "{{ service_name }}" image: alpine:3.8 resolve_image: no command: '/bin/sh -v -c "sleep 10m"' groups: + - "5678" - "1234" register: groups_3 ignore_errors: yes +- name: groups (change) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + groups: + - "1234" + register: groups_4 + ignore_errors: yes + - name: groups (empty) docker_swarm_service: name: "{{ service_name }}" @@ -820,7 +857,7 @@ resolve_image: no command: '/bin/sh -v -c "sleep 10m"' groups: [] - register: groups_4 + register: groups_5 ignore_errors: yes - name: groups (empty idempotency) @@ -830,7 +867,7 @@ resolve_image: no command: '/bin/sh -v -c "sleep 10m"' groups: [] - register: groups_5 + register: groups_6 ignore_errors: yes - name: cleanup @@ -843,9 +880,10 @@ that: - groups_1 is changed - groups_2 is not changed - - groups_3 is changed + - groups_3 is not changed - groups_4 is changed - - groups_5 is not changed + - groups_5 is changed + - groups_6 is not changed when: docker_api_version is version('1.25', '>=') and docker_py_version is version('2.6.0', '>=') - assert: that: diff --git a/test/integration/targets/docker_swarm_service/tasks/tests/placement.yml b/test/integration/targets/docker_swarm_service/tasks/tests/placement.yml index 76b064efa69..7d674b6bf46 100644 --- a/test/integration/targets/docker_swarm_service/tasks/tests/placement.yml +++ b/test/integration/targets/docker_swarm_service/tasks/tests/placement.yml @@ -142,6 +142,32 @@ register: constraints_3 ignore_errors: yes +- name: placement.constraints (add) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + placement: + constraints: + - "node.role == worker" + - "node.label != non_existent_label" + register: constraints_4 + ignore_errors: yes + +- name: placement.constraints (order idempotency) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + placement: + constraints: + - "node.label != non_existent_label" + - "node.role == worker" + register: constraints_5 + ignore_errors: yes + - name: placement.constraints (empty) docker_swarm_service: name: "{{ service_name }}" @@ -150,7 +176,7 @@ command: '/bin/sh -v -c "sleep 10m"' placement: constraints: [] - register: constraints_4 + register: constraints_6 ignore_errors: yes - name: placement.constraints (empty idempotency) @@ -161,7 +187,7 @@ command: '/bin/sh -v -c "sleep 10m"' placement: constraints: [] - register: constraints_5 + register: constraints_7 ignore_errors: yes - name: cleanup @@ -178,6 +204,8 @@ - constraints_3 is changed - constraints_4 is changed - constraints_5 is not changed + - constraints_6 is changed + - constraints_7 is not changed when: docker_api_version is version('1.27', '>=') and docker_py_version is version('2.4.0', '>=') - assert: that: diff --git a/test/integration/targets/docker_swarm_service/tasks/tests/secrets.yml b/test/integration/targets/docker_swarm_service/tasks/tests/secrets.yml index 5d23ca50c06..bdb903b74cd 100644 --- a/test/integration/targets/docker_swarm_service/tasks/tests/secrets.yml +++ b/test/integration/targets/docker_swarm_service/tasks/tests/secrets.yml @@ -97,6 +97,20 @@ register: secrets_5 ignore_errors: yes +- name: secrets (order idempotency) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + secrets: + - secret_name: "{{ secret_name_2 }}" + filename: "/run/secrets/{{ secret_name_2 }}.txt" + - secret_name: "{{ secret_name_1 }}" + filename: "/run/secrets/{{ secret_name_1 }}.txt" + register: secrets_6 + ignore_errors: yes + - name: secrets (empty) docker_swarm_service: name: "{{ service_name }}" @@ -104,7 +118,7 @@ resolve_image: no command: '/bin/sh -v -c "sleep 10m"' secrets: [] - register: secrets_6 + register: secrets_7 ignore_errors: yes - name: secrets (empty idempotency) @@ -114,7 +128,7 @@ resolve_image: no command: '/bin/sh -v -c "sleep 10m"' secrets: [] - register: secrets_7 + register: secrets_8 ignore_errors: yes - name: cleanup @@ -130,8 +144,9 @@ - secrets_3 is changed - secrets_4 is not changed - secrets_5 is not changed - - secrets_6 is changed - - secrets_7 is not changed + - secrets_6 is not changed + - secrets_7 is changed + - secrets_8 is not changed when: docker_api_version is version('1.25', '>=') and docker_py_version is version('2.4.0', '>=') - assert: that: diff --git a/test/units/modules/cloud/docker/test_docker_swarm_service.py b/test/units/modules/cloud/docker/test_docker_swarm_service.py index 7adb7dcb503..1f2eddac777 100644 --- a/test/units/modules/cloud/docker/test_docker_swarm_service.py +++ b/test/units/modules/cloud/docker/test_docker_swarm_service.py @@ -169,60 +169,87 @@ def test_has_dict_changed(docker_swarm_service): def test_has_list_changed(docker_swarm_service): + + # List comparisons without dictionaries + # I could improve the indenting, but pycodestyle wants this instead + assert not docker_swarm_service.has_list_changed(None, None) + assert not docker_swarm_service.has_list_changed(None, []) + assert not docker_swarm_service.has_list_changed(None, [1, 2]) + + assert not docker_swarm_service.has_list_changed([], None) + assert not docker_swarm_service.has_list_changed([], []) + assert docker_swarm_service.has_list_changed([], [1, 2]) + + assert docker_swarm_service.has_list_changed([1, 2], None) + assert docker_swarm_service.has_list_changed([1, 2], []) + + assert docker_swarm_service.has_list_changed([1, 2, 3], [1, 2]) + assert docker_swarm_service.has_list_changed([1, 2], [1, 2, 3]) + + # Check list sorting + assert not docker_swarm_service.has_list_changed([1, 2], [2, 1]) assert docker_swarm_service.has_list_changed( - [ - {"a": 1}, - {"b": 1} - ], - [ - {"a": 1} - ] + [1, 2], + [2, 1], + sort_lists=False ) + + # Check type matching + assert docker_swarm_service.has_list_changed([None, 1], [2, 1]) + assert docker_swarm_service.has_list_changed([2, 1], [None, 1]) assert docker_swarm_service.has_list_changed( - [ - {"a": 1}, - ], - [ - {"a": 1}, - {"b": 1}, - ] - ) - assert not docker_swarm_service.has_list_changed( - [ - {"a": 1}, - {"b": 1}, - ], - [ - {"a": 1}, - {"b": 1} - ] - ) - assert not docker_swarm_service.has_list_changed( - None, - [ - {"b": 1}, - {"a": 1} - ] + "command --with args", + ['command', '--with', 'args'] ) assert docker_swarm_service.has_list_changed( - [], - [ - {"b": 1}, - {"a": 1} - ] + ['sleep', '3400'], + [u'sleep', u'3600'], + sort_lists=False ) + + # List comparisons with dictionaries assert not docker_swarm_service.has_list_changed( - None, - None + [{'a': 1}], + [{'a': 1}], + sort_key='a' ) + assert not docker_swarm_service.has_list_changed( - [], - None + [{'a': 1}, {'a': 2}], + [{'a': 1}, {'a': 2}], + sort_key='a' ) + + with pytest.raises(Exception): + docker_swarm_service.has_list_changed( + [{'a': 1}, {'a': 2}], + [{'a': 1}, {'a': 2}] + ) + + # List sort checking with sort key assert not docker_swarm_service.has_list_changed( - None, - [] + [{'a': 1}, {'a': 2}], + [{'a': 2}, {'a': 1}], + sort_key='a' + ) + assert docker_swarm_service.has_list_changed( + [{'a': 1}, {'a': 2}], + [{'a': 2}, {'a': 1}], + sort_lists=False + ) + + assert docker_swarm_service.has_list_changed( + [{'a': 1}, {'a': 2}, {'a': 3}], + [{'a': 2}, {'a': 1}], + sort_key='a' + ) + assert docker_swarm_service.has_list_changed( + [{'a': 1}, {'a': 2}], + [{'a': 1}, {'a': 2}, {'a': 3}], + sort_lists=False ) + + # Additional dictionary elements assert not docker_swarm_service.has_list_changed( [ {"src": 1, "dst": 2}, @@ -231,7 +258,8 @@ def test_has_list_changed(docker_swarm_service): [ {"src": 1, "dst": 2, "protocol": "tcp"}, {"src": 1, "dst": 2, "protocol": "udp"}, - ] + ], + sort_key='dst' ) assert not docker_swarm_service.has_list_changed( [ @@ -241,7 +269,8 @@ def test_has_list_changed(docker_swarm_service): [ {"src": 1, "dst": 2, "protocol": "udp"}, {"src": 1, "dst": 3, "protocol": "tcp"}, - ] + ], + sort_key='dst' ) assert docker_swarm_service.has_list_changed( [ @@ -253,7 +282,8 @@ def test_has_list_changed(docker_swarm_service): {"src": 1, "dst": 3, "protocol": "udp"}, {"src": 1, "dst": 2, "protocol": "tcp"}, {"src": 3, "dst": 4, "protocol": "tcp"}, - ] + ], + sort_key='dst' ) assert docker_swarm_service.has_list_changed( [ @@ -263,7 +293,8 @@ def test_has_list_changed(docker_swarm_service): [ {"src": 1, "dst": 2, "protocol": "tcp"}, {"src": 1, "dst": 2, "protocol": "udp"}, - ] + ], + sort_key='dst' ) assert docker_swarm_service.has_list_changed( [ @@ -273,11 +304,130 @@ def test_has_list_changed(docker_swarm_service): [ {"src": 1, "dst": 2, "protocol": "udp"}, {"src": 1, "dst": 2, "protocol": "tcp"}, - ] + ], + sort_key='dst' ) assert not docker_swarm_service.has_list_changed( [{'id': '123', 'aliases': []}], - [{'id': '123'}] + [{'id': '123'}], + sort_key='id' + ) + + +def test_have_networks_changed(docker_swarm_service): + assert not docker_swarm_service.have_networks_changed( + None, + None + ) + + assert not docker_swarm_service.have_networks_changed( + [], + None + ) + + assert not docker_swarm_service.have_networks_changed( + [{'id': 1}], + [{'id': 1}] + ) + + assert docker_swarm_service.have_networks_changed( + [{'id': 1}], + [{'id': 1}, {'id': 2}] + ) + + assert not docker_swarm_service.have_networks_changed( + [{'id': 1}, {'id': 2}], + [{'id': 1}, {'id': 2}] + ) + + assert not docker_swarm_service.have_networks_changed( + [{'id': 1}, {'id': 2}], + [{'id': 2}, {'id': 1}] + ) + + assert not docker_swarm_service.have_networks_changed( + [ + {'id': 1}, + {'id': 2, 'aliases': []} + ], + [ + {'id': 1}, + {'id': 2} + ] + ) + + assert docker_swarm_service.have_networks_changed( + [ + {'id': 1}, + {'id': 2, 'aliases': ['alias1']} + ], + [ + {'id': 1}, + {'id': 2} + ] + ) + + assert docker_swarm_service.have_networks_changed( + [ + {'id': 1}, + {'id': 2, 'aliases': ['alias1', 'alias2']} + ], + [ + {'id': 1}, + {'id': 2, 'aliases': ['alias1']} + ] + ) + + assert not docker_swarm_service.have_networks_changed( + [ + {'id': 1}, + {'id': 2, 'aliases': ['alias1', 'alias2']} + ], + [ + {'id': 1}, + {'id': 2, 'aliases': ['alias1', 'alias2']} + ] + ) + + assert not docker_swarm_service.have_networks_changed( + [ + {'id': 1}, + {'id': 2, 'aliases': ['alias1', 'alias2']} + ], + [ + {'id': 1}, + {'id': 2, 'aliases': ['alias2', 'alias1']} + ] + ) + + assert not docker_swarm_service.have_networks_changed( + [ + {'id': 1, 'options': {}}, + {'id': 2, 'aliases': ['alias1', 'alias2']}], + [ + {'id': 1}, + {'id': 2, 'aliases': ['alias2', 'alias1']} + ] + ) + + assert not docker_swarm_service.have_networks_changed( + [ + {'id': 1, 'options': {'option1': 'value1'}}, + {'id': 2, 'aliases': ['alias1', 'alias2']}], + [ + {'id': 1, 'options': {'option1': 'value1'}}, + {'id': 2, 'aliases': ['alias2', 'alias1']} + ] + ) + + assert docker_swarm_service.have_networks_changed( + [ + {'id': 1, 'options': {'option1': 'value1'}}, + {'id': 2, 'aliases': ['alias1', 'alias2']}], + [ + {'id': 1, 'options': {'option1': 'value2'}}, + {'id': 2, 'aliases': ['alias2', 'alias1']} + ] )