@@ -48,10 +48,21 @@ const (
4848
4949// StackConfigFile represents the structure of terragrunt.stack.hcl stack file.
5050type 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.
12261233type 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