轉發請註明出處:cnblogs.com/guangze/p/1 ,知乎、博客園同步更新。

1. 介紹

最近,因為需要對 Kubernetes 進行二次開發,接觸了 client-go 庫。client-go 作為官方維護的 go 語言實現的 API client 庫,提供了大量的高質量代碼幫助開發者編寫自己的客戶端程序,來訪問、操作 Kubernetes 集羣。 在學習過程中我發現,除了官方的幾個 examples 和 README 外,介紹 client-go 的文章較少。因此,這裡有必要總結一下我的學習體會,分享出來。

訪問 Kubernetes 集羣的方式有多種(見 Access Clusters Using the Kubernetes API ),但本質上都要通過調用 Kubernetes REST API 實現對集羣的訪問和操作。比如,使用最多 kubernetes 命令行工具 kubectl,即是通過調用 Kubernetes REST API 完成的。當執行 kubectl get pods -n test 命令時, kubectl 向 Kubernetes API Server 完成認證、並發送 GET 請求:

GET /api/v1/namespaces/test/pods
---
200 OK
Content-Type: application/json
{
"kind": "PodList",
"apiVersion": "v1",
"metadata": {"resourceVersion":"10245"},
"items": [...]
}

那麼如何編寫自己的 http 客戶端程序呢? 這就需要 Kubernetes 提供的 Golang API client 庫。

本文通過解讀 Kubernetes client-go 官方例子之一 Create, Update & Delete Deployment ,詳細介紹 client-go 原理和使用方法。該例子實現了創建、更新、查詢、刪除 deployment 資源。

2. 運行測試

2.1 測試環境

  • Ubuntu 18.04.2
  • Minikube 1.0.0
  • golang 1.12.4
  • k8s.io/client-go v11.0.0
  • GoLand IDE

下載 Minikube release 地址:github.com/kubernetes/m

下載 k8s.io/client-go 源碼:github.com/kubernetes/c

client-go 源碼下載後,使用 go mod vendor 下載依賴庫,或直接從github上下載依賴的其他庫(如果沒有設置外網代理的話)。

2.2 運行結果

因為我自己開了 VPN 連接到遠程的 Kubernetes 集羣內網,並複製 .kube/config 到了本地,所以可以直接在 GoLand 上編譯運行,就能看到如下輸出:

Creating deployment...
Created deployment "demo-deployment".
-> Press Return key to continue.

Updating deployment...
Updated deployment...
-> Press Return key to continue.

Listing deployments in namespace "default":
* demo-deployment (1 replicas)
* intended-quail-fluentbit-operator (1 replicas)
* test (1 replicas)
-> Press Return key to continue.

Deleting deployment...
Deleted deployment.

Process finished with exit code 0

在運行過程中,你也可以通過 kubectl 命令觀察創建的 deployment 。可以看到,這個 example 分別完成了四個操作:

  • 在 default namespace 下創建了一個叫 demo-deployment 的 deployment
  • 更新該 deployment 的副本數量、修改容器鏡像版本到 nginx:1.13
  • 列出 default namespace 下的所有 deployment
  • 刪除創建的 demo-deployment

3. 原理解析

完成 deployment 資源的增刪改查,大體可以分為以下幾個步驟。這個流程對訪問其他 Kubernetes 資源也是一樣的:

  1. 通過 kubeconfig 信息,構造 Config 實例。該實例記錄了集羣證書、 API Server 地址等信息;
  2. 根據 Config 實例攜帶的信息,創建 http 客戶端;
  3. 向 apiserver 發送請求,創建 Kubernetes 資源等

我用 go-callvis 製作了 example 中的函數調用圖,以供參考:

3.1 獲取 kubeconfig 信息,並構造 rest#Config 實例

Note: 我用 <package>#<func, struct> 表示某包下的函數、結構體

在訪問 Kubernetes 集羣時,少不了身份認證。使用 kubeconfig 配置文件是其中一種主要的認證方式。kubeconfig 文件描述了集羣(cluster)、用戶(user)和上下文(context)信息。默認的 kubeconfig 文件位於 $HOME/.kube/config 下。可以通過 cat $HOME/.kube/config, 或者 kubectl config view 查看:

apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority-data: DATA+OMITTED
server: https://192.168.0.8:6443
name: cluster.local
contexts:
- context:
cluster: cluster.local
user: kubernetes-admin
name: [email protected]
users:
- name: kubernetes-admin
user:
client-certificate-data: REDACTED
client-key-data: REDACTED
current-context: [email protected]
preferences: {}

