Set bind propagation mode for media (#4308)

* Set bind propagation mode for media

* Add some test cases
This commit is contained in:
Mike Degatano 2023-05-24 09:12:35 -04:00 committed by GitHub
parent bb497c0c9f
commit a7c1693911
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 498 additions and 220 deletions

View File

@ -532,14 +532,14 @@ class AddonModel(CoreSysAttributes, ABC):
return ATTR_IMAGE not in self.data
@property
def map_volumes(self) -> dict[str, str]:
"""Return a dict of {volume: policy} from add-on."""
def map_volumes(self) -> dict[str, bool]:
"""Return a dict of {volume: read-only} from add-on."""
volumes = {}
for volume in self.data[ATTR_MAP]:
result = RE_VOLUME.match(volume)
if not result:
continue
volumes[result.group(1)] = result.group(2) or "ro"
volumes[result.group(1)] = result.group(2) != "rw"
return volumes

View File

@ -181,9 +181,9 @@ class CoreConfig(FileConfiguration):
return PurePath(os.environ[ENV_SUPERVISOR_SHARE])
@property
def path_extern_homeassistant(self) -> str:
def path_extern_homeassistant(self) -> PurePath:
"""Return config path external for Docker."""
return str(PurePath(self.path_extern_supervisor, HOMEASSISTANT_CONFIG))
return PurePath(self.path_extern_supervisor, HOMEASSISTANT_CONFIG)
@property
def path_homeassistant(self) -> Path:
@ -191,9 +191,9 @@ class CoreConfig(FileConfiguration):
return self.path_supervisor / HOMEASSISTANT_CONFIG
@property
def path_extern_ssl(self) -> str:
def path_extern_ssl(self) -> PurePath:
"""Return SSL path external for Docker."""
return str(PurePath(self.path_extern_supervisor, HASSIO_SSL))
return PurePath(self.path_extern_supervisor, HASSIO_SSL)
@property
def path_ssl(self) -> Path:
@ -291,9 +291,9 @@ class CoreConfig(FileConfiguration):
return PurePath(self.path_extern_supervisor, SHARE_DATA)
@property
def path_extern_dns(self) -> str:
def path_extern_dns(self) -> PurePath:
"""Return dns path external for Docker."""
return str(PurePath(self.path_extern_supervisor, DNS_DATA))
return PurePath(self.path_extern_supervisor, DNS_DATA)
@property
def path_dns(self) -> Path:

View File

@ -12,6 +12,7 @@ from typing import TYPE_CHECKING
from awesomeversion import AwesomeVersion
import docker
from docker.types import Mount
import requests
from ..addons.build import AddonBuild
@ -46,12 +47,16 @@ from ..resolution.const import ContextType, IssueType, SuggestionType
from ..utils import process_lock
from ..utils.sentry import capture_exception
from .const import (
DBUS_PATH,
DBUS_VOLUME,
ENV_TIME,
ENV_TOKEN,
ENV_TOKEN_OLD,
MOUNT_DBUS,
MOUNT_DEV,
MOUNT_DOCKER,
MOUNT_UDEV,
Capabilities,
MountType,
PropagationMode,
)
from .interface import DockerInterface
@ -320,74 +325,80 @@ class DockerAddon(DockerInterface):
return None
@property
def volumes(self) -> dict[str, dict[str, str]]:
"""Generate volumes for mappings."""
def mounts(self) -> list[Mount]:
"""Return mounts for container."""
addon_mapping = self.addon.map_volumes
volumes = {
"/dev": {"bind": "/dev", "mode": "ro"},
str(self.addon.path_extern_data): {"bind": "/data", "mode": "rw"},
}
mounts = [
MOUNT_DEV,
Mount(
type=MountType.BIND.value,
source=self.addon.path_extern_data.as_posix(),
target="/data",
read_only=False,
),
]
# setup config mappings
if MAP_CONFIG in addon_mapping:
volumes.update(
{
str(self.sys_config.path_extern_homeassistant): {
"bind": "/config",
"mode": addon_mapping[MAP_CONFIG],
}
}
mounts.append(
Mount(
type=MountType.BIND.value,
source=self.sys_config.path_extern_homeassistant.as_posix(),
target="/config",
read_only=addon_mapping[MAP_CONFIG],
)
)
if MAP_SSL in addon_mapping:
volumes.update(
{
str(self.sys_config.path_extern_ssl): {
"bind": "/ssl",
"mode": addon_mapping[MAP_SSL],
}
}
mounts.append(
Mount(
type=MountType.BIND.value,
source=self.sys_config.path_extern_ssl.as_posix(),
target="/ssl",
read_only=addon_mapping[MAP_SSL],
)
)
if MAP_ADDONS in addon_mapping:
volumes.update(
{
str(self.sys_config.path_extern_addons_local): {
"bind": "/addons",
"mode": addon_mapping[MAP_ADDONS],
}
}
mounts.append(
Mount(
type=MountType.BIND.value,
source=self.sys_config.path_extern_addons_local.as_posix(),
target="/addons",
read_only=addon_mapping[MAP_ADDONS],
)
)
if MAP_BACKUP in addon_mapping:
volumes.update(
{
str(self.sys_config.path_extern_backup): {
"bind": "/backup",
"mode": addon_mapping[MAP_BACKUP],
}
}
mounts.append(
Mount(
type=MountType.BIND.value,
source=self.sys_config.path_extern_backup.as_posix(),
target="/backup",
read_only=addon_mapping[MAP_BACKUP],
)
)
if MAP_SHARE in addon_mapping:
volumes.update(
{
str(self.sys_config.path_extern_share): {
"bind": "/share",
"mode": addon_mapping[MAP_SHARE],
}
}
mounts.append(
Mount(
type=MountType.BIND.value,
source=self.sys_config.path_extern_share.as_posix(),
target="/share",
read_only=addon_mapping[MAP_SHARE],
)
)
if MAP_MEDIA in addon_mapping:
volumes.update(
{
str(self.sys_config.path_extern_media): {
"bind": "/media",
"mode": addon_mapping[MAP_MEDIA],
}
}
mounts.append(
Mount(
type=MountType.BIND.value,
source=self.sys_config.path_extern_media.as_posix(),
target="/media",
read_only=addon_mapping[MAP_MEDIA],
propagation=PropagationMode.SLAVE.value,
)
)
# Init other hardware mappings
@ -397,72 +408,90 @@ class DockerAddon(DockerInterface):
for gpio_path in ("/sys/class/gpio", "/sys/devices/platform/soc"):
if not Path(gpio_path).exists():
continue
volumes.update({gpio_path: {"bind": gpio_path, "mode": "rw"}})
mounts.append(
Mount(
type=MountType.BIND.value,
source=gpio_path,
target=gpio_path,
read_only=False,
)
)
# DeviceTree support
if self.addon.with_devicetree:
volumes.update(
{
"/sys/firmware/devicetree/base": {
"bind": "/device-tree",
"mode": "ro",
}
}
mounts.append(
Mount(
type=MountType.BIND.value,
source="/sys/firmware/devicetree/base",
target="/device-tree",
read_only=True,
)
)
# Host udev support
if self.addon.with_udev:
volumes.update({"/run/udev": {"bind": "/run/udev", "mode": "ro"}})
mounts.append(MOUNT_UDEV)
# Kernel Modules support
if self.addon.with_kernel_modules:
volumes.update({"/lib/modules": {"bind": "/lib/modules", "mode": "ro"}})
mounts.append(
Mount(
type=MountType.BIND.value,
source="/lib/modules",
target="/lib/modules",
read_only=True,
)
)
# Docker API support
if not self.addon.protected and self.addon.access_docker_api:
volumes.update(
{"/run/docker.sock": {"bind": "/run/docker.sock", "mode": "ro"}}
)
mounts.append(MOUNT_DOCKER)
# Host D-Bus system
if self.addon.host_dbus:
volumes.update({DBUS_PATH: DBUS_VOLUME})
mounts.append(MOUNT_DBUS)
# Configuration Audio
if self.addon.with_audio:
volumes.update(
{
str(self.addon.path_extern_pulse): {
"bind": "/etc/pulse/client.conf",
"mode": "ro",
},
str(self.sys_plugins.audio.path_extern_pulse): {
"bind": "/run/audio",
"mode": "ro",
},
str(self.sys_plugins.audio.path_extern_asound): {
"bind": "/etc/asound.conf",
"mode": "ro",
},
}
)
mounts += [
Mount(
type=MountType.BIND.value,
source=self.sys_homeassistant.path_extern_pulse.as_posix(),
target="/etc/pulse/client.conf",
read_only=True,
),
Mount(
type=MountType.BIND.value,
source=self.sys_plugins.audio.path_extern_pulse.as_posix(),
target="/run/audio",
read_only=True,
),
Mount(
type=MountType.BIND.value,
source=self.sys_plugins.audio.path_extern_asound.as_posix(),
target="/etc/asound.conf",
read_only=True,
),
]
# System Journal access
if self.addon.with_journald:
volumes.update(
{
str(SYSTEMD_JOURNAL_PERSISTENT): {
"bind": str(SYSTEMD_JOURNAL_PERSISTENT),
"mode": "ro",
},
str(SYSTEMD_JOURNAL_VOLATILE): {
"bind": str(SYSTEMD_JOURNAL_VOLATILE),
"mode": "ro",
},
}
)
mounts += [
Mount(
type=MountType.BIND.value,
source=SYSTEMD_JOURNAL_PERSISTENT.as_posix(),
target=SYSTEMD_JOURNAL_PERSISTENT.as_posix(),
read_only=True,
),
Mount(
type=MountType.BIND.value,
source=SYSTEMD_JOURNAL_VOLATILE.as_posix(),
target=SYSTEMD_JOURNAL_VOLATILE.as_posix(),
read_only=True,
),
]
return volumes
return mounts
def _run(self) -> None:
"""Run Docker image.
@ -503,7 +532,7 @@ class DockerAddon(DockerInterface):
cpu_rt_runtime=self.cpu_rt_runtime,
security_opt=self.security_opt,
environment=self.environment,
volumes=self.volumes,
mounts=self.mounts,
tmpfs=self.tmpfs,
oom_score_adj=200,
)

View File

@ -2,11 +2,20 @@
import logging
import docker
from docker.types import Mount
from ..const import DOCKER_CPU_RUNTIME_ALLOCATION, MACHINE_ID
from ..coresys import CoreSysAttributes
from ..hardware.const import PolicyGroup
from .const import ENV_TIME, Capabilities
from .const import (
ENV_TIME,
MOUNT_DBUS,
MOUNT_DEV,
MOUNT_MACHINE_ID,
MOUNT_UDEV,
Capabilities,
MountType,
)
from .interface import DockerInterface
_LOGGER: logging.Logger = logging.getLogger(__name__)
@ -28,20 +37,25 @@ class DockerAudio(DockerInterface, CoreSysAttributes):
return AUDIO_DOCKER_NAME
@property
def volumes(self) -> dict[str, dict[str, str]]:
"""Return Volumes for the mount."""
volumes = {
"/dev": {"bind": "/dev", "mode": "ro"},
str(self.sys_config.path_extern_audio): {"bind": "/data", "mode": "rw"},
"/run/dbus": {"bind": "/run/dbus", "mode": "ro"},
"/run/udev": {"bind": "/run/udev", "mode": "ro"},
}
def mounts(self) -> list[Mount]:
"""Return mounts for container."""
mounts = [
MOUNT_DEV,
Mount(
type=MountType.BIND.value,
source=self.sys_config.path_extern_audio.as_posix(),
target="/data",
read_only=False,
),
MOUNT_DBUS,
MOUNT_UDEV,
]
# Machine ID
if MACHINE_ID.exists():
volumes.update({str(MACHINE_ID): {"bind": str(MACHINE_ID), "mode": "ro"}})
mounts.append(MOUNT_MACHINE_ID)
return volumes
return mounts
@property
def cgroups_rules(self) -> list[str]:
@ -96,7 +110,7 @@ class DockerAudio(DockerInterface, CoreSysAttributes):
environment={
ENV_TIME: self.sys_timezone,
},
volumes=self.volumes,
mounts=self.mounts,
)
self._meta = docker_container.attrs

View File

@ -1,6 +1,10 @@
"""Docker constants."""
from enum import Enum
from docker.types import Mount
from ..const import MACHINE_ID
class Capabilities(str, Enum):
"""Linux Capabilities."""
@ -40,11 +44,50 @@ class RestartPolicy(str, Enum):
ALWAYS = "always"
DBUS_PATH = "/run/dbus"
DBUS_VOLUME = {"bind": DBUS_PATH, "mode": "ro"}
class MountType(str, Enum):
"""Mount type."""
BIND = "bind"
VOLUME = "volume"
TMPFS = "tmpfs"
NPIPE = "npipe"
class PropagationMode(str, Enum):
"""Propagataion mode, only for bind type mounts."""
PRIVATE = "private"
SHARED = "shared"
SLAVE = "slave"
RPRIVATE = "rprivate"
RSHARED = "rshared"
RSLAVE = "rslave"
ENV_TIME = "TZ"
ENV_TOKEN = "SUPERVISOR_TOKEN"
ENV_TOKEN_OLD = "HASSIO_TOKEN"
LABEL_MANAGED = "supervisor_managed"
MOUNT_DBUS = Mount(
type=MountType.BIND.value, source="/run/dbus", target="/run/dbus", read_only=True
)
MOUNT_DEV = Mount(
type=MountType.BIND.value, source="/dev", target="/dev", read_only=True
)
MOUNT_DOCKER = Mount(
type=MountType.BIND.value,
source="/run/docker.sock",
target="/run/docker.sock",
read_only=True,
)
MOUNT_MACHINE_ID = Mount(
type=MountType.BIND.value,
source=MACHINE_ID.as_posix(),
target=MACHINE_ID.as_posix(),
read_only=True,
)
MOUNT_UDEV = Mount(
type=MountType.BIND.value, source="/run/udev", target="/run/udev", read_only=True
)

View File

@ -1,8 +1,10 @@
"""DNS docker object."""
import logging
from docker.types import Mount
from ..coresys import CoreSysAttributes
from .const import DBUS_PATH, DBUS_VOLUME, ENV_TIME
from .const import ENV_TIME, MOUNT_DBUS, MountType
from .interface import DockerInterface
_LOGGER: logging.Logger = logging.getLogger(__name__)
@ -46,10 +48,15 @@ class DockerDNS(DockerInterface, CoreSysAttributes):
detach=True,
security_opt=self.security_opt,
environment={ENV_TIME: self.sys_timezone},
volumes={
str(self.sys_config.path_extern_dns): {"bind": "/config", "mode": "rw"},
DBUS_PATH: DBUS_VOLUME,
},
mounts=[
Mount(
type=MountType.BIND.value,
source=self.sys_config.path_extern_dns.as_posix(),
target="/config",
read_only=False,
),
MOUNT_DBUS,
],
oom_score_adj=-300,
)

View File

@ -5,13 +5,24 @@ import logging
from awesomeversion import AwesomeVersion, AwesomeVersionCompareException
import docker
from docker.types import Mount
import requests
from ..const import LABEL_MACHINE, MACHINE_ID
from ..exceptions import DockerError
from ..hardware.const import PolicyGroup
from ..homeassistant.const import LANDINGPAGE
from .const import ENV_TIME, ENV_TOKEN, ENV_TOKEN_OLD
from .const import (
ENV_TIME,
ENV_TOKEN,
ENV_TOKEN_OLD,
MOUNT_DBUS,
MOUNT_DEV,
MOUNT_MACHINE_ID,
MOUNT_UDEV,
MountType,
PropagationMode,
)
from .interface import CommandReturn, DockerInterface
_LOGGER: logging.Logger = logging.getLogger(__name__)
@ -62,56 +73,64 @@ class DockerHomeAssistant(DockerInterface):
)
@property
def volumes(self) -> dict[str, dict[str, str]]:
"""Return Volumes for the mount."""
volumes = {
"/dev": {"bind": "/dev", "mode": "ro"},
"/run/dbus": {"bind": "/run/dbus", "mode": "ro"},
"/run/udev": {"bind": "/run/udev", "mode": "ro"},
}
# Add folders
volumes.update(
{
str(self.sys_config.path_extern_homeassistant): {
"bind": "/config",
"mode": "rw",
},
str(self.sys_config.path_extern_ssl): {"bind": "/ssl", "mode": "ro"},
str(self.sys_config.path_extern_share): {
"bind": "/share",
"mode": "rw",
},
str(self.sys_config.path_extern_media): {
"bind": "/media",
"mode": "rw",
},
}
)
def mounts(self) -> list[Mount]:
"""Return mounts for container."""
mounts = [
MOUNT_DEV,
MOUNT_DBUS,
MOUNT_UDEV,
# Add folders
Mount(
type=MountType.BIND.value,
source=self.sys_config.path_extern_homeassistant.as_posix(),
target="/config",
read_only=False,
),
Mount(
type=MountType.BIND.value,
source=self.sys_config.path_extern_ssl.as_posix(),
target="/ssl",
read_only=True,
),
Mount(
type=MountType.BIND.value,
source=self.sys_config.path_extern_share.as_posix(),
target="/share",
read_only=False,
),
Mount(
type=MountType.BIND.value,
source=self.sys_config.path_extern_media.as_posix(),
target="/media",
read_only=False,
propagation=PropagationMode.SLAVE.value,
),
# Configuration audio
Mount(
type=MountType.BIND.value,
source=self.sys_homeassistant.path_extern_pulse.as_posix(),
target="/etc/pulse/client.conf",
read_only=True,
),
Mount(
type=MountType.BIND.value,
source=self.sys_plugins.audio.path_extern_pulse.as_posix(),
target="/run/audio",
read_only=True,
),
Mount(
type=MountType.BIND.value,
source=self.sys_plugins.audio.path_extern_asound.as_posix(),
target="/etc/asound.conf",
read_only=True,
),
]
# Machine ID
if MACHINE_ID.exists():
volumes.update({str(MACHINE_ID): {"bind": str(MACHINE_ID), "mode": "ro"}})
mounts.append(MOUNT_MACHINE_ID)
# Configuration Audio
volumes.update(
{
str(self.sys_homeassistant.path_extern_pulse): {
"bind": "/etc/pulse/client.conf",
"mode": "ro",
},
str(self.sys_plugins.audio.path_extern_pulse): {
"bind": "/run/audio",
"mode": "ro",
},
str(self.sys_plugins.audio.path_extern_asound): {
"bind": "/etc/asound.conf",
"mode": "ro",
},
}
)
return volumes
return mounts
def _run(self) -> None:
"""Run Docker image.
@ -135,7 +154,7 @@ class DockerHomeAssistant(DockerInterface):
init=False,
security_opt=self.security_opt,
network_mode="host",
volumes=self.volumes,
mounts=self.mounts,
device_cgroup_rules=self.cgroups_rules,
extra_hosts={
"supervisor": self.sys_docker.network.supervisor,
@ -172,17 +191,26 @@ class DockerHomeAssistant(DockerInterface):
detach=True,
stdout=True,
stderr=True,
volumes={
str(self.sys_config.path_extern_homeassistant): {
"bind": "/config",
"mode": "rw",
},
str(self.sys_config.path_extern_ssl): {"bind": "/ssl", "mode": "ro"},
str(self.sys_config.path_extern_share): {
"bind": "/share",
"mode": "ro",
},
},
mounts=[
Mount(
type=MountType.BIND.value,
source=self.sys_config.path_extern_homeassistant.as_posix(),
target="/config",
read_only=False,
),
Mount(
type=MountType.BIND.value,
source=self.sys_config.path_extern_ssl.as_posix(),
target="/ssl",
read_only=True,
),
Mount(
type=MountType.BIND.value,
source=self.sys_config.path_extern_share.as_posix(),
target="/share",
read_only=False,
),
],
environment={ENV_TIME: self.sys_timezone},
)

View File

@ -3,7 +3,7 @@ import logging
from ..const import DOCKER_NETWORK_MASK
from ..coresys import CoreSysAttributes
from .const import ENV_TIME, ENV_TOKEN, RestartPolicy
from .const import ENV_TIME, ENV_TOKEN, MOUNT_DOCKER, RestartPolicy
from .interface import DockerInterface
_LOGGER: logging.Logger = logging.getLogger(__name__)
@ -53,7 +53,7 @@ class DockerObserver(DockerInterface, CoreSysAttributes):
ENV_TOKEN: self.sys_plugins.observer.supervisor_token,
ENV_NETWORK_MASK: DOCKER_NETWORK_MASK,
},
volumes={"/run/docker.sock": {"bind": "/run/docker.sock", "mode": "ro"}},
mounts=[MOUNT_DOCKER],
ports={"80/tcp": 4357},
oom_score_adj=-300,
)

View File

@ -2,7 +2,7 @@
import asyncio
from ipaddress import IPv4Address
import logging
from pathlib import Path
from pathlib import Path, PurePath
import shutil
import tarfile
from tempfile import TemporaryDirectory
@ -219,14 +219,14 @@ class HomeAssistant(FileConfiguration, CoreSysAttributes):
self._data[ATTR_REFRESH_TOKEN] = value
@property
def path_pulse(self):
def path_pulse(self) -> Path:
"""Return path to asound config."""
return Path(self.sys_config.path_tmp, "homeassistant_pulse")
@property
def path_extern_pulse(self):
def path_extern_pulse(self) -> PurePath:
"""Return path to asound config for Docker."""
return Path(self.sys_config.path_extern_tmp, "homeassistant_pulse")
return PurePath(self.sys_config.path_extern_tmp, "homeassistant_pulse")
@property
def audio_output(self) -> str | None:

View File

@ -374,6 +374,8 @@ async def tmp_supervisor_data(coresys: CoreSys, tmp_path: Path) -> Path:
coresys.config.path_backup.mkdir()
coresys.config.path_tmp.mkdir()
coresys.config.path_homeassistant.mkdir()
coresys.config.path_audio.mkdir()
coresys.config.path_dns.mkdir()
yield tmp_path

View File

@ -3,13 +3,13 @@ from ipaddress import IPv4Address
from unittest.mock import MagicMock, Mock, PropertyMock, patch
from docker.errors import NotFound
from docker.types import Mount
import pytest
from supervisor.addons import validate as vd
from supervisor.addons.addon import Addon
from supervisor.addons.model import Data
from supervisor.addons.options import AddonOptions
from supervisor.const import SYSTEMD_JOURNAL_PERSISTENT, SYSTEMD_JOURNAL_VOLATILE
from supervisor.coresys import CoreSys
from supervisor.docker.addon import DockerAddon
from supervisor.exceptions import CoreDNSError, DockerNotFound
@ -66,18 +66,23 @@ def test_base_volumes_included(
docker_addon = get_docker_addon(
coresys, addonsdata_system, "basic-addon-config.json"
)
volumes = docker_addon.volumes
# Dev added as ro
assert "/dev" in volumes
assert volumes["/dev"]["bind"] == "/dev"
assert volumes["/dev"]["mode"] == "ro"
assert (
Mount(type="bind", source="/dev", target="/dev", read_only=True)
in docker_addon.mounts
)
# Data added as rw
data_path = str(docker_addon.addon.path_extern_data)
assert data_path in volumes
assert volumes[data_path]["bind"] == "/data"
assert volumes[data_path]["mode"] == "rw"
assert (
Mount(
type="bind",
source=docker_addon.addon.path_extern_data.as_posix(),
target="/data",
read_only=False,
)
in docker_addon.mounts
)
def test_addon_map_folder_defaults(
@ -87,22 +92,42 @@ def test_addon_map_folder_defaults(
docker_addon = get_docker_addon(
coresys, addonsdata_system, "basic-addon-config.json"
)
volumes = docker_addon.volumes
# Config added and is marked rw
config_path = str(docker_addon.sys_config.path_extern_homeassistant)
assert config_path in volumes
assert volumes[config_path]["bind"] == "/config"
assert volumes[config_path]["mode"] == "rw"
assert (
Mount(
type="bind",
source=coresys.config.path_extern_homeassistant.as_posix(),
target="/config",
read_only=False,
)
in docker_addon.mounts
)
# SSL added and defaults to ro
ssl_path = str(docker_addon.sys_config.path_extern_ssl)
assert ssl_path in volumes
assert volumes[ssl_path]["bind"] == "/ssl"
assert volumes[ssl_path]["mode"] == "ro"
assert (
Mount(
type="bind",
source=coresys.config.path_extern_ssl.as_posix(),
target="/ssl",
read_only=True,
)
in docker_addon.mounts
)
# Media added and propagation set
assert (
Mount(
type="bind",
source=coresys.config.path_extern_media.as_posix(),
target="/media",
read_only=True,
propagation="slave",
)
in docker_addon.mounts
)
# Share not mapped
assert str(docker_addon.sys_config.path_extern_share) not in volumes
assert "/share" not in [mount["Target"] for mount in docker_addon.mounts]
def test_journald_addon(
@ -112,18 +137,25 @@ def test_journald_addon(
docker_addon = get_docker_addon(
coresys, addonsdata_system, "journald-addon-config.json"
)
volumes = docker_addon.volumes
assert str(SYSTEMD_JOURNAL_PERSISTENT) in volumes
assert volumes.get(str(SYSTEMD_JOURNAL_PERSISTENT)).get("bind") == str(
SYSTEMD_JOURNAL_PERSISTENT
assert (
Mount(
type="bind",
source="/var/log/journal",
target="/var/log/journal",
read_only=True,
)
in docker_addon.mounts
)
assert volumes.get(str(SYSTEMD_JOURNAL_PERSISTENT)).get("mode") == "ro"
assert str(SYSTEMD_JOURNAL_VOLATILE) in volumes
assert volumes.get(str(SYSTEMD_JOURNAL_VOLATILE)).get("bind") == str(
SYSTEMD_JOURNAL_VOLATILE
assert (
Mount(
type="bind",
source="/run/log/journal",
target="/run/log/journal",
read_only=True,
)
in docker_addon.mounts
)
assert volumes.get(str(SYSTEMD_JOURNAL_VOLATILE)).get("mode") == "ro"
def test_not_journald_addon(
@ -133,9 +165,8 @@ def test_not_journald_addon(
docker_addon = get_docker_addon(
coresys, addonsdata_system, "basic-addon-config.json"
)
volumes = docker_addon.volumes
assert str(SYSTEMD_JOURNAL_PERSISTENT) not in volumes
assert "/var/log/journal" not in [mount["Target"] for mount in docker_addon.mounts]
async def test_addon_run_docker_error(

View File

@ -0,0 +1,48 @@
"""Test audio plugin container."""
from ipaddress import IPv4Address
from pathlib import Path
from unittest.mock import patch
from docker.types import Mount
from supervisor.coresys import CoreSys
from supervisor.docker.manager import DockerAPI
async def test_start(coresys: CoreSys, tmp_supervisor_data: Path, path_extern):
"""Test starting audio plugin."""
config_file = tmp_supervisor_data / "audio" / "pulse_audio.json"
assert not config_file.exists()
with patch.object(DockerAPI, "run") as run:
await coresys.plugins.audio.start()
run.assert_called_once()
assert run.call_args.kwargs["ipv4"] == IPv4Address("172.30.32.4")
assert run.call_args.kwargs["name"] == "hassio_audio"
assert run.call_args.kwargs["hostname"] == "hassio-audio"
assert run.call_args.kwargs["cap_add"] == ["SYS_NICE", "SYS_RESOURCE"]
assert run.call_args.kwargs["ulimits"] == [
{"Name": "rtprio", "Soft": 10, "Hard": 10}
]
assert run.call_args.kwargs["mounts"] == [
Mount(type="bind", source="/dev", target="/dev", read_only=True),
Mount(
type="bind",
source=coresys.config.path_extern_audio.as_posix(),
target="/data",
read_only=False,
),
Mount(type="bind", source="/run/dbus", target="/run/dbus", read_only=True),
Mount(type="bind", source="/run/udev", target="/run/udev", read_only=True),
Mount(
type="bind",
source="/etc/machine-id",
target="/etc/machine-id",
read_only=True,
),
]
assert "volumes" not in run.call_args.kwargs
assert config_file.exists()

38
tests/docker/test_dns.py Normal file
View File

@ -0,0 +1,38 @@
"""Test DNS plugin container."""
from ipaddress import IPv4Address
from pathlib import Path
from unittest.mock import patch
from docker.types import Mount
from supervisor.coresys import CoreSys
from supervisor.docker.manager import DockerAPI
async def test_start(coresys: CoreSys, tmp_supervisor_data: Path, path_extern):
"""Test starting dns plugin."""
config_file = tmp_supervisor_data / "dns" / "coredns.json"
assert not config_file.exists()
with patch.object(DockerAPI, "run") as run:
await coresys.plugins.dns.start()
run.assert_called_once()
assert run.call_args.kwargs["ipv4"] == IPv4Address("172.30.32.3")
assert run.call_args.kwargs["name"] == "hassio_dns"
assert run.call_args.kwargs["hostname"] == "hassio-dns"
assert run.call_args.kwargs["dns"] is False
assert run.call_args.kwargs["oom_score_adj"] == -300
assert run.call_args.kwargs["mounts"] == [
Mount(
type="bind",
source=coresys.config.path_extern_dns.as_posix(),
target="/config",
read_only=False,
),
Mount(type="bind", source="/run/dbus", target="/run/dbus", read_only=True),
]
assert "volumes" not in run.call_args.kwargs
assert config_file.exists()

View File

@ -0,0 +1,38 @@
"""Test Observer plugin container."""
from ipaddress import IPv4Address, ip_network
from unittest.mock import patch
from docker.types import Mount
from supervisor.coresys import CoreSys
from supervisor.docker.manager import DockerAPI
async def test_start(coresys: CoreSys):
"""Test starting observer plugin."""
with patch.object(DockerAPI, "run") as run:
await coresys.plugins.observer.start()
run.assert_called_once()
assert run.call_args.kwargs["ipv4"] == IPv4Address("172.30.32.6")
assert run.call_args.kwargs["name"] == "hassio_observer"
assert run.call_args.kwargs["hostname"] == "hassio-observer"
assert run.call_args.kwargs["restart_policy"] == {"Name": "always"}
assert run.call_args.kwargs["extra_hosts"] == {
"supervisor": IPv4Address("172.30.32.2")
}
assert run.call_args.kwargs["oom_score_adj"] == -300
assert run.call_args.kwargs["environment"]["NETWORK_MASK"] == ip_network(
"172.30.32.0/23"
)
assert run.call_args.kwargs["ports"] == {"80/tcp": 4357}
assert run.call_args.kwargs["mounts"] == [
Mount(
type="bind",
source="/run/docker.sock",
target="/run/docker.sock",
read_only=True,
),
]
assert "volumes" not in run.call_args.kwargs

View File

@ -7,8 +7,8 @@
"url": "https://www.home-assistant.io/",
"startup": "application",
"boot": "auto",
"map": ["config:rw", "ssl"],
"map": ["config:rw", "ssl", "media"],
"options": {},
"schema": {},
"image": "test/{arch}-my-custom-addon"
}
}