Golang+Vue2从零开始搭建K8S后台管理系统(6)——web终端实现pod shell
上一章通过http chunked 长连接实现了pod的日志读取和展现;本章将通过前端的xterm.js库以及websocket来实现pod终端在浏览器页面上的实现。至此,我们就完成了pod shell的实现。在下一章中,我们将在此基础上进一步实现node shell。其实原理是类似的,基本思路就是通过ssh开启一个会话,并同样以其构造实现io.Reader和io.Writer接口的结构体。
目录
概述
上一章通过http chunked 长连接实现了pod的日志读取和展现;
本章将通过前端的xterm.js库以及websocket来实现pod终端在浏览器页面上的实现。
远程执行pod命令的基本方法
首先构建一个请求
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,
)
如果是一次性执行命令,我们会将command以
[]string{"你要执行的命令"}
的值传入;
以这个请求获取到url地址来创建远程命令执行对象
exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
值得一提的是SPDY是谷歌开发的通信协议,在TCP层之上,HTTP/2的关键功能主要来自SPDY技术,换言之,SPDY的成果被采纳而最终演变为HTTP/2。
通过前步获得到的远程命令执行对象,我们可以开启流式传输,并制定标准输入/输出的方向
exec.Stream(remotecommand.StreamOptions{
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
Tty: true,
})
执行,即可在Terminal控制台观察到相应的结果返回。
后端websocket的实现
我们知道,当我们在前端键入指令,传送到后端时。这个指令不可能是写死的,这就要求前后有一定的交互,并且后端执行后的结果需要通知到前端,来对页面进行渲染。所以我们需要使用到websocket。在上一节中我们将远程执行命令对象的返回结果定向到了os标准输出,也就是控制台,这一节我们来尝试将它通过 websocket 客户端返回给前端。
本文以gin框架来演示;
从*gin.Context中获取我们必要的参数,即交互的容器
ns := c.Query("ns")
pod := c.Query("name")
container := c.Query("cname")
老套路,把http连接升级成websocket连接
wsClient, err := wscore.Upgrader.Upgrade(c.Writer, c.Request, nil)
接下来很关键,我们需要把这个ws客户端取代os标准输出,作为远程命令对象的输出。
查看StreamOptions的成员变量
type StreamOptions struct { Stdin io.Reader Stdout io.Writer Stderr io.Writer Tty bool TerminalSizeQueue TerminalSizeQueue }
因此,我们需要通过ws客户端构建对象,并且实现Reader/Writer接口,其实就是往ws客户端里面灌数据/取数据
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
}
至此,后端部分就完成了;通过对应接口访问进来,远程命令执行对象会将通过ws收到的消息作为命令去执行,并通过ws连接把命令执行结果返回回去。
完整代码:
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
}
前端
附前端部分完整代码
<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>
当我们在终端输入框键入命令并回车结束时,会触发终端组件的onData事件,onData事件会通过ws客户端将这部分数据发送给后端。后端将处理完成的结果通过ws连接返回给前端,触发了ws客户端的onmessage事件,然后调用终端组建的write方法,将结果再打印出来。
总结
至此,我们就完成了pod shell的实现。
在下一章中,我们将在此基础上进一步实现node shell。其实原理是类似的,基本思路就是通过ssh开启一个会话,并同样以其构造实现io.Reader和io.Writer接口的结构体。
更多推荐
所有评论(0)