您好,登錄后才能下訂單哦!
這篇文章主要介紹“生成CRD與自定義控制器的方法”的相關(guān)知識(shí),小編通過(guò)實(shí)際案例向大家展示操作過(guò)程,操作方法簡(jiǎn)單快捷,實(shí)用性強(qiáng),希望這篇“生成CRD與自定義控制器的方法”文章能幫助大家解決問(wèn)題。
我們可以使用code-generator 以及controller-tools來(lái)進(jìn)行代碼自動(dòng)生成,通過(guò)代碼自動(dòng)生成可以幫我們自動(dòng)生成 CRD 資源對(duì)象,以及客戶端訪問(wèn)的 ClientSet、Informer、Lister 等工具包,接下來(lái)我們就來(lái)了解下如何編寫一個(gè)自定義的控制器。
首先初始化項(xiàng)目:
$ mkdir operator-crd && cd operator-crd $ go mod init operator-crd $ mkdir -p pkg/apis/example.com/v1
在該文件夾下新建doc.go
文件,內(nèi)容如下所示:
// +k8s:deepcopy-gen=package // +groupName=example.com package v1
根據(jù) CRD 的規(guī)范定義,這里我們定義的 group 為example.com
,版本為v1
,在頂部添加了一個(gè)代碼自動(dòng)生成的deepcopy-gen
的 tag,為整個(gè)包中的類型生成深拷貝方法。
然后就是非常重要的資源對(duì)象的結(jié)構(gòu)體定義,新建types.go
文件,types.go內(nèi)容可以使用type-scaffpld
自動(dòng)生成,具體文件內(nèi)容如下:
package v1 import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" // BarSpec defines the desired state of Bar type BarSpec struct { // INSERT ADDITIONAL SPEC FIELDS -- desired state of cluster DeploymentName string `json:"deploymentName"` Image string `json:"image"` Replicas *int32 `json:"replicas"` } // BarStatus defines the observed state of Bar. // It should always be reconstructable from the state of the cluster and/or outside world. type BarStatus struct { // INSERT ADDITIONAL STATUS FIELDS -- observed state of cluster } // 下面這個(gè)一定不能少,少了的話不能生成 lister 和 informer // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // Bar is the Schema for the bars API // +k8s:openapi-gen=true type Bar struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec BarSpec `json:"spec,omitempty"` Status BarStatus `json:"status,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // BarList contains a list of Bar type BarList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []Bar `json:"items"` }
然后可以參考系統(tǒng)內(nèi)置的資源對(duì)象,還需要提供 AddToScheme 與 Resource 兩個(gè)變量供 client 注冊(cè),新建 register.go 文件,內(nèi)容如下所示:
package v1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) // SchemeGroupVersion 注冊(cè)自己的自定義資源 var SchemeGroupVersion = schema.GroupVersion{Group: "example.com", Version: "v1"} // Kind takes an unqualified kind and returns back a Group qualified GroupKind func Kind(kind string) schema.GroupKind { return SchemeGroupVersion.WithKind(kind).GroupKind() } // Resource takes an unqualified resource and returns a Group qualified GroupResource func Resource(resource string) schema.GroupResource { return SchemeGroupVersion.WithResource(resource).GroupResource() } var ( // SchemeBuilder initializes a scheme builder SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) // AddToScheme is a global function that registers this API group & version to a scheme AddToScheme = SchemeBuilder.AddToScheme ) // Adds the list of known types to Scheme. func addKnownTypes(scheme *runtime.Scheme) error { // 添加 Bar 與 BarList這兩個(gè)資源到 scheme scheme.AddKnownTypes(SchemeGroupVersion, &Bar{}, &BarList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil }
使用controller-gen
生成crd:
$ controller-gen crd paths=./... output:crd:dir=crd
生成example.com_bars.yaml文件如下所示:
--- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: (devel) creationTimestamp: null name: bars.example.com spec: group: example.com names: kind: Bar listKind: BarList plural: bars singular: bar scope: Namespaced versions: - name: v1 schema: openAPIV3Schema: description: Bar is the Schema for the bars API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object spec: description: BarSpec defines the desired state of Bar properties: deploymentName: description: INSERT ADDITIONAL SPEC FIELDS -- desired state of cluster type: string image: type: string replicas: format: int32 type: integer required: - deploymentName - image - replicas type: object status: description: BarStatus defines the observed state of Bar. It should always be reconstructable from the state of the cluster and/or outside world. type: object type: object served: true storage: true
最終項(xiàng)目結(jié)構(gòu)如下所示:
$ tree . ├── crd │ └── example.com_bars.yaml ├── go.mod ├── go.sum └── pkg └── apis └── example.com └── v1 ├── doc.go ├── register.go └── types.go 5 directories, 6 files
上面我們準(zhǔn)備好資源的 API 資源類型后,就可以使用開(kāi)始生成 CRD 資源的客戶端使用的相關(guān)代碼了。
首先創(chuàng)建生成代碼的腳本,下面這些腳本均來(lái)源于sample-controller提供的示例:
$ mkdir hack && cd hack
在該目錄下面新建 tools.go 文件,添加 code-generator 依賴,因?yàn)樵跊](méi)有代碼使用 code-generator 時(shí),go module 默認(rèn)不會(huì)為我們依賴此包。文件內(nèi)容如下所示:
// +build tools // 建立 tools.go 來(lái)依賴 code-generator // 因?yàn)樵跊](méi)有代碼使用 code-generator 時(shí),go module 默認(rèn)不會(huì)為我們依賴此包. package tools import _ "k8s.io/code-generator"
然后新建 update-codegen.sh 腳本,用來(lái)配置代碼生成的腳本:
#!/usr/bin/env bash set -o errexit set -o nounset set -o pipefail SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ../code-generator)} bash "${CODEGEN_PKG}"/generate-groups.sh "deepcopy,client,informer,lister" \ operator-crd/pkg/client operator-crd/pkg/apis example.com:v1 \ --output-base "${SCRIPT_ROOT}"/../ \ --go-header-file "${SCRIPT_ROOT}"/hack/boilerplate.go.txt # To use your own boilerplate text append: # --go-header-file "${SCRIPT_ROOT}"/hack/custom-boilerplate.go.txt
同樣還有 verify-codegen.sh 腳本,用來(lái)校驗(yàn)生成的代碼是否是最新的:
#!/usr/bin/env bash set -o errexit set -o nounset set -o pipefail SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. DIFFROOT="${SCRIPT_ROOT}/pkg" TMP_DIFFROOT="${SCRIPT_ROOT}/_tmp/pkg" _tmp="${SCRIPT_ROOT}/_tmp" cleanup() { rm -rf "${_tmp}" } trap "cleanup" EXIT SIGINT cleanup mkdir -p "${TMP_DIFFROOT}" cp -a "${DIFFROOT}"/* "${TMP_DIFFROOT}" "${SCRIPT_ROOT}/hack/update-codegen.sh" echo "diffing ${DIFFROOT} against freshly generated codegen" ret=0 diff -Naupr "${DIFFROOT}" "${TMP_DIFFROOT}" || ret=$? cp -a "${TMP_DIFFROOT}"/* "${DIFFROOT}" if [[ $ret -eq 0 ]] then echo "${DIFFROOT} up to date." else echo "${DIFFROOT} is out of date. Please run hack/update-codegen.sh" exit 1 fi
還有一個(gè)為生成的代碼文件添加頭部?jī)?nèi)容的 boilerplate.go.txt 文件,內(nèi)容如下所示,其實(shí)就是為每個(gè)生成的代碼文件頭部添加上下面的開(kāi)源協(xié)議聲明:
/* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */
接下來(lái)我們就可以來(lái)執(zhí)行代碼生成的腳本了,首先將依賴包放置到 vendor 目錄中去:
$ go mod vendor
然后執(zhí)行腳本生成代碼:
$ chmod +x ./hack/update-codegen.sh $./hack/update-codegen.sh Generating deepcopy funcs Generating clientset for example.com:v1 at operator-crd/pkg/client/clientset Generating listers for example.com:v1 at operator-crd/pkg/client/listers Generating informers for example.com:v1 at operator-crd/pkg/client/informers
代碼生成后,整個(gè)項(xiàng)目的 pkg 包變成了下面的樣子:
$ tree pkg pkg ├── apis │ └── example.com │ └── v1 │ ├── doc.go │ ├── register.go │ ├── types.go │ └── zz_generated.deepcopy.go └── client ├── clientset │ └── versioned │ ├── clientset.go │ ├── doc.go │ ├── fake │ │ ├── clientset_generated.go │ │ ├── doc.go │ │ └── register.go │ ├── scheme │ │ ├── doc.go │ │ └── register.go │ └── typed │ └── example.com │ └── v1 │ ├── bar.go │ ├── doc.go │ ├── example.com_client.go │ ├── fake │ │ ├── doc.go │ │ ├── fake_bar.go │ │ └── fake_example.com_client.go │ └── generated_expansion.go ├── informers │ └── externalversions │ ├── example.com │ │ ├── interface.go │ │ └── v1 │ │ ├── bar.go │ │ └── interface.go │ ├── factory.go │ ├── generic.go │ └── internalinterfaces │ └── factory_interfaces.go └── listers └── example.com └── v1 ├── bar.go └── expansion_generated.go 20 directories, 26 files
仔細(xì)觀察可以發(fā)現(xiàn)pkg/apis/example.com/v1目錄下面多了一個(gè)zz_generated.deepcopy.go文件,在pkg/client文件夾下生成了 clientset和 informers 和 listers 三個(gè)目錄,有了這幾個(gè)自動(dòng)生成的客戶端相關(guān)操作包,我們就可以去訪問(wèn) CRD 資源了,可以和使用內(nèi)置的資源對(duì)象一樣去對(duì) Bar 進(jìn)行 List 和 Watch 操作了。
首先要先獲取訪問(wèn)資源對(duì)象的 ClientSet,在項(xiàng)目根目錄下面新建 main.go 文件。
package main import ( "k8s.io/client-go/tools/clientcmd" "k8s.io/klog/v2" clientset "operator-crd/pkg/client/clientset/versioned" "operator-crd/pkg/client/informers/externalversions" "time" "os" "os/signal" "syscall" ) var ( onlyOneSignalHandler = make(chan struct{}) shutdownSignals = []os.Signal{os.Interrupt, syscall.SIGTERM} ) // 注冊(cè) SIGTERM 和 SIGINT 信號(hào) // 返回一個(gè) stop channel, 該通道在捕獲到第一個(gè)信號(hào)時(shí)被關(guān)閉 // 如果捕獲到第二個(gè)信號(hào),程序直接退出 func setupSignalHandler() (stopCh <-chan struct{}) { // 當(dāng)調(diào)用兩次的時(shí)候 panics close(onlyOneSignalHandler) stop := make(chan struct{}) c := make(chan os.Signal, 2) // Notify 函數(shù)讓 signal 包將輸入信號(hào)轉(zhuǎn)發(fā)到c // 如果沒(méi)有列出要傳遞的信號(hào),會(huì)將所有輸入信號(hào)傳遞到 c; 否則只會(huì)傳遞列出的輸入信號(hào) signal.Notify(c, shutdownSignals...) go func() { <-c close(stop) <-c os.Exit(1) // 第二個(gè)信號(hào)直接退出 }() return stop } func main() { stopCh := setupSignalHandler() // 獲取config config, err := clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile) if err != nil { klog.Fatalln(err) } // 通過(guò)config構(gòu)建clientSet // 這里的clientSet 是 Bar 的 clientSet, err := clientset.NewForConfig(config) if err != nil { klog.Fatalln(err) } // informerFactory 工廠類, 這里注入我們通過(guò)代碼生成的 client // client 主要用于和 API Server 進(jìn)行通信,實(shí)現(xiàn) ListAndWatch factory := externalversions.NewSharedInformerFactory(clientSet, time.Second*30) // 實(shí)例化自定義控制器 controller := NewController(factory.Example().V1().Bars()) // 啟動(dòng) informer,開(kāi)始list 和 watch go factory.Start(stopCh) // 啟動(dòng)控制器 if err = controller.Run(2, stopCh); err != nil { klog.Fatalf("Error running controller: %s", err.Error()) } }
首先初始化一個(gè)用于訪問(wèn) Bar 資源的 ClientSet 對(duì)象,然后同樣新建一個(gè) Bar 的 InformerFactory 實(shí)例,通過(guò)這個(gè)工廠實(shí)例可以去啟動(dòng) Informer 開(kāi)始對(duì) Bar 的 List 和 Watch 操作,然后同樣我們要自己去封裝一個(gè)自定義的控制器,在這個(gè)控制器里面去實(shí)現(xiàn)一個(gè)控制循環(huán),不斷對(duì) Bar 的狀態(tài)進(jìn)行調(diào)諧。
在項(xiàng)目根目錄下新建controller.go文件,內(nèi)容如下所示:
package main import ( "fmt" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" "k8s.io/klog/v2" v1 "operator-crd/pkg/apis/example.com/v1" "time" informers "operator-crd/pkg/client/informers/externalversions/example.com/v1" ) type Controller struct { informer informers.BarInformer workqueue workqueue.RateLimitingInterface } func NewController(informer informers.BarInformer) *Controller { controller := &Controller{ informer: informer, // WorkQueue 的實(shí)現(xiàn),負(fù)責(zé)同步 Informer 和控制循環(huán)之間的數(shù)據(jù) workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "bar"), } klog.Info("Setting up Bar event handlers") // informer 注冊(cè)了三個(gè) Handler(AddFunc、UpdateFunc 和 DeleteFunc) // 分別對(duì)應(yīng) API 對(duì)象的“添加”“更新”和“刪除”事件。 // 而具體的處理操作,都是將該事件對(duì)應(yīng)的 API 對(duì)象加入到工作隊(duì)列中 informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: controller.addBar, UpdateFunc: controller.updateBar, DeleteFunc: controller.deleteBar, }) return controller } func (c *Controller) Run(thread int, stopCh <-chan struct{}) error { defer runtime.HandleCrash() defer c.workqueue.ShuttingDown() // 記錄開(kāi)始日志 klog.Info("Starting Bar control loop") klog.Info("Waiting for informer caches to sync") // 等待緩存同步數(shù)據(jù) if ok := cache.WaitForCacheSync(stopCh, c.informer.Informer().HasSynced); !ok { return fmt.Errorf("failed to wati for caches to sync") } klog.Info("Starting workers") for i := 0; i < thread; i++ { go wait.Until(c.runWorker, time.Second, stopCh) } klog.Info("Started workers") <-stopCh klog.Info("Shutting down workers") return nil } // runWorker 是一個(gè)不斷運(yùn)行的方法,并且一直會(huì)調(diào)用 c.processNextWorkItem 從 workqueue讀取消息 func (c *Controller) runWorker() { for c.processNExtWorkItem() { } } // 從workqueue讀取和讀取消息 func (c *Controller) processNExtWorkItem() bool { // 獲取 item item, shutdown := c.workqueue.Get() if shutdown { return false } if err := func(item interface{}) error { // 標(biāo)記以及處理 defer c.workqueue.Done(item) var key string var ok bool if key, ok = item.(string); !ok { // 判讀key的類型不是字符串,則直接丟棄 c.workqueue.Forget(item) runtime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", item)) return nil } if err := c.syncHandler(key); err != nil { return fmt.Errorf("error syncing '%s':%s", item, err.Error()) } c.workqueue.Forget(item) return nil }(item); err != nil { runtime.HandleError(err) return false } return true } // 嘗試從 Informer 維護(hù)的緩存中拿到了它所對(duì)應(yīng)的 Bar 對(duì)象 func (c *Controller) syncHandler(key string) error { namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { runtime.HandleError(fmt.Errorf("invalid respirce key:%s", key)) return err } bar, err := c.informer.Lister().Bars(namespace).Get(name) if err != nil { if errors.IsNotFound(err) { // 說(shuō)明是在刪除事件中添加進(jìn)來(lái)的 return nil } runtime.HandleError(fmt.Errorf("failed to get bar by: %s/%s", namespace, name)) return err } fmt.Printf("[BarCRD] try to process bar:%#v ...", bar) // 可以根據(jù)bar來(lái)做其他的事。 // todo return nil } func (c *Controller) addBar(item interface{}) { var key string var err error if key, err = cache.MetaNamespaceKeyFunc(item); err != nil { runtime.HandleError(err) return } c.workqueue.AddRateLimited(key) } func (c *Controller) deleteBar(item interface{}) { var key string var err error if key, err = cache.DeletionHandlingMetaNamespaceKeyFunc(item); err != nil { runtime.HandleError(err) return } fmt.Println("delete crd") c.workqueue.AddRateLimited(key) } func (c *Controller) updateBar(old, new interface{}) { oldItem := old.(*v1.Bar) newItem := new.(*v1.Bar) // 比較兩個(gè)資源版本,如果相同,則不處理 if oldItem.ResourceVersion == newItem.ResourceVersion { return } c.workqueue.AddRateLimited(new) }
我們這里自定義的控制器只封裝了一個(gè) Informer 和一個(gè)限速隊(duì)列,我們當(dāng)然也可以在里面添加一個(gè)用于訪問(wèn)本地緩存的 Indexer,但實(shí)際上 Informer 中已經(jīng)包含了 Lister,對(duì)于 List 和 Get 操作都會(huì)去通過(guò) Indexer 從本地緩存中獲取數(shù)據(jù),所以只用一個(gè) Informer 也是完全可行的。
同樣在 Informer 中注冊(cè)了3個(gè)事件處理器,將監(jiān)聽(tīng)的事件獲取到后送入 workqueue 隊(duì)列,然后通過(guò)控制器的控制循環(huán)不斷從隊(duì)列中消費(fèi)數(shù)據(jù),根據(jù)獲取的 key 來(lái)獲取數(shù)據(jù)判斷對(duì)象是需要?jiǎng)h除還是需要進(jìn)行其他業(yè)務(wù)處理,這里我們同樣也只是打印出了對(duì)應(yīng)的操作日志,對(duì)于實(shí)際的項(xiàng)目則進(jìn)行相應(yīng)的業(yè)務(wù)邏輯處理即可。
到這里一個(gè)完整的自定義 API 對(duì)象和它所對(duì)應(yīng)的自定義控制器就編寫完畢了。
接下來(lái)我們直接運(yùn)行我們的main函數(shù):
I0512 16:51:33.922138 39032 controller.go:29] Setting up Bar event handlers I0512 16:51:33.922255 39032 controller.go:47] Starting Bar control loop I0512 16:51:33.922258 39032 controller.go:48] Waiting for informer caches to sync I0512 16:51:34.023108 39032 controller.go:55] Starting workers I0512 16:51:34.023153 39032 controller.go:60] Started workers
現(xiàn)在我們創(chuàng)建一個(gè)Bar資源對(duì)象:
# bar.yaml apiVersion: example.com/v1 kind: Bar metadata: name: bar-demo namespace: default spec: image: "nginx:1.17.1" deploymentName: example-bar replicas: 2
直接創(chuàng)建上面的對(duì)象,注意觀察控制器的日志:
I0512 16:51:33.922138 39032 controller.go:29] Setting up Bar event handlers I0512 16:51:33.922255 39032 controller.go:47] Starting Bar control loop I0512 16:51:33.922258 39032 controller.go:48] Waiting for informer caches to sync I0512 16:51:34.023108 39032 controller.go:55] Starting workers I0512 16:51:34.023153 39032 controller.go:60] Started workers [BarCRD] try to process bar:"bar-demo" ...
可以看到,我們上面創(chuàng)建 bar.yaml 的操作,觸發(fā)了 EventHandler 的添加事件,從而被放進(jìn)了工作隊(duì)列。然后控制器的控制循環(huán)從隊(duì)列里拿到這個(gè)對(duì)象,并且打印出了正在處理這個(gè) bar 對(duì)象的日志信息。
同樣我們刪除這個(gè)資源的時(shí)候,也會(huì)有對(duì)應(yīng)的提示。
關(guān)于“生成CRD與自定義控制器的方法”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí),可以關(guān)注億速云行業(yè)資訊頻道,小編每天都會(huì)為大家更新不同的知識(shí)點(diǎn)。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。