Skip to content

Commit c4a9f8e

Browse files
committed
chore: Integrating vfs and optimizing some low-hanging fruit
1 parent 9bda0b8 commit c4a9f8e

11 files changed

Lines changed: 234 additions & 77 deletions

File tree

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

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ package redesign
22

33
import (
44
"io/fs"
5-
"os"
65
"path/filepath"
76
"strings"
87

98
"github.com/gruntwork-io/terragrunt/internal/errors"
109
"github.com/gruntwork-io/terragrunt/internal/services/catalog/ignore"
1110
"github.com/gruntwork-io/terragrunt/internal/services/catalog/module"
1211
"github.com/gruntwork-io/terragrunt/internal/util"
12+
"github.com/gruntwork-io/terragrunt/internal/vfs"
1313
"github.com/gruntwork-io/terragrunt/pkg/config"
1414
)
1515

@@ -41,12 +41,13 @@ const (
4141
// Construct one via NewComponentDiscovery, customize it with the With*
4242
// methods, then call Discover on a repo.
4343
type ComponentDiscovery struct {
44+
fsys vfs.FS
4445
extraIgnoreFile string
4546
walkWithSymlinks bool
4647
}
4748

4849
// NewComponentDiscovery returns a ComponentDiscovery with default settings:
49-
// no symlink following, no extra ignore file.
50+
// no symlink following, no extra ignore file, the real OS filesystem.
5051
func NewComponentDiscovery() *ComponentDiscovery {
5152
return &ComponentDiscovery{}
5253
}
@@ -65,6 +66,13 @@ func (cd *ComponentDiscovery) WithExtraIgnoreFile(i string) *ComponentDiscovery
6566
return cd
6667
}
6768

