120.kubernetes优雅的实现浏览器进去容器

1.背景

最近有客户提出能从paas平台上直接进入容器操作(当然前提是容器内有ssh), 本来一直想着直接连接到容器容器,但是面临很多安全问题(如证书拷贝),后续想到之前用过的阿里云的rds也是实现模式,借鉴并实现。
当然dashboard中也有实现方式,大家可以参考。

2.前端实现

<template>
  <div style="text-align: center;">
    <el-row>
      <el-col :span="6">&nbsp;</el-col>
      <el-col :span="12" style="">
        当前命名空间:<span style="color:red">default</span> 当前容器:<span style="color:red">gluster-nginx-7465bd6456-bc7xq</span>
        <el-button type="primary" @click="exec">执行</el-button>
        </el-col>
      <el-col :span="6">&nbsp;</el-col>
    </el-row>
    <el-row style="margin-bottom:20px">
      <el-col :span="6" style="text-align:right;font-size:20px">&nbsp;命令区:</el-col>
      <el-col :span="12">
        <el-input type="textarea":rows="10" class="command" v-model="command"></el-input>
      </el-col>
      <el-col :span="6">&nbsp;</el-col>
    </el-row>
    <el-row>
      <el-col :span="6"  style="text-align:right;font-size:20px">&nbsp; 结果区:</el-col>
      <el-col :span="12">
        
        <el-input type="textarea":rows="10" class="command"  v-model="result"></el-input>
      </el-col>
      <el-col :span="6">&nbsp;</el-col>
    </el-row>
  </div>
</template>

<script>
  export default {
    name: "overview",
    data() {
      return {
        sessionId:"",
        command:"",
        result:"",
        socket:null
      }
    },
    mounted(){
       this.initWebpack();
    },
    methods: {
      /*建立websocket连接*/
      initWebpack() {
        let vm = this;
        if ('WebSocket' in window) {
          this.socket = new WebSocket("ws://localhost:8081/ssh");
        } else {
          this.socket = new SockJS("http://localhost:8081/sockjs/ssh");
        }
        this.socket.onopen = () => {
          console.log('连接已建立 ...')
        }
        this.socket.onmessage = evt => {
          var msg = JSON.parse(evt.data)
          console.log(evt.data);
           console.log("---",msg)
          if (msg.type == 'OPEN') {
            console.log("---",msg.message)
            vm.sessionId = msg.message;
          } else if(msg.type == 'SSH') {
             vm.result =  msg.message
          }
           console.log(vm.sessionId)
 
        }
        this.socket.onclose = function () {
          console.log('连接已关闭 ...');
        }
      },
      exec() {
        let vm = this;
        let param = {
          sessionId: vm.sessionId,
          namespaces: "default",
          container:"",
          podName:"gluster-nginx-7465bd6456-bc7xq",
          command:vm.command
        };
        if(vm.socket!=null){
          console.log(param)
          vm.socket.send(JSON.stringify(param))
        }

      }
    }
  }
</script>

<style scoped>
.command{
  background-color: #000;
  color: yellow;
}
.el-textarea__inner{
  background-color: #000 !important;
}
</style>

借助element-ui将页面调整如下,上面为命令区(可写),下面为结果区(只读区),样式可自行调整,本实例主要为了说明过程

style.png

3.后端实现,工程基于springboot

  • 配置WebSocket的配置文件
package com.ecloud.common;

import com.ecloud.common.interceptor.HandshakeInterceptor;
import com.ecloud.socket.SSHWebSocketHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.socket.config.annotation.*;

/**
 *Created by dubblegao on 2018/11/13.
 */
@Configuration
@EnableWebMvc
@EnableWebSocket
public class WebSocketConfig extends WebMvcConfigurerAdapter implements  WebSocketConfigurer {

    public WebSocketConfig() {
    }

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(sshSocketHandler(), "/ssh")
                .addInterceptors(new HandshakeInterceptor())
                .setAllowedOrigins("*");
        registry.addHandler(sshSocketHandler(), "/sockjs/ssh")
                .addInterceptors(new HandshakeInterceptor())
                .setAllowedOrigins("*");

    }
    @Bean
    public SSHWebSocketHandler sshSocketHandler() {
        return new SSHWebSocketHandler();
    }


}

  • 配置WebSocket的拦截器
package com.ecloud.common.interceptor;

import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;

import java.util.Map;

/**
 * Created by dubblegao on 2018/11/13.
 */

@Component
public class HandshakeInterceptor extends HttpSessionHandshakeInterceptor {

    @Override
    public boolean beforeHandshake(ServerHttpRequest request,
                                   ServerHttpResponse response, WebSocketHandler wsHandler,
                                   Map<String, Object> attributes) throws Exception {

        //解决The extension [x-webkit-deflate-frame] is not supported问题
        if(request.getHeaders().containsKey("Sec-WebSocket-Extensions")) {
            request.getHeaders().set("Sec-WebSocket-Extensions", "permessage-deflate");
        }

        System.out.println("Before Handshake");
        boolean result = super.beforeHandshake(request, response, wsHandler, attributes);
        return result;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request,
                               ServerHttpResponse response, WebSocketHandler wsHandler,
                               Exception ex) {
        System.out.println("After Handshake");
        super.afterHandshake(request, response, wsHandler, ex);
    }

}

  • 配置WebSocket的处理器
package com.ecloud.socket;

import com.alibaba.fastjson.JSON;
import com.ecloud.common.model.SocketMessage;
import com.ecloud.framework.cache.MessageCache;
import com.ecloud.imwh.image.handler.ImageWebSocketHandler;
import com.google.common.collect.Maps;
import io.kubernetes.client.Exec;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.socket.*;

import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.Map;

/**
 *Created by dubblegao on 2018/11/13.
 */

public class SSHWebSocketHandler implements WebSocketHandler {

    protected static final Logger LOG = LoggerFactory.getLogger(SSHWebSocketHandler.class);

    public static Map<String, SSHWebSocketHandler> sessions = Maps.newConcurrentMap();

    private static WebSocketSession session = null;

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        this.session = session;
        sessions.put(session.getId(),this);
        //发送给客户端
        SocketMessage message = SocketMessage.build("OPEN",session.getId());
        sendMessage(message);
    }

    @Override
    public void handleMessage(WebSocketSession wss, WebSocketMessage<?> wsm) throws Exception {
        ExecBean execBean = JSON.parseObject(wsm.getPayload().toString(),ExecBean.class);
        this.exec(execBean);
        sendMessage(wsm.getPayload());

    }

    @Override
    public void handleTransportError(WebSocketSession wss, Throwable throwable) throws Exception {
        if(wss.isOpen()){
            wss.close();
        }
        System.out.println("socket connection closed......");
    }

    @Override
    public void afterConnectionClosed(WebSocketSession wss, CloseStatus cs) throws Exception {
        System.out.println("socket connection closed......");
    }

    @Override
    public boolean supportsPartialMessages() {
        return false;
    }

    public void sendMessage(Object message){
        try {
            session.sendMessage(new TextMessage(JSON.toJSONString(message)));
        }catch (Exception e){
            LOG.error(e.getMessage());
        }
    }

    public void exec(ExecBean bean){
        //此处的Exec  用的是kubernetes官方提供的客户端中的API
        //https://kubernetes.io/docs/reference/using-api/client-libraries/
        Exec  exec = new Exec();
        String[] command = {"sh", "-c", bean.getCommand()};
        try {
            final Process proc = exec.exec(
                    bean.getNamespaces(),
                    bean.getPodName(),
                    command,
                    bean.getContainer(),
                    false,
                    false);
            if(proc.waitFor()==0){
                StringWriter writer = new StringWriter();
                IOUtils.copy(proc.getInputStream(), writer, StandardCharsets.UTF_8.name());
                SocketMessage msg = SocketMessage.build("SSH",writer.toString());
                SSHWebSocketHandler.sessions.get(bean.getSessionId()).sendMessage(msg);
                proc.destroy();
            }

           /* 生产下建议用如下方式,上述方式会存在卡死
          Thread out = new Thread(
                    new Runnable() {
                        public void run() {
                            try {
                                StringWriter writer = new StringWriter();
                                IOUtils.copy(proc.getInputStream(), writer, StandardCharsets.UTF_8.name());
                                SocketMessage msg = SocketMessage.build("SSH",writer.toString());
                                SSHWebSocketHandler.sessions.get(bean.getSessionId()).sendMessage(msg);
                                //ByteStreams.copy(proc.getInputStream(), System.out);
                            } catch (IOException ex) {
                                ex.printStackTrace();
                            }
                        }
                    });
            out.start();

            proc.waitFor();
            // wait for any last output; no need to wait for input thread
            out.join();

            proc.destroy();
            System.exit(proc.exitValue());*/

        }catch (Exception e){
            e.printStackTrace();
        }

    }

}

4.测试

本例子中的参数,为了调试都是固定的,可以根据业务调整成动态

  • 查看目录文件


    ls.png
  • 查看文件内容


    cat.png

4.说明

另外如果想做到实时交互,可以调整 stdin和tty参数,将结果实时输出到输出流,将输入流拷贝的command中
该方式可以避免权限证书拷贝的问题,同时保证操作对象仅仅为容器。进供参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335