一个client-go连接k8s的pod,实现k8s-web-terminal的方案

参考github:

https://github.com/kikimo/k8s-terminal

这个方案,在我看来,是最好的,不要beego,使用gorilla/weboscket。单文件go实现。

这个代码有点小bug,我补全了。

还加了可以控制权限的东东,希望能用于生产环境哈。

server.go

package main

import (
        "encoding/json"
        "fmt"

        // "io/ioutil"
        "log"
        "net/http"
        "os"
        "path/filepath"
        "sync"

        "github.com/gorilla/websocket"
        "k8s.io/api/core/v1"
        "k8s.io/client-go/kubernetes"
        "k8s.io/client-go/kubernetes/scheme"
        "k8s.io/client-go/rest"
        "k8s.io/client-go/tools/clientcmd"
        "k8s.io/client-go/tools/remotecommand"
)

var (
        staticDir string
        clientset *kubernetes.Clientset
        kconfig   *rest.Config
)

// message from web socket client
type xtermMessage struct {
        MsgType string `json:"type"`  // 类型:resize客户端调整终端, input客户端输入
        Input   string `json:"input"` // msgtype=input情况下使用
        Rows    uint16 `json:"rows"`  // msgtype=resize情况下使用
        Cols    uint16 `json:"cols"`  // msgtype=resize情况下使用
}

type WSStreamHandler struct {
        conn        *websocket.Conn
        resizeEvent chan remotecommand.TerminalSize
        rbuf        []byte
        cond        *sync.Cond

        sync.Mutex
}

// Run start a loop to fetch from ws client and store the data in byte buffer
func (h *WSStreamHandler) Run(userToken string) {

        accessUrl := fmt.Sprintf("http://127.0.0.1:8080/api.html?token=%s", userToken)
        respToken, err := http.Get(accessUrl)
        if respToken != nil {
                defer respToken.Body.Close()
        }

        if err != nil {
                log.Println(err)
                return
        }

        if respToken.StatusCode != 200 {
                h.Write([]byte("亲,你无权进入此容器。bye~bye~"))
                h.conn.Close()
                return
        }

        for {
                _, p, err := h.conn.ReadMessage()
                if err != nil {
                        log.Println("ws ReadMessage err: ", err)
                        // 新增如果前端关闭,后端任然继续读取数据将会报错
                        // panic: repeated read on failed websocket connection。
                        h.conn.Close()
                        return
                }
                xmsg := xtermMessage{}
                if err := json.Unmarshal(p, &xmsg); err != nil {
                        log.Println("json.Unmarshal err: ", err)
                }

                switch xmsg.MsgType {
                case "input":
                        {
                                h.Lock()
                                // log.Printf("reading input: %s", string(xmsg.Input))
                                h.rbuf = append(h.rbuf, xmsg.Input...)
                                h.cond.Signal()
                                h.Unlock()
                        }
                case "resize":
                        {
                                ev := remotecommand.TerminalSize{
                                        Width:  xmsg.Cols,
                                        Height: xmsg.Rows}
                                h.resizeEvent <- ev
                        }
                default:
                        log.Println("other xmsg.MsgType: not input or resize.")
                }
        }
}

func (h *WSStreamHandler) Read(b []byte) (size int, err error) {
        h.Lock()
        for len(h.rbuf) == 0 {
                h.cond.Wait()
        }
        size = copy(b, h.rbuf)
        h.rbuf = h.rbuf[size:]
        h.Unlock()
        return
}

func (h *WSStreamHandler) Write(b []byte) (size int, err error) {
        size = len(b)
        err = h.conn.WriteMessage(websocket.TextMessage, b)

        return
}

func (h *WSStreamHandler) Next() (size *remotecommand.TerminalSize) {
        ret := <-h.resizeEvent
        size = &ret

        return
}

func wsHandler(resp http.ResponseWriter, req *http.Request) {
        var (
                conn          *websocket.Conn
                sshReq        *rest.Request
                podName       string
                podNs         string
                containerName string
                executor      remotecommand.Executor
                handler       *WSStreamHandler
                err           error
        )

        // 解析GET参数
        if err = req.ParseForm(); err != nil {
                return
        }

        var userToken string = req.Form.Get("userToken")
        podNs = req.Form.Get("namespace")
        podName = req.Form.Get("pod")
        containerName = req.Form.Get("container")

        // 得到websocket长连接
        upgrader := websocket.Upgrader{}
        if conn, err = upgrader.Upgrade(resp, req, nil); err != nil {
                log.Fatalf("error creating ws conn: %v", err)
        }

        sshReq = clientset.CoreV1().RESTClient().Post().
                Resource("pods").
                Name(podName).
                Namespace(podNs).
                SubResource("exec").
                VersionedParams(&v1.PodExecOptions{
                        Container: containerName,
                        Command:   []string{"sh"},
                        Stdin:     true,
                        Stdout:    true,
                        Stderr:    true,
                        TTY:       true,
                }, scheme.ParameterCodec)
        // 创建到容器的连接
        if executor, err = remotecommand.NewSPDYExecutor(kconfig, "POST", sshReq.URL()); err != nil {
                log.Fatalf("error creating spdy executor: %v", err)
        }

        log.Println("connectingi to pod...")
        handler = &WSStreamHandler{
                conn:        conn,
                resizeEvent: make(chan remotecommand.TerminalSize)}
        handler.cond = sync.NewCond(handler)

        // run loop to fetch data from ws client
        go handler.Run(userToken)

        err = executor.Stream(remotecommand.StreamOptions{
                Stdin:             handler,
                Stdout:            handler,
                Stderr:            handler,
                TerminalSizeQueue: handler,
                Tty:               true,
        })
        if err != nil {
                log.Println("error executor: ", err)
                handler.Write([]byte("亲,你选择的容器不存在!bye~bye~"))
                handler.conn.Close()
                return
        }

        return
}

func main() {
        staticDir = "./public"
        fs := http.FileServer(http.Dir(staticDir))
        http.Handle("/", fs)

        var err error
        cfgpath := filepath.Join(homeDir(), ".kube/config")
        if kconfig, err = clientcmd.BuildConfigFromFlags("", cfgpath); err != nil {
                log.Fatalf("error creating k8s config: %v", err)
        }

        if clientset, err = kubernetes.NewForConfig(kconfig); err != nil {
                log.Fatalf("error creating clientset: %v", err)
        }

        // log.Printf("%v", clientset)
        http.HandleFunc("/terminal", wsHandler)

        log.Println("running...")
        http.ListenAndServe(":8000", nil)
}

func homeDir() string {
        if h := os.Getenv("HOME"); h != "" {
                return h
        }

        return os.Getenv("USERPROFILE") // windows
}

  

index.html(前端后端都作token验证,这样更安全)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="xterm/dist/xterm.css" />
    <script src="xterm/dist/xterm.js"></script>
    <script src="xterm/dist/addons/fit/fit.js"></script>
    <script src="xterm/dist/addons/winptyCompat/winptyCompat.js"></script>
    <script src="xterm/dist/addons/webLinks/webLinks.js"></script>
    <script src="jquery/jquery-3.4.1.min.js"></script>
    <script src="jquery/js.cookie-2.2.1.min.js"></script>
    <style>
        body {
            margin: 0;
        }
        #terminal {
            height: 100vh;
            width: 100vw;
        }
    </style>
