Skip to content

terraform: iterate primary files in a deterministic order - #2633

Merged
bendrucker merged 2 commits into
terraform-linters:masterfrom
KetchupBomb:fix/deterministic-primary-file-order
Aug 26, 2026
Merged

terraform: iterate primary files in a deterministic order#2633
bendrucker merged 2 commits into
terraform-linters:masterfrom
KetchupBomb:fix/deterministic-primary-file-order

Conversation

@KetchupBomb

@KetchupBomb KetchupBomb commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Module.PartialContent iterates the m.primaries map directly. Because Go randomizes map iteration order, modules with multiple primary .tf files produce a different block order on every tflint invocation, and any rule whose logic depends on block order (e.g. "last matching block wins") produces non-reproducible results across runs of the same, unchanged config.

This mirrors the exact same bug class that was previously reported and fixed for override files — see tflint-ruleset-terraform#205 for the original symptom report (7 tflint runs on identical input, 2 failed), and #2124 for the fix that introduced overrideFilenames + sort.Strings so overrides are iterated in a stable lexicographical order. That fix never touched the general primaries path, which this PR closes.

Repro

A real Terraform module with several aws_* resources spread across multiple primary .tf files and no required_providers constraint will non-deterministically report the "missing version constraint" warning against a different resource on each tflint run (GetProviderRefs in the terraform ruleset overwrites providerRefs[providerName] on every match, so which resource "wins" the blame depends on the primary-file iteration order that this PR pins down). I hit this reliably as an intermittently-flaking Bazel tflint test — same file content, no changes, PASS/FAIL split across bazel test --runs_per_test=8.

Fix

Mirror the override treatment for primaries exactly:

  1. terraform/module.go: add primaryFilenames []string to Module, initialize it in NewEmptyModule.
  2. terraform/parser.go (LoadConfigDir): populate primaryFilenames as each primary file is loaded, then sort.Strings it alongside overrideFilenames.
  3. terraform/module.go (Module.PartialContent): iterate m.primaryFilenames and look up m.primaries[filename], matching the existing override loop.

Test

New TestPartialContent_deterministicPrimaryOrder (in terraform/module_test.go) sets up a module with four primary files (d.tf, b.tf, a.tf, c.tf), calls PartialContent 20 times, and asserts every call returns blocks in the same lexicographical filename order (a, b, c, d). Also asserts LoadConfigDir populates primaryFilenames in sorted order.

Without the fix, Go's per-range map iteration randomization over a 4-element map makes it effectively certain that at least one of the 20 iterations returns a different order.

The existing TestRebuild fixtures were updated to also populate primaryFilenames / overrideFilenames (they construct a Module directly, bypassing NewEmptyModule / LoadConfigDir).

Downstream impact

Any downstream ruleset rule that relies on GetModuleContent / PartialContent block order for anything order-sensitive is currently exposed to this same nondeterminism — this fix benefits all rulesets, not just the resource-order-sensitive terraform_required_providers case that surfaced it.

Test plan

  • go test ./terraform/... passes locally, including new TestPartialContent_deterministicPrimaryOrder
  • go vet ./terraform/... clean
  • CI green

Module.PartialContent used to iterate the m.primaries map directly.
Because Go randomizes map iteration order, modules with multiple
primary .tf files produced a different block order on every tflint
invocation, and any rule whose logic depends on block order (e.g.
"last matching block wins") produced non-reproducible results.

The same bug class was previously reported and fixed for override
files in terraform-linters#2124 (see tflint-ruleset-terraform#205 for the original
symptom report). That fix introduced overrideFilenames plus a
sort.Strings so overrides are iterated in a stable lexicographical
order, but it did not touch the general primaries path.

Mirror the override treatment for primaries: add primaryFilenames,
populate and sort it in LoadConfigDir, and iterate it in
PartialContent. Add a regression test that invokes PartialContent
many times against a module with several primary files and asserts
the returned block order is stable and matches the sorted filename
order on every call.

Co-authored-by: Cursor <cursoragent@cursor.com>
@KetchupBomb

Copy link
Copy Markdown
Contributor Author

@wata727, I realize I didn't create an issue for this first. Apologies. Happy to do so if it helps you track incoming requests.

Restore the original one-line comment on the sorts and drop the running commentary from the regression test.

Claude-Session: https://claude.ai/code/session_01Qnx14iQmY6PBeA5RiZ3Qxy
@bendrucker
bendrucker merged commit 99208b6 into terraform-linters:master Aug 26, 2026
16 checks passed
@bendrucker

Copy link
Copy Markdown
Member

Nice one!

@KetchupBomb

Copy link
Copy Markdown
Contributor Author

@bendrucker , my organization prefers to go through proper Github releases for obtaining our external dependencies. Do you know when a (beta) build will be available containing this fix? Thanks for your time.

@bendrucker

Copy link
Copy Markdown
Member

We don't do betas since it's 0.x, typical feature release cadence is every 1-2 months, bug fixes sooner. @wata727 is the one cutting releases, usually within a couple of days for a non-critical bug.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants