#!/bin/bash # Methods library for rev_proxy — sourced by install-rev_proxy.sh # and generated run-*.sh scripts. SCRIPT_DIR must be set before sourcing. _kubectl() { if command -v kubectl >/dev/null 2>&1; then command kubectl "$@" elif command -v k0s >/dev/null 2>&1; then k0s kubectl "$@" else echo "ERROR: no kubectl or k0s binary found" >&2; exit 127 fi } _require_env() { local var="$1" if [ -z "${!var:-}" ]; then echo "ERROR: required variable $var is not set" >&2; exit 1 fi } _pod_name() { _kubectl get pod \ --namespace "$RVPRXY_NAMESPACE" \ --selector "app.kubernetes.io/name=${RVPRXY_NAME}" \ -o jsonpath='{.items[0].metadata.name}' 2>/dev/null } _wait_for_pod() { local timeout="${1:-300}" _kubectl wait pod \ --namespace "$RVPRXY_NAMESPACE" \ --selector "app.kubernetes.io/name=${RVPRXY_NAME}" \ --for=condition=Ready \ --timeout="${timeout}s" } # ── Manifest plan helpers ───────────────────────────────────────────────────── _plan_apply() { local file="$1" local path="$SCRIPT_DIR/manifests/${file}.yaml" [ -f "$path" ] || { echo " [skip] ${file}.yaml not in manifests/"; return 0; } _kubectl apply -f "$path" } _plan_apply_skip() { local file="$1" local path="$SCRIPT_DIR/manifests/${file}.yaml" [ -f "$path" ] || { echo " [skip] ${file}.yaml not in manifests/"; return 0; } if _kubectl get -f "$path" >/dev/null 2>&1; then echo " [skip] ${file} already exists in cluster" return 0 fi _kubectl apply -f "$path" } _plan_rollout_restart() { local file="$1" local path="$SCRIPT_DIR/manifests/${file}.yaml" [ -f "$path" ] || { echo " [skip] ${file}.yaml not in manifests/"; return 0; } _kubectl rollout restart -f "$path" } _plan_delete() { local file="$1" local path="$SCRIPT_DIR/manifests/${file}.yaml" [ -f "$path" ] || { echo " [skip] ${file}.yaml not in manifests/"; return 0; } _kubectl delete -f "$path" --ignore-not-found } # ── Component methods ───────────────────────────────────────────────────────── _method_create-cf-secret() { [ -f "${SCRIPT_DIR}/_credentials.env" ] && set -a && source "${SCRIPT_DIR}/_credentials.env" && set +a local secret_name="${RVPRXY_CF_SECRET_NAME:-}" if [ -z "$secret_name" ]; then echo " [skip] RVPRXY_CF_SECRET_NAME not set — skipping Cloudflare secret creation" return 0 fi local env_key="CF_TOKEN_$(echo "${secret_name}" | tr '[:lower:]' '[:upper:]' | tr '-' '_')" local cf_token="${!env_key:-}" if [ -z "$cf_token" ]; then echo " [warn] ${env_key} not set — skipping Cloudflare secret '${secret_name}'" echo " [hint] set ${env_key}= in _credentials.env" return 0 fi # cert-manager reads this secret from its own namespace, not the component namespace. _kubectl create secret generic "$secret_name" \ --namespace "cert-manager" \ --from-literal=api-token="$cf_token" \ --dry-run=client -o yaml | _kubectl apply -f - echo " [ok] Cloudflare secret '${secret_name}' in cert-manager" } _method_create-credentials() { [ -f "${SCRIPT_DIR}/_credentials.env" ] && set -a && source "${SCRIPT_DIR}/_credentials.env" && set +a local site_count="${RVPRXY_SITE_COUNT:-0}" if [ "$site_count" -eq 0 ]; then echo " [skip] no sites configured — skipping upstreams secret creation" return 0 fi local upstreams upstreams="provider: \"file\" sticky_sessions: false upstreams:" for i in $(seq 0 $((site_count - 1))); do local domain_var="RVPRXY_SITE_${i}_DOMAIN" local domain="${!domain_var:-}" [ -z "$domain" ] && continue local domain_slug="${domain//./-}" local domain_slug_upper domain_slug_upper=$(echo "$domain_slug" | tr '[:lower:]' '[:upper:]' | tr '-' '_') local path_count_var="RVPRXY_SITE_${i}_PATH_COUNT" local path_count="${!path_count_var:-1}" upstreams+=" ${domain}: paths:" for j in $(seq 0 $((path_count - 1))); do local path_var="RVPRXY_SITE_${i}_PATH_${j}_PATH" local path="${!path_var:-/}" # Path slug: strip leading/trailing slashes, replace / with _, uppercase. local path_slug path_slug=$(echo "$path" | sed 's|^/||; s|/$||; s|/|_|g' | tr '[:lower:]' '[:upper:]') [ -z "$path_slug" ] && path_slug="ROOT" local auth_type_var="RVPRXY_SITE_${i}_PATH_${j}_AUTH_TYPE" local auth_type="${!auth_type_var:-}" # SOPS key: RVPRXY_AUTH_{DOMAIN_SLUG}_{PATH_SLUG} local auth_cred_var="RVPRXY_AUTH_${domain_slug_upper}_${path_slug}" local auth_cred="${!auth_cred_var:-}" upstreams+=" \"${path}\":" if [ -n "$auth_type" ] && [ -n "$auth_cred" ]; then upstreams+=" authorization: type: \"${auth_type}\" data: \"${auth_cred}\"" fi upstreams+=" servers: - 127.0.0.1:3002" done done _kubectl create secret generic "${RVPRXY_NAME}-upstreams" \ --namespace "$RVPRXY_NAMESPACE" \ --from-literal="upstreams.yaml=${upstreams}" \ --dry-run=client -o yaml | _kubectl apply -f - echo " [ok] upstreams secret '${RVPRXY_NAME}-upstreams' created in ${RVPRXY_NAMESPACE}" } _method_delete-credentials() { _kubectl delete secret "${RVPRXY_NAME}-upstreams" \ --namespace "$RVPRXY_NAMESPACE" \ --ignore-not-found echo " [ok] upstreams secret '${RVPRXY_NAME}-upstreams' deleted from ${RVPRXY_NAMESPACE}" } _method_wait-ready() { _wait_for_pod 300 } _method_cert-status-check() { local site_count="${RVPRXY_SITE_COUNT:-0}" if [ "$site_count" -eq 0 ]; then return 0 fi local all_ready=true for i in $(seq 0 $((site_count - 1))); do local domain_var="RVPRXY_SITE_${i}_DOMAIN" local domain="${!domain_var:-}" [ -z "$domain" ] && continue local domain_slug="${domain//./-}" local cert_name="${RVPRXY_NAME}-${domain_slug}-tls" local ready ready=$(_kubectl get certificate "$cert_name" \ --namespace "$RVPRXY_NAMESPACE" \ -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null) if [ "${ready}" = "True" ]; then echo " [ok] cert ${cert_name} Ready" else echo " [warn] cert ${cert_name} not Ready (${ready:-unknown}) — restart may serve stale cert" all_ready=false fi done # Non-blocking: restart proceeds regardless; warns so operator is aware. [ "$all_ready" = "false" ] && echo " [warn] one or more certs not ready — check cert-manager events" return 0 } _method_backup-register() { local pvc="${PLAN_PARAM_PVC:-${RVPRXY_NAME}-data}" # Annotate PVC with backup policy for backup-manager pickup. _kubectl annotate pvc "$pvc" \ --namespace "$RVPRXY_NAMESPACE" \ --overwrite \ "backup.provisioning.io/policy=longhorn-daily" \ "backup.provisioning.io/component=${RVPRXY_NAME}" # Longhorn RecurringJob for daily snapshots — retained 7 days. _kubectl apply -f - </dev/null) if [ -n "$pv_name" ]; then _kubectl label volume.longhorn.io "$pv_name" \ --namespace longhorn-system \ --overwrite \ "recurring-job.longhorn.io/${RVPRXY_NAME}-snap-daily=enabled" 2>/dev/null \ && echo " [ok] backup recurring job attached to volume '${pv_name}'" \ || echo " [warn] could not label Longhorn volume — run manually" else echo " [warn] PV not yet bound for '${pvc}' — backup label skipped" fi } _method_protect-volume() { local pvc="${PLAN_PARAM_PVC:-${RVPRXY_NAME}-data}" echo " waiting for PVC '${pvc}' to bind..." _kubectl wait pvc "$pvc" \ --namespace "$RVPRXY_NAMESPACE" \ --for=jsonpath='{.status.phase}'=Bound \ --timeout=120s local pv_name pv_name=$(_kubectl get pvc "$pvc" \ --namespace "$RVPRXY_NAMESPACE" \ -o jsonpath='{.spec.volumeName}') if [ -z "$pv_name" ]; then echo " [warn] could not resolve PV name for PVC '${pvc}' — skipping volume protection" return 0 fi if command -v hcloud >/dev/null 2>&1; then hcloud volume enable-protection "$pv_name" delete 2>/dev/null \ && echo " [ok] volume '${pv_name}' protected" \ || echo " [warn] hcloud protect failed — run manually: hcloud volume enable-protection ${pv_name} delete" else echo " [warn] hcloud CLI not found — skip volume protection for '${pv_name}'" fi }