diff --git a/pkg/flags/flags.go b/pkg/flags/flags.go index 409359cbc7..f82c5f3461 100644 --- a/pkg/flags/flags.go +++ b/pkg/flags/flags.go @@ -139,7 +139,7 @@ var F = struct { EnableWeightedL4NetLB bool EnableDiscretePortForwarding bool EnableMultiProjectMode bool - MultiProjectCRDProjectNameLabel string + ProviderConfigNameLabelKey string ProviderConfigAPIGroup string EnableL4ILBMixedProtocol bool EnableL4NetLBMixedProtocol bool @@ -330,10 +330,10 @@ L7 load balancing. CSV values accepted. Example: -node-port-ranges=80,8080,400-5 flag.IntVar(&F.KubeClientBurst, "kube-client-burst", 0, "The burst QPS that the controllers' kube client should adhere to through client side throttling. If zero, client will be created with default settings.") flag.BoolVar(&F.EnableDiscretePortForwarding, "enable-discrete-port-forwarding", false, "Enable forwarding of individual ports instead of port ranges.") flag.BoolVar(&F.EnableMultiProjectMode, "enable-multi-project-mode", false, "Enable running in multi-project mode.") - flag.StringVar(&F.MultiProjectCRDProjectNameLabel, "multi-project-crd-project-name-label", "", "The label key for project name of Project in a Project CRD in the Multi-Project cluster.") flag.BoolVar(&F.EnableL4ILBMixedProtocol, "enable-l4ilb-mixed-protocol", false, "Enable support for mixed protocol L4 internal load balancers.") flag.BoolVar(&F.EnableL4NetLBMixedProtocol, "enable-l4netlb-mixed-protocol", false, "Enable support for mixed protocol L4 external load balancers.") flag.StringVar(&F.ProviderConfigAPIGroup, "provider-config-api-group", "", "The API group for the ProviderConfig CRD.") + flag.StringVar(&F.ProviderConfigNameLabelKey, "provider-config-name-label-key", "", "The label key for provider-config name, which is used to identify the provider-config of objects in multi-project mode.") } func Validate() { diff --git a/pkg/multiproject/filteredinformer/filteredcache.go b/pkg/multiproject/filteredinformer/filteredcache.go new file mode 100644 index 0000000000..18ee974b89 --- /dev/null +++ b/pkg/multiproject/filteredinformer/filteredcache.go @@ -0,0 +1,61 @@ +package filteredinformer + +import ( + "k8s.io/client-go/tools/cache" +) + +// providerConfigFilteredCache implements cache.Store and cache.Indexer with provider config filtering. +type providerConfigFilteredCache struct { + cache.Indexer + providerConfigName string +} + +func (pc *providerConfigFilteredCache) ByIndex(indexName, indexedValue string) ([]interface{}, error) { + items, err := pc.Indexer.ByIndex(indexName, indexedValue) + if err != nil { + return nil, err + } + return providerConfigFilteredList(items, pc.providerConfigName), nil +} + +func (pc *providerConfigFilteredCache) Index(indexName string, obj interface{}) ([]interface{}, error) { + items, err := pc.Indexer.Index(indexName, obj) + if err != nil { + return nil, err + } + return providerConfigFilteredList(items, pc.providerConfigName), nil +} + +func (pc *providerConfigFilteredCache) List() []interface{} { + return providerConfigFilteredList(pc.Indexer.List(), pc.providerConfigName) +} + +func (pc *providerConfigFilteredCache) ListKeys() []string { + items := pc.List() + var keys []string + for _, item := range items { + if key, err := cache.MetaNamespaceKeyFunc(item); err == nil { + keys = append(keys, key) + } + } + return keys +} + +func (pc *providerConfigFilteredCache) Get(obj interface{}) (item interface{}, exists bool, err error) { + key, err := cache.MetaNamespaceKeyFunc(obj) + if err != nil { + return nil, false, err + } + return pc.GetByKey(key) +} + +func (pc *providerConfigFilteredCache) GetByKey(key string) (item interface{}, exists bool, err error) { + item, exists, err = pc.Indexer.GetByKey(key) + if !exists || err != nil { + return nil, exists, err + } + if isObjectInProviderConfig(item, pc.providerConfigName) { + return item, true, nil + } + return nil, false, nil +} diff --git a/pkg/multiproject/projectinformer/projectcache_test.go b/pkg/multiproject/filteredinformer/filteredcache_test.go similarity index 51% rename from pkg/multiproject/projectinformer/projectcache_test.go rename to pkg/multiproject/filteredinformer/filteredcache_test.go index 5f7c7a767f..343b07cecf 100644 --- a/pkg/multiproject/projectinformer/projectcache_test.go +++ b/pkg/multiproject/filteredinformer/filteredcache_test.go @@ -1,4 +1,4 @@ -package projectinformer +package filteredinformer import ( "testing" @@ -9,32 +9,32 @@ import ( "k8s.io/ingress-gce/pkg/flags" ) -func TestProjectCache_ByIndex(t *testing.T) { - flags.F.MultiProjectCRDProjectNameLabel = "project-name-label" +func TestProviderConfigFilteredCache_ByIndex(t *testing.T) { + flags.F.ProviderConfigNameLabelKey = "provider-config-name-label" testCases := []struct { - desc string - cacheProject string - objectsInCache []interface{} - queryName string - expectedItemNames []string + desc string + cacheProviderConfig string + objectsInCache []interface{} + queryName string + expectedItemNames []string }{ { - desc: "Retrieve items by index in project", - cacheProject: "p123456-abc", + desc: "Retrieve items by index in provider config", + cacheProviderConfig: "cs123456-abc", objectsInCache: []interface{}{ - &v1.ObjectMeta{Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p123456-abc"}, Namespace: "p123456-abc-namespace", Name: "obj1"}, - &v1.ObjectMeta{Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p123456-abc"}, Namespace: "p123456-abc-namespace", Name: "obj2"}, - &v1.ObjectMeta{Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p654321-edf"}, Namespace: "p654321-edf-namespace", Name: "obj1"}, + &v1.ObjectMeta{Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "cs123456-abc"}, Namespace: "cs123456-abc-namespace", Name: "obj1"}, + &v1.ObjectMeta{Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "cs123456-abc"}, Namespace: "cs123456-abc-namespace", Name: "obj2"}, + &v1.ObjectMeta{Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "cs654321-edf"}, Namespace: "cs654321-edf-namespace", Name: "obj1"}, }, queryName: "obj1", expectedItemNames: []string{"obj1"}, }, { - desc: "No items when index key does not match", - cacheProject: "p123456-abc", + desc: "No items when index key does not match", + cacheProviderConfig: "cs123456-abc", objectsInCache: []interface{}{ - &v1.ObjectMeta{Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p123456-abc"}, Name: "obj1"}, + &v1.ObjectMeta{Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "cs123456-abc"}, Name: "obj1"}, }, queryName: "nonexistent", expectedItemNames: []string{}, @@ -53,9 +53,9 @@ func TestProjectCache_ByIndex(t *testing.T) { tc := tc t.Run(tc.desc, func(t *testing.T) { indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, indexers) - nsCache := &projectCache{ - Indexer: indexer, - projectName: tc.cacheProject, + nsCache := &providerConfigFilteredCache{ + Indexer: indexer, + providerConfigName: tc.cacheProviderConfig, } for _, obj := range tc.objectsInCache { @@ -80,29 +80,29 @@ func TestProjectCache_ByIndex(t *testing.T) { } } -func TestProjectCache_List(t *testing.T) { - flags.F.MultiProjectCRDProjectNameLabel = "project-name-label" +func TestProviderConfigFilteredCache_List(t *testing.T) { + flags.F.ProviderConfigNameLabelKey = "provider-config-name-label" testCases := []struct { - desc string - cacheProject string - objectsInCache []interface{} - expectedItemNames []string + desc string + cacheProviderConfig string + objectsInCache []interface{} + expectedItemNames []string }{ { - desc: "List items in the project", - cacheProject: "p123456-abc", + desc: "List items in the provider config", + cacheProviderConfig: "p123456-abc", objectsInCache: []interface{}{ - &v1.ObjectMeta{Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p123456-abc"}, Name: "obj1"}, - &v1.ObjectMeta{Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p654321-edf"}, Name: "obj2"}, + &v1.ObjectMeta{Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "p123456-abc"}, Name: "obj1"}, + &v1.ObjectMeta{Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "p654321-edf"}, Name: "obj2"}, }, expectedItemNames: []string{"obj1"}, }, { - desc: "List no items when project has no objects", - cacheProject: "p123456-abc", + desc: "List no items when provider config has no objects", + cacheProviderConfig: "p123456-abc", objectsInCache: []interface{}{ - &v1.ObjectMeta{Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p654321-edf"}, Name: "obj1"}, + &v1.ObjectMeta{Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "p654321-edf"}, Name: "obj1"}, }, expectedItemNames: []string{}, }, @@ -112,9 +112,9 @@ func TestProjectCache_List(t *testing.T) { tc := tc t.Run(tc.desc, func(t *testing.T) { indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, nil) - nsCache := &projectCache{ - Indexer: indexer, - projectName: tc.cacheProject, + nsCache := &providerConfigFilteredCache{ + Indexer: indexer, + providerConfigName: tc.cacheProviderConfig, } for _, obj := range tc.objectsInCache { @@ -136,23 +136,23 @@ func TestProjectCache_List(t *testing.T) { } } -func TestProjectCache_GetByKey(t *testing.T) { - flags.F.MultiProjectCRDProjectNameLabel = "project-name-label" +func TestProviderConfigFilteredCache_GetByKey(t *testing.T) { + flags.F.ProviderConfigNameLabelKey = "provider-config-name-label" testCases := []struct { - desc string - cacheProject string - queryKey string - objectsInCache []interface{} - expectedExist bool - expectedName string + desc string + cacheProviderConfig string + queryKey string + objectsInCache []interface{} + expectedExist bool + expectedName string }{ { - desc: "Get existing item by key in project", - cacheProject: "p123456-abc", - queryKey: "p123456-abc-namespace/obj1", + desc: "Get existing item by key in provider config", + cacheProviderConfig: "p123456-abc", + queryKey: "p123456-abc-namespace/obj1", objectsInCache: []interface{}{&v1.ObjectMeta{ - Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p123456-abc"}, + Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "p123456-abc"}, Namespace: "p123456-abc-namespace", Name: "obj1", }}, @@ -160,21 +160,21 @@ func TestProjectCache_GetByKey(t *testing.T) { expectedName: "obj1", }, { - desc: "Item exists but in different project", - cacheProject: "p123456-abc", - queryKey: "p654321-edf-namespace/obj1", + desc: "Item exists but in different provider config", + cacheProviderConfig: "p123456-abc", + queryKey: "p654321-edf-namespace/obj1", objectsInCache: []interface{}{&v1.ObjectMeta{ - Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p654321-edf"}, + Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "p654321-edf"}, Namespace: "p654321-edf-namespace", Name: "obj1", }}, expectedExist: false, }, { - desc: "Item does not exist", - cacheProject: "p123456-abc", + desc: "Item does not exist", + cacheProviderConfig: "p123456-abc", objectsInCache: []interface{}{&v1.ObjectMeta{ - Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p123456-abc"}, + Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "p123456-abc"}, Namespace: "p123456-abc-namespace", Name: "obj1", }}, @@ -187,9 +187,9 @@ func TestProjectCache_GetByKey(t *testing.T) { tc := tc t.Run(tc.desc, func(t *testing.T) { indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, nil) - nsCache := &projectCache{ - Indexer: indexer, - projectName: tc.cacheProject, + nsCache := &providerConfigFilteredCache{ + Indexer: indexer, + providerConfigName: tc.cacheProviderConfig, } for _, obj := range tc.objectsInCache { diff --git a/pkg/multiproject/filteredinformer/filteredinformer.go b/pkg/multiproject/filteredinformer/filteredinformer.go new file mode 100644 index 0000000000..fbe31be4d3 --- /dev/null +++ b/pkg/multiproject/filteredinformer/filteredinformer.go @@ -0,0 +1,61 @@ +package filteredinformer + +import ( + "time" + + "k8s.io/client-go/tools/cache" +) + +// ProviderConfigFilteredInformer wraps a SharedIndexInformer to provide a ProviderConfig filtered view. +type ProviderConfigFilteredInformer struct { + cache.SharedIndexInformer + providerConfigName string +} + +// NewProviderConfigFilteredInformer creates a new ProviderConfigFilteredInformer. +func NewProviderConfigFilteredInformer(informer cache.SharedIndexInformer, providerConfigName string) cache.SharedIndexInformer { + return &ProviderConfigFilteredInformer{ + SharedIndexInformer: informer, + providerConfigName: providerConfigName, + } +} + +// AddEventHandler adds an event handler that only processes events for the specified ProviderConfig. +func (i *ProviderConfigFilteredInformer) AddEventHandler(handler cache.ResourceEventHandler) (cache.ResourceEventHandlerRegistration, error) { + return i.SharedIndexInformer.AddEventHandler( + cache.FilteringResourceEventHandler{ + FilterFunc: i.providerConfigFilter, + Handler: handler, + }, + ) +} + +// AddEventHandlerWithResyncPeriod adds an event handler with resync period. +func (i *ProviderConfigFilteredInformer) AddEventHandlerWithResyncPeriod(handler cache.ResourceEventHandler, resyncPeriod time.Duration) (cache.ResourceEventHandlerRegistration, error) { + return i.SharedIndexInformer.AddEventHandlerWithResyncPeriod( + cache.FilteringResourceEventHandler{ + FilterFunc: i.providerConfigFilter, + Handler: handler, + }, + resyncPeriod, + ) +} + +// providerConfigFilter filters objects based on the provider config. +func (i *ProviderConfigFilteredInformer) providerConfigFilter(obj interface{}) bool { + return isObjectInProviderConfig(obj, i.providerConfigName) +} + +func (i *ProviderConfigFilteredInformer) GetStore() cache.Store { + return &providerConfigFilteredCache{ + Indexer: i.SharedIndexInformer.GetIndexer(), + providerConfigName: i.providerConfigName, + } +} + +func (i *ProviderConfigFilteredInformer) GetIndexer() cache.Indexer { + return &providerConfigFilteredCache{ + Indexer: i.SharedIndexInformer.GetIndexer(), + providerConfigName: i.providerConfigName, + } +} diff --git a/pkg/multiproject/filteredinformer/filteredinformer_test.go b/pkg/multiproject/filteredinformer/filteredinformer_test.go new file mode 100644 index 0000000000..3bbc6509e2 --- /dev/null +++ b/pkg/multiproject/filteredinformer/filteredinformer_test.go @@ -0,0 +1,66 @@ +package filteredinformer + +import ( + "testing" + "time" + + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/ingress-gce/pkg/flags" +) + +// TestFilteredInformer_AddEventHandler verifies that the +// filteredinformer.AddEventHandler method does not return an error. +func TestFilteredInformer_AddEventHandler(t *testing.T) { + flags.F.ProviderConfigNameLabelKey = "provider-config-name-label" + + sharedInformer := cache.NewSharedIndexInformer(nil, &corev1.Pod{}, 0, nil) + filteredinformer := NewProviderConfigFilteredInformer(sharedInformer, "test-provider-config") + + handler := cache.ResourceEventHandlerFuncs{} + + _, err := filteredinformer.AddEventHandler(handler) + if err != nil { + t.Fatalf("Failed to add event handler: %v", err) + } +} + +// TestFilteredInformer_AddEventHandlerWithResyncPeriod verifies that the +// namespacedinformer.AddEventHandlerWithResyncPeriod method does not return an +// error. +func TestFilteredInformer_AddEventHandlerWithResyncPeriod(t *testing.T) { + flags.F.ProviderConfigNameLabelKey = "provider-config-name-label" + + testCases := []struct { + desc string + providerConfigName string + resyncPeriod time.Duration + }{ + { + desc: "Add event handler with resync period", + providerConfigName: "test-provider-config", + resyncPeriod: time.Minute, + }, + { + desc: "Add event handler with zero resync period", + providerConfigName: "test-provider-config", + resyncPeriod: 0, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + + sharedInformer := cache.NewSharedIndexInformer(nil, &corev1.Pod{}, 0, nil) + filteredinformer := NewProviderConfigFilteredInformer(sharedInformer, tc.providerConfigName) + + handler := cache.ResourceEventHandlerFuncs{} + _, err := filteredinformer.AddEventHandlerWithResyncPeriod(handler, tc.resyncPeriod) + if err != nil { + t.Fatalf("Failed to add event handler with resync period: %v", err) + } + }) + } +} diff --git a/pkg/multiproject/filteredinformer/helpers.go b/pkg/multiproject/filteredinformer/helpers.go new file mode 100644 index 0000000000..665e2f0143 --- /dev/null +++ b/pkg/multiproject/filteredinformer/helpers.go @@ -0,0 +1,26 @@ +package filteredinformer + +import ( + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/ingress-gce/pkg/flags" +) + +// isObjectInProviderConfig checks if an object belongs to a specific provider config. +func isObjectInProviderConfig(obj interface{}, providerConfigName string) bool { + metaObj, err := meta.Accessor(obj) + if err != nil { + return false + } + return metaObj.GetLabels()[flags.F.ProviderConfigNameLabelKey] == providerConfigName +} + +// providerConfigFilteredList filters a list of objects by provider config name. +func providerConfigFilteredList(items []interface{}, providerConfigName string) []interface{} { + var filtered []interface{} + for _, item := range items { + if isObjectInProviderConfig(item, providerConfigName) { + filtered = append(filtered, item) + } + } + return filtered +} diff --git a/pkg/multiproject/filteredinformer/helpers_test.go b/pkg/multiproject/filteredinformer/helpers_test.go new file mode 100644 index 0000000000..b55b336cda --- /dev/null +++ b/pkg/multiproject/filteredinformer/helpers_test.go @@ -0,0 +1,159 @@ +package filteredinformer + +import ( + "fmt" + "testing" + + meta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/ingress-gce/pkg/flags" +) + +func TestIsObjectInProviderConfig(t *testing.T) { + testCases := []struct { + desc string + providerConfigName string + object interface{} + expectedToMatch bool + }{ + { + desc: "Object in provider config should return true", + providerConfigName: "p123456-abc", + object: &metav1.ObjectMeta{Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "p123456-abc"}}, + expectedToMatch: true, + }, + { + desc: "Object in different provider config should return false", + providerConfigName: "p123456-abc", + object: &metav1.ObjectMeta{Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "p654321-def"}}, + expectedToMatch: false, + }, + { + desc: "Object with no provider config should return false", + providerConfigName: "p123456-abc", + object: &metav1.ObjectMeta{Name: "obj3"}, + expectedToMatch: false, + }, + { + desc: "Invalid object should return false", + providerConfigName: "p123456-abc", + object: "invalid-object", + expectedToMatch: false, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + + result := isObjectInProviderConfig(tc.object, tc.providerConfigName) + if result != tc.expectedToMatch { + t.Errorf("Expected isObjectInProviderConfig to return %v, got %v", tc.expectedToMatch, result) + } + }) + } +} + +func TestProviderConfigFilteredList(t *testing.T) { + testCases := []struct { + desc string + providerConfigName string + objects []interface{} + expectedObjects []interface{} + }{ + { + desc: "All objects in the provider config", + providerConfigName: "p123456-abc", + objects: []interface{}{ + &metav1.ObjectMeta{Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "p123456-abc"}}, + &metav1.ObjectMeta{Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "p123456-abc"}}, + }, + expectedObjects: []interface{}{ + &metav1.ObjectMeta{Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "p123456-abc"}}, + &metav1.ObjectMeta{Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "p123456-abc"}}, + }, + }, + { + desc: "Some objects in the provider config", + providerConfigName: "p123456-abc", + objects: []interface{}{ + &metav1.ObjectMeta{Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "p123456-abc"}}, + &metav1.ObjectMeta{Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "p654321-def"}}, + &metav1.ObjectMeta{Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "p123456-abc"}}, + }, + expectedObjects: []interface{}{ + &metav1.ObjectMeta{Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "p123456-abc"}}, + &metav1.ObjectMeta{Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "p123456-abc"}}, + }, + }, + { + desc: "No objects in the provider config", + providerConfigName: "p123456-abc", + objects: []interface{}{ + &metav1.ObjectMeta{Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "p654321-def"}}, + &metav1.ObjectMeta{Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "p654321-def"}}, + }, + expectedObjects: []interface{}{}, + }, + { + desc: "Invalid objects in the list", + providerConfigName: "p123456-abc", + objects: []interface{}{ + "invalid-object", + &metav1.ObjectMeta{Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "p123456-abc"}}, + 12345, // Non-object type + }, + expectedObjects: []interface{}{ + &metav1.ObjectMeta{Labels: map[string]string{flags.F.ProviderConfigNameLabelKey: "p123456-abc"}}, + }, + }, + { + desc: "Empty object list", + providerConfigName: "p123456-abc", + objects: []interface{}{}, + expectedObjects: []interface{}{}, + }, + } + + for _, tc := range testCases { + tc := tc // Capture range variable + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + + result := providerConfigFilteredList(tc.objects, tc.providerConfigName) + + if len(result) != len(tc.expectedObjects) { + t.Errorf("Expected %d objects, got %d", len(tc.expectedObjects), len(result)) + } + + for i, obj := range result { + expectedObj := tc.expectedObjects[i] + + objMeta, err1 := metaAccessor(obj) + expectedMeta, err2 := metaAccessor(expectedObj) + + if err1 != nil || err2 != nil { + t.Errorf("Error accessing object metadata: %v, %v", err1, err2) + continue + } + + if objMeta.GetName() != expectedMeta.GetName() || objMeta.GetNamespace() != expectedMeta.GetNamespace() { + t.Errorf("Expected object %v, got %v", expectedMeta, objMeta) + } + } + }) + } +} + +// Helper function to access metadata +func metaAccessor(obj interface{}) (metav1.Object, error) { + if accessor, ok := obj.(metav1.Object); ok { + return accessor, nil + } + if runtimeObj, ok := obj.(runtime.Object); ok { + return meta.Accessor(runtimeObj) + } + return nil, fmt.Errorf("object does not have ObjectMeta") +} diff --git a/pkg/multiproject/projectinformer/helpers.go b/pkg/multiproject/projectinformer/helpers.go deleted file mode 100644 index 8a5066b848..0000000000 --- a/pkg/multiproject/projectinformer/helpers.go +++ /dev/null @@ -1,26 +0,0 @@ -package projectinformer - -import ( - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/ingress-gce/pkg/flags" -) - -// isObjectInProject checks if an object belongs to a specific project. -func isObjectInProject(obj interface{}, projectName string) bool { - metaObj, err := meta.Accessor(obj) - if err != nil { - return false - } - return metaObj.GetLabels()[flags.F.MultiProjectCRDProjectNameLabel] == projectName -} - -// projectNameFilteredList filters a list of objects by project name. -func projectNameFilteredList(items []interface{}, projectName string) []interface{} { - var filtered []interface{} - for _, item := range items { - if isObjectInProject(item, projectName) { - filtered = append(filtered, item) - } - } - return filtered -} diff --git a/pkg/multiproject/projectinformer/helpers_test.go b/pkg/multiproject/projectinformer/helpers_test.go deleted file mode 100644 index 3f3d075a3a..0000000000 --- a/pkg/multiproject/projectinformer/helpers_test.go +++ /dev/null @@ -1,159 +0,0 @@ -package projectinformer - -import ( - "fmt" - "testing" - - meta "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/ingress-gce/pkg/flags" -) - -func TestIsObjectInProject(t *testing.T) { - testCases := []struct { - desc string - projectName string - object interface{} - expectedToMatch bool - }{ - { - desc: "Object in project should return true", - projectName: "p123456-abc", - object: &metav1.ObjectMeta{Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p123456-abc"}}, - expectedToMatch: true, - }, - { - desc: "Object in different project should return false", - projectName: "p123456-abc", - object: &metav1.ObjectMeta{Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p654321-def"}}, - expectedToMatch: false, - }, - { - desc: "Object with no project should return false", - projectName: "p123456-abc", - object: &metav1.ObjectMeta{Name: "obj3"}, - expectedToMatch: false, - }, - { - desc: "Invalid object should return false", - projectName: "p123456-abc", - object: "invalid-object", - expectedToMatch: false, - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.desc, func(t *testing.T) { - t.Parallel() - - result := isObjectInProject(tc.object, tc.projectName) - if result != tc.expectedToMatch { - t.Errorf("Expected isObjectInProject to return %v, got %v", tc.expectedToMatch, result) - } - }) - } -} - -func TestProjectFilteredList(t *testing.T) { - testCases := []struct { - desc string - projectName string - objects []interface{} - expectedObjects []interface{} - }{ - { - desc: "All objects in the project", - projectName: "p123456-abc", - objects: []interface{}{ - &metav1.ObjectMeta{Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p123456-abc"}}, - &metav1.ObjectMeta{Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p123456-abc"}}, - }, - expectedObjects: []interface{}{ - &metav1.ObjectMeta{Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p123456-abc"}}, - &metav1.ObjectMeta{Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p123456-abc"}}, - }, - }, - { - desc: "Some objects in the project", - projectName: "p123456-abc", - objects: []interface{}{ - &metav1.ObjectMeta{Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p123456-abc"}}, - &metav1.ObjectMeta{Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p654321-def"}}, - &metav1.ObjectMeta{Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p123456-abc"}}, - }, - expectedObjects: []interface{}{ - &metav1.ObjectMeta{Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p123456-abc"}}, - &metav1.ObjectMeta{Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p123456-abc"}}, - }, - }, - { - desc: "No objects in the project", - projectName: "p123456-abc", - objects: []interface{}{ - &metav1.ObjectMeta{Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p654321-def"}}, - &metav1.ObjectMeta{Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p654321-def"}}, - }, - expectedObjects: []interface{}{}, - }, - { - desc: "Invalid objects in the list", - projectName: "p123456-abc", - objects: []interface{}{ - "invalid-object", - &metav1.ObjectMeta{Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p123456-abc"}}, - 12345, // Non-object type - }, - expectedObjects: []interface{}{ - &metav1.ObjectMeta{Labels: map[string]string{flags.F.MultiProjectCRDProjectNameLabel: "p123456-abc"}}, - }, - }, - { - desc: "Empty object list", - projectName: "p123456-abc", - objects: []interface{}{}, - expectedObjects: []interface{}{}, - }, - } - - for _, tc := range testCases { - tc := tc // Capture range variable - t.Run(tc.desc, func(t *testing.T) { - t.Parallel() - - result := projectNameFilteredList(tc.objects, tc.projectName) - - if len(result) != len(tc.expectedObjects) { - t.Errorf("Expected %d objects, got %d", len(tc.expectedObjects), len(result)) - } - - for i, obj := range result { - expectedObj := tc.expectedObjects[i] - - objMeta, err1 := metaAccessor(obj) - expectedMeta, err2 := metaAccessor(expectedObj) - - if err1 != nil || err2 != nil { - t.Errorf("Error accessing object metadata: %v, %v", err1, err2) - continue - } - - if objMeta.GetName() != expectedMeta.GetName() || objMeta.GetNamespace() != expectedMeta.GetNamespace() { - t.Errorf("Expected object %v, got %v", expectedMeta, objMeta) - } - } - }) - } -} - -// Helper function to access metadata -func metaAccessor(obj interface{}) (metav1.Object, error) { - if accessor, ok := obj.(metav1.Object); ok { - return accessor, nil - } - if runtimeObj, ok := obj.(runtime.Object); ok { - return meta.Accessor(runtimeObj) - } - return nil, fmt.Errorf("object does not have ObjectMeta") -} diff --git a/pkg/multiproject/projectinformer/projectcache.go b/pkg/multiproject/projectinformer/projectcache.go deleted file mode 100644 index 951f6a79f0..0000000000 --- a/pkg/multiproject/projectinformer/projectcache.go +++ /dev/null @@ -1,61 +0,0 @@ -package projectinformer - -import ( - "k8s.io/client-go/tools/cache" -) - -// projectCache implements cache.Store and cache.Indexer with project filtering. -type projectCache struct { - cache.Indexer - projectName string -} - -func (pc *projectCache) ByIndex(indexName, indexedValue string) ([]interface{}, error) { - items, err := pc.Indexer.ByIndex(indexName, indexedValue) - if err != nil { - return nil, err - } - return projectNameFilteredList(items, pc.projectName), nil -} - -func (pc *projectCache) Index(indexName string, obj interface{}) ([]interface{}, error) { - items, err := pc.Indexer.Index(indexName, obj) - if err != nil { - return nil, err - } - return projectNameFilteredList(items, pc.projectName), nil -} - -func (pc *projectCache) List() []interface{} { - return projectNameFilteredList(pc.Indexer.List(), pc.projectName) -} - -func (pc *projectCache) ListKeys() []string { - items := pc.List() - var keys []string - for _, item := range items { - if key, err := cache.MetaNamespaceKeyFunc(item); err == nil { - keys = append(keys, key) - } - } - return keys -} - -func (pc *projectCache) Get(obj interface{}) (item interface{}, exists bool, err error) { - key, err := cache.MetaNamespaceKeyFunc(obj) - if err != nil { - return nil, false, err - } - return pc.GetByKey(key) -} - -func (pc *projectCache) GetByKey(key string) (item interface{}, exists bool, err error) { - item, exists, err = pc.Indexer.GetByKey(key) - if !exists || err != nil { - return nil, exists, err - } - if isObjectInProject(item, pc.projectName) { - return item, true, nil - } - return nil, false, nil -} diff --git a/pkg/multiproject/projectinformer/projectinformer.go b/pkg/multiproject/projectinformer/projectinformer.go deleted file mode 100644 index 701afc7cd5..0000000000 --- a/pkg/multiproject/projectinformer/projectinformer.go +++ /dev/null @@ -1,61 +0,0 @@ -package projectinformer - -import ( - "time" - - "k8s.io/client-go/tools/cache" -) - -// Informer wraps a SharedIndexInformer to provide a project filtered view. -type ProjectInformer struct { - cache.SharedIndexInformer - projectName string -} - -// NewInformer creates a new Informer. -func NewProjectInformer(informer cache.SharedIndexInformer, projectName string) cache.SharedIndexInformer { - return &ProjectInformer{ - SharedIndexInformer: informer, - projectName: projectName, - } -} - -// AddEventHandler adds an event handler that only processes events for the specified project. -func (i *ProjectInformer) AddEventHandler(handler cache.ResourceEventHandler) (cache.ResourceEventHandlerRegistration, error) { - return i.SharedIndexInformer.AddEventHandler( - cache.FilteringResourceEventHandler{ - FilterFunc: i.projectFilter, - Handler: handler, - }, - ) -} - -// AddEventHandlerWithResyncPeriod adds an event handler with resync period. -func (i *ProjectInformer) AddEventHandlerWithResyncPeriod(handler cache.ResourceEventHandler, resyncPeriod time.Duration) (cache.ResourceEventHandlerRegistration, error) { - return i.SharedIndexInformer.AddEventHandlerWithResyncPeriod( - cache.FilteringResourceEventHandler{ - FilterFunc: i.projectFilter, - Handler: handler, - }, - resyncPeriod, - ) -} - -// projectFilter filters objects based on the project. -func (i *ProjectInformer) projectFilter(obj interface{}) bool { - return isObjectInProject(obj, i.projectName) -} - -func (i *ProjectInformer) GetStore() cache.Store { - return &projectCache{ - Indexer: i.SharedIndexInformer.GetIndexer(), - projectName: i.projectName, - } -} - -func (i *ProjectInformer) GetIndexer() cache.Indexer { - return &projectCache{ - Indexer: i.SharedIndexInformer.GetIndexer(), - projectName: i.projectName, - } -} diff --git a/pkg/multiproject/projectinformer/projectinformer_test.go b/pkg/multiproject/projectinformer/projectinformer_test.go deleted file mode 100644 index 444e1e6e0c..0000000000 --- a/pkg/multiproject/projectinformer/projectinformer_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package projectinformer - -import ( - "testing" - "time" - - corev1 "k8s.io/api/core/v1" - "k8s.io/client-go/tools/cache" - "k8s.io/ingress-gce/pkg/flags" -) - -// TestProjectInformer_AddEventHandler verifies that the -// projectinformer.AddEventHandler method does not return an error. -func TestProjectInformer_AddEventHandler(t *testing.T) { - flags.F.MultiProjectCRDProjectNameLabel = "project-name-label" - - sharedInformer := cache.NewSharedIndexInformer(nil, &corev1.Pod{}, 0, nil) - projectInformer := NewProjectInformer(sharedInformer, "test-project") - - handler := cache.ResourceEventHandlerFuncs{} - - _, err := projectInformer.AddEventHandler(handler) - if err != nil { - t.Fatalf("Failed to add event handler: %v", err) - } -} - -// TestNamespacedInformer_AddEventHandlerWithResyncPeriod verifies that the -// namespacedinformer.AddEventHandlerWithResyncPeriod method does not return an -// error. -func TestProjectInformer_AddEventHandlerWithResyncPeriod(t *testing.T) { - flags.F.MultiProjectCRDProjectNameLabel = "project-name-label" - - testCases := []struct { - desc string - projectName string - resyncPeriod time.Duration - }{ - { - desc: "Add event handler with resync period", - projectName: "test-project", - resyncPeriod: time.Minute, - }, - { - desc: "Add event handler with zero resync period", - projectName: "test-project", - resyncPeriod: 0, - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.desc, func(t *testing.T) { - t.Parallel() - - sharedInformer := cache.NewSharedIndexInformer(nil, &corev1.Pod{}, 0, nil) - projectInformer := NewProjectInformer(sharedInformer, tc.projectName) - - handler := cache.ResourceEventHandlerFuncs{} - _, err := projectInformer.AddEventHandlerWithResyncPeriod(handler, tc.resyncPeriod) - if err != nil { - t.Fatalf("Failed to add event handler with resync period: %v", err) - } - }) - } -}