diff --git a/apps/authentication.py b/apps/authentication.py new file mode 100644 index 0000000..91443e4 --- /dev/null +++ b/apps/authentication.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸 (Blueking) 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 https://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 blueapps.account import get_user_model + +from common.log import logger +from django.contrib.auth.backends import ModelBackend +from django.contrib.auth.models import AnonymousUser + + +class ApiGatewayJWTUserModelBackend(ModelBackend): + """Get users by username""" + + def user_maker(self, bk_username): + user_model = get_user_model() + try: + user, _ = user_model.objects.get_or_create(defaults={"nickname": bk_username}, username=bk_username) + except Exception: + logger.exception(f"[{self.__class__.__name__}] Failed to get_or_create user -> {bk_username}.") + return None + else: + logger.info(f"test-user{user.username}, {user.is_superuser}") + return user + + def make_anonymous_user(self, bk_username=None): + user = AnonymousUser() + user.username = bk_username # type: ignore + return user + + def authenticate(self, request, gateway_name, bk_username, verified, **credentials): + if not verified: + return self.make_anonymous_user(bk_username=bk_username) + + return self.user_maker(bk_username) diff --git a/apps/core/gray/views.py b/apps/core/gray/views.py index 850756e..8e94469 100644 --- a/apps/core/gray/views.py +++ b/apps/core/gray/views.py @@ -25,25 +25,31 @@ class GrayViewSet(APIViewSet): permission_classes = (permission.GrayPermission,) @swagger_auto_schema( + operation_id="gray_build", operation_summary="GSE 2.0灰度", tags=GRAY_VIEW_TAGS, + extra_overrides={"is_register_apigw": True}, ) @action(detail=False, methods=["POST"], serializer_class=serializers.GraySerializer) def build(self, request): return Response(handlers.GrayHandler.build(self.validated_data)) @swagger_auto_schema( + operation_id="gray_rollback", operation_summary="GSE 2.0灰度回滚", tags=GRAY_VIEW_TAGS, + extra_overrides={"is_register_apigw": True}, ) @action(detail=False, methods=["POST"], serializer_class=serializers.GraySerializer) def rollback(self, request): return Response(handlers.GrayHandler.rollback(self.validated_data)) @swagger_auto_schema( + operation_id="gray_info", operation_summary="获取GSE 2.0灰度信息", tags=GRAY_VIEW_TAGS, responses={status.HTTP_200_OK: serializers.GraySerializer}, + extra_overrides={"is_register_apigw": True}, ) @action(detail=False, methods=["GET"]) def info(self, request): diff --git a/apps/gsekit/configfile/views/config_template.py b/apps/gsekit/configfile/views/config_template.py index 1084756..8d54148 100644 --- a/apps/gsekit/configfile/views/config_template.py +++ b/apps/gsekit/configfile/views/config_template.py @@ -76,10 +76,12 @@ def get_serializer_class(self, *args, **kwargs): return serializer_class @swagger_auto_schema( + operation_id="config_template_list", operation_summary="获取配置模板列表", tags=ConfigTemplateViewTags, query_serializer=config_template_serializer.ListConfigTemplateRequestSerializer(), responses={status.HTTP_200_OK: config_template_serializer.ListConfigTemplateResponseSerializer()}, + extra_overrides={"is_register_apigw": True}, ) @insert_permission_field( id_field=lambda d: d["config_template_id"], diff --git a/apps/gsekit/job/views.py b/apps/gsekit/job/views.py index 80eec43..091babb 100644 --- a/apps/gsekit/job/views.py +++ b/apps/gsekit/job/views.py @@ -80,7 +80,11 @@ def retrieve(self, request, *args, **kwargs): return super().retrieve(request, *args, **kwargs) @swagger_auto_schema( - operation_summary="创建任务", tags=JobViewTags, request_body=job_serializers.CreateJobRequestSerializer() + operation_id="create_job", + operation_summary="创建任务", + tags=JobViewTags, + request_body=job_serializers.CreateJobRequestSerializer(), + extra_overrides={"is_register_apigw": True}, ) def create(self, request, bk_biz_id, *args, **kwargs): self.serializer_class = job_serializers.CreateJobRequestSerializer @@ -120,9 +124,11 @@ def job_task_statistics(self, request, *args, **kwargs): return Response(JobHandlers(bk_biz_id=kwargs.get("bk_biz_id"), job_id=kwargs["pk"]).job_task_statistics()) @swagger_auto_schema( + operation_id="job_status", operation_summary="任务状态查询", tags=JobViewTags, responses={status.HTTP_200_OK: job_serializers.JobTaskResponseSerializer()}, + extra_overrides={"is_register_apigw": True}, ) @action(methods=["POST"], detail=True, serializer_class=job_serializers.RetryRequestSerializer) def job_status(self, request, *args, **kwargs): diff --git a/apps/gsekit/management/commands/generate_swagger_apigw.py b/apps/gsekit/management/commands/generate_swagger_apigw.py new file mode 100644 index 0000000..8eaf9db --- /dev/null +++ b/apps/gsekit/management/commands/generate_swagger_apigw.py @@ -0,0 +1,344 @@ +import os +import copy +import logging + +from django.contrib.auth import get_user_model +from django.core.exceptions import ImproperlyConfigured +from django.core.management.base import BaseCommand +from django.urls import get_script_prefix + +from rest_framework.settings import api_settings +from rest_framework.test import APIRequestFactory, force_authenticate +from rest_framework.views import APIView + +from drf_yasg import openapi +from drf_yasg.app_settings import swagger_settings +from drf_yasg.codecs import OpenAPICodecJson, OpenAPICodecYaml, VALIDATORS +from drf_yasg.generators import OpenAPISchemaGenerator +from drf_yasg.utils import get_consumes, get_produces +from drf_yasg.errors import SwaggerValidationError + +from coreapi.compat import urlparse +from coreapi.compat import force_bytes + + +class ApigwOpenAPICodecYaml(OpenAPICodecYaml): + def encode(self, document): + + spec = self.generate_swagger_object(document) + errors = {} + for validator in self.validators: + try: + VALIDATORS[validator](copy.deepcopy(spec)) + except SwaggerValidationError as e: + errors[validator] = str(e) + + if errors: + raise SwaggerValidationError("spec validation failed: {}".format(errors), errors, spec, self) + + return force_bytes(self._dump_dict(spec)) + + +class PathItem(openapi.PathItem): + def __init__(self, get=None, put=None, post=None, delete=None, options=None, head=None, patch=None, **extra): + super().__init__(**extra) + self.get = get + self.head = head + self.post = post + self.put = put + self.patch = patch + self.delete = delete + self.options = options + self._insert_extras__() + + +class ApigwSwagger(openapi.SwaggerDict): + def __init__( + self, + info=None, + _url=None, + _prefix=None, + _version=None, + consumes=None, + produces=None, + security_definitions=None, + security=None, + paths=None, + definitions=None, + **extra + ): + """Root Swagger object.""" + super(ApigwSwagger, self).__init__(**extra) + self.swagger = "2.0" + self.info = info + self.info.version = _version or info._default_version + + if _url: + url = urlparse.urlparse(_url) + assert url.netloc and url.scheme, "if given, url must have both schema and netloc" + self.host = url.netloc + self.schemes = [url.scheme] + + self.base_path = self.get_base_path(get_script_prefix(), _prefix) + self.paths = paths + self._insert_extras__() + + @classmethod + def get_base_path(cls, script_prefix, api_prefix): + # avoid double slash when joining script_name with api_prefix + if script_prefix and script_prefix.endswith("/"): + script_prefix = script_prefix[:-1] + if not api_prefix.startswith("/"): + api_prefix = "/" + api_prefix + + base_path = script_prefix + api_prefix + + # ensure that the base path has a leading slash and no trailing slash + if base_path and base_path.endswith("/"): + base_path = base_path[:-1] + if not base_path.startswith("/"): + base_path = "/" + base_path + + return base_path + + +class ApigwOpenAPISchemaGenerator(OpenAPISchemaGenerator): + def get_schema(self, request=None, public=False): + """Generate a :class:`.Swagger` object representing the API schema.""" + endpoints = self.get_endpoints(request) + components = self.reference_resolver_class(openapi.SCHEMA_DEFINITIONS, force_init=True) + self.consumes = get_consumes(api_settings.DEFAULT_PARSER_CLASSES) + self.produces = get_produces(api_settings.DEFAULT_RENDERER_CLASSES) + paths, prefix = self.get_paths(endpoints, components, request, public) + + security_definitions = self.get_security_definitions() + if security_definitions: + security_requirements = self.get_security_requirements(security_definitions) + else: + security_requirements = None + + url = self.url + if url is None and request is not None: + url = request.build_absolute_uri() + + return ApigwSwagger( + info=self.info, + paths=paths, + consumes=self.consumes or None, + produces=self.produces or None, + security_definitions=security_definitions, + security=security_requirements, + _url=url, + _prefix=prefix, + _version=self.version, + **dict(components) + ) + + def get_path_item(self, path, view_cls, operations): + return PathItem(**operations) + + def get_operation(self, view, path, prefix, method, components, request): + """Get an :class:`.Operation` for the given API endpoint (path, method). This method delegates to""" + overrides = self.get_overrides(view, method) + if not overrides.get("extra_overrides", {}).get("is_register_apigw", False): + return None + + operation = super().get_operation(view, path, prefix, method, components, request) + + apigw_operation = { + "operationId": operation["operationId"], + "summary": operation["summary"], + "description": operation["description"], + "tags": operation["tags"], + "x-bk-apigateway-resource": { + "isPublic": True, + "allowApplyPermission": True, + "matchSubpath": False, + "backend": { + "name": "default", + "method": method.lower(), + "path": path, + "matchSubpath": False, + "timeout": 0, + }, + "authConfig": { + "userVerifiedRequired": True, + "appVerifiedRequired": True, + "resourcePermissionRequired": True, + }, + }, + } + + return apigw_operation + + +class Command(BaseCommand): + help = "Write the Swagger schema to disk in JSON or YAML format." + + def add_arguments(self, parser): + parser.add_argument( + "output_file", + metavar="output-file", + nargs="?", + default="-", + type=str, + help='Output path for generated swagger document, or "-" for stdout.', + ) + parser.add_argument( + "-o", + "--overwrite", + default=False, + action="store_true", + help="Overwrite the output file if it already exists. " + "Default behavior is to stop if the output file exists.", + ) + parser.add_argument( + "-f", + "--format", + dest="format", + default="", + choices=["json", "yaml"], + type=str, + help="Output format. If not given, it is guessed from the output file extension and defaults to json.", + ) + parser.add_argument( + "-u", + "--url", + dest="api_url", + default="", + type=str, + help="Base API URL - sets the host and scheme attributes of the generated document.", + ) + parser.add_argument( + "-m", + "--mock-request", + dest="mock", + default=False, + action="store_true", + help="Use a mock request when generating the swagger schema. This is useful if your views or serializers " + "depend on context from a request in order to function.", + ) + parser.add_argument( + "--api-version", + dest="api_version", + type=str, + help="Version to use to generate schema. This option implies --mock-request.", + ) + parser.add_argument( + "--user", + dest="user", + help="Username of an existing user to use for mocked authentication. This option implies --mock-request.", + ) + parser.add_argument( + "-p", + "--private", + default=False, + action="store_true", + help="Hides endpoints not accesible to the target user. If --user is not given, only shows endpoints that " + "are accesible to unauthenticated users.\n" + "This has the same effect as passing public=False to get_schema_view() or " + "OpenAPISchemaGenerator.get_schema().\n" + "This option implies --mock-request.", + ) + parser.add_argument( + "-g", + "--generator-class", + dest="generator_class_name", + default="", + help="Import string pointing to an OpenAPISchemaGenerator subclass to use for schema generation.", + ) + + def write_schema(self, schema, stream, format): + if format == "json": + codec = OpenAPICodecJson(validators=[], pretty=True) + swagger_json = codec.encode(schema).decode("utf-8") + stream.write(swagger_json) + elif format == "yaml": + codec = ApigwOpenAPICodecYaml(validators=[]) + swagger_yaml = codec.encode(schema).decode("utf-8") + # YAML is already pretty! + stream.write(swagger_yaml) + else: # pragma: no cover + raise ValueError("unknown format %s" % format) + + def get_mock_request(self, url, format, user=None): + factory = APIRequestFactory() + + request = factory.get(url + "/swagger." + format) + if user is not None: + force_authenticate(request, user=user) + request = APIView().initialize_request(request) + return request + + def get_schema_generator(self, generator_class_name, api_info, api_version, api_url): + generator_class = ApigwOpenAPISchemaGenerator + + return generator_class( + info=api_info, + version=api_version, + url=api_url, + ) + + def get_schema(self, generator, request, public): + return generator.get_schema(request=request, public=public) + + def handle( + self, + output_file, + overwrite, + format, + api_url, + mock, + api_version, + user, + private, + generator_class_name, + *args, + **kwargs + ): + # disable logs of WARNING and below + logging.disable(logging.WARNING) + + info = getattr(swagger_settings, "DEFAULT_INFO", None) + if not isinstance(info, openapi.Info): + raise ImproperlyConfigured( + 'settings.SWAGGER_SETTINGS["DEFAULT_INFO"] should be an ' + "import string pointing to an openapi.Info object" + ) + + if not format: + if os.path.splitext(output_file)[1] in (".yml", ".yaml"): + format = "yaml" + format = format or "json" + + api_url = api_url or swagger_settings.DEFAULT_API_URL + + if user: + # Only call get_user_model if --user was passed in order to + # avoid crashing if auth is not configured in the project + user = get_user_model().objects.get(**{get_user_model().USERNAME_FIELD: user}) + + mock = mock or private or (user is not None) or (api_version is not None) + if mock and not api_url: + raise ImproperlyConfigured( + "--mock-request requires an API url; either provide " + "the --url argument or set the DEFAULT_API_URL setting" + ) + + request = None + if mock: + request = self.get_mock_request(api_url, format, user) + + api_version = api_version or api_settings.DEFAULT_VERSION + if request and api_version: + request.version = api_version + + generator = self.get_schema_generator(generator_class_name, info, api_version, api_url) + schema = self.get_schema(generator, request, not private) + + if output_file == "-": + self.write_schema(schema, self.stdout, format) + else: + flags = "w" if overwrite else "x" + with open(output_file, flags) as stream: + self.write_schema(schema, stream, format) diff --git a/apps/gsekit/management/commands/sync_apigw.py b/apps/gsekit/management/commands/sync_apigw.py new file mode 100644 index 0000000..587e72d --- /dev/null +++ b/apps/gsekit/management/commands/sync_apigw.py @@ -0,0 +1,29 @@ +from django.conf import settings +from django.core.management.base import BaseCommand +from django.core.management import call_command + + +class Command(BaseCommand): + def handle(self, *args, **kwargs): + if not getattr(settings, "SYNC_APIGATEWAY_ENABLED", False): + return + + # 待同步网关名,需修改为实际网关名;直接指定网关名,则不需要配置 Django settings BK_APIGW_NAME + gateway_name = settings.BK_APIGW_NAME + + # 待同步网关、资源定义文件,需调整为实际的配置文件地址 + definition_path = "support-files/apigw/definition.yaml" + resources_path = "support-files/apigw/resources.yaml" + + call_command("sync_apigw_config", f"--gateway-name={gateway_name}", f"--file={definition_path}") + call_command("sync_apigw_stage", f"--gateway-name={gateway_name}", f"--file={definition_path}") + call_command("sync_apigw_resources", f"--gateway-name={gateway_name}", "--delete", f"--file={resources_path}") + call_command("sync_resource_docs_by_archive", f"--gateway-name={gateway_name}", f"--file={definition_path}") + call_command( + "create_version_and_release_apigw", + f"--gateway-name={gateway_name}", + f"--file={definition_path}", + f"-s {settings.ENVIRONMENT}", + ) + call_command("grant_apigw_permissions", f"--gateway-name={gateway_name}", f"--file={definition_path}") + call_command("fetch_apigw_public_key", f"--gateway-name={gateway_name}") diff --git a/apps/gsekit/meta/views.py b/apps/gsekit/meta/views.py index 5e9a00c..f702efc 100644 --- a/apps/gsekit/meta/views.py +++ b/apps/gsekit/meta/views.py @@ -130,9 +130,11 @@ def expression_match(self, request, *args, **kwargs): return Response(MetaHandler().expression_match(expression, candidates)) @swagger_auto_schema( + operation_id="access_overview", operation_summary="业务接入情况概览", tags=MetaViewTags, responses={status.HTTP_200_OK: meta_serializer.AccessOverviewResponseSerializer()}, + extra_overrides={"is_register_apigw": True}, ) @action(methods=["GET"], detail=False) def access_overview(self, request, bk_biz_id, *args, **kwargs): diff --git a/apps/gsekit/process/views/process.py b/apps/gsekit/process/views/process.py index 5804c12..d61f5d6 100644 --- a/apps/gsekit/process/views/process.py +++ b/apps/gsekit/process/views/process.py @@ -25,6 +25,7 @@ from apps.utils.models import queryset_to_dict_list from apps.utils.drf import GeneralOrderingFilter + ProcessViewTags = ["process"] @@ -43,10 +44,12 @@ def get_permissions(self): return [ViewBusinessPermission()] @swagger_auto_schema( + operation_id="process_status", operation_summary="进程状态列表", tags=ProcessViewTags, request_body=process_serializer.ProcessStatusRequestSerializer(), responses={status.HTTP_200_OK: process_serializer.ProcessStatusResponseSerializer()}, + extra_overrides={"is_register_apigw": True}, ) @action(methods=["POST"], detail=False, serializer_class=process_serializer.ProcessStatusRequestSerializer) def process_status(self, request, bk_biz_id, *args, **kwargs): @@ -209,16 +212,20 @@ def delete_process_template(self, request, bk_biz_id, *args, **kwargs): ) @swagger_auto_schema( + operation_id="flush_process", operation_summary="刷新业务进程缓存", tags=ProcessViewTags, + extra_overrides={"is_register_apigw": True}, ) @action(detail=False, methods=["POST"]) def flush_process(self, request, bk_biz_id, *args, **kwargs): return Response(ProcessHandler(bk_biz_id=bk_biz_id).sync_biz_process()) @swagger_auto_schema( + operation_id="sync_process_status", operation_summary="同步进程状态", tags=ProcessViewTags, + extra_overrides={"is_register_apigw": True}, ) @action(detail=False, methods=["POST"]) def sync_process_status(self, request, bk_biz_id, *args, **kwargs): diff --git a/apps/gsekit/urls.py b/apps/gsekit/urls.py index 3c42ac5..76dcd99 100644 --- a/apps/gsekit/urls.py +++ b/apps/gsekit/urls.py @@ -48,15 +48,16 @@ ] if settings.ENVIRONMENT not in ["production", "prod"]: + openapi_info = openapi.Info( + title=_("{app_name} API").format(app_name=settings.APP_NAME), + default_version="v1", + description=_( + "{app_name} 是腾讯蓝鲸智云推出的一个专注于进程和配置文件管理的 SaaS 工具。\n" + "GitHub 开源,欢迎共建:https://github.com/TencentBlueKing/bk-process-config-manager" + ).format(app_name=settings.APP_NAME), + ) schema_view = get_schema_view( - openapi.Info( - title=_("{app_name} API").format(app_name=settings.APP_NAME), - default_version="v1", - description=_( - "{app_name} 是腾讯蓝鲸智云推出的一个专注于进程和配置文件管理的 SaaS 工具。\n" - "GitHub 开源,欢迎共建:https://github.com/TencentBlueKing/bk-process-config-manager" - ).format(app_name=settings.APP_NAME), - ), + openapi_info, public=True, permission_classes=(permissions.IsAdminUser,), ) diff --git a/bin/post-compile b/bin/post-compile index 430a5fd..fbc975a 100755 --- a/bin/post-compile +++ b/bin/post-compile @@ -1,3 +1,4 @@ #!/bin/bash python manage.py migrate --no-input -python manage.py createcachetable \ No newline at end of file +python manage.py createcachetable +python manage.py sync_apigw \ No newline at end of file diff --git a/config/default.py b/config/default.py index 7edaab0..62d83bf 100644 --- a/config/default.py +++ b/config/default.py @@ -46,11 +46,15 @@ "django_dbconn_retry", # django_prometheus "django_prometheus", + "apigw_manager.apigw", ) # 自定义中间件 MIDDLEWARE += ( - "blueapps.account.middlewares.BkJwtLoginRequiredMiddleware", + # "blueapps.account.middlewares.BkJwtLoginRequiredMiddleware", + "apigw_manager.apigw.authentication.ApiGatewayJWTGenericMiddleware", + "apigw_manager.apigw.authentication.ApiGatewayJWTAppMiddleware", + "apigw_manager.apigw.authentication.ApiGatewayJWTUserMiddleware", "apps.middlewares.CommonMid", "apps.middlewares.UserLocalMiddleware", ) @@ -71,7 +75,13 @@ # =============================================================================== # Authentication # =============================================================================== -AUTHENTICATION_BACKENDS += ("blueapps.account.backends.BkJwtBackend",) +AUTHENTICATION_BACKENDS = ( + "apps.authentication.ApiGatewayJWTUserModelBackend", + "blueapps.account.backends.BkJwtBackend", + "blueapps.account.backends.RioBackend", + "blueapps.account.backends.WeixinBackend", + "blueapps.account.backends.UserBackend", +) # 所有环境的日志级别可以在这里配置 # LOG_LEVEL = 'INFO' @@ -225,6 +235,10 @@ "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), } +SWAGGER_SETTINGS = { + "DEFAULT_INFO": "apps.gsekit.urls.openapi_info", +} + # 并发请求数 CONCURRENT_NUMBER = int(os.getenv("BKAPP_CONCURRENT_NUMBER", 50)) @@ -255,6 +269,12 @@ # 平台公共信息 BKPAAS_SHARED_RES_URL = os.getenv("BKPAAS_SHARED_RES_URL", "") +# 网关相关配置 +SYNC_APIGATEWAY_ENABLED = os.getenv("SYNC_APIGATEWAY_ENABLED", False) +BK_APIGW_NAME = "bk-gsekit" +BK_API_URL_TMPL = os.getenv("BK_API_URL_TMPL") +BK_GSEKIT_API_HOST = os.getenv("BK_GSEKIT_API_HOST", BK_SAAS_HOST) + # ============================================================================== # Cache # ============================================================================== diff --git a/requirements.txt b/requirements.txt index 9fb3c21..4afff6f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -55,6 +55,8 @@ lxml==4.6.3 ruamel.yaml==0.17.16 packaging==20.9 +apigw-manager[cryptography]==3.0.5 + # prometheus django-prometheus==2.2.0 diff --git a/support-files/apigw/apidocs/zh/gray_build.md b/support-files/apigw/apidocs/zh/gray_build.md new file mode 100644 index 0000000..11d0579 --- /dev/null +++ b/support-files/apigw/apidocs/zh/gray_build.md @@ -0,0 +1,33 @@ +### 描述 + +GSE 2.0灰度 + +### 输入参数 + +| 参数名称 | 参数类型 | 必选 | 描述 | +| ------------ | ------------ | ------ | ---------------- | +| bk_biz_ids | [integer] | 是 | 业务ID | + + +### 调用示例 +```json +{ + "bk_biz_ids": [ + 0 + ] +} +``` + +### 响应示例 +```json +{ + "result": true, + "data": { + "bk_biz_ids": [ + 0 + ] + }, + "code": 0, + "message": "" +} +``` diff --git a/support-files/apigw/apidocs/zh/gray_info.md b/support-files/apigw/apidocs/zh/gray_info.md new file mode 100644 index 0000000..9e72323 --- /dev/null +++ b/support-files/apigw/apidocs/zh/gray_info.md @@ -0,0 +1,22 @@ +### 描述 + +GSE 2.0灰度信息 + + +### 调用示例 +```json +``` + +### 响应示例 +```json +{ + "result": true, + "data": { + "bk_biz_ids": [ + 0 + ] + }, + "code": 0, + "message": "" +} +``` diff --git a/support-files/apigw/apidocs/zh/gray_rollback.md b/support-files/apigw/apidocs/zh/gray_rollback.md new file mode 100644 index 0000000..74633b1 --- /dev/null +++ b/support-files/apigw/apidocs/zh/gray_rollback.md @@ -0,0 +1,33 @@ +### 描述 + +GSE 2.0回滚 + +### 输入参数 + +| 参数名称 | 参数类型 | 必选 | 描述 | +| ------------ | ------------ | ------ | ---------------- | +| bk_biz_ids | [integer] | 是 | 业务ID | + + +### 调用示例 +```json +{ + "bk_biz_ids": [ + 0 + ] +} +``` + +### 响应示例 +```json +{ + "result": true, + "data": { + "bk_biz_ids": [ + 0 + ] + }, + "code": 0, + "message": "" +} +``` diff --git a/support-files/apigw/definition.yaml b/support-files/apigw/definition.yaml new file mode 100644 index 0000000..d1f2ccf --- /dev/null +++ b/support-files/apigw/definition.yaml @@ -0,0 +1,50 @@ +spec_version: 2 + +release: + # 发布版本号 + version: 1.0.9 + title: "API 初始化" + comment: "API 初始化" + +# 定义网关基本信息,用于命令 `sync_apigw_config` +apigateway: + description: "进程配置管理 API" + # 网关的英文描述,蓝鲸官方网关需提供英文描述,以支持国际化 + description_en: "GSEKit API" + is_public: true + api_type: 1 + maintainers: + - "noahchi" + +# 定义环境信息,用于命令 `sync_apigw_stage` +stages: + - name: "{{ settings.ENVIRONMENT }}" +{% if settings.ENVIRONMENT == "prod" %} + description: "正式环境" + description_en: "Prod" +{% elif settings.ENVIRONMENT == "stag" %} + description: "预发布环境" + description_en: "Test" +{% else %} + description: "开发测试环境" + description_en: "Development" +{% endif %} + backends: + - name: "default" + config: + timeout: 180 + loadbalance: "roundrobin" + hosts: + # 网关调用后端服务的默认域名或IP,不包含Path,比如: http://api.example.com + - host: "{{ settings.BK_GSEKIT_API_HOST }}" + weight: 100 +grant_permissions: + - bk_app_code: "{{ settings.APP_CODE }}" + grant_dimension: "gateway" + +resource_docs: + # 资源文档的归档文件,可为 tar.gz,zip 格式文件;创建归档文件可使用指令 `tar czvf xxx.tgz en zh` + # archivefile: "{{ settings.BK_APIGW_RESOURCE_DOCS_ARCHIVE_FILE }}" + # 资源文档目录,basedir 与 archivefile 二者至少一个有效,若同时存在,则 archivefile 优先 + # basedir: "{{ settings.BK_APIGW_RESOURCE_DOCS_BASE_DIR }}" + basedir: "support-files/apigw/apidocs" \ No newline at end of file diff --git a/support-files/apigw/resources.yaml b/support-files/apigw/resources.yaml new file mode 100644 index 0000000..1fc49c0 --- /dev/null +++ b/support-files/apigw/resources.yaml @@ -0,0 +1,219 @@ +swagger: '2.0' +info: + title: GSEKit API + description: |- + GSEKit 是腾讯蓝鲸智云推出的一个专注于进程和配置文件管理的 SaaS 工具。 + GitHub 开源,欢迎共建:https://github.com/TencentBlueKing/bk-process-config-manager + version: v1 +basePath: / +paths: + /api/core/gray/build/: + post: + operationId: gray_build + summary: "GSE 2.0\u7070\u5EA6" + description: '' + tags: + - gray + x-bk-apigateway-resource: + isPublic: true + allowApplyPermission: true + matchSubpath: false + backend: + name: default + method: post + path: /api/core/gray/build/ + matchSubpath: false + timeout: 0 + authConfig: + userVerifiedRequired: true + appVerifiedRequired: true + resourcePermissionRequired: true + /api/core/gray/info/: + get: + operationId: gray_info + summary: "\u83B7\u53D6GSE 2.0\u7070\u5EA6\u4FE1\u606F" + description: '' + tags: + - gray + x-bk-apigateway-resource: + isPublic: true + allowApplyPermission: true + matchSubpath: false + backend: + name: default + method: get + path: /api/core/gray/info/ + matchSubpath: false + timeout: 0 + authConfig: + userVerifiedRequired: true + appVerifiedRequired: true + resourcePermissionRequired: true + /api/core/gray/rollback/: + post: + operationId: gray_rollback + summary: "GSE 2.0\u7070\u5EA6\u56DE\u6EDA" + description: '' + tags: + - gray + x-bk-apigateway-resource: + isPublic: true + allowApplyPermission: true + matchSubpath: false + backend: + name: default + method: post + path: /api/core/gray/rollback/ + matchSubpath: false + timeout: 0 + authConfig: + userVerifiedRequired: true + appVerifiedRequired: true + resourcePermissionRequired: true + /api/{bk_biz_id}/config_template/: + get: + operationId: config_template_list + summary: "\u83B7\u53D6\u914D\u7F6E\u6A21\u677F\u5217\u8868" + description: "\u914D\u7F6E\u6A21\u677F\uFF08ConfigTemplate\uFF09" + tags: + - config_template + x-bk-apigateway-resource: + isPublic: true + allowApplyPermission: true + matchSubpath: false + backend: + name: default + method: get + path: /api/{bk_biz_id}/config_template/ + matchSubpath: false + timeout: 0 + authConfig: + userVerifiedRequired: true + appVerifiedRequired: true + resourcePermissionRequired: true + /api/{bk_biz_id}/job/: + post: + operationId: create_job + summary: "\u521B\u5EFA\u4EFB\u52A1" + description: "\u4EFB\u52A1\uFF08Job\uFF09" + tags: + - job + x-bk-apigateway-resource: + isPublic: true + allowApplyPermission: true + matchSubpath: false + backend: + name: default + method: post + path: /api/{bk_biz_id}/job/ + matchSubpath: false + timeout: 0 + authConfig: + userVerifiedRequired: true + appVerifiedRequired: true + resourcePermissionRequired: true + /api/{bk_biz_id}/job/{id}/job_status/: + post: + operationId: job_status + summary: "\u4EFB\u52A1\u72B6\u6001\u67E5\u8BE2" + description: "\u4EFB\u52A1\uFF08Job\uFF09" + tags: + - job + x-bk-apigateway-resource: + isPublic: true + allowApplyPermission: true + matchSubpath: false + backend: + name: default + method: post + path: /api/{bk_biz_id}/job/{id}/job_status/ + matchSubpath: false + timeout: 0 + authConfig: + userVerifiedRequired: true + appVerifiedRequired: true + resourcePermissionRequired: true + /api/{bk_biz_id}/meta/access_overview/: + get: + operationId: access_overview + summary: "\u4E1A\u52A1\u63A5\u5165\u60C5\u51B5\u6982\u89C8" + description: '' + tags: + - meta + x-bk-apigateway-resource: + isPublic: true + allowApplyPermission: true + matchSubpath: false + backend: + name: default + method: get + path: /api/{bk_biz_id}/meta/access_overview/ + matchSubpath: false + timeout: 0 + authConfig: + userVerifiedRequired: true + appVerifiedRequired: true + resourcePermissionRequired: true + /api/{bk_biz_id}/process/flush_process/: + post: + operationId: flush_process + summary: "\u5237\u65B0\u4E1A\u52A1\u8FDB\u7A0B\u7F13\u5B58" + description: '' + tags: + - process + x-bk-apigateway-resource: + isPublic: true + allowApplyPermission: true + matchSubpath: false + backend: + name: default + method: post + path: /api/{bk_biz_id}/process/flush_process/ + matchSubpath: false + timeout: 0 + authConfig: + userVerifiedRequired: true + appVerifiedRequired: true + resourcePermissionRequired: true + /api/{bk_biz_id}/process/process_status/: + post: + operationId: process_status + summary: "\u8FDB\u7A0B\u72B6\u6001\u5217\u8868" + description: '' + tags: + - process + x-bk-apigateway-resource: + isPublic: true + allowApplyPermission: true + matchSubpath: false + backend: + name: default + method: post + path: /api/{bk_biz_id}/process/process_status/ + matchSubpath: false + timeout: 0 + authConfig: + userVerifiedRequired: true + appVerifiedRequired: true + resourcePermissionRequired: true + /api/{bk_biz_id}/process/sync_process_status/: + post: + operationId: sync_process_status + summary: "\u540C\u6B65\u8FDB\u7A0B\u72B6\u6001" + description: '' + tags: + - process + x-bk-apigateway-resource: + isPublic: true + allowApplyPermission: true + matchSubpath: false + backend: + name: default + method: post + path: /api/{bk_biz_id}/process/sync_process_status/ + matchSubpath: false + timeout: 0 + authConfig: + userVerifiedRequired: true + appVerifiedRequired: true + resourcePermissionRequired: true