Templates (.tpl)
Files ending in .tpl under workspace/jobs/<job>/ are rendered at deploy staging time with Go text/template. Output drops the .tpl suffix (e.g. config.json.tpl → config.json).
Related: deploy.md · KV namespaces · cli/hooks.md
When rendering runs
During maand deploy, after job files are copied to tmp/workers/<ip>/jobs/<job>/ and before rsync to the worker. Each active allocation gets its own render pass (per-worker KV and version context).
Template data (dot context)
| Field | Type | Example |
|---|---|---|
.AllocationID |
string | Stable allocation UUID |
.Job |
string | Job name |
.CurrentVersion |
string | Running version on this allocation |
.NewVersion |
string | Target version from build |
.WorkerIP |
string | Worker host |
.WorkerID |
string | Worker UUID |
.Labels |
[]string |
Worker labels |
.BucketPath |
string | /opt/worker/<bucket_id> on worker |
.JobPath |
string | /opt/worker/<bucket_id>/jobs/<job> |
Equivalent KV:
{{ getJobWorker "version" }}
{{ get "maand/job/api/worker/10.0.0.1" "version" }}
Template functions
| Function | Usage | Notes |
|---|---|---|
get |
{{ get "vars/job/api" "cluster_name" }} |
Namespace must be allowed; panics if key missing |
getOptional |
{{ getOptional "maand/job/clickhouse" "workers" }} |
Same namespace rules; returns "" if key or namespace entry missing (still panics on disallowed namespace) |
getJob |
{{ getJob "workers" }} or {{ getJob "api" "workers" }} |
Shorthand for maand/job/<job>; one argument uses the rendering job |
getJobOptional |
{{ getJobOptional "rollout_order" }} |
Like getJob but returns "" when missing |
getJobWorker |
{{ getJobWorker "version" }} |
Shorthand for maand/job/<job>/worker/<.WorkerIP> on the current allocation |
getJobWorkerOptional |
{{ getJobWorkerOptional "peer_workers" }} |
Like getJobWorker but returns "" when missing |
getWorker |
{{ getWorker "hostname" }} |
Shorthand for maand/worker/<.WorkerIP> |
getWorkerOptional |
{{ getWorkerOptional "labels" }} |
Like getWorker but returns "" when missing |
getSecret |
{{ getSecret "db_password" }} |
Shorthand for secrets/job/<job>; panics if missing |
keys |
{{ range keys "vars/job/api" }}...{{ end }} |
List keys in a namespace |
split |
{{ split "a,b" "," }} |
String → slice |
trim |
{{ trim " host " }} |
strings.TrimSpace |
join |
{{ join .Labels "," }} |
Slice → string |
upper / lower |
{{ upper "hello" }} |
Case conversion |
add / sub / mul / div |
{{ add 1 2 }} |
Integer math |
min / max |
{{ min 128 (max 4 (div $memMB 64)) }} |
Integer bounds |
int |
{{ int (get "vars/job/postgres" "memory") }} |
Parse string or pass through int |
scrapeConfigs |
{{ scrapeConfigs }} |
Expands catalog scrape jobs at deploy render (live allocations + ports) |
ruleFiles |
{{ ruleFiles }} |
Lists assembled alert rule paths at deploy render (rules/<job>/<file>.yaml) |
Missing keys or disallowed namespaces panic at render time (deploy fails for that job), except getOptional which returns an empty string.
Deploy .tpl only: get, getOptional, getJob, getJobOptional, getJobWorker, getJobWorkerOptional, getWorker, getWorkerOptional, getSecret, scrapeConfigs, and ruleFiles are available on job templates at staging time.
_prometheus/scrape.yaml.tpl (build time) uses a smaller func map: get, getJob, getJobOptional, getSecret, keys — no getOptional, no getJobWorker, no getWorker, no scrapeConfigs, no .WorkerIP. See prometheus.md.
Go text/template built-ins are also available: eq, ne, lt, le, gt, ge, printf, and, or, not, len, index, range, if, define, template.
Hook scripts receive the same allocation context via env — see hook-api.md (ALLOCATION_INDEX, CURRENT_VERSION, NEW_VERSION, …).
Example
workspace/jobs/api/config.json.tpl:
{
"cluster": "{{ get "vars/bucket/job/api" "cluster_name" }}",
"version": "{{ .NewVersion }}",
"target_version": "{{ getJobWorker "version" }}",
"worker": "{{ .WorkerIP }}",
"hostname": "{{ getWorker "hostname" }}",
"listen_port": "{{ get "maand/bucket" "api_http_port" }}"
}
Populate vars/bucket/job/api via vars.conf or bucket.jobs*.conf. Use vars/job/api for hook-written values from pre_deploy / post_build.
Memory sizing (postgres-style)
workspace/jobs/postgres/postgresql.conf.tpl:
{{ define "pgMemUnit" -}}
{{- $mb := int . -}}
{{- if ge $mb 1024 -}}
{{- div $mb 1024 -}}GB
{{- else -}}
{{- $mb -}}MB
{{- end -}}
{{- end }}
{{- $memMB := int (get "vars/job/postgres" "memory") -}}
{{- $sharedMB := div $memMB 4 -}}
{{- $cacheMB := div (mul $memMB 3) 4 -}}
{{- $maintMB := min 2048 (div $memMB 16) -}}
{{- $workMB := min 128 (max 4 (div $memMB 64)) -}}
shared_buffers = {{ template "pgMemUnit" $sharedMB }}
effective_cache_size = {{ template "pgMemUnit" $cacheMB }}
maintenance_work_mem = {{ $maintMB }}MB
work_mem = {{ $workMB }}MB
Use {{ get (printf "maand/worker/%s" .WorkerIP) "postgres_allocation_index" }} for per-worker metadata from build.
Prometheus scrape configs
Jobs with _prometheus/scrape.yaml or scrape.yaml.tpl store unexpanded configs in KV at build. Template scrape files are rendered at build with job-level context (.Job, get, getSecret — not scrapeConfigs or .WorkerIP). {{ scrapeConfigs }} expands maand:port/* placeholders when the prometheus job is staged.
workspace/jobs/prometheus/prometheus.yml.tpl:
global:
scrape_interval: 15s
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ['127.0.0.1:9090']
{{ scrapeConfigs }}
See prometheus.md for _prometheus/ layout, alerts, runbooks, and dashboards.
Secrets in templates
Use getSecret for values written by hooks (put_job_secret). Do not commit secrets in the workspace.
password = {{ getSecret "db_password" }}
Common errors
| Symptom | Fix |
|---|---|
| Template panic: namespace not available | Key is outside allowed namespaces for this job — see KV persistence |
| Template panic: key not found | Use getOptional when upstream job KV may be empty; otherwise run hook that writes KV before deploy, or add vars.conf / bucket.jobs*.conf |
| Stale value after hook | Ensure hook runs in pre_deploy (before stage) or value is in build-time KV |
Debugging: debugging-deploy.md.