|
|
@@ -0,0 +1,373 @@
|
|
|
+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.EnvironmentService;
|
|
|
+import cn.seecoder.paas.service.facade.docker.DockerApi;
|
|
|
+import cn.seecoder.paas.service.facade.git.GitApi;
|
|
|
+import cn.seecoder.paas.service.facade.k8s.*;
|
|
|
+import cn.seecoder.paas.service.facade.k8s.model.*;
|
|
|
+import cn.seecoder.paas.service.facade.k8s.vo.SecretVO;
|
|
|
+import cn.seecoder.paas.service.model.converter.ConfigConverter;
|
|
|
+import cn.seecoder.paas.service.model.converter.EnvironmentConverter;
|
|
|
+import cn.seecoder.paas.service.model.vo.ConfigVO;
|
|
|
+import cn.seecoder.paas.service.model.vo.EnvironmentVO;
|
|
|
+import cn.seecoder.paas.util.ApplicationProperties;
|
|
|
+import cn.seecoder.paas.util.AsyncWrapper;
|
|
|
+import cn.seecoder.paas.util.LoggerUtil;
|
|
|
+import cn.seecoder.paas.util.ServiceException;
|
|
|
+import cn.seecoder.paas.util.enums.BuildStatus;
|
|
|
+import cn.seecoder.paas.util.enums.BuildType;
|
|
|
+import cn.seecoder.paas.util.enums.ConfigBelongsToType;
|
|
|
+import cn.seecoder.paas.util.enums.ResourceLabel;
|
|
|
+import com.spotify.docker.client.ProgressHandler;
|
|
|
+import com.spotify.docker.client.exceptions.DockerException;
|
|
|
+import com.spotify.docker.client.messages.ProgressMessage;
|
|
|
+import io.kubernetes.client.openapi.models.*;
|
|
|
+import org.eclipse.jgit.api.Git;
|
|
|
+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.util.CollectionUtils;
|
|
|
+
|
|
|
+import javax.transaction.Transactional;
|
|
|
+import java.nio.file.Files;
|
|
|
+import java.nio.file.Path;
|
|
|
+import java.nio.file.Paths;
|
|
|
+import java.util.*;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+@org.springframework.stereotype.Service
|
|
|
+public class EnvironmentServiceImpl implements EnvironmentService {
|
|
|
+
|
|
|
+ private static final Logger logger = LoggerUtil.getLogger(EnvironmentServiceImpl.class);
|
|
|
+
|
|
|
+ private final EnvironmentDAO environmentDAO;
|
|
|
+
|
|
|
+ private final ApplicationDAO applicationDAO;
|
|
|
+
|
|
|
+ private final ConfigDAO configDAO;
|
|
|
+
|
|
|
+ private final PodApi podApi;
|
|
|
+
|
|
|
+ private final DeploymentApi deploymentApi;
|
|
|
+
|
|
|
+ private final ServiceApi serviceApi;
|
|
|
+
|
|
|
+ private final IngressApi ingressApi;
|
|
|
+
|
|
|
+ private final SecretApi secretApi;
|
|
|
+
|
|
|
+ private final ConfigMapApi configMapApi;
|
|
|
+
|
|
|
+ private final AsyncWrapper asyncWrapper;
|
|
|
+
|
|
|
+ private final GitApi gitApi;
|
|
|
+
|
|
|
+ private final DockerApi dockerApi;
|
|
|
+
|
|
|
+ private final LogApi logApi;
|
|
|
+
|
|
|
+ private final ApplicationProperties applicationProperties;
|
|
|
+
|
|
|
+ private static final String DEFAULT_IMAGE_PULL_SECRET_NAME = "seecoder-paas-image-pull-secret";
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ public EnvironmentServiceImpl(EnvironmentDAO environmentDAO,
|
|
|
+ ApplicationDAO applicationDAO,
|
|
|
+ ConfigDAO configDAO,
|
|
|
+ PodApi podApi,
|
|
|
+ DeploymentApi deploymentApi,
|
|
|
+ ServiceApi serviceApi,
|
|
|
+ IngressApi ingressApi,
|
|
|
+ SecretApi secretApi,
|
|
|
+ ConfigMapApi configMapApi,
|
|
|
+ GitApi gitApi,
|
|
|
+ DockerApi dockerApi,
|
|
|
+ LogApi logApi,
|
|
|
+ ApplicationProperties properties,
|
|
|
+ AsyncWrapper asyncWrapper) {
|
|
|
+ this.environmentDAO = environmentDAO;
|
|
|
+ this.applicationDAO = applicationDAO;
|
|
|
+ this.configDAO = configDAO;
|
|
|
+ this.podApi = podApi;
|
|
|
+ this.deploymentApi = deploymentApi;
|
|
|
+ this.serviceApi = serviceApi;
|
|
|
+ this.ingressApi = ingressApi;
|
|
|
+ this.secretApi = secretApi;
|
|
|
+ this.configMapApi = configMapApi;
|
|
|
+ this.gitApi = gitApi;
|
|
|
+ this.dockerApi = dockerApi;
|
|
|
+ this.logApi = logApi;
|
|
|
+ this.applicationProperties = properties;
|
|
|
+ this.asyncWrapper = asyncWrapper;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Transactional
|
|
|
+ public EnvironmentVO createOrUpdate(EnvironmentVO environmentVO) throws ServiceException {
|
|
|
+ Application application = applicationDAO.findById(environmentVO.getAppId()).orElse(null);
|
|
|
+ if (application == null) {
|
|
|
+ throw ServiceException.BAD_REQUEST;
|
|
|
+ }
|
|
|
+ Environment environment = EnvironmentConverter.convertToEntity(environmentVO);
|
|
|
+ if (environmentVO.getId() != null) {
|
|
|
+ environment = environmentDAO.findById(environmentVO.getId()).orElse(null);
|
|
|
+ if (environment == null) {
|
|
|
+ throw ServiceException.BAD_REQUEST;
|
|
|
+ }
|
|
|
+ BeanUtils.copyProperties(environmentVO, environment, "id", "appId", "buildStatus", "buildOutput");
|
|
|
+ }
|
|
|
+ EnvironmentVO result = EnvironmentConverter.convertToVO(environmentDAO.save(environment));
|
|
|
+ ConfigVO resultConfigVO;
|
|
|
+ if (environmentVO.getId() == null) {
|
|
|
+ // 是create
|
|
|
+ // 获取
|
|
|
+ Config config = configDAO.findByConfigBelongsAndAndEntityId(ConfigBelongsToType.APPLICATION, application.getId());
|
|
|
+ if (config == null) {
|
|
|
+ config = ConfigConverter.convertToEntity(ConfigVO.getEmptyConfigVO(ConfigBelongsToType.ENVIRONMENT, result.getId()));
|
|
|
+ }
|
|
|
+ Config saved = new Config();
|
|
|
+ BeanUtils.copyProperties(config, saved);
|
|
|
+ saved.setId(null);
|
|
|
+ saved.setConfigBelongs(ConfigBelongsToType.ENVIRONMENT);
|
|
|
+ saved.setEntityId(result.getId());
|
|
|
+ resultConfigVO = ConfigConverter.convertToVO(configDAO.save(saved));
|
|
|
+ } else {
|
|
|
+ resultConfigVO = ConfigConverter.convertToVO(configDAO.findByConfigBelongsAndAndEntityId(ConfigBelongsToType.ENVIRONMENT, result.getId()));
|
|
|
+ }
|
|
|
+ result.setConfig(resultConfigVO);
|
|
|
+ String labelKey = ResourceLabel.RESOURCE.getCode();
|
|
|
+ String labelValue = ResourceLabel.RESOURCE.getGenerator().gen("environment", String.valueOf(result.getAppId()), String.valueOf(result.getId()));
|
|
|
+ K8sObjectRequest request = K8sObjectRequest.builder().namespace(applicationProperties.getDeploymentNamespace()).labels(Collections.singletonMap(labelKey, labelValue)).build();
|
|
|
+ List<Pod> pods = podApi.getByCondition(request);
|
|
|
+ result.setInstance(pods);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Transactional
|
|
|
+ public void delete(Integer id) {
|
|
|
+ environmentDAO.deleteById(id);
|
|
|
+ configDAO.deleteByConfigBelongsAndAndEntityId(ConfigBelongsToType.ENVIRONMENT, id);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public EnvironmentVO get(Integer id) throws ServiceException {
|
|
|
+ Environment result = environmentDAO.findById(id).orElse(null);
|
|
|
+ if (result == null) {
|
|
|
+ throw ServiceException.BAD_REQUEST;
|
|
|
+ }
|
|
|
+ ConfigVO resultConfigVO = ConfigConverter.convertToVO(configDAO.findByConfigBelongsAndAndEntityId(ConfigBelongsToType.ENVIRONMENT, result.getId()));
|
|
|
+ EnvironmentVO vo = EnvironmentConverter.convertToVO(result);
|
|
|
+ vo.setConfig(resultConfigVO);
|
|
|
+ String labelKey = ResourceLabel.RESOURCE.getCode();
|
|
|
+ String labelValue = ResourceLabel.RESOURCE.getGenerator().gen("environment", String.valueOf(result.getAppId()), String.valueOf(result.getId()));
|
|
|
+ K8sObjectRequest request = K8sObjectRequest.builder().namespace(applicationProperties.getDeploymentNamespace()).labels(Collections.singletonMap(labelKey, labelValue)).build();
|
|
|
+ List<Pod> pods = podApi.getByCondition(request);
|
|
|
+ vo.setInstance(pods);
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public List<EnvironmentVO> getListByAppId(Integer appId) {
|
|
|
+ return environmentDAO.findAllByAppId(appId).stream().map(EnvironmentConverter::convertToVO).collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Transactional
|
|
|
+ public EnvironmentVO deploy(Integer id) throws ServiceException {
|
|
|
+ Environment result = environmentDAO.findById(id).orElse(null);
|
|
|
+ if (result == null) {
|
|
|
+ throw ServiceException.BAD_REQUEST;
|
|
|
+ }
|
|
|
+ if (result.getBuildStatus() == BuildStatus.BUILDING) {
|
|
|
+ throw new ServiceException("101", "存在正在进行的构建!");
|
|
|
+ }
|
|
|
+ Config config = configDAO.findByConfigBelongsAndAndEntityId(ConfigBelongsToType.ENVIRONMENT, id);
|
|
|
+ if (config == null) {
|
|
|
+ throw ServiceException.INVALID_DATA;
|
|
|
+ }
|
|
|
+ ConfigVO.ConfigContent configContent = ConfigConverter.convertToVO(config).getConfig();
|
|
|
+ if (configContent.getHostPrefix() == null) {
|
|
|
+ throw ServiceException.INVALID_DATA;
|
|
|
+ }
|
|
|
+ result.setBuildOutput("");
|
|
|
+ result.setBuildStatus(BuildStatus.BUILDING);
|
|
|
+ asyncWrapper.asyncInvoke(() -> buildAsync(id));
|
|
|
+ return EnvironmentConverter.convertToVO(environmentDAO.save(result));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map<String,String> getLog(Integer id, String podName) throws ServiceException {
|
|
|
+ Environment result = environmentDAO.findById(id).orElse(null);
|
|
|
+ if (result == null) {
|
|
|
+ throw ServiceException.BAD_REQUEST;
|
|
|
+ }
|
|
|
+ String labelKey = ResourceLabel.RESOURCE.getCode();
|
|
|
+ String labelValue = ResourceLabel.RESOURCE.getGenerator().gen("environment", String.valueOf(result.getAppId()), String.valueOf(result.getId()));
|
|
|
+ K8sObjectRequest request = K8sObjectRequest.builder().name(podName).namespace(applicationProperties.getDeploymentNamespace()).labels(Collections.singletonMap(labelKey, labelValue)).build();
|
|
|
+ List<Pod> pods = podApi.getByCondition(request);
|
|
|
+ if (CollectionUtils.isEmpty(pods)) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ Map<String, String> res = new HashMap<>();
|
|
|
+ for (Container container : pods.get(0).getContainers()) {
|
|
|
+ res.put(container.getName(), logApi.getAll(applicationProperties.getDeploymentNamespace(), pods.get(0).getName(), container.getName()));
|
|
|
+ }
|
|
|
+ return res;
|
|
|
+ }
|
|
|
+
|
|
|
+ void buildAsync(Integer id) throws ServiceException {
|
|
|
+ Environment result = environmentDAO.findById(id).get();
|
|
|
+ Config config = configDAO.findByConfigBelongsAndAndEntityId(ConfigBelongsToType.ENVIRONMENT, id);
|
|
|
+ Application application = applicationDAO.findById(result.getAppId()).get();
|
|
|
+ ConfigVO.ConfigContent configContent = ConfigConverter.convertToVO(config).getConfig();
|
|
|
+ String labelKey = ResourceLabel.RESOURCE.getCode();
|
|
|
+ String labelValue = ResourceLabel.RESOURCE.getGenerator().gen("environment", String.valueOf(result.getAppId()), String.valueOf(result.getId()));
|
|
|
+ Map<String, String> labelsMap = Collections.singletonMap(labelKey, labelValue);
|
|
|
+ String deploymentNamespace = applicationProperties.getDeploymentNamespace();
|
|
|
+ String deploymentHost = applicationProperties.getDeploymentHost();
|
|
|
+ String imageName = result.getBuildTypeValue();
|
|
|
+ // 1. 有git从git拿到地址进行构建并push
|
|
|
+ if (result.getBuildType() == BuildType.FROM_BRANCH_OR_COMMIT) {
|
|
|
+ try {
|
|
|
+ Path directory = Files.createTempDirectory("seecoder-paas-");
|
|
|
+ Git git = gitApi.clone(application.getGitUrl(), directory.toString());
|
|
|
+ if (Files.exists(Paths.get(directory.toString(), "Dockerfile"))) {
|
|
|
+ git.reset().setMode(ResetCommand.ResetType.HARD).setRef(result.getBuildTypeValue()).call();
|
|
|
+ String imageTag = String.valueOf(new Date().getTime());
|
|
|
+ dockerApi.buildAndPush(directory.toString(), "seecoder-paas-" + labelValue, imageTag, new ProgressHandler() {
|
|
|
+ @Override
|
|
|
+ public void progress(ProgressMessage message) throws DockerException {
|
|
|
+ result.setBuildOutput(message.stream());
|
|
|
+ environmentDAO.save(result);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ imageName = applicationProperties.getDocker().getRegistry() + "/seecoder-paas-" + labelValue + ":" + imageTag;
|
|
|
+ result.setBuildStatus(BuildStatus.SUCCESS);
|
|
|
+ environmentDAO.save(result);
|
|
|
+ } else {
|
|
|
+ result.setBuildStatus(BuildStatus.FAIL);
|
|
|
+ result.setBuildOutput("找不到构建文件Dockerfile!");
|
|
|
+ environmentDAO.save(result);
|
|
|
+ throw new ServiceException("102", "找不到构建文件Dockerfile!");
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ result.setBuildStatus(BuildStatus.FAIL);
|
|
|
+ result.setBuildOutput(e.getLocalizedMessage());
|
|
|
+ environmentDAO.save(result);
|
|
|
+ LoggerUtil.error(logger, "构建失败!gitUrl={}, branchOrCommit={}", application.getGitUrl(), result.getBuildTypeValue());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ String[] ports = configContent.getServicePort().split(",");
|
|
|
+ if (configContent.getHostPrefix() != null) {
|
|
|
+ for (String port : ports) {
|
|
|
+ // ingress
|
|
|
+ List<Ingress> ingresses = ingressApi.getByCondition(K8sObjectRequest.builder()
|
|
|
+ .name(labelValue + "-" + port)
|
|
|
+ .namespace(deploymentNamespace).labels(labelsMap).build());
|
|
|
+ Ingress ingress = new Ingress();
|
|
|
+ ingress.setNamespace(deploymentNamespace);
|
|
|
+ ingress.setName(labelValue + "-" + port);
|
|
|
+ ingress.setLabels(labelsMap);
|
|
|
+ ingress.setHttpRules(Collections.singletonList(
|
|
|
+ Ingress.IngressRule.builder().host(configContent.getHostPrefix() + "-" + port + "." + deploymentHost).ruleValues(
|
|
|
+ Collections.singleton(Ingress.IngressPath.builder()
|
|
|
+ .path("/")
|
|
|
+ .serviceName(labelValue)
|
|
|
+ .port(Integer.parseInt(port))
|
|
|
+ .build())
|
|
|
+ ).build()
|
|
|
+ ));
|
|
|
+ if (CollectionUtils.isEmpty(ingresses) || ingresses.size() != 1) {
|
|
|
+ ingressApi.create(ingress);
|
|
|
+ } else {
|
|
|
+ ingressApi.update(ingress);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // service
|
|
|
+ List<Service> svcs = serviceApi.getByCondition(K8sObjectRequest.builder().namespace(deploymentNamespace).labels(labelsMap).build());
|
|
|
+ Service svc = Service.builder().name(labelValue).labels(labelsMap).namespace(deploymentNamespace).selectors(labelsMap).servicePorts(
|
|
|
+ Arrays.stream(ports).map(port ->
|
|
|
+ ServicePort.builder()
|
|
|
+ .name(labelValue + "-" + port)
|
|
|
+ .port(Integer.parseInt(port))
|
|
|
+ .targetPort(Integer.parseInt(port))
|
|
|
+ .build()).collect(Collectors.toSet())).build();
|
|
|
+ if (CollectionUtils.isEmpty(svcs) || svcs.size() != 1) {
|
|
|
+ serviceApi.create(svc);
|
|
|
+ } else {
|
|
|
+ serviceApi.update(svc);
|
|
|
+ }
|
|
|
+ // 检查secret,没有则创建
|
|
|
+ SecretVO secretVO = secretApi.getSecretByName(deploymentNamespace, DEFAULT_IMAGE_PULL_SECRET_NAME);
|
|
|
+ if (secretVO == null) {
|
|
|
+ secretApi.createPrivateRegistrySecret(
|
|
|
+ deploymentNamespace,
|
|
|
+ DEFAULT_IMAGE_PULL_SECRET_NAME,
|
|
|
+ applicationProperties.getK8s().getImageRegistry(),
|
|
|
+ applicationProperties.getDocker().getRegistryUsername(),
|
|
|
+ applicationProperties.getDocker().getRegistryPassword()
|
|
|
+ );
|
|
|
+ }
|
|
|
+ // configMap
|
|
|
+ List<ConfigMap> configMaps = configMapApi.getByCondition(K8sObjectRequest.builder().namespace(deploymentNamespace).labels(labelsMap).build());
|
|
|
+ ConfigMap configMap = ConfigMap.builder().data(configContent.getMounts()).build();
|
|
|
+ configMap.setName(labelValue);
|
|
|
+ configMap.setNamespace(deploymentNamespace);
|
|
|
+ configMap.setLabel(labelKey, labelValue);
|
|
|
+ if (CollectionUtils.isEmpty(configMaps) || configMaps.size() != 1) {
|
|
|
+ configMapApi.create(configMap);
|
|
|
+ } else {
|
|
|
+ configMapApi.update(configMap);
|
|
|
+ }
|
|
|
+ // deployment
|
|
|
+ List<Deployment> deployments = deploymentApi.getByCondition(K8sObjectRequest.builder().namespace(deploymentNamespace).labels(labelsMap).build());
|
|
|
+ Deployment deployment = Deployment.builder()
|
|
|
+ .imagePullSecrets(Collections.singletonList(DEFAULT_IMAGE_PULL_SECRET_NAME))
|
|
|
+ .replicas(configContent.getReplicas() == null ? 1 : configContent.getReplicas())
|
|
|
+ .volumes(Collections.singletonList(new V1VolumeBuilder().withName(labelValue).withConfigMap(
|
|
|
+ new V1ConfigMapVolumeSourceBuilder().withName(labelValue).withItems(
|
|
|
+ configMap.getData() == null ? null :
|
|
|
+ configMap.getData().keySet().stream().map(path -> new V1KeyToPathBuilder().withKey(path).withPath(path).build()).collect(Collectors.toList())
|
|
|
+ ).build()
|
|
|
+ ).build()))
|
|
|
+ .containers(Collections.singletonList(
|
|
|
+ Container.builder()
|
|
|
+ .env(configContent.getEnvs())
|
|
|
+ .ports(
|
|
|
+ Arrays.stream(ports).map(port ->
|
|
|
+ ContainerPort.builder()
|
|
|
+ .name(labelValue + "-" + port)
|
|
|
+ .port(Integer.parseInt(port))
|
|
|
+ .protocol("TCP")
|
|
|
+ .build()).collect(Collectors.toSet())
|
|
|
+ )
|
|
|
+ .args(configContent.getRunArgs() == null ? null : Arrays.asList(configContent.getRunArgs().split(" ")))
|
|
|
+ .command(configContent.getRunCommands() == null ? null : Arrays.asList(configContent.getRunCommands().split(" ")))
|
|
|
+ .name(labelValue)
|
|
|
+ .image(imageName)
|
|
|
+ .volumeMounts(Collections.singletonList(Container.VolumeMount.builder()
|
|
|
+ .name(labelValue)
|
|
|
+ .mountPath("/")
|
|
|
+ .build()))
|
|
|
+ .build()))
|
|
|
+ .build();
|
|
|
+ deployment.setName(labelValue);
|
|
|
+ deployment.setNamespace(deploymentNamespace);
|
|
|
+ deployment.setLabel(labelKey, labelValue);
|
|
|
+ if (CollectionUtils.isEmpty(deployments) || deployments.size() != 1) {
|
|
|
+ deploymentApi.create(deployment);
|
|
|
+ } else {
|
|
|
+ deploymentApi.update(deployment);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|