diff --git a/models/checks.go b/models/checks.go index 7bcda987..d31aacc9 100644 --- a/models/checks.go +++ b/models/checks.go @@ -147,7 +147,7 @@ func (c Check) GetLabelsMatcher() labels.Labels { } func (c Check) GetFieldsMatcher() fields.Fields { - return noopMatcher{} + return genericFieldMatcher{c.AsMap()} } type checkLabelsProvider struct { diff --git a/models/common.go b/models/common.go index a74ba03e..626c75eb 100644 --- a/models/common.go +++ b/models/common.go @@ -2,7 +2,6 @@ package models import ( "encoding/json" - "fmt" "github.com/flanksource/commons/logger" "github.com/google/uuid" @@ -149,7 +148,14 @@ type genericFieldMatcher struct { } func (c genericFieldMatcher) Get(key string) string { - return fmt.Sprintf("%v", c.Fields[key]) + val := c.Fields[key] + switch v := val.(type) { + case string: + return v + default: + marshalled, _ := json.Marshal(v) + return string(marshalled) + } } func (c genericFieldMatcher) Has(key string) bool { diff --git a/models/components.go b/models/components.go index c061eb8c..22e17ea4 100644 --- a/models/components.go +++ b/models/components.go @@ -22,14 +22,6 @@ import ( "k8s.io/apimachinery/pkg/labels" ) -var AllowedColumnFieldsInComponents = []string{ - "owner", - "topology_type", - "topology_id", - "parent_id", - "type", // Deprecated. Use resource_selector.types instead -} - // Ensure interface compliance var ( _ types.ResourceSelectable = Component{} @@ -340,7 +332,7 @@ func (c Component) GetLabelsMatcher() labels.Labels { } func (c Component) GetFieldsMatcher() fields.Fields { - return componentFieldsProvider{c} + return genericFieldMatcher{c.AsMap()} } type componentLabelsProvider struct { @@ -356,33 +348,6 @@ func (c componentLabelsProvider) Has(key string) bool { return ok } -type componentFieldsProvider struct { - Component -} - -func (c componentFieldsProvider) Get(key string) string { - if lo.Contains(AllowedColumnFieldsInComponents, key) { - return fmt.Sprintf("%v", c.AsMap()[key]) - } - - v := c.Properties.Find(key) - if v == nil { - return "" - } - - return fmt.Sprintf("%v", v.GetValue()) -} - -func (c componentFieldsProvider) Has(key string) bool { - if lo.Contains(AllowedColumnFieldsInComponents, key) { - _, ok := c.AsMap()[key] - return ok - } - - v := c.Properties.Find(key) - return v != nil -} - var ComponentID = func(c Component) string { return c.ID.String() } diff --git a/models/config.go b/models/config.go index 3d2f4661..d40ff31e 100644 --- a/models/config.go +++ b/models/config.go @@ -68,8 +68,6 @@ const ( AnalysisTypeTechDebt AnalysisType = "technical_debt" ) -var AllowedColumnFieldsInConfigs = []string{"config_class", "external_id"} - type RelatedConfigDirection string const ( @@ -277,34 +275,7 @@ func (c ConfigItem) GetLabelsMatcher() labels.Labels { } func (c ConfigItem) GetFieldsMatcher() fields.Fields { - return configFields{c} -} - -type configFields struct { - ConfigItem -} - -func (c configFields) Get(key string) string { - if lo.Contains(AllowedColumnFieldsInConfigs, key) { - return fmt.Sprintf("%v", c.AsMap()[key]) - } - - v := c.Properties.Find(key) - if v == nil { - return "" - } - - return fmt.Sprintf("%v", v.GetValue()) -} - -func (c configFields) Has(key string) bool { - if lo.Contains(AllowedColumnFieldsInConfigs, key) { - _, ok := c.AsMap()[key] - return ok - } - - v := c.Properties.Find(key) - return v != nil + return genericFieldMatcher{c.AsMap()} } type configLabels struct { diff --git a/query/commons.go b/query/commons.go index b9b23759..ad0ddebb 100644 --- a/query/commons.go +++ b/query/commons.go @@ -1,94 +1,19 @@ package query import ( - "fmt" - "net/url" - "strings" "time" "github.com/flanksource/duty/context" - "github.com/flanksource/duty/types" + "github.com/flanksource/duty/query/grammar" "github.com/patrickmn/go-cache" - "github.com/samber/lo" "gorm.io/gorm" "gorm.io/gorm/clause" ) var LocalFilter = "deleted_at is NULL AND agent_id = '00000000-0000-0000-0000-000000000000' OR agent_id IS NULL" -type expressions struct { - In []interface{} - Prefix []string - Suffix []string -} - -type Expressions []clause.Expression - var distinctTagsCache = cache.New(time.Minute*10, time.Hour) -// postgrestValues returns ["a", "b", "c"] as `"a","b","c"` -func postgrestValues(val []any) string { - return strings.Join(lo.Map(val, func(s any, i int) string { - return fmt.Sprintf(`"%s"`, s) - }), ",") -} - -func (query FilteringQuery) AppendPostgrest(key string, - queryParam url.Values, -) { - if len(query.In) > 0 { - queryParam.Add(key, fmt.Sprintf("in.(%s)", postgrestValues(query.In))) - } - - if len(query.Not.In) > 0 { - queryParam.Add(key, fmt.Sprintf("not.in.(%s)", postgrestValues(query.Not.In))) - } - - for _, p := range query.Prefix { - queryParam.Add(key, fmt.Sprintf("like.%s*", p)) - } - - for _, p := range query.Suffix { - queryParam.Add(key, fmt.Sprintf("like.*%s", p)) - } -} - -func (e expressions) ToExpression(field string) []clause.Expression { - var clauses []clause.Expression - if len(e.In) > 0 { - clauses = append(clauses, clause.IN{Column: clause.Column{Name: field}, Values: e.In}) - } - - for _, p := range e.Prefix { - clauses = append(clauses, clause.Like{ - Column: clause.Column{Name: field}, - Value: p + "%", - }) - } - - for _, s := range e.Suffix { - clauses = append(clauses, clause.Like{ - Column: clause.Column{Name: field}, - Value: "%" + s, - }) - } - - return clauses -} - -// ParseFilteringQuery parses a filtering query string. -// It returns four slices: 'in', 'notIN', 'prefix', and 'suffix'. -type FilteringQuery struct { - expressions - Not expressions -} - -func (fq *FilteringQuery) ToExpression(field string) []clause.Expression { - exprs := fq.expressions.ToExpression(field) - not := clause.Not(fq.Not.ToExpression(field)...) - return append(exprs, not) -} - // ParseFilteringQuery parses a filtering query string. // It returns four slices: 'in', 'notIN', 'prefix', and 'suffix'. func ParseFilteringQuery(query string, decodeURL bool) (in []interface{}, notIN []interface{}, prefix, suffix []string, err error) { @@ -96,10 +21,11 @@ func ParseFilteringQuery(query string, decodeURL bool) (in []interface{}, notIN return } - q, err := types.ParseFilteringQueryV2(query, decodeURL) + q, err := grammar.ParseFilteringQueryV2(query, decodeURL) if err != nil { return nil, nil, nil, nil, err } + return q.In, q.Not.In, q.Prefix, q.Suffix, nil } diff --git a/query/config.go b/query/config.go index 414313bc..6ac62d66 100644 --- a/query/config.go +++ b/query/config.go @@ -13,6 +13,7 @@ import ( "github.com/flanksource/duty/api" "github.com/flanksource/duty/context" "github.com/flanksource/duty/models" + "github.com/flanksource/duty/query/grammar" "github.com/flanksource/duty/types" "github.com/google/uuid" "gorm.io/gorm" @@ -228,7 +229,7 @@ func (t *ConfigSummaryRequest) filterClause(q *gorm.DB) *gorm.DB { var excludeClause *gorm.DB for k, v := range t.Filter { - query, _ := types.ParseFilteringQueryV2(v, true) + query, _ := grammar.ParseFilteringQueryV2(v, true) if len(query.Not.In) > 0 { if excludeClause == nil { diff --git a/types/filters.go b/query/grammar/filters.go similarity index 99% rename from types/filters.go rename to query/grammar/filters.go index 329f0c8d..d20689d6 100644 --- a/types/filters.go +++ b/query/grammar/filters.go @@ -1,4 +1,4 @@ -package types +package grammar import ( "fmt" diff --git a/query/grammar/grammar.go b/query/grammar/grammar.go index 735049da..dedcd4d3 100644 --- a/query/grammar/grammar.go +++ b/query/grammar/grammar.go @@ -15,8 +15,6 @@ import ( "sync" "unicode" "unicode/utf8" - - "github.com/flanksource/duty/types" ) var g = &grammar{ @@ -252,34 +250,34 @@ var g = &grammar{ }, }, &actionExpr{ - pos: position{line: 37, col: 5, offset: 590}, + pos: position{line: 37, col: 5, offset: 584}, run: (*parser).callonFieldQuery25, expr: &seqExpr{ - pos: position{line: 37, col: 5, offset: 590}, + pos: position{line: 37, col: 5, offset: 584}, exprs: []any{ &ruleRefExpr{ - pos: position{line: 37, col: 5, offset: 590}, + pos: position{line: 37, col: 5, offset: 584}, name: "_", }, &labeledExpr{ - pos: position{line: 37, col: 7, offset: 592}, + pos: position{line: 37, col: 7, offset: 586}, label: "n", expr: &choiceExpr{ - pos: position{line: 37, col: 10, offset: 595}, + pos: position{line: 37, col: 10, offset: 589}, alternatives: []any{ &ruleRefExpr{ - pos: position{line: 37, col: 10, offset: 595}, + pos: position{line: 37, col: 10, offset: 589}, name: "Word", }, &ruleRefExpr{ - pos: position{line: 37, col: 17, offset: 602}, + pos: position{line: 37, col: 17, offset: 596}, name: "Identifier", }, }, }, }, &ruleRefExpr{ - pos: position{line: 37, col: 29, offset: 614}, + pos: position{line: 37, col: 29, offset: 608}, name: "_", }, }, @@ -290,42 +288,42 @@ var g = &grammar{ }, { name: "Field", - pos: position{line: 46, col: 1, offset: 797}, + pos: position{line: 46, col: 1, offset: 785}, expr: &actionExpr{ - pos: position{line: 47, col: 5, offset: 807}, + pos: position{line: 47, col: 5, offset: 795}, run: (*parser).callonField1, expr: &seqExpr{ - pos: position{line: 47, col: 5, offset: 807}, + pos: position{line: 47, col: 5, offset: 795}, exprs: []any{ &labeledExpr{ - pos: position{line: 47, col: 5, offset: 807}, + pos: position{line: 47, col: 5, offset: 795}, label: "src", expr: &ruleRefExpr{ - pos: position{line: 47, col: 9, offset: 811}, + pos: position{line: 47, col: 9, offset: 799}, name: "Source", }, }, &ruleRefExpr{ - pos: position{line: 47, col: 16, offset: 818}, + pos: position{line: 47, col: 16, offset: 806}, name: "_", }, &labeledExpr{ - pos: position{line: 47, col: 18, offset: 820}, + pos: position{line: 47, col: 18, offset: 808}, label: "op", expr: &ruleRefExpr{ - pos: position{line: 47, col: 21, offset: 823}, + pos: position{line: 47, col: 21, offset: 811}, name: "Operator", }, }, &ruleRefExpr{ - pos: position{line: 47, col: 30, offset: 832}, + pos: position{line: 47, col: 30, offset: 820}, name: "_", }, &labeledExpr{ - pos: position{line: 47, col: 32, offset: 834}, + pos: position{line: 47, col: 32, offset: 822}, label: "value", expr: &ruleRefExpr{ - pos: position{line: 47, col: 38, offset: 840}, + pos: position{line: 47, col: 38, offset: 828}, name: "Value", }, }, @@ -335,37 +333,37 @@ var g = &grammar{ }, { name: "Source", - pos: position{line: 51, col: 1, offset: 952}, + pos: position{line: 51, col: 1, offset: 928}, expr: &actionExpr{ - pos: position{line: 52, col: 5, offset: 963}, + pos: position{line: 52, col: 5, offset: 939}, run: (*parser).callonSource1, expr: &seqExpr{ - pos: position{line: 52, col: 5, offset: 963}, + pos: position{line: 52, col: 5, offset: 939}, exprs: []any{ &labeledExpr{ - pos: position{line: 52, col: 5, offset: 963}, + pos: position{line: 52, col: 5, offset: 939}, label: "name", expr: &ruleRefExpr{ - pos: position{line: 52, col: 10, offset: 968}, + pos: position{line: 52, col: 10, offset: 944}, name: "Identifier", }, }, &labeledExpr{ - pos: position{line: 52, col: 21, offset: 979}, + pos: position{line: 52, col: 21, offset: 955}, label: "path", expr: &zeroOrMoreExpr{ - pos: position{line: 52, col: 26, offset: 984}, + pos: position{line: 52, col: 26, offset: 960}, expr: &seqExpr{ - pos: position{line: 52, col: 27, offset: 985}, + pos: position{line: 52, col: 27, offset: 961}, exprs: []any{ &litMatcher{ - pos: position{line: 52, col: 27, offset: 985}, + pos: position{line: 52, col: 27, offset: 961}, val: ".", ignoreCase: false, want: "\".\"", }, &ruleRefExpr{ - pos: position{line: 52, col: 31, offset: 989}, + pos: position{line: 52, col: 31, offset: 965}, name: "Identifier", }, }, @@ -378,9 +376,9 @@ var g = &grammar{ }, { name: "Not", - pos: position{line: 57, col: 1, offset: 1045}, + pos: position{line: 57, col: 1, offset: 1021}, expr: &litMatcher{ - pos: position{line: 57, col: 7, offset: 1051}, + pos: position{line: 57, col: 7, offset: 1027}, val: "-", ignoreCase: false, want: "\"-\"", @@ -388,54 +386,54 @@ var g = &grammar{ }, { name: "Operator", - pos: position{line: 59, col: 1, offset: 1056}, + pos: position{line: 59, col: 1, offset: 1032}, expr: &actionExpr{ - pos: position{line: 60, col: 5, offset: 1069}, + pos: position{line: 60, col: 5, offset: 1045}, run: (*parser).callonOperator1, expr: &labeledExpr{ - pos: position{line: 60, col: 5, offset: 1069}, + pos: position{line: 60, col: 5, offset: 1045}, label: "op", expr: &choiceExpr{ - pos: position{line: 61, col: 6, offset: 1079}, + pos: position{line: 61, col: 6, offset: 1055}, alternatives: []any{ &litMatcher{ - pos: position{line: 61, col: 6, offset: 1079}, + pos: position{line: 61, col: 6, offset: 1055}, val: "<=", ignoreCase: false, want: "\"<=\"", }, &litMatcher{ - pos: position{line: 62, col: 7, offset: 1090}, + pos: position{line: 62, col: 7, offset: 1066}, val: ">=", ignoreCase: false, want: "\">=\"", }, &litMatcher{ - pos: position{line: 63, col: 7, offset: 1101}, + pos: position{line: 63, col: 7, offset: 1077}, val: "=", ignoreCase: false, want: "\"=\"", }, &litMatcher{ - pos: position{line: 64, col: 7, offset: 1111}, + pos: position{line: 64, col: 7, offset: 1087}, val: ":", ignoreCase: false, want: "\":\"", }, &litMatcher{ - pos: position{line: 65, col: 7, offset: 1121}, + pos: position{line: 65, col: 7, offset: 1097}, val: "!=", ignoreCase: false, want: "\"!=\"", }, &litMatcher{ - pos: position{line: 66, col: 7, offset: 1132}, + pos: position{line: 66, col: 7, offset: 1108}, val: "<", ignoreCase: false, want: "\"<\"", }, &litMatcher{ - pos: position{line: 67, col: 7, offset: 1142}, + pos: position{line: 67, col: 7, offset: 1118}, val: ">", ignoreCase: false, want: "\">\"", @@ -447,46 +445,46 @@ var g = &grammar{ }, { name: "Value", - pos: position{line: 72, col: 1, offset: 1200}, + pos: position{line: 72, col: 1, offset: 1170}, expr: &actionExpr{ - pos: position{line: 73, col: 5, offset: 1210}, + pos: position{line: 73, col: 5, offset: 1180}, run: (*parser).callonValue1, expr: &labeledExpr{ - pos: position{line: 73, col: 5, offset: 1210}, + pos: position{line: 73, col: 5, offset: 1180}, label: "val", expr: &choiceExpr{ - pos: position{line: 74, col: 7, offset: 1222}, + pos: position{line: 74, col: 7, offset: 1192}, alternatives: []any{ &ruleRefExpr{ - pos: position{line: 74, col: 7, offset: 1222}, + pos: position{line: 74, col: 7, offset: 1192}, name: "DateTime", }, &ruleRefExpr{ - pos: position{line: 75, col: 7, offset: 1237}, + pos: position{line: 75, col: 7, offset: 1207}, name: "ISODate", }, &ruleRefExpr{ - pos: position{line: 76, col: 7, offset: 1251}, + pos: position{line: 76, col: 7, offset: 1221}, name: "Time", }, &ruleRefExpr{ - pos: position{line: 77, col: 7, offset: 1262}, + pos: position{line: 77, col: 7, offset: 1232}, name: "Measure", }, &ruleRefExpr{ - pos: position{line: 78, col: 7, offset: 1276}, + pos: position{line: 78, col: 7, offset: 1246}, name: "Float", }, &ruleRefExpr{ - pos: position{line: 79, col: 7, offset: 1288}, + pos: position{line: 79, col: 7, offset: 1258}, name: "Integer", }, &ruleRefExpr{ - pos: position{line: 80, col: 7, offset: 1302}, + pos: position{line: 80, col: 7, offset: 1272}, name: "Identifier", }, &ruleRefExpr{ - pos: position{line: 81, col: 7, offset: 1319}, + pos: position{line: 81, col: 7, offset: 1289}, name: "String", }, }, @@ -496,26 +494,26 @@ var g = &grammar{ }, { name: "String", - pos: position{line: 86, col: 1, offset: 1364}, + pos: position{line: 86, col: 1, offset: 1334}, expr: &actionExpr{ - pos: position{line: 87, col: 5, offset: 1375}, + pos: position{line: 87, col: 5, offset: 1345}, run: (*parser).callonString1, expr: &seqExpr{ - pos: position{line: 87, col: 5, offset: 1375}, + pos: position{line: 87, col: 5, offset: 1345}, exprs: []any{ &litMatcher{ - pos: position{line: 87, col: 5, offset: 1375}, + pos: position{line: 87, col: 5, offset: 1345}, val: "\"", ignoreCase: false, want: "\"\\\"\"", }, &labeledExpr{ - pos: position{line: 87, col: 9, offset: 1379}, + pos: position{line: 87, col: 9, offset: 1349}, label: "chars", expr: &zeroOrMoreExpr{ - pos: position{line: 87, col: 15, offset: 1385}, + pos: position{line: 87, col: 15, offset: 1355}, expr: &charClassMatcher{ - pos: position{line: 87, col: 15, offset: 1385}, + pos: position{line: 87, col: 15, offset: 1355}, val: "[^\"]", chars: []rune{'"'}, ignoreCase: false, @@ -524,7 +522,7 @@ var g = &grammar{ }, }, &litMatcher{ - pos: position{line: 87, col: 21, offset: 1391}, + pos: position{line: 87, col: 21, offset: 1361}, val: "\"", ignoreCase: false, want: "\"\\\"\"", @@ -535,76 +533,76 @@ var g = &grammar{ }, { name: "ISODate", - pos: position{line: 91, col: 1, offset: 1441}, + pos: position{line: 91, col: 1, offset: 1411}, expr: &actionExpr{ - pos: position{line: 92, col: 5, offset: 1453}, + pos: position{line: 92, col: 5, offset: 1423}, run: (*parser).callonISODate1, expr: &seqExpr{ - pos: position{line: 92, col: 5, offset: 1453}, + pos: position{line: 92, col: 5, offset: 1423}, exprs: []any{ &charClassMatcher{ - pos: position{line: 92, col: 5, offset: 1453}, + pos: position{line: 92, col: 5, offset: 1423}, val: "[0-9]", ranges: []rune{'0', '9'}, ignoreCase: false, inverted: false, }, &charClassMatcher{ - pos: position{line: 92, col: 10, offset: 1458}, + pos: position{line: 92, col: 10, offset: 1428}, val: "[0-9]", ranges: []rune{'0', '9'}, ignoreCase: false, inverted: false, }, &charClassMatcher{ - pos: position{line: 92, col: 15, offset: 1463}, + pos: position{line: 92, col: 15, offset: 1433}, val: "[0-9]", ranges: []rune{'0', '9'}, ignoreCase: false, inverted: false, }, &charClassMatcher{ - pos: position{line: 92, col: 20, offset: 1468}, + pos: position{line: 92, col: 20, offset: 1438}, val: "[0-9]", ranges: []rune{'0', '9'}, ignoreCase: false, inverted: false, }, &litMatcher{ - pos: position{line: 92, col: 26, offset: 1474}, + pos: position{line: 92, col: 26, offset: 1444}, val: "-", ignoreCase: false, want: "\"-\"", }, &charClassMatcher{ - pos: position{line: 92, col: 30, offset: 1478}, + pos: position{line: 92, col: 30, offset: 1448}, val: "[0-9]", ranges: []rune{'0', '9'}, ignoreCase: false, inverted: false, }, &charClassMatcher{ - pos: position{line: 92, col: 35, offset: 1483}, + pos: position{line: 92, col: 35, offset: 1453}, val: "[0-9]", ranges: []rune{'0', '9'}, ignoreCase: false, inverted: false, }, &litMatcher{ - pos: position{line: 92, col: 41, offset: 1489}, + pos: position{line: 92, col: 41, offset: 1459}, val: "-", ignoreCase: false, want: "\"-\"", }, &charClassMatcher{ - pos: position{line: 92, col: 45, offset: 1493}, + pos: position{line: 92, col: 45, offset: 1463}, val: "[0-9]", ranges: []rune{'0', '9'}, ignoreCase: false, inverted: false, }, &charClassMatcher{ - pos: position{line: 92, col: 50, offset: 1498}, + pos: position{line: 92, col: 50, offset: 1468}, val: "[0-9]", ranges: []rune{'0', '9'}, ignoreCase: false, @@ -616,62 +614,62 @@ var g = &grammar{ }, { name: "Time", - pos: position{line: 96, col: 1, offset: 1544}, + pos: position{line: 96, col: 1, offset: 1514}, expr: &actionExpr{ - pos: position{line: 97, col: 5, offset: 1553}, + pos: position{line: 97, col: 5, offset: 1523}, run: (*parser).callonTime1, expr: &seqExpr{ - pos: position{line: 97, col: 5, offset: 1553}, + pos: position{line: 97, col: 5, offset: 1523}, exprs: []any{ &charClassMatcher{ - pos: position{line: 97, col: 5, offset: 1553}, + pos: position{line: 97, col: 5, offset: 1523}, val: "[0-2]", ranges: []rune{'0', '2'}, ignoreCase: false, inverted: false, }, &charClassMatcher{ - pos: position{line: 97, col: 10, offset: 1558}, + pos: position{line: 97, col: 10, offset: 1528}, val: "[0-9]", ranges: []rune{'0', '9'}, ignoreCase: false, inverted: false, }, &litMatcher{ - pos: position{line: 97, col: 16, offset: 1564}, + pos: position{line: 97, col: 16, offset: 1534}, val: ":", ignoreCase: false, want: "\":\"", }, &charClassMatcher{ - pos: position{line: 97, col: 20, offset: 1568}, + pos: position{line: 97, col: 20, offset: 1538}, val: "[0-5]", ranges: []rune{'0', '5'}, ignoreCase: false, inverted: false, }, &charClassMatcher{ - pos: position{line: 97, col: 25, offset: 1573}, + pos: position{line: 97, col: 25, offset: 1543}, val: "[0-9]", ranges: []rune{'0', '9'}, ignoreCase: false, inverted: false, }, &litMatcher{ - pos: position{line: 97, col: 31, offset: 1579}, + pos: position{line: 97, col: 31, offset: 1549}, val: ":", ignoreCase: false, want: "\":\"", }, &charClassMatcher{ - pos: position{line: 97, col: 35, offset: 1583}, + pos: position{line: 97, col: 35, offset: 1553}, val: "[0-5]", ranges: []rune{'0', '5'}, ignoreCase: false, inverted: false, }, &charClassMatcher{ - pos: position{line: 97, col: 40, offset: 1588}, + pos: position{line: 97, col: 40, offset: 1558}, val: "[0-9]", ranges: []rune{'0', '9'}, ignoreCase: false, @@ -683,23 +681,23 @@ var g = &grammar{ }, { name: "DateTime", - pos: position{line: 101, col: 1, offset: 1634}, + pos: position{line: 101, col: 1, offset: 1604}, expr: &actionExpr{ - pos: position{line: 102, col: 5, offset: 1647}, + pos: position{line: 102, col: 5, offset: 1617}, run: (*parser).callonDateTime1, expr: &seqExpr{ - pos: position{line: 102, col: 5, offset: 1647}, + pos: position{line: 102, col: 5, offset: 1617}, exprs: []any{ &ruleRefExpr{ - pos: position{line: 102, col: 5, offset: 1647}, + pos: position{line: 102, col: 5, offset: 1617}, name: "ISODate", }, &ruleRefExpr{ - pos: position{line: 102, col: 13, offset: 1655}, + pos: position{line: 102, col: 13, offset: 1625}, name: "_", }, &ruleRefExpr{ - pos: position{line: 102, col: 15, offset: 1657}, + pos: position{line: 102, col: 15, offset: 1627}, name: "Time", }, }, @@ -708,24 +706,24 @@ var g = &grammar{ }, { name: "Word", - pos: position{line: 106, col: 1, offset: 1702}, + pos: position{line: 106, col: 1, offset: 1672}, expr: &choiceExpr{ - pos: position{line: 107, col: 5, offset: 1711}, + pos: position{line: 107, col: 5, offset: 1681}, alternatives: []any{ &ruleRefExpr{ - pos: position{line: 107, col: 5, offset: 1711}, + pos: position{line: 107, col: 5, offset: 1681}, name: "String", }, &actionExpr{ - pos: position{line: 107, col: 14, offset: 1720}, + pos: position{line: 107, col: 14, offset: 1690}, run: (*parser).callonWord3, expr: &seqExpr{ - pos: position{line: 107, col: 14, offset: 1720}, + pos: position{line: 107, col: 14, offset: 1690}, exprs: []any{ &zeroOrOneExpr{ - pos: position{line: 107, col: 14, offset: 1720}, + pos: position{line: 107, col: 14, offset: 1690}, expr: &charClassMatcher{ - pos: position{line: 107, col: 14, offset: 1720}, + pos: position{line: 107, col: 14, offset: 1690}, val: "[-]", chars: []rune{'-'}, ignoreCase: false, @@ -733,9 +731,9 @@ var g = &grammar{ }, }, &oneOrMoreExpr{ - pos: position{line: 107, col: 19, offset: 1725}, + pos: position{line: 107, col: 19, offset: 1695}, expr: &charClassMatcher{ - pos: position{line: 107, col: 19, offset: 1725}, + pos: position{line: 107, col: 19, offset: 1695}, val: "[a-zA-Z0-9_*-]", chars: []rune{'_', '*', '-'}, ranges: []rune{'a', 'z', 'A', 'Z', '0', '9'}, @@ -751,17 +749,17 @@ var g = &grammar{ }, { name: "Integer", - pos: position{line: 111, col: 1, offset: 1781}, + pos: position{line: 111, col: 1, offset: 1751}, expr: &actionExpr{ - pos: position{line: 112, col: 5, offset: 1793}, + pos: position{line: 112, col: 5, offset: 1763}, run: (*parser).callonInteger1, expr: &seqExpr{ - pos: position{line: 112, col: 5, offset: 1793}, + pos: position{line: 112, col: 5, offset: 1763}, exprs: []any{ &zeroOrOneExpr{ - pos: position{line: 112, col: 5, offset: 1793}, + pos: position{line: 112, col: 5, offset: 1763}, expr: &charClassMatcher{ - pos: position{line: 112, col: 5, offset: 1793}, + pos: position{line: 112, col: 5, offset: 1763}, val: "[+-]", chars: []rune{'+', '-'}, ignoreCase: false, @@ -769,9 +767,9 @@ var g = &grammar{ }, }, &oneOrMoreExpr{ - pos: position{line: 112, col: 11, offset: 1799}, + pos: position{line: 112, col: 11, offset: 1769}, expr: &charClassMatcher{ - pos: position{line: 112, col: 11, offset: 1799}, + pos: position{line: 112, col: 11, offset: 1769}, val: "[0-9]", ranges: []rune{'0', '9'}, ignoreCase: false, @@ -784,35 +782,35 @@ var g = &grammar{ }, { name: "Measure", - pos: position{line: 116, col: 1, offset: 1865}, + pos: position{line: 116, col: 1, offset: 1835}, expr: &actionExpr{ - pos: position{line: 117, col: 5, offset: 1877}, + pos: position{line: 117, col: 5, offset: 1847}, run: (*parser).callonMeasure1, expr: &seqExpr{ - pos: position{line: 117, col: 5, offset: 1877}, + pos: position{line: 117, col: 5, offset: 1847}, exprs: []any{ &labeledExpr{ - pos: position{line: 117, col: 5, offset: 1877}, + pos: position{line: 117, col: 5, offset: 1847}, label: "number", expr: &choiceExpr{ - pos: position{line: 117, col: 13, offset: 1885}, + pos: position{line: 117, col: 13, offset: 1855}, alternatives: []any{ &ruleRefExpr{ - pos: position{line: 117, col: 13, offset: 1885}, + pos: position{line: 117, col: 13, offset: 1855}, name: "Integer", }, &ruleRefExpr{ - pos: position{line: 117, col: 23, offset: 1895}, + pos: position{line: 117, col: 23, offset: 1865}, name: "Float", }, }, }, }, &labeledExpr{ - pos: position{line: 117, col: 30, offset: 1902}, + pos: position{line: 117, col: 30, offset: 1872}, label: "unit", expr: &ruleRefExpr{ - pos: position{line: 117, col: 35, offset: 1907}, + pos: position{line: 117, col: 35, offset: 1877}, name: "Identifier", }, }, @@ -822,17 +820,17 @@ var g = &grammar{ }, { name: "Float", - pos: position{line: 121, col: 1, offset: 1962}, + pos: position{line: 121, col: 1, offset: 1932}, expr: &actionExpr{ - pos: position{line: 122, col: 5, offset: 1972}, + pos: position{line: 122, col: 5, offset: 1942}, run: (*parser).callonFloat1, expr: &seqExpr{ - pos: position{line: 122, col: 5, offset: 1972}, + pos: position{line: 122, col: 5, offset: 1942}, exprs: []any{ &zeroOrOneExpr{ - pos: position{line: 122, col: 5, offset: 1972}, + pos: position{line: 122, col: 5, offset: 1942}, expr: &charClassMatcher{ - pos: position{line: 122, col: 5, offset: 1972}, + pos: position{line: 122, col: 5, offset: 1942}, val: "[+-]", chars: []rune{'+', '-'}, ignoreCase: false, @@ -840,12 +838,12 @@ var g = &grammar{ }, }, &seqExpr{ - pos: position{line: 122, col: 12, offset: 1979}, + pos: position{line: 122, col: 12, offset: 1949}, exprs: []any{ &zeroOrMoreExpr{ - pos: position{line: 122, col: 12, offset: 1979}, + pos: position{line: 122, col: 12, offset: 1949}, expr: &charClassMatcher{ - pos: position{line: 122, col: 12, offset: 1979}, + pos: position{line: 122, col: 12, offset: 1949}, val: "[0-9]", ranges: []rune{'0', '9'}, ignoreCase: false, @@ -853,15 +851,15 @@ var g = &grammar{ }, }, &litMatcher{ - pos: position{line: 122, col: 19, offset: 1986}, + pos: position{line: 122, col: 19, offset: 1956}, val: ".", ignoreCase: false, want: "\".\"", }, &oneOrMoreExpr{ - pos: position{line: 122, col: 23, offset: 1990}, + pos: position{line: 122, col: 23, offset: 1960}, expr: &charClassMatcher{ - pos: position{line: 122, col: 23, offset: 1990}, + pos: position{line: 122, col: 23, offset: 1960}, val: "[0-9]", ranges: []rune{'0', '9'}, ignoreCase: false, @@ -876,14 +874,14 @@ var g = &grammar{ }, { name: "Identifier", - pos: position{line: 126, col: 1, offset: 2060}, + pos: position{line: 126, col: 1, offset: 2030}, expr: &actionExpr{ - pos: position{line: 127, col: 5, offset: 2075}, + pos: position{line: 127, col: 5, offset: 2045}, run: (*parser).callonIdentifier1, expr: &oneOrMoreExpr{ - pos: position{line: 127, col: 5, offset: 2075}, + pos: position{line: 127, col: 5, offset: 2045}, expr: &charClassMatcher{ - pos: position{line: 127, col: 5, offset: 2075}, + pos: position{line: 127, col: 5, offset: 2045}, val: "[a-zA-Z0-9_*-:\\\\[\\]]", chars: []rune{'_', '\\', '[', ']'}, ranges: []rune{'a', 'z', 'A', 'Z', '0', '9', '*', ':'}, @@ -895,11 +893,11 @@ var g = &grammar{ }, { name: "_", - pos: position{line: 131, col: 1, offset: 2137}, + pos: position{line: 131, col: 1, offset: 2107}, expr: &zeroOrMoreExpr{ - pos: position{line: 132, col: 5, offset: 2143}, + pos: position{line: 132, col: 5, offset: 2113}, expr: &charClassMatcher{ - pos: position{line: 132, col: 5, offset: 2143}, + pos: position{line: 132, col: 5, offset: 2113}, val: "[ \\t]", chars: []rune{' ', '\t'}, ignoreCase: false, @@ -909,11 +907,11 @@ var g = &grammar{ }, { name: "EOF", - pos: position{line: 134, col: 1, offset: 2151}, + pos: position{line: 134, col: 1, offset: 2121}, expr: ¬Expr{ - pos: position{line: 135, col: 5, offset: 2159}, + pos: position{line: 135, col: 5, offset: 2129}, expr: &anyMatcher{ - line: 135, col: 6, offset: 2160, + line: 135, col: 6, offset: 2130, }, }, }, @@ -980,7 +978,7 @@ func (c *current) onFieldQuery18(n any) (any, error) { n = nStr + "*" } - return &types.QueryField{Field: "name", Op: "not", Value: n}, nil + return &QueryField{Field: "name", Op: "not", Value: n}, nil } @@ -995,7 +993,7 @@ func (c *current) onFieldQuery25(n any) (any, error) { n = nStr + "*" } - return &types.QueryField{Field: "name", Op: "=", Value: n}, nil + return &QueryField{Field: "name", Op: "=", Value: n}, nil } @@ -1006,7 +1004,7 @@ func (p *parser) callonFieldQuery25() (any, error) { } func (c *current) onField1(src, op, value any) (any, error) { - return &types.QueryField{Field: src.(string), Op: op.(types.QueryOperator), Value: value}, nil + return &QueryField{Field: src.(string), Op: op.(QueryOperator), Value: value}, nil } @@ -1028,7 +1026,7 @@ func (p *parser) callonSource1() (any, error) { } func (c *current) onOperator1(op any) (any, error) { - return types.QueryOperator(c.text), nil + return QueryOperator(c.text), nil } diff --git a/query/grammar/grammar.peg b/query/grammar/grammar.peg index 78fffa0e..6aaf3af1 100644 --- a/query/grammar/grammar.peg +++ b/query/grammar/grammar.peg @@ -31,7 +31,7 @@ FieldQuery n = nStr + "*" } - return &types.QueryField{Field: "name", Op: "not", Value: n}, nil + return &QueryField{Field: "name", Op: "not", Value: n}, nil } / _ n:(Word / Identifier) _ { @@ -39,13 +39,13 @@ FieldQuery n = nStr + "*" } - return &types.QueryField{Field: "name", Op: "=", Value: n}, nil + return &QueryField{Field: "name", Op: "=", Value: n}, nil } Field = src:Source _ op:Operator _ value:Value { - return &types.QueryField{Field:src.(string), Op: op.(types.QueryOperator), Value:value}, nil + return &QueryField{Field:src.(string), Op: op.(QueryOperator), Value:value}, nil } Source @@ -66,7 +66,7 @@ Operator / "<" / ">" ){ - return types.QueryOperator(c.text), nil + return QueryOperator(c.text), nil } Value diff --git a/query/grammar/grammar_parser.go b/query/grammar/grammar_parser.go index c9232a7b..b62e7bc3 100644 --- a/query/grammar/grammar_parser.go +++ b/query/grammar/grammar_parser.go @@ -6,7 +6,6 @@ import ( "strings" "github.com/flanksource/commons/logger" - "github.com/flanksource/duty/types" ) type Source struct { @@ -34,53 +33,53 @@ func makeSource(name interface{}, path interface{}) (string, error) { } func makeFQFromQuery(a interface{}) (interface{}, error) { - return a.(*types.QueryField), nil + return a.(*QueryField), nil } //nolint:unused -func makeCatchAll(f interface{}) (*types.QueryField, error) { +func makeCatchAll(f interface{}) (*QueryField, error) { logger.Warnf("ctach all %v (%T)", f, f) switch v := f.(type) { case string: - return &types.QueryField{Op: "rest", Value: v}, nil + return &QueryField{Op: "rest", Value: v}, nil case []byte: - return &types.QueryField{Op: "rest", Value: string(v)}, nil + return &QueryField{Op: "rest", Value: string(v)}, nil case []interface{}: rest := "" for _, i := range v { rest += fmt.Sprintf("%s", i) } - return &types.QueryField{Op: "rest", Value: rest}, nil + return &QueryField{Op: "rest", Value: rest}, nil } - return &types.QueryField{Op: "rest", Value: f}, nil + return &QueryField{Op: "rest", Value: f}, nil } -func makeFQFromField(f interface{}) (*types.QueryField, error) { - return f.(*types.QueryField), nil +func makeFQFromField(f interface{}) (*QueryField, error) { + return f.(*QueryField), nil } //nolint:unused -func makeQuery(a, b interface{}) (*types.QueryField, error) { - q := &types.QueryField{ +func makeQuery(a, b interface{}) (*QueryField, error) { + q := &QueryField{ Op: "or", } switch v := a.(type) { - case *types.QueryField: + case *QueryField: q.Fields = append(q.Fields, v) default: logger.Warnf("Unknown type for query.a: %v = %T", a, a) } switch v := b.(type) { - case *types.QueryField: + case *QueryField: q.Fields = append(q.Fields, v) case []interface{}: for _, i := range v { switch v2 := i.(type) { - case *types.QueryField: + case *QueryField: q.Fields = append(q.Fields, v2) default: @@ -94,11 +93,11 @@ func makeQuery(a, b interface{}) (*types.QueryField, error) { return q, nil } -func makeAndQuery(a any, b any) (*types.QueryField, error) { - q := &types.QueryField{Op: "and"} +func makeAndQuery(a any, b any) (*QueryField, error) { + q := &QueryField{Op: "and"} switch v := a.(type) { - case *types.QueryField: + case *QueryField: q.Fields = append(q.Fields, v) default: @@ -106,12 +105,12 @@ func makeAndQuery(a any, b any) (*types.QueryField, error) { } switch v := b.(type) { - case *types.QueryField: + case *QueryField: q.Fields = append(q.Fields, v) case []interface{}: for _, i := range v { switch v2 := i.(type) { - case *types.QueryField: + case *QueryField: q.Fields = append(q.Fields, v2) default: logger.Warnf("Unknown array item: %v (%T)", i, i) @@ -144,7 +143,7 @@ func stringFromChars(chars interface{}) string { return str } -func FlatFields(qf *types.QueryField) []string { +func FlatFields(qf *QueryField) []string { var fields []string if qf.Field != "" { fields = append(fields, qf.Field) @@ -155,7 +154,7 @@ func FlatFields(qf *types.QueryField) []string { return fields } -func ParsePEG(peg string) (*types.QueryField, error) { +func ParsePEG(peg string) (*QueryField, error) { stats := Stats{} v, err := Parse("", []byte(peg), Statistics(&stats, "no match")) @@ -163,9 +162,9 @@ func ParsePEG(peg string) (*types.QueryField, error) { return nil, fmt.Errorf("error parsing peg: %w", err) } - rv, ok := v.(*types.QueryField) + rv, ok := v.(*QueryField) if !ok { - return nil, fmt.Errorf("return type not types.QueryField") + return nil, fmt.Errorf("return type not QueryField") } return rv, nil diff --git a/query/grammar/query.go b/query/grammar/query.go new file mode 100644 index 00000000..00e56de3 --- /dev/null +++ b/query/grammar/query.go @@ -0,0 +1,74 @@ +package grammar + +import ( + "fmt" + + "gorm.io/gorm/clause" + "k8s.io/apimachinery/pkg/selection" +) + +type QueryOperator string + +const ( + Eq QueryOperator = "=" + Neq QueryOperator = "!=" + + Gt QueryOperator = ">" + Lt QueryOperator = "<" + In QueryOperator = "in" + NotIn QueryOperator = "notin" + Exists QueryOperator = "exists" + NotExists QueryOperator = "!" +) + +func (op QueryOperator) ToSelectionOperator() selection.Operator { + switch op { + case Eq: + return selection.Equals + case Neq: + return selection.NotEquals + case In: + return selection.In + case NotIn: + return selection.NotIn + case Exists: + return selection.Exists + case NotExists: + return selection.DoesNotExist + default: + return selection.Equals + } +} + +type QueryField struct { + Field string `json:"field,omitempty"` + Value interface{} `json:"value,omitempty"` + Op QueryOperator `json:"op,omitempty"` + Not bool `json:"not,omitempty"` + Fields []*QueryField `json:"fields,omitempty"` +} + +func (q QueryField) ToClauses() ([]clause.Expression, error) { + val := fmt.Sprint(q.Value) + + filters, err := ParseFilteringQueryV2(val, false) + if err != nil { + return nil, err + } + + var clauses []clause.Expression + switch q.Op { + case Eq: + clauses = append(clauses, filters.ToExpression(q.Field)...) + case Neq: + clauses = append(clauses, clause.Not(filters.ToExpression(q.Field)...)) + case Lt: + clauses = append(clauses, clause.Lt{Column: q.Field, Value: q.Value}) + case Gt: + clauses = append(clauses, clause.Gt{Column: q.Field, Value: q.Value}) + default: + return nil, fmt.Errorf("invalid operator: %s", q.Op) + } + + return clauses, nil +} diff --git a/query/models.go b/query/models.go index 8ad2b07f..ee7a290c 100644 --- a/query/models.go +++ b/query/models.go @@ -8,7 +8,7 @@ import ( "github.com/flanksource/duty/context" "github.com/flanksource/duty/models" - "github.com/flanksource/duty/types" + "github.com/flanksource/duty/query/grammar" "github.com/google/uuid" "github.com/pkg/errors" "github.com/timberio/go-datemath" @@ -36,9 +36,9 @@ var AgentMapper = func(ctx context.Context, id string) (any, error) { return nil, fmt.Errorf("invalid agent: %s", id) } -var JSONPathMapper = func(ctx context.Context, tx *gorm.DB, column string, op types.QueryOperator, path string, val string) *gorm.DB { - if !slices.Contains([]types.QueryOperator{types.Eq, types.Neq}, op) { - op = types.Eq +var JSONPathMapper = func(ctx context.Context, tx *gorm.DB, column string, op grammar.QueryOperator, path string, val string) *gorm.DB { + if !slices.Contains([]grammar.QueryOperator{grammar.Eq, grammar.Neq}, op) { + op = grammar.Eq } values := strings.Split(val, ",") for _, v := range values { @@ -223,7 +223,7 @@ func GetModelFromTable(table string) (QueryModel, error) { // as we modify the tx directly for them var ignoreFieldsForClauses = []string{"sort", "offset", "limit", "labels", "config", "tags", "properties"} -func (qm QueryModel) Apply(ctx context.Context, q types.QueryField, tx *gorm.DB) (*gorm.DB, []clause.Expression, error) { +func (qm QueryModel) Apply(ctx context.Context, q grammar.QueryField, tx *gorm.DB) (*gorm.DB, []clause.Expression, error) { if tx == nil { tx = ctx.DB().Table(qm.Table) } diff --git a/query/resource_selector.go b/query/resource_selector.go index 80f6913b..021c9172 100644 --- a/query/resource_selector.go +++ b/query/resource_selector.go @@ -140,45 +140,15 @@ func SetResourceSelectorClause( ) (*gorm.DB, error) { searchSetAgent := false - var searchConditions []string - if resourceSelector.Name != "" { - searchConditions = append(searchConditions, fmt.Sprintf("name = %q", resourceSelector.Name)) - } - - if resourceSelector.ID != "" { - searchConditions = append(searchConditions, fmt.Sprintf("id = %q", resourceSelector.ID)) - } - - if len(resourceSelector.Health) != 0 { - searchConditions = append(searchConditions, fmt.Sprintf("health = %q", resourceSelector.Health)) - } - - if resourceSelector.Namespace != "" { - searchConditions = append(searchConditions, fmt.Sprintf("namespace = %q", resourceSelector.Namespace)) - } - - if len(resourceSelector.Types) > 0 { - searchConditions = append(searchConditions, fmt.Sprintf("type = %q", strings.Join(resourceSelector.Types, ","))) - } - - if len(resourceSelector.Statuses) > 0 { - searchConditions = append(searchConditions, fmt.Sprintf("status = %q", strings.Join(resourceSelector.Statuses, ","))) - } - - if len(searchConditions) > 0 { - joined := strings.Join(searchConditions, " ") - resourceSelector.Search += fmt.Sprintf(" %s", joined) - } - qm, err := GetModelFromTable(table) if err != nil { return nil, fmt.Errorf("grammar parsing not implemented for table: %s", table) } - if resourceSelector.Search != "" { - qf, err := grammar.ParsePEG(resourceSelector.Search) + if peg := resourceSelector.ToPeg(false); peg != "" { + qf, err := grammar.ParsePEG(peg) if err != nil { - return nil, fmt.Errorf("error parsing grammar[%s]: %w", resourceSelector.Search, err) + return nil, fmt.Errorf("error parsing grammar[%s]: %w", peg, err) } flatFields := grammar.FlatFields(qf) diff --git a/tests/query_resource_selector_test.go b/tests/query_resource_selector_test.go index 8417e63b..ba81dd6c 100644 --- a/tests/query_resource_selector_test.go +++ b/tests/query_resource_selector_test.go @@ -220,6 +220,10 @@ var _ = ginkgo.Describe("SearchResourceSelectors", func() { }) for _, test := range testData { + // if test.description != "labels | IN Query" { + // continue + // } + ginkgo.It(test.description, func() { items, err := query.SearchResources(DefaultContext, test.query) Expect(err).To(BeNil()) diff --git a/types/resource_selector.go b/types/resource_selector.go index 1b06a326..d50db059 100644 --- a/types/resource_selector.go +++ b/types/resource_selector.go @@ -3,13 +3,16 @@ package types import ( "context" "database/sql/driver" + "encoding/json" "fmt" "net/url" + "strconv" "strings" "github.com/flanksource/commons/collections" "github.com/flanksource/commons/hash" "github.com/flanksource/commons/logger" + "github.com/flanksource/duty/query/grammar" "gorm.io/gorm" "gorm.io/gorm/clause" "gorm.io/gorm/schema" @@ -30,68 +33,6 @@ type Functions struct { ComponentConfigTraversal *ComponentConfigTraversalArgs `yaml:"component_config_traversal,omitempty" json:"component_config_traversal,omitempty"` } -type QueryOperator string - -const ( - Eq QueryOperator = "=" - Neq QueryOperator = "!=" - - Gt QueryOperator = ">" - Lt QueryOperator = "<" - In QueryOperator = "in" - NotIn QueryOperator = "notin" - Exists QueryOperator = "exists" - NotExists QueryOperator = "!" -) - -func (op QueryOperator) ToSelectionOperator() selection.Operator { - switch op { - case Eq: - return selection.Equals - case Neq: - return selection.NotEquals - case In: - return selection.In - case NotIn: - return selection.NotIn - case Exists: - return selection.Exists - case NotExists: - return selection.DoesNotExist - default: - return selection.Equals - } -} - -type QueryField struct { - Field string `json:"field,omitempty"` - Value interface{} `json:"value,omitempty"` - Op QueryOperator `json:"op,omitempty"` - Not bool `json:"not,omitempty"` - Fields []*QueryField `json:"fields,omitempty"` -} - -var CommonFields = map[string]bool{ - "id": true, - "type": true, -} - -func (f *QueryField) ToLabelSelector() (labels.Selector, error) { - selector := labels.NewSelector() - for _, field := range f.Fields { - if CommonFields[field.Field] { - continue - } - val := fmt.Sprintf("%s", field.Value) - req, err := labels.NewRequirement(field.Field, field.Op.ToSelectionOperator(), []string{val}) - if err != nil { - return nil, err - } - selector = selector.Add(*req) - } - return selector, nil -} - // +kubebuilder:object:generate=true type ResourceSelector struct { // Agent can be the agent id or the name of the agent. @@ -169,31 +110,6 @@ func ParseFilteringQuery(query string, decodeURL bool) (in []interface{}, notIN return } -func (q QueryField) ToClauses() ([]clause.Expression, error) { - val := fmt.Sprint(q.Value) - - filters, err := ParseFilteringQueryV2(val, false) - if err != nil { - return nil, err - } - - var clauses []clause.Expression - switch q.Op { - case Eq: - clauses = append(clauses, filters.ToExpression(q.Field)...) - case Neq: - clauses = append(clauses, clause.Not(filters.ToExpression(q.Field)...)) - case Lt: - clauses = append(clauses, clause.Lt{Column: q.Field, Value: q.Value}) - case Gt: - clauses = append(clauses, clause.Gt{Column: q.Field, Value: q.Value}) - default: - return nil, fmt.Errorf("invalid operator: %s", q.Op) - } - - return clauses, nil -} - func (c ResourceSelector) allEmptyButName() bool { return c.ID == "" && c.Namespace == "" && c.Agent == "" && c.Scope == "" && c.Search == "" && len(c.Types) == 0 && @@ -261,74 +177,172 @@ func (c ResourceSelector) Hash() string { return hash.Sha256Hex(strings.Join(items, "|")) } -func (rs ResourceSelector) Matches(s ResourceSelectable) bool { - if rs.IsEmpty() { - return false +func (rs ResourceSelector) ToPeg(convertSelectors bool) string { + var searchConditions []string + + if rs.ID != "" { + searchConditions = append(searchConditions, fmt.Sprintf("id = %q", rs.ID)) } - if rs.Wildcard() { - return true + + if rs.Name != "" { + searchConditions = append(searchConditions, fmt.Sprintf("name = %q", rs.Name)) } - if rs.ID != "" && rs.ID != s.GetID() { - return false + + if rs.Namespace != "" { + searchConditions = append(searchConditions, fmt.Sprintf("namespace = %q", rs.Namespace)) } - if rs.Name != "" && rs.Name != s.GetName() { - return false + + if len(rs.Health) != 0 { + searchConditions = append(searchConditions, fmt.Sprintf("health = %q", rs.Health)) } - if rs.Namespace != "" && rs.Namespace != s.GetNamespace() { - return false + + if len(rs.Types) > 0 { + searchConditions = append(searchConditions, fmt.Sprintf("type = %q", strings.Join(rs.Types, ","))) } - if len(rs.Types) > 0 && !rs.Types.Contains(s.GetType()) { - return false + if len(rs.Statuses) > 0 { + searchConditions = append(searchConditions, fmt.Sprintf("status = %q", strings.Join(rs.Statuses, ","))) } - if status, err := s.GetStatus(); err != nil { - logger.Errorf("failed to get status: %v", err) - return false - } else if len(rs.Statuses) > 0 && !rs.Statuses.Contains(status) { + if convertSelectors { + // Adding this flag for now until we migrate matchItems support in the SQL query + if rs.LabelSelector != "" { + searchConditions = append(searchConditions, selectorToPegCondition("labels.", rs.LabelSelector)...) + } + + if rs.TagSelector != "" { + searchConditions = append(searchConditions, selectorToPegCondition("tags.", rs.TagSelector)...) + } + + if rs.FieldSelector != "" { + searchConditions = append(searchConditions, selectorToPegCondition("", rs.FieldSelector)...) + } + } + + peg := rs.Search + if len(searchConditions) > 0 { + joined := strings.Join(searchConditions, " ") + peg += fmt.Sprintf(" %s", joined) + } + + return peg +} + +func selectorToPegCondition(fieldPrefix, selector string) []string { + parsed, err := labels.Parse(selector) + if err != nil { + return nil + } + + requirements, selectable := parsed.Requirements() + if !selectable { + return nil + } + + var searchConditions []string + for _, requirement := range requirements { + operator := grammar.Eq + + switch requirement.Operator() { + case selection.Equals, selection.In: + operator = grammar.Eq + case selection.NotEquals, selection.NotIn: + operator = grammar.Neq + case selection.GreaterThan: + operator = grammar.Gt + case selection.LessThan: + operator = grammar.Lt + } + + condition := fmt.Sprintf("%s%s %s %q", fieldPrefix, requirement.Key(), operator, strings.Join(requirement.Values().List(), ",")) + searchConditions = append(searchConditions, condition) + } + + return searchConditions +} + +func (rs ResourceSelector) Matches(s ResourceSelectable) bool { + if rs.IsEmpty() { return false } - if h, err := s.GetHealth(); err != nil { - logger.Errorf("failed to get health: %v", err) + if rs.Wildcard() { + return true + } + + peg := rs.ToPeg(true) + if peg == "" { return false - } else if len(rs.Health) > 0 && !rs.Health.Match(h) { + } + + qf, err := grammar.ParsePEG(peg) + if err != nil { return false } - if len(rs.TagSelector) > 0 { - if tagsMatcher, ok := s.(TagsMatchable); ok { - parsed, err := labels.Parse(rs.TagSelector) + return rs.matchGrammar(qf, s) +} + +func (rs *ResourceSelector) matchGrammar(qf *grammar.QueryField, s ResourceSelectable) bool { + if qf.Field != "" { + var err error + + value, err := extractResourceFieldValue(s, qf.Field) + if err != nil { + logger.Errorf("failed to extract value for field: %v", qf.Field) + return false + } + + var patterns []string + if qfs, ok := qf.Value.(string); ok { + patterns = strings.Split(qfs, ",") + } + + switch qf.Op { + case grammar.Eq: + return collections.MatchItems(value, patterns...) + + case grammar.Neq: + return !collections.MatchItems(value, patterns...) + + case grammar.Gt, grammar.Lt: + propertyValue, err := strconv.ParseFloat(value, 64) if err != nil { - logger.Errorf("bad tag selector: %v", err) + logger.WithValues("value", value).Errorf("properties lessthan and greaterthan operator only supports numbers") return false - } else if !parsed.Matches(tagsMatcher.GetTagsMatcher()) { + } + + queryValue, err := strconv.ParseFloat(qf.Value.(string), 64) + if err != nil { + logger.WithValues("value", value).Errorf("properties lessthan and greaterthan operator only supports numbers") return false } - } - } - if len(rs.LabelSelector) > 0 { - parsed, err := labels.Parse(rs.LabelSelector) - if err != nil { - logger.Errorf("bad label selector: %v", err) - return false - } else if !parsed.Matches(s.GetLabelsMatcher()) { + if qf.Op == grammar.Gt { + return propertyValue > queryValue + } + + return propertyValue < queryValue + + default: + logger.WithValues("operation", qf.Op).Infof("matchGrammar not-implemented") return false } } - if len(rs.FieldSelector) > 0 { - parsed, err := labels.Parse(rs.FieldSelector) - if err != nil { - logger.Errorf("bad field selector: %v", err) - return false - } else if !parsed.Matches(s.GetFieldsMatcher()) { - return false + var matchAny bool + for _, subQf := range qf.Fields { + match := rs.matchGrammar(subQf, s) + if match { + matchAny = true + } + + if qf.Op == "and" && !match { + return false // fail early } } - return true + return matchAny } type ResourceSelectors []ResourceSelector @@ -397,3 +411,60 @@ type ResourceSelectable interface { GetStatus() (string, error) GetHealth() (string, error) } + +func extractResourceFieldValue(rs ResourceSelectable, field string) (string, error) { + switch field { + case "name": + return rs.GetName(), nil + case "namespace": + return rs.GetNamespace(), nil + case "id": + return rs.GetID(), nil + case "type": + return rs.GetType(), nil + case "status": + value, err := rs.GetStatus() + if err != nil { + return "", fmt.Errorf("failed to get status: %w", err) + } + return value, nil + case "health": + value, err := rs.GetHealth() + if err != nil { + return "", fmt.Errorf("failed to get health: %w", err) + } + return value, nil + } + + if strings.HasPrefix(field, "labels.") { + key := strings.TrimSpace(strings.TrimPrefix(field, "labels.")) + return rs.GetLabelsMatcher().Get(key), nil + } else if strings.HasPrefix(field, "tags.") { + key := strings.TrimSpace(strings.TrimPrefix(field, "tags.")) + if tagsMatcher, ok := rs.(TagsMatchable); ok { + return tagsMatcher.GetTagsMatcher().Get(key), nil + } + } else if strings.HasPrefix(field, "properties.") { + propertyName := strings.TrimSpace(strings.TrimPrefix(field, "properties.")) + propertiesJSON := rs.GetFieldsMatcher().Get("properties") + var properties Properties + if err := json.Unmarshal([]byte(propertiesJSON), &properties); err != nil { + return "", fmt.Errorf("failed to unmarshall properties: %w", err) + } + + for _, p := range properties { + if p.Name != propertyName { + continue + } + + if p.Text != "" { + return p.Text, nil + } else if p.Value != nil { + return strconv.FormatInt(*p.Value, 10), nil + } + } + } + + // Unknown key is a field selector + return rs.GetFieldsMatcher().Get(strings.TrimSpace(field)), nil +} diff --git a/types/resource_selector_test.go b/types/resource_selector_test.go index 79651fd3..2ba109b3 100644 --- a/types/resource_selector_test.go +++ b/types/resource_selector_test.go @@ -1,6 +1,8 @@ package types_test import ( + "fmt" + "github.com/google/uuid" "github.com/samber/lo" @@ -49,15 +51,15 @@ var _ = Describe("Resource Selector", func() { Describe("Matches", func() { tests := []struct { - name string - resourceSelector types.ResourceSelector - selectable types.ResourceSelectable - unselectable types.ResourceSelectable + name string + resourceSelectors []types.ResourceSelector // canonical resource selectors + selectable types.ResourceSelectable + unselectable types.ResourceSelectable }{ { - name: "Blank", - resourceSelector: types.ResourceSelector{}, - selectable: nil, + name: "Blank", + resourceSelectors: []types.ResourceSelector{}, + selectable: nil, unselectable: models.ConfigItem{ Name: lo.ToPtr("silverbullet"), Labels: &types.JSONStringMap{ @@ -67,8 +69,8 @@ var _ = Describe("Resource Selector", func() { }, { name: "ID", - resourceSelector: types.ResourceSelector{ - ID: "4775d837-727a-4386-9225-1fa2c167cc96", + resourceSelectors: []types.ResourceSelector{ + {ID: "4775d837-727a-4386-9225-1fa2c167cc96"}, }, selectable: models.ConfigItem{ ID: uuid.MustParse("4775d837-727a-4386-9225-1fa2c167cc96"), @@ -81,9 +83,9 @@ var _ = Describe("Resource Selector", func() { }, { name: "Namespace & Name", - resourceSelector: types.ResourceSelector{ - Name: "airsonic", - Namespace: "default", + resourceSelectors: []types.ResourceSelector{ + {Name: "airsonic", Namespace: "default"}, + {Search: "name=airsonic namespace=default"}, }, selectable: models.ConfigItem{ Name: lo.ToPtr("airsonic"), @@ -100,8 +102,9 @@ var _ = Describe("Resource Selector", func() { }, { name: "Types", - resourceSelector: types.ResourceSelector{ - Types: []string{"Kubernetes::Pod"}, + resourceSelectors: []types.ResourceSelector{ + {Types: []string{"Kubernetes::Pod"}}, + {Search: "type=Kubernetes::Pod"}, }, selectable: models.ConfigItem{ Name: lo.ToPtr("cert-manager"), @@ -114,8 +117,9 @@ var _ = Describe("Resource Selector", func() { }, { name: "Types multiple", - resourceSelector: types.ResourceSelector{ - Types: []string{"Kubernetes::Node", "Kubernetes::Pod"}, + resourceSelectors: []types.ResourceSelector{ + {Types: []string{"Kubernetes::Node", "Kubernetes::Pod"}}, + {Search: "type=Kubernetes::Node,Kubernetes::Pod"}, }, selectable: models.ConfigItem{ Name: lo.ToPtr("cert-manager"), @@ -128,8 +132,9 @@ var _ = Describe("Resource Selector", func() { }, { name: "Type negatives", - resourceSelector: types.ResourceSelector{ - Types: []string{"!Kubernetes::Deployment", "Kubernetes::Pod"}, + resourceSelectors: []types.ResourceSelector{ + {Types: []string{"!Kubernetes::Deployment", "Kubernetes::Pod"}}, + {Search: "type=Kubernetes::Pod type!=Kubernetes::Deployment"}, }, selectable: models.ConfigItem{ Name: lo.ToPtr("cert-manager"), @@ -142,9 +147,9 @@ var _ = Describe("Resource Selector", func() { }, { name: "Statuses", - resourceSelector: types.ResourceSelector{ - Namespace: "default", - Statuses: []string{"healthy"}, + resourceSelectors: []types.ResourceSelector{ + {Namespace: "default", Statuses: []string{"healthy"}}, + {Search: "namespace=default status=healthy"}, }, selectable: models.ConfigItem{ Tags: types.JSONStringMap{ @@ -161,9 +166,9 @@ var _ = Describe("Resource Selector", func() { }, { name: "Healths", - resourceSelector: types.ResourceSelector{ - Namespace: "default", - Health: "healthy", + resourceSelectors: []types.ResourceSelector{ + {Namespace: "default", Health: "healthy"}, + {Search: "namespace=default health=healthy"}, }, selectable: models.ConfigItem{ Tags: types.JSONStringMap{ @@ -180,9 +185,9 @@ var _ = Describe("Resource Selector", func() { }, { name: "Healths multiple", - resourceSelector: types.ResourceSelector{ - Namespace: "default", - Health: "healthy,warning", + resourceSelectors: []types.ResourceSelector{ + {Namespace: "default", Health: "healthy,warning"}, + {Search: "namespace=default health=healthy,warning"}, }, selectable: models.ConfigItem{ Tags: types.JSONStringMap{ @@ -199,9 +204,9 @@ var _ = Describe("Resource Selector", func() { }, { name: "Label selector", - resourceSelector: types.ResourceSelector{ - Namespace: "default", - LabelSelector: "env=production", + resourceSelectors: []types.ResourceSelector{ + {Namespace: "default", LabelSelector: "env=production"}, + {Search: "namespace=default labels.env=production"}, }, selectable: models.ConfigItem{ ConfigClass: "Cluster", @@ -224,9 +229,8 @@ var _ = Describe("Resource Selector", func() { }, { name: "Label selector IN query", - resourceSelector: types.ResourceSelector{ - Namespace: "default", - LabelSelector: "env in (production)", + resourceSelectors: []types.ResourceSelector{ + {Namespace: "default", LabelSelector: "env in (production)"}, }, selectable: models.ConfigItem{ ConfigClass: "Cluster", @@ -247,11 +251,31 @@ var _ = Describe("Resource Selector", func() { }, }, }, + { + name: "Tag selector", + resourceSelectors: []types.ResourceSelector{ + {Namespace: "default", TagSelector: "cluster=aws"}, + {Search: "namespace=default tags.cluster=aws"}, + }, + selectable: models.ConfigItem{ + ConfigClass: "Cluster", + Tags: types.JSONStringMap{ + "cluster": "aws", + "namespace": "default", + }, + }, + unselectable: models.ConfigItem{ + ConfigClass: "Cluster", + Tags: types.JSONStringMap{ + "cluster": "workload", + "namespace": "default", + }, + }, + }, { name: "Field selector", - resourceSelector: types.ResourceSelector{ - Namespace: "default", - FieldSelector: "config_class=Cluster", + resourceSelectors: []types.ResourceSelector{ + {Namespace: "default", FieldSelector: "config_class=Cluster"}, }, selectable: models.ConfigItem{ Tags: types.JSONStringMap{ @@ -268,9 +292,8 @@ var _ = Describe("Resource Selector", func() { }, { name: "Field selector NOT IN query", - resourceSelector: types.ResourceSelector{ - Namespace: "default", - FieldSelector: "config_class notin (Cluster)", + resourceSelectors: []types.ResourceSelector{ + {Namespace: "default", FieldSelector: "config_class notin (Cluster)"}, }, selectable: models.ConfigItem{ Tags: types.JSONStringMap{ @@ -287,8 +310,8 @@ var _ = Describe("Resource Selector", func() { }, { name: "Field selector property matcher (text)", - resourceSelector: types.ResourceSelector{ - FieldSelector: "color=red", + resourceSelectors: []types.ResourceSelector{ + {FieldSelector: "properties.color=red"}, }, selectable: models.ConfigItem{ Properties: &types.Properties{ @@ -302,9 +325,9 @@ var _ = Describe("Resource Selector", func() { }, }, { - name: "Field selector property matcher (value)", - resourceSelector: types.ResourceSelector{ - FieldSelector: "memory>50", + name: "Property selector", + resourceSelectors: []types.ResourceSelector{ + {FieldSelector: "properties.memory>50"}, }, selectable: models.ConfigItem{ Properties: &types.Properties{ @@ -319,20 +342,26 @@ var _ = Describe("Resource Selector", func() { }, } - for _, tt := range tests { - if tt.name != "Healths multiple - II" { - continue - } + Describe("test", func() { + for _, tt := range tests { + // if tt.name != "Field selector" { + // continue + // } - It(tt.name, func() { - if tt.selectable != nil { - Expect(tt.resourceSelector.Matches(tt.selectable)).To(BeTrue()) - } + It(tt.name, func() { + if tt.selectable != nil { + for _, rs := range tt.resourceSelectors { + Expect(rs.Matches(tt.selectable)).To(BeTrue(), fmt.Sprintf("%v", rs)) + } + } - if tt.unselectable != nil { - Expect(tt.resourceSelector.Matches(tt.unselectable)).To(BeFalse()) - } - }) - } + if tt.unselectable != nil { + for _, rs := range tt.resourceSelectors { + Expect(rs.Matches(tt.unselectable)).To(BeFalse(), fmt.Sprintf("%v", rs)) + } + } + }) + } + }) }) })