diff --git a/config/default.py b/config/default.py index ba5c58e90a..be7bbd0adb 100644 --- a/config/default.py +++ b/config/default.py @@ -67,6 +67,7 @@ "gcloud.contrib.develop", "gcloud.contrib.collection", "gcloud.contrib.operate_record", + "gcloud.contrib.template_market", "gcloud.apigw", "gcloud.common_template", "gcloud.label", @@ -883,3 +884,9 @@ def check_engine_admin_permission(request, *args, **kwargs): if "BKAPP_SOPS_BROKER_URL" in os.environ: BROKER_URL = os.getenv("BKAPP_SOPS_BROKER_URL") print(f"BROKER_URL: {BROKER_URL}") + + +# 流程商店 +ENABLE_TEMPLATE_MARKET = env.ENABLE_TEMPLATE_MARKET +# 流程商店 API 地址 +TEMPLATE_MARKET_API_URL = env.TEMPLATE_MARKET_API_URL diff --git a/config/urls_custom.py b/config/urls_custom.py index 6ed3c5ecef..8c19756ae8 100644 --- a/config/urls_custom.py +++ b/config/urls_custom.py @@ -10,12 +10,11 @@ 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 rest_framework import permissions -from drf_yasg.views import get_schema_view -from drf_yasg import openapi -from django.conf.urls import include, url from django.conf import settings - +from django.conf.urls import include, url +from drf_yasg import openapi +from drf_yasg.views import get_schema_view +from rest_framework import permissions # 用户自定义 urlconf urlpatterns_custom = [ @@ -41,6 +40,7 @@ url(r"^plugin_service/", include("plugin_service.urls")), url(r"^mako_operations/", include("gcloud.mako_template_helper.urls")), url(r"^engine_admin/", include("pipeline.contrib.engine_admin.urls")), + url(r"^template_market/", include("gcloud.contrib.template_market.urls")), ] schema_view = get_schema_view( diff --git a/env.py b/env.py index 59c9045d14..2a4e1546ed 100644 --- a/env.py +++ b/env.py @@ -153,3 +153,8 @@ # bk_audit BK_AUDIT_ENDPOINT = os.getenv("BK_AUDIT_ENDPOINT", None) BK_AUDIT_DATA_TOKEN = os.getenv("BK_AUDIT_DATA_TOKEN", None) + +# 流程商店 +ENABLE_TEMPLATE_MARKET = False if os.getenv("ENABLE_TEMPLATE_MARKET") is None else True +# 流程商店 API 地址 +TEMPLATE_MARKET_API_URL = os.getenv("TEMPLATE_MARKET_API_URL", "") diff --git a/gcloud/apigw/management/commands/data/api-resources.yml b/gcloud/apigw/management/commands/data/api-resources.yml index de54d6e544..0f634b08de 100644 --- a/gcloud/apigw/management/commands/data/api-resources.yml +++ b/gcloud/apigw/management/commands/data/api-resources.yml @@ -757,3 +757,27 @@ paths: authConfig: userVerifiedRequired: true disabledStages: [] + /copy_template_across_project/{project_id}/: + post: + operationId: copy_template_across_project + description: 跨业务复制模板 + tags: + - 限制接口 + responses: + default: + description: '' + x-bk-apigateway-resource: + isPublic: false + allowApplyPermission: false + matchSubpath: false + backend: + type: HTTP + method: post + path: /{env.api_sub_path}apigw/copy_template_across_project/{project_id>}/ + matchSubpath: false + timeout: 0 + upstreams: {} + transformHeaders: {} + authConfig: + userVerifiedRequired: true + disabledStages: [] \ No newline at end of file diff --git a/gcloud/apigw/urls.py b/gcloud/apigw/urls.py index a4f4751328..51a09e31b4 100644 --- a/gcloud/apigw/urls.py +++ b/gcloud/apigw/urls.py @@ -55,6 +55,7 @@ from gcloud.apigw.views.register_project import register_project from gcloud.apigw.views.set_periodic_task_enabled import set_periodic_task_enabled from gcloud.apigw.views.start_task import start_task +from gcloud.apigw.views.copy_template_across_project import copy_template_across_project urlpatterns = [ url(r"^dispatch_plugin_query/$", dispatch_plugin_query), @@ -129,4 +130,5 @@ url(r"^create_clocked_task/(?P\d+)/(?P\d+)/$", create_clocked_task), url(r"^get_mini_app_list/(?P\d+)/$", get_mini_app_list), url(r"^get_task_count/(?P\d+)/$", get_task_count), + url(r"^copy_template_across_project/(?P\d+)/$", copy_template_across_project), ] diff --git a/gcloud/apigw/validators/copy_template_across_project.py b/gcloud/apigw/validators/copy_template_across_project.py new file mode 100644 index 0000000000..53d6094773 --- /dev/null +++ b/gcloud/apigw/validators/copy_template_across_project.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import json + +from gcloud.utils.validate import ObjectJsonBodyValidator + + +class CopyTemplateAcrossProjectValidator(ObjectJsonBodyValidator): + def validate(self, request, *args, **kwargs): + valid, err = super().validate(request, *args, **kwargs) + + if not valid: + return valid, err + + data = json.loads(request.body) + if not data.get("new_project_id") or not data.get("template_id"): + return False, "new_project_id and template_id are required" + + return True, "" diff --git a/gcloud/apigw/views/copy_template_across_project.py b/gcloud/apigw/views/copy_template_across_project.py new file mode 100644 index 0000000000..ad8f411e90 --- /dev/null +++ b/gcloud/apigw/views/copy_template_across_project.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import logging +import json +from apigw_manager.apigw.decorators import apigw_require +from blueapps.account.decorators import login_exempt +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_POST + +from gcloud import err_code +from gcloud.apigw.decorators import ( + mark_request_whether_is_trust, + project_inject, + return_json_response, +) + +from gcloud.apigw.views.utils import logger +from gcloud.contrib.template_market.models import TemplateSharedRecord +from gcloud.tasktmpl3.models import TaskTemplate +from gcloud.template_base.utils import format_import_result_to_response_data +from gcloud.utils.decorators import request_validate +from gcloud.apigw.validators.copy_template_across_project import CopyTemplateAcrossProjectValidator + + +@login_exempt +@csrf_exempt +@require_POST +@apigw_require +@return_json_response +@project_inject +@request_validate(CopyTemplateAcrossProjectValidator) +@mark_request_whether_is_trust +def copy_template_across_project(request, project_id): + if not request.is_trust: + return { + "result": False, + "message": "you have no permission to call this api.", + "code": err_code.REQUEST_FORBIDDEN_INVALID.code, + } + + params_data = json.loads(request.body) + new_project_id = params_data["new_project_id"] + template_id = params_data["template_id"] + + record = TemplateSharedRecord.objects.filter(project_id=request.project.id, template_id=template_id).first() + if record is None: + logging.warning("The specified template could not be found") + return { + "result": False, + "message": "The specified template could not be found", + "code": err_code.REQUEST_FORBIDDEN_INVALID.code, + } + + try: + export_data = TaskTemplate.objects.export_templates([template_id], is_full=False, project_id=request.project.id) + import_result = TaskTemplate.objects.import_templates( + template_data=export_data, + override=False, + project_id=new_project_id, + operator=request.user.username, + ) + except Exception as e: + logger.exception("The template fails to be copied across project: {}".format(e)) + return { + "result": False, + "message": "invalid flow data or error occur, please contact administrator", + "code": err_code.UNKNOWN_ERROR.code, + } + + return format_import_result_to_response_data(import_result) diff --git a/gcloud/contrib/template_market/__init__.py b/gcloud/contrib/template_market/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gcloud/contrib/template_market/admin.py b/gcloud/contrib/template_market/admin.py new file mode 100644 index 0000000000..8c415c5404 --- /dev/null +++ b/gcloud/contrib/template_market/admin.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" + +from django.contrib import admin + +from gcloud.contrib.template_market import models + + +@admin.register(models.TemplateSharedRecord) +class TemplateSharedRecordAdmin(admin.ModelAdmin): + list_display = ["project_id", "template_id", "creator", "create_at", "update_at", "extra_info"] + list_filter = ["project_id", "template_id", "creator", "create_at", "update_at"] + search_fields = ["project_id", "creator"] diff --git a/gcloud/contrib/template_market/apps.py b/gcloud/contrib/template_market/apps.py new file mode 100644 index 0000000000..891af520fc --- /dev/null +++ b/gcloud/contrib/template_market/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TemplatemakerConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "gcloud.contrib.template_market" diff --git a/gcloud/contrib/template_market/clients.py b/gcloud/contrib/template_market/clients.py new file mode 100644 index 0000000000..8b448e3273 --- /dev/null +++ b/gcloud/contrib/template_market/clients.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" + +import requests + +from gcloud.conf import settings + + +class MarketAPIClient: + def __init__(self): + self.base_url = settings.TEMPLATE_MARKET_API_URL + + def _get_url(self, endpoint): + return f"{self.base_url}{endpoint}" + + def get_shared_detail(self, market_record_id): + url = self._get_url(f"/sre_scene/flow_template_scene/{market_record_id}/") + response = requests.get(url) + return response.json() + + def get_shared_list(self): + url = self._get_url("/sre_scene/flow_template_scene/?is_all=true") + response = requests.get(url) + return response.json() + + def create_shared_record(self, data): + url = self._get_url("/sre_scene/flow_template_scene/") + response = requests.post(url, json=data) + return response.json() + + def patch_shared_record(self, data, market_record_id): + url = self._get_url(f"/sre_scene/flow_template_scene/{market_record_id}/") + response = requests.patch(url, json=data) + return response.json() diff --git a/gcloud/contrib/template_market/migrations/0001_initial.py b/gcloud/contrib/template_market/migrations/0001_initial.py new file mode 100644 index 0000000000..1a2ab4fc8a --- /dev/null +++ b/gcloud/contrib/template_market/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.15 on 2024-12-12 12:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="TemplateSharedRecord", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("project_id", models.IntegerField(default=-1, help_text="项目 ID", verbose_name="项目 ID")), + ("template_id", models.JSONField(db_index=True, help_text="模板 ID 列表", verbose_name="模板 ID 列表")), + ("creator", models.CharField(default="", max_length=32, verbose_name="创建者")), + ("create_at", models.DateTimeField(auto_now_add=True, verbose_name="创建时间")), + ("update_at", models.DateTimeField(auto_now=True, verbose_name="更新时间")), + ("extra_info", models.JSONField(blank=True, null=True, verbose_name="额外信息")), + ], + options={ + "verbose_name": "模板共享记录 TemplateSharedRecord", + "verbose_name_plural": "模板共享记录 TemplateSharedRecord", + }, + ), + ] diff --git a/gcloud/contrib/template_market/migrations/__init__.py b/gcloud/contrib/template_market/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gcloud/contrib/template_market/models.py b/gcloud/contrib/template_market/models.py new file mode 100644 index 0000000000..04d02eae07 --- /dev/null +++ b/gcloud/contrib/template_market/models.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" + +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from gcloud import err_code + + +class TemplateSharedManager(models.Manager): + def update_shared_record(self, new_template_ids, market_record_id, project_id, creator, existing_template_ids=None): + market_record_id = int(market_record_id) + + if existing_template_ids: + templates_to_remove = existing_template_ids - set(new_template_ids) + if templates_to_remove: + for template_id in templates_to_remove: + current_template_record = TemplateSharedRecord.objects.get(template_id=template_id) + current_market_ids = current_template_record.extra_info.get("market_record_ids", []) + if market_record_id in current_market_ids: + current_market_ids.remove(market_record_id) + current_template_record.extra_info["market_record_ids"] = current_market_ids + current_template_record.save() + if not current_template_record.extra_info["market_record_ids"]: + current_template_record.delete() + else: + return { + "result": False, + "message": "template {} is not in record {}".format(template_id, market_record_id), + "code": err_code.REQUEST_PARAM_INVALID.code, + } + + templates_to_add = set(new_template_ids) - existing_template_ids + if templates_to_add: + new_template_ids = list(templates_to_add) + + new_records = [] + for template_id in new_template_ids: + existing_record, created = TemplateSharedRecord.objects.get_or_create( + project_id=project_id, + template_id=template_id, + defaults={"creator": creator, "extra_info": {"market_record_ids": [market_record_id]}}, + ) + if not created: + market_ids = existing_record.extra_info.setdefault("market_record_ids", []) + if market_record_id not in market_ids: + market_ids.append(market_record_id) + new_records.append(existing_record) + + if new_records: + TemplateSharedRecord.objects.bulk_update(new_records, ["extra_info"]) + + return {"result": True, "message": "update shared record successfully", "code": err_code.SUCCESS.code} + + +class TemplateSharedRecord(models.Model): + project_id = models.IntegerField(_("项目 ID"), default=-1, help_text="项目 ID") + template_id = models.JSONField(_("模板 ID 列表"), help_text="模板 ID 列表", db_index=True) + creator = models.CharField(_("创建者"), max_length=32, default="") + create_at = models.DateTimeField(_("创建时间"), auto_now_add=True) + update_at = models.DateTimeField(verbose_name=_("更新时间"), auto_now=True) + extra_info = models.JSONField(_("额外信息"), blank=True, null=True) + + objects = TemplateSharedManager() + + class Meta: + verbose_name = _("模板共享记录 TemplateSharedRecord") + verbose_name_plural = _("模板共享记录 TemplateSharedRecord") diff --git a/gcloud/contrib/template_market/permission.py b/gcloud/contrib/template_market/permission.py new file mode 100644 index 0000000000..d2f6847012 --- /dev/null +++ b/gcloud/contrib/template_market/permission.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import logging + +from iam.exceptions import MultiAuthFailedException +from rest_framework import permissions + +from gcloud.conf import settings +from gcloud.contrib.template_market.models import TemplateSharedRecord +from gcloud.contrib.template_market.serializers import TemplateProjectBaseSerializer +from gcloud.iam_auth import IAMMeta +from gcloud.iam_auth.utils import iam_multi_resource_auth_or_raise + + +class TemplatePreviewPermission(permissions.BasePermission): + def has_permission(self, request, view): + serializer = TemplateProjectBaseSerializer(data=request.query_params) + serializer.is_valid(raise_exception=True) + + template_id = int(serializer.validated_data["template_id"]) + project_id = int(serializer.validated_data["project_id"]) + record = TemplateSharedRecord.objects.filter(project_id=project_id, template_id=template_id).first() + if record is None: + logging.warning("The specified template could not be found") + return False + + return True + + +class SharedTemplateRecordPermission(permissions.BasePermission): + def has_permission(self, request, view): + if not settings.ENABLE_TEMPLATE_MARKET: + return False + + if view.action in ["create", "partial_update"]: + username = request.user.username + serializer = view.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + + template_id_list = serializer.validated_data["template_ids"] + try: + iam_multi_resource_auth_or_raise( + username, IAMMeta.FLOW_EDIT_ACTION, template_id_list, "resources_list_for_flows" + ) + except MultiAuthFailedException: + logging.exception("Template permission verification failed") + return False + + return True diff --git a/gcloud/contrib/template_market/serializers.py b/gcloud/contrib/template_market/serializers.py new file mode 100644 index 0000000000..89be644f10 --- /dev/null +++ b/gcloud/contrib/template_market/serializers.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import json + +from rest_framework import serializers + + +class TemplatePreviewSerializer(serializers.Serializer): + name = serializers.CharField(read_only=True, help_text="模板名称") + pipeline_tree = serializers.SerializerMethodField(read_only=True, help_text="pipeline_tree") + + def get_pipeline_tree(self, obj): + # todo 节点信息防护 + return json.dumps(obj.pipeline_tree) + + +class TemplateProjectBaseSerializer(serializers.Serializer): + template_id = serializers.CharField(required=True, help_text="模板id") + project_id = serializers.CharField(required=True, help_text="项目id") + + +class TemplateSharedRecordSerializer(serializers.Serializer): + project_id = serializers.CharField(required=True, max_length=32, help_text="项目id") + template_ids = serializers.ListField(required=True, help_text="关联的模板列表") + creator = serializers.CharField(required=True, max_length=32, help_text="创建者") + extra_info = serializers.JSONField(required=False, allow_null=True, help_text="额外信息") + name = serializers.CharField(required=True, help_text="共享名称") + code = serializers.CharField(required=True, help_text="共享标识") + category = serializers.CharField(required=True, help_text="共享分类") + risk_level = serializers.IntegerField(required=True, help_text="风险级别") + usage_id = serializers.IntegerField(required=True, help_text="使用说明id") + labels = serializers.ListField(child=serializers.IntegerField(), required=True, help_text="共享标签列表") + usage_content = serializers.JSONField(required=True, help_text="使用说明") diff --git a/gcloud/contrib/template_market/urls.py b/gcloud/contrib/template_market/urls.py new file mode 100644 index 0000000000..a85a343124 --- /dev/null +++ b/gcloud/contrib/template_market/urls.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" + +from django.conf.urls import include, url +from rest_framework.routers import DefaultRouter +from gcloud.contrib.template_market.viewsets import TemplatePreviewAPIView, SharedTemplateRecordsViewSet + + +template_market_router = DefaultRouter() +template_market_router.register(r"shared_templates_records", SharedTemplateRecordsViewSet) + +urlpatterns = [ + url(r"^api/", include(template_market_router.urls)), + url(r"^api/template_preview/$", TemplatePreviewAPIView.as_view()), +] diff --git a/gcloud/contrib/template_market/viewsets.py b/gcloud/contrib/template_market/viewsets.py new file mode 100644 index 0000000000..b536ef5552 --- /dev/null +++ b/gcloud/contrib/template_market/viewsets.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import json +import logging + +from rest_framework import viewsets +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import permissions + +from gcloud import err_code +from gcloud.conf import settings +from drf_yasg.utils import swagger_auto_schema +from gcloud.contrib.template_market.serializers import ( + TemplateSharedRecordSerializer, + TemplatePreviewSerializer, + TemplateProjectBaseSerializer, +) +from gcloud.contrib.template_market.models import TemplateSharedRecord +from gcloud.taskflow3.models import TaskTemplate +from gcloud.contrib.template_market.clients import MarketAPIClient +from gcloud.contrib.template_market.permission import TemplatePreviewPermission, SharedTemplateRecordPermission + + +class TemplatePreviewAPIView(APIView): + queryset = TaskTemplate.objects.filter(pipeline_template__isnull=False, is_deleted=False) + serializer_class = TemplatePreviewSerializer + permission_classes = [permissions.IsAuthenticated, TemplatePreviewPermission] + + def get(self, request, *args, **kwargs): + request_serializer = TemplateProjectBaseSerializer(data=request.query_params) + request_serializer.is_valid(raise_exception=True) + + template_id = request_serializer.validated_data["template_id"] + project_id = request_serializer.validated_data["project_id"] + + instance = self.queryset.get(id=template_id, project_id=project_id) + serializer = self.serializer_class(instance) + + return Response({"result": True, "data": serializer.data, "code": err_code.SUCCESS.code}) + + +class SharedTemplateRecordsViewSet(viewsets.ViewSet): + queryset = TemplateSharedRecord.objects.all() + serializer_class = TemplateSharedRecordSerializer + permission_classes = [permissions.IsAuthenticated, SharedTemplateRecordPermission] + + market_client = MarketAPIClient() + + def _build_template_data(self, serializer, **kwargs): + templates = TaskTemplate.objects.filter(id__in=serializer.validated_data["template_ids"], is_deleted=False) + template_info = [{"id": template.id, "name": template.name} for template in templates] + data = { + "name": serializer.validated_data["name"], + "code": serializer.validated_data["code"], + "category": serializer.validated_data["category"], + "risk_level": serializer.validated_data["risk_level"], + "usage_id": serializer.validated_data["usage_id"], + "labels": serializer.validated_data["labels"], + "source_system": settings.APP_CODE, + "project_code": serializer.validated_data["project_id"], + "templates": json.dumps(template_info), + "usage_content": serializer.validated_data["usage_content"], + } + market_record_id = kwargs.get("market_record_id") + if market_record_id: + data["id"] = market_record_id + return data + + def list(self, request, *args, **kwargs): + response_data = self.market_client.get_shared_list() + + if not response_data["result"]: + logging.exception("Failed to obtain the market template list") + return Response( + { + "result": False, + "message": "Failed to obtain the market template list", + "code": err_code.OPERATION_FAIL.code, + } + ) + return Response({"result": True, "data": response_data, "code": err_code.SUCCESS.code}) + + @swagger_auto_schema(request_body=TemplateSharedRecordSerializer) + def create(self, request, *args, **kwargs): + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + + data = self._build_template_data(serializer) + response_data = self.market_client.create_shared_record(data) + if not response_data.get("result"): + return Response( + { + "result": False, + "message": "Failed to create market template record", + "code": err_code.OPERATION_FAIL.code, + } + ) + TemplateSharedRecord.objects.update_shared_record( + project_id=int(serializer.validated_data["project_id"]), + new_template_ids=serializer.validated_data["template_ids"], + market_record_id=response_data["data"]["id"], + creator=serializer.validated_data["creator"], + ) + return Response({"result": True, "data": response_data, "code": err_code.SUCCESS.code}) + + @swagger_auto_schema(request_body=TemplateSharedRecordSerializer) + def partial_update(self, request, *args, **kwargs): + market_record_id = kwargs["pk"] + serializer = self.serializer_class(data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + existing_records = self.market_client.get_shared_detail(market_record_id) + existing_template_ids = set([template["id"] for template in json.loads(existing_records["data"]["templates"])]) + data = self._build_template_data(serializer, market_record_id=market_record_id) + response_data = self.market_client.patch_shared_record(data, market_record_id) + if not response_data.get("result"): + return Response( + { + "result": False, + "message": "Failed to update market template record", + "code": err_code.OPERATION_FAIL.code, + } + ) + TemplateSharedRecord.objects.update_shared_record( + project_id=int(serializer.validated_data["project_id"]), + new_template_ids=serializer.validated_data["template_ids"], + market_record_id=market_record_id, + creator=serializer.validated_data["creator"], + existing_template_ids=existing_template_ids, + ) + return Response({"result": True, "data": response_data, "code": err_code.SUCCESS.code})