diff --git a/apiserver/paasng/conf.yaml.tpl b/apiserver/paasng/conf.yaml.tpl index 1f263bc7b1..b011da19dd 100644 --- a/apiserver/paasng/conf.yaml.tpl +++ b/apiserver/paasng/conf.yaml.tpl @@ -796,6 +796,26 @@ BK_AUDIT_ENDPOINT = "" # SERVICES_PLUGINS: {} +## ---------------------------------------- 沙箱相关配置 ---------------------------------------- + +## dev sandbox 中 devserver 的监听端口 +# DEV_SANDBOX_DEVSERVER_PORT: 8000 +## 沙箱镜像 +# DEV_SANDBOX_IMAGE: '' +## 沙箱工作目录 +# DEV_SANDBOX_WORKSPACE: '/cnb/devsandbox/src' +## 启动沙箱的数量上限,管理员通过集群的剩余资源计算得出 +# DEV_SANDBOX_COUNT_LIMIT: 5 +## 沙箱跨域访问源地址 +# DEV_SANDBOX_CORS_ALLOW_ORIGINS: '' + +## dev sandbox 中 code-editor 的监听地址 +# CODE_EDITOR_PORT: 8080 +## code-editor 的镜像 +# CODE_EDITOR_IMAGE: codercom/code-server:4.9.0 +## code-editor 的项目启动目录 +# CODE_EDITOR_START_DIR: '/home/coder/project' + ## ---------------------------------------- 资源限制配置 ---------------------------------------- ## Web 模块默认副本数量,默认值:{'stag': 1, 'prod': 2} diff --git a/apiserver/paasng/paas_wl/bk_app/dev_sandbox/kres_slzs/code_editor.py b/apiserver/paasng/paas_wl/bk_app/dev_sandbox/kres_slzs/code_editor.py index f017254e2f..35b310d035 100644 --- a/apiserver/paasng/paas_wl/bk_app/dev_sandbox/kres_slzs/code_editor.py +++ b/apiserver/paasng/paas_wl/bk_app/dev_sandbox/kres_slzs/code_editor.py @@ -61,6 +61,9 @@ def _construct_pod_spec(self, obj: "CodeEditor") -> Dict: "env": [{"name": str(key), "value": str(value)} for key, value in obj.runtime.envs.items()], "imagePullPolicy": obj.runtime.image_pull_policy, "ports": [{"containerPort": port_pair.target_port} for port_pair in CODE_SVC_PORT_PAIRS], + "readinessProbe": { + "httpGet": {"port": settings.CODE_EDITOR_PORT, "path": "/healthz"}, + }, } if obj.resources: diff --git a/apiserver/paasng/paas_wl/bk_app/dev_sandbox/kres_slzs/sandbox.py b/apiserver/paasng/paas_wl/bk_app/dev_sandbox/kres_slzs/sandbox.py index 1751c564f0..c74e1e595f 100644 --- a/apiserver/paasng/paas_wl/bk_app/dev_sandbox/kres_slzs/sandbox.py +++ b/apiserver/paasng/paas_wl/bk_app/dev_sandbox/kres_slzs/sandbox.py @@ -61,6 +61,9 @@ def _construct_pod_spec(self, obj: "DevSandbox") -> Dict: "env": [{"name": str(key), "value": str(value)} for key, value in obj.runtime.envs.items()], "imagePullPolicy": obj.runtime.image_pull_policy, "ports": [{"containerPort": port_pair.target_port} for port_pair in DEV_SANDBOX_SVC_PORT_PAIRS], + "readinessProbe": { + "httpGet": {"port": settings.DEV_SANDBOX_DEVSERVER_PORT, "path": "/healthz"}, + }, } if obj.resources: diff --git a/apiserver/paasng/paasng/accessories/dev_sandbox/config_var.py b/apiserver/paasng/paasng/accessories/dev_sandbox/config_var.py index 939bd20466..5dcfd68155 100644 --- a/apiserver/paasng/paasng/accessories/dev_sandbox/config_var.py +++ b/apiserver/paasng/paasng/accessories/dev_sandbox/config_var.py @@ -50,6 +50,10 @@ def generate_envs(app: Application, module: Module) -> Dict[str, str]: envs["REQUIRED_BUILDPACKS"] = _buildpacks_as_build_env(buildpacks) envs.update(_get_devserver_env()) + + # Inject cors config + envs.update({"CORS_ALLOW_ORIGINS": settings.DEV_SANDBOX_CORS_ALLOW_ORIGINS}) + return envs diff --git a/apiserver/paasng/paasng/accessories/dev_sandbox/management/commands/recycle_dev_sandbox.py b/apiserver/paasng/paasng/accessories/dev_sandbox/management/commands/recycle_dev_sandbox.py index 2a90b13e0c..62b10ab8e7 100644 --- a/apiserver/paasng/paasng/accessories/dev_sandbox/management/commands/recycle_dev_sandbox.py +++ b/apiserver/paasng/paasng/accessories/dev_sandbox/management/commands/recycle_dev_sandbox.py @@ -53,6 +53,6 @@ def handle(self, all, *args, **options): dev_sandbox_code=dev_sandbox.code, owner=dev_sandbox.owner, ) - if all or dev_sandbox.is_expired(): + if all or dev_sandbox.should_recycle(): controller.delete() dev_sandbox.delete() diff --git a/apiserver/paasng/paasng/accessories/dev_sandbox/management/commands/renew_dev_sandbox_expired_at.py b/apiserver/paasng/paasng/accessories/dev_sandbox/management/commands/renew_dev_sandbox_expired_at.py index 6dcaea8d49..ea4b67eaef 100644 --- a/apiserver/paasng/paasng/accessories/dev_sandbox/management/commands/renew_dev_sandbox_expired_at.py +++ b/apiserver/paasng/paasng/accessories/dev_sandbox/management/commands/renew_dev_sandbox_expired_at.py @@ -47,7 +47,7 @@ def handle(self, *args, **options): url = detail.urls.code_editor_health_url # 沙箱相关域名无法确定协议,因此全部遍历 http 和 https if check_alive(f"http://{url}") or check_alive(f"https://{url}"): - dev_sandbox.renew_expire_at() + dev_sandbox.renew_expired_at() # 通过 code_editor_health_url 检查沙箱是否存活 diff --git a/apiserver/paasng/paasng/accessories/dev_sandbox/models.py b/apiserver/paasng/paasng/accessories/dev_sandbox/models.py index ad3d9ce6e3..b38296276b 100644 --- a/apiserver/paasng/paasng/accessories/dev_sandbox/models.py +++ b/apiserver/paasng/paasng/accessories/dev_sandbox/models.py @@ -50,7 +50,7 @@ class DevSandbox(OwnerTimestampedModel): def renew_expired_at(self): self.expired_at = timezone.now() + timezone.timedelta(hours=2) - self.save(update_fields=["expire_at"]) + self.save(update_fields=["expired_at"]) def should_recycle(self) -> bool: """检查是否应该被回收""" diff --git a/apiserver/paasng/paasng/accessories/dev_sandbox/serializers.py b/apiserver/paasng/paasng/accessories/dev_sandbox/serializers.py index cceafc18a3..66a6c7ab16 100644 --- a/apiserver/paasng/paasng/accessories/dev_sandbox/serializers.py +++ b/apiserver/paasng/paasng/accessories/dev_sandbox/serializers.py @@ -16,6 +16,7 @@ # to the current version of the project delivered to anyone in the future. from dataclasses import asdict +from typing import Dict from rest_framework import serializers from rest_framework.fields import SerializerMethodField @@ -28,10 +29,19 @@ class DevSandboxDetailSLZ(serializers.Serializer): """Serializer for dev sandbox detail""" - url = serializers.CharField(help_text="dev sandbox 服务地址") + app_url = serializers.SerializerMethodField(help_text="dev sandbox saas 应用服务地址") + devserver_url = serializers.SerializerMethodField(help_text="dev sandbox devserver 服务地址") token = serializers.CharField(help_text="访问 dev sandbox 中 devserver 服务的 token") status = serializers.ChoiceField(choices=HealthPhase.get_django_choices(), help_text="dev sandbox 的运行状态") + def get_app_url(self, obj: Dict[str, str]) -> str: + # 拼接 app_url + return f"{obj['url']}/app/" + + def get_devserver_url(self, obj: Dict[str, str]) -> str: + # 拼接 devserver_url + return f"{obj['url']}/devserver/" + class CreateDevSandboxWithCodeEditorSLZ(serializers.Serializer): """Serializer for create dev sandbox""" @@ -84,7 +94,7 @@ class Meta: fields = [ "id", "status", - "expire_at", + "expired_at", "version_info_dict", "created", "updated", diff --git a/apiserver/paasng/paasng/accessories/dev_sandbox/urls.py b/apiserver/paasng/paasng/accessories/dev_sandbox/urls.py index 51cbd8d03e..46bad85e28 100644 --- a/apiserver/paasng/paasng/accessories/dev_sandbox/urls.py +++ b/apiserver/paasng/paasng/accessories/dev_sandbox/urls.py @@ -28,6 +28,10 @@ make_app_pattern(r"/user/dev_sandbox_with_code_editor/$", include_envs=False), DevSandboxWithCodeEditorViewSet.as_view({"post": "deploy", "delete": "delete", "get": "get_detail"}), ), + re_path( + r"api/bkapps/applications/(?P[^/]+)/user/dev_sandbox_with_code_editors/pre_deploy_check/$", + DevSandboxWithCodeEditorViewSet.as_view({"get": "pre_deploy_check"}), + ), re_path( r"api/bkapps/applications/(?P[^/]+)/user/dev_sandbox_with_code_editors/lists/$", DevSandboxWithCodeEditorViewSet.as_view({"get": "list_app_dev_sandbox"}), diff --git a/apiserver/paasng/paasng/accessories/dev_sandbox/views.py b/apiserver/paasng/paasng/accessories/dev_sandbox/views.py index 75f69b34f6..48f4d34258 100644 --- a/apiserver/paasng/paasng/accessories/dev_sandbox/views.py +++ b/apiserver/paasng/paasng/accessories/dev_sandbox/views.py @@ -19,6 +19,7 @@ from typing import Dict from bkpaas_auth.models import User +from django.conf import settings from drf_yasg.utils import swagger_auto_schema from rest_framework import status from rest_framework.permissions import IsAuthenticated @@ -98,6 +99,9 @@ class DevSandboxWithCodeEditorViewSet(GenericViewSet, ApplicationCodeInPathMixin @swagger_auto_schema(request_body=CreateDevSandboxWithCodeEditorSLZ, responses={"201": "没有返回数据"}) def deploy(self, request, code, module_name): """部署开发沙箱""" + if DevSandbox.objects.count() >= settings.DEV_SANDBOX_COUNT_LIMIT: + raise error_codes.DEV_SANDBOX_COUNT_OVER_LIMIT + app = self.get_application() module = self.get_module_via_path() @@ -126,7 +130,7 @@ def deploy(self, request, code, module_name): code=dev_sandbox_code, ) # 更新过期时间 - dev_sandbox.renew_expire_at() + dev_sandbox.renew_expired_at() # 生成代码编辑器密码 password = generate_password() @@ -164,9 +168,12 @@ def deploy(self, request, code, module_name): password=password, ) except DevSandboxAlreadyExists: + # 开发沙箱已存在,只删除 model 对象,不删除沙箱资源 dev_sandbox.delete() raise error_codes.DEV_SANDBOX_ALREADY_EXISTS except Exception: + # 除了沙箱已存在的情况,其它创建异常情况下,清理沙箱资源 + controller.delete() dev_sandbox.delete() raise @@ -247,6 +254,15 @@ def list_app_dev_sandbox(self, request, code): return Response(data=DevSandboxSLZ(dev_sandboxes, many=True).data) + @swagger_auto_schema(tags=["开发沙箱"]) + def pre_deploy_check(self, request, code): + """部署前确认是否可以部署""" + # 判断开发沙箱数量是否超过限制 + if DevSandbox.objects.count() >= settings.DEV_SANDBOX_COUNT_LIMIT: + return Response(data={"result": False}) + + return Response(data={"result": True}) + @staticmethod def _get_version_info(user: User, module: Module, params: Dict) -> VersionInfo: """Get VersionInfo from user inputted params""" diff --git a/apiserver/paasng/paasng/settings/workloads.py b/apiserver/paasng/paasng/settings/workloads.py index 77e9b1dd22..a57e65fdfa 100644 --- a/apiserver/paasng/paasng/settings/workloads.py +++ b/apiserver/paasng/paasng/settings/workloads.py @@ -85,21 +85,33 @@ # 默认容器内监听地址 CONTAINER_PORT = settings.get("CONTAINER_PORT", 5000) -# dev sandbox 中 devserver 的监听地址 +# 服务相关插件配置 +SERVICES_PLUGINS = settings.get("SERVICES_PLUGINS", default={}) + + +# --------------- +# 沙箱相关配置 +# --------------- + +# dev sandbox 中 devserver 的监听端口 DEV_SANDBOX_DEVSERVER_PORT = settings.get("DEV_SANDBOX_DEVSERVER_PORT", 8000) +# 沙箱镜像 DEV_SANDBOX_IMAGE = settings.get("DEV_SANDBOX_IMAGE", "bkpaas/dev-heroku-bionic:latest") +# 沙箱工作目录 DEV_SANDBOX_WORKSPACE = settings.get("DEV_SANDBOX_WORKSPACE", "/cnb/devsandbox/src") +# 启动沙箱的数量上限,管理员通过集群的剩余资源计算得出 +DEV_SANDBOX_COUNT_LIMIT = settings.get("DEV_SANDBOX_COUNT_LIMIT", 5) +# 沙箱跨域访问源地址 +DEV_SANDBOX_CORS_ALLOW_ORIGINS = settings.get("DEV_SANDBOX_CORS_ALLOW_ORIGINS", "") # dev sandbox 中 code-editor 的监听地址 CODE_EDITOR_PORT = settings.get("CODE_EDITOR_PORT", 8080) +# code-editor 的镜像 CODE_EDITOR_IMAGE = settings.get("CODE_EDITOR_IMAGE", "codercom/code-server:4.9.0") # code-editor 的项目启动目录 CODE_EDITOR_START_DIR = settings.get("CODE_EDITOR_START_DIR", "/home/coder/project") -# 服务相关插件配置 -SERVICES_PLUGINS = settings.get("SERVICES_PLUGINS", default={}) - # --------------- # 资源命名配置 # --------------- diff --git a/apiserver/paasng/paasng/utils/error_codes.py b/apiserver/paasng/paasng/utils/error_codes.py index 4b37fd21c6..eb20920134 100644 --- a/apiserver/paasng/paasng/utils/error_codes.py +++ b/apiserver/paasng/paasng/utils/error_codes.py @@ -184,6 +184,7 @@ class ErrorCodes: # dev sandbox DEV_SANDBOX_ALREADY_EXISTS = ErrorCode("dev sandbox already exists", status_code=409) DEV_SANDBOX_NOT_FOUND = ErrorCode("dev sandbox not found") + DEV_SANDBOX_COUNT_OVER_LIMIT = ErrorCode("dev sandbox count over limit") def dump(self, fh=None): """A function to dump ErrorCodes as markdown table.""" diff --git a/apiserver/paasng/tests/api/test_dev_sandbox.py b/apiserver/paasng/tests/api/test_dev_sandbox.py index ea351cfb8a..1a837d7a2a 100644 --- a/apiserver/paasng/tests/api/test_dev_sandbox.py +++ b/apiserver/paasng/tests/api/test_dev_sandbox.py @@ -56,4 +56,9 @@ def test_get_container_detail(self, api_client, bk_app, bk_module): ) response = api_client.get(f"/api/bkapps/applications/{bk_app.code}/modules/{bk_module.name}/dev_sandbox/") assert response.status_code == 200 - assert response.data == {"url": "http://bkpaas.devcontainer.com", "token": token, "status": "Healthy"} + assert response.data == { + "devserver_url": "http://bkpaas.devcontainer.com/devserver/", + "app_url": "http://bkpaas.devcontainer.com/app/", + "token": token, + "status": "Healthy", + } diff --git a/apiserver/paasng/tests/paas_wl/bk_app/dev_sandbox/test_kres_slzs.py b/apiserver/paasng/tests/paas_wl/bk_app/dev_sandbox/test_kres_slzs.py index aa4d1bbb8d..073d565414 100644 --- a/apiserver/paasng/tests/paas_wl/bk_app/dev_sandbox/test_kres_slzs.py +++ b/apiserver/paasng/tests/paas_wl/bk_app/dev_sandbox/test_kres_slzs.py @@ -80,6 +80,9 @@ def test_serialize(self, gvk_config, dev_sandbox_entity): {"containerPort": settings.DEV_SANDBOX_DEVSERVER_PORT}, {"containerPort": settings.CONTAINER_PORT}, ], + "readinessProbe": { + "httpGet": {"port": settings.DEV_SANDBOX_DEVSERVER_PORT, "path": "/healthz"}, + }, "resources": { "requests": {"cpu": "200m", "memory": "512Mi"}, "limits": {"cpu": "4", "memory": "2Gi"}, @@ -125,6 +128,9 @@ def test_serialize_with_source_code_config(self, gvk_config, source_configured_d {"containerPort": settings.DEV_SANDBOX_DEVSERVER_PORT}, {"containerPort": settings.CONTAINER_PORT}, ], + "readinessProbe": { + "httpGet": {"port": settings.DEV_SANDBOX_DEVSERVER_PORT, "path": "/healthz"}, + }, "resources": { "requests": {"cpu": "200m", "memory": "512Mi"}, "limits": {"cpu": "4", "memory": "2Gi"}, @@ -347,6 +353,9 @@ def test_serialize(self, gvk_config, code_editor_entity): "ports": [ {"containerPort": settings.CODE_EDITOR_PORT}, ], + "readinessProbe": { + "httpGet": {"port": settings.CODE_EDITOR_PORT, "path": "/healthz"}, + }, "resources": { "requests": {"cpu": "200m", "memory": "512Mi"}, "limits": {"cpu": "4", "memory": "2Gi"},