Skip to content

test(api/federation): guard provider bounds across bundles and list them in the README - #1295

Merged
IAmJSD merged 4 commits into
LeanerCloud:mainfrom
IAmJSD:fix/issue-55
Oct 26, 2026
Merged

IAmJSD merged 4 commits into
LeanerCloud:mainfrom
IAmJSD:fix/issue-55

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

Root cause

The five customer federation bundles (iac/federation/*/terraform/main.tf, served as zips by buildFederationBundle) each declare their own required_providers. Nothing checks that they stay consistent with each other, and the bundle's README.txt never said which provider versions the module expects or how a customer moves to a newer line. The issue's acceptance list asks for both.

Current main, re-checked against cristim's enumeration:

Bundle aws google http random azuread azurerm
aws-cross-account >= 5.0 >= 3.4 >= 3.0
aws-target >= 5.0 >= 3.4
gcp-target >= 5.0 >= 3.4
gcp-sa-impersonation >= 5.0 >= 3.4
azure-target >= 3.4 ~> 3.8 ~> 4.0

One correction to the comment's table: aws-cross-account line 10 is hashicorp/random >= 3.0, not azurerm >= 3.0. No bundle declares azurerm except azure-target.

Two things have changed since the issue was filed:

  1. The proposed ~> 5.0 floors no longer fit. Each bundle directory now has a committed .terraform.lock.hcl at aws 6.40.0 and google 7.27.0. go:embed skips dot files, so customers never receive those locks, and a fresh init today resolves aws 6.70.0 and google 8.8.0 (shown below). ~> 5.0 would exclude the repo's own locks and every current customer.
  2. fix(iac): bound every Terraform provider constraint below the next major #1203 (ours, open) already edits every constraint line in those five main.tf files. It keeps the existing floors and adds a ceiling below the next major: aws >= 5.0, < 7.0, google >= 5.0, < 9.0, http >= 3.4, < 4.0, random >= 3.0, < 4.0. That covers the "unbounded" half of this issue. This PR changes no main.tf line, so the two PRs don't conflict.

The floors are already the same across bundles for every provider that more than one bundle uses: aws 5.0 in both AWS bundles, google 5.0 in both GCP bundles, and http 3.4 in all five. What was missing is a guard that keeps them that way, plus the README note.

Fix

  • internal/api/federation_bundle_providers.go (new): a strict line parser for the required_providers block of a bundle's embedded main.tf. Any line it doesn't recognize is an error, so a layout change fails loudly instead of dropping a provider. bundleProviderReadmeSection adds a "Provider versions" section to the bundle README.txt. It lists each provider's range read from the shipped main.tf, so the README can't drift from the constraints. It also says the bundle ships without a lock file, and that to move to a newer provider line a customer copies the new .tf files over the old ones, keeps their state, backend configuration and lock file, then runs terraform init -upgrade and reviews plan.
  • internal/api/handler_federation.go: buildFederationBundle appends that section to README.txt. That's a 5-line change.
  • internal/api/federation_bundle_providers_test.go (new):
    • TestFederationBundles_ProviderBoundsAgreeAcrossBundles renders all five bundles through getFederationIaC (the real API path), parses the rendered terraform/main.tf and fails if two bundles declare a different floor or ceiling for the same provider source. Equivalent forms are normalized: ~> 4.0 equals >= 4.0, < 5.0, and 5 equals 5.0.0. Because it compares ceilings too, it also catches a partial rollout of fix(iac): bound every Terraform provider constraint below the next major #1203's ceilings.
    • TestProviderBoundsDisagreements_DetectsDivergence checks that the guard reports a mismatched floor and a ceiling present in only one bundle, and doesn't report aligned bundles, equivalent spellings, or a provider used by only one bundle.
    • TestConstraintBounds and TestParseRequiredProviders cover the normalizer (including ~> 5 having no ceiling, as in hashicorp/go-version) and the parser's errors (inline entries, missing version, unknown attribute, unterminated block, no block).
    • TestFederationBundleReadme_ListsProviderConstraints checks that every rendered README lists each provider range from its rendered main.tf and gives the upgrade steps.

Refs #55

On its own, this PR is only part of the fix for #55: the bundles stay unbounded until #1203 merges. Once both are merged, the floors are aligned and enforced, the ceilings are in place and the README note ships, so the issue can be shut then. #1203's body currently says it leaves #55 open. Whichever of the two merges second can shut the issue.

Verification

All of this is unit-level and fixture-level evidence on Linux. No cloud API was called, nothing was planned or applied, and AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true was set throughout. Go 1.26.9, Terraform 1.10.5.

Fail before / pass after:

  • I copied the new files into a git archive origin/main tree without the handler_federation.go change. TestFederationBundleReadme_ListsProviderConstraints then FAILS for all five bundles (... does not contain "Provider versions"). On this branch it passes.
  • TestFederationBundles_ProviderBoundsAgreeAcrossBundles passes on current main too, because the floors already agree there. It's a guard, not a regression test. These mutation probes show it can fail (each applied alone to a copy of this branch):
    • aws-target aws changed to >= 6.0: FAIL hashicorp/aws has 2 different bounds across bundles: floor=">= 5.0.0" ...: aws-cross-account (>= 5.0); floor=">= 6.0.0" ...: aws-target (>= 6.0)
    • azure-target http changed to >= 3.4, < 4.0, the other four left as they are (a partial fix(iac): bound every Terraform provider constraint below the next major #1203): FAIL hashicorp/http has 2 different bounds across bundles: ... ceiling="< 4.0.0": azure-target (>= 3.4, < 4.0)

Combined with #1203 and our unopened fix/issue-153:

Rendered bundles, Terraform: I dumped all five bundles from getFederationIaC on this branch and on the #1203-combined tree. With a throwaway TF_DATA_DIR and plugin cache, I ran terraform fmt -check, terraform init -backend=false and terraform validate in each rendered terraform/ directory:

  • aws-cross-account, aws-target, gcp-sa-impersonation and gcp-target: Success! The configuration is valid. on both trees. A fresh init with no lock resolved aws 6.70.0, google 8.8.0, http 3.6.3 and random 3.9.1, which confirms that no lock ships in the zip.
  • azure-target: init fails on both trees and on main (pre-existing, see below). With terraform/modules/iam/azure/cudly-reservation-role staged at the relative path the bundle expects, validate succeeds (azuread 3.10.0, azurerm 4.81.0, http 3.6.3).
  • fmt -check: every .tf file is clean. The one finding is pre-existing, in the rendered gcp-target *.auto.tfvars (column alignment in internal/iacfiles/templates/gcp-wif.tfvars.tmpl). It's cosmetic and not touched here.

Go and repo checks (this branch):

  • go build ./..., go vet ./... and go vet -tags integration ./...: clean
  • go test ./internal/api/ -count=1: ok
  • go test ./internal/mocks/ -count=1: ok
  • golangci-lint v2.10.1 run --allow-parallel-runners ./internal/api/, with and without --build-tags integration: 0 issues
  • gocyclo -over 10 -ignore "_test\.go" on federation_bundle_providers.go and handler_federation.go: silent

Combined-tree verification. This branch was tested together with the other queued branches on a local, never-pushed tree: queue/platform @ 936996ad = origin/main a14dc8f2 plus these heads, merged in this order:

Branch Tested head
fix/issue-337 ff3420c
fix/issue-169 7d75eb5
fix/issue-342 66b221f
fix/issue-310 5e11c21
fix/issue-326 5f0ea59
fix/issue-243 365e959
fix/issue-343 6c94036
fix/issue-312 d2848a8
fix/issue-316 acd60dd
fix/issue-339 75ea9b9
fix/issue-339-commitment-type 73bdb19
fix/issue-353 5d25fcd
fix/issue-313 3156bc4
fix/issue-357 53d9e58
fix/issue-368 0ba727d
fix/issue-419 61648c9
fix/issue-111 b04c372
fix/issue-686 0ad90b7
fix/issue-674 793095d
fix/issue-1055 4ec96f1
fix/issue-559 dfa9525
fix/issue-558 034eed4
fix/issue-874 1720539
fix/issue-666 1e120da
fix/issue-668 fb26345
fix/issue-568 ce04173
fix/issue-311 e54f250
fix/issue-177 0cbb88a
fix/issue-151 c21377c
fix/issue-166 41fb77b
fix/issue-27 2d67622
fix/issue-323 69288ae
fix/issue-59 36463e8
fix/issue-43 35e4069
fix/issue-158 6f76f8a
fix/issue-104 bb00491
fix/issue-55 971ef1b
fix/issue-361 89ac220
fix/issue-120 b458719
fix/issue-36 42bac6e
fix/issue-525 57c97dd
fix/issue-712 e5bcda0
fix/issue-72 6cc63ed

fix/issue-72 and fix/issue-312 conflict on one line in internal/api/handler_dashboard.go (both change the calculateCommitmentMetrics call). The tree used #72's side with #312's validated provider variable; whichever PR merges second takes that same resolution.

Linux, Go 1.26.9, AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true; Docker Postgres via testcontainers:

  • Passed at 936996ad: go build ./..., go vet ./..., go vet -tags integration ./... and the full go test -race -short -count=1 ./....
  • Static checks at 936996ad: golangci-lint v2.10.1 found 0 issues on every changed package, with and without --build-tags integration. tests/e2e is a separate module, so it was vetted under its own e2e tag. gocyclo -over 10 is silent on the changed non-test files.
  • Integration tests, every changed package, one package at a time (-tags integration -count=1, 60-minute local timeout because the VM was shared):
    • At 936996ad, these pass: internal/server, internal/credentials, internal/scheduler, internal/database/postgres/migrations, internal/purchase and internal/api.
    • internal/config passes except TestPostgresStoreDB_ResolveStaleRIExchange_ValidatesCloudCheck, which fails the same way on unmodified main (fixed by fix(config): compare resolve checked_at at the database's microsecond precision #1252).
    • These were run on the first build of this tree (26795376, same branches at earlier heads): internal/analytics, internal/auth, internal/commitmentopts, internal/database and cmd/lambda/clear-rate-limit all pass. The later fixes don't touch those packages.
  • What the combined run caught, each fixed on its own branch before this PR opened:
    • a compile break between fix/issue-104's test and fix/issue-326's httpToLambdaRequest signature change;
    • fix/issue-316 rejecting legacy-keyed plans;
    • fix/issue-120 versus main's global-filter test.
  • Secrets: git-secrets, using the repo's scripts/setup-git-secrets.sh patterns in a fresh repo with an isolated HOME, found nothing in the changed files, and a planted fake key was flagged. scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.
  • Nothing was applied to any cloud.

Out of scope / left for an owner decision

  • Raising the floors to the tested majors (for example aws >= 6.0, google >= 7.0) would give the "deliberate floor" the issue describes, but it edits the same lines as fix(iac): bound every Terraform provider constraint below the next major #1203. It also forces any customer still locked on aws 5.x or google 5.x/6.x to upgrade on their next init. If you want that, it belongs in fix(iac): bound every Terraform provider constraint below the next major #1203 or a follow-up after it, and the guard here keeps the bundles in step either way.
  • The azure-target bundle doesn't init as downloaded. main.tf calls ../../../../terraform/modules/iam/azure/cudly-reservation-role, but addBundleTerraform zips only federation/azure-target/terraform, so the module path doesn't resolve in the extracted zip (The directory could not be read for module "cudly_reservation_role" at main.tf:69). This is unrelated to version pinning and is also noted in fix(iac): bound every Terraform provider constraint below the next major #1203. It deserves its own issue.
  • The rendered gcp-target tfvars isn't terraform fmt-clean (see above).
  • scripts/generate-federation-iac.go builds a bundle with no README at all, so the provider note is API-only. That script is being changed on our fix/issue-153 branch, and I left it alone to avoid conflicts.
  • Out of scope per the issue: the host-side azurerm 3.x line, and terraform/environments/aws (~> 5) versus its ci-cd-permissions (~> 6).

Frontend follow-up for the UI owner

None. This PR has no UI impact.


Maintainers: please mirror the issue's triage labels onto this PR: type/chore, triaged, priority/p3, severity/low, urgency/eventually, impact/few, effort/s.

🤖 Generated with Claude Code

…hem in the README

The five customer federation bundles under iac/federation each declare
their own required_providers. Nothing stopped one bundle from drifting to
a different floor or ceiling than another for the same provider, and the
bundle README never said which provider versions the module expects or
how a customer should move to a newer line.

- Parse required_providers from each bundle's embedded main.tf and append
  a "Provider versions" section to the bundle's README.txt, with the
  ranges read from the file itself so the README cannot drift from them.
  It also says the bundle ships without a lock file and how to upgrade.
- Add a test that renders every bundle through getFederationIaC and fails
  when two bundles declare a different floor or ceiling for the same
  provider ("~> 4.0" and ">= 4.0, < 5.0" count as equal).

The constraint lines themselves are left to LeanerCloud#1203, which adds the
ceilings; this commit does not edit any main.tf.

Refs LeanerCloud#55

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD IAmJSD added type/chore Maintenance / non-user-visible triaged Item has been triaged priority/p3 Polish / idea / may never ship severity/low Minor harm urgency/eventually No deadline impact/few Limited audience effort/s Hours labels Oct 23, 2026
@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

This review includes 3 billable files and costs up to $0.75.

Or wait 59 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 5a2e31e and 4982e87.

Files selected for processing: 3
  • internal/api/federation_bundle_providers.go
  • internal/api/federation_bundle_providers_test.go
  • internal/api/handler_federation.go
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

Brings in main's CI fixes (LeanerCloud#1236, LeanerCloud#1252) and CI changes since, so this PR gets a fresh CI run.

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

IAmJSD commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: REQUEST_CHANGES dc6c71108970ee8bcb013dedcee5023329c21351

Blocking:

Non-blocking:

Attacked:

  • Head checked: dc6c7110.
  • Fail-before: with the merge-base handler_federation.go, TestFederationBundleReadme_ListsProviderConstraints fails for all five bundles.
  • The agreement guard passes on main by design. As a mutation probe, aws-target aws changed to >= 6.0 fails it with "hashicorp/aws has 2 different bounds across bundles".
  • I checked that bundleModuleDir maps every target/source pair to one of the five tested modules.
  • Commands run:
    • go build ./...; go vet ./internal/api/, with and without -tags integration.
    • go test -race -short -count=1 ./internal/api/ (ok); go test -race -tags integration -count=1 ./internal/api/ (ok on the head).
    • golangci-lint v2.10.1, with and without the integration tag (0 issues); gocyclo -over 10 on the changed non-test files (none).
    • The merge-with-main probe above, run in a scratch worktree and reset afterwards.

Body-stale:

Reviewer: rB19. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths.

IAmJSD and others added 2 commits October 25, 2026 22:43
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LeanerCloud#1223 (on main) gave federationHandler a *testing.T so it can register
AssertExpectations. This branch's new renderBundleFiles still called it
with no argument, so the internal/api test build broke once main was
merged in. Same handler, same assertions.

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

IAmJSD commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the review at dc6c7110: merged origin/main and changed the one call to federationHandler(t) (#1223's new signature). New head 4982e87f. go vet (both tag sets), race unit tests on internal/api and golangci-lint pass; secrets scan clean.

@IAmJSD

IAmJSD commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE 4982e87f09da0bee6ef651cdd156691cea50dcb5

Blocking:

  • none

Non-blocking:

  • internal/api/handler_federation.go:528: the bundle download now fails with an error if a bundle's main.tf stops matching the strict required_providers parser. That is deliberate, the input is embedded, and TestFederationBundleReadme_ListsProviderConstraints renders all five module dirs that bundleModuleDir can return, so CI catches it before a customer would. Worth knowing: a future configuration_aliases line, or a provider entry written on a single line, will turn into a bundle 500 unless the parser is extended in the same change.

  • Closing chore(iac/federation): align provider version floors in customer onboarding bundles #55 is an owner call, not something this PR can settle. fix(iac): bound every Terraform provider constraint below the next major #1203 is now merged, so the ceilings are in, and this PR adds the guard and the README note. The issue's literal acceptance criteria are still not met: ~> 5.0 floors are not used (the body argues convincingly that they're obsolete), and terraform validate is not clean for azure-target, whose module path doesn't resolve in the zip (pre-existing). Refs #55 is therefore right. A maintainer should close chore(iac/federation): align provider version floors in customer onboarding bundles #55 by hand with that rationale, or file the azure-target module-path bug separately.

  • From an earlier review (rB19, on dc6c7110): add a one-line test asserting that no .terraform.lock.hcl is in the zip. That keeps the README sentence honest if //go:embed federation ever gains the all: prefix. That review's blocking federationHandler(t) compile break is fixed at this head: I merged current main into a scratch copy, and vet (both tag sets) and the federation tests pass.

Attacked:

  • Parser and guard. go:embed federation (iac/embed.go) does skip dot files, so the README's "ships without a .terraform.lock.hcl" is true. The parser tolerates terraform fmt alignment (\s*=\s*) and the >= x, < y constraints from fix(iac): bound every Terraform provider constraint below the next major #1203. The current bundles on this branch (which already contain fix(iac): bound every Terraform provider constraint below the next major #1203) parse and agree.
  • Mutation probes on this head:
    • aws-target aws changed to >= 6.0, < 7.0: TestFederationBundles_ProviderBoundsAgreeAcrossBundles FAILS with hashicorp/aws has 2 different bounds.
    • gcp-target http changed to unbounded >= 3.4: the same test FAILS with a ceiling mismatch.
  • Fail-before: I restored the merge-base handler_federation.go. TestFederationBundleReadme_ListsProviderConstraints FAILS for all 5 bundles. Restored afterwards.
  • Ran: go build ./...; go vet ./internal/api/ with and without -tags integration; go test -race -short -count=1 ./internal/api/ (ok); golangci-lint v2.10.1 with and without --build-tags integration (0 issues); gocyclo -over 10 on both changed non-test files (silent). The head merges cleanly with current main (2 commits behind).

Body-stale:

Reviewer: rB22. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths.

@IAmJSD

IAmJSD commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator Author

Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE 4982e87f09da0bee6ef651cdd156691cea50dcb5

Blocking:

  • none

Non-blocking:

  • internal/api/federation_bundle_providers_test.go applyConstraintTerm: an exact pin (= 5.0, or a bare 5.0) is reduced to floor >= 5.0.0 with no ceiling. So = 5.0 in one bundle and >= 5.0 in another compare as equal, and the guard misses that divergence. Setting the ceiling to <= v for = would close it. No bundle uses = today.
  • buildFederationBundle now depends on the strict parser in the production download path. If a future main.tf gets a trailing comment on a version line, or configuration_aliases, every bundle download for that target returns 500. TestFederationBundles_ProviderBoundsAgreeAcrossBundles renders all five bundles and would fail in CI first, so this is acceptable. It's worth one line in the parser's doc comment saying the test is what protects production.
  • Floor policy is an owner decision. The issue asks for ~> 5.0 floors. This PR (correctly) keeps >= 5.0 because the repo's own locks are on aws 6 and google 7. Raising the floors to the tested majors is still open; see Body-stale.

Attacked:

  • Coverage. Checked that federationBundleCombos covers every bundleModuleDir branch (5 of 5). Checked that iac/embed.go (//go:embed federation) skips the committed .terraform.lock.hcl, so the README's "ships without a lock file" is true. Checked that ~> ceiling derivation matches go-version for one, two and three segments.
  • Commands. go build ./..., go vet (with and without -tags integration) on ./internal/api/, go test -race -short -count=1 ./internal/api/ (ok), golangci-lint v2.10.1 (0 issues), and gocyclo -over 10 on the new file (silent). I also merged current origin/main into the scratch checkout and re-ran the federation tests: all pass.
  • Fail-before. With the merge-base handler_federation.go restored, TestFederationBundleReadme_ListsProviderConstraints fails for all 5 bundles, and it passes with the fix.
  • The guard can fail. Changing aws-target aws to >= 6.0, < 7.0 makes it FAIL with a clear message. So does dropping the azure-target http ceiling. Both mutations were reverted.

Body-stale:

Reviewer: rB23. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths.

@IAmJSD

IAmJSD commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator Author

Local verification at 4982e87f09da0bee6ef651cdd156691cea50dcb5 merged onto main (a9b7eb8f), together with #1247, #1248, #1295, #1303, #1310 in that order (combined tree a016458b, Linux, Go 1.26.10): go build ./..., go vet with and without -tags integration, the full go test -race -short -count=1 ./..., and go test -race -tags integration -count=1 on every changed package, against Docker Postgres. All pass. CI Success and pre-commit are green on this head, with 2 approving independent review(s) on it. Nothing was run against any cloud.

@IAmJSD
IAmJSD marked this pull request as ready for review October 26, 2026 00:53
@IAmJSD
IAmJSD merged commit f5fc1ac into LeanerCloud:main Oct 26, 2026
38 checks passed
@IAmJSD

IAmJSD commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator Author

Merged by IAmJSD's agent at IAmJSD's request (IAmJSD is acting owner while the maintainers are away), under the bar IAmJSD set: 2 approving independent review(s) on 4982e87f (two required: money, auth or security path), green CI, and the combined-tree verification above. Squash-merged at exactly 4982e87f09da0bee6ef651cdd156691cea50dcb5.

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

Labels

effort/s Hours impact/few Limited audience priority/p3 Polish / idea / may never ship severity/low Minor harm triaged Item has been triaged type/chore Maintenance / non-user-visible urgency/eventually No deadline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant