Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions supervisor/apps/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@
from ..const import AppBoot, AppStartup, AppState
from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import (
AppNotSupportedError,
AppAlreadyInstalledError,
AppNotFoundError,
AppNotInstalledError,
AppNotInStoreError,
AppNoUpdateAvailableError,
AppRebuildImageBasedError,
AppRebuildVersionChangedError,
AppsError,
AppsJobError,
CoreDNSError,
Expand Down Expand Up @@ -204,11 +210,11 @@ async def install(
self.sys_jobs.current.reference = slug

if slug in self.local:
raise AppsError(f"App {slug} is already installed", _LOGGER.warning)
raise AppAlreadyInstalledError(_LOGGER.warning, addon=self.local[slug].name)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should use the slug not the name here. Or both if you prefer, but if so we should update all the other App errors to be consistent because we use slug in many of these right now (and even within this PR like for AppNotInstalledError below where we use slug but could get the name).

My reasoning is that we can look up name (and any other information about the app) from the slug later if the frontend wants. We cannot look up app info from the name if that's all we have.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've noticed that the Frontend prefixes errors with the App name, which leads to a weird mix of App name and slug in the frontend (see screenshot home-assistant/epics#50 (comment) as an example).

Could we add slug as metadata always and format using the name? 🤔

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea that sounds totally reasonable to me. Want to update all of them like that? Or just do these and then another pr for the other unrelated errors?

store = self.store.get(slug)

if not store:
raise AppsError(f"App {slug} does not exist", _LOGGER.error)
raise AppNotFoundError(_LOGGER.error, addon=slug)

store.validate_availability()

Expand Down Expand Up @@ -266,15 +272,15 @@ async def update(
self.sys_jobs.current.reference = slug

if slug not in self.local:
raise AppsError(f"App {slug} is not installed", _LOGGER.error)
raise AppNotInstalledError(_LOGGER.error, addon=slug)
app = self.local[slug]

if app.is_detached:
raise AppsError(f"App {slug} is not available inside store", _LOGGER.error)
raise AppNotInStoreError(_LOGGER.error, addon=app.name)
store = self.store[slug]

if app.version == store.version:
raise AppsError(f"No update available for app {slug}", _LOGGER.warning)
raise AppNoUpdateAvailableError(_LOGGER.warning, addon=app.name)

# Check if available, Maybe something have changed
store.validate_availability()
Expand Down Expand Up @@ -313,22 +319,18 @@ async def rebuild(self, slug: str, *, force: bool = False) -> asyncio.Task | Non
self.sys_jobs.current.reference = slug

if slug not in self.local:
raise AppsError(f"App {slug} is not installed", _LOGGER.error)
raise AppNotInstalledError(_LOGGER.error, addon=slug)
app = self.local[slug]

if app.is_detached:
raise AppsError(f"App {slug} is not available inside store", _LOGGER.error)
raise AppNotInStoreError(_LOGGER.error, addon=app.name)
store = self.store[slug]

# Check if a rebuild is possible now
if app.version != store.version:
raise AppsError(
"Version changed, use Update instead Rebuild", _LOGGER.error
)
raise AppRebuildVersionChangedError(_LOGGER.error, addon=app.name)
if not force and not app.need_build:
raise AppNotSupportedError(
"Can't rebuild an image-based app", _LOGGER.error
)
raise AppRebuildImageBasedError(_LOGGER.error, addon=app.name)

return await app.rebuild()

Expand Down
6 changes: 3 additions & 3 deletions supervisor/apps/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -729,7 +729,7 @@ def _validate_availability(
# Architecture
if not self.sys_arch.is_supported(config[ATTR_ARCH]):
raise AppNotSupportedArchitectureError(
logger, slug=self.slug, architectures=config[ATTR_ARCH]
logger, addon=self.name, architectures=config[ATTR_ARCH]
)

# Machine / Hardware
Expand All @@ -738,7 +738,7 @@ def _validate_availability(
f"!{self.sys_machine}" in machine or self.sys_machine not in machine
):
raise AppNotSupportedMachineTypeError(
logger, slug=self.slug, machine_types=machine
logger, addon=self.name, machine_types=machine
)

# Home Assistant
Expand All @@ -748,7 +748,7 @@ def _validate_availability(
self.sys_homeassistant.version, version
):
raise AppNotSupportedHomeAssistantVersionError(
logger, slug=self.slug, version=str(version)
logger, addon=self.name, version=str(version)
)

def _available(self, config) -> bool:
Expand Down
124 changes: 112 additions & 12 deletions supervisor/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,92 @@ class AppsError(HassioError):
"""Apps exception."""


class AppAlreadyInstalledError(AppsError, APIError):
"""Raise when attempting to install an app that is already installed."""

error_key = "addon_already_installed_error"
message_template = "App {addon} is already installed"

def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str
) -> None:
"""Initialize exception."""
self.extra_fields = {"addon": addon}
super().__init__(None, logger)


class AppNotFoundError(AppsError, APIError):
"""Raise when an app cannot be found in any store."""

error_key = "addon_not_found_error"
message_template = "App {addon} does not exist"

def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str
) -> None:
"""Initialize exception."""
self.extra_fields = {"addon": addon}
super().__init__(None, logger)


class AppNotInstalledError(AppsError, APIError):
"""Raise when an action is taken on an app that is not installed."""

error_key = "addon_not_installed_error"
message_template = "App {addon} is not installed"

def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str
) -> None:
"""Initialize exception."""
self.extra_fields = {"addon": addon}
super().__init__(None, logger)


class AppNotInStoreError(AppsError, APIError):
"""Raise when an installed app is no longer available in its store."""

error_key = "addon_not_in_store_error"
message_template = "App {addon} is not available inside store"

def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str
) -> None:
"""Initialize exception."""
self.extra_fields = {"addon": addon}
super().__init__(None, logger)


class AppNoUpdateAvailableError(AppsError, APIError):
"""Raise when an update is requested but local matches store version."""

error_key = "addon_no_update_available_error"
message_template = "No update available for app {addon}"

def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str
) -> None:
"""Initialize exception."""
self.extra_fields = {"addon": addon}
super().__init__(None, logger)


class AppRebuildVersionChangedError(AppsError, APIError):
"""Raise when rebuild is requested but local and store versions differ."""

error_key = "addon_rebuild_version_changed_error"
message_template = (
"Local and store versions of app {addon} differ, use Update instead of Rebuild"
)

def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str
) -> None:
"""Initialize exception."""
self.extra_fields = {"addon": addon}
super().__init__(None, logger)


class AppConfigurationError(AppsError):
"""Error with app configuration."""

Expand Down Expand Up @@ -429,57 +515,71 @@ class AppNotSupportedError(HassioNotSupportedError):
"""App doesn't support a function."""


class AppNotSupportedArchitectureError(AppNotSupportedError):
class AppNotSupportedArchitectureError(AppNotSupportedError, APIError):
"""App does not support system due to architecture."""

error_key = "addon_not_supported_architecture_error"
message_template = "App {slug} not supported on this platform, supported architectures: {architectures}"
message_template = "App {addon} not supported on this platform, supported architectures: {architectures}"

def __init__(
self,
logger: Callable[..., None] | None = None,
*,
slug: str,
addon: str,
architectures: list[str],
) -> None:
"""Initialize exception."""
self.extra_fields = {"slug": slug, "architectures": ", ".join(architectures)}
self.extra_fields = {"addon": addon, "architectures": ", ".join(architectures)}
super().__init__(None, logger)


class AppNotSupportedMachineTypeError(AppNotSupportedError):
class AppNotSupportedMachineTypeError(AppNotSupportedError, APIError):
"""App does not support system due to machine type."""

error_key = "addon_not_supported_machine_type_error"
message_template = "App {slug} not supported on this machine, supported machine types: {machine_types}"
message_template = "App {addon} not supported on this machine, supported machine types: {machine_types}"

def __init__(
self,
logger: Callable[..., None] | None = None,
*,
slug: str,
addon: str,
machine_types: list[str],
) -> None:
"""Initialize exception."""
self.extra_fields = {"slug": slug, "machine_types": ", ".join(machine_types)}
self.extra_fields = {"addon": addon, "machine_types": ", ".join(machine_types)}
super().__init__(None, logger)


class AppNotSupportedHomeAssistantVersionError(AppNotSupportedError):
class AppNotSupportedHomeAssistantVersionError(AppNotSupportedError, APIError):
"""App does not support system due to Home Assistant version."""

