Skip to content

feat(platform): implement backup strategy for mariadb - #2605

Merged
Andrey Kolkov (androndo) merged 1 commit into
mainfrom
feat/mariadb-backups
May 12, 2026
Merged

Andrey Kolkov (androndo) merged 1 commit into
mainfrom
feat/mariadb-backups

Conversation

@androndo

@androndo Andrey Kolkov (androndo) commented May 11, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

Screenshots

Release note

implement backup strategy for MariaDB

Summary by CodeRabbit

  • New Features

    • Added native MariaDB backup/restore strategy using the operator-native BackupClass/Plan/RestoreJob flow with example manifests for end-to-end workflows.
  • Documentation

    • Added MariaDB how‑to and examples; marked legacy restic/chart-side backup parameters as DEPRECATED and documented migration guidance.
  • Tests

    • Added unit tests covering MariaDB backup/restore reconciliation, templating, storage handling, idempotency, URIs, and deadlines.
  • Chores

    • Extended controller RBAC to allow observing MariaDB operator resources.

Review Change Stack

@github-actions github-actions Bot added area/platform Issues or PRs related to platform infrastructure (bundle, flux, talos, installer) kind/feature Categorizes issue or PR as related to a new feature size/XXL This PR changes 1000+ lines, ignoring generated files labels May 11, 2026
@dosubot dosubot Bot added the area/database Issues or PRs related to managed databases (postgres, mariadb, redis, etcd, kafka, clickhouse) label May 11, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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 implements a native backup and restore strategy for MariaDB by integrating with the k8s.mariadb.com operator. It introduces the necessary API definitions, controller logic, and RBAC permissions to manage MariaDB backups and restores effectively. Furthermore, it updates existing documentation and configuration to transition users away from the legacy backup method toward this new, operator-native approach.

Highlights

  • New API Types: Added new API types to define the MariaDB backup strategy and internal application types for integration.
  • Controller Implementation: Implemented the core reconciliation logic for MariaDB backups and restores, enabling interaction with the k8s.mariadb.com operator.
  • Controller Integration: Updated existing backup and restore job controllers to support the new MariaDB strategy.
  • Documentation and Legacy Support: Updated documentation and values to mark the previous restic-based backup mechanism as legacy.
  • RBAC Updates: Expanded RBAC permissions to allow the controller to manage MariaDB-related resources.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Ignored Files
  • Ignored by pattern: **/zz_generated.*.go (3)
    • api/backups/strategy/v1alpha1/zz_generated.deepcopy.go
    • internal/backupcontroller/mariadbapp/zz_generated.deepcopy.go
    • internal/backupcontroller/mariadbtypes/zz_generated.deepcopy.go
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 the 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 counterproductive. 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.

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. ↩

@coderabbitai

coderabbitai Bot commented May 11, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds operator-native MariaDB backup/restore support: strategy CR types, internal app/operator types, a strategy controller that orchestrates k8s.mariadb.com Backup/Restore CRs for BackupJob/RestoreJob reconciliation, controller scheme wiring, examples/tests, RBAC changes, and deprecation notes for the legacy restic-based chart backup.

Changes

MariaDB Backup/Restore Strategy

