Skip to content

Commit 2c6cce0

Browse files
authored
chore: Adding VFS testing (#5490)
* chore: Adding VFS testing * chore: Adding VFS integration in `providercache` * fix: Refactoring to route integration with `afero` through `internal/vfs` so that it's easier to swap out should we need to in the future. * fix: Cleaning up names * fix: renaming `provider_cache.go` to `providercache.go` * fix: Removing unnecessary indirection for this check * chore: Addressing review comments * fix: Cleanup * fix: Fixing error handlings * fix: Cleaner APIs for managing structs * fix: Cleanup of `FileExists` * fix: Cleanup of builder pattern and unzip logic * fix: Adding a little testing * fix: Cleaning up implementation of zip decompressor * fix: Adding reasonable limits * fix: Addressing review feedback
1 parent 5e8d005 commit 2c6cce0

File tree

10 files changed

+1619
-131
lines changed

10 files changed

+1619
-131
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ require (
9494
github.com/gobwas/glob v0.2.3
9595
github.com/invopop/jsonschema v0.13.0
9696
github.com/mattn/go-shellwords v1.0.12
97+
github.com/spf13/afero v1.15.0
9798
github.com/testcontainers/testcontainers-go v0.40.0
9899
github.com/wI2L/jsondiff v0.7.0
99100
github.com/xeipuuv/gojsonschema v1.2.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -981,6 +981,8 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
981981
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
982982
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
983983
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
984+
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
985+
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
984986
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
985987
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
986988
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
Lines changed: 73 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/gruntwork-io/terragrunt/internal/tf/cliconfig"
2828
"github.com/gruntwork-io/terragrunt/internal/tf/getproviders"
2929
"github.com/gruntwork-io/terragrunt/internal/util"
30+
"github.com/gruntwork-io/terragrunt/internal/vfs"
3031
"github.com/gruntwork-io/terragrunt/pkg/log"
3132
"github.com/gruntwork-io/terragrunt/pkg/options"
3233
)
@@ -78,23 +79,46 @@ type ProviderCache struct {
7879
*cache.Server
7980
cliCfg *cliconfig.Config
8081
providerService *services.ProviderService
82+
fs vfs.FS
8183
}
8284

83-
func InitServer(l log.Logger, opts *options.TerragruntOptions) (*ProviderCache, error) {
85+
// NewProviderCache creates a new ProviderCache with sensible defaults.
86+
// Use builder methods like WithFS() to customize the configuration.
87+
func NewProviderCache() *ProviderCache {
88+
return &ProviderCache{
89+
fs: vfs.NewOSFS(),
90+
}
91+
}
92+
93+
// WithFS sets the filesystem for file operations and returns the ProviderCache
94+
// for method chaining. If not called, defaults to the real OS filesystem.
95+
func (pc *ProviderCache) WithFS(fs vfs.FS) *ProviderCache {
96+
pc.fs = fs
97+
return pc
98+
}
99+
100+
// FS returns the configured filesystem.
101+
func (pc *ProviderCache) FS() vfs.FS {
102+
return pc.fs
103+
}
104+
105+
// Init initializes the ProviderCache with the given logger and options.
106+
// Call this after configuring the ProviderCache with builder methods.
107+
func (pc *ProviderCache) Init(l log.Logger, opts *options.TerragruntOptions) error {
84108
// ProviderCacheDir has the same file structure as terraform plugin_cache_dir.
85109
// https://developer.hashicorp.com/terraform/cli/config/config-file#provider-plugin-cache
86110
if opts.ProviderCacheDir == "" {
87111
cacheDir, err := util.GetCacheDir()
88112
if err != nil {
89-
return nil, err
113+
return fmt.Errorf("failed to get cache directory: %w", err)
90114
}
91115

92116
opts.ProviderCacheDir = filepath.Join(cacheDir, "providers")
93117
}
94118

95119
var err error
96120
if opts.ProviderCacheDir, err = filepath.Abs(opts.ProviderCacheDir); err != nil {
97-
return nil, errors.New(err)
121+
return errors.New(err)
98122
}
99123

100124
if opts.ProviderCacheToken == "" {
@@ -105,25 +129,26 @@ func InitServer(l log.Logger, opts *options.TerragruntOptions) (*ProviderCache,
105129
opts.ProviderCacheToken = fmt.Sprintf("%s:%s", APIKeyAuth, opts.ProviderCacheToken)
106130
}
107131

108-
cliCfg, err := cliconfig.LoadUserConfig()
132+
// Pass filesystem to LoadUserConfig
133+
cliCfg, err := cliconfig.LoadUserConfig(cliconfig.WithFS(pc.FS()))
109134
if err != nil {
110-
return nil, err
135+
return err
111136
}
112137

113138
userProviderDir, err := cliconfig.UserProviderDir()
114139
if err != nil {
115-
return nil, err
140+
return err
116141
}
117142

118-
providerService := services.NewProviderService(opts.ProviderCacheDir, userProviderDir, cliCfg.CredentialsSource(), l)
143+
providerService := services.NewProviderService(opts.ProviderCacheDir, userProviderDir, cliCfg.CredentialsSource(), l, services.WithFS(pc.FS()))
119144
proxyProviderHandler := handlers.NewProxyProviderHandler(l, cliCfg.CredentialsSource())
120145

121146
providerHandlers, err := handlers.NewProviderHandlers(cliCfg, l, opts.ProviderCacheRegistryNames)
122147
if err != nil {
123-
return nil, errors.Errorf("creating provider handlers failed: %w", err)
148+
return errors.Errorf("creating provider handlers failed: %w", err)
124149
}
125150

126-
cache := cache.NewServer(
151+
cacheServer := cache.NewServer(
127152
cache.WithHostname(opts.ProviderCacheHostname),
128153
cache.WithPort(opts.ProviderCachePort),
129154
cache.WithToken(opts.ProviderCacheToken),
@@ -134,18 +159,29 @@ func InitServer(l log.Logger, opts *options.TerragruntOptions) (*ProviderCache,
134159
cache.WithLogger(l),
135160
)
136161

137-
return &ProviderCache{
138-
Server: cache,
139-
cliCfg: cliCfg,
140-
providerService: providerService,
141-
}, nil
162+
pc.Server = cacheServer
163+
pc.cliCfg = cliCfg
164+
pc.providerService = providerService
165+
166+
return nil
167+
}
168+
169+
// InitServer creates and initializes a new ProviderCache with the given logger and options.
170+
// This is a convenience function that combines NewProviderCache() and Init().
171+
func InitServer(l log.Logger, opts *options.TerragruntOptions) (*ProviderCache, error) {
172+
pc := NewProviderCache()
173+
if err := pc.Init(l, opts); err != nil {
174+
return nil, err
175+
}
176+
177+
return pc, nil
142178
}
143179

144180
// TerraformCommandHook warms up the providers cache, creates `.terraform.lock.hcl` and runs the `tofu/terraform init`
145181
// command with using this cache. Used as a hook function that is called after running the target tofu/terraform command.
146182
// For example, if the target command is `tofu plan`, it will be intercepted before it is run in the `/shell` package,
147183
// then control will be passed to this function to init the working directory using cached providers.
148-
func (cache *ProviderCache) TerraformCommandHook(
184+
func (pc *ProviderCache) TerraformCommandHook(
149185
ctx context.Context,
150186
l log.Logger,
151187
opts *options.TerragruntOptions,
@@ -182,18 +218,18 @@ func (cache *ProviderCache) TerraformCommandHook(
182218

183219
env := providerCacheEnvironment(opts, cliConfigFilename)
184220

185-
if output, err := cache.warmUpCache(ctx, l, opts, cliConfigFilename, args, env); err != nil {
221+
if output, err := pc.warmUpCache(ctx, l, opts, cliConfigFilename, args, env); err != nil {
186222
return output, err
187223
}
188224

189225
if skipRunTargetCommand {
190226
return &util.CmdOutput{}, nil
191227
}
192228

193-
return cache.runTerraformWithCache(ctx, l, opts, cliConfigFilename, args, env)
229+
return pc.runTerraformWithCache(ctx, l, opts, cliConfigFilename, args, env)
194230
}
195231

196-
func (cache *ProviderCache) warmUpCache(
232+
func (pc *ProviderCache) warmUpCache(
197233
ctx context.Context,
198234
l log.Logger,
199235
opts *options.TerragruntOptions,
@@ -207,7 +243,7 @@ func (cache *ProviderCache) warmUpCache(
207243
)
208244

209245
// Create terraform cli config file that enables provider caching and does not use provider cache dir
210-
if err := cache.createLocalCLIConfig(ctx, opts, cliConfigFilename, cacheRequestID); err != nil {
246+
if err := pc.createLocalCLIConfig(ctx, opts, cliConfigFilename, cacheRequestID); err != nil {
211247
return nil, err
212248
}
213249

@@ -222,7 +258,7 @@ func (cache *ProviderCache) warmUpCache(
222258
}
223259
}
224260

225-
caches, err := cache.providerService.WaitForCacheReady(cacheRequestID)
261+
caches, err := pc.providerService.WaitForCacheReady(cacheRequestID)
226262
if err != nil {
227263
return nil, err
228264
}
@@ -265,7 +301,7 @@ func (cache *ProviderCache) warmUpCache(
265301
return nil, err
266302
}
267303

268-
func (cache *ProviderCache) runTerraformWithCache(
304+
func (pc *ProviderCache) runTerraformWithCache(
269305
ctx context.Context,
270306
l log.Logger,
271307
opts *options.TerragruntOptions,
@@ -274,7 +310,7 @@ func (cache *ProviderCache) runTerraformWithCache(
274310
env map[string]string,
275311
) (*util.CmdOutput, error) {
276312
// Create terraform cli config file that uses provider cache dir
277-
if err := cache.createLocalCLIConfig(ctx, opts, cliConfigFilename, ""); err != nil {
313+
if err := pc.createLocalCLIConfig(ctx, opts, cliConfigFilename, ""); err != nil {
278314
return nil, err
279315
}
280316

@@ -318,8 +354,8 @@ func (cache *ProviderCache) runTerraformWithCache(
318354
// It creates two types of configuration depending on the `cacheRequestID` variable set.
319355
// 1. If `cacheRequestID` is set, `terraform init` does _not_ use the provider cache directory, the cache server creates a cache for requested providers and returns HTTP status 423. Since for each module we create the CLI config, using `cacheRequestID` we have the opportunity later retrieve from the cache server exactly those cached providers that were requested by `terraform init` using this configuration.
320356
// 2. If `cacheRequestID` is empty, 'terraform init` uses provider cache directory, the cache server acts as a proxy.
321-
func (cache *ProviderCache) createLocalCLIConfig(ctx context.Context, opts *options.TerragruntOptions, filename string, cacheRequestID string) error {
322-
cfg := cache.cliCfg.Clone()
357+
func (pc *ProviderCache) createLocalCLIConfig(ctx context.Context, opts *options.TerragruntOptions, filename string, cacheRequestID string) error {
358+
cfg := pc.cliCfg.Clone()
323359
cfg.PluginCacheDir = ""
324360

325361
// Filter registries based on OpenTofu or Terraform implementation to avoid contacting unnecessary registries
@@ -333,13 +369,13 @@ func (cache *ProviderCache) createLocalCLIConfig(ctx context.Context, opts *opti
333369
for _, registryName := range filteredRegistryNames {
334370
providerInstallationIncludes = append(providerInstallationIncludes, registryName+"/*/*")
335371

336-
apiURLs, err := cache.DiscoveryURL(ctx, registryName)
372+
apiURLs, err := pc.DiscoveryURL(ctx, registryName)
337373
if err != nil {
338374
return err
339375
}
340376

341377
cfg.AddHost(registryName, map[string]string{
342-
"providers.v1": fmt.Sprintf("%s/%s/%s/", cache.ProviderController.URL(), cacheRequestID, registryName),
378+
"providers.v1": fmt.Sprintf("%s/%s/%s/", pc.ProviderController.URL(), cacheRequestID, registryName),
343379
// Since Terragrunt Provider Cache only caches providers, we need to route module requests to the original registry.
344380
"modules.v1": ResolveModulesURL(registryName, apiURLs.ModulesV1),
345381
})
@@ -357,8 +393,17 @@ func (cache *ProviderCache) createLocalCLIConfig(ctx context.Context, opts *opti
357393
cliconfig.NewProviderInstallationDirect(nil, nil),
358394
)
359395

360-
if cfgDir := filepath.Dir(filename); !util.FileExists(cfgDir) {
361-
if err := os.MkdirAll(cfgDir, os.ModePerm); err != nil {
396+
// Use VFS for directory operations
397+
fs := pc.FS()
398+
cfgDir := filepath.Dir(filename)
399+
400+
cfgDirExists, err := vfs.FileExists(fs, cfgDir)
401+
if err != nil {
402+
return errors.New(err)
403+
}
404+
405+
if !cfgDirExists {
406+
if err := fs.MkdirAll(cfgDir, os.ModePerm); err != nil {
362407
return errors.New(err)
363408
}
364409
}

internal/providercache/provider_cache_test.go renamed to internal/providercache/providercache_test.go

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/gruntwork-io/terragrunt/internal/tf/cache/handlers"
1818
"github.com/gruntwork-io/terragrunt/internal/tf/cache/services"
1919
"github.com/gruntwork-io/terragrunt/internal/tf/cliconfig"
20+
"github.com/gruntwork-io/terragrunt/internal/vfs"
2021
"github.com/gruntwork-io/terragrunt/pkg/options"
2122
"github.com/gruntwork-io/terragrunt/test/helpers"
2223
"github.com/gruntwork-io/terragrunt/test/helpers/logger"
@@ -269,42 +270,62 @@ func TestProviderCache(t *testing.T) {
269270
}
270271
}
271272

272-
func TestProviderCacheWithProviderCacheDir(t *testing.T) {
273-
// testing.T can Setenv, but can't Unsetenv
274-
unsetEnv := func(t *testing.T, v string) {
275-
t.Helper()
276-
277-
// let testing.T do the recovery and work around t.Parallel()
278-
t.Setenv(v, "")
279-
require.NoError(t, os.Unsetenv(v))
280-
}
273+
func TestProviderCacheHomeless(t *testing.T) {
274+
cacheDir := helpers.TmpDirWOSymlinks(t)
281275

282-
t.Run("Homeless", func(t *testing.T) { //nolint:paralleltest
283-
cacheDir := helpers.TmpDirWOSymlinks(t)
276+
t.Setenv("HOME", "")
277+
require.NoError(t, os.Unsetenv("HOME"))
284278

285-
unsetEnv(t, "HOME")
286-
unsetEnv(t, "XDG_CACHE_HOME")
279+
t.Setenv("XDG_CACHE_HOME", "")
280+
require.NoError(t, os.Unsetenv("XDG_CACHE_HOME"))
287281

288-
_, err := providercache.InitServer(logger.CreateLogger(), &options.TerragruntOptions{
289-
ProviderCacheDir: cacheDir,
290-
})
291-
require.NoError(t, err, "ProviderCache shouldn't read HOME environment variable")
282+
_, err := providercache.InitServer(logger.CreateLogger(), &options.TerragruntOptions{
283+
ProviderCacheDir: cacheDir,
292284
})
285+
require.NoError(t, err, "ProviderCache shouldn't read HOME environment variable")
286+
}
287+
288+
func TestProviderCacheWithProviderCacheDir(t *testing.T) {
289+
t.Parallel()
293290

294291
t.Run("NoNewDirectoriesAtHOME", func(t *testing.T) {
295-
home := helpers.TmpDirWOSymlinks(t)
296-
cacheDir := helpers.TmpDirWOSymlinks(t)
292+
t.Parallel()
293+
294+
// Use in-memory filesystem to isolate file operations from the real filesystem.
295+
// This ensures InitServer doesn't create any directories on the real filesystem
296+
// since all file operations are routed through the VFS.
297+
memFs := vfs.NewMemMapFS()
298+
cacheDir := "/test/provider-cache"
299+
300+
server := providercache.NewProviderCache().WithFS(memFs)
301+
err := server.Init(
302+
logger.CreateLogger(),
303+
&options.TerragruntOptions{
304+
ProviderCacheDir: cacheDir,
305+
},
306+
)
307+
require.NoError(t, err)
308+
309+
// With VFS, all file operations go through the in-memory filesystem,
310+
// so no directories should be created on the real filesystem at all.
311+
// We can verify the VFS is being used by checking it's not empty or
312+
// by the fact that no errors occurred despite using fake paths.
313+
})
297314

298-
t.Setenv("HOME", home)
315+
t.Run("InitServerWithVFS", func(t *testing.T) {
316+
t.Parallel()
299317

300-
_, err := providercache.InitServer(logger.CreateLogger(), &options.TerragruntOptions{
301-
ProviderCacheDir: cacheDir,
302-
})
303-
require.NoError(t, err)
318+
memFs := vfs.NewMemMapFS()
319+
cacheDir := "/vfs/provider-cache"
304320

305-
// Cache server shouldn't create any directory at $HOME when ProviderCacheDir is specified
306-
entries, err := os.ReadDir(home)
321+
server := providercache.NewProviderCache().WithFS(memFs)
322+
err := server.Init(
323+
logger.CreateLogger(),
324+
&options.TerragruntOptions{
325+
ProviderCacheDir: cacheDir,
326+
},
327+
)
307328
require.NoError(t, err)
308-
require.Empty(t, entries, "No new directories should be created at $HOME")
329+
require.NotNil(t, server, "Init should return a valid server when using VFS")
309330
})
310331
}

0 commit comments

Comments
 (0)