Skip to content

Commit b57157d

Browse files
committed
bug: handling of auto include parsing errors
1 parent 800bbdd commit b57157d

9 files changed

Lines changed: 201 additions & 64 deletions

File tree

internal/discovery/helpers.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,11 @@ func stackDependencyPaths(fs vfs.FS, depPaths []string, c component.Component) (
290290
expanded := make([]string, 0, len(depPaths))
291291

292292
for _, depPath := range depPaths {
293-
unitPaths := inthclparse.UnitPathsFromStackDir(fs, depPath)
293+
unitPaths, upErr := inthclparse.UnitPathsFromStackDir(fs, depPath)
294+
if upErr != nil {
295+
return nil, upErr
296+
}
297+
294298
if len(unitPaths) > 0 {
295299
expanded = append(expanded, unitPaths...)
296300

internal/hclparse/autoinclude.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -202,21 +202,31 @@ func AutoIncludeDependencyPaths(fs vfs.FS, unitDir string) ([]string, error) {
202202
return nil, UnexpectedBodyTypeError{FilePath: autoIncludePath}
203203
}
204204

205-
var paths []string
205+
paths := make([]string, 0, len(body.Blocks))
206206

207207
for _, block := range body.Blocks {
208208
if block.Type != blockDependency || len(block.Labels) == 0 {
209209
continue
210210
}
211211

212+
name := block.Labels[0]
213+
212214
configPathAttr, exists := block.Body.Attributes[attrConfigPath]
213215
if !exists {
214-
continue
216+
return nil, MalformedDependencyError{FilePath: autoIncludePath, Name: name, Reason: "missing config_path attribute"}
215217
}
216218

217219
val, valDiags := configPathAttr.Expr.Value(nil)
218-
if valDiags.HasErrors() || !val.IsKnown() || val.IsNull() || val.Type() != cty.String {
219-
continue
220+
if valDiags.HasErrors() {
221+
return nil, MalformedDependencyError{FilePath: autoIncludePath, Name: name, Reason: "config_path: " + valDiags.Error()}
222+
}
223+
224+
if !val.IsKnown() || val.IsNull() {
225+
return nil, MalformedDependencyError{FilePath: autoIncludePath, Name: name, Reason: "config_path is null or unknown"}
226+
}
227+
228+
if val.Type() != cty.String {
229+
return nil, MalformedDependencyError{FilePath: autoIncludePath, Name: name, Reason: "config_path must be a string literal"}
220230
}
221231

222232
depPath := val.AsString()

internal/hclparse/errors.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,14 @@ type LocalsMaxIterError struct {
133133
func (e LocalsMaxIterError) Error() string {
134134
return fmt.Sprintf("locals evaluation exceeded %d iterations with %d unresolved locals", e.MaxIterations, e.Remaining)
135135
}
136+
137+
// MalformedDependencyError indicates a dependency block in an autoinclude file is malformed.
138+
type MalformedDependencyError struct {
139+
FilePath string
140+
Name string
141+
Reason string
142+
}
143+
144+
func (e MalformedDependencyError) Error() string {
145+
return fmt.Sprintf("malformed dependency %q in %s: %s", e.Name, e.FilePath, e.Reason)
146+
}

internal/hclparse/fuzz_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,7 @@ unit "app" { source = "."; path = "app"
267267
StackDir: "/fuzz/live",
268268
})
269269

270-
// Also exercise DiscoverStackChildUnits directly on the middle stack.
271-
_ = hclparse.DiscoverStackChildUnits(fs, "/fuzz/stacks/mid", "/fuzz/gen")
270+
_, _ = hclparse.DiscoverStackChildUnits(fs, "/fuzz/stacks/mid", "/fuzz/gen")
272271
})
273272
}
274273

internal/hclparse/parse.go

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,11 @@ func ParseStackFile(fs vfs.FS, input *ParseStackFileInput) (*ParseResult, error)
8585
stackTargetDir := filepath.Join(input.StackDir, StackDir)
8686

8787
unitRefs := buildRefsWithAbsPath(stackTargetDir, stackFile.Units)
88-
stackRefs := buildStackRefsWithAbsPath(fs, input.StackDir, stackTargetDir, stackFile.Stacks, 0)
88+
89+
stackRefs, err := buildStackRefsWithAbsPath(fs, input.StackDir, stackTargetDir, stackFile.Stacks, 0)
90+
if err != nil {
91+
return nil, err
92+
}
8993

9094
// Pass 2: resolve autoinclude blocks using the eval context.
9195
evalCtx := BuildAutoIncludeEvalContext(unitRefs, stackRefs)
@@ -388,32 +392,29 @@ func buildRefsWithAbsPath(stackTargetDir string, units []*UnitBlockHCL) []Compon
388392
return refs
389393
}
390394

391-
// buildStackRefsWithAbsPath creates ComponentRef values for stack blocks.
392-
// It also attempts to parse each stack's source to discover child units,
393-
// enabling stack.stack_name.unit_name.path references.
394-
// depth is threaded to prevent unbounded recursion in circular stacks.
395-
func buildStackRefsWithAbsPath(fs vfs.FS, stackDir string, stackTargetDir string, stacks []*StackBlockHCL, depth int) []ComponentRef {
395+
// buildStackRefsWithAbsPath builds ComponentRef values for stack blocks and discovers their child units.
396+
func buildStackRefsWithAbsPath(fs vfs.FS, stackDir string, stackTargetDir string, stacks []*StackBlockHCL, depth int) ([]ComponentRef, error) {
396397
refs := make([]ComponentRef, 0, len(stacks))
397398

398399
for _, s := range stacks {
399400
stackGenPath := filepath.Join(stackTargetDir, s.Path)
400401

401-
ref := ComponentRef{
402-
Name: s.Name,
403-
Path: stackGenPath,
404-
}
405-
406-
// Resolve the source to find nested units within this stack.
407-
// The source may be relative to the stack file's directory.
408402
sourceDir := s.Source
409403
if !filepath.IsAbs(sourceDir) {
410404
sourceDir = filepath.Join(stackDir, sourceDir)
411405
}
412406

413-
ref.ChildRefs = discoverStackChildUnitsWithDepth(fs, sourceDir, stackGenPath, depth+1)
407+
childRefs, err := discoverStackChildUnitsWithDepth(fs, sourceDir, stackGenPath, depth+1)
408+
if err != nil {
409+
return nil, err
410+
}
414411

415-
refs = append(refs, ref)
412+
refs = append(refs, ComponentRef{
413+
Name: s.Name,
414+
Path: stackGenPath,
415+
ChildRefs: childRefs,
416+
})
416417
}
417418

418-
return refs
419+
return refs, nil
419420
}

internal/hclparse/stack.go

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -204,15 +204,17 @@ func ParseStackFileFromPath(fs vfs.FS, stackDir string) (*ParseResult, error) {
204204
})
205205
}
206206

