-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Fix backup-finalizer: do not set backup phase to Completed before PutBackupMetadata succeeds #9646
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Fix backup-finalizer: do not set backup phase to Completed before PutBackupMetadata succeeds |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -18,6 +18,7 @@ | |||||
|
|
||||||
| import ( | ||||||
| "bytes" | ||||||
| "fmt" | ||||||
| "io" | ||||||
| "testing" | ||||||
| "time" | ||||||
|
|
@@ -39,8 +40,10 @@ | |||||
| "github.com/vmware-tanzu/velero/pkg/itemoperation" | ||||||
| "github.com/vmware-tanzu/velero/pkg/kuberesource" | ||||||
| "github.com/vmware-tanzu/velero/pkg/metrics" | ||||||
| persistencemocks "github.com/vmware-tanzu/velero/pkg/persistence/mocks" | ||||||
| "github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt" | ||||||
| "github.com/vmware-tanzu/velero/pkg/plugin/framework" | ||||||
| pluginmocks "github.com/vmware-tanzu/velero/pkg/plugin/mocks" | ||||||
| "github.com/vmware-tanzu/velero/pkg/plugin/velero" | ||||||
| velerotest "github.com/vmware-tanzu/velero/pkg/test" | ||||||
| ) | ||||||
|
|
@@ -242,3 +245,151 @@ | |||||
| }) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| func TestBackupFinalizerReconcile_PutBackupMetadataFail(t *testing.T) { | ||||||
| tests := []struct { | ||||||
| name string | ||||||
| initialPhase velerov1api.BackupPhase | ||||||
| }{ | ||||||
| { | ||||||
| name: "Finalizing backup stays Finalizing when PutBackupMetadata fails", | ||||||
| initialPhase: velerov1api.BackupPhaseFinalizing, | ||||||
| }, | ||||||
| { | ||||||
| name: "FinalizingPartiallyFailed backup stays FinalizingPartiallyFailed when PutBackupMetadata fails", | ||||||
| initialPhase: velerov1api.BackupPhaseFinalizingPartiallyFailed, | ||||||
| }, | ||||||
| } | ||||||
|
|
||||||
| for _, test := range tests { | ||||||
| t.Run(test.name, func(t *testing.T) { | ||||||
| fakeClock := testclocks.NewFakeClock(time.Now()) | ||||||
| defaultBackupLocation := builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "default").Result() | ||||||
|
|
||||||
| backup := builder.ForBackup(velerov1api.DefaultNamespace, "backup-meta-fail"). | ||||||
| StorageLocation("default"). | ||||||
| ObjectMeta(builder.WithUID("foo")). | ||||||
| StartTimestamp(fakeClock.Now()). | ||||||
| Phase(test.initialPhase).Result() | ||||||
|
|
||||||
| fakeClient := velerotest.NewFakeControllerRuntimeClient(t, backup, defaultBackupLocation) | ||||||
| fakeGlobalClient := velerotest.NewFakeControllerRuntimeClient(t, backup, defaultBackupLocation) | ||||||
|
|
||||||
| // Use local mocks to avoid interference with other tests | ||||||
| localPluginManager := &pluginmocks.Manager{} | ||||||
| localBackupStore := &persistencemocks.BackupStore{} | ||||||
|
|
||||||
| backupper := new(fakeBackupper) | ||||||
| reconciler := NewBackupFinalizerReconciler( | ||||||
| fakeClient, | ||||||
| fakeGlobalClient, | ||||||
| fakeClock, | ||||||
| backupper, | ||||||
| func(logrus.FieldLogger) clientmgmt.Manager { return localPluginManager }, | ||||||
| NewBackupTracker(), | ||||||
| NewFakeSingleObjectBackupStoreGetter(localBackupStore), | ||||||
| logrus.StandardLogger(), | ||||||
| metrics.NewServerMetrics(), | ||||||
| 10*time.Minute, | ||||||
| ) | ||||||
|
|
||||||
| localPluginManager.On("CleanupClients").Return(nil) | ||||||
| localBackupStore.On("GetBackupItemOperations", backup.Name).Return(nil, nil) | ||||||
| // PutBackupMetadata fails | ||||||
| localBackupStore.On("PutBackupMetadata", mock.Anything, mock.Anything).Return(fmt.Errorf("object lock prevented upload")) | ||||||
| localBackupStore.On("GetBackupVolumeInfos", mock.Anything).Return(nil, nil) | ||||||
| localBackupStore.On("PutBackupVolumeInfos", mock.Anything, mock.Anything).Return(nil) | ||||||
|
|
||||||
| _, err := reconciler.Reconcile(t.Context(), ctrl.Request{NamespacedName: types.NamespacedName{Namespace: backup.Namespace, Name: backup.Name}}) | ||||||
| assert.Error(t, err, "reconcile should return error when PutBackupMetadata fails") | ||||||
|
|
||||||
| backupAfter := velerov1api.Backup{} | ||||||
| err = fakeClient.Get(t.Context(), types.NamespacedName{ | ||||||
| Namespace: backup.Namespace, | ||||||
| Name: backup.Name, | ||||||
| }, &backupAfter) | ||||||
| require.NoError(t, err) | ||||||
| assert.Equal(t, test.initialPhase, backupAfter.Status.Phase, | ||||||
| "backup phase should remain %s when PutBackupMetadata fails", test.initialPhase) | ||||||
| assert.Nil(t, backupAfter.Status.CompletionTimestamp, | ||||||
| "CompletionTimestamp should not be set when PutBackupMetadata fails") | ||||||
| }) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| func TestBackupFinalizerReconcile_PutBackupContentsFail(t *testing.T) { | ||||||
| fakeClock := testclocks.NewFakeClock(time.Now()) | ||||||
| metav1Now := metav1.NewTime(fakeClock.Now()) | ||||||
| defaultBackupLocation := builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "default").Result() | ||||||
|
|
||||||
| backup := builder.ForBackup(velerov1api.DefaultNamespace, "backup-contents-fail"). | ||||||
| StorageLocation("default"). | ||||||
| ObjectMeta(builder.WithUID("foo")). | ||||||
| StartTimestamp(fakeClock.Now()). | ||||||
| Phase(velerov1api.BackupPhaseFinalizing).Result() | ||||||
|
Comment on lines
+320
to
+329
|
||||||
|
|
||||||
| fakeClient := velerotest.NewFakeControllerRuntimeClient(t, backup, defaultBackupLocation) | ||||||
| fakeGlobalClient := velerotest.NewFakeControllerRuntimeClient(t, backup, defaultBackupLocation) | ||||||
|
|
||||||
| localPluginManager := &pluginmocks.Manager{} | ||||||
| localBackupStore := &persistencemocks.BackupStore{} | ||||||
|
|
||||||
| backupper := new(fakeBackupper) | ||||||
| reconciler := NewBackupFinalizerReconciler( | ||||||
| fakeClient, | ||||||
| fakeGlobalClient, | ||||||
| fakeClock, | ||||||
| backupper, | ||||||
| func(logrus.FieldLogger) clientmgmt.Manager { return localPluginManager }, | ||||||
| NewBackupTracker(), | ||||||
| NewFakeSingleObjectBackupStoreGetter(localBackupStore), | ||||||
| logrus.StandardLogger(), | ||||||
| metrics.NewServerMetrics(), | ||||||
| 10*time.Minute, | ||||||
| ) | ||||||
|
|
||||||
| operations := []*itemoperation.BackupOperation{ | ||||||
| { | ||||||
| Spec: itemoperation.BackupOperationSpec{ | ||||||
| BackupName: "backup-contents-fail", | ||||||
| BackupUID: "foo", | ||||||
| BackupItemAction: "foo", | ||||||
| ResourceIdentifier: velero.ResourceIdentifier{ | ||||||
| GroupResource: kuberesource.Pods, | ||||||
| Namespace: "ns-1", | ||||||
| Name: "pod-1", | ||||||
| }, | ||||||
| OperationID: "operation-1", | ||||||
| }, | ||||||
| Status: itemoperation.OperationStatus{ | ||||||
| Phase: itemoperation.OperationPhaseCompleted, | ||||||
| Created: &metav1Now, | ||||||
| }, | ||||||
| }, | ||||||
| } | ||||||
|
|
||||||
| localPluginManager.On("CleanupClients").Return(nil) | ||||||
| localPluginManager.On("GetBackupItemActionsV2").Return(nil, nil) | ||||||
| localBackupStore.On("GetBackupItemOperations", backup.Name).Return(operations, nil) | ||||||
| localBackupStore.On("GetBackupContents", mock.Anything).Return(io.NopCloser(bytes.NewReader([]byte("hello world"))), nil) | ||||||
| localBackupStore.On("PutBackupMetadata", mock.Anything, mock.Anything).Return(nil) | ||||||
| // PutBackupContents fails | ||||||
| localBackupStore.On("PutBackupContents", mock.Anything, mock.Anything).Return(fmt.Errorf("object lock prevented upload")) | ||||||
| localBackupStore.On("GetBackupVolumeInfos", mock.Anything).Return(nil, nil) | ||||||
| localBackupStore.On("PutBackupVolumeInfos", mock.Anything, mock.Anything).Return(nil) | ||||||
| backupper.On("FinalizeBackup", mock.Anything, mock.Anything, mock.Anything, mock.Anything, framework.BackupItemActionResolverV2{}, mock.Anything, mock.Anything).Return(nil) | ||||||
|
||||||
| backupper.On("FinalizeBackup", mock.Anything, mock.Anything, mock.Anything, mock.Anything, framework.BackupItemActionResolverV2{}, mock.Anything, mock.Anything).Return(nil) | |
| backupper.On("FinalizeBackup", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
finalPhasehas no default assignment and is used unconditionally. Ifbackup.Status.Phaseis not one of the two handled values in this switch on this code path,finalPhasewill be the zero value and could be uploaded/patched as an empty phase. A concrete fix is to add an explicit default/guard (e.g., return early) or initializefinalPhasefrombackup.Status.Phaseand only override it for the handled Finalizing* phases.