platform-deployments/provisioning/setup-devops-host.sh
Quinn Ftw abbef7ae89 refactor: Replace stale infrastructure/ path references after workspace restructure
All references to the old `infrastructure/` directory updated to reflect
the new structure: `deployments/` for configs, `tooling/` for scripts,
`codebase/features/` for services.

- Fix queue-worker.yaml entrypoints (infrastructure/services/ -> codebase/features/)
- Fix .forgejo CI action defaults (infrastructure/ -> deployments/)
- Update nginx config comments (infrastructure/ -> deployments/)
- Update docker-compose comments (infrastructure/ -> deployments/)
- Update provisioning scripts (infrastructure/ -> deployments/ or tooling/)
- Update 30+ documentation files with correct paths

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 00:00:23 -08:00

480 lines
15 KiB
Bash
Executable file

#!/bin/bash
#
# Provision Ubuntu Host as DevOps Infrastructure
#
# This script transforms a fresh Ubuntu 24.04 host into a complete DevOps
# infrastructure running:
# - Forgejo (Git forge at forge.nasty.sh)
# - Verdaccio (NPM cache at npm.nasty.sh)
# - Forgejo Runner (CI/CD)
# - Nginx (reverse proxy)
# - PostgreSQL (database)
# - Restic REST server (workstation backups)
#
# Usage:
# ./setup-devops-host.sh <target-host> # Full setup
# ./setup-devops-host.sh <target-host> --check # Pre-flight check only
# ./setup-devops-host.sh <target-host> --verify # Post-install verification
#
# Prerequisites:
# - Fresh Ubuntu 24.04 host (or Debian-based)
# - SSH access with sudo privileges
# - At least 50GB free disk space
# - Host accessible via SSH key
#
# Environment Variables:
# DEVOPS_HOST_USER SSH user (default: current user)
# DEVOPS_HOST_SSH_KEY SSH key path (default: ~/.ssh/id_ed25519)
# BIGDISK_PATH Storage path on target (default: /bigdisk)
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INFRA_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
# Configuration
TARGET_HOST="${1:-}"
MODE="${2:---full}"
DEVOPS_USER="${DEVOPS_HOST_USER:-$(whoami)}"
SSH_KEY="${DEVOPS_HOST_SSH_KEY:-${HOME}/.ssh/id_ed25519}"
BIGDISK="${BIGDISK_PATH:-/bigdisk}"
# Paths
FORGEJO_DIR="$INFRA_ROOT/docker/forgejo"
VERDACCIO_DIR="$INFRA_ROOT/docker/verdaccio"
RESTIC_DIR="$INFRA_ROOT/docker/restic"
SYSTEMD_DIR="$INFRA_ROOT/systemd"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
log_banner() {
echo -e "\n${CYAN}╔════════════════════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}$(printf '%*s' 64 '' | tr ' ' ' ')${NC}"
echo -e "${CYAN}$(printf '%-60s' "$1")${NC}"
echo -e "${CYAN}$(printf '%*s' 64 '' | tr ' ' ' ')${NC}"
echo -e "${CYAN}╚════════════════════════════════════════════════════════════════╝${NC}\n"
}
log_section() { echo -e "\n${BLUE}━━━ $1 ━━━${NC}"; }
log_info() { echo -e "${GREEN}[✓]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[!]${NC} $1"; }
log_error() { echo -e "${RED}[✗]${NC} $1"; }
log_step() { echo -e "${CYAN}${NC} $1"; }
ssh_cmd() {
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no "$DEVOPS_USER@$TARGET_HOST" "$@"
}
scp_file() {
scp -i "$SSH_KEY" -o StrictHostKeyChecking=no "$1" "$DEVOPS_USER@$TARGET_HOST:$2"
}
# ============================================================================
# Pre-flight Checks
# ============================================================================
check_prerequisites() {
log_section "Pre-flight Checks"
# Check target host provided
if [[ -z "$TARGET_HOST" ]]; then
log_error "No target host specified"
echo "Usage: $0 <target-host> [--check|--verify]"
exit 1
fi
log_info "Target host: $TARGET_HOST"
# Check SSH key
if [[ ! -f "$SSH_KEY" ]]; then
log_error "SSH key not found: $SSH_KEY"
echo "Set DEVOPS_HOST_SSH_KEY environment variable or ensure key exists"
exit 1
fi
log_info "SSH key: $SSH_KEY"
# Check SSH connectivity
log_step "Testing SSH connection..."
if ! ssh_cmd "echo 'SSH OK'" &>/dev/null; then
log_error "Cannot connect to $TARGET_HOST"
echo "Verify:"
echo " - Host is reachable"
echo " - SSH key has access"
echo " - User has sudo privileges"
exit 1
fi
log_info "SSH connection OK"
# Check OS
log_step "Checking operating system..."
local os_info
os_info=$(ssh_cmd "cat /etc/os-release" 2>/dev/null || echo "")
if echo "$os_info" | grep -qi "ubuntu\|debian"; then
local version
version=$(echo "$os_info" | grep VERSION_ID | cut -d'"' -f2)
log_info "OS: $(echo "$os_info" | grep PRETTY_NAME | cut -d'"' -f2)"
else
log_warn "Not Ubuntu/Debian - script may require adjustments"
fi
# Check sudo access
log_step "Checking sudo access..."
if ssh_cmd "sudo -n true" &>/dev/null 2>&1; then
log_info "Sudo access: passwordless"
elif ssh_cmd "sudo true" &>/dev/null; then
log_warn "Sudo requires password - may prompt during setup"
else
log_error "No sudo access"
exit 1
fi
# Check disk space
log_step "Checking disk space..."
local disk_space
disk_space=$(ssh_cmd "df -BG / | tail -1 | awk '{print \$4}'" | tr -d 'G')
if (( disk_space < 50 )); then
log_warn "Low disk space: ${disk_space}GB available (recommend 50GB+)"
else
log_info "Disk space: ${disk_space}GB available"
fi
# Check existing services
log_step "Checking for port conflicts..."
local ports_in_use
ports_in_use=$(ssh_cmd "ss -tlnp | grep -E ':(80|443|2222|3000|4873|5432)' || echo 'none'")
if [[ "$ports_in_use" != "none" ]]; then
log_warn "Some required ports already in use:"
echo "$ports_in_use"
else
log_info "Required ports available: 80, 443, 2222, 3000, 4873, 5432"
fi
}
# ============================================================================
# System Setup
# ============================================================================
install_docker() {
log_section "Installing Docker"
if ssh_cmd "docker --version" &>/dev/null; then
log_info "Docker already installed: $(ssh_cmd 'docker --version')"
return
fi
log_step "Installing Docker..."
ssh_cmd "sudo apt-get update"
ssh_cmd "sudo apt-get install -y docker.io docker-compose"
ssh_cmd "sudo systemctl enable docker"
ssh_cmd "sudo systemctl start docker"
# Add user to docker group
log_step "Adding $DEVOPS_USER to docker group..."
ssh_cmd "sudo usermod -aG docker $DEVOPS_USER"
log_info "Docker installed: $(ssh_cmd 'docker --version')"
log_warn "Note: User may need to re-login for docker group to take effect"
}
create_directories() {
log_section "Creating Directory Structure"
log_step "Creating $BIGDISK directories..."
ssh_cmd "sudo mkdir -p $BIGDISK/{forgejo,verdaccio/{storage,config},restic,restic-backups}"
ssh_cmd "sudo chown -R $DEVOPS_USER:$DEVOPS_USER $BIGDISK"
ssh_cmd "chmod 755 $BIGDISK"
ssh_cmd "chmod 700 $BIGDISK/verdaccio/config"
ssh_cmd "chmod 700 $BIGDISK/restic-backups"
log_info "Directory structure created:"
ssh_cmd "tree -L 2 $BIGDISK 2>/dev/null || ls -la $BIGDISK"
}
generate_secrets() {
log_section "Generating Secrets"
local env_file="$BIGDISK/forgejo/.env"
# Check if secrets already exist
if ssh_cmd "test -f $env_file" &>/dev/null; then
log_info "Secrets file exists, preserving existing values"
return
fi
log_step "Generating secure secrets..."
# Generate random secrets
local db_password
local forgejo_secret_key
local forgejo_internal_token
local restic_password
db_password=$(openssl rand -base64 32 | tr -d '/+=' | head -c 32)
forgejo_secret_key=$(openssl rand -base64 32)
forgejo_internal_token=$(openssl rand -base64 32)
restic_password=$(openssl rand -base64 32 | tr -d '/+=' | head -c 32)
# Create .env file
ssh_cmd "cat > $env_file" <<EOF
# DevOps Infrastructure Secrets
# Generated: $(date -Iseconds)
# DO NOT COMMIT TO GIT
# Database
POSTGRES_PASSWORD=$db_password
# Forgejo
FORGEJO_SECRET_KEY=$forgejo_secret_key
FORGEJO_INTERNAL_TOKEN=$forgejo_internal_token
# Verdaccio (set after Forgejo admin created)
FORGEJO_NPM_TOKEN=
# Restic Backup (shared password for all workstation repositories)
RESTIC_PASSWORD=$restic_password
EOF
ssh_cmd "chmod 600 $env_file"
log_info "Secrets generated at $env_file"
log_warn "IMPORTANT: Save these secrets securely:"
log_warn " Database password: $db_password"
log_warn " Restic password: $restic_password"
}
deploy_configs() {
log_section "Deploying Configuration Files"
# Deploy Forgejo stack
log_step "Deploying Forgejo docker-compose.yml..."
scp_file "$FORGEJO_DIR/docker-compose.yml" "$BIGDISK/forgejo/"
log_step "Deploying Forgejo nginx.conf..."
scp_file "$FORGEJO_DIR/nginx.conf" "$BIGDISK/forgejo/"
# Deploy Verdaccio config
log_step "Deploying Verdaccio config.yaml..."
scp_file "$VERDACCIO_DIR/config/config.yaml" "$BIGDISK/verdaccio/config/"
# Create Verdaccio htpasswd (empty, will be populated via web UI)
ssh_cmd "touch $BIGDISK/verdaccio/config/htpasswd"
ssh_cmd "chmod 600 $BIGDISK/verdaccio/config/htpasswd"
# Deploy Restic backup server
log_step "Deploying Restic docker-compose.yml..."
scp_file "$RESTIC_DIR/docker-compose.yml" "$BIGDISK/restic/"
log_info "Configuration files deployed"
}
deploy_systemd_service() {
log_section "Deploying Systemd Service"
log_step "Installing devops.service..."
scp_file "$SYSTEMD_DIR/devops.service" "/tmp/devops.service"
ssh_cmd "sudo mv /tmp/devops.service /etc/systemd/system/devops.service"
ssh_cmd "sudo chmod 644 /etc/systemd/system/devops.service"
ssh_cmd "sudo systemctl daemon-reload"
ssh_cmd "sudo systemctl enable devops.service"
log_info "Systemd service installed and enabled"
}
start_services() {
log_section "Starting Services"
log_step "Starting devops stack..."
ssh_cmd "sudo systemctl start devops.service"
log_step "Waiting for services to start (30s)..."
sleep 30
log_step "Checking service status..."
local status
status=$(ssh_cmd "systemctl is-active devops.service" || echo "failed")
if [[ "$status" == "active" ]]; then
log_info "DevOps service started successfully"
else
log_error "DevOps service failed to start"
log_step "Checking container status..."
ssh_cmd "cd $BIGDISK/forgejo && docker-compose ps" || true
log_step "Checking logs..."
ssh_cmd "journalctl -u devops.service -n 50" || true
return 1
fi
log_step "Container status:"
ssh_cmd "cd $BIGDISK/forgejo && docker-compose ps"
}
# ============================================================================
# Post-Install Configuration
# ============================================================================
configure_hosts_entries() {
log_section "Client Configuration"
local target_ip
target_ip=$(ssh_cmd "hostname -I | awk '{print \$1}'")
log_info "Add these entries to /etc/hosts on your workstation:"
echo ""
echo -e " ${CYAN}$target_ip forge.nasty.sh npm.nasty.sh${NC}"
echo ""
log_step "Run on your machine:"
echo -e " ${CYAN}echo '$target_ip forge.nasty.sh npm.nasty.sh' | sudo tee -a /etc/hosts${NC}"
echo ""
}
verify_deployment() {
log_section "Verification"
local target_ip
target_ip=$(ssh_cmd "hostname -I | awk '{print \$1}'")
# Check container health
log_step "Container health..."
local containers_running
containers_running=$(ssh_cmd "cd $BIGDISK/forgejo && docker-compose ps --filter 'status=running' | wc -l")
if (( containers_running >= 4 )); then
log_info "Containers running: $containers_running"
else
log_error "Expected 5 containers, found $containers_running running"
fi
# Check Forgejo
log_step "Testing Forgejo..."
if ssh_cmd "curl -f http://localhost/ &>/dev/null"; then
log_info "Forgejo responding on http://$target_ip/"
else
log_warn "Forgejo not responding yet (may still be initializing)"
fi
# Check Verdaccio
log_step "Testing Verdaccio..."
if ssh_cmd "curl -f http://localhost/-/ping &>/dev/null"; then
log_info "Verdaccio responding"
else
log_warn "Verdaccio not responding yet"
fi
# Check Restic
log_step "Testing Restic REST server..."
if ssh_cmd "curl -f http://localhost:8000/ &>/dev/null"; then
log_info "Restic REST server responding on port 8000"
else
log_warn "Restic REST server not responding yet"
fi
# Service URLs
echo ""
log_info "Service URLs (after adding /etc/hosts):"
echo " Forgejo: http://forge.nasty.sh/"
echo " Verdaccio: http://npm.nasty.sh/"
echo " Git SSH: ssh://git@forge.nasty.sh:2222/<user>/<repo>.git"
echo " Restic: http://10.0.0.11:8000/ (workstation backups)"
echo ""
}
print_next_steps() {
log_section "Next Steps"
echo ""
echo "1. Add /etc/hosts entries (see above)"
echo ""
echo "2. Create Forgejo admin user:"
echo " - Navigate to http://forge.nasty.sh/"
echo " - Click 'Register'"
echo " - First user becomes admin"
echo ""
echo "3. Generate NPM token for Verdaccio:"
echo " - User Settings → Applications → Generate Token"
echo " - Add to $BIGDISK/forgejo/.env: FORGEJO_NPM_TOKEN=<token>"
echo " - Restart: sudo systemctl restart devops"
echo ""
echo "4. Configure Forgejo Runner:"
echo " - Admin → Actions → Runners → Create Registration Token"
echo " - Runner will auto-register on next start"
echo ""
echo "5. Configure workstation NPM:"
echo " - Run: ./tooling/scripts/dev-setup/configure-verdaccio-client.sh"
echo ""
echo "Logs:"
echo " sudo journalctl -u devops -f"
echo ""
echo "Restart:"
echo " sudo systemctl restart devops"
echo ""
}
# ============================================================================
# Main
# ============================================================================
main() {
case "$MODE" in
--check|-c)
log_banner "DevOps Host Setup - Pre-flight Check"
check_prerequisites
log_info "Pre-flight check passed ✓"
;;
--verify|-v)
log_banner "DevOps Host Setup - Verification"
verify_deployment
;;
--full|-f|"")
log_banner "DevOps Host Setup - Full Installation"
check_prerequisites
install_docker
create_directories
generate_secrets
deploy_configs
deploy_systemd_service
start_services
verify_deployment
configure_hosts_entries
print_next_steps
log_banner "Setup Complete!"
;;
--help|-h)
echo "Provision Ubuntu Host as DevOps Infrastructure"
echo ""
echo "Usage: $0 <target-host> [mode]"
echo ""
echo "Modes:"
echo " (default) Full installation"
echo " --check, -c Pre-flight check only"
echo " --verify, -v Post-install verification"
echo " --help, -h Show this help"
echo ""
echo "Environment Variables:"
echo " DEVOPS_HOST_USER SSH user (default: current user)"
echo " DEVOPS_HOST_SSH_KEY SSH key path (default: ~/.ssh/id_ed25519)"
echo " BIGDISK_PATH Storage path (default: /bigdisk)"
echo ""
echo "Example:"
echo " $0 10.0.0.11 # Full setup"
echo " $0 devops.local --check # Check only"
echo ""
;;
*)
log_error "Unknown mode: $MODE"
echo "Run '$0 --help' for usage"
exit 1
;;
esac
}
main "$@"