diff --git a/changelogs/fragments/docker-swarm-service-defaults.yml b/changelogs/fragments/docker-swarm-service-defaults.yml new file mode 100644 index 00000000000..d1f025ae0d0 --- /dev/null +++ b/changelogs/fragments/docker-swarm-service-defaults.yml @@ -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. diff --git a/lib/ansible/modules/cloud/docker/docker_swarm_service.py b/lib/ansible/modules/cloud/docker/docker_swarm_service.py index b3eeb50aef0..46aa4f81a82 100644 --- a/lib/ansible/modules/cloud/docker/docker_swarm_service.py +++ b/lib/ansible/modules/cloud/docker/docker_swarm_service.py @@ -177,7 +177,7 @@ options: - 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) - Maps docker service --config option. - default: [] + default: null networks: required: false default: [] @@ -189,8 +189,9 @@ options: required: false description: - 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 , 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 + - If api_version >= 1.32 the dictionaries can contain the attribute 'mode' set to 'ingress' or 'host' (default 'ingress'). replicas: required: false default: -1 @@ -262,13 +263,11 @@ options: - Maps to docker service --update-max-failure-ratio update_order: required: false - default: stop-first + default: null description: - Specifies the order of operations when rolling out an updated task. - Maps to docker service --update-order - choices: - - stop-first - - start-first + - Requires docker api version >= 1.29 user: required: false default: root @@ -496,7 +495,7 @@ class DockerService(DockerBaseClass): self.mode = "replicated" self.user = "root" self.mounts = [] - self.configs = [] + self.configs = None self.secrets = [] self.constraints = [] self.networks = [] @@ -513,7 +512,7 @@ class DockerService(DockerBaseClass): self.update_failure_action = "continue" self.update_monitor = 5000000000 self.update_max_failure_ratio = 0.00 - self.update_order = "stop-first" + self.update_order = None def get_facts(self): return { @@ -530,7 +529,7 @@ class DockerService(DockerBaseClass): 'env': self.env, 'force_update': self.force_update, 'log_driver': self.log_driver, - 'log_driver_options ': self.log_driver_options, + 'log_driver_options': self.log_driver_options, 'publish': self.publish, 'constraints': self.constraints, 'labels': self.labels, @@ -608,13 +607,13 @@ class DockerService(DockerBaseClass): for param_p in ap['publish']: service_p = {} 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['target_port'] = int(param_p['target_port']) if service_p['protocol'] not in ['tcp', 'udp']: raise ValueError("got publish.protocol '%s', valid values:'tcp', 'udp'" % 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'" % service_p['mode']) s.publish.append(service_p) @@ -627,16 +626,18 @@ class DockerService(DockerBaseClass): service_m['target'] = param_m['target'] s.mounts.append(service_m) - s.configs = [] - for param_m in ap['configs']: - service_c = {} - service_c['config_id'] = param_m['config_id'] - service_c['config_name'] = str(param_m['config_name']) - service_c['filename'] = param_m.get('filename', service_c['config_name']) - service_c['uid'] = int(param_m.get('uid', "0")) - service_c['gid'] = int(param_m.get('gid', "0")) - service_c['mode'] = param_m.get('mode', 0o444) - s.configs.append(service_c) + s.configs = None + if ap['configs']: + s.configs = [] + for param_m in ap['configs']: + service_c = {} + service_c['config_id'] = param_m['config_id'] + service_c['config_name'] = str(param_m['config_name']) + service_c['filename'] = param_m.get('filename', service_c['config_name']) + service_c['uid'] = int(param_m.get('uid', "0")) + service_c['gid'] = int(param_m.get('gid', "0")) + service_c['mode'] = param_m.get('mode', 0o444) + s.configs.append(service_c) s.secrets = [] for param_m in ap['secrets']: @@ -754,18 +755,21 @@ class DockerService(DockerBaseClass): read_only=mount_config['readonly']) ) - configs = [] - for config_config in self.configs: - configs.append( - types.ConfigReference( - config_id=config_config['config_id'], - config_name=config_config['config_name'], - filename=config_config.get('filename'), - uid=config_config.get('uid'), - gid=config_config.get('gid'), - mode=config_config.get('mode') + configs = None + if self.configs: + configs = [] + for config_config in self.configs: + configs.append( + types.ConfigReference( + config_id=config_config['config_id'], + config_name=config_config['config_name'], + filename=config_config.get('filename'), + uid=config_config.get('uid'), + gid=config_config.get('gid'), + mode=config_config.get('mode') + ) ) - ) + secrets = [] for secret_config in self.secrets: secrets.append( @@ -846,7 +850,10 @@ class DockerService(DockerBaseClass): ports = {} 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) 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_monitor = update_config_data['Monitor'] 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) if dns_config: @@ -913,7 +922,7 @@ class DockerServiceManager(): for port in raw_data_endpoint_spec.get('Ports', []): ds.publish.append({ 'protocol': port['Protocol'], - 'mode': port.get('PublishMode', 'ingress'), + 'mode': port.get('PublishMode', None), 'published_port': int(port['PublishedPort']), 'target_port': int(port['TargetPort'])}) @@ -1019,7 +1028,8 @@ class DockerServiceManager(): {'param': 'hostname', 'attribute': 'hostname', 'min_version': '1.25'}, {'param': 'tty', 'attribute': 'tty', '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 empty_service = DockerService() for pv in parameters_versions: @@ -1113,7 +1123,7 @@ def main(): image=dict(type='str'), state=dict(default="present", choices=['present', 'absent']), mounts=dict(default=[], type='list'), - configs=dict(default=[], type='list'), + configs=dict(default=None, type='list'), secrets=dict(default=[], type='list'), networks=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_monitor=dict(default=5000000000, type='int'), 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')) required_if = [ ('state', 'present', ['image']) diff --git a/test/integration/targets/docker_swarm_service/aliases b/test/integration/targets/docker_swarm_service/aliases new file mode 100644 index 00000000000..2b3832dde58 --- /dev/null +++ b/test/integration/targets/docker_swarm_service/aliases @@ -0,0 +1,4 @@ +shippable/posix/group2 +skip/osx +skip/freebsd +destructive diff --git a/test/integration/targets/docker_swarm_service/meta/main.yml b/test/integration/targets/docker_swarm_service/meta/main.yml new file mode 100644 index 00000000000..07da8c6ddae --- /dev/null +++ b/test/integration/targets/docker_swarm_service/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - setup_docker diff --git a/test/integration/targets/docker_swarm_service/tasks/main.yml b/test/integration/targets/docker_swarm_service/tasks/main.yml new file mode 100644 index 00000000000..2368c7e296d --- /dev/null +++ b/test/integration/targets/docker_swarm_service/tasks/main.yml @@ -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 diff --git a/test/integration/targets/docker_swarm_service/tasks/test_swarm_service.yml b/test/integration/targets/docker_swarm_service/tasks/test_swarm_service.yml new file mode 100644 index 00000000000..d3d0d633f98 --- /dev/null +++ b/test/integration/targets/docker_swarm_service/tasks/test_swarm_service.yml @@ -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 diff --git a/test/integration/targets/docker_swarm_service/vars/main.yml b/test/integration/targets/docker_swarm_service/vars/main.yml new file mode 100644 index 00000000000..7bcd35e2e1d --- /dev/null +++ b/test/integration/targets/docker_swarm_service/vars/main.yml @@ -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