Skip to content

feat(backups): no manual actions for restore VMI - #2251

Merged
Andrey Kolkov (androndo) merged 1 commit into
mainfrom
feat/less-manual-actions-on-restore
Mar 31, 2026
Merged

Andrey Kolkov (androndo) merged 1 commit into
mainfrom
feat/less-manual-actions-on-restore

Conversation

@androndo

@androndo Andrey Kolkov (androndo) commented Mar 20, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

Release note

Introduce more accurate in-place backup/restores for VMDisk and VMInstance:
- keep IP/MAC of VM
- keep HR of VMDisk related to VMI volumes
- avoid Velero restore of DVs because it conflicts with  DV from HR of VMDisk
- propagate to BackupJob/RestoreJob failure messages from Velero to be more clear for namespace users when some job fails

Summary by CodeRabbit

  • New Features

    • Capture and persist underlying resource metadata (data volumes, OVN IP/MAC) in Backup status and Velero annotations; use it to better scope backups/restores.
    • New Backup controller to ensure Velero cleanup for deleted backups.
    • Pre-restore preparation: suspend releases, halt VMs, rename PVCs, delete DataVolumes, and apply resource-modifier rules for claim adoption and network annotations.
  • Bug Fixes

    • Improved Velero failure messages (includes hooks, async item ops, and DataUpload failures).
  • Chores

    • Expanded RBAC and added RoleBinding to support Velero/restore operations.

@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 significantly enhances the backup and restore capabilities for applications, particularly Virtual Machines, by automating the collection and re-application of critical underlying resource information. By storing details like VM disk names and network configurations (IP/MAC addresses) directly within the Backup object, the system can perform more robust and less manual restores. Additionally, a new controller ensures that Velero backups are properly cleaned up when their corresponding Backup resource is deleted, improving resource management.

Highlights

  • Enhanced Backup Data Model: Introduced new API types DataVolumeResource and UnderlyingResources to store detailed information about application resources (like VM disks, IP, and MAC addresses) directly within the BackupStatus object.
  • Automated Velero Backup Cleanup: Implemented a new BackupReconciler to manage the lifecycle of Backup objects, specifically adding a finalizer to ensure proper cleanup of associated Velero backups upon deletion.
  • VM-Specific Resource Collection and Restoration: Enhanced the VeleroStrategy controller to automatically discover and persist VM-specific underlying resources (data volumes, OVN IP/MAC addresses) during the backup process, and to generate Velero resource modifiers during restore to automatically re-apply network configurations to restored VMs.
  • Independent Backup Resource Lifecycle: Removed the OwnerReference from the Backup object to the BackupJob, allowing Backup resources to persist independently of their originating BackupJob for better artifact management.

🧠 New Feature in Public Preview: You can now enable Memory 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.

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.

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

@androndo
Andrey Kolkov (androndo) force-pushed the feat/less-manual-actions-on-restore branch from 1817037 to 8c52eb5 Compare March 20, 2026 07:08
@coderabbitai

coderabbitai Bot commented Mar 20, 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 VM underlying-resource discovery and persistence to Velero Backups and Backup status, a BackupReconciler that issues Velero DeleteBackupRequests on finalization, pre-restore preparation and resource-modifier wiring, deepcopy/CRD updates, RBAC/Helm template changes, and controller registration.

Changes

Cohort / File(s) Summary
API types & deepcopy
api/backups/v1alpha1/backup_types.go, api/backups/v1alpha1/zz_generated.deepcopy.go
Added DataVolumeResource and UnderlyingResources; added BackupStatus.UnderlyingResources and generated/updated deepcopy methods.
Backup CRD
packages/system/backup-controller/definitions/backups.cozystack.io_backups.yaml
Extended Backup CRD status with underlyingResources (fields: dataVolumes[] {applicationName,dataVolumeName}, ip, mac).
Backup reconciler & registration
internal/backupcontroller/backup_controller.go, cmd/backupstrategy-controller/main.go
New BackupReconciler with finalizer handling that creates Velero DeleteBackupRequest; registered controller in manager.
Velero strategy & restore flow
internal/backupcontroller/velerostrategy_controller.go
Added VM-aware underlying resource discovery, annotate Velero Backups with underlying-resources, include disk selectors, persist annotation into Backup status, add pre-restore prepare steps, create resource-modifier ConfigMaps, and patch restore behavior.
RestoreJob reconciler & tests
internal/backupcontroller/restorejob_controller.go, internal/backupcontroller/backupclass_resolver_test.go
Added restore finalizer and cleanupVeleroRestore, removed getTargetApplicationRef, removed test helper stringPtr.
RBAC & Helm templates
packages/system/backupstrategy-controller/templates/rbac.yaml, packages/system/backupstrategy-controller/templates/rbac-bind.yaml
Expanded ClusterRole verbs/resources (Velero datauploads, deletebackuprequests; pods/configmaps; KubeVirt/CDI/Helm/PVC/PV permissions); added namespaced Role and RoleBinding for cozy-velero configmap management.
Examples
examples/desired-backup.yaml
Deleted example manifest desired-backup.yaml.

Sequence Diagram

