Skip to content

Commit 50d3120

Browse files
agnersclaude
andauthored
Use Docker's official registry domain detection logic (#6360)
* Use Docker's official registry domain detection logic Replace the custom IMAGE_WITH_HOST regex with a proper implementation based on Docker's reference parser (vendor/github.com/distribution/ reference/normalize.go). Changes: - Change DOCKER_HUB from "hub.docker.com" to "docker.io" (official default) - Add DOCKER_HUB_LEGACY for backward compatibility with "hub.docker.com" - Add IMAGE_DOMAIN_REGEX and get_domain() function that properly detects: - localhost (with optional port) - Domains with "." (e.g., ghcr.io, 127.0.0.1) - Domains with ":" port (e.g., myregistry:5000) - IPv6 addresses (e.g., [::1]:5000) - Update credential handling to support both docker.io and hub.docker.com - Add comprehensive tests for domain detection 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * Refactor Docker domain detection to utils module Move get_domain function to supervisor/docker/utils.py and rename it to get_domain_from_image for consistency with get_registry_for_image. Use named group in the regex for better readability. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * Rename domain to registry for consistency Use consistent "registry" terminology throughout the codebase: - Rename get_domain_from_image to get_registry_from_image - Rename IMAGE_DOMAIN_REGEX to IMAGE_REGISTRY_REGEX - Update named group from "domain" to "registry" - Update all related comments and variable names 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent bac072a commit 50d3120

File tree

6 files changed

+164
-16
lines changed

6 files changed

+164
-16
lines changed

supervisor/addons/build.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
CpuArch,
2424
)
2525
from ..coresys import CoreSys, CoreSysAttributes
26-
from ..docker.const import DOCKER_HUB
26+
from ..docker.const import DOCKER_HUB, DOCKER_HUB_LEGACY
2727
from ..docker.interface import MAP_ARCH
2828
from ..exceptions import ConfigurationFileError, HassioArchNotFound
2929
from ..utils.common import FileConfiguration, find_one_filetype
@@ -155,8 +155,11 @@ def get_docker_config_json(self) -> str | None:
155155

156156
# Use the actual registry URL for the key
157157
# Docker Hub uses "https://index.docker.io/v1/" as the key
158+
# Support both docker.io (official) and hub.docker.com (legacy)
158159
registry_key = (
159-
"https://index.docker.io/v1/" if registry == DOCKER_HUB else registry
160+
"https://index.docker.io/v1/"
161+
if registry in (DOCKER_HUB, DOCKER_HUB_LEGACY)
162+
else registry
160163
)
161164

162165
config = {"auths": {registry_key: {"auth": auth_string}}}

supervisor/docker/const.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@
1515

1616
RE_RETRYING_DOWNLOAD_STATUS = re.compile(r"Retrying in \d+ seconds?")
1717

18-
# Docker Hub registry identifier
19-
DOCKER_HUB = "hub.docker.com"
18+
# Docker Hub registry identifier (official default)
19+
# Docker's default registry is docker.io
20+
DOCKER_HUB = "docker.io"
2021

21-
# Regex to match images with a registry host (e.g., ghcr.io/org/image)
22-
IMAGE_WITH_HOST = re.compile(r"^((?:[a-z0-9]+(?:-[a-z0-9]+)*\.)+[a-z]{2,})\/.+")
22+
# Legacy Docker Hub identifier for backward compatibility
23+
DOCKER_HUB_LEGACY = "hub.docker.com"
2324

2425

2526
class Capabilities(StrEnum):

supervisor/docker/interface.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,13 @@
4545
from ..jobs.job_group import JobGroup
4646
from ..resolution.const import ContextType, IssueType, SuggestionType
4747
from ..utils.sentry import async_capture_exception
48-
from .const import DOCKER_HUB, ContainerState, PullImageLayerStage, RestartPolicy
48+
from .const import (
49+
DOCKER_HUB,
50+
DOCKER_HUB_LEGACY,
51+
ContainerState,
52+
PullImageLayerStage,
53+
RestartPolicy,
54+
)
4955
from .manager import CommandReturn, PullLogEntry
5056
from .monitor import DockerContainerStateEvent
5157
from .stats import DockerStats
@@ -184,7 +190,8 @@ def _get_credentials(self, image: str) -> dict:
184190
stored = self.sys_docker.config.registries[registry]
185191
credentials[ATTR_USERNAME] = stored[ATTR_USERNAME]
186192
credentials[ATTR_PASSWORD] = stored[ATTR_PASSWORD]
187-
if registry != DOCKER_HUB:
193+
# Don't include registry for Docker Hub (both official and legacy)
194+
if registry not in (DOCKER_HUB, DOCKER_HUB_LEGACY):
188195
credentials[ATTR_REGISTRY] = registry
189196

190197
_LOGGER.debug(

supervisor/docker/manager.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,10 @@
4949
)
5050
from ..utils.common import FileConfiguration
5151
from ..validate import SCHEMA_DOCKER_CONFIG
52-
from .const import DOCKER_HUB, IMAGE_WITH_HOST, LABEL_MANAGED
52+
from .const import DOCKER_HUB, DOCKER_HUB_LEGACY, LABEL_MANAGED
5353
from .monitor import DockerMonitor
5454
from .network import DockerNetwork
55+
from .utils import get_registry_from_image
5556

5657
_LOGGER: logging.Logger = logging.getLogger(__name__)
5758

@@ -212,19 +213,25 @@ def get_registry_for_image(self, image: str) -> str | None:
212213
213214
Matches the image against configured registries and returns the registry
214215
name if found, or None if no matching credentials are configured.
216+
217+
Uses Docker's domain detection logic from:
218+
vendor/github.com/distribution/reference/normalize.go
215219
"""
216220
if not self.registries:
217221
return None
218222

219223
# Check if image uses a custom registry (e.g., ghcr.io/org/image)
220-
matcher = IMAGE_WITH_HOST.match(image)
221-
if matcher:
222-
registry = matcher.group(1)
224+
registry = get_registry_from_image(image)
225+
if registry:
223226
if registry in self.registries:
224227
return registry
225-
# If no registry prefix, check for Docker Hub credentials
226-
elif DOCKER_HUB in self.registries:
227-
return DOCKER_HUB
228+
else:
229+
# No registry prefix means Docker Hub
230+
# Support both docker.io (official) and hub.docker.com (legacy)
231+
if DOCKER_HUB in self.registries:
232+
return DOCKER_HUB
233+
if DOCKER_HUB_LEGACY in self.registries:
234+
return DOCKER_HUB_LEGACY
228235

229236
return None
230237

supervisor/docker/utils.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Docker utilities."""
2+
3+
from __future__ import annotations
4+
5+
import re
6+
7+
# Docker image reference domain regex
8+
# Based on Docker's reference implementation:
9+
# vendor/github.com/distribution/reference/normalize.go
10+
#
11+
# A domain is detected if the part before the first / contains:
12+
# - "localhost" (with optional port)
13+
# - Contains "." (like registry.example.com or 127.0.0.1)
14+
# - Contains ":" (like myregistry:5000)
15+
# - IPv6 addresses in brackets (like [::1]:5000)
16+
#
17+
# Note: Docker also treats uppercase letters as registry indicators since
18+
# namespaces must be lowercase, but this regex handles lowercase matching
19+
# and the get_registry_from_image() function validates the registry rules.
20+
IMAGE_REGISTRY_REGEX = re.compile(
21+
r"^(?P<registry>"
22+
r"localhost(?::[0-9]+)?|" # localhost with optional port
23+
r"(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])" # domain component
24+
r"(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*" # more components
25+
r"(?::[0-9]+)?|" # optional port
26+
r"\[[a-fA-F0-9:]+\](?::[0-9]+)?" # IPv6 with optional port
27+
r")/" # must be followed by /
28+
)
29+
30+
31+
def get_registry_from_image(image_ref: str) -> str | None:
32+
"""Extract registry from Docker image reference.
33+
34+
Returns the registry if the image reference contains one,
35+
or None if the image uses Docker Hub (docker.io).
36+
37+
Based on Docker's reference implementation:
38+
vendor/github.com/distribution/reference/normalize.go
39+
40+
Examples:
41+
get_registry_from_image("nginx") -> None (docker.io)
42+
get_registry_from_image("library/nginx") -> None (docker.io)
43+
get_registry_from_image("myregistry.com/nginx") -> "myregistry.com"
44+
get_registry_from_image("localhost/myimage") -> "localhost"
45+
get_registry_from_image("localhost:5000/myimage") -> "localhost:5000"
46+
get_registry_from_image("registry.io:5000/org/app:v1") -> "registry.io:5000"
47+
get_registry_from_image("[::1]:5000/myimage") -> "[::1]:5000"
48+
49+
"""
50+
match = IMAGE_REGISTRY_REGEX.match(image_ref)
51+
if match:
52+
registry = match.group("registry")
53+
# Must contain '.' or ':' or be 'localhost' to be a real registry
54+
# This prevents treating "myuser/myimage" as having registry "myuser"
55+
if "." in registry or ":" in registry or registry == "localhost":
56+
return registry
57+
return None # No registry = Docker Hub (docker.io)

tests/docker/test_credentials.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,49 @@
11
"""Test docker login."""
22

3+
import pytest
4+
35
# pylint: disable=protected-access
46
from supervisor.coresys import CoreSys
5-
from supervisor.docker.const import DOCKER_HUB
7+
from supervisor.docker.const import DOCKER_HUB, DOCKER_HUB_LEGACY
68
from supervisor.docker.interface import DockerInterface
9+
from supervisor.docker.utils import get_registry_from_image
10+
11+
12+
@pytest.mark.parametrize(
13+
("image_ref", "expected_registry"),
14+
[
15+
# No registry - Docker Hub images
16+
("nginx", None),
17+
("nginx:latest", None),
18+
("library/nginx", None),
19+
("library/nginx:latest", None),
20+
("homeassistant/amd64-supervisor", None),
21+
("homeassistant/amd64-supervisor:1.2.3", None),
22+
# Registry with dot
23+
("ghcr.io/homeassistant/amd64-supervisor", "ghcr.io"),
24+
("ghcr.io/homeassistant/amd64-supervisor:latest", "ghcr.io"),
25+
("myregistry.com/nginx", "myregistry.com"),
26+
("registry.example.com/org/image:v1", "registry.example.com"),
27+
("127.0.0.1/myimage", "127.0.0.1"),
28+
# Registry with port
29+
("myregistry:5000/myimage", "myregistry:5000"),
30+
("localhost:5000/myimage", "localhost:5000"),
31+
("registry.io:5000/org/app:v1", "registry.io:5000"),
32+
# localhost special case
33+
("localhost/myimage", "localhost"),
34+
("localhost/myimage:tag", "localhost"),
35+
# IPv6
36+
("[::1]:5000/myimage", "[::1]:5000"),
37+
("[2001:db8::1]:5000/myimage:tag", "[2001:db8::1]:5000"),
38+
],
39+
)
40+
def test_get_registry_from_image(image_ref: str, expected_registry: str | None):
41+
"""Test get_registry_from_image extracts registry from image reference.
42+
43+
Based on Docker's reference implementation:
44+
vendor/github.com/distribution/reference/normalize.go
45+
"""
46+
assert get_registry_from_image(image_ref) == expected_registry
747

848

949
def test_no_credentials(coresys: CoreSys, test_docker_interface: DockerInterface):
@@ -47,3 +87,36 @@ def test_matching_credentials(coresys: CoreSys, test_docker_interface: DockerInt
4787
)
4888
assert credentials["username"] == "Spongebob Squarepants"
4989
assert "registry" not in credentials
90+
91+
92+
def test_legacy_docker_hub_credentials(
93+
coresys: CoreSys, test_docker_interface: DockerInterface
94+
):
95+
"""Test legacy hub.docker.com credentials are used for Docker Hub images."""
96+
coresys.docker.config._data["registries"] = {
97+
DOCKER_HUB_LEGACY: {"username": "LegacyUser", "password": "Password1!"},
98+
}
99+
100+
credentials = test_docker_interface._get_credentials(
101+
"homeassistant/amd64-supervisor"
102+
)
103+
assert credentials["username"] == "LegacyUser"
104+
# No registry should be included for Docker Hub
105+
assert "registry" not in credentials
106+
107+
108+
def test_docker_hub_preferred_over_legacy(
109+
coresys: CoreSys, test_docker_interface: DockerInterface
110+
):
111+
"""Test docker.io is preferred over legacy hub.docker.com when both exist."""
112+
coresys.docker.config._data["registries"] = {
113+
DOCKER_HUB: {"username": "NewUser", "password": "Password1!"},
114+
DOCKER_HUB_LEGACY: {"username": "LegacyUser", "password": "Password2!"},
115+
}
116+
117+
credentials = test_docker_interface._get_credentials(
118+
"homeassistant/amd64-supervisor"
119+
)
120+
# docker.io should be preferred
121+
assert credentials["username"] == "NewUser"
122+
assert "registry" not in credentials

0 commit comments

Comments
 (0)