Layer / File(s) Summary
Strategy API Types
api/backups/strategy/v1alpha1/mariadb_types.go, api/backups/strategy/v1alpha1/zz_generated.deepcopy.go
Defines v1alpha1 MariaDB strategy CR (spec/template with storage union S3/PVC/Volume, compression, databases, retention, log level), MariaDBStrategyKind, scheme registration, and autogenerated deepcopy methods.
Internal App Types
internal/backupcontroller/mariadbapp/types.go, internal/backupcontroller/mariadbapp/zz_generated.deepcopy.go
Typed Cozystack MariaDB application types for apps.cozystack.io/v1alpha1 with scheme wiring and deepcopy helpers for controller reads.
Internal Operator Types
internal/backupcontroller/mariadbtypes/types.go, internal/backupcontroller/mariadbtypes/zz_generated.deepcopy.go
Minimal k8s.mariadb.com/v1alpha1 operator types (MariaDB, Backup, Restore) including storage unions, S3/TLS/secret selectors, backup/restore specs/statuses, and deepcopy implementations.
Controller Scheme Registration
cmd/backupstrategy-controller/main.go
Imports MariaDB app/operator modules and registers their schemes in the controller binary.
Strategy Controller
internal/backupcontroller/mariadbstrategy_controller.go
Implements reconcileMariaDB and reconcileMariaDBRestore: app ref validation, StartedAt persistence, template rendering, operator MariaDB existence checks, idempotent creation/reuse of operator Backup/Restore CRs via owning-job labels, polling Complete condition, wall-clock deadline enforcement, artifact materialization (underlyingResources + driver metadata), storage translation, restore target resolution, restore options parsing, and helpers.
Strategy Tests & Helpers
internal/backupcontroller/mariadbstrategy_controller_test.go
Unit tests covering app ref validation, storage translation (S3/PVC), template rendering, idempotency by label, restore target resolution, snapshot marshal/unmarshal, restore options parsing, deadline semantics, URI edge cases, reconcile edge flows, and test helpers for typed clients/resources.
BackupJob Dispatch
internal/backupcontroller/backupjob_controller.go, internal/backupcontroller/backupjob_controller_test.go
Dispatches MariaDBStrategyKind in BackupJob reconciler, centralizes supported kinds via helper, and adds test ensuring consistency.
RestoreJob Dispatch
internal/backupcontroller/restorejob_controller.go, internal/backupcontroller/restorejob_controller_test.go
Dispatches MariaDBStrategyKind in RestoreJob reconciler, includes MariaDB in no-cleanup group, and adds side-state kind test case.
Examples & Manifests
examples/backups/mariadb/*, examples/backups/mariadb/README.md
Adds example Bucket, source/target MariaDB manifests, MariaDB strategy template, BackupClass, Plan, BackupJob/RestoreJob examples, and E2E README demonstrating operator-native flow.
RBAC & Deprecation
packages/system/backupstrategy-controller/templates/rbac.yaml, api/apps/v1alpha1/mariadb/types.go, packages/apps/mariadb/*, packages/system/mariadb-rd/cozyrds/mariadb.yaml
Extends controller ClusterRole to include mariadb-operator resources and marks legacy chart backup.* configuration as DEPRECATED in API docs, Helm values, and schema, directing users to BackupClass/Plan/RestoreJob flow.

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs:

Suggested labels:
enhancement, size:XXL

Suggested reviewers:

  • lexfrei
  • sircthulhu
  • kvaps
  • IvanHunters

🐰 A backup tale, at last complete,
MariaDB dance—operator-sweet!
Templates, snapshots, tests in tow,
Old restic fades; new flows grow.
Hooray — artifacts safe and neat!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.29% 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 PR title accurately and concisely summarizes the main change: implementing a backup strategy for MariaDB. It is specific, clear, and directly reflects the primary objective of the changeset.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mariadb-backups

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 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 native MariaDB backup and restore strategy that integrates with the mariadb-operator. It adds the necessary API types, controller logic for BackupJob and RestoreJob reconciliation, and updates the MariaDB package to mark existing restic-based backups as legacy. Feedback identifies a potential nil pointer dereference when handling backup retention durations and recommends using status patches instead of updates to avoid resource conflicts. A minor optimization for the RestoreJob reconciliation loop was also suggested to prevent unnecessary requeues.

Databases: append([]string(nil), rendered.Databases...),
Compression: rendered.Compression,
LogLevel: rendered.LogLevel,
MaxRetention: rendered.MaxRetention.DeepCopy(),

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 MaxRetention field is a pointer (*metav1.Duration). Calling .DeepCopy() directly on a nil pointer will cause a panic. Please add a nil check before copying.

MaxRetention: func() *metav1.Duration {
	if rendered.MaxRetention == nil {
		return nil
	}
	return rendered.MaxRetention.DeepCopy()
}(),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

metav1.Duration.DeepCopy is generated nil-safe — apimachinery's zz_generated.deepcopy.go opens with if in == nil { return nil } before any dereference. Calling .DeepCopy() on a nil *metav1.Duration returns nil rather than panicking, so the existing call site is safe and matches the pattern used elsewhere in the codebase.

Comment on lines +176 to +178
if err := r.Status().Update(ctx, j); err != nil {
return ctrl.Result{}, err
}

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

Using Update for status updates can lead to optimistic locking conflicts (409 Conflict) if the object was modified elsewhere. It is generally safer and more robust to use Patch with a MergeFrom or StrategicMergePatch in Kubernetes controllers.

Suggested change
if err := r.Status().Update(ctx, j); err != nil {
return ctrl.Result{}, err
}
if err := r.Status().Patch(ctx, j, client.MergeFrom(base)); err != nil {
return ctrl.Result{}, err
}

Comment on lines +221 to +223
if err := r.Status().Update(ctx, j); err != nil {
return ctrl.Result{}, err
}

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

Similar to the previous comment, prefer using Patch over Update for status changes to avoid potential conflict errors during reconciliation.

Suggested change
if err := r.Status().Update(ctx, j); err != nil {
return ctrl.Result{}, err
}
if err := r.Status().Patch(ctx, j, client.MergeFrom(base)); err != nil {
return ctrl.Result{}, err
}

Comment on lines +515 to +517
if err := r.Status().Update(ctx, restoreJob); err != nil {
return ctrl.Result{}, err
}

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

Prefer using Patch for status updates to ensure consistency and avoid conflict errors.

Suggested change
if err := r.Status().Update(ctx, restoreJob); err != nil {
return ctrl.Result{}, err
}
if err := r.Status().Patch(ctx, restoreJob, client.MergeFrom(base)); err != nil {
return ctrl.Result{}, err
}

}
if fresh.Status.StartedAt != nil {
restoreJob.Status.StartedAt = fresh.Status.StartedAt
return ctrl.Result{RequeueAfter: mariadbPollInterval}, nil

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

If StartedAt is already set in the API server, there is no need to requeue the reconciliation. You can simply update the local object and continue with the rest of the logic, similar to how it's handled in the BackupJob path (lines 127-129).

Suggested change
return ctrl.Result{RequeueAfter: mariadbPollInterval}, nil
restoreJob.Status.StartedAt = fresh.Status.StartedAt

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/apps/mariadb/README.md (1)

112-124: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

CI failing: regenerate the ## Parameters block via make generate.

The Pre-Commit pipeline reports that running make generate rewrites the Backup parameters section heading and descriptions to include a [LEGACY] marker, but this README is checked in without that change. Per repo conventions, the ## Parameters block is auto-rewritten by cozyvalues-gen from values.yaml annotations, so don't edit it by hand — instead, update the chart's values.yaml (and/or its # arthur.xia (@Schema)/`# `@param annotations) for the backup.* fields to convey the legacy status, then regenerate.

The hand-written prose at lines 36–50 (the new "How to back up and restore a MariaDB application" section, including the [LEGACY] restic walkthrough) is fine to keep above ## Parameters since the generator does not touch that area.

Based on learnings: "In readme files within packages that are auto-generated by cozyvalues-gen ... do not edit them manually" and "cozyvalues-gen only rewrites the ## Parameters block in package README.md files when running make generate."

🤖 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 `@packages/apps/mariadb/README.md` around lines 112 - 124, The README's `Backup
parameters` table was modified manually but is auto-generated by cozyvalues-gen;
run `make generate` after updating the chart's values annotations instead of
editing README.md: open the chart's values.yaml and its `# `@schema``/`# `@param``
annotations for the backup.* fields (e.g., backup.enabled, backup.s3Region,
backup.s3Bucket, backup.schedule, backup.cleanupStrategy, backup.s3AccessKey,
backup.s3SecretKey, backup.resticPassword), add the `[LEGACY]` wording or adjust
descriptions there to reflect legacy status, then run `make generate` (which
invokes cozyvalues-gen) to regenerate the `## Parameters` block so the README
matches CI expectations.
🧹 Nitpick comments (4)
internal/backupcontroller/mariadbapp/zz_generated.deepcopy.go (1)

1-12: 💤 Low value

Filename convention contradicts "hand-written" status.

The zz_generated.deepcopy.go filename is the universal controller-gen output marker; future contributors (or a make generate run that ever enables deepcopy-gen on this internal package) are likely to overwrite this file. Consider either renaming to deepcopy.go to match the hand-written intent, or adding the standard +k8s:deepcopy-gen=package markers and generating it.

🤖 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/mariadbapp/zz_generated.deepcopy.go` around lines 1
- 12, The filename zz_generated.deepcopy.go signals generated output but the
file is handwritten; either rename the file to deepcopy.go (or another
non-generated name) to avoid being overwritten, or add the canonical k8s
deepcopy marker (e.g. +k8s:deepcopy-gen=package) and place the appropriate
autogenerated header so it can be managed by code generation; update package
mariadbapp's comments accordingly and ensure any CI/make targets reflect the
chosen approach to prevent accidental overwrites.
api/backups/strategy/v1alpha1/mariadb_types.go (2)

161-169: 💤 Low value

Add MinLength=1 to Name and Key on MariaDBSecretKeySelector.

Both fields are required to reference a real Secret/key. Today an empty string passes admission and only fails when the driver dereferences the Secret, producing a late and unhelpful error.

♻️ Proposed diff
 type MariaDBSecretKeySelector struct {
 	// Name is the Secret name. Templating is supported.
+	// +kubebuilder:validation:MinLength=1
 	Name string `json:"name"`

 	// Key is the key within the Secret holding the value.
+	// +kubebuilder:validation:MinLength=1
 	Key string `json:"key"`
 }
🤖 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 `@api/backups/strategy/v1alpha1/mariadb_types.go` around lines 161 - 169, The
MariaDBSecretKeySelector struct's Name and Key allow empty strings; add
Kubernetes validation to require non-empty values by adding a kubebuilder
MinLength=1 validation marker for both fields. Locate the struct
MariaDBSecretKeySelector and add the comment tag
"+kubebuilder:validation:MinLength=1" immediately above the Name field and
likewise above the Key field so the CRD enforces at least one character for Name
and Key.

99-117: ⚡ Quick win

Enforce the "exactly one of" storage constraint via CEL XValidation.

The doc comment states "Exactly one of S3, PersistentVolumeClaim or Volume must be set", but each field is just +optional and nothing rejects a CR with zero or multiple set at admission time. Misconfigured strategies will only surface at driver-render time, which is late and noisy for tenants.

♻️ Proposed XValidation rule
 // MariaDBStorageTemplate mirrors k8s.mariadb.com/v1alpha1 Backup.spec.storage.
 // Exactly one of S3, PersistentVolumeClaim or Volume must be set.
+// +kubebuilder:validation:XValidation:rule="[has(self.s3), has(self.persistentVolumeClaim), has(self.volume)].filter(x, x).size() == 1",message="exactly one of s3, persistentVolumeClaim or volume must be set"
 type MariaDBStorageTemplate struct {
controller-gen kubebuilder XValidation oneOf field set CEL syntax
🤖 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 `@api/backups/strategy/v1alpha1/mariadb_types.go` around lines 99 - 117, The
struct MariaDBStorageTemplate allows zero or multiple of S3,
PersistentVolumeClaim, and Volume to be set but must enforce "exactly one" at
admission; add a kubebuilder XValidation (CEL) rule on MariaDBStorageTemplate
that validates exactly one of the three fields is non-null (e.g. a oneOf
expression using nonEmpty checks on .s3, .persistentVolumeClaim, .volume or
equivalent CEL boolean arithmetic) so the API server rejects CRs with none or
more than one storage target set; attach the XValidation marker to the
MariaDBStorageTemplate type so validation runs at admission time.
internal/backupcontroller/mariadbstrategy_controller.go (1)

699-740: 💤 Low value

Validate exactly-one of S3/PVC/Volume rather than first-match.

The error message on line 738 says the template "requires exactly one of s3, persistentVolumeClaim or volume", but the switch only enforces "at least one". If a tenant authors a strategy template with both s3 and persistentVolumeClaim set, the case order silently picks S3 and discards the PVC branch without any indication. The operator-side OpenAPI schema will eventually reject mixed branches on the operator Backup CR for the cases it covers, but surfacing the conflict here as a strategy validation error gives the tenant a much clearer message and avoids ever creating the operator Backup with the wrong shape.

♻️ Proposed validation
 func buildMariaDBBackupStorage(in strategyv1alpha1.MariaDBStorageTemplate) (mariadbtypes.BackupStorage, error) {
+	set := 0
+	if in.S3 != nil {
+		set++
+	}
+	if in.PersistentVolumeClaim != nil {
+		set++
+	}
+	if in.Volume != nil {
+		set++
+	}
+	if set > 1 {
+		return mariadbtypes.BackupStorage{}, fmt.Errorf("strategy.spec.template.storage must set exactly one of s3, persistentVolumeClaim or volume; %d branches set", set)
+	}
 	switch {
 	case in.S3 != nil:
🤖 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/mariadbstrategy_controller.go` around lines 699 -
740, The buildMariaDBBackupStorage function currently uses a switch to pick the
first non-nil storage (S3, PersistentVolumeClaim, Volume) which allows multiple
fields to be set silently; change it to explicitly validate that exactly one of
in.S3, in.PersistentVolumeClaim, in.Volume is non-nil (e.g., count non-nil
fields) and return an error if the count != 1, then proceed to construct the
corresponding mariadbtypes.BackupStorage for the single selected branch (retain
existing logic that builds S3 with TLS and SessionTokenSecretKeyRef and uses
DeepCopy for PersistentVolumeClaim and Volume); include the same error message
used now when count != 1.
🤖 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/backupjob_controller.go`:
- Line 91: The logged "supported" slice in the default branch omits
strategyv1alpha1.AltinityStrategyKind even though the switch dispatches to
r.reconcileAltinity for that kind; update the supported slice to include
strategyv1alpha1.AltinityStrategyKind so the log accurately reflects all handled
kinds (i.e., add AltinityStrategyKind alongside JobStrategyKind,
VeleroStrategyKind, CNPGStrategyKind, MariaDBStrategyKind in the supported list
emitted in the default branch).

---

Outside diff comments:
In `@packages/apps/mariadb/README.md`:
- Around line 112-124: The README's `Backup parameters` table was modified
manually but is auto-generated by cozyvalues-gen; run `make generate` after
updating the chart's values annotations instead of editing README.md: open the
chart's values.yaml and its `# `@schema``/`# `@param`` annotations for the backup.*
fields (e.g., backup.enabled, backup.s3Region, backup.s3Bucket, backup.schedule,
backup.cleanupStrategy, backup.s3AccessKey, backup.s3SecretKey,
backup.resticPassword), add the `[LEGACY]` wording or adjust descriptions there
to reflect legacy status, then run `make generate` (which invokes
cozyvalues-gen) to regenerate the `## Parameters` block so the README matches CI
expectations.

---

Nitpick comments:
In `@api/backups/strategy/v1alpha1/mariadb_types.go`:
- Around line 161-169: The MariaDBSecretKeySelector struct's Name and Key allow
empty strings; add Kubernetes validation to require non-empty values by adding a
kubebuilder MinLength=1 validation marker for both fields. Locate the struct
MariaDBSecretKeySelector and add the comment tag
"+kubebuilder:validation:MinLength=1" immediately above the Name field and
likewise above the Key field so the CRD enforces at least one character for Name
and Key.
- Around line 99-117: The struct MariaDBStorageTemplate allows zero or multiple
of S3, PersistentVolumeClaim, and Volume to be set but must enforce "exactly
one" at admission; add a kubebuilder XValidation (CEL) rule on
MariaDBStorageTemplate that validates exactly one of the three fields is
non-null (e.g. a oneOf expression using nonEmpty checks on .s3,
.persistentVolumeClaim, .volume or equivalent CEL boolean arithmetic) so the API
server rejects CRs with none or more than one storage target set; attach the
XValidation marker to the MariaDBStorageTemplate type so validation runs at
admission time.

In `@internal/backupcontroller/mariadbapp/zz_generated.deepcopy.go`:
- Around line 1-12: The filename zz_generated.deepcopy.go signals generated
output but the file is handwritten; either rename the file to deepcopy.go (or
another non-generated name) to avoid being overwritten, or add the canonical k8s
deepcopy marker (e.g. +k8s:deepcopy-gen=package) and place the appropriate
autogenerated header so it can be managed by code generation; update package
mariadbapp's comments accordingly and ensure any CI/make targets reflect the
chosen approach to prevent accidental overwrites.

In `@internal/backupcontroller/mariadbstrategy_controller.go`:
- Around line 699-740: The buildMariaDBBackupStorage function currently uses a
switch to pick the first non-nil storage (S3, PersistentVolumeClaim, Volume)
which allows multiple fields to be set silently; change it to explicitly
validate that exactly one of in.S3, in.PersistentVolumeClaim, in.Volume is
non-nil (e.g., count non-nil fields) and return an error if the count != 1, then
proceed to construct the corresponding mariadbtypes.BackupStorage for the single
selected branch (retain existing logic that builds S3 with TLS and
SessionTokenSecretKeyRef and uses DeepCopy for PersistentVolumeClaim and
Volume); include the same error message used now when count != 1.
🪄 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: 2d4622fd-bf50-4405-bd75-276fe123705f

📥 Commits

Reviewing files that changed from the base of the PR and between 86f5267 and cbaf8ed.

📒 Files selected for processing (16)
  • api/backups/strategy/v1alpha1/mariadb_types.go
  • api/backups/strategy/v1alpha1/zz_generated.deepcopy.go
  • cmd/backupstrategy-controller/main.go
  • internal/backupcontroller/backupjob_controller.go
  • internal/backupcontroller/mariadbapp/types.go
  • internal/backupcontroller/mariadbapp/zz_generated.deepcopy.go
  • internal/backupcontroller/mariadbstrategy_controller.go
  • internal/backupcontroller/mariadbstrategy_controller_test.go
  • internal/backupcontroller/mariadbtypes/types.go
  • internal/backupcontroller/mariadbtypes/zz_generated.deepcopy.go
  • internal/backupcontroller/restorejob_controller.go
  • internal/backupcontroller/restorejob_controller_test.go
  • packages/apps/mariadb/README.md
  • packages/apps/mariadb/values.yaml
  • packages/system/backupstrategy-controller/definitions/strategy.backups.cozystack.io_mariadbs.yaml
  • packages/system/backupstrategy-controller/templates/rbac.yaml

Comment thread internal/backupcontroller/backupjob_controller.go Outdated

@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: 1

🧹 Nitpick comments (1)
internal/backupcontroller/mariadbstrategy_controller.go (1)

455-474: 💤 Low value

Restore StartedAt bootstrap requeues even when stale cache resolves — asymmetric with the backup path.

When the cache returned StartedAt == nil but a fresh Get finds it already set, the restore branch syncs the value into the in-memory object and immediately returns RequeueAfter (line 462). The backup-side bootstrap at lines 118-138 instead absorbs the value and falls through to continue the reconcile in the same loop. The restore behaviour is correct but costs an extra reconcile cycle on stale-cache hits and diverges from the documented "same idempotency pattern as the CNPG driver" comment at line 122. Worth aligning for consistency.

♻️ Proposed alignment
 		if fresh.Status.StartedAt != nil {
 			restoreJob.Status.StartedAt = fresh.Status.StartedAt
-			return ctrl.Result{RequeueAfter: mariadbPollInterval}, nil
+		} else {
+			base := fresh.DeepCopy()
+			now := metav1.Now()
+			fresh.Status.StartedAt = &now
+			fresh.Status.Phase = backupsv1alpha1.RestoreJobPhaseRunning
+			if err := r.Status().Patch(ctx, fresh, client.MergeFrom(base)); err != nil {
+				return ctrl.Result{}, err
+			}
+			restoreJob.Status.StartedAt = fresh.Status.StartedAt
+			restoreJob.Status.Phase = fresh.Status.Phase
+			return ctrl.Result{RequeueAfter: mariadbPollInterval}, nil
 		}
-		base := fresh.DeepCopy()
-		now := metav1.Now()
-		fresh.Status.StartedAt = &now
-		fresh.Status.Phase = backupsv1alpha1.RestoreJobPhaseRunning
-		if err := r.Status().Patch(ctx, fresh, client.MergeFrom(base)); err != nil {
-			return ctrl.Result{}, err
-		}
-		restoreJob.Status.StartedAt = fresh.Status.StartedAt
-		restoreJob.Status.Phase = fresh.Status.Phase
-		return ctrl.Result{RequeueAfter: mariadbPollInterval}, nil
 	}
🤖 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/mariadbstrategy_controller.go`:
- Line 31: Remove the unused constant mariadbFieldManager from this file — it is
never referenced and golangci-lint flags it as unused; locate the declaration
"mariadbFieldManager = \"cozystack-mariadb-backup-driver\"" and delete it, since
all status writes in this file use client.MergeFrom(base) and
r.Status().Update(...) (no client.FieldOwner usage). If you later migrate to
server-side apply, reintroduce the constant and pass it as
client.FieldOwner(mariadbFieldManager) on the Patch calls instead.
🪄 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: 38e319a0-8ca2-4f88-a809-0d794ce1afbe

📥 Commits

Reviewing files that changed from the base of the PR and between cbaf8ed and 7a94a9e.

📒 Files selected for processing (8)
  • api/backups/strategy/v1alpha1/mariadb_types.go
  • internal/backupcontroller/backupjob_controller.go
  • internal/backupcontroller/backupjob_controller_test.go
  • internal/backupcontroller/mariadbstrategy_controller.go
  • internal/backupcontroller/mariadbstrategy_controller_test.go
  • packages/system/backup-controller/values.yaml
  • packages/system/backupstrategy-controller/templates/rbac.yaml
  • packages/system/backupstrategy-controller/values.yaml
✅ Files skipped from review due to trivial changes (2)
  • packages/system/backup-controller/values.yaml
  • packages/system/backupstrategy-controller/values.yaml
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/backups/strategy/v1alpha1/mariadb_types.go

Comment thread internal/backupcontroller/mariadbstrategy_controller.go

@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/mariadbstrategy_controller.go`:
- Around line 755-795: The function buildMariaDBBackupStorage currently picks
the first non-nil storage option; change it to validate that exactly one of
in.S3, in.PersistentVolumeClaim, and in.Volume is set: compute a count of
non-nil options at the start of buildMariaDBBackupStorage (and return an error
if count != 1), and only then proceed with the existing per-branch construction
(S3 -> mariadbtypes.S3Storage, PersistentVolumeClaim -> DeepCopy(), Volume ->
DeepCopy()); keep the existing error message text for the invalid case. Ensure
you reference and update buildMariaDBBackupStorage and its handling of in.S3,
in.PersistentVolumeClaim, and in.Volume.
- Around line 268-304: ensureMariaDBBackup (and the sibling
ensureMariaDBRestore) must stop using GenerateName and instead set a
deterministic Name based on the BackupJob (e.g. fmt.Sprintf("%s-mariadb-backup",
j.Name) or similar) so creates are idempotent; when calling r.Create(ctx, obj)
handle apierrors.IsAlreadyExists(err) by doing a r.Get(ctx,
types.NamespacedName{Namespace: obj.Namespace, Name: obj.Name}, existing) and
returning the existing object, and only return an error for other create
failures; keep the same labels/spec fields (Databases, Storage, etc.) and ensure
the name generation logic matches the pattern used at line 401 so the Get lookup
will find the same resource on concurrent reconciles.
🪄 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: c3bdc772-73f5-435e-804d-ddc05a9364a7

📥 Commits

Reviewing files that changed from the base of the PR and between 7a94a9e and 0618661.

📒 Files selected for processing (22)
  • api/apps/v1alpha1/mariadb/types.go
  • api/backups/strategy/v1alpha1/mariadb_types.go
  • api/backups/strategy/v1alpha1/zz_generated.deepcopy.go
  • cmd/backupstrategy-controller/main.go
  • internal/backupcontroller/backupjob_controller.go
  • internal/backupcontroller/backupjob_controller_test.go
  • internal/backupcontroller/mariadbapp/types.go
  • internal/backupcontroller/mariadbapp/zz_generated.deepcopy.go
  • internal/backupcontroller/mariadbstrategy_controller.go
  • internal/backupcontroller/mariadbstrategy_controller_test.go
  • internal/backupcontroller/mariadbtypes/types.go
  • internal/backupcontroller/mariadbtypes/zz_generated.deepcopy.go
  • internal/backupcontroller/restorejob_controller.go
  • internal/backupcontroller/restorejob_controller_test.go
  • packages/apps/mariadb/README.md
  • packages/apps/mariadb/values.schema.json
  • packages/apps/mariadb/values.yaml
  • packages/system/backup-controller/values.yaml
  • packages/system/backupstrategy-controller/definitions/strategy.backups.cozystack.io_mariadbs.yaml
  • packages/system/backupstrategy-controller/templates/rbac.yaml
  • packages/system/backupstrategy-controller/values.yaml
  • packages/system/mariadb-rd/cozyrds/mariadb.yaml
✅ Files skipped from review due to trivial changes (7)
  • packages/apps/mariadb/README.md
  • packages/system/mariadb-rd/cozyrds/mariadb.yaml
  • packages/apps/mariadb/values.schema.json
  • api/backups/strategy/v1alpha1/zz_generated.deepcopy.go
  • api/apps/v1alpha1/mariadb/types.go
  • internal/backupcontroller/restorejob_controller_test.go
  • internal/backupcontroller/mariadbtypes/zz_generated.deepcopy.go
🚧 Files skipped from review as they are similar to previous changes (11)
  • internal/backupcontroller/restorejob_controller.go
  • internal/backupcontroller/backupjob_controller.go
  • internal/backupcontroller/mariadbapp/zz_generated.deepcopy.go
  • cmd/backupstrategy-controller/main.go
  • packages/system/backupstrategy-controller/values.yaml
  • packages/apps/mariadb/values.yaml
  • internal/backupcontroller/backupjob_controller_test.go
  • api/backups/strategy/v1alpha1/mariadb_types.go
  • packages/system/backup-controller/values.yaml
  • internal/backupcontroller/mariadbapp/types.go
  • internal/backupcontroller/mariadbtypes/types.go

Comment on lines +268 to +304
func (r *BackupJobReconciler) ensureMariaDBBackup(ctx context.Context, j *backupsv1alpha1.BackupJob, rendered *strategyv1alpha1.MariaDBTemplate) (*mariadbtypes.Backup, error) {
existing, err := r.findMariaDBBackupForJob(ctx, j)
if err != nil {
return nil, err
}
if existing != nil {
return existing, nil
}

storage, err := buildMariaDBBackupStorage(rendered.Storage)
if err != nil {
return nil, err
}

obj := &mariadbtypes.Backup{
ObjectMeta: metav1.ObjectMeta{
Namespace: j.Namespace,
GenerateName: fmt.Sprintf("%s-", j.Name),
Labels: map[string]string{
backupsv1alpha1.OwningJobNameLabel: j.Name,
backupsv1alpha1.OwningJobNamespaceLabel: j.Namespace,
},
},
Spec: mariadbtypes.BackupSpec{
MariaDBRef: mariadbtypes.MariaDBObjectRef{Name: mariadbNameForApp(j.Spec.ApplicationRef.Name)},
Storage: storage,
Databases: append([]string(nil), rendered.Databases...),
Compression: rendered.Compression,
LogLevel: rendered.LogLevel,
MaxRetention: rendered.MaxRetention.DeepCopy(),
},
}

if err := r.Create(ctx, obj); err != nil {
return nil, err
}
return obj, nil

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== MariaDB helper implementations =="
sed -n '265,305p;598,637p' internal/backupcontroller/mariadbstrategy_controller.go

echo
echo "== Relevant controller-runtime usage in this file =="
rg -n --type=go 'GenerateName|MatchingLabels|List\(|Create\(' internal/backupcontroller/mariadbstrategy_controller.go

Repository: cozystack/cozystack

Length of output: 3188


🏁 Script executed:

# Find findMariaDBBackupForJob implementation
rg -n "func.*findMariaDBBackupForJob" internal/backupcontroller/mariadbstrategy_controller.go -A 15

# Check what happens with Create errors (check context around lines 301, 633, 401)
sed -n '298,310p' internal/backupcontroller/mariadbstrategy_controller.go
sed -n '630,645p' internal/backupcontroller/mariadbstrategy_controller.go
sed -n '398,410p' internal/backupcontroller/mariadbstrategy_controller.go

Repository: cozystack/cozystack

Length of output: 1794


🏁 Script executed:

# Find which function contains the AlreadyExists handling around line 401
sed -n '380,415p' internal/backupcontroller/mariadbstrategy_controller.go

# Also check if there are any other uses of AlreadyExists in the file
rg -n "IsAlreadyExists" internal/backupcontroller/mariadbstrategy_controller.go -B 3 -A 3

Repository: cozystack/cozystack

Length of output: 1410


🏁 Script executed:

# Get the function signature for the good pattern around line 380-415
sed -n '350,390p' internal/backupcontroller/mariadbstrategy_controller.go | head -50

# Find the buildMariaDBBackupStorage function to check storage validation
rg -n "func.*buildMariaDBBackupStorage" internal/backupcontroller/mariadbstrategy_controller.go -A 20

Repository: cozystack/cozystack

Length of output: 2537


🏁 Script executed:

# Verify GenerateName behavior - check if it produces deterministic or random output
# Also verify the storage validation concern by checking the full function
sed -n '755,800p' internal/backupcontroller/mariadbstrategy_controller.go

# Check what the function comments say about constraints
sed -n '740,760p' internal/backupcontroller/mariadbstrategy_controller.go

Repository: cozystack/cozystack

Length of output: 2860


🏁 Script executed:

# Check the MariaDBStorageTemplate type definition to see if OpenAPI validation prevents multiple backends
rg -n "type MariaDBStorageTemplate" --type go -A 20

# Also verify if there's validation that ensures mutual exclusion
rg -n "MariaDBStorageTemplate" internal/backupcontroller/mariadbstrategy_controller.go | head -5

Repository: cozystack/cozystack

Length of output: 2212


Fix idempotency race: use deterministic names with AlreadyExists error handling.

The helpers ensureMariaDBBackup and ensureMariaDBRestore use GenerateName (which produces a random suffix on each call) combined with label-based List() lookup for idempotency. With controller-runtime's split client, if a follow-up reconcile runs before the cache catches up, it will not find the just-created object in the cached list, execute the same code path again, and create a duplicate Backup/Restore CR. This is especially dangerous for restore operations, where the same dump can be replayed twice against the live MariaDB.

The file already demonstrates the correct pattern (line 401): use a deterministic object Name derived from the job name, then handle AlreadyExists errors with an explicit Get. Apply this pattern to both MariaDB helpers to achieve true idempotency per controller-runtime best practices.

🤖 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/mariadbstrategy_controller.go` around lines 268 -
304, ensureMariaDBBackup (and the sibling ensureMariaDBRestore) must stop using
GenerateName and instead set a deterministic Name based on the BackupJob (e.g.
fmt.Sprintf("%s-mariadb-backup", j.Name) or similar) so creates are idempotent;
when calling r.Create(ctx, obj) handle apierrors.IsAlreadyExists(err) by doing a
r.Get(ctx, types.NamespacedName{Namespace: obj.Namespace, Name: obj.Name},
existing) and returning the existing object, and only return an error for other
create failures; keep the same labels/spec fields (Databases, Storage, etc.) and
ensure the name generation logic matches the pattern used at line 401 so the Get
lookup will find the same resource on concurrent reconciles.

Comment thread internal/backupcontroller/mariadbstrategy_controller.go

@lllamnyp Timofei Larkin (lllamnyp) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review saved to: /tmp/branch-review-feat-mariadb-backups-1778535640.md

NOT LGTM

Reviewing against target branch: main (PR #2605: feat(platform): implement backup strategy for mariadb). Branch is feat/mariadb-backups, merge base 86f5267dd. go build ./... and go test ./internal/backupcontroller/... both pass.

Overall the implementation is well-structured and mirrors the CNPG/Altinity strategy drivers closely. The single blocker is documentation drift that the PR introduces.

Blocking

1. README points at examples/backups/mariadb/, which this PR does not create

packages/apps/mariadb/README.md:36-43 adds a new "How to back up and restore a MariaDB application" section that tells users to "See examples/backups/mariadb for a numbered, end-to-end walkthrough (bucket, source, strategy, BackupClass, Plan, ad-hoc BackupJob, and both in-place + to-copy RestoreJob fixtures)."

That directory does not exist on this branch:

$ ls examples/backups/
clickhouse
postgres
vmi

For comparison, the analogous postgres PR (packages/apps/postgres/README.md:44, 60, 117, 119) ships matching manifests at examples/backups/postgres/00-bucket.yaml, 05-postgres-src.yaml, 10-cnpg-strategy.yaml, 15-backupclass.yaml, 20-plan.yaml, 25-backupjob-adhoc.yaml, 30-postgres-target.yaml, 35-restorejob-in-place.yaml, 40-restorejob-to-copy.yaml. The clickhouse PR ships an even richer set (numbered shell scripts plus scenario markdown). The mariadb PR ships zero example fixtures.

Per the project rules: stale/misleading documentation about the area being changed is a blocking issue. This is more than stale — the README explicitly promises an "end-to-end walkthrough" that a tenant clicking the relative link gets a 404 on. New users following the README to try the new operator-native flow will hit a dead end immediately.

  • Fix: add the examples/backups/mariadb/ directory with manifests matching the structure used by examples/backups/postgres/ — at minimum a bucket, MariaDB source app, strategy.backups.cozystack.io/MariaDB example, BackupClass, Plan, ad-hoc BackupJob, target MariaDB app, in-place RestoreJob, to-copy RestoreJob. The strategy and storage shapes need to exercise S3-on-seaweedfs since the strategy template (api/backups/strategy/v1alpha1/mariadb_types.go:124-161) calls that out as the canonical target. Or, drop the README pointer if examples aren't ready for this PR (but then drop the entire "end-to-end walkthrough" paragraph; do not leave a broken link).
  • Test that fails without the fix: an e2e test that lints/applies the manifests under examples/backups/mariadb/ (the postgres flow is exercised this way — see the examples/backups/postgres/ shape). A unit test that grep/parses the README to assert the path resolves on disk is the wrong shape per the project's own "Don't write tests that grep / parse source files" rule; the correct guard is the e2e fixture applying the files. If e2e for mariadb isn't ready yet, a make example-lint-style CI hook that fails when a relative link in packages/apps/*/README.md points at a non-existent path inside the repo is the next-best line of defense.

