mirror of
https://github.com/ansible-collections/community.docker.git
synced 2025-04-18 21:04:01 +03:00
Add proper platform handling. (#705)
This commit is contained in:
parent
b3ef5f5196
commit
c4c347c626
@ -1,7 +1,7 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
https://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
@ -176,13 +176,13 @@
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2016 Docker, Inc.
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
2
changelogs/fragments/705-docker_container-platform.yml
Normal file
2
changelogs/fragments/705-docker_container-platform.yml
Normal file
@ -0,0 +1,2 @@
|
||||
minor_changes:
|
||||
- "docker_container - implement better ``platform`` string comparisons to improve idempotency (https://github.com/ansible-collections/community.docker/issues/654, https://github.com/ansible-collections/community.docker/pull/705)."
|
179
plugins/module_utils/_platform.py
Normal file
179
plugins/module_utils/_platform.py
Normal file
@ -0,0 +1,179 @@
|
||||
# This code is part of the Ansible collection community.docker, but is an independent component.
|
||||
# This particular file, and this file only, is based on containerd's platforms Go module
|
||||
# (https://github.com/containerd/containerd/tree/main/platforms)
|
||||
#
|
||||
# Copyright (c) 2023 Felix Fontein <felix@fontein.de>
|
||||
# Copyright The containerd Authors
|
||||
#
|
||||
# It is licensed under the Apache 2.0 license (see LICENSES/Apache-2.0.txt in this collection)
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import re
|
||||
|
||||
|
||||
_VALID_STR = re.compile('^[A-Za-z0-9_-]+$')
|
||||
|
||||
|
||||
def _validate_part(string, part, part_name):
|
||||
if not part:
|
||||
raise ValueError('Invalid platform string "{string}": {part} is empty'.format(string=string, part=part_name))
|
||||
if not _VALID_STR.match(part):
|
||||
raise ValueError('Invalid platform string "{string}": {part} has invalid characters'.format(string=string, part=part_name))
|
||||
return part
|
||||
|
||||
|
||||
# See https://github.com/containerd/containerd/blob/main/platforms/database.go#L32-L38
|
||||
_KNOWN_OS = (
|
||||
"aix", "android", "darwin", "dragonfly", "freebsd", "hurd", "illumos", "ios", "js",
|
||||
"linux", "nacl", "netbsd", "openbsd", "plan9", "solaris", "windows", "zos",
|
||||
)
|
||||
|
||||
# See https://github.com/containerd/containerd/blob/main/platforms/database.go#L54-L60
|
||||
_KNOWN_ARCH = (
|
||||
"386", "amd64", "amd64p32", "arm", "armbe", "arm64", "arm64be", "ppc64", "ppc64le",
|
||||
"loong64", "mips", "mipsle", "mips64", "mips64le", "mips64p32", "mips64p32le",
|
||||
"ppc", "riscv", "riscv64", "s390", "s390x", "sparc", "sparc64", "wasm",
|
||||
)
|
||||
|
||||
|
||||
def _normalize_os(os_str):
|
||||
# See normalizeOS() in https://github.com/containerd/containerd/blob/main/platforms/database.go
|
||||
os_str = os_str.lower()
|
||||
if os_str == 'macos':
|
||||
os_str = 'darwin'
|
||||
return os_str
|
||||
|
||||
|
||||
_NORMALIZE_ARCH = {
|
||||
("i386", None): ("386", ""),
|
||||
("x86_64", "v1"): ("amd64", ""),
|
||||
("x86-64", "v1"): ("amd64", ""),
|
||||
("amd64", "v1"): ("amd64", ""),
|
||||
("x86_64", None): ("amd64", None),
|
||||
("x86-64", None): ("amd64", None),
|
||||
("amd64", None): ("amd64", None),
|
||||
("aarch64", "8"): ("arm64", ""),
|
||||
("arm64", "8"): ("arm64", ""),
|
||||
("aarch64", "v8"): ("arm64", ""),
|
||||
("arm64", "v8"): ("arm64", ""),
|
||||
("aarch64", None): ("arm64", None),
|
||||
("arm64", None): ("arm64", None),
|
||||
("armhf", None): ("arm", "v7"),
|
||||
("armel", None): ("arm", "v6"),
|
||||
("arm", ""): ("arm", "v7"),
|
||||
("arm", "5"): ("arm", "v5"),
|
||||
("arm", "6"): ("arm", "v6"),
|
||||
("arm", "7"): ("arm", "v7"),
|
||||
("arm", "8"): ("arm", "v8"),
|
||||
("arm", None): ("arm", None),
|
||||
}
|
||||
|
||||
|
||||
def _normalize_arch(arch_str, variant_str):
|
||||
# See normalizeArch() in https://github.com/containerd/containerd/blob/main/platforms/database.go
|
||||
arch_str = arch_str.lower()
|
||||
variant_str = variant_str.lower()
|
||||
res = _NORMALIZE_ARCH.get((arch_str, variant_str))
|
||||
if res is None:
|
||||
res = _NORMALIZE_ARCH.get((arch_str, None))
|
||||
if res is None:
|
||||
return arch_str, variant_str
|
||||
if res is not None:
|
||||
arch_str = res[0]
|
||||
if res[1] is not None:
|
||||
variant_str = res[1]
|
||||
return arch_str, variant_str
|
||||
|
||||
|
||||
class _Platform(object):
|
||||
def __init__(self, os=None, arch=None, variant=None):
|
||||
self.os = os
|
||||
self.arch = arch
|
||||
self.variant = variant
|
||||
if variant is not None:
|
||||
if arch is None:
|
||||
raise ValueError('If variant is given, architecture must be given too')
|
||||
if os is None:
|
||||
raise ValueError('If variant is given, os must be given too')
|
||||
|
||||
@classmethod
|
||||
def parse_platform_string(cls, string, daemon_os=None, daemon_arch=None):
|
||||
# See Parse() in https://github.com/containerd/containerd/blob/main/platforms/platforms.go
|
||||
if string is None:
|
||||
return cls()
|
||||
if not string:
|
||||
raise ValueError('Platform string must be non-empty')
|
||||
parts = string.split('/', 2)
|
||||
arch = None
|
||||
variant = None
|
||||
if len(parts) == 1:
|
||||
_validate_part(string, string, 'OS/architecture')
|
||||
# The part is either OS or architecture
|
||||
os = _normalize_os(string)
|
||||
if os in _KNOWN_OS:
|
||||
if daemon_arch is not None:
|
||||
arch, variant = _normalize_arch(daemon_arch, '')
|
||||
return cls(os=os, arch=arch, variant=variant)
|
||||
arch, variant = _normalize_arch(os, '')
|
||||
if arch in _KNOWN_ARCH:
|
||||
return cls(
|
||||
os=_normalize_os(daemon_os) if daemon_os else None,
|
||||
arch=arch or None,
|
||||
variant=variant or None,
|
||||
)
|
||||
raise ValueError('Invalid platform string "{0}": unknown OS or architecture'.format(string))
|
||||
os = _validate_part(string, parts[0], 'OS')
|
||||
if not os:
|
||||
raise ValueError('Invalid platform string "{0}": OS is empty'.format(string))
|
||||
arch = _validate_part(string, parts[1], 'architecture') if len(parts) > 1 else None
|
||||
if arch is not None and not arch:
|
||||
raise ValueError('Invalid platform string "{0}": architecture is empty'.format(string))
|
||||
variant = _validate_part(string, parts[2], 'variant') if len(parts) > 2 else None
|
||||
if variant is not None and not variant:
|
||||
raise ValueError('Invalid platform string "{0}": variant is empty'.format(string))
|
||||
arch, variant = _normalize_arch(arch, variant or '')
|
||||
if len(parts) == 2 and arch == 'arm' and variant == 'v7':
|
||||
variant = None
|
||||
if len(parts) == 3 and arch == 'arm64' and variant == '':
|
||||
variant = 'v8'
|
||||
return cls(os=_normalize_os(os), arch=arch, variant=variant or None)
|
||||
|
||||
def __str__(self):
|
||||
if self.variant:
|
||||
parts = [self.os, self.arch, self.variant]
|
||||
elif self.os:
|
||||
if self.arch:
|
||||
parts = [self.os, self.arch]
|
||||
else:
|
||||
parts = [self.os]
|
||||
elif self.arch is not None:
|
||||
parts = [self.arch]
|
||||
else:
|
||||
parts = []
|
||||
return '/'.join(parts)
|
||||
|
||||
def __repr__(self):
|
||||
return '_Platform(os={os!r}, arch={arch!r}, variant={variant!r})'.format(os=self.os, arch=self.arch, variant=self.variant)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.os == other.os and self.arch == other.arch and self.variant == other.variant
|
||||
|
||||
|
||||
def normalize_platform_string(string, daemon_os=None, daemon_arch=None):
|
||||
return str(_Platform.parse_platform_string(string, daemon_os=daemon_os, daemon_arch=daemon_arch))
|
||||
|
||||
|
||||
def compose_platform_string(os=None, arch=None, variant=None, daemon_os=None, daemon_arch=None):
|
||||
if os is None and daemon_os is not None:
|
||||
os = _normalize_os(daemon_os)
|
||||
if arch is None and daemon_arch is not None:
|
||||
arch, variant = _normalize_arch(daemon_arch, variant or '')
|
||||
variant = variant or None
|
||||
return str(_Platform(os=os, arch=arch, variant=variant or None))
|
||||
|
||||
|
||||
def compare_platform_strings(string1, string2):
|
||||
return _Platform.parse_platform_string(string1) == _Platform.parse_platform_string(string2)
|
@ -24,6 +24,10 @@ from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
omit_none_from_dict,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._platform import (
|
||||
compare_platform_strings,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
|
||||
parse_env_file,
|
||||
)
|
||||
@ -755,6 +759,15 @@ def _preprocess_ports(module, values):
|
||||
return values
|
||||
|
||||
|
||||
def _compare_platform(option, param_value, container_value):
|
||||
if option.comparison == 'ignore':
|
||||
return True
|
||||
try:
|
||||
return compare_platform_strings(param_value, container_value)
|
||||
except ValueError:
|
||||
return param_value == container_value
|
||||
|
||||
|
||||
OPTION_AUTO_REMOVE = (
|
||||
OptionGroup()
|
||||
.add_option('auto_remove', type='bool')
|
||||
@ -1031,7 +1044,7 @@ OPTION_PIDS_LIMIT = (
|
||||
|
||||
OPTION_PLATFORM = (
|
||||
OptionGroup()
|
||||
.add_option('platform', type='str')
|
||||
.add_option('platform', type='str', compare=_compare_platform)
|
||||
)
|
||||
|
||||
OPTION_PRIVILEGED = (
|
||||
|
@ -17,6 +17,11 @@ from ansible_collections.community.docker.plugins.module_utils.common_api import
|
||||
RequestException,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._platform import (
|
||||
compose_platform_string,
|
||||
normalize_platform_string,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.module_container.base import (
|
||||
OPTION_AUTO_REMOVE,
|
||||
OPTION_BLKIO_WEIGHT,
|
||||
@ -1048,16 +1053,48 @@ def _set_values_log(module, data, api_version, options, values):
|
||||
|
||||
|
||||
def _get_values_platform(module, container, api_version, options, image, host_info):
|
||||
if image and (image.get('Os') or image.get('Architecture') or image.get('Variant')):
|
||||
return {
|
||||
'platform': compose_platform_string(
|
||||
os=image.get('Os'),
|
||||
arch=image.get('Architecture'),
|
||||
variant=image.get('Variant'),
|
||||
daemon_os=host_info.get('OSType') if host_info else None,
|
||||
daemon_arch=host_info.get('Architecture') if host_info else None,
|
||||
)
|
||||
}
|
||||
return {
|
||||
'platform': container.get('Platform'),
|
||||
}
|
||||
|
||||
|
||||
def _get_expected_values_platform(module, client, api_version, options, image, values, host_info):
|
||||
expected_values = {}
|
||||
if 'platform' in values:
|
||||
try:
|
||||
expected_values['platform'] = normalize_platform_string(
|
||||
values['platform'],
|
||||
daemon_os=host_info.get('OSType') if host_info else None,
|
||||
daemon_arch=host_info.get('Architecture') if host_info else None,
|
||||
)
|
||||
except ValueError as exc:
|
||||
module.fail_json(msg='Error while parsing platform parameer: %s' % (to_native(exc), ))
|
||||
return expected_values
|
||||
|
||||
|
||||
def _set_values_platform(module, data, api_version, options, values):
|
||||
if 'platform' in values:
|
||||
data['platform'] = values['platform']
|
||||
|
||||
|
||||
def _needs_container_image_platform(values):
|
||||
return 'platform' in values
|
||||
|
||||
|
||||
def _needs_host_info_platform(values):
|
||||
return 'platform' in values
|
||||
|
||||
|
||||
def _get_values_restart(module, container, api_version, options, image, host_info):
|
||||
restart_policy = container['HostConfig'].get('RestartPolicy') or {}
|
||||
return {
|
||||
@ -1306,6 +1343,9 @@ OPTION_PIDS_LIMIT.add_engine('docker_api', DockerAPIEngine.host_config_value('Pi
|
||||
OPTION_PLATFORM.add_engine('docker_api', DockerAPIEngine(
|
||||
get_value=_get_values_platform,
|
||||
set_value=_set_values_platform,
|
||||
get_expected_values=_get_expected_values_platform,
|
||||
needs_container_image=_needs_container_image_platform,
|
||||
needs_host_info=_needs_host_info_platform,
|
||||
min_api_version='1.41',
|
||||
))
|
||||
|
||||
|
@ -733,9 +733,12 @@ options:
|
||||
platform:
|
||||
description:
|
||||
- Platform for the container in the format C(os[/arch[/variant]]).
|
||||
- "Please note that inspecting the container does not always return the exact platform string used to
|
||||
create the container. This can cause idempotency to break for this module. Use the O(comparisons) option
|
||||
with C(platform: ignore) to prevent accidental recreation of the container due to this."
|
||||
- "Note that since community.docker 3.5.0, the module uses both the image's metadata and the Docker
|
||||
daemon's information to normalize platform strings similarly to how Docker itself is doing this.
|
||||
If you notice idempotency problems, L(please create an issue in the community.docker GitHub repository,
|
||||
https://github.com/ansible-collections/community.docker/issues/new?assignees=&labels=&projects=&template=bug_report.md).
|
||||
For older community.docker versions, you can use the O(comparisons) option with C(platform: ignore)
|
||||
to prevent accidental recreation of the container due to this."
|
||||
type: str
|
||||
version_added: 3.0.0
|
||||
privileged:
|
||||
|
@ -3564,17 +3564,38 @@ avoid such warnings, please quote the value.' in (log_options_2.warnings | defau
|
||||
register: platform_1
|
||||
ignore_errors: true
|
||||
|
||||
- name: platform (idempotency)
|
||||
- name: platform (idempotency with full name)
|
||||
# Docker daemon only returns 'linux' as the platform for the container,
|
||||
# so this has to be handled correctly by our additional code
|
||||
docker_container:
|
||||
image: hello-world:latest
|
||||
name: "{{ cname }}"
|
||||
state: present
|
||||
# The container always reports 'linux' as platform instead of 'linux/amd64'...
|
||||
platform: linux
|
||||
platform: linux/amd64
|
||||
debug: true
|
||||
register: platform_2
|
||||
ignore_errors: true
|
||||
|
||||
- name: platform (idempotency with shorter name)
|
||||
docker_container:
|
||||
image: hello-world:latest
|
||||
name: "{{ cname }}"
|
||||
state: present
|
||||
platform: linux
|
||||
debug: true
|
||||
register: platform_3
|
||||
ignore_errors: true
|
||||
|
||||
- name: platform (idempotency with shorter name)
|
||||
docker_container:
|
||||
image: hello-world:latest
|
||||
name: "{{ cname }}"
|
||||
state: present
|
||||
platform: amd64
|
||||
debug: true
|
||||
register: platform_4
|
||||
ignore_errors: true
|
||||
|
||||
- name: platform (changed)
|
||||
docker_container:
|
||||
image: hello-world:latest
|
||||
@ -3587,7 +3608,19 @@ avoid such warnings, please quote the value.' in (log_options_2.warnings | defau
|
||||
comparisons:
|
||||
# Do not restart because of the changed image ID
|
||||
image: ignore
|
||||
register: platform_3
|
||||
register: platform_5
|
||||
ignore_errors: true
|
||||
|
||||
- name: platform (idempotency)
|
||||
docker_container:
|
||||
image: hello-world:latest
|
||||
name: "{{ cname }}"
|
||||
state: present
|
||||
pull: true
|
||||
platform: 386
|
||||
force_kill: true
|
||||
debug: true
|
||||
register: platform_6
|
||||
ignore_errors: true
|
||||
|
||||
- name: cleanup
|
||||
@ -3601,7 +3634,10 @@ avoid such warnings, please quote the value.' in (log_options_2.warnings | defau
|
||||
that:
|
||||
- platform_1 is changed
|
||||
- platform_2 is not changed and platform_2 is not failed
|
||||
- platform_3 is changed
|
||||
- platform_3 is not changed and platform_3 is not failed
|
||||
- platform_4 is not changed and platform_4 is not failed
|
||||
- platform_5 is changed
|
||||
- platform_6 is not changed and platform_6 is not failed
|
||||
when: docker_api_version is version('1.41', '>=')
|
||||
- assert:
|
||||
that:
|
||||
|
Loading…
x
Reference in New Issue
Block a user