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