feat(kafka): add topic-data backup/restore example and e2e roundtrip - #3580
feat(kafka): add topic-data backup/restore example and e2e roundtrip#3580Andrey Kolkov (androndo) wants to merge 8 commits into
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a complete Kafka topic-data backup and restore example. The workflow provisions Kafka and S3 resources, stores frozen-offset archives, restores data in place or to another cluster, documents each step, and validates the round trip with Chainsaw. ChangesKafka backup and restore workflow
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Operator
participant BackupJob
participant Kafka
participant S3
participant RestoreJob
Operator->>BackupJob: create backup
BackupJob->>Kafka: capture bounded topic records
BackupJob->>S3: upload archive
S3-->>RestoreJob: provide archive
RestoreJob->>Kafka: recreate topics and replay records
Kafka-->>Operator: report restored record count
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (2)
examples/backups/kafka/03-create-bucket.sh (1)
62-68: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winQuote the cached values.
The cache writes each value unquoted.
create_s3_secretsources this file, so a credential that contains a space, quote,$, or;breaks the assignment or executes shell syntax. Useprintf %qto emit safe literals.♻️ Proposed shell-safe cache write
-cat > "$SCRIPT_DIR/.bucket-info.env" <<ENV -export S3_ACCESS_KEY=${S3_ACCESS_KEY} -export S3_SECRET_KEY=${S3_SECRET_KEY} -export S3_ENDPOINT=${S3_ENDPOINT} -export S3_REGION=${S3_REGION} -export S3_BUCKET=${S3_BUCKET} -ENV +: > "$SCRIPT_DIR/.bucket-info.env" +for v in S3_ACCESS_KEY S3_SECRET_KEY S3_ENDPOINT S3_REGION S3_BUCKET; do + printf 'export %s=%q\n' "$v" "${!v}" >> "$SCRIPT_DIR/.bucket-info.env" +done🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/backups/kafka/03-create-bucket.sh` around lines 62 - 68, Update the .bucket-info.env cache generation in the heredoc to emit shell-safe quoted literals for every S3 value, using printf %q before writing S3_ACCESS_KEY, S3_SECRET_KEY, S3_ENDPOINT, S3_REGION, and S3_BUCKET. Keep create_s3_secret’s sourcing behavior unchanged while ensuring values containing spaces, quotes, dollar signs, or semicolons remain data.examples/backups/kafka/00-helpers.sh (1)
55-82: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winAdd a terminal-failure phase to
wait_for_field.
wait_for_fieldonly compares againstdesired. If aBackupJoborRestoreJobreachesFailed, the loop keeps polling until the timeout expires. Steps 05, 06 and 07 therefore stall for 600s on every failure. The etcd example passes an extra failure phase (examples/backups/etcd/04-create-backupjob.sh), and the comment inhack/e2e-chainsaw/kafka/chainsaw-test.yaml(Lines 192-193) already claims fail-fast behavior that this helper does not implement.♻️ Proposed fail-fast argument
wait_for_field() { local resource_type="$1" local resource_name="$2" local jsonpath="$3" local desired="$4" local namespace="${5:-}" local timeout="${6:-300}" + local failed="${7:-}" @@ if [[ "$current" == "$desired" ]]; then log_success "$resource_type/$resource_name reached '$desired'" return 0 fi + if [[ -n "$failed" && "$current" == "$failed" ]]; then + log_error "$resource_type/$resource_name reached terminal failure phase '$current'" + return 1 + fi🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/backups/kafka/00-helpers.sh` around lines 55 - 82, Update wait_for_field to accept an optional terminal-failure value in addition to desired, and return failure immediately when the current field matches it. Preserve existing success and timeout behavior, and keep the argument optional so current callers remain compatible while Kafka BackupJob/RestoreJob steps can pass Failed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@examples/backups/kafka/01-create-strategy.sh`:
- Around line 193-202: Update the Fidelity bullet in the Kafka backup README to
explicitly state that null-key records are replayed with the literal
four-character key “null” rather than a null key, in addition to the existing
partition-change caveat.
- Around line 120-124: Update the s3() wrapper to stop passing AWS credentials
via curl’s --user command-line argument; provide them through a curl
configuration file on stdin while preserving the existing SigV4, TLS, and
forwarded-argument behavior.
In `@examples/backups/kafka/05-create-backupjob.sh`:
- Around line 36-38: Persist the resolved backupRef name after retrieving it in
the backup job script, using the established variable or state mechanism
consumed by later steps. Update downstream steps 06 and 07 to reference the
cached backupRef instead of BACKUPJOB_NAME, while preserving the existing
empty-value validation and success logging.
In `@examples/backups/kafka/06-restore-in-place.sh`:
- Around line 37-38: Update the backupRef.name value in the restore
configuration to use the resolved Backup name from
BackupJob.status.backupRef.name rather than BACKUPJOB_NAME, preserving the
reference to the created Backup resource.
In `@examples/backups/kafka/07-restore-to-copy.sh`:
- Around line 50-51: Update the backupRef.name value in the restore manifest to
reference the resolved Backup resource name rather than BACKUPJOB_NAME, matching
the correction made in step 06.
In `@examples/backups/kafka/cleanup.sh`:
- Line 14: Update the cleanup command to delete the Backup using its resolved
Backup resource name rather than assuming it matches BACKUPJOB_NAME; reuse the
variable or lookup established by the script’s resolution flow, while preserving
the namespace and ignore-not-found options.
In `@hack/e2e-chainsaw/kafka/chainsaw-test.yaml`:
- Around line 200-205: Update the KAFKA_IMAGE extraction in the backup setup to
parse the multi-line STRIMZI_KAFKA_IMAGES version-to-image map by selecting one
explicit mapping and extracting its image value. Validate that the result is
exactly one non-empty image value, and retain the existing failure path when
parsing does not produce a valid image.
---
Nitpick comments:
In `@examples/backups/kafka/00-helpers.sh`:
- Around line 55-82: Update wait_for_field to accept an optional
terminal-failure value in addition to desired, and return failure immediately
when the current field matches it. Preserve existing success and timeout
behavior, and keep the argument optional so current callers remain compatible
while Kafka BackupJob/RestoreJob steps can pass Failed.
In `@examples/backups/kafka/03-create-bucket.sh`:
- Around line 62-68: Update the .bucket-info.env cache generation in the heredoc
to emit shell-safe quoted literals for every S3 value, using printf %q before
writing S3_ACCESS_KEY, S3_SECRET_KEY, S3_ENDPOINT, S3_REGION, and S3_BUCKET.
Keep create_s3_secret’s sourcing behavior unchanged while ensuring values
containing spaces, quotes, dollar signs, or semicolons remain data.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 982fd5d4-679d-4eaa-bdd5-b89165a71c7e
📒 Files selected for processing (12)
examples/backups/kafka/00-helpers.shexamples/backups/kafka/01-create-strategy.shexamples/backups/kafka/02-create-backupclass.shexamples/backups/kafka/03-create-bucket.shexamples/backups/kafka/04-create-kafka.shexamples/backups/kafka/05-create-backupjob.shexamples/backups/kafka/06-restore-in-place.shexamples/backups/kafka/07-restore-to-copy.shexamples/backups/kafka/README.mdexamples/backups/kafka/cleanup.shexamples/backups/kafka/run-all.shhack/e2e-chainsaw/kafka/chainsaw-test.yaml
| # curl --aws-sigv4 signs the request (SigV4) so no separate S3 | ||
| # client image is needed. -k accepts seaweedfs's internal | ||
| # self-signed cert; a production strategy would mount the tenant | ||
| # CA and drop -k. | ||
| s3() { curl -fsS -k --aws-sigv4 "aws:amz:\${S3_REGION}:s3" --user "\${AWS_ACCESS_KEY_ID}:\${AWS_SECRET_ACCESS_KEY}" "\$@"; } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Keep the S3 credentials off the curl command line.
--user "${AWS_ACCESS_KEY_ID}:${AWS_SECRET_ACCESS_KEY}" places the secret key in the process arguments. Any process in the Pod can read it from /proc/<pid>/cmdline, and any shell trace prints it. Pass the credentials through a curl config file on stdin instead.
🔒 Proposed credential handling
- s3() { curl -fsS -k --aws-sigv4 "aws:amz:\${S3_REGION}:s3" --user "\${AWS_ACCESS_KEY_ID}:\${AWS_SECRET_ACCESS_KEY}" "\$@"; }
+ s3() {
+ printf 'user = "%s:%s"\n' "\${AWS_ACCESS_KEY_ID}" "\${AWS_SECRET_ACCESS_KEY}" \
+ | curl -fsS -k --aws-sigv4 "aws:amz:\${S3_REGION}:s3" -K - "\$@"
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # curl --aws-sigv4 signs the request (SigV4) so no separate S3 | |
| # client image is needed. -k accepts seaweedfs's internal | |
| # self-signed cert; a production strategy would mount the tenant | |
| # CA and drop -k. | |
| s3() { curl -fsS -k --aws-sigv4 "aws:amz:\${S3_REGION}:s3" --user "\${AWS_ACCESS_KEY_ID}:\${AWS_SECRET_ACCESS_KEY}" "\$@"; } | |
| # curl --aws-sigv4 signs the request (SigV4) so no separate S3 | |
| # client image is needed. -k accepts seaweedfs's internal | |
| # self-signed cert; a production strategy would mount the tenant | |
| # CA and drop -k. | |
| s3() { | |
| printf 'user = "%s:%s"\n' "\${AWS_ACCESS_KEY_ID}" "\${AWS_SECRET_ACCESS_KEY}" \ | |
| | curl -fsS -k --aws-sigv4 "aws:amz:\${S3_REGION}:s3" -K - "\$@" | |
| } |
🧰 Tools
🪛 Betterleaks (1.7.3)
[high] 120-124: Discovered a potential basic authorization token provided in a curl command, which could compromise the curl accessed resource.
(curl-auth-user)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/backups/kafka/01-create-strategy.sh` around lines 120 - 124, Update
the s3() wrapper to stop passing AWS credentials via curl’s --user command-line
argument; provide them through a curl configuration file on stdin while
preserving the existing SigV4, TLS, and forwarded-argument behavior.
Source: Linters/SAST tools
| backupRef: | ||
| name: ${BACKUPJOB_NAME} |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Reference the resolved Backup name, not the BackupJob name.
spec.backupRef.name must carry the Backup name reported by BackupJob.status.backupRef.name. See the consolidated comment.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/backups/kafka/06-restore-in-place.sh` around lines 37 - 38, Update
the backupRef.name value in the restore configuration to use the resolved Backup
name from BackupJob.status.backupRef.name rather than BACKUPJOB_NAME, preserving
the reference to the created Backup resource.
| backupRef: | ||
| name: ${BACKUPJOB_NAME} |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Reference the resolved Backup name, not the BackupJob name.
Same defect as step 06. See the consolidated comment.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/backups/kafka/07-restore-to-copy.sh` around lines 50 - 51, Update
the backupRef.name value in the restore manifest to reference the resolved
Backup resource name rather than BACKUPJOB_NAME, matching the correction made in
step 06.
| kubectl -n "$NAMESPACE" delete restorejob "$RESTOREJOB_TOCOPY_NAME" --ignore-not-found | ||
| kubectl -n "$NAMESPACE" delete restorejob "$RESTOREJOB_INPLACE_NAME" --ignore-not-found | ||
| kubectl -n "$NAMESPACE" delete backupjob "$BACKUPJOB_NAME" --ignore-not-found | ||
| kubectl -n "$NAMESPACE" delete backup "$BACKUPJOB_NAME" --ignore-not-found |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Delete the Backup by its resolved name.
This line assumes the Backup carries the BackupJob name. See the consolidated comment.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/backups/kafka/cleanup.sh` at line 14, Update the cleanup command to
delete the Backup using its resolved Backup resource name rather than assuming
it matches BACKUPJOB_NAME; reuse the variable or lookup established by the
script’s resolution flow, while preserving the namespace and ignore-not-found
options.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
examples/backups/kafka/01-create-strategy.sh (2)
1-14: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winCorrect the consistency guarantee.
The offset queries run sequentially. Records produced during the backup can be included in partitions whose end offsets are queried later. State that each partition has an independent frozen cut, not that all partitions are captured at one instant.
Proposed fix
-# to that offset, so everything produced during the backup is excluded and all -# partitions are captured as of the same instant. This is a logical DATA +# to that partition's offset. Each partition has an independent frozen cut. +# This is a logical DATA🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/backups/kafka/01-create-strategy.sh` around lines 1 - 14, Update the consistency-model comments in the script header to describe an independent frozen end-offset cut per partition: offsets are queried sequentially, and records produced before a partition’s query may be included while later-produced records are excluded. Remove the implication that all partitions are captured at one instant.
182-185: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftReplace the line-oriented record format with a lossless encoding.
kafka-console-consumer.shwriteskey<TAB>value<LF>without escaping.kafka-console-producer.shreads one line per record and splits at the first tab. Newlines create extra records, tabs in keys change the key/value boundary, null fields become the literalnull, and non-UTF-8 bytes are not preserved.
- At
examples/backups/kafka/01-create-strategy.sh#L182-L185, export key and value bytes with explicit null-state and length-delimited or Base64 encoding.- At
examples/backups/kafka/01-create-strategy.sh#L219-L224, decode that format before producing the original key and value bytes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/backups/kafka/01-create-strategy.sh` around lines 182 - 185, The Kafka backup pipeline currently uses an ambiguous line-oriented TSV format that cannot preserve binary data, embedded tabs/newlines, or null fields. At examples/backups/kafka/01-create-strategy.sh:182-185, replace the kafka-console-consumer.sh output with an explicit null-state and length-delimited or Base64 encoding; at examples/backups/kafka/01-create-strategy.sh:219-224, update the kafka-console-producer.sh input path to decode that format and reproduce the original key and value bytes exactly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@examples/backups/kafka/01-create-strategy.sh`:
- Around line 1-14: Update the consistency-model comments in the script header
to describe an independent frozen end-offset cut per partition: offsets are
queried sequentially, and records produced before a partition’s query may be
included while later-produced records are excluded. Remove the implication that
all partitions are captured at one instant.
- Around line 182-185: The Kafka backup pipeline currently uses an ambiguous
line-oriented TSV format that cannot preserve binary data, embedded
tabs/newlines, or null fields. At
examples/backups/kafka/01-create-strategy.sh:182-185, replace the
kafka-console-consumer.sh output with an explicit null-state and
length-delimited or Base64 encoding; at
examples/backups/kafka/01-create-strategy.sh:219-224, update the
kafka-console-producer.sh input path to decode that format and reproduce the
original key and value bytes exactly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 3bb30d64-2e2e-42e5-b69c-67a27ebfa59e
📒 Files selected for processing (1)
examples/backups/kafka/01-create-strategy.sh
VerdictLGTM with non-blocking notes Documentation/example scripts plus one appended Chainsaw round-trip Test; no Findings[MINOR] In the "back up all topics" branch (empty TLIST=$("${BIN}"/kafka-topics.sh --bootstrap-server "${BOOT}" --list | grep -v '^_' || true)The script body runs under raw=$("${BIN}"/kafka-topics.sh --bootstrap-server "${BOOT}" --list) # fails closed on broker error
TLIST=$(printf '%s\n' "$raw" | grep -v '^_' || true) # empty = genuinely no user topicsNovelty pass — second independent reviewer (findings verified against the code)A second reviewer given no checklist raised the items below; each was re-verified against the code. Neither can produce a false GREEN e2e ( [MINOR] The Chainsaw test's comment claims a fail-fast property the kafka helpers do not implement. [MINOR] The Claim mismatches[UNVERIFIABLE] "Verified end to end on a live single-broker cluster … both restores returned 30 records" — cannot be re-executed in a hermetic static review (no live cluster). Plausible and consistent with the merged [PARTIAL] "the original 8/11/11 partition distribution preserved" — the e2e only asserts total Caveats
Recommended follow-ups (doc precision, non-blocking)
|
In the empty-TOPICS (back up all non-internal topics) branch, the topic list was resolved with a single pipeline under set -eu without pipefail: TLIST=$(kafka-topics.sh --list | grep -v '^_' || true) The pipeline's exit status is grep's alone, so a real broker error (unreachable / auth) that emits no stdout makes grep exit 1, || true swallows it, and TLIST is empty - indistinguishable from 'no non-internal topics'. The backup then drains nothing, tars an empty dir, PUTs it and reports success: a silent empty backup a later restore would treat as authoritative. Split the --list into its own command substitution so a broker error aborts the Pod under set -e; filter internal topics only afterwards, so an empty TLIST means genuinely no user topics. Addresses the reviewer's fail-open note on PR #3580. Signed-off-by: Andrey Kolkov <andrey.kolkov@aenix.io>
… Job logs Two CI-quality notes from the PR #3580 review, both diagnostics/wall-clock only (neither can turn a run falsely green): - The chainsaw comment claimed run-all.sh fails fast on terminal BackupJob/RestoreJob phases and Stalled HelmReleases, but the kafka helpers never implemented it (carried over from mariadb). A BackupJob that settled phase=Failed was polled the full 600s and then mislabelled 'Timeout', burning ~10 of the 40 chainsaw minutes per already-red run. Port the mariadb variants: wait_for_field gains a terminal fail_value arg (call sites pass Failed), and a new wait_hr_ready fails fast on Stalled=True with an existence backstop. HR waits in steps 03/04/07 now use it; the Strimzi Kafka CR waits keep their genuine Ready condition. run-all.sh runs under set -e, so a non-zero step aborts the flow immediately, matching the comment. - The catch block only captured the Strimzi broker Pods (app.kubernetes.io/instance=kafka-kafka-test); the generic Job strategy runs the drain/replay in controller-created Pods labelled job.strategy.backups.cozystack.io/mode=backup|restore (never that instance label) and not TTL-reaped, so the actual S3/SigV4 error left no log in the report. Add a second podLogs selector keyed on that label. Signed-off-by: Andrey Kolkov <andrey.kolkov@aenix.io>
In the empty-TOPICS (back up all non-internal topics) branch, the topic list was resolved with a single pipeline under set -eu without pipefail: TLIST=$(kafka-topics.sh --list | grep -v '^_' || true) The pipeline's exit status is grep's alone, so a real broker error (unreachable / auth) that emits no stdout makes grep exit 1, || true swallows it, and TLIST is empty - indistinguishable from 'no non-internal topics'. The backup then drains nothing, tars an empty dir, PUTs it and reports success: a silent empty backup a later restore would treat as authoritative. Split the --list into its own command substitution so a broker error aborts the Pod under set -e; filter internal topics only afterwards, so an empty TLIST means genuinely no user topics. Addresses the reviewer's fail-open note on PR #3580. Signed-off-by: Andrey Kolkov <andrey.kolkov@aenix.io>
… Job logs Two CI-quality notes from the PR #3580 review, both diagnostics/wall-clock only (neither can turn a run falsely green): - The chainsaw comment claimed run-all.sh fails fast on terminal BackupJob/RestoreJob phases and Stalled HelmReleases, but the kafka helpers never implemented it (carried over from mariadb). A BackupJob that settled phase=Failed was polled the full 600s and then mislabelled 'Timeout', burning ~10 of the 40 chainsaw minutes per already-red run. Port the mariadb variants: wait_for_field gains a terminal fail_value arg (call sites pass Failed), and a new wait_hr_ready fails fast on Stalled=True with an existence backstop. HR waits in steps 03/04/07 now use it; the Strimzi Kafka CR waits keep their genuine Ready condition. run-all.sh runs under set -e, so a non-zero step aborts the flow immediately, matching the comment. - The catch block only captured the Strimzi broker Pods (app.kubernetes.io/instance=kafka-kafka-test); the generic Job strategy runs the drain/replay in controller-created Pods labelled job.strategy.backups.cozystack.io/mode=backup|restore (never that instance label) and not TTL-reaped, so the actual S3/SigV4 error left no log in the report. Add a second podLogs selector keyed on that label. Signed-off-by: Andrey Kolkov <andrey.kolkov@aenix.io>
ed07141 to
f7fb5cf
Compare
In the empty-TOPICS (back up all non-internal topics) branch, the topic list was resolved with a single pipeline under set -eu without pipefail: TLIST=$(kafka-topics.sh --list | grep -v '^_' || true) The pipeline's exit status is grep's alone, so a real broker error (unreachable / auth) that emits no stdout makes grep exit 1, || true swallows it, and TLIST is empty - indistinguishable from 'no non-internal topics'. The backup then drains nothing, tars an empty dir, PUTs it and reports success: a silent empty backup a later restore would treat as authoritative. Split the --list into its own command substitution so a broker error aborts the Pod under set -e; filter internal topics only afterwards, so an empty TLIST means genuinely no user topics. Addresses the reviewer's fail-open note on PR #3580. Signed-off-by: Andrey Kolkov <andrey.kolkov@aenix.io>
… Job logs Two CI-quality notes from the PR #3580 review, both diagnostics/wall-clock only (neither can turn a run falsely green): - The chainsaw comment claimed run-all.sh fails fast on terminal BackupJob/RestoreJob phases and Stalled HelmReleases, but the kafka helpers never implemented it (carried over from mariadb). A BackupJob that settled phase=Failed was polled the full 600s and then mislabelled 'Timeout', burning ~10 of the 40 chainsaw minutes per already-red run. Port the mariadb variants: wait_for_field gains a terminal fail_value arg (call sites pass Failed), and a new wait_hr_ready fails fast on Stalled=True with an existence backstop. HR waits in steps 03/04/07 now use it; the Strimzi Kafka CR waits keep their genuine Ready condition. run-all.sh runs under set -e, so a non-zero step aborts the flow immediately, matching the comment. - The catch block only captured the Strimzi broker Pods (app.kubernetes.io/instance=kafka-kafka-test); the generic Job strategy runs the drain/replay in controller-created Pods labelled job.strategy.backups.cozystack.io/mode=backup|restore (never that instance label) and not TTL-reaped, so the actual S3/SigV4 error left no log in the report. Add a second podLogs selector keyed on that label. Signed-off-by: Andrey Kolkov <andrey.kolkov@aenix.io>
f7fb5cf to
46ab0e3
Compare
IvanHunters
left a comment
There was a problem hiding this comment.
Verdict
LGTM with non-blocking notes.
This is example scripts plus one appended Chainsaw Test. Nothing here touches a chart, package, controller, CRD, or values contract, so the upgrade / SSA / CRD-transition / vendored-rename / config-matrix regression classes don't apply, and I checked that against the envelope: 0 charts, 0 Go packages, 0 packages/ files, and the "make generate produces no diff" claim holds up. The scripts fail closed on the paths that matter, cleanup.sh deletes the right Backup object (Backup.Name == BackupJob.Name, internal/backupcontroller/jobstrategy_controller.go:348), and the e2e actually asserts on record count rather than passing vacuously.
The notes below are accuracy and coverage refinements. None of them block merge. Two are worth calling out first, though: the ^_ over-match and the unvalidated explicit-topic list both let a backup example silently produce an incomplete backup that still reports success, which is the one failure a backup example must not teach. Both are one-line fixes.
Findings
[MINOR] 01-create-strategy.sh:167, grep -v '^_' silently drops legal user topics in the default "all topics" mode
Kafka's internal topics are __consumer_offsets and __transaction_state, both double-underscore. This filter also drops every single-underscore name, so a user topic like _schemas (Confluent Schema Registry) or _events holds user data yet is silently excluded from the documented "every non-internal topic" default. The comment at line 155 ("bookkeeping topics start with _") carries the same imprecision. Use grep -v '^__'.
[MINOR] 01-create-strategy.sh:156-157, an explicit topics: value naming a missing topic yields a successful empty backup
The explicit-TOPICS branch takes the list verbatim with no existence check, unlike the empty branch right below it, which is deliberately fail-closed. kafka-get-offsets.sh on a nonexistent topic prints nothing and exits 0, so the loop writes no manifest entry, uploads a near-empty tarball, and the BackupJob reports Succeeded. A typo in the BackupClass topics parameter produces a "backup" that restores nothing, and you find out at restore time. (I reasoned the exit-0-on-missing-topic behaviour from the CLI, I did not run it here.) A pre-flight check that each requested topic exists, failing loudly otherwise, closes this.
[MINOR] 01-create-strategy.sh:139-140, one S3 key per app: a later backup overwrites the earlier one, but old Backup CRs stay Ready
The object key is ${SRC}/kafka-topics.tar, scoped only by app name. Take backup A, produce more data, take backup B, then run a RestoreJob referencing A: the restore succeeds and hands back B's data, because the key is the same. Nothing marks the older Backup CR as superseded. This matches the nats sibling's single-key pattern and may be forced by the engine (the template gets no per-backup identity in backup mode, .Backup is nil there), so I read it as a documentation gap rather than a code defect. The README should say that only the latest backup of an app is actually restorable through this strategy.
[MINOR] 06-restore-in-place.sh:22, the topic-deletion wait can false-positive on a transient broker error and hollow out what the e2e proves
Inside the Pod snippet (set -eu, no pipefail) the check is ! kafka-topics.sh ... --list | grep -qx "$TOPIC". If --list itself fails, its output is empty, grep exits 1, and the negation declares the topic deleted while it's still there with all its records. Restore then replays into the live topic, the count reaches 60, and because the verification is >= (next finding) it still passes. So the round-trip test can go green without ever exercising topic recreation. Add set -o pipefail to that snippet, or check --list's own exit status before trusting its output.
[MINOR] 06-restore-in-place.sh:49, 07-restore-to-copy.sh:65, the round-trip check is >= and total-only
Verification passes when count >= MESSAGE_COUNT over a partition-summed offset delta. That can't catch over-restore (duplicate records: a restore Pod retried mid-replay re-runs the whole producer, and --create --if-not-exists skips the topic, so records double) and it doesn't assert per-partition distribution. The PR's headline claim is partition-preserving replay ("8/11/11 distribution preserved"), but nothing in the scripts or the test asserts per-partition counts, so the murmur2 repartitioning this PR is about is never exercised by CI. Consider exact equality where the flow uses a fresh/empty topic, plus a per-partition kafka-get-offsets assertion.
[MINOR] 01-create-strategy.sh:189-194, the drain assumes end - begin equals the consumable-record count
--max-messages $((end - begin)) holds only for a non-compacted, non-transactional topic. On a compacted topic (sparse offsets) or one written by transactional producers (control markers occupy offsets but are never delivered), fewer records than end-begin are consumable, so kafka-console-consumer idles until --timeout-ms 120000 and exits non-zero, killing the Pod under set -e and failing the BackupJob. That's fail-closed, no silent partial tarball, which is the right direction. But the topic_message_count comment at 00-helpers.sh:190 ("exact ... even for a compacted or truncated topic") is about the offset-math helper, not the drain, and could mislead. A one-line README limitation that compacted/transactional topics are out of scope for this logical path settles it.
[MINOR] README.md:21 / 01-create-strategy.sh:227-232, null-key fidelity is understated
The README says null-key records "are not guaranteed to land in their original partition." It's worse than that. print.key=true renders a null key as the literal token null, and replay with parse.key=true turns it into the 4-byte string key "null". So a null-key record doesn't just move partition, it gains a non-null key, and every originally-null-key record collapses onto one partition; null values (tombstones) similarly become ordinary payloads. The demo uses keyed records only, so this isn't exercised, but the limitation text should say null-key/null-value records are not round-tripped faithfully. The pinned 3.8 image supports the null.marker property (KIP-810) if faithful round-tripping is wanted later.
[NIT] 01-create-strategy.sh:128-129, hand-rolled endpoint parse mishandles IPv6 / path-carrying endpoints
HOST="${HOSTPORT%%:*}" mis-splits an IPv6 literal like [::1]:8333, and any endpoint carrying a path prefix loses it, since OBJ_URL is rebuilt from the bare host. The BucketInfo host:port shape works; other legal endpoint forms don't. Demo-scope, worth a comment.
Claim mismatches
[PARTIAL] "every partition is captured as of the same instant" / "a coherent point-in-time view" (README.md:12, PR body). The --time -1/--time -2 reads sit inside the for t in $TLIST loop (01-create-strategy.sh:172-178), so each topic is frozen when its iteration starts, after the previous topics have drained. The cut is atomic within a topic, not across topics. Kafka gives no cross-topic guarantee anyway, but the prose generalizes to "every non-internal topic." Narrow it to per-topic consistency.
[PARTIAL] "recreates each topic with only its original partition count and replication factor" (README.md:18). The partition count is original (from the manifest). The replication factor is not: restore uses the static REPLICATION_FACTOR parameter, default 1 (01-create-strategy.sh:53-54, applied at :222), and the source RF is never captured. Restoring into a multi-broker cluster without setting the parameter recreates topics at RF=1, a silent durability downgrade. Say "the configured replication factor (default 1)", not "its original replication factor."
[PARTIAL] "partition distribution preserved" (PR body). Correct in the code path (topic recreated with original partition count, deterministic murmur2), but not asserted by the shipped e2e, which checks total record count only.
Caveats
- Mechanical fail-open sweep ran on all 10 shell files. Every
|| true/2>/dev/nullhit was verified non-gating:01-create-strategy.sh:167handles grep's no-match exit only (the deciding--listis a separate command underset -e);00-helpers.sh:77,110,116,126are poll-loop reads;00-helpers.sh:195-196|| exit 0returns empty and every caller rejects a non-numeric count (06:48,07:64);06:20--delete ... || trueis backstopped by the disappearance wait-loop. Clean, with the one exception that the wait-loop's own--listpipe is unguarded (finding above). - shellcheck SC2016 (
00-helpers.sh:175,194;06:19) and SC1091 (dynamicsource) are false positives: the single-quoted bodies are Pod snippets whose$VARmust stay unexpanded until they reach the in-Pod bash, and the sourced path resolves at runtime. No action. job-service-label-overlap: no overlap. The throwaway CLI pods usekubectl run(run=kafka-cli-<rand>), which doesn't match the Strimzi bootstrap Service selector; the strategy Job pod labels are stamped by the controller, out of this PR.- Sound on independent check:
.bucket-info.envis gitignored (examples/backups/*/.bucket-info.env); empty partitions are handled (no data file, manifest still records the partition count); a tab inside a value round-trips becauseparse.keysplits at the first separator only; the to-copy source-keyed S3 read via.Backup.ApplicationRef.Nameis correct; the curl 7.76.1--aws-sigv4port-stripping workaround is internally consistent.
Recommended follow-ups
grep -v '^__'and a topic-existence pre-check are the two cheapest, highest-value fixes: they stop the example from silently teaching a lossy backup.- Add a per-partition offset assertion (and exact equality where the flow uses a fresh topic) to the e2e, so the partition-preserving-replay claim is guarded and duplicate-on-retry is caught.
- Note the latest-only-restorable and compacted/transactional-out-of-scope limitations in the README.
Timofei Larkin (lllamnyp)
left a comment
There was a problem hiding this comment.
Thanks for this — the value here is bigger than the title suggests. The generic Job strategy has had unit tests and exactly one worked example (examples/backups/nats/) but no end-to-end coverage at all, and no Chainsaw suite drives it. kafka-2-backup-roundtrip is therefore the first e2e that exercises the generic driver for real: template rendering including .Mode/.Parameters/.Backup.ApplicationRef.Name, the parameter round-trip through the Backup's driverMetadata, and the to-copy retarget. The run bears it out — 452s, and the log shows 30 records seeded, backed up, the topic deleted, restored in place, then restored into a second cluster.
The design call also looks right to me. A logical frozen-end-offset drain is not the highest-fidelity Kafka backup available, but the README says so plainly and up front rather than burying it: no topic configs, no ACLs, no SCRAM users, no consumer-group offsets, offsets reassigned on restore, no headers or timestamps, and an explicit "for a faithful, offset-preserving Kafka backup, prefer a volume-snapshot strategy over this logical one". An example gets copied, so being told where it bites is most of what makes it safe to ship. The comment density throughout is well above average and the reasoning held up everywhere I cross-checked it against the controller.
Two things I'd like changed before merge, both small. The rest below are comments rather than blockers.
Blocking
1. Restore is not idempotent, and the assertion cannot see the failure mode. 01-create-strategy.sh:221-233 recreates topics with --create --if-not-exists (existing topics are left alone) and then replays every partition file unconditionally, so restoring into a topic that still holds data appends rather than replaces. That matters more than it looks, because buildJobStrategyBatchJob in internal/backupcontroller/jobstrategy_controller.go hard-codes backoffLimit: 2 — a restore Pod that dies partway through replay is retried, and the retry replays from the beginning. Both verifications then assert if (( count < MESSAGE_COUNT )), strictly less-than, so a restore that produced 45 or 60 records instead of 30 prints "In-place restore verified" and passes (06-restore-in-place.sh:49, 07-restore-to-copy.sh:65). Could we make both comparisons -eq, and add a line to the README's limitations saying restore appends into a non-empty topic and is safe only against an absent or empty one?
2. The roundtrip proves record counts, never record content. topic_message_count (00-helpers.sh:192) sums end - begin offsets per partition, and nothing ever consumes the records back. Meanwhile the README makes three fidelity claims the test does not check: keys survive, keyed records land in their original partition via the murmur2 partitioner, and per-partition order is preserved. A restore that replayed 30 records of garbage, or scrambled every key, passes today. The seeded sentinels are already perfect for this (k-<n> / order-<n>) — consuming the topic once and diffing a sorted list would close the gap in a few lines, and given that data fidelity is the entire subject of the example it seems worth having.
Non-blocking, roughly by weight
Helper Pods will time out on a first run outside CI. kafka_run (00-helpers.sh:157) uses kubectl run --restart=Never --rm -i, and kubectl run with attach waits for Running under --pod-running-timeout, whose default is 1m, with no override and no retry. CI never hits it because the Chainsaw test resolves KAFKA_IMAGE from the running strimzi-cluster-operator, so the image is already cached on the nodes. A user following the README gets the default quay.io/strimzi/kafka:0.45.0-kafka-3.8.0 pulled cold, which routinely takes longer than 60s, and step 04 dies with timed out waiting for the container to run before seeding anything. --pod-running-timeout=5m on the four call sites would cover it.
Helper Pods carry no securityContext at all. The passing CI log contains the evidence three times: Warning: would violate PodSecurity "restricted:latest": allowPrivilegeEscalation != false ..., unrestricted capabilities ..., runAsNonRoot != true ..., seccompProfile ... for each kafka-cli-* Pod. Warn-only in the e2e cluster, so the run stays green, but in a tenant that enforces restricted the demo cannot run at all. The strategy Pod is in better shape and its comment already acknowledges the runAsNonRoot gap; the helper Pods get no mention. Either --overrides with a securityContext, or a README line naming the requirement.
Null-key records come back with the literal key null. The dump uses --property print.key=true and Kafka's default formatter renders a null key as the string null; the replay reads that back through parse.key=true as a four-byte key (01-create-strategy.sh:190-192, 231-232). So such a record does not merely land in an unpredictable partition, which is what the README currently says — its key changes from null to "null", which also changes compaction semantics downstream. Worth restating that bullet in those terms.
No bound on the drain's local disk usage. WORK=/tmp/kafka-dump plus /tmp/kafka-topics.tar materialises the whole topic twice on the Pod's writable layer, and resources (01-create-strategy.sh:236-242) declares cpu and memory but no ephemeral-storage request or limit. A tenant copying this for a large topic fills the node's ephemeral storage and evicts unrelated Pods before the backup fails. A sizing caveat in the limitations section would earn its place.
Every backup of an app overwrites the previous one. The S3 key is ${SRC}/kafka-topics.tar with no run identifier (01-create-strategy.sh:139), so N Backup objects all resolve to the same tarball and restoring an older Backup silently restores current data. This is largely forced by the driver — at backup-render time the context carries only Application/Release/Mode/Parameters, and the Backup artifact is created after the Job completes, so the Pod has no unique handle — and the NATS example has the same shape. Still belongs in the limitations rather than being discovered later.
set -e fragility in the partition-count loop. [ "${mp}" -ge "${parts}" ] && parts=$((mp + 1)) as the last statement of a while read body (01-create-strategy.sh:217-220): if the final iteration's test is false, the loop returns non-zero and set -eu kills the restore Pod with no message. It cannot fire today because kafka-get-offsets emits partitions in ascending order, but if ... then ... fi is the same length and closes it.
The e2e never exercises the all-topics path. The BackupClass pins topics: "${TOPIC}" (02-create-backupclass.sh:33), so the --list-and-filter branch — exactly what "fix(kafka): fail closed when listing topics for an all-topics backup" addresses — has no coverage from the test that ships with it.
Smaller things: the hack/e2e-chainsaw/.chainsaw.yaml header enumerates the Tests that override the namespace to tenant-root as "postgres-2-, mariadb-2-, clickhouse-2-" and could pick up kafka-2-; every other script-based backup example ships the 90/91/92 scenario docs (clickhouse, nats, etcd, foundationdb) and this one has only the README, which may well be deliberate; and the default KAFKA_IMAGE pins 0.45.0 while the vendored operator chart is strimzi-kafka-operator 0.45.1-rc1, where the Chainsaw test already shows the drift-free way to resolve it.
On CI cost, for the record, since a backup roundtrip is the kind of thing that quietly grows the suite: hack/select-e2e.sh maps both examples/backups/kafka/* and hack/e2e-chainsaw/kafka/* to the single kafka suite, Chainsaw runs parallel: 1, and the measured 452s sits well inside the declared 40m budget, which is justified in-line as convention 6 asks. That reads as appropriately gated to me. Test hygiene follows the mariadb precedent closely and correctly — pre-clean before the run so a leftover Succeeded BackupJob in the shared namespace cannot instantly satisfy the wait, cleanup in an always-reached finally, wait_hr_ready failing fast on Stalled=True, wait_for_field failing fast on the terminal Failed phase, and a catch block selecting the strategy Pods by the real job.strategy.backups.cozystack.io/mode constant. The note in 05-create-backupjob.sh explaining why there is no intermediate "wait Running" gate is exactly the right instinct. I did not find anything I would expect to go red intermittently in CI.
Add examples/backups/kafka, a worked backup/restore of a Cozystack-managed Kafka application's topic data via the generic Job backup strategy - the same pattern as examples/backups/nats, with no purpose-built backup image: a stock Strimzi Kafka image (kafka-*.sh CLI + bash/curl/tar) runs one shell script that branches on .Mode. Consistency 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 is captured as of the same instant. Restore recreates each topic with its original partition count and replays every partition file; keyed records reproduce into their original partition via the default partitioner. This is deliberately a DATA-only backup: topic configs, ACLs, SCRAM users and consumer-group offsets are not captured, and re-produced records get fresh offsets. The README spells out the scope and points at a volume-snapshot strategy for faithful, offset-preserving backups. Add kafka-2-backup-roundtrip to hack/e2e-chainsaw/kafka, driving the example run-all.sh as the harness (Bucket -> source Kafka + seeded topic -> BackupJob -> in-place restore -> to-copy restore + verify) so the test and the documented flow cannot drift, mirroring mariadb-2-backup-roundtrip. The image is resolved from the running operator so the backup Pod reuses the cached Kafka image. Verified end to end on a live cluster: freeze cut orders:0 [0,8), orders:1 [0,11), orders:2 [0,11) = 30 records; in-place and to-copy restores both return 30 with the original 8/11/11 partition distribution. Assisted-By: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Andrey Kolkov <andrey.kolkov@aenix.io>
STRIMZI_KAFKA_IMAGES is a newline-separated version=image map with one entry per supported Kafka version. The old 'sed | tr -d [:space:]' collapsed every line into one string, producing an invalid multi-image reference (quay.io/...3.8.0quay.io/...3.8.1...) that failed 'kubectl run' with 'invalid reference format' and broke kafka-2-backup-roundtrip. Split on whitespace, keep image lines, strip the version prefix and take the last (newest) image so a single valid reference is exported. Signed-off-by: Andrey Kolkov <andrey.kolkov@aenix.io>
The Strimzi image ships curl 7.76.1, whose --aws-sigv4 omits a non-default port from the SigV4 canonical Host while still sending it in the Host header (a bug fixed in curl 7.87.0). seaweedfs recomputes the signature from the received host:port, so every PUT/GET to the :8333 in-cluster S3 Service the e2e harness targets was rejected with HTTP 403, failing kafka-2-backup-roundtrip at the backup upload. It passed on dev7 only because that run used a :443 ingress, where the port is absent from the Host anyway. Strip the port from the signed and sent URL and redirect the connection to the real port with --connect-to, so the signed, sent and server-side Host all agree on the bare host. Verified against the image's curl 7.76.1: the emitted SigV4 signature matches a portless canonical Host, and the request still reaches the :8333 listener. Correct on curl >= 7.87 too, which drops the default port from the canonical Host as well. Signed-off-by: Andrey Kolkov <andrey.kolkov@aenix.io>
In the empty-TOPICS (back up all non-internal topics) branch, the topic list was resolved with a single pipeline under set -eu without pipefail: TLIST=$(kafka-topics.sh --list | grep -v '^_' || true) The pipeline's exit status is grep's alone, so a real broker error (unreachable / auth) that emits no stdout makes grep exit 1, || true swallows it, and TLIST is empty - indistinguishable from 'no non-internal topics'. The backup then drains nothing, tars an empty dir, PUTs it and reports success: a silent empty backup a later restore would treat as authoritative. Split the --list into its own command substitution so a broker error aborts the Pod under set -e; filter internal topics only afterwards, so an empty TLIST means genuinely no user topics. Addresses the reviewer's fail-open note on PR #3580. Signed-off-by: Andrey Kolkov <andrey.kolkov@aenix.io>
… Job logs Two CI-quality notes from the PR #3580 review, both diagnostics/wall-clock only (neither can turn a run falsely green): - The chainsaw comment claimed run-all.sh fails fast on terminal BackupJob/RestoreJob phases and Stalled HelmReleases, but the kafka helpers never implemented it (carried over from mariadb). A BackupJob that settled phase=Failed was polled the full 600s and then mislabelled 'Timeout', burning ~10 of the 40 chainsaw minutes per already-red run. Port the mariadb variants: wait_for_field gains a terminal fail_value arg (call sites pass Failed), and a new wait_hr_ready fails fast on Stalled=True with an existence backstop. HR waits in steps 03/04/07 now use it; the Strimzi Kafka CR waits keep their genuine Ready condition. run-all.sh runs under set -e, so a non-zero step aborts the flow immediately, matching the comment. - The catch block only captured the Strimzi broker Pods (app.kubernetes.io/instance=kafka-kafka-test); the generic Job strategy runs the drain/replay in controller-created Pods labelled job.strategy.backups.cozystack.io/mode=backup|restore (never that instance label) and not TTL-reaped, so the actual S3/SigV4 error left no log in the report. Add a second podLogs selector keyed on that label. Signed-off-by: Andrey Kolkov <andrey.kolkov@aenix.io>
The backup roundtrip only summed offset counts, so a restore that replayed garbage, scrambled keys, or ran twice (the strategy Job's backoffLimit retry re-appends from the start) still passed. Both verifications also compared with `< MESSAGE_COUNT`, so an inflated count slipped through. Snapshot the source topic in step 04 as a per-partition, offset-ordered partition/key/value dump, and diff the restored topic against it in the in-place and to-copy steps: byte-equality proves keys, values, partition placement and per-partition ordering survived, not merely the count. Make both count checks exact. Document that restore appends into a non-empty topic and is safe only against an absent or empty one. Raise the helper Pods' --pod-running-timeout to 5m so a cold image pull on a first run outside CI does not abort before seeding. Assisted-By: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Andrey Kolkov <andrey.kolkov@aenix.io>
46ab0e3 to
ed3b808
Compare
|
Thanks for the thorough review. Both blockers addressed and the branch is rebased on fresh 1. Restore idempotency / blind assertion. Both verifications now compare exactly ( 2. Content, not just counts. Step 04 now snapshots the source topic as a per-partition, offset-ordered Also picked up the cold-pull non-blocker since the new The remaining non-blockers (helper-Pod securityContext, null-key rendered as literal |
Fold in the cheap items from review: document the caveats that make the example safe to copy - a null key round-trips as the literal string "null" (changing key and compaction identity), the drain stages the topic twice under /tmp with no ephemeral-storage bound, every backup of an app overwrites the same S3 key, and the Pods target warn-only Pod Security. Harden the all-topics partition-count loop: the final `[ ... ] && parts=...` returned non-zero under `set -e` when the last test was false, so use `if ... then ... fi`. Align the default KAFKA_IMAGE with the vendored operator (0.45.1-rc1) and list kafka-2- in the .chainsaw.yaml tenant-root header. Assisted-By: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Andrey Kolkov <andrey.kolkov@aenix.io>
|
Folded in the cheap non-blockers in
Left for a follow-up as discussed: enforced-PSA |
Fold in the cheap accuracy notes from review. The all-topics default filtered `^_`, which also drops single-underscore user topics such as `_schemas`, silently excluding user data from a backup that still reports success; filter `^__` (Kafka's internal topics are double-underscore). The in-place delete-wait ran `--list | grep` in a pod snippet under `set -eu` without pipefail, so a transient `--list` failure read as empty output and false-positived "deleted"; capture `--list` on its own and check its exit before trusting it. Sharpen the docs: the frozen-end-offset cut is per-topic, not cross-topic; restore uses the configured replication factor (default 1), not the source's; compacted and transactional topics are out of scope; and the endpoint parse is demo-scope (no IPv6 / path-carrying forms). Assisted-By: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Andrey Kolkov <andrey.kolkov@aenix.io>
|
Thanks IvanHunters — folded in the cheap accuracy/coverage notes across
Deferred (happy to take in a follow-up): the explicit- |
What this PR does
Adds a worked backup/restore example for a Cozystack-managed
Kafkaapplication's topic data, built on the genericJobbackup strategy — the same pattern asexamples/backups/nats, with no purpose-built backup image. A stock Strimzi Kafka image (thekafka-*.shCLI plusbash,curlandtar) runs one shell script that branches on{{ .Mode }}: on backup it freezes each partition's end offset and drains[begin, end)to a tarball itPUTs to S3; on restore itGETs the tarball, recreates the topics with their original partition count and replays every partition file. S3 access iscurl --aws-sigv4, so no extra client image is needed.The consistency model is a frozen-end-offset cut:
kafka-get-offsets --time -1is 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 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.
Also adds a
kafka-2-backup-roundtripChainsaw test tohack/e2e-chainsaw/kafka, which drives the examplerun-all.shas its harness (Bucket → source Kafka + a seeded 3-partition topic → BackupJob → in-place restore → to-copy restore + verify) so the test and the documented flow cannot drift, mirroringmariadb-2-backup-roundtrip. It resolves the Kafka image from the running operator so the backup Pod reuses the image already cached on the nodes.Verified end to end on a live single-broker cluster: the freeze cut captured
orders:0 [0,8),orders:1 [0,11),orders:2 [0,11)= 30 records; both the in-place restore and a to-copy restore into a freshly-provisioned empty cluster returned 30 records with the original 8/11/11 partition distribution preserved.Files:
examples/backups/kafka/— 11 files (00-helpers.sh,01..07,run-all.sh,cleanup.sh,README.md).hack/e2e-chainsaw/kafka/chainsaw-test.yaml— newkafka-2-backup-roundtripTest appended.No package sources (
values.yaml,values.schema.json,Chart.yaml, packageREADME.md) are touched, somake generateproduces no diff.Screenshots
N/A — no UI changes.
Downstream repositories
Walked the trigger map in
docs/agents/contributing.mdfile-by-file against the diff. The change adds example scripts underexamples/backups/kafkaand appends one Chainsaw Test to an existing file underhack/e2e-chainsaw/kafka— it adds no package underpackages/appsorpackages/extra, changes no CRD, noApplicationDefinition, novalues.schema.json, no platform/installer values, nohack/*.mkanchor or make-target behaviour, and no node prerequisites inhack/e2e-prepare-cluster.bats. None of the downstream triggers match.Release note
Summary by CodeRabbit
New Features
Documentation
Tests