在日常工作中kubectl exec 可以说是非常高频使用的,如果你想自己了解相关原理,不妨自己动手写一个。
知识储备:
-
websocket 阮一峰这篇《WebSocket 教程- 阮一峰的网络日志》写的比较详进。
-
kubectl exec 原理
https://itnext.io/how-it-works-kubectl-exec-e31325daa910
https://erkanerol.github.io/post/how-kubectl-exec-works/
如果你英文阅读能还可以,这两篇文章从原理方面介绍了exec是如何工作的。
了解了以上知识之后,接下来我们就开始动手吧。
首先来初始化一下项目,这里使用go mod作为依赖管理工具。k8s的client-go对机器版本是有要求的,所以在初始化的时候最好去官方那边找一下可用的版本。如果遇到mod/k8s.io/client-go@v10.0.0+incompatible/kubernetes/scheme/register.go:22:2: unknown import path "k8s.io/api/admissionregistration /v1alpha1": cannot find module providing package k8s.io/api/admissionregistration/v1alpha1
这种报错,可以尝试强制指定版本,这个也是从kubebuilder那里学到的。
go mod init k8sdemo
module k8sdemo
go 1.13
require (
github.com/gorilla/websocket v1.4.2
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586
k8s.io/api v0.17.2
k8s.io/apimachinery v0.17.2
k8s.io/client-go v0.17.2
)
client-go的example目录也有相关对象的CURD示例,我们可以先从这里入手,先熟悉相关操作,可以看到首先从kuebconfig读取配置,然后初始化各种client的一个集合,最后创建了一个deployment实例。
config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
if err != nil {
panic(err)
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
panic(err)
}
deploymentsClient := clientset.AppsV1().Deployments(apiv1.NamespaceDefault)
接下来我们看一下kubectl exec 究竟发送了什么请求,可以看到关键在于exec?command=date&container=nginx&stdin=true&stdout=true&tty=true
kubectl exec nginx-8486565b79-4hb5t -it -v 10 bash
curl -k -v -XPOST -H "User-Agent: kubectl/v1.16.2 (linux/amd64) kubernetes/c97fe50"
-H "X-Stream-Protocol-Version: v4.channel.k8s.io"
-H "X-Stream-Protocol-Version: v3.channel.k8s.io"
-H "X-Stream-Protocol-Version: v2.channel.k8s.io"
-H "X-Stream-Protocol-Version: channel.k8s.io"
'https://192.168.2.2:6443/api/v1/namespaces/default/pods/nginx-8486565b79-4hb5t/exec
?command=date&container=nginx&stdin=true&stdout=true&tty=true'
现在就开始做吧,从上面的URL可以看到exec 是属于pod的资源,
// 初始化pod所在的corev1资源组,发送请求
// PodExecOptions struct 包括Container stdout stdout Command 等结构
// scheme.ParameterCodec 应该是pod 的GVK (GroupVersion & Kind)之类的
req := clientset.CoreV1().RESTClient().Post().
Resource("pods").
Name("nginx-8486565b79-4hb5t").
Namespace("default").
SubResource("exec").
VersionedParams(&corev1.PodExecOptions{
Command: []string{"bash"},
Stdin: true,
Stdout: true,
Stderr: true,
TTY: false,
}, scheme.ParameterCodec)
// remotecommand 主要实现了http 转 SPDY 添加X-Stream-Protocol-Version相关header 并发送请求
exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
// 建立链接之后从请求的sream中发送、读取数据
if err = exec.Stream(remotecommand.StreamOptions{
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
Tty: false,
}); err != nil {
fmt.Print(err)
}
以上只实现了单个命令,实际上我们更多的是使用-it进入交互式终端,这个应该怎么做呢?
// 这里引入了ssh包 来做终端响应 golang.org/x/crypto/ssh/termina
// 检查是不是终端
if !terminal.IsTerminal(0) || !terminal.IsTerminal(1) {
fmt.Errorf("stdin/stdout should be terminal")
}
// 这个应该是处理Ctrl + C 这种特殊键位
oldState, err := terminal.MakeRaw(0)
if err != nil {
fmt.Println(err)
}
defer terminal.Restore(0, oldState)
// 用IO读写替换 os stdout
screen := struct {
io.Reader
io.Writer
}{os.Stdin, os.Stdout}
完整示例
package main
import (
"flag"
"fmt"
"io"
"os"
"path/filepath"
"golang.org/x/crypto/ssh/terminal"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/remotecommand"
"k8s.io/client-go/util/homedir"
)
func main() {
var kubeconfig *string
if home := homedir.HomeDir(); home != "" {
kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "vm"), "(optional) absolute path to the kubeconfig file")
} else {
kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
}
flag.Parse()
config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
if err != nil {
panic(err)
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
panic(err)
}
// 初始化pod所在的corev1资源组,发送请求
// PodExecOptions struct 包括Container stdout stdout Command 等结构
// scheme.ParameterCodec 应该是pod 的GVK (GroupVersion & Kind)之类的
req := clientset.CoreV1().RESTClient().Post().
Resource("pods").
Name("nginx-8486565b79-4hb5t").
Namespace("default").
SubResource("exec").
VersionedParams(&corev1.PodExecOptions{
Command: []string{"bash"},
Stdin: true,
Stdout: true,
Stderr: true,
TTY: false,
}, scheme.ParameterCodec)
// remotecommand 主要实现了http 转 SPDY 添加X-Stream-Protocol-Version相关header 并发送请求
exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
// 检查是不是终端
if !terminal.IsTerminal(0) || !terminal.IsTerminal(1) {
fmt.Errorf("stdin/stdout should be terminal")
}
// 这个应该是处理Ctrl + C 这种特殊键位
oldState, err := terminal.MakeRaw(0)
if err != nil {
fmt.Println(err)
}
defer terminal.Restore(0, oldState)
// 用IO读写替换 os stdout
screen := struct {
io.Reader
io.Writer
}{os.Stdin, os.Stdout}
// 建立链接之后从请求的sream中发送、读取数据
if err = exec.Stream(remotecommand.StreamOptions{
Stdin: screen,
Stdout: screen,
Stderr: screen,
Tty: false,
}); err != nil {
fmt.Print(err)
}
}