Skip to content

Commit

Permalink
feat: Prefix search on query selector
Browse files Browse the repository at this point in the history
  • Loading branch information
adityathebe authored and moshloop committed Jul 31, 2024
1 parent 8bc13c5 commit b68d001
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 12 deletions.
22 changes: 22 additions & 0 deletions query/commons.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/url"
"strings"

"gorm.io/gorm"
"gorm.io/gorm/clause"
)

Expand Down Expand Up @@ -73,3 +74,24 @@ func parseAndBuildFilteringQuery(query, field string, decodeURL bool) ([]clause.

return clauses, nil
}

func OrQueries(db *gorm.DB, queries ...*gorm.DB) *gorm.DB {
if len(queries) == 0 {
return db
}

if len(queries) == 1 {
return db.Where(queries[0])
}

union := queries[0]
for i, q := range queries {
if i == 0 {
continue
}

union = union.Or(q)
}

return db.Where(union)
}
34 changes: 25 additions & 9 deletions query/resource_selector.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func SearchResources(ctx context.Context, req SearchResourcesRequest) (*SearchRe
return &output, nil
}

func SetResourceSelectorClause(ctx context.Context, resourceSelector types.ResourceSelector, query *gorm.DB, table string, allowedColumnsAsFields []string) error {
func SetResourceSelectorClause(ctx context.Context, resourceSelector types.ResourceSelector, query *gorm.DB, table string, allowedColumnsAsFields []string) (*gorm.DB, error) {
if !resourceSelector.IncludeDeleted {
query = query.Where("deleted_at IS NULL")
}
Expand Down Expand Up @@ -149,7 +149,7 @@ func SetResourceSelectorClause(ctx context.Context, resourceSelector types.Resou
case "components":
query = query.Where("topology_id = ?", resourceSelector.Scope)
default:
return api.Errorf(api.EINVALID, "scope is not supported for %s", table)
return nil, api.Errorf(api.EINVALID, "scope is not supported for %s", table)
}
}

Expand All @@ -162,18 +162,18 @@ func SetResourceSelectorClause(ctx context.Context, resourceSelector types.Resou
} else { // assume it's an agent name
agent, err := FindCachedAgent(ctx, resourceSelector.Agent)
if err != nil {
return err
return nil, err
}
query = query.Where("agent_id = ?", agent.ID)
}

if len(resourceSelector.TagSelector) > 0 {
if table != "config_items" {
return api.Errorf(api.EINVALID, "tag selector is only supported for config_items")
return nil, api.Errorf(api.EINVALID, "tag selector is only supported for config_items")
} else {
parsedTagSelector, err := labels.Parse(resourceSelector.TagSelector)
if err != nil {
return api.Errorf(api.EINVALID, fmt.Sprintf("failed to parse tag selector: %v", err))
return nil, api.Errorf(api.EINVALID, fmt.Sprintf("failed to parse tag selector: %v", err))
}
requirements, _ := parsedTagSelector.Requirements()
for _, r := range requirements {
Expand All @@ -185,7 +185,7 @@ func SetResourceSelectorClause(ctx context.Context, resourceSelector types.Resou
if len(resourceSelector.LabelSelector) > 0 {
parsedLabelSelector, err := labels.Parse(resourceSelector.LabelSelector)
if err != nil {
return api.Errorf(api.EINVALID, fmt.Sprintf("failed to parse label selector: %v", err))
return nil, api.Errorf(api.EINVALID, fmt.Sprintf("failed to parse label selector: %v", err))
}
requirements, _ := parsedLabelSelector.Requirements()
for _, r := range requirements {
Expand All @@ -196,7 +196,7 @@ func SetResourceSelectorClause(ctx context.Context, resourceSelector types.Resou
if len(resourceSelector.FieldSelector) > 0 {
parsedFieldSelector, err := labels.Parse(resourceSelector.FieldSelector)
if err != nil {
return api.Errorf(api.EINVALID, fmt.Sprintf("failed to parse field selector: %v", err))
return nil, api.Errorf(api.EINVALID, fmt.Sprintf("failed to parse field selector: %v", err))
}

requirements, _ := parsedFieldSelector.Requirements()
Expand All @@ -209,7 +209,22 @@ func SetResourceSelectorClause(ctx context.Context, resourceSelector types.Resou
}
}

return nil
if resourceSelector.Search != "" {
var prefixQueries []*gorm.DB
if resourceSelector.Name == "" {
prefixQueries = append(prefixQueries, ctx.DB().Where("name ILIKE ?", resourceSelector.Search+"%"))
}
if resourceSelector.TagSelector == "" && table == "config_items" {
prefixQueries = append(prefixQueries, ctx.DB().Where("EXISTS (SELECT 1 FROM jsonb_each_text(tags) WHERE value ILIKE ?)", resourceSelector.Search+"%"))
}
if resourceSelector.LabelSelector == "" {
prefixQueries = append(prefixQueries, ctx.DB().Where("EXISTS (SELECT 1 FROM jsonb_each_text(labels) WHERE value ILIKE ?)", resourceSelector.Search+"%"))
}

query = OrQueries(query, prefixQueries...)
}

return query, nil
}

// queryResourceSelector runs the given resourceSelector and returns the resource ids
Expand All @@ -231,7 +246,8 @@ func queryResourceSelector(ctx context.Context, resourceSelector types.ResourceS
}

query := ctx.DB().Select("id").Table(table)
if err := SetResourceSelectorClause(ctx, resourceSelector, query, table, allowedColumnsAsFields); err != nil {
query, err := SetResourceSelectorClause(ctx, resourceSelector, query, table, allowedColumnsAsFields)
if err != nil {
return nil, err
}

Expand Down
99 changes: 99 additions & 0 deletions tests/query_resource_selector_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package tests

import (
"fmt"

"github.com/google/uuid"
ginkgo "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"github.com/flanksource/duty/models"
"github.com/flanksource/duty/query"
"github.com/flanksource/duty/tests/fixtures/dummy"
"github.com/flanksource/duty/types"
Expand Down Expand Up @@ -176,4 +180,99 @@ var _ = ginkgo.Describe("SearchResourceSelectors", func() {
Expect(ids).To(ConsistOf([]string{dummy.EKSCluster.ID.String(), dummy.KubernetesCluster.ID.String()}))
})
})

ginkgo.Context("search query", ginkgo.Ordered, func() {
testData := []struct {
description string
query query.SearchResourcesRequest
Configs []uuid.UUID
Components []uuid.UUID
Checks []uuid.UUID
}{
{
description: "name prefix | components",
query: query.SearchResourcesRequest{
Components: []types.ResourceSelector{{Search: "logistics-", Types: []string{"Application"}}},
},
Components: []uuid.UUID{dummy.LogisticsAPI.ID, dummy.LogisticsUI.ID, dummy.LogisticsWorker.ID},
},
{
description: "name prefix | checks",
query: query.SearchResourcesRequest{
Checks: []types.ResourceSelector{{Search: "logistics-", Types: []string{"http"}}},
},
Checks: []uuid.UUID{dummy.LogisticsAPIHomeHTTPCheck.ID, dummy.LogisticsAPIHealthHTTPCheck.ID},
},
{
description: "name prefix | configs",
query: query.SearchResourcesRequest{
Configs: []types.ResourceSelector{{Search: "node"}},
},
Configs: []uuid.UUID{dummy.KubernetesNodeA.ID, dummy.KubernetesNodeB.ID},
},
{
description: "name prefix with label selector",
query: query.SearchResourcesRequest{
Configs: []types.ResourceSelector{{Search: "node", LabelSelector: "region=us-west-2"}},
},
Configs: []uuid.UUID{dummy.KubernetesNodeB.ID},
},
{
description: "tag prefix - eg #1",
query: query.SearchResourcesRequest{
Configs: []types.ResourceSelector{{FieldSelector: fmt.Sprintf("config_class=%s", models.ConfigClassCluster), Search: "aws"}},
},
Configs: []uuid.UUID{dummy.EKSCluster.ID},
},
{
description: "tag prefix - eg #2",
query: query.SearchResourcesRequest{
Configs: []types.ResourceSelector{{FieldSelector: fmt.Sprintf("config_class=%s", models.ConfigClassCluster), Search: "demo"}},
},
Configs: []uuid.UUID{dummy.KubernetesCluster.ID},
},
{
description: "label prefix - eg #1",
query: query.SearchResourcesRequest{
Configs: []types.ResourceSelector{{FieldSelector: fmt.Sprintf("config_class=%s", models.ConfigClassCluster), Search: "prod"}},
},
Configs: []uuid.UUID{dummy.EKSCluster.ID},
},
{
description: "label prefix - eg #2",
query: query.SearchResourcesRequest{
Configs: []types.ResourceSelector{{FieldSelector: fmt.Sprintf("config_class=%s", models.ConfigClassCluster), Search: "develop"}},
},
Configs: []uuid.UUID{dummy.KubernetesCluster.ID},
},
}

for _, test := range testData {
ginkgo.It(test.description, func() {
items, err := query.SearchResources(DefaultContext, test.query)
Expect(err).To(BeNil())

{
// configs
Expect(len(items.Configs)).To(Equal(len(test.Configs)))
ids := lo.Map(items.Configs, func(item query.SelectedResource, _ int) uuid.UUID { return uuid.MustParse(item.ID) })
Expect(ids).To(ConsistOf(test.Configs))
}

{
// components
Expect(len(items.Components)).To(Equal(len(test.Components)))
ids := lo.Map(items.Components, func(item query.SelectedResource, _ int) uuid.UUID { return uuid.MustParse(item.ID) })
Expect(ids).To(ConsistOf(test.Components))
}

{
// checks
Expect(len(items.Checks)).To(Equal(len(test.Checks)))
ids := lo.Map(items.Checks, func(item query.SelectedResource, _ int) uuid.UUID { return uuid.MustParse(item.ID) })
Expect(ids).To(ConsistOf(test.Checks))
}
})
}
})
})
13 changes: 11 additions & 2 deletions types/resource_selector.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ type ResourceSelector struct {
// 'max-age=X' (cache for X duration)
Cache string `yaml:"cache,omitempty" json:"cache,omitempty"`

IncludeDeleted bool `yaml:"-" json:"-"`
// Search query that applies to the resource name, tag & labels.
Search string `yaml:"search,omitempty" json:"search,omitempty"`

IncludeDeleted bool `yaml:"includeDeleted,omitempty" json:"includeDeleted,omitempty"`

ID string `yaml:"id,omitempty" json:"id,omitempty"`
Name string `yaml:"name,omitempty" json:"name,omitempty"`
Expand All @@ -47,7 +50,7 @@ type ResourceSelector struct {
}

func (c ResourceSelector) IsEmpty() bool {
return c.ID == "" && c.Name == "" && c.Namespace == "" && c.Agent == "" && c.Scope == "" &&
return c.ID == "" && c.Name == "" && c.Namespace == "" && c.Agent == "" && c.Scope == "" && c.Search == "" &&
len(c.Types) == 0 &&
len(c.Statuses) == 0 &&
len(c.TagSelector) == 0 &&
Expand All @@ -66,6 +69,11 @@ func (c ResourceSelector) Immutable() bool {
return false
}

if c.Search == "" {
// too broad to be cached indefinitely
return false
}

if c.Namespace == "" {
return false // still not specific enough
}
Expand All @@ -91,6 +99,7 @@ func (c ResourceSelector) Hash() string {
collections.SortedMap(collections.SelectorToMap(c.LabelSelector)),
collections.SortedMap(collections.SelectorToMap(c.FieldSelector)),
fmt.Sprint(c.IncludeDeleted),
c.Search,
}

return hash.Sha256Hex(strings.Join(items, "|"))
Expand Down
2 changes: 1 addition & 1 deletion types/resource_selector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestResourceSelector_Hash_Consistency(t *testing.T) {
LabelSelector: "app=example,env=production",
FieldSelector: "owner=admin,path=/,icon=example.png",
},
expectedHash: "b2eadc3ba5ed46dc207470a96b1cc88ab7129c5cbb906ecee666e8fdd93d8fed",
expectedHash: "f591f8377f280e4e8a29695d70ab237e2862c9d594f073cfb145a8c55f709a0e",
},
}

Expand Down

0 comments on commit b68d001

Please sign in to comment.