diff --git a/changelogs/fragments/1201-docker_network.yml b/changelogs/fragments/1201-docker_network.yml new file mode 100644 index 00000000..de54be3d --- /dev/null +++ b/changelogs/fragments/1201-docker_network.yml @@ -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)." diff --git a/plugins/module_utils/util.py b/plugins/module_utils/util.py index 56b41dc0..140eeca4 100644 --- a/plugins/module_utils/util.py +++ b/plugins/module_utils/util.py @@ -447,3 +447,21 @@ def normalize_ip_address(ip_address): # If we don't have ipaddress, simply give up... # This mainly affects Python 2.7. return ip_address + + +def normalize_ip_network(network): + """ + 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 + if HAS_IPADDRESS: + try: + return ipaddress.ip_network(network).compressed + except ValueError: + # Fallback for invalid networks: simply return the input + return network + # If we don't have ipaddress, simply give up... + # This mainly affects Python 2.7. + return ip_address diff --git a/plugins/modules/docker_network.py b/plugins/modules/docker_network.py index db81a281..9c64208f 100644 --- a/plugins/modules/docker_network.py +++ b/plugins/modules/docker_network.py @@ -296,6 +296,8 @@ from ansible_collections.community.docker.plugins.module_utils.util import ( DockerBaseClass, DifferenceTracker, clean_dict_booleans_for_docker_api, + normalize_ip_address, + normalize_ip_network, sanitize_labels, ) from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException @@ -352,6 +354,7 @@ def validate_cidr(cidr): :rtype: str :raises ValueError: If ``cidr`` is not a valid CIDR """ + # TODO: Use ipaddress for this instead of rolling your own... if CIDR_IPV4.match(cidr): return 'ipv4' elif CIDR_IPV6.match(cidr): @@ -383,6 +386,19 @@ def dicts_are_essentially_equal(a, b): return True +def normalize_ipam_values(ipam_config): + 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(object): def __init__(self, client): @@ -480,24 +496,35 @@ class DockerNetworkManager(object): else: # Put network's IPAM config into the same format as module's IPAM config net_ipam_configs = [] + net_ipam_configs_normalized = [] for net_ipam_config in net['IPAM']['Config']: config = dict() for k, v in net_ipam_config.items(): config[normalize_ipam_config_key(k)] = v net_ipam_configs.append(config) + net_ipam_configs_normalized.append(normalize_ipam_values(config)) # Compare lists of dicts as sets of dicts for idx, ipam_config in enumerate(self.parameters.ipam_config): - net_config = dict() - for net_ipam_config in net_ipam_configs: - if dicts_are_essentially_equal(ipam_config, net_ipam_config): + ipam_config_normalized = normalize_ipam_values(ipam_config) + net_config = {} + net_config_normalized = {} + 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_normalized = net_ipam_config_normalized break for key, value in ipam_config.items(): if value is None: # due to recursive argument_spec, all keys are always present # (but have default value None if not specified) continue - if value != net_config.get(key): + if ipam_config_normalized[key] != net_config_normalized.get( + key + ): differences.add('ipam_config[%s].%s' % (idx, key), parameter=value, active=net_config.get(key))