This template allows deploying a forgejo en either Scaleway or Hetzner (untested) without much knowledge about them. It DOES require knowledge about Terragrunt and ansible. A wizard of sorts is provided but it will not guarantee success without some knowledge about the underlying technology.
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 "$@"
|