Fix IP subnet and address idempotency. (#1201)

This commit is contained in:
Felix Fontein 2025-11-16 10:47:35 +01:00 committed by GitHub
parent d207643e0c
commit 3da2799e03
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 54 additions and 3 deletions

View File

@ -0,0 +1,2 @@
bugfixes:
- "docker_network - fix idempotency for IPv6 addresses and networks with Docker 29.0.0 (https://github.com/ansible-collections/community.docker/pull/1201)."

View File

@ -528,3 +528,25 @@ def normalize_ip_address(ip_address: str | None) -> str | None:
except ValueError: except ValueError:
# Fallback for invalid addresses: simply return the input # Fallback for invalid addresses: simply return the input
return ip_address return ip_address
@t.overload
def normalize_ip_network(network: str) -> str: ...
@t.overload
def normalize_ip_network(network: str | None) -> str | None: ...
def normalize_ip_network(network: str | None) -> str | None:
"""
Given a network in CIDR notation as a string, normalize it so that it can be
used to compare networks as strings.
"""
if network is None:
return None
try:
return ipaddress.ip_network(network).compressed
except ValueError:
# Fallback for invalid networks: simply return the input
return network

View File

@ -299,6 +299,8 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
DifferenceTracker, DifferenceTracker,
DockerBaseClass, DockerBaseClass,
clean_dict_booleans_for_docker_api, clean_dict_booleans_for_docker_api,
normalize_ip_address,
normalize_ip_network,
sanitize_labels, sanitize_labels,
) )
@ -360,6 +362,7 @@ def validate_cidr(cidr: str) -> t.Literal["ipv4", "ipv6"]:
:rtype: str :rtype: str
:raises ValueError: If ``cidr`` is not a valid CIDR :raises ValueError: If ``cidr`` is not a valid CIDR
""" """
# TODO: Use ipaddress for this instead of rolling your own...
if CIDR_IPV4.match(cidr): if CIDR_IPV4.match(cidr):
return "ipv4" return "ipv4"
if CIDR_IPV6.match(cidr): if CIDR_IPV6.match(cidr):
@ -389,6 +392,19 @@ def dicts_are_essentially_equal(a: dict[str, t.Any], b: dict[str, t.Any]) -> boo
return True return True
def normalize_ipam_values(ipam_config: dict[str, t.Any]) -> dict[str, t.Any]:
result = {}
for key, value in ipam_config.items():
if key in ("subnet", "iprange"):
value = normalize_ip_network(value)
elif key in ("gateway",):
value = normalize_ip_address(value)
elif key in ("aux_addresses",) and value is not None:
value = {k: normalize_ip_address(v) for k, v in value.items()}
result[key] = value
return result
class DockerNetworkManager: class DockerNetworkManager:
def __init__(self, client: AnsibleDockerClient) -> None: def __init__(self, client: AnsibleDockerClient) -> None:
self.client = client self.client = client
@ -513,24 +529,35 @@ class DockerNetworkManager:
else: else:
# Put network's IPAM config into the same format as module's IPAM config # Put network's IPAM config into the same format as module's IPAM config
net_ipam_configs = [] net_ipam_configs = []
net_ipam_configs_normalized = []
for net_ipam_config in net["IPAM"]["Config"]: for net_ipam_config in net["IPAM"]["Config"]:
config = {} config = {}
for k, v in net_ipam_config.items(): for k, v in net_ipam_config.items():
config[normalize_ipam_config_key(k)] = v config[normalize_ipam_config_key(k)] = v
net_ipam_configs.append(config) net_ipam_configs.append(config)
net_ipam_configs_normalized.append(normalize_ipam_values(config))
# Compare lists of dicts as sets of dicts # Compare lists of dicts as sets of dicts
for idx, ipam_config in enumerate(self.parameters.ipam_config): for idx, ipam_config in enumerate(self.parameters.ipam_config):
ipam_config_normalized = normalize_ipam_values(ipam_config)
net_config = {} net_config = {}
for net_ipam_config in net_ipam_configs: net_config_normalized = {}
if dicts_are_essentially_equal(ipam_config, net_ipam_config): for net_ipam_config, net_ipam_config_normalized in zip(
net_ipam_configs, net_ipam_configs_normalized
):
if dicts_are_essentially_equal(
ipam_config_normalized, net_ipam_config_normalized
):
net_config = net_ipam_config net_config = net_ipam_config
net_config_normalized = net_ipam_config_normalized
break break
for key, value in ipam_config.items(): for key, value in ipam_config.items():
if value is None: if value is None:
# due to recursive argument_spec, all keys are always present # due to recursive argument_spec, all keys are always present
# (but have default value None if not specified) # (but have default value None if not specified)
continue continue
if value != net_config.get(key): if ipam_config_normalized[key] != net_config_normalized.get(
key
):
differences.add( differences.add(
f"ipam_config[{idx}].{key}", f"ipam_config[{idx}].{key}",
parameter=value, parameter=value,