client-go實戰之四:dynamicClient

程式設計師欣宸發表於2021-09-09

歡迎訪問我的GitHub

https://github.com/zq2599/blog_demos

內容:所有原創文章分類彙總及配套原始碼,涉及Java、Docker、Kubernetes、DevOPS等;

系列文章連結

  1. client-go實戰之一:準備工作
  2. client-go實戰之二:RESTClient
  3. client-go實戰之三:Clientset
  4. client-go實戰之四:dynamicClient
  5. client-go實戰之五:DiscoveryClient

本篇概覽

  • 本文是《client-go實戰》系列的第四篇,前文我們們學習了Clientset客戶端,發現Clientset在deployment、service這些kubernetes內建資源的時候是很方便的,每個資源都有其專屬的方法,配合官方API文件和資料結構定義,開發起來比Restclient高效;
  • 但如果要處理的不是kubernetes的內建資源呢?比如CRD,Clientset的程式碼中可沒有使用者自定義的東西,顯然就用不上Clientset了,此時本篇的主角dynamicClient就要登場啦!

相關知識儲備

  • 在正式學習dynamicClient之前,有兩個重要的知識點需要了解:Object.runtimeUnstructured,對於整個kubernetes來說它們都是非常重要的;

Object.runtime

  • 聊Object.runtime之前先要明確兩個概念:資源和資源物件,關於資源大家都很熟悉了,pod、deployment這些不都是資源嘛,個人的理解是資源更像一個嚴格的定義,當您在kubernetes中建立了一個deployment之後,這個新建的deployment例項就是資源物件了;
  • 在kubernetes的程式碼世界中,資源物件對應著具體的資料結構,這些資料結構都實現了同一個介面,名為Object.runtime,原始碼位置是staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go,定義如下:
type Object interface {
	GetObjectKind() schema.ObjectKind
	DeepCopyObject() Object
}
  • DeepCopyObject方法顧名思義,就是深拷貝,也就是將記憶體中的物件克隆出一個新的物件;
  • 至於GetObjectKind方法的作用,相信聰明的您也猜到了:處理Object.runtime型別的變數時,只要呼叫其GetObjectKind方法就知道它的具體身份了(如deployment,service等);
  • 最後再次強調:資源物件都是Object.runtime的實現

Unstructured

  • 在聊Unstructured之前,先看一個簡單的JSON字串:
{
	"id": 101,
	"name": "Tom"
}
  • 上述JSON的欄位名稱和欄位值型別都是固定的,因此可以針對性編寫一個資料結構來處理它:
type Person struct {
	ID int
	Name String
}
  • 對於上面的JSON字串就是結構化資料(Structured Data),這個應該好理解;
  • 與結構化資料相對的就是非結構化資料了(Unstructured Data),在實際的kubernetes環境中,可能會遇到一些無法預知結構的資料,例如前面的JSON字串中還有第三個欄位,欄位值的具體內容和型別在編碼時並不知曉,而是在真正執行的時候才知道,那麼在編碼時如何處理呢?相信您會想到用interface{}來表示,實際上client-go也是這麼做的,來看Unstructured資料結構的原始碼,路徑是staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructured.go
type Unstructured struct {
	// Object is a JSON compatible map with string, float, int, bool, []interface{}, or
	// map[string]interface{}
	// children.
	Object map[string]interface{}
}
  • 顯然,上述資料結構定義並不能發揮什麼作用,真正重要的是關聯的方法,如下圖,可見client-go已經為Unstructured準備了豐富的方法,藉助這些方法可以靈活的處理非結構化資料:

在這裡插入圖片描述

重要知識點:Unstructured與資源物件的相互轉換

  • 另外還有一個非常重要的知識點:可以用Unstructured例項生成資源物件,也可以用資源物件生成Unstructured例項,這個神奇的能力是unstructuredConverter的FromUnstructured和ToUnstructured方法分別實現的,下面的程式碼片段展示瞭如何將Unstructured例項轉為PodList例項:
// 例項化一個PodList資料結構,用於接收從unstructObj轉換後的結果
podList := &apiv1.PodList{}

