mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-10 18:56:30 +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
|
continue
|
||||||
# Delay auto-updates for a day in case of issues
|
# Delay auto-updates for a day in case of issues
|
||||||
if utcnow() < addon.latest_version_timestamp + timedelta(days=1):
|
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
|
continue
|
||||||
if not addon.test_update_schema():
|
if not addon.test_update_schema():
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
|
@ -15,7 +15,6 @@ from ..const import (
|
|||||||
ATTR_REPOSITORY,
|
ATTR_REPOSITORY,
|
||||||
ATTR_SLUG,
|
ATTR_SLUG,
|
||||||
ATTR_TRANSLATIONS,
|
ATTR_TRANSLATIONS,
|
||||||
ATTR_VERSION,
|
|
||||||
ATTR_VERSION_TIMESTAMP,
|
ATTR_VERSION_TIMESTAMP,
|
||||||
FILE_SUFFIX_CONFIGURATION,
|
FILE_SUFFIX_CONFIGURATION,
|
||||||
REPOSITORY_CORE,
|
REPOSITORY_CORE,
|
||||||
@ -25,7 +24,6 @@ from ..coresys import CoreSys, CoreSysAttributes
|
|||||||
from ..exceptions import ConfigurationFileError
|
from ..exceptions import ConfigurationFileError
|
||||||
from ..resolution.const import ContextType, IssueType, SuggestionType, UnhealthyReason
|
from ..resolution.const import ContextType, IssueType, SuggestionType, UnhealthyReason
|
||||||
from ..utils.common import find_one_filetype, read_json_or_yaml_file
|
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 ..utils.json import read_json_file
|
||||||
from .const import StoreType
|
from .const import StoreType
|
||||||
from .utils import extract_hash_from_path
|
from .utils import extract_hash_from_path
|
||||||
@ -142,23 +140,12 @@ class StoreData(CoreSysAttributes):
|
|||||||
repositories[repo.slug] = repo.config
|
repositories[repo.slug] = repo.config
|
||||||
addons.update(await self._read_addons_folder(repo.path, repo.slug))
|
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.repositories = repositories
|
||||||
self.addons = addons
|
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."""
|
"""Find add-ons in the path."""
|
||||||
|
|
||||||
def _get_addons_list() -> list[Path]:
|
def _get_addons_list() -> list[Path]:
|
||||||
@ -200,14 +187,14 @@ class StoreData(CoreSysAttributes):
|
|||||||
self, path: Path, repository: str
|
self, path: Path, repository: str
|
||||||
) -> dict[str, dict[str, Any]]:
|
) -> dict[str, dict[str, Any]]:
|
||||||
"""Read data from add-ons folder."""
|
"""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 {}
|
return {}
|
||||||
|
|
||||||
def _process_addons_config() -> dict[str, dict[str, Any]]:
|
def _process_addons_config() -> dict[str, dict[str, Any]]:
|
||||||
addons_config: dict[str, dict[str, Any]] = {}
|
addons: dict[str, dict[str, Any]] = {}
|
||||||
for addon in addon_list:
|
for addon_config in addon_config_list:
|
||||||
try:
|
try:
|
||||||
addon_config = read_json_or_yaml_file(addon)
|
addon = read_json_or_yaml_file(addon_config)
|
||||||
except ConfigurationFileError:
|
except ConfigurationFileError:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Can't read %s from repository %s", addon, repository
|
"Can't read %s from repository %s", addon, repository
|
||||||
@ -216,23 +203,24 @@ class StoreData(CoreSysAttributes):
|
|||||||
|
|
||||||
# validate
|
# validate
|
||||||
try:
|
try:
|
||||||
addon_config = SCHEMA_ADDON_CONFIG(addon_config)
|
addon = SCHEMA_ADDON_CONFIG(addon)
|
||||||
except vol.Invalid as ex:
|
except vol.Invalid as ex:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Can't read %s: %s", addon, humanize_error(addon_config, ex)
|
"Can't read %s: %s", addon, humanize_error(addon, ex)
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Generate slug
|
# Generate slug
|
||||||
addon_slug = f"{repository}_{addon_config[ATTR_SLUG]}"
|
addon_slug = f"{repository}_{addon[ATTR_SLUG]}"
|
||||||
|
|
||||||
# store
|
# store
|
||||||
addon_config[ATTR_REPOSITORY] = repository
|
addon[ATTR_REPOSITORY] = repository
|
||||||
addon_config[ATTR_LOCATION] = str(addon.parent)
|
addon[ATTR_LOCATION] = str(addon_config.parent)
|
||||||
addon_config[ATTR_TRANSLATIONS] = _read_addon_translations(addon.parent)
|
addon[ATTR_TRANSLATIONS] = _read_addon_translations(addon_config.parent)
|
||||||
addons_config[addon_slug] = addon_config
|
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)
|
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"),
|
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 len(addon_list) == 1
|
||||||
assert str(addon_list[0]) == "addon/config.yml"
|
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())):
|
with patch("pathlib.Path.glob", side_effect=(err := OSError())):
|
||||||
err.errno = errno.EBUSY
|
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 corrupt_repo in coresys.resolution.issues
|
||||||
assert reset_repo in coresys.resolution.suggestions
|
assert reset_repo in coresys.resolution.suggestions
|
||||||
assert coresys.core.healthy is True
|
assert coresys.core.healthy is True
|
||||||
|
|
||||||
coresys.resolution.dismiss_issue(corrupt_repo)
|
coresys.resolution.dismiss_issue(corrupt_repo)
|
||||||
err.errno = errno.EBADMSG
|
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 corrupt_repo in coresys.resolution.issues
|
||||||
assert reset_repo not in coresys.resolution.suggestions
|
assert reset_repo not in coresys.resolution.suggestions
|
||||||
assert coresys.core.healthy is False
|
assert coresys.core.healthy is False
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
"""Test store manager."""
|
"""Test store manager."""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from types import SimpleNamespace
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import PropertyMock, patch
|
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
|
# If a new version is seen processing repo, reset to utc now
|
||||||
install_addon_example.data_store["version"] = "1.1.0"
|
install_addon_example.data_store["version"] = "1.1.0"
|
||||||
|
|
||||||
# Signal the store repositories got updated
|
with patch(
|
||||||
with patch("supervisor.store.repository.Repository.update", return_value=True):
|
"pathlib.Path.stat",
|
||||||
|
return_value=SimpleNamespace(
|
||||||
|
st_mode=0o100644, st_mtime=datetime.now().timestamp()
|
||||||
|
),
|
||||||
|
):
|
||||||
await coresys.store.reload()
|
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