mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-10 10:46:29 +00:00
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:
parent
86c016b35d
commit
1faf529b42
@ -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(
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user