Skip to content

Commit 95ece88

Browse files
authored
feat(kafka): add topic-data backup/restore example and e2e roundtrip (#3580)
## What this PR does Adds a worked backup/restore example for a Cozystack-managed `Kafka` application's **topic data**, built on the generic `Job` backup strategy — the same pattern as `examples/backups/nats`, with no purpose-built backup image. A stock Strimzi Kafka image (the `kafka-*.sh` CLI plus `bash`, `curl` and `tar`) runs one shell script that branches on `{{ .Mode }}`: on backup it freezes each partition's end offset and drains `[begin, end)` to a tarball it `PUT`s to S3; on restore it `GET`s the tarball, recreates the topics with their original partition count and replication factor, and replays every partition file. S3 access is `curl --aws-sigv4`, verified against the endpoint's CA projected from the `<app>-backup-s3` Secret, so no extra client image is needed. The consistency model is a frozen-end-offset cut: `kafka-get-offsets --time -1` is read once per partition at backup start and the consumer drains only up to that offset, so anything produced during the run is excluded and every partition of a topic is captured as of the same instant — a cut by log position, independent of record timestamps. This is deliberately a DATA-only backup. Topic configs, ACLs, SCRAM users and **consumer-group offsets** are not captured, and re-produced records receive fresh offsets — the README spells out the scope and points at a volume-snapshot strategy for a faithful, offset-preserving backup. Keyed records reproduce into their original partition because the topic is recreated with the same partition count and the default (murmur2) partitioner is deterministic. The script fails closed rather than reporting a partial backup or a lossy restore as success: it refuses an offset listing short of the topic's partition count, a drain short of `end - begin`, a `topics` value with a duplicate or a name outside Kafka's charset, a manifest that disagrees with the data files, a target topic that already holds records (checked before the replay, so a retried Pod cannot append a second copy), a pre-existing topic with a different partition count or replication factor, a replication factor below the target's effective `min.insync.replicas`, and a post-replay count that does not match the manifest. Each topic's replication factor is recorded in the manifest and restored as captured; the `replicationFactor` BackupClass parameter overrides it for restoring into a smaller cluster. Also adds a `kafka-2-backup-roundtrip` Chainsaw test to `hack/e2e-chainsaw/kafka`, which drives the example `run-all.sh` as its harness (Bucket → two-broker source Kafka with a seeded 3-partition, 2-replica topic plus a decoy topic that the demo topic matches as a regex but not as a literal → BackupJob → in-place restore with count, content and replication-factor assertions → a second RestoreJob into the populated topic that must be refused with the topic untouched → two-broker target Kafka + to-copy restore + verify), mirroring `mariadb-2-backup-roundtrip`. Two brokers, not one, because that is the shape on which the chart sets `min.insync.replicas: 2` and a restore with the wrong replication factor cannot land a record. It resolves the Kafka image from the running operator so the backup Pod reuses the image already cached on the nodes. The strategy script itself is pinned by `hack/kafka-topic-backup-strategy.bats`, on the shape of `hack/kafka-backup-script.bats`: it renders the strategy through a stub `kubectl` exactly as step 01 applies it, lifts the container body out, and executes it against stubbed `kafka-*.sh`, a stubbed `curl` and a directory standing in for S3, with fault injection for the broker behaviours the guards exist for. Fourteen cases, one per guard; removing the partition-listing guard reddens one of them. Verified: `chainsaw/kafka-2-backup-roundtrip` passed in this PR's CI on the two-broker shape (record count, byte-identical content dump and replication factor 2 asserted after both restores, the refused RestoreJob settled Failed with the topic untouched); `hack/kafka-topic-backup-strategy.bats` passes under both `hack/cozytest.sh` and bats. Files: - `examples/backups/kafka/` — 14 files (`00-helpers.sh`, `01`..`07`, `run-all.sh`, `cleanup.sh`, `README.md`, `90`/`91`/`92` scenario walk-throughs). - `hack/kafka-topic-backup-strategy.bats` — hermetic test of the strategy script. - `hack/e2e-chainsaw/kafka/chainsaw-test.yaml` — new `kafka-2-backup-roundtrip` Test appended. - `hack/backup-access-preflight.bats` — the kafka example and Test join the S3-preflight roster. - `hack/select-e2e.sh`, `hack/e2e-chainsaw/.chainsaw.yaml` — comment-only: their enumerations of preflight-gated suites are replaced by a pointer. - `.gitignore` — the example's `.source-dump.txt` snapshot. No package sources (`values.yaml`, `values.schema.json`, `Chart.yaml`, package `README.md`) are touched, so `make generate` produces no diff. ### Screenshots N/A — no UI changes. ### Downstream repositories Walked the trigger map in `docs/agents/contributing.md` file-by-file against the diff. The change adds example scripts under `examples/backups/kafka`, one unit test under `hack/`, and appends one Chainsaw Test to an existing file under `hack/e2e-chainsaw/kafka` — it adds no package under `packages/apps` or `packages/extra`, changes no CRD, no `ApplicationDefinition`, no `values.schema.json`, no platform/installer values, no `hack/*.mk` anchor or make-target behaviour, and no node prerequisites in `hack/e2e-prepare-cluster.bats`. None of the downstream triggers match. - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: ### Release note ```release-note feat(kafka): add a topic-data backup/restore example and e2e round-trip on the generic Job backup strategy (data-only: topic records with a frozen-end-offset consistency cut and partition-preserving replay; consumer offsets, topic configs, ACLs and users are out of scope) ```
2 parents 61a7a25 + 7ce85a2 commit 95ece88

20 files changed

Lines changed: 2373 additions & 10 deletions

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ packages/core/platform/.build-revision
123123
examples/backups/*/.bucket-info.env
124124
examples/backups/*/.sentinel.env
125125
examples/backups/*/.backup-name.env
126+
examples/backups/*/.source-dump.txt
126127
packages/core/platform/images/migrations/etcd-operator-crds/
127128

128129
# CI scratch: promote-rc.yaml checks the dispatch ref here so release tooling

‎examples/backups/kafka/00-helpers.sh‎

Lines changed: 414 additions & 0 deletions
Large diffs are not rendered by default.

‎examples/backups/kafka/01-create-strategy.sh‎

Lines changed: 571 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/bin/bash
2+
# Step 02: Map the Kafka application kind to the Job strategy from step 01. The
3+
# strategy parameters (which topics to back up, an optional replication-factor
4+
# override for the topics restore recreates) travel through the BackupClass and
5+
# are exposed to the strategy template as `.Parameters`. They are also
6+
# snapshotted onto the resulting Backup's driverMetadata, so a later restore
7+
# re-renders with the same values.
8+
set -euo pipefail
9+
10+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11+
source "$SCRIPT_DIR/00-helpers.sh"
12+
13+
print_header "Step 02: Create BackupClass '${BACKUPCLASS_NAME}'"
14+
log_command "kubectl apply -f - (BackupClass: $BACKUPCLASS_NAME -> $STRATEGY_NAME)"
15+
16+
kubectl apply -f - <<EOF
17+
apiVersion: backups.cozystack.io/v1alpha1
18+
kind: BackupClass
19+
metadata:
20+
name: ${BACKUPCLASS_NAME}
21+
spec:
22+
strategies:
23+
- application:
24+
apiGroup: apps.cozystack.io
25+
kind: Kafka
26+
strategyRef:
27+
apiGroup: strategy.backups.cozystack.io
28+
kind: Job
29+
name: ${STRATEGY_NAME}
30+
parameters:
31+
# Empty "topics" backs up every non-internal topic - the wider net, not
32+
# the safer one: it can pull in a compacted or transactional topic,
33+
# which this logical strategy fails closed on (see the drain guard in
34+
# 01-create-strategy.sh and README "Scope and limitations"). Set a
35+
# comma/space-separated list to restrict the backup to topics known to
36+
# be plain.
37+
topics: "${TOPIC}"
38+
# Restore recreates each topic with the replication factor the backup
39+
# captured from the source. Set replicationFactor to override it - the
40+
# way to restore into a cluster with fewer brokers than the source.
41+
# Like every parameter it is snapshotted at backup time, so it has to
42+
# be in place before the BackupJob whose Backup will be restored.
43+
EOF
44+
45+
log_success "BackupClass '${BACKUPCLASS_NAME}' created."
46+
echo -e "\n${GREEN}${BOLD}Next:${NC} ./03-create-bucket.sh"
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#!/bin/bash
2+
# Step 03: Provision an in-cluster Bucket and cache its S3 coordinates. The
3+
# generic Job strategy has no chart support to emit a backup Secret, so the
4+
# tenant materialises the "<app>-backup-s3" Secret itself. This step only
5+
# caches the coordinates; create_s3_secret (00-helpers.sh) turns them into
6+
# per-app Secrets in steps 04 and 07.
7+
set -euo pipefail
8+
9+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10+
source "$SCRIPT_DIR/00-helpers.sh"
11+
12+
print_header "Step 03: Provision Bucket '${BUCKET_NAME}' in '${NAMESPACE}'"
13+
14+
log_command "kubectl apply -f - (Bucket: $BUCKET_NAME)"
15+
# spec.users.backup is required for the Bucket app to provision a
16+
# BucketAccess (and credentials Secret) the backup tooling can read.
17+
kubectl apply -f - <<EOF
18+
apiVersion: apps.cozystack.io/v1alpha1
19+
kind: Bucket
20+
metadata:
21+
name: ${BUCKET_NAME}
22+
namespace: ${NAMESPACE}
23+
spec:
24+
users:
25+
backup:
26+
readonly: false
27+
EOF
28+
29+
wait_hr_ready "bucket-${BUCKET_NAME}"
30+
kubectl -n "$NAMESPACE" wait bucketclaims.objectstorage.k8s.io "bucket-${BUCKET_NAME}" --for=jsonpath='{.status.bucketReady}'=true --timeout=300s
31+
# Cozystack's bucket app provisions a BucketAccess named "<bucket-name>-backup"
32+
# (the "-backup" suffix is the BucketAccessClass name); the BucketInfo Secret
33+
# carries the same name.
34+
kubectl -n "$NAMESPACE" wait bucketaccesses.objectstorage.k8s.io "bucket-${BUCKET_NAME}-backup" --for=jsonpath='{.status.accessGranted}'=true --timeout=300s
35+
36+
# E2E only: accessGranted describes the COSI object, not whether the S3 server
37+
# has reloaded the new IAM identity, so a freshly granted key can still answer
38+
# the strategy Pod's SigV4 PUT with 403 - which reds the run as a backup defect
39+
# rather than the grant-propagation race it is. The shared preflight proves the
40+
# key works before anything depends on it; the other backup round-trips gate it
41+
# the same way.
42+
if [[ "${COZY_E2E_BACKUP_PREFLIGHT:-0}" == "1" ]]; then
43+
# shellcheck source=/dev/null
44+
source "$SCRIPT_DIR/../../../hack/e2e-chainsaw/_lib/backup-access-preflight.sh"
45+
cozy_backup_access_preflight "$NAMESPACE" "bucket-${BUCKET_NAME}-backup" 90
46+
fi
47+
48+
log_substep "Reading bucket coordinates from BucketInfo Secret..."
49+
TMP=$(mktemp)
50+
trap 'rm -f "$TMP"' EXIT
51+
kubectl -n "$NAMESPACE" get secret "bucket-${BUCKET_NAME}-backup" -o jsonpath='{.data.BucketInfo}' | base64 -d > "$TMP"
52+
53+
S3_ACCESS_KEY=$(jq -r '.spec.secretS3.accessKeyID' "$TMP")
54+
S3_SECRET_KEY=$(jq -r '.spec.secretS3.accessSecretKey' "$TMP")
55+
# S3_ENDPOINT can be overridden via the environment: BucketInfo advertises the
56+
# external ingress URL, which in CI is an unroutable placeholder; the e2e
57+
# harness points this at the in-cluster seaweedfs Service instead.
58+
S3_ENDPOINT="${S3_ENDPOINT:-$(jq -r '.spec.secretS3.endpoint' "$TMP")}"
59+
S3_REGION=$(jq -r 'if (.spec.secretS3.region // "") == "" then "us-east-1" else .spec.secretS3.region end' "$TMP")
60+
S3_BUCKET=$(jq -r '.spec.bucketName' "$TMP")
61+
62+
# `jq -r` returns the literal string "null" for missing JSON paths; fail fast
63+
# here so a missing field surfaces at extraction time instead of as a confusing
64+
# curl/SigV4 error inside the backup Pod.
65+
for v in S3_ACCESS_KEY S3_SECRET_KEY S3_ENDPOINT S3_REGION S3_BUCKET; do
66+
[[ -n "${!v}" && "${!v}" != "null" ]] || { log_error "BucketInfo missing required field: ${v}"; exit 1; }
67+
done
68+
69+
# The endpoint's CA, so the strategy Pod verifies TLS instead of skipping it.
70+
# The default name tracks the seaweedfs chart's fullnameOverride (seaweedfs ->
71+
# seaweedfs-ca-cert), but a downstream fullname change would rename it, so fall
72+
# back to discovering the cert-manager CA Certificate (the seaweedfs-labelled
73+
# one with spec.isCA=true) and read its secretName. S3_CA_SECRET="" skips the
74+
# copy for a publicly-trusted endpoint (see 00-helpers.sh).
75+
S3_CA_B64=""
76+
if [[ -n "$S3_CA_SECRET" ]]; then
77+
# Only an absent Secret earns a discovery pass. The read crosses into
78+
# another namespace, which a tenant persona is not granted, and a denial
79+
# must say so rather than surface as "not found" and point at a name
80+
# that cannot help.
81+
if ! err=$(kubectl -n "$S3_CA_NAMESPACE" get secret "$S3_CA_SECRET" 2>&1 >/dev/null); then
82+
case "$err" in
83+
*NotFound*|*"not found"*) ;;
84+
*)
85+
log_error "Cannot read S3 CA secret ${S3_CA_NAMESPACE}/${S3_CA_SECRET}: ${err}"
86+
log_error "Run this step with a persona that can read Secrets in ${S3_CA_NAMESPACE}, or set S3_CA_SECRET=\"\" for a publicly-trusted endpoint."
87+
exit 1 ;;
88+
esac
89+
log_warning "S3 CA secret ${S3_CA_NAMESPACE}/${S3_CA_SECRET} not found; discovering the seaweedfs CA Certificate..."
90+
discovered=$(kubectl -n "$S3_CA_NAMESPACE" get certificates.cert-manager.io \
91+
-l app.kubernetes.io/name=seaweedfs \
92+
-o jsonpath='{range .items[*]}{.spec.isCA}{" "}{.spec.secretName}{"\n"}{end}' \
93+
| awk '$1=="true"{print $2; exit}' || true)
94+
[[ -n "$discovered" ]] || { log_error "No seaweedfs CA Certificate found in ${S3_CA_NAMESPACE} (or the read was denied, see above); set S3_CA_SECRET explicitly (or empty for a publicly-trusted endpoint)."; exit 1; }
95+
log_success "Discovered seaweedfs CA secret ${S3_CA_NAMESPACE}/${discovered}"
96+
S3_CA_SECRET="$discovered"
97+
fi
98+
log_substep "Caching S3 CA ${S3_CA_NAMESPACE}/${S3_CA_SECRET}[${S3_CA_KEY}]..."
99+
S3_CA_B64=$(kubectl -n "$S3_CA_NAMESPACE" get secret "$S3_CA_SECRET" -o jsonpath="{.data.${S3_CA_KEY//./\\.}}")
100+
[[ -n "$S3_CA_B64" ]] || { log_error "S3 CA secret ${S3_CA_NAMESPACE}/${S3_CA_SECRET} has no ${S3_CA_KEY}"; exit 1; }
101+
fi
102+
103+
# Persist for steps 04 / 07. The cache stores raw S3 credentials, so apply
104+
# restrictive perms before writing the body - umask alone could leave the file
105+
# group/world-readable. The CA stays base64 so the file remains one export per
106+
# line.
107+
umask 077
108+
cat > "$SCRIPT_DIR/.bucket-info.env" <<ENV
109+
export S3_ACCESS_KEY=${S3_ACCESS_KEY}
110+
export S3_SECRET_KEY=${S3_SECRET_KEY}
111+
export S3_ENDPOINT=${S3_ENDPOINT}
112+
export S3_REGION=${S3_REGION}
113+
export S3_BUCKET=${S3_BUCKET}
114+
export S3_CA_B64=${S3_CA_B64}
115+
ENV
116+
chmod 600 "$SCRIPT_DIR/.bucket-info.env"
117+
118+
log_success "Bucket '${BUCKET_NAME}' ready; coordinates cached in $(basename "$SCRIPT_DIR")/.bucket-info.env."
119+
echo -e "\n${GREEN}${BOLD}Next:${NC} ./04-create-kafka.sh"
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/bin/bash
2+
# Step 04: Provision a Kafka instance with KAFKA_REPLICAS brokers, create the
3+
# S3 Secret the Job strategy consumes, then seed a topic with sentinel messages
4+
# used to verify the backup/restore round-trip. The topic is created via the
5+
# CLI (not the chart's `topics:`) so there is no KafkaTopic CR - the in-place
6+
# restore in step 06 can then delete and let the driver recreate the topic
7+
# deterministically, with no Topic Operator reconciliation racing the restore.
8+
set -euo pipefail
9+
10+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11+
source "$SCRIPT_DIR/00-helpers.sh"
12+
13+
print_header "Step 04: Provision Kafka '${KAFKA_NAME}'"
14+
15+
kubectl apply -f - <<EOF
16+
apiVersion: apps.cozystack.io/v1alpha1
17+
kind: Kafka
18+
metadata:
19+
name: ${KAFKA_NAME}
20+
namespace: ${NAMESPACE}
21+
spec:
22+
external: false
23+
kafka:
24+
replicas: ${KAFKA_REPLICAS}
25+
size: 2Gi
26+
resourcesPreset: "c1.small"
27+
topics: []
28+
EOF
29+
30+
wait_hr_ready "kafka-${KAFKA_NAME}"
31+
32+
log_substep "Waiting for the Strimzi Kafka cluster to be Ready (broker and controller pools up)..."
33+
kubectl -n "$NAMESPACE" wait kafka.kafka.strimzi.io "kafka-${KAFKA_NAME}" \
34+
--for=condition=Ready --timeout=600s
35+
36+
log_substep "Creating S3 credentials Secret '${KAFKA_NAME}-backup-s3'..."
37+
create_s3_secret "$KAFKA_NAME"
38+
39+
log_substep "Creating topic '${TOPIC}' (${PARTITIONS} partitions, ${TOPIC_REPLICAS} replicas) and publishing ${MESSAGE_COUNT} sentinel messages..."
40+
seed_topic "$KAFKA_NAME"
41+
42+
count=$(topic_message_count "$KAFKA_NAME")
43+
[[ "$count" == "$MESSAGE_COUNT" ]] || { log_error "expected ${MESSAGE_COUNT} messages in '${TOPIC}', got '${count}'"; exit 1; }
44+
rf=$(topic_replication_factor "$KAFKA_NAME")
45+
[[ "$rf" == "$TOPIC_REPLICAS" ]] || { log_error "expected '${TOPIC}' to have ${TOPIC_REPLICAS} replicas, got '${rf}'"; exit 1; }
46+
log_success "Topic '${TOPIC}' holds ${count} record(s) at replication factor ${rf}."
47+
48+
# Seed the decoy. It is never named in the BackupClass, so a correct run must
49+
# leave it untouched; a --topic call that matched it as a regex instead would
50+
# either pull its partitions into the backup or delete it in step 06.
51+
log_substep "Creating decoy topic '${DECOY_TOPIC}' (${DECOY_COUNT} records) to pin literal topic matching..."
52+
seed_decoy_topic "$KAFKA_NAME"
53+
decoy=$(decoy_message_count "$KAFKA_NAME")
54+
[[ "$decoy" == "$DECOY_COUNT" ]] || { log_error "expected ${DECOY_COUNT} messages in '${DECOY_TOPIC}', got '${decoy}'"; exit 1; }
55+
log_success "Decoy topic '${DECOY_TOPIC}' holds ${decoy} record(s)."
56+
57+
# Snapshot the source content so the restore steps can diff against it and
58+
# prove the records - not just their count - round-tripped. cleanup.sh removes
59+
# this file.
60+
topic_dump "$KAFKA_NAME" > "$SCRIPT_DIR/.source-dump.txt"
61+
dumped=$(wc -l < "$SCRIPT_DIR/.source-dump.txt" | tr -d '[:space:]')
62+
[[ "$dumped" == "$MESSAGE_COUNT" ]] || { log_error "source dump has ${dumped} records, expected ${MESSAGE_COUNT}"; exit 1; }
63+
64+
echo -e "\n${GREEN}${BOLD}Next:${NC} ./05-create-backupjob.sh"
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/bin/bash
2+
# Step 05: Create a BackupJob and wait for it to succeed. The controller
3+
# resolves the BackupClass -> Job strategy, renders the Pod template against
4+
# the Kafka app, and runs it as a batch/v1.Job (owned by the BackupJob). The
5+
# Pod freezes each partition's end offset, drains [begin,end) and uploads a
6+
# tarball to S3.
7+
set -euo pipefail
8+
9+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10+
source "$SCRIPT_DIR/00-helpers.sh"
11+
12+
print_header "Step 05: Create BackupJob '${BACKUPJOB_NAME}'"
13+
14+
kubectl apply -f - <<EOF
15+
apiVersion: backups.cozystack.io/v1alpha1
16+
kind: BackupJob
17+
metadata:
18+
name: ${BACKUPJOB_NAME}
19+
namespace: ${NAMESPACE}
20+
spec:
21+
applicationRef:
22+
apiGroup: apps.cozystack.io
23+
kind: Kafka
24+
name: ${KAFKA_NAME}
25+
backupClassName: ${BACKUPCLASS_NAME}
26+
EOF
27+
28+
log_substep "Waiting for BackupJob to Succeed (this drains the topic to S3)..."
29+
# Skip an explicit "wait Running" gate: a fast-completing strategy Pod can
30+
# transition Pending -> Succeeded between two API observations, in which case
31+
# `kubectl wait --for=jsonpath='{.status.phase}'=Running` would time out even
32+
# though the BackupJob actually finished. wait_for_field already polls until
33+
# the terminal phase, which covers both the slow and the fast path.
34+
wait_for_field backupjob "$BACKUPJOB_NAME" '{.status.phase}' Succeeded "$NAMESPACE" 600 Failed
35+
36+
backup_ref=$(kubectl -n "$NAMESPACE" get backupjob "$BACKUPJOB_NAME" -o jsonpath='{.status.backupRef.name}')
37+
[[ -n "$backup_ref" ]] || { log_error "BackupJob succeeded but BackupRef is empty"; exit 1; }
38+
# The restore steps reference the Backup by this resolved name rather than by
39+
# assuming it equals the BackupJob's. cleanup.sh removes the file.
40+
printf 'export BACKUP_NAME=%q\n' "$backup_ref" > "$SCRIPT_DIR/.backup-name.env"
41+
log_success "Backup '${backup_ref}' is Ready."
42+
43+
echo -e "\n${GREEN}${BOLD}Next:${NC} ./06-restore-in-place.sh"

0 commit comments

Comments
 (0)