Skip to content

Commit

Permalink
feat: related changes search (#580)
Browse files Browse the repository at this point in the history
* related changes query

* implement filtering syntax

* feat: add summary

* tests
  • Loading branch information
adityathebe authored Mar 7, 2024
1 parent 6dd2708 commit a121315
Show file tree
Hide file tree
Showing 9 changed files with 338 additions and 24 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ require (
github.com/rodaine/table v1.1.0
github.com/samber/lo v1.39.0
github.com/spf13/pflag v1.0.5
github.com/timberio/go-datemath v0.1.0
github.com/xeipuuv/gojsonschema v1.2.0
github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2
github.com/zclconf/go-cty v1.14.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1382,6 +1382,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.0.4 h1:UcdIRXff12Lpnu3OLtZvnc03g4vH2suXDXhBwBqmzYg=
github.com/tidwall/sjson v1.0.4/go.mod h1:bURseu1nuBkFpIES5cz6zBtjmYeOQmEESshn7VpF15Y=
github.com/timberio/go-datemath v0.1.0 h1:1OUCvSIX1qXLJ57h12OWfgt6MNpJnsdNvrp8dLIUFtg=
github.com/timberio/go-datemath v0.1.0/go.mod h1:m7kjsbCuO4QKP3KLfnxiUZWiOiFXmxj30HeexjL3lc0=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
Expand Down
7 changes: 7 additions & 0 deletions hack/migrate/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,13 @@ require (
github.com/klauspost/compress v1.17.4 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/labstack/echo/v4 v4.11.4 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/liamylian/jsontime/v2 v2.0.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
Expand Down Expand Up @@ -129,8 +133,11 @@ require (
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.0.4 // indirect
github.com/timberio/go-datemath v0.1.0 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2 // indirect
github.com/yuin/gopher-lua v1.1.0 // indirect
Expand Down
14 changes: 14 additions & 0 deletions hack/migrate/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1128,6 +1128,10 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/liamylian/jsontime/v2 v2.0.0 h1:3if2kDW/boymUdO+4Qj/m4uaXMBSF6np9KEgg90cwH0=
github.com/liamylian/jsontime/v2 v2.0.0/go.mod h1:UHp1oAPqCBfspokvGmaGe0IAl2IgOpgOgDaKPcvcGGY=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
Expand All @@ -1141,11 +1145,15 @@ github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
Expand Down Expand Up @@ -1357,6 +1365,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.0.4 h1:UcdIRXff12Lpnu3OLtZvnc03g4vH2suXDXhBwBqmzYg=
github.com/tidwall/sjson v1.0.4/go.mod h1:bURseu1nuBkFpIES5cz6zBtjmYeOQmEESshn7VpF15Y=
github.com/timberio/go-datemath v0.1.0 h1:1OUCvSIX1qXLJ57h12OWfgt6MNpJnsdNvrp8dLIUFtg=
github.com/timberio/go-datemath v0.1.0/go.mod h1:m7kjsbCuO4QKP3KLfnxiUZWiOiFXmxj30HeexjL3lc0=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
Expand All @@ -1365,6 +1375,10 @@ github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
Expand Down
4 changes: 2 additions & 2 deletions models/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,8 @@ func (cr ConfigRelationship) TableName() string {

// ConfigChange represents the config change database table
type ConfigChange struct {
ExternalID string `gorm:"-"`
ConfigType string `gorm:"-"`
ExternalID string `gorm:"-" json:"-"`
ConfigType string `gorm:"-" json:"-"`
ExternalChangeId string `gorm:"column:external_change_id" json:"external_change_id"`
ID string `gorm:"primaryKey;unique_index;not null;column:id" json:"id"`
ConfigID string `gorm:"column:config_id;default:''" json:"config_id"`
Expand Down
53 changes: 53 additions & 0 deletions query/commons.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,56 @@
package query

import (
"fmt"
"strings"
)

var LocalFilter = "deleted_at is NULL AND agent_id = '00000000-0000-0000-0000-000000000000' OR agent_id IS NULL"

// parseFilteringQuery parses a filtering query string.
// It returns four slices: 'in', 'notIN', 'prefix', and 'suffix'.
func parseFilteringQuery(query string) (in, notIN, prefix, suffix []string) {
items := strings.Split(query, ",")

for _, item := range items {
if strings.HasPrefix(item, "!") {
notIN = append(notIN, strings.TrimPrefix(item, "!"))
} else if strings.HasPrefix(item, "*") {
suffix = append(suffix, strings.TrimPrefix(item, "*"))
} else if strings.HasSuffix(item, "*") {
prefix = append(prefix, strings.TrimSuffix(item, "*"))
} else {
in = append(in, item)
}
}

return
}

func parseAndBuildFilteringQuery(query string, field string) ([]string, map[string]any) {
var clauses []string
var args = map[string]any{}

in, notIN, prefixes, suffixes := parseFilteringQuery(query)
if len(in) > 0 {
clauses = append(clauses, fmt.Sprintf("%s IN @field_in", field))
args["field_in"] = in
}

if len(notIN) > 0 {
clauses = append(clauses, fmt.Sprintf("%s NOT IN @field_not_in", field))
args["field_not_in"] = notIN
}

for i, p := range prefixes {
clauses = append(clauses, fmt.Sprintf("%s LIKE @%s_prefix_%d", field, field, i))
args[fmt.Sprintf("prefix_%d", i)] = fmt.Sprintf("%s%%", p)
}

for i, s := range suffixes {
clauses = append(clauses, fmt.Sprintf("%s LIKE @%s_suffix_%d", field, field, i))
args[fmt.Sprintf("suffix_%d", i)] = fmt.Sprintf("%%%s", s)
}

return clauses, args
}
114 changes: 114 additions & 0 deletions query/config_changes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package query

import (
"fmt"
"strings"
"time"

"github.com/flanksource/commons/collections"
"github.com/flanksource/duty/api"
"github.com/flanksource/duty/context"
"github.com/flanksource/duty/models"
"github.com/google/uuid"
"github.com/samber/lo"
"github.com/timberio/go-datemath"
)

const (
CatalogChangeRecursiveUpstream = "upstream"
CatalogChangeRecursiveDownstream = "downstream"
CatalogChangeRecursiveBoth = "both"
)

type CatalogChangesSearchRequest struct {
CatalogID uuid.UUID `query:"id"`
ConfigType string `query:"config_type"`
ChangeType string `query:"type"`
From string `query:"from"`

// upstream | downstream | both
Recursive string `query:"recursive"`

fromParsed time.Time
}

func (t *CatalogChangesSearchRequest) Validate() error {
if t.CatalogID == uuid.Nil {
return fmt.Errorf("catalog id is required")
}

if t.Recursive != "" && !lo.Contains([]string{CatalogChangeRecursiveUpstream, CatalogChangeRecursiveDownstream, CatalogChangeRecursiveBoth}, t.Recursive) {
return fmt.Errorf("recursive must be one of 'upstream', 'downstream' or 'both'")
}

if t.From != "" {
if expr, err := datemath.Parse(t.From); err != nil {
return fmt.Errorf("invalid 'from' param: %w", err)
} else {
t.fromParsed = expr.Time()
}
}

return nil
}

type CatalogChangesSearchResponse struct {
Summary map[string]int `json:"summary,omitempty"`
Changes []models.ConfigChange `json:"changes,omitempty"`
}

func (t *CatalogChangesSearchResponse) Summarize() {
t.Summary = make(map[string]int)
for _, c := range t.Changes {
t.Summary[c.ChangeType]++
}
}

func FindCatalogChanges(ctx context.Context, req CatalogChangesSearchRequest) (*CatalogChangesSearchResponse, error) {
if err := req.Validate(); err != nil {
return nil, api.Errorf(api.EINVALID, "bad request: %v", err)
}

args := map[string]any{
"catalog_id": req.CatalogID,
"recursive": req.Recursive,
}

var clauses []string
query := "SELECT cc.* FROM related_changes_recursive(@catalog_id, @recursive) cc"
if req.Recursive == "" {
query = "SELECT cc.* FROM config_changes cc"
clauses = append(clauses, "cc.config_id = @catalog_id")
}

if req.ConfigType != "" {
query += " LEFT JOIN config_items ON cc.config_id = config_items.id"

_clauses, _args := parseAndBuildFilteringQuery(req.ConfigType, "config_items.type")
clauses = append(clauses, _clauses...)
args = collections.MergeMap(args, _args)
}

if req.ChangeType != "" {
_clauses, _args := parseAndBuildFilteringQuery(req.ChangeType, "cc.change_type")
clauses = append(clauses, _clauses...)
args = collections.MergeMap(args, _args)
}

if !req.fromParsed.IsZero() {
clauses = append(clauses, "cc.created_at >= @from")
args["from"] = req.fromParsed
}

if len(clauses) > 0 {
query += fmt.Sprintf(" WHERE %s", strings.Join(clauses, " AND "))
}

var output CatalogChangesSearchResponse
if err := ctx.DB().Raw(query, args).Find(&output.Changes).Error; err != nil {
return nil, err
}

output.Summarize()
return &output, nil
}
Loading

0 comments on commit a121315

Please sign in to comment.