611 lines
18 KiB
Bash
Executable File
611 lines
18 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# =============================================================================
|
|
# phase7_5_nginx_to_caddy.sh — One-time Nginx -> Caddy migration cutover helper
|
|
#
|
|
# Goals:
|
|
# - Serve both sintheus.com and privacyindesign.com hostnames from one Caddy
|
|
# - Keep public ingress HTTPS-only
|
|
# - Support canary-first rollout (default: tower.sintheus.com only)
|
|
# - Preserve current mixed backend schemes (http/https) unless strict mode is enabled
|
|
#
|
|
# Usage examples:
|
|
# ./phase7_5_nginx_to_caddy.sh
|
|
# ./phase7_5_nginx_to_caddy.sh --mode=full
|
|
# ./phase7_5_nginx_to_caddy.sh --mode=full --strict-backend-https
|
|
# ./phase7_5_nginx_to_caddy.sh --mode=canary --yes
|
|
# =============================================================================
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
source "${SCRIPT_DIR}/lib/common.sh"
|
|
|
|
AUTO_YES=false
|
|
MODE="canary" # canary|full
|
|
STRICT_BACKEND_HTTPS=false
|
|
|
|
# Reuse Unraid's existing Docker network.
|
|
UNRAID_DOCKER_NETWORK_NAME="br0"
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: $(basename "$0") [options]
|
|
|
|
Options:
|
|
--mode=canary|full Rollout scope (default: canary)
|
|
--strict-backend-https Require all upstream backends to be https://
|
|
--yes, -y Skip confirmation prompts
|
|
--help, -h Show this help
|
|
EOF
|
|
}
|
|
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--mode=*) MODE="${arg#*=}" ;;
|
|
--strict-backend-https) STRICT_BACKEND_HTTPS=true ;;
|
|
--yes|-y) AUTO_YES=true ;;
|
|
--help|-h) usage; exit 0 ;;
|
|
*)
|
|
log_error "Unknown argument: $arg"
|
|
usage
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ "$MODE" != "canary" && "$MODE" != "full" ]]; then
|
|
log_error "Invalid --mode '$MODE' (use: canary|full)"
|
|
exit 1
|
|
fi
|
|
|
|
confirm_action() {
|
|
local prompt="$1"
|
|
if [[ "$AUTO_YES" == "true" ]]; then
|
|
log_info "Auto-confirmed (--yes): ${prompt}"
|
|
return 0
|
|
fi
|
|
printf '%s' "$prompt"
|
|
read -r confirm
|
|
[[ "$confirm" =~ ^[Yy]$ ]]
|
|
}
|
|
|
|
load_env
|
|
require_vars UNRAID_IP UNRAID_SSH_USER UNRAID_COMPOSE_DIR \
|
|
UNRAID_CADDY_IP UNRAID_GITEA_IP \
|
|
GITEA_DOMAIN CADDY_DATA_PATH TLS_MODE
|
|
|
|
if [[ "$TLS_MODE" == "cloudflare" ]]; then
|
|
require_vars CLOUDFLARE_API_TOKEN
|
|
elif [[ "$TLS_MODE" == "existing" ]]; then
|
|
require_vars SSL_CERT_PATH SSL_KEY_PATH
|
|
else
|
|
log_error "Invalid TLS_MODE='${TLS_MODE}' — must be 'cloudflare' or 'existing'"
|
|
exit 1
|
|
fi
|
|
|
|
phase_header "7.5" "Nginx to Caddy Migration (Multi-domain)"
|
|
|
|
# host|upstream|streaming(true/false)|body_limit|insecure_skip_verify(true/false)
|
|
FULL_HOST_MAP=(
|
|
"ai.sintheus.com|http://192.168.1.82:8181 http://192.168.1.83:8181|true|50MB|false"
|
|
"getter.sintheus.com|http://192.168.1.3:8181|false||false"
|
|
"portainer.sintheus.com|https://192.168.1.181:9443|false||true"
|
|
"photos.sintheus.com|http://192.168.1.222:2283|false|50GB|false"
|
|
"fin.sintheus.com|http://192.168.1.233:8096|true||false"
|
|
"disk.sintheus.com|http://192.168.1.52:80|false|20GB|false"
|
|
"pi.sintheus.com|http://192.168.1.4:80|false||false"
|
|
"plex.sintheus.com|http://192.168.1.111:32400|true||false"
|
|
"sync.sintheus.com|http://192.168.1.119:8384|false||false"
|
|
"syno.sintheus.com|https://100.108.182.16:5001|false||true"
|
|
"tower.sintheus.com|https://192.168.1.82:443 https://192.168.1.83:443|false||true"
|
|
)
|
|
|
|
CANARY_HOST_MAP=(
|
|
"tower.sintheus.com|https://192.168.1.82:443 https://192.168.1.83:443|false||true"
|
|
)
|
|
|
|
GITEA_ENTRY="${GITEA_DOMAIN}|http://${UNRAID_GITEA_IP}:3000|false||false"
|
|
CADDY_COMPOSE_DIR="${UNRAID_COMPOSE_DIR}/caddy"
|
|
|
|
SELECTED_HOST_MAP=()
|
|
if [[ "$MODE" == "canary" ]]; then
|
|
SELECTED_HOST_MAP=( "${CANARY_HOST_MAP[@]}" )
|
|
else
|
|
SELECTED_HOST_MAP=( "${FULL_HOST_MAP[@]}" "$GITEA_ENTRY" )
|
|
fi
|
|
|
|
validate_backend_tls_policy() {
|
|
local -a non_tls_entries=()
|
|
local entry host upstream
|
|
for entry in "${SELECTED_HOST_MAP[@]}"; do
|
|
IFS='|' read -r host upstream _ <<< "$entry"
|
|
if [[ "$upstream" != https://* ]]; then
|
|
non_tls_entries+=( "${host} -> ${upstream}" )
|
|
fi
|
|
done
|
|
|
|
if [[ "${#non_tls_entries[@]}" -eq 0 ]]; then
|
|
log_success "All selected backends are HTTPS"
|
|
return 0
|
|
fi
|
|
|
|
if [[ "$STRICT_BACKEND_HTTPS" == "true" ]]; then
|
|
log_error "Strict backend HTTPS is enabled, but these entries are not HTTPS:"
|
|
printf '%s\n' "${non_tls_entries[@]}" | sed 's/^/ - /' >&2
|
|
return 1
|
|
fi
|
|
|
|
log_warn "Using mixed backend schemes (allowed):"
|
|
printf '%s\n' "${non_tls_entries[@]}" | sed 's/^/ - /' >&2
|
|
}
|
|
|
|
emit_site_block() {
|
|
local outfile="$1" host="$2" upstream="$3" streaming="$4" body_limit="$5" skip_verify="$6"
|
|
|
|
{
|
|
echo "${host} {"
|
|
if [[ "$TLS_MODE" == "existing" ]]; then
|
|
echo " tls ${SSL_CERT_PATH} ${SSL_KEY_PATH}"
|
|
fi
|
|
echo " import common_security"
|
|
echo
|
|
if [[ -n "$body_limit" ]]; then
|
|
echo " request_body {"
|
|
echo " max_size ${body_limit}"
|
|
echo " }"
|
|
echo
|
|
fi
|
|
echo " reverse_proxy ${upstream} {"
|
|
if [[ "$streaming" == "true" ]]; then
|
|
echo " import proxy_streaming"
|
|
else
|
|
echo " import proxy_headers"
|
|
fi
|
|
if [[ "$skip_verify" == "true" && "$upstream" == https://* ]]; then
|
|
echo " transport http {"
|
|
echo " tls_insecure_skip_verify"
|
|
echo " }"
|
|
fi
|
|
echo " }"
|
|
echo "}"
|
|
echo
|
|
} >> "$outfile"
|
|
}
|
|
|
|
emit_site_block_standalone() {
|
|
local outfile="$1" host="$2" upstream="$3" streaming="$4" body_limit="$5" skip_verify="$6"
|
|
|
|
{
|
|
echo "${host} {"
|
|
if [[ "$TLS_MODE" == "cloudflare" ]]; then
|
|
echo " tls {"
|
|
echo " dns cloudflare {env.CF_API_TOKEN}"
|
|
echo " }"
|
|
elif [[ "$TLS_MODE" == "existing" ]]; then
|
|
echo " tls ${SSL_CERT_PATH} ${SSL_KEY_PATH}"
|
|
fi
|
|
echo " encode zstd gzip"
|
|
echo " header {"
|
|
echo " Strict-Transport-Security \"max-age=31536000; includeSubDomains; preload\""
|
|
echo " X-Content-Type-Options \"nosniff\""
|
|
echo " X-Frame-Options \"SAMEORIGIN\""
|
|
echo " Referrer-Policy \"strict-origin-when-cross-origin\""
|
|
echo " -Server"
|
|
echo " }"
|
|
echo
|
|
if [[ -n "$body_limit" ]]; then
|
|
echo " request_body {"
|
|
echo " max_size ${body_limit}"
|
|
echo " }"
|
|
echo
|
|
fi
|
|
echo " reverse_proxy ${upstream} {"
|
|
echo " header_up Host {host}"
|
|
echo " header_up X-Real-IP {remote_host}"
|
|
if [[ "$streaming" == "true" ]]; then
|
|
echo " flush_interval -1"
|
|
fi
|
|
if [[ "$skip_verify" == "true" && "$upstream" == https://* ]]; then
|
|
echo " transport http {"
|
|
echo " tls_insecure_skip_verify"
|
|
echo " }"
|
|
fi
|
|
echo " }"
|
|
echo "}"
|
|
echo
|
|
} >> "$outfile"
|
|
}
|
|
|
|
caddy_block_extract_for_host() {
|
|
local infile="$1" host="$2" outfile="$3"
|
|
awk -v host="$host" '
|
|
function trim(s) {
|
|
sub(/^[[:space:]]+/, "", s)
|
|
sub(/[[:space:]]+$/, "", s)
|
|
return s
|
|
}
|
|
function brace_delta(s, tmp, opens, closes) {
|
|
tmp = s
|
|
opens = gsub(/\{/, "{", tmp)
|
|
closes = gsub(/\}/, "}", tmp)
|
|
return opens - closes
|
|
}
|
|
function has_host(labels, i, n, parts, token) {
|
|
labels = trim(labels)
|
|
gsub(/[[:space:]]+/, "", labels)
|
|
n = split(labels, parts, ",")
|
|
for (i = 1; i <= n; i++) {
|
|
token = parts[i]
|
|
if (token == host) {
|
|
return 1
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
BEGIN {
|
|
depth = 0
|
|
in_target = 0
|
|
target_depth = 0
|
|
found = 0
|
|
}
|
|
|
|
{
|
|
line = $0
|
|
|
|
if (!in_target) {
|
|
if (depth == 0) {
|
|
pos = index(line, "{")
|
|
if (pos > 0) {
|
|
labels = substr(line, 1, pos - 1)
|
|
if (trim(labels) != "" && labels !~ /^[[:space:]]*\(/ && has_host(labels)) {
|
|
in_target = 1
|
|
target_depth = brace_delta(line)
|
|
found = 1
|
|
print line
|
|
next
|
|
}
|
|
}
|
|
}
|
|
depth += brace_delta(line)
|
|
} else {
|
|
target_depth += brace_delta(line)
|
|
print line
|
|
if (target_depth <= 0) {
|
|
in_target = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
END {
|
|
if (!found) {
|
|
exit 1
|
|
}
|
|
}
|
|
' "$infile" > "$outfile"
|
|
}
|
|
|
|
caddy_block_remove_for_host() {
|
|
local infile="$1" host="$2" outfile="$3"
|
|
awk -v host="$host" '
|
|
function trim(s) {
|
|
sub(/^[[:space:]]+/, "", s)
|
|
sub(/[[:space:]]+$/, "", s)
|
|
return s
|
|
}
|
|
function brace_delta(s, tmp, opens, closes) {
|
|
tmp = s
|
|
opens = gsub(/\{/, "{", tmp)
|
|
closes = gsub(/\}/, "}", tmp)
|
|
return opens - closes
|
|
}
|
|
function has_host(labels, i, n, parts, token) {
|
|
labels = trim(labels)
|
|
gsub(/[[:space:]]+/, "", labels)
|
|
n = split(labels, parts, ",")
|
|
for (i = 1; i <= n; i++) {
|
|
token = parts[i]
|
|
if (token == host) {
|
|
return 1
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
BEGIN {
|
|
depth = 0
|
|
in_target = 0
|
|
target_depth = 0
|
|
removed = 0
|
|
}
|
|
|
|
{
|
|
line = $0
|
|
|
|
if (in_target) {
|
|
target_depth += brace_delta(line)
|
|
if (target_depth <= 0) {
|
|
in_target = 0
|
|
}
|
|
next
|
|
}
|
|
|
|
if (depth == 0) {
|
|
pos = index(line, "{")
|
|
if (pos > 0) {
|
|
labels = substr(line, 1, pos - 1)
|
|
if (trim(labels) != "" && labels !~ /^[[:space:]]*\(/ && has_host(labels)) {
|
|
in_target = 1
|
|
target_depth = brace_delta(line)
|
|
removed = 1
|
|
next
|
|
}
|
|
}
|
|
}
|
|
|
|
print line
|
|
depth += brace_delta(line)
|
|
}
|
|
|
|
END {
|
|
if (!removed) {
|
|
exit 1
|
|
}
|
|
}
|
|
' "$infile" > "$outfile"
|
|
}
|
|
|
|
upsert_site_block_by_host() {
|
|
local infile="$1" entry="$2" outfile="$3"
|
|
local host upstream streaming body_limit skip_verify
|
|
IFS='|' read -r host upstream streaming body_limit skip_verify <<< "$entry"
|
|
|
|
local tmp_new_block tmp_old_block tmp_without_old tmp_combined
|
|
tmp_new_block=$(mktemp)
|
|
tmp_old_block=$(mktemp)
|
|
tmp_without_old=$(mktemp)
|
|
tmp_combined=$(mktemp)
|
|
|
|
: > "$tmp_new_block"
|
|
emit_site_block_standalone "$tmp_new_block" "$host" "$upstream" "$streaming" "$body_limit" "$skip_verify"
|
|
|
|
if caddy_block_extract_for_host "$infile" "$host" "$tmp_old_block"; then
|
|
log_info "Domain '${host}' already exists; replacing existing site block"
|
|
log_info "Previous block for '${host}':"
|
|
sed 's/^/ | /' "$tmp_old_block" >&2
|
|
caddy_block_remove_for_host "$infile" "$host" "$tmp_without_old"
|
|
cat "$tmp_without_old" "$tmp_new_block" > "$tmp_combined"
|
|
else
|
|
log_info "Domain '${host}' not present; adding new site block"
|
|
cat "$infile" "$tmp_new_block" > "$tmp_combined"
|
|
fi
|
|
|
|
mv "$tmp_combined" "$outfile"
|
|
rm -f "$tmp_new_block" "$tmp_old_block" "$tmp_without_old"
|
|
}
|
|
|
|
build_caddyfile() {
|
|
local outfile="$1"
|
|
local entry host upstream streaming body_limit skip_verify
|
|
|
|
: > "$outfile"
|
|
{
|
|
echo "# Generated by phase7_5_nginx_to_caddy.sh"
|
|
echo "# Mode: ${MODE}"
|
|
echo
|
|
echo "{"
|
|
if [[ "$TLS_MODE" == "cloudflare" ]]; then
|
|
echo " acme_dns cloudflare {env.CF_API_TOKEN}"
|
|
fi
|
|
echo " servers {"
|
|
echo " trusted_proxies static private_ranges"
|
|
echo " protocols h1 h2 h3"
|
|
echo " }"
|
|
echo "}"
|
|
echo
|
|
echo "(common_security) {"
|
|
echo " encode zstd gzip"
|
|
echo " header {"
|
|
echo " Strict-Transport-Security \"max-age=31536000; includeSubDomains; preload\""
|
|
echo " X-Content-Type-Options \"nosniff\""
|
|
echo " X-Frame-Options \"SAMEORIGIN\""
|
|
echo " Referrer-Policy \"strict-origin-when-cross-origin\""
|
|
echo " -Server"
|
|
echo " }"
|
|
echo "}"
|
|
echo
|
|
echo "(proxy_headers) {"
|
|
echo " header_up Host {host}"
|
|
echo " header_up X-Real-IP {remote_host}"
|
|
echo "}"
|
|
echo
|
|
echo "(proxy_streaming) {"
|
|
echo " import proxy_headers"
|
|
echo " flush_interval -1"
|
|
echo "}"
|
|
echo
|
|
} >> "$outfile"
|
|
|
|
for entry in "${SELECTED_HOST_MAP[@]}"; do
|
|
IFS='|' read -r host upstream streaming body_limit skip_verify <<< "$entry"
|
|
emit_site_block "$outfile" "$host" "$upstream" "$streaming" "$body_limit" "$skip_verify"
|
|
done
|
|
}
|
|
|
|
if ! validate_backend_tls_policy; then
|
|
exit 1
|
|
fi
|
|
|
|
log_step 1 "Creating Caddy data directories on Unraid..."
|
|
ssh_exec UNRAID "mkdir -p '${CADDY_DATA_PATH}/data' '${CADDY_DATA_PATH}/config'"
|
|
log_success "Caddy data directories ensured"
|
|
|
|
log_step 2 "Deploying Caddy docker-compose on Unraid..."
|
|
if ! ssh_exec UNRAID "docker network inspect '${UNRAID_DOCKER_NETWORK_NAME}'" &>/dev/null; then
|
|
log_error "Required Docker network '${UNRAID_DOCKER_NETWORK_NAME}' not found on Unraid"
|
|
exit 1
|
|
fi
|
|
ssh_exec UNRAID "mkdir -p '${CADDY_COMPOSE_DIR}'"
|
|
|
|
TMP_COMPOSE=$(mktemp)
|
|
CADDY_CONTAINER_IP="${UNRAID_CADDY_IP}"
|
|
GITEA_NETWORK_NAME="${UNRAID_DOCKER_NETWORK_NAME}"
|
|
export CADDY_CONTAINER_IP CADDY_DATA_PATH GITEA_NETWORK_NAME
|
|
|
|
if [[ "$TLS_MODE" == "cloudflare" ]]; then
|
|
CADDY_ENV_VARS=" - CF_API_TOKEN=${CLOUDFLARE_API_TOKEN}"
|
|
CADDY_EXTRA_VOLUMES=""
|
|
else
|
|
CADDY_ENV_VARS=""
|
|
CADDY_EXTRA_VOLUMES=" - ${SSL_CERT_PATH}:${SSL_CERT_PATH}:ro
|
|
- ${SSL_KEY_PATH}:${SSL_KEY_PATH}:ro"
|
|
fi
|
|
export CADDY_ENV_VARS CADDY_EXTRA_VOLUMES
|
|
|
|
render_template "${SCRIPT_DIR}/templates/docker-compose-caddy.yml.tpl" "$TMP_COMPOSE" \
|
|
"\${CADDY_DATA_PATH} \${CADDY_CONTAINER_IP} \${CADDY_ENV_VARS} \${CADDY_EXTRA_VOLUMES} \${GITEA_NETWORK_NAME}"
|
|
|
|
if [[ -z "$CADDY_ENV_VARS" ]]; then
|
|
sed -i.bak '/^[[:space:]]*environment:$/d' "$TMP_COMPOSE"
|
|
rm -f "${TMP_COMPOSE}.bak"
|
|
fi
|
|
if [[ -z "$CADDY_EXTRA_VOLUMES" ]]; then
|
|
sed -i.bak -e :a -e '/^\n*$/{$d;N;ba' -e '}' "$TMP_COMPOSE"
|
|
rm -f "${TMP_COMPOSE}.bak"
|
|
fi
|
|
|
|
scp_to UNRAID "$TMP_COMPOSE" "${CADDY_COMPOSE_DIR}/docker-compose.yml"
|
|
rm -f "$TMP_COMPOSE"
|
|
log_success "Caddy compose deployed to ${CADDY_COMPOSE_DIR}"
|
|
|
|
log_step 3 "Generating and deploying multi-domain Caddyfile..."
|
|
TMP_CADDYFILE=$(mktemp)
|
|
HAS_EXISTING_CADDYFILE=false
|
|
if ssh_exec UNRAID "test -f '${CADDY_DATA_PATH}/Caddyfile'" 2>/dev/null; then
|
|
HAS_EXISTING_CADDYFILE=true
|
|
BACKUP_PATH="${CADDY_DATA_PATH}/Caddyfile.pre_phase7_5.$(date +%Y%m%d%H%M%S)"
|
|
ssh_exec UNRAID "cp '${CADDY_DATA_PATH}/Caddyfile' '${BACKUP_PATH}'"
|
|
log_info "Backed up previous Caddyfile to ${BACKUP_PATH}"
|
|
fi
|
|
|
|
if [[ "$MODE" == "canary" && "$HAS_EXISTING_CADDYFILE" == "true" ]]; then
|
|
TMP_WORK=$(mktemp)
|
|
TMP_NEXT=$(mktemp)
|
|
cp /dev/null "$TMP_NEXT"
|
|
ssh_exec UNRAID "cat '${CADDY_DATA_PATH}/Caddyfile'" > "$TMP_WORK"
|
|
|
|
for entry in "${CANARY_HOST_MAP[@]}"; do
|
|
upsert_site_block_by_host "$TMP_WORK" "$entry" "$TMP_NEXT"
|
|
mv "$TMP_NEXT" "$TMP_WORK"
|
|
TMP_NEXT=$(mktemp)
|
|
done
|
|
|
|
cp "$TMP_WORK" "$TMP_CADDYFILE"
|
|
rm -f "$TMP_WORK" "$TMP_NEXT"
|
|
log_info "Canary mode: existing routes preserved; canary domains upserted"
|
|
else
|
|
build_caddyfile "$TMP_CADDYFILE"
|
|
fi
|
|
|
|
scp_to UNRAID "$TMP_CADDYFILE" "${CADDY_DATA_PATH}/Caddyfile"
|
|
rm -f "$TMP_CADDYFILE"
|
|
log_success "Caddyfile deployed"
|
|
|
|
log_step 4 "Starting/reloading Caddy container..."
|
|
ssh_exec UNRAID "cd '${CADDY_COMPOSE_DIR}' && docker compose up -d 2>/dev/null || docker-compose up -d"
|
|
if ! ssh_exec UNRAID "docker exec caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile" &>/dev/null; then
|
|
log_warn "Hot reload failed; restarting caddy container"
|
|
ssh_exec UNRAID "docker restart caddy" >/dev/null
|
|
fi
|
|
log_success "Caddy container is running with new config"
|
|
|
|
probe_http_code_ok() {
|
|
local code="$1" role="$2"
|
|
if [[ "$role" == "gitea_api" ]]; then
|
|
[[ "$code" == "200" ]]
|
|
return
|
|
fi
|
|
[[ "$code" =~ ^(2|3)[0-9][0-9]$ || "$code" == "401" || "$code" == "403" ]]
|
|
}
|
|
|
|
probe_host_via_caddy() {
|
|
local host="$1" upstream="$2" role="$3"
|
|
local max_attempts="${4:-5}" wait_secs="${5:-5}"
|
|
local path="/"
|
|
if [[ "$role" == "gitea_api" ]]; then
|
|
path="/api/v1/version"
|
|
fi
|
|
|
|
local tmp_body http_code attempt
|
|
tmp_body=$(mktemp)
|
|
|
|
for (( attempt=1; attempt<=max_attempts; attempt++ )); do
|
|
http_code=$(curl -sk --resolve "${host}:443:${UNRAID_CADDY_IP}" \
|
|
-o "$tmp_body" -w "%{http_code}" "https://${host}${path}" 2>/dev/null) || true
|
|
[[ -z "$http_code" ]] && http_code="000"
|
|
|
|
if probe_http_code_ok "$http_code" "$role"; then
|
|
log_success "Probe passed: ${host} (HTTP ${http_code})"
|
|
rm -f "$tmp_body"
|
|
return 0
|
|
fi
|
|
|
|
if [[ $attempt -lt $max_attempts ]]; then
|
|
log_info "Probe attempt ${attempt}/${max_attempts} for ${host} (HTTP ${http_code}) — retrying in ${wait_secs}s..."
|
|
sleep "$wait_secs"
|
|
fi
|
|
done
|
|
|
|
log_error "Probe failed: ${host} (HTTP ${http_code}) after ${max_attempts} attempts"
|
|
if [[ "$http_code" == "502" || "$http_code" == "503" || "$http_code" == "504" || "$http_code" == "000" ]]; then
|
|
local upstream_probe_raw upstream_code
|
|
upstream_probe_raw=$(ssh_exec UNRAID "curl -sk -o /dev/null -w '%{http_code}' '${upstream}' || true" 2>/dev/null || true)
|
|
upstream_code=$(printf '%s' "$upstream_probe_raw" | tr -cd '0-9')
|
|
if [[ -z "$upstream_code" ]]; then
|
|
upstream_code="000"
|
|
elif [[ ${#upstream_code} -gt 3 ]]; then
|
|
upstream_code="${upstream_code:$((${#upstream_code} - 3))}"
|
|
fi
|
|
log_warn "Upstream check from Unraid: ${upstream} -> HTTP ${upstream_code}"
|
|
fi
|
|
rm -f "$tmp_body"
|
|
return 1
|
|
}
|
|
|
|
if [[ "$MODE" == "canary" ]]; then
|
|
if confirm_action "Run canary HTTPS probe for tower.sintheus.com via Caddy IP now? [y/N] "; then
|
|
if ! probe_host_via_caddy "tower.sintheus.com" "https://192.168.1.82:443" "generic"; then
|
|
log_error "Canary probe failed for tower.sintheus.com via ${UNRAID_CADDY_IP}"
|
|
exit 1
|
|
fi
|
|
fi
|
|
else
|
|
log_step 5 "Probing all configured hosts via Caddy IP..."
|
|
PROBE_FAILS=0
|
|
for entry in "${SELECTED_HOST_MAP[@]}"; do
|
|
IFS='|' read -r host upstream _ <<< "$entry"
|
|
role="generic"
|
|
if [[ "$host" == "$GITEA_DOMAIN" ]]; then
|
|
role="gitea_api"
|
|
fi
|
|
if ! probe_host_via_caddy "$host" "$upstream" "$role"; then
|
|
PROBE_FAILS=$((PROBE_FAILS + 1))
|
|
fi
|
|
done
|
|
if [[ "$PROBE_FAILS" -gt 0 ]]; then
|
|
log_error "One or more probes failed (${PROBE_FAILS})"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
printf '\n'
|
|
log_success "Phase 7.5 complete (${MODE} mode)"
|
|
log_info "Next (no DNS change required): verify via curl --resolve and browser checks"
|
|
log_info "LAN-only routing option: split-DNS/hosts override to ${UNRAID_CADDY_IP}"
|
|
log_info "Public routing option: point public DNS to WAN ingress (not 192.168.x.x) and forward 443 to Caddy"
|
|
if [[ "$MODE" == "canary" ]]; then
|
|
log_info "Canary host is tower.sintheus.com; existing routes were preserved"
|
|
else
|
|
log_info "Full host map is now active in Caddy"
|
|
fi
|