Design documents provide detailed implementation guidance for CCM’s resource types, providers, and internal components. They are intended for developers contributing to CCM or those seeking to understand specific implementation details.
For end-user documentation on how to use resources, see Resources.
Note
These design documents are largely written with AI assistance and reviewed before publication.
Contents
Each design document covers:
Purpose and scope: What the component does and its responsibilities
Architecture: How the component fits into CCM’s overall design
Implementation details: Key data structures, interfaces, and algorithms
Provider contracts: Requirements for implementing new providers
Multiple actions are joined with “. " (e.g., “Would have downloaded. Would have extracted”).
Desired State Validation
After applying changes (in non-noop mode), the type verifies the archive reached the desired state by calling Status() again and checking all conditions. If validation fails, ErrDesiredStateFailed is returned.
Subsections of Archive Type
HTTP Provider
This document describes the implementation details of the HTTP archive provider for downloading and extracting archives from HTTP/HTTPS URLs.
Provider Selection
The HTTP provider is selected when:
The URL scheme is http or https
The archive file extension is supported (.tar.gz, .tgz, .tar, .zip)
The required extraction tool (tar or unzip) is available in PATH
The IsManageable() function checks these conditions and returns a priority of 1 if all are met.
Operations
Download
Process:
Parse the URL and add Basic Auth credentials if username/password provided
Create HTTP request with custom headers (if specified)
Execute GET request via util.HttpGetResponse()
Verify HTTP 200 status code
Create temporary file in the same directory as the target
Check if creates file exists, return current state
Execute
Run the command, return exit code
Status Response
The Status method returns an ExecState containing:
typeExecStatestruct {
CommonResourceStateExitCode*int// Exit code from last execution (nil if not run)CreatesSatisfiedbool// Whether creates file exists}
The Ensure field in CommonResourceState is set to:
present if the creates file exists
absent if the creates file does not exist (or not specified)
Subscribe takes precedence over other idempotency checks - if a subscribed resource changed, the command executes regardless of creates file existence.
Exit Code Validation
By default, exit code 0 indicates success. The returns property customizes acceptable codes:
This document describes the implementation details of the Posix exec provider for executing commands without a shell.
Provider Selection
The Posix provider is the default exec provider. It is always available and returns priority 1 for all exec resources unless a different provider is explicitly requested via the provider property.
To use the shell provider instead, specify provider: shell in the resource properties.
Comparison with Shell Provider
Feature
Posix
Shell
Shell invocation
No
Yes (/bin/sh -c)
Pipes (|)
Not supported
Supported
Redirections (>, <)
Not supported
Supported
Shell builtins (cd, export)
Not supported
Supported
Glob expansion
Not supported
Supported
Command substitution ($(...))
Not supported
Supported
Argument parsing
shellquote.Split()
Passed as single string
Security
Lower attack surface
Shell injection possible
When to use Posix (default):
Simple commands with arguments
When shell features are not needed
For better security (no shell injection risk)
When to use Shell:
Commands with pipes, redirections, or shell builtins
Complex command strings
When shell expansion is required
Operations
Execute
Process:
Determine command source (Command property or Name if Command is empty)
Parse command string into words using shellquote.Split()
Extract command (first word) and arguments (remaining words)
Execute via CommandRunner.ExecuteWithOptions()
Optionally log output line-by-line if LogOutput is enabled
Command Parsing:
The command string is parsed using github.com/kballard/go-shellquote, which handles:
Syntax
Example
Result
Simple words
echo hello world
["echo", "hello", "world"]
Single quotes
echo 'hello world'
["echo", "hello world"]
Double quotes
echo "hello world"
["echo", "hello world"]
Escaped spaces
echo hello\ world
["echo", "hello world"]
Mixed quoting
echo "it's a test"
["echo", "it's a test"]
Execution Options:
Option
Source
Description
Command
First word after parsing
Executable path or name
Args
Remaining words
Command arguments
Cwd
properties.Cwd
Working directory
Environment
properties.Environment
Additional env vars (KEY=VALUE format)
Path
properties.Path
Search path for executables
Timeout
properties.ParsedTimeout
Maximum execution time
Output Logging:
When LogOutput: true is set and a user logger is provided:
The model validates exec properties before execution:
Property
Validation
name
Must be parseable by shellquote (balanced quotes)
timeout
Must be valid duration format (e.g., 30s, 5m)
subscribe
Each entry must be type#name format
path
Each directory must be absolute (start with /)
environment
Each entry must be KEY=VALUE format with non-empty key and value
Platform Support
The Posix provider works on all platforms supported by Go’s os/exec package. It does not use any platform-specific system calls directly.
The command runner (model.CommandRunner) handles the actual process execution, which may have platform-specific implementations.
Security Considerations
No Shell Injection
Unlike the shell provider, the posix provider does not invoke a shell. Arguments are passed directly to the executable, preventing shell injection attacks:
# Safe with posix provider - $USER is passed literally, not expanded- exec:
- /bin/echo $USER:
provider: posix # Default# Potentially dangerous with shell provider - $USER is expanded- exec:
- /bin/echo $USER:
provider: shell
Path Validation
The path property only accepts absolute directory paths, preventing path traversal via relative paths.
Environment Validation
Environment variables must have non-empty keys and values, preventing injection of empty or malformed entries.
Shell Provider
This document describes the implementation details of the Shell exec provider for executing commands via /bin/sh.
Provider Selection
The Shell provider is selected when provider: shell is explicitly specified in the resource properties. It has a lower priority (99) than the Posix provider (1), so it is never automatically selected.
Availability: The provider checks for the existence of /bin/sh via util.FileExists(). If /bin/sh does not exist, the provider is not available.
Comparison with Posix Provider
Feature
Shell
Posix
Shell invocation
Yes (/bin/sh -c)
No
Pipes (|)
Supported
Not supported
Redirections (>, <, >>)
Supported
Not supported
Shell builtins (cd, export, source)
Supported
Not supported
Glob expansion (*.txt, ?)
Supported
Not supported
Command substitution ($(...), `...`)
Supported
Not supported
Variable expansion ($VAR, ${VAR})
Supported
Not supported
Logical operators (&&, ||)
Supported
Not supported
Argument parsing
Passed as single string
shellquote.Split()
Security
Shell injection possible
Lower attack surface
When to use Shell:
Commands with pipes: cat file.txt | grep pattern | sort
Commands with redirections: echo "data" > /tmp/file
Commands with shell builtins: cd /tmp && pwd
Commands with variable expansion: echo $HOME
Complex one-liners with logical operators
When to use Posix (default):
Simple commands with arguments
When shell features are not needed
For better security (no shell injection risk)
Operations
Execute
Process:
Determine command source (Command property or Name if Command is empty)
Validate command is not empty
Execute via CommandRunner.ExecuteWithOptions() with /bin/sh -c "<command>"
Optionally log output line-by-line if LogOutput is enabled
Execution Method:
The entire command string is passed to the shell as a single argument:
/bin/sh -c "<entire command string>"
This allows the shell to interpret all shell syntax, including:
Pipes and redirections
Variable expansion
Glob patterns
Command substitution
Logical operators
Execution Options:
Option
Value
Description
Command
/bin/sh
Shell executable path
Args
["-c", "<command>"]
Shell flag and command string
Cwd
properties.Cwd
Working directory
Environment
properties.Environment
Additional env vars (KEY=VALUE format)
Path
properties.Path
Search path for executables
Timeout
properties.ParsedTimeout
Maximum execution time
Output Logging:
When LogOutput: true is set and a user logger is provided:
This allows manifests bundled with their source files to use relative paths.
Noop Mode
In noop mode, the file type:
Queries current state normally
Computes content checksums
Logs what actions would be taken
Sets appropriate NoopMessage:
“Would have created the file”
“Would have created directory”
“Would have removed the file”
Reports Changed: true if changes would occur
Does not call provider Store/CreateDirectory methods
Does not remove files
Desired State Validation
After applying changes (in non-noop mode), the type verifies the file reached the desired state by calling Status() again and checking all attributes match. If validation fails, ErrDesiredStateFailed is returned.
Subsections of File Type
Posix Provider
This document describes the implementation details of the Posix file provider for managing files and directories on Unix-like systems.
Provider Selection
The Posix provider is the default and only file provider. It is always available and returns priority 1 for all file resources.
Operations
Store (Create/Update File)
Process:
Verify parent directory exists
Parse file mode from octal string
Open source file if source property is set
Create temporary file in the same directory as target
Set file permissions on temp file
Write content (from source file or contents property)
This allows manifests to use relative paths for source files bundled with the manifest.
Platform Support
The Posix provider uses Unix-specific system calls:
Operation
System Call
Get file owner/group
syscall.Stat_t (UID/GID from stat)
Set ownership
os.Chown() β chown(2)
Set permissions
os.Chmod() β chmod(2)
The provider has separate implementations for Unix and Windows (file_unix.go, file_windows.go in internal/util), with Windows returning errors for ownership operations.
Security Considerations
Atomic Writes
Files are written atomically via temp file + rename. This prevents:
Partial file reads during write
Corruption if process is interrupted
Race conditions with concurrent readers
Permission Ordering
Permissions and ownership are set on the temp file before rename:
Chmod - Set permissions
Write content
Chown - Set ownership
Rename to target
This ensures the file never exists at the target path with incorrect permissions.
Path Validation
File paths must be absolute and clean (no . or .. components):
iffilepath.Clean(p.Name) !=p.Name {
returnfmt.Errorf("file path must be absolute")
}
Required Properties
Owner, group, and mode are required properties and cannot be empty, preventing accidental creation of files with default/inherited permissions.
Package Type
This document describes the design of the package resource type for managing software packages.
Overview
The package resource manages software packages with two aspects:
Existence: Whether the package is installed or absent
Version: The specific version installed (when applicable)
Provider Interface
Package providers must implement the PackageProvider interface:
βββββββββββββββββββββββββββββββββββββββββββ
β Get current state via Status() β
βββββββββββββββββββ¬ββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββ
β Is ensure = "latest"? β
βββββββββββββββββββ¬ββββββββββββββββββββββββ
Yes β No
βΌ β
βββββββββββββββββββββββ β
β Is package absent? β β
βββββββββββββββ¬ββββββββ β
Yes β No β
βΌ βΌ β
ββββββββββ ββββββββββ
βInstall β βUpgrade β
βlatest β βlatest β
ββββββββββ ββββββββββ
β
βΌ
βββββββββββββββββββββββββββ
β Is desired state met? β
βββββββββββββββ¬ββββββββββββ
Yes β No
βΌ β
βββββββββββββ β
β No change β βΌ
βββββββββββββ (Phase 2)
Phase 2: Handle Ensure Values
βββββββββββββββββββββββββββ
β What is desired ensure? β
βββββββββββββββ¬ββββββββββββ
β
βββββββββββββββββββββββββΌββββββββββββββββββββββββ
β absent β present β <version>
βΌ βΌ βΌ
ββββββββββββββ βββββββββββββββββ βββββββββββββββββ
β Uninstall β β Is absent? β β Is absent? β
ββββββββββββββ βββββββββ¬ββββββββ βββββββββ¬ββββββββ
Yes β No Yes β No
βΌ βΌ βΌ βΌ
ββββββββββ ββββββββββ ββββββββββ ββββββββββββββ
βInstall β βNo β βInstall β βCompare β
β β βchange β βversion β βversions β
ββββββββββ ββββββββββ ββββββββββ βββββββ¬βββββββ
β
ββββββββββββββββββΌβββββββββββββββββ
β current < β current = β current >
βΌ βΌ βΌ
βββββββββββββ βββββββββββββ βββββββββββββ
β Upgrade β β No change β β Downgrade β
βββββββββββββ βββββββββββββ βββββββββββββ
Version Comparison
The VersionCmp method compares two version strings:
Return Value
Meaning
-1
versionA < versionB (upgrade needed)
0
versionA == versionB (no change)
1
versionA > versionB (downgrade needed)
Version comparison is delegated to the provider, allowing platform-specific version parsing (e.g., RPM epoch handling, Debian revision suffixes).
Idempotency
The package resource is idempotent through state comparison:
Decision Table
Desired
Current State
Action
ensure: present
installed (any version)
None
ensure: present
absent
Install
ensure: absent
absent
None
ensure: absent
installed
Uninstall
ensure: latest
absent
Install latest
ensure: latest
installed
Upgrade (always runs)
ensure: <version>
same version
None
ensure: <version>
older version
Upgrade
ensure: <version>
newer version
Downgrade
ensure: <version>
absent
Install
Special Case: ensure: latest
When ensure: latest is used:
The package manager determines what “latest” means
Upgrade is always called when the package exists (package manager is idempotent)
The type cannot verify if “latest” was achieved (package managers may report stale data)
Desired state validation only checks that the package is not absent
Package Name Validation
Package names are validated to prevent injection attacks:
Allowed Characters:
Alphanumeric (a-z, A-Z, 0-9)
Period (.), underscore (_), plus (+)
Colon (:), tilde (~), hyphen (-)
Rejected:
Shell metacharacters (;, |, &, $, etc.)
Whitespace
Quotes and backticks
Path separators
Version strings (when ensure is a version) are also validated for dangerous characters.
Noop Mode
In noop mode, the package type:
Queries current state normally
Computes version comparison
Logs what actions would be taken
Sets appropriate NoopMessage:
“Would have installed latest”
“Would have upgraded to latest”
“Would have installed version X”
“Would have upgraded to X”
“Would have downgraded to X”
“Would have uninstalled”
Reports Changed: true if changes would occur
Does not call provider Install/Upgrade/Downgrade/Uninstall methods
Desired State Validation
After applying changes (in non-noop mode), the type verifies the package reached the desired state:
func (t*Type) isDesiredState(properties, state) bool {
switchproperties.Ensure {
case"present":
// Any installed version is acceptablereturnstate.Ensure!="absent"case"absent":
returnstate.Ensure=="absent"case"latest":
// Cannot verify "latest", just check not absentreturnstate.Ensure!="absent"default:
// Specific version must matchreturnVersionCmp(state.Ensure, properties.Ensure, false) ==0 }
}
If the desired state is not reached, an ErrDesiredStateFailed error is returned.
Subsections of Package Type
APT Provider
This document describes the implementation details of the APT package provider for Debian-based systems.
Environment
All commands are executed with the following environment variables to ensure non-interactive operation:
Variable
Value
Purpose
DEBIAN_FRONTEND
noninteractive
Prevents dpkg from prompting for user input
APT_LISTBUGS_FRONTEND
none
Suppresses apt-listbugs prompts
APT_LISTCHANGES_FRONTEND
none
Suppresses apt-listchanges prompts
Concurrency
A global package lock (model.PackageGlobalLock) is held during all command executions to prevent concurrent apt/dpkg operations within the same process. This prevents lock contention on /var/lib/dpkg/lock.
Helper methods: LessThan, GreaterThan, Equal, etc.
DNF Provider
This document describes the implementation details of the DNF package provider for RHEL/Fedora-based systems.
Concurrency
A global package lock (model.PackageGlobalLock) is held during all command executions to prevent concurrent dnf/rpm operations within the same process. This prevents lock contention on the RPM database.
Target directory must exist with rendered template files
absent
Managed files must be removed from the target
Template Engines
Two template engines are supported:
Engine
Library
Default Delimiters
Description
go
Go text/template
{{ / }}
Standard Go templates
jet
Jet templating
[[ / ]]
Jet template language
The engine defaults to jet if not specified. Delimiters can be customized via left_delimiter and right_delimiter properties.
Properties
Property
Type
Required
Description
source
string
Yes
Source template directory path or URL
engine
string
No
Template engine: go or jet (default: jet)
skip_empty
bool
No
Skip empty files in rendered output
left_delimiter
string
No
Custom left template delimiter
right_delimiter
string
No
Custom right template delimiter
purge
bool
No
Remove files in target not present in source
post
[]map[string]string
No
Post-processing: glob pattern to command mapping
# Render configuration templates using Jet engine- scaffold:
- /etc/app:
ensure: presentsource: templates/appengine: jetpurge: true# Render with Go templates and custom delimiters- scaffold:
- /etc/myservice:
ensure: presentsource: templates/myserviceengine: goleft_delimiter: "<<"right_delimiter: ">>"# With post-processing commands- scaffold:
- /opt/app:
ensure: presentsource: templates/apppost:
- "*.go": "go fmt {}"
Apply Logic
βββββββββββββββββββββββββββββββββββββββββββ
β 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
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.
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.
Desired
Target Exists
Changed Files
Purged Files
Purge Enabled
Stable?
absent
No
N/A
N/A
N/A
Yes
absent
Yes
None
Any
N/A
Yes (no managed files on disk)
absent
Yes
Some
Any
N/A
No (managed files remain)
present
Yes
None
None
Any
Yes
present
Yes
None
Some
No
Yes (purged files ignored)
present
Yes
None
Some
Yes
No (purge needed)
present
Yes
Some
Any
Any
No (render needed)
present
No
N/A
N/A
Any
No (target missing)
Source Resolution
The source property is resolved relative to the manager’s working directory when it is a relative path:
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-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).
Desired
Affected Count
Message
present
Changed + Purged (if purge enabled)
Would have changed N scaffold files
absent
Changed + 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.
Subsections of Scaffold Type
Choria Provider
This document describes the implementation details of the Choria scaffold provider for rendering template directories using the choria-io/scaffold library.
Provider Selection
The Choria provider is the default and only scaffold provider. It is always available and returns priority 1 for all scaffold resources.
Operations
Scaffold (Render Templates)
Process:
Check if target directory exists
Configure scaffold with source, target, engine, delimiters, post-processing, and skip_empty settings
Create scaffold instance using the appropriate engine (scaffold.New() for Go, scaffold.NewJet() for Jet)
Call Render() (real mode) or RenderNoop() (noop mode)
Categorize results into changed, stable, and purged file lists
Scaffold Configuration:
Config Field
Source Property
Description
TargetDirectory
Name
Target directory for rendered files
SourceDirectory
Source
Source template directory
MergeTargetDirectory
(always true)
Merge into existing target directory
Post
Post
Post-processing commands
SkipEmpty
SkipEmpty
Skip empty rendered files
CustomLeftDelimiter
LeftDelimiter
Custom template left delimiter
CustomRightDelimiter
RightDelimiter
Custom template right delimiter
Engine Selection:
Engine
Constructor
Default Delimiters
go
scaffold.New()
{{ / }}
jet
scaffold.NewJet()
[[ / ]]
Result Categorization:
Scaffold Action
Metadata List
Description
FileActionEqual
Stable
File content unchanged
FileActionAdd
Changed
New file created
FileActionUpdate
Changed
Existing file modified
FileActionRemove
Purged
File removed from target
File paths in the metadata lists are absolute paths, constructed by joining the target directory with the relative path from the scaffold result.
Purge Behavior:
When purge is enabled and a file has FileActionRemove, the provider deletes the file from disk during Scaffold(). In noop mode, the removal is logged but not performed. When purge is disabled, purged files are only tracked in metadata and not removed.
Status
Process:
Perform a dry-run render (noop mode) to determine what the scaffold would do
When ensure is absent, filter Changed and Stable lists to only include files that actually exist on disk
The noop render reports what would happen if the scaffold were applied. For ensure: present, this is the desired output β it shows what needs to change. For ensure: absent, the raw render output is misleading after removal (it would show files to be added), so the lists are filtered to reflect what managed files actually remain on disk.
State Detection:
Target Directory
Ensure Value
Metadata
Exists
present
Changed, stable, and purged file lists from render
Exists
absent
Changed and stable filtered to files on disk, purged from render
Does not exist
Any
Empty metadata, TargetExists: false
Remove
Process:
Collect managed files from the state’s Changed and Stable lists (purged files are not removed as they don’t belong to the scaffold)
Stop when no more empty directories can be removed
Best-effort removal of the target directory (only succeeds if empty)
File Removal Order:
Files are collected from two metadata lists:
Changed - Files that were added or modified
Stable - Files that were unchanged
Purged files are not removed because they are unrelated to the scaffold and may belong to other processes.
Directory Cleanup:
For each removed file:
Track its parent directory
Repeat:
For each tracked directory:
Skip if it is the target directory itself
Skip if not empty
Remove the directory
Track its parent directory
Until no more directories removed
Best-effort: remove the target directory (fails silently if not empty)
The target directory is removed if empty after all managed files and subdirectories are cleaned up. If unrelated files remain (purged files), the directory is preserved.
Error Handling:
Condition
Behavior
Non-absolute file path
Return error immediately
File removal fails
Log error, continue with remaining files
Directory removal fails
Log error, continue with remaining directories
File does not exist
Silently skip (os.IsNotExist check)
Target directory removal fails
Log at debug level, no error returned
Template Environment
Templates receive the full templates.Env environment, which provides access to:
facts - System facts for the managed node
data - Hiera-resolved configuration data
Template helper functions
This allows templates to generate host-specific configurations based on facts and hierarchical data.
Logging
The provider wraps the CCM logger in a scaffold-compatible interface:
This adapter translates the scaffold library’s Debugf/Infof calls to CCM’s structured logging.
Platform Support
The Choria provider is platform-independent. It uses the choria-io/scaffold library for template rendering, which operates on standard filesystem operations. No platform-specific system calls are used.
Service Type
This document describes the design of the service resource type for managing system services.
Overview
The service resource manages system services with two independent dimensions:
Running state: Whether the service is currently running or stopped
Enabled state: Whether the service starts automatically at boot
These are managed independently, allowing combinations like “running but disabled” or “stopped but enabled”.
Provider Interface
Service providers must implement the ServiceProvider interface:
The Status method returns a ServiceState containing:
typeServiceStatestruct {
CommonResourceStateMetadata*ServiceMetadata}
typeServiceMetadatastruct {
Namestring// Service nameProviderstring// Provider name (e.g., "systemd")Enabledbool// Whether service starts at bootRunningbool// Whether service is currently running}
The Ensure field in CommonResourceState is set to:
If the desired state is not reached, an ErrDesiredStateFailed error is returned.
Service Name Validation
Service names are validated to prevent injection attacks:
Allowed Characters:
Alphanumeric (a-z, A-Z, 0-9)
Period (.), underscore (_), plus (+)
Colon (:), tilde (~), hyphen (-)
Rejected:
Shell metacharacters (;, |, &, etc.)
Whitespace
Path separators
Noop Mode
In noop mode, the service type:
Queries current state normally
Logs what actions would be taken
Sets appropriate NoopMessage (e.g., “Would have started”, “Would have enabled”)
Reports Changed: true if changes would occur
Does not call provider Start/Stop/Restart/Enable/Disable methods
Subsections of Service Type
Systemd Provider
This document describes the implementation details of the Systemd service provider for managing system services via systemctl.
Provider Selection
The Systemd provider is selected when systemctl is found in the system PATH. The provider checks for the executable using util.ExecutableInPath("systemctl").
Availability Check:
Searches PATH for systemctl
Returns priority 1 if found
Returns unavailable if not found
Concurrency
A global service lock (model.ServiceGlobalLock) is held during all systemctl command executions to prevent concurrent systemd operations within the same process. This prevents race conditions when multiple service resources are managed simultaneously.
The provider performs a systemctl daemon-reload once per provider instance before any service operations. This ensures systemd picks up any unit file changes made by other resources (e.g., file resources managing unit files).
Integration points - Factory functions and registry
CLI commands - User-facing command line interface
JSON schemas - Validation for manifests and API requests
Documentation - User and design documentation
CCM Studio - Web-based manifest designer
File Checklist
File
Action
Purpose
model/resource_<type>.go
Create
Properties, state, metadata structs
model/resource_<type>_test.go
Create
Property validation tests
model/resource.go
Modify
Add case to factory function
resources/<type>/<type>.go
Create
Provider interface definition
resources/<type>/type.go
Create
Resource type implementation
resources/<type>/type_test.go
Create
Resource type tests
resources/<type>/provider_mock_test.go
Generate
Mock provider for tests
resources/<type>/<provider>/factory.go
Create
Provider factory
resources/<type>/<provider>/<provider>.go
Create
Provider implementation
resources/<type>/<provider>/<provider>_test.go
Create
Provider tests
resources/resources.go
Modify
Add case to NewResourceFromProperties
cmd/ensure_<type>.go
Create
CLI command handler
cmd/ensure.go
Modify
Register CLI command
internal/fs/schemas/manifest.json
Modify
Add resource schema definitions
internal/fs/schemas/resource_ensure_request.json
Modify
Add API request schema
docs/content/resources/<type>.md
Create
User documentation
docs/content/design/<type>/_index.md
Create
Design documentation
docs/content/design/<type>/<provider>.md
Create
Provider documentation
Step 1: Model Definitions
Create model/resource_<type>.go with the following components.
Constants
const (
// ResourceStatus<Type>Protocol is the protocol identifier for <type> resource stateResourceStatus<Type>Protocol = "io.choria.ccm.v1.resource.<type>.state"// <Type>TypeName is the type name for <type> resources <Type>TypeName = "<type>")
Properties Struct
The properties struct must satisfy model.ResourceProperties:
type <Type>Metadatastruct {
Namestring`json:"name" yaml:"name"`Providerstring`json:"provider,omitempty" yaml:"provider,omitempty"`// Add fields describing current system state}
type <Type>Statestruct {
CommonResourceStateMetadata*<Type>Metadata`json:"metadata,omitempty"`}
Embedding *base.Base provides implementations for Apply(), Healthcheck(), Type(), Name(), Properties(), and NewTransactionEvent(). The type must implement:
See resources/archive/type.go for a complete constructor example.
ApplyResource Method
The ApplyResource method (part of base.EmbeddedResource) contains the core logic. It should follow this pattern:
Get initial state via provider.Status()
Check if already in desired state (implement isDesiredState() helper)
If stable, call t.FinalizeState() and return early
Apply changes, respecting t.mgr.NoopMode()
Get final state and verify desired state was achieved
Call t.FinalizeState() with appropriate flags
See resources/archive/type.go:ApplyResource() for a complete example.
Provider Selection Methods
The SelectProvider() method should use registry.FindSuitableProvider() to select an appropriate provider. See resources/archive/type.go for the standard implementation pattern.
if !noop {
// Make actual changest.log.Info("Applying changes")
err = p.SomeAction(ctx, properties)
} else {
t.log.Info("Skipping changes as noop")
noopMessage = "Would have applied changes"}
Error Handling
Use sentinel errors from model/errors.go:
var (
ErrResourceInvalid = errors.New("resource invalid")
ErrProviderNotFound = errors.New("provider not found")
ErrNoSuitableProvider = errors.New("no suitable provider")
ErrDesiredStateFailed = errors.New("desired state not achieved")
)
Wrap errors with context:
err:=os.Remove(path)
iferr!=nil {
returnfmt.Errorf("could not remove file: %w", err)
}
Template Resolution
The ResolveTemplates method (part of model.ResourceProperties) should resolve all user-facing string fields using templates.ResolveTemplateString(). Always call the embedded CommonResourceProperties.ResolveTemplates(env) first.
Provider Selection
Providers declare manageability via IsManageable on the factory (see model.ProviderFactory in Step 3). Multiple providers can match; the one with highest priority is selected.
Documentation
Create user documentation in docs/content/resources/<type>.md covering:
Overview and use cases
Ensure states table
Properties table with descriptions
Usage examples (manifest, CLI, API)
Create design documentation in docs/content/design/<type>/_index.md covering:
Provider interface specification
State checking logic
Apply logic flowchart
Create provider documentation in docs/content/design/<type>/<provider>.md covering:
Provider selection criteria
Platform requirements
Implementation details
CCM Studio
CCM Studio is a web-based manifest designer. After adding a new resource type, update CCM Studio to support it:
Note
CCM Studio is a closed-source project. The maintainers will complete this step.
Add the new resource type to the resource palette
Create property editors for type-specific fields
Add validation matching the JSON schema definitions
Update any resource type documentation or help text