1. 程式人生 > 其它 >docker 標準輸出_通過瀏覽器連線docker容器

docker 標準輸出_通過瀏覽器連線docker容器

技術標籤:docker 標準輸出

前言

在公司內部使用 Jenkins 做 CI/CD 時,經常會碰到專案構建失敗的情況,一般情況下通過 Jenkins 的構建控制檯輸出都可以瞭解到大概發生的問題,但是有些特殊情況開發需要在 Jenkins 伺服器上排查問題,這個時候就只能找運維去除錯了,為了開發人員的體驗就調研了下 web terminal,能夠在構建失敗時提供容器終端給開發進行問題的排查。

效果展示

e0ebf3876dca2348cd4a99ef5172bf9d.png

支援顏色高亮,支援tab鍵補全,支援複製貼上,體驗基本上與平常的 terminal 一致。

基於 docker 的 web terminal 實現

docker exec 呼叫

首先想到的就是通過docker exec -it ubuntu /bin/bash

命令來開啟一個終端,然後將標準輸入和輸出通過 websocket 與前端進行互動。

然後發現 docker 有提供 API 和 SDK 進行開發的,通過 Go SDK可以很方便的在 docker 裡建立一個終端程序:

  • 安裝 sdk
go get -u github.com/docker/docker/[email protected]

這個專案新打的 tag 沒有遵循 go mod server 語義,所以如果直接go get -u github.com/docker/docker/client預設安裝的是 2017 年的打的一個 tag 版本,這裡我直接在 master 分支上找了一個 commit ID,具體原因參考issue

  • 呼叫 exec
package mainimport (    "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"))    // 輸出    scanner := bufio.NewScanner(hr.Conn)    for scanner.Scan() {        fmt.Println(scanner.Text())    }}

這個時候 docker 的終端的輸入輸出已經可以拿到了,接下來要通過 websocket 來和前端進行互動。

前端頁面

當我們在 linux terminal 上敲下ls命令時,看到的是:

[email protected]:/# lsbin   dev  home  lib64  mnt  proc  run   srv  tmp  varboot  etc  lib   media  opt  root  sbin  sys  usr

實際上從標準輸出裡返回的字串卻是:

[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 的,我們需要通過這個庫來做終端的顯示。

var term = new Terminal();term.open(document.getElementById("terminal"));term.write("Hello from x1B[1;3;31mxterm.jsx1B[0m $ ");

通過官方的例子,可以看到它會將特殊字元做對應的顯示:

b6ba472d0dd00df8cfad5c77632929ce.png

這樣的話只需要在 websocket 連上伺服器時,將獲取到的終端輸出使用term.write()寫出來,再把前端的輸入作為終端的輸入就可以實現我們需要的功能了。

思路是沒錯的,但是沒必要手寫,xterm.js已經提供了一個 websocket 外掛就是來做這個事的,我們只需要把標準輸入和輸出的內容通過 websocket 傳輸就可以了。

  • 安裝 xterm.js
npm install xterm
  • 基於 vue 寫的前端頁面

後端 websocket 支援

在 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"))    }()    // 轉發輸入/輸出至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 IDcontainer name就可以開啟指定的容器進行互動了。

完整程式碼:https://github.com/monkeyWie/docker-web-terminal

本文首發於我的部落格:https://monkeywie.cn,歡迎收藏!不定期分享JAVAGolang前端dockerk8s等乾貨知識。