diff --git a/changelogs/unreleased/9987-Shashank1306s b/changelogs/unreleased/9987-Shashank1306s new file mode 100644 index 000000000..4a975b5da --- /dev/null +++ b/changelogs/unreleased/9987-Shashank1306s @@ -0,0 +1 @@ +Fix ResourceDeletionStatusTracker key mismatch so restore into a terminating namespace waits once per namespace instead of once per resource diff --git a/pkg/util/kube/utils.go b/pkg/util/kube/utils.go index d76dad4a3..c3d0b2046 100644 --- a/pkg/util/kube/utils.go +++ b/pkg/util/kube/utils.go @@ -103,7 +103,10 @@ func EnsureNamespaceExistsAndIsReady(namespace *corev1api.Namespace, client core return true, err } if clusterNS != nil && (clusterNS.GetDeletionTimestamp() != nil || clusterNS.Status.Phase == corev1api.NamespaceTerminating) { - if resourceDeletionStatusTracker.Contains(clusterNS.Kind, clusterNS.Name, clusterNS.Name) { + // Use namespace.Kind (not clusterNS.Kind) so this key matches the one Add() + // writes below: client.Get() strips TypeMeta (Kind=""), but getNamespace() + // sets Kind="Namespace". Mismatched keys made Contains never match. + if resourceDeletionStatusTracker.Contains(namespace.Kind, namespace.Name, namespace.Name) { namespaceAlreadyInDeletionTracker = true return true, errors.Errorf("namespace %s is already present in the polling set, skipping execution", namespace.Name) } diff --git a/pkg/util/kube/utils_test.go b/pkg/util/kube/utils_test.go index 23db12a41..cc53b31b5 100644 --- a/pkg/util/kube/utils_test.go +++ b/pkg/util/kube/utils_test.go @@ -154,6 +154,39 @@ func TestEnsureNamespaceExistsAndIsReady(t *testing.T) { } } +// TestEnsureNamespaceExistsAndIsReadyTerminatingTrackerKindMismatch verifies the +// tracker skip-path fires when Add and Contains see different Kind values, as they +// do in production: getNamespace() sets Kind="Namespace" but client.Get() strips it. +func TestEnsureNamespaceExistsAndIsReadyTerminatingTrackerKindMismatch(t *testing.T) { + // Passed-in namespace mirrors getNamespace(): Kind is set. + namespace := &corev1api.Namespace{ + TypeMeta: metav1.TypeMeta{Kind: "Namespace", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + } + + // clusterNS mirrors client.Get(): Kind stripped, phase Terminating. + clusterNS := &corev1api.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Status: corev1api.NamespaceStatus{Phase: corev1api.NamespaceTerminating}, + } + + nsClient := &velerotest.FakeNamespaceClient{} + defer nsClient.AssertExpectations(t) + nsClient.On("Get", "test", metav1.GetOptions{}).Return(clusterNS, nil) + + // Seed the tracker as production Add() does. + tracker := NewResourceDeletionStatusTracker() + tracker.Add(namespace.Kind, namespace.Name, namespace.Name) + + result, nsCreated, err := EnsureNamespaceExistsAndIsReady(namespace, nsClient, time.Millisecond, tracker) + + assert.False(t, result) + assert.False(t, nsCreated) + // Skip-path must fire, not the full terminating-resource-timeout wait. + require.ErrorContains(t, err, "skipping polling for terminating namespace") + assert.NotContains(t, err.Error(), "timed out waiting for terminating namespace") +} + // TestGetVolumeDirectorySuccess tests that the GetVolumeDirectory function // returns a volume's name or a volume's name plus '/mount' when a PVC is present. func TestGetVolumeDirectorySuccess(t *testing.T) {