README ¶
Webhooks
This package contains methods and types used to customize validating/mutating webhooks for resources
Motivation
There are a few factors that motivate customizing new webhooks:
- provide a
context.Context
for each function: Just likeknative/pkg
and others, using a context inside each function can provide multiple helper methods to check specific request type, fetch the old object, etc. logger
fetched from context: Instead of having a global variable hosting an imutable logger, this project already supports having multiple loggers with dynamic log level controlled by a configmap inside the cluster. This project uses uber's zap logger in its Suggared implementation, with specificDebug
,Info
,Warning
, etc. methods. The defaultcontroller-runtime
provided logger has very unclear log level (0-9) which is counter-intuitive leading to not-standard and unexpected log output- Adding validation/mutation methods outside of the
apis
scope: For example validating if another object exists, adding feature-flag controlled validations, etc.
Usage
A few changes are needed in the current objects in order to use this customized webhooks:
- Add
context.Context
toValidateCreate
,ValidateUpdate
,ValidateDelete
andDefault
methods as the first parameter.Default
and/orValidation
can be changed as necessary. If required, one or both can keep using regular webhooks. - Import
"github.com/katanomi/pkg/webhook/admission"
on your<type>_webhook.go
files - Change the interface check from
var _ webhook.Defaulter = &Type{}
tovar _ admission.Defaulter = &Type{}
the last one coming from this package. Do the same forValidator
- Use
"knative.dev/pkg/logging"
to fetch a logger from the context and"knative.dev/pkg/apis"
methods likeapis.IsWithinCreate
- Change one of the below methods to setup the webhook within the app
The sample-controller has full usable example implemented
As default setup for object
In this scenario, the apis/<version>
package will declare all setup methods necessary and nothing else needs to be changed
- Implement
sharedmain.WebhookRegisterSetup
interface fromsharedmain
package:
Here is a simple example from deliveries
:
var _ sharedmain.WebhookRegisterSetup = &Delivery{}
// GetLoggerName returns the logger name
func (r *Delivery) GetLoggerName() string {
return "delivery"
}
// SetupRegisterWithManager implements method to register a webhook
func (r *Delivery) SetupRegisterWithManager(ctx context.Context, mgr ctrl.Manager) {
// gets logger for this webhook with the specific level
log := logging.FromContext(ctx)
// defaulting and validating should be added here, if not added the standard one will be used instead
err := admission.RegisterDefaultWebhookFor(ctx, mgr, r, admission.WithCreatedBy())
if err != nil {
log.Fatalw("register webhook failed", "err", err)
}
// adds validate webhook for object
err := admission.RegisterValidateWebhookFor(ctx, mgr, r, []admission.ValidateCreateFunc{}, []admission.ValidateUpdateFunc{}, []admission.ValidateDeleteFunc{})
if err != nil {
log.Fatalw("register webhook failed", "err", err)
}
}
Thats it. Once the object is passed to the sharedmain.App("").Webhooks()
method, it will automatically will use the new methods, as seen below from deliveries
controller:
sharedmain.App("deliveries-controller").
Scheme(scheme).
Log().
Profiling().
Controllers(
&deliveriescontrollers.StageReconciler{},
&deliveriescontrollers.StageRunReconciler{},
&deliveriescontrollers.DeliveryReconciler{},
&deliveriescontrollers.DeliveryRunReconciler{},
&deliveriescontrollers.ClusterStageReconciler{},
).
Webhooks(
&deliveriesv1alpha1.Stage{},
&deliveriesv1alpha1.StageRun{},
&deliveriesv1alpha1.Delivery{},
&deliveriesv1alpha1.DeliveryRun{},
&deliveriesv1alpha1.ClusterStage{},
).
Run()
As custom webhook with external methods
Using this method will give the ability to extend validation methods with new extra methods while keeping the default method pristine.
- Implements all the custom
Validation
andDefault
methods, seevalidation.go
andtransform.go
- Init the webhook object inside the
sharedmain.App("").Webhooks()
method:
Lets say we want to add new defaulting and validating methods to the Delivery
object:
sharedmain.App("deliveries-controller").
Scheme(scheme).
Log().
Profiling().
Controllers(
&deliveriescontrollers.StageReconciler{},
&deliveriescontrollers.StageRunReconciler{},
&deliveriescontrollers.DeliveryReconciler{},
&deliveriescontrollers.DeliveryRunReconciler{},
&deliveriescontrollers.ClusterStageReconciler{},
).
Webhooks(
&deliveriesv1alpha1.Stage{},
&deliveriesv1alpha1.StageRun{},
// custom default webhook
admission.NewDefaulterWebhook(&deliveriesv1alpha1.Delivery{}).WithTransformer(
somepackage.SomeTrasnformerMethod,
somepackage.AnotherMethod,
),
// custom validation webhook
admission.NewValidatorWebhook(&deliveriesv1alpha1.Delivery{}).
WithValidateCreate(somepackage.SomeValidateCreateFunc).
WithValidateUpdate(somepackage.SomeValidateUpdateFunc).
WithValidateDelete(somepackage.SomeValidateDeleteFunc),
&deliveriesv1alpha1.Delivery{},
&deliveriesv1alpha1.DeliveryRun{},
&deliveriesv1alpha1.ClusterStage{},
).
Run()
Implementing validation and default methods
A brief introduction on the methods, see validation.go
and transform.go
for specifics
Default (transform) methods
// TransformFuncused to make common defaulting logic amongst multiple resource
// using a context, an object and a request
type TransformFunc func(context.Context, runtime.Object, admission.Request)
An implementation example for automatically adding the current update time/creation time would be:
func WithCreateUpdateTimes() TransformFunc {
return func(ctx context.Context, obj runtime.Object, req admission.Request) {
metaobj, ok := obj.(metav1.Object)
if !ok {
return
}
log := logging.FromContext(ctx)
annotations := metaobj.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
now := time.Now().Format(time.RFC3339)
if apis.IsInCreate(ctx) {
annotations["createdAt"] = now
} else if apis.IsInUpdate(ctx) {
annotations["updatedAt"] = now
}
metaobj.SetAnnotations(annotations)
}
}
Validation methods
// ValidateCreateFunc function to add validation functions when operation is create
// using a context, an object and a request
type ValidateCreateFunc func(ctx context.Context, obj runtime.Object, req admission.Request) error
// ValidateUpdateFunc function to add validation functions when operation is update
// using a context, the current object, the old object and a request
type ValidateUpdateFunc func(ctx context.Context, obj runtime.Object, old runtime.Object, req admission.Request) error
// ValidateDeleteFunc function to add validation functions when operation is delete
// using a context, an object and a request
type ValidateDeleteFunc func(ctx context.Context, obj runtime.Object, req admission.Request) error
An implementation example for validating if the createdAt
annotation is preset:
func HasCreatedAtAnnotation() ValidateCreateFunc {
return func(ctx context.Context, obj runtime.Object, req admission.Request) error {
metaobj, ok := obj.(metav1.Object)
if !ok {
return
}
log := logging.FromContext(ctx)
annotations := metaobj.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
if annotations["createdAt"] == "" {
return fmt.Errorf("some validation error")
}
return nil
}
}