diff --git a/query/config_changes.go b/query/config_changes.go index 8128416e..932caa88 100644 --- a/query/config_changes.go +++ b/query/config_changes.go @@ -58,6 +58,8 @@ type CatalogChangesSearchRequest struct { // upstream | downstream | both Recursive string `query:"recursive" json:"recursive"` + // FIXME: Soft only works when Recursive=both and not with upstream/downstream + Soft bool `query:"soft" json:"soft"` fromParsed time.Time toParsed time.Time @@ -347,7 +349,7 @@ func FindCatalogChanges(ctx context.Context, req CatalogChangesSearchRequest) (* table := query.Table("catalog_changes") if err := uuid.Validate(req.CatalogID); err == nil { - table = query.Table("related_changes_recursive(?,?,?,?, true)", req.CatalogID, req.Recursive, req.IncludeDeletedConfigs, req.Depth) + table = query.Table("related_changes_recursive(?,?,?,?,?)", req.CatalogID, req.Recursive, req.IncludeDeletedConfigs, req.Depth, req.Soft) } else { clause, err := parseAndBuildFilteringQuery(req.CatalogID, "config_id", false) if err != nil { diff --git a/tests/config_changes_test.go b/tests/config_changes_test.go index cb81917b..c07d2929 100644 --- a/tests/config_changes_test.go +++ b/tests/config_changes_test.go @@ -1,6 +1,7 @@ package tests import ( + "strings" "time" "github.com/flanksource/duty/db" @@ -14,7 +15,7 @@ import ( "github.com/samber/lo" ) -var _ = ginkgo.FDescribe("Config changes recursive", ginkgo.Ordered, func() { +var _ = ginkgo.Describe("Config changes recursive", ginkgo.Ordered, func() { // Graph #1 (acyclic) // // U @@ -28,21 +29,25 @@ var _ = ginkgo.FDescribe("Config changes recursive", ginkgo.Ordered, func() { // Create a list of ConfigItems var ( U = models.ConfigItem{ID: uuid.New(), Tags: types.JSONStringMap{"namespace": "test-changes"}, Type: lo.ToPtr("Kubernetes::Node"), Name: lo.ToPtr("U"), ConfigClass: "Node"} - V = models.ConfigItem{ID: uuid.New(), Tags: types.JSONStringMap{"namespace": "test-changes"}, Type: lo.ToPtr("Kubernetes::Deployment"), Name: lo.ToPtr("V"), ConfigClass: "Deployment"} - W = models.ConfigItem{ID: uuid.New(), Tags: types.JSONStringMap{"namespace": "test-changes"}, Type: lo.ToPtr("Kubernetes::Pod"), Name: lo.ToPtr("W"), ConfigClass: "Pod"} - X = models.ConfigItem{ID: uuid.New(), Tags: types.JSONStringMap{"namespace": "test-changes"}, Type: lo.ToPtr("Kubernetes::ReplicaSet"), Name: lo.ToPtr("X"), ConfigClass: "ReplicaSet"} - Y = models.ConfigItem{ID: uuid.New(), Tags: types.JSONStringMap{"namespace": "test-changes"}, Type: lo.ToPtr("Kubernetes::PersistentVolume"), Name: lo.ToPtr("Y"), ConfigClass: "PersistentVolume"} - Z = models.ConfigItem{ID: uuid.New(), Tags: types.JSONStringMap{"namespace": "test-changes"}, Type: lo.ToPtr("Kubernetes::Pod"), Name: lo.ToPtr("Z"), ConfigClass: "Pod"} + V = models.ConfigItem{ID: uuid.New(), Tags: types.JSONStringMap{"namespace": "test-changes"}, Type: lo.ToPtr("Kubernetes::Deployment"), Name: lo.ToPtr("V"), ConfigClass: "Deployment", ParentID: lo.ToPtr(U.ID), Path: U.ID.String()} + W = models.ConfigItem{ID: uuid.New(), Tags: types.JSONStringMap{"namespace": "test-changes"}, Type: lo.ToPtr("Kubernetes::Pod"), Name: lo.ToPtr("W"), ConfigClass: "Pod", ParentID: lo.ToPtr(U.ID), Path: U.ID.String()} + X = models.ConfigItem{ID: uuid.New(), Tags: types.JSONStringMap{"namespace": "test-changes"}, Type: lo.ToPtr("Kubernetes::ReplicaSet"), Name: lo.ToPtr("X"), ConfigClass: "ReplicaSet", ParentID: lo.ToPtr(V.ID), Path: strings.Join([]string{U.ID.String(), V.ID.String()}, ".")} + Y = models.ConfigItem{ID: uuid.New(), Tags: types.JSONStringMap{"namespace": "test-changes"}, Type: lo.ToPtr("Kubernetes::PersistentVolume"), Name: lo.ToPtr("Y"), ConfigClass: "PersistentVolume", ParentID: lo.ToPtr(V.ID), Path: strings.Join([]string{U.ID.String(), V.ID.String()}, ".")} + Z = models.ConfigItem{ID: uuid.New(), Tags: types.JSONStringMap{"namespace": "test-changes"}, Type: lo.ToPtr("Kubernetes::Pod"), Name: lo.ToPtr("Z"), ConfigClass: "Pod", ParentID: lo.ToPtr(X.ID), Path: strings.Join([]string{U.ID.String(), V.ID.String(), X.ID.String()}, ".")} + + A = models.ConfigItem{ID: uuid.New(), Tags: types.JSONStringMap{"namespace": "test-changes"}, Type: lo.ToPtr("Kubernetes::ConfigMap"), Name: lo.ToPtr("A"), ConfigClass: "ConfigMap"} + B = models.ConfigItem{ID: uuid.New(), Tags: types.JSONStringMap{"namespace": "test-changes"}, Type: lo.ToPtr("Kubernetes::ConfigMap"), Name: lo.ToPtr("B"), ConfigClass: "ConfigMap"} + C = models.ConfigItem{ID: uuid.New(), Tags: types.JSONStringMap{"namespace": "test-changes"}, Type: lo.ToPtr("Kubernetes::ConfigMap"), Name: lo.ToPtr("C"), ConfigClass: "ConfigMap"} + D = models.ConfigItem{ID: uuid.New(), Tags: types.JSONStringMap{"namespace": "test-changes"}, Type: lo.ToPtr("Kubernetes::ConfigMap"), Name: lo.ToPtr("D"), ConfigClass: "ConfigMap"} ) - configItems := []models.ConfigItem{U, V, W, X, Y, Z} + configItems := []models.ConfigItem{U, V, W, X, Y, Z, A, B, C, D} // Create relationships between ConfigItems relationships := []models.ConfigRelationship{ - {ConfigID: U.ID.String(), RelatedID: V.ID.String(), Relation: "test-changes-UV"}, - {ConfigID: U.ID.String(), RelatedID: W.ID.String(), Relation: "test-changes-UW"}, - {ConfigID: V.ID.String(), RelatedID: X.ID.String(), Relation: "test-changes-VX"}, - {ConfigID: V.ID.String(), RelatedID: Y.ID.String(), Relation: "test-changes-VY"}, - {ConfigID: X.ID.String(), RelatedID: Z.ID.String(), Relation: "test-changes-XZ"}, + {ConfigID: U.ID.String(), RelatedID: A.ID.String(), Relation: "test-changes-UA"}, + {ConfigID: V.ID.String(), RelatedID: B.ID.String(), Relation: "test-changes-VB"}, + {ConfigID: X.ID.String(), RelatedID: C.ID.String(), Relation: "test-changes-XC"}, + //{ConfigID: Z.ID.String(), RelatedID: D.ID.String(), Relation: "test-changes-ZD"}, } // Create changes for each config @@ -54,7 +59,12 @@ var _ = ginkgo.FDescribe("Config changes recursive", ginkgo.Ordered, func() { YChange = models.ConfigChange{ID: uuid.New().String(), CreatedAt: lo.ToPtr(time.Now().Add(-time.Hour * 4)), Severity: "warn", ConfigID: Y.ID.String(), Summary: ".name.Y", ChangeType: "diff", Source: "test-changes"} ZChange = models.ConfigChange{ID: uuid.New().String(), CreatedAt: lo.ToPtr(time.Now().Add(-time.Hour * 5)), Severity: "info", ConfigID: Z.ID.String(), Summary: ".name.Z", ChangeType: "Pulled", Source: "test-changes"} - changes = []models.ConfigChange{UChange, VChange, WChange, XChange, YChange, ZChange} + AChange = models.ConfigChange{ID: uuid.New().String(), CreatedAt: lo.ToPtr(time.Now().Add(-time.Hour * 5)), Severity: "info", ConfigID: A.ID.String(), Summary: ".name.A", ChangeType: "Pulled", Source: "test-changes"} + BChange = models.ConfigChange{ID: uuid.New().String(), CreatedAt: lo.ToPtr(time.Now().Add(-time.Hour * 5)), Severity: "info", ConfigID: B.ID.String(), Summary: ".name.B", ChangeType: "Pulled", Source: "test-changes"} + CChange = models.ConfigChange{ID: uuid.New().String(), CreatedAt: lo.ToPtr(time.Now().Add(-time.Hour * 5)), Severity: "info", ConfigID: C.ID.String(), Summary: ".name.C", ChangeType: "Pulled", Source: "test-changes"} + DChange = models.ConfigChange{ID: uuid.New().String(), CreatedAt: lo.ToPtr(time.Now().Add(-time.Hour * 5)), Severity: "info", ConfigID: D.ID.String(), Summary: ".name.D", ChangeType: "Pulled", Source: "test-changes"} + + changes = []models.ConfigChange{UChange, VChange, WChange, XChange, YChange, ZChange, AChange, BChange, CChange, DChange} ) ginkgo.BeforeAll(func() { @@ -111,13 +121,12 @@ var _ = ginkgo.FDescribe("Config changes recursive", ginkgo.Ordered, func() { ginkgo.Context("Both ways", func() { ginkgo.It("should return changes upstream and downstream", func() { relatedChanges, err := findChanges(X.ID, "all", false) - Expect(err).To(BeNil()) - Expect(len(relatedChanges.Changes)).To(Equal(4)) + Expect(len(relatedChanges.Changes)).To(Equal(5)) relatedIDs := lo.Map(relatedChanges.Changes, func(rc query.ConfigChangeRow, _ int) string { return rc.ID }) - Expect(relatedIDs).To(ConsistOf([]string{UChange.ID, VChange.ID, XChange.ID, ZChange.ID})) + Expect(relatedIDs).To(ConsistOf([]string{UChange.ID, VChange.ID, XChange.ID, ZChange.ID, CChange.ID})) }) }) @@ -155,10 +164,10 @@ var _ = ginkgo.FDescribe("Config changes recursive", ginkgo.Ordered, func() { ginkgo.It("should return changes of a non-root node", func() { relatedChanges, err := findChanges(X.ID, "all", false) Expect(err).To(BeNil()) - Expect(len(relatedChanges.Changes)).To(Equal(4)) + Expect(len(relatedChanges.Changes)).To(Equal(5)) relatedIDs := lo.Map(relatedChanges.Changes, func(rc query.ConfigChangeRow, _ int) string { return rc.ID }) - Expect(relatedIDs).To(ConsistOf([]string{UChange.ID, VChange.ID, ZChange.ID, XChange.ID})) + Expect(relatedIDs).To(ConsistOf([]string{UChange.ID, VChange.ID, ZChange.ID, XChange.ID, CChange.ID})) }) }) @@ -320,10 +329,10 @@ var _ = ginkgo.FDescribe("Config changes recursive", ginkgo.Ordered, func() { Recursive: query.CatalogChangeRecursiveAll, }) Expect(err).To(BeNil()) - Expect(len(response.Changes)).To(Equal(5)) - Expect(response.Total).To(Equal(int64(5))) + Expect(len(response.Changes)).To(Equal(7)) + Expect(response.Total).To(Equal(int64(7))) Expect(response.Summary["diff"]).To(Equal(3)) - Expect(response.Summary["Pulled"]).To(Equal(1)) + Expect(response.Summary["Pulled"]).To(Equal(3)) Expect(response.Summary["RegisterNode"]).To(Equal(1)) }) }) diff --git a/views/006_config_views.sql b/views/006_config_views.sql index 2d104efe..b68b7d99 100644 --- a/views/006_config_views.sql +++ b/views/006_config_views.sql @@ -793,9 +793,16 @@ BEGIN cc.created_at, cc.severity, cc.change_type, cc.source, cc.summary, cc.created_by, cc.count, cc.first_observed, config_items.agent_id FROM config_changes cc LEFT JOIN config_items on config_items.id = cc.config_id - LEFT JOIN config_relationships cr ON cr.config_id = cc.config_id - WHERE starts_with(config_items.path, (SELECT CONCAT(config_items.path, '.', config_items.id) FROM config_items WHERE config_items.id = lookup_id )) OR - (soft AND cc.config_id = lookup_id ); + LEFT JOIN (SELECT config_relationships.config_id FROM config_relationships WHERE relation != 'hard') AS cr ON cr.config_id = cc.config_id + WHERE starts_with(config_items.path, ( + SELECT CASE + WHEN config_items.path = '' THEN config_items.id::text + ELSE CONCAT(config_items.path, '.', config_items.id) + END + FROM config_items WHERE config_items.id = lookup_id + )) OR + (cc.config_id = lookup_id) OR + (soft AND cr.config_id = lookup_id ); ELSIF type_filter IN ('upstream') THEN RETURN query @@ -804,9 +811,10 @@ BEGIN cc.created_at, cc.severity, cc.change_type, cc.source, cc.summary, cc.created_by, cc.count, cc.first_observed, config_items.agent_id FROM config_changes cc LEFT JOIN config_items on config_items.id = cc.config_id - LEFT JOIN config_relationships cr ON cr.config_id = cc.config_id - WHERE cc.config_id IN (SELECT id FROM get_recursive_path(lookup_id)) OR - (soft AND cc.config_id = lookup_id ); + LEFT JOIN (SELECT config_relationships.config_id FROM config_relationships WHERE relation != 'hard') AS cr ON cr.config_id = cc.config_id + WHERE cc.config_id IN (SELECT get_recursive_path.id FROM get_recursive_path(lookup_id)) OR + (cc.config_id = lookup_id) OR + (soft AND cr.config_id = lookup_id ); ELSE RETURN query