From 5e1eaa9dfec27e0ee6e60708cf0c1f76bf7d6ec4 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 5 Mar 2026 09:04:33 +0100 Subject: [PATCH] Respect auto-update setting for plug-in auto-updates (#6606) * Respect auto-update setting for plug-in auto-updates Co-Authored-By: Claude Opus 4.6 * Also skip auto-updating plug-ins in decorator * Raise if auto-update flag is not set and plug-in is not up to date --------- Co-authored-by: Claude Opus 4.6 --- supervisor/jobs/decorator.py | 5 +++++ supervisor/misc/tasks.py | 5 ++++- supervisor/plugins/manager.py | 7 +++++++ tests/jobs/test_job_decorator.py | 29 ++++++++++++++++++++++++++++ tests/misc/test_tasks.py | 15 ++++++++++++++ tests/plugins/test_plugin_manager.py | 28 +++++++++++++++++++++++++++ 6 files changed, 88 insertions(+), 1 deletion(-) diff --git a/supervisor/jobs/decorator.py b/supervisor/jobs/decorator.py index c1d890797..bbd83ea8b 100644 --- a/supervisor/jobs/decorator.py +++ b/supervisor/jobs/decorator.py @@ -457,6 +457,11 @@ class Job(CoreSysAttributes): if plugin.need_update ] ): + if not coresys.sys_updater.auto_update: + raise JobConditionException( + f"'{method_name}' blocked from execution, plugin(s) {', '.join(plugin.slug for plugin in out_of_date)} are not up to date and auto-update is disabled" + ) + errors = await asyncio.gather( *[plugin.update() for plugin in out_of_date], return_exceptions=True ) diff --git a/supervisor/misc/tasks.py b/supervisor/misc/tasks.py index 90440a25b..24edd454e 100644 --- a/supervisor/misc/tasks.py +++ b/supervisor/misc/tasks.py @@ -53,7 +53,10 @@ RUN_WATCHDOG_OBSERVER_APPLICATION = 180 RUN_CORE_BACKUP_CLEANUP = 86200 -PLUGIN_AUTO_UPDATE_CONDITIONS = PLUGIN_UPDATE_CONDITIONS + [JobCondition.RUNNING] +PLUGIN_AUTO_UPDATE_CONDITIONS = PLUGIN_UPDATE_CONDITIONS + [ + JobCondition.AUTO_UPDATE, + JobCondition.RUNNING, +] OLD_BACKUP_THRESHOLD = timedelta(days=2) diff --git a/supervisor/plugins/manager.py b/supervisor/plugins/manager.py index 987e32aa9..808212485 100644 --- a/supervisor/plugins/manager.py +++ b/supervisor/plugins/manager.py @@ -86,6 +86,13 @@ class PluginManager(CoreSysAttributes): if self.sys_supervisor.need_update: return + # Skip plugin auto-updates if auto updates are disabled + if not self.sys_updater.auto_update: + _LOGGER.debug( + "Skipping plugin auto-updates because Supervisor auto-update is disabled" + ) + return + # Check requirements for plugin in self.all_plugins: # Check if need an update diff --git a/tests/jobs/test_job_decorator.py b/tests/jobs/test_job_decorator.py index aa5396789..eaee93f37 100644 --- a/tests/jobs/test_job_decorator.py +++ b/tests/jobs/test_job_decorator.py @@ -528,6 +528,35 @@ async def test_plugins_updated(coresys: CoreSys): assert await test.execute() +async def test_plugins_updated_skips_update_when_auto_update_disabled(coresys: CoreSys): + """Test plugins updated condition blocks when auto update is disabled.""" + + class TestClass: + """Test class.""" + + def __init__(self, coresys: CoreSys): + """Initialize the test class.""" + self.coresys = coresys + + @Job( + name="test_plugins_updated_auto_update_disabled_execute", + conditions=[JobCondition.PLUGINS_UPDATED], + ) + async def execute(self) -> bool: + """Execute the class method.""" + return True + + test = TestClass(coresys) + coresys.updater.auto_update = False + + with ( + patch.object(PluginAudio, "need_update", new=PropertyMock(return_value=True)), + patch.object(PluginAudio, "update") as update, + ): + assert not await test.execute() + update.assert_not_called() + + async def test_auto_update(coresys: CoreSys): """Test the auto update decorator.""" diff --git a/tests/misc/test_tasks.py b/tests/misc/test_tasks.py index ad1f63e5b..81c7315f1 100644 --- a/tests/misc/test_tasks.py +++ b/tests/misc/test_tasks.py @@ -16,6 +16,7 @@ from supervisor.homeassistant.api import HomeAssistantAPI from supervisor.homeassistant.const import LANDINGPAGE from supervisor.homeassistant.core import HomeAssistantCore from supervisor.misc.tasks import Tasks +from supervisor.plugins.dns import PluginDns from supervisor.supervisor import Supervisor from tests.common import MockResponse, get_fixture_path @@ -229,6 +230,20 @@ async def test_core_backup_cleanup(tasks: Tasks, coresys: CoreSys): assert not old_tar.exists() +@pytest.mark.usefixtures("no_job_throttle") +async def test_update_dns_skipped_when_auto_update_disabled( + tasks: Tasks, coresys: CoreSys +): + """Test plugin auto-update task is skipped when auto update is disabled.""" + await coresys.core.set_state(CoreState.RUNNING) + coresys.hardware.disk.get_disk_free_space = lambda x: 5000 + coresys.updater.auto_update = False + + with patch.object(PluginDns, "update") as update: + await tasks._update_dns() + update.assert_not_called() + + @pytest.mark.usefixtures("tmp_supervisor_data") async def test_update_addons_auto_update_success( tasks: Tasks, diff --git a/tests/plugins/test_plugin_manager.py b/tests/plugins/test_plugin_manager.py index 2f5c63f9f..cc6e9e864 100644 --- a/tests/plugins/test_plugin_manager.py +++ b/tests/plugins/test_plugin_manager.py @@ -69,3 +69,31 @@ async def test_load( assert attach.call_count == 10 assert update.call_count == 5 + + +@pytest.mark.usefixtures("no_job_throttle") +async def test_load_skip_update_auto_update_disabled( + coresys: CoreSys, mock_update_data: MockResponse, supervisor_internet: AsyncMock +): + """Test plugin manager load skips updates when auto update is disabled.""" + coresys.hardware.disk.get_disk_free_space = lambda x: 5000 + await coresys.updater.load() + await coresys.updater.reload() + + coresys.updater.auto_update = False + + with ( + patch.object(DockerInterface, "attach") as attach, + patch.object(DockerInterface, "update") as update, + patch.object(Supervisor, "need_update", new=PropertyMock(return_value=False)), + patch.object(PluginBase, "need_update", new=PropertyMock(return_value=True)), + patch.object( + PluginBase, + "version", + new=PropertyMock(return_value=AwesomeVersion("1970-01-01")), + ), + ): + await coresys.plugins.load() + + assert attach.call_count == 5 + update.assert_not_called()