error_key = "addon_not_supported_home_assistant_version_error"
message_template = "App {slug} not supported on this system, requires Home Assistant version {version} or greater"
message_template = "App {addon} not supported on this system, requires Home Assistant version {version} or greater"

def __init__(
self,
logger: Callable[..., None] | None = None,
*,
slug: str,
addon: str,
version: str,
) -> None:
"""Initialize exception."""
self.extra_fields = {"slug": slug, "version": version}
self.extra_fields = {"addon": addon, "version": version}
super().__init__(None, logger)


class AppRebuildImageBasedError(AppNotSupportedError, APIError):
"""Raise when rebuild is requested for an image-based app."""

error_key = "addon_rebuild_image_based_error"
message_template = "Cannot rebuild app {addon}, it is image-based"

def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str
) -> None:
"""Initialize exception."""
self.extra_fields = {"addon": addon}
super().__init__(None, logger)


Expand Down
2 changes: 1 addition & 1 deletion tests/api/test_apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ async def container_events_task(*args, **kwargs):

assert resp.status == 400
result = await resp.json()
assert "Can't rebuild an image-based app" in result["message"]
assert result["message"] == "Cannot rebuild app Terminal & SSH, it is image-based"

# Reset state for next test
state_changes.clear()
Expand Down
18 changes: 9 additions & 9 deletions tests/api/test_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ async def test_api_store(
assert result["data"]["addons"][-1]["slug"] == store_app.slug
assert result["data"]["repositories"][-1]["slug"] == test_repository.slug

assert f"App {store_app.slug} not supported on this platform" not in caplog.text
assert f"App {store_app.name} not supported on this platform" not in caplog.text


async def test_api_store_apps(api_client: TestClient, store_app: AppStore):
Expand Down Expand Up @@ -601,7 +601,7 @@ async def test_background_app_install_fails_fast(
)
assert resp.status == 400
body = await resp.json()
assert body["message"] == "App local_ssh is already installed"
assert body["message"] == "App Terminal & SSH is already installed"


@pytest.mark.parametrize(
Expand Down Expand Up @@ -665,7 +665,7 @@ async def test_background_app_update_fails_fast(
)
assert resp.status == 400
body = await resp.json()
assert body["message"] == "No update available for app local_ssh"
assert body["message"] == "No update available for app Terminal & SSH"


async def test_api_store_apps_app_availability_success(
Expand Down Expand Up @@ -728,12 +728,12 @@ async def test_api_store_apps_app_availability_arch_not_supported(
result = await resp.json()
assert result["error_key"] == "addon_not_supported_architecture_error"
assert result["extra_fields"] == {
"slug": "test_arch_addon",
"addon": "Test Arch Add-on",
"architectures": (architectures := ", ".join(supported_architectures)),
}
assert (
result["message"]
== f"App test_arch_addon not supported on this platform, supported architectures: {architectures}"
== f"App Test Arch Add-on not supported on this platform, supported architectures: {architectures}"
)


Expand Down Expand Up @@ -791,12 +791,12 @@ async def test_api_store_apps_app_availability_machine_not_supported(
result = await resp.json()
assert result["error_key"] == "addon_not_supported_machine_type_error"
assert result["extra_fields"] == {
"slug": "test_machine_addon",
"addon": "Test Machine Add-on",
"machine_types": (machine_types := ", ".join(supported_machines)),
}
assert (
result["message"]
== f"App test_machine_addon not supported on this machine, supported machine types: {machine_types}"
== f"App Test Machine Add-on not supported on this machine, supported machine types: {machine_types}"
)


Expand Down Expand Up @@ -851,12 +851,12 @@ async def test_api_store_apps_app_availability_homeassistant_version_too_old(
result = await resp.json()
assert result["error_key"] == "addon_not_supported_home_assistant_version_error"
assert result["extra_fields"] == {
"slug": "test_version_addon",
"addon": "Test Version Add-on",
"version": "2023.1.1",
}
assert (
result["message"]
== "App test_version_addon not supported on this system, requires Home Assistant version 2023.1.1 or greater"
== "App Test Version Add-on not supported on this system, requires Home Assistant version 2023.1.1 or greater"
)


Expand Down
Loading