From ea2d67622bb1588a4e8d7e383cb47535a8135ab2 Mon Sep 17 00:00:00 2001 From: Frank Jogeleit Date: Thu, 28 Nov 2024 12:56:31 +0100 Subject: [PATCH] feat(api): Support LabelSelector in,exist and notexist operation (#607) Signed-off-by: Frank Jogeleit --- pkg/kubernetes/namespaces/client.go | 37 +++++++++++++++++++++--- pkg/kubernetes/namespaces/client_test.go | 32 ++++++++++++++++++-- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/pkg/kubernetes/namespaces/client.go b/pkg/kubernetes/namespaces/client.go index c07b9dca..cee0bd14 100644 --- a/pkg/kubernetes/namespaces/client.go +++ b/pkg/kubernetes/namespaces/client.go @@ -2,10 +2,14 @@ package namespaces import ( "context" + "strings" gocache "github.com/patrickmn/go-cache" + "go.uber.org/zap" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" v1 "k8s.io/client-go/kubernetes/typed/core/v1" "github.com/kyverno/policy-reporter/pkg/helper" @@ -22,13 +26,38 @@ type k8sClient struct { } func (c *k8sClient) List(ctx context.Context, selector map[string]string) ([]string, error) { - labelSelector := metav1.FormatLabelSelector(&metav1.LabelSelector{MatchLabels: selector}) - if cached, ok := c.cache.Get(labelSelector); ok { + s := labels.NewSelector() + for l, v := range selector { + var err error + var req *labels.Requirement + + if strings.Contains(v, ",") { + req, err = labels.NewRequirement(l, selection.In, helper.Map(strings.Split(v, ","), func(val string) string { + return strings.TrimSpace(val) + })) + } else if v == "*" { + req, err = labels.NewRequirement(l, selection.Exists, nil) + } else if v == "!*" { + req, err = labels.NewRequirement(l, selection.DoesNotExist, nil) + } else { + req, err = labels.NewRequirement(l, selection.Equals, []string{v}) + } + if err != nil { + zap.L().Error("failed to create selector requirement", zap.Error(err), zap.String("label", l), zap.String("value", v)) + continue + } + + s = s.Add(*req) + } + + zap.L().Debug("created label selector for namespace resolution", zap.String("selector", s.String())) + + if cached, ok := c.cache.Get(s.String()); ok { return cached.([]string), nil } list, err := kubernetes.Retry(func() ([]string, error) { - namespaces, err := c.client.List(ctx, metav1.ListOptions{LabelSelector: labelSelector}) + namespaces, err := c.client.List(ctx, metav1.ListOptions{LabelSelector: s.String()}) if err != nil { return nil, err } @@ -41,7 +70,7 @@ func (c *k8sClient) List(ctx context.Context, selector map[string]string) ([]str return nil, err } - c.cache.Set(labelSelector, list, 0) + c.cache.Set(s.String(), list, 0) return list, nil } diff --git a/pkg/kubernetes/namespaces/client_test.go b/pkg/kubernetes/namespaces/client_test.go index 310c2db7..6f74212c 100644 --- a/pkg/kubernetes/namespaces/client_test.go +++ b/pkg/kubernetes/namespaces/client_test.go @@ -21,8 +21,9 @@ func newFakeClient() v1.NamespaceInterface { ObjectMeta: metav1.ObjectMeta{ Name: "default", Labels: map[string]string{ - "team": "team-a", - "name": "default", + "team": "team-a", + "name": "default", + "exist": "yes", }, }, }, @@ -47,6 +48,33 @@ func (s *nsErrorClient) List(ctx context.Context, opts metav1.ListOptions) (*cor } func TestClient(t *testing.T) { + t.Run("read from api with list", func(t *testing.T) { + client := namespaces.NewClient(newFakeClient(), gocache.New(gocache.DefaultExpiration, gocache.DefaultExpiration)) + + list, err := client.List(context.Background(), map[string]string{"name": "default,user"}) + + assert.Nil(t, err) + assert.Equal(t, 2, len(list)) + }) + t.Run("read from api with exist check", func(t *testing.T) { + client := namespaces.NewClient(newFakeClient(), gocache.New(gocache.DefaultExpiration, gocache.DefaultExpiration)) + + list, err := client.List(context.Background(), map[string]string{"exist": "*"}) + + assert.Nil(t, err) + assert.Equal(t, 1, len(list)) + assert.Equal(t, list[0], "default") + }) + t.Run("read from api with not exist check", func(t *testing.T) { + client := namespaces.NewClient(newFakeClient(), gocache.New(gocache.DefaultExpiration, gocache.DefaultExpiration)) + + list, err := client.List(context.Background(), map[string]string{"exist": "!*"}) + + assert.Nil(t, err) + assert.Equal(t, 1, len(list)) + assert.Equal(t, list[0], "user") + }) + t.Run("read from api", func(t *testing.T) { client := namespaces.NewClient(newFakeClient(), gocache.New(gocache.DefaultExpiration, gocache.DefaultExpiration))