207-
// UnitPathsFromStackDir parses the stack file in stackDir and returns
208-
// absolute paths to each unit's generated directory under .terragrunt-stack/.
209-
// Returns nil if the file does not exist or cannot be parsed.
210-
func UnitPathsFromStackDir(fs vfs.FS, stackDir string) []string {
207+
// UnitPathsFromStackDir parses the stack file in stackDir and returns paths to each unit's generated directory.
208+
func UnitPathsFromStackDir(fs vfs.FS, stackDir string) ([]string, error) {
211209
stackDir = util.ResolvePath(stackDir)
212210

213211
result, err := ParseStackFileFromPath(fs, stackDir)
214-
if err != nil || result == nil {
215-
return nil
212+
if err != nil {
213+
return nil, err
214+
}
215+
216+
if result == nil {
217+
return nil, nil
216218
}
217219

218220
paths := make([]string, 0, len(result.Units))
@@ -226,32 +228,30 @@ func UnitPathsFromStackDir(fs vfs.FS, stackDir string) []string {
226228
paths = append(paths, unitPath)
227229
}
228230

229-
return paths
231+
return paths, nil
230232
}
231233

232234
// maxDiscoverDepth is the maximum recursion depth for DiscoverStackChildUnits
233235
// to prevent infinite loops from circular stack references.
234236
const maxDiscoverDepth = 1000
235237

236-
// DiscoverStackChildUnits parses a stack's source directory to find the
237-
// terragrunt.stack.hcl within it and extracts unit paths. This enables
238-
// stack.stack_name.unit_name.path references in autoinclude blocks.
239-
//
240-
// stackSourceDir is the directory where the stack's source files live
241-
// (or will be generated). stackGenDir is the absolute path where this
242-
// stack's units will be generated (.terragrunt-stack/stack_path/).
243-
func DiscoverStackChildUnits(fs vfs.FS, stackSourceDir, stackGenDir string) []ComponentRef {
238+
// DiscoverStackChildUnits parses a stack's source dir for stack.<name>.<unit>.path resolution.
239+
func DiscoverStackChildUnits(fs vfs.FS, stackSourceDir, stackGenDir string) ([]ComponentRef, error) {
244240
return discoverStackChildUnitsWithDepth(fs, stackSourceDir, stackGenDir, 0)
245241
}
246242

247-
func discoverStackChildUnitsWithDepth(fs vfs.FS, stackSourceDir, stackGenDir string, depth int) []ComponentRef {
243+
func discoverStackChildUnitsWithDepth(fs vfs.FS, stackSourceDir, stackGenDir string, depth int) ([]ComponentRef, error) {
248244
if depth > maxDiscoverDepth {
249-
return nil
245+
return nil, nil
250246
}
251247

252248
result, err := ParseStackFileFromPath(fs, stackSourceDir)
253-
if err != nil || result == nil {
254-
return nil
249+
if err != nil {
250+
return nil, err
251+
}
252+
253+
if result == nil {
254+
return nil, nil
255255
}
256256

257257
childTargetDir := filepath.Join(stackGenDir, StackDir)
@@ -270,7 +270,6 @@ func discoverStackChildUnitsWithDepth(fs vfs.FS, stackSourceDir, stackGenDir str
270270
})
271271
}
272272

273-
// Also discover nested stacks so stack.<name>.<nested_stack>.path works.
274273
for _, s := range result.Stacks {
275274
nestedGenPath := filepath.Join(childTargetDir, s.Path)
276275

@@ -279,14 +278,17 @@ func discoverStackChildUnitsWithDepth(fs vfs.FS, stackSourceDir, stackGenDir str
279278
nestedSourceDir = filepath.Join(stackSourceDir, nestedSourceDir)
280279
}
281280

282-
ref := ComponentRef{
283-
Name: s.Name,
284-
Path: nestedGenPath,
285-
ChildRefs: discoverStackChildUnitsWithDepth(fs, nestedSourceDir, nestedGenPath, depth+1),
281+
childRefs, childErr := discoverStackChildUnitsWithDepth(fs, nestedSourceDir, nestedGenPath, depth+1)
282+
if childErr != nil {
283+
return nil, childErr
286284
}
287285

288-
refs = append(refs, ref)
286+
refs = append(refs, ComponentRef{
287+
Name: s.Name,
288+
Path: nestedGenPath,
289+
ChildRefs: childRefs,
290+
})
289291
}
290292

291-
return refs
293+
return refs, nil
292294
}

internal/hclparse/stack_test.go

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,8 @@ unit "db" {
183183
}
184184
`), 0644))
185185

186-
refs := hclparse.DiscoverStackChildUnits(fs, "/test/stack-src", "/gen/networking")
187-
186+
refs, err := hclparse.DiscoverStackChildUnits(fs, "/test/stack-src", "/gen/networking")
187+
require.NoError(t, err)
188188
require.Len(t, refs, 2)
189189
assert.Equal(t, "vpc", refs[0].Name)
190190
assert.Equal(t, filepath.Join("/gen/networking", ".terragrunt-stack", "vpc"), refs[0].Path)
@@ -210,13 +210,11 @@ unit "db" {
210210
}
211211
`), 0644))
212212

213-
refs := hclparse.DiscoverStackChildUnits(fs, "/test/stack-src", "/gen/networking")
214-
213+
refs, err := hclparse.DiscoverStackChildUnits(fs, "/test/stack-src", "/gen/networking")
214+
require.NoError(t, err)
215215
require.Len(t, refs, 2)
216-
// vpc has no_dot_terragrunt_stack=true, goes directly under stackGenDir
217216
assert.Equal(t, "vpc", refs[0].Name)
218217
assert.Equal(t, filepath.Join("/gen/networking", "vpc"), refs[0].Path)
219-
// db is normal, goes under .terragrunt-stack/
220218
assert.Equal(t, "db", refs[1].Name)
221219
assert.Equal(t, filepath.Join("/gen/networking", ".terragrunt-stack", "db"), refs[1].Path)
222220
}
@@ -226,7 +224,8 @@ func TestDiscoverStackChildUnits_NoStackFile(t *testing.T) {
226224

227225
fs := vfs.NewMemMapFS()
228226

229-
refs := hclparse.DiscoverStackChildUnits(fs, "/nonexistent", "/gen")
227+
refs, err := hclparse.DiscoverStackChildUnits(fs, "/nonexistent", "/gen")
228+
require.NoError(t, err)
230229
assert.Nil(t, refs)
231230
}
232231

