Skip to content

Commit

Permalink
feat(backend): 标签功能 #6235
Browse files Browse the repository at this point in the history
  • Loading branch information
zhangzhw8 committed Oct 22, 2024
1 parent 5c690e4 commit 53c498c
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 18 deletions.
84 changes: 84 additions & 0 deletions dbm-ui/backend/db_meta/migrations/0043_auto_20241015_2128.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Generated by Django 3.2.25 on 2024-10-15 13:28

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("db_meta", "0042_auto_20240903_1138"),
]

operations = [
migrations.AlterModelOptions(
name="bksubzone",
options={"verbose_name": "蓝鲸园区表(BKSubzone)", "verbose_name_plural": "蓝鲸园区表(BKSubzone)"},
),
migrations.AddField(
model_name="cluster",
name="tags",
field=models.ManyToManyField(blank=True, help_text="标签(外键)", to="db_meta.Tag"),
),
migrations.AddField(
model_name="tag",
name="key",
field=models.CharField(default="", help_text="标签键", max_length=64),
),
migrations.AddField(
model_name="tag",
name="value",
field=models.CharField(default="", help_text="标签值", max_length=255),
),
migrations.AlterField(
model_name="spec",
name="cpu",
field=models.JSONField(help_text='cpu规格描述:{"min":1,"max":10}', null=True),
),
migrations.AlterField(
model_name="spec",
name="device_class",
field=models.JSONField(help_text='实际机器机型: ["class1","class2"]', null=True),
),
migrations.AlterField(
model_name="spec",
name="mem",
field=models.JSONField(help_text='mem规格描述:{"min":100,"max":1000}', null=True),
),
migrations.AlterField(
model_name="spec",
name="qps",
field=models.JSONField(default=dict, help_text='qps规格描述:{"min": 1, "max": 100}'),
),
migrations.AlterField(
model_name="spec",
name="storage_spec",
field=models.JSONField(help_text='存储磁盘需求配置:[{"mount_point":"/data","size":500,"type":"ssd"}]', null=True),
),
migrations.AlterField(
model_name="tag",
name="bk_biz_id",
field=models.IntegerField(default=0, help_text="业务 ID"),
),
migrations.AlterField(
model_name="tag",
name="type",
field=models.CharField(
choices=[("custom", "自定义标签"), ("system", "系统标签"), ("builtin", "内置标签")],
default="custom",
help_text="tag类型",
max_length=64,
),
),
migrations.AlterUniqueTogether(
name="tag",
unique_together={("bk_biz_id", "key", "value")},
),
migrations.RemoveField(
model_name="tag",
name="cluster",
),
migrations.RemoveField(
model_name="tag",
name="name",
),
]
6 changes: 4 additions & 2 deletions dbm-ui/backend/db_meta/models/tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
class Tag(AuditedModel):
bk_biz_id = models.IntegerField(help_text=_("业务 ID"), default=0)
key = models.CharField(help_text=_("标签键"), default="", max_length=64)
value = models.CharField(help_text=_("标签值"), default="", max_length=64)
type = models.CharField(help_text=_("tag类型"), max_length=64, choices=TagType.get_choices())
value = models.CharField(help_text=_("标签值"), default="", max_length=255)
type = models.CharField(
help_text=_("tag类型"), max_length=64, choices=TagType.get_choices(), default=TagType.CUSTOM.value
)

class Meta:
unique_together = ["bk_biz_id", "key", "value"]
Expand Down
2 changes: 1 addition & 1 deletion dbm-ui/backend/db_services/group/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from backend.db_meta.models import Group, GroupInstance


class TagSerializer(AuditedSerializer, serializers.ModelSerializer):
class GroupSerializer(AuditedSerializer, serializers.ModelSerializer):
instance_count = serializers.SerializerMethodField(help_text=_("实例数量"))

class Meta:
Expand Down
12 changes: 6 additions & 6 deletions dbm-ui/backend/db_services/group/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
)
from backend.db_meta.models import Group
from backend.db_services.group.handlers import GroupHandler
from backend.db_services.group.serializers import GroupMoveInstancesSerializer, TagSerializer
from backend.db_services.group.serializers import GroupMoveInstancesSerializer, GroupSerializer
from backend.iam_app.handlers.drf_perm.base import RejectPermission, get_request_key_id

