首页 > 其他分享 >基于client-go实现pod 交互式terminal

基于client-go实现pod 交互式terminal

时间:2023-04-23 14:11:09浏览次数:37  
标签:err nil terminal client ws go

基于client-go实现pod 交互式terminal

后端实现逻辑(golang)

package main

import (
	"errors"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/gorilla/websocket"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/tools/remotecommand"
	"log"
	"net/http"
	"strconv"
	"strings"
	"unicode/utf8"
)

//初始化k8s客户端
func initialClientSet(path string) (*kubernetes.Clientset, *rest.Config, error) {
	config, err := clientcmd.BuildConfigFromFlags("", path)
	if err != nil {
		log.Fatal(err)
	}

	ClientSet, err := kubernetes.NewForConfig(config)
	if err != nil {
		log.Fatal(err)
		return nil, nil, err
	}
	return ClientSet, config, err
}

func initialWS(c *gin.Context) (*websocket.Conn, error) {
	var upgrader = websocket.Upgrader{
		ReadBufferSize:  1024,
		WriteBufferSize: 1024,
		CheckOrigin: func(r *http.Request) bool {
			return true
		},
	}
	//将http协议提升为ws
	ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
	if err != nil {
		fmt.Println(err)
		return nil, err
	}
	return ws, err
}

//为remotecommand.StreamOptions提供方法
type streamHandler struct {
	ws          *websocket.Conn                 //ws
	inputMsg    chan []byte                     //客户端输入数据
	resizeEvent chan remotecommand.TerminalSize //窗口调整事件
}

//获取调整窗口事件
func (handler *streamHandler) Next() *remotecommand.TerminalSize {
	resize := <-handler.resizeEvent
	return &resize
}

//从ws获取客户端输入的数据
func (handler *streamHandler) Read(p []byte) (size int, err error) {
	data, ok := <-handler.inputMsg
	if !ok {
		return 0, errors.New("I/O data reading failed")
	}
	copy(p, data)
	return len(data), nil
}

//将标准输出、错误写入ws(客户端)
func (handler *streamHandler) Write(p []byte) (int, error) {
	// 处理非utf8字符
	if !utf8.Valid(p) {
		bufStr := string(p)
		buf := make([]rune, 0, len(bufStr))
		for _, r := range bufStr {
			if r == utf8.RuneError {
				buf = append(buf, []rune("@")...)
			} else {
				buf = append(buf, r)
			}
		}
		p = []byte(string(buf))
	}
	err := handler.ws.WriteMessage(websocket.TextMessage, p)
	return len(p), err
}

//将字符串转换为int类型
func ToInt(str string) int {
	data, err := strconv.Atoi(str)
	if err != nil {
		fmt.Println(err)
	}
	return data
}

//处理ws输入数据
func executeTask(handler *streamHandler) {
	for {
		_, msg, err := handler.ws.ReadMessage()
		if err != nil {
			return
		}
		//心跳检测
		if string(msg) == "ping" {
			continue
		}
		//调整窗口宽高
		if strings.Contains(string(msg), "resize") {
			resizeSlice := strings.Split(string(msg), ":")
			rows, _ := strconv.Atoi(resizeSlice[1])
			cols, _ := strconv.Atoi(resizeSlice[2])
			handler.resizeEvent <- remotecommand.TerminalSize{
				Width:  uint16(cols),
				Height: uint16(rows),
			}
			continue
		}
		handler.inputMsg <- msg
	}
}

