Skip to main content

Desired-state infrastructure convergence engine

Project description

CI Crates.io PyPI License: MIT codecov

verg

Desired-state infrastructure convergence engine. A fast, stateless alternative to Ansible, built in Rust.

Pre-1.0: verg is under active development. Expect breaking changes between minor versions.

Features

  • Fast - pushes a single static binary to each target over SSH; the agent executes locally, so there are no per-task SSH round-trips and no Python dependency on the target.
  • Stateless - every run reads actual system state and converges to desired state. No state files are kept on the control host between runs.
  • Dynamic inventory - generate hosts from a command (cloud, CMDB, scripts) and merge them with static hosts.
  • Simple - declare state in TOML files with Jinja2 templating for dynamic values. Strict config validation catches typos before any SSH connection is opened.
  • Agent-friendly - --json output, verg schema for machine-readable resource schemas, and structured exit codes make verg easy to drive from scripts and AI agents.
  • Secure by default - agent binary is checksum-verified before and after transfer; strict SSH host key checking; config validated locally before touching any target.
  • Continuous enforcement - agents pull and re-converge on a schedule with no central server. See docs/continuous-enforcement.md.
  • Encrypted secrets - secrets are encrypted at rest with age, decrypted on the control host at build time, and injected under the secret.* template namespace. See docs/secrets.md.
  • Extensible - define your own resource types as TOML data in verg/resources/; no recompile required.
  • Native providers - custom resource types as embedded-source scripts (any language) over a JSON protocol, self-contained in the bundle.

Install

# Cargo (crates.io)
cargo install verg

# PyPI
pip install verg

# Homebrew (tap)
brew install rvben/tap/verg

Quick Start

# Scaffold a new project in ./verg/
verg init

# Edit your inventory and desired state
$EDITOR verg/hosts.toml
$EDITOR verg/state/base.toml

# Preview what would change (dry-run, no modifications)
verg diff --targets all

# Apply to all hosts
verg apply --targets all

How It Works

verg (control CLI, runs on your machine) reads TOML state files and your host inventory, builds a host-specific bundle for each target, then pushes a compiled verg-agent binary over SSH. The agent reads the bundle from stdin, checks actual system state, converges each resource to desired state in dependency order, and returns a JSON summary to stdout. The control CLI aggregates results and presents them.

The agent is a static binary with no runtime dependencies. It is checksum-verified before deployment and runs entirely on the target - no per-resource SSH calls from the control host are needed once the agent is running.

Concepts

Project layout

A verg project lives in a directory named verg/ by default (override with --path or the VERG_PATH environment variable). Inside it:

verg/
  hosts.toml       # host inventory
  groups/          # one .toml per group, each with a [vars] table
  state/           # desired-state declarations; loaded in lexicographic order
  files/           # static files referenced by file resources
  templates/       # Jinja2 template files
  .verg/logs/      # structured apply logs (written automatically)

verg init scaffolds this layout with a starter hosts.toml and state/base.toml.

Inventory and selectors

Each host in hosts.toml has these fields:

Field Required Default Description
address yes - IP address or hostname
user no "root" SSH user
port no 22 SSH port
groups no [] Group memberships
[hosts.NAME.vars] no - Host-specific variables

The --targets flag accepts a selector expression:

Syntax Meaning
all Every host
web Hosts named web or in group web
a,b Union of selectors a and b
a:b Intersection (hosts in both a and b)
!x Exclude x
prod:!db In group prod but not group db

An unknown selector name is an error (exit code 6). The one exception is an exclusion (!x): excluding a group or host that matches nothing excludes nothing rather than erroring, so prod:!down still works when the down group is empty (and a misspelled exclusion is silently a no-op). Parentheses are not supported.

Hosts can also be generated dynamically by a command via an [inventory] section in hosts.toml. See docs/inventory.md for the static fields, the dynamic-inventory command, its JSON output format, and how static and dynamic hosts merge.

State files

State files live in state/ and are loaded in lexicographic order. Each file is a TOML document with this shape:

targets = ["web"]          # optional: limit to hosts in group/name "web"
                           # omit to apply to all hosts

[resource.<type>.<name>]   # one table per resource
property = "value"

A state file with no targets key applies to every host. A state file with targets = ["web"] applies only when the target host belongs to group web or has the name web. Only targets and resource are valid top-level keys; unrecognized keys are rejected by default.

See docs/resources.md for the full resource reference.

Variables and facts

Variables are interpolated using Jinja2 syntax ({{ var }}). Template sources:

Source Precedence
Host vars ([hosts.NAME.vars]) Highest
Group vars (groups/<g>.toml [vars]) Below host vars
Facts gathered at runtime Available as fact.arch, fact.os, etc.
Group membership Available as group.<name>
Inventory context (hosts.*, groups.*) Lowest

Available facts: fact.arch, fact.hostname, fact.os, fact.os_release, fact.os_version.

String values starting with $env. (e.g. password = "$env.MY_SECRET") are resolved from the environment at build time.

Inline content in a file resource is always interpolated. To interpolate a file loaded via source, set template = true.

cmd resources can register their stdout for use in downstream resources as {{ register.NAME }}. See docs/resources.md for details.

Ordering and handlers

Resources declare dependencies with after = ["type.name", ...]. verg sorts resources into execution layers using Kahn's topological sort; within a layer, resources run in FQN order. A resource whose dependency fails is skipped with "dependency failed".

Resources with handler = true are skipped unless another resource lists their FQN in notify. Handlers run after all normal resources; shorthand notify actions (e.g. reload:nginx, daemon-reload) run after handlers. Each shorthand runs at most once per host per run.

when expressions conditionally skip resources based on facts and group membership (e.g. when = "fact.os == 'ubuntu'").

See docs/resources.md for the full common attributes reference.

Resource Types

Type Purpose
pkg Install or remove system packages (apt-get, dnf, pacman - auto-detected)
file Manage a file's content, permissions, and ownership
directory Ensure a directory exists with the right permissions and ownership
service Manage a systemd service's running state and boot enablement
cmd Run a shell command with an idempotency guard
user Create or remove a system user; update shell, home, and groups on existing users
cron Manage a cron job file in /etc/cron.d/
sysctl Set a Linux kernel parameter
apt_repo Add or remove an APT repository with its GPG key
docker_compose Manage a Docker Compose stack
download Download a file from a URL, optionally extracting an archive
hostname Set the static system hostname (requires systemd)
timezone Set the system timezone by IANA name (requires systemd)
mount Manage an /etc/fstab entry and mount state
git Clone a git repository and keep it checked out at a ref

You can also define your own resource types as data in verg/resources/. See docs/resources.md - Custom resource types for the definition format, param schema, and a worked example.

For resource types that need a full scripting language or a richer protocol, native providers let you write the logic in any language and communicate over JSON-on-stdio. The script is embedded in the bundle at build time, so pull-mode agents are self-contained. See docs/providers.md for the declaration format, wire protocol, and a complete worked example.

Complete Example

A minimal nginx setup using pkg, file, and service resources with dependency ordering.

# verg/hosts.toml
[hosts.web1]
address = "192.0.2.10"
user = "root"
groups = ["web"]

[hosts.web1.vars]
http_port = "80"
server_name = "example.com"
# verg/state/nginx.toml
targets = ["web"]

[resource.pkg.nginx]
name = "nginx"
state = "present"

[resource.file.nginx-conf]
path = "/etc/nginx/sites-available/default"
content = """
server {
    listen {{ http_port }};
    server_name {{ server_name }};
    root /var/www/html;
}
"""
mode = "0644"
after = ["pkg.nginx"]
notify = ["reload:nginx"]

[resource.service.nginx]
name = "nginx"
state = "running"
enabled = true
after = ["file.nginx-conf"]

The FQN for each resource is type.name (e.g. pkg.nginx, file.nginx-conf, service.nginx). Every after reference in this example resolves to a resource defined in the same file.

Commands

Command Key args Description
verg apply --targets <TARGETS> (required) Converge targets to desired state
verg diff --targets <TARGETS> (default: all), --limit, --offset, --fields Show what would change without applying
verg check --targets <TARGETS> (default: all) Verify targets match desired state (exits 0 when drift found, 1 when all match)
verg schema - Print resource type schemas as JSON
verg init --force Scaffold a new project directory
verg completions <bash|fish|zsh|powershell|elvish> Generate shell completions
verg publish --targets <TARGETS>, --dest <DIR> Build per-host bundles offline for pull-mode agents

apply has no default target. --targets is required to prevent accidental mass applies. Running apply in a non-interactive pipeline without --yes exits with code 5 (confirmation required).

Global Flags

Flag Default Description
-o, --output <auto|text|json> auto Output format; auto selects JSON when stdout is not a TTY
--json - Force JSON output (hidden alias for -o json)
-q, --quiet - Suppress per-resource lines; print only the final summary
-y, --yes - Proceed when stdin is not a TTY (required for CI/pipelines)
--path <PATH> ./verg Path to the verg project directory (VERG_PATH)
--parallel <N> 10 Maximum parallel SSH connections
--ssh-config <PATH> - Path to SSH config file (VERG_SSH_CONFIG)
--agent-dir <PATH> - Directory containing verg-agent binaries per architecture (VERG_AGENT_DIR)
--host-key-checking <yes|accept-new|no> yes SSH host key checking policy
--ssh-known-hosts <PATH> - Path to a known_hosts file
--skip-agent-checksum - Skip agent binary checksum verification (air-gapped or local builds)
--lax-config - Downgrade config validation errors (unknown keys/types) to warnings
--timeout <SECONDS> 600 Per-host timeout in seconds

Exit Codes

Code Name Meaning
0 SUCCESS Resources changed or drift detected
1 NOTHING_CHANGED All resources already in desired state
2 PARTIAL_FAILURE Some resources failed; others succeeded
3 TOTAL_FAILURE All resources on all targets failed
4 CONNECTION_ERROR All failures were SSH connection errors
5 INVALID_CONFIG Config is invalid, or apply requires --yes (ConfirmationRequired)
6 TARGET_NOT_FOUND No hosts matched the given target selector
7 INTERNAL_ERROR Unexpected internal error (I/O or other)
8 CONFLICT State conflict that cannot be automatically resolved
130 INTERRUPTED Process received SIGINT (Ctrl-C)

check exits 0 when drift is found and 1 when everything already matches. diff exits 0 on any successful run (even with no changes); only connection or partial failures yield 4 or 2.

Security

verg pushes a compiled agent binary to target hosts and runs it as the configured SSH user (root by default). The agent binary is SHA-256 verified locally before transfer and remotely after transfer using sha256sum -c. SSH host key checking is strict by default (StrictHostKeyChecking=yes). All state files are validated locally before any SSH connection is opened. Resources marked sensitive = true have their payload redacted from output and the apply changelog.

See SECURITY.md for the full threat model, supply chain details, secret handling, and vulnerability reporting.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

verg-0.8.2.tar.gz (216.8 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

verg-0.8.2-py3-none-manylinux_2_28_x86_64.whl (2.9 MB view details)

Uploaded Python 3manylinux: glibc 2.28+ x86-64

verg-0.8.2-py3-none-manylinux_2_28_aarch64.whl (2.6 MB view details)

Uploaded Python 3manylinux: glibc 2.28+ ARM64

verg-0.8.2-py3-none-macosx_11_0_arm64.whl (2.6 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

verg-0.8.2-py3-none-macosx_10_12_x86_64.whl (2.7 MB view details)

Uploaded Python 3macOS 10.12+ x86-64

File details

Details for the file verg-0.8.2.tar.gz.

File metadata

  • Download URL: verg-0.8.2.tar.gz
  • Upload date:
  • Size: 216.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for verg-0.8.2.tar.gz
Algorithm Hash digest
SHA256 9ee1cfeaa7d81eaa2363a32ec6796cb35889159a3a495f6784b2f5faad529ba4
MD5 51abfd40f5f9e4b96f396ca979dc5fa1
BLAKE2b-256 9293df627b9b24b5f981aa008c103d08668a55165ee80256a2cec06015843e57

See more details on using hashes here.

File details

Details for the file verg-0.8.2-py3-none-manylinux_2_28_x86_64.whl.

File metadata

  • Download URL: verg-0.8.2-py3-none-manylinux_2_28_x86_64.whl
  • Upload date:
  • Size: 2.9 MB
  • Tags: Python 3, manylinux: glibc 2.28+ x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for verg-0.8.2-py3-none-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 ee17abbf417a8ab05cb0d592d445c4195f377e7de1f51e206a7d63d0f8980615
MD5 eaafda5bbfb0249fa406dd653b688d11
BLAKE2b-256 3df75d3df19d594119ae815a95fa53daa4d6d652285fc01d2a6a62a283598d45

See more details on using hashes here.

File details

Details for the file verg-0.8.2-py3-none-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for verg-0.8.2-py3-none-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 d13d0f1e524dcb9c2c16fe40d9c39fc52d67fbf13d9acc5bee5f20f25abd8331
MD5 6425399be23276c79695860811f8fbf9
BLAKE2b-256 7232e0ec6a9cb45e5a243d654d66813b26dea6c3124b5738ac6bb55f97c5888f

See more details on using hashes here.

File details

Details for the file verg-0.8.2-py3-none-macosx_11_0_arm64.whl.

File metadata

  • Download URL: verg-0.8.2-py3-none-macosx_11_0_arm64.whl
  • Upload date:
  • Size: 2.6 MB
  • Tags: Python 3, macOS 11.0+ ARM64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for verg-0.8.2-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 d7f61ed1848ac58a8efa4f2781fc17dc2b44d8f7b8b75f73a247f7b76c7bbd38
MD5 e8ff14b1091b5a6f193b58c8a07b1140
BLAKE2b-256 ba44b47eb2b744ed6a951a04ab7678cbe2720b41a4dbaea18c53ead6b9edb4dc

See more details on using hashes here.

File details

Details for the file verg-0.8.2-py3-none-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for verg-0.8.2-py3-none-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 638c115370e644e22359824651dfcdf99508e840db4a4cd38010eaab779c95a1
MD5 d469721665b0158dae0166168808f674
BLAKE2b-256 c7bfbc944ea4c653e714ebce70dfbd6d4beb4904cfb9b36f10b7ca256878bef4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page