From 2762512924035c0d62eb6a29122f46a7f867449b Mon Sep 17 00:00:00 2001 From: Zarko Aleksic Date: Mon, 1 Dec 2025 17:20:31 -0600 Subject: [PATCH 1/3] IAMRoleSelector - support for resource label selector --- apis/core/v1alpha1/iam_role_selector.go | 7 +- apis/core/v1alpha1/zz_generated.deepcopy.go | 1 + .../services.k8s.aws_iamroleselectors.yaml | 10 + config/crd/kustomization.yaml | 2 +- pkg/runtime/iamroleselector/cache.go | 5 +- pkg/runtime/iamroleselector/cache_test.go | 54 ++- pkg/runtime/iamroleselector/matcher.go | 34 +- pkg/runtime/iamroleselector/matcher_test.go | 339 ++++++++++++++++++ 8 files changed, 440 insertions(+), 12 deletions(-) diff --git a/apis/core/v1alpha1/iam_role_selector.go b/apis/core/v1alpha1/iam_role_selector.go index 0b37fb4..f1a8302 100644 --- a/apis/core/v1alpha1/iam_role_selector.go +++ b/apis/core/v1alpha1/iam_role_selector.go @@ -36,9 +36,10 @@ type GroupVersionKind struct { type IAMRoleSelectorSpec struct { // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable once set" - ARN string `json:"arn"` - NamespaceSelector NamespaceSelector `json:"namespaceSelector,omitempty"` - ResourceTypeSelector []GroupVersionKind `json:"resourceTypeSelector,omitempty"` + ARN string `json:"arn"` + NamespaceSelector NamespaceSelector `json:"namespaceSelector,omitempty"` + ResourceTypeSelector []GroupVersionKind `json:"resourceTypeSelector,omitempty"` + ResourceLabelSelector LabelSelector `json:"resourceLabelSelector,omitempty"` } type IAMRoleSelectorStatus struct{} diff --git a/apis/core/v1alpha1/zz_generated.deepcopy.go b/apis/core/v1alpha1/zz_generated.deepcopy.go index 9281717..c21775e 100644 --- a/apis/core/v1alpha1/zz_generated.deepcopy.go +++ b/apis/core/v1alpha1/zz_generated.deepcopy.go @@ -333,6 +333,7 @@ func (in *IAMRoleSelectorSpec) DeepCopyInto(out *IAMRoleSelectorSpec) { *out = make([]GroupVersionKind, len(*in)) copy(*out, *in) } + in.ResourceLabelSelector.DeepCopyInto(&out.ResourceLabelSelector) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IAMRoleSelectorSpec. diff --git a/config/crd/bases/services.k8s.aws_iamroleselectors.yaml b/config/crd/bases/services.k8s.aws_iamroleselectors.yaml index 9477c90..803a75c 100644 --- a/config/crd/bases/services.k8s.aws_iamroleselectors.yaml +++ b/config/crd/bases/services.k8s.aws_iamroleselectors.yaml @@ -63,6 +63,16 @@ spec: required: - names type: object + resourceLabelSelector: + description: LabelSelector is a label query over a set of resources. + properties: + matchLabels: + additionalProperties: + type: string + type: object + required: + - matchLabels + type: object resourceTypeSelector: items: properties: diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 8165534..65cb01b 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -3,5 +3,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - - bases/services.k8s.aws_iamroleselectors.yaml - bases/services.k8s.aws_fieldexports.yaml + - bases/services.k8s.aws_iamroleselectors.yaml diff --git a/pkg/runtime/iamroleselector/cache.go b/pkg/runtime/iamroleselector/cache.go index abf1611..e31f869 100644 --- a/pkg/runtime/iamroleselector/cache.go +++ b/pkg/runtime/iamroleselector/cache.go @@ -152,6 +152,7 @@ func (c *Cache) GetMatchingSelectors( namespace string, namespaceLabels map[string]string, gvk schema.GroupVersionKind, + resourceLabels map[string]string, ) ([]*ackv1alpha1.IAMRoleSelector, error) { if c.informer == nil { return nil, fmt.Errorf("cache not initialized") @@ -161,6 +162,7 @@ func (c *Cache) GetMatchingSelectors( Namespace: namespace, NamespaceLabels: namespaceLabels, GVK: gvk, + ResourceLabels: resourceLabels, } c.RLock() @@ -212,6 +214,7 @@ func (c *Cache) Matches(resource runtime.Object) ([]*ackv1alpha1.IAMRoleSelector namespaceName := metaObj.GetNamespace() namespaceLabels := c.Namespaces.GetLabels(namespaceName) + resourceLabels := metaObj.GetLabels() // Get GVK - should be set on ACK resources gvk := resource.GetObjectKind().GroupVersionKind() if gvk.Empty() { @@ -221,5 +224,5 @@ func (c *Cache) Matches(resource runtime.Object) ([]*ackv1alpha1.IAMRoleSelector // TODO: get namespace labels from a namespace lister/cache // For now, pass empty namespace labels - return c.GetMatchingSelectors(namespaceName, namespaceLabels, gvk) + return c.GetMatchingSelectors(namespaceName, namespaceLabels, gvk, resourceLabels) } diff --git a/pkg/runtime/iamroleselector/cache_test.go b/pkg/runtime/iamroleselector/cache_test.go index c49473f..1f617a2 100644 --- a/pkg/runtime/iamroleselector/cache_test.go +++ b/pkg/runtime/iamroleselector/cache_test.go @@ -111,10 +111,24 @@ func TestCache_Matches(t *testing.T) { }, }) + selector4 := createSelector("resource-label-based", ackv1alpha1.IAMRoleSelector{ + ObjectMeta: metav1.ObjectMeta{Name: "resource-label-based"}, + Spec: ackv1alpha1.IAMRoleSelectorSpec{ + ARN: "arn:aws:iam::123456789012:role/labeled-resources-role", + ResourceLabelSelector: ackv1alpha1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "myapp", + "tier": "backend", + }, + }, + }, + }) + // Simulate adding selectors via watcher watcher.Add(selector1) watcher.Add(selector2) watcher.Add(selector3) + watcher.Add(selector4) // Wait for cache to process time.Sleep(100 * time.Millisecond) @@ -128,24 +142,50 @@ func TestCache_Matches(t *testing.T) { }{ { name: "matches production S3 bucket", - resource: mockResource("production", "s3.services.k8s.aws", "v1alpha1", "Bucket"), + resource: mockResource("production", "s3.services.k8s.aws", "v1alpha1", "Bucket", nil), wantCount: 1, wantARNs: []string{"arn:aws:iam::123456789012:role/prod-s3-role"}, }, { name: "matches RDS in any namespace", - resource: mockResource("default", "rds.services.k8s.aws", "v1alpha1", "DBInstance"), + resource: mockResource("default", "rds.services.k8s.aws", "v1alpha1", "DBInstance", nil), wantCount: 1, wantARNs: []string{"arn:aws:iam::123456789012:role/rds-role"}, }, { name: "no match for wrong namespace", - resource: mockResource("development", "s3.services.k8s.aws", "v1alpha1", "Bucket"), + resource: mockResource("development", "s3.services.k8s.aws", "v1alpha1", "Bucket", nil), wantCount: 0, }, { name: "no match for wrong resource type", - resource: mockResource("production", "dynamodb.services.k8s.aws", "v1alpha1", "Table"), + resource: mockResource("production", "dynamodb.services.k8s.aws", "v1alpha1", "Table", nil), + wantCount: 0, + }, + { + name: "matches resource by labels", + resource: mockResource("default", "s3.services.k8s.aws", "v1alpha1", "Bucket", map[string]string{ + "app": "myapp", + "tier": "backend", + "extra": "label", + }), + wantCount: 1, + wantARNs: []string{"arn:aws:iam::123456789012:role/labeled-resources-role"}, + }, + { + name: "does not match resource with wrong labels", + resource: mockResource("default", "s3.services.k8s.aws", "v1alpha1", "Bucket", map[string]string{ + "app": "otherapp", + "tier": "backend", + }), + wantCount: 0, + }, + { + name: "does not match resource with missing labels", + resource: mockResource("default", "s3.services.k8s.aws", "v1alpha1", "Bucket", map[string]string{ + "app": "myapp", + // missing "tier" label + }), wantCount: 0, }, } @@ -232,9 +272,10 @@ func createSelector(name string, selector ackv1alpha1.IAMRoleSelector) *unstruct return u } -func mockResource(namespace, group, version, kind string) runtime.Object { +func mockResource(namespace, group, version, kind string, labels map[string]string) runtime.Object { return &testResource{ namespace: namespace, + labels: labels, gvk: schema.GroupVersionKind{ Group: group, Version: version, @@ -246,6 +287,7 @@ func mockResource(namespace, group, version, kind string) runtime.Object { // Minimal test resource implementation type testResource struct { namespace string + labels map[string]string gvk schema.GroupVersionKind } @@ -280,7 +322,7 @@ func (r *testResource) GetDeletionTimestamp() *metav1.Time { return n func (r *testResource) SetDeletionTimestamp(*metav1.Time) {} func (r *testResource) GetDeletionGracePeriodSeconds() *int64 { return nil } func (r *testResource) SetDeletionGracePeriodSeconds(*int64) {} -func (r *testResource) GetLabels() map[string]string { return nil } +func (r *testResource) GetLabels() map[string]string { return r.labels } func (r *testResource) SetLabels(map[string]string) {} func (r *testResource) GetAnnotations() map[string]string { return nil } func (r *testResource) SetAnnotations(map[string]string) {} diff --git a/pkg/runtime/iamroleselector/matcher.go b/pkg/runtime/iamroleselector/matcher.go index 5130228..3745068 100644 --- a/pkg/runtime/iamroleselector/matcher.go +++ b/pkg/runtime/iamroleselector/matcher.go @@ -28,6 +28,7 @@ type MatchContext struct { Namespace string NamespaceLabels map[string]string GVK schema.GroupVersionKind + ResourceLabels map[string]string } // Matches checks if a selector matches the given context @@ -35,7 +36,20 @@ type MatchContext struct { func Matches(selector *ackv1alpha1.IAMRoleSelector, ctx MatchContext) bool { // All conditions must match (AND logic between different selectors) return matchesNamespace(selector.Spec.NamespaceSelector, ctx.Namespace, ctx.NamespaceLabels) && - matchesResourceType(selector.Spec.ResourceTypeSelector, ctx.GVK) + matchesResourceType(selector.Spec.ResourceTypeSelector, ctx.GVK) && + matchesResourceLabels(selector.Spec.ResourceLabelSelector, ctx.ResourceLabels) +} + +// matchesResourceLabels checks if the resource label selector matches the given resource labels +func matchesResourceLabels(labelSelector ackv1alpha1.LabelSelector, resourceLabels map[string]string) bool { + // If no label selector specified, matches all resources + if len(labelSelector.MatchLabels) == 0 { + return true + } + + // Check if all specified labels match (AND logic within label selector) + selector := labels.SelectorFromSet(labelSelector.MatchLabels) + return selector.Matches(labels.Set(resourceLabels)) } // matchesNamespace checks if the namespace selector matches the given namespace and its labels @@ -122,6 +136,11 @@ func validateSelector(selector *ackv1alpha1.IAMRoleSelector) error { return fmt.Errorf("invalid resource type selector: %w", err) } + // Validate resource label selector + if err := validateResourceLabelSelector(selector.Spec.ResourceLabelSelector); err != nil { + return fmt.Errorf("invalid resource label selector: %w", err) + } + return nil } @@ -151,6 +170,19 @@ func validateNamespaceSelector(nsSelector ackv1alpha1.NamespaceSelector) error { return nil } +// validateResourceLabelSelector checks that the resource label selector has valid label keys +func validateResourceLabelSelector(labelSelector ackv1alpha1.LabelSelector) error { + if len(labelSelector.MatchLabels) > 0 { + for key := range labelSelector.MatchLabels { + if key == "" { + return fmt.Errorf("label key cannot be empty") + } + // Kubernetes label values can be empty, so we don't validate value + } + } + return nil +} + // validateResourceTypeSelectors checks that each resource type selector has at least one field specified // and that there are no duplicate selectors func validateResourceTypeSelectors(rtSelectors []ackv1alpha1.GroupVersionKind) error { diff --git a/pkg/runtime/iamroleselector/matcher_test.go b/pkg/runtime/iamroleselector/matcher_test.go index 753fcee..840bdfb 100644 --- a/pkg/runtime/iamroleselector/matcher_test.go +++ b/pkg/runtime/iamroleselector/matcher_test.go @@ -327,6 +327,193 @@ func TestMatches(t *testing.T) { }, want: false, }, + { + name: "matches resource by labels", + selector: &ackv1alpha1.IAMRoleSelector{ + Spec: ackv1alpha1.IAMRoleSelectorSpec{ + ARN: "arn:aws:iam::123456789012:role/test-role", + ResourceLabelSelector: ackv1alpha1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "myapp", + "tier": "backend", + }, + }, + }, + }, + ctx: MatchContext{ + Namespace: "default", + GVK: schema.GroupVersionKind{ + Group: "s3.services.k8s.aws", + Version: "v1alpha1", + Kind: "Bucket", + }, + ResourceLabels: map[string]string{ + "app": "myapp", + "tier": "backend", + "extra": "label", // extra labels should be ignored + }, + }, + want: true, + }, + { + name: "does not match resource with wrong labels", + selector: &ackv1alpha1.IAMRoleSelector{ + Spec: ackv1alpha1.IAMRoleSelectorSpec{ + ARN: "arn:aws:iam::123456789012:role/test-role", + ResourceLabelSelector: ackv1alpha1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "myapp", + }, + }, + }, + }, + ctx: MatchContext{ + Namespace: "default", + GVK: schema.GroupVersionKind{ + Group: "s3.services.k8s.aws", + Version: "v1alpha1", + Kind: "Bucket", + }, + ResourceLabels: map[string]string{ + "app": "otherapp", + }, + }, + want: false, + }, + { + name: "does not match resource with missing labels", + selector: &ackv1alpha1.IAMRoleSelector{ + Spec: ackv1alpha1.IAMRoleSelectorSpec{ + ARN: "arn:aws:iam::123456789012:role/test-role", + ResourceLabelSelector: ackv1alpha1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "myapp", + "tier": "backend", + }, + }, + }, + }, + ctx: MatchContext{ + Namespace: "default", + GVK: schema.GroupVersionKind{ + Group: "s3.services.k8s.aws", + Version: "v1alpha1", + Kind: "Bucket", + }, + ResourceLabels: map[string]string{ + "app": "myapp", + // missing "tier" label + }, + }, + want: false, + }, + { + name: "empty resource label selector matches all resources", + selector: &ackv1alpha1.IAMRoleSelector{ + Spec: ackv1alpha1.IAMRoleSelectorSpec{ + ARN: "arn:aws:iam::123456789012:role/test-role", + ResourceLabelSelector: ackv1alpha1.LabelSelector{ + MatchLabels: map[string]string{}, + }, + }, + }, + ctx: MatchContext{ + Namespace: "default", + GVK: schema.GroupVersionKind{ + Group: "s3.services.k8s.aws", + Version: "v1alpha1", + Kind: "Bucket", + }, + ResourceLabels: map[string]string{ + "any": "label", + }, + }, + want: true, + }, + { + name: "matches with namespace, resource type, and resource labels", + selector: &ackv1alpha1.IAMRoleSelector{ + Spec: ackv1alpha1.IAMRoleSelectorSpec{ + ARN: "arn:aws:iam::123456789012:role/test-role", + NamespaceSelector: ackv1alpha1.NamespaceSelector{ + Names: []string{"production"}, + }, + ResourceTypeSelector: []ackv1alpha1.GroupVersionKind{ + { + Kind: "Bucket", + }, + }, + ResourceLabelSelector: ackv1alpha1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "myapp", + }, + }, + }, + }, + ctx: MatchContext{ + Namespace: "production", + GVK: schema.GroupVersionKind{ + Group: "s3.services.k8s.aws", + Version: "v1alpha1", + Kind: "Bucket", + }, + ResourceLabels: map[string]string{ + "app": "myapp", + }, + }, + want: true, + }, + { + name: "does not match if resource labels don't match even when namespace and type match", + selector: &ackv1alpha1.IAMRoleSelector{ + Spec: ackv1alpha1.IAMRoleSelectorSpec{ + ARN: "arn:aws:iam::123456789012:role/test-role", + NamespaceSelector: ackv1alpha1.NamespaceSelector{ + Names: []string{"production"}, + }, + ResourceTypeSelector: []ackv1alpha1.GroupVersionKind{ + { + Kind: "Bucket", + }, + }, + ResourceLabelSelector: ackv1alpha1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "myapp", + }, + }, + }, + }, + ctx: MatchContext{ + Namespace: "production", + GVK: schema.GroupVersionKind{ + Group: "s3.services.k8s.aws", + Version: "v1alpha1", + Kind: "Bucket", + }, + ResourceLabels: map[string]string{ + "app": "otherapp", + }, + }, + want: false, + }, + { + name: "matches resource with nil resource labels when no label selector", + selector: &ackv1alpha1.IAMRoleSelector{ + Spec: ackv1alpha1.IAMRoleSelectorSpec{ + ARN: "arn:aws:iam::123456789012:role/test-role", + }, + }, + ctx: MatchContext{ + Namespace: "default", + GVK: schema.GroupVersionKind{ + Group: "s3.services.k8s.aws", + Version: "v1alpha1", + Kind: "Bucket", + }, + ResourceLabels: nil, + }, + want: true, + }, } for _, tt := range tests { @@ -462,6 +649,37 @@ func TestValidateSelector(t *testing.T) { wantErr: true, errMsg: "duplicate resource type selector: s3.services.k8s.aws/v1alpha1/Bucket", }, + { + name: "empty resource label key", + selector: &ackv1alpha1.IAMRoleSelector{ + Spec: ackv1alpha1.IAMRoleSelectorSpec{ + ARN: "arn:aws:iam::123456789012:role/test-role", + ResourceLabelSelector: ackv1alpha1.LabelSelector{ + MatchLabels: map[string]string{ + "": "value", + "app": "myapp", + }, + }, + }, + }, + wantErr: true, + errMsg: "invalid resource label selector: label key cannot be empty", + }, + { + name: "valid resource label selector", + selector: &ackv1alpha1.IAMRoleSelector{ + Spec: ackv1alpha1.IAMRoleSelectorSpec{ + ARN: "arn:aws:iam::123456789012:role/test-role", + ResourceLabelSelector: ackv1alpha1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "myapp", + "tier": "backend", + }, + }, + }, + }, + wantErr: false, + }, { name: "valid complex selector", selector: &ackv1alpha1.IAMRoleSelector{ @@ -485,6 +703,11 @@ func TestValidateSelector(t *testing.T) { Kind: "DBInstance", }, }, + ResourceLabelSelector: ackv1alpha1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "myapp", + }, + }, }, }, wantErr: false, @@ -824,6 +1047,122 @@ func TestMatchesResourceType(t *testing.T) { } } +func TestMatchesResourceLabels(t *testing.T) { + tests := []struct { + name string + labelSelector ackv1alpha1.LabelSelector + resourceLabels map[string]string + want bool + }{ + { + name: "empty selector matches all", + labelSelector: ackv1alpha1.LabelSelector{}, + resourceLabels: map[string]string{"any": "label"}, + want: true, + }, + { + name: "empty selector matches nil labels", + labelSelector: ackv1alpha1.LabelSelector{}, + resourceLabels: nil, + want: true, + }, + { + name: "matches single label", + labelSelector: ackv1alpha1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "myapp", + }, + }, + resourceLabels: map[string]string{ + "app": "myapp", + }, + want: true, + }, + { + name: "matches multiple labels", + labelSelector: ackv1alpha1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "myapp", + "tier": "backend", + }, + }, + resourceLabels: map[string]string{ + "app": "myapp", + "tier": "backend", + }, + want: true, + }, + { + name: "matches with extra labels on resource", + labelSelector: ackv1alpha1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "myapp", + }, + }, + resourceLabels: map[string]string{ + "app": "myapp", + "extra": "label", + "more": "labels", + }, + want: true, + }, + { + name: "does not match - wrong value", + labelSelector: ackv1alpha1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "myapp", + }, + }, + resourceLabels: map[string]string{ + "app": "otherapp", + }, + want: false, + }, + { + name: "does not match - missing label", + labelSelector: ackv1alpha1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "myapp", + "tier": "backend", + }, + }, + resourceLabels: map[string]string{ + "app": "myapp", + }, + want: false, + }, + { + name: "does not match - nil resource labels", + labelSelector: ackv1alpha1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "myapp", + }, + }, + resourceLabels: nil, + want: false, + }, + { + name: "does not match - empty resource labels", + labelSelector: ackv1alpha1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "myapp", + }, + }, + resourceLabels: map[string]string{}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := matchesResourceLabels(tt.labelSelector, tt.resourceLabels) + if got != tt.want { + t.Errorf("matchesResourceLabels() = %v, want %v", got, tt.want) + } + }) + } +} + // Helper function to check if a string contains a substring func contains(s, substr string) bool { return len(substr) > 0 && len(s) >= len(substr) && s[:len(substr)] == substr || From 69f297b91ee6efc7f48764b118605a7d56a607a8 Mon Sep 17 00:00:00 2001 From: Zarko Aleksic Date: Tue, 2 Dec 2025 11:54:53 -0600 Subject: [PATCH 2/3] Use helper for label validation and rename functions --- pkg/runtime/iamroleselector/matcher.go | 27 +++++++-------------- pkg/runtime/iamroleselector/matcher_test.go | 6 ++--- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/pkg/runtime/iamroleselector/matcher.go b/pkg/runtime/iamroleselector/matcher.go index 3745068..7657958 100644 --- a/pkg/runtime/iamroleselector/matcher.go +++ b/pkg/runtime/iamroleselector/matcher.go @@ -37,11 +37,11 @@ func Matches(selector *ackv1alpha1.IAMRoleSelector, ctx MatchContext) bool { // All conditions must match (AND logic between different selectors) return matchesNamespace(selector.Spec.NamespaceSelector, ctx.Namespace, ctx.NamespaceLabels) && matchesResourceType(selector.Spec.ResourceTypeSelector, ctx.GVK) && - matchesResourceLabels(selector.Spec.ResourceLabelSelector, ctx.ResourceLabels) + matchesLabels(selector.Spec.ResourceLabelSelector, ctx.ResourceLabels) } -// matchesResourceLabels checks if the resource label selector matches the given resource labels -func matchesResourceLabels(labelSelector ackv1alpha1.LabelSelector, resourceLabels map[string]string) bool { +// matchesLabels checks if the label selector matches the given resource labels +func matchesLabels(labelSelector ackv1alpha1.LabelSelector, resourceLabels map[string]string) bool { // If no label selector specified, matches all resources if len(labelSelector.MatchLabels) == 0 { return true @@ -136,9 +136,9 @@ func validateSelector(selector *ackv1alpha1.IAMRoleSelector) error { return fmt.Errorf("invalid resource type selector: %w", err) } - // Validate resource label selector - if err := validateResourceLabelSelector(selector.Spec.ResourceLabelSelector); err != nil { - return fmt.Errorf("invalid resource label selector: %w", err) + // Validate label selector + if err := validateLabelSelector(selector.Spec.ResourceLabelSelector); err != nil { + return fmt.Errorf("invalid label selector: %w", err) } return nil @@ -158,20 +158,11 @@ func validateNamespaceSelector(nsSelector ackv1alpha1.NamespaceSelector) error { } // Validate label selector - if len(nsSelector.LabelSelector.MatchLabels) > 0 { - for key := range nsSelector.LabelSelector.MatchLabels { - if key == "" { - return fmt.Errorf("label key cannot be empty") - } - // Kubernetes label values can be empty, so we don't validate value - } - } - - return nil + return validateLabelSelector(nsSelector.LabelSelector) } -// validateResourceLabelSelector checks that the resource label selector has valid label keys -func validateResourceLabelSelector(labelSelector ackv1alpha1.LabelSelector) error { +// validateLabelSelector checks that the label selector has valid label keys +func validateLabelSelector(labelSelector ackv1alpha1.LabelSelector) error { if len(labelSelector.MatchLabels) > 0 { for key := range labelSelector.MatchLabels { if key == "" { diff --git a/pkg/runtime/iamroleselector/matcher_test.go b/pkg/runtime/iamroleselector/matcher_test.go index 840bdfb..97c912a 100644 --- a/pkg/runtime/iamroleselector/matcher_test.go +++ b/pkg/runtime/iamroleselector/matcher_test.go @@ -1047,7 +1047,7 @@ func TestMatchesResourceType(t *testing.T) { } } -func TestMatchesResourceLabels(t *testing.T) { +func TestMatchesLabels(t *testing.T) { tests := []struct { name string labelSelector ackv1alpha1.LabelSelector @@ -1155,9 +1155,9 @@ func TestMatchesResourceLabels(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := matchesResourceLabels(tt.labelSelector, tt.resourceLabels) + got := matchesLabels(tt.labelSelector, tt.resourceLabels) if got != tt.want { - t.Errorf("matchesResourceLabels() = %v, want %v", got, tt.want) + t.Errorf("matchesLabels() = %v, want %v", got, tt.want) } }) } From 0c89f8a36d3b2f27baae778afa9122680eee3a75 Mon Sep 17 00:00:00 2001 From: Zarko Aleksic Date: Wed, 3 Dec 2025 09:06:55 -0600 Subject: [PATCH 3/3] fix unit-test expected value --- pkg/runtime/iamroleselector/matcher_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/runtime/iamroleselector/matcher_test.go b/pkg/runtime/iamroleselector/matcher_test.go index 97c912a..446e2ce 100644 --- a/pkg/runtime/iamroleselector/matcher_test.go +++ b/pkg/runtime/iamroleselector/matcher_test.go @@ -663,7 +663,7 @@ func TestValidateSelector(t *testing.T) { }, }, wantErr: true, - errMsg: "invalid resource label selector: label key cannot be empty", + errMsg: "invalid label selector: label key cannot be empty", }, { name: "valid resource label selector",