forked from heavy-duty/box
Refuse to change versions under existing boxes; file the migration as #67
Per @danmt on #66: hatch first, the version-aware migration as its own issue. Building the host stack from the installer means an upgrade is no longer a tree swap — it reaches under every box attached to that stack. So the installer now declines to guess. Same version and ref: it says so and changes nothing. Version or ref change with boxes on the host: it refuses, lists them, and does so BEFORE $DEST is touched, so a refusal leaves the working install intact. No boxes: nothing to lose, proceed. BOX_FORCE_UPGRADE=1 overrides, and the drill sets it, because arriving on a dirty host and wiping it is the drill's job. Ref, not just VERSION: a branch and main carry the same VERSION string, so VERSION alone would call an install of this very branch "unchanged" and skip the hatch. Both tag generations count as boxes — a pre-rename user.claudebox=1 box is just as much someone's work as a current one. The box query runs unprivileged first and escalates only if the socket refuses: anyone who owns boxes is already in incus-admin, and an installer should not demand a sudo password merely to look. The error deliberately does not suggest snapshot -> rm -> restore --from: 'box rm' deletes a box AND every snapshot it has, so that path loses the data at the rm. It says to copy anything needed out of the box first. Raised on #67. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ce3a0c5076
commit
3e1143f1d3
4 changed files with 100 additions and 7 deletions
15
CHANGELOG.md
15
CHANGELOG.md
|
|
@ -48,10 +48,17 @@ which records not just what changed but what each drill run proved.
|
|||
`install.sh`, since that is what triggers setup now.
|
||||
- **`install.sh` runs the host setup itself** (#64) — it printed a warning and
|
||||
left you a command to run, so the install reported success and `box new`
|
||||
failed on a host with no Incus. Since `setup-host` is idempotent, doing this
|
||||
on every install is also how an upgraded host picks up stack changes.
|
||||
`BOX_SKIP_SETUP_HOST=1` opts out; if setup fails, the install still stands
|
||||
and says what to re-run.
|
||||
failed on a host with no Incus. `BOX_SKIP_SETUP_HOST=1` opts out; if setup
|
||||
fails, the install still stands and says what to re-run.
|
||||
- **`install.sh` refuses to change versions under existing boxes** — building
|
||||
the host stack from the installer means an upgrade reaches under every box
|
||||
attached to that stack, so it is no longer only a tree swap. Same version and
|
||||
ref: says so, changes nothing. Version or ref change with boxes on the host
|
||||
(either tag generation): refuses, loudly, listing them — and refuses *before*
|
||||
`$DEST` is touched, so the working install survives the refusal. No boxes:
|
||||
proceeds. `BOX_FORCE_UPGRADE=1` overrides; the drill sets it, since wiping
|
||||
boxes is its job. The version-aware upgrade that migrates instead of refusing
|
||||
is #67.
|
||||
|
||||
## 0.5.0 — 2026-07-15
|
||||
|
||||
|
|
|
|||
15
README.md
15
README.md
|
|
@ -42,9 +42,18 @@ curl -fsSL https://raw.githubusercontent.com/heavy-duty/box/main/install.sh | ba
|
|||
|
||||
Installs the tree to `~/.local/share/box` and links `box` onto your
|
||||
`PATH`, then runs the host setup below for you (it may ask for `sudo`; set
|
||||
`BOX_SKIP_SETUP_HOST=1` to opt out and run it yourself). Re-run any time to
|
||||
upgrade — upgrading re-applies the host stack, and from a pre-0.4.0 install
|
||||
also retires the old `claudebox` symlink. (No `git clone` needed.)
|
||||
`BOX_SKIP_SETUP_HOST=1` to opt out and run it yourself). Upgrading from a
|
||||
pre-0.4.0 install also retires the old `claudebox` symlink. (No `git clone`
|
||||
needed.)
|
||||
|
||||
**Upgrading, and boxes.** Because the installer builds the host stack, an
|
||||
upgrade reaches under the boxes attached to it — so it is deliberate about it.
|
||||
Re-running the same version changes nothing and says so. Changing version on a
|
||||
host that has boxes **refuses**, lists them, and leaves your install exactly as
|
||||
it was; deal with the boxes and re-run, or say `BOX_FORCE_UPGRADE=1` to upgrade
|
||||
over them on purpose (boxes are not deleted, but the stack is rebuilt beneath
|
||||
them). With no boxes on the host, it just upgrades. Migrating boxes across an
|
||||
upgrade instead of refusing is [#67](https://github.com/heavy-duty/box/issues/67).
|
||||
|
||||
## One-time host setup (Ubuntu 24.04 / Debian 13)
|
||||
|
||||
|
|
|
|||
|
|
@ -202,6 +202,12 @@ EOF
|
|||
export BOX_SKIP_SETUP_HOST=1
|
||||
fi
|
||||
|
||||
# The installer refuses to change versions on a host that still has boxes.
|
||||
# That hatch is for humans with work to lose; the drill's whole job is to
|
||||
# arrive on a dirty host, wipe every box it recognises (below) and re-prove
|
||||
# the stack from there — so it opts out, deliberately and in one place.
|
||||
export BOX_FORCE_UPGRADE=1
|
||||
|
||||
BOX_REPO="$REPO" BOX_REF="$REF" \
|
||||
bash -c "$(curl -fsSL "https://raw.githubusercontent.com/$REPO/$REF/install.sh")" \
|
||||
|| { echo "install failed"; exit 1; }
|
||||
|
|
|
|||
71
install.sh
71
install.sh
|
|
@ -47,6 +47,77 @@ EXTRACTED="$(find "$TMPDIR" -mindepth 1 -maxdepth 1 -type d | head -n1)"
|
|||
[ -n "$EXTRACTED" ] || die "could not find the extracted source directory in archive"
|
||||
[ -f "$EXTRACTED/bin/box" ] || die "archive does not contain bin/box — is $REPO@$REF correct?"
|
||||
|
||||
# --- upgrade hatch ---------------------------------------------------------
|
||||
# This installer builds the host stack itself now, and every box on the host is
|
||||
# attached to that stack — so a version change here is not just a tree swap, it
|
||||
# reaches under running boxes. Until the version-aware migration exists (#67),
|
||||
# refuse rather than guess: if this would change what is installed AND there are
|
||||
# boxes on the host, stop and let a human decide. Checked BEFORE $DEST is
|
||||
# touched, so a refusal leaves the working install exactly as it was.
|
||||
# Same version + same ref = nothing to change: say so and carry on.
|
||||
new_ver="$(cat "$EXTRACTED/VERSION" 2>/dev/null || echo unknown)"
|
||||
old_ver="$(cat "$DEST/VERSION" 2>/dev/null || true)"
|
||||
old_from="$(cat "$DEST/INSTALLED_FROM" 2>/dev/null || true)"
|
||||
|
||||
if [ -n "$old_ver" ] && [ "$old_ver" = "$new_ver" ] && [ "$old_from" = "$REPO@$REF" ]; then
|
||||
CHANGING=0
|
||||
else
|
||||
CHANGING=1
|
||||
fi
|
||||
|
||||
# How we ask incus about boxes. No incus => no boxes, and nothing to protect.
|
||||
if [ "$(id -u)" -eq 0 ]; then PRIV=""
|
||||
elif command -v sudo >/dev/null 2>&1; then PRIV="sudo"
|
||||
else PRIV=""
|
||||
fi
|
||||
|
||||
# Unprivileged FIRST: anyone who owns boxes is already in incus-admin, so the
|
||||
# plain query answers it without making the installer demand a sudo password
|
||||
# just to look. Escalate only if the socket refuses us.
|
||||
incus_names() { # $1 = tag filter
|
||||
incus list "$1" --format csv --columns n 2>/dev/null && return 0
|
||||
[ -n "$PRIV" ] && $PRIV incus list "$1" --format csv --columns n 2>/dev/null
|
||||
return 0
|
||||
}
|
||||
|
||||
boxes_on_host() {
|
||||
command -v incus >/dev/null 2>&1 || return 0
|
||||
# BOTH tags: a pre-rename box carries user.claudebox=1 and is just as much
|
||||
# someone's work as a current one.
|
||||
{ incus_names "user.box=1"; incus_names "user.claudebox=1"; } | sed '/^$/d' | sort -u
|
||||
}
|
||||
|
||||
if [ "$CHANGING" = 0 ]; then
|
||||
log "already at $new_ver ($REPO@$REF) — reinstalling the same tree, nothing to migrate"
|
||||
elif [ -z "${BOX_FORCE_UPGRADE:-}" ]; then
|
||||
found="$(boxes_on_host)"
|
||||
if [ -n "$found" ]; then
|
||||
printf 'box-install: ERROR: this host has boxes, and this install would change what runs them.\n' >&2
|
||||
printf '\n installed: %s\n incoming: %s\n\n boxes on this host:\n' \
|
||||
"${old_from:-<none>} ${old_ver:-<no version file>}" "$REPO@$REF $new_ver" >&2
|
||||
printf '%s\n' "$found" | sed 's/^/ · /' >&2
|
||||
cat >&2 <<EOF
|
||||
|
||||
Refusing, and nothing has been changed — your current install is intact.
|
||||
The installer now builds the host stack (network, ACL, profile, firewall)
|
||||
itself, so upgrading reaches under boxes that are attached to it.
|
||||
|
||||
Your options:
|
||||
· Stay where you are. The boxes keep working. Nothing to do.
|
||||
· Deal with the boxes, then re-run this installer.
|
||||
NOTE: 'box rm' deletes a box AND every snapshot it has — it cannot be
|
||||
undone, and a snapshot does NOT survive its box. Copy anything you need
|
||||
OUT of a box first ('box shell <box>' / 'box exec <box> -- ...').
|
||||
· Upgrade anyway, on purpose:
|
||||
BOX_FORCE_UPGRADE=1 curl -fsSL <this url> | bash
|
||||
Boxes are not deleted, but the stack is rebuilt underneath them.
|
||||
|
||||
A version-aware upgrade that migrates boxes instead of refusing is #67.
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- atomically replace $DEST ---------------------------------------------
|
||||
log "installing into $DEST"
|
||||
rm -rf "$DEST"
|
||||
|
|
|
|||
Loading…
Reference in a new issue