diff --git a/supervisor/addons/build.py b/supervisor/addons/build.py index 41ae7934b6b..d486e138b9a 100644 --- a/supervisor/addons/build.py +++ b/supervisor/addons/build.py @@ -7,9 +7,10 @@ import json import logging from pathlib import Path, PurePath -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Self from awesomeversion import AwesomeVersion +import voluptuous as vol from ..const import ( ATTR_ARGS, @@ -19,7 +20,13 @@ ATTR_SQUASH, ATTR_USERNAME, FILE_SUFFIX_CONFIGURATION, - META_ADDON, + LABEL_ARCH, + LABEL_DESCRIPTION, + LABEL_NAME, + LABEL_TYPE, + LABEL_URL, + LABEL_VERSION, + META_APP, SOCKET_DOCKER, CpuArch, ) @@ -32,7 +39,7 @@ ConfigurationFileError, HassioArchNotFound, ) -from ..utils.common import FileConfiguration, find_one_filetype +from ..utils.common import find_one_filetype, read_json_or_yaml_file from .validate import SCHEMA_BUILD_CONFIG if TYPE_CHECKING: @@ -41,39 +48,72 @@ _LOGGER: logging.Logger = logging.getLogger(__name__) -class AddonBuild(FileConfiguration, CoreSysAttributes): +class AddonBuild(CoreSysAttributes): """Handle build options for add-ons.""" - def __init__(self, coresys: CoreSys, addon: AnyAddon) -> None: + def __init__(self, coresys: CoreSys, addon: AnyAddon, data: dict[str, Any]) -> None: """Initialize Supervisor add-on builder.""" self.coresys: CoreSys = coresys self.addon = addon + self._build_config: dict[str, Any] = data + + @classmethod + async def create(cls, coresys: CoreSys, addon: AnyAddon) -> Self: + """Create an AddonBuild by reading the build configuration from disk.""" + data = await coresys.run_in_executor(cls._read_build_config, addon) + + if data: + _LOGGER.warning( + "App %s uses build.yaml which is deprecated. " + "Move build parameters into the Dockerfile directly.", + addon.slug, + ) + + if data[ATTR_SQUASH]: + _LOGGER.warning( + "Ignoring squash build option for %s as Docker BuildKit" + " does not support it.", + addon.slug, + ) - # Search for build file later in executor - super().__init__(None, SCHEMA_BUILD_CONFIG) + return cls(coresys, addon, data or {}) - def _get_build_file(self) -> Path: - """Get build file. + @staticmethod + def _read_build_config(addon: AnyAddon) -> dict[str, Any] | None: + """Find and read the build configuration file. Must be run in executor. """ try: - return find_one_filetype( - self.addon.path_location, "build", FILE_SUFFIX_CONFIGURATION + build_file = find_one_filetype( + addon.path_location, "build", FILE_SUFFIX_CONFIGURATION ) except ConfigurationFileError: - return self.addon.path_location / "build.json" + # No build config file found, assuming modernized build + return None - async def read_data(self) -> None: - """Load data from file.""" - if not self._file: - self._file = await self.sys_run_in_executor(self._get_build_file) + try: + raw = read_json_or_yaml_file(build_file) + build_config = SCHEMA_BUILD_CONFIG(raw) + except ConfigurationFileError as ex: + _LOGGER.exception( + "Error reading %s build config (%s), using defaults", + addon.slug, + ex, + ) + build_config = SCHEMA_BUILD_CONFIG({}) + except vol.Invalid as ex: + _LOGGER.warning( + "Error parsing %s build config (%s), using defaults", addon.slug, ex + ) + build_config = SCHEMA_BUILD_CONFIG({}) - await super().read_data() + # Default base image is passed in BUILD_FROM only when build.yaml is used + # (this is legacy behavior - without build config, Dockerfile should specify it) + if not build_config[ATTR_BUILD_FROM]: + build_config[ATTR_BUILD_FROM] = "ghcr.io/home-assistant/base:latest" - async def save_data(self): - """Ignore save function.""" - raise RuntimeError() + return build_config @cached_property def arch(self) -> CpuArch: @@ -81,35 +121,32 @@ def arch(self) -> CpuArch: return self.sys_arch.match([self.addon.arch]) @property - def base_image(self) -> str: - """Return base image for this add-on.""" - if not self._data[ATTR_BUILD_FROM]: - return f"ghcr.io/home-assistant/{self.arch!s}-base:latest" + def base_image(self) -> str | None: + """Return base image for this add-on, or None to use Dockerfile default.""" + # No build config (otherwise default is coerced when reading the config) + if not self._build_config.get(ATTR_BUILD_FROM): + return None - if isinstance(self._data[ATTR_BUILD_FROM], str): - return self._data[ATTR_BUILD_FROM] + # Single base image in build config + if isinstance(self._build_config[ATTR_BUILD_FROM], str): + return self._build_config[ATTR_BUILD_FROM] - # Evaluate correct base image - if self.arch not in self._data[ATTR_BUILD_FROM]: + # Dict - per-arch base images in build config + if self.arch not in self._build_config[ATTR_BUILD_FROM]: raise HassioArchNotFound( f"Add-on {self.addon.slug} is not supported on {self.arch}" ) - return self._data[ATTR_BUILD_FROM][self.arch] - - @property - def squash(self) -> bool: - """Return True or False if squash is active.""" - return self._data[ATTR_SQUASH] + return self._build_config[ATTR_BUILD_FROM][self.arch] @property def additional_args(self) -> dict[str, str]: """Return additional Docker build arguments.""" - return self._data[ATTR_ARGS] + return self._build_config.get(ATTR_ARGS, {}) @property def additional_labels(self) -> dict[str, str]: """Return additional Docker labels.""" - return self._data[ATTR_LABELS] + return self._build_config.get(ATTR_LABELS, {}) def get_dockerfile(self) -> Path: """Return Dockerfile path. @@ -144,43 +181,33 @@ def build_is_valid() -> bool: system_arch_list=[arch.value for arch in self.sys_arch.supported], ) from None - def get_docker_config_json(self) -> str | None: - """Generate Docker config.json content with registry credentials for base image. + def _registry_key(self, registry: str) -> str: + """Return the Docker config.json key for a registry.""" + if registry in (DOCKER_HUB, DOCKER_HUB_LEGACY): + return "https://index.docker.io/v1/" + return registry - Returns a JSON string with registry credentials for the base image's registry, - or None if no matching registry is configured. + def _registry_auth(self, registry: str) -> str: + """Return base64-encoded auth string for a registry.""" + stored = self.sys_docker.config.registries[registry] + return base64.b64encode( + f"{stored[ATTR_USERNAME]}:{stored[ATTR_PASSWORD]}".encode() + ).decode() - Raises: - HassioArchNotFound: If the add-on is not supported on the current architecture. + def get_docker_config_json(self) -> str | None: + """Generate Docker config.json content with all configured registry credentials. + Returns a JSON string with registry credentials, or None if no registries + are configured. """ - # Early return before accessing base_image to avoid unnecessary arch lookup if not self.sys_docker.config.registries: return None - registry = self.sys_docker.config.get_registry_for_image(self.base_image) - if not registry: - return None - - stored = self.sys_docker.config.registries[registry] - username = stored[ATTR_USERNAME] - password = stored[ATTR_PASSWORD] - - # Docker config.json uses base64-encoded "username:password" for auth - auth_string = base64.b64encode(f"{username}:{password}".encode()).decode() - - # Use the actual registry URL for the key - # Docker Hub uses "https://index.docker.io/v1/" as the key - # Support both docker.io (official) and hub.docker.com (legacy) - registry_key = ( - "https://index.docker.io/v1/" - if registry in (DOCKER_HUB, DOCKER_HUB_LEGACY) - else registry - ) - - config = {"auths": {registry_key: {"auth": auth_string}}} - - return json.dumps(config) + auths = { + self._registry_key(registry): {"auth": self._registry_auth(registry)} + for registry in self.sys_docker.config.registries + } + return json.dumps({"auths": auths}) def get_docker_args( self, version: AwesomeVersion, image_tag: str, docker_config_path: Path | None @@ -203,27 +230,35 @@ def get_docker_args( ] labels = { - "io.hass.version": version, - "io.hass.arch": self.arch, - "io.hass.type": META_ADDON, - "io.hass.name": self._fix_label("name"), - "io.hass.description": self._fix_label("description"), + LABEL_VERSION: version, + LABEL_ARCH: self.arch, + LABEL_TYPE: META_APP, **self.additional_labels, } + # Set name only if non-empty, could have been set in Dockerfile + if name := self._fix_label("name"): + labels[LABEL_NAME] = name + + # Set description only if non-empty, could have been set in Dockerfile + if description := self._fix_label("description"): + labels[LABEL_DESCRIPTION] = description + if self.addon.url: - labels["io.hass.url"] = self.addon.url + labels[LABEL_URL] = self.addon.url for key, value in labels.items(): build_cmd.extend(["--label", f"{key}={value}"]) build_args = { - "BUILD_FROM": self.base_image, "BUILD_VERSION": version, "BUILD_ARCH": self.arch, **self.additional_args, } + if self.base_image is not None: + build_args["BUILD_FROM"] = self.base_image + for key, value in build_args.items(): build_cmd.extend(["--build-arg", f"{key}={value}"]) diff --git a/supervisor/const.py b/supervisor/const.py index de94e2c9794..996f32458bd 100644 --- a/supervisor/const.py +++ b/supervisor/const.py @@ -65,11 +65,15 @@ DNS_SUFFIX = "local.hass.io" LABEL_ARCH = "io.hass.arch" +LABEL_DESCRIPTION = "io.hass.description" LABEL_MACHINE = "io.hass.machine" +LABEL_NAME = "io.hass.name" LABEL_TYPE = "io.hass.type" +LABEL_URL = "io.hass.url" LABEL_VERSION = "io.hass.version" -META_ADDON = "addon" +META_ADDON = "addon" # legacy label for app +META_APP = "app" META_HOMEASSISTANT = "homeassistant" META_SUPERVISOR = "supervisor" diff --git a/supervisor/docker/addon.py b/supervisor/docker/addon.py index bf4627c8fed..afe97542e1d 100644 --- a/supervisor/docker/addon.py +++ b/supervisor/docker/addon.py @@ -680,16 +680,11 @@ async def install( async def _build(self, version: AwesomeVersion, image: str | None = None) -> None: """Build a Docker container.""" - build_env = await AddonBuild(self.coresys, self.addon).load_config() + build_env = await AddonBuild.create(self.coresys, self.addon) # Check if the build environment is valid, raises if not await build_env.is_valid() _LOGGER.info("Starting build for %s:%s", self.image, version) - if build_env.squash: - _LOGGER.warning( - "Ignoring squash build option for %s as Docker BuildKit does not support it.", - self.addon.slug, - ) addon_image_tag = f"{image or self.addon.image}:{version!s}" diff --git a/tests/addons/test_build.py b/tests/addons/test_build.py index d585bd463e8..e0de0069a97 100644 --- a/tests/addons/test_build.py +++ b/tests/addons/test_build.py @@ -2,6 +2,7 @@ import base64 import json +import logging from pathlib import Path, PurePath from unittest.mock import PropertyMock, patch @@ -17,9 +18,21 @@ from tests.common import is_in_list +def _is_build_arg_in_command(command: list[str], arg_name: str) -> bool: + """Check if a build arg is in docker command.""" + return f"--build-arg {arg_name}=" in " ".join(command) + + +def _is_label_in_command( + command: list[str], label_name: str, label_value: str = "" +) -> bool: + """Check if a label is in docker command.""" + return f"--label {label_name}={label_value}" in " ".join(command) + + async def test_platform_set(coresys: CoreSys, install_addon_ssh: Addon): """Test platform set in container build args.""" - build = await AddonBuild(coresys, install_addon_ssh).load_config() + build = await AddonBuild.create(coresys, install_addon_ssh) with ( patch.object( @@ -43,7 +56,7 @@ async def test_platform_set(coresys: CoreSys, install_addon_ssh: Addon): async def test_dockerfile_evaluation(coresys: CoreSys, install_addon_ssh: Addon): """Test dockerfile path in container build args.""" - build = await AddonBuild(coresys, install_addon_ssh).load_config() + build = await AddonBuild.create(coresys, install_addon_ssh) with ( patch.object( @@ -71,7 +84,7 @@ async def test_dockerfile_evaluation(coresys: CoreSys, install_addon_ssh: Addon) async def test_dockerfile_evaluation_arch(coresys: CoreSys, install_addon_ssh: Addon): """Test dockerfile arch evaluation in container build args.""" - build = await AddonBuild(coresys, install_addon_ssh).load_config() + build = await AddonBuild.create(coresys, install_addon_ssh) with ( patch.object( @@ -99,7 +112,7 @@ async def test_dockerfile_evaluation_arch(coresys: CoreSys, install_addon_ssh: A async def test_build_valid(coresys: CoreSys, install_addon_ssh: Addon): """Test platform set in docker args.""" - build = await AddonBuild(coresys, install_addon_ssh).load_config() + build = await AddonBuild.create(coresys, install_addon_ssh) with ( patch.object( type(coresys.arch), "supported", new=PropertyMock(return_value=["aarch64"]) @@ -113,7 +126,7 @@ async def test_build_valid(coresys: CoreSys, install_addon_ssh: Addon): async def test_build_invalid(coresys: CoreSys, install_addon_ssh: Addon): """Test build not supported because Dockerfile missing for specified architecture.""" - build = await AddonBuild(coresys, install_addon_ssh).load_config() + build = await AddonBuild.create(coresys, install_addon_ssh) with ( patch.object( type(coresys.arch), "supported", new=PropertyMock(return_value=["amd64"]) @@ -128,98 +141,58 @@ async def test_build_invalid(coresys: CoreSys, install_addon_ssh: Addon): 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() + build = await AddonBuild.create(coresys, install_addon_ssh) # 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() +async def test_docker_config_all_registries(coresys: CoreSys, install_addon_ssh: Addon): + """Test docker config includes all configured registries.""" + build = await AddonBuild.create(coresys, install_addon_ssh) - # 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"} + "ghcr.io": {"username": "testuser", "password": "testpass"}, + "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"} - } + config_json = build.get_docker_config_json() + assert config_json is not None - 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 "ghcr.io" in config["auths"] + assert "some.other.registry" in config["auths"] - config = json.loads(config_json) - assert "auths" in config - assert "ghcr.io" in config["auths"] + expected_ghcr = base64.b64encode(b"testuser:testpass").decode() + assert config["auths"]["ghcr.io"]["auth"] == expected_ghcr - # Verify base64-encoded credentials - expected_auth = base64.b64encode(b"testuser:testpass").decode() - assert config["auths"]["ghcr.io"]["auth"] == expected_auth + expected_other = base64.b64encode(b"user:pass").decode() + assert config["auths"]["some.other.registry"]["auth"] == expected_other 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() + """Test docker config uses special URL key for Docker Hub.""" + build = await AddonBuild.create(coresys, install_addon_ssh) - # 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 = 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"] + config = json.loads(config_json) + 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 + 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() + build = await AddonBuild.create(coresys, install_addon_ssh) with ( patch.object( @@ -256,7 +229,7 @@ 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() + build = await AddonBuild.create(coresys, install_addon_ssh) with ( patch.object( @@ -280,3 +253,206 @@ async def test_docker_args_without_config_path( # Verify no docker config mount for mount in args["mounts"]: assert mount.target != "/root/.docker/config.json" + + +async def test_build_file_deprecation_warning( + coresys: CoreSys, install_addon_ssh: Addon, caplog: pytest.LogCaptureFixture +): + """Test deprecation warning is logged when build.yaml exists.""" + with caplog.at_level(logging.WARNING): + await AddonBuild.create(coresys, install_addon_ssh) + assert "uses build.yaml which is deprecated" in caplog.text + + +async def test_no_build_file_no_deprecation_warning( + coresys: CoreSys, + install_addon_ssh: Addon, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +): + """Test no deprecation warning when no build file exists.""" + dockerfile = tmp_path / "Dockerfile" + dockerfile.write_text("ARG BUILD_FROM=ghcr.io/home-assistant/base:latest\n") + + with ( + patch.object( + type(install_addon_ssh), + "path_location", + new=PropertyMock(return_value=tmp_path), + ), + caplog.at_level(logging.WARNING), + ): + await AddonBuild.create(coresys, install_addon_ssh) + assert "uses build.yaml which is deprecated" not in caplog.text + + +async def test_no_build_yaml_base_image_none( + coresys: CoreSys, install_addon_ssh: Addon, tmp_path: Path +): + """Test base_image is None when no build file exists.""" + dockerfile = tmp_path / "Dockerfile" + dockerfile.write_text("ARG BUILD_FROM=ghcr.io/home-assistant/base:latest\n") + + with patch.object( + type(install_addon_ssh), + "path_location", + new=PropertyMock(return_value=tmp_path), + ): + build = await AddonBuild.create(coresys, install_addon_ssh) + assert build.base_image is None + + +async def test_no_build_yaml_no_build_from_arg( + coresys: CoreSys, install_addon_ssh: Addon, tmp_path: Path +): + """Test BUILD_FROM is not in docker args when no build file exists.""" + dockerfile = tmp_path / "Dockerfile" + dockerfile.write_text("ARG BUILD_FROM=ghcr.io/home-assistant/base:latest\n") + + with ( + patch.object( + type(install_addon_ssh), + "path_location", + new=PropertyMock(return_value=tmp_path), + ), + 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=PurePath("/addon/path/on/host"), + ), + ): + build = await AddonBuild.create(coresys, install_addon_ssh) + args = await coresys.run_in_executor( + build.get_docker_args, AwesomeVersion("1.0.0"), "test-image:1.0.0", None + ) + + assert not _is_build_arg_in_command(args["command"], "BUILD_FROM") + assert _is_build_arg_in_command(args["command"], "BUILD_VERSION") + assert _is_build_arg_in_command(args["command"], "BUILD_ARCH") + + +async def test_build_yaml_passes_build_from(coresys: CoreSys, install_addon_ssh: Addon): + """Test BUILD_FROM is in docker args when build.yaml exists.""" + build = await AddonBuild.create(coresys, install_addon_ssh) + + 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=PurePath("/addon/path/on/host"), + ), + ): + args = await coresys.run_in_executor( + build.get_docker_args, AwesomeVersion("1.0.0"), "test-image:1.0.0", None + ) + + assert _is_build_arg_in_command(args["command"], "BUILD_FROM") + assert _is_build_arg_in_command(args["command"], "BUILD_VERSION") + assert _is_build_arg_in_command(args["command"], "BUILD_ARCH") + + +async def test_no_build_yaml_docker_config_includes_registries( + coresys: CoreSys, install_addon_ssh: Addon, tmp_path: Path +): + """Test registries are included in docker config even without build file.""" + dockerfile = tmp_path / "Dockerfile" + dockerfile.write_text("ARG BUILD_FROM=ghcr.io/home-assistant/base:latest\n") + + # pylint: disable-next=protected-access + coresys.docker.config._data["registries"] = { + "ghcr.io": {"username": "ghcr_user", "password": "ghcr_pass"}, + } + + with patch.object( + type(install_addon_ssh), + "path_location", + new=PropertyMock(return_value=tmp_path), + ): + build = await AddonBuild.create(coresys, install_addon_ssh) + config_json = build.get_docker_config_json() + assert config_json is not None + + config = json.loads(config_json) + assert "ghcr.io" in config["auths"] + + +async def test_labels_include_name_and_description( + coresys: CoreSys, install_addon_ssh: Addon +): + """Test name and description labels are included when addon has them set.""" + build = await AddonBuild.create(coresys, install_addon_ssh) + + 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=PurePath("/addon/path/on/host"), + ), + ): + args = await coresys.run_in_executor( + build.get_docker_args, AwesomeVersion("1.0.0"), "test-image:1.0.0", None + ) + + assert _is_label_in_command(args["command"], "io.hass.name", "Terminal & SSH") + assert _is_label_in_command( + args["command"], + "io.hass.description", + "Allow logging in remotely to Home Assistant using SSH", + ) + + +async def test_labels_omit_name_and_description_when_empty( + coresys: CoreSys, install_addon_ssh: Addon +): + """Test name and description labels are omitted when addon has empty values.""" + build = await AddonBuild.create(coresys, install_addon_ssh) + + with ( + patch.object( + type(install_addon_ssh), "name", new=PropertyMock(return_value="") + ), + patch.object( + type(install_addon_ssh), + "description", + new=PropertyMock(return_value=""), + ), + 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=PurePath("/addon/path/on/host"), + ), + ): + args = await coresys.run_in_executor( + build.get_docker_args, AwesomeVersion("1.0.0"), "test-image:1.0.0", None + ) + + assert not _is_label_in_command(args["command"], "io.hass.name") + assert not _is_label_in_command(args["command"], "io.hass.description") + # Core labels should still be present + assert _is_label_in_command(args["command"], "io.hass.version", "1.0.0") + assert _is_label_in_command(args["command"], "io.hass.arch", "amd64") + assert _is_label_in_command(args["command"], "io.hass.type", "app")