果果哥 發表於 2019-8-19 10:53:00

使用 client-go 实现 k8s webshell

<p><em>更好的阅读体验建议点击下方原文链接。</em><br>
原文地址:http://maoqide.live/post/cloud/kubernetes-webshell/ </p>
<p>通过 client-go 提供的方法,实现通过网页进入 kubernetes pod 的终端操作。</p>

<ul>
<li>client-go remotecommand</li>
<li>websocket</li>
<li>xterm.js</li>
</ul>
<h2 id="remotecommand">remotecommand</h2>
<p><code>k8s.io/client-go/tools/remotecommand</code> kubernetes client-go 提供的 remotecommand 包,提供了方法与集群中的容器建立长连接,并设置容器的 stdin,stdout 等。<br>
remotecommand 包提供基于 SPDY 协议的 Executor interface,进行和 pod 终端的流的传输。初始化一个 Executor 很简单,只需要调用 remotecommand 的 NewSPDYExecutor 并传入对应参数。<br>
Executor 的 Stream 方法,会建立一个流传输的连接,直到服务端和调用端一端关闭连接,才会停止传输。常用的做法是定义一个如下 <code>PtyHandler</code> 的 interface,然后使用你想用的客户端实现该 interface 对应的<code>Read(p []byte) (int, error)</code>和<code>Write(p []byte) (int, error)</code>方法即可,调用 Stream 方法时,只要将 StreamOptions 的 Stdin Stdout 都设置为 ptyHandler,Executor 就会通过你定义的 write 和 read 方法来传输数据。</p>
<pre><code class="language-golang">// 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(&amp;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,
        })
</code></pre>
<h2 id="websocket">websocket</h2>
<p>github.com/gorilla/websocket 是 go 的一个 websocket 实现,提供了全面的 websocket 相关的方法,这里使用它来实现上面所说的<code>PtyHandler</code>接口。<br>
首先定义一个 TerminalSession 类,该类包含一个 <code>*websocket.Conn</code>,通过 websocket 连接实现<code>PtyHandler</code>接口的读写方法,Next 方法在 remotecommand 执行过程中会被调用。</p>
<pre><code class="language-golang">// 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 := &lt;-t.sizeChan:
                return &amp;size
        case &lt;-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), &amp;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 &lt;- 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()
}
</code></pre>
<h2 id="xtermjs">xterm.js</h2>
<p>前端页面使用xterm.js进行模拟terminal展示,只要 javascript 监听 Terminal 对象的对应事件及 websocket 连接的事件,进行对应的页面展示和消息推送就可以了。</p>
<hr>
<p>具体实现参考 https://github.com/maoqide/kubeutil.</p><br><br>
来源:https://www.cnblogs.com/maoqide/p/11375825.html
頁: [1]
查看完整版本: 使用 client-go 实现 k8s webshell