Browse Source

feat: add web shell

wanghongkai 5 years ago
parent
commit
29450f54d0

+ 4 - 0
pom.xml

@@ -101,6 +101,10 @@
             <artifactId>jjwt</artifactId>
             <version>0.9.0</version>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-websocket</artifactId>
+        </dependency>
     </dependencies>
 
     <build>

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

@@ -19,4 +19,6 @@ public interface EnvironmentService {
     EnvironmentVO deploy(Integer id) throws ServiceException;
 
     Map<String,String> getLog(Integer id, String podName) throws ServiceException;
+
+    EnvironmentVO restart(Integer id, String podName) throws ServiceException;
 }

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

@@ -34,4 +34,6 @@ public interface ExecApi {
      * @throws K8sApiException SYSTEM_ERROR pod存在多个container/系统异常/指令执行异常
      */
     boolean exec(String namespace, String podName, List<String> commands);
+
+    Process getTerminal(String namespace, String podName);
 }

+ 30 - 0
src/main/java/cn/seecoder/paas/service/facade/k8s/impl/ExecApiImpl.java

@@ -67,4 +67,34 @@ public class ExecApiImpl implements ExecApi {
         }
         return false;
     }
+
+    @Override
+    public Process getTerminal(String namespace, String podName) {
+        try {
+            final Process proc =
+                    exec.exec(
+                            namespace,
+                            podName,
+                            new String[]{"sh"},
+                            true,
+                            true);
+            return proc;
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "指令执行异常, namespace={}, podName={}, response={}",
+                    namespace, podName, e.getResponseBody());
+            if (e.getCode() == NOT_FOUND) {
+                throw new K8sApiException(NOT_FOUND, "容器不存在");
+            }else{
+                throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        } catch (IOException e) {
+            LoggerUtil.error(logger, e, "指令执行异常, namespace={}, podName={}",
+                    namespace, podName);
+            throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "指令执行异常, namespace={}, podName={}",
+                    namespace, podName);
+        }
+        return null;
+    }
 }

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

@@ -4,10 +4,14 @@ import cn.seecoder.paas.service.facade.k8s.PodApi;
 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.Namespace;
 import cn.seecoder.paas.service.facade.k8s.model.Pod;
 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.V1DeleteOptions;
+import io.kubernetes.client.openapi.models.V1DeleteOptionsBuilder;
 import io.kubernetes.client.openapi.models.V1Pod;
 import io.kubernetes.client.openapi.models.V1PodList;
 import org.slf4j.Logger;
@@ -18,6 +22,7 @@ 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.*;
 
@@ -49,7 +54,33 @@ public class PodApiImpl implements PodApi {
 
     @Override
     public void delete(Pod pod) {
-        throw  K8s_NOT_SUPPORT_METHOD;
+        try {
+            V1DeleteOptions v1DeleteOptions = new V1DeleteOptionsBuilder()
+                    .withApiVersion(Namespace.API_VERSION)
+                    .withPropagationPolicy(FOREGROUND_PROPAGATION_POLICY)
+                    .build();
+            coreV1Api.deleteNamespacedPod(
+                    pod.getName(),
+                    pod.getNamespace(),
+                    PRETTY_FORMAT,
+                    null,
+                    null,
+                    null,
+                    FOREGROUND_PROPAGATION_POLICY,
+                    v1DeleteOptions);
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "Pod删除失败,name={}, response={}", pod.getName() , e.getResponseBody());
+            if (e.getCode() == NOT_FOUND) {
+                throw K8s_NAMESPACE_NOT_EXIST;
+            }else {
+                throw K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        } catch (JsonSyntaxException e){
+            //do nothing
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "Pod删除异常, name={}", pod.getName());
+            throw K8s_SYSTEM_ERROR_EXCEPTION;
+        }
     }
 
     @Override

+ 15 - 1
src/main/java/cn/seecoder/paas/service/impl/EnvironmentServiceImpl.java

@@ -223,6 +223,20 @@ public class EnvironmentServiceImpl implements EnvironmentService {
         return res;
     }
 
