Compare commits

...

3 Commits
5.1.0 ... main

Author SHA1 Message Date
Felix Fontein
b00fc741e1
Make tests more lenient. (#1252) 2026-03-27 14:29:49 +01:00
spatterlight
9c313bb9d0
Docker image export platform (#1251)
* docker_image_export: Add 'platform' option

docker_image_export: Add 'platform' option

* docker_image_export: Add 'platform' option

* Update changelogs/fragments/1064-docker-image-export-platform.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/docker_image_export.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* docker_image_export: Add 'platform' option

* docker_image_export: Add 'platform' option

* docker_image_export: Add 'platform' option

---------

Co-authored-by: Felix Fontein <felix@fontein.de>
2026-03-27 13:16:04 +01:00
Felix Fontein
91b350019a The next expected release will be 5.2.0. 2026-03-23 20:40:52 +01:00
7 changed files with 89 additions and 12 deletions

View File

@ -0,0 +1,2 @@
minor_changes:
- docker_image_export - adds ``platform`` parameter to allow exporting a specific platform variant from a multi-arch image (https://github.com/ansible-collections/community.docker/issues/1064, https://github.com/ansible-collections/community.docker/pull/1251).

View File

@ -7,7 +7,7 @@
namespace: community namespace: community
name: docker name: docker
version: 5.1.0 version: 5.2.0
readme: README.md readme: README.md
authors: authors:
- Ansible Docker Working Group - Ansible Docker Working Group

View File

@ -31,9 +31,12 @@ attributes:
details: details:
- Whether the module is idempotent depends on the storage API used for images, - Whether the module is idempotent depends on the storage API used for images,
which determines how the image ID is computed. The idempotency check needs which determines how the image ID is computed. The idempotency check needs
that the image ID equals the ID stored in archive's C(manifest.json). the image ID to equal the ID stored in archive's C(manifest.json).
This seemed to have worked fine with the default storage backend up to Docker 28, This seemed to have worked fine with the default storage backend up to Docker 28,
but seems to have changed in Docker 29. but seems to have changed in Docker 29.
- This module is B(not idempotent) when used with multi-architecture images,
regardless of Docker version.
- Full idempotency requires Docker 28 or earlier B(and) a single-architecture image.
options: options:
names: names:
@ -61,6 +64,13 @@ options:
- Export the image even if the C(.tar) file already exists and seems to contain the right image. - Export the image even if the C(.tar) file already exists and seems to contain the right image.
type: bool type: bool
default: false default: false
platform:
description:
- Ask for this specific platform when exporting.
- For example, C(linux/amd64), C(linux/arm64).
- Requires Docker API 1.48 or newer.
type: str
version_added: 5.2.0
requirements: requirements:
- "Docker API >= 1.25" - "Docker API >= 1.25"
@ -98,6 +108,7 @@ images:
sample: [] sample: []
""" """
import json
import traceback import traceback
import typing as t import typing as t
@ -119,6 +130,9 @@ from ansible_collections.community.docker.plugins.module_utils._image_archive im
api_image_id, api_image_id,
load_archived_image_manifest, load_archived_image_manifest,
) )
from ansible_collections.community.docker.plugins.module_utils._platform import (
_Platform,
)
from ansible_collections.community.docker.plugins.module_utils._util import ( from ansible_collections.community.docker.plugins.module_utils._util import (
DockerBaseClass, DockerBaseClass,
is_image_name_id, is_image_name_id,
@ -137,6 +151,7 @@ class ImageExportManager(DockerBaseClass):
self.path = parameters["path"] self.path = parameters["path"]
self.force = parameters["force"] self.force = parameters["force"]
self.tag = parameters["tag"] self.tag = parameters["tag"]
self.platform = parameters["platform"]
if not is_valid_tag(self.tag, allow_empty=True): if not is_valid_tag(self.tag, allow_empty=True):
self.fail(f'"{self.tag}" is not a valid docker tag') self.fail(f'"{self.tag}" is not a valid docker tag')
@ -198,15 +213,31 @@ class ImageExportManager(DockerBaseClass):
except Exception as exc: # pylint: disable=broad-exception-caught except Exception as exc: # pylint: disable=broad-exception-caught
self.fail(f"Error writing image archive {self.path} - {exc}") self.fail(f"Error writing image archive {self.path} - {exc}")
def _platform_param(self) -> str:
platform = _Platform.parse_platform_string(self.platform)
platform_spec: dict[str, str] = {}
if platform.os:
platform_spec["os"] = platform.os
if platform.arch:
platform_spec["architecture"] = platform.arch
if platform.variant:
platform_spec["variant"] = platform.variant
return json.dumps(platform_spec)
def export_images(self) -> None: def export_images(self) -> None:
image_names = [name["joined"] for name in self.names] image_names = [name["joined"] for name in self.names]
image_names_str = ", ".join(image_names) image_names_str = ", ".join(image_names)
if len(image_names) == 1: if len(image_names) == 1:
self.log(f"Getting archive of image {image_names[0]}") self.log(f"Getting archive of image {image_names[0]}")
params: dict[str, t.Any] = {}
if self.platform:
params["platform"] = self._platform_param()
try: try:
chunks = self.client._stream_raw_result( chunks = self.client._stream_raw_result(
self.client._get( self.client._get(
self.client._url("/images/{0}/get", image_names[0]), stream=True self.client._url("/images/{0}/get", image_names[0]),
stream=True,
params=params,
), ),
chunk_size=DEFAULT_DATA_CHUNK_SIZE, chunk_size=DEFAULT_DATA_CHUNK_SIZE,
decode=False, decode=False,
@ -215,12 +246,15 @@ class ImageExportManager(DockerBaseClass):
self.fail(f"Error getting image {image_names[0]} - {exc}") self.fail(f"Error getting image {image_names[0]} - {exc}")
else: else:
self.log(f"Getting archive of images {image_names_str}") self.log(f"Getting archive of images {image_names_str}")
params = {"names": image_names}
if self.platform:
params["platform"] = self._platform_param()
try: try:
chunks = self.client._stream_raw_result( chunks = self.client._stream_raw_result(
self.client._get( self.client._get(
self.client._url("/images/get"), self.client._url("/images/get"),
stream=True, stream=True,
params={"names": image_names}, params=params,
), ),
chunk_size=DEFAULT_DATA_CHUNK_SIZE, chunk_size=DEFAULT_DATA_CHUNK_SIZE,
decode=False, decode=False,
@ -277,11 +311,17 @@ def main() -> None:
"aliases": ["name"], "aliases": ["name"],
}, },
"tag": {"type": "str", "default": "latest"}, "tag": {"type": "str", "default": "latest"},
"platform": {"type": "str"},
}
option_minimal_versions = {
"platform": {"docker_api_version": "1.48"},
} }
client = AnsibleDockerClient( client = AnsibleDockerClient(
argument_spec=argument_spec, argument_spec=argument_spec,
supports_check_mode=True, supports_check_mode=True,
option_minimal_versions=option_minimal_versions,
) )
try: try:

View File

@ -7,6 +7,10 @@
community.docker.current_container_facts: community.docker.current_container_facts:
register: result register: result
- name: Print facts returned by module
ansible.builtin.debug:
var: result.ansible_facts
# The following two tasks are useful if we ever have to debug why this fails. # The following two tasks are useful if we ever have to debug why this fails.
- name: Print all Ansible facts - name: Print all Ansible facts
@ -20,13 +24,10 @@
- /proc/self/cpuset - /proc/self/cpuset
- /proc/1/cgroup - /proc/1/cgroup
- /proc/1/environ - /proc/1/environ
ignore_errors: true # not all of these files always exist
loop_control: loop_control:
loop_var: path loop_var: path
- name: Print facts returned by module
ansible.builtin.debug:
var: result.ansible_facts
- name: Validate results - name: Validate results
ansible.builtin.assert: ansible.builtin.assert:
that: that:

View File

@ -27,8 +27,8 @@
- /proc/self/cgroup - /proc/self/cgroup
- /proc/self/cpuset - /proc/self/cpuset
- /proc/self/mountinfo - /proc/self/mountinfo
ignore_errors: true # not all of these files always exist
register: slurp register: slurp
ignore_errors: true
- name: Print files - name: Print files
ansible.builtin.debug: ansible.builtin.debug:

View File

@ -957,7 +957,7 @@
- when: device_read_bps_1 is failed - when: device_read_bps_1 is failed
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- "'error setting cgroup config for procHooks process' in device_read_bps_1.msg and 'blkio.throttle.read_bps_device: no such device' in device_read_bps_1.msg" - "'error setting cgroup config for procHooks process' in device_read_bps_1.msg and ': no such device' in device_read_bps_1.msg"
#################################################################### ####################################################################
## device_read_iops ################################################ ## device_read_iops ################################################
@ -1040,7 +1040,7 @@
- when: device_read_iops_1 is failed - when: device_read_iops_1 is failed
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- "'error setting cgroup config for procHooks process' in device_read_iops_1.msg and 'blkio.throttle.read_iops_device: no such device' in device_read_iops_1.msg" - "'error setting cgroup config for procHooks process' in device_read_iops_1.msg and ': no such device' in device_read_iops_1.msg"
#################################################################### ####################################################################
## device_write_bps and device_write_iops ########################## ## device_write_bps and device_write_iops ##########################
@ -1110,7 +1110,7 @@
- when: device_write_limit_1 is failed - when: device_write_limit_1 is failed
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- "'error setting cgroup config for procHooks process' in device_write_limit_1.msg and 'blkio.throttle.write_bps_device: no such device' in device_write_limit_1.msg" - "'error setting cgroup config for procHooks process' in device_write_limit_1.msg and ': no such device' in device_write_limit_1.msg"
#################################################################### ####################################################################
## device_requests ################################################# ## device_requests #################################################

View File

@ -0,0 +1,34 @@
---
# 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
- when: docker_api_version is version('1.48', '>=')
block:
- name: Pull image for platform test
community.docker.docker_image_pull:
name: "{{ docker_test_image_hello_world }}"
platform: linux/amd64
- name: Export image with platform (check mode)
community.docker.docker_image_export:
name: "{{ docker_test_image_hello_world }}"
path: "{{ remote_tmp_dir }}/platform-test.tar"
platform: linux/amd64
register: result_check
check_mode: true
- ansible.builtin.assert:
that:
- result_check is changed
- name: Export image with platform
community.docker.docker_image_export:
name: "{{ docker_test_image_hello_world }}"
path: "{{ remote_tmp_dir }}/platform-test.tar"
platform: linux/amd64
register: result
- ansible.builtin.assert:
that:
- result is changed