k8s 远程进入容器 terminal
k8s 实现的“进入某个容器”的功能,底层本质是 Docker 容器通过 exec 进入容器的扩展。本质是新建了一个“与目标容器,共享 namespace 的”新的 shell 进程。所以该 shell 进程,看到的世界,就是容器内的世界了。
通过 client-go 提供的方法,实现通过网页进入 kubernetes 任意容器的终端操作
remotecommand
http://k8s.io/client-go/tools/remotecommand
是 kubernetes client-go 提供的 remotecommand 包,提供了方法与集群中的容器建立长连接,并设置容器的 stdin,stdout 等。
remotecommand 包提供基于 SPDY 协议的 Executor interface,进行和 pod 终端的流的传输。初始化一个 Executor 很简单,只需要调用 remotecommand 的 NewSPDYExecutor 并传入对应参数。
func main() {
config, err := clientcmd.BuildConfigFromFlags("", "kubeconfig")
if err != nil {
log.Fatal(err)
}
client, err := kubernetes.NewForConfig(config)
if err != nil {
log.Fatal(err)
}
option := &coreV1.PodExecOptions{
Container: "nginxtest", // 容器名称
Command: []string{"sh", "-c", "ls"}, // 命令
Stdin: true,
Stdout: true,
Stderr: true,
}
req := client.CoreV1().RESTClient().Post().Resource("pods").
Namespace("default").
Name("myngx-79bdb4ccf8-nbln7"). // pod名称
SubResource("exec").
VersionedParams(option, scheme.ParameterCodec)
// 这里初始化了一个 remote-cmd 的对象
exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
if err != nil {
log.Fatal(err)
}
// 这里开始,将输入输出,进行实时传递(Stream)
err = exec.StreamWithContext(context.Background(), remotecommand.StreamOptions{
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
Tty: true,
})
if err != nil {
log.Fatal(err)
}
}
将 TTY
设置为 true,命令设置为 sh
进入容器交互式执行
option := &coreV1.PodExecOptions{
Container: "nginxtest",
Command: []string{"sh"},
Stdin: true,
Stdout: true,
Stderr: true,
TTY: true,
}
websocket
Executor 的 StreamWithContext 方法,会建立一个流传输的连接,直到服务端和调用端一端关闭连接,才会停止传输。常用的做法是定义一个你想用的客户端,实现 Read(p []byte) (int, error)
和 Write(p []byte) (int, error)
方法即可,调用 Stream 方法时,只要将 StreamOptions 的 Stdin Stdout 都设置为该客户端,Executor 就会通过你定义的 write 和 read 方法来传输数据。
var Upgrader websocket.Upgrader
func init() {
Upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
}
type WsShellClient struct {
client *websocket.Conn
}
func NewWsShellClient(client *websocket.Conn) *WsShellClient {
return &WsShellClient{client: client}
}
// 实现 io.Writer
func (this *WsShellClient) Write(p []byte) (n int, err error) {
err = this.client.WriteMessage(websocket.TextMessage, p)
if err != nil {
return 0, err
}
return len(p), nil
}
// 实现 io.Reader
func (this *WsShellClient) Read(p []byte) (n int, err error) {
_, b, err := this.client.ReadMessage()
if err != nil {
return 0, err
}
return copy(p, string(b)+"\n"), nil
}
func main() {
r := gin.New()
r.GET("/", func(c *gin.Context) {
wsClient, err := ws.Upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Println(err)
return
}
shellClient := ws.NewWsShellClient(wsClient)
option := &coreV1.PodExecOptions{
Container: "nginxtest",
Command: []string{"sh"},
Stdin: true,
Stdout: true,
Stderr: true,
TTY: true,
}
req := client.CoreV1().RESTClient().Post().Resource("pods").
Namespace("default").
Name("myngx-79bdb4ccf8-nbln7").
SubResource("exec").
VersionedParams(option, scheme.ParameterCodec)
exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
if err != nil {
log.Println(err)
}
err = exec.StreamWithContext(c, remotecommand.StreamOptions{
Stdin: shellClient,
Stdout: shellClient,
Stderr: shellClient,
Tty: true,
})
if err != nil {
log.Println(err)
}
})
r.Run(":8080")
}
测试html示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<div id="message" style="width: 500px;height:300px;border:solid 1px gray;overflow:auto">
</div>
<div>
<input type="type" id="txtCmd"/>
<input type="button" id="cmdBtn" value="发送"/>
<input type="button" onclick="document.getElementById('message').innerHTML=''" value="清空"/>
</div>
</div>
<script>
var ws = new WebSocket("ws://localhost:8080/");
ws.onopen = function(){
console.log("open");
}
ws.onmessage = function(e){
let html=document.getElementById("message").innerHTML;
html+='<p>服务端消息:' + e.data + '</p>'
document.getElementById("message").innerHTML=html
}
ws.onclose = function(e){
console.log("close");
}
ws.onerror = function(e){
console.log(e);
}
document.getElementById("cmdBtn").onclick= ()=>{
console.log(document.getElementById("txtCmd").value)
ws.send(document.getElementById("txtCmd").value)
}
</script>
</body>
</html>
xterm.js
前端页面使用 xterm.js 进行模拟terminal展示,只要 javascript 监听 Terminal 对象的对应事件及 websocket 连接的事件,进行对应的页面展示和消息推送就可以了。