Skip to content

feat(backups): implement Job backup strategy - #1721

Merged
Timofei Larkin (lllamnyp) merged 3 commits into
mainfrom
feat/jobdriver-implementation
May 27, 2026
Merged

Timofei Larkin (lllamnyp) merged 3 commits into
mainfrom
feat/jobdriver-implementation

Conversation

@lllamnyp

@lllamnyp Timofei Larkin (lllamnyp) commented Dec 12, 2025 •

Copy link
Copy Markdown
Member

What

Implements the strategy.backups.cozystack.io/Job backup strategy — a generic, application-agnostic driver that runs a user-supplied Kubernetes Job to create (and restore) a backup.

Background

This PR began as a draft written against an earlier shape of the backups API. The API has since moved to a BackupClass-based dispatch model, which broke the original draft on three counts:

  • BackupJob.Spec.StrategyRef and BackupJob.Spec.StorageRef were removed; the strategy and its parameters are now resolved from a BackupClass into a ResolvedBackupConfig.
  • Strategy handling moved from standalone per-strategy reconcilers to a dispatch model: BackupJobReconciler.Reconcile / RestoreJobReconciler.Reconcile resolve the strategy and switch into per-strategy methods (reconcileVelero, reconcileCNPG, reconcileAltinity, …).
  • The draft's template context (.Values, .Storage) diverged from the convention the other strategies adopted, and its .Storage half targeted a storage API that does not exist.

