From 5ee7d166876b27783d3ae5ab25fc6ea1589801dc Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Thu, 25 Jul 2024 07:45:38 -0400 Subject: [PATCH] Add hard-coded image fallback for plugins for offline start (#5204) --- supervisor/plugins/audio.py | 5 +- supervisor/plugins/cli.py | 5 +- supervisor/plugins/dns.py | 5 +- supervisor/plugins/multicast.py | 5 +- supervisor/plugins/observer.py | 5 +- tests/plugins/test_plugin_base.py | 126 ++++++++++++++++++------------ 6 files changed, 98 insertions(+), 53 deletions(-) diff --git a/supervisor/plugins/audio.py b/supervisor/plugins/audio.py index 64471622a..2911e7241 100644 --- a/supervisor/plugins/audio.py +++ b/supervisor/plugins/audio.py @@ -2,6 +2,7 @@ Code: https://github.com/home-assistant/plugin-audio """ + import errno import logging from pathlib import Path, PurePath @@ -73,7 +74,9 @@ class PluginAudio(PluginBase): @property def default_image(self) -> str: """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 def latest_version(self) -> AwesomeVersion | None: diff --git a/supervisor/plugins/cli.py b/supervisor/plugins/cli.py index bdb584052..51fa8a5b3 100644 --- a/supervisor/plugins/cli.py +++ b/supervisor/plugins/cli.py @@ -2,6 +2,7 @@ Code: https://github.com/home-assistant/plugin-cli """ + from collections.abc import Awaitable import logging import secrets @@ -42,7 +43,9 @@ class PluginCli(PluginBase): @property def default_image(self) -> str: """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 def latest_version(self) -> AwesomeVersion | None: diff --git a/supervisor/plugins/dns.py b/supervisor/plugins/dns.py index 6fa29e838..2a3c4787c 100644 --- a/supervisor/plugins/dns.py +++ b/supervisor/plugins/dns.py @@ -2,6 +2,7 @@ Code: https://github.com/home-assistant/plugin-dns """ + import asyncio from contextlib import suppress import errno @@ -111,7 +112,9 @@ class PluginDns(PluginBase): @property def default_image(self) -> str: """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 def latest_version(self) -> AwesomeVersion | None: diff --git a/supervisor/plugins/multicast.py b/supervisor/plugins/multicast.py index f5ad2d164..ebcd3debe 100644 --- a/supervisor/plugins/multicast.py +++ b/supervisor/plugins/multicast.py @@ -2,6 +2,7 @@ Code: https://github.com/home-assistant/plugin-multicast """ + import logging from awesomeversion import AwesomeVersion @@ -44,7 +45,9 @@ class PluginMulticast(PluginBase): @property def default_image(self) -> str: """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 def latest_version(self) -> AwesomeVersion | None: diff --git a/supervisor/plugins/observer.py b/supervisor/plugins/observer.py index 2da1a0922..b0136aa9d 100644 --- a/supervisor/plugins/observer.py +++ b/supervisor/plugins/observer.py @@ -2,6 +2,7 @@ Code: https://github.com/home-assistant/plugin-observer """ + import logging import secrets @@ -47,7 +48,9 @@ class PluginObserver(PluginBase): @property def default_image(self) -> str: """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 def latest_version(self) -> AwesomeVersion | None: diff --git a/tests/plugins/test_plugin_base.py b/tests/plugins/test_plugin_base.py index feb9a8bc1..8e08755be 100644 --- a/tests/plugins/test_plugin_base.py +++ b/tests/plugins/test_plugin_base.py @@ -1,4 +1,5 @@ """Test base plugin functionality.""" + import asyncio 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: """Test plugin watchdog works correctly.""" - with patch.object(type(plugin.instance), "attach"), patch.object( - type(plugin.instance), "is_running", return_value=True + with ( + patch.object(type(plugin.instance), "attach"), + patch.object(type(plugin.instance), "is_running", return_value=True), ): await plugin.load() - with patch.object(type(plugin), "rebuild") as rebuild, patch.object( - type(plugin), "start" - ) as start, patch.object(type(plugin.instance), "current_state") as current_state: + with ( + patch.object(type(plugin), "rebuild") as rebuild, + patch.object(type(plugin), "start") as start, + patch.object(type(plugin.instance), "current_state") as current_state, + ): current_state.return_value = ContainerState.UNHEALTHY coresys.bus.fire_event( BusEvent.DOCKER_CONTAINER_STATE_CHANGE, @@ -168,9 +172,10 @@ async def test_plugin_watchdog_max_failed_attempts( container.status = "stopped" container.attrs = {"State": {"ExitCode": 1}} - with patch("supervisor.plugins.base.WATCHDOG_RETRY_SECONDS", 0), patch.object( - type(plugin), "start", side_effect=error - ) as start: + with ( + patch("supervisor.plugins.base.WATCHDOG_RETRY_SECONDS", 0), + patch.object(type(plugin), "start", side_effect=error) as start, + ): await plugin.watchdog_container( DockerContainerStateEvent( name=plugin.instance.name, @@ -198,17 +203,18 @@ async def test_plugin_load_running_container( ) -> None: """Test plugins load and attach to a running container.""" test_version = AwesomeVersion("2022.7.3") - with patch.object( - type(coresys.bus), "register_event" - ) as register_event, patch.object( - type(plugin.instance), "attach" - ) as attach, patch.object(type(plugin), "install") as install, patch.object( - type(plugin), "start" - ) as start, patch.object( - type(plugin.instance), - "get_latest_version", - return_value=test_version, - ), patch.object(type(plugin.instance), "is_running", return_value=True): + with ( + patch.object(type(coresys.bus), "register_event") as register_event, + patch.object(type(plugin.instance), "attach") as attach, + patch.object(type(plugin), "install") as install, + patch.object(type(plugin), "start") as start, + patch.object( + type(plugin.instance), + "get_latest_version", + return_value=test_version, + ), + patch.object(type(plugin.instance), "is_running", return_value=True), + ): await plugin.load() register_event.assert_any_call( BusEvent.DOCKER_CONTAINER_STATE_CHANGE, plugin.watchdog_container @@ -230,17 +236,18 @@ async def test_plugin_load_stopped_container( ) -> None: """Test plugins load and start existing container.""" test_version = AwesomeVersion("2022.7.3") - with patch.object( - type(coresys.bus), "register_event" - ) as register_event, patch.object( - type(plugin.instance), "attach" - ) as attach, patch.object(type(plugin), "install") as install, patch.object( - type(plugin), "start" - ) as start, patch.object( - type(plugin.instance), - "get_latest_version", - return_value=test_version, - ), patch.object(type(plugin.instance), "is_running", return_value=False): + with ( + patch.object(type(coresys.bus), "register_event") as register_event, + patch.object(type(plugin.instance), "attach") as attach, + patch.object(type(plugin), "install") as install, + patch.object(type(plugin), "start") as start, + patch.object( + type(plugin.instance), + "get_latest_version", + return_value=test_version, + ), + patch.object(type(plugin.instance), "is_running", return_value=False), + ): await plugin.load() register_event.assert_any_call( BusEvent.DOCKER_CONTAINER_STATE_CHANGE, plugin.watchdog_container @@ -262,17 +269,20 @@ async def test_plugin_load_missing_container( ) -> None: """Test plugins load and create and start container.""" test_version = AwesomeVersion("2022.7.3") - with patch.object( - type(coresys.bus), "register_event" - ) as register_event, patch.object( - type(plugin.instance), "attach", side_effect=DockerError() - ) as attach, patch.object(type(plugin), "install") as install, patch.object( - type(plugin), "start" - ) as start, patch.object( - type(plugin.instance), - "get_latest_version", - return_value=test_version, - ), patch.object(type(plugin.instance), "is_running", return_value=False): + with ( + patch.object(type(coresys.bus), "register_event") as register_event, + patch.object( + type(plugin.instance), "attach", side_effect=DockerError() + ) as attach, + patch.object(type(plugin), "install") as install, + patch.object(type(plugin), "start") as start, + patch.object( + type(plugin.instance), + "get_latest_version", + return_value=test_version, + ), + patch.object(type(plugin.instance), "is_running", return_value=False), + ): await plugin.load() register_event.assert_any_call( 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.""" coresys.hardware.disk.get_disk_free_space = lambda x: 5000 - with patch.object( - type(coresys.supervisor), "need_update", new=PropertyMock(return_value=True) - ), pytest.raises(error): + with ( + patch.object( + type(coresys.supervisor), "need_update", new=PropertyMock(return_value=True) + ), + pytest.raises(error), + ): await plugin.update() @@ -316,10 +329,14 @@ async def test_repair_failed( coresys: CoreSys, capture_exception: Mock, plugin: PluginBase ): """Test repair failed.""" - with patch.object(DockerInterface, "exists", return_value=False), patch.object( - DockerInterface, "arch", new=PropertyMock(return_value=CpuArch.AMD64) - ), patch( - "supervisor.security.module.cas_validate", side_effect=CodeNotaryUntrusted + with ( + patch.object(DockerInterface, "exists", return_value=False), + patch.object( + DockerInterface, "arch", new=PropertyMock(return_value=CpuArch.AMD64) + ), + patch( + "supervisor.security.module.cas_validate", side_effect=CodeNotaryUntrusted + ), ): await plugin.repair() @@ -360,3 +377,16 @@ async def test_load_with_incorrect_image( platform="linux/amd64", ) 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}"