mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-15 13:16:29 +00:00
Capture exception if image is missing on run (#4621)
* Retry run if image missing and handle fixup * Fix lint and run error test * Remove retry and just capture exception
This commit is contained in:
parent
ab6745bc99
commit
77fd1b4017
@ -501,24 +501,16 @@ class DockerAddon(DockerInterface):
|
|||||||
)
|
)
|
||||||
async def run(self) -> None:
|
async def run(self) -> None:
|
||||||
"""Run Docker image."""
|
"""Run Docker image."""
|
||||||
if await self.is_running():
|
|
||||||
return
|
|
||||||
|
|
||||||
# Security check
|
# Security check
|
||||||
if not self.addon.protected:
|
if not self.addon.protected:
|
||||||
_LOGGER.warning("%s running with disabled protected mode!", self.addon.name)
|
_LOGGER.warning("%s running with disabled protected mode!", self.addon.name)
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
await self.stop()
|
|
||||||
|
|
||||||
# Don't set a hostname if no separate UTS namespace is used
|
# Don't set a hostname if no separate UTS namespace is used
|
||||||
hostname = None if self.uts_mode else self.addon.hostname
|
hostname = None if self.uts_mode else self.addon.hostname
|
||||||
|
|
||||||
# Create & Run container
|
# Create & Run container
|
||||||
try:
|
try:
|
||||||
docker_container = await self.sys_run_in_executor(
|
await self._run(
|
||||||
self.sys_docker.run,
|
|
||||||
self.image,
|
|
||||||
tag=str(self.addon.version),
|
tag=str(self.addon.version),
|
||||||
name=self.name,
|
name=self.name,
|
||||||
hostname=hostname,
|
hostname=hostname,
|
||||||
@ -549,7 +541,6 @@ class DockerAddon(DockerInterface):
|
|||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
self._meta = docker_container.attrs
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Starting Docker add-on %s with version %s", self.image, self.version
|
"Starting Docker add-on %s with version %s", self.image, self.version
|
||||||
)
|
)
|
||||||
|
@ -92,16 +92,7 @@ class DockerAudio(DockerInterface, CoreSysAttributes):
|
|||||||
)
|
)
|
||||||
async def run(self) -> None:
|
async def run(self) -> None:
|
||||||
"""Run Docker image."""
|
"""Run Docker image."""
|
||||||
if await self.is_running():
|
await self._run(
|
||||||
return
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
await self.stop()
|
|
||||||
|
|
||||||
# Create & Run container
|
|
||||||
docker_container = await self.sys_run_in_executor(
|
|
||||||
self.sys_docker.run,
|
|
||||||
self.image,
|
|
||||||
tag=str(self.sys_plugins.audio.version),
|
tag=str(self.sys_plugins.audio.version),
|
||||||
init=False,
|
init=False,
|
||||||
ipv4=self.sys_docker.network.audio,
|
ipv4=self.sys_docker.network.audio,
|
||||||
@ -118,8 +109,6 @@ class DockerAudio(DockerInterface, CoreSysAttributes):
|
|||||||
},
|
},
|
||||||
mounts=self.mounts,
|
mounts=self.mounts,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._meta = docker_container.attrs
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Starting Audio %s with version %s - %s",
|
"Starting Audio %s with version %s - %s",
|
||||||
self.image,
|
self.image,
|
||||||
|
@ -33,16 +33,7 @@ class DockerCli(DockerInterface, CoreSysAttributes):
|
|||||||
)
|
)
|
||||||
async def run(self) -> None:
|
async def run(self) -> None:
|
||||||
"""Run Docker image."""
|
"""Run Docker image."""
|
||||||
if await self.is_running():
|
await self._run(
|
||||||
return
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
await self.stop()
|
|
||||||
|
|
||||||
# Create & Run container
|
|
||||||
docker_container = await self.sys_run_in_executor(
|
|
||||||
self.sys_docker.run,
|
|
||||||
self.image,
|
|
||||||
entrypoint=["/init"],
|
entrypoint=["/init"],
|
||||||
tag=str(self.sys_plugins.cli.version),
|
tag=str(self.sys_plugins.cli.version),
|
||||||
init=False,
|
init=False,
|
||||||
@ -60,8 +51,6 @@ class DockerCli(DockerInterface, CoreSysAttributes):
|
|||||||
ENV_TOKEN: self.sys_plugins.cli.supervisor_token,
|
ENV_TOKEN: self.sys_plugins.cli.supervisor_token,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
self._meta = docker_container.attrs
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Starting CLI %s with version %s - %s",
|
"Starting CLI %s with version %s - %s",
|
||||||
self.image,
|
self.image,
|
||||||
|
@ -35,16 +35,7 @@ class DockerDNS(DockerInterface, CoreSysAttributes):
|
|||||||
)
|
)
|
||||||
async def run(self) -> None:
|
async def run(self) -> None:
|
||||||
"""Run Docker image."""
|
"""Run Docker image."""
|
||||||
if await self.is_running():
|
await self._run(
|
||||||
return
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
await self.stop()
|
|
||||||
|
|
||||||
# Create & Run container
|
|
||||||
docker_container = await self.sys_run_in_executor(
|
|
||||||
self.sys_docker.run,
|
|
||||||
self.image,
|
|
||||||
tag=str(self.sys_plugins.dns.version),
|
tag=str(self.sys_plugins.dns.version),
|
||||||
init=False,
|
init=False,
|
||||||
dns=False,
|
dns=False,
|
||||||
@ -65,8 +56,6 @@ class DockerDNS(DockerInterface, CoreSysAttributes):
|
|||||||
],
|
],
|
||||||
oom_score_adj=-300,
|
oom_score_adj=-300,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._meta = docker_container.attrs
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Starting DNS %s with version %s - %s",
|
"Starting DNS %s with version %s - %s",
|
||||||
self.image,
|
self.image,
|
||||||
|
@ -152,16 +152,7 @@ class DockerHomeAssistant(DockerInterface):
|
|||||||
)
|
)
|
||||||
async def run(self) -> None:
|
async def run(self) -> None:
|
||||||
"""Run Docker image."""
|
"""Run Docker image."""
|
||||||
if await self.is_running():
|
await self._run(
|
||||||
return
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
await self.stop()
|
|
||||||
|
|
||||||
# Create & Run container
|
|
||||||
docker_container = await self.sys_run_in_executor(
|
|
||||||
self.sys_docker.run,
|
|
||||||
self.image,
|
|
||||||
tag=(self.sys_homeassistant.version),
|
tag=(self.sys_homeassistant.version),
|
||||||
name=self.name,
|
name=self.name,
|
||||||
hostname=self.name,
|
hostname=self.name,
|
||||||
@ -186,8 +177,6 @@ class DockerHomeAssistant(DockerInterface):
|
|||||||
tmpfs={"/tmp": ""},
|
tmpfs={"/tmp": ""},
|
||||||
oom_score_adj=-300,
|
oom_score_adj=-300,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._meta = docker_container.attrs
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Starting Home Assistant %s with version %s", self.image, self.version
|
"Starting Home Assistant %s with version %s", self.image, self.version
|
||||||
)
|
)
|
||||||
|
@ -377,6 +377,27 @@ class DockerInterface(JobGroup):
|
|||||||
"""Run Docker image."""
|
"""Run Docker image."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
async def _run(self, **kwargs) -> None:
|
||||||
|
"""Run Docker image with retry inf necessary."""
|
||||||
|
if await self.is_running():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
await self.stop()
|
||||||
|
|
||||||
|
# Create & Run container
|
||||||
|
try:
|
||||||
|
docker_container = await self.sys_run_in_executor(
|
||||||
|
self.sys_docker.run, self.image, **kwargs
|
||||||
|
)
|
||||||
|
except DockerNotFound as err:
|
||||||
|
# If image is missing, capture the exception as this shouldn't happen
|
||||||
|
capture_exception(err)
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Store metadata
|
||||||
|
self._meta = docker_container.attrs
|
||||||
|
|
||||||
@Job(
|
@Job(
|
||||||
name="docker_interface_stop",
|
name="docker_interface_stop",
|
||||||
limit=JobExecutionLimit.GROUP_ONCE,
|
limit=JobExecutionLimit.GROUP_ONCE,
|
||||||
|
@ -38,16 +38,7 @@ class DockerMulticast(DockerInterface, CoreSysAttributes):
|
|||||||
)
|
)
|
||||||
async def run(self) -> None:
|
async def run(self) -> None:
|
||||||
"""Run Docker image."""
|
"""Run Docker image."""
|
||||||
if await self.is_running():
|
await self._run(
|
||||||
return
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
await self.stop()
|
|
||||||
|
|
||||||
# Create & Run container
|
|
||||||
docker_container = await self.sys_run_in_executor(
|
|
||||||
self.sys_docker.run,
|
|
||||||
self.image,
|
|
||||||
tag=str(self.sys_plugins.multicast.version),
|
tag=str(self.sys_plugins.multicast.version),
|
||||||
init=False,
|
init=False,
|
||||||
name=self.name,
|
name=self.name,
|
||||||
@ -59,8 +50,6 @@ class DockerMulticast(DockerInterface, CoreSysAttributes):
|
|||||||
extra_hosts={"supervisor": self.sys_docker.network.supervisor},
|
extra_hosts={"supervisor": self.sys_docker.network.supervisor},
|
||||||
environment={ENV_TIME: self.sys_timezone},
|
environment={ENV_TIME: self.sys_timezone},
|
||||||
)
|
)
|
||||||
|
|
||||||
self._meta = docker_container.attrs
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Starting Multicast %s with version %s - Host", self.image, self.version
|
"Starting Multicast %s with version %s - Host", self.image, self.version
|
||||||
)
|
)
|
||||||
|
@ -35,16 +35,7 @@ class DockerObserver(DockerInterface, CoreSysAttributes):
|
|||||||
)
|
)
|
||||||
async def run(self) -> None:
|
async def run(self) -> None:
|
||||||
"""Run Docker image."""
|
"""Run Docker image."""
|
||||||
if await self.is_running():
|
await self._run(
|
||||||
return
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
await self.stop()
|
|
||||||
|
|
||||||
# Create & Run container
|
|
||||||
docker_container = await self.sys_run_in_executor(
|
|
||||||
self.sys_docker.run,
|
|
||||||
self.image,
|
|
||||||
tag=str(self.sys_plugins.observer.version),
|
tag=str(self.sys_plugins.observer.version),
|
||||||
init=False,
|
init=False,
|
||||||
ipv4=self.sys_docker.network.observer,
|
ipv4=self.sys_docker.network.observer,
|
||||||
@ -63,8 +54,6 @@ class DockerObserver(DockerInterface, CoreSysAttributes):
|
|||||||
ports={"80/tcp": 4357},
|
ports={"80/tcp": 4357},
|
||||||
oom_score_adj=-300,
|
oom_score_adj=-300,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._meta = docker_container.attrs
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Starting Observer %s with version %s - %s",
|
"Starting Observer %s with version %s - %s",
|
||||||
self.image,
|
self.image,
|
||||||
|
57
supervisor/resolution/fixups/addon_execute_repair.py
Normal file
57
supervisor/resolution/fixups/addon_execute_repair.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
"""Helper to fix missing image for addon."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ...coresys import CoreSys
|
||||||
|
from ..const import ContextType, IssueType, SuggestionType
|
||||||
|
from .base import FixupBase
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(coresys: CoreSys) -> FixupBase:
|
||||||
|
"""Check setup function."""
|
||||||
|
return FixupAddonExecuteRepair(coresys)
|
||||||
|
|
||||||
|
|
||||||
|
class FixupAddonExecuteRepair(FixupBase):
|
||||||
|
"""Storage class for fixup."""
|
||||||
|
|
||||||
|
async def process_fixup(self, reference: str | None = None) -> None:
|
||||||
|
"""Pull the addons image."""
|
||||||
|
addon = self.sys_addons.get(reference, local_only=True)
|
||||||
|
if not addon:
|
||||||
|
_LOGGER.info(
|
||||||
|
"Cannot repair addon %s as it is not installed, dismissing suggestion",
|
||||||
|
reference,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if await addon.instance.exists():
|
||||||
|
_LOGGER.info(
|
||||||
|
"Addon %s does not need repair, dismissing suggestion", reference
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
_LOGGER.info("Installing image for addon %s")
|
||||||
|
await addon.instance.install(addon.version)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def suggestion(self) -> SuggestionType:
|
||||||
|
"""Return a SuggestionType enum."""
|
||||||
|
return SuggestionType.EXECUTE_REPAIR
|
||||||
|
|
||||||
|
@property
|
||||||
|
def context(self) -> ContextType:
|
||||||
|
"""Return a ContextType enum."""
|
||||||
|
return ContextType.ADDON
|
||||||
|
|
||||||
|
@property
|
||||||
|
def issues(self) -> list[IssueType]:
|
||||||
|
"""Return a IssueType enum list."""
|
||||||
|
return [IssueType.MISSING_IMAGE]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auto(self) -> bool:
|
||||||
|
"""Return if a fixup can be apply as auto fix."""
|
||||||
|
return True
|
@ -39,13 +39,6 @@ def fixture_addonsdata_user() -> dict[str, Data]:
|
|||||||
yield mock
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="os_environ")
|
|
||||||
def fixture_os_environ():
|
|
||||||
"""Mock os.environ."""
|
|
||||||
with patch("supervisor.config.os.environ") as mock:
|
|
||||||
yield mock
|
|
||||||
|
|
||||||
|
|
||||||
def get_docker_addon(
|
def get_docker_addon(
|
||||||
coresys: CoreSys, addonsdata_system: dict[str, Data], config_file: str
|
coresys: CoreSys, addonsdata_system: dict[str, Data], config_file: str
|
||||||
):
|
):
|
||||||
@ -60,7 +53,7 @@ def get_docker_addon(
|
|||||||
|
|
||||||
|
|
||||||
def test_base_volumes_included(
|
def test_base_volumes_included(
|
||||||
coresys: CoreSys, addonsdata_system: dict[str, Data], os_environ
|
coresys: CoreSys, addonsdata_system: dict[str, Data], path_extern
|
||||||
):
|
):
|
||||||
"""Dev and data volumes always included."""
|
"""Dev and data volumes always included."""
|
||||||
docker_addon = get_docker_addon(
|
docker_addon = get_docker_addon(
|
||||||
@ -86,7 +79,7 @@ def test_base_volumes_included(
|
|||||||
|
|
||||||
|
|
||||||
def test_addon_map_folder_defaults(
|
def test_addon_map_folder_defaults(
|
||||||
coresys: CoreSys, addonsdata_system: dict[str, Data], os_environ
|
coresys: CoreSys, addonsdata_system: dict[str, Data], path_extern
|
||||||
):
|
):
|
||||||
"""Validate defaults for mapped folders in addons."""
|
"""Validate defaults for mapped folders in addons."""
|
||||||
docker_addon = get_docker_addon(
|
docker_addon = get_docker_addon(
|
||||||
@ -143,7 +136,7 @@ def test_addon_map_folder_defaults(
|
|||||||
|
|
||||||
|
|
||||||
def test_journald_addon(
|
def test_journald_addon(
|
||||||
coresys: CoreSys, addonsdata_system: dict[str, Data], os_environ
|
coresys: CoreSys, addonsdata_system: dict[str, Data], path_extern
|
||||||
):
|
):
|
||||||
"""Validate volume for journald option."""
|
"""Validate volume for journald option."""
|
||||||
docker_addon = get_docker_addon(
|
docker_addon = get_docker_addon(
|
||||||
@ -171,7 +164,7 @@ def test_journald_addon(
|
|||||||
|
|
||||||
|
|
||||||
def test_not_journald_addon(
|
def test_not_journald_addon(
|
||||||
coresys: CoreSys, addonsdata_system: dict[str, Data], os_environ
|
coresys: CoreSys, addonsdata_system: dict[str, Data], path_extern
|
||||||
):
|
):
|
||||||
"""Validate journald option defaults off."""
|
"""Validate journald option defaults off."""
|
||||||
docker_addon = get_docker_addon(
|
docker_addon = get_docker_addon(
|
||||||
@ -182,10 +175,7 @@ def test_not_journald_addon(
|
|||||||
|
|
||||||
|
|
||||||
async def test_addon_run_docker_error(
|
async def test_addon_run_docker_error(
|
||||||
coresys: CoreSys,
|
coresys: CoreSys, addonsdata_system: dict[str, Data], path_extern
|
||||||
addonsdata_system: dict[str, Data],
|
|
||||||
capture_exception: Mock,
|
|
||||||
os_environ,
|
|
||||||
):
|
):
|
||||||
"""Test docker error when addon is run."""
|
"""Test docker error when addon is run."""
|
||||||
await coresys.dbus.timedate.connect(coresys.dbus.bus)
|
await coresys.dbus.timedate.connect(coresys.dbus.bus)
|
||||||
@ -203,14 +193,13 @@ async def test_addon_run_docker_error(
|
|||||||
Issue(IssueType.MISSING_IMAGE, ContextType.ADDON, reference="test_addon")
|
Issue(IssueType.MISSING_IMAGE, ContextType.ADDON, reference="test_addon")
|
||||||
in coresys.resolution.issues
|
in coresys.resolution.issues
|
||||||
)
|
)
|
||||||
capture_exception.assert_not_called()
|
|
||||||
|
|
||||||
|
|
||||||
async def test_addon_run_add_host_error(
|
async def test_addon_run_add_host_error(
|
||||||
coresys: CoreSys,
|
coresys: CoreSys,
|
||||||
addonsdata_system: dict[str, Data],
|
addonsdata_system: dict[str, Data],
|
||||||
capture_exception: Mock,
|
capture_exception: Mock,
|
||||||
os_environ,
|
path_extern,
|
||||||
):
|
):
|
||||||
"""Test error adding host when addon is run."""
|
"""Test error adding host when addon is run."""
|
||||||
await coresys.dbus.timedate.connect(coresys.dbus.bus)
|
await coresys.dbus.timedate.connect(coresys.dbus.bus)
|
||||||
|
@ -10,12 +10,18 @@ from docker.models.images import Image
|
|||||||
import pytest
|
import pytest
|
||||||
from requests import RequestException
|
from requests import RequestException
|
||||||
|
|
||||||
|
from supervisor.addons import Addon
|
||||||
from supervisor.const import BusEvent, CpuArch
|
from supervisor.const import BusEvent, CpuArch
|
||||||
from supervisor.coresys import CoreSys
|
from supervisor.coresys import CoreSys
|
||||||
from supervisor.docker.const import ContainerState
|
from supervisor.docker.const import ContainerState
|
||||||
from supervisor.docker.interface import DockerInterface
|
from supervisor.docker.interface import DockerInterface
|
||||||
from supervisor.docker.monitor import DockerContainerStateEvent
|
from supervisor.docker.monitor import DockerContainerStateEvent
|
||||||
from supervisor.exceptions import DockerAPIError, DockerError, DockerRequestError
|
from supervisor.exceptions import (
|
||||||
|
DockerAPIError,
|
||||||
|
DockerError,
|
||||||
|
DockerNotFound,
|
||||||
|
DockerRequestError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
@ -223,3 +229,21 @@ async def test_image_pull_fail(
|
|||||||
)
|
)
|
||||||
|
|
||||||
capture_exception.assert_called_once_with(err)
|
capture_exception.assert_called_once_with(err)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_run_missing_image(
|
||||||
|
coresys: CoreSys,
|
||||||
|
install_addon_ssh: Addon,
|
||||||
|
container: MagicMock,
|
||||||
|
capture_exception: Mock,
|
||||||
|
path_extern,
|
||||||
|
):
|
||||||
|
"""Test run captures the exception when image is missing."""
|
||||||
|
coresys.docker.containers.create.side_effect = [NotFound("missing"), MagicMock()]
|
||||||
|
container.status = "stopped"
|
||||||
|
install_addon_ssh.data["image"] = "test_image"
|
||||||
|
|
||||||
|
with pytest.raises(DockerNotFound):
|
||||||
|
await install_addon_ssh.instance.run()
|
||||||
|
|
||||||
|
capture_exception.assert_called_once()
|
||||||
|
73
tests/resolution/fixup/test_addon_execute_repair.py
Normal file
73
tests/resolution/fixup/test_addon_execute_repair.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
"""Test fixup core execute repair."""
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from docker.errors import NotFound
|
||||||
|
|
||||||
|
from supervisor.addons.addon import Addon
|
||||||
|
from supervisor.coresys import CoreSys
|
||||||
|
from supervisor.docker.addon import DockerAddon
|
||||||
|
from supervisor.docker.interface import DockerInterface
|
||||||
|
from supervisor.docker.manager import DockerAPI
|
||||||
|
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
|
||||||
|
from supervisor.resolution.fixups.addon_execute_repair import FixupAddonExecuteRepair
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fixup(docker: DockerAPI, coresys: CoreSys, install_addon_ssh: Addon):
|
||||||
|
"""Test fixup rebuilds addon's container."""
|
||||||
|
docker.images.get.side_effect = NotFound("missing")
|
||||||
|
install_addon_ssh.data["image"] = "test_image"
|
||||||
|
|
||||||
|
addon_execute_repair = FixupAddonExecuteRepair(coresys)
|
||||||
|
assert addon_execute_repair.auto is True
|
||||||
|
|
||||||
|
coresys.resolution.create_issue(
|
||||||
|
IssueType.MISSING_IMAGE,
|
||||||
|
ContextType.ADDON,
|
||||||
|
reference="local_ssh",
|
||||||
|
suggestions=[SuggestionType.EXECUTE_REPAIR],
|
||||||
|
)
|
||||||
|
with patch.object(DockerInterface, "install") as install:
|
||||||
|
await addon_execute_repair()
|
||||||
|
install.assert_called_once()
|
||||||
|
|
||||||
|
assert not coresys.resolution.issues
|
||||||
|
assert not coresys.resolution.suggestions
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fixup_no_addon(coresys: CoreSys):
|
||||||
|
"""Test fixup dismisses if addon is missing."""
|
||||||
|
addon_execute_repair = FixupAddonExecuteRepair(coresys)
|
||||||
|
assert addon_execute_repair.auto is True
|
||||||
|
|
||||||
|
coresys.resolution.create_issue(
|
||||||
|
IssueType.MISSING_IMAGE,
|
||||||
|
ContextType.ADDON,
|
||||||
|
reference="local_ssh",
|
||||||
|
suggestions=[SuggestionType.EXECUTE_REPAIR],
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(DockerAddon, "install") as install:
|
||||||
|
await addon_execute_repair()
|
||||||
|
install.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fixup_image_exists(
|
||||||
|
docker: DockerAPI, coresys: CoreSys, install_addon_ssh: Addon
|
||||||
|
):
|
||||||
|
"""Test fixup dismisses if image exists."""
|
||||||
|
docker.images.get.return_value = MagicMock()
|
||||||
|
|
||||||
|
addon_execute_repair = FixupAddonExecuteRepair(coresys)
|
||||||
|
assert addon_execute_repair.auto is True
|
||||||
|
|
||||||
|
coresys.resolution.create_issue(
|
||||||
|
IssueType.MISSING_IMAGE,
|
||||||
|
ContextType.ADDON,
|
||||||
|
reference="local_ssh",
|
||||||
|
suggestions=[SuggestionType.EXECUTE_REPAIR],
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(DockerAddon, "install") as install:
|
||||||
|
await addon_execute_repair()
|
||||||
|
install.assert_not_called()
|
Loading…
x
Reference in New Issue
Block a user