diff --git a/cmd/import.go b/cmd/import.go index 29c22b7..99c3765 100644 --- a/cmd/import.go +++ b/cmd/import.go @@ -42,6 +42,12 @@ var importFlags = []cli.Flag{ EnvVars: []string{"OUTPUT_DIR"}, Value: "./output", }, + &cli.StringFlag{ + Name: "dsn", + Usage: "The DSN for the SQLite database", + EnvVars: []string{"DSN"}, + Value: "file::memory:?cache=shared", + }, } var ImportCommand = &cli.Command{ @@ -92,6 +98,10 @@ func importAction(cliCtx *cli.Context) error { } console.Infof("Imported escalation policies from %s.\n", providerName) + if err := matchSchedulesToTeams(ctx); err != nil { + return fmt.Errorf("matching schedules to teams: %w", err) + } + tfr, err := tfrender.New(filepath.Join( cliCtx.String("output-dir"), fmt.Sprintf("%s_to_fh_signals.tf", strings.ToLower(providerName)), @@ -384,3 +394,63 @@ func importUsers(ctx context.Context, provider pager.Pager, fh *firehydrant.Clie } return nil } + +func matchSchedulesToTeams(ctx context.Context) error { + unmatchedSchedules, err := store.UseQueries(ctx).ListUnmatchedExtSchedule(ctx) + if err != nil { + return fmt.Errorf("unable to list schedules without team: %w", err) + } + if len(unmatchedSchedules) == 0 { + console.Successf("All schedules are already matched to teams.\n") + return nil + } + + // Get padding number to pretty print the information in table-like view. + idPad := console.PadStrings(unmatchedSchedules, func(s store.ExtSchedule) int { return len(s.ID) }) + namePad := console.PadStrings(unmatchedSchedules, func(s store.ExtSchedule) int { return len(s.Name) }) + + importOpts := []store.ExtSchedule{ + {ID: "[+] IMPORT ALL"}, + {ID: "[<] SKIP ALL "}, // Extra spaces for padding alignment of icons + } + importOpts = append(importOpts, unmatchedSchedules...) + selected, toImport, err := console.MultiSelectf(importOpts, func(s store.ExtSchedule) string { + return fmt.Sprintf("%*s %-*s %s", idPad, s.ID, namePad, s.Name, s.Description) + }, "FireHydrant requires every on-call schedule to be associated with a team. Without the owning team, they will be skipped in the import process.") + if err != nil { + return fmt.Errorf("selecting schedules to import: %w", err) + } + switch selected[0] { + case 0: + console.Successf("[+] All schedules will be imported.\n") + toImport = unmatchedSchedules + case 1: + console.Warnf("[<] Skipping manual schedule import.\n") + return nil + default: + console.Warnf("Selected %d schedules to be imported.\n", len(toImport)) + } + + // Now, we prompt users to match the schedules that we are importing to a known external team. + teams, err := store.UseQueries(ctx).ListExtTeams(ctx) + if err != nil { + return fmt.Errorf("unable to list teams: %w", err) + } + console.Warnf("Please match the following schedules to a FireHydrant team.\n") + for _, s := range toImport { + _, team, err := console.Selectf(teams, func(t store.ExtTeam) string { + return fmt.Sprintf("%s %s", t.ID, t.Name) + }, fmt.Sprintf("Which FireHydrant team should '%s' be imported to?", s.Name)) //nolint:govet + if err != nil { + return fmt.Errorf("selecting FireHydrant team for '%s': %w", s.Name, err) + } + if err := store.UseQueries(ctx).InsertExtScheduleTeam(ctx, store.InsertExtScheduleTeamParams{ + ScheduleID: s.ID, + TeamID: team.ID, + }); err != nil { + return fmt.Errorf("linking schedule '%s' to FireHydrant team: %w", s.Name, err) + } + } + + return nil +} diff --git a/store/queries.sql b/store/queries.sql index abdab8f..457fc8f 100644 --- a/store/queries.sql +++ b/store/queries.sql @@ -119,6 +119,12 @@ SELECT * FROM ext_schedules; -- name: ListExtSchedulesLikeID :many SELECT * FROM ext_schedules WHERE id LIKE ?; +-- name: ListUnmatchedExtSchedule :many +SELECT * FROM ext_schedules +WHERE id NOT IN ( + SELECT schedule_id FROM ext_schedule_teams +); + -- 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 2cb197c..b30b5a7 100644 --- a/store/queries.sql.go +++ b/store/queries.sql.go @@ -1156,6 +1156,46 @@ func (q *Queries) ListTeamsToImport(ctx context.Context) ([]LinkedTeam, error) { return items, nil } +const listUnmatchedExtSchedule = `-- name: ListUnmatchedExtSchedule :many +SELECT id, name, description, timezone, strategy, shift_duration, start_time, handoff_time, handoff_day FROM ext_schedules +WHERE id NOT IN ( + SELECT schedule_id FROM ext_schedule_teams +) +` + +func (q *Queries) ListUnmatchedExtSchedule(ctx context.Context) ([]ExtSchedule, error) { + rows, err := q.db.QueryContext(ctx, listUnmatchedExtSchedule) + 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 listUnmatchedExtUsers = `-- name: ListUnmatchedExtUsers :many SELECT id, name, email, fh_user_id, annotations FROM ext_users WHERE fh_user_id IS NULL