feat(bootstrap): 安装结束后引导进入设置向导

This commit is contained in:
Lemon-miaow committed 2026-10-02 17:07:52 +08:00
1 parent 94cbd7e120
commit d2a6b6ec73
4 files changed
+188 -15

No files matched your search

+2
View File
@@ -70,6 +70,8 @@ curl -fsSL https://raw.githubusercontent.com/FelisMC/Felis/main/deploy/bootstrap
脚本将安装 K3s,在 K3s 中部署 PostgreSQL 与控制平面,随后启动设置向导。设置完成后,通过浏览器访问所配置的域名即可进入控制面板。
* **设置向导**:向导首先绑定平台所有者:以 Minecraft Java 版加入向导所示的地址,登录服务器会给出 8 位绑定码(10 分钟内有效),将其输入向导即可。该步骤可以跳过,之后再次执行 `sudo felis setup` 补做;绑定所有者之前,任何人均无法登录控制面板。安装器仅在交互式终端中自动启动向导;输出重定向至日志或经由 cloud-init 安装时,请在安装结束后执行 `sudo felis setup`。设置 `FELIS_NO_SETUP=1` 时,安装器在输出摘要后直接结束。
* **支持的系统**:CentOS Stream 9(aarch64)已在实机上验证;Ubuntu 24.04(x86_64)在每次推送时由 CI 执行全新安装、重复安装、升级及上述安装命令(参见 [运维手册 §1](docs/operations.md#1-supported-hosts))。
* **安装前检查**:安装器在修改主机之前检查内存、磁盘、端口、网段冲突、已有的 Kubernetes 及外网连通性。发现问题时一次性列出全部问题并退出,主机保持原状(检查项参见 [运维手册 §1](docs/operations.md#1-supported-hosts))。
+2
View File
@@ -69,6 +69,8 @@ curl -fsSL https://raw.githubusercontent.com/FelisMC/Felis/main/deploy/bootstrap
The script installs K3s, deploys PostgreSQL and the control plane inside it, and launches a setup wizard. When setup completes, open the configured domain in a browser to reach the control panel.
* **Setup wizard**: The wizard first binds the platform Owner: join the address it shows in Minecraft Java Edition, then enter the 8-character link code that the login server displays (valid for 10 minutes). The step can be skipped and completed later by running `sudo felis setup` again; until an Owner is bound, nobody can sign in to the control panel. The installer launches the wizard automatically only on an interactive terminal; when output is redirected to a log or the install runs under cloud-init, run `sudo felis setup` after it finishes. Setting `FELIS_NO_SETUP=1` makes the installer end at its summary.
* **Supported hosts**: CentOS Stream 9 (aarch64) is verified on physical hardware; Ubuntu 24.04 (x86_64) is tested in CI on every push with a fresh install, a rerun, an upgrade and the install command above (see [operations §1](docs/operations.md#1-supported-hosts)).
* **Preflight checks**: Before modifying the host, the installer checks memory, disk, ports, network range conflicts, existing Kubernetes installations and outbound connectivity. If any check fails, it lists all problems and exits, leaving the host unchanged (see [operations §1](docs/operations.md#1-supported-hosts) for the checks).
+85 -13
View File
@@ -32,6 +32,9 @@
# export FELIS_INSTALL_MODE=nano; curl -fsSL <raw-url>/deploy/bootstrap.sh | sudo -E bash
# FELIS_INSTALL_MODE full|nano — skip the prompt (default: ask on a tty, else full; nano
# instead on a host that runs felis-nano and no full install)
# FELIS_NO_SETUP 1 ends a full install at its summary. By default an install that
# leaves no Owner account goes on into `felis setup` when it runs on
# a terminal
# FELIS_NANO_LISTEN listen addr for `felis nano` (default: the address an installed
# felis-nano already uses, else 127.0.0.1:8081 — loopback only; set a
# private-network IP to serve an off-host proxy)
@@ -440,6 +443,10 @@ OFFSITE_ENV="${STATE_DIR}/offsite.env"
# Where summary_offsite shows a newly generated off-site key: the operator's terminal alone.
# stdout and stderr are what `2>&1 | tee install.log`, cloud-init and CI keep on disk.
OFFSITE_KEY_TTY=/dev/tty
# Where the setup console the installer starts at the end reads its keys (setup_terminal).
SETUP_TTY=/dev/tty
# Set by summary_next when the installer goes on into the setup console.
SETUP_CONSOLE=0
# Host copies of the credentials `felis setup` takes at the keyboard, one bare value per
# file, mode 0600 (cmd/felis/hostcreds.go); apply_setup_credential_secrets applies their
# Secrets from them on every run.
@@ -588,13 +595,10 @@ on_error() {
}
cleanup() {
local status=$? id path unit
local status=$? id path
restore_previous_host_binary "$status"
if [ "$status" -ne 0 ]; then undo_postgres_move; fi
for unit in "${PKG_TIMERS_TO_RESTORE[@]-}"; do
[ -n "$unit" ] || continue
systemctl start "$unit" >/dev/null 2>&1 || true
done
resume_package_background_timers
if command -v docker >/dev/null 2>&1; then
for id in "${DOCKER_CONTAINERS[@]-}"; do
[ -n "$id" ] && docker rm "$id" >/dev/null 2>&1 || true
@@ -828,6 +832,17 @@ pause_package_background_timers() {
done
}
# Starts the timers pause_package_background_timers stopped: from the EXIT cleanup, and
# before the setup console, which stays open as long as the operator likes.
resume_package_background_timers() {
local unit
for unit in "${PKG_TIMERS_TO_RESTORE[@]-}"; do
[ -n "$unit" ] || continue
systemctl start "$unit" >/dev/null 2>&1 || true
done
PKG_TIMERS_TO_RESTORE=()
}
pkg_lock_files() {
case "${PKG:-}" in
apt) printf '%s\n' "${APT_LOCK_FILES[@]}" ;;
@@ -5675,14 +5690,6 @@ summary() {
log "The proxy authenticates against Mojang and forwards the verified profile to the"
log "login gate; the backends are reachable in-cluster only. Follow it with:"
log " sudo journalctl -u felis-velocity -f"
if [ "${FELIS_BOOTSTRAP_FROM_TUI:-}" = "1" ]; then
log "Returning to the setup console to create the Owner account and verify panel access."
else
log "Next: run 'sudo felis setup' on this host to create the Owner account."
fi
log "setup provisions the login/lobby servers, then asks the Owner to bind by joining"
log "the proxy in Minecraft — that is what makes the Owner's admin identity a real"
log "Mojang account rather than a password."
log "Use 'sudo felis breakGlass' only for emergency local Owner recovery/reset."
if [ -n "${PREVIOUS_FELIS_IMAGE:-}" ]; then
echo
@@ -5696,6 +5703,70 @@ summary() {
summary_alerts
summary_heartbeat
echo
summary_next
echo
}
# owner_state: whether the database holds a staff account (an Owner or an Admin), the test
# `felis setup` makes to choose between the Owner wizard and its status screen (AdminExists
# in internal/api/pgrepo.go). "unknown" when the database does not answer.
owner_state() {
local out
out="$(pg_exec psql -XtA -U postgres -d "$DB_NAME" -c "SELECT EXISTS (SELECT 1 FROM users WHERE role IN ('admin', 'owner'))" 2>/dev/null || true)"
case "$out" in
t) echo yes ;;
f) echo no ;;
*) echo unknown ;;
esac
}
# setup_terminal: whether the full-screen setup console has a terminal to draw on and to
# read keys from. Under `curl | sudo bash` stdin is the script, so the console reads
# SETUP_TTY; stdout must be the terminal itself, which `| tee install.log`, cloud-init and
# CI are not. /dev/tty is mode 0666 everywhere, so only opening it tells.
setup_terminal() { [ -t 1 ] && (: <"$SETUP_TTY") 2>/dev/null; }
# summary_next is the installer's last word: what the operator does now. It comes after the
# warnings, so it is what the terminal is left showing. Until a staff account exists that is
# `felis setup`, which creates the Owner; on a terminal the installer starts it itself
# (start_setup_console), as the README's install line promises. With an Owner in place it
# is the address to sign in at. Under felis setup the console carries on by itself.
summary_next() {
local owner rule="================================================================================"
if bootstrap_from_tui; then
log "Returning to the setup console to create the Owner account and verify panel access."
return 0
fi
owner="$(owner_state)"
log "$rule"
if [ "$owner" = yes ]; then
log "Felis is running. Sign in at https://$(auth_hostname admin_hostname "op.console.${FELIS_ROOT_DOMAIN}")"
log "(https://${NODE_IP}:${FELIS_PANEL_NODEPORT} until the edge routes it there)."
log "Email, edge and storage settings: sudo felis setup"
log "$rule"
return 0
fi
if [ "$owner" = no ] && [ -z "${FELIS_NO_SETUP:-}" ] && setup_terminal; then
SETUP_CONSOLE=1
log "Next: create the Owner account. The setup console starts now; if you leave it,"
log "run sudo felis setup to come back to it."
else
log "Next: create the Owner account. Run on this host:"
log " sudo felis setup"
fi
log "It starts the login and lobby servers, has you join ${NODE_IP}:${FELIS_GAME_PORT} in Minecraft to"
log "bind your Mojang account as the Owner, then sets up how the panel is reached."
log "$rule"
}
# start_setup_console runs `felis setup` when summary_next said it would. The install has
# succeeded by then, so the console's own failure never fails the run: the EXIT cleanup
# would take that for a failed install and undo the database move.
start_setup_console() {
[ "$SETUP_CONSOLE" = 1 ] || return 0
resume_package_background_timers
"$HOST_BIN" setup <"$SETUP_TTY" \
|| warn "the setup console exited with status $?; run 'sudo felis setup' to come back to it"
}
# ---------------------------------------------------------------------------
@@ -6204,6 +6275,7 @@ main() {
install_watchdog_timer
mark_bootstrap_done
summary
start_setup_console
}
main "$@"
+99 -2
View File
@@ -2428,10 +2428,107 @@ case "$(awk '/^validate_settings\(\) \{/,/^}/' "$BS")" in
*) echo "FAIL validate_settings must call validate_heartbeat_url"; fails=$((fails + 1)) ;;
esac
case "$(awk '/^summary\(\) \{/,/^}/' "$BS")" in
*summary_offsite*summary_alerts*summary_heartbeat*) echo "PASS the install's summary ends on the alerts, then the heartbeat" ;;
*) echo "FAIL summary must call summary_alerts, then summary_heartbeat"; fails=$((fails + 1)) ;;
*summary_offsite*summary_alerts*summary_heartbeat*summary_next*) echo "PASS the install's summary ends on the alerts, the heartbeat, then the next step" ;;
*) echo "FAIL summary must call summary_alerts, summary_heartbeat, then summary_next"; fails=$((fails + 1)) ;;
esac
# --- the installer's last word: what the operator does next ------------------------------
# An install that leaves no Owner ends on `felis setup`, and starts it when there is a
# terminal for it; one with an Owner ends on where to sign in. The setup console must never
# start into a log (`| tee`, cloud-init, CI), and its own failure must not fail the install.
case "$(awk '/^main\(\) \{/,/^}/' "$BS" | tail -n 3)" in
" summary
start_setup_console
}") echo "PASS the full install ends on the summary, then the setup console" ;;
*) echo "FAIL main must end with summary, then start_setup_console"; fails=$((fails + 1)) ;;
esac
nextblock=""
for fn in bootstrap_from_tui resume_package_background_timers owner_state setup_terminal summary_next start_setup_console; do
# A one-line function ends on its own line.
b="$(awk -v fn="$fn" '$0 ~ "^" fn "\\(\\) \\{" { print; if (/\}$/) exit; on = 1; next } on { print } on && /^}/ { exit }' "$BS")"
[ -n "$b" ] || { echo "FAIL: no ${fn} found in $BS"; exit 1; }
[ "$(printf '%s\n' "$b" | wc -l)" -lt 40 ] \
|| { echo "FAIL: the extracted block is not ${fn} -- did its closing brace move?"; exit 1; }
nextblock="${nextblock}${b}
"
done
next_dir="$(mktemp -d)"
printf 'keys from the terminal\n' > "$next_dir/tty"
cat > "$next_dir/felis" <<'XEOF'
#!/bin/sh
echo "felis $*" >> "$NEXT_JOURNAL"
cat > "$NEXT_STDIN"
exit "${SETUP_EXIT:-0}"
XEOF
chmod +x "$next_dir/felis"
# run_next PG_ANSWER: summary_next then start_setup_console under the installer's own shell
# options and ERR trap. PG_ANSWER is what psql prints, or "fail". TERMINAL=1 stands in for
# a terminal on stdout, which a test's captured output never is.
run_next() {
: > "$next_dir/journal"
: > "$next_dir/stdin"
PG_ANSWER="$1" NEXT_JOURNAL="$next_dir/journal" NEXT_STDIN="$next_dir/stdin" \
HOST_BIN="$next_dir/felis" SETUP_TTY="$next_dir/tty" bash -c '
set -Eeuo pipefail
trap "echo TRAP" ERR
log() { printf "LOG: %s\n" "$*"; }; warn() { printf "WARN: %s\n" "$*"; }
auth_hostname() { printf "%s" "$2"; }
pg_exec() { echo "pg_exec $*" >> "$NEXT_JOURNAL"; [ "$PG_ANSWER" != fail ] || return 1; echo "$PG_ANSWER"; }
systemctl() { echo "systemctl $*" >> "$NEXT_JOURNAL"; }
DB_NAME=felis FELIS_ROOT_DOMAIN=example.net NODE_IP=10.0.0.5 FELIS_PANEL_NODEPORT=30443 FELIS_GAME_PORT=25565
SETUP_CONSOLE=0
PKG_TIMERS_TO_RESTORE=(apt-daily.timer)
'"$nextblock"'
if [ "${TERMINAL:-}" = 1 ]; then setup_terminal() { return 0; }; fi
summary_next
start_setup_console
echo "exit 0"' 2>&1
}
started() { grep -q '^felis setup$' "$next_dir/journal"; }
out="$(TERMINAL=1 run_next t)"
expect "with an Owner the summary ends on where to sign in" "LOG: Felis is running. Sign in at https://op.console.example.net" "$out"
expect " and where to change settings" "LOG: Email, edge and storage settings: sudo felis setup" "$out"
if started; then echo "FAIL with an Owner the setup console must not start: $out"; fails=$((fails + 1)); else echo "PASS and the setup console does not start"; fi
admin_roles="$(grep -o "role IN ('admin', 'owner')" "$(dirname "$BS")/../internal/api/pgrepo.go" | head -n 1)"
expect "the Owner test is felis setup's own (AdminExists)" "${admin_roles:-AdminExists query not found}" "$(cat "$next_dir/journal")"
out="$(TERMINAL=1 run_next f)"
expect "with no Owner on a terminal the summary says the setup console starts" "LOG: Next: create the Owner account. The setup console starts now" "$out"
expect " and how it binds the Owner" "has you join 10.0.0.5:25565 in Minecraft" "$out"
if started; then echo "PASS and starts it"; else echo "FAIL the setup console did not start: $out"; fails=$((fails + 1)); fi
expect " on the terminal's keys" "keys from the terminal" "$(cat "$next_dir/stdin")"
case "$(cat "$next_dir/journal")" in
*"systemctl start apt-daily.timer"*"felis setup"*) echo "PASS after the package timers are back" ;;
*) echo "FAIL the package timers must restart before the setup console: $(cat "$next_dir/journal")"; fails=$((fails + 1)) ;;
esac
expect " and the install ends cleanly" "exit 0" "$out"
out="$(run_next f)"
expect "with no Owner and no terminal the summary names the command" "LOG: sudo felis setup" "$out"
if started; then echo "FAIL with no terminal the setup console must not start: $out"; fails=$((fails + 1)); else echo "PASS and the setup console does not start into the log"; fi
out="$(TERMINAL=1 FELIS_NO_SETUP=1 run_next f)"
expect "FELIS_NO_SETUP names the command" "LOG: sudo felis setup" "$out"
if started; then echo "FAIL FELIS_NO_SETUP must keep the setup console closed: $out"; fails=$((fails + 1)); else echo "PASS and keeps the setup console closed"; fi
out="$(TERMINAL=1 run_next fail)"
expect "a database that does not answer names the command" "LOG: sudo felis setup" "$out"
if started; then echo "FAIL an unknown Owner must not start the setup console: $out"; fails=$((fails + 1)); else echo "PASS and the setup console does not start"; fi
out="$(TERMINAL=1 FELIS_BOOTSTRAP_FROM_TUI=1 run_next f)"
expect "under felis setup the console carries on" "LOG: Returning to the setup console to create the Owner account" "$out"
if started; then echo "FAIL under felis setup a second console must not start: $out"; fails=$((fails + 1)); else echo "PASS and no second console starts"; fi
out="$(TERMINAL=1 SETUP_EXIT=3 run_next f)"
expect "a setup console that fails says how to come back" "WARN: the setup console exited with status 3; run 'sudo felis setup' to come back to it" "$out"
expect " and the install still succeeds" "exit 0" "$out"
case "$out" in
*TRAP*) echo "FAIL the setup console's failure must not reach the ERR trap: $out"; fails=$((fails + 1)) ;;
*) echo "PASS without the ERR trap" ;;
esac
rm -rf "$next_dir"
# The watchdog alerts by mail only, through the [smtp] relay. An install without one must
# say that its alerts are only logged; one with a relay must not cry wolf.
sablock="$(awk '/^summary_alerts\(\) \{/,/^}/' "$BS")"