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: add api to control devcontainer #1163

Merged
merged 13 commits into from
Mar 18, 2024
6 changes: 6 additions & 0 deletions apiserver/paasng/paas_wl/bk_app/applications/models/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ def module_name(self):
def latest_config(self):
return self.config_set.latest()

@property
def use_dev_sandbox(self) -> bool:
if self.name.endswith("-dev"):
return True
return False

def __str__(self) -> str:
return f"<{self.name}, region: {self.region}, type: {self.type}>"

Expand Down
16 changes: 13 additions & 3 deletions apiserver/paasng/paas_wl/bk_app/deploy/app_res/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,16 +161,26 @@ def get_default_services(app: "WlApp", process_type: str) -> ProcDefaultServices
class NamespacesHandler(ResourceHandlerBase):
"""Handler for namespace resources"""

def delete(self, namespace):
def ensure_namespace(self, namespace: str, max_wait_seconds: int = 15):
"""确保命名空间存在, 如果命名空间不存在, 那么将创建一个 Namespace 和 ServiceAccount
:param namespace: 需要确保存在的 namespace
:param max_wait_seconds: 等待 ServiceAccount 就绪的时间
"""
self.create(namespace)
self.check_service_account_secret(namespace, max_wait_seconds=max_wait_seconds)

def delete(self, namespace: str):
"""k8s 直接删除 namespace 将清除其下所有资源"""
KNamespace(self.client).delete(namespace)

def create(self, namespace):
def create(self, namespace: str):
"""
:return: instance of namespace, created
"""
return KNamespace(self.client).get_or_create(namespace)

def check_service_account_secret(self, namespace, max_wait_seconds=15):
def check_service_account_secret(self, namespace: str, max_wait_seconds=15):
try:
KNamespace(self.client).wait_for_default_sa(namespace, timeout=max_wait_seconds)
except CreateServiceAccountTimeout:
Expand Down
18 changes: 18 additions & 0 deletions apiserver/paasng/paas_wl/bk_app/dev_sandbox/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# -*- 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.
"""
53 changes: 53 additions & 0 deletions apiserver/paasng/paas_wl/bk_app/dev_sandbox/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# -*- 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.
"""
from typing import List

from django.conf import settings

from .entities import IngressPathBackend, ServicePortPair

_ingress_service_conf = [
# dev sandbox 中 devserver 的路径与端口映射
{
"path_prefix": "/devserver/",
"service_port_name": "devserver",
"port": 8000,
"target_port": settings.DEV_SANDBOX_DEVSERVER_PORT,
},
# dev sandbox 中 saas 应用的路径与端口映射
{"path_prefix": "/", "service_port_name": "app", "port": 80, "target_port": settings.CONTAINER_PORT},
]


DEV_SANDBOX_SVC_PORT_PAIRS: List[ServicePortPair] = [
ServicePortPair(name=conf["service_port_name"], port=conf["port"], target_port=conf["target_port"])
for conf in _ingress_service_conf
]


