【GO】K8s 管理系统项目15[API部分--WebShell]

K8s 管理系统项目[API部分–WebShell]

1. Kubectl exec原理

【GO】K8s 管理系统项目15[API部分--WebShell]_第1张图片

2. 实现方法

通过client-go提供的方法,实现通过网页进入kubernetes pod 终端操作.

  • client-go remotecommand
  • websocket
  • xterm.js

3. Terminal

server/terminal.go

重写读写方法

package service

import (
	"encoding/json"
	"errors"
	"fmt"
	"k8s-plantform/config"
	"log"
	"net/http"
	"time"

	"github.com/gorilla/websocket"
	"github.com/wonderivan/logger"
	v1 "k8s.io/api/core/v1"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/tools/remotecommand"
)

var Terminal terminal

type terminal struct{}

// wshanlder
func (t *terminal) WsHandler(w http.ResponseWriter, r *http.Request) {
	//加载k8s配置
	conf, err := clientcmd.BuildConfigFromFlags("", config.KubeConfig)
	if err != nil {
		logger.Error("加载k8s配置失败, " + err.Error())
		return
	}
	//解析form入参,获取namespace,pod,container参数
	if err := r.ParseForm(); err != nil {
		logger.Error("解析参数失败, " + err.Error())
		return
	}
	namespace := r.Form.Get("namespace")
	podName := r.Form.Get("pod_name")
	containerName := r.Form.Get("container_name")
	logger.Info("exec pod: %s, container: %s, namespace: %s\n", podName, containerName, namespace)

	//new一个terminalsession
	pty, err := NewTerminalSession(w, r, nil)
	if err != nil {
		logger.Error("实例化TerminalSession失败, " + err.Error())
		return
	}
	//处理关闭
	defer func() {
		logger.Info("关闭TerminalSession")
		pty.Close()
	}()
	//组装post请求
	req := K8s.ClientSet.CoreV1().RESTClient().Post().
		Resource("pods").
		Name(podName).
		Namespace(namespace).
		SubResource("exec").
		VersionedParams(&v1.PodExecOptions{
			Stdin:     true,
			Stdout:    true,
			Stderr:    true,
			TTY:       true,
			Container: containerName,
			Command:   []string{"/bin/bash"},
		}, scheme.ParameterCodec)
	logger.Info("exec post request url: ", req)

	//升级SPDY协议
	executor, err := remotecommand.NewSPDYExecutor(conf, "POST", req.URL())
	if err != nil {
		logger.Error("建立SPDY连接失败, " + err.Error())
		return
	}
	//与kubelet建立stream连接
	err = executor.Stream(remotecommand.StreamOptions{
		Stdin:             pty,
		Stdout:            pty,
		Stderr:            pty,
		Tty:               true,
		TerminalSizeQueue: pty,
	})

	if err != nil {
		logger.Error("执行 pod 命令失败, " + err.Error())
		//将报错返回给web端
		pty.Write([]byte("执行 pod 命令失败, " + err.Error()))
		//标记关闭
		pty.Done()
	}
}

// 消息内容
type terminalMessage struct {
	Operation string `json:"operation"`
	Data      string `json:"data"`
	Rows      uint16 `json:"rows"`
	Cols      uint16 `json:"cols"`
}

// 交互的结构体,接管输入和输出
type TerminalSession struct {
	wsConn   *websocket.Conn
	sizeChan chan remotecommand.TerminalSize
	doneChan chan struct{}
}

// 初始化一个websocket.Upgrader类型的对象,用于http协议升级为ws协议
var upgrader = func() websocket.Upgrader {
	upgrader := websocket.Upgrader{}
	upgrader.HandshakeTimeout = time.Second * 2
	upgrader.CheckOrigin = func(r *http.Request) bool {
		return true
	}
	return upgrader
}()

// 创建TerminalSession类型的对象并返回
func NewTerminalSession(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*TerminalSession, error) {
	//升级ws协议
	conn, err := upgrader.Upgrade(w, r, responseHeader)
	if err != nil {
		return nil, errors.New("升级websocket失败," + err.Error())
	}
	//new
	session := &TerminalSession{
		wsConn:   conn,
		sizeChan: make(chan remotecommand.TerminalSize),
		doneChan: make(chan struct{}),
	}

	return session, nil
}

// 读数据的方法
// 返回值int是读成功了多少数据
func (t *TerminalSession) Read(p []byte) (int, error) {
	//从ws中读取消息
	_, message, err := t.wsConn.ReadMessage()
	if err != nil {
		log.Printf("读取消息错误: %v", err)
		return 0, err
	}
	//反序列化
	var msg terminalMessage
	if err := json.Unmarshal(message, &msg); err != nil {
		log.Printf("读取消息语法错误: %v", err)
		return 0, err
	}
	//逻辑判断
	switch msg.Operation {
	case "stdin":
		return copy(p, msg.Data), nil
	case "resize":
		t.sizeChan <- remotecommand.TerminalSize{Width: msg.Cols, Height: msg.Rows}
		return 0, nil
	case "ping":
		return 0, nil
	default:
		log.Printf("消息类型错误'%s'", msg.Operation)
		return 0, fmt.Errorf("消息类型错误'%s'", msg.Operation)
	}
}

// 写数据的方法,拿到apiserver的返回内容,向web端输出
func (t *TerminalSession) Write(p []byte) (int, error) {
	msg, err := json.Marshal(terminalMessage{
		Operation: "stdout",
		Data:      string(p),
	})
	if err != nil {
		log.Printf("写消息语法错误: %v", err)
		return 0, err
	}
	if err := t.wsConn.WriteMessage(websocket.TextMessage, msg); err != nil {
		log.Printf("写消息错误: %v", err)
		return 0, err
	}

	return len(p), nil
}

// 标记关闭的方法
func (t *TerminalSession) Done() {
	close(t.doneChan)
}

// 关闭的方法
func (t *TerminalSession) Close() {
	t.wsConn.Close()
}

// resize方法,以及是否退出终端
func (t *TerminalSession) Next() *remotecommand.TerminalSize {
	select {
	case size := <-t.sizeChan:
		return &size
	case <-t.doneChan:
		return nil
	}
}

4. main

main.go

package main

import (
	"fmt"
	"k8s-plantform/config"
	"k8s-plantform/controller"
	"k8s-plantform/db"
	"k8s-plantform/middle"
	"k8s-plantform/service"
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	// 初始化数据库
	db.Init()
	// 初始化k8s client
	service.K8s.Init()
	// 初始化gin
	r := gin.Default()
	// 加载jwt中间件
	r.Use(middle.JWTAuth())
	// 加载跨域中间件
	r.Use(middle.Cors())
	// 初始化路由
	controller.Router.InitApiRouter(r)
	//启动websocket
    // 它是异步执行,所有要放在gin程序(阻塞)启动前
	go func() {
		http.HandleFunc("/ws", service.Terminal.WsHandler)
		http.ListenAndServe(":8081", nil)
	}()
	// gin 程序启动
	fmt.Println("http://192.168.31.1:9091/")
	r.Run(config.ListenAddr)
	// 关闭数据库
	db.Close()
}

修改main后重启

5. 测试webshell

地址输入:

ws://localhost:8081/ws?pod_name=nginx-64f8cb6df4-5mtjg&container_name=nginx&namespace=default

点击webshell连接后输入,并点发送消息

{"operation":"stdin","data":"ls -l","rows":0,"cols":0}

再次输入

{"operation":"stdin","data":"\r","rows":0,"cols":0}

获得以下返回

【GO】K8s 管理系统项目15[API部分--WebShell]_第2张图片

至此后端部分已经完成了.下面就是前端部分

你可能感兴趣的:(Go,云原生,golang,kubernetes)