mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-12-21 23:38:01 +00:00
Compare commits
4 Commits
autoupdate
...
add-reposi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bc19ae240 | ||
|
|
2c4c76daa5 | ||
|
|
20ad0a86ba | ||
|
|
7e39226f42 |
@@ -7,9 +7,6 @@ ENV \
|
||||
CRYPTOGRAPHY_OPENSSL_NO_LEGACY=1 \
|
||||
UV_SYSTEM_PYTHON=true
|
||||
|
||||
ARG \
|
||||
COSIGN_VERSION
|
||||
|
||||
# Install base
|
||||
WORKDIR /usr/src
|
||||
RUN \
|
||||
@@ -25,8 +22,6 @@ RUN \
|
||||
openssl \
|
||||
yaml \
|
||||
\
|
||||
&& curl -Lso /usr/bin/cosign "https://github.com/home-assistant/cosign/releases/download/${COSIGN_VERSION}/cosign_${BUILD_ARCH}" \
|
||||
&& chmod a+x /usr/bin/cosign \
|
||||
&& pip3 install uv==0.9.18
|
||||
|
||||
# Install requirements
|
||||
|
||||
@@ -5,8 +5,6 @@ build_from:
|
||||
cosign:
|
||||
base_identity: https://github.com/home-assistant/docker-base/.*
|
||||
identity: https://github.com/home-assistant/supervisor/.*
|
||||
args:
|
||||
COSIGN_VERSION: 2.5.3
|
||||
labels:
|
||||
io.hass.type: supervisor
|
||||
org.opencontainers.image.title: Home Assistant Supervisor
|
||||
|
||||
@@ -782,6 +782,10 @@ class RestAPI(CoreSysAttributes):
|
||||
web.delete(
|
||||
"/store/repositories/{repository}", api_store.remove_repository
|
||||
),
|
||||
web.post(
|
||||
"/store/repositories/{repository}/repair",
|
||||
api_store.repositories_repository_repair,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ from ..const import (
|
||||
)
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..exceptions import APIError, APIForbidden, APINotFound, StoreAddonNotFoundError
|
||||
from ..resolution.const import ContextType, SuggestionType
|
||||
from ..store.addon import AddonStore
|
||||
from ..store.repository import Repository
|
||||
from ..store.validate import validate_repository
|
||||
@@ -359,3 +360,20 @@ class APIStore(CoreSysAttributes):
|
||||
"""Remove repository from the store."""
|
||||
repository: Repository = self._extract_repository(request)
|
||||
await asyncio.shield(self.sys_store.remove_repository(repository))
|
||||
|
||||
@api_process
|
||||
async def repositories_repository_repair(self, request: web.Request) -> None:
|
||||
"""Repair repository."""
|
||||
repository: Repository = self._extract_repository(request)
|
||||
await asyncio.shield(repository.reset())
|
||||
|
||||
# If we have an execute reset suggestion on this repository, dismiss it and the issue
|
||||
for suggestion in self.sys_resolution.suggestions:
|
||||
if (
|
||||
suggestion.type == SuggestionType.EXECUTE_RESET
|
||||
and suggestion.context == ContextType.STORE
|
||||
and suggestion.reference == repository.slug
|
||||
):
|
||||
for issue in self.sys_resolution.issues_for_suggestion(suggestion):
|
||||
self.sys_resolution.dismiss_issue(issue)
|
||||
return
|
||||
|
||||
@@ -969,6 +969,18 @@ class StoreAddonNotFoundError(StoreError, APINotFound):
|
||||
super().__init__(None, logger)
|
||||
|
||||
|
||||
class StoreRepositoryLocalCannotReset(StoreError, APIError):
|
||||
"""Raise if user requests a reset on the local addon repository."""
|
||||
|
||||
error_key = "store_repository_local_cannot_reset"
|
||||
message_template = "Can't reset repository {local_repo} as it is not git based!"
|
||||
extra_fields = {"local_repo": "local"}
|
||||
|
||||
def __init__(self, logger: Callable[..., None] | None = None) -> None:
|
||||
"""Initialize exception."""
|
||||
super().__init__(None, logger)
|
||||
|
||||
|
||||
class StoreJobError(StoreError, JobException):
|
||||
"""Raise on job error with git."""
|
||||
|
||||
@@ -977,6 +989,18 @@ class StoreInvalidAddonRepo(StoreError):
|
||||
"""Raise on invalid addon repo."""
|
||||
|
||||
|
||||
class StoreRepositoryUnknownError(StoreError, APIUnknownSupervisorError):
|
||||
"""Raise when unknown error occurs taking an action for a store repository."""
|
||||
|
||||
error_key = "store_repository_unknown_error"
|
||||
message_template = "An unknown error occurred with addon repository {repo}"
|
||||
|
||||
def __init__(self, logger: Callable[..., None] | None = None, *, repo: str) -> None:
|
||||
"""Initialize exception."""
|
||||
self.extra_fields = {"repo": repo}
|
||||
super().__init__(logger)
|
||||
|
||||
|
||||
# Backup
|
||||
|
||||
|
||||
|
||||
@@ -19,7 +19,13 @@ from ..const import (
|
||||
REPOSITORY_LOCAL,
|
||||
)
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..exceptions import ConfigurationFileError, StoreError
|
||||
from ..exceptions import (
|
||||
ConfigurationFileError,
|
||||
StoreError,
|
||||
StoreGitError,
|
||||
StoreRepositoryLocalCannotReset,
|
||||
StoreRepositoryUnknownError,
|
||||
)
|
||||
from ..utils.common import read_json_or_yaml_file
|
||||
from .const import BuiltinRepository
|
||||
from .git import GitRepo
|
||||
@@ -198,8 +204,12 @@ class RepositoryGit(Repository, ABC):
|
||||
|
||||
async def reset(self) -> None:
|
||||
"""Reset add-on repository to fix corruption issue with files."""
|
||||
await self._git.reset()
|
||||
await self.load()
|
||||
try:
|
||||
await self._git.reset()
|
||||
await self.load()
|
||||
except StoreGitError as err:
|
||||
_LOGGER.error("Can't reset repository %s: %s", self.slug, err)
|
||||
raise StoreRepositoryUnknownError(repo=self.slug) from err
|
||||
|
||||
|
||||
class RepositoryLocal(RepositoryBuiltin):
|
||||
@@ -238,9 +248,7 @@ class RepositoryLocal(RepositoryBuiltin):
|
||||
|
||||
async def reset(self) -> None:
|
||||
"""Raise. Not supported for local repository."""
|
||||
raise StoreError(
|
||||
"Can't reset local repository as it is not git based!", _LOGGER.error
|
||||
)
|
||||
raise StoreRepositoryLocalCannotReset(_LOGGER.error)
|
||||
|
||||
|
||||
class RepositoryGitBuiltin(RepositoryBuiltin, RepositoryGit):
|
||||
|
||||
@@ -18,8 +18,11 @@ from supervisor.docker.addon import DockerAddon
|
||||
from supervisor.docker.const import ContainerState
|
||||
from supervisor.docker.interface import DockerInterface
|
||||
from supervisor.docker.monitor import DockerContainerStateEvent
|
||||
from supervisor.exceptions import StoreGitError
|
||||
from supervisor.homeassistant.const import WSEvent
|
||||
from supervisor.homeassistant.module import HomeAssistant
|
||||
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
|
||||
from supervisor.resolution.data import Issue, Suggestion
|
||||
from supervisor.store.addon import AddonStore
|
||||
from supervisor.store.repository import Repository
|
||||
|
||||
@@ -124,6 +127,81 @@ async def test_api_store_remove_repository(
|
||||
assert test_repository.slug not in coresys.store.repositories
|
||||
|
||||
|
||||
@pytest.mark.parametrize("repo", ["core", "a474bbd1"])
|
||||
@pytest.mark.usefixtures("test_repository")
|
||||
async def test_api_store_repair_repository(api_client: TestClient, repo: str):
|
||||
"""Test POST /store/repositories/{repository}/repair REST API."""
|
||||
with patch("supervisor.store.repository.RepositoryGit.reset") as mock_reset:
|
||||
response = await api_client.post(f"/store/repositories/{repo}/repair")
|
||||
|
||||
assert response.status == 200
|
||||
mock_reset.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"issue_type", [IssueType.CORRUPT_REPOSITORY, IssueType.FATAL_ERROR]
|
||||
)
|
||||
@pytest.mark.usefixtures("test_repository")
|
||||
async def test_api_store_repair_repository_removes_suggestion(
|
||||
api_client: TestClient,
|
||||
coresys: CoreSys,
|
||||
test_repository: Repository,
|
||||
issue_type: IssueType,
|
||||
):
|
||||
"""Test POST /store/repositories/core/repair REST API removes EXECUTE_RESET suggestions."""
|
||||
issue = Issue(issue_type, ContextType.STORE, reference=test_repository.slug)
|
||||
suggestion = Suggestion(
|
||||
SuggestionType.EXECUTE_RESET, ContextType.STORE, reference=test_repository.slug
|
||||
)
|
||||
coresys.resolution.add_issue(issue, suggestions=[SuggestionType.EXECUTE_RESET])
|
||||
with patch("supervisor.store.repository.RepositoryGit.reset") as mock_reset:
|
||||
response = await api_client.post(
|
||||
f"/store/repositories/{test_repository.slug}/repair"
|
||||
)
|
||||
|
||||
assert response.status == 200
|
||||
mock_reset.assert_called_once()
|
||||
assert issue not in coresys.resolution.issues
|
||||
assert suggestion not in coresys.resolution.suggestions
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("test_repository")
|
||||
async def test_api_store_repair_repository_local_fail(api_client: TestClient):
|
||||
"""Test POST /store/repositories/local/repair REST API fails."""
|
||||
response = await api_client.post("/store/repositories/local/repair")
|
||||
|
||||
assert response.status == 400
|
||||
result = await response.json()
|
||||
assert result["error_key"] == "store_repository_local_cannot_reset"
|
||||
assert result["extra_fields"] == {"local_repo": "local"}
|
||||
assert result["message"] == "Can't reset repository local as it is not git based!"
|
||||
|
||||
|
||||
async def test_api_store_repair_repository_git_error(
|
||||
api_client: TestClient, test_repository: Repository
|
||||
):
|
||||
"""Test POST /store/repositories/{repository}/repair REST API git error."""
|
||||
with patch(
|
||||
"supervisor.store.git.GitRepo.reset",
|
||||
side_effect=StoreGitError("Git error"),
|
||||
):
|
||||
response = await api_client.post(
|
||||
f"/store/repositories/{test_repository.slug}/repair"
|
||||
)
|
||||
|
||||
assert response.status == 500
|
||||
result = await response.json()
|
||||
assert result["error_key"] == "store_repository_unknown_error"
|
||||
assert result["extra_fields"] == {
|
||||
"repo": test_repository.slug,
|
||||
"logs_command": "ha supervisor logs",
|
||||
}
|
||||
assert (
|
||||
result["message"]
|
||||
== f"An unknown error occurred with addon repository {test_repository.slug}. Check supervisor logs for details (check with 'ha supervisor logs')"
|
||||
)
|
||||
|
||||
|
||||
async def test_api_store_update_healthcheck(
|
||||
api_client: TestClient,
|
||||
coresys: CoreSys,
|
||||
|
||||
Reference in New Issue
Block a user