diff --git a/changelogs/fragments/1134-docker_container-mounts.yml b/changelogs/fragments/1134-docker_container-mounts.yml new file mode 100644 index 00000000..ce5b4644 --- /dev/null +++ b/changelogs/fragments/1134-docker_container-mounts.yml @@ -0,0 +1,2 @@ +minor_changes: + - "docker_container - support missing fields and new mount types in ``mounts`` (https://github.com/ansible-collections/community.docker/issues/1129, https://github.com/ansible-collections/community.docker/pull/1134)." diff --git a/plugins/module_utils/module_container/base.py b/plugins/module_utils/module_container/base.py index a09450cd..8b05e4b0 100644 --- a/plugins/module_utils/module_container/base.py +++ b/plugins/module_utils/module_container/base.py @@ -38,13 +38,19 @@ _DEFAULT_IP_REPLACEMENT_STRING = '[[DEFAULT_IP:iewahhaeB4Sae6Aen8IeShairoh4zeph7 _MOUNT_OPTION_TYPES = dict( - volume_driver='volume', - volume_options='volume', - propagation='bind', - no_copy='volume', - labels='volume', - tmpfs_size='tmpfs', - tmpfs_mode='tmpfs', + create_mountpoint=('bind',), + labels=('volume',), + no_copy=('volume',), + non_recursive=('bind',), + propagation=('bind',), + read_only_force_recursive=('bind',), + read_only_non_recursive=('bind',), + subpath=('volume', 'image'), + tmpfs_size=('tmpfs',), + tmpfs_mode=('tmpfs',), + tmpfs_options=('tmpfs',), + volume_driver=('volume',), + volume_options=('volume',), ) @@ -583,12 +589,18 @@ def _preprocess_mounts(module, values): mount_dict = dict(mount) # Sanity checks - if mount['source'] is None and mount_type not in ('tmpfs', 'volume'): + if mount['source'] is None and mount_type not in ('tmpfs', 'volume', 'image', 'cluster'): module.fail_json(msg='source must be specified for mount "{0}" of type "{1}"'.format(target, mount_type)) - for option, req_mount_type in _MOUNT_OPTION_TYPES.items(): - if mount[option] is not None and mount_type != req_mount_type: + for option, req_mount_types in _MOUNT_OPTION_TYPES.items(): + if mount[option] is not None and mount_type not in req_mount_types: module.fail_json( - msg='{0} cannot be specified for mount "{1}" of type "{2}" (needs type "{3}")'.format(option, target, mount_type, req_mount_type) + msg='{0} cannot be specified for mount "{1}" of type "{2}" (needs type{3} "{4}")'.format( + option, + target, + mount_type, + "" if len(req_mount_types) == 1 else "s", + '", "'.join(req_mount_types), + ) ) # Streamline options @@ -607,6 +619,18 @@ def _preprocess_mounts(module, values): mount_dict['tmpfs_mode'] = int(mount_dict['tmpfs_mode'], 8) except Exception as dummy: module.fail_json(msg='tmp_fs mode of mount "{0}" is not an octal string!'.format(target)) + if mount_dict['tmpfs_options']: + opts = [] + for idx, opt in enumerate(mount_dict['tmpfs_options']): + if len(opt) != 1: + module.fail_json(msg='tmpfs_options[{1}] of mount "{0}" must be a one-element dictionary!'.format(target, idx + 1)) + k, v = list(opt.items())[0] + if not isinstance(k, str): + module.fail_json(msg='key {2!r} in tmpfs_options[{1}] of mount "{0}" must be a string!'.format(target, idx + 1, k)) + if v is not None and not isinstance(v, str): + module.fail_json(msg='value {2!r} in tmpfs_options[{1}] of mount "{0}" must be a string or null/none!'.format(target, idx + 1, v)) + opts.append([k, v] if v is not None else [k]) + mount_dict['tmpfs_options'] = opts # Add result to list mounts.append(omit_none_from_dict(mount_dict)) @@ -1169,7 +1193,7 @@ OPTION_MOUNTS_VOLUMES = ( .add_option('mounts', type='set', elements='dict', ansible_suboptions=dict( target=dict(type='str', required=True), source=dict(type='str'), - type=dict(type='str', choices=['bind', 'volume', 'tmpfs', 'npipe'], default='volume'), + type=dict(type='str', choices=['bind', 'volume', 'tmpfs', 'npipe', 'cluster', 'image'], default='volume'), read_only=dict(type='bool'), consistency=dict(type='str', choices=['default', 'consistent', 'cached', 'delegated']), propagation=dict(type='str', choices=['private', 'rprivate', 'shared', 'rshared', 'slave', 'rslave']), @@ -1179,6 +1203,12 @@ OPTION_MOUNTS_VOLUMES = ( volume_options=dict(type='dict'), tmpfs_size=dict(type='str'), tmpfs_mode=dict(type='str'), + non_recursive=dict(type='bool'), + create_mountpoint=dict(type='bool'), + read_only_non_recursive=dict(type='bool'), + read_only_force_recursive=dict(type='bool'), + subpath=dict(type='str'), + tmpfs_options=dict(type='list', elements='dict'), )) .add_option('volumes', type='set', elements='str') .add_option('volume_binds', type='set', elements='str', not_an_ansible_option=True, copy_comparison_from='volumes') diff --git a/plugins/module_utils/module_container/docker_api.py b/plugins/module_utils/module_container/docker_api.py index 4ebfa572..d44368e0 100644 --- a/plugins/module_utils/module_container/docker_api.py +++ b/plugins/module_utils/module_container/docker_api.py @@ -120,17 +120,6 @@ from ansible_collections.community.docker.plugins.module_utils._api.utils.utils _DEFAULT_IP_REPLACEMENT_STRING = '[[DEFAULT_IP:iewahhaeB4Sae6Aen8IeShairoh4zeph7xaekoh8Geingunaesaeweiy3ooleiwi]]' -_MOUNT_OPTION_TYPES = dict( - volume_driver='volume', - volume_options='volume', - propagation='bind', - no_copy='volume', - labels='volume', - tmpfs_size='tmpfs', - tmpfs_mode='tmpfs', -) - - def _get_ansible_type(type): if type == 'set': return 'list' @@ -934,6 +923,12 @@ def _get_values_mounts(module, container, api_version, options, image, host_info 'volume_options': mount.get('VolumeOptions', empty_dict).get('DriverConfig', empty_dict).get('Options', empty_dict), 'tmpfs_size': mount.get('TmpfsOptions', empty_dict).get('SizeBytes'), 'tmpfs_mode': mount.get('TmpfsOptions', empty_dict).get('Mode'), + 'non_recursive': mount.get('BindOptions', empty_dict).get('NonRecursive'), + 'create_mountpoint': mount.get('BindOptions', empty_dict).get('CreateMountpoint'), + 'read_only_non_recursive': mount.get('BindOptions', empty_dict).get('ReadOnlyNonRecursive'), + 'read_only_force_recursive': mount.get('BindOptions', empty_dict).get('ReadOnlyForceRecursive'), + 'subpath': mount.get('VolumeOptions', empty_dict).get('Subpath') or mount.get('ImageOptions', empty_dict).get('Subpath'), + 'tmpfs_options': mount.get('TmpfsOptions', empty_dict).get('Options'), }) mounts = result result = {} @@ -1026,10 +1021,19 @@ def _set_values_mounts(module, data, api_version, options, values): if 'consistency' in mount: mount_res['Consistency'] = mount['consistency'] if mount_type == 'bind': + bind_opts = {} if 'propagation' in mount: - mount_res['BindOptions'] = { - 'Propagation': mount['propagation'], - } + bind_opts['Propagation'] = mount['propagation'] + if 'non_recursive' in mount: + bind_opts['NonRecursive'] = mount['non_recursive'] + if 'create_mountpoint' in mount: + bind_opts['CreateMountpoint'] = mount['create_mountpoint'] + if 'read_only_non_recursive' in mount: + bind_opts['ReadOnlyNonRecursive'] = mount['read_only_non_recursive'] + if 'read_only_force_recursive' in mount: + bind_opts['ReadOnlyForceRecursive'] = mount['read_only_force_recursive'] + if bind_opts: + mount_res['BindOptions'] = bind_opts if mount_type == 'volume': volume_opts = {} if mount.get('no_copy'): @@ -1043,6 +1047,8 @@ def _set_values_mounts(module, data, api_version, options, values): if mount.get('volume_options'): driver_config['Options'] = mount.get('volume_options') volume_opts['DriverConfig'] = driver_config + if 'subpath' in mount: + volume_opts['Subpath'] = mount['subpath'] if volume_opts: mount_res['VolumeOptions'] = volume_opts if mount_type == 'tmpfs': @@ -1051,8 +1057,16 @@ def _set_values_mounts(module, data, api_version, options, values): tmpfs_opts['Mode'] = mount.get('tmpfs_mode') if mount.get('tmpfs_size'): tmpfs_opts['SizeBytes'] = mount.get('tmpfs_size') + if 'tmpfs_options' in mount: + tmpfs_opts['Options'] = mount['tmpfs_options'] if tmpfs_opts: mount_res['TmpfsOptions'] = tmpfs_opts + if mount_type == 'image': + image_opts = {} + if 'subpath' in mount: + image_opts['Subpath'] = mount['subpath'] + if image_opts: + mount_res['ImageOptions'] = image_opts mounts.append(mount_res) data['HostConfig']['Mounts'] = mounts if 'volumes' in values: @@ -1486,6 +1500,40 @@ OPTION_MOUNTS_VOLUMES.add_engine('docker_api', DockerAPIEngine( get_value=_get_values_mounts, get_expected_values=_get_expected_values_mounts, set_value=_set_values_mounts, + extra_option_minimal_versions={ + 'mounts.non_recursive': { + 'docker_api_version': '1.40', + 'detect_usage': lambda c: any(mount.get('non_recursive') is not None for mount in (c.module.params['mounts'] or [])), + }, + 'mounts.create_mountpoint': { + 'docker_api_version': '1.42', + 'detect_usage': lambda c: any(mount.get('create_mountpoint') is not None for mount in (c.module.params['mounts'] or [])), + }, + 'mounts.type=cluster': { + 'docker_api_version': '1.42', + 'detect_usage': lambda c: any(mount.get('type') == 'cluster' for mount in (c.module.params['mounts'] or [])), + }, + 'mounts.read_only_non_recursive': { + 'docker_api_version': '1.44', + 'detect_usage': lambda c: any(mount.get('read_only_non_recursive') is not None for mount in (c.module.params['mounts'] or [])), + }, + 'mounts.read_only_force_recursive': { + 'docker_api_version': '1.44', + 'detect_usage': lambda c: any(mount.get('read_only_force_recursive') is not None for mount in (c.module.params['mounts'] or [])), + }, + 'mounts.subpath': { + 'docker_api_version': '1.45', + 'detect_usage': lambda c: any(mount.get('subpath') is not None for mount in (c.module.params['mounts'] or [])), + }, + 'mounts.tmpfs_options': { + 'docker_api_version': '1.46', + 'detect_usage': lambda c: any(mount.get('tmpfs_options') is not None for mount in (c.module.params['mounts'] or [])), + }, + 'mounts.type=image': { + 'docker_api_version': '1.47', + 'detect_usage': lambda c: any(mount.get('type') == 'image' for mount in (c.module.params['mounts'] or [])), + }, + }, )) OPTION_PORTS.add_engine('docker_api', DockerAPIEngine( diff --git a/plugins/modules/docker_container.py b/plugins/modules/docker_container.py index 91c78699..86b6dbdd 100644 --- a/plugins/modules/docker_container.py +++ b/plugins/modules/docker_container.py @@ -580,12 +580,16 @@ options: description: - The mount type. - Note that V(npipe) is only supported by Docker for Windows. + - V(cluster) requires Docker API 1.42+ and has been added in community.docker 4.8.0. + - V(image) requires Docker API 1.47+ and has been added in community.docker 4.8.0. type: str choices: - bind - npipe - tmpfs - volume + - cluster + - image default: volume read_only: description: @@ -600,6 +604,13 @@ options: - consistent - default - delegated + create_mountpoint: + description: + - Create mount point on host if missing. + - Requires Docker API 1.42+. + - Only valid for O(mounts[].type=bind). + type: bool + version_added: 4.8.0 propagation: description: - Propagation mode. Only valid for the V(bind) type. @@ -616,6 +627,27 @@ options: - False if the volume should be populated with the data from the target. Only valid for the V(volume) type. - The default value is V(false). type: bool + non_recursive: + description: + - Disable recursive bind mount. + - Requires Docker API 1.40+. + - Only valid for O(mounts[].type=bind). + type: bool + version_added: 4.8.0 + read_only_non_recursive: + description: + - Make the mount non-recursively read-only, but still leave the mount recursive (unless NonRecursive is set to true in conjunction). + - Requires Docker API 1.44+. + - Only valid for O(mounts[].type=bind). + type: bool + version_added: 4.8.0 + read_only_force_recursive: + description: + - Raise an error if the mount cannot be made recursively read-only. + - Requires Docker API 1.44+. + - Only valid for O(mounts[].type=bind). + type: bool + version_added: 4.8.0 labels: description: - User-defined name and labels for the volume. Only valid for the V(volume) type. @@ -630,6 +662,13 @@ options: - Dictionary of options specific to the chosen volume_driver. See L(here,https://docs.docker.com/storage/volumes/#use-a-volume-driver) for details. type: dict + subpath: + type: str + description: + - Source path inside the volume/image. Must be relative without any back traversals. + - Requires Docker API 1.45+. + - Only valid for O(mounts[].type=volume) or O(mounts[].type=image). + version_added: 4.8.0 tmpfs_size: description: - The size for the tmpfs mount in bytes in format []. @@ -641,6 +680,16 @@ options: description: - The permission mode for the tmpfs mount. type: str + tmpfs_options: + type: list + elements: dict + description: + - Options to be passed to the tmpfs mount. + - Every list element must be a dictionary with one key and a value. + All keys must be strings, and values can be either a string or V(null)/V(none) for a flag. + - Requires Docker API 1.46+. + - Only valid for O(mounts[].type=tmpfs). + version_added: 4.8.0 name: description: - Assign a name to a new container or match an existing container.