diff --git a/CHANGELOG.md b/CHANGELOG.md index ed55967..4209cd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -138,6 +138,33 @@ which records not just what changed but what each drill run proved. class), which under `pipefail` could have narrated the wrong inheritance shape on a clone that does carry a `pristine`. +- **A mint that converges a tenant role marks a `bootstrapped` snapshot after + the hook succeeds** (#130, the half #104 deferred and #128 argued out) — + `pristine` throws the tenant role away; `bootstrapped` keeps it and throws + away only what you did afterwards, which is the undo an operator reaches for + far more often and which otherwise costs a ~10-minute re-mint. Same policy + as `pristine`, sharing one function with it (`snapshot_mark`) so the + never-fatal contract exists in exactly one place: default on, + `BOX_SNAPSHOT_BOOTSTRAPPED=0` opts out, a `dir` pool skips it loudly — with + two marks that disk objection is twice the size, so a CoW-less host is not + asked to pay for one full root copy per mint, let alone two — and a failed + snapshot warns without failing a good mint. **Deliberately conditional where + `pristine` is unconditional**: `pristine` marks a *moment* every fresh mint + has, `bootstrapped` marks an *event* — a rig hook box ran and watched + succeed — and a blank box has no such event, so it gets no mark rather than + a byte-identical duplicate of `pristine` at twice the cost. When the hook + **fails**, box takes no mark either and says so where the operator is + looking: the failure message now hands over `box snapshot + bootstrapped` to take after the by-hand re-run, because box will not label a + convergence it did not watch. So the label is documented as one-directional + on every surface — its **presence** means the hook converged and nothing has + touched the box since; its **absence** means nothing at all (a blank + template, a `dir` pool, an opt-out, or a hand-converged box). Same durability + caveats as `pristine`, restated rather than referenced: it dies with the box + on `box rm` (`box export` is the durable path), and no filesystem rollback + reaches off-box state such as a tailnet device record or a runner + registration (heavy-duty/rig#62). + ### Changed - **`state:needs-human` no longer waits on the cron to become true** (#141) diff --git a/README.md b/README.md index fa1f047..a8b89d4 100644 --- a/README.md +++ b/README.md @@ -374,6 +374,42 @@ multi-GB copy rather than a near-free copy-on-write mark, so the mint installs by default precisely so snapshots are cheap. `BOX_SNAPSHOT_PRISTINE=0` skips the mark on any host. +### `bootstrapped` — the same undo, one step later + +A mint whose template names a bootstrap role marks a second snapshot, +`bootstrapped` ([#130](https://github.com/heavy-duty/box/issues/130)), once +`rig bootstrap` has run and box has **watched it succeed**. That is the box +converged and not yet touched — the state you actually wanted back most of +the time. + +```sh +box restore work bootstrapped # keep the tenant role, undo what you did to it +box restore work pristine # throw the tenant role away too +``` + +Same rules as `pristine`: default on, never fatal, skipped loudly on a `dir` +pool (with two marks that disk cost is twice the size, so a CoW-less host is +not asked to pay it), and `BOX_SNAPSHOT_BOOTSTRAPPED=0` skips it anywhere. +The same two caveats apply, unchanged: it **dies with the box** on `box rm` +(`box export` is the durable path), and it **cannot reach off-box state** — +a tailnet device record, a runner registration (rig#62). + +**Read the label in one direction only.** Its _presence_ means the mint-time +hook converged and nothing has touched the box since. Its _absence_ means +nothing at all, because box only marks a hook it watched: + +- A **blank** box runs no hook, so there is no convergence to mark. It gets + none — `pristine` and `bootstrapped` would be the same disk state at twice + the cost, and a label claiming a convergence that never happened is worse + than no label. +- A box whose hook **failed** gets none either. box tells you to re-run the + role by hand through `box shell`, and a by-hand run happens in a shell box + does not watch — so box hands you `box snapshot bootstrapped` to take + at the moment it is true, rather than inventing a fact. +- A `dir`-pool host and `BOX_SNAPSHOT_BOOTSTRAPPED=0` both skip it. + +`box info ` is what actually tells you which labels a box has. + ## Survive the host: `box export` / `box import` Snapshots live _inside_ a box, and `box rm` deletes the box **and** its @@ -441,6 +477,8 @@ box restore [--force] # roll back to a snapshot — destructive, asks first # 'pristine' is auto-marked at mint: back to # pristine Debian + box's seed, before rig ran + # 'bootstrapped' too, if a rig hook converged: + # the role kept, everything since undone box export [] [--instance-only] # one portable file (snapshots incl.) — survives rm & host box import [--name ] diff --git a/bin/box b/bin/box index 0ef322c..f5ec84e 100755 --- a/bin/box +++ b/bin/box @@ -9,6 +9,12 @@ root="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)" remote=""; mode="auto"; name=""; from=""; template=""; force=0; json=0; want_help=0 cpu=""; memory=""; disk=""; instance_only=0 inst="" # the resolved Incus instance, set by the 'box' precondition +# Labels snapshot_mark actually CREATED this run, space-separated. Every path +# through snapshot_mark returns 0 on purpose — the never-fatal contract — so +# the exit status cannot tell a mark that was taken from one that was skipped +# (dir pool, knob=0) or refused (incus said no). Anything that offers the +# operator a mark must ask this, not assume the call happened. +marks="" die() { echo "box: $*" >&2; exit 1; } # 1 = it went wrong usage_error() { echo "box: $*" >&2; echo "try 'box help'." >&2; exit 2; } # 2 = you asked wrong @@ -311,6 +317,22 @@ none (copying a snapshot). On a 'dir'-backend host, where a snapshot is a full copy rather than a CoW mark, the mint skips it and says so. BOX_SNAPSHOT_PRISTINE=0 skips it anywhere. +A mint whose template names a bootstrap role marks a second snapshot, +'bootstrapped' (#130), once the rig hook has run and box has watched it +SUCCEED — the box converged and not yet touched. It is the cheaper undo: +'pristine' throws the tenant role away, 'bootstrapped' keeps it and throws +away only what you did afterwards. Same rules: never fatal, skipped on a +'dir' pool, and BOX_SNAPSHOT_BOOTSTRAPPED=0 skips it anywhere. + +It is deliberately NOT unconditional the way 'pristine' is. A blank box +runs no hook, so there is no convergence to mark and it gets none — the +label marks an event, not a mint. And if the hook FAILS, box tells you to +re-run it by hand and takes no mark afterwards: a by-hand run happens in a +shell box does not watch, so box will not label a convergence it never saw. +It names 'box snapshot bootstrapped' at that moment instead. So read +the label one way only: its PRESENCE means the hook converged untouched; +its ABSENCE means nothing at all. + box new --name scratch # blank, the default box new --name work --template claude-box box new --name lean --template claude-box --cpu 2 --memory 3GiB @@ -393,8 +415,14 @@ have. box snapshot work authed -One label is taken for you: every fresh mint marks 'pristine' after -cloud-init and before rig's tenant role runs (#104) — see 'box help restore'. +Two labels are taken for you (see 'box help restore'): every fresh mint +marks 'pristine' after cloud-init and before rig's tenant role runs (#104), +and a mint with a bootstrap role marks 'bootstrapped' after box has watched +that role converge (#130). 'bootstrapped' is skipped where there was no hook +to watch — a blank box, or a hook box told you to re-run by hand — so its +absence proves nothing. Take it yourself in that case: + + box snapshot work bootstrapped Snapshots do not outlive their box: 'box rm' deletes a box and every snapshot it has. They are an undo, not a backup — 'box export' is the @@ -424,6 +452,25 @@ a backup — it dies with the box on 'box rm'; 'box export' is what survives. A box minted with --from has no 'pristine' of its own unless it inherited one from its source, because a clone has no pristine moment to capture. +'bootstrapped' is the other label box takes for you (#130), and it is the +one you probably want more often: it is the box AFTER the tenant role +converged and before you touched it. + + box restore work bootstrapped # keep the role, undo what you did to it + box restore work pristine # throw the role away too + +The same two caveats apply to it exactly as written above — it dies with the +box on 'box rm' ('box export' is what survives), and it cannot undo anything +that left the box, a tailnet join or a runner registration among them +(heavy-duty/rig#62). + +Its absence proves NOTHING. box takes it only after a rig hook it ran and +watched succeed, so a blank box has none, a 'dir'-pool host has none, and a +box whose hook failed and was re-run by hand through 'box shell' has none +either — box will not label a convergence it did not watch. 'box info ' +tells you which labels a box actually has; take the mark yourself any time +with 'box snapshot bootstrapped'. + Destructive, so it asks first — naming the snapshot it is rolling back to, because the whole risk is picking the wrong label. --force (-f) skips the prompt; with no TTY to ask on it refuses rather than assuming yes. @@ -1129,14 +1176,29 @@ storage_driver() { printf '%s' "$driver" } -# Take the 'pristine' snapshot, or say loudly why not. Default ON: the value -# of this mark only exists if it is already there on the bad day, and nobody -# takes it by hand at the one moment it is true. The escape hatch is the -# BOX_LAUNCH_TIMEOUT shape — an environment knob, not another flag on 'new'. -snapshot_pristine() { - local instance="$1" name="$2" driver - if [ "${BOX_SNAPSHOT_PRISTINE:-1}" = 0 ]; then - echo "box: skipping the 'pristine' snapshot (BOX_SNAPSHOT_PRISTINE=0)." +# The shared policy behind every mark box takes for you (#104, #130): +# read the opt-out, read the storage driver, refuse to double the disk cost on +# a pool with no copy-on-write, and NEVER fail a mint over a checkpoint. +# +# It lives once on purpose. What generalises here is the MECHANISM, and the +# never-fatal contract inside it is exactly the property that must not be got +# subtly different in two places — one function, one place to review it, one +# place a future third mark inherits it from. What does NOT generalise is the +# PROSE: 'pristine' and 'bootstrapped' name different moments and are worth +# different sentences, so each wrapper below owns its own narration and passes +# it in. Two thin wrappers over one policy is the honest split; a single +# stringly-parameterised function taking five sentences would not be. +# +# The opt-out variable name is DERIVED from the label (BOX_SNAPSHOT_PRISTINE, +# BOX_SNAPSHOT_BOOTSTRAPPED) so the message can never drift from the knob the +# operator actually has to set. +# +# snapshot_mark