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:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user