Skip to content

Commit ad50498

Browse files
committed
Make error responses more translation/client library friendly
1 parent 723d364 commit ad50498

File tree

8 files changed

+167
-33
lines changed

8 files changed

+167
-33
lines changed

supervisor/addons/addon.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@
6767
from ..docker.stats import DockerStats
6868
from ..exceptions import (
6969
AddonConfigurationError,
70+
AddonNotSupportedError,
7071
AddonsError,
7172
AddonsJobError,
72-
AddonsNotSupportedError,
7373
ConfigurationFileError,
7474
DockerError,
7575
HomeAssistantAPIError,
@@ -1172,7 +1172,7 @@ async def stats(self) -> DockerStats:
11721172
async def write_stdin(self, data) -> None:
11731173
"""Write data to add-on stdin."""
11741174
if not self.with_stdin:
1175-
raise AddonsNotSupportedError(
1175+
raise AddonNotSupportedError(
11761176
f"Add-on {self.slug} does not support writing to stdin!", _LOGGER.error
11771177
)
11781178

@@ -1419,7 +1419,7 @@ def _extract_tarfile() -> tuple[TemporaryDirectory, dict[str, Any]]:
14191419

14201420
# If available
14211421
if not self._available(data[ATTR_SYSTEM]):
1422-
raise AddonsNotSupportedError(
1422+
raise AddonNotSupportedError(
14231423
f"Add-on {self.slug} is not available for this platform",
14241424
_LOGGER.error,
14251425
)

supervisor/addons/manager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
from ..const import AddonBoot, AddonStartup, AddonState
1515
from ..coresys import CoreSys, CoreSysAttributes
1616
from ..exceptions import (
17+
AddonNotSupportedError,
1718
AddonsError,
1819
AddonsJobError,
19-
AddonsNotSupportedError,
2020
CoreDNSError,
2121
DockerError,
2222
HassioError,
@@ -293,7 +293,7 @@ async def rebuild(self, slug: str, *, force: bool = False) -> asyncio.Task | Non
293293
"Version changed, use Update instead Rebuild", _LOGGER.error
294294
)
295295
if not force and not addon.need_build:
296-
raise AddonsNotSupportedError(
296+
raise AddonNotSupportedError(
297297
"Can't rebuild a image based add-on", _LOGGER.error
298298
)
299299

supervisor/addons/model.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,12 @@
8989
)
9090
from ..coresys import CoreSys
9191
from ..docker.const import Capabilities
92-
from ..exceptions import AddonsNotSupportedError
92+
from ..exceptions import (
93+
AddonNotSupportedArchitectureError,
94+
AddonNotSupportedError,
95+
AddonNotSupportedHomeAssistantVersionError,
96+
AddonNotSupportedMachineTypeError,
97+
)
9398
from ..jobs.const import JOB_GROUP_ADDON
9499
from ..jobs.job_group import JobGroup
95100
from ..utils import version_is_new_enough
@@ -680,19 +685,17 @@ def _validate_availability(
680685
"""Validate if addon is available for current system."""
681686
# Architecture
682687
if not self.sys_arch.is_supported(config[ATTR_ARCH]):
683-
raise AddonsNotSupportedError(
684-
f"Add-on {self.slug} not supported on this platform, supported architectures: {', '.join(config[ATTR_ARCH])}",
685-
logger,
688+
raise AddonNotSupportedArchitectureError(
689+
logger, slug=self.slug, architectures=config[ATTR_ARCH]
686690
)
687691

688692
# Machine / Hardware
689693
machine = config.get(ATTR_MACHINE)
690694
if machine and (
691695
f"!{self.sys_machine}" in machine or self.sys_machine not in machine
692696
):
693-
raise AddonsNotSupportedError(
694-
f"Add-on {self.slug} not supported on this machine, supported machine types: {', '.join(machine)}",
695-
logger,
697+
raise AddonNotSupportedMachineTypeError(
698+
logger, slug=self.slug, machine_types=machine
696699
)
697700

698701
# Home Assistant
@@ -701,16 +704,15 @@ def _validate_availability(
701704
if version and not version_is_new_enough(
702705
self.sys_homeassistant.version, version
703706
):
704-
raise AddonsNotSupportedError(
705-
f"Add-on {self.slug} not supported on this system, requires Home Assistant version {version} or greater",
706-
logger,
707+
raise AddonNotSupportedHomeAssistantVersionError(
708+
logger, slug=self.slug, version=str(version)
707709
)
708710

709711
def _available(self, config) -> bool:
710712
"""Return True if this add-on is available on this platform."""
711713
try:
712714
self._validate_availability(config)
713-
except AddonsNotSupportedError:
715+
except AddonNotSupportedError:
714716
return False
715717

716718
return True

supervisor/api/utils.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@
1414
HEADER_TOKEN,
1515
HEADER_TOKEN_OLD,
1616
JSON_DATA,
17+
JSON_ERROR_KEY,
18+
JSON_EXTRA_FIELDS,
1719
JSON_JOB_ID,
1820
JSON_MESSAGE,
21+
JSON_MESSAGE_TEMPLATE,
1922
JSON_RESULT,
2023
REQUEST_FROM,
2124
RESULT_ERROR,
@@ -133,10 +136,11 @@ async def wrap_api(
133136

134137

135138
def api_return_error(
136-
error: Exception | None = None,
139+
error: HassioError | None = None,
137140
message: str | None = None,
138141
error_type: str | None = None,
139142
status: int = 400,
143+
*,
140144
job_id: str | None = None,
141145
) -> web.Response:
142146
"""Return an API error message."""
@@ -155,12 +159,18 @@ def api_return_error(
155159
body=message.encode(), content_type=error_type, status=status
156160
)
157161
case _:
158-
result = {
162+
result: dict[str, Any] = {
159163
JSON_RESULT: RESULT_ERROR,
160164
JSON_MESSAGE: message,
161165
}
162166
if job_id:
163167
result[JSON_JOB_ID] = job_id
168+
if error and error.error_key:
169+
result[JSON_ERROR_KEY] = error.error_key
170+
if error and error.message_template:
171+
result[JSON_MESSAGE_TEMPLATE] = error.message_template
172+
if error and error.extra_fields:
173+
result[JSON_EXTRA_FIELDS] = error.extra_fields
164174

165175
return web.json_response(
166176
result,

supervisor/const.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@
7676
JSON_MESSAGE = "message"
7777
JSON_RESULT = "result"
7878
JSON_JOB_ID = "job_id"
79+
JSON_ERROR_KEY = "error_key"
80+
JSON_MESSAGE_TEMPLATE = "message_template"
81+
JSON_EXTRA_FIELDS = "extra_fields"
7982

8083
RESULT_ERROR = "error"
8184
RESULT_OK = "ok"

supervisor/exceptions.py

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,32 @@
11
"""Core Exceptions."""
22

33
from collections.abc import Callable
4+
from typing import Any
45

56

67
class HassioError(Exception):
78
"""Root exception."""
89

10+
error_key: str | None = None
11+
message_template: str | None = None
12+
913
def __init__(
1014
self,
1115
message: str | None = None,
1216
logger: Callable[..., None] | None = None,
17+
*,
18+
extra_fields: dict[str, Any] | None = None,
1319
) -> None:
1420
"""Raise & log."""
21+
self.extra_fields = extra_fields or {}
22+
23+
if not message and self.message_template:
24+
message = (
25+
self.message_template.format(**self.extra_fields)
26+
if self.extra_fields
27+
else self.message_template
28+
)
29+
1530
if logger is not None and message is not None:
1631
logger(message)
1732

@@ -235,8 +250,71 @@ class AddonConfigurationError(AddonsError):
235250
"""Error with add-on configuration."""
236251

237252

238-
class AddonsNotSupportedError(HassioNotSupportedError):
239-
"""Addons don't support a function."""
253+
class AddonNotSupportedError(HassioNotSupportedError):
254+
"""Addon doesn't support a function."""
255+
256+
257+
class AddonNotSupportedArchitectureError(AddonNotSupportedError):
258+
"""Addon does not support system due to architecture."""
259+
260+
error_key = "addon_not_supported_architecture_error"
261+
message_template = "Add-on {slug} not supported on this platform, supported architectures: {architectures}"
262+
263+
def __init__(
264+
self,
265+
logger: Callable[..., None] | None = None,
266+
*,
267+
slug: str,
268+
architectures: list[str],
269+
) -> None:
270+
"""Initialize exception."""
271+
super().__init__(
272+
None,
273+
logger,
274+
extra_fields={"slug": slug, "architectures": ", ".join(architectures)},
275+
)
276+
277+
278+
class AddonNotSupportedMachineTypeError(AddonNotSupportedError):
279+
"""Addon does not support system due to machine type."""
280+
281+
error_key = "addon_not_supported_machine_type_error"
282+
message_template = "Add-on {slug} not supported on this machine, supported machine types: {machine_types}"
283+
284+
def __init__(
285+
self,
286+
logger: Callable[..., None] | None = None,
287+
*,
288+
slug: str,
289+
machine_types: list[str],
290+
) -> None:
291+
"""Initialize exception."""
292+
super().__init__(
293+
None,
294+
logger,
295+
extra_fields={"slug": slug, "machine_types": ", ".join(machine_types)},
296+
)
297+
298+
299+
class AddonNotSupportedHomeAssistantVersionError(AddonNotSupportedError):
300+
"""Addon does not support system due to Home Assistant version."""
301+
302+
error_key = "addon_not_supported_home_assistant_version_error"
303+
message_template = "Add-on {slug} not supported on this system, requires Home Assistant version {version} or greater"
304+
305+
def __init__(
306+
self,
307+
logger: Callable[..., None] | None = None,
308+
*,
309+
slug: str,
310+
version: str,
311+
) -> None:
312+
"""Initialize exception."""
313+
super().__init__(
314+
None,
315+
logger,
316+
extra_fields={"slug": slug, "version": version},
317+
)
240318

241319

242320
class AddonsJobError(AddonsError, JobException):
@@ -319,10 +397,17 @@ def __init__(
319397
self,
320398
message: str | None = None,
321399
logger: Callable[..., None] | None = None,
400+
*,
322401
job_id: str | None = None,
402+
error: HassioError | None = None,
323403
) -> None:
324404
"""Raise & log, optionally with job."""
325-
super().__init__(message, logger)
405+
# Allow these to be set from another error here since APIErrors essentially wrap others to add a status
406+
self.error_key = error.error_key if error else None
407+
self.message_template = error.message_template if error else None
408+
super().__init__(
409+
message, logger, extra_fields=error.extra_fields if error else None
410+
)
326411
self.job_id = job_id
327412

328413

tests/api/test_store.py

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -403,8 +403,9 @@ async def test_api_store_addons_addon_availability_success(
403403
assert resp.status == 200
404404

405405

406+
@pytest.mark.parametrize("supported_architectures", [["i386"], ["i386", "aarch64"]])
406407
async def test_api_store_addons_addon_availability_arch_not_supported(
407-
api_client: TestClient, coresys: CoreSys
408+
api_client: TestClient, coresys: CoreSys, supported_architectures: list[str]
408409
):
409410
"""Test /store/addons/{addon}/availability REST API - architecture not supported."""
410411
# Create an addon with unsupported architecture
@@ -414,8 +415,8 @@ async def test_api_store_addons_addon_availability_arch_not_supported(
414415
# Set addon config with unsupported architecture
415416
addon_config = {
416417
"advanced": False,
417-
"arch": ["i386"], # Not supported on current system
418-
"slug": "test_arch",
418+
"arch": supported_architectures,
419+
"slug": "test_arch_addon",
419420
"description": "Test arch add-on",
420421
"name": "Test Arch Add-on",
421422
"repository": "test",
@@ -429,10 +430,23 @@ async def test_api_store_addons_addon_availability_arch_not_supported(
429430
resp = await api_client.get(f"/store/addons/{addon_obj.slug}/availability")
430431
assert resp.status == 400
431432
result = await resp.json()
432-
assert "not supported on this platform" in result["message"]
433+
assert result["error_key"] == "addon_not_supported_architecture_error"
434+
assert (
435+
result["message_template"]
436+
== "Add-on {slug} not supported on this platform, supported architectures: {architectures}"
437+
)
438+
assert result["extra_fields"] == {
439+
"slug": "test_arch_addon",
440+
"architectures": ", ".join(supported_architectures),
441+
}
442+
assert result["message"] == result["message_template"].format(
443+
**result["extra_fields"]
444+
)
433445

434446

435-
@pytest.mark.parametrize("supported_machines", [["odroid-n2"], ["!qemux86-64"]])
447+
@pytest.mark.parametrize(
448+
"supported_machines", [["odroid-n2"], ["!qemux86-64"], ["a", "b"]]
449+
)
436450
async def test_api_store_addons_addon_availability_machine_not_supported(
437451
api_client: TestClient, coresys: CoreSys, supported_machines: list[str]
438452
):
@@ -446,7 +460,7 @@ async def test_api_store_addons_addon_availability_machine_not_supported(
446460
"advanced": False,
447461
"arch": ["amd64"],
448462
"machine": supported_machines,
449-
"slug": "test_machine",
463+
"slug": "test_machine_addon",
450464
"description": "Test machine add-on",
451465
"name": "Test Machine Add-on",
452466
"repository": "test",
@@ -460,7 +474,18 @@ async def test_api_store_addons_addon_availability_machine_not_supported(
460474
resp = await api_client.get(f"/store/addons/{addon_obj.slug}/availability")
461475
assert resp.status == 400
462476
result = await resp.json()
463-
assert "not supported on this machine" in result["message"]
477+
assert result["error_key"] == "addon_not_supported_machine_type_error"
478+
assert (
479+
result["message_template"]
480+
== "Add-on {slug} not supported on this machine, supported machine types: {machine_types}"
481+
)
482+
assert result["extra_fields"] == {
483+
"slug": "test_machine_addon",
484+
"machine_types": ", ".join(supported_machines),
485+
}
486+
assert result["message"] == result["message_template"].format(
487+
**result["extra_fields"]
488+
)
464489

465490

466491
async def test_api_store_addons_addon_availability_homeassistant_version_too_old(
@@ -476,7 +501,7 @@ async def test_api_store_addons_addon_availability_homeassistant_version_too_old
476501
"advanced": False,
477502
"arch": ["amd64"],
478503
"homeassistant": "2023.1.1", # Requires newer version than current
479-
"slug": "test_version",
504+
"slug": "test_version_addon",
480505
"description": "Test version add-on",
481506
"name": "Test Version Add-on",
482507
"repository": "test",
@@ -494,8 +519,17 @@ async def test_api_store_addons_addon_availability_homeassistant_version_too_old
494519
resp = await api_client.get(f"/store/addons/{addon_obj.slug}/availability")
495520
assert resp.status == 400
496521
result = await resp.json()
522+
assert result["error_key"] == "addon_not_supported_home_assistant_version_error"
497523
assert (
498-
"requires Home Assistant version 2023.1.1 or greater" in result["message"]
524+
result["message_template"]
525+
== "Add-on {slug} not supported on this system, requires Home Assistant version {version} or greater"
526+
)
527+
assert result["extra_fields"] == {
528+
"slug": "test_version_addon",
529+
"version": "2023.1.1",
530+
}
531+
assert result["message"] == result["message_template"].format(
532+
**result["extra_fields"]
499533
)
500534

501535

0 commit comments

Comments
 (0)