我的測試環境 kubeconfig 配置顯示,集羣 API Server 地址位於 192.168.0.8:6443,集羣開啟 TLS,certificate-authority-data 指定公鑰。客戶端用戶名為 kubernetes-admin,證書為 client-certificate-data,通過私鑰 client-key-data 訪問集羣。上下文參數將集羣和用戶關聯了起來。關於 kubeconfig 的更多介紹可以參考 kubernetes中kubeconfig的用法

源碼中,kubeconfig 變數記錄了 kubeconfig 文件路徑。通過 BuildConfigFromFlags 函數返回了一個 rest#Config 結構體實例。該實例記錄了 kubeconfig 文件解析、處理後的信息。

var kubeconfig *string
if home := homedir.HomeDir(); home != "" {
kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(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)
}

BuildConfigFromFlags 函數是如何實例化 rest#Config 結構體的呢?

首先,BuildConfigFromFlags 函數接受一個 kubeconfigPath 變數,然後在內部依次調用如下函數:

  1. func NewNonInteractiveDeferredLoadingClientConfig(loader ClientConfigLoader, overrides *ConfigOverrides) ClientConfig
  2. func (config *DeferredLoadingClientConfig) ClientConfig() (*restclient.Config, error)

func BuildConfigFromFlags(masterUrl, kubeconfigPath string) (*restclient.Config, error) {
if kubeconfigPath == "" && masterUrl == "" {
...
}
return NewNonInteractiveDeferredLoadingClientConfig(
&ClientConfigLoadingRules{ExplicitPath: kubeconfigPath},
&ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: masterUrl}}).ClientConfig()
}

我們來看看這兩個鏈式調用的函數都做了哪些工作:

3.1.1 tools/clientcmd#NewNonInteractiveDeferredLoadingClientConfig

func NewNonInteractiveDeferredLoadingClientConfig(loader ClientConfigLoader, overrides *ConfigOverrides) ClientConfig {
return &DeferredLoadingClientConfig{loader: loader, overrides: overrides, icc: &inClusterClientConfig{overrides: overrides}}
}

返回值:

  • 返回一個 tools/clientcmd#DirectClientConfig 類型的實例。

DeferredLoadingClientConfig 結構體是 ClientConfig 介面的一種實現。主要工作是確保裝載的 rest#Config 實例使用的是最新 kubeconfig 數據(對於配置了多個集羣的,export KUBECONFIG=cluster1-config:cluster2-config,需要執行 merge)。雖然本例子中還感受不到 Deferred Loading 體現在何處。源碼注釋中有這樣一段話:

It is used in cases where the loading rules may change after youve instantiated them and you want to be sure that the most recent rules are used. This is useful in cases where you bind flags to loading rule parameters before the parse happens and you want your calling code to be ignorant of how the values are being mutated to avoid passing extraneous information down a call stack

參數列表:

  • loader ClientConfigLoader:

我的測試環境是通過單一的路徑 $HOME/.kube/config 獲取 kubeconfig。但 kubeconfig 可能由不只一個配置文件 merge 而成,loader 確保在最終創建 rest#Config 實例時,使用的是最新的 kubeconfig。loader 的 ExplicitPath 欄位記錄指定的 kubeconfig 文件路徑,Precedence 字元串數組記錄要 merge 的 kubeconfig 信息。這也是為什麼返回值叫 Deferred Loading ClientConfig

loader 接受一個 ClientConfigLoader 介面實現,比如:&ClientConfigLoadingRules{ExplicitPath: kubeconfigPath}(這裡是地址類型,因為是 *ClientConfigLoadingRules 實現了 ClientConfigLoader 介面,而不是 ClientConfigLoadingRules)。

  • overrides *ConfigOverrides:

overtrides 保存用於強制覆蓋 rest#Config 實例的信息。本例中沒有用到。

3.1.2 (*DeferredLoadingClientConfig).ClientConfig()

上一個函數返回了 ClientConfig 介面實例。這裡調用 ClientConfig 介面定義的 ClientConfig() 方法。ClientConfig() 工作是解析、處理 kubeconfig 文件裏的認證信息,並返回一個完整的 rest#Config 實例。

