diff --git a/bk-plugin-framework/bk_plugin_framework/kit/__init__.py b/bk-plugin-framework/bk_plugin_framework/kit/__init__.py index fe6e59a..644678a 100644 --- a/bk-plugin-framework/bk_plugin_framework/kit/__init__.py +++ b/bk-plugin-framework/bk_plugin_framework/kit/__init__.py @@ -14,9 +14,9 @@ except ImportError: from pydantic import Field # noqa -from bk_plugin_framework.kit.plugin import Context # noqa from bk_plugin_framework.kit.plugin import ( # noqa Callback, + Context, ContextRequire, FormModel, InputsModel, diff --git a/bk-plugin-framework/bk_plugin_framework/kit/plugin.py b/bk-plugin-framework/bk_plugin_framework/kit/plugin.py index 5d24958..ca776b2 100644 --- a/bk-plugin-framework/bk_plugin_framework/kit/plugin.py +++ b/bk-plugin-framework/bk_plugin_framework/kit/plugin.py @@ -20,7 +20,10 @@ from bk_plugin_framework.constants import State from bk_plugin_framework.hub import VersionHub -from bk_plugin_framework.runtime.callback.api import CallbackPreparation, prepare_callback +from bk_plugin_framework.runtime.callback.api import ( + CallbackPreparation, + prepare_callback, +) VALID_VERSION_PATTERN = re.compile(r"^[0-9]+\.[0-9]+\.[0-9][a-z0-9]*$") diff --git a/bk-plugin-framework/bk_plugin_framework/runtime/callback/celery/tasks.py b/bk-plugin-framework/bk_plugin_framework/runtime/callback/celery/tasks.py index 52e21d4..b7510f6 100644 --- a/bk-plugin-framework/bk_plugin_framework/runtime/callback/celery/tasks.py +++ b/bk-plugin-framework/bk_plugin_framework/runtime/callback/celery/tasks.py @@ -19,7 +19,11 @@ from bk_plugin_framework.envs import settings from bk_plugin_framework.hub import VersionHub from bk_plugin_framework.kit import State -from bk_plugin_framework.metrics import BK_PLUGIN_CALLBACK_EXCEPTION_COUNT, BK_PLUGIN_CALLBACK_TIME, HOSTNAME +from bk_plugin_framework.metrics import ( + BK_PLUGIN_CALLBACK_EXCEPTION_COUNT, + BK_PLUGIN_CALLBACK_TIME, + HOSTNAME, +) from bk_plugin_framework.runtime.executor import BKPluginExecutor from bk_plugin_framework.runtime.schedule.models import Schedule from bk_plugin_framework.runtime.schedule.utils import get_schedule_lock diff --git a/bk-plugin-framework/bk_plugin_framework/runtime/executor.py b/bk-plugin-framework/bk_plugin_framework/runtime/executor.py index 1e07e0d..6f6938a 100644 --- a/bk-plugin-framework/bk_plugin_framework/runtime/executor.py +++ b/bk-plugin-framework/bk_plugin_framework/runtime/executor.py @@ -22,7 +22,14 @@ from django.utils.timezone import now -from bk_plugin_framework.kit import Callback, Context, ContextRequire, InputsModel, Plugin, State +from bk_plugin_framework.kit import ( + Callback, + Context, + ContextRequire, + InputsModel, + Plugin, + State, +) from bk_plugin_framework.kit.plugin import PluginCallbackModel from bk_plugin_framework.metrics import ( BK_PLUGIN_EXECUTE_EXCEPTION_COUNT, diff --git a/bk-plugin-framework/bk_plugin_framework/serializers.py b/bk-plugin-framework/bk_plugin_framework/serializers.py new file mode 100644 index 0000000..817f64b --- /dev/null +++ b/bk-plugin-framework/bk_plugin_framework/serializers.py @@ -0,0 +1,19 @@ +from drf_spectacular.utils import extend_schema_serializer +from rest_framework import serializers + + +def standard_response_enveloper(serializer_class, many: bool = False): + """统一响应包装器""" + component_name = "Enveloped{}{}".format( + serializer_class.__name__.replace("Serializer", ""), + "List" if many else "", + ) + + @extend_schema_serializer(many=False, component_name=component_name) + class EnvelopeSerializer(serializers.Serializer): + code = serializers.IntegerField(help_text="状态码,0表示成功") + data = serializer_class(many=many) + message = serializers.CharField(help_text="响应消息") + result = serializers.BooleanField(help_text="操作结果") + + return EnvelopeSerializer diff --git a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/callback.py b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/callback.py index 0a10b6b..6de6af1 100644 --- a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/callback.py +++ b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/callback.py @@ -14,26 +14,50 @@ import traceback from apigw_manager.apigw.decorators import apigw_require +from apigw_manager.drf.utils import gen_apigateway_resource_config from blueapps.account.decorators import login_exempt from django.utils.decorators import method_decorator -from drf_yasg.utils import swagger_auto_schema +from drf_spectacular.utils import extend_schema +from rest_framework import serializers from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.views import APIView from bk_plugin_framework.runtime.callback.api import callback, parse_callback_token +from bk_plugin_framework.serializers import standard_response_enveloper logger = logging.getLogger("bk_plugin") +class PluginCallbackParamsSerializer(serializers.Serializer): + token = serializers.CharField(help_text="插件回调token", required=True) + + +class PluginCallbackResponseSerializer(serializers.Serializer): + result = serializers.BooleanField(help_text="回调结果,True表示成功,False表示失败") + message = serializers.CharField(help_text="回调结果信息", required=False) + + @method_decorator(login_exempt, name="dispatch") @method_decorator(apigw_require, name="dispatch") class PluginCallback(APIView): authentication_classes = [] # csrf exempt - @swagger_auto_schema( - method="POST", - operation_summary="plugin callback", + @extend_schema( + exclude=True, + summary="插件回调", + operation_id="callback", + request=PluginCallbackParamsSerializer, + responses={200: standard_response_enveloper(PluginCallbackResponseSerializer)}, + extensions=gen_apigateway_resource_config( + is_public=True, + allow_apply_permission=True, + user_verified_required=False, + app_verified_required=True, + resource_permission_required=True, + description_en="plugin callback", + match_subpath=False, + ), ) @action(methods=["POST"], detail=True) def post(self, request, token): diff --git a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/detail.py b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/detail.py index 7adc250..09371e7 100644 --- a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/detail.py +++ b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/detail.py @@ -11,16 +11,20 @@ import logging +from apigw_manager.drf.utils import gen_apigateway_resource_config from blueapps.account.decorators import login_exempt from django.utils.decorators import method_decorator -from drf_yasg.utils import swagger_auto_schema +from drf_spectacular.utils import extend_schema from rest_framework import permissions, serializers, status from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.views import APIView from bk_plugin_framework.hub import VersionHub -from bk_plugin_framework.services.bpf_service.api.serializers import StandardResponseSerializer +from bk_plugin_framework.serializers import standard_response_enveloper +from bk_plugin_framework.services.bpf_service.api.serializers import ( + StandardResponseSerializer, +) logger = logging.getLogger("root") @@ -64,10 +68,20 @@ class DetailFormsSerializer(serializers.Serializer): class Detail(APIView): permission_classes = [permissions.AllowAny] - @swagger_auto_schema( - method="GET", - operation_summary="Get plugin detail for specific version", - responses={200: DetailResponseSerializer}, + @extend_schema( + exclude=True, + summary="获取指定版本的插件详情", + operation_id="plugin_detail", + responses={200: standard_response_enveloper(DetailResponseSerializer)}, + extensions=gen_apigateway_resource_config( + is_public=True, + allow_apply_permission=True, + user_verified_required=True, + app_verified_required=True, + resource_permission_required=True, + description_en="Get plugin detail for specific version", + match_subpath=False, + ), ) @action(methods=["GET"], detail=True) def get(self, request, version): diff --git a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/invoke.py b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/invoke.py index edc6221..5135d21 100644 --- a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/invoke.py +++ b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/invoke.py @@ -12,9 +12,10 @@ import logging from apigw_manager.apigw.decorators import apigw_require +from apigw_manager.drf.utils import gen_apigateway_resource_config from blueapps.account.decorators import login_exempt from django.utils.decorators import method_decorator -from drf_yasg.utils import swagger_auto_schema +from drf_spectacular.utils import extend_schema from rest_framework import serializers, status from rest_framework.decorators import action from rest_framework.exceptions import ValidationError @@ -23,8 +24,13 @@ from bk_plugin_framework.hub import VersionHub from bk_plugin_framework.runtime.executor import BKPluginExecutor -from bk_plugin_framework.services.bpf_service.api.permissions import ScopeAllowPermission -from bk_plugin_framework.services.bpf_service.api.serializers import StandardResponseSerializer +from bk_plugin_framework.serializers import standard_response_enveloper +from bk_plugin_framework.services.bpf_service.api.permissions import ( + ScopeAllowPermission, +) +from bk_plugin_framework.services.bpf_service.api.serializers import ( + StandardResponseSerializer, +) logger = logging.getLogger("bk_plugin") @@ -52,11 +58,21 @@ class Invoke(APIView): authentication_classes = [] # csrf exempt permission_classes = [ScopeAllowPermission] - @swagger_auto_schema( - method="POST", - operation_summary="Invoke specific version plugin", - request_body=InvokeParamsSerializer, - responses={200: InvokeResponseSerializer}, + @extend_schema( + exclude=True, + summary="调用指定版本插件", + operation_id="invoke", + request=InvokeParamsSerializer, + responses={200: standard_response_enveloper(InvokeResponseSerializer)}, + extensions=gen_apigateway_resource_config( + is_public=True, + allow_apply_permission=True, + user_verified_required=False, + app_verified_required=True, + resource_permission_required=True, + description_en="Invoke specific version plugin", + match_subpath=False, + ), ) @action(methods=["POST"], detail=True) def post(self, request, version): diff --git a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/logs.py b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/logs.py index 289d7fa..adaeb82 100644 --- a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/logs.py +++ b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/logs.py @@ -11,16 +11,20 @@ import logging +from apigw_manager.drf.utils import gen_apigateway_resource_config from blueapps.account.decorators import login_exempt from django.utils.decorators import method_decorator -from drf_yasg.utils import swagger_auto_schema +from drf_spectacular.utils import extend_schema from rest_framework import permissions, serializers from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.views import APIView from bk_plugin_framework.runtime.loghub.models import LogEntry -from bk_plugin_framework.services.bpf_service.api.serializers import StandardResponseSerializer +from bk_plugin_framework.serializers import standard_response_enveloper +from bk_plugin_framework.services.bpf_service.api.serializers import ( + StandardResponseSerializer, +) logger = logging.getLogger("root") @@ -40,10 +44,20 @@ class Logs(APIView): permission_classes = [permissions.AllowAny] - @swagger_auto_schema( - method="GET", - operation_summary="Get plugin execution log with trace_id", - responses={200: LogsResponseSerializer}, + @extend_schema( + exclude=True, + summary="获取插件执行日志", + operation_id="plugin_logs", + responses={200: standard_response_enveloper(LogsResponseSerializer)}, + extensions=gen_apigateway_resource_config( + is_public=True, + allow_apply_permission=True, + user_verified_required=True, + app_verified_required=True, + resource_permission_required=True, + description_en="Get plugin execution log with trace_id", + match_subpath=False, + ), ) @action(methods=["GET"], detail=True) def get(self, request, trace_id): diff --git a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/meta.py b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/meta.py index 4377d06..4b027dc 100644 --- a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/meta.py +++ b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/meta.py @@ -12,10 +12,11 @@ import logging from importlib import import_module +from apigw_manager.drf.utils import gen_apigateway_resource_config from blueapps.account.decorators import login_exempt from django.conf import settings from django.utils.decorators import method_decorator -from drf_yasg.utils import swagger_auto_schema +from drf_spectacular.utils import extend_schema from rest_framework import permissions, serializers from rest_framework.decorators import action from rest_framework.response import Response @@ -23,7 +24,10 @@ from bk_plugin_framework import __version__ as bpf_version from bk_plugin_framework.hub import VersionHub -from bk_plugin_framework.services.bpf_service.api.serializers import StandardResponseSerializer +from bk_plugin_framework.serializers import standard_response_enveloper +from bk_plugin_framework.services.bpf_service.api.serializers import ( + StandardResponseSerializer, +) logger = logging.getLogger("root") @@ -58,10 +62,20 @@ class Meta(APIView): permission_classes = [permissions.AllowAny] - @swagger_auto_schema( - method="GET", - operation_summary="Get plugin meta info", - responses={200: MetaResponseSerializer}, + @extend_schema( + exclude=True, + summary="获取插件元信息", + operation_id="plugin_meta_info", + responses={200: standard_response_enveloper(MetaResponseSerializer)}, + extensions=gen_apigateway_resource_config( + is_public=True, + allow_apply_permission=True, + user_verified_required=True, + app_verified_required=True, + resource_permission_required=True, + description_en="Get plugin meta info", + match_subpath=False, + ), ) @action(methods=["GET"], detail=True) def get(self, request): diff --git a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/plugin_api_dispatch.py b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/plugin_api_dispatch.py index 61697f2..3835850 100644 --- a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/plugin_api_dispatch.py +++ b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/plugin_api_dispatch.py @@ -15,19 +15,25 @@ from urllib.parse import urlsplit from apigw_manager.apigw.decorators import apigw_require +from apigw_manager.drf.utils import gen_apigateway_resource_config from blueapps.account.decorators import login_exempt from django.test import RequestFactory from django.urls import Resolver404, resolve from django.utils.decorators import method_decorator -from drf_yasg.utils import swagger_auto_schema +from drf_spectacular.utils import extend_schema from rest_framework import serializers, status from rest_framework.decorators import action from rest_framework.exceptions import ValidationError from rest_framework.response import Response from rest_framework.views import APIView -from bk_plugin_framework.services.bpf_service.api.permissions import ScopeAllowPermission -from bk_plugin_framework.services.bpf_service.api.serializers import StandardResponseSerializer +from bk_plugin_framework.serializers import standard_response_enveloper +from bk_plugin_framework.services.bpf_service.api.permissions import ( + ScopeAllowPermission, +) +from bk_plugin_framework.services.bpf_service.api.serializers import ( + StandardResponseSerializer, +) logger = logging.getLogger("bk_plugin") @@ -73,11 +79,21 @@ class PluginAPIDispatch(APIView): authentication_classes = [] # csrf exempt permission_classes = [ScopeAllowPermission] - @swagger_auto_schema( - method="POST", - operation_summary="Plugin API dispatch", - request_body=PluginAPIDispatchParamsSerializer, - responses={200: PluginAPIDispatchResponseSerializer}, + @extend_schema( + exclude=True, + summary="插件API分发", + operation_id="plugin_api_dispatch", + request=PluginAPIDispatchParamsSerializer, + responses={200: standard_response_enveloper(PluginAPIDispatchResponseSerializer)}, + extensions=gen_apigateway_resource_config( + is_public=True, + allow_apply_permission=True, + user_verified_required=False, + app_verified_required=True, + resource_permission_required=True, + description_en="Plugin API dispatch", + match_subpath=False, + ), ) @action(methods=["POST"], detail=True) def post(self, request): diff --git a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/schedule.py b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/schedule.py index 2025097..031be58 100644 --- a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/schedule.py +++ b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/schedule.py @@ -12,20 +12,28 @@ import json import logging +from apigw_manager.drf.utils import gen_apigateway_resource_config from blueapps.account.decorators import login_exempt from django.utils.decorators import method_decorator -from drf_yasg.utils import swagger_auto_schema +from drf_spectacular.utils import extend_schema from rest_framework import permissions, serializers from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.views import APIView from bk_plugin_framework.runtime.schedule.models import Schedule as ScheduleModel -from bk_plugin_framework.services.bpf_service.api.serializers import StandardResponseSerializer +from bk_plugin_framework.serializers import standard_response_enveloper +from bk_plugin_framework.services.bpf_service.api.serializers import ( + StandardResponseSerializer, +) logger = logging.getLogger("root") +class ScheduleParamsSerializer(serializers.Serializer): + trace_id = serializers.CharField(help_text="插件调用 trace id") + + class ScheduleResponseSerializer(StandardResponseSerializer): class ScheduleDataSerializer(serializers.Serializer): class Meta: @@ -47,10 +55,21 @@ class Schedule(APIView): permission_classes = [permissions.AllowAny] - @swagger_auto_schema( - method="GET", - operation_summary="Get plugin schedule detail with trace_id", - responses={200: ScheduleResponseSerializer}, + @extend_schema( + exclude=True, + summary="获取插件调度详情", + operation_id="plugin_schedule", + parameters=[ScheduleParamsSerializer], + responses={200: standard_response_enveloper(ScheduleResponseSerializer)}, + extensions=gen_apigateway_resource_config( + is_public=True, + allow_apply_permission=True, + user_verified_required=True, + app_verified_required=True, + resource_permission_required=True, + description_en="Get plugin schedule detail with trace_id", + match_subpath=False, + ), ) @action(methods=["GET"], detail=True) def get(self, request, trace_id): diff --git a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/management/commands/data/api-definition.yml b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/management/commands/data/api-definition.yml deleted file mode 100644 index 3bfdd5d..0000000 --- a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/management/commands/data/api-definition.yml +++ /dev/null @@ -1,35 +0,0 @@ -stage: - name: {{ settings.BK_PLUGIN_APIGW_STAGE_NAME }} - {% if settings.BK_PLUGIN_APIGW_BACKEND_SUB_PATH %} - vars: - api_sub_path: {{ settings.BK_PLUGIN_APIGW_BACKEND_SUB_PATH }} - {% else %} - vars: {} - {% endif %} - proxy_http: - timeout: {{ settings.BK_APIGW_DEFAULT_TIMEOUT }} - upstreams: - loadbalance: "roundrobin" - hosts: - - host: "{{ settings.BK_PLUGIN_APIGW_BACKEND_SCHEME }}://{{ settings.BK_PLUGIN_APIGW_BACKEND_NETLOC }}/" - weight: 100 - plugin_configs: - - type: bk-cors - yaml: |- - allow_origins: '{{ settings.BK_APIGW_CORS_ALLOW_ORIGINS }}' - allow_methods: '{{ settings.BK_APIGW_CORS_ALLOW_METHODS }}' - allow_headers: '{{ settings.BK_APIGW_CORS_ALLOW_HEADERS }}' - expose_headers: '' - max_age: 86400 - allow_credential: true - -{% if settings.BK_APIGW_GRANTED_APPS %} -grant_permissions: - {% for app_code in settings.BK_APIGW_GRANTED_APPS %} - - bk_app_code: {{ app_code }} - grant_dimension: "gateway" - {% endfor %} -{% endif %} - -release: - comment: "auto release by bk-plugin-runtime" diff --git a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/management/commands/data/api-resources.yml b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/management/commands/data/api-resources.yml deleted file mode 100644 index edf3422..0000000 --- a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/management/commands/data/api-resources.yml +++ /dev/null @@ -1,146 +0,0 @@ -swagger: '2.0' -basePath: / -info: - version: '0.1' - title: API Gateway Resources - description: '' -schemes: -- http -paths: - /invoke/{version}/: - post: - operationId: invoke - description: invoke plugin - tags: [] - responses: - default: - description: '' - x-bk-apigateway-resource: - isPublic: true - allowApplyPermission: true - matchSubpath: false - backend: - type: HTTP - method: post - {% if settings.BK_PLUGIN_APIGW_BACKEND_SUB_PATH %} - path: /{env.api_sub_path}bk_plugin/invoke/{version} - {% else %} - path: /bk_plugin/invoke/{version} - {% endif %} - matchSubpath: false - timeout: 0 - upstreams: {} - transformHeaders: {} - authConfig: - userVerifiedRequired: false - disabledStages: [] - /plugin_api_dispatch: - post: - operationId: plugin_api_dispatch - description: '' - tags: [] - responses: - default: - description: '' - x-bk-apigateway-resource: - isPublic: true - allowApplyPermission: true - matchSubpath: false - backend: - type: HTTP - method: post - {% if settings.BK_PLUGIN_APIGW_BACKEND_SUB_PATH %} - path: /{env.api_sub_path}bk_plugin/plugin_api_dispatch/ - {% else %} - path: /bk_plugin/plugin_api_dispatch/ - {% endif %} - matchSubpath: false - timeout: 0 - upstreams: {} - transformHeaders: {} - authConfig: - userVerifiedRequired: false - disabledStages: [] - /bk_plugin/plugin_api/: - x-bk-apigateway-method-any: - operationId: plugin_api - description: '' - tags: [] - responses: - default: - description: '' - x-bk-apigateway-resource: - isPublic: true - allowApplyPermission: true - matchSubpath: true - backend: - type: HTTP - method: any - {% if settings.BK_PLUGIN_APIGW_BACKEND_SUB_PATH %} - path: /{env.api_sub_path}bk_plugin/plugin_api/ - {% else %} - path: /bk_plugin/plugin_api/ - {% endif %} - matchSubpath: true - timeout: 0 - upstreams: {} - transformHeaders: {} - authConfig: - userVerifiedRequired: true - appVerifiedRequired: false - disabledStages: [] - /bk_plugin/openapi/: - x-bk-apigateway-method-any: - operationId: plugin_openapi - description: '' - tags: [] - responses: - default: - description: '' - x-bk-apigateway-resource: - isPublic: true - allowApplyPermission: true - matchSubpath: true - backend: - type: HTTP - method: any - {% if settings.BK_PLUGIN_APIGW_BACKEND_SUB_PATH %} - path: /{env.api_sub_path}bk_plugin/openapi/ - {% else %} - path: /bk_plugin/openapi/ - {% endif %} - matchSubpath: true - timeout: 0 - upstreams: {} - transformHeaders: {} - authConfig: - userVerifiedRequired: false - appVerifiedRequired: true - disabledStages: [] - /callback/{token}/: - post: - operationId: callback - description: callback plugin - tags: [] - responses: - default: - description: '' - x-bk-apigateway-resource: - isPublic: true - allowApplyPermission: true - matchSubpath: false - backend: - type: HTTP - method: post - {% if settings.BK_PLUGIN_APIGW_BACKEND_SUB_PATH %} - path: /{env.api_sub_path}bk_plugin/callback/{token}/ - {% else %} - path: /bk_plugin/callback/{token}/ - {% endif %} - matchSubpath: false - timeout: 0 - upstreams: {} - transformHeaders: {} - authConfig: - userVerifiedRequired: false - disabledStages: [] diff --git a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/management/commands/data/api-strategy-cors.yml b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/management/commands/data/api-strategy-cors.yml deleted file mode 100644 index 01ad279..0000000 --- a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/management/commands/data/api-strategy-cors.yml +++ /dev/null @@ -1,13 +0,0 @@ -strategies: - - type: "cors" - comment: "" - name: "跨域资源共享(CORS)" - - config: - allowed_origins: {{ settings.BK_APIGW_CORS_ALLOW_ORIGINS }} - allowed_methods: {{ settings.BK_APIGW_CORS_ALLOW_METHODS }} - allowed_headers: {{ settings.BK_APIGW_CORS_ALLOW_HEADERS }} - exposed_headers: [] - max_age: 86400 - allow_credentials: true - option_passthrough: false \ No newline at end of file diff --git a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/management/commands/support-files/resources.yaml b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/management/commands/support-files/resources.yaml new file mode 100644 index 0000000..d4d5fc4 --- /dev/null +++ b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/management/commands/support-files/resources.yaml @@ -0,0 +1,437 @@ +openapi: 3.0.3 +info: + title: 'BK Plugin API' + version: '1.0.0' + description: '蓝鲸插件服务 API' + +paths: + + /callback/{token}/: + post: + operationId: callback + summary: 插件回调 + description: 插件异步执行完成后的回调接口 + tags: + - callback + parameters: + - in: path + name: token + schema: + type: string + required: true + description: 回调令牌 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PluginCallbackParams' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PluginCallbackParams' + multipart/form-data: + schema: + $ref: '#/components/schemas/PluginCallbackParams' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedPluginCallbackResponse' + description: '回调成功' + x-bk-apigateway-resource: + isPublic: true + matchSubpath: false + backend: + type: HTTP + name: default + method: post + {% if settings.BK_PLUGIN_APIGW_BACKEND_SUB_PATH %} + path: /{env.api_sub_path}bk_plugin/callback/{token}/ + {% else %} + path: /bk_plugin/callback/{token}/ + {% endif %} + matchSubpath: false + timeout: 0 + upstreams: {} + transformHeaders: {} + pluginConfigs: [] + allowApplyPermission: true + authConfig: + userVerifiedRequired: false + appVerifiedRequired: true + resourcePermissionRequired: true + disabledStages: [] + descriptionEn: plugin callback + + /invoke/{version}: + post: + operationId: invoke + summary: 调用指定版本插件 + description: 调用指定版本的插件执行任务 + tags: + - invoke + parameters: + - in: path + name: version + schema: + type: string + required: true + description: 插件版本号 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/InvokeParams' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/InvokeParams' + multipart/form-data: + schema: + $ref: '#/components/schemas/InvokeParams' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedInvokeResponse' + description: '调用成功' + x-bk-apigateway-resource: + isPublic: true + matchSubpath: false + backend: + type: HTTP + name: default + method: post + {% if settings.BK_PLUGIN_APIGW_BACKEND_SUB_PATH %} + path: /{env.api_sub_path}bk_plugin/invoke/{version} + {% else %} + path: /bk_plugin/invoke/{version} + {% endif %} + matchSubpath: false + timeout: 0 + upstreams: {} + transformHeaders: {} + pluginConfigs: [] + allowApplyPermission: true + authConfig: + userVerifiedRequired: false + appVerifiedRequired: true + resourcePermissionRequired: true + disabledStages: [] + descriptionEn: Invoke specific version plugin + /bk_plugin/plugin_api/: + x-bk-apigateway-method-any: + operationId: plugin_api + summary: 插件自定义API + description: 插件自定义 API 入口,支持子路径匹配 + tags: + - plugin_api + responses: + default: + description: '插件自定义响应' + x-bk-apigateway-resource: + isPublic: true + allowApplyPermission: true + matchSubpath: true + backend: + type: HTTP + name: default + method: any + {% if settings.BK_PLUGIN_APIGW_BACKEND_SUB_PATH %} + path: /{env.api_sub_path}bk_plugin/plugin_api/ + {% else %} + path: /bk_plugin/plugin_api/ + {% endif %} + matchSubpath: true + timeout: 0 + upstreams: {} + transformHeaders: {} + authConfig: + userVerifiedRequired: true + appVerifiedRequired: false + disabledStages: [] + descriptionEn: Plugin custom API entry + + /bk_plugin/openapi/: + x-bk-apigateway-method-any: + operationId: plugin_openapi + summary: 插件开放API + description: 插件开放 API 入口,支持子路径匹配 + tags: + - openapi + responses: + default: + description: '插件开放API响应' + x-bk-apigateway-resource: + isPublic: true + allowApplyPermission: true + matchSubpath: true + backend: + type: HTTP + name: default + method: any + {% if settings.BK_PLUGIN_APIGW_BACKEND_SUB_PATH %} + path: /{env.api_sub_path}bk_plugin/openapi/ + {% else %} + path: /bk_plugin/openapi/ + {% endif %} + matchSubpath: true + timeout: 0 + upstreams: {} + transformHeaders: {} + authConfig: + userVerifiedRequired: false + appVerifiedRequired: true + disabledStages: [] + descriptionEn: Plugin open API entry + + /plugin_api_dispatch/: + post: + operationId: plugin_api_dispatch + summary: 插件API分发 + description: 分发请求到插件的自定义 API + tags: + - plugin_api_dispatch + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PluginAPIDispatchParams' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PluginAPIDispatchParams' + multipart/form-data: + schema: + $ref: '#/components/schemas/PluginAPIDispatchParams' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedPluginAPIDispatchResponse' + description: '分发成功' + x-bk-apigateway-resource: + isPublic: true + matchSubpath: false + backend: + type: HTTP + name: default + method: post + {% if settings.BK_PLUGIN_APIGW_BACKEND_SUB_PATH %} + path: /{env.api_sub_path}bk_plugin/plugin_api_dispatch/ + {% else %} + path: /bk_plugin/plugin_api_dispatch/ + {% endif %} + matchSubpath: false + timeout: 0 + upstreams: {} + transformHeaders: {} + pluginConfigs: [] + allowApplyPermission: true + authConfig: + userVerifiedRequired: false + appVerifiedRequired: true + resourcePermissionRequired: true + disabledStages: [] + descriptionEn: Plugin API dispatch + +components: + schemas: + # ==================== 请求参数 ==================== + InvokeParams: + type: object + description: 插件调用请求参数 + properties: + inputs: + type: object + additionalProperties: {} + description: 插件调用参数 + context: + type: object + additionalProperties: {} + description: 插件执行上下文 + required: + - context + - inputs + + PluginCallbackParams: + type: object + description: 插件回调请求参数 + properties: + token: + type: string + description: 插件回调token + required: + - token + + PluginAPIDispatchParams: + type: object + description: 插件API分发请求参数 + properties: + url: + type: string + description: 数据接口 URL + method: + type: string + description: 调用方法 + username: + type: string + description: 用户名 + data: + type: object + additionalProperties: {} + default: {} + description: 接口数据 + dumped_data: + type: string + description: json dumps后的接口数据 + required: + - method + - url + - username + + # ==================== 响应数据 ==================== + InvokeData: + type: object + description: 插件调用返回数据 + properties: + outputs: + type: object + additionalProperties: {} + description: 插件输出数据 + state: + type: integer + description: '插件执行状态(2: POLL 3:CALLBACK 4:SUCCESS 5:FAIL)' + err: + type: string + description: 错误信息 + required: + - err + - outputs + - state + + InvokeResponse: + type: object + description: 插件调用响应 + properties: + result: + type: boolean + description: 请求是否成功 + message: + type: string + description: 请求额外信息,result 为 false 时读取 + trace_id: + type: string + description: 调用跟踪 ID + data: + allOf: + - $ref: '#/components/schemas/InvokeData' + description: 接口数据 + required: + - data + - message + - result + - trace_id + + PluginCallbackResponse: + type: object + description: 插件回调响应 + properties: + result: + type: boolean + description: 回调结果,True表示成功,False表示失败 + message: + type: string + description: 回调结果信息 + required: + - result + + PluginAPIDispatchResponse: + type: object + description: 插件API分发响应 + properties: + result: + type: boolean + description: 请求是否成功 + message: + type: string + description: 请求额外信息,result 为 false 时读取 + trace_id: + type: string + description: 调用跟踪 ID + data: + type: object + additionalProperties: {} + description: DATA API 返回的数据 + required: + - data + - message + - result + - trace_id + + # ==================== 统一响应封装 ==================== + EnvelopedInvokeResponse: + type: object + description: 插件调用统一响应封装 + properties: + code: + type: integer + description: 状态码,0表示成功 + data: + $ref: '#/components/schemas/InvokeResponse' + message: + type: string + description: 响应消息 + result: + type: boolean + description: 操作结果 + required: + - code + - data + - message + - result + + EnvelopedPluginCallbackResponse: + type: object + description: 插件回调统一响应封装 + properties: + code: + type: integer + description: 状态码,0表示成功 + data: + $ref: '#/components/schemas/PluginCallbackResponse' + message: + type: string + description: 响应消息 + result: + type: boolean + description: 操作结果 + required: + - code + - data + - message + - result + + EnvelopedPluginAPIDispatchResponse: + type: object + description: 插件API分发统一响应封装 + properties: + code: + type: integer + description: 状态码,0表示成功 + data: + $ref: '#/components/schemas/PluginAPIDispatchResponse' + message: + type: string + description: 响应消息 + result: + type: boolean + description: 操作结果 + required: + - code + - data + - message + - result \ No newline at end of file diff --git a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/management/commands/sync_apigateway_if_changed.py b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/management/commands/sync_apigateway_if_changed.py new file mode 100644 index 0000000..60da989 --- /dev/null +++ b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/management/commands/sync_apigateway_if_changed.py @@ -0,0 +1,237 @@ +""" +Tencent is pleased to support the open source community by making 蓝鲸智云 - PaaS平台 (BlueKing - PaaS System) available. +Copyright (C) 2022 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. +""" + +import hashlib +import os +import shutil +import time + +import yaml +from django.conf import settings +from django.core.management import call_command +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + help = "仅在接口定义发生变化时同步到API网关" + + def add_arguments(self, parser): + parser.add_argument( + "--force", + action="store_true", + default=False, + help="强制同步,忽略哈希值对比", + ) + + def handle(self, *args, **options): + force_sync = options.get("force", False) + + # 用于记录各步骤耗时(毫秒) + step_timings = {} + + # 1. 生成 definition.yaml 文件 + step_start = time.time() + self.stdout.write("[Sync] generate definition.yaml") + try: + call_command("generate_definition_yaml") + except Exception as e: + self.stderr.write( + self.style.ERROR( + f"run generate_definition_yaml fail: {e}, " + "please run this command on your development env to find out the reason" + ) + ) + raise SystemExit(1) + + # 输出生成的文件路径 + resources_yaml_path = os.path.join(settings.BASE_DIR, "resources.yaml") + definition_yaml_path = os.path.join(settings.BASE_DIR, "definition.yaml") + self.stdout.write(f"[Sync] Generated definition.yaml path: {definition_yaml_path}") + + step_timings["1. 生成 definition.yaml 文件"] = (time.time() - step_start) * 1000 + + # 1.1 复制 support-files/resources.yaml 到项目根目录 + # 注意:不再使用 generate_resources_yaml 命令扫描 URL 模块 + # plugin_api/ 和 openapi/ 等接口已在 support-files/resources.yaml 中通过子路径匹配方式定义 + step_start = time.time() + self._copy_support_files_resources() + self.stdout.write(f"[Sync] resources.yaml path: {resources_yaml_path}") + step_timings["1.1 复制 support-files/resources.yaml"] = (time.time() - step_start) * 1000 + + # 2. 计算当前哈希值(仅计算 resources.yaml) + step_start = time.time() + current_hash = self._calculate_resources_hash() + self.stdout.write(f"[Sync] Current resources.yaml hash: {current_hash[:16]}...") + step_timings["2. 计算当前哈希值"] = (time.time() - step_start) * 1000 + + # 3. 获取上次同步的哈希值 + step_start = time.time() + last_hash = self._get_last_sync_hash() + if last_hash: + self.stdout.write(f"[Sync] Last sync hash: {last_hash[:16]}...") + else: + self.stdout.write("[Sync] No previous sync record found") + step_timings["3. 获取上次同步的哈希值"] = (time.time() - step_start) * 1000 + + # 4. 对比决定是否同步 + step_start = time.time() + need_sync = force_sync or current_hash != last_hash + if not need_sync: + self.stdout.write(self.style.SUCCESS("[Sync] API definition unchanged, skip sync to apigateway")) + # 仍然获取公钥,确保公钥是最新的 + self._fetch_public_key() + step_timings["4. 对比决定是否同步"] = (time.time() - step_start) * 1000 + # 打印耗时统计 + self._print_timing_stats(step_timings) + return + step_timings["4. 对比决定是否同步"] = (time.time() - step_start) * 1000 + + if force_sync: + self.stdout.write(self.style.WARNING("[Sync] Force sync enabled")) + else: + self.stdout.write(self.style.WARNING("[Sync] API definition changed, start syncing...")) + + # 5. 执行同步 + step_start = time.time() + self.stdout.write("[Sync] sync to apigateway") + try: + call_command("sync_drf_apigateway") + except Exception as e: + self.stderr.write(self.style.ERROR(f"run sync_drf_apigateway fail: {e}")) + # 同步失败时更新状态 + self._save_sync_hash(current_hash, success=False) + raise SystemExit(1) + step_timings["5. 执行同步"] = (time.time() - step_start) * 1000 + + # 6. 获取公钥 + step_start = time.time() + self._fetch_public_key() + step_timings["6. 获取公钥"] = (time.time() - step_start) * 1000 + + # 7. 更新哈希值 + step_start = time.time() + self._save_sync_hash(current_hash, success=True) + self.stdout.write(self.style.SUCCESS("[Sync] API gateway sync completed successfully")) + step_timings["7. 更新哈希值"] = (time.time() - step_start) * 1000 + + # 打印耗时统计 + self._print_timing_stats(step_timings) + + def _print_timing_stats(self, step_timings): + """打印各步骤耗时统计""" + self.stdout.write("\n" + "=" * 50) + self.stdout.write(self.style.SUCCESS("[Sync] 各步骤耗时统计(毫秒):")) + self.stdout.write("=" * 50) + total_time = 0 + for step_name, duration in step_timings.items(): + self.stdout.write(f" {step_name}: {duration:.2f} ms") + total_time += duration + self.stdout.write("-" * 50) + self.stdout.write(f" 总耗时: {total_time:.2f} ms") + self.stdout.write("=" * 50 + "\n") + + def _copy_support_files_resources(self): + """ + 将 support-files/resources.yaml 复制到项目根目录作为 resources.yaml + + 说明: + - 不再使用 generate_resources_yaml 命令自动扫描 URL 模块 + - plugin_api/ 和 openapi/ 等接口已在 support-files/resources.yaml 中 + 通过子路径匹配(matchSubpath: true)的方式统一定义 + - 这样可以避免扫描 bk_plugin.apis.urls 和 bk_plugin.openapi.urls 模块 + """ + support_files_dir = os.path.join(os.path.dirname(__file__), "support-files") + support_filepath = os.path.join(support_files_dir, "resources.yaml") + target_filepath = os.path.join(settings.BASE_DIR, "resources.yaml") + + # 检查 support-files/resources.yaml 是否存在 + if not os.path.exists(support_filepath): + self.stderr.write(self.style.ERROR(f"[Sync] support-files/resources.yaml not found: {support_filepath}")) + raise SystemExit(1) + + try: + # 直接复制文件 + shutil.copy2(support_filepath, target_filepath) + self.stdout.write(self.style.SUCCESS(f"[Sync] Copied support-files/resources.yaml to {target_filepath}")) + except Exception as e: + self.stderr.write(self.style.ERROR(f"[Sync] Failed to copy support-files/resources.yaml: {e}")) + raise SystemExit(1) + + def _calculate_resources_hash(self): + """ + 计算 resources.yaml 的哈希值 + + 注意:为了避免 YAML 内容顺序变化导致的 hash 不一致问题, + 这里先将 YAML 解析为字典,然后用 sort_keys=True 重新序列化, + 确保相同内容的 YAML 文件总是产生相同的 hash 值。 + """ + filepath = os.path.join(settings.BASE_DIR, "resources.yaml") + if os.path.exists(filepath): + try: + with open(filepath, encoding="utf-8") as f: + data = yaml.safe_load(f) + + if data: + # 使用 sort_keys=True 确保输出顺序一致 + # 这样即使原始文件中 A,B,C 和 B,C,A 的顺序不同, + # 规范化后的内容也会相同,从而产生相同的 hash + normalized_content = yaml.dump( + data, default_flow_style=False, allow_unicode=True, sort_keys=True # 关键:排序所有 key + ) + return hashlib.sha256(normalized_content.encode()).hexdigest() + except Exception as e: + self.stdout.write( + self.style.WARNING( + f"[Sync] Failed to normalize resources.yaml for hash: {e}, fallback to raw content hash" + ) + ) + # 回退到原始方式 + with open(filepath, encoding="utf-8") as f: + content = f.read() + return hashlib.sha256(content.encode()).hexdigest() + return "" + + def _get_last_sync_hash(self): + """从数据库获取上次同步的哈希值""" + try: + from bk_plugin_framework.services.bpf_service.models import ( + APIGatewaySyncState, + ) + + state = APIGatewaySyncState.objects.filter(sync_success=True).first() + return state.api_hash if state else "" + except Exception as e: + self.stdout.write(self.style.WARNING(f"[Sync] Failed to get last sync hash: {e}")) + return "" + + def _save_sync_hash(self, hash_value, success=True): + """保存哈希值到数据库""" + try: + from bk_plugin_framework.services.bpf_service.models import ( + APIGatewaySyncState, + ) + + # 使用 update_or_create,保证只有一条记录 + APIGatewaySyncState.objects.update_or_create( + pk=1, defaults={"api_hash": hash_value, "sync_success": success} + ) + self.stdout.write(f"[Sync] Sync state saved (success={success})") + except Exception as e: + self.stdout.write(self.style.WARNING(f"[Sync] Failed to save sync hash: {e}")) + + def _fetch_public_key(self): + """获取 API 网关公钥""" + self.stdout.write("[Sync] fetch the public key") + try: + call_command("fetch_apigw_public_key") + except Exception as e: + self.stderr.write(self.style.ERROR(f"run fetch_apigw_public_key fail: {e}")) + raise SystemExit(1) diff --git a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/management/commands/sync_plugin_apigw.py b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/management/commands/sync_plugin_apigw.py deleted file mode 100644 index 46c246b..0000000 --- a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/management/commands/sync_plugin_apigw.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -Tencent is pleased to support the open source community by making 蓝鲸智云 - PaaS平台 (BlueKing - PaaS System) available. -Copyright (C) 2022 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. -""" - -import os - -from django.conf import settings -from django.core.management import call_command -from django.core.management.base import BaseCommand - - -class Command(BaseCommand): - def handle(self, *args, **kwargs): - definition_file_path = os.path.join(__file__.rsplit("/", 1)[0], "data/api-definition.yml") - resources_file_path = os.path.join(__file__.rsplit("/", 1)[0], "data/api-resources.yml") - print("[bk-plugin-framework]call sync_apigw_stage with definition: %s" % definition_file_path) - call_command("sync_apigw_stage", file=definition_file_path) - print("[bk-plugin-framework]call sync_apigw_resources with resources: %s" % resources_file_path) - call_command("sync_apigw_resources", file=resources_file_path) - print("[bk-plugin-framework]call sync_apigw_strategies with definition: %s" % definition_file_path) - call_command("sync_apigw_strategies", file=definition_file_path) - print("[bk-plugin-framework]call grant_apigw_permissions with definition: %s" % definition_file_path) - call_command("grant_apigw_permissions", file=definition_file_path) - - # if getattr(settings, "BK_APIGW_CORS_ALLOW_ORIGINS"): - # strategy_cors_file_path = os.path.join(__file__.rsplit("/", 1)[0], "data/api-strategy-cors.yml") - # print("[bk-plugin-framework]call sync_apigw_strategies cors with strategy: %s" % strategy_cors_file_path) - # call_command("sync_apigw_strategies", file=strategy_cors_file_path) - - print("[bk-plugin-framework]call create_version_and_release_apigw with definition: %s" % definition_file_path) - call_command( - "create_version_and_release_apigw", file=definition_file_path, stage=[settings.BK_PLUGIN_APIGW_STAGE_NAME] - ) diff --git a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/migrations/0001_initial.py b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/migrations/0001_initial.py new file mode 100644 index 0000000..1ac1731 --- /dev/null +++ b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/migrations/0001_initial.py @@ -0,0 +1,63 @@ +""" +Tencent is pleased to support the open source community by making 蓝鲸智云 - PaaS平台 (BlueKing - PaaS System) available. +Copyright (C) 2022 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 django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="APIGatewaySyncState", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "api_hash", + models.CharField( + default="", + max_length=64, + verbose_name="接口定义哈希值", + ), + ), + ( + "last_sync_at", + models.DateTimeField( + auto_now=True, + verbose_name="上次同步时间", + ), + ), + ( + "sync_success", + models.BooleanField( + default=False, + verbose_name="同步是否成功", + ), + ), + ], + options={ + "verbose_name": "API网关同步状态", + "verbose_name_plural": "API网关同步状态", + "db_table": "bpf_apigateway_sync_state", + }, + ), + ] diff --git a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/models.py b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/models.py new file mode 100644 index 0000000..330845c --- /dev/null +++ b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/models.py @@ -0,0 +1,35 @@ +""" +Tencent is pleased to support the open source community by making 蓝鲸智云 - PaaS平台 (BlueKing - PaaS System) available. +Copyright (C) 2022 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 django.db import models + + +class APIGatewaySyncState(models.Model): + """API网关同步状态记录 + + 用于存储上次成功同步时的接口定义哈希值, + 通过对比哈希值判断是否需要重新同步。 + """ + + # resources.yaml 的哈希值 + api_hash = models.CharField(max_length=64, default="", verbose_name="接口定义哈希值") + # 上次同步时间 + last_sync_at = models.DateTimeField(auto_now=True, verbose_name="上次同步时间") + # 同步是否成功 + sync_success = models.BooleanField(default=False, verbose_name="同步是否成功") + + class Meta: + db_table = "bpf_apigateway_sync_state" + verbose_name = "API网关同步状态" + verbose_name_plural = "API网关同步状态" + + def __str__(self): + return f"APIGatewaySyncState(hash={self.api_hash[:16]}..., success={self.sync_success})" diff --git a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/urls.py b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/urls.py index 07c8dec..f67660b 100644 --- a/bk-plugin-framework/bk_plugin_framework/services/bpf_service/urls.py +++ b/bk-plugin-framework/bk_plugin_framework/services/bpf_service/urls.py @@ -21,12 +21,14 @@ PLUGIN_OPENAPI_URLS_MODULE = "bk_plugin.openapi.urls" urlpatterns = [ + # 协议层接口(与平台方对接) + path(r"plugin_api_dispatch/", api.PluginAPIDispatch.as_view()), + path(r"callback//", api.PluginCallback.as_view()), + path(r"invoke/", api.Invoke.as_view()), + # 插件信息接口 path(r"meta/", api.Meta.as_view()), path(r"detail/", api.Detail.as_view()), - path(r"invoke/", api.Invoke.as_view()), path(r"schedule/", api.Schedule.as_view()), - path(r"plugin_api_dispatch/", api.PluginAPIDispatch.as_view()), - path(r"callback//", api.PluginCallback.as_view()), ] # add log api diff --git a/bk-plugin-framework/poetry.lock b/bk-plugin-framework/poetry.lock index 2681369..38aef85 100644 --- a/bk-plugin-framework/poetry.lock +++ b/bk-plugin-framework/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "amqp" @@ -188,26 +188,26 @@ tzdata = ["tzdata"] [[package]] name = "billiard" -version = "4.2.2" +version = "4.2.4" description = "Python multiprocessing fork with improvements and bugfixes" optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "billiard-4.2.2-py3-none-any.whl", hash = "sha256:4bc05dcf0d1cc6addef470723aac2a6232f3c7ed7475b0b580473a9145829457"}, - {file = "billiard-4.2.2.tar.gz", hash = "sha256:e815017a062b714958463e07ba15981d802dc53d41c5b69d28c5a7c238f8ecf3"}, + {file = "billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5"}, + {file = "billiard-4.2.4.tar.gz", hash = "sha256:55f542c371209e03cd5862299b74e52e4fbcba8250ba611ad94276b369b6a85f"}, ] [[package]] name = "bk-plugin-runtime" -version = "2.1.2" +version = "2.1.3rc4" description = "bk plugin python django runtime" optional = false python-versions = "<4.0.0,>=3.8.0" groups = ["main"] files = [ - {file = "bk_plugin_runtime-2.1.2-py3-none-any.whl", hash = "sha256:dd7b195baa8e3868902803dcca07638c6d855ca14f711c2627e2e7c3572df6b8"}, - {file = "bk_plugin_runtime-2.1.2.tar.gz", hash = "sha256:206297d4f45787703fcf77408408fbc2c80be0ad67f0bda3dd5d2399a0f1eb67"}, + {file = "bk_plugin_runtime-2.1.3rc4-py3-none-any.whl", hash = "sha256:7eb012c168d130e1a65756b31ecc01ed3e0b7717d88524bdfdcb8cbc3a8b9892"}, + {file = "bk_plugin_runtime-2.1.3rc4.tar.gz", hash = "sha256:9154012561dc7715921773413e1216e218257e7fa011c982910848e8024a0ff8"}, ] [package.dependencies] @@ -357,6 +357,7 @@ description = "Distributed Task Queue." optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.13\"" files = [ {file = "celery-5.5.3-py3-none-any.whl", hash = "sha256:0b5761a07057acee94694464ca482416b959568904c9dfa41ce8413a7d65d525"}, {file = "celery-5.5.3.tar.gz", hash = "sha256:6c972ae7968c2b5281227f01c3a3f984037d21c5129d07bf3550cc2afc6b10a5"}, @@ -408,6 +409,66 @@ yaml = ["kombu[yaml]"] zookeeper = ["kazoo (>=1.3.1)"] zstd = ["zstandard (==0.23.0)"] +[[package]] +name = "celery" +version = "5.6.2" +description = "Distributed Task Queue." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.13\"" +files = [ + {file = "celery-5.6.2-py3-none-any.whl", hash = "sha256:3ffafacbe056951b629c7abcf9064c4a2366de0bdfc9fdba421b97ebb68619a5"}, + {file = "celery-5.6.2.tar.gz", hash = "sha256:4a8921c3fcf2ad76317d3b29020772103581ed2454c4c042cc55dcc43585009b"}, +] + +[package.dependencies] +billiard = ">=4.2.1,<5.0" +click = ">=8.1.2,<9.0" +click-didyoumean = ">=0.3.0" +click-plugins = ">=1.1.1" +click-repl = ">=0.2.0" +exceptiongroup = {version = ">=1.3.0", markers = "python_version < \"3.11\""} +kombu = ">=5.6.0" +python-dateutil = ">=2.8.2" +tzlocal = "*" +vine = ">=5.1.0,<6.0" + +[package.extras] +arangodb = ["pyArango (>=2.0.2)"] +auth = ["cryptography (==46.0.3)"] +azureblockblob = ["azure-identity (>=1.19.0)", "azure-storage-blob (>=12.15.0)"] +brotli = ["brotli (>=1.0.0) ; platform_python_implementation == \"CPython\"", "brotlipy (>=0.7.0) ; platform_python_implementation == \"PyPy\""] +cassandra = ["cassandra-driver (>=3.25.0,<4)"] +consul = ["python-consul2 (==0.1.5)"] +cosmosdbsql = ["pydocumentdb (==2.3.5)"] +couchbase = ["couchbase (>=3.0.0) ; platform_python_implementation != \"PyPy\" and (platform_system != \"Windows\" or python_version < \"3.10\")"] +couchdb = ["pycouchdb (==1.16.0)"] +django = ["Django (>=2.2.28)"] +dynamodb = ["boto3 (>=1.26.143)"] +elasticsearch = ["elastic-transport (<=9.1.0)", "elasticsearch (<=9.1.2)"] +eventlet = ["eventlet (>=0.32.0) ; python_version < \"3.10\""] +gcs = ["google-cloud-firestore (==2.22.0)", "google-cloud-storage (>=2.10.0)", "grpcio (==1.75.1)"] +gevent = ["gevent (>=1.5.0)"] +librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] +memcache = ["pylibmc (==1.6.3) ; platform_system != \"Windows\""] +mongodb = ["kombu[mongodb]"] +msgpack = ["kombu[msgpack]"] +pydantic = ["pydantic (>=2.12.0a1) ; python_version >= \"3.14\"", "pydantic (>=2.4) ; python_version < \"3.14\""] +pymemcache = ["python-memcached (>=1.61)"] +pyro = ["pyro4 (==4.82) ; python_version < \"3.11\""] +pytest = ["pytest-celery[all] (>=1.2.0,<1.3.0)"] +redis = ["kombu[redis]"] +s3 = ["boto3 (>=1.26.143)"] +slmq = ["softlayer_messaging (>=1.0.3)"] +solar = ["ephem (==4.2) ; platform_python_implementation != \"PyPy\""] +sqlalchemy = ["kombu[sqlalchemy]"] +sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.5.0)", "pycurl (>=7.43.0.5,<7.45.4) ; sys_platform != \"win32\" and platform_python_implementation == \"CPython\" and python_version < \"3.9\"", "pycurl (>=7.45.4) ; sys_platform != \"win32\" and platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "urllib3 (>=1.26.16)"] +tblib = ["tblib (==3.2.2)"] +yaml = ["kombu[yaml]"] +zookeeper = ["kazoo (>=1.3.1)"] +zstd = ["zstandard (==0.23.0)"] + [[package]] name = "certifi" version = "2025.8.3" @@ -1071,21 +1132,20 @@ dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools ; python_version [[package]] name = "django" -version = "4.2.24" -description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." +version = "3.2.25" +description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." optional = false -python-versions = ">=3.8" +python-versions = ">=3.6" groups = ["main"] files = [ - {file = "django-4.2.24-py3-none-any.whl", hash = "sha256:a6527112c58821a0dfc5ab73013f0bdd906539790a17196658e36e66af43c350"}, - {file = "django-4.2.24.tar.gz", hash = "sha256:40cd7d3f53bc6cd1902eadce23c337e97200888df41e4a73b42d682f23e71d80"}, + {file = "Django-3.2.25-py3-none-any.whl", hash = "sha256:a52ea7fcf280b16f7b739cec38fa6d3f8953a5456986944c3ca97e79882b4e38"}, + {file = "Django-3.2.25.tar.gz", hash = "sha256:7ca38a78654aee72378594d63e51636c04b8e28574f5505dff630895b5472777"}, ] [package.dependencies] -asgiref = ">=3.6.0,<4" -"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} -sqlparse = ">=0.3.1" -tzdata = {version = "*", markers = "sys_platform == \"win32\""} +asgiref = ">=3.3.2,<4" +pytz = "*" +sqlparse = ">=0.2.2" [package.extras] argon2 = ["argon2-cffi (>=19.1.0)"] @@ -1147,20 +1207,20 @@ django = ">=3.2" [[package]] name = "django-cors-headers" -version = "4.9.0" +version = "4.5.0" description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." optional = false python-versions = ">=3.9" groups = ["main"] markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.13\"" files = [ - {file = "django_cors_headers-4.9.0-py3-none-any.whl", hash = "sha256:15c7f20727f90044dcee2216a9fd7303741a864865f0c3657e28b7056f61b449"}, - {file = "django_cors_headers-4.9.0.tar.gz", hash = "sha256:fe5d7cb59fdc2c8c646ce84b727ac2bca8912a247e6e68e1fb507372178e59e8"}, + {file = "django_cors_headers-4.5.0-py3-none-any.whl", hash = "sha256:28c1ded847aa70208798de3e42422a782f427b8b720e8d7319d34b654b5978e6"}, + {file = "django_cors_headers-4.5.0.tar.gz", hash = "sha256:6c01a85cf1ec779a7bde621db853aa3ce5c065a5ba8e27df7a9f9e8dac310f4f"}, ] [package.dependencies] asgiref = ">=3.6" -django = ">=4.2" +django = ">=3.2" [[package]] name = "django-dbconn-retry" @@ -1175,68 +1235,75 @@ files = [ [[package]] name = "django-prometheus" -version = "2.4.1" +version = "2.4.0" description = "Django middlewares to monitor your application with Prometheus.io." optional = false python-versions = "*" groups = ["main"] files = [ - {file = "django_prometheus-2.4.1-py2.py3-none-any.whl", hash = "sha256:7fe5af7f7c9ad9cd8a429fe0f3f1bf651f0e244f77162147869eab7ec09cc5e7"}, - {file = "django_prometheus-2.4.1.tar.gz", hash = "sha256:073628243d2a6de6a8a8c20e5b512872dfb85d66e1b60b28bcf1eca0155dad95"}, + {file = "django_prometheus-2.4.0-py2.py3-none-any.whl", hash = "sha256:5b46b5f07b02ba8dd7abdb03a3c39073e8fd9120e2293a1ecb949bbb865378ac"}, + {file = "django_prometheus-2.4.0.tar.gz", hash = "sha256:67da5c73d8e859aa73f6e11f52341c482691b17f8bd9844157cff6cdf51ce9bc"}, ] [package.dependencies] -Django = ">=4.2,<6.0" prometheus-client = ">=0.7" [[package]] name = "django-timezone-field" -version = "7.1" +version = "7.2.1" description = "A Django app providing DB, form, and REST framework fields for zoneinfo and pytz timezone objects." optional = false python-versions = "<4.0,>=3.8" groups = ["main"] files = [ - {file = "django_timezone_field-7.1-py3-none-any.whl", hash = "sha256:93914713ed882f5bccda080eda388f7006349f25930b6122e9b07bf8db49c4b4"}, - {file = "django_timezone_field-7.1.tar.gz", hash = "sha256:b3ef409d88a2718b566fabe10ea996f2838bc72b22d3a2900c0aa905c761380c"}, + {file = "django_timezone_field-7.2.1-py3-none-any.whl", hash = "sha256:276915b72c5816f57c3baf9e43f816c695ef940d1b21f91ebf6203c09bf4ad44"}, + {file = "django_timezone_field-7.2.1.tar.gz", hash = "sha256:def846f9e7200b7b8f2a28fcce2b78fb2d470f6a9f272b07c4e014f6ba4c6d2e"}, ] [package.dependencies] "backports.zoneinfo" = {version = ">=0.2.1,<0.3.0", markers = "python_version < \"3.9\""} -Django = ">=3.2,<6.0" +Django = ">=3.2,<6.1" [[package]] name = "djangorestframework" -version = "3.15.2" +version = "3.15.1" description = "Web APIs for Django, made easy." optional = false -python-versions = ">=3.8" +python-versions = ">=3.6" groups = ["main"] -markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.13\"" files = [ - {file = "djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20"}, - {file = "djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad"}, + {file = "djangorestframework-3.15.1-py3-none-any.whl", hash = "sha256:3ccc0475bce968608cf30d07fb17d8e52d1d7fc8bfe779c905463200750cbca6"}, + {file = "djangorestframework-3.15.1.tar.gz", hash = "sha256:f88fad74183dfc7144b2756d0d2ac716ea5b4c7c9840995ac3bfd8ec034333c1"}, ] [package.dependencies] "backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} -django = ">=4.2" +django = ">=3.0" [[package]] -name = "djangorestframework" -version = "3.16.1" -description = "Web APIs for Django, made easy." +name = "drf-spectacular" +version = "0.29.0" +description = "Sane and flexible OpenAPI 3 schema generation for Django REST framework" optional = false -python-versions = ">=3.9" +python-versions = ">=3.7" groups = ["main"] -markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.13\"" files = [ - {file = "djangorestframework-3.16.1-py3-none-any.whl", hash = "sha256:33a59f47fb9c85ede792cbf88bde71893bcda0667bc573f784649521f1102cec"}, - {file = "djangorestframework-3.16.1.tar.gz", hash = "sha256:166809528b1aced0a17dc66c24492af18049f2c9420dbd0be29422029cfc3ff7"}, + {file = "drf_spectacular-0.29.0-py3-none-any.whl", hash = "sha256:d1ee7c9535d89848affb4427347f7c4a22c5d22530b8842ef133d7b72e19b41a"}, + {file = "drf_spectacular-0.29.0.tar.gz", hash = "sha256:0a069339ea390ce7f14a75e8b5af4a0860a46e833fd4af027411a3e94fc1a0cc"}, ] [package.dependencies] -django = ">=4.2" +Django = ">=2.2" +djangorestframework = ">=3.10.3" +inflection = ">=0.3.1" +jsonschema = ">=2.6.0" +PyYAML = ">=5.1" +typing-extensions = {version = "*", markers = "python_version < \"3.10\""} +uritemplate = ">=2.0.0" + +[package.extras] +offline = ["drf-spectacular-sidecar"] +sidecar = ["drf-spectacular-sidecar"] [[package]] name = "drf-yasg" @@ -1269,12 +1336,12 @@ version = "1.3.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" -groups = ["dev"] -markers = "python_version < \"3.11\"" +groups = ["main", "dev"] files = [ {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, ] +markers = {main = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\" and python_version < \"3.11\"", dev = "python_version < \"3.11\""} [package.dependencies] typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} @@ -1692,6 +1759,7 @@ description = "Messaging library for Python." optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.13\"" files = [ {file = "kombu-5.5.4-py3-none-any.whl", hash = "sha256:a12ed0557c238897d8e518f1d1fdf84bd1516c5e305af2dacd85c2015115feb8"}, {file = "kombu-5.5.4.tar.gz", hash = "sha256:886600168275ebeada93b888e831352fe578168342f0d1d5833d88ba0d847363"}, @@ -1722,6 +1790,43 @@ sqs = ["boto3 (>=1.26.143)", "urllib3 (>=1.26.16)"] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=2.8.0)"] +[[package]] +name = "kombu" +version = "5.6.2" +description = "Messaging library for Python." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.13\"" +files = [ + {file = "kombu-5.6.2-py3-none-any.whl", hash = "sha256:efcfc559da324d41d61ca311b0c64965ea35b4c55cc04ee36e55386145dace93"}, + {file = "kombu-5.6.2.tar.gz", hash = "sha256:8060497058066c6f5aed7c26d7cd0d3b574990b09de842a8c5aaed0b92cc5a55"}, +] + +[package.dependencies] +amqp = ">=5.1.1,<6.0.0" +packaging = "*" +tzdata = ">=2025.2" +vine = "5.1.0" + +[package.extras] +azureservicebus = ["azure-servicebus (>=7.10.0)"] +azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6.0)"] +confluentkafka = ["confluent-kafka (>=2.2.0)"] +consul = ["python-consul2 (==0.1.5)"] +gcpubsub = ["google-cloud-monitoring (>=2.16.0)", "google-cloud-pubsub (>=2.18.4)", "grpcio (==1.75.1)", "protobuf (==6.32.1)"] +librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] +mongodb = ["pymongo (==4.15.3)"] +msgpack = ["msgpack (==1.1.2)"] +pyro = ["pyro4 (==4.82)"] +qpid = ["qpid-python (==1.36.0-1)", "qpid-tools (==1.36.0-1)"] +redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2,<6.5)"] +slmq = ["softlayer_messaging (>=1.0.3)"] +sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] +sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5) ; sys_platform != \"win32\" and platform_python_implementation == \"CPython\"", "urllib3 (>=1.26.16)"] +yaml = ["PyYAML (>=3.10)"] +zookeeper = ["kazoo (>=2.8.0)"] + [[package]] name = "mako" version = "1.3.10" @@ -4100,16 +4205,35 @@ typing-extensions = ">=4.12.0" [[package]] name = "tzdata" -version = "2025.2" +version = "2025.3" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" groups = ["main"] files = [ - {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, - {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, + {file = "tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1"}, + {file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"}, ] +[[package]] +name = "tzlocal" +version = "5.3.1" +description = "tzinfo object for the local timezone" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.13\"" +files = [ + {file = "tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d"}, + {file = "tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd"}, +] + +[package.dependencies] +tzdata = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] + [[package]] name = "uritemplate" version = "4.1.1" @@ -4404,4 +4528,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.8.0,<4.0" -content-hash = "1164524473a272e2ef48520906ad465781ddae26f408c996b9596ff5b11c77fb" +content-hash = "a9367238448a88e32a86a32ff94cd48a3c96cc6f4f67df603cc55d7b75b205e8" diff --git a/bk-plugin-framework/pyproject.toml b/bk-plugin-framework/pyproject.toml index d6f5405..1f42607 100644 --- a/bk-plugin-framework/pyproject.toml +++ b/bk-plugin-framework/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "bk-plugin-framework" -version = "2.3.3" +version = "2.3.4rc6" description = "bk plugin python framework" authors = ["Your Name "] license = "MIT" @@ -10,8 +10,9 @@ python = "^3.8.0,<4.0" pydantic = ">=1.0,<3" werkzeug = ">=2.0.0, <4.0" apigw-manager = {version = ">=1.0.6, <4", extras = ["extra"]} -bk-plugin-runtime = "2.1.2" +bk-plugin-runtime = "2.1.3rc4" jsonschema = ">=2.5.0,<5.0.0" +drf-spectacular = "^0.29.0" [tool.poetry.dev-dependencies] pytest = "^7.0.0" diff --git a/runtime/bk-plugin-runtime/bk_plugin_runtime/config/default.py b/runtime/bk-plugin-runtime/bk_plugin_runtime/config/default.py index 9fb36b8..bca1e59 100644 --- a/runtime/bk-plugin-runtime/bk_plugin_runtime/config/default.py +++ b/runtime/bk-plugin-runtime/bk_plugin_runtime/config/default.py @@ -12,24 +12,26 @@ import json import os import urllib +from urllib.parse import urlparse from blueapps.conf.default_settings import * # noqa from blueapps.conf.log import get_logging_config_dict BKPAAS_ENVIRONMENT = os.getenv("BKPAAS_ENVIRONMENT", "dev") -# 默认关闭可观侧性 +# 默认关闭可观测性 ENABLE_OTEL_METRICS = os.getenv("ENABLE_METRICS", False) # 请在这里加入你的自定义 APP INSTALLED_APPS += ( # noqa - "rest_framework", - "drf_yasg", "bk_plugin_framework.runtime.loghub", "bk_plugin_framework.runtime.schedule", "bk_plugin_framework.runtime.callback", "bk_plugin_framework.services.bpf_service", - "apigw_manager.apigw", + "rest_framework", + "drf_spectacular", "django_dbconn_retry", + "apigw_manager.drf", + "apigw_manager.apigw", ) if ENABLE_OTEL_METRICS: INSTALLED_APPS += ("blueapps.opentelemetry.instrument_app",) # noqa @@ -75,8 +77,6 @@ "apigw_manager.apigw.authentication.ApiGatewayJWTUserMiddleware", # JWT 透传的用户信息 ) -# 用户认证 -AUTHENTICATION_BACKENDS += ("bk_plugin_runtime.packages.apigw.backends.APIGWUserModelBackend",) # noqa # 所有环境的日志级别可以在这里配置 # LOG_LEVEL = 'INFO' @@ -219,6 +219,45 @@ def logging_addition_settings(logging_dict): break +# drf settings +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": [ + "apigw_manager.drf.authentication.ApiGatewayJWTAuthentication", + ], + "DEFAULT_PERMISSION_CLASSES": [ + "apigw_manager.drf.permission.ApiGatewayPermission", + ], + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", +} + + +# 网关是否公开,公开则其他开发者可见/可申请权限 +BK_APIGW_IS_PUBLIC = os.getenv("BK_APIGW_IS_PUBLIC", "true").lower() +# if BK_APIGW_IS_OFFICIAL is True, the BK_APIGW_NAME should be start with `bk-` +BK_APIGW_IS_OFFICIAL = 1 if os.getenv("BK_APIGW_IS_OFFICIAL", "false").lower() == "true" else 10 +# 网关管理员,请将负责人加入列表中 +BK_APIGW_MAINTAINERS = [m.strip() for m in os.getenv("BK_APIGW_MAINTAINERS", "admin").split(",") if m.strip()] +# 网关接口最大超时时间 +BK_APIGW_STAG_BACKEND_TIMEOUT = 60 + + +# analysis the app environment and address via bkpaas env vars +bkpaas_default_preallocated_urls = json.loads(os.getenv("BKPAAS_DEFAULT_PREALLOCATED_URLS", "{}")) +bkpaas_environment = os.getenv("BKPAAS_ENVIRONMENT", "dev") +app_address = bkpaas_default_preallocated_urls.get(bkpaas_environment) +parsed_url = urlparse(app_address) +app_scheme = parsed_url.scheme +app_domain = parsed_url.netloc +app_subpath = parsed_url.path.rstrip("/") + +BK_APIGW_STAGE_BACKEND_HOST = f"{app_scheme}://{app_domain}" +BK_APIGW_STAGE_BACKEND_SUBPATH = app_subpath + +# 网关同步 API 文档语言, zh/en, 如果配置了BK_APIGW_RESOURCE_DOCS_BASE_DIR(使用自定义文档), 那么必须将这个变量置空 +BK_APIGW_RELEASE_DOC_LANGUAGE = os.getenv("BK_APIGW_RELEASE_DOC_LANGUAGE", "") +# 在项目 docs目录下,通过 markdown文档自动化导入中英文文档; 注意markdown文件名必须等于接口的 operation_id; 见 demo 示例 +# BK_APIGW_RESOURCE_DOCS_BASE_DIR = env.str("BK_APIGW_RESOURCE_DOCS_BASE_DIR", default=BASE_DIR / "docs") + # BK SOPS RELATE BK_SOPS_APP_CODE = os.getenv("BK_SOPS_APP_CODE") diff --git a/runtime/bk-plugin-runtime/bk_plugin_runtime/config/prod.py b/runtime/bk-plugin-runtime/bk_plugin_runtime/config/prod.py index 42da106..7adc4e4 100644 --- a/runtime/bk-plugin-runtime/bk_plugin_runtime/config/prod.py +++ b/runtime/bk-plugin-runtime/bk_plugin_runtime/config/prod.py @@ -16,6 +16,16 @@ else: from blueapps.patch.settings_paas_services import * # noqa +# 生产环境网关配置 +BK_APIGW_STAGE_NAME = "prod" +BK_APIGW_STAGE_DESCRIPTION = "生产环境" +BK_APIGW_STAGE_DESCRIPTION_EN = "Production Env" + +# 生产环境网关环境变量 +BK_APIGW_STAGE_ENV_VARS = { + # "foo": "bar" +} + # 正式环境 RUN_MODE = "PRODUCT" diff --git a/runtime/bk-plugin-runtime/bk_plugin_runtime/config/stag.py b/runtime/bk-plugin-runtime/bk_plugin_runtime/config/stag.py index 7bf5260..118df04 100644 --- a/runtime/bk-plugin-runtime/bk_plugin_runtime/config/stag.py +++ b/runtime/bk-plugin-runtime/bk_plugin_runtime/config/stag.py @@ -18,6 +18,14 @@ else: from blueapps.patch.settings_paas_services import * # noqa +# 开发环境网关配置 +BK_APIGW_STAGE_NAME = "dev" +BK_APIGW_STAGE_DESCRIPTION = "开发环境" +BK_APIGW_STAGE_DESCRIPTION_EN = "Development Env" + +# 预发布环境网关环境变量 +BK_APIGW_STAGE_ENV_VARS = {"foo": "bar"} + # 预发布环境 RUN_MODE = "STAGING" diff --git a/runtime/bk-plugin-runtime/poetry.lock b/runtime/bk-plugin-runtime/poetry.lock index f31cb08..913196a 100644 --- a/runtime/bk-plugin-runtime/poetry.lock +++ b/runtime/bk-plugin-runtime/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "amqp" @@ -81,14 +81,14 @@ tzdata = ["tzdata"] [[package]] name = "billiard" -version = "4.2.1" +version = "4.2.4" description = "Python multiprocessing fork with improvements and bugfixes" optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "billiard-4.2.1-py3-none-any.whl", hash = "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb"}, - {file = "billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f"}, + {file = "billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5"}, + {file = "billiard-4.2.4.tar.gz", hash = "sha256:55f542c371209e03cd5862299b74e52e4fbcba8250ba611ad94276b369b6a85f"}, ] [[package]] @@ -158,6 +158,7 @@ description = "Distributed Task Queue." optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_version < \"3.13\"" files = [ {file = "celery-5.5.3-py3-none-any.whl", hash = "sha256:0b5761a07057acee94694464ca482416b959568904c9dfa41ce8413a7d65d525"}, {file = "celery-5.5.3.tar.gz", hash = "sha256:6c972ae7968c2b5281227f01c3a3f984037d21c5129d07bf3550cc2afc6b10a5"}, @@ -209,6 +210,65 @@ yaml = ["kombu[yaml]"] zookeeper = ["kazoo (>=1.3.1)"] zstd = ["zstandard (==0.23.0)"] +[[package]] +name = "celery" +version = "5.6.2" +description = "Distributed Task Queue." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "celery-5.6.2-py3-none-any.whl", hash = "sha256:3ffafacbe056951b629c7abcf9064c4a2366de0bdfc9fdba421b97ebb68619a5"}, + {file = "celery-5.6.2.tar.gz", hash = "sha256:4a8921c3fcf2ad76317d3b29020772103581ed2454c4c042cc55dcc43585009b"}, +] + +[package.dependencies] +billiard = ">=4.2.1,<5.0" +click = ">=8.1.2,<9.0" +click-didyoumean = ">=0.3.0" +click-plugins = ">=1.1.1" +click-repl = ">=0.2.0" +kombu = ">=5.6.0" +python-dateutil = ">=2.8.2" +tzlocal = "*" +vine = ">=5.1.0,<6.0" + +[package.extras] +arangodb = ["pyArango (>=2.0.2)"] +auth = ["cryptography (==46.0.3)"] +azureblockblob = ["azure-identity (>=1.19.0)", "azure-storage-blob (>=12.15.0)"] +brotli = ["brotli (>=1.0.0) ; platform_python_implementation == \"CPython\"", "brotlipy (>=0.7.0) ; platform_python_implementation == \"PyPy\""] +cassandra = ["cassandra-driver (>=3.25.0,<4)"] +consul = ["python-consul2 (==0.1.5)"] +cosmosdbsql = ["pydocumentdb (==2.3.5)"] +couchbase = ["couchbase (>=3.0.0) ; platform_python_implementation != \"PyPy\" and (platform_system != \"Windows\" or python_version < \"3.10\")"] +couchdb = ["pycouchdb (==1.16.0)"] +django = ["Django (>=2.2.28)"] +dynamodb = ["boto3 (>=1.26.143)"] +elasticsearch = ["elastic-transport (<=9.1.0)", "elasticsearch (<=9.1.2)"] +eventlet = ["eventlet (>=0.32.0) ; python_version < \"3.10\""] +gcs = ["google-cloud-firestore (==2.22.0)", "google-cloud-storage (>=2.10.0)", "grpcio (==1.75.1)"] +gevent = ["gevent (>=1.5.0)"] +librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] +memcache = ["pylibmc (==1.6.3) ; platform_system != \"Windows\""] +mongodb = ["kombu[mongodb]"] +msgpack = ["kombu[msgpack]"] +pydantic = ["pydantic (>=2.12.0a1) ; python_version >= \"3.14\"", "pydantic (>=2.4) ; python_version < \"3.14\""] +pymemcache = ["python-memcached (>=1.61)"] +pyro = ["pyro4 (==4.82) ; python_version < \"3.11\""] +pytest = ["pytest-celery[all] (>=1.2.0,<1.3.0)"] +redis = ["kombu[redis]"] +s3 = ["boto3 (>=1.26.143)"] +slmq = ["softlayer_messaging (>=1.0.3)"] +solar = ["ephem (==4.2) ; platform_python_implementation != \"PyPy\""] +sqlalchemy = ["kombu[sqlalchemy]"] +sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.5.0)", "pycurl (>=7.43.0.5,<7.45.4) ; sys_platform != \"win32\" and platform_python_implementation == \"CPython\" and python_version < \"3.9\"", "pycurl (>=7.45.4) ; sys_platform != \"win32\" and platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "urllib3 (>=1.26.16)"] +tblib = ["tblib (==3.2.2)"] +yaml = ["kombu[yaml]"] +zookeeper = ["kazoo (>=1.3.1)"] +zstd = ["zstandard (==0.23.0)"] + [[package]] name = "certifi" version = "2025.8.3" @@ -398,6 +458,7 @@ description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "python_version < \"3.13\"" files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -406,6 +467,22 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +[[package]] +name = "click" +version = "8.3.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, + {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "click-didyoumean" version = "0.3.1" @@ -478,6 +555,7 @@ description = "A Python library that converts cron expressions into human readab optional = false python-versions = "*" groups = ["main"] +markers = "python_version < \"3.13\"" files = [ {file = "cron_descriptor-1.4.5-py3-none-any.whl", hash = "sha256:736b3ae9d1a99bc3dbfc5b55b5e6e7c12031e7ba5de716625772f8b02dcd6013"}, {file = "cron_descriptor-1.4.5.tar.gz", hash = "sha256:f51ce4ffc1d1f2816939add8524f206c376a42c87a5fca3091ce26725b3b1bca"}, @@ -486,6 +564,26 @@ files = [ [package.extras] dev = ["polib"] +[[package]] +name = "cron-descriptor" +version = "2.0.6" +description = "A Python library that converts cron expressions into human readable strings." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "cron_descriptor-2.0.6-py3-none-any.whl", hash = "sha256:3a1c0d837c0e5a32e415f821b36cf758eb92d510e6beff8fbfe4fa16573d93d6"}, + {file = "cron_descriptor-2.0.6.tar.gz", hash = "sha256:e39d2848e1d8913cfb6e3452e701b5eec662ee18bea8cc5aa53ee1a7bb217157"}, +] + +[package.dependencies] +typing_extensions = "*" + +[package.extras] +dev = ["mypy", "polib", "ruff"] +test = ["pytest"] + [[package]] name = "cryptography" version = "43.0.3" @@ -574,21 +672,20 @@ dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools ; python_version [[package]] name = "django" -version = "4.2.23" -description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." +version = "3.2.25" +description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." optional = false -python-versions = ">=3.8" +python-versions = ">=3.6" groups = ["main"] files = [ - {file = "django-4.2.23-py3-none-any.whl", hash = "sha256:dafbfaf52c2f289bd65f4ab935791cb4fb9a198f2a5ba9faf35d7338a77e9803"}, - {file = "django-4.2.23.tar.gz", hash = "sha256:42fdeaba6e6449d88d4f66de47871015097dc6f1b87910db00a91946295cfae4"}, + {file = "Django-3.2.25-py3-none-any.whl", hash = "sha256:a52ea7fcf280b16f7b739cec38fa6d3f8953a5456986944c3ca97e79882b4e38"}, + {file = "Django-3.2.25.tar.gz", hash = "sha256:7ca38a78654aee72378594d63e51636c04b8e28574f5505dff630895b5472777"}, ] [package.dependencies] -asgiref = ">=3.6.0,<4" -"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} -sqlparse = ">=0.3.1" -tzdata = {version = "*", markers = "sys_platform == \"win32\""} +asgiref = ">=3.3.2,<4" +pytz = "*" +sqlparse = ">=0.2.2" [package.extras] argon2 = ["argon2-cffi (>=19.1.0)"] @@ -638,6 +735,7 @@ description = "django-cors-headers is a Django application for handling the serv optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_version < \"3.13\"" files = [ {file = "django_cors_headers-4.4.0-py3-none-any.whl", hash = "sha256:5c6e3b7fe870876a1efdfeb4f433782c3524078fa0dc9e0195f6706ce7a242f6"}, {file = "django_cors_headers-4.4.0.tar.gz", hash = "sha256:92cf4633e22af67a230a1456cb1b7a02bb213d6536d2dcb2a4a24092ea9cebc2"}, @@ -647,6 +745,23 @@ files = [ asgiref = ">=3.6" django = ">=3.2" +[[package]] +name = "django-cors-headers" +version = "4.5.0" +description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "django_cors_headers-4.5.0-py3-none-any.whl", hash = "sha256:28c1ded847aa70208798de3e42422a782f427b8b720e8d7319d34b654b5978e6"}, + {file = "django_cors_headers-4.5.0.tar.gz", hash = "sha256:6c01a85cf1ec779a7bde621db853aa3ce5c065a5ba8e27df7a9f9e8dac310f4f"}, +] + +[package.dependencies] +asgiref = ">=3.6" +django = ">=3.2" + [[package]] name = "django-dbconn-retry" version = "0.1.9" @@ -660,51 +775,50 @@ files = [ [[package]] name = "django-prometheus" -version = "2.4.1" +version = "2.4.0" description = "Django middlewares to monitor your application with Prometheus.io." optional = false python-versions = "*" groups = ["main"] files = [ - {file = "django_prometheus-2.4.1-py2.py3-none-any.whl", hash = "sha256:7fe5af7f7c9ad9cd8a429fe0f3f1bf651f0e244f77162147869eab7ec09cc5e7"}, - {file = "django_prometheus-2.4.1.tar.gz", hash = "sha256:073628243d2a6de6a8a8c20e5b512872dfb85d66e1b60b28bcf1eca0155dad95"}, + {file = "django_prometheus-2.4.0-py2.py3-none-any.whl", hash = "sha256:5b46b5f07b02ba8dd7abdb03a3c39073e8fd9120e2293a1ecb949bbb865378ac"}, + {file = "django_prometheus-2.4.0.tar.gz", hash = "sha256:67da5c73d8e859aa73f6e11f52341c482691b17f8bd9844157cff6cdf51ce9bc"}, ] [package.dependencies] -Django = ">=4.2,<6.0" prometheus-client = ">=0.7" [[package]] name = "django-timezone-field" -version = "7.1" +version = "7.2.1" description = "A Django app providing DB, form, and REST framework fields for zoneinfo and pytz timezone objects." optional = false python-versions = "<4.0,>=3.8" groups = ["main"] files = [ - {file = "django_timezone_field-7.1-py3-none-any.whl", hash = "sha256:93914713ed882f5bccda080eda388f7006349f25930b6122e9b07bf8db49c4b4"}, - {file = "django_timezone_field-7.1.tar.gz", hash = "sha256:b3ef409d88a2718b566fabe10ea996f2838bc72b22d3a2900c0aa905c761380c"}, + {file = "django_timezone_field-7.2.1-py3-none-any.whl", hash = "sha256:276915b72c5816f57c3baf9e43f816c695ef940d1b21f91ebf6203c09bf4ad44"}, + {file = "django_timezone_field-7.2.1.tar.gz", hash = "sha256:def846f9e7200b7b8f2a28fcce2b78fb2d470f6a9f272b07c4e014f6ba4c6d2e"}, ] [package.dependencies] "backports.zoneinfo" = {version = ">=0.2.1,<0.3.0", markers = "python_version < \"3.9\""} -Django = ">=3.2,<6.0" +Django = ">=3.2,<6.1" [[package]] name = "djangorestframework" -version = "3.15.2" +version = "3.15.1" description = "Web APIs for Django, made easy." optional = false -python-versions = ">=3.8" +python-versions = ">=3.6" groups = ["main"] files = [ - {file = "djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20"}, - {file = "djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad"}, + {file = "djangorestframework-3.15.1-py3-none-any.whl", hash = "sha256:3ccc0475bce968608cf30d07fb17d8e52d1d7fc8bfe779c905463200750cbca6"}, + {file = "djangorestframework-3.15.1.tar.gz", hash = "sha256:f88fad74183dfc7144b2756d0d2ac716ea5b4c7c9840995ac3bfd8ec034333c1"}, ] [package.dependencies] "backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} -django = ">=4.2" +django = ">=3.0" [[package]] name = "drf-yasg" @@ -963,6 +1077,7 @@ description = "Messaging library for Python." optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_version < \"3.13\"" files = [ {file = "kombu-5.5.4-py3-none-any.whl", hash = "sha256:a12ed0557c238897d8e518f1d1fdf84bd1516c5e305af2dacd85c2015115feb8"}, {file = "kombu-5.5.4.tar.gz", hash = "sha256:886600168275ebeada93b888e831352fe578168342f0d1d5833d88ba0d847363"}, @@ -993,6 +1108,43 @@ sqs = ["boto3 (>=1.26.143)", "urllib3 (>=1.26.16)"] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=2.8.0)"] +[[package]] +name = "kombu" +version = "5.6.2" +description = "Messaging library for Python." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "kombu-5.6.2-py3-none-any.whl", hash = "sha256:efcfc559da324d41d61ca311b0c64965ea35b4c55cc04ee36e55386145dace93"}, + {file = "kombu-5.6.2.tar.gz", hash = "sha256:8060497058066c6f5aed7c26d7cd0d3b574990b09de842a8c5aaed0b92cc5a55"}, +] + +[package.dependencies] +amqp = ">=5.1.1,<6.0.0" +packaging = "*" +tzdata = ">=2025.2" +vine = "5.1.0" + +[package.extras] +azureservicebus = ["azure-servicebus (>=7.10.0)"] +azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6.0)"] +confluentkafka = ["confluent-kafka (>=2.2.0)"] +consul = ["python-consul2 (==0.1.5)"] +gcpubsub = ["google-cloud-monitoring (>=2.16.0)", "google-cloud-pubsub (>=2.18.4)", "grpcio (==1.75.1)", "protobuf (==6.32.1)"] +librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] +mongodb = ["pymongo (==4.15.3)"] +msgpack = ["msgpack (==1.1.2)"] +pyro = ["pyro4 (==4.82)"] +qpid = ["qpid-python (==1.36.0-1)", "qpid-tools (==1.36.0-1)"] +redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2,<6.5)"] +slmq = ["softlayer_messaging (>=1.0.3)"] +sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] +sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5) ; sys_platform != \"win32\" and platform_python_implementation == \"CPython\"", "urllib3 (>=1.26.16)"] +yaml = ["PyYAML (>=3.10)"] +zookeeper = ["kazoo (>=2.8.0)"] + [[package]] name = "mako" version = "1.3.10" @@ -1481,14 +1633,14 @@ twisted = ["twisted"] [[package]] name = "prompt-toolkit" -version = "3.0.51" +version = "3.0.52" description = "Library for building powerful interactive command lines in Python" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07"}, - {file = "prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed"}, + {file = "prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955"}, + {file = "prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855"}, ] [package.dependencies] @@ -1853,16 +2005,35 @@ files = [ [[package]] name = "tzdata" -version = "2025.2" +version = "2025.3" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" groups = ["main"] files = [ - {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, - {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, + {file = "tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1"}, + {file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"}, ] +[[package]] +name = "tzlocal" +version = "5.3.1" +description = "tzinfo object for the local timezone" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d"}, + {file = "tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd"}, +] + +[package.dependencies] +tzdata = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] + [[package]] name = "uritemplate" version = "4.1.1" @@ -1907,14 +2078,14 @@ files = [ [[package]] name = "wcwidth" -version = "0.2.13" +version = "0.2.14" description = "Measures the displayed width of unicode strings in a terminal" optional = false -python-versions = "*" +python-versions = ">=3.6" groups = ["main"] files = [ - {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, - {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, + {file = "wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1"}, + {file = "wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605"}, ] [[package]] diff --git a/runtime/bk-plugin-runtime/pyproject.toml b/runtime/bk-plugin-runtime/pyproject.toml index d848a5d..2f14e0c 100644 --- a/runtime/bk-plugin-runtime/pyproject.toml +++ b/runtime/bk-plugin-runtime/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "bk-plugin-runtime" -version = "2.1.2" +version = "2.1.3rc4" description = "bk plugin python django runtime" authors = ["Your Name "] license = "MIT" diff --git a/template/{{cookiecutter.project_name}}/app_desc.yml b/template/{{cookiecutter.project_name}}/app_desc.yml index cb71b87..dee99b0 100644 --- a/template/{{cookiecutter.project_name}}/app_desc.yml +++ b/template/{{cookiecutter.project_name}}/app_desc.yml @@ -4,6 +4,9 @@ modules: isDefault: true language: python spec: + hooks: + preRelease: + procCommand: "bash bin/sync_apigateway.sh" processes: - name: web procCommand: gunicorn bk_plugin_runtime.wsgi --timeout 120 -k gthread --threads 16 -w 8 --max-requests=1000 --env prometheus_multiproc_dir=/tmp/ @@ -14,9 +17,9 @@ modules: targetPort: 5000 port: 80 - name: schedule - procCommand: celery worker -A blueapps.core.celery -P threads -n schedule_worker@%h -c 500 -Q plugin_schedule,plugin_callback,schedule_delete -l INFO + procCommand: celery -A blueapps.core.celery worker -P threads -n schedule_worker@%h -c 500 -Q plugin_schedule,plugin_callback,schedule_delete -l INFO - name: beat - procCommand: celery beat -A blueapps.core.celery -l INFO + procCommand: celery -A blueapps.core.celery beat -l INFO - name: c-monitor procCommand: celery-prometheus-exporter --broker amqp://$RABBITMQ_USER:$RABBITMQ_PASSWORD@$RABBITMQ_HOST:$RABBITMQ_PORT/$RABBITMQ_VHOST --addr 0.0.0.0:5001 --queue-list plugin_schedule plugin_callback schedule_delete configuration: diff --git a/template/{{cookiecutter.project_name}}/bin/post_compile b/template/{{cookiecutter.project_name}}/bin/post_compile deleted file mode 100644 index 19c7a3c..0000000 --- a/template/{{cookiecutter.project_name}}/bin/post_compile +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -# DO NOT MODIFY THIS SECTION !!! -env -python bin/manage.py migrate -python bin/manage.py sync_plugin_apigw -python bin/manage.py fetch_apigw_public_key -# DO NOT MODIFY THIS SECTION !!! \ No newline at end of file diff --git a/template/{{cookiecutter.project_name}}/bin/sync_apigateway.sh b/template/{{cookiecutter.project_name}}/bin/sync_apigateway.sh new file mode 100755 index 0000000..f4c3fa2 --- /dev/null +++ b/template/{{cookiecutter.project_name}}/bin/sync_apigateway.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# DO NOT MODIFY THIS SECTION !!! +echo "do migrate" +python bin/manage.py migrate --no-input + +echo "[Sync] BEGIN =====================" + +# 使用智能同步命令,仅在接口定义发生变化时才同步到API网关 +# 该命令会自动: +# 1. 生成 definition.yaml 和 resources.yaml +# 2. 计算文件哈希值并与上次同步的哈希值对比 +# 3. 仅在哈希值变化时执行 sync_drf_apigateway +# 4. 获取 API 网关公钥 +# +# 如需强制同步,可添加 --force 参数: +# python bin/manage.py sync_apigateway_if_changed --force + +python bin/manage.py sync_apigateway_if_changed +if [ $? -ne 0 ] +then + echo "sync_apigateway_if_changed fail" + exit 1 +fi + +echo "[Sync] DONE =====================" +# DO NOT MODIFY THIS SECTION !!! \ No newline at end of file diff --git a/template/{{cookiecutter.project_name}}/requirements.txt b/template/{{cookiecutter.project_name}}/requirements.txt index 5dd69ee..1d7ce4b 100644 --- a/template/{{cookiecutter.project_name}}/requirements.txt +++ b/template/{{cookiecutter.project_name}}/requirements.txt @@ -1,9 +1,8 @@ # base # DO NOT DELETE ANY PACKAGE IN base SECTION !!! -bk-plugin-framework==2.2.9 +bk-plugin-framework==2.3.4rc3 # opentelemetry celery-prometheus-exporter==1.7.0 -celery>=4.4.0,<5.0.0 # Explicit celery version constraint to resolve pip 25.x dependency conflict opentelemetry-api==1.25.0 opentelemetry-exporter-jaeger==1.21.0 opentelemetry-exporter-jaeger-proto-grpc==1.21.0