#!/usr/bin/env bash set -euo pipefail # init_cnpg_backup.sh # Purpose: # - Configure CloudNative-PG to backup to Garage (S3-compatible) via Barman Cloud Plugin # - Create initial backup # Initialize SCRIPT_DIR SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) # Load environment and config via prole_cfg.sh # shellcheck disable=SC1090 source "$SCRIPT_DIR/prole_cfg.sh" if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then prole_set_mode "${2:-}" shift 2 elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then prole_set_mode "${1#*=}" shift fi ACTION=${1:-start} NAMESPACE="${PROLE_NAMESPACE}" CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-prole-db} 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:-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.$GARAGE_NAMESPACE.svc.cluster.local:3900} GARAGE_S3_REGION=${GARAGE_S3_REGION:-garage} RUN_FIRST_BACKUP=${RUN_FIRST_BACKUP:-1} RETENTION_POLICY=${RETENTION_POLICY:-30d} BARMAN_PLUGIN_NAME=${BARMAN_PLUGIN_NAME:-barman-cloud.cloudnative-pg.io} BARMAN_OBJECT_NAME=${BARMAN_OBJECT_NAME:-prole-db-barman-objectstore} BACKUP_STATUS_TIMEOUT=${BACKUP_STATUS_TIMEOUT:-600} BACKUP_STATUS_INTERVAL=${BACKUP_STATUS_INTERVAL:-10} PLUGIN_READY_TIMEOUT=${PLUGIN_READY_TIMEOUT:-180} 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 } barman_crd_ready() { kubectl get crd objectstores.barmancloud.cnpg.io >/dev/null 2>&1 } get_garage_pod() { kubectl get pods -n "$GARAGE_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 '$GARAGE_NAMESPACE'." >&2 exit 1 fi kubectl exec -n "$GARAGE_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_ready() { echo "Checking Garage readiness ..." local i status_out for i in {1..30}; do if status_out=$(garage_exec status 2>/dev/null); then # If layout is applied, DataAvail should eventually show something or at least the node should be healthy. # A better check is 'layout show' to see if current layout version > 0 local layout_out version layout_out=$(garage_exec layout show 2>/dev/null | grep -v "INFO" || true) version=$(echo "$layout_out" | awk -F: '/Current cluster layout version/ {gsub(/[[:space:]]/,"",$2); print $2; exit}' || true) if [[ -n "$version" && "$version" -gt 0 ]]; then echo "Garage layout version $version is applied and ready." return 0 fi fi echo "Waiting for Garage layout to be applied... ($i/30)" sleep 5 done echo "ERROR: Garage not ready (layout not applied) after 30 attempts." >&2 exit 1 } ensure_garage_bucket_and_key() { ensure_garage_ready 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" \ --from-literal=REGION="$GARAGE_S3_REGION" \ --dry-run=client -o yaml | kubectl apply -f - } apply_barman_object_store() { if ! barman_crd_ready; then echo "ERROR: Barman Cloud plugin CRD not found (objectstores.barmancloud.cnpg.io). Install the plugin first." >&2 exit 1 fi echo "Applying Barman Cloud ObjectStore '$BARMAN_OBJECT_NAME' ..." kubectl apply -n "$NAMESPACE" -f - </dev/null || true) plugin_present=$(printf '%s\n' "$plugin_names" | tr ' ' '\n' | grep -F "$BARMAN_PLUGIN_NAME" || true) if [[ -n "$plugin_present" ]]; then return 0 fi local plugins_json plugins_json=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o jsonpath='{.spec.plugins}' 2>/dev/null || true) echo "Configuring CNPG to use Barman Cloud plugin '$BARMAN_PLUGIN_NAME' ..." if [[ -z "$plugins_json" || "$plugins_json" == "null" || "$plugins_json" == "[]" ]]; then kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type merge -p "{ \"spec\": { \"plugins\": [ { \"name\": \"$BARMAN_PLUGIN_NAME\", \"isWALArchiver\": true, \"parameters\": {\"barmanObjectName\": \"$BARMAN_OBJECT_NAME\"} } ] } }" else kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type json -p "[ { \"op\": \"add\", \"path\": \"/spec/plugins/-\", \"value\": { \"name\": \"$BARMAN_PLUGIN_NAME\", \"isWALArchiver\": true, \"parameters\": {\"barmanObjectName\": \"$BARMAN_OBJECT_NAME\"} } } ]" fi } remove_native_barman_config() { kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type merge -p "{\"spec\":{\"backup\":null}}" >/dev/null 2>&1 || true } wait_for_plugin_ready() { local start_time now plugin_names deployment_rows ns ready replicas start_time=$(date +%s) while true; do plugin_names=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o jsonpath='{.spec.plugins[*].name}' 2>/dev/null || true) if printf '%s\n' "$plugin_names" | tr ' ' '\n' | grep -Fxq "$BARMAN_PLUGIN_NAME"; then deployment_rows=$(kubectl get deployment -A -l app.kubernetes.io/name=barman-cloud -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.status.readyReplicas}{"\t"}{.status.replicas}{"\n"}{end}' 2>/dev/null || true) if [[ -z "$deployment_rows" ]]; then deployment_rows=$(kubectl get deployment -A -o jsonpath='{range .items[?(@.metadata.name=="barman-cloud")]}{.metadata.namespace}{"\t"}{.status.readyReplicas}{"\t"}{.status.replicas}{"\n"}{end}' 2>/dev/null || true) fi while IFS=$'\t' read -r ns ready replicas; do [[ -z "$ns" ]] && continue ready=${ready:-0} replicas=${replicas:-0} if (( ready >= 1 && replicas >= 1 )); then return 0 fi done <<< "$deployment_rows" fi now=$(date +%s) if (( now - start_time >= PLUGIN_READY_TIMEOUT )); then echo "ERROR: Timed out waiting for CNPG plugin '$BARMAN_PLUGIN_NAME' to become available." >&2 return 1 fi echo "Waiting for CNPG plugin '$BARMAN_PLUGIN_NAME' to become available ..." sleep 5 done } has_successful_base_backup() { local rows name phase method backup_type phase_lc method_lc backup_type_lc rows=$(kubectl get backup -n "$NAMESPACE" -o jsonpath="{range .items[?(@.spec.cluster.name=='$CNPG_CLUSTER_NAME')]}{.metadata.name}{\"\t\"}{.status.phase}{\"\t\"}{.spec.method}{\"\t\"}{.spec.pluginConfiguration.parameters.backupType}{\"\n\"}{end}" 2>/dev/null || true) if [[ -z "$rows" ]]; then return 1 fi while IFS=$'\t' read -r name phase method backup_type; do [[ -z "$name" ]] && continue phase_lc=$(echo "${phase:-}" | tr '[:upper:]' '[:lower:]') method_lc=$(echo "${method:-}" | tr '[:upper:]' '[:lower:]') backup_type_lc=$(echo "${backup_type:-}" | tr '[:upper:]' '[:lower:]') if [[ "$phase_lc" != "completed" && "$phase_lc" != "succeeded" ]]; then continue fi if [[ "$method_lc" == "plugin" ]] && [[ "$backup_type_lc" == "incremental" || "$backup_type_lc" == "incr" ]]; then continue fi return 0 done <<< "$rows" return 1 } wait_for_successful_base_backup() { local start_time now elapsed start_time=$(date +%s) while true; do if has_successful_base_backup; then return 0 fi now=$(date +%s) elapsed=$((now - start_time)) if (( elapsed >= BACKUP_STATUS_TIMEOUT )); then return 1 fi echo "Waiting for a successful base backup ... (${elapsed}s/${BACKUP_STATUS_TIMEOUT}s)" sleep "$BACKUP_STATUS_INTERVAL" done } trigger_backup() { local backup_type="${1:-full}" local backup_name backup_name="${CNPG_CLUSTER_NAME}-backup-$(date +%Y%m%d%H%M%S)" echo "Triggering ${backup_type} backup $backup_name ..." if [[ "$backup_type" == "incremental" || "$backup_type" == "incr" ]]; then kubectl apply -n "$NAMESPACE" -f - <&2 return 1 } case "$ACTION" in start) ensure_tools ensure_namespace ensure_cluster ensure_garage_bucket_and_key apply_barman_object_store ensure_barman_plugin_config wait_for_plugin_ready remove_native_barman_config if [[ "$RUN_FIRST_BACKUP" == "1" ]]; then trigger_backup fi if ! wait_for_successful_base_backup; then echo "ERROR: Timed out waiting for a successful base backup." >&2 status || true exit 1 fi status ;; backup) ensure_tools ensure_namespace ensure_cluster trigger_backup "${2:-full}" ;; status) status ;; *) usage ;; esac