概述
本篇将继续深入学习kubebuilder开发,并介绍一些深入使用时遇到的问题。包括:conversion webhook、finalizer、控制器对CRD的update status等。
status
我们先看一个新建的crd的结构体:
// 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
如果我们想像k8s中的设计那样,那么就要遵循k8s中status subresource
的使用规范:
- 用户只能指定一个CRD实例的spec部分;
- CRD实例的status部分由控制器进行变更。
设计subresource风格的status
-
需要在Bucket的注释中添加一行
// +kubebuilder:subresource:status
,变成如下:// +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中: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.
finalizer
finalizer
即终结器,存在于每一个k8s内的资源实例中,即**.metadata.finalizers
,它是一个字符串数组,每一个成员表示一个finalizer
。控制器在删除某个资源时,会根据该资源的finalizers
配置,进行异步预删除处理,所有的finalizer
都执行完毕后,该资源会被真正删除。
这里的预删除处理,一般指对该资源的关联资源进行增删改操作。比如:一个A资源被删除时,其finalizer规定必须将A资源的Selector指向的所有service都删除。
当我们需要设计这类finalizer时,就可以自定义一个controller来实现。
因为finalizer
的存在,资源的Delete操作,演变成了一个Update操作:给资源加入一个deletiontimestamp
。我们设计controller时,需要对这个字段做好检查。
范例
我们设计一个Bucket类和一个Playbook类,Playbook.Spec.Selector是一个选择器,可以通过该选择器找到对应的Bucket。Playbook控制器需要做以下事情:
- 如果一个Playbook对象没有删除时间戳(被创建或更新),我们检查并配置一个finalizer:
testdelete
给它 - 如果一个Playbook有删除时间戳(被删除),我们检查是否该对象的finalizer包含
testdelete
. - 如果包含,我们检查该Playbook对象的spec.Selector是否不为空
- 如果不为空,我们根据spec.Selector List相同namespace下所有的bucket,并将它们一一删除
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
}
}
cluster-scope
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
**
kubebuilder 注释标记
我们注意到,在设计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
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。
给Reconciler做扩展
增加eventer
我们需要在某些时候创建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)
}
reconciler监控多个资源变化
我们在开发过程中,可能需要开发一个类似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)
}
使用非缓存的client
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。
多版本切换
[toc]
概述
本篇将继续深入学习kubebuilder开发,并介绍一些深入使用时遇到的问题。包括:conversion webhook、finalizer、控制器对CRD的update status等。
status
我们先看一个新建的crd的结构体:
// 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
如果我们想像k8s中的设计那样,那么就要遵循k8s中status subresource
的使用规范:
- 用户只能指定一个CRD实例的spec部分;
- CRD实例的status部分由控制器进行变更。
设计subresource风格的status
-
需要在Bucket的注释中添加一行
// +kubebuilder:subresource:status
,变成如下:// +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中: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.
finalizer
finalizer
即终结器,存在于每一个k8s内的资源实例中,即**.metadata.finalizers
,它是一个字符串数组,每一个成员表示一个finalizer
。控制器在删除某个资源时,会根据该资源的finalizers
配置,进行异步预删除处理,所有的finalizer
都执行完毕后,该资源会被真正删除。
这里的预删除处理,一般指对该资源的关联资源进行增删改操作。比如:一个A资源被删除时,其finalizer规定必须将A资源的Selector指向的所有service都删除。
当我们需要设计这类finalizer时,就可以自定义一个controller来实现。
因为finalizer
的存在,资源的Delete操作,演变成了一个Update操作:给资源加入一个deletiontimestamp
。我们设计controller时,需要对这个字段做好检查。
范例
我们设计一个Bucket类和一个Playbook类,Playbook.Spec.Selector是一个选择器,可以通过该选择器找到对应的Bucket。Playbook控制器需要做以下事情:
- 如果一个Playbook对象没有删除时间戳(被创建或更新),我们检查并配置一个finalizer:
testdelete
给它 - 如果一个Playbook有删除时间戳(被删除),我们检查是否该对象的finalizer包含
testdelete
. - 如果包含,我们检查该Playbook对象的spec.Selector是否不为空
- 如果不为空,我们根据spec.Selector List相同namespace下所有的bucket,并将它们一一删除
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
}
}
cluster-scope
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
**
kubebuilder 注释标记
我们注意到,在设计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
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。
给Reconciler做扩展
增加eventer
我们需要在某些时候创建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)
}
reconciler监控多个资源变化
我们在开发过程中,可能需要开发一个类似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)
}
使用非缓存的client
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之间的数据转换。
贴下相关学习资料,有兴趣的看这个官方的开发文档就清楚了:
https://book.kubebuilder.io/m...