From c36ba456c1232c8ca1742c9417f26b183d021169 Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Fri, 6 Sep 2024 15:38:31 +0545 Subject: [PATCH] feat: add on insert trigger on config changes --- tests/config_changes_test.go | 65 ++++++++++++++++++++++++++++++++++ views/030_config_changes.sql | 68 ++++++++++++++++++++++++++++++++---- 2 files changed, 126 insertions(+), 7 deletions(-) diff --git a/tests/config_changes_test.go b/tests/config_changes_test.go index 1f9bcca5..f2b9ed40 100644 --- a/tests/config_changes_test.go +++ b/tests/config_changes_test.go @@ -3,8 +3,10 @@ package tests import ( "time" + "github.com/flanksource/duty/db" "github.com/flanksource/duty/models" "github.com/flanksource/duty/query" + "github.com/flanksource/duty/tests/fixtures/dummy" "github.com/flanksource/duty/types" "github.com/google/uuid" ginkgo "github.com/onsi/ginkgo/v2" @@ -366,3 +368,66 @@ var _ = ginkgo.Describe("Config changes recursive", ginkgo.Ordered, func() { }) }) }) + +var _ = ginkgo.Describe("handle external id conflict on config change inserts", ginkgo.Ordered, func() { + // An arbitrarily chosen time of the event we will be inserting multiple times + var referenceTime = time.Date(2020, 01, 15, 12, 00, 00, 00, time.UTC) + + dummyChanges := []models.ConfigChange{ + {ConfigID: dummy.EKSCluster.ID.String(), ExternalChangeID: lo.ToPtr("conflict_test_1"), Count: 1, CreatedAt: lo.ToPtr(referenceTime.Add(-time.Minute * 5)), Details: []byte(`{"replicas": "1"}`)}, + {ConfigID: dummy.EKSCluster.ID.String(), ExternalChangeID: lo.ToPtr("conflict_test_2"), Count: 1, CreatedAt: lo.ToPtr(referenceTime.Add(-time.Minute * 4)), Details: []byte(`{"replicas": "2"}`)}, + } + + ginkgo.BeforeAll(func() { + err := DefaultContext.DB().Create(dummyChanges).Error + Expect(err).To(BeNil()) + }) + + ginkgo.AfterAll(func() { + err := DefaultContext.DB().Delete(dummyChanges).Error + Expect(err).To(BeNil()) + }) + + ginkgo.It("should increase count when the details is changed", func() { + c := models.ConfigChange{ConfigID: dummy.EKSCluster.ID.String(), ExternalChangeID: lo.ToPtr("conflict_test_1"), Count: 1, CreatedAt: lo.ToPtr(referenceTime), Details: []byte(`{"replicas": "3"}`)} + err := DefaultContext.DB().Create(&c).Error + Expect(err).To(BeNil()) + + { + var inserted models.ConfigChange + err := DefaultContext.DB().Where("external_change_id = ? AND config_id = ?", c.ExternalChangeID, c.ConfigID).First(&inserted).Error + Expect(db.ErrorDetails(err)).NotTo(HaveOccurred()) + Expect(inserted.Count).To(Equal(2)) + } + }) + + ginkgo.It("should NOT increase count", func() { + // insert the same change with the same details and external change id + // and ensure the count doesn't change. + for i := 0; i < 10; i++ { + c := models.ConfigChange{ConfigID: dummy.EKSCluster.ID.String(), ExternalChangeID: lo.ToPtr("conflict_test_1"), CreatedAt: lo.ToPtr(referenceTime), Count: 1, Details: []byte(`{"replicas": "3"}`)} + err := DefaultContext.DB().Create(&c).Error + Expect(err).To(BeNil()) + + { + var inserted models.ConfigChange + err := DefaultContext.DB().Where("external_change_id = ? AND config_id = ?", c.ExternalChangeID, c.ConfigID).First(&inserted).Error + Expect(db.ErrorDetails(err)).NotTo(HaveOccurred()) + Expect(inserted.Count).To(Equal(2)) + } + } + }) + + ginkgo.It("should increase count when the details is the same but the created_at is changed", func() { + c := models.ConfigChange{ConfigID: dummy.EKSCluster.ID.String(), ExternalChangeID: lo.ToPtr("conflict_test_1"), Count: 1, CreatedAt: lo.ToPtr(referenceTime.Add(time.Minute)), Details: []byte(`{"replicas": "3"}`)} + err := DefaultContext.DB().Create(&c).Error + Expect(err).To(BeNil()) + + { + var inserted models.ConfigChange + err := DefaultContext.DB().Where("external_change_id = ? AND config_id = ?", c.ExternalChangeID, c.ConfigID).First(&inserted).Error + Expect(db.ErrorDetails(err)).NotTo(HaveOccurred()) + Expect(inserted.Count).To(Equal(3)) + } + }) +}) diff --git a/views/030_config_changes.sql b/views/030_config_changes.sql index 91a91a3c..8fee1476 100644 --- a/views/030_config_changes.sql +++ b/views/030_config_changes.sql @@ -1,5 +1,5 @@ -CREATE -OR REPLACE FUNCTION config_changes_update_trigger() RETURNS TRIGGER AS $$ +CREATE OR REPLACE FUNCTION config_changes_update_trigger() +RETURNS TRIGGER AS $$ DECLARE count_increment INT; BEGIN @@ -61,8 +61,62 @@ EXCEPTION END; $$ LANGUAGE plpgsql; -CREATE -OR REPLACE TRIGGER config_changes_update_trigger BEFORE -UPDATE - ON config_changes FOR EACH ROW - WHEN (pg_trigger_depth() = 0) EXECUTE FUNCTION config_changes_update_trigger(); \ No newline at end of file +CREATE OR REPLACE TRIGGER config_changes_update_trigger +BEFORE UPDATE +ON config_changes FOR EACH ROW +WHEN (pg_trigger_depth() = 0) EXECUTE FUNCTION config_changes_update_trigger(); + +--- +CREATE OR REPLACE FUNCTION config_changes_insert_trigger() +RETURNS TRIGGER AS $$ +DECLARE + existing_details JSONB; + existing_created_at TIMESTAMP WITH TIME ZONE; +BEGIN + -- run the original insert manually. + INSERT INTO config_changes SELECT NEW.*; + + -- Prevent the original insert by returning NULL + RETURN NULL; +EXCEPTION + WHEN unique_violation THEN + IF sqlerrm LIKE '%config_changes_config_id_external_change_id_key%' THEN + SELECT details, created_at FROM config_changes + WHERE external_change_id = NEW.external_change_id + INTO existing_details, existing_created_at; + + INSERT INTO event_queue(name, properties) VALUES('test', jsonb_build_object('id', NEW.id, 'oldDetails', OLD.details, 'newDetails', NEW.details)); + UPDATE config_changes + SET + change_type = NEW.change_type, + count = CASE + WHEN (NEW.details IS DISTINCT FROM existing_details OR NEW.created_at IS DISTINCT FROM existing_created_at) THEN config_changes.count + 1 + ELSE count + END, + created_at = NEW.created_at, + created_by = NEW.created_by, + details = NEW.details, + diff = NEW.diff, + external_created_by = NEW.external_created_by, + first_observed = LEAST(first_observed, created_at), + patches = NEW.patches, + severity = NEW.severity, + source = NEW.source, + summary = NEW.summary + WHERE + external_change_id = NEW.external_change_id; + + RETURN NULL; + ELSE + RAISE; + END IF; + WHEN OTHERS THEN + RAISE; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE TRIGGER config_changes_insert_trigger +BEFORE INSERT +ON config_changes FOR EACH ROW +WHEN (pg_trigger_depth() = 0) EXECUTE FUNCTION config_changes_insert_trigger(); +--- \ No newline at end of file