docker_image(_pull), docker_container: fix compatibility with Docker 29.0.0 (#1192) (#1198)

* Add debug flag to failing task.

* Add more debug output.

* Fix pull idempotency.

* Revert "Add more debug output."

This reverts commit 64020149bf.

* Fix casing.

* Remove unreliable test.

* Add 'debug: true' to all tasks.

* Reformat.

* Fix idempotency problem for IPv6 addresses.

* Fix expose ranges handling.

* Update changelog fragment to also mention other affected modules.

(cherry picked from commit 90c4b4c543)
This commit is contained in:
Felix Fontein
2025-11-15 17:47:34 +01:00
committed by GitHub
parent b58763e2e6
commit a80e6bf7ec
11 changed files with 198 additions and 43 deletions
+12 -3
View File
@@ -420,12 +420,21 @@ class AnsibleDockerClientBase(Client):
except Exception as exc:
self.fail("Error inspecting image ID %s - %s" % (image_id, str(exc)))
@staticmethod
def _compare_images(img1, img2):
if img1 is None or img2 is None:
return img1 == img2
filter_keys = {"Metadata"}
img1_filtered = {k: v for k, v in img1.items() if k not in filter_keys}
img2_filtered = {k: v for k, v in img2.items() if k not in filter_keys}
return img1_filtered == img2_filtered
def pull_image(self, name, tag="latest", platform=None):
'''
Pull an image
'''
self.log("Pulling image %s:%s" % (name, tag))
old_tag = self.find_image(name, tag)
old_image = self.find_image(name, tag)
try:
repository, image_tag = parse_repository_tag(name)
registry, repo_name = auth.resolve_repository_name(repository)
@@ -459,9 +468,9 @@ class AnsibleDockerClientBase(Client):
except Exception as exc:
self.fail("Error pulling image %s:%s - %s" % (name, tag, str(exc)))
new_tag = self.find_image(name, tag)
new_image = self.find_image(name, tag)
return new_tag, old_tag == new_tag
return new_image, self._compare_images(old_image, new_image)
class AnsibleDockerClient(AnsibleDockerClientBase):
+24 -19
View File
@@ -766,38 +766,43 @@ def _preprocess_ports(module, values):
binds[idx] = bind
values['published_ports'] = binds
exposed = []
exposed = set()
if 'exposed_ports' in values:
for port in values['exposed_ports']:
port = to_text(port, errors='surrogate_or_strict').strip()
protocol = 'tcp'
match = re.search(r'(/.+$)', port)
if match:
protocol = match.group(1).replace('/', '')
port = re.sub(r'/.+$', '', port)
exposed.append((port, protocol))
parts = port.split("/", maxsplit=1)
if len(parts) == 2:
port, protocol = parts
parts = port.split("-", maxsplit=1)
if len(parts) < 2:
try:
exposed.add((int(port), protocol))
except ValueError as e:
module.fail_json(msg="Cannot parse port {port!r}: {e}".format(port=port, e=e))
else:
try:
start_port = int(parts[0])
end_port = int(parts[1])
if start_port > end_port:
raise ValueError(
"start port must be smaller or equal to end port."
)
except ValueError as e:
module.fail_json(msg="Cannot parse port range {port!r}: {e}".format(port=port, e=e))
for port in range(start_port, end_port + 1):
exposed.add((port, protocol))
if 'published_ports' in values:
# Any published port should also be exposed
for publish_port in values['published_ports']:
match = False
if isinstance(publish_port, string_types) and '/' in publish_port:
port, protocol = publish_port.split('/')
port = int(port)
else:
protocol = 'tcp'
port = int(publish_port)
for exposed_port in exposed:
if exposed_port[1] != protocol:
continue
if isinstance(exposed_port[0], string_types) and '-' in exposed_port[0]:
start_port, end_port = exposed_port[0].split('-')
if int(start_port) <= port <= int(end_port):
match = True
elif exposed_port[0] == port:
match = True
if not match:
exposed.append((port, protocol))
values['ports'] = exposed
exposed.add((port, protocol))
values['ports'] = sorted(exposed)
return values
@@ -1185,10 +1185,20 @@ def _get_values_ports(module, container, api_version, options, image, host_info)
config = container['Config']
# "ExposedPorts": null returns None type & causes AttributeError - PR #5517
expected_exposed = []
if config.get('ExposedPorts') is not None:
expected_exposed = [_normalize_port(p) for p in config.get('ExposedPorts', dict()).keys()]
else:
expected_exposed = []
for port_and_protocol in config.get("ExposedPorts", {}):
port, protocol = _normalize_port(port_and_protocol).rsplit("/")
try:
start, end = port.split("-", 1)
start_port = int(start)
end_port = int(end)
for port_no in range(start_port, end_port + 1):
expected_exposed.append("{port_no}/{protocol}".format(port_no=port_no, protocol=protocol))
continue
except ValueError:
# Either it is not a range, or a broken one - in both cases, simply add the original form
expected_exposed.append("{port}/{protocol}".format(port=port, protocol=protocol))
return {
'published_ports': host_config.get('PortBindings'),
@@ -1224,8 +1234,8 @@ def _get_expected_values_ports(module, client, api_version, options, image, valu
image_ports = [_normalize_port(p) for p in image_exposed_ports]
param_ports = []
if 'ports' in values:
param_ports = [to_text(p[0], errors='surrogate_or_strict') + '/' + p[1] for p in values['ports']]
result = list(set(image_ports + param_ports))
param_ports = ["{0}/{1}".format(to_native(p[0]), p[1]) for p in values['ports']]
result = sorted(set(image_ports + param_ports))
expected_values['exposed_ports'] = result
if 'publish_all_ports' in values:
@@ -16,6 +16,7 @@ from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass,
compare_generic,
is_image_name_id,
normalize_ip_address,
sanitize_result,
)
@@ -611,9 +612,9 @@ class ContainerManager(DockerBaseClass):
else:
diff = False
network_info_ipam = network_info.get('IPAMConfig') or {}
if network.get('ipv4_address') and network['ipv4_address'] != network_info_ipam.get('IPv4Address'):
if network.get('ipv4_address') and normalize_ip_address(network['ipv4_address']) != normalize_ip_address(network_info_ipam.get('IPv4Address')):
diff = True
if network.get('ipv6_address') and network['ipv6_address'] != network_info_ipam.get('IPv6Address'):
if network.get('ipv6_address') and normalize_ip_address(network['ipv6_address']) != normalize_ip_address(network_info_ipam.get('IPv6Address')):
diff = True
if network.get('aliases'):
if not compare_generic(network['aliases'], network_info.get('Aliases'), 'allow_more_present', 'set'):
+24
View File
@@ -15,6 +15,12 @@ from ansible.module_utils.common.collections import is_sequence
from ansible_collections.community.docker.plugins.module_utils._six import string_types, urlparse
from ansible.module_utils.common.text.converters import to_text
try:
import ipaddress
HAS_IPADDRESS = True
except ImportError:
HAS_IPADDRESS = False
DEFAULT_DOCKER_HOST = 'unix:///var/run/docker.sock'
DEFAULT_TLS = False
@@ -423,3 +429,21 @@ def omit_none_from_dict(d):
Return a copy of the dictionary with all keys with value None omitted.
"""
return dict((k, v) for (k, v) in d.items() if v is not None)
def normalize_ip_address(ip_address):
"""
Given an IP address as a string, normalize it so that it can be
used to compare IP addresses as strings.
"""
if ip_address is None:
return None
if HAS_IPADDRESS:
try:
return ipaddress.ip_address(ip_address).compressed
except ValueError:
# Fallback for invalid addresses: simply return the input
return ip_address
# If we don't have ipaddress, simply give up...
# This mainly affects Python 2.7.
return ip_address
+4
View File
@@ -23,6 +23,10 @@ notes:
- If the module needs to recreate the container, it will only use the options provided to the module to create the new container
(except O(image)). Therefore, always specify B(all) options relevant to the container.
- When O(restart) is set to V(true), the module will only restart the container if no config changes are detected.
- When specifying IPv6 addresses for networks, Docker since version 29 no longer returns the orignal address used
when adding a container to a network, but normalizes them. The module will try to normalize IP addresses for comparison,
but it uses the C(ipaddress) module from the Python 3 standard library for that. When using the module with Python 2,
please install the L(ipaddress backport for Python 2.x, https://pypi.org/project/ipaddress/).
extends_documentation_fragment:
- community.docker.docker.api_documentation
- community.docker.attributes