Unreleased documentation. Choose your installed release in the version menu. Features described here may be absent from that release.
Validation test reference¶
Look up fixture fields, test requirements, and assertion types. For a first test and commands to run it, use Test your templates.
Define tests under controller.config.validationTests in Helm values or
spec.validationTests in an HAProxyTemplateConfig or HAProxyTemplateLibrary.
Test structure¶
Each test consists of:
| Component | Description |
|---|---|
| Name | Unique identifier (kebab-case, for example test-ingress-tls-routing) |
| Description | What the test verifies |
| Fixtures | Simulated Kubernetes resources |
| Assertions | Checks on rendered output |
HTTP fixtures (httpResources) |
Optional — mocked responses for http.Fetch() URLs (see HTTP Fixtures) |
Min HAProxy version (minHAProxyVersion) |
Optional — skip the test unless the HAProxy version under test is at least this (for version-gated features) |
Extra context (extraContext) |
Optional — per-test values deep-merged into the global templatingSettings.extraContext: nested maps merge key by key with per-test leaves winning, so overriding one key keeps its siblings. Pin every value your assertions depend on — a sibling you leave unset keeps its deployment-configured value. To pin an exact key set instead of merging, give the nested map __replace__: true: it replaces the deployment's map at that key wholesale, and the sentinel is stripped from the result |
Current servers (currentServers) |
Optional — the servers a previous deployment had, keyed by backend and server name, exposed to templates as currentConfig.ServerIndex; use it to exercise slot-preservation logic (see Current servers) |
Current config (currentConfig) |
Deprecated — a raw haproxy.cfg the runner parses down to the same server index. Use currentServers |
Current files (currentFiles) |
Optional — filename → content of the general files already deployed, exposed to templates as currentFiles; use it for templates that read their own prior output, such as self-rotating TLS session-ticket keys |
Requires (requires / requiresFields) |
Optional — strip the test when a watched resource or schema field is unavailable (see Conditional Tests) |
Fixtures¶
Fixtures simulate Kubernetes resources:
fixtures:
services:
- apiVersion: v1
kind: Service
metadata:
name: api
namespace: production
spec:
ports:
- port: 80
ingresses:
- apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: main
namespace: production
spec:
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api
port:
number: 80
HTTP fixtures¶
Mock HTTP responses for templates using http.Fetch():
httpResources:
- url: "http://blocklist.example.com/list.txt"
content: |
blocked-value-1
blocked-value-2
Templates calling http.Fetch() for unmocked URLs fail with an error. Define shared HTTP fixtures in the _global test to make them available to all tests.
Current servers¶
Use currentServers to simulate a previous deployment. Key entries by backend
name, then server name. Templates read this data through currentConfig.ServerIndex.
For example, test that existing pods retain their server names during a rollout:
currentServers:
default_api_svc_api_80:
api-pod-1: {address: 10.0.0.1, port: 8080}
api-pod-2: {address: 10.0.0.2, port: 8080}
Without currentServers, currentConfig is nil — the first-deployment case.
The deprecated currentConfig fixture field accepts a raw haproxy.cfg and
extracts its server index. Use currentServers for new tests. Setting both fields
fails the test.
Fixture keys¶
Fixture keys name watchedResources entries, with one reserved exception: haproxy-pods populates the auto-injected HAProxy pod store that templates read as controller.haproxy_pods. Its entries default to apiVersion: v1 / kind: Pod and are indexed by namespace and name. Any other key fails the test with resource type "<key>" in fixtures not found in watched resources.
The reserved _global entry¶
Put shared fixtures, httpResources, and extraContext in an entry named
_global. They apply to every test; assertions on _global don't run. Multiple
libraries can contribute to this entry. All other test names must be unique
across the merged configuration.
Conditional Tests (requires and requiresFields)¶
requires lists watchedResources keys the test depends on. When an optional
watched resource named there is unavailable (no candidate API version served
by the cluster), the test is stripped from the effective configuration at load
time — the same mechanism templateSnippets use.
requiresFields goes one level deeper: a list of schema field paths in the
form <watchedResourceKey>.<field.path>:
validationTests:
test-httproute-cors-filter:
requires: [httproutes]
requiresFields: [httproutes.spec.rules.filters.cors]
# ...
HAPTIC skips the test if any listed field is missing from the resource's schema.
This lets a library support older CRD versions that lack a feature. The first
path segment must name a watchedResources entry. Array levels are implicit:
httproutes.spec.rules.filters.cors addresses the CORS field inside each rule's
filters. Inspect skipped tests at /debug/vars/effectiveConfigResolution.
Assertion types¶
Assertion Targets¶
The contains, not_contains, match_count, equals, and match_order assertion types share a target field selecting which rendered output to check:
| Target | What's checked |
|---|---|
haproxy.cfg (or empty) |
The rendered main HAProxy configuration |
map:<name> |
A rendered map file. <name> matches against either the full path or the basename |
file:<name> |
A rendered general file (error pages, etc.), matched by filename |
cert:<name> |
A rendered SSL certificate, matched by basename |
crt-list:<name> |
A rendered crt-list file (registered by a template via fileRegistry.Register("crt-list", …)), matched by basename |
k8s:<template-name> |
The rendered YAML of a spec.k8sResources template (potentially multi-doc with ---), so you can assert on emitted Kubernetes resources |
status:<ns>/<name>:<phase> |
The JSON status payload a statusPatch() call emitted for resource <ns>/<name> in the given pipeline phase (rendered, deployed, renderFailed, or deployFailed) — the way to test status-patch templates |
events |
The Kubernetes Events the templates recorded via recordEvent(), one per line as <Type> <Reason> <apiVersion> <Kind> <ns>/<name>: <message> |
rendering_error |
The simplified render error string, populated only when the render itself failed. Use this on negative tests where you expect rendering to be rejected |
Unknown targets fall back to haproxy.cfg silently
Typos in target: won't error — they'll just match the wrong content. Sanity-check via --dump-rendered if an assertion behaves unexpectedly.
haproxy_valid¶
Validates HAProxy configuration syntax using the HAProxy binary:
Include this assertion when the test expects valid HAProxy output. Tests that expect a render error instead assert that error; they can't also assert valid output.
contains¶
Verifies target content matches a regex pattern:
- type: contains
target: haproxy.cfg
pattern: "backend api-production"
description: Must create backend for API service
not_contains¶
Verifies target content doesn't match a pattern:
- type: not_contains
target: haproxy.cfg
pattern: "ssl-verify none"
description: Must not disable SSL verification
equals¶
Checks entire content matches exactly:
- type: equals
target: map:hostnames.map
expected: |
api.example.com backend-api
www.example.com backend-web
description: Hostname map must match exactly
Use for small, deterministic files. Not recommended for large configs.
jsonpath¶
Evaluates a JSONPath expression against the template rendering context and compares the single result to expected. JSONPath reads plain values from the context — it can't invoke the store methods templates use (resources.services.List()), so it fits scalar context values (for example a spec.templatingSettings.extraContext key, which is injected into the context by name):
- type: jsonpath
jsonpath: "{.environment}" # set via spec.templatingSettings.extraContext.environment
expected: "production"
description: extraContext.environment is wired through
To assert on watched resources or rendered output, use contains, match_count, or equals against haproxy.cfg (or a map: / file: target) instead.
match_count¶
Asserts that a regex pattern matches an exact number of times in the target. Useful for catching duplicate or missing entries:
- type: match_count
target: haproxy.cfg
pattern: "(?m)^backend "
expected: "3" # string — parsed as integer
description: Exactly 3 backends must be generated
match_order¶
Asserts that multiple patterns appear in the target in the listed order. Critical for HAProxy first-match-wins constructs (Gateway API route precedence, ordering of ACLs):
- type: match_order
target: map:path-prefix.map
patterns:
- "^/api/v2/users" # must come before /api/v2
- "^/api/v2" # must come before /api
- "^/api"
description: Path map entries must be sorted most-specific-first
deterministic¶
Renders the templates a second time with the same inputs and asserts the output is byte-for-byte identical. Catches unstable map ordering, time-dependent values, and other sources of non-determinism:
The check covers haproxy.cfg and every auxiliary file the template produced; no target or pattern is needed.