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
373 lines
12 KiB
Bash
Executable File
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 ""
|