From 13364fc5302ee4d457db817da16d624542a7cc31 Mon Sep 17 00:00:00 2001 From: Hannes Ljungberg Date: Sun, 18 Aug 2019 08:55:54 +0200 Subject: [PATCH] docker_swarm_service: Allow passing dicts in networks (#58961) * Add support for passing networks as dicts * Add function to compare a list of different objects * Handle comparing falsy values to missing values * Pass docker versions to Service * Move can_update_networks to Service class * Pass Networks in TaskTemplate when supported * Remove weird __str__ * Add networks integration tests * Add unit tests * Add example * Add changelog fragment * Make sure that network options are clean Co-Authored-By: Felix Fontein * Set networks elements as raw in arg spec Co-Authored-By: Felix Fontein * Fix wrong variable naming * Check for network options that are not valid * Only check for None options * Validate that aliases is a list --- ...er_swarm_service-support-dict-networks.yml | 2 + .../cloud/docker/docker_swarm_service.py | 219 ++++++--- .../tasks/tests/networks.yml | 448 ++++++++++++++++++ .../tasks/tests/options.yml | 157 +----- .../cloud/docker/test_docker_swarm_service.py | 111 ++++- 5 files changed, 701 insertions(+), 236 deletions(-) create mode 100644 changelogs/fragments/58961-docker_swarm_service-support-dict-networks.yml create mode 100644 test/integration/targets/docker_swarm_service/tasks/tests/networks.yml diff --git a/changelogs/fragments/58961-docker_swarm_service-support-dict-networks.yml b/changelogs/fragments/58961-docker_swarm_service-support-dict-networks.yml new file mode 100644 index 00000000000..9592b5637e3 --- /dev/null +++ b/changelogs/fragments/58961-docker_swarm_service-support-dict-networks.yml @@ -0,0 +1,2 @@ +minor_changes: + - "docker_swarm_service - Support passing dictionaries in ``networks`` to allow setting ``aliases`` and ``options``." diff --git a/lib/ansible/modules/cloud/docker/docker_swarm_service.py b/lib/ansible/modules/cloud/docker/docker_swarm_service.py index fa095af0063..d77de04bb56 100644 --- a/lib/ansible/modules/cloud/docker/docker_swarm_service.py +++ b/lib/ansible/modules/cloud/docker/docker_swarm_service.py @@ -358,7 +358,9 @@ options: required: yes networks: description: - - List of the service networks names. + - List of the service networks names or dictionaries. + - When passed dictionaries valid sub-options are C(name) which is required and + C(aliases) and C(options). - Prior to API version 1.29, updating and removing networks is not supported. If changes are made the service will then be removed and recreated. - Corresponds to the C(--network) option of C(docker service create). @@ -997,6 +999,17 @@ EXAMPLES = ''' networks: - mynetwork +- name: Set networks as a dictionary + docker_swarm_service: + name: myservice + image: alpine:edge + networks: + - name: "mynetwork" + aliases: + - "mynetwork_alias" + options: + foo: bar + - name: Set secrets docker_swarm_service: name: myservice @@ -1048,6 +1061,7 @@ from ansible.module_utils.docker.common import ( DockerBaseClass, convert_duration_to_nanosecond, parse_healthcheck, + clean_dict_booleans_for_docker_api, RequestException, ) @@ -1116,6 +1130,58 @@ def get_docker_environment(env, env_files): return sorted(env_list) +def get_docker_networks(networks, network_ids): + """ + Validate a list of network names or a list of network dictionaries. + Network names will be resolved to ids by using the network_ids mapping. + """ + if networks is None: + return None + parsed_networks = [] + for network in networks: + if isinstance(network, string_types): + parsed_network = {'name': network} + elif isinstance(network, dict): + if 'name' not in network: + raise TypeError( + '"name" is required when networks are passed as dictionaries.' + ) + name = network.pop('name') + parsed_network = {'name': name} + aliases = network.pop('aliases', None) + if aliases is not None: + if not isinstance(aliases, list): + raise TypeError('"aliases" network option is only allowed as a list') + if not all( + isinstance(alias, string_types) for alias in aliases + ): + raise TypeError('Only strings are allowed as network aliases.') + parsed_network['aliases'] = aliases + options = network.pop('options', None) + if options is not None: + if not isinstance(options, dict): + raise TypeError('Only dict is allowed as network options.') + parsed_network['options'] = clean_dict_booleans_for_docker_api(options) + # Check if any invalid keys left + if network: + invalid_keys = ', '.join(network.keys()) + raise TypeError( + '%s are not valid keys for the networks option' % invalid_keys + ) + + else: + raise TypeError( + 'Only a list of strings or dictionaries are allowed to be passed as networks.' + ) + network_name = parsed_network.pop('name') + try: + parsed_network['id'] = network_ids[network_name] + except KeyError as e: + raise ValueError('Could not find a network named: %s.' % e) + parsed_networks.append(parsed_network) + return parsed_networks or [] + + def get_nanoseconds_from_raw_option(name, value): if value is None: return None @@ -1154,14 +1220,17 @@ def has_dict_changed(new_dict, old_dict): if value is not None ) for option, value in defined_options.items(): - if value != old_dict.get(option): + old_value = old_dict.get(option) + if not value and not old_value: + continue + if value != old_value: return True return False -def has_list_of_dicts_changed(new_list, old_list): +def has_list_changed(new_list, old_list): """ - Check two lists of dicts has differences. + Check two lists has differences. """ if new_list is None: return False @@ -1169,13 +1238,20 @@ def has_list_of_dicts_changed(new_list, old_list): if len(new_list) != len(old_list): return True for new_item, old_item in zip(new_list, old_list): - if has_dict_changed(new_item, old_item): + is_same_type = type(new_item) == type(old_item) + if not is_same_type: + return True + if isinstance(new_item, dict): + if has_dict_changed(new_item, old_item): + return True + elif new_item != old_item: return True + return False class DockerService(DockerBaseClass): - def __init__(self): + def __init__(self, docker_api_version, docker_py_version): super(DockerService, self).__init__() self.image = "" self.command = None @@ -1227,7 +1303,9 @@ class DockerService(DockerBaseClass): self.update_max_failure_ratio = None self.update_order = None self.working_dir = None - self.can_update_networks = None + + self.docker_api_version = docker_api_version + self.docker_py_version = docker_py_version def get_facts(self): return { @@ -1281,6 +1359,22 @@ class DockerService(DockerBaseClass): 'working_dir': self.working_dir, } + @property + def can_update_networks(self): + # Before Docker API 1.29 adding/removing networks was not supported + return ( + self.docker_api_version >= LooseVersion('1.29') and + self.docker_py_version >= LooseVersion('2.7') + ) + + @property + def can_use_task_template_networks(self): + # In Docker API 1.25 attaching networks to TaskTemplate is preferred over Spec + return ( + self.docker_api_version >= LooseVersion('1.25') and + self.docker_py_version >= LooseVersion('2.7') + ) + @staticmethod def get_restart_config_from_ansible_params(params): restart_config = params['restart_config'] or {} @@ -1474,11 +1568,18 @@ class DockerService(DockerBaseClass): @classmethod def from_ansible_params( - cls, ap, old_service, image_digest, can_update_networks, secret_ids, config_ids + cls, + ap, + old_service, + image_digest, + secret_ids, + config_ids, + network_ids, + docker_api_version, + docker_py_version, ): - s = DockerService() + s = DockerService(docker_api_version, docker_py_version) s.image = image_digest - s.can_update_networks = can_update_networks s.args = ap['args'] s.endpoint_mode = ap['endpoint_mode'] s.dns = ap['dns'] @@ -1491,12 +1592,13 @@ class DockerService(DockerBaseClass): s.labels = ap['labels'] s.container_labels = ap['container_labels'] s.mode = ap['mode'] - s.networks = ap['networks'] s.stop_signal = ap['stop_signal'] s.user = ap['user'] s.working_dir = ap['working_dir'] s.read_only = ap['read_only'] + s.networks = get_docker_networks(ap['networks'], network_ids) + s.command = ap['command'] if isinstance(s.command, string_types): s.command = shlex.split(s.command) @@ -1650,13 +1752,13 @@ class DockerService(DockerBaseClass): if self.mode != os.mode: needs_rebuild = True differences.add('mode', parameter=self.mode, active=os.mode) - if has_list_of_dicts_changed(self.mounts, os.mounts): + if has_list_changed(self.mounts, os.mounts): differences.add('mounts', parameter=self.mounts, active=os.mounts) - if has_list_of_dicts_changed(self.configs, os.configs): + if has_list_changed(self.configs, os.configs): differences.add('configs', parameter=self.configs, active=os.configs) - if has_list_of_dicts_changed(self.secrets, os.secrets): + if has_list_changed(self.secrets, os.secrets): differences.add('secrets', parameter=self.secrets, active=os.secrets) - if self.networks is not None and self.networks != (os.networks or []): + if has_list_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: @@ -1774,18 +1876,6 @@ class DockerService(DockerBaseClass): old_image = old_image.split('@')[0] return self.image != old_image, old_image - def __str__(self): - return str({ - 'mode': self.mode, - 'env': self.env, - 'endpoint_mode': self.endpoint_mode, - 'mounts': self.mounts, - 'configs': self.configs, - 'secrets': self.secrets, - 'networks': self.networks, - 'replicas': self.replicas - }) - def build_container_spec(self): mounts = None if self.mounts is not None: @@ -2000,6 +2090,10 @@ class DockerService(DockerBaseClass): task_template_args['resources'] = resources if self.force_update: task_template_args['force_update'] = self.force_update + if self.can_use_task_template_networks: + networks = self.build_networks() + if networks: + task_template_args['networks'] = networks return types.TaskTemplate(container_spec=container_spec, **task_template_args) def build_service_mode(self): @@ -2007,22 +2101,17 @@ class DockerService(DockerBaseClass): self.replicas = None return types.ServiceMode(self.mode, replicas=self.replicas) - def build_networks(self, docker_networks): + def build_networks(self): networks = None if self.networks is not None: networks = [] - for network_name in self.networks: - network_id = None - try: - network_id = list( - filter(lambda n: n['name'] == network_name, docker_networks) - )[0]['id'] - except (IndexError, KeyError): - pass - if network_id: - networks.append({'Target': network_id}) - else: - raise Exception('no docker networks named: %s' % network_name) + for network in self.networks: + docker_network = {'Target': network['id']} + if 'aliases' in network: + docker_network['Aliases'] = network['aliases'] + if 'options' in network: + docker_network['DriverOpts'] = network['options'] + networks.append(docker_network) return networks def build_endpoint_spec(self): @@ -2043,7 +2132,7 @@ class DockerService(DockerBaseClass): endpoint_spec_args['mode'] = self.endpoint_mode return types.EndpointSpec(**endpoint_spec_args) if endpoint_spec_args else None - def build_docker_service(self, docker_networks): + def build_docker_service(self): container_spec = self.build_container_spec() placement = self.build_placement() task_template = self.build_task_template(container_spec, placement) @@ -2051,7 +2140,6 @@ class DockerService(DockerBaseClass): update_config = self.build_update_config() rollback_config = self.build_rollback_config() service_mode = self.build_service_mode() - networks = self.build_networks(docker_networks) endpoint_spec = self.build_endpoint_spec() service = {'task_template': task_template, 'mode': service_mode} @@ -2059,12 +2147,14 @@ class DockerService(DockerBaseClass): service['update_config'] = update_config if rollback_config: service['rollback_config'] = rollback_config - if networks: - service['networks'] = networks if endpoint_spec: service['endpoint_spec'] = endpoint_spec if self.labels: service['labels'] = self.labels + if not self.can_use_task_template_networks: + networks = self.build_networks() + if networks: + service['networks'] = networks return service @@ -2075,15 +2165,12 @@ class DockerServiceManager(object): self.retries = 2 self.diff_tracker = None - def get_networks_names_ids(self): - return [{'name': n['Name'], 'id': n['Id']} for n in self.client.networks()] - def get_service(self, name): try: raw_data = self.client.inspect_service(name) except NotFound: return None - ds = DockerService() + ds = DockerService(self.client.docker_api_version, self.client.docker_py_version) task_template_data = raw_data['Spec']['TaskTemplate'] ds.image = task_template_data['ContainerSpec']['Image'] @@ -2263,24 +2350,22 @@ class DockerServiceManager(object): 'mode': secret_data['File'].get('Mode') }) - networks_names_ids = self.get_networks_names_ids() raw_networks_data = task_template_data.get('Networks', raw_data['Spec'].get('Networks')) if raw_networks_data: ds.networks = [] for network_data in raw_networks_data: - network_name = [network_name_id['name'] for network_name_id in networks_names_ids if - network_name_id['id'] == network_data['Target']] - if len(network_name) == 0: - ds.networks.append(network_data['Target']) - else: - ds.networks.append(network_name[0]) - + network = {'id': network_data['Target']} + if 'Aliases' in network_data: + network['aliases'] = network_data['Aliases'] + if 'DriverOpts' in network_data: + network['options'] = network_data['DriverOpts'] + ds.networks.append(network) ds.service_version = raw_data['Version']['Index'] ds.service_id = raw_data['ID'] return ds def update_service(self, name, old_service, new_service): - service_data = new_service.build_docker_service(self.get_networks_names_ids()) + service_data = new_service.build_docker_service() result = self.client.update_service( old_service.service_id, old_service.service_version, @@ -2292,7 +2377,7 @@ class DockerServiceManager(object): self.client.report_warnings(result, ['Warning']) def create_service(self, name, service): - service_data = service.build_docker_service(self.get_networks_names_ids()) + service_data = service.build_docker_service() result = self.client.create_service(name=name, **service_data) self.client.report_warnings(result, ['Warning']) @@ -2313,11 +2398,9 @@ class DockerServiceManager(object): digest = distribution_data['Descriptor']['digest'] return '%s@%s' % (name, digest) - def can_update_networks(self): - # Before Docker API 1.29 adding/removing networks was not supported - return ( - self.client.docker_api_version >= LooseVersion('1.29') and - self.client.docker_py_version >= LooseVersion('2.7') + def get_networks_names_ids(self): + return dict( + (network['Name'], network['Id']) for network in self.client.networks() ) def get_missing_secret_ids(self): @@ -2392,16 +2475,18 @@ class DockerServiceManager(object): % (module.params['name'], e) ) try: - can_update_networks = self.can_update_networks() secret_ids = self.get_missing_secret_ids() config_ids = self.get_missing_config_ids() + network_ids = self.get_networks_names_ids() new_service = DockerService.from_ansible_params( module.params, current_service, image_digest, - can_update_networks, secret_ids, - config_ids + config_ids, + network_ids, + self.client.docker_api_version, + self.client.docker_py_version ) except Exception as e: return self.client.fail( @@ -2567,7 +2652,7 @@ def main(): gid=dict(type='str'), mode=dict(type='int'), )), - networks=dict(type='list', elements='str'), + networks=dict(type='list', elements='raw'), command=dict(type='raw'), args=dict(type='list', elements='str'), env=dict(type='raw'), diff --git a/test/integration/targets/docker_swarm_service/tasks/tests/networks.yml b/test/integration/targets/docker_swarm_service/tasks/tests/networks.yml new file mode 100644 index 00000000000..c369c04f93f --- /dev/null +++ b/test/integration/targets/docker_swarm_service/tasks/tests/networks.yml @@ -0,0 +1,448 @@ +--- + +- name: Registering service name + set_fact: + service_name: "{{ name_prefix ~ '-networks' }}" + network_name_1: "{{ name_prefix ~ '-network-1' }}" + network_name_2: "{{ name_prefix ~ '-network-2' }}" + +- name: Registering service name + set_fact: + service_names: "{{ service_names + [service_name] }}" + network_names: "{{ network_names + [network_name_1, network_name_2] }}" + +- docker_network: + name: "{{ network_name }}" + driver: "overlay" + state: present + loop: + - "{{ network_name_1 }}" + - "{{ network_name_2 }}" + loop_control: + loop_var: network_name + +##################################################################### +## networks ######################################################### +##################################################################### + +- name: networks + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + networks: + - "{{ network_name_1 }}" + register: networks_1 + +- name: networks (idempotency) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + networks: + - "{{ network_name_1 }}" + register: networks_2 + +- name: networks (dict 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 }}" + register: networks_3 + +- name: networks (change more) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + networks: + - "{{ network_name_1 }}" + - "{{ network_name_2 }}" + register: networks_4 + +- name: networks (change more idempotency) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + networks: + - "{{ network_name_1 }}" + - "{{ network_name_2 }}" + register: networks_5 + +- name: networks (change more dict 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 }}" + - name: "{{ network_name_2 }}" + register: networks_6 + +- name: networks (change more mixed 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 }}" + - "{{ network_name_2 }}" + register: networks_7 + +- name: networks (change mixed order) + 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_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 }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + networks: + - "{{ network_name_2 }}" + register: networks_10 + +- name: networks (change less 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 }}" + register: networks_11 + +- name: networks (empty) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + networks: [] + register: networks_12 + +- name: networks (empty idempotency) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + networks: [] + register: networks_13 + +- name: networks (unknown network) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + networks: + - "idonotexist" + register: networks_14 + ignore_errors: yes + +- name: networks (missing dict key name) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + networks: + - foo: "bar" + register: networks_15 + ignore_errors: yes + +- name: networks (invalid list type) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + networks: + - [1, 2, 3] + register: networks_16 + ignore_errors: yes + +- name: cleanup + docker_swarm_service: + name: "{{ service_name }}" + state: absent + diff: no + +- assert: + that: + - networks_1 is changed + - networks_2 is not changed + - networks_3 is not changed + - networks_4 is changed + - 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_14 is failed + - '"Could not find a network named: ''idonotexist''" 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" + +- assert: + that: + - networks_4.rebuilt == false + - networks_7.rebuilt == false + when: docker_api_version is version('1.29', '>=') and docker_py_version is version('2.7.0', '>=') + +- assert: + that: + - networks_4.rebuilt == true + - networks_7.rebuilt == true + when: docker_api_version is version('1.29', '<') or docker_py_version is version('2.7.0', '<') + +#################################################################### +## networks.aliases ################################################ +#################################################################### + +- name: networks.aliases + 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" + - "alias2" + register: networks_aliases_1 + +- name: networks.aliases (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: + - "alias1" + - "alias2" + register: networks_aliases_2 + +- 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_3 + +- name: networks.aliases (empty) + 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: [] + register: networks_aliases_4 + +- name: networks.aliases (empty 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: [] + register: networks_aliases_5 + +- name: networks.aliases (invalid type) + 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: + - [1, 2, 3] + register: networks_aliases_6 + ignore_errors: yes + +- name: cleanup + docker_swarm_service: + name: "{{ service_name }}" + state: absent + diff: no + +- assert: + that: + - networks_aliases_1 is changed + - networks_aliases_2 is not changed + - networks_aliases_3 is 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.options ################################################ +#################################################################### + +- name: networks.options + 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 }}" + options: + foo: bar + test: hello + register: networks_options_1 + +- name: networks.options (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 }}" + options: + foo: bar + test: hello + register: networks_options_2 + +- name: networks.options (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 }}" + options: + foo: bar + test: hej + register: networks_options_3 + +- name: networks.options (change less) + 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 }}" + options: + foo: bar + register: networks_options_4 + +- name: networks.options (invalid type) + 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 }}" + options: [1, 2, 3] + register: networks_options_5 + ignore_errors: yes + +- name: networks.options (empty) + 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 }}" + options: {} + register: networks_options_6 + +- name: networks.options (empty 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 }}" + options: {} + register: networks_options_7 + +- name: cleanup + docker_swarm_service: + name: "{{ service_name }}" + state: absent + diff: no + +- assert: + that: + - networks_options_1 is changed + - networks_options_2 is not changed + - networks_options_3 is changed + - networks_options_4 is changed + - networks_options_5 is failed + - "'Only dict is allowed as network options' in networks_options_5.msg" + - networks_options_6 is changed + - networks_options_7 is not changed + +#################################################################### +#################################################################### +#################################################################### + +- name: Delete networks + docker_network: + name: "{{ network_name }}" + state: absent + force: yes + loop: + - "{{ network_name_1 }}" + - "{{ network_name_2 }}" + loop_control: + loop_var: network_name + ignore_errors: yes 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 4f08a462cec..3ec3fbf11e2 100644 --- a/test/integration/targets/docker_swarm_service/tasks/tests/options.yml +++ b/test/integration/targets/docker_swarm_service/tasks/tests/options.yml @@ -1,25 +1,12 @@ --- -- name: Registering container name +- name: Registering service name set_fact: service_name: "{{ name_prefix ~ '-options' }}" - network_name_1: "{{ name_prefix ~ '-network-1' }}" - network_name_2: "{{ name_prefix ~ '-network-2' }}" -- name: Registering container name +- name: Registering service name set_fact: service_names: "{{ service_names + [service_name] }}" - network_names: "{{ network_names + [network_name_1, network_name_2] }}" - -- docker_network: - name: "{{ network_name }}" - driver: "overlay" - state: present - loop: - - "{{ network_name_1 }}" - - "{{ network_name_2 }}" - loop_control: - loop_var: network_name #################################################################### ## args ############################################################ @@ -1275,119 +1262,6 @@ - mode_2 is not changed - mode_3 is changed -#################################################################### -## networks ######################################################## -#################################################################### - -- name: networks - docker_swarm_service: - name: "{{ service_name }}" - image: alpine:3.8 - resolve_image: no - command: '/bin/sh -v -c "sleep 10m"' - networks: - - "{{ network_name_1 }}" - register: networks_1 - -- name: networks (idempotency) - docker_swarm_service: - name: "{{ service_name }}" - image: alpine:3.8 - resolve_image: no - command: '/bin/sh -v -c "sleep 10m"' - networks: - - "{{ network_name_1 }}" - register: networks_2 - -- name: networks (change more) - docker_swarm_service: - name: "{{ service_name }}" - image: alpine:3.8 - resolve_image: no - command: '/bin/sh -v -c "sleep 10m"' - networks: - - "{{ network_name_1 }}" - - "{{ network_name_2 }}" - register: networks_3 - -- name: networks (change more idempotency) - docker_swarm_service: - name: "{{ service_name }}" - image: alpine:3.8 - resolve_image: no - command: '/bin/sh -v -c "sleep 10m"' - networks: - - "{{ network_name_1 }}" - - "{{ network_name_2 }}" - register: networks_4 - -- name: networks (change less) - docker_swarm_service: - name: "{{ service_name }}" - image: alpine:3.8 - resolve_image: no - command: '/bin/sh -v -c "sleep 10m"' - networks: - - "{{ network_name_2 }}" - register: networks_5 - -- name: networks (change less 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 }}" - register: networks_6 - -- name: networks (empty) - docker_swarm_service: - name: "{{ service_name }}" - image: alpine:3.8 - resolve_image: no - command: '/bin/sh -v -c "sleep 10m"' - networks: [] - register: networks_7 - -- name: networks (empty idempotency) - docker_swarm_service: - name: "{{ service_name }}" - image: alpine:3.8 - resolve_image: no - command: '/bin/sh -v -c "sleep 10m"' - networks: [] - register: networks_8 - -- name: cleanup - docker_swarm_service: - name: "{{ service_name }}" - state: absent - diff: no - -- assert: - that: - - networks_1 is changed - - networks_2 is not changed - - networks_3 is changed - - networks_4 is not changed - - networks_5 is changed - - networks_6 is not changed - - networks_7 is changed - - networks_8 is not changed - -- assert: - that: - - networks_3.rebuilt == false - - networks_5.rebuilt == false - when: docker_api_version is version('1.29', '>=') and docker_py_version is version('2.7.0', '>=') - -- assert: - that: - - networks_3.rebuilt == true - - networks_5.rebuilt == true - when: docker_api_version is version('1.29', '<') or docker_py_version is version('2.7.0', '<') - #################################################################### ## stop_grace_period ############################################### #################################################################### @@ -1915,30 +1789,3 @@ - working_dir_1 is changed - working_dir_2 is not changed - working_dir_3 is changed - -#################################################################### -#################################################################### -#################################################################### - -- name: Delete networks - docker_network: - name: "{{ network_name }}" - state: absent - force: yes - loop: - - "{{ network_name_1 }}" - - "{{ network_name_2 }}" - loop_control: - loop_var: network_name - ignore_errors: yes - -- name: Delete volumes - docker_volume: - name: "{{ volume_name }}" - state: absent - loop: - - "{{ volume_name_1 }}" - - "{{ volume_name_2 }}" - loop_control: - loop_var: volume_name - ignore_errors: yes 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 668f2837ea0..7adb7dcb503 100644 --- a/test/units/modules/cloud/docker/test_docker_swarm_service.py +++ b/test/units/modules/cloud/docker/test_docker_swarm_service.py @@ -168,8 +168,8 @@ def test_has_dict_changed(docker_swarm_service): ) -def test_has_list_of_dicts_changed(docker_swarm_service): - assert docker_swarm_service.has_list_of_dicts_changed( +def test_has_list_changed(docker_swarm_service): + assert docker_swarm_service.has_list_changed( [ {"a": 1}, {"b": 1} @@ -178,7 +178,7 @@ def test_has_list_of_dicts_changed(docker_swarm_service): {"a": 1} ] ) - assert docker_swarm_service.has_list_of_dicts_changed( + assert docker_swarm_service.has_list_changed( [ {"a": 1}, ], @@ -187,7 +187,7 @@ def test_has_list_of_dicts_changed(docker_swarm_service): {"b": 1}, ] ) - assert not docker_swarm_service.has_list_of_dicts_changed( + assert not docker_swarm_service.has_list_changed( [ {"a": 1}, {"b": 1}, @@ -197,33 +197,33 @@ def test_has_list_of_dicts_changed(docker_swarm_service): {"b": 1} ] ) - assert not docker_swarm_service.has_list_of_dicts_changed( + assert not docker_swarm_service.has_list_changed( None, [ {"b": 1}, {"a": 1} ] ) - assert docker_swarm_service.has_list_of_dicts_changed( + assert docker_swarm_service.has_list_changed( [], [ {"b": 1}, {"a": 1} ] ) - assert not docker_swarm_service.has_list_of_dicts_changed( + assert not docker_swarm_service.has_list_changed( None, None ) - assert not docker_swarm_service.has_list_of_dicts_changed( + assert not docker_swarm_service.has_list_changed( [], None ) - assert not docker_swarm_service.has_list_of_dicts_changed( + assert not docker_swarm_service.has_list_changed( None, [] ) - assert not docker_swarm_service.has_list_of_dicts_changed( + assert not docker_swarm_service.has_list_changed( [ {"src": 1, "dst": 2}, {"src": 1, "dst": 2, "protocol": "udp"}, @@ -233,7 +233,7 @@ def test_has_list_of_dicts_changed(docker_swarm_service): {"src": 1, "dst": 2, "protocol": "udp"}, ] ) - assert not docker_swarm_service.has_list_of_dicts_changed( + assert not docker_swarm_service.has_list_changed( [ {"src": 1, "dst": 2, "protocol": "udp"}, {"src": 1, "dst": 3, "protocol": "tcp"}, @@ -243,7 +243,7 @@ def test_has_list_of_dicts_changed(docker_swarm_service): {"src": 1, "dst": 3, "protocol": "tcp"}, ] ) - assert docker_swarm_service.has_list_of_dicts_changed( + assert docker_swarm_service.has_list_changed( [ {"src": 1, "dst": 2, "protocol": "udp"}, {"src": 1, "dst": 2}, @@ -255,7 +255,7 @@ def test_has_list_of_dicts_changed(docker_swarm_service): {"src": 3, "dst": 4, "protocol": "tcp"}, ] ) - assert docker_swarm_service.has_list_of_dicts_changed( + assert docker_swarm_service.has_list_changed( [ {"src": 1, "dst": 3, "protocol": "tcp"}, {"src": 1, "dst": 2, "protocol": "udp"}, @@ -265,7 +265,7 @@ def test_has_list_of_dicts_changed(docker_swarm_service): {"src": 1, "dst": 2, "protocol": "udp"}, ] ) - assert docker_swarm_service.has_list_of_dicts_changed( + assert docker_swarm_service.has_list_changed( [ {"src": 1, "dst": 2, "protocol": "udp"}, {"src": 1, "dst": 2, "protocol": "tcp", "extra": {"test": "foo"}}, @@ -275,3 +275,86 @@ def test_has_list_of_dicts_changed(docker_swarm_service): {"src": 1, "dst": 2, "protocol": "tcp"}, ] ) + assert not docker_swarm_service.has_list_changed( + [{'id': '123', 'aliases': []}], + [{'id': '123'}] + ) + + +def test_get_docker_networks(docker_swarm_service): + network_names = [ + 'network_1', + 'network_2', + 'network_3', + 'network_4', + ] + networks = [ + network_names[0], + {'name': network_names[1]}, + {'name': network_names[2], 'aliases': ['networkalias1']}, + {'name': network_names[3], 'aliases': ['networkalias2'], 'options': {'foo': 'bar'}}, + ] + network_ids = { + network_names[0]: '1', + network_names[1]: '2', + network_names[2]: '3', + network_names[3]: '4', + } + parsed_networks = docker_swarm_service.get_docker_networks( + networks, + network_ids + ) + assert len(parsed_networks) == 4 + for i, network in enumerate(parsed_networks): + assert 'name' not in network + assert 'id' in network + expected_name = network_names[i] + assert network['id'] == network_ids[expected_name] + if i == 2: + assert network['aliases'] == ['networkalias1'] + if i == 3: + assert network['aliases'] == ['networkalias2'] + if i == 3: + assert 'foo' in network['options'] + # Test missing name + with pytest.raises(TypeError): + docker_swarm_service.get_docker_networks([{'invalid': 'err'}], {'err': 1}) + # test for invalid aliases type + with pytest.raises(TypeError): + docker_swarm_service.get_docker_networks( + [{'name': 'test', 'aliases': 1}], + {'test': 1} + ) + # Test invalid aliases elements + with pytest.raises(TypeError): + docker_swarm_service.get_docker_networks( + [{'name': 'test', 'aliases': [1]}], + {'test': 1} + ) + # Test for invalid options type + with pytest.raises(TypeError): + docker_swarm_service.get_docker_networks( + [{'name': 'test', 'options': 1}], + {'test': 1} + ) + # Test for invalid networks type + with pytest.raises(TypeError): + docker_swarm_service.get_docker_networks( + 1, + {'test': 1} + ) + # Test for non existing networks + with pytest.raises(ValueError): + docker_swarm_service.get_docker_networks( + [{'name': 'idontexist'}], + {'test': 1} + ) + # Test empty values + assert docker_swarm_service.get_docker_networks([], {}) == [] + assert docker_swarm_service.get_docker_networks(None, {}) is None + # Test invalid options + with pytest.raises(TypeError): + docker_swarm_service.get_docker_networks( + [{'name': 'test', 'nonexisting_option': 'foo'}], + {'test': '1'} + )