Blueprint Templating
Teabar blueprints support Go’s text/template syntax, enabling dynamic configuration based on variables, environment context, and resource references.
Template Basics
Templates use double curly braces {{ }} for expressions:
resources:
vms:
- name: "participant-{{ .Index }}"
count: "{{ .Variables.participant_count }}"
userData: |
#cloud-config
hostname: participant-{{ .Index }} When Templates Are Evaluated
Templates are evaluated at environment creation time, not when the blueprint is saved. This means:
- Variable values come from the creation request
- Resource references resolve to actual provisioned values
- Index values expand for multi-instance resources
Template Context
Inside templates, you have access to several context objects:
.Variables
Access user-provided variable values:
variables:
- name: participant_count
type: integer
default: 10
- name: domain_prefix
type: string
default: "workshop"
resources:
clusters:
- name: main
nodes:
workers: "{{ .Variables.participant_count }}"
dns:
- name: "{{ .Variables.domain_prefix }}"
type: A .Environment
Access environment metadata:
| Field | Description | Example |
|---|---|---|
.Environment.Name | Environment name | june-workshop |
.Environment.ID | Environment UUID | env-7f3a2b1c... |
.Environment.Domain | Assigned domain | june-workshop.acme.teabar.dev |
.Environment.Organization | Organization slug | acme |
.Environment.Project | Project slug | training |
.Environment.CreatedBy | Creator email | [email protected] |
resources:
helm:
- name: gitlab
values:
global:
hosts:
domain: "{{ .Environment.Domain }}"
externalUrl: "https://gitlab.{{ .Environment.Domain }}" .Resources
Reference other resources in the blueprint:
resources:
clusters:
- name: main
# ... cluster config
dns:
- name: "*.apps"
type: A
target: "{{ .Resources.clusters.main.ingress_ip }}" Warning
Resource references are resolved in dependency order. You can only reference resources that are provisioned before the current resource.
.Index
For resources with count, access the current index (0-based):
resources:
vms:
- name: "worker-{{ .Index }}"
count: 5
# Creates: worker-0, worker-1, worker-2, worker-3, worker-4 .Participant
In participant-scoped resources, access participant info:
access:
terminal:
shell:
env:
PARTICIPANT_NAME: "{{ .Participant.Name }}"
PARTICIPANT_EMAIL: "{{ .Participant.Email }}"
PARTICIPANT_ID: "{{ .Participant.ID }}" Built-in Functions
Teabar includes many built-in template functions:
String Functions
| Function | Description | Example |
|---|---|---|
lower | Lowercase string | {{ lower .Variables.name }} |
upper | Uppercase string | {{ upper .Variables.name }} |
title | Title case | {{ title .Variables.name }} |
trim | Trim whitespace | {{ trim .Variables.name }} |
trimPrefix | Remove prefix | {{ trimPrefix "env-" .name }} |
trimSuffix | Remove suffix | {{ trimSuffix ".yaml" .file }} |
replace | Replace substring | {{ replace "_" "-" .name }} |
contains | Check substring | {{ if contains "prod" .env }} |
hasPrefix | Check prefix | {{ if hasPrefix "dev" .name }} |
hasSuffix | Check suffix | {{ if hasSuffix ".yaml" .file }} |
resources:
vms:
- name: "{{ lower .Variables.team_name }}-worker-{{ .Index }}"
labels:
team: "{{ lower (replace " " "-" .Variables.team_name) }}" Quoting Functions
| Function | Description | Example |
|---|---|---|
quote | Double-quote string | {{ quote .Variables.name }} |
squote | Single-quote string | {{ squote .Variables.name }} |
resources:
manifests:
- template: |
apiVersion: v1
kind: ConfigMap
data:
config.json: {{ quote .Variables.config }} Default Values
| Function | Description | Example |
|---|---|---|
default | Provide default value | {{ default "10" .Variables.count }} |
required | Fail if empty | {{ required "name is required" .Variables.name }} |
coalesce | First non-empty value | {{ coalesce .Variables.a .Variables.b "default" }} |
resources:
clusters:
- name: main
nodes:
workers: "{{ default 3 .Variables.worker_count }}"
version: "{{ required "k8s_version is required" .Variables.k8s_version }}" Encoding Functions
| Function | Description | Example |
|---|---|---|
b64enc | Base64 encode | {{ b64enc .Variables.secret }} |
b64dec | Base64 decode | {{ b64dec .encoded }} |
toJson | Convert to JSON | {{ toJson .Variables.config }} |
toYaml | Convert to YAML | {{ toYaml .Variables.values }} |
fromJson | Parse JSON string | {{ fromJson .json_string }} |
fromYaml | Parse YAML string | {{ fromYaml .yaml_string }} |
resources:
secrets:
- name: api-credentials
data:
token: "{{ b64enc .Variables.api_token }}"
config: "{{ b64enc (toJson .Variables.config) }}" Math Functions
| Function | Description | Example |
|---|---|---|
add | Addition | {{ add .Index 1 }} |
sub | Subtraction | {{ sub .Variables.count 1 }} |
mul | Multiplication | {{ mul .Variables.count 2 }} |
div | Division | {{ div .Variables.total 2 }} |
mod | Modulo | {{ mod .Index 2 }} |
max | Maximum | {{ max .Variables.a .Variables.b }} |
min | Minimum | {{ min .Variables.a .Variables.b }} |
resources:
vms:
- name: "worker-{{ add .Index 1 }}" # 1-based numbering
count: "{{ .Variables.participant_count }}"
clusters:
- name: main
nodes:
# At least 1 worker, max 10
workers: "{{ min 10 (max 1 .Variables.worker_count) }}" Random Functions
| Function | Description | Example |
|---|---|---|
randAlphaNum | Random alphanumeric | {{ randAlphaNum 16 }} |
randAlpha | Random letters | {{ randAlpha 8 }} |
randNumeric | Random numbers | {{ randNumeric 6 }} |
randAscii | Random ASCII | {{ randAscii 20 }} |
uuidv4 | Generate UUID | {{ uuidv4 }} |
resources:
secrets:
- name: generated-passwords
data:
admin_password: "{{ randAlphaNum 24 }}"
api_key: "{{ randAlphaNum 32 }}"
session_id: "{{ uuidv4 }}" Note
Random functions generate new values each time the template is rendered. For consistent values across restarts, use the
secrets resource type with type: generated.Date/Time Functions
| Function | Description | Example |
|---|---|---|
now | Current time | {{ now }} |
date | Format time | {{ now \| date "2006-01-02" }} |
dateModify | Modify time | {{ now \| dateModify "+24h" }} |
toDate | Parse date string | {{ toDate "2006-01-02" "2024-06-15" }} |
resources:
manifests:
- template: |
apiVersion: v1
kind: ConfigMap
metadata:
annotations:
created: "{{ now | date "2006-01-02T15:04:05Z" }}"
expires: "{{ now | dateModify "+72h" | date "2006-01-02T15:04:05Z" }}" List Functions
| Function | Description | Example |
|---|---|---|
list | Create list | {{ list "a" "b" "c" }} |
first | First element | {{ first .list }} |
last | Last element | {{ last .list }} |
rest | All but first | {{ rest .list }} |
initial | All but last | {{ initial .list }} |
append | Add to list | {{ append .list "new" }} |
prepend | Add to front | {{ prepend .list "first" }} |
concat | Join lists | {{ concat .list1 .list2 }} |
join | Join with separator | {{ join "," .list }} |
sortAlpha | Sort strings | {{ sortAlpha .list }} |
uniq | Remove duplicates | {{ uniq .list }} |
has | Check if contains | {{ if has "admin" .roles }} |
resources:
helm:
- name: app
values:
admins: {{ toJson .Variables.admin_emails }}
features: {{ toJson (concat .Variables.base_features .Variables.extra_features) }} Control Flow
Conditionals
resources:
clusters:
- name: main
{{- if gt .Variables.participant_count 20 }}
nodes:
workers: 5
{{- else }}
nodes:
workers: 3
{{- end }}
helm:
{{- if .Variables.enable_monitoring }}
- name: prometheus
chart: prometheus-community/prometheus
{{- end }} Comparison Operators
| Operator | Description | Example |
|---|---|---|
eq | Equal | {{ if eq .val "test" }} |
ne | Not equal | {{ if ne .val "" }} |
lt | Less than | {{ if lt .count 10 }} |
le | Less or equal | {{ if le .count 10 }} |
gt | Greater than | {{ if gt .count 5 }} |
ge | Greater or equal | {{ if ge .count 5 }} |
and | Logical AND | {{ if and .a .b }} |
or | Logical OR | {{ if or .a .b }} |
not | Logical NOT | {{ if not .disabled }} |
resources:
clusters:
- name: main
{{- if and (ge .Variables.participant_count 10) .Variables.high_availability }}
nodes:
controlPlane: 3
workers: 5
{{- else }}
nodes:
controlPlane: 1
workers: "{{ max 2 .Variables.participant_count }}"
{{- end }} Loops
Use range to iterate:
# Loop over a list
resources:
dns:
{{- range $service := list "gitlab" "argocd" "grafana" }}
- name: "{{ $service }}"
type: CNAME
target: ingress
{{- end }}
# Loop with index
vms:
{{- range $i := until .Variables.participant_count }}
- name: "participant-{{ $i }}"
image: ubuntu-22.04
{{- end }} Whitespace Control
Use - to trim whitespace:
# Without whitespace control (creates empty lines)
{{if .condition}}
content
{{end}}
# With whitespace control (no empty lines)
{{- if .condition }}
content
{{- end }} {{-trims whitespace before-}}trims whitespace after{{- -}}trims both
resources:
manifests:
- template: |
apiVersion: v1
kind: ConfigMap
metadata:
name: config
{{- if .Variables.labels }}
labels:
{{- range $k, $v := .Variables.labels }}
{{ $k }}: {{ $v | quote }}
{{- end }}
{{- end }} Practical Examples
Dynamic Participant VMs
variables:
- name: participant_count
type: integer
default: 10
- name: vm_size
type: string
default: "cx21"
constraints:
enum: ["cx11", "cx21", "cx31", "cx41"]
resources:
vms:
- name: "participant-{{ .Index }}"
count: "{{ .Variables.participant_count }}"
provider: hetzner
type: "{{ .Variables.vm_size }}"
image: ubuntu-22.04
userData: |
#cloud-config
hostname: participant-{{ .Index }}
users:
- name: participant
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
runcmd:
- echo "Welcome participant {{ add .Index 1 }} of {{ .Variables.participant_count }}" > /etc/motd Conditional Monitoring Stack
variables:
- name: enable_monitoring
type: boolean
default: false
- name: monitoring_retention
type: string
default: "7d"
resources:
helm:
{{- if .Variables.enable_monitoring }}
- name: prometheus
chart: prometheus-community/kube-prometheus-stack
namespace: monitoring
values:
prometheus:
prometheusSpec:
retention: "{{ .Variables.monitoring_retention }}"
grafana:
adminPassword: "{{ randAlphaNum 16 }}"
{{- end }} Environment-Aware Configuration
resources:
helm:
- name: app
values:
ingress:
enabled: true
hosts:
- host: "app.{{ .Environment.Domain }}"
paths:
- path: /
env:
- name: ENVIRONMENT_NAME
value: "{{ .Environment.Name }}"
- name: ENVIRONMENT_ID
value: "{{ .Environment.ID }}"
- name: BASE_URL
value: "https://app.{{ .Environment.Domain }}" Multi-Cluster Setup
variables:
- name: cluster_count
type: integer
default: 2
constraints:
min: 1
max: 5
resources:
clusters:
{{- range $i := until .Variables.cluster_count }}
- name: "cluster-{{ $i }}"
provider: hetzner
type: talos
nodes:
controlPlane: 1
workers: 2
nodeSize: cx21
{{- end }}
dns:
{{- range $i := until .Variables.cluster_count }}
- name: "cluster-{{ $i }}"
type: A
target: "{{ index $.Resources.clusters (printf "cluster-%d" $i) "ingress_ip" }}"
{{- end }} Debugging Templates
Validate Locally
# Validate blueprint with variables
teactl blueprint validate -f blueprint.yaml
--var participant_count=10
# Render and see output
teactl blueprint render -f blueprint.yaml
--var participant_count=10 Common Errors
| Error | Cause | Fix |
|---|---|---|
undefined variable | Referencing non-existent variable | Check variable name spelling |
can't evaluate field | Wrong context path | Use correct path (.Variables.x not .x) |
wrong type for value | Type mismatch | Ensure correct types (strings vs integers) |
index out of range | Invalid list access | Check list bounds |
Tip
Use
teactl blueprint render to see the fully rendered blueprint before creating an environment. This helps catch template errors early.Best Practices
- Use defaults liberally - Always provide sensible defaults with
default - Quote strings in YAML - Use
quotewhen values might contain special characters - Validate required inputs - Use
requiredfor mandatory variables - Control whitespace - Use
{{-and-}}to avoid formatting issues - Keep it readable - Complex logic should be in multiple lines with comments
- Test with edge cases - Render with min/max values to verify behavior
Next Steps
- Blueprint Variables - Define typed inputs
- Blueprint Composition - Extend and layer blueprints
- Blueprint Syntax - Full schema reference