webhook

package
v0.7.9 Latest Latest
Warning

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

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

README

pkg/webhook

The webhook package owns Orkestra's entire HTTPS admission and conversion surface. It implements domain.Komponent so it is managed by the same lifecycle supervisor that manages all other runtime components.

A single WebhookServer binds one HTTPS listener and handles all webhook traffic:

  • /validate — admission validation (deny or warn)
  • /mutate — admission mutation (apply defaults)
  • /convert — CRD version conversion
  • /deletion-protection — block deletion of managed CRDs and Orkestra resources
  • /namespace-protection — enforce allowed/restricted namespace rules
  • /strict-mode-protection — block removal of the deletion-protection label (strict mode only)

The HTTPS server starts only when the Katalog declares at least one webhook capability. When no capabilities are declared, Start() is a no-op.

Responsibilities

Concern Where
HTTPS server lifecycle webhook.go
Webhook configuration registration registration.go
Continuous reconciliation controller housekeeper.go
Infrastructure security reconciliation infrastructure.go
AdmissionReview types admission_review.go
Validation and mutation rule evaluation admission_evaluation.go
/validate and /mutate handlers admission.go
Conversion logic conversion_logic.go
/convert handler conversion.go
/deletion-protection handler deletion_protection.go
/namespace-protection handler namespace_protection.go
/strict-mode-protection handler strict_mode_protection.go

Separation from pkg/health

The HealthServer in pkg/health serves only HTTP probes and Prometheus metrics. The WebhookServer in this package serves only HTTPS webhook endpoints.

This separation allows the health server to start first — making /ready available immediately — while the webhook server starts after it, once the cluster-facing admission surface is needed.

Lifecycle

NewWebhookServer(kubeClient, katalog, konfig)
    ↓
ws.SetCertManager(certMgr)           ← optional, only when certs were auto-generated
    ↓
ws.SetCertBundle(cert, key, ca, ...)  ← optional, only when certs were auto-generated
    ↓
WireWebhookHousekeeperInfra(ws, kube, kat, kfg)
    ↓                                 ← registers ConversionCRDPatcher + CRDWatcher
ws.Start(ctx)
    • resolves deletion/namespace/strict-mode protection state from Katalog
    • registers HTTPS endpoints based on declared capabilities
    • starts HTTPS server (skipped if no capabilities declared)
    • registers ValidatingWebhookConfiguration / MutatingWebhookConfiguration (in-cluster only)
    • starts housekeeper goroutine
    ↓
ws.Shutdown(ctx)
    • cancels controller goroutine
    • drains HTTPS server
    • removes webhook configurations (when cleanupOnShutdown: true)
    • removes TLS secret (when auto-generated and cleanupOnShutdown: true)

TLS

Certificates are provisioned by cmd/internal.ensureSecurity before Start() is called. The paths are written to konfig.Security().Webhooks.TLSCert and .TLSKey. The webhook server reads them from konfig at construction time and uses them for the HTTPS server.

Three modes (handled by ensureSecurity, not this package):

  1. External — user provides TLS_CERT / TLS_KEY env vars pointing to existing files.
  2. Self-signed — Orkestra generates a self-signed bundle via pkg/certmanager and stores it in the orkestra-tls Secret.

The webhook server itself never generates certificates.

→ Next: docs/01-architecture.md

Documentation

Overview

webhook/admission.go — /validate and /mutate HTTP handlers.

The Kubernetes API server POSTs an AdmissionReview to /validate for every CREATE and UPDATE on a resource covered by the ValidatingWebhookConfiguration, and to /mutate for every CREATE and UPDATE covered by the MutatingWebhookConfiguration.

Mutation fires before validation in the Kubernetes admission chain, so defaults are applied before validation sees the object.

webhook/admission_evaluation.go — validation and mutation rule evaluation.

webhook/admission_review.go — Kubernetes AdmissionReview types.

These mirror the Kubernetes admissionregistration.k8s.io/v1 types. Declared here to keep the webhook package self-contained without importing the full k8s.io/api module where it isn't needed.

The Kubernetes API server sends an AdmissionReview to /validate and /mutate. Orkestra reads the Request, evaluates rules from the Katalog, and writes the Response. The API server reads the Response and either stores the object (allowed) or rejects it (denied).

webhook/conversion.go — /convert HTTP handler for CRD version conversion.

The Kubernetes API server POSTs a ConversionReview to this endpoint when it needs to convert a CR from one version to another. Orkestra applies the conversion rules declared in the Katalog for the object's Kind.

