Skip to content

Commit 01f9006

Browse files
committed
chore: Wiring expansion into the unit and stack parse
1 parent ac9c536 commit 01f9006

2 files changed

Lines changed: 192 additions & 31 deletions

File tree

pkg/config/expansion_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ unit "app" {
3838
}
3939
4040
source = "./modules/app"
41-
path = "app"
41+
path = "app/${each.key}"
4242
}
4343
`
4444

@@ -55,7 +55,7 @@ stack "team" {
5555
}
5656
5757
source = "./stacks/team"
58-
path = "team"
58+
path = "team/${count.index}"
5959
}
6060
`
6161

pkg/config/stack.go

Lines changed: 190 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,21 @@ const (
4848

4949
// StackConfigFile represents the structure of terragrunt.stack.hcl stack file.
5050
type StackConfigFile struct {
51-
Locals *terragruntLocal `hcl:"locals,block"`
52-
Includes []*StackIncludeFile `hcl:"include,block"`
53-
Stacks []*Stack `hcl:"stack,block"`
54-
Units []*Unit `hcl:"unit,block"`
51+
Locals *terragruntLocal `hcl:"locals,block"`
52+
Includes []*StackIncludeFile `hcl:"include,block"`
53+
StackBlocks []componentHeader `hcl:"stack,block"`
54+
UnitBlocks []componentHeader `hcl:"unit,block"`
55+
Stacks []*Stack
56+
Units []*Unit
57+
}
58+
59+
// componentHeader keeps `unit` and `stack` in the stack file's schema without evaluating the
60+
// block body. Source, path and values are attributes, so a whole-file decode resolves them
61+
// before expansion can bind each.*/count.index, and a block referencing either fails on an
62+
// undefined variable.
63+
type componentHeader struct {
64+
Remain hcl.Body `hcl:",remain"`
65+
Name string `hcl:",label"`
5566
}
5667

5768
// StackIncludeFile represents an include block in a stack file.
@@ -1162,6 +1173,11 @@ func ParseStackConfig(
11621173
return nil, decodeErr
11631174
}
11641175

1176+
config.Units, config.Stacks, err = decodeComponents(file, evalParsingContext)
1177+
if err != nil {
1178+
return nil, err
1179+
}
1180+
11651181
// Process include blocks and merge any generated stack-level autoinclude file.
11661182
stackDir := filepath.Dir(file.ConfigPath)
11671183

@@ -1213,15 +1229,6 @@ func ParseStackConfig(
12131229
return stackConfig, nil
12141230
}
12151231

1216-
// stackComponentHeaders captures only the label and path of each unit/stack block
1217-
// so component paths can be resolved before the full decode evaluates values.
1218-
// source, values, and every other attribute are left in the block body, unevaluated.
1219-
type stackComponentHeaders struct {
1220-
Remain hcl.Body `hcl:",remain"`
1221-
Stacks []*stackComponentHeader `hcl:"stack,block"`
1222-
Units []*stackComponentHeader `hcl:"unit,block"`
1223-
}
1224-
12251232
// stackComponentHeader is the path-only shape of a unit or stack block.
12261233
type stackComponentHeader struct {
12271234
Remain hcl.Body `hcl:",remain"`
@@ -1249,14 +1256,14 @@ func injectStackComponentRefs(
12491256
stackDir string,
12501257
parserOpts []hclparse.Option,
12511258
) error {
1252-
headers := &stackComponentHeaders{}
1253-
if err := file.Decode(headers, evalCtx); err != nil {
1259+
baseUnits, baseStacks, err := decodeComponentHeaders(file, evalCtx)
1260+
if err != nil {
12541261
return err
12551262
}
12561263

12571264
// Publish the base refs first so a sibling autoinclude block whose path references unit.<name>.path /
12581265
// stack.<name>.path can resolve against the base components, matching how the full decode resolves them.
1259-
setStackComponentRefVars(evalCtx, stackDir, headers.Units, headers.Stacks)
1266+
setStackComponentRefVars(evalCtx, stackDir, baseUnits, baseStacks)
12601267

12611268
autoUnits, autoStacks, err := stackAutoIncludeComponentHeaders(
12621269
fsys,
@@ -1269,8 +1276,8 @@ func injectStackComponentRefs(
12691276
}
12701277

12711278
// Republish so an overridden component's path reflects the override, not the base path it replaced.
1272-
units := util.MergeNamed(headers.Units, autoUnits, componentHeaderName)
1273-
stacks := util.MergeNamed(headers.Stacks, autoStacks, componentHeaderName)
1279+
units := util.MergeNamed(baseUnits, autoUnits, componentHeaderName)
1280+
stacks := util.MergeNamed(baseStacks, autoStacks, componentHeaderName)
12741281
setStackComponentRefVars(evalCtx, stackDir, units, stacks)
12751282

12761283
return nil
@@ -1336,16 +1343,69 @@ func stackAutoIncludeComponentHeaders(
13361343
return nil, nil, fmt.Errorf("failed to read stack autoinclude %q: %w", autoIncludePath, err)
13371344
}
13381345

1339-
headers := &stackComponentHeaders{}
1340-
if decodeErr := incFile.Decode(headers, evalCtx); decodeErr != nil {
1346+
autoUnits, autoStacks, decodeErr := decodeComponentHeaders(incFile, evalCtx)
1347+
if decodeErr != nil {
13411348
return nil, nil, fmt.Errorf(
13421349
"failed to decode stack autoinclude headers %q: %w",
13431350
autoIncludePath,
13441351
decodeErr,
13451352
)
13461353
}
13471354

1348-
return headers.Units, headers.Stacks, nil
1355+
return autoUnits, autoStacks, nil
1356+
}
1357+
1358+
// decodeComponentHeaders reads the label and path of each unit and stack block. Blocks are
1359+
// expanded so that a path referencing each.*/count.index still decodes, and only unexpanded
1360+
// components come back: a component reference names a whole block, which an expanded one has
1361+
// no single path to answer for.
1362+
func decodeComponentHeaders(
1363+
file *hclparse.File,
1364+
evalCtx *hcl.EvalContext,
1365+
) ([]*stackComponentHeader, []*stackComponentHeader, error) {
1366+
units, err := expandComponentHeaders(file, MetadataUnit, evalCtx)
1367+
if err != nil {
1368+
return nil, nil, err
1369+
}
1370+
1371+
stacks, err := expandComponentHeaders(file, MetadataStack, evalCtx)
1372+
if err != nil {
1373+
return nil, nil, err
1374+
}
1375+
1376+
return units, stacks, nil
1377+
}
1378+
1379+
func expandComponentHeaders(
1380+
file *hclparse.File,
1381+
blockType string,
1382+
evalCtx *hcl.EvalContext,
1383+
) ([]*stackComponentHeader, error) {
1384+
instances, err := file.ExpandBlocks(blockType, &stackComponentHeader{}, evalCtx)
1385+
if err != nil {
1386+
return nil, err
1387+
}
1388+
1389+
headers := make([]*stackComponentHeader, 0, len(instances))
1390+
1391+
for _, instance := range instances {
1392+
if instance.Expanded() {
1393+
continue
1394+
}
1395+
1396+
header, ok := instance.Value.(*stackComponentHeader)
1397+
if !ok {
1398+
panic(fmt.Sprintf(
1399+
"ExpandBlocks returned %T for a %s block, but it decodes into the type it is given",
1400+
instance.Value,
1401+
blockType,
1402+
))
1403+
}
1404+
1405+
headers = append(headers, header)
1406+
}
1407+
1408+
return headers, nil
13491409
}
13501410

13511411
// componentHeaderName returns a header's block name, or an empty string for a nil entry so MergeNamed leaves it untouched.
@@ -1462,6 +1522,93 @@ func pruneOverriddenStackAutoIncludes(
14621522
return nil
14631523
}
14641524

1525+
// componentAddress identifies one instance of a unit or stack block. Every instance of an
1526+
// expanded block carries its label, so the label alone would fold a whole set into one entry.
1527+
func componentAddress(name string, expansion *hclparse.ExpansionBlock) string {
1528+
if expansion == nil || !expansion.Expanded() {
1529+
return name
1530+
}
1531+
1532+
return name + "[" + expansion.Key() + "]"
1533+
}
1534+
1535+
// decodeComponents decodes a stack file's unit and stack blocks, returning one value per
1536+
// iteration element. A block that declares no expansion yields a single value.
1537+
func decodeComponents(
1538+
file *hclparse.File,
1539+
evalCtx *hcl.EvalContext,
1540+
) ([]*Unit, []*Stack, error) {
1541+
units, err := decodeUnitBlocks(file, evalCtx)
1542+
if err != nil {
1543+
return nil, nil, err
1544+
}
1545+
1546+
stacks, err := decodeStackBlocks(file, evalCtx)
1547+
if err != nil {
1548+
return nil, nil, err
1549+
}
1550+
1551+
return units, stacks, nil
1552+
}
1553+
1554+
// decodeUnitBlocks decodes a stack file's unit blocks, returning one Unit per iteration
1555+
// element. A block that declares no expansion yields a single Unit.
1556+
func decodeUnitBlocks(file *hclparse.File, evalContext *hcl.EvalContext) ([]*Unit, error) {
1557+
instances, err := file.ExpandBlocks(MetadataUnit, &Unit{}, evalContext)
1558+
if err != nil {
1559+
return nil, err
1560+
}
1561+
1562+
units := make([]*Unit, 0, len(instances))
1563+
1564+
for _, instance := range instances {
1565+
unit, ok := instance.Value.(*Unit)
1566+
if !ok {
1567+
panic(fmt.Sprintf(
1568+
"ExpandBlocks returned %T for a unit block, but it decodes into the type it is given",
1569+
instance.Value,
1570+
))
1571+
}
1572+
1573+
if unit.Expansion != nil {
1574+
unit.Expansion.InstanceKey = instance.InstanceKey
1575+
}
1576+
1577+
units = append(units, unit)
1578+
}
1579+
1580+
return units, nil
1581+
}
1582+
1583+
// decodeStackBlocks decodes a stack file's stack blocks, returning one Stack per iteration
1584+
// element. A block that declares no expansion yields a single Stack.
1585+
func decodeStackBlocks(file *hclparse.File, evalContext *hcl.EvalContext) ([]*Stack, error) {
1586+
instances, err := file.ExpandBlocks(MetadataStack, &Stack{}, evalContext)
1587+
if err != nil {
1588+
return nil, err
1589+
}
1590+
1591+
stacks := make([]*Stack, 0, len(instances))
1592+
1593+
for _, instance := range instances {
1594+
stack, ok := instance.Value.(*Stack)
1595+
if !ok {
1596+
panic(fmt.Sprintf(
1597+
"ExpandBlocks returned %T for a stack block, but it decodes into the type it is given",
1598+
instance.Value,
1599+
))
1600+
}
1601+
1602+
if stack.Expansion != nil {
1603+
stack.Expansion.InstanceKey = instance.InstanceKey
1604+
}
1605+
1606+
stacks = append(stacks, stack)
1607+
}
1608+
1609+
return stacks, nil
1610+
}
1611+
14651612
// processStackConfigIncludes resolves include blocks during stack file parsing.
14661613
// It reads each included file, parses it with the same eval context, and merges
14671614
// its units and stacks into the main config so generation sees all components,
@@ -1494,6 +1641,11 @@ func processStackConfigIncludes(
14941641
return fmt.Errorf("failed to decode include %q: %w", inc.Name, decodeErr)
14951642
}
14961643

1644+
included.Units, included.Stacks, err = decodeComponents(incFile, evalCtx)
1645+
if err != nil {
1646+
return fmt.Errorf("failed to decode include %q: %w", inc.Name, err)
1647+
}
1648+
14971649
if included.Locals != nil {
14981650
return fmt.Errorf("included stack file %q must not define locals", inc.Name)
14991651
}
@@ -1510,22 +1662,24 @@ func processStackConfigIncludes(
15101662
seen := make(map[string]struct{}, len(config.Units))
15111663

15121664
for _, u := range config.Units {
1513-
if _, exists := seen[u.Name]; exists {
1665+
address := componentAddress(u.Name, u.Expansion)
1666+
if _, exists := seen[address]; exists {
15141667
return inthclparse.DuplicateUnitNameError{Name: u.Name}
15151668
}
15161669

1517-
seen[u.Name] = struct{}{}
1670+
seen[address] = struct{}{}
15181671
}
15191672

15201673
// Validate no duplicate stack names after merge.
15211674
seen = make(map[string]struct{}, len(config.Stacks))
15221675

15231676
for _, s := range config.Stacks {
1524-
if _, exists := seen[s.Name]; exists {
1677+
address := componentAddress(s.Name, s.Expansion)
1678+
if _, exists := seen[address]; exists {
15251679
return inthclparse.DuplicateStackNameError{Name: s.Name}
15261680
}
15271681

1528-
seen[s.Name] = struct{}{}
1682+
seen[address] = struct{}{}
15291683
}
15301684

15311685
return nil
@@ -1588,6 +1742,11 @@ func mergeStackAutoIncludeFile(
15881742
return fmt.Errorf("failed to decode stack autoinclude %q: %w", autoIncludePath, decodeErr)
15891743
}
15901744

1745+
included.Units, included.Stacks, err = decodeComponents(incFile, evalCtx)
1746+
if err != nil {
1747+
return fmt.Errorf("failed to decode stack autoinclude %q: %w", autoIncludePath, err)
1748+
}
1749+
15911750
if included.Locals != nil {
15921751
return fmt.Errorf("stack autoinclude %q must not define locals", autoIncludePath)
15931752
}
@@ -1621,11 +1780,12 @@ func validateUniqueComponentNames(units []*Unit, stacks []*Stack) error {
16211780
continue
16221781
}
16231782

1624-
if _, dup := seenUnits[u.Name]; dup {
1783+
address := componentAddress(u.Name, u.Expansion)
1784+
if _, dup := seenUnits[address]; dup {
16251785
return inthclparse.DuplicateUnitNameError{Name: u.Name}
16261786
}
16271787

1628-
seenUnits[u.Name] = struct{}{}
1788+
seenUnits[address] = struct{}{}
16291789
}
16301790

16311791
seenStacks := make(map[string]struct{}, len(stacks))
@@ -1635,11 +1795,12 @@ func validateUniqueComponentNames(units []*Unit, stacks []*Stack) error {
16351795
continue
16361796
}
16371797

1638-
if _, dup := seenStacks[s.Name]; dup {
1798+
address := componentAddress(s.Name, s.Expansion)
1799+
if _, dup := seenStacks[address]; dup {
16391800
return inthclparse.DuplicateStackNameError{Name: s.Name}
16401801
}
16411802

1642-
seenStacks[s.Name] = struct{}{}
1803+
seenStacks[address] = struct{}{}
16431804
}
16441805

16451806
return nil

0 commit comments

Comments
 (0)