經過瀏覽器鏈接docker容器

前言

在公司內部使用 Jenkins 作 CI/CD 時,常常會碰到項目構建失敗的狀況,通常狀況下經過 Jenkins 的構建控制檯輸出均可以瞭解到大概發生的問題,可是有些特殊狀況開發須要在 Jenkins 服務器上排查問題,這個時候就只能找運維去調試了,爲了開發人員的體驗就調研了下 web terminal,可以在構建失敗時提供容器終端給開發進行問題的排查。css

效果展現

支持顏色高亮,支持tab鍵補全,支持複製粘貼,體驗基本上與日常的 terminal 一致。前端

基於 docker 的 web terminal 實現

docker exec 調用

首先想到的就是經過docker exec -it ubuntu /bin/bash命令來開啓一個終端,而後將標準輸入和輸出經過 websocket 與前端進行交互。vue

而後發現 docker 有提供 API 和 SDK 進行開發的,經過 Go SDK能夠很方便的在 docker 裏建立一個終端進程:linux

  • 安裝 sdk
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

  • 調用 exec
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 傳輸就能夠了。

  • 安裝 xterm.js
npm install xterm
  • 基於 vue 寫的前端頁面
<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>

後端 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\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 IDcontainer name就能夠打開指定的容器進行交互了。

完整代碼:https://github.com/monkeyWie/...

我是 MonkeyWie,歡迎掃碼👇👇關注!不按期在公衆號中分享 JAVAGolang前端dockerk8s等乾貨知識。

wechat

相關文章
相關標籤/搜索