From 14a5773a2d21c2277c34c05c31815d8700d55f96 Mon Sep 17 00:00:00 2001 From: S Date: Mon, 2 Mar 2026 22:45:49 -0600 Subject: [PATCH] feat: add phase 8.5 Nginx to Caddy migration wrapper and enhance post-check script for direct access handling --- phase8_5_nginx_to_caddy.sh | 7 ++ phase8_cutover.sh | 141 +++++++++++++++++++++++++++++++++---- phase8_post_check.sh | 54 ++++++++++++-- 3 files changed, 180 insertions(+), 22 deletions(-) create mode 100755 phase8_5_nginx_to_caddy.sh diff --git a/phase8_5_nginx_to_caddy.sh b/phase8_5_nginx_to_caddy.sh new file mode 100755 index 0000000..a20f89f --- /dev/null +++ b/phase8_5_nginx_to_caddy.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Backward-compat wrapper: phase 8.5 was renamed to phase 7.5. +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +echo "[WARN] phase8_5_nginx_to_caddy.sh was renamed to phase7_5_nginx_to_caddy.sh" >&2 +exec "${SCRIPT_DIR}/phase7_5_nginx_to_caddy.sh" "$@" diff --git a/phase8_cutover.sh b/phase8_cutover.sh index 8f57b5a..a9d387f 100755 --- a/phase8_cutover.sh +++ b/phase8_cutover.sh @@ -42,6 +42,72 @@ PHASE8_STATE_FILE="${PHASE8_STATE_DIR}/phase8_github_repo_state.json" UNRAID_DOCKER_NETWORK_NAME="br0" # Compose files live in a centralized project directory. CADDY_COMPOSE_DIR="${UNRAID_COMPOSE_DIR}/caddy" +PHASE8_GITEA_ROUTE_BEGIN="# BEGIN_PHASE8_GITEA_ROUTE" +PHASE8_GITEA_ROUTE_END="# END_PHASE8_GITEA_ROUTE" + +wait_for_https_public() { + local host="$1" max_secs="${2:-30}" + local elapsed=0 + while [[ $elapsed -lt $max_secs ]]; do + if curl -sf -o /dev/null "https://${host}/api/v1/version" 2>/dev/null; then + return 0 + fi + sleep 2 + elapsed=$((elapsed + 2)) + done + return 1 +} + +wait_for_https_via_resolve() { + local host="$1" ip="$2" max_secs="${3:-300}" + local elapsed=0 + log_info "Waiting for HTTPS via direct Caddy path (--resolve ${host}:443:${ip})..." + while [[ $elapsed -lt $max_secs ]]; do + if curl -skf --resolve "${host}:443:${ip}" "https://${host}/api/v1/version" >/dev/null 2>&1; then + log_success "HTTPS reachable via Caddy IP (after ${elapsed}s)" + return 0 + fi + sleep 2 + elapsed=$((elapsed + 2)) + done + log_error "Timeout waiting for HTTPS via --resolve (${host} -> ${ip}) after ${max_secs}s" + if ssh_exec UNRAID "docker ps --format '{{.Names}}' | grep -qx 'caddy'" >/dev/null 2>&1; then + log_warn "Recent Caddy logs (tail 80):" + ssh_exec UNRAID "docker logs --tail 80 caddy 2>&1" || true + fi + return 1 +} + +caddyfile_has_domain_block() { + local file="$1" domain="$2" + awk -v domain="$domain" ' + function trim(s) { + sub(/^[[:space:]]+/, "", s) + sub(/[[:space:]]+$/, "", s) + return s + } + { + line = $0 + if (line ~ /^[[:space:]]*#/) next + pos = index(line, "{") + if (pos <= 0) next + + labels = trim(substr(line, 1, pos - 1)) + if (labels == "" || labels ~ /^\(/) next + + gsub(/[[:space:]]+/, "", labels) + n = split(labels, parts, ",") + for (i = 1; i <= n; i++) { + if (parts[i] == domain) { + found = 1 + } + } + } + END { + exit(found ? 0 : 1) + } + ' "$file" +} # --------------------------------------------------------------------------- # Helper: persist original GitHub repo settings for teardown symmetry @@ -145,28 +211,62 @@ fi # Step 2: Render + deploy Caddyfile # --------------------------------------------------------------------------- log_step 2 "Deploying Caddyfile..." -if ssh_exec UNRAID "test -f '${CADDY_DATA_PATH}/Caddyfile'" 2>/dev/null; then - log_info "Caddyfile already exists — skipping" -else - TMPFILE=$(mktemp) - GITEA_CONTAINER_IP="${UNRAID_GITEA_IP}" - export GITEA_CONTAINER_IP GITEA_DOMAIN CADDY_DOMAIN +GITEA_CONTAINER_IP="${UNRAID_GITEA_IP}" +export GITEA_CONTAINER_IP GITEA_DOMAIN CADDY_DOMAIN +CADDYFILE_UPDATED=0 - # Build TLS block based on TLS_MODE - if [[ "$TLS_MODE" == "cloudflare" ]]; then - TLS_BLOCK=" tls { +# Build TLS block based on TLS_MODE +if [[ "$TLS_MODE" == "cloudflare" ]]; then + TLS_BLOCK=" tls { dns cloudflare {env.CF_API_TOKEN} }" +else + TLS_BLOCK=" tls ${SSL_CERT_PATH} ${SSL_KEY_PATH}" +fi +export TLS_BLOCK + +if ssh_exec UNRAID "test -f '${CADDY_DATA_PATH}/Caddyfile'" 2>/dev/null; then + TMP_EXISTING=$(mktemp) + TMP_UPDATED=$(mktemp) + TMP_ROUTE_BLOCK=$(mktemp) + + ssh_exec UNRAID "cat '${CADDY_DATA_PATH}/Caddyfile'" > "$TMP_EXISTING" + + if caddyfile_has_domain_block "$TMP_EXISTING" "$GITEA_DOMAIN"; then + log_info "Caddyfile already has a route for ${GITEA_DOMAIN} — preserving existing file" else - TLS_BLOCK=" tls ${SSL_CERT_PATH} ${SSL_KEY_PATH}" + log_warn "Caddyfile exists but has no explicit route for ${GITEA_DOMAIN}" + log_info "Appending managed Gitea route block" + { + echo + echo "${PHASE8_GITEA_ROUTE_BEGIN}" + echo "${GITEA_DOMAIN} {" + printf '%s\n' "$TLS_BLOCK" + echo + echo " reverse_proxy ${GITEA_CONTAINER_IP}:3000" + echo "}" + echo "${PHASE8_GITEA_ROUTE_END}" + echo + } > "$TMP_ROUTE_BLOCK" + + # Remove a stale managed block (if present), then append refreshed block. + sed "/^${PHASE8_GITEA_ROUTE_BEGIN}\$/,/^${PHASE8_GITEA_ROUTE_END}\$/d" "$TMP_EXISTING" > "$TMP_UPDATED" + cat "$TMP_UPDATED" "$TMP_ROUTE_BLOCK" > "${TMP_UPDATED}.final" + scp_to UNRAID "${TMP_UPDATED}.final" "${CADDY_DATA_PATH}/Caddyfile" + log_success "Appended managed Gitea route to existing Caddyfile" + CADDYFILE_UPDATED=1 fi - export TLS_BLOCK + + rm -f "$TMP_EXISTING" "$TMP_UPDATED" "$TMP_ROUTE_BLOCK" "${TMP_UPDATED}.final" +else + TMPFILE=$(mktemp) render_template "${SCRIPT_DIR}/templates/Caddyfile.tpl" "$TMPFILE" \ "\${CADDY_DOMAIN} \${GITEA_DOMAIN} \${TLS_BLOCK} \${GITEA_CONTAINER_IP}" scp_to UNRAID "$TMPFILE" "${CADDY_DATA_PATH}/Caddyfile" rm -f "$TMPFILE" log_success "Caddyfile deployed" + CADDYFILE_UPDATED=1 fi # --------------------------------------------------------------------------- @@ -221,7 +321,14 @@ fi log_step 4 "Starting Caddy container..." CONTAINER_STATUS=$(ssh_exec UNRAID "docker ps --filter name=caddy --format '{{.Status}}'" 2>/dev/null || true) if [[ "$CONTAINER_STATUS" == *"Up"* ]]; then - log_info "Caddy container already running — skipping" + log_info "Caddy container already running" + if [[ "$CADDYFILE_UPDATED" -eq 1 ]]; then + log_info "Caddyfile changed — restarting caddy to apply updated config" + ssh_exec UNRAID "docker restart caddy >/dev/null" + log_success "Caddy container restarted with new config" + else + log_info "Caddyfile unchanged — restart not required" + fi else ssh_exec UNRAID "cd '${CADDY_COMPOSE_DIR}' && docker compose up -d 2>/dev/null || docker-compose up -d" log_success "Caddy container started" @@ -232,9 +339,13 @@ fi # Caddy auto-obtains certs — poll until HTTPS responds. # --------------------------------------------------------------------------- log_step 5 "Waiting for HTTPS (Caddy auto-provisions cert)..." -wait_for_http "https://${GITEA_DOMAIN}/api/v1/version" 120 - -log_success "HTTPS verified — https://${GITEA_DOMAIN} works" +if wait_for_https_public "${GITEA_DOMAIN}" 30; then + log_success "HTTPS verified through current domain routing — https://${GITEA_DOMAIN} works" +else + log_warn "Public-domain routing to Caddy is not ready yet" + wait_for_https_via_resolve "${GITEA_DOMAIN}" "${UNRAID_CADDY_IP}" 300 + log_success "HTTPS verified via direct Caddy path; public routing can be completed later" +fi # --------------------------------------------------------------------------- # Step 6: Mark GitHub repos as offsite backup only diff --git a/phase8_post_check.sh b/phase8_post_check.sh index b795344..5511a70 100755 --- a/phase8_post_check.sh +++ b/phase8_post_check.sh @@ -16,7 +16,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "${SCRIPT_DIR}/lib/common.sh" load_env -require_vars GITEA_DOMAIN GITEA_ADMIN_TOKEN GITEA_ORG_NAME \ +require_vars GITEA_DOMAIN UNRAID_CADDY_IP GITEA_ADMIN_TOKEN GITEA_ORG_NAME \ GITHUB_USERNAME GITHUB_TOKEN \ REPO_NAMES @@ -37,16 +37,44 @@ run_check() { fi } +ACCESS_MODE="public" +if ! curl -sf -o /dev/null "https://${GITEA_DOMAIN}/api/v1/version" 2>/dev/null; then + ACCESS_MODE="direct" + log_warn "Public routing to ${GITEA_DOMAIN} not reachable from control plane" + log_warn "Using direct Caddy-IP checks via --resolve (${UNRAID_CADDY_IP})" +else + log_info "Using public-domain checks for ${GITEA_DOMAIN}" +fi + +curl_https() { + if [[ "$ACCESS_MODE" == "direct" ]]; then + curl -sk --resolve "${GITEA_DOMAIN}:443:${UNRAID_CADDY_IP}" "$@" + else + curl -s "$@" + fi +} + +curl_http() { + if [[ "$ACCESS_MODE" == "direct" ]]; then + curl -s --resolve "${GITEA_DOMAIN}:80:${UNRAID_CADDY_IP}" "$@" + else + curl -s "$@" + fi +} + # Check 1: HTTPS works -run_check "HTTPS returns 200 at https://${GITEA_DOMAIN}" \ - curl -sf -o /dev/null "https://${GITEA_DOMAIN}/api/v1/version" +# shellcheck disable=SC2329 +check_https_version() { + curl_https -f -o /dev/null "https://${GITEA_DOMAIN}/api/v1/version" +} +run_check "HTTPS returns 200 at https://${GITEA_DOMAIN}" check_https_version # Check 2: HTTP redirects to HTTPS (returns 301) # shellcheck disable=SC2329 check_redirect() { local http_code - http_code=$(curl -sI -o /dev/null -w "%{http_code}" "http://${GITEA_DOMAIN}/") - [[ "$http_code" == "301" ]] + http_code=$(curl_http -I -o /dev/null -w "%{http_code}" "http://${GITEA_DOMAIN}/") + [[ "$http_code" == "301" || "$http_code" == "308" ]] } run_check "HTTP → HTTPS redirect (301)" check_redirect @@ -54,17 +82,29 @@ run_check "HTTP → HTTPS redirect (301)" check_redirect # shellcheck disable=SC2329 check_ssl_cert() { # Verify openssl can connect and the cert is issued by a recognized CA + local connect_target + if [[ "$ACCESS_MODE" == "direct" ]]; then + connect_target="${UNRAID_CADDY_IP}:443" + else + connect_target="${GITEA_DOMAIN}:443" + fi local issuer - issuer=$(echo | openssl s_client -connect "${GITEA_DOMAIN}:443" -servername "${GITEA_DOMAIN}" 2>/dev/null | openssl x509 -noout -issuer 2>/dev/null || echo "") + issuer=$(echo | openssl s_client -connect "${connect_target}" -servername "${GITEA_DOMAIN}" 2>/dev/null | openssl x509 -noout -issuer 2>/dev/null || echo "") # Check that the issuer is not empty (meaning cert is valid) [[ -n "$issuer" ]] } run_check "SSL certificate is valid" check_ssl_cert # Check 4: All repos accessible via HTTPS +# shellcheck disable=SC2329 +check_repo_access() { + local repo="$1" + curl_https -f -o /dev/null -H "Authorization: token ${GITEA_ADMIN_TOKEN}" \ + "https://${GITEA_DOMAIN}/api/v1/repos/${GITEA_ORG_NAME}/${repo}" +} for repo in "${REPOS[@]}"; do run_check "Repo ${repo} accessible at https://${GITEA_DOMAIN}/${GITEA_ORG_NAME}/${repo}" \ - curl -sf -o /dev/null -H "Authorization: token ${GITEA_ADMIN_TOKEN}" "https://${GITEA_DOMAIN}/api/v1/repos/${GITEA_ORG_NAME}/${repo}" + check_repo_access "$repo" done # Check 5: GitHub repos are marked as offsite backup