webhook/conversion_logic.go — CRD version conversion logic.

webhook/deletion_protection.go — /deletion-protection webhook handler.

Registered only when security.deletionProtection.enabled: true in the Katalog. Intercepts DELETE operations on CRDs owned by this operator and Orkestra's own resources (deployment, service, ingress, webhook configurations).

failurePolicy: Fail — if Orkestra is unreachable, DELETE is blocked. To decommission: set deletionProtection.enabled: false, redeploy, then delete.

webhook/housekeeper.go — self-healing maintenance of Orkestra's admission surface.

The housekeeper runs in the background and ensures that the webhook configurations Orkestra owns exist exactly when the Katalog declares them — and disappear when they do not.

Declarative Webhook Lifecycle

security.admission.enabled: true
    → create/update the validating and mutating webhook configurations

security.deletionProtection.enabled: true
    → create/update the deletion-protection webhook

(inverse: when disabled, the housekeeper removes the corresponding webhook)

Event-Driven Reconciliation

The housekeeper uses a Kubernetes Watch on ValidatingWebhookConfiguration and MutatingWebhookConfiguration objects as the fast path. A DELETED event triggers an immediate reconcile — the window during which a webhook is absent is bounded only by the API server round-trip, not by a poll interval. MODIFIED events are intentionally ignored: every reconcile Update fires one, and reacting to it would create a tight loop. Content drift is caught by the safety ticker instead.

A safety ticker (HOUSEKEEPER_SYNC_INTERVAL, default 30 s) runs in parallel as a backstop for drift that Watch silently misses: partial mutations, silent stream drops on some managed clusters, and token expiry.

A buffered trigger channel (capacity 1) coalesces bursts — any number of rapid DELETE/MODIFY events produce exactly one reconcile call.

Cleanup Semantics

Webhook configurations are not removed on pod restart by default. Cleanup is opt-in via security.admission.cleanupOnShutdown and security.deletionProtection.cleanupOnShutdown. This avoids race conditions during rollout where a new leader sees an existing webhook while the old leader is still shutting down.

The housekeeper returns only fatal initialization errors. Reconciliation itself is continuous and non-blocking.

webhook/infrastructure.go

Housekeeper reconcilers for infrastructure security state.

Two resources are applied once at startup by cmd/internal.ensureSecurity but have no ongoing reconciler — a human can silently break security by removing them after startup:

  1. Namespace labels — the deletion-protection webhook uses ObjectSelector to narrow to labeled resources. Removing the orkestra.io/deletion-protection label from the operator namespace means the webhook no longer intercepts deletion attempts against the namespace itself. The safety ticker catches this at the normal reconcile cadence.

  2. CRD conversion caBundle — conversion webhooks require the caBundle in the CRD's spec.conversion.webhook.clientConfig.caBundle. If stripped, any CR at an old API version fails conversion immediately. This is watched with a dedicated goroutine so the restore happens within a single API round-trip, not at the next safety-ticker interval.

Kubernetes API server semantics guarantee no reconcile loop: if the patcher produces no change to the stored object (caBundle already correct), no MODIFIED event is emitted. So the event chain terminates after at most two reconciles.

webhook/namespace_protection.go — /namespace-protection webhook handler.

Registered only when security.namespaceProtection.enabled: true in the Katalog. Intercepts CREATE and UPDATE on CRDs that declare allowedNamespaces or restrictedNamespaces, and rejects operations in forbidden namespaces.

failurePolicy: Fail — if Orkestra is unreachable, the operation is blocked.

webhook/registration.go — webhook configuration registration and cleanup.

At startup, when admission webhooks or deletion/namespace protection are enabled, Orkestra creates or updates the corresponding ValidatingWebhookConfiguration and MutatingWebhookConfiguration objects that tell the API server to call Orkestra during admission.

All registration functions are idempotent — safe to call on restart or from the reconciliation controller. Existing configurations are updated to match the current Katalog state.

webhook/strict_mode_protection.go — /strict-mode-protection webhook handler.

Registered only when security.deletionProtection.strictMode: true in the Katalog. Intercepts UPDATE operations on resources that carry the deletion-protection label and denies any request that removes that label — treating label removal as a deletion attempt.

Design

This handler is intentionally separate from /deletion-protection. Deletion protection guards DELETE operations. Strict mode guards label removal on UPDATE. Keeping them separate keeps each handler focused and independently controllable.

Enforcement

