Skip to content

fix(terraform): converge when a random_* value reaches a module, and evaluate every submodule on each pass - #84

Open
PushTheLimit wants to merge 2 commits into
coder:coder/preview_v0_69from
PushTheLimit:fix/random-preset-convergence
Open

PushTheLimit wants to merge 2 commits into
coder:coder/preview_v0_69from
PushTheLimit:fix/random-preset-convergence

Conversation

@PushTheLimit

@PushTheLimit PushTheLimit commented Sep 28, 2026 •

Copy link
Copy Markdown

Fixes coder/coder#30056.

Two evaluator fixes for a template that passes a random_* value into a module. Today that makes coder/preview skip every module declared after it, so their parameters disappear from the create form, and it makes every render run all 32 evaluation passes.

Evaluate every submodule on each pass. evaluateSubmodules did changed = changed || e.evaluateSubmodule(ctx, sm), so once one submodule reported a change, || short-circuited past every later submodule for that pass. A submodule whose inputs never settle therefore starved the rest of them in every pass. The call now runs before the OR.

Stable random_* placeholders. createPresetValues returned a new uuid.New() / rand.Int64() value on every call, so any local, module argument or output that read one changed on every pass and the fixed-point loop never exited early. Each placeholder is now uuid.NewSHA1 of the block ID and attribute name, which is the same on every pass within a parse and different for each block and attribute.

Either fix alone makes the reproduction in #30056 render correctly. Both are needed for the general case: the first keeps later modules from being starved by any input that doesn't settle, and the second lets the random case converge instead of running every pass.

Testing

  • TestSubmoduleWithUnstableInputDoesNotStarveLaterSubmodules: a later module loads even though an earlier one is fed a random value.
  • TestRandomPresetLetsEvaluationConverge: evaluation of a local reading random_password.result finishes in fewer than maxContextIterations steps. It fails without the preset change.
  • pkg/iac/terraform/... and pkg/iac/scanners/terraform/... pass.
  • The #30056 reproduction through coder/preview c0dfdeecbd89: params=[] plus a "Module not loaded" diagnostic before, params=[region] with no diagnostics after.

Without the two changes (the new tests only):

--- FAIL: TestSubmoduleWithUnstableInputDoesNotStarveLaterSubmodules
        	Error:      	"[0x140005421e0 0x140005423c0]" should have 3 item(s), but has 2
--- FAIL: TestRandomPresetLetsEvaluationConverge
        	Error:      	"64" is not less than "32"
FAIL	github.com/aquasecurity/trivy/pkg/iac/scanners/terraform/parser	1.014s

With them:

--- PASS: TestSubmoduleWithUnstableInputDoesNotStarveLaterSubmodules
--- PASS: TestRandomPresetLetsEvaluationConverge
ok  	github.com/aquasecurity/trivy/pkg/iac/scanners/terraform/parser	1.165s

The #30056 reproduction rendered through preview.Preview (coder/preview c0dfdeecbd89), before:

params=[]
  diag: Module not loaded. Did you run `terraform init`? | Module 'module "b_region"' in file "main.tf:18,1-18" cannot be resolved. This module will be ignored.

After:

params=[region]

We have run both changes in production for a week. On our largest template a render went from 3.2s to 0.28s, and the 11 modules it reported as "Module not loaded" load again.

The coder/preview and coder/coder pin bumps can follow the same path as #74.

AI disclosure

Written with AI assistance (Claude Code). I reviewed the change, ran the tests and the reproduction above myself, and have run it in production.

jasonwbarnett and others added 2 commits September 28, 2026 14:39
…ping at the first change

evaluateSubmodules OR-ed the result as `changed = changed || e.evaluateSubmodule(ctx, sm)`, so
once one submodule reported a change, every submodule after it in that pass was skipped. A
submodule whose inputs never settle, such as one fed a random_* preset (regenerated with
uuid.New() on each pass), keeps `changed` true on every pass, so the later submodules are never
evaluated at all and come back with no blocks. coder/preview then reports each of them as
"Module not loaded".

Evaluate the submodule first and OR afterwards, so every submodule is evaluated on every pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…converges

createPresetValues filled random_password, random_string, random_id, random_bytes, random_uuid
and random_integer with a fresh uuid.New() or rand.Int64() on every call, and it runs on every
evaluation pass. Any expression outside a resource that reads one of those values (a local, a
module argument, an output) therefore changes on every pass, the context never matches the
previous one, and evaluateSteps always runs all maxContextIterations passes.

Derive each placeholder from the block ID and attribute name instead, so it is stable across
passes within one parse and the fixed-point loop exits as soon as everything else has settled.
Distinct blocks and attributes still get distinct values.

On a 60-resource Coder template that passes a random_password into a module, this takes a
parameter render from 3.2s to 0.29s.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants