本篇将继续深刻学习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之间的数据转换。
贴下学习资料: