Compare commits

...

6 Commits

Author SHA1 Message Date
dependabot[bot]
7ae8dfe587 Bump docker/login-action from 1.14.0 to 1.14.1 (#3479)
Bumps [docker/login-action](https://github.com/docker/login-action) from 1.14.0 to 1.14.1.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v1.14.0...v1.14.1)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-02 10:14:53 +01:00
Mike Degatano
c931a4c3e5 Error response handling & tests (#3473) 2022-03-01 21:52:50 +01:00
Mike Degatano
c58fa816d9 Use enum in arch to platform map (#3474) 2022-03-01 15:23:28 -05:00
dependabot[bot]
557f029aa0 Bump actions/setup-python from 2.3.2 to 3 (#3472)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2.3.2 to 3.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v2.3.2...v3)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-01 09:39:34 +01:00
dependabot[bot]
e8e3cc2f67 Bump docker/login-action from 1.13.0 to 1.14.0 (#3471)
Bumps [docker/login-action](https://github.com/docker/login-action) from 1.13.0 to 1.14.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v1.13.0...v1.14.0)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-01 09:39:24 +01:00
Mike Degatano
b0e4983488 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
2022-03-01 09:38:58 +01:00
10 changed files with 218 additions and 34 deletions

View File

@@ -110,14 +110,14 @@ jobs:
- name: Login to DockerHub
if: needs.init.outputs.publish == 'true'
uses: docker/login-action@v1.13.0
uses: docker/login-action@v1.14.1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: needs.init.outputs.publish == 'true'
uses: docker/login-action@v1.13.0
uses: docker/login-action@v1.14.1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -151,7 +151,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
if: needs.init.outputs.publish == 'true'
uses: actions/setup-python@v2.3.2
uses: actions/setup-python@v3
with:
python-version: ${{ env.DEFAULT_PYTHON }}

View File

@@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@v2.4.0
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v2.3.2
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Restore Python virtual environment
@@ -66,7 +66,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v2.4.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2.3.2
uses: actions/setup-python@v3
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -110,7 +110,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v2.4.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2.3.2
uses: actions/setup-python@v3
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -154,7 +154,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v2.4.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2.3.2
uses: actions/setup-python@v3
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -186,7 +186,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v2.4.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2.3.2
uses: actions/setup-python@v3
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -227,7 +227,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v2.4.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2.3.2
uses: actions/setup-python@v3
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -271,7 +271,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v2.4.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2.3.2
uses: actions/setup-python@v3
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -303,7 +303,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v2.4.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2.3.2
uses: actions/setup-python@v3
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -347,7 +347,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v2.4.0
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2.3.2
uses: actions/setup-python@v3
id: python
with:
python-version: ${{ matrix.python-version }}
@@ -405,7 +405,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v2.4.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2.3.2
uses: actions/setup-python@v3
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}

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 = {
CpuArch.ARMV7: "linux/arm/v7",
CpuArch.ARMHF: "linux/arm/v6",
CpuArch.AARCH64: "linux/arm64",
CpuArch.I386: "linux/386",
CpuArch.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

@@ -21,6 +21,7 @@ _CAS_CMD: str = (
_CACHE: set[tuple[str, str]] = set()
_ATTR_ERROR: Final = "error"
_ATTR_STATUS: Final = "status"
@@ -88,6 +89,9 @@ async def cas_validate(
f"Can't parse CodeNotary output: {data!s} - {err!s}", _LOGGER.error
) from err
if _ATTR_ERROR in data_json:
raise CodeNotaryBackendError(data_json[_ATTR_ERROR], _LOGGER.warning)
if data_json[_ATTR_STATUS] == 0:
_CACHE.add((checksum, signer))
else:

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

View File

@@ -1,11 +1,51 @@
"""Test CodeNotary."""
from __future__ import annotations
from dataclasses import dataclass
from unittest.mock import AsyncMock, Mock, patch
import pytest
from supervisor.exceptions import CodeNotaryUntrusted
from supervisor.exceptions import (
CodeNotaryBackendError,
CodeNotaryError,
CodeNotaryUntrusted,
)
from supervisor.utils.codenotary import calc_checksum, cas_validate
@dataclass
class SubprocessResponse:
"""Class for specifying subprocess exec response."""
returncode: int = 0
data: str = ""
error: str | None = None
exception: Exception | None = None
@pytest.fixture(name="subprocess_exec")
def fixture_subprocess_exec(request):
"""Mock subprocess exec with specific return."""
response = request.param
if response.exception:
communicate_return = AsyncMock(side_effect=response.exception)
else:
err_return = None
if response.error:
err_return = Mock(decode=Mock(return_value=response.error))
communicate_return = AsyncMock(return_value=(response.data, err_return))
exec_return = Mock(returncode=response.returncode, communicate=communicate_return)
with patch(
"supervisor.utils.codenotary.asyncio.create_subprocess_exec",
return_value=exec_return,
) as subprocess_exec:
yield subprocess_exec
def test_checksum_calc():
"""Calc Checkusm as test."""
assert calc_checksum("test") == calc_checksum(b"test")
@@ -30,3 +70,46 @@ async def test_invalid_checksum():
"notary@home-assistant.io",
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
)
@pytest.mark.parametrize(
"subprocess_exec",
[
SubprocessResponse(returncode=1, error="test"),
SubprocessResponse(returncode=0, data='{"error":"asn1: structure error"}'),
],
indirect=True,
)
async def test_cas_backend_error(subprocess_exec):
"""Test backend error executing cas command."""
with pytest.raises(CodeNotaryBackendError):
await cas_validate(
"notary@home-assistant.io",
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
)
@pytest.mark.parametrize(
"subprocess_exec",
[SubprocessResponse(returncode=0, data='{"status":1}')],
indirect=True,
)
async def test_cas_notarized_untrusted(subprocess_exec):
"""Test cas found notarized but untrusted content."""
with pytest.raises(CodeNotaryUntrusted):
await cas_validate(
"notary@home-assistant.io",
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
)
@pytest.mark.parametrize(
"subprocess_exec", [SubprocessResponse(exception=OSError())], indirect=True
)
async def test_cas_exec_os_error(subprocess_exec):
"""Test os error attempting to execute cas command."""
with pytest.raises(CodeNotaryError):
await cas_validate(
"notary@home-assistant.io",
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
)