diff --git a/changelogs/fragments/53559-docker_swarm_service-mounts-options.yaml b/changelogs/fragments/53559-docker_swarm_service-mounts-options.yaml new file mode 100644 index 00000000000..462a4dadbeb --- /dev/null +++ b/changelogs/fragments/53559-docker_swarm_service-mounts-options.yaml @@ -0,0 +1,2 @@ +minor_changes: + - "docker_swarm_service - Extended ``mounts`` options. It now also accepts ``labels``, ``propagation``, ``no_copy``, ``driver_config``, ``tmpfs_size``, ``tmpfs_mode``." diff --git a/lib/ansible/modules/cloud/docker/docker_swarm_service.py b/lib/ansible/modules/cloud/docker/docker_swarm_service.py index 4587e5d04d5..689f54490aa 100644 --- a/lib/ansible/modules/cloud/docker/docker_swarm_service.py +++ b/lib/ansible/modules/cloud/docker/docker_swarm_service.py @@ -299,7 +299,59 @@ options: description: - Whether the mount should be read-only. type: bool - default: no + labels: + description: + - Volume labels to apply. + type: dict + version_added: "2.8" + propagation: + description: + - The propagation mode to use. + - Can only be used when I(mode) is C(bind). + type: str + choices: + - shared + - slave + - private + - rshared + - rslave + - rprivate + version_added: "2.8" + no_copy: + description: + - Disable copying of data from a container when a volume is created. + - Can only be used when I(mode) is C(volume). + type: bool + version_added: "2.8" + driver_config: + description: + - Volume driver configuration. + - Can only be used when I(mode) is C(volume). + suboptions: + name: + description: + - Name of the volume-driver plugin to use for the volume. + type: str + options: + description: + - Options as key-value pairs to pass to the driver for this volume. + type: dict + type: dict + version_added: "2.8" + tmpfs_size: + description: + - "Size of the tmpfs mount (format: C([])). Number is a positive integer. + Unit can be C(B) (byte), C(K) (kibibyte, 1024B), C(M) (mebibyte), C(G) (gibibyte), + C(T) (tebibyte), or C(P) (pebibyte)." + - Can only be used when I(mode) is C(tmpfs). + type: str + version_added: "2.8" + tmpfs_mode: + description: + - File mode of the tmpfs in octal. + - Can only be used when I(mode) is C(tmpfs). + type: int + version_added: "2.8" name: description: - Service name. @@ -743,7 +795,13 @@ swarm_service: "readonly": false, "source": "/tmp/", "target": "/remote_tmp/", - "type": "bind" + "type": "bind", + "labels": null, + "propagation": null, + "no_copy": null, + "driver_config": null, + "tmpfs_size": null, + "tmpfs_mode": null } ], "networks": null, @@ -1410,6 +1468,21 @@ class DockerService(DockerBaseClass): service_m['type'] = param_m['type'] service_m['source'] = param_m['source'] service_m['target'] = param_m['target'] + service_m['labels'] = param_m['labels'] + service_m['no_copy'] = param_m['no_copy'] + service_m['propagation'] = param_m['propagation'] + service_m['driver_config'] = param_m['driver_config'] + service_m['tmpfs_mode'] = param_m['tmpfs_mode'] + tmpfs_size = param_m['tmpfs_size'] + if tmpfs_size is not None: + try: + tmpfs_size = human_to_bytes(tmpfs_size) + except ValueError as exc: + raise ValueError( + 'Failed to convert tmpfs_size to bytes: %s' % exc + ) + + service_m['tmpfs_size'] = tmpfs_size s.mounts.append(service_m) if ap['configs'] is not None: @@ -1592,14 +1665,25 @@ class DockerService(DockerBaseClass): if self.mounts is not None: mounts = [] for mount_config in self.mounts: - mounts.append( - types.Mount( - target=mount_config['target'], - source=mount_config['source'], - type=mount_config['type'], - read_only=mount_config['readonly'] - ) - ) + mount_options = { + 'target': 'target', + 'source': 'source', + 'type': 'type', + 'readonly': 'read_only', + 'propagation': 'propagation', + 'labels': 'labels', + 'no_copy': 'no_copy', + 'driver_config': 'driver_config', + 'tmpfs_size': 'tmpfs_size', + 'tmpfs_mode': 'tmpfs_mode' + } + mount_args = {} + for option, mount_arg in mount_options.items(): + value = mount_config.get(option) + if value is not None: + mount_args[mount_arg] = value + + mounts.append(types.Mount(**mount_args)) configs = None if self.configs is not None: @@ -1962,11 +2046,24 @@ class DockerServiceManager(object): if raw_data_mounts: ds.mounts = [] for mount_data in raw_data_mounts: + bind_options = mount_data.get('BindOptions', {}) + volume_options = mount_data.get('VolumeOptions', {}) + tmpfs_options = mount_data.get('TmpfsOptions', {}) + driver_config = volume_options.get('DriverConfig', {}) + driver_config = dict( + (key.lower(), value) for key, value in driver_config.items() + ) or None ds.mounts.append({ 'source': mount_data['Source'], 'type': mount_data['Type'], 'target': mount_data['Target'], - 'readonly': mount_data.get('ReadOnly', False) + 'readonly': mount_data.get('ReadOnly'), + 'propagation': bind_options.get('Propagation'), + 'no_copy': volume_options.get('NoCopy'), + 'labels': volume_options.get('Labels'), + 'driver_config': driver_config, + 'tmpfs_mode': tmpfs_options.get('Mode'), + 'tmpfs_size': tmpfs_options.get('SizeBytes'), }) raw_data_configs = task_template_data['ContainerSpec'].get('Configs') @@ -2173,6 +2270,17 @@ def _detect_healthcheck_start_period(client): return False +def _detect_mount_tmpfs_usage(client): + for mount in client.module.params['mounts'] or []: + if mount.get('type') == 'tmpfs': + return True + if mount.get('tmpfs_size') is not None: + return True + if mount.get('tmpfs_mode') is not None: + return True + return False + + def main(): argument_spec = dict( name=dict(type='str', required=True), @@ -2184,9 +2292,28 @@ def main(): type=dict( type='str', default='bind', - choices=['bind', 'volume', 'tmpfs'] + choices=['bind', 'volume', 'tmpfs'], ), - readonly=dict(type='bool', default=False), + readonly=dict(type='bool'), + labels=dict(type='dict'), + propagation=dict( + type='str', + choices=[ + 'shared', + 'slave', + 'private', + 'rshared', + 'rslave', + 'rprivate' + ] + ), + no_copy=dict(type='bool'), + driver_config=dict(type='dict', options=dict( + name=dict(type='str'), + options=dict(type='dict') + )), + tmpfs_size=dict(type='str'), + tmpfs_mode=dict(type='int') )), configs=dict(type='list', elements='dict', options=dict( config_id=dict(type='str', required=True), @@ -2378,6 +2505,11 @@ def main(): ) is not None, usage_msg='set placement.constraints' ), + mounts_tmpfs=dict( + docker_py_version='2.6.0', + detect_usage=_detect_mount_tmpfs_usage, + usage_msg='set mounts.tmpfs' + ), ) required_if = [ diff --git a/test/integration/targets/docker_swarm_service/tasks/tests/mounts.yml b/test/integration/targets/docker_swarm_service/tasks/tests/mounts.yml new file mode 100644 index 00000000000..631f55d96d0 --- /dev/null +++ b/test/integration/targets/docker_swarm_service/tasks/tests/mounts.yml @@ -0,0 +1,525 @@ +- name: Registering service name + set_fact: + service_name: "{{ name_prefix ~ '-mounts' }}" + volume_name_1: "{{ name_prefix ~ '-volume-1' }}" + volume_name_2: "{{ name_prefix ~ '-volume-2' }}" + +- name: Registering service name + set_fact: + service_names: "{{ service_names }} + [service_name]" + volume_names: "{{ volume_names }} + [volume_name_1, volume_name_2]" + +- docker_volume: + name: "{{ volume_name }}" + state: present + loop: + - "{{ volume_name_1 }}" + - "{{ volume_name_2 }}" + loop_control: + loop_var: volume_name + +#################################################################### +## mounts ########################################################## +#################################################################### + +- name: mounts + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "{{ volume_name_1 }}" + target: "/tmp/{{ volume_name_1 }}" + type: "volume" + register: mounts_1 + +- name: mounts (idempotency) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "{{ volume_name_1 }}" + target: "/tmp/{{ volume_name_1 }}" + type: "volume" + register: mounts_2 + +- name: mounts (add) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "{{ volume_name_1 }}" + target: "/tmp/{{ volume_name_1 }}" + type: "volume" + - source: "/tmp/" + target: "/tmp/{{ volume_name_2 }}" + type: "bind" + register: mounts_3 + +- name: mounts (empty) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: [] + register: mounts_4 + +- name: mounts (empty idempotency) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: [] + register: mounts_5 + +- name: cleanup + docker_swarm_service: + name: "{{ service_name }}" + state: absent + diff: no + +- assert: + that: + - mounts_1 is changed + - mounts_2 is not changed + - mounts_3 is changed + - mounts_4 is changed + - mounts_5 is not changed + +#################################################################### +## mounts.readonly ################################################# +#################################################################### + +- name: mounts.readonly + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "{{ volume_name_1 }}" + target: "/tmp/{{ volume_name_1 }}" + readonly: true + register: mounts_readonly_1 + + +- name: mounts.readonly (idempotency) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "{{ volume_name_1 }}" + target: "/tmp/{{ volume_name_1 }}" + readonly: true + register: mounts_readonly_2 + +- name: mounts.readonly (change) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "{{ volume_name_1 }}" + target: "/tmp/{{ volume_name_1 }}" + readonly: false + register: mounts_readonly_3 + +- name: cleanup + docker_swarm_service: + name: "{{ service_name }}" + state: absent + diff: no + +- assert: + that: + - mounts_readonly_1 is changed + - mounts_readonly_2 is not changed + - mounts_readonly_3 is changed + +#################################################################### +## mounts.propagation ############################################## +#################################################################### + +- name: mounts.propagation + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "/tmp" + target: "/tmp/{{ volume_name_1 }}" + type: "bind" + propagation: "slave" + register: mounts_propagation_1 + + +- name: mounts.propagation (idempotency) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "/tmp" + target: "/tmp/{{ volume_name_1 }}" + type: "bind" + propagation: "slave" + register: mounts_propagation_2 + +- name: mounts.propagation (change) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "/tmp" + target: "/tmp/{{ volume_name_1 }}" + type: "bind" + propagation: "rprivate" + register: mounts_propagation_3 + +- name: cleanup + docker_swarm_service: + name: "{{ service_name }}" + state: absent + diff: no + +- assert: + that: + - mounts_propagation_1 is changed + - mounts_propagation_2 is not changed + - mounts_propagation_3 is changed + +#################################################################### +## mounts.labels ################################################## +#################################################################### + +- name: mounts.labels + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "{{ volume_name_1 }}" + target: "/tmp/{{ volume_name_1 }}" + type: "volume" + labels: + mylabel: hello-world + my-other-label: hello-mars + register: mounts_labels_1 + + +- name: mounts.labels (idempotency) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "{{ volume_name_1 }}" + target: "/tmp/{{ volume_name_1 }}" + type: "volume" + labels: + mylabel: hello-world + my-other-label: hello-mars + register: mounts_labels_2 + +- name: mounts.labels (change) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "{{ volume_name_1 }}" + target: "/tmp/{{ volume_name_1 }}" + type: "volume" + labels: + mylabel: hello-world + register: mounts_labels_3 + +- name: cleanup + docker_swarm_service: + name: "{{ service_name }}" + state: absent + diff: no + +- assert: + that: + - mounts_labels_1 is changed + - mounts_labels_2 is not changed + - mounts_labels_3 is changed + +#################################################################### +## mounts.no_copy ################################################## +#################################################################### + +- name: mounts.no_copy + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "{{ volume_name_1 }}" + target: "/tmp/{{ volume_name_1 }}" + type: "volume" + no_copy: true + register: mounts_no_copy_1 + + +- name: mounts.no_copy (idempotency) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "{{ volume_name_1 }}" + target: "/tmp/{{ volume_name_1 }}" + type: "volume" + no_copy: true + register: mounts_no_copy_2 + +- name: mounts.no_copy (change) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "{{ volume_name_1 }}" + target: "/tmp/{{ volume_name_1 }}" + type: "volume" + no_copy: false + register: mounts_no_copy_3 + +- name: cleanup + docker_swarm_service: + name: "{{ service_name }}" + state: absent + diff: no + +- assert: + that: + - mounts_no_copy_1 is changed + - mounts_no_copy_2 is not changed + - mounts_no_copy_3 is changed + +#################################################################### +## mounts.driver_config ############################################ +#################################################################### + +- name: mounts.driver_config + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "{{ volume_name_1 }}" + target: "/tmp/{{ volume_name_1 }}" + type: "volume" + driver_config: + name: "nfs" + options: + addr: "127.0.0.1" + register: mounts_driver_config_1 + +- name: mounts.driver_config + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "{{ volume_name_1 }}" + target: "/tmp/{{ volume_name_1 }}" + type: "volume" + driver_config: + name: "nfs" + options: + addr: "127.0.0.1" + register: mounts_driver_config_2 + +- name: mounts.driver_config + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "{{ volume_name_1 }}" + target: "/tmp/{{ volume_name_1 }}" + type: "volume" + driver_config: + name: "local" + register: mounts_driver_config_3 + +- name: cleanup + docker_swarm_service: + name: "{{ service_name }}" + state: absent + diff: no + +- assert: + that: + - mounts_driver_config_1 is changed + - mounts_driver_config_2 is not changed + - mounts_driver_config_3 is changed + +#################################################################### +## mounts.tmpfs_size ############################################### +#################################################################### + +- name: mounts.tmpfs_size + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "{{ volume_name_1 }}" + target: "/tmp/{{ volume_name_1 }}" + type: "tmpfs" + tmpfs_size: "50M" + register: mounts_tmpfs_size_1 + ignore_errors: yes + +- name: mounts.tmpfs_size (idempotency) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "{{ volume_name_1 }}" + target: "/tmp/{{ volume_name_1 }}" + type: "tmpfs" + tmpfs_size: "50M" + register: mounts_tmpfs_size_2 + ignore_errors: yes + +- name: mounts.tmpfs_size (change) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "{{ volume_name_1 }}" + target: "/tmp/{{ volume_name_1 }}" + type: "tmpfs" + tmpfs_size: "25M" + register: mounts_tmpfs_size_3 + ignore_errors: yes + +- name: cleanup + docker_swarm_service: + name: "{{ service_name }}" + state: absent + diff: no + +- assert: + that: + - mounts_tmpfs_size_1 is changed + - mounts_tmpfs_size_2 is not changed + - mounts_tmpfs_size_3 is changed + when: docker_py_version is version('2.6.0', '>=') +- assert: + that: + - mounts_tmpfs_size_1 is failed + - "'Minimum version required' in mounts_tmpfs_size_1.msg" + when: docker_py_version is version('2.6.0', '<') + +#################################################################### +## mounts.tmpfs_mode ############################################### +#################################################################### + +- name: mounts.tmpfs_mode + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "{{ volume_name_1 }}" + target: "/tmp/{{ volume_name_1 }}" + type: "tmpfs" + tmpfs_mode: 0444 + register: mounts_tmpfs_mode_1 + ignore_errors: yes + +- name: mounts.tmpfs_mode (idempotency) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "{{ volume_name_1 }}" + target: "/tmp/{{ volume_name_1 }}" + type: "tmpfs" + tmpfs_mode: 0444 + register: mounts_tmpfs_mode_2 + ignore_errors: yes + +- name: mounts.tmpfs_mode (change) + docker_swarm_service: + name: "{{ service_name }}" + image: alpine:3.8 + resolve_image: no + command: '/bin/sh -v -c "sleep 10m"' + mounts: + - source: "{{ volume_name_1 }}" + target: "/tmp/{{ volume_name_1 }}" + type: "tmpfs" + tmpfs_mode: 0777 + register: mounts_tmpfs_mode_3 + ignore_errors: yes + +- name: cleanup + docker_swarm_service: + name: "{{ service_name }}" + state: absent + diff: no + +- assert: + that: + - mounts_tmpfs_mode_1 is changed + - mounts_tmpfs_mode_2 is not changed + - mounts_tmpfs_mode_3 is changed + when: docker_py_version is version('2.6.0', '>=') +- assert: + that: + - mounts_tmpfs_size_1 is failed + - "'Minimum version required' in mounts_tmpfs_size_1.msg" + when: docker_py_version is version('2.6.0', '<') + +#################################################################### +#################################################################### +#################################################################### + +- 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/integration/targets/docker_swarm_service/tasks/tests/options.yml b/test/integration/targets/docker_swarm_service/tasks/tests/options.yml index 97bd1244040..9538d2c25a8 100644 --- a/test/integration/targets/docker_swarm_service/tasks/tests/options.yml +++ b/test/integration/targets/docker_swarm_service/tasks/tests/options.yml @@ -7,8 +7,6 @@ network_name_2: "{{ name_prefix ~ '-network-2' }}" config_name_1: "{{ name_prefix ~ '-configs-1' }}" config_name_2: "{{ name_prefix ~ '-configs-2' }}" - volume_name_1: "{{ name_prefix ~ '-volume-1' }}" - volume_name_2: "{{ name_prefix ~ '-volume-2' }}" secret_name_1: "{{ name_prefix ~ '-secret-1' }}" secret_name_2: "{{ name_prefix ~ '-secret-2' }}" @@ -17,7 +15,6 @@ service_names: "{{ service_names }} + [service_name]" network_names: "{{ network_names }} + [network_name_1, network_name_2]" config_names: "{{ config_names }} + [config_name_1, config_name_2]" - volume_names: "{{ volume_names }} + [volume_name_1, volume_name_2]" secret_names: "{{ secret_names }} + [secret_name_1, secret_name_2]" - docker_config: @@ -58,15 +55,6 @@ loop_control: loop_var: network_name -- docker_volume: - name: "{{ volume_name }}" - state: present - loop: - - "{{ volume_name_1 }}" - - "{{ volume_name_2 }}" - loop_control: - loop_var: volume_name - #################################################################### ## args ############################################################ #################################################################### @@ -1408,81 +1396,6 @@ - mode_2 is not changed - mode_3 is changed -#################################################################### -## mounts ########################################################## -#################################################################### - -- name: mounts - docker_swarm_service: - name: "{{ service_name }}" - image: alpine:3.8 - resolve_image: no - command: '/bin/sh -v -c "sleep 10m"' - mounts: - - source: "{{ volume_name_1 }}" - target: "/tmp/{{ volume_name_1 }}" - type: "volume" - register: mounts_1 - -- name: mounts (idempotency) - docker_swarm_service: - name: "{{ service_name }}" - image: alpine:3.8 - resolve_image: no - command: '/bin/sh -v -c "sleep 10m"' - mounts: - - source: "{{ volume_name_1 }}" - target: "/tmp/{{ volume_name_1 }}" - type: "volume" - register: mounts_2 - -- name: mounts (add) - docker_swarm_service: - name: "{{ service_name }}" - image: alpine:3.8 - resolve_image: no - command: '/bin/sh -v -c "sleep 10m"' - mounts: - - source: "{{ volume_name_1 }}" - target: "/tmp/{{ volume_name_1 }}" - type: "volume" - - source: "{{ volume_name_2 }}" - target: "/tmp/{{ volume_name_2 }}" - type: "volume" - register: mounts_3 - -- name: mounts (empty) - docker_swarm_service: - name: "{{ service_name }}" - image: alpine:3.8 - resolve_image: no - command: '/bin/sh -v -c "sleep 10m"' - mounts: [] - register: mounts_4 - -- name: mounts (empty idempotency) - docker_swarm_service: - name: "{{ service_name }}" - image: alpine:3.8 - resolve_image: no - command: '/bin/sh -v -c "sleep 10m"' - mounts: [] - register: mounts_5 - -- name: cleanup - docker_swarm_service: - name: "{{ service_name }}" - state: absent - diff: no - -- assert: - that: - - mounts_1 is changed - - mounts_2 is not changed - - mounts_3 is changed - - mounts_4 is changed - - mounts_5 is not changed - #################################################################### ## networks ######################################################## ####################################################################