sequenceDiagram
    participant Strat as VeleroStrategyController
    participant VM as VMInstance
    participant Pod as VM Pod
    participant VeleroB as Velero Backup
    participant InternalB as Backup (backups.cozystack.io)
    participant Cleanup as BackupReconciler

    Note over Strat,VM: Backup creation
    Strat->>VM: collect spec.disks -> dataVolume names
    Strat->>Pod: query OVN IP/MAC annotations
    Strat->>VeleroB: create Backup (OrLabelSelectors + annotation `backups.cozystack.io/underlying-resources`)

    Note over VeleroB,InternalB: Propagation
    VeleroB-->>InternalB: controller reads annotation -> persists into status.underlyingResources

    Note over InternalB,Cleanup: Deletion / cleanup
    InternalB->>Cleanup: deletion detected (finalizer present)
    Cleanup->>VeleroB: read driverMetadata -> create DeleteBackupRequest
    Cleanup-->>InternalB: remove finalizer & update
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • lexfrei
  • kvaps

Poem

🐰 I hopped through backups, disks, and nets,
I sniffed the pods for IP and MAC sets,
I tucked the data in Velero notes,
And nudged the store when the finalizer votes,
🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'no manual actions for restore VMI' directly aligns with the PR's main objective of automating restore procedures and eliminating manual intervention steps, as evidenced by the prepareForRestore functionality and resource modifier creation.

✏️ 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/less-manual-actions-on-restore

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 significant enhancements to the backup and restore process, particularly for VM-based applications. It adds logic to discover and persist underlying resources like VM disks and network configurations (IP/MAC addresses) during backup. This information is then leveraged during restore to automatically configure the restored VM, reducing manual steps. A new BackupReconciler is also introduced to manage the lifecycle of Backup resources and ensure proper cleanup of underlying Velero backups. While this is a valuable improvement, I've identified a critical compilation error in the new controller, a high-severity functional regression in the restore logic, and a medium-severity maintainability concern.

velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
)

const backupFinalizer = "backups.cozystack.io/cleanup-velero"

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.

critical

This file uses several constants (veleroBackupNameMetadataKey, veleroBackupNamespaceMetadataKey, veleroNamespace) that are not defined, which will lead to a compilation error. These constants appear to be defined in internal/backupcontroller/velerostrategy_controller.go.

To fix this, you should define these constants in this file. For better maintainability, consider moving them to a shared package if they are used in multiple places.

const (
	backupFinalizer                  = "backups.cozystack.io/cleanup-velero"
	veleroBackupNameMetadataKey      = "velero.io/backup-name"
	veleroBackupNamespaceMetadataKey = "velero.io/backup-namespace"
	veleroNamespace                  = "cozy-velero"
)

