mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-15 05:06:30 +00:00
Passing platform arg on image pull (#3465)
* Passing platform arg on image pull * Passing in addon arch to image pull * Move sys_arch above sys_plugins in setup * Default to supervisor arch * Cleanup from feedback
This commit is contained in:
parent
205f3a74dd
commit
b0e4983488
@ -178,7 +178,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
await addon.install_apparmor()
|
await addon.install_apparmor()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await addon.instance.install(store.version, store.image)
|
await addon.instance.install(store.version, store.image, arch=addon.arch)
|
||||||
except DockerError as err:
|
except DockerError as err:
|
||||||
self.data.uninstall(addon)
|
self.data.uninstall(addon)
|
||||||
raise AddonsError() from err
|
raise AddonsError() from err
|
||||||
|
@ -503,6 +503,14 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
"""Return list of supported machine."""
|
"""Return list of supported machine."""
|
||||||
return self.data.get(ATTR_MACHINE, [])
|
return self.data.get(ATTR_MACHINE, [])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def arch(self) -> str:
|
||||||
|
"""Return architecture to use for the addon's image."""
|
||||||
|
if ATTR_IMAGE in self.data:
|
||||||
|
return self.sys_arch.match(self.data[ATTR_ARCH])
|
||||||
|
|
||||||
|
return self.sys_arch.default
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def image(self) -> Optional[str]:
|
def image(self) -> Optional[str]:
|
||||||
"""Generate image name from data."""
|
"""Generate image name from data."""
|
||||||
@ -618,11 +626,10 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
"""Generate image name from data."""
|
"""Generate image name from data."""
|
||||||
# Repository with Dockerhub images
|
# Repository with Dockerhub images
|
||||||
if ATTR_IMAGE in config:
|
if ATTR_IMAGE in config:
|
||||||
arch = self.sys_arch.match(config[ATTR_ARCH])
|
return config[ATTR_IMAGE].format(arch=self.arch)
|
||||||
return config[ATTR_IMAGE].format(arch=arch)
|
|
||||||
|
|
||||||
# local build
|
# local build
|
||||||
return f"{config[ATTR_REPOSITORY]}/{self.sys_arch.default}-addon-{config[ATTR_SLUG]}"
|
return f"{config[ATTR_REPOSITORY]}/{self.arch}-addon-{config[ATTR_SLUG]}"
|
||||||
|
|
||||||
def install(self) -> Awaitable[None]:
|
def install(self) -> Awaitable[None]:
|
||||||
"""Install this add-on."""
|
"""Install this add-on."""
|
||||||
|
@ -454,3 +454,13 @@ class BusEvent(str, Enum):
|
|||||||
|
|
||||||
HARDWARE_NEW_DEVICE = "hardware_new_device"
|
HARDWARE_NEW_DEVICE = "hardware_new_device"
|
||||||
HARDWARE_REMOVE_DEVICE = "hardware_remove_device"
|
HARDWARE_REMOVE_DEVICE = "hardware_remove_device"
|
||||||
|
|
||||||
|
|
||||||
|
class CpuArch(str, Enum):
|
||||||
|
"""Supported CPU architectures."""
|
||||||
|
|
||||||
|
ARMV7 = "armv7"
|
||||||
|
ARMHF = "armhf"
|
||||||
|
AARCH64 = "aarch64"
|
||||||
|
I386 = "i386"
|
||||||
|
AMD64 = "amd64"
|
||||||
|
@ -31,6 +31,7 @@ from ..const import (
|
|||||||
SYSTEMD_JOURNAL_PERSISTENT,
|
SYSTEMD_JOURNAL_PERSISTENT,
|
||||||
SYSTEMD_JOURNAL_VOLATILE,
|
SYSTEMD_JOURNAL_VOLATILE,
|
||||||
BusEvent,
|
BusEvent,
|
||||||
|
CpuArch,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSys
|
from ..coresys import CoreSys
|
||||||
from ..exceptions import (
|
from ..exceptions import (
|
||||||
@ -515,7 +516,11 @@ class DockerAddon(DockerInterface):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _install(
|
def _install(
|
||||||
self, version: AwesomeVersion, image: str | None = None, latest: bool = False
|
self,
|
||||||
|
version: AwesomeVersion,
|
||||||
|
image: str | None = None,
|
||||||
|
latest: bool = False,
|
||||||
|
arch: CpuArch | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Pull Docker image or build it.
|
"""Pull Docker image or build it.
|
||||||
|
|
||||||
@ -524,7 +529,7 @@ class DockerAddon(DockerInterface):
|
|||||||
if self.addon.need_build:
|
if self.addon.need_build:
|
||||||
self._build(version)
|
self._build(version)
|
||||||
else:
|
else:
|
||||||
super()._install(version, image, latest)
|
super()._install(version, image, latest, arch)
|
||||||
|
|
||||||
def _build(self, version: AwesomeVersion) -> None:
|
def _build(self, version: AwesomeVersion) -> None:
|
||||||
"""Build a Docker container.
|
"""Build a Docker container.
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
"""Interface class for Supervisor Docker object."""
|
"""Interface class for Supervisor Docker object."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from typing import Any, Awaitable, Optional
|
from typing import Any, Awaitable
|
||||||
|
|
||||||
from awesomeversion import AwesomeVersion
|
from awesomeversion import AwesomeVersion
|
||||||
from awesomeversion.strategy import AwesomeVersionStrategy
|
from awesomeversion.strategy import AwesomeVersionStrategy
|
||||||
@ -17,6 +19,7 @@ from ..const import (
|
|||||||
ATTR_USERNAME,
|
ATTR_USERNAME,
|
||||||
LABEL_ARCH,
|
LABEL_ARCH,
|
||||||
LABEL_VERSION,
|
LABEL_VERSION,
|
||||||
|
CpuArch,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from ..exceptions import (
|
from ..exceptions import (
|
||||||
@ -37,6 +40,14 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
|
|||||||
IMAGE_WITH_HOST = re.compile(r"^((?:[a-z0-9]+(?:-[a-z0-9]+)*\.)+[a-z]{2,})\/.+")
|
IMAGE_WITH_HOST = re.compile(r"^((?:[a-z0-9]+(?:-[a-z0-9]+)*\.)+[a-z]{2,})\/.+")
|
||||||
DOCKER_HUB = "hub.docker.com"
|
DOCKER_HUB = "hub.docker.com"
|
||||||
|
|
||||||
|
MAP_ARCH = {
|
||||||
|
"armv7": "linux/arm/v7",
|
||||||
|
"armhf": "linux/arm/v6",
|
||||||
|
"aarch64": "linux/arm64",
|
||||||
|
"i386": "linux/386",
|
||||||
|
"amd64": "linux/amd64",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class DockerInterface(CoreSysAttributes):
|
class DockerInterface(CoreSysAttributes):
|
||||||
"""Docker Supervisor interface."""
|
"""Docker Supervisor interface."""
|
||||||
@ -44,7 +55,7 @@ class DockerInterface(CoreSysAttributes):
|
|||||||
def __init__(self, coresys: CoreSys):
|
def __init__(self, coresys: CoreSys):
|
||||||
"""Initialize Docker base wrapper."""
|
"""Initialize Docker base wrapper."""
|
||||||
self.coresys: CoreSys = coresys
|
self.coresys: CoreSys = coresys
|
||||||
self._meta: Optional[dict[str, Any]] = None
|
self._meta: dict[str, Any] | None = None
|
||||||
self.lock: asyncio.Lock = asyncio.Lock()
|
self.lock: asyncio.Lock = asyncio.Lock()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -53,7 +64,7 @@ class DockerInterface(CoreSysAttributes):
|
|||||||
return 10
|
return 10
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> Optional[str]:
|
def name(self) -> str | None:
|
||||||
"""Return name of Docker container."""
|
"""Return name of Docker container."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -77,7 +88,7 @@ class DockerInterface(CoreSysAttributes):
|
|||||||
return self.meta_config.get("Labels") or {}
|
return self.meta_config.get("Labels") or {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def image(self) -> Optional[str]:
|
def image(self) -> str | None:
|
||||||
"""Return name of Docker image."""
|
"""Return name of Docker image."""
|
||||||
try:
|
try:
|
||||||
return self.meta_config["Image"].partition(":")[0]
|
return self.meta_config["Image"].partition(":")[0]
|
||||||
@ -85,14 +96,14 @@ class DockerInterface(CoreSysAttributes):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def version(self) -> Optional[AwesomeVersion]:
|
def version(self) -> AwesomeVersion | None:
|
||||||
"""Return version of Docker image."""
|
"""Return version of Docker image."""
|
||||||
if LABEL_VERSION not in self.meta_labels:
|
if LABEL_VERSION not in self.meta_labels:
|
||||||
return None
|
return None
|
||||||
return AwesomeVersion(self.meta_labels[LABEL_VERSION])
|
return AwesomeVersion(self.meta_labels[LABEL_VERSION])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def arch(self) -> Optional[str]:
|
def arch(self) -> str | None:
|
||||||
"""Return arch of Docker image."""
|
"""Return arch of Docker image."""
|
||||||
return self.meta_labels.get(LABEL_ARCH)
|
return self.meta_labels.get(LABEL_ARCH)
|
||||||
|
|
||||||
@ -150,19 +161,28 @@ class DockerInterface(CoreSysAttributes):
|
|||||||
|
|
||||||
@process_lock
|
@process_lock
|
||||||
def install(
|
def install(
|
||||||
self, version: AwesomeVersion, image: Optional[str] = None, latest: bool = False
|
self,
|
||||||
|
version: AwesomeVersion,
|
||||||
|
image: str | None = None,
|
||||||
|
latest: bool = False,
|
||||||
|
arch: CpuArch | None = None,
|
||||||
):
|
):
|
||||||
"""Pull docker image."""
|
"""Pull docker image."""
|
||||||
return self.sys_run_in_executor(self._install, version, image, latest)
|
return self.sys_run_in_executor(self._install, version, image, latest, arch)
|
||||||
|
|
||||||
def _install(
|
def _install(
|
||||||
self, version: AwesomeVersion, image: Optional[str] = None, latest: bool = False
|
self,
|
||||||
|
version: AwesomeVersion,
|
||||||
|
image: str | None = None,
|
||||||
|
latest: bool = False,
|
||||||
|
arch: CpuArch | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Pull Docker image.
|
"""Pull Docker image.
|
||||||
|
|
||||||
Need run inside executor.
|
Need run inside executor.
|
||||||
"""
|
"""
|
||||||
image = image or self.image
|
image = image or self.image
|
||||||
|
arch = arch or self.sys_arch.supervisor
|
||||||
|
|
||||||
_LOGGER.info("Downloading docker image %s with tag %s.", image, version)
|
_LOGGER.info("Downloading docker image %s with tag %s.", image, version)
|
||||||
try:
|
try:
|
||||||
@ -171,7 +191,10 @@ class DockerInterface(CoreSysAttributes):
|
|||||||
self._docker_login(image)
|
self._docker_login(image)
|
||||||
|
|
||||||
# Pull new image
|
# Pull new image
|
||||||
docker_image = self.sys_docker.images.pull(f"{image}:{version!s}")
|
docker_image = self.sys_docker.images.pull(
|
||||||
|
f"{image}:{version!s}",
|
||||||
|
platform=MAP_ARCH[arch],
|
||||||
|
)
|
||||||
|
|
||||||
# Validate content
|
# Validate content
|
||||||
try:
|
try:
|
||||||
@ -378,13 +401,13 @@ class DockerInterface(CoreSysAttributes):
|
|||||||
|
|
||||||
@process_lock
|
@process_lock
|
||||||
def update(
|
def update(
|
||||||
self, version: AwesomeVersion, image: Optional[str] = None, latest: bool = False
|
self, version: AwesomeVersion, image: str | None = None, latest: bool = False
|
||||||
) -> Awaitable[None]:
|
) -> Awaitable[None]:
|
||||||
"""Update a Docker image."""
|
"""Update a Docker image."""
|
||||||
return self.sys_run_in_executor(self._update, version, image, latest)
|
return self.sys_run_in_executor(self._update, version, image, latest)
|
||||||
|
|
||||||
def _update(
|
def _update(
|
||||||
self, version: AwesomeVersion, image: Optional[str] = None, latest: bool = False
|
self, version: AwesomeVersion, image: str | None = None, latest: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update a docker image.
|
"""Update a docker image.
|
||||||
|
|
||||||
@ -428,11 +451,11 @@ class DockerInterface(CoreSysAttributes):
|
|||||||
return b""
|
return b""
|
||||||
|
|
||||||
@process_lock
|
@process_lock
|
||||||
def cleanup(self, old_image: Optional[str] = None) -> Awaitable[None]:
|
def cleanup(self, old_image: str | None = None) -> Awaitable[None]:
|
||||||
"""Check if old version exists and cleanup."""
|
"""Check if old version exists and cleanup."""
|
||||||
return self.sys_run_in_executor(self._cleanup, old_image)
|
return self.sys_run_in_executor(self._cleanup, old_image)
|
||||||
|
|
||||||
def _cleanup(self, old_image: Optional[str] = None) -> None:
|
def _cleanup(self, old_image: str | None = None) -> None:
|
||||||
"""Check if old version exists and cleanup.
|
"""Check if old version exists and cleanup.
|
||||||
|
|
||||||
Need run inside executor.
|
Need run inside executor.
|
||||||
|
52
tests/docker/test_interface.py
Normal file
52
tests/docker/test_interface.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
"""Test Docker interface."""
|
||||||
|
from unittest.mock import Mock, PropertyMock, call, patch
|
||||||
|
|
||||||
|
from awesomeversion import AwesomeVersion
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from supervisor.const import CpuArch
|
||||||
|
from supervisor.coresys import CoreSys
|
||||||
|
from supervisor.docker.interface import DockerInterface
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def mock_verify_content(coresys: CoreSys):
|
||||||
|
"""Mock verify_content utility during tests."""
|
||||||
|
with patch.object(
|
||||||
|
coresys.security, "verify_content", return_value=None
|
||||||
|
) as verify_content:
|
||||||
|
yield verify_content
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"cpu_arch, platform",
|
||||||
|
[
|
||||||
|
(CpuArch.ARMV7, "linux/arm/v7"),
|
||||||
|
(CpuArch.ARMHF, "linux/arm/v6"),
|
||||||
|
(CpuArch.AARCH64, "linux/arm64"),
|
||||||
|
(CpuArch.I386, "linux/386"),
|
||||||
|
(CpuArch.AMD64, "linux/amd64"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_docker_image_platform(coresys: CoreSys, cpu_arch: str, platform: str):
|
||||||
|
"""Test platform set correctly from arch."""
|
||||||
|
with patch.object(
|
||||||
|
coresys.docker.images, "pull", return_value=Mock(id="test:1.2.3")
|
||||||
|
) as pull:
|
||||||
|
instance = DockerInterface(coresys)
|
||||||
|
await instance.install(AwesomeVersion("1.2.3"), "test", arch=cpu_arch)
|
||||||
|
assert pull.call_count == 1
|
||||||
|
assert pull.call_args == call("test:1.2.3", platform=platform)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_docker_image_default_platform(coresys: CoreSys):
|
||||||
|
"""Test platform set using supervisor arch when omitted."""
|
||||||
|
with patch.object(
|
||||||
|
type(coresys.supervisor), "arch", PropertyMock(return_value="i386")
|
||||||
|
), patch.object(
|
||||||
|
coresys.docker.images, "pull", return_value=Mock(id="test:1.2.3")
|
||||||
|
) as pull:
|
||||||
|
instance = DockerInterface(coresys)
|
||||||
|
await instance.install(AwesomeVersion("1.2.3"), "test")
|
||||||
|
assert pull.call_count == 1
|
||||||
|
assert pull.call_args == call("test:1.2.3", platform="linux/386")
|
Loading…
x
Reference in New Issue
Block a user