Docker swarm service integration tests (#45674)

* integration test for docker_swarm_service

* ensure stack de-initialization

* Set default value for 'configs' parameter to None

Docker-py uses None as a default value for configs.
Using the same default here allows to create services on older docker
setups (docker_api<1.30).

* Set default value for 'update_order' parameter to None

Docker-py uses None as a default value for update_order.
Using the same default here allows to create services on older docker
setups (docker_api<1.29)

* Set default value for 'publish.mode' parameter to None

Docker-py uses None as a default value for publish_mode.
Using the same default here allows to create services on older docker
setups (docker_api<1.32)

* Allow tests to run on older version of docker.

* remove workarounds for old docker versions

* test correct swarm cleanup

* changelog fragment for docker_swarm_service defaults change
pull/47742/head
Dario Zanzico 6 years ago committed by John R Barker
parent bba8c23585
commit 2162d7d4de

@ -0,0 +1,2 @@
minor_changes:
- change default value for ``configs`` from ``[]`` to ``null`` and for ``update_order`` from ``stop-first`` to ``null``, matching docker API and allowing the module to interact with older docker daemons.

@ -177,7 +177,7 @@ options:
- List of dictionaries describing the service configs. - List of dictionaries describing the service configs.
- Every item must be a dictionary exposing the keys config_id, config_name, filename, uid (defaults to 0), gid (defaults to 0), mode (defaults to 0o444) - Every item must be a dictionary exposing the keys config_id, config_name, filename, uid (defaults to 0), gid (defaults to 0), mode (defaults to 0o444)
- Maps docker service --config option. - Maps docker service --config option.
default: [] default: null
networks: networks:
required: false required: false
default: [] default: []
@ -189,8 +189,9 @@ options:
required: false required: false
description: description:
- List of dictionaries describing the service published ports. - List of dictionaries describing the service published ports.
- Every item must be a dictionary exposing the keys published_port, target_port, protocol (defaults to 'tcp'), mode <ingress|host>, default to ingress. - Every item must be a dictionary exposing the keys published_port, target_port, protocol (defaults to 'tcp')
- Only used with api_version >= 1.25 - Only used with api_version >= 1.25
- If api_version >= 1.32 the dictionaries can contain the attribute 'mode' set to 'ingress' or 'host' (default 'ingress').
replicas: replicas:
required: false required: false
default: -1 default: -1
@ -262,13 +263,11 @@ options:
- Maps to docker service --update-max-failure-ratio - Maps to docker service --update-max-failure-ratio
update_order: update_order:
required: false required: false
default: stop-first default: null
description: description:
- Specifies the order of operations when rolling out an updated task. - Specifies the order of operations when rolling out an updated task.
- Maps to docker service --update-order - Maps to docker service --update-order
choices: - Requires docker api version >= 1.29
- stop-first
- start-first
user: user:
required: false required: false
default: root default: root
@ -496,7 +495,7 @@ class DockerService(DockerBaseClass):
self.mode = "replicated" self.mode = "replicated"
self.user = "root" self.user = "root"
self.mounts = [] self.mounts = []
self.configs = [] self.configs = None
self.secrets = [] self.secrets = []
self.constraints = [] self.constraints = []
self.networks = [] self.networks = []
@ -513,7 +512,7 @@ class DockerService(DockerBaseClass):
self.update_failure_action = "continue" self.update_failure_action = "continue"
self.update_monitor = 5000000000 self.update_monitor = 5000000000
self.update_max_failure_ratio = 0.00 self.update_max_failure_ratio = 0.00
self.update_order = "stop-first" self.update_order = None
def get_facts(self): def get_facts(self):
return { return {
@ -530,7 +529,7 @@ class DockerService(DockerBaseClass):
'env': self.env, 'env': self.env,
'force_update': self.force_update, 'force_update': self.force_update,
'log_driver': self.log_driver, 'log_driver': self.log_driver,
'log_driver_options ': self.log_driver_options, 'log_driver_options': self.log_driver_options,
'publish': self.publish, 'publish': self.publish,
'constraints': self.constraints, 'constraints': self.constraints,
'labels': self.labels, 'labels': self.labels,
@ -608,13 +607,13 @@ class DockerService(DockerBaseClass):
for param_p in ap['publish']: for param_p in ap['publish']:
service_p = {} service_p = {}
service_p['protocol'] = param_p.get('protocol', 'tcp') service_p['protocol'] = param_p.get('protocol', 'tcp')
service_p['mode'] = param_p.get('mode', 'ingress') service_p['mode'] = param_p.get('mode', None)
service_p['published_port'] = int(param_p['published_port']) service_p['published_port'] = int(param_p['published_port'])
service_p['target_port'] = int(param_p['target_port']) service_p['target_port'] = int(param_p['target_port'])
if service_p['protocol'] not in ['tcp', 'udp']: if service_p['protocol'] not in ['tcp', 'udp']:
raise ValueError("got publish.protocol '%s', valid values:'tcp', 'udp'" % raise ValueError("got publish.protocol '%s', valid values:'tcp', 'udp'" %
service_p['protocol']) service_p['protocol'])
if service_p['mode'] not in ['ingress', 'host']: if service_p['mode'] not in [None, 'ingress', 'host']:
raise ValueError("got publish.mode '%s', valid values:'ingress', 'host'" % raise ValueError("got publish.mode '%s', valid values:'ingress', 'host'" %
service_p['mode']) service_p['mode'])
s.publish.append(service_p) s.publish.append(service_p)
@ -627,16 +626,18 @@ class DockerService(DockerBaseClass):
service_m['target'] = param_m['target'] service_m['target'] = param_m['target']
s.mounts.append(service_m) s.mounts.append(service_m)
s.configs = [] s.configs = None
for param_m in ap['configs']: if ap['configs']:
service_c = {} s.configs = []
service_c['config_id'] = param_m['config_id'] for param_m in ap['configs']:
service_c['config_name'] = str(param_m['config_name']) service_c = {}
service_c['filename'] = param_m.get('filename', service_c['config_name']) service_c['config_id'] = param_m['config_id']
service_c['uid'] = int(param_m.get('uid', "0")) service_c['config_name'] = str(param_m['config_name'])
service_c['gid'] = int(param_m.get('gid', "0")) service_c['filename'] = param_m.get('filename', service_c['config_name'])
service_c['mode'] = param_m.get('mode', 0o444) service_c['uid'] = int(param_m.get('uid', "0"))
s.configs.append(service_c) service_c['gid'] = int(param_m.get('gid', "0"))
service_c['mode'] = param_m.get('mode', 0o444)
s.configs.append(service_c)
s.secrets = [] s.secrets = []
for param_m in ap['secrets']: for param_m in ap['secrets']:
@ -754,18 +755,21 @@ class DockerService(DockerBaseClass):
read_only=mount_config['readonly']) read_only=mount_config['readonly'])
) )
configs = [] configs = None
for config_config in self.configs: if self.configs:
configs.append( configs = []
types.ConfigReference( for config_config in self.configs:
config_id=config_config['config_id'], configs.append(
config_name=config_config['config_name'], types.ConfigReference(
filename=config_config.get('filename'), config_id=config_config['config_id'],
uid=config_config.get('uid'), config_name=config_config['config_name'],
gid=config_config.get('gid'), filename=config_config.get('filename'),
mode=config_config.get('mode') uid=config_config.get('uid'),
gid=config_config.get('gid'),
mode=config_config.get('mode')
)
) )
)
secrets = [] secrets = []
for secret_config in self.secrets: for secret_config in self.secrets:
secrets.append( secrets.append(
@ -846,7 +850,10 @@ class DockerService(DockerBaseClass):
ports = {} ports = {}
for port in self.publish: for port in self.publish:
ports[int(port['published_port'])] = (int(port['target_port']), port['protocol'], port['mode']) if port['mode']:
ports[int(port['published_port'])] = (int(port['target_port']), port['protocol'], port['mode'])
else:
ports[int(port['published_port'])] = (int(port['target_port']), port['protocol'])
endpoint_spec = types.EndpointSpec(mode=self.endpoint_mode, ports=ports) endpoint_spec = types.EndpointSpec(mode=self.endpoint_mode, ports=ports)
return update_policy, task_template, networks, endpoint_spec, mode, self.labels return update_policy, task_template, networks, endpoint_spec, mode, self.labels
@ -883,7 +890,9 @@ class DockerServiceManager():
ds.update_failure_action = update_config_data['FailureAction'] ds.update_failure_action = update_config_data['FailureAction']
ds.update_monitor = update_config_data['Monitor'] ds.update_monitor = update_config_data['Monitor']
ds.update_max_failure_ratio = update_config_data['MaxFailureRatio'] ds.update_max_failure_ratio = update_config_data['MaxFailureRatio']
ds.update_order = update_config_data['Order']
if 'Order' in update_config_data:
ds.update_order = update_config_data['Order']
dns_config = task_template_data['ContainerSpec'].get('DNSConfig', None) dns_config = task_template_data['ContainerSpec'].get('DNSConfig', None)
if dns_config: if dns_config:
@ -913,7 +922,7 @@ class DockerServiceManager():
for port in raw_data_endpoint_spec.get('Ports', []): for port in raw_data_endpoint_spec.get('Ports', []):
ds.publish.append({ ds.publish.append({
'protocol': port['Protocol'], 'protocol': port['Protocol'],
'mode': port.get('PublishMode', 'ingress'), 'mode': port.get('PublishMode', None),
'published_port': int(port['PublishedPort']), 'published_port': int(port['PublishedPort']),
'target_port': int(port['TargetPort'])}) 'target_port': int(port['TargetPort'])})
@ -1019,7 +1028,8 @@ class DockerServiceManager():
{'param': 'hostname', 'attribute': 'hostname', 'min_version': '1.25'}, {'param': 'hostname', 'attribute': 'hostname', 'min_version': '1.25'},
{'param': 'tty', 'attribute': 'tty', 'min_version': '1.25'}, {'param': 'tty', 'attribute': 'tty', 'min_version': '1.25'},
{'param': 'secrets', 'attribute': 'secrets', 'min_version': '1.25'}, {'param': 'secrets', 'attribute': 'secrets', 'min_version': '1.25'},
{'param': 'configs', 'attribute': 'configs', 'min_version': '1.30'}] {'param': 'configs', 'attribute': 'configs', 'min_version': '1.30'},
{'param': 'update_order', 'attribute': 'update_order', 'min_version': '1.29'}]
params = self.client.module.params params = self.client.module.params
empty_service = DockerService() empty_service = DockerService()
for pv in parameters_versions: for pv in parameters_versions:
@ -1113,7 +1123,7 @@ def main():
image=dict(type='str'), image=dict(type='str'),
state=dict(default="present", choices=['present', 'absent']), state=dict(default="present", choices=['present', 'absent']),
mounts=dict(default=[], type='list'), mounts=dict(default=[], type='list'),
configs=dict(default=[], type='list'), configs=dict(default=None, type='list'),
secrets=dict(default=[], type='list'), secrets=dict(default=[], type='list'),
networks=dict(default=[], type='list'), networks=dict(default=[], type='list'),
args=dict(default=[], type='list'), args=dict(default=[], type='list'),
@ -1146,7 +1156,7 @@ def main():
update_failure_action=dict(default='continue', choices=['continue', 'pause']), update_failure_action=dict(default='continue', choices=['continue', 'pause']),
update_monitor=dict(default=5000000000, type='int'), update_monitor=dict(default=5000000000, type='int'),
update_max_failure_ratio=dict(default=0, type='float'), update_max_failure_ratio=dict(default=0, type='float'),
update_order=dict(default='stop-first', choices=['stop-first', 'start-first']), update_order=dict(default=None, type='string'),
user=dict(default='root')) user=dict(default='root'))
required_if = [ required_if = [
('state', 'present', ['image']) ('state', 'present', ['image'])

@ -0,0 +1,4 @@
shippable/posix/group2
skip/osx
skip/freebsd
destructive

@ -0,0 +1,4 @@
- include_tasks: test_swarm_service.yml
when:
- ansible_os_family != 'RedHat' or ansible_distribution_major_version != '6'
- ansible_distribution != 'Fedora' or ansible_distribution_major_version|int >= 26

@ -0,0 +1,122 @@
- name: Create a Swarm cluster
docker_swarm:
state: present
advertise_addr: "{{ansible_default_ipv4.address}}"
- name: Create a swarm service without name
register: output
docker_swarm_service:
state: present
ignore_errors: yes
- name: assert failure when name not set
assert:
that:
- output is failed
- 'output.msg == "missing required arguments: name"'
- name: Remove an non-existing service
register: output
docker_swarm_service:
state: absent
name: non_existing_service
- name: assert output not changed when deleting non-existing service
assert:
that:
- output is not changed
- name: create sample service
register: output
docker_swarm_service:
name: test_service
endpoint_mode: dnsrr
image: busybox
args:
- sleep
- "3600"
- name: assert sample service is created
assert:
that:
- output is changed
- name: change service args
register: output
docker_swarm_service:
name: test_service
image: busybox
args:
- sleep
- "1800"
- name: assert service args are correct
assert:
that:
- output.ansible_docker_service.args == ['sleep', '1800']
- name: set service mode to global
register: output
docker_swarm_service:
name: test_service
image: busybox
endpoint_mode: vip
mode: global
args:
- sleep
- "1800"
- name: assert service mode changed caused service rebuild
assert:
that:
- output.rebuilt
- name: add published ports to service
register: output
docker_swarm_service:
name: test_service
image: busybox
mode: global
args:
- sleep
- "1800"
endpoint_mode: vip
publish:
- protocol: tcp
published_port: 60001
target_port: 60001
- protocol: udp
published_port: 60001
target_port: 60001
- name: assert service matches expectations
assert:
that:
- output.ansible_docker_service == service_expected_output
- name: delete sample service
register: output
docker_swarm_service:
name: test_service
state: absent
- name: assert service deletion returns changed
assert:
that:
- output is success
- output is changed
- name: Remove the Swarm cluster
docker_swarm:
state: absent
force: true
- name: Try reitializing the swarm cluster
docker_swarm:
state: present
advertise_addr: "{{ansible_default_ipv4.address}}"
- name: Clean the docker daemon status
docker_swarm:
state: absent
force: true

@ -0,0 +1,38 @@
service_expected_output:
args: [sleep, '1800']
configs: null
constraints: []
container_labels: {}
dns: []
dns_options: []
dns_search: []
endpoint_mode: vip
env: []
force_update: null
hostname: ''
image: busybox
labels: {}
limit_cpu: 0.0
limit_memory: 0
log_driver: json-file
log_driver_options: {}
mode: global
mounts: []
networks: []
publish:
- {mode: null, protocol: tcp, published_port: 60001, target_port: 60001}
- {mode: null, protocol: udp, published_port: 60001, target_port: 60001}
replicas: null
reserve_cpu: 0.0
reserve_memory: 0
restart_policy: none
restart_policy_attempts: 0
restart_policy_delay: 0
restart_policy_window: 0
tty: false
update_delay: 10
update_failure_action: continue
update_max_failure_ratio: 0.0
update_monitor: 5000000000
update_order: null
update_parallelism: 1
Loading…
Cancel
Save