轉發請注明出處:https://www.cnblogs.com/guangze/p/10753929.html,知乎、博客園同步更新。
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 地址:https://github.com/kubernetes/minikube/releases
下載 k8s.io/client-go 源碼:https://github.com/kubernetes/client-go
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 資源的增刪改查,大體可以分為以下幾個步驟。這個流程對訪問其他 Kubernete 資源也是一樣的:
- 通過 kubeconfig 信息,構造 Config 實例。該實例記錄了集群證書、 API Server 地址等信息;
- 根據 Config 實例攜帶的信息,創建 http 客戶端;
- 向 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: kubernetes-admin@cluster.local
users:
- name: kubernetes-admin
user:
client-certificate-data: REDACTED
client-key-data: REDACTED
current-context: kubernetes-admin@cluster.local
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 變量,然后在內部依次調用如下函數:
func NewNonInteractiveDeferredLoadingClientConfig(loader ClientConfigLoader, overrides *ConfigOverrides) ClientConfig
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 you've 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 文件后生成的對象
type Config struct {
// Legacy field from pkg/api/types.go TypeMeta.
// TODO(jlowdermilk): remove this after eliminating downstream dependencies.
// +optional
Kind string `json:"kind,omitempty"`
// Legacy field from pkg/api/types.go TypeMeta.
// TODO(jlowdermilk): remove this after eliminating downstream dependencies.
// +optional
APIVersion string `json:"apiVersion,omitempty"`
// Preferences holds general information to be use for cli interactions
Preferences Preferences `json:"preferences"`
// Clusters is a map of referencable names to cluster configs
Clusters map[string]*Cluster `json:"clusters"`
// AuthInfos is a map of referencable names to user configs
AuthInfos map[string]*AuthInfo `json:"users"`
// Contexts is a map of referencable names to context configs
Contexts map[string]*Context `json:"contexts"`
// CurrentContext is the name of the context that you would like to use by default
CurrentContext string `json:"current-context"`
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
// +optional
Extensions map[string]runtime.Object `json:"extensions,omitempty"`
}
- tools/clientcmd#ClientConfig:負責用 api#Config 真正創建 rest#Config。處理、解析 kubeconfig 中的認證信息,有了它才能創建 rest#Config,所以命名叫 ClientConfig
- rest#Config:用於創建 http 客戶端
type Config struct {
// Host must be a host string, a host:port pair, or a URL to the base of the apiserver.
// If a URL is given then the (optional) Path of that URL represents a prefix that must
// be appended to all request URIs used to access the apiserver. This allows a frontend
// proxy to easily relocate all of the apiserver endpoints.
Host string
// APIPath is a sub-path that points to an API root.
APIPath string
// ContentConfig contains settings that affect how objects are transformed when
// sent to the server.
ContentConfig
// Server requires Basic authentication
Username string
Password string
// Server requires Bearer authentication. This client will not attempt to use
// refresh tokens for an OAuth2 flow.
// TODO: demonstrate an OAuth2 compatible client.
BearerToken string
// Path to a file containing a BearerToken.
// If set, the contents are periodically read.
// The last successfully read value takes precedence over BearerToken.
BearerTokenFile string
// Impersonate is the configuration that RESTClient will use for impersonation.
Impersonate ImpersonationConfig
// Server requires plugin-specified authentication.
AuthProvider *clientcmdapi.AuthProviderConfig
// Callback to persist config for AuthProvider.
AuthConfigPersister AuthProviderConfigPersister
// Exec-based authentication provider.
ExecProvider *clientcmdapi.ExecConfig
// TLSClientConfig contains settings to enable transport layer security
TLSClientConfig
// UserAgent is an optional field that specifies the caller of this request.
UserAgent string
// Transport may be used for custom HTTP behavior. This attribute may not
// be specified with the TLS client certificate options. Use WrapTransport
// to provide additional per-server middleware behavior.
Transport http.RoundTripper
// WrapTransport will be invoked for custom HTTP behavior after the underlying
// transport is initialized (either the transport created from TLSClientConfig,
// Transport, or http.DefaultTransport). The config may layer other RoundTrippers
// on top of the returned RoundTripper.
//
// A future release will change this field to an array. Use config.Wrap()
// instead of setting this value directly.
WrapTransport transport.WrapperFunc
// QPS indicates the maximum QPS to the master from this client.
// If it's zero, the created RESTClient will use DefaultQPS: 5
QPS float32
// Maximum burst for throttle.
// If it's zero, the created RESTClient will use DefaultBurst: 10.
Burst int
// Rate limiter for limiting connections to the master from this client. If present overwrites QPS/Burst
RateLimiter flowcontrol.RateLimiter
// The maximum length of time to wait before giving up on a server request. A value of zero means no timeout.
Timeout time.Duration
// Dial specifies the dial function for creating unencrypted TCP connections.
Dial func(ctx context.Context, network, address string) (net.Conn, error)
// Version forces a specific version to be used (if registered)
// Do we need this?
// Version string
}
對於 merge 后的 api#Config,調用 NewNonInteractiveClientConfig 創建一個 ClientConfig 接口的實現。
2.mergedConfig, err := mergedClientConfig.ClientConfig()
真正創建 rest#Config 的地方。在這里解析、處理 kubeconfig 中的認證信息。
3.2 創建 ClientSet
// NewForConfig creates a new Clientset for the given config.
config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
if err != nil {
panic(err)
}
ClientSet 是一個重要的對象。它就是負責訪問集群 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 server's 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 單步調試,搞清楚每一步的含義。