diff --git a/pkg/controllers/subnetset/namespace_handler.go b/pkg/controllers/subnetset/namespace_handler.go index d7c4e038c..408a35d12 100644 --- a/pkg/controllers/subnetset/namespace_handler.go +++ b/pkg/controllers/subnetset/namespace_handler.go @@ -41,7 +41,7 @@ func (e *EnqueueRequestForNamespace) Update(_ context.Context, updateEvent event obj := updateEvent.ObjectNew.(*v1.Namespace) err := requeueSubnetSet(e.Client, obj.Name, l) if err != nil { - log.Error(err, "failed to reconcile subnet") + log.Error(err, "Failed to requeue subnet") } } @@ -52,9 +52,9 @@ var PredicateFuncsNs = predicate.Funcs{ UpdateFunc: func(e event.UpdateEvent) bool { oldObj := e.ObjectOld.(*v1.Namespace) newObj := e.ObjectNew.(*v1.Namespace) - log.V(1).Info("Receive Namespace update event", "name", oldObj.Name) + log.V(1).Info("Receive Namespace update event", "Name", oldObj.Name) if reflect.DeepEqual(oldObj.ObjectMeta.Labels, newObj.ObjectMeta.Labels) { - log.Info("label of Namespace is not changed, ignore it", "name", oldObj.Name) + log.Info("Label of Namespace is not changed, ignore it", "name", oldObj.Name) return false } return true @@ -64,14 +64,21 @@ var PredicateFuncsNs = predicate.Funcs{ }, } -func requeueSubnetSet(c client.Client, namespace string, q workqueue.RateLimitingInterface) error { +func listSubnetSet(c client.Client, ctx context.Context, options ...client.ListOption) (*v1alpha1.SubnetSetList, error) { subnetSetList := &v1alpha1.SubnetSetList{} - err := c.List(context.Background(), subnetSetList, client.InNamespace(namespace)) + err := c.List(ctx, subnetSetList, options...) if err != nil { - log.Error(err, "Failed to list all the Subnets") - return err + return nil, err } + return subnetSetList, nil +} +func requeueSubnetSet(c client.Client, namespace string, q workqueue.RateLimitingInterface) error { + subnetSetList, err := listSubnetSet(c, context.Background(), client.InNamespace(namespace)) + if err != nil { + log.Error(err, "Failed to list all the SubnetSets") + return err + } for _, subnetSet := range subnetSetList.Items { log.Info("Requeue SubnetSet because Namespace updated", "Namespace", subnetSet.Namespace, "Name", subnetSet.Name) q.Add(reconcile.Request{ diff --git a/pkg/controllers/subnetset/namespace_handler_test.go b/pkg/controllers/subnetset/namespace_handler_test.go new file mode 100644 index 000000000..ddf455d71 --- /dev/null +++ b/pkg/controllers/subnetset/namespace_handler_test.go @@ -0,0 +1,161 @@ +package subnetset + +import ( + "context" + "testing" + + "github.com/agiledragon/gomonkey/v2" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/util/workqueue" + ctlclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/event" + + "github.com/vmware-tanzu/nsx-operator/pkg/apis/vpc/v1alpha1" +) + +func TestEnqueueRequestForNamespace_Create(t *testing.T) { + client := fake.NewClientBuilder().Build() + e := &EnqueueRequestForNamespace{Client: client} + queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) + + e.Create(context.TODO(), event.CreateEvent{}, queue) + // No asserts here because Create does nothing, just ensuring no errors. +} + +func TestEnqueueRequestForNamespace_Delete(t *testing.T) { + client := fake.NewClientBuilder().Build() + e := &EnqueueRequestForNamespace{Client: client} + queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) + + e.Delete(context.TODO(), event.DeleteEvent{}, queue) + // No asserts here because Delete does nothing, just ensuring no errors. +} + +func TestEnqueueRequestForNamespace_Generic(t *testing.T) { + client := fake.NewClientBuilder().Build() + e := &EnqueueRequestForNamespace{Client: client} + queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) + + e.Generic(context.TODO(), event.GenericEvent{}, queue) + // No asserts here because Generic does nothing, just ensuring no errors. +} + +func TestEnqueueRequestForNamespace_Update(t *testing.T) { + // Prepare test data + oldNamespace := &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-namespace", + Labels: map[string]string{"env": "test"}, + }, + } + + newNamespace := &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-namespace", + Labels: map[string]string{"env": "prod"}, + }, + } + + client := fake.NewClientBuilder().WithObjects(newNamespace).Build() + e := &EnqueueRequestForNamespace{Client: client} + queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) + + updateEvent := event.UpdateEvent{ + ObjectOld: oldNamespace, + ObjectNew: newNamespace, + } + + subnetSet := v1alpha1.SubnetSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-subnetset", + Namespace: "test-namespace", + }, + } + subnetSetList := &v1alpha1.SubnetSetList{ + TypeMeta: metav1.TypeMeta{}, + ListMeta: metav1.ListMeta{}, + Items: []v1alpha1.SubnetSet{subnetSet}, + } + + e.Update(context.TODO(), updateEvent, queue) + assert.Equal(t, 0, queue.Len(), "Expected 1 item to be requeued") + + patches := gomonkey.ApplyFunc(listSubnetSet, func(c ctlclient.Client, ctx context.Context, options ...ctlclient.ListOption) (*v1alpha1.SubnetSetList, error) { + return subnetSetList, nil + }) + defer patches.Reset() + + e.Update(context.TODO(), updateEvent, queue) + assert.Equal(t, 1, queue.Len(), "Expected 1 item to be requeued") +} + +func TestPredicateFuncsNs_UpdateFunc(t *testing.T) { + oldNamespace := &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-namespace", + Labels: map[string]string{"env": "test"}, + }, + } + + newNamespace := &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-namespace", + Labels: map[string]string{"env": "prod"}, + }, + } + + updateEvent := event.UpdateEvent{ + ObjectOld: oldNamespace, + ObjectNew: newNamespace, + } + + // Test the update function logic in PredicateFuncsNs + result := PredicateFuncsNs.UpdateFunc(updateEvent) + assert.True(t, result, "Expected update event to trigger requeue") + + // Test with no label change + noChangeEvent := event.UpdateEvent{ + ObjectOld: oldNamespace, + ObjectNew: oldNamespace, + } + + result = PredicateFuncsNs.UpdateFunc(noChangeEvent) + assert.False(t, result, "Expected no action when labels have not changed") + + res := PredicateFuncsNs.CreateFunc(event.CreateEvent{Object: newNamespace}) + assert.False(t, res, "Expected no action when labels have not changed") + + res = PredicateFuncsNs.DeleteFunc(event.DeleteEvent{Object: newNamespace}) + assert.False(t, res, "Expected no action when labels have not changed") +} + +func TestRequeueSubnetSet(t *testing.T) { + // Prepare test data + subnetSet := &v1alpha1.SubnetSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-subnetset", + Namespace: "test-namespace", + }, + } + + scheme := clientgoscheme.Scheme + v1alpha1.AddToScheme(scheme) + + // Test for empty namespace (no SubnetSets found) + emptyClient := fake.NewClientBuilder().Build() + queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) + err := requeueSubnetSet(emptyClient, "empty-namespace", queue) + assert.NoError(t, err, "Expected no error with empty namespace") + assert.Equal(t, 0, queue.Len(), "Expected no items to be requeued for empty namespace") + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(subnetSet).Build() + queue = workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) + + err = requeueSubnetSet(client, "test-namespace", queue) + assert.NoError(t, err, "Expected no error while requeueing SubnetSets") + assert.Equal(t, 1, queue.Len(), "Expected 1 item to be requeued") +} diff --git a/pkg/controllers/subnetset/subnetset_controller.go b/pkg/controllers/subnetset/subnetset_controller.go index c9502cc78..c88a0d6e9 100644 --- a/pkg/controllers/subnetset/subnetset_controller.go +++ b/pkg/controllers/subnetset/subnetset_controller.go @@ -261,8 +261,7 @@ func (r *SubnetSetReconciler) CollectGarbage(ctx context.Context) { log.Info("SubnetSet garbage collection completed", "duration(ms)", time.Since(startTime).Milliseconds()) }() - crdSubnetSetList := &v1alpha1.SubnetSetList{} - err := r.Client.List(ctx, crdSubnetSetList) + crdSubnetSetList, err := listSubnetSet(r.Client, ctx) if err != nil { log.Error(err, "Failed to list SubnetSet CRs") return diff --git a/pkg/controllers/subnetset/subnetset_controller_test.go b/pkg/controllers/subnetset/subnetset_controller_test.go new file mode 100644 index 000000000..ebe7ea88c --- /dev/null +++ b/pkg/controllers/subnetset/subnetset_controller_test.go @@ -0,0 +1,746 @@ +package subnetset + +import ( + "context" + "errors" + "fmt" + "net/http" + "reflect" + "testing" + "time" + + "github.com/agiledragon/gomonkey/v2" + "github.com/stretchr/testify/assert" + "github.com/vmware/vsphere-automation-sdk-go/services/nsxt/model" + v12 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/sets" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + "github.com/vmware-tanzu/nsx-operator/pkg/apis/vpc/v1alpha1" + "github.com/vmware-tanzu/nsx-operator/pkg/config" + ctlcommon "github.com/vmware-tanzu/nsx-operator/pkg/controllers/common" + "github.com/vmware-tanzu/nsx-operator/pkg/nsx" + "github.com/vmware-tanzu/nsx-operator/pkg/nsx/services/common" + "github.com/vmware-tanzu/nsx-operator/pkg/nsx/services/subnet" + "github.com/vmware-tanzu/nsx-operator/pkg/nsx/services/subnetport" + "github.com/vmware-tanzu/nsx-operator/pkg/nsx/services/vpc" +) + +type fakeRecorder struct{} + +func (recorder fakeRecorder) Event(object runtime.Object, eventtype, reason, message string) { +} + +func (recorder fakeRecorder) Eventf(object runtime.Object, eventtype, reason, messageFmt string, args ...interface{}) { +} + +func (recorder fakeRecorder) AnnotatedEventf(object runtime.Object, annotations map[string]string, eventtype, reason, messageFmt string, args ...interface{}) { +} + +func createFakeSubnetSetReconciler(objs []client.Object) *SubnetSetReconciler { + newScheme := runtime.NewScheme() + utilruntime.Must(clientgoscheme.AddToScheme(newScheme)) + utilruntime.Must(v1alpha1.AddToScheme(newScheme)) + fakeClient := fake.NewClientBuilder().WithScheme(newScheme).WithObjects(objs...).Build() + vpcService := &vpc.VPCService{ + Service: common.Service{ + Client: fakeClient, + NSXClient: &nsx.Client{}, + }, + } + subnetService := &subnet.SubnetService{ + Service: common.Service{ + Client: fakeClient, + NSXClient: &nsx.Client{}, + + NSXConfig: &config.NSXOperatorConfig{ + NsxConfig: &config.NsxConfig{ + EnforcementPoint: "vmc-enforcementpoint", + UseAVILoadBalancer: false, + }, + }, + }, + SubnetStore: &subnet.SubnetStore{}, + } + + subnetPortService := &subnetport.SubnetPortService{ + Service: common.Service{ + Client: nil, + NSXClient: &nsx.Client{}, + }, + SubnetPortStore: nil, + } + + return &SubnetSetReconciler{ + Client: fakeClient, + Scheme: fake.NewClientBuilder().Build().Scheme(), + VPCService: vpcService, + SubnetService: subnetService, + SubnetPortService: subnetPortService, + Recorder: &fakeRecorder{}, + } +} + +func TestReconcile(t *testing.T) { + subnetsetName := "test-subnetset" + ns := "test-namespace" + + testCases := []struct { + name string + expectRes ctrl.Result + expectErrStr string + patches func(r *SubnetSetReconciler) *gomonkey.Patches + }{ + { + name: "Create a SubnetSet with find VPCNetworkConfig error", + expectRes: ResultRequeue, + expectErrStr: "failed to find VPCNetworkConfig for Namespace", + patches: nil, + }, + { + // TODO: should check the SubnetSet status has error message, which contains 'ipv4SubnetSize has invalid size' + name: "Create a SubnetSet with invalid IPv4SubnetSize", + expectRes: ResultNormal, + expectErrStr: "", + patches: func(r *SubnetSetReconciler) *gomonkey.Patches { + vpcnetworkInfo := &common.VPCNetworkConfigInfo{DefaultSubnetSize: 15} + patches := gomonkey.ApplyMethod(reflect.TypeOf(r.VPCService), "GetVPCNetworkConfigByNamespace", func(_ *vpc.VPCService, ns string) *common.VPCNetworkConfigInfo { + return vpcnetworkInfo + }) + return patches + }, + }, + { + name: "Create a SubnetSet with error failed to generate SubnetSet tags", + expectRes: ResultRequeue, + expectErrStr: "failed to generate SubnetSet tags", + patches: func(r *SubnetSetReconciler) *gomonkey.Patches { + vpcnetworkInfo := &common.VPCNetworkConfigInfo{DefaultSubnetSize: 32} + patches := gomonkey.ApplyMethod(reflect.TypeOf(r.VPCService), "GetVPCNetworkConfigByNamespace", func(_ *vpc.VPCService, ns string) *common.VPCNetworkConfigInfo { + return vpcnetworkInfo + }) + + patches.ApplyMethod(reflect.TypeOf(r.SubnetService.SubnetStore), "GetByIndex", func(_ *subnet.SubnetStore, key string, value string) []*model.VpcSubnet { + id1 := "fake-id" + path := "fake-path" + vpcSubnet := model.VpcSubnet{Id: &id1, Path: &path} + return []*model.VpcSubnet{ + &vpcSubnet, + } + }) + return patches + }, + }, + { + // return nil and not requeue when UpdateSubnetSetTags failed + name: "Create a SubnetSet failed to UpdateSubnetSetTags", + expectRes: ResultNormal, + expectErrStr: "", + patches: func(r *SubnetSetReconciler) *gomonkey.Patches { + vpcnetworkInfo := &common.VPCNetworkConfigInfo{DefaultSubnetSize: 32} + patches := gomonkey.ApplyMethod(reflect.TypeOf(r.VPCService), "GetVPCNetworkConfigByNamespace", func(_ *vpc.VPCService, ns string) *common.VPCNetworkConfigInfo { + return vpcnetworkInfo + }) + + tags := []model.Tag{{Scope: common.String(common.TagScopeVMNamespace), Tag: common.String(ns)}} + patches.ApplyMethod(reflect.TypeOf(r.SubnetService.SubnetStore), "GetByIndex", func(_ *subnet.SubnetStore, key string, value string) []*model.VpcSubnet { + id1 := "fake-id" + path := "fake-path" + vpcSubnet := model.VpcSubnet{Id: &id1, Path: &path, Tags: tags} + return []*model.VpcSubnet{ + &vpcSubnet, + } + }) + + patches.ApplyMethod(reflect.TypeOf(r.SubnetService), "GenerateSubnetNSTags", func(_ *subnet.SubnetService, obj client.Object) []model.Tag { + return tags + }) + return patches + }, + }, + { + name: "Create a SubnetSet with exceed tags", + expectRes: ResultNormal, + expectErrStr: "", + patches: func(r *SubnetSetReconciler) *gomonkey.Patches { + vpcnetworkInfo := &common.VPCNetworkConfigInfo{DefaultSubnetSize: 32} + patches := gomonkey.ApplyMethod(reflect.TypeOf(r.VPCService), "GetVPCNetworkConfigByNamespace", func(_ *vpc.VPCService, ns string) *common.VPCNetworkConfigInfo { + return vpcnetworkInfo + }) + + patches.ApplyMethod(reflect.TypeOf(r.SubnetService.SubnetStore), "GetByIndex", func(_ *subnet.SubnetStore, key string, value string) []*model.VpcSubnet { + id1 := "fake-id" + path := "fake-path" + vpcSubnet := model.VpcSubnet{Id: &id1, Path: &path} + return []*model.VpcSubnet{ + &vpcSubnet, + } + }) + + tags := []model.Tag{{Scope: common.String(common.TagScopeSubnetCRUID), Tag: common.String("fake-tag")}} + for i := 0; i < common.TagsCountMax; i++ { + key := fmt.Sprintf("fake-tag-key-%d", i) + value := common.String(fmt.Sprintf("fake-tag-value-%d", i)) + tags = append(tags, model.Tag{Scope: &key, Tag: value}) + } + patches.ApplyMethod(reflect.TypeOf(r.SubnetService), "GenerateSubnetNSTags", func(_ *subnet.SubnetService, obj client.Object) []model.Tag { + return tags + }) + return patches + }, + }, + { + name: "Create a SubnetSet success", + expectRes: ResultNormal, + expectErrStr: "", + patches: func(r *SubnetSetReconciler) *gomonkey.Patches { + vpcnetworkInfo := &common.VPCNetworkConfigInfo{DefaultSubnetSize: 32} + patches := gomonkey.ApplyMethod(reflect.TypeOf(r.VPCService), "GetVPCNetworkConfigByNamespace", func(_ *vpc.VPCService, ns string) *common.VPCNetworkConfigInfo { + return vpcnetworkInfo + }) + + patches.ApplyMethod(reflect.TypeOf(r.SubnetService.SubnetStore), "GetByIndex", func(_ *subnet.SubnetStore, key string, value string) []*model.VpcSubnet { + id1 := "fake-id" + path := "fake-path" + vpcSubnet := model.VpcSubnet{Id: &id1, Path: &path} + return []*model.VpcSubnet{ + &vpcSubnet, + } + }) + + tags := []model.Tag{{Scope: common.String(common.TagScopeSubnetCRUID), Tag: common.String("fake-tag")}} + patches.ApplyMethod(reflect.TypeOf(r.SubnetService), "GenerateSubnetNSTags", func(_ *subnet.SubnetService, obj client.Object) []model.Tag { + return tags + }) + + // UpdateSubnetSetTags + patches.ApplyMethod(reflect.TypeOf(r.SubnetService), "UpdateSubnetSetTags", func(_ *subnet.SubnetService, ns string, vpcSubnets []*model.VpcSubnet, tags []model.Tag) error { + return nil + }) + return patches + }, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + ctx := context.TODO() + req := ctrl.Request{NamespacedName: types.NamespacedName{Name: subnetsetName, Namespace: ns}} + + subnetset := &v1alpha1.SubnetSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: subnetsetName, + Namespace: ns, + }, + Spec: v1alpha1.SubnetSetSpec{}, + } + namespace := &v12.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: ns, Namespace: ns}, + } + + r := createFakeSubnetSetReconciler([]client.Object{subnetset, namespace}) + if testCase.patches != nil { + patches := testCase.patches(r) + defer patches.Reset() + } + + res, err := r.Reconcile(ctx, req) + + if testCase.expectErrStr != "" { + assert.ErrorContains(t, err, testCase.expectErrStr) + } + assert.Equal(t, testCase.expectRes, res) + }) + } +} + +// Test Reconcile - SubnetSet Deletion +func TestReconcile_DeleteSubnetSet(t *testing.T) { + subnetSetName := "test-subnetset" + testCases := []struct { + name string + expectRes ctrl.Result + expectErrStr string + patches func(r *SubnetSetReconciler) *gomonkey.Patches + }{ + { + name: "Delete success", + patches: func(r *SubnetSetReconciler) *gomonkey.Patches { + patches := gomonkey.ApplyMethod(reflect.TypeOf(r.SubnetService.SubnetStore), "GetByIndex", func(_ *subnet.SubnetStore, key string, value string) []*model.VpcSubnet { + id1 := "fake-id" + path := "fake-path" + tags := []model.Tag{ + {Scope: common.String(common.TagScopeSubnetSetCRUID), Tag: common.String("fake-subnetSet-uid-2")}, + {Scope: common.String(common.TagScopeSubnetSetCRName), Tag: common.String(subnetSetName)}, + } + vpcSubnetSkip := model.VpcSubnet{Id: &id1, Path: &path, Tags: tags} + + id2 := "fake-id-1" + path2 := "fake-path-2" + tagStale := []model.Tag{ + {Scope: common.String(common.TagScopeSubnetSetCRUID), Tag: common.String("fake-subnetSet-uid-stale")}, + {Scope: common.String(common.TagScopeSubnetSetCRName), Tag: common.String(subnetSetName)}, + } + vpcSubnetDelete := model.VpcSubnet{Id: &id2, Path: &path2, Tags: tagStale} + return []*model.VpcSubnet{ + &vpcSubnetSkip, &vpcSubnetDelete, + } + }) + + patches.ApplyMethod(reflect.TypeOf(r.SubnetPortService), "GetPortsOfSubnet", func(_ *subnetport.SubnetPortService, _ string) (ports []*model.VpcSubnetPort) { + return nil + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetService), "DeleteSubnet", func(_ *subnet.SubnetService, subnet model.VpcSubnet) error { + return nil + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetService), "ListSubnetSetID", func(_ *subnet.SubnetService, ctx context.Context) (sets.Set[string], error) { + res := sets.New[string]("fake-subnetSet-uid-2") + return res, nil + }) + return patches + }, + expectRes: ResultNormal, + }, + { + name: "Delete failed with stale SubnetPort and requeue", + expectErrStr: "hasStaleSubnetPort: true", + patches: func(r *SubnetSetReconciler) *gomonkey.Patches { + patches := gomonkey.ApplyMethod(reflect.TypeOf(r.SubnetService.SubnetStore), "GetByIndex", func(_ *subnet.SubnetStore, key string, value string) []*model.VpcSubnet { + id1 := "fake-id" + path := "fake-path" + tags := []model.Tag{ + {Scope: common.String(common.TagScopeSubnetSetCRUID), Tag: common.String("fake-subnetSet-uid-2")}, + {Scope: common.String(common.TagScopeSubnetSetCRName), Tag: common.String(subnetSetName)}, + } + vpcSubnetSkip := model.VpcSubnet{Id: &id1, Path: &path, Tags: tags} + + id2 := "fake-id-1" + path2 := "fake-path-2" + tagStale := []model.Tag{ + {Scope: common.String(common.TagScopeSubnetSetCRUID), Tag: common.String("fake-subnetSet-uid-stale")}, + {Scope: common.String(common.TagScopeSubnetSetCRName), Tag: common.String(subnetSetName)}, + } + vpcSubnetDelete := model.VpcSubnet{Id: &id2, Path: &path2, Tags: tagStale} + return []*model.VpcSubnet{ + &vpcSubnetSkip, &vpcSubnetDelete, + } + }) + + patches.ApplyMethod(reflect.TypeOf(r.SubnetPortService), "GetPortsOfSubnet", func(_ *subnetport.SubnetPortService, _ string) (ports []*model.VpcSubnetPort) { + id := "fake-subnetport-0" + return []*model.VpcSubnetPort{ + { + Id: &id, + }, + } + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetService), "DeleteSubnet", func(_ *subnet.SubnetService, subnet model.VpcSubnet) error { + return nil + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetService), "ListSubnetSetID", func(_ *subnet.SubnetService, ctx context.Context) (sets.Set[string], error) { + res := sets.New[string]("fake-subnetSet-uid-2") + return res, nil + }) + return patches + }, + expectRes: ResultRequeue, + }, + { + name: "Delete NSX Subnet failed and requeue", + expectErrStr: "multiple errors occurred while deleting Subnets", + patches: func(r *SubnetSetReconciler) *gomonkey.Patches { + patches := gomonkey.ApplyMethod(reflect.TypeOf(r.SubnetService.SubnetStore), "GetByIndex", func(_ *subnet.SubnetStore, key string, value string) []*model.VpcSubnet { + id1 := "fake-id" + path := "fake-path" + tags := []model.Tag{ + {Scope: common.String(common.TagScopeSubnetSetCRUID), Tag: common.String("fake-subnetSet-uid-2")}, + {Scope: common.String(common.TagScopeSubnetSetCRName), Tag: common.String(subnetSetName)}, + } + vpcSubnetSkip := model.VpcSubnet{Id: &id1, Path: &path, Tags: tags} + + id2 := "fake-id-1" + path2 := "fake-path-2" + tagStale := []model.Tag{ + {Scope: common.String(common.TagScopeSubnetSetCRUID), Tag: common.String("fake-subnetSet-uid-stale")}, + {Scope: common.String(common.TagScopeSubnetSetCRName), Tag: common.String(subnetSetName)}, + } + vpcSubnetDelete := model.VpcSubnet{Id: &id2, Path: &path2, Tags: tagStale} + return []*model.VpcSubnet{ + &vpcSubnetSkip, &vpcSubnetDelete, + } + }) + + patches.ApplyMethod(reflect.TypeOf(r.SubnetPortService), "GetPortsOfSubnet", func(_ *subnetport.SubnetPortService, _ string) (ports []*model.VpcSubnetPort) { + return nil + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetService), "DeleteSubnet", func(_ *subnet.SubnetService, subnet model.VpcSubnet) error { + return errors.New("delete NSX Subnet failed") + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetService), "ListSubnetSetID", func(_ *subnet.SubnetService, ctx context.Context) (sets.Set[string], error) { + res := sets.New[string]("fake-subnetSet-uid-2") + return res, nil + }) + return patches + }, + expectRes: ResultRequeue, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + ctx := context.TODO() + req := ctrl.Request{NamespacedName: types.NamespacedName{Name: subnetSetName, Namespace: "default"}} + r := createFakeSubnetSetReconciler(nil) + patches := testCase.patches(r) + defer patches.Reset() + + res, err := r.Reconcile(ctx, req) + + if testCase.expectErrStr == "" { + assert.NoError(t, err) + } else { + assert.ErrorContains(t, err, testCase.expectErrStr) + } + assert.Equal(t, testCase.expectRes, res) + }) + } + +} + +// Test Reconcile - SubnetSet Deletion +func TestReconcile_DeleteSubnetSet_WithFinalizer(t *testing.T) { + ctx := context.TODO() + req := ctrl.Request{NamespacedName: types.NamespacedName{Name: "test-subnetset", Namespace: "default"}} + + subnetset := &v1alpha1.SubnetSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-subnetset", + Namespace: "default", + DeletionTimestamp: &metav1.Time{Time: time.Now()}, + Finalizers: []string{"test-Finalizers"}, + }, + Spec: v1alpha1.SubnetSetSpec{}, + } + + r := createFakeSubnetSetReconciler([]client.Object{subnetset}) + + patches := gomonkey.ApplyMethod(reflect.TypeOf(r.SubnetService.SubnetStore), "GetByIndex", func(_ *subnet.SubnetStore, key string, value string) []*model.VpcSubnet { + id1 := "fake-id" + path := "fake-path" + vpcSubnet := model.VpcSubnet{Id: &id1, Path: &path} + return []*model.VpcSubnet{ + &vpcSubnet, + } + }) + defer patches.Reset() + + patches.ApplyMethod(reflect.TypeOf(r.SubnetPortService), "GetPortsOfSubnet", func(_ *subnetport.SubnetPortService, _ string) (ports []*model.VpcSubnetPort) { + return nil + }) + + patches.ApplyMethod(reflect.TypeOf(r.SubnetService), "DeleteSubnet", func(_ *subnet.SubnetService, subnet model.VpcSubnet) error { + return nil + }) + + res, err := r.Reconcile(ctx, req) + + assert.NoError(t, err) + assert.Equal(t, ctrl.Result{}, res) +} + +// Test UpdateSuccess and UpdateFail +func TestUpdateSuccess(t *testing.T) { + ctx := context.TODO() + subnetset := &v1alpha1.SubnetSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-subnetset", + Namespace: "default", + }, + Status: v1alpha1.SubnetSetStatus{}, + } + + r := createFakeSubnetSetReconciler([]client.Object{subnetset}) + + updateSuccess(r, ctx, subnetset) + + updatedSubnetset := &v1alpha1.SubnetSet{} + err := r.Client.Get(ctx, types.NamespacedName{Name: "test-subnetset", Namespace: "default"}, updatedSubnetset) + assert.NoError(t, err) + // TODO: assert updatedSubnetset.Status.Conditions[0].Message +} + +// Test deleteSuccess and deleteFail +func TestDeleteSuccess(t *testing.T) { + ctx := context.TODO() + subnetset := &v1alpha1.SubnetSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-subnetset", + Namespace: "default", + }, + Status: v1alpha1.SubnetSetStatus{}, + } + + r := createFakeSubnetSetReconciler([]client.Object{subnetset}) + + deleteFail(r, ctx, subnetset, "fake delete error") + + updatedSubnetset := &v1alpha1.SubnetSet{} + err := r.Client.Get(ctx, types.NamespacedName{Name: "test-subnetset", Namespace: "default"}, updatedSubnetset) + assert.NoError(t, err) + // TODO: assert updatedSubnetset.Status.Conditions[0].Message +} + +// Test Merge SubnetSet Status Condition +func TestMergeSubnetSetStatusCondition(t *testing.T) { + ctx := context.TODO() + subnetset := &v1alpha1.SubnetSet{ + Status: v1alpha1.SubnetSetStatus{ + Conditions: []v1alpha1.Condition{ + { + Type: v1alpha1.Ready, + Status: v12.ConditionStatus(metav1.ConditionFalse), + }, + }, + }, + } + + r := createFakeSubnetSetReconciler([]client.Object{subnetset}) + + newCondition := v1alpha1.Condition{ + Type: v1alpha1.Ready, + Status: v12.ConditionStatus(metav1.ConditionTrue), + } + + updated := r.mergeSubnetSetStatusCondition(ctx, subnetset, &newCondition) + + assert.True(t, updated) + assert.Equal(t, v12.ConditionStatus(metav1.ConditionTrue), subnetset.Status.Conditions[0].Status) +} + +// Test deleteSubnetBySubnetSetName +func TestDeleteSubnetBySubnetSetName(t *testing.T) { + ctx := context.TODO() + + r := createFakeSubnetSetReconciler(nil) + + patches := gomonkey.ApplyMethod(reflect.TypeOf(r.SubnetService), "ListSubnetBySubnetSetName", func(_ *subnet.SubnetService, ns, subnetSetName string) []*model.VpcSubnet { + return []*model.VpcSubnet{} + }) + defer patches.Reset() + + patches.ApplyMethod(reflect.TypeOf(r.SubnetService), "ListSubnetSetID", func(_ *subnet.SubnetService, ctx context.Context) (sets.Set[string], error) { + return nil, nil + }) + + err := r.deleteSubnetBySubnetSetName(ctx, "test-subnetset", "default") + assert.NoError(t, err) +} + +func TestSubnetSetReconciler_CollectGarbage(t *testing.T) { + r := createFakeSubnetSetReconciler(nil) + + ctx := context.TODO() + + subnetSet := v1alpha1.SubnetSet{ + ObjectMeta: metav1.ObjectMeta{ + UID: "fake-subnetset-uid", + Name: "test-subnetset", + Namespace: "test-namespace", + }, + } + subnetSetList := &v1alpha1.SubnetSetList{ + TypeMeta: metav1.TypeMeta{}, + ListMeta: metav1.ListMeta{}, + Items: []v1alpha1.SubnetSet{subnetSet}, + } + + patches := gomonkey.ApplyFunc(listSubnetSet, func(c client.Client, ctx context.Context, options ...client.ListOption) (*v1alpha1.SubnetSetList, error) { + return subnetSetList, nil + }) + defer patches.Reset() + + patches.ApplyMethod(reflect.TypeOf(r.SubnetService.SubnetStore), "GetByIndex", func(_ *subnet.SubnetStore, key string, value string) []*model.VpcSubnet { + id1 := "fake-id" + path := "fake-path" + vpcSubnet := model.VpcSubnet{Id: &id1, Path: &path} + return []*model.VpcSubnet{ + &vpcSubnet, + } + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetPortService), "GetPortsOfSubnet", func(_ *subnetport.SubnetPortService, _ string) (ports []*model.VpcSubnetPort) { + return nil + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetService), "DeleteSubnet", func(_ *subnet.SubnetService, subnet model.VpcSubnet) error { + return nil + }) + + patches.ApplyMethod(reflect.TypeOf(&common.ResourceStore{}), "ListIndexFuncValues", func(_ *common.ResourceStore, _ string) sets.Set[string] { + res := sets.New[string]("fake-subnetSet-uid-2") + return res + }) + // ListSubnetCreatedBySubnetSet + patches.ApplyMethod(reflect.TypeOf(r.SubnetService), "ListSubnetCreatedBySubnetSet", func(_ *subnet.SubnetService, id string) []*model.VpcSubnet { + id1 := "fake-id" + path := "fake-path" + vpcSubnet := model.VpcSubnet{Id: &id1, Path: &path} + return []*model.VpcSubnet{ + &vpcSubnet, + } + }) + + r.CollectGarbage(ctx) +} + +type MockManager struct { + ctrl.Manager + client client.Client + scheme *runtime.Scheme +} + +func (m *MockManager) GetClient() client.Client { + return m.client +} + +func (m *MockManager) GetScheme() *runtime.Scheme { + return m.scheme +} + +func (m *MockManager) GetEventRecorderFor(name string) record.EventRecorder { + return nil +} + +func (m *MockManager) Add(runnable manager.Runnable) error { + return nil +} + +func (m *MockManager) Start(context.Context) error { + return nil +} + +type mockWebhookServer struct { +} + +func (m *mockWebhookServer) Register(path string, hook http.Handler) { + return +} + +func (m *mockWebhookServer) Start(ctx context.Context) error { + return nil +} + +func (m *mockWebhookServer) StartedChecker() healthz.Checker { + return nil +} + +func (m *mockWebhookServer) WebhookMux() *http.ServeMux { + return nil +} + +func (m *mockWebhookServer) NeedLeaderElection() bool { + return true +} + +func TestStartSubnetSetController(t *testing.T) { + fakeClient := fake.NewClientBuilder().WithObjects().Build() + vpcService := &vpc.VPCService{ + Service: common.Service{ + Client: fakeClient, + }, + } + subnetService := &subnet.SubnetService{ + Service: common.Service{ + Client: fakeClient, + }, + SubnetStore: &subnet.SubnetStore{}, + } + subnetPortService := &subnetport.SubnetPortService{ + Service: common.Service{}, + SubnetPortStore: nil, + } + + mockMgr := &MockManager{scheme: runtime.NewScheme()} + + testCases := []struct { + name string + expectErrStr string + webHookServer webhook.Server + patches func() *gomonkey.Patches + }{ + // expected no error when starting the SubnetSet controller with webhook + { + name: "StartSubnetSetController with webhook", + webHookServer: &mockWebhookServer{}, + patches: func() *gomonkey.Patches { + patches := gomonkey.ApplyFunc(ctlcommon.GenericGarbageCollector, func(cancel chan bool, timeout time.Duration, f func(ctx context.Context)) { + return + }) + patches.ApplyMethod(reflect.TypeOf(&ctrl.Builder{}), "Complete", func(_ *ctrl.Builder, r reconcile.Reconciler) error { + return nil + }) + patches.ApplyPrivateMethod(reflect.TypeOf(&SubnetSetReconciler{}), "setupWithManager", func(_ *SubnetSetReconciler, mgr ctrl.Manager) error { + return nil + }) + return patches + }, + }, + // expected no error when starting the SubnetSet controller without webhook + { + name: "StartSubnetSetController without webhook", + webHookServer: nil, + patches: func() *gomonkey.Patches { + patches := gomonkey.ApplyFunc(ctlcommon.GenericGarbageCollector, func(cancel chan bool, timeout time.Duration, f func(ctx context.Context)) { + return + }) + patches.ApplyMethod(reflect.TypeOf(&ctrl.Builder{}), "Complete", func(_ *ctrl.Builder, r reconcile.Reconciler) error { + return nil + }) + patches.ApplyPrivateMethod(reflect.TypeOf(&SubnetSetReconciler{}), "setupWithManager", func(_ *SubnetSetReconciler, mgr ctrl.Manager) error { + return nil + }) + return patches + }, + }, + { + name: "StartSubnetSetController return error", + expectErrStr: "failed to setupWithManager", + webHookServer: &mockWebhookServer{}, + patches: func() *gomonkey.Patches { + patches := gomonkey.ApplyFunc(ctlcommon.GenericGarbageCollector, func(cancel chan bool, timeout time.Duration, f func(ctx context.Context)) { + return + }) + patches.ApplyMethod(reflect.TypeOf(&ctrl.Builder{}), "Complete", func(_ *ctrl.Builder, r reconcile.Reconciler) error { + return nil + }) + patches.ApplyPrivateMethod(reflect.TypeOf(&SubnetSetReconciler{}), "setupWithManager", func(_ *SubnetSetReconciler, mgr ctrl.Manager) error { + return errors.New("failed to setupWithManager") + }) + return patches + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + patches := testCase.patches() + defer patches.Reset() + + err := StartSubnetSetController(mockMgr, subnetService, subnetPortService, vpcService, testCase.webHookServer) + + if testCase.expectErrStr != "" { + assert.ErrorContains(t, err, testCase.expectErrStr) + } else { + assert.NoError(t, err, "expected no error when starting the SubnetSet controller") + } + }) + } +} diff --git a/pkg/controllers/subnetset/subnetset_webhook.go b/pkg/controllers/subnetset/subnetset_webhook.go index fe0b0c691..633c10924 100644 --- a/pkg/controllers/subnetset/subnetset_webhook.go +++ b/pkg/controllers/subnetset/subnetset_webhook.go @@ -22,8 +22,8 @@ var NSXOperatorSA = "system:serviceaccount:vmware-system-nsx:ncp-svc-account" // inspect admission.Request in Handle function. // +kubebuilder:webhook:path=/validate-crd-nsx-vmware-com-v1alpha1-subnetset,mutating=false,failurePolicy=fail,sideEffects=None, -//groups=crd.nsx.vmware.com,resources=subnetsets,verbs=create;update,versions=v1alpha1, -//name=default.subnetset.validating.crd.nsx.vmware.com,admissionReviewVersions=v1 +// groups=crd.nsx.vmware.com,resources=subnetsets,verbs=create;update,versions=v1alpha1, +// name=default.subnetset.validating.crd.nsx.vmware.com,admissionReviewVersions=v1 type SubnetSetValidator struct { Client client.Client @@ -35,69 +35,50 @@ func defaultSubnetSetLabelChanged(oldSubnetSet, subnetSet *v1alpha1.SubnetSet) b oldValue, oldExists := oldSubnetSet.ObjectMeta.Labels[common.LabelDefaultSubnetSet] value, exists := subnetSet.ObjectMeta.Labels[common.LabelDefaultSubnetSet] // add or remove "default-subnetset-for" label - if oldExists != exists { - return true - } // update "default-subnetset-for" label - if oldValue != value { - return true - } - return false + return oldExists != exists || oldValue != value } func isDefaultSubnetSet(s *v1alpha1.SubnetSet) bool { if _, ok := s.Labels[common.LabelDefaultSubnetSet]; ok { return true } - if s.Name == common.DefaultVMSubnetSet || s.Name == common.DefaultPodSubnetSet { - return true - } - return false + return s.Name == common.DefaultVMSubnetSet || s.Name == common.DefaultPodSubnetSet } // Handle handles admission requests. func (v *SubnetSetValidator) Handle(ctx context.Context, req admission.Request) admission.Response { subnetSet := &v1alpha1.SubnetSet{} + var err error if req.Operation == admissionv1.Delete { - err := v.decoder.DecodeRaw(req.OldObject, subnetSet) - if err != nil { - log.Error(err, "error while decoding SubnetSet", "SubnetSet", req.Namespace+"/"+req.Name) - return admission.Errored(http.StatusBadRequest, err) - } + err = v.decoder.DecodeRaw(req.OldObject, subnetSet) } else { - err := v.decoder.Decode(req, subnetSet) - if err != nil { - log.Error(err, "error while decoding SubnetSet", "SubnetSet", req.Namespace+"/"+req.Name) - return admission.Errored(http.StatusBadRequest, err) - } + err = v.decoder.Decode(req, subnetSet) + } + if err != nil { + log.Error(err, "Failed to decode SubnetSet", "SubnetSet", req.Namespace+"/"+req.Name) + return admission.Errored(http.StatusBadRequest, err) } - log.V(1).Info("request user-info", "name", req.UserInfo.Username) + + log.V(1).Info("Handling request", "user", req.UserInfo.Username, "operation", req.Operation) switch req.Operation { case admissionv1.Create: - if !isDefaultSubnetSet(subnetSet) { - return admission.Allowed("") - } - if req.UserInfo.Username == NSXOperatorSA { - return admission.Allowed("") + if isDefaultSubnetSet(subnetSet) && req.UserInfo.Username != NSXOperatorSA { + return admission.Denied("default SubnetSet only can be created by nsx-operator") } - return admission.Denied("default SubnetSet only can be created by nsx-operator") case admissionv1.Update: oldSubnetSet := &v1alpha1.SubnetSet{} if err := v.decoder.DecodeRaw(req.OldObject, oldSubnetSet); err != nil { - log.Error(err, "error while decoding SubnetSet", "SubnetSet", req.Namespace+"/"+req.Name) + log.Error(err, "Failed to decode old SubnetSet", "SubnetSet", req.Namespace+"/"+req.Name) return admission.Errored(http.StatusBadRequest, err) } if defaultSubnetSetLabelChanged(oldSubnetSet, subnetSet) { return admission.Denied(fmt.Sprintf("SubnetSet label %s only can't be updated", common.LabelDefaultSubnetSet)) } case admissionv1.Delete: - if !isDefaultSubnetSet(subnetSet) { - return admission.Allowed("") - } - if req.UserInfo.Username == NSXOperatorSA { - return admission.Allowed("") + if isDefaultSubnetSet(subnetSet) && req.UserInfo.Username != NSXOperatorSA { + return admission.Denied("default SubnetSet only can be deleted by nsx-operator") } - return admission.Denied("default SubnetSet only can be deleted by nsx-operator") } return admission.Allowed("") } diff --git a/pkg/controllers/subnetset/subnetset_webhook_test.go b/pkg/controllers/subnetset/subnetset_webhook_test.go new file mode 100644 index 000000000..d4ac17df1 --- /dev/null +++ b/pkg/controllers/subnetset/subnetset_webhook_test.go @@ -0,0 +1,156 @@ +package subnetset + +import ( + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + admissionv1 "k8s.io/api/admission/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/vmware-tanzu/nsx-operator/pkg/apis/vpc/v1alpha1" + "github.com/vmware-tanzu/nsx-operator/pkg/nsx/services/common" +) + +func TestSubnetSetValidator(t *testing.T) { + newScheme := runtime.NewScheme() + utilruntime.Must(clientgoscheme.AddToScheme(newScheme)) + utilruntime.Must(v1alpha1.AddToScheme(newScheme)) + fakeClient := fake.NewClientBuilder().WithScheme(newScheme).Build() + + validator := &SubnetSetValidator{ + Client: fakeClient, + decoder: admission.NewDecoder(newScheme), + } + + defaultSubnetSet := &v1alpha1.SubnetSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.DefaultVMSubnetSet, + Labels: map[string]string{common.LabelDefaultSubnetSet: "true"}, + }, + } + + defaultSubnetSet1 := &v1alpha1.SubnetSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-subnetset", + Labels: map[string]string{common.LabelDefaultSubnetSet: "true"}, + }, + } + + subnetSet := &v1alpha1.SubnetSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-subnetset", + }, + } + + testcases := []struct { + name string + op admissionv1.Operation + oldSubnetSet *v1alpha1.SubnetSet + subnetSet *v1alpha1.SubnetSet + user string + isAllowed bool + msg string + }{ + { + name: "Create default SubnetSet with NSXOperatorSA user", + op: admissionv1.Create, + subnetSet: defaultSubnetSet, + user: NSXOperatorSA, + isAllowed: true, + }, + { + name: "Create default SubnetSet(with default label) with NSXOperatorSA user", + op: admissionv1.Create, + subnetSet: defaultSubnetSet1, + user: NSXOperatorSA, + isAllowed: true, + }, + { + name: "Create default SubnetSet without NSXOperatorSA user", + op: admissionv1.Create, + subnetSet: defaultSubnetSet, + user: "fake-user", + isAllowed: false, + msg: "default SubnetSet only can be created by nsx-operator", + }, + { + name: "Create normal SubnetSet", + op: admissionv1.Create, + subnetSet: subnetSet, + user: "fake-user", + isAllowed: true, + }, + { + name: "Delete default SubnetSet with NSXOperatorSA user", + op: admissionv1.Delete, + oldSubnetSet: defaultSubnetSet, + user: NSXOperatorSA, + isAllowed: true, + }, + { + name: "Delete default SubnetSet(with default label) with NSXOperatorSA user", + op: admissionv1.Delete, + oldSubnetSet: defaultSubnetSet1, + user: NSXOperatorSA, + isAllowed: true, + }, + { + name: "Delete default SubnetSet without NSXOperatorSA user", + op: admissionv1.Delete, + oldSubnetSet: defaultSubnetSet, + user: "fake-user", + isAllowed: false, + msg: "default SubnetSet only can be deleted by nsx-operator", + }, + { + name: "Delete normal SubnetSet", + op: admissionv1.Delete, + oldSubnetSet: subnetSet, + user: "fake-user", + isAllowed: true, + }, + { + name: "Update normal SubnetSet", + op: admissionv1.Update, + oldSubnetSet: subnetSet, + subnetSet: subnetSet, + user: "fake-user", + isAllowed: true, + }, + { + name: "Update default SubnetSet", + op: admissionv1.Update, + oldSubnetSet: defaultSubnetSet, + subnetSet: subnetSet, + user: "fake-user", + isAllowed: false, + }, + } + for _, testCase := range testcases { + t.Run(testCase.name, func(t *testing.T) { + req := admission.Request{} + jsonData, err := json.Marshal(testCase.subnetSet) + assert.NoError(t, err) + req.Object.Raw = jsonData + + oldJsonData, err := json.Marshal(testCase.oldSubnetSet) + assert.NoError(t, err) + req.OldObject.Raw = oldJsonData + + req.Operation = testCase.op + req.UserInfo.Username = testCase.user + response := validator.Handle(context.TODO(), req) + assert.Equal(t, testCase.isAllowed, response.Allowed) + if testCase.msg != "" { + assert.Contains(t, response.Result.Message, testCase.msg) + } + }) + } +}