Templating¶
Overview¶
HAPTIC uses Scriggo, a Go template engine, to generate HAProxy configurations from Kubernetes resources. The Helm chart ships with ready-to-use template libraries that cover standard Ingress and Gateway API use cases — you only need to write templates when you want to extend or replace that default behavior. Templates access watched Kubernetes resources, and the controller renders them whenever resources change, validates the output, and deploys it to HAProxy instances.
Templates are rendered automatically when any watched resource changes, during initial synchronization, or periodically for drift detection.
Hit Run live above to render the bundled Ingress example entirely in your browser. Edit the template on the left and watch haproxy.cfg update on the right — then switch tabs to see the maps, files, and status it also produces. Click any output line to jump to the template line that produced it, or Open in full playground to bring your changes into the full editor.
What you can template¶
| Template Type | Use When |
|---|---|
haproxyConfig |
Main HAProxy configuration (frontends, backends, global settings) |
maps |
HAProxy lookup tables for host/path routing decisions |
files |
Auxiliary files like custom error pages |
sslCertificates |
TLS certificate files assembled from Kubernetes Secrets |
HAProxy Configuration¶
The main haproxyConfig template generates the complete HAProxy configuration file. This one loops over the watched Ingresses and emits a backend for each — run it, then add or edit an Ingress on the right and watch the backends change.
Important
Whenever your HAProxy config references a map file, error file, certificate, or crt-list, use pathResolver.GetPath(filename, type) instead of a hard-coded path. The controller deploys these files to a configurable directory (set in spec.dataplane.mapsDir, sslCertsDir, generalStorageDir) and pathResolver knows where they live, so the path stays correct even if you reconfigure those directories.
Now that you've seen a config render, try editing one. This template has no loops — just a static frontend — so you can focus on the edit-and-run cycle.
Named and multiple defaults sections
The haproxyConfig template's rendered text is the HAProxy configuration — HAPTIC parses, validates, and deploys it as written, so any construct your HAProxy version accepts is available. That includes multiple named defaults sections: a defaults <name> block that later frontend, backend, or listen sections opt into with from <name>. HAPTIC's config comparator tracks each defaults section by name and creates, updates, or deletes them independently. The bundled base library ships a single unnamed defaults section; add named ones in your own template or snippets when a subset of sections needs different defaults.
Map files¶
Each maps entry renders one HAProxy lookup table. They're written to spec.dataplane.mapsDir (default /etc/haproxy/maps/) on the HAProxy pod. This template turns each Ingress host into a backend-name entry — switch to the maps tab to read the generated host.map.
General files¶
Auxiliary files like custom error pages. Written to spec.dataplane.generalStorageDir (default /etc/haproxy/general/). The errorfile directive points HAProxy at the rendered file — open the files tab to see 503.http.
Changing a general file reloads HAProxy, because the running worker holds the old content. That's wrong for a file HAProxy never reads — a config for a sidecar that watches the file itself. Set reloadOnPush: false on the entry and the controller writes the new content without the reload:
Registering the file at render time takes the same flag as a fourth argument:
SSL certificates¶
SSL/TLS certificate files are assembled from Kubernetes Secrets. Written to spec.dataplane.sslCertsDir (default /etc/haproxy/ssl/). This reads a TLS Secret and concatenates its certificate and key into one PEM — the certs tab shows the result.
Note
Certificate data in Secrets is base64-encoded. Use the b64decode filter to decode it.
Template snippets¶
Reusable template fragments are included via {{ render "snippet-name" }} — or {{ render_glob "pattern" }} to pull in every match at once. This config keeps each backend in its own snippet and stitches them into the config with render_glob, which renders matches in alphabetical order.
Include a single snippet in a template:
Include all snippets matching a glob pattern (rendered in alphabetical order):
Pass local variables to rendered snippets with inherit_context:
Post-processing¶
The haproxyConfig section supports a postProcessing list that transforms the rendered output before deployment. Post-processors run sequentially on the rendered configuration.
Available types:
| Type | Description |
|---|---|
regex_replace |
Line-by-line regex find/replace (pattern and replace params) |
template |
Scriggo template transformation with access to the rendered output via the input variable (source param) |
The config below renders a __REGION__ marker, then runs two post-processors in order: a template step rewrites the marker to a value, and a regex_replace step renames the header. The haproxy.cfg tab shows the final, post-processed output.
The template post-processor receives the fully rendered output as the input variable and has access to all standard Scriggo builtins (regexp, replace, len, tostring, etc.). Its output becomes the new rendered content.
Template syntax¶
For complete syntax reference, see the Scriggo documentation.
Control structures¶
{# Loops #}
{% for _, ingress := range resources.ingresses.List() %}
backend {{ ingress.metadata.name }}
{% end %}
{# Conditionals #}
{% if ingress.spec.tls != nil %}
bind *:443 ssl crt {{ pathResolver.GetPath(ingress.metadata.name + ".pem", "cert") }}
{% end %}
{# Variables #}
{% var service_name = path.backend.service.name %}
{% var port = fallback(path.backend.service.port.number, 80) %}
{# Comments #}
{# This is a comment #}
Reserved identifiers
Scriggo uses Go's grammar, so you can't use Go's keywords as variable names: break, case, chan, const, continue, default, defer, else, fallthrough, for, func, go, goto, if, import, interface, map, package, range, return, select, struct, switch, type, and var. Writing {% var type = … %} or {% var range = … %} produces a parse error. In template mode, these words are also reserved: and, contains, end, extends, in, macro, not, or, raw, render, render_glob, inherit_context, show, and using. This is why the nil-default helper is fallback, not default.
Helper functions¶
Beyond Scriggo's built-ins, HAPTIC adds helpers for the patterns ingress templates need: nil-safe navigation (dig, fallback, toSlice), string and map utilities, deduplication (first_seen), sorting (sort_by), and version gates (semver_gte). The Template Reference lists every function with its calling styles and an example each.
Try the helpers live in a pure Scriggo scratchpad — no config, no resources, just the template language and every function from the reference. Edit it and watch the output.
Ready for a challenge? Sort a list of backends heaviest-first, breaking ties by name. Edit the template to fix the sort — or peek at the solution.
Next, use first_seen to collapse duplicates — a real pattern when several
routes point at the same backend and you must emit each backend block exactly
once.
Path resolution¶
pathResolver.GetPath(filename, type) returns the path HAProxy should use to reference a rendered auxiliary file — type is one of "map", "file", "cert", or "crt-list". Use it instead of writing paths by hand so the controller and HAProxy agree on where files live. The Template Reference shows one example per file type and explains how the returned paths resolve against HAProxy's default-path directive (and what to keep if you replace the chart's base library).
Available template data¶
Context variables¶
Templates receive a set of top-level variables: resources (the watched-resource stores), pathResolver, capabilities (HAProxy feature flags), currentConfig (the servers the running config has), shared (a compute-once cache), extraContext, and more. The Template Reference documents each one. The one you'll use constantly is resources, covered next.
The resources variable¶
Templates access watched resources through the resources variable. Each store provides List(), Fetch(), and GetSingle() methods.
Note
The keys available under resources.* are determined by the watchedResources configuration. See Watching Resources to add resource types beyond the defaults.
{# List all resources #}
{% for _, ingress := range resources.ingresses.List() %}
{# Fetch by index keys (parameters match indexBy configuration) #}
{% for _, ingress := range resources.ingresses.Fetch("default", "my-ingress") %}
{# Get single resource or nil #}
{% var secret = resources.secrets.GetSingle("default", "my-secret") %}
Typed resource access¶
When a schema is loaded for a watched resource (live in production, or via --schema-dir offline), both the resources.<name> store wrapper and a top-level global named <name> return typed pointers instead of map[string]any. Field access goes through the strongly typed struct, so a misspelled field is a compile-time error rather than a silently-nil dig().
A typed field resolves by either its Go-PascalCase name or its lowercase JSON tag: gw.metadata.name and gw.Metadata.Name reach the same field, because the engine falls back to the JSON tag when the Go field name doesn't match. That's why the lowercase ingress.spec.rules / ingress.metadata.name examples elsewhere on this page are typed access too — not untyped dig(). The code blocks below use the PascalCase form to make the struct mapping explicit, but either spelling compiles.
{# Typed access — fields resolve at engine compile time #}
{%- for _, gw := range resources.gateways.List() %}
# {{ gw.Metadata.Namespace }}/{{ gw.Metadata.Name }}: {{ len(gw.Spec.Listeners) }} listeners
{%- end %}
{# Identical behaviour via the typed top-level global #}
{%- for _, gw := range gateways %}
# {{ gw.Metadata.Namespace }}/{{ gw.Metadata.Name }}
{%- end %}
Typed return types. With a schema loaded, every store method returns typed pointers:
| Call | Return type |
|---|---|
resources.<name>.List() |
[]*resources.<name>.T |
resources.<name>.Fetch(keys...) |
[]*resources.<name>.T |
resources.<name>.GetSingle(keys...) |
*resources.<name>.T (nil if not found) |
resources.<name>.APIVersion() |
string, with or without a schema — the group/version this resource is actually watched at — the candidate the effective config resolved to. Pass it to statusPatch() instead of hardcoding a version literal |
Without a schema (for example, haptic validate without --schema-dir), the same calls fall back to []any / map[string]any exactly as before. The chart's dig()-based snippets work in either mode.
<name>.T is a usable type expression. Macros, var declarations, type assertions, slice types, and type-switch case clauses all accept it:
{# Macro parameter typed against one kind #}
{% macro RenderGateway(gw *resources.gateways.T) %}
# gw.Metadata.Name is statically typed here
# {{ gw.Metadata.Name }}
{% end %}
{# Type-switch dispatch across multiple kinds (polymorphic `any` boundary) #}
{%- switch r := routeInfo["route"].(type) %}
{%- case *resources.httproutes.T %}
# r is statically *resources.httproutes.T inside this branch
# {{ r.Metadata.Name }}: {{ len(r.Spec.Rules) }} rules
{%- case *resources.grpcroutes.T %}
# {{ r.Metadata.Name }} (gRPC)
{%- case *resources.tlsroutes.T %}
# {{ r.Metadata.Name }} (TLS passthrough)
{%- end %}
{# Slice type for sharded parallel rendering #}
{% var shard []*resources.gateways.T = shard_slice(allGateways, i, n) %}
The type-switch case-clause form is the canonical pattern for chart code that crosses a polymorphic any boundary — the chart's gateway library uses it inside 60-frontend.yaml to dispatch on HTTPRoute / GRPCRoute / TLSRoute. shard_slice is type-preserving: when its input is a typed slice, the result is the same typed slice (not []any), so the downstream loop variable stays statically typed.
Nested shapes have names too, derived from the field path, so you can write the type of a value found inside a resource:
{% type Listener = resources.gateways.SpecListeners %}
{% type EP = resources.endpoints.Endpoints %}
Collection pipelines¶
When you're filtering, flattening, or deduplicating watched resources, chain type-preserving helpers instead of nesting loops around a map[string]bool{} you maintain yourself:
{%%
var addresses = resources.endpoints.List() |
flat_map(s => s.Endpoints) |
reject(e => e.TargetRef.Name == "") |
flat_map(e => e.Addresses) |
unique()
%%}
The helpers are map, filter, reject, flat_map, unique, unique_by, group_by and sort_by. Each preserves the element type, so e.TargetRef.Name still resolves after four stages — and a typo in a field name fails the config load rather than rendering an empty file.
x => expr¶
s => s.Endpoints is a function of one argument returning one expression. You don't write either type: the parameter is the element type of whatever is piped in, and the result is whatever the expression evaluates to. Both are still checked — reject(e => e.Adresses) fails the load with an unknown-field error, and a predicate that doesn't return bool is rejected at the same point.
The long form stays valid, and you need it when the body is more than one expression:
{%- var names = pods | map(func(p *resources.pods.T) string {
if p.Metadata.Labels["app"] != "" { return p.Metadata.Labels["app"] }
return p.Metadata.Name
}) %}
An arrow works anywhere a function is expected, not only in a pipeline — including your own helpers:
{%- var Where = func(ps []*resources.pods.T, pred func(*resources.pods.T) bool) []*resources.pods.T {
return ps | filter(pred)
} %}
{%- var ready = Where(pods, p => p.Status.Phase == "Running") %}
Predicates are closures, not strings. That's deliberate: dig-style string paths return nothing when a field name is wrong, and nothing errors. unique_by and group_by additionally accept an attribute path (unique_by("host")) for data that reaches you as any.
Macros compose with chains from either end, but not in the middle. A macro returns text, so it can consume a chain (… | map(p => p.Name) | Render()) or act as a stage closure (… | map(Label)) — it can't pass a collection onward. A shared helper that returns a collection is an exported var holding a function; it imports exactly like a macro and its return type is unrestricted:
{# in a snippet #}
{% var ReadyAddresses = func(svc string) []string {
return resources.endpoints.Fetch(svc) | flat_map(s => s.Endpoints) |
reject(e => e.TargetRef.Name == "") | flat_map(e => e.Addresses)
} %}
{# in another #}
{% import "util-endpoints" for ReadyAddresses %}
{%- for _, addr := range ReadyAddresses("default/api") %}
Four rules the compiler enforces:
- Put the pipe at the end of a line, not the start — Go's semicolon insertion ends the statement otherwise.
- Write chains inside
{%% %%}, not{{ }}; a{{ }}expression can't span lines. sort_byreturns a value and an error. As a pipe stage that's fine — the pipe keeps only the first result, sox | sort_by(…)assigns to one variable. A direct call can't sit in single-value context: writevar rows, sortErr = sort_by(items, criteria)and checksortErr.mapkeeps one output per input. Reach forflat_mapwhen the closure returns a slice you want flattened in.
Reach for a {%% %%} loop instead of a pipeline when the body has side effects — fail(), registering a file, recording an Event — or needs break.
Asking whether an optional field was set¶
A struct is falsy when every field is its zero value, so the question needs no helper:
{%- if ingress.Spec.DefaultBackend.Service %} {# set #}
{%- if not gateway.Spec.Tls.Frontend %} {# absent or empty #}
Use not / and / or rather than ! / && / || when an operand is a struct — the Go operators need a bool, these coerce any value. That works inside a pipeline predicate too: filter(o => not o.Spec.Tls.Frontend).
Prefer this to a dig() probe like dig(ingress, "spec", "defaultBackend") != nil: same answer, and the field path is checked when the config loads.
Absence and emptiness deliberately give the same answer, because that's the only distinction the typed shape carries: an optional object that the source omitted and one it supplied empty both arrive as the zero value.
Field name convention: Go-PascalCase of the JSON tag, with NO acronym preservation. This matters because chart authors are used to upstream Go-style names (APIVersion, IPBlock) — those don't apply here. (Where the JSON tag already has an uppercase acronym, like loadBalancerIP, the typed field keeps it — LoadBalancerIP — which happens to match upstream; only rune 0 is ever changed.)
| JSON tag (source YAML) | Typed field |
|---|---|
metadata |
Metadata |
spec |
Spec |
apiVersion |
ApiVersion |
tls |
Tls |
ingressClassName |
IngressClassName |
matchLabels |
MatchLabels |
clusterIP |
ClusterIP |
loadBalancerIP |
LoadBalancerIP |
kubernetes.io/foo |
Kubernetes_io_foo (non-letter/digit → _) |
Templates write gw.ApiVersion, not gw.APIVersion. Why the convention works this way — and the regression canary that pins it — is covered in Typed Access Internals.
Inside a typed scope (typed for-range, typed macro parameter, type-switch case branch) use direct field access — no dig(), no tostring(), no fallback() on already-typed primitives. Reach for dig() only at genuine polymorphic boundaries (a routeInfo["route"] switch entry, an any macro parameter, a shared.Get(...) return, a ConfigMap with no schema bundled, a listenerOwner that may be a Gateway or a ListenerSet, etc.). Mixed-shape chart code — some snippets typed, some not — is the expected adoption pattern, and dig() navigates typed structs by JSON tag, so a snippet ported one at a time keeps working without churning its callers.
Iterate an optional typed slice directly. An absent (nil) optional typed slice ranges zero times, so for _, r := range ingress.spec.rules is panic-free with no guard. Don't wrap a typed slice in fallback(x, []any{}): fallback returns any, which erases the element type and makes the following typed field access (such as r.host) fail to compile. When you need to branch on emptiness, test len(x.field) > 0 (as in the map-file example earlier on this page), not a dig(...) | toSlice() guard.
Optional fields normalise to nil through dig(). A typegen-produced struct field whose schema entry is not in the OpenAPI required list carries a json:"…,omitempty" tag; dig() returns nil when such a field's value is the type's zero value ("", 0, false, empty slice). The universal dig(obj, "field") | fallback(default) chart pattern therefore behaves identically across typed and untyped shapes — without the normalisation, an unpopulated optional string would return "", fallback() would skip, and downstream key composition would silently produce malformed strings. Required fields keep their zero values intact.
Schema source. Typed shapes are generated from each resource's OpenAPI v3 schema:
- Production: the controller fetches schemas live from the kube-apiserver — CRDs via their embedded
openAPIV3Schema, Kubernetes core resources via the apiserver's OpenAPI v3 endpoint. - Offline (
haptic validate/ chartvalidationTests/scripts/test-templates.sh): schemas come from a directory passed via--schema-dir(orHAPTIC_SCHEMA_DIRenv var). The directory accepts full CRD YAMLs (kubectl get crd X -o yamloutput) and bare OpenAPI v3spec.Schemafiles with anx-kubernetes-group-version-kindextension. Without--schema-dir, no resources receive typed support; templates that reach for typed access in that case fail at engine compile time with a clear "no schema for X" pointer back to--schema-dir.
This repo's tests/schemas/ bundles schemas for both the Gateway API CRDs / haptic CRDs and the Kubernetes built-ins the chart watches (Namespace, Service, Secret, EndpointSlice, Ingress). All built-ins are CRD-wrapped so the offline GVK resolver picks up the (apiVersion, plural) mapping — haptic validate --schema-dir tests/schemas therefore unlocks typed access for every chart-watched resource, not just the CRDs. The chart-test script auto-wires this directory; copy it into your own project's schema-dir if you reuse the bundled libraries. To refresh from a running cluster, run scripts/fetch-k8s-openapi-schemas.sh (queries kubectl get --raw '/openapi/v3/...', inlines $refs, emits CRD-wrapped YAML).
Index Configuration¶
The indexBy field on a watchedResources entry determines what parameters Fetch() expects — see Watching Resources — Indexing for index shapes, prefix scans, and the dot-escaping rule for label keys.
Custom template variables¶
Add custom variables via templatingSettings.extraContext:
Access in templates:
{% if extraContext.environment == "production" %}
http-response set-header X-Environment production
{% end %}
global
maxconn {{ extraContext.limits.maxConn }}
Common patterns¶
Reading a custom annotation¶
Custom annotations are the usual way to let application teams opt individual Ingresses into behavior your templates control, without a controller fork or a new release. Read the annotation off the resource and branch on its value.
The config below defines the haptic.example.com/balance annotation: when an Ingress carries it, its backend uses that load-balancing algorithm; otherwise it falls back to roundrobin. The shop Ingress sets leastconn; blog sets nothing. Run it, then edit either Ingress's annotation in the Resources panel and watch the balance line follow.
ingress.metadata.annotations is a typed map[string]string, so indexing an absent key returns "" — the algo != "" check covers both a missing annotation and an empty one. Pick an annotation prefix you own (here haptic.example.com/) so it can't collide with another controller's. The same read-and-branch pattern drives rate limits, header rewrites, custom ACLs — anything HAProxy can express. In the chart, place the snippet under a features-* or backend-directives-* extension point so the bundled libraries pick it up (see Template Libraries).
Servers named after their pods (avoid reloads)¶
Each endpoint becomes one server line named after its pod, so adding or removing a pod is an add or remove of a named server over the runtime API — no reload, and no pre-allocated slot pool (ADR-0011). Run this, then add a third endpoint and re-run to watch a new server line appear:
Benefit: A rolling update or scale event changes only the set of named servers at runtime; established connections to unaffected pods keep flowing.
The bundled libraries' BackendServers macro does exactly this — one server per endpoint, named after the pod and keyed by a stable guid — so a real backend needs no hand-written loop. See Reload-free updates.
Maximize Runtime API Usage
Keep server lines minimal — only address:port plus the pod name and its guid. Place all other options (check, proto h2, SSL settings) on the default-server directive:
backend my-backend
default-server check proto h2
server api-pod-1 10.0.0.1:8080 guid srv:my-backend:api-pod-1 # Pod: api-pod-1
server api-pod-2 10.0.0.2:8080 guid srv:my-backend:api-pod-2 # Pod: api-pod-2
HAProxy's runtime API can add and remove named servers and update their address and port without reloading. Options like check on individual server lines trigger reloads on any change, so they belong on default-server.
Cross-Resource Lookups¶
Use a field from one resource to query another. Each Ingress's backend service name drives a Fetch() into the matching EndpointSlices — run it, then edit the Ingress or the endpoints and watch the backend servers change:
The two indexBy entries above are what make the lookup work: ingresses is indexed by namespace + name, and endpoints is indexed by the kubernetes.io/service-name label so Fetch(svc) returns every EndpointSlice for that service (dots in label keys need escaping — see Watching Resources — Indexing).
Safe Iteration¶
With untyped map[string]any data (no schema loaded), wrap every field access in dig(...) | toSlice() so a missing field yields an empty range instead of a panic. With typed access, skip this — range optional typed slices directly (see Typed resource access). The second endpoint below has no addresses, so it's skipped rather than breaking the render:
Filtering with conditionals¶
Test a field before you use it to skip resources that lack it. Only the rule with an http section produces a backend line; the bare TCP host is filtered out:
Challenge: Add health checks¶
Put the loop-and-dig pattern to work:
Challenge: Default a missing port¶
Combine dig() with fallback() to supply a default when a field is absent:
Mutable variables¶
Accumulate values across nested loops with append, then emit the collected result. This flattens every endpoint address into one numbered server list:
Whitespace control¶
Add - inside a tag to trim adjacent whitespace: {%- strips whitespace before the tag, -%} strips whitespace after it.
{%- for _, item := range items %} {# Strip before #}
{% for _, item := range items -%} {# Strip after #}
{%- for _, item := range items -%} {# Strip both #}
The stripped loop below renders one clean line per environment. Delete a dash and re-run to see the blank lines it was removing:
Status patches¶
Templates can register status patches for Kubernetes resources using the statusPatch() function. The controller applies these patches to the /status subresource via Server-Side Apply (SSA) after each reconciliation phase.
This allows templates to report processing results back to resources (for example, setting Accepted and Programmed conditions on Gateways, or propagating LoadBalancer addresses to Ingress status) without the controller needing to understand any specific resource's status schema.
statusPatch()¶
Registers a status patch for a Kubernetes resource with outcome-keyed variants. Each variant's value is the resource's .status content directly (for example, conditions, loadBalancer) — the controller writes it under .status via SSA, so don't wrap it in another status key:
{% statusPatch(namespace, name, apiVersion, kind, map[string]any{
"deployed": map[string]any{
"conditions": []any{
condition("Accepted", "True", "Accepted", "Resource accepted", generation, transitionTime(dig(resource, "status", "conditions"), "Accepted", "True")),
},
},
"deployFailed": map[string]any{
"conditions": []any{
condition("Accepted", "True", "Accepted", "Resource accepted", generation, transitionTime(dig(resource, "status", "conditions"), "Accepted", "True")),
condition("Programmed", "False", "AddressNotAssigned", "No address available", generation, transitionTime(dig(resource, "status", "conditions"), "Programmed", "False")),
},
},
}) %}
Templates render all variants upfront; the controller selects the variant matching the pipeline outcome (rendered, deployed, renderFailed, or deployFailed). The Template Reference lists the parameters and when each variant applies.
condition()¶
Creates a metav1.Condition-compatible map. Run it — toJSON makes the returned map visible:
The parameter list is in the Template Reference.
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. The first argument is 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. Run the demo with a literal conditions list:
For resources with nested condition arrays (for example, Gateway API Route parents[]), navigate to the parent's conditions first — see the Template Reference for the pattern.
Using status patches in custom templates¶
In the chart, status patch snippets should use the status-patches-* extension point (priority 200). This renders after feature analysis but before complex config generation, ensuring patches are captured even if later rendering fails.
The embed below is a self-contained version that patches an Ingress with typed field access. Run it and open the status tab to see the .status.conditions HAPTIC would write back:
The built-in Ingress and Gateway API libraries already include status patch snippets. You only need custom status patches for resources not covered by the default libraries.
Complete example¶
Full ingress → service → endpoints chain with servers named after their pods, using typed access throughout. Press Run live, open the maps tab for the host map, and edit the resources to add or remove endpoints:
See also¶
- Template Reference — context variables, functions and filters,
pathResolver, status-patch parameters - Validation Tests — assert on rendered output before it reaches a cluster
- Watching Resources — stores, indexing, selectors, and debounce
- Template Engine Reference
- Scriggo Documentation
- HAProxy Configuration Manual