A set of go libraries for building Kubernetes controllers with structured abstractions for managers, reconcilers, clients, caches, webhooks, and testing
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"},
)
)Install with Tessl CLI
npx tessl i tessl/golang-sigs-k8s-io--controller-runtime