Use add-on config timestamp to determine add-on update age (#5897)

* Use add-on config timestamp to determine add-on update age

Instead of using the current timestamp when loading the add-on config,
simply use the add-on config modification timestamp. This way, we can
get a timetsamp even when Supervisor got restarted. It also simplifies
the code a bit.

* Fix pytest

* Patch stat() instead of modifing fixture files
This commit is contained in:
Stefan Agner 2025-05-21 13:46:20 +02:00 committed by GitHub
parent 86c016b35d
commit 1faf529b42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 34 additions and 34 deletions

View File

@ -121,6 +121,12 @@ class Tasks(CoreSysAttributes):
continue
# Delay auto-updates for a day in case of issues
if utcnow() < addon.latest_version_timestamp + timedelta(days=1):
_LOGGER.debug(
"Not updating add-on %s from %s to %s as the latest version is less than a day old",
addon.slug,
addon.version,
addon.latest_version,
)
continue
if not addon.test_update_schema():
_LOGGER.warning(

View File

@ -15,7 +15,6 @@ from ..const import (
ATTR_REPOSITORY,
ATTR_SLUG,
ATTR_TRANSLATIONS,
ATTR_VERSION,
ATTR_VERSION_TIMESTAMP,
FILE_SUFFIX_CONFIGURATION,
REPOSITORY_CORE,
@ -25,7 +24,6 @@ from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import ConfigurationFileError
from ..resolution.const import ContextType, IssueType, SuggestionType, UnhealthyReason
from ..utils.common import find_one_filetype, read_json_or_yaml_file
from ..utils.dt import utcnow
from ..utils.json import read_json_file
from .const import StoreType
from .utils import extract_hash_from_path
@ -142,23 +140,12 @@ class StoreData(CoreSysAttributes):
repositories[repo.slug] = repo.config
addons.update(await self._read_addons_folder(repo.path, repo.slug))
# Add a timestamp when we first see a new version
for slug, config in addons.items():
old_config = self.addons.get(slug)
if (
not old_config
or ATTR_VERSION_TIMESTAMP not in old_config
or old_config.get(ATTR_VERSION) != config.get(ATTR_VERSION)
):
config[ATTR_VERSION_TIMESTAMP] = utcnow().timestamp()
else:
config[ATTR_VERSION_TIMESTAMP] = old_config[ATTR_VERSION_TIMESTAMP]
self.repositories = repositories
self.addons = addons
async def _find_addons(self, path: Path, repository: dict) -> list[Path] | None:
async def _find_addon_configs(
self, path: Path, repository: dict
) -> list[Path] | None:
"""Find add-ons in the path."""
def _get_addons_list() -> list[Path]:
@ -200,14 +187,14 @@ class StoreData(CoreSysAttributes):
self, path: Path, repository: str
) -> dict[str, dict[str, Any]]:
"""Read data from add-ons folder."""
if not (addon_list := await self._find_addons(path, repository)):
if not (addon_config_list := await self._find_addon_configs(path, repository)):
return {}
def _process_addons_config() -> dict[str, dict[str, Any]]:
addons_config: dict[str, dict[str, Any]] = {}
for addon in addon_list:
addons: dict[str, dict[str, Any]] = {}
for addon_config in addon_config_list:
try:
addon_config = read_json_or_yaml_file(addon)
addon = read_json_or_yaml_file(addon_config)
except ConfigurationFileError:
_LOGGER.warning(
"Can't read %s from repository %s", addon, repository
@ -216,23 +203,24 @@ class StoreData(CoreSysAttributes):
# validate
try:
addon_config = SCHEMA_ADDON_CONFIG(addon_config)
addon = SCHEMA_ADDON_CONFIG(addon)
except vol.Invalid as ex:
_LOGGER.warning(
"Can't read %s: %s", addon, humanize_error(addon_config, ex)
"Can't read %s: %s", addon, humanize_error(addon, ex)
)
continue
# Generate slug
addon_slug = f"{repository}_{addon_config[ATTR_SLUG]}"
addon_slug = f"{repository}_{addon[ATTR_SLUG]}"
# store
addon_config[ATTR_REPOSITORY] = repository
addon_config[ATTR_LOCATION] = str(addon.parent)
addon_config[ATTR_TRANSLATIONS] = _read_addon_translations(addon.parent)
addons_config[addon_slug] = addon_config
addon[ATTR_REPOSITORY] = repository
addon[ATTR_LOCATION] = str(addon_config.parent)
addon[ATTR_TRANSLATIONS] = _read_addon_translations(addon_config.parent)
addon[ATTR_VERSION_TIMESTAMP] = addon_config.stat().st_mtime
addons[addon_slug] = addon
return addons_config
return addons
return await self.sys_run_in_executor(_process_addons_config)

View File

@ -25,7 +25,7 @@ async def test_read_addon_files(coresys: CoreSys):
Path(".circleci/config.yml"),
],
):
addon_list = await coresys.store.data._find_addons(Path("test"), {})
addon_list = await coresys.store.data._find_addon_configs(Path("test"), {})
assert len(addon_list) == 1
assert str(addon_list[0]) == "addon/config.yml"
@ -38,14 +38,14 @@ async def test_reading_addon_files_error(coresys: CoreSys):
with patch("pathlib.Path.glob", side_effect=(err := OSError())):
err.errno = errno.EBUSY
assert (await coresys.store.data._find_addons(Path("test"), {})) is None
assert (await coresys.store.data._find_addon_configs(Path("test"), {})) is None
assert corrupt_repo in coresys.resolution.issues
assert reset_repo in coresys.resolution.suggestions
assert coresys.core.healthy is True
coresys.resolution.dismiss_issue(corrupt_repo)
err.errno = errno.EBADMSG
assert (await coresys.store.data._find_addons(Path("test"), {})) is None
assert (await coresys.store.data._find_addon_configs(Path("test"), {})) is None
assert corrupt_repo in coresys.resolution.issues
assert reset_repo not in coresys.resolution.suggestions
assert coresys.core.healthy is False

View File

@ -1,5 +1,7 @@
"""Test store manager."""
from datetime import datetime
from types import SimpleNamespace
from typing import Any
from unittest.mock import PropertyMock, patch
@ -251,7 +253,11 @@ async def test_addon_version_timestamp(coresys: CoreSys, install_addon_example:
# If a new version is seen processing repo, reset to utc now
install_addon_example.data_store["version"] = "1.1.0"
# Signal the store repositories got updated
with patch("supervisor.store.repository.Repository.update", return_value=True):
with patch(
"pathlib.Path.stat",
return_value=SimpleNamespace(
st_mode=0o100644, st_mtime=datetime.now().timestamp()
),
):
await coresys.store.reload()
assert timestamp < install_addon_example.latest_version_timestamp
assert timestamp < install_addon_example.latest_version_timestamp