Fix expose ranges handling.

This commit is contained in:
Felix Fontein 2025-11-15 16:16:54 +01:00
parent b3723a30c3
commit 5e951c03fa
5 changed files with 146 additions and 34 deletions

View File

@ -1,3 +0,0 @@
bugfixes:
- "docker_container - fix ``pull`` idempotency with Docker 29.0.0 (https://github.com/ansible-collections/community.docker/pull/1192)."
- "docker_container - fix idempotency for IPv6 addresses with Docker 29.0.0 (https://github.com/ansible-collections/community.docker/pull/1192)."

View File

@ -0,0 +1,7 @@
bugfixes:
- "docker_container - fix ``pull`` idempotency with Docker 29.0.0 (https://github.com/ansible-collections/community.docker/pull/1192)."
- "docker_container - fix idempotency for IPv6 addresses with Docker 29.0.0 (https://github.com/ansible-collections/community.docker/pull/1192)."
- "docker_container - fix handling of exposed port ranges. So far, the module used an undocumented feature of Docker that was removed from Docker 29.0.0,
that allowed to pass the range to the deamon and let handle it. Now the module explodes ranges into a list of all contained ports, same as the
Docker CLI does. For backwards compatibility with Docker < 29.0.0, it also explodes ranges returned by the API for existing containers so that
comparison should only indicate a difference if the ranges actually change (https://github.com/ansible-collections/community.docker/pull/1192)."

View File

@ -1016,7 +1016,7 @@ def _preprocess_ports(
else:
port_binds = len(container_ports) * [(ipaddr,)]
else:
return module.fail_json(
module.fail_json(
msg=f'Invalid port description "{port}" - expected 1 to 3 colon-separated parts, but got {p_len}. '
"Maybe you forgot to use square brackets ([...]) around an IPv6 address?"
)
@ -1037,38 +1037,43 @@ def _preprocess_ports(
binds[idx] = bind
values["published_ports"] = binds
exposed = []
exposed: set[tuple[int, str]] = set()
if "exposed_ports" in values:
for port in values["exposed_ports"]:
port = to_text(port, errors="surrogate_or_strict").strip()
protocol = "tcp"
matcher = re.search(r"(/.+$)", port)
if matcher:
protocol = matcher.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=f"Cannot parse port {port!r}: {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=f"Cannot parse port range {port!r}: {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, str) 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], str) 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

View File

@ -1970,10 +1970,20 @@ def _get_values_ports(
config = container["Config"]
# "ExposedPorts": null returns None type & causes AttributeError - PR #5517
expected_exposed: list[str] = []
if config.get("ExposedPorts") is not None:
expected_exposed = [_normalize_port(p) for p in config.get("ExposedPorts", {})]
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(f"{port_no}/{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(f"{port}/{protocol}")
return {
"published_ports": host_config.get("PortBindings"),
@ -2027,17 +2037,14 @@ def _get_expected_values_ports(
]
expected_values["published_ports"] = expected_bound_ports
image_ports = []
image_ports: set[str] = set()
if image:
image_exposed_ports = image["Config"].get("ExposedPorts") or {}
image_ports = [_normalize_port(p) for p in image_exposed_ports]
param_ports = []
image_ports = {_normalize_port(p) for p in image_exposed_ports}
param_ports: set[str] = set()
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 = {f"{p[0]}/{p[1]}" for p in values["ports"]}
result = sorted(image_ports | param_ports)
expected_values["exposed_ports"] = result
if "publish_all_ports" in values:

View File

@ -106,6 +106,101 @@
force_kill: true
register: published_ports_3
- name: published_ports -- port range (same range, but listed explicitly)
community.docker.docker_container:
image: "{{ docker_test_image_alpine }}"
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
exposed_ports:
- "9001"
- "9010"
- "9011"
- "9012"
- "9013"
- "9014"
- "9015"
- "9016"
- "9017"
- "9018"
- "9019"
- "9020"
- "9021"
- "9022"
- "9023"
- "9024"
- "9025"
- "9026"
- "9027"
- "9028"
- "9029"
- "9030"
- "9031"
- "9032"
- "9033"
- "9034"
- "9035"
- "9036"
- "9037"
- "9038"
- "9039"
- "9040"
- "9041"
- "9042"
- "9043"
- "9044"
- "9045"
- "9046"
- "9047"
- "9048"
- "9049"
- "9050"
published_ports:
- "9001:9001"
- "9020:9020"
- "9021:9021"
- "9022:9022"
- "9023:9023"
- "9024:9024"
- "9025:9025"
- "9026:9026"
- "9027:9027"
- "9028:9028"
- "9029:9029"
- "9030:9030"
- "9031:9031"
- "9032:9032"
- "9033:9033"
- "9034:9034"
- "9035:9035"
- "9036:9036"
- "9037:9037"
- "9038:9038"
- "9039:9039"
- "9040:9040"
- "9041:9041"
- "9042:9042"
- "9043:9043"
- "9044:9044"
- "9045:9045"
- "9046:9046"
- "9047:9047"
- "9048:9048"
- "9049:9049"
- "9050:9050"
- "9051:9051"
- "9052:9052"
- "9053:9053"
- "9054:9054"
- "9055:9055"
- "9056:9056"
- "9057:9057"
- "9058:9058"
- "9059:9059"
- "9060:9060"
force_kill: true
register: published_ports_4
- name: cleanup
community.docker.docker_container:
name: "{{ cname }}"
@ -118,6 +213,7 @@
- published_ports_1 is changed
- published_ports_2 is not changed
- published_ports_3 is changed
- published_ports_4 is not changed
####################################################################
## published_ports: one-element container port range ###############