《 Kubebuilder v2 使用指南 》-P6-CRD Admission Webhook

CRD Admission Webhook

前言

前面的文章中,实现了Unit资源对象实例持久化之后的controller管理的过程。除此之外,Kubernetes额外支持了一些很有趣且实用的功能,例如经常被用在资源准入控制上的Adminssion Webhook,它是对APIServer接收准入请求的扩展。详情请参考官方文档:

extensible-admission-controllers

认识Adminssion Webhook

什么是Adminssion Webhook?

在官方文档里对的话来说就是:

Admission webhooks are HTTP callbacks that receive admission requests and do something with them. You can define two types of admission webhooks, validating admission webhook and mutating admission webhook. Mutating admission webhooks are invoked first, and can modify objects sent to the API server to enforce custom defaults. After all object modifications are complete, and after the incoming object is validated by the API server, validating admission webhooks are invoked and can reject requests to enforce custom policies.

一句话概括:Adminssion Webhook是APIServer接收到资源准入请求后执行的回调钩子

在K8s中,追求一切皆资源,Webhook也是一种GVK,可以动态地向APIServer注入/修改/更新钩子。

Adminssion Webhook的作用

Adminssion Webhook可以用作:在资源对象实例持久化之前,将 Kubernetes APIServer 的请求进行拦截,加入自定义的逻辑再处理之后,再返回给APIServer,更形象一点说,即是对APIServer接收的请求进行拦截和“篡改”

Adminssion Webhook的分类

mutating webhook

可用作一些"篡改"的逻辑,例如修改pod的结构,为pod注入sidecar容器,istio就是这么干的。

validating webhook

用作校验的逻辑,虽然go是严格的强类型语言,结构体内的字段已经做了type校验,但字段的检验总是相对生硬死板的,validating webhook可以做很好的补充校验。

注意:按照执行顺序,validating webhook在mutating webhook之后执行。

Adminssion Webhook流程图

Kubebuilder使用webhook

回到项目根目录,执行:

# group version kind替换成自己的
mbp-16in:Unit ywq$ kubebuilder create webhook --group custom --version v1 --kind Unit --defaulting --programmatic-validation

Writing scaffold for you to edit...
api/v1/unit_webhook.go

可以看到,api/v1/下多了一个unit_webhook.go文件,里面有4个方法以及列出来了

// Default implements webhook.Defaulter so a webhook will be registered for the type
func (r *Unit) Default() {

	unitlog.Info("default", "name", r.Name)
	// TODO(user): fill in your defaulting logic.

}

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// +kubebuilder:webhook:verbs=create;update,path=/validate-custom-my-crd-com-v1-unit,mutating=false,failurePolicy=fail,groups=custom.my.crd.com,resources=units,versions=v1,name=vunit.kb.io

var _ webhook.Validator = &Unit{}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *Unit) ValidateCreate() error {
	unitlog.Info("validate create", "name", r.Name)

	// TODO(user): fill in your validation logic upon object creation.

}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *Unit) ValidateUpdate(old runtime.Object) error {
	unitlog.Info("validate update", "name", r.Name)

	// TODO(user): fill in your validation logic upon object update.
	return nil
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *Unit) ValidateDelete() error {
	unitlog.Info("validate delete", "name", r.Name)

	// TODO(user): fill in your validation logic upon object deletion.
	return nil
}

从命名和注释就可以看出来,4个方法分别用于:

  • Default() 用于修改,即对应mutating webhook
  • ValidateCreate()用于校验,对应validating webhook,仅在create操作使用
  • ValidateUpdate()用于校验,对应validating webhook,仅在create操作使用
  • ValidateDelete()用于校验,对应validating webhook,仅在create操作使用

Default()ValidateCreate()为例,来实现简单的Unit实例持久化之前的修改Unit默认字段、内容校验的逻辑

Default()

func (r *Unit) Default() {
	unitlog.Info("default", "name", r.Name)

	// TODO(user): fill in your defaulting logic.
	// 这里可以加入一些Unit 结构体对象初始化之前的一些默认的逻辑,比如给一些字段填充默认值

	// default replicas set to 1
	unitlog.Info("default", "name", r.Name)

	if r.Spec.Replicas == nil {
    defaultReplicas := int32(1)
		r.Spec.Replicas = &defaultReplicas
	}

	// add default selector label
	labelMap := make(map[string]string, 1)
	labelMap["app"] = r.Name
	r.Spec.Selector = &metav1.LabelSelector{
		MatchLabels: labelMap,
	}

	// add default template label
	r.Spec.Template.Labels = labelMap

	r.Status.LastUpdateTime = metav1.Now()
	
	// 当然,还可以根据需求加一些适合在初始化时做的逻辑,例如为pod注入sidecar
	// ...

}

