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>
480 lines
15 KiB
Bash
Executable file
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 "$@"
|