mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-12-26 21:47:15 +00:00
Pass registry credentials to add-on build for private base images (#6356)
* Pass registry credentials to add-on build for private base images When building add-ons that use a base image from a private registry, the build would fail because credentials configured via the Supervisor API were not passed to the Docker-in-Docker build container. This fix: - Adds get_docker_config_json() to generate a Docker config.json with registry credentials for the base image - Creates a temporary config file and mounts it into the build container at /root/.docker/config.json so BuildKit can authenticate when pulling the base image - Cleans up the temporary file after build completes Fixes #6354 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix pylint errors * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor registry credential extraction into shared helper Extract duplicate logic for determining which registry matches an image into a shared `get_registry_for_image()` method in `DockerConfig`. This method is now used by both `DockerInterface._get_credentials()` and `AddonBuild.get_docker_config_json()`. Move `DOCKER_HUB` and `IMAGE_WITH_HOST` constants to `docker/const.py` to avoid circular imports between manager.py and interface.py. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * ruff format * Document raises --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Mike Degatano <michael.degatano@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
"""Test addon build."""
|
||||
|
||||
import base64
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import PropertyMock, patch
|
||||
|
||||
from awesomeversion import AwesomeVersion
|
||||
@@ -7,6 +10,7 @@ from awesomeversion import AwesomeVersion
|
||||
from supervisor.addons.addon import Addon
|
||||
from supervisor.addons.build import AddonBuild
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.docker.const import DOCKER_HUB
|
||||
|
||||
from tests.common import is_in_list
|
||||
|
||||
@@ -29,7 +33,7 @@ async def test_platform_set(coresys: CoreSys, install_addon_ssh: Addon):
|
||||
),
|
||||
):
|
||||
args = await coresys.run_in_executor(
|
||||
build.get_docker_args, AwesomeVersion("latest"), "test-image:latest"
|
||||
build.get_docker_args, AwesomeVersion("latest"), "test-image:latest", None
|
||||
)
|
||||
|
||||
assert is_in_list(["--platform", "linux/amd64"], args["command"])
|
||||
@@ -53,7 +57,7 @@ async def test_dockerfile_evaluation(coresys: CoreSys, install_addon_ssh: Addon)
|
||||
),
|
||||
):
|
||||
args = await coresys.run_in_executor(
|
||||
build.get_docker_args, AwesomeVersion("latest"), "test-image:latest"
|
||||
build.get_docker_args, AwesomeVersion("latest"), "test-image:latest", None
|
||||
)
|
||||
|
||||
assert is_in_list(["--file", "Dockerfile"], args["command"])
|
||||
@@ -81,7 +85,7 @@ async def test_dockerfile_evaluation_arch(coresys: CoreSys, install_addon_ssh: A
|
||||
),
|
||||
):
|
||||
args = await coresys.run_in_executor(
|
||||
build.get_docker_args, AwesomeVersion("latest"), "test-image:latest"
|
||||
build.get_docker_args, AwesomeVersion("latest"), "test-image:latest", None
|
||||
)
|
||||
|
||||
assert is_in_list(["--file", "Dockerfile.aarch64"], args["command"])
|
||||
@@ -117,3 +121,158 @@ async def test_build_invalid(coresys: CoreSys, install_addon_ssh: Addon):
|
||||
),
|
||||
):
|
||||
assert not await build.is_valid()
|
||||
|
||||
|
||||
async def test_docker_config_no_registries(coresys: CoreSys, install_addon_ssh: Addon):
|
||||
"""Test docker config generation when no registries configured."""
|
||||
build = await AddonBuild(coresys, install_addon_ssh).load_config()
|
||||
|
||||
# No registries configured by default
|
||||
assert build.get_docker_config_json() is None
|
||||
|
||||
|
||||
async def test_docker_config_no_matching_registry(
|
||||
coresys: CoreSys, install_addon_ssh: Addon
|
||||
):
|
||||
"""Test docker config generation when registry doesn't match base image."""
|
||||
build = await AddonBuild(coresys, install_addon_ssh).load_config()
|
||||
|
||||
# Configure a registry that doesn't match the base image
|
||||
# pylint: disable-next=protected-access
|
||||
coresys.docker.config._data["registries"] = {
|
||||
"some.other.registry": {"username": "user", "password": "pass"}
|
||||
}
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
type(coresys.arch), "supported", new=PropertyMock(return_value=["amd64"])
|
||||
),
|
||||
patch.object(
|
||||
type(coresys.arch), "default", new=PropertyMock(return_value="amd64")
|
||||
),
|
||||
):
|
||||
# Base image is ghcr.io/home-assistant/... which doesn't match
|
||||
assert build.get_docker_config_json() is None
|
||||
|
||||
|
||||
async def test_docker_config_matching_registry(
|
||||
coresys: CoreSys, install_addon_ssh: Addon
|
||||
):
|
||||
"""Test docker config generation when registry matches base image."""
|
||||
build = await AddonBuild(coresys, install_addon_ssh).load_config()
|
||||
|
||||
# Configure ghcr.io registry which matches the default base image
|
||||
# pylint: disable-next=protected-access
|
||||
coresys.docker.config._data["registries"] = {
|
||||
"ghcr.io": {"username": "testuser", "password": "testpass"}
|
||||
}
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
type(coresys.arch), "supported", new=PropertyMock(return_value=["amd64"])
|
||||
),
|
||||
patch.object(
|
||||
type(coresys.arch), "default", new=PropertyMock(return_value="amd64")
|
||||
),
|
||||
):
|
||||
config_json = build.get_docker_config_json()
|
||||
assert config_json is not None
|
||||
|
||||
config = json.loads(config_json)
|
||||
assert "auths" in config
|
||||
assert "ghcr.io" in config["auths"]
|
||||
|
||||
# Verify base64-encoded credentials
|
||||
expected_auth = base64.b64encode(b"testuser:testpass").decode()
|
||||
assert config["auths"]["ghcr.io"]["auth"] == expected_auth
|
||||
|
||||
|
||||
async def test_docker_config_docker_hub(coresys: CoreSys, install_addon_ssh: Addon):
|
||||
"""Test docker config generation for Docker Hub registry."""
|
||||
build = await AddonBuild(coresys, install_addon_ssh).load_config()
|
||||
|
||||
# Configure Docker Hub registry
|
||||
# pylint: disable-next=protected-access
|
||||
coresys.docker.config._data["registries"] = {
|
||||
DOCKER_HUB: {"username": "hubuser", "password": "hubpass"}
|
||||
}
|
||||
|
||||
# Mock base_image to return a Docker Hub image (no registry prefix)
|
||||
with patch.object(
|
||||
type(build),
|
||||
"base_image",
|
||||
new=PropertyMock(return_value="library/alpine:latest"),
|
||||
):
|
||||
config_json = build.get_docker_config_json()
|
||||
assert config_json is not None
|
||||
|
||||
config = json.loads(config_json)
|
||||
# Docker Hub uses special URL as key
|
||||
assert "https://index.docker.io/v1/" in config["auths"]
|
||||
|
||||
expected_auth = base64.b64encode(b"hubuser:hubpass").decode()
|
||||
assert config["auths"]["https://index.docker.io/v1/"]["auth"] == expected_auth
|
||||
|
||||
|
||||
async def test_docker_args_with_config_path(coresys: CoreSys, install_addon_ssh: Addon):
|
||||
"""Test docker args include config volume when path provided."""
|
||||
build = await AddonBuild(coresys, install_addon_ssh).load_config()
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
type(coresys.arch), "supported", new=PropertyMock(return_value=["amd64"])
|
||||
),
|
||||
patch.object(
|
||||
type(coresys.arch), "default", new=PropertyMock(return_value="amd64")
|
||||
),
|
||||
patch.object(
|
||||
type(coresys.config),
|
||||
"local_to_extern_path",
|
||||
side_effect=lambda p: f"/extern{p}",
|
||||
),
|
||||
):
|
||||
config_path = Path("/data/supervisor/tmp/config.json")
|
||||
args = await coresys.run_in_executor(
|
||||
build.get_docker_args,
|
||||
AwesomeVersion("latest"),
|
||||
"test-image:latest",
|
||||
config_path,
|
||||
)
|
||||
|
||||
# Check that config is mounted
|
||||
assert "/extern/data/supervisor/tmp/config.json" in args["volumes"]
|
||||
assert (
|
||||
args["volumes"]["/extern/data/supervisor/tmp/config.json"]["bind"]
|
||||
== "/root/.docker/config.json"
|
||||
)
|
||||
assert args["volumes"]["/extern/data/supervisor/tmp/config.json"]["mode"] == "ro"
|
||||
|
||||
|
||||
async def test_docker_args_without_config_path(
|
||||
coresys: CoreSys, install_addon_ssh: Addon
|
||||
):
|
||||
"""Test docker args don't include config volume when no path provided."""
|
||||
build = await AddonBuild(coresys, install_addon_ssh).load_config()
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
type(coresys.arch), "supported", new=PropertyMock(return_value=["amd64"])
|
||||
),
|
||||
patch.object(
|
||||
type(coresys.arch), "default", new=PropertyMock(return_value="amd64")
|
||||
),
|
||||
patch.object(
|
||||
type(coresys.config),
|
||||
"local_to_extern_path",
|
||||
return_value="/addon/path/on/host",
|
||||
),
|
||||
):
|
||||
args = await coresys.run_in_executor(
|
||||
build.get_docker_args, AwesomeVersion("latest"), "test-image:latest", None
|
||||
)
|
||||
|
||||
# Only docker socket and addon path should be mounted
|
||||
assert len(args["volumes"]) == 2
|
||||
# Verify no docker config mount
|
||||
for bind in args["volumes"].values():
|
||||
assert bind["bind"] != "/root/.docker/config.json"
|
||||
|
||||
Reference in New Issue
Block a user