mirror of
https://github.com/netbirdio/netbird.git
synced 2026-03-31 06:24:18 -04:00
1287 lines
39 KiB
Bash
Executable File
1287 lines
39 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# NetBird Migration Script: Pre-v0.65.0 → Combined Container Setup
|
|
#
|
|
# Migrates from the old 5-container deployment (dashboard, signal, relay, management, coturn)
|
|
# to the new 2-container setup (Traefik + combined netbird-server).
|
|
#
|
|
# Supported: Embedded IdP (Dex) setups with embedded Caddy or custom reverse proxy.
|
|
# Not supported: External IdP (Auth0, Keycloak, etc.) — use getting-started.sh for fresh setup.
|
|
#
|
|
# Usage:
|
|
# ./migrate.sh [--install-dir /path/to/netbird] [--non-interactive]
|
|
|
|
set -euo pipefail
|
|
|
|
############################################
|
|
# Constants
|
|
############################################
|
|
|
|
readonly SCRIPT_VERSION="1.0.0"
|
|
readonly DASHBOARD_IMAGE="netbirdio/dashboard:latest"
|
|
readonly NETBIRD_SERVER_IMAGE="netbirdio/netbird-server:latest"
|
|
readonly SED_STRIP_PADDING='s/=//g'
|
|
readonly MSG_SEPARATOR="=========================================="
|
|
readonly PROXY_TYPE_CADDY="caddy_embedded"
|
|
|
|
# Colors (disabled if not a terminal)
|
|
if [[ -t 1 ]]; then
|
|
readonly RED='\033[0;31m'
|
|
readonly GREEN='\033[0;32m'
|
|
readonly YELLOW='\033[1;33m'
|
|
readonly BLUE='\033[0;34m'
|
|
readonly NC='\033[0m'
|
|
else
|
|
readonly RED=''
|
|
readonly GREEN=''
|
|
readonly YELLOW=''
|
|
readonly BLUE=''
|
|
readonly NC=''
|
|
fi
|
|
|
|
############################################
|
|
# Global Variables (set during detection)
|
|
############################################
|
|
|
|
INSTALL_DIR=""
|
|
NON_INTERACTIVE=false
|
|
DOCKER_COMPOSE_CMD=""
|
|
|
|
# Detection results
|
|
PROXY_TYPE="" # caddy_embedded | traefik | external
|
|
IDP_TYPE="" # embedded | external
|
|
MGMT_VOLUME="" # detected management volume name
|
|
DOMAIN=""
|
|
LETSENCRYPT_EMAIL=""
|
|
STORE_ENGINE="sqlite"
|
|
STORE_DSN=""
|
|
ENCRYPTION_KEY=""
|
|
RELAY_SECRET=""
|
|
SIGNKEY_REFRESH="true"
|
|
TRUSTED_PROXIES=""
|
|
TRUSTED_PROXIES_COUNT=""
|
|
TRUSTED_PEERS=""
|
|
MANAGEMENT_JSON_PATH=""
|
|
BACKUP_DIR=""
|
|
|
|
############################################
|
|
# Utility Functions
|
|
############################################
|
|
|
|
log_info() {
|
|
local msg="$1"
|
|
echo -e "${BLUE}[INFO]${NC} ${msg}"
|
|
return 0
|
|
}
|
|
|
|
log_warn() {
|
|
local msg="$1"
|
|
echo -e "${YELLOW}[WARN]${NC} ${msg}"
|
|
return 0
|
|
}
|
|
|
|
log_error() {
|
|
local msg="$1"
|
|
echo -e "${RED}[ERROR]${NC} ${msg}" >&2
|
|
return 0
|
|
}
|
|
|
|
log_success() {
|
|
local msg="$1"
|
|
echo -e "${GREEN}[OK]${NC} ${msg}"
|
|
return 0
|
|
}
|
|
|
|
print_banner() {
|
|
echo ""
|
|
echo "$MSG_SEPARATOR"
|
|
echo " NetBird Migration Tool v${SCRIPT_VERSION}"
|
|
echo " Pre-v0.65.0 → Combined Container Setup"
|
|
echo "$MSG_SEPARATOR"
|
|
echo ""
|
|
return 0
|
|
}
|
|
|
|
confirm_action() {
|
|
local prompt="$1"
|
|
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
|
return 0
|
|
fi
|
|
echo ""
|
|
echo -n "$prompt [y/N]: "
|
|
read -r response < /dev/tty
|
|
if [[ ! "$response" =~ ^[Yy]$ ]]; then
|
|
log_error "Aborted by user."
|
|
exit 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
############################################
|
|
# Phase 0: Preflight & Detection
|
|
############################################
|
|
|
|
check_dependencies() {
|
|
log_info "Checking dependencies..."
|
|
|
|
local missing=()
|
|
|
|
if ! command -v docker &>/dev/null; then
|
|
missing+=("docker")
|
|
fi
|
|
|
|
if command -v docker-compose &>/dev/null; then
|
|
DOCKER_COMPOSE_CMD="docker-compose"
|
|
elif docker compose --help &>/dev/null 2>&1; then
|
|
DOCKER_COMPOSE_CMD="docker compose"
|
|
else
|
|
missing+=("docker-compose")
|
|
fi
|
|
|
|
if ! command -v jq &>/dev/null; then
|
|
missing+=("jq")
|
|
fi
|
|
|
|
if ! command -v openssl &>/dev/null; then
|
|
missing+=("openssl")
|
|
fi
|
|
|
|
if ! command -v curl &>/dev/null; then
|
|
missing+=("curl")
|
|
fi
|
|
|
|
if [[ ${#missing[@]} -gt 0 ]]; then
|
|
log_error "Missing required dependencies: ${missing[*]}"
|
|
echo "Please install them and re-run the script."
|
|
exit 1
|
|
fi
|
|
|
|
log_success "All dependencies found (docker compose: '$DOCKER_COMPOSE_CMD')"
|
|
return 0
|
|
}
|
|
|
|
detect_install_dir() {
|
|
if [[ -n "$INSTALL_DIR" ]]; then
|
|
if [[ ! -d "$INSTALL_DIR" ]]; then
|
|
log_error "Specified install directory does not exist: $INSTALL_DIR"
|
|
exit 1
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
log_info "Detecting installation directory..."
|
|
|
|
local search_paths=("$PWD" "/opt/netbird" "/opt/wiretrustee")
|
|
for dir in "${search_paths[@]}"; do
|
|
if [[ -f "$dir/management.json" ]] || [[ -f "$dir/artifacts/management.json" ]]; then
|
|
INSTALL_DIR="$dir"
|
|
log_success "Found installation at: $INSTALL_DIR"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
|
log_error "Could not auto-detect installation directory. Use --install-dir to specify."
|
|
exit 1
|
|
fi
|
|
|
|
echo ""
|
|
echo -n "Enter the path to your NetBird installation directory: "
|
|
read -r INSTALL_DIR < /dev/tty
|
|
if [[ ! -d "$INSTALL_DIR" ]]; then
|
|
log_error "Directory does not exist: $INSTALL_DIR"
|
|
exit 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
validate_old_setup() {
|
|
log_info "Validating old setup..."
|
|
|
|
# Find management.json — check both root and artifacts/
|
|
if [[ -f "$INSTALL_DIR/management.json" ]]; then
|
|
MANAGEMENT_JSON_PATH="$INSTALL_DIR/management.json"
|
|
elif [[ -f "$INSTALL_DIR/artifacts/management.json" ]]; then
|
|
MANAGEMENT_JSON_PATH="$INSTALL_DIR/artifacts/management.json"
|
|
else
|
|
log_error "Cannot find management.json in $INSTALL_DIR or $INSTALL_DIR/artifacts/"
|
|
echo "This doesn't appear to be a valid NetBird installation."
|
|
exit 1
|
|
fi
|
|
|
|
# Check for docker-compose.yml (in root or artifacts/)
|
|
local compose_found=false
|
|
if [[ -f "$INSTALL_DIR/docker-compose.yml" ]]; then
|
|
compose_found=true
|
|
elif [[ -f "$INSTALL_DIR/artifacts/docker-compose.yml" ]]; then
|
|
compose_found=true
|
|
fi
|
|
|
|
if [[ "$compose_found" != "true" ]]; then
|
|
log_error "Cannot find docker-compose.yml in $INSTALL_DIR or $INSTALL_DIR/artifacts/"
|
|
exit 1
|
|
fi
|
|
|
|
log_success "Found management.json at: $MANAGEMENT_JSON_PATH"
|
|
return 0
|
|
}
|
|
|
|
check_already_migrated() {
|
|
if [[ -f "$INSTALL_DIR/config.yaml" ]]; then
|
|
log_warn "config.yaml already exists in $INSTALL_DIR"
|
|
echo "It appears this installation has already been migrated."
|
|
echo "If you want to re-run the migration, remove config.yaml first."
|
|
exit 0
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
detect_reverse_proxy() {
|
|
log_info "Detecting reverse proxy type..."
|
|
|
|
local compose_file=""
|
|
if [[ -f "$INSTALL_DIR/docker-compose.yml" ]]; then
|
|
compose_file="$INSTALL_DIR/docker-compose.yml"
|
|
elif [[ -f "$INSTALL_DIR/artifacts/docker-compose.yml" ]]; then
|
|
compose_file="$INSTALL_DIR/artifacts/docker-compose.yml"
|
|
fi
|
|
|
|
# Check for Traefik service or labels
|
|
if grep -q 'traefik' "$compose_file" 2>/dev/null; then
|
|
PROXY_TYPE="traefik"
|
|
log_info "Detected: Traefik reverse proxy"
|
|
return 0
|
|
fi
|
|
|
|
# Check for embedded Caddy — two patterns:
|
|
# 1. Old configure.sh: dashboard container with LETSENCRYPT_DOMAIN env var + ports 80/443
|
|
# 2. v0.62+ getting-started.sh: Caddy service in compose or standalone Caddyfile
|
|
if grep -q 'LETSENCRYPT_DOMAIN' "$compose_file" 2>/dev/null && { grep -q '443:443' "$compose_file" 2>/dev/null || grep -q '443:' "$compose_file" 2>/dev/null; }; then
|
|
PROXY_TYPE="$PROXY_TYPE_CADDY"
|
|
log_info "Detected: Embedded Caddy (dashboard container with Let's Encrypt)"
|
|
return 0
|
|
fi
|
|
|
|
# Check for Caddy service in docker-compose.yml (v0.62+ pattern)
|
|
if grep -qE '^\s+caddy:|^\s+image:.*caddy' "$compose_file" 2>/dev/null; then
|
|
PROXY_TYPE="$PROXY_TYPE_CADDY"
|
|
log_info "Detected: Caddy reverse proxy (in Docker Compose)"
|
|
return 0
|
|
fi
|
|
|
|
# Check for standalone Caddyfile in install directory (v0.62+ getting-started.sh)
|
|
if [[ -f "$INSTALL_DIR/Caddyfile" ]]; then
|
|
# Verify Caddy is referenced in docker-compose.yml or running as a container
|
|
if grep -q 'caddy' "$compose_file" 2>/dev/null || grep -q 'Caddyfile' "$compose_file" 2>/dev/null; then
|
|
PROXY_TYPE="$PROXY_TYPE_CADDY"
|
|
log_info "Detected: Caddy reverse proxy (Caddyfile + Docker Compose)"
|
|
return 0
|
|
fi
|
|
# Caddyfile exists but not in compose — might be running on host
|
|
PROXY_TYPE="$PROXY_TYPE_CADDY"
|
|
log_info "Detected: Caddy reverse proxy (standalone Caddyfile)"
|
|
return 0
|
|
fi
|
|
|
|
# Check for disabled Let's Encrypt (external proxy)
|
|
if [[ -f "$INSTALL_DIR/setup.env" ]] && grep -q 'NETBIRD_DISABLE_LETSENCRYPT=true' "$INSTALL_DIR/setup.env" 2>/dev/null; then
|
|
PROXY_TYPE="external"
|
|
log_info "Detected: External reverse proxy (Let's Encrypt disabled)"
|
|
return 0
|
|
fi
|
|
|
|
# Default to external
|
|
PROXY_TYPE="external"
|
|
log_info "Detected: External/custom reverse proxy"
|
|
return 0
|
|
}
|
|
|
|
detect_idp_type() {
|
|
log_info "Detecting identity provider type..."
|
|
|
|
# Check for embedded IdP (v0.62.0+ getting-started.sh format)
|
|
local embedded_enabled
|
|
embedded_enabled=$(jq -r '.EmbeddedIdP.Enabled // false' "$MANAGEMENT_JSON_PATH" 2>/dev/null || echo "false")
|
|
if [[ "$embedded_enabled" == "true" ]]; then
|
|
IDP_TYPE="embedded"
|
|
log_success "IdP type: embedded (suitable for migration)"
|
|
return 0
|
|
fi
|
|
|
|
# Check IdpManagerConfig.ManagerType (old configure.sh format)
|
|
local manager_type
|
|
manager_type=$(jq -r '.IdpManagerConfig.ManagerType // ""' "$MANAGEMENT_JSON_PATH" 2>/dev/null || echo "")
|
|
|
|
if [[ -n "$manager_type" && "$manager_type" != "null" && "$manager_type" != "none" && "$manager_type" != "" ]]; then
|
|
IDP_TYPE="external"
|
|
log_error "External IdP detected: $manager_type"
|
|
echo ""
|
|
echo "This migration script only supports embedded IdP setups."
|
|
echo "External IdP providers (Auth0, Keycloak, Zitadel, etc.) require"
|
|
echo "a fresh installation using getting-started.sh."
|
|
echo ""
|
|
echo "Please refer to the NetBird documentation for upgrade instructions:"
|
|
echo " https://docs.netbird.io/selfhosted/getting-started"
|
|
exit 1
|
|
fi
|
|
|
|
# Check HttpConfig.AuthIssuer for well-known external providers
|
|
local auth_issuer
|
|
auth_issuer=$(jq -r '.HttpConfig.AuthIssuer // ""' "$MANAGEMENT_JSON_PATH" 2>/dev/null || echo "")
|
|
|
|
if [[ -n "$auth_issuer" && "$auth_issuer" != "null" ]]; then
|
|
for provider in "auth0.com" "accounts.google.com" "login.microsoftonline.com" "keycloak" "zitadel" "authentik"; do
|
|
if echo "$auth_issuer" | grep -qi "$provider" 2>/dev/null; then
|
|
log_error "External OIDC provider detected: $auth_issuer"
|
|
echo ""
|
|
echo "This migration script only supports embedded IdP setups."
|
|
echo "Please use getting-started.sh for a fresh installation."
|
|
exit 1
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# No embedded IdP and no external IdP detected — assume old setup without IdP manager
|
|
IDP_TYPE="embedded"
|
|
log_success "IdP type: embedded (suitable for migration)"
|
|
return 0
|
|
}
|
|
|
|
detect_volumes() {
|
|
log_info "Detecting Docker volumes..."
|
|
|
|
local volumes_list
|
|
volumes_list=$(docker volume ls --format '{{.Name}}' 2>/dev/null || echo "")
|
|
|
|
# Check for well-known volume name patterns (exact match)
|
|
local volume_patterns=(
|
|
"wiretrustee-mgmt"
|
|
"netbird-mgmt"
|
|
)
|
|
for pattern in "${volume_patterns[@]}"; do
|
|
if echo "$volumes_list" | grep -q "^${pattern}$"; then
|
|
MGMT_VOLUME="$pattern"
|
|
log_success "Found management volume: $MGMT_VOLUME"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
# Check compose-prefixed patterns (e.g., netbird_netbird-mgmt, infrastructure_files_netbird-mgmt)
|
|
local compose_prefixed
|
|
compose_prefixed=$(echo "$volumes_list" | grep -E '(netbird|wiretrustee).*mgmt' | head -n1 || echo "")
|
|
if [[ -n "$compose_prefixed" ]]; then
|
|
MGMT_VOLUME="$compose_prefixed"
|
|
log_success "Found management volume (compose-prefixed): $MGMT_VOLUME"
|
|
return 0
|
|
fi
|
|
|
|
# Try to extract volume name from old docker-compose.yml
|
|
local compose_file=""
|
|
if [[ -f "$INSTALL_DIR/docker-compose.yml" ]]; then
|
|
compose_file="$INSTALL_DIR/docker-compose.yml"
|
|
elif [[ -f "$INSTALL_DIR/artifacts/docker-compose.yml" ]]; then
|
|
compose_file="$INSTALL_DIR/artifacts/docker-compose.yml"
|
|
fi
|
|
if [[ -n "$compose_file" ]]; then
|
|
# Look for volume mount on /var/lib/netbird in management or netbird-server service
|
|
local vol_name
|
|
vol_name=$(grep -E '^\s+-\s+\S+:/var/lib/netbird' "$compose_file" 2>/dev/null | head -1 | sed 's/.*- //' | sed 's/:.*//' | tr -d ' ' || echo "")
|
|
if [[ -n "$vol_name" && "$vol_name" != "." && "$vol_name" != "/" ]]; then
|
|
# Check if this volume exists in Docker
|
|
local full_vol
|
|
full_vol=$(echo "$volumes_list" | grep -F "$vol_name" | head -1 || echo "")
|
|
if [[ -n "$full_vol" ]]; then
|
|
MGMT_VOLUME="$full_vol"
|
|
log_success "Found management volume (from compose): $MGMT_VOLUME"
|
|
return 0
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
log_warn "Could not detect management volume. A new volume will be created."
|
|
MGMT_VOLUME=""
|
|
return 0
|
|
}
|
|
|
|
detect_domain() {
|
|
log_info "Detecting domain..."
|
|
|
|
# Try setup.env first
|
|
if [[ -z "$DOMAIN" && -f "$INSTALL_DIR/setup.env" ]]; then
|
|
DOMAIN=$(grep '^NETBIRD_DOMAIN=' "$INSTALL_DIR/setup.env" 2>/dev/null | cut -d'=' -f2 | tr -d '"' | tr -d "'" || echo "")
|
|
fi
|
|
|
|
# Try EmbeddedIdP.Issuer (v0.62.0+ getting-started.sh format)
|
|
if [[ -z "$DOMAIN" ]]; then
|
|
local issuer
|
|
issuer=$(jq -r '.EmbeddedIdP.Issuer // ""' "$MANAGEMENT_JSON_PATH" 2>/dev/null || echo "")
|
|
if [[ -n "$issuer" && "$issuer" != "null" ]]; then
|
|
DOMAIN=$(echo "$issuer" | sed 's|https\?://||' | sed 's|/.*||' | sed 's|:.*||')
|
|
fi
|
|
fi
|
|
|
|
# Try HttpConfig.AuthIssuer (old configure.sh format)
|
|
if [[ -z "$DOMAIN" ]]; then
|
|
local issuer
|
|
issuer=$(jq -r '.HttpConfig.AuthIssuer // ""' "$MANAGEMENT_JSON_PATH" 2>/dev/null || echo "")
|
|
if [[ -n "$issuer" && "$issuer" != "null" ]]; then
|
|
DOMAIN=$(echo "$issuer" | sed 's|https\?://||' | sed 's|/.*||' | sed 's|:.*||')
|
|
fi
|
|
fi
|
|
|
|
# Try dashboard.env NETBIRD_MGMT_API_ENDPOINT
|
|
if [[ -z "$DOMAIN" && -f "$INSTALL_DIR/dashboard.env" ]]; then
|
|
local endpoint
|
|
endpoint=$(grep '^NETBIRD_MGMT_API_ENDPOINT=' "$INSTALL_DIR/dashboard.env" 2>/dev/null | cut -d'=' -f2 | tr -d '"' | tr -d "'" || echo "")
|
|
if [[ -n "$endpoint" ]]; then
|
|
DOMAIN=$(echo "$endpoint" | sed 's|https\?://||' | sed 's|/.*||' | sed 's|:.*||')
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "$DOMAIN" ]]; then
|
|
log_error "Could not detect domain from management.json, setup.env, or dashboard.env."
|
|
exit 1
|
|
fi
|
|
|
|
# Detect Let's Encrypt email from setup.env or dashboard.env LETSENCRYPT_DOMAIN
|
|
if [[ -f "$INSTALL_DIR/setup.env" ]]; then
|
|
LETSENCRYPT_EMAIL=$(grep '^NETBIRD_LETSENCRYPT_EMAIL=' "$INSTALL_DIR/setup.env" 2>/dev/null | cut -d'=' -f2 | tr -d '"' | tr -d "'" || echo "")
|
|
fi
|
|
|
|
log_success "Domain: $DOMAIN"
|
|
if [[ -n "$LETSENCRYPT_EMAIL" ]]; then
|
|
log_success "Let's Encrypt email: $LETSENCRYPT_EMAIL"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
detect_store_config() {
|
|
log_info "Detecting store configuration..."
|
|
|
|
# Engine from management.json
|
|
local engine
|
|
engine=$(jq -r '.StoreConfig.Engine // ""' "$MANAGEMENT_JSON_PATH" 2>/dev/null || echo "")
|
|
if [[ -n "$engine" && "$engine" != "null" && "$engine" != "" ]]; then
|
|
STORE_ENGINE="$engine"
|
|
fi
|
|
|
|
# DSN from environment files
|
|
if [[ -f "$INSTALL_DIR/setup.env" ]]; then
|
|
local pg_dsn
|
|
pg_dsn=$(grep '^NETBIRD_STORE_ENGINE_POSTGRES_DSN=' "$INSTALL_DIR/setup.env" 2>/dev/null | sed 's/^NETBIRD_STORE_ENGINE_POSTGRES_DSN=//' | tr -d '"' || echo "")
|
|
if [[ -n "$pg_dsn" ]]; then
|
|
STORE_DSN="$pg_dsn"
|
|
fi
|
|
|
|
local mysql_dsn
|
|
mysql_dsn=$(grep '^NETBIRD_STORE_ENGINE_MYSQL_DSN=' "$INSTALL_DIR/setup.env" 2>/dev/null | sed 's/^NETBIRD_STORE_ENGINE_MYSQL_DSN=//' | tr -d '"' || echo "")
|
|
if [[ -n "$mysql_dsn" ]]; then
|
|
STORE_DSN="$mysql_dsn"
|
|
fi
|
|
fi
|
|
|
|
# Also check base.setup.env
|
|
if [[ -z "$STORE_DSN" && -f "$INSTALL_DIR/base.setup.env" ]]; then
|
|
local pg_dsn
|
|
pg_dsn=$(grep '^NETBIRD_STORE_ENGINE_POSTGRES_DSN=' "$INSTALL_DIR/base.setup.env" 2>/dev/null | sed 's/^NETBIRD_STORE_ENGINE_POSTGRES_DSN=//' | tr -d '"' || echo "")
|
|
if [[ -n "$pg_dsn" ]]; then
|
|
STORE_DSN="$pg_dsn"
|
|
fi
|
|
|
|
local mysql_dsn
|
|
mysql_dsn=$(grep '^NETBIRD_STORE_ENGINE_MYSQL_DSN=' "$INSTALL_DIR/base.setup.env" 2>/dev/null | sed 's/^NETBIRD_STORE_ENGINE_MYSQL_DSN=//' | tr -d '"' || echo "")
|
|
if [[ -n "$mysql_dsn" ]]; then
|
|
STORE_DSN="$mysql_dsn"
|
|
fi
|
|
fi
|
|
|
|
log_success "Store engine: $STORE_ENGINE"
|
|
if [[ -n "$STORE_DSN" ]]; then
|
|
log_success "Store DSN: [detected]"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
extract_config_values() {
|
|
log_info "Extracting configuration from management.json..."
|
|
|
|
# DataStoreEncryptionKey
|
|
ENCRYPTION_KEY=$(jq -r '.DataStoreEncryptionKey // ""' "$MANAGEMENT_JSON_PATH" 2>/dev/null || echo "")
|
|
if [[ -z "$ENCRYPTION_KEY" || "$ENCRYPTION_KEY" == "null" ]]; then
|
|
ENCRYPTION_KEY=$(openssl rand -base64 32)
|
|
log_warn "No encryption key found in management.json — generated a new one."
|
|
log_warn "IMPORTANT: Save this key! Without it, existing encrypted data cannot be read."
|
|
echo " Encryption key: $ENCRYPTION_KEY"
|
|
fi
|
|
|
|
# Relay secret from management.json
|
|
RELAY_SECRET=$(jq -r '.Relay.Secret // ""' "$MANAGEMENT_JSON_PATH" 2>/dev/null || echo "")
|
|
|
|
# Fallback: relay secret from setup.env
|
|
if [[ (-z "$RELAY_SECRET" || "$RELAY_SECRET" == "null") && -f "$INSTALL_DIR/setup.env" ]]; then
|
|
RELAY_SECRET=$(grep '^NETBIRD_RELAY_AUTH_SECRET=' "$INSTALL_DIR/setup.env" 2>/dev/null | cut -d'=' -f2 | tr -d '"' | tr -d "'" || echo "")
|
|
fi
|
|
|
|
# Fallback: relay secret from base.setup.env
|
|
if [[ (-z "$RELAY_SECRET" || "$RELAY_SECRET" == "null") && -f "$INSTALL_DIR/base.setup.env" ]]; then
|
|
RELAY_SECRET=$(grep '^NETBIRD_RELAY_AUTH_SECRET=' "$INSTALL_DIR/base.setup.env" 2>/dev/null | cut -d'=' -f2 | tr -d '"' | tr -d "'" || echo "")
|
|
fi
|
|
|
|
# Generate if still empty
|
|
if [[ -z "$RELAY_SECRET" || "$RELAY_SECRET" == "null" ]]; then
|
|
RELAY_SECRET=$(openssl rand -base64 32 | sed "$SED_STRIP_PADDING")
|
|
log_warn "No relay secret found — generated a new one."
|
|
fi
|
|
|
|
# IdpSignKeyRefreshEnabled — check both HttpConfig and EmbeddedIdP locations
|
|
local signkey_raw
|
|
signkey_raw=$(jq -r '(.HttpConfig.IdpSignKeyRefreshEnabled // .EmbeddedIdP.SignKeyRefreshEnabled) // "true"' "$MANAGEMENT_JSON_PATH" 2>/dev/null || echo "true")
|
|
if [[ "$signkey_raw" == "false" ]]; then
|
|
SIGNKEY_REFRESH="false"
|
|
else
|
|
SIGNKEY_REFRESH="true"
|
|
fi
|
|
|
|
# ReverseProxy settings (may not exist in v0.62+ getting-started.sh format)
|
|
TRUSTED_PROXIES=$(jq -c '.ReverseProxy.TrustedHTTPProxies // []' "$MANAGEMENT_JSON_PATH" 2>/dev/null || echo "[]")
|
|
TRUSTED_PROXIES_COUNT=$(jq -r '.ReverseProxy.TrustedHTTPProxiesCount // 0' "$MANAGEMENT_JSON_PATH" 2>/dev/null || echo "0")
|
|
TRUSTED_PEERS=$(jq -c '.ReverseProxy.TrustedPeers // []' "$MANAGEMENT_JSON_PATH" 2>/dev/null || echo "[]")
|
|
|
|
log_success "Configuration values extracted"
|
|
return 0
|
|
}
|
|
|
|
print_detection_summary() {
|
|
echo ""
|
|
echo "$MSG_SEPARATOR"
|
|
echo " Migration Summary"
|
|
echo "$MSG_SEPARATOR"
|
|
echo ""
|
|
echo " Install directory: $INSTALL_DIR"
|
|
echo " Domain: $DOMAIN"
|
|
echo " Reverse proxy: $PROXY_TYPE"
|
|
echo " Store engine: $STORE_ENGINE"
|
|
if [[ -n "$STORE_DSN" ]]; then
|
|
echo " Store DSN: [configured]"
|
|
fi
|
|
if [[ -n "$MGMT_VOLUME" ]]; then
|
|
echo " Management volume: $MGMT_VOLUME"
|
|
else
|
|
echo " Management volume: [new volume will be created]"
|
|
fi
|
|
echo " Encryption key: ${ENCRYPTION_KEY:0:8}..."
|
|
echo " Relay secret: ${RELAY_SECRET:0:8}..."
|
|
echo ""
|
|
|
|
if [[ "$PROXY_TYPE" == "$PROXY_TYPE_CADDY" ]]; then
|
|
echo " Migration mode: AUTOMATIC"
|
|
echo " A Traefik-based docker-compose.yml will be generated and services"
|
|
echo " will be stopped and restarted automatically."
|
|
else
|
|
echo " Migration mode: MANUAL"
|
|
echo " New config files will be generated. You will need to stop old"
|
|
echo " containers, replace docker-compose.yml, and restart manually."
|
|
fi
|
|
echo ""
|
|
return 0
|
|
}
|
|
|
|
############################################
|
|
# Phase 1: Backup
|
|
############################################
|
|
|
|
create_backup() {
|
|
BACKUP_DIR="$INSTALL_DIR/backup-$(date +%Y%m%d-%H%M%S)"
|
|
log_info "Creating backup at: $BACKUP_DIR"
|
|
mkdir -p "$BACKUP_DIR"
|
|
|
|
# Copy config files
|
|
local files_to_backup=(
|
|
"docker-compose.yml"
|
|
"management.json"
|
|
"setup.env"
|
|
"base.setup.env"
|
|
"turnserver.conf"
|
|
"dashboard.env"
|
|
)
|
|
|
|
for f in "${files_to_backup[@]}"; do
|
|
if [[ -f "$INSTALL_DIR/$f" ]]; then
|
|
cp "$INSTALL_DIR/$f" "$BACKUP_DIR/$f"
|
|
fi
|
|
done
|
|
|
|
# Back up artifacts/ if it exists
|
|
if [[ -d "$INSTALL_DIR/artifacts" ]]; then
|
|
cp -r "$INSTALL_DIR/artifacts" "$BACKUP_DIR/artifacts"
|
|
fi
|
|
|
|
# Record state
|
|
{
|
|
echo "# NetBird migration backup state"
|
|
echo "# Created: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
|
echo ""
|
|
echo "## Docker volumes"
|
|
docker volume ls --format '{{.Name}}' 2>/dev/null | grep -E '(netbird|wiretrustee)' || echo "(none found)"
|
|
echo ""
|
|
echo "## Running containers"
|
|
docker ps --format '{{.Names}}\t{{.Image}}\t{{.Status}}' 2>/dev/null | grep -E '(netbird|wiretrustee|dashboard|signal|relay|management|coturn)' || echo "(none running)"
|
|
} > "$BACKUP_DIR/state.txt"
|
|
|
|
# Generate rollback script
|
|
generate_rollback_script
|
|
|
|
log_success "Backup created at: $BACKUP_DIR"
|
|
return 0
|
|
}
|
|
|
|
generate_rollback_script() {
|
|
cat > "$BACKUP_DIR/rollback.sh" <<'ROLLBACK_HEADER'
|
|
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
# NetBird Migration Rollback Script
|
|
# Restores the pre-migration configuration and restarts old containers.
|
|
|
|
ROLLBACK_HEADER
|
|
|
|
cat >> "$BACKUP_DIR/rollback.sh" <<ROLLBACK_BODY
|
|
INSTALL_DIR="$INSTALL_DIR"
|
|
BACKUP_DIR="$BACKUP_DIR"
|
|
|
|
echo "Restoring NetBird configuration from backup..."
|
|
|
|
# Stop new containers if running
|
|
cd "\$INSTALL_DIR"
|
|
if command -v docker-compose &>/dev/null; then
|
|
COMPOSE_CMD="docker-compose"
|
|
elif docker compose --help &>/dev/null 2>&1; then
|
|
COMPOSE_CMD="docker compose"
|
|
else
|
|
echo "ERROR: docker compose not found" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Stopping current containers..."
|
|
\$COMPOSE_CMD down 2>/dev/null || true
|
|
|
|
# Restore old config files
|
|
echo "Restoring configuration files..."
|
|
for f in docker-compose.yml management.json setup.env base.setup.env turnserver.conf dashboard.env; do
|
|
if [[ -f "\$BACKUP_DIR/\$f" ]]; then
|
|
cp "\$BACKUP_DIR/\$f" "\$INSTALL_DIR/\$f"
|
|
echo " Restored: \$f"
|
|
fi
|
|
done
|
|
|
|
# Remove new config files
|
|
for f in config.yaml; do
|
|
if [[ -f "\$INSTALL_DIR/\$f" ]]; then
|
|
rm "\$INSTALL_DIR/\$f"
|
|
echo " Removed: \$f"
|
|
fi
|
|
done
|
|
|
|
# Restart old containers
|
|
echo "Starting old containers..."
|
|
cd "\$INSTALL_DIR"
|
|
\$COMPOSE_CMD up -d
|
|
|
|
echo ""
|
|
echo "Rollback complete. Old containers are running."
|
|
echo "Verify with: \$COMPOSE_CMD ps"
|
|
ROLLBACK_BODY
|
|
|
|
chmod +x "$BACKUP_DIR/rollback.sh"
|
|
return 0
|
|
}
|
|
|
|
############################################
|
|
# Phase 3: Generate New Configuration Files
|
|
############################################
|
|
|
|
generate_config_yaml() {
|
|
log_info "Generating config.yaml..."
|
|
|
|
local dsn_line=""
|
|
if [[ -n "$STORE_DSN" ]]; then
|
|
dsn_line=" dsn: \"$STORE_DSN\""
|
|
fi
|
|
|
|
local reverse_proxy_section=""
|
|
# Only add reverseProxy if there are non-default values
|
|
local has_proxy_config=false
|
|
if [[ "$TRUSTED_PROXIES" != "[]" && -n "$TRUSTED_PROXIES" ]]; then
|
|
has_proxy_config=true
|
|
fi
|
|
if [[ "$TRUSTED_PROXIES_COUNT" != "0" && -n "$TRUSTED_PROXIES_COUNT" ]]; then
|
|
has_proxy_config=true
|
|
fi
|
|
if [[ "$TRUSTED_PEERS" != "[]" && -n "$TRUSTED_PEERS" ]]; then
|
|
# Check if it's only the default ["0.0.0.0/0"]
|
|
local default_peers='["0.0.0.0/0"]'
|
|
if [[ "$TRUSTED_PEERS" != "$default_peers" ]]; then
|
|
has_proxy_config=true
|
|
fi
|
|
fi
|
|
|
|
if [[ "$has_proxy_config" == "true" ]]; then
|
|
reverse_proxy_section="
|
|
reverseProxy:"
|
|
if [[ "$TRUSTED_PROXIES" != "[]" && -n "$TRUSTED_PROXIES" ]]; then
|
|
reverse_proxy_section+="
|
|
trustedHTTPProxies:"
|
|
for proxy in $(echo "$TRUSTED_PROXIES" | jq -r '.[]' 2>/dev/null); do
|
|
reverse_proxy_section+="
|
|
- \"$proxy\""
|
|
done
|
|
fi
|
|
if [[ "$TRUSTED_PROXIES_COUNT" != "0" && -n "$TRUSTED_PROXIES_COUNT" ]]; then
|
|
reverse_proxy_section+="
|
|
trustedHTTPProxiesCount: $TRUSTED_PROXIES_COUNT"
|
|
fi
|
|
if [[ "$TRUSTED_PEERS" != "[]" && -n "$TRUSTED_PEERS" ]]; then
|
|
reverse_proxy_section+="
|
|
trustedPeers:"
|
|
for peer in $(echo "$TRUSTED_PEERS" | jq -r '.[]' 2>/dev/null); do
|
|
reverse_proxy_section+="
|
|
- \"$peer\""
|
|
done
|
|
fi
|
|
fi
|
|
|
|
{
|
|
cat <<EOF
|
|
# Combined NetBird Server Configuration (Simplified)
|
|
# Generated by migrate.sh on $(date -u '+%Y-%m-%d %H:%M:%S UTC')
|
|
|
|
server:
|
|
listenAddress: ":80"
|
|
exposedAddress: "https://${DOMAIN}:443"
|
|
stunPorts:
|
|
- 3478
|
|
metricsPort: 9090
|
|
healthcheckAddress: ":9000"
|
|
logLevel: "info"
|
|
logFile: "console"
|
|
|
|
authSecret: "${RELAY_SECRET}"
|
|
dataDir: "/var/lib/netbird"
|
|
|
|
auth:
|
|
issuer: "https://${DOMAIN}/oauth2"
|
|
signKeyRefreshEnabled: ${SIGNKEY_REFRESH}
|
|
dashboardRedirectURIs:
|
|
- "https://${DOMAIN}/nb-auth"
|
|
- "https://${DOMAIN}/nb-silent-auth"
|
|
cliRedirectURIs:
|
|
- "http://localhost:53000/"
|
|
|
|
store:
|
|
engine: "${STORE_ENGINE}"
|
|
encryptionKey: "${ENCRYPTION_KEY}"
|
|
EOF
|
|
if [[ -n "$dsn_line" ]]; then
|
|
echo "$dsn_line"
|
|
fi
|
|
if [[ -n "$reverse_proxy_section" ]]; then
|
|
echo "$reverse_proxy_section"
|
|
fi
|
|
} > "$INSTALL_DIR/config.yaml"
|
|
|
|
log_success "Generated config.yaml"
|
|
return 0
|
|
}
|
|
|
|
generate_dashboard_env() {
|
|
log_info "Generating dashboard.env..."
|
|
|
|
cat > "$INSTALL_DIR/dashboard.env" <<EOF
|
|
# Endpoints
|
|
NETBIRD_MGMT_API_ENDPOINT=https://${DOMAIN}
|
|
NETBIRD_MGMT_GRPC_API_ENDPOINT=https://${DOMAIN}
|
|
# OIDC - using embedded IdP
|
|
AUTH_AUDIENCE=netbird-dashboard
|
|
AUTH_CLIENT_ID=netbird-dashboard
|
|
AUTH_CLIENT_SECRET=
|
|
AUTH_AUTHORITY=https://${DOMAIN}/oauth2
|
|
USE_AUTH0=false
|
|
AUTH_SUPPORTED_SCOPES=openid profile email groups
|
|
AUTH_REDIRECT_URI=/nb-auth
|
|
AUTH_SILENT_REDIRECT_URI=/nb-silent-auth
|
|
# SSL
|
|
NGINX_SSL_PORT=443
|
|
# Letsencrypt
|
|
LETSENCRYPT_DOMAIN=none
|
|
EOF
|
|
|
|
log_success "Generated dashboard.env"
|
|
return 0
|
|
}
|
|
|
|
generate_docker_compose_traefik() {
|
|
log_info "Generating docker-compose.yml (Traefik)..."
|
|
|
|
local acme_email="${LETSENCRYPT_EMAIL:-admin@${DOMAIN}}"
|
|
|
|
# Volume config: use old volume if detected
|
|
local volume_config=""
|
|
if [[ -n "$MGMT_VOLUME" ]]; then
|
|
volume_config=" netbird_data:
|
|
external: true
|
|
name: ${MGMT_VOLUME}"
|
|
else
|
|
volume_config=" netbird_data:"
|
|
fi
|
|
|
|
cat > "$INSTALL_DIR/docker-compose.yml" <<EOF
|
|
services:
|
|
# Traefik reverse proxy (automatic TLS via Let's Encrypt)
|
|
traefik:
|
|
image: traefik:v3.6
|
|
container_name: netbird-traefik
|
|
restart: unless-stopped
|
|
networks:
|
|
netbird:
|
|
ipv4_address: 172.30.0.10
|
|
command:
|
|
# Logging
|
|
- "--log.level=INFO"
|
|
- "--accesslog=true"
|
|
# Docker provider
|
|
- "--providers.docker=true"
|
|
- "--providers.docker.exposedbydefault=false"
|
|
- "--providers.docker.network=netbird"
|
|
# Entrypoints
|
|
- "--entrypoints.web.address=:80"
|
|
- "--entrypoints.websecure.address=:443"
|
|
- "--entrypoints.websecure.allowACMEByPass=true"
|
|
# Disable timeouts for long-lived gRPC streams
|
|
- "--entrypoints.websecure.transport.respondingTimeouts.readTimeout=0"
|
|
- "--entrypoints.websecure.transport.respondingTimeouts.writeTimeout=0"
|
|
- "--entrypoints.websecure.transport.respondingTimeouts.idleTimeout=0"
|
|
# HTTP to HTTPS redirect
|
|
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
|
|
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
|
|
# Let's Encrypt ACME
|
|
- "--certificatesresolvers.letsencrypt.acme.email=${acme_email}"
|
|
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
|
|
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
|
|
# gRPC transport settings
|
|
- "--serverstransport.forwardingtimeouts.responseheadertimeout=0s"
|
|
- "--serverstransport.forwardingtimeouts.idleconntimeout=0s"
|
|
ports:
|
|
- '443:443'
|
|
- '80:80'
|
|
volumes:
|
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
- netbird_traefik_letsencrypt:/letsencrypt
|
|
logging:
|
|
driver: "json-file"
|
|
options:
|
|
max-size: "500m"
|
|
max-file: "2"
|
|
|
|
# UI dashboard
|
|
dashboard:
|
|
image: ${DASHBOARD_IMAGE}
|
|
container_name: netbird-dashboard
|
|
restart: unless-stopped
|
|
networks: [netbird]
|
|
env_file:
|
|
- ./dashboard.env
|
|
labels:
|
|
- traefik.enable=true
|
|
- traefik.http.routers.netbird-dashboard.rule=Host(\`${DOMAIN}\`)
|
|
- traefik.http.routers.netbird-dashboard.entrypoints=websecure
|
|
- traefik.http.routers.netbird-dashboard.tls=true
|
|
- traefik.http.routers.netbird-dashboard.tls.certresolver=letsencrypt
|
|
- traefik.http.routers.netbird-dashboard.service=dashboard
|
|
- traefik.http.routers.netbird-dashboard.priority=1
|
|
- traefik.http.services.dashboard.loadbalancer.server.port=80
|
|
logging:
|
|
driver: "json-file"
|
|
options:
|
|
max-size: "500m"
|
|
max-file: "2"
|
|
|
|
# Combined server (Management + Signal + Relay + STUN)
|
|
netbird-server:
|
|
image: ${NETBIRD_SERVER_IMAGE}
|
|
container_name: netbird-server
|
|
restart: unless-stopped
|
|
networks: [netbird]
|
|
ports:
|
|
- '3478:3478/udp'
|
|
volumes:
|
|
- netbird_data:/var/lib/netbird
|
|
- ./config.yaml:/etc/netbird/config.yaml
|
|
command: ["--config", "/etc/netbird/config.yaml"]
|
|
labels:
|
|
- traefik.enable=true
|
|
# gRPC router (needs h2c backend for HTTP/2 cleartext)
|
|
- traefik.http.routers.netbird-grpc.rule=Host(\`${DOMAIN}\`) && (PathPrefix(\`/signalexchange.SignalExchange/\`) || PathPrefix(\`/management.ManagementService/\`))
|
|
- traefik.http.routers.netbird-grpc.entrypoints=websecure
|
|
- traefik.http.routers.netbird-grpc.tls=true
|
|
- traefik.http.routers.netbird-grpc.tls.certresolver=letsencrypt
|
|
- traefik.http.routers.netbird-grpc.service=netbird-server-h2c
|
|
- traefik.http.routers.netbird-grpc.priority=100
|
|
# Backend router (relay, WebSocket, API, OAuth2)
|
|
- traefik.http.routers.netbird-backend.rule=Host(\`${DOMAIN}\`) && (PathPrefix(\`/relay\`) || PathPrefix(\`/ws-proxy/\`) || PathPrefix(\`/api\`) || PathPrefix(\`/oauth2\`))
|
|
- traefik.http.routers.netbird-backend.entrypoints=websecure
|
|
- traefik.http.routers.netbird-backend.tls=true
|
|
- traefik.http.routers.netbird-backend.tls.certresolver=letsencrypt
|
|
- traefik.http.routers.netbird-backend.service=netbird-server
|
|
- traefik.http.routers.netbird-backend.priority=100
|
|
# Services
|
|
- traefik.http.services.netbird-server.loadbalancer.server.port=80
|
|
- traefik.http.services.netbird-server-h2c.loadbalancer.server.port=80
|
|
- traefik.http.services.netbird-server-h2c.loadbalancer.server.scheme=h2c
|
|
logging:
|
|
driver: "json-file"
|
|
options:
|
|
max-size: "500m"
|
|
max-file: "2"
|
|
|
|
volumes:
|
|
${volume_config}
|
|
netbird_traefik_letsencrypt:
|
|
|
|
networks:
|
|
netbird:
|
|
driver: bridge
|
|
ipam:
|
|
config:
|
|
- subnet: 172.30.0.0/24
|
|
gateway: 172.30.0.1
|
|
EOF
|
|
|
|
log_success "Generated docker-compose.yml"
|
|
return 0
|
|
}
|
|
|
|
generate_docker_compose_exposed_ports() {
|
|
log_info "Generating docker-compose.yml (exposed ports for custom proxy)..."
|
|
|
|
# Volume config: use old volume if detected
|
|
local volume_config=""
|
|
if [[ -n "$MGMT_VOLUME" ]]; then
|
|
volume_config=" netbird_data:
|
|
external: true
|
|
name: ${MGMT_VOLUME}"
|
|
else
|
|
volume_config=" netbird_data:"
|
|
fi
|
|
|
|
cat > "$INSTALL_DIR/docker-compose.yml" <<EOF
|
|
services:
|
|
# UI dashboard
|
|
dashboard:
|
|
image: ${DASHBOARD_IMAGE}
|
|
container_name: netbird-dashboard
|
|
restart: unless-stopped
|
|
networks: [netbird]
|
|
ports:
|
|
- '127.0.0.1:8080:80'
|
|
env_file:
|
|
- ./dashboard.env
|
|
logging:
|
|
driver: "json-file"
|
|
options:
|
|
max-size: "500m"
|
|
max-file: "2"
|
|
|
|
# Combined server (Management + Signal + Relay + STUN)
|
|
netbird-server:
|
|
image: ${NETBIRD_SERVER_IMAGE}
|
|
container_name: netbird-server
|
|
restart: unless-stopped
|
|
networks: [netbird]
|
|
ports:
|
|
- '127.0.0.1:8081:80'
|
|
- '3478:3478/udp'
|
|
volumes:
|
|
- netbird_data:/var/lib/netbird
|
|
- ./config.yaml:/etc/netbird/config.yaml
|
|
command: ["--config", "/etc/netbird/config.yaml"]
|
|
logging:
|
|
driver: "json-file"
|
|
options:
|
|
max-size: "500m"
|
|
max-file: "2"
|
|
|
|
volumes:
|
|
${volume_config}
|
|
|
|
networks:
|
|
netbird:
|
|
EOF
|
|
|
|
log_success "Generated docker-compose.yml"
|
|
return 0
|
|
}
|
|
|
|
generate_docker_compose() {
|
|
if [[ "$PROXY_TYPE" == "$PROXY_TYPE_CADDY" ]]; then
|
|
generate_docker_compose_traefik
|
|
else
|
|
generate_docker_compose_exposed_ports
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
############################################
|
|
# Phase 4: Apply Migration
|
|
############################################
|
|
|
|
stop_old_services() {
|
|
log_info "Stopping old containers..."
|
|
|
|
# Try to find old compose file location
|
|
local old_compose_dir="$INSTALL_DIR"
|
|
if [[ -f "$INSTALL_DIR/artifacts/docker-compose.yml" && ! -f "$INSTALL_DIR/docker-compose.yml" ]]; then
|
|
old_compose_dir="$INSTALL_DIR/artifacts"
|
|
fi
|
|
|
|
(cd "$old_compose_dir" && $DOCKER_COMPOSE_CMD down 2>/dev/null) || true
|
|
|
|
log_success "Old containers stopped"
|
|
return 0
|
|
}
|
|
|
|
start_new_services() {
|
|
log_info "Starting new containers..."
|
|
|
|
(cd "$INSTALL_DIR" && $DOCKER_COMPOSE_CMD up -d)
|
|
|
|
log_success "New containers started"
|
|
return 0
|
|
}
|
|
|
|
wait_for_health() {
|
|
log_info "Waiting for services to become healthy..."
|
|
|
|
local max_attempts=60
|
|
local attempt=0
|
|
|
|
set +e
|
|
echo -n " Checking"
|
|
while [[ $attempt -lt $max_attempts ]]; do
|
|
# Try OIDC endpoint through reverse proxy
|
|
if curl -sk -f -o /dev/null "https://${DOMAIN}/oauth2/.well-known/openid-configuration" 2>/dev/null; then
|
|
echo " done"
|
|
set -e
|
|
log_success "Services are healthy"
|
|
return 0
|
|
fi
|
|
|
|
# Also try health check endpoint directly
|
|
if curl -sk -f -o /dev/null "http://127.0.0.1:9000/" 2>/dev/null; then
|
|
echo " done"
|
|
set -e
|
|
log_success "Services are healthy (via healthcheck)"
|
|
return 0
|
|
fi
|
|
|
|
echo -n " ."
|
|
sleep 2
|
|
attempt=$((attempt + 1))
|
|
|
|
if [[ $attempt -eq 30 ]]; then
|
|
echo ""
|
|
log_warn "Taking longer than expected. Checking container logs..."
|
|
(cd "$INSTALL_DIR" && $DOCKER_COMPOSE_CMD logs --tail=10 netbird-server 2>/dev/null) || true
|
|
echo -n " Still checking"
|
|
fi
|
|
done
|
|
echo ""
|
|
set -e
|
|
|
|
log_warn "Health check timed out after $((max_attempts * 2)) seconds."
|
|
log_warn "Services may still be starting. Check with: cd $INSTALL_DIR && $DOCKER_COMPOSE_CMD logs"
|
|
return 0
|
|
}
|
|
|
|
############################################
|
|
# Phase 5: Verification & Summary
|
|
############################################
|
|
|
|
verify_migration() {
|
|
log_info "Running verification checks..."
|
|
|
|
local checks_passed=0
|
|
local checks_total=3
|
|
|
|
# Check 1: Container health
|
|
local running
|
|
running=$(cd "$INSTALL_DIR" && $DOCKER_COMPOSE_CMD ps --format '{{.Name}}' 2>/dev/null | wc -l || echo "0")
|
|
if [[ "$running" -ge 2 ]]; then
|
|
log_success "Containers are running ($running services)"
|
|
checks_passed=$((checks_passed + 1))
|
|
else
|
|
log_warn "Expected at least 2 running containers, found $running"
|
|
fi
|
|
|
|
# Check 2: OIDC endpoint
|
|
local oidc_status
|
|
oidc_status=$(curl -sk -o /dev/null -w '%{http_code}' "https://${DOMAIN}/oauth2/.well-known/openid-configuration" 2>/dev/null || echo "000")
|
|
if [[ "$oidc_status" == "200" ]]; then
|
|
log_success "OIDC endpoint responding (HTTP $oidc_status)"
|
|
checks_passed=$((checks_passed + 1))
|
|
else
|
|
log_warn "OIDC endpoint returned HTTP $oidc_status (expected 200)"
|
|
fi
|
|
|
|
# Check 3: Management API (expect 401 = working but needs auth, not 502 = proxy error)
|
|
local api_status
|
|
api_status=$(curl -sk -o /dev/null -w '%{http_code}' "https://${DOMAIN}/api/accounts" 2>/dev/null || echo "000")
|
|
if [[ "$api_status" == "401" || "$api_status" == "200" || "$api_status" == "403" ]]; then
|
|
log_success "Management API responding (HTTP $api_status)"
|
|
checks_passed=$((checks_passed + 1))
|
|
else
|
|
log_warn "Management API returned HTTP $api_status (expected 401/200/403)"
|
|
fi
|
|
|
|
echo ""
|
|
echo " Verification: $checks_passed/$checks_total checks passed"
|
|
return 0
|
|
}
|
|
|
|
print_summary() {
|
|
echo ""
|
|
echo "$MSG_SEPARATOR"
|
|
echo " Migration Complete"
|
|
echo "$MSG_SEPARATOR"
|
|
echo ""
|
|
|
|
if [[ "$PROXY_TYPE" == "$PROXY_TYPE_CADDY" ]]; then
|
|
echo " What was done:"
|
|
echo " - Old 5-container setup stopped"
|
|
echo " - New config.yaml generated (combined server config)"
|
|
echo " - New dashboard.env generated (embedded IdP)"
|
|
echo " - New docker-compose.yml generated (Traefik + combined server)"
|
|
echo " - New containers started"
|
|
else
|
|
echo " What was done:"
|
|
echo " - New config.yaml generated (combined server config)"
|
|
echo " - New dashboard.env generated (embedded IdP)"
|
|
echo " - New docker-compose.yml generated (exposed ports)"
|
|
echo ""
|
|
echo " What you need to do:"
|
|
echo " 1. Stop old containers:"
|
|
echo " cd $INSTALL_DIR && $DOCKER_COMPOSE_CMD down"
|
|
echo ""
|
|
echo " 2. Start new containers:"
|
|
echo " cd $INSTALL_DIR && $DOCKER_COMPOSE_CMD up -d"
|
|
echo ""
|
|
echo " 3. Update your reverse proxy to route:"
|
|
echo " - /signalexchange.SignalExchange/* -> 127.0.0.1:8081 (gRPC/h2c)"
|
|
echo " - /management.ManagementService/* -> 127.0.0.1:8081 (gRPC/h2c)"
|
|
echo " - /relay*, /ws-proxy/* -> 127.0.0.1:8081 (WebSocket)"
|
|
echo " - /api/*, /oauth2/* -> 127.0.0.1:8081 (HTTP)"
|
|
echo " - /* -> 127.0.0.1:8080 (dashboard)"
|
|
fi
|
|
|
|
echo ""
|
|
echo " Backup location: $BACKUP_DIR"
|
|
echo " Rollback command: bash $BACKUP_DIR/rollback.sh"
|
|
echo ""
|
|
echo " IMPORTANT:"
|
|
echo " - Existing peers, routes, and policies are preserved in the database."
|
|
echo " - The embedded IdP data is preserved in the management volume."
|
|
echo " - Clients should reconnect automatically; if not: netbird down && netbird up"
|
|
echo ""
|
|
echo " Next steps:"
|
|
echo " - Access the dashboard: https://$DOMAIN"
|
|
echo " - Re-authenticate all clients: netbird down && netbird up"
|
|
echo " - Check logs: cd $INSTALL_DIR && $DOCKER_COMPOSE_CMD logs -f"
|
|
echo ""
|
|
return 0
|
|
}
|
|
|
|
############################################
|
|
# Main
|
|
############################################
|
|
|
|
main() {
|
|
# Parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
local arg="$1"
|
|
case "$arg" in
|
|
--install-dir)
|
|
local dir_value="$2"
|
|
INSTALL_DIR="$dir_value"
|
|
shift 2
|
|
;;
|
|
--non-interactive)
|
|
NON_INTERACTIVE=true
|
|
shift
|
|
;;
|
|
--help|-h)
|
|
echo "Usage: $0 [--install-dir /path/to/netbird] [--non-interactive]"
|
|
echo ""
|
|
echo "Migrates a pre-v0.65.0 NetBird deployment to the combined container setup."
|
|
echo ""
|
|
echo "Options:"
|
|
echo " --install-dir DIR Path to existing NetBird installation"
|
|
echo " --non-interactive Skip confirmation prompts (for automation)"
|
|
echo " -h, --help Show this help message"
|
|
exit 0
|
|
;;
|
|
*)
|
|
log_error "Unknown option: $arg"
|
|
echo "Use --help for usage information."
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
print_banner
|
|
|
|
# Phase 0: Preflight & Detection
|
|
check_dependencies
|
|
detect_install_dir
|
|
validate_old_setup
|
|
check_already_migrated
|
|
detect_reverse_proxy
|
|
detect_idp_type
|
|
detect_volumes
|
|
detect_domain
|
|
detect_store_config
|
|
extract_config_values
|
|
print_detection_summary
|
|
|
|
confirm_action "Proceed with migration?"
|
|
|
|
# Phase 1: Backup
|
|
create_backup
|
|
|
|
# Phase 4: Apply migration
|
|
if [[ "$PROXY_TYPE" == "$PROXY_TYPE_CADDY" ]]; then
|
|
# Stop old containers BEFORE overwriting docker-compose.yml
|
|
stop_old_services
|
|
|
|
# Phase 2 + 3: Generate new configuration files
|
|
generate_config_yaml
|
|
generate_dashboard_env
|
|
generate_docker_compose
|
|
|
|
start_new_services
|
|
sleep 3
|
|
wait_for_health
|
|
|
|
# Phase 5: Verification
|
|
verify_migration
|
|
else
|
|
# For manual proxy setups, just generate files (don't stop/start)
|
|
generate_config_yaml
|
|
generate_dashboard_env
|
|
generate_docker_compose
|
|
fi
|
|
|
|
print_summary
|
|
return 0
|
|
}
|
|
|
|
main "$@"
|