forked from heavy-duty/box
fix: a clone clears mode.asked — nobody asked THIS box anything (#103)
The clone's re-stamp split provenance into two columns: event facts
re-stamped (schema/version/created/origin/origin.from), lineage facts left
alone (template/user/image/role/rig/mode). Review found one key that sits in
neither: user.box.mode.asked.
It is a mint-EVENT fact by the split's own criterion — only the mint knew
whether a container was asked for or fallen back into for want of /dev/kvm —
but the asker was the SOURCE's operator. A clone refuses --vm/--container
outright, so nobody was asked anything about this instance. Riding through
'incus copy' untouched, it made 'box info' on a clone print
MODE vm (asked: auto)
describing a demand never made of it, with nothing marking it ancestral.
There is no true value to re-stamp it with, so it is CLEARED — cleared and
not set-to-empty, because an empty value is still a key a reader would find.
The unset tolerates failure: a source that predates the stamp never carried
the key, and a clone must not die over a key that was already absent. It
lands with the re-stamp and before the start, the same rule and the same
reason as the rest: a clone is never observable wearing an 'asked' its
operator never gave.
The read side needs no special case. The MODE line was already gated on
'asked', so absence renders as silence — the same absence-is-silence rule the
whole provenance block uses — while TYPE still reports VM or CT off the
instance type 'incus copy' preserves.
+7 checks (529 -> 536), each proven to bite.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
be633aa5f8
commit
aed2f5bf1f
3 changed files with 63 additions and 2 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -43,6 +43,17 @@ which records not just what changed but what each drill run proved.
|
||||||
records **one hop** — a clone of a clone names its parent and forgets its
|
records **one hop** — a clone of a clone names its parent and forgets its
|
||||||
grandparent, because the alternative is an unbounded chain in a config
|
grandparent, because the alternative is an unbounded chain in a config
|
||||||
value and the parent is the box an operator can go look at.
|
value and the parent is the box an operator can go look at.
|
||||||
|
One key sits in **neither** column and is therefore *cleared*:
|
||||||
|
`user.box.mode.asked`. It records whether a container was asked for or
|
||||||
|
fallen back into, which makes it a mint-*event* fact — and the asker was
|
||||||
|
the **source's** operator. A clone refuses `--vm`/`--container` outright,
|
||||||
|
so nobody was asked anything about this instance, and there is no true
|
||||||
|
value to re-stamp it with; inheriting it made `box info` print
|
||||||
|
`MODE vm (asked: auto)` on a clone, describing a demand never made of it.
|
||||||
|
The clear lands with the re-stamp, before the start, and the read side
|
||||||
|
needs no special case: the `MODE` line is gated on `asked`, so absence
|
||||||
|
renders as silence while `TYPE` still reports VM or CT off the preserved
|
||||||
|
instance type.
|
||||||
- **`box info` grew a provenance block** (#103) — it printed
|
- **`box info` grew a provenance block** (#103) — it printed
|
||||||
`NAME / STATE / TYPE / IPV4`, exposures and snapshots, and surfaced *none*
|
`NAME / STATE / TYPE / IPV4`, exposures and snapshots, and surfaced *none*
|
||||||
of the `user.box.*` keys, including the two that already existed. A stamp
|
of the `user.box.*` keys, including the two that already existed. A stamp
|
||||||
|
|
|
||||||
16
bin/box
16
bin/box
|
|
@ -1079,6 +1079,17 @@ cmd_new() {
|
||||||
# that rig role — reading them off the source is the whole point of a clone.
|
# that rig role — reading them off the source is the whole point of a clone.
|
||||||
# ('incus copy' preserves the instance type too, so mode stays true as well.)
|
# ('incus copy' preserves the instance type too, so mode stays true as well.)
|
||||||
#
|
#
|
||||||
|
# 'mode.asked' is the one key that sits in NEITHER column, and so it is
|
||||||
|
# CLEARED rather than re-stamped or inherited. It is a mint-event fact —
|
||||||
|
# only the mint knew whether a container was asked for or fallen back into
|
||||||
|
# for want of /dev/kvm — and the asker was the SOURCE's operator. A clone
|
||||||
|
# refuses --vm/--container outright (nobody was asked anything here), so an
|
||||||
|
# inherited 'asked' makes 'box info' print a demand that was never made of
|
||||||
|
# this instance. There is no true value to re-stamp it with: the honest
|
||||||
|
# answer is absence, and absence is already how the whole block renders
|
||||||
|
# what it does not know — the MODE line simply does not print, while TYPE
|
||||||
|
# above still says VM or CT off the preserved instance type.
|
||||||
|
#
|
||||||
# origin.from records ONE hop. A clone of a clone names its parent and
|
# origin.from records ONE hop. A clone of a clone names its parent and
|
||||||
# forgets its grandparent: the alternative is an unbounded chain in a config
|
# forgets its grandparent: the alternative is an unbounded chain in a config
|
||||||
# value, and the parent is the box an operator can actually go look at.
|
# value, and the parent is the box an operator can actually go look at.
|
||||||
|
|
@ -1088,6 +1099,11 @@ cmd_new() {
|
||||||
user.box.created="$(mint_time)" \
|
user.box.created="$(mint_time)" \
|
||||||
user.box.origin=clone \
|
user.box.origin=clone \
|
||||||
user.box.origin.from="$srcref"
|
user.box.origin.from="$srcref"
|
||||||
|
# Cleared, not set-to-empty: an empty value is still a key, and a reader
|
||||||
|
# that greps the config would find it. Tolerated failure because the source
|
||||||
|
# may predate the stamp and never have carried the key at all — a clone
|
||||||
|
# must not die over a key that was already absent.
|
||||||
|
incus config unset "$instance" user.box.mode.asked >/dev/null 2>&1 || true
|
||||||
incus start "$instance"
|
incus start "$instance"
|
||||||
wait_agent "$instance"
|
wait_agent "$instance"
|
||||||
reset_identity "$instance"
|
reset_identity "$instance"
|
||||||
|
|
|
||||||
38
test/cli.sh
38
test/cli.sh
|
|
@ -1256,6 +1256,17 @@ check "clone: does NOT re-stamp the image — the disk really came from it (#103
|
||||||
restamp_has "$CLONELOG" 'user\.box\.image'
|
restamp_has "$CLONELOG" 'user\.box\.image'
|
||||||
check "clone: does NOT re-stamp the role — the source's rig converged it (#103)" 1 "" \
|
check "clone: does NOT re-stamp the role — the source's rig converged it (#103)" 1 "" \
|
||||||
restamp_has "$CLONELOG" 'user\.box\.role'
|
restamp_has "$CLONELOG" 'user\.box\.role'
|
||||||
|
# The third column, and the one review found: 'mode.asked' is neither lineage
|
||||||
|
# nor re-stampable. It is a mint-event fact whose asker was the SOURCE's
|
||||||
|
# operator, and a clone refuses --vm/--container, so nobody was asked anything
|
||||||
|
# here. It is CLEARED — there is no true value to give it.
|
||||||
|
check "clone: clears the inherited mode.asked — nobody asked THIS box (#103)" 0 "" \
|
||||||
|
grep -qF 'config unset w2 user.box.mode.asked' "$CLONELOG"
|
||||||
|
check "clone: ...and does not re-stamp it with a fabricated answer (#103)" 1 "" \
|
||||||
|
restamp_has "$CLONELOG" 'user\.box\.mode\.asked'
|
||||||
|
# Cleared, never set-to-empty: an empty value is still a key on the instance.
|
||||||
|
check "clone: clears it rather than setting it empty (#103)" 1 "" \
|
||||||
|
grep -qE 'config set .*user\.box\.mode\.asked=($|[[:space:]])' "$CLONELOG"
|
||||||
# Order: the re-stamp lands on the copied instance BEFORE it is started, so a
|
# Order: the re-stamp lands on the copied instance BEFORE it is started, so a
|
||||||
# clone is never observable wearing its source's provenance. Fail-closed — an
|
# clone is never observable wearing its source's provenance. Fail-closed — an
|
||||||
# absent line makes the arithmetic fail, not pass.
|
# absent line makes the arithmetic fail, not pass.
|
||||||
|
|
@ -1267,6 +1278,17 @@ restamp_precedes_start() {
|
||||||
}
|
}
|
||||||
check "clone: the re-stamp precedes the start (#103)" 0 "" \
|
check "clone: the re-stamp precedes the start (#103)" 0 "" \
|
||||||
restamp_precedes_start "$CLONELOG"
|
restamp_precedes_start "$CLONELOG"
|
||||||
|
# The clear rides the same rule for the same reason: a clone must never be
|
||||||
|
# observable — not for one moment, not to 'box info' — wearing an 'asked' its
|
||||||
|
# operator never gave. Fail-closed the same way.
|
||||||
|
clear_precedes_start() {
|
||||||
|
local unset_ln start
|
||||||
|
unset_ln="$(grep -n 'config unset .* user.box.mode.asked' "$1" | head -1 | cut -d: -f1)"
|
||||||
|
start="$(grep -n '^incus start ' "$1" | head -1 | cut -d: -f1)"
|
||||||
|
[ -n "$unset_ln" ] && [ -n "$start" ] && [ "$unset_ln" -lt "$start" ]
|
||||||
|
}
|
||||||
|
check "clone: the mode.asked clear precedes the start too (#103)" 0 "" \
|
||||||
|
clear_precedes_start "$CLONELOG"
|
||||||
|
|
||||||
# --- the read half: 'box info' surfaces it ---------------------------------
|
# --- the read half: 'box info' surfaces it ---------------------------------
|
||||||
# A stamp nothing can read is not done. cmd_info printed NAME/STATE/TYPE/IPV4
|
# A stamp nothing can read is not done. cmd_info printed NAME/STATE/TYPE/IPV4
|
||||||
|
|
@ -1304,13 +1326,25 @@ check "info: surfaces the origin (#103)" 0 "ORIGIN mint" infobox "$STAMPED"
|
||||||
# Still the box it always was: the new block is additive, above the snapshots.
|
# Still the box it always was: the new block is additive, above the snapshots.
|
||||||
check "info: still prints the state block it always did" 0 "IPV4 10.1.2.3" infobox "$STAMPED"
|
check "info: still prints the state block it always did" 0 "IPV4 10.1.2.3" infobox "$STAMPED"
|
||||||
|
|
||||||
# A clone reads back as a clone, naming its source.
|
# A clone reads back as a clone, naming its source. Modelled on what the
|
||||||
|
# --from branch actually leaves behind: origin re-stamped, mode.asked cleared.
|
||||||
CLONECFG="$MWORK/clone.cfg"
|
CLONECFG="$MWORK/clone.cfg"
|
||||||
{ grep -v '^user.box.origin ' "$STAMPED"
|
{ grep -v -e '^user.box.origin ' -e '^user.box.mode.asked ' "$STAMPED"
|
||||||
echo 'user.box.origin clone'
|
echo 'user.box.origin clone'
|
||||||
echo 'user.box.origin.from work/authed'; } > "$CLONECFG"
|
echo 'user.box.origin.from work/authed'; } > "$CLONECFG"
|
||||||
check "info: a clone says so, and names the box it came from (#103)" \
|
check "info: a clone says so, and names the box it came from (#103)" \
|
||||||
0 "ORIGIN clone of work/authed" infobox "$CLONECFG"
|
0 "ORIGIN clone of work/authed" infobox "$CLONECFG"
|
||||||
|
# ...and stays silent about a demand nobody made of it. The MODE line is gated
|
||||||
|
# on 'asked' precisely so absence renders as silence rather than as a guess;
|
||||||
|
# TYPE above still reports VM off the instance type, so nothing is lost.
|
||||||
|
info_has_mode() { infobox "$1" | grep -q '^MODE'; }
|
||||||
|
check "info: a clone prints no MODE line — nobody asked IT anything (#103)" 1 "" \
|
||||||
|
info_has_mode "$CLONECFG"
|
||||||
|
check "info: ...while TYPE still reports what it actually is (#103)" \
|
||||||
|
0 "TYPE VM" infobox "$CLONECFG"
|
||||||
|
# The mint keeps its MODE line — there, the operator really was asked.
|
||||||
|
check "info: a MINT still surfaces what was asked for (#103)" \
|
||||||
|
0 "MODE vm (asked: auto)" infobox "$STAMPED"
|
||||||
|
|
||||||
# --- legacy boxes: the promise that they keep working under every verb ------
|
# --- legacy boxes: the promise that they keep working under every verb ------
|
||||||
# A box carrying the boundary tag and NOTHING else — every box minted before
|
# A box carrying the boundary tag and NOTHING else — every box minted before
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue