webhook

package
v0.2.0-alpha.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 5, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

README

pkg/webhook

A focused HTTPS server for Kubernetes validating admission webhooks. Given TLS credentials and a set of validator functions keyed by resource GVK, it speaks the AdmissionReview v1 protocol, dispatches to the right validator, and shuts down cleanly on context cancellation.

The package intentionally does not handle certificate provisioning — issuance, the Secret, the CA bundle, or ValidatingWebhookConfiguration management — those are the controller's and Helm chart's concern (the chart provisions the Secret plus validatingwebhookconfiguration.yaml). It does serve and hot-reload the mounted cert files it's pointed at: when ServerConfig.CertDir is set, the server re-reads tls.crt/tls.key from that directory on content change, so a cert-manager renewal is picked up without a restart. Keeping this library narrow means it can be reused by any controller that already has a cert pipeline.

Module path: gitlab.com/haproxy-haptic/haptic. Source is authoritative (go doc ./pkg/webhook); this README is a short map.

Minimal Example

import (
    "context"
    "log"

    "gitlab.com/haproxy-haptic/haptic/pkg/webhook"
)

func main() {
    // In production, point the server at the directory where the TLS Secret
    // is mounted (cert-manager writes tls.crt/tls.key there). The server
    // re-reads them per handshake and hot-reloads on rotation — no restart.
    srv := webhook.NewServer(&webhook.ServerConfig{
        Port:    9443,
        CertDir: "/etc/webhook/certs",
        // For unit tests without a mounted Secret, set CertPEM/KeyPEM instead
        // to serve a fixed cert.
        // BindAddress defaults to 0.0.0.0, Path to /validate,
        // ReadTimeout and WriteTimeout to 10s.
    })

    srv.RegisterValidator("networking.k8s.io/v1.Ingress", validateIngress)
    srv.RegisterValidator("gateway.networking.k8s.io/v1.HTTPRoute", validateHTTPRoute)

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    if err := srv.Start(ctx); err != nil {
        log.Fatal(err)
    }
}

func validateIngress(ctx *webhook.ValidationContext) (bool, string, []string, error) {
    // ctx.Object / ctx.OldObject are *unstructured.Unstructured.
    // ctx.Operation is "CREATE" / "UPDATE" / "DELETE" / "CONNECT".
    // ctx.UserInfo carries the requester identity.
    return true, "", nil, nil
}

RegisterValidator is thread-safe — you can add or replace validators while the server is running, though in practice the controller registers everything before calling Start.

Types at a Glance

  • ServerConfig — port (default 9443), bind address, path (default /validate), read/write timeouts, and the TLS source. In production set CertDir to the directory holding tls.crt/tls.key (typically the mounted cert Secret); the server resolves the cert per handshake via a tls.Config.GetCertificate callback and a certReloader that re-parses the files on content change — a cert-manager renewal is served without a restart. When CertDir is unset (e.g. unit tests), it serves a fixed cert from the PEM-encoded CertPEM/KeyPEM instead.
  • Server — wraps an http.Server, maintains a GVK → ValidationFunc map under an RWMutex.
  • ValidationContext — everything the server extracts from an AdmissionRequest: Object, OldObject, Operation, Namespace, Name, UID, UserInfo. Object and OldObject are *unstructured.Unstructured so validators don't need their own typed decoders.
  • ValidationFuncfunc(*ValidationContext) (allowed bool, reason string, warnings []string, err error). The allowed/reason pair maps to AdmissionResponse.Allowed and .Status.Message; warnings surfaces as AdmissionResponse.Warnings (visible to kubectl users even when allowed is true). A non-nil error is treated as a denied decision (Allowed: false, Result.Code: 500, Result.Message: "validation error: <err>") — not a transport-layer HTTP 500. The HTTP response itself is always 200 OK with a well-formed AdmissionReview. The API server's failurePolicy only kicks in when the webhook fails to respond (TLS handshake failure, malformed body, etc.); a ValidationFunc returning an error never triggers it.

GVK Keys

Keys are version.Kind for core types and group/version.Kind otherwise:

"v1.Pod"
"v1.ConfigMap"
"apps/v1.Deployment"
"networking.k8s.io/v1.Ingress"
"gateway.networking.k8s.io/v1.HTTPRoute"

Unknown GVKs are rejected with a denial message — the server never calls an unregistered validator.

Endpoints Exposed

  • POST <Path> — admission endpoint.
  • GET /healthz — 200 OK when the server is running. Useful as a liveness probe without interacting with the cert pipeline.

Graceful Shutdown

Start(ctx) blocks until either the server returns an error or ctx is cancelled. On cancellation it calls http.Server.Shutdown with a 30-second deadline. Any in-flight admission calls run to completion (subject to the shutdown deadline) — the API server sees their responses.

What's NOT In This Package

