Implementing admission webhooks

If you want to implement admission webhooksfor your CRD, the only thing you need to do is to implement the Defaulterand (or) the Validator interface.

Kubebuilder takes care of the rest for you, such as

  • Creating the webhook server.
  • Ensuring the server has been added in the manager.
  • Creating handlers for your webhooks.
  • Registering each handler with a path in your server.

Apache License

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

  1. http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, softwaredistributed 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 andlimitations under the License. Go imports

  1. package v1
  2. import (
  3. "github.com/robfig/cron"
  4. apierrors "k8s.io/apimachinery/pkg/api/errors"
  5. "k8s.io/apimachinery/pkg/runtime"
  6. "k8s.io/apimachinery/pkg/runtime/schema"
  7. validationutils "k8s.io/apimachinery/pkg/util/validation"
  8. "k8s.io/apimachinery/pkg/util/validation/field"
  9. ctrl "sigs.k8s.io/controller-runtime"
  10. logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
  11. "sigs.k8s.io/controller-runtime/pkg/webhook"
  12. )

Next, we’ll setup a logger for the webhooks.

  1. var cronjoblog = logf.Log.WithName("cronjob-resource")

Then, we set up the webhook with the manager.

  1. func (r *CronJob) SetupWebhookWithManager(mgr ctrl.Manager) error {
  2. return ctrl.NewWebhookManagedBy(mgr).
  3. For(r).
  4. Complete()
  5. }

Notice that we use kubebuilder markers to generate webhook manifests.This markers is responsible for generating a mutating webhook manifest.

The meaning of each marker can be found here.

  1. // +kubebuilder:webhook:path=/mutate-batch-tutorial-kubebuilder-io-v1-cronjob,mutating=true,failurePolicy=fail,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v1,name=mcronjob.kb.io

We use the webhook.Defaulter 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.

  1. var _ webhook.Defaulter = &CronJob{}
  2. // Default implements webhook.Defaulter so a webhook will be registered for the type
  3. func (r *CronJob) Default() {
  4. cronjoblog.Info("default", "name", r.Name)
  5. if r.Spec.ConcurrencyPolicy == "" {
  6. r.Spec.ConcurrencyPolicy = AllowConcurrent
  7. }
  8. if r.Spec.Suspend == nil {
  9. r.Spec.Suspend = new(bool)
  10. }
  11. if r.Spec.SuccessfulJobsHistoryLimit == nil {
  12. r.Spec.SuccessfulJobsHistoryLimit = new(int32)
  13. *r.Spec.SuccessfulJobsHistoryLimit = 3
  14. }
  15. if r.Spec.FailedJobsHistoryLimit == nil {
  16. r.Spec.FailedJobsHistoryLimit = new(int32)
  17. *r.Spec.FailedJobsHistoryLimit = 1
  18. }
  19. }

Notice that we use kubebuilder markers to generate webhook manifests.This markers is responsible for generating a validating webhook manifest.

The meaning of each marker can be found here.

  1. // +kubebuilder:webhook:path=/validate-batch-tutorial-kubebuilder-io-v1-cronjob,mutating=false,failurePolicy=fail,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v1,name=vcronjob.kb.io

To validate our CRD beyond what’s possible with declarative validation.Generally, declarative validation should be sufficient, but sometimes moreadvanced use cases call for complex validation.

For instance, we’ll see below that we use this to validate a well-formed cronschedule without making up a long regular expression.

If webhook.Validator interface is implemented, a webhook will automatically beserved that calls the validation.

The ValidateCreate and ValidateUpdate methods are expected to validate that itsreceiver upon creation and update respectively. We separate out ValidateCreatefrom ValidateUpdate to allow behavior like making certain fields immutable, sothat they can only be set on creation.Here, however, we just use the same shared validation.

  1. var _ webhook.Validator = &CronJob{}
  2. // ValidateCreate implements webhook.Validator so a webhook will be registered for the type
  3. func (r *CronJob) ValidateCreate() error {
  4. cronjoblog.Info("validate create", "name", r.Name)
  5. return r.validateCronJob()
  6. }
  7. // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
  8. func (r *CronJob) ValidateUpdate(old runtime.Object) error {
  9. cronjoblog.Info("validate update", "name", r.Name)
  10. return r.validateCronJob()
  11. }

We validate the name and the spec of the CronJob.

  1. func (r *CronJob) validateCronJob() error {
  2. var allErrs field.ErrorList
  3. if err := r.validateCronJobName(); err != nil {
  4. allErrs = append(allErrs, err)
  5. }
  6. if err := r.validateCronJobSpec(); err != nil {
  7. allErrs = append(allErrs, err)
  8. }
  9. if len(allErrs) == 0 {
  10. return nil
  11. }
  12. return apierrors.NewInvalid(
  13. schema.GroupKind{Group: "batch.tutorial.kubebuilder.io", Kind: "CronJob"},
  14. r.Name, allErrs)
  15. }

Some fields are declaratively validated by OpenAPI schema.You can find kubebuilder validation markers (prefixedwith // +kubebuilder:validation) in the APIYou can find all of the kubebuilder supported markers fordeclaring validation by running controller-gen crd -w,or here.

  1. func (r *CronJob) validateCronJobSpec() *field.Error {
  2. // The field helpers from the kubernetes API machinery help us return nicely
  3. // structured validation errors.
  4. return validateScheduleFormat(
  5. r.Spec.Schedule,
  6. field.NewPath("spec").Child("schedule"))
  7. }

We’ll need to validate the cron scheduleis well-formatted.

  1. func validateScheduleFormat(schedule string, fldPath *field.Path) *field.Error {
  2. if _, err := cron.ParseStandard(schedule); err != nil {
  3. return field.Invalid(fldPath, schedule, err.Error())
  4. }
  5. return nil
  6. }

Validate object nameValidating the length of a string field can be done declaratively bythe validation schema.

But the ObjectMeta.Name field is defined in a shared package underthe apimachinery repo, so we can’t declaratively validate it usingthe validation schema.

  1. func (r *CronJob) validateCronJobName() *field.Error {
  2. if len(r.ObjectMeta.Name) > validationutils.DNS1035LabelMaxLength-11 {
  3. // The job name length is 63 character like all Kubernetes objects
  4. // (which must fit in a DNS subdomain). The cronjob controller appends
  5. // a 11-character suffix to the cronjob (`-$TIMESTAMP`) when creating
  6. // a job. The job name length limit is 63 characters. Therefore cronjob
  7. // names must have length <= 63-11=52. If we don't validate this here,
  8. // then job creation will fail later.
  9. return field.Invalid(field.NewPath("metadata").Child("name"), r.Name, "must be no more than 52 characters")
  10. }
  11. return nil
  12. }