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:
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.
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 ¶
- func GVRKey(group, version, resource string) string
- func RegisterAdmissionWebhooks(ctx context.Context, client kubernetes.Interface, ...) error
- func UnregisterAdmissionWebhooks(ctx context.Context, client kubernetes.Interface, opts WebhookCleanupOptions) error
- type AdmissionRequest
- type AdmissionResponse
- type AdmissionReview
- type AdmissionStatus
- type CRDWatcher
- type ConversionCRDPatchFn
- type ConversionReview
- type ConversionReviewRequest
- type ConversionReviewResponse
- type JSONPatchOp
- type NamespaceRules
- type Status
- type WebhookCleanupOptions
- type WebhookRegistrationOptions
- type WebhookServer
- func (ws *WebhookServer) AdmissionStatsFor(gvrKey string) *health.AdmissionStats
- func (ws *WebhookServer) ConversionStatsFor(gvrKey string) *health.ConversionStats
- func (ws *WebhookServer) HousekeeperStats() *health.WebhookStats
- func (ws *WebhookServer) InfraProtectionStats() *health.DeletionProtectionStats
- func (ws *WebhookServer) Name() string
- func (ws *WebhookServer) NamespaceStatsFor(gvrKey string) *health.NamespaceProtectionStats
- func (ws *WebhookServer) ProtectionStatsFor(gvrKey string) *health.DeletionProtectionStats
- func (ws *WebhookServer) SetCRDWatcher(w CRDWatcher)
- func (ws *WebhookServer) SetCertBundle(certPEM, keyPEM, caPEM []byte, secretName, namespace string)
- func (ws *WebhookServer) SetCertManager(m certManagerIface)
- func (ws *WebhookServer) SetConversionCRDPatcher(fn ConversionCRDPatchFn)
- func (ws *WebhookServer) Shutdown(ctx context.Context)
- func (ws *WebhookServer) Start(ctx context.Context) error
- func (ws *WebhookServer) Started() bool
- func (ws *WebhookServer) StrictModeStats() *health.DeletionProtectionStats
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func GVRKey ¶ added in v0.4.9
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 ¶
AdmissionStatus is the rejection reason returned when Allowed is false.
type CRDWatcher ¶ added in v0.6.2
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 ¶
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 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).