285 lines
8.9 KiB
Bash
Executable file
285 lines
8.9 KiB
Bash
Executable file
#!/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
|
|
}
|