The webhook ObjectSelector is set to the deletion-protection label selector, so the API server only calls this handler when either the old or new object carries the label. The handler then checks:

oldObject has label AND newObject does not → DENY (label removal blocked)
otherwise → ALLOW

Enforcement is stateless: every decision is made by comparing old and new labels in the admission request. No in-process register is required.

Unlock

To remove the label from a protected resource, set strictMode: false in the Katalog, edit the Orkestra ConfigMap, and restart Orkestra. The new pods will not register the strict-mode webhook, so label removal becomes possible again. This mirrors how deletion protection itself is disabled — the trust boundary is the Katalog, not a separate escape hatch.

Package webhook implements Orkestra's admission and conversion webhook surface as a domain.Komponent that can be registered and managed alongside other runtime components.

Stats key design

Per-CRD stats (admission, conversion, deletion-protection, namespace-protection) are keyed by "group/version/resource" — the canonical GVR string. This is the merge key used by the control center when combining the runtime and gateway /katalog responses.

Conversion stats require a secondary lookup map (gvkToGVRKey) because the conversion webhook handler receives objects identified by apiVersion+kind, not by their resource plural. Using the full GVK ("group/version/Kind") as the key — rather than just "Kind" — avoids a collision when two CRD entries share the same kind name but differ by version (e.g. cronjob-v1 and cronjob-v2 both have kind: CronJob). Keying by kind alone would cause the second entry in the initialization loop to overwrite the first, so all recorded conversions would land on whichever version happened to be iterated last.

Responsibility

The webhook package owns everything required to operate Kubernetes admission and conversion webhooks:

  • TLS HTTPS server that handles /validate, /mutate, /convert, /deletion-protection, /namespace-protection, and /strict-mode-protection
  • Webhook configuration registration and reconciliation with the API server
  • All HTTP handlers for admission review processing
  • Periodic controller that keeps webhook configurations in sync with the Katalog

The health package (pkg/health) handles only HTTP health and readiness probes. This package handles only the HTTPS webhook surface.

TLS

TLS certificates are provisioned by cmd/internal.ensureSecurity before Start() is called. The certificate file paths are read from konfig.Security().Webhooks after ensureSecurity writes them there. WebhookServer never generates its own certificates — that is the caller's responsibility.

Lifecycle

WebhookServer implements domain.Komponent:

New(kubeClient, katalog, konfig)
  → Start(ctx)    — validate TLS, register endpoints, start HTTPS server,
                    register webhook configs, start controller
  → Shutdown(ctx) — stop HTTPS server, clean up webhook configs on shutdown

All registration and reconciliation errors are logged and non-fatal. The HTTPS server starts only when at least one webhook capability is declared in the Katalog. When no capabilities are declared, Start() is a no-op.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func GVRKey added in v0.4.9

func GVRKey(group, version, resource string) string

GVRKey formats group/version/resource into the canonical stats key. Exported so the gateway handler can build keys from crd.GVR() without duplicating the formatting logic.

func RegisterAdmissionWebhooks

func RegisterAdmissionWebhooks(
	ctx context.Context,
	client kubernetes.Interface,
	registry admissionRegistryReader,
	opts WebhookRegistrationOptions,
) error

RegisterAdmissionWebhooks creates or updates the ValidatingWebhookConfiguration and MutatingWebhookConfiguration based on the current admission registry. Idempotent — safe to call on restart and from the housekeeper.

func UnregisterAdmissionWebhooks

func UnregisterAdmissionWebhooks(
	ctx context.Context,
	client kubernetes.Interface,
	opts WebhookCleanupOptions,
) error

UnregisterAdmissionWebhooks removes the ValidatingWebhookConfiguration and/or MutatingWebhookConfiguration entries previously created from the admission registry. Called from Shutdown() when cleanupOnShutdown is enabled.

Types

type AdmissionRequest

type AdmissionRequest struct {
	UID       string                      `json:"uid"`
	Kind      metav1.GroupVersionKind     `json:"kind"`
	Resource  metav1.GroupVersionResource `json:"resource"`
	Name      string                      `json:"name,omitempty"`
	Namespace string                      `json:"namespace,omitempty"`
	Operation string                      `json:"operation"`
	Object    json.RawMessage             `json:"object,omitempty"`
	OldObject json.RawMessage             `json:"oldObject,omitempty"`
	DryRun    *bool                       `json:"dryRun,omitempty"`
}

AdmissionRequest contains the object the API server is asking about.

type AdmissionResponse

