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