Scaffold Type

This document describes the design of the scaffold resource type for rendering template directories into target directories.

Overview

The scaffold resource renders files from a source template directory to a target directory:

  • Rendering: Process templates using Go text/template or Jet engine
  • Synchronization: Detect changed, stable, and purgeable files
  • Cleanup: Optionally remove files in the target not present in the source

Templates have access to facts and data from Hiera, enabling dynamic configuration generation from directory structures.

Provider Interface

Scaffold providers must implement the ScaffoldProvider interface:

type ScaffoldProvider interface {
    model.Provider

    Remove(ctx context.Context, prop *model.ScaffoldResourceProperties, state *model.ScaffoldState) error
    Scaffold(ctx context.Context, env *templates.Env, prop *model.ScaffoldResourceProperties, noop bool) (*model.ScaffoldState, error)
    Status(ctx context.Context, env *templates.Env, prop *model.ScaffoldResourceProperties) (*model.ScaffoldState, error)
}

Method Responsibilities

MethodResponsibility
StatusRender in noop mode to determine current state of managed files
ScaffoldRender templates to target directory (or noop to preview changes)
RemoveDelete managed files (changed and stable) and clean up directories

Status Response

The Status method returns a ScaffoldState containing:

type ScaffoldState struct {
    CommonResourceState
    Metadata *ScaffoldMetadata
}

type ScaffoldMetadata struct {
    Name         string                 // Target directory
    Provider     string                 // Provider name (e.g., "choria")
    TargetExists bool                   // Whether target directory exists
    Changed      []string               // Files created or modified
    Purged       []string               // Files removed (not in source)
    Stable       []string               // Files unchanged
    Engine       ScaffoldResourceEngine // Template engine used
}

The Ensure field in CommonResourceState is set to:

  • present if the target directory exists
  • absent if the target directory does not exist

Available Providers

ProviderEngine SupportDocumentation
choriaGo, JetChoria

Ensure States

ValueDescription
presentTarget directory must exist with rendered template files
absentManaged files must be removed from the target

Template Engines

Two template engines are supported:

EngineLibraryDefault DelimitersDescription
goGo text/template{{ / }}Standard Go templates
jetJet templating[[ / ]]Jet template language

The engine defaults to jet if not specified. Delimiters can be customized via left_delimiter and right_delimiter properties.

Properties

PropertyTypeRequiredDescription
sourcestringYesSource template directory path or URL
enginestringNoTemplate engine: go or jet (default: jet)
skip_emptyboolNoSkip empty files in rendered output
left_delimiterstringNoCustom left template delimiter
right_delimiterstringNoCustom right template delimiter
purgeboolNoRemove files in target not present in source
datamap[string]anyNoCustom data that replaces Hiera data for template rendering
post[]map[string]stringNoPost-processing: glob pattern to command mapping
# Render configuration templates using Jet engine
- scaffold:
    - /etc/app:
        ensure: present
        source: templates/app
        engine: jet
        purge: true

# Render with Go templates and custom delimiters
- scaffold:
    - /etc/myservice:
        ensure: present
        source: templates/myservice
        engine: go
        left_delimiter: "<<"
        right_delimiter: ">>"

# With post-processing commands
- scaffold:
    - /opt/app:
        ensure: present
        source: templates/app
        post:
          - "*.go": "go fmt {}"

# With custom data replacing Hiera data
- scaffold:
    - /etc/app:
        ensure: present
        source: templates/app
        engine: jet
        data:
          app_name: myapp
          version: "{{ Facts.version }}"
          port: 8080

Custom Data

The data property allows supplying custom data that completely replaces the Hiera-resolved data for template rendering. When data is set and non-empty, templates receive only the custom data via data instead of the Hiera-resolved data from the manifest.

This is useful when a scaffold resource needs data that differs from or is unrelated to the global Hiera data, or when you want to provide a self-contained data set for a specific scaffold.

Behavior

  • When data is not set or empty: templates receive the Hiera-resolved data from the manager as normal.
  • When data is set and non-empty: env.Data is replaced with the custom data before calling Status() and Scaffold(). The custom data is used consistently throughout the entire apply cycle.
  • facts remain available regardless of whether custom data is provided.

Template Resolution in Data Values

String values in the data map support template expressions that are resolved during property template resolution. Both keys and values can contain templates:

data:
  "{{ Facts.key_name }}": "{{ Facts.value }}"
  static_key: "{{ Facts.hostname }}"
  port: 8080

Non-string values (integers, booleans, lists, maps) are preserved as-is without template resolution.