def get_ingress_path_backends(service_name: str) -> List[IngressPathBackend]:
"""get ingress path backends from _ingress_service_conf with service_name"""
return [
IngressPathBackend(
path_prefix=conf["path_prefix"],
service_name=service_name,
service_port_name=conf["service_port_name"],
)
for conf in _ingress_service_conf
]
144 changes: 144 additions & 0 deletions apiserver/paasng/paas_wl/bk_app/dev_sandbox/controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# -*- 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.
"""
from typing import Dict

from django.conf import settings

from paas_wl.bk_app.applications.models import WlApp
from paas_wl.bk_app.deploy.app_res.controllers import NamespacesHandler
from paas_wl.bk_app.dev_sandbox.entities import DevSandboxDetail, HealthPhase, Resources, ResourceSpec, Runtime
from paas_wl.bk_app.dev_sandbox.kres_entities import (
DevSandbox,
DevSandboxIngress,
DevSandboxService,
get_dev_sandbox_name,
get_ingress_name,
)
from paas_wl.infras.resources.kube_res.base import AppEntityManager
from paas_wl.infras.resources.kube_res.exceptions import AppEntityNotFound
from paasng.platform.applications.models import Application
from paasng.platform.modules.constants import DEFAULT_ENGINE_APP_PREFIX, ModuleName

from .exceptions import DevSandboxAlreadyExists, DevSandboxResourceNotFound


class DevSandboxController:
"""DevSandbox Controller
:param app: Application 实例
:param module_name: 模块名称
"""

sandbox_mgr: AppEntityManager[DevSandbox] = AppEntityManager[DevSandbox](DevSandbox)
service_mgr: AppEntityManager[DevSandboxService] = AppEntityManager[DevSandboxService](DevSandboxService)
ingress_mgr: AppEntityManager[DevSandboxIngress] = AppEntityManager[DevSandboxIngress](DevSandboxIngress)

def __init__(self, app: Application, module_name: str):
self.app = app
self.dev_wl_app: WlApp = _DevWlAppCreator(app, module_name).create()

def deploy(self, envs: Dict[str, str]):
"""部署 dev sandbox
:param envs: 启动开发沙箱所需要的环境变量
"""
sandbox_name = get_dev_sandbox_name(self.dev_wl_app)
try:
self.sandbox_mgr.get(self.dev_wl_app, sandbox_name)
except AppEntityNotFound:
self._deploy(envs)
else:
raise DevSandboxAlreadyExists(f"dev sandbox {sandbox_name} already exists")

def delete(self):
"""通过直接删除命名空间的方式, 销毁 dev sandbox 服务"""
ns_handler = NamespacesHandler.new_by_app(self.dev_wl_app)
ns_handler.delete(namespace=self.dev_wl_app.namespace)

def get_sandbox_detail(self) -> DevSandboxDetail:
"""获取 dev sandbox 详情"""
try:
ingress_entity: DevSandboxIngress = self.ingress_mgr.get(
self.dev_wl_app, get_ingress_name(self.dev_wl_app)
)
except AppEntityNotFound:
raise DevSandboxResourceNotFound("dev sandbox url not found")

url = ingress_entity.domains[0].host

try:
container_entity: DevSandbox = self.sandbox_mgr.get(self.dev_wl_app, get_dev_sandbox_name(self.dev_wl_app))
except AppEntityNotFound:
raise DevSandboxResourceNotFound("dev sandbox not found")

status = container_entity.status.to_health_phase() if container_entity.status else HealthPhase.UNKNOWN
return DevSandboxDetail(url=url, envs=container_entity.runtime.envs, status=status)

def _deploy(self, envs: Dict[str, str]):
"""部署 sandbox 服务"""
# step 1. ensure namespace
ns_handler = NamespacesHandler.new_by_app(self.dev_wl_app)
ns_handler.ensure_namespace(namespace=self.dev_wl_app.namespace)

# step 2. create dev sandbox
default_resources = Resources(
limits=ResourceSpec(cpu="4", memory="2Gi"),
requests=ResourceSpec(cpu="200m", memory="512Mi"),
)

sandbox_entity = DevSandbox.create(
self.dev_wl_app,
runtime=Runtime(envs=envs, image=settings.DEV_SANDBOX_IMAGE),
resources=default_resources,
)
self.sandbox_mgr.create(sandbox_entity)

# step 3. upsert service
service_entity = DevSandboxService.create(self.dev_wl_app)
self.service_mgr.upsert(service_entity)

# step 4. upsert ingress
ingress_entity = DevSandboxIngress.create(self.dev_wl_app, self.app.code)
self.ingress_mgr.upsert(ingress_entity)


class _DevWlAppCreator:
"""WlApp 实例构造器"""

def __init__(self, app: Application, module_name: str):
self.app = app
self.module_name = module_name

def create(self) -> WlApp:
"""创建 WlApp 实例"""
dev_wl_app = WlApp(region=self.app.region, name=self._make_dev_wl_app_name(), type=self.app.type)
jamesgetx marked this conversation as resolved.
Show resolved Hide resolved

# 因为 dev_wl_app 不是查询集结果, 所以需要覆盖 namespace 和 module_name, 以保证 AppEntityManager 模式能够正常工作
# TODO 考虑更规范的方式处理这两个 cached_property 属性. 如考虑使用 WlAppProtocol 满足 AppEntityManager 模式
setattr(dev_wl_app, "namespace", f"{DEFAULT_ENGINE_APP_PREFIX}-{self.app.code}-dev".replace("_", "0us0"))
setattr(dev_wl_app, "module_name", self.module_name)

return dev_wl_app

def _make_dev_wl_app_name(self) -> str:
"""参考 make_engine_app_name 规则, 生成 dev 环境的 WlApp name"""
if self.module_name == ModuleName.DEFAULT.value:
return f"{DEFAULT_ENGINE_APP_PREFIX}-{self.app.code}-dev"
else:
return f"{DEFAULT_ENGINE_APP_PREFIX}-{self.app.code}-m-{self.module_name}-dev"
118 changes: 118 additions & 0 deletions apiserver/paasng/paas_wl/bk_app/dev_sandbox/entities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# -*- 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.
"""
from dataclasses import dataclass, field
from typing import Dict, List, Optional

from blue_krill.data_types.enum import EnumField, StructuredEnum

from paas_wl.workloads.release_controller.constants import ImagePullPolicy


class HealthPhase(str, StructuredEnum):
HEALTHY = EnumField("Healthy")
PROGRESSING = EnumField("Progressing")
UNHEALTHY = EnumField("Unhealthy")
UNKNOWN = EnumField("Unknown")


@dataclass
class Runtime:
"""容器运行相关配置"""

envs: Dict[str, str]
image: str
image_pull_policy: ImagePullPolicy = field(default=ImagePullPolicy.ALWAYS)


@dataclass
class ResourceSpec:
cpu: str
memory: str

def to_dict(self):
return {
"cpu": self.cpu,
"memory": self.memory,
}


@dataclass
class Resources:
"""计算资源定义"""

limits: Optional[ResourceSpec] = None
requests: Optional[ResourceSpec] = None

def to_dict(self):
d = {}
if self.limits:
d["limits"] = self.limits.to_dict()
if self.requests:
d["requests"] = self.requests.to_dict()
return d


@dataclass
class Status:
replicas: int
ready_replicas: int

def to_health_phase(self) -> str:
if self.replicas == self.ready_replicas:
return HealthPhase.HEALTHY

# TODO 如果需要细化出 Unhealthy, 可以结合 Conditions 处理
# 将 Unhealthy 也并入 Progressing, 简化需求
return HealthPhase.PROGRESSING


@dataclass
class ServicePortPair:
"""Service port pair"""

name: str
port: int
target_port: int
protocol: str = "TCP"


@dataclass
class IngressPathBackend:
"""Ingress Path Backend object"""

path_prefix: str
service_name: str
service_port_name: str


@dataclass
class IngressDomain:
"""Ingress Domain object"""

host: str
path_backends: List[IngressPathBackend]
tls_enabled: bool = False
tls_secret_name: str = ""


@dataclass
class DevSandboxDetail:
url: str
envs: Dict[str, str]
status: str
Loading