Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Addon devs can block auto update for breaking versions #4832

Merged
merged 1 commit into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions supervisor/addons/addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
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
Expand Down Expand Up @@ -279,6 +280,28 @@ def auto_update(self, value: bool) -> None:
"""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."""
Expand Down
1 change: 1 addition & 0 deletions supervisor/addons/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions supervisor/addons/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
from .configuration import FolderMapping
from .const import (
ATTR_BACKUP,
ATTR_BREAKING_VERSIONS,
ATTR_CODENOTARY,
ATTR_PATH,
ATTR_READ_ONLY,
Expand Down Expand Up @@ -620,6 +621,11 @@ def codenotary(self) -> str | None:
"""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)
Expand Down
2 changes: 2 additions & 0 deletions supervisor/addons/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
)
from .const import (
ATTR_BACKUP,
ATTR_BREAKING_VERSIONS,
ATTR_CODENOTARY,
ATTR_PATH,
ATTR_READ_ONLY,
Expand Down Expand Up @@ -422,6 +423,7 @@ def _migrate(config: dict[str, Any]):
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],
mdegat01 marked this conversation as resolved.
Show resolved Hide resolved
},
extra=vol.REMOVE_EXTRA,
)
Expand Down
8 changes: 8 additions & 0 deletions supervisor/misc/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ async def _update_addons(self):
# 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
Expand Down
27 changes: 27 additions & 0 deletions tests/addons/test_addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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
Expand Down Expand Up @@ -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
3 changes: 3 additions & 0 deletions tests/fixtures/addons/local/example/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ schema:
message: "str?"
ingress: true
ingress_port: 0
breaking_versions:
- test
- 1.0
Loading