diff --git a/sdks/apigw-manager/CHANGE.md b/sdks/apigw-manager/CHANGE.md index e5609dc5..f6f4e910 100644 --- a/sdks/apigw-manager/CHANGE.md +++ b/sdks/apigw-manager/CHANGE.md @@ -1,5 +1,10 @@ ## Change logs +### 2.0.2 +- 优化请求 bk-apigateway 接口失败时,打印的错误消息 +- 添加指令 add_related_apps,支持为网关添加关联应用 +- definition.yaml 添加 spec_version 字段,指定配置文件版本号 + ### 2.0.1 - 修复镜像 sync-apigateway 中,同步任务失败时,脚本退出码为 0 的问题 diff --git a/sdks/apigw-manager/definition.yaml b/sdks/apigw-manager/definition.yaml index da11e81d..d8dab221 100644 --- a/sdks/apigw-manager/definition.yaml +++ b/sdks/apigw-manager/definition.yaml @@ -1,3 +1,6 @@ +# definition.yaml 配置文件版本号,必填,固定值 1 +spec_version: 1 + release: # 发布版本号, # 资源配置更新,需更新此版本号才会发布资源版本,此版本号和 sdk 版本号一致,错误设置会影响调用方使用 @@ -47,11 +50,17 @@ stage: # 主动授权,网关主动给应用,添加访问网关所有资源的权限 grant_permissions: - bk_app_code: "{{ settings.BK_APP_CODE }}" + # 按网关授权,包括网关下所有资源,包括未来新创建的资源 + grant_dimension: "gateway" # 应用申请指定网关所有资源的权限,待网关管理员审批后,应用才可访问网关资源 apply_permissions: - api_name: "{{ settings.BK_APIGW_NAME }}" +# 为网关添加关联应用,关联应用可以通过网关 bk-apigateway 的接口操作网关数据;每个网关最多可有 10 个关联应用 +related_apps: + - "{{ settings.BK_APP_CODE }}" + # 网关资源,建议资源配置采用单独的配置文件,可通过网关管理端导出资源配置 resources: swagger: "2.0" diff --git a/sdks/apigw-manager/pyproject.toml b/sdks/apigw-manager/pyproject.toml index 309c14ac..21956fe6 100644 --- a/sdks/apigw-manager/pyproject.toml +++ b/sdks/apigw-manager/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "apigw-manager" -version = "2.0.1" +version = "2.0.2" description = "The SDK for managing blueking gateway resource." readme = "README.md" authors = ["blueking "] diff --git a/sdks/apigw-manager/src/apigw_manager/apigw/command.py b/sdks/apigw-manager/src/apigw_manager/apigw/command.py index 9d26ab7e..9ca9ab1a 100644 --- a/sdks/apigw-manager/src/apigw_manager/apigw/command.py +++ b/sdks/apigw-manager/src/apigw_manager/apigw/command.py @@ -9,6 +9,7 @@ * specific language governing permissions and limitations under the License. """ import os +import sys import typing from django.conf import settings @@ -16,6 +17,7 @@ from apigw_manager.apigw.helper import Definition from apigw_manager.apigw.utils import get_configuration, parse_value_list +from apigw_manager.core.exceptions import ApiResponseError from apigw_manager.core.fetch import Fetcher from apigw_manager.core.permission import Manager as PermissionManager from apigw_manager.core.sync import Synchronizer @@ -32,6 +34,19 @@ def add_arguments(self, parser): def get_configuration(self, **kwargs): return get_configuration(**{k: v for k, v in kwargs.items() if v is not None}) + def handle(self, *args, **kwargs): + configuration = self.get_configuration(**kwargs) + manager = self.manager_class(configuration) + + try: + self.do(manager=manager, configuration=configuration, *args, **kwargs) + except ApiResponseError as err: + self.stderr.write(str(err)) + sys.exit(1) + + def do(self, manager, configuration, *args, **kwargs): + pass + class DefinitionCommand(ApiCommand): default_namespace = "" @@ -67,16 +82,20 @@ def get_definition(self, define, file, namespace, **kwargs): return definition.get(namespace, {}) - def do(self, manager, definition, *args, **kwargs): - pass - def handle(self, *args, **kwargs): configuration = self.get_configuration(**kwargs) manager = self.manager_class(configuration) definition = self.get_definition(**kwargs) - self.do(manager=manager, definition=definition, configuration=configuration, *args, **kwargs) + try: + self.do(manager=manager, definition=definition, configuration=configuration, *args, **kwargs) + except ApiResponseError as err: + self.stderr.write(str(err)) + sys.exit(1) + + def do(self, manager, definition, *args, **kwargs): + pass class FetchCommand(ApiCommand): diff --git a/sdks/apigw-manager/src/apigw_manager/apigw/helper.py b/sdks/apigw-manager/src/apigw_manager/apigw/helper.py index cdf63aaf..65a1b87d 100644 --- a/sdks/apigw-manager/src/apigw_manager/apigw/helper.py +++ b/sdks/apigw-manager/src/apigw_manager/apigw/helper.py @@ -27,6 +27,8 @@ class Definition: """Gateway model definitions""" + valid_spec_versions = ["1"] + @classmethod def load_from(cls, path, dictionary): with open(path) as fp: @@ -41,7 +43,20 @@ def load(cls, definition, dictionary): return cls(rendered) def __init__(self, definition): - self.loaded = yaml_load(definition) + loaded = yaml_load(definition) + + self._check_spec_version(loaded) + + self.loaded = loaded + + def _check_spec_version(self, definition): + spec_version = definition.get("spec_version") + if not spec_version: + logger.warning("please add `spec_version: 1` to definition.yaml") + return + + if str(spec_version) not in self.valid_spec_versions: + raise ValueError("spec_version configured in definition.yaml is wrong, choices: 1") def _get_namespace_list(self, namespace): if not namespace: diff --git a/sdks/apigw-manager/src/apigw_manager/apigw/management/commands/add_related_apps.py b/sdks/apigw-manager/src/apigw_manager/apigw/management/commands/add_related_apps.py new file mode 100644 index 00000000..f5a1e5d3 --- /dev/null +++ b/sdks/apigw-manager/src/apigw_manager/apigw/management/commands/add_related_apps.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +""" + * TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. + * Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://opensource.org/licenses/MIT + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. +""" +from apigw_manager.apigw.command import SyncCommand +from apigw_manager.core.exceptions import ApiResponseError + + +class Command(SyncCommand): + """Add related apps for gateway""" + + default_namespace = "related_apps" + + def do(self, manager, definition, *args, **kwargs): + if not definition: + print("no related apps found, skip") + return + + try: + manager.add_related_apps(target_app_codes=definition) + except ApiResponseError as err: + print("warning!! Add related apps error, %s" % str(err)) + return + + print( + "Add related apps for gateway %s: %s" + % ( + manager.config.api_name, + ", ".join(definition) + ) + ) diff --git a/sdks/apigw-manager/src/apigw_manager/apigw/management/commands/fetch_apigw_public_key.py b/sdks/apigw-manager/src/apigw_manager/apigw/management/commands/fetch_apigw_public_key.py index 2b12e430..1f454a9a 100644 --- a/sdks/apigw-manager/src/apigw_manager/apigw/management/commands/fetch_apigw_public_key.py +++ b/sdks/apigw-manager/src/apigw_manager/apigw/management/commands/fetch_apigw_public_key.py @@ -8,6 +8,8 @@ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. """ +import sys + from apigw_manager.apigw import helper from apigw_manager.apigw.command import FetchCommand @@ -25,9 +27,7 @@ def add_arguments(self, parser): parser.add_argument("--bk-app-secret", dest="bk_app_secret", help="app secret") parser.add_argument("--no-save", default=False, action="store_true", help="do not save the public key") - def handle(self, print_, no_save, *args, **kwargs): - configuration = self.get_configuration(**kwargs) - manager = self.manager_class(configuration) + def do(self, manager, configuration, print_, no_save, *args, **kwargs): result = manager.public_key() if print_: diff --git a/sdks/apigw-manager/src/apigw_manager/apigw/management/commands/grant_apigw_permissions.py b/sdks/apigw-manager/src/apigw_manager/apigw/management/commands/grant_apigw_permissions.py index 7187c52c..f2d9038f 100644 --- a/sdks/apigw-manager/src/apigw_manager/apigw/management/commands/grant_apigw_permissions.py +++ b/sdks/apigw-manager/src/apigw_manager/apigw/management/commands/grant_apigw_permissions.py @@ -23,14 +23,18 @@ def do(self, manager, definition, *args, **kwargs): for permission in definition: permission.setdefault("target_app_code", permission.pop("bk_app_code", None)) - permission.setdefault("grant_dimension", "api") - manager.grant_permission(**permission) + grant_dimension = permission.pop("grant_dimension", "api") + if grant_dimension == "gateway": + grant_dimension = "api" + + manager.grant_permission(grant_dimension=grant_dimension, **permission) + print( - "Granted API gateway %s permission for app code %s, dimension %s" + "Granted gateway %s permission for app code %s, dimension %s" % ( manager.config.api_name, permission["target_app_code"], - permission["grant_dimension"], + grant_dimension, ) ) diff --git a/sdks/apigw-manager/src/apigw_manager/core/exceptions.py b/sdks/apigw-manager/src/apigw_manager/core/exceptions.py index e7ca9bed..9ab88e26 100644 --- a/sdks/apigw-manager/src/apigw_manager/core/exceptions.py +++ b/sdks/apigw-manager/src/apigw_manager/core/exceptions.py @@ -18,20 +18,18 @@ def __str__(self): return self.operation_id -class ApiResultError(Exception): - def __init__(self, code, message): - self.code = code +class ApiResponseError(Exception): + """There was an exception that occurred while handling the response""" + def __init__(self, message): self.message = message def __str__(self): - return "code: %s, %s" % (self.code, self.message) + return self.message - -class ApiRequestError(Exception): - def __init__(self, status_code, code, message): - self.status_code = status_code +class ApiResultError(ApiResponseError): + def __init__(self, code, message): self.code = code self.message = message def __str__(self): - return "status_code: %s, code: %s, %s" % (self.status_code, self.code, self.message) + return "code: %s, %s" % (self.code, self.message) \ No newline at end of file diff --git a/sdks/apigw-manager/src/apigw_manager/core/handler.py b/sdks/apigw-manager/src/apigw_manager/core/handler.py index 4ca73f48..125161b4 100644 --- a/sdks/apigw-manager/src/apigw_manager/core/handler.py +++ b/sdks/apigw-manager/src/apigw_manager/core/handler.py @@ -15,8 +15,8 @@ from bkapi.bk_apigateway.client import Client as BKAPIGatewayClient from future.utils import raise_from -from apigw_manager.core.exceptions import ApiException, ApiRequestError, ApiResultError -from bkapi_client_core.exceptions import HTTPResponseError +from apigw_manager.core.exceptions import ApiException, ApiResponseError, ApiResultError +from bkapi_client_core.exceptions import ResponseError if typing.TYPE_CHECKING: from apigw_manager.core import configuration @@ -92,8 +92,9 @@ def _call(self, operation, files=None, **kwargs): try: return operation(**data) - except HTTPResponseError as err: - raise_from(self._convert_operation_exception(err), err) + except ResponseError as err: + message = "%s\n%s\nResponse: %s" % (err, err.curl_command, err.response_text) + raise ApiResponseError(message) except Exception as err: raise_from(ApiException(operation_id), err) @@ -107,19 +108,3 @@ def _parse_result(self, result, convertor, code=0): ) return convertor(result) - - def _convert_operation_exception(self, err: HTTPResponseError): - response = err.response - if response is None or response.status_code / 100 != 4: - return err - - try: - result = response.json() - except Exception: - return err - - return ApiRequestError( - response.status_code, - result.get("code"), - result.get("message"), - ) diff --git a/sdks/apigw-manager/src/apigw_manager/core/sync.py b/sdks/apigw-manager/src/apigw_manager/core/sync.py index 5cb8c553..e4612c08 100644 --- a/sdks/apigw-manager/src/apigw_manager/core/sync.py +++ b/sdks/apigw-manager/src/apigw_manager/core/sync.py @@ -34,3 +34,7 @@ def sync_resources_config(self, content, *args, **kwargs): def sync_resource_docs_by_archive(self, *args, **kwargs): result = self._call(self.client.api.import_resource_docs_by_archive, *args, **kwargs) return self._parse_result(result, itemgetter("data")) + + def add_related_apps(self, *args, **kwargs): + result = self._call(self.client.api.add_related_apps, *args, **kwargs) + return self._parse_result(result, itemgetter("data")) \ No newline at end of file diff --git a/sdks/apigw-manager/tests/apigw_manager/core/test_hander.py b/sdks/apigw-manager/tests/apigw_manager/core/test_hander.py index df5e4af4..2a963ce3 100644 --- a/sdks/apigw-manager/tests/apigw_manager/core/test_hander.py +++ b/sdks/apigw-manager/tests/apigw_manager/core/test_hander.py @@ -11,7 +11,7 @@ from faker import Faker from pytest import fixture, raises -from apigw_manager.core.exceptions import ApiRequestError +from apigw_manager.core.exceptions import ApiResponseError from apigw_manager.core.handler import Handler from bkapi_client_core.exceptions import HTTPResponseError @@ -90,12 +90,12 @@ def test_call_with_cache(self, handler: Handler, operation, operation_id, faker, def test_call_connect_error(self, handler: Handler, operation): operation.side_effect = HTTPResponseError() - with raises(HTTPResponseError): + with raises(ApiResponseError): handler._call(operation) def test_call_server_error(self, handler: Handler, operation, mocker): operation.side_effect = HTTPResponseError(response=mocker.MagicMock(status_code=500)) - with raises(HTTPResponseError): + with raises(ApiResponseError): handler._call(operation) def test_call_request_error(self, handler: Handler, operation, mocker): @@ -106,7 +106,7 @@ def test_call_request_error(self, handler: Handler, operation, mocker): ) ) - with raises(ApiRequestError): + with raises(ApiResponseError): handler._call(operation) def test_call_request_error_with_no_json(self, handler: Handler, operation, mocker): @@ -117,5 +117,5 @@ def test_call_request_error_with_no_json(self, handler: Handler, operation, mock ) ) - with raises(HTTPResponseError): + with raises(ApiResponseError): handler._call(operation)