What changed

  • reconcileJob / reconcileJobRestore (internal/backupcontroller/jobstrategy_controller.go): replaces the standalone BackupJobStrategyReconciler with the two dispatch methods the BackupJob / RestoreJob reconcilers call. The Job strategy is the generic counterpart of the Altinity driver — same lifecycle (render PodTemplateSpec → run a one-shot batch/v1.Job → poll its terminal condition → emit a Backup), minus the ClickHouse-specific applicationRef gate, so it runs for any application. Shared package helpers (renderJobTemplate, jobConditionState, getApplicationUnstructured, status helpers, deterministic Job naming, controller refs) are reused.
  • Template context follows the in-tree convention used by the other drivers: .Application (live application object), .Release.Name / .Release.Namespace, .Mode (backup / restore), .Parameters (resolved BackupClass parameters), and .Backup (restore runs only). Strategy parameters round-trip through the Backup's driverMetadata so a later restore re-renders against the same values.
  • Restore re-renders the same template with .Mode == "restore". Cross-namespace restore is unsupported (the RestoreJob target reference is namespace-local), matching the Altinity driver.
  • JobSpec.Template doc + regenerated CRD: the field doc previously advertised .Values; it now documents the real context, and strategy.backups.cozystack.io_jobs.yaml is regenerated to match (the change is limited to that field's description).
  • Tests (jobstrategy_controller_test.go): backup-Job creation / args / labels / status, completion → Backup artifact with parameter round-trip, Job-failure surfacing, missing-strategy terminal failure, restore into the target namespace, restore completion, and the parameter-prefix unit. A deliberately generic application kind pins that the driver never gates on applicationRef.kind.

No RBAC changes are required: the controller already holds strategy.backups.cozystack.io/*, batch/jobs, apps.cozystack.io/*, and backups permissions.

Notes for reviewers

  • Backup and restore share one template, so a strategy author's PodTemplateSpec must branch on .Mode to do anything restore-specific. This matches the existing convention; worth confirming it's the intended contract for the Job strategy's docs/examples.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Implemented full backup/restore job strategy with deterministic Job creation, idempotent execution, and status/condition updates; added demo example scripts and reusable shell helpers for a NATS JetStream backup/restore workflow.
  • Documentation

    • Clarified Job template rendering context and listed supported template variables; added README and step-by-step scenario docs for the NATS example.
  • Tests

    • Added comprehensive unit tests covering job creation, completion/failure handling, parameter extraction, and unmappable-target failure cases.

Review Change Stack

@coderabbitai

coderabbitai Bot commented Dec 12, 2025 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Implements Job strategy reconciliation for backups and restores: documents template rendering context, adds controller logic to render templates and manage deterministic batch Jobs, persists Backup artifacts with driver-parameter round-trip, and adds comprehensive unit tests plus NATS example/demo scripts and docs.

Changes

Job Strategy Controller Implementation

Layer / File(s) Summary
API Contracts and Documentation
api/backups/strategy/v1alpha1/job_types.go, packages/system/backupstrategy-controller/definitions/strategy.backups.cozystack.io_jobs.yaml
JobSpec.Template and CRD spec.template comments updated to enumerate template context keys available at render time: .Application, .Release.Name/.Release.Namespace, .Mode, .Parameters, and restore-only .Backup.*.
Implementation Foundation
internal/backupcontroller/jobstrategy_controller.go (lines 1–65)
Adds job-strategy constants and jobStrategyParameters to extract prefixed template parameters from Backup.Spec.DriverMetadata.
Backup Reconciliation Logic
internal/backupcontroller/jobstrategy_controller.go (lines 67–327)
BackupJobReconciler.reconcileJob implements phase gating, StartedAt init (refetch+patch+requeue), strategy/app resolution, template rendering to PodTemplateSpec, idempotent owned batch/v1.Job creation with labels/controller reference, Job status mapping to BackupJob conditions, and Backup artifact creation embedding strategy ref and driver metadata parameters. Helpers: ensureJobStrategyJob, buildJobStrategyBatchJob, createJobBackupArtifact.
Restore Reconciliation Logic
internal/backupcontroller/jobstrategy_controller.go (lines 329–516)
RestoreJobReconciler.reconcileJobRestore resolves effective restore target (source Backup override + APIGroup), performs StartedAt bookkeeping, fetches strategy and target app, renders restore-mode template using parameters from source Backup, ensures deterministic owned batch Job in restore namespace, and updates RestoreJob status on Job outcome. Includes ensureJobStrategyRestoreJob.
Comprehensive Test Coverage
internal/backupcontroller/jobstrategy_controller_test.go
New test harness and unit tests validating backup and restore flows: Job creation, label/arg rendering, phase transitions on success/failure, Backup artifact creation and parameter round-trip, missing-strategy and unmappable-kind failures, and jobStrategyParameters extraction behavior.
NATS example/demo
examples/backups/nats/*
Adds NATS JetStream demo: 00-helpers.sh, strategy Job manifest (01-create-strategy.sh), BackupClass, bucket provisioning, NATS provisioning, backup/restore job scripts (05–07), verification and helper scripts, README and scenario docs, plus run-all.sh and cleanup.sh to exercise the end-to-end flow.

Sequence Diagram(s)

sequenceDiagram
  participant BackupJobReconciler
  participant JobStrategy as JobStrategy_CR
  participant App as Target_Application
  participant BatchJob as batch/v1.Job
  participant Backup as Backup_Artifact

  BackupJobReconciler->>BackupJobReconciler: phase short-circuit check
  BackupJobReconciler->>BackupJobReconciler: set StartedAt (refetch + patch)
  BackupJobReconciler->>JobStrategy: fetch strategy CR
  BackupJobReconciler->>App: fetch target application
  BackupJobReconciler->>BackupJobReconciler: render template (.Application, .Mode, .Parameters)
  BackupJobReconciler->>BatchJob: ensure deterministic owned batch/v1.Job (mode/owner labels)
  BatchJob-->>BackupJobReconciler: Job status (Running/Failed/Succeeded)
  BackupJobReconciler->>Backup: on success create Backup CR with StrategyRef + driver metadata
Loading
sequenceDiagram
  participant RestoreJobReconciler
  participant SourceBackup as Source_Backup
  participant JobStrategy as JobStrategy_CR
  participant TargetApp as Target_Application
  participant BatchJob as batch/v1.Job

  RestoreJobReconciler->>RestoreJobReconciler: phase short-circuit check
  RestoreJobReconciler->>SourceBackup: extract application ref and parameters from DriverMetadata
  RestoreJobReconciler->>RestoreJobReconciler: resolve effective target (targetApplicationRef override)
  RestoreJobReconciler->>RestoreJobReconciler: set StartedAt (refetch + patch)
  RestoreJobReconciler->>JobStrategy: fetch strategy CR
  RestoreJobReconciler->>TargetApp: fetch resolved target application
  RestoreJobReconciler->>RestoreJobReconciler: render restore-mode template (.Application, .Mode, .Parameters, .Backup)
  RestoreJobReconciler->>BatchJob: ensure deterministic owned batch/v1.Job in restore namespace
  BatchJob-->>RestoreJobReconciler: Job status (Running/Failed/Succeeded)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Suggested labels

area/platform

Suggested reviewers

  • lexfrei
  • androndo
  • kvaps
  • sircthulhu
  • myasnikovdaniil

Poem

🐰 I hopped through templates, labels, and pods,

rendered contexts, requeued the odds.
Jobs took their places, artifacts kept,
Tests nodded as controllers swept.
A crunchy carrot for backups well prepped.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'feat(backups): implement Job backup strategy' accurately and concisely describes the primary change: implementation of a generic Job-based backup strategy driver.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/jobdriver-implementation

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello Timofei Larkin (@lllamnyp), I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request lays the foundational groundwork for a robust backup system by implementing the BackupJobStrategyReconciler. This controller intelligently interprets BackupJob resources, dynamically retrieves related application and strategy definitions, and then leverages a newly introduced generic templating engine to construct and deploy Kubernetes batch/v1.Job resources. This design ensures a highly configurable and adaptable approach to managing application backups within the Kubernetes ecosystem.

Highlights

  • BackupJob Strategy Reconciler: Introduced the initial implementation of the BackupJobStrategyReconciler which processes BackupJob custom resources. This reconciler is responsible for orchestrating backup operations based on defined strategies.
  • Dynamic Resource Fetching: The reconciler now dynamically fetches associated application and strategy resources using Kubernetes' dynamic client and REST mapper, allowing for flexible interaction with various custom resource definitions.
  • Generic Templating Utility: A new generic templating utility has been added, enabling the dynamic population of fields within Kubernetes object specifications. This is crucial for creating flexible and configurable backup jobs.
  • Kubernetes Job Creation: The controller now creates Kubernetes batch/v1.Job resources. These jobs are constructed using a templated PodTemplateSpec derived from the JobStrategy object, allowing for customized backup execution.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new JobStrategy implementation for BackupJob resources, allowing the creation of Kubernetes Jobs based on templated PodTemplateSpec definitions. It also includes a generic templating utility to process Go template expressions within Kubernetes objects. The changes are a good step towards a flexible backup strategy. However, there are a few areas that could be improved for robustness and error handling, particularly regarding the templating logic and handling of API group references.

Comment on lines +19 to +25
templateFunc := func(in string) string {
out, err := template(in, templateContext)
if err != nil {
return in
}
return out
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The templateFunc silently swallows errors from template(in, templateContext), returning the original string in instead. This can lead to subtle bugs where template expressions are malformed or refer to non-existent fields, but no error is propagated, and the resulting object contains untemplated strings. It would be more robust to propagate these errors or at least log them with a higher severity to make debugging easier.

Suggested change
templateFunc := func(in string) string {
out, err := template(in, templateContext)
if err != nil {
return in
}
return out
}
templateFunc := func(in string) string {
out, err := template(in, templateContext)
if err != nil {
// TODO: Log this error or return it from Template function for better error visibility
return in
}
return out
}

Comment on lines +56 to +64
if j.Spec.ApplicationRef.APIGroup != nil {
applicationRefAPIGroup = *j.Spec.ApplicationRef.APIGroup
}
if j.Spec.StrategyRef.APIGroup != nil {
strategyRefAPIGroup = *j.Spec.StrategyRef.APIGroup
}
if j.Spec.StorageRef.APIGroup != nil {
storageRefAPIGroup = *j.Spec.StorageRef.APIGroup
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The APIGroup fields in TypedLocalObjectReference are pointers to strings. If they are nil, the current logic initializes applicationRefAPIGroup, strategyRefAPIGroup, and storageRefAPIGroup to empty strings. While an empty string for APIGroup typically refers to the core Kubernetes API group, this behavior is implicit and not explicitly handled or documented. It might be clearer to explicitly check for nil and assign "" if that's the intended behavior for core API group resources, or handle it as an error if APIGroup is always expected to be set for non-core resources.

var applicationRefAPIGroup string
var strategyRefAPIGroup string
var storageRefAPIGroup string
if j.Spec.ApplicationRef.APIGroup != nil {
	applicationRefAPIGroup = *j.Spec.ApplicationRef.APIGroup
} else {
	// Default to core API group if not specified
	applicationRefAPIGroup = ""
}
if j.Spec.StrategyRef.APIGroup != nil {
	strategyRefAPIGroup = *j.Spec.StrategyRef.APIGroup
} else {
	strategyRefAPIGroup = ""
}
if j.Spec.StorageRef.APIGroup != nil {
	storageRefAPIGroup = *j.Spec.StorageRef.APIGroup
} else {
	storageRefAPIGroup = ""
}

Comment on lines +105 to +108
values, ok := app.Object["spec"].(map[string]any)
if !ok {
values = map[string]any{}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The type assertion app.Object["spec"].(map[string]any) and subsequent fallback to an empty map if the assertion fails could mask potential issues. If the application object's spec is expected to always be a map[string]any, a failure here might indicate a malformed resource or an unexpected API version. It would be safer to log a warning or return an error in such cases, rather than silently proceeding with an empty map, which could lead to incorrect template rendering.

values, ok := app.Object["spec"].(map[string]any)
if !ok {
	log.Info("Application object 'spec' field is not a map[string]any, using empty values", "application", client.ObjectKeyFromObject(app))
	values = map[string]any{}
}

Base automatically changed from feat/backup-jobdriver-api to main December 15, 2025 06:53
@github-actions github-actions Bot added do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress and removed do-not-merge labels Apr 27, 2026
@lllamnyp
Timofei Larkin (lllamnyp) force-pushed the feat/jobdriver-implementation branch from 15ce380 to 89c72b0 Compare May 22, 2026 08:38
@github-actions github-actions Bot added area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review size/XXL This PR changes 1000+ lines, ignoring generated files labels May 22, 2026
@lllamnyp Timofei Larkin (lllamnyp) changed the title [backups][WIP] Draft Job strategy implementation feat(backups): implement Job backup strategy May 22, 2026
@github-actions github-actions Bot added area/storage Issues or PRs related to storage (linstor, seaweedfs, bucket, velero, harbor) kind/feature Categorizes issue or PR as related to a new feature labels May 22, 2026
Complete the strategy.backups.cozystack.io/Job driver as the generic,
app-agnostic counterpart to the Altinity driver: render the strategy
PodTemplateSpec, run it as a one-shot batch/v1.Job, poll its terminal
condition, and emit a Backup. Wire it into the existing BackupClass
dispatch via reconcileJob/reconcileJobRestore rather than a standalone
reconciler, matching the post-BackupClass backups API (the old draft
read BackupJob.Spec.StrategyRef/StorageRef, which no longer exist).

Template context follows the in-tree convention (.Application, .Release,
.Mode, .Parameters, .Backup); the JobSpec.Template doc and regenerated
CRD are updated to match. Restore re-renders the same template with
.Mode=restore; cross-namespace restore is unsupported, mirroring
Altinity.

Signed-off-by: Timofei Larkin <[email protected]>
@lllamnyp
Timofei Larkin (lllamnyp) force-pushed the feat/jobdriver-implementation branch from 89c72b0 to 0291149 Compare May 22, 2026 13:46
@lllamnyp
Timofei Larkin (lllamnyp) marked this pull request as ready for review May 24, 2026 07:04
@dosubot dosubot Bot added the kind/api-change Categorizes issue or PR as related to adding, removing, or otherwise changing an API label May 24, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements the generic Job strategy for backups and restores, enabling app-agnostic backup tools to be executed as Kubernetes batch jobs. The implementation includes controller logic for template rendering, job lifecycle management, and backup artifact creation, along with comprehensive unit tests and updated CRD documentation. Feedback identifies opportunities to normalize application references to avoid potential lookup failures and suggests making the hardcoded job backoff limit configurable for improved flexibility.

return ctrl.Result{}, err
}

app, err := r.getApplicationUnstructured(ctx, j.Namespace, j.Spec.ApplicationRef)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The ApplicationRef should be normalized to ensure the default API group (apps.cozystack.io) is used if it's missing in the spec. While the caller in backupjob_controller.go performs normalization for resolving the backup class, it passes the original unnormalized object j to this method. This could lead to NotFound errors if getApplicationUnstructured expects a fully qualified group.

Suggested change
app, err := r.getApplicationUnstructured(ctx, j.Namespace, j.Spec.ApplicationRef)
app, err := r.getApplicationUnstructured(ctx, j.Namespace, backupsv1alpha1.NormalizeApplicationRef(j.Spec.ApplicationRef))

Comment on lines +362 to +366
targetRef := corev1.TypedLocalObjectReference{
APIGroup: stringPtr(targetAPIGroup),
Kind: targetAppKind,
Name: targetAppName,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to the backup path, the targetRef should be normalized to ensure the default API group is correctly populated if missing. This ensures that getApplicationUnstructured and the subsequent template rendering use the correct fully qualified group.

Suggested change
targetRef := corev1.TypedLocalObjectReference{
APIGroup: stringPtr(targetAPIGroup),
Kind: targetAppKind,
Name: targetAppName,
}
targetRef := backupsv1alpha1.NormalizeApplicationRef(corev1.TypedLocalObjectReference{
APIGroup: stringPtr(targetAPIGroup),
Kind: targetAppKind,
Name: targetAppName,
})

for k, v := range labels {
pod.Labels[k] = v
}
backoffLimit := int32(2)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

The backoffLimit is hardcoded to 2. While this matches the Altinity driver, for a generic Job strategy, it might be beneficial to allow this to be configurable via BackupClass parameters or the Job spec in the future, as different backup tools might have different retry requirements.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 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 `@internal/backupcontroller/jobstrategy_controller_test.go`:
- Around line 189-190: The test assertion prints entire []string slices with %q;
change the t.Errorf calls that compare containerArgs and args to report the
specific element values (use containerArgs[i] and args[i]) and guard against
out-of-range by substituting "<missing>" when an index is absent; update both
the backup check (using variables containerArgs and args around the existing
t.Errorf) and the similar restore-args check near the other assertion (~line
436) so failures show the specific element instead of the whole slice.
- Around line 62-64: The test setup currently ignores errors returned by
scheme.AddToScheme, backupsv1alpha1.AddToScheme, and
strategyv1alpha1.AddToScheme—update the test initialization to check each error
and call t.Fatalf with the error if non-nil (e.g., if err :=
scheme.AddToScheme(testScheme); err != nil { t.Fatalf("...: %v", err) }) so
failures fail fast; additionally, replace use of %q for slice values in t.Errorf
calls that log containerArgs and args (at the assertions around
containerArgs/args) with %v or use strings.Join(slice, " ") so the output is
readable (update the t.Errorf format strings that reference containerArgs and
args accordingly).
🪄 Autofix (Beta)

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

Run ID: 81d3c182-bade-4de5-abef-b3cca7432007

📥 Commits

Reviewing files that changed from the base of the PR and between debb9f8 and 0291149.

📒 Files selected for processing (4)
  • api/backups/strategy/v1alpha1/job_types.go
  • internal/backupcontroller/jobstrategy_controller.go
  • internal/backupcontroller/jobstrategy_controller_test.go
  • packages/system/backupstrategy-controller/definitions/strategy.backups.cozystack.io_jobs.yaml

Comment on lines +62 to +64
_ = scheme.AddToScheme(testScheme)
_ = backupsv1alpha1.AddToScheme(testScheme)
_ = strategyv1alpha1.AddToScheme(testScheme)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify ignored AddToScheme errors in this test file
rg -n '^\s*_\s*=\s*(scheme\.AddToScheme|backupsv1alpha1\.AddToScheme|strategyv1alpha1\.AddToScheme)\(' internal/backupcontroller/jobstrategy_controller_test.go

Repository: cozystack/cozystack

Length of output: 199


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="internal/backupcontroller/jobstrategy_controller_test.go"

# Show the scheme setup region (context around 62-64)
sed -n '45,90p' "$FILE"

# Locate t.Errorf usages with %q in the file
rg -n 't\.Errorf\([^\)]*%q' "$FILE"

# Show surrounding context for the specific line numbers mentioned in the review comment (best-effort)
sed -n '170,220p' "$FILE"
sed -n '410,470p' "$FILE"

Repository: cozystack/cozystack

Length of output: 7307


Fail fast on scheme registration errors in test setup

  • internal/backupcontroller/jobstrategy_controller_test.go:62-64 currently ignores returned errors from scheme.AddToScheme, backupsv1alpha1.AddToScheme, and strategyv1alpha1.AddToScheme; use t.Fatalf on any error.
Suggested patch
 	testScheme := runtime.NewScheme()
-	_ = scheme.AddToScheme(testScheme)
-	_ = backupsv1alpha1.AddToScheme(testScheme)
-	_ = strategyv1alpha1.AddToScheme(testScheme)
+	if err := scheme.AddToScheme(testScheme); err != nil {
+		t.Fatalf("add core scheme: %v", err)
+	}
+	if err := backupsv1alpha1.AddToScheme(testScheme); err != nil {
+		t.Fatalf("add backups v1alpha1 scheme: %v", err)
+	}
+	if err := strategyv1alpha1.AddToScheme(testScheme); err != nil {
+		t.Fatalf("add strategy v1alpha1 scheme: %v", err)
+	}
  • Around internal/backupcontroller/jobstrategy_controller_test.go:189 and ~436, t.Errorf uses %q for slice values (containerArgs/args of type []string); switch to %v (or strings.Join) so failure output is readable.
📝 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.

Suggested change
_ = scheme.AddToScheme(testScheme)
_ = backupsv1alpha1.AddToScheme(testScheme)
_ = strategyv1alpha1.AddToScheme(testScheme)
testScheme := runtime.NewScheme()
if err := scheme.AddToScheme(testScheme); err != nil {
t.Fatalf("add core scheme: %v", err)
}
if err := backupsv1alpha1.AddToScheme(testScheme); err != nil {
t.Fatalf("add backups v1alpha1 scheme: %v", err)
}
if err := strategyv1alpha1.AddToScheme(testScheme); err != nil {
t.Fatalf("add strategy v1alpha1 scheme: %v", err)
}
🤖 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 `@internal/backupcontroller/jobstrategy_controller_test.go` around lines 62 -
64, The test setup currently ignores errors returned by scheme.AddToScheme,
backupsv1alpha1.AddToScheme, and strategyv1alpha1.AddToScheme—update the test
initialization to check each error and call t.Fatalf with the error if non-nil
(e.g., if err := scheme.AddToScheme(testScheme); err != nil { t.Fatalf("...:
%v", err) }) so failures fail fast; additionally, replace use of %q for slice
values in t.Errorf calls that log containerArgs and args (at the assertions
around containerArgs/args) with %v or use strings.Join(slice, " ") so the output
is readable (update the t.Errorf format strings that reference containerArgs and
args accordingly).

Comment on lines +189 to +190
t.Errorf("rendered args[%d]: want %q, got %q", i, want, containerArgs)
break

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify potentially incorrect %q usage with []string values in test error formatting
rg -nP 't\.Errorf\(".*got %q".*,\s*(containerArgs|args)\s*\)' internal/backupcontroller/jobstrategy_controller_test.go

Repository: cozystack/cozystack

Length of output: 215


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '160,215p' internal/backupcontroller/jobstrategy_controller_test.go | nl -ba -w2 -s':'
echo '---'
sed -n '410,465p' internal/backupcontroller/jobstrategy_controller_test.go | nl -ba -w2 -s':'

Repository: cozystack/cozystack

Length of output: 106


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '160,215p' internal/backupcontroller/jobstrategy_controller_test.go | cat -n -w2 -s':'
echo '---'
sed -n '410,465p' internal/backupcontroller/jobstrategy_controller_test.go | cat -n -w2 -s':'

Repository: cozystack/cozystack

Length of output: 129


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== section 160-215 =="
sed -n '160,215p' internal/backupcontroller/jobstrategy_controller_test.go | cat -n

echo "== section 410-465 =="
sed -n '410,465p' internal/backupcontroller/jobstrategy_controller_test.go | cat -n

Repository: cozystack/cozystack

Length of output: 5311


Fix %q formatting of slice values in assertion failures.

containerArgs and args are []string, but the failure message currently passes the whole slice to %q (noisy/less actionable). Report the specific element containerArgs[i] / args[i] (or "<missing>" when out of range) instead.

File: internal/backupcontroller/jobstrategy_controller_test.go
Lines: 189-190 (also applies to the restore args check at line ~436)

Suggested patch
 	for i, want := range wantArgs {
 		if i >= len(containerArgs) || containerArgs[i] != want {
-			t.Errorf("rendered args[%d]: want %q, got %q", i, want, containerArgs)
+			got := "<missing>"
+			if i < len(containerArgs) {
+				got = containerArgs[i]
+			}
+			t.Errorf("rendered args[%d]: want %q, got %q", i, want, got)
 			break
 		}
 	}
@@
 	for i, w := range want {
 		if i >= len(args) || args[i] != w {
-			t.Errorf("rendered restore args[%d]: want %q, got %q", i, w, args)
+			got := "<missing>"
+			if i < len(args) {
+				got = args[i]
+			}
+			t.Errorf("rendered restore args[%d]: want %q, got %q", i, w, got)
 		}
 	}
📝 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.

Suggested change
t.Errorf("rendered args[%d]: want %q, got %q", i, want, containerArgs)
break
got := "<missing>"
if i < len(containerArgs) {
got = containerArgs[i]
}
t.Errorf("rendered args[%d]: want %q, got %q", i, want, got)
break
🤖 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 `@internal/backupcontroller/jobstrategy_controller_test.go` around lines 189 -
190, The test assertion prints entire []string slices with %q; change the
t.Errorf calls that compare containerArgs and args to report the specific
element values (use containerArgs[i] and args[i]) and guard against out-of-range
by substituting "<missing>" when an index is absent; update both the backup
check (using variables containerArgs and args around the existing t.Errorf) and
the similar restore-args check near the other assertion (~line 436) so failures
show the specific element instead of the whole slice.

@lexfrei Aleksei Sviridkin (lexfrei) removed the area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review label May 25, 2026
@androndo

Copy link
Copy Markdown
Contributor
 NOT LGTM

 The driver is overwhelmingly a solid mirror of the Altinity driver — the StartedAt patch dance is correctly
 reproduced, apimeta.IsNoMatchError is properly classified as terminal (the one place the Job driver legitimately
 diverges from Altinity, since it has no kind gate), idempotent Job creation handles AlreadyExists, controllerRef
 gives kube-gc the right hook, and the test surface for the changed code is genuinely good. But there are blocking
 issues that need addressing before merge.

 Blocking findings

 1. renderJobTemplate is documented to expose .Application to the template, but no test ever exercises it.

 The CRD description (the user-visible contract) advertises:

 ▎ .Application — the live application object (e.g. .Application.spec.replicas)

 Every other context key (.Release.Name, .Mode, .Parameters, .Backup.ApplicationRef.Name) has at least one assertion
  in the test suite. .Application has none. The driver is the only mechanism by which a generic backup tool can read
  app spec — .Parameters carries BackupClass static config, not the live object — so this is the load-bearing
 template key for any non-trivial Job-strategy use case (e.g. --shards={{ .Application.spec.shards }},
 --password-from-secret={{ .Application.spec.users.admin.passwordSecretRef.name }}). If a refactor of
 renderJobTemplate (it's in altinitystrategy_controller.go:108) ever drops .Application from the context map, no
 test in this file will catch it. Add a template arg like --replicas={{ .Application.spec.replicas }} to one of the
 strategies in the existing tests and assert the rendered value.

 2. examples/backups/job/ does not exist, and there is no e2e for the Job driver.

 Per the bundled-PR convention recorded for this codebase, a new backup driver lands together with its examples and
 an e2e that exercises the full BackupClass → BackupJob → Backup → RestoreJob flow. Every other driver in
 examples/backups/ (clickhouse, mariadb, postgres, foundationdb, vmi) ships numbered example scripts and the
 dispatch path is exercised against a real cluster. The Job driver has neither, and unit tests against the fake
 client cannot prove that the dispatch hook in backupjob_controller.go:78 actually routes a real
 BackupClass-resolved Job strategy end-to-end — that contract lives between running components in a real cluster,
 not between unit tests and a faked dynamic client. Land the examples + e2e in this same PR.

 3. Test pollution: TestReconcileJob_FailsOnMissingStrategy doesn't seed the application object the way the
 success-path tests do.

 In newJobStrategyTestEnv the application is passed in unconditionally
 (dynamicfake.NewSimpleDynamicClientWithCustomListKinds(... , app)), so an application named app-test exists. But
 the test exercises a code path that must short-circuit before reaching getApplicationUnstructured (the strategy
 lookup at jobstrategy_controller.go:121 is supposed to fire first). If that ordering ever flips — say, somebody
 factors out the app-fetch above the strategy-fetch — the test silently keeps passing because the seeded app
 satisfies the dynamic-client call. Either drop the app from the env for this test, or add an explicit assertion
 that the failure Message references the strategy name ("Job strategy not found: missing-strategy"), so a
 re-ordering surfaces in the error text. Same nit applies to TestReconcileJob_FailsOnUnmappableKind: the failure
 message check is updated.Status.Message == "", which would still pass if the wrong error path produced a populated
 message. Assert on the substring "kind not registered" or "kind=\"Generic\"".

 4. j.Status writes after Status().Patch in the same reconcile.

 In reconcileJob, after the StartedAt-patch path the function returns early with a requeue — good. But in the
 success branch (batchv1.JobComplete), the code mutates the local j and calls r.Status().Update(ctx, j) (line ~155
 of the diff). On the second reconcile (StartedAt already set), j is whatever the parent Reconcile handed in — and
 that parent fetches it from cache. If the cache lags behind the prior Status patch the parent did from outside this
  driver (it doesn't, today, but the assumption is fragile), Update will fail with Conflict. The Altinity driver has
  the identical shape so this is a pre-existing pattern, not a regression — but per the project's
 no-pre-existing-excuse rule, this PR touched the area and owns at least a comment documenting why the second-pass
 full Update is safe (or a switch to Status().Patch with MergeFrom, which is what the patch-StartedAt block already
 uses).

 5. BackoffLimit = 2 is hard-coded with no operator escape hatch.

 For an app-agnostic driver this is a real footgun. A clickhouse-backup invocation may want zero retries (each retry
  is expensive S3 I/O); a Velero-style sidecar wants more. The Altinity driver gets away with hard-coding it because
  Altinity is one tool. The Job driver is supposed to be the generic alternative for everything else. Either:
 - expose backoffLimit as a strategy parameter under JobSpec, OR
 - document in the CRD description that the rendered batch/v1.Job has backoffLimit=2 and cannot be overridden, so
 operators know to keep their image idempotent.

 The first is the right call long-term; the second is the minimum acceptable for this PR.

 6. The --mode={{ .Mode }} testing strategy is the only place restore vs backup is distinguished by the rendered
 container.

 Backups and restores produce a single batch/v1.Job per owning resource via jobNameForBackupJob and
 jobNameForRestoreJob. They live in different names but in the same j.Spec.Template.Spec.Containers. Without .Mode
 in the rendered args (which is operator-supplied — nothing forces a strategy author to add it), the operator cannot
  tell from looking at the running pod whether it's restoring or backing up. The driver already emits a
 jobStrategyLabelMode label on the Job, but does not propagate it down to the pod template's labels in
 buildJobStrategyBatchJob — wait, actually it does (lines 251–255 add labels into pod.Labels). Good, withdraw this —
  the pod has the mode label. But then the same corev1.PodTemplateSpec.Labels ends up in every rendered pod
 regardless of whether the strategy author thinks to template it, which means kubectl get pods -l
 job.strategy.backups.cozystack.io/mode=restore works for end users. That's fine — not a blocking issue,
 withdrawing.

 Recommended (non-blocking)

 - The two ensureJobStrategy*Job helpers (ensureJobStrategyJob and ensureJobStrategyRestoreJob) are byte-for-byte
 identical except the receiver and the error message on SetControllerReference. The Altinity driver has the same
 duplication — fine to keep for parallel symmetry, but a follow-up that extracts a free function
 ensureJobStrategyBatchJob(ctx, client, scheme, owner, ...) would halve the surface area across both drivers.
 - jobStrategyParameters is a function (good) but backupParameters for Altinity does the identical thing with a
 different prefix constant. A single extractDriverParameters(b, prefix) helper would prevent the next driver from
 copy-pasting the third variant of the same loop.
 - The package-doc comment on reconcileJob (jobstrategy_controller.go:69-77) says "The Altinity driver is a
 specialisation of this flow with a ClickHouse-specific applicationRef gate; the Job driver applies no such gate".
 Useful framing. Consider a one-line note on validateAltinityApplicationRef linking the two so a future maintainer
 cleaning up the gate doesn't accidentally remove the relationship.

 Reviewer pass on the CRD description
 cleaning up the gate doesn't accidentally remove the relationship.

 Reviewer pass on the CRD description

 The CRD description (packages/system/backupstrategy-controller/definitions/strategy.backups.cozystack.io_jobs.yaml)
  matches the Go doc comment byte for byte — good, that's a regen, not a hand-edit. The previous description claimed
  .Values (the helm-template name) was exposed; the new one correctly names .Application — this is the
 documented-API correction that motivates this PR's CRD diff.

Address review feedback on the Job backup strategy:

- Assert .Application is templated: the rendered backup Job now reads
  .Application.spec.replicas, the load-bearing context key that lets a
  generic backup tool read live app spec (previously only .Release,
  .Mode, .Parameters and .Backup had coverage).
- Pin failure-path messages so a reordered error path can't pass on the
  wrong branch: the missing-strategy test asserts the strategy name, and
  the unmappable-kind tests assert "kind not registered".
- Document why the full Status().Update in the success branches is safe
  (the object is freshly Get() each pass; the StartedAt write requeues).
- Note the fixed batch/v1.Job backoffLimit=2 and link the follow-up to
  make it configurable (#2742).

Signed-off-by: Timofei Larkin <[email protected]>
Add examples/backups/nats/, the first worked example of the generic Job
backup strategy. It backs up a NATS JetStream stream that has no dedicated
driver, using a stock natsio/nats-box image: 'nats stream backup' -> tar ->
curl --aws-sigv4 PUT to the COSI/seaweedfs S3 endpoint, and the reverse for
restore. One PodTemplateSpec serves both directions, branching at runtime on
{{ .Mode }} (the engine templates each string leaf independently and cannot
restructure the Pod per mode).

Mirrors the clickhouse example's structure: numbered scripts (bucket -> NATS
src + seeded stream -> strategy -> BackupClass -> BackupJob -> in-place and
to-copy RestoreJob), shared helpers, run-all.sh, README, and scenario docs.
Like every examples/backups/* dir it is a runnable demo, not CI-gated.

Signed-off-by: Timofei Larkin <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 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/nats/03-create-bucket.sh`:
- Around line 60-66: The current here-doc writes raw S3_* variables into
.bucket-info.env which can allow command injection when that file is sourced;
instead, escape or quote each value before writing: replace the heredoc that
creates "$SCRIPT_DIR/.bucket-info.env" with per-variable safe writes that use
shell-safe quoting (e.g. printf with %q or otherwise shell-escape each of
S3_ACCESS_KEY, S3_SECRET_KEY, S3_ENDPOINT, S3_REGION, S3_BUCKET) so the
resulting file contains exported, safely-escaped assignments (refer to the
variables S3_ACCESS_KEY, S3_SECRET_KEY, S3_ENDPOINT, S3_REGION, S3_BUCKET and
the target file .bucket-info.env).

In `@examples/backups/nats/06-restore-in-place.sh`:
- Around line 23-24: The restore YAML is using the BackupJob name variable
`${BACKUPJOB_NAME}` but must reference the actual Backup object name at
`BackupJob.status.backupRef.name`; update the script so it queries the BackupJob
status (e.g., `kubectl get backupjob $BACKUPJOB_NAME -o
jsonpath='{.status.backupRef.name}'` or equivalent) stores that value into a
variable like `BACKUP_NAME`, and then set `RestoreJob.spec.backupRef.name` to
that resolved `BACKUP_NAME` instead of `${BACKUPJOB_NAME}` so the RestoreJob
points to the real Backup resource.

In `@examples/backups/nats/cleanup.sh`:
- Around line 4-24: The script currently uses set -uo pipefail (missing -e) so
failures won’t abort and log_success at the end can report success even if
deletes failed; change the top options to include -e (set -euo pipefail) so the
script exits on any command error, and ensure critical cleanup commands (the
kubectl delete invocations that operate on
restorejob/backupjob/backup/nats/bucket/backupclass/job.strategy.backups.cozystack.io
and the rm) will cause the script to fail rather than reach
print_header/log_success; keep --ignore-not-found where intentional but rely on
-e for real failures so print_header and log_success reflect actual success.
🪄 Autofix (Beta)

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

Run ID: 7463f7c4-a5a6-4438-8d60-3ceb77259301

📥 Commits

Reviewing files that changed from the base of the PR and between 5029af6 and 727343d.

📒 Files selected for processing (14)
  • examples/backups/nats/00-helpers.sh
  • examples/backups/nats/01-create-strategy.sh
  • examples/backups/nats/02-create-backupclass.sh
  • examples/backups/nats/03-create-bucket.sh
  • examples/backups/nats/04-create-nats.sh
  • examples/backups/nats/05-create-backupjob.sh
  • examples/backups/nats/06-restore-in-place.sh
  • examples/backups/nats/07-restore-to-copy.sh
  • examples/backups/nats/90-scenario-admin-prepare.md
  • examples/backups/nats/91-scenario-user-backup.md
  • examples/backups/nats/92-scenario-user-restore.md
  • examples/backups/nats/README.md
  • examples/backups/nats/cleanup.sh
  • examples/backups/nats/run-all.sh
✅ Files skipped from review due to trivial changes (4)
  • examples/backups/nats/91-scenario-user-backup.md
  • examples/backups/nats/90-scenario-admin-prepare.md
  • examples/backups/nats/92-scenario-user-restore.md
  • examples/backups/nats/README.md

Comment on lines +60 to +66
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Escape values before writing .bucket-info.env to avoid command injection

Line 60-Line 66 write unescaped values into a file that is later sourced. If any field contains $(...), backticks, or other shell syntax, it can execute on the operator host.

Proposed fix
 umask 077
-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
+{
+  printf 'export S3_ACCESS_KEY=%q\n' "$S3_ACCESS_KEY"
+  printf 'export S3_SECRET_KEY=%q\n' "$S3_SECRET_KEY"
+  printf 'export S3_ENDPOINT=%q\n' "$S3_ENDPOINT"
+  printf 'export S3_REGION=%q\n' "$S3_REGION"
+  printf 'export S3_BUCKET=%q\n' "$S3_BUCKET"
+} > "$SCRIPT_DIR/.bucket-info.env"
 chmod 600 "$SCRIPT_DIR/.bucket-info.env"
🤖 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/nats/03-create-bucket.sh` around lines 60 - 66, The current
here-doc writes raw S3_* variables into .bucket-info.env which can allow command
injection when that file is sourced; instead, escape or quote each value before
writing: replace the heredoc that creates "$SCRIPT_DIR/.bucket-info.env" with
per-variable safe writes that use shell-safe quoting (e.g. printf with %q or
otherwise shell-escape each of S3_ACCESS_KEY, S3_SECRET_KEY, S3_ENDPOINT,
S3_REGION, S3_BUCKET) so the resulting file contains exported, safely-escaped
assignments (refer to the variables S3_ACCESS_KEY, S3_SECRET_KEY, S3_ENDPOINT,
S3_REGION, S3_BUCKET and the target file .bucket-info.env).

Comment on lines +23 to +24
backupRef:
name: ${BACKUPJOB_NAME}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the resolved Backup name in RestoreJob.spec.backupRef

Line 24 uses ${BACKUPJOB_NAME}, but backupRef.name is expected to be the Backup object name. Resolve it from BackupJob.status.backupRef.name before applying the RestoreJob.

Proposed fix
+backup_ref=$(kubectl -n "$NAMESPACE" get backupjob "$BACKUPJOB_NAME" -o jsonpath='{.status.backupRef.name}')
+[[ -n "$backup_ref" ]] || { log_error "BackupRef is empty on BackupJob '${BACKUPJOB_NAME}'"; exit 1; }
+
 kubectl apply -f - <<EOF
 apiVersion: backups.cozystack.io/v1alpha1
 kind: RestoreJob
 metadata:
   name: ${RESTOREJOB_INPLACE_NAME}
   namespace: ${NAMESPACE}
 spec:
   backupRef:
-    name: ${BACKUPJOB_NAME}
+    name: ${backup_ref}
 EOF
🤖 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/nats/06-restore-in-place.sh` around lines 23 - 24, The
restore YAML is using the BackupJob name variable `${BACKUPJOB_NAME}` but must
reference the actual Backup object name at `BackupJob.status.backupRef.name`;
update the script so it queries the BackupJob status (e.g., `kubectl get
backupjob $BACKUPJOB_NAME -o jsonpath='{.status.backupRef.name}'` or equivalent)
stores that value into a variable like `BACKUP_NAME`, and then set
`RestoreJob.spec.backupRef.name` to that resolved `BACKUP_NAME` instead of
`${BACKUPJOB_NAME}` so the RestoreJob points to the real Backup resource.

Comment on lines +4 to +24
set -uo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/00-helpers.sh"

print_header "Cleanup NATS backup demo"

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
kubectl -n "$NAMESPACE" delete secret "${NATS_RESTORE_NAME}-backup-s3" --ignore-not-found
kubectl -n "$NAMESPACE" delete secret "${NATS_NAME}-backup-s3" --ignore-not-found
kubectl -n "$NAMESPACE" delete nats "$NATS_RESTORE_NAME" --ignore-not-found
kubectl -n "$NAMESPACE" delete nats "$NATS_NAME" --ignore-not-found
kubectl -n "$NAMESPACE" delete bucket "$BUCKET_NAME" --ignore-not-found
rm -f "$SCRIPT_DIR/.bucket-info.env"
kubectl delete backupclass "$BACKUPCLASS_NAME" --ignore-not-found
kubectl delete job.strategy.backups.cozystack.io "$STRATEGY_NAME" --ignore-not-found

log_success "Cleanup complete."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cleanup can silently fail but still report success.

On Line [4], missing -e means command failures won’t abort; then Line [24] always prints success. This can leave resources undeleted while reporting “Cleanup complete.”

Suggested fix
- set -uo pipefail
+ set -euo pipefail
📝 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.

Suggested change
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/00-helpers.sh"
print_header "Cleanup NATS backup demo"
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
kubectl -n "$NAMESPACE" delete secret "${NATS_RESTORE_NAME}-backup-s3" --ignore-not-found
kubectl -n "$NAMESPACE" delete secret "${NATS_NAME}-backup-s3" --ignore-not-found
kubectl -n "$NAMESPACE" delete nats "$NATS_RESTORE_NAME" --ignore-not-found
kubectl -n "$NAMESPACE" delete nats "$NATS_NAME" --ignore-not-found
kubectl -n "$NAMESPACE" delete bucket "$BUCKET_NAME" --ignore-not-found
rm -f "$SCRIPT_DIR/.bucket-info.env"
kubectl delete backupclass "$BACKUPCLASS_NAME" --ignore-not-found
kubectl delete job.strategy.backups.cozystack.io "$STRATEGY_NAME" --ignore-not-found
log_success "Cleanup complete."
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/00-helpers.sh"
print_header "Cleanup NATS backup demo"
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
kubectl -n "$NAMESPACE" delete secret "${NATS_RESTORE_NAME}-backup-s3" --ignore-not-found
kubectl -n "$NAMESPACE" delete secret "${NATS_NAME}-backup-s3" --ignore-not-found
kubectl -n "$NAMESPACE" delete nats "$NATS_RESTORE_NAME" --ignore-not-found
kubectl -n "$NAMESPACE" delete nats "$NATS_NAME" --ignore-not-found
kubectl -n "$NAMESPACE" delete bucket "$BUCKET_NAME" --ignore-not-found
rm -f "$SCRIPT_DIR/.bucket-info.env"
kubectl delete backupclass "$BACKUPCLASS_NAME" --ignore-not-found
kubectl delete job.strategy.backups.cozystack.io "$STRATEGY_NAME" --ignore-not-found
log_success "Cleanup complete."
🧰 Tools
🪛 Shellcheck (0.11.0)

[info] 7-7: Not following: ./00-helpers.sh was not specified as input (see shellcheck -x).

(SC1091)

🤖 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/nats/cleanup.sh` around lines 4 - 24, The script currently
uses set -uo pipefail (missing -e) so failures won’t abort and log_success at
the end can report success even if deletes failed; change the top options to
include -e (set -euo pipefail) so the script exits on any command error, and
ensure critical cleanup commands (the kubectl delete invocations that operate on
restorejob/backupjob/backup/nats/bucket/backupclass/job.strategy.backups.cozystack.io
and the rm) will cause the script to fail rather than reach
print_header/log_success; keep --ignore-not-found where intentional but rely on
-e for real failures so print_header and log_success reflect actual success.

@androndo

Copy link
Copy Markdown
Contributor
 LGTM
 
 This branch implements the generic, app-agnostic Job backup strategy across three commits (feat(backups): implement
  Job backup strategy, test(backups): harden Job strategy tests, docs(backups): add NATS Job-strategy example). I
 reviewed it against main (merge base ee9d1544e), since the upstream tracking branch is just the branch tracking
 itself. The implementation is clean, well-tested, and consistent with the existing driver patterns.

 What I verified:

 Implementation (internal/backupcontroller/jobstrategy_controller.go, 502 new lines)
 - The driver is a faithful mirror of the Altinity driver minus the ClickHouse applicationRef.kind gate, which is
 the whole point of a generic driver. Backup path (reconcileJob), restore path (reconcileJobRestore), idempotent Job
  materialisation, deterministic Job naming, controller refs, parameter round-tripping via DriverMetadata — all
 present and correct.
 - Dispatch is fully wired in all the right places: backupjob_controller.go:77, restorejob_controller.go:109,
 backup_controller.go:99 (cleanup no-op), restorejob_controller.go:213 (cleanup no-op), and
 supportedBackupStrategyKinds(). No half-wired path.
 - The ResourceVersion/StartedAt bookkeeping (refetch-before-write, requeue-instead-of-reuse) matches the
 Altinity/CNPG convention exactly, so the documented Conflict-avoidance rationale holds.
 - One deliberate and correct divergence from Altinity: the Job driver additionally classifies
 apimeta.IsNoMatchError as terminal (lines 132, 414). Because it forwards arbitrary kinds into RESTMapping with no
 kind-gate, a typo'd/uninstalled kind would otherwise requeue forever in Running. Good catch by the author, and it's
  tested.

 Documentation accuracy — The doc-comment context contract was changed from the old .Values shape to .Application /
 .Release / .Mode / .Parameters / .Backup in both api/backups/strategy/v1alpha1/job_types.go and the generated CRD
 strategy.backups.cozystack.io_jobs.yaml, and these match what renderJobTemplate (in
 altinitystrategy_controller.go:108) actually injects. I grepped the backups tree for stale .Values references in
 strategy docs/examples — none (the only .Values hits are unrelated Helm chart templating for the controller's own
 deployment).

 Tests (jobstrategy_controller_test.go, 649 lines) — Coverage is strong and each test pins a real contract:
 create-batch-job (asserts .Application.spec.replicas reads the live object, not just static params),
 completes-and-creates-backup (asserts parameter round-trip via DriverMetadata), fails-on-job-failed,
 fails-on-missing-strategy (message-pinned so it can't pass on the wrong path), restore-in-target-namespace (asserts
  to-copy source/target split), restore-completes, parameter round-trip filtering, and the unmappable-kind
 terminal-failure tests for both backup and restore paths. go test ./internal/backupcontroller/... and go vet both
 pass clean.

 terminal-failure tests for both backup and restore paths. go test ./internal/backupcontroller/... and go vet both
 pass clean.

 Examples (examples/backups/nats/) — A complete, runnable NATS JetStream demo. Uses the
 single-BackupClass-with-strategies[]-array shape, numbered 00-/01-/... prefixes, and a single PodTemplateSpec that
 branches at runtime on $MODE (since the engine templates leaves and can't add/remove containers per mode). The
 strategy template exercises the full documented context. Restore scripts have defensive numeric-count guards.
 README is accurate and honestly documents the caveats (not run in CI, self-signed S3 -k, nats CLI version drift).

 Non-blocking observations (no fix required for this PR):
 - buildJobStrategyBatchJob hardcodes backoffLimit=2, inherited from Altinity. The author already flags this as a
 footgun for an app-agnostic driver and tracks making it configurable in issue #2742. Reasonable to defer.
 - The shared internal/template engine swallows template execution errors (returns the input string verbatim on
 failure), so renderJobTemplate's "failed to template..." branch is effectively only reachable on JSON marshal
 errors, not bad templates. This is pre-existing shared infrastructure (Altinity/CNPG/MariaDB all use it
 identically), not introduced here, so it's out of scope to fix in this PR — but worth being aware of: a malformed
 strategy template won't fail the BackupJob, it'll render a broken Pod spec.
 - The NATS example pins a fixed demo password (jetstream-demo-pw) — clearly labelled as a demo convenience, not a
 leaked secret.

@lllamnyp
Timofei Larkin (lllamnyp) merged commit 72c4eb9 into main May 27, 2026
17 of 19 checks passed
@lllamnyp
Timofei Larkin (lllamnyp) deleted the feat/jobdriver-implementation branch May 27, 2026 10:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/storage Issues or PRs related to storage (linstor, seaweedfs, bucket, velero, harbor) do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress kind/api-change Categorizes issue or PR as related to adding, removing, or otherwise changing an API kind/feature Categorizes issue or PR as related to a new feature size/XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants