diff --git a/pager/opsgenie.go b/pager/opsgenie.go index b2bd067..da9d0e7 100644 --- a/pager/opsgenie.go +++ b/pager/opsgenie.go @@ -2,7 +2,9 @@ package pager import ( "context" + "database/sql" "fmt" + "math" "strconv" "strings" "time" @@ -11,6 +13,7 @@ import ( "github.com/firehydrant/signals-migrator/store" "github.com/gosimple/slug" "github.com/opsgenie/opsgenie-go-sdk-v2/client" + "github.com/opsgenie/opsgenie-go-sdk-v2/escalation" "github.com/opsgenie/opsgenie-go-sdk-v2/og" "github.com/opsgenie/opsgenie-go-sdk-v2/schedule" "github.com/opsgenie/opsgenie-go-sdk-v2/team" @@ -18,9 +21,10 @@ import ( ) type Opsgenie struct { - userClient *user.Client - teamClient *team.Client - scheduleClient *schedule.Client + userClient *user.Client + teamClient *team.Client + scheduleClient *schedule.Client + escalationClient *escalation.Client } func NewOpsgenie(apiKey string) *Opsgenie { @@ -46,10 +50,15 @@ func NewOpsgenieWithConfig(conf *client.Config) *Opsgenie { if err != nil { panic(fmt.Sprintf("creating opsgenie schedule client: %v", err)) } + escalationClient, err := escalation.NewClient(conf) + if err != nil { + panic(fmt.Sprintf("creating opsgenie escalation client: %v", err)) + } return &Opsgenie{ - userClient: userClient, - teamClient: teamClient, - scheduleClient: scheduleClient, + userClient: userClient, + teamClient: teamClient, + scheduleClient: scheduleClient, + escalationClient: escalationClient, } } @@ -322,7 +331,170 @@ func (o *Opsgenie) saveRotationToDB(ctx context.Context, s schedule.Schedule, r } func (o *Opsgenie) LoadEscalationPolicies(ctx context.Context) error { - // TODO: implement - console.Warnf("opsgenie.LoadEscalationPolicies is not currently supported.") + resp, err := o.escalationClient.List(ctx) + if err != nil { + return err + } + + for _, policy := range resp.Escalations { + if err := o.saveEscalationPolicyToDB(ctx, policy); err != nil { + return fmt.Errorf("saving escalation policy to db: %w", err) + } + } + + return nil +} + +func (o *Opsgenie) saveEscalationPolicyToDB(ctx context.Context, policy escalation.Escalation) error { + var repeatLimit int64 + var repeatInterval sql.NullString + if policy.Repeat != nil { + repeatLimit = int64(policy.Repeat.Count) + repeatInterval.Valid = true + repeatInterval.String = fmt.Sprintf("PT%dM", policy.Repeat.WaitInterval) + } + ep := store.InsertExtEscalationPolicyParams{ + ID: policy.Id, + Name: policy.Name, + Description: policy.Description, + TeamID: sql.NullString{Valid: true, String: policy.OwnerTeam.Id}, + RepeatInterval: repeatInterval, + RepeatLimit: repeatLimit, + } + if err := store.UseQueries(ctx).InsertExtEscalationPolicy(ctx, ep); err != nil { + return fmt.Errorf("saving escalation policy %s (%s): %w", ep.Name, ep.ID, err) + } + + for i, rule := range policy.Rules { + timeout := calculateTimeout(policy, i) + if err := o.saveEscalationPolicyStepToDB(ctx, ep.ID, rule, int64(i), timeout); err != nil { + return fmt.Errorf("saving escalation rule to db: %w", err) + } + } + return nil +} + +// regarding step.Timeout +// Opsgenie has a delay property on each rule, indicating the total time delay before executing that step (where time zero is the start time of the escalation +// policy). On the FH side, we have a timeout value for each step, which represents the amount of time to wait *after* firing that step before moving to the +// next step, with a minimum timeout of 1 minutes and a max of 60 min. Opsgenie could have multiple steps with the same delay value, in which case all those +// steps would fire simultaneously (this is not supported on the FH side). Opsgenie also supports time intervals on the order of hours, days, weeks, or months +// none of which FH supports, so we're locking the max time between steps to 1 hour (and so are only interested in a time unit of minutes). + +// To do a best approximation of this, we are assuming that the Opsgenie steps are delivered in order (they seem to be) and are setting the timeout to: +// Max(1, Min(60, step.next[delay minutes] - step.current[delay minutes])) +// with a special rule for the final step of: +// Max(1, Min(60, policy.Repeat.WaitInterval minutes)) + +func calculateTimeout(policy escalation.Escalation, position int) string { + timeout := "PT1M" + if position+1 == len(policy.Rules) { + if policy.Repeat != nil { + // WaitInterval is always in minutes, but delay amounts can be other units. + timeout = fmt.Sprintf("PT%dM", int(math.Max(1, math.Min(float64(policy.Repeat.WaitInterval), 60)))) + } + // if the policy doesn't repeat, then it shouldn't matter what this value is. We'll go with a default of 1 min. + } else { + currentDelayMin := uint32(60) + if policy.Rules[position].Delay.TimeUnit == og.Minutes { + currentDelayMin = policy.Rules[position].Delay.TimeAmount + } + nextDelayMin := uint32(60) + if policy.Rules[position+1].Delay.TimeUnit == og.Minutes { + nextDelayMin = policy.Rules[position+1].Delay.TimeAmount + } + // Warn the user that we're locking to min/max and give the actual value + if policy.Rules[position+1].Delay.TimeUnit != og.Minutes || policy.Rules[position+1].Delay.TimeAmount > 60 { + console.Warnf("Actual delay time for step %d is %d %s. Locking to a max of 60 minutes.\n", + position+1, + policy.Rules[position+1].Delay.TimeAmount, + policy.Rules[position+1].Delay.TimeUnit) + } + if policy.Rules[position+1].Delay.TimeAmount == 0 { + console.Warnf("Actual delay time for step %d is 0. Locking to a min of 1 minute.\n", position+1) + } + + timeout = fmt.Sprintf("PT%dM", int(math.Max(1, math.Min(float64(nextDelayMin-currentDelayMin), 60)))) + } + return timeout +} + +func (o *Opsgenie) saveEscalationPolicyStepToDB(ctx context.Context, policyID string, rule escalation.Rule, position int64, timeout string) error { + stepID := fmt.Sprintf("%s-%d", policyID, position) + step := store.InsertExtEscalationPolicyStepParams{ + ID: stepID, + EscalationPolicyID: policyID, + Position: position, + Timeout: timeout, + } + + t := store.InsertExtEscalationPolicyStepTargetParams{ + EscalationPolicyStepID: stepID, + TargetID: rule.Recipient.Id, + } + + // The actual target for a rule is a combination of rule.Recipient.Type and rule.NotifyType, with only some combinations being valid. + // A handy chart (because if I have to know this, so do you): + // RecipientType NotifyType Notes + // User default Just notify the user. + // Schedule default Notify the currently on-call person for this schedule + // Team default Notify the default escalation policy for a team. We support this only as a handoff step, not in the middle of a policy + // Team users Notify all non-admin members of a team + // Team admins Notify all admin members of a team + // Team all Notify all members of a team + // Team random Notify a random?! member of the team (don't even get me started...) + // Schedule next Notify the person who will be on-call next in the given schedule + // Schedule previous Notify the person who was previously oncall in the given schedule + // We only support recepients of User or Schedule and only the 'default' NotifyType. Anything else we're just logging and skipping. + + if rule.NotifyType != og.Default { + console.Warnf("Escalation policy step target is '%s' notify type '%s' for policy '%s' step %d, skipping...\n", + rule.Recipient.Type, + rule.NotifyType, + policyID, + position) + return nil + } + + switch rule.Recipient.Type { + case og.User: + t.TargetType = store.TARGET_TYPE_USER + case og.Schedule: + t.TargetType = store.TARGET_TYPE_SCHEDULE + default: + console.Warnf("Escalation policy step target is '%s' notify type '%s' for policy '%s' step %d, skipping...\n", + rule.Recipient.Type, + rule.NotifyType, + policyID, + position) + return nil + } + + if err := store.UseQueries(ctx).InsertExtEscalationPolicyStep(ctx, step); err != nil { + return fmt.Errorf("saving escalation policy step: %w", err) + } + + // For Opsgenie, the person(s) on-call for a schedule is the oncall person in all rotations for that schedule for the given time (rotations may overlap). + // We've saved each rotation as a separate FH schedule. So, since we're not sure exactly what was intended by escalating to a Opsgenie schedule, we're + // adding all of the FH schedules corresponding to the Opsgenie rotations for that schedule as targets. This should be viewed as an approximation and + // review should certainly be required here. + + if t.TargetType == store.TARGET_TYPE_SCHEDULE { + schedules, err := store.UseQueries(ctx).ListExtSchedulesLikeID(ctx, fmt.Sprintf(`%s%%`, rule.Recipient.Id)) + if err != nil { + return fmt.Errorf("getting schedules starting with ID %s: %w", rule.Recipient.Id, err) + } + for _, schedule := range schedules { + t.TargetID = schedule.ID + if err := store.UseQueries(ctx).InsertExtEscalationPolicyStepTarget(ctx, t); err != nil { + return fmt.Errorf("saving escalation policy step target: %w", err) + } + } + } else { + if err := store.UseQueries(ctx).InsertExtEscalationPolicyStepTarget(ctx, t); err != nil { + return fmt.Errorf("saving escalation policy step target: %w", err) + } + } + return nil } diff --git a/pager/opsgenie_test.go b/pager/opsgenie_test.go index bcc5fd8..c0b7c52 100644 --- a/pager/opsgenie_test.go +++ b/pager/opsgenie_test.go @@ -40,6 +40,40 @@ func TestOpsgenie(t *testing.T) { assertJSON(t, u) }) + t.Run("LoadTeams", func(t *testing.T) { + ctx, og := setup(t) + + if err := og.LoadTeams(ctx); err != nil { + t.Fatalf("error loading teams: %s", err) + } + + u, err := store.UseQueries(ctx).ListExtTeams(ctx) + if err != nil { + t.Fatalf("error loading teams: %s", err) + } + assertJSON(t, u) + }) + + t.Run("LoadTeamMembers", func(t *testing.T) { + ctx, og := setup(t) + + if err := og.LoadUsers(ctx); err != nil { + t.Fatalf("error loading users: %s", err) + } + if err := og.LoadTeams(ctx); err != nil { + t.Fatalf("error loading teams: %s", err) + } + if err := og.LoadTeamMembers(ctx); err != nil { + t.Fatalf("error loading team members: %s", err) + } + + u, err := store.UseQueries(ctx).ListExtTeamMemberships(ctx) + if err != nil { + t.Fatalf("error loading team memberships: %s", err) + } + assertJSON(t, u) + }) + t.Run("LoadSchedules", func(t *testing.T) { ctx, og := setup(t) @@ -54,4 +88,29 @@ func TestOpsgenie(t *testing.T) { t.Logf("found %d schedules", len(s)) assertJSON(t, s) }) + + t.Run("LoadEscalationPolicies", func(t *testing.T) { + ctx, og := setup(t) + + if err := og.LoadUsers(ctx); err != nil { + t.Fatalf("error loading users: %s", err) + } + if err := og.LoadTeams(ctx); err != nil { + t.Fatalf("error loading teams: %s", err) + } + if err := og.LoadSchedules(ctx); err != nil { + t.Fatalf("error loading schedules: %s", err) + } + + if err := og.LoadEscalationPolicies(ctx); err != nil { + t.Fatalf("error loading escalation policies: %s", err) + } + + s, err := store.UseQueries(ctx).ListExtEscalationPolicies(ctx) + if err != nil { + t.Fatalf("error loading escalation policies: %s", err) + } + t.Logf("found %d escalation policies", len(s)) + assertJSON(t, s) + }) } diff --git a/pager/testdata/TestOpsgenie/LoadEscalationPolicies.golden.json b/pager/testdata/TestOpsgenie/LoadEscalationPolicies.golden.json new file mode 100644 index 0000000..fb6daec --- /dev/null +++ b/pager/testdata/TestOpsgenie/LoadEscalationPolicies.golden.json @@ -0,0 +1,19 @@ +[ + { + "id": "2a6feab7-936d-4829-800f-e781a96bdf1b", + "name": "Customer Success_escalation", + "description": "", + "team_id": { + "String": "b7acbc33-9853-4150-8a4b-10156d9408c8", + "Valid": true + }, + "repeat_limit": 0, + "repeat_interval": { + "String": "", + "Valid": false + }, + "handoff_target_type": "", + "handoff_target_id": "", + "to_import": 0 + } +] diff --git a/pager/testdata/TestOpsgenie/LoadTeamMembers.golden.json b/pager/testdata/TestOpsgenie/LoadTeamMembers.golden.json new file mode 100644 index 0000000..cc969b3 --- /dev/null +++ b/pager/testdata/TestOpsgenie/LoadTeamMembers.golden.json @@ -0,0 +1,24 @@ +[ + { + "ext_team": { + "id": "b7acbc33-9853-4150-8a4b-10156d9408c8", + "name": "Customer Success", + "slug": "customer-success", + "fh_team_id": { + "String": "", + "Valid": false + }, + "is_group": 0, + "to_import": 0 + }, + "ext_user": { + "id": "b5b92115-bfe7-43eb-8c2a-e467f2e5ddc4", + "name": "john doe", + "email": "john.doe@opsgenie.com", + "fh_user_id": { + "String": "", + "Valid": false + } + } + } +] diff --git a/pager/testdata/TestOpsgenie/LoadTeams.golden.json b/pager/testdata/TestOpsgenie/LoadTeams.golden.json new file mode 100644 index 0000000..8e4af82 --- /dev/null +++ b/pager/testdata/TestOpsgenie/LoadTeams.golden.json @@ -0,0 +1,13 @@ +[ + { + "id": "b7acbc33-9853-4150-8a4b-10156d9408c8", + "name": "Customer Success", + "slug": "customer-success", + "fh_team_id": { + "String": "", + "Valid": false + }, + "is_group": 0, + "to_import": 0 + } +] diff --git a/pager/testdata/TestOpsgenie/apiserver/v2-escalations.json b/pager/testdata/TestOpsgenie/apiserver/v2-escalations.json new file mode 100644 index 0000000..5b64424 --- /dev/null +++ b/pager/testdata/TestOpsgenie/apiserver/v2-escalations.json @@ -0,0 +1,43 @@ +{ + "data": [ + { + "id": "2a6feab7-936d-4829-800f-e781a96bdf1b", + "name": "Customer Success_escalation", + "description": "", + "ownerTeam": { + "id": "b7acbc33-9853-4150-8a4b-10156d9408c8", + "name": "Customer Success" + }, + "rules": [ + { + "condition": "if-not-acked", + "notifyType": "default", + "delay": { + "timeAmount": 0, + "timeUnit": "minutes" + }, + "recipient": { + "type": "user", + "id": "b5b92115-bfe7-43eb-8c2a-e467f2e5ddc4", + "username": "john.doe@opsgenie.com" + } + }, + { + "condition": "if-not-acked", + "notifyType": "default", + "delay": { + "timeAmount": 0, + "timeUnit": "minutes" + }, + "recipient": { + "type": "schedule", + "id": "3fee43f2-02da-49be-ab50-c88ed13aecc3", + "name": "Customer Success_schedule" + } + } + ] + } + ], + "took": 0.106, + "requestId": "fb1c49a1-6f00-46d8-80ca-3f8e4ad7e31e" +} \ No newline at end of file diff --git a/pager/testdata/TestOpsgenie/apiserver/v2-teams-b7acbc33-9853-4150-8a4b-10156d9408c8-identifiertype-id.json b/pager/testdata/TestOpsgenie/apiserver/v2-teams-b7acbc33-9853-4150-8a4b-10156d9408c8-identifiertype-id.json new file mode 100644 index 0000000..43f1765 --- /dev/null +++ b/pager/testdata/TestOpsgenie/apiserver/v2-teams-b7acbc33-9853-4150-8a4b-10156d9408c8-identifiertype-id.json @@ -0,0 +1,22 @@ +{ + "data": { + "id": "b7acbc33-9853-4150-8a4b-10156d9408c8", + "name": "Customer Success", + "description": "", + "members": [ + { + "user": { + "id": "b5b92115-bfe7-43eb-8c2a-e467f2e5ddc4", + "username": "john.doe@opsgenie.com" + }, + "role": "admin" + } + ], + "links": { + "web": "https://app.opsgenie.com/teams/dashboard/b7acbc33-9853-4150-8a4b-10156d9408c8/main", + "api": "https://api.opsgenie.com/v2/teams/b7acbc33-9853-4150-8a4b-10156d9408c8" + } + }, + "took": 0.029, + "requestId": "5a53826f-7864-4bf2-ada3-2979784d1e98" +} \ No newline at end of file diff --git a/pager/testdata/TestOpsgenie/apiserver/v2-teams.json b/pager/testdata/TestOpsgenie/apiserver/v2-teams.json new file mode 100644 index 0000000..45edfed --- /dev/null +++ b/pager/testdata/TestOpsgenie/apiserver/v2-teams.json @@ -0,0 +1,15 @@ +{ + "data": [ + { + "id": "b7acbc33-9853-4150-8a4b-10156d9408c8", + "name": "Customer Success", + "description": "", + "links": { + "web": "https://app.opsgenie.com/teams/dashboard/b7acbc33-9853-4150-8a4b-10156d9408c8/main", + "api": "https://api.opsgenie.com/v2/teams/b7acbc33-9853-4150-8a4b-10156d9408c8" + } + } + ], + "took": 0.009, + "requestId": "0fd19d50-632a-4f13-a357-b26d0065adc5" +} \ No newline at end of file diff --git a/store/queries.sql b/store/queries.sql index af3545f..90e8a07 100644 --- a/store/queries.sql +++ b/store/queries.sql @@ -101,6 +101,9 @@ SELECT * FROM ext_schedules WHERE id = ?; -- name: ListExtSchedules :many SELECT * FROM ext_schedules; +-- name: ListExtSchedulesLikeID :many +SELECT * FROM ext_schedules WHERE id LIKE ?; + -- name: InsertExtSchedule :exec INSERT INTO ext_schedules (id, name, description, timezone, strategy, shift_duration, start_time, handoff_time, handoff_day) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); diff --git a/store/queries.sql.go b/store/queries.sql.go index fef003b..5247f53 100644 --- a/store/queries.sql.go +++ b/store/queries.sql.go @@ -547,6 +547,43 @@ func (q *Queries) ListExtSchedules(ctx context.Context) ([]ExtSchedule, error) { return items, nil } +const listExtSchedulesLikeID = `-- name: ListExtSchedulesLikeID :many +SELECT id, name, description, timezone, strategy, shift_duration, start_time, handoff_time, handoff_day FROM ext_schedules WHERE id LIKE ? +` + +func (q *Queries) ListExtSchedulesLikeID(ctx context.Context, id string) ([]ExtSchedule, error) { + rows, err := q.db.QueryContext(ctx, listExtSchedulesLikeID, id) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ExtSchedule + for rows.Next() { + var i ExtSchedule + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.Timezone, + &i.Strategy, + &i.ShiftDuration, + &i.StartTime, + &i.HandoffTime, + &i.HandoffDay, + ); 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 listExtTeamMemberships = `-- name: ListExtTeamMemberships :many SELECT ext_teams.id, ext_teams.name, ext_teams.slug, ext_teams.fh_team_id, ext_teams.is_group, ext_teams.to_import, ext_users.id, ext_users.name, ext_users.email, ext_users.fh_user_id FROM ext_memberships JOIN ext_teams ON ext_teams.id = ext_memberships.team_id diff --git a/tfrender/testdata/TestRenderOpsgenie/EscalationPolicy.golden.tf b/tfrender/testdata/TestRenderOpsgenie/EscalationPolicy.golden.tf new file mode 100644 index 0000000..01fd1df --- /dev/null +++ b/tfrender/testdata/TestRenderOpsgenie/EscalationPolicy.golden.tf @@ -0,0 +1,197 @@ +terraform { + required_providers { + firehydrant = { + source = "firehydrant/firehydrant" + version = ">= 0.8.0" + } + } +} + +data "firehydrant_user" "jsmith" { + email = "jsmith@firehydrant.com" +} + +data "firehydrant_user" "fh_demo" { + email = "fh-demo@firehydrant.io" +} + +data "firehydrant_user" "fh_eng" { + email = "fh-eng@firehydrant.io" +} + +data "firehydrant_user" "fh_success" { + email = "fh-success@firehydrant.com" +} + +resource "firehydrant_team" "aj_team" { + name = "AJ Team" + + memberships { + user_id = data.firehydrant_user.fh_eng.id + } + + memberships { + user_id = data.firehydrant_user.fh_demo.id + } + + memberships { + user_id = data.firehydrant_user.jsmith.id + } + + memberships { + user_id = data.firehydrant_user.fh_success.id + } +} + +resource "firehydrant_on_call_schedule" "aj_team_aj_team_schedule_rota3" { + name = "AJ Team_schedule - Rota3" + description = "(Rota3)" + team_id = firehydrant_team.aj_team.id + time_zone = "America/Los_Angeles" + + member_ids = [data.firehydrant_user.fh_demo.id, data.firehydrant_user.fh_eng.id, data.firehydrant_user.fh_success.id] + + strategy { + type = "custom" + shift_duration = "PT2H" + } +} + +resource "firehydrant_on_call_schedule" "aj_team_aj_team_schedule_daytime_rotation" { + name = "AJ Team_schedule - Daytime rotation" + description = "(Daytime rotation)" + team_id = firehydrant_team.aj_team.id + time_zone = "America/Los_Angeles" + + member_ids = [data.firehydrant_user.fh_demo.id, data.firehydrant_user.fh_eng.id, data.firehydrant_user.jsmith.id, data.firehydrant_user.fh_success.id] + + strategy { + type = "weekly" + handoff_day = "monday" + handoff_time = "07:00:00" + } + + restrictions { + start_day = "monday" + start_time = "08:00:00" + end_day = "monday" + end_time = "18:00:00" + } + + restrictions { + start_day = "tuesday" + start_time = "08:00:00" + end_day = "tuesday" + end_time = "18:00:00" + } + + restrictions { + start_day = "wednesday" + start_time = "08:00:00" + end_day = "wednesday" + end_time = "18:00:00" + } + + restrictions { + start_day = "thursday" + start_time = "08:00:00" + end_day = "thursday" + end_time = "18:00:00" + } + + restrictions { + start_day = "friday" + start_time = "08:00:00" + end_day = "friday" + end_time = "18:00:00" + } +} + +resource "firehydrant_on_call_schedule" "aj_team_aj_team_schedule_nighttime_rotation" { + name = "AJ Team_schedule - Nighttime rotation" + description = "(Nighttime rotation)" + team_id = firehydrant_team.aj_team.id + time_zone = "America/Los_Angeles" + + member_ids = [data.firehydrant_user.fh_demo.id, data.firehydrant_user.fh_eng.id, data.firehydrant_user.jsmith.id, data.firehydrant_user.fh_success.id] + + strategy { + type = "daily" + handoff_time = "15:00:00" + } + + restrictions { + start_day = "sunday" + start_time = "18:00:00" + end_day = "monday" + end_time = "08:00:00" + } + + restrictions { + start_day = "monday" + start_time = "18:00:00" + end_day = "tuesday" + end_time = "08:00:00" + } + + restrictions { + start_day = "tuesday" + start_time = "18:00:00" + end_day = "wednesday" + end_time = "08:00:00" + } + + restrictions { + start_day = "wednesday" + start_time = "18:00:00" + end_day = "thursday" + end_time = "08:00:00" + } + + restrictions { + start_day = "thursday" + start_time = "18:00:00" + end_day = "friday" + end_time = "08:00:00" + } + + restrictions { + start_day = "friday" + start_time = "18:00:00" + end_day = "saturday" + end_time = "08:00:00" + } + + restrictions { + start_day = "saturday" + start_time = "18:00:00" + end_day = "sunday" + end_time = "08:00:00" + } +} + +resource "firehydrant_escalation_policy" "aj_team_escalation" { + name = "AJ Team_escalation" + team_id = firehydrant_team.aj_team.id + + step { + timeout = "PT1M" + + targets { + type = "OnCallSchedule" + id = firehydrant_on_call_schedule.aj_team_aj_team_schedule_daytime_rotation.id + } + + targets { + type = "OnCallSchedule" + id = firehydrant_on_call_schedule.aj_team_aj_team_schedule_nighttime_rotation.id + } + + targets { + type = "OnCallSchedule" + id = firehydrant_on_call_schedule.aj_team_aj_team_schedule_rota3.id + } + } + + repetitions = 0 +} diff --git a/tfrender/testdata/TestRenderOpsgenie/EscalationPolicy_seed.sql b/tfrender/testdata/TestRenderOpsgenie/EscalationPolicy_seed.sql new file mode 100644 index 0000000..5a045a6 --- /dev/null +++ b/tfrender/testdata/TestRenderOpsgenie/EscalationPolicy_seed.sql @@ -0,0 +1,67 @@ +BEGIN TRANSACTION; + +INSERT INTO fh_users VALUES('49ef2cda-ab4f-4599-852c-8cc2c8884523','John Smith','jsmith@firehydrant.com'); +INSERT INTO fh_users VALUES('90c17208-46b4-4e82-b9b8-b8f5d8215a05','FireHydrant Demo','fh-demo@firehydrant.io'); +INSERT INTO fh_users VALUES('66506894-ecbc-4034-b8e6-30851dabf5f3','FireHydrant Eng','fh-eng@firehydrant.io'); +INSERT INTO fh_users VALUES('a8fc03aa-8443-4c76-819c-8b7242fec459','FireHydrant Success','fh-success@firehydrant.com'); + +INSERT INTO ext_users VALUES('e0a51be7-3c7e-407f-8678-292ab421f55f','John Smith','jsmith@firehydrant.com','49ef2cda-ab4f-4599-852c-8cc2c8884523'); +INSERT INTO ext_users VALUES('1dc37638-ab52-44f3-848e-a16bcc584fb7','FireHydrant Demo','fh-demo@firehydrant.io','90c17208-46b4-4e82-b9b8-b8f5d8215a05'); +INSERT INTO ext_users VALUES('9253cf00-6195-4123-a9a6-f9f1e25718d8','FireHydrant Eng','fh-eng@firehydrant.io','66506894-ecbc-4034-b8e6-30851dabf5f3'); +INSERT INTO ext_users VALUES('e94e17aa-418c-44f7-8e47-1eaebf6b5343','FireHydrant Success','fh-success@firehydrant.com','a8fc03aa-8443-4c76-819c-8b7242fec459'); + +INSERT INTO fh_teams VALUES('8c465512-b0b4-47df-ba59-735574bc4dde','Alerting','alerting'); +INSERT INTO fh_teams VALUES('d98aa7e2-9b38-41a8-b5de-49743c3b9ac2','Assign Product On-call','on-call-gameday'); +INSERT INTO fh_teams VALUES('20c766e5-318a-4acc-a8f9-660e824e50f8','Customer Success and Support','customer-success-and-support'); + + +INSERT INTO ext_teams VALUES('946bf740-0497-4d5d-b31f-23a6e55a2719','AJ Team','aj-team',NULL,0,1); + +INSERT INTO ext_memberships VALUES('9253cf00-6195-4123-a9a6-f9f1e25718d8','946bf740-0497-4d5d-b31f-23a6e55a2719'); +INSERT INTO ext_memberships VALUES('1dc37638-ab52-44f3-848e-a16bcc584fb7','946bf740-0497-4d5d-b31f-23a6e55a2719'); +INSERT INTO ext_memberships VALUES('e0a51be7-3c7e-407f-8678-292ab421f55f','946bf740-0497-4d5d-b31f-23a6e55a2719'); +INSERT INTO ext_memberships VALUES('e94e17aa-418c-44f7-8e47-1eaebf6b5343','946bf740-0497-4d5d-b31f-23a6e55a2719'); + +INSERT INTO ext_schedules VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-b1103233-600f-433c-bbdc-5269ad010255','AJ Team_schedule - Rota3','(Rota3)','America/Los_Angeles','custom','PT2H','','15:00:00','friday'); +INSERT INTO ext_schedules VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-2b3ba1f8-5df9-4af7-a1ac-73f90bd30b2d','AJ Team_schedule - Daytime rotation','(Daytime rotation)','America/Los_Angeles','weekly','','','07:00:00','monday'); +INSERT INTO ext_schedules VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-9b488cc6-efa0-44f3-a432-b913acab9147','AJ Team_schedule - Nighttime rotation','(Nighttime rotation)','America/Los_Angeles','daily','','','15:00:00','wednesday'); + +INSERT INTO ext_schedule_restrictions VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-2b3ba1f8-5df9-4af7-a1ac-73f90bd30b2d','0','08:00:00','monday','18:00:00','monday'); +INSERT INTO ext_schedule_restrictions VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-2b3ba1f8-5df9-4af7-a1ac-73f90bd30b2d','1','08:00:00','tuesday','18:00:00','tuesday'); +INSERT INTO ext_schedule_restrictions VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-2b3ba1f8-5df9-4af7-a1ac-73f90bd30b2d','2','08:00:00','wednesday','18:00:00','wednesday'); +INSERT INTO ext_schedule_restrictions VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-2b3ba1f8-5df9-4af7-a1ac-73f90bd30b2d','3','08:00:00','thursday','18:00:00','thursday'); +INSERT INTO ext_schedule_restrictions VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-2b3ba1f8-5df9-4af7-a1ac-73f90bd30b2d','4','08:00:00','friday','18:00:00','friday'); +INSERT INTO ext_schedule_restrictions VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-9b488cc6-efa0-44f3-a432-b913acab9147','0','18:00:00','sunday','08:00:00','monday'); +INSERT INTO ext_schedule_restrictions VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-9b488cc6-efa0-44f3-a432-b913acab9147','1','18:00:00','monday','08:00:00','tuesday'); +INSERT INTO ext_schedule_restrictions VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-9b488cc6-efa0-44f3-a432-b913acab9147','2','18:00:00','tuesday','08:00:00','wednesday'); +INSERT INTO ext_schedule_restrictions VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-9b488cc6-efa0-44f3-a432-b913acab9147','3','18:00:00','wednesday','08:00:00','thursday'); +INSERT INTO ext_schedule_restrictions VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-9b488cc6-efa0-44f3-a432-b913acab9147','4','18:00:00','thursday','08:00:00','friday'); +INSERT INTO ext_schedule_restrictions VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-9b488cc6-efa0-44f3-a432-b913acab9147','5','18:00:00','friday','08:00:00','saturday'); +INSERT INTO ext_schedule_restrictions VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-9b488cc6-efa0-44f3-a432-b913acab9147','6','18:00:00','saturday','08:00:00','sunday'); + +INSERT INTO ext_schedule_teams VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-b1103233-600f-433c-bbdc-5269ad010255','946bf740-0497-4d5d-b31f-23a6e55a2719'); +INSERT INTO ext_schedule_teams VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-2b3ba1f8-5df9-4af7-a1ac-73f90bd30b2d','946bf740-0497-4d5d-b31f-23a6e55a2719'); +INSERT INTO ext_schedule_teams VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-9b488cc6-efa0-44f3-a432-b913acab9147','946bf740-0497-4d5d-b31f-23a6e55a2719'); + + +INSERT INTO ext_schedule_members VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-b1103233-600f-433c-bbdc-5269ad010255','9253cf00-6195-4123-a9a6-f9f1e25718d8'); +INSERT INTO ext_schedule_members VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-b1103233-600f-433c-bbdc-5269ad010255','1dc37638-ab52-44f3-848e-a16bcc584fb7'); +INSERT INTO ext_schedule_members VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-b1103233-600f-433c-bbdc-5269ad010255','e94e17aa-418c-44f7-8e47-1eaebf6b5343'); +INSERT INTO ext_schedule_members VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-2b3ba1f8-5df9-4af7-a1ac-73f90bd30b2d','9253cf00-6195-4123-a9a6-f9f1e25718d8'); +INSERT INTO ext_schedule_members VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-2b3ba1f8-5df9-4af7-a1ac-73f90bd30b2d','1dc37638-ab52-44f3-848e-a16bcc584fb7'); +INSERT INTO ext_schedule_members VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-2b3ba1f8-5df9-4af7-a1ac-73f90bd30b2d','e94e17aa-418c-44f7-8e47-1eaebf6b5343'); +INSERT INTO ext_schedule_members VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-2b3ba1f8-5df9-4af7-a1ac-73f90bd30b2d','e0a51be7-3c7e-407f-8678-292ab421f55f'); +INSERT INTO ext_schedule_members VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-9b488cc6-efa0-44f3-a432-b913acab9147','9253cf00-6195-4123-a9a6-f9f1e25718d8'); +INSERT INTO ext_schedule_members VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-9b488cc6-efa0-44f3-a432-b913acab9147','1dc37638-ab52-44f3-848e-a16bcc584fb7'); +INSERT INTO ext_schedule_members VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-9b488cc6-efa0-44f3-a432-b913acab9147','e94e17aa-418c-44f7-8e47-1eaebf6b5343'); +INSERT INTO ext_schedule_members VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-9b488cc6-efa0-44f3-a432-b913acab9147','e0a51be7-3c7e-407f-8678-292ab421f55f'); + +INSERT INTO ext_escalation_policies VALUES('880ec24e-58db-441b-9681-2cb527bd24b2','AJ Team_escalation','','946bf740-0497-4d5d-b31f-23a6e55a2719',0,NULL,'','',1); + +INSERT INTO ext_escalation_policy_steps VALUES('880ec24e-58db-441b-9681-2cb527bd24b2-0','880ec24e-58db-441b-9681-2cb527bd24b2',0,'PT1M'); + +INSERT INTO ext_escalation_policy_step_targets VALUES('880ec24e-58db-441b-9681-2cb527bd24b2-0','OnCallSchedule','8ab5a183-8ef5-47db-9de0-56663cfbae7c-b1103233-600f-433c-bbdc-5269ad010255'); +INSERT INTO ext_escalation_policy_step_targets VALUES('880ec24e-58db-441b-9681-2cb527bd24b2-0','OnCallSchedule','8ab5a183-8ef5-47db-9de0-56663cfbae7c-2b3ba1f8-5df9-4af7-a1ac-73f90bd30b2d'); +INSERT INTO ext_escalation_policy_step_targets VALUES('880ec24e-58db-441b-9681-2cb527bd24b2-0','OnCallSchedule','8ab5a183-8ef5-47db-9de0-56663cfbae7c-9b488cc6-efa0-44f3-a432-b913acab9147'); + +COMMIT; \ No newline at end of file diff --git a/tfrender/testdata/TestRenderOpsgenie/TeamWithSchedules_seed.sql b/tfrender/testdata/TestRenderOpsgenie/TeamWithSchedules_seed.sql index ae3794f..fc026f5 100644 --- a/tfrender/testdata/TestRenderOpsgenie/TeamWithSchedules_seed.sql +++ b/tfrender/testdata/TestRenderOpsgenie/TeamWithSchedules_seed.sql @@ -1,3 +1,5 @@ +BEGIN TRANSACTION; + 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'); @@ -54,3 +56,5 @@ INSERT INTO ext_schedule_members VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-2b INSERT INTO ext_schedule_members VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-9b488cc6-efa0-44f3-a432-b913acab9147','9253cf00-6195-4123-a9a6-f9f1e25718d8'); INSERT INTO ext_schedule_members VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-9b488cc6-efa0-44f3-a432-b913acab9147','1dc37638-ab52-44f3-848e-a16bcc584fb7'); INSERT INTO ext_schedule_members VALUES('8ab5a183-8ef5-47db-9de0-56663cfbae7c-9b488cc6-efa0-44f3-a432-b913acab9147','e94e17aa-418c-44f7-8e47-1eaebf6b5343'); + +COMMIT; \ No newline at end of file diff --git a/tfrender/testdata/TestRenderPagerDuty/EscalationPolicy.golden.tf b/tfrender/testdata/TestRenderPagerDuty/EscalationPolicy.golden.tf index 9d64fce..bcd4bc3 100644 --- a/tfrender/testdata/TestRenderPagerDuty/EscalationPolicy.golden.tf +++ b/tfrender/testdata/TestRenderPagerDuty/EscalationPolicy.golden.tf @@ -42,7 +42,7 @@ resource "firehydrant_on_call_schedule" "cowboy_coders_atalice_bob_is_always_on_ resource "firehydrant_escalation_policy" "atalice_bob_test_service_ep" { name = "🐴 @alice.bob Test Service-ep" - steps { + step { timeout = "PT30M" targets { @@ -57,7 +57,7 @@ resource "firehydrant_escalation_policy" "atalice_bob_test_service_ep" { resource "firehydrant_escalation_policy" "notify_atalice_bob" { name = "🐴 Notify @alice.bob" - steps { + step { timeout = "PT30M" targets { diff --git a/tfrender/tfrender.go b/tfrender/tfrender.go index bd3a219..991e62c 100644 --- a/tfrender/tfrender.go +++ b/tfrender/tfrender.go @@ -149,7 +149,7 @@ func (r *TFRender) ResourceFireHydrantEscalationPolicy(ctx context.Context) erro for _, s := range steps { b.AppendNewline() - step := b.AppendNewBlock("steps", nil).Body() + step := b.AppendNewBlock("step", nil).Body() step.SetAttributeValue("timeout", cty.StringVal(s.Timeout)) targets, err := store.UseQueries(ctx).ListExtEscalationPolicyStepTargets(ctx, s.ID) diff --git a/tfrender/tfrender_opsgenie_test.go b/tfrender/tfrender_opsgenie_test.go index b2b34de..95701bc 100644 --- a/tfrender/tfrender_opsgenie_test.go +++ b/tfrender/tfrender_opsgenie_test.go @@ -7,4 +7,7 @@ import ( func TestRenderOpsgenie(t *testing.T) { // Render Terraform configuration for slightly complex teams (with memberships) and schedules. t.Run("TeamWithSchedules", assertRenderPager) + + // Render Terraform configuration for a base case for escalation policy. + t.Run("EscalationPolicy", assertRenderPager) }