1327 lines
42 KiB
Bash
Executable file
1327 lines
42 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
#
|
||
# Forgejo Self-Hosting Setup Wizard
|
||
# Interactive setup script for deploying Forgejo on Scaleway or Hetzner
|
||
#
|
||
# This wizard guides you through:
|
||
# 1. Checking dependencies
|
||
# 2. Configuring secrets (Ansible Vault)
|
||
# 3. Setting up cloud provider credentials
|
||
# 4. Creating infrastructure (Terraform/Terragrunt)
|
||
# 5. Configuring Ansible inventory
|
||
# 6. Deploying Forgejo with proper Tailscale/UFW ordering
|
||
#
|
||
# Works on: macOS, Debian, Ubuntu
|
||
#
|
||
|
||
set -euo pipefail
|
||
|
||
# ==============================================================================
|
||
# Configuration
|
||
# ==============================================================================
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
ANSIBLE_DIR="${SCRIPT_DIR}/ansible"
|
||
TERRAFORM_DIR="${SCRIPT_DIR}/terraform"
|
||
INVENTORY_FILE="${ANSIBLE_DIR}/inventory/production/hosts.yml"
|
||
SECRETS_FILE="${ANSIBLE_DIR}/playbooks/vars/secrets.yml"
|
||
SECRETS_EXAMPLE="${ANSIBLE_DIR}/playbooks/vars/secrets.yml.example"
|
||
STATE_FILE="${SCRIPT_DIR}/.wizard-state"
|
||
|
||
# Point Ansible to our config file
|
||
export ANSIBLE_CONFIG="${ANSIBLE_DIR}/ansible.cfg"
|
||
|
||
# State tracking (compatible with bash 3.x - no associative arrays)
|
||
VAULT_PASSWORD=""
|
||
|
||
load_state() {
|
||
# State is read directly from file when needed (bash 3.x compatible)
|
||
true
|
||
}
|
||
|
||
save_state() {
|
||
local key="$1"
|
||
local value="$2"
|
||
|
||
# Create file if it doesn't exist
|
||
if [[ ! -f "$STATE_FILE" ]]; then
|
||
echo "# Wizard state - auto-generated" >"$STATE_FILE"
|
||
fi
|
||
|
||
# Remove existing key if present, then add new value
|
||
if grep -q "^${key}=" "$STATE_FILE" 2>/dev/null; then
|
||
# Key exists, update it (macOS/Linux compatible)
|
||
local temp_file="${STATE_FILE}.tmp"
|
||
grep -v "^${key}=" "$STATE_FILE" >"$temp_file"
|
||
echo "${key}=${value}" >>"$temp_file"
|
||
mv "$temp_file" "$STATE_FILE"
|
||
else
|
||
# Key doesn't exist, append it
|
||
echo "${key}=${value}" >>"$STATE_FILE"
|
||
fi
|
||
}
|
||
|
||
get_state() {
|
||
local key="$1"
|
||
if [[ -f "$STATE_FILE" ]]; then
|
||
# Use || true to prevent exit on grep finding no matches (exit code 1)
|
||
grep "^${key}=" "$STATE_FILE" 2>/dev/null | cut -d'=' -f2- | head -1 || true
|
||
fi
|
||
}
|
||
|
||
# Check if a step is marked as "done"
|
||
is_step_done() {
|
||
[[ "$(get_state "$1")" == "done" ]]
|
||
}
|
||
|
||
# Check if a step has any value set (for steps that store actual values like provider, domain)
|
||
has_state() {
|
||
local value
|
||
value="$(get_state "$1")"
|
||
[[ -n "$value" ]]
|
||
}
|
||
|
||
# ==============================================================================
|
||
# Colors and Formatting (works on macOS and Linux)
|
||
# ==============================================================================
|
||
|
||
# Check if terminal supports colors
|
||
if [[ -t 1 ]] && command -v tput &>/dev/null && [[ $(tput colors 2>/dev/null || echo 0) -ge 8 ]]; then
|
||
RED=$(tput setaf 1)
|
||
GREEN=$(tput setaf 2)
|
||
YELLOW=$(tput setaf 3)
|
||
BLUE=$(tput setaf 4)
|
||
MAGENTA=$(tput setaf 5)
|
||
CYAN=$(tput setaf 6)
|
||
BOLD=$(tput bold)
|
||
DIM=$(tput dim)
|
||
RESET=$(tput sgr0)
|
||
else
|
||
RED=""
|
||
GREEN=""
|
||
YELLOW=""
|
||
BLUE=""
|
||
MAGENTA=""
|
||
CYAN=""
|
||
BOLD=""
|
||
DIM=""
|
||
RESET=""
|
||
fi
|
||
|
||
# ==============================================================================
|
||
# Helper Functions
|
||
# ==============================================================================
|
||
|
||
print_banner() {
|
||
clear
|
||
echo "${CYAN}${BOLD}"
|
||
echo "╔═══════════════════════════════════════════════════════════════════════════╗"
|
||
echo "║ ║"
|
||
echo "║ ███████╗ ██████╗ ██████╗ ██████╗ ███████╗ ██╗ ██████╗ ║"
|
||
echo "║ ██╔════╝██╔═══██╗██╔══██╗██╔════╝ ██╔════╝ ██║██╔═══██╗ ║"
|
||
echo "║ █████╗ ██║ ██║██████╔╝██║ ███╗█████╗ ██║██║ ██║ ║"
|
||
echo "║ ██╔══╝ ██║ ██║██╔══██╗██║ ██║██╔══╝ ██ ██║██║ ██║ ║"
|
||
echo "║ ██║ ╚██████╔╝██║ ██║╚██████╔╝███████╗╚█████╔╝╚██████╔╝ ║"
|
||
echo "║ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚════╝ ╚═════╝ ║"
|
||
echo "║ ║"
|
||
echo "║ Self-Hosting Setup Wizard (by Dumontix) ║"
|
||
echo "║ ║"
|
||
echo "╚═══════════════════════════════════════════════════════════════════════════╝"
|
||
echo "${RESET}"
|
||
echo ""
|
||
}
|
||
|
||
print_step() {
|
||
local step_num="$1"
|
||
local step_title="$2"
|
||
|
||
# Clear screen for each new step (cleaner experience)
|
||
clear
|
||
|
||
# Show mini header
|
||
echo "${CYAN}${BOLD}Forgejo Setup Wizard${RESET}"
|
||
echo ""
|
||
echo "${BLUE}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
|
||
echo "${BLUE}${BOLD} Step ${step_num}: ${step_title}${RESET}"
|
||
echo "${BLUE}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
|
||
echo ""
|
||
}
|
||
|
||
print_info() {
|
||
echo "${CYAN}ℹ ${RESET}$1"
|
||
}
|
||
|
||
print_success() {
|
||
echo "${GREEN}✓ ${RESET}$1"
|
||
}
|
||
|
||
print_warning() {
|
||
echo "${YELLOW}⚠ ${RESET}$1"
|
||
}
|
||
|
||
print_error() {
|
||
echo "${RED}✗ ${RESET}$1"
|
||
}
|
||
|
||
print_action() {
|
||
echo "${MAGENTA}▶ ${RESET}$1"
|
||
}
|
||
|
||
press_enter() {
|
||
echo ""
|
||
echo -n "${DIM}Press Enter to continue...${RESET}"
|
||
read -r </dev/tty
|
||
}
|
||
|
||
ask_yes_no() {
|
||
local prompt="$1"
|
||
local default="${2:-y}"
|
||
local yn
|
||
|
||
if [[ "$default" == "y" ]]; then
|
||
prompt="${prompt} [Y/n]: "
|
||
else
|
||
prompt="${prompt} [y/N]: "
|
||
fi
|
||
|
||
echo -n "${YELLOW}? ${RESET}${prompt}"
|
||
read -r yn </dev/tty
|
||
yn="${yn:-$default}"
|
||
|
||
# Case-insensitive comparison (compatible with bash 3.x on macOS)
|
||
case "$yn" in
|
||
[Yy] | [Yy][Ee][Ss]) return 0 ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
ask_input() {
|
||
local prompt="$1"
|
||
local default="${2:-}"
|
||
local result
|
||
|
||
# Output prompt to stderr so it's not captured by $()
|
||
if [[ -n "$default" ]]; then
|
||
echo -n "${YELLOW}? ${RESET}${prompt} [${default}]: " >&2
|
||
else
|
||
echo -n "${YELLOW}? ${RESET}${prompt}: " >&2
|
||
fi
|
||
read -r result </dev/tty
|
||
echo "${result:-$default}"
|
||
}
|
||
|
||
# Ask for sensitive input (no echo)
|
||
ask_secret() {
|
||
local prompt="$1"
|
||
local result
|
||
|
||
echo -n "${YELLOW}? ${RESET}${prompt}: " >&2
|
||
read -rs result </dev/tty
|
||
echo "" >&2 # New line after hidden input
|
||
echo "$result"
|
||
}
|
||
|
||
ask_choice() {
|
||
local prompt="$1"
|
||
shift
|
||
local options=("$@")
|
||
local choice
|
||
|
||
# Output menu to stderr so it's not captured by $()
|
||
echo "${YELLOW}? ${RESET}${prompt}" >&2
|
||
for i in "${!options[@]}"; do
|
||
echo " ${CYAN}$((i + 1))${RESET}) ${options[$i]}" >&2
|
||
done
|
||
echo -n " Enter choice [1-${#options[@]}]: " >&2
|
||
# Read from /dev/tty to ensure we read from terminal
|
||
read -r choice </dev/tty
|
||
|
||
if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le "${#options[@]}" ]]; then
|
||
echo "$((choice - 1))"
|
||
else
|
||
echo "0"
|
||
fi
|
||
}
|
||
|
||
generate_password() {
|
||
# Works on both macOS and Linux
|
||
if command -v openssl &>/dev/null; then
|
||
openssl rand -base64 "$1" | tr -d '\n'
|
||
elif [[ -r /dev/urandom ]]; then
|
||
head -c "$1" /dev/urandom | base64 | tr -d '\n' | head -c "$1"
|
||
else
|
||
print_error "Cannot generate random password"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
open_editor() {
|
||
local file="$1"
|
||
local editor="${EDITOR:-${VISUAL:-}}"
|
||
|
||
# Try to find a suitable editor
|
||
if [[ -z "$editor" ]]; then
|
||
for cmd in nano vim vi; do
|
||
if command -v "$cmd" &>/dev/null; then
|
||
editor="$cmd"
|
||
break
|
||
fi
|
||
done
|
||
fi
|
||
|
||
if [[ -z "$editor" ]]; then
|
||
print_error "No editor found. Please set \$EDITOR environment variable."
|
||
print_info "For now, please edit the file manually: ${file}"
|
||
press_enter
|
||
return 1
|
||
fi
|
||
|
||
print_action "Opening ${file} in ${editor}..."
|
||
"$editor" "$file"
|
||
}
|
||
|
||
run_command() {
|
||
local description="$1"
|
||
shift
|
||
|
||
print_action "${description}..."
|
||
echo "${DIM} \$ $*${RESET}"
|
||
|
||
if "$@"; then
|
||
print_success "${description} completed"
|
||
return 0
|
||
else
|
||
print_error "${description} failed"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# ==============================================================================
|
||
# Step Functions
|
||
# ==============================================================================
|
||
|
||
check_dependencies() {
|
||
print_step "1" "Checking Dependencies"
|
||
|
||
local missing=()
|
||
local deps=(terraform terragrunt ansible ansible-playbook ansible-vault make ssh ssh-keygen)
|
||
|
||
for dep in "${deps[@]}"; do
|
||
if command -v "$dep" &>/dev/null; then
|
||
print_success "$dep found: $(command -v "$dep")"
|
||
else
|
||
print_error "$dep not found"
|
||
missing+=("$dep")
|
||
fi
|
||
done
|
||
|
||
echo ""
|
||
|
||
if [[ ${#missing[@]} -gt 0 ]]; then
|
||
echo ""
|
||
print_error "Missing dependencies: ${missing[*]}"
|
||
echo ""
|
||
print_info "Please install the missing tools:"
|
||
echo ""
|
||
echo " ${BOLD}macOS (with Homebrew):${RESET}"
|
||
echo " brew install terraform terragrunt ansible make"
|
||
echo ""
|
||
echo " ${BOLD}Ubuntu/Debian:${RESET}"
|
||
echo " sudo apt update"
|
||
echo " sudo apt install make ansible"
|
||
echo " # Install terraform and terragrunt from their websites"
|
||
echo ""
|
||
echo " ${BOLD}on a Python virtualenv:${RESET} (likely can be done with UV, I am not sure how)"
|
||
echo " python3 -m venv .venv;. .venv/bin/activate"
|
||
echo " python3 -m pip install ansible"
|
||
echo " # Install terraform and terragrunt from their websites"
|
||
echo ""
|
||
exit 1
|
||
fi
|
||
|
||
print_success "All dependencies satisfied!"
|
||
}
|
||
|
||
select_ssh_key() {
|
||
print_step "1b" "Select SSH Key"
|
||
|
||
# Find all SSH keys (private keys without .pub extension that have a matching .pub)
|
||
local ssh_keys=()
|
||
local key_names=()
|
||
|
||
for key in ~/.ssh/id_* ~/.ssh/*.pem; do
|
||
# Skip if glob didn't match
|
||
[[ -e "$key" ]] || continue
|
||
# Skip public keys
|
||
[[ "$key" == *.pub ]] && continue
|
||
# Skip known_hosts and config files
|
||
[[ "$key" == *known_hosts* ]] && continue
|
||
[[ "$key" == *config* ]] && continue
|
||
[[ "$key" == *authorized_keys* ]] && continue
|
||
# Check if it's a file (not directory)
|
||
[[ -f "$key" ]] || continue
|
||
|
||
ssh_keys+=("$key")
|
||
# Get a friendly name (just the filename)
|
||
key_names+=("$(basename "$key")")
|
||
done
|
||
|
||
if [[ ${#ssh_keys[@]} -eq 0 ]]; then
|
||
print_warning "No SSH keys found in ~/.ssh/"
|
||
if ask_yes_no "Generate a new SSH key?"; then
|
||
local key_name
|
||
key_name=$(ask_input "Key name" "id_ed25519")
|
||
ssh-keygen -t ed25519 -f ~/.ssh/"$key_name" -N ""
|
||
SSH_KEY=~/.ssh/"$key_name"
|
||
print_success "SSH key generated: ${SSH_KEY}"
|
||
else
|
||
print_error "SSH key is required for server access"
|
||
exit 1
|
||
fi
|
||
elif [[ ${#ssh_keys[@]} -eq 1 ]]; then
|
||
SSH_KEY="${ssh_keys[0]}"
|
||
print_info "Using only available SSH key: ${SSH_KEY}"
|
||
else
|
||
print_info "Found ${#ssh_keys[@]} SSH keys:"
|
||
echo ""
|
||
|
||
# Display keys with numbers
|
||
local i=1
|
||
for key in "${ssh_keys[@]}"; do
|
||
local pub_key="${key}.pub"
|
||
local key_info=""
|
||
if [[ -f "$pub_key" ]]; then
|
||
# Extract comment/email from public key
|
||
key_info=$(awk '{print $3}' "$pub_key" 2>/dev/null || true)
|
||
fi
|
||
if [[ -n "$key_info" ]]; then
|
||
echo " ${CYAN}${i}${RESET}) $(basename "$key") ${DIM}(${key_info})${RESET}"
|
||
else
|
||
echo " ${CYAN}${i}${RESET}) $(basename "$key")"
|
||
fi
|
||
i=$((i + 1))
|
||
done
|
||
echo ""
|
||
|
||
local choice
|
||
echo -n " Select SSH key [1-${#ssh_keys[@]}]: " >&2
|
||
read -r choice </dev/tty
|
||
|
||
if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le "${#ssh_keys[@]}" ]]; then
|
||
SSH_KEY="${ssh_keys[$((choice - 1))]}"
|
||
else
|
||
SSH_KEY="${ssh_keys[0]}"
|
||
print_warning "Invalid choice, using first key"
|
||
fi
|
||
fi
|
||
|
||
print_success "Selected SSH key: ${SSH_KEY}"
|
||
export SSH_KEY
|
||
|
||
# Read the public key content for cloud provider registration
|
||
local pub_key_file="${SSH_KEY}.pub"
|
||
if [[ -f "$pub_key_file" ]]; then
|
||
SSH_PUBLIC_KEY=$(cat "$pub_key_file")
|
||
export SSH_PUBLIC_KEY
|
||
print_success "Public key loaded from ${pub_key_file}"
|
||
else
|
||
print_warning "No .pub file found for this key"
|
||
print_info "You may need to manually add your SSH key to your cloud provider"
|
||
fi
|
||
}
|
||
|
||
choose_provider() {
|
||
print_step "2" "Choose Cloud Provider"
|
||
|
||
print_info "This setup supports two European cloud providers:"
|
||
echo ""
|
||
echo " ${BOLD}Scaleway${RESET} (France) - ~€9/month (as of Jan 8 2026)"
|
||
echo " - DEV1-M: 4GB RAM, 2 vCPUs, 40GB SSD"
|
||
echo " - Object Storage for LFS and backups"
|
||
echo ""
|
||
echo " ${BOLD}Hetzner${RESET} (Germany) - ~€8/month (as of Jan 8 2026)"
|
||
echo " - CPX21: 4 vCPUs, 8GB RAM"
|
||
echo " - Excellent price/performance"
|
||
echo ""
|
||
|
||
local choice
|
||
choice=$(ask_choice "Select your cloud provider:" "Scaleway" "Hetzner")
|
||
|
||
if [[ "$choice" == "0" ]]; then
|
||
PROVIDER="scaleway"
|
||
else
|
||
PROVIDER="hetzner"
|
||
fi
|
||
|
||
print_success "Selected provider: ${PROVIDER}"
|
||
export PROVIDER
|
||
}
|
||
|
||
is_vault_encrypted() {
|
||
local file="$1"
|
||
[[ -f "$file" ]] && head -1 "$file" 2>/dev/null | grep -q '^\$ANSIBLE_VAULT'
|
||
}
|
||
|
||
read_vault_secret() {
|
||
local file="$1"
|
||
local key="$2"
|
||
local password="$3"
|
||
|
||
# Decrypt and extract value using ansible-vault
|
||
echo "$password" | ansible-vault view "$file" --vault-password-file=/dev/stdin 2>/dev/null | grep "^${key}:" | sed "s/^${key}:[[:space:]]*//" | tr -d '"' | tr -d "'"
|
||
}
|
||
|
||
configure_secrets() {
|
||
print_step "3" "Configure Secrets"
|
||
|
||
# Check if secrets file exists and is encrypted
|
||
if [[ -f "$SECRETS_FILE" ]] && is_vault_encrypted "$SECRETS_FILE"; then
|
||
print_info "Encrypted secrets file found: ${SECRETS_FILE}"
|
||
echo ""
|
||
|
||
local choice
|
||
choice=$(ask_choice "What would you like to do?" "Use existing secrets (enter vault password to view admin password)" "Generate new secrets (overwrites existing)")
|
||
|
||
if [[ "$choice" == "0" ]]; then
|
||
# Read existing secrets
|
||
print_info "Enter your Ansible Vault password to read existing secrets."
|
||
echo ""
|
||
|
||
local vault_pass
|
||
vault_pass=$(ask_secret "Vault password")
|
||
|
||
if [[ -z "$vault_pass" ]]; then
|
||
print_error "No password entered"
|
||
return 1
|
||
fi
|
||
|
||
# Verify password by trying to view the file
|
||
local admin_password
|
||
admin_password=$(read_vault_secret "$SECRETS_FILE" "vault_forgejo_admin_password" "$vault_pass")
|
||
|
||
if [[ -z "$admin_password" ]]; then
|
||
print_error "Could not decrypt secrets. Wrong password?"
|
||
if ask_yes_no "Try again?" "y"; then
|
||
configure_secrets
|
||
return $?
|
||
fi
|
||
return 1
|
||
fi
|
||
|
||
# Show admin password
|
||
echo ""
|
||
echo "${GREEN}${BOLD}┌─────────────────────────────────────────────────────────────────┐${RESET}"
|
||
echo "${GREEN}${BOLD}│ Secrets decrypted successfully! │${RESET}"
|
||
echo "${GREEN}${BOLD}│ │${RESET}"
|
||
echo "${GREEN}${BOLD}│ Admin Username: admin │${RESET}"
|
||
echo "${GREEN}${BOLD}│ Admin Password: ${admin_password} ${RESET}"
|
||
echo "${GREEN}${BOLD}│ │${RESET}"
|
||
echo "${GREEN}${BOLD}└─────────────────────────────────────────────────────────────────┘${RESET}"
|
||
echo ""
|
||
|
||
print_success "Using existing secrets"
|
||
return 0
|
||
fi
|
||
# Fall through to generate new secrets
|
||
print_warning "Existing secrets will be overwritten."
|
||
fi
|
||
|
||
print_info "Secrets are stored encrypted with Ansible Vault."
|
||
print_info "You'll need to remember your vault password!"
|
||
echo ""
|
||
|
||
# Copy example file
|
||
if [[ -f "$SECRETS_EXAMPLE" ]]; then
|
||
cp "$SECRETS_EXAMPLE" "$SECRETS_FILE"
|
||
else
|
||
print_error "Secrets example file not found: ${SECRETS_EXAMPLE}"
|
||
exit 1
|
||
fi
|
||
|
||
# Generate passwords
|
||
print_action "Generating secure passwords..."
|
||
|
||
local db_password
|
||
local admin_password
|
||
local secret_key
|
||
local internal_token
|
||
local jwt_secret
|
||
local metrics_token
|
||
|
||
db_password=$(generate_password 32)
|
||
admin_password=$(generate_password 24)
|
||
secret_key=$(generate_password 48)
|
||
internal_token=$(generate_password 48)
|
||
jwt_secret=$(generate_password 32)
|
||
metrics_token=$(generate_password 24)
|
||
|
||
# Update secrets file
|
||
if [[ "$(uname)" == "Darwin" ]]; then
|
||
# macOS sed requires different syntax
|
||
sed -i '' "s|CHANGE_ME_STRONG_PASSWORD_HERE|${db_password}|g" "$SECRETS_FILE"
|
||
sed -i '' "s|CHANGE_ME_ADMIN_PASSWORD_HERE|${admin_password}|g" "$SECRETS_FILE"
|
||
sed -i '' "s|CHANGE_ME_SECRET_KEY_64_CHARS_MINIMUM_XXXXXXXXXXXXXXXXX|${secret_key}|g" "$SECRETS_FILE"
|
||
sed -i '' "s|CHANGE_ME_INTERNAL_TOKEN_XXXXXXXXXXXXXXXXXXXXXXXXX|${internal_token}|g" "$SECRETS_FILE"
|
||
sed -i '' "s|CHANGE_ME_JWT_SECRET_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX|${jwt_secret}|g" "$SECRETS_FILE"
|
||
sed -i '' "s|CHANGE_ME_METRICS_TOKEN_XXXXXXXXX|${metrics_token}|g" "$SECRETS_FILE"
|
||
else
|
||
# Linux sed
|
||
sed -i "s|CHANGE_ME_STRONG_PASSWORD_HERE|${db_password}|g" "$SECRETS_FILE"
|
||
sed -i "s|CHANGE_ME_ADMIN_PASSWORD_HERE|${admin_password}|g" "$SECRETS_FILE"
|
||
sed -i "s|CHANGE_ME_SECRET_KEY_64_CHARS_MINIMUM_XXXXXXXXXXXXXXXXX|${secret_key}|g" "$SECRETS_FILE"
|
||
sed -i "s|CHANGE_ME_INTERNAL_TOKEN_XXXXXXXXXXXXXXXXXXXXXXXXX|${internal_token}|g" "$SECRETS_FILE"
|
||
sed -i "s|CHANGE_ME_JWT_SECRET_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX|${jwt_secret}|g" "$SECRETS_FILE"
|
||
sed -i "s|CHANGE_ME_METRICS_TOKEN_XXXXXXXXX|${metrics_token}|g" "$SECRETS_FILE"
|
||
fi
|
||
|
||
print_success "Passwords generated and saved"
|
||
|
||
# Show admin password
|
||
echo ""
|
||
echo "${YELLOW}${BOLD}┌─────────────────────────────────────────────────────────────────┐${RESET}"
|
||
echo "${YELLOW}${BOLD}│ IMPORTANT: Save your admin password! │${RESET}"
|
||
echo "${YELLOW}${BOLD}│ │${RESET}"
|
||
echo "${YELLOW}${BOLD}│ Admin Username: admin │${RESET}"
|
||
echo "${YELLOW}${BOLD}│ Admin Password: ${admin_password} │${RESET}"
|
||
echo "${YELLOW}${BOLD}│ │${RESET}"
|
||
echo "${YELLOW}${BOLD}│ You can change this later in the Forgejo web interface. │${RESET}"
|
||
echo "${YELLOW}${BOLD}└─────────────────────────────────────────────────────────────────┘${RESET}"
|
||
echo ""
|
||
|
||
# Encrypt with Ansible Vault
|
||
print_info "Now you need to encrypt the secrets file with Ansible Vault."
|
||
print_info "Choose a strong password and remember it!"
|
||
echo ""
|
||
|
||
if ansible-vault encrypt "$SECRETS_FILE"; then
|
||
print_success "Secrets encrypted successfully"
|
||
else
|
||
print_error "Failed to encrypt secrets"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
configure_cloud_credentials() {
|
||
print_step "4" "Configure Cloud Provider Credentials"
|
||
|
||
if [[ "$PROVIDER" == "scaleway" ]]; then
|
||
print_info "You need Scaleway API credentials."
|
||
print_info "Get them from: https://console.scaleway.com/iam/api-keys"
|
||
echo ""
|
||
|
||
if [[ -z "${SCW_ACCESS_KEY:-}" ]]; then
|
||
SCW_ACCESS_KEY=$(ask_input "Scaleway Access Key")
|
||
export SCW_ACCESS_KEY
|
||
else
|
||
print_success "SCW_ACCESS_KEY already set: ${SCW_ACCESS_KEY:0:8}..."
|
||
fi
|
||
|
||
if [[ -z "${SCW_SECRET_KEY:-}" ]]; then
|
||
SCW_SECRET_KEY=$(ask_input "Scaleway Secret Key")
|
||
export SCW_SECRET_KEY
|
||
else
|
||
print_success "SCW_SECRET_KEY already set"
|
||
fi
|
||
|
||
echo ""
|
||
print_info "Project ID determines where resources are created."
|
||
print_info "Find it at: https://console.scaleway.com/project/settings"
|
||
print_info " (Click on the project you want, then Settings → Project ID)"
|
||
echo ""
|
||
|
||
if [[ -n "${SCW_DEFAULT_PROJECT_ID:-}" ]]; then
|
||
print_info "Current project ID: ${SCW_DEFAULT_PROJECT_ID}"
|
||
if ask_yes_no "Use this project?" "y"; then
|
||
print_success "Using existing project ID"
|
||
else
|
||
SCW_DEFAULT_PROJECT_ID=$(ask_input "Scaleway Project ID")
|
||
export SCW_DEFAULT_PROJECT_ID
|
||
fi
|
||
else
|
||
SCW_DEFAULT_PROJECT_ID=$(ask_input "Scaleway Project ID")
|
||
export SCW_DEFAULT_PROJECT_ID
|
||
fi
|
||
|
||
else # hetzner
|
||
print_info "You need a Hetzner Cloud API token."
|
||
print_info "Get it from: https://console.hetzner.cloud/ → Security → API Tokens"
|
||
echo ""
|
||
|
||
if [[ -z "${HCLOUD_TOKEN:-}" ]]; then
|
||
HCLOUD_TOKEN=$(ask_input "Hetzner API Token")
|
||
export HCLOUD_TOKEN
|
||
else
|
||
print_success "HCLOUD_TOKEN already set"
|
||
fi
|
||
fi
|
||
|
||
print_success "Cloud credentials configured"
|
||
}
|
||
|
||
configure_domain() {
|
||
print_step "5" "Configure Domain"
|
||
|
||
print_info "Your Forgejo instance needs a domain name."
|
||
print_info "Example: git.yourdomain.com"
|
||
echo ""
|
||
|
||
DOMAIN=$(ask_input "Enter your domain" "git.example.com")
|
||
|
||
print_info "After deployment, you'll need to create DNS records:"
|
||
echo ""
|
||
echo " ${DOMAIN} IN A <server-ipv4>"
|
||
echo " ${DOMAIN} IN AAAA <server-ipv6>"
|
||
echo ""
|
||
|
||
# Create inventory file from example if it doesn't exist
|
||
local inventory_example="${ANSIBLE_DIR}/inventory/production/hosts.yml.example"
|
||
if [[ ! -f "$INVENTORY_FILE" ]] && [[ -f "$inventory_example" ]]; then
|
||
print_action "Creating inventory file from example..."
|
||
cp "$inventory_example" "$INVENTORY_FILE"
|
||
fi
|
||
|
||
# Update inventory file
|
||
if [[ -f "$INVENTORY_FILE" ]]; then
|
||
if [[ "$(uname)" == "Darwin" ]]; then
|
||
sed -i '' "s|forgejo_domain:.*|forgejo_domain: ${DOMAIN}|g" "$INVENTORY_FILE"
|
||
else
|
||
sed -i "s|forgejo_domain:.*|forgejo_domain: ${DOMAIN}|g" "$INVENTORY_FILE"
|
||
fi
|
||
print_success "Domain configured in inventory"
|
||
else
|
||
print_error "Could not find or create inventory file"
|
||
return 1
|
||
fi
|
||
|
||
export DOMAIN
|
||
}
|
||
|
||
create_infrastructure() {
|
||
print_step "6" "Create Infrastructure"
|
||
|
||
print_info "This will create the following resources on ${PROVIDER}:"
|
||
echo ""
|
||
if [[ "$PROVIDER" == "scaleway" ]]; then
|
||
echo " - DEV1-M compute instance"
|
||
echo " - 50GB block storage volume"
|
||
echo " - Security group"
|
||
echo " - Reserved IP address"
|
||
echo " - SSH key registration"
|
||
else
|
||
echo " - CPX21 compute instance"
|
||
echo " - Block volume"
|
||
echo " - Firewall rules"
|
||
echo " - SSH key registration"
|
||
fi
|
||
echo ""
|
||
|
||
if ! ask_yes_no "Create infrastructure now?"; then
|
||
print_warning "Skipping infrastructure creation"
|
||
return 0
|
||
fi
|
||
|
||
cd "$TERRAFORM_DIR/$PROVIDER/compute"
|
||
|
||
# Update terraform.tfvars with the SSH public key and domain
|
||
local tfvars_file="terraform.tfvars"
|
||
if [[ -n "${SSH_PUBLIC_KEY:-}" ]]; then
|
||
print_action "Configuring SSH key in Terraform..."
|
||
|
||
# Create or update terraform.tfvars with the SSH key
|
||
if [[ -f "$tfvars_file" ]]; then
|
||
# Remove existing ssh_public_key line if present
|
||
grep -v '^ssh_public_key' "$tfvars_file" >"${tfvars_file}.tmp" || true
|
||
mv "${tfvars_file}.tmp" "$tfvars_file"
|
||
fi
|
||
|
||
# Append SSH public key
|
||
echo "" >>"$tfvars_file"
|
||
echo "# SSH public key (added by setup wizard)" >>"$tfvars_file"
|
||
echo "ssh_public_key = \"${SSH_PUBLIC_KEY}\"" >>"$tfvars_file"
|
||
|
||
print_success "SSH public key configured"
|
||
else
|
||
print_warning "No SSH public key available - you may need to add it manually to your cloud provider"
|
||
fi
|
||
|
||
# Update domain in tfvars if set
|
||
if [[ -n "${DOMAIN:-}" ]]; then
|
||
if [[ -f "$tfvars_file" ]]; then
|
||
if [[ "$(uname)" == "Darwin" ]]; then
|
||
sed -i '' "s|^domain_name = .*|domain_name = \"${DOMAIN}\"|" "$tfvars_file"
|
||
else
|
||
sed -i "s|^domain_name = .*|domain_name = \"${DOMAIN}\"|" "$tfvars_file"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# Initialize
|
||
run_command "Initializing Terraform" terragrunt init
|
||
|
||
# Plan
|
||
print_action "Planning infrastructure..."
|
||
terragrunt plan
|
||
|
||
echo ""
|
||
if ! ask_yes_no "Apply this plan?"; then
|
||
print_warning "Infrastructure creation cancelled"
|
||
return 1
|
||
fi
|
||
|
||
# Apply
|
||
run_command "Creating infrastructure" terragrunt apply -auto-approve
|
||
|
||
# Get outputs
|
||
echo ""
|
||
print_success "Infrastructure created!"
|
||
echo ""
|
||
|
||
SERVER_IP=$(terragrunt output -raw server_ip 2>/dev/null || terragrunt output -raw server_ipv4 2>/dev/null || echo "")
|
||
|
||
if [[ -n "$SERVER_IP" ]]; then
|
||
echo "${GREEN}${BOLD}┌─────────────────────────────────────────────────────────────────┐${RESET}"
|
||
echo "${GREEN}${BOLD}│ Server IP: ${SERVER_IP} ${RESET}"
|
||
echo "${GREEN}${BOLD}└─────────────────────────────────────────────────────────────────┘${RESET}"
|
||
echo ""
|
||
|
||
# Update inventory
|
||
if [[ "$(uname)" == "Darwin" ]]; then
|
||
sed -i '' "s|ansible_host:.*|ansible_host: ${SERVER_IP}|g" "$INVENTORY_FILE"
|
||
else
|
||
sed -i "s|ansible_host:.*|ansible_host: ${SERVER_IP}|g" "$INVENTORY_FILE"
|
||
fi
|
||
print_success "Server IP updated in inventory"
|
||
|
||
export SERVER_IP
|
||
fi
|
||
|
||
echo ""
|
||
print_warning "IMPORTANT: Create DNS records now!"
|
||
echo ""
|
||
echo " ${DOMAIN} IN A ${SERVER_IP}"
|
||
echo ""
|
||
print_info "DNS propagation may take a few minutes."
|
||
|
||
press_enter
|
||
|
||
cd "$SCRIPT_DIR"
|
||
}
|
||
|
||
wait_for_server() {
|
||
print_step "7" "Waiting for Server"
|
||
|
||
print_info "Waiting for server to be ready..."
|
||
print_info "Using SSH key: ${SSH_KEY}"
|
||
|
||
local max_attempts=30
|
||
local attempt=1
|
||
|
||
while [[ $attempt -le $max_attempts ]]; do
|
||
echo -n " Attempt ${attempt}/${max_attempts}: "
|
||
if ssh -i "$SSH_KEY" -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o BatchMode=yes "root@${SERVER_IP}" "echo 'Server ready'" 2>/dev/null; then
|
||
echo ""
|
||
print_success "Server is ready!"
|
||
return 0
|
||
fi
|
||
echo "waiting..."
|
||
sleep 10
|
||
((attempt++))
|
||
done
|
||
|
||
print_error "Server not reachable after ${max_attempts} attempts"
|
||
print_info "You may need to wait longer or check your cloud provider console"
|
||
|
||
if ask_yes_no "Continue anyway?"; then
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
deploy_base() {
|
||
print_step "8" "Deploy Base Configuration"
|
||
|
||
print_info "This will deploy:"
|
||
echo " - System packages and configuration"
|
||
echo " - Docker"
|
||
echo " - PostgreSQL database"
|
||
echo " - Redis cache"
|
||
echo " - Forgejo application"
|
||
echo " - Caddy web server (HTTPS)"
|
||
echo " - Tailscale VPN"
|
||
echo ""
|
||
print_warning "Tailscale will be installed but NOT UFW yet!"
|
||
print_info "We'll configure UFW after you authenticate Tailscale."
|
||
echo ""
|
||
|
||
if ! ask_yes_no "Start deployment?"; then
|
||
return 1
|
||
fi
|
||
|
||
# Deploy everything except UFW (run from ansible dir to pick up ansible.cfg)
|
||
cd "$ANSIBLE_DIR"
|
||
|
||
print_action "Running Ansible deployment (without UFW)..."
|
||
ansible-playbook \
|
||
-i "inventory/production/hosts.yml" \
|
||
"playbooks/deploy.yml" \
|
||
--ask-vault-pass \
|
||
--skip-tags "ufw,firewall"
|
||
|
||
cd "$SCRIPT_DIR"
|
||
|
||
print_success "Base deployment completed!"
|
||
}
|
||
|
||
configure_tailscale() {
|
||
print_step "9" "Configure Tailscale VPN"
|
||
|
||
echo ""
|
||
echo "${YELLOW}${BOLD}╔═══════════════════════════════════════════════════════════════════════════╗${RESET}"
|
||
echo "${YELLOW}${BOLD}║ TAILSCALE AUTHENTICATION REQUIRED ║${RESET}"
|
||
echo "${YELLOW}${BOLD}╚═══════════════════════════════════════════════════════════════════════════╝${RESET}"
|
||
echo ""
|
||
print_info "Tailscale is installed but needs to be authenticated."
|
||
print_info "This connects your server to your private Tailscale network."
|
||
echo ""
|
||
print_warning "After this step, SSH will ONLY be accessible via Tailscale!"
|
||
echo ""
|
||
|
||
echo "You have two options:"
|
||
echo ""
|
||
echo " ${BOLD}Option 1: Interactive (recommended)${RESET}"
|
||
echo " 1. SSH into the server: ssh -i ${SSH_KEY} root@${SERVER_IP}"
|
||
echo " 2. Run: sudo tailscale up --ssh"
|
||
echo " 3. Open the URL shown in your browser to authenticate"
|
||
echo ""
|
||
echo " ${BOLD}Option 2: Auth Key (for automation)${RESET}"
|
||
echo " 1. Generate a key at: https://login.tailscale.com/admin/settings/keys"
|
||
echo " 2. SSH into the server: ssh -i ${SSH_KEY} root@${SERVER_IP}"
|
||
echo " 3. Run: sudo tailscale up --authkey=tskey-auth-XXXXX"
|
||
echo ""
|
||
|
||
if ask_yes_no "Open SSH session to configure Tailscale now?"; then
|
||
print_action "Opening SSH session..."
|
||
print_info "Run: sudo tailscale up --ssh"
|
||
print_info "Then exit the SSH session when done."
|
||
echo ""
|
||
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no "root@${SERVER_IP}" || true
|
||
fi
|
||
|
||
echo ""
|
||
print_warning "VERIFY: Is Tailscale authenticated and connected?"
|
||
echo ""
|
||
|
||
if ! ask_yes_no "Have you successfully authenticated Tailscale?"; then
|
||
print_error "Tailscale must be authenticated before enabling UFW!"
|
||
print_info "Without Tailscale, you will be locked out of your server."
|
||
echo ""
|
||
|
||
if ! ask_yes_no "Try again?"; then
|
||
print_warning "Skipping UFW configuration. Your server SSH is still publicly accessible."
|
||
print_info "Run 'make deploy-tags TAGS=ufw' later after configuring Tailscale."
|
||
return 1
|
||
fi
|
||
|
||
configure_tailscale
|
||
return $?
|
||
fi
|
||
|
||
print_success "Tailscale configured!"
|
||
|
||
# Verify Tailscale connection
|
||
print_action "Verifying Tailscale connection..."
|
||
local tailscale_ip
|
||
tailscale_ip=$(ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no "root@${SERVER_IP}" "tailscale ip -4" 2>/dev/null || echo "")
|
||
|
||
if [[ -n "$tailscale_ip" ]]; then
|
||
echo ""
|
||
echo "${GREEN}${BOLD}┌─────────────────────────────────────────────────────────────────┐${RESET}"
|
||
echo "${GREEN}${BOLD}│ Tailscale IP: ${tailscale_ip} ${RESET}"
|
||
echo "${GREEN}${BOLD}│ │${RESET}"
|
||
echo "${GREEN}${BOLD}│ After UFW is enabled, use this IP for SSH: │${RESET}"
|
||
echo "${GREEN}${BOLD}│ ssh root@${tailscale_ip} ${RESET}"
|
||
echo "${GREEN}${BOLD}└─────────────────────────────────────────────────────────────────┘${RESET}"
|
||
echo ""
|
||
export TAILSCALE_IP="$tailscale_ip"
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
enable_firewall() {
|
||
print_step "10" "Enable UFW Firewall"
|
||
|
||
print_warning "This will enable the UFW firewall with the following rules:"
|
||
echo ""
|
||
echo " ${GREEN}PUBLIC ACCESS:${RESET}"
|
||
echo " - Port 80 (HTTP) - Caddy/Let's Encrypt"
|
||
echo " - Port 443 (HTTPS) - Forgejo web interface"
|
||
echo ""
|
||
echo " ${YELLOW}TAILSCALE-ONLY ACCESS:${RESET}"
|
||
echo " - Port 22 (SSH) - System administration"
|
||
echo " - Port 2222 (Git SSH) - Git clone/push/pull"
|
||
echo " - All other internal services"
|
||
echo ""
|
||
print_warning "After this, public SSH will be blocked!"
|
||
print_info "You will need to use Tailscale to access SSH."
|
||
echo ""
|
||
|
||
if ! ask_yes_no "Enable UFW firewall now?"; then
|
||
print_warning "Skipping UFW. Your SSH is still publicly accessible."
|
||
print_info "Run 'make deploy-tags TAGS=ufw' later to enable UFW."
|
||
return 0
|
||
fi
|
||
|
||
# Run from ansible dir to pick up ansible.cfg
|
||
cd "$ANSIBLE_DIR"
|
||
|
||
print_action "Enabling UFW firewall..."
|
||
ansible-playbook \
|
||
-i "inventory/production/hosts.yml" \
|
||
"playbooks/deploy.yml" \
|
||
--ask-vault-pass \
|
||
--tags "ufw,firewall"
|
||
|
||
cd "$SCRIPT_DIR"
|
||
|
||
print_success "UFW firewall enabled!"
|
||
|
||
# Verify we can still connect via Tailscale
|
||
if [[ -n "${TAILSCALE_IP:-}" ]]; then
|
||
print_action "Verifying Tailscale SSH access..."
|
||
if ssh -i "$SSH_KEY" -o ConnectTimeout=10 -o StrictHostKeyChecking=no "root@${TAILSCALE_IP}" "echo 'Tailscale SSH works!'" 2>/dev/null; then
|
||
print_success "Tailscale SSH access confirmed!"
|
||
else
|
||
print_warning "Could not verify Tailscale SSH. Please test manually."
|
||
fi
|
||
fi
|
||
}
|
||
|
||
show_completion() {
|
||
print_step "✓" "Setup Complete!"
|
||
|
||
echo ""
|
||
echo "${GREEN}${BOLD}╔═══════════════════════════════════════════════════════════════════════════╗${RESET}"
|
||
echo "${GREEN}${BOLD}║ ║${RESET}"
|
||
echo "${GREEN}${BOLD}║ 🎉 Congratulations! Your Forgejo instance is ready! ║${RESET}"
|
||
echo "${GREEN}${BOLD}║ ║${RESET}"
|
||
echo "${GREEN}${BOLD}╚═══════════════════════════════════════════════════════════════════════════╝${RESET}"
|
||
echo ""
|
||
|
||
echo "${CYAN}${BOLD}Access Your Forgejo:${RESET}"
|
||
echo ""
|
||
echo " Web Interface: https://${DOMAIN}"
|
||
echo " Admin Username: admin"
|
||
echo " Admin Password: (the one shown earlier)"
|
||
echo ""
|
||
|
||
echo "${CYAN}${BOLD}SSH Access (via Tailscale):${RESET}"
|
||
echo ""
|
||
if [[ -n "${TAILSCALE_IP:-}" ]]; then
|
||
echo " ssh -i ${SSH_KEY} root@${TAILSCALE_IP}"
|
||
else
|
||
echo " ssh -i ${SSH_KEY} root@<tailscale-ip>"
|
||
fi
|
||
echo ""
|
||
|
||
echo "${CYAN}${BOLD}Git Clone URLs:${RESET}"
|
||
echo ""
|
||
echo " HTTPS: https://${DOMAIN}/user/repo.git"
|
||
echo " SSH: git@<tailscale-hostname>:user/repo.git"
|
||
echo ""
|
||
|
||
echo "${CYAN}${BOLD}Useful Commands:${RESET}"
|
||
echo ""
|
||
echo " make status # Check service status"
|
||
echo " make logs # View Forgejo logs"
|
||
echo " make backup # Create backup"
|
||
echo " make update # Update Forgejo"
|
||
echo ""
|
||
|
||
echo "${CYAN}${BOLD}Documentation:${RESET}"
|
||
echo ""
|
||
echo " docs/CONFIGURATION.md - All configuration options"
|
||
echo " docs/OPERATIONS.md - Operations guide"
|
||
echo " README.md - Quick reference"
|
||
echo ""
|
||
|
||
echo "${YELLOW}${BOLD}Next Steps:${RESET}"
|
||
echo ""
|
||
echo " 1. Wait for DNS propagation (check: dig ${DOMAIN})"
|
||
echo " 2. Access https://${DOMAIN} in your browser"
|
||
echo " 3. Log in with admin credentials"
|
||
echo " 4. Configure 2FA for the admin account"
|
||
echo " 5. Create your first repository!"
|
||
echo ""
|
||
}
|
||
|
||
# ==============================================================================
|
||
# Main
|
||
# ==============================================================================
|
||
|
||
show_resume_status() {
|
||
echo ""
|
||
echo "${CYAN}${BOLD}Previous session detected. Completed steps:${RESET}"
|
||
echo ""
|
||
|
||
# Check each step individually (some use is_step_done, some use has_state)
|
||
# Steps that store "done": dependencies, secrets, cloud_credentials, infrastructure, server_ready, base_deploy, tailscale, firewall
|
||
# Steps that store values: provider, domain, server_ip
|
||
|
||
if is_step_done "dependencies"; then
|
||
echo " ${GREEN}✓${RESET} Check Dependencies"
|
||
else
|
||
echo " ${DIM}○${RESET} Check Dependencies"
|
||
fi
|
||
|
||
if has_state "ssh_key"; then
|
||
echo " ${GREEN}✓${RESET} SSH Key ($(basename "$(get_state ssh_key)"))"
|
||
else
|
||
echo " ${DIM}○${RESET} SSH Key"
|
||
fi
|
||
|
||
if has_state "provider"; then
|
||
echo " ${GREEN}✓${RESET} Choose Provider ($(get_state provider))"
|
||
else
|
||
echo " ${DIM}○${RESET} Choose Provider"
|
||
fi
|
||
|
||
if is_step_done "secrets"; then
|
||
echo " ${GREEN}✓${RESET} Configure Secrets"
|
||
else
|
||
echo " ${DIM}○${RESET} Configure Secrets"
|
||
fi
|
||
|
||
if is_step_done "cloud_credentials"; then
|
||
echo " ${GREEN}✓${RESET} Cloud Credentials"
|
||
else
|
||
echo " ${DIM}○${RESET} Cloud Credentials"
|
||
fi
|
||
|
||
if has_state "domain"; then
|
||
echo " ${GREEN}✓${RESET} Configure Domain ($(get_state domain))"
|
||
else
|
||
echo " ${DIM}○${RESET} Configure Domain"
|
||
fi
|
||
|
||
if is_step_done "infrastructure"; then
|
||
echo " ${GREEN}✓${RESET} Create Infrastructure"
|
||
else
|
||
echo " ${DIM}○${RESET} Create Infrastructure"
|
||
fi
|
||
|
||
if is_step_done "server_ready"; then
|
||
echo " ${GREEN}✓${RESET} Wait for Server"
|
||
else
|
||
echo " ${DIM}○${RESET} Wait for Server"
|
||
fi
|
||
|
||
if is_step_done "base_deploy"; then
|
||
echo " ${GREEN}✓${RESET} Base Deployment"
|
||
else
|
||
echo " ${DIM}○${RESET} Base Deployment"
|
||
fi
|
||
|
||
if is_step_done "tailscale"; then
|
||
echo " ${GREEN}✓${RESET} Tailscale Setup"
|
||
else
|
||
echo " ${DIM}○${RESET} Tailscale Setup"
|
||
fi
|
||
|
||
if is_step_done "firewall"; then
|
||
echo " ${GREEN}✓${RESET} Enable Firewall"
|
||
else
|
||
echo " ${DIM}○${RESET} Enable Firewall"
|
||
fi
|
||
|
||
# Show server IP if set
|
||
local saved_ip
|
||
saved_ip=$(get_state "server_ip")
|
||
if [[ -n "$saved_ip" ]]; then
|
||
echo ""
|
||
echo "${CYAN}${BOLD}Server IP:${RESET} ${saved_ip}"
|
||
fi
|
||
echo ""
|
||
}
|
||
|
||
main() {
|
||
# Load previous state if exists
|
||
load_state
|
||
|
||
print_banner
|
||
|
||
# Check if resuming
|
||
if [[ -f "$STATE_FILE" ]]; then
|
||
show_resume_status
|
||
|
||
echo ""
|
||
local choice
|
||
choice=$(ask_choice "What would you like to do?" "Continue from where you left off" "Start fresh (reset all progress)")
|
||
|
||
if [[ "$choice" == "1" ]]; then
|
||
# Reset state
|
||
rm -f "$STATE_FILE"
|
||
print_info "Starting fresh..."
|
||
echo ""
|
||
else
|
||
# Restore saved values
|
||
PROVIDER=$(get_state "provider")
|
||
DOMAIN=$(get_state "domain")
|
||
SERVER_IP=$(get_state "server_ip")
|
||
TAILSCALE_IP=$(get_state "tailscale_ip")
|
||
SSH_KEY=$(get_state "ssh_key")
|
||
SSH_PUBLIC_KEY=$(get_state "ssh_public_key")
|
||
export PROVIDER DOMAIN SERVER_IP TAILSCALE_IP SSH_KEY SSH_PUBLIC_KEY
|
||
print_info "Resuming previous session..."
|
||
echo ""
|
||
fi
|
||
else
|
||
echo "This wizard will guide you through setting up your own Forgejo instance."
|
||
echo "It takes about 10-15 minutes to complete."
|
||
echo ""
|
||
|
||
if ! ask_yes_no "Ready to begin?"; then
|
||
echo "Setup cancelled."
|
||
exit 0
|
||
fi
|
||
fi
|
||
|
||
# Step 1: Dependencies
|
||
if ! is_step_done "dependencies"; then
|
||
check_dependencies
|
||
save_state "dependencies" "done"
|
||
press_enter
|
||
else
|
||
print_info "Skipping dependencies check (already completed)"
|
||
fi
|
||
|
||
# Step 1b: SSH Key selection (stores path, not "done")
|
||
if ! has_state "ssh_key"; then
|
||
select_ssh_key
|
||
save_state "ssh_key" "$SSH_KEY"
|
||
# Store public key content for terraform (base64 encoded to handle newlines/spaces)
|
||
if [[ -n "${SSH_PUBLIC_KEY:-}" ]]; then
|
||
save_state "ssh_public_key" "$SSH_PUBLIC_KEY"
|
||
fi
|
||
press_enter
|
||
else
|
||
SSH_KEY=$(get_state "ssh_key")
|
||
SSH_PUBLIC_KEY=$(get_state "ssh_public_key")
|
||
export SSH_KEY SSH_PUBLIC_KEY
|
||
print_info "Using SSH key: ${SSH_KEY}"
|
||
fi
|
||
|
||
# Step 2: Provider (stores actual value, not "done")
|
||
if ! has_state "provider"; then
|
||
choose_provider
|
||
save_state "provider" "$PROVIDER"
|
||
press_enter
|
||
else
|
||
print_info "Skipping provider selection (using: ${PROVIDER})"
|
||
fi
|
||
|
||
# Step 3: Secrets
|
||
if ! is_step_done "secrets"; then
|
||
configure_secrets
|
||
save_state "secrets" "done"
|
||
press_enter
|
||
else
|
||
print_info "Skipping secrets configuration (already completed)"
|
||
fi
|
||
|
||
# Step 4: Cloud Credentials
|
||
# Skip cloud credentials entirely if infrastructure is already created
|
||
# (remaining steps are Ansible-only and don't need cloud provider access)
|
||
if is_step_done "infrastructure"; then
|
||
print_info "Skipping cloud credentials (infrastructure already exists)"
|
||
elif ! is_step_done "cloud_credentials"; then
|
||
configure_cloud_credentials
|
||
save_state "cloud_credentials" "done"
|
||
press_enter
|
||
else
|
||
print_info "Skipping cloud credentials (already configured)"
|
||
# Re-prompt for credentials since they're not persisted (security)
|
||
configure_cloud_credentials
|
||
fi
|
||
|
||
# Step 5: Domain (stores actual value, not "done")
|
||
if ! has_state "domain"; then
|
||
configure_domain
|
||
save_state "domain" "$DOMAIN"
|
||
press_enter
|
||
else
|
||
print_info "Skipping domain configuration (using: ${DOMAIN})"
|
||
fi
|
||
|
||
# Step 6: Infrastructure
|
||
if ! is_step_done "infrastructure"; then
|
||
create_infrastructure
|
||
if [[ -n "${SERVER_IP:-}" ]]; then
|
||
save_state "infrastructure" "done"
|
||
save_state "server_ip" "$SERVER_IP"
|
||
fi
|
||
else
|
||
print_info "Skipping infrastructure creation (already created)"
|
||
fi
|
||
|
||
# Ensure we have server IP
|
||
if [[ -z "${SERVER_IP:-}" ]]; then
|
||
SERVER_IP=$(ask_input "Enter server IP address")
|
||
save_state "server_ip" "$SERVER_IP"
|
||
export SERVER_IP
|
||
fi
|
||
|
||
# Step 7: Wait for Server
|
||
if ! is_step_done "server_ready"; then
|
||
wait_for_server
|
||
save_state "server_ready" "done"
|
||
press_enter
|
||
else
|
||
print_info "Skipping server wait (already verified)"
|
||
fi
|
||
|
||
# Step 8: Base Deployment
|
||
if ! is_step_done "base_deploy"; then
|
||
deploy_base
|
||
save_state "base_deploy" "done"
|
||
press_enter
|
||
else
|
||
print_info "Skipping base deployment (already deployed)"
|
||
fi
|
||
|
||
# Step 9: Tailscale
|
||
if ! is_step_done "tailscale"; then
|
||
if configure_tailscale; then
|
||
save_state "tailscale" "done"
|
||
if [[ -n "${TAILSCALE_IP:-}" ]]; then
|
||
save_state "tailscale_ip" "$TAILSCALE_IP"
|
||
fi
|
||
fi
|
||
else
|
||
print_info "Skipping Tailscale configuration (already configured)"
|
||
fi
|
||
|
||
# Step 10: Firewall
|
||
if ! is_step_done "firewall"; then
|
||
if is_step_done "tailscale"; then
|
||
enable_firewall
|
||
save_state "firewall" "done"
|
||
else
|
||
print_warning "Skipping firewall - Tailscale must be configured first"
|
||
fi
|
||
else
|
||
print_info "Skipping firewall setup (already enabled)"
|
||
fi
|
||
|
||
show_completion
|
||
|
||
# Clean up state file on successful completion
|
||
if is_step_done "firewall" || is_step_done "tailscale"; then
|
||
if ask_yes_no "Setup complete! Remove progress tracking file?" "y"; then
|
||
rm -f "$STATE_FILE"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# Run main function
|
||
main "$@"
|