Unreleased documentation. Choose your installed release in the version menu. Features described here may be absent from that release.
CRD & validation design notes¶
This page captures the durable design decisions behind the HAProxyTemplateConfig CRD and the validation stack. For field-by-field reference see CRD Reference; for user-facing validation workflows see Validation Tests.
Why a CRD Instead of a ConfigMap¶
The controller originally accepted its configuration via ConfigMap. That was replaced with a typed, namespaced Kubernetes API (scope: Namespaced, defined in pkg/apis/haproxytemplate/v1alpha1) because:
- Schema validation at admission. OpenAPI rules in the CRD reject malformed YAML before it reaches the controller — a
ConfigMapaccepts any strings. - Embedded test fixtures.
spec.validationTestslets users ship assertions alongside templates and run them fromhaptic validateandhaptic preflight, and on every controller start via the load gate, without duplicating fixtures elsewhere. - Native tooling.
kubectl get htplcfg,kubectl describe, status subresource, RBAC on a real Kind — all come for free. - Typed client. The generated clientset (
pkg/generated) gives both the controller and test harnesses a typed view of the config.
Defence in depth¶
Validation of the config itself and validation of watched resources take different paths. Four layers run in sequence:
- OpenAPI schema and validation rules (Kubernetes API server). Rejects structural errors the moment
kubectl applyhits the apiserver — invalid enum values, missing required fields, bad types. A spec-levelx-kubernetes-validationsrule additionally enforces that a config carriespodSelector, at least onewatchedResourcesentry, and ahaproxyConfigeither inline or from aspec.libraryRefsentry. This covers structure only; whether the templates compile and render to a confighaproxy -caccepts is layers 3 and 4. - Validating admission webhook (the controller itself, served at
/validate). The chart emits one rule per watched resource markedenableValidationWebhook: true. Bundled libraries enable admission for routing resources and Gateway policies. Each rule renders the proposed object over the live store and rejects invalid configuration. Services, EndpointSlices, ConfigMaps, and Secrets remain watched without an admission webhook. Gateway policy credentials and external WAF catalogs require immutable Secrets and ConfigMaps: Kubernetes prevents data changes, and route/policy admission validates each new reference. See ADR-0026. - Authoritative render validation (leader and proposal paths). The
HAProxyTemplateConfighas no admission webhook — ADR-0016 removed it, because admission sees one object at a time and a configuration is a set. The leader instead validates the merged set. Watched-resource admission and HTTP-store promotion runhaproxy -cand every configured rendered-output validator through the proposal pipeline. Reconciliation runs rendered-output validators in its pipeline andhaproxy -cin the leader-side render gate. Protocol-v1 external checks run on every applicable invocation. The render gate tracks HAProxy verdicts by plan identity. ADR-0022 defines the current validation paths. - Validation at apply (HAProxy itself). The pod's own binary parses the configuration at reload and rejects a command it can't run. This is defence in depth, not a replacement for the controller-side gate: no invalid render may publish a success event or reach deployment. A rejection carries HAProxy's own message back, and the agent restores the last known good file set.
A fifth gate sits outside the reconcile path: the startup load gate runs the config's embedded validationTests on every fresh or upgraded controller pod and fails the iteration if they don't pass, so a bad config crash-loops the new pod rather than replacing a working one. It stamps the reason onto status.conditions[Validated] with reason LoadGateFailed before it does. Operators can run the same checks ahead of helm upgrade with haptic preflight.
A rejected update preserves the last accepted configuration. Traffic still depends on the backends in that configuration remaining available. The watched-resource webhooks ship failurePolicy: Fail (charts/haptic/templates/validatingwebhookconfiguration.yaml), so creates and updates of opted-in resources are rejected when the controller is unreachable; that's deliberate, since rendering an admission decision from an unvalidated overlay is riskier than asking the user to retry.
Credentials Stay in a Secret¶
spec.credentialsSecretRef points at a Secret rather than embedding credentials inline:
- Keeps
HAProxyTemplateConfignon-sensitive, so it can be stored in Git / Helm values / etc. - Allows independent rotation: the credentials loader watches the Secret and re-publishes
CredentialsUpdatedEventwithout a full reinitialization cycle. - Follows the conventional Kubernetes split between "what to do" (typed API) and "secrets needed to do it" (opaque Secret).
Required keys: dataplane_username, dataplane_password.
Webhook Architecture¶
The validating webhook runs inside the controller pod rather than a dedicated deployment. Rationale:
- Single source of truth. The webhook uses the same render/validate code path as the reconciler, so there's no way for the admission decision to drift from the runtime decision.
- No extra moving parts. One deployment, one Service, one Lease, one cert.
- Shared store. The webhook reuses the watched-resource store to build a realistic render context with proposed changes overlaid (see
pkg/stores/overlay.go).
TLS certificates come from the chart's own self-signed issuance (the default), from cert-manager (controller.webhook.certManager.enabled=true, auto-rotating), or supplied manually via controller.webhook.caBundle; the chart wires all three options.
Multi-controller isolation¶
Each Helm release has its own webhook Service. Kubernetes calls every webhook whose rules match the proposed resource; different Service names don't isolate admission. Each controller applies its configured watch selectors when building the proposal. Keep class ownership distinct as described in multiple HAPTIC installations. Don't narrow webhook coverage to work around a rejected configuration change.
CRD versioning posture¶
The API is at v1alpha1. The project's posture during alpha:
- Breaking changes are allowed but batched: new sub-fields should be added as optional; renames/removals wait for a minor bump.
- No conversion webhooks yet. Graduating to
v1beta1adds one, plus a conversion strategy in the release notes. - The generated clientset, informers, and listers live in
pkg/generated; regenerate viamake generateafter changing types inpkg/apis/haproxytemplate/v1alpha1/.
Relationship to other docs¶
| Concern | Canonical doc |
|---|---|
| Every field with types and defaults | CRD Reference |
| Writing and debugging validation tests | Validation Tests |
| How configuration composes (chart layers, snippets) | Configuration Model |
| Webhook TLS, RBAC, chart deployment | Deploying with Helm, SSL Certificates |
| Go type definitions | pkg/apis/haproxytemplate/v1alpha1/types_*.go |
| Generated code | pkg/generated/ (make generate to refresh) |