利用kubernetes exec接口实现任意容器的web-terminal

网友投稿 1760 2022-05-30

一、   Kubectl exec命令登录指定容器

如果你用过k8s,那么kubectl exec 命令一定不要错过。简单的敲上:

kubectl exec -it pod名 -- /bin/sh

就可以登录到任意节点的指定的容器里面,效果和使用ssh登录到一台机器进行操作一模一样,非常的方便。

那有没有想过:

这个功能是怎么实现的呢?

能不能在Web网页上面,直接拥有这个功能呢?

接下来,我们一一解读。

二、   Kubectl exec实现

1.     最底层实现:Docker容器的exec命令

K8s实现的“进入某个容器”的功能,底层本质是Docker容器通过exec进入容器的扩展。即从本机容器,扩展为任意节点的容器。

所以咱们先看看Docker怎么通过exec进入容器的呢?

docker exec -it 容器id /bin/sh

通过上面的命令,就可以进入到容内部。 本质是新建了一个“与目标容器,共享namespace的”新的shell进程。所以该shell进程,看到的世界,就是容器内的世界了。

那么K8s要做的就是,跨节点利用Docker的这个功能。

2.     Kubectl到容器的超长路径

从kubectl命令行工具,到容器内部,这里经过的网络路径其实是很长的。如下:

因为exec命令行,是实时交互的。即输入和输出,实时发生。

所以K8s选择了使用 类似Websocket 这种双向实时通信的协议,来传递输入/输出内容。

Kubectl <---(双向实时协议)---> Kube-Api-Server <---(双向实时协议)--->节点kubelet

3.     Kubectl实现exec代码简析

通过简单查询 kubectl 的源码:

import "k8s.io/client-go/tools/remotecommand"

//这里初始化了一个 remote-cmd 的对象

exec, err := remotecommand.NewSPDYExecutor(config, method, url)

if err != nil {

return err

}

//这里开始,将输入输出,进行实时传递(Stream)

return exec.Stream(remotecommand.StreamOptions{

Stdin:             stdin,

Stdout:            stdout,

Stderr:            stderr,

Tty:               tty,

TerminalSizeQueue: terminalSizeQueue,

})

这里可以看到,kubectl使用了一个叫 SPDY 的协议去连K8s的API-server。 这里的SPDY协议是Google公司搞的,基本类似Websocket可以进行双向实时传输,但是这个协议已经被淘汰了,被HTTP2所替代。见k8s的issue:SPDY is deprecated. Switch to HTTP/2。

(https://github.com/Kubernetes/kubernetes/issues/7452)

好在K8s的API-server除了支持 SPDY协议,也支持Websocket协议。

(https://github.com/kubernetes/kubernetes/issues/89163)

三、   网页web-terminal直连容器

有了前面的背景知识,那么如果想在web网页中,实现exec实时登录到容器里面。那么可以有以下思路:

首先,SPDY是个淘汰的协议,所以前端JS代码可供参考的很少。而前端对Websocket的支持则很广泛。所以咱们Web侧选择使用Websocket。

于是,实现的方案有如下几种:

1.     web网页使用Websocket直连K8s

这种场景,虽然看着最直接,但是适用场景反而有限。 因为权限隔离问题,一般情况你不可能让前端获得最大的k8s权限,允许进入任意容器中。

2.     Web网页经过一个后端,中转至K8s

在前端和K8s的中间,增加一个自研的Server,可以很好的控制权限隔离,封装K8s到业务的转换。

那么对于自研Server来说,它就是一个类似Proxy的程序。其中,[Web<---->自研Server]这一段肯定是使用Websocket协议。但是[自研Server<---->K8s]这一段,则有2种实现:1种是直接使用Websocket协议。第2种是使用SPDY协议(即利用 remotecommand代码实现)。

下面分2种场景分析。

3.     中转Server通过SPDY与K8s相连

因为kubectl代码中有exec的实现(通过SPDY),所以中转Server直接借鉴,也是很方便。

这种实现方案,可以参考:https://github.com/jcops/k8-web-terminal

整体Server使用GO语言的beego框架,简单好用。前段连接使用Websocket,后段连接使用了SPDY协议。

不过经过代码分析,感觉后段连接的实现不如纯Websocket转发简洁。所以这里更推荐下一种实现方式。

4.     中转Server通过Websocket与K8S相连

因为SPDY协议已经被淘汰了,所以直接使用Websocket实现,显得更高大上,并且代码也更简洁。

这里没有找到参考实现的仓库,直接贴一点我们自己的代码实现。使用的包是:

import " github.com/gorilla/websocket"

后段连接主要代码逻辑:

// Server去连接K8s,得到websocket的连接

ws, _, err := websocket.DefaultDialer.Dial(addr, h)

// 与前端的websocket,进行proxy桥接

go k8stoweb(connFrontEnd, connBackEnd, errFrontEnd) go webtok8s(connBackEnd, connFrontEnd, errBackEnd)

// 其中,桥接函数如下

func replicateWebSocket (dst, src *websocket.Conn, errc chan error) { for { msgType, msg, err1 := src.ReadMessage() if err1 != nil { m := websocket.FormatCloseMessage(websocket.CloseNormalClosure, fmt.Sprintf("%v", err1)) if e, ok := err1.(*websocket.CloseError); ok { if e.Code != websocket.CloseNoStatusReceived { m = websocket.FormatCloseMessage(e.Code, e.Text) } } errc <- err1 _ = dst.WriteMessage(websocket.CloseMessage, m) break } err1 = dst.WriteMessage(msgType, msg) if err1 != nil { errc <- err1 break } } }

这种实现,后段转发比SPDY那个参考仓库更简洁,我们自己选用了此方式。

四、   K8s的Websocket协议,是有扩展的!

中转Proxy说完,我们要好好说道一下这个前端。因为前端是使用Websocket,经过proxy中转,直接到达K8s。所以相当于直接与K8s的Websocket协议互连。

所以这里就要引出实现中,遇到的最大的坑。即:K8s的exec在使用Websocket协议时,是有扩展的,并且扩展规则是K8s自己设置的规则。 我们以 用户敲下“ls”命令到容器,然后容器list文件列表为例来说明。

如果你直接发送”ls”内容,那么肯定是不通的。因为K8s根本不认这种“输入”。

K8s认为websocket的报文内容,有“频道”的。不然一条cmd命令行执行后,用户无法判断响应的内容是 stdout,还是 stderr。所以k8s这么约定:

1.     websocket报文内容的第一个字节,用来表示“频道”:

第一字节值

其余内容含义

0

标准输入

利用kubernetes exec接口实现任意容器的web-terminal

1

标准输出

2

标准错误

3

服务端异常信息

4

terminal窗口大小调整resize

参考:https://www.cnblogs.com/a00ium/p/10905279.html

不过文章中,调整窗口的行为,没有提。最终是在

https://github.com/kubernetes-ui/container-terminal/blob/ba560d4f715f405beb0a64bab8fb29a21aac2671/container-terminal.js#L152

看到的。前端调整窗口时,需要这么发送信息给K8s。

2.     频道的内容,需要使用Base64进行编码!

即要发送“ls”命令,需要向K8s发送的内容为:

sendMsg := "0" + base64.Encoding("ls")

这样发送才行,K8s才认为是收到”ls”命令。

收到响应,要先去掉第一个字节,然后再进行base64解码。

Ps:这里推荐一个websocket调试网站:http://coolaf.com/tool/chattest

用来连自己的中转Proxy,比较方便。

3.     前端JS的实现

因为中间的“自研Server”主要是进行中转Proxy,所以刚才提到的K8s接口内容中,首字节频道,以及响应的编码,其实都是交由前端来处理的。

这里可以直接参考k8s的web-ui的实现:https://github.com/kubernetes-ui/container-terminal

其中的container-terminal.js文件中,主要实现如下:

//发送内容

ws.send("0" + utf8_to_b64(data));

//接收内容

switch(ev.data[0]) {

case '1':

case '2':

case '3':

term.write(b64_to_utf8(data));

break;

}

五、   总结

到此,如果你想自己实现K8s的web-terminal,并且增加各种权限控制之类的业务逻辑,应该是没有障碍了。还有哪里需要补充的也欢迎交流。

Docker Kubernetes web前端

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:【上电即上华为云】华为云smart智联Cat.1+PLC无线网关_3121N-IED_MC615-CN-L610-CN
下一篇:C库函数与系统函数
相关文章