|
| 1 | +#!/bin/bash |
| 2 | +# Shared helpers for the RabbitMQ definitions backup/restore demo. |
| 3 | +# Source this file in other scripts: source "$(dirname "$0")/00-helpers.sh" |
| 4 | + |
| 5 | +export RED='\033[0;31m' |
| 6 | +export GREEN='\033[0;32m' |
| 7 | +export YELLOW='\033[1;33m' |
| 8 | +export BLUE='\033[0;34m' |
| 9 | +export MAGENTA='\033[0;35m' |
| 10 | +export CYAN='\033[0;36m' |
| 11 | +export WHITE='\033[1;37m' |
| 12 | +export NC='\033[0m' |
| 13 | +export BOLD='\033[1m' |
| 14 | + |
| 15 | +# Default settings (override via environment). |
| 16 | +export NAMESPACE="${NAMESPACE:-tenant-root}" |
| 17 | +export RABBITMQ_SRC_NAME="${RABBITMQ_SRC_NAME:-rabbitmq-src}" |
| 18 | +export RABBITMQ_TARGET_NAME="${RABBITMQ_TARGET_NAME:-rabbitmq-target}" |
| 19 | +# The rabbitmq-rd ApplicationDefinition renders the HelmRelease with |
| 20 | +# releaseName = "rabbitmq-" + appName (release.prefix in |
| 21 | +# packages/system/rabbitmq-rd/cozyrds/rabbitmq.yaml), and the chart sets the |
| 22 | +# rabbitmq.com/RabbitmqCluster metadata.name to .Release.Name, so the |
| 23 | +# operator-side CR (its Service, default-user Secret and server pods) is |
| 24 | +# rabbitmq-<app>. |
| 25 | +export RABBITMQ_SRC_CR="rabbitmq-${RABBITMQ_SRC_NAME}" |
| 26 | +export RABBITMQ_TARGET_CR="rabbitmq-${RABBITMQ_TARGET_NAME}" |
| 27 | +# cozy-default routes RabbitMQ to the shipped cozy-default-rabbitmq Job |
| 28 | +# strategy, which exports broker DEFINITIONS to the platform cozy-backups |
| 29 | +# bucket. No demo Bucket and no app backup block are needed: the strategy |
| 30 | +# carries the S3 coordinates and the controller projects cozy-backups-creds |
| 31 | +# into this namespace before each run. |
| 32 | +export BACKUPCLASS_NAME="${BACKUPCLASS_NAME:-cozy-default}" |
| 33 | +export BACKUPJOB_NAME="${BACKUPJOB_NAME:-rabbitmq-src-adhoc}" |
| 34 | +export RESTOREJOB_INPLACE_NAME="${RESTOREJOB_INPLACE_NAME:-rabbitmq-src-in-place}" |
| 35 | +export RESTOREJOB_TOCOPY_NAME="${RESTOREJOB_TOCOPY_NAME:-rabbitmq-src-to-rabbitmq-target}" |
| 36 | +export PLAN_NAME="${PLAN_NAME:-rabbitmq-src-daily}" |
| 37 | + |
| 38 | +log_info() { echo -e "${BLUE}i${NC} $*" >&2; } |
| 39 | +log_success() { echo -e "${GREEN}OK${NC} $*" >&2; } |
| 40 | +log_warning() { echo -e "${YELLOW}!${NC} $*" >&2; } |
| 41 | +log_error() { echo -e "${RED}x${NC} $*" >&2; } |
| 42 | +log_step() { echo -e "\n${MAGENTA}${BOLD}> $*${NC}" >&2; } |
| 43 | +log_substep() { echo -e "${CYAN} -> $*${NC}" >&2; } |
| 44 | + |
| 45 | +print_header() { |
| 46 | + echo -e "\n${MAGENTA}${BOLD}== $1 ==${NC}\n" >&2 |
| 47 | +} |
| 48 | + |
| 49 | +# Wait until a JSONPath value on a resource matches the desired string. |
| 50 | +# Optional 7th arg is a TERMINAL failure value: once the field reaches it the |
| 51 | +# wait returns 1 immediately instead of polling to the timeout. BackupJob and |
| 52 | +# RestoreJob settle on a terminal phase=Failed that never becomes Succeeded, so |
| 53 | +# failing fast on it keeps wall-clock in reach. |
| 54 | +wait_for_field() { |
| 55 | + local resource_type="$1" |
| 56 | + local resource_name="$2" |
| 57 | + local jsonpath="$3" |
| 58 | + local desired="$4" |
| 59 | + local namespace="${5:-}" |
| 60 | + local timeout="${6:-300}" |
| 61 | + local fail_value="${7:-}" |
| 62 | + |
| 63 | + log_substep "Waiting for $resource_type/$resource_name $jsonpath to become '$desired'..." |
| 64 | + local elapsed=0 |
| 65 | + local ns_flag=() |
| 66 | + [[ -n "$namespace" ]] && ns_flag=(-n "$namespace") |
| 67 | + |
| 68 | + while true; do |
| 69 | + local current |
| 70 | + current=$(kubectl get "$resource_type" "$resource_name" "${ns_flag[@]}" -o jsonpath="$jsonpath" 2>/dev/null || true) |
| 71 | + if [[ "$current" == "$desired" ]]; then |
| 72 | + log_success "$resource_type/$resource_name reached '$desired'" |
| 73 | + return 0 |
| 74 | + fi |
| 75 | + if [[ -n "$fail_value" && "$current" == "$fail_value" ]]; then |
| 76 | + log_error "$resource_type/$resource_name reached terminal '$current' (expected '$desired')" |
| 77 | + return 1 |
| 78 | + fi |
| 79 | + if [[ $elapsed -ge $timeout ]]; then |
| 80 | + log_error "Timeout waiting for $resource_type/$resource_name (current: '$current', expected: '$desired')" |
| 81 | + return 1 |
| 82 | + fi |
| 83 | + sleep 5 |
| 84 | + elapsed=$((elapsed + 5)) |
| 85 | + done |
| 86 | +} |
| 87 | + |
| 88 | +# Wait for a HelmRelease to become Ready, with an existence backstop (the apps |
| 89 | +# controller creates the HR asynchronously) and a fail-fast on Stalled=True. |
| 90 | +wait_hr_ready() { |
| 91 | + local name="$1" |
| 92 | + local timeout="${2:-300}" |
| 93 | + local elapsed=0 |
| 94 | + |
| 95 | + log_substep "Waiting for HelmRelease/$name to become Ready..." |
| 96 | + while true; do |
| 97 | + if kubectl -n "$NAMESPACE" get hr "$name" >/dev/null 2>&1; then |
| 98 | + local ready stalled |
| 99 | + ready=$(kubectl -n "$NAMESPACE" get hr "$name" \ |
| 100 | + -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || true) |
| 101 | + if [[ "$ready" == "True" ]]; then |
| 102 | + log_success "HelmRelease/$name is Ready" |
| 103 | + return 0 |
| 104 | + fi |
| 105 | + stalled=$(kubectl -n "$NAMESPACE" get hr "$name" \ |
| 106 | + -o jsonpath='{.status.conditions[?(@.type=="Stalled")].status}' 2>/dev/null || true) |
| 107 | + if [[ "$stalled" == "True" ]]; then |
| 108 | + log_error "HelmRelease/$name is Stalled (terminal): $(kubectl -n "$NAMESPACE" get hr "$name" \ |
| 109 | + -o jsonpath='{.status.conditions[?(@.type=="Ready")].message}' 2>/dev/null)" |
| 110 | + return 1 |
| 111 | + fi |
| 112 | + fi |
| 113 | + if [[ $elapsed -ge $timeout ]]; then |
| 114 | + log_error "Timeout waiting for HelmRelease/$name to become Ready:" |
| 115 | + kubectl -n "$NAMESPACE" get hr "$name" \ |
| 116 | + -o jsonpath='{.status.conditions[?(@.type=="Ready")].message}' >&2 2>/dev/null || true |
| 117 | + return 1 |
| 118 | + fi |
| 119 | + sleep 5 |
| 120 | + elapsed=$((elapsed + 5)) |
| 121 | + done |
| 122 | +} |
| 123 | + |
| 124 | +# Wait until the rabbitmq.com/RabbitmqCluster CR reports AllReplicasReady=True |
| 125 | +# (the cluster-operator's own readiness signal — every server pod is up and the |
| 126 | +# management API is serving). No fail-fast value: the condition can be briefly |
| 127 | +# absent or False during rolling startup before it converges. |
| 128 | +wait_rabbitmq_ready() { |
| 129 | + local cr="$1" timeout="${2:-600}" |
| 130 | + wait_for_field rabbitmqclusters.rabbitmq.com "$cr" \ |
| 131 | + '{.status.conditions[?(@.type=="AllReplicasReady")].status}' True "$NAMESPACE" "$timeout" |
| 132 | +} |
| 133 | + |
| 134 | +# Run rabbitmqctl inside a RabbitmqCluster's first server pod. rabbitmqctl talks |
| 135 | +# to the local node over the Erlang dist port and needs no HTTP credentials, so |
| 136 | +# it is the simplest way to mutate and read the DEFINITIONS (vhosts, policies) |
| 137 | +# this demo uses as its round-trip sentinel. Args: <cr-name> <rabbitmqctl args...> |
| 138 | +rabbitmqctl_exec() { |
| 139 | + local cr="$1"; shift |
| 140 | + kubectl -n "$NAMESPACE" exec "${cr}-server-0" -c rabbitmq -- rabbitmqctl "$@" |
| 141 | +} |
| 142 | + |
| 143 | +# Seed the sentinel: a vhost plus a policy on it. Both are broker DEFINITIONS |
| 144 | +# captured by /api/definitions, so their survival through S3 proves the backup |
| 145 | +# round-tripped the definitions (not merely that a Job ran). Args: <cr> <token> |
| 146 | +rabbitmq_seed_sentinel() { |
| 147 | + local cr="$1" token="$2" |
| 148 | + rabbitmqctl_exec "$cr" add_vhost "$token" >/dev/null |
| 149 | + rabbitmqctl_exec "$cr" set_policy -p "$token" sentinel-policy '.*' \ |
| 150 | + '{"max-length":1000}' --apply-to queues >/dev/null |
| 151 | +} |
| 152 | + |
| 153 | +# True when the sentinel vhost exists on the broker. Args: <cr> <token> |
| 154 | +rabbitmq_has_sentinel() { |
| 155 | + local cr="$1" token="$2" |
| 156 | + rabbitmqctl_exec "$cr" list_vhosts name 2>/dev/null | grep -qxF "$token" |
| 157 | +} |
0 commit comments