From 9e42f4848a506daf09998c7528e72a8a662f32a3 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Fri, 12 Apr 2024 15:12:29 +0200 Subject: [PATCH 01/19] Apply styles and markdown rendering --- cmd/tea_app.go | 10 +++++++--- cmd/terraform_apply.go | 2 +- cmd/terraform_plan.go | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/cmd/tea_app.go b/cmd/tea_app.go index 8656f24a..5c2f06c7 100644 --- a/cmd/tea_app.go +++ b/cmd/tea_app.go @@ -9,6 +9,7 @@ import ( "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" ) // waitForCancellation returns a tea.Cmd that will wait for SIGINT and SIGTERM and run the provided cancel on receipt. @@ -55,9 +56,12 @@ type taskModel struct { func NewTaskModel(title string) taskModel { return taskModel{ - status: taskStatusPending, - title: title, - spinner: spinner.New(spinner.WithSpinner(spinner.Dot)), + status: taskStatusPending, + title: title, + spinner: spinner.New( + spinner.WithSpinner(spinner.Pulse), + spinner.WithStyle(lipgloss.NewStyle().Foreground(lipgloss.Color(ColorPalette.Light.BgMain))), + ), } } diff --git a/cmd/terraform_apply.go b/cmd/terraform_apply.go index d6009700..9ad12a00 100644 --- a/cmd/terraform_apply.go +++ b/cmd/terraform_apply.go @@ -175,7 +175,7 @@ func (m tfApplyModel) View() string { strings.Join(m.progress, "\n") + "\n" } - return m.applyHeader + return markdownToString(m.applyHeader) } func (m tfApplyModel) startStartChangeCmd() tea.Cmd { diff --git a/cmd/terraform_plan.go b/cmd/terraform_plan.go index 2cf63aef..28bf8fe9 100644 --- a/cmd/terraform_plan.go +++ b/cmd/terraform_plan.go @@ -311,10 +311,10 @@ func (m tfPlanModel) View() string { bits := []string{} if m.runTfPlan { - bits = append(bits, m.planHeader) + bits = append(bits, markdownToString(m.planHeader)) } if m.tfPlanFinished { - bits = append(bits, m.processingHeader) + bits = append(bits, markdownToString(m.processingHeader)) } bits = append(bits, m.progress...) From fcfe75a8baef159e96e7bdc477a9f3b0be5379ea Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Fri, 12 Apr 2024 15:13:37 +0200 Subject: [PATCH 02/19] Remove unused type --- cmd/tea_snapshot.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/cmd/tea_snapshot.go b/cmd/tea_snapshot.go index b6e7436f..b3642215 100644 --- a/cmd/tea_snapshot.go +++ b/cmd/tea_snapshot.go @@ -13,16 +13,6 @@ type snapshotModel struct { edges uint32 } -// utility interface to remove the need for a type assertion -type connectResultStream interface { - // Receive advances the stream to the next message, which will then be - // available through the Msg method. It returns false when the stream stops, - // either by reaching the end or by encountering an unexpected error. After - // Receive returns false, the Err method will return any unexpected error - // encountered. - Receive() bool -} - type startSnapshotMsg struct { newState string } From 31d88fea238849be4de26a829090e4cb9f48cf65 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Fri, 12 Apr 2024 15:17:06 +0200 Subject: [PATCH 03/19] Initialise snapshotModels --- cmd/terraform_apply.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/terraform_apply.go b/cmd/terraform_apply.go index 9ad12a00..454a66ad 100644 --- a/cmd/terraform_apply.go +++ b/cmd/terraform_apply.go @@ -102,6 +102,7 @@ func (m tfApplyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case sourcesInitialisedMsg: m.isStarting = true return m, tea.Batch( + m.startingChangeSnapshot.Init(), m.startStartChangeCmd(), m.waitForStartingActivity, ) @@ -159,6 +160,7 @@ func (m tfApplyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tfApplyFinishedMsg: m.isEnding = true return m, tea.Batch( + m.endingChangeSnapshot.Init(), m.startEndChangeCmd(), m.waitForEndingActivity, ) From 8031125f54349af77f87d55ccbe29dd57ec2e894 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Fri, 12 Apr 2024 16:45:24 +0200 Subject: [PATCH 04/19] Finish and cleanup terraform commands --- cmd/tea_ensuretoken.go | 24 ++++----- cmd/tea_snapshot.go | 16 +++++- cmd/terraform_apply.go | 28 ++++++---- cmd/terraform_plan.go | 113 ++++++++++++++++++++++++++++++++++------- 4 files changed, 139 insertions(+), 42 deletions(-) diff --git a/cmd/tea_ensuretoken.go b/cmd/tea_ensuretoken.go index 2888536b..78e0dfec 100644 --- a/cmd/tea_ensuretoken.go +++ b/cmd/tea_ensuretoken.go @@ -95,15 +95,15 @@ func (m ensureTokenModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.title = "Manual device authorization." beginAuthMessage := `# Authenticate with a browser - Attempting to automatically open the SSO authorization page in your default browser. - If the browser does not open or you wish to use a different device to authorize this request, open the following URL: +Attempting to automatically open the SSO authorization page in your default browser. +If the browser does not open or you wish to use a different device to authorize this request, open the following URL: - %v + %v - Then enter the code: +Then enter the code: - %v - ` + %v +` m.deviceMessage = markdownToString(fmt.Sprintf(beginAuthMessage, msg.deviceCode.VerificationURI, msg.deviceCode.UserCode)) return m, m.awaitTokenCmd case waitingForAuthorizationMsg: @@ -113,15 +113,15 @@ func (m ensureTokenModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.title = "Waiting for device authorization, check your browser." beginAuthMessage := `# Authenticate with a browser - Attempting to automatically open the SSO authorization page in your default browser. - If the browser does not open or you wish to use a different device to authorize this request, open the following URL: +Attempting to automatically open the SSO authorization page in your default browser. +If the browser does not open or you wish to use a different device to authorize this request, open the following URL: - %v + %v - Then enter the code: +Then enter the code: - %v - ` + %v +` m.deviceMessage = markdownToString(fmt.Sprintf(beginAuthMessage, msg.deviceCode.VerificationURI, msg.deviceCode.UserCode)) return m, m.awaitTokenCmd case tokenLoadedMsg: diff --git a/cmd/tea_snapshot.go b/cmd/tea_snapshot.go index b3642215..2184ad04 100644 --- a/cmd/tea_snapshot.go +++ b/cmd/tea_snapshot.go @@ -4,6 +4,7 @@ import ( "fmt" tea "github.com/charmbracelet/bubbletea" + log "github.com/sirupsen/logrus" ) type snapshotModel struct { @@ -44,11 +45,24 @@ func (m snapshotModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.items = msg.items m.edges = msg.edges } + log.Debugf("updated %+v => %v", msg, m) return m, nil } func (m snapshotModel) View() string { // TODO: add spinner and/or progressbar; complication: we do not have a // expected number of items/edges to count towards for the progressbar - return fmt.Sprintf("%s: %d items, %d edges", m.state, m.items, m.edges) + if m.items == 0 && m.edges == 0 { + return fmt.Sprintf("%s - %s", m.title, m.state) + } else if m.items == 1 && m.edges == 0 { + return fmt.Sprintf("%s - %s: 1 item", m.title, m.state) + } else if m.items == 1 && m.edges == 1 { + return fmt.Sprintf("%s - %s: 1 item, 1 edge", m.title, m.state) + } else if m.items > 1 && m.edges == 0 { + return fmt.Sprintf("%s - %s: %d items", m.title, m.state, m.items) + } else if m.items > 1 && m.edges == 1 { + return fmt.Sprintf("%s - %s: %d items, 1 edge", m.title, m.state, m.items) + } else { + return fmt.Sprintf("%s - %s: %d items, %d edges", m.title, m.state, m.items, m.edges) + } } diff --git a/cmd/terraform_apply.go b/cmd/terraform_apply.go index 454a66ad..de355e81 100644 --- a/cmd/terraform_apply.go +++ b/cmd/terraform_apply.go @@ -56,15 +56,14 @@ type runTfApplyMsg struct{} type tfApplyFinishedMsg struct{} func NewTfApplyModel(args []string) tea.Model { - // TODO: this is the real command - // args = append([]string{"apply"}, args...) - // // plan file needs to go last - // args = append(args, "overmind.plan") + args = append([]string{"apply"}, args...) + // plan file needs to go last + args = append(args, "overmind.plan") - // TODO: remove this test setup - args = append([]string{"plan"}, args...) - // -out needs to go last to override whatever the user specified on the command line - args = append(args, "-out", "overmind.plan") + // // TODO: remove this test setup + // args = append([]string{"plan"}, args...) + // // -out needs to go last to override whatever the user specified on the command line + // args = append(args, "-out", "overmind.plan") applyHeader := `# Applying Changes @@ -73,7 +72,7 @@ Running ` + "`" + `terraform %v` + "`\n" processingHeader := `# Applying Changes -Processing plan from ` + "`" + `terraform %v` + "`\n" +Applying changes with ` + "`" + `terraform %v` + "`\n" processingHeader = fmt.Sprintf(processingHeader, strings.Join(args, " ")) return tfApplyModel{ @@ -159,6 +158,13 @@ func (m tfApplyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { }) case tfApplyFinishedMsg: m.isEnding = true + // TODO: make this hack less ugly + lines := strings.Split(m.View(), "\n") + for range lines { + // scroll up to avoid overwriting the output from terraform + fmt.Println() + } + return m, tea.Batch( m.endingChangeSnapshot.Init(), m.startEndChangeCmd(), @@ -171,13 +177,13 @@ func (m tfApplyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m tfApplyModel) View() string { if m.isStarting || m.runTfApply || m.isEnding { - return m.processingHeader + + return markdownToString(m.processingHeader) + "\n" + m.startingChangeSnapshot.View() + "\n" + m.endingChangeSnapshot.View() + "\n" + strings.Join(m.progress, "\n") + "\n" } - return markdownToString(m.applyHeader) + return markdownToString(m.applyHeader) + "\n" } func (m tfApplyModel) startStartChangeCmd() tea.Cmd { diff --git a/cmd/terraform_plan.go b/cmd/terraform_plan.go index 28bf8fe9..3e39cc09 100644 --- a/cmd/terraform_plan.go +++ b/cmd/terraform_plan.go @@ -91,6 +91,8 @@ func CmdWrapper(action string, requiredScopes []string, commandModel func([]stri } defer f.Close() log.SetOutput(f) + viper.Set("log", "trace") + log.SetLevel(log.TraceLevel) } else { // avoid log messages from sources and others to interrupt bubbletea rendering viper.Set("log", "error") @@ -126,7 +128,11 @@ func CmdWrapper(action string, requiredScopes []string, commandModel func([]stri } // avoid overwriting the last view - fmt.Println() + // fmt.Println("1") + // fmt.Println("2") + // fmt.Println("3") + // fmt.Println("4") + // fmt.Println("5") return nil }() @@ -224,16 +230,20 @@ type tfPlanModel struct { planHeader string processingHeader string - runTfPlan bool - tfPlanFinished bool - processing chan tea.Msg - progress []string + runTfPlan bool + tfPlanFinished bool + processing chan tea.Msg + processingModel snapshotModel + progress []string + changeUrl string } type triggerTfPlanMsg struct{} type tfPlanFinishedMsg struct{} type processingActivityMsg struct{ text string } +type changeUpdatedMsg struct{ url string } type processingFinishedActivityMsg struct{ text string } +type delayQuitMsg struct{} func NewTfPlanModel(args []string) tea.Model { args = append([]string{"plan"}, args...) @@ -245,7 +255,7 @@ func NewTfPlanModel(args []string) tea.Model { Running ` + "`" + `terraform %v` + "`\n" planHeader = fmt.Sprintf(planHeader, strings.Join(args, " ")) - processingHeader := `# Planning Changes + processingHeader := `# Processing Planned Changes Processing plan from ` + "`" + `terraform %v` + "`\n" processingHeader = fmt.Sprintf(processingHeader, strings.Join(args, " ")) @@ -255,8 +265,9 @@ Processing plan from ` + "`" + `terraform %v` + "`\n" planHeader: planHeader, processingHeader: processingHeader, - processing: make(chan tea.Msg, 10), // provide a small buffer for sending updates, so we don't block the processing - progress: []string{}, + processing: make(chan tea.Msg, 10), // provide a small buffer for sending updates, so we don't block the processing + processingModel: snapshotModel{title: "Calculating Blast Radius", state: "pending"}, + progress: []string{}, } } @@ -292,6 +303,12 @@ func (m tfPlanModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { }) case tfPlanFinishedMsg: m.tfPlanFinished = true + // TODO: make this hack less ugly + lines := strings.Split(m.View(), "\n") + for range lines { + // scroll up to avoid overwriting the output from terraform + fmt.Println() + } return m, tea.Batch( m.processPlanCmd, m.waitForProcessingActivity, @@ -301,7 +318,26 @@ func (m tfPlanModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, m.waitForProcessingActivity case processingFinishedActivityMsg: m.progress = append(m.progress, msg.text) + return m, m.waitForProcessingActivity + case changeUpdatedMsg: + m.changeUrl = msg.url + return m, m.waitForProcessingActivity + + case startSnapshotMsg: + mdl, cmd := m.processingModel.Update(msg) + m.processingModel = mdl.(snapshotModel) + return m, tea.Batch(m.waitForProcessingActivity, cmd) + case progressSnapshotMsg: + mdl, cmd := m.processingModel.Update(msg) + m.processingModel = mdl.(snapshotModel) + return m, tea.Batch(m.waitForProcessingActivity, cmd) + case finishSnapshotMsg: + mdl, cmd := m.processingModel.Update(msg) + m.processingModel = mdl.(snapshotModel) + return m, tea.Sequence(cmd, func() tea.Msg { return delayQuitMsg{} }) + case delayQuitMsg: return m, tea.Quit + } return m, nil @@ -310,26 +346,36 @@ func (m tfPlanModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m tfPlanModel) View() string { bits := []string{} - if m.runTfPlan { + if m.runTfPlan && !m.tfPlanFinished { bits = append(bits, markdownToString(m.planHeader)) - } - if m.tfPlanFinished { + } else if m.tfPlanFinished { bits = append(bits, markdownToString(m.processingHeader)) + bits = append(bits, m.processingModel.View()) } - bits = append(bits, m.progress...) - return strings.Join(bits, "\n") + // bits = append(bits, m.progress...) + + if m.changeUrl != "" { + bits = append(bits, markdownToString(fmt.Sprintf("Change ready: [%v](%v)", m.changeUrl, m.changeUrl))) + } + return strings.Join(bits, "\n") + "\n" } // A command that waits for the activity on the processing channel. func (m tfPlanModel) waitForProcessingActivity() tea.Msg { - return <-m.processing + // TODO: remove debugging aids + time.Sleep(500 * time.Millisecond) + msg := <-m.processing + log.Debugf("received %+v", msg) + return msg } func (m tfPlanModel) processPlanCmd() tea.Msg { ctx := m.ctx span := trace.SpanFromContext(ctx) + m.processing <- startSnapshotMsg{newState: "converting terraform plan to JSON"} + tfPlanJsonCmd := exec.CommandContext(ctx, "terraform", "show", "-json", "overmind.plan") tfPlanJsonCmd.Stderr = os.Stderr // TODO: capture and output this through the View() instead @@ -344,6 +390,7 @@ func (m tfPlanModel) processPlanCmd() tea.Msg { } m.processing <- processingActivityMsg{"converted terraform plan to JSON"} + m.processing <- progressSnapshotMsg{newState: "converted terraform plan to JSON"} ticketLink := viper.GetString("ticket-link") if ticketLink == "" { @@ -365,6 +412,7 @@ func (m tfPlanModel) processPlanCmd() tea.Msg { if changeUuid == uuid.Nil { m.processing <- processingActivityMsg{"Creating a new change"} + m.processing <- progressSnapshotMsg{newState: "creating a new change"} log.Debug("Creating a new change") createResponse, err := client.CreateChange(ctx, &connect.Request[sdp.CreateChangeRequest]{ Msg: &sdp.CreateChangeRequest{ @@ -395,6 +443,7 @@ func (m tfPlanModel) processPlanCmd() tea.Msg { ) } else { m.processing <- processingActivityMsg{"Updating an existing change"} + m.processing <- progressSnapshotMsg{newState: "updating an existing change"} log.WithField("change", changeUuid).Debug("Updating an existing change") span.SetAttributes( attribute.String("ovm.change.uuid", changeUuid.String()), @@ -422,6 +471,7 @@ func (m tfPlanModel) processPlanCmd() tea.Msg { m.processing <- processingActivityMsg{"Uploading planned changes"} log.WithField("change", changeUuid).Debug("Uploading planned changes") + m.processing <- progressSnapshotMsg{newState: "uploading planned changes"} resultStream, err := client.UpdatePlannedChanges(ctx, &connect.Request[sdp.UpdatePlannedChangesRequest]{ Msg: &sdp.UpdatePlannedChangesRequest{ @@ -435,18 +485,37 @@ func (m tfPlanModel) processPlanCmd() tea.Msg { last_log := time.Now() first_log := true + var msg *sdp.CalculateBlastRadiusResponse for resultStream.Receive() { - msg := resultStream.Msg() + msg = resultStream.Msg() // log the first message and at most every 250ms during discovery // to avoid spanning the cli output time_since_last_log := time.Since(last_log) if first_log || msg.GetState() != sdp.CalculateBlastRadiusResponse_STATE_DISCOVERING || time_since_last_log > 250*time.Millisecond { - log.WithField("msg", msg).Info("Status update") + log.WithField("msg", msg).Trace("Status update") last_log = time.Now() first_log = false } m.processing <- processingActivityMsg{fmt.Sprintf("Status update: %v", msg)} + stateLabel := "unknown" + switch msg.GetState() { + case sdp.CalculateBlastRadiusResponse_STATE_UNSPECIFIED: + stateLabel = "unknown" + case sdp.CalculateBlastRadiusResponse_STATE_DISCOVERING: + stateLabel = "discovering blast radius" + case sdp.CalculateBlastRadiusResponse_STATE_FINDING_APPS: + stateLabel = "finding apps" + case sdp.CalculateBlastRadiusResponse_STATE_SAVING: + stateLabel = "saving blast radius" + case sdp.CalculateBlastRadiusResponse_STATE_DONE: + stateLabel = "done" + } + m.processing <- progressSnapshotMsg{ + newState: stateLabel, + items: msg.GetNumItems(), + edges: msg.GetNumEdges(), + } } if resultStream.Err() != nil { return fatalError{err: fmt.Errorf("error streaming results: %w", err)} @@ -455,8 +524,16 @@ func (m tfPlanModel) processPlanCmd() tea.Msg { changeUrl := *m.oi.FrontendUrl changeUrl.Path = fmt.Sprintf("%v/changes/%v/blast-radius", changeUrl.Path, changeUuid) log.WithField("change-url", changeUrl.String()).Info("Change ready") - fmt.Println(changeUrl.String()) - return processingFinishedActivityMsg{"Done"} + + // fmt.Println(changeUrl.String()) + + m.processing <- changeUpdatedMsg{url: changeUrl.String()} + m.processing <- processingFinishedActivityMsg{"Done"} + return finishSnapshotMsg{ + newState: "calculated blast radius", + items: msg.GetNumItems(), + edges: msg.GetNumEdges(), + } } // getTicketLinkFromPlan reads the plan file to create a unique hash to identify this change From 6470338b103d7e9f6a96e03dbf4404d88da2da88 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Mon, 15 Apr 2024 09:46:56 +0200 Subject: [PATCH 05/19] Start testing interactivity with terraform Add a variable to the test tf file to test interactivity with the running terraform command. --- main.tf | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/main.tf b/main.tf index 1ea36a28..25708a45 100644 --- a/main.tf +++ b/main.tf @@ -13,11 +13,16 @@ terraform { provider "aws" {} +variable "bucket_postfix" { + type = string + description = "The prefix to apply to the bucket name." +} + module "bucket" { source = "terraform-aws-modules/s3-bucket/aws" version = "~> 4.0" - bucket_prefix = "cli-test" + bucket_prefix = "cli-test${var.bucket_postfix}" control_object_ownership = true object_ownership = "BucketOwnerEnforced" From 69545c7d6ee23cdae0c80f913daab787fd71d673 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Mon, 15 Apr 2024 11:43:44 +0200 Subject: [PATCH 06/19] "Always" display the full terraform output See the code comments for limitations of this approach. --- cmd/tea_terraform.go | 28 +++++++++++++++++++++++++++- cmd/terraform_apply.go | 7 ------- cmd/terraform_plan.go | 17 +++++++++++------ 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/cmd/tea_terraform.go b/cmd/tea_terraform.go index 08251b20..6d5645c2 100644 --- a/cmd/tea_terraform.go +++ b/cmd/tea_terraform.go @@ -75,23 +75,49 @@ func (m cmdModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.String() == "ctrl+c" { return m, tea.Quit } + case fatalError: if msg.id == 0 { m.fatalError = msg.err.Error() } + skipView(m.View()) return m, tea.Quit - // return m, nil + case instanceLoadedMsg: m.oi = msg.instance // skip irrelevant status messages // delete(m.tasks, "00_oi") + case tokenAvailableMsg: return m.tokenChecks(msg.token) + + case tfPlanFinishedMsg, tfApplyFinishedMsg: + skipView(m.View()) + return m, nil } return m, tea.Batch(batch...) } +// skipView scrolls the terminal contents up after ExecCommand() to avoid +// overwriting the output from terraform when rendering the next View(). this +// has to be used here in the cmdModel to catch the entire View() output. +// +// NOTE: this is quite brittle and _requires_ that the View() after terraform +// returned is at least many lines as the view before ExecCommand(), otherwise +// the difference will get eaten by bubbletea on re-rendering. +// +// TODO: make this hack less ugly +func skipView(view string) { + lines := strings.Split(view, "\n") + for range lines { + fmt.Println() + } + + // log.Debugf("printed %v lines:", len(lines)) + // log.Debug(lines) +} + func (m cmdModel) tokenChecks(token *oauth2.Token) (cmdModel, tea.Cmd) { // Check that we actually got the claims we asked for. If you don't have // permission auth0 will just not assign those scopes rather than fail diff --git a/cmd/terraform_apply.go b/cmd/terraform_apply.go index de355e81..caeb6a4d 100644 --- a/cmd/terraform_apply.go +++ b/cmd/terraform_apply.go @@ -158,13 +158,6 @@ func (m tfApplyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { }) case tfApplyFinishedMsg: m.isEnding = true - // TODO: make this hack less ugly - lines := strings.Split(m.View(), "\n") - for range lines { - // scroll up to avoid overwriting the output from terraform - fmt.Println() - } - return m, tea.Batch( m.endingChangeSnapshot.Init(), m.startEndChangeCmd(), diff --git a/cmd/terraform_plan.go b/cmd/terraform_plan.go index 3e39cc09..37578b89 100644 --- a/cmd/terraform_plan.go +++ b/cmd/terraform_plan.go @@ -236,6 +236,8 @@ type tfPlanModel struct { processingModel snapshotModel progress []string changeUrl string + + fatalError string } type triggerTfPlanMsg struct{} @@ -303,12 +305,7 @@ func (m tfPlanModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { }) case tfPlanFinishedMsg: m.tfPlanFinished = true - // TODO: make this hack less ugly - lines := strings.Split(m.View(), "\n") - for range lines { - // scroll up to avoid overwriting the output from terraform - fmt.Println() - } + return m, tea.Batch( m.processPlanCmd, m.waitForProcessingActivity, @@ -338,6 +335,9 @@ func (m tfPlanModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case delayQuitMsg: return m, tea.Quit + case fatalError: + m.fatalError = msg.err.Error() + return m, tea.Quit } return m, nil @@ -358,6 +358,11 @@ func (m tfPlanModel) View() string { if m.changeUrl != "" { bits = append(bits, markdownToString(fmt.Sprintf("Change ready: [%v](%v)", m.changeUrl, m.changeUrl))) } + + if m.fatalError != "" { + bits = append(bits, deletedLineStyle.Render(fmt.Sprintf("Error: %v", m.fatalError))) + } + return strings.Join(bits, "\n") + "\n" } From 004ceb9bb1de0c24fe9b00bc7333e11719dae26c Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Thu, 2 May 2024 14:50:53 +0200 Subject: [PATCH 07/19] Avoid log.Fatal() to give bubbletea a chance to reset the terminal before exiting --- cmd/root.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 486a35f2..fb0fbc60 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -70,34 +70,38 @@ func NewOvermindInstance(ctx context.Context, app string) (OvermindInstance, err instanceDataUrl := fmt.Sprintf("%v/api/public/instance-data", instance.FrontendUrl) req, err := http.NewRequest("GET", instanceDataUrl, nil) if err != nil { - log.WithError(err).Fatal("could not initialize instance-data fetch") + log.WithContext(ctx).WithError(err).Error("could not initialize instance-data fetch") + return OvermindInstance{}, fmt.Errorf("could not initialize instance-data fetch: %w", err) } req = req.WithContext(ctx) log.WithField("instanceDataUrl", instanceDataUrl).Debug("Fetching instance-data") res, err := otelhttp.DefaultClient.Do(req) if err != nil { - log.WithError(err).Fatal("could not fetch instance-data") + log.WithContext(ctx).WithError(err).Error("could not fetch instance-data") + return OvermindInstance{}, fmt.Errorf("could not fetch instance-data: %w", err) } if res.StatusCode != 200 { - log.WithField("status-code", res.StatusCode).Fatal("instance-data fetch returned non-200 status") + log.WithContext(ctx).WithField("status-code", res.StatusCode).Error("instance-data fetch returned non-200 status") + return OvermindInstance{}, fmt.Errorf("instance-data fetch returned non-200 status: %v", res.StatusCode) } defer res.Body.Close() data := instanceData{} err = json.NewDecoder(res.Body).Decode(&data) if err != nil { - log.WithError(err).Fatal("could not parse instance-data") + log.WithContext(ctx).WithError(err).Error("could not parse instance-data") + return OvermindInstance{}, fmt.Errorf("could not parse instance-data: %w", err) } instance.ApiUrl, err = url.Parse(data.Api) if err != nil { - return instance, fmt.Errorf("invalid api_url value '%v' in instance-data, error: %w", data.Api, err) + return OvermindInstance{}, fmt.Errorf("invalid api_url value '%v' in instance-data, error: %w", data.Api, err) } instance.NatsUrl, err = url.Parse(data.Nats) if err != nil { - return instance, fmt.Errorf("invalid nats_url value '%v' in instance-data, error: %w", data.Nats, err) + return OvermindInstance{}, fmt.Errorf("invalid nats_url value '%v' in instance-data, error: %w", data.Nats, err) } instance.Audience = data.Aud From 4caa93687e179a515e723b420e2b76a5e897a191 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Thu, 2 May 2024 14:59:02 +0200 Subject: [PATCH 08/19] Add defense-in-depth checks to this test --- cmd/changes_submit_plan_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/cmd/changes_submit_plan_test.go b/cmd/changes_submit_plan_test.go index 54ec5d7c..133ffd8c 100644 --- a/cmd/changes_submit_plan_test.go +++ b/cmd/changes_submit_plan_test.go @@ -48,12 +48,24 @@ func TestMappedItemDiffsFromPlan(t *testing.T) { // t.Logf("item: %v", item.Attributes.AttrStruct.Fields["terraform_address"].GetStringValue()) if item.GetAttributes().GetAttrStruct().GetFields()["terraform_address"].GetStringValue() == "kubernetes_deployment.nats_box" { + if nats_box_deployment != nil { + t.Errorf("Found multiple nats_box_deployment: %v, %v", nats_box_deployment, diff) + } nats_box_deployment = diff } else if item.GetAttributes().GetAttrStruct().GetFields()["terraform_address"].GetStringValue() == "kubernetes_deployment.api_server" { + if api_server_deployment != nil { + t.Errorf("Found multiple api_server_deployment: %v, %v", api_server_deployment, diff) + } api_server_deployment = diff } else if item.GetType() == "iam-policy" { + if aws_iam_policy != nil { + t.Errorf("Found multiple aws_iam_policy: %v, %v", aws_iam_policy, diff) + } aws_iam_policy = diff } else if item.GetType() == "Secret" { + if secret != nil { + t.Errorf("Found multiple secrets: %v, %v", secret, diff) + } secret = diff } } From 437b41da8dfeccfe5d9c9add7fd1e8b9a162184a Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Thu, 2 May 2024 15:00:16 +0200 Subject: [PATCH 09/19] Don't render the VerificationURI as code this allows bubbletea to render it as clickable URL instead. This still doesn't fix the blinking issue with the spinner causing refreshes. --- cmd/root.go | 2 +- cmd/tea_ensuretoken.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index fb0fbc60..685a492e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -221,7 +221,7 @@ func (m authenticateModel) View() string { Attempting to automatically open the SSO authorization page in your default browser. If the browser does not open or you wish to use a different device to authorize this request, open the following URL: - %v +%v Then enter the code: diff --git a/cmd/tea_ensuretoken.go b/cmd/tea_ensuretoken.go index 78e0dfec..e40bb007 100644 --- a/cmd/tea_ensuretoken.go +++ b/cmd/tea_ensuretoken.go @@ -98,7 +98,7 @@ func (m ensureTokenModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { Attempting to automatically open the SSO authorization page in your default browser. If the browser does not open or you wish to use a different device to authorize this request, open the following URL: - %v +%v Then enter the code: @@ -116,7 +116,7 @@ Then enter the code: Attempting to automatically open the SSO authorization page in your default browser. If the browser does not open or you wish to use a different device to authorize this request, open the following URL: - %v +%v Then enter the code: From 8fbf41cb16dec138fdaef2b2f833e132e58bd252 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Thu, 2 May 2024 15:49:07 +0200 Subject: [PATCH 10/19] Remove debugging aids --- cmd/terraform_plan.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/cmd/terraform_plan.go b/cmd/terraform_plan.go index 37578b89..02ebdf31 100644 --- a/cmd/terraform_plan.go +++ b/cmd/terraform_plan.go @@ -368,8 +368,6 @@ func (m tfPlanModel) View() string { // A command that waits for the activity on the processing channel. func (m tfPlanModel) waitForProcessingActivity() tea.Msg { - // TODO: remove debugging aids - time.Sleep(500 * time.Millisecond) msg := <-m.processing log.Debugf("received %+v", msg) return msg From 72fcb4cb7c9d9ca7d524596ff7aacdb7c06cd692 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Thu, 2 May 2024 16:09:21 +0200 Subject: [PATCH 11/19] Improve high-level teabugging messages --- cmd/tea_terraform.go | 3 +++ cmd/terraform_plan.go | 20 +++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/cmd/tea_terraform.go b/cmd/tea_terraform.go index 6d5645c2..49b72bb0 100644 --- a/cmd/tea_terraform.go +++ b/cmd/tea_terraform.go @@ -9,6 +9,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/overmindtech/sdp-go" + log "github.com/sirupsen/logrus" "golang.org/x/oauth2" ) @@ -51,6 +52,8 @@ func (m cmdModel) Init() tea.Cmd { } func (m cmdModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + log.Debugf("cmdModel: Update %T received %+v", msg, msg) + batch := []tea.Cmd{} // update the main command diff --git a/cmd/terraform_plan.go b/cmd/terraform_plan.go index 02ebdf31..f9719120 100644 --- a/cmd/terraform_plan.go +++ b/cmd/terraform_plan.go @@ -278,6 +278,8 @@ func (m tfPlanModel) Init() tea.Cmd { } func (m tfPlanModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + log.Debugf("tfPlanModel: Update %T received %+v", msg, msg) + switch msg := msg.(type) { case loadSourcesConfigMsg: m.ctx = msg.ctx @@ -294,6 +296,8 @@ func (m tfPlanModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if aws_profile := viper.GetString("aws-profile"); aws_profile != "" { c.Env = append(c.Env, fmt.Sprintf("AWS_PROFILE=%v", aws_profile)) } + + m.processingModel.state = "executing terraform plan" return m, tea.ExecProcess( c, func(err error) tea.Msg { @@ -306,18 +310,23 @@ func (m tfPlanModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tfPlanFinishedMsg: m.tfPlanFinished = true + m.processingModel.state = "executed terraform plan" + return m, tea.Batch( m.processPlanCmd, m.waitForProcessingActivity, ) case processingActivityMsg: + m.processingModel.state = "processing" m.progress = append(m.progress, msg.text) return m, m.waitForProcessingActivity case processingFinishedActivityMsg: + m.processingModel.state = "finished" m.progress = append(m.progress, msg.text) return m, m.waitForProcessingActivity case changeUpdatedMsg: m.changeUrl = msg.url + m.processingModel.state = "Change updated" return m, m.waitForProcessingActivity case startSnapshotMsg: @@ -369,7 +378,7 @@ func (m tfPlanModel) View() string { // A command that waits for the activity on the processing channel. func (m tfPlanModel) waitForProcessingActivity() tea.Msg { msg := <-m.processing - log.Debugf("received %+v", msg) + log.Debugf("waitForProcessingActivity received %T: %+v", msg, msg) return msg } @@ -384,11 +393,13 @@ func (m tfPlanModel) processPlanCmd() tea.Msg { planJson, err := tfPlanJsonCmd.Output() if err != nil { + close(m.processing) return fatalError{err: fmt.Errorf("failed to convert terraform plan to JSON: %w", err)} } plannedChanges, err := mappedItemDiffsFromPlan(ctx, planJson, "overmind.plan", log.Fields{}) if err != nil { + close(m.processing) return fatalError{err: fmt.Errorf("failed to parse terraform plan: %w", err)} } @@ -399,6 +410,7 @@ func (m tfPlanModel) processPlanCmd() tea.Msg { if ticketLink == "" { ticketLink, err = getTicketLinkFromPlan() if err != nil { + close(m.processing) return err } } @@ -406,6 +418,7 @@ func (m tfPlanModel) processPlanCmd() tea.Msg { client := AuthenticatedChangesClient(ctx, m.oi) changeUuid, err := getChangeUuid(ctx, m.oi, sdp.ChangeStatus_CHANGE_STATUS_DEFINING, ticketLink, false) if err != nil { + close(m.processing) return fatalError{err: fmt.Errorf("failed searching for existing changes: %w", err)} } @@ -431,11 +444,13 @@ func (m tfPlanModel) processPlanCmd() tea.Msg { }, }) if err != nil { + close(m.processing) return fatalError{err: fmt.Errorf("failed to create a new change: %w", err)} } maybeChangeUuid := createResponse.Msg.GetChange().GetMetadata().GetUUIDParsed() if maybeChangeUuid == nil { + close(m.processing) return fatalError{err: fmt.Errorf("failed to read change id: %w", err)} } @@ -468,6 +483,7 @@ func (m tfPlanModel) processPlanCmd() tea.Msg { }, }) if err != nil { + close(m.processing) return fatalError{err: fmt.Errorf("failed to update change: %w", err)} } } @@ -483,6 +499,7 @@ func (m tfPlanModel) processPlanCmd() tea.Msg { }, }) if err != nil { + close(m.processing) return fatalError{err: fmt.Errorf("failed to update planned changes: %w", err)} } @@ -521,6 +538,7 @@ func (m tfPlanModel) processPlanCmd() tea.Msg { } } if resultStream.Err() != nil { + close(m.processing) return fatalError{err: fmt.Errorf("error streaming results: %w", err)} } From 35a7076eb8272a860b5901b00ccb35c7270933ed Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Thu, 2 May 2024 16:09:39 +0200 Subject: [PATCH 12/19] Fix hang after `terraform plan` execution --- cmd/tea_terraform.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/tea_terraform.go b/cmd/tea_terraform.go index 49b72bb0..898cca2e 100644 --- a/cmd/tea_terraform.go +++ b/cmd/tea_terraform.go @@ -95,8 +95,8 @@ func (m cmdModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m.tokenChecks(msg.token) case tfPlanFinishedMsg, tfApplyFinishedMsg: + // bump screen after terraform ran skipView(m.View()) - return m, nil } return m, tea.Batch(batch...) From b0906400ccd6593ca282ba8d776b17b71e27be5c Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Thu, 2 May 2024 16:21:25 +0200 Subject: [PATCH 13/19] Update plan.tape --- .air.toml | 4 ++-- demos/plan.tape | 5 +++++ main.tf | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.air.toml b/.air.toml index 473de762..169904c7 100644 --- a/.air.toml +++ b/.air.toml @@ -22,8 +22,8 @@ exclude_unchanged = false follow_symlink = false # contrary to other repos, this does wait for the debugger to attach, as cli processes are very shortlived # full_bin = "dlv exec --accept-multiclient --continue --headless --listen :9087 --api-version 2 ./tmp/overmind -- terraform plan" -# full_bin = "dlv exec --accept-multiclient --headless --listen :9087 --api-version 2 --tty /dev/pts/3 ./tmp/overmind -- terraform plan" -full_bin = "docker run --rm -e AWS_PROFILE=sso-david -e OVM_API_KEY -e APP -e RESET_STORED_CONFIG=true -e TEABUG=true -e LOG=trace -v $PWD:/vhs -v ~/.aws:/root/.aws ghcr.io/charmbracelet/vhs /vhs/demos/plan.tape && code demos/plan.gif" +# full_bin = "dlv exec --accept-multiclient --headless --listen :9087 --api-version 2 ./tmp/overmind -- terraform plan" +full_bin = "docker run --rm -e AWS_PROFILE=sso-david -e OVM_API_KEY -e APP=https://df.overmind-demo.com -e RESET_STORED_CONFIG=true -e TEABUG=true -e LOG=trace -v $PWD:/vhs -v ~/.aws:/root/.aws ghcr.io/charmbracelet/vhs /vhs/demos/plan.tape && code demos/plan.gif" include_dir = [] include_ext = ["go", "tpl", "tmpl", "templ", "html", "sql", "css", "md", "tape"] include_file = ["sqlc.yaml"] diff --git a/demos/plan.tape b/demos/plan.tape index c315fed8..689f18c2 100644 --- a/demos/plan.tape +++ b/demos/plan.tape @@ -1,6 +1,10 @@ Output demos/plan.gif # Output demos/plan.mp4 +Set Margin 20 +Set MarginFill "#7a70eb" # use Dark.BgMain +Set BorderRadius 10 + Hide Type "cd tmp" Enter @@ -19,6 +23,7 @@ Down Sleep 1 Enter Type "sso-dogfood" +Sleep 1 Enter Sleep 20 diff --git a/main.tf b/main.tf index 25708a45..bc45c567 100644 --- a/main.tf +++ b/main.tf @@ -16,6 +16,7 @@ provider "aws" {} variable "bucket_postfix" { type = string description = "The prefix to apply to the bucket name." + default = "test" } module "bucket" { From 7f718046feb39135a411c7a1ed8f163b133d4f0c Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Thu, 2 May 2024 16:37:28 +0200 Subject: [PATCH 14/19] Plug a few more holes in cmdModel.Update() --- cmd/tea_terraform.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cmd/tea_terraform.go b/cmd/tea_terraform.go index 898cca2e..55163e4c 100644 --- a/cmd/tea_terraform.go +++ b/cmd/tea_terraform.go @@ -80,11 +80,15 @@ func (m cmdModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case fatalError: + log.WithError(msg.err).Debug("cmdModel: fatalError received") if msg.id == 0 { m.fatalError = msg.err.Error() } skipView(m.View()) - return m, tea.Quit + return m, tea.Sequence( + tea.Batch(batch...), + tea.Quit, + ) case instanceLoadedMsg: m.oi = msg.instance @@ -92,7 +96,9 @@ func (m cmdModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // delete(m.tasks, "00_oi") case tokenAvailableMsg: - return m.tokenChecks(msg.token) + tm, cmd := m.tokenChecks(msg.token) + batch = append(batch, cmd) + return tm, tea.Batch(batch...) case tfPlanFinishedMsg, tfApplyFinishedMsg: // bump screen after terraform ran From 7090619d480fe41794f7d76a49ad6fdf19339b5f Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Thu, 2 May 2024 16:44:25 +0200 Subject: [PATCH 15/19] Improve rendering of fatal error --- cmd/tea_terraform.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/tea_terraform.go b/cmd/tea_terraform.go index 55163e4c..45068a92 100644 --- a/cmd/tea_terraform.go +++ b/cmd/tea_terraform.go @@ -80,7 +80,7 @@ func (m cmdModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case fatalError: - log.WithError(msg.err).Debug("cmdModel: fatalError received") + log.WithError(msg.err).WithField("msg.id", msg.id).Debug("cmdModel: fatalError received") if msg.id == 0 { m.fatalError = msg.err.Error() } @@ -175,7 +175,7 @@ func (m cmdModel) View() string { } tasks = append(tasks, m.cmd.View()) if m.fatalError != "" { - tasks = append(tasks, fmt.Sprintf("Fatal Error: %v", m.fatalError)) + tasks = append(tasks, markdownToString(fmt.Sprintf("> Fatal Error: %v\n", m.fatalError))) } return strings.Join(tasks, "\n") } From 3c02db769f720ccff3918c74277db7d07ae3afda Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Thu, 2 May 2024 16:44:45 +0200 Subject: [PATCH 16/19] Improve TEABUG log file handling --- cmd/terraform_plan.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/cmd/terraform_plan.go b/cmd/terraform_plan.go index f9719120..7752b5da 100644 --- a/cmd/terraform_plan.go +++ b/cmd/terraform_plan.go @@ -89,7 +89,8 @@ func CmdWrapper(action string, requiredScopes []string, commandModel func([]stri fmt.Println("fatal:", err) os.Exit(1) } - defer f.Close() + // leave the log file open until the very last moment, so we capture everything + // defer f.Close() log.SetOutput(f) viper.Set("log", "trace") log.SetLevel(log.TraceLevel) @@ -127,13 +128,6 @@ func CmdWrapper(action string, requiredScopes []string, commandModel func([]stri return fmt.Errorf("could not start program: %w", err) } - // avoid overwriting the last view - // fmt.Println("1") - // fmt.Println("2") - // fmt.Println("3") - // fmt.Println("4") - // fmt.Println("5") - return nil }() if err != nil { From a37f679c9e43cf128845de7356f9765332c0ab42 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Thu, 2 May 2024 16:50:10 +0200 Subject: [PATCH 17/19] Improve copy and URL rendering --- cmd/terraform_plan.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/terraform_plan.go b/cmd/terraform_plan.go index 7752b5da..1ed35ac9 100644 --- a/cmd/terraform_plan.go +++ b/cmd/terraform_plan.go @@ -359,7 +359,7 @@ func (m tfPlanModel) View() string { // bits = append(bits, m.progress...) if m.changeUrl != "" { - bits = append(bits, markdownToString(fmt.Sprintf("Change ready: [%v](%v)", m.changeUrl, m.changeUrl))) + bits = append(bits, fmt.Sprintf("\nCheck the blast radius graph and risks at:\n%v\n\n", m.changeUrl)) } if m.fatalError != "" { From 35254ed4cbea126d133d38961473bf8bd9d21900 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Thu, 2 May 2024 18:15:09 +0200 Subject: [PATCH 18/19] Don't display deviceMessage after the task is done --- cmd/tea_ensuretoken.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/tea_ensuretoken.go b/cmd/tea_ensuretoken.go index e40bb007..654d3c59 100644 --- a/cmd/tea_ensuretoken.go +++ b/cmd/tea_ensuretoken.go @@ -159,7 +159,7 @@ func (m ensureTokenModel) View() string { if len(m.errors) > 0 { view += fmt.Sprintf("\n%v\n", strings.Join(m.errors, "\n")) } - if m.deviceMessage != "" { + if m.deviceMessage != "" && !(m.status == taskStatusDone || m.status == taskStatusError) { view += fmt.Sprintf("\n%v\n", m.deviceMessage) } return view From b6bbab9ff6e54df73a5c1ca89f5c6e19a56aa90c Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Thu, 2 May 2024 18:37:48 +0200 Subject: [PATCH 19/19] Add taskModels for planning --- cmd/tea_terraform.go | 1 + cmd/terraform_plan.go | 38 +++++++++++++++++++++++++++----------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/cmd/tea_terraform.go b/cmd/tea_terraform.go index 45068a92..00cea0da 100644 --- a/cmd/tea_terraform.go +++ b/cmd/tea_terraform.go @@ -47,6 +47,7 @@ func (m cmdModel) Init() tea.Cmd { waitForCancellation(m.ctx, m.cancel), m.tasks["00_oi"].Init(), m.tasks["01_token"].Init(), + m.tasks["02_config"].Init(), m.cmd.Init(), ) } diff --git a/cmd/terraform_plan.go b/cmd/terraform_plan.go index 1ed35ac9..23020293 100644 --- a/cmd/terraform_plan.go +++ b/cmd/terraform_plan.go @@ -221,7 +221,9 @@ type tfPlanModel struct { oi OvermindInstance args []string + planTask taskModel planHeader string + processingTask taskModel processingHeader string runTfPlan bool @@ -246,19 +248,17 @@ func NewTfPlanModel(args []string) tea.Model { // -out needs to go last to override whatever the user specified on the command line args = append(args, "-out", "overmind.plan") - planHeader := `# Planning Changes - -Running ` + "`" + `terraform %v` + "`\n" + planHeader := `Running ` + "`" + `terraform %v` + "`\n" planHeader = fmt.Sprintf(planHeader, strings.Join(args, " ")) - processingHeader := `# Processing Planned Changes - -Processing plan from ` + "`" + `terraform %v` + "`\n" + processingHeader := `Processing plan from ` + "`" + `terraform %v` + "`\n" processingHeader = fmt.Sprintf(processingHeader, strings.Join(args, " ")) return tfPlanModel{ args: args, + planTask: NewTaskModel("Planning Changes"), planHeader: planHeader, + processingTask: NewTaskModel("Processing Planned Changes"), processingHeader: processingHeader, processing: make(chan tea.Msg, 10), // provide a small buffer for sending updates, so we don't block the processing @@ -268,7 +268,10 @@ Processing plan from ` + "`" + `terraform %v` + "`\n" } func (m tfPlanModel) Init() tea.Cmd { - return nil + return tea.Batch( + m.planTask.Init(), + m.processingTask.Init(), + ) } func (m tfPlanModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -281,6 +284,7 @@ func (m tfPlanModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case sourcesInitialisedMsg: m.runTfPlan = true + m.planTask.status = taskStatusRunning // defer the actual command to give the view a chance to show the header return m, func() tea.Msg { return triggerTfPlanMsg{} } case triggerTfPlanMsg: @@ -303,7 +307,9 @@ func (m tfPlanModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { }) case tfPlanFinishedMsg: m.tfPlanFinished = true + m.planTask.status = taskStatusDone + m.processingTask.status = taskStatusRunning m.processingModel.state = "executed terraform plan" return m, tea.Batch( @@ -316,6 +322,7 @@ func (m tfPlanModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, m.waitForProcessingActivity case processingFinishedActivityMsg: m.processingModel.state = "finished" + m.processingTask.status = taskStatusDone m.progress = append(m.progress, msg.text) return m, m.waitForProcessingActivity case changeUpdatedMsg: @@ -341,23 +348,32 @@ func (m tfPlanModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case fatalError: m.fatalError = msg.err.Error() return m, tea.Quit + default: + var planCmd, processingCmd tea.Cmd + m.planTask, planCmd = m.planTask.Update(msg) + m.processingTask, processingCmd = m.processingTask.Update(msg) + return m, tea.Batch(planCmd, processingCmd) } return m, nil } func (m tfPlanModel) View() string { - bits := []string{} + bits := []string{ + m.planTask.View(), + } if m.runTfPlan && !m.tfPlanFinished { bits = append(bits, markdownToString(m.planHeader)) - } else if m.tfPlanFinished { + } + + bits = append(bits, m.processingTask.View()) + + if m.tfPlanFinished { bits = append(bits, markdownToString(m.processingHeader)) bits = append(bits, m.processingModel.View()) } - // bits = append(bits, m.progress...) - if m.changeUrl != "" { bits = append(bits, fmt.Sprintf("\nCheck the blast radius graph and risks at:\n%v\n\n", m.changeUrl)) }