自己動手實現一個 kubectl exec

在日常工作中kubectl exec可以說是非常高頻使用的,如果你想自己瞭解相關原理,不妨自己動手寫一個。

知識儲備:

如果你英文閱讀能還可以,這兩篇文章從原理方面介紹了 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/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/VmEcIYfsUbgh-p4RCssGFQ