Skip to content
Closed
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
15 changes: 15 additions & 0 deletions runtime-overlay/manifest-inputs/managed_paths.json
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,11 @@
"link": "/usr/local/share/airplanes/airplanes-diagnostics.sh",
"target": "/opt/airplanes-runtime/current/share/airplanes/airplanes-diagnostics.sh"
},
{
"mode": "symlink",
"link": "/usr/local/share/airplanes/airplanes-stats.sh",
"target": "/opt/airplanes-runtime/current/share/airplanes/airplanes-stats.sh"
},
{
"mode": "symlink",
"link": "/usr/local/share/airplanes/apl-feed",
Expand Down Expand Up @@ -291,5 +296,15 @@
"mode": "symlink",
"link": "/etc/systemd/system/airplanes-config-sync.timer",
"target": "/opt/airplanes-runtime/current/systemd/airplanes-config-sync.timer"
},
{
"mode": "symlink",
"link": "/etc/systemd/system/airplanes-stats.service",
"target": "/opt/airplanes-runtime/current/systemd/airplanes-stats.service"
},
{
"mode": "symlink",
"link": "/etc/systemd/system/airplanes-stats.timer",
"target": "/opt/airplanes-runtime/current/systemd/airplanes-stats.timer"
}
]
3 changes: 2 additions & 1 deletion runtime-overlay/manifest-inputs/systemd.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"airplanes-feed.service",
"airplanes-mlat.service",
"airplanes-diagnostics.timer",
"airplanes-config-sync.timer"
"airplanes-config-sync.timer",
"airplanes-stats.timer"
],
"disable": [],
"reload_or_restart": [
Expand Down
49 changes: 37 additions & 12 deletions runtime-overlay/scripts/gates/exec-bit-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -212,21 +212,46 @@ if [[ -d "$systemd_dir" ]]; then
raw="${raw:1}"
done
bin_path="${raw%% *}"
[[ "$bin_path" == /* ]] || continue

rel=""
if [[ "$bin_path" == "$CURRENT_PREFIX"* ]]; then
rel="${bin_path#"$CURRENT_PREFIX"}"
elif [[ -n "${managed_link_to_release_local[$bin_path]:-}" ]]; then
rel="${managed_link_to_release_local[$bin_path]}"
else
# Host-installed binary; out of scope.
continue

# (1) argv[0] exec-bit / shebang shape check, when the executable
# resolves into the release tree (directly under the release
# prefix or via a managed_paths symlink). A host binary
# (/usr/bin/..., /bin/...) or a path under a managed directory
# symlink resolves to nothing here and is left to the host — its
# shape is out of scope.
if [[ "$bin_path" == /* ]]; then
rel=""
if [[ "$bin_path" == "$CURRENT_PREFIX"* ]]; then
rel="${bin_path#"$CURRENT_PREFIX"}"
elif [[ -n "${managed_link_to_release_local[$bin_path]:-}" ]]; then
rel="${managed_link_to_release_local[$bin_path]}"
fi
if [[ -n "$rel" ]] && ! _assert_exec_file "$rel" "$unit_base Exec=$bin_path"; then
fail_count=$((fail_count + 1))
fi
fi

if ! _assert_exec_file "$rel" "$unit_base Exec=$bin_path"; then
# (2) Every absolute path in the Exec line that is a DIRECT child of
# the overlay-managed runtime dir must have a managed_paths
# symlink. Scan the whole line, not just argv[0], so an
# interpreter form (`/bin/bash /usr/local/share/airplanes/foo.sh`)
# is covered as well as the direct form. A missing managed link
# means the overlay ships a unit referencing a runtime script it
# doesn't manage — exactly the airplanes-stats gap: the
# .timer/.service were globbed into the overlay but
# airplanes-stats.sh had no managed_paths row and was never
# staged, so a feeder updating to it got a timer pointing at a
# missing ExecStart. Deeper paths (venv/bin/..., apl-feed/*,
# lib/*) are provided wholesale by a managed *directory* symlink
# and carry a further slash, so they're excluded by design.
read -ra _exec_tokens <<< "$raw"
for tok in "${_exec_tokens[@]}"; do
[[ "$tok" == /usr/local/share/airplanes/* \
&& "$tok" != /usr/local/share/airplanes/*/* ]] || continue
[[ -n "${managed_link_to_release_local[$tok]:-}" ]] && continue
echo "exec-bit-check: $unit_base Exec references unmanaged runtime script: $tok — add a managed_paths symlink for it" >&2
fail_count=$((fail_count + 1))
fi
done
done < <(grep -hE '^[[:space:]]*Exec(Start|StartPre|StartPost|Stop|StopPost|Reload)=' \
"$unit" 2>/dev/null || true)
done
Expand Down
21 changes: 17 additions & 4 deletions runtime-overlay/scripts/lib/stage-feed.sh
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,24 @@ fetch_repo "$FEED_SRC" "$FEED_REPO" "$FEED_REF"
FEED_SHA="$(git -C "$FEED_SRC" rev-parse HEAD)"

# Daemon wrappers → share/airplanes/
#
# Glob feed/scripts/airplanes-*.sh, mirroring the unit glob below, so a wrapper
# added in feed lands on the image automatically. The previous hand-listed
# allowlist (airplanes-feed/mlat/diagnostics.sh) silently dropped
# airplanes-stats.sh when feed added it — the overlay shipped airplanes-stats.timer
# (globbed) pointing at a script that wasn't staged. apl-feed.sh is NOT matched
# (apl-* prefix) and is staged to bin/ below; the runtime libs under
# scripts/lib/ stay an explicit curated subset (install/update-only libs are
# deliberately excluded — see below).
install -d -m 0755 "$OUTPUT_DIR/share/airplanes"
for wrapper in airplanes-feed.sh airplanes-mlat.sh airplanes-diagnostics.sh; do
if [[ -f "$FEED_SRC/scripts/$wrapper" ]]; then
install -m 0755 "$FEED_SRC/scripts/$wrapper" "$OUTPUT_DIR/share/airplanes/$wrapper"
fi
shopt -s nullglob
feed_wrappers=("$FEED_SRC"/scripts/airplanes-*.sh)
shopt -u nullglob
if [[ ${#feed_wrappers[@]} -eq 0 ]]; then
die "no airplanes-*.sh daemon wrappers found in $FEED_SRC/scripts/"
fi
for wrapper in "${feed_wrappers[@]}"; do
install -m 0755 "$wrapper" "$OUTPUT_DIR/share/airplanes/$(basename "$wrapper")"
done

# apl-feed CLI entry point → bin/
Expand Down
43 changes: 43 additions & 0 deletions test/runtime-overlay/test_stage_feed_unit_symmetry.bats
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,23 @@ _feed_units() {
done | LC_ALL=C sort
}

# Collect every airplanes-*.sh daemon wrapper feed ships from scripts/ — the
# top-level wrappers stage-feed.sh installs into share/airplanes/. Excludes
# apl-feed.sh (apl-* CLI entry point → bin/) and the curated scripts/lib/
# runtime libs. Empty result in a populated checkout is structural breakage.
_feed_wrappers() {
local f
shopt -s nullglob
local -a wrappers=("$FEED_SRC"/scripts/airplanes-*.sh)
shopt -u nullglob
if (( ${#wrappers[@]} == 0 )); then
return 1
fi
for f in "${wrappers[@]}"; do
basename "$f"
done | LC_ALL=C sort
}

# A unit "needs enabling" iff it carries an [Install] section that wires it
# into a target at boot. Oneshot timer-driven services carry no [Install] —
# the timer's `Unit=` directive activates them — and must stay out of the
Expand Down Expand Up @@ -105,6 +122,32 @@ _unit_needs_enable() {
fi
}

@test "every feed airplanes-*.sh wrapper is symlinked by managed_paths.json" {
local wrappers_out
wrappers_out="$(_feed_wrappers)" \
|| { echo "no airplanes-*.sh wrappers in $FEED_SRC/scripts/ — feed restructure?" >&2; return 1; }
mapfile -t wrappers <<< "$wrappers_out"

local missing=()
local wrapper expected_link expected_target
for wrapper in "${wrappers[@]}"; do
expected_link="/usr/local/share/airplanes/$wrapper"
expected_target="/opt/airplanes-runtime/current/share/airplanes/$wrapper"
if ! jq -e --arg link "$expected_link" --arg target "$expected_target" '
any(.[]; .mode == "symlink" and .link == $link and .target == $target)
' "$MANAGED_PATHS_JSON" >/dev/null; then
missing+=("$wrapper")
fi
done

if (( ${#missing[@]} > 0 )); then
printf 'managed_paths.json missing symlink for feed wrapper: %s\n' "${missing[@]}" >&2
echo "expected entry shape:" >&2
echo ' { "mode": "symlink", "link": "/usr/local/share/airplanes/<wrapper>.sh", "target": "/opt/airplanes-runtime/current/share/airplanes/<wrapper>.sh" }' >&2
return 1
fi
}

@test "systemd.json enable matches feed units with [Install] sections" {
local units_out
units_out="$(_feed_units)" \
Expand Down