SWAGGER_TAG = _("分组")
Expand All @@ -35,7 +35,7 @@ class GroupViewSet(viewsets.AuditedModelViewSet):
"""

queryset = Group.objects.all()
serializer_class = TagSerializer
serializer_class = GroupSerializer
pagination_class = AuditedLimitOffsetPagination

# TODO: 暂时屏蔽对influxdb的鉴权
Expand Down Expand Up @@ -74,7 +74,7 @@ def get_queryset(self):

@common_swagger_auto_schema(
operation_summary=_("分组详情"),
responses={status.HTTP_200_OK: TagSerializer(label=_("分组详情"))},
responses={status.HTTP_200_OK: GroupSerializer(label=_("分组详情"))},
tags=[SWAGGER_TAG],
)
def retrieve(self, request, *args, **kwargs):
Expand All @@ -98,7 +98,7 @@ def list(self, request, *args, **kwargs):
@common_swagger_auto_schema(
operation_summary=_("创建新分组"),
auto_schema=ResponseSwaggerAutoSchema,
responses={status.HTTP_200_OK: TagSerializer(label=_("创建新分组"))},
responses={status.HTTP_200_OK: GroupSerializer(label=_("创建新分组"))},
tags=[SWAGGER_TAG],
)
def create(self, request, *args, **kwargs):
Expand All @@ -107,7 +107,7 @@ def create(self, request, *args, **kwargs):
@common_swagger_auto_schema(
operation_summary=_("更新分组信息"),
auto_schema=ResponseSwaggerAutoSchema,
responses={status.HTTP_200_OK: TagSerializer(label=_("更新分组信息"))},
responses={status.HTTP_200_OK: GroupSerializer(label=_("更新分组信息"))},
tags=[SWAGGER_TAG],
)
def update(self, request, *args, **kwargs):
Expand All @@ -116,7 +116,7 @@ def update(self, request, *args, **kwargs):
@common_swagger_auto_schema(
operation_summary=_("删除分组"),
auto_schema=ResponseSwaggerAutoSchema,
responses={status.HTTP_200_OK: TagSerializer(label=_("删除分组"))},
responses={status.HTTP_200_OK: GroupSerializer(label=_("删除分组"))},
tags=[SWAGGER_TAG],
)
def destroy(self, request, *args, **kwargs):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,10 @@
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.utils.translation import gettext_lazy as _

from blue_krill.data_types.enum import EnumField, StructuredEnum


class TagResourceType(str, StructuredEnum):
DB_RESOURCE = EnumField("db_resource", _("资源池"))
66 changes: 62 additions & 4 deletions dbm-ui/backend/db_services/tag/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@
"""
from typing import Dict, List

from django.db.models import ManyToManyRel
from django.utils.translation import gettext_lazy as _

from backend.db_meta.models import Tag
from backend.db_services.tag.constants import TagResourceType
from backend.exceptions import ValidationError


class TagHandler:
"""标签的操作类"""
Expand All @@ -23,20 +30,71 @@ def batch_set_tags(self, tag_ids: List[int]):
# 2. 批量设置标签
pass

def delete_tags(self, tag_ids: List[int]):
@classmethod
def delete_tags(cls, bk_biz_id: int, ids: List[int]):
"""
删除标签
"""
# 1. 检查标签是否被引用
related_resources = cls.query_related_resources(ids)
for related_resource in related_resources:
if related_resource["count"] > 0:
raise ValidationError(_("标签被引用,无法删除"))

# 2. 批量删除标签
Tag.objects.filter(bk_biz_id=bk_biz_id, id__in=ids).delete()

def create_tags(self, tags: List[Dict[str, str]]):
@classmethod
def query_related_resources(cls, ids: List[int], resource_type: str = None):
"""
查询关联资源
"""
# 1. 查询外键关联资源
data = []
for tag_id in ids:
info = {"id": tag_id, "related_resources": []}
for field in Tag._meta.get_fields():
if isinstance(field, ManyToManyRel) and (field.name == resource_type or resource_type is None):
related_objs = field.related_model.objects.prefetch_related("tags").filter(tags__id=tag_id)
info["related_resources"].append(
{
"resource_type": field.name,
"count": related_objs.count(),
}
)

# 2. 查询第三方服务关联资源(如资源池、后续可能扩展的别的服务)
if resource_type == TagResourceType.DB_RESOURCE.value or resource_type is None:
info["related_resources"].append(
{
"resource_type": TagResourceType.DB_RESOURCE.value,
# TODO 请求资源池接口得到统计数量
"count": 0,
}
)
data.append(info)
return data

@classmethod
def batch_create(cls, bk_biz_id: int, tags: List[Dict[str, str]], creator: str = ""):
"""
批量创建标签
"""
duplicate_tags = cls.verify_duplicated(bk_biz_id, tags)
if duplicate_tags:
raise ValidationError(_("检查到重复的标签"), data=duplicate_tags)

tag_models = [Tag(bk_biz_id=bk_biz_id, key=tag["key"], value=tag["value"], creator=creator) for tag in tags]
Tag.objects.bulk_create(tag_models)

