Skip to content

Commit

Permalink
openapi sync stage 不触发发布
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-smile committed Nov 8, 2023
1 parent 2f47a67 commit fd7a754
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 37 deletions.
28 changes: 28 additions & 0 deletions src/dashboard/apigateway/apigateway/apis/open/stage/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@
# We undertake not to change the open source license (MIT license) applicable
# to the current version of the project delivered to anyone in the future.
#
import json

from rest_framework import serializers
from tencent_apigateway_common.i18n.field import SerializerTranslatedField

from apigateway.apps.stage.serializers import StageSLZ
from apigateway.biz.stage import StageHandler
from apigateway.core.models import Stage


class StageV1SLZ(serializers.Serializer):
id = serializers.IntegerField()
Expand All @@ -42,3 +48,25 @@ def to_representation(self, instance):

def get_released(self, obj):
return bool(obj.resource_version)


class StageSyncInputSLZ(StageSLZ):
def update(self, instance, validated_data):
# 未校验 resource version 中引用的 stage vars 是否存在,
# 因此,不触发 update 信号,以防止误发布
Stage.objects.filter(id=instance.id).update(
description=validated_data.get("description"),
description_en=validated_data.get("description_en"),
_vars=json.dumps(vars),
updated_by=validated_data.get("updated_by", ""),
)

StageHandler.save_related_data(
instance,
validated_data["proxy_http"],
validated_data.get("rate_limit"),
)

StageHandler.add_update_audit_log(validated_data["api"], instance, validated_data.get("updated_by", ""))

return instance
6 changes: 3 additions & 3 deletions src/dashboard/apigateway/apigateway/apis/open/stage/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
from rest_framework import status, viewsets

