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 <felix@fontein.de>

* Set networks elements as raw in arg spec

Co-Authored-By: Felix Fontein <felix@fontein.de>

* Fix wrong variable naming

* Check for network options that are not valid

* Only check for None options

* Validate that aliases is a list
This commit is contained in:
Hannes Ljungberg 2019-08-18 08:55:54 +02:00 committed by Felix Fontein
parent aaaa4f1809
commit 13364fc530
5 changed files with 701 additions and 236 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- "docker_swarm_service - Support passing dictionaries in ``networks`` to allow setting ``aliases`` and ``options``."

View file

@ -358,7 +358,9 @@ options:
required: yes required: yes
networks: networks:
description: 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. - 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. If changes are made the service will then be removed and recreated.
- Corresponds to the C(--network) option of C(docker service create). - Corresponds to the C(--network) option of C(docker service create).
@ -997,6 +999,17 @@ EXAMPLES = '''
networks: networks:
- mynetwork - 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 - name: Set secrets
docker_swarm_service: docker_swarm_service:
name: myservice name: myservice
@ -1048,6 +1061,7 @@ from ansible.module_utils.docker.common import (
DockerBaseClass, DockerBaseClass,
convert_duration_to_nanosecond, convert_duration_to_nanosecond,
parse_healthcheck, parse_healthcheck,
clean_dict_booleans_for_docker_api,
RequestException, RequestException,
) )
@ -1116,6 +1130,58 @@ def get_docker_environment(env, env_files):
return sorted(env_list) 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): def get_nanoseconds_from_raw_option(name, value):
if value is None: if value is None:
return None return None
@ -1154,14 +1220,17 @@ def has_dict_changed(new_dict, old_dict):
if value is not None if value is not None
) )
for option, value in defined_options.items(): 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 True
return False 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: if new_list is None:
return False return False
@ -1169,13 +1238,20 @@ def has_list_of_dicts_changed(new_list, old_list):
if len(new_list) != len(old_list): if len(new_list) != len(old_list):
return True return True
for new_item, old_item in zip(new_list, old_list): 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 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 return False
class DockerService(DockerBaseClass): class DockerService(DockerBaseClass):
def __init__(self): def __init__(self, docker_api_version, docker_py_version):
super(DockerService, self).__init__() super(DockerService, self).__init__()
self.image = "" self.image = ""
self.command = None self.command = None
@ -1227,7 +1303,9 @@ class DockerService(DockerBaseClass):
self.update_max_failure_ratio = None self.update_max_failure_ratio = None
self.update_order = None self.update_order = None
self.working_dir = 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): def get_facts(self):
return { return {
@ -1281,6 +1359,22 @@ class DockerService(DockerBaseClass):
'working_dir': self.working_dir, '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 @staticmethod
def get_restart_config_from_ansible_params(params): def get_restart_config_from_ansible_params(params):
restart_config = params['restart_config'] or {} restart_config = params['restart_config'] or {}
@ -1474,11 +1568,18 @@ class DockerService(DockerBaseClass):
@classmethod @classmethod
def from_ansible_params( 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.image = image_digest
s.can_update_networks = can_update_networks
s.args = ap['args'] s.args = ap['args']
s.endpoint_mode = ap['endpoint_mode'] s.endpoint_mode = ap['endpoint_mode']
s.dns = ap['dns'] s.dns = ap['dns']
@ -1491,12 +1592,13 @@ class DockerService(DockerBaseClass):
s.labels = ap['labels'] s.labels = ap['labels']
s.container_labels = ap['container_labels'] s.container_labels = ap['container_labels']
s.mode = ap['mode'] s.mode = ap['mode']
s.networks = ap['networks']
s.stop_signal = ap['stop_signal'] s.stop_signal = ap['stop_signal']
s.user = ap['user'] s.user = ap['user']
s.working_dir = ap['working_dir'] s.working_dir = ap['working_dir']
s.read_only = ap['read_only'] s.read_only = ap['read_only']
s.networks = get_docker_networks(ap['networks'], network_ids)
s.command = ap['command'] s.command = ap['command']
if isinstance(s.command, string_types): if isinstance(s.command, string_types):
s.command = shlex.split(s.command) s.command = shlex.split(s.command)
@ -1650,13 +1752,13 @@ class DockerService(DockerBaseClass):
if self.mode != os.mode: if self.mode != os.mode:
needs_rebuild = True needs_rebuild = True
differences.add('mode', parameter=self.mode, active=os.mode) 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) 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) 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) 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) differences.add('networks', parameter=self.networks, active=os.networks)
needs_rebuild = not self.can_update_networks needs_rebuild = not self.can_update_networks
if self.replicas != os.replicas: if self.replicas != os.replicas:
@ -1774,18 +1876,6 @@ class DockerService(DockerBaseClass):
old_image = old_image.split('@')[0] old_image = old_image.split('@')[0]
return self.image != old_image, old_image 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): def build_container_spec(self):
mounts = None mounts = None
if self.mounts is not None: if self.mounts is not None:
@ -2000,6 +2090,10 @@ class DockerService(DockerBaseClass):
task_template_args['resources'] = resources task_template_args['resources'] = resources
if self.force_update: if self.force_update:
task_template_args['force_update'] = 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) return types.TaskTemplate(container_spec=container_spec, **task_template_args)
def build_service_mode(self): def build_service_mode(self):
@ -2007,22 +2101,17 @@ class DockerService(DockerBaseClass):
self.replicas = None self.replicas = None
return types.ServiceMode(self.mode, replicas=self.replicas) return types.ServiceMode(self.mode, replicas=self.replicas)
def build_networks(self, docker_networks): def build_networks(self):
networks = None networks = None
if self.networks is not None: if self.networks is not None:
networks = [] networks = []
for network_name in self.networks: for network in self.networks:
network_id = None docker_network = {'Target': network['id']}
try: if 'aliases' in network:
network_id = list( docker_network['Aliases'] = network['aliases']
filter(lambda n: n['name'] == network_name, docker_networks) if 'options' in network:
)[0]['id'] docker_network['DriverOpts'] = network['options']
except (IndexError, KeyError): networks.append(docker_network)
pass
if network_id:
networks.append({'Target': network_id})
else:
raise Exception('no docker networks named: %s' % network_name)
return networks return networks
def build_endpoint_spec(self): def build_endpoint_spec(self):
@ -2043,7 +2132,7 @@ class DockerService(DockerBaseClass):
endpoint_spec_args['mode'] = self.endpoint_mode endpoint_spec_args['mode'] = self.endpoint_mode
return types.EndpointSpec(**endpoint_spec_args) if endpoint_spec_args else None 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() container_spec = self.build_container_spec()
placement = self.build_placement() placement = self.build_placement()
task_template = self.build_task_template(container_spec, placement) task_template = self.build_task_template(container_spec, placement)
@ -2051,7 +2140,6 @@ class DockerService(DockerBaseClass):
update_config = self.build_update_config() update_config = self.build_update_config()
rollback_config = self.build_rollback_config() rollback_config = self.build_rollback_config()
service_mode = self.build_service_mode() service_mode = self.build_service_mode()
networks = self.build_networks(docker_networks)
endpoint_spec = self.build_endpoint_spec() endpoint_spec = self.build_endpoint_spec()
service = {'task_template': task_template, 'mode': service_mode} service = {'task_template': task_template, 'mode': service_mode}
@ -2059,12 +2147,14 @@ class DockerService(DockerBaseClass):
service['update_config'] = update_config service['update_config'] = update_config
if rollback_config: if rollback_config:
service['rollback_config'] = rollback_config service['rollback_config'] = rollback_config
if networks:
service['networks'] = networks
if endpoint_spec: if endpoint_spec:
service['endpoint_spec'] = endpoint_spec service['endpoint_spec'] = endpoint_spec
if self.labels: if self.labels:
service['labels'] = 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 return service
@ -2075,15 +2165,12 @@ class DockerServiceManager(object):
self.retries = 2 self.retries = 2
self.diff_tracker = None 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): def get_service(self, name):
try: try:
raw_data = self.client.inspect_service(name) raw_data = self.client.inspect_service(name)
except NotFound: except NotFound:
return None return None
ds = DockerService() ds = DockerService(self.client.docker_api_version, self.client.docker_py_version)
task_template_data = raw_data['Spec']['TaskTemplate'] task_template_data = raw_data['Spec']['TaskTemplate']
ds.image = task_template_data['ContainerSpec']['Image'] ds.image = task_template_data['ContainerSpec']['Image']
@ -2263,24 +2350,22 @@ class DockerServiceManager(object):
'mode': secret_data['File'].get('Mode') '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')) raw_networks_data = task_template_data.get('Networks', raw_data['Spec'].get('Networks'))
if raw_networks_data: if raw_networks_data:
ds.networks = [] ds.networks = []
for network_data in raw_networks_data: for network_data in raw_networks_data:
network_name = [network_name_id['name'] for network_name_id in networks_names_ids if network = {'id': network_data['Target']}
network_name_id['id'] == network_data['Target']] if 'Aliases' in network_data:
if len(network_name) == 0: network['aliases'] = network_data['Aliases']
ds.networks.append(network_data['Target']) if 'DriverOpts' in network_data:
else: network['options'] = network_data['DriverOpts']
ds.networks.append(network_name[0]) ds.networks.append(network)
ds.service_version = raw_data['Version']['Index'] ds.service_version = raw_data['Version']['Index']
ds.service_id = raw_data['ID'] ds.service_id = raw_data['ID']
return ds return ds
def update_service(self, name, old_service, new_service): 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( result = self.client.update_service(
old_service.service_id, old_service.service_id,
old_service.service_version, old_service.service_version,
@ -2292,7 +2377,7 @@ class DockerServiceManager(object):
self.client.report_warnings(result, ['Warning']) self.client.report_warnings(result, ['Warning'])
def create_service(self, name, service): 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) result = self.client.create_service(name=name, **service_data)
self.client.report_warnings(result, ['Warning']) self.client.report_warnings(result, ['Warning'])
@ -2313,11 +2398,9 @@ class DockerServiceManager(object):
digest = distribution_data['Descriptor']['digest'] digest = distribution_data['Descriptor']['digest']
return '%s@%s' % (name, digest) return '%s@%s' % (name, digest)
def can_update_networks(self): def get_networks_names_ids(self):
# Before Docker API 1.29 adding/removing networks was not supported return dict(
return ( (network['Name'], network['Id']) for network in self.client.networks()
self.client.docker_api_version >= LooseVersion('1.29') and
self.client.docker_py_version >= LooseVersion('2.7')
) )
def get_missing_secret_ids(self): def get_missing_secret_ids(self):
@ -2392,16 +2475,18 @@ class DockerServiceManager(object):
% (module.params['name'], e) % (module.params['name'], e)
) )
try: try:
can_update_networks = self.can_update_networks()
secret_ids = self.get_missing_secret_ids() secret_ids = self.get_missing_secret_ids()
config_ids = self.get_missing_config_ids() config_ids = self.get_missing_config_ids()
network_ids = self.get_networks_names_ids()
new_service = DockerService.from_ansible_params( new_service = DockerService.from_ansible_params(
module.params, module.params,
current_service, current_service,
image_digest, image_digest,
can_update_networks,
secret_ids, secret_ids,
config_ids config_ids,
network_ids,
self.client.docker_api_version,
self.client.docker_py_version
) )
except Exception as e: except Exception as e:
return self.client.fail( return self.client.fail(
@ -2567,7 +2652,7 @@ def main():
gid=dict(type='str'), gid=dict(type='str'),
mode=dict(type='int'), mode=dict(type='int'),
)), )),
networks=dict(type='list', elements='str'), networks=dict(type='list', elements='raw'),
command=dict(type='raw'), command=dict(type='raw'),
args=dict(type='list', elements='str'), args=dict(type='list', elements='str'),
env=dict(type='raw'), env=dict(type='raw'),

View file

@ -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

View file

@ -1,25 +1,12 @@
--- ---
- name: Registering container name - name: Registering service name
set_fact: set_fact:
service_name: "{{ name_prefix ~ '-options' }}" 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: set_fact:
service_names: "{{ service_names + [service_name] }}" 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 ############################################################ ## args ############################################################
@ -1275,119 +1262,6 @@
- mode_2 is not changed - mode_2 is not changed
- mode_3 is 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 ############################################### ## stop_grace_period ###############################################
#################################################################### ####################################################################
@ -1915,30 +1789,3 @@
- working_dir_1 is changed - working_dir_1 is changed
- working_dir_2 is not changed - working_dir_2 is not changed
- working_dir_3 is 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

View file

@ -168,8 +168,8 @@ def test_has_dict_changed(docker_swarm_service):
) )
def test_has_list_of_dicts_changed(docker_swarm_service): def test_has_list_changed(docker_swarm_service):
assert docker_swarm_service.has_list_of_dicts_changed( assert docker_swarm_service.has_list_changed(
[ [
{"a": 1}, {"a": 1},
{"b": 1} {"b": 1}
@ -178,7 +178,7 @@ def test_has_list_of_dicts_changed(docker_swarm_service):
{"a": 1} {"a": 1}
] ]
) )
assert docker_swarm_service.has_list_of_dicts_changed( assert docker_swarm_service.has_list_changed(
[ [
{"a": 1}, {"a": 1},
], ],
@ -187,7 +187,7 @@ def test_has_list_of_dicts_changed(docker_swarm_service):
{"b": 1}, {"b": 1},
] ]
) )
assert not docker_swarm_service.has_list_of_dicts_changed( assert not docker_swarm_service.has_list_changed(
[ [
{"a": 1}, {"a": 1},
{"b": 1}, {"b": 1},
@ -197,33 +197,33 @@ def test_has_list_of_dicts_changed(docker_swarm_service):
{"b": 1} {"b": 1}
] ]
) )
assert not docker_swarm_service.has_list_of_dicts_changed( assert not docker_swarm_service.has_list_changed(
None, None,
[ [
{"b": 1}, {"b": 1},
{"a": 1} {"a": 1}
] ]
) )
assert docker_swarm_service.has_list_of_dicts_changed( assert docker_swarm_service.has_list_changed(
[], [],
[ [
{"b": 1}, {"b": 1},
{"a": 1} {"a": 1}
] ]
) )
assert not docker_swarm_service.has_list_of_dicts_changed( assert not docker_swarm_service.has_list_changed(
None, None,
None None
) )
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, 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},
{"src": 1, "dst": 2, "protocol": "udp"}, {"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"}, {"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": 2, "protocol": "udp"},
{"src": 1, "dst": 3, "protocol": "tcp"}, {"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"}, {"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, "protocol": "udp"},
{"src": 1, "dst": 2}, {"src": 1, "dst": 2},
@ -255,7 +255,7 @@ def test_has_list_of_dicts_changed(docker_swarm_service):
{"src": 3, "dst": 4, "protocol": "tcp"}, {"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": 3, "protocol": "tcp"},
{"src": 1, "dst": 2, "protocol": "udp"}, {"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"}, {"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": "udp"},
{"src": 1, "dst": 2, "protocol": "tcp", "extra": {"test": "foo"}}, {"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"}, {"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'}
)