Recommended to fix (not blocking)

A. Dead code in mariadbtypes

internal/backupcontroller/mariadbtypes/types.go declares MariaDBSpec.BootstrapFrom, the BootstrapFrom struct, and the BackupContentTypeLogical constant. None of these are referenced anywhere in the controller, command, or tests:

$ grep -rn "BootstrapFrom\|BackupContentTypeLogical\b" internal/ cmd/ api/
internal/backupcontroller/mariadbtypes/types.go: (declarations only)
internal/backupcontroller/mariadbtypes/zz_generated.deepcopy.go: (codegen for the dead type)

The strategy doc comment in mariadbstrategy_controller.go:39 is explicit that the driver does NOT touch MariaDB.spec.bootstrapFrom — the restore path goes through a separate k8s.mariadb.com/Restore CR. So MariaDBSpec ends up with one field, that field is unused, and a generated DeepCopyInto exists for a struct nothing reads. Drop the field + struct + constant from mariadbtypes/types.go and regenerate deepcopy via the project codegen target. (Leave BackupReference — it's reused for RestoreSpec.BackupRef.)

B. marshalMariaDBBackupSnapshot first parameter is unused

internal/backupcontroller/mariadbstrategy_controller.go:689 declares marshalMariaDBBackupSnapshot(_ *mariadbapp.MariaDB, ...) — the source app is accepted and ignored. The CNPG analogue (marshalCNPGBackupSnapshot, line 937 of cnpgstrategy_controller.go) reads the app to snapshot spec.databases / spec.users. For MariaDB the app spec is currently empty so there's nothing to snapshot, but the function signature implies otherwise. The call site (mariadbstrategy_controller.go:369) also passes the live app — both are misleading.

Either:

  1. Drop the parameter (caller no longer threads sourceApp through createMariaDBBackupArtifact), or
  2. Add a one-liner doc-comment on marshalMariaDBBackupSnapshot explaining why the app argument is reserved (mirroring the mariadbapp.MariaDBSpec "intentionally empty" note).

Option 1 is cleaner; option 2 is the smaller diff. Either way the underscore-as-name in a public-ish helper is reviewer-bait.

C. Snapshot is persisted but never consumed

marshalMariaDBBackupSnapshot writes a structured payload into Backup.status.underlyingResources with the rendered storage descriptor and parameters. The doc comment (lines 678-687) claims this exists "so a future RestoreJob can reproduce them exactly when the operator-side Backup CR has been pruned." But the restore path at mariadbstrategy_controller.go:512-520 terminally fails if the operator-side k8s.mariadb.com/Backup CR is missing — it never falls back to the snapshot. So the rationale in the doc comment is aspirational at best.

This is either a) genuinely forward-looking and OK to keep, or b) drift that should match reality. If you're keeping the snapshot for forward-compat, tighten the doc comment to say "reserved for a future fallback path that re-creates the operator Backup from the snapshot when the original has been GC'd" instead of claiming the current restore path uses it.

