From ce7402dc9f505fb027f7242549c7177f820dd0c9 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Tue, 2 Jan 2024 09:21:45 +0100 Subject: [PATCH] Add docker_image_build module. (#735) --- README.md | 1 + meta/runtime.yml | 1 + plugins/doc_fragments/docker.py | 97 +++++ plugins/module_utils/common.py | 2 +- plugins/module_utils/common_api.py | 2 +- plugins/module_utils/common_cli.py | 345 ++++++++++++++++++ plugins/modules/docker_image.py | 2 + plugins/modules/docker_image_build.py | 316 ++++++++++++++++ .../targets/docker_image_build/aliases | 6 + .../targets/docker_image_build/meta/main.yml | 10 + .../targets/docker_image_build/tasks/main.yml | 13 + .../docker_image_build/tasks/run-test.yml | 7 + .../targets/docker_image_build/tasks/test.yml | 57 +++ .../tasks/tests/options.yml | 204 +++++++++++ .../templates/ArgsDockerfile | 13 + .../docker_image_build/templates/Dockerfile | 7 + .../templates/EtcHostsDockerfile | 7 + .../docker_image_build/templates/MyDockerfile | 9 + .../templates/StagedDockerfile | 11 + .../targets/setup_docker/tasks/Alpine.yml | 3 +- .../targets/setup_docker/tasks/main.yml | 8 + .../setup_docker_cli_buildx/meta/main.yml | 7 + .../setup_docker_cli_buildx/tasks/Alpine.yml | 13 + .../tasks/Archlinux.yml | 8 + .../setup_docker_cli_buildx/tasks/Debian.yml | 1 + .../setup_docker_cli_buildx/tasks/Fedora.yml | 1 + .../tasks/RedHat-7.yml | 1 + .../tasks/RedHat-8.yml | 8 + .../tasks/RedHat-9.yml | 1 + .../setup_docker_cli_buildx/tasks/Suse.yml | 14 + .../setup_docker_cli_buildx/tasks/main.yml | 49 +++ .../setup_docker_cli_buildx/tasks/nothing.yml | 7 + .../setup_docker_cli_buildx/vars/default.yml | 4 + 33 files changed, 1232 insertions(+), 3 deletions(-) create mode 100644 plugins/module_utils/common_cli.py create mode 100644 plugins/modules/docker_image_build.py create mode 100644 tests/integration/targets/docker_image_build/aliases create mode 100644 tests/integration/targets/docker_image_build/meta/main.yml create mode 100644 tests/integration/targets/docker_image_build/tasks/main.yml create mode 100644 tests/integration/targets/docker_image_build/tasks/run-test.yml create mode 100644 tests/integration/targets/docker_image_build/tasks/test.yml create mode 100644 tests/integration/targets/docker_image_build/tasks/tests/options.yml create mode 100644 tests/integration/targets/docker_image_build/templates/ArgsDockerfile create mode 100644 tests/integration/targets/docker_image_build/templates/Dockerfile create mode 100644 tests/integration/targets/docker_image_build/templates/EtcHostsDockerfile create mode 100644 tests/integration/targets/docker_image_build/templates/MyDockerfile create mode 100644 tests/integration/targets/docker_image_build/templates/StagedDockerfile create mode 100644 tests/integration/targets/setup_docker_cli_buildx/meta/main.yml create mode 100644 tests/integration/targets/setup_docker_cli_buildx/tasks/Alpine.yml create mode 100644 tests/integration/targets/setup_docker_cli_buildx/tasks/Archlinux.yml create mode 120000 tests/integration/targets/setup_docker_cli_buildx/tasks/Debian.yml create mode 120000 tests/integration/targets/setup_docker_cli_buildx/tasks/Fedora.yml create mode 120000 tests/integration/targets/setup_docker_cli_buildx/tasks/RedHat-7.yml create mode 100644 tests/integration/targets/setup_docker_cli_buildx/tasks/RedHat-8.yml create mode 120000 tests/integration/targets/setup_docker_cli_buildx/tasks/RedHat-9.yml create mode 100644 tests/integration/targets/setup_docker_cli_buildx/tasks/Suse.yml create mode 100644 tests/integration/targets/setup_docker_cli_buildx/tasks/main.yml create mode 100644 tests/integration/targets/setup_docker_cli_buildx/tasks/nothing.yml create mode 100644 tests/integration/targets/setup_docker_cli_buildx/vars/default.yml diff --git a/README.md b/README.md index 82f8f1b8f..c7ad2fc42 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ If you use the Ansible package and do not update collections independently, use - community.docker.docker_container_info: retrieve information on Docker containers - community.docker.docker_host_info: retrieve information on the Docker daemon - community.docker.docker_image: manage Docker images + - community.docker.docker_image_build: build Docker images using Docker buildx - community.docker.docker_image_info: retrieve information on Docker images - community.docker.docker_image_load: load Docker images from archives - community.docker.docker_image_pull: pull Docker images from registries diff --git a/meta/runtime.yml b/meta/runtime.yml index 50e72ea0e..ba8534315 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -14,6 +14,7 @@ action_groups: - docker_container_info - docker_host_info - docker_image + - docker_image_build - docker_image_info - docker_image_load - docker_image_pull diff --git a/plugins/doc_fragments/docker.py b/plugins/doc_fragments/docker.py index 90a8c70b6..1b9e67dec 100644 --- a/plugins/doc_fragments/docker.py +++ b/plugins/doc_fragments/docker.py @@ -297,3 +297,100 @@ class ModuleDocFragment(object): - pyOpenSSL (when using TLS) - backports.ssl_match_hostname (when using TLS on Python 2) ''' + + # Docker doc fragment when using the Docker CLI + CLI_DOCUMENTATION = r''' +options: + docker_cli: + description: + - Path to the Docker CLI. If not provided, will search for Docker CLI on the E(PATH). + type: path + docker_host: + description: + - The URL or Unix socket path used to connect to the Docker API. To connect to a remote host, provide the + TCP connection string. For example, V(tcp://192.0.2.23:2376). If TLS is used to encrypt the connection, + the module will automatically replace C(tcp) in the connection URL with C(https). + - If the value is not specified in the task, the value of environment variable E(DOCKER_HOST) will be used + instead. If the environment variable is not set, the default value will be used. + type: str + default: unix:///var/run/docker.sock + aliases: [ docker_url ] + tls_hostname: + description: + - When verifying the authenticity of the Docker Host server, provide the expected name of the server. + - If the value is not specified in the task, the value of environment variable E(DOCKER_TLS_HOSTNAME) will + be used instead. If the environment variable is not set, the default value will be used. + - Note that this option had a default value V(localhost) in older versions. It was removed in community.docker 3.0.0. + type: str + api_version: + description: + - The version of the Docker API running on the Docker Host. + - Defaults to the latest version of the API supported by this collection and the docker daemon. + - If the value is not specified in the task, the value of environment variable E(DOCKER_API_VERSION) will be + used instead. If the environment variable is not set, the default value will be used. + type: str + default: auto + aliases: [ docker_api_version ] + timeout: + description: + - The maximum amount of time in seconds to wait on a response from the API. + - If the value is not specified in the task, the value of environment variable E(DOCKER_TIMEOUT) will be used + instead. If the environment variable is not set, the default value will be used. + type: int + default: 60 + ca_cert: + description: + - Use a CA certificate when performing server verification by providing the path to a CA certificate file. + - If the value is not specified in the task and the environment variable E(DOCKER_CERT_PATH) is set, + the file C(ca.pem) from the directory specified in the environment variable E(DOCKER_CERT_PATH) will be used. + type: path + aliases: [ tls_ca_cert, cacert_path ] + client_cert: + description: + - Path to the client's TLS certificate file. + - If the value is not specified in the task and the environment variable E(DOCKER_CERT_PATH) is set, + the file C(cert.pem) from the directory specified in the environment variable E(DOCKER_CERT_PATH) will be used. + type: path + aliases: [ tls_client_cert, cert_path ] + client_key: + description: + - Path to the client's TLS key file. + - If the value is not specified in the task and the environment variable E(DOCKER_CERT_PATH) is set, + the file C(key.pem) from the directory specified in the environment variable E(DOCKER_CERT_PATH) will be used. + type: path + aliases: [ tls_client_key, key_path ] + tls: + description: + - Secure the connection to the API by using TLS without verifying the authenticity of the Docker host + server. Note that if O(validate_certs) is set to V(true) as well, it will take precedence. + - If the value is not specified in the task, the value of environment variable E(DOCKER_TLS) will be used + instead. If the environment variable is not set, the default value will be used. + type: bool + default: false + validate_certs: + description: + - Secure the connection to the API by using TLS and verifying the authenticity of the Docker host server. + - If the value is not specified in the task, the value of environment variable E(DOCKER_TLS_VERIFY) will be + used instead. If the environment variable is not set, the default value will be used. + type: bool + default: false + aliases: [ tls_verify ] + debug: + description: + - Debug mode + type: bool + default: false + cli_context: + description: + - The Docker CLI context to use. + type: str + +notes: + - Connect to the Docker daemon by providing parameters with each task or by defining environment variables. + You can define E(DOCKER_HOST), E(DOCKER_TLS_HOSTNAME), E(DOCKER_API_VERSION), E(DOCKER_CERT_PATH), + E(DOCKER_TLS), E(DOCKER_TLS_VERIFY) and E(DOCKER_TIMEOUT). If you are using docker machine, run the script shipped + with the product that sets up the environment. It will set these variables for you. See + U(https://docs.docker.com/machine/reference/env/) for more details. + - 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. +''' diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py index d68390e3e..de36936ad 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -482,7 +482,7 @@ def find_image(self, name, tag): images = self._image_lookup(lookup, tag) if len(images) > 1: - self.fail("Registry returned more than one result for %s:%s" % (name, tag)) + self.fail("Daemon returned more than one result for %s:%s" % (name, tag)) if len(images) == 1: try: diff --git a/plugins/module_utils/common_api.py b/plugins/module_utils/common_api.py index 7d46a153a..c0ca08df8 100644 --- a/plugins/module_utils/common_api.py +++ b/plugins/module_utils/common_api.py @@ -396,7 +396,7 @@ def find_image(self, name, tag): images = self._image_lookup(lookup, tag) if len(images) > 1: - self.fail("Registry returned more than one result for %s:%s" % (name, tag)) + self.fail("Daemon returned more than one result for %s:%s" % (name, tag)) if len(images) == 1: try: diff --git a/plugins/module_utils/common_cli.py b/plugins/module_utils/common_cli.py new file mode 100644 index 000000000..75cef8861 --- /dev/null +++ b/plugins/module_utils/common_cli.py @@ -0,0 +1,345 @@ +# Copyright (c) 2023, Felix Fontein +# 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 + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +import abc +import json +import shlex + +from ansible.module_utils.basic import AnsibleModule, env_fallback +from ansible.module_utils.common.process import get_bin_path +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion + +from ansible_collections.community.docker.plugins.module_utils._api.auth import resolve_repository_name + +from ansible_collections.community.docker.plugins.module_utils.util import ( # noqa: F401, pylint: disable=unused-import + DEFAULT_DOCKER_HOST, + DEFAULT_TLS, + DEFAULT_TLS_VERIFY, + DEFAULT_TIMEOUT_SECONDS, + DOCKER_MUTUALLY_EXCLUSIVE, + DOCKER_REQUIRED_TOGETHER, + sanitize_result, +) + + +DOCKER_COMMON_ARGS = dict( + docker_cli=dict(type='path'), + docker_host=dict(type='str', default=DEFAULT_DOCKER_HOST, fallback=(env_fallback, ['DOCKER_HOST']), aliases=['docker_url']), + tls_hostname=dict(type='str', fallback=(env_fallback, ['DOCKER_TLS_HOSTNAME'])), + api_version=dict(type='str', default='auto', fallback=(env_fallback, ['DOCKER_API_VERSION']), aliases=['docker_api_version']), + timeout=dict(type='int', default=DEFAULT_TIMEOUT_SECONDS, fallback=(env_fallback, ['DOCKER_TIMEOUT'])), + ca_cert=dict(type='path', aliases=['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']), + tls=dict(type='bool', default=DEFAULT_TLS, fallback=(env_fallback, ['DOCKER_TLS'])), + validate_certs=dict(type='bool', default=DEFAULT_TLS_VERIFY, fallback=(env_fallback, ['DOCKER_TLS_VERIFY']), aliases=['tls_verify']), + debug=dict(type='bool', default=False), + cli_context=dict(type='str'), +) + + +class DockerException(Exception): + pass + + +class AnsibleDockerClientBase(object): + def __init__(self, common_args, min_docker_api_version=None): + self._environment = {} + if common_args['tls_hostname']: + self._environment['DOCKER_TLS_HOSTNAME'] = common_args['tls_hostname'] + if common_args['api_version'] and common_args['api_version'] != 'auto': + self._environment['DOCKER_API_VERSION'] = common_args['api_version'] + self._cli = common_args.get('docker_cli') + if self._cli is None: + try: + self._cli = get_bin_path('docker') + except ValueError: + self.fail('Cannot find docker CLI in path. Please provide it explicitly with the docker_cli parameter') + + self._cli_base = [self._cli] + self._cli_base.extend(['--host', common_args['docker_host']]) + if common_args['validate_certs']: + self._cli_base.append('--tlsverify') + elif common_args['tls']: + self._cli_base.append('--tls') + if common_args['ca_cert']: + self._cli_base.extend(['--tlscacert', common_args['ca_cert']]) + if common_args['client_cert']: + self._cli_base.extend(['--tlscert', common_args['client_cert']]) + if common_args['client_key']: + self._cli_base.extend(['--tlskey', common_args['client_key']]) + if common_args['cli_context']: + self._cli_base.extend(['--context', common_args['cli_context']]) + + # `--format json` was only added as a shorthand for `--format {{ json . }}` in Docker 23.0 + dummy, self._version, dummy = self.call_cli_json('version', '--format', '{{ json . }}', check_rc=True) + self._info = None + + self.docker_api_version_str = self._version['Server']['ApiVersion'] + self.docker_api_version = LooseVersion(self.docker_api_version_str) + min_docker_api_version = min_docker_api_version or '1.25' + if self.docker_api_version < LooseVersion(min_docker_api_version): + self.fail('Docker API version is %s. Minimum version required is %s.' % (self.docker_api_version_str, min_docker_api_version)) + + def log(self, msg, pretty_print=False): + pass + # if self.debug: + # log_file = open('docker.log', 'a') + # if pretty_print: + # log_file.write(json.dumps(msg, sort_keys=True, indent=4, separators=(',', ': '))) + # log_file.write(u'\n') + # else: + # log_file.write(msg + u'\n') + + def get_cli(self): + return self._cli + + def get_version_info(self): + return self._version + + def _compose_cmd(self, args): + return self._cli_base + list(args) + + def _compose_cmd_str(self, args): + return ' '.join(shlex.quote(a) for a in self._compose_cmd(args)) + + @abc.abstractmethod + # def call_cli(self, *args, check_rc=False, data=None, cwd=None, environ_update=None): + def call_cli(self, *args, **kwargs): + # Python 2.7 doesn't like anything than '**kwargs' after '*args', so we have to do this manually... + pass + + # def call_cli_json(self, *args, check_rc=False, data=None, cwd=None, environ_update=None, warn_on_stderr=False): + def call_cli_json(self, *args, **kwargs): + warn_on_stderr = kwargs.pop('warn_on_stderr', False) + rc, stdout, stderr = self.call_cli(*args, **kwargs) + if warn_on_stderr and stderr: + self.warn(to_native(stderr)) + try: + data = json.loads(stdout) + except Exception as exc: + self.fail('Error while parsing JSON output of {cmd}: {exc}\nJSON output: {stdout}'.format( + cmd=self._compose_cmd_str(args), + exc=to_native(exc), + stdout=to_native(stdout), + )) + return rc, data, stderr + + # def call_cli_json_stream(self, *args, check_rc=False, data=None, cwd=None, environ_update=None, warn_on_stderr=False): + def call_cli_json_stream(self, *args, **kwargs): + warn_on_stderr = kwargs.pop('warn_on_stderr', False) + rc, stdout, stderr = self.call_cli(*args, **kwargs) + if warn_on_stderr and stderr: + self.warn(to_native(stderr)) + result = [] + try: + for line in stdout.splitlines(): + line = line.strip() + if line.startswith(b'{'): + result.append(json.loads(line)) + except Exception as exc: + self.fail('Error while parsing JSON output of {cmd}: {exc}\nJSON output: {stdout}'.format( + cmd=self._compose_cmd_str(args), + exc=to_native(exc), + stdout=to_native(stdout), + )) + return rc, result, stderr + + @abc.abstractmethod + def fail(self, msg, **kwargs): + pass + + @abc.abstractmethod + def warn(self, msg): + pass + + @abc.abstractmethod + def deprecate(self, msg, version=None, date=None, collection_name=None): + pass + + def get_cli_info(self): + if self._info is None: + dummy, self._info, dummy = self.call_cli_json('info', '--format', '{{ json . }}', check_rc=True) + return self._info + + def get_client_plugin_info(self, component): + for plugin in self.get_cli_info()['ClientInfo'].get('Plugins') or []: + if plugin.get('Name') == component: + return plugin + return None + + def _image_lookup(self, name, tag): + ''' + Including a tag in the name parameter sent to the Docker SDK for Python images method + does not work consistently. Instead, get the result set for name and manually check + if the tag exists. + ''' + dummy, images, dummy = self.call_cli_json_stream( + 'image', 'ls', '--format', '{{ json . }}', '--no-trunc', '--filter', 'reference={0}'.format(name), + check_rc=True, + ) + if tag: + lookup = "%s:%s" % (name, tag) + lookup_digest = "%s@%s" % (name, tag) + response = images + images = [] + for image in response: + if image.get('Tag') == tag or image.get('Digest') == tag: + images = [image] + break + return images + + def find_image(self, name, tag): + ''' + Lookup an image (by name and tag) and return the inspection results. + ''' + if not name: + return None + + self.log("Find image %s:%s" % (name, tag)) + images = self._image_lookup(name, tag) + if not images: + # In API <= 1.20 seeing 'docker.io/' as the name of images pulled from docker hub + registry, repo_name = resolve_repository_name(name) + if registry == 'docker.io': + # If docker.io is explicitly there in name, the image + # isn't found in some cases (#41509) + self.log("Check for docker.io image: %s" % repo_name) + images = self._image_lookup(repo_name, tag) + if not images and repo_name.startswith('library/'): + # Sometimes library/xxx images are not found + lookup = repo_name[len('library/'):] + self.log("Check for docker.io image: %s" % lookup) + images = self._image_lookup(lookup, tag) + if not images: + # Last case for some Docker versions: if docker.io wasn't there, + # it can be that the image wasn't found either + # (https://github.com/ansible/ansible/pull/15586) + lookup = "%s/%s" % (registry, repo_name) + self.log("Check for docker.io image: %s" % lookup) + images = self._image_lookup(lookup, tag) + if not images and '/' not in repo_name: + # This seems to be happening with podman-docker + # (https://github.com/ansible-collections/community.docker/issues/291) + lookup = "%s/library/%s" % (registry, repo_name) + self.log("Check for docker.io image: %s" % lookup) + images = self._image_lookup(lookup, tag) + + if len(images) > 1: + self.fail("Daemon returned more than one result for %s:%s" % (name, tag)) + + if len(images) == 1: + rc, image, stderr = self.call_cli_json('image', 'inspect', images[0]['ID']) + if not image: + self.log("Image %s:%s not found." % (name, tag)) + return None + if rc != 0: + self.fail("Error inspecting image %s:%s - %s" % (name, tag, to_native(stderr))) + return image[0] + + self.log("Image %s:%s not found." % (name, tag)) + return None + + def find_image_by_id(self, image_id, accept_missing_image=False): + ''' + Lookup an image (by ID) and return the inspection results. + ''' + if not image_id: + return None + + self.log("Find image %s (by ID)" % image_id) + rc, image, stderr = self.call_cli_json('image', 'inspect', image_id) + if not image: + if not accept_missing_image: + self.fail("Error inspecting image ID %s - %s" % (image_id, to_native(stderr))) + self.log("Image %s not found." % image_id) + return None + if rc != 0: + self.fail("Error inspecting image ID %s - %s" % (image_id, to_native(stderr))) + return image[0] + + +class AnsibleModuleDockerClient(AnsibleDockerClientBase): + def __init__(self, argument_spec=None, supports_check_mode=False, mutually_exclusive=None, + required_together=None, required_if=None, required_one_of=None, required_by=None, + min_docker_api_version=None, fail_results=None): + + # Modules can put information in here which will always be returned + # in case client.fail() is called. + self.fail_results = fail_results or {} + + merged_arg_spec = dict() + merged_arg_spec.update(DOCKER_COMMON_ARGS) + if argument_spec: + merged_arg_spec.update(argument_spec) + self.arg_spec = merged_arg_spec + + mutually_exclusive_params = [] + mutually_exclusive_params += DOCKER_MUTUALLY_EXCLUSIVE + if mutually_exclusive: + mutually_exclusive_params += mutually_exclusive + + required_together_params = [] + required_together_params += DOCKER_REQUIRED_TOGETHER + if required_together: + required_together_params += required_together + + self.module = AnsibleModule( + argument_spec=merged_arg_spec, + supports_check_mode=supports_check_mode, + mutually_exclusive=mutually_exclusive_params, + required_together=required_together_params, + required_if=required_if, + required_one_of=required_one_of, + required_by=required_by or {}, + ) + + self.debug = self.module.params['debug'] + self.check_mode = self.module.check_mode + self.diff = self.module._diff + + common_args = dict((k, self.module.params[k]) for k in DOCKER_COMMON_ARGS) + super(AnsibleModuleDockerClient, self).__init__(common_args, min_docker_api_version=min_docker_api_version) + + # def call_cli(self, *args, check_rc=False, data=None, cwd=None, environ_update=None): + def call_cli(self, *args, **kwargs): + # Python 2.7 doesn't like anything than '**kwargs' after '*args', so we have to do this manually... + check_rc = kwargs.pop('check_rc', False) + data = kwargs.pop('data', None) + cwd = kwargs.pop('cwd', None) + environ_update = kwargs.pop('environ_update', None) + if kwargs: + raise TypeError("call_cli() got an unexpected keyword argument '%s'" % list(kwargs)[0]) + + environment = self._environment.copy() + if environ_update: + environment.update(environ_update) + rc, stdout, stderr = self.module.run_command( + self._compose_cmd(args), + binary_data=True, + check_rc=check_rc, + cwd=cwd, + data=data, + encoding=None, + environ_update=environment, + expand_user_and_vars=False, + ignore_invalid_cwd=False, + ) + return rc, stdout, stderr + + def fail(self, msg, **kwargs): + self.fail_results.update(kwargs) + self.module.fail_json(msg=msg, **sanitize_result(self.fail_results)) + + def warn(self, msg): + self.module.warn(msg) + + def deprecate(self, msg, version=None, date=None, collection_name=None): + self.module.deprecate(msg, version=version, date=date, collection_name=collection_name) diff --git a/plugins/modules/docker_image.py b/plugins/modules/docker_image.py index 855a712c7..97e7ec8cc 100644 --- a/plugins/modules/docker_image.py +++ b/plugins/modules/docker_image.py @@ -20,6 +20,7 @@ notes: - Building images is done using Docker daemon's API. It is not possible to use BuildKit / buildx this way. + Use M(community.docker.docker_image_build) to build images with BuildKit. extends_documentation_fragment: - community.docker.docker.api_documentation @@ -251,6 +252,7 @@ - Sorin Sbarnea (@ssbarnea) seealso: + - module: community.docker.docker_image_build - module: community.docker.docker_image_info - module: community.docker.docker_image_load - module: community.docker.docker_image_pull diff --git a/plugins/modules/docker_image_build.py b/plugins/modules/docker_image_build.py new file mode 100644 index 000000000..68c707914 --- /dev/null +++ b/plugins/modules/docker_image_build.py @@ -0,0 +1,316 @@ +#!/usr/bin/python +# +# Copyright (c) 2023, Felix Fontein +# 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 + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: docker_image_build + +short_description: Build Docker images using Docker buildx + +version_added: 3.6.0 + +description: + - This module allos to build Docker images using Docker's buildx plugin (BuildKit). + +extends_documentation_fragment: + - community.docker.docker.cli_documentation + - community.docker.attributes + - community.docker.attributes.actiongroup_docker + +attributes: + check_mode: + support: full + diff_mode: + support: none + +options: + name: + description: + - "Image name. Name format will be one of: C(name), C(repository/name), C(registry_server:port/name). + When pushing or pulling an image the name can optionally include the tag by appending C(:tag_name)." + - Note that image IDs (hashes) and names with digest cannot be used. + type: str + required: true + tag: + description: + - Tag for the image name O(name) that is to be tagged. + - If O(name)'s format is C(name:tag), then the tag value from O(name) will take precedence. + type: str + default: latest + path: + description: + - The path for the build environment. + type: path + required: true + dockerfile: + description: + - Provide an alternate name for the Dockerfile to use when building an image. + - This can also include a relative path (relative to O(path)). + type: str + cache_from: + description: + - List of image names to consider as cache source. + type: list + elements: str + pull: + description: + - When building an image downloads any updates to the FROM image in Dockerfile. + type: bool + default: false + network: + description: + - The network to use for C(RUN) build instructions. + type: str + nocache: + description: + - Do not use cache when building an image. + type: bool + default: false + etc_hosts: + description: + - Extra hosts to add to C(/etc/hosts) in building containers, as a mapping of hostname to IP address. + type: dict + args: + description: + - Provide a dictionary of C(key:value) build arguments that map to Dockerfile ARG directive. + - Docker expects the value to be a string. For convenience any non-string values will be converted to strings. + type: dict + target: + description: + - When building an image specifies an intermediate build stage by + name as a final stage for the resulting image. + type: str + platform: + description: + - Platform in the format C(os[/arch[/variant]]). + type: str + shm_size: + description: + - "Size of C(/dev/shm) in format C([]). Number is positive integer. + Unit can be 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. If you omit the size entirely, Docker daemon uses V(64M). + type: str + labels: + description: + - Dictionary of key value pairs. + type: dict + rebuild: + description: + - Defines the behavior of the module if the image to build (as specified in O(name) and O(tag)) already exists. + type: str + choices: + - never + - always + default: never + +requirements: + - "Docker CLI with Docker buildx plugin" + +author: + - Felix Fontein (@felixfontein) + +seealso: + - module: community.docker.docker_image_push + - module: community.docker.docker_image_tag +''' + +EXAMPLES = ''' +- name: Build Python 3.12 image + community.docker.docker_image_build: + name: localhost/python/3.12:latest + path: /home/user/images/python + dockerfile: Dockerfile-3.12 +''' + +RETURN = ''' +image: + description: Image inspection results for the affected image. + returned: success + type: dict + sample: {} +''' + +import os +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_cli import ( + AnsibleModuleDockerClient, + DockerException, +) + +from ansible_collections.community.docker.plugins.module_utils.util import ( + DockerBaseClass, + clean_dict_booleans_for_docker_api, + is_image_name_id, + is_valid_tag, +) + +from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import ( + parse_repository_tag, +) + + +def convert_to_bytes(value, module, name, unlimited_value=None): + if value is None: + return value + try: + if unlimited_value is not None and value in ('unlimited', str(unlimited_value)): + return unlimited_value + return human_to_bytes(value) + except ValueError as exc: + module.fail_json(msg='Failed to convert %s to bytes: %s' % (name, to_native(exc))) + + +def dict_to_list(dictionary, concat='='): + return ['%s%s%s' % (k, concat, v) for k, v in sorted(dictionary.items())] + + +class ImageBuilder(DockerBaseClass): + def __init__(self, client): + super(ImageBuilder, self).__init__() + self.client = client + self.check_mode = self.client.check_mode + parameters = self.client.module.params + + self.cache_from = parameters['cache_from'] + self.pull = parameters['pull'] + self.network = parameters['network'] + self.nocache = parameters['nocache'] + self.etc_hosts = clean_dict_booleans_for_docker_api(parameters['etc_hosts']) + self.args = clean_dict_booleans_for_docker_api(parameters['args']) + self.target = parameters['target'] + self.platform = parameters['platform'] + 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'] + + 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())) + + self.path = parameters['path'] + if not os.path.isdir(self.path): + self.fail('"{0}" is not an existing directory'.format(self.path)) + self.dockerfile = parameters['dockerfile'] + if self.dockerfile and not os.path.isfile(os.path.join(self.path, self.dockerfile)): + self.fail('"{0}" is not an existing file'.format(os.path.join(self.path, self.dockerfile))) + + self.name = parameters['name'] + self.tag = parameters['tag'] + if not is_valid_tag(self.tag, allow_empty=True): + self.fail('"{0}" is not a valid docker tag'.format(self.tag)) + if is_image_name_id(self.name): + self.fail('Image name must not be a digest') + + # If name contains a tag, it takes precedence over tag parameter. + repo, repo_tag = parse_repository_tag(self.name) + if repo_tag: + self.name = repo + self.tag = repo_tag + + if is_image_name_id(self.tag): + self.fail('Image name must not contain a digest, but have a tag') + + def fail(self, msg, **kwargs): + self.client.fail(msg, **kwargs) + + def add_list_arg(self, args, option, values): + for value in values: + args.extend([option, value]) + + def add_args(self, args): + args.extend(['--tag', '%s:%s' % (self.name, self.tag)]) + if self.dockerfile: + args.extend(['--file', os.path.join(self.path, self.dockerfile)]) + if self.cache_from: + self.add_list_arg(args, '--cache-from', self.cache_from) + if self.pull: + args.append('--pull') + if self.network: + args.extend(['--network', self.network]) + if self.nocache: + args.append('--no-cache') + if self.etc_hosts: + self.add_list_arg(args, '--add-host', dict_to_list(self.etc_hosts, ':')) + if self.args: + self.add_list_arg(args, '--build-arg', dict_to_list(self.args)) + if self.target: + args.extend(['--target', self.target]) + if self.platform: + args.extend(['--platform', self.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)) + + def build_image(self): + image = self.client.find_image(self.name, self.tag) + results = dict( + changed=False, + actions=[], + image=image or {}, + ) + + if image: + if self.rebuild == 'never': + return results + + results['changed'] = True + if not self.check_mode: + args = ['buildx', 'build', '--progress', 'plain'] + self.add_args(args) + args.extend(['--', self.path]) + rc, stdout, stderr = self.client.call_cli(*args) + 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) + results['stderr'] = to_native(stderr) + results['image'] = self.client.find_image(self.name, self.tag) or {} + + return results + + +def main(): + argument_spec = dict( + name=dict(type='str', required=True), + tag=dict(type='str', default='latest'), + path=dict(type='path', required=True), + dockerfile=dict(type='str'), + cache_from=dict(type='list', elements='str'), + pull=dict(type='bool', default=False), + network=dict(type='str'), + nocache=dict(type='bool', default=False), + etc_hosts=dict(type='dict'), + args=dict(type='dict'), + target=dict(type='str'), + platform=dict(type='str'), + shm_size=dict(type='str'), + labels=dict(type='dict'), + rebuild=dict(type='str', choices=['never', 'always'], default='never'), + ) + + client = AnsibleModuleDockerClient( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + try: + results = ImageBuilder(client).build_image() + client.module.exit_json(**results) + except DockerException as e: + client.fail('An unexpected Docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc()) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/docker_image_build/aliases b/tests/integration/targets/docker_image_build/aliases new file mode 100644 index 000000000..2e1acc0ad --- /dev/null +++ b/tests/integration/targets/docker_image_build/aliases @@ -0,0 +1,6 @@ +# Copyright (c) Ansible Project +# 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 + +azp/4 +destructive diff --git a/tests/integration/targets/docker_image_build/meta/main.yml b/tests/integration/targets/docker_image_build/meta/main.yml new file mode 100644 index 000000000..71ac98d4f --- /dev/null +++ b/tests/integration/targets/docker_image_build/meta/main.yml @@ -0,0 +1,10 @@ +--- +# Copyright (c) Ansible Project +# 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 + +dependencies: + - setup_docker_cli_buildx + # The Python dependencies are needed for the other modules + - setup_docker_python_deps + - setup_remote_tmp_dir diff --git a/tests/integration/targets/docker_image_build/tasks/main.yml b/tests/integration/targets/docker_image_build/tasks/main.yml new file mode 100644 index 000000000..88b23cfe7 --- /dev/null +++ b/tests/integration/targets/docker_image_build/tasks/main.yml @@ -0,0 +1,13 @@ +--- +# Copyright (c) Ansible Project +# 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 + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- when: ansible_facts.distribution ~ ansible_facts.distribution_major_version not in ['CentOS6', 'RedHat6'] + include_tasks: + file: test.yml diff --git a/tests/integration/targets/docker_image_build/tasks/run-test.yml b/tests/integration/targets/docker_image_build/tasks/run-test.yml new file mode 100644 index 000000000..65853ddd8 --- /dev/null +++ b/tests/integration/targets/docker_image_build/tasks/run-test.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# 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 + +- name: "Loading tasks from {{ item }}" + include_tasks: "{{ item }}" diff --git a/tests/integration/targets/docker_image_build/tasks/test.yml b/tests/integration/targets/docker_image_build/tasks/test.yml new file mode 100644 index 000000000..4f7fb9d13 --- /dev/null +++ b/tests/integration/targets/docker_image_build/tasks/test.yml @@ -0,0 +1,57 @@ +--- +# Copyright (c) Ansible Project +# 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 + +- name: Create random name prefix + set_fact: + name_prefix: "{{ 'ansible-docker-test-%0x' % ((2**32) | random) }}" +- name: Create image and container list + set_fact: + inames: [] + cnames: [] + +- debug: + msg: "Using name prefix {{ name_prefix }}" + +- name: Create files directory + file: + path: '{{ remote_tmp_dir }}/files' + state: directory + +- name: Template files + template: + src: '{{ item }}' + dest: '{{ remote_tmp_dir }}/files/{{ item }}' + loop: + - ArgsDockerfile + - Dockerfile + - EtcHostsDockerfile + - MyDockerfile + - StagedDockerfile + +- debug: + msg: "Has buildx plugin: {{ docker_has_buildx }}" + +- block: + - include_tasks: run-test.yml + with_fileglob: + - "tests/*.yml" + + always: + - name: "Make sure all images are removed" + docker_image: + name: "{{ item }}" + state: absent + with_items: "{{ inames }}" + - name: "Make sure all containers are removed" + docker_container: + name: "{{ item }}" + state: absent + force_kill: true + with_items: "{{ cnames }}" + + when: docker_api_version is version('1.25', '>=') and docker_cli_version is version('19.03', '>=') and docker_has_buildx + +- fail: msg="Too old docker / docker-py version to run docker_image tests!" + when: not(docker_api_version is version('1.25', '>=') and docker_cli_version is version('19.03', '>=')) and (ansible_distribution != 'CentOS' or ansible_distribution_major_version|int > 6) and docker_has_buildx diff --git a/tests/integration/targets/docker_image_build/tasks/tests/options.yml b/tests/integration/targets/docker_image_build/tasks/tests/options.yml new file mode 100644 index 000000000..990035788 --- /dev/null +++ b/tests/integration/targets/docker_image_build/tasks/tests/options.yml @@ -0,0 +1,204 @@ +--- +# Copyright (c) Ansible Project +# 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 + +- name: Registering image name + set_fact: + iname: "{{ name_prefix ~ '-options' }}" + +- name: Registering image name + set_fact: + inames: "{{ inames + [iname] }}" + +#################################################################### +## args ############################################################ +#################################################################### + +- name: cleanup + docker_image_remove: + name: "{{ iname }}" + +- name: buildargs + docker_image_build: + name: "{{ iname }}" + path: "{{ remote_tmp_dir }}/files" + dockerfile: "ArgsDockerfile" + args: + IMAGE: "{{ docker_test_image_busybox }}" + TEST1: val1 + TEST2: val2 + TEST3: "True" + pull: false + register: buildargs_1 + +- name: buildargs (idempotency) + docker_image_build: + name: "{{ iname }}" + path: "{{ remote_tmp_dir }}/files" + dockerfile: "ArgsDockerfile" + args: + IMAGE: "{{ docker_test_image_busybox }}" + TEST1: val1 + TEST2: val2 + TEST3: "True" + pull: false + register: buildargs_2 + +- name: cleanup + docker_image_remove: + name: "{{ iname }}" + +- assert: + that: + - buildargs_1 is changed + - buildargs_2 is not changed + +#################################################################### +## dockerfile ###################################################### +#################################################################### + +- name: dockerfile + docker_image_build: + name: "{{ iname }}" + path: "{{ remote_tmp_dir }}/files" + dockerfile: "MyDockerfile" + pull: false + register: dockerfile_1 + +- name: cleanup + docker_image_remove: + name: "{{ iname }}" + +- assert: + that: + - dockerfile_1 is changed + - "('FROM ' ~ docker_test_image_alpine) in dockerfile_1.stderr" + - dockerfile_1['image']['Config']['WorkingDir'] == '/newdata' + +#################################################################### +## platform ######################################################## +#################################################################### + +- name: cleanup + docker_image_remove: + name: "{{ iname }}" + +- name: platform + docker_image_build: + name: "{{ iname }}" + path: "{{ remote_tmp_dir }}/files" + platform: linux + pull: false + register: platform_1 + +- name: platform (idempotency) + docker_image_build: + name: "{{ iname }}" + path: "{{ remote_tmp_dir }}/files" + platform: linux + pull: false + register: platform_2 + +- name: cleanup + docker_image_remove: + name: "{{ iname }}" + +- assert: + that: + - platform_1 is changed + - platform_2 is not changed + +#################################################################### +## target ########################################################## +#################################################################### + +- name: Build multi-stage image + docker_image_build: + name: "{{ iname }}" + path: "{{ remote_tmp_dir }}/files" + dockerfile: "StagedDockerfile" + target: first + pull: false + register: dockerfile_2 + +- name: cleanup + docker_image_remove: + name: "{{ iname }}" + +- assert: + that: + - dockerfile_2 is changed + - dockerfile_2.image.Config.WorkingDir == '/first' + +#################################################################### +## etc_hosts ####################################################### +#################################################################### + +- name: Build image with custom etc_hosts + docker_image_build: + name: "{{ iname }}" + path: "{{ remote_tmp_dir }}/files" + dockerfile: "EtcHostsDockerfile" + pull: false + etc_hosts: + some-custom-host: "127.0.0.1" + register: path_1 + +- name: cleanup + docker_image_remove: + name: "{{ iname }}" + +- assert: + that: + - path_1 is changed + +#################################################################### +## shm_size ######################################################## +#################################################################### + +- name: Build image with custom shm_size + docker_image_build: + name: "{{ iname }}" + path: "{{ remote_tmp_dir }}/files" + dockerfile: "MyDockerfile" + pull: false + shm_size: 128MB + register: path_1 + +- name: cleanup + docker_image_remove: + name: "{{ iname }}" + +- assert: + that: + - path_1 is changed + +#################################################################### +## labels ########################################################## +#################################################################### + +- name: Build image with labels + docker_image_build: + name: "{{ iname }}" + path: "{{ remote_tmp_dir }}/files" + dockerfile: "MyDockerfile" + pull: false + labels: + FOO: BAR + this is a label: this is the label's value + register: labels_1 + +- name: cleanup + docker_image_remove: + name: "{{ iname }}" + +- name: Show image information + debug: + var: labels_1.image + +- assert: + that: + - labels_1 is changed + - labels_1.image.Config.Labels.FOO == 'BAR' + - labels_1.image.Config.Labels["this is a label"] == "this is the label's value" diff --git a/tests/integration/targets/docker_image_build/templates/ArgsDockerfile b/tests/integration/targets/docker_image_build/templates/ArgsDockerfile new file mode 100644 index 000000000..dedd88a8f --- /dev/null +++ b/tests/integration/targets/docker_image_build/templates/ArgsDockerfile @@ -0,0 +1,13 @@ +# Copyright (c) 2022, Felix Fontein +# 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 + +ARG IMAGE +ARG TEST1 +ARG TEST2 +ARG TEST3 + +FROM ${IMAGE} +ENV foo /bar +WORKDIR ${foo} +RUN echo "${TEST1} - ${TEST2} - ${TEST3}" diff --git a/tests/integration/targets/docker_image_build/templates/Dockerfile b/tests/integration/targets/docker_image_build/templates/Dockerfile new file mode 100644 index 000000000..286094b9e --- /dev/null +++ b/tests/integration/targets/docker_image_build/templates/Dockerfile @@ -0,0 +1,7 @@ +# Copyright (c) Ansible Project +# 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 + +FROM {{ docker_test_image_busybox }} +ENV foo /bar +WORKDIR ${foo} diff --git a/tests/integration/targets/docker_image_build/templates/EtcHostsDockerfile b/tests/integration/targets/docker_image_build/templates/EtcHostsDockerfile new file mode 100644 index 000000000..bc21b966b --- /dev/null +++ b/tests/integration/targets/docker_image_build/templates/EtcHostsDockerfile @@ -0,0 +1,7 @@ +# Copyright (c) Ansible Project +# 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 + +FROM {{ docker_test_image_busybox }} +# This should fail building if docker cannot resolve some-custom-host +RUN ping -c1 some-custom-host diff --git a/tests/integration/targets/docker_image_build/templates/MyDockerfile b/tests/integration/targets/docker_image_build/templates/MyDockerfile new file mode 100644 index 000000000..24b1c926f --- /dev/null +++ b/tests/integration/targets/docker_image_build/templates/MyDockerfile @@ -0,0 +1,9 @@ +# Copyright (c) Ansible Project +# 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 + +FROM {{ docker_test_image_alpine }} +ENV INSTALL_PATH /newdata +RUN mkdir -p $INSTALL_PATH + +WORKDIR $INSTALL_PATH diff --git a/tests/integration/targets/docker_image_build/templates/StagedDockerfile b/tests/integration/targets/docker_image_build/templates/StagedDockerfile new file mode 100644 index 000000000..da2253425 --- /dev/null +++ b/tests/integration/targets/docker_image_build/templates/StagedDockerfile @@ -0,0 +1,11 @@ +# Copyright (c) Ansible Project +# 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 + +FROM {{ docker_test_image_busybox }} AS first +ENV dir /first +WORKDIR ${dir} + +FROM {{ docker_test_image_busybox }} AS second +ENV dir /second +WORKDIR ${dir} diff --git a/tests/integration/targets/setup_docker/tasks/Alpine.yml b/tests/integration/targets/setup_docker/tasks/Alpine.yml index 64f6eb34a..cb07fbf78 100644 --- a/tests/integration/targets/setup_docker/tasks/Alpine.yml +++ b/tests/integration/targets/setup_docker/tasks/Alpine.yml @@ -5,6 +5,7 @@ - name: Install docker apk: - name: docker + name: + - docker update_cache: true notify: cleanup docker diff --git a/tests/integration/targets/setup_docker/tasks/main.yml b/tests/integration/targets/setup_docker/tasks/main.yml index 28b65ec3b..e3d7c413f 100644 --- a/tests/integration/targets/setup_docker/tasks/main.yml +++ b/tests/integration/targets/setup_docker/tasks/main.yml @@ -25,6 +25,14 @@ set_fact: needs_docker_daemon: '{{ not ansible_module_running_in_container }}' + - name: + debug: + msg: |- + OS family: {{ ansible_facts.os_family }} + Distribution: {{ ansible_facts.distribution }} + Distribution major version: {{ ansible_facts.distribution_major_version }} + Distribution full version: {{ ansible_facts.distribution_version }} + - name: Include distribution specific variables include_vars: "{{ lookup('first_found', params) }}" vars: diff --git a/tests/integration/targets/setup_docker_cli_buildx/meta/main.yml b/tests/integration/targets/setup_docker_cli_buildx/meta/main.yml new file mode 100644 index 000000000..5769ff1cb --- /dev/null +++ b/tests/integration/targets/setup_docker_cli_buildx/meta/main.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# 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 + +dependencies: + - setup_docker diff --git a/tests/integration/targets/setup_docker_cli_buildx/tasks/Alpine.yml b/tests/integration/targets/setup_docker_cli_buildx/tasks/Alpine.yml new file mode 100644 index 000000000..48d357a3b --- /dev/null +++ b/tests/integration/targets/setup_docker_cli_buildx/tasks/Alpine.yml @@ -0,0 +1,13 @@ +--- +# Copyright (c) Ansible Project +# 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 + +- name: Buildx is available from Alpine 3.14 on + set_fact: + docker_has_buildx: "{{ ansible_facts.distribution_version is version('3.14', '>=') }}" + +- name: Install Docker buildx CLI plugin + apk: + name: docker-cli-buildx + when: docker_has_buildx diff --git a/tests/integration/targets/setup_docker_cli_buildx/tasks/Archlinux.yml b/tests/integration/targets/setup_docker_cli_buildx/tasks/Archlinux.yml new file mode 100644 index 000000000..5d2709017 --- /dev/null +++ b/tests/integration/targets/setup_docker_cli_buildx/tasks/Archlinux.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# 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 + +- name: Install Docker buildx CLI plugin + community.general.pacman: + name: docker-buildx diff --git a/tests/integration/targets/setup_docker_cli_buildx/tasks/Debian.yml b/tests/integration/targets/setup_docker_cli_buildx/tasks/Debian.yml new file mode 120000 index 000000000..0b0695149 --- /dev/null +++ b/tests/integration/targets/setup_docker_cli_buildx/tasks/Debian.yml @@ -0,0 +1 @@ +nothing.yml \ No newline at end of file diff --git a/tests/integration/targets/setup_docker_cli_buildx/tasks/Fedora.yml b/tests/integration/targets/setup_docker_cli_buildx/tasks/Fedora.yml new file mode 120000 index 000000000..0b0695149 --- /dev/null +++ b/tests/integration/targets/setup_docker_cli_buildx/tasks/Fedora.yml @@ -0,0 +1 @@ +nothing.yml \ No newline at end of file diff --git a/tests/integration/targets/setup_docker_cli_buildx/tasks/RedHat-7.yml b/tests/integration/targets/setup_docker_cli_buildx/tasks/RedHat-7.yml new file mode 120000 index 000000000..0b0695149 --- /dev/null +++ b/tests/integration/targets/setup_docker_cli_buildx/tasks/RedHat-7.yml @@ -0,0 +1 @@ +nothing.yml \ No newline at end of file diff --git a/tests/integration/targets/setup_docker_cli_buildx/tasks/RedHat-8.yml b/tests/integration/targets/setup_docker_cli_buildx/tasks/RedHat-8.yml new file mode 100644 index 000000000..6bb81a941 --- /dev/null +++ b/tests/integration/targets/setup_docker_cli_buildx/tasks/RedHat-8.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# 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 + +- name: For some reason we don't have the buildx plugin + set_fact: + docker_has_buildx: false diff --git a/tests/integration/targets/setup_docker_cli_buildx/tasks/RedHat-9.yml b/tests/integration/targets/setup_docker_cli_buildx/tasks/RedHat-9.yml new file mode 120000 index 000000000..0b0695149 --- /dev/null +++ b/tests/integration/targets/setup_docker_cli_buildx/tasks/RedHat-9.yml @@ -0,0 +1 @@ +nothing.yml \ No newline at end of file diff --git a/tests/integration/targets/setup_docker_cli_buildx/tasks/Suse.yml b/tests/integration/targets/setup_docker_cli_buildx/tasks/Suse.yml new file mode 100644 index 000000000..fd667cc24 --- /dev/null +++ b/tests/integration/targets/setup_docker_cli_buildx/tasks/Suse.yml @@ -0,0 +1,14 @@ +--- +# Copyright (c) Ansible Project +# 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 + +- name: Buildx is available in OpenSuSE 15.5, not sure which versions before + set_fact: + docker_has_buildx: "{{ ansible_facts.distribution_version is version('15.5', '>=') }}" + +- name: Install Docker buildx CLI plugin + community.general.zypper: + name: docker-buildx + disable_gpg_check: true + when: docker_has_buildx diff --git a/tests/integration/targets/setup_docker_cli_buildx/tasks/main.yml b/tests/integration/targets/setup_docker_cli_buildx/tasks/main.yml new file mode 100644 index 000000000..979ad2ba6 --- /dev/null +++ b/tests/integration/targets/setup_docker_cli_buildx/tasks/main.yml @@ -0,0 +1,49 @@ +--- +# Copyright (c) Ansible Project +# 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 + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Setup Docker + when: ansible_facts.distribution ~ ansible_facts.distribution_major_version not in ['CentOS6', 'RedHat6'] + block: + - name: + debug: + msg: |- + OS family: {{ ansible_facts.os_family }} + Distribution: {{ ansible_facts.distribution }} + Distribution major version: {{ ansible_facts.distribution_major_version }} + Distribution full version: {{ ansible_facts.distribution_version }} + + - name: Set facts for Docker features to defaults + set_fact: + docker_has_buildx: true + + - name: Include distribution specific variables + include_vars: "{{ lookup('first_found', params) }}" + vars: + params: + files: + - "{{ ansible_facts.distribution }}-{{ ansible_facts.distribution_major_version }}.yml" + - "{{ ansible_facts.os_family }}-{{ ansible_facts.distribution_major_version }}.yml" + - "{{ ansible_facts.distribution }}.yml" + - "{{ ansible_facts.os_family }}.yml" + - default.yml + paths: + - "{{ role_path }}/vars" + + - name: Include distribution specific tasks + include_tasks: "{{ lookup('first_found', params) }}" + vars: + params: + files: + - "{{ ansible_facts.distribution }}-{{ ansible_facts.distribution_major_version }}.yml" + - "{{ ansible_facts.os_family }}-{{ ansible_facts.distribution_major_version }}.yml" + - "{{ ansible_facts.distribution }}.yml" + - "{{ ansible_facts.os_family }}.yml" + paths: + - "{{ role_path }}/tasks" diff --git a/tests/integration/targets/setup_docker_cli_buildx/tasks/nothing.yml b/tests/integration/targets/setup_docker_cli_buildx/tasks/nothing.yml new file mode 100644 index 000000000..a93d788ac --- /dev/null +++ b/tests/integration/targets/setup_docker_cli_buildx/tasks/nothing.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# 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 + +# nothing to do +[] diff --git a/tests/integration/targets/setup_docker_cli_buildx/vars/default.yml b/tests/integration/targets/setup_docker_cli_buildx/vars/default.yml new file mode 100644 index 000000000..f55df21f8 --- /dev/null +++ b/tests/integration/targets/setup_docker_cli_buildx/vars/default.yml @@ -0,0 +1,4 @@ +--- +# Copyright (c) Ansible Project +# 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