Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions apis/core/v1alpha1/iam_role_selector.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down
1 change: 1 addition & 0 deletions apis/core/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions config/crd/bases/services.k8s.aws_iamroleselectors.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion config/crd/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 4 additions & 1 deletion pkg/runtime/iamroleselector/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -161,6 +162,7 @@ func (c *Cache) GetMatchingSelectors(
Namespace: namespace,
NamespaceLabels: namespaceLabels,
GVK: gvk,
ResourceLabels: resourceLabels,
}

c.RLock()
Expand Down Expand Up @@ -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() {
Expand All @@ -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)
}
54 changes: 48 additions & 6 deletions pkg/runtime/iamroleselector/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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,
},
}
Expand Down Expand Up @@ -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,
Expand All @@ -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
}

Expand Down Expand Up @@ -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) {}
Expand Down
31 changes: 27 additions & 4 deletions pkg/runtime/iamroleselector/matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,28 @@ 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
// Rules: AND between different field types, OR within arrays
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) &&
matchesLabels(selector.Spec.ResourceLabelSelector, ctx.ResourceLabels)
}

// 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
}

// Check if all specified labels match (AND logic within label selector)
selector := labels.SelectorFromSet(labelSelector.MatchLabels)
return selector.Matches(labels.Set(resourceLabels))
Comment on lines +45 to +52
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: same here..we can rename this to matchesLabels?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Please see the latest commit.

}

// matchesNamespace checks if the namespace selector matches the given namespace and its labels
Expand Down Expand Up @@ -122,6 +136,11 @@ func validateSelector(selector *ackv1alpha1.IAMRoleSelector) error {
return fmt.Errorf("invalid resource type selector: %w", err)
}

// Validate label selector
if err := validateLabelSelector(selector.Spec.ResourceLabelSelector); err != nil {
return fmt.Errorf("invalid label selector: %w", err)
}

return nil
}

Expand All @@ -139,15 +158,19 @@ func validateNamespaceSelector(nsSelector ackv1alpha1.NamespaceSelector) error {
}

// Validate label selector
if len(nsSelector.LabelSelector.MatchLabels) > 0 {
for key := range nsSelector.LabelSelector.MatchLabels {
return validateLabelSelector(nsSelector.LabelSelector)
}

// 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 == "" {
return fmt.Errorf("label key cannot be empty")
}
// Kubernetes label values can be empty, so we don't validate value
}
}

return nil
}

Expand Down
Loading