D. CRD does not enforce "exactly one of S3 / PVC / Volume"

packages/system/backupstrategy-controller/definitions/strategy.backups.cozystack.io_mariadbs.yaml declares storage as required but allows zero, one, two, or three of its branches to be populated. The Go code (buildMariaDBBackupStorage, line 755) catches the zero case but silently picks S3 first when multiple are set. The user-facing failure mode is "operator picks an unexpected branch."

controller-gen doesn't easily express oneOf, but a // +kubebuilder:validation:XValidation:rule="..." on MariaDBStorageTemplate would surface the conflict at admission time. Optional polish; matches the gap in CNPG so it's consistent with the rest of the codebase.

Notes on what's correct (no action required)

  • The OwningJob{Name,Namespace} idempotency on both backup and restore CR creation is sound, and findMariaDBBackupForJob's deterministic-pick fallback for the duplicate-race case is tested (TestFindMariaDBBackupForJob_DuplicateMatches).
  • The wall-clock deadline gate (mariadbBackupDeadlineExceeded) correctly returns false on nil StartedAt to avoid tripping on the very first reconcile; tested.
  • The validateMariaDBApplicationRef accepts apiGroup="" (legacy refs) but rejects apiGroup="other.example.com" — same shape as CNPG; tested.
  • The transient-missing-target handling in reconcileMariaDBRestore mirrors the backup path and is explicitly tested (TestReconcileMariaDBRestore_TargetMariaDBNotFoundIsTransient).
  • The Status().Patch + RequeueAfter pattern when writing StartedAt is correct (avoids stale ResourceVersion on a subsequent Status().Update in the same reconcile, matching the CNPG implementation note).
  • RBAC additions in packages/system/backupstrategy-controller/templates/rbac.yaml are minimal: read on mariadbs, create+list+get+watch on backups/restores. No patch/delete, consistent with the driver's behavior. Sound.
  • cmd/backupstrategy-controller/main.go registers both mariadbtypes and mariadbapp with the scheme — needed so controller-runtime's cache can serve typed Get/List for both API groups.
  • supportedBackupStrategyKinds() centralization is a nice refactor; TestSupportedBackupStrategyKindsMatchesDispatch locks the invariant.
  • The deprecation marking of the chart's backup.* block in packages/apps/mariadb/values.yaml, values.schema.json, README.md, and the rendered mariadb-rd/cozyrds/mariadb.yaml openAPISchema is consistent across all four surfaces.
  • No leaked secrets in the diff.