from apigateway.apis.open.stage import serializers
from apigateway.apps.stage.serializers import StageSLZ
from apigateway.common.permissions import GatewayRelatedAppPermission
from apigateway.core.constants import StageStatusEnum
from apigateway.core.models import Release, Stage
Expand Down Expand Up @@ -87,14 +86,15 @@ def list_stages_with_resource_version(self, request, gateway_name: str, *args, *
class StageSyncViewSet(viewsets.ViewSet):
permission_classes = [GatewayRelatedAppPermission]

@swagger_auto_schema(request_body=StageSLZ, tags=["OpenAPI.Stage"])
@swagger_auto_schema(request_body=serializers.StageSyncInputSLZ, tags=["OpenAPI.Stage"])
def sync(self, request, gateway_name: str, *args, **kwargs):
instance = get_object_or_None(Stage, api=request.gateway, name=request.data.get("name", ""))
slz = StageSLZ(
slz = serializers.StageSyncInputSLZ(
instance,
data=request.data,
context={
"request": request,
"allow_var_not_exist": True,
},
)
slz.is_valid(raise_exception=True)
Expand Down
35 changes: 4 additions & 31 deletions src/dashboard/apigateway/apigateway/apps/release/releasers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@
from apigateway.apps.audit.constants import OpObjectTypeEnum, OpStatusEnum, OpTypeEnum
from apigateway.apps.audit.utils import record_audit_log
from apigateway.apps.release.serializers import ReleaseBatchSLZ
from apigateway.apps.stage.validators import StageVarsValuesValidator
from apigateway.apps.support.models import ReleasedResourceDoc, ResourceDocVersion
from apigateway.common.contexts import StageProxyHTTPContext
from apigateway.common.event.event import PublishEventReporter
from apigateway.controller.tasks import (
mark_release_history_failure,
Expand All @@ -53,15 +51,13 @@
)
from apigateway.core.signals import reversion_update_signal

from .validators import ReleaseValidationError, ReleaseValidator


class ReleaseError(Exception):
"""发布失败"""


class ReleaseValidationError(Exception):
"""发布校验失败"""


class NonRelatedMicroGatewayError(Exception):
"""环境未关联微网关实例"""

Expand Down Expand Up @@ -168,31 +164,8 @@ def release_batch(self):
def _validate(self):
"""校验待发布数据"""
for stage in self.stages:
self._validate_stage_upstreams(self.gateway.id, stage, self.resource_version.id)
self._validate_stage_vars(stage, self.resource_version.id)

def _validate_stage_upstreams(self, gateway_id: int, stage: Stage, resource_version_id: int):
"""检查环境的代理配置,如果未配置任何有效的上游主机地址(Hosts),则报错。
:raise ReleaseValidationError: 当未配置 Hosts 时。
"""
if not StageProxyHTTPContext().contain_hosts(stage.id):
raise ReleaseValidationError(
_("网关环境【{stage_name}】中代理配置 Hosts 未配置,请在网关 `基本设置 -> 环境管理` 中进行配置。").format( # noqa: E501
stage_name=stage.name,
)
)

def _validate_stage_vars(self, stage: Stage, resource_version_id: int):
validator = StageVarsValuesValidator()
validator(
{
"gateway": self.gateway,
"stage_name": stage.name,
"vars": stage.vars,
"resource_version_id": resource_version_id,
}
)
validator = ReleaseValidator(self.gateway, stage, self.resource_version.id)
validator.validate()

def _do_release(self, releases: List[Release], release_history: ReleaseHistory):
"""发布资源版本"""
Expand Down
67 changes: 67 additions & 0 deletions src/dashboard/apigateway/apigateway/apps/release/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
#
# TencentBlueKing is pleased to support the open source community by making
# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available.
# Copyright (C) 2017 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.
#
# We undertake not to change the open source license (MIT license) applicable
# to the current version of the project delivered to anyone in the future.
#
from django.utils.translation import gettext as _

from apigateway.apps.stage.validators import StageVarsValuesValidator
from apigateway.common.contexts import StageProxyHTTPContext
from apigateway.core.models import Gateway, Stage


class ReleaseValidationError(Exception):
"""发布校验失败"""


class ReleaseValidator:
"""
发布校验器
- 1. 校验环境的代理配置中,上游主机地址有效
- 2. 校验环境变量有效,且 resource version 中资源引用的环境变量有效
"""

def __init__(self, gateway: Gateway, stage: Stage, resource_version_id: int):
self.gateway = gateway
self.stage = stage
self.resource_version_id = resource_version_id

def validate(self):
self._validate_stage_upstreams()
self._validate_stage_vars()

def _validate_stage_upstreams(self):
"""检查环境的代理配置,如果未配置任何有效的上游主机地址(Hosts),则报错。
:raise ReleaseValidationError: 当未配置 Hosts 时。
"""
if not StageProxyHTTPContext().contain_hosts(self.stage.id):
raise ReleaseValidationError(
_("网关环境【{stage_name}】中代理配置 Hosts 未配置,请在网关 `基本设置 -> 环境管理` 中进行配置。").format(
stage_name=self.stage.name,
)
)

def _validate_stage_vars(self):
validator = StageVarsValuesValidator()
validator(
{
"gateway": self.gateway,
"stage_name": self.stage.name,
"vars": self.stage.vars,
"resource_version_id": self.resource_version_id,
}
)
21 changes: 19 additions & 2 deletions src/dashboard/apigateway/apigateway/apps/stage/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,24 @@ def __call__(self, attrs):
stage_vars = attrs["vars"]
resource_version_id = attrs["resource_version_id"]

# 允许变量不存在:
# openapi 同步环境时,存在修改变量名的情况,此时,当前 resource version 中资源引用的变量可能不存在;
# 针对此场景,允许更新环境,但是将不再触发版本发布
allow_var_not_exist = attrs.get("allow_var_not_exist", False)

used_stage_vars = ResourceVersion.objects.get_used_stage_vars(gateway.id, resource_version_id)
if not used_stage_vars:
return

for key in used_stage_vars["in_path"]:
if key not in stage_vars:
if allow_var_not_exist:
continue

raise serializers.ValidationError(
_("环境【{stage_name}】中,环境变量【{key}】在发布版本的资源配置中被用作路径变量,必须存在。").format(stage_name=stage_name, key=key),
)

if not STAGE_VAR_FOR_PATH_PATTERN.match(stage_vars[key]):
raise serializers.ValidationError(
_("环境【{stage_name}】中,环境变量【{key}】在发布版本的资源配置中被用作路径变量,变量值不是一个合法的 URL 路径片段。").format(
Expand All @@ -57,11 +66,15 @@ def __call__(self, attrs):
for key in used_stage_vars["in_host"]:
_value = stage_vars.get(key)
if not _value:
if allow_var_not_exist:
continue

raise serializers.ValidationError(
_("环境【{stage_name}】中,环境变量【{key}】在发布版本的资源配置中被用作 Host 变量,不能为空。").format(
stage_name=stage_name, key=key
),
)

if not HOST_WITHOUT_SCHEME_PATTERN.match(_value):
raise serializers.ValidationError(
_('环境【{stage_name}】中,环境变量【{key}】在发布版本的资源配置中被用作 Host 变量,变量值不是一个合法的 Host(不包含"http(s)://")。').format(
Expand All @@ -82,8 +95,11 @@ def __call__(self, attrs: dict, serializer):
gateway = self._get_gateway(serializer)
instance = getattr(serializer, "instance", None)

context = getattr(serializer, "context", {})
allow_var_not_exist = context.get("allow_var_not_exist", False)

self._validate_vars_keys(attrs["vars"])
self._validate_vars_values(attrs["vars"], gateway, instance)
self._validate_vars_values(attrs["vars"], gateway, instance, allow_var_not_exist)

def _validate_vars_keys(self, _vars: dict):
"""
Expand All @@ -95,7 +111,7 @@ def _validate_vars_keys(self, _vars: dict):
_("变量名【{key}】非法,应由字母、数字、下划线(_)组成,首字符必须是字母,长度小于50个字符。").format(key=key),
)

def _validate_vars_values(self, _vars: dict, gateway, instance):
def _validate_vars_values(self, _vars: dict, gateway, instance, allow_var_not_exist: bool):
"""
校验变量的值是否符合要求
- 用作路径变量时:值应符合路径片段规则
Expand All @@ -116,5 +132,6 @@ def _validate_vars_values(self, _vars: dict, gateway, instance):
"stage_name": instance.name,
"vars": _vars,
"resource_version_id": stage_release["resource_version_id"],
"allow_var_not_exist": allow_var_not_exist,
}
)
10 changes: 10 additions & 0 deletions src/dashboard/apigateway/apigateway/controller/tasks/syncing.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
from blue_krill.async_utils.django_utils import delay_on_commit
from celery import shared_task
from django.conf import settings
from rest_framework.exceptions import ValidationError

from apigateway.apps.release.validators import ReleaseValidationError, ReleaseValidator
from apigateway.controller.distributor.combine import CombineDistributor
from apigateway.controller.procedure_logger.release_logger import ReleaseProcedureLogger
from apigateway.core.constants import APIHostingTypeEnum, APIStatusEnum, StageStatusEnum
Expand Down Expand Up @@ -71,6 +73,14 @@ def rolling_update_release(gateway_id, publish_id: Optional[int] = None):
procedure_logger.warning("stage is not active, ignored")
continue

try:
validator = ReleaseValidator(gateway, stage, release.resource_version_id)
validator.validate()
except (ValidationError, ReleaseValidationError) as err:
procedure_logger.warning(f"release(id={release.pk}) validate failed, ignored, error={err}")
has_failure = True
continue

procedure_logger.info("distribute begin")
if not distributor.distribute(
release,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
MicroGatewayReleaser,
ReleaseBatchManager,
ReleaseError,
ReleaseValidationError,
)
from apigateway.apps.release.validators import ReleaseValidationError
from apigateway.core.constants import APIHostingTypeEnum, ReleaseStatusEnum
from apigateway.core.models import Release, ReleaseHistory, ResourceVersion, Stage

Expand Down

0 comments on commit fd7a754

Please sign in to comment.