The webhook framework provides comprehensive support for implementing Kubernetes webhooks, including admission webhooks (validating and mutating), authentication webhooks, and conversion webhooks with automatic server management.
Controller-runtime supports three types of webhooks:
Import Path: sigs.k8s.io/controller-runtime/pkg/webhook
package webhook
import (
"context"
"crypto/tls"
"net/http"
"sigs.k8s.io/controller-runtime/pkg/healthz"
)
// Server manages webhook registration and serving
type Server interface {
// NeedLeaderElection returns true if the webhook server needs leader election
NeedLeaderElection() bool
// Register registers a webhook handler at the given path
Register(path string, hook http.Handler)
// Start starts the webhook server
Start(ctx context.Context) error
// StartedChecker returns a health check for the webhook server
StartedChecker() healthz.Checker
// WebhookMux returns the webhook multiplexer
WebhookMux() *http.ServeMux
}
// NewServer creates a new webhook server
func NewServer(o Options) Servertype Options struct {
// Host is the address that the server will listen on
Host string
// Port is the port that the server will listen on
Port int
// CertDir is the directory that contains the server key and certificate
CertDir string
// CertName is the server certificate name
CertName string
// KeyName is the server key name
KeyName string
// ClientCAName is the CA certificate name for client authentication
ClientCAName string
// TLSOpts is a list of TLS configuration options
TLSOpts []func(*tls.Config)
// WebhookMux is the multiplexer for webhooks
WebhookMux *http.ServeMux
}
var DefaultPort = 9443// DefaultServer is the default webhook server implementation
type DefaultServer struct {
Options Options
// Has unexported fields
}
func (*DefaultServer) NeedLeaderElection() bool
func (s *DefaultServer) Register(path string, hook http.Handler)
func (s *DefaultServer) Start(ctx context.Context) error
func (s *DefaultServer) StartedChecker() healthz.Checker
func (s *DefaultServer) WebhookMux() *http.ServeMuxvar (
// Allowed creates an allowed admission response
Allowed func(message string) admission.Response
// Denied creates a denied admission response
Denied func(message string) admission.Response
// Patched creates a patched admission response
Patched func(message string, patches ...jsonpatch.Operation) admission.Response
// Errored creates an errored admission response
Errored func(code int32, err error) admission.Response
)type Admission = admission.Webhook
type AdmissionDecoder = admission.Decoder
type AdmissionHandler = admission.Handler
type AdmissionRequest = admission.Request
type AdmissionResponse = admission.Response
type CustomDefaulter = admission.CustomDefaulter
type CustomValidator = admission.CustomValidator
type JSONPatchOp = jsonpatch.OperationImport Path: sigs.k8s.io/controller-runtime/pkg/webhook/admission
package admission
import (
"context"
"net/http"
"github.com/go-logr/logr"
admissionv1 "k8s.io/api/admission/v1"
authenticationv1 "k8s.io/api/authentication/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)
// Handler processes admission requests
type Handler interface {
// Handle processes an admission request
Handle(context.Context, Request) Response
}// HandlerFunc implements Handler using a function
type HandlerFunc func(context.Context, Request) Response
func (f HandlerFunc) Handle(ctx context.Context, req Request) Response// MultiMutatingHandler combines multiple mutating handlers
func MultiMutatingHandler(handlers ...Handler) Handler
// MultiValidatingHandler combines multiple validating handlers
func MultiValidatingHandler(handlers ...Handler) Handler// Request contains information from an admission request
type Request struct {
admissionv1.AdmissionRequest
}
// RequestFromContext extracts the admission request from a context
func RequestFromContext(ctx context.Context) (Request, error)
// NewContextWithRequest creates a context with an admission request
func NewContextWithRequest(ctx context.Context, req Request) context.Context// Response is the output of an admission handler
type Response struct {
// Patches are the JSON patches to apply to the object
Patches []jsonpatch.JsonPatchOperation
admissionv1.AdmissionResponse
}
// Allowed creates an allowed response
func Allowed(message string) Response
// Denied creates a denied response
func Denied(message string) Response
// Errored creates an error response
func Errored(code int32, err error) Response
// Patched creates a patched response
func Patched(message string, patches ...jsonpatch.JsonPatchOperation) Response
// PatchResponseFromRaw creates a patch response from raw bytes
func PatchResponseFromRaw(original, current []byte) Response
// ValidationResponse creates a validation response
func ValidationResponse(allowed bool, message string) Response// Response methods
func (r *Response) Complete(req Request) error
func (r Response) WithWarnings(warnings ...string) Response// Warnings is a list of warning messages
type Warnings []string// CustomDefaulter defines functions for setting defaults on objects
type CustomDefaulter interface {
// Default sets defaults on the given object
Default(ctx context.Context, obj runtime.Object) error
}// CustomValidator defines functions for validating operations on objects
type CustomValidator interface {
// ValidateCreate validates a create operation
ValidateCreate(ctx context.Context, obj runtime.Object) (warnings Warnings, err error)
// ValidateUpdate validates an update operation
ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (warnings Warnings, err error)
// ValidateDelete validates a delete operation
ValidateDelete(ctx context.Context, obj runtime.Object) (warnings Warnings, err error)
}// Webhook represents an admission webhook
type Webhook struct {
// Handler is the admission handler
Handler Handler
// RecoverPanic indicates whether to recover from panics
RecoverPanic *bool
// WithContextFunc can add additional information to the context
WithContextFunc func(context.Context, *http.Request) context.Context
// LogConstructor constructs a logger for a request
LogConstructor func(base logr.Logger, req *Request) logr.Logger
// Has unexported fields
}
func (wh *Webhook) Handle(ctx context.Context, req Request) (response Response)
func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request)
func (wh *Webhook) WithRecoverPanic(recoverPanic bool) *Webhook// WithCustomDefaulter creates a webhook with a custom defaulter
func WithCustomDefaulter(scheme *runtime.Scheme, obj runtime.Object, defaulter CustomDefaulter, opts ...DefaulterOption) *Webhook
// WithCustomValidator creates a webhook with a custom validator
func WithCustomValidator(scheme *runtime.Scheme, obj runtime.Object, validator CustomValidator) *Webhooktype DefaulterOption func(*defaulterOptions)
// DefaulterRemoveUnknownOrOmitableFields configures the defaulter to remove unknown fields
func DefaulterRemoveUnknownOrOmitableFields(o *defaulterOptions)// Decoder decodes admission requests
type Decoder interface {
// Decode decodes the request into the given object
Decode(req Request, into runtime.Object) error
// DecodeRaw decodes raw extension data into the given object
DecodeRaw(rawObj runtime.RawExtension, into runtime.Object) error
}
// NewDecoder creates a new decoder
func NewDecoder(scheme *runtime.Scheme) Decoder// DefaultLogConstructor is the default log constructor for webhooks
func DefaultLogConstructor(base logr.Logger, req *Request) logr.Logger// StandaloneWebhook creates a standalone webhook HTTP handler
func StandaloneWebhook(hook *Webhook, opts StandaloneOptions) (http.Handler, error)
type StandaloneOptions struct {
// Logger is the logger to use
Logger logr.Logger
// MetricsPath is the path for metrics
MetricsPath string
}package webhooks
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)
type PodWebhook struct {
decoder *admission.Decoder
}
// Default implements admission.CustomDefaulter
func (w *PodWebhook) Default(ctx context.Context, obj runtime.Object) error {
pod, ok := obj.(*corev1.Pod)
if !ok {
return fmt.Errorf("expected a Pod but got a %T", obj)
}
// Set default values
if pod.Spec.RestartPolicy == "" {
pod.Spec.RestartPolicy = corev1.RestartPolicyAlways
}
if pod.Labels == nil {
pod.Labels = make(map[string]string)
}
pod.Labels["defaulted"] = "true"
return nil
}
// ValidateCreate implements admission.CustomValidator
func (w *PodWebhook) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
pod, ok := obj.(*corev1.Pod)
if !ok {
return nil, fmt.Errorf("expected a Pod but got a %T", obj)
}
if len(pod.Spec.Containers) == 0 {
return nil, fmt.Errorf("pod must have at least one container")
}
warnings := admission.Warnings{}
if pod.Spec.RestartPolicy == corev1.RestartPolicyNever {
warnings = append(warnings, "RestartPolicy Never is discouraged")
}
return warnings, nil
}
// ValidateUpdate implements admission.CustomValidator
func (w *PodWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
oldPod, ok := oldObj.(*corev1.Pod)
if !ok {
return nil, fmt.Errorf("expected a Pod but got a %T", oldObj)
}
newPod, ok := newObj.(*corev1.Pod)
if !ok {
return nil, fmt.Errorf("expected a Pod but got a %T", newObj)
}
// Prevent changing pod name
if oldPod.Name != newPod.Name {
return nil, fmt.Errorf("pod name is immutable")
}
return nil, nil
}
// ValidateDelete implements admission.CustomValidator
func (w *PodWebhook) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
pod, ok := obj.(*corev1.Pod)
if !ok {
return nil, fmt.Errorf("expected a Pod but got a %T", obj)
}
// Prevent deletion of pods with specific label
if pod.Labels["protected"] == "true" {
return nil, fmt.Errorf("cannot delete protected pod")
}
return nil, nil
}
func (w *PodWebhook) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(&corev1.Pod{}).
WithDefaulter(w).
WithValidator(w).
Complete()
}package webhooks
import (
"context"
"encoding/json"
"fmt"
corev1 "k8s.io/api/core/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)
type CustomPodHandler struct {
decoder *admission.Decoder
}
func (h *CustomPodHandler) Handle(ctx context.Context, req admission.Request) admission.Response {
pod := &corev1.Pod{}
err := h.decoder.Decode(req, pod)
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
// Custom validation logic
if len(pod.Spec.Containers) == 0 {
return admission.Denied("pod must have at least one container")
}
// Custom mutation logic
if pod.Labels == nil {
pod.Labels = make(map[string]string)
}
pod.Labels["mutated-by"] = "custom-handler"
marshaledPod, err := json.Marshal(pod)
if err != nil {
return admission.Errored(http.StatusInternalServerError, err)
}
return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod)
}
func (h *CustomPodHandler) SetupWebhookWithManager(mgr ctrl.Manager) error {
h.decoder = admission.NewDecoder(mgr.GetScheme())
mgr.GetWebhookServer().Register("/mutate-v1-pod",
&admission.Webhook{Handler: h})
return nil
}Import Path: sigs.k8s.io/controller-runtime/pkg/webhook/authentication
package authentication
import (
"context"
"net/http"
authenticationv1 "k8s.io/api/authentication/v1"
)
// Handler processes authentication requests
type Handler interface {
Handle(context.Context, Request) Response
}
// HandlerFunc implements Handler using a function
type HandlerFunc func(context.Context, Request) Response
func (f HandlerFunc) Handle(ctx context.Context, req Request) Response// Request contains information from a TokenReview request
type Request struct {
authenticationv1.TokenReview
}// Response is the output of an authentication handler
type Response struct {
authenticationv1.TokenReview
}
// Authenticated creates an authenticated response
func Authenticated(reason string, user authenticationv1.UserInfo) Response
// Unauthenticated creates an unauthenticated response
func Unauthenticated(reason string, user authenticationv1.UserInfo) Response
// Errored creates an error response
func Errored(err error) Response
// ReviewResponse creates a token review response
func ReviewResponse(authenticated bool, user authenticationv1.UserInfo, err string, audiences ...string) Responsefunc (r *Response) Complete(req Request) error// Webhook represents an authentication webhook
type Webhook struct {
// Handler is the authentication handler
Handler Handler
// WithContextFunc can add additional information to the context
WithContextFunc func(context.Context, *http.Request) context.Context
// Has unexported fields
}
func (wh *Webhook) Handle(ctx context.Context, req Request) Response
func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request)package webhooks
import (
"context"
"strings"
authenticationv1 "k8s.io/api/authentication/v1"
"sigs.k8s.io/controller-runtime/pkg/webhook/authentication"
)
type TokenAuthenticator struct{}
func (a *TokenAuthenticator) Handle(ctx context.Context, req authentication.Request) authentication.Response {
token := req.Spec.Token
// Custom token validation logic
if !strings.HasPrefix(token, "valid-") {
return authentication.Unauthenticated("invalid token", authenticationv1.UserInfo{})
}
// Extract user info from token
username := strings.TrimPrefix(token, "valid-")
userInfo := authenticationv1.UserInfo{
Username: username,
Groups: []string{"authenticated"},
}
return authentication.Authenticated("token validated", userInfo)
}
func (a *TokenAuthenticator) SetupWebhookWithManager(mgr ctrl.Manager) error {
webhook := &authentication.Webhook{
Handler: a,
}
mgr.GetWebhookServer().Register("/authenticate", webhook)
return nil
}Import Path: sigs.k8s.io/controller-runtime/pkg/webhook/conversion
The conversion package provides utilities for implementing conversion webhooks.
package conversion
import (
"net/http"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// NewWebhookHandler creates a webhook handler for CRD conversions
func NewWebhookHandler(scheme *runtime.Scheme) http.Handler
// IsConvertible checks if an object implements conversion interfaces
func IsConvertible(scheme *runtime.Scheme, obj runtime.Object) (bool, error)// Decoder decodes conversion webhook requests
type Decoder struct {
// Has unexported fields
}
// NewDecoder creates a new conversion decoder
func NewDecoder(scheme *runtime.Scheme) *Decoder
func (d *Decoder) Decode(content []byte) (runtime.Object, *schema.GroupVersionKind, error)
func (d *Decoder) DecodeInto(content []byte, into runtime.Object) error// PartialImplementationError indicates partial conversion implementation
type PartialImplementationError struct {
// Has unexported fields
}
func (e PartialImplementationError) Error() stringpackage v1
import (
"sigs.k8s.io/controller-runtime/pkg/conversion"
)
// MyResourceV1 is version v1 of MyResource
type MyResourceV1 struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec MyResourceV1Spec `json:"spec,omitempty"`
}
type MyResourceV1Spec struct {
Field1 string `json:"field1"`
}
// MyResourceV2 is the hub version
type MyResourceV2 struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec MyResourceV2Spec `json:"spec,omitempty"`
}
type MyResourceV2Spec struct {
Field1 string `json:"field1"`
Field2 string `json:"field2"` // New field in v2
}
// Hub marks MyResourceV2 as the hub version
func (*MyResourceV2) Hub() {}
// ConvertTo converts v1 to the hub version (v2)
func (src *MyResourceV1) ConvertTo(dstRaw conversion.Hub) error {
dst := dstRaw.(*MyResourceV2)
// Convert ObjectMeta
dst.ObjectMeta = src.ObjectMeta
// Convert Spec
dst.Spec.Field1 = src.Spec.Field1
// Field2 doesn't exist in v1, so leave it empty
return nil
}
// ConvertFrom converts from the hub version (v2) to v1
func (dst *MyResourceV1) ConvertFrom(srcRaw conversion.Hub) error {
src := srcRaw.(*MyResourceV2)
// Convert ObjectMeta
dst.ObjectMeta = src.ObjectMeta
// Convert Spec
dst.Spec.Field1 = src.Spec.Field1
// Field2 is lost when converting to v1
return nil
}
// Setup conversion webhook
func SetupConversionWebhook(mgr ctrl.Manager) error {
// The conversion webhook is automatically registered when the CRD
// is installed with conversion strategy: Webhook
return nil
}package main
import (
"os"
corev1 "k8s.io/api/core/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/webhook"
)
func main() {
ctrl.SetLogger(zap.New(zap.UseDevMode(true)))
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
WebhookServer: webhook.NewServer(webhook.Options{
Port: 9443,
CertDir: "/tmp/k8s-webhook-server/serving-certs",
}),
})
if err != nil {
os.Exit(1)
}
// Setup webhooks
if err := (&PodWebhook{}).SetupWebhookWithManager(mgr); err != nil {
os.Exit(1)
}
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
os.Exit(1)
}
}Import Path: sigs.k8s.io/controller-runtime/pkg/webhook/admission/metrics
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
)
var (
// WebhookPanics tracks the total number of panics from webhooks
WebhookPanics = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "controller_runtime_webhook_panics_total",
Help: "Total number of admission webhook panics",
},
[]string{"webhook"},
)
)Import Path: sigs.k8s.io/controller-runtime/pkg/webhook/conversion/metrics
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
)
var (
// WebhookPanics tracks the total number of panics from conversion webhooks
WebhookPanics = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "controller_runtime_conversion_webhook_panics_total",
Help: "Total number of conversion webhook panics",
},
[]string{"webhook"},
)
)