preface

In the internal use of Jenkins for CI/CD, project construction failure is often encountered. In general, problems can be known through the output of Jenkins construction console, but in some special cases, problems need to be checked on Jenkins server. At this time, we can only find operation and maintenance to debug. For the experience of developers, WE investigated Web terminal, which can provide container terminal for the development to troubleshoot problems when the construction fails.

Results show

Supports color highlighting, TAB key completion, and copy and paste. The experience is basically the same as that of ordinary Terminal.

Docker-based Web terminal implementation

Docker exec call

The first thing that comes to mind is to start a terminal with the Docker exec it Ubuntu /bin/bash command, and then interact with the front end with the standard input and output through the websocket.

Then I found that Docker provides API and SDK for development, through Go SDK can be very convenient to create a terminal process in Docker:

  • Installing the SDK
go get -u github.com/docker/docker/client@8c8457b0f2f8
Copy the code

New played tag did not follow the project go mod server semantics, so if you go directly to get the -u github.com/docker/docker/client default installation is 2017 dozen of a tag version, Here I directly found a commit ID on the master branch, see issue for specific reasons

  • Call the exec
package main

import (
	"bufio"
	"context"
	"fmt"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/client"
)

func main(a) {
	// Initialize the GO SDK
	ctx := context.Background()
	cli, err := client.NewClientWithOpts(client.FromEnv)
	iferr ! =nil {
		panic(err)
	}

	cli.NegotiateAPIVersion(ctx)

	// Run the /bin/bash command in the specified container
	ir, err := cli.ContainerExecCreate(ctx, "test", types.ExecConfig{
		AttachStdin:  true,
		AttachStdout: true,
		AttachStderr: true,
		Cmd:          []string{"/bin/bash"},
		Tty:          true,})iferr ! =nil {
		panic(err)
	}

	// Attach to the /bin/bash process created above
	hr, err := cli.ContainerExecAttach(ctx, ir.ID, types.ExecStartCheck{Detach: false, Tty: true})
	iferr ! =nil {
		panic(err)
	}
	/ / close the I/O
	defer hr.Close()
	/ / input
	hr.Conn.Write([]byte("ls\r"))
	/ / output
	scanner := bufio.NewScanner(hr.Conn)
	for scanner.Scan() {
		fmt.Println(scanner.Text())
	}
}
Copy the code

At this time, the input and output of docker terminal can be obtained, and the next step is to interact with the front end through websocket.

The front page

When we press ls on Linux terminal, we can see:

root@a09f2e7ded0d:/# ls
bin   dev  home  lib64  mnt  proc  run   srv  tmp  var
boot  etc  lib   media  opt  root  sbin  sys  usr
Copy the code

In fact, the string returned from standard output is:

[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
Copy the code

In this case, there is already a library called xterm.js, which is used to simulate Terminal, and we need to use this library to do Terminal display.

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

As you can see from the official example, it will display the corresponding special characters:

In this way, we only need to use term.write() to write out the terminal output when websocket is connected to the server, and then use the front-end input as the terminal input to achieve the function we need.

The idea is correct, but there is no need to write it by hand. Xterm.js already provides a webSocket plug-in for this purpose. All you need to do is transfer standard input and output over webSocket.

  • Install the xterm. Js
npm install xterm
Copy the code
  • Front-end page written based on VUE
<template>
  <div ref="terminal"></div>
</template>

<script>
/ / into the CSS
import "xterm/dist/xterm.css";
import "xterm/dist/addons/fullscreen/fullscreen.css";

import { Terminal } from "xterm";
// Adaptive plugins
import * as fit from "xterm/lib/addons/fit/fit";
// Full screen plug-in
import * as fullscreen from "xterm/lib/addons/fullscreen/fullscreen";
// Web link plug-in
import * as webLinks from "xterm/lib/addons/webLinks/webLinks";
/ / the websocket plug-in
import * as attach from "xterm/lib/addons/attach/attach";

export default {
  name: "Index".created() {
    // Install the plug-in
    Terminal.applyAddon(attach);
    Terminal.applyAddon(fit);
    Terminal.applyAddon(fullscreen);
    Terminal.applyAddon(webLinks);

    // Initialize the terminal
    const terminal = new Terminal();
    / / open the websocket
    const ws = new WebSocket(Ws: / / "127.0.0.1:8000 / terminal? container=test");
    // Bind to dom
    terminal.open(this.$refs.terminal);
    // Load the plug-interminal.fit(); terminal.toggleFullScreen(); terminal.webLinksInit(); terminal.attach(ws); }};</script>
Copy the code

Back-end Websocket support

The WebSocket module is not provided in the go standard library, so we use the official webSocket library.

go get -u github.com/gorilla/websocket
Copy the code

The core code is as follows:

// WebSocket handshake configuration, ignoring Origin detection
var upgrader = websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool {
		return true}},func terminal(w http.ResponseWriter, r *http.Request) {
	/ / the websocket handshake
	conn, err := upgrader.Upgrade(w, r, nil)
	iferr ! =nil {
		log.Error(err)
		return
	}
	defer conn.Close()

	r.ParseForm()
	// Get the container ID or name
	container := r.Form.Get("container")
	// Execute exec to get the connection to the container terminal
	hr, err := exec(container)
	iferr ! =nil {
		log.Error(err)
		return
	}
	// Close the I/O stream
	defer hr.Close()
	// Exit the process
	defer func(a) {
		hr.Conn.Write([]byte("exit\r"))
	}()

	// Forward input/output to websocket
	go func(a) {
		wsWriterCopy(hr.Conn, conn)
	}()
	wsReaderCopy(conn, hr.Conn)
}

func exec(container string) (hr types.HijackedResponse, err error) {
	// Run the /bin/bash command
	ir, err := cli.ContainerExecCreate(ctx, container, types.ExecConfig{
		AttachStdin:  true,
		AttachStdout: true,
		AttachStderr: true,
		Cmd:          []string{"/bin/bash"},
		Tty:          true,})iferr ! =nil {
		return
	}

	// Attach to the /bin/bash process created above
	hr, err = cli.ContainerExecAttach(ctx, ir.ID, types.ExecStartCheck{Detach: false, Tty: true})
	iferr ! =nil {
		return
	}
	return
}

// Forward the output of the terminal to the front-end
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])
			iferr ! =nil {
				return}}iferr ! =nil {
			return}}}// Forward the input from the front end to the terminal
func wsReaderCopy(reader *websocket.Conn, writer io.Writer) {
	for {
		messageType, p, err := reader.ReadMessage()
		iferr ! =nil {
			return
		}
		if messageType == websocket.TextMessage {
			writer.Write(p)
		}
	}
}
Copy the code

conclusion

The above completes a simple Docker Web Terminal function. After that, you only need to pass the container ID or container name through the front end to open the specified container for interaction.

Full code: github.com/monkeyWie/d…

This article was first published on my blog: monkeywie.cn. Share the knowledge of JAVA, Golang, front-end, Docker, K8S and other dry goods from time to time.