mirror of
https://github.com/ansible-collections/community.docker.git
synced 2025-12-13 02:22:04 +00:00
Compare commits
8 Commits
fe7250f0f3
...
3086fa95de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3086fa95de | ||
|
|
947ec9a442 | ||
|
|
25e7ba222e | ||
|
|
6ab8cc0d82 | ||
|
|
159df0ab91 | ||
|
|
174c0c8058 | ||
|
|
2efcd6b2ec | ||
|
|
6f53ad9bd7 |
717
CHANGELOG.md
717
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,20 @@ Docker Community Collection Release Notes
|
||||
|
||||
.. 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
|
||||
======
|
||||
|
||||
|
||||
@ -2330,3 +2330,18 @@ releases:
|
||||
- 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'
|
||||
|
||||
@ -197,7 +197,11 @@ class AnsibleDockerClientBase:
|
||||
data = json.loads(stdout)
|
||||
except Exception as exc: # pylint: disable=broad-exception-caught
|
||||
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
|
||||
|
||||
@ -223,7 +227,11 @@ class AnsibleDockerClientBase:
|
||||
result.append(json.loads(line))
|
||||
except Exception as exc: # pylint: disable=broad-exception-caught
|
||||
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
|
||||
|
||||
|
||||
@ -132,7 +132,7 @@ DOCKER_PULL_PROGRESS_DONE = frozenset(
|
||||
"Pull complete",
|
||||
)
|
||||
)
|
||||
DOCKER_PULL_PROGRESS_WORKING = frozenset(
|
||||
DOCKER_PULL_PROGRESS_WORKING_OLD = frozenset(
|
||||
(
|
||||
"Pulling fs layer",
|
||||
"Waiting",
|
||||
@ -141,6 +141,7 @@ DOCKER_PULL_PROGRESS_WORKING = frozenset(
|
||||
"Extracting",
|
||||
)
|
||||
)
|
||||
DOCKER_PULL_PROGRESS_WORKING = frozenset(DOCKER_PULL_PROGRESS_WORKING_OLD | {"Working"})
|
||||
|
||||
|
||||
class ResourceType:
|
||||
@ -191,7 +192,7 @@ _RE_PULL_EVENT = re.compile(
|
||||
)
|
||||
|
||||
_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(
|
||||
@ -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"}
|
||||
# (The longer form happens since Docker Compose 2.39.0)
|
||||
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)
|
||||
try:
|
||||
resource_type = ResourceType.from_docker_compose_event(
|
||||
@ -513,7 +524,7 @@ def parse_json_events(
|
||||
status, text = text, status
|
||||
elif (
|
||||
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
|
||||
status, text = text, status
|
||||
|
||||
117
plugins/module_utils/_image_name.py
Normal file
117
plugins/module_utils/_image_name.py
Normal file
@ -0,0 +1,117 @@
|
||||
# Copyright (c) 2025 Felix Fontein
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
|
||||
# Do not use this from other collections or standalone plugins/modules!
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import typing as t
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
_PATH_RE = re.compile(
|
||||
r"^[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*$"
|
||||
)
|
||||
_TAG_RE = re.compile(r"^[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}$")
|
||||
_DIGEST_RE = re.compile(r"^sha256:[0-9a-fA-F]{64}$")
|
||||
|
||||
|
||||
def is_digest(name: str, allow_empty: bool = False) -> bool:
|
||||
"""Check whether the given name is in fact an image ID (hash)."""
|
||||
if not name:
|
||||
return allow_empty
|
||||
return _DIGEST_RE.match(name) is not None
|
||||
|
||||
|
||||
def is_tag(name: str, allow_empty: bool = False) -> bool:
|
||||
"""Check whether the given name can be an image tag."""
|
||||
if not name:
|
||||
return allow_empty
|
||||
return _TAG_RE.match(name) is not None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImageName:
|
||||
registry: str | None
|
||||
path: str
|
||||
tag: str | None
|
||||
digest: str | None
|
||||
|
||||
@classmethod
|
||||
def parse(cls, name: str) -> t.Self:
|
||||
registry: str | None = None
|
||||
tag: str | None = None
|
||||
digest: str | None = None
|
||||
parts = name.rsplit("@", 1)
|
||||
if len(parts) == 2:
|
||||
name, digest = parts
|
||||
parts = name.rsplit(":", 1)
|
||||
if len(parts) == 2 and "/" not in parts[1]:
|
||||
name, tag = parts
|
||||
parts = name.split("/", 1)
|
||||
if len(parts) == 2 and (
|
||||
"." in parts[0] or ":" in parts[0] or parts[0] == "localhost"
|
||||
):
|
||||
registry, name = parts
|
||||
return cls(registry, name, tag, digest)
|
||||
|
||||
def validate(self) -> t.Self:
|
||||
if self.registry:
|
||||
if self.registry[0] == "-" or self.registry[-1] == "-":
|
||||
raise ValueError(
|
||||
f'Invalid registry name ({self.registry}): must not begin or end with a "-".'
|
||||
)
|
||||
if self.registry[-1] == ":":
|
||||
raise ValueError(
|
||||
f'Invalid registry name ({self.registry}): must not end with ":".'
|
||||
)
|
||||
if not _PATH_RE.match(self.path):
|
||||
raise ValueError(f"Invalid path ({self.path}).")
|
||||
if self.tag and not is_tag(self.tag):
|
||||
raise ValueError(f"Invalid tag ({self.tag}).")
|
||||
if self.digest and not is_digest(self.digest):
|
||||
raise ValueError(f"Invalid digest ({self.digest}).")
|
||||
return self
|
||||
|
||||
def combine(self) -> str:
|
||||
parts = []
|
||||
if self.registry:
|
||||
parts.append(self.registry)
|
||||
if self.path:
|
||||
parts.append("/")
|
||||
parts.append(self.path)
|
||||
if self.tag:
|
||||
parts.append(":")
|
||||
parts.append(self.tag)
|
||||
if self.digest:
|
||||
parts.append("@")
|
||||
parts.append(self.digest)
|
||||
return "".join(parts)
|
||||
|
||||
def normalize(self) -> ImageName:
|
||||
registry = self.registry
|
||||
path = self.path
|
||||
if registry in ("", None, "index.docker.io", "registry.hub.docker.com"):
|
||||
registry = "docker.io"
|
||||
if registry == "docker.io" and "/" not in path and path:
|
||||
path = f"library/{path}"
|
||||
return ImageName(registry, path, self.tag, self.digest)
|
||||
|
||||
def get_hostname_and_port(self) -> tuple[str, int]:
|
||||
if self.registry is None:
|
||||
raise ValueError(
|
||||
"Cannot get hostname when there is no registry. Normalize first!"
|
||||
)
|
||||
if self.registry == "docker.io":
|
||||
return "index.docker.io", 443
|
||||
parts = self.registry.split(":", 1)
|
||||
if len(parts) == 2:
|
||||
try:
|
||||
port = int(parts[1])
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError(f"Cannot parse port {parts[1]!r}") from exc
|
||||
return parts[0], port
|
||||
return self.registry, 443
|
||||
@ -81,16 +81,19 @@
|
||||
- ansible.builtin.assert:
|
||||
that:
|
||||
- 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 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_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 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
|
||||
|
||||
####################################################################
|
||||
|
||||
@ -9,12 +9,10 @@
|
||||
non_existing_image: does-not-exist:latest
|
||||
project_src: "{{ remote_tmp_dir }}/{{ pname }}"
|
||||
test_service_non_existing: |
|
||||
version: '3'
|
||||
services:
|
||||
{{ cname }}:
|
||||
image: {{ non_existing_image }}
|
||||
test_service_simple: |
|
||||
version: '3'
|
||||
services:
|
||||
{{ cname }}:
|
||||
image: {{ docker_test_image_simple_1 }}
|
||||
|
||||
@ -77,7 +77,8 @@
|
||||
- ansible.builtin.assert:
|
||||
that:
|
||||
- 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
|
||||
and
|
||||
|
||||
@ -9,6 +9,7 @@ import pytest
|
||||
from ansible_collections.community.docker.plugins.module_utils._compose_v2 import (
|
||||
Event,
|
||||
parse_events,
|
||||
parse_json_events,
|
||||
)
|
||||
|
||||
from .compose_v2_test_cases import EVENT_TEST_CASES
|
||||
@ -384,3 +385,208 @@ def test_parse_events(
|
||||
|
||||
assert collected_events == events
|
||||
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
|
||||
|
||||
272
tests/unit/plugins/module_utils/test__image_name.py
Normal file
272
tests/unit/plugins/module_utils/test__image_name.py
Normal file
@ -0,0 +1,272 @@
|
||||
# Copyright (c) 2025 Felix Fontein
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import typing as t
|
||||
|
||||
import pytest
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._image_name import (
|
||||
ImageName,
|
||||
is_digest,
|
||||
is_tag,
|
||||
)
|
||||
|
||||
|
||||
TEST_IS_DIGEST: list[tuple[str, dict[str, t.Any], bool]] = [
|
||||
("", {}, False),
|
||||
("", {"allow_empty": True}, True),
|
||||
("sha256:abc", {}, False),
|
||||
(f"sha256:{'a' * 63}", {}, False),
|
||||
(f"sha256:{'a' * 64}", {}, True),
|
||||
(f"sha256:{'a' * 65}", {}, False),
|
||||
(
|
||||
"sha256:d02f9b9db4d759ef27dc26b426b842ff2fb881c5c6079612d27ec36e36b132dd",
|
||||
{},
|
||||
True,
|
||||
),
|
||||
("1.25.3", {}, False),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name, kwargs, expected", TEST_IS_DIGEST)
|
||||
def test_is_digest(name: str, kwargs: dict[str, t.Any], expected: bool) -> None:
|
||||
assert is_digest(name, **kwargs) == expected
|
||||
|
||||
|
||||
TEST_IS_TAG: list[tuple[str, dict[str, t.Any], bool]] = [
|
||||
("", {}, False),
|
||||
("", {"allow_empty": True}, True),
|
||||
("foo", {}, True),
|
||||
("-foo", {}, False),
|
||||
("f" * 128, {}, True),
|
||||
("f" * 129, {}, False),
|
||||
(
|
||||
"sha256:d02f9b9db4d759ef27dc26b426b842ff2fb881c5c6079612d27ec36e36b132dd",
|
||||
{},
|
||||
False,
|
||||
),
|
||||
("1.25.3", {}, True),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name, kwargs, expected", TEST_IS_TAG)
|
||||
def test_is_tag(name: str, kwargs: dict[str, t.Any], expected: bool) -> None:
|
||||
assert is_tag(name, **kwargs) == expected
|
||||
|
||||
|
||||
TEST_IMAGE_NAME_VALIDATE_SUCCESS: list[ImageName] = [
|
||||
ImageName(registry="localhost", path="nginx", tag=None, digest=None),
|
||||
ImageName(registry=None, path="nginx", tag="1.25.3", digest=None),
|
||||
ImageName(
|
||||
registry=None,
|
||||
path="nginx",
|
||||
tag=None,
|
||||
digest="sha256:d02f9b9db4d759ef27dc26b426b842ff2fb881c5c6079612d27ec36e36b132dd",
|
||||
),
|
||||
ImageName(
|
||||
registry=None,
|
||||
path="nginx",
|
||||
tag="1.25.3",
|
||||
digest="sha256:d02f9b9db4d759ef27dc26b426b842ff2fb881c5c6079612d27ec36e36b132dd",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("data", TEST_IMAGE_NAME_VALIDATE_SUCCESS)
|
||||
def test_imagename_validate_success(data: ImageName) -> None:
|
||||
assert data.validate() is data
|
||||
|
||||
|
||||
TEST_IMAGE_NAME_VALIDATE_FAILED: list[tuple[ImageName, str]] = [
|
||||
(
|
||||
ImageName(registry="-foo", path="", tag=None, digest=None),
|
||||
'Invalid registry name (-foo): must not begin or end with a "-".',
|
||||
),
|
||||
(
|
||||
ImageName(registry="foo:", path="", tag=None, digest=None),
|
||||
'Invalid registry name (foo:): must not end with ":".',
|
||||
),
|
||||
(ImageName(registry=None, path="", tag=None, digest=None), "Invalid path ()."),
|
||||
(ImageName(registry=None, path="-", tag=None, digest=None), "Invalid path (-)."),
|
||||
(ImageName(registry=None, path="/", tag=None, digest=None), "Invalid path (/)."),
|
||||
(ImageName(registry=None, path="a", tag="-", digest=None), "Invalid tag (-)."),
|
||||
(ImageName(registry=None, path="a", tag=None, digest="-"), "Invalid digest (-)."),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("data, expected", TEST_IMAGE_NAME_VALIDATE_FAILED)
|
||||
def test_imagename_validate_failed(data: ImageName, expected: str) -> None:
|
||||
with pytest.raises(ValueError, match=f"^{re.escape(expected)}$"):
|
||||
data.validate()
|
||||
|
||||
|
||||
TEST_IMAGE_NAME_PARSE: list[tuple[str, ImageName]] = [
|
||||
("", ImageName(registry=None, path="", tag=None, digest=None)),
|
||||
("foo", ImageName(registry=None, path="foo", tag=None, digest=None)),
|
||||
("foo:5000", ImageName(registry=None, path="foo", tag="5000", digest=None)),
|
||||
("foo:5000/", ImageName(registry="foo:5000", path="", tag=None, digest=None)),
|
||||
("foo:5000/bar", ImageName(registry="foo:5000", path="bar", tag=None, digest=None)),
|
||||
("/bar", ImageName(registry=None, path="/bar", tag=None, digest=None)),
|
||||
(
|
||||
"localhost/foo:5000",
|
||||
ImageName(registry="localhost", path="foo", tag="5000", digest=None),
|
||||
),
|
||||
(
|
||||
"foo.bar/baz:5000",
|
||||
ImageName(registry="foo.bar", path="baz", tag="5000", digest=None),
|
||||
),
|
||||
(
|
||||
"foo:bar/baz:bam:5000",
|
||||
ImageName(registry="foo:bar", path="baz:bam", tag="5000", digest=None),
|
||||
),
|
||||
("foo:bar:baz", ImageName(registry=None, path="foo:bar", tag="baz", digest=None)),
|
||||
("foo@bar@baz", ImageName(registry=None, path="foo@bar", tag=None, digest="baz")),
|
||||
("nginx:1.25.3", ImageName(registry=None, path="nginx", tag="1.25.3", digest=None)),
|
||||
(
|
||||
"nginx@sha256:d02f9b9db4d759ef27dc26b426b842ff2fb881c5c6079612d27ec36e36b132dd",
|
||||
ImageName(
|
||||
registry=None,
|
||||
path="nginx",
|
||||
tag=None,
|
||||
digest="sha256:d02f9b9db4d759ef27dc26b426b842ff2fb881c5c6079612d27ec36e36b132dd",
|
||||
),
|
||||
),
|
||||
(
|
||||
"nginx:1.25.3@sha256:d02f9b9db4d759ef27dc26b426b842ff2fb881c5c6079612d27ec36e36b132dd",
|
||||
ImageName(
|
||||
registry=None,
|
||||
path="nginx",
|
||||
tag="1.25.3",
|
||||
digest="sha256:d02f9b9db4d759ef27dc26b426b842ff2fb881c5c6079612d27ec36e36b132dd",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name, expected", TEST_IMAGE_NAME_PARSE)
|
||||
def test_imagename_parse(name: str, expected: ImageName) -> None:
|
||||
assert ImageName.parse(name) == expected
|
||||
|
||||
|
||||
TEST_IMAGE_NAME_COMBINE: list[tuple[ImageName, str]] = [
|
||||
(ImageName(registry=None, path="", tag=None, digest=None), ""),
|
||||
(ImageName(registry=None, path="nginx", tag="1.25.3", digest=None), "nginx:1.25.3"),
|
||||
(
|
||||
ImageName(
|
||||
registry=None,
|
||||
path="nginx",
|
||||
tag=None,
|
||||
digest="sha256:d02f9b9db4d759ef27dc26b426b842ff2fb881c5c6079612d27ec36e36b132dd",
|
||||
),
|
||||
"nginx@sha256:d02f9b9db4d759ef27dc26b426b842ff2fb881c5c6079612d27ec36e36b132dd",
|
||||
),
|
||||
(
|
||||
ImageName(
|
||||
registry=None,
|
||||
path="nginx",
|
||||
tag="1.25.3",
|
||||
digest="sha256:d02f9b9db4d759ef27dc26b426b842ff2fb881c5c6079612d27ec36e36b132dd",
|
||||
),
|
||||
"nginx:1.25.3@sha256:d02f9b9db4d759ef27dc26b426b842ff2fb881c5c6079612d27ec36e36b132dd",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("data, expected", TEST_IMAGE_NAME_COMBINE)
|
||||
def test_imagename_combine(data: ImageName, expected: str) -> None:
|
||||
assert data.combine() == expected
|
||||
|
||||
|
||||
TEST_IMAGE_NAME_NORMALIZE: list[tuple[ImageName, ImageName]] = [
|
||||
(
|
||||
ImageName(registry=None, path="", tag=None, digest=None),
|
||||
ImageName(registry="docker.io", path="", tag=None, digest=None),
|
||||
),
|
||||
(
|
||||
ImageName(registry="", path="", tag=None, digest=None),
|
||||
ImageName(registry="docker.io", path="", tag=None, digest=None),
|
||||
),
|
||||
(
|
||||
ImageName(registry="index.docker.io", path="", tag=None, digest=None),
|
||||
ImageName(registry="docker.io", path="", tag=None, digest=None),
|
||||
),
|
||||
(
|
||||
ImageName(registry="registry.hub.docker.com", path="", tag=None, digest=None),
|
||||
ImageName(registry="docker.io", path="", tag=None, digest=None),
|
||||
),
|
||||
(
|
||||
ImageName(registry=None, path="foo/bar", tag=None, digest=None),
|
||||
ImageName(registry="docker.io", path="foo/bar", tag=None, digest=None),
|
||||
),
|
||||
(
|
||||
ImageName(registry=None, path="nginx", tag="1.25.3", digest=None),
|
||||
ImageName(
|
||||
registry="docker.io", path="library/nginx", tag="1.25.3", digest=None
|
||||
),
|
||||
),
|
||||
(
|
||||
ImageName(
|
||||
registry=None,
|
||||
path="nginx",
|
||||
tag=None,
|
||||
digest="sha256:d02f9b9db4d759ef27dc26b426b842ff2fb881c5c6079612d27ec36e36b132dd",
|
||||
),
|
||||
ImageName(
|
||||
registry="docker.io",
|
||||
path="library/nginx",
|
||||
tag=None,
|
||||
digest="sha256:d02f9b9db4d759ef27dc26b426b842ff2fb881c5c6079612d27ec36e36b132dd",
|
||||
),
|
||||
),
|
||||
(
|
||||
ImageName(
|
||||
registry=None,
|
||||
path="nginx",
|
||||
tag="1.25.3",
|
||||
digest="sha256:d02f9b9db4d759ef27dc26b426b842ff2fb881c5c6079612d27ec36e36b132dd",
|
||||
),
|
||||
ImageName(
|
||||
registry="docker.io",
|
||||
path="library/nginx",
|
||||
tag="1.25.3",
|
||||
digest="sha256:d02f9b9db4d759ef27dc26b426b842ff2fb881c5c6079612d27ec36e36b132dd",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("data, expected", TEST_IMAGE_NAME_NORMALIZE)
|
||||
def test_imagename_normalize(data: ImageName, expected: ImageName) -> None:
|
||||
assert data.normalize() == expected
|
||||
|
||||
|
||||
TEST_IMAGE_NAME_HOSTNAME_AND_PORT: list[tuple[ImageName, str, int]] = [
|
||||
(
|
||||
ImageName(registry="docker.io", path="", tag=None, digest=None),
|
||||
"index.docker.io",
|
||||
443,
|
||||
),
|
||||
(ImageName(registry="localhost", path="", tag=None, digest=None), "localhost", 443),
|
||||
(ImageName(registry="foo:5000", path="", tag=None, digest=None), "foo", 5000),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"data, expected_hostname, expected_port", TEST_IMAGE_NAME_HOSTNAME_AND_PORT
|
||||
)
|
||||
def test_imagename_get_hostname_and_port(
|
||||
data: ImageName, expected_hostname: str, expected_port: int
|
||||
) -> None:
|
||||
hostname, port = data.get_hostname_and_port()
|
||||
assert hostname == expected_hostname
|
||||
assert port == expected_port
|
||||
|
||||
|
||||
def test_imagename_get_hostname_and_port_fail() -> None:
|
||||
msg = "Cannot get hostname when there is no registry. Normalize first!"
|
||||
with pytest.raises(ValueError, match=f"^{re.escape(msg)}$"):
|
||||
ImageName(registry=None, path="", tag=None, digest=None).get_hostname_and_port()
|
||||
Loading…
Reference in New Issue
Block a user