diff --git a/supervisor/docker/interface.py b/supervisor/docker/interface.py index a36c7182f..fc2d86256 100644 --- a/supervisor/docker/interface.py +++ b/supervisor/docker/interface.py @@ -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 diff --git a/supervisor/docker/manager.py b/supervisor/docker/manager.py index 55101fb16..6d987bdaf 100644 --- a/supervisor/docker/manager.py +++ b/supervisor/docker/manager.py @@ -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: diff --git a/tests/docker/test_interface.py b/tests/docker/test_interface.py index ac3c1770f..c309d647a 100644 --- a/tests/docker/test_interface.py +++ b/tests/docker/test_interface.py @@ -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") diff --git a/tests/homeassistant/test_core.py b/tests/homeassistant/test_core.py index 069289f3d..6a7ccffe2 100644 --- a/tests/homeassistant/test_core.py +++ b/tests/homeassistant/test_core.py @@ -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) diff --git a/tests/plugins/test_plugin_base.py b/tests/plugins/test_plugin_base.py index 1f0645717..04e825fcc 100644 --- a/tests/plugins/test_plugin_base.py +++ b/tests/plugins/test_plugin_base.py @@ -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)