diff --git a/bcs-app/backend/components/bcs_api.py b/bcs-app/backend/components/bcs_api/__init__.py similarity index 85% rename from bcs-app/backend/components/bcs_api.py rename to bcs-app/backend/components/bcs_api/__init__.py index 4c30ed28a..8150da549 100644 --- a/bcs-app/backend/components/bcs_api.py +++ b/bcs-app/backend/components/bcs_api/__init__.py @@ -12,13 +12,14 @@ an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ +import json from typing import Dict from django.conf import settings from requests import PreparedRequest from requests.auth import AuthBase -from .base import BaseHttpClient, BkApiClient, ComponentAuth, update_url_parameters +from ..base import BaseHttpClient, BkApiClient, ComponentAuth, update_url_parameters class BcsApiConfig: @@ -49,6 +50,17 @@ def __call__(self, r: PreparedRequest): return r +class BcsApiGatewayAuth(BcsApiAuth): + """用于调用 bcs-api-gateway 系统的鉴权对象""" + + def __call__(self, r: PreparedRequest): + # 从配置文件读取访问系统的 admin token, 放置到请求头中 + r.headers["Authorization"] = f"Bearer {getattr(settings, 'BCS_API_GW_AUTH_TOKEN', '')}" + r.headers["Content-Type"] = "application/json" + r.headers['X-BKAPI-AUTHORIZATION'] = json.dumps({"access_token": self.access_token}) + return r + + class BcsApiClient(BkApiClient): """访问 BCS API 服务的 Client 对象 @@ -69,7 +81,6 @@ def __init__(self, auth: ComponentAuth): def query_cluster_id(self, env_name: str, project_id: str, cluster_id: str) -> str: """查询集群在 BCS-Api 中的 ID - :returns: 集群 ID 字符串 """ url = self._config.query_cluster_id_url.format(env_name=env_name) @@ -81,7 +92,6 @@ def query_cluster_id(self, env_name: str, project_id: str, cluster_id: str) -> s def get_cluster_credentials(self, env_name: str, bcs_cluster_id: str) -> Dict: """ 获取访问集群 apiserver 所需的鉴权信息,比如证书、user_token、server_address_path 等 - :returns: 包含集群鉴权信息的字典 """ url = self._config.get_cluster_credentials_url.format(env_name=env_name, bcs_cluster_id=bcs_cluster_id) diff --git a/bcs-app/backend/components/bcs_api/cluster.py b/bcs-app/backend/components/bcs_api/cluster.py new file mode 100644 index 000000000..30da92c5e --- /dev/null +++ b/bcs-app/backend/components/bcs_api/cluster.py @@ -0,0 +1,206 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) available. +Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://opensource.org/licenses/MIT + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" +from typing import Dict, List + +from attr import asdict, dataclass +from django.conf import settings + +from ..base import BaseHttpClient, BkApiClient, ComponentAuth, response_handler +from . import BcsApiGatewayAuth +from .constants import BCS_API_VERSION, ClusterManageType, ManageType + + +class BcsApiGatewayConfig: + def __init__(self, host: str, env: str): + self.host = host + + _cluster_api_url_prefix = f"{host}/{env}/{BCS_API_VERSION}/clustermanager/v1" + # cluster manager中集群相关接口url + self.add_cluster_url = f"{_cluster_api_url_prefix}/cluster/{{cluster_id}}" + self.delete_cluster_url = f"{_cluster_api_url_prefix}/cluster/{{cluster_id}}" + self.update_cluster_url = f"{_cluster_api_url_prefix}/cluster/{{cluster_id}}" + self.add_nodes_url = f"{_cluster_api_url_prefix}/cluster/{{cluster_id}}/node" + self.delete_nodes_url = f"{_cluster_api_url_prefix}/cluster/{{cluster_id}}/node" + self.query_task_url = f"{_cluster_api_url_prefix}/task/{{task_id}}" + + +@dataclass +class CloudClusterConfig: + """集群初始化需要的基本配置 + region: 区域 + manageType: 集群管理类型,公有云时生效,MANAGED_CLUSTER(云上托管集群),INDEPENDENT_CLUSTER(独立集群,自行维护) + master: 集群master + vpcID: vpc ID + cloudID: 所属云ID + nodes: 集群的节点 + networkSettings: 网络配置 + clusterBasicSettings: 集群的基本配置 + clusterAdvanceSettings: 集群的高级配置 + nodeSettings: 节点配置信息 + systemReinstall: 是否重装master节点的系统,机器被托管情况下有效 + initLoginPassword: 重装master节点的系统时,初始化password,机器被托管情况下有效 + status: 状态 + """ + + region: str + master: List + manageType: str = ManageType.MANAGED_CLUSTER + vpcID: str = "" + cloudID: str = "" + nodes: List = [] + networkSettings: Dict = {} + clusterBasicSettings: Dict = {} + clusterAdvanceSettings: Dict = {} + nodeSettings: Dict = {} + systemReinstall: bool = False + initLoginPassword: str = "" + status: str = "" + + +@dataclass +class BcsBasicConfig: + """集群初始化时,BCS需要的基本配置""" + + projectID: str + businessID: str + clusterID: str + clusterName: str + provider: str + environment: str # 集群环境,如prod、debug、test + engineType: str # 引擎类型,如k8s + clusterType: str = ClusterManageType.SINGLE + isExclusive: bool = False # 是否为独占集群 + federationClusterID: str = "" # 联邦集群ID + labels: Dict = {} + onlyCreateInfo: bool = False + bcsAddons: Dict = {} + extraAddons: Dict = {} + + +@dataclass +class ClusterConfig: + """集群配置""" + + creator: str + bcs_basic_config: BcsBasicConfig + cloud_cluster_config: CloudClusterConfig + + +@dataclass +class UpdatedClusterConfig: + """更新的集群配置选项""" + + projectID: str + clusterID: str + updater: str + status: str + clusterName: str = "" + labels: Dict = {} + + +@dataclass +class NodeConfig: + nodes: List[str] + nodeGroupID: str = "" + onlyCreateInfo: bool = False + initLoginPassword: str = "" + + +class BcsClusterApiClient(BkApiClient): + """访问 BCS 集群 API 服务的 Client 对象 + :param auth: 包含校验信息的对象 + """ + + def __init__(self, auth: ComponentAuth): + self._config = BcsApiGatewayConfig(host=settings.BCS_API_GW_DOMAIN, env=settings.BCS_API_GW_ENV) + self._client = BaseHttpClient(BcsApiGatewayAuth(auth.access_token)) + + def add_cluster(self, cluster_config: ClusterConfig) -> Dict: + """添加集群 + :param creator: 创建者 + :param cluster_config: 集群初始化的配置,包含bcs平台需要的信息和集群初始化需要的信息 + :return: 返回初始化的集群的信息,任务信息,格式: {"data": {}, "task": {}} + """ + # 组装参数 + cluster_config_data = asdict(cluster_config.cloud_cluster_config) + cluster_config_data.update(asdict(cluster_config.bcs_basic_config), creator=cluster_config.creator) + # 下发初始化配置 + url = self._config.add_cluster_url.format(cluster_id=cluster_config_data["clusterID"]) + return self._client.request_json("POST", url, json=cluster_config_data) + + @response_handler() + def update_cluster(self, cluster_config: UpdatedClusterConfig) -> Dict: + """更新集群 + :param cluster_config: 更新集群的配置 + :return: 返回集群的配置 + """ + # 组装参数,并去掉为None的属性 + cluster_config_data = {k: v for k, v in asdict(cluster_config).items() if v is not None} + # 下发更新的配置 + url = self._config.update_cluster_url.format(cluster_id=cluster_config_data["clusterID"]) + return self._client.request_json("PUT", url, json=cluster_config_data) + + def delete_cluster( + self, + cluster_id: str, + is_force: bool = False, + is_clean_resource: bool = True, + only_delete_info: bool = False, + ) -> Dict: + """删除集群 + :param cluster_id: 集群ID + :param is_force: 是否强制删除,默认为False + :param is_clean_resource: 强制删除有效时,是否清理机器上部署的资源 + :param only_delete_info: 是否仅删除记录的信息,默认为False + :return: 返回删除的集群信息,任务信息 + """ + url = self._config.delete_cluster_url.format(cluster_id=cluster_id) + params = {"isForced": is_force, "resourceClean": is_clean_resource, "onlyDeleteInfo": only_delete_info} + return self._client.request_json("DELETE", url, params=params) + + @response_handler() + def add_nodes(self, cluster_id: str, node_config: NodeConfig) -> Dict: + """添加节点 + :param cluster_id: 集群ID + :param node_config: 节点配置 + :return: 返回节点的数据,包含任务ID + """ + url = self._config.add_nodes_url.format(cluster_id=cluster_id) + data = asdict(node_config) + data["clusterID"] = cluster_id + return self._client.request_json("POST", url, json=data) + + @response_handler() + def delete_nodes( + self, cluster_id: str, nodes: List[str], delete_mode: str = "RETAIN", is_force: bool = False + ) -> Dict: + """删除节点 + :param cluster_id: 集群ID + :param nodes: 节点列表 + :param delete_mode: 删除模式,RETAIN(移除集群,但是保留主机),TERMINATE(只支持按量计费的机器) + :param is_force: 是否强制删除 + :return: 返回删除的节点信息,包含任务ID + """ + url = self._config.delete_nodes_url.format(cluster_id=cluster_id) + data = {"clusterID": cluster_id, "nodes": nodes, "deleteMode": delete_mode, "isForce": is_force} + return self._client.request_json("DELETE", url, json=data) + + def query_task(self, task_id: str) -> Dict: + """查询任务状态 + :param task_id: 任务ID + :return: 返回任务的执行详情 + """ + url = self._config.query_task_url.format(task_id=task_id) + return self._client.request_json("GET", url) diff --git a/bcs-app/backend/components/bcs_api/constants.py b/bcs-app/backend/components/bcs_api/constants.py new file mode 100644 index 000000000..9bd46c9ba --- /dev/null +++ b/bcs-app/backend/components/bcs_api/constants.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) available. +Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://opensource.org/licenses/MIT + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" +from django.utils.translation import ugettext as _ + +from backend.packages.blue_krill.data_types.enum import EnumField, StructuredEnum + +# bcs api的版本 +BCS_API_VERSION = "v4" + +# 资源记录不存在的code +RECORD_NOT_EXIST_CODE = 1405420 +# 资源记录存在的code +RECORD_EXIST_CODE = 1405405 + + +class DeployType(str, StructuredEnum): + """部署类型, 使用物理机或者容器部署""" + + Physical = EnumField(1, label=_("物理机部署")) + Container = EnumField(2, label=_("容器部署")) + + +class ProjectType(str, StructuredEnum): + """项目类型,项目属于平台或者业务""" + + Platform = EnumField(1, label="platform") + Business = EnumField(2, label="business") + + +class ManageType(str, StructuredEnum): + """集群管理类型""" + + MANAGED_CLUSTER = EnumField("MANAGED_CLUSTER", label=_("云上托管集群")) + INDEPENDENT_CLUSTER = EnumField("INDEPENDENT_CLUSTER", label=_("独立集群,自行维护")) + + +class ClusterManageType(str, StructuredEnum): + """集群管理类型""" + + SINGLE = EnumField("single", label=_("独立集群")) + FEDERATION = EnumField("federation", label=_("联邦集群")) diff --git a/bcs-app/backend/components/bcs_api/project.py b/bcs-app/backend/components/bcs_api/project.py new file mode 100644 index 000000000..a8746f7ba --- /dev/null +++ b/bcs-app/backend/components/bcs_api/project.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) available. +Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://opensource.org/licenses/MIT + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" +from typing import Dict + +from attr import asdict, dataclass +from django.conf import settings + +from ..base import BaseHttpClient, BkApiClient, ComponentAuth, response_handler +from . import BcsApiGatewayAuth +from .constants import BCS_API_VERSION, DeployType, ProjectType + + +class BcsApiGatewayConfig: + def __init__(self, host: str, env: str): + self.host = host + + # cluster manager相关接口url + _project_api_url_prefix = f"{host}/{env}/{BCS_API_VERSION}/clustermanager/v1/project" + self.query_project_url = f"{_project_api_url_prefix}/{{project_id}}" + self.create_project_url = f"{_project_api_url_prefix}/{{project_id}}" + self.update_project_url = f"{_project_api_url_prefix}/{{project_id}}" + + +@dataclass +class ProjectReservedConfig: + bgID: str = "" + bgName: str = "" + deptID: str = "" + deptName: str = "" + centerID: str = "" + centerName: str = "" + isSecret: bool = False # 标识项目是否有私密项目,默认为False + deployType: int = DeployType.Physical # 业务部署类型,1:物理机部署,2:容器部署 + isOffline: bool = False # 项目是否已经离线,默认False + useBKRes: bool = False # 是否使用蓝鲸提供的资源池,主要用于资源计费,默认False + projectType: int = ProjectType.Platform # 1:platform,2:business + + +@dataclass +class ProjectBasicConfig: + """项目的基本配置 + projectID: 项目ID,长度为32位字符串 + name: 项目名称 + englishName: 项目英文缩写,长度不能超过32字符 + kind: 项目中集群类型,支持k8s + businessID: 项目绑定的BK CC的业务ID + credentials: 记录的账户信息 + description: 项目的描述信息,默认为空 + """ + + projectID: str + name: str + englishName: str + kind: str + businessID: str + description: str + credentials: Dict = {} + + +@dataclass +class ProjectConfig: + """项目配置信息""" + + creator: str + basic_config: ProjectBasicConfig + reserved_config: ProjectReservedConfig + + +@dataclass +class UpdatedProjectConfig: + """更新项目配置信息""" + + projectID: str + updater: str + name: str + kind: str + businessID: str + + +class BcsProjectApiClient(BkApiClient): + """访问 BCS 项目 API 服务的 Client 对象 + :param auth: 包含校验信息的对象 + """ + + def __init__(self, auth: ComponentAuth): + self._config = BcsApiGatewayConfig(host=settings.BCS_API_GW_DOMAIN, env=settings.BCS_API_GW_ENV) + self._client = BaseHttpClient(BcsApiGatewayAuth(auth.access_token)) + + @response_handler() + def query_project(self, project_id: str) -> Dict: + """查询项目信息 + :param project_id: 项目ID + :return: 项目信息 + """ + url = self._config.query_project_url.format(project_id=project_id) + return self._client.request_json("GET", url) + + def create_project(self, project_config: ProjectConfig) -> Dict: + """创建项目 + :param project_config: 项目信息,包含项目的基本信息和项目的保留字段信息 + :return: 项目信息 + """ + # 组装参数 + project_config_data = asdict(project_config.basic_config) + project_config_data.update(asdict(project_config.reserved_config), creator=project_config.creator) + # 下发配置 + url = self._config.create_project_url.format(project_id=project_config_data["projectID"]) + return self._client.request_json("POST", url, json=project_config_data) + + def update_project(self, project_config: UpdatedProjectConfig) -> Dict: + """更新项目 + :param project_config: 更新的项目信息 + :return: 更新的项目信息 + """ + # 组装参数 + project_config_data = asdict(project_config) + # 下发配置 + url = self._config.create_project_url.format(project_id=project_config_data["projectID"]) + return self._client.request_json("PUT", url, json=project_config_data) diff --git a/bcs-app/backend/components/paas_cc.py b/bcs-app/backend/components/paas_cc.py index f8afcc170..cdfcb4bb4 100644 --- a/bcs-app/backend/components/paas_cc.py +++ b/bcs-app/backend/components/paas_cc.py @@ -521,6 +521,9 @@ def __init__(self, host: str): self.update_node_list_url = f"{host}/projects/{{project_id}}/clusters/{{cluster_id}}/nodes/" self.get_cluster_namespace_list_url = f"{host}/projects/{{project_id}}/clusters/{{cluster_id}}/namespaces/" self.get_node_list_url = f"{host}/projects/{{project_id}}/nodes/" + self.update_project_url = f"{host}/projects/{{project_id}}/" + self.add_cluster_url = f"{host}/projects/{{project_id}}/clusters/" + self.add_nodes_url = f"{host}/projects/{{project_id}}/clusters/{{cluster_id}}/nodes/" @dataclass @@ -531,7 +534,6 @@ class UpdateNodesData: class PaaSCCClient(BkApiClient): """访问 PaaSCC 服务的 Client 对象 - :param auth: 包含校验信息的对象 """ @@ -553,7 +555,6 @@ def get_project(self, project_id: str) -> Dict: @response_handler() def update_cluster(self, project_id: str, cluster_id: str, data: Dict) -> Dict: """更新集群信息 - :param project_id: 项目32为长度 ID :param cluster_id: 集群ID :param data: 更新的集群属性,包含status,名称、描述等 @@ -564,7 +565,6 @@ def update_cluster(self, project_id: str, cluster_id: str, data: Dict) -> Dict: @response_handler() def delete_cluster(self, project_id: str, cluster_id: str): """删除集群 - :param project_id: 项目32为长度 ID :param cluster_id: 集群ID """ @@ -574,7 +574,6 @@ def delete_cluster(self, project_id: str, cluster_id: str): @response_handler() def update_node_list(self, project_id: str, cluster_id: str, nodes: List[UpdateNodesData]) -> List: """更新节点信息 - :param project_id: 项目32为长度 ID :param cluster_id: 集群ID :param nodes: 更新的节点属性,包含IP和状态 @@ -594,7 +593,6 @@ def get_cluster_namespace_list( desire_all_data: Union[bool, int] = None, ) -> Dict[str, Union[int, List[Dict]]]: """获取集群下命名空间列表 - :param project_id: 项目ID :param cluster_id: 集群ID :param limit: 每页的数量 @@ -633,6 +631,37 @@ def get_node_list(self, project_id: str, cluster_id: str, params: Dict = None) - req_params.setdefault("desire_all_data", 1) return self._client.request_json("GET", url, params=req_params) + @response_handler() + def update_project(self, project_id: str, data: Dict) -> Dict: + """更新项目信息 + :param project_id: 项目ID + :param data: 要更新的项目信息 + :returns: 返回更新后的项目信息 + """ + url = self._config.update_project_url.format(project_id=project_id) + return self._client.request_json("PUT", url, json=data) + + @response_handler() + def create_cluster(self, project_id: str, data: Dict) -> Dict: + """创建集群信息 + :param project_id: 项目ID + :param data: 集群信息 + :returns: 返回创建的集群信息 + """ + url = self._config.add_cluster_url.format(project_id=project_id) + return self._client.request_json("POST", url, json=data) + + @response_handler() + def add_nodes(self, project_id: str, cluster_id: str, data: Dict) -> List: + """添加节点信息 + :param project_id: 项目ID + :param cluster_id: 集群ID + :param data: 添加的节点信息 + :returns: 返回节点数据 + """ + url = self._config.add_nodes_url.format(project_id=project_id, cluster_id=cluster_id) + return self._client.request_json("PATCH", url, json=data) + try: from .paas_cc_ext import get_auth_project # noqa diff --git a/bcs-app/backend/container_service/clusters/cluster_manager.py b/bcs-app/backend/container_service/clusters/cluster_manager.py new file mode 100644 index 000000000..4f33b708c --- /dev/null +++ b/bcs-app/backend/container_service/clusters/cluster_manager.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) available. +Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://opensource.org/licenses/MIT + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" +import logging +from typing import Dict, List + +from django.conf import settings + +from backend.components import base as comp_base +from backend.components import paas_cc +from backend.components.bcs_api import cluster +from backend.container_service.clusters.models import CommonStatus + +from . import constants + +logger = logging.getLogger(__name__) + + +def create_cluster(access_token: str, project_id: str, cc_app_id: int, data: Dict) -> Dict: + """创建集群信息""" + bcs_cc_client = paas_cc.PaaSCCClient(comp_base.ComponentAuth(access_token)) + cluster_env = settings.CLUSTER_ENV.get(data["environment"]) + data["environment"] = cluster_env + cluster_data = bcs_cc_client.create_cluster(project_id, data) + + # 写入数据到clustermanager + bcs_basic_config = cluster.BcsBasicConfig( + projectID=project_id, + businessID=str(cc_app_id), + clusterID=cluster_data["cluster_id"], + clusterName=data["name"], + provider="bcs", + environment=cluster_env, + engineType=data["coes"], + onlyCreateInfo=True, + ) + # NOTE: 切换流程后需要适配集群的配置 + cloud_cluster_config = cluster.CloudClusterConfig( + region=data.get("area_id"), + master=data.get("master_ips") or [], + vpcID=data.get("vpc_id"), + clusterBasicSettings={"version": data.get("version", "")}, + clusterAdvanceSettings={ + "containerRuntime": constants.ContainerRuntime.Docker, + "IPVS": True if data.get("kube_proxy_mode") == constants.KubeProxy.IPVS else False, + }, + ) + try: + cluster_config = cluster.ClusterConfig( + creator=data["creator"], bcs_basic_config=bcs_basic_config, cloud_cluster_config=cloud_cluster_config + ) + client = cluster.BcsClusterApiClient(comp_base.ComponentAuth(access_token)) + client.add_cluster(cluster_config) + except Exception as e: + logger.error("add clustermanager cluster failed, %s", e) + return cluster_data + + +def update_cluster(access_token: str, project_id: str, cluster_id: str, data: Dict) -> Dict: + """更新集群""" + bcs_cc_client = paas_cc.PaaSCCClient(comp_base.ComponentAuth(access_token)) + cluster_data = bcs_cc_client.update_cluster(project_id, cluster_id, data) + cluster_config = cluster.UpdatedClusterConfig( + projectID=project_id, + clusterID=cluster_id, + updater=data.get("updater"), + clusterName=data.get("name"), + status=constants.ClusterStatus.RUNNING, + ) + try: + client = cluster.BcsClusterApiClient(comp_base.ComponentAuth(access_token)) + client.update_cluster(cluster_config) + except Exception as e: + logger.error("update clustermanager cluster failed, %s", e) + + return cluster_data + + +def delete_cluster(access_token: str, project_id: str, cluster_id: str) -> Dict: + """删除集群""" + bcs_cc_client = paas_cc.PaaSCCClient(comp_base.ComponentAuth(access_token)) + cluster_data = bcs_cc_client.update_cluster(project_id, cluster_id, {"status": CommonStatus.Removing}) + try: + client = cluster.BcsClusterApiClient(comp_base.ComponentAuth(access_token)) + client.delete_cluster(cluster_id=cluster_id, only_delete_info=True) + except Exception as e: + logger.error("delete clustermanager cluster failed, %s", e) + return cluster_data + + +def add_nodes(access_token: str, project_id: str, cluster_id: str, data: Dict): + """添加节点""" + bcs_cc_client = paas_cc.PaaSCCClient(comp_base.ComponentAuth(access_token)) + bcs_cc_client.add_nodes(project_id, cluster_id, data) + try: + client = cluster.BcsClusterApiClient(comp_base.ComponentAuth(access_token)) + node_config = cluster.NodeConfig(nodes=[node["inner_ip"] for node in data["objects"]], onlyCreateInfo=True) + client.add_nodes(cluster_id, node_config) + except Exception as e: + logger.error("add clustermanager nodes failed, %s", e) + + +def delete_nodes(access_token: str, project_id: str, cluster_id: str, node_ips: List[str]): + """删除节点""" + bcs_cc_client = paas_cc.PaaSCCClient(comp_base.ComponentAuth(access_token)) + data = [paas_cc.UpdateNodesData(inner_ip=ip, status=CommonStatus.Removing) for ip in node_ips] + bcs_cc_client.update_node_list(project_id, cluster_id, data) + try: + client = cluster.BcsClusterApiClient(comp_base.ComponentAuth(access_token)) + client.delete_nodes(cluster_id, node_ips) + except Exception as e: + logger.error("update clustermanager nodes failed, %s", e) diff --git a/bcs-app/backend/container_service/clusters/constants.py b/bcs-app/backend/container_service/clusters/constants.py index d1914e790..958c97cfa 100644 --- a/bcs-app/backend/container_service/clusters/constants.py +++ b/bcs-app/backend/container_service/clusters/constants.py @@ -196,6 +196,24 @@ class KubeProxy(str, StructuredEnum): K8S_NODE_ROLE_MASTER = "node-role.kubernetes.io/master" +class ContainerRuntime(str, StructuredEnum): + """容器运行时""" + + Docker = "docker" + Containerd = "containerd" + + +class ClusterStatus(str, StructuredEnum): + """集群状态""" + + CREATING = EnumField("CREATING", label=_("创建中")) + RUNNING = EnumField("RUNNING", label=_("运行中,标识集群正常")) + DELETING = EnumField("DELETING", label=_("删除中")) + FAILURE = EnumField("FAILURE", label=_("失败")) + INITIALIZATION = EnumField("INITIALIZATION", label=_("初始化")) + DELETED = EnumField("DELETED", label=_("已删除")) + + # docker状态排序 # default will be 100 DockerStatusDefaultOrder = 100 diff --git a/bcs-app/backend/container_service/projects/auditor.py b/bcs-app/backend/container_service/projects/auditor.py new file mode 100644 index 000000000..5511a2512 --- /dev/null +++ b/bcs-app/backend/container_service/projects/auditor.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) available. +Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://opensource.org/licenses/MIT + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" +from backend.bcs_web.audit_log.audit.auditors import Auditor +from backend.bcs_web.audit_log.audit.context import AuditContext +from backend.bcs_web.audit_log.constants import ResourceType + + +class ProjectAuditor(Auditor): + def __init__(self, audit_ctx: AuditContext): + super().__init__(audit_ctx) + self.audit_ctx.resource_type = ResourceType.Project diff --git a/bcs-app/backend/container_service/projects/project_manager.py b/bcs-app/backend/container_service/projects/project_manager.py new file mode 100644 index 000000000..fd545b489 --- /dev/null +++ b/bcs-app/backend/container_service/projects/project_manager.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) available. +Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://opensource.org/licenses/MIT + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" +import logging +from typing import Dict + +from backend.components import base as comp_base +from backend.components import paas_cc +from backend.components.bcs_api import constants, project +from backend.utils.error_codes import error_codes + +from .base.constants import ProjectKindName + +logger = logging.getLogger(__name__) + + +def create_project(access_token: str, project_config: project.ProjectConfig) -> Dict: + """创建cluster manager中项目信息""" + try: + client = project.BcsProjectApiClient(comp_base.ComponentAuth(access_token)) + resp = client.create_project(project_config) + except Exception as e: + raise error_codes.APIError(f"create clustermanager project error, {e}") + return resp + + +def update_project(access_token: str, project_config: project.UpdatedProjectConfig) -> Dict: + """更新cluster manager中项目信息""" + try: + client = project.BcsProjectApiClient(comp_base.ComponentAuth(access_token)) + resp = client.update_project(project_config) + except Exception as e: + raise error_codes.APIError(f"update clustermanager project error, {e}") + return resp + + +def update_or_create_project(access_token: str, project_id: str, data: Dict) -> Dict: + """创建cluster manager所属的项目信息 + NOTE: 当调用接口出现异常后,现阶段不抛出异常;待下线bcs cc模块时,调整为必选步骤 + """ + # TODO: bcs cc服务下掉后,删除对应的接口 + bcs_cc_client = paas_cc.PaaSCCClient(comp_base.ComponentAuth(access_token)) + project_data = bcs_cc_client.update_project(project_id, data) + + updated_project_config = project.UpdatedProjectConfig( + projectID=project_id, + updater=data["updator"], + name=project_data["project_name"], + kind=ProjectKindName, + businessID=project_data["cc_app_id"], + ) + # 更新项目信息 + try: + resp = update_project(access_token, updated_project_config) + except Exception as e: + logger.error("update clustermanager project failed, %s", e) + return project_data + # 根据code判断项目是否存在,当项目不存在时,调用创建项目接口 + if resp.get("code") == constants.RECORD_NOT_EXIST_CODE: + # 组装写入cluster manager的数据 + reserved_config = project.ProjectReservedConfig( + bgID=project_data["bg_id"], + bgName=project_data["bg_name"], + deptName=project_data["dept_name"], + deptID=project_data["dept_id"], + centerID=project_data["center_id"], + centerName=project_data["center_name"], + isSecret=project_data["is_secrecy"], + isOffline=project_data["is_offlined"], + useBKRes=project_data["use_bk"], + projectType=project_data["project_type"], + ) + basic_config = project.ProjectBasicConfig( + projectID=project_id, + name=project_data["project_name"], + englishName=project_data["english_name"], + kind="k8s", + businessID=project_data["cc_app_id"], + description=project_data["description"], + ) + project_config = project.ProjectConfig( + creator=project_data["creator"], basic_config=basic_config, reserved_config=reserved_config + ) + # 当提示项目不存在时,需要添加项目 + try: + create_project(access_token, project_config) + except Exception as e: + logger.error("update or create clustermanager project failed, %s", e) + + return project_data diff --git a/bcs-app/backend/container_service/projects/views.py b/bcs-app/backend/container_service/projects/views.py index 079fbb9d7..2ab6a3028 100644 --- a/bcs-app/backend/container_service/projects/views.py +++ b/bcs-app/backend/container_service/projects/views.py @@ -14,6 +14,7 @@ """ import json import logging +from typing import Dict from django.conf import settings from django.utils.translation import ugettext_lazy as _ @@ -22,7 +23,8 @@ from rest_framework.response import Response from rest_framework.views import APIView -from backend.bcs_web.audit_log import client +from backend.bcs_web.audit_log.audit.decorators import log_audit_on_view +from backend.bcs_web.audit_log.constants import ActivityType from backend.bcs_web.constants import bcs_project_cache_key from backend.bcs_web.iam.permissions import ProjectPermission from backend.bcs_web.viewsets import SystemViewSet @@ -39,7 +41,8 @@ from backend.utils.error_codes import error_codes from backend.utils.renderers import BKAPIRenderer -from . import serializers +from . import project_manager, serializers +from .auditor import ProjectAuditor from .cmdb import list_biz_maintainers logger = logging.getLogger(__name__) @@ -131,6 +134,7 @@ def invalid_project_cache(self, project_id): # NOTE: 后续permission统一后,可以删除下面的缓存标识 region.delete(f"BK_DEVOPS_BCS:HAS_BCS_SERVICE:{project_id}") + @log_audit_on_view(ProjectAuditor, activity_type=ActivityType.Modify) def update(self, request, project_id): """更新项目信息""" if not self.can_edit(request, project_id): @@ -139,21 +143,8 @@ def update(self, request, project_id): access_token = request.user.token.access_token data["updator"] = request.user.username - # 添加操作日志 - ual_client = client.UserActivityLogClient( - project_id=project_id, - user=request.user.username, - resource_type="project", - resource=request.project.project_name, - resource_id=project_id, - description="{}: {}".format(_("更新项目"), request.project.project_name), - ) - resp = paas_cc.update_project_new(access_token, project_id, data) - if resp.get("code") != ErrorCode.NoError: - ual_client.log_modify(activity_status="failed") - raise error_codes.APIError(_("更新项目信息失败,错误详情: {}").format(resp.get("message"))) - ual_client.log_modify(activity_status="succeed") - project_data = resp.get("data") + # 统一更新或创建项目部分,方便bcs cc模块移除后的处理 + project_data = project_manager.update_or_create_project(access_token, project_id, data) if project_data: project_data["created_at"], project_data["updated_at"] = self.normalize_create_update_time( project_data["created_at"], project_data["updated_at"] diff --git a/bcs-app/backend/settings/ce/base.py b/bcs-app/backend/settings/ce/base.py index 047d6005d..754c87451 100644 --- a/bcs-app/backend/settings/ce/base.py +++ b/bcs-app/backend/settings/ce/base.py @@ -236,6 +236,8 @@ def configure_workers(*args, **kwargs): BCS_CLUSTER_ENV_AND_HTTPS_SERVER_HOST = {"prod": os.environ.get("BKAPP_BCS_API_DOMAIN")} # BCS API PRE URL BCS_API_PRE_URL = f"{APIGW_HOST}/api/apigw/bcs_api" +BCS_API_GW_DOMAIN = f"{APIGW_HOST}/api/apigw/bcs-api-gateway" +BCS_API_GW_ENV = "prod" BK_SSM_HOST = os.environ.get("BKAPP_SSM_HOST") diff --git a/bcs-app/backend/tests/components/bcs_api/__init__.py b/bcs-app/backend/tests/components/bcs_api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/bcs-app/backend/tests/components/bcs_api/test_cluster.py b/bcs-app/backend/tests/components/bcs_api/test_cluster.py new file mode 100644 index 000000000..b2277d5c5 --- /dev/null +++ b/bcs-app/backend/tests/components/bcs_api/test_cluster.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) available. +Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://opensource.org/licenses/MIT + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" +from requests_mock import ANY + +from backend.components.base import ComponentAuth +from backend.components.bcs_api import cluster + +SUCCESS_CODE = 0 + + +class TestBcsApiClient: + def test_add_cluster(self, project_id, cluster_id, request_user, random_name, requests_mock): + expected_data = {"cluster_id": cluster_id} + expected_task = {"taskID": random_name} + requests_mock.post(ANY, json={"code": SUCCESS_CODE, "data": expected_data, "task": expected_task}) + + client = cluster.BcsClusterApiClient(ComponentAuth(request_user.token.access_token)) + cloud_cluster_config = cluster.CloudClusterConfig( + region=random_name, manageType="INDEPENDENT_CLUSTER", master=["fake_ip"], vpcID=random_name, cloudID=1 + ) + bcs_basic_config = cluster.BcsBasicConfig( + projectID=project_id, + businessID=1, + clusterID=cluster_id, + clusterName=random_name, + provider="", + environment="test", + engineType="k8s", + isExclusive=False, + clusterType="k8s", + federationClusterID="", + labels={}, + onlyCreateInfo=True, + bcsAddons={}, + extraAddons={}, + ) + cluster_config = cluster.ClusterConfig( + creator=request_user.username, + cloud_cluster_config=cloud_cluster_config, + bcs_basic_config=bcs_basic_config, + ) + resp = client.add_cluster(cluster_config) + assert resp["task"] == expected_task + assert resp["data"] == expected_data + + def test_update_cluster(self, project_id, cluster_id, request_user, random_name, requests_mock): + expected_data = {"cluster_id": cluster_id} + requests_mock.put(ANY, json={"code": SUCCESS_CODE, "data": expected_data}) + + client = cluster.BcsClusterApiClient(ComponentAuth(request_user.token.access_token)) + cluster_config = cluster.UpdatedClusterConfig( + projectID=project_id, + clusterID=cluster_id, + updater=request_user.username, + clusterName="test", + status="RUNNING", + ) + resp = client.update_cluster(cluster_config) + assert resp == expected_data + + def test_delete_cluster(self, cluster_id, random_name, request_user, requests_mock): + expected_task = {"taskID": random_name} + requests_mock.delete(ANY, json={"code": SUCCESS_CODE, "data": {}, "task": expected_task}) + + client = cluster.BcsClusterApiClient(ComponentAuth(request_user.token.access_token)) + resp = client.delete_cluster(cluster_id) + assert resp["task"] == expected_task + + def test_query_task(self, random_name, request_user, requests_mock): + expected_data = {"taskID": random_name} + requests_mock.get(ANY, json={"code": SUCCESS_CODE, "data": expected_data}) + + client = cluster.BcsClusterApiClient(ComponentAuth(request_user.token.access_token)) + resp = client.query_task(random_name) + assert resp["data"] == expected_data diff --git a/bcs-app/backend/tests/components/bcs_api/test_project.py b/bcs-app/backend/tests/components/bcs_api/test_project.py new file mode 100644 index 000000000..306bbced6 --- /dev/null +++ b/bcs-app/backend/tests/components/bcs_api/test_project.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) available. +Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://opensource.org/licenses/MIT + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" +from requests_mock import ANY + +from backend.components.base import ComponentAuth +from backend.components.bcs_api import project + +SUCCESS_CODE = 0 + + +class TestBcsApiProjectClient: + def test_query_project(self, project_id, request_user, requests_mock): + expected_data = {"project_id": project_id} + requests_mock.get(ANY, json={"code": SUCCESS_CODE, "data": expected_data}) + + client = project.BcsProjectApiClient(ComponentAuth(request_user.token.access_token)) + resp = client.query_project(project_id) + assert resp == expected_data + + def test_create_project(self, project_id, request_user, random_name, requests_mock): + requests_mock.post(ANY, json={"code": SUCCESS_CODE, "message": "success"}) + + client = project.BcsProjectApiClient(ComponentAuth(request_user.token.access_token)) + basic_config = project.ProjectBasicConfig( + projectID=project_id, name=random_name, englishName=random_name, kind="k8s", businessID="1", description="" + ) + reserved_config = project.ProjectReservedConfig() + project_config = project.ProjectConfig( + creator=request_user.username, basic_config=basic_config, reserved_config=reserved_config + ) + resp = client.create_project(project_config) + assert resp["code"] == SUCCESS_CODE + + def test_update_project(self, project_id, request_user, random_name, requests_mock): + expected_data = {"project_id": project_id} + requests_mock.put(ANY, json={"code": SUCCESS_CODE, "data": expected_data}) + + client = project.BcsProjectApiClient(ComponentAuth(request_user.token.access_token)) + project_config = project.UpdatedProjectConfig( + projectID=project_id, + updater=request_user.username, + name=random_name, + kind="k8s", + businessID=1, + ) + resp = client.update_project(project_config) + assert resp["code"] == SUCCESS_CODE diff --git a/bcs-app/backend/tests/components/test_bcs_api.py b/bcs-app/backend/tests/components/test_bcs_api.py index 6eebf7d9c..47f5ee98d 100644 --- a/bcs-app/backend/tests/components/test_bcs_api.py +++ b/bcs-app/backend/tests/components/test_bcs_api.py @@ -5,9 +5,7 @@ Copyright (C) 2017-2021 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. @@ -15,6 +13,7 @@ import pytest from requests_mock import ANY +from backend.components import bcs_api from backend.components.base import ComponentAuth from backend.components.bcs_api import BcsApiClient diff --git a/bcs-app/backend/tests/container_service/clusters/test_cluster_manager.py b/bcs-app/backend/tests/container_service/clusters/test_cluster_manager.py new file mode 100644 index 000000000..3b1c50c83 --- /dev/null +++ b/bcs-app/backend/tests/container_service/clusters/test_cluster_manager.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) available. +Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://opensource.org/licenses/MIT + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" +from unittest.mock import patch + +import pytest + +from backend.container_service.clusters import cluster_manager +from backend.container_service.clusters.models import CommonStatus +from backend.tests.testing_utils.mocks import paas_cc +from backend.tests.testing_utils.mocks.bcs_api import cluster + +fake_biz_id = 1 +fake_coes = "k8s" +fake_cluster_name = "test-cluster" +fake_cluster_data = { + "creator": "admin", + "environment": "test", + "name": fake_cluster_name, + "coes": fake_coes, + "area_id": 1, + "version": "1.12", +} + + +@pytest.fixture(autouse=True) +def pre_patch(): + with patch( + "backend.container_service.clusters.cluster_manager.paas_cc.PaaSCCClient", new=paas_cc.StubPaaSCCClient + ), patch( + "backend.container_service.clusters.cluster_manager.cluster.BcsClusterApiClient", + new=cluster.StubBcsClusterApiClient, + ): + yield + + +def test_create_cluster(project_id, cluster_id, request_user): + cluster_data = cluster_manager.create_cluster( + request_user.token.access_token, project_id, fake_biz_id, fake_cluster_data + ) + assert cluster_data["type"] == fake_coes + + +def test_update_cluster(project_id, cluster_id, request_user): + cluster_data = cluster_manager.update_cluster( + request_user.token.access_token, project_id, cluster_id, {"name": fake_cluster_name} + ) + assert cluster_data["name"] == fake_cluster_name + + +def test_delete_cluster(project_id, cluster_id, request_user): + cluster_data = cluster_manager.delete_cluster(request_user.token.access_token, project_id, cluster_id) + assert cluster_data["status"] == CommonStatus.Removing diff --git a/bcs-app/backend/tests/container_service/projects/test_project_manager.py b/bcs-app/backend/tests/container_service/projects/test_project_manager.py new file mode 100644 index 000000000..f7343a314 --- /dev/null +++ b/bcs-app/backend/tests/container_service/projects/test_project_manager.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) available. +Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://opensource.org/licenses/MIT + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" +from unittest.mock import patch + +from backend.container_service.projects.project_manager import update_or_create_project +from backend.tests.testing_utils.mocks import paas_cc +from backend.tests.testing_utils.mocks.bcs_api import project + +fake_project_kind = 1 +fake_project_data = {"updator": "admin", "kind": fake_project_kind, "cc_app_id": 1} + + +@patch("backend.container_service.projects.project_manager.paas_cc.PaaSCCClient", new=paas_cc.StubPaaSCCClient) +@patch( + "backend.container_service.projects.project_manager.project.BcsProjectApiClient", + new=project.StubBcsProjectApiClient, +) +def test_update_project(request_user, project_id): + """更新项目信息 + 项目已经存在于clustermanager中 + """ + project_data = update_or_create_project(request_user.token.access_token, project_id, fake_project_data) + assert project_data["kind"] == fake_project_kind + + +@patch("backend.container_service.projects.project_manager.paas_cc.PaaSCCClient", new=paas_cc.StubPaaSCCClient) +@patch( + "backend.container_service.projects.project_manager.project.BcsProjectApiClient.create_project", + new=project.StubBcsProjectApiClient().create_project, +) +@patch( + "backend.container_service.projects.project_manager.project.BcsProjectApiClient.update_project", + new=project.StubBcsProjectApiClient().update_project_not_exist, +) +def test_create_project(request_user, project_id): + project_data = update_or_create_project(request_user.token.access_token, project_id, fake_project_data) + assert project_data["kind"] == fake_project_kind diff --git a/bcs-app/backend/tests/testing_utils/mocks/bcs_api.py b/bcs-app/backend/tests/testing_utils/mocks/bcs_api/__init__.py similarity index 97% rename from bcs-app/backend/tests/testing_utils/mocks/bcs_api.py rename to bcs-app/backend/tests/testing_utils/mocks/bcs_api/__init__.py index 4fe02257b..b2562d0a5 100644 --- a/bcs-app/backend/tests/testing_utils/mocks/bcs_api.py +++ b/bcs-app/backend/tests/testing_utils/mocks/bcs_api/__init__.py @@ -14,7 +14,7 @@ """ from typing import Dict -from .utils import mockable_function +from ..utils import mockable_function class StubBcsApiClient: diff --git a/bcs-app/backend/tests/testing_utils/mocks/bcs_api/cluster.py b/bcs-app/backend/tests/testing_utils/mocks/bcs_api/cluster.py new file mode 100644 index 000000000..03bc41828 --- /dev/null +++ b/bcs-app/backend/tests/testing_utils/mocks/bcs_api/cluster.py @@ -0,0 +1,856 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) available. +Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://opensource.org/licenses/MIT + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" +from typing import Dict, List + +from backend.components.bcs_api import cluster + + +class StubBcsClusterApiClient: + """使用假数据的 BCS cluster Api client 对象""" + + def __init__(self, *args, **kwargs): + pass + + def add_cluster(self, *args, **kwargs) -> Dict: + return {"code": 0, "data": self.make_add_cluster_resp()} + + def update_cluster(self, cluster_config: cluster.UpdatedClusterConfig) -> Dict: + return self.make_update_cluster_data() + + def delete_cluster( + self, cluster_id: str, is_force: bool = False, is_clean_resource=True, only_delete_info=False + ) -> Dict: + return {"code": 0, "data": self.make_delete_cluster_resp(cluster_id)} + + def add_nodes(self, cluster_id: str, node_config: cluster.NodeConfig) -> Dict: + return self.make_add_nodes_data() + + def delete_nodes(self, cluster_id: str, nodes: List[str], delete_mode: str, is_force: bool = False) -> Dict: + return self.make_delete_nodes_data() + + def query_task(self, task_id: str) -> Dict: + return self.make_query_task_data(task_id) + + @staticmethod + def make_add_cluster_resp() -> Dict: + return { + "code": 0, + "message": "string", + "result": True, + "data": { + "clusterID": "string", + "clusterName": "string", + "federationClusterID": "string", + "provider": "string", + "region": "string", + "vpcID": "string", + "projectID": "string", + "businessID": "string", + "environment": "string", + "engineType": "string", + "isExclusive": True, + "clusterType": "string", + "labels": {"additionalProp1": "string", "additionalProp2": "string", "additionalProp3": "string"}, + "creator": "string", + "createTime": "string", + "updateTime": "string", + "bcsAddons": { + "additionalProp1": { + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + }, + "additionalProp2": { + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + }, + "additionalProp3": { + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + }, + }, + "extraAddons": { + "additionalProp1": { + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + }, + "additionalProp2": { + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + }, + "additionalProp3": { + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + }, + }, + "systemID": "string", + "manageType": "string", + "master": { + "additionalProp1": { + "nodeID": "string", + "innerIP": "string", + "instanceType": "string", + "CPU": 0, + "mem": 0, + "GPU": 0, + "status": "string", + "zoneID": "string", + "nodeGroupID": "string", + "clusterID": "string", + }, + "additionalProp2": { + "nodeID": "string", + "innerIP": "string", + "instanceType": "string", + "CPU": 0, + "mem": 0, + "GPU": 0, + "status": "string", + "zoneID": "string", + "nodeGroupID": "string", + "clusterID": "string", + }, + "additionalProp3": { + "nodeID": "string", + "innerIP": "string", + "instanceType": "string", + "CPU": 0, + "mem": 0, + "GPU": 0, + "status": "string", + "zoneID": "string", + "nodeGroupID": "string", + "clusterID": "string", + }, + }, + "networkSettings": { + "clusterIPv4CIDR": "string", + "serviceIPv4CIDR": "string", + "maxNodePodNum": "string", + "maxServiceNum": "string", + }, + "clusterBasicSettings": { + "OS": "string", + "version": "string", + "clusterTags": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + }, + "clusterAdvanceSettings": { + "IPVS": True, + "containerRuntime": "string", + "runtimeVersion": "string", + "extraArgs": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + }, + "nodeSettings": {"dockerGraphPath": "string", "mountTarget": "string"}, + "status": "string", + "updator": "string", + }, + "task": { + "taskID": "string", + "taskType": "string", + "status": "string", + "message": "string", + "start": "string", + "end": "string", + "executionTime": 0, + "currentStep": "string", + "stepSequence": ["string"], + "steps": { + "additionalProp1": { + "name": "string", + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + "retry": 0, + "start": "string", + "end": "string", + "executionTime": 0, + "status": "string", + "message": "string", + "lastUpdate": "string", + }, + "additionalProp2": { + "name": "string", + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + "retry": 0, + "start": "string", + "end": "string", + "executionTime": 0, + "status": "string", + "message": "string", + "lastUpdate": "string", + }, + "additionalProp3": { + "name": "string", + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + "retry": 0, + "start": "string", + "end": "string", + "executionTime": 0, + "status": "string", + "message": "string", + "lastUpdate": "string", + }, + }, + "clusterID": "string", + "projectID": "string", + "creator": "string", + "lastUpdate": "string", + "updator": "string", + "forceTerminate": True, + }, + } + + @staticmethod + def make_update_cluster_data() -> Dict: + { + "clusterID": "string", + "clusterName": "string", + "federationClusterID": "string", + "provider": "string", + "region": "string", + "vpcID": "string", + "projectID": "string", + "businessID": "string", + "environment": "string", + "engineType": "string", + "isExclusive": True, + "clusterType": "string", + "labels": {"additionalProp1": "string", "additionalProp2": "string", "additionalProp3": "string"}, + "creator": "string", + "createTime": "string", + "updateTime": "string", + "bcsAddons": { + "additionalProp1": { + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + }, + "additionalProp2": { + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + }, + "additionalProp3": { + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + }, + }, + "extraAddons": { + "additionalProp1": { + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + }, + "additionalProp2": { + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + }, + "additionalProp3": { + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + }, + }, + "systemID": "string", + "manageType": "string", + "master": { + "additionalProp1": { + "nodeID": "string", + "innerIP": "string", + "instanceType": "string", + "CPU": 0, + "mem": 0, + "GPU": 0, + "status": "string", + "zoneID": "string", + "nodeGroupID": "string", + "clusterID": "string", + }, + "additionalProp2": { + "nodeID": "string", + "innerIP": "string", + "instanceType": "string", + "CPU": 0, + "mem": 0, + "GPU": 0, + "status": "string", + "zoneID": "string", + "nodeGroupID": "string", + "clusterID": "string", + }, + "additionalProp3": { + "nodeID": "string", + "innerIP": "string", + "instanceType": "string", + "CPU": 0, + "mem": 0, + "GPU": 0, + "status": "string", + "zoneID": "string", + "nodeGroupID": "string", + "clusterID": "string", + }, + }, + "networkSettings": { + "clusterIPv4CIDR": "string", + "serviceIPv4CIDR": "string", + "maxNodePodNum": "string", + "maxServiceNum": "string", + }, + "clusterBasicSettings": { + "OS": "string", + "version": "string", + "clusterTags": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + }, + "clusterAdvanceSettings": { + "IPVS": True, + "containerRuntime": "string", + "runtimeVersion": "string", + "extraArgs": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + }, + "nodeSettings": {"dockerGraphPath": "string", "mountTarget": "string"}, + "status": "string", + "updator": "string", + } + + @staticmethod + def make_delete_cluster_resp(cluster_id: str) -> Dict: + return { + "code": 0, + "message": "string", + "result": True, + "data": { + "clusterID": cluster_id, + "clusterName": "string", + "federationClusterID": "string", + "provider": "string", + "region": "string", + "vpcID": "string", + "projectID": "string", + "businessID": "string", + "environment": "string", + "engineType": "string", + "isExclusive": True, + "clusterType": "string", + "labels": {"additionalProp1": "string", "additionalProp2": "string", "additionalProp3": "string"}, + "creator": "string", + "createTime": "string", + "updateTime": "string", + "bcsAddons": { + "additionalProp1": { + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + }, + "additionalProp2": { + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + }, + "additionalProp3": { + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + }, + }, + "extraAddons": { + "additionalProp1": { + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + }, + "additionalProp2": { + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + }, + "additionalProp3": { + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + }, + }, + "systemID": "string", + "manageType": "string", + "master": { + "additionalProp1": { + "nodeID": "string", + "innerIP": "string", + "instanceType": "string", + "CPU": 0, + "mem": 0, + "GPU": 0, + "status": "string", + "zoneID": "string", + "nodeGroupID": "string", + "clusterID": "string", + }, + "additionalProp2": { + "nodeID": "string", + "innerIP": "string", + "instanceType": "string", + "CPU": 0, + "mem": 0, + "GPU": 0, + "status": "string", + "zoneID": "string", + "nodeGroupID": "string", + "clusterID": "string", + }, + "additionalProp3": { + "nodeID": "string", + "innerIP": "string", + "instanceType": "string", + "CPU": 0, + "mem": 0, + "GPU": 0, + "status": "string", + "zoneID": "string", + "nodeGroupID": "string", + "clusterID": "string", + }, + }, + "networkSettings": { + "clusterIPv4CIDR": "string", + "serviceIPv4CIDR": "string", + "maxNodePodNum": "string", + "maxServiceNum": "string", + }, + "clusterBasicSettings": { + "OS": "string", + "version": "string", + "clusterTags": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + }, + "clusterAdvanceSettings": { + "IPVS": True, + "containerRuntime": "string", + "runtimeVersion": "string", + "extraArgs": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + }, + "nodeSettings": {"dockerGraphPath": "string", "mountTarget": "string"}, + "status": "string", + "updator": "string", + }, + "tasks": [ + { + "taskID": "string", + "taskType": "string", + "status": "string", + "message": "string", + "start": "string", + "end": "string", + "executionTime": 0, + "currentStep": "string", + "stepSequence": ["string"], + "steps": { + "additionalProp1": { + "name": "string", + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + "retry": 0, + "start": "string", + "end": "string", + "executionTime": 0, + "status": "string", + "message": "string", + "lastUpdate": "string", + }, + "additionalProp2": { + "name": "string", + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + "retry": 0, + "start": "string", + "end": "string", + "executionTime": 0, + "status": "string", + "message": "string", + "lastUpdate": "string", + }, + "additionalProp3": { + "name": "string", + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + "retry": 0, + "start": "string", + "end": "string", + "executionTime": 0, + "status": "string", + "message": "string", + "lastUpdate": "string", + }, + }, + "clusterID": "string", + "projectID": "string", + "creator": "string", + "lastUpdate": "string", + "updator": "string", + "forceTerminate": True, + } + ], + } + + @staticmethod + def make_add_nodes_data() -> Dict: + return { + "taskID": "string", + "taskType": "string", + "status": "string", + "message": "string", + "start": "string", + "end": "string", + "executionTime": 0, + "currentStep": "string", + "stepSequence": ["string"], + "steps": { + "additionalProp1": { + "name": "string", + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + "retry": 0, + "start": "string", + "end": "string", + "executionTime": 0, + "status": "string", + "message": "string", + "lastUpdate": "string", + }, + "additionalProp2": { + "name": "string", + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + "retry": 0, + "start": "string", + "end": "string", + "executionTime": 0, + "status": "string", + "message": "string", + "lastUpdate": "string", + }, + "additionalProp3": { + "name": "string", + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + "retry": 0, + "start": "string", + "end": "string", + "executionTime": 0, + "status": "string", + "message": "string", + "lastUpdate": "string", + }, + }, + "clusterID": "string", + "projectID": "string", + "creator": "string", + "lastUpdate": "string", + "updator": "string", + "forceTerminate": True, + } + + @staticmethod + def make_delete_nodes_data() -> Dict: + return { + "taskID": "string", + "taskType": "string", + "status": "string", + "message": "string", + "start": "string", + "end": "string", + "executionTime": 0, + "currentStep": "string", + "stepSequence": ["string"], + "steps": { + "additionalProp1": { + "name": "string", + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + "retry": 0, + "start": "string", + "end": "string", + "executionTime": 0, + "status": "string", + "message": "string", + "lastUpdate": "string", + }, + "additionalProp2": { + "name": "string", + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + "retry": 0, + "start": "string", + "end": "string", + "executionTime": 0, + "status": "string", + "message": "string", + "lastUpdate": "string", + }, + "additionalProp3": { + "name": "string", + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + "retry": 0, + "start": "string", + "end": "string", + "executionTime": 0, + "status": "string", + "message": "string", + "lastUpdate": "string", + }, + }, + "clusterID": "string", + "projectID": "string", + "creator": "string", + "lastUpdate": "string", + "updator": "string", + "forceTerminate": True, + } + + @staticmethod + def make_query_task_data(task_id: str) -> Dict: + return { + "taskID": task_id, + "taskType": "string", + "status": "string", + "message": "string", + "start": "string", + "end": "string", + "executionTime": 0, + "currentStep": "string", + "stepSequence": ["string"], + "steps": { + "additionalProp1": { + "name": "string", + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + "retry": 0, + "start": "string", + "end": "string", + "executionTime": 0, + "status": "string", + "message": "string", + "lastUpdate": "string", + }, + "additionalProp2": { + "name": "string", + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + "retry": 0, + "start": "string", + "end": "string", + "executionTime": 0, + "status": "string", + "message": "string", + "lastUpdate": "string", + }, + "additionalProp3": { + "name": "string", + "system": "string", + "link": "string", + "params": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string", + }, + "retry": 0, + "start": "string", + "end": "string", + "executionTime": 0, + "status": "string", + "message": "string", + "lastUpdate": "string", + }, + }, + "clusterID": "string", + "projectID": "string", + "creator": "string", + "lastUpdate": "string", + "updator": "string", + "forceTerminate": True, + } diff --git a/bcs-app/backend/tests/testing_utils/mocks/bcs_api/project.py b/bcs-app/backend/tests/testing_utils/mocks/bcs_api/project.py new file mode 100644 index 000000000..6cd8a9ae6 --- /dev/null +++ b/bcs-app/backend/tests/testing_utils/mocks/bcs_api/project.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) available. +Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://opensource.org/licenses/MIT + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" +from typing import Dict + +from backend.components.bcs_api.constants import RECORD_NOT_EXIST_CODE + + +class StubBcsProjectApiClient: + """使用假数据的 BCS project Api client 对象""" + + def __init__(self, *args, **kwargs): + pass + + def create_project(self, *args, **kwargs) -> Dict: + return {"code": 0, "data": self.make_project_data()} + + def query_project(self, project_id) -> Dict: + return self.make_project_data() + + def update_project(self, *args, **kwargs) -> Dict: + return {"code": 0, "data": self.make_project_data()} + + def update_project_not_exist(self, *args, **kwargs) -> Dict: + return {"code": RECORD_NOT_EXIST_CODE} + + @staticmethod + def make_project_data() -> Dict: + return { + "projectID": "string", + "name": "string", + "englishName": "string", + "creator": "string", + "updater": "string", + "projectType": 0, + "useBKRes": True, + "description": "string", + "isOffline": False, + "kind": "string", + "businessID": "string", + "deployType": 0, + "bgID": "string", + "bgName": "string", + "deptID": "string", + "deptName": "string", + "centerID": "string", + "centerName": "string", + "isSecret": True, + "credentials": { + "additionalProp1": {"key": "string", "secret": "string"}, + "additionalProp2": {"key": "string", "secret": "string"}, + "additionalProp3": {"key": "string", "secret": "string"}, + }, + "createTime": "string", + "updateTime": "string", + } diff --git a/bcs-app/backend/tests/testing_utils/mocks/paas_cc.py b/bcs-app/backend/tests/testing_utils/mocks/paas_cc.py index 3b908af02..07802e1fd 100644 --- a/bcs-app/backend/tests/testing_utils/mocks/paas_cc.py +++ b/bcs-app/backend/tests/testing_utils/mocks/paas_cc.py @@ -13,7 +13,7 @@ specific language governing permissions and limitations under the License. """ import uuid -from typing import Dict +from typing import Dict, List from .utils import mockable_function @@ -36,6 +36,26 @@ def get_project(self, project_id: str) -> Dict: def get_cluster_namespace_list(self, project_id: str, cluster_id: str) -> Dict: return self.make_cluster_namespace_data(project_id, cluster_id) + @mockable_function + def update_project(self, project_id: str, *args, **kwargs) -> Dict: + return self.make_project_data(project_id) + + @mockable_function + def create_cluster(self, project_id: str, cluster_id: str, *args, **kwargs) -> Dict: + return self.make_cluster_data(project_id, cluster_id) + + @mockable_function + def update_cluster(self, project_id: str, cluster_id: str, *args, **kwargs) -> Dict: + return self.make_cluster_data(project_id, cluster_id, *args, **kwargs) + + @mockable_function + def add_nodes(self, project_id: str, cluster_id: str) -> List: + return self.make_nodes_data(project_id, cluster_id) + + @mockable_function + def update_node_list(self, project_id: str, cluster_id: str) -> List: + return self.make_nodes_data(project_id, cluster_id) + @staticmethod def wrap_resp(data): return { @@ -47,7 +67,8 @@ def wrap_resp(data): } @staticmethod - def make_cluster_data(project_id: str, cluster_id: str): + def make_cluster_data(project_id: str, cluster_id: str, *args, **kwargs): + status = args[0]["status"] if args and "status" in args[0] else "normal" _stub_time = '2021-01-01T00:00:00+08:00' return { 'area_id': 1, @@ -72,7 +93,7 @@ def make_cluster_data(project_id: str, cluster_id: str): 'remain_cpu': 10, 'remain_disk': 0, 'remain_mem': 10, - 'status': 'normal', + 'status': status, 'total_cpu': 12, 'total_disk': 0, 'total_mem': 64, @@ -82,7 +103,6 @@ def make_cluster_data(project_id: str, cluster_id: str): @staticmethod def make_project_data(project_id: str): - _stub_time = '2021-01-01T00:00:00+08:00' return { "approval_status": 2, "approval_time": "2020-01-01T00:00:00+08:00", @@ -136,3 +156,7 @@ def make_cluster_namespace_data(project_id: str, cluster_id: str) -> Dict: } ], } + + @staticmethod + def make_nodes_data(project_id: str, cluster_id: str) -> List: + return [{"id": 1, "inner_ip": "127.0.0.1", "project_id": project_id, "cluster_id": cluster_id}]