@@ -273,7 +272,8 @@ unit "db" {
273272
}
274273
`), 0644))
275274

276-
paths := hclparse.UnitPathsFromStackDir(fs, "/test")
275+
paths, err := hclparse.UnitPathsFromStackDir(fs, "/test")
276+
require.NoError(t, err)
277277
require.Len(t, paths, 2)
278278
assert.Contains(t, paths[0], ".terragrunt-stack")
279279
assert.Contains(t, paths[1], ".terragrunt-stack")
@@ -285,15 +285,31 @@ func TestUnitPathsFromStackDir_NotAStack(t *testing.T) {
285285
fs := vfs.NewMemMapFS()
286286
require.NoError(t, fs.MkdirAll("/test", 0755))
287287

288-
assert.Nil(t, hclparse.UnitPathsFromStackDir(fs, "/test"))
288+
paths, err := hclparse.UnitPathsFromStackDir(fs, "/test")
289+
require.NoError(t, err)
290+
assert.Nil(t, paths)
289291
}
290292

291293
func TestUnitPathsFromStackDir_Nonexistent(t *testing.T) {
292294
t.Parallel()
293295

294296
fs := vfs.NewMemMapFS()
295297

296-
assert.Nil(t, hclparse.UnitPathsFromStackDir(fs, "/nonexistent"))
298+
paths, err := hclparse.UnitPathsFromStackDir(fs, "/nonexistent")
299+
require.NoError(t, err)
300+
assert.Nil(t, paths)
301+
}
302+
303+
func TestUnitPathsFromStackDir_MalformedReturnsError(t *testing.T) {
304+
t.Parallel()
305+
306+
fs := vfs.NewMemMapFS()
307+
require.NoError(t, fs.MkdirAll("/test", 0755))
308+
require.NoError(t, vfs.WriteFile(fs, "/test/terragrunt.stack.hcl", []byte(`unit "x" { source = "." `), 0644))
309+
310+
paths, err := hclparse.UnitPathsFromStackDir(fs, "/test")
311+
require.Error(t, err)
312+
assert.Nil(t, paths)
297313
}
298314

299315
func TestParseStackFileFromPath(t *testing.T) {
@@ -370,8 +386,8 @@ unit "vpc" {
370386
symlinkDir := filepath.Join(tmpDir, "symlinked-stack")
371387
require.NoError(t, os.Symlink(realDir, symlinkDir))
372388

373-
// UnitPathsFromStackDir via symlink should return paths based on resolved real dir
374-
paths := hclparse.UnitPathsFromStackDir(vfs.NewOSFS(), symlinkDir)
389+
paths, err := hclparse.UnitPathsFromStackDir(vfs.NewOSFS(), symlinkDir)
390+
require.NoError(t, err)
375391
require.Len(t, paths, 1)
376392
// Path should be based on the REAL directory, not the symlink
377393
assert.Contains(t, paths[0], "real-stack")
@@ -397,12 +413,24 @@ unit "db" {
397413
symlinkSrcDir := filepath.Join(tmpDir, "symlinked-source")
398414
require.NoError(t, os.Symlink(realSrcDir, symlinkSrcDir))
399415

400-
// DiscoverStackChildUnits via symlink should work
401-
refs := hclparse.DiscoverStackChildUnits(vfs.NewOSFS(), symlinkSrcDir, "/gen/stack")
416+
refs, err := hclparse.DiscoverStackChildUnits(vfs.NewOSFS(), symlinkSrcDir, "/gen/stack")
417+
require.NoError(t, err)
402418
require.Len(t, refs, 1)
403419
assert.Equal(t, "db", refs[0].Name)
404420
}
405421

422+
func TestDiscoverStackChildUnits_MalformedReturnsError(t *testing.T) {
423+
t.Parallel()
424+
425+
fs := vfs.NewMemMapFS()
426+
require.NoError(t, fs.MkdirAll("/test/stack-src", 0755))
427+
require.NoError(t, vfs.WriteFile(fs, "/test/stack-src/terragrunt.stack.hcl", []byte(`unit "x" { source = "."`), 0644))
428+
429+
refs, err := hclparse.DiscoverStackChildUnits(fs, "/test/stack-src", "/gen")
430+
require.Error(t, err)
431+
assert.Nil(t, refs)
432+
}
433+
406434
func TestParseStackFile_WithInclude(t *testing.T) {
407435
t.Parallel()
408436

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Regression fixture for issue #5980: stack file uses an HCL function in path that
2+
// only the production parser has registered (the simplified two-pass autoinclude
3+
// parser uses nil eval context). With an autoinclude block declared, this must fail
4+
// loudly during stack generate instead of silently skipping autoinclude generation.
5+
6+
unit "vpc" {
7+
source = "../catalog/units/vpc"
8+
path = format("%s", "vpc")
9+
}
10+
11+
unit "subnet" {
12+
source = "../catalog/units/subnet"
13+
path = "subnet"
14+
15+
autoinclude {
16+
dependency "vpc" {
17+
config_path = unit.vpc.path
18+
19+
mock_outputs_allowed_terraform_commands = ["validate", "plan", "apply"]
20+
mock_outputs = {
21+
id = "mock-id"
22+
}
23+
}
24+
25+
inputs = {
26+
vpc_id = dependency.vpc.outputs.id
27+
}
28+
}
29+
}

0 commit comments

Comments
 (0)