Comment on lines 649 to +656
templateContext := map[string]interface{}{
"Application": app.Object,
"Application": map[string]interface{}{
"metadata": map[string]interface{}{
"name": backup.Spec.ApplicationRef.Name,
"namespace": backup.Namespace,
},
"kind": backup.Spec.ApplicationRef.Kind,
},

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 template context for the restore is now built using the original application's name and namespace from the Backup object. This implementation ignores restoreJob.Spec.TargetApplicationRef, which is designed to allow restoring an application to a different name or location. This change breaks a key feature of the restore functionality.

The context should be built using the effective target application reference, which can be determined by calling r.getTargetApplicationRef(restoreJob, backup).

Suggested change
templateContext := map[string]interface{}{
"Application": app.Object,
"Application": map[string]interface{}{
"metadata": map[string]interface{}{
"name": backup.Spec.ApplicationRef.Name,
"namespace": backup.Namespace,
},
"kind": backup.Spec.ApplicationRef.Kind,
},
targetAppRef := r.getTargetApplicationRef(restoreJob, backup)
templateContext := map[string]interface{}{
"Application": map[string]interface{}{
"metadata": map[string]interface{}{
"name": targetAppRef.Name,
"namespace": restoreJob.Namespace,
},
"kind": targetAppRef.Kind,
},

Comment thread internal/backupcontroller/velerostrategy_controller.go Outdated
@androndo
Andrey Kolkov (androndo) force-pushed the feat/less-manual-actions-on-restore branch from 8c52eb5 to d491bb4 Compare March 20, 2026 07:12
@androndo Andrey Kolkov (androndo) changed the title Feat/less manual actions on restore feat(backups)/ no manual actions for restore Mar 20, 2026
@androndo
Andrey Kolkov (androndo) force-pushed the feat/less-manual-actions-on-restore branch 2 times, most recently from 06f140b to 3295820 Compare March 23, 2026 21:59
@androndo Andrey Kolkov (androndo) changed the title feat(backups)/ no manual actions for restore feat(backups)/ no manual actions for restore VMI Mar 24, 2026
@androndo
Andrey Kolkov (androndo) force-pushed the feat/less-manual-actions-on-restore branch 4 times, most recently from b17efd8 to 41e4d63 Compare March 24, 2026 08:26
@androndo
Andrey Kolkov (androndo) marked this pull request as ready for review March 24, 2026 08:33
@dosubot dosubot Bot added size/XL This PR changes 500-999 lines, ignoring generated files kind/feature Categorizes issue or PR as related to a new feature labels Mar 24, 2026

@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 (2)
internal/backupcontroller/backup_controller.go (1)

25-29: Consider removing unused Recorder field.

The Recorder field is declared but never used in this controller. The cleanupVeleroBackup method logs events but doesn't emit Kubernetes events via the recorder. Either remove the field or add event recording for cleanup operations.

🔧 Option 1: Remove unused field
 type BackupReconciler struct {
 	client.Client
 	Scheme   *runtime.Scheme
-	Recorder record.EventRecorder
 }

And update main.go accordingly.

🔧 Option 2: Add event recording for cleanup
 	if err := r.Delete(ctx, veleroBackup); err != nil {
 		if apierrors.IsNotFound(err) {
 			return nil
 		}
 		return fmt.Errorf("failed to delete Velero backup %s/%s: %w", veleroBackupNamespace, veleroBackupName, err)
 	}

+	r.Recorder.Event(backup, corev1.EventTypeNormal, "VeleroBackupDeleted",
+		fmt.Sprintf("Deleted Velero Backup %s/%s", veleroBackupNamespace, veleroBackupName))
 	logger.Info("deleted Velero backup", "name", veleroBackupName, "namespace", veleroBackupNamespace)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/backupcontroller/backup_controller.go` around lines 25 - 29, The
BackupReconciler currently declares an unused Recorder field; either remove the
field from the BackupReconciler struct and delete any related initialization in
main.go, or wire the recorder into cleanup operations by using r.Recorder to
emit Kubernetes events (e.g., r.Recorder.Eventf or r.Recorder.Event) inside
cleanupVeleroBackup and initialize it in main.go with
mgr.GetEventRecorderFor("backup-controller") when constructing BackupReconciler;
update references accordingly so there are no unused fields or missing event
recorder initialization.
cmd/backupstrategy-controller/main.go (1)

179-186: Consider using a distinct event recorder name for BackupReconciler.

The event recorder name "backup-controller" on line 182 is the same as the one used for BackupJobReconciler on line 164. While this won't cause functional issues, using a distinct name like "backup-cleanup-controller" would improve event attribution clarity when debugging.

🔧 Suggested change
 	if err = (&backupcontroller.BackupReconciler{
 		Client:   mgr.GetClient(),
 		Scheme:   mgr.GetScheme(),
-		Recorder: mgr.GetEventRecorderFor("backup-controller"),
+		Recorder: mgr.GetEventRecorderFor("backup-cleanup-controller"),
 	}).SetupWithManager(mgr); err != nil {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/backupstrategy-controller/main.go` around lines 179 - 186, The
BackupReconciler is using the same event recorder name string
"backup-controller" as BackupJobReconciler; update the recorder invocation in
the BackupReconciler setup (the mgr.GetEventRecorderFor(...) call inside the
BackupReconciler initialization used by BackupReconciler.SetupWithManager) to a
distinct name such as "backup-cleanup-controller" so events are attributed
clearly when debugging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/backupcontroller/velerostrategy_controller.go`:
- Around line 655-659: The ConfigMap name generation using cmName :=
fmt.Sprintf("restore-modifiers-%s-%s", restoreJob.Namespace, restoreJob.Name)
then bluntly truncating to 253 chars can yield invalid DNS-1123 names (may
start/end with non-alphanumeric or end with a hyphen); update the logic around
cmName to: build the base string, lowercase it, truncate to 253 characters but
then trim any non-alphanumeric trailing/leading characters (e.g., remove
trailing '-' characters), and if trimming yields an empty suffix ensure a safe
fallback token; implement this around the cmName variable (where cmName is
created) and/or extract a helper like sanitizeDNS1123Name(name string) that
enforces lowercase, max length 253, and start/end alphanumeric requirements
before using cmName.

---

Nitpick comments:
In `@cmd/backupstrategy-controller/main.go`:
- Around line 179-186: The BackupReconciler is using the same event recorder
name string "backup-controller" as BackupJobReconciler; update the recorder
invocation in the BackupReconciler setup (the mgr.GetEventRecorderFor(...) call
inside the BackupReconciler initialization used by
BackupReconciler.SetupWithManager) to a distinct name such as
"backup-cleanup-controller" so events are attributed clearly when debugging.

In `@internal/backupcontroller/backup_controller.go`:
- Around line 25-29: The BackupReconciler currently declares an unused Recorder
field; either remove the field from the BackupReconciler struct and delete any
related initialization in main.go, or wire the recorder into cleanup operations
by using r.Recorder to emit Kubernetes events (e.g., r.Recorder.Eventf or
r.Recorder.Event) inside cleanupVeleroBackup and initialize it in main.go with
mgr.GetEventRecorderFor("backup-controller") when constructing BackupReconciler;
update references accordingly so there are no unused fields or missing event
recorder initialization.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6a6fd0a6-9cc7-462d-9dc5-6954ddad858c

📥 Commits

Reviewing files that changed from the base of the PR and between 3119be1 and 41e4d63.

📒 Files selected for processing (11)
  • api/backups/v1alpha1/backup_types.go
  • api/backups/v1alpha1/zz_generated.deepcopy.go
  • cmd/backupstrategy-controller/main.go
  • examples/desired-backup.yaml
  • internal/backupcontroller/backup_controller.go
  • internal/backupcontroller/backupclass_resolver_test.go
  • internal/backupcontroller/restorejob_controller.go
  • internal/backupcontroller/velerostrategy_controller.go
  • packages/system/backup-controller/definitions/backups.cozystack.io_backups.yaml
  • packages/system/backupstrategy-controller/templates/rbac-bind.yaml
  • packages/system/backupstrategy-controller/templates/rbac.yaml
💤 Files with no reviewable changes (3)
  • internal/backupcontroller/backupclass_resolver_test.go
  • examples/desired-backup.yaml
  • internal/backupcontroller/restorejob_controller.go

Comment on lines +655 to +659
cmName := fmt.Sprintf("restore-modifiers-%s-%s", restoreJob.Namespace, restoreJob.Name)
// Truncate name to fit Kubernetes 253-char limit
if len(cmName) > 253 {
cmName = cmName[:253]
}

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

ConfigMap name truncation may produce invalid names.

The truncation at exactly 253 characters could cut a name mid-character or leave a trailing hyphen, which would be invalid. Kubernetes names must also start and end with alphanumeric characters.

🔧 Suggested improvement
 	cmName := fmt.Sprintf("restore-modifiers-%s-%s", restoreJob.Namespace, restoreJob.Name)
-	// Truncate name to fit Kubernetes 253-char limit
-	if len(cmName) > 253 {
-		cmName = cmName[:253]
+	// Truncate name to fit Kubernetes 253-char limit, ensuring valid ending
+	if len(cmName) > 253 {
+		cmName = strings.TrimRight(cmName[:253], "-.")
 	}
📝 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
cmName := fmt.Sprintf("restore-modifiers-%s-%s", restoreJob.Namespace, restoreJob.Name)
// Truncate name to fit Kubernetes 253-char limit
if len(cmName) > 253 {
cmName = cmName[:253]
}
cmName := fmt.Sprintf("restore-modifiers-%s-%s", restoreJob.Namespace, restoreJob.Name)
// Truncate name to fit Kubernetes 253-char limit, ensuring valid ending
if len(cmName) > 253 {
cmName = strings.TrimRight(cmName[:253], "-.")
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/backupcontroller/velerostrategy_controller.go` around lines 655 -
659, The ConfigMap name generation using cmName :=
fmt.Sprintf("restore-modifiers-%s-%s", restoreJob.Namespace, restoreJob.Name)
then bluntly truncating to 253 chars can yield invalid DNS-1123 names (may
start/end with non-alphanumeric or end with a hyphen); update the logic around
cmName to: build the base string, lowercase it, truncate to 253 characters but
then trim any non-alphanumeric trailing/leading characters (e.g., remove
trailing '-' characters), and if trimming yields an empty suffix ensure a safe
fallback token; implement this around the cmName variable (where cmName is
created) and/or extract a helper like sanitizeDNS1123Name(name string) that
enforces lowercase, max length 253, and start/end alphanumeric requirements
before using cmName.

@dosubot dosubot Bot added size/XXL This PR changes 1000+ lines, ignoring generated files and removed size/XL This PR changes 500-999 lines, ignoring generated files labels Mar 25, 2026

@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 (3)
internal/backupcontroller/backup_controller.go (1)

117-123: IsAlreadyExists check is unreachable with GenerateName.

When using GenerateName, the API server generates a unique name for each Create request, so IsAlreadyExists will never be returned. This dead code path could mask issues if the logic changes. Consider removing it or adding a comment explaining the defensive intent.

🧹 Suggested cleanup
 	if err := r.Create(ctx, dbr); err != nil {
-		if apierrors.IsAlreadyExists(err) {
-			logger.V(1).Info("DeleteBackupRequest already exists", "backup", veleroBackupName)
-			return nil
-		}
 		return fmt.Errorf("failed to create DeleteBackupRequest for %s/%s: %w", veleroBackupNamespace, veleroBackupName, err)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/backupcontroller/backup_controller.go` around lines 117 - 123, The
IsAlreadyExists check inside the Create handling for the DeleteBackupRequest is
unreachable because the request uses GenerateName (so the API server will never
return AlreadyExists); update the Create error handling in the function that
calls r.Create for the DeleteBackupRequest to either remove the
apierrors.IsAlreadyExists branch (and just return the formatted error) or
replace it with a short comment clarifying this is defensive and why it remains;
reference the r.Create call that constructs the DeleteBackupRequest and the
apierrors.IsAlreadyExists/ logger.V(1).Info code paths when making the change.
internal/backupcontroller/velerostrategy_controller.go (1)

932-963: Consider crash recovery scenario in PVC rename sequence.

If the controller crashes after deleting the old PVC (line 933) but before creating the new PVC (line 961), the PV will be left orphaned with Retain policy and no associated PVC. On requeue, the idempotency check at line 901 would pass (new PVC doesn't exist), but the original PVC is already gone.

The current logic handles this gracefully since line 907-909 returns nil when the original PVC is not found. However, the user would lose the ability to "rename" the PVC if this edge case occurs.

Consider adding a check for an existing PV with a cleared claimRef that matches the expected pvName to improve recovery:

💡 Recovery improvement suggestion
+	// Check if PV already exists with cleared claimRef (recovery from partial rename)
+	pv := &corev1.PersistentVolume{}
+	if err := r.Get(ctx, client.ObjectKey{Name: pvName}, pv); err == nil {
+		if pv.Spec.ClaimRef == nil || pv.Spec.ClaimRef.Name == "" {
+			// PV exists and is unbound - proceed to create new PVC
+			goto createNewPVC
+		}
+	}
+
 	// Get the original PVC
 	oldPVC := &corev1.PersistentVolumeClaim{}

Alternatively, document this limitation in the function comment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/backupcontroller/velerostrategy_controller.go` around lines 932 -
963, The delete-then-create PVC flow can leave the PV orphaned if the controller
crashes after deleting oldPVC but before creating newPVC; modify the
reconciliation around the oldPVC removal (the block handling r.Get for oldPVC
and the subsequent pv/pvName handling) so that if oldPVC is not found you
re-fetch the PV (r.Get with client.ObjectKey{Name: pvName}) and, when that PV
exists and pv.Spec.ClaimRef == nil (and optionally PV.Status.Phase indicates it
was bound), proactively recreate the new PVC (newPVC with VolumeName: pvName and
the same spec as oldPVC) or adopt the PV by setting ClaimRef appropriately
before calling r.Create/r.Update; otherwise fall back to returning nil or
document the limitation—use the existing variables (oldPVC, newPVC, pv, pvName)
and methods (r.Get, r.Create, r.Update) to implement the recovery path.
packages/system/backupstrategy-controller/templates/rbac.yaml (1)

60-63: Remove unnecessary delete verb for Velero resources.

The delete verb is granted for backups, restores, and datauploads, but the controller never directly deletes these resources. Instead, it creates DeleteBackupRequest objects (see backup_controller.go lines 74-125), which is the proper Velero mechanism. Direct deletion of Backup objects would be ineffective anyway since BSL sync recreates the object from storage. The RBAC already correctly permits deletebackuprequests management.

Suggested least-privilege adjustment
 # Velero Backup/Restore/DataUpload in cozy-velero namespace (DataUpload: enrich BackupJob failure messages)
 - apiGroups: ["velero.io"]
   resources: ["backups", "restores", "datauploads"]
-  verbs: ["create", "get", "list", "watch", "update", "patch", "delete"]
+  verbs: ["create", "get", "list", "watch", "update", "patch"]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/system/backupstrategy-controller/templates/rbac.yaml` around lines
60 - 63, Remove the unnecessary "delete" verb from the Velero resource rules in
the RBAC template so the Role no longer grants delete on resources
["backups","restores","datauploads"]; the controller uses DeleteBackupRequest
objects (see backup_controller.go handling around DeleteBackupRequest creation)
instead, and delete privilege is already provided for deletebackuprequests, so
simply delete "delete" from the verbs array for that rule in rbac.yaml.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@internal/backupcontroller/backup_controller.go`:
- Around line 117-123: The IsAlreadyExists check inside the Create handling for
the DeleteBackupRequest is unreachable because the request uses GenerateName (so
the API server will never return AlreadyExists); update the Create error
handling in the function that calls r.Create for the DeleteBackupRequest to
either remove the apierrors.IsAlreadyExists branch (and just return the
formatted error) or replace it with a short comment clarifying this is defensive
and why it remains; reference the r.Create call that constructs the
DeleteBackupRequest and the apierrors.IsAlreadyExists/ logger.V(1).Info code
paths when making the change.

In `@internal/backupcontroller/velerostrategy_controller.go`:
- Around line 932-963: The delete-then-create PVC flow can leave the PV orphaned
if the controller crashes after deleting oldPVC but before creating newPVC;
modify the reconciliation around the oldPVC removal (the block handling r.Get
for oldPVC and the subsequent pv/pvName handling) so that if oldPVC is not found
you re-fetch the PV (r.Get with client.ObjectKey{Name: pvName}) and, when that
PV exists and pv.Spec.ClaimRef == nil (and optionally PV.Status.Phase indicates
it was bound), proactively recreate the new PVC (newPVC with VolumeName: pvName
and the same spec as oldPVC) or adopt the PV by setting ClaimRef appropriately
before calling r.Create/r.Update; otherwise fall back to returning nil or
document the limitation—use the existing variables (oldPVC, newPVC, pv, pvName)
and methods (r.Get, r.Create, r.Update) to implement the recovery path.

In `@packages/system/backupstrategy-controller/templates/rbac.yaml`:
- Around line 60-63: Remove the unnecessary "delete" verb from the Velero
resource rules in the RBAC template so the Role no longer grants delete on
resources ["backups","restores","datauploads"]; the controller uses
DeleteBackupRequest objects (see backup_controller.go handling around
DeleteBackupRequest creation) instead, and delete privilege is already provided
for deletebackuprequests, so simply delete "delete" from the verbs array for
that rule in rbac.yaml.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c944168c-d99d-4580-bd52-d1fd9a3723cc

📥 Commits

Reviewing files that changed from the base of the PR and between 41e4d63 and 37bc12a.

📒 Files selected for processing (3)
  • internal/backupcontroller/backup_controller.go
  • internal/backupcontroller/velerostrategy_controller.go
  • packages/system/backupstrategy-controller/templates/rbac.yaml

@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 (3)
internal/backupcontroller/restorejob_controller.go (3)

69-75: Consider returning after adding finalizer.

After successfully adding the finalizer, the common controller-runtime pattern is to return early and let the reconciliation re-trigger with the updated object. This avoids operating on a potentially stale in-memory copy.

♻️ Suggested change
 	// Ensure finalizer is present
 	if !controllerutil.ContainsFinalizer(restoreJob, restoreJobFinalizer) {
 		controllerutil.AddFinalizer(restoreJob, restoreJobFinalizer)
 		if err := r.Update(ctx, restoreJob); err != nil {
 			return ctrl.Result{}, err
 		}
+		return ctrl.Result{}, nil
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/backupcontroller/restorejob_controller.go` around lines 69 - 75, The
block that adds the finalizer (using controllerutil.ContainsFinalizer,
controllerutil.AddFinalizer, and r.Update on restoreJob with
restoreJobFinalizer) should return immediately after a successful r.Update to
avoid continuing reconciliation on a stale in-memory object; change the flow so
that after adding the finalizer and updating the API you return (e.g., return
ctrl.Result{Requeue: true}, nil or return ctrl.Result{}, nil) so the reconcile
loop is re-triggered with the fresh object instead of proceeding.

56-67: Cleanup errors don't block finalizer removal, potentially orphaning Velero resources.

If cleanupVeleroRestore fails to delete Velero Restore or ConfigMap resources (e.g., transient API error), the finalizer is still removed and the RestoreJob deletion proceeds. This could leave orphaned Velero resources in the velero namespace.

Consider returning an error from cleanupVeleroRestore and blocking finalizer removal on failure, or at minimum re-queuing for a retry:

♻️ Suggested approach to handle cleanup failures
 	// Handle deletion: clean up Velero Restore
 	if !restoreJob.DeletionTimestamp.IsZero() {
 		if controllerutil.ContainsFinalizer(restoreJob, restoreJobFinalizer) {
-			r.cleanupVeleroRestore(ctx, restoreJob)
+			if err := r.cleanupVeleroRestore(ctx, restoreJob); err != nil {
+				logger.Error(err, "cleanup failed, will retry")
+				return ctrl.Result{}, err
+			}
 			controllerutil.RemoveFinalizer(restoreJob, restoreJobFinalizer)

And update cleanupVeleroRestore to return an error (see related comment below).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/backupcontroller/restorejob_controller.go` around lines 56 - 67, The
finalizer removal block currently calls r.cleanupVeleroRestore(ctx, restoreJob)
but ignores its failure; change cleanupVeleroRestore to return an error and in
the deletion path check that error and abort removal if non-nil: call err :=
r.cleanupVeleroRestore(ctx, restoreJob), if err != nil return ctrl.Result{}, err
(or requeue), only then call controllerutil.RemoveFinalizer(restoreJob,
restoreJobFinalizer) and r.Update(ctx, restoreJob); ensure
cleanupVeleroRestore's signature and all callers are updated to handle/propagate
the error.

156-177: Consider returning errors and consistent event recording.

Two issues with this cleanup method:

  1. No error return: The method logs errors but doesn't return them, preventing the caller from deciding whether to retry or block deletion.

  2. Inconsistent observability: Velero Restore deletion failure records a warning event (line 170-171), but ConfigMap deletion failure does not. Consider consistent behavior for both.

♻️ Proposed refactor to return errors and add consistent event recording
-func (r *RestoreJobReconciler) cleanupVeleroRestore(ctx context.Context, restoreJob *backupsv1alpha1.RestoreJob) {
+func (r *RestoreJobReconciler) cleanupVeleroRestore(ctx context.Context, restoreJob *backupsv1alpha1.RestoreJob) error {
 	logger := log.FromContext(ctx)
 	opts := []client.DeleteAllOfOption{
 		client.InNamespace(veleroNamespace),
 		client.MatchingLabels{
 			backupsv1alpha1.OwningJobNameLabel:      restoreJob.Name,
 			backupsv1alpha1.OwningJobNamespaceLabel: restoreJob.Namespace,
 		},
 	}
 
 	if err := r.DeleteAllOf(ctx, &velerov1.Restore{}, opts...); err != nil {
 		logger.Error(err, "failed to delete Velero Restore(s)")
 		r.Recorder.Event(restoreJob, corev1.EventTypeWarning, "CleanupFailed",
 			fmt.Sprintf("Failed to delete Velero Restore: %v", err))
+		return err
 	}
 
 	if err := r.DeleteAllOf(ctx, &corev1.ConfigMap{}, opts...); err != nil {
 		logger.Error(err, "failed to delete resourceModifiers ConfigMap(s)")
+		r.Recorder.Event(restoreJob, corev1.EventTypeWarning, "CleanupFailed",
+			fmt.Sprintf("Failed to delete resourceModifier ConfigMap: %v", err))
+		return err
 	}
+	return nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/backupcontroller/restorejob_controller.go` around lines 156 - 177,
The cleanupVeleroRestore function should return an error and consistently record
events for both deletion failures: change the signature of cleanupVeleroRestore
to (ctx context.Context, restoreJob *backupsv1alpha1.RestoreJob) error, capture
errors from both r.DeleteAllOf calls (for &velerov1.Restore{} and
&corev1.ConfigMap{}), call r.Recorder.Event with a matching warning event and
descriptive message for the ConfigMap deletion failure (same pattern used for
Velero Restore), and return a consolidated error (or the first error) to the
caller so the reconciler can decide retry/abort; update any callers to handle
the returned error accordingly and keep logger.Error calls for both failures
(use RestoreJobReconciler, cleanupVeleroRestore, r.DeleteAllOf,
r.Recorder.Event, and log.FromContext to locate places to edit).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@internal/backupcontroller/restorejob_controller.go`:
- Around line 69-75: The block that adds the finalizer (using
controllerutil.ContainsFinalizer, controllerutil.AddFinalizer, and r.Update on
restoreJob with restoreJobFinalizer) should return immediately after a
successful r.Update to avoid continuing reconciliation on a stale in-memory
object; change the flow so that after adding the finalizer and updating the API
you return (e.g., return ctrl.Result{Requeue: true}, nil or return
ctrl.Result{}, nil) so the reconcile loop is re-triggered with the fresh object
instead of proceeding.
- Around line 56-67: The finalizer removal block currently calls
r.cleanupVeleroRestore(ctx, restoreJob) but ignores its failure; change
cleanupVeleroRestore to return an error and in the deletion path check that
error and abort removal if non-nil: call err := r.cleanupVeleroRestore(ctx,
restoreJob), if err != nil return ctrl.Result{}, err (or requeue), only then
call controllerutil.RemoveFinalizer(restoreJob, restoreJobFinalizer) and
r.Update(ctx, restoreJob); ensure cleanupVeleroRestore's signature and all
callers are updated to handle/propagate the error.
- Around line 156-177: The cleanupVeleroRestore function should return an error
and consistently record events for both deletion failures: change the signature
of cleanupVeleroRestore to (ctx context.Context, restoreJob
*backupsv1alpha1.RestoreJob) error, capture errors from both r.DeleteAllOf calls
(for &velerov1.Restore{} and &corev1.ConfigMap{}), call r.Recorder.Event with a
matching warning event and descriptive message for the ConfigMap deletion
failure (same pattern used for Velero Restore), and return a consolidated error
(or the first error) to the caller so the reconciler can decide retry/abort;
update any callers to handle the returned error accordingly and keep
logger.Error calls for both failures (use RestoreJobReconciler,
cleanupVeleroRestore, r.DeleteAllOf, r.Recorder.Event, and log.FromContext to
locate places to edit).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 09ffc56d-ee18-4738-8d26-45be7610e056

📥 Commits

Reviewing files that changed from the base of the PR and between 37bc12a and 090566c.

📒 Files selected for processing (1)
  • internal/backupcontroller/restorejob_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.

♻️ Duplicate comments (1)
internal/backupcontroller/velerostrategy_controller.go (1)

667-671: ⚠️ Potential issue | 🟡 Minor

ConfigMap name truncation may produce invalid names.

The truncation at exactly 253 characters could cut a name mid-character or leave a trailing hyphen, which would be invalid. Kubernetes names must also start and end with alphanumeric characters.

🔧 Suggested improvement
 	cmName := fmt.Sprintf("restore-modifiers-%s-%s", restoreJob.Namespace, restoreJob.Name)
 	// Truncate name to fit Kubernetes 253-char limit
 	if len(cmName) > 253 {
-		cmName = cmName[:253]
+		cmName = strings.TrimRight(cmName[:253], "-.")
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/backupcontroller/velerostrategy_controller.go` around lines 667 -
671, The current creation of cmName ("restore-modifiers-%s-%s") truncates by
byte length and can cut multi-byte runes or leave invalid start/end characters;
change logic in the block that builds cmName to (1) work with runes (not bytes)
when truncating to the 253-character DNS-1123 label max, (2) ensure the name
starts and ends with an alphanumeric character by trimming any leading/trailing
non-alphanumerics (remove trailing hyphens) and, if necessary, prefixing a safe
alphanumeric character, and (3) after truncation re-validate against DNS-1123
rules (lowercase letters, digits, hyphen) replacing or removing invalid
characters; apply this to the variable cmName constructed from
restoreJob.Namespace and restoreJob.Name so the final cmName is a valid
Kubernetes resource name.
🧹 Nitpick comments (2)
internal/backupcontroller/velerostrategy_controller.go (2)

959-973: Clarify the "atomic" comment - the operation is safe but not truly atomic.

The comment says "This is atomic" but the PV rebinding involves multiple API calls. The safety comes from pre-binding the new PVC (via VolumeName) before deleting the old one, which prevents other PVCs from claiming the PV. Consider clarifying:

📝 Suggested comment clarification
-	// Point the PV's claimRef directly to the new -orig PVC.
-	// This is atomic — no window where the PV is Available for other PVCs to grab.
+	// Point the PV's claimRef directly to the new -orig PVC.
+	// Safe because the new PVC was pre-bound via VolumeName, preventing other claims.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/backupcontroller/velerostrategy_controller.go` around lines 959 -
973, Update the misleading comment above the PV rebinding sequence (around
r.Get, pv.Spec.ClaimRef assignment, and r.Update) to clarify that the operation
is not truly atomic because it uses multiple API calls; explain that safety is
achieved by pre-binding the new PVC via its VolumeName (newPVC.Spec.VolumeName)
before removing the old PVC, which prevents other PVCs from claiming the PV, and
state any remaining small race windows rather than calling it "atomic".

270-278: Multiple VM pods may exist during live migration.

When a VM is live migrating, there can be multiple pods with the same vm.kubevirt.io/name label. The current code picks the first pod arbitrarily. Consider selecting the pod in Running phase or with the most recent creation timestamp to get consistent network info.

♻️ Optional improvement to prefer the running pod
 	var ip, mac string
 	if len(podList.Items) > 0 {
-		pod := podList.Items[0]
+		// Prefer the running pod if multiple exist (e.g., during live migration)
+		pod := podList.Items[0]
+		for _, p := range podList.Items {
+			if p.Status.Phase == corev1.PodRunning {
+				pod = p
+				break
+			}
+		}
 		ip = pod.Annotations[ovnIPAnnotation]
 		mac = pod.Annotations[ovnMACAnnotation]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/backupcontroller/velerostrategy_controller.go` around lines 270 -
278, The code currently uses podList.Items[0] arbitrarily which can pick the
wrong pod during live migration; update the selection logic to choose the most
appropriate pod (prefer a pod whose Status.Phase == "Running", otherwise pick
the pod with the newest CreationTimestamp) before reading ovnIPAnnotation and
ovnMACAnnotation. Concretely, iterate podList.Items to find a Running pod (fall
back to comparing CreationTimestamp to pick the latest), assign that pod to the
local variable used to extract ip and mac (the same variables currently set from
pod.Annotations using ovnIPAnnotation and ovnMACAnnotation), and keep the
existing logger.Debug call to emit pod.Name and the extracted values (and still
log the "no VM pod found for OVN info" case using vmName if the list is empty).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@internal/backupcontroller/velerostrategy_controller.go`:
- Around line 667-671: The current creation of cmName
("restore-modifiers-%s-%s") truncates by byte length and can cut multi-byte
runes or leave invalid start/end characters; change logic in the block that
builds cmName to (1) work with runes (not bytes) when truncating to the
253-character DNS-1123 label max, (2) ensure the name starts and ends with an
alphanumeric character by trimming any leading/trailing non-alphanumerics
(remove trailing hyphens) and, if necessary, prefixing a safe alphanumeric
character, and (3) after truncation re-validate against DNS-1123 rules
(lowercase letters, digits, hyphen) replacing or removing invalid characters;
apply this to the variable cmName constructed from restoreJob.Namespace and
restoreJob.Name so the final cmName is a valid Kubernetes resource name.

---

Nitpick comments:
In `@internal/backupcontroller/velerostrategy_controller.go`:
- Around line 959-973: Update the misleading comment above the PV rebinding
sequence (around r.Get, pv.Spec.ClaimRef assignment, and r.Update) to clarify
that the operation is not truly atomic because it uses multiple API calls;
explain that safety is achieved by pre-binding the new PVC via its VolumeName
(newPVC.Spec.VolumeName) before removing the old PVC, which prevents other PVCs
from claiming the PV, and state any remaining small race windows rather than
calling it "atomic".
- Around line 270-278: The code currently uses podList.Items[0] arbitrarily
which can pick the wrong pod during live migration; update the selection logic
to choose the most appropriate pod (prefer a pod whose Status.Phase ==
"Running", otherwise pick the pod with the newest CreationTimestamp) before
reading ovnIPAnnotation and ovnMACAnnotation. Concretely, iterate podList.Items
to find a Running pod (fall back to comparing CreationTimestamp to pick the
latest), assign that pod to the local variable used to extract ip and mac (the
same variables currently set from pod.Annotations using ovnIPAnnotation and
ovnMACAnnotation), and keep the existing logger.Debug call to emit pod.Name and
the extracted values (and still log the "no VM pod found for OVN info" case
using vmName if the list is empty).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5bee4c06-c74d-43fe-9240-71cc0099d071

📥 Commits

Reviewing files that changed from the base of the PR and between ee9073e and 7eb8392.

📒 Files selected for processing (1)
  • internal/backupcontroller/velerostrategy_controller.go

@androndo
Andrey Kolkov (androndo) force-pushed the feat/less-manual-actions-on-restore branch 2 times, most recently from 901a8fb to 7e5e566 Compare March 31, 2026 07:26
@androndo
Andrey Kolkov (androndo) force-pushed the feat/less-manual-actions-on-restore branch from 7e5e566 to 15d319b Compare March 31, 2026 08:01
@androndo Andrey Kolkov (androndo) changed the title feat(backups)/ no manual actions for restore VMI feat(backups): no manual actions for restore VMI Mar 31, 2026
@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Mar 31, 2026
@androndo
Andrey Kolkov (androndo) merged commit b4f760c into main Mar 31, 2026
7 checks passed
@androndo
Andrey Kolkov (androndo) deleted the feat/less-manual-actions-on-restore branch March 31, 2026 14:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/feature Categorizes issue or PR as related to a new feature lgtm This PR has been approved by a maintainer size/XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants