diff --git a/pkg/syncer/metadatasyncer.go b/pkg/syncer/metadatasyncer.go index 21f300db67..c77b08cf35 100644 --- a/pkg/syncer/metadatasyncer.go +++ b/pkg/syncer/metadatasyncer.go @@ -141,7 +141,8 @@ const ( ResourceKindSnapshot = "VolumeSnapshot" PVCQuotaExtensionServiceName = "volume.cns.vsphere.vmware.com" SnapQuotaExtensionServiceName = "snapshot.cns.vsphere.vmware.com" - VMServiceExtensionServiceName = "vmware-system-vmop-webhook-service" + VMExtensionServiceName = "vmware-system-vmop-webhook-service" + VMSnapshotExtensionServiceName = "snapshot-vmware-system-vmop-webhook-service" scParamStoragePolicyID = "storagePolicyID" StorageQuotaPeriodicSyncInstanceName = "storage-quota-periodic-sync" FileVolumePrefix = "file:" @@ -1196,7 +1197,8 @@ func calculateVMServiceStoragePolicyUsageReservedForNamespace(ctx context.Contex } storagePolicyToReservedMap := make(map[string]*resource.Quantity) for _, storagePolicyUsage := range supList.Items { - if storagePolicyUsage.Spec.ResourceExtensionName == VMServiceExtensionServiceName { + if storagePolicyUsage.Spec.ResourceExtensionName == VMExtensionServiceName || + storagePolicyUsage.Spec.ResourceExtensionName == VMSnapshotExtensionServiceName { log.Debugf("calculateVMServiceStoragePolicyUsageReservedForNamespace: Processing StoragePolicyUsage"+ " Name: %q, Namespace: %q", storagePolicyUsage.Name, storagePolicyUsage.Namespace) if storagePolicyUsage.DeletionTimestamp != nil { diff --git a/pkg/syncer/metadatasyncer_test.go b/pkg/syncer/metadatasyncer_test.go index 14443975f0..25a6c59506 100644 --- a/pkg/syncer/metadatasyncer_test.go +++ b/pkg/syncer/metadatasyncer_test.go @@ -23,6 +23,10 @@ import ( "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + cnsoperatorv1alpha1 "sigs.k8s.io/vsphere-csi-driver/v3/pkg/apis/cnsoperator" storagepolicyv1alpha2 "sigs.k8s.io/vsphere-csi-driver/v3/pkg/apis/cnsoperator/storagepolicy/v1alpha2" ) @@ -214,3 +218,545 @@ func quantitiesEqual(a, b map[string]*resource.Quantity) bool { } return true } + +// errorClient is a wrapper around a fake client that returns errors on List operations +type errorClient struct { + client.Client + err error +} + +func (e *errorClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + return e.err +} + +func TestCalculateVMServiceStoragePolicyUsageReservedForNamespace(t *testing.T) { + ctx := context.Background() + namespace := "test-namespace" + + tests := []struct { + name string + setupClient func() client.Client + expectedResult map[string]*resource.Quantity + expectError bool + }{ + { + name: "Success with single StoragePolicyUsage for VM extension", + setupClient: func() client.Client { + scheme := runtime.NewScheme() + _ = cnsoperatorv1alpha1.AddToScheme(scheme) + + tenGi := resource.MustParse("10Gi") + spu := &storagepolicyv1alpha2.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spu-1", + Namespace: namespace, + }, + Spec: storagepolicyv1alpha2.StoragePolicyUsageSpec{ + StoragePolicyId: "policy-1", + ResourceExtensionName: VMExtensionServiceName, + }, + Status: storagepolicyv1alpha2.StoragePolicyUsageStatus{ + ResourceTypeLevelQuotaUsage: &storagepolicyv1alpha2.QuotaUsageDetails{ + Reserved: &tenGi, + }, + }, + } + + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(spu). + Build() + }, + expectedResult: func() map[string]*resource.Quantity { + tenGi := resource.MustParse("10Gi") + return map[string]*resource.Quantity{ + "policy-1": resource.NewQuantity(tenGi.Value(), resource.BinarySI), + } + }(), + expectError: false, + }, + { + name: "Success with single StoragePolicyUsage for VM snapshot extension", + setupClient: func() client.Client { + scheme := runtime.NewScheme() + _ = cnsoperatorv1alpha1.AddToScheme(scheme) + + fiveGi := resource.MustParse("5Gi") + spu := &storagepolicyv1alpha2.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spu-2", + Namespace: namespace, + }, + Spec: storagepolicyv1alpha2.StoragePolicyUsageSpec{ + StoragePolicyId: "policy-2", + ResourceExtensionName: VMSnapshotExtensionServiceName, + }, + Status: storagepolicyv1alpha2.StoragePolicyUsageStatus{ + ResourceTypeLevelQuotaUsage: &storagepolicyv1alpha2.QuotaUsageDetails{ + Reserved: &fiveGi, + }, + }, + } + + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(spu). + Build() + }, + expectedResult: func() map[string]*resource.Quantity { + fiveGi := resource.MustParse("5Gi") + return map[string]*resource.Quantity{ + "policy-2": resource.NewQuantity(fiveGi.Value(), resource.BinarySI), + } + }(), + expectError: false, + }, + { + name: "Success with multiple StoragePolicyUsage objects for same policy", + setupClient: func() client.Client { + scheme := runtime.NewScheme() + _ = cnsoperatorv1alpha1.AddToScheme(scheme) + + tenGi := resource.MustParse("10Gi") + fiveGi := resource.MustParse("5Gi") + spu1 := &storagepolicyv1alpha2.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spu-1", + Namespace: namespace, + }, + Spec: storagepolicyv1alpha2.StoragePolicyUsageSpec{ + StoragePolicyId: "policy-1", + ResourceExtensionName: VMExtensionServiceName, + }, + Status: storagepolicyv1alpha2.StoragePolicyUsageStatus{ + ResourceTypeLevelQuotaUsage: &storagepolicyv1alpha2.QuotaUsageDetails{ + Reserved: &tenGi, + }, + }, + } + spu2 := &storagepolicyv1alpha2.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spu-2", + Namespace: namespace, + }, + Spec: storagepolicyv1alpha2.StoragePolicyUsageSpec{ + StoragePolicyId: "policy-1", + ResourceExtensionName: VMSnapshotExtensionServiceName, + }, + Status: storagepolicyv1alpha2.StoragePolicyUsageStatus{ + ResourceTypeLevelQuotaUsage: &storagepolicyv1alpha2.QuotaUsageDetails{ + Reserved: &fiveGi, + }, + }, + } + + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(spu1, spu2). + Build() + }, + expectedResult: func() map[string]*resource.Quantity { + fifteenGi := resource.MustParse("15Gi") + return map[string]*resource.Quantity{ + "policy-1": resource.NewQuantity(fifteenGi.Value(), resource.BinarySI), + } + }(), + expectError: false, + }, + { + name: "Success with multiple StoragePolicyUsage objects for different policies", + setupClient: func() client.Client { + scheme := runtime.NewScheme() + _ = cnsoperatorv1alpha1.AddToScheme(scheme) + + tenGi := resource.MustParse("10Gi") + fiveGi := resource.MustParse("5Gi") + spu1 := &storagepolicyv1alpha2.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spu-1", + Namespace: namespace, + }, + Spec: storagepolicyv1alpha2.StoragePolicyUsageSpec{ + StoragePolicyId: "policy-1", + ResourceExtensionName: VMExtensionServiceName, + }, + Status: storagepolicyv1alpha2.StoragePolicyUsageStatus{ + ResourceTypeLevelQuotaUsage: &storagepolicyv1alpha2.QuotaUsageDetails{ + Reserved: &tenGi, + }, + }, + } + spu2 := &storagepolicyv1alpha2.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spu-2", + Namespace: namespace, + }, + Spec: storagepolicyv1alpha2.StoragePolicyUsageSpec{ + StoragePolicyId: "policy-2", + ResourceExtensionName: VMSnapshotExtensionServiceName, + }, + Status: storagepolicyv1alpha2.StoragePolicyUsageStatus{ + ResourceTypeLevelQuotaUsage: &storagepolicyv1alpha2.QuotaUsageDetails{ + Reserved: &fiveGi, + }, + }, + } + + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(spu1, spu2). + Build() + }, + expectedResult: func() map[string]*resource.Quantity { + tenGi := resource.MustParse("10Gi") + fiveGi := resource.MustParse("5Gi") + return map[string]*resource.Quantity{ + "policy-1": resource.NewQuantity(tenGi.Value(), resource.BinarySI), + "policy-2": resource.NewQuantity(fiveGi.Value(), resource.BinarySI), + } + }(), + expectError: false, + }, + { + name: "Success with empty list", + setupClient: func() client.Client { + scheme := runtime.NewScheme() + _ = cnsoperatorv1alpha1.AddToScheme(scheme) + + return fake.NewClientBuilder(). + WithScheme(scheme). + Build() + }, + expectedResult: map[string]*resource.Quantity{}, + expectError: false, + }, + { + name: "Success with StoragePolicyUsage filtered out - wrong ResourceExtensionName", + setupClient: func() client.Client { + scheme := runtime.NewScheme() + _ = cnsoperatorv1alpha1.AddToScheme(scheme) + + tenGi := resource.MustParse("10Gi") + spu := &storagepolicyv1alpha2.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spu-1", + Namespace: namespace, + }, + Spec: storagepolicyv1alpha2.StoragePolicyUsageSpec{ + StoragePolicyId: "policy-1", + ResourceExtensionName: "other-extension", + }, + Status: storagepolicyv1alpha2.StoragePolicyUsageStatus{ + ResourceTypeLevelQuotaUsage: &storagepolicyv1alpha2.QuotaUsageDetails{ + Reserved: &tenGi, + }, + }, + } + + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(spu). + Build() + }, + expectedResult: map[string]*resource.Quantity{}, + expectError: false, + }, + { + name: "Success with StoragePolicyUsage filtered out - marked for deletion", + setupClient: func() client.Client { + scheme := runtime.NewScheme() + _ = cnsoperatorv1alpha1.AddToScheme(scheme) + + now := metav1.Now() + tenGi := resource.MustParse("10Gi") + spu := &storagepolicyv1alpha2.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spu-1", + Namespace: namespace, + DeletionTimestamp: &now, + Finalizers: []string{"test-finalizer"}, + }, + Spec: storagepolicyv1alpha2.StoragePolicyUsageSpec{ + StoragePolicyId: "policy-1", + ResourceExtensionName: VMExtensionServiceName, + }, + Status: storagepolicyv1alpha2.StoragePolicyUsageStatus{ + ResourceTypeLevelQuotaUsage: &storagepolicyv1alpha2.QuotaUsageDetails{ + Reserved: &tenGi, + }, + }, + } + + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(spu). + Build() + }, + expectedResult: map[string]*resource.Quantity{}, + expectError: false, + }, + { + name: "Success with StoragePolicyUsage filtered out - missing ResourceTypeLevelQuotaUsage", + setupClient: func() client.Client { + scheme := runtime.NewScheme() + _ = cnsoperatorv1alpha1.AddToScheme(scheme) + + spu := &storagepolicyv1alpha2.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spu-1", + Namespace: namespace, + }, + Spec: storagepolicyv1alpha2.StoragePolicyUsageSpec{ + StoragePolicyId: "policy-1", + ResourceExtensionName: VMExtensionServiceName, + }, + Status: storagepolicyv1alpha2.StoragePolicyUsageStatus{ + ResourceTypeLevelQuotaUsage: nil, + }, + } + + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(spu). + Build() + }, + expectedResult: map[string]*resource.Quantity{}, + expectError: false, + }, + { + name: "Success with mixed valid and invalid StoragePolicyUsage objects", + setupClient: func() client.Client { + scheme := runtime.NewScheme() + _ = cnsoperatorv1alpha1.AddToScheme(scheme) + + now := metav1.Now() + tenGi := resource.MustParse("10Gi") + fiveGi := resource.MustParse("5Gi") + + // Valid SPU + spu1 := &storagepolicyv1alpha2.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spu-1", + Namespace: namespace, + }, + Spec: storagepolicyv1alpha2.StoragePolicyUsageSpec{ + StoragePolicyId: "policy-1", + ResourceExtensionName: VMExtensionServiceName, + }, + Status: storagepolicyv1alpha2.StoragePolicyUsageStatus{ + ResourceTypeLevelQuotaUsage: &storagepolicyv1alpha2.QuotaUsageDetails{ + Reserved: &tenGi, + }, + }, + } + + // Invalid SPU - wrong extension name + spu2 := &storagepolicyv1alpha2.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spu-2", + Namespace: namespace, + }, + Spec: storagepolicyv1alpha2.StoragePolicyUsageSpec{ + StoragePolicyId: "policy-2", + ResourceExtensionName: "other-extension", + }, + Status: storagepolicyv1alpha2.StoragePolicyUsageStatus{ + ResourceTypeLevelQuotaUsage: &storagepolicyv1alpha2.QuotaUsageDetails{ + Reserved: &fiveGi, + }, + }, + } + + // Invalid SPU - marked for deletion + spu3 := &storagepolicyv1alpha2.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spu-3", + Namespace: namespace, + DeletionTimestamp: &now, + Finalizers: []string{"test-finalizer"}, + }, + Spec: storagepolicyv1alpha2.StoragePolicyUsageSpec{ + StoragePolicyId: "policy-3", + ResourceExtensionName: VMSnapshotExtensionServiceName, + }, + Status: storagepolicyv1alpha2.StoragePolicyUsageStatus{ + ResourceTypeLevelQuotaUsage: &storagepolicyv1alpha2.QuotaUsageDetails{ + Reserved: &fiveGi, + }, + }, + } + + // Invalid SPU - missing status + spu4 := &storagepolicyv1alpha2.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spu-4", + Namespace: namespace, + }, + Spec: storagepolicyv1alpha2.StoragePolicyUsageSpec{ + StoragePolicyId: "policy-4", + ResourceExtensionName: VMExtensionServiceName, + }, + Status: storagepolicyv1alpha2.StoragePolicyUsageStatus{ + ResourceTypeLevelQuotaUsage: nil, + }, + } + + // Valid SPU + spu5 := &storagepolicyv1alpha2.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spu-5", + Namespace: namespace, + }, + Spec: storagepolicyv1alpha2.StoragePolicyUsageSpec{ + StoragePolicyId: "policy-1", + ResourceExtensionName: VMSnapshotExtensionServiceName, + }, + Status: storagepolicyv1alpha2.StoragePolicyUsageStatus{ + ResourceTypeLevelQuotaUsage: &storagepolicyv1alpha2.QuotaUsageDetails{ + Reserved: &fiveGi, + }, + }, + } + + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(spu1, spu2, spu3, spu4, spu5). + Build() + }, + expectedResult: func() map[string]*resource.Quantity { + fifteenGi := resource.MustParse("15Gi") + return map[string]*resource.Quantity{ + "policy-1": resource.NewQuantity(fifteenGi.Value(), resource.BinarySI), + } + }(), + expectError: false, + }, + { + name: "Error when List fails", + setupClient: func() client.Client { + scheme := runtime.NewScheme() + _ = cnsoperatorv1alpha1.AddToScheme(scheme) + + baseClient := fake.NewClientBuilder(). + WithScheme(scheme). + Build() + + return &errorClient{ + Client: baseClient, + err: errors.New("list error"), + } + }, + expectedResult: nil, + expectError: true, + }, + { + name: "Success with zero reserved quantity", + setupClient: func() client.Client { + scheme := runtime.NewScheme() + _ = cnsoperatorv1alpha1.AddToScheme(scheme) + + zero := resource.MustParse("0Gi") + spu := &storagepolicyv1alpha2.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spu-1", + Namespace: namespace, + }, + Spec: storagepolicyv1alpha2.StoragePolicyUsageSpec{ + StoragePolicyId: "policy-1", + ResourceExtensionName: VMExtensionServiceName, + }, + Status: storagepolicyv1alpha2.StoragePolicyUsageStatus{ + ResourceTypeLevelQuotaUsage: &storagepolicyv1alpha2.QuotaUsageDetails{ + Reserved: &zero, + }, + }, + } + + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(spu). + Build() + }, + expectedResult: map[string]*resource.Quantity{ + "policy-1": resource.NewQuantity(0, resource.BinarySI), + }, + expectError: false, + }, + { + name: "Success with StoragePolicyUsage in different namespace", + setupClient: func() client.Client { + scheme := runtime.NewScheme() + _ = cnsoperatorv1alpha1.AddToScheme(scheme) + + tenGi := resource.MustParse("10Gi") + // SPU in different namespace + spu1 := &storagepolicyv1alpha2.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spu-1", + Namespace: "other-namespace", + }, + Spec: storagepolicyv1alpha2.StoragePolicyUsageSpec{ + StoragePolicyId: "policy-1", + ResourceExtensionName: VMExtensionServiceName, + }, + Status: storagepolicyv1alpha2.StoragePolicyUsageStatus{ + ResourceTypeLevelQuotaUsage: &storagepolicyv1alpha2.QuotaUsageDetails{ + Reserved: &tenGi, + }, + }, + } + + // SPU in target namespace + fiveGi := resource.MustParse("5Gi") + spu2 := &storagepolicyv1alpha2.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spu-2", + Namespace: namespace, + }, + Spec: storagepolicyv1alpha2.StoragePolicyUsageSpec{ + StoragePolicyId: "policy-2", + ResourceExtensionName: VMExtensionServiceName, + }, + Status: storagepolicyv1alpha2.StoragePolicyUsageStatus{ + ResourceTypeLevelQuotaUsage: &storagepolicyv1alpha2.QuotaUsageDetails{ + Reserved: &fiveGi, + }, + }, + } + + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(spu1, spu2). + Build() + }, + expectedResult: func() map[string]*resource.Quantity { + fiveGi := resource.MustParse("5Gi") + return map[string]*resource.Quantity{ + "policy-2": resource.NewQuantity(fiveGi.Value(), resource.BinarySI), + } + }(), + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeClient := tt.setupClient() + result, err := calculateVMServiceStoragePolicyUsageReservedForNamespace(ctx, fakeClient, namespace) + + if tt.expectError { + if err == nil { + t.Errorf("calculateVMServiceStoragePolicyUsageReservedForNamespace() expected error but got nil") + } + } else { + if err != nil { + t.Errorf("calculateVMServiceStoragePolicyUsageReservedForNamespace() returned error: %v", err) + } + if !quantitiesEqual(result, tt.expectedResult) { + t.Errorf("calculateVMServiceStoragePolicyUsageReservedForNamespace() result = %v; want %v", + result, tt.expectedResult) + } + } + }) + } +}