From 7071a6a70336ef9797a4560b26877ee63020348b Mon Sep 17 00:00:00 2001 From: magodo Date: Wed, 22 Jan 2025 15:40:28 +1100 Subject: [PATCH] Display information in the "Plan" stage --- internal/csv/csv.go | 4 +- internal/plainui/ui.go | 36 +++++----- internal/state/plan_info.go | 67 +++++++++++++++++++ ...rce_info.go => resource_operation_info.go} | 46 ++++++------- internal/ui/ui.go | 56 ++++++++++------ 5 files changed, 144 insertions(+), 65 deletions(-) create mode 100644 internal/state/plan_info.go rename internal/state/{resource_info.go => resource_operation_info.go} (68%) diff --git a/internal/csv/csv.go b/internal/csv/csv.go index 9ddc4c7..81d144c 100644 --- a/internal/csv/csv.go +++ b/internal/csv/csv.go @@ -7,8 +7,8 @@ import ( ) type Input struct { - RefreshInfos state.ResourceInfos - ApplyInfos state.ResourceInfos + RefreshInfos state.ResourceOperationInfos + ApplyInfos state.ResourceOperationInfos } func ToCsv(input Input) []byte { diff --git a/internal/plainui/ui.go b/internal/plainui/ui.go index dd6c179..2f2ee7e 100644 --- a/internal/plainui/ui.go +++ b/internal/plainui/ui.go @@ -20,8 +20,8 @@ type UIModel struct { reader reader.Reader writer io.Writer - refreshInfos state.ResourceInfos - applyInfos state.ResourceInfos + refreshInfos state.ResourceOperationInfos + applyInfos state.ResourceOperationInfos totalCnt int doneCnt int @@ -117,28 +117,28 @@ func (m *UIModel) Run() error { case views.HookMsg: switch hook := msg.Hook.(type) { case json.RefreshStart: - res := &state.ResourceInfo{ + res := &state.ResourceOperationInfo{ Idx: len(m.refreshInfos) + 1, RawResourceAddr: hook.Resource, - Loc: state.ResourceInfoLocator{ + Loc: state.ResourceOperationInfoLocator{ Module: hook.Resource.Module, ResourceAddr: hook.Resource.Addr, Action: "refresh", }, - Status: state.ResourceStatusStart, + Status: state.ResourceOperationStatusStart, StartTime: msg.TimeStamp, } m.refreshInfos = append(m.refreshInfos, res) msgstr = msg.Message case json.RefreshComplete: - loc := state.ResourceInfoLocator{ + loc := state.ResourceOperationInfoLocator{ Module: hook.Resource.Module, ResourceAddr: hook.Resource.Addr, Action: "refresh", } - status := state.ResourceStatusComplete - update := state.ResourceInfoUpdate{ + status := state.ResourceOperationStatusComplete + update := state.ResourceOperationInfoUpdate{ Status: &status, Endtime: &msg.TimeStamp, } @@ -149,15 +149,15 @@ func (m *UIModel) Run() error { msgstr = msg.Message case json.OperationStart: - info := &state.ResourceInfo{ + info := &state.ResourceOperationInfo{ Idx: len(m.applyInfos) + 1, RawResourceAddr: hook.Resource, - Loc: state.ResourceInfoLocator{ + Loc: state.ResourceOperationInfoLocator{ Module: hook.Resource.Module, ResourceAddr: hook.Resource.Addr, Action: string(hook.Action), }, - Status: state.ResourceStatusStart, + Status: state.ResourceOperationStatusStart, StartTime: msg.TimeStamp, } m.applyInfos = append(m.applyInfos, info) @@ -166,7 +166,7 @@ func (m *UIModel) Run() error { msgstr = fmt.Sprintf("[%*d/%*d] %s", w, info.Idx, w, m.totalCnt, msg.Message) case json.OperationProgress: - loc := state.ResourceInfoLocator{ + loc := state.ResourceOperationInfoLocator{ Module: hook.Resource.Module, ResourceAddr: hook.Resource.Addr, Action: string(hook.Action), @@ -181,13 +181,13 @@ func (m *UIModel) Run() error { msgstr = fmt.Sprintf("[%*d/%*d] %s", w, info.Idx, w, m.totalCnt, msg.Message) case json.OperationComplete: - loc := state.ResourceInfoLocator{ + loc := state.ResourceOperationInfoLocator{ Module: hook.Resource.Module, ResourceAddr: hook.Resource.Addr, Action: string(hook.Action), } - status := state.ResourceStatusComplete - update := state.ResourceInfoUpdate{ + status := state.ResourceOperationStatusComplete + update := state.ResourceOperationInfoUpdate{ Status: &status, Endtime: &msg.TimeStamp, } @@ -201,13 +201,13 @@ func (m *UIModel) Run() error { msgstr = fmt.Sprintf("[%*d/%*d] %s", w, info.Idx, w, m.totalCnt, msg.Message) case json.OperationErrored: - loc := state.ResourceInfoLocator{ + loc := state.ResourceOperationInfoLocator{ Module: hook.Resource.Module, ResourceAddr: hook.Resource.Addr, Action: string(hook.Action), } - status := state.ResourceStatusErrored - update := state.ResourceInfoUpdate{ + status := state.ResourceOperationStatusErrored + update := state.ResourceOperationInfoUpdate{ Status: &status, Endtime: &msg.TimeStamp, } diff --git a/internal/state/plan_info.go b/internal/state/plan_info.go new file mode 100644 index 0000000..398b4d0 --- /dev/null +++ b/internal/state/plan_info.go @@ -0,0 +1,67 @@ +package state + +import ( + "fmt" + "strconv" + + "github.com/charmbracelet/bubbles/table" + "github.com/magodo/pipeform/internal/terraform/views/json" +) + +type PlanInfo struct { + Resource json.ResourceAddr + Action json.ChangeAction + + PrevResource *json.ResourceAddr + Reason json.ChangeReason +} + +type PlanInfos []*PlanInfo + +func (infos PlanInfos) ToRows() []table.Row { + var rows []table.Row + for i, info := range infos { + var comment string + switch info.Action { + case json.ActionDelete, json.ActionReplace: + comment = string(info.Reason) + case json.ActionMove: + if info.PrevResource != nil { + source := info.PrevResource.Addr + if info.PrevResource.Module != "" { + source = fmt.Sprintf("%s (%s)", source, info.PrevResource.Module) + } + comment = fmt.Sprintf("Moved from %s", source) + } + } + row := []string{ + strconv.Itoa(i + 1), + info.Resource.Module, + info.Resource.Addr, + string(info.Action), + comment, + } + rows = append(rows, row) + } + return rows +} + +func (infos PlanInfos) ToColumns(width int) []table.Column { + const indexWidth = 6 + const actionWidth = 8 + + dynamicWidth := width - indexWidth - actionWidth + + commentWidth := dynamicWidth / 3 + moduleWidth := dynamicWidth / 3 + resourceWidth := dynamicWidth / 3 + + return []table.Column{ + {Title: "Index", Width: indexWidth}, + {Title: "Module", Width: moduleWidth}, + {Title: "Resource", Width: resourceWidth}, + {Title: "Action", Width: actionWidth}, + // Comment is a combination of "reason" (for delete/replace) and a modified version of "previous_resource" (for move) + {Title: "Comment", Width: commentWidth}, + } +} diff --git a/internal/state/resource_info.go b/internal/state/resource_operation_info.go similarity index 68% rename from internal/state/resource_info.go rename to internal/state/resource_operation_info.go index 6f9e5f8..040569d 100644 --- a/internal/state/resource_info.go +++ b/internal/state/resource_operation_info.go @@ -10,57 +10,57 @@ import ( "github.com/magodo/pipeform/internal/terraform/views/json" ) -type ResourceStatus string +type ResourceOperationStatus string const ( // Once received one OperationStart hook message - ResourceStatusStart ResourceStatus = "start" + ResourceOperationStatusStart ResourceOperationStatus = "start" // Once received one OperationComplete hook message - ResourceStatusComplete ResourceStatus = "complete" + ResourceOperationStatusComplete ResourceOperationStatus = "complete" // Once received one OperationErrored hook message - ResourceStatusErrored ResourceStatus = "error" + ResourceOperationStatusErrored ResourceOperationStatus = "error" // TODO: Support refresh? (refresh is an independent lifecycle than the resource apply lifecycle) // TODO: Support provision? (provision is a intermidiate stage in the resource apply lifecycle) ) -func resourceStatusEmoji(status ResourceStatus) string { +func resourceOperationStatusEmoji(status ResourceOperationStatus) string { switch status { - case ResourceStatusStart: + case ResourceOperationStatusStart: return "🕛" - case ResourceStatusComplete: + case ResourceOperationStatusComplete: return "✅" - case ResourceStatusErrored: + case ResourceOperationStatusErrored: return "❌" default: return "❓" } } -type ResourceInfoLocator struct { +type ResourceOperationInfoLocator struct { Module string ResourceAddr string Action string } -type ResourceInfo struct { +type ResourceOperationInfo struct { Idx int RawResourceAddr json.ResourceAddr - Loc ResourceInfoLocator - Status ResourceStatus + Loc ResourceOperationInfoLocator + Status ResourceOperationStatus StartTime time.Time EndTime time.Time } -type ResourceInfoUpdate struct { - Status *ResourceStatus +type ResourceOperationInfoUpdate struct { + Status *ResourceOperationStatus Endtime *time.Time } -// ResourceInfos records the operation information for each resource's action. -type ResourceInfos []*ResourceInfo +// ResourceOperationInfos records the operation information for each resource's action. +type ResourceOperationInfos []*ResourceOperationInfo -func (infos ResourceInfos) Find(loc ResourceInfoLocator) *ResourceInfo { +func (infos ResourceOperationInfos) Find(loc ResourceOperationInfoLocator) *ResourceOperationInfo { for _, info := range infos { if info.Loc == loc { return info @@ -69,7 +69,7 @@ func (infos ResourceInfos) Find(loc ResourceInfoLocator) *ResourceInfo { return nil } -func (infos ResourceInfos) Update(loc ResourceInfoLocator, update ResourceInfoUpdate) *ResourceInfo { +func (infos ResourceOperationInfos) Update(loc ResourceOperationInfoLocator, update ResourceOperationInfoUpdate) *ResourceOperationInfo { info := infos.Find(loc) if info == nil { return nil @@ -85,7 +85,7 @@ func (infos ResourceInfos) Update(loc ResourceInfoLocator, update ResourceInfoUp // ToRows turns the ResourceInfos into table rows. // The total is used to decorate the index as a fraction, if total > 0. -func (infos ResourceInfos) ToRows(total int) []table.Row { +func (infos ResourceOperationInfos) ToRows(total int) []table.Row { now := time.Now() var rows []table.Row for _, info := range infos { @@ -103,7 +103,7 @@ func (infos ResourceInfos) ToRows(total int) []table.Row { row := []string{ idx, - resourceStatusEmoji(info.Status), + resourceOperationStatusEmoji(info.Status), string(info.Loc.Action), module, info.Loc.ResourceAddr, @@ -114,7 +114,7 @@ func (infos ResourceInfos) ToRows(total int) []table.Row { return rows } -func (infos ResourceInfos) ToColumns(width int) []table.Column { +func (infos ResourceOperationInfos) ToColumns(width int) []table.Column { const statusWidth = 6 const actionWidth = 8 const timeWidth = 24 @@ -135,7 +135,7 @@ func (infos ResourceInfos) ToColumns(width int) []table.Column { } } -func (infos ResourceInfos) ToCsv(stage string) []string { +func (infos ResourceOperationInfos) ToCsv(stage string) []string { var out []string now := time.Now() for _, info := range infos { @@ -157,7 +157,7 @@ func (infos ResourceInfos) ToCsv(stage string) []string { return out } -func (info ResourceInfo) Duration(now time.Time) time.Duration { +func (info ResourceOperationInfo) Duration(now time.Time) time.Duration { var dur time.Duration if info.EndTime.Equal(time.Time{}) { dur = now.Sub(info.StartTime).Truncate(time.Second) diff --git a/internal/ui/ui.go b/internal/ui/ui.go index d89ebaf..4c6c055 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -48,10 +48,10 @@ type UIModel struct { diags Diags - refreshInfos state.ResourceInfos - applyInfos state.ResourceInfos - - outputInfos state.OutputInfos + refreshInfos state.ResourceOperationInfos + planInfos state.PlanInfos + applyInfos state.ResourceOperationInfos + outputInfos state.OutputInfos versionMsg *string @@ -244,14 +244,20 @@ func (m UIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // There's no much useful information for now. case views.PlannedChangeMsg: + m.planInfos = append(m.planInfos, &state.PlanInfo{ + Resource: msg.Change.Resource, + Action: msg.Change.Action, + PrevResource: msg.Change.PreviousResource, + Reason: msg.Change.Reason, + }) + // Normally, we don't need to handle the PlannedChangeMsg here, as the ChangeSummaryMsg has all these information. // The exception is that when apply with a plan file, there is no ChangeSummaryMsg sent from Terraform at this moment. // (see: https://github.com/magodo/pipeform/issues/1) // The counting here is a fallback logic to cover the case above. Otherwise, it will just be overwritten by ChangeSummaryMsg. // // TODO: Once https://github.com/hashicorp/terraform/pull/36245 merged, remove this part. - - m.logger.Debug("Planned Change", "action", msg.Change.Action, "resource", msg.Change.Resource.Addr, "prev_resource", msg.Change.PreviousResource) + // // Referencing the logic of terraform: internal/command/views/operation.go // But we also count the "import" switch msg.Change.Action { @@ -296,27 +302,27 @@ func (m UIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.logger.Debug("Hook message", "type", fmt.Sprintf("%T", msg.Hook)) switch hook := msg.Hook.(type) { case json.RefreshStart: - res := &state.ResourceInfo{ + res := &state.ResourceOperationInfo{ Idx: len(m.refreshInfos) + 1, RawResourceAddr: hook.Resource, - Loc: state.ResourceInfoLocator{ + Loc: state.ResourceOperationInfoLocator{ Module: hook.Resource.Module, ResourceAddr: hook.Resource.Addr, Action: "refresh", }, - Status: state.ResourceStatusStart, + Status: state.ResourceOperationStatusStart, StartTime: msg.TimeStamp, } m.refreshInfos = append(m.refreshInfos, res) case json.RefreshComplete: - loc := state.ResourceInfoLocator{ + loc := state.ResourceOperationInfoLocator{ Module: hook.Resource.Module, ResourceAddr: hook.Resource.Addr, Action: "refresh", } - status := state.ResourceStatusComplete - update := state.ResourceInfoUpdate{ + status := state.ResourceOperationStatusComplete + update := state.ResourceOperationInfoUpdate{ Status: &status, Endtime: &msg.TimeStamp, } @@ -326,15 +332,15 @@ func (m UIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case json.OperationStart: - res := &state.ResourceInfo{ + res := &state.ResourceOperationInfo{ Idx: len(m.applyInfos) + 1, RawResourceAddr: hook.Resource, - Loc: state.ResourceInfoLocator{ + Loc: state.ResourceOperationInfoLocator{ Module: hook.Resource.Module, ResourceAddr: hook.Resource.Addr, Action: string(hook.Action), }, - Status: state.ResourceStatusStart, + Status: state.ResourceOperationStatusStart, StartTime: msg.TimeStamp, } m.applyInfos = append(m.applyInfos, res) @@ -343,13 +349,13 @@ func (m UIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Ignore case json.OperationComplete: - loc := state.ResourceInfoLocator{ + loc := state.ResourceOperationInfoLocator{ Module: hook.Resource.Module, ResourceAddr: hook.Resource.Addr, Action: string(hook.Action), } - status := state.ResourceStatusComplete - update := state.ResourceInfoUpdate{ + status := state.ResourceOperationStatusComplete + update := state.ResourceOperationInfoUpdate{ Status: &status, Endtime: &msg.TimeStamp, } @@ -363,13 +369,13 @@ func (m UIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, m.progress.SetPercent(percentage)) case json.OperationErrored: - loc := state.ResourceInfoLocator{ + loc := state.ResourceOperationInfoLocator{ Module: hook.Resource.Module, ResourceAddr: hook.Resource.Addr, Action: string(hook.Action), } - status := state.ResourceStatusErrored - update := state.ResourceInfoUpdate{ + status := state.ResourceOperationStatusErrored + update := state.ResourceOperationInfoUpdate{ Status: &status, Endtime: &msg.TimeStamp, } @@ -432,6 +438,8 @@ func (m *UIModel) setTableOutlook() { switch m.getViewState() { case ViewStateRefresh: m.table.SetColumns(m.refreshInfos.ToColumns(m.tableSize.Width)) + case ViewStatePlan: + m.table.SetColumns(m.planInfos.ToColumns(m.tableSize.Width)) case ViewStateApply: m.table.SetColumns(m.applyInfos.ToColumns(m.tableSize.Width)) case ViewStateSummary: @@ -444,6 +452,8 @@ func (m *UIModel) setTableRows() { switch m.getViewState() { case ViewStateRefresh: m.table.SetRows(m.refreshInfos.ToRows(0)) + case ViewStatePlan: + m.table.SetRows(m.planInfos.ToRows()) case ViewStateApply: m.table.SetRows(m.applyInfos.ToRows(m.totalCnt)) case ViewStateSummary: @@ -528,7 +538,9 @@ func (m UIModel) View() string { s += "\n\n" + m.stateView() - s += "\n\n" + StyleTableBase.Render(m.table.View()) + if m.getViewState() != ViewStateIdle { + s += "\n\n" + StyleTableBase.Render(m.table.View()) + } var progressBar string if m.getViewState() == ViewStateApply {