Skip to content

Commit 1e0629b

Browse files
feat(updates): add safe deployment upgrade primitives
1 parent 11d6566 commit 1e0629b

1 file changed

Lines changed: 148 additions & 20 deletions

File tree

deployment-files/run-fleet.sh

Lines changed: 148 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,21 @@
44
# Proto Fleet Installation and Setup Script
55
# ============================================================================
66

7-
PROJECT_ROOT="$(pwd)"
7+
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
88
COMPOSE_FILE="$PROJECT_ROOT/docker-compose.yaml"
99
COMPOSE_ALERTS_FILE="$PROJECT_ROOT/docker-compose.alerts.yaml"
1010
COMPOSE_SYSTEM_MONITORING_FILE="$PROJECT_ROOT/docker-compose.system-monitoring.yaml"
1111
COMPOSE_TRACING_FILE="$PROJECT_ROOT/docker-compose.tracing.yaml"
12+
COMPOSE_UPDATER_FILE="$PROJECT_ROOT/docker-compose.updater.yaml"
1213
ENV_FILE="$PROJECT_ROOT/.env"
1314

1415
ENABLE_BETA_ALERTS=false
1516
ENABLE_SYSTEM_MONITORING=false
1617
ENABLE_TRACING=false
18+
ENABLE_ONE_CLICK_UPDATES=false
19+
NON_INTERACTIVE=false
20+
PREFLIGHT_ONLY=false
21+
SKIP_BUILD=false
1722

