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,6 +250,19 @@ - "alias2" register: networks_aliases_2 +- name: networks.aliases (order idempotency) + 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: + - "alias2" + - "alias1" + register: networks_aliases_3 + - name: networks.aliases (change) docker_swarm_service: name: "{{ service_name }}" @@ -272,7 +273,7 @@ - name: "{{ network_name_1 }}" aliases: - "alias1" - register: networks_aliases_3 + register: networks_aliases_4 - name: networks.aliases (empty) docker_swarm_service: @@ -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,16 +601,18 @@ - "TEST2=val3" register: env_3 -- name: env (empty) +- 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: [] + env: + - "TEST2=val3" + - "TEST1=val1" register: env_4 -- name: env (empty idempotency) +- name: env (empty) docker_swarm_service: name: "{{ service_name }}" image: alpine:3.8 @@ -606,6 +621,15 @@ env: [] register: env_5 +- name: env (empty idempotency) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + env: [] + register: env_6 + - name: env (fail unwrapped values) docker_swarm_service: name: "{{ service_name }}" @@ -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,6 +827,18 @@ register: groups_2 ignore_errors: yes +- 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 }}" @@ -810,7 +847,7 @@ command: '/bin/sh -v -c "sleep 10m"' groups: - "1234" - register: groups_3 + register: groups_4 ignore_errors: yes - name: groups (empty) @@ -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( + "command --with args", + ['command', '--with', 'args'] ) assert docker_swarm_service.has_list_changed( - [ - {"a": 1}, - ], - [ - {"a": 1}, - {"b": 1}, - ] + ['sleep', '3400'], + [u'sleep', u'3600'], + sort_lists=False ) + + # List comparisons with dictionaries assert not docker_swarm_service.has_list_changed( - [ - {"a": 1}, - {"b": 1}, - ], - [ - {"a": 1}, - {"b": 1} - ] + [{'a': 1}], + [{'a': 1}], + sort_key='a' ) + assert not docker_swarm_service.has_list_changed( - None, - [ - {"b": 1}, - {"a": 1} - ] + [{'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( + [{'a': 1}, {'a': 2}], + [{'a': 2}, {'a': 1}], + sort_key='a' ) assert docker_swarm_service.has_list_changed( - [], - [ - {"b": 1}, - {"a": 1} - ] + [{'a': 1}, {'a': 2}], + [{'a': 2}, {'a': 1}], + sort_lists=False ) - assert not docker_swarm_service.has_list_changed( - None, - None + + assert docker_swarm_service.has_list_changed( + [{'a': 1}, {'a': 2}, {'a': 3}], + [{'a': 2}, {'a': 1}], + sort_key='a' ) - assert not docker_swarm_service.has_list_changed( - [], - None - ) - assert not docker_swarm_service.has_list_changed( - None, - [] + 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']} + ] )