Add docker_image_build module. (#735)

This commit is contained in:
Felix Fontein
2024-01-02 09:21:45 +01:00
committed by GitHub
parent 199d9e50d3
commit ce7402dc9f
33 changed files with 1232 additions and 3 deletions
+97
View File
@@ -297,3 +297,100 @@ requirements:
- 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.
'''
+1 -1
View File
@@ -482,7 +482,7 @@ class AnsibleDockerClientBase(Client):
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:
+1 -1
View File
@@ -396,7 +396,7 @@ class AnsibleDockerClientBase(Client):
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:
+345
View File
@@ -0,0 +1,345 @@
# Copyright (c) 2023, 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
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/<name>' 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)
+2
View File
@@ -20,6 +20,7 @@ description:
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 @@ author:
- 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
+316
View File
@@ -0,0 +1,316 @@
#!/usr/bin/python
#
# Copyright (c) 2023, 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
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>[<unit>]). 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()