ソースを参照

feat: 为学生应用限制资源

370774330@qq.com 5 年 前
コミット
16c2e8558d

+ 7 - 0
api/src/main/java/cn/seecoder/api/DevcloudApiAutoConfiguration.java

@@ -99,6 +99,13 @@ public class DevcloudApiAutoConfiguration {
         return new PodApiImpl(k8sApiConfiguration().coreV1Api());
     }
 
+    @Bean
+    @ConditionalOnBean(K8sApiConfiguration.class)
+    public LimitRangeApi limitRangeApi(){
+        return new LimitRangeApiImpl(k8sApiConfiguration().coreV1Api());
+    }
+
+
     @Bean
     @ConditionalOnBean(K8sApiConfiguration.class)
     public SecretApi secretApi(){

+ 2 - 0
api/src/main/java/cn/seecoder/api/k8s/K8sConstants.java

@@ -56,4 +56,6 @@ public class K8sConstants {
     public final static String EXTERNAL_SERVICE_SUFFIX = "-external-service";
 
     public final static String INGRESS_SUFFIX = "-ingress";
+
+    public final static String LIMIT_RANGE_SUFFIX = "-limit-range";
 }

+ 6 - 0
api/src/main/java/cn/seecoder/api/k8s/LimitRangeApi.java

@@ -0,0 +1,6 @@
+package cn.seecoder.api.k8s;
+
+import cn.seecoder.api.k8s.model.LimitRange;
+
+public interface LimitRangeApi extends AbstractApi<LimitRange>{
+}

+ 165 - 0
api/src/main/java/cn/seecoder/api/k8s/impl/LimitRangeApiImpl.java

@@ -0,0 +1,165 @@
+package cn.seecoder.api.k8s.impl;
+
+import cn.seecoder.api.k8s.LimitRangeApi;
+import cn.seecoder.api.k8s.exception.K8sApiException;
+import cn.seecoder.api.k8s.model.K8sObjectRequest;
+import cn.seecoder.api.k8s.model.LabelSelector;
+import cn.seecoder.api.k8s.model.LimitRange;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.apis.CoreV1Api;
+import io.kubernetes.client.openapi.models.V1DeleteOptions;
+import io.kubernetes.client.openapi.models.V1DeleteOptionsBuilder;
+import io.kubernetes.client.openapi.models.V1LimitRange;
+import io.kubernetes.client.openapi.models.V1LimitRangeList;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static cn.seecoder.api.k8s.K8sConstants.FOREGROUND_PROPAGATION_POLICY;
+import static cn.seecoder.api.k8s.K8sConstants.PRETTY_FORMAT;
+import static cn.seecoder.api.k8s.exception.K8sApiException.*;
+
+@Service
+@Slf4j
+public class LimitRangeApiImpl implements LimitRangeApi {
+
+    private final CoreV1Api coreV1Api;
+
+    @Autowired
+    public LimitRangeApiImpl(CoreV1Api coreV1Api) {
+        this.coreV1Api = coreV1Api;
+    }
+
+    @Override
+    public LimitRange create(LimitRange limitRange) {
+        try {
+            V1LimitRange v1LimitRange = limitRange.toK8sObject();
+            v1LimitRange = coreV1Api.createNamespacedLimitRange(limitRange.getNamespace(), v1LimitRange, PRETTY_FORMAT, null, null);
+            return v1LimitRange == null ? null : new LimitRange(v1LimitRange);
+        } catch (ApiException e) {
+            log.error("limit range创建失败, limit range={}, response={}", limitRange, e.getResponseBody());
+            if (e.getCode() == ALREADY_EXIST) {
+                throw new K8sApiException(ALREADY_EXIST, "limit range已经存在");
+            }else if (e.getCode() == NOT_FOUND){
+                throw new K8sApiException(NOT_FOUND, "Namespace不存在");
+            }else {
+                throw K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        }
+    }
+
+    @Override
+    public LimitRange update(LimitRange limitRange) {
+
+        try {
+            V1LimitRange v1LimitRange = getV1LimitRange(limitRange.getNamespace(), limitRange.getName());
+            limitRange.merge(v1LimitRange);
+            v1LimitRange = coreV1Api.replaceNamespacedLimitRange(limitRange.getName(), limitRange.getNamespace(), v1LimitRange, PRETTY_FORMAT, null, null);
+            return v1LimitRange == null ? null : new LimitRange(v1LimitRange);
+        } catch (ApiException e) {
+            log.error("LimitRange替换失败, limit range={}, response={}", limitRange, e.getResponseBody());
+            if (e.getCode() == NOT_FOUND){
+                throw new K8sApiException(NOT_FOUND, "LimitRange不存在");
+            }else {
+                throw K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        } catch (Exception e) {
+            log.error("LimitRange替换异常, limit range={}", limitRange);
+            throw K8s_SYSTEM_ERROR_EXCEPTION;
+        }
+    }
+
+    @Override
+    public void delete(LimitRange limitRange) {
+        try {
+            V1DeleteOptions v1DeleteOptions = new V1DeleteOptionsBuilder()
+                    .withApiVersion(LimitRange.API_VERSION)
+                    .withPropagationPolicy(FOREGROUND_PROPAGATION_POLICY)
+                    .build();
+            coreV1Api.deleteNamespacedService(
+                    limitRange.getName(),
+                    limitRange.getNamespace(),
+                    PRETTY_FORMAT,
+                    null,
+                    null,
+                    null,
+                    FOREGROUND_PROPAGATION_POLICY,
+                    v1DeleteOptions);
+        }catch (ApiException e) {
+            log.error("LimitRange删除失败,namespace={}, name={}, response={}",
+                    limitRange.getNamespace(), limitRange.getName(), e.getResponseBody());
+            if (e.getCode() == NOT_FOUND) {
+                return;
+            }else {
+                return;
+            }
+        }catch (Exception e) {
+            log.error("LimitRange删除异常,namespace={}, name={}, response={}", limitRange.getNamespace(), limitRange.getName(),e.getMessage());
+        }
+    }
+
+    @Override
+    public List<LimitRange> getByCondition(K8sObjectRequest request) {
+        try {
+            if (request.getName() != null) {
+                V1LimitRange obj = coreV1Api.readNamespacedLimitRange(
+                        request.getName(),
+                        request.getNamespace(),
+                        PRETTY_FORMAT,
+                        null,
+                        null);
+                return obj == null ? Collections.emptyList() : Collections.singletonList(new LimitRange(obj));
+            }
+            LabelSelector selector = null;
+            if (request.getSelector() != null) {
+                selector = request.getSelector();
+            }
+            if (request.getLabels() != null) {
+                if (selector == null) {
+                    selector = new LabelSelector();
+                }
+                selector.addAll(request.getLabels());
+            }
+            V1LimitRangeList objList = coreV1Api.listNamespacedLimitRange(
+                    request.getNamespace(),
+                    PRETTY_FORMAT,
+                    null,
+                    null,
+                    null,
+                    selector.toJsonString(),
+                    null,
+                    null,
+                    null,
+                    null);
+            return objList.getItems().stream().map(LimitRange::new).collect(Collectors.toList());
+        } catch (ApiException e) {
+            log.error("查找pod失败,namespace = {}, response={}",
+                    request.getNamespace(), e.getResponseBody());
+            if (e.getCode() == NOT_FOUND) {
+                return Collections.emptyList();
+            }else {
+                return Collections.emptyList();
+            }
+        } catch (Exception e) {
+            log.error("查找pod失败,namespace = {}, name={}",
+                    request.getNamespace(),request.getName());
+            return Collections.emptyList();
+        }
+
+
+    }
+
+
+    private V1LimitRange getV1LimitRange(String namespace, String name) throws ApiException {
+        return coreV1Api.readNamespacedLimitRange(
+                name,
+                namespace,
+                PRETTY_FORMAT,
+                null,
+                null);
+    }
+}

+ 86 - 0
api/src/main/java/cn/seecoder/api/k8s/model/LimitRange.java

@@ -0,0 +1,86 @@
+package cn.seecoder.api.k8s.model;
+
+import io.kubernetes.client.custom.Quantity;
+import io.kubernetes.client.openapi.models.*;
+import lombok.*;
+
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper=true)
+@Builder
+@AllArgsConstructor
+public class LimitRange extends K8sAbstractObject<V1LimitRange>{
+
+    public static final String API_VERSION = "v1";
+
+    private static final String KIND = "LimitRange";
+
+    private String name;
+
+    private String namespace;
+
+    @Builder.Default
+    String defaultCpu = "100m";
+
+    @Builder.Default
+    String defaultMemory = "768Mi";
+
+    @Builder.Default
+    String defaultRequestCpu = "50m";
+
+    @Builder.Default
+    String defaultRequestMemory = "512Mi";
+
+    @Builder.Default
+    String minCpu = "25m";
+
+    @Builder.Default
+    String minMemory = "256Mi";
+
+    @Builder.Default
+    String maxCpu = "100m";
+
+    @Builder.Default
+    String maxMemory = "768Mi";
+
+    @Builder.Default
+    String type = "Container";
+
+    public LimitRange(V1LimitRange v1LimitRange){
+        super(v1LimitRange);
+
+    }
+
+    private V1LimitRangeSpec toV1LimitRangeSpec() {
+        V1LimitRangeItem item = new V1LimitRangeItemBuilder()
+                .addToDefault("cpu",Quantity.fromString(defaultCpu))
+                .addToDefault("memory",Quantity.fromString(defaultMemory))
+                .addToDefaultRequest("cpu",Quantity.fromString(defaultRequestCpu))
+                .addToDefaultRequest("memory",Quantity.fromString(defaultRequestMemory))
+                .addToMax("cpu",Quantity.fromString(maxCpu))
+                .addToMax("memory",Quantity.fromString(maxMemory))
+                .addToMin("cpu",Quantity.fromString(minCpu))
+                .addToMin("memory", Quantity.fromString(minMemory))
+                .withType(type)
+                .build();
+        V1LimitRangeSpec limitRangeSpec = new V1LimitRangeSpec();
+        limitRangeSpec.addLimitsItem(item);
+        return limitRangeSpec;
+    }
+
+    @Override
+    public V1LimitRange toK8sObject() {
+        return new V1LimitRangeBuilder()
+                .withKind(getKind() == null ? KIND : getKind())
+                .withMetadata(this.toV1ObjectMeta())
+                .withApiVersion(getApiVersion() == null ? API_VERSION : getApiVersion())
+                .withSpec(toV1LimitRangeSpec())
+                .build();
+    }
+
+    public void merge(V1LimitRange v1LimitRange) {
+        v1LimitRange.getMetadata().setNamespace(this.namespace);
+        v1LimitRange.getMetadata().setName(this.name);
+        v1LimitRange.setSpec(toV1LimitRangeSpec());
+    }
+}

+ 2 - 0
web/src/main/java/cn/seecoder/web/core/pipeline/PipelineException.java

@@ -16,6 +16,8 @@ public class PipelineException extends Exception{
     public static final String INGRESS_CREATE_ERROR = "K8S ingress 创建错误";
     public static final String SERVICE_CREATE_ERROR = "K8S service 创建错误";
     public static final String DEPLOYMENT_CREATE_ERROR = "K8S deployment 创建错误";
+    public static final String LIMIT_RANGE_CREATE_ERROR = "K8S limit range 创建错误";
+
     public static final String NAMESPACE_CREATE_ERROR = "K8S namespace 创建错误";
     public static final String CONFIG_JSON_ERROR = "获取模板的config json错误";
     public static final String DOCKERFILE_MODIFY_ERROR = "修改模板的dockerfile错误";

+ 28 - 1
web/src/main/java/cn/seecoder/web/core/pipeline/handler/DeployHandler.java

@@ -34,9 +34,12 @@ public class DeployHandler extends AbstractHandler{
 
     private final IngressApi ingressApi;
 
+    private final LimitRangeApi limitRangeApi;
+
     private final static String DEFAULT_IMAGE_PULL_SECRET_NAME = "seecoder-devcloud-image-pull-secret";
 
     public DeployHandler(){
+        this.limitRangeApi = SpringUtil.getBean(LimitRangeApi.class);
         this.applicationProperties = SpringUtil.getBean(ApplicationProperties.class);
         secretApi = SpringUtil.getBean(SecretApi.class);
         deploymentApi = SpringUtil.getBean(DeploymentApi.class);
@@ -50,7 +53,7 @@ public class DeployHandler extends AbstractHandler{
     @Override
     public void process(Context context) throws PipelineException {
 
-        //1. 创建namespace
+        //1. 创建namespace 和 limit range
         Namespace k8sNamespace = new Namespace();
         k8sNamespace.setName(context.getNamespace());
         //todo namespace在底下资源未删除时,不能轻易删除重建,只能直接通过异常查是否已创建,丑陋,待修改
@@ -69,6 +72,30 @@ public class DeployHandler extends AbstractHandler{
         }
         context.appendSuccessResult("K8s Namespace创建成功: [namespace: " + context.getNamespace() + ", deployName: " + context.getDeployName() + "]");
 
+        LimitRange limitRange = new LimitRange();
+        limitRange.setNamespace(context.getNamespace());
+        limitRange.setName(context.getDeployName()+K8sConstants.LIMIT_RANGE_SUFFIX);
+//        limitRange.setMinCpu("100m");
+//        limitRange.setMaxCpu("2000m");
+//        limitRange.setMinMemory("512Mi");
+//        limitRange.setMaxMemory("1024Mi");
+        try {
+            List<LimitRange> limitRanges = limitRangeApi.getByCondition(K8sObjectRequest.builder()
+                    .namespace(limitRange.getNamespace()).name(limitRange.getName()).build());
+            if (limitRanges.size() == 0) {
+                limitRangeApi.create(limitRange);
+                log.info("limitRange创建成功: [namespace: " + context.getNamespace() + ", deployName: " + context.getDeployName() + "]");
+            } else {
+                limitRangeApi.update(limitRange);
+                log.info("limitRange更新成功: [namespace: " + context.getNamespace() + ", deployName: " + context.getDeployName() + "]");
+
+            }
+        } catch (K8sApiException e) {
+            context.appendErrorResult(PipelineException.LIMIT_RANGE_CREATE_ERROR, e);
+            throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.LIMIT_RANGE_CREATE_ERROR, e);
+        }
+        context.appendSuccessResult("K8s limit range创建成功: [namespace: " + context.getNamespace() + ", deployName: " + context.getDeployName() + "]");
+
 
         //2. 创建deployment
         //2.0. 镜像配置信息获取

+ 30 - 0
web/src/main/java/cn/seecoder/web/core/pipeline/handler/MysqlDeployHandler.java

@@ -39,6 +39,8 @@ public class MysqlDeployHandler extends AbstractHandler{
 
     private final ServiceApi serviceApi;
 
+    private final LimitRangeApi limitRangeApi;
+
     private final static String DEFAULT_IMAGE_PULL_SECRET_NAME = "seecoder-devcloud-image-pull-secret";
 
     public MysqlDeployHandler(){
@@ -47,6 +49,7 @@ public class MysqlDeployHandler extends AbstractHandler{
         deploymentApi = SpringUtil.getBean(DeploymentApi.class);
         namespaceApi = SpringUtil.getBean(NamespaceApi.class);
         serviceApi = SpringUtil.getBean(ServiceApi.class);
+        limitRangeApi = SpringUtil.getBean(LimitRangeApi.class);
     }
 
     private String version = "5.7";
@@ -77,6 +80,31 @@ public class MysqlDeployHandler extends AbstractHandler{
         }
         context.appendSuccessResult("K8s Namespace创建成功: [namespace: " + context.getNamespace() + ", deployName: " + context.getDeployName() + "]");
 
+        LimitRange limitRange = new LimitRange();
+        limitRange.setNamespace(context.getNamespace());
+        limitRange.setName(context.getDeployName()+K8sConstants.LIMIT_RANGE_SUFFIX);
+//        limitRange.setMinCpu("100m");
+//        limitRange.setMaxCpu("2000m");
+//        limitRange.setMinMemory("512Mi");
+//        limitRange.setMaxMemory("1024Mi");
+        try {
+            List<LimitRange> limitRanges = limitRangeApi.getByCondition(K8sObjectRequest.builder()
+                    .namespace(limitRange.getNamespace()).name(limitRange.getName()).build());
+            if (limitRanges.size() == 0) {
+                limitRangeApi.create(limitRange);
+                log.info("limitRange创建成功: [namespace: " + context.getNamespace() + ", deployName: " + context.getDeployName() + "]");
+            } else {
+                limitRangeApi.update(limitRange);
+                log.info("limitRange更新成功: [namespace: " + context.getNamespace() + ", deployName: " + context.getDeployName() + "]");
+
+            }
+        } catch (K8sApiException e) {
+            context.appendErrorResult(PipelineException.LIMIT_RANGE_CREATE_ERROR, e);
+            throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.LIMIT_RANGE_CREATE_ERROR, e);
+        }
+        context.appendSuccessResult("K8s limit range创建成功: [namespace: " + context.getNamespace() + ", deployName: " + context.getDeployName() + "]");
+
+
 
         //2. 创建deployment
         //2.0. 镜像配置信息获取
@@ -140,6 +168,8 @@ public class MysqlDeployHandler extends AbstractHandler{
             context.appendErrorResult(PipelineException.DEPLOYMENT_CREATE_ERROR, e);
             throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.DEPLOYMENT_CREATE_ERROR, e);
         }
+        context.appendSuccessResult("K8s deployment创建成功: [namespace: " + context.getNamespace() + ", deployName: " + context.getDeployName() + "]");
+
 
         //3. 创建service
         //3.1. 配置K8s的service

+ 3 - 0
web/src/main/java/cn/seecoder/web/service/impl/pipeline/DeploymentServiceImpl.java

@@ -10,6 +10,7 @@ import cn.seecoder.web.model.po.pipeline.PipelineRecordPO;
 import cn.seecoder.web.model.po.pipeline.PipelinePO;
 import cn.seecoder.web.model.po.project.ProjectPO;
 import io.kubernetes.client.openapi.models.V1DeploymentStatus;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.http.HttpStatus;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Async;
@@ -38,6 +39,7 @@ import static cn.seecoder.web.core.pipeline.template.PipelineTemplateTable.MYSQL
  * @description:
  */
 @Service
+@Slf4j
 public class DeploymentServiceImpl implements DeploymentService {
 
     private final PipelineMapper pipelineMapper;
@@ -147,6 +149,7 @@ public class DeploymentServiceImpl implements DeploymentService {
         } catch (PipelineException e) {
             record.setResult(PipelineRecordPO.DEPLOY_FAIL);
             record.setDetails(pipeline.getContext().getResult());
+            log.error("流水线配置转换发生错误");
             throw new ServiceException(HttpStatus.SC_INTERNAL_SERVER_ERROR,"应用部署失败",e);
         } catch (RuntimeException e){
             pipeline.getContext().appendErrorResult("运行时错误: ", e);