1
0
wanghongkai 5 лет назад
Родитель
Сommit
d9191120ca
23 измененных файлов с 571 добавлено и 60 удалено
  1. 19 0
      k8s-paas-init.yml
  2. 8 1
      src/main/java/cn/seecoder/paas/service/ApplicationService.java
  3. 1 1
      src/main/java/cn/seecoder/paas/service/EnvironmentService.java
  4. 6 0
      src/main/java/cn/seecoder/paas/service/facade/k8s/PersistentVolumeClaimApi.java
  5. 3 3
      src/main/java/cn/seecoder/paas/service/facade/k8s/impl/ConfigMapApiImpl.java
  6. 3 3
      src/main/java/cn/seecoder/paas/service/facade/k8s/impl/DeploymentApiImpl.java
  7. 3 3
      src/main/java/cn/seecoder/paas/service/facade/k8s/impl/IngressApiImpl.java
  8. 3 3
      src/main/java/cn/seecoder/paas/service/facade/k8s/impl/NamespaceApiImpl.java
  9. 179 0
      src/main/java/cn/seecoder/paas/service/facade/k8s/impl/PersistentVolumeClaimImpl.java
  10. 3 3
      src/main/java/cn/seecoder/paas/service/facade/k8s/impl/PodApiImpl.java
  11. 2 2
      src/main/java/cn/seecoder/paas/service/facade/k8s/impl/SecretApiImpl.java
  12. 2 2
      src/main/java/cn/seecoder/paas/service/facade/k8s/impl/ServiceApiImpl.java
  13. 2 0
      src/main/java/cn/seecoder/paas/service/facade/k8s/model/K8sAbstractObject.java
  14. 84 0
      src/main/java/cn/seecoder/paas/service/facade/k8s/model/PersistentVolumeClaim.java
  15. 111 2
      src/main/java/cn/seecoder/paas/service/impl/ApplicationServiceImpl.java
  16. 83 33
      src/main/java/cn/seecoder/paas/service/impl/EnvironmentServiceImpl.java
  17. 22 0
      src/main/java/cn/seecoder/paas/service/model/vo/AppVolumeVO.java
  18. 4 0
      src/main/java/cn/seecoder/paas/service/model/vo/ApplicationVO.java
  19. 8 1
      src/main/java/cn/seecoder/paas/service/model/vo/ConfigVO.java
  20. 2 0
      src/main/java/cn/seecoder/paas/util/ApplicationProperties.java
  21. 20 1
      src/main/java/cn/seecoder/paas/web/controller/api/ApplicationController.java
  22. 1 1
      src/main/java/cn/seecoder/paas/web/controller/api/EnvironmentController.java
  23. 2 1
      src/main/resources/application.yml

+ 19 - 0
k8s-paas-init.yml

@@ -0,0 +1,19 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: seec
+
+---
+
+apiVersion: storage.k8s.io/v1beta1
+kind: StorageClass
+metadata:
+  name: seecoder-paas
+provisioner: kubernetes.io/glusterfs
+reclaimPolicy: Retain
+allowVolumeExpansion: true
+parameters:
+  resturl: "http://10.43.40.223:8080" #glusterfs service url
+  restauthenabled: "true"
+  restuser: "admin"
+  restuserkey: "admin"

+ 8 - 1
src/main/java/cn/seecoder/paas/service/ApplicationService.java

@@ -1,5 +1,6 @@
 package cn.seecoder.paas.service;
 
+import cn.seecoder.paas.service.model.vo.AppVolumeVO;
 import cn.seecoder.paas.service.model.vo.ApplicationVO;
 import cn.seecoder.paas.util.ServiceException;
 
@@ -9,9 +10,15 @@ public interface ApplicationService {
 
     ApplicationVO createOrUpdate(ApplicationVO vo) throws ServiceException;
 
-    void delete(Integer id);
+    void delete(Integer id) throws ServiceException;
 
     List<ApplicationVO> getAll();
 
     ApplicationVO getDetailById(Integer id) throws ServiceException;
+
+    void createAppVolume(AppVolumeVO appVolumeVO) throws ServiceException;
+
+    void updateAppVolume(AppVolumeVO appVolumeVO) throws ServiceException;
+
+    void deleteAppVolume(AppVolumeVO appVolumeVO) throws ServiceException;
 }

+ 1 - 1
src/main/java/cn/seecoder/paas/service/EnvironmentService.java

@@ -10,7 +10,7 @@ public interface EnvironmentService {
 
     EnvironmentVO createOrUpdate(EnvironmentVO environmentVO) throws ServiceException;
 
-    void delete(Integer id);
+    void delete(Integer id) throws ServiceException;
 
     EnvironmentVO get(Integer id, boolean fetchAll) throws ServiceException;
 

+ 6 - 0
src/main/java/cn/seecoder/paas/service/facade/k8s/PersistentVolumeClaimApi.java

@@ -0,0 +1,6 @@
+package cn.seecoder.paas.service.facade.k8s;
+
+import cn.seecoder.paas.service.facade.k8s.model.PersistentVolumeClaim;
+
+public interface PersistentVolumeClaimApi extends AbstractApi<PersistentVolumeClaim> {
+}

+ 3 - 3
src/main/java/cn/seecoder/paas/service/facade/k8s/impl/ConfigMapApiImpl.java

@@ -99,15 +99,15 @@ public class ConfigMapApiImpl implements ConfigMapApi {
             LoggerUtil.error(logger, e, "删除ConfigMap失败,namespace={}, name={}, response={}", configMap.getNamespace(), configMap.getName(), e.getResponseBody());
 
             if (e.getCode() == K8sApiException.NOT_FOUND) {
-                throw new K8sApiException(NOT_FOUND, "configMap不存在");
+                return;
             }else {
-                throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+                return;
             }
         } catch (JsonSyntaxException e) {
             //do nothing
         } catch (Exception e) {
             LoggerUtil.error(logger, e, "删除ConfigMap异常,namespace={}, name={}", configMap.getNamespace(), configMap.getName());
-            throw K8s_SYSTEM_ERROR_EXCEPTION;
+            return;
         }
     }
 

+ 3 - 3
src/main/java/cn/seecoder/paas/service/facade/k8s/impl/DeploymentApiImpl.java

@@ -125,15 +125,15 @@ public class DeploymentApiImpl implements DeploymentApi {
             LoggerUtil.error(logger, e, "删除Deployment失败,namespace={}, name={}, response={}", deployment.getNamespace(), deployment.getName(), e.getResponseBody());
 
             if (e.getCode() == K8sApiException.NOT_FOUND) {
-                throw new K8sApiException(NOT_FOUND, "deployment不存在");
+                return;
             }else {
-                throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+                return;
             }
         } catch (JsonSyntaxException e) {
             //do nothing
         } catch (Exception e) {
             LoggerUtil.error(logger, e, "删除Deployment异常,namespace={}, name={}", deployment.getNamespace(), deployment.getName());
-            throw K8s_SYSTEM_ERROR_EXCEPTION;
+            return;
         }
     }
 

+ 3 - 3
src/main/java/cn/seecoder/paas/service/facade/k8s/impl/IngressApiImpl.java

@@ -126,15 +126,15 @@ public class IngressApiImpl implements IngressApi {
         }catch (ApiException e) {
             LoggerUtil.error(logger, e, "ingress 删除失败,name={}, namespace={}, response={}", ingress.getName(), ingress.getNamespace(), e.getResponseBody());
             if (e.getCode() == NOT_FOUND) {
-                throw new K8sApiException(NOT_FOUND, "ingress不存在");
+                return;
             }else {
-                throw K8s_SYSTEM_ERROR_EXCEPTION;
+                return;
             }
         }catch (JsonSyntaxException e) {
             //do nothing
         }catch (Exception e) {
             LoggerUtil.error(logger, e, "ingress 删除异常,name={}, namespace={}", ingress.getName(), ingress.getNamespace());
-            throw e;
+            return;
         }
     }
 

+ 3 - 3
src/main/java/cn/seecoder/paas/service/facade/k8s/impl/NamespaceApiImpl.java

@@ -86,15 +86,15 @@ public class NamespaceApiImpl implements NamespaceApi {
         } catch (ApiException e) {
             LoggerUtil.error(logger, e, "Namespace删除失败,name={}, response={}", namespace.getName() , e.getResponseBody());
             if (e.getCode() == NOT_FOUND) {
-                throw K8s_NAMESPACE_NOT_EXIST;
+                return;
             }else {
-                throw K8s_SYSTEM_ERROR_EXCEPTION;
+                return;
             }
         } catch (JsonSyntaxException e){
             //do nothing
         } catch (Exception e) {
             LoggerUtil.error(logger, e, "Namespace删除异常, name={}", namespace.getName());
-            throw K8s_SYSTEM_ERROR_EXCEPTION;
+            return;
         }
     }
 

+ 179 - 0
src/main/java/cn/seecoder/paas/service/facade/k8s/impl/PersistentVolumeClaimImpl.java