Deliberate omissions — and where to look instead in this repo:

Concern Lives in
Issuing/renewing the TLS cert cert-manager (chart provisions a Certificate and mounts the Secret). The server serves and hot-reloads the mounted files via ServerConfig.CertDir, but does not fetch or issue them.
Creating / updating ValidatingWebhookConfiguration charts/haptic/templates/validatingwebhookconfiguration.yaml
Multi-controller isolation Each Helm release deploys its own ValidatingWebhookConfiguration whose clientConfig.service points at that release's controller Service. The chart does not set objectSelector — add one if you need label-based scoping on top of that.
Fail-policy, scope, operations on each rule Chart values + validatingwebhookconfiguration.yaml
Registering validators that render templates with overlay stores pkg/controller/dryrunvalidator, pkg/controller/proposalvalidator
Metrics / events for webhook activity pkg/controller/webhook, pkg/controller/metrics

Testing

go test ./pkg/webhook/...           # unit tests
go test ./pkg/webhook/... -race     # race detector

Server-level tests bring up a real TLS listener on a random port with a self-signed cert and exercise the AdmissionReview request/response path. Validator-level tests should call the ValidationFunc directly with a crafted ValidationContext — the server's dispatch logic is exercised once in the server tests, not in every validator's test suite.

See Also

  • Kubernetes admission webhooks reference
  • AdmissionReview v1 API
  • pkg/controller/webhook — event adapter that owns the Server lifecycle inside the controller
  • The controller passes the mounted Secret directory to the server via ServerConfig.CertDir, which the server reads and hot-reloads on rotation; the Helm chart provisions and mounts that Secret
  • docs/controller/docs/development/crd-validation-design.md — why the webhook lives in the controller pod and fails closed

License

Apache-2.0 — see root LICENSE.

Documentation

Overview

Package webhook provides a pure library for Kubernetes admission webhooks.

This package implements an HTTPS webhook server with flexible validation handlers, without dependencies on other project packages. It can be used in any Kubernetes controller project.

The package provides:

  • Generic webhook server with configurable validation
  • AdmissionReview v1 request/response handling
  • ValidationContext with full admission request details
  • Thread-safe concurrent request handling

External dependencies required (not provided by this library):

  • TLS certificates (from cert-manager, Kubernetes Secret, or Helm)
  • ValidatingWebhookConfiguration (via Helm chart or kubectl apply)

Example usage:

// Load certificates from external source (Kubernetes Secret)
secret, err := client.CoreV1().Secrets("default").Get(ctx, "webhook-certs", metav1.GetOptions{})
certPEM := secret.Data["tls.crt"]
keyPEM := secret.Data["tls.key"]

// Create webhook server
server := webhook.NewServer(&webhook.ServerConfig{
    Port:     9443,
    CertPEM:  certPEM,
    KeyPEM:   keyPEM,
})

// Register validator with full context
server.RegisterValidator("networking.k8s.io/v1.Ingress", func(ctx *webhook.ValidationContext) (bool, string, error) {
    // Validation logic with access to operation type and old/new objects
    if ctx.Operation == "UPDATE" && ctx.OldObject != nil {
        // Implement immutability checks
    }
    return true, "", nil
})

// Start server
server.Start(ctx)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is an HTTPS webhook server that validates Kubernetes resources.

The server handles AdmissionReview requests from the Kubernetes API server and calls registered validation functions to determine whether resources should be admitted.

The server is thread-safe and can handle multiple concurrent requests.

The certificate is resolved per TLS handshake through getCertificate. When ServerConfig.CertDir is set, that callback reloads tls.crt/tls.key from disk on change, so a rotated certificate (cert-manager renewal written to a mounted Secret) is served without restarting. Otherwise getCertificate returns a fixed certificate parsed once from CertPEM/KeyPEM. Either way the certificate is validated eagerly in NewServer so a malformed cert surfaces there rather than at the first handshake.

func NewServer

func NewServer(config *ServerConfig) (*Server, error)

NewServer creates a new webhook server with the given configuration.

The server will not start until Start() is called. The certificate is loaded eagerly — from CertDir (reloading, on change) or from CertPEM/KeyPEM (fixed) — so configuration errors surface here rather than at the first TLS handshake.

func (*Server) Listening

func (s *Server) Listening() <-chan struct{}

Listening returns a channel that is closed once the TLS listener has been bound to the configured port. Until this channel is closed, admission requests sent to the server's address fail with "connection refused". The controller uses this signal so its Pod readiness probe only flips healthy after the webhook is actually reachable.

func (*Server) RegisterValidator

func (s *Server) RegisterValidator(gvk string, fn ValidationFunc)

RegisterValidator registers a validation function for a specific resource type.

The gvk parameter should be in the format "version.Kind" (e.g., "v1.Ingress"). For resources with a group, use "group/version.Kind" (e.g., "networking.k8s.io/v1.Ingress").

