自己动手实现一个 kubectl exec

k8s技术圈

共 7751字,需浏览 16分钟

 · 2021-05-11

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

原文链接:https://vsxen.github.io/2020/06/20/kubectl-exec/



K8S 进阶训练营


 点击屏末  | 即刻学习
浏览 110
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报