From 94a389a625b48b1a9da82d9c474f869f38c5d624 Mon Sep 17 00:00:00 2001 From: Qubad786 Date: Wed, 20 Nov 2024 13:18:35 +0500 Subject: [PATCH] Remove accepts style decorator from catalog plugin (#14951) --- .../middlewared/api/v25_04_0/__init__.py | 2 + .../middlewared/api/v25_04_0/app.py | 31 +++++ .../middlewared/api/v25_04_0/catalog.py | 123 ++++++++++++++++++ .../plugins/catalog/app_version.py | 35 +---- .../middlewared/plugins/catalog/apps.py | 52 ++------ .../plugins/catalog/apps_details.py | 45 +------ .../middlewared/plugins/catalog/sync.py | 8 +- .../middlewared/plugins/catalog/update.py | 32 +---- tests/api2/test_catalog_roles.py | 6 +- 9 files changed, 187 insertions(+), 147 deletions(-) create mode 100644 src/middlewared/middlewared/api/v25_04_0/app.py create mode 100644 src/middlewared/middlewared/api/v25_04_0/catalog.py diff --git a/src/middlewared/middlewared/api/v25_04_0/__init__.py b/src/middlewared/middlewared/api/v25_04_0/__init__.py index 692a1410b5f7d..f29fb08f8e28c 100644 --- a/src/middlewared/middlewared/api/v25_04_0/__init__.py +++ b/src/middlewared/middlewared/api/v25_04_0/__init__.py @@ -4,8 +4,10 @@ from .alert import * # noqa from .alertservice import * # noqa from .api_key import * # noqa +from .app import * # noqa from .auth import * # noqa from .boot_environments import * # noqa +from .catalog import * # noqa from .cloud_sync import * # noqa from .common import * # noqa from .core import * # noqa diff --git a/src/middlewared/middlewared/api/v25_04_0/app.py b/src/middlewared/middlewared/api/v25_04_0/app.py new file mode 100644 index 0000000000000..84bc8ad73626a --- /dev/null +++ b/src/middlewared/middlewared/api/v25_04_0/app.py @@ -0,0 +1,31 @@ +from middlewared.api.base import BaseModel, NonEmptyString + +from .catalog import CatalogAppInfo + + +__all__ = [ + 'AppCategoriesArgs', 'AppCategoriesResult', 'AppSimilarArgs', 'AppSimilarResult', 'AppAvailableResponse', +] + + +class AppAvailableResponse(CatalogAppInfo): + catalog: NonEmptyString + installed: bool + train: NonEmptyString + + +class AppCategoriesArgs(BaseModel): + pass + + +class AppCategoriesResult(BaseModel): + result: list[NonEmptyString] + + +class AppSimilarArgs(BaseModel): + app_name: NonEmptyString + train: NonEmptyString + + +class AppSimilarResult(BaseModel): + result: list[AppAvailableResponse] diff --git a/src/middlewared/middlewared/api/v25_04_0/catalog.py b/src/middlewared/middlewared/api/v25_04_0/catalog.py new file mode 100644 index 0000000000000..b8d1c1d4c30ee --- /dev/null +++ b/src/middlewared/middlewared/api/v25_04_0/catalog.py @@ -0,0 +1,123 @@ +from datetime import datetime + +from pydantic import ConfigDict, Field, RootModel + +from middlewared.api.base import BaseModel, ForUpdateMetaclass, NonEmptyString, single_argument_args, LongString + + +__all__ = [ + 'CatalogEntry', 'CatalogUpdateArgs', 'CatalogUpdateResult', 'CatalogTrainsArgs', 'CatalogTrainsResult', + 'CatalogSyncArgs', 'CatalogSyncResult', 'CatalogAppInfo', 'CatalogAppsArgs', 'CatalogAppsResult', + 'CatalogAppDetailsArgs', 'CatalogAppDetailsResult', +] + + +class CatalogEntry(BaseModel): + id: NonEmptyString + label: NonEmptyString = Field(pattern=r'^\w+[\w.-]*$') + preferred_trains: list[NonEmptyString] + + +@single_argument_args('catalog_update') +class CatalogUpdateArgs(BaseModel, metaclass=ForUpdateMetaclass): + preferred_trains: list[NonEmptyString] + + +class CatalogUpdateResult(BaseModel): + result: CatalogEntry + + +class CatalogTrainsArgs(BaseModel): + pass + + +class CatalogTrainsResult(BaseModel): + result: list[NonEmptyString] + + +class CatalogSyncArgs(BaseModel): + pass + + +class CatalogSyncResult(BaseModel): + result: None + + +class Maintainer(BaseModel): + name: str + email: str + url: str | None + + +class CatalogAppInfo(BaseModel): + app_readme: LongString | None + '''HTML content of the app README.''' + categories: list[str] + '''List of categories for the app.''' + description: str + '''Short description of the app.''' + healthy: bool + '''Health status of the app.''' + healthy_error: str | None = None + '''Error if app is not healthy.''' + home: str + '''Homepage URL of the app.''' + location: str + '''Local path to the app's location.''' + latest_version: str | None + '''Latest available app version.''' + latest_app_version: str | None + '''Latest available app version in repository.''' + latest_human_version: str | None + '''Human-readable version of the app.''' + last_update: datetime | None + '''Timestamp of the last update in ISO format.''' + name: str + '''Name of the app.''' + recommended: bool + '''Indicates if the app is recommended.''' + title: str + '''Title of the app.''' + maintainers: list[Maintainer] + '''List of app maintainers.''' + tags: list[str] + '''Tags associated with the app.''' + screenshots: list[str] + '''List of screenshot URLs.''' + sources: list[str] + '''List of source URLs.''' + icon_url: str | None = None + '''URL of the app icon''' + + # We do this because if we change anything in catalog.json, even older releases will + # get this new field and different roles will start breaking due to this + model_config = ConfigDict(extra='allow') + + +@single_argument_args('catalog_apps_options') +class CatalogAppsArgs(BaseModel): + cache: bool = True + cache_only: bool = False + retrieve_all_trains: bool = True + trains: list[NonEmptyString] = Field(default_factory=list) + + +class CatalogTrainInfo(RootModel[dict[str, CatalogAppInfo]]): + pass + + +class CatalogAppsResult(BaseModel): + result: dict[str, CatalogTrainInfo] + + +class CatalogAppVersionDetails(BaseModel): + train: NonEmptyString + + +class CatalogAppDetailsArgs(BaseModel): + app_name: NonEmptyString + app_version_details: CatalogAppVersionDetails + + +class CatalogAppDetailsResult(BaseModel): + result: CatalogAppInfo diff --git a/src/middlewared/middlewared/plugins/catalog/app_version.py b/src/middlewared/middlewared/plugins/catalog/app_version.py index 768a137443703..0545532d67ffc 100644 --- a/src/middlewared/middlewared/plugins/catalog/app_version.py +++ b/src/middlewared/middlewared/plugins/catalog/app_version.py @@ -4,7 +4,8 @@ from catalog_reader.train_utils import get_train_path -from middlewared.schema import accepts, Bool, Dict, List, returns, Str +from middlewared.api import api_method +from middlewared.api.current import CatalogAppDetailsArgs, CatalogAppDetailsResult from middlewared.service import CallError, Service from .apps_util import get_app_details @@ -15,37 +16,7 @@ class CatalogService(Service): class Config: cli_namespace = 'app.catalog' - @accepts( - Str('app_name'), - Dict( - 'app_version_details', - Str('train', required=True), - ), - roles=['CATALOG_READ'], - ) - @returns(Dict( - # TODO: Make sure keys here are mapped appropriately - 'app_details', - Str('name', required=True), - List('categories', items=[Str('category')], required=True), - List('maintainers', required=True), - List('tags', required=True), - List('screenshots', required=True, items=[Str('screenshot')]), - List('sources', required=True, items=[Str('source')]), - Str('app_readme', null=True, required=True), - Str('location', required=True), - Bool('healthy', required=True), - Bool('recommended', required=True), - Str('healthy_error', required=True, null=True), - Str('healthy_error', required=True, null=True), - Dict('versions', required=True, additional_attrs=True), - Str('latest_version', required=True, null=True), - Str('latest_app_version', required=True, null=True), - Str('latest_human_version', required=True, null=True), - Str('last_update', required=True, null=True), - Str('icon_url', required=True, null=True), - Str('home', required=True), - )) + @api_method(CatalogAppDetailsArgs, CatalogAppDetailsResult, roles=['CATALOG_READ']) def get_app_details(self, app_name, options): """ Retrieve information of `app_name` `app_version_details.catalog` catalog app. diff --git a/src/middlewared/middlewared/plugins/catalog/apps.py b/src/middlewared/middlewared/plugins/catalog/apps.py index 4aacb75a69e21..c7b87b8d5a32e 100644 --- a/src/middlewared/middlewared/plugins/catalog/apps.py +++ b/src/middlewared/middlewared/plugins/catalog/apps.py @@ -1,5 +1,9 @@ -from middlewared.schema import accepts, Bool, Datetime, Dict, List, Ref, returns, Str -from middlewared.service import filterable, filterable_returns, Service +from middlewared.api import api_method +from middlewared.api.current import ( + AppCategoriesArgs, AppCategoriesResult, AppAvailableResponse, +) +from middlewared.api.v25_04_0 import AppSimilarArgs, AppSimilarResult +from middlewared.service import filterable_api_method, Service from middlewared.utils import filter_list @@ -8,8 +12,7 @@ class AppService(Service): class Config: cli_namespace = 'app' - @filterable(roles=['CATALOG_READ']) - @filterable_returns(Ref('available_apps')) + @filterable_api_method(item=AppAvailableResponse, roles=['CATALOG_READ']) async def latest(self, filters, options): """ Retrieve latest updated apps. @@ -22,38 +25,7 @@ async def latest(self, filters, options): ), filters, options ) - @filterable(roles=['CATALOG_READ']) - @filterable_returns(Dict( - 'available_apps', - Bool('healthy', required=True), - Bool('installed', required=True), - Bool('recommended', required=True), - Datetime('last_update', required=True), - List('capabilities', required=True), - List('run_as_context', required=True), - List('categories', required=True), - List('maintainers', required=True), - List('tags', required=True), - List('screenshots', required=True, items=[Str('screenshot')]), - List('sources', required=True, items=[Str('source')]), - Str('name', required=True), - Str('title', required=True), - Str('description', required=True), - Str('app_readme', required=True), - Str('location', required=True), - Str('healthy_error', required=True, null=True), - Str('home', required=True), - Str('latest_version', required=True), - Str('latest_app_version', required=True), - Str('latest_human_version', required=True), - Str('icon_url', null=True, required=True), - Str('train', required=True), - Str('catalog', required=True), - register=True, - # We do this because if we change anything in catalog.json, even older releases will - # get this new field and different roles will start breaking due to this - additional_attrs=True, - )) + @filterable_api_method(item=AppAvailableResponse, roles=['CATALOG_READ']) def available(self, filters, options): """ Retrieve all available applications from all configured catalogs. @@ -68,7 +40,7 @@ def available(self, filters, options): ] catalog = self.middleware.call_sync('catalog.config') - for train, train_data in self.middleware.call_sync('catalog.apps').items(): + for train, train_data in self.middleware.call_sync('catalog.apps', {}).items(): if train not in catalog['preferred_trains']: continue @@ -82,16 +54,14 @@ def available(self, filters, options): return filter_list(results, filters, options) - @accepts(roles=['CATALOG_READ']) - @returns(List(items=[Str('category')])) + @api_method(AppCategoriesArgs, AppCategoriesResult, roles=['CATALOG_READ']) async def categories(self): """ Retrieve list of valid categories which have associated applications. """ return sorted(list(await self.middleware.call('catalog.retrieve_mapped_categories'))) - @accepts(Str('app_name'), Str('train'), roles=['CATALOG_READ']) - @returns(List(items=[Ref('available_apps')])) + @api_method(AppSimilarArgs, AppSimilarResult, roles=['CATALOG_READ']) def similar(self, app_name, train): """ Retrieve applications which are similar to `app_name`. diff --git a/src/middlewared/middlewared/plugins/catalog/apps_details.py b/src/middlewared/middlewared/plugins/catalog/apps_details.py index d69cc7cb57aca..2cacd1e46378c 100644 --- a/src/middlewared/middlewared/plugins/catalog/apps_details.py +++ b/src/middlewared/middlewared/plugins/catalog/apps_details.py @@ -11,7 +11,8 @@ from datetime import datetime from jsonschema import validate as json_schema_validate, ValidationError as JsonValidationError -from middlewared.schema import accepts, Bool, Dict, List, returns, Str +from middlewared.api import api_method +from middlewared.api.current import CatalogAppsArgs, CatalogAppsResult from middlewared.service import private, Service from .apps_util import get_app_version_details @@ -45,47 +46,7 @@ def train_to_apps_version_mapping(self): def cached(self, label): return self.middleware.call_sync('cache.has_key', get_cache_key(label)) - @accepts( - Dict( - 'options', - Bool('cache', default=True), - Bool('cache_only', default=False), - Bool('retrieve_all_trains', default=True), - List('trains', items=[Str('train_name')]), - ), - roles=['CATALOG_READ'] - ) - @returns(Dict( - 'trains', - additional_attrs=True, - example={ - 'stable': { - 'plex': { - 'app_readme': '

Plex

', - 'categories': ['media'], - 'description': 'Plex is a media server that allows you to stream your media to any Plex client.', - 'healthy': True, - 'healthy_error': None, - 'home': 'https://plex.tv', - 'location': '/mnt/.ix-apps/truenas_catalog/stable/plex', - 'latest_version': '1.0.0', - 'latest_app_version': '1.40.2.8395', - 'latest_human_version': '1.40.2.8395_1.0.0', - 'last_update': '2024-07-30 13:40:47+00:00', - 'name': 'plex', - 'recommended': False, - 'title': 'Plex', - 'maintainers': [ - {'email': 'dev@ixsystems.com', 'name': 'truenas', 'url': 'https://www.truenas.com/'}, - ], - 'tags': ['plex', 'media', 'entertainment', 'movies', 'series', 'tv', 'streaming'], - 'screenshots': ['https://media.sys.truenas.net/apps/plex/screenshots/screenshot2.png'], - 'sources': ['https://plex.tv', 'https://hub.docker.com/r/plexinc/pms-docker'], - 'icon_url': 'https://media.sys.truenas.net/apps/plex/icons/icon.png' - }, - }, - } - )) + @api_method(CatalogAppsArgs, CatalogAppsResult, roles=['CATALOG_READ']) def apps(self, options): """ Retrieve apps details for `label` catalog. diff --git a/src/middlewared/middlewared/plugins/catalog/sync.py b/src/middlewared/middlewared/plugins/catalog/sync.py index c30274341ec8e..24467b692f4e6 100644 --- a/src/middlewared/middlewared/plugins/catalog/sync.py +++ b/src/middlewared/middlewared/plugins/catalog/sync.py @@ -1,5 +1,6 @@ -from middlewared.schema import accepts -from middlewared.service import job, private, returns, Service +from middlewared.api import api_method +from middlewared.api.current import CatalogSyncArgs, CatalogSyncResult +from middlewared.service import job, private, Service from .git_utils import pull_clone_repository from .utils import OFFICIAL_LABEL, OFFICIAL_CATALOG_REPO, OFFICIAL_CATALOG_BRANCH @@ -13,8 +14,7 @@ class CatalogService(Service): async def synced(self): return self.SYNCED - @accepts(roles=['CATALOG_WRITE']) - @returns() + @api_method(CatalogSyncArgs, CatalogSyncResult, roles=['CATALOG_WRITE']) @job(lock='official_catalog_sync') async def sync(self, job): """ diff --git a/src/middlewared/middlewared/plugins/catalog/update.py b/src/middlewared/middlewared/plugins/catalog/update.py index 499add8c156fb..8334fbfb1bece 100644 --- a/src/middlewared/middlewared/plugins/catalog/update.py +++ b/src/middlewared/middlewared/plugins/catalog/update.py @@ -2,11 +2,13 @@ import middlewared.sqlalchemy as sa +from middlewared.api import api_method +from middlewared.api.current import ( + CatalogEntry, CatalogUpdateArgs, CatalogUpdateResult, CatalogTrainsArgs, CatalogTrainsResult, +) from middlewared.plugins.docker.state_utils import catalog_ds_path, CATALOG_DATASET_NAME -from middlewared.schema import accepts, Dict, List, returns, Str from middlewared.service import ConfigService, private, ValidationErrors from middlewared.utils import ProductType -from middlewared.validators import Match from .utils import OFFICIAL_ENTERPRISE_TRAIN, OFFICIAL_LABEL, TMP_IX_APPS_CATALOGS @@ -29,20 +31,7 @@ class Config: cli_namespace = 'app.catalog' namespace = 'catalog' role_prefix = 'CATALOG' - - ENTRY = Dict( - 'catalog_create', - List('preferred_trains'), - Str('id'), - Str( - 'label', required=True, validators=[Match( - r'^\w+[\w.-]*$', - explanation='Label must start with an alphanumeric character and can include dots and dashes.' - )], - max_length=60, - ), - register=True, - ) + entry = CatalogEntry @private def extend(self, data, context): @@ -52,8 +41,7 @@ def extend(self, data, context): }) return data - @accepts() - @returns(List('trains', items=[Str('train')])) + @api_method(CatalogTrainsArgs, CatalogTrainsResult, roles=['CATALOG_READ']) async def trains(self): """ Retrieve available trains. @@ -103,13 +91,7 @@ async def common_validation(self, schema, data): verrors.check() - @accepts( - Dict( - 'catalog_update', - List('preferred_trains'), - update=True - ) - ) + @api_method(CatalogUpdateArgs, CatalogUpdateResult) async def do_update(self, data): """ Update catalog preferences. diff --git a/tests/api2/test_catalog_roles.py b/tests/api2/test_catalog_roles.py index 4233c88b353eb..f48370579a5b1 100644 --- a/tests/api2/test_catalog_roles.py +++ b/tests/api2/test_catalog_roles.py @@ -19,9 +19,9 @@ ('app.similar', 'CATALOG_READ', True, True), ('app.similar', 'CATALOG_WRITE', True, True), ('app.similar', 'APPS_WRITE', True, True), - ('catalog.apps', 'CATALOG_READ', True, False), - ('catalog.apps', 'CATALOG_WRITE', True, False), - ('catalog.apps', 'DOCKER_READ', False, False), + ('catalog.apps', 'CATALOG_READ', True, True), + ('catalog.apps', 'CATALOG_WRITE', True, True), + ('catalog.apps', 'DOCKER_READ', False, True), ('catalog.sync', 'CATALOG_READ', False, False), ('catalog.sync', 'CATALOG_WRITE', True, False), ('catalog.update', 'CATALOG_READ', False, True),