本篇將繼續深刻學習kubebuilder開發,並介紹一些深刻使用時遇到的問題。包括:conversion webhook、finalizer、控制器對CRD的update status等。html
咱們先看一個新建的crd的結構體:node
// BucketStatus defines the observed state of Bucket type BucketStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file Progress int32 `json:"progress"` } // +kubebuilder:object:root=true // Bucket is the Schema for the buckets API type Bucket struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec BucketSpec `json:"spec,omitempty"` Status BucketStatus `json:"status,omitempty"` }
這裏,Spec和Status均是Bucket的成員變量,Status並不像Pod.Status
同樣,是Pod
的subResource
.所以,若是咱們在controller的代碼中調用到Status().Update()
,會觸發panic,並報錯:the server could not find the requested resource
git
若是咱們想像k8s中的設計那樣,那麼就要遵循k8s中status subresource
的使用規範:github
須要在Bucket的註釋中添加一行// +kubebuilder:subresource:status
,變成以下:web
// +kubebuilder:subresource:status // +kubebuilder:object:root=true // Bucket is the Schema for the buckets API type Bucket struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec BucketSpec `json:"spec,omitempty"` Status BucketStatus `json:"status,omitempty"` }
建立Bucket資源時,即使咱們填入了非空的status
結構,也不會更新到apiserver中。Status只能經過對應的client進行更新。好比在controller中:json
if bucket.Status.Progress == 0 { bucket.Status.Progress = 1 err := r.Status().Update(ctx, &bucket) if err != nil { return ctrl.Result{}, err } }
這樣,只要bucket實例的status.Progress
爲0時(好比咱們建立一個bucket實例時,因爲status.Progress
沒法配置,故初始化爲默認值,即0),controller就會幫咱們將它變動爲1.api
注意:
kubebuilder 2.0開發生成的crd模板,沒法經過apiserver的crd校驗。社區有相關的記錄和修復https://github.com/kubernetes...,可是這個修復沒有針對1.11.*版本。
因此1.11.*版本的k8s,要使用kubebuilder 2.0 必須給apiserver配置一個featuregate: - --feature-gates=CustomResourceValidation=false,關閉對crd的校驗。數組
finalizer
即終結器,存在於每個k8s內的資源實例中,即**.metadata.finalizers
,它是一個字符串數組,每個成員表示一個finalizer
。控制器在刪除某個資源時,會根據該資源的finalizers
配置,進行異步預刪除處理,全部的finalizer
都執行完畢後,該資源會被真正刪除。緩存
這裏的預刪除處理,通常指對該資源的關聯資源進行增刪改操做。好比:一個A資源被刪除時,其finalizer規定必須將A資源的Selector指向的全部service都刪除。app
當咱們須要設計這類finalizer時,就能夠自定義一個controller來實現。
由於finalizer
的存在,資源的Delete操做,演變成了一個Update操做:給資源加入一個deletiontimestamp
。咱們設計controller時,須要對這個字段作好檢查。
咱們設計一個Bucket類和一個Playbook類,Playbook.Spec.Selector是一個選擇器,能夠經過該選擇器找到對應的Bucket。Playbook控制器須要作如下事情:
testdelete
給它testdelete
.Reconcile函數中增長以下代碼:
myplaybookFinalizerName := "testdelete" if book.ObjectMeta.DeletionTimestamp.IsZero() { if !containsString(book.ObjectMeta.Finalizers, myplaybookFinalizerName) { book.ObjectMeta.Finalizers = append(book.ObjectMeta.Finalizers, myplaybookFinalizerName) err := r.Update(ctx, &book) if err != nil { return ctrl.Result{}, err } } } else { if containsString(book.ObjectMeta.Finalizers, myplaybookFinalizerName) && book.Spec.Selector != nil { bList := &opsv1.BucketList{} err := r.List(ctx, bList, client.InNamespace(book.Namespace), client.MatchingLabels(book.Spec.Selector)) if err != nil { return ctrl.Result{}, fmt.Errorf("can't find buckets match playbook, %s", err.Error()) } for _, b := range bList.Items { err = r.Delete(ctx, &b) if err != nil { return ctrl.Result{}, fmt.Errorf("can't delete buckets %s/%s, %s",b.Namespace, b.Name, err.Error()) } } book.ObjectMeta.Finalizers = removeString(book.ObjectMeta.Finalizers, myplaybookFinalizerName) err = r.Update(ctx, &book) return ctrl.Result{}, err } }
k8s中node、pv等資源是集羣級別的,它們沒有namespace字段,所以查詢node資源時也無需規定要從哪一個namespace查。
咱們在進行k8s operator時常常也須要設計這樣的字段,可是默認狀況下,kubebuilder會給咱們建立namespace scope的crd資源,能夠經過以下方式修改:
在執行kubebuilder create api ****
後,咱們在生成的資源的*_types.go
文件中,找到資源的主結構體,增長一條註釋kubebuilder:resource:scope=Cluster
,好比:
// +kubebuilder:object:root=true // +kubebuilder:resource:scope=Cluster // Bookbox is the Schema for the bookboxes API type Bookbox struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec BookboxSpec `json:"spec,omitempty"` Status BookboxStatus `json:"status,omitempty"` }
這樣執行make install
,會在config/crd/bases/
目錄下生成對應的crd的yaml文件,裏面就申明瞭該crd的scope:
apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: creationTimestamp: null name: bookboxes.ops.netease.com spec: group: ops.netease.com names: kind: Bookbox plural: bookboxes scope: Cluster **
咱們注意到,在設計subresource風格的status和cluster-scope中咱們都是用kubebuilder的註釋標記,實現咱們想要的資源形態,這裏有更多關於註釋標記的說明,好比:令crd支持kubectl scale
,對crd實例進行基礎的值校驗,容許在kubectl get
命令中顯示crd的更多字段,等等.此處舉兩例:
kubectl get 時顯示crd的status.replicas:
// +kubebuilder:printcolumn:JSONPath=".status.replicas",name=Replicas,type=string
限定字段的值爲固定的幾個:
type Host struct { .. Spec HostSpec } type HostSpec struct { // +kubebuilder:validation:Enum=Wallace;Gromit;Chicken HostName string }
kubebuilder的log使用了第三方包"github.com/go-logr/logr"
。當咱們在開發reconciler時,若是須要在某處打日誌,咱們須要在Reconcile
方法中將
_ = r.Log.WithValues("playbook", req.NamespacedName)
改成
log := r.Log.WithValues("playbook", req.NamespacedName)
從而得到一個logger實例。以後的邏輯中,咱們能夠執行:
log.Info("this is the message", $KEY, $VALUE)
注意,這裏KEY和VALUE都是interface{}結構,能夠是字符串或整型等,他們表示在上下文中記錄的鍵值對,反映到程序日誌中,會是這個樣子:
// code: log.Info("will try get bucket from changed","bucket-name", req.NamespacedName) // output: 2019-09-11T11:53:58.017+0800 INFO controllers.Playbook will try get bucket from changed {"playbook": "default/playbook-sample", "bucket-name": {"namespace": "default", "name": "playbook-sample"}}
logr包提供的logger只有Info和Error兩種類型,但能夠經過V(int)
配置日誌級別。不論是Info仍是Error,都採用上面例子的格式,即:
log.Info(string, {key, value} * n ) log.Error(string, {key, value} * n ) n>=0
若是不遵循這種格式,運行期間會拋出panic。
咱們須要在某些時候建立k8s event進行事件記錄,但Reconciler默認是隻有一個Client接口和一個Logger的:
type PlaybookReconciler struct { client.Client Log logr.Logger }
咱們能夠往struct中添油加醋:
type PlaybookReconciler struct { client.Client Eventer record.EventRecorder Log logr.Logger }
PlaybookReconciler
的初始化在main.go
中,kubebuilder設計的manager自帶了事件廣播的生成方法,直接使用便可:
if err = (&controllers.PlaybookReconciler{ Client: mgr.GetClient(), Eventer: mgr.GetEventRecorderFor("playbook-controller"), Log: ctrl.Log.WithName("controllers").WithName("Playbook"), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Playbook") os.Exit(1) }
咱們在開發過程當中,可能須要開發一個相似service-->selector-->pods
的資源邏輯,那麼,在service的reconciler裏,咱們關注service的seletor的配置,而且檢查匹配的pods是否有所變動(增長或減小),並更新到同名的endpoints裏;同時,咱們還要關注pod的更新,若是pod的label發生變化,那麼要找出全部'以前匹配了這些pod'的service,檢查service的selector是否仍然匹配pod的label,若有變更,也要更新endpoints。
這就意味着,咱們須要能讓reconciler能觀察到service和pod兩種資源的變動。咱們在serviceReconciler的SetupWithManager方法中,能夠看到:
func (r *ServiceReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&opsv1.Service{}). Complete(r) }
只須要在For
方法調用後再調用Watches
方法便可:
func (r *ServiceReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&opsv1.Service{}).Watches(&source.Kind{Type: &opsv1.Pod{}}, &handler.EnqueueRequestForObject{}). Complete(r) }
此外,咱們能夠將service設計爲pod的owner,而後在podController的For
方法後在調用Owns
方法:
func (r *ServiceReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&opsv1.Service{}).Owns(&opsv1.Pod{}). Complete(r) }
咱們在Owns
方法的定義註釋中能夠看到它與Watch方法實際上是相似的:
// Owns defines types of Objects being *generated* by the ControllerManagedBy, and configures the ControllerManagedBy to respond to // create / delete / update events by *reconciling the owner object*. This is the equivalent of calling // Watches(&handler.EnqueueRequestForOwner{&source.Kind{Type: <ForType-apiType>}, &handler.EnqueueRequestForOwner{OwnerType: apiType, IsController: true}) func (blder *Builder) Owns(apiType runtime.Object) *Builder { blder.managedObjects = append(blder.managedObjects, apiType) return blder }
不管是For
,Own
,Watch
,都是kubebuilder中的Builder
提供的,Builder
是kubebuilder開放給用戶構建控制器的惟一合法入口(你還能夠用更hack的手段去構建,可能對源碼形成入侵),它還提供了許多有用的方法,可讓咱們更靈活自由地初始化一個controller。
有時候咱們想讓本身的代碼更加清晰,讓控制器的工做更有針對性。好比上文中舉了一個service經過selector綁定bod的設想:咱們在service的controller中list一遍service實例的selector指向的pod,並與status中的pods記錄進行對比,這意味着,全部對service和pod的操做,都會觸發這個操做。
咱們想要在控制器watch pod資源變動時,檢查pod是否變動了label,若是label沒有變動,就不去執行reconcile,以此省去反覆的list pod操做帶來的開銷。要如何實現呢?
Builder
爲咱們提供了另外一個方法:
func (blder *Builder) WithEventFilter(p predicate.Predicate) *Builder
這個方法,是爲Builder中每一個Watch
的對象設計一個變動過濾器:Predicate
。Predicate
實現了幾個方法:
type Predicate interface { // Create returns true if the Create event should be processed Create(event.CreateEvent) bool // Delete returns true if the Delete event should be processed Delete(event.DeleteEvent) bool // Update returns true if the Update event should be processed Update(event.UpdateEvent) bool // Generic returns true if the Generic event should be processed Generic(event.GenericEvent) bool }
咱們以此設計一個本身的predicate:
package controllers import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/event" ) type ResourceLabelChangedPredicate struct { predicate.Funcs } func (rl *ResourceLabelChangedPredicate) Update (e event.UpdateEvent) bool{ if !compareMaps(e.MetaOld.GetLabels(), e.MetaNew.GetLabels()) { return true } return false }
而後修改註冊控制器的方式:
func (r *ServiceReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&opsv1.Service{}).Watches(&source.Kind{Type: &opsv1.Pod{}}, &handler.EnqueueRequestForObject{}).WithEventFilter(&ResourceLabelChangedPredicate{}). Complete(r) }
這樣,ServiceReconciler
在監聽其關注的對象時,只會關注對象的label是否發生變動,只有當label發生變動時,纔會入隊並進入reconcile
邏輯。
這個方法目前看應該是kubebuilder團隊推薦使用的方法,可是有個問題是,加入了predicate後,會在Reconciler
關注的全部的對象上生效。也就是說即便Service實例的label發生變動,也會觸發reconcile
。這不是咱們想看到的,咱們想看到的是Service的selector變動時會進行reconcile。這時候咱們可能就須要在predicate中增長對象類型的判斷,好比:
func (rl *ResourceLabelChangedPredicate) Update (e event.UpdateEvent) bool{ oldobj, ok1 := e.ObjectOld.(*opsv1.Service) newobj, ok2 := e.ObjectNew.(*opsv1.Service) if ok1 && ok2 { if !compareMaps(oldobj.Spec.Selector, newobj.Spec.Selector) { return true } else { return false } } _, ok1 = e.ObjectOld.(*opsv1.Pod) _, ok2 = e.ObjectNew.(*opsv1.Pod) if ok1 && ok2 { if !compareMaps(e.MetaOld.GetLabels(), e.MetaNew.GetLabels()) { return true } } return false }
咱們先看上面提到的Watch
方法,這個方法容許用戶本身設計handler.EventHandler
接口,這個接口實現了Create
,Update
,Delete
,Generic
方法,用來在資源實例的不一樣生命階段,進行判斷與入隊。
在sigs.k8s.io/controller-runtime/pkg/handler/enqueue.go
中就有一個默認的實現:EnqueueRequestForObject
。咱們能夠參考它設計一個本身的接口實現——名爲EnqueueRequestForLabelChanged
的入隊器.
重寫該入隊器的Update
方法,改成判斷新舊兩個實例的label是否一致,不一致則進行入隊:
func (e *EnqueueRequestForLabelChanged) Update(evt event.UpdateEvent, q workqueue.RateLimitingInterface) { if !compareMaps(evt.MetaOld.GetLabels(), evt.MetaNew.GetLabels()) { q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ Name: evt.MetaNew.GetName(), Namespace: evt.MetaNew.GetNamespace(), }}) } }
註冊reconciler時,watches的eventhandler參數使用自定義的enqueue:
func (r *PlaybookReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&opsv1.Playbook{}).Watches(&source.Kind{Type: &opsv1.Bucket{}}, &EnqueueRequestForLabelChanged{}). Complete(r) }
這樣,ServiceReconciler將會監聽service資源的全部變動,以及pod資源的label變動。
經過前文咱們瞭解到:
WithEventFilter
配置變動過濾器,能夠針對reconciler
watch的全部資源,統一地設置事件監聽規則;EventHandler
,能夠在reconciler
watch特定資源時,設置該資源的事件監聽規則。閱讀controller-runtime的代碼咱們會發現,官方容許用戶調用WithEventFilter
配置變動過濾器,但沒有提供一個公開的方法讓用戶配置入隊器,用戶只能本身主動實現。其實在1.X的kubebuilder中,Watch
方法容許用戶配置predicate,用戶能夠給不一樣資源配置不一樣的變動過濾器。但在2.0中,這個函數被從新封裝,再也不直接開放給用戶。取而代之的是用WithEventFilter
方法配置應用到全部資源的變動過濾器。
可能設計者認爲,一個reconciler
要負責的應該是一個/多個資源對象的一種/同種變化。
事實上,在開發operator的過程當中,最好也是將一個reconciler的工做內容細粒度化。特別是:不該該在一個reconciler邏輯中進行兩次資源的update(update status除外),不然會引起版本不一致的報錯。
Reconciler中的client.Client
是一個結構,提供了Get,List,Update,Delete等一系列k8s client的操做,可是其Get,List方法均是從cache中獲取數據,若是Reconciler同步數據不及時(須要注意,實際上同步數據的是manager中的成員對象:cache,Reconciler直接引用了該對象),獲取到的就是髒數據。
與EventRecorder相似地, manger中其實也初始化好了一個即時的client:apiReader,供咱們使用,只須要調用mgr.GetAPIReader()
便可獲取。
注意到apiReader是一個只讀client,,其使用方法與Reconciler的Client相似(Get方法,List方法):
r.ApiReader.Get(ctx, req.NamespacedName, bucket)
官方建議咱們直接使用帶cache的client便可,該client是一個分離的client,其讀方法(get,list)均從一個cache中獲取數據。寫方法則直接更新到apiserver。
在crd的開發和演進過程當中,必然會存在一個crd的不一樣版本。 kubebuilder支持以一個conversion webhook
的方式,支持對一個crd資源以不一樣版本進行讀取。簡單地描述就是:
kubectl apply -f config/samples/batch_v2_cronjob.yaml
建立一個v2的cronjob後,能夠經過v1和v2兩種版本進行讀取:
kubectl get cronjobs.v2.batch.tutorial.kubebuilder.io -o yaml kubectl get cronjobs.v1.batch.tutorial.kubebuilder.io -o yaml
顯然,get命令獲得的v1和v2版本的cronjob會存在一些字段上的不一樣,conversion webhook
會負責進行不一樣版本的cronjob之間的數據轉換。
貼下學習資料: