diff --git a/supervisor/addons/addon.py b/supervisor/addons/addon.py index 0d4e96620..db6c26680 100644 --- a/supervisor/addons/addon.py +++ b/supervisor/addons/addon.py @@ -15,6 +15,7 @@ from tempfile import TemporaryDirectory from typing import Any, Final import aiohttp +from awesomeversion import AwesomeVersionCompareException from deepmerge import Merger from securetar import atomic_contents_add, secure_path import voluptuous as vol @@ -279,6 +280,28 @@ class Addon(AddonModel): """Set auto update.""" self.persist[ATTR_AUTO_UPDATE] = value + @property + def auto_update_available(self) -> bool: + """Return if it is safe to auto update addon.""" + if not self.need_update or not self.auto_update: + return False + + for version in self.breaking_versions: + try: + # Must update to latest so if true update crosses a breaking version + if self.version < version: + return False + except AwesomeVersionCompareException: + # If version scheme changed, we may get compare exception + # If latest version >= breaking version then assume update will + # cross it as the version scheme changes + # If both versions have compare exception, ignore as its in the past + with suppress(AwesomeVersionCompareException): + if self.latest_version >= version: + return False + + return True + @property def watchdog(self) -> bool: """Return True if watchdog is enable.""" diff --git a/supervisor/addons/const.py b/supervisor/addons/const.py index edc4d415d..f81488c42 100644 --- a/supervisor/addons/const.py +++ b/supervisor/addons/const.py @@ -28,6 +28,7 @@ class MappingType(StrEnum): ATTR_BACKUP = "backup" +ATTR_BREAKING_VERSIONS = "breaking_versions" ATTR_CODENOTARY = "codenotary" ATTR_READ_ONLY = "read_only" ATTR_PATH = "path" diff --git a/supervisor/addons/model.py b/supervisor/addons/model.py index bc3f161c7..b1c4f657d 100644 --- a/supervisor/addons/model.py +++ b/supervisor/addons/model.py @@ -90,6 +90,7 @@ from ..utils import version_is_new_enough from .configuration import FolderMapping from .const import ( ATTR_BACKUP, + ATTR_BREAKING_VERSIONS, ATTR_CODENOTARY, ATTR_PATH, ATTR_READ_ONLY, @@ -620,6 +621,11 @@ class AddonModel(JobGroup, ABC): """Return Signer email address for CAS.""" return self.data.get(ATTR_CODENOTARY) + @property + def breaking_versions(self) -> list[AwesomeVersion]: + """Return breaking versions of addon.""" + return self.data[ATTR_BREAKING_VERSIONS] + def validate_availability(self) -> None: """Validate if addon is available for current system.""" return self._validate_availability(self.data, logger=_LOGGER.error) diff --git a/supervisor/addons/validate.py b/supervisor/addons/validate.py index 69faeba43..115896456 100644 --- a/supervisor/addons/validate.py +++ b/supervisor/addons/validate.py @@ -112,6 +112,7 @@ from ..validate import ( ) from .const import ( ATTR_BACKUP, + ATTR_BREAKING_VERSIONS, ATTR_CODENOTARY, ATTR_PATH, ATTR_READ_ONLY, @@ -422,6 +423,7 @@ _SCHEMA_ADDON_CONFIG = vol.Schema( vol.Coerce(int), vol.Range(min=10, max=300) ), vol.Optional(ATTR_JOURNALD, default=False): vol.Boolean(), + vol.Optional(ATTR_BREAKING_VERSIONS, default=list): [version_tag], }, extra=vol.REMOVE_EXTRA, ) diff --git a/supervisor/misc/tasks.py b/supervisor/misc/tasks.py index 50497c580..55333d966 100644 --- a/supervisor/misc/tasks.py +++ b/supervisor/misc/tasks.py @@ -95,6 +95,14 @@ class Tasks(CoreSysAttributes): # Evaluate available updates if not addon.need_update: continue + if not addon.auto_update_available: + _LOGGER.debug( + "Not updating add-on %s from %s to %s as that would cross a known breaking version", + addon.slug, + addon.version, + addon.latest_version, + ) + continue if not addon.test_update_schema(): _LOGGER.warning( "Add-on %s will be ignored, schema tests failed", addon.slug diff --git a/tests/addons/test_addon.py b/tests/addons/test_addon.py index d205c890c..d69af1c4e 100644 --- a/tests/addons/test_addon.py +++ b/tests/addons/test_addon.py @@ -6,6 +6,7 @@ import errno from pathlib import Path from unittest.mock import MagicMock, PropertyMock, patch +from awesomeversion import AwesomeVersion from docker.errors import DockerException, NotFound import pytest from securetar import SecureTarFile @@ -721,3 +722,29 @@ def test_addon_pulse_error( assert "can't write pulse/client.config" in caplog.text assert coresys.core.healthy is False + + +def test_auto_update_available(coresys: CoreSys, install_addon_example: Addon): + """Test auto update availability based on versions.""" + assert install_addon_example.auto_update is False + assert install_addon_example.need_update is False + assert install_addon_example.auto_update_available is False + + with patch.object( + Addon, "version", new=PropertyMock(return_value=AwesomeVersion("1.0")) + ): + assert install_addon_example.need_update is True + assert install_addon_example.auto_update_available is False + + install_addon_example.auto_update = True + assert install_addon_example.auto_update_available is True + + with patch.object( + Addon, "version", new=PropertyMock(return_value=AwesomeVersion("0.9")) + ): + assert install_addon_example.auto_update_available is False + + with patch.object( + Addon, "version", new=PropertyMock(return_value=AwesomeVersion("test")) + ): + assert install_addon_example.auto_update_available is False diff --git a/tests/fixtures/addons/local/example/config.yaml b/tests/fixtures/addons/local/example/config.yaml index 0606593e5..8a269b562 100644 --- a/tests/fixtures/addons/local/example/config.yaml +++ b/tests/fixtures/addons/local/example/config.yaml @@ -20,3 +20,6 @@ schema: message: "str?" ingress: true ingress_port: 0 +breaking_versions: + - test + - 1.0