mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-27 11:06:32 +00:00
Handle Store suggestion (#2306)
* Handle Store suggestion * Add fixup * Add more fixup & list * Enable fixups * Add tests * fix index * fix break * fix import * Load it anyway * Run suFix ccestion on load too * fix error message * fix error message * Fix remove * Finishing * Add tests * Fix error * fix cleanup stale stuff * Fix source * use source as url * add test for url * Apply suggestions from code review Co-authored-by: Joakim Sørensen <joasoe@gmail.com> Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
This commit is contained in:
parent
841520b75e
commit
aa5297026f
@ -125,6 +125,8 @@ echo "Start Test-Env"
|
|||||||
start_docker
|
start_docker
|
||||||
trap "stop_docker" ERR
|
trap "stop_docker" ERR
|
||||||
|
|
||||||
|
docker system prune -f
|
||||||
|
|
||||||
build_supervisor
|
build_supervisor
|
||||||
cleanup_lastboot
|
cleanup_lastboot
|
||||||
cleanup_docker
|
cleanup_docker
|
||||||
|
@ -6,6 +6,8 @@ from typing import Any, Awaitable, Dict
|
|||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from supervisor.resolution.const import ContextType, SuggestionType
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_ADDONS,
|
ATTR_ADDONS,
|
||||||
ATTR_ADDONS_REPOSITORIES,
|
ATTR_ADDONS_REPOSITORIES,
|
||||||
@ -143,10 +145,20 @@ class APISupervisor(CoreSysAttributes):
|
|||||||
if ATTR_ADDONS_REPOSITORIES in body:
|
if ATTR_ADDONS_REPOSITORIES in body:
|
||||||
new = set(body[ATTR_ADDONS_REPOSITORIES])
|
new = set(body[ATTR_ADDONS_REPOSITORIES])
|
||||||
await asyncio.shield(self.sys_store.update_repositories(new))
|
await asyncio.shield(self.sys_store.update_repositories(new))
|
||||||
if sorted(body[ATTR_ADDONS_REPOSITORIES]) != sorted(
|
|
||||||
self.sys_config.addons_repositories
|
# Fix invalid repository
|
||||||
):
|
found_invalid = False
|
||||||
raise APIError("Not a valid add-on repository")
|
for suggestion in self.sys_resolution.suggestions:
|
||||||
|
if (
|
||||||
|
suggestion.type != SuggestionType.EXECUTE_REMOVE
|
||||||
|
and suggestion.context != ContextType
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
found_invalid = True
|
||||||
|
await self.sys_resolution.apply_suggestion(suggestion)
|
||||||
|
|
||||||
|
if found_invalid:
|
||||||
|
raise APIError("Invalid Add-on repository!")
|
||||||
|
|
||||||
self.sys_updater.save_data()
|
self.sys_updater.save_data()
|
||||||
self.sys_config.save_data()
|
self.sys_config.save_data()
|
||||||
|
@ -310,6 +310,10 @@ class StoreGitError(StoreError):
|
|||||||
"""Raise if something on git is happening."""
|
"""Raise if something on git is happening."""
|
||||||
|
|
||||||
|
|
||||||
|
class StoreNotFound(StoreError):
|
||||||
|
"""Raise if slug is not known."""
|
||||||
|
|
||||||
|
|
||||||
# JobManager
|
# JobManager
|
||||||
|
|
||||||
|
|
||||||
|
@ -66,5 +66,5 @@ class SuggestionType(str, Enum):
|
|||||||
EXECUTE_REPAIR = "execute_repair"
|
EXECUTE_REPAIR = "execute_repair"
|
||||||
EXECUTE_RESET = "execute_reset"
|
EXECUTE_RESET = "execute_reset"
|
||||||
EXECUTE_RELOAD = "execute_reload"
|
EXECUTE_RELOAD = "execute_reload"
|
||||||
|
EXECUTE_REMOVE = "execute_remove"
|
||||||
REGISTRY_LOGIN = "registry_login"
|
REGISTRY_LOGIN = "registry_login"
|
||||||
NEW_INITIALIZE = "new_initialize"
|
|
||||||
|
@ -9,6 +9,9 @@ from ..coresys import CoreSys, CoreSysAttributes
|
|||||||
from .fixups.base import FixupBase
|
from .fixups.base import FixupBase
|
||||||
from .fixups.clear_full_snapshot import FixupClearFullSnapshot
|
from .fixups.clear_full_snapshot import FixupClearFullSnapshot
|
||||||
from .fixups.create_full_snapshot import FixupCreateFullSnapshot
|
from .fixups.create_full_snapshot import FixupCreateFullSnapshot
|
||||||
|
from .fixups.store_execute_reload import FixupStoreExecuteReload
|
||||||
|
from .fixups.store_execute_remove import FixupStoreExecuteRemove
|
||||||
|
from .fixups.store_execute_reset import FixupStoreExecuteReset
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -22,11 +25,20 @@ class ResolutionFixup(CoreSysAttributes):
|
|||||||
|
|
||||||
self._create_full_snapshot = FixupCreateFullSnapshot(coresys)
|
self._create_full_snapshot = FixupCreateFullSnapshot(coresys)
|
||||||
self._clear_full_snapshot = FixupClearFullSnapshot(coresys)
|
self._clear_full_snapshot = FixupClearFullSnapshot(coresys)
|
||||||
|
self._store_execute_reset = FixupStoreExecuteReset(coresys)
|
||||||
|
self._store_execute_reload = FixupStoreExecuteReload(coresys)
|
||||||
|
self._store_execute_remove = FixupStoreExecuteRemove(coresys)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def all_fixes(self) -> List[FixupBase]:
|
def all_fixes(self) -> List[FixupBase]:
|
||||||
"""Return a list of all fixups."""
|
"""Return a list of all fixups."""
|
||||||
return [self._create_full_snapshot, self._clear_full_snapshot]
|
return [
|
||||||
|
self._create_full_snapshot,
|
||||||
|
self._clear_full_snapshot,
|
||||||
|
self._store_execute_reload,
|
||||||
|
self._store_execute_reset,
|
||||||
|
self._store_execute_remove,
|
||||||
|
]
|
||||||
|
|
||||||
async def run_autofix(self) -> None:
|
async def run_autofix(self) -> None:
|
||||||
"""Run all startup fixes."""
|
"""Run all startup fixes."""
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
"""Baseclass for system fixup."""
|
"""Baseclass for system fixup."""
|
||||||
from abc import ABC, abstractmethod, abstractproperty
|
from abc import ABC, abstractmethod, abstractproperty
|
||||||
from contextlib import suppress
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from ...coresys import CoreSys, CoreSysAttributes
|
from ...coresys import CoreSys, CoreSysAttributes
|
||||||
from ...exceptions import ResolutionError, ResolutionFixupError
|
from ...exceptions import ResolutionFixupError
|
||||||
from ..const import ContextType, IssueType, SuggestionType
|
from ..const import ContextType, IssueType, SuggestionType
|
||||||
from ..data import Issue, Suggestion
|
from ..data import Issue, Suggestion
|
||||||
|
|
||||||
@ -42,13 +41,12 @@ class FixupBase(ABC, CoreSysAttributes):
|
|||||||
|
|
||||||
self.sys_resolution.dismiss_suggestion(fixing_suggestion)
|
self.sys_resolution.dismiss_suggestion(fixing_suggestion)
|
||||||
|
|
||||||
if self.issue is None:
|
# Cleanup issue
|
||||||
return
|
for issue_type in self.issues:
|
||||||
|
issue = Issue(issue_type, self.context, fixing_suggestion.reference)
|
||||||
with suppress(ResolutionError):
|
if issue not in self.sys_resolution.issues:
|
||||||
self.sys_resolution.dismiss_issue(
|
continue
|
||||||
Issue(self.issue, self.context, fixing_suggestion.reference)
|
self.sys_resolution.dismiss_issue(issue)
|
||||||
)
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def process_fixup(self, reference: Optional[str] = None) -> None:
|
async def process_fixup(self, reference: Optional[str] = None) -> None:
|
||||||
@ -65,9 +63,9 @@ class FixupBase(ABC, CoreSysAttributes):
|
|||||||
"""Return a ContextType enum."""
|
"""Return a ContextType enum."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def issue(self) -> Optional[IssueType]:
|
def issues(self) -> List[IssueType]:
|
||||||
"""Return a IssueType enum."""
|
"""Return a IssueType enum list."""
|
||||||
return None
|
return []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def auto(self) -> bool:
|
def auto(self) -> bool:
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"""Helpers to check and fix issues with free space."""
|
"""Helpers to check and fix issues with free space."""
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from ...const import SNAPSHOT_FULL
|
from ...const import SNAPSHOT_FULL
|
||||||
from ..const import MINIMUM_FULL_SNAPSHOTS, ContextType, IssueType, SuggestionType
|
from ..const import MINIMUM_FULL_SNAPSHOTS, ContextType, IssueType, SuggestionType
|
||||||
@ -36,6 +36,6 @@ class FixupClearFullSnapshot(FixupBase):
|
|||||||
return ContextType.SYSTEM
|
return ContextType.SYSTEM
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def issue(self) -> IssueType:
|
def issues(self) -> List[IssueType]:
|
||||||
"""Return a IssueType enum."""
|
"""Return a IssueType enum list."""
|
||||||
return IssueType.FREE_SPACE
|
return [IssueType.FREE_SPACE]
|
||||||
|
50
supervisor/resolution/fixups/store_execute_reload.py
Normal file
50
supervisor/resolution/fixups/store_execute_reload.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
"""Helpers to check and fix issues with free space."""
|
||||||
|
import logging
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from supervisor.exceptions import ResolutionFixupError, StoreError, StoreNotFound
|
||||||
|
|
||||||
|
from ..const import ContextType, IssueType, SuggestionType
|
||||||
|
from .base import FixupBase
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FixupStoreExecuteReload(FixupBase):
|
||||||
|
"""Storage class for fixup."""
|
||||||
|
|
||||||
|
async def process_fixup(self, reference: Optional[str] = None) -> None:
|
||||||
|
"""Initialize the fixup class."""
|
||||||
|
_LOGGER.info("Reload Store: %s", reference)
|
||||||
|
try:
|
||||||
|
repository = self.sys_store.get(reference)
|
||||||
|
except StoreNotFound:
|
||||||
|
_LOGGER.warning("Can't find store %s for fixup", reference)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Load data again
|
||||||
|
try:
|
||||||
|
await repository.load()
|
||||||
|
await repository.update()
|
||||||
|
except StoreError:
|
||||||
|
raise ResolutionFixupError() from None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def suggestion(self) -> SuggestionType:
|
||||||
|
"""Return a SuggestionType enum."""
|
||||||
|
return SuggestionType.EXECUTE_RELOAD
|
||||||
|
|
||||||
|
@property
|
||||||
|
def context(self) -> ContextType:
|
||||||
|
"""Return a ContextType enum."""
|
||||||
|
return ContextType.STORE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def issues(self) -> List[IssueType]:
|
||||||
|
"""Return a IssueType enum list."""
|
||||||
|
return [IssueType.FATAL_ERROR]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auto(self) -> bool:
|
||||||
|
"""Return if a fixup can be apply as auto fix."""
|
||||||
|
return True
|
52
supervisor/resolution/fixups/store_execute_remove.py
Normal file
52
supervisor/resolution/fixups/store_execute_remove.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
"""Helpers to check and fix issues with free space."""
|
||||||
|
import logging
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from supervisor.exceptions import ResolutionFixupError, StoreError, StoreNotFound
|
||||||
|
|
||||||
|
from ..const import ContextType, IssueType, SuggestionType
|
||||||
|
from .base import FixupBase
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FixupStoreExecuteRemove(FixupBase):
|
||||||
|
"""Storage class for fixup."""
|
||||||
|
|
||||||
|
async def process_fixup(self, reference: Optional[str] = None) -> None:
|
||||||
|
"""Initialize the fixup class."""
|
||||||
|
_LOGGER.info("Remove invalid Store: %s", reference)
|
||||||
|
try:
|
||||||
|
repository = self.sys_store.get(reference)
|
||||||
|
except StoreNotFound:
|
||||||
|
_LOGGER.warning("Can't find store %s for fixup", reference)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Remove repository
|
||||||
|
try:
|
||||||
|
await repository.remove()
|
||||||
|
except StoreError:
|
||||||
|
raise ResolutionFixupError() from None
|
||||||
|
|
||||||
|
self.sys_config.drop_addon_repository(repository.source)
|
||||||
|
self.sys_config.save_data()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def suggestion(self) -> SuggestionType:
|
||||||
|
"""Return a SuggestionType enum."""
|
||||||
|
return SuggestionType.EXECUTE_REMOVE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def context(self) -> ContextType:
|
||||||
|
"""Return a ContextType enum."""
|
||||||
|
return ContextType.STORE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def issues(self) -> List[IssueType]:
|
||||||
|
"""Return a IssueType enum list."""
|
||||||
|
return [IssueType.CORRUPT_REPOSITORY]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auto(self) -> bool:
|
||||||
|
"""Return if a fixup can be apply as auto fix."""
|
||||||
|
return True
|
52
supervisor/resolution/fixups/store_execute_reset.py
Normal file
52
supervisor/resolution/fixups/store_execute_reset.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
"""Helpers to check and fix issues with free space."""
|
||||||
|
import logging
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from supervisor.exceptions import ResolutionFixupError, StoreError, StoreNotFound
|
||||||
|
|
||||||
|
from ...utils import remove_folder
|
||||||
|
from ..const import ContextType, IssueType, SuggestionType
|
||||||
|
from .base import FixupBase
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FixupStoreExecuteReset(FixupBase):
|
||||||
|
"""Storage class for fixup."""
|
||||||
|
|
||||||
|
async def process_fixup(self, reference: Optional[str] = None) -> None:
|
||||||
|
"""Initialize the fixup class."""
|
||||||
|
_LOGGER.info("Reset corrupt Store: %s", reference)
|
||||||
|
try:
|
||||||
|
repository = self.sys_store.get(reference)
|
||||||
|
except StoreNotFound:
|
||||||
|
_LOGGER.warning("Can't find store %s for fixup", reference)
|
||||||
|
return
|
||||||
|
|
||||||
|
await remove_folder(repository.git.path)
|
||||||
|
|
||||||
|
# Load data again
|
||||||
|
try:
|
||||||
|
await repository.load()
|
||||||
|
except StoreError:
|
||||||
|
raise ResolutionFixupError() from None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def suggestion(self) -> SuggestionType:
|
||||||
|
"""Return a SuggestionType enum."""
|
||||||
|
return SuggestionType.EXECUTE_RESET
|
||||||
|
|
||||||
|
@property
|
||||||
|
def context(self) -> ContextType:
|
||||||
|
"""Return a ContextType enum."""
|
||||||
|
return ContextType.STORE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def issues(self) -> List[IssueType]:
|
||||||
|
"""Return a IssueType enum list."""
|
||||||
|
return [IssueType.CORRUPT_REPOSITORY, IssueType.FATAL_ERROR]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auto(self) -> bool:
|
||||||
|
"""Return if a fixup can be apply as auto fix."""
|
||||||
|
return True
|
@ -1,25 +1,20 @@
|
|||||||
"""Add-on Store handler."""
|
"""Add-on Store handler."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from supervisor.store.validate import SCHEMA_REPOSITORY_CONFIG
|
|
||||||
from supervisor.utils.json import read_json_file
|
|
||||||
|
|
||||||
from ..const import REPOSITORY_CORE, REPOSITORY_LOCAL
|
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from ..exceptions import JsonFileError, StoreGitError
|
from ..exceptions import StoreGitError, StoreNotFound
|
||||||
from ..jobs.decorator import Job, JobCondition
|
from ..jobs.decorator import Job, JobCondition
|
||||||
|
from ..resolution.const import ContextType, IssueType, SuggestionType
|
||||||
from .addon import AddonStore
|
from .addon import AddonStore
|
||||||
|
from .const import StoreType
|
||||||
from .data import StoreData
|
from .data import StoreData
|
||||||
from .repository import Repository
|
from .repository import Repository
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
BUILTIN_REPOSITORIES = {REPOSITORY_CORE, REPOSITORY_LOCAL}
|
BUILTIN_REPOSITORIES = {StoreType.CORE.value, StoreType.LOCAL.value}
|
||||||
|
|
||||||
|
|
||||||
class StoreManager(CoreSysAttributes):
|
class StoreManager(CoreSysAttributes):
|
||||||
@ -36,6 +31,20 @@ class StoreManager(CoreSysAttributes):
|
|||||||
"""Return list of add-on repositories."""
|
"""Return list of add-on repositories."""
|
||||||
return list(self.repositories.values())
|
return list(self.repositories.values())
|
||||||
|
|
||||||
|
def get(self, slug: str) -> Repository:
|
||||||
|
"""Return Repository with slug."""
|
||||||
|
if slug not in self.repositories:
|
||||||
|
raise StoreNotFound()
|
||||||
|
return self.repositories[slug]
|
||||||
|
|
||||||
|
def get_from_url(self, url: str) -> Repository:
|
||||||
|
"""Return Repository with slug."""
|
||||||
|
for repository in self.all:
|
||||||
|
if repository.source != url:
|
||||||
|
continue
|
||||||
|
return repository
|
||||||
|
raise StoreNotFound()
|
||||||
|
|
||||||
async def load(self) -> None:
|
async def load(self) -> None:
|
||||||
"""Start up add-on management."""
|
"""Start up add-on management."""
|
||||||
self.data.update()
|
self.data.update()
|
||||||
@ -48,7 +57,7 @@ class StoreManager(CoreSysAttributes):
|
|||||||
|
|
||||||
async def reload(self) -> None:
|
async def reload(self) -> None:
|
||||||
"""Update add-ons from repository and reload list."""
|
"""Update add-ons from repository and reload list."""
|
||||||
tasks = [repository.update() for repository in self.repositories.values()]
|
tasks = [repository.update() for repository in self.all]
|
||||||
if tasks:
|
if tasks:
|
||||||
await asyncio.wait(tasks)
|
await asyncio.wait(tasks)
|
||||||
|
|
||||||
@ -61,35 +70,33 @@ class StoreManager(CoreSysAttributes):
|
|||||||
"""Add a new custom repository."""
|
"""Add a new custom repository."""
|
||||||
job = self.sys_jobs.get_job("storemanager_update_repositories")
|
job = self.sys_jobs.get_job("storemanager_update_repositories")
|
||||||
new_rep = set(list_repositories)
|
new_rep = set(list_repositories)
|
||||||
old_rep = set(self.repositories)
|
old_rep = {repository.source for repository in self.all}
|
||||||
|
|
||||||
# add new repository
|
# add new repository
|
||||||
async def _add_repository(url: str, step: int):
|
async def _add_repository(url: str, step: int):
|
||||||
"""Add a repository."""
|
"""Add a repository."""
|
||||||
job.update(progress=job.progress + step, stage=f"Checking {url} started")
|
job.update(progress=job.progress + step, stage=f"Checking {url} started")
|
||||||
repository = Repository(self.coresys, url)
|
repository = Repository(self.coresys, url)
|
||||||
|
|
||||||
|
# Load the repository
|
||||||
try:
|
try:
|
||||||
await repository.load()
|
await repository.load()
|
||||||
except StoreGitError:
|
except StoreGitError:
|
||||||
_LOGGER.error("Can't load data from repository %s", url)
|
_LOGGER.error("Can't load data from repository %s", url)
|
||||||
return
|
else:
|
||||||
|
if not repository.validate():
|
||||||
# don't add built-in repository to config
|
_LOGGER.error("%s is not a valid add-on repository", url)
|
||||||
if url not in BUILTIN_REPOSITORIES:
|
self.sys_resolution.create_issue(
|
||||||
# Verify that it is a add-on repository
|
IssueType.CORRUPT_REPOSITORY,
|
||||||
repository_file = Path(repository.git.path, "repository.json")
|
ContextType.STORE,
|
||||||
try:
|
reference=repository.slug,
|
||||||
await self.sys_run_in_executor(
|
suggestions=[SuggestionType.EXECUTE_REMOVE],
|
||||||
SCHEMA_REPOSITORY_CONFIG, read_json_file(repository_file)
|
|
||||||
)
|
)
|
||||||
except (JsonFileError, vol.Invalid) as err:
|
|
||||||
_LOGGER.error("%s is not a valid add-on repository. %s", url, err)
|
|
||||||
await repository.remove()
|
|
||||||
return
|
|
||||||
|
|
||||||
self.sys_config.add_addon_repository(url)
|
# Add Repository to list
|
||||||
|
if repository.type == StoreType.GIT:
|
||||||
self.repositories[url] = repository
|
self.sys_config.add_addon_repository(repository.source)
|
||||||
|
self.repositories[repository.slug] = repository
|
||||||
|
|
||||||
job.update(progress=10, stage="Check repositories")
|
job.update(progress=10, stage="Check repositories")
|
||||||
repos = new_rep - old_rep
|
repos = new_rep - old_rep
|
||||||
@ -97,9 +104,10 @@ class StoreManager(CoreSysAttributes):
|
|||||||
if tasks:
|
if tasks:
|
||||||
await asyncio.wait(tasks)
|
await asyncio.wait(tasks)
|
||||||
|
|
||||||
# del new repository
|
# Delete stale repositories
|
||||||
for url in old_rep - new_rep - BUILTIN_REPOSITORIES:
|
for url in old_rep - new_rep - BUILTIN_REPOSITORIES:
|
||||||
await self.repositories.pop(url).remove()
|
repository = self.get_from_url(url)
|
||||||
|
await self.repositories.pop(repository.slug).remove()
|
||||||
self.sys_config.drop_addon_repository(url)
|
self.sys_config.drop_addon_repository(url)
|
||||||
|
|
||||||
# update data
|
# update data
|
||||||
|
10
supervisor/store/const.py
Normal file
10
supervisor/store/const.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
"""Constants for the add-on store."""
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class StoreType(str, Enum):
|
||||||
|
"""Store Types."""
|
||||||
|
|
||||||
|
CORE = "core"
|
||||||
|
LOCAL = "local"
|
||||||
|
GIT = "git"
|
@ -3,7 +3,6 @@ import asyncio
|
|||||||
import functools as ft
|
import functools as ft
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
import git
|
import git
|
||||||
@ -13,6 +12,7 @@ from ..coresys import CoreSys, CoreSysAttributes
|
|||||||
from ..exceptions import StoreGitError
|
from ..exceptions import StoreGitError
|
||||||
from ..jobs.decorator import Job, JobCondition
|
from ..jobs.decorator import Job, JobCondition
|
||||||
from ..resolution.const import ContextType, IssueType, SuggestionType
|
from ..resolution.const import ContextType, IssueType, SuggestionType
|
||||||
|
from ..utils import remove_folder
|
||||||
from ..validate import RE_REPOSITORY
|
from ..validate import RE_REPOSITORY
|
||||||
from .utils import get_hash_from_repository
|
from .utils import get_hash_from_repository
|
||||||
|
|
||||||
@ -30,7 +30,6 @@ class GitRepo(CoreSysAttributes):
|
|||||||
self.lock: asyncio.Lock = asyncio.Lock()
|
self.lock: asyncio.Lock = asyncio.Lock()
|
||||||
|
|
||||||
self.data: Dict[str, str] = RE_REPOSITORY.match(url).groupdict()
|
self.data: Dict[str, str] = RE_REPOSITORY.match(url).groupdict()
|
||||||
self.slug: str = url
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self) -> str:
|
def url(self) -> str:
|
||||||
@ -59,11 +58,12 @@ class GitRepo(CoreSysAttributes):
|
|||||||
git.NoSuchPathError,
|
git.NoSuchPathError,
|
||||||
git.GitCommandError,
|
git.GitCommandError,
|
||||||
) as err:
|
) as err:
|
||||||
_LOGGER.error("Can't load %s repo: %s.", self.path, err)
|
_LOGGER.error("Can't load %s", self.path)
|
||||||
self.sys_resolution.create_issue(
|
self.sys_resolution.create_issue(
|
||||||
IssueType.FATAL_ERROR,
|
IssueType.FATAL_ERROR,
|
||||||
ContextType.STORE,
|
ContextType.STORE,
|
||||||
reference=self.slug,
|
reference=self.path.stem,
|
||||||
|
suggestions=[SuggestionType.EXECUTE_RESET],
|
||||||
)
|
)
|
||||||
raise StoreGitError() from err
|
raise StoreGitError() from err
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ class GitRepo(CoreSysAttributes):
|
|||||||
self.sys_resolution.create_issue(
|
self.sys_resolution.create_issue(
|
||||||
IssueType.CORRUPT_REPOSITORY,
|
IssueType.CORRUPT_REPOSITORY,
|
||||||
ContextType.STORE,
|
ContextType.STORE,
|
||||||
reference=self.slug,
|
reference=self.path.stem,
|
||||||
suggestions=[SuggestionType.EXECUTE_RESET],
|
suggestions=[SuggestionType.EXECUTE_RESET],
|
||||||
)
|
)
|
||||||
raise StoreGitError() from err
|
raise StoreGitError() from err
|
||||||
@ -114,8 +114,8 @@ class GitRepo(CoreSysAttributes):
|
|||||||
self.sys_resolution.create_issue(
|
self.sys_resolution.create_issue(
|
||||||
IssueType.FATAL_ERROR,
|
IssueType.FATAL_ERROR,
|
||||||
ContextType.STORE,
|
ContextType.STORE,
|
||||||
reference=self.slug,
|
reference=self.path.stem,
|
||||||
suggestions=[SuggestionType.NEW_INITIALIZE],
|
suggestions=[SuggestionType.EXECUTE_RELOAD],
|
||||||
)
|
)
|
||||||
raise StoreGitError() from err
|
raise StoreGitError() from err
|
||||||
|
|
||||||
@ -156,8 +156,8 @@ class GitRepo(CoreSysAttributes):
|
|||||||
self.sys_resolution.create_issue(
|
self.sys_resolution.create_issue(
|
||||||
IssueType.CORRUPT_REPOSITORY,
|
IssueType.CORRUPT_REPOSITORY,
|
||||||
ContextType.STORE,
|
ContextType.STORE,
|
||||||
reference=self.slug,
|
reference=self.path.stem,
|
||||||
suggestions=[SuggestionType.EXECUTE_RELOAD],
|
suggestions=[SuggestionType.EXECUTE_RESET],
|
||||||
)
|
)
|
||||||
raise StoreGitError() from err
|
raise StoreGitError() from err
|
||||||
|
|
||||||
@ -169,14 +169,7 @@ class GitRepo(CoreSysAttributes):
|
|||||||
|
|
||||||
if not self.path.is_dir():
|
if not self.path.is_dir():
|
||||||
return
|
return
|
||||||
|
await remove_folder(self.path)
|
||||||
def log_err(funct, path, _):
|
|
||||||
"""Log error."""
|
|
||||||
_LOGGER.warning("Can't remove %s", path)
|
|
||||||
|
|
||||||
await self.sys_run_in_executor(
|
|
||||||
ft.partial(shutil.rmtree, self.path, onerror=log_err)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class GitRepoHassIO(GitRepo):
|
class GitRepoHassIO(GitRepo):
|
||||||
|
@ -1,75 +1,103 @@
|
|||||||
"""Represent a Supervisor repository."""
|
"""Represent a Supervisor repository."""
|
||||||
from ..const import (
|
import logging
|
||||||
ATTR_MAINTAINER,
|
from pathlib import Path
|
||||||
ATTR_NAME,
|
from typing import Dict, Optional
|
||||||
ATTR_URL,
|
|
||||||
REPOSITORY_CORE,
|
import voluptuous as vol
|
||||||
REPOSITORY_LOCAL,
|
|
||||||
)
|
from ..const import ATTR_MAINTAINER, ATTR_NAME, ATTR_URL
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from ..exceptions import APIError
|
from ..exceptions import JsonFileError, StoreError
|
||||||
|
from ..utils.json import read_json_file
|
||||||
|
from .const import StoreType
|
||||||
from .git import GitRepoCustom, GitRepoHassIO
|
from .git import GitRepoCustom, GitRepoHassIO
|
||||||
from .utils import get_hash_from_repository
|
from .utils import get_hash_from_repository
|
||||||
|
from .validate import SCHEMA_REPOSITORY_CONFIG
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
UNKNOWN = "unknown"
|
UNKNOWN = "unknown"
|
||||||
|
|
||||||
|
|
||||||
class Repository(CoreSysAttributes):
|
class Repository(CoreSysAttributes):
|
||||||
"""Repository in Supervisor."""
|
"""Repository in Supervisor."""
|
||||||
|
|
||||||
slug: str = None
|
def __init__(self, coresys: CoreSys, repository: str):
|
||||||
|
|
||||||
def __init__(self, coresys, repository):
|
|
||||||
"""Initialize repository object."""
|
"""Initialize repository object."""
|
||||||
self.coresys = coresys
|
self.coresys: CoreSys = coresys
|
||||||
self.source = None
|
self.git: Optional[str] = None
|
||||||
self.git = None
|
|
||||||
|
|
||||||
if repository == REPOSITORY_LOCAL:
|
self.source: str = repository
|
||||||
self.slug = repository
|
if repository == StoreType.LOCAL:
|
||||||
elif repository == REPOSITORY_CORE:
|
self._slug = repository
|
||||||
self.slug = repository
|
self._type = StoreType.LOCAL
|
||||||
|
elif repository == StoreType.CORE:
|
||||||
self.git = GitRepoHassIO(coresys)
|
self.git = GitRepoHassIO(coresys)
|
||||||
|
self._slug = repository
|
||||||
|
self._type = StoreType.CORE
|
||||||
else:
|
else:
|
||||||
self.slug = get_hash_from_repository(repository)
|
|
||||||
self.git = GitRepoCustom(coresys, repository)
|
self.git = GitRepoCustom(coresys, repository)
|
||||||
self.source = repository
|
self.source = repository
|
||||||
|
self._slug = get_hash_from_repository(repository)
|
||||||
|
self._type = StoreType.GIT
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def data(self):
|
def slug(self) -> str:
|
||||||
|
"""Return repo slug."""
|
||||||
|
return self._slug
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self) -> StoreType:
|
||||||
|
"""Return type of the store."""
|
||||||
|
return self._type
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self) -> Dict:
|
||||||
"""Return data struct repository."""
|
"""Return data struct repository."""
|
||||||
return self.sys_store.data.repositories.get(self.slug, {})
|
return self.sys_store.data.repositories.get(self.slug, {})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self) -> str:
|
||||||
"""Return name of repository."""
|
"""Return name of repository."""
|
||||||
return self.data.get(ATTR_NAME, UNKNOWN)
|
return self.data.get(ATTR_NAME, UNKNOWN)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self) -> str:
|
||||||
"""Return URL of repository."""
|
"""Return URL of repository."""
|
||||||
return self.data.get(ATTR_URL, self.source)
|
return self.data.get(ATTR_URL, self.source)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def maintainer(self):
|
def maintainer(self) -> str:
|
||||||
"""Return url of repository."""
|
"""Return url of repository."""
|
||||||
return self.data.get(ATTR_MAINTAINER, UNKNOWN)
|
return self.data.get(ATTR_MAINTAINER, UNKNOWN)
|
||||||
|
|
||||||
async def load(self):
|
def validate(self) -> bool:
|
||||||
|
"""Check if store is valid."""
|
||||||
|
if self.type != StoreType.GIT:
|
||||||
|
return True
|
||||||
|
|
||||||
|
repository_file = Path(self.git.path, "repository.json")
|
||||||
|
try:
|
||||||
|
SCHEMA_REPOSITORY_CONFIG(read_json_file(repository_file))
|
||||||
|
except (JsonFileError, vol.Invalid):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def load(self) -> None:
|
||||||
"""Load addon repository."""
|
"""Load addon repository."""
|
||||||
if not self.git:
|
if not self.git:
|
||||||
return
|
return
|
||||||
await self.git.load()
|
await self.git.load()
|
||||||
|
|
||||||
async def update(self):
|
async def update(self) -> None:
|
||||||
"""Update add-on repository."""
|
"""Update add-on repository."""
|
||||||
if not self.git:
|
if self.type == StoreType.LOCAL:
|
||||||
return
|
return
|
||||||
await self.git.pull()
|
await self.git.pull()
|
||||||
|
|
||||||
async def remove(self):
|
async def remove(self) -> None:
|
||||||
"""Remove add-on repository."""
|
"""Remove add-on repository."""
|
||||||
if self.slug in (REPOSITORY_CORE, REPOSITORY_LOCAL):
|
if self.type != StoreType.GIT:
|
||||||
raise APIError("Can't remove built-in repositories!")
|
_LOGGER.error("Can't remove built-in repositories!")
|
||||||
|
raise StoreError()
|
||||||
|
|
||||||
await self.git.remove()
|
await self.git.remove()
|
||||||
|
@ -3,6 +3,7 @@ import asyncio
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
@ -132,3 +133,26 @@ def get_message_from_exception_chain(err: Exception) -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
return get_message_from_exception_chain(err.__context__)
|
return get_message_from_exception_chain(err.__context__)
|
||||||
|
|
||||||
|
|
||||||
|
async def remove_folder(folder: Path, content_only: bool = False) -> None:
|
||||||
|
"""Remove folder and reset privileged.
|
||||||
|
|
||||||
|
Is needed to avoid issue with:
|
||||||
|
- CAP_DAC_OVERRIDE
|
||||||
|
- CAP_DAC_READ_SEARCH
|
||||||
|
"""
|
||||||
|
del_folder = f"{folder}" + "/{,.[!.],..?}*" if content_only else f"{folder}"
|
||||||
|
try:
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
"bash", "-c", f"rm -rf {del_folder}", stdout=asyncio.subprocess.DEVNULL
|
||||||
|
)
|
||||||
|
|
||||||
|
_, error_msg = await proc.communicate()
|
||||||
|
except OSError as err:
|
||||||
|
error_msg = str(err)
|
||||||
|
else:
|
||||||
|
if proc.returncode == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
_LOGGER.error("Can't remove folder %s: %s", folder, error_msg)
|
||||||
|
@ -182,7 +182,6 @@ async def api_client(aiohttp_client, coresys: CoreSys):
|
|||||||
def store_manager(coresys: CoreSys):
|
def store_manager(coresys: CoreSys):
|
||||||
"""Fixture for the store manager."""
|
"""Fixture for the store manager."""
|
||||||
sm_obj = coresys.store
|
sm_obj = coresys.store
|
||||||
sm_obj.repositories = set(coresys.config.addons_repositories)
|
|
||||||
with patch("supervisor.store.data.StoreData.update", return_value=MagicMock()):
|
with patch("supervisor.store.data.StoreData.update", return_value=MagicMock()):
|
||||||
yield sm_obj
|
yield sm_obj
|
||||||
|
|
||||||
|
32
tests/resolution/fixup/test_store_execute_reload.py
Normal file
32
tests/resolution/fixup/test_store_execute_reload.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"""Test evaluation base."""
|
||||||
|
# pylint: disable=import-error,protected-access
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from supervisor.coresys import CoreSys
|
||||||
|
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
|
||||||
|
from supervisor.resolution.data import Issue, Suggestion
|
||||||
|
from supervisor.resolution.fixups.store_execute_reload import FixupStoreExecuteReload
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fixup(coresys: CoreSys):
|
||||||
|
"""Test fixup."""
|
||||||
|
store_execute_reload = FixupStoreExecuteReload(coresys)
|
||||||
|
|
||||||
|
assert store_execute_reload.auto
|
||||||
|
|
||||||
|
coresys.resolution.suggestions = Suggestion(
|
||||||
|
SuggestionType.EXECUTE_RELOAD, ContextType.STORE, reference="test"
|
||||||
|
)
|
||||||
|
coresys.resolution.issues = Issue(
|
||||||
|
IssueType.FATAL_ERROR, ContextType.STORE, reference="test"
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_repositorie = AsyncMock()
|
||||||
|
coresys.store.repositories["test"] = mock_repositorie
|
||||||
|
|
||||||
|
await store_execute_reload()
|
||||||
|
|
||||||
|
assert mock_repositorie.load.called
|
||||||
|
assert mock_repositorie.update.called
|
||||||
|
assert len(coresys.resolution.suggestions) == 0
|
||||||
|
assert len(coresys.resolution.issues) == 0
|
32
tests/resolution/fixup/test_store_execute_remove.py
Normal file
32
tests/resolution/fixup/test_store_execute_remove.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"""Test evaluation base."""
|
||||||
|
# pylint: disable=import-error,protected-access
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from supervisor.coresys import CoreSys
|
||||||
|
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
|
||||||
|
from supervisor.resolution.data import Issue, Suggestion
|
||||||
|
from supervisor.resolution.fixups.store_execute_remove import FixupStoreExecuteRemove
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fixup(coresys: CoreSys):
|
||||||
|
"""Test fixup."""
|
||||||
|
store_execute_remove = FixupStoreExecuteRemove(coresys)
|
||||||
|
|
||||||
|
assert store_execute_remove.auto
|
||||||
|
|
||||||
|
coresys.resolution.suggestions = Suggestion(
|
||||||
|
SuggestionType.EXECUTE_REMOVE, ContextType.STORE, reference="test"
|
||||||
|
)
|
||||||
|
coresys.resolution.issues = Issue(
|
||||||
|
IssueType.CORRUPT_REPOSITORY, ContextType.STORE, reference="test"
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_repositorie = AsyncMock()
|
||||||
|
coresys.store.repositories["test"] = mock_repositorie
|
||||||
|
|
||||||
|
await store_execute_remove()
|
||||||
|
|
||||||
|
assert mock_repositorie.remove.called
|
||||||
|
assert coresys.config.save_data.called
|
||||||
|
assert len(coresys.resolution.suggestions) == 0
|
||||||
|
assert len(coresys.resolution.issues) == 0
|
38
tests/resolution/fixup/test_store_execute_reset.py
Normal file
38
tests/resolution/fixup/test_store_execute_reset.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
"""Test evaluation base."""
|
||||||
|
# pylint: disable=import-error,protected-access
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from supervisor.coresys import CoreSys
|
||||||
|
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
|
||||||
|
from supervisor.resolution.data import Issue, Suggestion
|
||||||
|
from supervisor.resolution.fixups.store_execute_reset import FixupStoreExecuteReset
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fixup(coresys: CoreSys, tmp_path):
|
||||||
|
"""Test fixup."""
|
||||||
|
store_execute_reset = FixupStoreExecuteReset(coresys)
|
||||||
|
test_repo = Path(tmp_path, "test_repo")
|
||||||
|
|
||||||
|
assert store_execute_reset.auto
|
||||||
|
|
||||||
|
coresys.resolution.suggestions = Suggestion(
|
||||||
|
SuggestionType.EXECUTE_RESET, ContextType.STORE, reference="test"
|
||||||
|
)
|
||||||
|
coresys.resolution.issues = Issue(
|
||||||
|
IssueType.CORRUPT_REPOSITORY, ContextType.STORE, reference="test"
|
||||||
|
)
|
||||||
|
|
||||||
|
test_repo.mkdir()
|
||||||
|
assert test_repo.exists()
|
||||||
|
|
||||||
|
mock_repositorie = AsyncMock()
|
||||||
|
mock_repositorie.git.path = test_repo
|
||||||
|
coresys.store.repositories["test"] = mock_repositorie
|
||||||
|
|
||||||
|
await store_execute_reset()
|
||||||
|
|
||||||
|
assert not test_repo.exists()
|
||||||
|
assert mock_repositorie.load.called
|
||||||
|
assert len(coresys.resolution.suggestions) == 0
|
||||||
|
assert len(coresys.resolution.issues) == 0
|
@ -4,16 +4,35 @@ from unittest.mock import patch
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from supervisor.resolution.const import SuggestionType
|
||||||
|
from supervisor.store import BUILTIN_REPOSITORIES
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_add_valid_repository(coresys, store_manager):
|
async def test_add_valid_repository(coresys, store_manager):
|
||||||
"""Test add custom repository."""
|
"""Test add custom repository."""
|
||||||
current = coresys.config.addons_repositories
|
current = coresys.config.addons_repositories
|
||||||
with patch("supervisor.store.repository.Repository.load", return_value=True), patch(
|
with patch("supervisor.store.repository.Repository.load", return_value=None), patch(
|
||||||
"pathlib.Path.read_text",
|
"pathlib.Path.read_text",
|
||||||
return_value=json.dumps({"name": "Awesome repository"}),
|
return_value=json.dumps({"name": "Awesome repository"}),
|
||||||
):
|
):
|
||||||
await store_manager.update_repositories(current + ["http://example.com"])
|
await store_manager.update_repositories(current + ["http://example.com"])
|
||||||
|
assert store_manager.get_from_url("http://example.com").validate()
|
||||||
|
assert "http://example.com" in coresys.config.addons_repositories
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_valid_repository_url(coresys, store_manager):
|
||||||
|
"""Test add custom repository."""
|
||||||
|
current = coresys.config.addons_repositories
|
||||||
|
with patch("supervisor.store.repository.Repository.load", return_value=None), patch(
|
||||||
|
"pathlib.Path.read_text",
|
||||||
|
return_value=json.dumps(
|
||||||
|
{"name": "Awesome repository", "url": "http://example2.com/docs"}
|
||||||
|
),
|
||||||
|
):
|
||||||
|
await store_manager.update_repositories(current + ["http://example.com"])
|
||||||
|
assert store_manager.get_from_url("http://example.com").validate()
|
||||||
assert "http://example.com" in coresys.config.addons_repositories
|
assert "http://example.com" in coresys.config.addons_repositories
|
||||||
|
|
||||||
|
|
||||||
@ -21,9 +40,21 @@ async def test_add_valid_repository(coresys, store_manager):
|
|||||||
async def test_add_invalid_repository(coresys, store_manager):
|
async def test_add_invalid_repository(coresys, store_manager):
|
||||||
"""Test add custom repository."""
|
"""Test add custom repository."""
|
||||||
current = coresys.config.addons_repositories
|
current = coresys.config.addons_repositories
|
||||||
with patch("supervisor.store.repository.Repository.load", return_value=True), patch(
|
with patch("supervisor.store.repository.Repository.load", return_value=None), patch(
|
||||||
"pathlib.Path.read_text",
|
"pathlib.Path.read_text",
|
||||||
return_value="",
|
return_value="",
|
||||||
):
|
):
|
||||||
await store_manager.update_repositories(current + ["http://example.com"])
|
await store_manager.update_repositories(current + ["http://example.com"])
|
||||||
assert "http://example.com" not in coresys.config.addons_repositories
|
assert not store_manager.get_from_url("http://example.com").validate()
|
||||||
|
|
||||||
|
assert "http://example.com" in coresys.config.addons_repositories
|
||||||
|
assert coresys.resolution.suggestions[-1].type == SuggestionType.EXECUTE_REMOVE
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_preinstall_valid_repository(coresys, store_manager):
|
||||||
|
"""Test add core repository valid."""
|
||||||
|
with patch("supervisor.store.repository.Repository.load", return_value=None):
|
||||||
|
await store_manager.update_repositories(BUILTIN_REPOSITORIES)
|
||||||
|
assert store_manager.get("core").validate()
|
||||||
|
assert store_manager.get("local").validate()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user