Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: record service instance which is unbound and recycle later #1775

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions apiserver/paasng/paasng/accessories/servicehub/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""

Expand Down
86 changes: 82 additions & 4 deletions apiserver/paasng/paasng/accessories/servicehub/local/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -37,8 +37,13 @@
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,
Expand All @@ -50,8 +55,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
Expand Down Expand Up @@ -165,8 +171,19 @@ def provision(self):
).inc()

def recycle_resource(self):
if self.is_provisioned() and self.db_obj.service.prefer_async_delete:
self.mark_unbound()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

解绑过程需要加逻辑 而不是在回收逻辑里面
你需要先在开发者中心试一下解绑服务是由那个接口或者入口触发的

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

由这个接口触发,
DELETE make_app_pattern(f"/services/{SERVICE_UUID}/$", include_envs=False),
,会触发 ModuleCleaner.delete_services(),传入 service_id。 删除module时也会解绑增强服务,也会触发ModuleCleaner.delete_services(),不传service_id。

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

远程增强服务也需要有 self.db_obj.service.prefer_async_delete 类似的判断逻辑

self.db_obj.clean_service_instance()

def mark_unbound(self):
UnboundServiceEngineAppAttachment.objects.create(
songzxc789 marked this conversation as resolved.
Show resolved Hide resolved
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:
"""Get service instance object"""
if not self.is_provisioned():
Expand All @@ -190,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"""

Expand Down Expand Up @@ -338,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.unbound_service_attachment.all()
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(
Expand Down Expand Up @@ -386,6 +454,16 @@ 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,
)
except UnboundServiceEngineAppAttachment.DoesNotExist as e:
raise UnboundSvcAttachmentDoesNotExist from e
return UnboundLocalEngineAppInstanceRel(instance)


class LocalPlanMgr(BasePlanMgr):
"""Local in-database plans manager"""
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
22 changes: 21 additions & 1 deletion apiserver/paasng/paasng/accessories/servicehub/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,6 +28,7 @@
DuplicatedServiceBoundError,
ServiceObjNotFound,
SvcAttachmentDoesNotExist,
UnboundSvcAttachmentDoesNotExist,
)
from paasng.accessories.servicehub.local.manager import LocalPlanMgr, LocalServiceMgr, LocalServiceObj
from paasng.accessories.servicehub.models import (
Expand All @@ -36,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
Expand Down Expand Up @@ -218,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

Expand Down Expand Up @@ -273,6 +283,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"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Generated by Django 4.2.16 on 2024-12-11 07:40

from django.db import migrations, models
import django.db.models.deletion
import paasng.utils.models


class Migration(migrations.Migration):

dependencies = [
('services', '0006_alter_servicecategory_name_en'),
('engine', '0023_remove_deployment_hooks_remove_deployment_procfile'),
('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)),
('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(
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)),
('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')},
},
),
]
47 changes: 47 additions & 0 deletions apiserver/paasng/paasng/accessories/servicehub/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,33 @@ def __str__(self):
return "{prefix}-no-provision".format(prefix=prefix)


class UnboundServiceEngineAppAttachment(OwnerTimestampedModel):
songzxc789 marked this conversation as resolved.
Show resolved Hide resolved
"""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="蓝鲸引擎应用",
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:
songzxc789 marked this conversation as resolved.
Show resolved Hide resolved
unique_together = ("service", "engine_app")
verbose_name = "本地已解绑增强服务"

def clean_service_instance(self):
"""回收增强服务资源"""
self.service.delete_service_instance(self.service_instance)


class RemoteServiceModuleAttachment(OwnerTimestampedModel):
"""Binding relationship of module <-> remote service"""

Expand Down Expand Up @@ -151,6 +178,26 @@ class Meta:
unique_together = ("service_id", "engine_app")


class UnboundRemoteServiceEngineAppAttachment(OwnerTimestampedModel):
songzxc789 marked this conversation as resolved.
Show resolved Hide resolved
"""Remote service instance which is unbound with engine app"""

engine_app = models.ForeignKey(
"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")
credentials_enabled = models.BooleanField(default=True, verbose_name="是否使用凭证")

class Meta:
unique_together = ("service_id", "engine_app")
verbose_name = "远程已解绑增强服务"


class ServiceDBProperties:
"""Storing service related database properties"""

Expand Down
16 changes: 14 additions & 2 deletions apiserver/paasng/paasng/accessories/servicehub/remote/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
Loading