自己動手實現(xiàn)一個 kubectl exec
在日常工作中kubectl exec可以說是非常高頻使用的,如果你想自己了解相關原理,不妨自己動手寫一個。
知識儲備:
websocket 阮一峰這篇《WebSocket 教程- 阮一峰的網(wǎng)絡日志》寫的比較詳細。
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/[email protected]+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
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的一個集合,最后創(chuàng)建了一個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 究竟發(fā)送了什么請求,可以看到關鍵在于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'
現(xiàn)在就開始做吧,從上面的URL可以看到exec 是屬于pod的資源,
// 初始化pod所在的corev1資源組,發(fā)送請求
// PodExecOptions struct 包括Container stdout stdout Command 等結(jié)構
// 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 主要實現(xiàn)了http 轉(zhuǎn) SPDY 添加X-Stream-Protocol-Version相關header 并發(fā)送請求
exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
// 建立鏈接之后從請求的sream中發(fā)送、讀取數(shù)據(jù)
if err = exec.Stream(remotecommand.StreamOptions{
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
Tty: false,
}); err != nil {
fmt.Print(err)
}
以上只實現(xiàn)了單個命令,實際上我們更多的是使用-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資源組,發(fā)送請求
// PodExecOptions struct 包括Container stdout stdout Command 等結(jié)構
// 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 主要實現(xiàn)了http 轉(zhuǎn) SPDY 添加X-Stream-Protocol-Version相關header 并發(fā)送請求
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中發(fā)送、讀取數(shù)據(jù)
if err = exec.Stream(remotecommand.StreamOptions{
Stdin: screen,
Stdout: screen,
Stderr: screen,
Tty: false,
}); err != nil {
fmt.Print(err)
}
}
原文鏈接:https://vsxen.github.io/2020/06/20/kubectl-exec/
K8S 進階訓練營
點擊屏末 | 閱讀原文 | 即刻學習
