From cc0672557cc487ce6dabe50c71da6b63cf1c15bb Mon Sep 17 00:00:00 2001 From: Lemon-miaow Date: Sun, 27 Sep 2026 17:24:33 +0800 Subject: [PATCH] =?UTF-8?q?fix(bootstrap):=20=E4=B8=8B=E8=BD=BD=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E7=89=88=E4=BA=8C=E8=BF=9B=E5=88=B6=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E6=99=AE=E9=80=9A=E8=AF=AD=E5=8F=A5=E8=B0=83=E7=94=A8=EF=BC=8C?= =?UTF-8?q?=E5=9B=9E=E9=80=80=E7=9C=8B=20HAVE=5FPREBUILT=5FBINARY=EF=BC=8C?= =?UTF-8?q?errexit=20=E8=A6=86=E7=9B=96=E6=9B=BF=E6=8D=A2=20HOST=5FBIN=20?= =?UTF-8?q?=E7=9A=84=E6=AD=A5=E9=AA=A4=EF=BC=9B=E6=97=A7=E4=BA=8C=E8=BF=9B?= =?UTF-8?q?=E5=88=B6=E5=A4=87=E4=BB=BD=E6=B2=A1=E6=8B=B7=E5=AE=8C=E5=B0=B1?= =?UTF-8?q?=E5=88=A0=E6=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/bootstrap.sh | 62 +++++++++++++--------- deploy/bootstrap_test.sh | 111 ++++++++++++++++++++++++++++++++------- 2 files changed, 131 insertions(+), 42 deletions(-) diff --git a/deploy/bootstrap.sh b/deploy/bootstrap.sh index bc9f602..72e5518 100644 --- a/deploy/bootstrap.sh +++ b/deploy/bootstrap.sh @@ -609,7 +609,12 @@ keep_previous_host_binary() { [ -x "$HOST_BIN" ] || return 0 HOST_BIN_PREV="${HOST_BIN}.prev" rm -f "$HOST_BIN_PREV" - cp "$HOST_BIN" "$HOST_BIN_PREV" + # A copy cut short (a full disk) is no copy: the failed run would install it over the + # binary it was taken from. + if ! cp "$HOST_BIN" "$HOST_BIN_PREV"; then + rm -f "$HOST_BIN_PREV" + return 1 + fi } restore_previous_host_binary() { # exit-status @@ -2427,9 +2432,13 @@ use_release_binary() { [ "$FELIS_VERSION_BOOTSTRAP" = "release" ] } -# download_release_binary installs the prebuilt felis binary for $FELIS_REF onto the host. -# It returns non-zero to ask the caller to build that same tag from source instead; it never -# dies, because no reachable failure here is worth aborting an install over. +# download_release_binary installs the prebuilt felis binary for $FELIS_REF onto the host and +# sets HAVE_PREBUILT_BINARY. It leaves HAVE_PREBUILT_BINARY empty, and returns 0, to ask the +# caller to build that same tag from source instead: a release without a usable asset is no +# reason to abort an install. Callers run it as a plain command, so errexit covers the steps +# that replace HOST_BIN. Bash turns errexit off inside a function run as an if or && condition, +# and there an install(1) that failed after the rm below left no binary at all while the +# install went on as if it had one. # # One asset covers the panel too: internal/panel/panel.go go:embeds internal/panel/static, and # release.yml builds through the repo Dockerfile so that tree holds the real npm output rather @@ -2438,7 +2447,7 @@ download_release_binary() { local arch asset tmp got if ! arch="$(felis_asset_arch)"; then warn "no prebuilt felis binary for architecture $(uname -m); building ${FELIS_REF} from source on this host instead" - return 1 + return 0 fi asset="felis-linux-${arch}" @@ -2465,7 +2474,7 @@ download_release_binary() { # different commit than the tag the operator asked for. Falling back keeps the tag and # changes only how it is obtained, so no operator action is needed at all. warn "release ${FELIS_REF} publishes no usable ${asset}; building ${FELIS_REF} from source on this host instead" - return 1 + return 0 fi # The hash comes BEFORE the exec below: until the file matches the release's SHA256SUMS it @@ -2474,7 +2483,7 @@ download_release_binary() { # fall back to the source build of the same tag, which trusts only the git fetch. if ! verify_release_checksum "$FELIS_REF" "$asset" "$tmp"; then rm -f "$tmp" - return 1 + return 0 fi chmod 0755 "$tmp" @@ -2487,7 +2496,7 @@ download_release_binary() { if [ "$got" != "felis ${FELIS_REF}" ]; then rm -f "$tmp" warn "downloaded ${asset} reports '${got:-nothing}' rather than 'felis ${FELIS_REF}'; building ${FELIS_REF} from source on this host instead" - return 1 + return 0 fi # install(1) onto a freshly created destination, matching install_embedded_binary. A rename @@ -5679,15 +5688,32 @@ build_nano_binary() { ok "felis binary on host at ${HOST_BIN}" } +# acquire_felis_binary is the full install's four ways to a felis binary, in preference +# order. FELIS_ARTIFACT_DIR and the release download skip compiling: each is the CI artifact +# for its tag, panel included. The other arms leave HAVE_PREBUILT_BINARY unset where a source +# build is what actually happens, which is what routes build_image. Neither the download nor +# fetch_source is an if condition or a command before && or ||: errexit is off inside those. +acquire_felis_binary() { + if [ -n "$FELIS_ARTIFACT_DIR" ]; then + install_artifact_binary + elif bootstrap_from_tui; then + install_embedded_binary + else + if use_release_binary; then + download_release_binary + fi + [ -n "$HAVE_PREBUILT_BINARY" ] || fetch_source + fi +} + acquire_nano_binary() { # The release channel takes the same prebuilt binary the control plane does. This is the # biggest win on this path: a host that only wants the auth multiplexer stops needing a Go # toolchain and a checkout at all. if use_release_binary; then resolve_install_ref - if download_release_binary; then - return 0 - fi + download_release_binary + [ -z "$HAVE_PREBUILT_BINARY" ] || return 0 fi # Raw curl|bash with no usable release: build it straight from source with a pinned Go # toolchain — nano needs one static binary, not an image, so dragging in docker @@ -5930,19 +5956,7 @@ main() { # The registry mirror must exist before the bundle's pods start pulling (and # before any re-run's rollouts). configure_registry_mirror - # Four ways to end up with a felis binary, in preference order. FELIS_ARTIFACT_DIR and the - # release download skip compiling: each is the CI artifact for its tag, panel included. The - # other arms leave HAVE_PREBUILT_BINARY unset where a source build is what actually - # happens, which is what routes build_image below. - if [ -n "$FELIS_ARTIFACT_DIR" ]; then - install_artifact_binary - elif bootstrap_from_tui; then - install_embedded_binary - elif use_release_binary && download_release_binary; then - : - else - fetch_source - fi + acquire_felis_binary select_release_artifacts resolve_felis_image # The registry's and the database's own images must be in containerd before their diff --git a/deploy/bootstrap_test.sh b/deploy/bootstrap_test.sh index ee9f9c2..56041af 100644 --- a/deploy/bootstrap_test.sh +++ b/deploy/bootstrap_test.sh @@ -984,25 +984,41 @@ out="$(run_verify "")" expect "a release without SHA256SUMS falls back to a source build" "publishes no SHA256SUMS" "$out" case "$out" in *VERIFIED*) echo "FAIL: a release without SHA256SUMS must not verify"; fails=$((fails + 1)) ;; esac -# download_release_binary itself, with the real verify_release_checksum: the stand-in asset -# writes to the same log as ok and warn each time it runs, so the log shows whether the hash -# was checked before the file was first executed, and that a refused one never runs at all. +# download_release_binary itself, with the real verify_release_checksum, run the way the +# installer runs it: under set -Eeuo pipefail, from acquire_felis_binary or +# acquire_nano_binary. The stand-in asset writes to the same log as ok and warn each time it +# runs, so the log shows whether the hash was checked before the file was first executed, +# and that a refused one never runs at all. dblock="$(awk '/^download_release_binary\(\) \{/,/^}/' "$BS")" [ -n "$dblock" ] || { echo "FAIL: no download_release_binary found in $BS"; exit 1; } [ "$(printf '%s\n' "$dblock" | wc -l)" -lt 100 ] \ || { echo "FAIL: the extracted block is not the function -- did its closing brace move?"; exit 1; } +acqblock="" +for fn in acquire_felis_binary acquire_nano_binary; do + blk="$(awk "/^${fn}\\(\\) \\{/,/^}/" "$BS")" + [ -n "$blk" ] || { echo "FAIL: no ${fn} found in $BS"; exit 1; } + [ "$(printf '%s\n' "$blk" | wc -l)" -lt 40 ] \ + || { echo "FAIL: the extracted block is not ${fn} -- did its closing brace move?"; exit 1; } + acqblock="${acqblock}${blk} +" +done ddir="$sdir/download" mkdir -p "$ddir/bin" printf '#!/bin/sh\necho "RAN: $*" >> "%s"\necho "felis v9.9.9"\n' "$ddir/log" > "$ddir/asset" dsum="$(sha256sum <"$ddir/asset" | cut -d' ' -f1)" -run_download() { # SHA256SUMS-content ("" = the release has none); the log lands in $ddir/log +# run_download SUMS [entry]: SHA256SUMS-content ("" = the release has none), and the caller +# (acquire_felis_binary by default). INSTALL_FAILS makes install(1) fail, FETCH_FAILS a +# command inside fetch_source. The log lands in $ddir/log; stdout says what the caller did. +run_download() { rm -f "$ddir/bin/felis" : > "$ddir/log" - SUMS="$1" ASSET="$ddir/asset" LOG="$ddir/log" FELIS_REF=v9.9.9 HOST_BIN="$ddir/bin/felis" \ - TMPDIR="$sdir" bash -c ' + SUMS="$1" ENTRY="${2:-acquire_felis_binary}" ASSET="$ddir/asset" LOG="$ddir/log" FELIS_REF=v9.9.9 \ + HOST_BIN="$ddir/bin/felis" TMPDIR="$sdir" INSTALL_FAILS="${INSTALL_FAILS:-}" FETCH_FAILS="${FETCH_FAILS:-}" bash -c ' + set -Eeuo pipefail ok() { printf "OK: %s\n" "$*" >> "$LOG"; } warn() { printf "WARN: %s\n" "$*" >> "$LOG"; } + log() { :; } remember_temp() { :; } felis_asset_arch() { echo amd64; } keep_previous_host_binary() { :; } @@ -1013,42 +1029,81 @@ run_download() { # SHA256SUMS-content ("" = the release has none); the log lands *) return 1 ;; esac } + use_release_binary() { :; } + bootstrap_from_tui() { return 1; } + resolve_install_ref() { :; } + fetch_source() { echo FETCH_SOURCE; [ -z "$FETCH_FAILS" ] || { false; echo "FETCH WENT ON"; }; } + install_go_toolchain() { :; } + build_nano_binary() { echo BUILD_NANO; } + [ -z "$INSTALL_FAILS" ] || install() { echo "install: No space left on device" >&2; return 1; } + FELIS_ARTIFACT_DIR="" HAVE_PREBUILT_BINARY="" '"$vblock"' '"$dblock"' - download_release_binary' + '"$acqblock"' + "$ENTRY" + echo "PREBUILT[$HAVE_PREBUILT_BINARY]"' 2>&1 } same_log() { # label want if [ "$(cat "$ddir/log")" = "$2" ]; then echo "PASS $1"; else printf 'FAIL %s: got\n%s\nwant\n%s\n' "$1" "$(cat "$ddir/log")" "$2"; fails=$((fails + 1)); fi } +same_out() { # label want got-rc: the caller's exit status and its whole output + if [ "$3" = 0 ] && [ "$out" = "$2" ]; then echo "PASS $1"; else printf 'FAIL %s: exit %s, got\n%s\nwant\n%s\n' "$1" "$3" "$out" "$2"; fails=$((fails + 1)); fi +} refused_download() { # label: nothing installed, and the staged download removed if [ -e "$ddir/bin/felis" ]; then echo "FAIL $1: a binary was installed"; fails=$((fails + 1)); else echo "PASS $1 installs nothing"; fi left="$(ls -A "$ddir/bin")" if [ -z "$left" ]; then echo "PASS $1 leaves no staged download"; else echo "FAIL $1 left $left"; fails=$((fails + 1)); fi } +died_midway() { # label rc: the run stopped at the failure, and the caller went no further + if [ "$2" -ne 0 ]; then echo "PASS $1 stops the install"; else echo "FAIL $1 must stop the install"; fails=$((fails + 1)); fi + case "$out" in + *PREBUILT*|*"WENT ON"*|*BUILD_NANO*) echo "FAIL $1 went on: $out"; fails=$((fails + 1)) ;; + *) echo "PASS $1 goes no further" ;; + esac +} -run_download "$(printf '%s felis-linux-amd64\n' "$dsum")" -rc=$? -if [ "$rc" -eq 0 ]; then echo "PASS a verified release binary installs"; else echo "FAIL a verified release binary must install (exit $rc)"; fails=$((fails + 1)); fi +out="$(run_download "$(printf '%s felis-linux-amd64\n' "$dsum")")" +same_out "a verified release binary installs, with no source build" "PREBUILT[1]" $? same_log "the release binary is hashed before it is first executed" "$(printf '%s\n' \ "OK: felis-linux-amd64 matches release v9.9.9's SHA256SUMS" \ "RAN: version" \ "OK: installed felis-linux-amd64 v9.9.9 at $ddir/bin/felis")" if cmp -s "$ddir/asset" "$ddir/bin/felis"; then echo "PASS the installed binary is the download"; else echo "FAIL the installed binary is not the download"; fails=$((fails + 1)); fi -run_download "$(printf '%s felis-linux-amd64\n' deadbeef)" -rc=$? -if [ "$rc" -ne 0 ]; then echo "PASS a mismatched release binary asks for the source build"; else echo "FAIL a mismatched release binary must ask for the source build"; fails=$((fails + 1)); fi +out="$(run_download "$(printf '%s felis-linux-amd64\n' deadbeef)")" +same_out "a mismatched release binary asks for the source build" "FETCH_SOURCE +PREBUILT[]" $? same_log "a mismatched release binary is never executed" \ "WARN: downloaded felis-linux-amd64 hashes to ${dsum}, but release v9.9.9's SHA256SUMS says deadbeef; discarding it and building v9.9.9 from source on this host instead" refused_download "a mismatched release binary" -run_download "" -rc=$? -if [ "$rc" -ne 0 ]; then echo "PASS a release binary without SHA256SUMS asks for the source build"; else echo "FAIL a release binary without SHA256SUMS must ask for the source build"; fails=$((fails + 1)); fi +out="$(run_download "")" +same_out "a release binary without SHA256SUMS asks for the source build" "FETCH_SOURCE +PREBUILT[]" $? same_log "a release binary without SHA256SUMS is never executed" \ "WARN: release v9.9.9 publishes no SHA256SUMS, so felis-linux-amd64 cannot be verified; building v9.9.9 from source on this host instead" refused_download "a release binary without SHA256SUMS" +# errexit reaches inside: run as an if or && condition it did not, and an install(1) that +# failed after HOST_BIN was removed read as an installed binary. +out="$(INSTALL_FAILS=1 run_download "$(printf '%s felis-linux-amd64\n' "$dsum")")" +died_midway "an install(1) of the release binary that fails" $? +case "$(cat "$ddir/log")" in + *"OK: installed"*) echo "FAIL a failed install(1) reported the binary installed"; fails=$((fails + 1)) ;; + *) echo "PASS a failed install(1) reports nothing installed" ;; +esac +out="$(FETCH_FAILS=1 run_download "")" +died_midway "a failure inside the source fetch the download fell back to" $? + +out="$(run_download "$(printf '%s felis-linux-amd64\n' "$dsum")" acquire_nano_binary)" +same_out "nano: a verified release binary installs, with no source build" "PREBUILT[1]" $? +out="$(run_download "" acquire_nano_binary)" +same_out "nano: no usable release binary builds nano from source" "FETCH_SOURCE +BUILD_NANO +PREBUILT[]" $? +out="$(INSTALL_FAILS=1 run_download "$(printf '%s felis-linux-amd64\n' "$dsum")" acquire_nano_binary)" +died_midway "nano: an install(1) of the release binary that fails" $? + # --- cloudflared is a pinned release, checked before it is installed --------------------- cfblock="$(awk '/^install_cloudflared\(\) \{/,/^}/' "$BS")" [ -n "$cfblock" ] || { echo "FAIL: no install_cloudflared found in $BS"; exit 1; } @@ -2720,9 +2775,9 @@ esac # The binary names the release, the release names the images, and each image is looked for in # the release's bundles before anything is pulled or built. -imorder="$(awk '/^main\(\) \{/,/^}/' "$BS" | grep -nE '^[[:space:]]*(install_k3s|configure_registry_mirror|install_artifact_binary|fetch_source|select_release_artifacts|resolve_felis_image|import_release_images felis registry postgres|import_platform_images|build_image|pin_platform_images)$' | sed 's/^[0-9]*:[[:space:]]*//' | tr '\n' ' ')" +imorder="$(awk '/^main\(\) \{/,/^}/' "$BS" | grep -nE '^[[:space:]]*(install_k3s|configure_registry_mirror|acquire_felis_binary|select_release_artifacts|resolve_felis_image|import_release_images felis registry postgres|import_platform_images|build_image|pin_platform_images)$' | sed 's/^[0-9]*:[[:space:]]*//' | tr '\n' ' ')" expect "main takes the binary, then the release's images, before pulling or building any" \ - "install_k3s configure_registry_mirror install_artifact_binary fetch_source select_release_artifacts resolve_felis_image import_release_images felis registry postgres import_platform_images build_image pin_platform_images " "$imorder" + "install_k3s configure_registry_mirror acquire_felis_binary select_release_artifacts resolve_felis_image import_release_images felis registry postgres import_platform_images build_image pin_platform_images " "$imorder" if awk '/^main\(\) \{/,/^}/' "$BS" | grep -qE '^[[:space:]]*install_docker$'; then echo "FAIL: main installs Docker up front; only a build may (ensure_docker)"; fails=$((fails + 1)) else @@ -2919,6 +2974,26 @@ case "$out" in *"PREV LEFT"*) echo "FAIL: the previous binary copy must be remov out="$(run_host_bin 1 0 fresh)" expect "a first install has nothing to restore" "BIN: new" "$out" case "$out" in *WARN:*) echo "FAIL: a first install must not restore the binary it just installed"; fails=$((fails + 1)) ;; esac +# A copy cut short (a full disk) stops the run, and the exit's restore must not put it over +# the binary it was taken from. +rm -f "$hbdir"/felis* +printf 'old\n' > "$hbdir/felis"; chmod 0755 "$hbdir/felis" +out="$(HOST_BIN="$hbdir/felis" bash -c ' + set -e + warn() { printf "WARN: %s\n" "$*"; } + HOST_BIN_PREV=""; HOST_BIN_KEPT=0; HOST_BIN_IN_USE=0 + '"$(awk '/^keep_previous_host_binary\(\) \{/,/^}/' "$BS")"' + '"$(awk '/^restore_previous_host_binary\(\) \{/,/^}/' "$BS")"' + cp() { printf "ol" > "$2"; return 1; } + on_exit() { restore_previous_host_binary "$?"; printf "BIN: %s\n" "$(cat "$HOST_BIN")"; [ ! -e "$HOST_BIN.prev" ] || echo "PREV LEFT"; } + trap on_exit EXIT + keep_previous_host_binary + echo KEPT' 2>&1)" +expect "a copy of the binary cut short leaves the binary as it was" "BIN: old" "$out" +case "$out" in + *KEPT*|*"PREV LEFT"*) echo "FAIL a copy of the binary cut short went on or was left behind: $out"; fails=$((fails + 1)) ;; + *) echo "PASS a copy of the binary cut short stops the run and is removed" ;; +esac rm -rf "$hbdir"