the site subtitle

自己动手实现一个kubectl exec

2020.06.20

在日常工作中kubectl 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)
	}
}