给Unit指定默认的副本数、添加默认的自定义标签,还可以添加一些其他的东西,例如sidecar、initContainer等等

###ValidateCreate()

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// +kubebuilder:webhook:verbs=create;update,path=/validate-custom-my-crd-com-v1-unit,mutating=false,failurePolicy=fail,groups=custom.my.crd.com,resources=units,versions=v1,name=vunit.kb.io

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *Unit) ValidateCreate() error {
	unitlog.Info("validate create", "name", r.Name)

	// TODO(user): fill in your validation logic upon object creation.

	// 检查Unit.Spec.Category
	switch r.Spec.Category {
	case CategoryDeployment:
		return nil
	case CategoryStatefulSet:
		return nil
	default:
		err := errors.New("spec.category only support Deployment or StatefulSet")
		unitlog.Error(err, "creating validate failed", "name", r.Name)
		return err
	}
}

因Unit动态支持Sts和Deploy,但只能二取其一,默认Unit结构体内的字段校验显然不适合做这种类型的校验,所以把校验的操作放到这里来。

Kubebuilder marker

在validateCreate()方法上方,有两行特殊的注释不知有没有吸引到你的注意力:

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// +kubebuilder:webhook:verbs=create;update,path=/validate-custom-my-crd-com-v1-unit,mutating=false,failurePolicy=fail,groups=custom.my.crd.com,resources=units,versions=v1,name=vunit.kb.io

第一行注释说明,修改第二行中的verbs字段可以改变validateCreate()的工作插入时机。

看看第二行注释的擡头:+kubebuilder:,有没有突然想起项目中有多处这种擡头的注释。这种格式的注释对kubebuilder有着特殊的意义,被称为marker。在执行make指令的时候,kubebuilder会根据特定位置的marker,进行相应的转换逻辑。

主要在如下3个地方使用到了marker:

webhook

api/v1/unit_webhook.go:39


// +kubebuilder:webhook:path=/mutate-custom-my-crd-com-v1-unit,mutating=true,failurePolicy=fail,groups=custom.my.crd.com,resources=units,verbs=create;update,versions=v1,name=munit.kb.io

var _ webhook.Defaulter = &Unit{}

// Default implements webhook.Defaulter so a webhook will be registered for the type
func (r *Unit) Default() {
	unitlog.Info("default", "name", r.Name)

	...

}

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// +kubebuilder:webhook:verbs=create;update,path=/validate-custom-my-crd-com-v1-unit,mutating=false,failurePolicy=fail,groups=custom.my.crd.com,resources=units,versions=v1,name=vunit.kb.io


// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *Unit) ValidateCreate() error {
	unitlog.Info("validate create", "name", r.Name)
  ...
}

这两个钩子函数的上方,都有marker的存在,这里的marker的内容,会影响最终生成的Webhook资源的配置。

CRD type

api/v1/unit_types.go:79

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.selector
// Unit is the Schema for the units API
type Unit struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec   UnitSpec   `json:"spec,omitempty"`
	Status UnitStatus `json:"status,omitempty"`
}

这里有两个后加入的marker,分别讲一下:

+kubebuilder:subresource:status

上一篇文章中有提过,更新Unit.Status()时,尽量局部更新避免并发冲突。实现Status局部更新,需要指定此marker

+kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.selector

添加这个marker并正确指定replicas字段的specpath以及statuspath后,可以使用kubectl scale命令的形式来方便地修改CRD的replicas值,就像Deploy/Sts一样。

Reconcile

controllers/unit_controller.go:64

// +kubebuilder:rbac:groups=custom.my.crd.com,resources=units,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=custom.my.crd.com,resources=units/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=apps,resources=statefulSet,verbs=get;update;patch;delete
// +kubebuilder:rbac:groups=apps,resources=deployment,verbs=get;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=service,verbs=get;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=endpoint,verbs=get
// +kubebuilder:rbac:groups=core,resources=persistentVolumeClaimStatus,verbs=get;update;patch;delete
// +kubebuilder:rbac:groups=extensions,resources=ingress,verbs=get;update;patch;delete

func (r *UnitReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
...
}

很容易理解,Reconcile控制逻辑运行过程中,难免要对多种资源进行增删改查操作,rbac授权必不可少,因此,每种需要关注的GVK资源的授权都在这里的以marker形式指定,在make之后,会生成相应的rbac yaml文件,体现在config/rbac目录下。

总结

准备工作已经完成了,但代码编写很难一蹴而就,总会遇到error需要反复调试,下一篇开始讲述如何使本地环境与K8s集群连接,在本地环境中进行代码调试。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章