Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions internal/cli/commands/catalog/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/gruntwork-io/terragrunt/internal/cli/commands/catalog/tui"
"github.com/gruntwork-io/terragrunt/internal/configbridge"
"github.com/gruntwork-io/terragrunt/internal/venv"
"github.com/gruntwork-io/terragrunt/pkg/config"
"github.com/gruntwork-io/terragrunt/pkg/log"
"github.com/gruntwork-io/terragrunt/pkg/options"
Expand All @@ -29,32 +30,32 @@ const urlChannelBufferSize = 10
// loaded; otherwise source discovery walks the configuration to find catalog
// and source URLs. As components are found, the TUI transitions to the
// component list, or shows a welcome screen when nothing is discovered.
func Run(ctx context.Context, l log.Logger, opts *options.TerragruntOptions, repoURL string) error {
func Run(ctx context.Context, l log.Logger, v venv.Venv, opts *options.TerragruntOptions, repoURL string) error {
// Fail fast with a clear error when there is no terminal to attach the
// TUI to, instead of surfacing bubbletea's raw TTY failure.
if err := tui.EnsureOSTTY(l); err != nil {
return err
}

return tui.Run(
ctx, l, opts, opts.Writers.ErrWriter,
ctx, l, v, opts, opts.Writers.ErrWriter,
func(
ctx context.Context, status tui.StatusFunc, componentCh chan<- *tui.ComponentEntry,
) error {
if repoURL != "" {
status("Loading " + repoURL + "...")

return tui.LoadURL(ctx, l, opts, repoURL, componentCh)
return tui.LoadURL(ctx, l, v, opts, repoURL, componentCh)
}

return discoverAndLoad(ctx, l, opts, status, componentCh)
return discoverAndLoad(ctx, l, v, opts, status, componentCh)
})
}

// discoverAndLoad runs the two concurrent URL discoverers and loads each
// distinct repo URL they surface into componentCh, bounded by parallelism.
func discoverAndLoad(
ctx context.Context, l log.Logger, opts *options.TerragruntOptions,
ctx context.Context, l log.Logger, v venv.Venv, opts *options.TerragruntOptions,
status tui.StatusFunc, componentCh chan<- *tui.ComponentEntry,
) error {
urlCh := make(chan string, urlChannelBufferSize)
Expand Down Expand Up @@ -103,7 +104,7 @@ func discoverAndLoad(
seen[repoURL] = struct{}{}

loaders.Go(func() error {
err := tui.LoadURL(loadCtx, l, opts, repoURL, componentCh)
err := tui.LoadURL(loadCtx, l, v, opts, repoURL, componentCh)
if err == nil {
return nil
}
Expand Down
5 changes: 3 additions & 2 deletions internal/cli/commands/catalog/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/gruntwork-io/terragrunt/internal/cli/flags"
"github.com/gruntwork-io/terragrunt/internal/cli/flags/shared"
"github.com/gruntwork-io/terragrunt/internal/clihelper"
"github.com/gruntwork-io/terragrunt/internal/venv"
"github.com/gruntwork-io/terragrunt/pkg/log"
"github.com/gruntwork-io/terragrunt/pkg/options"
)
Expand Down Expand Up @@ -68,7 +69,7 @@ func NewFlags(opts *options.TerragruntOptions, prefix flags.Prefix) clihelper.Fl
return append(shared.NewScaffoldingFlags(opts, prefix), catalogFlags...)
}

func NewCommand(l log.Logger, opts *options.TerragruntOptions) *clihelper.Command {
func NewCommand(l log.Logger, opts *options.TerragruntOptions, v venv.Venv) *clihelper.Command {
return &clihelper.Command{
Name: CommandName,
Usage: "Launch the user interface for searching and managing your module catalog.",
Expand All @@ -84,7 +85,7 @@ func NewCommand(l log.Logger, opts *options.TerragruntOptions) *clihelper.Comman
opts.ScaffoldRootFileName = scaffold.GetDefaultRootFileName(ctx, opts)
}

return Run(ctx, l, opts.OptionsFromContext(ctx), repoPath)
return Run(ctx, l, v, opts.OptionsFromContext(ctx), repoPath)
},
}
}
11 changes: 5 additions & 6 deletions internal/cli/commands/catalog/tui/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/gruntwork-io/terragrunt/internal/experiment"
"github.com/gruntwork-io/terragrunt/internal/services/catalog/module"
"github.com/gruntwork-io/terragrunt/internal/util"
"github.com/gruntwork-io/terragrunt/internal/vfs"
"github.com/gruntwork-io/terragrunt/internal/venv"
"github.com/gruntwork-io/terragrunt/pkg/log"
"github.com/gruntwork-io/terragrunt/pkg/options"
)
Expand All @@ -36,6 +36,7 @@ func CatalogTempPath(repoURL string) string {
func LoadURL(
ctx context.Context,
l log.Logger,
v venv.Venv,
opts *options.TerragruntOptions,
repoURL string,
componentCh chan<- *ComponentEntry,
Expand All @@ -53,9 +54,7 @@ func LoadURL(

l.Debugf("Processing repository %s in temporary path %s", repoURL, tempPath)

fsys := vfs.NewOSFS()

repo, err := module.NewRepo(ctx, l, fsys, &module.RepoOpts{
repo, err := module.NewRepo(ctx, l, v.FS, &module.RepoOpts{
CloneURL: repoURL,
Path: tempPath,
WalkWithSymlinks: walkWithSymlinks,
Expand All @@ -68,7 +67,7 @@ func LoadURL(
return fmt.Errorf("failed to initialize repository %s: %w", repoURL, err)
}

discovery := NewComponentDiscovery().WithFS(fsys).WithExtraIgnoreFile(opts.CatalogIgnoreFile)
discovery := NewComponentDiscovery().WithFS(v.FS).WithExtraIgnoreFile(opts.CatalogIgnoreFile)
if walkWithSymlinks {
discovery = discovery.WithWalkWithSymlinks()
}
Expand All @@ -87,7 +86,7 @@ func LoadURL(

// Resolve the latest release tag once per repo. All components from the
// same repo share the Repo, so the tag is set for everyone.
repo.ResolveLatestTag(ctx, l)
repo.ResolveLatestTag(ctx, l, v.Exec)

source := ExtractRepoURL(repo.SourceURL())

Expand Down
130 changes: 99 additions & 31 deletions internal/cli/commands/catalog/tui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

"github.com/gruntwork-io/terragrunt/internal/cli/commands/catalog/tui/components/buttonbar"
"github.com/gruntwork-io/terragrunt/internal/cli/commands/scaffold"
"github.com/gruntwork-io/terragrunt/internal/venv"
"github.com/gruntwork-io/terragrunt/pkg/log"
"github.com/gruntwork-io/terragrunt/pkg/options"
)
Expand Down Expand Up @@ -68,44 +69,110 @@ type Model struct {
// work (e.g. scaffold.Prepare downloading sources) propagates the
// user's Ctrl+C through this context, so the call returns instead of
// blocking on an abandoned download.
ctx context.Context
lists [numTabs]list.Model
logger log.Logger
ctx context.Context
// lists holds one list per tab; activeTab indexes it. A component is
// inserted into every list whose tab filter accepts it, so the same
// entry can appear under All and its kind-specific tab at once.
lists [numTabs]list.Model
// logger surfaces non-fatal diagnostics (e.g. an unknown button press)
// and is handed down to the scaffold/copy/form-discovery leaves.
logger log.Logger
// terragruntOptions carries the resolved CLI options that drive
// discovery and scaffolding; it is threaded into every leaf operation.
terragruntOptions *options.TerragruntOptions
// selectedComponent is the entry the user acted on. It is carried into
// the pager and form so scaffold and "view source" know their target
// even after the list selection moves on.
selectedComponent *Component
delegateKeys *DelegateKeyMap
buttonBar *buttonbar.ButtonBar
componentCh chan *ComponentEntry
errCh chan error
mdRenderer *glamour.TermRenderer
form *FormModel
scaffoldPlan *scaffold.Plan
valuesRefs *ValuesReferences
// delegateKeys are the list-row keybindings (choose, interactive
// scaffold) matched while in the list view.
delegateKeys *DelegateKeyMap
// buttonBar is the pager's action strip (Scaffold / View Source). It is
// rebuilt per component because the available actions depend on kind.
buttonBar *buttonbar.ButtonBar
// componentCh streams discovered components from the background loader.
// A nil channel disables the listener (used by the non-streaming test
// constructor).
componentCh chan *ComponentEntry
// errCh carries the loader's final result, drained after componentCh
// closes so completion is observed only after every component.
errCh chan error
// mdRenderer is the cached glamour renderer for README markdown. It is
// reused while mdRendererWidth and mdRendererDark still match the
// current width and background, and rebuilt otherwise.
mdRenderer *glamour.TermRenderer
// form is the variable-entry form, nil until a formReadyMsg arrives
// from scaffold-variable discovery.
form *FormModel
// scaffoldPlan is the prepared plan backing the active form. Its
// temporary source download is cleaned up when the form is abandoned
// or the scaffold is consumed.
scaffoldPlan *scaffold.Plan
// valuesRefs holds the `values.*` references collected from a copyable
// unit/stack, used to build that component's values form.
valuesRefs *ValuesReferences
// terminalErr is the failure that ended the session (a scaffold, copy,
// or form-discovery error). Run returns it so the catalog command exits
// nonzero; a deliberate quit leaves it nil.
terminalErr error
// loadErr is a non-fatal discovery failure (some catalog sources failed
// to load while others produced components). It renders as a notice in
// the list view rather than ending the session.
loadErr error
pagerKeys PagerKeyMap
listKeys list.KeyMap
loadErr error
// venv is the root virtualized environment threaded from the CLI
// entrypoint, so scaffold and form-discovery leaves run against the
// same filesystem and exec handles as the rest of Terragrunt.
venv venv.Venv
// pagerKeys are the keybindings handled while reading a README in the
// pager view.
pagerKeys PagerKeyMap
// listKeys are the list-view keybindings, including quit.
listKeys list.KeyMap
// currentPagerButtons are the actions offered for the component in the
// pager; activeButton indexes into it.
currentPagerButtons []button
exitMessage string
viewport viewport.Model
activeButton button
State sessionState
priorState sessionState
activeTab TabKind
height int
width int
mdRendererWidth int
ready bool
loading bool
userNavigated bool
hasDarkBG bool
mdRendererDark bool
// exitMessage is the styled message stashed for printing after the alt
// screen tears down (a success callout or a failure notice), since
// writes during the alt screen are discarded.
exitMessage string
// viewport is the scrollable pager that renders README content.
viewport viewport.Model
// activeButton is the focused entry in currentPagerButtons.
activeButton button
// State is the current view (list, pager, form, or scaffold).
State sessionState
// priorState is the view to return to when a form is cancelled,
// recorded when the form is entered.
priorState sessionState
// activeTab is the focused tab (All / Modules / Templates / Stacks);
// it indexes lists.
activeTab TabKind
// height is the last terminal height, refreshed on each WindowSizeMsg
// and used to size the viewport and form.
height int
// width is the last terminal width, refreshed on each WindowSizeMsg and
// used to size the viewport, form, and markdown renderer.
width int
// mdRendererWidth is the width the cached mdRenderer was built for; a
// change invalidates it.
mdRendererWidth int
// ready reports whether the first WindowSizeMsg has sized the viewport,
// gating its one-time creation.
ready bool
// loading reports whether discovery is still streaming components; it
// drives the "(loading...)" suffix on the tab bar.
loading bool
// userNavigated reports whether the user has moved the list cursor.
// Until they do, newly streamed components keep the selection pinned to
// the top instead of shifting it.
userNavigated bool
// hasDarkBG is the detected terminal background brightness. It selects
// the markdown style and, when it changes, invalidates the cached
// renderer.
hasDarkBG bool
// mdRendererDark is the background brightness the cached mdRenderer was
// built for; a change invalidates it.
mdRendererDark bool
// softWrap toggles glamour's word-wrap in the pager view. Default true
// matches the prior behavior; the `w` key flips it so users reading a
// README with intentionally long lines (ascii diagrams, wide tables)
Expand All @@ -119,10 +186,10 @@ type Model struct {
// it can synthesize a DiscoveryCompleteMsg without racing the welcome model.
// ctx is the cancellable context the welcome layer hands down so off-UI work
// can observe Ctrl+C.
func NewModelStreaming(ctx context.Context, l log.Logger, opts *options.TerragruntOptions, initial *ComponentEntry, componentCh chan *ComponentEntry, errCh chan error) Model {
func NewModelStreaming(ctx context.Context, l log.Logger, v venv.Venv, opts *options.TerragruntOptions, initial *ComponentEntry, componentCh chan *ComponentEntry, errCh chan error) Model {
items := []list.Item{initial}

m := newModelWithItems(l, opts, items, componentCh)
m := newModelWithItems(l, v, opts, items, componentCh)
m.ctx = ctx
m.errCh = errCh
m.loading = true
Expand Down Expand Up @@ -331,7 +398,7 @@ func isDuplicate(items []list.Item, sourcePath string) bool {
return false
}

func newModelWithItems(l log.Logger, opts *options.TerragruntOptions, items []list.Item, componentCh chan *ComponentEntry) Model {
func newModelWithItems(l log.Logger, v venv.Venv, opts *options.TerragruntOptions, items []list.Item, componentCh chan *ComponentEntry) Model {
listKeys := NewListKeyMap()
delegateKeys := NewDelegateKeyMap()
pagerKeys := NewPagerKeyMap()
Expand Down Expand Up @@ -380,6 +447,7 @@ func newModelWithItems(l log.Logger, opts *options.TerragruntOptions, items []li
terragruntOptions: opts,
logger: l,
componentCh: componentCh,
venv: v,
// Matches lipgloss.HasDarkBackground's fallback. Corrected on the
// first tea.BackgroundColorMsg.
hasDarkBG: true,
Expand Down
Loading
Loading