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:

FieldDescriptionExample
.Environment.NameEnvironment namejune-workshop
.Environment.IDEnvironment UUIDenv-7f3a2b1c...
.Environment.DomainAssigned domainjune-workshop.acme.teabar.dev
.Environment.OrganizationOrganization slugacme
.Environment.ProjectProject slugtraining
.Environment.CreatedByCreator 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 }}"

.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

FunctionDescriptionExample
lowerLowercase string{{ lower .Variables.name }}
upperUppercase string{{ upper .Variables.name }}
titleTitle case{{ title .Variables.name }}
trimTrim whitespace{{ trim .Variables.name }}
trimPrefixRemove prefix{{ trimPrefix "env-" .name }}
trimSuffixRemove suffix{{ trimSuffix ".yaml" .file }}
replaceReplace substring{{ replace "_" "-" .name }}
containsCheck substring{{ if contains "prod" .env }}
hasPrefixCheck prefix{{ if hasPrefix "dev" .name }}
hasSuffixCheck suffix{{ if hasSuffix ".yaml" .file }}
resources:
  vms:
    - name: "{{ lower .Variables.team_name }}-worker-{{ .Index }}"
      labels:
        team: "{{ lower (replace " " "-" .Variables.team_name) }}"

Quoting Functions

FunctionDescriptionExample
quoteDouble-quote string{{ quote .Variables.name }}
squoteSingle-quote string{{ squote .Variables.name }}
resources:
  manifests:
    - template: |
        apiVersion: v1
        kind: ConfigMap
        data:
          config.json: {{ quote .Variables.config }}

Default Values

FunctionDescriptionExample
defaultProvide default value{{ default "10" .Variables.count }}
requiredFail if empty{{ required "name is required" .Variables.name }}
coalesceFirst 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

FunctionDescriptionExample
b64encBase64 encode{{ b64enc .Variables.secret }}
b64decBase64 decode{{ b64dec .encoded }}
toJsonConvert to JSON{{ toJson .Variables.config }}
toYamlConvert to YAML{{ toYaml .Variables.values }}
fromJsonParse JSON string{{ fromJson .json_string }}
fromYamlParse YAML string{{ fromYaml .yaml_string }}
resources:
  secrets:
    - name: api-credentials
      data:
        token: "{{ b64enc .Variables.api_token }}"
        config: "{{ b64enc (toJson .Variables.config) }}"

Math Functions

FunctionDescriptionExample
addAddition{{ add .Index 1 }}
subSubtraction{{ sub .Variables.count 1 }}
mulMultiplication{{ mul .Variables.count 2 }}
divDivision{{ div .Variables.total 2 }}
modModulo{{ mod .Index 2 }}
maxMaximum{{ max .Variables.a .Variables.b }}
minMinimum{{ 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

FunctionDescriptionExample
randAlphaNumRandom alphanumeric{{ randAlphaNum 16 }}
randAlphaRandom letters{{ randAlpha 8 }}
randNumericRandom numbers{{ randNumeric 6 }}
randAsciiRandom ASCII{{ randAscii 20 }}
uuidv4Generate UUID{{ uuidv4 }}
resources:
  secrets:
    - name: generated-passwords
      data:
        admin_password: "{{ randAlphaNum 24 }}"
        api_key: "{{ randAlphaNum 32 }}"
        session_id: "{{ uuidv4 }}"

Date/Time Functions

FunctionDescriptionExample
nowCurrent time{{ now }}
dateFormat time{{ now \| date "2006-01-02" }}
dateModifyModify time{{ now \| dateModify "+24h" }}
toDateParse 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

FunctionDescriptionExample
listCreate list{{ list "a" "b" "c" }}
firstFirst element{{ first .list }}
lastLast element{{ last .list }}
restAll but first{{ rest .list }}
initialAll but last{{ initial .list }}
appendAdd to list{{ append .list "new" }}
prependAdd to front{{ prepend .list "first" }}
concatJoin lists{{ concat .list1 .list2 }}
joinJoin with separator{{ join "," .list }}
sortAlphaSort strings{{ sortAlpha .list }}
uniqRemove duplicates{{ uniq .list }}
hasCheck 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

OperatorDescriptionExample
eqEqual{{ if eq .val "test" }}
neNot equal{{ if ne .val "" }}
ltLess than{{ if lt .count 10 }}
leLess or equal{{ if le .count 10 }}
gtGreater than{{ if gt .count 5 }}
geGreater or equal{{ if ge .count 5 }}
andLogical AND{{ if and .a .b }}
orLogical OR{{ if or .a .b }}
notLogical 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

ErrorCauseFix
undefined variableReferencing non-existent variableCheck variable name spelling
can't evaluate fieldWrong context pathUse correct path (.Variables.x not .x)
wrong type for valueType mismatchEnsure correct types (strings vs integers)
index out of rangeInvalid list accessCheck list bounds

Best Practices

  1. Use defaults liberally - Always provide sensible defaults with default
  2. Quote strings in YAML - Use quote when values might contain special characters
  3. Validate required inputs - Use required for mandatory variables
  4. Control whitespace - Use {{- and -}} to avoid formatting issues
  5. Keep it readable - Complex logic should be in multiple lines with comments
  6. Test with edge cases - Render with min/max values to verify behavior

Next Steps

ende