Skip to content

fix(iac): require PostgreSQL 15+ in every environment's version input - #1255

Draft
IAmJSD wants to merge 2 commits into
LeanerCloud:mainfrom
IAmJSD:fix/issue-1233
Draft

IAmJSD wants to merge 2 commits into
LeanerCloud:mainfrom
IAmJSD:fix/issue-1233

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

Root cause

The migrations need PostgreSQL 15+ (#1110, migration 000134, uses UNIQUE NULLS NOT DISTINCT), but the Terraform environment inputs still accepted older majors:

  • terraform/environments/azure/variables.tf postgres_version: validation allowed 11-16.
  • terraform/environments/aws/variables.tf database_engine_version: no validation.
  • terraform/environments/gcp/variables.tf database_version: no validation.

So a custom or self-hosted deployment on an older engine would plan and apply cleanly, then fail at migration time.

Fix

Validate each input at plan time:

  • Azure postgres_version: contains(["15", "16"], ...).
  • GCP database_version: contains(["POSTGRES_15", "POSTGRES_16"], ...).
  • AWS database_engine_version: try(tonumber(regex("^([0-9]+)(\\.[0-9]+)?$", v)[0]) >= 15, false). This accepts the major-only pin ("16") and the "16.6" form the terraform/profiles/aws/*.tfvars.example files use. try() makes a malformed value such as "sixteen" fail validation cleanly instead of failing with a tonumber() evaluation error. (Terraform 1.10's && does not short-circuit, so the issue's plain tonumber(split(".", v)[0]) >= 15 would error instead of fail.)

Each variable's description and error message now name the 15+ requirement. docs/DEPLOYMENT.md → Prerequisites states the minimum and names the input for each cloud.

Every shipped tfvars (github-{dev,staging,prod}.tfvars, dev.tfvars.example, profiles) pins 16, so no deployed environment's plan changes.

Deploy gating: the AWS check takes effect on the next AWS plan. The Azure and GCP changes only take effect at their next deploy, which is gated on the paused Azure/GCP deploys. Until those resume, they are covered only by CI (terraform validate plus the new tftests below).

Tests

A new terraform/environments/{aws,azure,gcp}/tests/postgres_version.tftest.hcl per environment:

  • AWS: rejects 14, 14.12 and sixteen; accepts 15, 16 and 16.6.
  • Azure: rejects 14; accepts 15 and 16.
  • GCP: rejects POSTGRES_14; accepts POSTGRES_15 and POSTGRES_16.

Every provider is mocked (mock_provider), so nothing reaches a cloud API. Each run uses command = plan with plan_options { target = [module.database] }. module.database is the input's only consumer, and targeting it keeps the variable's validation in the graph without planning the whole environment against mock data. An untargeted mocked plan fails on unrelated mock-data gaps (empty AZ lists, non-12-digit mock account IDs, an unknown-at-plan count). I also checked that targeting an unrelated resource (random_id.suffix) drops the validation and gives "Missing expected failure", which is why the tests target the consumer. Fixtures used: AWS mocks aws_caller_identity/aws_availability_zones with the placeholder 123456789012 that the fargate tftest already uses. GCP sets a 64-zero placeholder credential_encryption_key, because the auto-generated one is unknown at plan and the secrets module uses it in a count.

CI did not run any environment-level tftests before this change. A new "Test environment inputs" step in the terraform-validate job (after Terraform Validate) runs terraform test in terraform/environments/<cloud> when a tests/ dir exists.

Verification

All of this is local, mocked-provider tftest evidence (Terraform v1.10.5, the CI pin). No terraform plan/apply was run against a real backend or with real credentials. Everything ran with AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null.

Commands, run on temp copies of terraform/ (after = this branch, before = origin/main plus the new test files) so .terraform/ never touched the worktree:

terraform fmt -check -recursive terraform/                         # OK
actionlint .github/workflows/ci.yml                                 # OK
terraform -chdir=<copy>/terraform/environments/<cloud> init -backend=false -input=false   # aws, azure, gcp: OK
terraform -chdir=<copy>/terraform/environments/<cloud> validate     # aws, azure, gcp: "Success! The configuration is valid."
terraform -chdir=<copy>/terraform/environments/<cloud> test         # see below
go test -count=1 ./terraform/environments/aws/ ./terraform/environments/gcp/   # ok, ok (existing tfvars/variable guard tests)

Fail-before / pass-after (terraform test):

Env before (main validation) after (this branch)
aws rejects_postgres_14 fail: "Missing expected failure", rest skipped 6 passed, 0 failed
azure rejects_postgres_14 fail: "Missing expected failure", rest skipped 3 passed, 0 failed
gcp rejects_postgres_14 fail: "Missing expected failure", rest skipped 3 passed, 0 failed

Also checked:

  • The accept runs pass on main: with the reject runs removed from the before copy, aws 3/3, azure 2/2 and gcp 2/2 passed. A failing reject run therefore comes from the missing validation, not from the harness.
  • Mutation, 15 rejected (AWS >= 16, Azure ["16"], GCP ["POSTGRES_16"]): accepts_postgres_15 fails with "Invalid value for variable" in all three environments, so the accept runs do catch an over-tight floor.

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

Branch Tested head
fix/issue-47 62feb4f
fix/issue-76 3b8c241
fix/issue-1233 e1638b7
fix/issue-153 8729074
fix/issue-532 0971624
fix/issue-64 fefdc43
fix/issue-54 91827b9

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: go build ./..., go vet ./..., go vet -tags integration ./... and the full go test -race -short -count=1 ./....
  • Static checks (changed packages): golangci-lint v2.10.1 found 0 issues, with and without --build-tags integration, and gocyclo -over 10 is silent on the changed non-test files. These ran at b61a9c30, which differs from 77d116c6 only by one .gitallowed line on fix/issue-1233.
  • Integration tests, every changed package, one package at a time (-tags integration -count=1):
    • These packages pass: cmd/cleanup-lambda, internal/analytics, internal/auth, internal/database/postgres/migrations and internal/purchase.
    • internal/api passes, except two tests whose Postgres container didn't start, or whose migrations hit a timeout, under machine load. Rerun alone on the same tree, both pass: TestDeleteManagedPlanByExecutionStatus and TestNotifiedRow_RescheduleDoesNotOverwriteConcurrentCancel.
    • 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).
    • Packages were run with a 60-minute timeout (rather than CI's 20) because the VM was shared with other test runs. Some reruns used TESTCONTAINERS_RYUK_DISABLED=true to avoid a testcontainers reaper race under load.
  • 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

  • The module-level inputs (terraform/modules/database/azure postgres_version, terraform/modules/database/gcp database_version, terraform/modules/database/aws engine_version) are still unvalidated. The Azure module's description still lists "11, 12, 13, 14, 15, 16". The issue targets the environment inputs, which are the only callers in this repo. Adding the same floor to the modules would be a small follow-up.
  • PostgreSQL 17 is not added to the Azure/GCP allowlists. The issue specifies 15/16, and accepting 17 is a separate decision about provider and service support. The AWS check is a numeric floor, so it accepts 17 already.
  • docs/DEPLOYMENT.md still says "Aurora Serverless v2 PostgreSQL 16.4" for AWS, while the env provisions an aws_db_instance pinned to major 16. This is pre-existing doc drift and is left alone.

Maintainer: please mirror the issue's triage labels onto this PR: triaged, priority/p3, severity/low, urgency/this-quarter, impact/few, effort/xs, type/bug.

Closes #1233

🤖 Generated with Claude Code

IAmJSD and others added 2 commits October 23, 2026 04:22
The migrations need PostgreSQL 15 (000134 uses UNIQUE NULLS NOT
DISTINCT), but the environment inputs still accepted older majors:
Azure's postgres_version allowed 11-16, and AWS database_engine_version
and GCP database_version had no validation. A custom deployment on an
older engine would plan and apply, then fail at migration time.

Validate all three at plan time: Azure "15"/"16", GCP
POSTGRES_15/POSTGRES_16, and AWS "<major>" or "<major>.<minor>" with a
major of 15 or more (try() makes a malformed value a validation failure
rather than an evaluation error). Every shipped tfvars pins 16, so no
deployed environment changes.

Add a mocked tftest per environment (old major rejected, 15 and 16
accepted), run by a new "Test environment inputs" step in the
terraform-validate CI job, and state the minimum in docs/DEPLOYMENT.md.

Closes LeanerCloud#1233

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

The combined-tree git-secrets run flagged
terraform/environments/aws/tests/postgres_version.tftest.hcl:10, a bare
account_id = "123456789012" in the mocked aws_caller_identity. The repo
allows that placeholder only on AccountID lines in _test.go files, so add
an anchored whole-line entry for this one mock line (bracketed digits so
the entry does not match itself), like the other terraform/ entries.

Refs LeanerCloud#1233

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD IAmJSD added triaged Item has been triaged priority/p3 Polish / idea / may never ship severity/low Minor harm urgency/this-quarter Within the quarter impact/few Limited audience effort/xs Trivial / one-liner type/bug Defect labels Oct 23, 2026
@coderabbitai

coderabbitai Bot commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

IAmJSD added a commit to IAmJSD/cloud-commitments-platform that referenced this pull request Oct 23, 2026
…S tftest

The combined-tree git-secrets run flagged
terraform/environments/aws/tests/lambda_cors_origins.tftest.hcl:10, a bare
account_id = "123456789012" in the mocked aws_caller_identity. Add an
anchored whole-line .gitallowed entry for that one line (bracketed digits so
the entry does not match itself), placed before the other terraform/ entries
so it does not collide with LeanerCloud#1255's entry for its own tftest.

Refs LeanerCloud#324

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

This branch has not been deployed

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

Labels

effort/xs Trivial / one-liner impact/few Limited audience priority/p3 Polish / idea / may never ship severity/low Minor harm triaged Item has been triaged type/bug Defect urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(iac): require PostgreSQL 15+ in every environment's version input

1 participant