实现默认值/验证的 Webhook
如果您想为您的 CRD 实现 admission webhooks ,您需要做的唯一一件事是实现 CustomDefaulter
和(或)CustomValidator
接口。
Kubebuilder 会为您处理其余的任务,例如:
- 创建 webhook 服务器。
- 确保服务器已在管理器中添加。
- 为您的 webhooks 创建处理程序。
- 在你的服务器中为每个处理程序注册路径。
首先,让我们为我们的自定义资源定义(CronJob)搭建 Webhook。我们需要使用 --defaulting
和 --programmatic-validation
标志运行以下命令(因为我们的测试项目将使用默认和验证 Webhook):
kubebuilder create webhook --group batch --version v1 --kind CronJob --defaulting --programmatic-validation
这将为您构建 webhook 函数,并在您的 main.go
中将 webhook 注册到管理器。
Apache License
Copyright 2025 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.
Go imports
package v1
import (
"context"
"fmt"
"github.com/robfig/cron"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
validationutils "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
batchv1 "tutorial.kubebuilder.io/project/api/v1"
)
Next, we’ll setup a logger for the webhooks.
var cronjoblog = logf.Log.WithName("cronjob-resource")
Then, we set up the webhook with the manager.
// SetupCronJobWebhookWithManager registers the webhook for CronJob in the manager.
func SetupCronJobWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).For(&batchv1.CronJob{}).
WithValidator(&CronJobCustomValidator{}).
WithDefaulter(&CronJobCustomDefaulter{
DefaultConcurrencyPolicy: batchv1.AllowConcurrent,
DefaultSuspend: false,
DefaultSuccessfulJobsHistoryLimit: 3,
DefaultFailedJobsHistoryLimit: 1,
}).
Complete()
}
Notice that we use kubebuilder markers to generate webhook manifests. This marker is responsible for generating a mutating webhook manifest.
The meaning of each marker can be found here.
This marker is responsible for generating a mutation webhook manifest.
// +kubebuilder:webhook:path=/mutate-batch-tutorial-kubebuilder-io-v1-cronjob,mutating=true,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v1,name=mcronjob-v1.kb.io,admissionReviewVersions=v1
// CronJobCustomDefaulter struct is responsible for setting default values on the custom resource of the
// Kind CronJob when those are created or updated.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as it is used only for temporary operations and does not need to be deeply copied.
type CronJobCustomDefaulter struct {
// Default values for various CronJob fields
DefaultConcurrencyPolicy batchv1.ConcurrencyPolicy
DefaultSuspend bool
DefaultSuccessfulJobsHistoryLimit int32
DefaultFailedJobsHistoryLimit int32
}
var _ webhook.CustomDefaulter = &CronJobCustomDefaulter{}
We use the webhook.CustomDefaulter
interface to set defaults to our CRD. A webhook will automatically be served that calls this defaulting.
The Default
method is expected to mutate the receiver, setting the defaults.
// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind CronJob.
func (d *CronJobCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error {
cronjob, ok := obj.(*batchv1.CronJob)
if !ok {
return fmt.Errorf("expected an CronJob object but got %T", obj)
}
cronjoblog.Info("Defaulting for CronJob", "name", cronjob.GetName())
// Set default values
d.applyDefaults(cronjob)
return nil
}
// applyDefaults applies default values to CronJob fields.
func (d *CronJobCustomDefaulter) applyDefaults(cronJob *batchv1.CronJob) {
if cronJob.Spec.ConcurrencyPolicy == "" {
cronJob.Spec.ConcurrencyPolicy = d.DefaultConcurrencyPolicy
}
if cronJob.Spec.Suspend == nil {
cronJob.Spec.Suspend = new(bool)
*cronJob.Spec.Suspend = d.DefaultSuspend
}
if cronJob.Spec.SuccessfulJobsHistoryLimit == nil {
cronJob.Spec.SuccessfulJobsHistoryLimit = new(int32)
*cronJob.Spec.SuccessfulJobsHistoryLimit = d.DefaultSuccessfulJobsHistoryLimit
}
if cronJob.Spec.FailedJobsHistoryLimit == nil {
cronJob.Spec.FailedJobsHistoryLimit = new(int32)
*cronJob.Spec.FailedJobsHistoryLimit = d.DefaultFailedJobsHistoryLimit
}
}
We can validate our CRD beyond what’s possible with declarative validation. Generally, declarative validation should be sufficient, but sometimes more advanced use cases call for complex validation.
For instance, we’ll see below that we use this to validate a well-formed cron schedule without making up a long regular expression.
If webhook.CustomValidator
interface is implemented, a webhook will automatically be served that calls the validation.
The ValidateCreate
, ValidateUpdate
and ValidateDelete
methods are expected to validate its receiver upon creation, update and deletion respectively. We separate out ValidateCreate from ValidateUpdate to allow behavior like making certain fields immutable, so that they can only be set on creation. ValidateDelete is also separated from ValidateUpdate to allow different validation behavior on deletion. Here, however, we just use the same shared validation for ValidateCreate
and ValidateUpdate
. And we do nothing in ValidateDelete
, since we don’t need to validate anything on deletion.
This marker is responsible for generating a validation webhook manifest.
// +kubebuilder:webhook:path=/validate-batch-tutorial-kubebuilder-io-v1-cronjob,mutating=false,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v1,name=vcronjob-v1.kb.io,admissionReviewVersions=v1
// CronJobCustomValidator struct is responsible for validating the CronJob resource
// when it is created, updated, or deleted.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as this struct is used only for temporary operations and does not need to be deeply copied.
type CronJobCustomValidator struct {
// TODO(user): Add more fields as needed for validation
}
var _ webhook.CustomValidator = &CronJobCustomValidator{}
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type CronJob.
func (v *CronJobCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
cronjob, ok := obj.(*batchv1.CronJob)
if !ok {
return nil, fmt.Errorf("expected a CronJob object but got %T", obj)
}
cronjoblog.Info("Validation for CronJob upon creation", "name", cronjob.GetName())
return nil, validateCronJob(cronjob)
}
// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type CronJob.
func (v *CronJobCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
cronjob, ok := newObj.(*batchv1.CronJob)
if !ok {
return nil, fmt.Errorf("expected a CronJob object for the newObj but got %T", newObj)
}
cronjoblog.Info("Validation for CronJob upon update", "name", cronjob.GetName())
return nil, validateCronJob(cronjob)
}
// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type CronJob.
func (v *CronJobCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
cronjob, ok := obj.(*batchv1.CronJob)
if !ok {
return nil, fmt.Errorf("expected a CronJob object but got %T", obj)
}
cronjoblog.Info("Validation for CronJob upon deletion", "name", cronjob.GetName())
// TODO(user): fill in your validation logic upon object deletion.
return nil, nil
}
We validate the name and the spec of the CronJob.
// validateCronJob validates the fields of a CronJob object.
func validateCronJob(cronjob *batchv1.CronJob) error {
var allErrs field.ErrorList
if err := validateCronJobName(cronjob); err != nil {
allErrs = append(allErrs, err)
}
if err := validateCronJobSpec(cronjob); err != nil {
allErrs = append(allErrs, err)
}
if len(allErrs) == 0 {
return nil
}
return apierrors.NewInvalid(
schema.GroupKind{Group: "batch.tutorial.kubebuilder.io", Kind: "CronJob"},
cronjob.Name, allErrs)
}
Some fields are declaratively validated by OpenAPI schema. You can find kubebuilder validation markers (prefixed with // +kubebuilder:validation
) in the Designing an API section. You can find all of the kubebuilder supported markers for declaring validation by running controller-gen crd -w
, or here.
func validateCronJobSpec(cronjob *batchv1.CronJob) *field.Error {
// The field helpers from the kubernetes API machinery help us return nicely
// structured validation errors.
return validateScheduleFormat(
cronjob.Spec.Schedule,
field.NewPath("spec").Child("schedule"))
}
We’ll need to validate the cron schedule is well-formatted.
func validateScheduleFormat(schedule string, fldPath *field.Path) *field.Error {
if _, err := cron.ParseStandard(schedule); err != nil {
return field.Invalid(fldPath, schedule, err.Error())
}
return nil
}
Validate object name
Validating the length of a string field can be done declaratively by the validation schema.
But the ObjectMeta.Name
field is defined in a shared package under the apimachinery repo, so we can’t declaratively validate it using the validation schema.
func validateCronJobName(cronjob *batchv1.CronJob) *field.Error {
if len(cronjob.ObjectMeta.Name) > validationutils.DNS1035LabelMaxLength-11 {
// The job name length is 63 characters like all Kubernetes objects
// (which must fit in a DNS subdomain). The cronjob controller appends
// a 11-character suffix to the cronjob (`-$TIMESTAMP`) when creating
// a job. The job name length limit is 63 characters. Therefore cronjob
// names must have length <= 63-11=52. If we don't validate this here,
// then job creation will fail later.
return field.Invalid(field.NewPath("metadata").Child("name"), cronjob.ObjectMeta.Name, "must be no more than 52 characters")
}
return nil
}