1
0
mirror of https://github.com/home-assistant/supervisor.git synced 2025-12-23 03:48:57 +00:00

Fix private registry authentication for aiodocker image pulls (#6355)

* Fix private registry authentication for aiodocker image pulls

After PR #6252 migrated image pulling from dockerpy to aiodocker,
private registry authentication stopped working. The old _docker_login()
method stored credentials in ~/.docker/config.json via dockerpy, but
aiodocker doesn't read that file - it requires credentials passed
explicitly via the auth parameter.

Changes:
- Remove unused _docker_login() method (dockerpy login was ineffective)
- Pass credentials directly to pull_image() via new auth parameter
- Add auth parameter to DockerAPI.pull_image() method
- Add unit tests for Docker Hub and custom registry authentication

Fixes #6345

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Ignore protected access in test

* Fix plug-in pull test

* Fix HA core tests

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Stefan Agner
2025-11-26 17:37:24 +01:00
committed by GitHub
parent e06e792e74
commit ae7700f52c
5 changed files with 51 additions and 21 deletions

View File

@@ -208,17 +208,6 @@ class DockerInterface(JobGroup, ABC):
return credentials
async def _docker_login(self, image: str) -> None:
"""Try to log in to the registry if there are credentials available."""
if not self.sys_docker.config.registries:
return
credentials = self._get_credentials(image)
if not credentials:
return
await self.sys_run_in_executor(self.sys_docker.dockerpy.login, **credentials)
def _process_pull_image_log( # noqa: C901
self, install_job_id: str, reference: PullLogEntry
) -> None:
@@ -403,9 +392,8 @@ class DockerInterface(JobGroup, ABC):
_LOGGER.info("Downloading docker image %s with tag %s.", image, version)
try:
if self.sys_docker.config.registries:
# Try login if we have defined credentials
await self._docker_login(image)
# Get credentials for private registries to pass to aiodocker
credentials = self._get_credentials(image) or None
curr_job_id = self.sys_jobs.current.uuid
@@ -421,12 +409,13 @@ class DockerInterface(JobGroup, ABC):
BusEvent.DOCKER_IMAGE_PULL_UPDATE, process_pull_image_log
)
# Pull new image
# Pull new image, passing credentials to aiodocker
docker_image = await self.sys_docker.pull_image(
self.sys_jobs.current.uuid,
image,
str(version),
platform=MAP_ARCH[image_arch],
auth=credentials,
)
# Tag latest

View File

@@ -432,6 +432,7 @@ class DockerAPI(CoreSysAttributes):
repository: str,
tag: str = "latest",
platform: str | None = None,
auth: dict[str, str] | None = None,
) -> dict[str, Any]:
"""Pull the specified image and return it.
@@ -441,7 +442,7 @@ class DockerAPI(CoreSysAttributes):
on the bus so listeners can use that to update status for users.
"""
async for e in self.images.pull(
repository, tag=tag, platform=platform, stream=True
repository, tag=tag, platform=platform, auth=auth, stream=True
):
entry = PullLogEntry.from_pull_log_dict(job_id, e)
if entry.error:

View File

@@ -16,7 +16,7 @@ from supervisor.addons.manager import Addon
from supervisor.const import BusEvent, CoreState, CpuArch
from supervisor.coresys import CoreSys
from supervisor.docker.const import ContainerState
from supervisor.docker.interface import DockerInterface
from supervisor.docker.interface import DOCKER_HUB, DockerInterface
from supervisor.docker.manager import PullLogEntry, PullProgressDetail
from supervisor.docker.monitor import DockerContainerStateEvent
from supervisor.exceptions import (
@@ -51,7 +51,7 @@ async def test_docker_image_platform(
coresys.docker.images.inspect.return_value = {"Id": "test:1.2.3"}
await test_docker_interface.install(AwesomeVersion("1.2.3"), "test", arch=cpu_arch)
coresys.docker.images.pull.assert_called_once_with(
"test", tag="1.2.3", platform=platform, stream=True
"test", tag="1.2.3", platform=platform, auth=None, stream=True
)
coresys.docker.images.inspect.assert_called_once_with("test:1.2.3")
@@ -68,12 +68,50 @@ async def test_docker_image_default_platform(
):
await test_docker_interface.install(AwesomeVersion("1.2.3"), "test")
coresys.docker.images.pull.assert_called_once_with(
"test", tag="1.2.3", platform="linux/386", stream=True
"test", tag="1.2.3", platform="linux/386", auth=None, stream=True
)
coresys.docker.images.inspect.assert_called_once_with("test:1.2.3")
@pytest.mark.parametrize(
"image,registry_key",
[
("homeassistant/amd64-supervisor", DOCKER_HUB),
("ghcr.io/home-assistant/amd64-supervisor", "ghcr.io"),
],
)
async def test_private_registry_credentials_passed_to_pull(
coresys: CoreSys,
test_docker_interface: DockerInterface,
image: str,
registry_key: str,
):
"""Test credentials for private registries are passed to aiodocker pull."""
coresys.docker.images.inspect.return_value = {"Id": f"{image}:1.2.3"}
# Configure registry credentials
coresys.docker.config._data["registries"] = { # pylint: disable=protected-access
registry_key: {"username": "testuser", "password": "testpass"}
}
with patch.object(
type(coresys.supervisor), "arch", PropertyMock(return_value="amd64")
):
await test_docker_interface.install(
AwesomeVersion("1.2.3"), image, arch=CpuArch.AMD64
)
# Verify credentials were passed to aiodocker
expected_auth = {"username": "testuser", "password": "testpass"}
if registry_key != DOCKER_HUB:
expected_auth["registry"] = registry_key
coresys.docker.images.pull.assert_called_once_with(
image, tag="1.2.3", platform="linux/amd64", auth=expected_auth, stream=True
)
@pytest.mark.parametrize(
"attrs,expected",
[
@@ -319,7 +357,7 @@ async def test_install_fires_progress_events(
):
await test_docker_interface.install(AwesomeVersion("1.2.3"), "test")
coresys.docker.images.pull.assert_called_once_with(
"test", tag="1.2.3", platform="linux/386", stream=True
"test", tag="1.2.3", platform="linux/386", auth=None, stream=True
)
coresys.docker.images.inspect.assert_called_once_with("test:1.2.3")

View File

@@ -477,6 +477,7 @@ async def test_core_loads_wrong_image_for_machine(
"ghcr.io/home-assistant/qemux86-64-homeassistant",
"2024.4.0",
platform="linux/amd64",
auth=None,
)
container.remove.assert_called_once_with(force=True, v=True)
@@ -535,6 +536,7 @@ async def test_core_loads_wrong_image_for_architecture(
"ghcr.io/home-assistant/qemux86-64-homeassistant",
"2024.4.0",
platform="linux/amd64",
auth=None,
)
container.remove.assert_called_once_with(force=True, v=True)

View File

@@ -369,7 +369,7 @@ async def test_load_with_incorrect_image(
with patch.object(DockerAPI, "pull_image", return_value=img_data) as pull_image:
await plugin.load()
pull_image.assert_called_once_with(
ANY, correct_image, "2024.4.0", platform="linux/amd64"
ANY, correct_image, "2024.4.0", platform="linux/amd64", auth=None
)
container.remove.assert_called_once_with(force=True, v=True)