Skip to content

Commit 9519647

Browse files
committed
feat: Adding support for values
1 parent 1b65aa8 commit 9519647

9 files changed

Lines changed: 860 additions & 41 deletions

File tree

internal/cli/commands/catalog/tui/redesign/component_discovery.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ const (
3535
// surfaced as separate components.
3636
//
3737
// Unlike the legacy module.Repo.FindModules walker (which only scans the
38-
// `modules/` convention), this walks the entire repo templates may live
39-
// anywhere, and the redesign treats module/template discovery uniformly.
38+
// `modules/` convention), this walks the entire repo, since templates may
39+
// live anywhere and the redesign treats all component kinds uniformly.
4040
//
4141
// Construct one via NewComponentDiscovery, customize it with the With*
4242
// methods, then call Discover on a repo.

internal/cli/commands/catalog/tui/redesign/component_discovery_test.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"testing"
77

88
"github.com/gruntwork-io/terragrunt/internal/cli/commands/catalog/tui/redesign"
9+
910
"github.com/gruntwork-io/terragrunt/internal/services/catalog/module"
1011
"github.com/gruntwork-io/terragrunt/test/helpers"
1112
"github.com/gruntwork-io/terragrunt/test/helpers/logger"
@@ -58,14 +59,14 @@ func TestDiscoverComponents_ClassifiesFixtureTree(t *testing.T) {
5859
// foo/ is a plain module (has main.tf, no boilerplate).
5960
writeFile(t, filepath.Join(repoDir, "foo", "main.tf"), "# vpc terraform")
6061

61-
// bar/ has a .boilerplate/ subdir — template at bar/.
62+
// bar/ has a .boilerplate/ subdir. Template at bar/.
6263
writeFile(t, filepath.Join(repoDir, "bar", ".boilerplate", "boilerplate.yml"), "variables: []\n")
6364
writeFile(t, filepath.Join(repoDir, "bar", ".boilerplate", "README.md"), "# bar template boilerplate dir")
6465

65-
// baz/ has a top-level boilerplate.yml — template at baz/.
66+
// baz/ has a top-level boilerplate.yml. Template at baz/.
6667
writeFile(t, filepath.Join(repoDir, "baz", "boilerplate.yml"), "variables: []\n")
6768

68-
// qux/ has both main.tf AND a .boilerplate/ — template wins.
69+
// qux/ has both main.tf AND a .boilerplate/. Template wins.
6970
writeFile(t, filepath.Join(repoDir, "qux", "main.tf"), "# mixed")
7071
writeFile(t, filepath.Join(repoDir, "qux", ".boilerplate", "boilerplate.yml"), "variables: []\n")
7172

@@ -128,11 +129,11 @@ func TestDiscoverComponents_UnitsAndStacks(t *testing.T) {
128129
writeFile(t, filepath.Join(repoDir, "mixed-stack", "terragrunt.stack.hcl"), "# stack")
129130
writeFile(t, filepath.Join(repoDir, "mixed-stack", "terragrunt.hcl"), "# also present")
130131

131-
// A nested .tf file under a unit must NOT surface as a second module
132-
// the unit's subtree is SkipDir'd.
132+
// A nested .tf file under a unit must NOT surface as a second module.
133+
// The unit's subtree is SkipDir'd.
133134
writeFile(t, filepath.Join(repoDir, "unit-a", "nested", "main.tf"), "# should not surface")
134135

135-
// A nested unit under a stack must NOT surface — the stack's subtree is
136+
// A nested unit under a stack must NOT surface. The stack's subtree is
136137
// SkipDir'd.
137138
writeFile(t, filepath.Join(repoDir, "stack-a", "generated", "terragrunt.hcl"), "# should not surface")
138139

internal/cli/commands/catalog/tui/redesign/copy.go

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,59 +11,93 @@ import (
1111
"github.com/gruntwork-io/terragrunt/pkg/options"
1212
)
1313

14-
// copyCmd is a tea.ExecCommand that copies a unit or stack component's
14+
// CopyCmd is a tea.ExecCommand that copies a unit or stack component's
1515
// directory tree into the user's working directory. Unlike scaffold, it does
16-
// not generate a new terragrunt.hclit simply materializes the component's
17-
// files so the user can edit them in place.
18-
type copyCmd struct {
16+
// not generate a new terragrunt.hcl; it materializes the component's files
17+
// so the user can edit them in place.
18+
type CopyCmd struct {
1919
component *Component
2020
opts *options.TerragruntOptions
2121
logger log.Logger
22+
result copyResult
2223
}
2324

24-
func newCopyCmd(logger log.Logger, opts *options.TerragruntOptions, c *Component) *copyCmd {
25-
return &copyCmd{component: c, opts: opts, logger: logger}
25+
// copyResult records what the copy step did beyond the raw file copy, so the
26+
// TUI can surface an appropriate exit message to the user.
27+
type copyResult struct { //nolint:govet // field order favors readability over GC-scan bytes
28+
references ValuesReferences
29+
workingDir string
30+
valuesWritten bool
31+
valuesSkipped bool
2632
}
2733

28-
func (c *copyCmd) Run() error {
34+
func NewCopyCmd(logger log.Logger, opts *options.TerragruntOptions, c *Component) *CopyCmd {
35+
return &CopyCmd{component: c, opts: opts, logger: logger}
36+
}
37+
38+
func (c *CopyCmd) Run() error {
2939
src, dst, err := c.resolvePaths()
3040
if err != nil {
3141
return err
3242
}
3343

3444
c.logger.Debugf("Copying component %q to %q", src, dst)
3545

36-
return copyDir(src, dst)
37-
}
46+
if err := copyDir(src, dst); err != nil {
47+
return err
48+
}
49+
50+
c.result.workingDir = dst
3851

39-
func (c *copyCmd) SetStdin(io.Reader) {}
40-
func (c *copyCmd) SetStdout(io.Writer) {}
41-
func (c *copyCmd) SetStderr(io.Writer) {}
52+
configName := configFileForKind(c.component.Kind)
53+
if configName == "" {
54+
return nil
55+
}
56+
57+
refs, err := CollectValuesReferences(filepath.Join(src, configName))
58+
if err != nil {
59+
return err
60+
}
4261

43-
// CopyCmdRunner is the test-visible contract for the copy command — it
44-
// exposes Run() so tests can execute the command without a full TUI loop.
45-
type CopyCmdRunner interface {
46-
Run() error
62+
if refs.IsEmpty() {
63+
return nil
64+
}
65+
66+
c.result.references = refs
67+
68+
written, err := WriteValuesStub(dst, refs)
69+
if err != nil {
70+
return err
71+
}
72+
73+
c.result.valuesWritten = written
74+
c.result.valuesSkipped = !written
75+
76+
return nil
4777
}
4878

49-
// NewCopyCmdForTest constructs a CopyCmdRunner for use in tests. It is
50-
// intentionally kept to a narrow surface so tests don't depend on internals.
51-
func NewCopyCmdForTest(logger log.Logger, opts *options.TerragruntOptions, c *Component) CopyCmdRunner {
52-
return newCopyCmd(logger, opts, c)
79+
// Result exposes the outcome of the last Run call. Intended for the TUI
80+
// update loop to format an exit message; tests may use it too.
81+
func (c *CopyCmd) Result() copyResult {
82+
return c.result
5383
}
5484

85+
func (c *CopyCmd) SetStdin(io.Reader) {}
86+
func (c *CopyCmd) SetStdout(io.Writer) {}
87+
func (c *CopyCmd) SetStderr(io.Writer) {}
88+
5589
// resolvePaths returns the absolute source directory (inside the cloned repo)
5690
// and the destination directory (the user's working directory) for this copy.
5791
// Files from src are materialized directly into the working directory so the
5892
// action mirrors how scaffold emits its output.
59-
func (c *copyCmd) resolvePaths() (string, string, error) {
93+
func (c *CopyCmd) resolvePaths() (string, string, error) {
6094
if c.component == nil || c.component.Repo == nil {
61-
return "", "", errors.New("copyCmd: nil component or repo")
95+
return "", "", errors.New("CopyCmd: nil component or repo")
6296
}
6397

6498
repoPath := c.component.Repo.Path()
6599
if repoPath == "" {
66-
return "", "", errors.New("copyCmd: empty repo path")
100+
return "", "", errors.New("CopyCmd: empty repo path")
67101
}
68102

69103
src := repoPath
@@ -73,7 +107,7 @@ func (c *copyCmd) resolvePaths() (string, string, error) {
73107

74108
workingDir := c.opts.WorkingDir
75109
if workingDir == "" {
76-
return "", "", errors.New("copyCmd: empty working directory")
110+
return "", "", errors.New("CopyCmd: empty working directory")
77111
}
78112

79113
return src, workingDir, nil

internal/cli/commands/catalog/tui/redesign/copy_test.go

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package redesign_test
22

33
import (
4+
"os"
45
"path/filepath"
6+
"strings"
57
"testing"
68

79
"github.com/gruntwork-io/terragrunt/internal/cli/commands/catalog/tui/redesign"
@@ -32,15 +34,92 @@ func TestCopyCmd_CopiesIntoWorkingDirectory(t *testing.T) {
3234
opts := options.NewTerragruntOptions()
3335
opts.WorkingDir = workingDir
3436

35-
cmd := redesign.NewCopyCmdForTest(logger.CreateLogger(), opts, components[0])
36-
require.NoError(t, cmd.Run())
37+
require.NoError(t, redesign.NewCopyCmd(logger.CreateLogger(), opts, components[0]).Run())
3738

3839
assert.FileExists(t, filepath.Join(workingDir, "terragrunt.hcl"))
3940
assert.FileExists(t, filepath.Join(workingDir, "inputs.hcl"))
4041
assert.NoFileExists(t, filepath.Join(workingDir, ".terragrunt-cache", "junk.txt"))
4142
assert.NoDirExists(t, filepath.Join(workingDir, ".terragrunt-cache"))
4243
}
4344

45+
func TestCopyCmd_WritesValuesStubForUnit(t *testing.T) {
46+
t.Parallel()
47+
48+
repoDir := helpers.TmpDirWOSymlinks(t)
49+
50+
unitBody := `
51+
locals {
52+
region = values.region
53+
}
54+
55+
inputs = {
56+
app = values.app
57+
env = values.env
58+
}
59+
`
60+
writeFile(t, filepath.Join(repoDir, "vpc", "terragrunt.hcl"), unitBody)
61+
62+
repo := newFakeRepo(t, repoDir)
63+
64+
components, err := redesign.NewComponentDiscovery().Discover(repo)
65+
require.NoError(t, err)
66+
require.Len(t, components, 1)
67+
68+
workingDir := t.TempDir()
69+
opts := options.NewTerragruntOptions()
70+
opts.WorkingDir = workingDir
71+
72+
require.NoError(t, redesign.NewCopyCmd(logger.CreateLogger(), opts, components[0]).Run())
73+
74+
valuesPath := filepath.Join(workingDir, "terragrunt.values.hcl")
75+
assert.FileExists(t, valuesPath)
76+
77+
raw, err := os.ReadFile(valuesPath)
78+
require.NoError(t, err)
79+
80+
content := string(raw)
81+
assert.Contains(t, content, "# Auto-generated by `terragrunt catalog`")
82+
83+
appIdx := strings.Index(content, "\napp")
84+
85+
envIdx := strings.Index(content, "\nenv")
86+
87+
regionIdx := strings.Index(content, "\nregion")
88+
89+
require.NotEqual(t, -1, appIdx)
90+
require.NotEqual(t, -1, envIdx)
91+
require.NotEqual(t, -1, regionIdx)
92+
assert.Less(t, appIdx, envIdx)
93+
assert.Less(t, envIdx, regionIdx)
94+
}
95+
96+
func TestCopyCmd_LeavesExistingValuesFileAlone(t *testing.T) {
97+
t.Parallel()
98+
99+
repoDir := helpers.TmpDirWOSymlinks(t)
100+
writeFile(t, filepath.Join(repoDir, "vpc", "terragrunt.hcl"),
101+
`locals { region = values.region }`)
102+
103+
repo := newFakeRepo(t, repoDir)
104+
105+
components, err := redesign.NewComponentDiscovery().Discover(repo)
106+
require.NoError(t, err)
107+
require.Len(t, components, 1)
108+
109+
workingDir := t.TempDir()
110+
existing := []byte(`region = "us-east-1"` + "\n")
111+
require.NoError(t, os.WriteFile(filepath.Join(workingDir, "terragrunt.values.hcl"), existing, 0o644))
112+
113+
opts := options.NewTerragruntOptions()
114+
opts.WorkingDir = workingDir
115+
116+
require.NoError(t, redesign.NewCopyCmd(logger.CreateLogger(), opts, components[0]).Run())
117+
118+
got, err := os.ReadFile(filepath.Join(workingDir, "terragrunt.values.hcl"))
119+
require.NoError(t, err)
120+
assert.Equal(t, existing, got, "pre-existing values file should be untouched")
121+
}
122+
44123
func TestCopyCmd_RefusesToOverwriteExistingFile(t *testing.T) {
45124
t.Parallel()
46125

@@ -59,8 +138,7 @@ func TestCopyCmd_RefusesToOverwriteExistingFile(t *testing.T) {
59138
opts := options.NewTerragruntOptions()
60139
opts.WorkingDir = workingDir
61140

62-
cmd := redesign.NewCopyCmdForTest(logger.CreateLogger(), opts, components[0])
63-
err = cmd.Run()
141+
err = redesign.NewCopyCmd(logger.CreateLogger(), opts, components[0]).Run()
64142
require.Error(t, err)
65143
assert.Contains(t, err.Error(), "already exists")
66144
}

internal/cli/commands/catalog/tui/redesign/model.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ type Model struct {
6161
pagerKeys tui.PagerKeyMap
6262
listKeys list.KeyMap
6363
currentPagerButtons []button
64+
exitMessage string
6465
viewport viewport.Model
6566
activeButton button
6667
State sessionState
@@ -72,6 +73,14 @@ type Model struct {
7273
userNavigated bool
7374
}
7475

76+
// ExitMessage returns the styled post-exit message the model set while
77+
// handling its final action (e.g., a successful copy that generated a
78+
// terragrunt.values.hcl file). The caller is responsible for printing it
79+
// after the tea.Program returns, once the alt screen has been torn down.
80+
func (m Model) ExitMessage() string { //nolint:gocritic
81+
return m.exitMessage
82+
}
83+
7584
// List returns the currently active list — the one filtered by the active
7685
// tab. Exposed for tests and view code that need to inspect items.
7786
func (m Model) List() list.Model { //nolint:gocritic

0 commit comments

Comments
 (0)