Table of contents
The basic method of remotely executing pod commands
Implementation of backend websocket
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.