mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-10 10:46:29 +00:00
Addon devs can block auto update for breaking versions (#4832)
This commit is contained in:
parent
d24543e103
commit
ddadbec7e3
@ -15,6 +15,7 @@ from tempfile import TemporaryDirectory
|
|||||||
from typing import Any, Final
|
from typing import Any, Final
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
from awesomeversion import AwesomeVersionCompareException
|
||||||
from deepmerge import Merger
|
from deepmerge import Merger
|
||||||
from securetar import atomic_contents_add, secure_path
|
from securetar import atomic_contents_add, secure_path
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -279,6 +280,28 @@ class Addon(AddonModel):
|
|||||||
"""Set auto update."""
|
"""Set auto update."""
|
||||||
self.persist[ATTR_AUTO_UPDATE] = value
|
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
|
@property
|
||||||
def watchdog(self) -> bool:
|
def watchdog(self) -> bool:
|
||||||
"""Return True if watchdog is enable."""
|
"""Return True if watchdog is enable."""
|
||||||
|
@ -28,6 +28,7 @@ class MappingType(StrEnum):
|
|||||||
|
|
||||||
|
|
||||||
ATTR_BACKUP = "backup"
|
ATTR_BACKUP = "backup"
|
||||||
|
ATTR_BREAKING_VERSIONS = "breaking_versions"
|
||||||
ATTR_CODENOTARY = "codenotary"
|
ATTR_CODENOTARY = "codenotary"
|
||||||
ATTR_READ_ONLY = "read_only"
|
ATTR_READ_ONLY = "read_only"
|
||||||
ATTR_PATH = "path"
|
ATTR_PATH = "path"
|
||||||
|
@ -90,6 +90,7 @@ from ..utils import version_is_new_enough
|
|||||||
from .configuration import FolderMapping
|
from .configuration import FolderMapping
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_BACKUP,
|
ATTR_BACKUP,
|
||||||
|
ATTR_BREAKING_VERSIONS,
|
||||||
ATTR_CODENOTARY,
|
ATTR_CODENOTARY,
|
||||||
ATTR_PATH,
|
ATTR_PATH,
|
||||||
ATTR_READ_ONLY,
|
ATTR_READ_ONLY,
|
||||||
@ -620,6 +621,11 @@ class AddonModel(JobGroup, ABC):
|
|||||||
"""Return Signer email address for CAS."""
|
"""Return Signer email address for CAS."""
|
||||||
return self.data.get(ATTR_CODENOTARY)
|
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:
|
def validate_availability(self) -> None:
|
||||||
"""Validate if addon is available for current system."""
|
"""Validate if addon is available for current system."""
|
||||||
return self._validate_availability(self.data, logger=_LOGGER.error)
|
return self._validate_availability(self.data, logger=_LOGGER.error)
|
||||||
|
@ -112,6 +112,7 @@ from ..validate import (
|
|||||||
)
|
)
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_BACKUP,
|
ATTR_BACKUP,
|
||||||
|
ATTR_BREAKING_VERSIONS,
|
||||||
ATTR_CODENOTARY,
|
ATTR_CODENOTARY,
|
||||||
ATTR_PATH,
|
ATTR_PATH,
|
||||||
ATTR_READ_ONLY,
|
ATTR_READ_ONLY,
|
||||||
@ -422,6 +423,7 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
|
|||||||
vol.Coerce(int), vol.Range(min=10, max=300)
|
vol.Coerce(int), vol.Range(min=10, max=300)
|
||||||
),
|
),
|
||||||
vol.Optional(ATTR_JOURNALD, default=False): vol.Boolean(),
|
vol.Optional(ATTR_JOURNALD, default=False): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_BREAKING_VERSIONS, default=list): [version_tag],
|
||||||
},
|
},
|
||||||
extra=vol.REMOVE_EXTRA,
|
extra=vol.REMOVE_EXTRA,
|
||||||
)
|
)
|
||||||
|
@ -95,6 +95,14 @@ class Tasks(CoreSysAttributes):
|
|||||||
# Evaluate available updates
|
# Evaluate available updates
|
||||||
if not addon.need_update:
|
if not addon.need_update:
|
||||||
continue
|
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():
|
if not addon.test_update_schema():
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Add-on %s will be ignored, schema tests failed", addon.slug
|
"Add-on %s will be ignored, schema tests failed", addon.slug
|
||||||
|
@ -6,6 +6,7 @@ import errno
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import MagicMock, PropertyMock, patch
|
from unittest.mock import MagicMock, PropertyMock, patch
|
||||||
|
|
||||||
|
from awesomeversion import AwesomeVersion
|
||||||
from docker.errors import DockerException, NotFound
|
from docker.errors import DockerException, NotFound
|
||||||
import pytest
|
import pytest
|
||||||
from securetar import SecureTarFile
|
from securetar import SecureTarFile
|
||||||
@ -721,3 +722,29 @@ def test_addon_pulse_error(
|
|||||||
|
|
||||||
assert "can't write pulse/client.config" in caplog.text
|
assert "can't write pulse/client.config" in caplog.text
|
||||||
assert coresys.core.healthy is False
|
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
|
||||||
|
@ -20,3 +20,6 @@ schema:
|
|||||||
message: "str?"
|
message: "str?"
|
||||||
ingress: true
|
ingress: true
|
||||||
ingress_port: 0
|
ingress_port: 0
|
||||||
|
breaking_versions:
|
||||||
|
- test
|
||||||
|
- 1.0
|
||||||
|
Loading…
x
Reference in New Issue
Block a user