@@ -0,0 +1,179 @@
+package cn.seecoder.paas.service.facade.k8s.impl;
+
+import cn.seecoder.paas.service.facade.k8s.DeploymentApi;
+import cn.seecoder.paas.service.facade.k8s.PersistentVolumeClaimApi;
+import cn.seecoder.paas.service.facade.k8s.exception.K8sApiException;
+import cn.seecoder.paas.service.facade.k8s.model.K8sObjectRequest;
+import cn.seecoder.paas.service.facade.k8s.model.LabelSelector;
+import cn.seecoder.paas.service.facade.k8s.model.PersistentVolumeClaim;
+import cn.seecoder.paas.util.LoggerUtil;
+import com.google.gson.JsonSyntaxException;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.apis.CoreV1Api;
+import io.kubernetes.client.openapi.models.*;
+import org.slf4j.Logger;
+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.paas.service.facade.k8s.K8sConstants.FOREGROUND_PROPAGATION_POLICY;
+import static cn.seecoder.paas.service.facade.k8s.K8sConstants.PRETTY_FORMAT;
+import static cn.seecoder.paas.service.facade.k8s.exception.K8sApiException.ALREADY_EXIST;
+import static cn.seecoder.paas.service.facade.k8s.exception.K8sApiException.NOT_FOUND;
+
+@Service
+public class PersistentVolumeClaimImpl implements PersistentVolumeClaimApi {
+
+    private static final Logger logger = LoggerUtil.getLogger(DeploymentApi.class);
+
+    private final CoreV1Api coreV1Api;
+
+    @Autowired
+    public PersistentVolumeClaimImpl(CoreV1Api coreV1Api) {
+        this.coreV1Api = coreV1Api;
+    }
+
+    @Override
+    public PersistentVolumeClaim create(PersistentVolumeClaim persistentVolumeClaim) {
+        try {
+            V1PersistentVolumeClaim v1PersistentVolumeClaim = persistentVolumeClaim.toK8sObject();
+            V1PersistentVolumeClaim result = coreV1Api.createNamespacedPersistentVolumeClaim(
+                    persistentVolumeClaim.getNamespace(),
+                    v1PersistentVolumeClaim,
+                    PRETTY_FORMAT,
+                    null,
+                    null);
+            return result == null ? null : new PersistentVolumeClaim(result);
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "pvc 创建失败, persistentVolumeClaim={}, response={}", persistentVolumeClaim, e.getResponseBody());
+
+            if (e.getCode() == NOT_FOUND) {
+                throw new K8sApiException(NOT_FOUND, "namespace不存在");
+            } else if (e.getCode() == ALREADY_EXIST) {
+                throw new K8sApiException(ALREADY_EXIST, "persistentVolumeClaim已经存在");
+            } else {
+                throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "pvc 创建异常, persistentVolumeClaim={}", persistentVolumeClaim);
+            throw e;
+        }
+    }
+
+
+    @Override
+    public PersistentVolumeClaim update(PersistentVolumeClaim persistentVolumeClaim) {
+        try {
+            V1PersistentVolumeClaim v1PersistentVolumeClaim = getV1PersistenceVolumeClaim(persistentVolumeClaim.getNamespace(), persistentVolumeClaim.getName());
+            persistentVolumeClaim.merge(v1PersistentVolumeClaim);
+            V1PersistentVolumeClaim result = coreV1Api.replaceNamespacedPersistentVolumeClaim(persistentVolumeClaim.getName(), persistentVolumeClaim.getNamespace(), v1PersistentVolumeClaim, PRETTY_FORMAT, null, null);
+            return result == null ? null : new PersistentVolumeClaim(result);
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "persistentVolumeClaim 更新失败, persistentVolumeClaim={}, response={}", persistentVolumeClaim, e.getResponseBody());
+
+            if (e.getCode() == NOT_FOUND) {
+                throw new K8sApiException(NOT_FOUND, "namespace不存在");
+            } else if (e.getCode() == ALREADY_EXIST) {
+                throw new K8sApiException(ALREADY_EXIST, "persistentVolumeClaim 已经存在");
+            } else {
+                throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "persistentVolumeClaim 更新异常, persistentVolumeClaim={}", persistentVolumeClaim);
+            throw e;
+        }
+    }
+
+    @Override
+    public void delete(PersistentVolumeClaim persistentVolumeClaim) {
+        try {
+            V1DeleteOptions v1DeleteOptions = new V1DeleteOptionsBuilder()
+                    .withApiVersion(PersistentVolumeClaim.API_VERSION)
+                    .withPropagationPolicy(FOREGROUND_PROPAGATION_POLICY)
+                    .build();
+            coreV1Api.deleteNamespacedPersistentVolumeClaim(
+                    persistentVolumeClaim.getName(),
+                    persistentVolumeClaim.getNamespace(),
+                    PRETTY_FORMAT,
+                    null,
+                    null,
+                    null,
+                    FOREGROUND_PROPAGATION_POLICY,
+                    v1DeleteOptions);
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "删除 PersistentVolumeClaim 失败,namespace={}, name={}, response={}", persistentVolumeClaim.getNamespace(), persistentVolumeClaim.getName(), e.getResponseBody());
+
+            if (e.getCode() == K8sApiException.NOT_FOUND) {
+                return;
+            }else {
+                return;
+            }
+        } catch (JsonSyntaxException e) {
+            //do nothing
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "删除 PersistentVolumeClaim 异常,namespace={}, name={}", persistentVolumeClaim.getNamespace(), persistentVolumeClaim.getName());
+            return;
+        }
+    }
+
+    @Override
+    public List<PersistentVolumeClaim> getByCondition(K8sObjectRequest request) {
+        try {
+            if (request.getName() != null) {
+                V1PersistentVolumeClaim obj = coreV1Api.readNamespacedPersistentVolumeClaim(
+                        request.getName(),
+                        request.getNamespace(),
+                        PRETTY_FORMAT,
+                        null,
+                        null);
+                return obj == null ? Collections.emptyList() : Collections.singletonList(new PersistentVolumeClaim(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());
+            }
+            V1PersistentVolumeClaimList objList = coreV1Api.listNamespacedPersistentVolumeClaim(
+                    request.getNamespace(),
+                    PRETTY_FORMAT,
+                    null,
+                    null,
+                    null,
+                    selector.toJsonString(),
+                    null,
+                    null,
+                    null,
+                    null);
+            return objList.getItems().stream().map(PersistentVolumeClaim::new).collect(Collectors.toList());
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "查找 persistentVolumeClaim 失败,namespace = {}, response={}",
+                    request.getNamespace(), e.getResponseBody());
+            if (e.getCode() == NOT_FOUND) {
+                return Collections.emptyList();
+            }else {
+                return Collections.emptyList();
+            }
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "查找 persistentVolumeClaim 失败,namespace = {}, name={}",
+                    request.getNamespace());
+            return Collections.emptyList();
+        }
+    }
+
+    private V1PersistentVolumeClaim getV1PersistenceVolumeClaim(String namespace, String name) throws ApiException {
+        return coreV1Api.readNamespacedPersistentVolumeClaim(
+                name,
+                namespace,
+                PRETTY_FORMAT,
+                false,
+                false);
+    }
+}

+ 3 - 3
src/main/java/cn/seecoder/paas/service/facade/k8s/impl/PodApiImpl.java

