未验证 提交 15110056 编写于 作者: Q qianli2022 提交者: GitHub

[Feature][api] Task can run on kubernetes - k8s client and namespace #8247 (#8252)

* add db operator for namespace

* add k8s client

* add k8s controller

* add namespace ui

* add licenses

* add kubernetes model known-dependencies

* add new licenses

* fix SonarCloud Code Analysis bug

* remove license for standard protocol

* fix variable MemoryStr to memoryStr
Co-authored-by: Nqianl4 <qianl4@cicso.com>
上级 68ad22b0
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
package org.apache.dolphinscheduler.api.controller;
import static org.apache.dolphinscheduler.api.enums.Status.CREATE_K8S_NAMESPACE_ERROR;
import static org.apache.dolphinscheduler.api.enums.Status.DELETE_K8S_NAMESPACE_BY_ID_ERROR;
import static org.apache.dolphinscheduler.api.enums.Status.QUERY_K8S_NAMESPACE_LIST_PAGING_ERROR;
import static org.apache.dolphinscheduler.api.enums.Status.UPDATE_K8S_NAMESPACE_ERROR;
import static org.apache.dolphinscheduler.api.enums.Status.VERIFY_K8S_NAMESPACE_ERROR;
import org.apache.dolphinscheduler.api.aspect.AccessLogAnnotation;
import org.apache.dolphinscheduler.api.exceptions.ApiException;
import org.apache.dolphinscheduler.api.service.K8sNameSpaceService;
import org.apache.dolphinscheduler.api.utils.Result;
import org.apache.dolphinscheduler.common.Constants;
import org.apache.dolphinscheduler.common.utils.ParameterUtils;
import org.apache.dolphinscheduler.dao.entity.User;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import springfox.documentation.annotations.ApiIgnore;
/**
* k8s namespace controller
*/
@Api(tags = "K8S_NAMESPACE_TAG")
@RestController
@RequestMapping("/k8s-namespace")
public class K8sNamespaceController extends BaseController {
@Autowired
private K8sNameSpaceService k8sNameSpaceService;
/**
* query namespace list paging
*
* @param loginUser login user
* @param searchVal search value
* @param pageSize page size
* @param pageNo page number
* @return namespace list which the login user have permission to see
*/
@ApiOperation(value = "queryNamespaceListPaging", notes = "QUERY_NAMESPACE_LIST_PAGING_NOTES")
@ApiImplicitParams({
@ApiImplicitParam(name = "searchVal", value = "SEARCH_VAL", dataType = "String"),
@ApiImplicitParam(name = "pageSize", value = "PAGE_SIZE", required = true, dataType = "Int", example = "10"),
@ApiImplicitParam(name = "pageNo", value = "PAGE_NO", required = true, dataType = "Int", example = "1")
})
@GetMapping()
@ResponseStatus(HttpStatus.OK)
@ApiException(QUERY_K8S_NAMESPACE_LIST_PAGING_ERROR)
@AccessLogAnnotation(ignoreRequestArgs = "loginUser")
public Result queryProjectListPaging(@ApiIgnore @RequestAttribute(value = Constants.SESSION_USER) User loginUser,
@RequestParam(value = "searchVal", required = false) String searchVal,
@RequestParam("pageSize") Integer pageSize,
@RequestParam("pageNo") Integer pageNo
) {
Result result = checkPageParams(pageNo, pageSize);
if (!result.checkResult()) {
return result;
}
searchVal = ParameterUtils.handleEscapes(searchVal);
result = k8sNameSpaceService.queryListPaging(loginUser, searchVal, pageNo, pageSize);
return result;
}
/**
* create namespace,if not exist on k8s,will create,if exist only register in db
*
* @param loginUser
* @param namespace k8s namespace
* @param owner owner
* @param tag Which type of job is available, can be empty, all available
* @param k8s k8s name
* @param limitsCpu max cpu
* @param limitsMemory max memory
* @return
*/
@ApiOperation(value = "createK8sNamespace", notes = "CREATE_NAMESPACE_NOTES")
@ApiImplicitParams({
@ApiImplicitParam(name = "namespace", value = "NAMESPACE", required = true, dataType = "String"),
@ApiImplicitParam(name = "owner", value = "OWNER", required = false, dataType = "String"),
@ApiImplicitParam(name = "tag", value = "TAG", required = false, dataType = "String"),
@ApiImplicitParam(name = "k8s", value = "K8S", required = true, dataType = "String"),
@ApiImplicitParam(name = "limits_cpu", value = "LIMITS_CPU", required = false, dataType = "Double"),
@ApiImplicitParam(name = "limits_memory", value = "LIMITS_MEMORY", required = false, dataType = "Integer")
})
@PostMapping()
@ResponseStatus(HttpStatus.CREATED)
@ApiException(CREATE_K8S_NAMESPACE_ERROR)
@AccessLogAnnotation(ignoreRequestArgs = "loginUser")
public Result createNamespace(@ApiIgnore @RequestAttribute(value = Constants.SESSION_USER) User loginUser,
@RequestParam(value = "namespace") String namespace,
@RequestParam(value = "k8s") String k8s,
@RequestParam(value = "owner", required = false) String owner,
@RequestParam(value = "tag", required = false) String tag,
@RequestParam(value = "limitsCpu", required = false) Double limitsCpu,
@RequestParam(value = "limitsMemory", required = false) Integer limitsMemory
) {
Map<String, Object> result = k8sNameSpaceService.createK8sNamespace(loginUser, namespace, k8s, owner, tag, limitsCpu, limitsMemory);
return returnDataList(result);
}
/**
* update namespace,namespace and k8s not allowed update, because may create on k8s,can delete and create new instead
*
* @param loginUser
* @param owner owner
* @param tag Which type of job is available,such as flink,means only flink job can use, can be empty, all available
* @param limitsCpu max cpu
* @param limitsMemory max memory
* @return
*/
@ApiOperation(value = "updateK8sNamespace", notes = "UPDATE_NAMESPACE_NOTES")
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "K8S_NAMESPACE_ID", required = true, dataType = "Int", example = "100"),
@ApiImplicitParam(name = "owner", value = "OWNER", required = false, dataType = "String"),
@ApiImplicitParam(name = "tag", value = "TAG", required = false, dataType = "String"),
@ApiImplicitParam(name = "limitsCpu", value = "LIMITS_CPU", required = false, dataType = "Double"),
@ApiImplicitParam(name = "limitsMemory", value = "LIMITS_MEMORY", required = false, dataType = "Integer")})
@PutMapping(value = "/{id}")
@ResponseStatus(HttpStatus.CREATED)
@ApiException(UPDATE_K8S_NAMESPACE_ERROR)
@AccessLogAnnotation(ignoreRequestArgs = "loginUser")
public Result updateNamespace(@ApiIgnore @RequestAttribute(value = Constants.SESSION_USER) User loginUser,
@PathVariable(value = "id") int id,
@RequestParam(value = "owner", required = false) String owner,
@RequestParam(value = "tag", required = false) String tag,
@RequestParam(value = "limitsCpu", required = false) Double limitsCpu,
@RequestParam(value = "limitsMemory", required = false) Integer limitsMemory) {
Map<String, Object> result = k8sNameSpaceService.updateK8sNamespace(loginUser, id, owner, tag, limitsCpu, limitsMemory);
return returnDataList(result);
}
/**
* verify namespace and k8s,one k8s namespace is unique
*
* @param loginUser login user
* @param namespace namespace
* @param k8s k8s
* @return true if the k8s and namespace not exists, otherwise return false
*/
@ApiOperation(value = "verifyNamespaceK8s", notes = "VERIFY_NAMESPACE_K8S_NOTES")
@ApiImplicitParams({
@ApiImplicitParam(name = "namespace", value = "NAMESPACE", required = true, dataType = "String"),
@ApiImplicitParam(name = "k8s", value = "K8S", required = true, dataType = "String")
})
@PostMapping(value = "/verify")
@ResponseStatus(HttpStatus.OK)
@ApiException(VERIFY_K8S_NAMESPACE_ERROR)
@AccessLogAnnotation(ignoreRequestArgs = "loginUser")
public Result verifyNamespace(@ApiIgnore @RequestAttribute(value = Constants.SESSION_USER) User loginUser,
@RequestParam(value = "namespace") String namespace,
@RequestParam(value = "k8s") String k8s
) {
return k8sNameSpaceService.verifyNamespaceK8s(namespace, k8s);
}
/**
* delete namespace by id
*
* @param loginUser login user
* @param id namespace id
* @return delete result code
*/
@ApiOperation(value = "delNamespaceById", notes = "DELETE_NAMESPACE_BY_ID_NOTES")
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "NAMESPACE_ID", required = true, dataType = "Int", example = "100")
})
@PostMapping(value = "/delete")
@ResponseStatus(HttpStatus.OK)
@ApiException(DELETE_K8S_NAMESPACE_BY_ID_ERROR)
@AccessLogAnnotation
public Result delNamespaceById(@ApiIgnore @RequestAttribute(value = Constants.SESSION_USER) User loginUser,
@RequestParam(value = "id") int id) {
Map<String, Object> result = k8sNameSpaceService.deleteNamespaceById(loginUser, id);
return returnDataList(result);
}
}
......@@ -380,6 +380,15 @@ public enum Status {
TASK_GROUP_STATUS_OPENED(130019,"The task group has been opened.","任务组已经被开启"),
NOT_ALLOW_TO_DISABLE_OWN_ACCOUNT(130020, "Not allow to disable your own account", "不能停用自己的账号"),
NOT_ALLOW_TO_DELETE_DEFAULT_ALARM_GROUP(130030, "Not allow to delete the default alarm group ", "不能删除默认告警组"),
QUERY_K8S_NAMESPACE_LIST_PAGING_ERROR(1300001, "login user query k8s namespace list paging error", "分页查询k8s名称空间列表错误"),
K8S_NAMESPACE_EXIST(1300002, "k8s namespace {0} already exists", "k8s命名空间[{0}]已存在"),
CREATE_K8S_NAMESPACE_ERROR(1300003, "create k8s namespace error", "创建k8s命名空间错误"),
UPDATE_K8S_NAMESPACE_ERROR(1300004, "update k8s namespace error", "更新k8s命名空间信息错误"),
K8S_NAMESPACE_NOT_EXIST(1300005, "k8s namespace {0} not exists", "命名空间ID[{0}]不存在"),
K8S_CLIENT_OPS_ERROR(1300006, "k8s error with exception {0}", "k8s操作报错[{0}]"),
VERIFY_K8S_NAMESPACE_ERROR(1300007, "verify k8s and namespace error", "验证k8s命名空间信息错误"),
DELETE_K8S_NAMESPACE_BY_ID_ERROR(1300008, "delete k8s namespace by id error", "删除命名空间错误"),
;
private final int code;
......
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
package org.apache.dolphinscheduler.api.service;
import org.apache.dolphinscheduler.api.utils.Result;
import org.apache.dolphinscheduler.dao.entity.User;
import java.util.Map;
/**
* k8s namespace service impl
*/
public interface K8sNameSpaceService {
/**
* query namespace list paging
*
* @param loginUser login user
* @param pageNo page number
* @param searchVal search value
* @param pageSize page size
* @return k8s namespace list
*/
Result queryListPaging(User loginUser, String searchVal, Integer pageNo, Integer pageSize);
/**
* create namespace,if not exist on k8s,will create,if exist only register in db
*
* @param loginUser login user
* @param namespace namespace
* @param k8s k8s not null
* @param owner owner can null
* @param tag can null,if set means just used for one type job,such as flink,spark
* @param limitsCpu limits cpu, can null means not limit
* @param limitsMemory limits memory, can null means not limit
* @return
*/
Map<String, Object> createK8sNamespace(User loginUser, String namespace, String k8s, String owner, String tag, Double limitsCpu, Integer limitsMemory);
/**
* update K8s Namespace tag and resource limit
*
* @param loginUser login user
* @param owner owner
* @param tag Which type of job is available,such as flink,means only flink job can use, can be empty, all available
* @param limitsCpu max cpu
* @param limitsMemory max memory
* @return
*/
Map<String, Object> updateK8sNamespace(User loginUser, int id, String owner, String tag, Double limitsCpu, Integer limitsMemory);
/**
* verify namespace and k8s
*
* @param namespace namespace
* @param k8s k8s
* @return true if the k8s and namespace not exists, otherwise return false
*/
Result<Object> verifyNamespaceK8s(String namespace, String k8s);
/**
* delete namespace by id
*
* @param loginUser login user
* @param id namespace id
* @return
*/
Map<String, Object> deleteNamespaceById(User loginUser, int id);
}
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
package org.apache.dolphinscheduler.api.service.impl;
import org.apache.dolphinscheduler.api.enums.Status;
import org.apache.dolphinscheduler.api.service.K8sNameSpaceService;
import org.apache.dolphinscheduler.api.utils.PageInfo;
import org.apache.dolphinscheduler.api.utils.Result;
import org.apache.dolphinscheduler.common.Constants;
import org.apache.dolphinscheduler.dao.entity.K8sNamespace;
import org.apache.dolphinscheduler.dao.entity.User;
import org.apache.dolphinscheduler.dao.mapper.K8sNamespaceMapper;
import org.apache.dolphinscheduler.service.k8s.K8sClientService;
import org.apache.commons.lang.StringUtils;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
/**
* k8s namespace service impl
*/
@Service
public class K8sNameSpaceServiceImpl extends BaseServiceImpl implements K8sNameSpaceService {
private static final Logger logger = LoggerFactory.getLogger(K8sNameSpaceServiceImpl.class);
private static String resourceYaml = "apiVersion: v1\n"
+ "kind: ResourceQuota\n"
+ "metadata:\n"
+ " name: ${name}\n"
+ " namespace: ${namespace}\n"
+ "spec:\n"
+ " hard:\n"
+ " ${limitCpu}\n"
+ " ${limitMemory}\n";
@Autowired
private K8sNamespaceMapper k8sNamespaceMapper;
@Autowired
private K8sClientService k8sClientService;
/**
* query namespace list paging
*
* @param loginUser login user
* @param pageNo page number
* @param searchVal search value
* @param pageSize page size
* @return k8s namespace list
*/
@Override
public Result queryListPaging(User loginUser, String searchVal, Integer pageNo, Integer pageSize) {
Result result = new Result();
if (!isAdmin(loginUser)) {
putMsg(result, Status.USER_NO_OPERATION_PERM);
return result;
}
Page<K8sNamespace> page = new Page<>(pageNo, pageSize);
IPage<K8sNamespace> k8sNamespaceList = k8sNamespaceMapper.queryK8sNamespacePaging(page, searchVal);
Integer count = (int) k8sNamespaceList.getTotal();
PageInfo<K8sNamespace> pageInfo = new PageInfo<>(pageNo, pageSize);
pageInfo.setTotal(count);
pageInfo.setTotalList(k8sNamespaceList.getRecords());
result.setData(pageInfo);
putMsg(result, Status.SUCCESS);
return result;
}
/**
* create namespace,if not exist on k8s,will create,if exist only register in db
*
* @param loginUser login user
* @param namespace namespace
* @param k8s k8s not null
* @param owner owner can null
* @param tag can null,if set means just used for one type job,such as flink,spark
* @param limitsCpu limits cpu, can null means not limit
* @param limitsMemory limits memory, can null means not limit
* @return
*/
@Override
public Map<String, Object> createK8sNamespace(User loginUser, String namespace, String k8s, String owner, String tag, Double limitsCpu, Integer limitsMemory) {
Map<String, Object> result = new HashMap<>();
if (isNotAdmin(loginUser, result)) {
return result;
}
if (StringUtils.isEmpty(namespace)) {
putMsg(result, Status.REQUEST_PARAMS_NOT_VALID_ERROR, Constants.NAMESPACE);
return result;
}
if (StringUtils.isEmpty(k8s)) {
putMsg(result, Status.REQUEST_PARAMS_NOT_VALID_ERROR, Constants.K8S);
return result;
}
if (limitsCpu != null && limitsCpu < 0.0) {
putMsg(result, Status.REQUEST_PARAMS_NOT_VALID_ERROR, Constants.LIMITS_CPU);
return result;
}
if (limitsMemory != null && limitsMemory < 0) {
putMsg(result, Status.REQUEST_PARAMS_NOT_VALID_ERROR, Constants.LIMITS_MEMORY);
return result;
}
if (checkNamespaceExistInDb(namespace, k8s)) {
putMsg(result, Status.K8S_NAMESPACE_EXIST, namespace, k8s);
return result;
}
K8sNamespace k8sNamespaceObj = new K8sNamespace();
Date now = new Date();
k8sNamespaceObj.setNamespace(namespace);
k8sNamespaceObj.setK8s(k8s);
k8sNamespaceObj.setOwner(owner);
k8sNamespaceObj.setTag(tag);
k8sNamespaceObj.setLimitsCpu(limitsCpu);
k8sNamespaceObj.setLimitsMemory(limitsMemory);
k8sNamespaceObj.setOnlineJobNum(0);
k8sNamespaceObj.setPodReplicas(0);
k8sNamespaceObj.setPodRequestCpu(0.0);
k8sNamespaceObj.setPodRequestMemory(0);
k8sNamespaceObj.setCreateTime(now);
k8sNamespaceObj.setUpdateTime(now);
try {
String yamlStr = genDefaultResourceYaml(k8sNamespaceObj);
k8sClientService.upsertNamespaceAndResourceToK8s(k8sNamespaceObj, yamlStr);
} catch (Exception e) {
logger.error("namespace create to k8s error", e);
putMsg(result, Status.K8S_CLIENT_OPS_ERROR, e.getMessage());
return result;
}
k8sNamespaceMapper.insert(k8sNamespaceObj);
putMsg(result, Status.SUCCESS);
return result;
}
/**
* update K8s Namespace tag and resource limit
*
* @param loginUser login user
* @param owner owner
* @param tag Which type of job is available,such as flink,means only flink job can use, can be empty, all available
* @param limitsCpu max cpu
* @param limitsMemory max memory
* @return
*/
@Override
public Map<String, Object> updateK8sNamespace(User loginUser, int id, String owner, String tag, Double limitsCpu, Integer limitsMemory) {
Map<String, Object> result = new HashMap<>();
if (isNotAdmin(loginUser, result)) {
return result;
}
if (limitsCpu != null && limitsCpu < 0.0) {
putMsg(result, Status.REQUEST_PARAMS_NOT_VALID_ERROR, Constants.LIMITS_CPU);
return result;
}
if (limitsMemory != null && limitsMemory < 0) {
putMsg(result, Status.REQUEST_PARAMS_NOT_VALID_ERROR, Constants.LIMITS_MEMORY);
return result;
}
K8sNamespace k8sNamespaceObj = k8sNamespaceMapper.selectById(id);
if (k8sNamespaceObj == null) {
putMsg(result, Status.K8S_NAMESPACE_NOT_EXIST, id);
return result;
}
Date now = new Date();
k8sNamespaceObj.setTag(tag);
k8sNamespaceObj.setLimitsCpu(limitsCpu);
k8sNamespaceObj.setLimitsMemory(limitsMemory);
k8sNamespaceObj.setUpdateTime(now);
k8sNamespaceObj.setOwner(owner);
try {
String yamlStr = genDefaultResourceYaml(k8sNamespaceObj);
k8sClientService.upsertNamespaceAndResourceToK8s(k8sNamespaceObj, yamlStr);
} catch (Exception e) {
logger.error("namespace update to k8s error", e);
putMsg(result, Status.K8S_CLIENT_OPS_ERROR, e.getMessage());
return result;
}
// update to db
k8sNamespaceMapper.updateById(k8sNamespaceObj);
putMsg(result, Status.SUCCESS);
return result;
}
/**
* verify namespace and k8s
*
* @param namespace namespace
* @param k8s k8s
* @return true if the k8s and namespace not exists, otherwise return false
*/
@Override
public Result<Object> verifyNamespaceK8s(String namespace, String k8s) {
Result<Object> result = new Result<>();
if (StringUtils.isEmpty(namespace)) {
putMsg(result, Status.REQUEST_PARAMS_NOT_VALID_ERROR, Constants.NAMESPACE);
return result;
}
if (StringUtils.isEmpty(k8s)) {
putMsg(result, Status.REQUEST_PARAMS_NOT_VALID_ERROR, Constants.K8S);
return result;
}
if (checkNamespaceExistInDb(namespace, k8s)) {
putMsg(result, Status.K8S_NAMESPACE_EXIST, namespace, k8s);
return result;
}
putMsg(result, Status.SUCCESS);
return result;
}
/**
* delete namespace by id
*
* @param loginUser login user
* @param id namespace id
* @return
*/
@Override
public Map<String, Object> deleteNamespaceById(User loginUser, int id) {
Map<String, Object> result = new HashMap<>();
if (isNotAdmin(loginUser, result)) {
return result;
}
K8sNamespace k8sNamespaceObj = k8sNamespaceMapper.selectById(id);
if (k8sNamespaceObj == null) {
putMsg(result, Status.K8S_NAMESPACE_NOT_EXIST, id);
return result;
}
k8sClientService.deleteNamespaceToK8s(k8sNamespaceObj.getNamespace(), k8sNamespaceObj.getK8s());
k8sNamespaceMapper.deleteById(id);
putMsg(result, Status.SUCCESS);
return result;
}
/**
* check namespace name exist
*
* @param namespace namespace
* @return true if the k8s and namespace not exists, otherwise return false
*/
private boolean checkNamespaceExistInDb(String namespace, String k8s) {
return k8sNamespaceMapper.existNamespace(namespace, k8s) == Boolean.TRUE;
}
/**
* use cpu memory create yaml
*
* @param k8sNamespace
* @return yaml file
*/
private String genDefaultResourceYaml(K8sNamespace k8sNamespace) {
//resource use same name with namespace
String name = k8sNamespace.getNamespace();
String namespace = k8sNamespace.getNamespace();
String cpuStr = null;
if (k8sNamespace.getLimitsCpu() != null) {
cpuStr = k8sNamespace.getLimitsCpu() + "";
}
String memoryStr = null;
if (k8sNamespace.getLimitsMemory() != null) {
memoryStr = k8sNamespace.getLimitsMemory() + "Gi";
}
String result = resourceYaml.replace("${name}", name)
.replace("${namespace}", namespace);
if (cpuStr == null) {
result = result.replace("${limitCpu}", "");
} else {
result = result.replace("${limitCpu}", "limits.cpu: '" + cpuStr + "'");
}
if (memoryStr == null) {
result = result.replace("${limitMemory}", "");
} else {
result = result.replace("${limitMemory}", "limits.memory: " + memoryStr);
}
return result;
}
}
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
package org.apache.dolphinscheduler.api.controller;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.apache.dolphinscheduler.api.enums.Status;
import org.apache.dolphinscheduler.api.utils.Result;
import org.apache.dolphinscheduler.common.utils.JSONUtils;
import org.apache.dolphinscheduler.dao.entity.User;
import org.junit.Assert;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* k8s namespace controller test
*/
public class K8sNamespaceControllerTest extends AbstractControllerTest {
private static final String NAMESPACE_CREATE_STRING = "namespace1";
private static final Logger logger = LoggerFactory.getLogger(K8sNamespaceControllerTest.class);
protected User user;
@Test
public void queryProjectListPaging() throws Exception {
MultiValueMap<String, String> paramsMap = new LinkedMultiValueMap<>();
paramsMap.add("searchVal", "");
paramsMap.add("pageNo", "1");
paramsMap.add("pageSize", "20");
MvcResult mvcResult = mockMvc.perform(get("/k8s-namespace")
.header(SESSION_ID, sessionId)
.params(paramsMap))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andReturn();
Result result = JSONUtils.parseObject(mvcResult.getResponse().getContentAsString(), Result.class);
Assert.assertEquals(Status.SUCCESS.getCode(), result.getCode().intValue());
}
@Test
public void createNamespace() throws Exception {
MultiValueMap<String, String> paramsMap = new LinkedMultiValueMap<>();
paramsMap.add("namespace", NAMESPACE_CREATE_STRING);
paramsMap.add("k8s", "k8s");
MvcResult mvcResult = mockMvc.perform(post("/k8s-namespace")
.header(SESSION_ID, sessionId)
.params(paramsMap))
.andExpect(status().isCreated()) //it can
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andReturn();
Result result = JSONUtils.parseObject(mvcResult.getResponse().getContentAsString(), Result.class);
Assert.assertEquals(Status.K8S_CLIENT_OPS_ERROR.getCode(), result.getCode().intValue());//because we not have a k8s cluster in test env
logger.info("create queue return result:{}", mvcResult.getResponse().getContentAsString());
}
@Test
public void updateNamespace() throws Exception {
MultiValueMap<String, String> paramsMap = new LinkedMultiValueMap<>();
paramsMap.add("id", "1");
paramsMap.add("owner", "owmer1");
paramsMap.add("tag", "flink");
MvcResult mvcResult = mockMvc.perform(put("/k8s-namespace/{id}", 1)
.header(SESSION_ID, sessionId)
.params(paramsMap))
.andExpect(status().isCreated())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andReturn();
Result result = JSONUtils.parseObject(mvcResult.getResponse().getContentAsString(), Result.class);
Assert.assertEquals(Status.K8S_CLIENT_OPS_ERROR.getCode(), result.getCode().intValue());
logger.info("update queue return result:{}", mvcResult.getResponse().getContentAsString());
}
@Test
public void verifyNamespace() throws Exception {
// queue value exist
MultiValueMap<String, String> paramsMap = new LinkedMultiValueMap<>();
paramsMap.add("namespace", NAMESPACE_CREATE_STRING);
paramsMap.add("k8s", "default");
// success
MvcResult mvcResult = mockMvc.perform(post("/k8s-namespace/verify")
.header(SESSION_ID, sessionId)
.params(paramsMap))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andReturn();
Result result = JSONUtils.parseObject(mvcResult.getResponse().getContentAsString(), Result.class);
Assert.assertEquals(Status.SUCCESS.getCode(), result.getCode().intValue());
logger.info(mvcResult.getResponse().getContentAsString());
logger.info("verify namespace return result:{}", mvcResult.getResponse().getContentAsString());
//error
paramsMap.clear();
paramsMap.add("namespace", null);
paramsMap.add("k8s", "default");
mvcResult = mockMvc.perform(post("/k8s-namespace/verify")
.header(SESSION_ID, sessionId)
.params(paramsMap))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andReturn();
result = JSONUtils.parseObject(mvcResult.getResponse().getContentAsString(), Result.class);
Assert.assertEquals(Status.VERIFY_K8S_NAMESPACE_ERROR.getCode(), result.getCode().intValue());
logger.info(mvcResult.getResponse().getContentAsString());
logger.info("verify namespace return result:{}", mvcResult.getResponse().getContentAsString());
}
@Test
public void delNamespaceById() throws Exception {
MultiValueMap<String, String> paramsMap = new LinkedMultiValueMap<>();
paramsMap.add("id", "1");
MvcResult mvcResult = mockMvc.perform(post("/k8s-namespace/delete")
.header(SESSION_ID, sessionId)
.params(paramsMap))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andReturn();
Result result = JSONUtils.parseObject(mvcResult.getResponse().getContentAsString(), Result.class);
Assert.assertEquals(Status.DELETE_K8S_NAMESPACE_BY_ID_ERROR.getCode(), result.getCode().intValue());//there is no k8s cluster in test env
logger.info(mvcResult.getResponse().getContentAsString());
}
}
\ No newline at end of file
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
package org.apache.dolphinscheduler.api.service;
import org.apache.dolphinscheduler.api.enums.Status;
import org.apache.dolphinscheduler.api.service.impl.K8sNameSpaceServiceImpl;
import org.apache.dolphinscheduler.api.utils.PageInfo;
import org.apache.dolphinscheduler.api.utils.Result;
import org.apache.dolphinscheduler.common.Constants;
import org.apache.dolphinscheduler.common.enums.UserType;
import org.apache.dolphinscheduler.dao.entity.K8sNamespace;
import org.apache.dolphinscheduler.dao.entity.User;
import org.apache.dolphinscheduler.dao.mapper.K8sNamespaceMapper;
import org.apache.dolphinscheduler.dao.mapper.UserMapper;
import org.apache.dolphinscheduler.service.k8s.K8sClientService;
import org.apache.commons.collections.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@RunWith(MockitoJUnitRunner.class)
public class K8sNameSpaceServiceTest {
private static final Logger logger = LoggerFactory.getLogger(K8sNameSpaceServiceTest.class);
@InjectMocks
private K8sNameSpaceServiceImpl k8sNameSpaceService;
@Mock
private K8sNamespaceMapper k8sNamespaceMapper;
@Mock
private K8sClientService k8sClientService;
@Mock
private UserMapper userMapper;
private String namespace = "default";
private String k8s = "default";
@Before
public void setUp() throws Exception {
Mockito.when(k8sClientService.upsertNamespaceAndResourceToK8s(Mockito.any(K8sNamespace.class), Mockito.anyString())).thenReturn(null);
Mockito.when(k8sClientService.deleteNamespaceToK8s(Mockito.anyString(), Mockito.anyString())).thenReturn(null);
}
@After
public void tearDown() throws Exception {
}
@Test
public void queryListPaging() {
IPage<K8sNamespace> page = new Page<>(1, 10);
page.setTotal(1L);
page.setRecords(getNamespaceList());
Mockito.when(k8sNamespaceMapper.queryK8sNamespacePaging(Mockito.any(Page.class), Mockito.eq(namespace))).thenReturn(page);
Result result = k8sNameSpaceService.queryListPaging(getLoginUser(), namespace, 1, 10);
logger.info(result.toString());
PageInfo<K8sNamespace> pageInfo = (PageInfo<K8sNamespace>) result.getData();
Assert.assertTrue(CollectionUtils.isNotEmpty(pageInfo.getTotalList()));
}
@Test
public void createK8sNamespace() {
// namespace is null
Map<String, Object> result = k8sNameSpaceService.createK8sNamespace(getLoginUser(), null, k8s, null, "tag", 10.0, 100);
logger.info(result.toString());
Assert.assertEquals(Status.REQUEST_PARAMS_NOT_VALID_ERROR, result.get(Constants.STATUS));
// k8s is null
result = k8sNameSpaceService.createK8sNamespace(getLoginUser(), namespace, null, null, "tag", 10.0, 100);
logger.info(result.toString());
Assert.assertEquals(Status.REQUEST_PARAMS_NOT_VALID_ERROR, result.get(Constants.STATUS));
// correct
result = k8sNameSpaceService.createK8sNamespace(getLoginUser(), namespace, k8s, null, "tag", 10.0, 100);
logger.info(result.toString());
Assert.assertEquals(Status.SUCCESS, result.get(Constants.STATUS));
//null limit cpu and mem
result = k8sNameSpaceService.createK8sNamespace(getLoginUser(), namespace, k8s, null, "tag", null, null);
logger.info(result.toString());
Assert.assertEquals(Status.SUCCESS, result.get(Constants.STATUS));
}
@Test
public void updateK8sNamespace() {
Mockito.when(k8sNamespaceMapper.selectById(1)).thenReturn(getNamespace());
Map<String, Object> result = k8sNameSpaceService.updateK8sNamespace(getLoginUser(), 1, null, "tag", null, null);
logger.info(result.toString());
Assert.assertEquals(Status.SUCCESS, result.get(Constants.STATUS));
result = k8sNameSpaceService.updateK8sNamespace(getLoginUser(), 1, null, "tag", -1.0, 100);
logger.info(result.toString());
Assert.assertEquals(Status.REQUEST_PARAMS_NOT_VALID_ERROR, result.get(Constants.STATUS));
result = k8sNameSpaceService.updateK8sNamespace(getLoginUser(), 1, null, "tag", 1.0, 100);
logger.info(result.toString());
Assert.assertEquals(Status.SUCCESS, result.get(Constants.STATUS));
}
@Test
public void verifyNamespaceK8s() {
Mockito.when(k8sNamespaceMapper.existNamespace(namespace, k8s)).thenReturn(true);
//namespace null
Result result = k8sNameSpaceService.verifyNamespaceK8s(null, k8s);
logger.info(result.toString());
Assert.assertEquals(result.getCode().intValue(), Status.REQUEST_PARAMS_NOT_VALID_ERROR.getCode());
//k8s null
result = k8sNameSpaceService.verifyNamespaceK8s(namespace, null);
logger.info(result.toString());
Assert.assertEquals(result.getCode().intValue(), Status.REQUEST_PARAMS_NOT_VALID_ERROR.getCode());
//exist
result = k8sNameSpaceService.verifyNamespaceK8s(namespace, k8s);
logger.info(result.toString());
Assert.assertEquals(result.getCode().intValue(), Status.K8S_NAMESPACE_EXIST.getCode());
//not exist
result = k8sNameSpaceService.verifyNamespaceK8s(namespace, "other k8s");
logger.info(result.toString());
Assert.assertEquals(result.getCode().intValue(), Status.SUCCESS.getCode());
}
@Test
public void deleteNamespaceById() {
Mockito.when(k8sNamespaceMapper.deleteById(Mockito.any())).thenReturn(1);
Mockito.when(k8sNamespaceMapper.selectById(1)).thenReturn(getNamespace());
Map<String, Object> result = k8sNameSpaceService.deleteNamespaceById(getLoginUser(), 1);
logger.info(result.toString());
Assert.assertEquals(Status.SUCCESS, result.get(Constants.STATUS));
}
private User getLoginUser() {
User loginUser = new User();
loginUser.setUserType(UserType.ADMIN_USER);
loginUser.setId(99999999);
return loginUser;
}
private K8sNamespace getNamespace() {
K8sNamespace k8sNamespace = new K8sNamespace();
k8sNamespace.setId(1);
k8sNamespace.setK8s(k8s);
k8sNamespace.setNamespace(namespace);
return k8sNamespace;
}
private List<K8sNamespace> getNamespaceList() {
List<K8sNamespace> k8sNamespaceList = new ArrayList<>();
k8sNamespaceList.add(getNamespace());
return k8sNamespaceList;
}
}
\ No newline at end of file
......@@ -809,4 +809,11 @@ public final class Constants {
public static final String CACHE_KEY_VALUE_ALL = "'all'";
/**
* use for k8s
*/
public static final String NAMESPACE = "namespace";
public static final String K8S = "k8s";
public static final String LIMITS_CPU = "limitsCpu";
public static final String LIMITS_MEMORY = "limitsMemory";
}
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
package org.apache.dolphinscheduler.dao.entity;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
/**
* multi-data centre k8s temporary structure, waiting for new feature to complete will switch
*/
@TableName("t_ds_k8s")
public class K8s {
/**
* id
*/
@TableId(value = "id", type = IdType.AUTO)
private int id;
/**
* k8s name
*/
@TableField(value = "k8s_name")
private String k8sName;
/**
* k8s client config(yaml or json)
*/
@TableField(value = "k8s_config")
private String k8sConfig;
/**
* create_time
*/
@TableField("create_time")
private Date createTime;
/**
* update_time
*/
@TableField("update_time")
private Date updateTime;
public K8s() {
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getK8sName() {
return k8sName;
}
public void setK8sName(String k8sName) {
this.k8sName = k8sName;
}
public String getK8sConfig() {
return k8sConfig;
}
public void setK8sConfig(String k8sConfig) {
this.k8sConfig = k8sConfig;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Date getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}
}
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
package org.apache.dolphinscheduler.dao.entity;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
/**
* k8s namespace
*/
@TableName("t_ds_k8s_namespace")
public class K8sNamespace {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* namespace name
*/
@TableField(value = "namespace")
private String namespace;
/**
* total cpu limit
*/
@TableField(value = "limits_cpu")
private Double limitsCpu;
/**
* total memory limit,mi
*/
private Integer limitsMemory;
/**
* owner
*/
@TableField(value = "owner")
private String owner;
/**
* create_time
*/
@TableField("create_time")
private Date createTime;
/**
* update_time
*/
@TableField("update_time")
private Date updateTime;
/**
* tag use for set this namespace allow run which type
*/
@TableField("tag")
private String tag;
@TableField("pod_request_cpu")
private Double podRequestCpu = 0.0;
/**
* Mi
*/
@TableField("pod_request_memory")
private Integer podRequestMemory = 0;
/**
*
*/
@TableField("pod_replicas")
private Integer podReplicas = 0;
/**
* online job
*/
@TableField("online_job_num")
private Integer onlineJobNum = 0;
/**
* k8s name
*/
@TableField("k8s")
private String k8s;
public Integer getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getNamespace() {
return namespace;
}
public void setNamespace(String namespace) {
this.namespace = namespace;
}
public Double getLimitsCpu() {
return limitsCpu;
}
public void setLimitsCpu(Double limitsCpu) {
this.limitsCpu = limitsCpu;
}
public Integer getLimitsMemory() {
return limitsMemory;
}
public void setLimitsMemory(Integer limitsMemory) {
this.limitsMemory = limitsMemory;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Date getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}
public String getTag() {
return tag;
}
public void setTag(String tag) {
this.tag = tag;
}
public Integer getPodRequestMemory() {
return podRequestMemory;
}
public void setPodRequestMemory(Integer podRequestMemory) {
this.podRequestMemory = podRequestMemory;
}
public Integer getPodReplicas() {
return podReplicas;
}
public void setPodReplicas(Integer podReplicas) {
this.podReplicas = podReplicas;
}
public Integer getOnlineJobNum() {
return onlineJobNum;
}
public void setOnlineJobNum(Integer onlineJobNum) {
this.onlineJobNum = onlineJobNum;
}
public String getK8s() {
return k8s;
}
public void setK8s(String k8s) {
this.k8s = k8s;
}
public Double getPodRequestCpu() {
return podRequestCpu;
}
public void setPodRequestCpu(Double podRequestCpu) {
this.podRequestCpu = podRequestCpu;
}
}
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
package org.apache.dolphinscheduler.dao.mapper;
import org.apache.dolphinscheduler.dao.entity.K8s;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* k8s mapper interface
*/
public interface K8sMapper extends BaseMapper<K8s> {
}
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
package org.apache.dolphinscheduler.dao.mapper;
import org.apache.dolphinscheduler.dao.entity.K8sNamespace;
import org.apache.ibatis.annotations.Param;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
/**
* namespace interface
*/
public interface K8sNamespaceMapper extends BaseMapper<K8sNamespace> {
/**
* k8s namespace page
*
* @param page page
* @param searchVal searchVal
* @return k8s namespace IPage
*/
IPage<K8sNamespace> queryK8sNamespacePaging(IPage<K8sNamespace> page,
@Param("searchVal") String searchVal);
/**
* check the target namespace exist
*
* @param namespace namespace
* @param k8s k8s name
* @return true if exist else return null
*/
Boolean existNamespace(@Param("namespace") String namespace, @Param("k8s") String k8s);
}
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You under the Apache License, Version 2.0
~ (the "License"); you may not use this file except in compliance with
~ the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ 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.
-->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.apache.dolphinscheduler.dao.mapper.K8sNamespaceMapper">
<sql id="baseSql">
id, namespace, k8s, owner, tag, limits_memory, limits_cpu, online_job_num, pod_replicas, pod_request_cpu, pod_request_memory, create_time, update_time
</sql>
<select id="queryK8sNamespacePaging" resultType="org.apache.dolphinscheduler.dao.entity.K8sNamespace">
select
<include refid="baseSql"/>
from t_ds_k8s_namespace
where 1= 1
<if test="searchVal != null and searchVal != ''">
and namespace like concat('%', #{searchVal}, '%')
</if>
order by update_time desc
</select>
<select id="existNamespace" resultType="java.lang.Boolean">
select 1 = 1
from t_ds_k8s_namespace
where 1 = 1
<if test="namespace != null and namespace != ''">
and namespace = #{namespace}
</if>
<if test="k8s != null and k8s != ''">
and k8s =#{k8s}
</if>
</select>
</mapper>
......@@ -1870,4 +1870,41 @@ CREATE TABLE t_ds_audit_log
time timestamp NULL DEFAULT CURRENT_TIMESTAMP,
resource_id int(11) NOT NULL,
PRIMARY KEY (id)
);
\ No newline at end of file
);
DROP TABLE IF EXISTS t_ds_k8s;
CREATE TABLE t_ds_k8s
(
id int(11) NOT NULL AUTO_INCREMENT ,
k8s_name varchar(100) DEFAULT NULL ,
k8s_config text DEFAULT NULL,
create_time datetime DEFAULT NULL ,
update_time datetime DEFAULT NULL ,
PRIMARY KEY (id)
);
DROP TABLE IF EXISTS t_ds_k8s_namespace;
CREATE TABLE t_ds_k8s_namespace (
id int(11) NOT NULL AUTO_INCREMENT ,
limits_memory int(11) DEFAULT NULL,
namespace varchar(100) DEFAULT NULL,
online_job_num int(11) DEFAULT NULL,
owner varchar(100) DEFAULT NULL,
pod_replicas int(11) DEFAULT NULL,
pod_request_cpu decimal(14,3) DEFAULT NULL,
pod_request_memory int(11) DEFAULT NULL,
tag varchar(100) DEFAULT NULL,
limits_cpu decimal(14,3) DEFAULT NULL,
k8s varchar(100) DEFAULT NULL,
create_time datetime DEFAULT NULL ,
update_time datetime DEFAULT NULL ,
PRIMARY KEY (id) ,
UNIQUE KEY k8s_namespace_unique (namespace,k8s)
);
-- ----------------------------
-- Records of t_ds_k8s_namespace
-- ----------------------------
INSERT INTO t_ds_k8s_namespace
VALUES (1, 10000, 'default', 99, 'owner',1,NULL,1,'test',NULL,'default',null,null);
\ No newline at end of file
......@@ -1856,4 +1856,39 @@ CREATE TABLE `t_ds_audit_log` (
`time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT 'create time',
`resource_id` int(11) NULL DEFAULT NULL COMMENT 'resource id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT= 1 DEFAULT CHARSET=utf8;
\ No newline at end of file
) ENGINE=InnoDB AUTO_INCREMENT= 1 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for t_ds_k8s
-- ----------------------------
DROP TABLE IF EXISTS `t_ds_k8s`;
CREATE TABLE `t_ds_k8s` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`k8s_name` varchar(100) DEFAULT NULL,
`k8s_config` text DEFAULT NULL,
`create_time` datetime DEFAULT NULL COMMENT 'create time',
`update_time` datetime DEFAULT NULL COMMENT 'update time',
PRIMARY KEY (`id`)
) ENGINE= INNODB AUTO_INCREMENT= 1 DEFAULT CHARSET= utf8;
-- ----------------------------
-- Table structure for t_ds_k8s_namespace
-- ----------------------------
DROP TABLE IF EXISTS `t_ds_k8s_namespace`;
CREATE TABLE `t_ds_k8s_namespace` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`limits_memory` int(11) DEFAULT NULL,
`namespace` varchar(100) DEFAULT NULL,
`online_job_num` int(11) DEFAULT NULL,
`owner` varchar(100) DEFAULT NULL,
`pod_replicas` int(11) DEFAULT NULL,
`pod_request_cpu` decimal(14,3) DEFAULT NULL,
`pod_request_memory` int(11) DEFAULT NULL,
`tag` varchar(100) DEFAULT NULL,
`limits_cpu` decimal(14,3) DEFAULT NULL,
`k8s` varchar(100) DEFAULT NULL,
`create_time` datetime DEFAULT NULL COMMENT 'create time',
`update_time` datetime DEFAULT NULL COMMENT 'update time',
PRIMARY KEY (`id`),
UNIQUE KEY `k8s_namespace_unique` (`namespace`,`k8s`)
) ENGINE= INNODB AUTO_INCREMENT= 1 DEFAULT CHARSET= utf8;
......@@ -1850,4 +1850,41 @@ CREATE TABLE t_ds_audit_log (
time timestamp DEFAULT NULL ,
resource_id int NOT NULL,
PRIMARY KEY (id)
);
\ No newline at end of file
);
--
-- Table structure for table t_ds_k8s
--
DROP TABLE IF EXISTS t_ds_k8s;
CREATE TABLE t_ds_k8s (
id serial NOT NULL,
k8s_name VARCHAR(100) DEFAULT NULL ,
k8s_config text ,
create_time timestamp DEFAULT NULL ,
update_time timestamp DEFAULT NULL ,
PRIMARY KEY (id)
);
--
-- Table structure for table t_ds_k8s_namespace
--
DROP TABLE IF EXISTS t_ds_k8s_namespace;
CREATE TABLE t_ds_k8s_namespace (
id serial NOT NULL,
limits_memory int DEFAULT NULL ,
namespace varchar(100) DEFAULT NULL ,
online_job_num int DEFAULT '0' ,
owner varchar(100) DEFAULT NULL,
pod_replicas int(11) DEFAULT NULL,
pod_request_cpu NUMERIC(13,4) NULL,
pod_request_memory int(11) DEFAULT NULL,
tag varchar(100) DEFAULT NULL,
limits_cpu NUMERIC(13,4) NULL,
k8s varchar(100) DEFAULT NULL,
create_time timestamp DEFAULT NULL ,
update_time timestamp DEFAULT NULL ,
PRIMARY KEY (id) ,
CONSTRAINT k8s_namespace_unique UNIQUE (namespace,k8s)
);
......@@ -168,3 +168,39 @@ CREATE TABLE `t_ds_relation_rule_input_entry` (
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for t_ds_k8s
-- ----------------------------
DROP TABLE IF EXISTS `t_ds_k8s`;
CREATE TABLE `t_ds_k8s` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`k8s_name` varchar(100) DEFAULT NULL,
`k8s_config` text DEFAULT NULL,
`create_time` datetime DEFAULT NULL COMMENT 'create time',
`update_time` datetime DEFAULT NULL COMMENT 'update time',
PRIMARY KEY (`id`)
) ENGINE= INNODB AUTO_INCREMENT= 1 DEFAULT CHARSET= utf8;
-- ----------------------------
-- Table structure for t_ds_k8s_namespace
-- ----------------------------
DROP TABLE IF EXISTS `t_ds_k8s_namespace`;
CREATE TABLE `t_ds_k8s_namespace` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`limits_memory` int(11) DEFAULT NULL,
`namespace` varchar(100) DEFAULT NULL,
`online_job_num` int(11) DEFAULT NULL,
`owner` varchar(100) DEFAULT NULL,
`pod_replicas` int(11) DEFAULT NULL,
`pod_request_cpu` decimal(14,3) DEFAULT NULL,
`pod_request_memory` int(11) DEFAULT NULL,
`tag` varchar(100) DEFAULT NULL,
`limits_cpu` decimal(14,3) DEFAULT NULL,
`k8s` varchar(100) DEFAULT NULL,
`create_time` datetime DEFAULT NULL COMMENT 'create time',
`update_time` datetime DEFAULT NULL COMMENT 'update time',
PRIMARY KEY (`id`),
UNIQUE KEY `k8s_namespace_unique` (`namespace`,`k8s`)
) ENGINE= INNODB AUTO_INCREMENT= 1 DEFAULT CHARSET= utf8;
\ No newline at end of file
......@@ -154,6 +154,32 @@ EXECUTE 'CREATE INDEX IF NOT EXISTS idx_task_definition_log_project_code ON ' ||
EXECUTE 'DROP INDEX IF EXISTS "idx_task_instance_code_version"';
EXECUTE 'CREATE INDEX IF NOT EXISTS idx_task_instance_code_version ON' || quote_ident(v_schema) ||'.t_ds_task_instance USING Btree("task_code","task_definition_version")';
EXECUTE 'CREATE TABLE IF NOT EXISTS '|| quote_ident(v_schema) ||'."t_ds_k8s" (
id serial NOT NULL,
k8s_name VARCHAR(100) DEFAULT NULL ,
k8s_config text ,
create_time timestamp DEFAULT NULL ,
update_time timestamp DEFAULT NULL ,
PRIMARY KEY (id)
)';
EXECUTE 'CREATE TABLE IF NOT EXISTS '|| quote_ident(v_schema) ||'."t_ds_k8s_namespace" (
id serial NOT NULL,
limits_memory int DEFAULT NULL ,
namespace varchar(100) DEFAULT NULL ,
online_job_num int DEFAULT ''0'' ,
owner varchar(100) DEFAULT NULL,
pod_replicas int(11) DEFAULT NULL,
pod_request_cpu NUMERIC(13,4) NULL,
pod_request_memory int(11) DEFAULT NULL,
tag varchar(100) DEFAULT NULL,
limits_cpu NUMERIC(13,4) NULL,
k8s varchar(100) DEFAULT NULL,
create_time timestamp DEFAULT NULL ,
update_time timestamp DEFAULT NULL ,
PRIMARY KEY (id) ,
CONSTRAINT k8s_namespace_unique UNIQUE (namespace,k8s)
)';
return 'Success!';
exception when others then
......
......@@ -405,6 +405,33 @@ The text of each license is also included at licenses/LICENSE-[project].txt.
protostuff-collectionschema 1.7.2: https://github.com/protostuff/protostuff/protostuff-collectionschema Apache-2.0
prometheus client_java(simpleclient) 0.12.0: https://github.com/prometheus/client_java, Apache 2.0
snowflake snowflake-2010: https://github.com/twitter-archive/snowflake/tree/snowflake-2010, Apache 2.0
kubernetes-client 5.8.0: https://mvnrepository.com/artifact/io.fabric8/kubernetes-client/5.8.0, Apache 2.0
kubernetes-model-admissionregistration 5.8.0: https://mvnrepository.com/artifact/io.fabric8/kubernetes-model-admissionregistration/5.8.0, Apache 2.0
kubernetes-model-apiextensions 5.8.0: https://mvnrepository.com/artifact/io.fabric8/kubernetes-model-apiextensions/5.8.0, Apache 2.0
kubernetes-model-apps 5.8.0: https://mvnrepository.com/artifact/io.fabric8/kubernetes-model-apps/5.8.0, Apache 2.0
kubernetes-model-autoscaling 5.8.0: https://mvnrepository.com/artifact/io.fabric8/kubernetes-model-autoscaling/5.8.0, Apache 2.0
kubernetes-model-batch 5.8.0: https://mvnrepository.com/artifact/io.fabric8/kubernetes-model-autoscaling/5.8.0, Apache 2.0
kubernetes-model-certificates 5.8.0: https://mvnrepository.com/artifact/io.fabric8/kubernetes-model-certificates/5.8.0, Apache 2.0
kubernetes-model-common 5.8.0: https://mvnrepository.com/artifact/io.fabric8/kubernetes-model-common/5.8.0, Apache 2.0
kubernetes-model-coordination 5.8.0: https://mvnrepository.com/artifact/io.fabric8/kubernetes-model-coordination/5.8.0, Apache 2.0
kubernetes-model-core 5.8.0: https://mvnrepository.com/artifact/io.fabric8/kubernetes-model-core/5.8.0, Apache 2.0
kubernetes-model-discovery 5.8.0: https://mvnrepository.com/artifact/io.fabric8/kubernetes-model-discovery/5.8.0, Apache 2.0
kubernetes-model-events 5.8.0: https://mvnrepository.com/artifact/io.fabric8/kubernetes-model-events/5.8.0, Apache 2.0
kubernetes-model-extensions 5.8.0: https://mvnrepository.com/artifact/io.fabric8/kubernetes-model-extensions/5.8.0, Apache 2.0
kubernetes-model-flowcontrol 5.8.0: https://mvnrepository.com/artifact/io.fabric8/kubernetes-model-flowcontrol/5.8.0, Apache 2.0
kubernetes-model-metrics 5.8.0: https://mvnrepository.com/artifact/io.fabric8/kubernetes-model-metrics/5.8.0, Apache 2.0
kubernetes-model-networking 5.8.0: https://mvnrepository.com/artifact/io.fabric8/kubernetes-model-networking/5.8.0, Apache 2.0
kubernetes-model-node 5.8.0: https://mvnrepository.com/artifact/io.fabric8/kubernetes-model-node/5.8.0, Apache 2.0
kubernetes-model-policy 5.8.0: https://mvnrepository.com/artifact/io.fabric8/kubernetes-model-policy/5.8.0, Apache 2.0
kubernetes-model-rbac 5.8.0: https://mvnrepository.com/artifact/io.fabric8/kubernetes-model-rbac/5.8.0, Apache 2.0
kubernetes-model-scheduling 5.8.0: https://mvnrepository.com/artifact/io.fabric8/kubernetes-model-scheduling/5.8.0, Apache 2.0
kubernetes-model-storageclass 5.8.0: https://mvnrepository.com/artifact/io.fabric8/kubernetes-model-storageclass/5.8.0, Apache 2.0
zjsonpatch 0.3.0 https://mvnrepository.com/artifact/io.fabric8/zjsonpatch/0.3.0, Apache 2.0
generex 1.0.2 https://mvnrepository.com/artifact/com.github.mifmif/generex/1.0.2, Apache 2.0
jackson-dataformat-yaml 2.12.5 https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.12.5, Apache 2.0
logging-interceptor 3.14.9 https://mvnrepository.com/artifact/com.squareup.okhttp3/logging-interceptor/3.14.9, Apache 2.0
okhttp 3.14.3 https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp/3.14.3, Apache 2.0
okio 1.17.2 https://mvnrepository.com/artifact/com.squareup.okio/okio/1.17.2, Apache 2.0
========================================================================
BSD licenses
......@@ -428,6 +455,7 @@ The text of each license is also included at licenses/LICENSE-[project].txt.
LatencyUtils 2.0.3: https://github.com/LatencyUtils/LatencyUtils, BSD-2-Clause
janino 3.1.6: https://mvnrepository.com/artifact/org.codehaus.janino/janino/3.1.6, BSD 3-clause
commons-compiler 3.1.6: https://mvnrepository.com/artifact/org.codehaus.janino/janino/3.1.6, BSD 3-clause
automaton 1.11-8 https://mvnrepository.com/artifact/dk.brics.automaton/automaton/1.11-8, BSD 2-clause
========================================================================
CDDL licenses
......
Copyright (c) 2001-2022 Anders Moeller
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. The name of the author may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
/*
* Licensed to Apache Software Foundation (ASF) under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Apache Software Foundation (ASF) licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
package org.apache.dolphinscheduler.e2e.pages.security;
import org.apache.dolphinscheduler.e2e.pages.common.NavBarPage;
import java.util.List;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import lombok.Getter;
@Getter
public final class NamespacePage extends NavBarPage implements SecurityPage.Tab {
@FindBy(id = "btnCreateNamespace")
private WebElement buttonCreateNamespace;
@FindBy(className = "items")
private List<WebElement> namespaceList;
private final NamespaceForm createNamespaceForm;
private final NamespaceForm editNamespaceForm;
public NamespacePage(RemoteWebDriver driver) {
super(driver);
createNamespaceForm = new NamespaceForm();
editNamespaceForm = new NamespaceForm();
}
public NamespacePage create(String namespaceName, String namespaceValue) {
buttonCreateNamespace().click();
createNamespaceForm().inputNamespaceName().sendKeys(namespaceName);
createNamespaceForm().inputNamespaceValue().sendKeys(namespaceValue);
createNamespaceForm().buttonSubmit().click();
return this;
}
public NamespacePage update(String namespaceName, String editNamespaceName, String editNamespaceValue) {
namespaceList()
.stream()
.filter(it -> it.findElement(By.className("namespaceName")).getAttribute("innerHTML").contains(namespaceName))
.flatMap(it -> it.findElements(By.className("edit")).stream())
.filter(WebElement::isDisplayed)
.findFirst()
.orElseThrow(() -> new RuntimeException("No edit button in namespace list"))
.click();
editNamespaceForm().inputNamespaceName().sendKeys(editNamespaceName);
editNamespaceForm().inputNamespaceValue().sendKeys(editNamespaceValue);
editNamespaceForm().buttonSubmit().click();
return this;
}
@Getter
public class NamespaceForm {
NamespaceForm() {
PageFactory.initElements(driver, this);
}
@FindBy(id = "inputNamespaceName")
private WebElement inputNamespaceName;
@FindBy(id = "inputNamespaceValue")
private WebElement inputNamespaceValue;
@FindBy(id = "btnSubmit")
private WebElement buttonSubmit;
@FindBy(id = "btnCancel")
private WebElement buttonCancel;
}
}
......@@ -52,6 +52,9 @@ public class SecurityPage extends NavBarPage implements NavBarItem {
@FindBy(className = "tab-token-manage")
private WebElement menuTokenManage;
@FindBy(className = "tab-namespace-manage")
private WebElement menuNamespaceManage;
public SecurityPage(RemoteWebDriver driver) {
super(driver);
}
......@@ -88,6 +91,10 @@ public class SecurityPage extends NavBarPage implements NavBarItem {
menuTokenManage().click();
return tab.cast(new TokenPage(driver));
}
if (tab == NamespacePage.class) {
menuNamespaceManage().click();
return tab.cast(new NamespacePage(driver));
}
throw new UnsupportedOperationException("Unknown tab: " + tab.getName());
}
......
......@@ -90,5 +90,9 @@
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-client</artifactId>
</dependency>
</dependencies>
</project>
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
package org.apache.dolphinscheduler.service.k8s;
import org.apache.dolphinscheduler.dao.entity.K8sNamespace;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.yaml.snakeyaml.Yaml;
import io.fabric8.kubernetes.api.model.Namespace;
import io.fabric8.kubernetes.api.model.NamespaceList;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.api.model.ResourceQuota;
import io.fabric8.kubernetes.client.KubernetesClient;
/**
* Encapsulates all client-related operations, not involving the db
*/
@Component
public class K8sClientService {
private static Yaml yaml = new Yaml();
@Autowired
private K8sManager k8sManager;
public ResourceQuota upsertNamespaceAndResourceToK8s(K8sNamespace k8sNamespace, String yamlStr) {
upsertNamespaceToK8s(k8sNamespace.getNamespace(), k8sNamespace.getK8s());
return upsertNamespacedResourceToK8s(k8sNamespace, yamlStr);
}
public Optional<Namespace> deleteNamespaceToK8s(String name, String k8s) {
Optional<Namespace> result = getNamespaceFromK8s(name, k8s);
if (result.isPresent()) {
KubernetesClient client = k8sManager.getK8sClient(k8s);
Namespace body = new Namespace();
ObjectMeta meta = new ObjectMeta();
meta.setNamespace(name);
meta.setName(name);
body.setMetadata(meta);
client.namespaces().delete(body);
}
return getNamespaceFromK8s(name, k8s);
}
private ResourceQuota upsertNamespacedResourceToK8s(K8sNamespace k8sNamespace, String yamlStr) {
KubernetesClient client = k8sManager.getK8sClient(k8sNamespace.getK8s());
//创建资源
ResourceQuota queryExist = client.resourceQuotas()
.inNamespace(k8sNamespace.getNamespace())
.withName(k8sNamespace.getNamespace())
.get();
ResourceQuota body = yaml.loadAs(yamlStr, ResourceQuota.class);
if (queryExist != null) {
if (k8sNamespace.getLimitsCpu() == null && k8sNamespace.getLimitsMemory() == null) {
client.resourceQuotas().inNamespace(k8sNamespace.getNamespace())
.withName(k8sNamespace.getNamespace())
.delete();
return null;
}
}
return client.resourceQuotas().inNamespace(k8sNamespace.getNamespace())
.withName(k8sNamespace.getNamespace())
.createOrReplace(body);
}
private Optional<Namespace> getNamespaceFromK8s(String name, String k8s) {
NamespaceList listNamespace =
k8sManager.getK8sClient(k8s).namespaces().list();
Optional<Namespace> list =
listNamespace.getItems().stream()
.filter((Namespace namespace) ->
namespace.getMetadata().getName().equals(name))
.findFirst();
return list;
}
private Namespace upsertNamespaceToK8s(String name, String k8s) {
Optional<Namespace> result = getNamespaceFromK8s(name, k8s);
//if not exist create
if (!result.isPresent()) {
KubernetesClient client = k8sManager.getK8sClient(k8s);
Namespace body = new Namespace();
ObjectMeta meta = new ObjectMeta();
meta.setNamespace(name);
meta.setName(name);
body.setMetadata(meta);
return client.namespaces().create(body);
}
return result.get();
}
}
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
package org.apache.dolphinscheduler.service.k8s;
import org.apache.dolphinscheduler.dao.entity.K8s;
import org.apache.dolphinscheduler.dao.mapper.K8sMapper;
import org.apache.dolphinscheduler.remote.exceptions.RemotingException;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClient;
/**
* A separate class, because then wait for multiple environment feature, currently using db configuration, later unified
*/
@Component
public class K8sManager {
private static final Logger logger = LoggerFactory.getLogger(K8sManager.class);
/**
* cache k8s client
*/
private static Map<String, KubernetesClient> clientMap = new Hashtable<>();
@Autowired
private K8sMapper k8sMapper;
public KubernetesClient getK8sClient(String k8sName) {
if (null == k8sName) {
return null;
}
return clientMap.get(k8sName);
}
@EventListener
public void buildApiClientAll(ApplicationReadyEvent readyEvent) throws RemotingException {
QueryWrapper<K8s> nodeWrapper = new QueryWrapper<>();
List<K8s> k8sList = k8sMapper.selectList(nodeWrapper);
if (k8sList != null) {
for (K8s k8s : k8sList) {
DefaultKubernetesClient client = getClient(k8s.getK8sConfig());
clientMap.put(k8s.getK8sName(), client);
}
}
}
private DefaultKubernetesClient getClient(String configYaml) throws RemotingException {
try {
Config config = Config.fromKubeconfig(configYaml);
return new DefaultKubernetesClient(config);
} catch (Exception e) {
logger.error("fail to get k8s ApiClient", e);
throw new RemotingException("fail to get k8s ApiClient:" + e.getMessage());
}
}
}
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
package org.apache.dolphinscheduler.service.k8s;
import org.apache.dolphinscheduler.dao.entity.K8s;
import org.apache.dolphinscheduler.dao.mapper.K8sMapper;
import java.util.ArrayList;
import java.util.List;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import io.fabric8.kubernetes.client.KubernetesClient;
@RunWith(MockitoJUnitRunner.Silent.class)
public class K8sManagerTest {
@InjectMocks
private K8sManager k8sManager;
@Mock
private K8sMapper k8sMapper;
@Before
public void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
}
@Test
public void getK8sClient() {
Mockito.when(k8sMapper.selectList(Mockito.any())).thenReturn(getK8sList());
KubernetesClient result = k8sManager.getK8sClient("must null");
Assert.assertNull(result);
result = k8sManager.getK8sClient(null);
Assert.assertNull(result);
}
private K8s getK8s() {
K8s k8s = new K8s();
k8s.setId(1);
k8s.setK8sName("default");
k8s.setK8sConfig("k8s config");
return k8s;
}
private List<K8s> getK8sList() {
List<K8s> k8sList = new ArrayList<>();
k8sList.add(getK8s());
return k8sList;
}
}
\ No newline at end of file
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
<template>
<m-popover
ref="popover"
:ok-text="item ? $t('Edit') : $t('Submit')"
@ok="_ok"
@close="close">
<template slot="content">
<div class="create-tenement-model">
<m-list-box-f>
<template slot="name"><strong>*</strong>{{$t('Name')}}</template>
<template slot="content">
<el-input
type="input"
v-model="namespace"
maxlength="60"
size="mini"
:disabled="item ? true: false"
:placeholder="$t('Please enter name')">
</el-input>
</template>
</m-list-box-f>
<m-list-box-f>
<template slot="name"><strong>*</strong>{{$t('K8s Cluster')}}</template>
<template slot="content">
<el-input
type="input"
v-model="k8s"
maxlength="60"
size="mini"
:disabled="item ? true: false"
:placeholder="$t('Please enter k8s cluster')">
</el-input>
</template>
</m-list-box-f>
<m-list-box-f>
<template slot="name">{{$t('K8s Tag')}}</template>
<template slot="content">
<el-input
type="input"
v-model="tag"
maxlength="60"
size="mini"
:placeholder="$t('Please enter k8s cluster')">
</el-input>
</template>
</m-list-box-f>
<m-list-box-f>
<template slot="name">{{$t('Limits Cpu')}}</template>
<template slot="content">
<el-input
v-model="limitsCpu"
size="small"
>
<template slot="append">Core</template>
</el-input>
</template>
</m-list-box-f>
<m-list-box-f>
<template slot="name">{{$t('Limits Memory')}}</template>
<template slot="content">
<el-input
v-model="limitsMemory"
size="small"
>
<template slot="append">GB</template>
</el-input>
</template>
</m-list-box-f>
<m-list-box-f>
<template slot="name">{{$t('Namespace Owner')}}</template>
<template slot="content">
<el-input
type="input"
v-model="owner"
maxlength="60"
size="mini"
:placeholder="$t('Please enter owner')">
</el-input>
</template>
</m-list-box-f>
</div>
</template>
</m-popover>
</template>
<script>
import _ from 'lodash'
import i18n from '@/module/i18n'
import store from '@/conf/home/store'
import mPopover from '@/module/components/popup/popover'
import mListBoxF from '@/module/components/listBoxF/listBoxF'
export default {
name: 'create-namespace',
data () {
return {
store,
namespace: '',
k8s: '',
owner: '',
tag: '',
limitsCpu: '',
limitsMemory: ''
}
},
props: {
item: Object
},
methods: {
_ok () {
if (!this._verification()) {
return
}
let param = {
namespace: _.trim(this.namespace),
k8s: _.trim(this.k8s),
owner: _.trim(this.owner),
tag: _.trim(this.tag),
limitsCpu: _.trim(this.limitsCpu),
limitsMemory: _.trim(this.limitsMemory)
}
// edit
if (this.item) {
param.id = this.item.id
}
let $then = (res) => {
this.$emit('onUpdate')
this.$message.success(res.msg)
this.$refs.popover.spinnerLoading = false
}
let $catch = (e) => {
this.$message.error(e.msg || '')
this.$refs.popover.spinnerLoading = false
}
if (this.item) {
this.$refs.popover.spinnerLoading = true
this.store.dispatch('security/updateNamespace', param).then(res => {
$then(res)
}).catch(e => {
$catch(e)
})
} else {
this._verifyName(param).then(() => {
this.$refs.popover.spinnerLoading = true
this.store.dispatch('security/createNamespace', param).then(res => {
$then(res)
}).catch(e => {
$catch(e)
})
}).catch(e => {
this.$message.error(e.msg || '')
})
}
},
_verification () {
if (!this.namespace.replace(/\s*/g, '')) {
this.$message.warning(`${i18n.$t('Please enter name')}`)
return false
}
if (!this.k8s.replace(/\s*/g, '')) {
this.$message.warning(`${i18n.$t('Please enter namespace')}`)
return false
}
return true
},
_verifyName (param) {
return new Promise((resolve, reject) => {
this.store.dispatch('security/verifyNamespaceK8s', param).then(res => {
resolve()
}).catch(e => {
reject(e)
})
})
},
close () {
this.$emit('close')
}
},
watch: {
},
created () {
if (this.item) {
this.namespace = this.item.namespace
this.k8s = this.item.k8s
this.owner = this.item.owner
this.tag = this.item.tag
this.limitsCpu = this.item.limitsCpu
this.limitsMemory = this.item.limitsMemory
}
},
mounted () {
},
components: { mPopover, mListBoxF }
}
</script>
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
<template>
<div class="list-model">
<div class="table-box">
<el-table :data="list" size="mini" style="width: 100%">
<el-table-column type="index" :label="$t('#')" width="50"></el-table-column>
<el-table-column prop="namespace" :label="$t('K8s Namespace')"></el-table-column>
<el-table-column prop="k8s" :label="$t('K8s Cluster')"></el-table-column>
<el-table-column prop="owner" :label="$t('Namespace Owner')"></el-table-column>
<el-table-column prop="tag" :label="$t('K8s Tag')"></el-table-column>
<el-table-column prop="limitsCpu" :label="$t('Limits Cpu')"></el-table-column>
<el-table-column prop="limitsMemory" :label="$t('Limits Memory')"></el-table-column>
<el-table-column :label="$t('Create Time')" min-width="120">
<template slot-scope="scope">
<span>{{scope.row.createTime | formatDate}}</span>
</template>
</el-table-column>
<el-table-column :label="$t('Update Time')" min-width="120">
<template slot-scope="scope">
<span>{{scope.row.updateTime | formatDate}}</span>
</template>
</el-table-column>
<el-table-column :label="$t('Operation')" width="100">
<template slot-scope="scope">
<el-tooltip :content="$t('Edit')" placement="top">
<el-button type="primary" size="mini" icon="el-icon-edit-outline" @click="_edit(scope.row)" circle></el-button>
</el-tooltip>
<el-tooltip :content="$t('Delete')" placement="top">
<el-popconfirm
:confirmButtonText="$t('Confirm')"
:cancelButtonText="$t('Cancel')"
icon="el-icon-info"
iconColor="red"
:title="$t('Delete?')"
@onConfirm="_delete(scope.row,scope.row.id)"
>
<el-button type="danger" size="mini" icon="el-icon-delete" circle slot="reference"></el-button>
</el-popconfirm>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
name: 'namespace-list',
data () {
return {
list: []
}
},
props: {
namespaceList: Array,
pageNo: Number,
pageSize: Number
},
methods: {
...mapActions('security', ['deleteNamespace']),
_delete (item, i) {
this.deleteNamespace({
id: item.id
}).then(res => {
this.list.splice(i, 1)
this.$message.success(res.msg)
this.$emit('onUpdate')
}).catch(e => {
this.$message.error(e.msg || '')
})
},
_edit (item) {
this.$emit('on-edit', item)
}
},
watch: {
namespaceList (a) {
console.warn(a)
this.list = []
setTimeout(() => {
this.list = a
})
}
},
created () {
this.list = this.namespaceList
},
mounted () {
},
components: { }
}
</script>
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
<template>
<m-list-construction :title="$t('Namespace manage')">
<template slot="conditions">
<m-conditions @on-conditions="_onConditions">
<template slot="button-group" v-if="isADMIN">
<el-button size="mini" @click="_create('')">{{$t('Create namespace')}}</el-button>
<el-dialog
:title="item ? $t('Edit namespace') : $t('Create namespace')"
v-if="createNamespaceDialog"
:visible.sync="createNamespaceDialog"
width="auto">
<m-create-namespace :item="item" @onUpdate="onUpdate" @close="close"></m-create-namespace>
</el-dialog>
</template>
</m-conditions>
</template>
<template slot="content">
<template v-if="namespaceList.length || total>0">
<m-list @on-edit="_onEdit"
@onUpdate="onUpdate"
:namespace-list="namespaceList"
:page-no="searchParams.pageNo"
:page-size="searchParams.pageSize">
</m-list>
<div class="page-box">
<el-pagination
background
@current-change="_page"
@size-change="_pageSize"
:page-size="searchParams.pageSize"
:current-page.sync="searchParams.pageNo"
:page-sizes="[10, 30, 50]"
layout="sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</template>
<template v-if="!namespaceList.length && total<=0">
<m-no-data></m-no-data>
</template>
<m-spin :is-spin="isLoading" :is-left="isLeft"></m-spin>
</template>
</m-list-construction>
</template>
<script>
import _ from 'lodash'
import { mapActions } from 'vuex'
import mList from './_source/list'
import store from '@/conf/home/store'
import mSpin from '@/module/components/spin/spin'
import mCreateNamespace from './_source/createNamespace'
import mNoData from '@/module/components/noData/noData'
import listUrlParamHandle from '@/module/mixin/listUrlParamHandle'
import mConditions from '@/module/components/conditions/conditions'
import mListConstruction from '@/module/components/listConstruction/listConstruction'
export default {
name: 'namespace-index',
data () {
return {
total: null,
isLoading: true,
namespaceList: [],
searchParams: {
pageSize: 10,
pageNo: 1,
searchVal: ''
},
isLeft: true,
isADMIN: store.state.user.userInfo.userType === 'ADMIN_USER',
item: {},
createNamespaceDialog: false
}
},
mixins: [listUrlParamHandle],
props: {},
methods: {
...mapActions('security', ['getNamespaceListP']),
/**
* Query
*/
_onConditions (o) {
this.searchParams = _.assign(this.searchParams, o)
this.searchParams.pageNo = 1
},
_page (val) {
this.searchParams.pageNo = val
},
_pageSize (val) {
this.searchParams.pageSize = val
},
_onEdit (item) {
this._create(item)
},
_create (item) {
this.item = item
this.createNamespaceDialog = true
},
onUpdate () {
this._debounceGET('false')
this.createNamespaceDialog = false
},
close () {
this.createNamespaceDialog = false
},
_getList (flag) {
if (sessionStorage.getItem('isLeft') === '0') {
this.isLeft = false
} else {
this.isLeft = true
}
this.isLoading = !flag
this.getNamespaceListP(this.searchParams).then(res => {
if (this.searchParams.pageNo > 1 && res.totalList.length === 0) {
this.searchParams.pageNo = this.searchParams.pageNo - 1
} else {
this.namespaceList = res.totalList
this.total = res.total
this.isLoading = false
}
}).catch(e => {
this.isLoading = false
})
}
},
watch: {
// router
'$route' (a) {
// url no params get instance list
this.searchParams.pageNo = _.isEmpty(a.query) ? 1 : a.query.pageNo
}
},
created () {
},
mounted () {
},
beforeDestroy () {
sessionStorage.setItem('isLeft', 1)
},
components: { mList, mListConstruction, mConditions, mSpin, mNoData, mCreateNamespace }
}
</script>
......@@ -94,6 +94,14 @@ const security = [
meta: {
title: `${i18n.$t('Token manage')}`
}
},
{
path: '/security/namespace',
name: 'namespace',
component: resolve => require(['../../pages/security/pages/namespace'], resolve),
meta: {
title: `${i18n.$t('K8s Namespace')}`
}
}
]
}
......
......@@ -710,5 +710,68 @@ export default {
reject(e)
})
})
},
/**
* create namespace
*/
createNamespace ({ state }, payload) {
return new Promise((resolve, reject) => {
io.post('k8s-namespace', payload, res => {
resolve(res)
}).catch(e => {
reject(e)
})
})
},
/**
* update namespace
*/
updateNamespace ({ state }, payload) {
return new Promise((resolve, reject) => {
io.put(`k8s-namespace/${payload.id}`, payload, res => {
resolve(res)
}).catch(e => {
reject(e)
})
})
},
/**
* update namespace k8s
*/
verifyNamespaceK8s ({ state }, payload) {
return new Promise((resolve, reject) => {
io.post('k8s-namespace/verify', payload, res => {
resolve(res)
}).catch(e => {
reject(e)
})
})
},
/**
* delete namespace
* @param "id":int
*/
deleteNamespace ({ state }, payload) {
return new Promise((resolve, reject) => {
io.post('k8s-namespace/delete', payload, res => {
resolve(res)
}).catch(e => {
reject(e)
})
})
},
/**
* get namespace list pages
*/
getNamespaceListP ({ state }, payload) {
return new Promise((resolve, reject) => {
io.get('k8s-namespace', payload, res => {
resolve(res.data)
}).catch(e => {
reject(e)
})
})
}
}
......@@ -187,6 +187,16 @@ const menu = {
icon: 'el-icon-document',
children: [],
classNames: 'tab-token-manage'
},
{
name: `${i18n.$t('K8s Namespace')}`,
id: 2,
path: 'namespace',
isOpen: true,
icon: 'el-icon-s-grid',
children: [],
enabled: true,
classNames: 'tab-namespace-manage'
}
],
resource: [
......
......@@ -901,5 +901,19 @@ export default {
Last7DayFluctuation: 'Last7DayFluctuation',
Last30DayFluctuation: 'Last30DayFluctuation',
SrcTableTotalRows: 'SrcTableTotalRows',
TargetTableTotalRows: 'TargetTableTotalRows'
TargetTableTotalRows: 'TargetTableTotalRows',
// k8s
'Create namespace': 'Create namespace',
'Edit namespace': 'Edit namespace',
'Namespace manage': 'K8s namespace manage',
'K8s Namespace': 'k8s Namespace',
'Limits Cpu': 'Limit Cpu',
'Limits Memory': 'Limit Memory',
'K8s Cluster': 'k8s',
'Namespace Owner': 'Owner',
'Please enter k8s cluster': 'Please enter k8s cluster',
'Please enter namespace': 'Please enter namespace',
'Please enter namespace tag': 'Please enter namespace tag can null',
'Please enter owner': 'Please enter owner can null',
'K8s Tag': 'tag'
}
......@@ -901,5 +901,19 @@ export default {
Last7DayFluctuation: '最近7天波动',
Last30DayFluctuation: '最近30天波动',
SrcTableTotalRows: '源表总行数',
TargetTableTotalRows: '目标表总行数'
TargetTableTotalRows: '目标表总行数',
// k8s
'Create namespace': '创建命名空间',
'Edit namespace': '编辑命名空间',
'Namespace manage': 'k8s命名空间 管理',
'K8s Namespace': 'k8s命名空间',
'Limits Cpu': '最大Cpu',
'Limits Memory': '最大内存',
'K8s Cluster': 'k8s集群',
'Namespace Owner': '负责人',
'Please enter k8s cluster': '请输入k8s集群值',
'Please enter namespace': '请输入命名空间',
'Please enter namespace tag': '请输入命名空间标签可空',
'Please enter owner': '请输入owner可空',
'K8s Tag': '标签'
}
......@@ -127,6 +127,7 @@
<error_prone_annotations.version>2.5.1</error_prone_annotations.version>
<exec-maven-plugin.version>3.0.0</exec-maven-plugin.version>
<janino.version>3.1.6</janino.version>
<kubernetes.version>5.8.0</kubernetes.version>
<docker.hub>apache</docker.hub>
<docker.repo>${project.name}</docker.repo>
......@@ -902,6 +903,12 @@
<artifactId>error_prone_annotations</artifactId>
<version>${error_prone_annotations.version}</version>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-client</artifactId>
<version>${kubernetes.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
......
......@@ -237,3 +237,31 @@ xmlbeans-3.1.0.jar
xmlenc-0.52.jar
zookeeper-3.4.14.jar
Java-WebSocket-1.5.1.jar
kubernetes-client-5.8.0.jar
kubernetes-model-admissionregistration-5.8.0.jar
kubernetes-model-apiextensions-5.8.0.jar
kubernetes-model-apps-5.8.0.jar
kubernetes-model-autoscaling-5.8.0.jar
kubernetes-model-batch-5.8.0.jar
kubernetes-model-certificates-5.8.0.jar
kubernetes-model-common-5.8.0.jar
kubernetes-model-coordination-5.8.0.jar
kubernetes-model-core-5.8.0.jar
kubernetes-model-discovery-5.8.0.jar
kubernetes-model-events-5.8.0.jar
kubernetes-model-extensions-5.8.0.jar
kubernetes-model-flowcontrol-5.8.0.jar
kubernetes-model-metrics-5.8.0.jar
kubernetes-model-networking-5.8.0.jar
kubernetes-model-node-5.8.0.jar
kubernetes-model-policy-5.8.0.jar
kubernetes-model-rbac-5.8.0.jar
kubernetes-model-scheduling-5.8.0.jar
kubernetes-model-storageclass-5.8.0.jar
zjsonpatch-0.3.0.jar
automaton-1.11-8.jar
generex-1.0.2.jar
jackson-dataformat-yaml-2.12.5.jar
logging-interceptor-3.14.9.jar
okhttp-3.14.9.jar
okio-1.17.2.jar
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册