69+
// WithFS sets the filesystem used for the discovery walk and per-component
70+
// README reads. When unset, Discover uses vfs.NewOSFS().
71+
func (cd *ComponentDiscovery) WithFS(fsys vfs.FS) *ComponentDiscovery {
72+
cd.fsys = fsys
73+
return cd
74+
}
75+
6876
// Discover runs component discovery against repo.
6977
func (cd *ComponentDiscovery) Discover(repo *module.Repo) (Components, error) {
7078
if repo == nil {
@@ -78,18 +86,28 @@ func (cd *ComponentDiscovery) Discover(repo *module.Repo) (Components, error) {
7886
return nil, errors.New("ComponentDiscovery.Discover: empty repo path")
7987
}
8088

81-
walkFunc := filepath.WalkDir
89+
fsys := cd.fsys
90+
if fsys == nil {
91+
fsys = vfs.NewOSFS()
92+
}
93+
94+
// util.WalkDirWithSymlinks is OS-only; when symlink following is on, we
95+
// continue using it and leave vfs integration as a future cleanup.
96+
walkFunc := func(root string, fn fs.WalkDirFunc) error {
97+
return vfs.WalkDir(fsys, root, fn)
98+
}
99+
82100
if cd.walkWithSymlinks {
83101
walkFunc = util.WalkDirWithSymlinks
84102
}
85103

86-
ignoreMatcher, err := ignore.Load(repoPath)
104+
ignoreMatcher, err := ignore.Load(fsys, repoPath)
87105
if err != nil {
88106
return nil, err
89107
}
90108

91109
if cd.extraIgnoreFile != "" {
92-
extraMatcher, err := ignore.LoadFile(cd.extraIgnoreFile)
110+
extraMatcher, err := ignore.LoadFile(fsys, cd.extraIgnoreFile)
93111
if err != nil {
94112
return nil, err
95113
}
@@ -126,7 +144,7 @@ func (cd *ComponentDiscovery) Discover(repo *module.Repo) (Components, error) {
126144
return fs.SkipDir
127145
}
128146

129-
kind, isComponent, err := classifyDir(dir)
147+
kind, isComponent, err := classifyDir(fsys, dir)
130148
if err != nil {
131149
return err
132150
}
@@ -135,7 +153,7 @@ func (cd *ComponentDiscovery) Discover(repo *module.Repo) (Components, error) {
135153
return nil
136154
}
137155

138-
c, err := newComponent(repo, repoPath, cloneURL, relDir, kind)
156+
c, err := newComponent(fsys, repo, repoPath, cloneURL, relDir, kind)
139157
if err != nil {
140158
return err
141159
}
@@ -164,8 +182,8 @@ func (cd *ComponentDiscovery) Discover(repo *module.Repo) (Components, error) {
164182
// classifyDir inspects a single directory and returns its ComponentKind.
165183
// Precedence: stack > unit > template > module. A terragrunt.stack.hcl wins
166184
// over a terragrunt.hcl, a .boilerplate/, and plain .tf files.
167-
func classifyDir(dir string) (ComponentKind, bool, error) {
168-
entries, err := os.ReadDir(dir)
185+
func classifyDir(fsys vfs.FS, dir string) (ComponentKind, bool, error) {
186+
entries, err := readDirEntries(fsys, dir)
169187
if err != nil {
170188
return 0, false, errors.New(err)
171189
}
@@ -229,8 +247,8 @@ func isSkippableDir(name string) bool {
229247
// newComponent constructs a *Component for a directory that has been
230248
// classified. It populates the doc and URL fields the same way the legacy
231249
// module.NewModule does, but into the redesign-owned Component type.
232-
func newComponent(repo *module.Repo, repoPath, cloneURL, relDir string, kind ComponentKind) (*Component, error) {
233-
doc, err := FindComponentDoc(filepath.Join(repoPath, relDir))
250+
func newComponent(fsys vfs.FS, repo *module.Repo, repoPath, cloneURL, relDir string, kind ComponentKind) (*Component, error) {
251+
doc, err := FindComponentDoc(fsys, filepath.Join(repoPath, relDir))
234252
if err != nil {
235253
return nil, err
236254
}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,31 @@ import (
88
"github.com/gruntwork-io/terragrunt/internal/cli/commands/catalog/tui/redesign"
99

1010
"github.com/gruntwork-io/terragrunt/internal/services/catalog/module"
11+
"github.com/gruntwork-io/terragrunt/internal/vfs"
1112
"github.com/gruntwork-io/terragrunt/test/helpers"
1213
"github.com/gruntwork-io/terragrunt/test/helpers/logger"
1314
"github.com/stretchr/testify/assert"
1415
"github.com/stretchr/testify/require"
1516
)
1617

18+
// TestDiscoverComponents_WithCustomFS proves discovery runs against an
19+
// injected vfs.FS — passing vfs.NewOSFS() explicitly produces the same
20+
// result as the zero-arg constructor's internal default.
21+
func TestDiscoverComponents_WithCustomFS(t *testing.T) {
22+
t.Parallel()
23+
24+
repoDir := helpers.TmpDirWOSymlinks(t)
25+
writeFile(t, filepath.Join(repoDir, "foo", "main.tf"), "# module")
26+
27+
repo := newFakeRepo(t, repoDir)
28+
29+
components, err := redesign.NewComponentDiscovery().WithFS(vfs.NewOSFS()).Discover(repo)
30+
require.NoError(t, err)
31+
require.Len(t, components, 1)
32+
assert.Equal(t, "foo", components[0].Dir)
33+
assert.Equal(t, redesign.ComponentKindModule, components[0].Kind)
34+
}
35+
1736
// newFakeRepo creates a bare-minimum cloned repo on disk that module.NewRepo
1837
// can successfully consume (requires .git/config and .git/HEAD). The returned
1938
// *module.Repo has the walk-relevant Path() / CloneURL() pointing at repoDir.

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

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
package redesign
22

33
import (
4-
"os"
4+
"io/fs"
55
"path/filepath"
66
"regexp"
77
"strings"
88

99
"github.com/gruntwork-io/terragrunt/internal/errors"
10+
"github.com/gruntwork-io/terragrunt/internal/vfs"
1011
)
1112

1213
// Note: this file is a redesign-owned fork of internal/services/catalog/module/doc.go.
@@ -113,17 +114,17 @@ func NewComponentDoc(rawContent, fileExt string) *ComponentDoc {
113114
return doc
114115
}
115116

116-
// FindComponentDoc reads the first README-like file in dir and returns a
117-
// populated ComponentDoc. Returns a zero-value *ComponentDoc (non-nil) when
118-
// no README is present.
119-
func FindComponentDoc(dir string) (*ComponentDoc, error) {
120-
var filePath, fileExt string
121-
122-
files, err := os.ReadDir(dir)
117+
// FindComponentDoc reads the first README-like file in dir from fsys and
118+
// returns a populated ComponentDoc. Returns a zero-value *ComponentDoc
119+
// (non-nil) when no README is present.
120+
func FindComponentDoc(fsys vfs.FS, dir string) (*ComponentDoc, error) {
121+
files, err := readDirEntries(fsys, dir)
123122
if err != nil {
124123
return nil, errors.New(err)
125124
}
126125

126+
var filePath, fileExt string
127+
127128
for _, file := range files {
128129
if file.IsDir() {
129130
continue
@@ -147,14 +148,43 @@ func FindComponentDoc(dir string) (*ComponentDoc, error) {
147148
return &ComponentDoc{}, nil
148149
}
149150

150-
contentByte, err := os.ReadFile(filePath)
151+
contentByte, err := vfs.ReadFile(fsys, filePath)
151152
if err != nil {
152153
return nil, errors.New(err)
153154
}
154155

155156
return NewComponentDoc(string(contentByte), fileExt), nil
156157
}
157158

159+
// readDirEntries opens dir on fsys and returns its entries. It prefers the
160+
// fs.ReadDirFile fast path when the backing file supports it.
161+
func readDirEntries(fsys vfs.FS, dir string) ([]fs.DirEntry, error) {
162+
f, err := fsys.Open(dir)
163+
if err != nil {
164+
return nil, err
165+
}
166+
167+
defer func() {
168+
_ = f.Close()
169+
}()
170+
171+
if rdf, ok := f.(fs.ReadDirFile); ok {
172+
return rdf.ReadDir(-1)
173+
}
174+
175+
infos, err := f.Readdir(-1)
176+
if err != nil {
177+
return nil, err
178+
}
179+
180+
entries := make([]fs.DirEntry, len(infos))
181+
for i, info := range infos {
182+
entries[i] = vfs.FileInfoDirEntry{FileInfo: info}
183+
}
184+
185+
return entries, nil
186+
}
187+
158188
// Title returns the doc title from frontmatter or the first H1.
159189
func (doc *ComponentDoc) Title() string {
160190
if title := doc.parseFrontmatter(docTitle); title != "" {

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

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package redesign
22

33
import (
4+
stderrors "errors"
45
"io"
56
"io/fs"
67
"os"
78
"path/filepath"
89

910
"github.com/gruntwork-io/terragrunt/internal/errors"
11+
"github.com/gruntwork-io/terragrunt/internal/vfs"
1012
"github.com/gruntwork-io/terragrunt/pkg/log"
1113
"github.com/gruntwork-io/terragrunt/pkg/options"
1214
)
@@ -19,6 +21,7 @@ type CopyCmd struct {
1921
component *Component
2022
opts *options.TerragruntOptions
2123
logger log.Logger
24+
fsys vfs.FS
2225
result copyResult
2326
}
2427

@@ -35,15 +38,27 @@ func NewCopyCmd(logger log.Logger, opts *options.TerragruntOptions, c *Component
3538
return &CopyCmd{component: c, opts: opts, logger: logger}
3639
}
3740

41+
// WithFS overrides the filesystem used for source reads and destination writes.
42+
// When unset, Run uses vfs.NewOSFS().
43+
func (c *CopyCmd) WithFS(fsys vfs.FS) *CopyCmd {
44+
c.fsys = fsys
45+
return c
46+
}
47+
3848
func (c *CopyCmd) Run() error {
49+
fsys := c.fsys
50+
if fsys == nil {
51+
fsys = vfs.NewOSFS()
52+
}
53+
3954
src, dst, err := c.resolvePaths()
4055
if err != nil {
4156
return err
4257
}
4358

4459
c.logger.Debugf("Copying component %q to %q", src, dst)
4560

46-
if err := copyDir(src, dst); err != nil {
61+
if err := copyDir(fsys, src, dst); err != nil {
4762
return err
4863
}
4964

@@ -54,7 +69,7 @@ func (c *CopyCmd) Run() error {
5469
return nil
5570
}
5671

57-
refs, err := CollectValuesReferences(filepath.Join(src, configName))
72+
refs, err := CollectValuesReferences(fsys, filepath.Join(src, configName))
5873
if err != nil {
5974
return err
6075
}
@@ -65,7 +80,7 @@ func (c *CopyCmd) Run() error {
6580

6681
c.result.references = refs
6782

68-
written, err := WriteValuesStub(dst, refs)
83+
written, err := WriteValuesStub(fsys, dst, refs)
6984
if err != nil {
7085
return err
7186
}
@@ -120,10 +135,10 @@ func skipDuringCopy(name string) bool {
120135
return name == ".terragrunt-cache" || name == ".terragrunt-stack"
121136
}
122137

123-
// copyDir recursively copies src to dst, preserving file modes and skipping
124-
// regenerated artifact directories.
125-
func copyDir(src, dst string) error {
126-
return filepath.WalkDir(src, func(path string, d fs.DirEntry, walkErr error) error {
138+
// copyDir recursively copies src to dst on fsys, preserving file modes and
139+
// skipping regenerated artifact directories.
140+
func copyDir(fsys vfs.FS, src, dst string) error {
141+
return vfs.WalkDir(fsys, src, func(path string, d fs.DirEntry, walkErr error) error {
127142
if walkErr != nil {
128143
return walkErr
129144
}
@@ -145,20 +160,20 @@ func copyDir(src, dst string) error {
145160
return errors.New(err)
146161
}
147162

148-
return os.MkdirAll(target, info.Mode().Perm())
163+
return fsys.MkdirAll(target, info.Mode().Perm())
149164
}
150165

151166
// Skip symlinks and irregular files; copy only regular files.
152167
if !d.Type().IsRegular() {
153168
return nil
154169
}
155170

156-
return copyFile(path, target)
171+
return copyFile(fsys, path, target)
157172
})
158173
}
159174

160-
func copyFile(src, dst string) error {
161-
in, err := os.Open(src)
175+
func copyFile(fsys vfs.FS, src, dst string) error {
176+
in, err := fsys.Open(src)
162177
if err != nil {
163178
return errors.New(err)
164179
}
@@ -176,9 +191,9 @@ func copyFile(src, dst string) error {
176191

177192
// O_EXCL ensures we refuse to overwrite existing files in the working
178193
// directory, so copying a unit or stack can't silently clobber user edits.
179-
out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, info.Mode().Perm())
194+
out, err := fsys.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, info.Mode().Perm())
180195
if err != nil {
181-
if os.IsExist(err) {
196+
if stderrors.Is(err, fs.ErrExist) {
182197
return errors.Errorf("destination %q already exists; refusing to overwrite", dst)
183198
}
184199

0 commit comments

Comments
 (0)