@@ -71,15 +71,15 @@ public class PodApiImpl implements PodApi {
         } catch (ApiException e) {
             LoggerUtil.error(logger, e, "Pod删除失败,name={}, response={}", pod.getName() , e.getResponseBody());
             if (e.getCode() == NOT_FOUND) {
-                throw K8s_NAMESPACE_NOT_EXIST;
+                return;
             }else {
-                throw K8s_SYSTEM_ERROR_EXCEPTION;
+                return;
             }
         } catch (JsonSyntaxException e){
             //do nothing
         } catch (Exception e) {
             LoggerUtil.error(logger, e, "Pod删除异常, name={}", pod.getName());
-            throw K8s_SYSTEM_ERROR_EXCEPTION;
+            return;
         }
     }
 

+ 2 - 2
src/main/java/cn/seecoder/paas/service/facade/k8s/impl/SecretApiImpl.java

@@ -139,9 +139,9 @@ public class SecretApiImpl implements SecretApi {
             V1Status v1Status = coreV1Api.deleteNamespacedSecret(name, namespace, PRETTY_FORMAT, null, 5, false, "", body);
         } catch (ApiException e) {
             if (e.getCode() == K8sApiException.NOT_FOUND) {
-                throw new K8sApiException(K8sApiException.NOT_FOUND);
+                return;
             } else {
-                throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+                return;
             }
         }
     }

+ 2 - 2
src/main/java/cn/seecoder/paas/service/facade/k8s/impl/ServiceApiImpl.java

