How to write, register, compose, and test Converge blueprints, plus a complete reference for every built-in resource type with per-platform examples.
For real-world blueprints, see the blueprints/ directory, including CIS L1 benchmarks for Windows, Ubuntu, and macOS.
- What Is a Blueprint
- Writing a Blueprint
- Registering in main.go
- Platform-Conditional Logic
- Dependencies and Conditions
- Daemon Mode (
converge serve) - Blueprint Composition
- Testing Blueprints
- Resource Reference
- File (content, remote download, block management)
- Package, Service, Exec (shell, PowerShell, bash)
- User, Firewall, Reboot
- Template, Hostname, Cron, Repository
- KernelModule, Sysctl
- Registry, SecurityPolicy, AuditPolicy
- Plist
- InShard, Secret / Encrypted Config
- Conditions
A blueprint is a Go function with the signature func(r *dsl.Run). Inside the function, you call resource methods on r to declare what the system should look like. The function is registered by name with app.Register() and compiled into the binary.
Blueprints don't do anything directly. They declare intent. The engine diffs current state vs. declared state and applies only what's needed.
package blueprints
import "github.com/TsekNet/converge/dsl"
func Baseline(r *dsl.Run) {
p := r.Platform()
// Cross-platform: ensure git is installed everywhere
r.Package("git", dsl.PackageOpts{State: dsl.Present})
// Platform-specific system banner
switch p.OS {
case "linux":
r.File("/etc/motd", dsl.FileOpts{
Content: "Managed by Converge\n",
Mode: 0644,
})
r.Service("sshd", dsl.ServiceOpts{State: dsl.Running, Enable: true})
case "darwin":
r.File("/etc/motd", dsl.FileOpts{
Content: "Managed by Converge\n",
Mode: 0644,
})
case "windows":
r.File(`C:\ProgramData\Converge\motd.txt`, dsl.FileOpts{
Content: "Managed by Converge\r\n",
})
}
}This declares a cross-platform package, platform-specific banner files, and a Linux service. The engine handles the rest.
Here's a more complete example showing cross-platform logic, encrypted secrets, and canary rollouts:
package blueprints
import "github.com/TsekNet/converge/dsl"
func Baseline(r *dsl.Run) {
p := r.Platform()
// Cross-platform: install git everywhere
r.Package("git", dsl.PackageOpts{State: dsl.Present})
// Cross-platform: allow SSH inbound
r.Firewall("Allow SSH", dsl.FirewallOpts{Port: 22, Action: "allow"})
// Cross-platform: use encrypted secrets
enrollSecret := r.Secret("fleet.enroll_secret")
// Platform-specific config files
switch p.OS {
case "linux":
r.File("/etc/default/orbit", dsl.FileOpts{
Content: "ORBIT_ENROLL_SECRET=" + enrollSecret + "\n",
Mode: 0600,
})
r.Service("orbit", dsl.ServiceOpts{State: dsl.Running, Enable: true})
case "darwin":
r.File("/etc/orbit/secret", dsl.FileOpts{
Content: enrollSecret,
Mode: 0600,
})
case "windows":
r.File(`C:\Program Files\Orbit\secret`, dsl.FileOpts{
Content: enrollSecret,
})
}
// Canary rollout: 10% of fleet gets the new agent
if r.InShard(10) {
r.Package("new-monitoring-agent", dsl.PackageOpts{State: dsl.Present})
}
}package main
import (
"github.com/TsekNet/converge/dsl"
"github.com/myorg/myinfra/blueprints/myblueprint"
)
func main() {
app := dsl.New()
app.Register("myblueprint", "My system blueprint", myblueprint.Blueprint)
app.Execute()
}Register takes three arguments: name, description, and blueprint function. After building:
converge plan myblueprint # dry-run, show what would change
sudo converge serve myblueprint # run as persistent daemon
sudo converge serve myblueprint --timeout 1s # converge and exitYou can register as many blueprints as you want. Each becomes a subcommand target.
Blueprints are Go code, so platform branching is just if statements:
func Blueprint(r *dsl.Run) {
p := r.Platform()
switch p.OS {
case "linux":
r.File("/etc/motd", dsl.FileOpts{
Content: "Linux host managed by Converge\n",
})
case "darwin":
r.File("/etc/motd", dsl.FileOpts{
Content: "macOS host managed by Converge\n",
})
case "windows":
r.File(`C:\ProgramData\Converge\motd.txt`, dsl.FileOpts{
Content: "Windows host managed by Converge\r\n",
})
}
}r.Platform() returns a platform.Info struct with OS, Distro, Arch, PkgManager, and InitSystem.
Use p.Distro for distro-specific logic within the same OS:
func Blueprint(r *dsl.Run) {
p := r.Platform()
switch p.Distro {
case "ubuntu":
r.Package("apt-transport-https", dsl.PackageOpts{State: dsl.Present})
case "fedora":
r.Package("dnf-plugins-core", dsl.PackageOpts{State: dsl.Present})
}
}For platform-specific resources like r.Registry() (Windows), r.Sysctl() (Linux), or r.Plist() (macOS), use Go build tags on the blueprint file itself:
//go:build windows
package blueprints
import "github.com/TsekNet/converge/dsl"
func Windows(r *dsl.Run) {
r.Registry(`HKLM\SOFTWARE\MyOrg\Converge`, dsl.RegistryOpts{
Value: "Managed",
Type: "string",
Data: "true",
})
}The compiler enforces this: you can't call r.Registry() from a Linux-tagged file. No runtime "skipped" messages.
Auto-edges handle most dependency relationships automatically (Service->Package, File->parent Dir). For dependencies that auto-edges cannot detect, use typed resource conditions (e.g. condition.Package, condition.File) to declare explicit DAG edges. Use condition.Shell to gate resources on arbitrary command output. Combine conditions with condition.All, condition.Any, and condition.Not.
import (
"github.com/TsekNet/converge/condition"
"github.com/TsekNet/converge/dsl"
)
func Blueprint(r *dsl.Run) {
r.Package("postgresql", dsl.PackageOpts{State: dsl.Present})
r.Exec("db-migrate", dsl.ExecOpts{
Command: "/usr/bin/db-migrate",
Condition: condition.All(
condition.Package("postgresql"),
condition.Shell("test -f /var/lib/myapp/.migration-done"),
),
})
r.Service("myapp", dsl.ServiceOpts{
State: dsl.Running,
Enable: true,
Condition: condition.Exec("db-migrate"),
})
}This creates a three-layer DAG: package:postgresql -> exec:db-migrate -> service:myapp. In daemon mode, if the postgresql package drifts (e.g. gets uninstalled), converge reinstalls it and then walks the DAG forward to re-check the migration and service.
The daemon is the primary way to run converge in production. It performs an initial convergence, then watches for drift via OS events:
# Linux
sudo converge serve baseline
# macOS
sudo converge serve baseline
# Windows (elevated PowerShell)
converge.exe serve baselineWhat happens:
- The DAG is built with auto-edges and explicit typed resource conditions (e.g.
condition.Package,condition.File) dependencies - Initial convergence runs all resources in topological order
- Per-resource watchers start (inotify for files, dbus for services, etc.)
- When drift is detected, the drifted resource is re-checked and its DAG dependents are re-evaluated
- On failure, exponential backoff retries up to
--max-retries(default 3)
For CI/CD or image baking (Packer), use --timeout to converge and exit:
# CI pipeline
sudo converge serve baseline --timeout 1s --detailed-exit-codes
# Packer provisioner
sudo converge serve cis --timeout 1s
# Packer: converge and exit after 60s of stability
sudo converge serve baseline --timeout 60sSplit large blueprints and compose with r.Include():
func Blueprint(r *dsl.Run) {
r.Include("base") // calls the "base" blueprint
r.Include("security") // calls the "security" blueprint
r.Include("monitoring") // calls the "monitoring" blueprint
}Include calls another registered blueprint by name, injecting its resources into the current Run.
Blueprints are Go functions compiled into the binary. Test them with go test by registering the blueprint and building the DAG:
package blueprints
import (
"testing"
"github.com/TsekNet/converge/dsl"
)
func TestBaseline(t *testing.T) {
app := dsl.New()
app.Register("baseline", "test", Baseline)
// BuildGraph calls the blueprint, builds the DAG, and adds auto-edges.
// It returns an error if the blueprint declares duplicate resources,
// references missing dependencies, or fails validation.
_, err := app.BuildGraph("baseline")
if err != nil {
t.Fatalf("BuildGraph failed: %v", err)
}
}BuildGraph exercises the full blueprint: resource declaration, duplicate detection, dependency validation, and auto-edge wiring. No containers, no VMs, no network calls, no root required.
All option structs share these common fields directly on each Opts struct:
| Field | Type | Default | Description |
|---|---|---|---|
Critical |
bool |
false |
If true, failure aborts the run. Set true for resources whose failure should abort the run. |
Noop |
bool |
false |
Per-resource dry-run: Check only, skip Apply. |
Retry |
int |
0 |
Per-resource max retries. 0 = use daemon default (--max-retries). |
Limit |
float64 |
0 |
Per-resource rate limit (events/sec). 0 = use daemon default. |
AutoEdge |
*bool |
nil |
Set to falseVal := false; &falseVal to disable auto-edges for this resource. |
AutoGroup |
*bool |
nil |
Set to falseVal := false; &falseVal to disable auto-grouping for this resource. |
Condition |
extensions.Condition |
nil |
Gate on runtime state. See Conditions. |
Auto-edges: Dependencies are detected automatically: service:X depends on package:X, files depend on parent directories, services depend on config files. Use typed resource conditions (e.g. condition.Package, condition.File) for dependencies auto-edges cannot detect.
Auto-grouping: Package resources with the same manager and state are automatically batched into a single install/remove transaction (e.g., apt install git curl neovim instead of three separate calls). Set AutoGroup to a *bool pointing to false to opt out (e.g., falseVal := false; dsl.PackageOpts{State: dsl.Present, AutoGroup: &falseVal}).
Use typed resource conditions (e.g. condition.Package, condition.File) for dependencies auto-edges cannot detect:
import "github.com/TsekNet/converge/condition"
r.Exec("migrate", dsl.ExecOpts{
Command: "/usr/bin/db-migrate",
Condition: condition.Package("postgresql"),
})Manage file content, permissions, ownership, remote downloads, and tagged blocks within existing files. Three modes of operation, determined by which fields are set:
r.File(path string, opts dsl.FileOpts)| Field | Type | Default | Description |
|---|---|---|---|
Content |
string |
"" |
Desired file content. Empty string creates an empty file. |
Mode |
os.FileMode |
0644 |
POSIX permission bits. Ignored on Windows. |
Owner |
string |
"" |
File owner (username). No-op if empty. |
Group |
string |
"" |
File group. No-op if empty. |
Append |
bool |
false |
If true, appends Content instead of replacing. |
URL |
string |
"" |
When set, downloads from this URL instead of using Content. |
Checksum |
string |
"" |
Expected SHA-256 hex digest. Only used with URL. |
BlockName |
string |
"" |
When set, manages a tagged block within the file instead of owning the entire file. |
BlockComment |
string |
"#" |
Comment prefix for block markers. Only used with BlockName. |
Platform behavior: Full support on Linux/macOS. On Windows, Mode/Owner/Group are ignored.
Idempotency: Compares content byte-for-byte and stat metadata. No write if current state matches.
Content mode (default): write literal content or append.
// Linux: system banner
r.File("/etc/motd", dsl.FileOpts{
Content: "Managed by Converge\n",
Mode: 0644,
})
// macOS: LaunchDaemon plist
r.File("/Library/LaunchDaemons/com.example.agent.plist", dsl.FileOpts{
Content: agentPlist,
Mode: 0644,
Owner: "root",
Group: "wheel",
})
// Windows: config file (Mode/Owner/Group ignored)
r.File(`C:\ProgramData\MyApp\config.json`, dsl.FileOpts{
Content: `{"managed": true}`,
})
// Append to an existing file (all platforms)
r.File("/etc/hosts", dsl.FileOpts{
Content: "10.0.0.5 internal.example.com\n",
Append: true,
})Remote download mode (URL set): downloads a URL to a local path. Optional SHA-256 checksum verification: if the checksum mismatches after download, the file is removed and Apply fails.
r.File("/opt/tools/binary", dsl.FileOpts{
URL: "https://releases.example.com/v1.2/tool-linux-amd64",
Checksum: "e3b0c44298fc1c149afbf4c8996fb924...",
Mode: 0755,
})Block mode (BlockName set): manages a tagged block within an existing file, leaving surrounding content untouched. Blocks are delimited by sentinel markers:
# BEGIN converge:<name>
<managed content>
# END converge:<name>
r.File("/etc/hosts", dsl.FileOpts{
Content: "10.0.0.1 api.internal\n10.0.0.2 db.internal",
BlockName: "internal-dns",
})Install or remove packages via the detected system package manager.
r.Package(name string, opts dsl.PackageOpts)| Field | Type | Default | Description |
|---|---|---|---|
State |
dsl.ResourceState |
dsl.Present |
Present to install, Absent to remove. |
| Platform | Package Manager |
|---|---|
| Linux (Debian/Ubuntu) | apt |
| Linux (RHEL/Fedora) | dnf / yum |
| Linux (SUSE) | zypper |
| Linux (Alpine) | apk |
| Linux (Arch) | pacman |
| macOS | brew |
| Windows | choco / winget |
Idempotency: Queries the package manager before acting. Installing an already-installed package or removing a missing one is a no-op.
Examples:
// Linux (Ubuntu/Debian): install via apt
r.Package("curl", dsl.PackageOpts{State: dsl.Present})
// Linux (Fedora): install via dnf (auto-detected)
r.Package("httpd", dsl.PackageOpts{State: dsl.Present})
// macOS: install via brew
r.Package("jq", dsl.PackageOpts{State: dsl.Present})
// Windows: install via choco
r.Package("7zip", dsl.PackageOpts{State: dsl.Present})
// Remove a package (all platforms)
r.Package("telnet", dsl.PackageOpts{State: dsl.Absent})Manage service runtime state, boot-time enablement, and startup type.
r.Service(name string, opts dsl.ServiceOpts)| Field | Type | Default | Description |
|---|---|---|---|
State |
dsl.ServiceState |
dsl.Running |
Running or Stopped. |
Enable |
bool |
false |
If true, enable the service to start at boot. |
StartupType |
string |
"" |
Windows SCM startup type: "auto", "delayed-auto", "manual", "disabled". |
| Platform | Init System | API |
|---|---|---|
| Linux | systemd |
systemctl |
| macOS | launchd |
stub (not yet implemented) |
| Windows | Windows SCM | golang.org/x/sys/windows/svc/mgr (native Win32) |
macOS note: launchd is not yet implemented natively. Use
r.Exec()withlaunchctlas a workaround.
Idempotency: Checks current state before acting. Starting a running service is a no-op.
Examples:
// Linux: enable and start sshd via systemd
r.Service("sshd", dsl.ServiceOpts{
State: dsl.Running,
Enable: true,
})
// Linux: stop and disable a service
r.Service("cups", dsl.ServiceOpts{
State: dsl.Stopped,
Enable: false,
})
// Windows: set a service to delayed auto-start
r.Service("wuauserv", dsl.ServiceOpts{
State: dsl.Running,
Enable: true,
StartupType: "delayed-auto",
})
// Windows: disable Windows Update service
r.Service("wuauserv", dsl.ServiceOpts{
State: dsl.Stopped,
StartupType: "disabled",
})Run arbitrary commands. Use sparingly: prefer declarative resources when they exist.
r.Exec(name string, opts dsl.ExecOpts)| Field | Type | Default | Description |
|---|---|---|---|
Command |
string |
"" |
The command to execute (or script body when Shell is set). Required. |
Args |
[]string |
nil |
Arguments passed to the command. Ignored when Shell is set. |
Shell |
string |
"" |
"auto" (bash on Linux/macOS, powershell on Windows), "powershell", "pwsh", "cmd", "bash", "sh", or custom path. |
ShellParams |
[]string |
nil |
When set, replaces the default shell flags. PowerShell default: -NoProfile -NonInteractive -ExecutionPolicy Bypass. |
Dir |
string |
"" |
Working directory. Empty = inherit. |
Env |
[]string |
nil |
Additional environment variables (KEY=VALUE format). |
Retries |
int |
0 |
Number of retry attempts on failure. |
RetryDelay |
time.Duration |
0 |
Delay between retries. |
Idempotency: Not inherently idempotent. Always use condition.Shell("...") on the Condition field to make it conditional.
Examples:
import "github.com/TsekNet/converge/condition"
// Linux: run a script only if a marker file is missing
r.Exec("bootstrap", dsl.ExecOpts{
Command: "/usr/local/bin/bootstrap.sh",
Condition: condition.Shell("test -f /var/lib/myapp/.bootstrapped"),
})
// macOS: flush DNS cache
r.Exec("flush-dns", dsl.ExecOpts{
Command: "/usr/bin/dscacheutil",
Args: []string{"-flushcache"},
})
// Windows: enable TLS 1.2 (condition checks if already set)
r.Exec("enable-tls12", dsl.ExecOpts{
Command: "powershell.exe",
Args: []string{"-NoProfile", "-Command", "New-ItemProperty -Path 'HKLM:\\SYSTEM\\...' -Name Enabled -Value 1"},
Condition: condition.Shell(`powershell.exe -NoProfile -Command "(Get-ItemProperty 'HKLM:\SYSTEM\...').Enabled -eq 1"`),
})
// Retry on transient failure (all platforms)
r.Exec("download-agent", dsl.ExecOpts{
Command: "curl",
Args: []string{"-fsSL", "-o", "/tmp/agent.tar.gz", "https://example.com/agent.tar.gz"},
Retries: 3,
RetryDelay: 5 * time.Second,
Condition: condition.Shell("test -f /tmp/agent.tar.gz"),
})
// PowerShell: enable Windows feature (5.1, default path)
r.Exec("enable-wsl", dsl.ExecOpts{
Shell: "powershell",
Command: "Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux -NoRestart",
Condition: condition.Shell("(Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux).State").In("powershell").Match("Enabled"),
})
// PowerShell 7+: multi-line script
r.Exec("configure-defender", dsl.ExecOpts{
Shell: "pwsh",
Command: `
$prefs = Get-MpPreference
if ($prefs.DisableRealtimeMonitoring) {
Set-MpPreference -DisableRealtimeMonitoring $false
}
`,
Condition: condition.Shell("(Get-MpPreference).DisableRealtimeMonitoring").In("pwsh").Match("False"),
})
// PowerShell with custom parameters
r.Exec("run-with-profile", dsl.ExecOpts{
Shell: "powershell",
ShellParams: []string{"-ExecutionPolicy", "RemoteSigned"},
Command: "Import-Module MyModule; Invoke-Setup",
})
// Bash: multi-line script with output-based guard
r.Exec("set-timezone", dsl.ExecOpts{
Shell: "bash",
Command: "timedatectl set-timezone America/Los_Angeles",
Condition: condition.Shell("timedatectl show --property=Timezone --value").Match("America/Los_Angeles"),
})Create and manage local user accounts and group membership.
r.User(name string, opts dsl.UserOpts)| Field | Type | Default | Description |
|---|---|---|---|
Groups |
[]string |
nil |
Supplementary groups the user should belong to. |
Shell |
string |
"" |
Login shell path. No-op if empty. |
Home |
string |
"" |
Home directory path. Uses OS default if empty. |
System |
bool |
false |
If true, creates a system account (low UID, no home by default). |
| Platform | Tooling |
|---|---|
| Linux | useradd / usermod |
| macOS | dscl / Directory Services |
| Windows | net user / net localgroup (Shell and System ignored) |
Idempotency: Creates user if missing. Modifies only divergent attributes if user exists.
Examples:
// Linux: create app user with specific shell and groups
r.User("deploy", dsl.UserOpts{
Shell: "/bin/bash",
Home: "/home/deploy",
Groups: []string{"docker", "sudo"},
})
// Linux: system account (low UID, no home)
r.User("prometheus", dsl.UserOpts{
System: true,
Shell: "/usr/sbin/nologin",
})
// macOS: add user to admin group
r.User("admin", dsl.UserOpts{
Groups: []string{"admin", "staff"},
})
// Windows: add user to local Administrators (Shell/System ignored)
r.User("svc-agent", dsl.UserOpts{
Groups: []string{"Administrators"},
})Manage Windows registry keys and values via native Win32 API. Available only in //go:build windows blueprints.
r.Registry(key string, opts dsl.RegistryOpts)The key is the full registry path, e.g., HKLM\SOFTWARE\MyApp.
| Field | Type | Default | Description |
|---|---|---|---|
Value |
string |
"" |
The registry value name under the key. |
Type |
string |
"sz" |
"sz", "dword", "qword", "expandstring", "multistring", "binary". Also accepts "REG_DWORD" etc. |
Data |
any |
nil |
The data to set. Type must match Type. |
State |
dsl.ResourceState |
dsl.Present |
Present to create/set, Absent to delete the value. |
Supported root keys: HKLM, HKCU, HKCR, HKU, HKCC (and their long forms like HKEY_LOCAL_MACHINE).
API: golang.org/x/sys/windows/registry: no reg.exe.
Idempotency: Reads current value via type-appropriate getter and compares. Creates intermediate keys if needed without disturbing existing sibling values.
Examples:
//go:build windows
// Set a string value
r.Registry(`HKLM\SOFTWARE\MyOrg`, dsl.RegistryOpts{
Value: "Environment",
Type: "sz",
Data: "production",
})
// Set a DWORD (integer) value
r.Registry(`HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU`, dsl.RegistryOpts{
Value: "NoAutoUpdate",
Type: "dword",
Data: uint32(1),
})
// Delete a registry value
r.Registry(`HKLM\SOFTWARE\MyOrg`, dsl.RegistryOpts{
Value: "DeprecatedSetting",
State: dsl.Absent,
})Manage Windows local security policy settings via native Win32 APIs. Available only in //go:build windows blueprints.
r.SecurityPolicy(name string, opts dsl.SecurityPolicyOpts)| Field | Type | Default | Description |
|---|---|---|---|
Category |
string |
"" |
"password" or "lockout". Required. |
Key |
string |
"" |
Setting name. Required. |
Value |
string |
"" |
Desired value as string. |
Password policy keys (Category: "password"):
| Key | Description |
|---|---|
MinimumPasswordLength |
Minimum password length |
MaximumPasswordAge |
Maximum password age (seconds) |
MinimumPasswordAge |
Minimum password age (seconds) |
PasswordHistorySize |
Number of remembered passwords |
ForceLogoff |
Force logoff time (seconds) |
Lockout policy keys (Category: "lockout"):
| Key | Description |
|---|---|
LockoutThreshold |
Failed logon attempts before lockout |
LockoutDuration |
Lockout duration (seconds) |
LockoutObservationWindow |
Observation window (seconds) |
API: netapi32.dll NetUserModalsGet/Set: no secedit.exe.
Examples:
//go:build windows
// Require 12-character minimum passwords
r.SecurityPolicy("min-password-length", dsl.SecurityPolicyOpts{
Category: "password",
Key: "MinimumPasswordLength",
Value: "12",
})
// Lock accounts after 5 failed attempts for 30 minutes
r.SecurityPolicy("lockout-threshold", dsl.SecurityPolicyOpts{
Category: "lockout",
Key: "LockoutThreshold",
Value: "5",
})
r.SecurityPolicy("lockout-duration", dsl.SecurityPolicyOpts{
Category: "lockout",
Key: "LockoutDuration",
Value: "1800",
})Manage Windows advanced audit policy via native Win32 APIs. Available only in //go:build windows blueprints.
r.AuditPolicy(name string, opts dsl.AuditPolicyOpts)| Field | Type | Default | Description |
|---|---|---|---|
Subcategory |
string |
"" |
Audit subcategory name (case-insensitive). Required. |
Success |
bool |
false |
Enable success auditing. |
Failure |
bool |
false |
Enable failure auditing. |
Supported subcategories (59 total, organized by category):
| Category | Subcategories |
|---|---|
| Account Logon | Credential Validation, Kerberos Authentication Service, Kerberos Service Ticket Operations, Other Account Logon Events |
| Account Management | Application Group Management, Computer Account Management, Distribution Group Management, Other Account Management Events, Security Group Management, User Account Management |
| Detailed Tracking | DPAPI Activity, Plug and Play Events, Process Creation, Process Termination, RPC Events, Token Right Adjusted Events |
| DS Access | Directory Service Access, Directory Service Changes, Directory Service Replication, Detailed Directory Service Replication |
| Logon/Logoff | Account Lockout, Group Membership, IPsec Extended Mode, IPsec Main Mode, IPsec Quick Mode, Logoff, Logon, Network Policy Server, Other Logon/Logoff Events, Special Logon, User / Device Claims |
| Object Access | Application Generated, Central Policy Staging, Certification Services, Detailed File Share, File Share, File System, Filtering Platform Connection, Filtering Platform Packet Drop, Handle Manipulation, Kernel Object, Other Object Access Events, Registry, Removable Storage, SAM |
| Policy Change | Audit Policy Change, Authentication Policy Change, Authorization Policy Change, Filtering Platform Policy Change, MPSSVC Rule-Level Policy Change, Other Policy Change Events |
| Privilege Use | Non Sensitive Privilege Use, Other Privilege Use Events, Sensitive Privilege Use |
| System | IPsec Driver, Other System Events, Security State Change, Security System Extension, System Integrity |
API: advapi32.dll AuditQuerySystemPolicy/AuditSetSystemPolicy: no auditpol.exe.
Examples:
//go:build windows
// Audit successful and failed logon attempts
r.AuditPolicy("audit-logon", dsl.AuditPolicyOpts{
Subcategory: "Logon",
Success: true,
Failure: true,
})
// Audit process creation (for security monitoring)
r.AuditPolicy("audit-process-creation", dsl.AuditPolicyOpts{
Subcategory: "Process Creation",
Success: true,
})Manage Linux kernel parameters via /proc/sys/. Available only in //go:build linux blueprints.
r.Sysctl(key string, opts dsl.SysctlOpts)The key uses dotted notation, e.g., net.ipv4.ip_forward.
| Field | Type | Default | Description |
|---|---|---|---|
Value |
string |
"" |
Desired kernel parameter value. Required. |
Persist |
bool |
false |
If true, writes to /etc/sysctl.d/99-converge.conf so the setting survives reboots. Set true for reboot persistence. |
API: Direct file I/O to /proc/sys/: no sysctl command.
Idempotency: Reads the live kernel value from /proc/sys/<key> and compares. Writes only on mismatch.
Examples:
//go:build linux
// Enable IP forwarding
r.Sysctl("net.ipv4.ip_forward", dsl.SysctlOpts{
Value: "1",
})
// Harden TCP SYN cookies
r.Sysctl("net.ipv4.tcp_syncookies", dsl.SysctlOpts{
Value: "1",
})
// Set without persisting to disk (takes effect until reboot only)
r.Sysctl("vm.swappiness", dsl.SysctlOpts{
Value: "10",
Persist: false,
})Manage macOS preference domain keys via native binary plist encoding. Available only in //go:build darwin blueprints.
r.Plist(domain string, opts dsl.PlistOpts)The domain is the preference domain, e.g., com.apple.screensaver.
| Field | Type | Default | Description |
|---|---|---|---|
Key |
string |
"" |
The preference key to set. Required. |
Value |
any |
nil |
Desired value. Type inferred from Go type or Type field. |
Type |
string |
"" |
Explicit type hint: "bool", "int", "float", "string". |
Host |
bool |
false |
If true, targets /Library/Preferences (system-wide). If false, targets ~/Library/Preferences. |
API: howett.net/plist for binary plist encode/decode: no defaults command.
Idempotency: Reads the plist file, decodes, and compares the key's current value. Writes only on mismatch using read-modify-write to preserve other keys.
Examples:
//go:build darwin
// Disable screensaver password grace period (system-wide)
r.Plist("com.apple.screensaver", dsl.PlistOpts{
Key: "askForPasswordDelay",
Value: 0,
Type: "int",
Host: true,
})
// Set Finder preferences (user-level)
r.Plist("com.apple.finder", dsl.PlistOpts{
Key: "AppleShowAllExtensions",
Value: true,
Type: "bool",
})
// Disable Gatekeeper assessments
r.Plist("com.apple.LaunchServices", dsl.PlistOpts{
Key: "LSQuarantine",
Value: false,
Type: "bool",
Host: true,
})Manage host firewall rules. Cross-platform: available in all blueprints.
r.Firewall(name string, opts dsl.FirewallOpts)| Field | Type | Default | Description |
|---|---|---|---|
Port |
int |
(required) | Port number (1-65535). |
Protocol |
string |
"tcp" |
"tcp" or "udp". |
Direction |
string |
"inbound" |
"inbound" or "outbound". |
Action |
string |
"allow" |
"allow" or "block". |
Source |
string |
"" |
Source IP or CIDR. Empty = any. |
Dest |
string |
"" |
Destination IP or CIDR. Empty = any. |
State |
dsl.ResourceState |
dsl.Present |
Present to create, Absent to delete. |
| Platform | Backend | API |
|---|---|---|
| Linux | nftables (IPv4 only) | github.com/google/nftables (netlink) |
| macOS | pf (anchor-based) | /etc/pf.anchors/converge + pfctl reload |
| Windows | Windows Firewall (registry) | HKLM\...\FirewallRules + SCM notify |
Input validation: All fields are validated at construction time. Invalid names, out-of-range ports, and malformed source/dest addresses are rejected with a panic.
Idempotency: Checks if a matching rule exists before creating. On Linux, rules are identified by nftables UserData tag. On macOS, by a pf comment tag. On Windows, by the registry value name and content.
Examples:
// Allow SSH inbound (all platforms)
r.Firewall("Allow SSH", dsl.FirewallOpts{
Port: 22,
Protocol: "tcp",
Action: "allow",
})
// Block outbound to a specific port
r.Firewall("Block SMTP out", dsl.FirewallOpts{
Port: 25,
Direction: "outbound",
Action: "block",
})
// Allow inbound from a specific subnet only
r.Firewall("Allow monitoring", dsl.FirewallOpts{
Port: 9090,
Source: "10.0.0.0/8",
Action: "allow",
})
// Remove a rule that was previously managed
r.Firewall("Legacy RDP", dsl.FirewallOpts{
Port: 3389,
State: dsl.Absent,
})Schedule a platform-native OS reboot and track whether it has occurred. Available on all platforms.
r.Reboot(name string, opts dsl.RebootOpts)| Field | Type | Default | Description |
|---|---|---|---|
Reason |
string |
"" |
Internal reason, written to the sentinel and used in log output. |
Message |
string |
"" |
User-facing description shown in converge output before the reboot fires (all platforms). |
Delay |
time.Duration |
0 |
How long before the OS fires the reboot. On Windows, the OS handles the countdown asynchronously (minimum 1s); on Linux/macOS, the daemon waits (context-cancellable). |
| Platform | API |
|---|---|
| Windows | InitiateSystemShutdownExW (advapi32.dll) with SE_SHUTDOWN_PRIVILEGE |
| Linux | unix.Reboot(LINUX_REBOOT_CMD_RESTART) |
| macOS | unix.Reboot(RB_AUTOBOOT) + kern.boottime via sysctl |
Idempotency: Apply writes a second-precision sentinel to /var/lib/converge/reboot-<name>.sentinel (Linux/macOS) or C:\ProgramData\converge\reboot-<name>.sentinel (Windows) before triggering the reboot. Check compares the sentinel timestamp to the current system boot time (with a 2-second grace window for clock imprecision). If the machine booted after the sentinel was written, Check returns compliant, and the resource is a no-op on all future runs.
Cancelled reboots: If a reboot is aborted (e.g., AbortSystemShutdown on Windows), the sentinel remains on disk. After 10 minutes without a completed reboot, Apply removes the stale sentinel before writing a fresh one, recovering automatically. To manually recover, delete the sentinel file.
Daemon continuation: When converge is registered as a system service (the typical production setup), it starts automatically after the reboot. Resources that depend on the reboot resource proceed once the boot-time comparison passes.
Examples:
import "github.com/TsekNet/converge/condition"
// Reboot after installing a kernel module, then gate post-reboot setup
// on a device node that only appears after the driver loads.
r.Exec("install-driver", dsl.ExecOpts{
Command: "/usr/bin/dkms install mydriver/1.0",
Condition: condition.Shell("test ! -d /sys/module/mydriver"),
})
r.Reboot("driver-install", dsl.RebootOpts{
Reason: "Kernel module requires reboot to activate",
Message: "Rebooting to load newly installed driver.",
Delay: 30 * time.Second,
Condition: condition.Exec("install-driver"),
})
r.Exec("configure-driver", dsl.ExecOpts{
Command: "/usr/local/bin/configure-driver",
Condition: condition.All(
condition.Reboot("driver-install"),
condition.FileExists("/dev/mydriver0"),
),
})r.Reboot() is cross-platform. The //go:build windows tag below is required because of condition.RegistryValueExists, not because of r.Reboot() itself. Pair with registry conditions to gate post-reboot work on state the OS or an installer writes after restart (no polling, uses RegNotifyChangeKeyValue):
//go:build windows
r.Exec("install-agent", dsl.ExecOpts{
Command: `C:\installers\agent-setup.exe /quiet`,
Condition: condition.Shell(`powershell -NoProfile -Command "!(Test-Path 'C:\Program Files\MyAgent\agent.exe')"`),
})
r.Reboot("agent-install", dsl.RebootOpts{
Reason: "Agent installation requires reboot to complete service registration",
Message: "This device will restart in 30 seconds to finish agent setup.",
Delay: 30 * time.Second,
Condition: condition.Exec("install-agent"),
})
r.Exec("configure-agent", dsl.ExecOpts{
Command: `C:\Program Files\MyAgent\agent.exe --configure`,
// Block until the reboot completes and the agent's registry key appears.
Condition: condition.All(
condition.Reboot("agent-install"),
condition.RegistryValueExists(
`HKLM\SOFTWARE\MyOrg\Agent`,
"InstallComplete",
),
),
})Renders a Go text/template to a file. Variables come from the Vars map. Missing keys are a build error (missingkey=error).
r.Template("/etc/nginx/nginx.conf", dsl.TemplateOpts{
Source: "server {\n listen {{ .Port }};\n server_name {{ .Host }};\n}\n",
Vars: map[string]string{"Port": "8080", "Host": "example.com"},
Mode: 0644,
})Ensures the system hostname matches the desired value. Uses sethostname(2) on Linux/macOS and SetComputerNameExW on Windows.
r.Hostname("web01.example.com", dsl.HostnameOpts{})Loads or blacklists a kernel module. Blacklisted modules are unloaded and added to /etc/modprobe.d/converge-blacklist.conf. Requires //go:build linux on the blueprint file.
r.KernelModule("cramfs", dsl.KernelModuleOpts{State: dsl.ModuleBlacklisted})
r.KernelModule("vfat", dsl.KernelModuleOpts{State: dsl.ModuleLoaded})Manages cron jobs on Linux/macOS (writes to /etc/cron.d/) and Windows Task Scheduler entries (via COM API). On Windows, uses the Task Scheduler 2.0 COM interface directly, no shell-outs.
r.Cron("nightly-backup", dsl.CronOpts{
Schedule: "0 2 * * *",
Command: "/usr/local/bin/backup.sh",
User: "root",
})Manages package repository sources: APT sources in /etc/apt/sources.list.d/ and DNF/YUM repos in /etc/yum.repos.d/.
r.Repository("google-chrome", dsl.RepositoryOpts{
URI: "https://dl.google.com/linux/chrome/deb/",
Distribution: "stable",
Components: "main",
GPGKey: "https://dl.google.com/linux/linux_signing_key.pub",
})Percentage-based rollout sharding. Not a resource (no Check/Apply), but a DSL helper for conditional logic in blueprints.
// Roll out a new package to 10% of the fleet
if r.InShard(10) {
r.Package("new-agent", dsl.PackageOpts{State: dsl.Present})
}
// Canary a config change: 5% first, then expand to 50%, then 100%
if r.InShard(5) {
r.File("/etc/myapp/experimental.conf", dsl.FileOpts{
Content: "feature_v2=true\n",
})
}
// Use InShardWithSerial in tests to verify shard logic
// (the serial is normalized the same way as InShard)
if r.InShardWithSerial(10, "TEST-SERIAL") {
// This runs deterministically in tests
}The shard is computed from the first 7 characters of the hardware serial number via SHA-256. The same machine always lands in the same bucket across runs. Known placeholder serials (Not Specified, To Be Filled By O.E.M., etc.) are rejected, causing InShard to return false.
| Method | Signature | Description |
|---|---|---|
InShard |
(percent int) bool |
Uses auto-detected hardware serial. |
InShardWithSerial |
(percent int, serial string) bool |
Uses an explicit serial (for testing). The serial is normalized identically to InShard (trimmed, placeholder-checked, truncated to 7 chars). |
ShardBucket |
(serial string) uint64 |
Returns the shard bucket [0, 100) for a given serial. Package-level function, useful for testing. |
| Platform | Serial Source |
|---|---|
| Linux | /sys/class/dmi/id/product_serial |
| macOS | kern.uuid via /usr/sbin/sysctl (hardware UUID) |
| Windows | HKLM\HARDWARE\DESCRIPTION\System\BIOS\SerialNumber registry |
Retrieve config values from Go maps registered at compile time. Values wrapped in ENC[AES256:...] are decrypted transparently with AES-256-GCM.
// In a config file (e.g., config.go):
func init() {
dsl.RegisterConfig(map[string]any{
"fleet": map[string]any{
"server_url": "https://fleet.example.com",
"enroll_secret": "ENC[AES256:base64ciphertext...]",
},
})
}
// In a blueprint:
func Blueprint(r *dsl.Run) {
secret := r.Secret("fleet.enroll_secret") // decrypted automatically
url := r.Secret("fleet.server_url") // plain value returned as-is
}Key management: Call dsl.SetConfigKey("your-key") once at startup, typically sourced from an environment variable. The key is SHA-256 hashed to 32 bytes, so use a high-entropy random key (not a passphrase). Generate encrypted values with val, err := dsl.Encrypt("plaintext").
Fail-closed: If the key is missing or decryption fails, Secret returns an empty string (never leaks ciphertext).
No external deps: Uses Go stdlib crypto/aes + crypto/cipher (AES-256-GCM with random nonces).
Conditions gate resource convergence on system state. The daemon skips the resource until the condition is true, then triggers convergence. Ongoing drift detection continues normally after the condition is first satisfied.
import "github.com/TsekNet/converge/condition"r.Firewall("allow-internal", dsl.FirewallOpts{
Port: 8443, Protocol: "tcp", Action: "allow",
// Only apply when tun0 (VPN) interface is up.
Condition: condition.NetworkInterface("tun0"),
})r.File("/mnt/nfs/config/app.conf", dsl.FileOpts{Content: appConfig})
r.Service("app", dsl.ServiceOpts{
State: dsl.Running,
// Wait for NFS mount and config file before managing the service.
Condition: condition.All(
condition.MountPoint("/mnt/nfs"),
condition.File("/mnt/nfs/config/app.conf"),
),
})r.Exec("enroll-cert", dsl.ExecOpts{
Command: "/usr/local/bin/enroll",
// Wait for the CA bundle to be placed by provisioning.
Condition: condition.FileExists("/etc/ssl/ca-bundle.crt"),
})r.File("/etc/apt/apt.conf.d/99proxy", dsl.FileOpts{Content: proxyConf})
r.Package("curl", dsl.PackageOpts{
State: dsl.Present,
// Only install packages once the proxy is reachable and config is in place.
Condition: condition.All(
condition.NetworkReachable("proxy.corp.example.com", 3128),
condition.File("/etc/apt/apt.conf.d/99proxy"),
),
})Gate convergence on Windows registry state. Uses RegNotifyChangeKeyValue so no polling occurs while waiting.
//go:build windows
// Gate on key existence (any subkey or value creation under this path)
r.Exec("post-install-step", dsl.ExecOpts{
Command: `C:\Program Files\MyApp\setup.exe --post-install`,
Condition: condition.RegistryKeyExists(`HKLM\SOFTWARE\MyOrg\MyApp`),
})
// Gate on a specific value existing under a key
r.File(`C:\ProgramData\MyApp\config.json`, dsl.FileOpts{
Content: `{"mode": "managed"}`,
Condition: condition.RegistryValueExists(
`HKLM\SOFTWARE\MyOrg\MyApp`,
"SetupComplete",
),
})
// Gate on a value equaling a specific string or integer
r.Exec("activate-feature", dsl.ExecOpts{
Command: `C:\Program Files\MyApp\activate.exe`,
Condition: condition.RegistryValueEquals(
`HKLM\SOFTWARE\MyOrg\MyApp`,
"LicenseState",
"active",
),
})All three constructors return *registryCondition, which implements extensions.Condition. They are available only in //go:build windows blueprint files.
| Constructor | Satisfied when |
|---|---|
RegistryKeyExists(key) |
The registry key exists (regardless of values) |
RegistryValueExists(key, value) |
The named value exists under key |
RegistryValueEquals(key, value, data) |
The named value exists and its data equals fmt.Sprintf("%v", data) |
If the target key does not yet exist, Wait walks up the key path to find the nearest existing ancestor and watches the subtree with bWatchSubtree=true.
condition.Shell runs a command in the platform-default shell (bash on Linux/macOS, powershell on Windows). Exit code 0 means the condition is met. Chain .Match("expected") to compare trimmed stdout instead of exit code. Chain .In("shell") to override the shell.
import "github.com/TsekNet/converge/condition"
// Exit-code guard: met when the command exits 0
r.Exec("bootstrap", dsl.ExecOpts{
Command: "/usr/local/bin/bootstrap.sh",
Condition: condition.Shell("test -f /var/lib/myapp/.bootstrapped"),
})
// Output-match guard: met when stdout matches "Enabled"
r.Exec("enable-wsl", dsl.ExecOpts{
Shell: "powershell",
Command: "Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux -NoRestart",
Condition: condition.Shell("(Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux).State").In("powershell").Match("Enabled"),
})
// Explicit shell override with output match
r.Exec("set-timezone", dsl.ExecOpts{
Shell: "bash",
Command: "timedatectl set-timezone America/Los_Angeles",
Condition: condition.Shell("timedatectl show --property=Timezone --value").In("bash").Match("America/Los_Angeles"),
})typed resource conditions (e.g. condition.Package, condition.File) declares an explicit DAG dependency on another resource by its ID (e.g., "package:nginx", "exec:migrate"). The condition is met when the referenced resource has completed successfully. This replaces the former DependsOn field.
import "github.com/TsekNet/converge/condition"
r.Package("postgresql", dsl.PackageOpts{State: dsl.Present})
r.Exec("migrate", dsl.ExecOpts{
Command: "/usr/bin/db-migrate",
Condition: condition.Package("postgresql"),
})Combine conditions with condition.All (AND), condition.Any (OR), and condition.Not (inversion).
import "github.com/TsekNet/converge/condition"
// All: both conditions must be met
r.Service("app", dsl.ServiceOpts{
State: dsl.Running,
Condition: condition.All(
condition.MountPoint("/mnt/nfs"),
condition.File("/mnt/nfs/config/app.conf"),
),
})
// Any: at least one condition must be met
r.Exec("fetch-config", dsl.ExecOpts{
Command: "/usr/local/bin/fetch-config",
Condition: condition.Any(
condition.NetworkReachable("primary.example.com", 443),
condition.NetworkReachable("fallback.example.com", 443),
),
})
// Not: invert a condition
r.Exec("setup-vpn", dsl.ExecOpts{
Command: "/usr/local/bin/setup-vpn",
Condition: condition.Not(condition.NetworkInterface("tun0")),
})
// Nested: All with Not and Any
r.Exec("complex-gate", dsl.ExecOpts{
Command: "/usr/local/bin/deploy",
Condition: condition.All(
condition.Package("myapp"),
condition.Not(condition.Shell("pgrep -x deploy")),
condition.Any(
condition.FileExists("/etc/myapp/config.yaml"),
condition.FileExists("/etc/myapp/config.json"),
),
),
})| Constructor | Platform | Satisfied when | Wait mechanism |
|---|---|---|---|
condition.Shell("cmd") |
All | Command exits 0 (platform-default shell) | 5s poll |
condition.Shell("cmd").Match("str") |
All | Command stdout matches str |
5s poll |
condition.Shell("cmd").In("shell").Match("str") |
All | Command stdout matches str (explicit shell) |
5s poll |
condition.Package("name") |
All | Referenced resource completed successfully | DAG engine (no poll) |
condition.All(a, b, ...) |
All | All sub-conditions are met | Delegates to sub-conditions |
condition.Any(a, b, ...) |
All | At least one sub-condition is met | Delegates to sub-conditions |
condition.Not(a) |
All | Sub-condition is NOT met | Delegates to sub-condition |
condition.NetworkInterface(name) |
All | Named interface exists and is up | netlink RTMGRP_LINK (Linux), NotifyIpInterfaceChange (Windows), 2s poll (macOS) |
condition.NetworkReachable(host, port) |
All | TCP connect to host:port succeeds | 5s poll (no kernel event for TCP reachability) |
condition.MountPoint(path) |
All | path is on a different device than its parent | inotify on /proc/self/mountinfo (Linux), kqueue on / (macOS), 5s poll (Windows) |
condition.FileExists(path) |
All | os.Stat(path) succeeds |
inotify on parent dir (Linux), kqueue on parent (macOS), ReadDirectoryChangesW (Windows) |
condition.RegistryKeyExists(key) |
Windows | Registry key exists | RegNotifyChangeKeyValue on nearest existing ancestor |
condition.RegistryValueExists(key, value) |
Windows | Named value exists under key | RegNotifyChangeKeyValue on nearest existing ancestor |
condition.RegistryValueEquals(key, value, data) |
Windows | Named value's data equals the string form of data | RegNotifyChangeKeyValue on nearest existing ancestor |
All resource types have typed constructors. See condition/resource.go for the full list.