#!/usr/bin/env bash set -euo pipefail # init_prole-db-backup.sh # Purpose: # - Configure CloudNative-PG to backup to Garage (S3-compatible) # - Create initial backup # Initialize SCRIPT_DIR SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) ACTION=${1:-start} # Load env if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then set -- # shellcheck disable=SC1090 source "$PROLE_HOME/env.sh" elif [[ -f "$HOME/.prole/env.sh" ]]; then set -- # shellcheck disable=SC1090 source "$HOME/.prole/env.sh" fi NAMESPACE=${NAMESPACE:-default} CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-prole-db} GARAGE_NAME=${GARAGE_NAME:-garage} GARAGE_BACKUP_BUCKET=${GARAGE_BACKUP_BUCKET:-prole-db-backups} GARAGE_BACKUP_KEY_NAME=${GARAGE_BACKUP_KEY_NAME:-prole-db-backup} GARAGE_BACKUP_SECRET_NAME=${GARAGE_BACKUP_SECRET_NAME:-prole-db-barman-s3} GARAGE_S3_ENDPOINT=${GARAGE_S3_ENDPOINT:-http://$GARAGE_NAME.$NAMESPACE.svc.cluster.local:3900} RUN_FIRST_BACKUP=${RUN_FIRST_BACKUP:-1} usage() { cat </dev/null || { echo "Missing required tool: $t" >&2; exit 1; } done } ensure_namespace() { if ! kubectl get namespace "$NAMESPACE" >/dev/null 2>&1; then echo "Creating namespace '$NAMESPACE' ..." kubectl create namespace "$NAMESPACE" >/dev/null 2>&1 || true fi } ensure_cluster() { if ! kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" >/dev/null 2>&1; then echo "ERROR: CNPG cluster '$CNPG_CLUSTER_NAME' not found in namespace '$NAMESPACE'." >&2 exit 1 fi } get_garage_pod() { kubectl get pods -n "$NAMESPACE" -l "app=$GARAGE_NAME" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true } garage_exec() { local pod pod=$(get_garage_pod) if [[ -z "$pod" ]]; then echo "ERROR: Garage pod not found in namespace '$NAMESPACE'." >&2 exit 1 fi kubectl exec -n "$NAMESPACE" "$pod" -- /garage "$@" } parse_key_output() { local output="$1" local access_key secret_key access_key=$(echo "$output" | sed -nE 's/^(Access key ID|Key ID):[[:space:]]+//p' | head -n1) secret_key=$(echo "$output" | sed -nE 's/^(Secret access key|Secret key):[[:space:]]+//p' | head -n1) if [[ -z "$access_key" || -z "$secret_key" ]]; then return 1 fi printf "%s\n%s" "$access_key" "$secret_key" } ensure_garage_bucket_and_key() { echo "Ensuring Garage bucket and access key for backups ..." local key_info parsed access_key secret_key if key_info=$(garage_exec key info --show-secret "$GARAGE_BACKUP_KEY_NAME" 2>/dev/null); then : else key_info=$(garage_exec key create "$GARAGE_BACKUP_KEY_NAME") fi if ! parsed=$(parse_key_output "$key_info"); then echo "ERROR: Unable to parse Garage key output." >&2 echo "$key_info" >&2 exit 1 fi access_key=$(echo "$parsed" | sed -n '1p') secret_key=$(echo "$parsed" | sed -n '2p') if ! garage_exec bucket info "$GARAGE_BACKUP_BUCKET" >/dev/null 2>&1; then garage_exec bucket create "$GARAGE_BACKUP_BUCKET" fi garage_exec bucket allow --read --write --owner --key "$GARAGE_BACKUP_KEY_NAME" "$GARAGE_BACKUP_BUCKET" || true echo "Creating/updating Kubernetes secret '$GARAGE_BACKUP_SECRET_NAME' ..." kubectl create secret generic "$GARAGE_BACKUP_SECRET_NAME" -n "$NAMESPACE" \ --from-literal=ACCESS_KEY_ID="$access_key" \ --from-literal=SECRET_ACCESS_KEY="$secret_key" \ --dry-run=client -o yaml | kubectl apply -f - } configure_cnpg_backup() { echo "Configuring CNPG backup to use Garage bucket '$GARAGE_BACKUP_BUCKET' ..." kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type merge -p "{ \"spec\": { \"backup\": { \"barmanObjectStore\": { \"destinationPath\": \"s3://$GARAGE_BACKUP_BUCKET/\", \"endpointURL\": \"$GARAGE_S3_ENDPOINT\", \"s3Credentials\": { \"accessKeyId\": {\"name\": \"$GARAGE_BACKUP_SECRET_NAME\", \"key\": \"ACCESS_KEY_ID\"}, \"secretAccessKey\": {\"name\": \"$GARAGE_BACKUP_SECRET_NAME\", \"key\": \"SECRET_ACCESS_KEY\"} }, \"wal\": {\"compression\": \"gzip\"}, \"data\": {\"compression\": \"gzip\"} }, \"retentionPolicy\": \"30d\" } } }" } trigger_backup() { local backup_name backup_name="${CNPG_CLUSTER_NAME}-backup-$(date +%Y%m%d%H%M%S)" echo "Triggering backup $backup_name ..." kubectl apply -n "$NAMESPACE" -f - <