or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

builder.mdclient-cache.mdcore-framework.mdevent-handling.mdindex.mdsupporting-services.mdtesting.mdutilities.mdwebhooks.md
tile.json

webhooks.mddocs/

Webhooks

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.

Overview

Controller-runtime supports three types of webhooks:

  1. Admission Webhooks - Validate and/or mutate objects before they are persisted
    • Mutating webhooks - Modify objects (defaulting, injection)
    • Validating webhooks - Accept or reject objects
  2. Authentication Webhooks - Authenticate token review requests
  3. Conversion Webhooks - Convert between API versions

Webhook Package

Import Path: sigs.k8s.io/controller-runtime/pkg/webhook

Webhook Server

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) Server

Server Options

type 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

// 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.ServeMux

Response Helpers

var (
    // 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 Aliases

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.Operation

Admission Package

Import Path: sigs.k8s.io/controller-runtime/pkg/webhook/admission

Handler Interface

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

// HandlerFunc implements Handler using a function
type HandlerFunc func(context.Context, Request) Response

func (f HandlerFunc) Handle(ctx context.Context, req Request) Response

Multi-Handler Support

// MultiMutatingHandler combines multiple mutating handlers
func MultiMutatingHandler(handlers ...Handler) Handler

// MultiValidatingHandler combines multiple validating handlers
func MultiValidatingHandler(handlers ...Handler) Handler

Request

// 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

// 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

// Warnings is a list of warning messages
type Warnings []string

CustomDefaulter Interface

// 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 Interface

// 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

// 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

Creating Webhooks

// 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) *Webhook

Defaulter Options

type DefaulterOption func(*defaulterOptions)

// DefaulterRemoveUnknownOrOmitableFields configures the defaulter to remove unknown fields
func DefaulterRemoveUnknownOrOmitableFields(o *defaulterOptions)

Decoder

// 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

Log Constructor

// DefaultLogConstructor is the default log constructor for webhooks
func DefaultLogConstructor(base logr.Logger, req *Request) logr.Logger

Standalone Webhooks

// 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
}

Admission Webhook Example

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()
}

Custom Handler Example

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
}

Authentication Package

Import Path: sigs.k8s.io/controller-runtime/pkg/webhook/authentication

Handler Interface

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

// Request contains information from a TokenReview request
type Request struct {
    authenticationv1.TokenReview
}

Response

// 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) Response
func (r *Response) Complete(req Request) error

Webhook

// 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)

Authentication Webhook Example

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
}

Conversion Package

Import Path: sigs.k8s.io/controller-runtime/pkg/webhook/conversion

The conversion package provides utilities for implementing conversion webhooks.

Functions

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

// 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

Errors

// PartialImplementationError indicates partial conversion implementation
type PartialImplementationError struct {
    // Has unexported fields
}

func (e PartialImplementationError) Error() string

Conversion Example

package 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
}

Webhook Server Setup

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)
    }
}

Webhook Metrics

Admission Metrics

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"},
    )
)

Conversion Metrics

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"},
    )
)