前端vue后端GO利用client-go实现web terminal远程ssh到k8s的pod内部
前端vue后端GO利用client-go实现web terminal远程ssh到k8s的pod内部
·
参考:https://github.com/owenliang/k8s-client-go
一、简介
- websocket简介:https://www.ruanyifeng.com/blog/2017/05/websocket.html
- 前端通过websocket与后端GO程序进行连接,后端使用
github.com/gorilla/websocket
- GO程序通过
client-go
包连接到k8s中指定的pod内容器,构造出executor - 将executor的stdin,stdout,stderr连接到websocket
二、后端
后端代码地址:https://github.com/fanb129/Kube-CC/tree/main/service/ws/podSsh
处理websocket通信
// http升级websocket协议的配置
var wsUpgrader = websocket.Upgrader{
// 允许所有CORS跨域请求
CheckOrigin: func(r *http.Request) bool {
return true
},
}
// WsMessage websocket消息
type WsMessage struct {
MessageType int
Data []byte
}
// WsConnection 封装websocket连接
type WsConnection struct {
wsSocket *websocket.Conn // 底层websocket
inChan chan *WsMessage // 读取队列
outChan chan *WsMessage // 发送队列
mutex sync.Mutex // 避免重复关闭管道
isClosed bool
closeChan chan byte // 关闭通知
}
// 读取协程
func (wsConn *WsConnection) wsReadLoop(){}
// 发送协程
func (wsConn *WsConnection) wsWriteLoop(){}
// 发送消息
func (wsConn *WsConnection) WsWrite(messageType int, data []byte) (err error){}
// 读取消息
func (wsConn *WsConnection) WsRead() (msg *WsMessage, err error){}
// 关闭连接
func (wsConn *WsConnection) WsClose(){}
处理websocket与ssh之间的通信
// ssh流式处理器
type streamHandler struct {
wsConn *WsConnection
resizeEvent chan remotecommand.TerminalSize
}
// web终端发来的包
type xtermMessage struct {
MsgType string `json:"type"` // 类型:resize客户端调整终端, input客户端输入
Input string `json:"input"` // msgtype=input情况下使用
Rows uint16 `json:"rows"` // msgtype=resize情况下使用
Cols uint16 `json:"cols"` // msgtype=resize情况下使用
}
// Next executor回调获取web是否resize
func (handler *streamHandler) Next() (size *remotecommand.TerminalSize){}
// executor回调读取web端的输入
func (handler *streamHandler) Read(p []byte) (size int, err error) {}
// executor回调向web端输出
func (handler *streamHandler) Write(p []byte) (size int, err error){}
连接pod,监听路由
func PodWsSsh(resp http.ResponseWriter, req *http.Request) {
var (
wsConn *WsConnection
restConf *rest.Config
sshReq *rest.Request
podName string
podNs string
containerName string
executor remotecommand.Executor
handler *streamHandler
err error
)
// 解析GET参数
if err = req.ParseForm(); err != nil {
return
}
podNs = req.Form.Get("podNs")
podName = req.Form.Get("podName")
containerName = req.Form.Get("containerName")
fmt.Println(podNs, podName, containerName)
// 得到websocket长连接
if wsConn, err = InitWebsocket(resp, req); err != nil {
return
}
// 获取pods
// 获取k8s rest client配置
if restConf, err = clientcmd.BuildConfigFromFlags("", conf.KubeConfig); err != nil {
goto END
}
// URL长相:
// https://172.18.11.25:6443/api/v1/namespaces/default/pods/nginx-deployment-5cbd8757f-d5qvx/exec?command=sh&container=nginx&stderr=true&stdin=true&stdout=true&tty=true
sshReq = dao.ClientSet.CoreV1().RESTClient().Post().
Resource("pods").
Name(podName).
Namespace(podNs).
SubResource("exec").
VersionedParams(&v1.PodExecOptions{
Container: containerName,
Command: []string{"bash"},
Stdin: true,
Stdout: true,
Stderr: true,
TTY: true,
}, scheme.ParameterCodec)
// 创建到容器的连接
if executor, err = remotecommand.NewSPDYExecutor(restConf, "POST", sshReq.URL()); err != nil {
goto END
}
// 配置与容器之间的数据流处理回调
handler = &streamHandler{wsConn: wsConn, resizeEvent: make(chan remotecommand.TerminalSize)}
if err = executor.Stream(remotecommand.StreamOptions{
Stdin: handler,
Stdout: handler,
Stderr: handler,
TerminalSizeQueue: handler,
Tty: true,
}); err != nil {
goto END
}
return
END:
wsConn.WsClose()
}
三、前端
前端代码地址:https://github.com/fanb129/Kube-CC_vue/blob/master/src/components/Terminal/PodTerminal.vue
使用vue
<template>
<div>
<div class="ssh-container" ref="terminal"></div>
</div>
</template>
<script>
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'
import 'xterm/css/xterm.css'
import { debounce } from 'lodash'
const packResize = (cols, rows) =>
JSON.stringify({
type: 'resize',
cols: cols,
rows: rows
})
export default {
name: 'PodTerminal',
data() {
return {
initText: '连接中...\r\n',
first: true,
term: null,
fitAddon: null,
ws: null,
socketUrl: 'ws://127.0.0.1:8080/api/' + this.$route.query['r'],
option: {
lineHeight: 1.0,
cursorBlink: true,
cursorStyle: 'block', // 光标样式 'block' | 'underline' | 'bar'
fontSize: 18,
fontFamily: "Monaco, Menlo, Consolas, 'Courier New', monospace",
theme: {
background: '#181d28'
},
cols: 100 // 初始化的时候不要设置fit,设置col为较小值(最小为可展示initText初始文字即可)方便屏幕缩放
}
}
},
mounted() {
this.initTerm()
this.initSocket()
this.onTerminalResize()
this.onTerminalKeyPress()
},
beforeDestroy() {
this.removeResizeListener()
this.term && this.term.dispose()
},
methods: {
utf8_to_b64(rawString) {
return btoa(unescape(encodeURIComponent(rawString)));
},
b64_to_utf8(encodeString) {
return decodeURIComponent(escape(atob(encodeString)));
},
bytesHuman(bytes, precision) {
if (!/^([-+])?|(\.\d+)(\d+(\.\d+)?|(\d+\.)|Infinity)$/.test(bytes)) {
return '-'
}
if (bytes === 0) return '0';
if (typeof precision === 'undefined') precision = 1;
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'BB'];
const num = Math.floor(Math.log(bytes) / Math.log(1024));
const value = (bytes / Math.pow(1024, Math.floor(num))).toFixed(precision);
return `${value} ${units[num]}`
},
isWsOpen() {
return this.ws && this.ws.readyState === 1
},
initTerm() {
this.term = new Terminal(this.option)
this.fitAddon = new FitAddon()
this.term.loadAddon(this.fitAddon)
this.term.open(this.$refs.terminal)
// this.fitAddon.fit() // 初始化的时候不要使用fit
setTimeout(() => {
this.fitAddon.fit()
}, 500); // 必须延时处理
},
onTerminalKeyPress() {
this.term.onData(data => {
this.isWsOpen() && this.ws.send(JSON.stringify({
type: 'input',
// input: this.utf8_to_b64(data)
input: data
}))
})
},
// resize 相关
resizeRemoteTerminal() {
const { cols, rows } = this.term
console.log('列数、行数设置为:', cols, rows)
// 调整后端终端大小 使后端与前端终端大小一致
this.isWsOpen() && this.ws.send(packResize(cols, rows))
},
onResize: debounce(function () {
this.fitAddon.fit()
}, 500),
onTerminalResize() {
window.addEventListener('resize', this.onResize)
this.term.onResize(this.resizeRemoteTerminal)
},
removeResizeListener() {
window.removeEventListener('resize', this.onResize)
},
// socket
initSocket() {
this.term.write(this.initText)
this.ws = new WebSocket(this.socketUrl)
this.onOpenSocket()
this.onCloseSocket()
this.onErrorSocket()
this.term._initialized = true
this.onMessageSocket()
},
// 打开连接
onOpenSocket() {
this.ws.onopen = () => {
console.log('websocket 已连接')
// this.ws.send(JSON.stringify({ type: "addr", data: this.utf8_to_b64(this.ip + ':' + this.port) }));
// // socket.send(JSON.stringify({ type: "term", data: utf8_to_b64("linux") }));
// this.ws.send(JSON.stringify({ type: "login", data: this.utf8_to_b64(this.user) }));
// this.ws.send(JSON.stringify({ type: "password", data: this.utf8_to_b64(this.pwd) }));
this.term.reset()
setTimeout(() => {
this.resizeRemoteTerminal()
}, 500)
}
},
// 关闭连接
onCloseSocket() {
this.ws.onclose = () => {
console.log('关闭连接')
this.term.write("未连接, 刷新后重连...\r\n");
// setTimeout(() => {
// this.initSocket();
// }, 3000)
}
},
// 连接错误
onErrorSocket() {
this.ws.onerror = () => {
this.term.write('连接失败,请刷新!')
}
},
// 接收消息
onMessageSocket() {
this.ws.onmessage = res => {
console.log(res)
// const msg = JSON.parse(res.data)
const msg = res.data
// console.log(msg)
const term = this.term
// console.log("receive: " + data)
// 第一次连接成功将 initText 清空
if (this.first) {
this.first = false
term.reset()
term.element && term.focus()
this.resizeRemoteTerminal()
}
term.write(msg)
}
}
}
}
//
</script>
<style lang="scss">
body {
margin: 0;
padding: 0;
}
.ssh-container {
overflow: hidden;
height: 93vh;
border-radius: 4px;
background: rgb(24, 29, 40);
padding: 0px;
color: rgb(255, 255, 255);
.xterm-scroll-area::-webkit-scrollbar-thumb {
background-color: #b7c4d1;
/* 滚动条的背景颜色 */
}
}
</style>
四、展示
通过pod页面提供namespace、pod、container信息跳转到Terminal页面进行连接进入到pod内部的容器
更多推荐
已为社区贡献2条内容
所有评论(0)