// unstructObj
err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructObj.UnstructuredContent(), podList)
  • 您可能會好奇上述FromUnstructured方法究竟是如何實現轉換的,我們們去看下此方法的內部實現,如下圖所示,其實也沒啥懸念了,通過反射可以得到podList的欄位資訊:

在這裡插入圖片描述

  • 至此,Unstructured的分析就結束了嗎?沒有,強烈推薦您進入上圖紅框2中的fromUnstructured方法去看細節,這裡面是非常精彩的,以podList為例,這是個資料結構,而fromUnstructured只處理原始型別,對於資料結構會呼叫structFromUnstructured方法處理,在structFromUnstructured方法中
    處理資料結構的每個欄位,又會呼叫fromUnstructured,這是相互迭代的過程,最終,不論podList中有多少資料結構的巢狀都會被處理掉,篇幅所限就不展開相信分析了,下圖是一部分關鍵程式碼:

在這裡插入圖片描述

  • 小結:Unstructured轉為資源物件的套路並不神祕,無非是用反射取得資源物件的欄位型別,然後按照欄位名去Unstructured的map中取得原始資料,再用反射設定到資源物件的欄位中即可;
  • 做完了準備工作,接下來該回到本篇文章的主題了:dynamicClient客戶端

關於dynamicClient

  • deployment、pod這些資源,其資料結構是明確的固定的,可以精確對應到Clientset中的資料結構和方法,但是對於CRD(使用者自定義資源),Clientset客戶端就無能為力了,此時需要有一種資料結構來承載資源物件的資料,也要有對應的方法來處理這些資料;
  • 此刻,前面提到的Unstructured可以登場了,沒錯,把Clientset不支援的資源物件交給Unstructured來承載,接下來看看dynamicClient和Unstructured的關係:
  • 先看資料結構定義,和clientset沒啥區別,只有個restClient欄位:
type dynamicClient struct {
	client *rest.RESTClient
}
  • 這個資料結構只有一個關聯方法Resource,入參為GVR,返回的是另一個資料結構dynamicResourceClient:
func (c *dynamicClient) Resource(resource schema.GroupVersionResource) NamespaceableResourceInterface {
	return &dynamicResourceClient{client: c, resource: resource}
}
  • 通過上述程式碼可知,dynamicClient的關鍵是資料結構dynamicResourceClient及其關聯方法,來看看這個dynamicResourceClient,如下圖,果然,dynamicClient所有和資源相關的操作都是dynamicResourceClient在做(代理模式?),選了create方法細看,序列化和反序列化都交給unstructured的UnstructuredJSONScheme,與kubernetes的互動交給Restclient:

在這裡插入圖片描述

  • 小結:
  1. 與Clientset不同,dynamicClient為各種型別的資源都提供統一的操作API,資源需要包裝為Unstructured資料結構;
  2. 內部使用了Restclient與kubernetes互動;
  • 對dynamicClient的介紹分析就這些吧,可以開始實戰了;

需求確認

  • 本次編碼實戰的需求很簡單:查詢指定namespace下的所有pod,然後在控制檯列印出來,要求用dynamicClient實現;
  • 您可能會問:pod是kubernetes的內建資源,更適合Clientset來操作,而dynamicClient更適合處理CRD不是麼?---您說得沒錯,這裡用pod是因為折騰CRD太麻煩了,定義好了還要在kubernetes上釋出,於是乾脆用pod來代替CRD,反正dynamicClient都能處理,我們們通過實戰掌握dynamicClient的用法就行了,以後遇到各種資源都能處理之;

原始碼下載

名稱 連結 備註
專案主頁 https://github.com/zq2599/blog_demos 該專案在GitHub上的主頁
git倉庫地址(https) https://github.com/zq2599/blog_demos.git 該專案原始碼的倉庫地址,https協議
git倉庫地址(ssh) git@github.com:zq2599/blog_demos.git 該專案原始碼的倉庫地址,ssh協議
  • 這個git專案中有多個資料夾,client-go相關的應用在client-go-tutorials資料夾下,如下圖紅框所示:

在這裡插入圖片描述

  • client-go-tutorials資料夾下有多個子資料夾,本篇對應的原始碼在dynamicclientdemo目錄下,如下圖紅框所示:

