From 628952d2e918c253dcb16eb2966d9aa053a17605 Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Tue, 3 Dec 2024 18:26:11 +0800 Subject: [PATCH 01/31] feat: record service instance which is unbound and recycle later --- .../accessories/servicehub/constants.py | 10 +++ .../accessories/servicehub/exceptions.py | 4 ++ .../accessories/servicehub/local/manager.py | 63 ++++++++++++++++-- .../paasng/accessories/servicehub/manager.py | 12 ++++ ...oundserviceengineappattachment_and_more.py | 59 +++++++++++++++++ .../paasng/accessories/servicehub/models.py | 60 ++++++++++++++++- .../accessories/servicehub/remote/manager.py | 66 ++++++++++++++++++- .../accessories/servicehub/serializers.py | 24 +++++++ .../paasng/accessories/servicehub/services.py | 41 +++++++++++- .../paasng/accessories/servicehub/urls.py | 6 ++ .../paasng/accessories/servicehub/views.py | 51 +++++++++++++- 11 files changed, 385 insertions(+), 11 deletions(-) create mode 100644 apiserver/paasng/paasng/accessories/servicehub/migrations/0005_unboundserviceengineappattachment_and_more.py diff --git a/apiserver/paasng/paasng/accessories/servicehub/constants.py b/apiserver/paasng/paasng/accessories/servicehub/constants.py index 3d34c72373..d050b610a1 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/constants.py +++ b/apiserver/paasng/paasng/accessories/servicehub/constants.py @@ -58,3 +58,13 @@ class ServiceBindingType(IntStructuredEnum): NORMAL = 1 SHARING = 2 + + +class ServiceUnboundStatus: + Unbound = 1 + Recycled = 2 + + CHOICES = ( + (Unbound, "Unbound service instance with engine app"), + (Recycled, "Recycled service instance"), + ) diff --git a/apiserver/paasng/paasng/accessories/servicehub/exceptions.py b/apiserver/paasng/paasng/accessories/servicehub/exceptions.py index 57e7603544..a0cd4176ee 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/exceptions.py +++ b/apiserver/paasng/paasng/accessories/servicehub/exceptions.py @@ -44,6 +44,10 @@ class SvcAttachmentDoesNotExist(BaseServicesException): """remote or local service attachment does not exist""" +class UnboundSvcAttachmentDoesNotExist(BaseServicesException): + """unbound remote or local service attachment does not exist""" + + class CanNotModifyPlan(BaseServicesException): """remote or local service attachment already provided""" diff --git a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py index f6e0efa6f1..a99eef549c 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py @@ -15,8 +15,8 @@ # 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. -"""Local services manager -""" +"""Local services manager""" + import json import logging import uuid @@ -31,14 +31,20 @@ from django.utils.translation import gettext_lazy as _ from paasng.accessories.servicehub import constants +from paasng.accessories.servicehub.constants import ServiceUnboundStatus from paasng.accessories.servicehub.exceptions import ( BindServiceNoPlansError, CanNotModifyPlan, ProvisionInstanceError, ServiceObjNotFound, SvcAttachmentDoesNotExist, + UnboundSvcAttachmentDoesNotExist, +) +from paasng.accessories.servicehub.models import ( + ServiceEngineAppAttachment, + ServiceModuleAttachment, + UnboundServiceEngineAppAttachment, ) -from paasng.accessories.servicehub.models import ServiceEngineAppAttachment, ServiceModuleAttachment from paasng.accessories.servicehub.services import ( NOTSET, BasePlanMgr, @@ -50,8 +56,9 @@ ServiceObj, ServiceSpecificationDefinition, ServiceSpecificationHelper, + UnboundEngineAppInstanceRel, ) -from paasng.accessories.services.models import Plan, Service +from paasng.accessories.services.models import Plan, Service, ServiceInstance from paasng.misc.metrics import SERVICE_PROVISION_COUNTER from paasng.platform.applications.models import ModuleEnvironment from paasng.platform.engine.constants import AppEnvName @@ -165,8 +172,21 @@ def provision(self): ).inc() def recycle_resource(self): + if self.db_obj.service.prefer_async_delete: + self.mark_unbound() self.db_obj.clean_service_instance() + def mark_unbound(self): + db_env = ModuleEnvironment.objects.get(engine_app=self.db_obj.engine_app) + UnboundServiceEngineAppAttachment.objects.create( + application=db_env.application, + module=db_env.module, + environment=db_env.environment, + engine_app=self.db_obj.engine_app, + service=self.db_obj.service, + service_instance=self.db_obj.service_instance, + ) + def get_instance(self) -> ServiceInstanceObj: """Get service instance object""" if not self.is_provisioned(): @@ -386,6 +406,17 @@ def get_attachment_by_engine_app(self, service: ServiceObj, engine_app: EngineAp except ServiceEngineAppAttachment.DoesNotExist as e: raise SvcAttachmentDoesNotExist from e + def get_unbound_instance_rel_by_instance_id(self, service: ServiceObj, service_instance_id: uuid.UUID): + try: + instance = UnboundServiceEngineAppAttachment.objects.get( + service_id=service.uuid, + service_instance__uuid=service_instance_id, + status=ServiceUnboundStatus.Unbound, + ) + except UnboundServiceEngineAppAttachment.DoesNotExist as e: + raise UnboundSvcAttachmentDoesNotExist from e + return LocalUnboundEngineAppInstanceRel(instance) + class LocalPlanMgr(BasePlanMgr): """Local in-database plans manager""" @@ -450,6 +481,30 @@ def _handle_plan_data(self, data: Dict) -> Dict: return data +class LocalUnboundEngineAppInstanceRel(UnboundEngineAppInstanceRel): + """A unbound relationship between EngineApp and Provisioned instance""" + + def __init__(self, db_obj: UnboundServiceEngineAppAttachment): + self.db_obj = db_obj + + def is_unbound(self): + return self.db_obj.status == ServiceUnboundStatus.Unbound + + def is_recycled(self): + return not ServiceInstance.objects.filter(uuid=self.db_obj.service_instance_id).exists() + + def recycle_resource(self): + if not self.is_unbound(): + raise UnboundSvcAttachmentDoesNotExist("service instance is not unbound") + + if self.is_recycled(): + self.db_obj.status = constants.ServiceUnboundStatus.Recycled + self.db_obj.save() + return + + self.db_obj.clean_service_instance() + + class LocalPlainInstanceMgr(PlainInstanceMgr): """纯粹的本地增强服务实例的管理器, 不涉及增强服务资源申请的流程""" diff --git a/apiserver/paasng/paasng/accessories/servicehub/manager.py b/apiserver/paasng/paasng/accessories/servicehub/manager.py index 83b9475fdb..45af3937fa 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/manager.py @@ -17,6 +17,7 @@ import logging import operator +import uuid from typing import Callable, Dict, Generator, Iterable, Iterator, List, NamedTuple, Optional, TypeVar, cast from django.db.models import QuerySet @@ -27,6 +28,7 @@ DuplicatedServiceBoundError, ServiceObjNotFound, SvcAttachmentDoesNotExist, + UnboundSvcAttachmentDoesNotExist, ) from paasng.accessories.servicehub.local.manager import LocalPlanMgr, LocalServiceMgr, LocalServiceObj from paasng.accessories.servicehub.models import ( @@ -273,6 +275,16 @@ def get_attachment_by_engine_app(self, service: ServiceObj, engine_app: EngineAp continue raise SvcAttachmentDoesNotExist(f"engine_app<{engine_app}> has no attachment with service<{service.uuid}>") + def get_unbound_instance_rel_by_instance_id(self, service: ServiceObj, service_instance_id: uuid.UUID): + for mgr in self.mgr_instances: + try: + return mgr.get_unbound_instance_rel_by_instance_id(service, service_instance_id) + except UnboundSvcAttachmentDoesNotExist: + continue + raise UnboundSvcAttachmentDoesNotExist( + f"service<{ServiceObj}> has no attachment with service_instance_id<{service_instance_id}>" + ) + class MixedPlanMgr: """A hub for managing plans of mixed sources: database and remote REST plans""" diff --git a/apiserver/paasng/paasng/accessories/servicehub/migrations/0005_unboundserviceengineappattachment_and_more.py b/apiserver/paasng/paasng/accessories/servicehub/migrations/0005_unboundserviceengineappattachment_and_more.py new file mode 100644 index 0000000000..60b20c8389 --- /dev/null +++ b/apiserver/paasng/paasng/accessories/servicehub/migrations/0005_unboundserviceengineappattachment_and_more.py @@ -0,0 +1,59 @@ +# Generated by Django 4.2.16 on 2024-12-03 10:04 + +from django.db import migrations, models +import django.db.models.deletion +import paasng.utils.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('applications', '0013_applicationdeploymentmoduleorder'), + ('modules', '0016_auto_20240904_1439'), + ('engine', '0022_builtinconfigvar'), + ('services', '0006_alter_servicecategory_name_en'), + ('servicehub', '0004_auto_20240412_1723'), + ] + + operations = [ + migrations.CreateModel( + name='UnboundServiceEngineAppAttachment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('region', models.CharField(help_text='部署区域', max_length=32)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('owner', paasng.utils.models.BkUserField(blank=True, db_index=True, max_length=64, null=True)), + ('environment', models.CharField(max_length=16, verbose_name='部署环境')), + ('status', models.IntegerField(choices=[(1, 'Unbound service instance with engine app'), (2, 'Recycled service instance')], default=1, help_text='1 已解绑; 2 已回收;', verbose_name='解绑状态')), + ('application', models.ForeignKey(db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to='applications.application', verbose_name='蓝鲸应用')), + ('engine_app', models.ForeignKey(db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to='engine.engineapp', verbose_name='蓝鲸引擎应用')), + ('module', models.ForeignKey(db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to='modules.module', verbose_name='蓝鲸应用模块')), + ('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='services.service', verbose_name='增强服务')), + ('service_instance', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='services.serviceinstance', verbose_name='增强服务实例')), + ], + options={ + 'verbose_name': '本地已解绑增强服务', + }, + ), + migrations.CreateModel( + name='UnboundRemoteServiceEngineAppAttachment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('region', models.CharField(help_text='部署区域', max_length=32)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('owner', paasng.utils.models.BkUserField(blank=True, db_index=True, max_length=64, null=True)), + ('environment', models.CharField(max_length=16, verbose_name='部署环境')), + ('service_id', models.UUIDField(verbose_name='远程增强服务 ID')), + ('service_instance_id', models.UUIDField(null=True, verbose_name='远程增强服务实例 ID')), + ('status', models.IntegerField(choices=[(1, 'Unbound service instance with engine app'), (2, 'Recycled service instance')], default=1, help_text='1 已解绑; 2 已回收;', verbose_name='解绑状态')), + ('application', models.ForeignKey(db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to='applications.application', verbose_name='蓝鲸应用')), + ('engine_app', models.ForeignKey(db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to='engine.engineapp', verbose_name='蓝鲸引擎应用')), + ('module', models.ForeignKey(db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to='modules.module', verbose_name='蓝鲸应用模块')), + ], + options={ + 'verbose_name': '远程已解绑增强服务', + }, + ), + ] diff --git a/apiserver/paasng/paasng/accessories/servicehub/models.py b/apiserver/paasng/paasng/accessories/servicehub/models.py index cfd2a83282..5e16a079fb 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/models.py +++ b/apiserver/paasng/paasng/accessories/servicehub/models.py @@ -22,7 +22,7 @@ from django.db import models -from paasng.accessories.servicehub.constants import ServiceType +from paasng.accessories.servicehub.constants import ServiceType, ServiceUnboundStatus from paasng.accessories.servicehub.services import ServiceObj from paasng.accessories.services.models import Plan, Service, ServiceInstance from paasng.platform.applications.models import ApplicationEnvironment @@ -123,6 +123,38 @@ def __str__(self): return "{prefix}-no-provision".format(prefix=prefix) +class UnboundServiceEngineAppAttachment(OwnerTimestampedModel): + """Local service instance which is unbound with engine app""" + + application = models.ForeignKey( + "applications.Application", on_delete=models.SET_NULL, null=True, db_constraint=False, verbose_name="蓝鲸应用" + ) + module = models.ForeignKey( + "modules.Module", on_delete=models.SET_NULL, null=True, db_constraint=False, verbose_name="蓝鲸应用模块" + ) + environment = models.CharField(verbose_name="部署环境", max_length=16) + engine_app = models.ForeignKey( + "engine.EngineApp", on_delete=models.SET_NULL, null=True, db_constraint=False, verbose_name="蓝鲸引擎应用" + ) + service = models.ForeignKey(Service, on_delete=models.CASCADE, verbose_name="增强服务") + service_instance = models.ForeignKey( + ServiceInstance, on_delete=models.CASCADE, null=True, blank=True, verbose_name="增强服务实例" + ) + status = models.IntegerField( + choices=ServiceUnboundStatus.CHOICES, + default=ServiceUnboundStatus.Unbound, + verbose_name="解绑状态", + help_text="1 已解绑; 2 已回收;", + ) + + class Meta: + verbose_name = "本地已解绑增强服务" + + def clean_service_instance(self): + """回收增强服务资源""" + self.service.delete_service_instance(self.service_instance) + + class RemoteServiceModuleAttachment(OwnerTimestampedModel): """Binding relationship of module <-> remote service""" @@ -151,6 +183,32 @@ class Meta: unique_together = ("service_id", "engine_app") +class UnboundRemoteServiceEngineAppAttachment(OwnerTimestampedModel): + """Remote service instance which is unbound with engine app""" + + application = models.ForeignKey( + "applications.Application", on_delete=models.SET_NULL, null=True, db_constraint=False, verbose_name="蓝鲸应用" + ) + module = models.ForeignKey( + "modules.Module", on_delete=models.SET_NULL, null=True, db_constraint=False, verbose_name="蓝鲸应用模块" + ) + environment = models.CharField(verbose_name="部署环境", max_length=16) + engine_app = models.ForeignKey( + "engine.EngineApp", on_delete=models.SET_NULL, null=True, db_constraint=False, verbose_name="蓝鲸引擎应用" + ) + service_id = models.UUIDField(verbose_name="远程增强服务 ID") + service_instance_id = models.UUIDField(null=True, verbose_name="远程增强服务实例 ID") + status = models.IntegerField( + choices=ServiceUnboundStatus.CHOICES, + default=ServiceUnboundStatus.Unbound, + verbose_name="解绑状态", + help_text="1 已解绑; 2 已回收;", + ) + + class Meta: + verbose_name = "远程已解绑增强服务" + + class ServiceDBProperties: """Storing service related database properties""" diff --git a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py index 4a94add879..8ec8e657e2 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py @@ -32,8 +32,12 @@ from paas_wl.infras.cluster.shim import EnvClusterService from paas_wl.workloads.networking.egress.shim import get_cluster_egress_info from paasng.accessories.servicehub import constants, exceptions -from paasng.accessories.servicehub.exceptions import BindServiceNoPlansError -from paasng.accessories.servicehub.models import RemoteServiceEngineAppAttachment, RemoteServiceModuleAttachment +from paasng.accessories.servicehub.exceptions import BindServiceNoPlansError, UnboundSvcAttachmentDoesNotExist +from paasng.accessories.servicehub.models import ( + RemoteServiceEngineAppAttachment, + RemoteServiceModuleAttachment, + UnboundRemoteServiceEngineAppAttachment, +) from paasng.accessories.servicehub.remote.client import RemoteServiceClient from paasng.accessories.servicehub.remote.collector import RemoteSpecDefinitionUpdateSLZ, refresh_remote_service from paasng.accessories.servicehub.remote.exceptions import ( @@ -55,6 +59,7 @@ ServicePlansHelper, ServiceSpecificationDefinition, ServiceSpecificationHelper, + UnboundEngineAppInstanceRel, ) from paasng.accessories.services.models import ServiceCategory from paasng.core.region.models import get_all_regions @@ -279,6 +284,7 @@ def sync_instance_config(self): def recycle_resource(self): """对于 remote service 我们默认其已经具备了回收的能力""" if self.is_provisioned(): + self.mark_unbound() try: self.remote_client.delete_instance(instance_id=str(self.db_obj.service_instance_id)) except Exception as e: @@ -287,6 +293,16 @@ def recycle_resource(self): self.db_obj.service_instance_id = None self.db_obj.save() + def mark_unbound(self): + UnboundRemoteServiceEngineAppAttachment.objects.create( + application=self.db_application, + module=self.db_module, + environment=self.db_env.environment, + engine_app=self.db_engine_app, + service_id=self.db_obj.service_id, + service_instance_id=self.db_obj.service_instance_id, + ) + def get_instance(self) -> ServiceInstanceObj: """Get service instance object""" if not self.is_provisioned(): @@ -678,6 +694,52 @@ def get_attachment_by_engine_app(self, service: ServiceObj, engine_app: EngineAp except RemoteServiceEngineAppAttachment.DoesNotExist as e: raise exceptions.SvcAttachmentDoesNotExist from e + def get_unbound_instance_rel_by_instance_id(self, service: ServiceObj, service_instance_id: uuid.UUID): + try: + instance = UnboundRemoteServiceEngineAppAttachment.objects.get( + service_id=service.uuid, + service_instance_id=service_instance_id, + status=constants.ServiceUnboundStatus.Unbound, + ) + except UnboundRemoteServiceEngineAppAttachment.DoesNotExist as e: + raise UnboundSvcAttachmentDoesNotExist from e + return RemoteUnboundEngineAppInstanceRel(instance, self.store) + + +class RemoteUnboundEngineAppInstanceRel(UnboundEngineAppInstanceRel): + """A unbound relationship between EngineApp and Provisioned instance""" + + def __init__(self, db_obj: UnboundRemoteServiceEngineAppAttachment, store: RemoteServiceStore): + self.db_obj = db_obj + self.store = store + + # Client components + self.remote_config = self.store.get_source_config(str(self.db_obj.service_id)) + self.remote_client = RemoteServiceClient(self.remote_config) + + def is_unbound(self): + return self.db_obj.status == constants.ServiceUnboundStatus.Unbound + + def is_recycled(self): + instance_data = self.remote_client.retrieve_instance(str(self.db_obj.service_instance_id)) + # TODO: More data validations + return instance_data.get("uuid") != str(self.db_obj.service_instance_id) + + def recycle_resource(self): + if not self.is_unbound(): + raise UnboundSvcAttachmentDoesNotExist("service instance is not unbound") + + if self.is_recycled(): + self.db_obj.status = constants.ServiceUnboundStatus.Recycled + self.db_obj.save() + return + + try: + self.remote_client.delete_instance(instance_id=str(self.db_obj.service_instance_id)) + except Exception as e: + logger.exception("Error occurs during recycling") + raise exceptions.SvcInstanceDeleteError("unable to delete instance") from e + class RemotePlanMgr(BasePlanMgr): """Remote REST plans manager""" diff --git a/apiserver/paasng/paasng/accessories/servicehub/serializers.py b/apiserver/paasng/paasng/accessories/servicehub/serializers.py index 890e3289fc..30dca91795 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/serializers.py +++ b/apiserver/paasng/paasng/accessories/servicehub/serializers.py @@ -23,6 +23,7 @@ from paasng.accessories.servicehub.remote.exceptions import ServiceConfigNotFound from paasng.accessories.servicehub.remote.store import get_remote_store from paasng.accessories.services.models import ServiceCategory +from paasng.platform.modules.models import Module from paasng.platform.modules.serializers import MinimalModuleSLZ @@ -258,3 +259,26 @@ class ServiceEngineAppAttachmentSLZ(serializers.Serializer): class UpdateServiceEngineAppAttachmentSLZ(serializers.Serializer): credentials_enabled = serializers.BooleanField(help_text="是否使用凭证") + + +class UnboundServiceEngineAppAttachmentSLZ(serializers.Serializer): + module_id = serializers.CharField(write_only=True, help_text="模块 id") + module_name = serializers.SerializerMethodField(help_text="模块名") + environment = serializers.CharField(help_text="运行环境") + service = ServiceMinimalSLZ(help_text="增强服务信息") + service_instance_id = serializers.UUIDField(help_text="增强服务实例 id") + + def get_module_name(self, obj): + module_id = obj.get("module_id") + if module_id: + try: + module = Module.objects.get(id=module_id) + except Module.DoesNotExist: + return None + return module.name + return None + + +class RecycleUnboundServiceEngineAppAttachmentSLZ(serializers.Serializer): + service_id = serializers.UUIDField(help_text="增强服务 id", required=True) + service_instance_id = serializers.UUIDField(help_text="增强服务实例 id", required=True) diff --git a/apiserver/paasng/paasng/accessories/servicehub/services.py b/apiserver/paasng/paasng/accessories/servicehub/services.py index b09b6b4e32..c8d399c98e 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/services.py +++ b/apiserver/paasng/paasng/accessories/servicehub/services.py @@ -15,8 +15,8 @@ # 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. -"""The universal services module, handles both services from database and remote REST API -""" +"""The universal services module, handles both services from database and remote REST API""" + import logging import uuid import weakref @@ -200,7 +200,7 @@ def get_plan(self) -> PlanObj: raise NotImplementedError def delete(self): - """include delete rel & real resource recycle""" + """include delete rel, mark_unbound & real resource recycle""" if self.is_provisioned(): self.recycle_resource() logger.info("going to delete remote service attachment from db") @@ -212,6 +212,30 @@ def recycle_resource(self): """Recycle resources but do not unbind the service""" raise NotImplementedError + @abstractmethod + def mark_unbound(self): + """Add to unbound instances, which is unbound with engine app, could be recycled later""" + raise NotImplementedError + + +class UnboundEngineAppInstanceRel(metaclass=ABCMeta): + """A provinsioned instance which is unbound with engine app""" + + db_obj: Any + + @abstractmethod + def is_unbound(self) -> bool: + raise NotImplementedError + + @abstractmethod + def is_recycled(self) -> bool: + raise NotImplementedError + + @abstractmethod + def recycle_resource(self): + """Recycle resources""" + raise NotImplementedError + class PlainInstanceMgr(metaclass=ABCMeta): """纯粹的增强服务实例的管理器, 不涉及增强服务资源申请的流程""" @@ -295,6 +319,10 @@ def find_by_name(self, name: str, region: str) -> ServiceObj: def get_attachment_by_engine_app(self, service: ServiceObj, engine_app: EngineApp): raise NotImplementedError + @abstractmethod + def get_unbound_instance_rel_by_instance_id(self, service: ServiceObj, service_instance_id: uuid.UUID): + raise NotImplementedError + class BasePlanMgr: """Base class for plan manager""" @@ -475,3 +503,10 @@ def fill_protected_specs(self): method = getattr(self, f"fill_spec_{name}", None) if callable(method): method() + + +@dataclass +class BaseUnboundEngineAppInstanceMgr: + @abstractmethod + def list_by_app_code(self, app_code: str) -> Generator[ServiceObj, None, None]: + raise NotImplementedError diff --git a/apiserver/paasng/paasng/accessories/servicehub/urls.py b/apiserver/paasng/paasng/accessories/servicehub/urls.py index b036898d8d..bccea3988d 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/urls.py +++ b/apiserver/paasng/paasng/accessories/servicehub/urls.py @@ -112,6 +112,12 @@ views.ServiceEngineAppAttachmentViewSet.as_view({"get": "list", "put": "update"}), name="api.services.credentials_enabled", ), + # List unbound engine_app attachment instances, and recycle instance + re_path( + r"^api/bkapps/applications/(?P[^/]+)/service/attachment/instances/unbound/$", + views.UnboundServiceEngineAppAttachmentViewSet.as_view({"get": "list", "post": "recycle_instance"}), + name="api.services.attachment.unbound", + ), # Service sharing APIs re_path( make_app_pattern(f"/services/{SERVICE_UUID}/shareable_modules/$", include_envs=False), diff --git a/apiserver/paasng/paasng/accessories/servicehub/views.py b/apiserver/paasng/paasng/accessories/servicehub/views.py index eca117aab5..775ff5388a 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/views.py +++ b/apiserver/paasng/paasng/accessories/servicehub/views.py @@ -31,6 +31,7 @@ from rest_framework.response import Response from paasng.accessories.servicehub import serializers as slzs +from paasng.accessories.servicehub.constants import ServiceUnboundStatus from paasng.accessories.servicehub.exceptions import ( BindServiceNoPlansError, ReferencedAttachmentNotFound, @@ -38,7 +39,11 @@ SharedAttachmentAlreadyExists, ) from paasng.accessories.servicehub.manager import mixed_service_mgr -from paasng.accessories.servicehub.models import ServiceSetGroupByName +from paasng.accessories.servicehub.models import ( + ServiceSetGroupByName, + UnboundRemoteServiceEngineAppAttachment, + UnboundServiceEngineAppAttachment, +) from paasng.accessories.servicehub.remote.manager import ( RemoteServiceInstanceMgr, RemoteServiceMgr, @@ -224,6 +229,7 @@ def retrieve_specs(self, request, code, module_name, service_id): return Response({"results": slz.data}) @app_action_required(AppAction.MANAGE_ADDONS_SERVICES) + @transaction.atomic def unbind(self, request, code, module_name, service_id): """删除一个服务绑定关系""" application = self._get_application_by_code(code) @@ -247,6 +253,7 @@ def unbind(self, request, code, module_name, service_id): raise error_codes.CANNOT_DESTROY_SERVICE.f(f"{e}") module_attachment.delete() + add_app_audit_record( app_code=code, user=request.user.pk, @@ -753,3 +760,45 @@ def update(self, request, code, module_name, service_id): results.append(attachment) return Response(slzs.ServiceEngineAppAttachmentSLZ(results, many=True).data) + + +class UnboundServiceEngineAppAttachmentViewSet(viewsets.ViewSet, ApplicationCodeInPathMixin): + permission_classes = [IsAuthenticated, application_perm_class(AppAction.BASIC_DEVELOP)] + + def list(self, request, code): + """查看已解绑但未回收增强服务""" + app = self.get_application() + remote_intsances = UnboundRemoteServiceEngineAppAttachment.objects.filter( + application=app, status=ServiceUnboundStatus.Unbound + ) + local_instances = UnboundServiceEngineAppAttachment.objects.filter( + application=app, status=ServiceUnboundStatus.Unbound + ) + + results = [] + for instance in remote_intsances + local_instances: + service_obj = mixed_service_mgr.get_or_404(instance.service_id, app.region) + results.append( + { + "module_id": instance.module, + "environment": instance.environment, + "service": service_obj, + "service_instance_id": instance.service_instance_id, + } + ) + + return Response(slzs.UnboundServiceEngineAppAttachmentSLZ(results, many=True).data) + + def recycle_instance(self, request, code): + """回收已解绑增强服务""" + serializer = slzs.RecycleUnboundServiceEngineAppAttachmentSLZ(data=request.data) + serializer.is_valid(raise_exception=True) + data = serializer.validated_data + + service_obj = mixed_service_mgr.get_or_404(data.service_id, self.get_application().region) + unbound_instance = mixed_service_mgr.get_unbound_instance_rel_by_instance_id( + service_obj, data.service_instance_id + ) + unbound_instance.recycle_instance() + + return Response() From 58d7bdfea355a3039fdaf0fd643d678b963e2e8d Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Tue, 3 Dec 2024 21:20:53 +0800 Subject: [PATCH 02/31] fix: retrive remote instance check http status 404 --- .../accessories/servicehub/local/manager.py | 3 +-- .../accessories/servicehub/remote/client.py | 16 ++++++++++++++-- .../accessories/servicehub/remote/manager.py | 17 +++++++++++++---- .../paasng/accessories/servicehub/services.py | 7 ------- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py index a99eef549c..a5cb15f28d 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py @@ -498,8 +498,7 @@ def recycle_resource(self): raise UnboundSvcAttachmentDoesNotExist("service instance is not unbound") if self.is_recycled(): - self.db_obj.status = constants.ServiceUnboundStatus.Recycled - self.db_obj.save() + self.db_obj.delete() return self.db_obj.clean_service_instance() diff --git a/apiserver/paasng/paasng/accessories/servicehub/remote/client.py b/apiserver/paasng/paasng/accessories/servicehub/remote/client.py index dfbaf44b35..112626801d 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/remote/client.py +++ b/apiserver/paasng/paasng/accessories/servicehub/remote/client.py @@ -15,8 +15,8 @@ # 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. -"""Client for remote services -""" +"""Client for remote services""" + import json import logging from contextlib import contextmanager @@ -251,6 +251,18 @@ def delete_instance(self, instance_id: str): self.validate_resp(resp) return + def delete_instance_synchronously(self, instance_id: str): + """Delete a provisioned instance synchronously + + We assume the remote service is already able to recycle resources + """ + url = self.config.delete_instance_url.format(instance_id=instance_id) + + with wrap_request_exc(self): + resp = requests.delete(url, auth=self.auth, timeout=self.REQUEST_DELETE_TIMEOUT) + self.validate_resp(resp) + return + def update_instance_config(self, instance_id: str, config: Dict): """Update an provisioned instance's config diff --git a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py index 8ec8e657e2..16a981f702 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py @@ -42,6 +42,7 @@ from paasng.accessories.servicehub.remote.collector import RemoteSpecDefinitionUpdateSLZ, refresh_remote_service from paasng.accessories.servicehub.remote.exceptions import ( GetClusterEgressInfoError, + RClientResponseError, ServiceNotFound, UnsupportedOperationError, ) @@ -721,9 +722,15 @@ def is_unbound(self): return self.db_obj.status == constants.ServiceUnboundStatus.Unbound def is_recycled(self): - instance_data = self.remote_client.retrieve_instance(str(self.db_obj.service_instance_id)) - # TODO: More data validations - return instance_data.get("uuid") != str(self.db_obj.service_instance_id) + try: + self.remote_client.retrieve_instance(str(self.db_obj.service_instance_id)) + except RClientResponseError as e: + # if not find service instance with this id, remote response http status code 404 + if e.status_code == 404: + return True + raise + + return False def recycle_resource(self): if not self.is_unbound(): @@ -735,7 +742,9 @@ def recycle_resource(self): return try: - self.remote_client.delete_instance(instance_id=str(self.db_obj.service_instance_id)) + self.remote_client.delete_instance_synchronously(instance_id=str(self.db_obj.service_instance_id)) + self.db_obj.status = constants.ServiceUnboundStatus.Recycled + self.db_obj.save() except Exception as e: logger.exception("Error occurs during recycling") raise exceptions.SvcInstanceDeleteError("unable to delete instance") from e diff --git a/apiserver/paasng/paasng/accessories/servicehub/services.py b/apiserver/paasng/paasng/accessories/servicehub/services.py index c8d399c98e..a43790dfd5 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/services.py +++ b/apiserver/paasng/paasng/accessories/servicehub/services.py @@ -503,10 +503,3 @@ def fill_protected_specs(self): method = getattr(self, f"fill_spec_{name}", None) if callable(method): method() - - -@dataclass -class BaseUnboundEngineAppInstanceMgr: - @abstractmethod - def list_by_app_code(self, app_code: str) -> Generator[ServiceObj, None, None]: - raise NotImplementedError From d16b36d2a80a2ddca86ba47d95dd3a31b85e6a34 Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Wed, 4 Dec 2024 17:20:02 +0800 Subject: [PATCH 03/31] fix: mark unbound if prefer_async_delete --- .../paasng/paasng/accessories/servicehub/remote/manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py index 16a981f702..0682ccaaf7 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py @@ -285,12 +285,13 @@ def sync_instance_config(self): def recycle_resource(self): """对于 remote service 我们默认其已经具备了回收的能力""" if self.is_provisioned(): - self.mark_unbound() try: self.remote_client.delete_instance(instance_id=str(self.db_obj.service_instance_id)) except Exception as e: logger.exception("Error occurs during recycling") raise exceptions.SvcInstanceDeleteError("unable to delete instance") from e + if not self.db_obj.prefer_async_delete: + self.mark_unbound() self.db_obj.service_instance_id = None self.db_obj.save() @@ -729,7 +730,6 @@ def is_recycled(self): if e.status_code == 404: return True raise - return False def recycle_resource(self): From 0d2b57e60a21870832505f3cb0890c43a1dbfc20 Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Wed, 11 Dec 2024 15:06:23 +0800 Subject: [PATCH 04/31] fix: use mix_service_mgr to get unbound instance list --- .../accessories/servicehub/constants.py | 10 -- .../accessories/servicehub/local/manager.py | 84 ++++++---- .../paasng/accessories/servicehub/manager.py | 10 +- .../paasng/accessories/servicehub/models.py | 42 ++--- .../accessories/servicehub/remote/manager.py | 146 ++++++++++++------ .../accessories/servicehub/serializers.py | 25 +-- .../paasng/accessories/servicehub/services.py | 16 +- .../paasng/accessories/servicehub/urls.py | 18 ++- .../paasng/accessories/servicehub/views.py | 98 ++++++++---- 9 files changed, 273 insertions(+), 176 deletions(-) diff --git a/apiserver/paasng/paasng/accessories/servicehub/constants.py b/apiserver/paasng/paasng/accessories/servicehub/constants.py index d050b610a1..3d34c72373 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/constants.py +++ b/apiserver/paasng/paasng/accessories/servicehub/constants.py @@ -58,13 +58,3 @@ class ServiceBindingType(IntStructuredEnum): NORMAL = 1 SHARING = 2 - - -class ServiceUnboundStatus: - Unbound = 1 - Recycled = 2 - - CHOICES = ( - (Unbound, "Unbound service instance with engine app"), - (Recycled, "Recycled service instance"), - ) diff --git a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py index a5cb15f28d..cf24272811 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py @@ -31,7 +31,6 @@ from django.utils.translation import gettext_lazy as _ from paasng.accessories.servicehub import constants -from paasng.accessories.servicehub.constants import ServiceUnboundStatus from paasng.accessories.servicehub.exceptions import ( BindServiceNoPlansError, CanNotModifyPlan, @@ -177,14 +176,12 @@ def recycle_resource(self): self.db_obj.clean_service_instance() def mark_unbound(self): - db_env = ModuleEnvironment.objects.get(engine_app=self.db_obj.engine_app) UnboundServiceEngineAppAttachment.objects.create( - application=db_env.application, - module=db_env.module, - environment=db_env.environment, engine_app=self.db_obj.engine_app, service=self.db_obj.service, + plan=self.db_obj.plan, service_instance=self.db_obj.service_instance, + credentials_enabled=self.db_obj.credentials_enabled, ) def get_instance(self) -> ServiceInstanceObj: @@ -210,6 +207,47 @@ def get_plan(self) -> LocalPlanObj: return LocalPlanObj.from_db(self.db_obj.plan) +class UnboundLocalEngineAppInstanceRel(UnboundEngineAppInstanceRel): + """A unbound relationship between EngineApp and Provisioned instance""" + + def __init__(self, db_obj: UnboundServiceEngineAppAttachment): + self.db_obj = db_obj + + def get_service(self) -> LocalServiceObj: + return LocalServiceObj.from_db_object(self.db_obj.service) + + def is_recycled(self) -> bool: + if not ServiceInstance.objects.filter(uuid=self.db_obj.service_instance).exists(): + self.db_obj.delete() + return True + return False + + def recycle_resource(self) -> None: + if self.is_recycled(): + return + + self.db_obj.clean_service_instance() + + def get_instance(self) -> ServiceInstanceObj: + """Get service instance object""" + # All local service instance's credentials was prefixed with service name + service_name = self.db_obj.service.name + should_hidden_fields = constants.SERVICE_HIDDEN_FIELDS.get(service_name, []) + should_remove_fields = constants.SERVICE_SENSITIVE_FIELDS.get(service_name, []) + + return ServiceInstanceObj( + uuid=str(self.db_obj.service_instance), + credentials=json.loads(self.db_obj.service_instance.credentials), + config=self.db_obj.service_instance.config, + should_hidden_fields=should_hidden_fields, + should_remove_fields=should_remove_fields, + create_time=self.db_obj.created, + ) + + def get_plan(self) -> LocalPlanObj: + return LocalPlanObj.from_db(self.db_obj.plan) + + class LocalServiceMgr(BaseServiceMgr): """Local in-database services manager""" @@ -358,6 +396,16 @@ def list_provisioned_rels( for attachment in qs: yield self.transform_rel_db_obj(attachment) + def list_unbound_instance_rels( + self, engine_app: EngineApp, service: Optional[ServiceObj] = None + ) -> Generator[UnboundEngineAppInstanceRel, None, None]: + """Return all unbound engine_app <-> local service instances by specified service (None for all)""" + qs = engine_app.service_attachment.exclude(service_instance__isnull=True) + if service: + qs = qs.filter(service_id=service.uuid) + for attachment in qs: + yield UnboundLocalEngineAppInstanceRel(attachment) + def get_attachment_by_instance_id(self, service: ServiceObj, service_instance_id: uuid.UUID): try: return ServiceEngineAppAttachment.objects.get( @@ -411,11 +459,10 @@ def get_unbound_instance_rel_by_instance_id(self, service: ServiceObj, service_i instance = UnboundServiceEngineAppAttachment.objects.get( service_id=service.uuid, service_instance__uuid=service_instance_id, - status=ServiceUnboundStatus.Unbound, ) except UnboundServiceEngineAppAttachment.DoesNotExist as e: raise UnboundSvcAttachmentDoesNotExist from e - return LocalUnboundEngineAppInstanceRel(instance) + return UnboundLocalEngineAppInstanceRel(instance) class LocalPlanMgr(BasePlanMgr): @@ -481,29 +528,6 @@ def _handle_plan_data(self, data: Dict) -> Dict: return data -class LocalUnboundEngineAppInstanceRel(UnboundEngineAppInstanceRel): - """A unbound relationship between EngineApp and Provisioned instance""" - - def __init__(self, db_obj: UnboundServiceEngineAppAttachment): - self.db_obj = db_obj - - def is_unbound(self): - return self.db_obj.status == ServiceUnboundStatus.Unbound - - def is_recycled(self): - return not ServiceInstance.objects.filter(uuid=self.db_obj.service_instance_id).exists() - - def recycle_resource(self): - if not self.is_unbound(): - raise UnboundSvcAttachmentDoesNotExist("service instance is not unbound") - - if self.is_recycled(): - self.db_obj.delete() - return - - self.db_obj.clean_service_instance() - - class LocalPlainInstanceMgr(PlainInstanceMgr): """纯粹的本地增强服务实例的管理器, 不涉及增强服务资源申请的流程""" diff --git a/apiserver/paasng/paasng/accessories/servicehub/manager.py b/apiserver/paasng/paasng/accessories/servicehub/manager.py index 45af3937fa..e2fb2c539a 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/manager.py @@ -38,7 +38,12 @@ ) from paasng.accessories.servicehub.remote.manager import RemotePlanMgr, RemoteServiceMgr, RemoteServiceObj from paasng.accessories.servicehub.remote.store import get_remote_store -from paasng.accessories.servicehub.services import EngineAppInstanceRel, PlanObj, ServiceObj +from paasng.accessories.servicehub.services import ( + EngineAppInstanceRel, + PlanObj, + ServiceObj, + UnboundEngineAppInstanceRel, +) from paasng.accessories.services.models import ServiceCategory from paasng.core.region.models import get_all_regions, set_service_categories_loader from paasng.platform.engine.models import EngineApp @@ -220,6 +225,9 @@ def get_provisioned_queryset_by_services(self, services: List[ServiceObj], appli ) list_by_region: Callable[..., Iterable[ServiceObj]] = _proxied_chained_generator("list_by_region") list = cast(Callable[..., Iterable[ServiceObj]], _proxied_chained_generator("list")) + list_unbound_instance_rels = cast( + Callable[..., Iterable[UnboundEngineAppInstanceRel]], _proxied_chained_generator("list_unbound_instance_rels") + ) # Proxied generator methods end diff --git a/apiserver/paasng/paasng/accessories/servicehub/models.py b/apiserver/paasng/paasng/accessories/servicehub/models.py index 5e16a079fb..08bf82d8d2 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/models.py +++ b/apiserver/paasng/paasng/accessories/servicehub/models.py @@ -22,7 +22,7 @@ from django.db import models -from paasng.accessories.servicehub.constants import ServiceType, ServiceUnboundStatus +from paasng.accessories.servicehub.constants import ServiceType from paasng.accessories.servicehub.services import ServiceObj from paasng.accessories.services.models import Plan, Service, ServiceInstance from paasng.platform.applications.models import ApplicationEnvironment @@ -126,28 +126,18 @@ def __str__(self): class UnboundServiceEngineAppAttachment(OwnerTimestampedModel): """Local service instance which is unbound with engine app""" - application = models.ForeignKey( - "applications.Application", on_delete=models.SET_NULL, null=True, db_constraint=False, verbose_name="蓝鲸应用" - ) - module = models.ForeignKey( - "modules.Module", on_delete=models.SET_NULL, null=True, db_constraint=False, verbose_name="蓝鲸应用模块" - ) - environment = models.CharField(verbose_name="部署环境", max_length=16) engine_app = models.ForeignKey( - "engine.EngineApp", on_delete=models.SET_NULL, null=True, db_constraint=False, verbose_name="蓝鲸引擎应用" + "engine.EngineApp", on_delete=models.CASCADE, null=True, db_constraint=False, verbose_name="蓝鲸引擎应用" ) service = models.ForeignKey(Service, on_delete=models.CASCADE, verbose_name="增强服务") + plan = models.ForeignKey(Plan, on_delete=models.CASCADE) service_instance = models.ForeignKey( ServiceInstance, on_delete=models.CASCADE, null=True, blank=True, verbose_name="增强服务实例" ) - status = models.IntegerField( - choices=ServiceUnboundStatus.CHOICES, - default=ServiceUnboundStatus.Unbound, - verbose_name="解绑状态", - help_text="1 已解绑; 2 已回收;", - ) + credentials_enabled = models.BooleanField(default=True, verbose_name="是否使用凭证") class Meta: + unique_together = ("service", "engine_app") verbose_name = "本地已解绑增强服务" def clean_service_instance(self): @@ -186,26 +176,20 @@ class Meta: class UnboundRemoteServiceEngineAppAttachment(OwnerTimestampedModel): """Remote service instance which is unbound with engine app""" - application = models.ForeignKey( - "applications.Application", on_delete=models.SET_NULL, null=True, db_constraint=False, verbose_name="蓝鲸应用" - ) - module = models.ForeignKey( - "modules.Module", on_delete=models.SET_NULL, null=True, db_constraint=False, verbose_name="蓝鲸应用模块" - ) - environment = models.CharField(verbose_name="部署环境", max_length=16) engine_app = models.ForeignKey( - "engine.EngineApp", on_delete=models.SET_NULL, null=True, db_constraint=False, verbose_name="蓝鲸引擎应用" + "engine.EngineApp", + on_delete=models.CASCADE, + db_constraint=False, + verbose_name="蓝鲸引擎应用", + related_name="unbound_remote_service_attachment", ) service_id = models.UUIDField(verbose_name="远程增强服务 ID") + plan_id = models.UUIDField(verbose_name="远程增强服务 Plan ID") service_instance_id = models.UUIDField(null=True, verbose_name="远程增强服务实例 ID") - status = models.IntegerField( - choices=ServiceUnboundStatus.CHOICES, - default=ServiceUnboundStatus.Unbound, - verbose_name="解绑状态", - help_text="1 已解绑; 2 已回收;", - ) + credentials_enabled = models.BooleanField(default=True, verbose_name="是否使用凭证") class Meta: + unique_together = ("service_id", "engine_app") verbose_name = "远程已解绑增强服务" diff --git a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py index 0682ccaaf7..9d57e9ae4b 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py @@ -32,7 +32,11 @@ from paas_wl.infras.cluster.shim import EnvClusterService from paas_wl.workloads.networking.egress.shim import get_cluster_egress_info from paasng.accessories.servicehub import constants, exceptions -from paasng.accessories.servicehub.exceptions import BindServiceNoPlansError, UnboundSvcAttachmentDoesNotExist +from paasng.accessories.servicehub.exceptions import ( + BindServiceNoPlansError, + SvcInstanceNotFound, + UnboundSvcAttachmentDoesNotExist, +) from paasng.accessories.servicehub.models import ( RemoteServiceEngineAppAttachment, RemoteServiceModuleAttachment, @@ -290,19 +294,18 @@ def recycle_resource(self): except Exception as e: logger.exception("Error occurs during recycling") raise exceptions.SvcInstanceDeleteError("unable to delete instance") from e - if not self.db_obj.prefer_async_delete: + if self.db_obj.prefer_async_delete: self.mark_unbound() self.db_obj.service_instance_id = None self.db_obj.save() def mark_unbound(self): UnboundRemoteServiceEngineAppAttachment.objects.create( - application=self.db_application, - module=self.db_module, - environment=self.db_env.environment, engine_app=self.db_engine_app, service_id=self.db_obj.service_id, + plan_id=self.db_obj.plan_id, service_instance_id=self.db_obj.service_instance_id, + credentials_enabled=self.db_obj.credentials_enabled, ) def get_instance(self) -> ServiceInstanceObj: @@ -374,6 +377,84 @@ def get_plan(self) -> RemotePlanObj: raise RuntimeError("Plan not found") +class UnboundRemoteEngineAppInstanceRel(UnboundEngineAppInstanceRel): + """A unbound relationship between EngineApp and Provisioned instance""" + + def __init__( + self, db_obj: UnboundRemoteServiceEngineAppAttachment, mgr: "RemoteServiceMgr", store: RemoteServiceStore + ): + self.store = store + self.mgr = mgr + + # Database objects + self.db_obj = db_obj + self.db_env = ModuleEnvironment.objects.get(engine_app=self.db_obj.engine_app) + self.db_application = self.db_env.application + + # Client components + self.remote_config = self.store.get_source_config(str(self.db_obj.service_id)) + self.remote_client = RemoteServiceClient(self.remote_config) + + def get_service(self) -> RemoteServiceObj: + return self.mgr.get(str(self.db_obj.service_id), region=self.db_application.region) + + def get_instance(self) -> ServiceInstanceObj: + """Get service instance object""" + try: + instance_data = self.remote_client.retrieve_instance(str(self.db_obj.service_instance_id)) + except RClientResponseError as e: + # if not find service instance with this id, remote response http status code 404 + if e.status_code == 404: + self.db_obj.delete() + raise SvcInstanceNotFound(f"service instance {self.db_obj.service_instance_id} not found") + raise + + svc_obj = self.get_service() + create_time = arrow.get(instance_data.get("created")) # type: ignore + return create_svc_instance_obj_from_remote( + uuid=str(self.db_obj.service_instance_id), + credentials=instance_data["credentials"], + config=instance_data["config"], + field_prefix=svc_obj.name, + create_time=create_time.datetime, + ) + + def is_recycled(self) -> bool: + try: + self.remote_client.retrieve_instance(str(self.db_obj.service_instance_id)) + except RClientResponseError as e: + # if not find service instance with this id, remote response http status code 404 + if e.status_code == 404: + self.db_obj.delete() + return True + raise + return False + + def recycle_resource(self) -> None: + if self.is_recycled(): + return + + try: + self.remote_client.delete_instance_synchronously(instance_id=str(self.db_obj.service_instance_id)) + self.db_obj.delete() + except Exception as e: + logger.exception("Error occurs during recycling") + raise exceptions.SvcInstanceDeleteError("unable to delete instance") from e + + def get_plan(self) -> RemotePlanObj: + plan_id = str(self.db_obj.plan_id) + # 兼容从v2迁移至v3的增强服务, 避免前端因此出现异常 + if plan_id == str(constants.LEGACY_PLAN_ID): + return RemotePlanObj.from_data(constants.LEGACY_PLAN_INSTANCE) + + svc_data = self.store.get(str(self.db_obj.service_id), region=self.db_application.region) + for d in svc_data["plans"]: + if d["uuid"] == plan_id: + return RemotePlanObj.from_data(d) + + raise RuntimeError("Plan not found") + + class RemotePlainInstanceMgr(PlainInstanceMgr): """纯粹的远程增强服务实例的管理器, 仅调用远程接口创建增强服务实例, 不涉及增强服务资源申请的流程""" @@ -629,6 +710,16 @@ def list_provisioned_rels( for attachment in qs: yield self.transform_rel_db_obj(attachment) + def list_unbound_instance_rels( + self, engine_app: EngineApp, service: Optional[ServiceObj] = None + ) -> Generator[UnboundRemoteEngineAppInstanceRel, None, None]: + """Return all unbound engine_app <-> remote service instances""" + qs = engine_app.unbound_remote_service_attachment + if service: + qs = qs.filter(service_id=service.uuid) + for attachment in qs: + yield UnboundRemoteEngineAppInstanceRel(attachment, self, self.store) + def get_attachment_by_instance_id(self, service: ServiceObj, service_instance_id: uuid.UUID): try: return RemoteServiceEngineAppAttachment.objects.get( @@ -701,53 +792,10 @@ def get_unbound_instance_rel_by_instance_id(self, service: ServiceObj, service_i instance = UnboundRemoteServiceEngineAppAttachment.objects.get( service_id=service.uuid, service_instance_id=service_instance_id, - status=constants.ServiceUnboundStatus.Unbound, ) except UnboundRemoteServiceEngineAppAttachment.DoesNotExist as e: raise UnboundSvcAttachmentDoesNotExist from e - return RemoteUnboundEngineAppInstanceRel(instance, self.store) - - -class RemoteUnboundEngineAppInstanceRel(UnboundEngineAppInstanceRel): - """A unbound relationship between EngineApp and Provisioned instance""" - - def __init__(self, db_obj: UnboundRemoteServiceEngineAppAttachment, store: RemoteServiceStore): - self.db_obj = db_obj - self.store = store - - # Client components - self.remote_config = self.store.get_source_config(str(self.db_obj.service_id)) - self.remote_client = RemoteServiceClient(self.remote_config) - - def is_unbound(self): - return self.db_obj.status == constants.ServiceUnboundStatus.Unbound - - def is_recycled(self): - try: - self.remote_client.retrieve_instance(str(self.db_obj.service_instance_id)) - except RClientResponseError as e: - # if not find service instance with this id, remote response http status code 404 - if e.status_code == 404: - return True - raise - return False - - def recycle_resource(self): - if not self.is_unbound(): - raise UnboundSvcAttachmentDoesNotExist("service instance is not unbound") - - if self.is_recycled(): - self.db_obj.status = constants.ServiceUnboundStatus.Recycled - self.db_obj.save() - return - - try: - self.remote_client.delete_instance_synchronously(instance_id=str(self.db_obj.service_instance_id)) - self.db_obj.status = constants.ServiceUnboundStatus.Recycled - self.db_obj.save() - except Exception as e: - logger.exception("Error occurs during recycling") - raise exceptions.SvcInstanceDeleteError("unable to delete instance") from e + return UnboundRemoteEngineAppInstanceRel(instance, self, self.store) class RemotePlanMgr(BasePlanMgr): diff --git a/apiserver/paasng/paasng/accessories/servicehub/serializers.py b/apiserver/paasng/paasng/accessories/servicehub/serializers.py index 30dca91795..bb1c222450 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/serializers.py +++ b/apiserver/paasng/paasng/accessories/servicehub/serializers.py @@ -23,7 +23,6 @@ from paasng.accessories.servicehub.remote.exceptions import ServiceConfigNotFound from paasng.accessories.servicehub.remote.store import get_remote_store from paasng.accessories.services.models import ServiceCategory -from paasng.platform.modules.models import Module from paasng.platform.modules.serializers import MinimalModuleSLZ @@ -262,23 +261,9 @@ class UpdateServiceEngineAppAttachmentSLZ(serializers.Serializer): class UnboundServiceEngineAppAttachmentSLZ(serializers.Serializer): - module_id = serializers.CharField(write_only=True, help_text="模块 id") - module_name = serializers.SerializerMethodField(help_text="模块名") - environment = serializers.CharField(help_text="运行环境") service = ServiceMinimalSLZ(help_text="增强服务信息") - service_instance_id = serializers.UUIDField(help_text="增强服务实例 id") - - def get_module_name(self, obj): - module_id = obj.get("module_id") - if module_id: - try: - module = Module.objects.get(id=module_id) - except Module.DoesNotExist: - return None - return module.name - return None - - -class RecycleUnboundServiceEngineAppAttachmentSLZ(serializers.Serializer): - service_id = serializers.UUIDField(help_text="增强服务 id", required=True) - service_instance_id = serializers.UUIDField(help_text="增强服务实例 id", required=True) + unbound_instances = ServiceInstanceInfoSLZ(many=True, help_text="已解绑增强服务实例") + count = serializers.SerializerMethodField(help_text="数量") + + def get_count(self, obj): + return len(obj.get("unbound_instances") or []) diff --git a/apiserver/paasng/paasng/accessories/servicehub/services.py b/apiserver/paasng/paasng/accessories/servicehub/services.py index a43790dfd5..cd74e56707 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/services.py +++ b/apiserver/paasng/paasng/accessories/servicehub/services.py @@ -224,7 +224,15 @@ class UnboundEngineAppInstanceRel(metaclass=ABCMeta): db_obj: Any @abstractmethod - def is_unbound(self) -> bool: + def get_service(self) -> ServiceObj: + raise NotImplementedError + + @abstractmethod + def get_instance(self) -> ServiceInstanceObj: + raise NotImplementedError + + @abstractmethod + def get_plan(self) -> PlanObj: raise NotImplementedError @abstractmethod @@ -291,6 +299,12 @@ def list_provisioned_rels( ) -> Generator[EngineAppInstanceRel, None, None]: raise NotImplementedError + @abstractmethod + def list_unbound_instance_rels( + self, engine_app: EngineApp, service: Optional[ServiceObj] = None + ) -> Generator[UnboundEngineAppInstanceRel, None, None]: + raise NotImplementedError + @abstractmethod def get_provisioned_queryset(self, service: ServiceObj, application_ids: List[str]) -> QuerySet: raise NotImplementedError diff --git a/apiserver/paasng/paasng/accessories/servicehub/urls.py b/apiserver/paasng/paasng/accessories/servicehub/urls.py index bccea3988d..a8374153ee 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/urls.py +++ b/apiserver/paasng/paasng/accessories/servicehub/urls.py @@ -24,6 +24,7 @@ SERVICE_UUID = "(?P[0-9a-f-]{32,36})" APP_UUID = "(?P[0-9a-f-]{32,36})" CATEGORY_ID = r"(?P[\d]+)" +SERVICE_INTANCE_ID = "(?P[0-9a-f-]{32,36})" urlpatterns = [ # service APIs @@ -100,6 +101,11 @@ views.ModuleServicesViewSet.as_view({"get": "retrieve", "delete": "unbind"}), name="api.services.list_by_application", ), + re_path( + make_app_pattern(f"/services/{SERVICE_UUID}/unbound$", include_envs=False), + views.ModuleServicesViewSet.as_view({"get": "retrieve_unbound_instances"}), + name="api.services.list_unbound_instance", + ), # Manager service attachments (from services side) re_path( r"^api/services/service-attachments/$", @@ -112,12 +118,18 @@ views.ServiceEngineAppAttachmentViewSet.as_view({"get": "list", "put": "update"}), name="api.services.credentials_enabled", ), - # List unbound engine_app attachment instances, and recycle instance + # List unbound engine_app attachment instances re_path( - r"^api/bkapps/applications/(?P[^/]+)/service/attachment/instances/unbound/$", - views.UnboundServiceEngineAppAttachmentViewSet.as_view({"get": "list", "post": "recycle_instance"}), + make_app_pattern("/services/attachments/unbound/$", include_envs=False), + views.UnboundServiceEngineAppAttachmentViewSet.as_view({"get": "list"}), name="api.services.attachment.unbound", ), + # Recycle unbound instance + re_path( + make_app_pattern(f"/services/{SERVICE_UUID}/unbound/{SERVICE_INTANCE_ID}/$", include_envs=False), + views.UnboundServiceEngineAppAttachmentViewSet.as_view({"delete": "recycle"}), + name="api.services.attachment.unbound.recycle", + ), # Service sharing APIs re_path( make_app_pattern(f"/services/{SERVICE_UUID}/shareable_modules/$", include_envs=False), diff --git a/apiserver/paasng/paasng/accessories/servicehub/views.py b/apiserver/paasng/paasng/accessories/servicehub/views.py index 775ff5388a..f23bdf66c4 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/views.py +++ b/apiserver/paasng/paasng/accessories/servicehub/views.py @@ -31,19 +31,15 @@ from rest_framework.response import Response from paasng.accessories.servicehub import serializers as slzs -from paasng.accessories.servicehub.constants import ServiceUnboundStatus from paasng.accessories.servicehub.exceptions import ( BindServiceNoPlansError, ReferencedAttachmentNotFound, ServiceObjNotFound, SharedAttachmentAlreadyExists, + SvcInstanceNotFound, ) from paasng.accessories.servicehub.manager import mixed_service_mgr -from paasng.accessories.servicehub.models import ( - ServiceSetGroupByName, - UnboundRemoteServiceEngineAppAttachment, - UnboundServiceEngineAppAttachment, -) +from paasng.accessories.servicehub.models import ServiceSetGroupByName from paasng.accessories.servicehub.remote.manager import ( RemoteServiceInstanceMgr, RemoteServiceMgr, @@ -200,6 +196,34 @@ def retrieve(self, request, code, module_name, service_id): serializer = slzs.ServiceInstanceInfoSLZ(results, many=True) return Response({"count": len(results), "results": serializer.data}) + @app_action_required(AppAction.BASIC_DEVELOP) + def retrieve_unbound_instances(self, request, code, module_name, service_id): + """查看应用模块与增强服务已解绑实例详情""" + application = self.get_application() + module = self.get_module_via_path() + service = self.get_service(service_id, application) + + results = [] + for env in module.envs.all(): + for rel in mixed_service_mgr.list_unbound_instance_rels(env.engine_app, service=service): + try: + instance = rel.get_instance() + except SvcInstanceNotFound: + # 如果已经回收了,获取不到 instance,跳过 + continue + plan = rel.get_plan() + results.append( + { + "service_instance": instance, + "environment": env.environment, + "environment_name": AppEnvName.get_choice_label(env.environment), + "service_specs": plan.specifications, + "usage": "{}", + } + ) + serializer = slzs.ServiceInstanceInfoSLZ(results, many=True) + return Response({"count": len(results), "results": serializer.data}) + @app_action_required(AppAction.BASIC_DEVELOP) def retrieve_specs(self, request, code, module_name, service_id): """获取应用已绑定的服务规格""" @@ -765,40 +789,48 @@ def update(self, request, code, module_name, service_id): class UnboundServiceEngineAppAttachmentViewSet(viewsets.ViewSet, ApplicationCodeInPathMixin): permission_classes = [IsAuthenticated, application_perm_class(AppAction.BASIC_DEVELOP)] - def list(self, request, code): - """查看已解绑但未回收增强服务""" - app = self.get_application() - remote_intsances = UnboundRemoteServiceEngineAppAttachment.objects.filter( - application=app, status=ServiceUnboundStatus.Unbound - ) - local_instances = UnboundServiceEngineAppAttachment.objects.filter( - application=app, status=ServiceUnboundStatus.Unbound - ) + def list(self, request, code, module_name): + """查看模块已解绑增强服务,按 service 分类""" + application = self.get_application() + module = self.get_module_via_path() + + categorized_rels: Dict[str, List[Dict]] = {} + for env in module.envs.all(): + for rel in mixed_service_mgr.list_unbound_instance_rels(env.engine_app): + try: + instance = rel.get_instance() + except SvcInstanceNotFound: + # 如果已经回收了,获取不到 instance,跳过 + continue + plan = rel.get_plan() + + service_id = rel.db_obj.service_id + if service_id not in categorized_rels: + categorized_rels[service_id] = [] + + categorized_rels[service_id].append( + { + "service_instance": instance, + "environment": env.environment, + "environment_name": AppEnvName.get_choice_label(env.environment), + "service_specs": plan.specifications, + "usage": "{}", + } + ) results = [] - for instance in remote_intsances + local_instances: - service_obj = mixed_service_mgr.get_or_404(instance.service_id, app.region) + for service_id, rels in categorized_rels.items(): results.append( - { - "module_id": instance.module, - "environment": instance.environment, - "service": service_obj, - "service_instance_id": instance.service_instance_id, - } + {"service": mixed_service_mgr.get_or_404(service_id, application.region), "unbound_instances": rels} ) - return Response(slzs.UnboundServiceEngineAppAttachmentSLZ(results, many=True).data) + serializer = slzs.UnboundServiceEngineAppAttachmentSLZ(results, many=True) + return Response(serializer.data) - def recycle_instance(self, request, code): + def recycle(self, request, code, module_name, service_id, service_instance_id): """回收已解绑增强服务""" - serializer = slzs.RecycleUnboundServiceEngineAppAttachmentSLZ(data=request.data) - serializer.is_valid(raise_exception=True) - data = serializer.validated_data - - service_obj = mixed_service_mgr.get_or_404(data.service_id, self.get_application().region) - unbound_instance = mixed_service_mgr.get_unbound_instance_rel_by_instance_id( - service_obj, data.service_instance_id - ) + service_obj = mixed_service_mgr.get_or_404(service_id, self.get_application().region) + unbound_instance = mixed_service_mgr.get_unbound_instance_rel_by_instance_id(service_obj, service_instance_id) unbound_instance.recycle_instance() return Response() From 0bf8cacbb717482b6abad09f4649a8e6f2b52e20 Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Wed, 11 Dec 2024 15:43:06 +0800 Subject: [PATCH 05/31] fix: migrations --- ...oundserviceengineappattachment_and_more.py | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/apiserver/paasng/paasng/accessories/servicehub/migrations/0005_unboundserviceengineappattachment_and_more.py b/apiserver/paasng/paasng/accessories/servicehub/migrations/0005_unboundserviceengineappattachment_and_more.py index 60b20c8389..62cb45b98a 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/migrations/0005_unboundserviceengineappattachment_and_more.py +++ b/apiserver/paasng/paasng/accessories/servicehub/migrations/0005_unboundserviceengineappattachment_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.16 on 2024-12-03 10:04 +# Generated by Django 4.2.16 on 2024-12-11 07:40 from django.db import migrations, models import django.db.models.deletion @@ -8,10 +8,8 @@ class Migration(migrations.Migration): dependencies = [ - ('applications', '0013_applicationdeploymentmoduleorder'), - ('modules', '0016_auto_20240904_1439'), - ('engine', '0022_builtinconfigvar'), ('services', '0006_alter_servicecategory_name_en'), + ('engine', '0023_remove_deployment_hooks_remove_deployment_procfile'), ('servicehub', '0004_auto_20240412_1723'), ] @@ -24,16 +22,15 @@ class Migration(migrations.Migration): ('created', models.DateTimeField(auto_now_add=True)), ('updated', models.DateTimeField(auto_now=True)), ('owner', paasng.utils.models.BkUserField(blank=True, db_index=True, max_length=64, null=True)), - ('environment', models.CharField(max_length=16, verbose_name='部署环境')), - ('status', models.IntegerField(choices=[(1, 'Unbound service instance with engine app'), (2, 'Recycled service instance')], default=1, help_text='1 已解绑; 2 已回收;', verbose_name='解绑状态')), - ('application', models.ForeignKey(db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to='applications.application', verbose_name='蓝鲸应用')), - ('engine_app', models.ForeignKey(db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to='engine.engineapp', verbose_name='蓝鲸引擎应用')), - ('module', models.ForeignKey(db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to='modules.module', verbose_name='蓝鲸应用模块')), + ('credentials_enabled', models.BooleanField(default=True, verbose_name='是否使用凭证')), + ('engine_app', models.ForeignKey(db_constraint=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='engine.engineapp', verbose_name='蓝鲸引擎应用')), + ('plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='services.plan')), ('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='services.service', verbose_name='增强服务')), ('service_instance', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='services.serviceinstance', verbose_name='增强服务实例')), ], options={ 'verbose_name': '本地已解绑增强服务', + 'unique_together': {('service', 'engine_app')}, }, ), migrations.CreateModel( @@ -44,16 +41,15 @@ class Migration(migrations.Migration): ('created', models.DateTimeField(auto_now_add=True)), ('updated', models.DateTimeField(auto_now=True)), ('owner', paasng.utils.models.BkUserField(blank=True, db_index=True, max_length=64, null=True)), - ('environment', models.CharField(max_length=16, verbose_name='部署环境')), ('service_id', models.UUIDField(verbose_name='远程增强服务 ID')), + ('plan_id', models.UUIDField(verbose_name='远程增强服务 Plan ID')), ('service_instance_id', models.UUIDField(null=True, verbose_name='远程增强服务实例 ID')), - ('status', models.IntegerField(choices=[(1, 'Unbound service instance with engine app'), (2, 'Recycled service instance')], default=1, help_text='1 已解绑; 2 已回收;', verbose_name='解绑状态')), - ('application', models.ForeignKey(db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to='applications.application', verbose_name='蓝鲸应用')), - ('engine_app', models.ForeignKey(db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to='engine.engineapp', verbose_name='蓝鲸引擎应用')), - ('module', models.ForeignKey(db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to='modules.module', verbose_name='蓝鲸应用模块')), + ('credentials_enabled', models.BooleanField(default=True, verbose_name='是否使用凭证')), + ('engine_app', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, related_name='unbound_remote_service_attachment', to='engine.engineapp', verbose_name='蓝鲸引擎应用')), ], options={ 'verbose_name': '远程已解绑增强服务', + 'unique_together': {('service_id', 'engine_app')}, }, ), ] From 8d69111594e1724a22481589f43184bc6890155b Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Wed, 11 Dec 2024 20:03:23 +0800 Subject: [PATCH 06/31] fix: check if remote recycled unbound instance --- .../accessories/servicehub/local/manager.py | 4 +-- .../commands/check_unbound_remote_instance.py | 33 ++++++++++++++++++ .../paasng/accessories/servicehub/models.py | 7 +++- .../paasng/accessories/servicehub/tasks.py | 34 ++++++++++++++++++- .../paasng/accessories/servicehub/views.py | 9 ++--- 5 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 apiserver/paasng/paasng/accessories/servicehub/management/commands/check_unbound_remote_instance.py diff --git a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py index cf24272811..4c5d6d25f4 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py @@ -171,7 +171,7 @@ def provision(self): ).inc() def recycle_resource(self): - if self.db_obj.service.prefer_async_delete: + if self.is_provisioned() and self.db_obj.service.prefer_async_delete: self.mark_unbound() self.db_obj.clean_service_instance() @@ -400,7 +400,7 @@ def list_unbound_instance_rels( self, engine_app: EngineApp, service: Optional[ServiceObj] = None ) -> Generator[UnboundEngineAppInstanceRel, None, None]: """Return all unbound engine_app <-> local service instances by specified service (None for all)""" - qs = engine_app.service_attachment.exclude(service_instance__isnull=True) + qs = engine_app.unbound_service_attachment if service: qs = qs.filter(service_id=service.uuid) for attachment in qs: diff --git a/apiserver/paasng/paasng/accessories/servicehub/management/commands/check_unbound_remote_instance.py b/apiserver/paasng/paasng/accessories/servicehub/management/commands/check_unbound_remote_instance.py new file mode 100644 index 0000000000..5eff1dc4dc --- /dev/null +++ b/apiserver/paasng/paasng/accessories/servicehub/management/commands/check_unbound_remote_instance.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - PaaS 平台 (BlueKing - PaaS System) 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. + +import logging + +from django.core.management.base import BaseCommand + +from paasng.accessories.servicehub.tasks import check_is_unbound_remote_service_instance_recycled + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = "Check if unbound remote service instance is recycled, if it is recycled, delete object in database." + + def handle(self, *args, **options): + print("Start to check if remote unbound service instance recycled") + check_is_unbound_remote_service_instance_recycled() + print("Complete check if remote unbound service instance recycled") diff --git a/apiserver/paasng/paasng/accessories/servicehub/models.py b/apiserver/paasng/paasng/accessories/servicehub/models.py index 08bf82d8d2..64b9897b8b 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/models.py +++ b/apiserver/paasng/paasng/accessories/servicehub/models.py @@ -127,7 +127,12 @@ class UnboundServiceEngineAppAttachment(OwnerTimestampedModel): """Local service instance which is unbound with engine app""" engine_app = models.ForeignKey( - "engine.EngineApp", on_delete=models.CASCADE, null=True, db_constraint=False, verbose_name="蓝鲸引擎应用" + "engine.EngineApp", + on_delete=models.CASCADE, + null=True, + db_constraint=False, + verbose_name="蓝鲸引擎应用", + related_name="unbound_service_attachment", ) service = models.ForeignKey(Service, on_delete=models.CASCADE, verbose_name="增强服务") plan = models.ForeignKey(Plan, on_delete=models.CASCADE) diff --git a/apiserver/paasng/paasng/accessories/servicehub/tasks.py b/apiserver/paasng/paasng/accessories/servicehub/tasks.py index 9dfd11b11e..6d725c0225 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/tasks.py +++ b/apiserver/paasng/paasng/accessories/servicehub/tasks.py @@ -16,8 +16,13 @@ # to the current version of the project delivered to anyone in the future. import logging +from collections import defaultdict -from .models import ServiceInstance +from paasng.accessories.servicehub.remote.client import RemoteServiceClient +from paasng.accessories.servicehub.remote.exceptions import RClientResponseError +from paasng.accessories.servicehub.remote.store import get_remote_store + +from .models import ServiceInstance, UnboundRemoteServiceEngineAppAttachment logger = logging.getLogger(__name__) @@ -44,3 +49,30 @@ def clean_instances(): continue else: logger.info(f"instance<{uuid}> cleaned. ") + + +def check_is_unbound_remote_service_instance_recycled(): + store = get_remote_store() + unbound_instances = UnboundRemoteServiceEngineAppAttachment.objects.all() + + if not unbound_instances: + logger.info("no unbound remote instances waiting for to be recycled") + return + + categorized_instances = defaultdict(list) + for instance in unbound_instances: + categorized_instances[str(instance.service_id)].append(instance) + + for service_id, instances in categorized_instances.items(): + remote_config = store.get_source_config(service_id) + remote_client = RemoteServiceClient(remote_config) + for instance in instances: + try: + remote_client.retrieve_instance(instance.service_instance_id) + except RClientResponseError as e: + # if not find service instance with this id, remote response http status code 404 + if e.status_code == 404: + instance.delete() + logger.info(f"unbound instance<{instance.service_instance_id}> recycled. ") + continue + logger.warning(f"retrive remote instance<{instance.service_instance_id}> failed.") diff --git a/apiserver/paasng/paasng/accessories/servicehub/views.py b/apiserver/paasng/paasng/accessories/servicehub/views.py index f23bdf66c4..939c859f27 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/views.py +++ b/apiserver/paasng/paasng/accessories/servicehub/views.py @@ -16,6 +16,7 @@ # to the current version of the project delivered to anyone in the future. import logging +from collections import defaultdict from typing import Any, Dict, List from django.core.exceptions import ObjectDoesNotExist @@ -794,7 +795,7 @@ def list(self, request, code, module_name): application = self.get_application() module = self.get_module_via_path() - categorized_rels: Dict[str, List[Dict]] = {} + categorized_rels = defaultdict(list) for env in module.envs.all(): for rel in mixed_service_mgr.list_unbound_instance_rels(env.engine_app): try: @@ -804,11 +805,7 @@ def list(self, request, code, module_name): continue plan = rel.get_plan() - service_id = rel.db_obj.service_id - if service_id not in categorized_rels: - categorized_rels[service_id] = [] - - categorized_rels[service_id].append( + categorized_rels[str(rel.db_obj.service_id)].append( { "service_instance": instance, "environment": env.environment, From cc6e3f146377673d56b9115c3f50980b851ae624 Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Thu, 12 Dec 2024 11:59:25 +0800 Subject: [PATCH 07/31] fix: minor --- .../paasng/accessories/servicehub/tasks.py | 8 +- .../paasng/accessories/servicehub/urls.py | 25 +-- .../paasng/accessories/servicehub/views.py | 146 +++++++++--------- 3 files changed, 91 insertions(+), 88 deletions(-) diff --git a/apiserver/paasng/paasng/accessories/servicehub/tasks.py b/apiserver/paasng/paasng/accessories/servicehub/tasks.py index 6d725c0225..4b33fb6027 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/tasks.py +++ b/apiserver/paasng/paasng/accessories/servicehub/tasks.py @@ -73,6 +73,10 @@ def check_is_unbound_remote_service_instance_recycled(): # if not find service instance with this id, remote response http status code 404 if e.status_code == 404: instance.delete() - logger.info(f"unbound instance<{instance.service_instance_id}> recycled. ") + logger.info(f"unbound service instance<{instance.service_instance_id}> is recycled.") continue - logger.warning(f"retrive remote instance<{instance.service_instance_id}> failed.") + logger.warning(f"retrive unbound remote service instance<{instance.service_instance_id}> failed: {e}") + except Exception as e: + logger.warning(f"retrive unbound remote service instance<{instance.service_instance_id}> failed: {e}") + else: + logger.info(f"unbound service instance<{instance.service_instance_id}> is not recycled.") diff --git a/apiserver/paasng/paasng/accessories/servicehub/urls.py b/apiserver/paasng/paasng/accessories/servicehub/urls.py index a8374153ee..47d8a4fa81 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/urls.py +++ b/apiserver/paasng/paasng/accessories/servicehub/urls.py @@ -101,11 +101,24 @@ views.ModuleServicesViewSet.as_view({"get": "retrieve", "delete": "unbind"}), name="api.services.list_by_application", ), + # retrieve unbound instances by module and service_id re_path( make_app_pattern(f"/services/{SERVICE_UUID}/unbound$", include_envs=False), views.ModuleServicesViewSet.as_view({"get": "retrieve_unbound_instances"}), name="api.services.list_unbound_instance", ), + # List unbound instances by module + re_path( + make_app_pattern("/services/attachments/unbound/$", include_envs=False), + views.ModuleServicesViewSet.as_view({"get": "list_unbound_instances_by_module"}), + name="api.services.attachment.unbound", + ), + # Recycle unbound instance + re_path( + make_app_pattern(f"/services/{SERVICE_UUID}/unbound/{SERVICE_INTANCE_ID}/$", include_envs=False), + views.ModuleServicesViewSet.as_view({"delete": "recycle_unbound_instance"}), + name="api.services.attachment.unbound.recycle", + ), # Manager service attachments (from services side) re_path( r"^api/services/service-attachments/$", @@ -118,18 +131,6 @@ views.ServiceEngineAppAttachmentViewSet.as_view({"get": "list", "put": "update"}), name="api.services.credentials_enabled", ), - # List unbound engine_app attachment instances - re_path( - make_app_pattern("/services/attachments/unbound/$", include_envs=False), - views.UnboundServiceEngineAppAttachmentViewSet.as_view({"get": "list"}), - name="api.services.attachment.unbound", - ), - # Recycle unbound instance - re_path( - make_app_pattern(f"/services/{SERVICE_UUID}/unbound/{SERVICE_INTANCE_ID}/$", include_envs=False), - views.UnboundServiceEngineAppAttachmentViewSet.as_view({"delete": "recycle"}), - name="api.services.attachment.unbound.recycle", - ), # Service sharing APIs re_path( make_app_pattern(f"/services/{SERVICE_UUID}/shareable_modules/$", include_envs=False), diff --git a/apiserver/paasng/paasng/accessories/servicehub/views.py b/apiserver/paasng/paasng/accessories/servicehub/views.py index 939c859f27..462f007071 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/views.py +++ b/apiserver/paasng/paasng/accessories/servicehub/views.py @@ -197,34 +197,6 @@ def retrieve(self, request, code, module_name, service_id): serializer = slzs.ServiceInstanceInfoSLZ(results, many=True) return Response({"count": len(results), "results": serializer.data}) - @app_action_required(AppAction.BASIC_DEVELOP) - def retrieve_unbound_instances(self, request, code, module_name, service_id): - """查看应用模块与增强服务已解绑实例详情""" - application = self.get_application() - module = self.get_module_via_path() - service = self.get_service(service_id, application) - - results = [] - for env in module.envs.all(): - for rel in mixed_service_mgr.list_unbound_instance_rels(env.engine_app, service=service): - try: - instance = rel.get_instance() - except SvcInstanceNotFound: - # 如果已经回收了,获取不到 instance,跳过 - continue - plan = rel.get_plan() - results.append( - { - "service_instance": instance, - "environment": env.environment, - "environment_name": AppEnvName.get_choice_label(env.environment), - "service_specs": plan.specifications, - "usage": "{}", - } - ) - serializer = slzs.ServiceInstanceInfoSLZ(results, many=True) - return Response({"count": len(results), "results": serializer.data}) - @app_action_required(AppAction.BASIC_DEVELOP) def retrieve_specs(self, request, code, module_name, service_id): """获取应用已绑定的服务规格""" @@ -306,6 +278,78 @@ def list_provisioned_env_keys(self, request, code, module_name): } return Response(data=env_key_dict) + @app_action_required(AppAction.BASIC_DEVELOP) + def retrieve_unbound_instances(self, request, code, module_name, service_id): + """查看应用模块与增强服务已解绑实例详情""" + application = self.get_application() + module = self.get_module_via_path() + service = self.get_service(service_id, application) + + results = [] + for env in module.envs.all(): + for rel in mixed_service_mgr.list_unbound_instance_rels(env.engine_app, service=service): + try: + instance = rel.get_instance() + except SvcInstanceNotFound: + # 如果已经回收了,获取不到 instance,跳过 + continue + plan = rel.get_plan() + results.append( + { + "service_instance": instance, + "environment": env.environment, + "environment_name": AppEnvName.get_choice_label(env.environment), + "service_specs": plan.specifications, + "usage": "{}", + } + ) + serializer = slzs.ServiceInstanceInfoSLZ(results, many=True) + return Response({"count": len(results), "results": serializer.data}) + + @app_action_required(AppAction.BASIC_DEVELOP) + def list_unbound_instances_by_module(self, request, code, module_name): + """查看模块所有已解绑增强服务实例,按增强服务归类""" + application = self.get_application() + module = self.get_module_via_path() + + categorized_rels = defaultdict(list) + for env in module.envs.all(): + for rel in mixed_service_mgr.list_unbound_instance_rels(env.engine_app): + try: + instance = rel.get_instance() + except SvcInstanceNotFound: + # 如果已经回收了,获取不到 instance,跳过 + continue + plan = rel.get_plan() + + categorized_rels[str(rel.db_obj.service_id)].append( + { + "service_instance": instance, + "environment": env.environment, + "environment_name": AppEnvName.get_choice_label(env.environment), + "service_specs": plan.specifications, + "usage": "{}", + } + ) + + results = [] + for service_id, rels in categorized_rels.items(): + results.append( + {"service": mixed_service_mgr.get_or_404(service_id, application.region), "unbound_instances": rels} + ) + + serializer = slzs.UnboundServiceEngineAppAttachmentSLZ(results, many=True) + return Response(serializer.data) + + @app_action_required(AppAction.MANAGE_ADDONS_SERVICES) + def recycle_unbound_instance(self, request, code, module_name, service_id, service_instance_id): + """回收已解绑增强服务""" + service_obj = mixed_service_mgr.get_or_404(service_id, self.get_application().region) + unbound_instance = mixed_service_mgr.get_unbound_instance_rel_by_instance_id(service_obj, service_instance_id) + unbound_instance.recycle_instance() + + return Response() + class ServiceViewSet(viewsets.ViewSet, ApplicationCodeInPathMixin): """增强服务相关视图(与应用无关的)""" @@ -785,49 +829,3 @@ def update(self, request, code, module_name, service_id): results.append(attachment) return Response(slzs.ServiceEngineAppAttachmentSLZ(results, many=True).data) - - -class UnboundServiceEngineAppAttachmentViewSet(viewsets.ViewSet, ApplicationCodeInPathMixin): - permission_classes = [IsAuthenticated, application_perm_class(AppAction.BASIC_DEVELOP)] - - def list(self, request, code, module_name): - """查看模块已解绑增强服务,按 service 分类""" - application = self.get_application() - module = self.get_module_via_path() - - categorized_rels = defaultdict(list) - for env in module.envs.all(): - for rel in mixed_service_mgr.list_unbound_instance_rels(env.engine_app): - try: - instance = rel.get_instance() - except SvcInstanceNotFound: - # 如果已经回收了,获取不到 instance,跳过 - continue - plan = rel.get_plan() - - categorized_rels[str(rel.db_obj.service_id)].append( - { - "service_instance": instance, - "environment": env.environment, - "environment_name": AppEnvName.get_choice_label(env.environment), - "service_specs": plan.specifications, - "usage": "{}", - } - ) - - results = [] - for service_id, rels in categorized_rels.items(): - results.append( - {"service": mixed_service_mgr.get_or_404(service_id, application.region), "unbound_instances": rels} - ) - - serializer = slzs.UnboundServiceEngineAppAttachmentSLZ(results, many=True) - return Response(serializer.data) - - def recycle(self, request, code, module_name, service_id, service_instance_id): - """回收已解绑增强服务""" - service_obj = mixed_service_mgr.get_or_404(service_id, self.get_application().region) - unbound_instance = mixed_service_mgr.get_unbound_instance_rel_by_instance_id(service_obj, service_instance_id) - unbound_instance.recycle_instance() - - return Response() From a9484cd70cb75340fe3ef7725a6abdfa254f30a8 Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Thu, 12 Dec 2024 20:50:00 +0800 Subject: [PATCH 08/31] fix: minor --- .../accessories/servicehub/local/manager.py | 2 +- .../accessories/servicehub/remote/manager.py | 4 +- .../paasng/accessories/servicehub/urls.py | 36 ++--- .../paasng/accessories/servicehub/views.py | 152 +++++++++--------- 4 files changed, 101 insertions(+), 93 deletions(-) diff --git a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py index 4c5d6d25f4..137574e430 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py @@ -400,7 +400,7 @@ def list_unbound_instance_rels( self, engine_app: EngineApp, service: Optional[ServiceObj] = None ) -> Generator[UnboundEngineAppInstanceRel, None, None]: """Return all unbound engine_app <-> local service instances by specified service (None for all)""" - qs = engine_app.unbound_service_attachment + qs = engine_app.unbound_service_attachment.all() if service: qs = qs.filter(service_id=service.uuid) for attachment in qs: diff --git a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py index 9d57e9ae4b..d89c8c1672 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py @@ -294,7 +294,7 @@ def recycle_resource(self): except Exception as e: logger.exception("Error occurs during recycling") raise exceptions.SvcInstanceDeleteError("unable to delete instance") from e - if self.db_obj.prefer_async_delete: + if self.remote_client.config.prefer_async_delete: self.mark_unbound() self.db_obj.service_instance_id = None self.db_obj.save() @@ -714,7 +714,7 @@ def list_unbound_instance_rels( self, engine_app: EngineApp, service: Optional[ServiceObj] = None ) -> Generator[UnboundRemoteEngineAppInstanceRel, None, None]: """Return all unbound engine_app <-> remote service instances""" - qs = engine_app.unbound_remote_service_attachment + qs = engine_app.unbound_remote_service_attachment.all() if service: qs = qs.filter(service_id=service.uuid) for attachment in qs: diff --git a/apiserver/paasng/paasng/accessories/servicehub/urls.py b/apiserver/paasng/paasng/accessories/servicehub/urls.py index 47d8a4fa81..9ca83ce974 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/urls.py +++ b/apiserver/paasng/paasng/accessories/servicehub/urls.py @@ -101,24 +101,6 @@ views.ModuleServicesViewSet.as_view({"get": "retrieve", "delete": "unbind"}), name="api.services.list_by_application", ), - # retrieve unbound instances by module and service_id - re_path( - make_app_pattern(f"/services/{SERVICE_UUID}/unbound$", include_envs=False), - views.ModuleServicesViewSet.as_view({"get": "retrieve_unbound_instances"}), - name="api.services.list_unbound_instance", - ), - # List unbound instances by module - re_path( - make_app_pattern("/services/attachments/unbound/$", include_envs=False), - views.ModuleServicesViewSet.as_view({"get": "list_unbound_instances_by_module"}), - name="api.services.attachment.unbound", - ), - # Recycle unbound instance - re_path( - make_app_pattern(f"/services/{SERVICE_UUID}/unbound/{SERVICE_INTANCE_ID}/$", include_envs=False), - views.ModuleServicesViewSet.as_view({"delete": "recycle_unbound_instance"}), - name="api.services.attachment.unbound.recycle", - ), # Manager service attachments (from services side) re_path( r"^api/services/service-attachments/$", @@ -153,6 +135,24 @@ views.RelatedApplicationsInfoViewSet.as_view({"get": "retrieve_related_applications_info"}), name="api.services.mysql.retrieve_related_applications_info", ), + # retrieve unbound instances by module and service_id + re_path( + make_app_pattern(f"/services/{SERVICE_UUID}/attachments/unbound/$", include_envs=False), + views.UnboundServiceEngineAppAttachmentViewSet.as_view({"get": "list_by_service"}), + name="api.services.attachment.unbound.list_by_service", + ), + # List unbound instances by module + re_path( + make_app_pattern("/services/attachments/unbound/$", include_envs=False), + views.UnboundServiceEngineAppAttachmentViewSet.as_view({"get": "list_by_module"}), + name="api.services.attachment.unbound.list_by_module", + ), + # Recycle unbound instance + re_path( + make_app_pattern(f"/services/{SERVICE_UUID}/unbound/{SERVICE_INTANCE_ID}/$", include_envs=False), + views.UnboundServiceEngineAppAttachmentViewSet.as_view({"delete": "recycle"}), + name="api.services.attachment.unbound.recycle", + ), ] # Multi-editions specific start diff --git a/apiserver/paasng/paasng/accessories/servicehub/views.py b/apiserver/paasng/paasng/accessories/servicehub/views.py index 462f007071..f46e92b013 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/views.py +++ b/apiserver/paasng/paasng/accessories/servicehub/views.py @@ -278,78 +278,6 @@ def list_provisioned_env_keys(self, request, code, module_name): } return Response(data=env_key_dict) - @app_action_required(AppAction.BASIC_DEVELOP) - def retrieve_unbound_instances(self, request, code, module_name, service_id): - """查看应用模块与增强服务已解绑实例详情""" - application = self.get_application() - module = self.get_module_via_path() - service = self.get_service(service_id, application) - - results = [] - for env in module.envs.all(): - for rel in mixed_service_mgr.list_unbound_instance_rels(env.engine_app, service=service): - try: - instance = rel.get_instance() - except SvcInstanceNotFound: - # 如果已经回收了,获取不到 instance,跳过 - continue - plan = rel.get_plan() - results.append( - { - "service_instance": instance, - "environment": env.environment, - "environment_name": AppEnvName.get_choice_label(env.environment), - "service_specs": plan.specifications, - "usage": "{}", - } - ) - serializer = slzs.ServiceInstanceInfoSLZ(results, many=True) - return Response({"count": len(results), "results": serializer.data}) - - @app_action_required(AppAction.BASIC_DEVELOP) - def list_unbound_instances_by_module(self, request, code, module_name): - """查看模块所有已解绑增强服务实例,按增强服务归类""" - application = self.get_application() - module = self.get_module_via_path() - - categorized_rels = defaultdict(list) - for env in module.envs.all(): - for rel in mixed_service_mgr.list_unbound_instance_rels(env.engine_app): - try: - instance = rel.get_instance() - except SvcInstanceNotFound: - # 如果已经回收了,获取不到 instance,跳过 - continue - plan = rel.get_plan() - - categorized_rels[str(rel.db_obj.service_id)].append( - { - "service_instance": instance, - "environment": env.environment, - "environment_name": AppEnvName.get_choice_label(env.environment), - "service_specs": plan.specifications, - "usage": "{}", - } - ) - - results = [] - for service_id, rels in categorized_rels.items(): - results.append( - {"service": mixed_service_mgr.get_or_404(service_id, application.region), "unbound_instances": rels} - ) - - serializer = slzs.UnboundServiceEngineAppAttachmentSLZ(results, many=True) - return Response(serializer.data) - - @app_action_required(AppAction.MANAGE_ADDONS_SERVICES) - def recycle_unbound_instance(self, request, code, module_name, service_id, service_instance_id): - """回收已解绑增强服务""" - service_obj = mixed_service_mgr.get_or_404(service_id, self.get_application().region) - unbound_instance = mixed_service_mgr.get_unbound_instance_rel_by_instance_id(service_obj, service_instance_id) - unbound_instance.recycle_instance() - - return Response() - class ServiceViewSet(viewsets.ViewSet, ApplicationCodeInPathMixin): """增强服务相关视图(与应用无关的)""" @@ -829,3 +757,83 @@ def update(self, request, code, module_name, service_id): results.append(attachment) return Response(slzs.ServiceEngineAppAttachmentSLZ(results, many=True).data) + + +class UnboundServiceEngineAppAttachmentViewSet(viewsets.ViewSet, ApplicationCodeInPathMixin): + """已解绑待回收增强服务实例相关API""" + + @staticmethod + def get_service(service_id, application): + return mixed_service_mgr.get_or_404(service_id, region=application.region) + + @app_action_required(AppAction.BASIC_DEVELOP) + def list_by_service(self, request, code, module_name, service_id): + """查看应用模块与增强服务已解绑实例详情""" + application = self.get_application() + module = self.get_module_via_path() + service = self.get_service(service_id, application) + + results = [] + for env in module.envs.all(): + for rel in mixed_service_mgr.list_unbound_instance_rels(env.engine_app, service=service): + try: + instance = rel.get_instance() + except SvcInstanceNotFound: + # 如果已经回收了,获取不到 instance,跳过 + continue + plan = rel.get_plan() + results.append( + { + "service_instance": instance, + "environment": env.environment, + "environment_name": AppEnvName.get_choice_label(env.environment), + "service_specs": plan.specifications, + "usage": "{}", + } + ) + serializer = slzs.ServiceInstanceInfoSLZ(results, many=True) + return Response({"count": len(results), "results": serializer.data}) + + @app_action_required(AppAction.BASIC_DEVELOP) + def list_by_module(self, request, code, module_name): + """查看模块所有已解绑增强服务实例,按增强服务归类""" + application = self.get_application() + module = self.get_module_via_path() + + categorized_rels = defaultdict(list) + for env in module.envs.all(): + for rel in mixed_service_mgr.list_unbound_instance_rels(env.engine_app): + try: + instance = rel.get_instance() + except SvcInstanceNotFound: + # 如果已经回收了,获取不到 instance,跳过 + continue + plan = rel.get_plan() + + categorized_rels[str(rel.db_obj.service_id)].append( + { + "service_instance": instance, + "environment": env.environment, + "environment_name": AppEnvName.get_choice_label(env.environment), + "service_specs": plan.specifications, + "usage": "{}", + } + ) + + results = [] + for service_id, rels in categorized_rels.items(): + results.append( + {"service": mixed_service_mgr.get_or_404(service_id, application.region), "unbound_instances": rels} + ) + + serializer = slzs.UnboundServiceEngineAppAttachmentSLZ(results, many=True) + return Response(serializer.data) + + @app_action_required(AppAction.MANAGE_ADDONS_SERVICES) + def recycle(self, request, code, module_name, service_id, service_instance_id): + """回收已解绑增强服务""" + service_obj = mixed_service_mgr.get_or_404(service_id, self.get_application().region) + unbound_instance = mixed_service_mgr.get_unbound_instance_rel_by_instance_id(service_obj, service_instance_id) + unbound_instance.recycle_instance() + + return Response() From c56ce05c0c18baef4763f83f17aa208a59b05a2e Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Thu, 12 Dec 2024 21:48:57 +0800 Subject: [PATCH 09/31] fix: unit test --- apiserver/paasng/tests/api/test_servicehub.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/apiserver/paasng/tests/api/test_servicehub.py b/apiserver/paasng/tests/api/test_servicehub.py index 85ea2bf2c6..ea29b81a8f 100644 --- a/apiserver/paasng/tests/api/test_servicehub.py +++ b/apiserver/paasng/tests/api/test_servicehub.py @@ -16,10 +16,12 @@ # to the current version of the project delivered to anyone in the future. import datetime +import uuid from unittest import mock import pytest from django_dynamic_fixture import G +from rest_framework import status from paasng.accessories.servicehub.models import RemoteServiceEngineAppAttachment from paasng.accessories.servicehub.services import ServiceInstanceObj @@ -94,3 +96,40 @@ def test_config_vars(self, list_provisioned_rels, api_client, bk_app, bk_module) assert set(response.data[service.display_name]) == {"a", "b"} # 增强服务环境变量设置为不写入则不返回 assert credentials_disabled_service.display_name not in return_svc_names + + +class TestUnboundServiceEngineAppAttachmentViewSet: + def create_mock_rel(self, service, credentials_enabled, create_time, **credentials): + rel = mock.MagicMock() + rel.get_instance.return_value = ServiceInstanceObj( + uuid=str(uuid.uuid4()), credentials=credentials, config={}, create_time=create_time + ) + rel.get_plan.return_value = mock.MagicMock(spec=["specifications"]) + rel.get_plan.return_value.specifications = {"name": "version"} + rel.get_service.return_value = service + rel.db_obj.credentials_enabled = credentials_enabled + return rel + + @mock.patch("paasng.accessories.servicehub.views.mixed_service_mgr.list_unbound_instance_rels") + @mock.patch("paasng.accessories.servicehub.views.mixed_service_mgr.get_or_404") + def test_retrieve_unbound_service_instances( + self, mock_get_or_404, mock_list_unbound_instance_rels, api_client, bk_app, bk_module + ): + service = G(Service) + mock_get_or_404.return_value = service + + mock_rel1 = self.create_mock_rel(service, True, datetime.datetime(2020, 1, 1), a=1, b=2) + mock_rel2 = self.create_mock_rel(service, False, datetime.datetime(2020, 1, 1), c=3) + mock_list_unbound_instance_rels.return_value = [mock_rel1, mock_rel2] + + url = f"/api/bkapps/applications/{bk_app.code}/modules/{bk_module.name}/services/{str(service.uuid)}/attachments/unbound/" + + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK + response_data = response.json() + print(response_data) + assert response_data["count"] == 4 + assert len(response_data["results"]) == 4 + assert response_data["results"][0]["service_instance"]["credentials"] == '{"a": 1, "b": 2}' + assert response_data["results"][3]["service_specs"] == {"name": "version"} From d72446e75f98e35ecafa4fe5eedd7ad35a4a0d6e Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Mon, 16 Dec 2024 19:20:45 +0800 Subject: [PATCH 10/31] fix: unit test --- .../paasng/accessories/servicehub/manager.py | 2 +- ...oundserviceengineappattachment_and_more.py | 4 +- .../paasng/accessories/servicehub/models.py | 1 - apiserver/paasng/tests/api/test_servicehub.py | 66 +++++++++++++++++-- 4 files changed, 63 insertions(+), 10 deletions(-) diff --git a/apiserver/paasng/paasng/accessories/servicehub/manager.py b/apiserver/paasng/paasng/accessories/servicehub/manager.py index e2fb2c539a..b844d286e7 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/manager.py @@ -290,7 +290,7 @@ def get_unbound_instance_rel_by_instance_id(self, service: ServiceObj, service_i except UnboundSvcAttachmentDoesNotExist: continue raise UnboundSvcAttachmentDoesNotExist( - f"service<{ServiceObj}> has no attachment with service_instance_id<{service_instance_id}>" + f"service<{service}> has no attachment with service_instance_id<{service_instance_id}>" ) diff --git a/apiserver/paasng/paasng/accessories/servicehub/migrations/0005_unboundserviceengineappattachment_and_more.py b/apiserver/paasng/paasng/accessories/servicehub/migrations/0005_unboundserviceengineappattachment_and_more.py index 62cb45b98a..556429abd8 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/migrations/0005_unboundserviceengineappattachment_and_more.py +++ b/apiserver/paasng/paasng/accessories/servicehub/migrations/0005_unboundserviceengineappattachment_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.16 on 2024-12-11 07:40 +# Generated by Django 4.2.16 on 2024-12-16 11:17 from django.db import migrations, models import django.db.models.deletion @@ -23,7 +23,7 @@ class Migration(migrations.Migration): ('updated', models.DateTimeField(auto_now=True)), ('owner', paasng.utils.models.BkUserField(blank=True, db_index=True, max_length=64, null=True)), ('credentials_enabled', models.BooleanField(default=True, verbose_name='是否使用凭证')), - ('engine_app', models.ForeignKey(db_constraint=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='engine.engineapp', verbose_name='蓝鲸引擎应用')), + ('engine_app', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, related_name='unbound_service_attachment', to='engine.engineapp', verbose_name='蓝鲸引擎应用')), ('plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='services.plan')), ('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='services.service', verbose_name='增强服务')), ('service_instance', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='services.serviceinstance', verbose_name='增强服务实例')), diff --git a/apiserver/paasng/paasng/accessories/servicehub/models.py b/apiserver/paasng/paasng/accessories/servicehub/models.py index 64b9897b8b..88a0540e8d 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/models.py +++ b/apiserver/paasng/paasng/accessories/servicehub/models.py @@ -129,7 +129,6 @@ class UnboundServiceEngineAppAttachment(OwnerTimestampedModel): engine_app = models.ForeignKey( "engine.EngineApp", on_delete=models.CASCADE, - null=True, db_constraint=False, verbose_name="蓝鲸引擎应用", related_name="unbound_service_attachment", diff --git a/apiserver/paasng/tests/api/test_servicehub.py b/apiserver/paasng/tests/api/test_servicehub.py index ea29b81a8f..b1cce6d1e1 100644 --- a/apiserver/paasng/tests/api/test_servicehub.py +++ b/apiserver/paasng/tests/api/test_servicehub.py @@ -23,9 +23,11 @@ from django_dynamic_fixture import G from rest_framework import status +from paasng.accessories.servicehub.local.manager import LocalServiceObj from paasng.accessories.servicehub.models import RemoteServiceEngineAppAttachment +from paasng.accessories.servicehub.remote.manager import RemoteServiceObj from paasng.accessories.servicehub.services import ServiceInstanceObj -from paasng.accessories.services.models import Service +from paasng.accessories.services.models import Service, ServiceCategory pytestmark = pytest.mark.django_db @@ -107,14 +109,13 @@ def create_mock_rel(self, service, credentials_enabled, create_time, **credentia rel.get_plan.return_value = mock.MagicMock(spec=["specifications"]) rel.get_plan.return_value.specifications = {"name": "version"} rel.get_service.return_value = service + rel.db_obj.service_id = str(service.uuid) rel.db_obj.credentials_enabled = credentials_enabled return rel @mock.patch("paasng.accessories.servicehub.views.mixed_service_mgr.list_unbound_instance_rels") @mock.patch("paasng.accessories.servicehub.views.mixed_service_mgr.get_or_404") - def test_retrieve_unbound_service_instances( - self, mock_get_or_404, mock_list_unbound_instance_rels, api_client, bk_app, bk_module - ): + def test_list_by_service(self, mock_get_or_404, mock_list_unbound_instance_rels, api_client, bk_app, bk_module): service = G(Service) mock_get_or_404.return_value = service @@ -128,8 +129,61 @@ def test_retrieve_unbound_service_instances( assert response.status_code == status.HTTP_200_OK response_data = response.json() - print(response_data) assert response_data["count"] == 4 - assert len(response_data["results"]) == 4 assert response_data["results"][0]["service_instance"]["credentials"] == '{"a": 1, "b": 2}' assert response_data["results"][3]["service_specs"] == {"name": "version"} + + @mock.patch("paasng.accessories.servicehub.views.mixed_service_mgr.list_unbound_instance_rels") + @mock.patch("paasng.accessories.servicehub.views.mixed_service_mgr.get_or_404") + def test_list_by_module(self, mock_get_or_404, mock_list_unbound_instance_rels, api_client, bk_app, bk_module): + service1 = G( + Service, uuid=uuid.uuid4(), logo_b64="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQCAYAAAC" + ) + service_category = G(ServiceCategory) + service2 = G( + Service, uuid=uuid.uuid4(), logo_b64="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQCAYAAAC" + ) + service2_dict = vars(service2) + service2_dict["category"] = service_category.id + mock_get_or_404.side_effect = ( + lambda service_id, region: LocalServiceObj.from_db_object(service1) + if service_id == str(service1.uuid) + else RemoteServiceObj.from_data(service2_dict) + ) + + mock_rel1 = self.create_mock_rel(service1, True, datetime.datetime(2020, 1, 1), a=1, b=1) + mock_rel2 = self.create_mock_rel(service1, False, datetime.datetime(2020, 1, 1), c=1) + mock_rel3 = self.create_mock_rel(service2, True, datetime.datetime(2020, 1, 1), d=1, e=1) + mock_rel4 = self.create_mock_rel(service2, False, datetime.datetime(2020, 1, 1), f=1) + mock_list_unbound_instance_rels.return_value = [mock_rel1, mock_rel2, mock_rel3, mock_rel4] + + url = f"/api/bkapps/applications/{bk_app.code}/modules/{bk_module.name}/services/attachments/unbound/" + + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK + response_data = response.json() + assert len(response_data) == 2 + assert response_data[0]["service"]["uuid"] == str(service1.uuid) + assert response_data[0]["count"] == 4 + assert response_data[0]["unbound_instances"][0] == { + "service_instance": { + "config": {}, + "credentials": '{"a": 1, "b": 1}', + "sensitive_fields": [], + "hidden_fields": {}, + }, + "environment": "prod", + "environment_name": "生产环境", + "usage": "{}", + "service_specs": {"name": "version"}, + } + assert response_data[1]["service"]["uuid"] == str(service2.uuid) + assert response_data[1]["count"] == 4 + assert response_data[1]["unbound_instances"][3] == { + "service_instance": {"config": {}, "credentials": '{"f": 1}', "sensitive_fields": [], "hidden_fields": {}}, + "environment": "stag", + "environment_name": "预发布环境", + "usage": "{}", + "service_specs": {"name": "version"}, + } From 1f666d6080148200f145e9d2a6a7fedbb1115fb8 Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Tue, 17 Dec 2024 19:23:58 +0800 Subject: [PATCH 11/31] fix: fixes --- .../accessories/servicehub/local/manager.py | 16 +++++----------- ...clean_recycled_unbound_remote_instances.py} | 8 ++++---- .../accessories/servicehub/remote/client.py | 15 +++++++++++++++ .../accessories/servicehub/remote/manager.py | 18 +++++++++++++++--- .../paasng/accessories/servicehub/services.py | 4 ---- .../paasng/accessories/servicehub/tasks.py | 4 ++-- 6 files changed, 41 insertions(+), 24 deletions(-) rename apiserver/paasng/paasng/accessories/servicehub/management/commands/{check_unbound_remote_instance.py => clean_recycled_unbound_remote_instances.py} (79%) diff --git a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py index 137574e430..707af020f5 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py @@ -57,7 +57,7 @@ ServiceSpecificationHelper, UnboundEngineAppInstanceRel, ) -from paasng.accessories.services.models import Plan, Service, ServiceInstance +from paasng.accessories.services.models import Plan, Service from paasng.misc.metrics import SERVICE_PROVISION_COUNTER from paasng.platform.applications.models import ModuleEnvironment from paasng.platform.engine.constants import AppEnvName @@ -176,13 +176,16 @@ def recycle_resource(self): self.db_obj.clean_service_instance() def mark_unbound(self): - UnboundServiceEngineAppAttachment.objects.create( + att = UnboundServiceEngineAppAttachment.objects.create( engine_app=self.db_obj.engine_app, service=self.db_obj.service, plan=self.db_obj.plan, service_instance=self.db_obj.service_instance, credentials_enabled=self.db_obj.credentials_enabled, ) + logger.info( + f"Create unbound remote service engine app attachment: service id: {att.service_id}, service instance id: {att.service_instance_id}" + ) def get_instance(self) -> ServiceInstanceObj: """Get service instance object""" @@ -216,16 +219,7 @@ def __init__(self, db_obj: UnboundServiceEngineAppAttachment): def get_service(self) -> LocalServiceObj: return LocalServiceObj.from_db_object(self.db_obj.service) - def is_recycled(self) -> bool: - if not ServiceInstance.objects.filter(uuid=self.db_obj.service_instance).exists(): - self.db_obj.delete() - return True - return False - def recycle_resource(self) -> None: - if self.is_recycled(): - return - self.db_obj.clean_service_instance() def get_instance(self) -> ServiceInstanceObj: diff --git a/apiserver/paasng/paasng/accessories/servicehub/management/commands/check_unbound_remote_instance.py b/apiserver/paasng/paasng/accessories/servicehub/management/commands/clean_recycled_unbound_remote_instances.py similarity index 79% rename from apiserver/paasng/paasng/accessories/servicehub/management/commands/check_unbound_remote_instance.py rename to apiserver/paasng/paasng/accessories/servicehub/management/commands/clean_recycled_unbound_remote_instances.py index 5eff1dc4dc..a3684648e5 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/management/commands/check_unbound_remote_instance.py +++ b/apiserver/paasng/paasng/accessories/servicehub/management/commands/clean_recycled_unbound_remote_instances.py @@ -19,7 +19,7 @@ from django.core.management.base import BaseCommand -from paasng.accessories.servicehub.tasks import check_is_unbound_remote_service_instance_recycled +from paasng.accessories.servicehub.tasks import clean_recycled_unbound_remote_instances logger = logging.getLogger(__name__) @@ -28,6 +28,6 @@ class Command(BaseCommand): help = "Check if unbound remote service instance is recycled, if it is recycled, delete object in database." def handle(self, *args, **options): - print("Start to check if remote unbound service instance recycled") - check_is_unbound_remote_service_instance_recycled() - print("Complete check if remote unbound service instance recycled") + logger.info("Start to check if remote unbound service instance recycled") + clean_recycled_unbound_remote_instances() + logger.info("Complete check if remote unbound service instance recycled") diff --git a/apiserver/paasng/paasng/accessories/servicehub/remote/client.py b/apiserver/paasng/paasng/accessories/servicehub/remote/client.py index 112626801d..e4c4bed8a6 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/remote/client.py +++ b/apiserver/paasng/paasng/accessories/servicehub/remote/client.py @@ -81,6 +81,9 @@ def __post_init__(self): self.update_plan_url = urljoin(self.endpoint_url, "plans/{plan_id}/") self.retrieve_instance_url = urljoin(self.endpoint_url, "instances/{instance_id}/") + self.retrieve_instance_to_be_delete_url = urljoin( + self.endpoint_url, "instances/{instance_id}/?to_be_delete={to_be_delete}" + ) self.retrieve_instance_by_name_url = urljoin(self.endpoint_url, "services/{service_id}/instances/?name={name}") self.update_inst_config_url = urljoin(self.endpoint_url, "instances/{instance_id}/config/") self.create_instance_url = urljoin(self.endpoint_url, "services/{service_id}/instances/{instance_id}/") @@ -224,6 +227,18 @@ def retrieve_instance(self, instance_id: str) -> Dict: self.validate_resp(resp) return resp.json() + def retrieve_instance_to_be_delete(self, instance_id: str) -> Dict: + """Retrieve a provisioned instance info + + :raises: RemoteClientError + :return: + """ + url = self.config.retrieve_instance_to_be_delete_url.format(instance_id=instance_id, to_be_delete=True) + with wrap_request_exc(self): + resp = requests.get(url, auth=self.auth, timeout=self.REQUEST_LIST_TIMEOUT) + self.validate_resp(resp) + return resp.json() + def retrieve_instance_by_name(self, service_id: str, instance_name: str) -> Dict: """Retrieve a provisioned instance info by name diff --git a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py index d89c8c1672..1ac1cf66f6 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py @@ -300,13 +300,16 @@ def recycle_resource(self): self.db_obj.save() def mark_unbound(self): - UnboundRemoteServiceEngineAppAttachment.objects.create( + att = UnboundRemoteServiceEngineAppAttachment.objects.create( engine_app=self.db_engine_app, service_id=self.db_obj.service_id, plan_id=self.db_obj.plan_id, service_instance_id=self.db_obj.service_instance_id, credentials_enabled=self.db_obj.credentials_enabled, ) + logger.info( + f"Create unbound remote service engine app attachment: service id: {att.service_id}, service instance id: {att.service_instance_id}" + ) def get_instance(self) -> ServiceInstanceObj: """Get service instance object""" @@ -401,10 +404,13 @@ def get_service(self) -> RemoteServiceObj: def get_instance(self) -> ServiceInstanceObj: """Get service instance object""" try: - instance_data = self.remote_client.retrieve_instance(str(self.db_obj.service_instance_id)) + instance_data = self.remote_client.retrieve_instance_to_be_delete(str(self.db_obj.service_instance_id)) except RClientResponseError as e: # if not find service instance with this id, remote response http status code 404 if e.status_code == 404: + logger.info( + f"Unbound remote service instance is recycled, service_id: {self.db_obj.service_id}, service_instance_id: {self.db_obj.service_instance_id}" + ) self.db_obj.delete() raise SvcInstanceNotFound(f"service instance {self.db_obj.service_instance_id} not found") raise @@ -421,10 +427,13 @@ def get_instance(self) -> ServiceInstanceObj: def is_recycled(self) -> bool: try: - self.remote_client.retrieve_instance(str(self.db_obj.service_instance_id)) + self.remote_client.retrieve_instance_to_be_delete(str(self.db_obj.service_instance_id)) except RClientResponseError as e: # if not find service instance with this id, remote response http status code 404 if e.status_code == 404: + logger.info( + f"Unbound remote service instance is recycled, service_id: {self.db_obj.service_id}, service_instance_id: {self.db_obj.service_instance_id}" + ) self.db_obj.delete() return True raise @@ -436,6 +445,9 @@ def recycle_resource(self) -> None: try: self.remote_client.delete_instance_synchronously(instance_id=str(self.db_obj.service_instance_id)) + logger.info( + f"Unbound remote service instance is recycled, service_id: {self.db_obj.service_id}, service_instance_id: {self.db_obj.service_instance_id}" + ) self.db_obj.delete() except Exception as e: logger.exception("Error occurs during recycling") diff --git a/apiserver/paasng/paasng/accessories/servicehub/services.py b/apiserver/paasng/paasng/accessories/servicehub/services.py index cd74e56707..152874d65a 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/services.py +++ b/apiserver/paasng/paasng/accessories/servicehub/services.py @@ -235,10 +235,6 @@ def get_instance(self) -> ServiceInstanceObj: def get_plan(self) -> PlanObj: raise NotImplementedError - @abstractmethod - def is_recycled(self) -> bool: - raise NotImplementedError - @abstractmethod def recycle_resource(self): """Recycle resources""" diff --git a/apiserver/paasng/paasng/accessories/servicehub/tasks.py b/apiserver/paasng/paasng/accessories/servicehub/tasks.py index 4b33fb6027..fa9b355f9e 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/tasks.py +++ b/apiserver/paasng/paasng/accessories/servicehub/tasks.py @@ -51,7 +51,7 @@ def clean_instances(): logger.info(f"instance<{uuid}> cleaned. ") -def check_is_unbound_remote_service_instance_recycled(): +def clean_recycled_unbound_remote_instances(): store = get_remote_store() unbound_instances = UnboundRemoteServiceEngineAppAttachment.objects.all() @@ -68,7 +68,7 @@ def check_is_unbound_remote_service_instance_recycled(): remote_client = RemoteServiceClient(remote_config) for instance in instances: try: - remote_client.retrieve_instance(instance.service_instance_id) + remote_client.retrieve_instance_to_be_delete(instance.service_instance_id) except RClientResponseError as e: # if not find service instance with this id, remote response http status code 404 if e.status_code == 404: From 7937e83ce5448899540c7346f10b1d49df54ad42 Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Tue, 17 Dec 2024 21:37:34 +0800 Subject: [PATCH 12/31] fix: fixes --- .../accessories/servicehub/local/manager.py | 24 ++++----- .../accessories/servicehub/remote/manager.py | 54 ++++++++----------- .../paasng/accessories/servicehub/services.py | 5 -- .../paasng/accessories/servicehub/views.py | 33 ------------ 4 files changed, 34 insertions(+), 82 deletions(-) diff --git a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py index 707af020f5..93f521c4fa 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py @@ -172,20 +172,18 @@ def provision(self): def recycle_resource(self): if self.is_provisioned() and self.db_obj.service.prefer_async_delete: - self.mark_unbound() - self.db_obj.clean_service_instance() + att = UnboundServiceEngineAppAttachment.objects.create( + engine_app=self.db_obj.engine_app, + service=self.db_obj.service, + plan=self.db_obj.plan, + service_instance=self.db_obj.service_instance, + credentials_enabled=self.db_obj.credentials_enabled, + ) + logger.info( + f"Create unbound remote service engine app attachment: service id: {att.service_id}, service instance id: {att.service_instance_id}" + ) - def mark_unbound(self): - att = UnboundServiceEngineAppAttachment.objects.create( - engine_app=self.db_obj.engine_app, - service=self.db_obj.service, - plan=self.db_obj.plan, - service_instance=self.db_obj.service_instance, - credentials_enabled=self.db_obj.credentials_enabled, - ) - logger.info( - f"Create unbound remote service engine app attachment: service id: {att.service_id}, service instance id: {att.service_instance_id}" - ) + self.db_obj.clean_service_instance() def get_instance(self) -> ServiceInstanceObj: """Get service instance object""" diff --git a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py index 1ac1cf66f6..0c6cb51ce2 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py @@ -286,6 +286,7 @@ def sync_instance_config(self): except Exception: logger.exception(f"Error when updating instance config for {instance_id}") + @atomic def recycle_resource(self): """对于 remote service 我们默认其已经具备了回收的能力""" if self.is_provisioned(): @@ -294,23 +295,22 @@ def recycle_resource(self): except Exception as e: logger.exception("Error occurs during recycling") raise exceptions.SvcInstanceDeleteError("unable to delete instance") from e - if self.remote_client.config.prefer_async_delete: - self.mark_unbound() + + if self.remote_client.config.prefer_async_delete: + att = UnboundRemoteServiceEngineAppAttachment.objects.create( + engine_app=self.db_engine_app, + service_id=self.db_obj.service_id, + plan_id=self.db_obj.plan_id, + service_instance_id=self.db_obj.service_instance_id, + credentials_enabled=self.db_obj.credentials_enabled, + ) + logger.info( + f"Create unbound remote service engine app attachment: service id: {att.service_id}, service instance id: {att.service_instance_id}" + ) + self.db_obj.service_instance_id = None self.db_obj.save() - def mark_unbound(self): - att = UnboundRemoteServiceEngineAppAttachment.objects.create( - engine_app=self.db_engine_app, - service_id=self.db_obj.service_id, - plan_id=self.db_obj.plan_id, - service_instance_id=self.db_obj.service_instance_id, - credentials_enabled=self.db_obj.credentials_enabled, - ) - logger.info( - f"Create unbound remote service engine app attachment: service id: {att.service_id}, service instance id: {att.service_instance_id}" - ) - def get_instance(self) -> ServiceInstanceObj: """Get service instance object""" if not self.is_provisioned(): @@ -401,8 +401,7 @@ def __init__( def get_service(self) -> RemoteServiceObj: return self.mgr.get(str(self.db_obj.service_id), region=self.db_application.region) - def get_instance(self) -> ServiceInstanceObj: - """Get service instance object""" + def retrieve_instance_to_be_delete(self) -> dict: try: instance_data = self.remote_client.retrieve_instance_to_be_delete(str(self.db_obj.service_instance_id)) except RClientResponseError as e: @@ -414,6 +413,11 @@ def get_instance(self) -> ServiceInstanceObj: self.db_obj.delete() raise SvcInstanceNotFound(f"service instance {self.db_obj.service_instance_id} not found") raise + return instance_data + + def get_instance(self) -> ServiceInstanceObj: + """Get service instance object""" + instance_data = self.retrieve_instance_to_be_delete() svc_obj = self.get_service() create_time = arrow.get(instance_data.get("created")) # type: ignore @@ -425,22 +429,10 @@ def get_instance(self) -> ServiceInstanceObj: create_time=create_time.datetime, ) - def is_recycled(self) -> bool: - try: - self.remote_client.retrieve_instance_to_be_delete(str(self.db_obj.service_instance_id)) - except RClientResponseError as e: - # if not find service instance with this id, remote response http status code 404 - if e.status_code == 404: - logger.info( - f"Unbound remote service instance is recycled, service_id: {self.db_obj.service_id}, service_instance_id: {self.db_obj.service_instance_id}" - ) - self.db_obj.delete() - return True - raise - return False - def recycle_resource(self) -> None: - if self.is_recycled(): + try: + self.retrieve_instance_to_be_delete() + except SvcInstanceNotFound: return try: diff --git a/apiserver/paasng/paasng/accessories/servicehub/services.py b/apiserver/paasng/paasng/accessories/servicehub/services.py index 152874d65a..e4fcaa8b1b 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/services.py +++ b/apiserver/paasng/paasng/accessories/servicehub/services.py @@ -212,11 +212,6 @@ def recycle_resource(self): """Recycle resources but do not unbind the service""" raise NotImplementedError - @abstractmethod - def mark_unbound(self): - """Add to unbound instances, which is unbound with engine app, could be recycled later""" - raise NotImplementedError - class UnboundEngineAppInstanceRel(metaclass=ABCMeta): """A provinsioned instance which is unbound with engine app""" diff --git a/apiserver/paasng/paasng/accessories/servicehub/views.py b/apiserver/paasng/paasng/accessories/servicehub/views.py index f46e92b013..1a07484a22 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/views.py +++ b/apiserver/paasng/paasng/accessories/servicehub/views.py @@ -226,7 +226,6 @@ def retrieve_specs(self, request, code, module_name, service_id): return Response({"results": slz.data}) @app_action_required(AppAction.MANAGE_ADDONS_SERVICES) - @transaction.atomic def unbind(self, request, code, module_name, service_id): """删除一个服务绑定关系""" application = self._get_application_by_code(code) @@ -762,38 +761,6 @@ def update(self, request, code, module_name, service_id): class UnboundServiceEngineAppAttachmentViewSet(viewsets.ViewSet, ApplicationCodeInPathMixin): """已解绑待回收增强服务实例相关API""" - @staticmethod - def get_service(service_id, application): - return mixed_service_mgr.get_or_404(service_id, region=application.region) - - @app_action_required(AppAction.BASIC_DEVELOP) - def list_by_service(self, request, code, module_name, service_id): - """查看应用模块与增强服务已解绑实例详情""" - application = self.get_application() - module = self.get_module_via_path() - service = self.get_service(service_id, application) - - results = [] - for env in module.envs.all(): - for rel in mixed_service_mgr.list_unbound_instance_rels(env.engine_app, service=service): - try: - instance = rel.get_instance() - except SvcInstanceNotFound: - # 如果已经回收了,获取不到 instance,跳过 - continue - plan = rel.get_plan() - results.append( - { - "service_instance": instance, - "environment": env.environment, - "environment_name": AppEnvName.get_choice_label(env.environment), - "service_specs": plan.specifications, - "usage": "{}", - } - ) - serializer = slzs.ServiceInstanceInfoSLZ(results, many=True) - return Response({"count": len(results), "results": serializer.data}) - @app_action_required(AppAction.BASIC_DEVELOP) def list_by_module(self, request, code, module_name): """查看模块所有已解绑增强服务实例,按增强服务归类""" From c1ea1eb22bb754f169befda7951bf86a1ff97eff Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Tue, 17 Dec 2024 21:47:31 +0800 Subject: [PATCH 13/31] fix: rm list_by_service --- .../paasng/accessories/servicehub/urls.py | 10 ++------- apiserver/paasng/tests/api/test_servicehub.py | 22 +------------------ 2 files changed, 3 insertions(+), 29 deletions(-) diff --git a/apiserver/paasng/paasng/accessories/servicehub/urls.py b/apiserver/paasng/paasng/accessories/servicehub/urls.py index 9ca83ce974..c103d90d13 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/urls.py +++ b/apiserver/paasng/paasng/accessories/servicehub/urls.py @@ -135,17 +135,11 @@ views.RelatedApplicationsInfoViewSet.as_view({"get": "retrieve_related_applications_info"}), name="api.services.mysql.retrieve_related_applications_info", ), - # retrieve unbound instances by module and service_id - re_path( - make_app_pattern(f"/services/{SERVICE_UUID}/attachments/unbound/$", include_envs=False), - views.UnboundServiceEngineAppAttachmentViewSet.as_view({"get": "list_by_service"}), - name="api.services.attachment.unbound.list_by_service", - ), # List unbound instances by module re_path( - make_app_pattern("/services/attachments/unbound/$", include_envs=False), + make_app_pattern("/services/unbound_attachments/$", include_envs=False), views.UnboundServiceEngineAppAttachmentViewSet.as_view({"get": "list_by_module"}), - name="api.services.attachment.unbound.list_by_module", + name="api.services.attachment.unbound", ), # Recycle unbound instance re_path( diff --git a/apiserver/paasng/tests/api/test_servicehub.py b/apiserver/paasng/tests/api/test_servicehub.py index b1cce6d1e1..e89e2be7dc 100644 --- a/apiserver/paasng/tests/api/test_servicehub.py +++ b/apiserver/paasng/tests/api/test_servicehub.py @@ -113,26 +113,6 @@ def create_mock_rel(self, service, credentials_enabled, create_time, **credentia rel.db_obj.credentials_enabled = credentials_enabled return rel - @mock.patch("paasng.accessories.servicehub.views.mixed_service_mgr.list_unbound_instance_rels") - @mock.patch("paasng.accessories.servicehub.views.mixed_service_mgr.get_or_404") - def test_list_by_service(self, mock_get_or_404, mock_list_unbound_instance_rels, api_client, bk_app, bk_module): - service = G(Service) - mock_get_or_404.return_value = service - - mock_rel1 = self.create_mock_rel(service, True, datetime.datetime(2020, 1, 1), a=1, b=2) - mock_rel2 = self.create_mock_rel(service, False, datetime.datetime(2020, 1, 1), c=3) - mock_list_unbound_instance_rels.return_value = [mock_rel1, mock_rel2] - - url = f"/api/bkapps/applications/{bk_app.code}/modules/{bk_module.name}/services/{str(service.uuid)}/attachments/unbound/" - - response = api_client.get(url) - - assert response.status_code == status.HTTP_200_OK - response_data = response.json() - assert response_data["count"] == 4 - assert response_data["results"][0]["service_instance"]["credentials"] == '{"a": 1, "b": 2}' - assert response_data["results"][3]["service_specs"] == {"name": "version"} - @mock.patch("paasng.accessories.servicehub.views.mixed_service_mgr.list_unbound_instance_rels") @mock.patch("paasng.accessories.servicehub.views.mixed_service_mgr.get_or_404") def test_list_by_module(self, mock_get_or_404, mock_list_unbound_instance_rels, api_client, bk_app, bk_module): @@ -157,7 +137,7 @@ def test_list_by_module(self, mock_get_or_404, mock_list_unbound_instance_rels, mock_rel4 = self.create_mock_rel(service2, False, datetime.datetime(2020, 1, 1), f=1) mock_list_unbound_instance_rels.return_value = [mock_rel1, mock_rel2, mock_rel3, mock_rel4] - url = f"/api/bkapps/applications/{bk_app.code}/modules/{bk_module.name}/services/attachments/unbound/" + url = f"/api/bkapps/applications/{bk_app.code}/modules/{bk_module.name}/services/unbound_attachments/" response = api_client.get(url) From e0b048cf87f25b8e2bb5f3b490cda78d02697682 Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Wed, 18 Dec 2024 16:39:17 +0800 Subject: [PATCH 14/31] fix: simplify model, rm unnesessary fields --- .../accessories/servicehub/local/manager.py | 7 +----- .../paasng/accessories/servicehub/models.py | 6 ----- .../accessories/servicehub/remote/manager.py | 24 ++++--------------- .../accessories/servicehub/serializers.py | 12 +++++++++- .../paasng/accessories/servicehub/services.py | 8 ------- .../paasng/accessories/servicehub/urls.py | 5 ++-- .../paasng/accessories/servicehub/views.py | 11 +++++---- apiserver/paasng/tests/api/test_servicehub.py | 6 ++--- 8 files changed, 26 insertions(+), 53 deletions(-) diff --git a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py index 93f521c4fa..da6cf0950a 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py @@ -170,6 +170,7 @@ def provision(self): environment=db_env.environment, service=self.db_obj.service.name, plan=self.db_obj.plan.name ).inc() + @atomic def recycle_resource(self): if self.is_provisioned() and self.db_obj.service.prefer_async_delete: att = UnboundServiceEngineAppAttachment.objects.create( @@ -214,9 +215,6 @@ class UnboundLocalEngineAppInstanceRel(UnboundEngineAppInstanceRel): def __init__(self, db_obj: UnboundServiceEngineAppAttachment): self.db_obj = db_obj - def get_service(self) -> LocalServiceObj: - return LocalServiceObj.from_db_object(self.db_obj.service) - def recycle_resource(self) -> None: self.db_obj.clean_service_instance() @@ -236,9 +234,6 @@ def get_instance(self) -> ServiceInstanceObj: create_time=self.db_obj.created, ) - def get_plan(self) -> LocalPlanObj: - return LocalPlanObj.from_db(self.db_obj.plan) - class LocalServiceMgr(BaseServiceMgr): """Local in-database services manager""" diff --git a/apiserver/paasng/paasng/accessories/servicehub/models.py b/apiserver/paasng/paasng/accessories/servicehub/models.py index 88a0540e8d..66e84e38c8 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/models.py +++ b/apiserver/paasng/paasng/accessories/servicehub/models.py @@ -134,14 +134,11 @@ class UnboundServiceEngineAppAttachment(OwnerTimestampedModel): related_name="unbound_service_attachment", ) service = models.ForeignKey(Service, on_delete=models.CASCADE, verbose_name="增强服务") - plan = models.ForeignKey(Plan, on_delete=models.CASCADE) service_instance = models.ForeignKey( ServiceInstance, on_delete=models.CASCADE, null=True, blank=True, verbose_name="增强服务实例" ) - credentials_enabled = models.BooleanField(default=True, verbose_name="是否使用凭证") class Meta: - unique_together = ("service", "engine_app") verbose_name = "本地已解绑增强服务" def clean_service_instance(self): @@ -188,12 +185,9 @@ class UnboundRemoteServiceEngineAppAttachment(OwnerTimestampedModel): related_name="unbound_remote_service_attachment", ) service_id = models.UUIDField(verbose_name="远程增强服务 ID") - plan_id = models.UUIDField(verbose_name="远程增强服务 Plan ID") service_instance_id = models.UUIDField(null=True, verbose_name="远程增强服务实例 ID") - credentials_enabled = models.BooleanField(default=True, verbose_name="是否使用凭证") class Meta: - unique_together = ("service_id", "engine_app") verbose_name = "远程已解绑增强服务" diff --git a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py index 0c6cb51ce2..0cdd80ae8f 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py @@ -398,9 +398,6 @@ def __init__( self.remote_config = self.store.get_source_config(str(self.db_obj.service_id)) self.remote_client = RemoteServiceClient(self.remote_config) - def get_service(self) -> RemoteServiceObj: - return self.mgr.get(str(self.db_obj.service_id), region=self.db_application.region) - def retrieve_instance_to_be_delete(self) -> dict: try: instance_data = self.remote_client.retrieve_instance_to_be_delete(str(self.db_obj.service_instance_id)) @@ -418,9 +415,9 @@ def retrieve_instance_to_be_delete(self) -> dict: def get_instance(self) -> ServiceInstanceObj: """Get service instance object""" instance_data = self.retrieve_instance_to_be_delete() - - svc_obj = self.get_service() + svc_obj = self.mgr.get(str(self.db_obj.service_id), region=self.db_application.region) create_time = arrow.get(instance_data.get("created")) # type: ignore + return create_svc_instance_obj_from_remote( uuid=str(self.db_obj.service_instance_id), credentials=instance_data["credentials"], @@ -437,27 +434,14 @@ def recycle_resource(self) -> None: try: self.remote_client.delete_instance_synchronously(instance_id=str(self.db_obj.service_instance_id)) + self.db_obj.delete() logger.info( - f"Unbound remote service instance is recycled, service_id: {self.db_obj.service_id}, service_instance_id: {self.db_obj.service_instance_id}" + f"Manually recycled unbound remote service instance, service_id: {self.db_obj.service_id}, service_instance_id: {self.db_obj.service_instance_id}" ) - self.db_obj.delete() except Exception as e: logger.exception("Error occurs during recycling") raise exceptions.SvcInstanceDeleteError("unable to delete instance") from e - def get_plan(self) -> RemotePlanObj: - plan_id = str(self.db_obj.plan_id) - # 兼容从v2迁移至v3的增强服务, 避免前端因此出现异常 - if plan_id == str(constants.LEGACY_PLAN_ID): - return RemotePlanObj.from_data(constants.LEGACY_PLAN_INSTANCE) - - svc_data = self.store.get(str(self.db_obj.service_id), region=self.db_application.region) - for d in svc_data["plans"]: - if d["uuid"] == plan_id: - return RemotePlanObj.from_data(d) - - raise RuntimeError("Plan not found") - class RemotePlainInstanceMgr(PlainInstanceMgr): """纯粹的远程增强服务实例的管理器, 仅调用远程接口创建增强服务实例, 不涉及增强服务资源申请的流程""" diff --git a/apiserver/paasng/paasng/accessories/servicehub/serializers.py b/apiserver/paasng/paasng/accessories/servicehub/serializers.py index bb1c222450..cb99b371a6 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/serializers.py +++ b/apiserver/paasng/paasng/accessories/servicehub/serializers.py @@ -260,10 +260,20 @@ class UpdateServiceEngineAppAttachmentSLZ(serializers.Serializer): credentials_enabled = serializers.BooleanField(help_text="是否使用凭证") +class ServiceInstanceMinimalInfoSLZ(serializers.Serializer): + service_instance = ServiceInstanceSLZ(help_text="增强服务实例信息") + environment = serializers.CharField(help_text="环境") + environment_name = serializers.CharField(help_text="环境名称") + + class UnboundServiceEngineAppAttachmentSLZ(serializers.Serializer): service = ServiceMinimalSLZ(help_text="增强服务信息") - unbound_instances = ServiceInstanceInfoSLZ(many=True, help_text="已解绑增强服务实例") + unbound_instances = ServiceInstanceMinimalInfoSLZ(many=True, help_text="已解绑增强服务实例") count = serializers.SerializerMethodField(help_text="数量") def get_count(self, obj): return len(obj.get("unbound_instances") or []) + + +class DeleteUnboundServiceEngineAppAttachmentSLZ(serializers.Serializer): + instance_id = serializers.UUIDField(help_text="解绑待回收实例ID") diff --git a/apiserver/paasng/paasng/accessories/servicehub/services.py b/apiserver/paasng/paasng/accessories/servicehub/services.py index e4fcaa8b1b..e93f0063e2 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/services.py +++ b/apiserver/paasng/paasng/accessories/servicehub/services.py @@ -218,18 +218,10 @@ class UnboundEngineAppInstanceRel(metaclass=ABCMeta): db_obj: Any - @abstractmethod - def get_service(self) -> ServiceObj: - raise NotImplementedError - @abstractmethod def get_instance(self) -> ServiceInstanceObj: raise NotImplementedError - @abstractmethod - def get_plan(self) -> PlanObj: - raise NotImplementedError - @abstractmethod def recycle_resource(self): """Recycle resources""" diff --git a/apiserver/paasng/paasng/accessories/servicehub/urls.py b/apiserver/paasng/paasng/accessories/servicehub/urls.py index c103d90d13..4641de90dc 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/urls.py +++ b/apiserver/paasng/paasng/accessories/servicehub/urls.py @@ -24,7 +24,6 @@ SERVICE_UUID = "(?P[0-9a-f-]{32,36})" APP_UUID = "(?P[0-9a-f-]{32,36})" CATEGORY_ID = r"(?P[\d]+)" -SERVICE_INTANCE_ID = "(?P[0-9a-f-]{32,36})" urlpatterns = [ # service APIs @@ -143,8 +142,8 @@ ), # Recycle unbound instance re_path( - make_app_pattern(f"/services/{SERVICE_UUID}/unbound/{SERVICE_INTANCE_ID}/$", include_envs=False), - views.UnboundServiceEngineAppAttachmentViewSet.as_view({"delete": "recycle"}), + make_app_pattern(f"/services/{SERVICE_UUID}/unbound_attachments/$", include_envs=False), + views.UnboundServiceEngineAppAttachmentViewSet.as_view({"delete": "delete"}), name="api.services.attachment.unbound.recycle", ), ] diff --git a/apiserver/paasng/paasng/accessories/servicehub/views.py b/apiserver/paasng/paasng/accessories/servicehub/views.py index 1a07484a22..830da1c50e 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/views.py +++ b/apiserver/paasng/paasng/accessories/servicehub/views.py @@ -775,15 +775,12 @@ def list_by_module(self, request, code, module_name): except SvcInstanceNotFound: # 如果已经回收了,获取不到 instance,跳过 continue - plan = rel.get_plan() categorized_rels[str(rel.db_obj.service_id)].append( { "service_instance": instance, "environment": env.environment, "environment_name": AppEnvName.get_choice_label(env.environment), - "service_specs": plan.specifications, - "usage": "{}", } ) @@ -797,10 +794,14 @@ def list_by_module(self, request, code, module_name): return Response(serializer.data) @app_action_required(AppAction.MANAGE_ADDONS_SERVICES) - def recycle(self, request, code, module_name, service_id, service_instance_id): + def delete(self, request, code, module_name, service_id): """回收已解绑增强服务""" + slz = slzs.DeleteUnboundServiceEngineAppAttachmentSLZ(data=request.data) + slz.is_valid(raise_exception=True) + data = slz.validated_data + service_obj = mixed_service_mgr.get_or_404(service_id, self.get_application().region) - unbound_instance = mixed_service_mgr.get_unbound_instance_rel_by_instance_id(service_obj, service_instance_id) + unbound_instance = mixed_service_mgr.get_unbound_instance_rel_by_instance_id(service_obj, data.instance_id) unbound_instance.recycle_instance() return Response() diff --git a/apiserver/paasng/tests/api/test_servicehub.py b/apiserver/paasng/tests/api/test_servicehub.py index e89e2be7dc..bde85d384b 100644 --- a/apiserver/paasng/tests/api/test_servicehub.py +++ b/apiserver/paasng/tests/api/test_servicehub.py @@ -143,6 +143,8 @@ def test_list_by_module(self, mock_get_or_404, mock_list_unbound_instance_rels, assert response.status_code == status.HTTP_200_OK response_data = response.json() + print("------") + print(response_data) assert len(response_data) == 2 assert response_data[0]["service"]["uuid"] == str(service1.uuid) assert response_data[0]["count"] == 4 @@ -155,8 +157,6 @@ def test_list_by_module(self, mock_get_or_404, mock_list_unbound_instance_rels, }, "environment": "prod", "environment_name": "生产环境", - "usage": "{}", - "service_specs": {"name": "version"}, } assert response_data[1]["service"]["uuid"] == str(service2.uuid) assert response_data[1]["count"] == 4 @@ -164,6 +164,4 @@ def test_list_by_module(self, mock_get_or_404, mock_list_unbound_instance_rels, "service_instance": {"config": {}, "credentials": '{"f": 1}', "sensitive_fields": [], "hidden_fields": {}}, "environment": "stag", "environment_name": "预发布环境", - "usage": "{}", - "service_specs": {"name": "version"}, } From 846066eecfe939dbf2cfd2c59e1488b6b1efcccb Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Wed, 18 Dec 2024 16:45:57 +0800 Subject: [PATCH 15/31] fix: migrations --- .../0005_unboundserviceengineappattachment_and_more.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/apiserver/paasng/paasng/accessories/servicehub/migrations/0005_unboundserviceengineappattachment_and_more.py b/apiserver/paasng/paasng/accessories/servicehub/migrations/0005_unboundserviceengineappattachment_and_more.py index 556429abd8..103957f223 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/migrations/0005_unboundserviceengineappattachment_and_more.py +++ b/apiserver/paasng/paasng/accessories/servicehub/migrations/0005_unboundserviceengineappattachment_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.16 on 2024-12-16 11:17 +# Generated by Django 4.2.16 on 2024-12-18 08:44 from django.db import migrations, models import django.db.models.deletion @@ -8,8 +8,8 @@ class Migration(migrations.Migration): dependencies = [ - ('services', '0006_alter_servicecategory_name_en'), ('engine', '0023_remove_deployment_hooks_remove_deployment_procfile'), + ('services', '0006_alter_servicecategory_name_en'), ('servicehub', '0004_auto_20240412_1723'), ] @@ -22,15 +22,12 @@ class Migration(migrations.Migration): ('created', models.DateTimeField(auto_now_add=True)), ('updated', models.DateTimeField(auto_now=True)), ('owner', paasng.utils.models.BkUserField(blank=True, db_index=True, max_length=64, null=True)), - ('credentials_enabled', models.BooleanField(default=True, verbose_name='是否使用凭证')), ('engine_app', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, related_name='unbound_service_attachment', to='engine.engineapp', verbose_name='蓝鲸引擎应用')), - ('plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='services.plan')), ('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='services.service', verbose_name='增强服务')), ('service_instance', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='services.serviceinstance', verbose_name='增强服务实例')), ], options={ 'verbose_name': '本地已解绑增强服务', - 'unique_together': {('service', 'engine_app')}, }, ), migrations.CreateModel( @@ -42,14 +39,11 @@ class Migration(migrations.Migration): ('updated', models.DateTimeField(auto_now=True)), ('owner', paasng.utils.models.BkUserField(blank=True, db_index=True, max_length=64, null=True)), ('service_id', models.UUIDField(verbose_name='远程增强服务 ID')), - ('plan_id', models.UUIDField(verbose_name='远程增强服务 Plan ID')), ('service_instance_id', models.UUIDField(null=True, verbose_name='远程增强服务实例 ID')), - ('credentials_enabled', models.BooleanField(default=True, verbose_name='是否使用凭证')), ('engine_app', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, related_name='unbound_remote_service_attachment', to='engine.engineapp', verbose_name='蓝鲸引擎应用')), ], options={ 'verbose_name': '远程已解绑增强服务', - 'unique_together': {('service_id', 'engine_app')}, }, ), ] From 763979719d36caabfa73b0281e7aa470708d0b86 Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Wed, 18 Dec 2024 19:18:07 +0800 Subject: [PATCH 16/31] fix: fixes --- .../accessories/servicehub/local/manager.py | 2 -- .../accessories/servicehub/remote/manager.py | 2 -- .../accessories/servicehub/serializers.py | 7 ++++--- .../paasng/accessories/servicehub/urls.py | 2 +- .../paasng/accessories/servicehub/views.py | 16 +++++++++++--- apiserver/paasng/tests/api/test_servicehub.py | 21 +++++++++---------- 6 files changed, 28 insertions(+), 22 deletions(-) diff --git a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py index da6cf0950a..d88f6e701e 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py @@ -176,9 +176,7 @@ def recycle_resource(self): att = UnboundServiceEngineAppAttachment.objects.create( engine_app=self.db_obj.engine_app, service=self.db_obj.service, - plan=self.db_obj.plan, service_instance=self.db_obj.service_instance, - credentials_enabled=self.db_obj.credentials_enabled, ) logger.info( f"Create unbound remote service engine app attachment: service id: {att.service_id}, service instance id: {att.service_instance_id}" diff --git a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py index 0cdd80ae8f..0784afccc9 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py @@ -300,9 +300,7 @@ def recycle_resource(self): att = UnboundRemoteServiceEngineAppAttachment.objects.create( engine_app=self.db_engine_app, service_id=self.db_obj.service_id, - plan_id=self.db_obj.plan_id, service_instance_id=self.db_obj.service_instance_id, - credentials_enabled=self.db_obj.credentials_enabled, ) logger.info( f"Create unbound remote service engine app attachment: service id: {att.service_id}, service instance id: {att.service_instance_id}" diff --git a/apiserver/paasng/paasng/accessories/servicehub/serializers.py b/apiserver/paasng/paasng/accessories/servicehub/serializers.py index cb99b371a6..7e4ce6e812 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/serializers.py +++ b/apiserver/paasng/paasng/accessories/servicehub/serializers.py @@ -260,7 +260,8 @@ class UpdateServiceEngineAppAttachmentSLZ(serializers.Serializer): credentials_enabled = serializers.BooleanField(help_text="是否使用凭证") -class ServiceInstanceMinimalInfoSLZ(serializers.Serializer): +class UnboundServiceInstanceInfoSLZ(serializers.Serializer): + instance_id = serializers.UUIDField(help_text="增强服务实例 id") service_instance = ServiceInstanceSLZ(help_text="增强服务实例信息") environment = serializers.CharField(help_text="环境") environment_name = serializers.CharField(help_text="环境名称") @@ -268,7 +269,7 @@ class ServiceInstanceMinimalInfoSLZ(serializers.Serializer): class UnboundServiceEngineAppAttachmentSLZ(serializers.Serializer): service = ServiceMinimalSLZ(help_text="增强服务信息") - unbound_instances = ServiceInstanceMinimalInfoSLZ(many=True, help_text="已解绑增强服务实例") + unbound_instances = UnboundServiceInstanceInfoSLZ(many=True, help_text="已解绑增强服务实例") count = serializers.SerializerMethodField(help_text="数量") def get_count(self, obj): @@ -276,4 +277,4 @@ def get_count(self, obj): class DeleteUnboundServiceEngineAppAttachmentSLZ(serializers.Serializer): - instance_id = serializers.UUIDField(help_text="解绑待回收实例ID") + instance_id = serializers.UUIDField(help_text="增强服务实例 id") diff --git a/apiserver/paasng/paasng/accessories/servicehub/urls.py b/apiserver/paasng/paasng/accessories/servicehub/urls.py index 4641de90dc..425b815eee 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/urls.py +++ b/apiserver/paasng/paasng/accessories/servicehub/urls.py @@ -144,7 +144,7 @@ re_path( make_app_pattern(f"/services/{SERVICE_UUID}/unbound_attachments/$", include_envs=False), views.UnboundServiceEngineAppAttachmentViewSet.as_view({"delete": "delete"}), - name="api.services.attachment.unbound.recycle", + name="api.services.attachment.unbound.delete", ), ] diff --git a/apiserver/paasng/paasng/accessories/servicehub/views.py b/apiserver/paasng/paasng/accessories/servicehub/views.py index 830da1c50e..fcf0384bb0 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/views.py +++ b/apiserver/paasng/paasng/accessories/servicehub/views.py @@ -38,6 +38,7 @@ ServiceObjNotFound, SharedAttachmentAlreadyExists, SvcInstanceNotFound, + UnboundSvcAttachmentDoesNotExist, ) from paasng.accessories.servicehub.manager import mixed_service_mgr from paasng.accessories.servicehub.models import ServiceSetGroupByName @@ -762,6 +763,7 @@ class UnboundServiceEngineAppAttachmentViewSet(viewsets.ViewSet, ApplicationCode """已解绑待回收增强服务实例相关API""" @app_action_required(AppAction.BASIC_DEVELOP) + @swagger_auto_schema(tags=["增强服务"], response_serializer=slzs.UnboundServiceEngineAppAttachmentSLZ(many=True)) def list_by_module(self, request, code, module_name): """查看模块所有已解绑增强服务实例,按增强服务归类""" application = self.get_application() @@ -778,6 +780,7 @@ def list_by_module(self, request, code, module_name): categorized_rels[str(rel.db_obj.service_id)].append( { + "instance_id": rel.db_obj.service_instance_id, "service_instance": instance, "environment": env.environment, "environment_name": AppEnvName.get_choice_label(env.environment), @@ -794,14 +797,21 @@ def list_by_module(self, request, code, module_name): return Response(serializer.data) @app_action_required(AppAction.MANAGE_ADDONS_SERVICES) + @swagger_auto_schema(tags=["增强服务"], query_serializer=slzs.DeleteUnboundServiceEngineAppAttachmentSLZ) def delete(self, request, code, module_name, service_id): """回收已解绑增强服务""" - slz = slzs.DeleteUnboundServiceEngineAppAttachmentSLZ(data=request.data) + slz = slzs.DeleteUnboundServiceEngineAppAttachmentSLZ(data=request.query_params) slz.is_valid(raise_exception=True) data = slz.validated_data service_obj = mixed_service_mgr.get_or_404(service_id, self.get_application().region) - unbound_instance = mixed_service_mgr.get_unbound_instance_rel_by_instance_id(service_obj, data.instance_id) - unbound_instance.recycle_instance() + try: + unbound_instance = mixed_service_mgr.get_unbound_instance_rel_by_instance_id( + service_obj, data["instance_id"] + ) + except UnboundSvcAttachmentDoesNotExist: + raise Http404 + + unbound_instance.recycle_resource() return Response() diff --git a/apiserver/paasng/tests/api/test_servicehub.py b/apiserver/paasng/tests/api/test_servicehub.py index bde85d384b..ffad87dead 100644 --- a/apiserver/paasng/tests/api/test_servicehub.py +++ b/apiserver/paasng/tests/api/test_servicehub.py @@ -101,16 +101,15 @@ def test_config_vars(self, list_provisioned_rels, api_client, bk_app, bk_module) class TestUnboundServiceEngineAppAttachmentViewSet: - def create_mock_rel(self, service, credentials_enabled, create_time, **credentials): + def create_mock_rel(self, service, create_time, **credentials): rel = mock.MagicMock() + instance_id = str(uuid.uuid4()) rel.get_instance.return_value = ServiceInstanceObj( - uuid=str(uuid.uuid4()), credentials=credentials, config={}, create_time=create_time + uuid=instance_id, credentials=credentials, config={}, create_time=create_time ) - rel.get_plan.return_value = mock.MagicMock(spec=["specifications"]) - rel.get_plan.return_value.specifications = {"name": "version"} rel.get_service.return_value = service rel.db_obj.service_id = str(service.uuid) - rel.db_obj.credentials_enabled = credentials_enabled + rel.db_obj.service_instance_id = instance_id return rel @mock.patch("paasng.accessories.servicehub.views.mixed_service_mgr.list_unbound_instance_rels") @@ -131,10 +130,10 @@ def test_list_by_module(self, mock_get_or_404, mock_list_unbound_instance_rels, else RemoteServiceObj.from_data(service2_dict) ) - mock_rel1 = self.create_mock_rel(service1, True, datetime.datetime(2020, 1, 1), a=1, b=1) - mock_rel2 = self.create_mock_rel(service1, False, datetime.datetime(2020, 1, 1), c=1) - mock_rel3 = self.create_mock_rel(service2, True, datetime.datetime(2020, 1, 1), d=1, e=1) - mock_rel4 = self.create_mock_rel(service2, False, datetime.datetime(2020, 1, 1), f=1) + mock_rel1 = self.create_mock_rel(service1, datetime.datetime(2020, 1, 1), a=1, b=1) + mock_rel2 = self.create_mock_rel(service1, datetime.datetime(2020, 1, 1), c=1) + mock_rel3 = self.create_mock_rel(service2, datetime.datetime(2020, 1, 1), d=1, e=1) + mock_rel4 = self.create_mock_rel(service2, datetime.datetime(2020, 1, 1), f=1) mock_list_unbound_instance_rels.return_value = [mock_rel1, mock_rel2, mock_rel3, mock_rel4] url = f"/api/bkapps/applications/{bk_app.code}/modules/{bk_module.name}/services/unbound_attachments/" @@ -143,12 +142,11 @@ def test_list_by_module(self, mock_get_or_404, mock_list_unbound_instance_rels, assert response.status_code == status.HTTP_200_OK response_data = response.json() - print("------") - print(response_data) assert len(response_data) == 2 assert response_data[0]["service"]["uuid"] == str(service1.uuid) assert response_data[0]["count"] == 4 assert response_data[0]["unbound_instances"][0] == { + "instance_id": mock_rel1.db_obj.service_instance_id, "service_instance": { "config": {}, "credentials": '{"a": 1, "b": 1}', @@ -161,6 +159,7 @@ def test_list_by_module(self, mock_get_or_404, mock_list_unbound_instance_rels, assert response_data[1]["service"]["uuid"] == str(service2.uuid) assert response_data[1]["count"] == 4 assert response_data[1]["unbound_instances"][3] == { + "instance_id": mock_rel4.db_obj.service_instance_id, "service_instance": {"config": {}, "credentials": '{"f": 1}', "sensitive_fields": [], "hidden_fields": {}}, "environment": "stag", "environment_name": "预发布环境", From b34f9a85fd9daf8ce0fa21eebbfd0b4793d8b130 Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Thu, 19 Dec 2024 14:40:15 +0800 Subject: [PATCH 17/31] fix: rename to_be_deleted --- .../paasng/paasng/accessories/servicehub/remote/client.py | 8 ++++---- .../paasng/accessories/servicehub/remote/manager.py | 8 ++++---- apiserver/paasng/paasng/accessories/servicehub/tasks.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apiserver/paasng/paasng/accessories/servicehub/remote/client.py b/apiserver/paasng/paasng/accessories/servicehub/remote/client.py index fa1822fa0c..5439a15f7f 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/remote/client.py +++ b/apiserver/paasng/paasng/accessories/servicehub/remote/client.py @@ -80,8 +80,8 @@ def __post_init__(self): self.update_plan_url = urljoin(self.endpoint_url, "plans/{plan_id}/") self.retrieve_instance_url = urljoin(self.endpoint_url, "instances/{instance_id}/") - self.retrieve_instance_to_be_delete_url = urljoin( - self.endpoint_url, "instances/{instance_id}/?to_be_delete={to_be_delete}" + self.retrieve_instance_to_be_deleted_url = urljoin( + self.endpoint_url, "instances/{instance_id}/?to_be_deleted={to_be_deleted}" ) self.retrieve_instance_by_name_url = urljoin(self.endpoint_url, "services/{service_id}/instances/?name={name}") self.update_inst_config_url = urljoin(self.endpoint_url, "instances/{instance_id}/config/") @@ -226,13 +226,13 @@ def retrieve_instance(self, instance_id: str) -> Dict: self.validate_resp(resp) return resp.json() - def retrieve_instance_to_be_delete(self, instance_id: str) -> Dict: + def retrieve_instance_to_be_deleted(self, instance_id: str) -> Dict: """Retrieve a provisioned instance info :raises: RemoteClientError :return: """ - url = self.config.retrieve_instance_to_be_delete_url.format(instance_id=instance_id, to_be_delete=True) + url = self.config.retrieve_instance_to_be_deleted_url.format(instance_id=instance_id, to_be_deleted=True) with wrap_request_exc(self): resp = requests.get(url, auth=self.auth, timeout=self.REQUEST_LIST_TIMEOUT) self.validate_resp(resp) diff --git a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py index 0784afccc9..2b44c9c18c 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py @@ -396,9 +396,9 @@ def __init__( self.remote_config = self.store.get_source_config(str(self.db_obj.service_id)) self.remote_client = RemoteServiceClient(self.remote_config) - def retrieve_instance_to_be_delete(self) -> dict: + def retrieve_instance_to_be_deleted(self) -> dict: try: - instance_data = self.remote_client.retrieve_instance_to_be_delete(str(self.db_obj.service_instance_id)) + instance_data = self.remote_client.retrieve_instance_to_be_deleted(str(self.db_obj.service_instance_id)) except RClientResponseError as e: # if not find service instance with this id, remote response http status code 404 if e.status_code == 404: @@ -412,7 +412,7 @@ def retrieve_instance_to_be_delete(self) -> dict: def get_instance(self) -> ServiceInstanceObj: """Get service instance object""" - instance_data = self.retrieve_instance_to_be_delete() + instance_data = self.retrieve_instance_to_be_deleted() svc_obj = self.mgr.get(str(self.db_obj.service_id), region=self.db_application.region) create_time = arrow.get(instance_data.get("created")) # type: ignore @@ -426,7 +426,7 @@ def get_instance(self) -> ServiceInstanceObj: def recycle_resource(self) -> None: try: - self.retrieve_instance_to_be_delete() + self.retrieve_instance_to_be_deleted() except SvcInstanceNotFound: return diff --git a/apiserver/paasng/paasng/accessories/servicehub/tasks.py b/apiserver/paasng/paasng/accessories/servicehub/tasks.py index fa9b355f9e..51c90b6757 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/tasks.py +++ b/apiserver/paasng/paasng/accessories/servicehub/tasks.py @@ -68,7 +68,7 @@ def clean_recycled_unbound_remote_instances(): remote_client = RemoteServiceClient(remote_config) for instance in instances: try: - remote_client.retrieve_instance_to_be_delete(instance.service_instance_id) + remote_client.retrieve_instance_to_be_deleted(instance.service_instance_id) except RClientResponseError as e: # if not find service instance with this id, remote response http status code 404 if e.status_code == 404: From f6363b98279c5c0b11a57e4b0931fb184962bb04 Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Fri, 20 Dec 2024 18:31:15 +0800 Subject: [PATCH 18/31] fix: unit test --- .../paasng/accessories/servicehub/services.py | 2 +- .../accessories/servicehub/test_manager.py | 87 ++++++++++++++++++- 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/apiserver/paasng/paasng/accessories/servicehub/services.py b/apiserver/paasng/paasng/accessories/servicehub/services.py index e93f0063e2..75f56a0afa 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/services.py +++ b/apiserver/paasng/paasng/accessories/servicehub/services.py @@ -200,7 +200,7 @@ def get_plan(self) -> PlanObj: raise NotImplementedError def delete(self): - """include delete rel, mark_unbound & real resource recycle""" + """include delete rel, real resource recycle. If prefer asynchronous delete, add a unbound attachment record.""" if self.is_provisioned(): self.recycle_resource() logger.info("going to delete remote service attachment from db") diff --git a/apiserver/paasng/tests/paasng/accessories/servicehub/test_manager.py b/apiserver/paasng/tests/paasng/accessories/servicehub/test_manager.py index b5c5d00832..8aec67c449 100644 --- a/apiserver/paasng/tests/paasng/accessories/servicehub/test_manager.py +++ b/apiserver/paasng/tests/paasng/accessories/servicehub/test_manager.py @@ -25,13 +25,18 @@ from django_dynamic_fixture import G from paasng.accessories.servicehub.constants import Category -from paasng.accessories.servicehub.exceptions import BindServiceNoPlansError, ServiceObjNotFound +from paasng.accessories.servicehub.exceptions import ( + BindServiceNoPlansError, + ServiceObjNotFound, + UnboundSvcAttachmentDoesNotExist, +) from paasng.accessories.servicehub.local import LocalServiceMgr, LocalServiceObj from paasng.accessories.servicehub.manager import mixed_service_mgr from paasng.accessories.servicehub.models import ServiceEngineAppAttachment from paasng.accessories.servicehub.remote import RemoteServiceObj from paasng.accessories.servicehub.services import ServiceInstanceObj from paasng.accessories.services.models import Plan, Service, ServiceCategory, ServiceInstance +from paasng.platform.modules.manager import ModuleCleaner from tests.paasng.accessories.servicehub import data_mocks pytestmark = [pytest.mark.django_db, pytest.mark.xdist_group(name="remote-services")] @@ -255,6 +260,86 @@ def test_get_attachment_by_instance_id(self, bk_module): for service_instance_id in expect_obj: assert mgr.get_attachment_by_instance_id(svc, service_instance_id) == expect_obj[service_instance_id] + @mock.patch("paasng.accessories.services.models.Service.delete_service_instance") + @mock.patch("paasng.accessories.services.models.Service.create_service_instance_by_plan") + def test_list_unbound_instance_rels( + self, create_service_instance_by_plan, delete_service_instance, instance_factory, svc, bk_app, bk_module + ): + """Test service instance provision""" + create_service_instance_by_plan.side_effect = [instance_factory(), instance_factory()] + delete_service_instance.side_effect = ( + lambda service_instance: service_instance.delete() if service_instance else None + ) + + mgr = LocalServiceMgr() + service = mgr.get(svc.uuid, region=bk_module.region) + mgr.bind_service(service, bk_module) + + attachments: Dict[UUID, ServiceEngineAppAttachment] = {} + for env in bk_app.envs.all(): + for rel in mgr.list_unprovisioned_rels(env.engine_app): + rel.provision() + attachments[rel.db_obj.service_instance_id] = rel.db_obj + + assert len(attachments) == 2 + + cleaner = ModuleCleaner(bk_module) + cleaner.delete_services(svc.uuid) + + unbound_rels = [] + for env in bk_app.envs.all(): + for unbound_rel in mgr.list_unbound_instance_rels(env.engine_app): + assert unbound_rel.db_obj.service_instance_id in attachments + assert unbound_rel.db_obj.service_id == attachments[unbound_rel.db_obj.service_instance_id].service_id + assert unbound_rel.db_obj.engine_app == attachments[unbound_rel.db_obj.service_instance_id].engine_app + unbound_rels.append(unbound_rel) + + assert len(unbound_rels) == len(attachments) + + for unbound_rel in unbound_rels: + unbound_rel.recycle_resource() + + for env in bk_app.envs.all(): + for _rel in mgr.list_unbound_instance_rels(env.engine_app): + pytest.fail("Expect no return unbound instance rels after recycle resource") + + @mock.patch("paasng.accessories.services.models.Service.delete_service_instance") + @mock.patch("paasng.accessories.services.models.Service.create_service_instance_by_plan") + def test_get_unbound_instance_rel_by_instance_id( + self, create_service_instance_by_plan, delete_service_instance, instance_factory, svc, bk_app, bk_module + ): + """Test service instance provision""" + create_service_instance_by_plan.side_effect = [instance_factory(), instance_factory()] + delete_service_instance.side_effect = ( + lambda service_instance: service_instance.delete() if service_instance else None + ) + + mgr = LocalServiceMgr() + service = mgr.get(svc.uuid, region=bk_module.region) + mgr.bind_service(service, bk_module) + + attachments: Dict[UUID, ServiceEngineAppAttachment] = {} + for env in bk_app.envs.all(): + for rel in mgr.list_unprovisioned_rels(env.engine_app): + rel.provision() + attachments[rel.db_obj.service_instance_id] = rel.db_obj + + assert len(attachments) == 2 + + cleaner = ModuleCleaner(bk_module) + cleaner.delete_services(svc.uuid) + + for instance_id in attachments: + rel = mgr.get_unbound_instance_rel_by_instance_id(svc, instance_id) + assert rel.db_obj.service_instance_id in attachments + assert rel.db_obj.service_id == attachments[rel.db_obj.service_instance_id].service_id + assert rel.db_obj.engine_app == attachments[rel.db_obj.service_instance_id].engine_app + rel.recycle_resource() + + for instance_id in attachments: + with pytest.raises(UnboundSvcAttachmentDoesNotExist): + mgr.get_unbound_instance_rel_by_instance_id(svc, instance_id) + class TestLocalRabbitMQMgr: @pytest.fixture(autouse=True) From ae37ff13260dc6a454a5eeb6d4b1cfc82f5ecc35 Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Mon, 23 Dec 2024 10:31:34 +0800 Subject: [PATCH 19/31] fix: fixes from code review --- .../accessories/servicehub/local/manager.py | 22 ++++---- ...heck_unbound_remote_services_recycling.py} | 6 +-- ...oundserviceengineappattachment_and_more.py | 4 +- .../paasng/accessories/servicehub/models.py | 2 + .../accessories/servicehub/remote/manager.py | 51 +++++++++---------- .../accessories/servicehub/serializers.py | 2 +- .../paasng/accessories/servicehub/services.py | 2 +- .../paasng/accessories/servicehub/tasks.py | 2 +- .../paasng/accessories/servicehub/views.py | 6 +-- .../servicehub/remote/test_client.py | 11 ++++ 10 files changed, 56 insertions(+), 52 deletions(-) rename apiserver/paasng/paasng/accessories/servicehub/management/commands/{clean_recycled_unbound_remote_instances.py => check_unbound_remote_services_recycling.py} (86%) diff --git a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py index d88f6e701e..f9dbb1bdc9 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py @@ -170,19 +170,15 @@ def provision(self): environment=db_env.environment, service=self.db_obj.service.name, plan=self.db_obj.plan.name ).inc() - @atomic def recycle_resource(self): - if self.is_provisioned() and self.db_obj.service.prefer_async_delete: - att = UnboundServiceEngineAppAttachment.objects.create( - engine_app=self.db_obj.engine_app, - service=self.db_obj.service, - service_instance=self.db_obj.service_instance, - ) - logger.info( - f"Create unbound remote service engine app attachment: service id: {att.service_id}, service instance id: {att.service_instance_id}" - ) - - self.db_obj.clean_service_instance() + if self.is_provisioned(): + self.db_obj.clean_service_instance() + if self.db_obj.service.prefer_async_delete: + UnboundServiceEngineAppAttachment.objects.create( + engine_app=self.db_obj.engine_app, + service=self.db_obj.service, + service_instance=self.db_obj.service_instance, + ) def get_instance(self) -> ServiceInstanceObj: """Get service instance object""" @@ -216,7 +212,7 @@ def __init__(self, db_obj: UnboundServiceEngineAppAttachment): def recycle_resource(self) -> None: self.db_obj.clean_service_instance() - def get_instance(self) -> ServiceInstanceObj: + def get_instance(self) -> Optional[ServiceInstanceObj]: """Get service instance object""" # All local service instance's credentials was prefixed with service name service_name = self.db_obj.service.name diff --git a/apiserver/paasng/paasng/accessories/servicehub/management/commands/clean_recycled_unbound_remote_instances.py b/apiserver/paasng/paasng/accessories/servicehub/management/commands/check_unbound_remote_services_recycling.py similarity index 86% rename from apiserver/paasng/paasng/accessories/servicehub/management/commands/clean_recycled_unbound_remote_instances.py rename to apiserver/paasng/paasng/accessories/servicehub/management/commands/check_unbound_remote_services_recycling.py index a3684648e5..82535f25be 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/management/commands/clean_recycled_unbound_remote_instances.py +++ b/apiserver/paasng/paasng/accessories/servicehub/management/commands/check_unbound_remote_services_recycling.py @@ -19,15 +19,15 @@ from django.core.management.base import BaseCommand -from paasng.accessories.servicehub.tasks import clean_recycled_unbound_remote_instances +from paasng.accessories.servicehub.tasks import check_unbound_remote_services_recycling logger = logging.getLogger(__name__) class Command(BaseCommand): - help = "Check if unbound remote service instance is recycled, if it is recycled, delete object in database." + help = "Check if unbound remote service instance is recycled, if it is recycled, delete unbound attachment object in database." def handle(self, *args, **options): logger.info("Start to check if remote unbound service instance recycled") - clean_recycled_unbound_remote_instances() + check_unbound_remote_services_recycling() logger.info("Complete check if remote unbound service instance recycled") diff --git a/apiserver/paasng/paasng/accessories/servicehub/migrations/0005_unboundserviceengineappattachment_and_more.py b/apiserver/paasng/paasng/accessories/servicehub/migrations/0005_unboundserviceengineappattachment_and_more.py index 103957f223..f45509a787 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/migrations/0005_unboundserviceengineappattachment_and_more.py +++ b/apiserver/paasng/paasng/accessories/servicehub/migrations/0005_unboundserviceengineappattachment_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.16 on 2024-12-18 08:44 +# Generated by Django 4.2.16 on 2024-12-23 02:22 from django.db import migrations, models import django.db.models.deletion @@ -28,6 +28,7 @@ class Migration(migrations.Migration): ], options={ 'verbose_name': '本地已解绑增强服务', + 'unique_together': {('service', 'engine_app', 'service_instance')}, }, ), migrations.CreateModel( @@ -44,6 +45,7 @@ class Migration(migrations.Migration): ], options={ 'verbose_name': '远程已解绑增强服务', + 'unique_together': {('service_id', 'engine_app', 'service_instance_id')}, }, ), ] diff --git a/apiserver/paasng/paasng/accessories/servicehub/models.py b/apiserver/paasng/paasng/accessories/servicehub/models.py index 66e84e38c8..d428b14553 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/models.py +++ b/apiserver/paasng/paasng/accessories/servicehub/models.py @@ -140,6 +140,7 @@ class UnboundServiceEngineAppAttachment(OwnerTimestampedModel): class Meta: verbose_name = "本地已解绑增强服务" + unique_together = ("service", "engine_app", "service_instance") def clean_service_instance(self): """回收增强服务资源""" @@ -189,6 +190,7 @@ class UnboundRemoteServiceEngineAppAttachment(OwnerTimestampedModel): class Meta: verbose_name = "远程已解绑增强服务" + unique_together = ("service_id", "engine_app", "service_instance_id") class ServiceDBProperties: diff --git a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py index 2b44c9c18c..4384df9894 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py @@ -34,7 +34,6 @@ from paasng.accessories.servicehub import constants, exceptions from paasng.accessories.servicehub.exceptions import ( BindServiceNoPlansError, - SvcInstanceNotFound, UnboundSvcAttachmentDoesNotExist, ) from paasng.accessories.servicehub.models import ( @@ -296,15 +295,12 @@ def recycle_resource(self): logger.exception("Error occurs during recycling") raise exceptions.SvcInstanceDeleteError("unable to delete instance") from e - if self.remote_client.config.prefer_async_delete: - att = UnboundRemoteServiceEngineAppAttachment.objects.create( - engine_app=self.db_engine_app, - service_id=self.db_obj.service_id, - service_instance_id=self.db_obj.service_instance_id, - ) - logger.info( - f"Create unbound remote service engine app attachment: service id: {att.service_id}, service instance id: {att.service_instance_id}" - ) + if self.remote_client.config.prefer_async_delete: + UnboundRemoteServiceEngineAppAttachment.objects.create( + engine_app=self.db_engine_app, + service_id=self.db_obj.service_id, + service_instance_id=self.db_obj.service_instance_id, + ) self.db_obj.service_instance_id = None self.db_obj.save() @@ -400,19 +396,18 @@ def retrieve_instance_to_be_deleted(self) -> dict: try: instance_data = self.remote_client.retrieve_instance_to_be_deleted(str(self.db_obj.service_instance_id)) except RClientResponseError as e: - # if not find service instance with this id, remote response http status code 404 + # If not found service instance by instance id, which means it has been recycled, remote will return 404. if e.status_code == 404: - logger.info( - f"Unbound remote service instance is recycled, service_id: {self.db_obj.service_id}, service_instance_id: {self.db_obj.service_instance_id}" - ) self.db_obj.delete() - raise SvcInstanceNotFound(f"service instance {self.db_obj.service_instance_id} not found") + return {} raise return instance_data - def get_instance(self) -> ServiceInstanceObj: + def get_instance(self) -> Optional[ServiceInstanceObj]: """Get service instance object""" instance_data = self.retrieve_instance_to_be_deleted() + if not instance_data: + return None svc_obj = self.mgr.get(str(self.db_obj.service_id), region=self.db_application.region) create_time = arrow.get(instance_data.get("created")) # type: ignore @@ -425,20 +420,20 @@ def get_instance(self) -> ServiceInstanceObj: ) def recycle_resource(self) -> None: - try: - self.retrieve_instance_to_be_deleted() - except SvcInstanceNotFound: - return - try: self.remote_client.delete_instance_synchronously(instance_id=str(self.db_obj.service_instance_id)) - self.db_obj.delete() - logger.info( - f"Manually recycled unbound remote service instance, service_id: {self.db_obj.service_id}, service_instance_id: {self.db_obj.service_instance_id}" - ) - except Exception as e: - logger.exception("Error occurs during recycling") - raise exceptions.SvcInstanceDeleteError("unable to delete instance") from e + except RClientResponseError as e: + # If not found service instance by instance id, which means it has been recycled, remote will return 404. + if e.status_code == 404: + pass + else: + logger.exception("Error occurs during recycling") + raise exceptions.SvcInstanceDeleteError("unable to delete instance") from e + + self.db_obj.delete() + logger.info( + f"Manually recycled unbound remote service instance, service_id: {self.db_obj.service_id}, service_instance_id: {self.db_obj.service_instance_id}" + ) class RemotePlainInstanceMgr(PlainInstanceMgr): diff --git a/apiserver/paasng/paasng/accessories/servicehub/serializers.py b/apiserver/paasng/paasng/accessories/servicehub/serializers.py index 7e4ce6e812..a5f6d36ec4 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/serializers.py +++ b/apiserver/paasng/paasng/accessories/servicehub/serializers.py @@ -273,7 +273,7 @@ class UnboundServiceEngineAppAttachmentSLZ(serializers.Serializer): count = serializers.SerializerMethodField(help_text="数量") def get_count(self, obj): - return len(obj.get("unbound_instances") or []) + return len(obj.get("unbound_instances", [])) class DeleteUnboundServiceEngineAppAttachmentSLZ(serializers.Serializer): diff --git a/apiserver/paasng/paasng/accessories/servicehub/services.py b/apiserver/paasng/paasng/accessories/servicehub/services.py index 75f56a0afa..206e1ff0cb 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/services.py +++ b/apiserver/paasng/paasng/accessories/servicehub/services.py @@ -219,7 +219,7 @@ class UnboundEngineAppInstanceRel(metaclass=ABCMeta): db_obj: Any @abstractmethod - def get_instance(self) -> ServiceInstanceObj: + def get_instance(self) -> Optional[ServiceInstanceObj]: raise NotImplementedError @abstractmethod diff --git a/apiserver/paasng/paasng/accessories/servicehub/tasks.py b/apiserver/paasng/paasng/accessories/servicehub/tasks.py index 51c90b6757..c0c89e5ced 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/tasks.py +++ b/apiserver/paasng/paasng/accessories/servicehub/tasks.py @@ -51,7 +51,7 @@ def clean_instances(): logger.info(f"instance<{uuid}> cleaned. ") -def clean_recycled_unbound_remote_instances(): +def check_unbound_remote_services_recycling(): store = get_remote_store() unbound_instances = UnboundRemoteServiceEngineAppAttachment.objects.all() diff --git a/apiserver/paasng/paasng/accessories/servicehub/views.py b/apiserver/paasng/paasng/accessories/servicehub/views.py index fcf0384bb0..0f7f0ddead 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/views.py +++ b/apiserver/paasng/paasng/accessories/servicehub/views.py @@ -37,7 +37,6 @@ ReferencedAttachmentNotFound, ServiceObjNotFound, SharedAttachmentAlreadyExists, - SvcInstanceNotFound, UnboundSvcAttachmentDoesNotExist, ) from paasng.accessories.servicehub.manager import mixed_service_mgr @@ -772,9 +771,8 @@ def list_by_module(self, request, code, module_name): categorized_rels = defaultdict(list) for env in module.envs.all(): for rel in mixed_service_mgr.list_unbound_instance_rels(env.engine_app): - try: - instance = rel.get_instance() - except SvcInstanceNotFound: + instance = rel.get_instance() + if not instance: # 如果已经回收了,获取不到 instance,跳过 continue diff --git a/apiserver/paasng/tests/paasng/accessories/servicehub/remote/test_client.py b/apiserver/paasng/tests/paasng/accessories/servicehub/remote/test_client.py index 74b36b0ccb..e932c7d1c2 100644 --- a/apiserver/paasng/tests/paasng/accessories/servicehub/remote/test_client.py +++ b/apiserver/paasng/tests/paasng/accessories/servicehub/remote/test_client.py @@ -66,6 +66,17 @@ def test_retrieve_instance_normal(self, mocked_get, client): auth_inst = mocked_get.call_args[1]["auth"] assert isinstance(auth_inst, ClientJWTAuth) + @mock.patch("requests.get") + def test_retrieve_instance_to_be_deleted_normal(self, mocked_get, client): + mocked_get.return_value = mock_json_response(data_mocks.REMOTE_INSTANCE_JSON) + + data = client.retrieve_instance_to_be_deleted(instance_id="faked-id") + assert data is not None + assert mocked_get.called + assert mocked_get.call_args[0][0] == "http://faked-host/instances/faked-id/?to_be_deleted=true" + auth_inst = mocked_get.call_args[1]["auth"] + assert isinstance(auth_inst, ClientJWTAuth) + @mock.patch("requests.post") def test_provision_instance_normal(self, mocked_post, client): mocked_post.return_value = mock_json_response(data_mocks.REMOTE_INSTANCE_JSON) From a75a0ab236dc3723ff1272f4dcfccecd7c69e655 Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Mon, 23 Dec 2024 12:14:34 +0800 Subject: [PATCH 20/31] fix: minor --- .../paasng/paasng/accessories/servicehub/remote/manager.py | 1 - .../tests/paasng/accessories/servicehub/remote/test_client.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py index 4384df9894..e83d7c2c28 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py @@ -285,7 +285,6 @@ def sync_instance_config(self): except Exception: logger.exception(f"Error when updating instance config for {instance_id}") - @atomic def recycle_resource(self): """对于 remote service 我们默认其已经具备了回收的能力""" if self.is_provisioned(): diff --git a/apiserver/paasng/tests/paasng/accessories/servicehub/remote/test_client.py b/apiserver/paasng/tests/paasng/accessories/servicehub/remote/test_client.py index e932c7d1c2..03c6a06aef 100644 --- a/apiserver/paasng/tests/paasng/accessories/servicehub/remote/test_client.py +++ b/apiserver/paasng/tests/paasng/accessories/servicehub/remote/test_client.py @@ -73,7 +73,7 @@ def test_retrieve_instance_to_be_deleted_normal(self, mocked_get, client): data = client.retrieve_instance_to_be_deleted(instance_id="faked-id") assert data is not None assert mocked_get.called - assert mocked_get.call_args[0][0] == "http://faked-host/instances/faked-id/?to_be_deleted=true" + assert mocked_get.call_args[0][0] == "http://faked-host/instances/faked-id/?to_be_deleted=True" auth_inst = mocked_get.call_args[1]["auth"] assert isinstance(auth_inst, ClientJWTAuth) From a0689c9fd354ca2782206c6ca20a829d00a038f1 Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Mon, 23 Dec 2024 16:08:55 +0800 Subject: [PATCH 21/31] fix: minor --- .../accessories/servicehub/local/manager.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py index f9dbb1bdc9..87aa6c4839 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py @@ -171,14 +171,14 @@ def provision(self): ).inc() def recycle_resource(self): - if self.is_provisioned(): - self.db_obj.clean_service_instance() - if self.db_obj.service.prefer_async_delete: - UnboundServiceEngineAppAttachment.objects.create( - engine_app=self.db_obj.engine_app, - service=self.db_obj.service, - service_instance=self.db_obj.service_instance, - ) + service_instance = self.db_obj.service_instance + self.db_obj.clean_service_instance() + if self.db_obj.service.prefer_async_delete: + UnboundServiceEngineAppAttachment.objects.create( + engine_app=self.db_obj.engine_app, + service=self.db_obj.service, + service_instance=service_instance, + ) def get_instance(self) -> ServiceInstanceObj: """Get service instance object""" From 1db97f08acd6637ab69f968c6ac29e3349ec9a96 Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Mon, 23 Dec 2024 18:43:41 +0800 Subject: [PATCH 22/31] fix: unit test for remote mgr --- .../servicehub/remote/test_manager.py | 104 +++++++++++++++++- .../accessories/servicehub/test_manager.py | 18 +-- 2 files changed, 111 insertions(+), 11 deletions(-) diff --git a/apiserver/paasng/tests/paasng/accessories/servicehub/remote/test_manager.py b/apiserver/paasng/tests/paasng/accessories/servicehub/remote/test_manager.py index 91718dd8a2..6e0e597bb1 100644 --- a/apiserver/paasng/tests/paasng/accessories/servicehub/remote/test_manager.py +++ b/apiserver/paasng/tests/paasng/accessories/servicehub/remote/test_manager.py @@ -26,12 +26,18 @@ from django_dynamic_fixture import G from paas_wl.infras.cluster.models import Cluster -from paasng.accessories.servicehub.exceptions import BindServiceNoPlansError, CanNotModifyPlan, ServiceObjNotFound +from paasng.accessories.servicehub.exceptions import ( + BindServiceNoPlansError, + CanNotModifyPlan, + ServiceObjNotFound, + UnboundSvcAttachmentDoesNotExist, +) from paasng.accessories.servicehub.manager import mixed_service_mgr -from paasng.accessories.servicehub.models import RemoteServiceEngineAppAttachment +from paasng.accessories.servicehub.models import RemoteServiceEngineAppAttachment, ServiceEngineAppAttachment from paasng.accessories.servicehub.remote import RemoteServiceMgr, collector from paasng.accessories.servicehub.remote.manager import MetaInfo, RemoteEngineAppInstanceRel, RemotePlanObj from paasng.accessories.servicehub.remote.store import get_remote_store +from paasng.platform.modules.manager import ModuleCleaner from paasng.platform.modules.models import Module from tests.paasng.accessories.servicehub import data_mocks from tests.utils.api import mock_json_response @@ -553,6 +559,100 @@ def test_get_attachment_by_instance_id(self, store, bk_module): for service_instance_id in expect_obj: assert mgr.get_attachment_by_instance_id(svc, service_instance_id) == expect_obj[service_instance_id] + @mock.patch("paasng.accessories.servicehub.remote.client.RemoteServiceClient.delete_instance_synchronously") + @mock.patch("paasng.accessories.servicehub.remote.client.RemoteServiceClient.delete_instance") + @mock.patch("paasng.accessories.servicehub.remote.client.RemoteServiceClient.provision_instance") + @mock.patch("paasng.accessories.servicehub.remote.manager.get_cluster_egress_info") + def test_list_unbound_instance_rels( + self, + get_cluster_egress_info, + mocked_provision, + delete_instance, + delete_instance_synchronously, + store, + bk_app, + bk_module, + ): + """Test service instance provision""" + get_cluster_egress_info.return_value = {"egress_ips": ["1.1.1.1"], "digest_version": "foo"} + + mgr = RemoteServiceMgr(store=store) + svc = mgr.get(id_of_first_service, region=bk_module.region) + mgr.bind_service(svc, bk_module) + + attachments: Dict[str, ServiceEngineAppAttachment] = {} + for env in bk_app.envs.all(): + for rel in mgr.list_unprovisioned_rels(env.engine_app): + assert rel.is_provisioned() is False + rel.provision() + attachments[str(rel.db_obj.service_instance_id)] = rel.db_obj + + assert len(attachments) == 2 + + cleaner = ModuleCleaner(bk_module) + cleaner.delete_services(svc.uuid) + + unbound_rels = [] + for env in bk_app.envs.all(): + for u_rel in mgr.list_unbound_instance_rels(env.engine_app): + assert str(u_rel.db_obj.service_instance_id) in attachments + assert u_rel.db_obj.service_id == attachments[str(u_rel.db_obj.service_instance_id)].service_id + assert u_rel.db_obj.engine_app == attachments[str(u_rel.db_obj.service_instance_id)].engine_app + unbound_rels.append(u_rel) + + assert len(unbound_rels) == len(attachments) + + for u_rel in unbound_rels: + u_rel.recycle_resource() + + for env in bk_app.envs.all(): + for _rel in mgr.list_unbound_instance_rels(env.engine_app): + pytest.fail("Expect no return unbound instance rels after recycle resource") + + @mock.patch("paasng.accessories.servicehub.remote.client.RemoteServiceClient.delete_instance_synchronously") + @mock.patch("paasng.accessories.servicehub.remote.client.RemoteServiceClient.delete_instance") + @mock.patch("paasng.accessories.servicehub.remote.client.RemoteServiceClient.provision_instance") + @mock.patch("paasng.accessories.servicehub.remote.manager.get_cluster_egress_info") + def test_get_unbound_instance_rel_by_instance_id( + self, + get_cluster_egress_info, + mocked_provision, + delete_instance, + delete_instance_synchronously, + store, + bk_app, + bk_module, + ): + """Test service instance provision""" + get_cluster_egress_info.return_value = {"egress_ips": ["1.1.1.1"], "digest_version": "foo"} + + mgr = RemoteServiceMgr(store=store) + svc = mgr.get(id_of_first_service, region=bk_module.region) + mgr.bind_service(svc, bk_module) + + attachments: Dict[str, ServiceEngineAppAttachment] = {} + for env in bk_app.envs.all(): + for rel in mgr.list_unprovisioned_rels(env.engine_app): + assert rel.is_provisioned() is False + rel.provision() + attachments[str(rel.db_obj.service_instance_id)] = rel.db_obj + + assert len(attachments) == 2 + + cleaner = ModuleCleaner(bk_module) + cleaner.delete_services(svc.uuid) + + for instance_id in attachments: + rel = mgr.get_unbound_instance_rel_by_instance_id(svc, uuid.UUID(instance_id)) + assert str(rel.db_obj.service_instance_id) in attachments + assert rel.db_obj.service_id == attachments[str(rel.db_obj.service_instance_id)].service_id + assert rel.db_obj.engine_app == attachments[str(rel.db_obj.service_instance_id)].engine_app + rel.recycle_resource() + + for instance_id in attachments: + with pytest.raises(UnboundSvcAttachmentDoesNotExist): + mgr.get_unbound_instance_rel_by_instance_id(svc, uuid.UUID(instance_id)) + class TestLegacyRemoteMgr: app_region = "rr1" diff --git a/apiserver/paasng/tests/paasng/accessories/servicehub/test_manager.py b/apiserver/paasng/tests/paasng/accessories/servicehub/test_manager.py index 8aec67c449..62b474d3ba 100644 --- a/apiserver/paasng/tests/paasng/accessories/servicehub/test_manager.py +++ b/apiserver/paasng/tests/paasng/accessories/servicehub/test_manager.py @@ -265,7 +265,7 @@ def test_get_attachment_by_instance_id(self, bk_module): def test_list_unbound_instance_rels( self, create_service_instance_by_plan, delete_service_instance, instance_factory, svc, bk_app, bk_module ): - """Test service instance provision""" + """Test list unbound instance rels""" create_service_instance_by_plan.side_effect = [instance_factory(), instance_factory()] delete_service_instance.side_effect = ( lambda service_instance: service_instance.delete() if service_instance else None @@ -288,16 +288,16 @@ def test_list_unbound_instance_rels( unbound_rels = [] for env in bk_app.envs.all(): - for unbound_rel in mgr.list_unbound_instance_rels(env.engine_app): - assert unbound_rel.db_obj.service_instance_id in attachments - assert unbound_rel.db_obj.service_id == attachments[unbound_rel.db_obj.service_instance_id].service_id - assert unbound_rel.db_obj.engine_app == attachments[unbound_rel.db_obj.service_instance_id].engine_app - unbound_rels.append(unbound_rel) + for u_rel in mgr.list_unbound_instance_rels(env.engine_app): + assert u_rel.db_obj.service_instance_id in attachments + assert u_rel.db_obj.service_id == attachments[u_rel.db_obj.service_instance_id].service_id + assert u_rel.db_obj.engine_app == attachments[u_rel.db_obj.service_instance_id].engine_app + unbound_rels.append(u_rel) assert len(unbound_rels) == len(attachments) - for unbound_rel in unbound_rels: - unbound_rel.recycle_resource() + for u_rel in unbound_rels: + u_rel.recycle_resource() for env in bk_app.envs.all(): for _rel in mgr.list_unbound_instance_rels(env.engine_app): @@ -308,7 +308,7 @@ def test_list_unbound_instance_rels( def test_get_unbound_instance_rel_by_instance_id( self, create_service_instance_by_plan, delete_service_instance, instance_factory, svc, bk_app, bk_module ): - """Test service instance provision""" + """Test get unbound instance rel by instance id""" create_service_instance_by_plan.side_effect = [instance_factory(), instance_factory()] delete_service_instance.side_effect = ( lambda service_instance: service_instance.delete() if service_instance else None From 52dc76a9a39e5cab545c4e57bef8b3fc7e8d3ae0 Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Mon, 23 Dec 2024 20:13:47 +0800 Subject: [PATCH 23/31] fix: migrations --- ....py => 0006_unboundserviceengineappattachment_and_more.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename apiserver/paasng/paasng/accessories/servicehub/migrations/{0005_unboundserviceengineappattachment_and_more.py => 0006_unboundserviceengineappattachment_and_more.py} (95%) diff --git a/apiserver/paasng/paasng/accessories/servicehub/migrations/0005_unboundserviceengineappattachment_and_more.py b/apiserver/paasng/paasng/accessories/servicehub/migrations/0006_unboundserviceengineappattachment_and_more.py similarity index 95% rename from apiserver/paasng/paasng/accessories/servicehub/migrations/0005_unboundserviceengineappattachment_and_more.py rename to apiserver/paasng/paasng/accessories/servicehub/migrations/0006_unboundserviceengineappattachment_and_more.py index f45509a787..a6ffdb8a3a 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/migrations/0005_unboundserviceengineappattachment_and_more.py +++ b/apiserver/paasng/paasng/accessories/servicehub/migrations/0006_unboundserviceengineappattachment_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.16 on 2024-12-23 02:22 +# Generated by Django 4.2.16 on 2024-12-23 12:11 from django.db import migrations, models import django.db.models.deletion @@ -10,7 +10,7 @@ class Migration(migrations.Migration): dependencies = [ ('engine', '0023_remove_deployment_hooks_remove_deployment_procfile'), ('services', '0006_alter_servicecategory_name_en'), - ('servicehub', '0004_auto_20240412_1723'), + ('servicehub', '0005_servicebindingpolicy_servicebindingprecedencepolicy'), ] operations = [ From 56bd0429c1c20d9468ed286d4394627e216e0cf9 Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Mon, 23 Dec 2024 20:54:21 +0800 Subject: [PATCH 24/31] fix: comments --- .../paasng/accessories/servicehub/remote/test_manager.py | 4 ++-- .../tests/paasng/accessories/servicehub/test_manager.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/apiserver/paasng/tests/paasng/accessories/servicehub/remote/test_manager.py b/apiserver/paasng/tests/paasng/accessories/servicehub/remote/test_manager.py index 6e0e597bb1..ad7a1520d2 100644 --- a/apiserver/paasng/tests/paasng/accessories/servicehub/remote/test_manager.py +++ b/apiserver/paasng/tests/paasng/accessories/servicehub/remote/test_manager.py @@ -573,7 +573,7 @@ def test_list_unbound_instance_rels( bk_app, bk_module, ): - """Test service instance provision""" + """Test list unbound instance rels""" get_cluster_egress_info.return_value = {"egress_ips": ["1.1.1.1"], "digest_version": "foo"} mgr = RemoteServiceMgr(store=store) @@ -623,7 +623,7 @@ def test_get_unbound_instance_rel_by_instance_id( bk_app, bk_module, ): - """Test service instance provision""" + """Test get unbound service instance rel by instance id""" get_cluster_egress_info.return_value = {"egress_ips": ["1.1.1.1"], "digest_version": "foo"} mgr = RemoteServiceMgr(store=store) diff --git a/apiserver/paasng/tests/paasng/accessories/servicehub/test_manager.py b/apiserver/paasng/tests/paasng/accessories/servicehub/test_manager.py index 62b474d3ba..87ed222c5e 100644 --- a/apiserver/paasng/tests/paasng/accessories/servicehub/test_manager.py +++ b/apiserver/paasng/tests/paasng/accessories/servicehub/test_manager.py @@ -265,7 +265,6 @@ def test_get_attachment_by_instance_id(self, bk_module): def test_list_unbound_instance_rels( self, create_service_instance_by_plan, delete_service_instance, instance_factory, svc, bk_app, bk_module ): - """Test list unbound instance rels""" create_service_instance_by_plan.side_effect = [instance_factory(), instance_factory()] delete_service_instance.side_effect = ( lambda service_instance: service_instance.delete() if service_instance else None @@ -308,7 +307,6 @@ def test_list_unbound_instance_rels( def test_get_unbound_instance_rel_by_instance_id( self, create_service_instance_by_plan, delete_service_instance, instance_factory, svc, bk_app, bk_module ): - """Test get unbound instance rel by instance id""" create_service_instance_by_plan.side_effect = [instance_factory(), instance_factory()] delete_service_instance.side_effect = ( lambda service_instance: service_instance.delete() if service_instance else None From 4c3ba86cbeef46708785f2f6a98607c3ab2dcb42 Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Mon, 23 Dec 2024 20:56:12 +0800 Subject: [PATCH 25/31] fix: minor --- .../tests/paasng/accessories/servicehub/remote/test_manager.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/apiserver/paasng/tests/paasng/accessories/servicehub/remote/test_manager.py b/apiserver/paasng/tests/paasng/accessories/servicehub/remote/test_manager.py index ad7a1520d2..10194f565d 100644 --- a/apiserver/paasng/tests/paasng/accessories/servicehub/remote/test_manager.py +++ b/apiserver/paasng/tests/paasng/accessories/servicehub/remote/test_manager.py @@ -573,7 +573,6 @@ def test_list_unbound_instance_rels( bk_app, bk_module, ): - """Test list unbound instance rels""" get_cluster_egress_info.return_value = {"egress_ips": ["1.1.1.1"], "digest_version": "foo"} mgr = RemoteServiceMgr(store=store) @@ -623,7 +622,6 @@ def test_get_unbound_instance_rel_by_instance_id( bk_app, bk_module, ): - """Test get unbound service instance rel by instance id""" get_cluster_egress_info.return_value = {"egress_ips": ["1.1.1.1"], "digest_version": "foo"} mgr = RemoteServiceMgr(store=store) From 05558b16e9684fb73683d76be47950ccdd0ae344 Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Tue, 24 Dec 2024 17:30:19 +0800 Subject: [PATCH 26/31] fix: comment --- .../paasng/paasng/accessories/servicehub/local/manager.py | 3 ++- .../paasng/accessories/servicehub/remote/manager.py | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py index 87aa6c4839..a62e0d06dd 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py @@ -380,7 +380,7 @@ def list_provisioned_rels( def list_unbound_instance_rels( self, engine_app: EngineApp, service: Optional[ServiceObj] = None ) -> Generator[UnboundEngineAppInstanceRel, None, None]: - """Return all unbound engine_app <-> local service instances by specified service (None for all)""" + """Return all local provisioned service instances which is unbound with engine app, filter by specified service (None for all)""" qs = engine_app.unbound_service_attachment.all() if service: qs = qs.filter(service_id=service.uuid) @@ -436,6 +436,7 @@ def get_attachment_by_engine_app(self, service: ServiceObj, engine_app: EngineAp raise SvcAttachmentDoesNotExist from e def get_unbound_instance_rel_by_instance_id(self, service: ServiceObj, service_instance_id: uuid.UUID): + """Return a local provisioned service instance which is unbound with engine app by specified service and service instance id""" try: instance = UnboundServiceEngineAppAttachment.objects.get( service_id=service.uuid, diff --git a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py index e83d7c2c28..00f882b4c5 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py @@ -391,7 +391,7 @@ def __init__( self.remote_config = self.store.get_source_config(str(self.db_obj.service_id)) self.remote_client = RemoteServiceClient(self.remote_config) - def retrieve_instance_to_be_deleted(self) -> dict: + def _retrieve_instance_to_be_deleted(self) -> dict: try: instance_data = self.remote_client.retrieve_instance_to_be_deleted(str(self.db_obj.service_instance_id)) except RClientResponseError as e: @@ -404,7 +404,7 @@ def retrieve_instance_to_be_deleted(self) -> dict: def get_instance(self) -> Optional[ServiceInstanceObj]: """Get service instance object""" - instance_data = self.retrieve_instance_to_be_deleted() + instance_data = self._retrieve_instance_to_be_deleted() if not instance_data: return None svc_obj = self.mgr.get(str(self.db_obj.service_id), region=self.db_application.region) @@ -419,6 +419,7 @@ def get_instance(self) -> Optional[ServiceInstanceObj]: ) def recycle_resource(self) -> None: + """Recycle unbound service instance resource synchronously""" try: self.remote_client.delete_instance_synchronously(instance_id=str(self.db_obj.service_instance_id)) except RClientResponseError as e: @@ -693,7 +694,7 @@ def list_provisioned_rels( def list_unbound_instance_rels( self, engine_app: EngineApp, service: Optional[ServiceObj] = None ) -> Generator[UnboundRemoteEngineAppInstanceRel, None, None]: - """Return all unbound engine_app <-> remote service instances""" + """Return all remote provisioned service instances which is unbound with engine app, filter by specific service (None for all)""" qs = engine_app.unbound_remote_service_attachment.all() if service: qs = qs.filter(service_id=service.uuid) @@ -768,6 +769,7 @@ def get_attachment_by_engine_app(self, service: ServiceObj, engine_app: EngineAp raise exceptions.SvcAttachmentDoesNotExist from e def get_unbound_instance_rel_by_instance_id(self, service: ServiceObj, service_instance_id: uuid.UUID): + """Return a remote provisioned service instances which is unbound with engine app, filter by specific service and service instance id""" try: instance = UnboundRemoteServiceEngineAppAttachment.objects.get( service_id=service.uuid, From ec4ca73a3b93a8429a4fe4e2da1dde32435e5997 Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Tue, 24 Dec 2024 19:23:52 +0800 Subject: [PATCH 27/31] fix: add region and owner field when create unbound instance obj --- apiserver/paasng/paasng/accessories/servicehub/local/manager.py | 2 ++ .../paasng/paasng/accessories/servicehub/remote/manager.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py index a62e0d06dd..791f35332b 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py @@ -175,6 +175,8 @@ def recycle_resource(self): self.db_obj.clean_service_instance() if self.db_obj.service.prefer_async_delete: UnboundServiceEngineAppAttachment.objects.create( + region=self.db_obj.region, + owner=self.db_obj.owner, engine_app=self.db_obj.engine_app, service=self.db_obj.service, service_instance=service_instance, diff --git a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py index 00f882b4c5..4f170f08a7 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py @@ -296,6 +296,8 @@ def recycle_resource(self): if self.remote_client.config.prefer_async_delete: UnboundRemoteServiceEngineAppAttachment.objects.create( + region=self.db_obj.region, + owner=self.db_obj.owner, engine_app=self.db_engine_app, service_id=self.db_obj.service_id, service_instance_id=self.db_obj.service_instance_id, From 34b1060551a6fed27249f2d2b61a9525e7867ff4 Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Wed, 25 Dec 2024 11:28:36 +0800 Subject: [PATCH 28/31] style: comments --- .../paasng/paasng/accessories/servicehub/local/manager.py | 4 ++-- apiserver/paasng/paasng/accessories/servicehub/models.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py index 791f35332b..8c35db0bbe 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py @@ -382,7 +382,7 @@ def list_provisioned_rels( def list_unbound_instance_rels( self, engine_app: EngineApp, service: Optional[ServiceObj] = None ) -> Generator[UnboundEngineAppInstanceRel, None, None]: - """Return all local provisioned service instances which is unbound with engine app, filter by specified service (None for all)""" + """Return all local provisioned service instances which is unbound with engine app, filter by specific service (None for all)""" qs = engine_app.unbound_service_attachment.all() if service: qs = qs.filter(service_id=service.uuid) @@ -438,7 +438,7 @@ def get_attachment_by_engine_app(self, service: ServiceObj, engine_app: EngineAp raise SvcAttachmentDoesNotExist from e def get_unbound_instance_rel_by_instance_id(self, service: ServiceObj, service_instance_id: uuid.UUID): - """Return a local provisioned service instance which is unbound with engine app by specified service and service instance id""" + """Return a local provisioned service instance which is unbound with engine app by specific service and service instance id""" try: instance = UnboundServiceEngineAppAttachment.objects.get( service_id=service.uuid, diff --git a/apiserver/paasng/paasng/accessories/servicehub/models.py b/apiserver/paasng/paasng/accessories/servicehub/models.py index 99ae64b307..408d94e78e 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/models.py +++ b/apiserver/paasng/paasng/accessories/servicehub/models.py @@ -124,7 +124,7 @@ def __str__(self): class UnboundServiceEngineAppAttachment(OwnerTimestampedModel): - """Local service instance which is unbound with engine app""" + """Unbound binding relationship between the engine app and the local service""" engine_app = models.ForeignKey( "engine.EngineApp", @@ -176,7 +176,7 @@ class Meta: class UnboundRemoteServiceEngineAppAttachment(OwnerTimestampedModel): - """Remote service instance which is unbound with engine app""" + """Unbound binding relationship between the engine app and the remote service""" engine_app = models.ForeignKey( "engine.EngineApp", From 7152261949c47b054056010fabc312ddb4ae1e6d Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Wed, 25 Dec 2024 17:37:38 +0800 Subject: [PATCH 29/31] fix: set db_constraint=False in model, optimize comments --- .../accessories/servicehub/local/manager.py | 4 ++-- ...nboundserviceengineappattachment_and_more.py | 10 +++++----- .../paasng/accessories/servicehub/models.py | 17 +++++++++++------ .../accessories/servicehub/remote/manager.py | 4 ++-- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py index 8c35db0bbe..24e3e3fc08 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py @@ -382,7 +382,7 @@ def list_provisioned_rels( def list_unbound_instance_rels( self, engine_app: EngineApp, service: Optional[ServiceObj] = None ) -> Generator[UnboundEngineAppInstanceRel, None, None]: - """Return all local provisioned service instances which is unbound with engine app, filter by specific service (None for all)""" + """Return all unbound local service instances, filter by specific service (None for all)""" qs = engine_app.unbound_service_attachment.all() if service: qs = qs.filter(service_id=service.uuid) @@ -438,7 +438,7 @@ def get_attachment_by_engine_app(self, service: ServiceObj, engine_app: EngineAp raise SvcAttachmentDoesNotExist from e def get_unbound_instance_rel_by_instance_id(self, service: ServiceObj, service_instance_id: uuid.UUID): - """Return a local provisioned service instance which is unbound with engine app by specific service and service instance id""" + """Return a unbound local service instance, filter by specific service and service instance id""" try: instance = UnboundServiceEngineAppAttachment.objects.get( service_id=service.uuid, diff --git a/apiserver/paasng/paasng/accessories/servicehub/migrations/0006_unboundserviceengineappattachment_and_more.py b/apiserver/paasng/paasng/accessories/servicehub/migrations/0006_unboundserviceengineappattachment_and_more.py index a6ffdb8a3a..0feb46d8fd 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/migrations/0006_unboundserviceengineappattachment_and_more.py +++ b/apiserver/paasng/paasng/accessories/servicehub/migrations/0006_unboundserviceengineappattachment_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.16 on 2024-12-23 12:11 +# Generated by Django 4.2.16 on 2024-12-25 09:35 from django.db import migrations, models import django.db.models.deletion @@ -23,11 +23,11 @@ class Migration(migrations.Migration): ('updated', models.DateTimeField(auto_now=True)), ('owner', paasng.utils.models.BkUserField(blank=True, db_index=True, max_length=64, null=True)), ('engine_app', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, related_name='unbound_service_attachment', to='engine.engineapp', verbose_name='蓝鲸引擎应用')), - ('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='services.service', verbose_name='增强服务')), - ('service_instance', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='services.serviceinstance', verbose_name='增强服务实例')), + ('service', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, to='services.service', verbose_name='增强服务')), + ('service_instance', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='services.serviceinstance', verbose_name='增强服务实例')), ], options={ - 'verbose_name': '本地已解绑增强服务', + 'verbose_name': 'unbound attachment between local service and engine app', 'unique_together': {('service', 'engine_app', 'service_instance')}, }, ), @@ -44,7 +44,7 @@ class Migration(migrations.Migration): ('engine_app', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, related_name='unbound_remote_service_attachment', to='engine.engineapp', verbose_name='蓝鲸引擎应用')), ], options={ - 'verbose_name': '远程已解绑增强服务', + 'verbose_name': 'unbound attachment between remote service and engine app', 'unique_together': {('service_id', 'engine_app', 'service_instance_id')}, }, ), diff --git a/apiserver/paasng/paasng/accessories/servicehub/models.py b/apiserver/paasng/paasng/accessories/servicehub/models.py index 408d94e78e..6ce6158cae 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/models.py +++ b/apiserver/paasng/paasng/accessories/servicehub/models.py @@ -124,7 +124,7 @@ def __str__(self): class UnboundServiceEngineAppAttachment(OwnerTimestampedModel): - """Unbound binding relationship between the engine app and the local service""" + """Unbound attachment between local service and engine app""" engine_app = models.ForeignKey( "engine.EngineApp", @@ -133,13 +133,18 @@ class UnboundServiceEngineAppAttachment(OwnerTimestampedModel): verbose_name="蓝鲸引擎应用", related_name="unbound_service_attachment", ) - service = models.ForeignKey(Service, on_delete=models.CASCADE, verbose_name="增强服务") + service = models.ForeignKey(Service, on_delete=models.CASCADE, db_constraint=False, verbose_name="增强服务") service_instance = models.ForeignKey( - ServiceInstance, on_delete=models.CASCADE, null=True, blank=True, verbose_name="增强服务实例" + ServiceInstance, + on_delete=models.CASCADE, + null=True, + blank=True, + db_constraint=False, + verbose_name="增强服务实例", ) class Meta: - verbose_name = "本地已解绑增强服务" + verbose_name = "unbound attachment between local service and engine app" unique_together = ("service", "engine_app", "service_instance") def clean_service_instance(self): @@ -176,7 +181,7 @@ class Meta: class UnboundRemoteServiceEngineAppAttachment(OwnerTimestampedModel): - """Unbound binding relationship between the engine app and the remote service""" + """Unbound attachment between remote service and engine app""" engine_app = models.ForeignKey( "engine.EngineApp", @@ -189,7 +194,7 @@ class UnboundRemoteServiceEngineAppAttachment(OwnerTimestampedModel): service_instance_id = models.UUIDField(null=True, verbose_name="远程增强服务实例 ID") class Meta: - verbose_name = "远程已解绑增强服务" + verbose_name = "unbound attachment between remote service and engine app" unique_together = ("service_id", "engine_app", "service_instance_id") diff --git a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py index 4f170f08a7..b00a64f00e 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py @@ -696,7 +696,7 @@ def list_provisioned_rels( def list_unbound_instance_rels( self, engine_app: EngineApp, service: Optional[ServiceObj] = None ) -> Generator[UnboundRemoteEngineAppInstanceRel, None, None]: - """Return all remote provisioned service instances which is unbound with engine app, filter by specific service (None for all)""" + """Return all unbound remote service instances, filter by specific service (None for all)""" qs = engine_app.unbound_remote_service_attachment.all() if service: qs = qs.filter(service_id=service.uuid) @@ -771,7 +771,7 @@ def get_attachment_by_engine_app(self, service: ServiceObj, engine_app: EngineAp raise exceptions.SvcAttachmentDoesNotExist from e def get_unbound_instance_rel_by_instance_id(self, service: ServiceObj, service_instance_id: uuid.UUID): - """Return a remote provisioned service instances which is unbound with engine app, filter by specific service and service instance id""" + """Return a unbound remote service instances, filter by specific service and service instance id""" try: instance = UnboundRemoteServiceEngineAppAttachment.objects.get( service_id=service.uuid, From 62571c51345bd374ef718d1d2e1b72339ea74c6b Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Wed, 25 Dec 2024 17:54:28 +0800 Subject: [PATCH 30/31] fix: comments --- apiserver/paasng/paasng/accessories/servicehub/local/manager.py | 2 +- apiserver/paasng/paasng/accessories/servicehub/remote/client.py | 2 +- .../paasng/paasng/accessories/servicehub/remote/manager.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py index 24e3e3fc08..2d93aee730 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py @@ -206,7 +206,7 @@ def get_plan(self) -> LocalPlanObj: class UnboundLocalEngineAppInstanceRel(UnboundEngineAppInstanceRel): - """A unbound relationship between EngineApp and Provisioned instance""" + """unbound relationship between EngineApp and local provisioned instance""" def __init__(self, db_obj: UnboundServiceEngineAppAttachment): self.db_obj = db_obj diff --git a/apiserver/paasng/paasng/accessories/servicehub/remote/client.py b/apiserver/paasng/paasng/accessories/servicehub/remote/client.py index 5439a15f7f..0aaa89628a 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/remote/client.py +++ b/apiserver/paasng/paasng/accessories/servicehub/remote/client.py @@ -227,7 +227,7 @@ def retrieve_instance(self, instance_id: str) -> Dict: return resp.json() def retrieve_instance_to_be_deleted(self, instance_id: str) -> Dict: - """Retrieve a provisioned instance info + """Retrieve a provisioned instance info, which is to be deleted :raises: RemoteClientError :return: diff --git a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py index b00a64f00e..3b009149d8 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/remote/manager.py @@ -376,7 +376,7 @@ def get_plan(self) -> RemotePlanObj: class UnboundRemoteEngineAppInstanceRel(UnboundEngineAppInstanceRel): - """A unbound relationship between EngineApp and Provisioned instance""" + """Unbound relationship between EngineApp and remote provisioned instance""" def __init__( self, db_obj: UnboundRemoteServiceEngineAppAttachment, mgr: "RemoteServiceMgr", store: RemoteServiceStore From ad4d576dd959f51b2ec36e9a49ebe2e2194840f2 Mon Sep 17 00:00:00 2001 From: songzxc789 Date: Wed, 25 Dec 2024 17:57:21 +0800 Subject: [PATCH 31/31] style: comments --- apiserver/paasng/paasng/accessories/servicehub/local/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py index 2d93aee730..1fc973187a 100644 --- a/apiserver/paasng/paasng/accessories/servicehub/local/manager.py +++ b/apiserver/paasng/paasng/accessories/servicehub/local/manager.py @@ -206,7 +206,7 @@ def get_plan(self) -> LocalPlanObj: class UnboundLocalEngineAppInstanceRel(UnboundEngineAppInstanceRel): - """unbound relationship between EngineApp and local provisioned instance""" + """Unbound relationship between EngineApp and local provisioned instance""" def __init__(self, db_obj: UnboundServiceEngineAppAttachment): self.db_obj = db_obj