diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-05 16:18:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-05 16:18:41 +0000 |
commit | b643c52cf29ce5bbab738b43290af3556efa1ca9 (patch) | |
tree | 21d5c53d7a9b696627a255777cefdf6f78968824 /ansible_collections/community/docker/plugins | |
parent | Releasing progress-linux version 9.5.1+dfsg-1~progress7.99u1. (diff) | |
download | ansible-b643c52cf29ce5bbab738b43290af3556efa1ca9.tar.xz ansible-b643c52cf29ce5bbab738b43290af3556efa1ca9.zip |
Merging upstream version 10.0.0+dfsg.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/community/docker/plugins')
19 files changed, 455 insertions, 20 deletions
diff --git a/ansible_collections/community/docker/plugins/connection/docker.py b/ansible_collections/community/docker/plugins/connection/docker.py index 68247dae2..133fe6a75 100644 --- a/ansible_collections/community/docker/plugins/connection/docker.py +++ b/ansible_collections/community/docker/plugins/connection/docker.py @@ -83,7 +83,6 @@ import os.path import subprocess import re -from ansible.compat import selectors from ansible.errors import AnsibleError, AnsibleFileNotFound from ansible.module_utils.six.moves import shlex_quote from ansible.module_utils.common.process import get_bin_path @@ -91,6 +90,7 @@ from ansible.module_utils.common.text.converters import to_bytes, to_native, to_ from ansible.plugins.connection import ConnectionBase, BUFSIZE from ansible.utils.display import Display +from ansible_collections.community.docker.plugins.module_utils.selectors import selectors from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion display = Display() diff --git a/ansible_collections/community/docker/plugins/connection/docker_api.py b/ansible_collections/community/docker/plugins/connection/docker_api.py index 3b99281c3..a6dec85aa 100644 --- a/ansible_collections/community/docker/plugins/connection/docker_api.py +++ b/ansible_collections/community/docker/plugins/connection/docker_api.py @@ -21,6 +21,7 @@ notes: with Python's C(SSLSocket)s. See U(https://github.com/ansible-collections/community.docker/issues/605) for more information. extends_documentation_fragment: - community.docker.docker.api_documentation + - community.docker.docker.ssl_version_deprecation - community.docker.docker.var_names options: remote_user: diff --git a/ansible_collections/community/docker/plugins/connection/nsenter.py b/ansible_collections/community/docker/plugins/connection/nsenter.py index f429f8cef..ccc660b99 100644 --- a/ansible_collections/community/docker/plugins/connection/nsenter.py +++ b/ansible_collections/community/docker/plugins/connection/nsenter.py @@ -50,13 +50,15 @@ import fcntl import ansible.constants as C from ansible.errors import AnsibleError -from ansible.module_utils.compat import selectors from ansible.module_utils.six import binary_type, text_type from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text from ansible.plugins.connection import ConnectionBase from ansible.utils.display import Display from ansible.utils.path import unfrackpath +from ansible_collections.community.docker.plugins.module_utils.selectors import selectors + + display = Display() diff --git a/ansible_collections/community/docker/plugins/doc_fragments/docker.py b/ansible_collections/community/docker/plugins/doc_fragments/docker.py index 92989a97b..2c78c5fae 100644 --- a/ansible_collections/community/docker/plugins/doc_fragments/docker.py +++ b/ansible_collections/community/docker/plugins/doc_fragments/docker.py @@ -392,3 +392,12 @@ notes: - This module does B(not) use the L(Docker SDK for Python,https://docker-py.readthedocs.io/en/stable/) to communicate with the Docker daemon. It directly calls the Docker CLI program. ''' + + # DEPRECATED: this will be removed from community.docker 4.0.0! Use with care! + SSL_VERSION_DEPRECATION = ''' +options: + ssl_version: + deprecated: + why: This was necessary a long time ago to handle problems with older TLS/SSL versions. It is no longer necessary nowadays. + version: 4.0.0 +''' diff --git a/ansible_collections/community/docker/plugins/inventory/docker_containers.py b/ansible_collections/community/docker/plugins/inventory/docker_containers.py index 0cae05472..f353b03bd 100644 --- a/ansible_collections/community/docker/plugins/inventory/docker_containers.py +++ b/ansible_collections/community/docker/plugins/inventory/docker_containers.py @@ -21,6 +21,7 @@ author: extends_documentation_fragment: - ansible.builtin.constructed - community.docker.docker.api_documentation + - community.docker.docker.ssl_version_deprecation - community.library_inventory_filtering_v1.inventory_filter description: - Reads inventories from the Docker API. diff --git a/ansible_collections/community/docker/plugins/inventory/docker_swarm.py b/ansible_collections/community/docker/plugins/inventory/docker_swarm.py index acceac86c..6d1556ff5 100644 --- a/ansible_collections/community/docker/plugins/inventory/docker_swarm.py +++ b/ansible_collections/community/docker/plugins/inventory/docker_swarm.py @@ -78,6 +78,9 @@ DOCUMENTATION = ''' - Provide a valid SSL version number. Default value determined by L(SSL Python module, https://docs.python.org/3/library/ssl.html). type: str + deprecated: + why: This was necessary a long time ago to handle problems with SSL versions. It is no longer necessary nowadays. + version: 4.0.0 api_version: description: - The version of the Docker API running on the Docker Host. diff --git a/ansible_collections/community/docker/plugins/module_utils/_api/transport/basehttpadapter.py b/ansible_collections/community/docker/plugins/module_utils/_api/transport/basehttpadapter.py index 2afa60aea..14062a0ba 100644 --- a/ansible_collections/community/docker/plugins/module_utils/_api/transport/basehttpadapter.py +++ b/ansible_collections/community/docker/plugins/module_utils/_api/transport/basehttpadapter.py @@ -18,3 +18,15 @@ class BaseHTTPAdapter(_HTTPAdapter): super(BaseHTTPAdapter, self).close() if hasattr(self, 'pools'): self.pools.clear() + + # Hotfix for requests 2.32.0 and 2.32.1: its commit + # https://github.com/psf/requests/commit/c0813a2d910ea6b4f8438b91d315b8d181302356 + # changes requests.adapters.HTTPAdapter to no longer call get_connection() from + # send(), but instead call _get_connection(). + def _get_connection(self, request, *args, **kwargs): + return self.get_connection(request.url, kwargs.get('proxies')) + + # Fix for requests 2.32.2+: + # https://github.com/psf/requests/commit/c98e4d133ef29c46a9b68cd783087218a8075e05 + def get_connection_with_tls_context(self, request, verify, proxies=None, cert=None): + return self.get_connection(request.url, proxies) diff --git a/ansible_collections/community/docker/plugins/module_utils/module_container/base.py b/ansible_collections/community/docker/plugins/module_utils/module_container/base.py index 0f776aa5c..410ffb153 100644 --- a/ansible_collections/community/docker/plugins/module_utils/module_container/base.py +++ b/ansible_collections/community/docker/plugins/module_utils/module_container/base.py @@ -935,9 +935,11 @@ OPTION_HEALTHCHECK = ( OptionGroup(preprocess=_preprocess_healthcheck) .add_option('healthcheck', type='dict', ansible_suboptions=dict( test=dict(type='raw'), + test_cli_compatible=dict(type='bool', default=False), interval=dict(type='str'), timeout=dict(type='str'), start_period=dict(type='str'), + start_interval=dict(type='str'), retries=dict(type='int'), )) ) diff --git a/ansible_collections/community/docker/plugins/module_utils/module_container/docker_api.py b/ansible_collections/community/docker/plugins/module_utils/module_container/docker_api.py index 61a5500c9..d3da84fe5 100644 --- a/ansible_collections/community/docker/plugins/module_utils/module_container/docker_api.py +++ b/ansible_collections/community/docker/plugins/module_utils/module_container/docker_api.py @@ -436,6 +436,7 @@ class DockerAPIEngine(Engine): min_api_version=None, preprocess_value=None, update_parameter=None, + extra_option_minimal_versions=None, ): def preprocess_value_(module, client, api_version, options, values): if len(options) != 1: @@ -499,6 +500,7 @@ class DockerAPIEngine(Engine): set_value=set_value, min_api_version=min_api_version, update_value=update_value, + extra_option_minimal_versions=extra_option_minimal_versions, ) @classmethod @@ -512,6 +514,7 @@ class DockerAPIEngine(Engine): min_api_version=None, preprocess_value=None, update_parameter=None, + extra_option_minimal_versions=None, ): def preprocess_value_(module, client, api_version, options, values): if len(options) != 1: @@ -577,6 +580,7 @@ class DockerAPIEngine(Engine): set_value=set_value, min_api_version=min_api_version, update_value=update_value, + extra_option_minimal_versions=extra_option_minimal_versions, ) @@ -742,7 +746,7 @@ def _preprocess_etc_hosts(module, client, api_version, value): def _preprocess_healthcheck(module, client, api_version, value): if value is None: return value - if not value or not value.get('test'): + if not value or not (value.get('test') or (value.get('test_cli_compatible') and value.get('test') is None)): value = {'test': ['NONE']} elif 'test' in value: value['test'] = normalize_healthcheck_test(value['test']) @@ -751,6 +755,7 @@ def _preprocess_healthcheck(module, client, api_version, value): 'Interval': value.get('interval'), 'Timeout': value.get('timeout'), 'StartPeriod': value.get('start_period'), + 'StartInterval': value.get('start_interval'), 'Retries': value.get('retries'), }) @@ -1300,7 +1305,16 @@ OPTION_ETC_HOSTS.add_engine('docker_api', DockerAPIEngine.host_config_value('Ext OPTION_GROUPS.add_engine('docker_api', DockerAPIEngine.host_config_value('GroupAdd')) OPTION_HEALTHCHECK.add_engine('docker_api', DockerAPIEngine.config_value( - 'Healthcheck', preprocess_value=_preprocess_healthcheck, postprocess_for_get=_postprocess_healthcheck_get_value)) + 'Healthcheck', + preprocess_value=_preprocess_healthcheck, + postprocess_for_get=_postprocess_healthcheck_get_value, + extra_option_minimal_versions={ + 'healthcheck.start_interval': { + 'docker_api_version': '1.44', + 'detect_usage': lambda c: c.module.params['healthcheck'] and c.module.params['healthcheck']['start_interval'] is not None, + }, + }, +)) OPTION_HOSTNAME.add_engine('docker_api', DockerAPIEngine.config_value('Hostname')) diff --git a/ansible_collections/community/docker/plugins/module_utils/selectors.py b/ansible_collections/community/docker/plugins/module_utils/selectors.py new file mode 100644 index 000000000..ca52cc877 --- /dev/null +++ b/ansible_collections/community/docker/plugins/module_utils/selectors.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Provide selectors import.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +# Once we drop support for ansible-core 2.16, we can remove the try/except. + +from sys import version_info as _python_version_info + + +if _python_version_info < (3, 4): + from ansible.module_utils.compat import selectors # noqa: F401, pylint: disable=unused-import +else: + import selectors # noqa: F401, pylint: disable=unused-import diff --git a/ansible_collections/community/docker/plugins/module_utils/util.py b/ansible_collections/community/docker/plugins/module_utils/util.py index efd3301f1..9235e34d7 100644 --- a/ansible_collections/community/docker/plugins/module_utils/util.py +++ b/ansible_collections/community/docker/plugins/module_utils/util.py @@ -29,7 +29,12 @@ DOCKER_COMMON_ARGS = dict( ca_path=dict(type='path', aliases=['ca_cert', 'tls_ca_cert', 'cacert_path']), client_cert=dict(type='path', aliases=['tls_client_cert', 'cert_path']), client_key=dict(type='path', aliases=['tls_client_key', 'key_path']), - ssl_version=dict(type='str', fallback=(env_fallback, ['DOCKER_SSL_VERSION'])), + ssl_version=dict( + type='str', + fallback=(env_fallback, ['DOCKER_SSL_VERSION']), + removed_in_version='4.0.0', + removed_from_collection='community.docker', + ), tls=dict(type='bool', default=DEFAULT_TLS, fallback=(env_fallback, ['DOCKER_TLS'])), use_ssh_client=dict(type='bool', default=False), validate_certs=dict(type='bool', default=DEFAULT_TLS_VERIFY, fallback=(env_fallback, ['DOCKER_TLS_VERIFY']), aliases=['tls_verify']), @@ -348,9 +353,9 @@ def normalize_healthcheck(healthcheck, normalize_test=False): result = dict() # All supported healthcheck parameters - options = ('test', 'interval', 'timeout', 'start_period', 'retries') + options = ('test', 'test_cli_compatible', 'interval', 'timeout', 'start_period', 'start_interval', 'retries') - duration_options = ('interval', 'timeout', 'start_period') + duration_options = ('interval', 'timeout', 'start_period', 'start_interval') for key in options: if key in healthcheck: @@ -361,7 +366,7 @@ def normalize_healthcheck(healthcheck, normalize_test=False): continue if key in duration_options: value = convert_duration_to_nanosecond(value) - if not value: + if not value and not (healthcheck.get('test_cli_compatible') and key == 'test'): continue if key == 'retries': try: @@ -371,7 +376,7 @@ def normalize_healthcheck(healthcheck, normalize_test=False): 'Cannot parse number of retries for healthcheck. ' 'Expected an integer, got "{0}".'.format(value) ) - if key == 'test' and normalize_test: + if key == 'test' and value and normalize_test: value = normalize_healthcheck_test(value) result[key] = value diff --git a/ansible_collections/community/docker/plugins/modules/docker_compose.py b/ansible_collections/community/docker/plugins/modules/docker_compose.py index f8edbee4b..3af3bebb1 100644 --- a/ansible_collections/community/docker/plugins/modules/docker_compose.py +++ b/ansible_collections/community/docker/plugins/modules/docker_compose.py @@ -14,6 +14,11 @@ module: docker_compose short_description: Manage multi-container Docker applications with Docker Compose V1 +deprecated: + removed_in: 4.0.0 + why: This module uses docker-compose v1, which is End of Life since July 2022. + alternative: Migrate to M(community.docker.docker_compose_v2) + author: "Chris Houseknecht (@chouseknecht)" description: diff --git a/ansible_collections/community/docker/plugins/modules/docker_container.py b/ansible_collections/community/docker/plugins/modules/docker_container.py index d7dbc3780..60768637f 100644 --- a/ansible_collections/community/docker/plugins/modules/docker_container.py +++ b/ansible_collections/community/docker/plugins/modules/docker_container.py @@ -369,7 +369,7 @@ options: - Configure a check that is run to determine whether or not containers for this service are "healthy". - "See the docs for the L(HEALTHCHECK Dockerfile instruction,https://docs.docker.com/engine/reference/builder/#healthcheck) for details on how healthchecks work." - - "O(healthcheck.interval), O(healthcheck.timeout) and O(healthcheck.start_period) are specified as durations. + - "O(healthcheck.interval), O(healthcheck.timeout), O(healthcheck.start_period), and O(healthcheck.start_interval) are specified as durations. They accept duration as a string in a format that look like: V(5h34m56s), V(1m30s), and so on. The supported units are V(us), V(ms), V(s), V(m) and V(h)." type: dict @@ -379,6 +379,16 @@ options: - Command to run to check health. - Must be either a string or a list. If it is a list, the first item must be one of V(NONE), V(CMD) or V(CMD-SHELL). type: raw + test_cli_compatible: + description: + - If set to V(true), omitting O(healthcheck.test) while providing O(healthcheck) does not disable healthchecks, + but simply overwrites the image's values by the ones specified in O(healthcheck). This is + the behavior used by the Docker CLI. + - If set to V(false), omitting O(healthcheck.test) will disable the container's health check. + This is the classical behavior of the module and currently the default behavior. + default: false + type: bool + version_added: 3.10.0 interval: description: - Time between running the check. @@ -399,6 +409,12 @@ options: - Start period for the container to initialize before starting health-retries countdown. - The default used by the Docker daemon is V(0s). type: str + start_interval: + description: + - Time between health checks during the start period. This option requires Docker Engine version 25.0 or later. + - The default used by the Docker daemon is V(5s). + type: str + version_added: 3.10.0 hostname: description: - The container's hostname. @@ -1196,6 +1212,7 @@ EXAMPLES = ''' timeout: 10s retries: 3 start_period: 30s + start_interval: 10s - name: Remove healthcheck from container community.docker.docker_container: diff --git a/ansible_collections/community/docker/plugins/modules/docker_container_exec.py b/ansible_collections/community/docker/plugins/modules/docker_container_exec.py index 0d92dad96..251af5bcd 100644 --- a/ansible_collections/community/docker/plugins/modules/docker_container_exec.py +++ b/ansible_collections/community/docker/plugins/modules/docker_container_exec.py @@ -162,7 +162,6 @@ import shlex import traceback from ansible.module_utils.common.text.converters import to_text, to_bytes, to_native -from ansible.module_utils.compat import selectors from ansible.module_utils.six import string_types from ansible_collections.community.docker.plugins.module_utils.common_api import ( @@ -170,6 +169,8 @@ from ansible_collections.community.docker.plugins.module_utils.common_api import RequestException, ) +from ansible_collections.community.docker.plugins.module_utils.selectors import selectors + from ansible_collections.community.docker.plugins.module_utils.socket_handler import ( DockerSocketHandlerModule, ) diff --git a/ansible_collections/community/docker/plugins/modules/docker_image_build.py b/ansible_collections/community/docker/plugins/modules/docker_image_build.py index 7f9502098..48478b550 100644 --- a/ansible_collections/community/docker/plugins/modules/docker_image_build.py +++ b/ansible_collections/community/docker/plugins/modules/docker_image_build.py @@ -18,6 +18,9 @@ version_added: 3.6.0 description: - This module allows you to build Docker images using Docker's buildx plugin (BuildKit). + - Note that the module is B(not idempotent) in the sense of classical Ansible modules. + The only idempotence check is whether the built image already exists. This check can + be disabled with the O(rebuild) option. extends_documentation_fragment: - community.docker.docker.cli_documentation @@ -89,8 +92,10 @@ options: type: str platform: description: - - Platform in the format C(os[/arch[/variant]]). - type: str + - Platforms in the format C(os[/arch[/variant]]). + - Since community.docker 3.10.0 this can be a list of platforms, instead of just a single platform. + type: list + elements: str shm_size: description: - "Size of C(/dev/shm) in format C(<number>[<unit>]). Number is positive integer. @@ -110,7 +115,121 @@ options: - never - always default: never - + secrets: + description: + - Secrets to expose to the build. + type: list + elements: dict + version_added: 3.10.0 + suboptions: + id: + description: + - The secret identifier. + - The secret will be made available as a file in the container under C(/run/secrets/<id>). + type: str + required: true + type: + description: + - Type of the secret. + type: str + choices: + file: + - Reads the secret from a file on the target. + - The file must be specified in O(secrets[].src). + env: + - Reads the secret from an environment variable on the target. + - The environment variable must be named in O(secrets[].env). + - Note that this requires the Buildkit plugin to have version 0.6.0 or newer. + value: + - Provides the secret from a given value O(secrets[].value). + - B(Note) that the secret will be passed as an environment variable to C(docker compose). + Use another mean of transport if you consider this not safe enough. + - Note that this requires the Buildkit plugin to have version 0.6.0 or newer. + required: true + src: + description: + - Source path of the secret. + - Only supported and required for O(secrets[].type=file). + type: path + env: + description: + - Environment value of the secret. + - Only supported and required for O(secrets[].type=env). + type: str + value: + description: + - Value of the secret. + - B(Note) that the secret will be passed as an environment variable to C(docker compose). + Use another mean of transport if you consider this not safe enough. + - Only supported and required for O(secrets[].type=value). + type: str + outputs: + description: + - Output destinations. + - You can provide a list of exporters to export the built image in various places. + Note that not all exporters might be supported by the build driver used. + - Note that depending on how this option is used, no image with name O(name) and tag O(tag) might + be created, which can cause the basic idempotency this module offers to not work. + - Providing an empty list to this option is equivalent to not specifying it at all. + The default behavior is a single entry with O(outputs[].type=image). + type: list + elements: dict + version_added: 3.10.0 + suboptions: + type: + description: + - The type of exporter to use. + type: str + choices: + local: + - This export type writes all result files to a directory on the client. + The new files will be owned by the current user. + On multi-platform builds, all results will be put in subdirectories by their platform. + - The destination has to be provided in O(outputs[].dest). + tar: + - This export type export type writes all result files as a single tarball on the client. + On multi-platform builds, all results will be put in subdirectories by their platform. + - The destination has to be provided in O(outputs[].dest). + oci: + - This export type writes the result image or manifest list as an + L(OCI image layout, https://github.com/opencontainers/image-spec/blob/v1.0.1/image-layout.md) + tarball on the client. + - The destination has to be provided in O(outputs[].dest). + docker: + - This export type writes the single-platform result image as a Docker image specification tarball on the client. + Tarballs created by this exporter are also OCI compatible. + - The destination can be provided in O(outputs[].dest). + If not specified, the tar will be loaded automatically to the local image store. + - The Docker context where to import the result can be provided in O(outputs[].context). + image: + - This exporter writes the build result as an image or a manifest list. + When using this driver, the image will appear in C(docker images). + - The image name can be provided in O(outputs[].name). If it is not provided, the + - Optionally, image can be automatically pushed to a registry by setting O(outputs[].push=true). + required: true + dest: + description: + - The destination path. + - Required for O(outputs[].type=local), O(outputs[].type=tar), O(outputs[].type=oci). + - Optional for O(outputs[].type=docker). + type: path + context: + description: + - Name for the Docker context where to import the result. + - Optional for O(outputs[].type=docker). + type: str + name: + description: + - Name under which the image is stored under. + - If not provided, O(name) and O(tag) will be used. + - Optional for O(outputs[].type=image). + type: str + push: + description: + - Whether to push the built image to a registry. + - Only used for O(outputs[].type=image). + type: bool + default: false requirements: - "Docker CLI with Docker buildx plugin" @@ -128,6 +247,15 @@ EXAMPLES = ''' name: localhost/python/3.12:latest path: /home/user/images/python dockerfile: Dockerfile-3.12 + +- name: Build multi-platform image + community.docker.docker_image_build: + name: multi-platform-image + tag: "1.5.2" + path: /home/user/images/multi-platform + platform: + - linux/amd64 + - linux/arm64/v8 ''' RETURN = ''' @@ -138,6 +266,7 @@ image: sample: {} ''' +import base64 import os import traceback @@ -156,6 +285,8 @@ from ansible_collections.community.docker.plugins.module_utils.util import ( is_valid_tag, ) +from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion + from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import ( parse_repository_tag, ) @@ -194,10 +325,26 @@ class ImageBuilder(DockerBaseClass): self.shm_size = convert_to_bytes(parameters['shm_size'], self.client.module, 'shm_size') self.labels = clean_dict_booleans_for_docker_api(parameters['labels']) self.rebuild = parameters['rebuild'] + self.secrets = parameters['secrets'] + self.outputs = parameters['outputs'] buildx = self.client.get_client_plugin_info('buildx') if buildx is None: self.fail('Docker CLI {0} does not have the buildx plugin installed'.format(self.client.get_cli())) + buildx_version = buildx['Version'].lstrip('v') + + if self.secrets: + for secret in self.secrets: + if secret['type'] in ('env', 'value'): + if LooseVersion(buildx_version) < LooseVersion('0.6.0'): + self.fail('The Docker buildx plugin has version {version}, but 0.6.0 is needed for secrets of type=env and type=value'.format( + version=buildx_version, + )) + if self.outputs and len(self.outputs) > 1: + if LooseVersion(buildx_version) < LooseVersion('0.13.0'): + self.fail('The Docker buildx plugin has version {version}, but 0.13.0 is needed to specify more than one output'.format( + version=buildx_version, + )) self.path = parameters['path'] if not os.path.isdir(self.path): @@ -230,6 +377,7 @@ class ImageBuilder(DockerBaseClass): args.extend([option, value]) def add_args(self, args): + environ_update = {} args.extend(['--tag', '%s:%s' % (self.name, self.tag)]) if self.dockerfile: args.extend(['--file', os.path.join(self.path, self.dockerfile)]) @@ -248,11 +396,54 @@ class ImageBuilder(DockerBaseClass): if self.target: args.extend(['--target', self.target]) if self.platform: - args.extend(['--platform', self.platform]) + for platform in self.platform: + args.extend(['--platform', platform]) if self.shm_size: args.extend(['--shm-size', str(self.shm_size)]) if self.labels: self.add_list_arg(args, '--label', dict_to_list(self.labels)) + if self.secrets: + random_prefix = None + for index, secret in enumerate(self.secrets): + if secret['type'] == 'file': + args.extend(['--secret', 'id={id},type=file,src={src}'.format(id=secret['id'], src=secret['src'])]) + if secret['type'] == 'env': + args.extend(['--secret', 'id={id},type=env,env={env}'.format(id=secret['id'], env=secret['src'])]) + if secret['type'] == 'value': + # We pass values on using environment variables. The user has been warned in the documentation + # that they should only use this mechanism when being comfortable with it. + if random_prefix is None: + # Use /dev/urandom to generate some entropy to make the environment variable's name unguessable + random_prefix = base64.b64encode(os.urandom(16)).decode('utf-8').replace('=', '') + env_name = 'ANSIBLE_DOCKER_COMPOSE_ENV_SECRET_{random}_{id}'.format( + random=random_prefix, + id=index, + ) + environ_update[env_name] = secret['value'] + args.extend(['--secret', 'id={id},type=env,env={env}'.format(id=secret['id'], env=env_name)]) + if self.outputs: + for output in self.outputs: + if output['type'] == 'local': + args.extend(['--output', 'type=local,dest={dest}'.format(dest=output['dest'])]) + if output['type'] == 'tar': + args.extend(['--output', 'type=tar,dest={dest}'.format(dest=output['dest'])]) + if output['type'] == 'oci': + args.extend(['--output', 'type=oci,dest={dest}'.format(dest=output['dest'])]) + if output['type'] == 'docker': + more = [] + if output['dest'] is not None: + more.append('dest={dest}'.format(dest=output['dest'])) + if output['dest'] is not None: + more.append('context={context}'.format(context=output['context'])) + args.extend(['--output', 'type=docker,{more}'.format(more=','.join(more))]) + if output['type'] == 'image': + more = [] + if output['name'] is not None: + more.append('name={name}'.format(name=output['name'])) + if output['push']: + more.append('push=true') + args.extend(['--output', 'type=image,{more}'.format(more=','.join(more))]) + return environ_update def build_image(self): image = self.client.find_image(self.name, self.tag) @@ -269,9 +460,9 @@ class ImageBuilder(DockerBaseClass): results['changed'] = True if not self.check_mode: args = ['buildx', 'build', '--progress', 'plain'] - self.add_args(args) + environ_update = self.add_args(args) args.extend(['--', self.path]) - rc, stdout, stderr = self.client.call_cli(*args) + rc, stdout, stderr = self.client.call_cli(*args, environ_update=environ_update) if rc != 0: self.fail('Building %s:%s failed' % (self.name, self.tag), stdout=to_native(stdout), stderr=to_native(stderr)) results['stdout'] = to_native(stdout) @@ -294,10 +485,52 @@ def main(): etc_hosts=dict(type='dict'), args=dict(type='dict'), target=dict(type='str'), - platform=dict(type='str'), + platform=dict(type='list', elements='str'), shm_size=dict(type='str'), labels=dict(type='dict'), rebuild=dict(type='str', choices=['never', 'always'], default='never'), + secrets=dict( + type='list', + elements='dict', + options=dict( + id=dict(type='str', required=True), + type=dict(type='str', choices=['file', 'env', 'value'], required=True), + src=dict(type='path'), + env=dict(type='str'), + value=dict(type='str', no_log=True), + ), + required_if=[ + ('type', 'file', ['src']), + ('type', 'env', ['env']), + ('type', 'value', ['value']), + ], + mutually_exclusive=[ + ('src', 'env', 'value'), + ], + no_log=False, + ), + outputs=dict( + type='list', + elements='dict', + options=dict( + type=dict(type='str', choices=['local', 'tar', 'oci', 'docker', 'image'], required=True), + dest=dict(type='path'), + context=dict(type='str'), + name=dict(type='str'), + push=dict(type='bool', default=False), + ), + required_if=[ + ('type', 'local', ['dest']), + ('type', 'tar', ['dest']), + ('type', 'oci', ['dest']), + ], + mutually_exclusive=[ + ('dest', 'name'), + ('dest', 'push'), + ('context', 'name'), + ('context', 'push'), + ], + ), ) client = AnsibleModuleDockerClient( diff --git a/ansible_collections/community/docker/plugins/modules/docker_network.py b/ansible_collections/community/docker/plugins/modules/docker_network.py index 5670ceea0..c5dd3b229 100644 --- a/ansible_collections/community/docker/plugins/modules/docker_network.py +++ b/ansible_collections/community/docker/plugins/modules/docker_network.py @@ -35,6 +35,18 @@ options: aliases: - network_name + config_from: + description: + - Specifies the config only network to use the config from. + type: str + version_added: 3.10.0 + + config_only: + description: + - Sets that this is a config only network. + type: bool + version_added: 3.10.0 + connected: description: - List of container names or container IDs to connect to a network. @@ -283,6 +295,8 @@ class TaskParameters(DockerBaseClass): self.name = None self.connected = None + self.config_from = None + self.config_only = None self.driver = None self.driver_options = None self.ipam_driver = None @@ -300,6 +314,11 @@ class TaskParameters(DockerBaseClass): for key, value in client.module.params.items(): setattr(self, key, value) + # config_only sets driver to 'null' (and scope to 'local') so force that here. Otherwise we get + # diffs of 'null' --> 'bridge' given that the driver option defaults to 'bridge'. + if self.config_only: + self.driver = 'null' + def container_names_in_network(network): return [c['Name'] for c in network['Containers'].values()] if network['Containers'] else [] @@ -401,6 +420,14 @@ class DockerNetworkManager(object): :return: (bool, list) ''' differences = DifferenceTracker() + if self.parameters.config_only is not None and self.parameters.config_only != net.get('ConfigOnly', False): + differences.add('config_only', + parameter=self.parameters.config_only, + active=net.get('ConfigOnly', False)) + if self.parameters.config_from is not None and self.parameters.config_from != net.get('ConfigFrom', {}).get('Network', ''): + differences.add('config_from', + parameter=self.parameters.config_from, + active=net.get('ConfigFrom', {}).get('Network', '')) if self.parameters.driver and self.parameters.driver != net['Driver']: differences.add('driver', parameter=self.parameters.driver, @@ -503,6 +530,10 @@ class DockerNetworkManager(object): 'CheckDuplicate': None, } + if self.parameters.config_only is not None: + data['ConfigOnly'] = self.parameters.config_only + if self.parameters.config_from: + data['ConfigFrom'] = {'Network': self.parameters.config_from} if self.parameters.enable_ipv6: data['EnableIPv6'] = True if self.parameters.internal: @@ -630,6 +661,8 @@ class DockerNetworkManager(object): def main(): argument_spec = dict( name=dict(type='str', required=True, aliases=['network_name']), + config_from=dict(type='str'), + config_only=dict(type='bool'), connected=dict(type='list', default=[], elements='str', aliases=['containers']), state=dict(type='str', default='present', choices=['present', 'absent']), driver=dict(type='str', default='bridge'), @@ -653,6 +686,8 @@ def main(): ) option_minimal_versions = dict( + config_from=dict(docker_api_version='1.30'), + config_only=dict(docker_api_version='1.30'), scope=dict(docker_api_version='1.30'), attachable=dict(docker_api_version='1.26'), ) diff --git a/ansible_collections/community/docker/plugins/modules/docker_prune.py b/ansible_collections/community/docker/plugins/modules/docker_prune.py index 1dfbf290e..a333c52fe 100644 --- a/ansible_collections/community/docker/plugins/modules/docker_prune.py +++ b/ansible_collections/community/docker/plugins/modules/docker_prune.py @@ -81,6 +81,28 @@ options: - Whether to prune the builder cache. type: bool default: false + builder_cache_all: + description: + - Whether to remove all types of build cache. + type: bool + default: false + version_added: 3.10.0 + builder_cache_filters: + description: + - A dictionary of filter values used for selecting images to delete. + - "For example, C(until: 10m)." + - See L(the API documentation,https://docs.docker.com/engine/api/v1.44/#tag/Image/operation/BuildPrune) + for more information on possible filters. + type: dict + version_added: 3.10.0 + builder_cache_keep_storage: + description: + - Amount of disk space to keep for cache in format C(<number>[<unit>])." + - "Number is a positive integer. Unit can be one of V(B) (byte), V(K) (kibibyte, 1024B), V(M) (mebibyte), V(G) (gibibyte), + V(T) (tebibyte), or V(P) (pebibyte)." + - "Omitting the unit defaults to bytes." + type: str + version_added: 3.10.0 author: - "Felix Fontein (@felixfontein)" @@ -181,11 +203,20 @@ builder_cache_space_reclaimed: returned: O(builder_cache=true) type: int sample: 0 +builder_cache_caches_deleted: + description: + - The build caches that were deleted. + returned: O(builder_cache=true) and API version is 1.39 or later + type: list + elements: str + sample: [] + version_added: 3.10.0 ''' import traceback from ansible.module_utils.common.text.converters import to_native +from ansible.module_utils.common.text.formatters import human_to_bytes from ansible_collections.community.docker.plugins.module_utils.common_api import ( AnsibleDockerClient, @@ -209,13 +240,29 @@ def main(): volumes=dict(type='bool', default=False), volumes_filters=dict(type='dict'), builder_cache=dict(type='bool', default=False), + builder_cache_all=dict(type='bool', default=False), + builder_cache_filters=dict(type='dict'), + builder_cache_keep_storage=dict(type='str'), # convert to bytes ) client = AnsibleDockerClient( argument_spec=argument_spec, + option_minimal_versions=dict( + builder_cache=dict(docker_py_version='1.31'), + builder_cache_all=dict(docker_py_version='1.39'), + builder_cache_filters=dict(docker_py_version='1.31'), + builder_cache_keep_storage=dict(docker_py_version='1.39'), + ), # supports_check_mode=True, ) + builder_cache_keep_storage = None + if client.module.params.get('builder_cache_keep_storage') is not None: + try: + builder_cache_keep_storage = human_to_bytes(client.module.params.get('builder_cache_keep_storage')) + except ValueError as exc: + client.module.fail_json(msg='Error while parsing value of builder_cache_keep_storage: {0}'.format(exc)) + try: result = dict() changed = False @@ -256,10 +303,21 @@ def main(): changed = True if client.module.params['builder_cache']: - res = client.post_to_json('/build/prune') + filters = clean_dict_booleans_for_docker_api(client.module.params.get('builder_cache_filters')) + params = {'filters': convert_filters(filters)} + if client.module.params.get('builder_cache_all'): + params['all'] = 'true' + if builder_cache_keep_storage is not None: + params['keep-storage'] = builder_cache_keep_storage + res = client.post_to_json('/build/prune', params=params) result['builder_cache_space_reclaimed'] = res['SpaceReclaimed'] if result['builder_cache_space_reclaimed']: changed = True + if 'CachesDeleted' in res: + # API version 1.39+: return value CachesDeleted (list of str) + result['builder_cache_caches_deleted'] = res['CachesDeleted'] + if result['builder_cache_caches_deleted']: + changed = True result['changed'] = changed client.module.exit_json(**result) diff --git a/ansible_collections/community/docker/plugins/modules/docker_swarm_service.py b/ansible_collections/community/docker/plugins/modules/docker_swarm_service.py index 95cc10366..4660d1138 100644 --- a/ansible_collections/community/docker/plugins/modules/docker_swarm_service.py +++ b/ansible_collections/community/docker/plugins/modules/docker_swarm_service.py @@ -83,6 +83,11 @@ options: - Dictionary of key value pairs. - Corresponds to the C(--container-label) option of C(docker service create). type: dict + sysctls: + description: + - Dictionary of key, value pairs. + version_added: 3.10.0 + type: dict dns: description: - List of custom DNS servers. @@ -681,6 +686,7 @@ swarm_service: "engine.labels.operatingsystem == ubuntu 14.04" ], "container_labels": null, + "sysctls": null, "dns": null, "dns_options": null, "dns_search": null, @@ -1226,6 +1232,7 @@ class DockerService(DockerBaseClass): self.log_driver_options = None self.labels = None self.container_labels = None + self.sysctls = None self.limit_cpu = None self.limit_memory = None self.reserve_cpu = None @@ -1292,6 +1299,7 @@ class DockerService(DockerBaseClass): 'placement_preferences': self.placement_preferences, 'labels': self.labels, 'container_labels': self.container_labels, + 'sysctls': self.sysctls, 'mode': self.mode, 'replicas': self.replicas, 'endpoint_mode': self.endpoint_mode, @@ -1539,6 +1547,7 @@ class DockerService(DockerBaseClass): s.tty = ap['tty'] s.labels = ap['labels'] s.container_labels = ap['container_labels'] + s.sysctls = ap['sysctls'] s.mode = ap['mode'] s.stop_signal = ap['stop_signal'] s.user = ap['user'] @@ -1740,6 +1749,8 @@ class DockerService(DockerBaseClass): differences.add('reserve_memory', parameter=self.reserve_memory, active=os.reserve_memory) if self.container_labels is not None and self.container_labels != (os.container_labels or {}): differences.add('container_labels', parameter=self.container_labels, active=os.container_labels) + if self.sysctls is not None and self.sysctls != (os.sysctls or {}): + differences.add('sysctls', parameter=self.sysctls, active=os.sysctls) if self.stop_signal is not None and self.stop_signal != os.stop_signal: differences.add('stop_signal', parameter=self.stop_signal, active=os.stop_signal) if self.stop_grace_period is not None and self.stop_grace_period != os.stop_grace_period: @@ -1934,6 +1945,8 @@ class DockerService(DockerBaseClass): container_spec_args['user'] = self.user if self.container_labels is not None: container_spec_args['labels'] = self.container_labels + if self.sysctls is not None: + container_spec_args['sysctls'] = self.sysctls if self.healthcheck is not None: container_spec_args['healthcheck'] = types.Healthcheck(**self.healthcheck) elif self.healthcheck_disabled: @@ -2163,6 +2176,7 @@ class DockerServiceManager(object): ds.read_only = task_template_data['ContainerSpec'].get('ReadOnly') ds.cap_add = task_template_data['ContainerSpec'].get('CapabilityAdd') ds.cap_drop = task_template_data['ContainerSpec'].get('CapabilityDrop') + ds.sysctls = task_template_data['ContainerSpec'].get('Sysctls') healthcheck_data = task_template_data['ContainerSpec'].get('Healthcheck') if healthcheck_data: @@ -2676,6 +2690,7 @@ def main(): hosts=dict(type='dict'), labels=dict(type='dict'), container_labels=dict(type='dict'), + sysctls=dict(type='dict'), mode=dict( type='str', default='replicated', @@ -2751,6 +2766,7 @@ def main(): init=dict(docker_py_version='4.0.0', docker_api_version='1.37'), cap_add=dict(docker_py_version='5.0.3', docker_api_version='1.41'), cap_drop=dict(docker_py_version='5.0.3', docker_api_version='1.41'), + sysctls=dict(docker_py_version='6.0.0', docker_api_version='1.40'), # specials publish_mode=dict( docker_py_version='3.0.0', diff --git a/ansible_collections/community/docker/plugins/plugin_utils/socket_handler.py b/ansible_collections/community/docker/plugins/plugin_utils/socket_handler.py index 204996f24..e8fd266c8 100644 --- a/ansible_collections/community/docker/plugins/plugin_utils/socket_handler.py +++ b/ansible_collections/community/docker/plugins/plugin_utils/socket_handler.py @@ -6,7 +6,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from ansible.compat import selectors +from ansible_collections.community.docker.plugins.module_utils.selectors import selectors from ansible_collections.community.docker.plugins.module_utils.socket_handler import ( DockerSocketHandlerBase, |