Resources

Resources describe the desired state of your infrastructure. Each resource represents something to manage and is backed by a provider that implements platform-specific management logic.

Every resource has a type, a unique name, and resource-specific properties.

Resource Types

  • Archive - Download, extract and copy files from tar.gz and zip archives
  • Exec - Idempotent command execution
  • File - Manage files content, ownership and more
  • Package - Install, upgrade, downgrade and remove packages using OS Native packagers
  • Scaffold - Render template directories into target directories with synchronization and purge support
  • Service - Enable, disable, start, stop and restart services using OS Native service managers

Common Properties

All resources support the following common properties:

PropertyDescription
nameUnique identifier for the resource
ensureDesired state (values vary by resource type)
aliasAlternative name for use in subscribe, require, and logging
providerForce a specific provider
requireList of resources (type#name or type#alias) that must succeed first
health_checksHealth checks to run after applying (see Monitoring)
controlConditional execution rules (see below)

Conditional Resource Execution

Resources can be conditionally executed using a control section and expressions that should resolve to boolean values.

package:
  name: zsh
  ensure: 5.9
  control:
    if: lookup("facts.host.info.os") == "linux"
    unless: lookup("facts.host.info.virtualizationSystem") == "docker"
ccm ensure package zsh 5.9 \
    --if "lookup('facts.host.info.os') == 'linux'"
    --unless "lookup('facts.host.info.virtualizationSystem') == 'docker'"
{
  "protocol": "io.choria.ccm.v1.resource.ensure.request",
  "type": "package",
  "properties": {
    "name": "zsh",
    "ensure": "5.9",
    "control": {
      "if": "lookup(\"facts.host.info.os\") == \"linux\"",
      "unless": "lookup(\"facts.host.info.virtualizationSystem\") == \"docker\""
    }
  }
}

Here we install zsh on all linux machines unless they are running inside a docker container.

The following table shows how the two conditions interact:

ifunlessResource Managed?
(not set)(not set)Yes
true(not set)Yes
false(not set)No
(not set)trueNo
(not set)falseYes
truetrueNo
truefalseYes
falsetrueNo
falsefalseNo

About Names

Resources can be specified like:

/etc/motd:
  ensure: present

This sets name to /etc/motd, in the following paragraphs we will refer to this as name.

Subsections of Resources

Archive

The archive resource downloads and extracts archives from HTTP/HTTPS URLs. It supports tar.gz, tgz, tar, and zip formats.

Note

The archive file path (name) must have the same archive type extension as the URL. For example, if the URL ends in .tar.gz, the name must also end in .tar.gz.

- archive:
    - /opt/downloads/app-v1.2.3.tar.gz:
        url: https://releases.example.com/app/v1.2.3/app-v1.2.3.tar.gz
        checksum: "a1b2c3d4e5f6..."
        extract_parent: /opt/app
        creates: /opt/app/bin/app
        owner: app
        group: app
        cleanup: true
ccm ensure archive /opt/downloads/app.tar.gz \
    --url https://releases.example.com/app.tar.gz \
    --extract-parent /opt/app \
    --creates /opt/app/bin/app \
    --owner root --group root
{
  "protocol": "io.choria.ccm.v1.resource.ensure.request",
  "type": "archive",
  "properties": {
    "name": "/opt/downloads/app-v1.2.3.tar.gz",
    "url": "https://releases.example.com/app/v1.2.3/app-v1.2.3.tar.gz",
    "checksum": "a1b2c3d4e5f6...",
    "extract_parent": "/opt/app",
    "creates": "/opt/app/bin/app",
    "owner": "app",
    "group": "app",
    "cleanup": true
  }
}

This downloads the archive, extracts it to /opt/app, and removes the archive file after extraction. Future runs skip the download if /opt/app/bin/app exists.

Ensure Values

ValueDescription
presentThe archive must be downloaded
absentThe archive file must not exist

Properties

PropertyDescription
nameAbsolute path where the archive will be saved
urlHTTP/HTTPS URL to download the archive from
checksumExpected SHA256 checksum of the downloaded file
extract_parentDirectory to extract the archive contents into
createsFile path; if this file exists, the archive is not downloaded or extracted
cleanupRemove the archive file after successful extraction (requires extract_parent and creates)
ownerOwner of the downloaded archive file (username)
groupGroup of the downloaded archive file (group name)
usernameUsername for HTTP Basic Authentication
passwordPassword for HTTP Basic Authentication
headersAdditional HTTP headers to send with the request (map of header name to value)
providerForce a specific provider (http only)

Authentication

The archive resource supports two authentication methods:

Basic Authentication:

- archive:
    - /opt/downloads/private-app.tar.gz:
        url: https://private.example.com/app.tar.gz
        username: deploy
        password: "{{ lookup('data.deploy_password') }}"
        extract_parent: /opt/app
        owner: root
        group: root
ccm ensure archive /opt/downloads/private-app.tar.gz \
    --url https://private.example.com/app.tar.gz \
    --username deploy \
    --password "{{ lookup('data.deploy_password') }}"
{
  "protocol": "io.choria.ccm.v1.resource.ensure.request",
  "type": "archive",
  "properties": {
    "name": "/opt/downloads/private-app.tar.gz",
    "url": "https://private.example.com/app.tar.gz",
    "username": "deploy",
    "password": "secret",
    "extract_parent": "/opt/app",
    "owner": "root",
    "group": "root"
  }
}

Custom Headers:

- archive:
    - /opt/downloads/app.tar.gz:
        url: https://api.example.com/releases/app.tar.gz
        headers:
          Authorization: "Bearer {{ lookup('data.api_token') }}"
          X-Custom-Header: custom-value
        extract_parent: /opt/app
        owner: root
        group: root
ccm ensure archive /opt/downloads/app.tar.gz \
    --url https://api.example.com/releases/app.tar.gz \
    --headers "Authorization:{{ lookup('data.api_token') }}"
{
  "protocol": "io.choria.ccm.v1.resource.ensure.request",
  "type": "archive",
  "properties": {
    "name": "/opt/downloads/app.tar.gz",
    "url": "https://api.example.com/releases/app.tar.gz",
    "headers": {
      "Authorization": "Bearer mytoken",
      "X-Custom-Header": "custom-value"
    },
    "extract_parent": "/opt/app",
    "owner": "root",
    "group": "root"
  }
}

Idempotency

The archive resource is idempotent through multiple mechanisms:

  1. Checksum verification: If a checksum is provided and the existing file matches, no download occurs.
  2. Creates file: If creates is specified and that file exists, neither download nor extraction occurs.
  3. File existence: If the archive file exists with matching checksum and owner/group, no changes are made.

For best idempotency, always specify either checksum or creates (or both).

Cleanup Behavior

When cleanup: true is set:

  • The archive file is deleted after successful extraction
  • The extract_parent property is required
  • The creates property is required to track extraction state across runs

Supported Archive Formats

ExtensionExtraction Tool
.tar.gz, .tgztar -xzf
.tartar -xf
.zipunzip
Note

The extraction tools (tar, unzip) must be available in the system PATH.

Exec

The exec resource executes commands to bring the system into the desired state. It is idempotent when used with the creates property or refreshonly mode.

Warning

Specify commands with their full path, or use the path property to set the search path.

- exec:
    - /usr/bin/touch /tmp/hello:
        creates: /tmp/hello
        timeout: 30s
        cwd: /tmp

Alternatively for long commands or to improve UX for referencing execs in require or subscribe:

- exec:
    - touch_hello:
        command: /usr/bin/touch /tmp/hello
        creates: /tmp/hello
        timeout: 30s
        cwd: /tmp
ccm ensure exec "/usr/bin/touch /tmp/hello" --creates /tmp/hello --timeout 30s
{
  "protocol": "io.choria.ccm.v1.resource.ensure.request",
  "type": "exec",
  "properties": {
    "name": "/usr/bin/touch /tmp/hello",
    "creates": "/tmp/hello",
    "timeout": "30s",
    "cwd": "/tmp"
  }
}

The command runs only if /tmp/hello does not exist.

Providers

The exec resource supports two providers:

ProviderDescription
posixDefault. Executes commands directly without a shell. Arguments are parsed and passed to the executable.
shellExecutes commands via /bin/sh -c "...". Use this for shell features like pipes, redirections, and builtins.

The posix provider is the default and is suitable for most commands. Use the shell provider when you need shell features:

- exec:
    - cleanup-logs:
        command: find /var/log -name '*.log' -mtime +30 -delete && echo "Done"
        provider: shell

    - check-service:
        command: systemctl is-active httpd || systemctl start httpd
        provider: shell

    - process-data:
        command: cat /tmp/input.txt | grep -v '^#' | sort | uniq > /tmp/output.txt
        provider: shell
Note

The shell provider passes the entire command string to /bin/sh -c, so shell quoting rules apply. The posix provider parses arguments using shell-like quoting but does not invoke a shell.

Properties

PropertyDescription
nameThe command to execute (used as the resource identifier)
commandAlternative command to run instead of name
cwdWorking directory for command execution
environment (array)Environment variables in KEY=VALUE format
pathSearch path for executables as a colon-separated list (e.g., /usr/bin:/bin)
returns (array)Exit codes indicating success (default: [0])
timeoutMaximum execution time (e.g., 30s, 5m); command is killed if exceeded
createsFile path; if this file exists, the command does not run
refreshonly (boolean)Only run when notified by a subscribed resource
subscribe (array)Resources to subscribe to for refresh notifications (type#name or type#alias)
logoutput (boolean)Log the command output
providerForce a specific provider (posix or shell)

File

The file resource manages files and directories, including their content, ownership, and permissions.

Warning

Use absolute file paths and primary group names.

- file:
    - /etc/motd:
        ensure: present
        content: |
          Managed by CCM {{ now() }}
        owner: root
        group: root
        mode: "0644"
ccm ensure file /etc/motd --source /tmp/ccm/motd --owner root --group root --mode 0644
{
  "protocol": "io.choria.ccm.v1.resource.ensure.request",
  "type": "file",
  "properties": {
    "name": "/etc/motd",
    "ensure": "present",
    "content": "Managed by CCM\n",
    "owner": "root",
    "group": "root",
    "mode": "0644"
  }
}

This copies the contents of /tmp/ccm/motd to /etc/motd verbatim and sets ownership.

Use --content or --content-file to parse content through the template engine before writing.

Ensure Values

ValueDescription
presentThe file must exist
absentThe file must not exist
directoryThe path must be a directory

Properties

PropertyDescription
nameAbsolute path to the file
ensureDesired state (present, absent, directory)
contentFile contents, parsed through the template engine
sourceCopy contents from another local file
ownerFile owner (username)
groupFile group (group name)
modeFile permissions in octal notation (e.g., "0644")
providerForce a specific provider (posix only)

Package

The package resource manages system packages. Specify whether the package should be present, absent, at the latest version, or at a specific version.

Warning

Use real package names, not virtual names, aliases, or group names.

- package:
    - zsh:
        ensure: "5.9"
ccm ensure package zsh 5.9
{
  "protocol": "io.choria.ccm.v1.resource.ensure.request",
  "type": "package",
  "properties": {
    "name": "zsh",
    "ensure": "5.9"
  }
}

Ensure Values

ValueDescription
presentThe package must be installed
latestThe package must be installed at latest version
absentThe package must not be installed
<version>The package must be installed at this version

Properties

PropertyDescription
namePackage name
ensureDesired state or version
providerForce a specific provider (dnf, apt)

Provider Notes

APT (Debian/Ubuntu)

The APT provider preserves existing configuration files during package installation and upgrades. When a package is upgraded and the maintainer has provided a new version of a configuration file, the existing file is kept (--force-confold behavior).

Packages in a partially installed or config-files state (removed but configuration remains) are treated as absent. Reinstalling such packages will preserve the existing configuration files.

Note

The provider will not run apt update before installing a package. Use an exec resource to update the package index if necessary.

The provider runs non-interactively and suppresses prompts from apt-listbugs and apt-listchanges.

Scaffold

The scaffold resource renders files from a source template directory to a target directory. Templates have access to facts and Hiera data, enabling dynamic configuration generation from directory structures.

Warning

Target paths must be absolute and canonical (no . or .. components).

- scaffold:
    - /etc/app:
        ensure: present
        source: templates/app
        engine: jet
        purge: true
ccm ensure scaffold /etc/app templates/app --engine jet --purge
{
  "protocol": "io.choria.ccm.v1.resource.ensure.request",
  "type": "scaffold",
  "properties": {
    "name": "/etc/app",
    "ensure": "present",
    "source": "templates/app",
    "engine": "jet",
    "purge": true
  }
}

This renders templates from the templates/app directory into /etc/app using the Jet template engine, removing any files in the target not present in the source.

Tip

This is implemented using the github.com/choria-io/scaffold Go library, you can use this in your own projects or use the included scaffold CLI tool.

Ensure Values

ValueDescription
presentTarget directory must exist with rendered template files
absentManaged files must be removed; target directory removed if empty

Properties

PropertyDescription
nameAbsolute path to the target directory
sourceSource template directory path (relative to working directory or absolute)
engineTemplate engine: go or jet (default: jet)
skip_emptyDo not create empty files in rendered output
left_delimiterCustom left template delimiter
right_delimiterCustom right template delimiter
purgeRemove files in target not present in source
postPost-processing commands: glob pattern to command mapping
providerForce a specific provider (choria only)

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.

Custom Delimiters

- scaffold:
    - /etc/myservice:
        ensure: present
        source: templates/myservice
        engine: go
        left_delimiter: "<<"
        right_delimiter: ">>"
ccm ensure scaffold /etc/myservice templates/myservice \
    --engine go --left-delimiter "<<" --right-delimiter ">>"
{
  "protocol": "io.choria.ccm.v1.resource.ensure.request",
  "type": "scaffold",
  "properties": {
    "name": "/etc/myservice",
    "ensure": "present",
    "source": "templates/myservice",
    "engine": "go",
    "left_delimiter": "<<",
    "right_delimiter": ">>"
  }
}

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 {} in the command as a placeholder for the file’s full path; if omitted, the path is appended as the last argument.

- scaffold:
    - /opt/app:
        ensure: present
        source: templates/app
        post:
          - "*.go": "go fmt {}"
          - "*.sh": "chmod +x {}"
ccm ensure scaffold /opt/app templates/app \
    --post "*.go"="go fmt {}" --post "*.sh"="chmod +x {}"
{
  "protocol": "io.choria.ccm.v1.resource.ensure.request",
  "type": "scaffold",
  "properties": {
    "name": "/opt/app",
    "ensure": "present",
    "source": "templates/app",
    "post": [
      {"*.go": "go fmt {}"},
      {"*.sh": "chmod +x {}"}
    ]
  }
}

Post-processing runs immediately after each file is rendered. Files skipped due to skip_empty are not post-processed.

Purge Behavior

When purge: true is set, files in the target directory that are not present in the source template directory are deleted during rendering. In noop mode, these deletions are logged but not performed.

When purge is disabled (the default), files not present in the source are tracked but not removed. They do not affect idempotency checks for ensure: present, meaning the resource is considered stable even if extra files exist in the target.

Removal Behavior

When ensure: absent, only managed files (changed and stable) are removed. Files not belonging to the scaffold (purged files) are left untouched. After removing managed files and empty subdirectories, the target directory itself is removed on a best-effort basis — it is only deleted if empty. If unrelated files remain, the directory is preserved and no error is raised.

Idempotency

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

For ensure: present:

  • Changed files: Files that would be created or modified. Any changed files make the resource unstable.
  • Stable files: Files whose content matches the rendered output. At least one stable file must exist for the resource to be considered stable.
  • Purged files: Files in the target not present in the source. These only affect stability when purge is enabled.

For ensure: absent, the status check filters Changed and Stable lists to only include files that actually exist on disk. This means after a successful removal, the scaffold is considered absent even if the target directory still exists with unrelated files. Purged files never affect the absent stability check.

Source Resolution

The source property is resolved relative to the manager’s working directory when it is a relative path. URL sources (with a scheme) are passed through unchanged. This allows manifests bundled with template directories to use relative paths.

Template Environment

Templates receive the full template environment, which provides access to:

  • facts - System facts for the managed node
  • data - Hiera-resolved configuration data
  • Template helper functions

Creating Scaffolds

A scaffold source is a directory tree where every file is a template. The directory structure is mirrored directly into the target, so the source layout becomes the output layout.

Source Directory Structure

templates/app/
├── _partials/
│   └── header.conf
├── config.yaml
├── scripts/
│   └── setup.sh
└── README.md

This renders into:

/etc/app/
├── config.yaml
├── scripts/
│   └── setup.sh
└── README.md

The _partials directory is special — its contents are available to templates but are never copied to the target.

Template Syntax

Every file in the source directory is processed as a template. The syntax depends on the engine selected.

Jet engine (default, [[ / ]] delimiters):

# config.yaml
hostname: [[ facts.hostname ]]
environment: [[ data.environment ]]
workers: [[ data.worker_count ]]

Go engine ({{ / }} delimiters):

# config.yaml
hostname: {{ .facts.hostname }}
environment: {{ .data.environment }}
workers: {{ .data.worker_count }}

The Jet engine is the default because its [[ / ]] delimiters avoid conflicts with configuration files that use curly braces (YAML, JSON, systemd units). Use the Go engine when you need access to Sprig functions.

Partials

Files inside a _partials directory are reusable template fragments. They are rendered on demand using the render function but are excluded from the output.

This is useful for shared headers, repeated configuration blocks, or any content used across multiple files.

Jet:

[[ render("_partials/header.conf", .) ]]

server {
    listen [[ data.port ]];
}

Go:

{{ render "_partials/header.conf" . }}

server {
    listen {{ .data.port }};
}

Built-in Functions

Two functions are available in both template engines:

render evaluates another template file from the source directory and returns its output as a string. The partial is rendered using the same engine and data as the calling template.

[[ render("_partials/database.conf", .) ]]
{{ render "_partials/database.conf" . }}

write creates an additional file in the target directory from within a template. This is useful for dynamically generating files based on data — for example, creating one configuration file per service.

[[ write("extra.conf", "generated content") ]]
{{ write "extra.conf" "generated content" }}

Sprig Functions

When using the Go template engine, all Sprig template functions are available. These provide string manipulation, math, date formatting, list operations, and more:

# Go engine example with Sprig functions
hostname: {{ .facts.hostname | upper }}
packages: {{ join ", " .data.packages }}
generated: {{ now | date "2006-01-02" }}

Example Scaffold

A complete scaffold for an application configuration:

Source structure:

templates/myapp/
├── _partials/
│   └── logging.conf
├── myapp.conf
└── scripts/
    └── healthcheck.sh

_partials/logging.conf (Jet):

log_level = [[ data.log_level ]]
log_file = /var/log/myapp/[[ facts.hostname ]].log

myapp.conf (Jet):

[[ render("_partials/logging.conf", .) ]]

[server]
bind = 0.0.0.0
port = [[ data.port ]]
workers = [[ data.workers ]]

scripts/healthcheck.sh (Jet):

#!/bin/bash
curl -sf http://localhost:[[ data.port ]]/health || exit 1

Manifest using this scaffold:

- scaffold:
    - /etc/myapp:
        ensure: present
        source: templates/myapp
        purge: true
        post:
          - "*.sh": "chmod +x {}"

With facts {"hostname": "web01"} and data {"port": 8080, "workers": 4, "log_level": "info"}, this renders:

/etc/myapp/
├── myapp.conf
└── scripts/
    └── healthcheck.sh

Where myapp.conf contains:

log_level = info
log_file = /var/log/myapp/web01.log

[server]
bind = 0.0.0.0
port = 8080
workers = 4

Service

The service resource manages system services. Services have two independent properties: whether they are running and whether they are enabled to start at boot.

Warning

Use real service names, not virtual names or aliases.

Services can subscribe to other resources and restart when those resources change.

- service:
    - httpd:
        ensure: running
        enable: true
        subscribe:
          - package#httpd
ccm ensure service httpd running --enable --subscribe package#httpd
{
  "protocol": "io.choria.ccm.v1.resource.ensure.request",
  "type": "service",
  "properties": {
    "name": "httpd",
    "ensure": "running",
    "enable": true,
    "subscribe": ["package#httpd"]
  }
}

Ensure Values

ValueDescription
runningThe service must be running
stoppedThe service must be stopped

If ensure is not specified, it defaults to running.

Properties

PropertyDescription
nameService name
ensureDesired state (running or stopped; default: running)
enable (boolean)Enable the service to start at boot
subscribe (array)Resources to watch; restart the service when they change (type#name or type#alias)
providerForce a specific provider (systemd only)