</head>
<body>
<div id="terminal"></div>
<script>
    $(document).ready(function () {
        document.getElementById('terminal').innerHTML = ""
        var userToken = Cookies.get('k8s-access-token')
        var accessUrl = `http://localhost:8080/test.html?name=${userToken}`
        $.ajax({
            url: accessUrl,
            method: 'GET',
            success: function(data) {
                console.log('success!')
                openTerminal()
            },
            error: function(xhr) {
                console.log(accessUrl);
                // 导致出错的原因较多,以后再研究
                alert('error:' + JSON.stringify(xhr));
            }
        });
        
        // 新建终端
        function openTerminal() {
            const scrollBack = getUrlParam('scrollBack') || 1000
            // 获取要连接的容器信息
            const param = {
                namespace: getUrlParam('namespace'),
                pod: getUrlParam('pod'),
                container: getUrlParam('container') // must have this arg
            }
            document.title = `${param.container}@${param.pod}@${param.namespace}`
        
            // 创建终端
            // xterm配置自适应大小插件
            Terminal.applyAddon(fit);
            
            // 这俩插件不知道干嘛的, 用总比不用好
            Terminal.applyAddon(winptyCompat)
            Terminal.applyAddon(webLinks)
            
            const term = new Terminal({
                cursorBlink: true,
                scrollback: scrollBack
            })
        
            term.open(document.getElementById('terminal'))
            term.setOption('fontSize', 14)
        
            // 使用fit插件自适应terminal size
            term.fit()
            term.winptyCompatInit()
            term.webLinksInit()
            // 取得输入焦点
            term.focus()
        
            // 建立websocket连接
            const wsUrl = `ws://${location.host}/terminal`
            const url = `${wsUrl}?userToken=${userToken}&namespace=${param.namespace}&pod=${param.pod}&container=${param.container}`
            ws = new WebSocket(url)
        
            ws.onopen = function(event) {
                console.log("onopen")
            }
            ws.onclose = function(event) {
                console.log("onclose")
                alert('操作结束,远程关闭连接。')
            }
            ws.onmessage = function(event) {
                // 服务端ssh输出, 写到web shell展示
                term.write(event.data)
            }
            ws.onerror = function(event) {
                console.log("onerror")
                alert('远程连接错误,请重新进入。')
            }
        
            // 当浏览器窗口变化时, 重新适配终端
            window.addEventListener("resize", function () {
                term.fit()
        
                // 把web终端的尺寸term.rows和term.cols发给服务端, 通知sshd调整输出宽度
                var msg = {type: "resize", rows: term.rows, cols: term.cols}
                ws.send(JSON.stringify(msg))
        
                console.log(term.rows + "," + term.cols)
            })
        
            // 当向web终端敲入字符时候的回调
            term.on('data', function(input) {
                var msg = {type: "input", input: input}
                // 写给服务端, 由服务端发给container
                ws.send(JSON.stringify(msg))
            })
        }
        
        function getUrlParam(name) {
            let reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)")
            let r = window.location.search.substr(1).match(reg)
            if (r != null) return unescape(r[2])
            return null
        }
        
    })
</script>
</body>
</html>

test.html(测试一下,手工造数据,使用cookie在不同网页间传token,便于部署的解耦)

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>test</title>
        <script src="jquery/jquery-3.4.1.min.js"></script>
        <script src="jquery/js.cookie-2.2.1.min.js"></script>
    </head>
    <body>
        <form method="post" id="k8s-form" onsubmit="return check()">
            namespace:<input type="text"  value="default"  id="podNs">
            podName:<input  type="text" value="django-7fd6585c6b-s98q9" id="podName">
            containerName:<input  type="text" value="django" id="containerName">
            <input type="hidden" id="token" value="KJHIU77ukj" />
            
            <input id="ssh" type="button" value="ssh">
        </form>
        <script>
            $(document).ready(function () {
                $("#ssh").click(function() {
                    var token = document.getElementById("token").value;
                    Cookies.set('k8s-access-token', token)
                    // 获取要连接的容器信息
                    var hostDomain = "http://127.0.0.1:8000"
                    var podNs = document.getElementById("podNs").value;
                    var podName = document.getElementById("podName").value;
                    var containerName = document.getElementById("containerName").value;
                    var url =`${hostDomain}/?namespace=${podNs}&pod=${podName}&container=${containerName}`; 
                    window.open(url);
                });
            });
            
        </script>
    </body>
</html>

猜你喜欢

转载自www.cnblogs.com/aguncn/p/12512025.html