If a validator is already registered for this gvk, it will be replaced.

This method is thread-safe.

func (*Server) Start

func (s *Server) Start(ctx context.Context) error

Start starts the HTTPS webhook server.

The server binds to the configured port synchronously, closes the channel returned by Listening() to signal readiness, and then serves in a background goroutine. The method blocks until the server is shut down (context cancellation) or the serve loop returns an error.

Splitting bind from serve matters because the Pod readiness probe must not flip healthy until admission is reachable — otherwise the API server starts routing AdmissionReview requests at the controller before net.Listen has returned, and every request bounces with "connection refused" until the listener finally binds. Callers that need to gate on the bind read Listening().

type ServerConfig

type ServerConfig struct {
	// Port is the HTTPS port to listen on.
	// Default: 9443
	Port int

	// BindAddress is the address to bind to.
	// Default: "0.0.0.0"
	BindAddress string

	// CertDir, when set, is a directory containing tls.crt and tls.key. The
	// server reads them through a reloading GetCertificate callback, so a
	// rotated certificate (e.g. a cert-manager renewal written to a mounted
	// Secret) is picked up on the next handshake without a restart. Takes
	// precedence over CertPEM/KeyPEM.
	CertDir string

	// CertPEM is the PEM-encoded server certificate.
	// Required unless CertDir is set.
	CertPEM []byte

	// KeyPEM is the PEM-encoded private key.
	// Required unless CertDir is set.
	KeyPEM []byte

	// Path is the URL path for the webhook endpoint.
	// Default: "/validate"
	Path string

	// ReadTimeout is the maximum duration for reading the entire request.
	// Default: 10s
	ReadTimeout time.Duration

	// WriteTimeout is the maximum duration before timing out writes of the response.
	// Default: 10s
	WriteTimeout time.Duration
}

ServerConfig configures the webhook HTTPS server.

type ValidationContext

type ValidationContext struct {
	// Object is the resource object being validated (new version).
	// For CREATE: the object being created
	// For UPDATE: the new version of the object
	// For DELETE: the object being deleted
	// Stored as unstructured.Unstructured (same type as resource stores use).
	Object *unstructured.Unstructured

	// OldObject is the existing version of the resource (for UPDATE/DELETE operations).
	// For CREATE: nil
	// For UPDATE: the current version in the cluster
	// For DELETE: the object being deleted (same as Object)
	// Stored as unstructured.Unstructured (same type as resource stores use).
	OldObject *unstructured.Unstructured

	// Operation indicates the admission operation type.
	// Values: "CREATE", "UPDATE", "DELETE", "CONNECT"
	Operation string

	// Namespace is the namespace of the resource (empty for cluster-scoped resources).
	Namespace string

	// Name is the name of the resource.
	// May be empty for CREATE operations using generateName.
	Name string

	// UID is a unique identifier for this admission request.
	// Can be used for correlation and logging.
	UID string

	// UserInfo contains information about the user making the request.
	// Includes username, UID, groups, and extra fields.
	// Can be used for authorization decisions.
	UserInfo authenticationv1.UserInfo
}

ValidationContext provides the complete context for validating a Kubernetes resource.

This includes the resource object, operation type, and related metadata from the AdmissionRequest. This allows validators to make informed decisions based on the full context of the admission request.

type ValidationFunc

type ValidationFunc func(ctx *ValidationContext) (allowed bool, reason string, warnings []string, err error)

ValidationFunc is called to validate a Kubernetes resource admission request.

Parameters:

  • ctx: The validation context with full admission request information

Returns:

  • allowed: Whether the resource should be admitted
  • reason: Human-readable reason for denial (empty if allowed)
  • warnings: Soft warnings surfaced via AdmissionResponse.Warnings. Returned for both allowed and denied responses so kubectl prints them as "Warning:" lines without blocking admission. Each entry should fit in 256 chars; over that, the API server truncates.
  • err: Error during validation (500 response if non-nil)

The function receives complete context including both old and new objects, operation type, and metadata. This allows validators to implement sophisticated validation logic based on the admission operation.

Example:

func validateIngress(ctx *webhook.ValidationContext) (bool, string, []string, error) {
    // Access new object (already unstructured.Unstructured)
    if ctx.Object == nil {
        return false, "", nil, errors.New("object is nil")
    }

    // For UPDATE operations, compare with old object
    if ctx.Operation == "UPDATE" && ctx.OldObject != nil {
        // Both ctx.Object and ctx.OldObject are *unstructured.Unstructured
        // Validate the change...
    }

    spec, found, err := unstructured.NestedMap(ctx.Object.Object, "spec")
    if err != nil || !found {
        return false, "spec is required", nil, nil
    }

    return true, "", nil, nil
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL