Skip to content

Commit f3b7c3f

Browse files
authored
chore: Add fast-copy strict control (#5966)
* feat: Adding `fast-copy` strict control * feat: Adding in `fastwalk` and `WalkDirParallel` in `vfs` * chore: Use options pattern over long parameter list * fix: Addressing lint findings * docs: Docs update * fix: Adding symlink following * fix: Fixing usage of `CopyFolderContents` * docs: Reverting change to `mark-many-as-read` experiment * docs: Documenting `fast-copy` in changelog * fix: Addressing review notes * fix: Addressing review feedback
1 parent 0459e24 commit f3b7c3f

20 files changed

Lines changed: 743 additions & 118 deletions
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
version: "v1.0.4"
3+
category: "performance-improvements"
4+
---
5+
6+
#### `fast-copy` strict control
7+
8+
With the new [`fast-copy`](/reference/strict-controls/active#fast-copy) strict control enabled, Terragrunt compiles each `include_in_copy` and `exclude_from_copy` pattern once and evaluates it inline during a single copy walk. This avoids re-walking subdirectories for every pattern, which should result in noticeable speed improvements for large source modules.
9+
10+
```bash
11+
terragrunt run plan --strict-control fast-copy
12+
```
13+
14+
The new matcher does not collapse `**` to zero path segments when a neighbor is a wildcard, so `a/**/*.tf` matches `a/sub/main.tf` but not `a/main.tf`. Patterns that relied on the old collapsing behavior should use brace alternation like `{*.tf,**/*.tf}` to cover both depths.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
name: fast-copy
3+
status: active
4+
since: "1.0.4"
5+
---
6+
7+
Switches `include_in_copy` and `exclude_from_copy` pattern matching from [zglob](https://pkg.go.dev/github.com/mattn/go-zglob) to [gobwas/glob](https://pkg.go.dev/github.com/gobwas/glob) when Terragrunt copies a module's source folder.
8+
9+
### `fast-copy` - Reason
10+
11+
The default implementation runs zglob once per pattern and recursively re-expands every directory match, which can dominate copy time on large module sources. gobwas compiles each pattern once and matches inline during the existing copy walk.
12+
13+
gobwas does not collapse `**` when a neighbor is a wildcard, so `a/**/*.tf` matches `a/sub/main.tf` but not `a/main.tf`. Patterns that depended on zero-depth `**` collapsing need brace alternation like `{*.tf,**/*.tf}` to cover both depths.

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ require (
8585
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0
8686
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10
8787
github.com/aws/smithy-go v1.24.3
88+
github.com/charlievieth/fastwalk v1.0.14
8889
github.com/charmbracelet/colorprofile v0.4.2
8990
github.com/charmbracelet/x/ansi v0.11.6
9091
github.com/charmbracelet/x/term v0.2.2

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,8 @@ github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F9
279279
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
280280
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
281281
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
282+
github.com/charlievieth/fastwalk v1.0.14 h1:3Eh5uaFGwHZd8EGwTjJnSpBkfwfsak9h6ICgnWlhAyg=
283+
github.com/charlievieth/fastwalk v1.0.14/go.mod h1:diVcUreiU1aQ4/Wu3NbxxH4/KYdKpLDojrQ1Bb2KgNY=
282284
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
283285
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
284286
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=

internal/glob/glob.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
// literals. With literals on both sides, "a/**/b" matches "a/b" as well as
1515
// "a/x/b". If either neighbor is a wildcard (for example "a/**/*.tf" or
1616
// "*/**/b.tf"), "**" does not collapse and a zero-depth match fails. Use
17-
// brace alternation — for example "{*.tf,**/*.tf}" to cover both depths
18-
// when the trailing segment contains a wildcard.
17+
// brace alternation like "{*.tf,**/*.tf}" to cover both depths when the
18+
// trailing segment contains a wildcard.
1919
//
2020
// # When to use what
2121
//

internal/runner/run/download_source.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,15 @@ func DownloadTerraformSource(
9696
// Always include the .tflint.hcl file, if it exists
9797
includeInCopy := slices.Concat(cfg.Terraform.IncludeInCopy, []string{tfLintConfig})
9898

99-
err = util.CopyFolderContents(
100-
l,
101-
opts.WorkingDir,
102-
terraformSource.WorkingDir,
103-
ModuleManifestName,
104-
includeInCopy,
105-
cfg.Terraform.ExcludeFromCopy,
106-
)
99+
copyOpts := []util.CopyOption{
100+
util.WithIncludeInCopy(includeInCopy...),
101+
util.WithExcludeFromCopy(cfg.Terraform.ExcludeFromCopy...),
102+
}
103+
if isFastCopyEnabled(opts.StrictControls) {
104+
copyOpts = append(copyOpts, util.WithFastCopy())
105+
}
106+
107+
err = util.CopyFolderContents(l, opts.WorkingDir, terraformSource.WorkingDir, ModuleManifestName, copyOpts...)
107108
if err != nil {
108109
return nil, err
109110
}
@@ -316,6 +317,7 @@ func UpdateGetters(l log.Logger, opts *Options, cfg *runcfg.RunConfig) func(*get
316317
Logger: l,
317318
IncludeInCopy: cfg.Terraform.IncludeInCopy,
318319
ExcludeFromCopy: cfg.Terraform.ExcludeFromCopy,
320+
FastCopy: isFastCopyEnabled(opts.StrictControls),
319321
}
320322
client.Getters["http"] = &getter.HttpGetter{Netrc: true}
321323
client.Getters["https"] = &getter.HttpGetter{Netrc: true}

internal/runner/run/download_source_test.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -566,8 +566,6 @@ func copyFolder(t *testing.T, src string, dest string) {
566566
filepath.FromSlash(src),
567567
filepath.FromSlash(dest),
568568
".terragrunt-test",
569-
nil,
570-
nil,
571569
)
572570
require.NoError(t, err)
573571
}

internal/runner/run/file_copy_getter.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,18 @@ import (
55
"os"
66

77
"github.com/gruntwork-io/terragrunt/internal/errors"
8+
"github.com/gruntwork-io/terragrunt/internal/strict"
9+
"github.com/gruntwork-io/terragrunt/internal/strict/controls"
810
"github.com/gruntwork-io/terragrunt/internal/util"
911
"github.com/gruntwork-io/terragrunt/pkg/log"
1012
"github.com/hashicorp/go-getter"
1113
)
1214

15+
// isFastCopyEnabled reports whether the `fast-copy` strict control is enabled.
16+
func isFastCopyEnabled(strictControls strict.Controls) bool {
17+
return len(strictControls.FilterByNames(controls.FastCopy).FilterByEnabled()) > 0
18+
}
19+
1320
// SourceManifestName is the manifest for files copied from the URL specified in the terraform { source = "<URL>" } config
1421
const SourceManifestName = ".terragrunt-source-manifest"
1522

@@ -24,6 +31,11 @@ type FileCopyGetter struct {
2431
// Terragrunt, which will skip hidden folders.
2532
IncludeInCopy []string
2633
ExcludeFromCopy []string
34+
35+
// FastCopy routes the [util.CopyFolderContents] call through the
36+
// fast-copy path. Set at construction time from the `fast-copy`
37+
// strict control.
38+
FastCopy bool
2739
}
2840

2941
// Get replaces the original FileGetter
@@ -42,7 +54,15 @@ func (g *FileCopyGetter) Get(dst string, u *url.URL) error {
4254
return errors.Errorf("source path must be a directory")
4355
}
4456

45-
return util.CopyFolderContents(g.Logger, path, dst, SourceManifestName, g.IncludeInCopy, g.ExcludeFromCopy)
57+
copyOpts := []util.CopyOption{
58+
util.WithIncludeInCopy(g.IncludeInCopy...),
59+
util.WithExcludeFromCopy(g.ExcludeFromCopy...),
60+
}
61+
if g.FastCopy {
62+
copyOpts = append(copyOpts, util.WithFastCopy())
63+
}
64+
65+
return util.CopyFolderContents(g.Logger, path, dst, SourceManifestName, copyOpts...)
4666
}
4767

4868
// GetFile The original FileGetter already knows how to do file copying so long as we set the Copy flag to true, so just

internal/strict/controls/controls.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ const (
7979

8080
// DisableDependentModules is the control that prevents the use of the deprecated `--disable-dependent-modules` flag.
8181
DisableDependentModules = "disable-dependent-modules"
82+
83+
// FastCopy is the control that switches `include_in_copy` and
84+
// `exclude_from_copy` pattern matching from zglob to gobwas.
85+
FastCopy = "fast-copy"
8286
)
8387

8488
//nolint:lll
@@ -290,6 +294,11 @@ func New() strict.Controls {
290294
Error: errors.New("The `--disable-dependent-modules` flag is no longer supported. Dependent modules discovery has been removed from `terragrunt render`."),
291295
Warning: "The `--disable-dependent-modules` flag is deprecated and will be removed in a future version of Terragrunt. Dependent modules discovery has been removed from `terragrunt render`, so this flag has no effect.",
292296
},
297+
&Control{
298+
Name: FastCopy,
299+
Description: "Switches `include_in_copy` and `exclude_from_copy` pattern matching from zglob to gobwas. `**` no longer collapses when adjacent to a wildcard, so `a/**/*.tf` will not match `a/foo.tf`. Use brace alternation like `{*.tf,**/*.tf}` to cover both depths.",
300+
Category: stageCategory,
301+
},
293302
}
294303

295304
return controls.Sort()

0 commit comments

Comments
 (0)