HAProxyTemplateConfig CRD reference¶
Overview¶
One HAProxyTemplateConfig resource defines everything HAPTIC does: what it watches, what it renders, and the tests that gate deployment. It provides schema validation, status conditions, and embedded testing capabilities. Bulky template content can live in separate HAProxyTemplateLibrary objects that the config pulls in through libraryRefs — the chart does this for every template library it ships.
API Group: haproxy-haptic.org
API Version: v1alpha1
Kind: HAProxyTemplateConfig
Short Names: htplcfg, haptpl
The schema is deliberately resource-agnostic — you template whatever you watch, so it works on a bespoke CRD exactly as it does on Ingress.
▶ Open the custom-CRD example in the playground — HAPTIC templating any resource, not just Ingress.
Basic example¶
Run the whole custom resource in your browser to watch it render to a minimal haproxy.cfg.
Spec fields¶
The three fields the apiserver requires come first (podSelector, watchedResources, and a haproxyConfig — inline or supplied by a libraryRefs entry), followed by credentialsSecretRef, the template entries, and the operational tuning fields.
credentialsSecretRef¶
Names the Secret holding the agent credentials. Optional — the schema doesn't require it, and the controller never reads it: it resolves the credentials Secret by the name given in --secret-name / the SECRET_NAME environment variable, both set by the Helm chart. The field records the wiring for readers and tooling; the namespace sub-field has no effect.
| Field | Type | Required | Default |
|---|---|---|---|
name |
string | Yes | — |
namespace |
string | No | The config's namespace |
The Secret must contain the keys dataplane_username and dataplane_password — the keys keep their names across the agent cutover, so a rotation set up before it still works. Credentials authenticate the controller to each pod's agent; config validation runs locally against the haproxy binary and needs no credentials. See Security — Credentials for rotation and GitOps caveats.
podSelector¶
Labels that identify which HAProxy pods the controller manages. Required.
| Field | Type | Required | Default |
|---|---|---|---|
matchLabels |
map[string]string |
Yes (at least one label) | — |
The Helm chart ships app.kubernetes.io/component: loadbalancer (plus dynamically set app.kubernetes.io/name / app.kubernetes.io/instance); use any labels your HAProxy pods actually carry. See HAProxy Deployment — Pod Requirements for what discovered pods must provide.
watchedResources¶
Defines which Kubernetes resources to watch. Each map key is an arbitrary name that appears in templates as resources.<key>. Required (at least one entry).
More than one key can target the same Kubernetes group, version, and resource tuple, including with different selectors. Admission validation applies a proposed API write to every matching alias, so the dry-run view matches the post-admission watcher stores.
| Field | Type | Required | Default |
|---|---|---|---|
apiVersion |
string | Exactly one of apiVersion / apiVersions |
— |
apiVersions |
[]string |
Exactly one of apiVersion / apiVersions |
— |
optional |
bool | No | false |
resources |
string | Yes | — |
indexBy |
[]string |
Optional in the schema, required in practice | — (at least one expression; config.ValidateStructure rejects a merged config whose watched resource declares none, so an empty list is refused at config load rather than at kubectl apply) |
labelSelector |
string | No | "" (equality-only, "k=v[,k=v]"; set-based syntax not supported) |
fieldSelector |
string | No | "" (client-side JSONPath equality, "field.path=value"; matches any field) |
store |
string (full / on-demand) |
No | full |
enableValidationWebhook |
bool | No | false |
debounceInterval |
string | No | "" — empty / invalid uses the 100ms default; an explicit "0" disables debouncing |
watchedResources:
ingresses:
apiVersion: networking.k8s.io/v1
resources: ingresses
indexBy:
- metadata.namespace
- metadata.name
Instead of a single apiVersion, an entry can declare an ordered
apiVersions candidate list together with optional: true. The controller
resolves the entry to the first candidate the cluster serves — at startup
and again whenever a matching CRD is installed, upgraded, or removed — so
your configuration works across CRD releases without redeployment:
watchedResources:
tcproutes:
apiVersions:
- gateway.networking.k8s.io/v1
- gateway.networking.k8s.io/v1alpha2
optional: true # no served candidate → drop the watch, strip dependent features
resources: tcproutes
indexBy:
- metadata.namespace
- metadata.name
Rules:
apiVersionandapiVersionsare mutually exclusive; exactly one must be set.- A required entry (no
optional) whose candidates are all unserved fails startup with an error naming the resource — the controller retries and converges when the CRD appears. - An optional entry whose candidates are all unserved is dropped, and every
templateSnippets/validationTestsentry whoserequiresnames it gets stripped from the effective configuration. - Templates read the resolved version via
resources.<name>.APIVersion(). - The current resolution is visible at
/debug/vars/effectiveConfigResolution.
See Watching Resources for the store types, indexing semantics, and selector behaviour.
watchedResourcesIgnoreFields¶
JSONPath expressions for fields to remove from all watched resources before they're indexed, reducing memory usage.
| Field | Type | Required | Default |
|---|---|---|---|
watchedResourcesIgnoreFields |
[]string |
No | — |
watchedResourcesIgnoreFields:
- metadata.managedFields
- metadata.annotations['kubectl.kubernetes.io/last-applied-configuration']
Applies uniformly to every watched resource; fields referenced by indexBy must not be trimmed. See Watching Resources — Trimming Fields.
haproxyConfig¶
The main HAProxy configuration template. Required.
| Field | Type | Required | Default |
|---|---|---|---|
template |
string | Yes | — |
postProcessing |
[]PostProcessor |
No | — (see postProcessing) |
haproxyConfig:
template: |
global
daemon
maxconn 4096
defaults
mode http
timeout connect 5s
frontend http
bind *:80
use_backend %[req.hdr(host),map({{ pathResolver.GetPath("host.map", "map") }})]
See the Templating Guide for syntax, loops, and helper functions.
libraryRefs¶
Ordered list of HAProxyTemplateLibrary objects whose content is merged into this config (optional).
| Field | Type | Required | Default |
|---|---|---|---|
name |
string | Yes | — (a HAProxyTemplateLibrary in this config's namespace) |
revision |
string | Yes | — (must equal that object's spec.revision) |
libraryRefs:
- {name: haproxy-config-base, revision: "base-43dc4467f7e88090"}
- {name: haproxy-config-ssl, revision: "ssl-5da793f017afc1c5"}
Earlier entries are overridden by later ones, and the config's own inline content wins last — so the object you edit is always the override point, whatever the order of the list.
The controller renders only when every reference resolves to an object reporting exactly that spec.revision. Otherwise it keeps serving the last-good configuration and logs Holding the last-good configuration. Libraries deliberately override one another, so a half-applied set silently changes behaviour rather than removing it — a config missing its WAF library would render fine and serve traffic unarmed.
The revisions are compared as strings and never recomputed from content. A writer that applies the config and its libraries together stamps the same value on each, so a torn apply shows up as a mismatch; editing a snippet's body in place leaves the revision alone and takes effect immediately.
templateSnippets¶
Reusable template fragments, included in other templates via {{ render "snippet-name" }}.
| Field | Type | Required | Default |
|---|---|---|---|
template |
string | Yes | — |
requires |
[]string |
No | — (names of watchedResources keys) |
templateSnippets:
backend-name:
requires: [ingresses]
template: |
ing_{{ ingress.metadata.namespace }}_{{ ingress.metadata.name }}
requires entries must name watchedResources keys: when an optional watched
resource named there is unavailable, the snippet is stripped from the effective
configuration. A snippet that must survive stripping may reach a stripped
resource only through compile-safe seams — render "..." default "",
render_glob extension points, or shared state — never a direct typed
resources.<name> reference. See Templating — Template Snippets.
maps¶
HAProxy map file templates. Each key is a map filename, referenced in config via {{ pathResolver.GetPath("host.map", "map") }}.
| Field | Type | Required | Default |
|---|---|---|---|
template |
string | Yes | — |
postProcessing |
[]PostProcessor |
No | — (see postProcessing) |
ordered |
bool | No | true |
maps:
host.map:
ordered: false
template: |
{% for _, ingress := range resources.ingresses.List() %}
{% for _, rule := range ingress.spec.rules %}
{{ rule.host }} {{ ingress.metadata.name }}_backend
{% end %}
{% end %}
Set ordered: false when the configuration reads the map with map_str, map_beg, map_ip or map_str_int. Those find a key by its own value, so the controller can add a new entry over the runtime API instead of rewriting the file and reloading HAProxy.
Keep the default true for map_reg, map_sub, map_dom, map_dir and map_end. HAProxy evaluates those as a list and takes the first match, so an entry has to land in its intended position — appending it to the end would silently never match.
files¶
General auxiliary file templates (error pages, etc.). Each key is a filename, referenced in config via {{ pathResolver.GetPath("503.http", "file") }}.
| Field | Type | Required | Default |
|---|---|---|---|
template |
string | Yes | — |
postProcessing |
[]PostProcessor |
No | — (see postProcessing) |
reloadOnPush |
bool | No | true |
files:
503.http:
template: |
HTTP/1.1 503 Service Unavailable
<html><body><h1>503</h1></body></html>
Set reloadOnPush: false when a sidecar owns the file and watches it itself — the bundled Vector and SPOA-hub configs both do. HAProxy never opens those, so the controller writes the new content and skips the reload. Keep the default for anything the HAProxy configuration references: only a reload makes that content take effect.
reloadOnPush governs writes. Removing a file reloads only when the rendered configuration, or a crt-list, still names it — that reference would otherwise dangle until some later change reloaded HAProxy and every worker failed to start. A sidecar-owned file is named nowhere, so removing it doesn't reload either.
See Templating — General Files.
sslCertificates¶
SSL certificate templates, typically assembled from watched Secrets. Each key is a certificate name, referenced in config via {{ pathResolver.GetPath("example-com", "cert") }}.
| Field | Type | Required | Default |
|---|---|---|---|
template |
string | Yes | — |
postProcessing |
[]PostProcessor |
No | — (see postProcessing) |
sslCertificates:
example-com:
template: |
{% var secret = resources.secrets.GetSingle("default", "tls-cert") %}
{{ b64decode(secret.data["tls.crt"]) }}
{{ b64decode(secret.data["tls.key"]) }}
See Templating — SSL Certificates.
k8sResources¶
Templates that emit Kubernetes resources for the controller to apply via Server-Side Apply. Each entry's rendered output is parsed as one or more YAML documents (multi-doc supported via --- separators); each document must declare apiVersion, kind, and metadata.name (plus metadata.namespace for namespaced kinds).
| Field | Type | Required | Default |
|---|---|---|---|
template |
string | Yes | — |
postProcessing |
[]PostProcessor |
No | — (see postProcessing) |
The controller injects an OwnerReference to the HAProxyTemplateConfig CR (controller=true, blockOwnerDeletion=true) on every full-ownership applied resource, so cascade-delete (for example helm uninstall) GCs the rendered objects. Resources that disappear from the rendered set across reconciliations are pruned. The applier respects the haproxy-haptic.org/ownership: partial annotation: when present on a rendered resource the Server-Side Apply (SSA) payload omits the managed-by label and the OwnerReference, the resource is excluded from the orphan-cleanup set, and the annotation itself is stripped before apply — useful for jointly owned objects on which HAPTIC only contributes a subset of fields (Server-Side Apply's per-list-map-entry merge keeps each owner's contribution intact).
Templates have full access to the same engine context as haproxyConfig — resources, filters, templateSnippets, fileRegistry, extraContext, and the per-render shared cache — so a k8sResources template can render extension points (render_glob patterns) and read shared state populated by the main config template.
k8sResources:
edge-service:
template: |
apiVersion: v1
kind: Service
metadata:
name: edge
namespace: {{ extraContext["controllerNamespace"] }}
spec:
type: LoadBalancer
selector:
app.kubernetes.io/component: loadbalancer
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
---
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: edge-default
namespace: {{ extraContext["controllerNamespace"] }}
labels:
kubernetes.io/service-name: edge
addressType: IPv4
endpoints:
- addresses: ["10.0.0.1"]
ports:
- name: http
port: 80
protocol: TCP
Use this when the resource shape derives from observed cluster state (Ingresses, Gateways, Endpoints, …); use the chart's own static templates/*.yaml for fixed install-time wiring (RBAC, the internal agent Service, etc.). The chart's charts/haptic/charts/base/library.yaml ships a canonical example: the haproxy-service entry that renders the user-facing HAProxy LoadBalancer Service from listener state.
postProcessing (all template entries)¶
Every template-bearing entry — haproxyConfig and each entry under maps, files, sslCertificates, and k8sResources — accepts an optional postProcessing list that transforms the rendered output before it's used. Processors run sequentially.
| Field | Type | Required | Default |
|---|---|---|---|
type |
string (regex_replace / template) |
Yes | — |
params |
map[string]string |
Yes | — |
Params per type:
| Type | Params |
|---|---|
regex_replace |
pattern (regular expression), replace (replacement string) — applied line by line |
template |
source (a Scriggo template; the rendered output is available as the input variable) |
haproxyConfig:
template: |
...
postProcessing:
- type: regex_replace
params:
pattern: '[ \t]+$'
replace: ""
- type: template
params:
source: "{{ replace(input, \"__REGION__\", \"eu-west-1\") }}"
See Templating — Post-Processing for a runnable example.
templatingSettings¶
Template rendering configuration and custom variables.
| Field | Type | Required | Default |
|---|---|---|---|
extraContext |
object (any JSON value) | No | — |
engine |
string (scriggo) |
No | scriggo (the only valid value) |
Custom variables are exposed to templates as the extraContext map. Read a key with extraContext["key"], or extraContext | dig("key") | fallback(default) when it may be unset:
{% if extraContext["environment"] == "production" %}
timeout client {{ extraContext | dig("customTimeout") | fallback("300") }}s
{% end %}
See Templating — Custom Template Variables for detailed examples.
validationTests¶
Embedded validation tests (optional; run by the pre-rollout validation Job, the validate CLI, and the controller itself on config load and on every live config change). Across a merged set, a test name may be defined by only one object — a duplicate is an error naming both — while the reserved _global baseline accumulates across objects.
| Field | Type | Required | Default |
|---|---|---|---|
description |
string | No | — |
fixtures |
map[string][]object |
No | — (keys must name watchedResources entries, plus the reserved haproxy-pods key) |
assertions |
[]Assertion |
Yes | — |
httpResources |
[]object |
No | — (mocked responses for http.Fetch() calls) |
currentServers |
map[string]map[string]object |
No | — (backend → server → {address, port} of a previous deployment, exposed to templates as currentConfig.ServerIndex) |
currentConfig |
string | No | — (deprecated: a raw HAProxy config parsed down to the same server index as currentServers) |
currentFiles |
map[string]string |
No | — (filename → content of the general files currently deployed, exposed to templates as currentFiles) |
extraContext |
object | No | — (per-test overrides of templatingSettings.extraContext) |
minHAProxyVersion |
string | No | — (skip the test on older HAProxy) |
requires |
[]string |
No | — (strip the test when a named optional watched resource is unavailable) |
requiresFields |
[]string |
No | — (strip the test when a schema field path is absent) |
validationTests:
test-basic-ingress:
description: Validate basic ingress routing
fixtures:
ingresses:
- apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: test-ingress
namespace: default
spec:
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: test-service
port:
number: 80
assertions:
- type: haproxy_valid
description: Generated config must be valid
- type: contains
target: haproxy.cfg
pattern: "example.com"
description: Config must include host
See Validation Tests for the full test-framework reference — fixtures, assertion types, CLI usage, and the requires / requiresFields stripping semantics — and CRD & Validation Design for the design rationale.
validators¶
Pluggable validator sidecars consulted before rendered output is published or deployed (optional).
| Field | Type | Required | Default |
|---|---|---|---|
name |
string | Yes | — (RFC 1123 label, unique across the array) |
socketPath |
string | Yes | — (absolute path to a Unix domain socket inside the controller pod) |
files |
[]string |
Yes (at least one) | — (glob patterns matched against rendered file paths) |
dataFiles |
[]string |
No | — (glob patterns for files sent as data, never validated on their own) |
timeoutMs |
integer | No | 5000 (range 1–60000) |
maxConnections |
integer | No | 4 (range 1–32) |
Globs follow Go's path/filepath.Match rules and must use the same relative or absolute form as the rendered path: * and ? don't cross /, and ** isn't supported. Malformed patterns fail configuration validation.
dataFiles covers files the validator needs in order to check something else but must not check on its own. Every match is attached to every request to that validator, marked as data. A validator sidecar runs in the controller pod and can't read the HAProxy pod's filesystem, so a config that Includes a ruleset by path is only checkable if the ruleset's content travels with the request. A file matching both files and dataFiles is treated as data.
validators:
- name: spoa-hub
socketPath: /var/run/haptic-validators/spoa-hub.sock
files:
- "/etc/haproxy-spoa-hub/*.toml"
dataFiles:
- "/etc/haproxy/general/*.conf"
See Pluggable Validators for the wire protocol, sidecar wiring, and routing examples.
controller¶
Controller-level settings for leader election and config publishing.
| Field | Type | Required | Default |
|---|---|---|---|
leaderElection.enabled |
bool | No | true |
leaderElection.leaseName |
string | No | "" → haptic-leader (the Helm chart sets the release fullname) |
leaderElection.leaseDuration |
string | No | 30s |
leaderElection.renewDeadline |
string | No | 20s |
leaderElection.retryPeriod |
string | No | 5s |
Note
There is no reconciler-level debounce knob. The Reconciler fires immediately on every resource/HTTP event; batching is per-watcher (spec.watchedResources.<name>.debounceInterval, default 100ms) and reload throttling is the deployer's spec.dataplane.minDeploymentInterval.
Note
These are the controller's built-in defaults from pkg/core/config/defaults.go — deliberately 2x the values kube-controller-manager and kube-scheduler ship with (15s/10s/2s), so the leader rides out multi-second API-server or CPU starvation stalls without losing the lease. The Helm chart sets the same values; setting any of these fields on the CRD only matters if you need different values (for example faster crash-failover, or clusters with significant clock skew).
See High Availability for leader election details.
configPublishing¶
Controls how rendered configurations are stored in HAProxyCfg CRD resources.
| Field | Type | Required | Default |
|---|---|---|---|
compressionThreshold |
int64 | No | 1048576 (1 MiB). A value of 0 is treated as unset — the 1 MiB default applies (compression can't currently be disabled) |
When the rendered configuration exceeds the threshold, it's compressed with zstd and base64-encoded; the HAProxyCfg resource stores it with spec.compressed: true, reducing etcd storage and speeding up watch events for large configurations. To read a published config back in plaintext, use haptic config view — see Debugging.
logging¶
Log level configuration.
| Field | Type | Required | Default |
|---|---|---|---|
level |
string (TRACE, DEBUG, INFO, WARN, ERROR; case-insensitive) |
No | "" → the LOG_LEVEL environment variable → INFO |
dataplane¶
Connection and pacing settings for the agent in each HAProxy pod. The block keeps its name: it configures the endpoint the controller applies to, which is now the HAPTIC agent.
| Field | Type | Required | Default |
|---|---|---|---|
port |
integer (1–65535) | No | 5555 |
minDeploymentInterval |
string | No | 2s (the Helm chart ships 5s) |
driftPreventionInterval |
string | No | 60s |
deploymentTimeout |
string | No | 30s |
configPublishInterval |
string | No | 10s |
reloadVerificationTimeout |
string | No | 60s (the agent's ceiling, which is also its maximum) |
syncTimeout |
string | No | 2m |
mapsDir |
string | No | /etc/haproxy/maps |
sslCertsDir |
string | No | /etc/haproxy/certs (the Helm chart sets /etc/haproxy/ssl) |
generalStorageDir |
string | No | /etc/haproxy/general |
configFile |
string | No | /etc/haproxy/haproxy.cfg |
The three *Dir paths are used by the controller's local haproxy -c validation step as well as for rendering the paths the configuration references — they must match where the HAProxy pod mounts each directory. The Helm chart keeps them in sync by deriving both sides from a single set of chart values.
minDeploymentInterval and reloadVerificationTimeout also become agent flags whenever the chart deploys the HAProxy fleet. The agent rejects either above 60s and exits at startup, so the chart fails the render instead. For tuning guidance on the interval fields, see Performance — Deployment Pacing.
Status Subresource¶
The controller updates the status field with validation results:
| Field | Type | Description |
|---|---|---|
observedGeneration |
int64 | The .metadata.generation the status reflects |
lastValidated |
timestamp | Last successful validation |
validationStatus |
string | Valid, Invalid, or Unknown — the printer column shown by kubectl get htplcfg |
validationMessage |
string | Human-readable summary |
validationErrors |
[]string |
Populated when Invalid; each entry names the template and error context |
conditions |
[]Condition |
Standard metav1.Condition list. The controller writes exactly one type, Validated. |
The Validated condition carries its own observedGeneration, so kubectl wait --for=condition=Validated answers whether the controller has processed this generation, rather than whether some past generation validated. Its reasons are ValidationSucceeded, ConfigInvalid, HAProxyValidationFailed, and LoadGateFailed — the last meaning the fatal startup load gate rejected the config, so the pod is in CrashLoopBackOff rather than merely having a rejected live reload.
When a config is assembled from several objects (see libraryRefs), the same set-level result is stamped on every HAProxyTemplateConfig in the set, each with its own observedGeneration.
status:
observedGeneration: 1
lastValidated: "2025-01-27T10:00:00Z"
validationStatus: Valid
validationMessage: "All validation tests passed"
validationErrors:
- "haproxy.cfg: parse error at line 12: …" # only when Invalid
conditions:
- type: Validated
status: "True"
reason: ValidationSucceeded
observedGeneration: 1
lastTransitionTime: "2025-01-27T10:00:00Z"
HAProxyCfg deployment status¶
The controller publishes the rendered configuration as an HAProxyCfg resource and records what each HAProxy pod runs in status.deployedToPods[]:
| Field | Type | Description |
|---|---|---|
podName |
string | The HAProxy pod this entry describes |
podUID |
string | The pod incarnation the entry belongs to |
podRuntimeID |
string | The container execution epoch the entry belongs to |
checksum |
string | Checksum of the configuration applied to the pod. It equals spec.checksum once the pod has converged |
appliedPlanID |
string | The render plan the pod last accepted |
runningPlanID |
string | The render plan the pod's running HAProxy serves. It trails appliedPlanID while a reload is still pending |
mode |
string | How the plan was applied: runtime, file_only, reload, scheduled, noop, or rejected. Empty when the applier reports no mode |
reasons |
[]string |
Why the apply took that mode, most significant first, at most 8 entries; when more were recorded the last entry says how many were omitted |
lastError |
string | Error message from the most recent failed sync, cleared when a sync succeeds |
consecutiveErrors |
int | Number of consecutive sync failures, reset to 0 on success |
HAProxyTemplateLibrary¶
A second kind carrying template-library content only, referenced from a config's libraryRefs. It exists because templateSnippets and validationTests are ~94% of a full configuration's bulk, which puts a single object against etcd's per-object limit.
API Group: haproxy-haptic.org
API Version: v1alpha1
Kind: HAProxyTemplateLibrary
Short Name: htpllib
| Field | Type | Required | Description |
|---|---|---|---|
revision |
string | Yes | Identifies this content to the configs that reference it |
templateSnippets |
map | No | Same shape as the config's templateSnippets |
validationTests |
map | No | Same shape as validationTests |
maps |
map | No | Same shape as maps |
files |
map | No | Same shape as files |
sslCertificates |
map | No | Same shape as sslCertificates |
k8sResources |
map | No | Same shape as k8sResources |
templatingSettings |
object | No | Template-context defaults; the config merges last, so an operator always wins |
haproxyConfig |
object | No | Exactly one member of a merged set supplies it |
A library carries no podSelector, watchedResources, dataplane, validators, controller or logging — it can't redefine the controller's operational identity.
You choose the revision value; the controller only ever compares it against the reference and never derives one from the content. That's what lets kubectl edit change a snippet in place and take effect immediately — the content moves, the revision doesn't, so the reference still matches. A digest of the content is the convenient source for a generator, because it changes exactly when the content does.
apiVersion: haproxy-haptic.org/v1alpha1
kind: HAProxyTemplateLibrary
metadata:
name: haproxy-config-base
namespace: default
spec:
revision: "base-43dc4467f7e88090"
templateSnippets:
global-section:
template: |
global
daemon
Names must be unique across the merged set for validationTests. See ADR-0017 for the rationale, and haptic config view --input to print the merged result.
Command-line management¶
View Configurations¶
# List all configs
kubectl get haproxytemplateconfig
kubectl get htplcfg # Short name
# View specific config
kubectl get htplcfg haproxy-config -o yaml
# Watch for changes
kubectl get htplcfg -w
A Helm install creates exactly one of these — <configName>, built from your own
controller.config. Every enabled template library ships as a separate
HAProxyTemplateLibrary object named
<configName>-<library>, and the config's libraryRefs declares
which of them are pulled in and in what order: later entries win, and the
config's own inline content wins last. CRD_NAME on the controller Deployment
names that single config and nothing else.
Only <configName> is yours to edit. The library objects are chart output and
helm upgrade overwrites them; to change what a library emits, override the
snippet by name under controller.config.templateSnippets instead.
To see what the controller actually assembles from the whole set:
Validation status is reported on <configName> only — it represents the merged
set. Offline, haptic validate -f <file> accepts the flag repeatedly
and accepts multi-document files, so you can validate a whole rendered set:
helm template charts/haptic > all.yaml # validate keeps the config + library docs and ignores the rest
haptic validate -f all.yaml
haptic validate -f all.yaml --dump-merged # print the merged spec
Applying a single hand-written HAProxyTemplateConfig — without Helm — still
works exactly as before: point --crd-name at it and it's the whole config.
Validate before applying¶
# Validate local file
haptic validate -f haproxy-config.yaml
# Validate deployed config
kubectl get htplcfg -n haptic haproxy-config -o yaml > /tmp/haproxy-config.yaml
haptic validate -f /tmp/haproxy-config.yaml
Edit Configuration¶
# Interactive edit
kubectl edit htplcfg haproxy-config
# Apply from file
kubectl apply -f haproxy-config.yaml
# Patch specific fields
kubectl patch htplcfg haproxy-config --type=merge -p '
spec:
logging:
level: DEBUG
'
Validation¶
The CRD includes OpenAPI schema validation that checks:
- Required fields are present
- Field types are correct
- String lengths meet minimum/maximum requirements
- Integer values are within valid ranges
- Enum values match allowed options
Additional validation occurs when:
- Pre-rollout Helm hook - the chart's
pre-install/pre-upgradeJob runshaptic preflight, which renders the chart from your values and runs the embedded tests before any object is applied - Controller startup - the load gate runs the embedded tests before the controller serves; a failure crash-loops the new pod instead of replacing a working one
- Live config change - the same suite re-runs on every config change; a failure is refused and the last-good config keeps serving
- CLI command -
haptic validateruns tests locally
Best practices¶
Security:
- Never include credentials in the CRD - use credentialsSecretRef
- Restrict RBAC access to HAProxyTemplateConfig resources
- Use separate namespaces for controller and configs in multi-tenant scenarios
Organization:
- One HAProxyTemplateConfig per controller instance
- Use descriptive names that indicate purpose or environment
- Label configs for filtering:
environment: production
Testing:
- Include validation tests for critical routing paths
- Test with realistic fixtures, not toy examples
- Run
haptic validatebefore applying changes - Use CI/CD to validate configs in pull requests
Templates:
- Use
templateSnippetsfor reusable logic - Keep
haproxyConfigtemplate focused on structure - Comment complex template logic
- Test templates with various resource combinations
See also¶
- Templating Guide — template syntax, loops, status patches
- Template Reference — context variables, functions,
pathResolver - Watching Resources — store types, indexing, selectors
- Validation Tests — writing and running embedded tests
- CRD & Validation Design — rationale behind the CRD shape and validation layers
- Getting Started — installation walkthrough