mirror of
https://github.com/ansible-collections/community.docker.git
synced 2025-12-17 04:18:42 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
947ec9a442 | ||
|
|
25e7ba222e | ||
|
|
6ab8cc0d82 | ||
|
|
159df0ab91 | ||
|
|
174c0c8058 | ||
|
|
2efcd6b2ec | ||
|
|
faa7dee456 | ||
|
|
908c23a3c3 | ||
|
|
350f67d971 | ||
|
|
846fc8564b | ||
|
|
d2947476f7 | ||
|
|
5d2b4085ec | ||
|
|
a869184ad4 | ||
|
|
a985e05482 |
2
.github/workflows/docker-images.yml
vendored
2
.github/workflows/docker-images.yml
vendored
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
713
CHANGELOG.md
713
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,33 @@ Docker Community Collection Release Notes
|
|||||||
|
|
||||||
.. contents:: Topics
|
.. contents:: Topics
|
||||||
|
|
||||||
|
v5.0.4
|
||||||
|
======
|
||||||
|
|
||||||
|
Release Summary
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Bugfix release.
|
||||||
|
|
||||||
|
Bugfixes
|
||||||
|
--------
|
||||||
|
|
||||||
|
- CLI-based modules - when parsing JSON output fails, also provide standard error output. Also provide information on the command and its result in machine-readable way (https://github.com/ansible-collections/community.docker/issues/1216, https://github.com/ansible-collections/community.docker/pull/1221).
|
||||||
|
- docker_compose_v2, docker_compose_v2_pull - adjust parsing from image pull events to changes in Docker Compose 5.0.0 (https://github.com/ansible-collections/community.docker/pull/1219).
|
||||||
|
|
||||||
|
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
|
||||||
======
|
======
|
||||||
|
|
||||||
|
|||||||
@ -2318,3 +2318,30 @@ 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'
|
||||||
|
5.0.4:
|
||||||
|
changes:
|
||||||
|
bugfixes:
|
||||||
|
- CLI-based modules - when parsing JSON output fails, also provide standard
|
||||||
|
error output. Also provide information on the command and its result in
|
||||||
|
machine-readable way (https://github.com/ansible-collections/community.docker/issues/1216,
|
||||||
|
https://github.com/ansible-collections/community.docker/pull/1221).
|
||||||
|
- docker_compose_v2, docker_compose_v2_pull - adjust parsing from image pull
|
||||||
|
events to changes in Docker Compose 5.0.0 (https://github.com/ansible-collections/community.docker/pull/1219).
|
||||||
|
release_summary: Bugfix release.
|
||||||
|
fragments:
|
||||||
|
- 1219-compose-v2-pull.yml
|
||||||
|
- 1221-cli-json-errors.yml
|
||||||
|
- 5.0.4.yml
|
||||||
|
release_date: '2025-12-06'
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
namespace: community
|
namespace: community
|
||||||
name: docker
|
name: docker
|
||||||
version: 5.0.2
|
version: 5.1.0
|
||||||
readme: README.md
|
readme: README.md
|
||||||
authors:
|
authors:
|
||||||
- Ansible Docker Working Group
|
- Ansible Docker Working Group
|
||||||
|
|||||||
@ -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__(
|
||||||
|
|||||||
@ -197,7 +197,11 @@ class AnsibleDockerClientBase:
|
|||||||
data = json.loads(stdout)
|
data = json.loads(stdout)
|
||||||
except Exception as exc: # pylint: disable=broad-exception-caught
|
except Exception as exc: # pylint: disable=broad-exception-caught
|
||||||
self.fail(
|
self.fail(
|
||||||
f"Error while parsing JSON output of {self._compose_cmd_str(args)}: {exc}\nJSON output: {to_text(stdout)}"
|
f"Error while parsing JSON output of {self._compose_cmd_str(args)}: {exc}\nJSON output: {to_text(stdout)}\n\nError output:\n{to_text(stderr)}",
|
||||||
|
cmd=self._compose_cmd_str(args),
|
||||||
|
rc=rc,
|
||||||
|
stdout=stdout,
|
||||||
|
stderr=stderr,
|
||||||
)
|
)
|
||||||
return rc, data, stderr
|
return rc, data, stderr
|
||||||
|
|
||||||
@ -223,7 +227,11 @@ class AnsibleDockerClientBase:
|
|||||||
result.append(json.loads(line))
|
result.append(json.loads(line))
|
||||||
except Exception as exc: # pylint: disable=broad-exception-caught
|
except Exception as exc: # pylint: disable=broad-exception-caught
|
||||||
self.fail(
|
self.fail(
|
||||||
f"Error while parsing JSON output of {self._compose_cmd_str(args)}: {exc}\nJSON output: {to_text(stdout)}"
|
f"Error while parsing JSON output of {self._compose_cmd_str(args)}: {exc}\nJSON output: {to_text(stdout)}\n\nError output:\n{to_text(stderr)}",
|
||||||
|
cmd=self._compose_cmd_str(args),
|
||||||
|
rc=rc,
|
||||||
|
stdout=stdout,
|
||||||
|
stderr=stderr,
|
||||||
)
|
)
|
||||||
return rc, result, stderr
|
return rc, result, stderr
|
||||||
|
|
||||||
|
|||||||
@ -132,7 +132,7 @@ DOCKER_PULL_PROGRESS_DONE = frozenset(
|
|||||||
"Pull complete",
|
"Pull complete",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
DOCKER_PULL_PROGRESS_WORKING = frozenset(
|
DOCKER_PULL_PROGRESS_WORKING_OLD = frozenset(
|
||||||
(
|
(
|
||||||
"Pulling fs layer",
|
"Pulling fs layer",
|
||||||
"Waiting",
|
"Waiting",
|
||||||
@ -141,6 +141,7 @@ DOCKER_PULL_PROGRESS_WORKING = frozenset(
|
|||||||
"Extracting",
|
"Extracting",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
DOCKER_PULL_PROGRESS_WORKING = frozenset(DOCKER_PULL_PROGRESS_WORKING_OLD | {"Working"})
|
||||||
|
|
||||||
|
|
||||||
class ResourceType:
|
class ResourceType:
|
||||||
@ -191,7 +192,7 @@ _RE_PULL_EVENT = re.compile(
|
|||||||
)
|
)
|
||||||
|
|
||||||
_DOCKER_PULL_PROGRESS_WD = sorted(
|
_DOCKER_PULL_PROGRESS_WD = sorted(
|
||||||
DOCKER_PULL_PROGRESS_DONE | DOCKER_PULL_PROGRESS_WORKING
|
DOCKER_PULL_PROGRESS_DONE | DOCKER_PULL_PROGRESS_WORKING_OLD
|
||||||
)
|
)
|
||||||
|
|
||||||
_RE_PULL_PROGRESS = re.compile(
|
_RE_PULL_PROGRESS = re.compile(
|
||||||
@ -494,7 +495,17 @@ def parse_json_events(
|
|||||||
# {"dry-run":true,"id":"ansible-docker-test-dc713f1f-container ==> ==>","text":"naming to ansible-docker-test-dc713f1f-image"}
|
# {"dry-run":true,"id":"ansible-docker-test-dc713f1f-container ==> ==>","text":"naming to ansible-docker-test-dc713f1f-image"}
|
||||||
# (The longer form happens since Docker Compose 2.39.0)
|
# (The longer form happens since Docker Compose 2.39.0)
|
||||||
continue
|
continue
|
||||||
if isinstance(resource_id, str) and " " in resource_id:
|
if (
|
||||||
|
status in ("Working", "Done")
|
||||||
|
and isinstance(line_data.get("parent_id"), str)
|
||||||
|
and line_data["parent_id"].startswith("Image ")
|
||||||
|
):
|
||||||
|
# Compose 5.0.0+:
|
||||||
|
# {"id":"63a26ae4e8a8","parent_id":"Image ghcr.io/ansible-collections/simple-1:tag","status":"Working"}
|
||||||
|
# {"id":"63a26ae4e8a8","parent_id":"Image ghcr.io/ansible-collections/simple-1:tag","status":"Done","percent":100}
|
||||||
|
resource_type = ResourceType.IMAGE_LAYER
|
||||||
|
resource_id = line_data["parent_id"][len("Image ") :]
|
||||||
|
elif isinstance(resource_id, str) and " " in resource_id:
|
||||||
resource_type_str, resource_id = resource_id.split(" ", 1)
|
resource_type_str, resource_id = resource_id.split(" ", 1)
|
||||||
try:
|
try:
|
||||||
resource_type = ResourceType.from_docker_compose_event(
|
resource_type = ResourceType.from_docker_compose_event(
|
||||||
@ -513,7 +524,7 @@ def parse_json_events(
|
|||||||
status, text = text, status
|
status, text = text, status
|
||||||
elif (
|
elif (
|
||||||
text in DOCKER_PULL_PROGRESS_DONE
|
text in DOCKER_PULL_PROGRESS_DONE
|
||||||
or line_data.get("text") in DOCKER_PULL_PROGRESS_WORKING
|
or line_data.get("text") in DOCKER_PULL_PROGRESS_WORKING_OLD
|
||||||
):
|
):
|
||||||
resource_type = ResourceType.IMAGE_LAYER
|
resource_type = ResourceType.IMAGE_LAYER
|
||||||
status, text = text, status
|
status, text = text, status
|
||||||
|
|||||||
@ -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 not isinstance(port_specs, list):
|
||||||
|
port_specs = [port_specs]
|
||||||
|
for port_spec in port_specs:
|
||||||
if port_spec[0] == _DEFAULT_IP_REPLACEMENT_STRING:
|
if port_spec[0] == _DEFAULT_IP_REPLACEMENT_STRING:
|
||||||
found = True
|
found = True
|
||||||
break
|
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 isinstance(port_specs, list):
|
||||||
|
for index, port_spec in enumerate(port_specs):
|
||||||
if port_spec[0] == _DEFAULT_IP_REPLACEMENT_STRING:
|
if port_spec[0] == _DEFAULT_IP_REPLACEMENT_STRING:
|
||||||
values["published_ports"][port] = tuple([default_ip] + list(port_spec[1:]))
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -81,16 +81,19 @@
|
|||||||
- ansible.builtin.assert:
|
- ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- present_1_check is failed or present_1_check is changed
|
- present_1_check is failed or present_1_check is changed
|
||||||
- present_1_check is changed or present_1_check.msg.startswith('General error:')
|
- present_1_check is changed or 'General error:' in present_1_check.msg
|
||||||
- present_1_check.warnings | default([]) | select('regex', ' please report this at ') | length == 0
|
- present_1_check.warnings | default([]) | select('regex', ' please report this at ') | length == 0
|
||||||
- present_1 is failed
|
- present_1 is failed
|
||||||
- present_1.msg.startswith('General error:')
|
- >-
|
||||||
|
'General error:' in present_1.msg
|
||||||
- present_1.warnings | default([]) | select('regex', ' please report this at ') | length == 0
|
- present_1.warnings | default([]) | select('regex', ' please report this at ') | length == 0
|
||||||
- present_2_check is failed
|
- present_2_check is failed
|
||||||
- present_2_check.msg.startswith('Error when processing ' ~ cname ~ ':')
|
- present_2_check.msg.startswith('Error when processing ' ~ cname ~ ':') or
|
||||||
|
present_2_check.msg.startswith('Error when processing image ' ~ non_existing_image ~ ':')
|
||||||
- present_2_check.warnings | default([]) | select('regex', ' please report this at ') | length == 0
|
- present_2_check.warnings | default([]) | select('regex', ' please report this at ') | length == 0
|
||||||
- present_2 is failed
|
- present_2 is failed
|
||||||
- present_2.msg.startswith('Error when processing ' ~ cname ~ ':')
|
- present_2.msg.startswith('Error when processing ' ~ cname ~ ':') or
|
||||||
|
present_2.msg.startswith('Error when processing image ' ~ non_existing_image ~ ':')
|
||||||
- present_2.warnings | default([]) | select('regex', ' please report this at ') | length == 0
|
- present_2.warnings | default([]) | select('regex', ' please report this at ') | length == 0
|
||||||
|
|
||||||
####################################################################
|
####################################################################
|
||||||
|
|||||||
@ -9,12 +9,10 @@
|
|||||||
non_existing_image: does-not-exist:latest
|
non_existing_image: does-not-exist:latest
|
||||||
project_src: "{{ remote_tmp_dir }}/{{ pname }}"
|
project_src: "{{ remote_tmp_dir }}/{{ pname }}"
|
||||||
test_service_non_existing: |
|
test_service_non_existing: |
|
||||||
version: '3'
|
|
||||||
services:
|
services:
|
||||||
{{ cname }}:
|
{{ cname }}:
|
||||||
image: {{ non_existing_image }}
|
image: {{ non_existing_image }}
|
||||||
test_service_simple: |
|
test_service_simple: |
|
||||||
version: '3'
|
|
||||||
services:
|
services:
|
||||||
{{ cname }}:
|
{{ cname }}:
|
||||||
image: {{ docker_test_image_simple_1 }}
|
image: {{ docker_test_image_simple_1 }}
|
||||||
|
|||||||
@ -77,7 +77,8 @@
|
|||||||
- ansible.builtin.assert:
|
- ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- result_1.rc == 0
|
- result_1.rc == 0
|
||||||
- result_1.stderr == ""
|
# Since Compose 5, unrelated output shows up in stderr...
|
||||||
|
- result_1.stderr == "" or ("Creating" in result_1.stderr and "Created" in result_1.stderr)
|
||||||
- >-
|
- >-
|
||||||
"usr" in result_1.stdout_lines
|
"usr" in result_1.stdout_lines
|
||||||
and
|
and
|
||||||
|
|||||||
@ -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 #################################
|
||||||
####################################################################
|
####################################################################
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import pytest
|
|||||||
from ansible_collections.community.docker.plugins.module_utils._compose_v2 import (
|
from ansible_collections.community.docker.plugins.module_utils._compose_v2 import (
|
||||||
Event,
|
Event,
|
||||||
parse_events,
|
parse_events,
|
||||||
|
parse_json_events,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .compose_v2_test_cases import EVENT_TEST_CASES
|
from .compose_v2_test_cases import EVENT_TEST_CASES
|
||||||
@ -384,3 +385,208 @@ def test_parse_events(
|
|||||||
|
|
||||||
assert collected_events == events
|
assert collected_events == events
|
||||||
assert collected_warnings == warnings
|
assert collected_warnings == warnings
|
||||||
|
|
||||||
|
|
||||||
|
JSON_TEST_CASES: list[tuple[str, str, str, list[Event], list[str]]] = [
|
||||||
|
(
|
||||||
|
"pull-compose-2",
|
||||||
|
"2.40.3",
|
||||||
|
'{"level":"warning","msg":"/tmp/ansible.f9pcm_i3.test/ansible-docker-test-3c46cd06-pull/docker-compose.yml: the attribute `version`'
|
||||||
|
' is obsolete, it will be ignored, please remove it to avoid potential confusion","time":"2025-12-06T13:16:30Z"}\n'
|
||||||
|
'{"id":"ansible-docker-test-3c46cd06-cont","text":"Pulling"}\n'
|
||||||
|
'{"id":"63a26ae4e8a8","parent_id":"ansible-docker-test-3c46cd06-cont","text":"Pulling fs layer"}\n'
|
||||||
|
'{"id":"63a26ae4e8a8","parent_id":"ansible-docker-test-3c46cd06-cont","text":"Downloading","status":"[\\u003e '
|
||||||
|
' ] 6.89kB/599.9kB","current":6890,"total":599883,"percent":1}\n'
|
||||||
|
'{"id":"63a26ae4e8a8","parent_id":"ansible-docker-test-3c46cd06-cont","text":"Download complete","percent":100}\n'
|
||||||
|
'{"id":"63a26ae4e8a8","parent_id":"ansible-docker-test-3c46cd06-cont","text":"Extracting","status":"[==\\u003e '
|
||||||
|
' ] 32.77kB/599.9kB","current":32768,"total":599883,"percent":5}\n'
|
||||||
|
'{"id":"63a26ae4e8a8","parent_id":"ansible-docker-test-3c46cd06-cont","text":"Extracting","status":"[============'
|
||||||
|
'======================================\\u003e] 599.9kB/599.9kB","current":599883,"total":599883,"percent":100}\n'
|
||||||
|
'{"id":"63a26ae4e8a8","parent_id":"ansible-docker-test-3c46cd06-cont","text":"Extracting","status":"[============'
|
||||||
|
'======================================\\u003e] 599.9kB/599.9kB","current":599883,"total":599883,"percent":100}\n'
|
||||||
|
'{"id":"63a26ae4e8a8","parent_id":"ansible-docker-test-3c46cd06-cont","text":"Pull complete","percent":100}\n'
|
||||||
|
'{"id":"ansible-docker-test-3c46cd06-cont","text":"Pulled"}\n',
|
||||||
|
[
|
||||||
|
Event(
|
||||||
|
"unknown",
|
||||||
|
None,
|
||||||
|
"Warning",
|
||||||
|
"/tmp/ansible.f9pcm_i3.test/ansible-docker-test-3c46cd06-pull/docker-compose.yml: the attribute `version` is obsolete,"
|
||||||
|
" it will be ignored, please remove it to avoid potential confusion",
|
||||||
|
),
|
||||||
|
Event(
|
||||||
|
"image",
|
||||||
|
"ansible-docker-test-3c46cd06-cont",
|
||||||
|
"Pulling",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
Event(
|
||||||
|
"image-layer",
|
||||||
|
"63a26ae4e8a8",
|
||||||
|
"Pulling fs layer",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
Event(
|
||||||
|
"image-layer",
|
||||||
|
"63a26ae4e8a8",
|
||||||
|
"Downloading",
|
||||||
|
"[> ] 6.89kB/599.9kB",
|
||||||
|
),
|
||||||
|
Event(
|
||||||
|
"image-layer",
|
||||||
|
"63a26ae4e8a8",
|
||||||
|
"Download complete",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
Event(
|
||||||
|
"image-layer",
|
||||||
|
"63a26ae4e8a8",
|
||||||
|
"Extracting",
|
||||||
|
"[==> ] 32.77kB/599.9kB",
|
||||||
|
),
|
||||||
|
Event(
|
||||||
|
"image-layer",
|
||||||
|
"63a26ae4e8a8",
|
||||||
|
"Extracting",
|
||||||
|
"[==================================================>] 599.9kB/599.9kB",
|
||||||
|
),
|
||||||
|
Event(
|
||||||
|
"image-layer",
|
||||||
|
"63a26ae4e8a8",
|
||||||
|
"Extracting",
|
||||||
|
"[==================================================>] 599.9kB/599.9kB",
|
||||||
|
),
|
||||||
|
Event(
|
||||||
|
"image-layer",
|
||||||
|
"63a26ae4e8a8",
|
||||||
|
"Pull complete",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
Event(
|
||||||
|
"image",
|
||||||
|
"ansible-docker-test-3c46cd06-cont",
|
||||||
|
"Pulled",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"pull-compose-5",
|
||||||
|
"5.0.0",
|
||||||
|
'{"level":"warning","msg":"/tmp/ansible.1n0q46aj.test/ansible-docker-test-b2fa9191-pull/docker-compose.yml: the attribute'
|
||||||
|
' `version` is obsolete, it will be ignored, please remove it to avoid potential confusion","time":"2025-12-06T13:08:22Z"}\n'
|
||||||
|
'{"id":"Image ghcr.io/ansible-collections/simple-1:tag","status":"Working","text":"Pulling"}\n'
|
||||||
|
'{"id":"63a26ae4e8a8","parent_id":"Image ghcr.io/ansible-collections/simple-1:tag","status":"Working"}\n'
|
||||||
|
'{"id":"63a26ae4e8a8","parent_id":"Image ghcr.io/ansible-collections/simple-1:tag","status":"Working","text":"[\\u003e '
|
||||||
|
' ] 6.89kB/599.9kB","current":6890,"total":599883,"percent":1}\n'
|
||||||
|
'{"id":"63a26ae4e8a8","parent_id":"Image ghcr.io/ansible-collections/simple-1:tag","status":"Working","text":"[=============='
|
||||||
|
'====================================\\u003e] 599.9kB/599.9kB","current":599883,"total":599883,"percent":100}\n'
|
||||||
|
'{"id":"63a26ae4e8a8","parent_id":"Image ghcr.io/ansible-collections/simple-1:tag","status":"Working"}\n'
|
||||||
|
'{"id":"63a26ae4e8a8","parent_id":"Image ghcr.io/ansible-collections/simple-1:tag","status":"Done","percent":100}\n'
|
||||||
|
'{"id":"63a26ae4e8a8","parent_id":"Image ghcr.io/ansible-collections/simple-1:tag","status":"Working","text":"[==\\u003e '
|
||||||
|
' ] 32.77kB/599.9kB","current":32768,"total":599883,"percent":5}\n'
|
||||||
|
'{"id":"63a26ae4e8a8","parent_id":"Image ghcr.io/ansible-collections/simple-1:tag","status":"Working","text":"[=============='
|
||||||
|
'====================================\\u003e] 599.9kB/599.9kB","current":599883,"total":599883,"percent":100}\n'
|
||||||
|
'{"id":"63a26ae4e8a8","parent_id":"Image ghcr.io/ansible-collections/simple-1:tag","status":"Working","text":"[=============='
|
||||||
|
'====================================\\u003e] 599.9kB/599.9kB","current":599883,"total":599883,"percent":100}\n'
|
||||||
|
'{"id":"63a26ae4e8a8","parent_id":"Image ghcr.io/ansible-collections/simple-1:tag","status":"Done","percent":100}\n'
|
||||||
|
'{"id":"Image ghcr.io/ansible-collections/simple-1:tag","status":"Done","text":"Pulled"}\n',
|
||||||
|
[
|
||||||
|
Event(
|
||||||
|
"unknown",
|
||||||
|
None,
|
||||||
|
"Warning",
|
||||||
|
"/tmp/ansible.1n0q46aj.test/ansible-docker-test-b2fa9191-pull/docker-compose.yml: the attribute `version`"
|
||||||
|
" is obsolete, it will be ignored, please remove it to avoid potential confusion",
|
||||||
|
),
|
||||||
|
Event(
|
||||||
|
"image",
|
||||||
|
"ghcr.io/ansible-collections/simple-1:tag",
|
||||||
|
"Pulling",
|
||||||
|
"Working",
|
||||||
|
),
|
||||||
|
Event(
|
||||||
|
"image-layer",
|
||||||
|
"ghcr.io/ansible-collections/simple-1:tag",
|
||||||
|
"Working",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
Event(
|
||||||
|
"image-layer",
|
||||||
|
"ghcr.io/ansible-collections/simple-1:tag",
|
||||||
|
"Working",
|
||||||
|
"[> ] 6.89kB/599.9kB",
|
||||||
|
),
|
||||||
|
Event(
|
||||||
|
"image-layer",
|
||||||
|
"ghcr.io/ansible-collections/simple-1:tag",
|
||||||
|
"Working",
|
||||||
|
"[==================================================>] 599.9kB/599.9kB",
|
||||||
|
),
|
||||||
|
Event(
|
||||||
|
"image-layer",
|
||||||
|
"ghcr.io/ansible-collections/simple-1:tag",
|
||||||
|
"Working",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
Event(
|
||||||
|
"image-layer", "ghcr.io/ansible-collections/simple-1:tag", "Done", None
|
||||||
|
),
|
||||||
|
Event(
|
||||||
|
"image-layer",
|
||||||
|
"ghcr.io/ansible-collections/simple-1:tag",
|
||||||
|
"Working",
|
||||||
|
"[==> ] 32.77kB/599.9kB",
|
||||||
|
),
|
||||||
|
Event(
|
||||||
|
"image-layer",
|
||||||
|
"ghcr.io/ansible-collections/simple-1:tag",
|
||||||
|
"Working",
|
||||||
|
"[==================================================>] 599.9kB/599.9kB",
|
||||||
|
),
|
||||||
|
Event(
|
||||||
|
"image-layer",
|
||||||
|
"ghcr.io/ansible-collections/simple-1:tag",
|
||||||
|
"Working",
|
||||||
|
"[==================================================>] 599.9kB/599.9kB",
|
||||||
|
),
|
||||||
|
Event(
|
||||||
|
"image-layer", "ghcr.io/ansible-collections/simple-1:tag", "Done", None
|
||||||
|
),
|
||||||
|
Event(
|
||||||
|
"image", "ghcr.io/ansible-collections/simple-1:tag", "Pulled", "Done"
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"test_id, compose_version, stderr, events, warnings",
|
||||||
|
JSON_TEST_CASES,
|
||||||
|
ids=[tc[0] for tc in JSON_TEST_CASES],
|
||||||
|
)
|
||||||
|
def test_parse_json_events(
|
||||||
|
test_id: str,
|
||||||
|
compose_version: str,
|
||||||
|
stderr: str,
|
||||||
|
events: list[Event],
|
||||||
|
warnings: list[str],
|
||||||
|
) -> None:
|
||||||
|
collected_warnings = []
|
||||||
|
|
||||||
|
def collect_warning(msg: str) -> None:
|
||||||
|
collected_warnings.append(msg)
|
||||||
|
|
||||||
|
collected_events = parse_json_events(
|
||||||
|
stderr.encode("utf-8"),
|
||||||
|
warn_function=collect_warning,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(collected_events)
|
||||||
|
print(collected_warnings)
|
||||||
|
|
||||||
|
assert collected_events == events
|
||||||
|
assert collected_warnings == warnings
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user