Template Reference¶
Lookup tables for everything available inside a HAPTIC template: the context variables, every function and filter, pathResolver, and the status-patch functions. For a guided introduction with runnable examples, see the Templating Guide.
Context Variables¶
All templates have access to the following top-level variables:
| Variable | Type | Description |
|---|---|---|
resources |
map of stores | Kubernetes resources indexed per watchedResources config — entries are wrappers exposing .List() / .Fetch(keys...) / .GetSingle(keys...) |
controller |
map of stores | Controller-managed stores; currently only controller.haproxy_pods for the discovered HAProxy pod set |
pathResolver |
object | Resolves filenames to HAProxy paths — see pathResolver |
capabilities |
map (bool values) | HAProxy feature flags derived from the local HAProxy version, snake_case keys (e.g. capabilities.supports_crt_list). Use for {% if capabilities.supports_crt_list %}…{% end %} branches — a mistyped key is silently falsy, not an error. |
currentConfig |
parsed config (or nil) | The previously-deployed HAProxy configuration as a *parser.StructuredConfig. Nil on first deployment — guard with {% if !isNil(currentConfig) %}. Used for slot-preserving updates. |
dataplane |
config.Dataplane block |
The CRD's spec.dataplane block — port, timeouts, paths |
shared |
*SharedContext |
Thread-safe compute-once cache for expensive computations (shared.ComputeIfAbsent(key, factory) + shared.Get(key); no Set — prevents racy check-then-act patterns) |
templateSnippets |
list | Names of all available template snippets — useful for dynamic render_glob patterns |
runtimeEnvironment |
object | Runtime info exposed by the controller (e.g. runtimeEnvironment.GOMAXPROCS) |
fileRegistry |
object | Lets templates dynamically register auxiliary files at render time via fileRegistry.Register("file"/"cert"/"map"/"crt-list", filename, content); returns the resolved path. Used by the SSL, haproxytech, and haproxy-ingress libraries to materialise CA bundles, client certs, and SSL crt-lists from Secrets. |
http |
object | HTTP fetcher for http.Fetch("https://example.com/...") — see Watching Resources — HTTP Resources for the auto-registration and refresh mechanism |
extraContext |
map | The full templatingSettings.extraContext map. Read a key with extraContext.key or extraContext["key"] — see Custom Template Variables. |
Note: the controller doesn't inject a haproxyVersion variable on its own. The Helm chart populates templatingSettings.extraContext.haproxyVersion from its haproxyVersion value, so chart-deployed templates read it as {{ extraContext.haproxyVersion }}. If you bypass the chart, set the value yourself in templatingSettings.extraContext.haproxyVersion. For feature checks prefer capabilities.* flags, which the controller derives from the local HAProxy probe.
Functions and Filters¶
Every entry below is callable in two equivalent styles: as a plain function (fn(x, args...)) or via the pipe operator (x | fn(args...)), which passes the left-hand value as the first argument. The pipe requires parentheses on the right side — {{ value | toLower() }} works, {{ value | toLower }} is a parse error. The one exception is len, a language builtin: call len(x) directly.
| Function | Purpose | Example |
|---|---|---|
fallback(value, default) |
Return default if value is nil. Empty strings and zeroes pass through — only dig() on optional typed fields normalises zero values to nil first |
fallback(svc.port.number, 80) |
dig(obj, "k1", "k2", ...) |
Walk a nested map / typed struct without nil-checking each level (navigates JSON tags on typed structs) | dig(ing, "metadata", "annotations") |
toSlice(v) |
Coerce any to []any (safe to range over even if nil) |
for _, r := range toSlice(ing.spec.rules) |
to_str_map(v) |
Normalise any string-keyed map (map[string]string from typegen, map[string]any from the untyped store path) to map[string]string — use on labels / matchLabels / annotations |
for k, v := range route.Metadata.Labels \| to_str_map() |
shard_slice(items, idx, n) |
Type-preserving split of a slice into n shards, returning shard idx — input element type is kept |
shard_slice(gateways, i, totalShards) |
tostring(v), toint(v), tofloat(v) |
Type conversions from any |
port = toint(annotation) |
len(v) |
Length of slice / map / string (language builtin — not pipeable) | len(ing.spec.rules) |
keys(m) |
Sorted keys of a map | for _, k := range keys(annotations) |
merge(a, b) |
New map combining a and b (b wins on conflict) |
merge(defaults, overrides) |
toLower(s) / toUpper(s) |
Case conversion | host = toLower(rule.host) |
replace(s, old, new), split(s, sep), join(slice, sep), strip(s), trim(s, cutset), hasPrefix(s, p), hasSuffix(s, p) |
String operations (strip trims whitespace; trim takes an explicit cutset) |
join(items, ", ") |
first_seen(prefix, keys...) |
Returns true only the first time the key tuple is seen — for deduplicating |
if first_seen("backend", svc.namespace, svc.name) |
sanitize_regex(s) |
Escape regex metacharacters in user input | sanitize_regex(annotation) |
semver_gte(version, "3.3") |
Compare a semver string (major.minor) against a target | if semver_gte(extraContext.haproxyVersion, "3.3") (the chart auto-populates extraContext.haproxyVersion; outside the chart, set it yourself via templatingSettings.extraContext.haproxyVersion — see Custom Template Variables) |
fail(msg) |
Abort rendering with an error message (surfaces in validation tests and webhooks) | fail("missing required annotation") |
b64decode(s) |
Decode base64 strings (Secret .data values) |
{{ secret.data.password \| b64decode() }} |
glob_match(items, pattern) |
Filter strings by glob pattern | {{ templateSnippets \| glob_match("backend-*") }} |
group_by(items, keyPath) |
Group items by dotted key path | {{ ingresses \| group_by("metadata.namespace") }} |
map_extract(items, keyPath) |
Pluck one field (dotted key path) from each item into a flat slice | {{ routes \| map_extract("routeId") }} |
indent(s, n) |
Indent lines by N spaces (first and blank lines excluded) | {{ render "snippet" \| indent(4) }} |
sort_by(items, criteria) |
Sort by JSONPath expressions — see sort_by modifiers | {{ routes \| sort_by(["$.priority:desc"]) }} |
debug(v, label) |
Output as JSON comment | {{ routes \| debug("routes") }} |
toJSON(v) |
Convert value to JSON string | {{ myMap \| toJSON() }} |
For complete coverage including crypto, encoding, and Scriggo built-ins (abs, min, max, sprintf, now(), etc.), see the Scriggo built-ins reference.
sort_by modifiers¶
sort_by criteria accept modifiers: :desc (descending), :exists (by field presence), | length (by length).
Example — route precedence sorting:
{% var sorted = sort_by(routes, []string{
"$.match.method:exists:desc",
"$.match.headers | length:desc",
"$.match.path.value | length:desc",
}) %}
pathResolver¶
pathResolver is available in every template. Its GetPath(filename, type) method returns the path that HAProxy should use to reference an auxiliary file (map, error file, certificate, crt-list). Use it instead of writing paths by hand so the controller and HAProxy agree on where files live.
Arguments: filename (string), type (one of "map", "file", "cert", "crt-list")
{# Map files — resolves to maps/host.map #}
use_backend %[req.hdr(host),lower,map({{ pathResolver.GetPath("host.map", "map") }})]
{# General files — resolves to general/504.http (chart default GeneralStorageDir basename) #}
errorfile 504 {{ pathResolver.GetPath("504.http", "file") }}
{# SSL certificates — resolves to ssl/example_com.pem (dots in cert/crt-list names are sanitized to _) #}
bind *:443 ssl crt {{ pathResolver.GetPath("example.com.pem", "cert") }}
{# crt-list files — resolves to general/cert-list.txt (CRTListDir defaults to GeneralStorageDir basename) #}
bind *:443 ssl crt-list {{ pathResolver.GetPath("cert-list.txt", "crt-list") }}
By default GetPath returns paths relative to HAProxy's default-path directive. The chart's base template library renders default-path origin {{ pathResolver.GetBaseDir() }} in the global section (e.g. default-path origin /etc/haproxy in production), which tells HAProxy to resolve relative paths against that explicit base directory. The controller writes maps, certs, and general files under the same base, so the relative paths line up at runtime; the validation pipeline rewrites just the default-path origin argument to a per-call temp directory so the same rendered config validates against a sandbox tree of identical shape. If you replace the base library, keep that directive (or render an absolute path yourself) — without it HAProxy resolves the relative paths from its own working directory and the file lookups fail.
Status-Patch Functions¶
Templates register status patches with statusPatch(); the controller applies them to the resource's /status subresource via Server-Side Apply after each reconciliation phase. The Templating Guide — Status Patches walks through the pattern with runnable examples.
statusPatch()¶
Registers a status patch for a Kubernetes resource with outcome-keyed variants. Each variant's value is the resource's .status content directly (e.g. conditions, loadBalancer) — the controller writes it under .status via SSA, so don't wrap it in another status key.
Parameters:
| Parameter | Type | Description |
|---|---|---|
namespace |
string |
Resource namespace |
name |
string |
Resource name |
apiVersion |
string |
Resource API version (e.g., networking.k8s.io/v1) |
kind |
string |
Resource kind (e.g., Ingress, Gateway) |
variants |
map[string]any |
Status payloads keyed by pipeline phase |
Variants:
| Key | Applied When |
|---|---|
rendered |
After successful template rendering (before deployment) |
deployed |
After successful HAProxy deployment |
renderFailed |
When a later rendering phase fails |
deployFailed |
When HAProxy deployment fails |
Templates render all variants upfront. The controller selects the appropriate variant based on the pipeline outcome.
condition()¶
Creates a metav1.Condition-compatible map.
Parameters: type, status, reason, message, observedGeneration, lastTransitionTime
{{ condition("Accepted", "True", "Accepted", "Resource is accepted", 1, "2024-01-01T00:00:00Z") | toJSON() }}
transitionTime()¶
Returns the correct lastTransitionTime for a condition: preserves the existing timestamp if the condition status hasn't changed, or returns the current time if it has changed or doesn't exist yet.
Parameters: existingConditions (the resource's existing conditions list — navigate to it yourself with dig(resource, "status", "conditions"), so the helper stays agnostic to where a given resource keeps its conditions), type, status.
For resources with nested condition arrays (e.g., Gateway API Route parents[]), navigate to the parent's conditions first:
{%- var parents = dig(resource, "status", "parents") | toSlice() %}
{{ transitionTime(dig(parents[parentIndex], "conditions"), "Accepted", "True") }}
Typed Access Internals¶
Background
This section documents how typed field names are generated — background for chart authors and contributors. Day-to-day usage, including the field-name table and the typed-vs-untyped decision rule, lives in Templating — Typed Resource Access.
The field-name rule lives in pkg/k8s/typegen/converter.go::GoFieldName: Go-PascalCase of the JSON tag, with no acronym dictionary. The no-acronym-dictionary choice is deliberate — there is no translation table to keep in sync, so apiVersion becomes ApiVersion (not the upstream-Go-style APIVersion), and only rune 0 of a tag is ever changed.
Worked example and regression canary. charts/haptic/charts/gateway/05-typed-access-smoke.yaml is the canonical single-snippet example — it emits one HAProxy comment per Gateway using gw.Metadata.Namespace / gw.Metadata.Name. Its companion test test-gateway-typed-access-smoke pins the wiring end-to-end (engine declarations + runtime bindings + actual render output) and acts as a regression canary for typed access generally.
See ADR-0010 — Typed Watched Resources for the design rationale and the alternatives considered.