在這裡插入圖片描述

編碼

  • 新建資料夾dynamicclientdemo,在裡面執行以下命令,新建module:
go mod init dynamicclientdemo
  • 新增k8s.io/api和k8s.io/client-go這兩個依賴,注意版本要匹配kubernetes環境:
go get k8s.io/api@v0.20.0
go get k8s.io/client-go@v0.20.0
  • 新建main.go,內容如下,稍後會說一下要注意的重點:
package main

import (
	"context"
	"flag"
	"fmt"
	apiv1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/util/homedir"
	"path/filepath"
)

func main() {

	var kubeconfig *string

	// home是家目錄,如果能取得家目錄的值,就可以用來做預設值
	if home:=homedir.HomeDir(); home != "" {
		// 如果輸入了kubeconfig引數,該引數的值就是kubeconfig檔案的絕對路徑,
		// 如果沒有輸入kubeconfig引數,就用預設路徑~/.kube/config
		kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
	} else {
		// 如果取不到當前使用者的家目錄,就沒辦法設定kubeconfig的預設目錄了,只能從入參中取
		kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
	}

	flag.Parse()

	// 從本機載入kubeconfig配置檔案,因此第一個引數為空字串
	config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)

	// kubeconfig載入失敗就直接退出了
	if err != nil {
		panic(err.Error())
	}

	dynamicClient, err := dynamic.NewForConfig(config)

	if err != nil {
		panic(err.Error())
	}

	// dynamicClient的唯一關聯方法所需的入參
	gvr := schema.GroupVersionResource{Version: "v1", Resource: "pods"}

	// 使用dynamicClient的查詢列表方法,查詢指定namespace下的所有pod,
	// 注意此方法返回的資料結構型別是UnstructuredList
	unstructObj, err := dynamicClient.
		Resource(gvr).
		Namespace("kube-system").
		List(context.TODO(), metav1.ListOptions{Limit: 100})

	if err != nil {
		panic(err.Error())
	}

	// 例項化一個PodList資料結構,用於接收從unstructObj轉換後的結果
	podList := &apiv1.PodList{}

	// 轉換
	err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructObj.UnstructuredContent(), podList)

	if err != nil {
		panic(err.Error())
	}

	// 表頭
	fmt.Printf("namespace\t status\t\t name\n")

	// 每個pod都列印namespace、status.Phase、name三個欄位
	for _, d := range podList.Items {
		fmt.Printf("%v\t %v\t %v\n",
			d.Namespace,
			d.Status.Phase,
			d.Name)
	}
}
  • 上述程式碼中有三處重點需要注意:
  1. Resource方法指定了本次操作的資源型別;
  2. List方法向kubernetes發起請求;
  3. FromUnstructured將Unstructured資料結構轉成PodList,其原理前面已經分析過;
  • 執行go run main.go,如下,可以從kubernetes取得資料,並且轉換成PodList也正常:
zhaoqin@zhaoqindeMBP-2 dynamicclientdemo % go run main.go
namespace        status          name
kube-system      Running         coredns-7f89b7bc75-5pdwc
kube-system      Running         coredns-7f89b7bc75-nvbvm
kube-system      Running         etcd-hedy
kube-system      Running         kube-apiserver-hedy
kube-system      Running         kube-controller-manager-hedy
kube-system      Running         kube-flannel-ds-v84vc
kube-system      Running         kube-proxy-hlppx
kube-system      Running         kube-scheduler-hedy
  • 至此,dynamicClient的學習和實戰就完成了,它是名副其實的動態客戶端工具,用一套API處理所有資源,除了突破Clientset的內建資源限制,還讓我們的業務程式碼有了更大的靈活性,希望本文能給您一些參考,輔助您寫出與場景更加匹配的程式碼;

你不孤單,欣宸原創一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 資料庫+中介軟體系列
  6. DevOps系列

歡迎關注公眾號:程式設計師欣宸

微信搜尋「程式設計師欣宸」,我是欣宸,期待與您一同暢遊Java世界...
https://github.com/zq2599/blog_demos

相關文章