使用 client-go 實現 k8s webshell


更好的閱讀體驗建議點擊下方原文鏈接。
原文地址:http://maoqide.live/post/cloud/kubernetes-webshell/

通過 client-go 提供的方法,實現通過網頁進入 kubernetes pod 的終端操作。

  • client-go remotecommand
  • websocket
  • xterm.js

remotecommand

k8s.io/client-go/tools/remotecommand kubernetes client-go 提供的 remotecommand 包,提供了方法與集群中的容器建立長連接,並設置容器的 stdin,stdout 等。
remotecommand 包提供基於 SPDY 協議的 Executor interface,進行和 pod 終端的流的傳輸。初始化一個 Executor 很簡單,只需要調用 remotecommand 的 NewSPDYExecutor 並傳入對應參數。
Executor 的 Stream 方法,會建立一個流傳輸的連接,直到服務端和調用端一端關閉連接,才會停止傳輸。常用的做法是定義一個如下 PtyHandler 的 interface,然后使用你想用的客戶端實現該 interface 對應的Read(p []byte) (int, error)Write(p []byte) (int, error)方法即可,調用 Stream 方法時,只要將 StreamOptions 的 Stdin Stdout 都設置為 ptyHandler,Executor 就會通過你定義的 write 和 read 方法來傳輸數據。

// PtyHandler
type PtyHandler interface {
	io.Reader
	io.Writer
	remotecommand.TerminalSizeQueue
}

// NewSPDYExecutor
req := kubeClient.CoreV1().RESTClient().Post().
		Resource("pods").
		Name(podName).
		Namespace(namespace).
		SubResource("exec")
req.VersionedParams(&corev1.PodExecOptions{
	Container: containerName,
	Command:   cmd,
	Stdin:     true,
	Stdout:    true,
	Stderr:    true,
	TTY:       true,
}, scheme.ParameterCodec)
executor, err := remotecommand.NewSPDYExecutor(cfg, "POST", req.URL())
if err != nil {
	log.Printf("NewSPDYExecutor err: %v", err)
	return err
}

// Stream
err = executor.Stream(remotecommand.StreamOptions{
		Stdin:             ptyHandler,
		Stdout:            ptyHandler,
		Stderr:            ptyHandler,
		TerminalSizeQueue: ptyHandler,
		Tty:               true,
	})

websocket

github.com/gorilla/websocket 是 go 的一個 websocket 實現,提供了全面的 websocket 相關的方法,這里使用它來實現上面所說的PtyHandler接口。
首先定義一個 TerminalSession 類,該類包含一個 *websocket.Conn,通過 websocket 連接實現PtyHandler接口的讀寫方法,Next 方法在 remotecommand 執行過程中會被調用。

// TerminalSession
type TerminalSession struct {
	wsConn   *websocket.Conn
	sizeChan chan remotecommand.TerminalSize
	doneChan chan struct{}
}

// Next called in a loop from remotecommand as long as the process is running
func (t *TerminalSession) Next() *remotecommand.TerminalSize {
	select {
	case size := <-t.sizeChan:
		return &size
	case <-t.doneChan:
		return nil
	}
}
// Read called in a loop from remotecommand as long as the process is running
func (t *TerminalSession) Read(p []byte) (int, error) {
	_, message, err := t.wsConn.ReadMessage()
	if err != nil {
		log.Printf("read message err: %v", err)
		return copy(p, webshell.EndOfTransmission), err
	}
	var msg webshell.TerminalMessage
	if err := json.Unmarshal([]byte(message), &msg); err != nil {
		log.Printf("read parse message err: %v", err)
		// return 0, nil
		return copy(p, webshell.EndOfTransmission), 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
	default:
		log.Printf("unknown message type '%s'", msg.Operation)
		// return 0, nil
		return copy(p, webshell.EndOfTransmission), fmt.Errorf("unknown message type '%s'", msg.Operation)
	}
}

// Write called from remotecommand whenever there is any output
func (t *TerminalSession) Write(p []byte) (int, error) {
	msg, err := json.Marshal(webshell.TerminalMessage{
		Operation: "stdout",
		Data:      string(p),
	})
	if err != nil {
		log.Printf("write parse message err: %v", err)
		return 0, err
	}
	if err := t.wsConn.WriteMessage(websocket.TextMessage, msg); err != nil {
		log.Printf("write message err: %v", err)
		return 0, err
	}
	return len(p), nil
}

// Close close session
func (t *TerminalSession) Close() error {
	return t.wsConn.Close()
}

xterm.js

前端頁面使用xterm.js進行模擬terminal展示,只要 javascript 監聽 Terminal 對象的對應事件及 websocket 連接的事件,進行對應的頁面展示和消息推送就可以了。


具體實現參考 https://github.com/maoqide/kubeutil.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM