Declarative Does Not Mean Destructive
This article describes the unreleased Barn 0.9.0 candidate. See Status for current validation and remaining release checks.
“Declarative” is often shortened to “make reality equal the file.” That is a useful slogan until the file is incomplete, the wrong branch is checked out, or one YAML group is temporarily removed. If absence is treated as deletion, an ordinary editing mistake becomes a destructive operation.
Barn uses a narrower rule:
Desired state may authorize creation. It can describe drift. It never authorizes destruction by omission.
The distinction is central to a VM runtime because roots, data disks, SSH keys, and local evidence are not stateless replicas. Recreating them may be correct, but it must be a decision the operator can see.
From Inventory to node identity
Barn does not hash the whole Pigsty Inventory. It first extracts the fields it owns, fills defaults, canonicalizes image selectors and architecture, and builds a canonical resolved spec. Exact Catalog artifact resolution remains separate, so a channel update does not itself change a node’s spec hash. Each node then receives a hash of:
- the deployment envelope shared by every node, such as subnet, login user, architecture policy, and the deployment-level default image request; and
- exactly that node’s resolved definition.
Adding a peer therefore does not change an existing node’s hash. Editing an unconsumed Pigsty field—PostgreSQL version, packages, or service policy—does not produce VM drift. The VM layer reacts only to the contract it actually understands.
This also avoids a dangerous half-promise: Barn does not pretend to implement
Ansible’s entire variable system. Unknown vm_* keys and conflicting values
inside the owned namespace fail. Everything outside the documented boundary
is opaque rather than partially interpreted.
Decision status: current. Barn converges additions automatically, but definition changes and removal require explicit commands. See Daily Operations for the command workflow.
The five plan outcomes
barn plan compares desired state, applied deployment state, and committed
node state. The result is intentionally small:
| Outcome | Meaning | Apply path |
|---|---|---|
| create | desired node has no committed state | barn up creates it |
| unchanged | definition and runtime still match | running peer stays untouched; stopped peer may start |
| recreate | node definition changed | explicit barn recreate --force <node> |
| missing | applied node is absent or skipped in the Inventory | explicit barn destroy <node> --force, or restore it to the file |
| envelope drift | subnet, login identity, architecture, or runtime policy changed | whole-deployment recreate |
Plan is read-only. It reports the exact node sets and, in text mode, the command that applies the required explicit transition.
Why up stops at drift
Barn could decide that changing CPU or memory is harmless enough to apply,
or that a new image should silently rebuild a root disk. Pre-1.0 intentionally
does neither. A changed VM definition is classified as recreate and up
returns a typed conflict.
That conservative boundary has two advantages:
- all changes that can invalidate Guest state share one visible operation;
- Barn can finish every prerequisite check before touching the current node.
The recreate path resolves the selected emulator, acceleration policy, firmware, image bytes, network backend, shares, and persistent-disk contract before destruction. If a foreign emulator is missing or a share is unsafe, the existing VM remains intact.
Why missing nodes block convergence
A node can disappear from desired state for many reasons that do not express deletion intent:
- the operator opened a reduced Inventory while debugging;
- a group was renamed or filtered;
vm_skiptemporarily marks a real or external host;- a merge conflict dropped a YAML branch;
- the configuration file itself is unavailable.
When applied state contains such a node, up stops and names it. The operator
must either restore the definition or run the explicit destroy command. This
is deliberately more friction than automatic garbage collection—and far less
friction than recovering an unintended disk deletion.
Persistent data disks add another boundary. Normal destroy preserves them.
Whole-deployment destroy --delete-persistent explicitly includes owned
persistent disks, including retained disks, and accepts no node selectors;
deleting deployment keys as well requires whole-deployment
destroy --purge or purge. Retention is not a backup guarantee: guest
bootstrap can reset unrecognized or confirmed damaged test filesystems, even
on persistent disks. See Data disks.
One confirmation cannot silently grow into broader authority.
Convergence is still incremental
Safety does not mean rebuilding everything. New nodes are created without stopping existing peers. Selected stopped nodes start without recreating running ones. A per-node recreate preserves peers and, when requested by the disk contract, persistent data.
The result is declarative where desired state is strong evidence—creation and comparison—and explicit where the cost is irreversible. Barn does not make the operator manually calculate drift, but it also does not confuse a diff with permission.
Read next: A PID is not a virtual machine.