mirror of
https://github.com/home-assistant/supervisor.git
synced 2026-04-17 23:33:35 +01:00
Make app builds work without build.yaml
The builds are moving build configuration into the Dockerfile itself (base image defaults via `ARG`, labels via `LABEL`). This change makes `build.yaml` optional for local app builds while preserving backward compatibility for apps that still define it. Key changes: * Track whether `build.yaml` was found, log a warning if it is. * Skip `BUILD_FROM` build arg when no build file exists, letting the Dockerfile's own `ARG BUILD_FROM=...` default take effect. * Always include all configured registry credentials in docker config instead of matching only the base image's registry. * Only set `io.hass.name` and `io.hass.description` labels when non-empty, as they could be defined in the Dockerfile directly. * Log a deprecation warning when `build.yaml` is present. Refs home-assistant/epics#33
This commit is contained in:
@@ -19,7 +19,13 @@ from ..const import (
|
|||||||
ATTR_SQUASH,
|
ATTR_SQUASH,
|
||||||
ATTR_USERNAME,
|
ATTR_USERNAME,
|
||||||
FILE_SUFFIX_CONFIGURATION,
|
FILE_SUFFIX_CONFIGURATION,
|
||||||
META_ADDON,
|
LABEL_ARCH,
|
||||||
|
LABEL_DESCRIPTION,
|
||||||
|
LABEL_NAME,
|
||||||
|
LABEL_TYPE,
|
||||||
|
LABEL_URL,
|
||||||
|
LABEL_VERSION,
|
||||||
|
META_APP,
|
||||||
SOCKET_DOCKER,
|
SOCKET_DOCKER,
|
||||||
CpuArch,
|
CpuArch,
|
||||||
)
|
)
|
||||||
@@ -48,20 +54,29 @@ class AddonBuild(FileConfiguration, CoreSysAttributes):
|
|||||||
"""Initialize Supervisor add-on builder."""
|
"""Initialize Supervisor add-on builder."""
|
||||||
self.coresys: CoreSys = coresys
|
self.coresys: CoreSys = coresys
|
||||||
self.addon = addon
|
self.addon = addon
|
||||||
|
self._has_build_file: bool = False
|
||||||
|
|
||||||
# Search for build file later in executor
|
# Search for build file later in executor
|
||||||
super().__init__(None, SCHEMA_BUILD_CONFIG)
|
super().__init__(None, SCHEMA_BUILD_CONFIG)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_build_file(self) -> bool:
|
||||||
|
"""Return True if a build configuration file was found on disk."""
|
||||||
|
return self._has_build_file
|
||||||
|
|
||||||
def _get_build_file(self) -> Path:
|
def _get_build_file(self) -> Path:
|
||||||
"""Get build file.
|
"""Get build file.
|
||||||
|
|
||||||
Must be run in executor.
|
Must be run in executor.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return find_one_filetype(
|
result = find_one_filetype(
|
||||||
self.addon.path_location, "build", FILE_SUFFIX_CONFIGURATION
|
self.addon.path_location, "build", FILE_SUFFIX_CONFIGURATION
|
||||||
)
|
)
|
||||||
|
self._has_build_file = True
|
||||||
|
return result
|
||||||
except ConfigurationFileError:
|
except ConfigurationFileError:
|
||||||
|
self._has_build_file = False
|
||||||
return self.addon.path_location / "build.json"
|
return self.addon.path_location / "build.json"
|
||||||
|
|
||||||
async def read_data(self) -> None:
|
async def read_data(self) -> None:
|
||||||
@@ -81,10 +96,12 @@ class AddonBuild(FileConfiguration, CoreSysAttributes):
|
|||||||
return self.sys_arch.match([self.addon.arch])
|
return self.sys_arch.match([self.addon.arch])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def base_image(self) -> str:
|
def base_image(self) -> str | None:
|
||||||
"""Return base image for this add-on."""
|
"""Return base image for this add-on, or None to use Dockerfile default."""
|
||||||
if not self._data[ATTR_BUILD_FROM]:
|
if not self._data[ATTR_BUILD_FROM]:
|
||||||
return f"ghcr.io/home-assistant/{self.arch!s}-base:latest"
|
if self._has_build_file:
|
||||||
|
return "ghcr.io/home-assistant/base:latest"
|
||||||
|
return None
|
||||||
|
|
||||||
if isinstance(self._data[ATTR_BUILD_FROM], str):
|
if isinstance(self._data[ATTR_BUILD_FROM], str):
|
||||||
return self._data[ATTR_BUILD_FROM]
|
return self._data[ATTR_BUILD_FROM]
|
||||||
@@ -144,43 +161,33 @@ class AddonBuild(FileConfiguration, CoreSysAttributes):
|
|||||||
system_arch_list=[arch.value for arch in self.sys_arch.supported],
|
system_arch_list=[arch.value for arch in self.sys_arch.supported],
|
||||||
) from None
|
) from None
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
def get_docker_config_json(self) -> str | None:
|
def get_docker_config_json(self) -> str | None:
|
||||||
"""Generate Docker config.json content with registry credentials for base image.
|
"""Generate Docker config.json content with all configured registry credentials.
|
||||||
|
|
||||||
Returns a JSON string with registry credentials for the base image's registry,
|
|
||||||
or None if no matching registry is configured.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
HassioArchNotFound: If the add-on is not supported on the current architecture.
|
|
||||||
|
|
||||||
|
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:
|
if not self.sys_docker.config.registries:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
registry = self.sys_docker.config.get_registry_for_image(self.base_image)
|
auths = {
|
||||||
if not registry:
|
self._registry_key(registry): {"auth": self._registry_auth(registry)}
|
||||||
return None
|
for registry in self.sys_docker.config.registries
|
||||||
|
}
|
||||||
stored = self.sys_docker.config.registries[registry]
|
return json.dumps({"auths": auths})
|
||||||
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)
|
|
||||||
|
|
||||||
def get_docker_args(
|
def get_docker_args(
|
||||||
self, version: AwesomeVersion, image_tag: str, docker_config_path: Path | None
|
self, version: AwesomeVersion, image_tag: str, docker_config_path: Path | None
|
||||||
@@ -203,27 +210,35 @@ class AddonBuild(FileConfiguration, CoreSysAttributes):
|
|||||||
]
|
]
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
"io.hass.version": version,
|
LABEL_VERSION: version,
|
||||||
"io.hass.arch": self.arch,
|
LABEL_ARCH: self.arch,
|
||||||
"io.hass.type": META_ADDON,
|
LABEL_TYPE: META_APP,
|
||||||
"io.hass.name": self._fix_label("name"),
|
|
||||||
"io.hass.description": self._fix_label("description"),
|
|
||||||
**self.additional_labels,
|
**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:
|
if self.addon.url:
|
||||||
labels["io.hass.url"] = self.addon.url
|
labels[LABEL_URL] = self.addon.url
|
||||||
|
|
||||||
for key, value in labels.items():
|
for key, value in labels.items():
|
||||||
build_cmd.extend(["--label", f"{key}={value}"])
|
build_cmd.extend(["--label", f"{key}={value}"])
|
||||||
|
|
||||||
build_args = {
|
build_args = {
|
||||||
"BUILD_FROM": self.base_image,
|
|
||||||
"BUILD_VERSION": version,
|
"BUILD_VERSION": version,
|
||||||
"BUILD_ARCH": self.arch,
|
"BUILD_ARCH": self.arch,
|
||||||
**self.additional_args,
|
**self.additional_args,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.base_image is not None:
|
||||||
|
build_args["BUILD_FROM"] = self.base_image
|
||||||
|
|
||||||
for key, value in build_args.items():
|
for key, value in build_args.items():
|
||||||
build_cmd.extend(["--build-arg", f"{key}={value}"])
|
build_cmd.extend(["--build-arg", f"{key}={value}"])
|
||||||
|
|
||||||
|
|||||||
@@ -65,11 +65,15 @@ DOCKER_CPU_RUNTIME_ALLOCATION = int(DOCKER_CPU_RUNTIME_TOTAL / 5)
|
|||||||
DNS_SUFFIX = "local.hass.io"
|
DNS_SUFFIX = "local.hass.io"
|
||||||
|
|
||||||
LABEL_ARCH = "io.hass.arch"
|
LABEL_ARCH = "io.hass.arch"
|
||||||
|
LABEL_DESCRIPTION = "io.hass.description"
|
||||||
LABEL_MACHINE = "io.hass.machine"
|
LABEL_MACHINE = "io.hass.machine"
|
||||||
|
LABEL_NAME = "io.hass.name"
|
||||||
LABEL_TYPE = "io.hass.type"
|
LABEL_TYPE = "io.hass.type"
|
||||||
|
LABEL_URL = "io.hass.url"
|
||||||
LABEL_VERSION = "io.hass.version"
|
LABEL_VERSION = "io.hass.version"
|
||||||
|
|
||||||
META_ADDON = "addon"
|
META_ADDON = "addon" # legacy label for app
|
||||||
|
META_APP = "app"
|
||||||
META_HOMEASSISTANT = "homeassistant"
|
META_HOMEASSISTANT = "homeassistant"
|
||||||
META_SUPERVISOR = "supervisor"
|
META_SUPERVISOR = "supervisor"
|
||||||
|
|
||||||
|
|||||||
@@ -684,6 +684,13 @@ class DockerAddon(DockerInterface):
|
|||||||
# Check if the build environment is valid, raises if not
|
# Check if the build environment is valid, raises if not
|
||||||
await build_env.is_valid()
|
await build_env.is_valid()
|
||||||
|
|
||||||
|
if build_env.has_build_file:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Add-on %s uses build.yaml which is deprecated. "
|
||||||
|
"Move build parameters into the Dockerfile directly.",
|
||||||
|
self.addon.slug,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER.info("Starting build for %s:%s", self.image, version)
|
_LOGGER.info("Starting build for %s:%s", self.image, version)
|
||||||
if build_env.squash:
|
if build_env.squash:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
|
|||||||
@@ -17,6 +17,18 @@ from supervisor.exceptions import AddonBuildDockerfileMissingError
|
|||||||
from tests.common import is_in_list
|
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):
|
async def test_platform_set(coresys: CoreSys, install_addon_ssh: Addon):
|
||||||
"""Test platform set in container build args."""
|
"""Test platform set in container build args."""
|
||||||
build = await AddonBuild(coresys, install_addon_ssh).load_config()
|
build = await AddonBuild(coresys, install_addon_ssh).load_config()
|
||||||
@@ -134,87 +146,47 @@ async def test_docker_config_no_registries(coresys: CoreSys, install_addon_ssh:
|
|||||||
assert build.get_docker_config_json() is None
|
assert build.get_docker_config_json() is None
|
||||||
|
|
||||||
|
|
||||||
async def test_docker_config_no_matching_registry(
|
async def test_docker_config_all_registries(coresys: CoreSys, install_addon_ssh: Addon):
|
||||||
coresys: CoreSys, install_addon_ssh: Addon
|
"""Test docker config includes all configured registries."""
|
||||||
):
|
|
||||||
"""Test docker config generation when registry doesn't match base image."""
|
|
||||||
build = await AddonBuild(coresys, install_addon_ssh).load_config()
|
build = await AddonBuild(coresys, install_addon_ssh).load_config()
|
||||||
|
|
||||||
# Configure a registry that doesn't match the base image
|
|
||||||
# pylint: disable-next=protected-access
|
# pylint: disable-next=protected-access
|
||||||
coresys.docker.config._data["registries"] = {
|
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 (
|
config_json = build.get_docker_config_json()
|
||||||
patch.object(
|
assert config_json is not None
|
||||||
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
|
|
||||||
|
|
||||||
|
config = json.loads(config_json)
|
||||||
|
assert "ghcr.io" in config["auths"]
|
||||||
|
assert "some.other.registry" in config["auths"]
|
||||||
|
|
||||||
async def test_docker_config_matching_registry(
|
expected_ghcr = base64.b64encode(b"testuser:testpass").decode()
|
||||||
coresys: CoreSys, install_addon_ssh: Addon
|
assert config["auths"]["ghcr.io"]["auth"] == expected_ghcr
|
||||||
):
|
|
||||||
"""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
|
expected_other = base64.b64encode(b"user:pass").decode()
|
||||||
# pylint: disable-next=protected-access
|
assert config["auths"]["some.other.registry"]["auth"] == expected_other
|
||||||
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):
|
async def test_docker_config_docker_hub(coresys: CoreSys, install_addon_ssh: Addon):
|
||||||
"""Test docker config generation for Docker Hub registry."""
|
"""Test docker config uses special URL key for Docker Hub."""
|
||||||
build = await AddonBuild(coresys, install_addon_ssh).load_config()
|
build = await AddonBuild(coresys, install_addon_ssh).load_config()
|
||||||
|
|
||||||
# Configure Docker Hub registry
|
|
||||||
# pylint: disable-next=protected-access
|
# pylint: disable-next=protected-access
|
||||||
coresys.docker.config._data["registries"] = {
|
coresys.docker.config._data["registries"] = {
|
||||||
DOCKER_HUB: {"username": "hubuser", "password": "hubpass"}
|
DOCKER_HUB: {"username": "hubuser", "password": "hubpass"}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Mock base_image to return a Docker Hub image (no registry prefix)
|
config_json = build.get_docker_config_json()
|
||||||
with patch.object(
|
assert config_json is not None
|
||||||
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)
|
config = json.loads(config_json)
|
||||||
# Docker Hub uses special URL as key
|
assert "https://index.docker.io/v1/" in config["auths"]
|
||||||
assert "https://index.docker.io/v1/" in config["auths"]
|
|
||||||
|
|
||||||
expected_auth = base64.b64encode(b"hubuser:hubpass").decode()
|
expected_auth = base64.b64encode(b"hubuser:hubpass").decode()
|
||||||
assert config["auths"]["https://index.docker.io/v1/"]["auth"] == expected_auth
|
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):
|
async def test_docker_args_with_config_path(coresys: CoreSys, install_addon_ssh: Addon):
|
||||||
@@ -280,3 +252,198 @@ async def test_docker_args_without_config_path(
|
|||||||
# Verify no docker config mount
|
# Verify no docker config mount
|
||||||
for mount in args["mounts"]:
|
for mount in args["mounts"]:
|
||||||
assert mount.target != "/root/.docker/config.json"
|
assert mount.target != "/root/.docker/config.json"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_has_build_file_true(coresys: CoreSys, install_addon_ssh: Addon):
|
||||||
|
"""Test has_build_file is True when build.yaml exists."""
|
||||||
|
build = await AddonBuild(coresys, install_addon_ssh).load_config()
|
||||||
|
assert build.has_build_file is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_has_build_file_false(
|
||||||
|
coresys: CoreSys, install_addon_ssh: Addon, tmp_path: Path
|
||||||
|
):
|
||||||
|
"""Test has_build_file is False when no build file exists."""
|
||||||
|
# Create a minimal addon directory without build.yaml
|
||||||
|
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(coresys, install_addon_ssh).load_config()
|
||||||
|
assert build.has_build_file is False
|
||||||
|
|
||||||
|
|
||||||
|
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(coresys, install_addon_ssh).load_config()
|
||||||
|
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(coresys, install_addon_ssh).load_config()
|
||||||
|
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(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=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(coresys, install_addon_ssh).load_config()
|
||||||
|
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(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=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(coresys, install_addon_ssh).load_config()
|
||||||
|
|
||||||
|
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")
|
||||||
|
|||||||
Reference in New Issue
Block a user