1
0
mirror of https://github.com/home-assistant/supervisor.git synced 2025-12-23 11:58:49 +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 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 def _process_pull_image_log( # noqa: C901
self, install_job_id: str, reference: PullLogEntry self, install_job_id: str, reference: PullLogEntry
) -> None: ) -> None:
@@ -403,9 +392,8 @@ class DockerInterface(JobGroup, ABC):
_LOGGER.info("Downloading docker image %s with tag %s.", image, version) _LOGGER.info("Downloading docker image %s with tag %s.", image, version)
try: try:
if self.sys_docker.config.registries: # Get credentials for private registries to pass to aiodocker
# Try login if we have defined credentials credentials = self._get_credentials(image) or None
await self._docker_login(image)
curr_job_id = self.sys_jobs.current.uuid 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 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( docker_image = await self.sys_docker.pull_image(
self.sys_jobs.current.uuid, self.sys_jobs.current.uuid,
image, image,
str(version), str(version),
platform=MAP_ARCH[image_arch], platform=MAP_ARCH[image_arch],
auth=credentials,
) )
# Tag latest # Tag latest

View File

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

View File

@@ -16,7 +16,7 @@ from supervisor.addons.manager import Addon
from supervisor.const import BusEvent, CoreState, CpuArch from supervisor.const import BusEvent, CoreState, CpuArch
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.docker.const import ContainerState 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.manager import PullLogEntry, PullProgressDetail
from supervisor.docker.monitor import DockerContainerStateEvent from supervisor.docker.monitor import DockerContainerStateEvent
from supervisor.exceptions import ( 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"} coresys.docker.images.inspect.return_value = {"Id": "test:1.2.3"}
await test_docker_interface.install(AwesomeVersion("1.2.3"), "test", arch=cpu_arch) await test_docker_interface.install(AwesomeVersion("1.2.3"), "test", arch=cpu_arch)
coresys.docker.images.pull.assert_called_once_with( 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") 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") await test_docker_interface.install(AwesomeVersion("1.2.3"), "test")
coresys.docker.images.pull.assert_called_once_with( 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") 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( @pytest.mark.parametrize(
"attrs,expected", "attrs,expected",
[ [
@@ -319,7 +357,7 @@ async def test_install_fires_progress_events(
): ):
await test_docker_interface.install(AwesomeVersion("1.2.3"), "test") await test_docker_interface.install(AwesomeVersion("1.2.3"), "test")
coresys.docker.images.pull.assert_called_once_with( 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") 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", "ghcr.io/home-assistant/qemux86-64-homeassistant",
"2024.4.0", "2024.4.0",
platform="linux/amd64", platform="linux/amd64",
auth=None,
) )
container.remove.assert_called_once_with(force=True, v=True) 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", "ghcr.io/home-assistant/qemux86-64-homeassistant",
"2024.4.0", "2024.4.0",
platform="linux/amd64", platform="linux/amd64",
auth=None,
) )
container.remove.assert_called_once_with(force=True, v=True) 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: with patch.object(DockerAPI, "pull_image", return_value=img_data) as pull_image:
await plugin.load() await plugin.load()
pull_image.assert_called_once_with( 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) container.remove.assert_called_once_with(force=True, v=True)