def list_tags(self):
@classmethod
def verify_duplicated(cls, bk_biz_id: int, tags: List[Dict[str, str]]) -> List[Dict[str, str]]:
"""
标签列表
检查标签是否重复
"""
biz_tags = [f"{tag.key}:{tag.value}" for tag in Tag.objects.filter(bk_biz_id=bk_biz_id)]
duplicate_tags = []
for tag in tags:
if f'{tag["key"]}:{tag["value"]}' in biz_tags:
duplicate_tags.append(tag)
return duplicate_tags
31 changes: 31 additions & 0 deletions dbm-ui/backend/db_services/tag/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,43 @@
specific language governing permissions and limitations under the License.
"""

from django.utils.translation import gettext_lazy as _
from rest_framework import serializers

from backend.bk_web.serializers import AuditedSerializer
from backend.db_meta.models import Tag


class TagSerializer(AuditedSerializer, serializers.ModelSerializer):
"""
标签序列化器
"""

class Meta:
model = Tag
fields = "__all__"


class BatchCreateTagsSerializer(serializers.Serializer):
class CreateTagSerializer(serializers.Serializer):
key = serializers.CharField(help_text=_("标签key"))
value = serializers.CharField(help_text=_("标签value"))

bk_biz_id = serializers.IntegerField(help_text=_("业务ID"))
tags = serializers.ListField(child=CreateTagSerializer())


class UpdateTagSerializer(serializers.Serializer):
bk_biz_id = serializers.IntegerField(help_text=_("业务ID"))
id = serializers.IntegerField(help_text=_("标签 ID"))
value = serializers.CharField(help_text=_("标签value"))


class DeleteTagsSerializer(serializers.Serializer):
bk_biz_id = serializers.IntegerField(help_text=_("业务ID"))
ids = serializers.ListSerializer(child=serializers.IntegerField(help_text=_("标签 ID")), help_text=_("标签 ID 列表"))


class QueryRelatedResourceSerializer(serializers.Serializer):
ids = serializers.ListSerializer(child=serializers.IntegerField(help_text=_("标签 ID")), help_text=_("标签 ID 列表"))
resource_type = serializers.CharField(help_text=_("资源类型"), required=False)
74 changes: 69 additions & 5 deletions dbm-ui/backend/db_services/tag/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,84 @@
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.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters
from rest_framework.decorators import action
from rest_framework.response import Response

from backend.bk_web import viewsets
from backend.bk_web.swagger import common_swagger_auto_schema
from backend.bk_web.viewsets import AuditedModelViewSet
from backend.db_meta.models import Tag
from backend.db_services.group.serializers import TagSerializer
from backend.db_services.tag import serializers
from backend.db_services.tag.handlers import TagHandler

SWAGGER_TAG = _("标签")


class TagViewSet(viewsets.AuditedModelViewSet):
@method_decorator(
name="partial_update",
decorator=common_swagger_auto_schema(
operation_summary=_("更新标签"), tags=[SWAGGER_TAG], request_body=serializers.UpdateTagSerializer()
),
)
@method_decorator(
name="list",
decorator=common_swagger_auto_schema(operation_summary=_("查询标签列表"), tags=[SWAGGER_TAG]),
)
class TagViewSet(AuditedModelViewSet):
"""
标签视图
"""

queryset = Tag.objects.all()
serializer_class = TagSerializer
serializer_class = serializers.TagSerializer
filter_backends = [filters.SearchFilter, DjangoFilterBackend]
filter_fields = ("bk_biz_id", "key", "value", "type")

@common_swagger_auto_schema(
operation_summary=_("查询标签关联资源"), request_body=serializers.QueryRelatedResourceSerializer(), tags=[SWAGGER_TAG]
)
@action(methods=["POST"], detail=False, serializer_class=serializers.QueryRelatedResourceSerializer)
def related_resources(self, request, *args, **kwargs):
"""
查询标签关联资源
"""
validated_data = self.params_validate(self.get_serializer_class())
return Response(TagHandler.query_related_resources(validated_data["ids"], validated_data.get("resource_type")))

@common_swagger_auto_schema(
operation_summary=_("批量创建标签"), request_body=serializers.BatchCreateTagsSerializer(), tags=[SWAGGER_TAG]
)
@action(methods=["POST"], detail=False, serializer_class=serializers.BatchCreateTagsSerializer)
def batch_create(self, request, *args, **kwargs):
"""
创建标签
"""
validated_data = self.params_validate(self.get_serializer_class())
return Response(
TagHandler.batch_create(validated_data["bk_biz_id"], validated_data["tags"], request.user.username)
)

@common_swagger_auto_schema(
operation_summary=_("批量删除标签"), request_body=serializers.DeleteTagsSerializer(), tags=[SWAGGER_TAG]
)
@action(methods=["DELETE"], detail=False, serializer_class=serializers.DeleteTagsSerializer)
def batch_delete(self, request, *args, **kwargs):
"""
删除标签
"""
validated_data = self.params_validate(self.get_serializer_class())
return Response(TagHandler.delete_tags(validated_data["bk_biz_id"], validated_data["ids"]))

@common_swagger_auto_schema(
operation_summary=_("校验标签是否重复"), request_body=serializers.BatchCreateTagsSerializer(), tags=[SWAGGER_TAG]
)
@action(methods=["POST"], detail=False, serializer_class=serializers.BatchCreateTagsSerializer)
def verify_duplicated(self, request, *args, **kwargs):
"""
校验
"""
validated_data = self.params_validate(self.get_serializer_class())
return Response(TagHandler.verify_duplicated(validated_data["bk_biz_id"], validated_data["tags"]))

0 comments on commit 53c498c

Please sign in to comment.