internal/ui/ui.go (241 lines of code) (raw):

package ui import ( "context" "fmt" "github.com/Azure/aztfexport/internal/config" "github.com/Azure/aztfexport/internal/log" internalmeta "github.com/Azure/aztfexport/internal/meta" "github.com/Azure/aztfexport/pkg/meta" "github.com/Azure/aztfexport/internal/ui/aztfexportclient" "github.com/Azure/aztfexport/internal/ui/common" "github.com/mitchellh/go-wordwrap" "github.com/muesli/reflow/indent" "github.com/Azure/aztfexport/internal/ui/importlist" "github.com/Azure/aztfexport/internal/ui/progress" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" ) const indentLevel = 2 func NewProgram(ctx context.Context, cfg config.InteractiveModeConfig) (*tea.Program, error) { m, err := newModel(ctx, cfg) if err != nil { return nil, err } return tea.NewProgram(m, tea.WithAltScreen()), nil } type status int const ( statusInit status = iota statusListingResource statusBuildingImportList statusImporting statusImportErrorMsg statusGeneratingCfg statusCleaningUpWorkspaceCfg statusPushState statusExportResourceMapping statusExportSkippedResources statusSummary statusQuitting statusError ) func (s status) String() string { return [...]string{ "initializing", "listing Azure resources", "building import list", "importing", "import error message", "generating Terraform configuration", "cleaning up output directory", "pushing state", "exporting resource mapping file", "exporting skipped resources file", "summary", "quitting", "error", }[s] } type model struct { ctx context.Context meta meta.Meta parallelism int status status err error // winsize is used to keep track of current windows size, it is used to set the size for other models that are initialized in status (e.g. the importlist). winsize tea.WindowSizeMsg spinner spinner.Model importlist importlist.Model progress progress.Model importerrormsg aztfexportclient.ShowImportErrorMsg } func newModel(ctx context.Context, cfg config.InteractiveModeConfig) (*model, error) { s := spinner.NewModel() s.Spinner = common.Spinner var c meta.Meta = internalmeta.NewGroupMetaDummy(cfg.ResourceGroupName, cfg.ProviderName) if !cfg.MockMeta { var err error c, err = meta.NewMeta(cfg.Config) if err != nil { return nil, err } } m := &model{ ctx: ctx, meta: c, parallelism: cfg.Parallelism, status: statusInit, spinner: s, } return m, nil } func (m model) Init() tea.Cmd { return tea.Batch( aztfexportclient.NewClient(m.meta), spinner.Tick, ) } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if _, ok := msg.(spinner.TickMsg); !ok { m.meta.Logger().Log(context.Background(), log.LevelTrace, "UI update", "status", m.status, "msg", fmt.Sprintf("%#v", msg)) } switch msg := msg.(type) { case tea.WindowSizeMsg: m.winsize = msg case tea.KeyMsg: switch msg.Type { case tea.KeyCtrlC: m.status = statusQuitting return m, aztfexportclient.Quit(m.ctx, m.meta) } case spinner.TickMsg: var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd case aztfexportclient.NewClientMsg: m.meta = msg m.status = statusInit return m, aztfexportclient.Init(m.ctx, m.meta) case aztfexportclient.InitProviderDoneMsg: m.status = statusListingResource return m, aztfexportclient.ListResource(m.ctx, m.meta) case aztfexportclient.ListResourceDoneMsg: m.status = statusBuildingImportList m.importlist = importlist.NewModel(m.ctx, m.meta, msg.List, 0) // Trigger a windows resize cmd to resize the importlist model. // Though we can pass the winsize as input variable during model initialization. // But this way we only need to maintain the resizing logic at one place (which takes consideration of the title height). cmd := func() tea.Msg { return m.winsize } return m, cmd case aztfexportclient.ShowImportErrorMsg: m.status = statusImportErrorMsg m.importerrormsg = msg return m, nil case aztfexportclient.StartImportMsg: m.status = statusImporting m.progress = progress.NewModel(m.ctx, m.meta, m.parallelism, msg.List) return m, tea.Batch( m.progress.Init(), // Resize the progress bar func() tea.Msg { return m.winsize }, ) case aztfexportclient.ImportDoneMsg: for idx, item := range msg.List { if item.ImportError != nil { m.status = statusBuildingImportList m.importlist = importlist.NewModel(m.ctx, m.meta, msg.List, idx) cmd := func() tea.Msg { return m.winsize } return m, cmd } } m.status = statusPushState return m, aztfexportclient.PushState(m.ctx, m.meta, msg.List) case aztfexportclient.PushStateDoneMsg: m.status = statusExportResourceMapping return m, aztfexportclient.ExportResourceMapping(m.ctx, m.meta, msg.List) case aztfexportclient.ExportResourceMappingDoneMsg: m.status = statusExportSkippedResources return m, aztfexportclient.ExportSkippedResources(m.ctx, m.meta, msg.List) case aztfexportclient.ExportSkippedResourcesDoneMsg: m.status = statusGeneratingCfg return m, aztfexportclient.GenerateCfg(m.ctx, m.meta, msg.List) case aztfexportclient.GenerateCfgDoneMsg: m.status = statusCleaningUpWorkspaceCfg return m, aztfexportclient.CleanUpWorkspace(m.ctx, m.meta) case aztfexportclient.WorkspaceCleanupDoneMsg: m.status = statusSummary return m, nil case aztfexportclient.QuitMsg: return m, tea.Quit case aztfexportclient.CleanTFStateMsg: m.meta.CleanTFState(m.ctx, msg.Addr) return m, nil case aztfexportclient.ErrMsg: m.status = statusError m.err = msg return m, nil } return updateChildren(msg, m) } func updateChildren(msg tea.Msg, m model) (model, tea.Cmd) { var cmd tea.Cmd switch m.status { case statusBuildingImportList: m.importlist, cmd = m.importlist.Update(msg) return m, cmd case statusImportErrorMsg: if _, ok := msg.(tea.KeyMsg); ok { m.status = statusBuildingImportList m.importlist = importlist.NewModel(m.ctx, m.meta, m.importerrormsg.List, m.importerrormsg.Index) cmd = func() tea.Msg { return m.winsize } return m, cmd } case statusImporting: m.progress, cmd = m.progress.Update(msg) return m, cmd case statusSummary: switch msg.(type) { case tea.KeyMsg: m.status = statusQuitting return m, aztfexportclient.Quit(m.ctx, m.meta) } } return m, nil } func (m model) View() string { s := m.logoView() switch m.status { case statusInit: s += m.spinner.View() + " Initializing..." case statusListingResource: s += m.spinner.View() + " Listing Azure Resources..." case statusBuildingImportList: s += m.importlist.View() case statusImportErrorMsg: s += importErrorView(m) case statusImporting: s += m.spinner.View() + m.progress.View() case statusPushState: s += m.spinner.View() + " Pushing Terraform Status..." case statusExportResourceMapping: s += m.spinner.View() + " Exporting Resource Mapping..." case statusExportSkippedResources: s += m.spinner.View() + " Exporting Skipped Resources..." case statusGeneratingCfg: s += m.spinner.View() + " Generating Terraform Configurations..." case statusCleaningUpWorkspaceCfg: s += m.spinner.View() + " Cleaning up the output directory..." case statusSummary: s += summaryView(m) case statusError: s += errorView(m) } return indent.String(s, indentLevel) } func (m model) logoView() string { return "\n" + common.TitleStyle.Render(" Microsoft Azure Export for Terraform ") + "\n\n" } func importErrorView(m model) string { // #nosec G115 return m.importerrormsg.Item.TFResourceId + "\n\n" + common.ErrorMsgStyle.Render(wordwrap.WrapString(m.importerrormsg.Item.ImportError.Error(), uint(m.winsize.Width-indentLevel))) } func summaryView(m model) string { return fmt.Sprintf("Terraform state and the config are generated at: %s\n\n", m.meta.Workspace()) + common.QuitMsgStyle.Render("Press any key to quit\n") } func errorView(m model) string { // #nosec G115 return common.ErrorMsgStyle.Render(wordwrap.WrapString(m.err.Error(), uint(m.winsize.Width-indentLevel))) }