Golang+Vue2 builds K8S background management system from scratch (6)——web terminal implements pod shell

Table of contents

overview

The basic method of remotely executing pod commands

Implementation of backend websocket

front end

Summarize


overview

In the previous chapter, pod log reading and display was realized through http chunked long connection;

This chapter will implement the pod terminal on the browser page through the front-end xterm.js library and websocket.

The basic method of remotely executing pod commands

First build a request

	option := &v1.PodExecOptions{
		Container: container,
		Command:   command,
		//如果是一次性执行命令"sh -c ls"这种就关闭stdin(打开也不影响)
		Stdin:  true,
		Stdout: true,
		Stderr: true,
		//终端
		TTY: true,
	}
	//构建一个地址
	req := client.CoreV1().RESTClient().Post().Resource("pods").
		Namespace(ns).
		Name(pod).
		SubResource("exec").
		Param("color", "false").
		VersionedParams(
			option,
			scheme.ParameterCodec,
		)

If it is a one-time execution command, we will command to

[]string{"The command you want to execute"}

The value is passed in;

Get the url address with this request to create a remote command execution object

exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())

It is worth mentioning that SPDY is a communication protocol developed by Google. On top of the TCP layer, the key functions of HTTP/2 mainly come from SPDY technology. In other words, the results of SPDY were adopted and eventually evolved into HTTP/2.

Through the remote command execution object obtained in the previous step, we can start streaming and specify the direction of standard input/output

	exec.Stream(remotecommand.StreamOptions{ 
			Stdin:  os.Stdin,
			Stdout: os.Stdout,
			Stderr: os.Stderr,
			Tty:    true,
		})

 Execute, and you can observe the corresponding result returned on the Terminal console.

Implementation of backend websocket

We know that when we type an instruction on the front end and send it to the back end. This command cannot be hard-coded, which requires a certain amount of interaction before and after, and the result after the back-end execution needs to be notified to the front-end to render the page. So we need to use websocket. In the previous section, we directed the return result of the remote execution command object to the os standard output, that is, the console. In this section, we try to return it to the front end through the websocket client.

This article uses the gin framework to demonstrate;

Get our necessary parameters from *gin.Context, that is, the interactive container

    ns := c.Query("ns")
	pod := c.Query("name")
	container := c.Query("cname")

 The old routine, upgrade the http connection to a websocket connection

    wsClient, err := wscore.Upgrader.Upgrade(c.Writer, c.Request, nil)

The next step is critical, we need to replace the os standard output with this ws client as the output of the remote command object.

View the member variables of StreamOptions

type StreamOptions struct {
   Stdin             io.Reader
   Stdout            io.Writer
   Stderr            io.Writer
   Tty               bool
   TerminalSizeQueue TerminalSizeQueue
}

Therefore, we need to build objects through the ws client and implement the Reader/Writer interface, which is actually filling/fetching data into the ws client

type WsShellClient struct {
	client *websocket.Conn
}

func NewWsShellClient(client *websocket.Conn) *WsShellClient {
	return &WsShellClient{client: client}
}

func (this *WsShellClient) Write(p []byte) (n int, err error) {
	err = this.client.WriteMessage(websocket.TextMessage,
		p)
	if err != nil {
		return 0, err
	}
	return len(p), nil
}
func (this *WsShellClient) Read(p []byte) (n int, err error) {

	_, b, err := this.client.ReadMessage()

	if err != nil {
		return 0, err
	}
	return copy(p, string(b)), nil
}

At this point, the back-end part is completed; through the corresponding interface access, the remote command execution object will execute the message received through ws as a command, and return the command execution result through the ws connection.

Full code:

