bunker-admin f2284a9cdf Fix curl|bash install: redirect stdin from /dev/tty for interactive prompts
When piped (curl | bash), stdin is the curl output, not the terminal.
All read prompts in config.sh were reading leftover pipe data or EOF,
causing infinite password validation loops and garbage domain values.

Bunker Admin
2026-03-25 19:45:29 -06:00

373 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
# =============================================================================
# Changemaker Lite — One-Line Installer
#
# Downloads the latest release tarball and runs the configuration wizard.
#
# Usage:
# curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/v2/scripts/install.sh | bash
# bash install.sh [OPTIONS]
#
# Options:
# --dir DIR Install directory (default: ~/changemaker.lite)
# --version TAG Specific version tag (default: latest release)
# --tarball FILE Use a local tarball instead of downloading
# --help Show this help
# =============================================================================
set -euo pipefail
GITEA_URL="https://gitea.bnkops.com"
REPO="admin/changemaker.lite"
INSTALL_DIR="${HOME}/changemaker.lite"
VERSION=""
LOCAL_TARBALL=""
MIN_DISK_MB=10000
HEALTH_TIMEOUT=180
HEALTH_INTERVAL=5
# --- State flags for cleanup ---
EXTRACT_DIR=""
TARBALL_PATH=""
CONFIG_COMPLETE=false
# --- Colors ---
if [[ -t 1 ]] && [[ -z "${NO_COLOR:-}" ]]; then
RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m'
BLUE='\033[0;34m' BOLD='\033[1m' NC='\033[0m'
else
RED='' GREEN='' YELLOW='' BLUE='' BOLD='' NC=''
fi
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
success() { echo -e "${GREEN}[OK]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
# --- Cleanup on failure ---
cleanup() {
local exit_code=$?
if [[ $exit_code -ne 0 ]]; then
# Clean up temp extraction directory
if [[ -n "$EXTRACT_DIR" ]] && [[ -d "$EXTRACT_DIR" ]]; then
rm -rf "$EXTRACT_DIR"
fi
# Clean up downloaded tarball (but not user-provided ones)
if [[ -z "$LOCAL_TARBALL" ]] && [[ -n "$TARBALL_PATH" ]] && [[ -f "$TARBALL_PATH" ]]; then
rm -f "$TARBALL_PATH"
fi
# Remove install dir only if config wizard never ran (no user data to lose)
if [[ "$CONFIG_COMPLETE" == "false" ]] && [[ -d "$INSTALL_DIR" ]] && [[ ! -f "$INSTALL_DIR/.env" ]]; then
rm -rf "$INSTALL_DIR"
fi
echo ""
error "Installation failed. See errors above."
fi
}
trap cleanup EXIT
# --- Arg parser ---
while [[ $# -gt 0 ]]; do
case "$1" in
--dir) INSTALL_DIR="$2"; shift 2 ;;
--version) VERSION="$2"; shift 2 ;;
--tarball) LOCAL_TARBALL="$2"; shift 2 ;;
--help|-h)
sed -n '2,20p' "$0" | grep '^#' | sed 's/^# \?//'
exit 0 ;;
*) shift ;;
esac
done
echo -e "${BOLD}Changemaker Lite — Installer${NC}"
echo ""
# =============================================================================
# Step 1: Check prerequisites
# =============================================================================
info "Checking prerequisites..."
MISSING=()
command -v docker >/dev/null 2>&1 || MISSING+=("docker")
docker compose version >/dev/null 2>&1 || MISSING+=("docker-compose-v2")
command -v openssl >/dev/null 2>&1 || MISSING+=("openssl")
command -v curl >/dev/null 2>&1 || MISSING+=("curl")
if [[ ${#MISSING[@]} -gt 0 ]]; then
error "Missing required tools: ${MISSING[*]}"
echo ""
echo "Install Docker: https://docs.docker.com/engine/install/"
echo "Install OpenSSL: apt install openssl (or equivalent)"
exit 1
fi
success "Prerequisites OK (Docker $(docker --version | grep -oP '\d+\.\d+\.\d+'), OpenSSL available)"
# Docker daemon must be running (not just installed)
if ! docker info >/dev/null 2>&1; then
error "Docker daemon is not running."
echo ""
echo " Start it with: sudo systemctl start docker"
echo " Or: sudo service docker start"
exit 1
fi
success "Docker daemon is running"
# Disk space check
AVAILABLE_MB=$(df -m "$(dirname "$INSTALL_DIR")" | awk 'NR==2 {print $4}')
if [[ "$AVAILABLE_MB" -lt "$MIN_DISK_MB" ]]; then
error "Insufficient disk space: ${AVAILABLE_MB}MB available, ${MIN_DISK_MB}MB required."
echo ""
echo " The full stack (images + volumes) needs ~10GB of free space."
exit 1
fi
success "Disk space: ${AVAILABLE_MB}MB available (${MIN_DISK_MB}MB required)"
# =============================================================================
# Step 2: Check install directory
# =============================================================================
if [[ -d "$INSTALL_DIR" ]]; then
if [[ -f "$INSTALL_DIR/docker-compose.yml" ]]; then
error "Changemaker Lite is already installed at $INSTALL_DIR"
echo " To upgrade: cd $INSTALL_DIR && ./scripts/upgrade.sh"
echo " To reinstall: rm -rf $INSTALL_DIR && re-run this script"
exit 1
fi
fi
# =============================================================================
# Step 3: Get tarball
# =============================================================================
if [[ -n "$LOCAL_TARBALL" ]]; then
if [[ ! -f "$LOCAL_TARBALL" ]]; then
error "Tarball not found: $LOCAL_TARBALL"
exit 1
fi
TARBALL_PATH="$LOCAL_TARBALL"
info "Using local tarball: $LOCAL_TARBALL"
else
# Determine download URL
if [[ -n "$VERSION" ]]; then
RELEASE_URL="${GITEA_URL}/api/v1/repos/${REPO}/releases/tags/${VERSION}"
else
RELEASE_URL="${GITEA_URL}/api/v1/repos/${REPO}/releases/latest"
fi
info "Fetching release info from Gitea..."
RELEASE_JSON=$(curl -sf "$RELEASE_URL" 2>/dev/null || true)
if [[ -z "$RELEASE_JSON" ]]; then
error "Could not fetch release info from ${GITEA_URL}"
echo ""
echo "If the server is unreachable:"
echo " 1. Download the tarball manually from ${GITEA_URL}/${REPO}/releases"
echo " 2. Run: bash install.sh --tarball /path/to/changemaker-lite-*.tar.gz"
exit 1
fi
TARBALL_URL=$(echo "$RELEASE_JSON" | python3 -c "
import sys, json
data = json.load(sys.stdin)
assets = data.get('assets', [])
for a in assets:
if a['name'].endswith('.tar.gz'):
print(a['browser_download_url'])
break
" 2>/dev/null || true)
if [[ -z "$TARBALL_URL" ]]; then
error "No tarball found in the release. Check ${GITEA_URL}/${REPO}/releases"
exit 1
fi
RELEASE_TAG=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tag_name','unknown'))" 2>/dev/null)
info "Downloading Changemaker Lite ${RELEASE_TAG}..."
TARBALL_PATH="/tmp/changemaker-lite-install.tar.gz"
curl -fSL "$TARBALL_URL" -o "$TARBALL_PATH"
success "Downloaded $(du -h "$TARBALL_PATH" | cut -f1)"
fi
# =============================================================================
# Step 4: Extract
# =============================================================================
info "Extracting to ${INSTALL_DIR}..."
mkdir -p "$(dirname "$INSTALL_DIR")"
# Extract to temp, then move (handles tarball root directory naming)
EXTRACT_DIR=$(mktemp -d)
tar xzf "$TARBALL_PATH" -C "$EXTRACT_DIR"
# Find the extracted directory (tarball might have any root name)
EXTRACTED=$(find "$EXTRACT_DIR" -maxdepth 1 -mindepth 1 -type d | head -1)
if [[ -z "$EXTRACTED" ]]; then
error "Tarball extraction failed — no directory found"
exit 1
fi
mv "$EXTRACTED" "$INSTALL_DIR"
rm -rf "$EXTRACT_DIR"
EXTRACT_DIR="" # Clear so cleanup doesn't try to remove it
# Clean up downloaded tarball
if [[ -z "$LOCAL_TARBALL" ]] && [[ -f "$TARBALL_PATH" ]]; then
rm -f "$TARBALL_PATH"
fi
success "Extracted to ${INSTALL_DIR}"
# =============================================================================
# Step 5: Run config wizard
# =============================================================================
echo ""
echo -e "${BOLD}Starting configuration wizard...${NC}"
echo ""
cd "$INSTALL_DIR"
# Redirect stdin from terminal — when piped (curl | bash), stdin is the pipe,
# so interactive prompts in config.sh would read garbage instead of user input.
if [[ -e /dev/tty ]]; then
bash config.sh </dev/tty
else
bash config.sh
fi
CONFIG_COMPLETE=true
# =============================================================================
# Step 6: Start services
# =============================================================================
echo ""
echo -e "${BOLD}Configuration complete!${NC}"
echo ""
START_SERVICES="y"
if [[ -t 0 ]]; then
read -rp "Start all services now? [Y/n]: " START_SERVICES
START_SERVICES=${START_SERVICES:-y}
elif [[ -e /dev/tty ]]; then
read -rp "Start all services now? [Y/n]: " START_SERVICES </dev/tty
START_SERVICES=${START_SERVICES:-y}
fi
if [[ "$START_SERVICES" =~ ^[Yy]$ ]]; then
echo ""
info "Pulling images from registry (this may take a few minutes on first run)..."
echo ""
cd "$INSTALL_DIR"
if ! docker compose pull 2>&1; then
echo ""
error "Failed to pull images from the registry."
echo ""
echo " Check that the registry is reachable:"
echo " curl -sf https://gitea.bnkops.com/api/v1/repos/admin/changemaker.lite/releases/latest"
echo ""
echo " If pulling from a private registry, log in first:"
echo " docker login gitea.bnkops.com"
echo ""
echo " Then retry:"
echo " cd ${INSTALL_DIR} && docker compose pull && docker compose up -d"
exit 1
fi
success "All images pulled"
echo ""
info "Starting services..."
if ! docker compose up -d 2>&1; then
echo ""
error "Failed to start services."
echo " Check logs: docker compose logs --tail 30"
exit 1
fi
# --- Post-startup health verification ---
echo ""
info "Waiting for services to become healthy (up to ${HEALTH_TIMEOUT}s)..."
info " Database migrations and seeding run automatically on first boot."
echo ""
CORE_SERVICES=("v2-postgres" "redis" "api" "admin")
ELAPSED=0
ALL_HEALTHY=false
while [[ $ELAPSED -lt $HEALTH_TIMEOUT ]]; do
ALL_HEALTHY=true
for svc in "${CORE_SERVICES[@]}"; do
# Detect crashed containers early
state=$(docker compose ps "$svc" --format '{{.State}}' 2>/dev/null || echo "missing")
if [[ "$state" == "exited" || "$state" == "dead" ]]; then
echo ""
error "Service '${svc}' exited unexpectedly. Last logs:"
docker compose logs "$svc" --tail 20 2>/dev/null || true
echo ""
error "Fix the issue and retry: cd ${INSTALL_DIR} && docker compose up -d"
exit 1
fi
# Check health status
health=$(docker compose ps "$svc" --format '{{.Health}}' 2>/dev/null || echo "")
if [[ "$health" != "healthy" ]]; then
ALL_HEALTHY=false
fi
done
if [[ "$ALL_HEALTHY" == "true" ]]; then
break
fi
sleep "$HEALTH_INTERVAL"
ELAPSED=$((ELAPSED + HEALTH_INTERVAL))
done
# Print status table
echo ""
echo -e "${BOLD} Service Status:${NC}"
for svc in "${CORE_SERVICES[@]}"; do
health=$(docker compose ps "$svc" --format '{{.Health}}' 2>/dev/null || echo "unknown")
state=$(docker compose ps "$svc" --format '{{.State}}' 2>/dev/null || echo "unknown")
if [[ "$health" == "healthy" ]]; then
echo -e " ${GREEN}[healthy]${NC} $svc"
elif [[ "$state" == "running" ]]; then
echo -e " ${YELLOW}[starting]${NC} $svc"
else
echo -e " ${RED}[${state}]${NC} $svc"
fi
done
echo ""
if [[ "$ALL_HEALTHY" == "true" ]]; then
success "All core services are healthy! (${ELAPSED}s)"
echo ""
echo " Admin GUI: http://localhost:3000"
echo " API: http://localhost:4000"
echo ""
echo " Check full stack: docker compose ps"
echo " View API logs: docker compose logs -f api --tail 20"
else
warn "Some services are still starting after ${HEALTH_TIMEOUT}s."
echo ""
echo " This may be normal on first boot (migrations + seeding can be slow)."
echo " Monitor progress with:"
echo " docker compose logs -f api --tail 30"
echo ""
echo " Check status with:"
echo " docker compose ps"
fi
else
echo ""
info "Skipped. Start services manually when ready:"
echo ""
echo " cd ${INSTALL_DIR} && docker compose up -d"
echo ""
echo " Pre-built images will be pulled from the registry (~2 min first time)."
echo " Database migrations and seeding run automatically on startup."
fi
echo ""
echo -e "${BOLD}${GREEN}Installation complete!${NC}"
echo ""
echo -e " \033[0;33mIMPORTANT: Change your admin password after first login!\033[0m"
echo ""