type AdmissionResponse struct {
	UID       string           `json:"uid"`
	Allowed   bool             `json:"allowed"`
	Status    *AdmissionStatus `json:"status,omitempty"`
	Patch     []byte           `json:"patch,omitempty"`
	PatchType *string          `json:"patchType,omitempty"`
	Warnings  []string         `json:"warnings,omitempty"`
}

AdmissionResponse is what Orkestra writes in reply to an AdmissionRequest.

type AdmissionReview

type AdmissionReview struct {
	APIVersion string             `json:"apiVersion"`
	Kind       string             `json:"kind"`
	Request    *AdmissionRequest  `json:"request,omitempty"`
	Response   *AdmissionResponse `json:"response,omitempty"`
}

AdmissionReview is the top-level wrapper sent by the API server. The same struct is used for both request and response directions.

type AdmissionStatus

type AdmissionStatus struct {
	Message string `json:"message"`
	Code    int32  `json:"code"`
}

AdmissionStatus is the rejection reason returned when Allowed is false.

type CRDWatcher added in v0.6.2

type CRDWatcher interface {
	Watch(ctx context.Context, crdName string) (watch.Interface, error)
}

CRDWatcher provides CRD watch capability without importing the apiextensions package into this package. Implemented by the caller (cmd/internal) using the apiextensions client.

type ConversionCRDPatchFn added in v0.6.2

type ConversionCRDPatchFn func(ctx context.Context, crdName, caBundle64, storageVersion string) error

ConversionCRDPatchFn patches the caBundle on a single CRD's conversion webhook. Called by the housekeeper for each CRD that declares conversion.updateCRD: true. The closure captures serviceName, serviceNamespace, and the apiextensions client.

type ConversionReview

type ConversionReview struct {
	APIVersion string                    `json:"apiVersion"`
	Kind       string                    `json:"kind"`
	Request    *ConversionReviewRequest  `json:"request,omitempty"`
	Response   *ConversionReviewResponse `json:"response,omitempty"`
}

type ConversionReviewRequest

type ConversionReviewRequest struct {
	UID               string            `json:"uid"`
	DesiredAPIVersion string            `json:"desiredAPIVersion"`
	Objects           []json.RawMessage `json:"objects"`
}

type ConversionReviewResponse

type ConversionReviewResponse struct {
	UID              string            `json:"uid"`
	ConvertedObjects []json.RawMessage `json:"convertedObjects"`
	Result           *Status           `json:"result"`
}

type JSONPatchOp

type JSONPatchOp struct {
	Op    string      `json:"op"`
	Path  string      `json:"path"`
	Value interface{} `json:"value,omitempty"`
}

JSONPatchOp is one operation in a JSON Patch document (RFC 6902).

type NamespaceRules

type NamespaceRules struct {
	Allowed    map[string]struct{}
	Restricted map[string]struct{}
}

NamespaceRules holds the allow/restrict namespace sets for one CRD.

func (*NamespaceRules) IsNamespaceAllowed

func (r *NamespaceRules) IsNamespaceAllowed(ns string) bool

IsNamespaceAllowed returns true when the namespace passes the CRD's namespace rules.

type Status

type Status struct {
	Status  string `json:"status"`
	Message string `json:"message,omitempty"`
}

type WebhookCleanupOptions

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

WebhookCleanupOptions selects which webhook configurations to remove.

func CleanupAllWebhooks added in v0.3.7

func CleanupAllWebhooks() WebhookCleanupOptions

CleanupAllWebhooks returns a WebhookCleanupOptions that removes both ValidatingWebhookConfiguration and MutatingWebhookConfiguration.

type WebhookRegistrationOptions

type WebhookRegistrationOptions struct {
	Caller                 string
	ServiceName            string
	ServiceNamespace       string
	Port                   int32
	FailurePolicy          admissionv1.FailurePolicyType
	TLSCertFile            string
	OrkestraResourceLabels map[string]string
}

WebhookRegistrationOptions holds the configuration for webhook registration.

type WebhookServer

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

WebhookServer is Orkestra's HTTPS admission and conversion webhook surface. It owns the HTTPS server, all webhook handlers, registration, and the housekeeper that reconciles configurations with the API server.

Created via NewWebhookServer and registered as a domain.Komponent in cmd/internal/runtime_konstructor.go. It starts after the HealthServer so that /ready is live before webhook registration runs.

func NewWebhookServer

