Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .claude/rules/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ Functions whose contract is "abort the script on failure" — for example `build

This rule applies to **fail-loud build/install helpers**, not to predicate helpers (functions that intentionally return a boolean for use in conditionals — those are designed for tested contexts and work fine there).

## Runtime-overlay-managed root guard

The new `airplanes-live/image` delivers feed scripts, the readsb feed client, the mlat-client venv, and the systemd units as **symlinks owned by the runtime overlay**, not as real files written by `feed/install.sh` / `feed/update.sh`. An operator who SSHs onto such a feeder and runs the upstream installer would replace those symlinks with stale real files, breaking the next overlay update.

`airplanes_guard_overlay_managed_root` in `scripts/lib/install-update-common.sh` aborts with EX_CONFIG (78) when `/etc/airplanes/runtime-manifest.json` is present, with two intentional bypasses:

- `AIRPLANES_BUILD_MODE=1` — image-build orchestration runs `feed/install.sh --build-mode` against a rootfs it owns; the overlay stage runs later in the same pipeline. The marker isn't laid yet at that point, but the bypass keeps build flows clean if ordering ever changes.
- `AIRPLANES_ALLOW_OVERLAY_BYPASS=1` — explicit recovery / development override. Emits a stderr warning when it fires.

**Call-site contract.** Both `update.sh` and `install.sh` invoke the guard immediately after `airplanes_init_paths` and `airplanes_require_root`, before any destructive work, network I/O, or apt step. Don't move the call later — the point is to fail loudly before anything mutable runs.

**Inline-fallback discipline applies.** Both `airplanes_is_overlay_managed_root` and `airplanes_guard_overlay_managed_root` exist in the lib AND in the inline fallback of both scripts; `test_inline_fallback_drift.bats` pins them via the same `declare -f` body comparison the existing helpers use (so source-text indentation and comments may differ, but the parsed function definitions must match).

## Image vs non-image install branches

`IMAGE_INSTALL` is set by `airplanes_is_image_install`. The two branches diverge on:
Expand Down
2 changes: 1 addition & 1 deletion .claude/rules/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ PR + push (workflow `ci.yml`):
- `script install (debian:13, bundle)` / `(debian:13, bootstrap)` / `(ubuntu:24, bundle)` / `(ubuntu:24, bootstrap)` — `install.sh` on a fresh OS, either with the full repo mounted (`bundle`) or with `install.sh` alone in a temp dir (`bootstrap`, simulating `curl … \| bash`).
- `image build mode` — runs `update.sh` in `AIRPLANES_BUILD_MODE=1` against a synthetic build-time rootfs (synthetic rootfs from `airplanes-live/airplanes-update`).
- `mounted image upgrade (legacy contract)` — mounts the legacy ARM64 image rootfs (downloaded from `airplanes-live/image-releases`) and runs `update.sh` chroot-style with stubbed systemd; asserts post-update state.
- `mounted image upgrade (new contract)` — same shape, against `airplanes-live/image`. Sources its image asset via the new tier-based library (`test/lib/image-source.sh`) — stable release on `main` paths, the rolling `dev-latest` prerelease on `dev` paths.
- `mounted image upgrade (new contract)` — mounts the new runtime-overlay-managed image rootfs (downloaded from `airplanes-live/image`) and asserts feed/update.sh's overlay guard refuses the update (exit 78). Sources its image asset via the tier-based library (`test/lib/image-source.sh`) — stable release on `main` paths, the rolling `dev-latest` prerelease on `dev` paths. Does NOT exercise the full update flow against the new image: overlay-managed feeders update via the runtime-overlay orchestrator, not via feed/update.sh.
- `webconfig drift` — builds a webconfig-flavored rootfs, fingerprints webconfig-owned artifacts, runs `update.sh`, fails on any drift. Catches feed/update.sh clobbering files the image's webconfig layer owns.
- `script upgrade (stable main)` — installs `origin/main` HEAD, seeds legacy `USER=`, runs candidate `update.sh`, asserts MLAT migration + wire endpoints + state files.
- `script upgrade (pre-schema-split pin)` — same with a pinned pre-schema-split source SHA (historical regression coverage).
Expand Down
51 changes: 51 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,48 @@ else
done
}

airplanes_is_overlay_managed_root() {
local manifest
manifest="$(airplanes_path /etc/airplanes/runtime-manifest.json)"
[[ -e "$manifest" || -L "$manifest" ]]
}

airplanes_guard_overlay_managed_root() {
local script_name="${1:-this feed script}"
if airplanes_is_build_mode; then
return 0
fi
if [[ "${AIRPLANES_ALLOW_OVERLAY_BYPASS:-0}" == "1" ]]; then
if airplanes_is_overlay_managed_root; then
echo "WARNING: AIRPLANES_ALLOW_OVERLAY_BYPASS=1 - proceeding on an overlay-managed root" >&2
fi
return 0
fi
if ! airplanes_is_overlay_managed_root; then
return 0
fi
local manifest
manifest="$(airplanes_path /etc/airplanes/runtime-manifest.json)"
cat >&2 <<MSG
ERROR: This system is managed by the airplanes-live runtime overlay
($manifest is present). The overlay delivers and updates feed
scripts (apl-feed, airplanes-feed, airplanes-mlat, the readsb feed
client, and the mlat-client venv) atomically as part of an overlay
refresh.

Running $script_name directly would replace overlay-owned files
with stale copies and break the next overlay update.

To update this feeder, use the web UI's "Update System" button
(which invokes airplanes-update-orchestrator), or run that
orchestrator manually if you have shell access.

To override this guard for recovery or development, set
AIRPLANES_ALLOW_OVERLAY_BYPASS=1.
MSG
exit 78
}

airplanes_require_root() {
if [[ "${AIRPLANES_SKIP_ROOT_CHECK:-0}" == "1" ]]; then
return 0
Expand Down Expand Up @@ -124,6 +166,15 @@ airplanes_enable_build_mode_from_args "$@"

airplanes_init_paths
airplanes_require_root

# Refuse to run on an image whose feed stack ships through the runtime
# overlay (the new airplanes-live/image atomic delivery). The overlay
# owns the feed binaries and scripts as symlinks; clobbering them with
# real files via this script would break the next overlay update.
# Build-mode invocations and AIRPLANES_ALLOW_OVERLAY_BYPASS=1 skip the
# guard; see scripts/lib/install-update-common.sh for the contract.
airplanes_guard_overlay_managed_root "feed install.sh"

mkdir -p "$IPATH"
airplanes_install_bootstrap_deps

Expand Down
66 changes: 66 additions & 0 deletions scripts/lib/install-update-common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,72 @@ airplanes_enable_build_mode_from_args() {
done
}

# Returns 0 if the current rootfs is managed by the airplanes-live runtime
# overlay (atomic delivery of feed scripts, decoders, webconfig, mlat-client
# venv as a single signed payload), and 1 otherwise.
#
# Detection: presence of /etc/airplanes/runtime-manifest.json, which the
# overlay's install pipeline writes in both build mode (regular file copy of
# the active release manifest) and runtime mode (symlink to the current
# release's manifest.json). Both `-e` and `-L` cover the symlink form,
# including the brief mid-flip window when the link may dangle.
#
# Legacy images and manual installs do not carry this marker.
airplanes_is_overlay_managed_root() {
local manifest
manifest="$(airplanes_path /etc/airplanes/runtime-manifest.json)"
[[ -e "$manifest" || -L "$manifest" ]]
}

# Aborts with EX_CONFIG (78) if the rootfs is overlay-managed, so feed's
# install.sh / update.sh cannot replace overlay-owned symlinks (apl-feed,
# airplanes-feed.sh, airplanes-mlat.sh, the readsb feed client, the mlat-
# client venv) with stale real files. Stomping those symlinks breaks the
# next overlay update.
#
# Bypasses:
# - Build mode (AIRPLANES_BUILD_MODE=1). Image-build pipelines run feed's
# install.sh --build-mode against a rootfs they own; the overlay stage
# runs later in the same pipeline. Skipping the guard keeps build
# orchestration clean.
# - AIRPLANES_ALLOW_OVERLAY_BYPASS=1. Explicit recovery / development
# override. Emits a stderr warning when it fires.
airplanes_guard_overlay_managed_root() {
local script_name="${1:-this feed script}"
if airplanes_is_build_mode; then
return 0
fi
if [[ "${AIRPLANES_ALLOW_OVERLAY_BYPASS:-0}" == "1" ]]; then
if airplanes_is_overlay_managed_root; then
echo "WARNING: AIRPLANES_ALLOW_OVERLAY_BYPASS=1 - proceeding on an overlay-managed root" >&2
fi
return 0
fi
if ! airplanes_is_overlay_managed_root; then
return 0
fi
local manifest
manifest="$(airplanes_path /etc/airplanes/runtime-manifest.json)"
cat >&2 <<MSG
ERROR: This system is managed by the airplanes-live runtime overlay
($manifest is present). The overlay delivers and updates feed
scripts (apl-feed, airplanes-feed, airplanes-mlat, the readsb feed
client, and the mlat-client venv) atomically as part of an overlay
refresh.

Running $script_name directly would replace overlay-owned files
with stale copies and break the next overlay update.

To update this feeder, use the web UI's "Update System" button
(which invokes airplanes-update-orchestrator), or run that
orchestrator manually if you have shell access.

To override this guard for recovery or development, set
AIRPLANES_ALLOW_OVERLAY_BYPASS=1.
MSG
exit 78
}

airplanes_require_root() {
if [[ "${AIRPLANES_SKIP_ROOT_CHECK:-0}" == "1" ]]; then
return 0
Expand Down
Loading