1823
# How long the post-start steps wait for fleet-api to finish its migrations.
1924
# 300 x 2s = 10 minutes: a first boot on SD-card-class hardware (Raspberry Pi)
@@ -49,6 +54,17 @@ Options:
4954
the .env file. Off by default. Can also be
5055
enabled by setting ENABLE_TRACING=true in
5156
the .env file.
57+
--enable-one-click-updates Connect fleet-api to the host updater Unix
58+
socket. The installer sets this after the
59+
systemd updater is installed successfully.
60+
--non-interactive Reuse complete persisted configuration and
61+
fail instead of prompting. Intended for the
62+
host updater, not first-time setup.
63+
--preflight-only Validate configuration, load release images,
64+
and build the new stack without stopping the
65+
running deployment.
66+
--skip-build Skip image preparation because a successful
67+
preflight already prepared this exact release.
5268
-h, --help Show this help and exit.
5369
EOF
5470
}
@@ -67,6 +83,22 @@ while [ $# -gt 0 ]; do
6783
ENABLE_TRACING=true
6884
shift
6985
;;
86+
--enable-one-click-updates)
87+
ENABLE_ONE_CLICK_UPDATES=true
88+
shift
89+
;;
90+
--non-interactive)
91+
NON_INTERACTIVE=true
92+
shift
93+
;;
94+
--preflight-only)
95+
PREFLIGHT_ONLY=true
96+
shift
97+
;;
98+
--skip-build)
99+
SKIP_BUILD=true
100+
shift
101+
;;
70102
-h|--help)
71103
usage
72104
exit 0
@@ -83,17 +115,25 @@ while [ $# -gt 0 ]; do
83115
esac
84116
done
85117

118+
if [ "$PREFLIGHT_ONLY" = "true" ] && [ "$SKIP_BUILD" = "true" ]; then
119+
echo "Error: --preflight-only and --skip-build cannot be combined." >&2
120+
exit 1
121+
fi
122+
86123
# Also honor ENABLE_BETA_ALERTS=true from the .env file.
87-
if grep -Eqi "^ENABLE_BETA_ALERTS=true$" "$ENV_FILE" 2>/dev/null; then
124+
if grep -Eqi "^ENABLE_BETA_ALERTS=true[[:space:]]*$" "$ENV_FILE" 2>/dev/null; then
88125
ENABLE_BETA_ALERTS=true
89126
fi
90-
if grep -Eqi "^ENABLE_SYSTEM_MONITORING=true$" "$ENV_FILE" 2>/dev/null; then
127+
if grep -Eqi "^ENABLE_SYSTEM_MONITORING=true[[:space:]]*$" "$ENV_FILE" 2>/dev/null; then
91128
ENABLE_SYSTEM_MONITORING=true
92129
fi
93130
# [[:space:]]*$ tolerates CRLF line endings from Windows/WSL-edited .env files.
94131
if grep -Eqi "^ENABLE_TRACING=true[[:space:]]*$" "$ENV_FILE" 2>/dev/null; then
95132
ENABLE_TRACING=true
96133
fi
134+
if grep -Eqi "^ENABLE_ONE_CLICK_UPDATES=true[[:space:]]*$" "$ENV_FILE" 2>/dev/null; then
135+
ENABLE_ONE_CLICK_UPDATES=true
136+
fi
97137

98138
# System monitoring rides the alerts stack (the in-process metrics writer,
99139
# Grafana rule evaluation, and webhook delivery are all alerts-gated), so it
@@ -131,6 +171,13 @@ refresh_compose_files() {
131171
if [ "$ENABLE_TRACING" = "true" ] && [ -f "$COMPOSE_TRACING_FILE" ]; then
132172
COMPOSE_FILES+=(-f "$COMPOSE_TRACING_FILE")
133173
fi
174+
if [ "$ENABLE_ONE_CLICK_UPDATES" = "true" ]; then
175+
if [ ! -f "$COMPOSE_UPDATER_FILE" ]; then
176+
echo "Error: one-click updates are enabled but $COMPOSE_UPDATER_FILE is missing." >&2
177+
exit 1
178+
fi
179+
COMPOSE_FILES+=(-f "$COMPOSE_UPDATER_FILE")
180+
fi
134181
}
135182
refresh_compose_files
136183

@@ -386,6 +433,10 @@ fix_wsl_networking() {
386433
# ----------------------------------------------------------------------------
387434

388435
if ! command -v docker &> /dev/null; then
436+
if [ "$NON_INTERACTIVE" = "true" ]; then
437+
echo "Error: Docker is not installed; non-interactive upgrade cannot install host prerequisites." >&2
438+
exit 1
439+
fi
389440
echo "Docker is not installed. Attempting to install Docker..."
390441

391442
if [ "$(uname)" == "Linux" ]; then
@@ -409,6 +460,10 @@ fi
409460
if [ "$(uname)" == "Linux" ]; then
410461
# Check if Docker is set to start on boot
411462
if ! systemctl is-enabled docker &>/dev/null; then
463+
if [ "$NON_INTERACTIVE" = "true" ]; then
464+
echo "Error: Docker is not enabled at boot; fix the host before retrying the upgrade." >&2
465+
exit 1
466+
fi
412467
echo "Configuring Docker to start on system boot..."
413468
sudo systemctl enable docker
414469
fi
@@ -420,6 +475,10 @@ if [ "$(uname)" == "Linux" ]; then
420475
# for the sudo-mismatch detection in install.sh) would exit here telling
421476
# the user to log out and back in, leaving the upgrade half-applied.
422477
if [ "$(id -u)" -ne 0 ] && ! groups $USER | grep -q '\bdocker\b'; then
478+
if [ "$NON_INTERACTIVE" = "true" ]; then
479+
echo "Error: the upgrade user cannot access Docker." >&2
480+
exit 1
481+
fi
423482
echo "Adding current user to the docker group for passwordless Docker usage..."
424483
sudo usermod -aG docker $USER
425484
echo "Please log out and log back in to apply group changes, then re-run this script."
@@ -432,6 +491,10 @@ fi
432491
# ----------------------------------------------------------------------------
433492

434493
if ! docker info > /dev/null 2>&1; then
494+
if [ "$NON_INTERACTIVE" = "true" ]; then
495+
echo "Error: Docker daemon is not available; non-interactive upgrade will not mutate host services." >&2
496+
exit 1
497+
fi
435498
echo "Docker daemon is not running. Starting Docker..."
436499

437500
# For macOS, attempt to start Docker Desktop
@@ -485,6 +548,10 @@ fi
485548
# ----------------------------------------------------------------------------
486549

487550
if ! docker compose version &> /dev/null; then
551+
if [ "$NON_INTERACTIVE" = "true" ]; then
552+
echo "Error: docker compose is not installed; non-interactive upgrade cannot install host prerequisites." >&2
553+
exit 1
554+
fi
488555
echo "docker compose is not installed. Attempting to install it..."
489556

490557
if [ "$(uname)" == "Linux" ]; then
@@ -582,7 +649,9 @@ prompt_fleet_profile() {
582649
# curl | bash installs reach prompts with stdin at EOF; never let an
583650
# unanswered prompt persist a profile
584651
maybe_prompt_fleet_profile() {
585-
if [ -t 0 ]; then
652+
if [ "$NON_INTERACTIVE" = "true" ]; then
653+
echo "No FLEET_PROFILE is persisted; keeping the existing conservative defaults."
654+
elif [ -t 0 ]; then
586655
prompt_fleet_profile
587656
else
588657
echo "Hint: host profiles are available; set FLEET_PROFILE=standard|mini|max in $ENV_FILE and re-run to tune for this hardware."
@@ -610,8 +679,12 @@ if [ -f "$ENV_FILE" ]; then
610679
done
611680

612681
if [ $missing_keys -eq 0 ]; then
613-
echo -n "Existing environment file found with all required keys. Use it? (Y/n): "
614-
read use_existing_creds
682+
if [ "$NON_INTERACTIVE" = "true" ]; then
683+
use_existing_creds="y"
684+
else
685+
echo -n "Existing environment file found with all required keys. Use it? (Y/n): "
686+
read use_existing_creds
687+
fi
615688
if [[ -z "$use_existing_creds" || $use_existing_creds =~ ^[Yy]$ ]]; then
616689
use_existing="yes"
617690
echo "Using existing environment file."
@@ -623,11 +696,20 @@ if [ -f "$ENV_FILE" ]; then
623696
prompt_store_reinit || { echo "Aborting due to existing data volume."; exit 1; }
624697
fi
625698
else
699+
if [ "$NON_INTERACTIVE" = "true" ]; then
700+
echo "Error: existing environment file is incomplete; refusing to regenerate secrets during an upgrade." >&2
701+
exit 1
702+
fi
626703
echo "Existing environment file is incomplete. Regenerating…"
627704
prompt_store_reinit || { echo "Cannot proceed with incomplete env + existing data."; exit 1; }
628705
fi
629706
fi
630707

708+
if [ "$NON_INTERACTIVE" = "true" ] && [ "$use_existing" = "no" ]; then
709+
echo "Error: non-interactive mode requires an existing complete $ENV_FILE." >&2
710+
exit 1
711+
fi
712+
631713
# ----------------------------------------------------------------------------
632714
# Generate New Environment Configuration
633715
# ----------------------------------------------------------------------------
@@ -708,6 +790,31 @@ if [ "$use_existing" == "no" ]; then
708790
echo "Environment variables saved to $ENV_FILE"
709791
fi
710792

793+
# Persist every deployment overlay as explicit state. Historically, flags were
794+
# process-only, so the next upgrade could silently disable alerts, monitoring,
795+
# or tracing. Last value wins, matching Compose's .env behavior.
796+
persist_boolean_setting() {
797+
local key="$1"
798+
local value="$2"
799+
local temp
800+
temp=$(mktemp)
801+
grep -v "^${key}=" "$ENV_FILE" > "$temp" || true
802+
if [ -s "$temp" ] && [ -n "$(tail -c1 "$temp")" ]; then
803+
echo >> "$temp"
804+
fi
805+
echo "${key}=${value}" >> "$temp"
806+
cat "$temp" > "$ENV_FILE"
807+
rm -f "$temp"
808+
}
809+
810+
persist_boolean_setting ENABLE_BETA_ALERTS "$ENABLE_BETA_ALERTS"
811+
persist_boolean_setting ENABLE_SYSTEM_MONITORING "$ENABLE_SYSTEM_MONITORING"
812+
persist_boolean_setting ENABLE_TRACING "$ENABLE_TRACING"
813+
persist_boolean_setting ENABLE_ONE_CLICK_UPDATES "$ENABLE_ONE_CLICK_UPDATES"
814+
chmod 600 "$ENV_FILE"
815+
refresh_compose_files
816+
refresh_compose_env_args
817+
711818
# ----------------------------------------------------------------------------
712819
# Docker Compose File Validation
713820
# ----------------------------------------------------------------------------
@@ -807,6 +914,14 @@ if [ -f "$SSL_CERT" ] && [ -f "$SSL_KEY" ]; then
807914
echo " Private Key: $SSL_KEY"
808915
PROTOCOL_MODE="https"
809916
else
917+
if [ "$NON_INTERACTIVE" = "true" ]; then
918+
if grep -Eqi "^SESSION_COOKIE_SECURE=true[[:space:]]*$" "$ENV_FILE"; then
919+
echo "Error: HTTPS is persisted but its certificate/key are missing; refusing to switch protocol during upgrade." >&2
920+
exit 1
921+
fi
922+
echo "No SSL certificates found; preserving HTTP mode from $ENV_FILE."
923+
PROTOCOL_MODE="http"
924+
else
810925
echo ""
811926
echo "No SSL certificates found in $SSL_DIR"
812927
echo ""
@@ -842,6 +957,7 @@ else
842957
PROTOCOL_MODE="http"
843958
;;
844959
esac
960+
fi
845961
fi
846962

847963
echo ""
@@ -882,26 +998,38 @@ refresh_compose_env_args
882998
# Docker Image Preparation
883999
# ----------------------------------------------------------------------------
8841000

885-
echo "Pulling latest Docker images..."
886-
compose pull
1001+
if [ "$SKIP_BUILD" != "true" ]; then
1002+
echo "Pulling latest Docker images..."
1003+
if ! compose pull; then
1004+
echo "Error: Failed to pull required Docker images."
1005+
exit 1
1006+
fi
8871007

888-
# Load pre-built TimescaleDB image if available (built in CI for the target architecture)
889-
TSDB_IMAGE="$PROJECT_ROOT/images/timescaledb.tar.gz"
890-
if [ -f "$TSDB_IMAGE" ]; then
891-
echo "Loading pre-built TimescaleDB image..."
892-
if gunzip -c "$TSDB_IMAGE" | docker load; then
893-
echo "TimescaleDB image loaded successfully."
1008+
# Load pre-built TimescaleDB image if available (built in CI for the target architecture)
1009+
TSDB_IMAGE="$PROJECT_ROOT/images/timescaledb.tar.gz"
1010+
if [ -f "$TSDB_IMAGE" ]; then
1011+
echo "Loading pre-built TimescaleDB image..."
1012+
if gunzip -c "$TSDB_IMAGE" | docker load; then
1013+
echo "TimescaleDB image loaded successfully."
1014+
else
1015+
echo "Error: Failed to load TimescaleDB image from $TSDB_IMAGE"
1016+
exit 1
1017+
fi
8941018
else
895-
echo "Error: Failed to load TimescaleDB image from $TSDB_IMAGE"
896-
exit 1
1019+
echo "Warning: Pre-built TimescaleDB image not found at $TSDB_IMAGE."
1020+
echo "The deployment will fail unless the image 'proto-fleet-timescaledb:latest' already exists locally."
8971021
fi
1022+
1023+
# Build Docker images (fleet-api and fleet-client only; TimescaleDB uses pre-built image)
1024+
compose build --no-cache || { echo "Error: Build failed. Exiting."; exit 1; }
8981025
else
899-
echo "Warning: Pre-built TimescaleDB image not found at $TSDB_IMAGE."
900-
echo "The deployment will fail unless the image 'proto-fleet-timescaledb:latest' already exists locally."
1026+
echo "Skipping image preparation; this release already passed updater preflight."
9011027
fi
9021028

903-
# Build Docker images (fleet-api and fleet-client only; TimescaleDB uses pre-built image)
904-
compose build --no-cache || { echo "Error: Build failed. Exiting."; exit 1; }
1029+
if [ "$PREFLIGHT_ONLY" = "true" ]; then
1030+
echo "Upgrade preflight completed successfully; the running stack was not stopped."
1031+
exit 0
1032+
fi
9051033

9061034
# ----------------------------------------------------------------------------
9071035
# Service Management

0 commit comments

Comments
 (0)