Apply Logic

┌─────────────────────────────────────────┐
│ Get template environment from manager   │
└─────────────────┬───────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────┐
│ Custom data set? Override env.Data      │
└─────────────────┬───────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────┐
│ Get current state via Status()          │
└─────────────────┬───────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────┐
│ Is current state desired state?         │
└─────────────────┬───────────────────────┘
              Yes │         No
                  ▼         │
          ┌───────────┐     │
          │ No change │     │
          └───────────┘     │
                            ▼
              ┌─────────────────────────┐
              │ What is desired ensure? │
              └─────────────┬───────────┘
                            │
            ┌───────────────┴───────────────┐
            │ absent                        │ present
            ▼                               ▼
      ┌───────────┐                   ┌───────────┐
      │ Noop?     │                   │ Noop?     │
      └─────┬─────┘                   └─────┬─────┘
        Yes │     No                    Yes │     No
            ▼     │                         ▼     │
    ┌────────────┐│                 ┌────────────┐│
    │ Set noop   ││                 │ Set noop   ││
    │ message    ││                 │ message    ││
    └────────────┘│                 └────────────┘│
                  ▼                               ▼
        ┌───────────────┐             ┌─────────────────────┐
        │ Remove all    │             │ Scaffold            │
        │ managed files │             │ (render templates)  │
        │ and empty dirs│             │                     │
        └───────────────┘             └─────────────────────┘

Idempotency

The scaffold resource determines idempotency by rendering templates in noop mode and comparing results against the target directory.

State Checks

  1. Ensure absent: Target must not exist, or no managed files remain on disk (Changed and Stable lists empty). Purged files (files not belonging to the scaffold) do not affect this check.
  2. Ensure present: The Changed list must be empty, and the Purged list must be empty when purge is enabled (all files are stable). When purge is disabled, purged files do not affect stability.

Decision Table

For ensure: absent, purged files never affect stability since they don’t belong to the scaffold. For ensure: present, purged files only affect stability when purge is enabled.

When ensure: absent, the Status method filters Changed and Stable lists to only include files that actually exist on disk, so the state reflects reality after removal rather than what the scaffold would create.

DesiredTarget ExistsChanged FilesPurged FilesPurge EnabledStable?
absentNoN/AN/AN/AYes
absentYesNoneAnyN/AYes (no managed files on disk)
absentYesSomeAnyN/ANo (managed files remain)
presentYesNoneNoneAnyYes
presentYesNoneSomeNoYes (purged files ignored)
presentYesNoneSomeYesNo (purge needed)
presentYesSomeAnyAnyNo (render needed)
presentNoN/AN/AAnyNo (target missing)

Source Resolution

The source property is resolved relative to the manager’s working directory when it is a relative path:

parsed, _ := url.Parse(properties.Source)
if parsed == nil || parsed.Scheme == "" {
    if !filepath.IsAbs(properties.Source) {
        t.prop.Source = filepath.Join(mgr.WorkingDirectory(), properties.Source)
    }
}

This allows manifests bundled with template directories to use relative paths. URL sources (with a scheme) are passed through unchanged.

Path Validation

Target paths (the resource name) must be:

  • Absolute (start with /)
  • Canonical (no . or .. components, filepath.Clean(path) == path)

Post-Processing

The post property defines commands to run on rendered files. Each entry is a map where the key is a glob pattern matched against the file’s basename and the value is a command to execute. Use {} as a placeholder for the file’s full path; if omitted, the path is appended as the last argument.

post:
  - "*.go": "go fmt {}"
  - "*.sh": "chmod +x {}"

Post-processing runs immediately after each file is rendered. Validation ensures neither keys nor values are empty.

Noop Mode

In noop mode, the scaffold type queries the current state via Status() and reports what would change without modifying the filesystem. Neither Scaffold() nor Remove() are called.

For ensure: present, the affected count is the number of changed files plus purged files (when purge is enabled). For ensure: absent, the affected count is the number of changed and stable files plus purged files (when purge is enabled).

DesiredAffected CountMessage
presentChanged + Purged (if purge enabled)Would have changed N scaffold files
absentChanged + Stable + Purged (if purge enabled)Would have removed N scaffold files

Changed is set to true only when the affected count is greater than zero. When the resource is already in the desired state, Changed is false and NoopMessage is empty.

Desired State Validation

After applying changes (in non-noop mode), the type verifies the scaffold reached the desired state by checking the changed and purged file lists. If validation fails, ErrDesiredStateFailed is returned.