// 錯誤處理省略
func (config *DeferredLoadingClientConfig) ClientConfig() (*restclient.Config, error) {
mergedClientConfig, err := config.createClientConfig()
...

// load the configuration and return on non-empty errors and if the
// content differs from the default config
mergedConfig, err := mergedClientConfig.ClientConfig()
...

// check for in-cluster configuration and use it
if config.icc.Possible() {
klog.V(4).Infof("Using in-cluster configuration")
return config.icc.ClientConfig()
}

// return the result of the merged client config
return mergedConfig, err
}

這個函數主要有兩個重要部分:

1. mergedClientConfig, err := config.createClientConfig()

內部執行遍歷 kubeconfig files (如果有多個), 對每個 kubeconfig 執行 LoadFromFile 返回 tools/clientcmd/api#Config 實例。api#Config 顧名思義 api 包下的 Config,是把 kubeconfig (eg. $HOME/.kube/config) 序列化為一個 API 資源對象。

現在,我們看到了幾種結構體或介面命名相似,不要混淆了:

  • api#Config:序列化 kubeconfig 文件後生成的對象
  • tools/clientcmd#ClientConfig:負責用 api#Config 真正創建 rest#Config。處理、解析 kubeconfig 中的認證信息,有了它才能創建 rest#Config,所以命名叫 ClientConfig
  • rest#Config:用於創建 http 客戶端

對於 merge 後的 api#Config,調用 NewNonInteractiveClientConfig 創建一個 ClientConfig 介面的實現.

2. mergedConfig, err := mergedClientConfig.ClientConfig()

真正創建 rest#Config 的地方。在這裡解析、處理 kubeconfig 中的認證信息。

3.2 創建 kubernetes#ClientSet

// NewForConfig creates a new Clientset for the given config.
config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
if err != nil {
panic(err)
}

ClientSet 是 kubernetes 包下的一個重要結構體,負責訪問集羣 apiserver 的客戶端。那為什麼叫 ClientSet 呢? 說明 Client 不止一個。比如 deployment 的 extensions/v1beta1、apps/v1beta、最新的 apps/v1 有多種版本(API Group),每種都有一個 Client 用於創建該版本的 deployment

// Clientset contains the clients for groups. Each group has exactly one
// version included in a Clientset.
type Clientset struct {
...
appsV1 *appsv1.AppsV1Client
appsV1beta1 *appsv1beta1.AppsV1beta1Client
appsV1beta2 *appsv1beta2.AppsV1beta2Client
...
extensionsV1beta1 *extensionsv1beta1.ExtensionsV1beta1Client
}

3.3 創建一個 default 命名空間下的 apps/v1#deployment 資源

3.3.1 創建 deploymentsClient

創建 apps/v1 版本的 deployment,首先獲得該版本的 client。

deploymentsClient := clientset.AppsV1().Deployments(apiv1.NamespaceDefault)

3.3.2 構造一個 apps/v1#deployment 實例

deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "demo-deployment", // 指定 deployment 名字
},
Spec: appsv1.DeploymentSpec{
Replicas: int32Ptr(2), // 指定副本數
Selector: &metav1.LabelSelector{ // 指定標籤
MatchLabels: map[string]string{
"app": "demo",
},
},
Template: apiv1.PodTemplateSpec{ // 容器模板
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": "demo",
},
},
Spec: apiv1.PodSpec{
...
},
},
},
}

3.3.3 向 apiserver 發送 POST 創建 deployment

有興趣的朋友可以進一步看源碼這裡是如何實現 http client 的。

result, err := deploymentsClient.Create(deployment)

---

// Create takes the representation of a deployment and creates it. Returns the servers representation of the deployment, and an error, if there is any.
func (c *deployments) Create(deployment *v1.Deployment) (result *v1.Deployment, err error) {
result = &v1.Deployment{}
err = c.client.Post().
Namespace(c.ns).
Resource("deployments").
Body(deployment).
Do().
Into(result)
return
}

至此,一個 deployment 就創建完成了。刪、改、查操作也是一樣。

4. 總結

要徹底搞清楚 client-go,一方面要多查看 K8s 的 API 文檔,另一方建議用 GoLand 單步調試,搞清楚每一步的含義。

5. 參考資料

Access Clusters Using the Kubernetes API

Kubernetes API Concepts

kubernetes中kubeconfig的用法

Building stuff with the Kubernetes API


推薦閱讀:
相關文章