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:
Mike Degatano 2022-03-01 03:38:58 -05:00 committed by GitHub
parent 205f3a74dd
commit b0e4983488
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 117 additions and 20 deletions

View File

@ -178,7 +178,7 @@ class AddonManager(CoreSysAttributes):
await addon.install_apparmor()
try:
await addon.instance.install(store.version, store.image)
await addon.instance.install(store.version, store.image, arch=addon.arch)
except DockerError as err:
self.data.uninstall(addon)
raise AddonsError() from err

View File

@ -503,6 +503,14 @@ class AddonModel(CoreSysAttributes, ABC):
"""Return list of supported 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
def image(self) -> Optional[str]:
"""Generate image name from data."""
@ -618,11 +626,10 @@ class AddonModel(CoreSysAttributes, ABC):
"""Generate image name from data."""
# Repository with Dockerhub images
if ATTR_IMAGE in config:
arch = self.sys_arch.match(config[ATTR_ARCH])
return config[ATTR_IMAGE].format(arch=arch)
return config[ATTR_IMAGE].format(arch=self.arch)
# 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]:
"""Install this add-on."""

View File

@ -454,3 +454,13 @@ class BusEvent(str, Enum):
HARDWARE_NEW_DEVICE = "hardware_new_device"
HARDWARE_REMOVE_DEVICE = "hardware_remove_device"
class CpuArch(str, Enum):
"""Supported CPU architectures."""
ARMV7 = "armv7"
ARMHF = "armhf"
AARCH64 = "aarch64"
I386 = "i386"
AMD64 = "amd64"

View File

@ -31,6 +31,7 @@ from ..const import (
SYSTEMD_JOURNAL_PERSISTENT,
SYSTEMD_JOURNAL_VOLATILE,
BusEvent,
CpuArch,
)
from ..coresys import CoreSys
from ..exceptions import (
@ -515,7 +516,11 @@ class DockerAddon(DockerInterface):
)
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:
"""Pull Docker image or build it.
@ -524,7 +529,7 @@ class DockerAddon(DockerInterface):
if self.addon.need_build:
self._build(version)
else:
super()._install(version, image, latest)
super()._install(version, image, latest, arch)
def _build(self, version: AwesomeVersion) -> None:
"""Build a Docker container.

View File

@ -1,9 +1,11 @@
"""Interface class for Supervisor Docker object."""
from __future__ import annotations
import asyncio
from contextlib import suppress
import logging
import re
from typing import Any, Awaitable, Optional
from typing import Any, Awaitable
from awesomeversion import AwesomeVersion
from awesomeversion.strategy import AwesomeVersionStrategy
@ -17,6 +19,7 @@ from ..const import (
ATTR_USERNAME,
LABEL_ARCH,
LABEL_VERSION,
CpuArch,
)
from ..coresys import CoreSys, CoreSysAttributes
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,})\/.+")
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):
"""Docker Supervisor interface."""
@ -44,7 +55,7 @@ class DockerInterface(CoreSysAttributes):
def __init__(self, coresys: CoreSys):
"""Initialize Docker base wrapper."""
self.coresys: CoreSys = coresys
self._meta: Optional[dict[str, Any]] = None
self._meta: dict[str, Any] | None = None
self.lock: asyncio.Lock = asyncio.Lock()
@property
@ -53,7 +64,7 @@ class DockerInterface(CoreSysAttributes):
return 10
@property
def name(self) -> Optional[str]:
def name(self) -> str | None:
"""Return name of Docker container."""
return None
@ -77,7 +88,7 @@ class DockerInterface(CoreSysAttributes):
return self.meta_config.get("Labels") or {}
@property
def image(self) -> Optional[str]:
def image(self) -> str | None:
"""Return name of Docker image."""
try:
return self.meta_config["Image"].partition(":")[0]
@ -85,14 +96,14 @@ class DockerInterface(CoreSysAttributes):
return None
@property
def version(self) -> Optional[AwesomeVersion]:
def version(self) -> AwesomeVersion | None:
"""Return version of Docker image."""
if LABEL_VERSION not in self.meta_labels:
return None
return AwesomeVersion(self.meta_labels[LABEL_VERSION])
@property
def arch(self) -> Optional[str]:
def arch(self) -> str | None:
"""Return arch of Docker image."""
return self.meta_labels.get(LABEL_ARCH)
@ -150,19 +161,28 @@ class DockerInterface(CoreSysAttributes):
@process_lock
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."""
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(
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:
"""Pull Docker image.
Need run inside executor.
"""
image = image or self.image
arch = arch or self.sys_arch.supervisor
_LOGGER.info("Downloading docker image %s with tag %s.", image, version)
try:
@ -171,7 +191,10 @@ class DockerInterface(CoreSysAttributes):
self._docker_login(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
try:
@ -378,13 +401,13 @@ class DockerInterface(CoreSysAttributes):
@process_lock
def update(
self, version: AwesomeVersion, image: Optional[str] = None, latest: bool = False
self, version: AwesomeVersion, image: str | None = None, latest: bool = False
) -> Awaitable[None]:
"""Update a Docker image."""
return self.sys_run_in_executor(self._update, version, image, latest)
def _update(
self, version: AwesomeVersion, image: Optional[str] = None, latest: bool = False
self, version: AwesomeVersion, image: str | None = None, latest: bool = False
) -> None:
"""Update a docker image.
@ -428,11 +451,11 @@ class DockerInterface(CoreSysAttributes):
return b""
@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."""
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.
Need run inside executor.

View 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")