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: 23 Imported by: 0

README

pkg/controller/webhook

Event adapter that mounts the pure HTTPS server from pkg/webhook on the controller's lifecycle, registers one ValidationFunc per webhook rule, and routes each admission request through the dry-run validator.

Certificates are supplied ready-to-use — cert-manager provisions the Secret, the chart mounts it, and the controller passes that mounted directory into this component's Config.CertDir. The pure server reads tls.crt/tls.key from there and hot-reloads them on rotation (a cert-manager renewal is served without a restart). This package does not manage certs, CA bundles, or ValidatingWebhookConfiguration resources (the Helm chart provisions those).

Minimal Usage

import (
    "context"

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

cfg := &webhook.Config{
    Port:            9443,                    // default
    Path:            "/validate",             // default
    CertDir:         "/etc/webhook/certs",    // mounted cert Secret; server reads + hot-reloads tls.crt/tls.key
    Rules:           rules,                   // built by ExtractWebhookRules(cfg)
    DryRunValidator: dryRunValidator,         // pkg/controller/dryrunvalidator.Component
}

comp := webhook.New(logger, cfg, restMapper, metricsRecorder)
if err := comp.Start(ctx); err != nil {
    return err
}

Start blocks until the server returns an error or ctx is cancelled; on cancellation it shuts the HTTPS listener down gracefully. The component's reinitialisation lifecycle is owned by the controller — when the CRD changes, the controller cancels the iteration context and Start returns cleanly.

Config

Field Notes
Port TCP port for the HTTPS listener (default 9443)
Path URL path that handles POST /… AdmissionReview calls (default /validate)
CertDir Directory holding the mounted cert Secret (tls.crt/tls.key). In production the controller sets this to the mount path. The pure server resolves the cert per handshake and hot-reloads it on rotation — a cert-manager renewal is served without a restart.
CertPEM / KeyPEM Fixed PEM-encoded TLS material, used only when CertDir is unset (e.g. unit tests). Empty values with no CertDir cause Start to return an error.
Rules []WebhookRule, one per kind to register. Built from the CRD via ExtractWebhookRules(cfg *config.Config).
DryRunValidator Interface with a single method: ValidateDirect(ctx, gvk, namespace, name, object, operation) (allowed bool, reason string, warnings []string). Satisfied by pkg/controller/dryrunvalidator.Component. Warnings flow through to AdmissionResponse.Warnings on both allow and deny paths (e.g. soft diagnostics from pluggable validator sidecars). If nil, the component fails open (accepts everything) — useful only in tests.

restMapper is used to resolve (APIGroup, APIVersion, Resource)Kind when wiring rules into "group/version.Kind" registration keys that the underlying pkg/webhook.Server expects. A live meta.RESTMapper from the controller's cluster connection is required.

metrics implements two methods: RecordWebhookRequest(gvk, result, durationSec) and RecordWebhookValidation(gvk, result). pkg/controller/metrics satisfies this directly; pass nil to skip metrics entirely.

Validator Flow

Registration happens once at Start:

  1. For each Rule, resolve Kind via restMapper and build a group/version.Kind key.
  2. Register a thin wrapper ValidationFunc that:
    • Performs basic structural sanity (validateBasicStructure) and short-circuits on failure before touching the validator.
    • Wraps the call in a 5-second context.WithTimeout — kept shorter than the chart's timeoutSeconds: 10 so a stuck render returns a structured deny rather than an HTTP transport failure.
    • Delegates to DryRunValidator.ValidateDirect(ctx, gvk, namespace, name, object, operation).
    • Records metrics and logs the outcome.

ValidateDirect renders the config against an overlay store that includes the proposed change (via pkg/stores.StoreOverlay) and runs the full three-phase HAProxy validation. If any phase fails, the webhook denies with the simplified error message; if all pass, it allows.

Integration Points

  • Upstream — cert-manager provisions the Secret, the chart mounts it, and the controller passes the mount path into this component's Config.CertDir. There is no API fetch of the Secret and no dedicated cert event: the pure server reads tls.crt/tls.key from CertDir and hot-reloads them when cert-manager renews the cert — served within ~a minute, no controller iteration restart or pod restart needed.
  • Downstreampkg/controller/dryrunvalidator is the only implementation of the DryRunValidator interface in the tree. It in turn delegates the actual render+validate to pkg/controller/proposalvalidator, which is the same pipeline the leader-side reconciler uses — so anything that passes admission will also pass at deploy time.
  • Chartcharts/haptic/templates/validatingwebhookconfiguration.yaml defines the ValidatingWebhookConfiguration with two webhook entries: watched-resource webhooks use failurePolicy: Fail, timeoutSeconds: 10; the HAProxyTemplateConfig webhook uses failurePolicy: Ignore, timeoutSeconds: 5. When webhook.certManager.enabled, cert-manager's cert-manager.io/inject-ca-from annotation is added. The chart does not set an objectSelector; multi-controller isolation comes from each release deploying its own webhook configuration whose clientConfig.service points at that release's controller Service.

See Also

  • pkg/webhook — pure HTTPS server + AdmissionReview protocol
  • pkg/controller/dryrunvalidator — the DryRunValidator implementation this component calls into
  • pkg/controller/proposalvalidator — the speculative render+validate pipeline shared by dryrunvalidator (synchronous, this webhook path) and the background HTTP-content refresh in pkg/controller/httpstore (asynchronous)
  • The controller supplies the mounted Secret directory via Config.CertDir, which the pure server reads and hot-reloads; the Helm chart provisions and mounts the cert Secret
  • docs/controller/docs/development/crd-validation-design.md — why the webhook fails closed, lives in the controller pod, and runs the same render/validate code as the reconciler

License

Apache-2.0 — see root LICENSE.

Documentation

Overview

Package webhook provides the webhook adapter component that bridges the pure webhook library to the event-driven controller architecture.

The webhook component manages the lifecycle of admission webhooks including:

  • HTTPS webhook server
  • Integration with controller validators

Note: TLS certificates are fetched from Kubernetes Secret via API. ValidatingWebhookConfiguration is created by Helm at installation time.

Index

Constants

View Source
const (
	// ComponentName is the unique identifier for this component.
	ComponentName = "webhook"

	// DefaultWebhookPort is the default HTTPS port for the webhook server.
	DefaultWebhookPort = 9443

	// DefaultWebhookPath is the default URL path for validation requests.
	DefaultWebhookPath = "/validate"
)
View Source
const HAProxyTemplateConfigGVK = "haproxy-haptic.org/v1alpha1.HAProxyTemplateConfig"

HAProxyTemplateConfigGVK is the canonical GVK string the webhook server uses to dispatch admission requests for the controller's own CRD.

Variables

This section is empty.

Functions

This section is empty.

Types

type Component

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

Component is the webhook adapter component that manages webhook lifecycle.

It coordinates the pure webhook library server with the event-driven controller architecture.

func New

func New(logger *slog.Logger, config *Config, restMapper meta.RESTMapper, metrics MetricsRecorder) *Component

New creates a new webhook component.

Parameters:

  • logger: Structured logger
  • config: Component configuration (must include CertPEM and KeyPEM)
  • restMapper: RESTMapper for resolving resource kinds from GVR
  • metrics: Optional metrics recorder (can be nil)

Returns:

  • A new Component instance ready to be started

func (*Component) Listening

func (c *Component) Listening() <-chan struct{}

Listening returns a channel that is closed once the underlying webhook server has bound its TLS listener. Until this channel is closed, an admission request routed at the controller fails with "connection refused", because the listening socket simply doesn't exist yet.

func (*Component) Start

func (c *Component) Start(ctx context.Context) error

Start starts the webhook component.

This method: 1. Validates TLS certificates from configuration 2. Creates and starts the webhook HTTPS server 3. Publishes lifecycle events

The server continues running until the context is cancelled.

type Config

type Config struct {
	// Port is the HTTPS port for the webhook server.
	// Default: 9443
	Port int

	// Path is the URL path for validation requests.
	// Default: "/validate"
	Path string

	// CertDir, when set, is a directory containing tls.crt and tls.key
	// (typically the mounted webhook-cert Secret). The server reads them
	// per-handshake and hot-reloads a rotated certificate without a restart.
	// Takes precedence over CertPEM/KeyPEM; production uses this path.
	CertDir string

	// CertPEM is the PEM-encoded TLS certificate.
	// Used when CertDir is unset (e.g. tests pass certs directly).
	CertPEM []byte

	// KeyPEM is the PEM-encoded TLS private key.
	// Used when CertDir is unset.
	KeyPEM []byte

	// Rules defines which resources the webhook validates.
	// Used for registering validators by GVK.
	Rules []WebhookRule

	// DryRunValidator performs dry-run validation of resources.
	// If nil, validation is skipped (fail-open).
	DryRunValidator DryRunValidator

	// ConfigValidator validates HAProxyTemplateConfig admission requests.
	// If nil, HAProxyTemplateConfig admission is admitted unconditionally
	// (no handler registered → pure server's fail-open path). The chart
	// pairs this with failurePolicy=Ignore so missing controller doesn't
	// break the chicken-and-egg of first install / recovery.
	ConfigValidator ConfigValidatorFunc
}

Config configures the webhook component.

type ConfigValidator

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

ConfigValidator validates a prospective HAProxyTemplateConfig admission by:

  1. Parsing the admitted CRD spec into a *config.Config.
  2. Compiling an ephemeral template engine from the prospective templates.
  3. Building an ephemeral RenderService + Pipeline (strict ValidationService) and executing it against the controller's CURRENT resource stores.
  4. Mapping the result to an admission decision.

This is the upstream gate that lets the leader-side reconcile pipeline safely skip `haproxy -c`.

Failure-policy on the chart-side ValidatingWebhookConfiguration is `Ignore`, so this validator MUST be safe to be entirely absent: when the webhook is unreachable, the dataplane API still runs `haproxy -c` server-side before accepting any /raw push, and the controller surfaces the resulting failure via HAProxyCfg.status.

func NewConfigValidator

func NewConfigValidator(cfg *ConfigValidatorConfig) *ConfigValidator

NewConfigValidator constructs a ConfigValidator. Panics if any required field is nil — these are construction-time mistakes that should fail loudly, not at admission time.

func (*ConfigValidator) ValidateDirect

func (v *ConfigValidator) ValidateDirect(ctx context.Context, gvk, namespace, name string, object any, operation string) (allowed bool, reason string, warnings []string)

ValidateDirect performs synchronous validation of a prospective HAProxyTemplateConfig admission. Returns the same triple the webhook component's DryRunValidator interface returns so it plugs into the same ValidationFunc bridge.

On DELETE: admits without rendering — the deletion can't render anything. On CREATE/UPDATE: parses the prospective CRD, builds an ephemeral pipeline, executes against live stores, maps result to a decision.

type ConfigValidatorConfig

type ConfigValidatorConfig struct {
	// Logger is the structured logger.
	Logger *slog.Logger

	// StrictValidator is the strict ValidationService used for the
	// admission webhook (full syntax + schema + `haproxy -c` semantic
	// validation). MUST be the strict instance — the whole point of this
	// gate is to catch bad templates that the fast leader-side pipeline
	// would skip.
	StrictValidator *validation.ValidationService

	// StoreProvider grants access to the controller's live resource
	// stores. The prospective config is rendered against current cluster
	// state — no overlay is applied (unlike DryRunValidator, which is
	// validating a hypothetical resource addition/update).
	StoreProvider stores.StoreProvider

	// Capabilities are the HAProxy version capabilities the ephemeral
	// RenderService needs to compute capability-conditional output.
	Capabilities dataplane.Capabilities

	// HTTPStoreComponent (optional) wires `http.Fetch` for templates that
	// use it. Pass the live controller's instance — admission renders
	// against accepted HTTP-store content, same as DryRunValidator.
	HTTPStoreComponent *ctrlhttpstore.Component

	// Declarations carries the typed-resource globals from typebootstrap
	// (and the currentConfig declaration). The ephemeral engine MUST be
	// constructed with these so chart templates compile identically
	// against either render path.
	Declarations map[string]any

	// TypedResourceTypes is the per-resource generated Go type map fed
	// to the ephemeral RenderService so it wraps each store snapshot
	// into the typed shape expected by templates. Used as the fallback
	// when Bootstrap is nil or its live-schema resolution fails.
	TypedResourceTypes map[string]reflect.Type

	// Bootstrap (optional) resolves typed schemas from the PROSPECTIVE config
	// at admission, so admission builds the engine from the same type set the
	// daemon load gate will use on load (true parity — see SchemaBootstrapper).
	// When nil, the validator falls back to the startup-fixed Declarations /
	// TypedResourceTypes (the prior behavior; still correct for configs that
	// don't change their own typed watchedResources). Production wires the live
	// bootstrapper; unit tests may leave it nil or pass a stub.
	Bootstrap SchemaBootstrapper
}

ConfigValidatorConfig wires the ConfigValidator's dependencies. All fields are required except HTTPStoreComponent (templates that use `http.Fetch` need it; chart-default templates do not).

type ConfigValidatorFunc

type ConfigValidatorFunc func(ctx context.Context, gvk, namespace, name string, object any, operation string) (allowed bool, reason string, warnings []string)

ConfigValidatorFunc validates a HAProxyTemplateConfig admission request. Same signature shape as DryRunValidator.ValidateDirect — kept as a function type rather than an interface so test wiring can pass a plain closure without declaring a satellite type. Nil means "no validator configured" — handler falls back to allow (failurePolicy=Ignore on the chart-side ValidatingWebhookConfiguration covers the remaining gap).

type DryRunValidator

type DryRunValidator interface {
	ValidateDirect(ctx context.Context, gvk, namespace, name string, object any, operation string) (allowed bool, reason string, warnings []string)
}

DryRunValidator defines the synchronous interface the webhook uses to validate resources. The implementation in pkg/controller/dryrunvalidator is a library, not a lifecycle component.

Warnings are surfaced via AdmissionResponse.Warnings on both allow and deny paths so kubectl prints them as "Warning:" lines without blocking admission. Pluggable validators (e.g. the SPOA hub running in --validate-socket mode) populate this slice from their non-fatal diagnostics; the standard render+validate pipeline does not.

type MetricsRecorder

type MetricsRecorder interface {
	RecordWebhookRequest(gvk, result string, durationSeconds float64)
	RecordWebhookValidation(gvk, result string)
}

MetricsRecorder defines the interface for recording webhook metrics. This allows the component to work with or without metrics.

type SchemaBootstrapper

type SchemaBootstrapper func(ctx context.Context, cfg *coreconfig.Config) (*typebootstrap.Result, error)

SchemaBootstrapper resolves the typed-resource reflect.Types and declarations for a prospective config from live schemas. It mirrors the validator.TypeBootstrapper the daemon load gate uses; the two share the same underlying signature so the controller wiring can convert one to the other.

When set on a ConfigValidator, admission bootstraps schemas from the PROSPECTIVE config — exactly as the load gate does on load — so the two gates build the test/render engine from the same type set. This is what makes the "a config the webhook admits will load" guarantee hold even when the prospective config changes its own typed `watchedResources` relative to what the controller was started with. Without it, the webhook validated against the startup-fixed type set, which could both false-reject a config that adds a new typed watched resource and false-admit one whose tests pass under the stale types but fail under the new ones.

type WebhookRule

type WebhookRule struct {
	// APIGroup that this rule matches.
	// Example: "networking.k8s.io"
	APIGroup string

	// APIVersion that this rule matches.
	// Example: "v1"
	APIVersion string

	// Resource that this rule matches (plural, lowercase).
	// Example: "ingresses"
	Resource string
}

WebhookRule specifies which resources a webhook should intercept.

func ExtractWebhookRules

func ExtractWebhookRules(cfg *config.Config) []WebhookRule

ExtractWebhookRules extracts webhook rules from controller configuration.

It iterates through watched resources and creates webhook rules for resources with enable_validation_webhook: true.

Parameters:

  • cfg: Controller configuration containing watched resources

Returns:

  • Slice of webhook rules for resources that have validation enabled
  • Empty slice if no resources have webhook validation enabled

Jump to

Keyboard shortcuts

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