在公司內部使用 Jenkins 作 CI/CD 時,常常會碰到項目構建失敗的狀況,通常狀況下經過 Jenkins 的構建控制檯輸出均可以瞭解到大概發生的問題,可是有些特殊狀況開發須要在 Jenkins 服務器上排查問題,這個時候就只能找運維去調試了,爲了開發人員的體驗就調研了下 web terminal,可以在構建失敗時提供容器終端給開發進行問題的排查。css
支持顏色高亮,支持tab鍵補全,支持複製粘貼,體驗基本上與日常的 terminal 一致。前端
首先想到的就是經過docker exec -it ubuntu /bin/bash
命令來開啓一個終端,而後將標準輸入和輸出經過 websocket
與前端進行交互。vue
而後發現 docker 有提供 API 和 SDK 進行開發的,經過 Go SDK
能夠很方便的在 docker 裏建立一個終端進程:linux
go get -u github.com/docker/docker/client@8c8457b0f2f8
這個項目新打的 tag 沒有遵循 go mod server 語義,因此若是直接go get -u github.com/docker/docker/client
默認安裝的是 2017 年的打的一個 tag 版本,這裏我直接在 master 分支上找了一個 commit ID,具體緣由參考issuegit
package main import ( "bufio" "context" "fmt" "github.com/docker/docker/api/types" "github.com/docker/docker/client" ) func main() { // 初始化 go sdk ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv) if err != nil { panic(err) } cli.NegotiateAPIVersion(ctx) // 在指定容器中執行/bin/bash命令 ir, err := cli.ContainerExecCreate(ctx, "test", types.ExecConfig{ AttachStdin: true, AttachStdout: true, AttachStderr: true, Cmd: []string{"/bin/bash"}, Tty: true, }) if err != nil { panic(err) } // 附加到上面建立的/bin/bash進程中 hr, err := cli.ContainerExecAttach(ctx, ir.ID, types.ExecStartCheck{Detach: false, Tty: true}) if err != nil { panic(err) } // 關閉I/O defer hr.Close() // 輸入 hr.Conn.Write([]byte("ls\r")) // 輸出 scanner := bufio.NewScanner(hr.Conn) for scanner.Scan() { fmt.Println(scanner.Text()) } }
這個時候 docker 的終端的輸入輸出已經能夠拿到了,接下來要經過 websocket 來和前端進行交互。github
當咱們在 linux terminal 上敲下ls
命令時,看到的是:web
root@a09f2e7ded0d:/# ls bin dev home lib64 mnt proc run srv tmp var boot etc lib media opt root sbin sys usr
實際上從標準輸出裏返回的字符串倒是:docker
[0m[01;34mbin[0m [01;34mdev[0m [01;34mhome[0m [01;34mlib64[0m [01;34mmnt[0m [01;34mproc[0m [01;34mrun[0m [01;34msrv[0m [30;42mtmp[0m [01;34mvar[0m [01;34mboot[0m [01;34metc[0m [01;34mlib[0m [01;34mmedia[0m [01;34mopt[0m [01;34mroot[0m [01;34msbin[0m [01;34msys[0m [01;34musr[0m
對於這種狀況,已經有了一個叫xterm.js
的庫,專門用來模擬 Terminal 的,咱們須要經過這個庫來作終端的顯示。shell
var term = new Terminal(); term.open(document.getElementById("terminal")); term.write("Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ");
經過官方的例子,能夠看到它會將特殊字符作對應的顯示:npm
這樣的話只須要在 websocket 連上服務器時,將獲取到的終端輸出使用term.write()
寫出來,再把前端的輸入做爲終端的輸入就能夠實現咱們須要的功能了。
思路是沒錯的,可是不必手寫,xterm.js
已經提供了一個 websocket 插件就是來作這個事的,咱們只須要把標準輸入和輸出的內容經過 websocket 傳輸就能夠了。
npm install xterm
<template> <div ref="terminal"></div> </template> <script> // 引入css import "xterm/dist/xterm.css"; import "xterm/dist/addons/fullscreen/fullscreen.css"; import { Terminal } from "xterm"; // 自適應插件 import * as fit from "xterm/lib/addons/fit/fit"; // 全屏插件 import * as fullscreen from "xterm/lib/addons/fullscreen/fullscreen"; // web連接插件 import * as webLinks from "xterm/lib/addons/webLinks/webLinks"; // websocket插件 import * as attach from "xterm/lib/addons/attach/attach"; export default { name: "Index", created() { // 安裝插件 Terminal.applyAddon(attach); Terminal.applyAddon(fit); Terminal.applyAddon(fullscreen); Terminal.applyAddon(webLinks); // 初始化終端 const terminal = new Terminal(); // 打開websocket const ws = new WebSocket("ws://127.0.0.1:8000/terminal?container=test"); // 綁定到dom上 terminal.open(this.$refs.terminal); // 加載插件 terminal.fit(); terminal.toggleFullScreen(); terminal.webLinksInit(); terminal.attach(ws); } }; </script>
在 go 的標準庫中是沒有提供 websocket 模塊的,這裏咱們使用官方欽點的 websocket 庫。
go get -u github.com/gorilla/websocket
核心代碼以下:
// websocket握手配置,忽略Origin檢測 var upgrader = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } func terminal(w http.ResponseWriter, r *http.Request) { // websocket握手 conn, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Error(err) return } defer conn.Close() r.ParseForm() // 獲取容器ID或name container := r.Form.Get("container") // 執行exec,獲取到容器終端的鏈接 hr, err := exec(container) if err != nil { log.Error(err) return } // 關閉I/O流 defer hr.Close() // 退出進程 defer func() { hr.Conn.Write([]byte("exit\r")) }() // 轉發輸入/輸出至websocket go func() { wsWriterCopy(hr.Conn, conn) }() wsReaderCopy(conn, hr.Conn) } func exec(container string) (hr types.HijackedResponse, err error) { // 執行/bin/bash命令 ir, err := cli.ContainerExecCreate(ctx, container, types.ExecConfig{ AttachStdin: true, AttachStdout: true, AttachStderr: true, Cmd: []string{"/bin/bash"}, Tty: true, }) if err != nil { return } // 附加到上面建立的/bin/bash進程中 hr, err = cli.ContainerExecAttach(ctx, ir.ID, types.ExecStartCheck{Detach: false, Tty: true}) if err != nil { return } return } // 將終端的輸出轉發到前端 func wsWriterCopy(reader io.Reader, writer *websocket.Conn) { buf := make([]byte, 8192) for { nr, err := reader.Read(buf) if nr > 0 { err := writer.WriteMessage(websocket.BinaryMessage, buf[0:nr]) if err != nil { return } } if err != nil { return } } } // 將前端的輸入轉發到終端 func wsReaderCopy(reader *websocket.Conn, writer io.Writer) { for { messageType, p, err := reader.ReadMessage() if err != nil { return } if messageType == websocket.TextMessage { writer.Write(p) } } }
以上就完成了一個簡單的 docker web terminal 功能,以後只須要經過前端傳遞container ID
或container name
就能夠打開指定的容器進行交互了。
完整代碼:https://github.com/monkeyWie/...
我是 MonkeyWie,歡迎掃碼👇👇關注!不按期在公衆號中分享JAVA
、Golang
、前端
、docker
、k8s
等乾貨知識。