func PodConnect(c *gin.Context) {
	//获取容器相关对应参数
	ns := c.Query("ns")
	pod := c.Query("name")
	container := c.Query("cname")
	//升级http客户端到ws客户端
	wsClient, err := wscore.Upgrader.Upgrade(c.Writer, c.Request, nil)
	if err != nil {
		log.Println(err)
		return
	}
	shellClient := wscore.NewWsShellClient(wsClient) //以ws客户端构建实现reader/writer接口的对象
    //构建远程执行命令对象
	err = helpers.HandleCommand(ns, pod, container, this.Client, this.Config, []string{"sh"}).
		Stream(remotecommand.StreamOptions{ //以流的方式来读取结果
			Stdin:  shellClient,
			Stdout: shellClient,
			Stderr: shellClient,
			Tty:    true,
		})
	return
}

front end

The complete code of the front end is attached

<template>

  <div
    style="min-height: 500px;
    padding: 10px"
  >
    <div style="padding-left: 20px;padding-top:30px">
    容器: 
      <el-select  @change="containerChange"  placeholder="选择容器"
                        v-model="selectedContainer">
        <el-option v-for="c in containers "
                  :label="c.Name"
                  :value="c.Name"/>
      </el-select>
    </div>
    <div id="terminal" ref="terminal"></div>
  </div>

</template>
<script>
  import { Terminal } from "xterm";
  import "xterm/css/xterm.css";
  import { getPodContainers } from "@/api/pod";

  export default {

    data(){
      return {
        Name: "",
        NameSpace: "",
        containers: [],
        selectedContainer: "",
        rows: 40,
        cols: 100,
        term:null,//终端对象
        ws:null, //ws 客户端
        wsInited:false  //是否初始化完毕
      }
    },
    created() {
      this.Name = this.$route.params.name
      this.NameSpace = this.$route.params.ns
      if(this.Name===undefined||this.NameSpace===undefined){
        alert("错误的参数!")
      } else {
        getPodContainers(this.NameSpace,this.Name).then(rsp=>{
          this.containers=rsp.data
        })
      }
    },
    methods:{
      containerChange(){
        this.initWS()// 初始化 websocket
        this.initTerm()
      },
      initTerm(){
        let term = new Terminal({
          rendererType: "canvas", //渲染类型
          rows: parseInt(this.rows), //行数
          cols: parseInt(this.cols), // 不指定行数,自动回车后光标从下一行开始
          convertEol: true, //启用时,光标将设置为下一行的开头
          disableStdin: false, //是否应禁用输入。
          cursorStyle: "underline", //光标样式
          cursorBlink: true, //光标闪烁
          theme: {
            foreground: "#7e9192", //字体
            background: "#002833", //背景色
            cursor: "help", //设置光标
            lineHeight: 16
          }
        });
        // 创建terminal实例
        term.open(this.$refs["terminal"]);
        term.prompt = () => {
          term.writeln("\n\n Welcome. ");
          term.writeln("\n 正在初始化终端");
        };
        term.prompt();

        //回车触发
        term.onData((key)=> {
            if(this.wsInited){
              this.ws.send(key)
            }
        });


        this.term=term
      },
      //初始化 websocket 客户端
      initWS(){
        var ws = new WebSocket("ws://localhost:8080/podws?ns="+
        this.NameSpace+"&name="+this.Name+"&cname="+this.selectedContainer);
        ws.onopen = function(){
          console.log("open");
        }
        ws.onmessage = (e)=>{
          this.wsInited=true //初始化完毕
          this.term.write(e.data) //调用term的打印方法打印后端返回的消息结果
        }
        ws.onclose = function(e){
          console.log("close");
        }
        ws.onerror = function(e){
          console.log(e);
        }
        this.ws=ws
      }
    }

  }

</script>

When we type a command in the terminal input box and press Enter to end, the onData event of the terminal component will be triggered, and the onData event will send this part of the data to the backend through the ws client. The backend returns the processed result to the frontend through the ws connection, triggers the onmessage event of the ws client, and then calls the write method formed by the terminal to print out the result.

Summarize

So far, we have completed the implementation of the pod shell.

In the next chapter, we will further implement the node shell on this basis. In fact, the principle is similar. The basic idea is to open a session through ssh, and also use its structure to realize the structure of the io.Reader and io.Writer interfaces.

Guess you like

Origin blog.csdn.net/kingu_crimson/article/details/128015465