func podTerminal(c *gin.Context) {
	podName := c.Query("podName")
	namespace := c.Query("namespace")
	containerName := c.Query("containerName")
	cols := c.Query("cols")
	rows := c.Query("rows")

	ClientSet, config, err := initialClientSet("./kube/config")
	if err != nil {
		return
	}

	//初始化请求体
	req := ClientSet.CoreV1().RESTClient().Post().
		Resource("pods").
		Name(podName). //podName
		Namespace(namespace). //namespace
		SubResource("exec").
		VersionedParams(&corev1.PodExecOptions{
			Container: containerName, //containerName
			Command:   []string{"bash"},
			Stdin:     true,
			Stdout:    true,
			Stderr:    true,
			TTY:       true, // 启用终端
		}, scheme.ParameterCodec)

	// http转SPDY,添加X-Stream-Protocol-Version等相关header并发送请求
	exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
	if err != nil {
		log.Println(err)
		return
	}

	ws, err := initialWS(c)
	defer func() {
		ws.Close()
		if err := recover(); err != nil {
			log.Println(err)
		}
	}()

	handler := &streamHandler{
		ws:          ws,
		inputMsg:    make(chan []byte, 1024),
		resizeEvent: make(chan remotecommand.TerminalSize, 1),
	}
	//将初次获取的窗口cols、rows写入channel
	handler.resizeEvent <- remotecommand.TerminalSize{Width: uint16(ToInt(cols)), Height: uint16(ToInt(rows))}

	//获取ws输入数据
	go executeTask(handler)

	if err := exec.Stream(remotecommand.StreamOptions{
		Stdin:             handler,
		Stdout:            handler,
		Stderr:            handler,
		Tty:               true,
		TerminalSizeQueue: handler,
	}); err != nil {
		ws.Close()
		return
	}
}

前端实现逻辑(vue3)

<template>
    <div id="terminal"></div>
</template>

<script>
  import { Terminal } from 'xterm'
  import { FitAddon } from 'xterm-addon-fit'
  import { AttachAddon } from 'xterm-addon-attach'
  import {onBeforeUnmount, onMounted } from "vue"
  import 'xterm/css/xterm.css'
   export default {
     name: 'web-terminal',
     setup(){
       //初始化ws连接
       let ws = new WebSocket("ws://localhost:9090?podName=xxxx?namespace=xxxx?containerName=xxx")
       ws.onopen = ()=>{
         console.log(Date(), 'onopen')
         heartCheck.start()
       }
       ws.onclose = ()=>{
         console.log(Date(), 'onclose')
         heartCheck.stop()
       }
       ws.onerror = ()=> {
         console.log(Date(), 'onerror')
       }

       //心跳检查
       const heartCheck = {
         timeout: 5000, // 5s发一次心跳
         //关闭心跳检查
         stop: function() {
           clearInterval(this.timer)
         },
         //开启心跳检查
         start: function() {
           this.timer = setInterval(function() {
             if (ws !== null && ws.readyState === 1) {
               ws.send('ping')
             }
           }, this.timeout)
         }
       }
       //页面挂载后初始化terminal功能
       onMounted(()=>{
         let webTerminal = document.getElementById('terminal')
         let terminal = new Terminal(
             {
               fontSize: 16
             }
         )
         let fitAddon = new FitAddon()
         terminal.loadAddon(fitAddon)
         terminal.open(webTerminal)
         try {
           fitAddon.fit()
         } catch (e) {
           console.error(e)
         }

         //加载attach插件,通过ws实现web终端与远程终端进行实时交互
         terminal.loadAddon(new AttachAddon(ws))

         //增加滚轮事件监听,用于调整web终端字体大小
         webTerminal.addEventListener("wheel", (e) => {
           if (e.ctrlKey) {
             e.preventDefault()
             if (e.deltaY < 0) {
               terminal.options.fontSize = ++self.fontSize
             } else {
               terminal.options.fontSize = --self.fontSize
             }
             try { fitAddon.fit() } catch (e) {/**/}
             if (ws !== null && ws.readyState === 1) {
               ws.send(`resize:${terminal.rows}:${terminal.cols}`)
             }
           }

           //为window添加窗口大小调整事件,用于实时调整终端窗口
           window.addEventListener('resize', () => {
             webTerminal.style.height = document.documentElement.clientHeight + 'px'
             try { fitAddon.fit() } catch (e) {/**/}
             if (ws !== null && ws.readyState === 1) {
               ws.send(`resize:${terminal.rows}:${terminal.cols}`)
             }
           })
         })
         
         onBeforeUnmount(()=>{
           if (ws !== null) {
             ws.close()
           }
           if (terminal !== null) {
             terminal.dispose()
           }
         })
       })

     }
   }
