From 4f8f28b9f6298baa4fe04a608b39c6c85fe17516 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Wed, 3 Aug 2022 09:44:18 -0400 Subject: [PATCH] addons/info returns info on all addons (#3762) * Change to legacy routing approach * Revert launch.json changes --- supervisor/addons/addon.py | 3 +++ supervisor/api/__init__.py | 20 ++++++++++++++++++-- supervisor/api/addons.py | 11 ++++++++--- supervisor/exceptions.py | 4 ++++ tests/api/test_addons.py | 36 ++++++++++++++++++++++++++++++++++++ 5 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 tests/api/test_addons.py diff --git a/supervisor/addons/addon.py b/supervisor/addons/addon.py index 2252e7bb3..6c5b69430 100644 --- a/supervisor/addons/addon.py +++ b/supervisor/addons/addon.py @@ -356,6 +356,9 @@ class Addon(AddonModel): @property def ingress_panel(self) -> Optional[bool]: """Return True if the add-on access support ingress.""" + if not self.with_ingress: + return None + return self.persist[ATTR_INGRESS_PANEL] @ingress_panel.setter diff --git a/supervisor/api/__init__.py b/supervisor/api/__init__.py index 51fc3f11a..76e7abeb4 100644 --- a/supervisor/api/__init__.py +++ b/supervisor/api/__init__.py @@ -1,10 +1,13 @@ """Init file for Supervisor RESTful API.""" import logging from pathlib import Path -from typing import Optional +from typing import Any, Optional from aiohttp import web +from supervisor.api.utils import api_process +from supervisor.exceptions import APIAddonNotInstalled + from ..coresys import CoreSys, CoreSysAttributes from .addons import APIAddons from .audio import APIAudio @@ -383,7 +386,6 @@ class RestAPI(CoreSysAttributes): self.webapp.add_routes( [ web.get("/addons", api_addons.list), - web.get("/addons/{addon}/info", api_addons.info), web.post("/addons/{addon}/uninstall", api_addons.uninstall), web.post("/addons/{addon}/start", api_addons.start), web.post("/addons/{addon}/stop", api_addons.stop), @@ -401,6 +403,20 @@ class RestAPI(CoreSysAttributes): ] ) + # Legacy routing to support requests for not installed addons + api_store = APIStore() + api_store.coresys = self.coresys + + @api_process + async def addons_addon_info(request: web.Request) -> dict[str, Any]: + """Route to store if info requested for not installed addon.""" + try: + return await api_addons.info(request) + except APIAddonNotInstalled: + return await api_store.addons_addon_info(request) + + self.webapp.add_routes([web.get("/addons/{addon}/info", addons_addon_info)]) + def _register_ingress(self) -> None: """Register Ingress functions.""" api_ingress = APIIngress() diff --git a/supervisor/api/addons.py b/supervisor/api/addons.py index 6a4afb5b1..ed41343db 100644 --- a/supervisor/api/addons.py +++ b/supervisor/api/addons.py @@ -96,7 +96,13 @@ from ..const import ( ) from ..coresys import CoreSysAttributes from ..docker.stats import DockerStats -from ..exceptions import APIError, APIForbidden, PwnedError, PwnedSecret +from ..exceptions import ( + APIAddonNotInstalled, + APIError, + APIForbidden, + PwnedError, + PwnedSecret, +) from ..validate import docker_ports from .const import ATTR_SIGNED, CONTENT_TYPE_BINARY from .utils import api_process, api_process_raw, api_validate, json_loads @@ -140,7 +146,7 @@ class APIAddons(CoreSysAttributes): if not addon: raise APIError(f"Addon {addon_slug} does not exist") if not isinstance(addon, Addon) or not addon.is_installed: - raise APIError("Addon is not installed") + raise APIAddonNotInstalled("Addon is not installed") return addon @@ -177,7 +183,6 @@ class APIAddons(CoreSysAttributes): """Reload all add-on data from store.""" await asyncio.shield(self.sys_store.reload()) - @api_process async def info(self, request: web.Request) -> dict[str, Any]: """Return add-on information.""" addon: AnyAddon = self._extract_addon(request) diff --git a/supervisor/exceptions.py b/supervisor/exceptions.py index 5bde3a52c..5d3730026 100644 --- a/supervisor/exceptions.py +++ b/supervisor/exceptions.py @@ -263,6 +263,10 @@ class APIForbidden(APIError): """API forbidden error.""" +class APIAddonNotInstalled(APIError): + """Not installed addon requested at addons API.""" + + # Service / Discovery diff --git a/tests/api/test_addons.py b/tests/api/test_addons.py new file mode 100644 index 000000000..fea8ba208 --- /dev/null +++ b/tests/api/test_addons.py @@ -0,0 +1,36 @@ +"""Test addons api.""" + +from supervisor.addons.addon import Addon +from supervisor.const import AddonState +from supervisor.coresys import CoreSys +from supervisor.store.repository import Repository + +from ..const import TEST_ADDON_SLUG + + +async def test_addons_info(api_client, coresys: CoreSys, install_addon_ssh: Addon): + """Test getting addon info.""" + install_addon_ssh.state = AddonState.STOPPED + install_addon_ssh.ingress_panel = True + install_addon_ssh.protected = True + install_addon_ssh.watchdog = False + + resp = await api_client.get(f"/addons/{TEST_ADDON_SLUG}/info") + result = await resp.json() + assert result["data"]["version_latest"] == "9.2.1" + assert result["data"]["version"] == "9.2.1" + assert result["data"]["state"] == "stopped" + assert result["data"]["ingress_panel"] is True + assert result["data"]["protected"] is True + assert result["data"]["watchdog"] is False + + +# DEPRECATED - Remove with legacy routing logic on 1/2023 +async def test_addons_info_not_installed( + api_client, coresys: CoreSys, repository: Repository +): + """Test getting addon info for not installed addon.""" + resp = await api_client.get(f"/addons/{TEST_ADDON_SLUG}/info") + result = await resp.json() + assert result["data"]["version_latest"] == "9.2.1" + assert result["data"]["version"] is None