Skip to content

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.

apiVersion: haproxy-haptic.org/v1alpha1
kind: HAProxyTemplateConfig
metadata:
  name: haproxy-config
  namespace: default
spec:
  credentialsSecretRef:
    name: haproxy-credentials

  podSelector:
    matchLabels:
      app.kubernetes.io/component: loadbalancer

  watchedResources:
    ingresses:
      apiVersion: networking.k8s.io/v1
      resources: ingresses
      indexBy:
        - metadata.namespace
        - metadata.name

  haproxyConfig:
    template: |
      global
          daemon
      defaults
          timeout connect 5s
      frontend http
          bind *:80

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
credentialsSecretRef:
  name: haproxy-credentials

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)
podSelector:
  matchLabels:
    app.kubernetes.io/component: loadbalancer

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:

  • apiVersion and apiVersions are 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 / validationTests entry whose requires names 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.

See Templating — Map Files.

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 haproxyConfigresources, 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)
templatingSettings:
  extraContext:
    environment: production
    featureFlags:
      rateLimiting: true

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
controller:
  leaderElection:
    enabled: true
    leaseDuration: 30s
    renewDeadline: 20s
    retryPeriod: 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)
controller:
  configPublishing:
    compressionThreshold: 1048576

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
logging:
  level: DEBUG

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
dataplane:
  port: 5555
  minDeploymentInterval: 2s
  driftPreventionInterval: 60s

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
kubectl get haproxytemplatelibrary   # or: kubectl get htpllib

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:

haptic config view --input --namespace haptic

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:

  1. Pre-rollout Helm hook - the chart's pre-install/pre-upgrade Job runs haptic preflight, which renders the chart from your values and runs the embedded tests before any object is applied
  2. 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
  3. Live config change - the same suite re-runs on every config change; a failure is refused and the last-good config keeps serving
  4. CLI command - haptic validate runs 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 validate before applying changes
  • Use CI/CD to validate configs in pull requests

Templates:

  • Use templateSnippets for reusable logic
  • Keep haproxyConfig template focused on structure
  • Comment complex template logic
  • Test templates with various resource combinations

See also

Found a problem on this page? Report it or edit the page with the pencil icon above the title.