1. 寫在前面
微信公眾號:[double12gzh]
個人主頁: https://gzh.readthedocs.io
關注容器技術、關注
Kubernetes
。問題或建議,請公眾號留言。
Kubernetes架構的設計模式,我們可以很方便的使用CRD(Custom Resource Definitions)對k8s API進行擴展。但是問題,通過client-go來獲取這些CRD或開發用戶自定義控制器,那是比較麻煩的一件事情,除此之外,市面上對於client-go
的介紹並不是很多。
本文將會通過一個示例,簡單介紹一下如何通過client-go獲取CRD。
2. 寫作動機
我在PaaS平台的日常開發工作中,想要將第三方存儲廠商集成到Kubernetes集群中時,遇到了這個挑戰。計划是使用自定義資源定義來定義諸如文件系統池和文件系統。然后,一個自定義的Operator可以監聽這些資源的創建和刪除,並負責這些資源的生命周期的管理。
3. 定義CR(Custom Resource)
在本文中,我們將以一個簡單的例子來進行演示。使用kubectl可以很容易地創建自定義資源定義,對於這個例子,我們將從一個簡單的資源定義開始做起:
apiVersion: "apiextensions.k8s.io/v1beta1"
kind: "CustomResourceDefinition"
metadata:
name: "projects.examples-gzh.com"
spec:
group: "examples-gzh.com"
version: "v1alpha1"
scope: "Namespaced"
names:
plural: "projects"
singular: "project"
kind: "Project"
validation:
openAPIV3Schema:
required: ["spec"]
properties:
replicas:
type: "integer"
minimum: 1
-
確定
Group
的名字。在定義CRD時,我們首先需要定義它所在的
Group
(在上面的代碼中,其Group
為:examples-gzh.com
)。對於Group的定義,為了避免命名沖突,通常會使用一些比較特別的字符串(如:你的個人主頁的地址、你公司的域名等),Group
名字確定了之后,由於CRD的名字是按<plural-resource-name>.<api-group-name>
這個格式進行命名的,所以這里我們的CRD的名字為projects.example-gzh.com
。 -
確定
version
。這里的
version
,即spec.version
。如果你的代碼沒有開發完成,或者還在快速迭代中,那么,建議你使用alpha
這樣的命名規則。這樣的好處是,如果別人想使用你的代碼去使用,那么,他單從版本號上就可以很方便的快速知道,你這是一個不穩定的版本。 -
schema校驗
在上面我們的CRD中,我們引入了
spec.validation.openAPIV3Schema
,它的作用是對其中的字段進行校驗,如果用戶在使用我們的CRD時,提供了一個不符合要求的字段后,validation可以很方便的對其進行校驗。除了在這里引用validation之外,我們還可以選擇在admintion controller中通過Validate
階段進行驗證,不過樣是需要開啟admission webhook的。
將上面的代碼保存到一個文件中之后,我們就可以通過kubectl apply -f demo.yaml
進行部署了。我在本機通過minikue啟動了一個K8S集群:
PS C:\Users\guanzenghui> kubectl get po -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system coredns-f9fd979d6-7h2b7 1/1 Running 1 9h
kube-system etcd-minikube 0/1 Running 2 9h
kube-system kube-apiserver-minikube 1/1 Running 2 9h
kube-system kube-controller-manager-minikube 0/1 Running 2 9h
kube-system kube-proxy-p8zb7 1/1 Running 1 9h
kube-system kube-scheduler-minikube 0/1 Running 2 9h
kube-system storage-provisioner 1/1 Running 1 9h
kubernetes-dashboard dashboard-metrics-scraper-c95fcf479-gvhpd 1/1 Running 1 9h
kubernetes-dashboard kubernetes-dashboard-5c448bc4bf-lpwqh 1/1 Running 1 9h
PS C:\Users\guanzenghui> kubectl version
Client Version: version.Info{Major:"1", Minor:"16+", GitVersion:"v1.16.6-beta.0", GitCommit:"e7f962ba86f4ce7033828210ca3556393c377bcc", GitTreeState:"clean", BuildDate:"2020-01-15T08:26:26Z", GoVersion:"go1.13.5", Compiler:"gc", Platform:"windows/amd64"}
Server Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.2", GitCommit:"f5743093fd1c663cb0cbc89748f730662345d44d", GitTreeState:"clean", BuildDate:"2020-09-16T13:32:58Z", GoVersion:"go1.15", Compiler:"gc", Platform:"linux/amd64"}
部署我們的CRD:
PS C:\Users\guanzenghui\Documents> kubectl apply -f .\Untitled-2.yaml
customresourcedefinition.apiextensions.k8s.io/projects.examples-gzh.com created
PS C:\Users\guanzenghui\Documents> kubectl get crd
NAME CREATED AT
projects.examples-gzh.com 2020-09-25T10:40:01Z
如果需要查看其詳情,可以使用命令: kubectl describe crd projects.examples-gzh.com
既然CRD已經創建完成了,接下來我們看一下如何使用這個CRD來創建與之相對應的CR。CR相關的文件內容如下:
apiVersion: "examples-gzh.com/v1alpha1"
kind: Project
metadata:
name: gzh-cr
namespace: default
spec:
replica: 2
創建CR
PS C:\Users\guanzenghui\Documents> kubectl apply -f cr.yaml
project.examples-gzh.com/gzh-cr created
PS C:\Users\guanzenghui\Documents> kubectl get Project
NAME AGE
gzh-cr 39s
接下來,我們將使用client-go來獲取這個CR。
4. 創建golang client
在進行本節前,我假設您已經對client-go、k8s控制器機制有所理解,並且有一定的GoLang的開發經驗。
另外,與其它一些講解Operator的文章不同的是,這些使用CRD的文檔會假設你正在使用某種代碼生成器來自動生成客戶端庫。然而,對於這個過程的文檔很少,而且從閱讀Github上的一些激烈的討論中,我們可以看出,它仍然是一個正在進行中的工作。
本文中,我將堅持使用(大部分)手動實現的客戶端的方式給大家展示。
首先,您可以創建一個自己的項目路徑,並安裝依賴:
mkdir github.com/double12gzh/k8s-crd-demo
go get k8s.io/client-go@v0.17.0
go get k8s.io/apimachinery@v0.17.0
4.1 定義類型
package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
type ProjectSpec struct {
Replicas int `json:"replicas"`
}
type Project struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ProjectSpec `json:"spec"`
}
type ProjectList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Project `json:"items"`
}
metav1.ObjectMeta
中包含了一個比較重要的類型metadata
,k8s中所有的資源有都這個屬性,這里面可以定義諸如:name
,namespace
,label
等的屬性。
4.2 定義DeepCopy方法
Kubernetes API 所服務的每個類型(在本例中,Project 和 ProjectList)都需要實現 k8s.io/apimachinery/pkg/runtime.Object 接口。這個接口定義了兩個方法GetObjectKind()和DeepCopyObject()。第一個方法已經由內嵌的metav1.TypeMeta結構提供了;第二個方法你必須自己實現。
DeepCopyObject方法的目的是生成一個對象的深度拷貝。由於這涉及到大量的模板代碼,所以這些方法通常是自動生成的。在本文中,我們將手動進行。繼續在同一個包中添加第二個文件 deepcopy.go。
package v1alpha1
import "k8s.io/apimachinery/pkg/runtime"
// DeepCopyInto 把一個對象的所有屬性復制給此對象類型的指針
func (in *Project) DeepCopyInto(out *Project) {
out.TypeMeta = in.TypeMeta
out.ObjectMeta = in.ObjectMeta
out.Spec = ProjectSpec{
Replicas: in.Spec.Replicas,
}
}
// DeepCopyObject 返回一個對象類型
func (in *Project) DeepCopyObject() runtime.Object {
out := Project{}
in.DeepCopyInto(&out)
return &out
}
// DeepCopyObject 返回一個對像類型
func (in *ProjectList) DeepCopyObject() runtime.Object {
out := ProjectList{}
out.TypeMeta = in.TypeMeta
out.ListMeta = in.ListMeta
if in.Items != nil {
out.Items = make([]Project, len(in.Items))
for i := range in.Items {
in.Items[i].DeepCopyInto(&out.Items[i])
}
}
return &out
}
上面這個DeepCopy是我們手動來生成的,你可能已經注意到,定義所有這些不同的 DeepCopy 方法並不是一件很有趣的事情。有很多不同的工具和框架可以自動生成這些方法(所有的文檔和整體成熟度都有很大的不同)。我發現效果最好的是控制器生成工具,它是Kubebuilder框架的一部分。
下面我們就來看一下:
go get -u github.com/kubernetes-sigs/controller-tools/cmd/controller-gen
為了能夠使用controller-gen
,我們需要在CRD類型上面的添加一個annotation,如下:
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type Project struct {
// ...
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type ProjectList struct {
// ...
}
說明: 對於這些annotation我們沒有必要去全部記住,只有當使用到的時候再去查閱一下就行,根據二八原則,只需要記住一些常用的就可以了,其它那些不常用的只需要了解一下。
寫好了上述代碼,我們運行一下命令controller-gen object paths=./api/types/v1alpha1/project.go
即可生成需要代碼。
為了更加的簡化,你甚至可以在代碼文件的前面加一個聲明go:generate
,具體請參考。如:
package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
//go:generate controller-gen object paths=$GOFILE
// ...
然后只需要在代碼的根路徑中執行go generate ./...
即可。
4.3 注冊類型
接下來,你需要讓客戶端庫知道你的新類型。這將允許客戶端在與API服務器通信時(或多或少)自動處理你的新類型。
為此,在你的包中添加一個新文件 register.go。
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
const GroupName = "example-gzh.com"
const GroupVersion = "v1alpha1"
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: GroupVersion}
var (
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
AddToScheme = SchemeBuilder.AddToScheme
)
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Project{},
&ProjectList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}
正如你所注意到的,這段代碼還沒有真正做任何事情(除了創建一個新的runtime.SchemeBuilder實例)。重要的部分是AddToScheme函數(第16行),它是第15行創建的runtime.SchemeBuilder類型的導出結構成員。只要Kubernetes客戶端被初始化以注冊你的類型定義,你就可以在以后從客戶端代碼的任何部分調用這個函數。
4.4 創建HTTP Client
在定義了類型並添加了一個方法來在全局方案構建器上注冊它們之后,你現在可以創建一個能夠加載你的自定義資源的HTTP客戶端。
為此,將以下代碼添加到你的包的main.go文件中:
package main
import (
"flag"
"log"
"ks.io/apimachinery/pkg/runtime/schema"
"ks.io/apimachinery/pkg/runtime/serializer"
"github.com/double12gzh/k8s-demo/api/types/valpha"
"ks.io/client-go/kubernetes/scheme"
"ks.io/client-go/rest"
"ks.io/client-go/tools/clientcmd"
)
var kubeconfig string
func init() {
flag.StringVar(&kubeconfig, "kubeconfig", "", "path to Kubernetes config file")
flag.Parse()
}
func main() {
var config *rest.Config
var err error
if kubeconfig == "" {
log.Printf("using in-cluster configuration")
config, err = rest.InClusterConfig()
} else {
log.Printf("using configuration from '%s'", kubeconfig)
config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
}
if err != nil {
panic(err)
}
valpha.AddToScheme(scheme.Scheme)
crdConfig := *config
crdConfig.ContentConfig.GroupVersion = &schema.GroupVersion{Group: valpha.GroupName, Version: valpha.GroupVersion}
crdConfig.APIPath = "/apis"
crdConfig.NegotiatedSerializer = serializer.NewCodecFactory(scheme.Scheme)
crdConfig.UserAgent = rest.DefaultKubernetesUserAgent()
exampleRestClient, err := rest.UnversionedRESTClientFor(&crdConfig)
if err != nil {
panic(err)
}
}
現在你可以使用第48行創建的exampleRestClient來查詢example.martin-helmich.de/v1alpha1 API組中的所有自定義資源。例如:
result := v1alpha1.ProjectList{}
err := exampleRestClient.Get().Resource("projects").Do().Into(&result)
為了以一種更安全的方式使用你的API,通常情況下,我們最好在自己的clientet中封裝這些操作。為此,創建一個新的子包clientet/v1alpha1。
首先,實現一個定義你的API組類型的接口,並將配置設置從你的主方法移到該clientet的構造函數中(下面例子中的NewForConfig)。
package valpha
import (
"github.com/double12gzh/k8s-demo/api/types/valpha"
"ks.io/apimachinery/pkg/runtime/schema"
"ks.io/client-go/kubernetes/scheme"
"ks.io/client-go/rest"
)
type ExampleVAlphaInterface interface {
Projects(namespace string) ProjectInterface
}
type ExampleVAlphaClient struct {
restClient rest.Interface
}
func NewForConfig(c *rest.Config) (*ExampleVAlphaClient, error) {
config := *c
config.ContentConfig.GroupVersion = &schema.GroupVersion{Group: valpha.GroupName, Version: valpha.GroupVersion}
config.APIPath = "/apis"
config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
config.UserAgent = rest.DefaultKubernetesUserAgent()
client, err := rest.RESTClientFor(&config)
if err != nil {
return nil, err
}
return &ExampleVAlphaClient{restClient: client}, nil
}
func (c *ExampleVAlphaClient) Projects(namespace string) ProjectInterface {
return &projectClient{
restClient: c.restClient,
ns: namespace,
}
}
下面的代碼還不能編譯,因為它仍然缺少ProjectInterface
和projectClient
類型。我們稍后將討論這些類型。
ExampleV1Alpha1Interface
和它的實現--ExampleV1Alpha1Client
結構現在是訪問自定義資源的中心點。現在,你可以在main.go
中簡單地調用clientet, err := v1alpha1.NewForConfig(config)
來創建一個新的客戶集。
接下來,你需要實現一個特定的clientset
來訪問Project自定義資源(注意,上面的例子已經使用了ProjectInterface
和projectClient
類型,我們仍然需要提供)。在同一個包中創建第二個文件projects.go
。
package valpha
import (
"github.com/double12gzh/k8s-demo/api/types/valpha"
metav "ks.io/apimachinery/pkg/apis/meta/v"
"ks.io/apimachinery/pkg/watch"
"ks.io/client-go/kubernetes/scheme"
"ks.io/client-go/rest"
)
type ProjectInterface interface {
List(opts metav.ListOptions) (*valpha.ProjectList, error)
Get(name string, options metav.GetOptions) (*valpha.Project, error)
Create(*valpha.Project) (*valpha.Project, error)
Watch(opts metav.ListOptions) (watch.Interface, error)
// ...
}
type projectClient struct {
restClient rest.Interface
ns string
}
func (c *projectClient) List(opts metav.ListOptions) (*valpha.ProjectList, error) {
result := valpha.ProjectList{}
err := c.restClient.
Get().
Namespace(c.ns).
Resource("projects").
VersionedParams(&opts, scheme.ParameterCodec).
Do().
Into(&result)
return &result, err
}
func (c *projectClient) Get(name string, opts metav.GetOptions) (*valpha.Project, error) {
result := valpha.Project{}
err := c.restClient.
Get().
Namespace(c.ns).
Resource("projects").
Name(name).
VersionedParams(&opts, scheme.ParameterCodec).
Do().
Into(&result)
return &result, err
}
func (c *projectClient) Create(project *valpha.Project) (*valpha.Project, error) {
result := valpha.Project{}
err := c.restClient.
Post().
Namespace(c.ns).
Resource("projects").
Body(project).
Do().
Into(&result)
return &result, err
}
func (c *projectClient) Watch(opts metav.ListOptions) (watch.Interface, error) {
opts.Watch = true
return c.restClient.
Get().
Namespace(c.ns).
Resource("projects").
VersionedParams(&opts, scheme.ParameterCodec).
Watch()
}
這個client顯然還不完善,還缺失了刪除、更新等方法。不過,這些方法可以和已有的方法類似實現。看看現有的clientset(例如,Pod clientset)以獲得靈感。
在創建了clientset之后,用它來列出你現有的資源就變得非常容易了。
package main
import (
"fmt"
clientValpha "github.com/double12gzh/k8s-demo/clientset/valpha"
)
// ...
func main() {
// ...
clientSet, err := clientValpha.NewForConfig(config)
if err != nil {
panic(err)
}
projects, err := clientSet.Projects("default").List(metav.ListOptions{})
if err != nil {
panic(err)
}
fmt.Printf("projects found: %+v\n", projects)
}
4.5 生成Informer
在構建Kubernetes Operator時,您通常希望能夠對新創建或更新的資源做出反應。理論上,您可以定期調用List()方法,檢查是否有新資源被添加。在實踐中,這是一個次優的解決方案,尤其是當您有很多這樣的資源時。
大多數Operator的工作方式是通過使用初始List()調用來初始加載資源的所有相關實例,然后使用Watch()調用來訂閱更新。然后,初始對象列表和從Watch接收到的更新被用來構建一個本地緩存,允許快速訪問任何自定義資源,而不必每次都打到API服務器。
這種模式非常常見,以至於client-go庫為此提供了一個助手:k8s.io/client-go/tools/cache包中的Informer。您可以為您的自定義資源構建一個新的 Informer,如下所示:
package main
import (
"time"
"github.com/double12gzh/k8s-demo/api/types/valpha"
client_valpha "github.com/double12gzh/k8s-demo/clientset/valpha"
metav "ks.io/apimachinery/pkg/apis/meta/v"
"ks.io/apimachinery/pkg/runtime"
"ks.io/apimachinery/pkg/util/wait"
"ks.io/apimachinery/pkg/watch"
"ks.io/client-go/tools/cache"
)
func WatchResources(clientSet client_valpha.ExampleVAlphaInterface) cache.Store {
projectStore, projectController := cache.NewInformer(
&cache.ListWatch{
ListFunc: func(lo metav.ListOptions) (result runtime.Object, err error) {
return clientSet.Projects("some-namespace").List(lo)
},
WatchFunc: func(lo metav.ListOptions) (watch.Interface, error) {
return clientSet.Projects("some-namespace").Watch(lo)
},
},
&valpha.Project{},
*time.Minute,
cache.ResourceEventHandlerFuncs{},
)
go projectController.Run(wait.NeverStop)
return projectStore
}
NewInformer
方法返回兩個對象。第二個返回值,控制器控制List()
和Watch()
調用,並在第一個返回值,即存儲中填充一個(或多或少)最近在API服務器上被監視的資源狀態的緩存(在本例中,項目CRD)。
現在,你可以使用 store
來輕松訪問你的 CRD
,要么列出所有的 CRD,要么通過名稱來訪問它們。請記住,存儲函數返回的是通用interface{}
類型,所以您必須將它們類型化回您的CRD類型。
store := WatchResource(clientSet)
project := store.GetByKey("some-namespace/some-project").(*v1alpha1.Project)
5. 總結
為Custom Resources構建客戶端是(至少,目前)只有很少的文檔,有時可能會有點棘手。
如本文所示,為你的Custom Resource建立一個客戶端庫,以及相應的Informer是一個很好的起點,可以構建你自己的Kubernetes Operator,對Custom Resource的變化做出反應。
您可以到我的github上查看完整代碼