Add hard-coded image fallback for plugins for offline start (#5204)

This commit is contained in:
Mike Degatano 2024-07-25 07:45:38 -04:00 committed by GitHub
parent 4ab4350c58
commit 5ee7d16687
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 98 additions and 53 deletions

View File

@ -2,6 +2,7 @@
Code: https://github.com/home-assistant/plugin-audio Code: https://github.com/home-assistant/plugin-audio
""" """
import errno import errno
import logging import logging
from pathlib import Path, PurePath from pathlib import Path, PurePath
@ -73,7 +74,9 @@ class PluginAudio(PluginBase):
@property @property
def default_image(self) -> str: def default_image(self) -> str:
"""Return default image for audio plugin.""" """Return default image for audio plugin."""
return self.sys_updater.image_audio if self.sys_updater.image_audio:
return self.sys_updater.image_audio
return super().default_image
@property @property
def latest_version(self) -> AwesomeVersion | None: def latest_version(self) -> AwesomeVersion | None:

View File

@ -2,6 +2,7 @@
Code: https://github.com/home-assistant/plugin-cli Code: https://github.com/home-assistant/plugin-cli
""" """
from collections.abc import Awaitable from collections.abc import Awaitable
import logging import logging
import secrets import secrets
@ -42,7 +43,9 @@ class PluginCli(PluginBase):
@property @property
def default_image(self) -> str: def default_image(self) -> str:
"""Return default image for cli plugin.""" """Return default image for cli plugin."""
return self.sys_updater.image_cli if self.sys_updater.image_cli:
return self.sys_updater.image_cli
return super().default_image
@property @property
def latest_version(self) -> AwesomeVersion | None: def latest_version(self) -> AwesomeVersion | None:

View File

@ -2,6 +2,7 @@
Code: https://github.com/home-assistant/plugin-dns Code: https://github.com/home-assistant/plugin-dns
""" """
import asyncio import asyncio
from contextlib import suppress from contextlib import suppress
import errno import errno
@ -111,7 +112,9 @@ class PluginDns(PluginBase):
@property @property
def default_image(self) -> str: def default_image(self) -> str:
"""Return default image for dns plugin.""" """Return default image for dns plugin."""
return self.sys_updater.image_dns if self.sys_updater.image_dns:
return self.sys_updater.image_dns
return super().default_image
@property @property
def latest_version(self) -> AwesomeVersion | None: def latest_version(self) -> AwesomeVersion | None:

View File

@ -2,6 +2,7 @@
Code: https://github.com/home-assistant/plugin-multicast Code: https://github.com/home-assistant/plugin-multicast
""" """
import logging import logging
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
@ -44,7 +45,9 @@ class PluginMulticast(PluginBase):
@property @property
def default_image(self) -> str: def default_image(self) -> str:
"""Return default image for multicast plugin.""" """Return default image for multicast plugin."""
return self.sys_updater.image_multicast if self.sys_updater.image_multicast:
return self.sys_updater.image_multicast
return super().default_image
@property @property
def latest_version(self) -> AwesomeVersion | None: def latest_version(self) -> AwesomeVersion | None:

View File

@ -2,6 +2,7 @@
Code: https://github.com/home-assistant/plugin-observer Code: https://github.com/home-assistant/plugin-observer
""" """
import logging import logging
import secrets import secrets
@ -47,7 +48,9 @@ class PluginObserver(PluginBase):
@property @property
def default_image(self) -> str: def default_image(self) -> str:
"""Return default image for observer plugin.""" """Return default image for observer plugin."""
return self.sys_updater.image_observer if self.sys_updater.image_observer:
return self.sys_updater.image_observer
return super().default_image
@property @property
def latest_version(self) -> AwesomeVersion | None: def latest_version(self) -> AwesomeVersion | None:

View File

@ -1,4 +1,5 @@
"""Test base plugin functionality.""" """Test base plugin functionality."""
import asyncio import asyncio
from unittest.mock import MagicMock, Mock, PropertyMock, patch from unittest.mock import MagicMock, Mock, PropertyMock, patch
@ -60,14 +61,17 @@ async def fixture_plugin(
) )
async def test_plugin_watchdog(coresys: CoreSys, plugin: PluginBase) -> None: async def test_plugin_watchdog(coresys: CoreSys, plugin: PluginBase) -> None:
"""Test plugin watchdog works correctly.""" """Test plugin watchdog works correctly."""
with patch.object(type(plugin.instance), "attach"), patch.object( with (
type(plugin.instance), "is_running", return_value=True patch.object(type(plugin.instance), "attach"),
patch.object(type(plugin.instance), "is_running", return_value=True),
): ):
await plugin.load() await plugin.load()
with patch.object(type(plugin), "rebuild") as rebuild, patch.object( with (
type(plugin), "start" patch.object(type(plugin), "rebuild") as rebuild,
) as start, patch.object(type(plugin.instance), "current_state") as current_state: patch.object(type(plugin), "start") as start,
patch.object(type(plugin.instance), "current_state") as current_state,
):
current_state.return_value = ContainerState.UNHEALTHY current_state.return_value = ContainerState.UNHEALTHY
coresys.bus.fire_event( coresys.bus.fire_event(
BusEvent.DOCKER_CONTAINER_STATE_CHANGE, BusEvent.DOCKER_CONTAINER_STATE_CHANGE,
@ -168,9 +172,10 @@ async def test_plugin_watchdog_max_failed_attempts(
container.status = "stopped" container.status = "stopped"
container.attrs = {"State": {"ExitCode": 1}} container.attrs = {"State": {"ExitCode": 1}}
with patch("supervisor.plugins.base.WATCHDOG_RETRY_SECONDS", 0), patch.object( with (
type(plugin), "start", side_effect=error patch("supervisor.plugins.base.WATCHDOG_RETRY_SECONDS", 0),
) as start: patch.object(type(plugin), "start", side_effect=error) as start,
):
await plugin.watchdog_container( await plugin.watchdog_container(
DockerContainerStateEvent( DockerContainerStateEvent(
name=plugin.instance.name, name=plugin.instance.name,
@ -198,17 +203,18 @@ async def test_plugin_load_running_container(
) -> None: ) -> None:
"""Test plugins load and attach to a running container.""" """Test plugins load and attach to a running container."""
test_version = AwesomeVersion("2022.7.3") test_version = AwesomeVersion("2022.7.3")
with patch.object( with (
type(coresys.bus), "register_event" patch.object(type(coresys.bus), "register_event") as register_event,
) as register_event, patch.object( patch.object(type(plugin.instance), "attach") as attach,
type(plugin.instance), "attach" patch.object(type(plugin), "install") as install,
) as attach, patch.object(type(plugin), "install") as install, patch.object( patch.object(type(plugin), "start") as start,
type(plugin), "start" patch.object(
) as start, patch.object( type(plugin.instance),
type(plugin.instance), "get_latest_version",
"get_latest_version", return_value=test_version,
return_value=test_version, ),
), patch.object(type(plugin.instance), "is_running", return_value=True): patch.object(type(plugin.instance), "is_running", return_value=True),
):
await plugin.load() await plugin.load()
register_event.assert_any_call( register_event.assert_any_call(
BusEvent.DOCKER_CONTAINER_STATE_CHANGE, plugin.watchdog_container BusEvent.DOCKER_CONTAINER_STATE_CHANGE, plugin.watchdog_container
@ -230,17 +236,18 @@ async def test_plugin_load_stopped_container(
) -> None: ) -> None:
"""Test plugins load and start existing container.""" """Test plugins load and start existing container."""
test_version = AwesomeVersion("2022.7.3") test_version = AwesomeVersion("2022.7.3")
with patch.object( with (
type(coresys.bus), "register_event" patch.object(type(coresys.bus), "register_event") as register_event,
) as register_event, patch.object( patch.object(type(plugin.instance), "attach") as attach,
type(plugin.instance), "attach" patch.object(type(plugin), "install") as install,
) as attach, patch.object(type(plugin), "install") as install, patch.object( patch.object(type(plugin), "start") as start,
type(plugin), "start" patch.object(
) as start, patch.object( type(plugin.instance),
type(plugin.instance), "get_latest_version",
"get_latest_version", return_value=test_version,
return_value=test_version, ),
), patch.object(type(plugin.instance), "is_running", return_value=False): patch.object(type(plugin.instance), "is_running", return_value=False),
):
await plugin.load() await plugin.load()
register_event.assert_any_call( register_event.assert_any_call(
BusEvent.DOCKER_CONTAINER_STATE_CHANGE, plugin.watchdog_container BusEvent.DOCKER_CONTAINER_STATE_CHANGE, plugin.watchdog_container
@ -262,17 +269,20 @@ async def test_plugin_load_missing_container(
) -> None: ) -> None:
"""Test plugins load and create and start container.""" """Test plugins load and create and start container."""
test_version = AwesomeVersion("2022.7.3") test_version = AwesomeVersion("2022.7.3")
with patch.object( with (
type(coresys.bus), "register_event" patch.object(type(coresys.bus), "register_event") as register_event,
) as register_event, patch.object( patch.object(
type(plugin.instance), "attach", side_effect=DockerError() type(plugin.instance), "attach", side_effect=DockerError()
) as attach, patch.object(type(plugin), "install") as install, patch.object( ) as attach,
type(plugin), "start" patch.object(type(plugin), "install") as install,
) as start, patch.object( patch.object(type(plugin), "start") as start,
type(plugin.instance), patch.object(
"get_latest_version", type(plugin.instance),
return_value=test_version, "get_latest_version",
), patch.object(type(plugin.instance), "is_running", return_value=False): return_value=test_version,
),
patch.object(type(plugin.instance), "is_running", return_value=False),
):
await plugin.load() await plugin.load()
register_event.assert_any_call( register_event.assert_any_call(
BusEvent.DOCKER_CONTAINER_STATE_CHANGE, plugin.watchdog_container BusEvent.DOCKER_CONTAINER_STATE_CHANGE, plugin.watchdog_container
@ -301,9 +311,12 @@ async def test_update_fails_if_out_of_date(
"""Test update of plugins fail when supervisor is out of date.""" """Test update of plugins fail when supervisor is out of date."""
coresys.hardware.disk.get_disk_free_space = lambda x: 5000 coresys.hardware.disk.get_disk_free_space = lambda x: 5000
with patch.object( with (
type(coresys.supervisor), "need_update", new=PropertyMock(return_value=True) patch.object(
), pytest.raises(error): type(coresys.supervisor), "need_update", new=PropertyMock(return_value=True)
),
pytest.raises(error),
):
await plugin.update() await plugin.update()
@ -316,10 +329,14 @@ async def test_repair_failed(
coresys: CoreSys, capture_exception: Mock, plugin: PluginBase coresys: CoreSys, capture_exception: Mock, plugin: PluginBase
): ):
"""Test repair failed.""" """Test repair failed."""
with patch.object(DockerInterface, "exists", return_value=False), patch.object( with (
DockerInterface, "arch", new=PropertyMock(return_value=CpuArch.AMD64) patch.object(DockerInterface, "exists", return_value=False),
), patch( patch.object(
"supervisor.security.module.cas_validate", side_effect=CodeNotaryUntrusted DockerInterface, "arch", new=PropertyMock(return_value=CpuArch.AMD64)
),
patch(
"supervisor.security.module.cas_validate", side_effect=CodeNotaryUntrusted
),
): ):
await plugin.repair() await plugin.repair()
@ -360,3 +377,16 @@ async def test_load_with_incorrect_image(
platform="linux/amd64", platform="linux/amd64",
) )
assert plugin.image == correct_image assert plugin.image == correct_image
@pytest.mark.parametrize(
"plugin",
[PluginAudio, PluginCli, PluginDns, PluginMulticast, PluginObserver],
indirect=True,
)
async def test_default_image_fallback(
coresys: CoreSys, container: MagicMock, plugin: PluginBase
):
"""Test default image falls back to hard-coded constant if we fail to fetch version file."""
assert getattr(coresys.updater, f"image_{plugin.slug}") is None
assert plugin.default_image == f"ghcr.io/home-assistant/amd64-hassio-{plugin.slug}"