Reminder: "Pre-existing" is not a thing. If a problem is flagged in this review, it must be fixed in this PR — no exceptions.

Every issue listed above must have a corresponding test. No matter the severity — bug, logic error, security issue, error handling gap, documentation drift — each one must be covered by a test that fails without the fix and passes with it. Untested fixes are not fixes.

@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.

🧹 Nitpick comments (1)
examples/backups/mariadb/05-mariadb-src.yaml (1)

27-29: ⚡ Quick win

Add an explicit “no plaintext secrets” warning next to the sample password.

This example is likely to be copied; a short warning here helps prevent accidental plaintext credential commits.

Proposed diff
   users:
     app:
+      # Example placeholder only. Do not commit real credentials in Git.
+      # Replace via your secret-management flow before apply.
       password: REPLACE_WITH_PASSWORD
       maxUserConnections: 100
🤖 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/mariadb/05-mariadb-src.yaml` around lines 27 - 29, Replace
the plaintext sample password in the YAML by either changing app.password to a
non-secret placeholder (e.g. REPLACE_WITH_SECRET or <REDACTED>) and add an
inline YAML comment warning not to store plaintext credentials, or keep the
current placeholder and append a comment like "# WARNING: do not commit
plaintext secrets — use env or secret store"; target the app.password key in the
file (near app.maxUserConnections) so the warning is clearly visible where users
copy the example.
🤖 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.

Nitpick comments:
In `@examples/backups/mariadb/05-mariadb-src.yaml`:
- Around line 27-29: Replace the plaintext sample password in the YAML by either
changing app.password to a non-secret placeholder (e.g. REPLACE_WITH_SECRET or
<REDACTED>) and add an inline YAML comment warning not to store plaintext
credentials, or keep the current placeholder and append a comment like "#
WARNING: do not commit plaintext secrets — use env or secret store"; target the
app.password key in the file (near app.maxUserConnections) so the warning is
clearly visible where users copy the example.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1cb3c7cc-4474-40f0-a41b-0a664ae88819

📥 Commits

Reviewing files that changed from the base of the PR and between 3103510 and 55af5ec.

📒 Files selected for processing (30)
  • api/apps/v1alpha1/mariadb/types.go
  • api/backups/strategy/v1alpha1/mariadb_types.go
  • api/backups/strategy/v1alpha1/zz_generated.deepcopy.go
  • cmd/backupstrategy-controller/main.go
  • examples/backups/mariadb/00-bucket.yaml
  • examples/backups/mariadb/05-mariadb-src.yaml
  • examples/backups/mariadb/10-mariadb-strategy.yaml
  • examples/backups/mariadb/15-backupclass.yaml
  • examples/backups/mariadb/20-plan.yaml
  • examples/backups/mariadb/25-backupjob-adhoc.yaml
  • examples/backups/mariadb/30-mariadb-target.yaml
  • examples/backups/mariadb/35-restorejob-in-place.yaml
  • examples/backups/mariadb/40-restorejob-to-copy.yaml
  • examples/backups/mariadb/README.md
  • internal/backupcontroller/backupjob_controller.go
  • internal/backupcontroller/backupjob_controller_test.go
  • internal/backupcontroller/mariadbapp/types.go
  • internal/backupcontroller/mariadbapp/zz_generated.deepcopy.go
  • internal/backupcontroller/mariadbstrategy_controller.go
  • internal/backupcontroller/mariadbstrategy_controller_test.go
  • internal/backupcontroller/mariadbtypes/types.go
  • internal/backupcontroller/mariadbtypes/zz_generated.deepcopy.go
  • internal/backupcontroller/restorejob_controller.go
  • internal/backupcontroller/restorejob_controller_test.go
  • packages/apps/mariadb/README.md
  • packages/apps/mariadb/values.schema.json
  • packages/apps/mariadb/values.yaml
  • packages/system/backupstrategy-controller/definitions/strategy.backups.cozystack.io_mariadbs.yaml
  • packages/system/backupstrategy-controller/templates/rbac.yaml
  • packages/system/mariadb-rd/cozyrds/mariadb.yaml
✅ Files skipped from review due to trivial changes (11)
  • examples/backups/mariadb/00-bucket.yaml
  • examples/backups/mariadb/10-mariadb-strategy.yaml
  • examples/backups/mariadb/30-mariadb-target.yaml
  • examples/backups/mariadb/25-backupjob-adhoc.yaml
  • examples/backups/mariadb/README.md
  • packages/apps/mariadb/README.md
  • packages/apps/mariadb/values.schema.json
  • api/apps/v1alpha1/mariadb/types.go
  • packages/system/mariadb-rd/cozyrds/mariadb.yaml
  • api/backups/strategy/v1alpha1/zz_generated.deepcopy.go
  • internal/backupcontroller/backupjob_controller_test.go
🚧 Files skipped from review as they are similar to previous changes (12)
  • internal/backupcontroller/restorejob_controller_test.go
  • cmd/backupstrategy-controller/main.go
  • internal/backupcontroller/mariadbapp/types.go
  • packages/apps/mariadb/values.yaml
  • internal/backupcontroller/backupjob_controller.go
  • internal/backupcontroller/mariadbtypes/types.go
  • internal/backupcontroller/mariadbapp/zz_generated.deepcopy.go
  • internal/backupcontroller/restorejob_controller.go
  • api/backups/strategy/v1alpha1/mariadb_types.go
  • internal/backupcontroller/mariadbtypes/zz_generated.deepcopy.go
  • internal/backupcontroller/mariadbstrategy_controller.go
  • internal/backupcontroller/mariadbstrategy_controller_test.go

@lllamnyp Timofei Larkin (lllamnyp) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review saved to: /tmp/branch-review-feat-mariadb-backups-1747055000.md

NOT LGTM

Reviewing against target branch: main (PR #2605 #2605).

The PR is overall well-built: a new strategy.backups.cozystack.io/v1alpha1 MariaDB strategy that delegates to mariadb-operator, with typed partial schemas, XValidation on storage, idempotent backup/restore Create paths, wall-clock deadlines, snapshot persistence in Backup.status.underlyingResources, and substantive table-driven tests. Two blocking issues plus several documentation / consistency drifts must be fixed before merge.


Blocker 1 — BackupReconciler.cleanupOnDelete is missing the MariaDBStrategyKind branch

internal/backupcontroller/backup_controller.go:96-123 dispatches per-strategy cleanup. The new strategy adds MariaDBStrategyKind to:

  • BackupJobReconciler.Reconcile dispatch (backupjob_controller.go:85-86)
  • RestoreJobReconciler.Reconcile dispatch (restorejob_controller.go:117-118)
  • RestoreJobReconciler.cleanupOnDelete (restorejob_controller.go:211)
  • supportedBackupStrategyKinds() (backupjob_controller.go:100-108)

but NOT to BackupReconciler.cleanupOnDelete. A MariaDB-strategy Cozystack Backup therefore falls into the default: branch which calls cleanupVeleroBackup. Today that's a happy no-op because the MariaDB driver never sets veleroBackupNameMetadataKey. However:

  1. The Altinity strategy faced the identical situation and was guarded with an explicit case strategyv1alpha1.AltinityStrategyKind: return nil precisely because falling into the Velero default is a maintenance trap. The fix even has a dedicated regression test, TestBackupCleanup_Altinity_DoesNotFallThroughToVelero (backup_controller_test.go:122-171), whose docstring spells out the exact concern: "with the old default-fallback dispatch, this would synthesise a DeleteBackupRequest. The Altinity branch must short-circuit before that happens." The new MariaDB strategy has reintroduced the same hole that test was written to guard against.
  2. If a tenant ever stamps velero.io/backup-name onto a MariaDB Backup's driverMetadata (e.g. during a migration, or by a future driver refactor that reuses the key), the default branch will synthesise a DeleteBackupRequest against cozy-velero. Cozystack does not own the mariadb-operator-side k8s.mariadb.com/Backup CR (rbac.yaml:117-122 grants only get/list/watch/create); the deletion contract for that archive lives elsewhere. The dispatcher must encode that explicitly, not by accident of which metadata key the driver happens not to set today.

Fix: add to backup_controller.go:96:

case strategyv1alpha1.MariaDBStrategyKind:
    // Cozystack Backup deletion does NOT delete the operator-side
    // k8s.mariadb.com/Backup CR. Lifecycle of that CR (and the
    // backing S3 / PVC archive) is owned by mariadb-operator's own
    // MaxRetention and by tenants / explicit teardown flows. This
    // mirrors the Altinity branch's "we do not own S3" contract.
    return nil

Required test (mirrors TestBackupCleanup_Altinity_DoesNotFallThroughToVelero): a MariaDB-strategy Cozystack Backup with veleroBackupNameMetadataKey + veleroBackupNamespaceMetadataKey stamped into DriverMetadata, plus a pre-seeded velero.io/Backup named after that key — cleanupOnDelete must complete without creating any DeleteBackupRequest and the Velero Backup must survive. Without the case, this test fails because the default branch fires cleanupVeleroBackup, which sees the metadata key, finds the Velero Backup, and creates a DBR.

Also extend TestStrategyKindForBackup (backup_controller_test.go:175-202) with a {"mariadb", strategyv1alpha1.MariaDBStrategyKind, strategyv1alpha1.MariaDBStrategyKind} row for symmetry with the other strategies — this is a small but cheap drift guard for the dispatcher's lookup helper.


Blocker 2 — Documentation references an e2e test that does not exist

Two files in this PR promise a CI e2e harness:

  • examples/backups/mariadb/README.md:31-33: "Same-namespace flows are covered by the e2e (hack/e2e-apps/backup-mariadb.bats); cross-tenant restores are blocked by the per-tenant Cilium egress policy and stay a manual flow."
  • examples/backups/mariadb/30-mariadb-target.yaml:15-18: "The CI e2e (hack/e2e-apps/backup-mariadb.bats) deploys this manifest in the same namespace as mariadb-src, exercising the same-namespace to-copy happy path."
  • examples/backups/mariadb/40-restorejob-to-copy.yaml:7-9: "Driven by the CI e2e (hack/e2e-apps/backup-mariadb.bats) for the same-namespace happy path."

hack/e2e-apps/backup-mariadb.bats does not exist on this branch (find hack -name 'backup-mariadb*' returns nothing; hack/e2e-apps/ contains mariadb.bats and a disabled backup.bats.disabled that has zero MariaDB references). A reader following the README to "see how the CI exercises this" hits a dead end. Per the rubric, documentation that contradicts the code is a blocking issue.

Fix: either land the bats file in the same PR (preferred — there is matching prior art for the CNPG/Altinity strategies; the file should exercise apply -f on the numbered manifests, write a sentinel row via the app user, kick the ad-hoc BackupJob, wait for BackupRef, run 35-restorejob-in-place.yaml, and verify the sentinel survives), or remove the three references and replace them with neutral phrasing ("smoke-testable manually with kubectl apply -f"). Do not leave references to a missing file.

Required test (for the "remove references" path): grep-style smoke verification is brittle (see project rule against parsing-style tests in ~/.claude/CLAUDE.md), but the artifact itself is exercised by find hack/e2e-apps -name backup-mariadb.bats in CI's existing hack/ discovery if such a check exists — preferred path is just to land the bats file and use the harness's own existence as the test.


Documentation drift — mariadbapp/types.go contradicts the actual driver behaviour

internal/backupcontroller/mariadbapp/types.go:55-64:

// MariaDBSpec is intentionally empty: the driver does not currently read or
// patch any apps.cozystack.io/MariaDB spec fields. It targets the downstream
// k8s.mariadb.com/MariaDB CR (rendered by the chart's HelmRelease) for both
// bootstrapFrom patching and existence checks.

The driver does NOT patch bootstrapFrom. The strategy-level docstring at api/backups/strategy/v1alpha1/mariadb_types.go:36-39 (and the rendered CRD at definitions/strategy.backups.cozystack.io_mariadbs.yaml:27-28) explicitly says "the driver does not touch MariaDB.spec.bootstrapFrom." The RBAC at packages/system/backupstrategy-controller/templates/rbac.yaml:117-119 confirms it: verbs: ["get", "list", "watch"] on k8s.mariadb.com/mariadbs. The mariadbapp/types.go comment is straight-up wrong.

This is documentation drift inside the diff. Per the rubric "if the PR changes behaviour but existing documentation still describes the old behaviour — blocking", and this file is part of the PR.

Fix: replace lines 56-59 to drop the bootstrapFrom claim. Suggested replacement:

// MariaDBSpec is intentionally empty: the driver does not currently read or
// patch any apps.cozystack.io/MariaDB spec fields. The driver only reads the
// downstream k8s.mariadb.com/MariaDB CR (rendered by the chart's HelmRelease)
// for existence checks - restores create k8s.mariadb.com/Restore CRs whose
// mariaDbRef points at the live MariaDB; the operator replays the dump into
// the running instance, so the driver does NOT touch
// MariaDB.spec.bootstrapFrom or any other field on the operator CR.

Required test: a code-level assertion that the driver does not write to mariadbtypes.MariaDB would catch a regression of the comment becoming true again, but per the project rule against grep/parse tests the right place to anchor this is the existing absence of patch verbs in the rbac.yaml and the fact that the only usage of mariadbtypes.MariaDB{} in the controller is the two r.Get calls (lines 168, 531) — no test exercises a Patch because no Patch call exists. That's the de facto guard.


Documentation drift — MariaDBSecretKeySelector.Key claims no templating, but template.Template walks every string

api/backups/strategy/v1alpha1/mariadb_types.go:170-175:

type MariaDBSecretKeySelector struct {
    // Name is the Secret name. Templating is supported.
    Name string `json:"name"`

    // Key is the key within the Secret holding the value.
    Key string `json:"key"`
}

The docstring on Name says templating is supported; the docstring on Key is silent, which a reader will interpret as "Key is NOT templated." But internal/template/template.go:41-58 walks every string in the JSON-marshalled object and substitutes it through the templating function, so Key is in fact templated. Same caveat applies to MariaDBS3Template.Bucket, Endpoint, Prefix, Region (those are documented correctly), and to MariaDBS3TLS.Enabled (which is a bool — not affected) and CASecretKeyRef.Key (silent — same drift).

This is borderline. The honest options are:

  1. Update the comments to say templating is supported on every string field of the template.
  2. Add an explicit "Templating is NOT supported on Key" guard in renderMariaDBTemplate that errors out if Key contains {{.

Pick one. Right now the doc says one thing and the code does the other — a tenant who templates Key against .Application.metadata.name to vary per-app keys will get behaviour the docs claim is unsupported.

Required test: if you go with option 1 (documentation only) — no test, just truth in advertising. If you go with option 2 (enforcement) — a unit test that feeds Key: "{{ .Application.metadata.name }}" and asserts renderMariaDBTemplate returns a non-nil error.


Recommendations (non-blocking)

  • MariaDBSpec in api/backups/strategy/v1alpha1/mariadb_types.go:57-68 documents that Parameters MUST NOT carry credentials. No enforcement exists. If a tenant sets parameters.secretAccessKey: hunter2 on a BackupClassStrategy, the value gets templated in, persisted into Backup.status.underlyingResources via marshalMariaDBBackupSnapshot, and is now world-readable in cluster RBAC scope. Not a regression introduced by this PR (the CNPG strategy has the same shape), but it would be nice to have a future-proof comment at marshalMariaDBBackupSnapshot reiterating that the snapshot is plaintext-readable and Parameters values land there verbatim.
  • mariadbBackupURI (mariadbstrategy_controller.go:417-427) handles the empty-prefix case correctly (pinned by TestMariaDBBackupURI_EmptyPrefix). One omission: when prefix == "/" exactly, the helper produces s3://bkt//<name> (double slash). Not a bug — that's a valid S3 key — but worth a follow-up "normalise leading-slash prefix" if the operator ever rejects double-slash prefixes. Not blocking.
  • findMariaDBBackupForJob (mariadbstrategy_controller.go:318-341) returns list.Items[0] on duplicate matches. The fake client lists by name; the real apiserver lists by resourceVersion (ETCd order). The test TestFindMariaDBBackupForJob_DuplicateMatches accepts either name on a single call but asserts a second call returns the same one — that's a fake-client artefact, not a property of the real apiserver. Real apiserver list order is stable per cache snapshot but can change between watches. Picking a different Items[0] between reconciles would result in driving two operator Backups in lockstep (the orphaned one stays Running and gets reaped by the deadline). Acceptable; the V(1) breadcrumb is the right place to surface that we hit the race.
  • mariadbBackupSnapshot.Storage carries the rendered secret-name fields (e.g. mariadb-src-mariadb-backup-creds). These are pointers — not the secret values — so the snapshot is safe. Re-emphasise this in the snapshot godoc to head off future readers wondering why secret refs are persisted plaintext.

Reminder: "Pre-existing" is not a thing. If a problem is flagged in this review, it must be fixed in this PR — no exceptions.

Every issue listed above must have a corresponding test. No matter the severity — bug, logic error, security issue, error handling gap, documentation drift — each one must be covered by a test that fails without the fix and passes with it. Untested fixes are not fixes.

@androndo
Andrey Kolkov (androndo) force-pushed the feat/mariadb-backups branch 3 times, most recently from 2a4612c to 9408f84 Compare May 12, 2026 09:48

@lllamnyp Timofei Larkin (lllamnyp) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

Reviewed the single new commit on feat/mariadb-backups (62605a198 feat(platform): implement backup strategy for mariadb) against main upstream. The branch carries other commits already merged upstream as part of PR #2607 (instance-type resource presets); the local main ref is stale and shows them as branch-only, but they are not part of this PR's review surface.

Scope of changes reviewed:

  • New strategy.backups.cozystack.io/v1alpha1.MariaDB CRD (api/backups/strategy/v1alpha1/mariadb_types.go + zz_generated, CRD yaml in packages/system/backupstrategy-controller/definitions/)
  • New driver internal/backupcontroller/mariadbstrategy_controller.go (~866 LOC) implementing the BackupJob + RestoreJob reconcile paths against k8s.mariadb.com/v1alpha1 Backup/Restore CRs
  • Local partial mirror of k8s.mariadb.com/v1alpha1 shapes in internal/backupcontroller/mariadbtypes/ and the apps-side handle in internal/backupcontroller/mariadbapp/
  • Dispatch wiring in backup_controller.go, backupjob_controller.go, restorejob_controller.go and cmd/backupstrategy-controller/main.go
  • RBAC additions in packages/system/backupstrategy-controller/templates/rbac.yaml
  • End-to-end fixture set under examples/backups/mariadb/
  • DEPRECATED-tagging of the legacy chart-side restic CronJob fields (packages/apps/mariadb/{values.yaml,values.schema.json,README.md} plus the openAPI schema in packages/system/mariadb-rd/cozyrds/mariadb.yaml)

Build + tests are clean: go build ./..., go vet ./internal/backupcontroller/..., and go test ./internal/backupcontroller/... -count=1 all pass.

Things I checked and confirmed are fine

  • Strategy template only accepts Secret REFERENCES (Name+Key) for S3 credentials. No code path persists raw credential values; marshalMariaDBBackupSnapshot snapshot is plaintext-readable but only carries Secret refs, not Secret bytes, with the contract documented at the type. RBAC carries no cluster-scoped Secret read verb, which is the right enforcement.
  • XValidation rule on MariaDBStorageTemplate plus the in-Go defence-in-depth check in buildMariaDBBackupStorage together pin the "exactly one of s3 / pvc / volume" invariant at admission AND at runtime; the test TestBuildMariaDBBackupStorage_RejectsMultiple exists specifically to prove the runtime guard fires even if a strategy CR predates admission.
  • Idempotency on both ensure paths is by OwningJob{Name,Namespace} label match, with findMariaDBBackupForJob deterministic on duplicate hits (TestFindMariaDBBackupForJob_DuplicateMatches).
  • Stale-cache pattern for StartedAt mirrors the CNPG path exactly: refetch fresh, patch Status, requeue.
  • markBackupJobFailed/markRestoreJobFailed use meta.SetStatusCondition, not append — pinned by TestMarkBackupJobFailed_DoesNotAppendDuplicateReady.
  • Validation hoist on the RestoreJob path: Kind/APIGroup check runs before srcBackup Get so a malformed TargetApplicationRef does not waste an apiserver round-trip — pinned by TestReconcileMariaDBRestore_KindCheckedBeforeSrcBackupGet.
  • The RestoreJob target-MariaDB-not-found branch IS deadline-gated via options.effectiveRestoreDeadline() — pinned by TestReconcileMariaDBRestore_TargetMariaDBNotFoundIsTransient and (terminal path) by TestReconcileMariaDBRestore_StartedAtPersistedContinuesInline.
  • Operator-side Backup CR not found on the RestoreJob path correctly maps to a terminal failure (the operator-side artifact has been reaped) — covered by TestReconcileMariaDBRestore_StartedAtPersistedContinuesInline.
  • mariadbBackupURI correctly handles empty Prefix (no leading double slash) — pinned by TestMariaDBBackupURI_EmptyPrefix.
  • PVC/Volume storage correctly leaves Status.Artifact = nil (no URI{URI:""} confusion) — pinned by TestCreateMariaDBBackupArtifact_ArtifactShape.
  • Cozystack Backup deletion explicitly routes MariaDB to a no-op cleanup (not Velero fall-through) — pinned by TestBackupCleanup_MariaDB_DoesNotFallThroughToVelero and supported by the RBAC explicitly NOT having a delete verb on k8s.mariadb.com/backups.
  • The unsupported-strategy diagnostic and the dispatch switch can no longer drift apart — pinned by TestSupportedBackupStrategyKindsMatchesDispatch.
  • The cozystack mariadb HelmRelease prefix (mariadb-) matches mariadbNameForApp, verified directly against packages/system/mariadb-rd/cozyrds/mariadb.yaml and the chart's templates/mariadb.yaml (which uses .Release.Name).
  • Backup CRD does NOT have +kubebuilder:subresource:status (confirmed in api/backups/v1alpha1/backup_types.go), so Create with Status populated is the documented pattern and matches CNPG/Altinity drivers.
  • Legacy backup.* block on the chart is correctly disabled by default (backup.enabled=false) and templates are gated on it, so the DEPRECATED tagging does not change runtime behaviour for existing tenants.

Follow-up backlog (non-blocking — NOT a condition of LGTM)

Surfacing these explicitly so they don't get lost in the merge. Pick up or discard as you see fit; none of them gate this PR.

FU-1 — Align BackupJob "operator-side MariaDB CR not found" with its RestoreJob counterpart

The BackupJob path's "operator-side MariaDB CR not found" branch (internal/backupcontroller/mariadbstrategy_controller.go:169-181) returns a transient Ready=False/MariaDBNotReady + requeue with no deadline gate, while the symmetric RestoreJob branch DOES deadline-gate that case via options.effectiveRestoreDeadline().

  • Symptom: If a tenant fires a BackupJob against an applicationRef whose chart never produces the operator-side MariaDB CR, the BackupJob is stuck Running until either the CR appears or the user manually deletes the BackupJob.
  • Eventual safety net: once the MariaDB CR does materialise and ensureMariaDBBackup creates an operator-side Backup, the existing mariadbBackupDeadlineExceeded gate kicks in 30m later. So the worst case is "no early failure, but eventual failure once the operator CR exists."
  • Why it's not blocking: the CNPG driver ships the exact same shape today and the trade-off is deliberate there. Aligning the MariaDB driver internally is the smaller of the two follow-ups; aligning all drivers is the bigger one.
  • Suggested test when picked up: a unit test asserting that a BackupJob whose applicationRef points at a MariaDB app with no operator-side k8s.mariadb.com/MariaDB CR transitions to Failed after the effectiveBackupDeadline() window, mirroring TestReconcileMariaDBRestore_TargetMariaDBNotFoundIsTransient.

FU-2 — Template engine swallows malformed templates (cross-driver)

internal/template/template.go silently returns the original input on template-evaluation error (if err != nil { return in }).

  • Symptom: A typo like {{ .Application.metadata.name (missing closing brace) in a strategy template yields a literal "{{ .Application.metadata.name" in the rendered operator-side Backup CR. The mariadb-operator then rejects the resulting Backup with its own error message, which is the only breadcrumb the tenant gets.
  • Not new in this PR: CNPG and Velero drivers share the same engine and the same trade-off. Out of scope for the MariaDB PR.
  • Suggested fix when picked up: log the template-evaluation error at V(0) with the strategy ref attached, OR surface as a Ready=False/TemplateRenderFailed condition on the BackupJob/RestoreJob. Either is a cross-driver change that should live in its own PR.

FU-3 — Automate the COSI BucketInfo → mariadb-operator credential Secret extraction

The example fixture under examples/backups/mariadb/ documents that tenants must hand-roll a per-app <app>-mariadb-backup-creds Secret extracted from the COSI BucketInfo blob, because mariadb-operator expects separate AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY keys, while the COSI BucketClaim materialises a single BucketInfo JSON blob.

  • Symptom: known UX gap. The README acknowledges it and defers automation to a follow-up bats e2e harness.
  • Why it's not blocking: it is explicitly called out in the example README and is a UX rough edge, not a regression — the existing manual flow works.
  • Suggested shape: either a small webhook/controller that watches BucketInfo Secrets matching a label and projects out the two operator-shaped keys, or build the projection into the BackupClass / Plan reconcile path. The bats e2e harness will need this either way.

Ticket Compliance: not applicable (no --ticket was provided).

@androndo
Andrey Kolkov (androndo) merged commit 5f83ae7 into main May 12, 2026
10 checks passed
@androndo
Andrey Kolkov (androndo) deleted the feat/mariadb-backups branch May 12, 2026 13:10
@kvaps Andrei Kvapil (kvaps) added the kind/backport Categorizes issue or PR as requiring a backport to the current release line label May 14, 2026
@github-actions

Copy link
Copy Markdown

Backport failed for release-1.3, because it was unable to cherry-pick the commit(s).

Please cherry-pick the changes locally and resolve any conflicts.

git fetch origin release-1.3
git worktree add -d .worktree/backport-2605-to-release-1.3 origin/release-1.3
cd .worktree/backport-2605-to-release-1.3
git switch --create backport-2605-to-release-1.3
git cherry-pick -x 62605a198bfc0d8f3d834d01bd6c044a64c2ad8b

@kvaps Andrei Kvapil (kvaps) removed the kind/backport Categorizes issue or PR as requiring a backport to the current release line label May 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/database Issues or PRs related to managed databases (postgres, mariadb, redis, etcd, kafka, clickhouse) area/platform Issues or PRs related to platform infrastructure (bundle, flux, talos, installer) 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