From be0e89142f69a8b6ce636ec8b286be063667ecb9 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Wed, 13 Mar 2024 13:51:21 -0300 Subject: [PATCH] Add migrations for calendar events (#17585) #17230 --- server/datastore/mysql/hosts.go | 1 + server/datastore/mysql/hosts_test.go | 19 ++++++- .../20240313085226_AddCalendarEventTables.go | 52 ++++++++++++++++++ ...40313085226_AddCalendarEventTables_test.go | 53 +++++++++++++++++++ server/fleet/calendar_events.go | 29 ++++++++++ 5 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 server/datastore/mysql/migrations/tables/20240313085226_AddCalendarEventTables.go create mode 100644 server/datastore/mysql/migrations/tables/20240313085226_AddCalendarEventTables_test.go create mode 100644 server/fleet/calendar_events.go diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index df2f4395bc..ca8986e2e5 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -502,6 +502,7 @@ var hostRefs = []string{ "query_results", "host_activities", "host_mdm_actions", + "host_calendar_events", } // NOTE: The following tables are explicity excluded from hostRefs list and accordingly are not diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index d1dedf0617..b57784b1b3 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -2554,7 +2554,6 @@ func testHostLiteByIdentifierAndID(t *testing.T, ds *Datastore) { h, err = ds.HostLiteByID(context.Background(), 0) assert.ErrorIs(t, err, sql.ErrNoRows) assert.Nil(t, h) - } func testHostsAddToTeam(t *testing.T, ds *Datastore) { @@ -2795,7 +2794,6 @@ func testHostsTotalAndUnseenSince(t *testing.T, ds *Datastore) { assert.Equal(t, 2, total) require.Len(t, unseen, 1) assert.Equal(t, host3.ID, unseen[0]) - } func testHostsListByPolicy(t *testing.T, ds *Datastore) { @@ -6577,6 +6575,23 @@ func testHostsDeleteHosts(t *testing.T, ds *Datastore) { `, host.ID) require.NoError(t, err) + // Add a calendar event for the host. + _, err = ds.writer(context.Background()).Exec(` + INSERT INTO calendar_events (email, start_time, end_time, event) + VALUES ('foobar@example.com', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, '{}'); + `) + require.NoError(t, err) + var calendarEventID int + err = ds.writer(context.Background()).Get(&calendarEventID, ` + SELECT id FROM calendar_events WHERE email = 'foobar@example.com'; + `) + require.NoError(t, err) + _, err = ds.writer(context.Background()).Exec(` + INSERT INTO host_calendar_events (host_id, calendar_event_id, webhook_status) + VALUES (?, ?, 1); + `, host.ID, calendarEventID) + require.NoError(t, err) + // Check there's an entry for the host in all the associated tables. for _, hostRef := range hostRefs { var ok bool diff --git a/server/datastore/mysql/migrations/tables/20240313085226_AddCalendarEventTables.go b/server/datastore/mysql/migrations/tables/20240313085226_AddCalendarEventTables.go new file mode 100644 index 0000000000..242a11ecbb --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20240313085226_AddCalendarEventTables.go @@ -0,0 +1,52 @@ +package tables + +import ( + "database/sql" + "fmt" +) + +func init() { + MigrationClient.AddMigration(Up_20240313085226, Down_20240313085226) +} + +func Up_20240313085226(tx *sql.Tx) error { + // TODO(lucas): Check if we need more indexes. + + if _, err := tx.Exec(` + CREATE TABLE IF NOT EXISTS calendar_events ( + id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + email VARCHAR(255) NOT NULL, + start_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + end_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + event JSON NOT NULL, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ); +`); err != nil { + return fmt.Errorf("create calendar_events table: %w", err) + } + + if _, err := tx.Exec(` + CREATE TABLE IF NOT EXISTS host_calendar_events ( + id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + host_id INT(10) UNSIGNED NOT NULL, + calendar_event_id INT(10) UNSIGNED NOT NULL, + webhook_status TINYINT NOT NULL, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + UNIQUE KEY idx_one_calendar_event_per_host (host_id), + FOREIGN KEY (calendar_event_id) REFERENCES calendar_events(id) ON DELETE CASCADE + ); +`); err != nil { + return fmt.Errorf("create host_calendar_events table: %w", err) + } + + return nil +} + +func Down_20240313085226(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20240313085226_AddCalendarEventTables_test.go b/server/datastore/mysql/migrations/tables/20240313085226_AddCalendarEventTables_test.go new file mode 100644 index 0000000000..216a3e468b --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20240313085226_AddCalendarEventTables_test.go @@ -0,0 +1,53 @@ +package tables + +import ( + "testing" + "time" + + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/stretchr/testify/require" +) + +func TestUp_20240313085226(t *testing.T) { + db := applyUpToPrev(t) + applyNext(t, db) + + sampleEvent := fleet.CalendarEvent{ + Email: "foo@example.com", + StartTime: time.Now().UTC(), + EndTime: time.Now().UTC().Add(30 * time.Minute), + Data: []byte("{\"foo\": \"bar\"}"), + } + sampleEvent.ID = uint(execNoErrLastID(t, db, + `INSERT INTO calendar_events (email, start_time, end_time, event) VALUES (?, ?, ?, ?);`, + sampleEvent.Email, sampleEvent.StartTime, sampleEvent.EndTime, sampleEvent.Data, + )) + + sampleHostEvent := fleet.HostCalendarEvent{ + HostID: 1, + CalendarEventID: sampleEvent.ID, + WebhookStatus: fleet.CalendarWebhookStatusPending, + } + sampleHostEvent.ID = uint(execNoErrLastID(t, db, + `INSERT INTO host_calendar_events (host_id, calendar_event_id, webhook_status) VALUES (?, ?, ?);`, + sampleHostEvent.HostID, sampleHostEvent.CalendarEventID, sampleHostEvent.WebhookStatus, + )) + + var event fleet.CalendarEvent + err := db.Get(&event, `SELECT * FROM calendar_events WHERE id = ?;`, sampleEvent.ID) + require.NoError(t, err) + sampleEvent.CreatedAt = event.CreatedAt // sampleEvent doesn't have this set. + sampleEvent.UpdatedAt = event.UpdatedAt // sampleEvent doesn't have this set. + sampleEvent.StartTime = sampleEvent.StartTime.Round(time.Second) + sampleEvent.EndTime = sampleEvent.EndTime.Round(time.Second) + event.StartTime = event.StartTime.Round(time.Second) + event.EndTime = event.EndTime.Round(time.Second) + require.Equal(t, sampleEvent, event) + + var hostEvent fleet.HostCalendarEvent + err = db.Get(&hostEvent, `SELECT * FROM host_calendar_events WHERE id = ?;`, sampleHostEvent.ID) + require.NoError(t, err) + sampleHostEvent.CreatedAt = hostEvent.CreatedAt // sampleHostEvent doesn't have this set. + sampleHostEvent.UpdatedAt = hostEvent.UpdatedAt // sampleHostEvent doesn't have this set. + require.Equal(t, sampleHostEvent, hostEvent) +} diff --git a/server/fleet/calendar_events.go b/server/fleet/calendar_events.go new file mode 100644 index 0000000000..7671b4aba5 --- /dev/null +++ b/server/fleet/calendar_events.go @@ -0,0 +1,29 @@ +package fleet + +import "time" + +type CalendarEvent struct { + ID uint `db:"id"` + Email string `db:"email"` + StartTime time.Time `db:"start_time"` + EndTime time.Time `db:"end_time"` + Data []byte `db:"event"` + + UpdateCreateTimestamps +} + +type CalendarWebhookStatus int + +const ( + CalendarWebhookStatusPending CalendarWebhookStatus = iota + CalendarWebhookStatusSent +) + +type HostCalendarEvent struct { + ID uint `db:"id"` + HostID uint `db:"host_id"` + CalendarEventID uint `db:"calendar_event_id"` + WebhookStatus CalendarWebhookStatus `db:"webhook_status"` + + UpdateCreateTimestamps +}