mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 12:03:59 +00:00
391 lines
11 KiB
Bash
Executable File
391 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
# init_knoe-db-reset.sh
|
|
# Purpose:
|
|
# - Orderly shutdown of knoe-db with mandatory backup/export
|
|
# - Reset namespace resources
|
|
# - Recreate knoe-db from backup if available, else fresh init
|
|
#
|
|
# Usage:
|
|
# ./init_knoe-db-reset.sh reset # shutdown + reset + recreate
|
|
# ./init_knoe-db-reset.sh shutdown # orderly shutdown only
|
|
# ./init_knoe-db-reset.sh recreate # recreate from backup if available
|
|
|
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
|
|
# Load environment and config via knoe_cfg.sh
|
|
# shellcheck disable=SC1090
|
|
source "$SCRIPT_DIR/knoe_cfg.sh"
|
|
|
|
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
|
|
knoe_set_mode "${2:-}"
|
|
shift 2
|
|
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
|
|
knoe_set_mode "${1#*=}"
|
|
shift
|
|
fi
|
|
|
|
ACTION=${1:-reset}
|
|
|
|
NAMESPACE="${PROLE_NAMESPACE}"
|
|
CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-knoe-db}
|
|
KNOE_HOME=${KNOE_HOME:-$(cd "$SCRIPT_DIR/.." && pwd)}
|
|
|
|
ADMIN_USER=${KNOE_DB_ADMIN_USER:-postgres}
|
|
ADMIN_DB=${KNOE_DB_ADMIN_DB:-postgres}
|
|
|
|
GARAGE_NAME=${GARAGE_NAME:-garage}
|
|
SERVICE_NAMESPACE=${SERVICE_NAMESPACE:-}
|
|
if [[ -z "${GARAGE_NAMESPACE:-}" ]]; then
|
|
if [[ -n "$SERVICE_NAMESPACE" ]]; then
|
|
GARAGE_NAMESPACE="$SERVICE_NAMESPACE"
|
|
else
|
|
GARAGE_NAMESPACE="$NAMESPACE"
|
|
fi
|
|
fi
|
|
GARAGE_BACKUP_BUCKET=${GARAGE_BACKUP_BUCKET:-knoe-db-backups}
|
|
GARAGE_BACKUP_SECRET_NAME=${GARAGE_BACKUP_SECRET_NAME:-knoe-db-barman-s3}
|
|
GARAGE_S3_ENDPOINT=${GARAGE_S3_ENDPOINT:-http://$GARAGE_NAME.$GARAGE_NAMESPACE.svc.cluster.local:3900}
|
|
|
|
BACKUP_DIR=${BACKUP_DIR:-$KNOE_HOME/knoe/backup}
|
|
BACKUP_WAIT_TIMEOUT=${BACKUP_WAIT_TIMEOUT:-1800}
|
|
EXPORT_WAIT_TIMEOUT=${EXPORT_WAIT_TIMEOUT:-1800}
|
|
ALLOW_NO_BACKUP=${ALLOW_NO_BACKUP:-0}
|
|
|
|
RECOVERY_TEMPLATE="$SCRIPT_DIR/../k8s/knoe/knoe-db-recovery.yaml.tpl"
|
|
|
|
log() { printf '%s\n' "$*"; }
|
|
err() { printf '%s\n' "$*" >&2; }
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: $0 {reset|shutdown|recreate}
|
|
|
|
Actions:
|
|
reset Orderly shutdown + reset namespace + recreate (default)
|
|
shutdown Orderly shutdown only (backup + export + reset namespace)
|
|
recreate Recreate from backup if available, else fresh init
|
|
|
|
Env overrides:
|
|
BACKUP_DIR, BACKUP_WAIT_TIMEOUT, EXPORT_WAIT_TIMEOUT, ALLOW_NO_BACKUP
|
|
KNOE_DB_ADMIN_USER, KNOE_DB_ADMIN_DB
|
|
GARAGE_BACKUP_BUCKET, GARAGE_BACKUP_SECRET_NAME, GARAGE_S3_ENDPOINT
|
|
EOF
|
|
}
|
|
|
|
ensure_tools() {
|
|
for t in kubectl sed awk; do
|
|
command -v "$t" >/dev/null || { err "Missing required tool: $t"; exit 1; }
|
|
done
|
|
}
|
|
|
|
cluster_exists() {
|
|
kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" >/dev/null 2>&1
|
|
}
|
|
|
|
get_primary_pod() {
|
|
local primary
|
|
primary=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o jsonpath='{.status.currentPrimary}' 2>/dev/null || true)
|
|
if [[ -z "$primary" ]]; then
|
|
primary=$(kubectl -n "$NAMESPACE" get pods -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
|
|
fi
|
|
printf '%s' "$primary"
|
|
}
|
|
|
|
|
|
terminate_non_admin_connections() {
|
|
local pod
|
|
pod=$(get_primary_pod)
|
|
if [[ -z "$pod" ]]; then
|
|
log "No primary CNPG pod detected; skipping connection cleanup."
|
|
return 0
|
|
fi
|
|
|
|
log "Terminating non-admin database connections (admin=$ADMIN_USER) ..."
|
|
kubectl -n "$NAMESPACE" exec "$pod" -c postgres -- \
|
|
psql -U "$ADMIN_USER" -d "$ADMIN_DB" -v ON_ERROR_STOP=1 -c \
|
|
"SELECT pg_terminate_backend(pid)
|
|
FROM pg_stat_activity
|
|
WHERE usename <> '${ADMIN_USER}'
|
|
AND pid <> pg_backend_pid();" || true
|
|
}
|
|
|
|
latest_backup_name() {
|
|
kubectl -n "$NAMESPACE" get backup \
|
|
--sort-by=.metadata.creationTimestamp \
|
|
-o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null | tail -n 1
|
|
}
|
|
|
|
latest_completed_backup_name() {
|
|
kubectl -n "$NAMESPACE" get backup \
|
|
--sort-by=.metadata.creationTimestamp \
|
|
-o jsonpath='{range .items[*]}{.metadata.name}{"|"}{.status.phase}{"\n"}{end}' 2>/dev/null | \
|
|
awk -F'|' '{p=tolower($2); if (p=="completed" || p=="succeeded") {name=$1}} END {print name}'
|
|
}
|
|
|
|
wait_for_backup() {
|
|
local backup_name="$1"
|
|
local start_time now phase phase_lc
|
|
start_time=$(date +%s)
|
|
|
|
while true; do
|
|
phase=$(kubectl -n "$NAMESPACE" get backup "$backup_name" -o jsonpath='{.status.phase}' 2>/dev/null || true)
|
|
phase_lc=$(printf '%s' "$phase" | tr '[:upper:]' '[:lower:]')
|
|
|
|
case "$phase_lc" in
|
|
completed|succeeded)
|
|
log "Backup $backup_name completed."
|
|
return 0
|
|
;;
|
|
failed|error)
|
|
err "Backup $backup_name failed (phase=$phase)."
|
|
return 1
|
|
;;
|
|
esac
|
|
|
|
now=$(date +%s)
|
|
if (( now - start_time > BACKUP_WAIT_TIMEOUT )); then
|
|
err "Timed out waiting for backup $backup_name."
|
|
return 1
|
|
fi
|
|
|
|
log "Waiting for backup $backup_name to complete (phase=${phase:-unknown}) ..."
|
|
sleep 10
|
|
done
|
|
}
|
|
|
|
run_barman_backup() {
|
|
if ! cluster_exists; then
|
|
err "Cluster '$CNPG_CLUSTER_NAME' not found; cannot run backup."
|
|
return 1
|
|
fi
|
|
|
|
if [[ -x "$SCRIPT_DIR/init_cnpg_backup.sh" ]]; then
|
|
log "Running barman backup (Garage bucket: $GARAGE_BACKUP_BUCKET) ..."
|
|
NAMESPACE="$NAMESPACE" \
|
|
GARAGE_BACKUP_BUCKET="$GARAGE_BACKUP_BUCKET" \
|
|
GARAGE_BACKUP_SECRET_NAME="$GARAGE_BACKUP_SECRET_NAME" \
|
|
GARAGE_S3_ENDPOINT="$GARAGE_S3_ENDPOINT" \
|
|
RUN_FIRST_BACKUP=1 "$SCRIPT_DIR/init_cnpg_backup.sh" start
|
|
else
|
|
err "init_cnpg_backup.sh not found; cannot run backup."
|
|
return 1
|
|
fi
|
|
|
|
sleep 2
|
|
local backup_name
|
|
backup_name=$(latest_backup_name)
|
|
if [[ -z "$backup_name" ]]; then
|
|
err "No backup resource detected after triggering backup."
|
|
return 1
|
|
fi
|
|
|
|
wait_for_backup "$backup_name"
|
|
}
|
|
|
|
export_backup_bucket() {
|
|
if [[ -z "$GARAGE_BACKUP_BUCKET" ]]; then
|
|
err "GARAGE_BACKUP_BUCKET is not set; cannot export backup."
|
|
return 1
|
|
fi
|
|
if ! kubectl -n "$NAMESPACE" get secret "$GARAGE_BACKUP_SECRET_NAME" >/dev/null 2>&1; then
|
|
err "Garage backup secret '$GARAGE_BACKUP_SECRET_NAME' not found; cannot export backup."
|
|
return 1
|
|
fi
|
|
|
|
local image pod_name export_subdir
|
|
image=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o jsonpath='{.spec.imageName}' 2>/dev/null || true)
|
|
image=${image:-${KNOE_DB_IMAGE:-knoe-db:latest}}
|
|
|
|
pod_name="knoe-barman-export-$(date +%s)"
|
|
export_subdir="${BACKUP_DIR}/garage-export-$(date +%Y%m%d%H%M%S)"
|
|
|
|
log "Creating export pod $pod_name using image $image ..."
|
|
cat <<EOF | kubectl apply -n "$NAMESPACE" -f -
|
|
apiVersion: v1
|
|
kind: Pod
|
|
metadata:
|
|
name: ${pod_name}
|
|
namespace: ${NAMESPACE}
|
|
spec:
|
|
restartPolicy: Never
|
|
containers:
|
|
- name: exporter
|
|
image: ${image}
|
|
command: ["sleep","3600"]
|
|
env:
|
|
- name: AWS_ACCESS_KEY_ID
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: ${GARAGE_BACKUP_SECRET_NAME}
|
|
key: ACCESS_KEY_ID
|
|
- name: AWS_SECRET_ACCESS_KEY
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: ${GARAGE_BACKUP_SECRET_NAME}
|
|
key: SECRET_ACCESS_KEY
|
|
- name: AWS_DEFAULT_REGION
|
|
value: us-east-1
|
|
- name: GARAGE_S3_ENDPOINT
|
|
value: ${GARAGE_S3_ENDPOINT}
|
|
- name: GARAGE_BACKUP_BUCKET
|
|
value: ${GARAGE_BACKUP_BUCKET}
|
|
- name: EXPORT_DIR
|
|
value: /backup
|
|
volumeMounts:
|
|
- name: backup
|
|
mountPath: /backup
|
|
volumes:
|
|
- name: backup
|
|
emptyDir: {}
|
|
EOF
|
|
trap 'kubectl -n "$NAMESPACE" delete pod "$pod_name" --ignore-not-found >/dev/null 2>&1 || true' RETURN
|
|
|
|
log "Waiting for export pod to become ready..."
|
|
if ! kubectl -n "$NAMESPACE" wait --for=condition=Ready pod/"$pod_name" --timeout="${EXPORT_WAIT_TIMEOUT}s" >/dev/null 2>&1; then
|
|
err "Export pod did not become ready."
|
|
kubectl -n "$NAMESPACE" describe pod "$pod_name" || true
|
|
return 1
|
|
fi
|
|
|
|
log "Exporting Garage bucket to pod filesystem..."
|
|
kubectl -n "$NAMESPACE" exec "$pod_name" -- sh -c 'python3 - <<PY
|
|
import os
|
|
import sys
|
|
try:
|
|
import boto3
|
|
from botocore.config import Config
|
|
except Exception as exc:
|
|
print(f"boto3 is required in the export image: {exc}", file=sys.stderr)
|
|
sys.exit(3)
|
|
|
|
endpoint = os.environ.get("GARAGE_S3_ENDPOINT")
|
|
bucket_name = os.environ.get("GARAGE_BACKUP_BUCKET")
|
|
export_dir = os.environ.get("EXPORT_DIR", "/backup")
|
|
|
|
if not endpoint or not bucket_name:
|
|
print("Missing GARAGE_S3_ENDPOINT or GARAGE_BACKUP_BUCKET", file=sys.stderr)
|
|
sys.exit(2)
|
|
|
|
os.makedirs(export_dir, exist_ok=True)
|
|
|
|
config = Config(s3={"addressing_style": "path"})
|
|
s3 = boto3.resource("s3", endpoint_url=endpoint, config=config)
|
|
bucket = s3.Bucket(bucket_name)
|
|
|
|
count = 0
|
|
for obj in bucket.objects.all():
|
|
key = obj.key
|
|
if key.endswith("/"):
|
|
continue
|
|
dest = os.path.join(export_dir, key)
|
|
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
|
bucket.download_file(key, dest)
|
|
count += 1
|
|
|
|
print(f"Downloaded {count} objects from {bucket_name} to {export_dir}")
|
|
PY'
|
|
|
|
log "Copying exported backup to $export_subdir ..."
|
|
mkdir -p "$export_subdir"
|
|
kubectl -n "$NAMESPACE" cp "$pod_name":/backup "$export_subdir"
|
|
|
|
log "Cleaning up export pod $pod_name ..."
|
|
kubectl -n "$NAMESPACE" delete pod "$pod_name" --ignore-not-found
|
|
trap - RETURN
|
|
}
|
|
|
|
shutdown_database() {
|
|
if [[ -x "$SCRIPT_DIR/init_cloudnative_pg.sh" ]]; then
|
|
log "Stopping knoe-db cluster ..."
|
|
"$SCRIPT_DIR/init_cloudnative_pg.sh" stop || true
|
|
else
|
|
log "init_cloudnative_pg.sh not found; deleting cluster resource directly."
|
|
kubectl -n "$NAMESPACE" delete cluster "$CNPG_CLUSTER_NAME" --ignore-not-found
|
|
fi
|
|
}
|
|
|
|
reset_namespace() {
|
|
if [[ -x "$SCRIPT_DIR/../scripts/reset-ns.sh" ]]; then
|
|
log "Resetting namespace $NAMESPACE ..."
|
|
"$SCRIPT_DIR/../scripts/reset-ns.sh" -n "$NAMESPACE"
|
|
else
|
|
err "scripts/reset-ns.sh not found."
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
recreate_from_backup() {
|
|
local backup_name manifest_tmp
|
|
backup_name=$(latest_completed_backup_name)
|
|
if [[ -z "$backup_name" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
if [[ ! -f "$RECOVERY_TEMPLATE" ]]; then
|
|
err "Recovery template not found: $RECOVERY_TEMPLATE"
|
|
return 1
|
|
fi
|
|
|
|
manifest_tmp="/tmp/knoe-db-recovery-$$.yaml"
|
|
sed "s/{{BACKUP_NAME}}/${backup_name}/g" "$RECOVERY_TEMPLATE" > "$manifest_tmp"
|
|
|
|
log "Recreating knoe-db from backup $backup_name ..."
|
|
CNPG_MANIFEST_OVERRIDE="$manifest_tmp" "$SCRIPT_DIR/init_cloudnative_pg.sh" initialize
|
|
|
|
rm -f "$manifest_tmp"
|
|
}
|
|
|
|
recreate_fresh() {
|
|
log "Recreating knoe-db from scratch ..."
|
|
"$SCRIPT_DIR/init_cloudnative_pg.sh" initialize
|
|
}
|
|
|
|
shutdown() {
|
|
ensure_tools
|
|
terminate_non_admin_connections
|
|
|
|
if run_barman_backup; then
|
|
export_backup_bucket
|
|
else
|
|
if [[ "$ALLOW_NO_BACKUP" == "1" ]]; then
|
|
err "Backup failed, but ALLOW_NO_BACKUP=1 set; proceeding without export."
|
|
else
|
|
err "Backup failed and ALLOW_NO_BACKUP is not set; aborting shutdown."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
shutdown_database
|
|
reset_namespace
|
|
}
|
|
|
|
recreate() {
|
|
ensure_tools
|
|
if recreate_from_backup; then
|
|
log "Recovery initiated from existing backup."
|
|
else
|
|
log "No completed backups found; starting fresh."
|
|
recreate_fresh
|
|
fi
|
|
}
|
|
|
|
case "$ACTION" in
|
|
shutdown)
|
|
shutdown
|
|
;;
|
|
recreate)
|
|
recreate
|
|
;;
|
|
reset)
|
|
shutdown
|
|
recreate
|
|
;;
|
|
*)
|
|
usage
|
|
exit 2
|
|
;;
|
|
esac
|