provisioning-catalog/components/rev_proxy/cluster/rev_proxy-lib.sh

286 lines
8.9 KiB
Bash
Raw Permalink Normal View History

#!/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}=<token> 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 - <<EOF
apiVersion: longhorn.io/v1beta2
kind: RecurringJob
metadata:
name: ${RVPRXY_NAME}-snap-daily
namespace: longhorn-system
labels:
app.kubernetes.io/managed-by: provisioning
app.kubernetes.io/component: ${RVPRXY_NAME}
spec:
cron: "0 3 * * *"
task: snapshot
retain: 7
concurrency: 1
labels:
backup.provisioning.io/component: ${RVPRXY_NAME}
EOF
# Attach the RecurringJob to the PV backing this PVC.
local pv_name
pv_name=$(_kubectl get pvc "$pvc" \
--namespace "$RVPRXY_NAMESPACE" \
-o jsonpath='{.spec.volumeName}' 2>/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
}