参考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>
扫描二维码关注公众号,回复:
9899239 查看本文章