From 35660f21063c90105ddb694dc15d2792de3bd40c Mon Sep 17 00:00:00 2001 From: "Wilson E. Husin" Date: Tue, 9 Apr 2024 15:06:54 -0700 Subject: [PATCH 01/14] tfrender: fix memberships and user_id --- tfrender/tfrender.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tfrender/tfrender.go b/tfrender/tfrender.go index 04fbdd2..e1ca9c3 100644 --- a/tfrender/tfrender.go +++ b/tfrender/tfrender.go @@ -193,8 +193,8 @@ func (r *TFRender) ResourceFireHydrantTeams(ctx context.Context) error { b := fhTeamBlocks[name] b.AppendNewline() - b.AppendNewBlock("membership", []string{}).Body(). - SetAttributeTraversal("id", hcl.Traversal{ + b.AppendNewBlock("memberships", []string{}).Body(). + SetAttributeTraversal("user_id", hcl.Traversal{ hcl.TraverseRoot{Name: "data"}, hcl.TraverseAttr{Name: "firehydrant_user"}, hcl.TraverseAttr{Name: m.TFSlug()}, From 9d2f68064abac5625c771ae9eb69aee4b31b4120 Mon Sep 17 00:00:00 2001 From: "Wilson E. Husin" Date: Tue, 9 Apr 2024 15:28:13 -0700 Subject: [PATCH 02/14] tfrender/schedule: add timezone --- tfrender/tfrender.go | 1 + 1 file changed, 1 insertion(+) diff --git a/tfrender/tfrender.go b/tfrender/tfrender.go index e1ca9c3..6c4ad8c 100644 --- a/tfrender/tfrender.go +++ b/tfrender/tfrender.go @@ -130,6 +130,7 @@ func (r *TFRender) ResourceFireHydrantOnCallSchedule(ctx context.Context) error hcl.TraverseAttr{Name: t.TFSlug()}, hcl.TraverseAttr{Name: "id"}, }) + b.SetAttributeValue("time_zone", cty.StringVal(s.Timezone)) members, err := store.UseQueries(ctx).ListFhMembersByExtScheduleID(ctx, s.ID) if err != nil { From bfd300c57dccee4ab9ebe878df2821e0ce2bd87e Mon Sep 17 00:00:00 2001 From: "Wilson E. Husin" Date: Tue, 9 Apr 2024 15:58:00 -0700 Subject: [PATCH 03/14] pagerduty/scheduling: restriction-aware import --- Justfile | 2 +- pager/pagerduty.go | 81 ++++++++++++++++--- pager/testdata/TestPagerDuty/schedules.json | 5 +- store/models.go | 24 ++++-- store/queries.sql | 8 +- store/queries.sql.go | 80 +++++++++++++++--- store/schema.sql | 12 +++ ...TestRenderOnCallScheduleResource.golden.tf | 7 +- .../testdata/TestRenderTeamResource.golden.tf | 8 +- tfrender/tfrender.go | 22 ++++- 10 files changed, 208 insertions(+), 41 deletions(-) diff --git a/Justfile b/Justfile index fb96568..f447f25 100644 --- a/Justfile +++ b/Justfile @@ -12,7 +12,7 @@ test: generate go test -v ./... update-golden: generate - go test -v ./... -test.update-golden + go test -v ./tfrender -test.update-golden mod: go mod tidy diff --git a/pager/pagerduty.go b/pager/pagerduty.go index ddfe5a5..2903b32 100644 --- a/pager/pagerduty.go +++ b/pager/pagerduty.go @@ -3,6 +3,7 @@ package pager import ( "context" "fmt" + "strconv" "strings" "time" @@ -66,23 +67,36 @@ func (p *PagerDuty) saveScheduleToDB(ctx context.Context, schedule pagerduty.Sch } func (p *PagerDuty) saveLayerToDB(ctx context.Context, schedule pagerduty.Schedule, layer pagerduty.ScheduleLayer) error { + desc := fmt.Sprintf("(%s)", layer.Name) + if schedule.Description == "" { + desc = schedule.Description + " " + desc + } s := store.InsertExtScheduleParams{ ID: schedule.ID + "-" + layer.ID, - Name: schedule.Name + "-" + layer.Name, + Name: schedule.Name + " - " + layer.Name, Timezone: schedule.TimeZone, // Add fallback values and override them later if API provides valid information. - Description: fmt.Sprintf("%s (%s)", schedule.Description, layer.Name), - HandoffTime: "11:00", - HandoffDay: "wednesday", - Strategy: "weekly", + Description: desc, + HandoffTime: "11:00:00", + HandoffDay: "wednesday", + Strategy: "weekly", + ShiftDuration: "", } - // TODO: support custom strategy. For now: - // - any less than "weekly" is considered "daily" - // - any more than "weekly" is considered "weekly" anyway - if layer.RotationTurnLengthSeconds < 60*60*24*7 { + switch layer.RotationTurnLengthSeconds { + case 60 * 60 * 24: s.Strategy = "daily" + case 60 * 60 * 24 * 7: + s.Strategy = "weekly" + default: + console.Warnf("Found custom shift duration '%d seconds' for schedule '%s', rounding to daily value.\n", layer.RotationTurnLengthSeconds, s.Name) + if layer.RotationTurnLengthSeconds < 60*60*24 { + s.Strategy = "daily" + } else { + s.Strategy = "custom" + s.ShiftDuration = fmt.Sprintf("P%dD", layer.RotationTurnLengthSeconds/60/60/24) + } } virtualStart, err := time.Parse(time.RFC3339, layer.RotationVirtualStart) if err == nil { @@ -127,7 +141,54 @@ func (p *PagerDuty) saveLayerToDB(ctx context.Context, schedule pagerduty.Schedu } } - // TODO: add restrictions + for i, restriction := range layer.Restrictions { + switch restriction.Type { + case "daily_restriction": + for day := range 7 { + dayStr := strings.ToLower(time.Weekday(day).String()) + start, err := time.Parse(time.TimeOnly, restriction.StartTimeOfDay) + if err != nil { + return fmt.Errorf("parsing start time of day '%s': %w", restriction.StartTimeOfDay, err) + } + end := start.Add(time.Duration(restriction.DurationSeconds) * time.Second) + + r := store.InsertExtScheduleRestrictionParams{ + ScheduleID: s.ID, + RestrictionIndex: fmt.Sprintf("%d-%d", i, day), + StartTime: start.Format("15:04:05"), + EndTime: end.Format("15:04:05"), + StartDay: dayStr, + EndDay: dayStr, + } + if err := q.InsertExtScheduleRestriction(ctx, r); err != nil { + return fmt.Errorf("saving daily restriction: %w", err) + } + } + case "weekly_restriction": + start, err := time.Parse(time.TimeOnly, restriction.StartTimeOfDay) + if err != nil { + return fmt.Errorf("parsing start time of day '%s': %w", restriction.StartTimeOfDay, err) + } + // 0000-01-01 is a Saturday, so we need to adjust +1 such that when + // restriction.StartDayOfWeek is 0, it is a Sunday. + start = start.AddDate(0, 0, int(restriction.StartDayOfWeek)+1) + end := start.Add(time.Duration(restriction.DurationSeconds) * time.Second) + + r := store.InsertExtScheduleRestrictionParams{ + ScheduleID: s.ID, + RestrictionIndex: strconv.Itoa(i), + StartTime: start.Format(time.TimeOnly), + StartDay: strings.ToLower(start.Weekday().String()), + EndTime: end.Format(time.TimeOnly), + EndDay: strings.ToLower(end.Weekday().String()), + } + if err := q.InsertExtScheduleRestriction(ctx, r); err != nil { + return fmt.Errorf("saving weekly restriction: %w", err) + } + default: + console.Warnf("Unknown schedule restriction type '%s' for schedule '%s', skipping...\n", restriction.Type, s.ID) + } + } return nil } diff --git a/pager/testdata/TestPagerDuty/schedules.json b/pager/testdata/TestPagerDuty/schedules.json index 61d726a..cdbca72 100644 --- a/pager/testdata/TestPagerDuty/schedules.json +++ b/pager/testdata/TestPagerDuty/schedules.json @@ -54,9 +54,10 @@ "rendered_schedule_entries": [], "restrictions": [ { - "duration_seconds": 28800, + "type": "weekly_restriction", "start_time_of_day": "09:00:00", - "type": "daily_restriction" + "duration_seconds": 374400, + "start_day_of_week": 1 } ], "rotation_turn_length_seconds": 604800, diff --git a/store/models.go b/store/models.go index 86b96d6..b5d3c70 100644 --- a/store/models.go +++ b/store/models.go @@ -14,13 +14,14 @@ type ExtMembership struct { } type ExtSchedule struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Timezone string `json:"timezone"` - Strategy string `json:"strategy"` - HandoffTime string `json:"handoff_time"` - HandoffDay string `json:"handoff_day"` + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Timezone string `json:"timezone"` + Strategy string `json:"strategy"` + ShiftDuration string `json:"shift_duration"` + HandoffTime string `json:"handoff_time"` + HandoffDay string `json:"handoff_day"` } type ExtScheduleMember struct { @@ -28,6 +29,15 @@ type ExtScheduleMember struct { UserID string `json:"user_id"` } +type ExtScheduleRestriction struct { + ScheduleID string `json:"schedule_id"` + RestrictionIndex string `json:"restriction_index"` + StartTime string `json:"start_time"` + StartDay string `json:"start_day"` + EndTime string `json:"end_time"` + EndDay string `json:"end_day"` +} + type ExtScheduleTeam struct { ScheduleID string `json:"schedule_id"` TeamID string `json:"team_id"` diff --git a/store/queries.sql b/store/queries.sql index 1c49fa5..7e5b4d1 100644 --- a/store/queries.sql +++ b/store/queries.sql @@ -47,7 +47,13 @@ WHERE ext_teams.id = ?; SELECT * FROM ext_schedules; -- name: InsertExtSchedule :exec -INSERT INTO ext_schedules (id, name, description, timezone, strategy, handoff_time, handoff_day) VALUES (?, ?, ?, ?, ?, ?, ?); +INSERT INTO ext_schedules (id, name, description, timezone, strategy, shift_duration, handoff_time, handoff_day) VALUES (?, ?, ?, ?, ?, ?, ?, ?); + +-- name: ListExtScheduleRestrictionsByExtScheduleID :many +SELECT * FROM ext_schedule_restrictions WHERE schedule_id = ?; + +-- name: InsertExtScheduleRestriction :exec +INSERT INTO ext_schedule_restrictions (schedule_id, restriction_index, start_time, start_day, end_time, end_day) VALUES (?, ?, ?, ?, ?, ?); -- name: ListFhTeamsByExtScheduleID :many SELECT ext_teams.*, fh_teams.name as fh_team_name, fh_teams.slug as fh_team_slug FROM ext_schedule_teams diff --git a/store/queries.sql.go b/store/queries.sql.go index 590d930..b8632c8 100644 --- a/store/queries.sql.go +++ b/store/queries.sql.go @@ -47,17 +47,18 @@ func (q *Queries) InsertExtMembership(ctx context.Context, arg InsertExtMembersh } const insertExtSchedule = `-- name: InsertExtSchedule :exec -INSERT INTO ext_schedules (id, name, description, timezone, strategy, handoff_time, handoff_day) VALUES (?, ?, ?, ?, ?, ?, ?) +INSERT INTO ext_schedules (id, name, description, timezone, strategy, shift_duration, handoff_time, handoff_day) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ` type InsertExtScheduleParams struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Timezone string `json:"timezone"` - Strategy string `json:"strategy"` - HandoffTime string `json:"handoff_time"` - HandoffDay string `json:"handoff_day"` + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Timezone string `json:"timezone"` + Strategy string `json:"strategy"` + ShiftDuration string `json:"shift_duration"` + HandoffTime string `json:"handoff_time"` + HandoffDay string `json:"handoff_day"` } func (q *Queries) InsertExtSchedule(ctx context.Context, arg InsertExtScheduleParams) error { @@ -67,6 +68,7 @@ func (q *Queries) InsertExtSchedule(ctx context.Context, arg InsertExtSchedulePa arg.Description, arg.Timezone, arg.Strategy, + arg.ShiftDuration, arg.HandoffTime, arg.HandoffDay, ) @@ -87,6 +89,31 @@ func (q *Queries) InsertExtScheduleMember(ctx context.Context, arg InsertExtSche return err } +const insertExtScheduleRestriction = `-- name: InsertExtScheduleRestriction :exec +INSERT INTO ext_schedule_restrictions (schedule_id, restriction_index, start_time, start_day, end_time, end_day) VALUES (?, ?, ?, ?, ?, ?) +` + +type InsertExtScheduleRestrictionParams struct { + ScheduleID string `json:"schedule_id"` + RestrictionIndex string `json:"restriction_index"` + StartTime string `json:"start_time"` + StartDay string `json:"start_day"` + EndTime string `json:"end_time"` + EndDay string `json:"end_day"` +} + +func (q *Queries) InsertExtScheduleRestriction(ctx context.Context, arg InsertExtScheduleRestrictionParams) error { + _, err := q.db.ExecContext(ctx, insertExtScheduleRestriction, + arg.ScheduleID, + arg.RestrictionIndex, + arg.StartTime, + arg.StartDay, + arg.EndTime, + arg.EndDay, + ) + return err +} + const insertExtScheduleTeam = `-- name: InsertExtScheduleTeam :exec INSERT INTO ext_schedule_teams (schedule_id, team_id) VALUES (?, ?) ` @@ -201,8 +228,42 @@ func (q *Queries) LinkExtUser(ctx context.Context, arg LinkExtUserParams) error return err } +const listExtScheduleRestrictionsByExtScheduleID = `-- name: ListExtScheduleRestrictionsByExtScheduleID :many +SELECT schedule_id, restriction_index, start_time, start_day, end_time, end_day FROM ext_schedule_restrictions WHERE schedule_id = ? +` + +func (q *Queries) ListExtScheduleRestrictionsByExtScheduleID(ctx context.Context, scheduleID string) ([]ExtScheduleRestriction, error) { + rows, err := q.db.QueryContext(ctx, listExtScheduleRestrictionsByExtScheduleID, scheduleID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ExtScheduleRestriction + for rows.Next() { + var i ExtScheduleRestriction + if err := rows.Scan( + &i.ScheduleID, + &i.RestrictionIndex, + &i.StartTime, + &i.StartDay, + &i.EndTime, + &i.EndDay, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const listExtSchedules = `-- name: ListExtSchedules :many -SELECT id, name, description, timezone, strategy, handoff_time, handoff_day FROM ext_schedules +SELECT id, name, description, timezone, strategy, shift_duration, handoff_time, handoff_day FROM ext_schedules ` func (q *Queries) ListExtSchedules(ctx context.Context) ([]ExtSchedule, error) { @@ -220,6 +281,7 @@ func (q *Queries) ListExtSchedules(ctx context.Context) ([]ExtSchedule, error) { &i.Description, &i.Timezone, &i.Strategy, + &i.ShiftDuration, &i.HandoffTime, &i.HandoffDay, ); err != nil { diff --git a/store/schema.sql b/store/schema.sql index ed80753..c11a80f 100644 --- a/store/schema.sql +++ b/store/schema.sql @@ -38,10 +38,22 @@ CREATE TABLE IF NOT EXISTS ext_schedules ( description TEXT NOT NULL, timezone TEXT NOT NULL, strategy TEXT NOT NULL, + shift_duration TEXT NOT NULL, handoff_time TEXT NOT NULL, handoff_day TEXT NOT NULL ) STRICT; +CREATE TABLE IF NOT EXISTS ext_schedule_restrictions ( + schedule_id TEXT NOT NULL, + restriction_index TEXT NOT NULL, + start_time TEXT NOT NULL, + start_day TEXT NOT NULL, + end_time TEXT NOT NULL, + end_day TEXT NOT NULL, + PRIMARY KEY (schedule_id, restriction_index), + FOREIGN KEY (schedule_id) REFERENCES ext_schedules(id) +) STRICT; + CREATE TABLE IF NOT EXISTS ext_schedule_teams ( schedule_id TEXT NOT NULL, team_id TEXT NOT NULL, diff --git a/tfrender/testdata/TestRenderOnCallScheduleResource.golden.tf b/tfrender/testdata/TestRenderOnCallScheduleResource.golden.tf index 2f77936..de0d1a4 100644 --- a/tfrender/testdata/TestRenderOnCallScheduleResource.golden.tf +++ b/tfrender/testdata/TestRenderOnCallScheduleResource.golden.tf @@ -29,7 +29,7 @@ resource "firehydrant_team" "team_0_slug" { import { id = "id-for-team-0" - to = resource.firehydrant_team.team_0_slug.id + to = firehydrant_team.team_0_slug } resource "firehydrant_team" "team_1_slug" { @@ -42,17 +42,18 @@ resource "firehydrant_team" "team_2_slug" { import { id = "id-for-team-2" - to = resource.firehydrant_team.team_2_slug.id + to = firehydrant_team.team_2_slug } resource "firehydrant_team" "team_3_slug" { name = "Team 3" } -resource "firehydrant_on_call_schedule" "schedule_0" { +resource "firehydrant_on_call_schedule" "team_1_slug_schedule_0" { name = "Schedule 0" description = "Schedule 0 description" team_id = resource.firehydrant_team.team_1_slug.id + time_zone = "" member_ids = [data.firehydrant_user.user_0.id] diff --git a/tfrender/testdata/TestRenderTeamResource.golden.tf b/tfrender/testdata/TestRenderTeamResource.golden.tf index a2bf0f6..2c846fd 100644 --- a/tfrender/testdata/TestRenderTeamResource.golden.tf +++ b/tfrender/testdata/TestRenderTeamResource.golden.tf @@ -26,14 +26,14 @@ data "firehydrant_user" "user_3" { resource "firehydrant_team" "team_0_slug" { name = "Team 0" - membership { - id = data.firehydrant_user.user_0.id + memberships { + user_id = data.firehydrant_user.user_0.id } } import { id = "id-for-team-0" - to = resource.firehydrant_team.team_0_slug.id + to = firehydrant_team.team_0_slug } resource "firehydrant_team" "team_1_slug" { @@ -46,7 +46,7 @@ resource "firehydrant_team" "team_2_slug" { import { id = "id-for-team-2" - to = resource.firehydrant_team.team_2_slug.id + to = firehydrant_team.team_2_slug } resource "firehydrant_team" "team_3_slug" { diff --git a/tfrender/tfrender.go b/tfrender/tfrender.go index 6c4ad8c..3b5d148 100644 --- a/tfrender/tfrender.go +++ b/tfrender/tfrender.go @@ -119,7 +119,10 @@ func (r *TFRender) ResourceFireHydrantOnCallSchedule(ctx context.Context) error for _, t := range teams { r.root.AppendNewline() - b := r.root.AppendNewBlock("resource", []string{"firehydrant_on_call_schedule", s.TFSlug()}).Body() + b := r.root.AppendNewBlock("resource", []string{ + "firehydrant_on_call_schedule", + fmt.Sprintf("%s_%s", t.TFSlug(), s.TFSlug()), + }).Body() b.SetAttributeValue("name", cty.StringVal(s.Name)) if s.Description != "" { b.SetAttributeValue("description", cty.StringVal(s.Description)) @@ -156,6 +159,19 @@ func (r *TFRender) ResourceFireHydrantOnCallSchedule(ctx context.Context) error strategy.SetAttributeValue("type", cty.StringVal(s.Strategy)) strategy.SetAttributeValue("handoff_time", cty.StringVal(s.HandoffTime)) strategy.SetAttributeValue("handoff_day", cty.StringVal(s.HandoffDay)) + + restrictions, err := store.UseQueries(ctx).ListExtScheduleRestrictionsByExtScheduleID(ctx, s.ID) + if err != nil { + return fmt.Errorf("querying restrictions for schedule '%s': %w", s.Name, err) + } + for _, r := range restrictions { + b.AppendNewline() + restriction := b.AppendNewBlock("restrictions", []string{}).Body() + restriction.SetAttributeValue("start_day", cty.StringVal(r.StartDay)) + restriction.SetAttributeValue("start_time", cty.StringVal(r.StartTime)) + restriction.SetAttributeValue("end_day", cty.StringVal(r.EndDay)) + restriction.SetAttributeValue("end_time", cty.StringVal(r.EndTime)) + } } } return nil @@ -210,10 +226,8 @@ func (r *TFRender) ResourceFireHydrantTeams(ctx context.Context) error { importBody := r.root.AppendNewBlock("import", []string{}).Body() importBody.SetAttributeValue("id", cty.StringVal(t.FhTeamID.String)) importBody.SetAttributeTraversal("to", hcl.Traversal{ - hcl.TraverseRoot{Name: "resource"}, - hcl.TraverseAttr{Name: "firehydrant_team"}, + hcl.TraverseRoot{Name: "firehydrant_team"}, hcl.TraverseAttr{Name: tfSlug}, - hcl.TraverseAttr{Name: "id"}, }) importedTeams[t.FhTeamID.String] = true } From bd1f4f518fe1e4d94eea3f812ca55b7da774e486 Mon Sep 17 00:00:00 2001 From: "Wilson E. Husin" Date: Tue, 9 Apr 2024 16:39:08 -0700 Subject: [PATCH 04/14] tests --- pager/testdata/TestPagerDuty/teams.json | 18 +- .../TestRenderPagerDutySample.golden.tf | 167 ++++++++++++++++++ tfrender/testdata/pagerduty_seed.sql | 50 ++++++ tfrender/tfrender_pagerduty_test.go | 35 ++++ 4 files changed, 261 insertions(+), 9 deletions(-) create mode 100644 tfrender/testdata/TestRenderPagerDutySample.golden.tf create mode 100644 tfrender/testdata/pagerduty_seed.sql create mode 100644 tfrender/tfrender_pagerduty_test.go diff --git a/pager/testdata/TestPagerDuty/teams.json b/pager/testdata/TestPagerDuty/teams.json index a70a72b..32e54d4 100644 --- a/pager/testdata/TestPagerDuty/teams.json +++ b/pager/testdata/TestPagerDuty/teams.json @@ -6,7 +6,7 @@ { "default_role": "manager", "description": null, - "html_url": "https://firehydrant-eng.pagerduty.com/teams/PT54U20", + "html_url": "https://acme-inc.pagerduty.com/teams/PT54U20", "id": "PT54U20", "name": "Jen", "parent": null, @@ -17,7 +17,7 @@ { "default_role": "manager", "description": null, - "html_url": "https://firehydrant-eng.pagerduty.com/teams/PO206TE", + "html_url": "https://acme-inc.pagerduty.com/teams/PO206TE", "id": "PO206TE", "name": "canary-team", "parent": null, @@ -28,7 +28,7 @@ { "default_role": "manager", "description": null, - "html_url": "https://firehydrant-eng.pagerduty.com/teams/PO34CI9", + "html_url": "https://acme-inc.pagerduty.com/teams/PO34CI9", "id": "PO34CI9", "name": "CS-Team-Test", "parent": null, @@ -39,7 +39,7 @@ { "default_role": "manager", "description": null, - "html_url": "https://firehydrant-eng.pagerduty.com/teams/PEGBTKE", + "html_url": "https://acme-inc.pagerduty.com/teams/PEGBTKE", "id": "PEGBTKE", "name": "CS-Test-Alerting", "parent": null, @@ -50,7 +50,7 @@ { "default_role": "manager", "description": null, - "html_url": "https://firehydrant-eng.pagerduty.com/teams/P7E9UET", + "html_url": "https://acme-inc.pagerduty.com/teams/P7E9UET", "id": "P7E9UET", "name": "caroline-team-test", "parent": null, @@ -61,7 +61,7 @@ { "default_role": "manager", "description": null, - "html_url": "https://firehydrant-eng.pagerduty.com/teams/P7A9Q6H", + "html_url": "https://acme-inc.pagerduty.com/teams/P7A9Q6H", "id": "P7A9Q6H", "name": "operational-team-test", "parent": null, @@ -72,7 +72,7 @@ { "default_role": "manager", "description": null, - "html_url": "https://firehydrant-eng.pagerduty.com/teams/P5PH8KY", + "html_url": "https://acme-inc.pagerduty.com/teams/P5PH8KY", "id": "P5PH8KY", "name": "Page Responder Team", "parent": null, @@ -83,7 +83,7 @@ { "default_role": "manager", "description": null, - "html_url": "https://firehydrant-eng.pagerduty.com/teams/PV9JOXL", + "html_url": "https://acme-inc.pagerduty.com/teams/PV9JOXL", "id": "PV9JOXL", "name": "Service Catalog Team", "parent": null, @@ -94,7 +94,7 @@ { "default_role": "manager", "description": null, - "html_url": "https://firehydrant-eng.pagerduty.com/teams/PD2F80U", + "html_url": "https://acme-inc.pagerduty.com/teams/PD2F80U", "id": "PD2F80U", "name": "Jack Team", "parent": null, diff --git a/tfrender/testdata/TestRenderPagerDutySample.golden.tf b/tfrender/testdata/TestRenderPagerDutySample.golden.tf new file mode 100644 index 0000000..52d35b6 --- /dev/null +++ b/tfrender/testdata/TestRenderPagerDutySample.golden.tf @@ -0,0 +1,167 @@ +terraform { + required_providers { + firehydrant = { + source = "firehydrant/firehydrant" + version = ">= 0.7.1" + } + } +} + +data "firehydrant_user" "local" { + email = "local@example.io" +} + +data "firehydrant_user" "mika" { + email = "mika@example.com" +} + +data "firehydrant_user" "kiran" { + email = "kiran@example.com" +} + +data "firehydrant_user" "horse" { + email = "horse@example.com" +} + +data "firehydrant_user" "jack" { + email = "jack@example.com" +} + +data "firehydrant_user" "wong" { + email = "wong@example.com" +} + +resource "firehydrant_team" "aaaa_ipv6_migration_strategy" { + name = "AAAA IPv6 migration strategy" + + memberships { + user_id = data.firehydrant_user.local.id + } + + memberships { + user_id = data.firehydrant_user.kiran.id + } + + memberships { + user_id = data.firehydrant_user.wong.id + } +} + +import { + id = "47016143-6547-483a-b68a-5220b21681fd" + to = firehydrant_team.aaaa_ipv6_migration_strategy +} + +resource "firehydrant_team" "dunder_mifflin_scranton" { + name = "Dunder Mifflin Scranton" + + memberships { + user_id = data.firehydrant_user.jack.id + } +} + +import { + id = "97d539b0-47a5-44f6-81e6-b6fcd98f23ac" + to = firehydrant_team.dunder_mifflin_scranton +} + +resource "firehydrant_on_call_schedule" "aaaa_ipv6_migration_strategy_jen_primary_layer_2" { + name = "Jen - primary - Layer 2" + description = "(Layer 2)" + team_id = resource.firehydrant_team.aaaa_ipv6_migration_strategy.id + time_zone = "America/Los_Angeles" + + member_ids = [data.firehydrant_user.kiran.id] + + strategy { + type = "weekly" + handoff_time = "16:00" + handoff_day = "monday" + } + + restrictions { + start_day = "monday" + start_time = "09:00:00" + end_day = "friday" + end_time = "17:00:00" + } +} + +resource "firehydrant_on_call_schedule" "aaaa_ipv6_migration_strategy_jen_primary_layer_1" { + name = "Jen - primary - Layer 1" + description = "(Layer 1)" + team_id = resource.firehydrant_team.aaaa_ipv6_migration_strategy.id + time_zone = "America/Los_Angeles" + + member_ids = [data.firehydrant_user.wong.id, data.firehydrant_user.local.id] + + strategy { + type = "weekly" + handoff_time = "16:00" + handoff_day = "friday" + } + + restrictions { + start_day = "sunday" + start_time = "09:00:00" + end_day = "sunday" + end_time = "17:00:00" + } + + restrictions { + start_day = "monday" + start_time = "09:00:00" + end_day = "monday" + end_time = "17:00:00" + } + + restrictions { + start_day = "tuesday" + start_time = "09:00:00" + end_day = "tuesday" + end_time = "17:00:00" + } + + restrictions { + start_day = "wednesday" + start_time = "09:00:00" + end_day = "wednesday" + end_time = "17:00:00" + } + + restrictions { + start_day = "thursday" + start_time = "09:00:00" + end_day = "thursday" + end_time = "17:00:00" + } + + restrictions { + start_day = "friday" + start_time = "09:00:00" + end_day = "friday" + end_time = "17:00:00" + } + + restrictions { + start_day = "saturday" + start_time = "09:00:00" + end_day = "saturday" + end_time = "17:00:00" + } +} + +resource "firehydrant_on_call_schedule" "dunder_mifflin_scranton_jack_on_call_schedule_layer_1" { + name = "Jack On-Call Schedule - Layer 1" + description = " (Layer 1)" + team_id = resource.firehydrant_team.dunder_mifflin_scranton.id + time_zone = "America/Los_Angeles" + + member_ids = [data.firehydrant_user.jack.id] + + strategy { + type = "weekly" + handoff_time = "14:00" + handoff_day = "friday" + } +} diff --git a/tfrender/testdata/pagerduty_seed.sql b/tfrender/testdata/pagerduty_seed.sql new file mode 100644 index 0000000..6238f20 --- /dev/null +++ b/tfrender/testdata/pagerduty_seed.sql @@ -0,0 +1,50 @@ +INSERT INTO fh_users VALUES('0946be55-ea20-4483-b9ab-617d5f0969e2','Admin Account','local@example.io'); +INSERT INTO fh_users VALUES('e6009411-0015-43e3-815e-ca9db72f4088','Mika','mika@example.com'); +INSERT INTO fh_users VALUES('4c3f28fa-b402-453c-9652-f014ecbe65a9', 'Kiran','kiran@example.com'); +INSERT INTO fh_users VALUES('35b5390f-d134-4bc6-966d-0b4048788b62','Horse','horse@example.com'); +INSERT INTO fh_users VALUES('6c08bff2-98f6-4ee9-8de1-12202186d084','Jack T.','jack@example.com'); +INSERT INTO fh_users VALUES('032a1f07-987e-4f76-8273-136e08e50baa', 'Wong','wong@example.com'); + +INSERT INTO ext_users VALUES('PXI6XNI','Admin','local@example.io','0946be55-ea20-4483-b9ab-617d5f0969e2'); +INSERT INTO ext_users VALUES('P5A1XH2','Mika','mika@example.io','e6009411-0015-43e3-815e-ca9db72f4088'); +INSERT INTO ext_users VALUES('P8ZZ1ZB','Kiran','kiran@example.io','4c3f28fa-b402-453c-9652-f014ecbe65a9'); +INSERT INTO ext_users VALUES('PRXEEQ8','Horse','horse@example.io','35b5390f-d134-4bc6-966d-0b4048788b62'); +INSERT INTO ext_users VALUES('P4CMCAU','Jack T.','jack@example.io','6c08bff2-98f6-4ee9-8de1-12202186d084'); +INSERT INTO ext_users VALUES('P2C9LBA','Wong','wong@example.io','032a1f07-987e-4f76-8273-136e08e50baa'); + +INSERT INTO fh_teams VALUES('47016143-6547-483a-b68a-5220b21681fd','AAAA IPv6 migration strategy','aaaa-ipv6-migration-strategy'); +INSERT INTO fh_teams VALUES('f159b173-1ffd-41ac-9254-ce8ec1142267','🐴 Cowboy Coders','cowboy-coders'); +INSERT INTO fh_teams VALUES('97d539b0-47a5-44f6-81e6-b6fcd98f23ac','Dunder Mifflin Scranton','dunder-mifflin-scranton'); + +INSERT INTO ext_teams VALUES('PT54U20','Jen','jen','47016143-6547-483a-b68a-5220b21681fd'); +INSERT INTO ext_teams VALUES('PD2F80U','Jack Team','jack-team','97d539b0-47a5-44f6-81e6-b6fcd98f23ac'); + +INSERT INTO ext_memberships VALUES('PXI6XNI','PT54U20'); +INSERT INTO ext_memberships VALUES('P8ZZ1ZB','PT54U20'); +INSERT INTO ext_memberships VALUES('P2C9LBA','PT54U20'); +INSERT INTO ext_memberships VALUES('P4CMCAU','PD2F80U'); + +INSERT INTO ext_schedules VALUES('P3D7DLW-PC1DX4O','Jen - primary - Layer 2','(Layer 2)','America/Los_Angeles','weekly','','16:00','monday'); +INSERT INTO ext_schedules VALUES('P3D7DLW-PSQ0VRL','Jen - primary - Layer 1','(Layer 1)','America/Los_Angeles','weekly','','16:00','friday'); +INSERT INTO ext_schedules VALUES('PVJMUIC-P3BRVNT','CS-on-call - Layer 1',' (Layer 1)','America/Los_Angeles','daily','','10:00','tuesday'); +INSERT INTO ext_schedules VALUES('P85QTXZ-PE2BA4Y','Jack On-Call Schedule - Layer 1',' (Layer 1)','America/Los_Angeles','weekly','','14:00','friday'); +INSERT INTO ext_schedules VALUES('PGR96WL-PR3J6XJ','🐴 is always on call - Layer 1','(Layer 1)','America/Los_Angeles','weekly','','12:00','friday'); + +INSERT INTO ext_schedule_restrictions VALUES('P3D7DLW-PC1DX4O','0','09:00:00','monday','17:00:00','friday'); +INSERT INTO ext_schedule_restrictions VALUES('P3D7DLW-PSQ0VRL','0-0','09:00:00','sunday','17:00:00','sunday'); +INSERT INTO ext_schedule_restrictions VALUES('P3D7DLW-PSQ0VRL','0-1','09:00:00','monday','17:00:00','monday'); +INSERT INTO ext_schedule_restrictions VALUES('P3D7DLW-PSQ0VRL','0-2','09:00:00','tuesday','17:00:00','tuesday'); +INSERT INTO ext_schedule_restrictions VALUES('P3D7DLW-PSQ0VRL','0-3','09:00:00','wednesday','17:00:00','wednesday'); +INSERT INTO ext_schedule_restrictions VALUES('P3D7DLW-PSQ0VRL','0-4','09:00:00','thursday','17:00:00','thursday'); +INSERT INTO ext_schedule_restrictions VALUES('P3D7DLW-PSQ0VRL','0-5','09:00:00','friday','17:00:00','friday'); +INSERT INTO ext_schedule_restrictions VALUES('P3D7DLW-PSQ0VRL','0-6','09:00:00','saturday','17:00:00','saturday'); + +INSERT INTO ext_schedule_teams VALUES('P3D7DLW-PC1DX4O','PT54U20'); +INSERT INTO ext_schedule_teams VALUES('P3D7DLW-PSQ0VRL','PT54U20'); +INSERT INTO ext_schedule_teams VALUES('P85QTXZ-PE2BA4Y','PD2F80U'); + +INSERT INTO ext_schedule_members VALUES('P3D7DLW-PC1DX4O','P8ZZ1ZB'); +INSERT INTO ext_schedule_members VALUES('P3D7DLW-PSQ0VRL','PXI6XNI'); +INSERT INTO ext_schedule_members VALUES('P3D7DLW-PSQ0VRL','P2C9LBA'); +INSERT INTO ext_schedule_members VALUES('P85QTXZ-PE2BA4Y','P4CMCAU'); +INSERT INTO ext_schedule_members VALUES('PGR96WL-PR3J6XJ','PRXEEQ8'); diff --git a/tfrender/tfrender_pagerduty_test.go b/tfrender/tfrender_pagerduty_test.go new file mode 100644 index 0000000..080b7b2 --- /dev/null +++ b/tfrender/tfrender_pagerduty_test.go @@ -0,0 +1,35 @@ +package tfrender_test + +import ( + "os" + "strings" + "testing" + + "github.com/firehydrant/signals-migrator/store" + "gotest.tools/golden" +) + +func TestRenderPagerDutySample(t *testing.T) { + ctx, tfr := tfrInit(t) + + seed, err := os.ReadFile("testdata/pagerduty_seed.sql") + if err != nil { + t.Fatal(err) + } + + sql := strings.TrimSpace(string(seed)) + + if _, err := store.FromContext(ctx).ExecContext(ctx, sql); err != nil { + t.Fatal(err) + } + if err := tfr.Write(ctx); err != nil { + t.Fatal(err) + } + + content, err := os.ReadFile(tfr.Filepath()) + if err != nil { + t.Fatal(err) + } + + golden.Assert(t, string(content), goldenFile(tfr.Filename())) +} From c491b6930dcaa2594c08fd96e8471db3b85b7ebc Mon Sep 17 00:00:00 2001 From: "Wilson E. Husin" Date: Tue, 9 Apr 2024 16:39:43 -0700 Subject: [PATCH 05/14] trimspace --- pager/pagerduty.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pager/pagerduty.go b/pager/pagerduty.go index 2903b32..aada532 100644 --- a/pager/pagerduty.go +++ b/pager/pagerduty.go @@ -71,6 +71,7 @@ func (p *PagerDuty) saveLayerToDB(ctx context.Context, schedule pagerduty.Schedu if schedule.Description == "" { desc = schedule.Description + " " + desc } + desc = strings.TrimSpace(desc) s := store.InsertExtScheduleParams{ ID: schedule.ID + "-" + layer.ID, Name: schedule.Name + " - " + layer.Name, From 3bf1a6ff82f7c5b12ddf59dc51847763f0777707 Mon Sep 17 00:00:00 2001 From: "Wilson E. Husin" Date: Tue, 9 Apr 2024 16:40:24 -0700 Subject: [PATCH 06/14] simplify desc --- pager/pagerduty.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pager/pagerduty.go b/pager/pagerduty.go index aada532..f47ad43 100644 --- a/pager/pagerduty.go +++ b/pager/pagerduty.go @@ -67,11 +67,9 @@ func (p *PagerDuty) saveScheduleToDB(ctx context.Context, schedule pagerduty.Sch } func (p *PagerDuty) saveLayerToDB(ctx context.Context, schedule pagerduty.Schedule, layer pagerduty.ScheduleLayer) error { - desc := fmt.Sprintf("(%s)", layer.Name) - if schedule.Description == "" { - desc = schedule.Description + " " + desc - } + desc := fmt.Sprintf("%s (%s)", schedule.Description, layer.Name) desc = strings.TrimSpace(desc) + s := store.InsertExtScheduleParams{ ID: schedule.ID + "-" + layer.ID, Name: schedule.Name + " - " + layer.Name, From e00fe7f3844f0e2d29e6905a31fd9c5d35e557c2 Mon Sep 17 00:00:00 2001 From: "Wilson E. Husin" Date: Tue, 9 Apr 2024 16:42:49 -0700 Subject: [PATCH 07/14] tfrender: test with timezone --- tfrender/testdata/TestRenderOnCallScheduleResource.golden.tf | 2 +- tfrender/tfrender_test.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tfrender/testdata/TestRenderOnCallScheduleResource.golden.tf b/tfrender/testdata/TestRenderOnCallScheduleResource.golden.tf index de0d1a4..52ad4ab 100644 --- a/tfrender/testdata/TestRenderOnCallScheduleResource.golden.tf +++ b/tfrender/testdata/TestRenderOnCallScheduleResource.golden.tf @@ -53,7 +53,7 @@ resource "firehydrant_on_call_schedule" "team_1_slug_schedule_0" { name = "Schedule 0" description = "Schedule 0 description" team_id = resource.firehydrant_team.team_1_slug.id - time_zone = "" + time_zone = "UTC" member_ids = [data.firehydrant_user.user_0.id] diff --git a/tfrender/tfrender_test.go b/tfrender/tfrender_test.go index a4b0e48..23a224a 100644 --- a/tfrender/tfrender_test.go +++ b/tfrender/tfrender_test.go @@ -153,6 +153,7 @@ func TestRenderOnCallScheduleResource(t *testing.T) { HandoffTime: "11:00", HandoffDay: "wednesday", Strategy: "daily", + Timezone: "UTC", }); err != nil { t.Fatal(err) } From 6b98410d28d135fef57425a261f9f97f0499c380 Mon Sep 17 00:00:00 2001 From: "Wilson E. Husin" Date: Wed, 10 Apr 2024 09:14:25 -0700 Subject: [PATCH 08/14] use proper time formatter --- pager/pagerduty.go | 2 +- tfrender/testdata/pagerduty_seed.sql | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pager/pagerduty.go b/pager/pagerduty.go index f47ad43..ba491b9 100644 --- a/pager/pagerduty.go +++ b/pager/pagerduty.go @@ -99,7 +99,7 @@ func (p *PagerDuty) saveLayerToDB(ctx context.Context, schedule pagerduty.Schedu } virtualStart, err := time.Parse(time.RFC3339, layer.RotationVirtualStart) if err == nil { - s.HandoffTime = fmt.Sprintf("%02d:%02d", virtualStart.Hour(), virtualStart.Minute()) + s.HandoffTime = virtualStart.Format(time.TimeOnly) s.HandoffDay = strings.ToLower(virtualStart.Weekday().String()) } else { console.Errorf("unable to parse virtual start time '%v', assuming default values", layer.RotationVirtualStart) diff --git a/tfrender/testdata/pagerduty_seed.sql b/tfrender/testdata/pagerduty_seed.sql index 6238f20..73d4c63 100644 --- a/tfrender/testdata/pagerduty_seed.sql +++ b/tfrender/testdata/pagerduty_seed.sql @@ -24,11 +24,11 @@ INSERT INTO ext_memberships VALUES('P8ZZ1ZB','PT54U20'); INSERT INTO ext_memberships VALUES('P2C9LBA','PT54U20'); INSERT INTO ext_memberships VALUES('P4CMCAU','PD2F80U'); -INSERT INTO ext_schedules VALUES('P3D7DLW-PC1DX4O','Jen - primary - Layer 2','(Layer 2)','America/Los_Angeles','weekly','','16:00','monday'); -INSERT INTO ext_schedules VALUES('P3D7DLW-PSQ0VRL','Jen - primary - Layer 1','(Layer 1)','America/Los_Angeles','weekly','','16:00','friday'); -INSERT INTO ext_schedules VALUES('PVJMUIC-P3BRVNT','CS-on-call - Layer 1',' (Layer 1)','America/Los_Angeles','daily','','10:00','tuesday'); -INSERT INTO ext_schedules VALUES('P85QTXZ-PE2BA4Y','Jack On-Call Schedule - Layer 1',' (Layer 1)','America/Los_Angeles','weekly','','14:00','friday'); -INSERT INTO ext_schedules VALUES('PGR96WL-PR3J6XJ','🐴 is always on call - Layer 1','(Layer 1)','America/Los_Angeles','weekly','','12:00','friday'); +INSERT INTO ext_schedules VALUES('P3D7DLW-PC1DX4O','Jen - primary - Layer 2','(Layer 2)','America/Los_Angeles','weekly','','16:00:00','monday'); +INSERT INTO ext_schedules VALUES('P3D7DLW-PSQ0VRL','Jen - primary - Layer 1','(Layer 1)','America/Los_Angeles','weekly','','16:00:00','friday'); +INSERT INTO ext_schedules VALUES('PVJMUIC-P3BRVNT','CS-on-call - Layer 1',' (Layer 1)','America/Los_Angeles','daily','','10:00:00','tuesday'); +INSERT INTO ext_schedules VALUES('P85QTXZ-PE2BA4Y','Jack On-Call Schedule - Layer 1',' (Layer 1)','America/Los_Angeles','weekly','','14:00:00','friday'); +INSERT INTO ext_schedules VALUES('PGR96WL-PR3J6XJ','🐴 is always on call - Layer 1','(Layer 1)','America/Los_Angeles','weekly','','12:00:00','friday'); INSERT INTO ext_schedule_restrictions VALUES('P3D7DLW-PC1DX4O','0','09:00:00','monday','17:00:00','friday'); INSERT INTO ext_schedule_restrictions VALUES('P3D7DLW-PSQ0VRL','0-0','09:00:00','sunday','17:00:00','sunday'); From 42a80439910607d60b5cea13440b54f75d396878 Mon Sep 17 00:00:00 2001 From: "Wilson E. Husin" Date: Wed, 10 Apr 2024 09:18:53 -0700 Subject: [PATCH 09/14] README: update roadmap --- readme.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/readme.md b/readme.md index f4519b7..e727d1c 100644 --- a/readme.md +++ b/readme.md @@ -37,11 +37,14 @@ Afterwards, the tool will generate the mapping appropriately, handling de-duplic ## Feature roadmap -- [x] Importing users -- [x] Importing teams and members -- [ ] Pre-create a default escalation policy -- [ ] Import scheduling strategy -- [ ] Pre-create scheduling strategy +| | PagerDuty | VictorOps | OpsGenie | +| --- | --- | --- | --- | +| Import users | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| Import teams and members | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| Import escalation policies | :x: | :x: | :x: | +| Import scheduling strategy | :heavy_check_mark: | :x: | :x: | + + - [ ] Getting transposer URLs (e.g. Datadog) to the team data resource or a Signals ingest URL data resource - [ ] Support for importing escalation policies - [ ] Auto-run `terraform apply` for users who would not manage their organization with Terraform after importing From d72166975b14a2b4895131ec498f77099f5031eb Mon Sep 17 00:00:00 2001 From: "Wilson E. Husin" Date: Wed, 10 Apr 2024 09:21:50 -0700 Subject: [PATCH 10/14] More contrast --- readme.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index e727d1c..3e31bf8 100644 --- a/readme.md +++ b/readme.md @@ -5,6 +5,7 @@ This can be used to import resources from legacy alerting providers into Signals ## Supported providers ### Alerting + - PagerDuty - VictorOps - OpsGenie @@ -39,10 +40,10 @@ Afterwards, the tool will generate the mapping appropriately, handling de-duplic | | PagerDuty | VictorOps | OpsGenie | | --- | --- | --- | --- | -| Import users | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| Import teams and members | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| Import users | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Import teams and members | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Import escalation policies | :x: | :x: | :x: | -| Import scheduling strategy | :heavy_check_mark: | :x: | :x: | +| Import scheduling strategy | :white_check_mark: | :x: | :x: | - [ ] Getting transposer URLs (e.g. Datadog) to the team data resource or a Signals ingest URL data resource From c47b2fff55d9be412d7e5a0003cdd784292a5891 Mon Sep 17 00:00:00 2001 From: "Wilson E. Husin" Date: Wed, 10 Apr 2024 09:24:27 -0700 Subject: [PATCH 11/14] update golden --- tfrender/testdata/TestRenderPagerDutySample.golden.tf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tfrender/testdata/TestRenderPagerDutySample.golden.tf b/tfrender/testdata/TestRenderPagerDutySample.golden.tf index 52d35b6..9a608d8 100644 --- a/tfrender/testdata/TestRenderPagerDutySample.golden.tf +++ b/tfrender/testdata/TestRenderPagerDutySample.golden.tf @@ -75,7 +75,7 @@ resource "firehydrant_on_call_schedule" "aaaa_ipv6_migration_strategy_jen_primar strategy { type = "weekly" - handoff_time = "16:00" + handoff_time = "16:00:00" handoff_day = "monday" } @@ -97,7 +97,7 @@ resource "firehydrant_on_call_schedule" "aaaa_ipv6_migration_strategy_jen_primar strategy { type = "weekly" - handoff_time = "16:00" + handoff_time = "16:00:00" handoff_day = "friday" } @@ -161,7 +161,7 @@ resource "firehydrant_on_call_schedule" "dunder_mifflin_scranton_jack_on_call_sc strategy { type = "weekly" - handoff_time = "14:00" + handoff_time = "14:00:00" handoff_day = "friday" } } From 57dcef74ce5cca2a2b745bc641133bd6a45a3c26 Mon Sep 17 00:00:00 2001 From: "Wilson E. Husin" Date: Wed, 10 Apr 2024 20:22:48 -0700 Subject: [PATCH 12/14] pagerduty: support real custom shift duration --- pager/pagerduty.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pager/pagerduty.go b/pager/pagerduty.go index ba491b9..e6bc8d6 100644 --- a/pager/pagerduty.go +++ b/pager/pagerduty.go @@ -89,13 +89,8 @@ func (p *PagerDuty) saveLayerToDB(ctx context.Context, schedule pagerduty.Schedu case 60 * 60 * 24 * 7: s.Strategy = "weekly" default: - console.Warnf("Found custom shift duration '%d seconds' for schedule '%s', rounding to daily value.\n", layer.RotationTurnLengthSeconds, s.Name) - if layer.RotationTurnLengthSeconds < 60*60*24 { - s.Strategy = "daily" - } else { - s.Strategy = "custom" - s.ShiftDuration = fmt.Sprintf("P%dD", layer.RotationTurnLengthSeconds/60/60/24) - } + s.Strategy = "custom" + s.ShiftDuration = fmt.Sprintf("PT%dS", layer.RotationTurnLengthSeconds) } virtualStart, err := time.Parse(time.RFC3339, layer.RotationVirtualStart) if err == nil { From 9c643ee47d3c94bbf2c63d1dbf4900c3d3666dd8 Mon Sep 17 00:00:00 2001 From: "Wilson E. Husin" Date: Wed, 10 Apr 2024 20:43:04 -0700 Subject: [PATCH 13/14] Support true custom shift duration --- pager/pagerduty.go | 23 +++++++++++++------ store/models.go | 1 + store/queries.sql | 2 +- store/queries.sql.go | 7 ++++-- store/schema.sql | 1 + ...TestRenderOnCallScheduleResource.golden.tf | 1 - .../TestRenderPagerDutySample.golden.tf | 14 +++++------ tfrender/testdata/pagerduty_seed.sql | 10 ++++---- tfrender/tfrender.go | 13 +++++++++-- 9 files changed, 47 insertions(+), 25 deletions(-) diff --git a/pager/pagerduty.go b/pager/pagerduty.go index e6bc8d6..ca27069 100644 --- a/pager/pagerduty.go +++ b/pager/pagerduty.go @@ -91,6 +91,15 @@ func (p *PagerDuty) saveLayerToDB(ctx context.Context, schedule pagerduty.Schedu default: s.Strategy = "custom" s.ShiftDuration = fmt.Sprintf("PT%dS", layer.RotationTurnLengthSeconds) + + now := time.Now() + loc, err := time.LoadLocation(schedule.TimeZone) + if err == nil { + now = now.In(loc) + } else { + console.Warnf("unable to parse timezone '%v', using current machine's local time", schedule.TimeZone) + } + s.StartTime = now.Format(time.RFC3339) } virtualStart, err := time.Parse(time.RFC3339, layer.RotationVirtualStart) if err == nil { @@ -139,20 +148,20 @@ func (p *PagerDuty) saveLayerToDB(ctx context.Context, schedule pagerduty.Schedu switch restriction.Type { case "daily_restriction": for day := range 7 { - dayStr := strings.ToLower(time.Weekday(day).String()) start, err := time.Parse(time.TimeOnly, restriction.StartTimeOfDay) if err != nil { return fmt.Errorf("parsing start time of day '%s': %w", restriction.StartTimeOfDay, err) } end := start.Add(time.Duration(restriction.DurationSeconds) * time.Second) + dayStr := strings.ToLower(time.Weekday(day).String()) r := store.InsertExtScheduleRestrictionParams{ ScheduleID: s.ID, RestrictionIndex: fmt.Sprintf("%d-%d", i, day), - StartTime: start.Format("15:04:05"), - EndTime: end.Format("15:04:05"), StartDay: dayStr, EndDay: dayStr, + StartTime: start.Format(time.TimeOnly), + EndTime: end.Format(time.TimeOnly), } if err := q.InsertExtScheduleRestriction(ctx, r); err != nil { return fmt.Errorf("saving daily restriction: %w", err) @@ -164,17 +173,17 @@ func (p *PagerDuty) saveLayerToDB(ctx context.Context, schedule pagerduty.Schedu return fmt.Errorf("parsing start time of day '%s': %w", restriction.StartTimeOfDay, err) } // 0000-01-01 is a Saturday, so we need to adjust +1 such that when - // restriction.StartDayOfWeek is 0, it is a Sunday. - start = start.AddDate(0, 0, int(restriction.StartDayOfWeek)+1) + // restriction.StartDayOfWeek is 0, it yields Sunday. + start = start.AddDate(0, 0, int(restriction.StartDayOfWeek+1)) end := start.Add(time.Duration(restriction.DurationSeconds) * time.Second) r := store.InsertExtScheduleRestrictionParams{ ScheduleID: s.ID, RestrictionIndex: strconv.Itoa(i), - StartTime: start.Format(time.TimeOnly), StartDay: strings.ToLower(start.Weekday().String()), - EndTime: end.Format(time.TimeOnly), EndDay: strings.ToLower(end.Weekday().String()), + StartTime: start.Format(time.TimeOnly), + EndTime: end.Format(time.TimeOnly), } if err := q.InsertExtScheduleRestriction(ctx, r); err != nil { return fmt.Errorf("saving weekly restriction: %w", err) diff --git a/store/models.go b/store/models.go index b5d3c70..a292ae0 100644 --- a/store/models.go +++ b/store/models.go @@ -20,6 +20,7 @@ type ExtSchedule struct { Timezone string `json:"timezone"` Strategy string `json:"strategy"` ShiftDuration string `json:"shift_duration"` + StartTime string `json:"start_time"` HandoffTime string `json:"handoff_time"` HandoffDay string `json:"handoff_day"` } diff --git a/store/queries.sql b/store/queries.sql index 7e5b4d1..626bd0c 100644 --- a/store/queries.sql +++ b/store/queries.sql @@ -47,7 +47,7 @@ WHERE ext_teams.id = ?; SELECT * FROM ext_schedules; -- name: InsertExtSchedule :exec -INSERT INTO ext_schedules (id, name, description, timezone, strategy, shift_duration, handoff_time, handoff_day) VALUES (?, ?, ?, ?, ?, ?, ?, ?); +INSERT INTO ext_schedules (id, name, description, timezone, strategy, shift_duration, start_time, handoff_time, handoff_day) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); -- name: ListExtScheduleRestrictionsByExtScheduleID :many SELECT * FROM ext_schedule_restrictions WHERE schedule_id = ?; diff --git a/store/queries.sql.go b/store/queries.sql.go index b8632c8..a07ff6f 100644 --- a/store/queries.sql.go +++ b/store/queries.sql.go @@ -47,7 +47,7 @@ func (q *Queries) InsertExtMembership(ctx context.Context, arg InsertExtMembersh } const insertExtSchedule = `-- name: InsertExtSchedule :exec -INSERT INTO ext_schedules (id, name, description, timezone, strategy, shift_duration, handoff_time, handoff_day) VALUES (?, ?, ?, ?, ?, ?, ?, ?) +INSERT INTO ext_schedules (id, name, description, timezone, strategy, shift_duration, start_time, handoff_time, handoff_day) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ` type InsertExtScheduleParams struct { @@ -57,6 +57,7 @@ type InsertExtScheduleParams struct { Timezone string `json:"timezone"` Strategy string `json:"strategy"` ShiftDuration string `json:"shift_duration"` + StartTime string `json:"start_time"` HandoffTime string `json:"handoff_time"` HandoffDay string `json:"handoff_day"` } @@ -69,6 +70,7 @@ func (q *Queries) InsertExtSchedule(ctx context.Context, arg InsertExtSchedulePa arg.Timezone, arg.Strategy, arg.ShiftDuration, + arg.StartTime, arg.HandoffTime, arg.HandoffDay, ) @@ -263,7 +265,7 @@ func (q *Queries) ListExtScheduleRestrictionsByExtScheduleID(ctx context.Context } const listExtSchedules = `-- name: ListExtSchedules :many -SELECT id, name, description, timezone, strategy, shift_duration, handoff_time, handoff_day FROM ext_schedules +SELECT id, name, description, timezone, strategy, shift_duration, start_time, handoff_time, handoff_day FROM ext_schedules ` func (q *Queries) ListExtSchedules(ctx context.Context) ([]ExtSchedule, error) { @@ -282,6 +284,7 @@ func (q *Queries) ListExtSchedules(ctx context.Context) ([]ExtSchedule, error) { &i.Timezone, &i.Strategy, &i.ShiftDuration, + &i.StartTime, &i.HandoffTime, &i.HandoffDay, ); err != nil { diff --git a/store/schema.sql b/store/schema.sql index c11a80f..98a0f54 100644 --- a/store/schema.sql +++ b/store/schema.sql @@ -39,6 +39,7 @@ CREATE TABLE IF NOT EXISTS ext_schedules ( timezone TEXT NOT NULL, strategy TEXT NOT NULL, shift_duration TEXT NOT NULL, + start_time TEXT NOT NULL, handoff_time TEXT NOT NULL, handoff_day TEXT NOT NULL ) STRICT; diff --git a/tfrender/testdata/TestRenderOnCallScheduleResource.golden.tf b/tfrender/testdata/TestRenderOnCallScheduleResource.golden.tf index 52ad4ab..f34d470 100644 --- a/tfrender/testdata/TestRenderOnCallScheduleResource.golden.tf +++ b/tfrender/testdata/TestRenderOnCallScheduleResource.golden.tf @@ -60,6 +60,5 @@ resource "firehydrant_on_call_schedule" "team_1_slug_schedule_0" { strategy { type = "daily" handoff_time = "11:00" - handoff_day = "wednesday" } } diff --git a/tfrender/testdata/TestRenderPagerDutySample.golden.tf b/tfrender/testdata/TestRenderPagerDutySample.golden.tf index 9a608d8..fad6b7b 100644 --- a/tfrender/testdata/TestRenderPagerDutySample.golden.tf +++ b/tfrender/testdata/TestRenderPagerDutySample.golden.tf @@ -70,13 +70,13 @@ resource "firehydrant_on_call_schedule" "aaaa_ipv6_migration_strategy_jen_primar description = "(Layer 2)" team_id = resource.firehydrant_team.aaaa_ipv6_migration_strategy.id time_zone = "America/Los_Angeles" + start_time = "2024-04-10T20:39:29-07:00" member_ids = [data.firehydrant_user.kiran.id] strategy { - type = "weekly" - handoff_time = "16:00:00" - handoff_day = "monday" + type = "custom" + shift_duration = "PT93600S" } restrictions { @@ -92,13 +92,13 @@ resource "firehydrant_on_call_schedule" "aaaa_ipv6_migration_strategy_jen_primar description = "(Layer 1)" team_id = resource.firehydrant_team.aaaa_ipv6_migration_strategy.id time_zone = "America/Los_Angeles" + start_time = "2024-04-10T20:39:29-07:00" member_ids = [data.firehydrant_user.wong.id, data.firehydrant_user.local.id] strategy { - type = "weekly" - handoff_time = "16:00:00" - handoff_day = "friday" + type = "custom" + shift_duration = "PT7200S" } restrictions { @@ -161,7 +161,7 @@ resource "firehydrant_on_call_schedule" "dunder_mifflin_scranton_jack_on_call_sc strategy { type = "weekly" - handoff_time = "14:00:00" handoff_day = "friday" + handoff_time = "14:00:00" } } diff --git a/tfrender/testdata/pagerduty_seed.sql b/tfrender/testdata/pagerduty_seed.sql index 73d4c63..d87ad65 100644 --- a/tfrender/testdata/pagerduty_seed.sql +++ b/tfrender/testdata/pagerduty_seed.sql @@ -24,11 +24,11 @@ INSERT INTO ext_memberships VALUES('P8ZZ1ZB','PT54U20'); INSERT INTO ext_memberships VALUES('P2C9LBA','PT54U20'); INSERT INTO ext_memberships VALUES('P4CMCAU','PD2F80U'); -INSERT INTO ext_schedules VALUES('P3D7DLW-PC1DX4O','Jen - primary - Layer 2','(Layer 2)','America/Los_Angeles','weekly','','16:00:00','monday'); -INSERT INTO ext_schedules VALUES('P3D7DLW-PSQ0VRL','Jen - primary - Layer 1','(Layer 1)','America/Los_Angeles','weekly','','16:00:00','friday'); -INSERT INTO ext_schedules VALUES('PVJMUIC-P3BRVNT','CS-on-call - Layer 1',' (Layer 1)','America/Los_Angeles','daily','','10:00:00','tuesday'); -INSERT INTO ext_schedules VALUES('P85QTXZ-PE2BA4Y','Jack On-Call Schedule - Layer 1',' (Layer 1)','America/Los_Angeles','weekly','','14:00:00','friday'); -INSERT INTO ext_schedules VALUES('PGR96WL-PR3J6XJ','🐴 is always on call - Layer 1','(Layer 1)','America/Los_Angeles','weekly','','12:00:00','friday'); +INSERT INTO ext_schedules VALUES('P3D7DLW-PC1DX4O','Jen - primary - Layer 2','(Layer 2)','America/Los_Angeles','custom','PT93600S','2024-04-10T20:39:29-07:00','16:00:00','wednesday'); +INSERT INTO ext_schedules VALUES('P3D7DLW-PSQ0VRL','Jen - primary - Layer 1','(Layer 1)','America/Los_Angeles','custom','PT7200S','2024-04-10T20:39:29-07:00','16:00:00','wednesday'); +INSERT INTO ext_schedules VALUES('PVJMUIC-P3BRVNT','CS-on-call - Layer 1',' (Layer 1)','America/Los_Angeles','daily','','','10:00:00','tuesday'); +INSERT INTO ext_schedules VALUES('P85QTXZ-PE2BA4Y','Jack On-Call Schedule - Layer 1',' (Layer 1)','America/Los_Angeles','weekly','','','14:00:00','friday'); +INSERT INTO ext_schedules VALUES('PGR96WL-PR3J6XJ','🐴 is always on call - Layer 1','(Layer 1)','America/Los_Angeles','weekly','','','12:00:00','friday'); INSERT INTO ext_schedule_restrictions VALUES('P3D7DLW-PC1DX4O','0','09:00:00','monday','17:00:00','friday'); INSERT INTO ext_schedule_restrictions VALUES('P3D7DLW-PSQ0VRL','0-0','09:00:00','sunday','17:00:00','sunday'); diff --git a/tfrender/tfrender.go b/tfrender/tfrender.go index 3b5d148..96ec5b5 100644 --- a/tfrender/tfrender.go +++ b/tfrender/tfrender.go @@ -134,6 +134,9 @@ func (r *TFRender) ResourceFireHydrantOnCallSchedule(ctx context.Context) error hcl.TraverseAttr{Name: "id"}, }) b.SetAttributeValue("time_zone", cty.StringVal(s.Timezone)) + if s.Strategy == "custom" && s.StartTime != "" { + b.SetAttributeValue("start_time", cty.StringVal(s.StartTime)) + } members, err := store.UseQueries(ctx).ListFhMembersByExtScheduleID(ctx, s.ID) if err != nil { @@ -157,8 +160,14 @@ func (r *TFRender) ResourceFireHydrantOnCallSchedule(ctx context.Context) error b.AppendNewline() strategy := b.AppendNewBlock("strategy", []string{}).Body() strategy.SetAttributeValue("type", cty.StringVal(s.Strategy)) - strategy.SetAttributeValue("handoff_time", cty.StringVal(s.HandoffTime)) - strategy.SetAttributeValue("handoff_day", cty.StringVal(s.HandoffDay)) + if s.Strategy == "weekly" { + strategy.SetAttributeValue("handoff_day", cty.StringVal(s.HandoffDay)) + } + if s.Strategy == "custom" { + strategy.SetAttributeValue("shift_duration", cty.StringVal(s.ShiftDuration)) + } else { + strategy.SetAttributeValue("handoff_time", cty.StringVal(s.HandoffTime)) + } restrictions, err := store.UseQueries(ctx).ListExtScheduleRestrictionsByExtScheduleID(ctx, s.ID) if err != nil { From 072a9999a562a5b9019b9a8302cb0e84c9277810 Mon Sep 17 00:00:00 2001 From: "Wilson E. Husin" Date: Thu, 11 Apr 2024 11:40:05 -0700 Subject: [PATCH 14/14] pagerduty: add proper tests --- pager/pagerduty_test.go | 51 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/pager/pagerduty_test.go b/pager/pagerduty_test.go index acda7ca..a618b0b 100644 --- a/pager/pagerduty_test.go +++ b/pager/pagerduty_test.go @@ -6,9 +6,11 @@ import ( "net/http/httptest" "net/url" "os" + "reflect" "testing" "github.com/firehydrant/signals-migrator/pager" + "github.com/firehydrant/signals-migrator/store" "github.com/gosimple/slug" ) @@ -34,6 +36,13 @@ func pagerDutyTestClient(t *testing.T, apiURL string) (context.Context, *pager.P return ctx, pd } +func assertDeepEqual[T any](t *testing.T, got, want T) { + t.Helper() + if !reflect.DeepEqual(got, want) { + t.Fatalf("[FAIL]\n got: %+v\nwant: %+v", got, want) + } +} + func TestPagerDuty(t *testing.T) { ts := pagerDutyHttpServer(t) ctx, pd := pagerDutyTestClient(t, ts.URL) @@ -44,17 +53,47 @@ func TestPagerDuty(t *testing.T) { t.Fatalf("error loading schedules: %s", err) } t.Logf("found %d users", len(u)) - // The only assertion so far from this test is that method will not error. - // We need stronger checks for this test to be more useful. - // TODO: verify that the users are accurate. + + // Verify that the first user is as expected. + mika := u[0] + expected := &pager.User{ + Email: "mika+eng@example.com", + Resource: pager.Resource{ + ID: "P5A1XH2", + Name: "Mika", + }, + } + assertDeepEqual(t, mika, expected) }) t.Run("LoadSchedules", func(t *testing.T) { if err := pd.LoadSchedules(ctx); err != nil { t.Fatalf("error loading schedules: %s", err) } - // The only assertion so far from this test is that method will not error. - // We need stronger checks for this test to be more useful. - // TODO: verify that the schedules were saved to the database. + + // At the moment, this will show "Team ... not found" warning in logs because + // we didn't seed the database with that information. After we refactor the methods + // ListTeams and ListUsers to use database, as LoadTeams and LoadUsers respectively, + // we should expect the warning to go away. + s, err := store.UseQueries(ctx).ListExtSchedules(ctx) + if err != nil { + t.Fatalf("error loading schedules: %s", err) + } + t.Logf("found %d schedules", len(s)) + + // Verify that the first schedule is as expected. + first := s[0] + expected := store.ExtSchedule{ + ID: "P3D7DLW-PC1DX4O", + Name: "Jen - primary - Layer 2", + Description: "(Layer 2)", + Timezone: "America/Los_Angeles", + Strategy: "weekly", + ShiftDuration: "", + StartTime: "", + HandoffTime: "16:00:00", + HandoffDay: "monday", + } + assertDeepEqual(t, first, expected) }) }