+    @Override
+    public EnvironmentVO restart(Integer id, String podName) throws ServiceException {
+        Environment result = environmentDAO.findById(id).orElse(null);
+        if (result == null) {
+            throw ServiceException.BAD_REQUEST;
+        }
+        Pod pod = new Pod();
+        pod.setName(podName);
+        pod.setNamespace(applicationProperties.getDeploymentNamespace());
+        podApi.delete(pod);
+        result.setDeployStatus(DeployStatus.RESTARTING);
+        return EnvironmentConverter.convertToVO(environmentDAO.save(result));
+    }
+
     void buildAsync(Integer id) {
         Environment result = environmentDAO.findById(id).get();
         try {
@@ -383,7 +397,7 @@ public class EnvironmentServiceImpl implements EnvironmentService {
             }
             if (!CollectionUtils.isEmpty(configContent.getHostPath())) {
                 for (Map.Entry<String, String> entry : configContent.getHostPath().entrySet()) {
-                    String name = labelValue + "-" + entry.getKey().replaceAll("/", "-");
+                    String name = labelValue + "-" + entry.getKey().replaceAll("[/.]", "-");
                     volumes.add(new V1VolumeBuilder().withName(name).withHostPath(
                             new V1HostPathVolumeSourceBuilder().withPath(entry.getKey()).build()
                     ).build());

+ 2 - 0
src/main/java/cn/seecoder/paas/service/scheduler/DeployScheduler.java

@@ -49,6 +49,7 @@ public class DeployScheduler {
             }
         }
         environments = environmentDAO.findAllByDeployStatus(DeployStatus.DEPLOYING);
+        environments.addAll(environmentDAO.findAllByDeployStatus(DeployStatus.RESTARTING));
         if (!CollectionUtils.isEmpty(environments)) {
             for (Environment environment : environments) {
                 if (environment.getDeployStartTime().isBefore(startTime)) {
@@ -70,6 +71,7 @@ public class DeployScheduler {
                             V1DeploymentCondition condition = conditions.get(0);
                             switch (condition.getType()) {
                                 case "Available":
+                                case "Complete":
                                     environment.setDeployStatus(DeployStatus.SUCCESS);
                                     environment.setDeployOutput(condition.getMessage());
                                     break;

+ 1 - 0
src/main/java/cn/seecoder/paas/util/enums/DeployStatus.java

@@ -11,6 +11,7 @@ public enum DeployStatus {
     SUCCESS("SUCCESS", "成功"),
     DEPLOYING("DEPLOYING", "正在部署"),
     FAIL("FAIL", "失败"),
+    RESTARTING("RESTARTING", "重启中"),
     ;
 
     private String code;

+ 62 - 0
src/main/java/cn/seecoder/paas/util/socket/ServerEndpointConfig.java

@@ -0,0 +1,62 @@
+package cn.seecoder.paas.util.socket;
+
+import cn.seecoder.paas.util.JwtTokenUtil;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.security.SecurityProperties;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+
+@Component
+@Data
+@NoArgsConstructor
+public class ServerEndpointConfig extends javax.websocket.server.ServerEndpointConfig.Configurator implements ApplicationContextAware {
+
+    private static ApplicationContext applicationContext;
+
+    private SecurityProperties securityProperties;
+
+    private JwtTokenUtil jwtTokenUtil;
+
+    private UserDetailsService userDetailsService;
+
+    @Override
+    public boolean checkOrigin(String originHeaderValue) {
+        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+        HttpServletRequest request = servletRequestAttributes.getRequest();
+        String token = request.getParameter("token");
+        // 怪异无比
+        this.jwtTokenUtil = applicationContext.getBean(JwtTokenUtil.class);
+        this.securityProperties = applicationContext.getBean(SecurityProperties.class);
+        this.userDetailsService = (UserDetailsService) applicationContext.getBean("paasUserDetailService");
+        if (token == null) {
+            return false;
+        } else {
+            String username = jwtTokenUtil.getUsernameFromToken(token);
+            if (username != null) {
+                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
+                if (!jwtTokenUtil.validateToken(token, userDetails)) {
+                    return false;
+                }
+            } else {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        this.applicationContext = applicationContext;
+    }
+}

+ 25 - 0
src/main/java/cn/seecoder/paas/util/socket/WebSocketConfig.java

@@ -0,0 +1,25 @@
+package cn.seecoder.paas.util.socket;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
+import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
+import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
+import org.springframework.web.socket.server.standard.ServerEndpointExporter;
+
+@Configuration
+@EnableWebSocket
+@Controller
+public class WebSocketConfig implements WebSocketConfigurer {
+
+    @Bean
+    public ServerEndpointExporter serverEndpointExporter() {
+        return new ServerEndpointExporter();
+    }
+
+    @Override
+    public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
+    }
+}

+ 18 - 0
src/main/java/cn/seecoder/paas/util/socket/WebSocketSchedulerConfig.java

@@ -0,0 +1,18 @@
+package cn.seecoder.paas.util.socket;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.TaskScheduler;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
+
+@Configuration
+public class WebSocketSchedulerConfig {
+
+    @Bean
+    public TaskScheduler taskScheduler() {
+        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
+        taskScheduler.setPoolSize(10);
+        taskScheduler.initialize();
+        return taskScheduler;
+    }
+}

+ 124 - 0
src/main/java/cn/seecoder/paas/util/socket/WsServerEndpoint.java

@@ -0,0 +1,124 @@
+package cn.seecoder.paas.util.socket;
+
+import cn.seecoder.paas.service.facade.k8s.ExecApi;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.extern.apachecommons.CommonsLog;
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
+
+import javax.websocket.OnClose;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+import java.io.*;
+
+@ServerEndpoint(value = "/ws/terminal", configurator = ServerEndpointConfig.class)
+@NoArgsConstructor
+@Data
+@Component
+@CommonsLog
+public class WsServerEndpoint implements ApplicationContextAware {
+
+    private Session session;
+
+    private ExecApi execApi;
+
+    private Process process;
+
+    private Thread outThread;
+
+    private static ApplicationContext applicationContext;
+
+    private InputStream inputStream;
+
+    private OutputStream outputStream;
+
+    @OnOpen
+    public void onOpen(Session session) {
+        this.session = session;
+        try {
+            this.execApi = applicationContext.getBean(ExecApi.class);
+            String namespace = session.getRequestParameterMap().get("namespace").get(0);
+            String podName = session.getRequestParameterMap().get("podName").get(0);
+            this.process = execApi.getTerminal(namespace, podName);
+            outputStream = process.getOutputStream();
+            inputStream = process.getInputStream();
+            outThread = new Thread(() -> {
+                try {
+                    while (true){
+                        try {
+                            byte data[] = new byte[1024];
+                            if (inputStream.read(data) != -1) {
+                                session.getBasicRemote().sendText(new String(data));
+                            }
+                        } catch (IOException e) {
+//                            e.printStackTrace();
+                        }
+                        if (!process.isAlive()) {
+                            session.getBasicRemote().sendText("Timeout exited.\r");
+                            session.close();
+                            process.destroy();
+                            break;
+                        }
+                    }
+                } catch (Exception e) {
+                    e.printStackTrace();
+                } finally {
+                    process.destroy();
+                    try {
+                        session.getBasicRemote().sendText("Exited.\r");
+                    } catch (IOException e) {
+                        e.printStackTrace();
+                    } finally {
+                        try {
+                            session.close();
+                        } catch (IOException e) {
+                            e.printStackTrace();
+                        }
+                    }
+                }
+            });
+            outThread.start();
+            log.info("连接成功!\n");
+            this.session.getBasicRemote().sendText("连接成功!");
+        } catch (Exception e) {
+            log.error("连接失败!\n", e);
+            try {
+                this.session.getBasicRemote().sendText("连接失败!Error:" + e.getMessage() + "\n");
+            } catch (IOException ex) {
+                log.error("发送失败!\n", ex);
+            }
+        }
+    }
+
+    @OnClose
+    public void onClose() {
+        if (process != null) {
+            process.destroy();
+        }
+        log.info("连接中断!\n");
+    }
+
+    @OnMessage
+    public String onMessage(String text, Session session) {
+        if (this.process != null && this.outputStream != null) {
+            try {
+                outputStream.write(text.getBytes());
+                return null;
+            } catch (Exception e) {
+                return null;
+            }
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        this.applicationContext = applicationContext;
+    }
+}

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

@@ -57,4 +57,10 @@ public class EnvironmentController {
         return Response.buildSuccess(
                 environmentService.deploy(id));
     }
+
+    @PostMapping("/restart")
+    public Response deploy(@RequestParam Integer id, @RequestParam String podName) throws ServiceException {
+        return Response.buildSuccess(
+                environmentService.restart(id, podName));
+    }
 }