Compare commits

..

8 Commits

Author SHA1 Message Date
mjbogusz
ca38a543e8
Merge 260e9cc254 into faa7dee456 2025-12-01 17:27:08 +01:00
Felix Fontein
faa7dee456 The next release will be 5.1.0. 2025-11-29 23:16:22 +01:00
Felix Fontein
908c23a3c3 Release 5.0.3. 2025-11-29 22:35:55 +01:00
Felix Fontein
350f67d971 Prepare 5.0.3. 2025-11-26 07:30:53 +01:00
Felix Fontein
846fc8564b
docker_container: do not send wrong host IP for duplicate ports (#1214)
* DRY.

* Port spec can be a list of port specs.

* Add changelog fragment.

* Add test.
2025-11-26 07:29:30 +01:00
dependabot[bot]
d2947476f7
Bump actions/checkout from 5 to 6 in the ci group (#1211)
Bumps the ci group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 5 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: ci
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 06:20:29 +01:00
Felix Fontein
5d2b4085ec
Remove code that's not used. (#1209) 2025-11-23 09:48:34 +01:00
Felix Fontein
a869184ad4 Shut up pylint due to bugs. 2025-11-23 08:56:42 +01:00
8 changed files with 463 additions and 600 deletions

View File

@ -45,7 +45,7 @@ jobs:
steps: steps:
- name: Check out repository - name: Check out repository
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
persist-credentials: false persist-credentials: false

View File

@ -388,6 +388,8 @@ disable=raw-checker-failed,
unused-argument, unused-argument,
# Cannot remove yet due to inadequacy of rules # Cannot remove yet due to inadequacy of rules
inconsistent-return-statements, # doesn't notice that fail_json() does not return inconsistent-return-statements, # doesn't notice that fail_json() does not return
# Buggy impementation in pylint:
relative-beyond-top-level, # TODO
# Enable the message, report, category or checker with the given id(s). You can # Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option # either give multiple identifier separated by comma (,) or put this option

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,19 @@ Docker Community Collection Release Notes
.. contents:: Topics .. contents:: Topics
v5.0.3
======
Release Summary
---------------
Bugfix release.
Bugfixes
--------
- docker_container - when the same port is mapped more than once for the same protocol without specifying an interface, a bug caused an invalid value to be passed for the interface (https://github.com/ansible-collections/community.docker/issues/1213, https://github.com/ansible-collections/community.docker/pull/1214).
v5.0.2 v5.0.2
====== ======

View File

@ -2318,3 +2318,15 @@ releases:
- 1201-docker_network.yml - 1201-docker_network.yml
- 5.0.2.yml - 5.0.2.yml
release_date: '2025-11-16' release_date: '2025-11-16'
5.0.3:
changes:
bugfixes:
- docker_container - when the same port is mapped more than once for the same
protocol without specifying an interface, a bug caused an invalid value
to be passed for the interface (https://github.com/ansible-collections/community.docker/issues/1213,
https://github.com/ansible-collections/community.docker/pull/1214).
release_summary: Bugfix release.
fragments:
- 1214-docker_container-ports.yml
- 5.0.3.yml
release_date: '2025-11-29'

View File

@ -43,10 +43,8 @@ docker_version: str | None # pylint: disable=invalid-name
try: try:
from docker import __version__ as docker_version from docker import __version__ as docker_version
from docker import auth from docker.errors import APIError, TLSParameterError
from docker.errors import APIError, NotFound, TLSParameterError
from docker.tls import TLSConfig from docker.tls import TLSConfig
from requests.exceptions import SSLError
if LooseVersion(docker_version) >= LooseVersion("3.0.0"): if LooseVersion(docker_version) >= LooseVersion("3.0.0"):
HAS_DOCKER_PY_3 = True # pylint: disable=invalid-name HAS_DOCKER_PY_3 = True # pylint: disable=invalid-name
@ -391,242 +389,6 @@ class AnsibleDockerClientBase(Client):
) )
self.fail(f"SSL Exception: {error}") self.fail(f"SSL Exception: {error}")
def get_container_by_id(self, container_id: str) -> dict[str, t.Any] | None:
try:
self.log(f"Inspecting container Id {container_id}")
result = self.inspect_container(container=container_id)
self.log("Completed container inspection")
return result
except NotFound:
return None
except Exception as exc: # pylint: disable=broad-exception-caught
self.fail(f"Error inspecting container: {exc}")
def get_container(self, name: str | None) -> dict[str, t.Any] | None:
"""
Lookup a container and return the inspection results.
"""
if name is None:
return None
search_name = name
if not name.startswith("/"):
search_name = "/" + name
result = None
try:
for container in self.containers(all=True):
self.log(f"testing container: {container['Names']}")
if (
isinstance(container["Names"], list)
and search_name in container["Names"]
):
result = container
break
if container["Id"].startswith(name):
result = container
break
if container["Id"] == name:
result = container
break
except SSLError as exc:
self._handle_ssl_error(exc)
except Exception as exc: # pylint: disable=broad-exception-caught
self.fail(f"Error retrieving container list: {exc}")
if result is None:
return None
return self.get_container_by_id(result["Id"])
def get_network(
self, name: str | None = None, network_id: str | None = None
) -> dict[str, t.Any] | None:
"""
Lookup a network and return the inspection results.
"""
if name is None and network_id is None:
return None
result = None
if network_id is None:
try:
for network in self.networks():
self.log(f"testing network: {network['Name']}")
if name == network["Name"]:
result = network
break
if network["Id"].startswith(name):
result = network
break
except SSLError as exc:
self._handle_ssl_error(exc)
except Exception as exc: # pylint: disable=broad-exception-caught
self.fail(f"Error retrieving network list: {exc}")
if result is not None:
network_id = result["Id"]
if network_id is not None:
try:
self.log(f"Inspecting network Id {network_id}")
result = self.inspect_network(network_id)
self.log("Completed network inspection")
except NotFound:
return None
except Exception as exc: # pylint: disable=broad-exception-caught
self.fail(f"Error inspecting network: {exc}")
return result
def find_image(self, name: str, tag: str) -> dict[str, t.Any] | None:
"""
Lookup an image (by name and tag) and return the inspection results.
"""
if not name:
return None
self.log(f"Find image {name}:{tag}")
images = self._image_lookup(name, tag)
if not images:
# In API <= 1.20 seeing 'docker.io/<name>' as the name of images pulled from docker hub
registry, repo_name = auth.resolve_repository_name(name)
if registry == "docker.io":
# If docker.io is explicitly there in name, the image
# is not found in some cases (#41509)
self.log(f"Check for docker.io image: {repo_name}")
images = self._image_lookup(repo_name, tag)
if not images and repo_name.startswith("library/"):
# Sometimes library/xxx images are not found
lookup = repo_name[len("library/") :]
self.log(f"Check for docker.io image: {lookup}")
images = self._image_lookup(lookup, tag)
if not images:
# Last case for some Docker versions: if docker.io was not there,
# it can be that the image was not found either
# (https://github.com/ansible/ansible/pull/15586)
lookup = f"{registry}/{repo_name}"
self.log(f"Check for docker.io image: {lookup}")
images = self._image_lookup(lookup, tag)
if not images and "/" not in repo_name:
# This seems to be happening with podman-docker
# (https://github.com/ansible-collections/community.docker/issues/291)
lookup = f"{registry}/library/{repo_name}"
self.log(f"Check for docker.io image: {lookup}")
images = self._image_lookup(lookup, tag)
if len(images) > 1:
self.fail(f"Daemon returned more than one result for {name}:{tag}")
if len(images) == 1:
try:
inspection = self.inspect_image(images[0]["Id"])
except NotFound:
self.log(f"Image {name}:{tag} not found.")
return None
except Exception as exc: # pylint: disable=broad-exception-caught
self.fail(f"Error inspecting image {name}:{tag} - {exc}")
return inspection
self.log(f"Image {name}:{tag} not found.")
return None
def find_image_by_id(
self, image_id: str, accept_missing_image: bool = False
) -> dict[str, t.Any] | None:
"""
Lookup an image (by ID) and return the inspection results.
"""
if not image_id:
return None
self.log(f"Find image {image_id} (by ID)")
try:
inspection = self.inspect_image(image_id)
except NotFound as exc:
if not accept_missing_image:
self.fail(f"Error inspecting image ID {image_id} - {exc}")
self.log(f"Image {image_id} not found.")
return None
except Exception as exc: # pylint: disable=broad-exception-caught
self.fail(f"Error inspecting image ID {image_id} - {exc}")
return inspection
def _image_lookup(self, name: str, tag: str) -> list[dict[str, t.Any]]:
"""
Including a tag in the name parameter sent to the Docker SDK for Python images method
does not work consistently. Instead, get the result set for name and manually check
if the tag exists.
"""
try:
response = self.images(name=name)
except Exception as exc: # pylint: disable=broad-exception-caught
self.fail(f"Error searching for image {name} - {exc}")
images = response
if tag:
lookup = f"{name}:{tag}"
lookup_digest = f"{name}@{tag}"
images = []
for image in response:
tags = image.get("RepoTags")
digests = image.get("RepoDigests")
if (tags and lookup in tags) or (digests and lookup_digest in digests):
images = [image]
break
return images
def pull_image(
self, name: str, tag: str = "latest", image_platform: str | None = None
) -> tuple[dict[str, t.Any] | None, bool]:
"""
Pull an image
"""
kwargs = {
"tag": tag,
"stream": True,
"decode": True,
}
if image_platform is not None:
kwargs["platform"] = image_platform
self.log(f"Pulling image {name}:{tag}")
old_tag = self.find_image(name, tag)
try:
for line in self.pull(name, **kwargs):
self.log(line, pretty_print=True)
if line.get("error"):
if line.get("errorDetail"):
error_detail = line.get("errorDetail")
self.fail(
f"Error pulling {name} - code: {error_detail.get('code')} message: {error_detail.get('message')}"
)
else:
self.fail(f"Error pulling {name} - {line.get('error')}")
except Exception as exc: # pylint: disable=broad-exception-caught
self.fail(f"Error pulling image {name}:{tag} - {exc}")
new_tag = self.find_image(name, tag)
return new_tag, old_tag == new_tag
def inspect_distribution(self, image: str, **kwargs: t.Any) -> dict[str, t.Any]:
"""
Get image digest by directly calling the Docker API when running Docker SDK < 4.0.0
since prior versions did not support accessing private repositories.
"""
if self.docker_py_version < LooseVersion("4.0.0"):
registry = auth.resolve_repository_name(image)[0]
header = auth.get_config_header(self, registry)
if header:
return self._result(
self._get(
self._url("/distribution/{0}/json", image),
headers={"X-Registry-Auth": header},
),
json=True,
)
return super().inspect_distribution(image, **kwargs)
class AnsibleDockerClient(AnsibleDockerClientBase): class AnsibleDockerClient(AnsibleDockerClientBase):
def __init__( def __init__(

View File

@ -29,6 +29,7 @@ from ansible_collections.community.docker.plugins.module_utils._common_api impor
RequestException, RequestException,
) )
from ansible_collections.community.docker.plugins.module_utils._module_container.base import ( from ansible_collections.community.docker.plugins.module_utils._module_container.base import (
_DEFAULT_IP_REPLACEMENT_STRING,
OPTION_AUTO_REMOVE, OPTION_AUTO_REMOVE,
OPTION_BLKIO_WEIGHT, OPTION_BLKIO_WEIGHT,
OPTION_CAP_DROP, OPTION_CAP_DROP,
@ -127,11 +128,6 @@ if t.TYPE_CHECKING:
Sentry = object Sentry = object
_DEFAULT_IP_REPLACEMENT_STRING = (
"[[DEFAULT_IP:iewahhaeB4Sae6Aen8IeShairoh4zeph7xaekoh8Geingunaesaeweiy3ooleiwi]]"
)
_SENTRY: Sentry = object() _SENTRY: Sentry = object()
@ -2093,16 +2089,26 @@ def _preprocess_value_ports(
if "published_ports" not in values: if "published_ports" not in values:
return values return values
found = False found = False
for port_spec in values["published_ports"].values(): for port_specs in values["published_ports"].values():
if port_spec[0] == _DEFAULT_IP_REPLACEMENT_STRING: if not isinstance(port_specs, list):
found = True port_specs = [port_specs]
break for port_spec in port_specs:
if port_spec[0] == _DEFAULT_IP_REPLACEMENT_STRING:
found = True
break
if not found: if not found:
return values return values
default_ip = _get_default_host_ip(module, client) default_ip = _get_default_host_ip(module, client)
for port, port_spec in values["published_ports"].items(): for port, port_specs in values["published_ports"].items():
if port_spec[0] == _DEFAULT_IP_REPLACEMENT_STRING: if isinstance(port_specs, list):
values["published_ports"][port] = tuple([default_ip] + list(port_spec[1:])) for index, port_spec in enumerate(port_specs):
if port_spec[0] == _DEFAULT_IP_REPLACEMENT_STRING:
port_specs[index] = tuple([default_ip] + list(port_spec[1:]))
else:
if port_specs[0] == _DEFAULT_IP_REPLACEMENT_STRING:
values["published_ports"][port] = tuple(
[default_ip] + list(port_specs[1:])
)
return values return values

View File

@ -277,6 +277,58 @@
- published_ports_2 is not changed - published_ports_2 is not changed
- published_ports_3 is changed - published_ports_3 is changed
####################################################################
## published_ports: duplicate ports ################################
####################################################################
- name: published_ports -- duplicate ports
community.docker.docker_container:
image: "{{ docker_test_image_alpine }}"
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
published_ports:
- 8000:80
- 10000:80
register: published_ports_1
- name: published_ports -- duplicate ports (idempotency)
community.docker.docker_container:
image: "{{ docker_test_image_alpine }}"
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
published_ports:
- 8000:80
- 10000:80
force_kill: true
register: published_ports_2
- name: published_ports -- duplicate ports (idempotency w/ protocol)
community.docker.docker_container:
image: "{{ docker_test_image_alpine }}"
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
published_ports:
- 8000:80/tcp
- 10000:80/tcp
force_kill: true
register: published_ports_3
- name: cleanup
community.docker.docker_container:
name: "{{ cname }}"
state: absent
force_kill: true
diff: false
- ansible.builtin.assert:
that:
- published_ports_1 is changed
- published_ports_2 is not changed
- published_ports_3 is not changed
#################################################################### ####################################################################
## published_ports: IPv6 addresses ################################# ## published_ports: IPv6 addresses #################################
#################################################################### ####################################################################