mirror of
https://github.com/home-assistant/supervisor.git
synced 2026-08-17 18:32:57 +01:00
The unsupported container evaluation split an image reference on its first colon to strip the tag. For a registry with a port, that colon belongs to the port, so `myregistry:5000/app:1.0` was reduced to `myregistry` and reported as an unsupported image on the host. Splitting an image reference correctly needs to know where the registry domain ends, and the pieces for that were already there but wired up in a way that could not be reused. IMAGE_REGISTRY_REGEX is a port of Docker's DomainRegexp, which Docker itself uses to validate a domain, not to find one. Placing it in front of the search meant get_registry_from_image() still had to re-derive the answer with the dot/colon/localhost checks that follow the match, and callers that needed the rest of the reference recovered it by slicing off len(registry) + 1 characters. Split the two jobs apart, mirroring Docker's reference implementation: - split_docker_domain() finds the domain the way splitDockerDomain() does, by cutting at the first slash and testing the candidate. It returns the remainder as well, so callers no longer slice by length, and it canonicalizes index.docker.io to docker.io, which lets stored Docker Hub credentials apply to references using the legacy domain. - is_registry_domain() validates a domain against DomainRegexp, which is what the regex is for. Image validation keeps rejecting malformed domains such as ".ghcr.io" through this check. - get_registry_from_image() stays as a wrapper for the callers that only need the domain. With the domain handled, splitting off the tag is a matter of taking the last colon that has no slash after it, which is what Docker's TagRegexp allows. split_image_tag() does that and drops any digest, so a digest-pinned image no longer reads as unsupported either. Move the image reference tests to tests/docker/test_utils.py next to the code under test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
122 lines
4.6 KiB
Python
122 lines
4.6 KiB
Python
"""Test Docker utilities."""
|
|
|
|
import pytest
|
|
|
|
from supervisor.docker.const import DOCKER_HUB
|
|
from supervisor.docker.utils import (
|
|
get_registry_from_image,
|
|
is_registry_domain,
|
|
split_docker_domain,
|
|
split_image_tag,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("image_ref", "expected"),
|
|
[
|
|
# No registry, hosted on Docker Hub
|
|
("nginx", (None, "nginx")),
|
|
("nginx:latest", (None, "nginx:latest")),
|
|
("library/nginx", (None, "library/nginx")),
|
|
("homeassistant/amd64-supervisor", (None, "homeassistant/amd64-supervisor")),
|
|
# Registry with a dot
|
|
(
|
|
"ghcr.io/home-assistant/amd64-supervisor",
|
|
("ghcr.io", "home-assistant/amd64-supervisor"),
|
|
),
|
|
("registry.example.com/org/image:v1", ("registry.example.com", "org/image:v1")),
|
|
("127.0.0.1/myimage", ("127.0.0.1", "myimage")),
|
|
# Registry with a port
|
|
("myregistry:5000/myimage", ("myregistry:5000", "myimage")),
|
|
("registry.io:5000/org/app:v1", ("registry.io:5000", "org/app:v1")),
|
|
# localhost is a reserved namespace and always a registry
|
|
("localhost/myimage", ("localhost", "myimage")),
|
|
("localhost:5000/myimage:tag", ("localhost:5000", "myimage:tag")),
|
|
# IPv6 registry
|
|
("[::1]:5000/myimage", ("[::1]:5000", "myimage")),
|
|
("[2001:db8::1]:5000/myimage:tag", ("[2001:db8::1]:5000", "myimage:tag")),
|
|
# Legacy Docker Hub domain gets canonicalized
|
|
("index.docker.io/library/nginx", (DOCKER_HUB, "library/nginx")),
|
|
# Uppercase is not allowed in a path component, so it is a registry
|
|
("Foo/bar", ("Foo", "bar")),
|
|
],
|
|
)
|
|
def test_split_docker_domain(image_ref: str, expected: tuple[str | None, str]):
|
|
"""Test splitting an image reference into registry domain and remainder."""
|
|
assert split_docker_domain(image_ref) == expected
|
|
|
|
|
|
def test_get_registry_from_image():
|
|
"""Test get_registry_from_image returns only the registry domain."""
|
|
assert get_registry_from_image("ghcr.io/home-assistant/supervisor") == "ghcr.io"
|
|
assert get_registry_from_image("homeassistant/supervisor") is None
|
|
assert get_registry_from_image("index.docker.io/library/nginx") == DOCKER_HUB
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("domain", "valid"),
|
|
[
|
|
("ghcr.io", True),
|
|
("registry.example.com", True),
|
|
("myregistry:5000", True),
|
|
("localhost", True),
|
|
("localhost:5000", True),
|
|
("127.0.0.1", True),
|
|
("[::1]:5000", True),
|
|
("[2001:db8::1]", True),
|
|
# Malformed domains
|
|
(".ghcr.io", False),
|
|
("ghcr.io.", False),
|
|
("-bad-.com", False),
|
|
("bad-.com", False),
|
|
("....", False),
|
|
("ghcr.io:", False),
|
|
("ghcr.io:port", False),
|
|
("ghcr.io/org", False),
|
|
],
|
|
)
|
|
def test_is_registry_domain(domain: str, valid: bool):
|
|
"""Test validation of registry domains."""
|
|
assert is_registry_domain(domain) is valid
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("image_ref", "expected"),
|
|
[
|
|
# No tag
|
|
("nginx", ("nginx", None)),
|
|
("library/nginx", ("library/nginx", None)),
|
|
(
|
|
"ghcr.io/home-assistant/amd64-supervisor",
|
|
("ghcr.io/home-assistant/amd64-supervisor", None),
|
|
),
|
|
# With tag
|
|
("nginx:latest", ("nginx", "latest")),
|
|
(
|
|
"homeassistant/amd64-supervisor:1.2.3",
|
|
("homeassistant/amd64-supervisor", "1.2.3"),
|
|
),
|
|
# Registry with a port, the port must stay part of the image name
|
|
("myregistry:5000/myimage", ("myregistry:5000/myimage", None)),
|
|
("registry.io:5000/org/app:v1", ("registry.io:5000/org/app", "v1")),
|
|
(
|
|
"gitlab.example.com:5005/org/app/aarch64:0.3.3-dev1",
|
|
("gitlab.example.com:5005/org/app/aarch64", "0.3.3-dev1"),
|
|
),
|
|
# localhost with a port
|
|
("localhost:5000/myimage", ("localhost:5000/myimage", None)),
|
|
("localhost:5000/myimage:tag", ("localhost:5000/myimage", "tag")),
|
|
# IPv6 registry
|
|
("[::1]:5000/myimage", ("[::1]:5000/myimage", None)),
|
|
("[2001:db8::1]:5000/myimage:tag", ("[2001:db8::1]:5000/myimage", "tag")),
|
|
# Digests are stripped along with the tag
|
|
("nginx@sha256:1234abcd", ("nginx", None)),
|
|
("ghcr.io/org/app@sha256:1234abcd", ("ghcr.io/org/app", None)),
|
|
# A bare digest keeps its algorithm prefix as the name
|
|
("sha256:1234abcd", ("sha256", "1234abcd")),
|
|
],
|
|
)
|
|
def test_split_image_tag(image_ref: str, expected: tuple[str, str | None]):
|
|
"""Test splitting an image reference into image name and tag."""
|
|
assert split_image_tag(image_ref) == expected
|