88
99 "github.com/hashicorp/hcl/v2"
1010 "github.com/hashicorp/hcl/v2/ext/typeexpr"
11+ "github.com/samber/lo"
1112 "github.com/zclconf/go-cty/cty"
1213 "github.com/zclconf/go-cty/cty/convert"
1314 "golang.org/x/exp/slices"
@@ -102,6 +103,7 @@ func (e *evaluator) evaluateStep() {
102103
103104 e .ctx .Set (e .getValuesByBlockType ("data" ), "data" )
104105 e .ctx .Set (e .getValuesByBlockType ("output" ), "output" )
106+ e .ctx .Set (e .getValuesByBlockType ("module" ), "module" )
105107}
106108
107109// exportOutputs is used to export module outputs to the parent module
@@ -126,48 +128,100 @@ func (e *evaluator) EvaluateAll(ctx context.Context) (terraform.Modules, map[str
126128 fsMap := make (map [string ]fs.FS )
127129 fsMap [fsKey ] = e .filesystem
128130
129- var lastContext hcl.EvalContext
130131 e .debug .Log ("Starting module evaluation..." )
131- for i := 0 ; i < maxContextIterations ; i ++ {
132+ e . evaluateSteps ()
132133
133- e .evaluateStep ()
134+ // expand out resources and modules via count, for-each and dynamic
135+ // (not a typo, we do this twice so every order is processed)
136+ e .blocks = e .expandBlocks (e .blocks )
137+ e .blocks = e .expandBlocks (e .blocks )
134138
135- // if ctx matches the last evaluation, we can bail, nothing left to resolve
136- if i > 0 && reflect .DeepEqual (lastContext .Variables , e .ctx .Inner ().Variables ) {
137- break
138- }
139+ e .debug .Log ("Starting submodule evaluation..." )
140+ submodules := e .loadSubmodules (ctx )
139141
140- if len (e .ctx .Inner ().Variables ) != len (lastContext .Variables ) {
141- lastContext .Variables = make (map [string ]cty.Value , len (e .ctx .Inner ().Variables ))
142+ for i := 0 ; i < maxContextIterations ; i ++ {
143+ changed := false
144+ for _ , sm := range submodules {
145+ changed = changed || e .evaluateSubmodule (ctx , sm )
142146 }
143- for k , v := range e .ctx .Inner ().Variables {
144- lastContext .Variables [k ] = v
147+ if ! changed {
148+ e .debug .Log ("All submodules are evaluated at i=%d" , i )
149+ break
145150 }
146151 }
147152
148- // expand out resources and modules via count, for-each and dynamic
149- // (not a typo, we do this twice so every order is processed)
150- e .blocks = e .expandBlocks (e .blocks )
151- e .blocks = e .expandBlocks (e .blocks )
153+ e .debug .Log ("Starting post-submodule evaluation..." )
154+ e .evaluateSteps ()
152155
153- e .debug .Log ("Starting submodule evaluation..." )
154156 var modules terraform.Modules
157+ for _ , sm := range submodules {
158+ modules = append (modules , sm .modules ... )
159+ fsMap = lo .Assign (fsMap , sm .fsMap )
160+ }
161+
162+ e .debug .Log ("Finished processing %d submodule(s)." , len (modules ))
163+
164+ e .debug .Log ("Module evaluation complete." )
165+ rootModule := terraform .NewModule (e .projectRootPath , e .modulePath , e .blocks , e .ignores )
166+ return append (terraform.Modules {rootModule }, modules ... ), fsMap
167+ }
168+
169+ type submodule struct {
170+ definition * ModuleDefinition
171+ eval * evaluator
172+ modules terraform.Modules
173+ lastState map [string ]cty.Value
174+ fsMap map [string ]fs.FS
175+ }
176+
177+ func (e * evaluator ) loadSubmodules (ctx context.Context ) []* submodule {
178+ var submodules []* submodule
179+
155180 for _ , definition := range e .loadModules (ctx ) {
156- submodules , outputs , err := definition .Parser .EvaluateAll (ctx )
157- if err != nil {
158- e .debug .Log ("Failed to evaluate submodule '%s': %s." , definition .Name , err )
181+ eval , err := definition .Parser .Load (ctx )
182+ if errors .Is (err , ErrNoFiles ) {
183+ continue
184+ } else if err != nil {
185+ e .debug .Log ("Failed to load submodule '%s': %s." , definition .Name , err )
159186 continue
160187 }
161- // export module outputs
162- e .ctx .Set (outputs , "module" , definition .Name )
163- modules = append (modules , submodules ... )
164- for key , val := range definition .Parser .GetFilesystemMap () {
165- fsMap [key ] = val
188+
189+ submodules = append (submodules , & submodule {
190+ definition : definition ,
191+ eval : eval ,
192+ fsMap : make (map [string ]fs.FS ),
193+ })
194+ }
195+
196+ return submodules
197+ }
198+
199+ func (e * evaluator ) evaluateSubmodule (ctx context.Context , sm * submodule ) bool {
200+ inputVars := sm .definition .inputVars ()
201+ if len (sm .modules ) > 0 {
202+ if reflect .DeepEqual (inputVars , sm .lastState ) {
203+ e .debug .Log ("Submodule %s inputs unchanged" , sm .definition .Name )
204+ return false
166205 }
167206 }
168- e .debug .Log ("Finished processing %d submodule(s)." , len (modules ))
169207
170- e .debug .Log ("Starting post-submodule evaluation..." )
208+ e .debug .Log ("Evaluating submodule %s" , sm .definition .Name )
209+ sm .eval .inputVars = inputVars
210+ sm .modules , sm .fsMap = sm .eval .EvaluateAll (ctx )
211+ outputs := sm .eval .exportOutputs ()
212+
213+ // lastState needs to be captured after applying outputs – so that they
214+ // don't get treated as changes – but before running post-submodule
215+ // evaluation, so that changes from that can trigger re-evaluations of
216+ // the submodule if/when they feed back into inputs.
217+ e .ctx .Set (outputs , "module" , sm .definition .Name )
218+ sm .lastState = sm .definition .inputVars ()
219+ e .evaluateSteps ()
220+ return true
221+ }
222+
223+ func (e * evaluator ) evaluateSteps () {
224+ var lastContext hcl.EvalContext
171225 for i := 0 ; i < maxContextIterations ; i ++ {
172226
173227 e .evaluateStep ()
@@ -176,18 +230,13 @@ func (e *evaluator) EvaluateAll(ctx context.Context) (terraform.Modules, map[str
176230 if i > 0 && reflect .DeepEqual (lastContext .Variables , e .ctx .Inner ().Variables ) {
177231 break
178232 }
179-
180233 if len (e .ctx .Inner ().Variables ) != len (lastContext .Variables ) {
181234 lastContext .Variables = make (map [string ]cty.Value , len (e .ctx .Inner ().Variables ))
182235 }
183236 for k , v := range e .ctx .Inner ().Variables {
184237 lastContext .Variables [k ] = v
185238 }
186239 }
187-
188- e .debug .Log ("Module evaluation complete." )
189- rootModule := terraform .NewModule (e .projectRootPath , e .modulePath , e .blocks , e .ignores )
190- return append (terraform.Modules {rootModule }, modules ... ), fsMap
191240}
192241
193242func (e * evaluator ) expandBlocks (blocks terraform.Blocks ) terraform.Blocks {
@@ -217,7 +266,9 @@ func (e *evaluator) expandDynamicBlock(b *terraform.Block) {
217266 b .InjectBlock (content , blockName )
218267 }
219268 }
220- sub .MarkExpanded ()
269+ if len (expanded ) > 0 {
270+ sub .MarkExpanded ()
271+ }
221272 }
222273}
223274
@@ -246,6 +297,10 @@ func (e *evaluator) expandBlockForEaches(blocks terraform.Blocks, isDynamic bool
246297 clones := make (map [string ]cty.Value )
247298 _ = forEachAttr .Each (func (key cty.Value , val cty.Value ) {
248299
300+ if val .IsNull () {
301+ return
302+ }
303+
249304 // instances are identified by a map key (or set member) from the value provided to for_each
250305 idx , err := convert .Convert (key , cty .String )
251306 if err != nil {
0 commit comments