Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

in-memory resource client: support expression selector #551

Merged
merged 8 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
4 changes: 4 additions & 0 deletions changelog/v0.34.3/mem-client-expression-selector.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
changelog:
- type: NON_USER_FACING
description: >-
Add support for ExpressionSelector in memory ResourceClient.
21 changes: 4 additions & 17 deletions pkg/api/v1/clients/kube/resource_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@ import (
"strings"
"time"

"github.com/solo-io/solo-kit/pkg/utils/specutils"

"github.com/solo-io/solo-kit/pkg/utils/kubeutils"
"k8s.io/apimachinery/pkg/types"

"github.com/solo-io/go-utils/stringutils"
"github.com/solo-io/solo-kit/pkg/api/shared"
"github.com/solo-io/solo-kit/pkg/api/v1/clients"
Expand All @@ -20,12 +15,14 @@ import (
v1 "github.com/solo-io/solo-kit/pkg/api/v1/clients/kube/crd/solo.io/v1"
"github.com/solo-io/solo-kit/pkg/api/v1/resources"
"github.com/solo-io/solo-kit/pkg/errors"
"github.com/solo-io/solo-kit/pkg/utils/kubeutils"
"github.com/solo-io/solo-kit/pkg/utils/specutils"
"go.opencensus.io/stats"
"go.opencensus.io/stats/view"
"go.opencensus.io/tag"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
)

var (
Expand Down Expand Up @@ -298,7 +295,7 @@ func (rc *ResourceClient) List(namespace string, opts clients.ListOpts) (resourc
return nil, err
}

labelSelector, err := rc.getLabelSelector(opts)
labelSelector, err := kubeutils.ToLabelSelector(opts)
if err != nil {
return nil, errors.Wrapf(err, "parsing label selector")
}
Expand Down Expand Up @@ -458,16 +455,6 @@ func (rc *ResourceClient) Watch(namespace string, opts clients.WatchOpts) (<-cha
return resourcesChan, errs, nil
}

func (rc *ResourceClient) getLabelSelector(listOpts clients.ListOpts) (labels.Selector, error) {
// https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#set-based-requirement
if listOpts.ExpressionSelector != "" {
return labels.Parse(listOpts.ExpressionSelector)
}

// https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#equality-based-requirement
return labels.SelectorFromSet(listOpts.Selector), nil
}

// Checks whether the group version kind of the given resource matches that of the client's underlying CRD:
func (rc *ResourceClient) matchesClientGVK(resource v1.Resource) bool {
return resource.GroupVersionKind().String() == rc.crd.GroupVersionKind().String()
Expand Down
14 changes: 11 additions & 3 deletions pkg/api/v1/clients/memory/resource_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/solo-io/solo-kit/pkg/api/v1/clients"
"github.com/solo-io/solo-kit/pkg/api/v1/resources"
"github.com/solo-io/solo-kit/pkg/errors"
"github.com/solo-io/solo-kit/pkg/utils/kubeutils"
"k8s.io/apimachinery/pkg/labels"
)

Expand Down Expand Up @@ -197,9 +198,15 @@ func (rc *ResourceClient) Delete(namespace, name string, opts clients.DeleteOpts
func (rc *ResourceClient) List(namespace string, opts clients.ListOpts) (resources.ResourceList, error) {
opts = opts.WithDefaults()
cachedResources := rc.cache.List(rc.Prefix(namespace))

labelSelector, err := kubeutils.ToLabelSelector(opts)
if err != nil {
return nil, errors.Wrapf(err, "parsing label selector")
}

var resourceList resources.ResourceList
for _, resource := range cachedResources {
if labels.SelectorFromSet(opts.Selector).Matches(labels.Set(resource.GetMetadata().Labels)) {
if labelSelector.Matches(labels.Set(resource.GetMetadata().Labels)) {
clone := resources.Clone(resource)
resourceList = append(resourceList, clone)
}
Expand All @@ -220,8 +227,9 @@ func (rc *ResourceClient) Watch(namespace string, opts clients.WatchOpts) (<-cha
errs := make(chan error)
updateResourceList := func() {
list, err := rc.List(namespace, clients.ListOpts{
Ctx: opts.Ctx,
Selector: opts.Selector,
Ctx: opts.Ctx,
Selector: opts.Selector,
ExpressionSelector: opts.ExpressionSelector,
})
if err != nil {
errs <- err
Expand Down
172 changes: 172 additions & 0 deletions pkg/api/v1/clients/memory/resource_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,178 @@ var _ = Describe("Base", func() {
Expect(listret[0]).NotTo(BeIdenticalTo(listret2[0]))
})

Context("listing resources", func() {
var (
obj1 *v1.MockResource
obj2 *v1.MockResource
obj3 *v1.MockResource
obj4 *v1.MockResource
obj5 *v1.MockResource
)

BeforeEach(func() {
obj1 = &v1.MockResource{
Metadata: &core.Metadata{
Name: "name1",
Namespace: "ns1",
Labels: map[string]string{
"key": "val1",
},
},
}
obj2 = &v1.MockResource{
Metadata: &core.Metadata{
Name: "name2",
Namespace: "ns2",
Labels: map[string]string{
"key": "val2",
},
},
}
obj3 = &v1.MockResource{
Metadata: &core.Metadata{
Name: "name3",
Namespace: "ns1",
Labels: map[string]string{
"key": "val2",
},
},
}
obj4 = &v1.MockResource{
Metadata: &core.Metadata{
Name: "name4",
Namespace: "ns2",
Labels: map[string]string{
"key": "val3",
},
},
}
obj5 = &v1.MockResource{
Metadata: &core.Metadata{
Name: "name5",
Namespace: "ns1",
Labels: map[string]string{
"key": "val3",
},
},
}

_, err := client.Write(obj1, clients.WriteOpts{})
Expect(err).NotTo(HaveOccurred())
_, err = client.Write(obj2, clients.WriteOpts{})
Expect(err).NotTo(HaveOccurred())
_, err = client.Write(obj3, clients.WriteOpts{})
Expect(err).NotTo(HaveOccurred())
_, err = client.Write(obj4, clients.WriteOpts{})
Expect(err).NotTo(HaveOccurred())
_, err = client.Write(obj5, clients.WriteOpts{})
Expect(err).NotTo(HaveOccurred())
})

It("lists all resources when empty namespace is provided", func() {
resources, err := client.List("", clients.ListOpts{})
Expect(err).NotTo(HaveOccurred())

// resources are sorted by namespace, then name
expectedResourceNames := []string{
"name1", "name3", "name5", // ns1
"name2", "name4", // ns2
}
Expect(resources).To(HaveLen(len(expectedResourceNames)))
for i, r := range resources {
Expect(r.GetMetadata().GetName()).To(Equal(expectedResourceNames[i]))
}
})

It("lists resources in a given namespace", func() {
resources, err := client.List("ns2", clients.ListOpts{})
Expect(err).NotTo(HaveOccurred())

expectedResourceNames := []string{
"name2", "name4",
}
Expect(resources).To(HaveLen(len(expectedResourceNames)))
for i, r := range resources {
Expect(r.GetMetadata().GetName()).To(Equal(expectedResourceNames[i]))
}
})

It("returns empty list if namespace is invalid", func() {
resources, err := client.List("invalid-namespace", clients.ListOpts{})
Expect(err).NotTo(HaveOccurred())
Expect(resources).To(HaveLen(0))
})

It("returns resources matching the given selector, across all namespaces", func() {
resources, err := client.List("", clients.ListOpts{
Selector: map[string]string{
"key": "val2",
},
})
Expect(err).NotTo(HaveOccurred())

// resources are sorted by namespace, then name
expectedResourceNames := []string{
"name3", "name2",
}
Expect(resources).To(HaveLen(len(expectedResourceNames)))
for i, r := range resources {
Expect(r.GetMetadata().GetName()).To(Equal(expectedResourceNames[i]))
}
})

It("returns resources matching the given selector, in given namespace", func() {
resources, err := client.List("ns2", clients.ListOpts{
Selector: map[string]string{
"key": "val2",
},
})
Expect(err).NotTo(HaveOccurred())

Expect(resources).To(HaveLen(1))
Expect(resources[0].GetMetadata().GetName()).To(Equal("name2"))
})

It("returns resources matching the given expression selector, across all namespaces", func() {
resources, err := client.List("", clients.ListOpts{
ExpressionSelector: "key in (val1,val3)",
})
Expect(err).NotTo(HaveOccurred())

// resources are sorted by namespace, then name
expectedResourceNames := []string{
"name1", "name5", "name4",
}
Expect(resources).To(HaveLen(len(expectedResourceNames)))
for i, r := range resources {
Expect(r.GetMetadata().GetName()).To(Equal(expectedResourceNames[i]))
}
})

It("returns resources matching the given expression selector, in given namespace", func() {
resources, err := client.List("ns2", clients.ListOpts{
ExpressionSelector: "key in (val1,val3)",
})
Expect(err).NotTo(HaveOccurred())

Expect(resources).To(HaveLen(1))
Expect(resources[0].GetMetadata().GetName()).To(Equal("name4"))
})

It("when both selector and expression selector are provided, uses expression selector", func() {
resources, err := client.List("ns2", clients.ListOpts{
Selector: map[string]string{
"ignored": "selector",
},
ExpressionSelector: "key in (val1,val3)",
})
Expect(err).NotTo(HaveOccurred())

Expect(resources).To(HaveLen(1))
Expect(resources[0].GetMetadata().GetName()).To(Equal("name4"))
})
})

Context("Benchmarks", func() {
Measure("it should perform list efficiently", func(b Benchmarker) {
const numobjs = 10000
Expand Down
13 changes: 13 additions & 0 deletions pkg/utils/kubeutils/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,22 @@ import (
"github.com/solo-io/solo-kit/pkg/api/v1/clients"
"github.com/solo-io/solo-kit/pkg/api/v1/resources/core"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
kubetypes "k8s.io/apimachinery/pkg/types"
)

// ToLabelSelector converts the selector specified by the ListOpts into an apimachinery label selector.
// If both ExpressionSelector and Selector are specified in the opts, only ExpressionSelector is used.
func ToLabelSelector(listOpts clients.ListOpts) (labels.Selector, error) {
// https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#set-based-requirement
if listOpts.ExpressionSelector != "" {
return labels.Parse(listOpts.ExpressionSelector)
}

// https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#equality-based-requirement
return labels.SelectorFromSet(listOpts.Selector), nil
}

func FromKubeMeta(meta metav1.ObjectMeta, copyOwnerReferences bool) *core.Metadata {

var metaData = &core.Metadata{}
Expand Down