</script>

<style>
   #terminal{
     position: absolute;
     top: 0; right: 0; bottom: 0; left: 0;
   }
</style>

标签:err,nil,terminal,client,ws,go
From: https://www.cnblogs.com/duyixu/p/17346370.html

相关文章

  • Go语言入门10(异常处理)
    异常处理panic异常处理​ 如果出现了panic异常,那么会停止当前函数的运行,然后会找recover()方法,如果没有的话,就会报错退出程序,如果有就会执行recover的方法体中的方法​ 我们可以使用defer延迟处理函数来捕获panic异常,用recover()来从错误场景中恢复,必须的在defer修饰的方法中......
  • jmeter中的java请求 用httpclient写的http请求 及参数化
    首先,jmeter中的sample的原理: jmeter 中的java 请求,sample 原理,java testjmeter自带的包,把包放在类路径下面,通过反射机制,通过反射机制扫出来。 先导入五个jar包  packagecom.young.testing91;importjava.io.IOException;importorg.apache.http.client.C......
  • 业务接口造数据(httpclient)
    导入httpclientjar包创建maven工程 httpclient发送get请求packagecom.testing91;importjava.io.BufferedReader;importjava.io.IOException;importjava.io.InputStreamReader;importorg.apache.http.client.ClientProtocolException;importorg.apache.http.client......
  • Django全栈进阶之路1 Django4下载与安装
    python下载安装:下载网址:https://www.python.org/downloads/ 安装方法:https://www.cnblogs.com/beichengshiqiao/p/16153586.html新版的python一般无需配置环境,在安装的时候勾选带有AddPython3.10toPATH的选项即可,如果需要手动配置的,参考:https://www.cnblogs.com/beicheng......
  • 基于django+ansible+webssh运维自动化管理系统
    基于django+ansible+webssh运维自动化管理系统 前言最初开发这个基于Djangoansible运维自动化管理系统的想法其实从大学时候就已经有了,但是苦于技术原因和没有线上环境原因一直没有开发,现在有了这个技术和环境之后开始着手开发了这个项目,项目难点在于你要理解如何设计数据库,......
  • django前后端连接数据库的增删查改
    目录配置修改1.templates目录修改路径2.如果要添加新的应用的话则需要在installed_apps里面加上去3.static静态文件的配置成动态文件4.连接数据库5.前期发送post请求需要注释掉配置文件中的某一行urls.py存储网址后缀与函数名对应的关系(开设接口)数据库数据数据展现功能1.t......
  • go语言context.Context
    go语言context.Contextcontext是Golang从1.7版本引入的一个标准库。它使得一个request范围内所有goroutine运行时的取消可以得到有效的控制。当最上层的goroutine因为某些原因执行失败时,下层的Goroutine由于没有接收到这个信号所以会继续工作;但是当我们正确地使用conte......
  • GoodSync(最好的文件同步软件)
    GoodSync是一款使用创新的最好的文件同步软件,可以在你的台式机、笔记本、USB外置驱动器等设备直接进行数据文件同步软件。GoodSync将高度稳定的可靠性和极其简单的易用性完美结合起来,可以实现两台电脑、电脑与云端网盘、电脑和远程FTP服务器、电脑与U盘之间的数据和文件......
  • golang实现RPC
      一、RPC工作流程:摘自《goweb编程》二、go支持三个级别的RPC(HTTP,TCP,JSONRPC)三、实现http的RPC实例:3.1GORPC的函数只有符合以下条件才能被远程访问函数必须是首字母是大写必须有两个首字母大写的参数第一个参数是接收的参数,第二个参数是返回给客户端的参数,第二......
  • Django框架基础5
    本节知识重点:  1、判断变量值是否相等(equal)  2、extends模板继承标签  3、load加载标签或过滤器  4、模板继承的应用(block与extends)  5、路由分发函数(include)  6、url标签实现反向解析(先app_name='index',再{%url%})  7、reverse()函数实现反向解析(......