func NewWebhookServer(kubeClient kubernetes.Interface, kat *katalog.Katalog, kfg *konfig.Konfig) *WebhookServer

NewWebhookServer constructs a WebhookServer and resolves all declarative webhook behavior from the Katalog and Konfig. No I/O is performed — Start() activates servers, registration, and reconciliation.

func (*WebhookServer) AdmissionStatsFor added in v0.4.9

func (ws *WebhookServer) AdmissionStatsFor(gvrKey string) *health.AdmissionStats

AdmissionStatsFor returns the admission stats for the CRD identified by gvrKey. Returns nil when no stats exist for that key (CRD has no admission webhooks).

func (*WebhookServer) ConversionStatsFor added in v0.4.9

func (ws *WebhookServer) ConversionStatsFor(gvrKey string) *health.ConversionStats

ConversionStatsFor returns the conversion stats for the CRD identified by gvrKey.

func (*WebhookServer) HousekeeperStats added in v0.7.8

func (ws *WebhookServer) HousekeeperStats() *health.WebhookStats

HousekeeperStats returns the housekeeper reconciliation statistics.

func (*WebhookServer) InfraProtectionStats added in v0.4.9

func (ws *WebhookServer) InfraProtectionStats() *health.DeletionProtectionStats

InfraProtectionStats returns the process-level deletion-protection stats that cover the webhook configuration itself and Orkestra infra resources (Deployment, Service, etc.) — events not attributable to a specific CRD GVR.

func (*WebhookServer) Name

func (ws *WebhookServer) Name() string

Name returns the component name.

func (*WebhookServer) NamespaceStatsFor added in v0.4.9

func (ws *WebhookServer) NamespaceStatsFor(gvrKey string) *health.NamespaceProtectionStats

NamespaceStatsFor returns the namespace-protection stats for the CRD identified by gvrKey.

func (*WebhookServer) ProtectionStatsFor added in v0.4.9

func (ws *WebhookServer) ProtectionStatsFor(gvrKey string) *health.DeletionProtectionStats

ProtectionStatsFor returns the deletion-protection stats for the CRD identified by gvrKey.

func (*WebhookServer) SetCRDWatcher added in v0.6.2

func (ws *WebhookServer) SetCRDWatcher(w CRDWatcher)

SetCRDWatcher provides the housekeeper with the CRD watch capability. Called from cmd/internal when conversion is enabled. Without this, the conversion CRD reconciler still runs on the safety ticker — the watcher is the fast path only.

func (*WebhookServer) SetCertBundle added in v0.4.9

func (ws *WebhookServer) SetCertBundle(certPEM, keyPEM, caPEM []byte, secretName, namespace string)

SetCertBundle stores the TLS bundle and Secret coordinates so the housekeeper can restore the Secret if it is deleted during a rollout. Only called when Orkestra generated the certificates (not when the user provides TLS_CERT/TLS_KEY).

func (*WebhookServer) SetCertManager

func (ws *WebhookServer) SetCertManager(m certManagerIface)

SetCertManager provides the cert manager for TLS secret cleanup on graceful shutdown. Set only when Orkestra generated self-signed certificates (certMgr == nil when the user provided explicit TLS_CERT/TLS_KEY).

func (*WebhookServer) SetConversionCRDPatcher added in v0.6.2

func (ws *WebhookServer) SetConversionCRDPatcher(fn ConversionCRDPatchFn)

SetConversionCRDPatcher provides the housekeeper with the function it needs to re-patch CRD conversion webhooks. Called from cmd/internal after the kubeclient and katalog are available.

func (*WebhookServer) Shutdown

func (ws *WebhookServer) Shutdown(ctx context.Context)

Shutdown gracefully stops the HTTPS server and performs declarative cleanup of webhook configurations as declared in the Katalog.

func (*WebhookServer) Start

func (ws *WebhookServer) Start(ctx context.Context) error

Start activates the WebhookServer. It registers all declared webhook endpoints, launches the HTTPS server, performs best-effort webhook registration with the API server, and starts the reconciliation controller.

Start is a no-op when no webhook capabilities are declared in the Katalog or when the process is not running inside a Kubernetes cluster.

func (*WebhookServer) Started

func (ws *WebhookServer) Started() bool

Started reports whether Start() has been called.

func (*WebhookServer) StrictModeStats added in v0.4.9

func (ws *WebhookServer) StrictModeStats() *health.DeletionProtectionStats

StrictModeStats returns the strict-mode-protection statistics (process-global).

Jump to

Keyboard shortcuts

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