@@ -103,9 +103,9 @@ public class ServiceApiImpl implements ServiceApi {
             LoggerUtil.error(logger, e, "Service删除失败,namespace={}, name={}, response={}",
                     service.getNamespace(), service.getName(), e.getResponseBody());
             if (e.getCode() == NOT_FOUND) {
-                throw new K8sApiException(K8sApiException.NOT_FOUND, "Service不存在");
+                return;
             }else {
-                throw K8s_SYSTEM_ERROR_EXCEPTION;
+                return;
             }
         }catch (Exception e) {
             LoggerUtil.error(logger, e, "Service删除异常,namespace={}, name={}, response={}", service.getNamespace(), service.getName());

+ 2 - 0
src/main/java/cn/seecoder/paas/service/facade/k8s/model/K8sAbstractObject.java

@@ -3,12 +3,14 @@ package cn.seecoder.paas.service.facade.k8s.model;
 import io.kubernetes.client.common.KubernetesObject;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import lombok.ToString;
 
 import java.util.HashMap;
 import java.util.Map;
 
 @NoArgsConstructor
 @Data
+@ToString
 public abstract class K8sAbstractObject<T extends KubernetesObject> implements K8sObject, K8sObjectTransformer<T> {
 
     private String apiVersion;

+ 84 - 0
src/main/java/cn/seecoder/paas/service/facade/k8s/model/PersistentVolumeClaim.java

@@ -0,0 +1,84 @@
+package cn.seecoder.paas.service.facade.k8s.model;
+
+import io.kubernetes.client.custom.Quantity;
+import io.kubernetes.client.openapi.models.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class PersistentVolumeClaim extends K8sAbstractObject<V1PersistentVolumeClaim> {
+
+    public static final String API_VERSION = "v1";
+
+    public static final String KIND = "PersistentVolumeClaim";
+
+    private String storageClassName;
+
+    private String volumeMode;
+
+    private List<String> accessModes = new ArrayList<>();
+
+    private Quantity quantity;
+
+    private static final Quantity DEFAULT_QUANTITY = Quantity.fromString("1Gi");
+
+    private static final String STORAGE_KEY = "storage";
+
+    public PersistentVolumeClaim(V1PersistentVolumeClaim v1PersistentVolumeClaim) {
+        super(v1PersistentVolumeClaim);
+        accessModes = v1PersistentVolumeClaim.getSpec().getAccessModes();
+        storageClassName = v1PersistentVolumeClaim.getSpec().getStorageClassName();
+        volumeMode = v1PersistentVolumeClaim.getSpec().getVolumeMode();
+        if (v1PersistentVolumeClaim.getSpec().getResources() != null && v1PersistentVolumeClaim.getSpec().getResources().getRequests() != null) {
+            quantity = v1PersistentVolumeClaim.getSpec().getResources().getRequests().get(STORAGE_KEY);
+        }
+    }
+
+    public void merge(V1PersistentVolumeClaim v1PersistentVolumeClaim) {
+        v1PersistentVolumeClaim.getMetadata().setName(getName());
+        v1PersistentVolumeClaim.getMetadata().setNamespace(this.getNamespace());
+        v1PersistentVolumeClaim.getMetadata().setLabels(this.getLabels());
+        v1PersistentVolumeClaim.getMetadata().setAnnotations(this.getAnnotations());
+        v1PersistentVolumeClaim.setSpec(
+                new V1PersistentVolumeClaimSpecBuilder()
+                        .withAccessModes(accessModes)
+                        .withVolumeMode(volumeMode)
+                        .withStorageClassName(storageClassName)
+                        .withResources(
+                                new V1ResourceRequirementsBuilder()
+                                        .withRequests(Collections.singletonMap(STORAGE_KEY, quantity == null ? DEFAULT_QUANTITY : quantity))
+                                        .build()
+                        )
+                        .build());
+    }
+
+    @Override
+    public V1PersistentVolumeClaim toK8sObject() {
+        return new V1PersistentVolumeClaimBuilder()
+                .withApiVersion(API_VERSION)
+                .withKind(KIND)
+                .withMetadata(toV1ObjectMeta())
+                .withSpec(
+                    new V1PersistentVolumeClaimSpecBuilder()
+                        .withAccessModes(accessModes)
+                        .withVolumeMode(volumeMode)
+                        .withStorageClassName(storageClassName)
+                        .withResources(
+                                new V1ResourceRequirementsBuilder()
+                                .withRequests(Collections.singletonMap(STORAGE_KEY, quantity == null ? DEFAULT_QUANTITY : quantity))
+                                .build()
+                        )
+                        .build()
+                )
+                .build();
+    }
+}

+ 111 - 2
src/main/java/cn/seecoder/paas/service/impl/ApplicationServiceImpl.java

@@ -2,19 +2,31 @@ package cn.seecoder.paas.service.impl;
 
 import cn.seecoder.paas.data.dao.ApplicationDAO;
 import cn.seecoder.paas.data.dao.ConfigDAO;
+import cn.seecoder.paas.data.dao.EnvironmentDAO;
 import cn.seecoder.paas.data.entity.Application;
 import cn.seecoder.paas.data.entity.Config;
+import cn.seecoder.paas.data.entity.Environment;
 import cn.seecoder.paas.service.ApplicationService;
+import cn.seecoder.paas.service.EnvironmentService;
+import cn.seecoder.paas.service.facade.k8s.PersistentVolumeClaimApi;
+import cn.seecoder.paas.service.facade.k8s.model.K8sObjectRequest;
+import cn.seecoder.paas.service.facade.k8s.model.PersistentVolumeClaim;
 import cn.seecoder.paas.service.model.converter.ApplicationConverter;
 import cn.seecoder.paas.service.model.converter.ConfigConverter;
+import cn.seecoder.paas.service.model.vo.AppVolumeVO;
 import cn.seecoder.paas.service.model.vo.ApplicationVO;
 import cn.seecoder.paas.service.model.vo.ConfigVO;
+import cn.seecoder.paas.util.ApplicationProperties;
 import cn.seecoder.paas.util.ServiceException;
 import cn.seecoder.paas.util.enums.ConfigBelongsToType;
+import cn.seecoder.paas.util.enums.ResourceLabel;
+import io.kubernetes.client.custom.Quantity;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
 
 import javax.transaction.Transactional;
+import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -25,10 +37,22 @@ public class ApplicationServiceImpl implements ApplicationService {
 
     private final ConfigDAO configDAO;
 
+    private final EnvironmentService environmentService;
+
+    private final EnvironmentDAO environmentDAO;
+
+    private final PersistentVolumeClaimApi persistentVolumeClaimApi;
+
+    private final ApplicationProperties applicationProperties;
+
     @Autowired
-    public ApplicationServiceImpl(ApplicationDAO applicationDAO, ConfigDAO configDAO) {
+    public ApplicationServiceImpl(ApplicationDAO applicationDAO, ConfigDAO configDAO, EnvironmentService environmentService, EnvironmentDAO environmentDAO, PersistentVolumeClaimApi persistentVolumeClaimApi, ApplicationProperties applicationProperties) {
         this.applicationDAO = applicationDAO;
         this.configDAO = configDAO;
+        this.environmentService = environmentService;
+        this.environmentDAO = environmentDAO;
+        this.persistentVolumeClaimApi = persistentVolumeClaimApi;
+        this.applicationProperties = applicationProperties;
     }
 
     @Override
@@ -45,8 +69,25 @@ public class ApplicationServiceImpl implements ApplicationService {
 
     @Override
     @Transactional
-    public void delete(Integer id) {
+    public void delete(Integer id) throws ServiceException {
         applicationDAO.deleteById(id);
+        configDAO.deleteByConfigBelongsAndAndEntityId(ConfigBelongsToType.APPLICATION, id);
+        List<Environment> environments = environmentDAO.findAllByAppId(id);
+        for (Environment environment : environments) {
+            environmentService.delete(environment.getId());
+        }
+        String labelKey = ResourceLabel.RESOURCE.getCode();
+        String labelValue = ResourceLabel.RESOURCE.getGenerator().gen("application", String.valueOf(id));
+        List<PersistentVolumeClaim> pvcs = persistentVolumeClaimApi.getByCondition(
+                K8sObjectRequest.builder()
+                        .namespace(applicationProperties.getDeploymentNamespace())
+                        .labels(Collections.singletonMap(labelKey, labelValue))
+                        .build());
+        if (!CollectionUtils.isEmpty(pvcs)) {
+            for (PersistentVolumeClaim pvc : pvcs) {
+                persistentVolumeClaimApi.delete(pvc);
+            }
+        }
     }
 
     @Override
@@ -67,7 +108,75 @@ public class ApplicationServiceImpl implements ApplicationService {
         } else {
             vo.setConfig(ConfigVO.getEmptyConfigVO(ConfigBelongsToType.APPLICATION, id));
         }
+        String labelKey = ResourceLabel.RESOURCE.getCode();
+        String labelValue = ResourceLabel.RESOURCE.getGenerator().gen("application", String.valueOf(application.getId()));
+        List<PersistentVolumeClaim> pvcs = persistentVolumeClaimApi.getByCondition(
+                K8sObjectRequest.builder()
+                        .namespace(applicationProperties.getDeploymentNamespace())
+                        .labels(Collections.singletonMap(labelKey, labelValue))
+                        .build());
+        vo.setPvcs(pvcs.stream().map(pvc -> {
+            return new AppVolumeVO(pvc.getName(), application.getId(), pvc.getQuantity().toSuffixedString());
+        }).collect(Collectors.toList()));
         return vo;
     }
 
+    @Override
+    public void createAppVolume(AppVolumeVO appVolumeVO) throws ServiceException {
+        Application application = applicationDAO.findById(appVolumeVO.getAppId()).orElse(null);
+        if (application == null) {
+            throw ServiceException.INVALID_DATA;
+        }
+        PersistentVolumeClaim pvc = getPVCByAppVolume(appVolumeVO);
+        persistentVolumeClaimApi.create(pvc);
+    }
+
+    @Override
+    public void updateAppVolume(AppVolumeVO appVolumeVO) throws ServiceException {
+        Application application = applicationDAO.findById(appVolumeVO.getAppId()).orElse(null);
+        if (application == null) {
+            throw ServiceException.INVALID_DATA;
+        }
+        String labelKey = ResourceLabel.RESOURCE.getCode();
+        String labelValue = ResourceLabel.RESOURCE.getGenerator().gen("application", String.valueOf(appVolumeVO.getAppId()));
+        List<PersistentVolumeClaim> pvcs = persistentVolumeClaimApi.getByCondition(
+                K8sObjectRequest.builder()
+                        .name("a" + appVolumeVO.getAppId() + "-" + appVolumeVO.getName())
+                        .namespace(applicationProperties.getDeploymentNamespace())
+                        .labels(Collections.singletonMap(labelKey, labelValue))
+                        .build());
+        if (CollectionUtils.isEmpty(pvcs)) {
+            throw ServiceException.BAD_REQUEST;
+        }
+        PersistentVolumeClaim pvc = getPVCByAppVolume(appVolumeVO);
+        persistentVolumeClaimApi.update(pvc);
+    }
+
+    private PersistentVolumeClaim getPVCByAppVolume(AppVolumeVO appVolumeVO) {
+        String labelKey = ResourceLabel.RESOURCE.getCode();
+        String labelValue = ResourceLabel.RESOURCE.getGenerator().gen("application", String.valueOf(appVolumeVO.getAppId()));
+        PersistentVolumeClaim pvc = PersistentVolumeClaim.builder()
+                .storageClassName(applicationProperties.getPvcStorageClassName())
+                .accessModes(Collections.singletonList("ReadWriteMany"))
+                .volumeMode("Filesystem")
+                .quantity(Quantity.fromString(appVolumeVO.getQuantity()))
+                .build();
+        pvc.setName("a" + appVolumeVO.getAppId() + "-" + appVolumeVO.getName());
+        pvc.setNamespace(applicationProperties.getDeploymentNamespace());
+        pvc.setLabel(labelKey, labelValue);
+        return pvc;
+    }
+
+    @Override
+    public void deleteAppVolume(AppVolumeVO appVolumeVO) throws ServiceException {
+        Application application = applicationDAO.findById(appVolumeVO.getAppId()).orElse(null);
+        if (application == null) {
+            throw ServiceException.INVALID_DATA;
+        }
+        PersistentVolumeClaim pvc = new PersistentVolumeClaim();
+        pvc.setNamespace(applicationProperties.getDeploymentNamespace());
+        pvc.setName(appVolumeVO.getName());
+        persistentVolumeClaimApi.delete(pvc);
+    }
+
 }

+ 83 - 33
src/main/java/cn/seecoder/paas/service/impl/EnvironmentServiceImpl.java

@@ -31,7 +31,6 @@ import org.eclipse.jgit.api.ResetCommand;
 import org.slf4j.Logger;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.support.TransactionSynchronizationAdapter;
 import org.springframework.transaction.support.TransactionSynchronizationManager;
@@ -154,9 +153,33 @@ public class EnvironmentServiceImpl implements EnvironmentService {
 
     @Override
     @Transactional
-    public void delete(Integer id) {
+    public void delete(Integer id) throws ServiceException {
+        Environment environment = environmentDAO.findById(id).orElse(null);
+        if (environment == null) {
+            throw ServiceException.BAD_REQUEST;
+        }
         environmentDAO.deleteById(id);
         configDAO.deleteByConfigBelongsAndAndEntityId(ConfigBelongsToType.ENVIRONMENT, id);
+        String labelValue = ResourceLabel.RESOURCE.getGenerator().gen("environment", String.valueOf(environment.getAppId()), String.valueOf(environment.getId()));
+        String namespace = applicationProperties.getDeploymentNamespace();
+        Map<String, String> labels = Collections.singletonMap(ResourceLabel.RESOURCE.getCode(), labelValue);
+        AbstractApi[] deleteApis = {ingressApi, serviceApi, deploymentApi, configMapApi};
+        for (AbstractApi api : deleteApis) {
+            this.findAndDelete(api, namespace, labels);
+        }
+    }
+
+    private void findAndDelete(AbstractApi abstractApi, String deployNamespace, Map<String, String> labels) {
+        List<K8sAbstractObject> objects = abstractApi.getByCondition(K8sObjectRequest.builder().namespace(deployNamespace).labels(labels).build());
+        if (!CollectionUtils.isEmpty(objects)) {
+            for (K8sAbstractObject object : objects) {
+                try {
+                    abstractApi.delete(object);
+                } catch (Exception e) {
+                    LoggerUtil.error(logger, e, "deleteObjectError: object={}, labels={}", object, labels);
+                }
+            }
+        }
     }
 
     @Override
@@ -285,30 +308,40 @@ public class EnvironmentServiceImpl implements EnvironmentService {
                             .name(labelValue)
                             .namespace(applicationProperties.getDeploymentNamespace())
                             .build());
-                    if (!CollectionUtils.isEmpty(deployments)) {
-                        V1DeploymentStatus status = deployments.get(0).getStatus();
-                        List<V1DeploymentCondition> conditions = status.getConditions();
-                        if (!CollectionUtils.isEmpty(conditions)) {
-                            logger.info(conditions.stream().map(condition -> condition.toString()).collect(Collectors.joining("\n----\n")));
-                            Collections.sort(conditions, Comparator.comparingLong(c -> ((V1DeploymentCondition)c).getLastUpdateTime().getMillis()).reversed());
-                            V1DeploymentCondition condition = conditions.get(0);
-                            switch (condition.getType()) {
-                                case "Available":
-                                case "Complete":
-                                    environment.setDeployStatus(DeployStatus.SUCCESS);
-                                    environment.setDeployOutput(condition.getMessage());
-                                    break;
-                                case "Progressing":
-                                    environment.setDeployStatus(DeployStatus.DEPLOYING);
-                                    environment.setDeployOutput(condition.getMessage());
-                                    break;
-                                default:
+                    List<Pod> pods = podApi.getByCondition(K8sObjectRequest
+                            .builder()
+                            .name(labelValue)
+                            .namespace(applicationProperties.getDeploymentNamespace())
+                            .build());
+                    // 优先级先看pod,信息比较多
+                    if (!CollectionUtils.isEmpty(pods)) {
+                        V1PodStatus status = pods.get(0).getPodStatus();
+                        List<V1PodCondition> conditions = status.getConditions() == null ? Collections.emptyList() : status.getConditions();
+                        Collections.sort(conditions, Comparator.comparingLong(c -> ((V1DeploymentCondition)c).getLastUpdateTime().getMillis()).reversed());
+                        String deployOutput = "Type\tStatus\tMessage\tReason\tTime\n";
+                        deployOutput += conditions.stream().map(condition -> {
+                            return condition.getType() + "\t" + condition.getStatus() + "\t"  + condition.getMessage() + "\t" + condition.getReason() + "\t" + condition.getLastTransitionTime().toString();
+                        }).collect(Collectors.joining("\n"));
+                        switch (status.getPhase()) {
+                            case "Succeeded":
+                            case "Running":
+                                if (conditions.get(0) != null && conditions.get(0).getStatus().equals("False")) {
                                     environment.setDeployStatus(DeployStatus.FAIL);
-                                    environment.setDeployOutput(condition.getMessage());
-                                    break;
-                            }
-                            environmentDAO.save(environment);
+                                } else {
+                                    environment.setDeployStatus(DeployStatus.SUCCESS);
+                                }
+                                break;
+                            case "Pending":
+                                environment.setDeployStatus(DeployStatus.DEPLOYING);
+                                break;
+                            case "Failed":
+                            case "Unknown":
+                            default:
+                                environment.setDeployStatus(DeployStatus.FAIL);
+                                break;
                         }
+                        environment.setDeployOutput(deployOutput);
+                        environmentDAO.save(environment);
                     }
                 }
             }
@@ -341,16 +374,21 @@ public class EnvironmentServiceImpl implements EnvironmentService {
                         dockerApi.buildAndPush(directory.toString(), "seecoder-paas-" + labelValue, imageTag, new ProgressHandler() {
                             @Override
                             public void progress(ProgressMessage message) throws DockerException {
-                                if (message.stream() != null) {
-                                    sb.append(message.stream());
-                                    if (sb.length() > DEFAULT_BUFFER_LENGTH) {
-                                        result.setBuildOutput(result.getBuildOutput() + sb.toString());
-                                        if (result.getBuildOutput().length() > MAX_BUFFER_SIZE) {
-                                            result.setBuildOutput(result.getBuildOutput().substring((int) (result.getBuildOutput().length() - MAX_BUFFER_SIZE)));
-                                        }
-                                        sb.setLength(0);
-                                        environmentDAO.save(result);
+                                String value = message.stream();
+                                if (value == null) {
+                                    value = "";
+                                }
+                                if (!StringUtils.isEmpty(message.error())) {
+                                    value += "\n" + message.error();
+                                }
+                                sb.append(value);
+                                if (sb.length() > DEFAULT_BUFFER_LENGTH) {
+                                    result.setBuildOutput(result.getBuildOutput() + sb.toString());
+                                    if (result.getBuildOutput().length() > MAX_BUFFER_SIZE) {
+                                        result.setBuildOutput(result.getBuildOutput().substring((int) (result.getBuildOutput().length() - MAX_BUFFER_SIZE)));
                                     }
+                                    sb.setLength(0);
+                                    environmentDAO.save(result);
                                 }
                             }
                         });
@@ -511,6 +549,18 @@ public class EnvironmentServiceImpl implements EnvironmentService {
                 }
             }
 
+            if (!CollectionUtils.isEmpty(configContent.getPvcPaths())) {
+                for (Map.Entry<String, String> entry : configContent.getPvcPaths().entrySet()) {
+                    String name = labelValue + "-" + entry.getKey();
+                    volumes.add(new V1VolumeBuilder().withName(name).withPersistentVolumeClaim(
+                            new V1PersistentVolumeClaimVolumeSourceBuilder().withClaimName("a" + result.getAppId() + "-" + entry.getKey()).build()
+                    ).build());
+                    volumeMounts.add(Container.VolumeMount.builder()
+                            .name(name)
+                            .mountPath(entry.getValue())
+                            .build());
+                }
+            }
             Deployment deployment = Deployment.builder()
                     .imagePullSecrets(Collections.singletonList(DEFAULT_IMAGE_PULL_SECRET_NAME))
                     .timeout(Deployment.DEFAULT_PROGRESS_DEADLINE_SECONDS)

+ 22 - 0
src/main/java/cn/seecoder/paas/service/model/vo/AppVolumeVO.java

@@ -0,0 +1,22 @@
+package cn.seecoder.paas.service.model.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class AppVolumeVO {
+
+    @NotBlank
+    private String name;
+
+    @NotNull
+    private Integer appId;
+
+    private String quantity;
+}

+ 4 - 0
src/main/java/cn/seecoder/paas/service/model/vo/ApplicationVO.java

@@ -4,6 +4,7 @@ import lombok.Data;
 
 import javax.validation.constraints.NotBlank;
 import javax.validation.constraints.NotNull;
+import java.util.List;
 
 @Data
 public class ApplicationVO {
@@ -19,4 +20,7 @@ public class ApplicationVO {
     private String gitUrl;
 
     private ConfigVO config;
+
+    private List<AppVolumeVO> pvcs;
+    
 }

+ 8 - 1
src/main/java/cn/seecoder/paas/service/model/vo/ConfigVO.java

@@ -27,7 +27,7 @@ public class ConfigVO {
     private static final ConfigContent EMPTY_CONFIG_CONTENT;
 
     static {
-        EMPTY_CONFIG_CONTENT = new ConfigContent(null, null, Collections.emptyMap(), null, null, Collections.emptyMap(), Collections.emptyMap(), null, null, Collections.emptyMap());
+        EMPTY_CONFIG_CONTENT = new ConfigContent(null, null, Collections.emptyMap(), null, null, Collections.emptyMap(), Collections.emptyMap(), null, null, Collections.emptyMap(), Collections.emptyMap());
     }
 
     public static ConfigVO getEmptyConfigVO(ConfigBelongsToType configBelongs, Integer entityId) {
@@ -98,6 +98,13 @@ public class ConfigVO {
         @JsonProperty("ingressAnnotations")
         private Map<String, Map<String, String>> ingressAnnotations;
 
+        /**
+         * key:pvcName
+         * value:pvcValue
+         */
+        @JsonProperty("pvcPaths")
+        private Map<String, String> pvcPaths;
+
         @JsonIgnore()
         public boolean isValid() {
             return servicePort != null && hostPrefix != null;

+ 2 - 0
src/main/java/cn/seecoder/paas/util/ApplicationProperties.java

@@ -11,6 +11,8 @@ public class ApplicationProperties {
 
     private String deploymentNamespace;
 
+    private String pvcStorageClassName;
+
     private String deploymentHost;
 
     private K8s k8s = new K8s();

+ 20 - 1
src/main/java/cn/seecoder/paas/web/controller/api/ApplicationController.java

@@ -1,6 +1,7 @@
 package cn.seecoder.paas.web.controller.api;
 
 import cn.seecoder.paas.service.ApplicationService;
+import cn.seecoder.paas.service.model.vo.AppVolumeVO;
 import cn.seecoder.paas.service.model.vo.ApplicationVO;
 import cn.seecoder.paas.util.ServiceException;
 import cn.seecoder.paas.web.model.Response;
@@ -39,8 +40,26 @@ public class ApplicationController {
     }
 
     @DeleteMapping("/{id}")
-    public Response delete(@PathVariable("id") Integer id) {
+    public Response delete(@PathVariable("id") Integer id) throws ServiceException {
         applicationService.delete(id);
         return Response.buildSuccess(null);
     }
+
+    @PostMapping("/volume")
+    public Response createVolume(@RequestBody AppVolumeVO appVolumeVO) throws ServiceException {
+        applicationService.createAppVolume(appVolumeVO);
+        return Response.buildSuccess(null);
+    }
+
+    @PutMapping("/volume")
+    public Response updateAppVolume(@RequestBody AppVolumeVO appVolumeVO) throws ServiceException {
+        applicationService.updateAppVolume(appVolumeVO);
+        return Response.buildSuccess(null);
+    }
+
+    @DeleteMapping("/volume")
+    public Response deleteAppVolume(@RequestBody AppVolumeVO appVolumeVO) throws ServiceException {
+        applicationService.deleteAppVolume(appVolumeVO);
+        return Response.buildSuccess(null);
+    }
 }

+ 1 - 1
src/main/java/cn/seecoder/paas/web/controller/api/EnvironmentController.java

@@ -43,7 +43,7 @@ public class EnvironmentController {
     }
 
     @DeleteMapping("/{id}")
-    public Response delete(@PathVariable("id") Integer id) {
+    public Response delete(@PathVariable("id") Integer id) throws ServiceException {
         environmentService.delete(id);
         return Response.buildSuccess(null);
     }

+ 2 - 1
src/main/resources/application.yml

@@ -40,4 +40,5 @@ paas:
     debug: false
     imageRegistry: 192.168.68.79:18082
   deployment-namespace: seec
-  deployment-host: seecii.cn
+  deployment-host: seecii.cn
+  pvcStorageClassName: seecoder-paas