Skip to content

Commit 9f9929a

Browse files
authored
feat: stack validation improvements (#4078)
* Add basic check for not existing terragrunt path * Removed unused consant * Add stack generation fix * locals check * Add basic test for stack validation * test update * Docs update * Docs update * Tests update * Updated wording on error message * Separated test for stack validation * Added test for wording * Added check for destination path * Source path update * fetching not existing location * Updated tests * Tests cleanup * Worker package update * Simplified worker execution * removed test code * Worker pool naming convention update * Field alignment fix * Improved error message * switched to RWMutex * run update * Worker race conditions
1 parent 3ff0ab8 commit 9f9929a

File tree

29 files changed

+526
-209
lines changed

29 files changed

+526
-209
lines changed

cli/commands/backend/bootstrap/bootstrap.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ func Run(ctx context.Context, opts *options.TerragruntOptions) error {
1313
if err != nil {
1414
return err
1515
}
16+
1617
if remoteState == nil {
1718
opts.Logger.Debug("Did not find remote `remote_state` block in the config")
1819

cli/commands/backend/delete/delete.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ func Run(ctx context.Context, cmdOpts *Options) error {
1515
if err != nil {
1616
return err
1717
}
18+
1819
if remoteState == nil {
1920
opts.Logger.Debug("Did not find remote `remote_state` block in the config")
2021

cli/commands/stack/cli.go

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const (
1515
JSONFormatFlagName = "json"
1616
RawFormatFlagName = "raw"
1717
NoStackGenerate = "no-stack-generate"
18+
NoStackValidate = "no-stack-validate"
1819

1920
generateCommandName = "generate"
2021
runCommandName = "run"
@@ -72,6 +73,28 @@ func NewCommand(opts *options.TerragruntOptions) *cli.Command {
7273
}
7374
}
7475

76+
func defaultFlags(opts *options.TerragruntOptions, prefix flags.Prefix) cli.Flags {
77+
tgPrefix := prefix.Prepend(flags.TgPrefix)
78+
79+
flags := cli.Flags{
80+
flags.NewFlag(&cli.BoolFlag{
81+
Name: NoStackGenerate,
82+
EnvVars: tgPrefix.EnvVars(NoStackGenerate),
83+
Destination: &opts.NoStackGenerate,
84+
Usage: "Disable automatic stack regeneration before running the command.",
85+
}),
86+
flags.NewFlag(&cli.BoolFlag{
87+
Name: NoStackValidate,
88+
EnvVars: tgPrefix.EnvVars(NoStackValidate),
89+
Destination: &opts.NoStackValidate,
90+
Hidden: true,
91+
Usage: "Disable automatic stack validation after generation.",
92+
}),
93+
}
94+
95+
return append(run.NewFlags(opts, nil), flags...)
96+
}
97+
7598
func outputFlags(opts *options.TerragruntOptions, prefix flags.Prefix) cli.Flags {
7699
tgPrefix := prefix.Prepend(flags.TgPrefix)
77100

@@ -102,18 +125,3 @@ func outputFlags(opts *options.TerragruntOptions, prefix flags.Prefix) cli.Flags
102125

103126
return append(defaultFlags(opts, prefix), flags...)
104127
}
105-
106-
func defaultFlags(opts *options.TerragruntOptions, prefix flags.Prefix) cli.Flags {
107-
tgPrefix := prefix.Prepend(flags.TgPrefix)
108-
109-
generateFlags := cli.Flags{
110-
flags.NewFlag(&cli.BoolFlag{
111-
Name: NoStackGenerate,
112-
EnvVars: tgPrefix.EnvVars(NoStackGenerate),
113-
Destination: &opts.NoStackGenerate,
114-
Usage: "Disable automatic stack regeneration before running the command.",
115-
}),
116-
}
117-
118-
return append(run.NewFlags(opts, nil), generateFlags...)
119-
}

config/config_as_cty_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ func TestStackLocalsCtyReading(t *testing.T) {
252252
stackMap, err := ctyhelper.ParseCtyValueToMap(tgConfigCty)
253253
require.NoError(t, err)
254254
assert.NotNil(t, stackMap)
255-
locals := stackMap["locals"].(map[string]any)
255+
locals := stackMap["local"].(map[string]any)
256256
assert.NotNil(t, locals)
257257
}
258258

config/stack.go

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"strings"
99

1010
"github.com/gruntwork-io/terragrunt/internal/ctyhelper"
11+
"github.com/gruntwork-io/terragrunt/internal/worker"
1112

1213
"github.com/gruntwork-io/terragrunt/internal/experiment"
1314
"github.com/hashicorp/go-getter/v2"
@@ -69,7 +70,7 @@ type Stack struct {
6970
// GenerateStacks generates the stack files.
7071
func GenerateStacks(ctx context.Context, opts *options.TerragruntOptions) error {
7172
processedFiles := make(map[string]bool)
72-
wp := util.NewWorkerPool(opts.Parallelism)
73+
wp := worker.NewWorkerPool(opts.Parallelism)
7374
// stop worker pool on exit
7475
defer wp.Stop()
7576
// initial files setting as stack file
@@ -96,8 +97,8 @@ func GenerateStacks(ctx context.Context, opts *options.TerragruntOptions) error
9697
}
9798
}
9899

99-
if err := wp.Wait(); err != nil {
100-
return err
100+
if wpError := wp.Wait(); wpError != nil {
101+
return wpError
101102
}
102103

103104
if !processedNewFiles {
@@ -171,7 +172,7 @@ func StackOutput(ctx context.Context, opts *options.TerragruntOptions) (map[stri
171172
// generateStackFile processes the Terragrunt stack configuration from the given stackFilePath,
172173
// reads necessary values, and generates units and stacks in the target directory.
173174
// It handles the creation of required directories and returns any errors encountered.
174-
func generateStackFile(ctx context.Context, opts *options.TerragruntOptions, pool *util.WorkerPool, stackFilePath string) error {
175+
func generateStackFile(ctx context.Context, opts *options.TerragruntOptions, pool *worker.Pool, stackFilePath string) error {
175176
stackSourceDir := filepath.Dir(stackFilePath)
176177

177178
values, err := ReadValues(ctx, opts, stackSourceDir)
@@ -201,7 +202,7 @@ func generateStackFile(ctx context.Context, opts *options.TerragruntOptions, poo
201202
// generateUnits iterates through a slice of Unit objects, processing each one by copying
202203
// source files to their destination paths and writing unit-specific values.
203204
// It logs the processing progress and returns any errors encountered during the operation.
204-
func generateUnits(ctx context.Context, opts *options.TerragruntOptions, pool *util.WorkerPool, sourceDir, targetDir string, units []*Unit) error {
205+
func generateUnits(ctx context.Context, opts *options.TerragruntOptions, pool *worker.Pool, sourceDir, targetDir string, units []*Unit) error {
205206
for _, unit := range units {
206207
unitCopy := unit // Create a copy to avoid capturing the loop variable reference
207208

@@ -214,6 +215,7 @@ func generateUnits(ctx context.Context, opts *options.TerragruntOptions, pool *u
214215
source: unitCopy.Source,
215216
values: unitCopy.Values,
216217
noStack: unitCopy.NoStack != nil && *unitCopy.NoStack,
218+
kind: unitKind,
217219
}
218220

219221
opts.Logger.Infof("Processing unit %s", unitCopy.Name)
@@ -231,7 +233,7 @@ func generateUnits(ctx context.Context, opts *options.TerragruntOptions, pool *u
231233

232234
// generateStacks processes each stack by resolving its destination path and copying files from the source.
233235
// It logs each operation and returns early if any error is encountered.
234-
func generateStacks(ctx context.Context, opts *options.TerragruntOptions, pool *util.WorkerPool, sourceDir, targetDir string, stacks []*Stack) error {
236+
func generateStacks(ctx context.Context, opts *options.TerragruntOptions, pool *worker.Pool, sourceDir, targetDir string, stacks []*Stack) error {
235237
for _, stack := range stacks {
236238
stackCopy := stack // Create a copy to avoid capturing the loop variable reference
237239

@@ -244,6 +246,7 @@ func generateStacks(ctx context.Context, opts *options.TerragruntOptions, pool *
244246
source: stackCopy.Source,
245247
noStack: stackCopy.NoStack != nil && *stackCopy.NoStack,
246248
values: stackCopy.Values,
249+
kind: stackKind,
247250
}
248251

249252
opts.Logger.Infof("Processing stack %s", stackCopy.Name)
@@ -259,6 +262,13 @@ func generateStacks(ctx context.Context, opts *options.TerragruntOptions, pool *
259262
return nil
260263
}
261264

265+
type componentKind int
266+
267+
const (
268+
unitKind componentKind = iota
269+
stackKind
270+
)
271+
262272
// componentToProcess represents an item of work for processing a stack or unit.
263273
// It contains information about the source and target directories, the name and path of the item, the source URL or path,
264274
// and any associated values that need to be processed.
@@ -270,6 +280,7 @@ type componentToProcess struct {
270280
path string
271281
source string
272282
noStack bool
283+
kind componentKind
273284
}
274285

275286
// processComponent copies files from the source directory to the target destination and generates a corresponding values file.
@@ -286,6 +297,11 @@ func processComponent(ctx context.Context, opts *options.TerragruntOptions, cmp
286297
return errors.Errorf("path %s must be relative", cmp.path)
287298
}
288299

300+
kindStr := "unit"
301+
if cmp.kind == stackKind {
302+
kindStr = "stack"
303+
}
304+
289305
// building destination path based on target directory
290306
dest := filepath.Join(cmp.targetDir, cmp.path)
291307

@@ -304,7 +320,7 @@ func processComponent(ctx context.Context, opts *options.TerragruntOptions, cmp
304320

305321
// validate that the destination path is within the stack directory
306322
if !strings.HasPrefix(absDest, absStackDir) {
307-
return errors.Errorf("destination path '%s' is outside of the stack directory '%s'", absDest, absStackDir)
323+
return errors.Errorf("%s destination path '%s' is outside of the stack directory '%s'", cmp.name, absDest, absStackDir)
308324
}
309325

310326
if cmp.noStack {
@@ -315,7 +331,39 @@ func processComponent(ctx context.Context, opts *options.TerragruntOptions, cmp
315331
opts.Logger.Debugf("Processing: %s (%s) to %s", cmp.name, source, dest)
316332

317333
if err := copyFiles(ctx, opts, cmp.name, cmp.sourceDir, source, dest); err != nil {
318-
return errors.Errorf("Failed to copy %s to %s %w", source, dest, err)
334+
return errors.Errorf(
335+
"Failed to fetch %s %s\n"+
336+
" Source: %s\n"+
337+
" Destination: %s\n\n"+
338+
"Troubleshooting:\n"+
339+
" 1. Check if your source path is correct relative to the stack file location\n"+
340+
" 2. Verify the units or stacks directory exists at the expected location\n"+
341+
" 3. Ensure you have proper permissions to read from source and write to destination\n\n"+
342+
"Original error: %v",
343+
kindStr,
344+
cmp.name,
345+
source,
346+
dest,
347+
err,
348+
)
349+
}
350+
351+
if !cmp.noStack {
352+
// validate what was copied to the destination, don't do validation for special noStack components
353+
expectedFile := DefaultTerragruntConfigPath
354+
355+
if cmp.kind == stackKind {
356+
expectedFile = defaultStackFile
357+
}
358+
359+
if err := validateTargetDir(kindStr, cmp.name, dest, expectedFile); err != nil {
360+
if opts.NoStackValidate {
361+
// print warning if validation is skipped
362+
opts.Logger.Warnf("Suppressing validation error for %s %s at path %s: expected %s to generate with %s file at root of generated directory.", kindStr, cmp.name, cmp.targetDir, kindStr, expectedFile)
363+
} else {
364+
return errors.Errorf("Validation failed for %s %s at path %s: expected %s to generate with %s file at root of generated directory.", kindStr, cmp.name, cmp.targetDir, kindStr, expectedFile)
365+
}
366+
}
319367
}
320368

321369
// generate values file
@@ -656,3 +704,19 @@ func listStackFiles(opts *options.TerragruntOptions, dir string) ([]string, erro
656704

657705
return stackFiles, nil
658706
}
707+
708+
// validateTargetDir target destination directory.
709+
func validateTargetDir(kind, name, destDir, expectedFile string) error {
710+
expectedPath := filepath.Join(destDir, expectedFile)
711+
712+
info, err := os.Stat(expectedPath)
713+
if err != nil {
714+
return fmt.Errorf("%s '%s': expected file '%s' not found in target directory '%s': %w", kind, name, expectedFile, destDir, err)
715+
}
716+
717+
if info.IsDir() {
718+
return fmt.Errorf("%s '%s': expected file '%s' is a directory, not a file", kind, name, expectedFile)
719+
}
720+
721+
return nil
722+
}

docs-starlight/src/data/commands/stack/generate.mdx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ terragrunt stack generate --parallelism 4
7777

7878
Automatic Discovery: The command automatically discovers all `terragrunt.stack.hcl` files within the directory structure and generates them in parallel.
7979

80+
Validation of Units and Stacks: During the stack generation, the system will validate that each unit and stack's target directory contains the appropriate configuration file (`terragrunt.hcl` for units and `terragrunt.stack.hcl` for stacks). This ensures the directories are correctly structured before proceeding with the stack generation.
81+
To **skip this validation**, you can use the `--no-stack-validate` flag:
82+
83+
```bash
84+
terragrunt stack generate --no-stack-validate
85+
```
86+
8087
</Aside>
8188

8289
<Aside type="caution">

docs/_docs/04_reference/02-cli-options.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,13 @@ terragrunt stack generate --parallelism 4
356356

357357
- Path Restrictions: If an absolute path is provided as an argument, the command will throw an error. Only relative paths within the working directory are supported.
358358

359+
- Validation of Units and Stacks: During the stack generation, the system will validate that each unit and stack's target directory contains the appropriate configuration file (`terragrunt.hcl` for units and `terragrunt.stack.hcl` for stacks). This ensures the directories are correctly structured before proceeding with the stack generation.
360+
To **skip this validation**, you can use the `--no-stack-validate` flag:
361+
362+
```bash
363+
terragrunt stack generate --no-stack-validate
364+
```
365+
359366
#### stack run
360367

361368
The `stack run *` command allows users to execute IaC commands across all units defined in a `terragrunt.stack.hcl` file.

0 commit comments

Comments
 (0)