Skip to content
Draft
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
73 changes: 62 additions & 11 deletions query/resource_selector.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ import (
"github.com/flanksource/duty/types"
)

var resourceSelectorPEGCache = cache.New(time.Hour, 2*time.Hour)
var resourceSelectorLabelRequirementsCache = cache.New(time.Hour, 2*time.Hour)

type parsedResourceSelectorPEG struct {
queryField grammar.QueryField
flatFields []string
}

type parsedSelectorRequirements struct {
requirements []labels.Requirement
}

type SearchResourcesRequest struct {
// Limit the number of results returned per resource type
Limit int `json:"limit"`
Expand Down Expand Up @@ -235,21 +247,20 @@ func SetResourceSelectorClause(
}

if peg := resourceSelector.ToPeg(false); peg != "" {
qf, err := grammar.ParsePEG(peg)
parsedPEG, err := getParsedResourceSelectorPEG(peg)
if err != nil {
return nil, fmt.Errorf("error parsing grammar[%s]: %w", peg, err)
}

flatFields := grammar.FlatFields(qf)
searchSetAgent = slices.ContainsFunc(flatFields, func(s string) bool {
searchSetAgent = slices.ContainsFunc(parsedPEG.flatFields, func(s string) bool {
field := strings.ToLower(s)
if alias, ok := qm.Aliases[field]; ok {
field = alias
}
return field == "agent_id"
})

searchSetDeleted = slices.ContainsFunc(flatFields, func(s string) bool {
searchSetDeleted = slices.ContainsFunc(parsedPEG.flatFields, func(s string) bool {
field := strings.ToLower(s)
if alias, ok := qm.Aliases[field]; ok {
field = alias
Expand All @@ -258,7 +269,7 @@ func SetResourceSelectorClause(
})

var clauses []clause.Expression
query, clauses, err = qm.Apply(ctx, *qf, query)
query, clauses, err = qm.Apply(ctx, parsedPEG.queryField, query)
if err != nil {
return nil, fmt.Errorf("error applying query model: %w", err)
}
Expand Down Expand Up @@ -305,11 +316,10 @@ func SetResourceSelectorClause(
if !qm.HasTags {
return nil, api.Errorf(api.EINVALID, "tagSelector is not supported for table=%s", table)
} else {
parsedTagSelector, err := labels.Parse(resourceSelector.TagSelector)
requirements, err := getSelectorRequirements(resourceSelector.TagSelector)
if err != nil {
return nil, api.Errorf(api.EINVALID, "failed to parse tag selector: %v", err)
}
requirements, _ := parsedTagSelector.Requirements()
for _, r := range requirements {
query = jsonColumnRequirementsToSQLClause(query, "tags", r)
}
Expand All @@ -321,23 +331,21 @@ func SetResourceSelectorClause(
return nil, api.Errorf(api.EINVALID, "labelSelector is not supported for table=%s", table)
}

parsedLabelSelector, err := labels.Parse(resourceSelector.LabelSelector)
requirements, err := getSelectorRequirements(resourceSelector.LabelSelector)
if err != nil {
return nil, api.Errorf(api.EINVALID, "failed to parse label selector: %v", err)
}
requirements, _ := parsedLabelSelector.Requirements()
for _, r := range requirements {
query = jsonColumnRequirementsToSQLClause(query, "labels", r)
}
}

if len(resourceSelector.FieldSelector) > 0 {
parsedFieldSelector, err := labels.Parse(resourceSelector.FieldSelector)
requirements, err := getSelectorRequirements(resourceSelector.FieldSelector)
if err != nil {
return nil, api.Errorf(api.EINVALID, "failed to parse field selector: %v", err)
}

requirements, _ := parsedFieldSelector.Requirements()
for _, r := range requirements {
query = jsonColumnRequirementsToSQLClause(query, "properties", r)
}
Expand All @@ -353,6 +361,49 @@ func SetResourceSelectorClause(
return query, nil
}

func getParsedResourceSelectorPEG(peg string) (parsedResourceSelectorPEG, error) {
if value, ok := resourceSelectorPEGCache.Get(peg); ok {
if parsed, ok := value.(parsedResourceSelectorPEG); ok {
return parsed, nil
}
resourceSelectorPEGCache.Delete(peg)
}

qf, err := grammar.ParsePEG(peg)
if err != nil {
return parsedResourceSelectorPEG{}, err
}

parsed := parsedResourceSelectorPEG{
queryField: *qf,
flatFields: grammar.FlatFields(qf),
}
resourceSelectorPEGCache.SetDefault(peg, parsed)

return parsed, nil
}

func getSelectorRequirements(selector string) ([]labels.Requirement, error) {
if value, ok := resourceSelectorLabelRequirementsCache.Get(selector); ok {
if cached, ok := value.(parsedSelectorRequirements); ok {
return cached.requirements, nil
}
resourceSelectorLabelRequirementsCache.Delete(selector)
}

parsedSelector, err := labels.Parse(selector)
if err != nil {
return nil, err
}

requirements, _ := parsedSelector.Requirements()
resourceSelectorLabelRequirementsCache.SetDefault(selector, parsedSelectorRequirements{
requirements: requirements,
})

return requirements, nil
}

// queryResourceSelector runs the given resourceSelector and returns the resource ids
func queryResourceSelector[T any](
ctx context.Context,
Expand Down
62 changes: 62 additions & 0 deletions query/resource_selector_cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package query

import (
"testing"

"github.com/onsi/gomega"
)

func TestGetParsedResourceSelectorPEGUsesCache(t *testing.T) {
g := gomega.NewWithT(t)

resourceSelectorPEGCache.Flush()

first, err := getParsedResourceSelectorPEG(`name="coredns",type="Kubernetes::Pod"`)
g.Expect(err).ToNot(gomega.HaveOccurred())

second, err := getParsedResourceSelectorPEG(`name="coredns",type="Kubernetes::Pod"`)
g.Expect(err).ToNot(gomega.HaveOccurred())

g.Expect(second.queryField).To(gomega.Equal(first.queryField))
g.Expect(second.flatFields).To(gomega.Equal(first.flatFields))
g.Expect(resourceSelectorPEGCache.ItemCount()).To(gomega.Equal(1))
}

func TestGetParsedResourceSelectorPEGCachesByPEGValue(t *testing.T) {
g := gomega.NewWithT(t)

resourceSelectorPEGCache.Flush()

_, err := getParsedResourceSelectorPEG(`name="coredns"`)
g.Expect(err).ToNot(gomega.HaveOccurred())

_, err = getParsedResourceSelectorPEG(`name="metrics-server"`)
g.Expect(err).ToNot(gomega.HaveOccurred())

g.Expect(resourceSelectorPEGCache.ItemCount()).To(gomega.Equal(2))
}

func TestGetSelectorRequirementsUsesCache(t *testing.T) {
g := gomega.NewWithT(t)

resourceSelectorLabelRequirementsCache.Flush()

first, err := getSelectorRequirements("cluster=aws")
g.Expect(err).ToNot(gomega.HaveOccurred())

second, err := getSelectorRequirements("cluster=aws")
g.Expect(err).ToNot(gomega.HaveOccurred())

g.Expect(second).To(gomega.Equal(first))
g.Expect(resourceSelectorLabelRequirementsCache.ItemCount()).To(gomega.Equal(1))
}

func TestGetSelectorRequirementsReturnsErrorForInvalidSelector(t *testing.T) {
g := gomega.NewWithT(t)

resourceSelectorLabelRequirementsCache.Flush()

_, err := getSelectorRequirements("=aws")
g.Expect(err).To(gomega.HaveOccurred())
g.Expect(resourceSelectorLabelRequirementsCache.ItemCount()).To(gomega.Equal(0))
}
31 changes: 31 additions & 0 deletions types/resource_selector_canonical.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ const (
func (rs ResourceSelector) Canonical() ResourceSelector {
out := rs

if !resourceSelectorHasWildcard(out) {
return out
}

if isWildcardValue(out.ID) {
out.ID = ""
}
Expand Down Expand Up @@ -139,3 +143,30 @@ func filterWildcardCSV(value string) string {
func isWildcardValue(value string) bool {
return strings.TrimSpace(value) == wildcardValue
}

func resourceSelectorHasWildcard(rs ResourceSelector) bool {
if (rs.ID != "" && strings.Contains(rs.ID, wildcardValue)) ||
(rs.Namespace != "" && strings.Contains(rs.Namespace, wildcardValue)) ||
(rs.Scope != "" && strings.Contains(rs.Scope, wildcardValue)) ||
(rs.Agent != "" && strings.Contains(rs.Agent, wildcardValue)) ||
(rs.TagSelector != "" && strings.Contains(rs.TagSelector, wildcardValue)) ||
(rs.LabelSelector != "" && strings.Contains(rs.LabelSelector, wildcardValue)) ||
(rs.FieldSelector != "" && strings.Contains(rs.FieldSelector, wildcardValue)) ||
(rs.Health != "" && strings.Contains(string(rs.Health), wildcardValue)) {
return true
}

for _, item := range rs.Types {
if strings.Contains(item, wildcardValue) {
return true
}
}

for _, item := range rs.Statuses {
if strings.Contains(item, wildcardValue) {
return true
}
}

return false
}
25 changes: 25 additions & 0 deletions types/resource_selector_canonical_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package types

import (
"testing"

"github.com/onsi/gomega"
)

func TestResourceSelectorHasWildcard(t *testing.T) {
g := gomega.NewWithT(t)

g.Expect(resourceSelectorHasWildcard(ResourceSelector{
Name: "api-server",
TagSelector: "cluster=aws",
LabelSelector: "app=backend",
FieldSelector: "owner=platform",
Types: Items{"Kubernetes::Pod"},
Statuses: Items{"healthy"},
Health: "healthy",
})).To(gomega.BeFalse())

g.Expect(resourceSelectorHasWildcard(ResourceSelector{
TagSelector: "cluster=*",
})).To(gomega.BeTrue())
}
14 changes: 14 additions & 0 deletions types/resource_selector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,20 @@ var _ = Describe("Resource Selector", func() {
Expect(canonical.Statuses).To(Equal(types.Items{"healthy"}))
Expect(canonical.Health).To(Equal(types.MatchExpression("warning")))
})

It("should keep selectors without wildcard unchanged", func() {
rs := types.ResourceSelector{
Name: "api-server",
TagSelector: "cluster=aws",
LabelSelector: "app=backend",
FieldSelector: "owner=platform",
Types: []string{"Kubernetes::Pod"},
Statuses: []string{"healthy"},
Health: "healthy",
}

Expect(rs.Canonical()).To(Equal(rs))
})
})
})
})
Loading