From 398cda71aea71ed044aa607e50bb6a23a61c4ab0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 09:37:11 +0000 Subject: [PATCH] Align test and staging databases with their Terraform stacks (#172 follow-up) infra/terraform-test and infra/terraform-staging create te_test and te_staging for server admin pgadmin, while the deployer built te_mgmt_test and te_mgmt_staging owned by postgres: the same three-way disagreement #172 fixed for prod. They are code only (#31), so nothing was deployed; this lands before #31 resumes. - env_test/env_staging.example.sql: db_name te_test/te_staging, db_owner pgadmin. deploy_all.sh already reads both from the launcher. - infra/terraform-test, -staging: Key Vault secret and job wiring for APP_USER_PASSWORD, as for prod (entrypoint.sh refuses to deploy without it). - CI creates te_test, te_staging and te_prod owned by pgadmin, dev by postgres. - Consumers of the old names follow: evals Tier E, parity and env tests, run_tests.sh, config examples, provisioning script, README, docs. - tests/test_prod_db_naming.py becomes tests/test_azure_db_naming.py and its static checks run for prod, test and staging, so one test covers every stack. Dev stays te_mgmt_dev / postgres and is pinned as unchanged. - Decision record extended. Stacked on #172: it needs the launcher-driven deployer and the runtime launcher generation from that change. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_011T6WqjTEDRRQGLEYPPFVXx --- .github/workflows/quality-gate.yml | 15 +-- DECISIONS/2026-11-09-prod-db-naming.md | 27 ++++-- README.md | 2 +- build/config.env.example | 4 +- build/environments/env_staging.example.sql | 6 +- build/environments/env_test.example.sql | 6 +- docs/DATABASE_ENVIRONMENTS.md | 9 +- docs/ENV_VARIABLES.md | 2 +- evals/runner.py | 4 +- infra/terraform-prod/main.tf | 2 +- infra/terraform-staging/main.tf | 36 +++++++ infra/terraform-test/main.tf | 36 +++++++ scripts/provision_full_test_env.sh | 4 +- tests/environments/test_env_staging.py | 2 +- tests/environments/test_env_test.py | 2 +- tests/run_tests.sh | 4 +- ...d_db_naming.py => test_azure_db_naming.py} | 96 ++++++++++++------- tests/test_parity.py | 4 +- 18 files changed, 189 insertions(+), 72 deletions(-) rename tests/{test_prod_db_naming.py => test_azure_db_naming.py} (67%) diff --git a/.github/workflows/quality-gate.yml b/.github/workflows/quality-gate.yml index b0189a8..52ad511 100644 --- a/.github/workflows/quality-gate.yml +++ b/.github/workflows/quality-gate.yml @@ -113,9 +113,9 @@ jobs: PG_SUPERUSER_PASSWORD="postgres" PG_DB_DEV="te_mgmt_dev" PG_SCHEMA_DEV="te_dev" - PG_DB_TEST="te_mgmt_test" + PG_DB_TEST="te_test" PG_SCHEMA_TEST="te_test" - PG_DB_STAGING="te_mgmt_staging" + PG_DB_STAGING="te_staging" PG_SCHEMA_STAGING="te_staging" PG_DB_PROD="te_prod" PG_SCHEMA_PROD="te_prod" @@ -133,14 +133,15 @@ jobs: done ls -1 build/environments/ - # Prod's database is te_prod owned by pgadmin, as in Azure (#172); the - # others are te_mgmt_ owned by postgres. The owners must match each - # build/environments/env_.example.sql, or deploy_all.sh warns. + # Test, staging and prod databases are named and owned as in Azure + # (te_, owner pgadmin; #172); dev stays te_mgmt_dev owned by postgres. + # The owners must match each build/environments/env_.example.sql, or + # deploy_all.sh warns. - name: Create environment databases run: | psql -v ON_ERROR_STOP=1 -d postgres -c "CREATE ROLE pgadmin NOLOGIN" - for db in te_mgmt_dev te_mgmt_test te_mgmt_staging te_prod; do - owner=postgres; [ "$db" = te_prod ] && owner=pgadmin + for db in te_mgmt_dev te_test te_staging te_prod; do + owner=pgadmin; [ "$db" = te_mgmt_dev ] && owner=postgres if [ -z "$(psql -d postgres -tA -c "SELECT 1 FROM pg_database WHERE datname = '$db'")" ]; then psql -v ON_ERROR_STOP=1 -d postgres -c \ "CREATE DATABASE \"$db\" WITH OWNER = $owner ENCODING = 'UTF8' TEMPLATE = template0 CONNECTION LIMIT = -1" diff --git a/DECISIONS/2026-11-09-prod-db-naming.md b/DECISIONS/2026-11-09-prod-db-naming.md index e6bf861..550c2e2 100644 --- a/DECISIONS/2026-11-09-prod-db-naming.md +++ b/DECISIONS/2026-11-09-prod-db-naming.md @@ -1,4 +1,4 @@ -# Decision: Production database is `te_prod`, owned by `pgadmin` +# Decision: Azure databases are `te_`, owned by `pgadmin` Status: Decided and applied Date: 2026-11-09 @@ -10,6 +10,10 @@ The production database is **`te_prod`** and its owner is **`pgadmin`**, in all artefacts that describe it: `infra/terraform-prod/main.tf`, `build/deploy_all.sh` and `infra/entrypoint.sh`. Option (a) of #172. The schema inside it stays `te_prod`. +**Extended to test and staging (9 Nov 2026, same day):** `te_test` and `te_staging`, also +owned by `pgadmin`, matching `infra/terraform-test` and `infra/terraform-staging`. Dev +stays `te_mgmt_dev` owned by `postgres`: it is local-only and has no stack. + `build/environments/env_prod.example.sql` is the single place that states the name and owner (`db_name`, `db_owner`). The deployer reads them from the launcher it is about to run instead of keeping its own copy. @@ -36,8 +40,9 @@ canonical name on 9 Nov 2026. ## Trade-offs accepted -- Prod now differs from dev, test and staging (`te_mgmt_`, owner `postgres`). The - reading code does not care; the asymmetry is in the data. +- The Azure environments (`te_`, owner `pgadmin`) now differ from dev + (`te_mgmt_dev`, owner `postgres`). The reading code does not care; the asymmetry is in + the data, and it is the difference between a local database and a managed one. - Where `pgadmin` does not exist (CI, a dev server, the offline install), `deploy_all.sh` creates it as `NOLOGIN`. It owns the database and nothing else, so it adds no credential, but a stray role appears on those servers. @@ -47,14 +52,16 @@ canonical name on 9 Nov 2026. - `deploy` now requires `APP_USER_PASSWORD`. Without it the application role would be created with the template's placeholder password. -## Not decided here +## Test and staging -`infra/terraform-test` and `infra/terraform-staging` create `te_test` and `te_staging`, -while the deployer builds `te_mgmt_test` and `te_mgmt_staging`: the same disagreement. -Those stacks are code only (#31). Settle their names before #31 resumes. +`infra/terraform-test` and `infra/terraform-staging` already created `te_test` and +`te_staging`, while the deployer built `te_mgmt_test` and `te_mgmt_staging`: the same +disagreement as prod. Those stacks are code only (#31), so nothing was deployed. They are +aligned now, before #31 resumes, with the same launcher-driven deployer and the same +`APP_USER_PASSWORD` wiring as prod. ## Reopen trigger -Reconsider if test and staging are given the Azure-style names (then prod no longer -stands apart and the `te_mgmt_` prefix may be dropped everywhere), or if the application -is ever to run as a role other than the database owner in Azure. +Reconsider if dev is given an Azure stack (then it should follow `te_dev`/`pgadmin` and the +`te_mgmt_` prefix can go), or if the application is ever to run as a role other than the +database owner in Azure. diff --git a/README.md b/README.md index 39fba1c..f5417cd 100644 --- a/README.md +++ b/README.md @@ -332,7 +332,7 @@ organisations ──< personnel | Setting | Dev | Test | Staging | Prod | |---|---|---|---|---| -| Database | `te_mgmt_dev` | `te_mgmt_test` | `te_mgmt_staging` | `te_prod` | +| Database | `te_mgmt_dev` | `te_test` | `te_staging` | `te_prod` | | Schema | `te_dev` | `te_test` | `te_staging` | `te_prod` | | App User | `te_dev_user` | `te_test_user` | `te_stg_user` | `te_prod_user` | | Connection Limit | 10 | 15 | 25 | 50 | diff --git a/build/config.env.example b/build/config.env.example index facd282..ca9f735 100644 --- a/build/config.env.example +++ b/build/config.env.example @@ -26,13 +26,13 @@ PG_APP_USER_DEV="te_dev_user" PG_APP_PASSWORD_DEV="" PG_CONN_LIMIT_DEV="10" -PG_DB_TEST="te_mgmt_test" +PG_DB_TEST="te_test" PG_SCHEMA_TEST="te_test" PG_APP_USER_TEST="te_test_user" PG_APP_PASSWORD_TEST="" PG_CONN_LIMIT_TEST="15" -PG_DB_STAGING="te_mgmt_staging" +PG_DB_STAGING="te_staging" PG_SCHEMA_STAGING="te_staging" PG_APP_USER_STAGING="te_stg_user" PG_APP_PASSWORD_STAGING="" diff --git a/build/environments/env_staging.example.sql b/build/environments/env_staging.example.sql index 3e580f7..c90d36d 100644 --- a/build/environments/env_staging.example.sql +++ b/build/environments/env_staging.example.sql @@ -1,8 +1,10 @@ -- env_staging.example.sql (TEMPLATE - safe to commit; no real secrets) -- Copy to env_staging.sql (gitignored) and fill in, or inject app_password at deploy. +-- db_name and db_owner are read by build/deploy_all.sh and must match infra/terraform-staging +-- (database te_staging, server admin pgadmin). #172; DECISIONS/2026-11-09-prod-db-naming.md. \set env_label STAGING -\set db_name te_mgmt_staging -\set db_owner postgres +\set db_name te_staging +\set db_owner pgadmin \set schema_name te_staging \set app_user te_stg_user \set app_password '__INJECT_AT_DEPLOY__' diff --git a/build/environments/env_test.example.sql b/build/environments/env_test.example.sql index 7ad9ac1..1d25e5d 100644 --- a/build/environments/env_test.example.sql +++ b/build/environments/env_test.example.sql @@ -1,8 +1,10 @@ -- env_test.example.sql (TEMPLATE - safe to commit; no real secrets) -- Copy to env_test.sql (gitignored) and fill in, or inject app_password at deploy. +-- db_name and db_owner are read by build/deploy_all.sh and must match infra/terraform-test +-- (database te_test, server admin pgadmin). #172; DECISIONS/2026-11-09-prod-db-naming.md. \set env_label TEST -\set db_name te_mgmt_test -\set db_owner postgres +\set db_name te_test +\set db_owner pgadmin \set schema_name te_test \set app_user te_test_user \set app_password '__INJECT_AT_DEPLOY__' diff --git a/docs/DATABASE_ENVIRONMENTS.md b/docs/DATABASE_ENVIRONMENTS.md index 6adb482..8c48b33 100644 --- a/docs/DATABASE_ENVIRONMENTS.md +++ b/docs/DATABASE_ENVIRONMENTS.md @@ -12,16 +12,17 @@ for names: `build/config.env.example` (template) / `build/config.local.env` | Environment | Database | Schema | App user | Conn limit | Seeded? | |---|---|---|---|---|---| | **dev** | `te_mgmt_dev` | `te_dev` | `te_dev_user` | 10 | yes (`SEED_DEV=true`) | -| **test** | `te_mgmt_test` | `te_test` | `te_test_user` | 15 | yes (`SEED_TEST=true`) | -| **staging** | `te_mgmt_staging` | `te_staging` | `te_stg_user` | 25 | no (`SEED_STAGING=false`) | +| **test** | `te_test` | `te_test` | `te_test_user` | 15 | yes (`SEED_TEST=true`) | +| **staging** | `te_staging` | `te_staging` | `te_stg_user` | 25 | no (`SEED_STAGING=false`) | | **prod** | `te_prod` | `te_prod` | `te_prod_user` | 50 | no (`SEED_PROD=false`) | The database name and owner come from `build/environments/env_.sql` (`db_name`, `db_owner`), which `deploy_all.sh` reads; nothing else keeps its own copy. Prod's -database is `te_prod`, owned by `pgadmin`, to match `infra/terraform-prod` (#172; +database is `te_prod`, and test and staging are `te_test` and `te_staging`: each owned +by `pgadmin`, to match its `infra/terraform-` stack (#172; `DECISIONS/2026-11-09-prod-db-naming.md`). Where `pgadmin` does not exist yet (a dev server, CI, the offline install) `deploy_all.sh` creates it `NOLOGIN`: it owns the -database and nothing else. Dev, test and staging keep `te_mgmt_` owned by `postgres`. +database and nothing else. Dev is local-only and keeps `te_mgmt_dev` owned by `postgres`. Every environment also gets a `csv_uploads` schema in its own database (same name across environments — no collision, since each environment is a diff --git a/docs/ENV_VARIABLES.md b/docs/ENV_VARIABLES.md index b5bea1c..8c9f328 100644 --- a/docs/ENV_VARIABLES.md +++ b/docs/ENV_VARIABLES.md @@ -60,7 +60,7 @@ the environment SQL templates. Copy `build/config.env.example` to | Variable | Dev | Test | Staging | Prod | |---|---|---|---|---| -| `PG_DB_*` | `te_mgmt_dev` | `te_mgmt_test` | `te_mgmt_staging` | `te_prod` | +| `PG_DB_*` | `te_mgmt_dev` | `te_test` | `te_staging` | `te_prod` | | `PG_SCHEMA_*` | `te_dev` | `te_test` | `te_staging` | `te_prod` | | `PG_APP_USER_*` | `te_dev_user` | `te_test_user` | `te_stg_user` | `te_prod_user` | | `PG_APP_PASSWORD_*` | _(empty)_ | _(empty)_ | _(empty)_ | _(empty)_ | diff --git a/evals/runner.py b/evals/runner.py index 48822e4..342f393 100644 --- a/evals/runner.py +++ b/evals/runner.py @@ -557,8 +557,8 @@ def _run_fresh_deploy_then_tests( ENV_TARGETS = [ ("dev", "te_mgmt_dev", "te_dev"), - ("test", "te_mgmt_test", "te_test"), - ("staging", "te_mgmt_staging", "te_staging"), + ("test", "te_test", "te_test"), + ("staging", "te_staging", "te_staging"), ("prod", "te_prod", "te_prod"), ] diff --git a/infra/terraform-prod/main.tf b/infra/terraform-prod/main.tf index e8a88f3..91ad5c6 100644 --- a/infra/terraform-prod/main.tf +++ b/infra/terraform-prod/main.tf @@ -295,7 +295,7 @@ resource "azurerm_postgresql_flexible_server" "pg" { # The production database. Its name and the server admin ("pgadmin", the job's # PGUSER and the database owner) must equal db_name and db_owner in # build/environments/env_prod.example.sql: that file is what build/deploy_all.sh -# reads. tests/test_prod_db_naming.py fails if they drift (RV-S1, #172; +# reads. tests/test_azure_db_naming.py fails if they drift (RV-S1, #172; # DECISIONS/2026-11-09-prod-db-naming.md). resource "azurerm_postgresql_flexible_server_database" "te_prod" { name = "te_prod" diff --git a/infra/terraform-staging/main.tf b/infra/terraform-staging/main.tf index bc78fb4..46e70ec 100644 --- a/infra/terraform-staging/main.tf +++ b/infra/terraform-staging/main.tf @@ -230,6 +230,27 @@ resource "azurerm_key_vault_secret" "pg_password" { depends_on = [azurerm_role_assignment.kv_admin_terraform] } +# Password for te_stg_user, the application role te_core_schema.sql creates. +# The migration job passes it to infra/entrypoint.sh as APP_USER_PASSWORD, which +# writes it into the launcher at run time. Without it the role would be created +# with the template's placeholder password (#172). +resource "random_password" "app_user_password" { + length = 32 + special = true + min_lower = 4 + min_upper = 4 + min_numeric = 4 + override_special = "!@#$%^&*()-_=+" +} + +resource "azurerm_key_vault_secret" "app_user_password" { + name = "pg-app-user-password" + value = random_password.app_user_password.result + key_vault_id = azurerm_key_vault.kv.id + + depends_on = [azurerm_role_assignment.kv_admin_terraform] +} + # --- PostgreSQL Flexible Server (managed, HA-capable) ------------------- resource "azurerm_postgresql_flexible_server" "pg" { @@ -274,6 +295,11 @@ resource "azurerm_postgresql_flexible_server" "pg" { } } +# The staging database. Its name and the server admin ("pgadmin", the job's PGUSER +# and the database owner) must equal db_name and db_owner in +# build/environments/env_staging.example.sql: that file is what build/deploy_all.sh +# reads. tests/test_azure_db_naming.py fails if they drift (#172; +# DECISIONS/2026-11-09-prod-db-naming.md). resource "azurerm_postgresql_flexible_server_database" "te_staging" { name = "te_staging" server_id = azurerm_postgresql_flexible_server.pg.id @@ -480,6 +506,10 @@ resource "azurerm_container_app_job" "migration" { name = "PGPASSWORD" secret_name = "pg-password" } + env { + name = "APP_USER_PASSWORD" + secret_name = "app-user-password" + } } } @@ -489,6 +519,12 @@ resource "azurerm_container_app_job" "migration" { key_vault_secret_id = azurerm_key_vault_secret.pg_password.id } + secret { + name = "app-user-password" + identity = azurerm_user_assigned_identity.runner.id + key_vault_secret_id = azurerm_key_vault_secret.app_user_password.id + } + tags = local.tags lifecycle { diff --git a/infra/terraform-test/main.tf b/infra/terraform-test/main.tf index 3aa76e9..ccd96a0 100644 --- a/infra/terraform-test/main.tf +++ b/infra/terraform-test/main.tf @@ -231,6 +231,27 @@ resource "azurerm_key_vault_secret" "pg_password" { depends_on = [azurerm_role_assignment.kv_admin_terraform] } +# Password for te_test_user, the application role te_core_schema.sql creates. +# The migration job passes it to infra/entrypoint.sh as APP_USER_PASSWORD, which +# writes it into the launcher at run time. Without it the role would be created +# with the template's placeholder password (#172). +resource "random_password" "app_user_password" { + length = 32 + special = true + min_lower = 4 + min_upper = 4 + min_numeric = 4 + override_special = "!@#$%^&*()-_=+" +} + +resource "azurerm_key_vault_secret" "app_user_password" { + name = "pg-app-user-password" + value = random_password.app_user_password.result + key_vault_id = azurerm_key_vault.kv.id + + depends_on = [azurerm_role_assignment.kv_admin_terraform] +} + # --- PostgreSQL Flexible Server (managed, HA-capable) ------------------- resource "azurerm_postgresql_flexible_server" "pg" { @@ -275,6 +296,11 @@ resource "azurerm_postgresql_flexible_server" "pg" { } } +# The test database. Its name and the server admin ("pgadmin", the job's PGUSER +# and the database owner) must equal db_name and db_owner in +# build/environments/env_test.example.sql: that file is what build/deploy_all.sh +# reads. tests/test_azure_db_naming.py fails if they drift (#172; +# DECISIONS/2026-11-09-prod-db-naming.md). resource "azurerm_postgresql_flexible_server_database" "te_test" { name = "te_test" server_id = azurerm_postgresql_flexible_server.pg.id @@ -481,6 +507,10 @@ resource "azurerm_container_app_job" "migration" { name = "PGPASSWORD" secret_name = "pg-password" } + env { + name = "APP_USER_PASSWORD" + secret_name = "app-user-password" + } } } @@ -490,6 +520,12 @@ resource "azurerm_container_app_job" "migration" { key_vault_secret_id = azurerm_key_vault_secret.pg_password.id } + secret { + name = "app-user-password" + identity = azurerm_user_assigned_identity.runner.id + key_vault_secret_id = azurerm_key_vault_secret.app_user_password.id + } + tags = local.tags lifecycle { diff --git a/scripts/provision_full_test_env.sh b/scripts/provision_full_test_env.sh index 0428033..a153900 100644 --- a/scripts/provision_full_test_env.sh +++ b/scripts/provision_full_test_env.sh @@ -63,8 +63,8 @@ PG_PORT=${PGPORT:-5432} PG_SUPERUSER=${PGUSER:-postgres} PG_SUPERUSER_PASSWORD=${PGPASSWORD:-} PG_DB_DEV=te_mgmt_dev -PG_DB_TEST=te_mgmt_test -PG_DB_STAGING=te_mgmt_staging +PG_DB_TEST=te_test +PG_DB_STAGING=te_staging PG_DB_PROD=te_prod PG_SCHEMA_DEV=te_dev PG_SCHEMA_TEST=te_test diff --git a/tests/environments/test_env_staging.py b/tests/environments/test_env_staging.py index aac766a..42e05c7 100644 --- a/tests/environments/test_env_staging.py +++ b/tests/environments/test_env_staging.py @@ -7,5 +7,5 @@ class StagingEnvironment(EnvironmentContract, unittest.TestCase): - ENV = "staging"; DB = "te_mgmt_staging"; SCHEMA = "te_staging" + ENV = "staging"; DB = "te_staging"; SCHEMA = "te_staging" APP_USER = "te_stg_user"; CONN_LIMIT = 25; SEEDED = False diff --git a/tests/environments/test_env_test.py b/tests/environments/test_env_test.py index 38efb30..f5cdd69 100644 --- a/tests/environments/test_env_test.py +++ b/tests/environments/test_env_test.py @@ -7,5 +7,5 @@ class TestEnvironment(EnvironmentContract, unittest.TestCase): - ENV = "test"; DB = "te_mgmt_test"; SCHEMA = "te_test" + ENV = "test"; DB = "te_test"; SCHEMA = "te_test" APP_USER = "te_test_user"; CONN_LIMIT = 15; SEEDED = True diff --git a/tests/run_tests.sh b/tests/run_tests.sh index d1777cc..37486fe 100644 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -35,8 +35,8 @@ separator() { echo "──────────────────── # ── Environment → DB/schema/table mapping ───────────────────────────────────── declare -A ENV_DB=( [dev]="te_mgmt_dev" - [test]="te_mgmt_test" - [staging]="te_mgmt_staging" + [test]="te_test" + [staging]="te_staging" [prod]="te_prod" ) declare -A ENV_SCHEMA=( diff --git a/tests/test_prod_db_naming.py b/tests/test_azure_db_naming.py similarity index 67% rename from tests/test_prod_db_naming.py rename to tests/test_azure_db_naming.py index 5f2c582..d19d82d 100644 --- a/tests/test_prod_db_naming.py +++ b/tests/test_azure_db_naming.py @@ -1,4 +1,4 @@ -"""RV-S1 (#172): Terraform, the deployer and the entrypoint agree on the production database. +"""#172: Terraform, the deployer and the entrypoint agree on each Azure environment's database. Before this, three artefacts described production three ways: @@ -7,14 +7,16 @@ infra/entrypoint.sh required a concrete env_prod.sql that nothing in a freshly built image generates -Decision (3 Nov 2026, canonical name chosen 9 Nov 2026): the database is ``te_prod`` and -its owner is ``pgadmin``. ``build/environments/env_.example.sql`` is the single -place that says so; the deployer reads it rather than keeping its own copy. +Decision (3 Nov 2026; names chosen by the product owner 9 Nov 2026): prod, test and +staging use ``te_`` owned by ``pgadmin``, matching their Terraform stacks. Dev stays +``te_mgmt_dev`` owned by ``postgres``: it is local-only and has no stack. +``build/environments/env_.example.sql`` is the single place that says so; the +deployer reads it rather than keeping its own copy. -``ProductionArtefactsAgree`` is the regression guard: static, no database, runs in CI's -free-tier job. ``EntrypointOnAFreshServer`` is the behavioural check: it runs the real -entrypoint from a tree holding only committed files (what a clean image contains) against -a live server with no pre-existing owner role. +``AzureArtefactsAgree`` is the regression guard: static, no database, runs in CI's +free-tier job, once per Azure environment. ``EntrypointOnAFreshServer`` is the behavioural +check: it runs the real entrypoint from a tree holding only committed files (what a clean +image contains) against a live server with no pre-existing owner role. """ from __future__ import annotations @@ -36,8 +38,9 @@ ROOT = Path(__file__).resolve().parents[1] ENV_DIR = ROOT / "build" / "environments" -CANONICAL_DB = "te_prod" CANONICAL_OWNER = "pgadmin" +# environment -> its database. Each has an infra/terraform- stack. +AZURE_DATABASES = {"prod": "te_prod", "test": "te_test", "staging": "te_staging"} def launcher_var(path: Path, name: str) -> str: @@ -49,39 +52,66 @@ def launcher_var(path: Path, name: str) -> str: raise AssertionError(f"\\set {name} not found in {path}") +def launcher(env: str) -> Path: + return ENV_DIR / f"env_{env}.example.sql" + + +def terraform(env: str) -> str: + return (ROOT / "infra" / f"terraform-{env}" / "main.tf").read_text(encoding="utf-8") + + @pytest.mark.unit -class ProductionArtefactsAgree(unittest.TestCase): - prod_template = ENV_DIR / "env_prod.example.sql" - tf = (ROOT / "infra" / "terraform-prod" / "main.tf").read_text(encoding="utf-8") +class AzureArtefactsAgree(unittest.TestCase): deployer = (ROOT / "build" / "deploy_all.sh").read_text(encoding="utf-8") entrypoint = (ROOT / "infra" / "entrypoint.sh").read_text(encoding="utf-8") - def test_the_launcher_names_the_canonical_database_and_owner(self): - self.assertEqual(launcher_var(self.prod_template, "db_name"), CANONICAL_DB) - self.assertEqual(launcher_var(self.prod_template, "db_owner"), CANONICAL_OWNER) + def each_env(self): + for env, db in AZURE_DATABASES.items(): + with self.subTest(env=env): + yield env, db + + def test_each_launcher_names_the_canonical_database_and_owner(self): + for env, db in self.each_env(): + self.assertEqual(launcher_var(launcher(env), "db_name"), db) + self.assertEqual(launcher_var(launcher(env), "db_owner"), CANONICAL_OWNER) + + def test_dev_is_unchanged_and_local_only(self): + self.assertEqual(launcher_var(launcher("dev"), "db_name"), "te_mgmt_dev") + self.assertEqual(launcher_var(launcher("dev"), "db_owner"), "postgres") def test_terraform_creates_the_same_database(self): - names = re.findall( - r'resource\s+"azurerm_postgresql_flexible_server_database"\s+"\w+"\s*\{[^}]*?name\s*=\s*"([^"]+)"', - self.tf) - self.assertEqual(names, [launcher_var(self.prod_template, "db_name")], - "Terraform's database differs from the one the deployer will deploy to") + for env, _ in self.each_env(): + names = re.findall( + r'resource\s+"azurerm_postgresql_flexible_server_database"\s+"\w+"\s*\{[^}]*?name\s*=\s*"([^"]+)"', + terraform(env)) + self.assertEqual(names, [launcher_var(launcher(env), "db_name")], + "Terraform's database differs from the one the deployer will deploy to") def test_terraform_admin_is_the_launchers_owner(self): - admin = re.search(r'administrator_login\s*=\s*"([^"]+)"', self.tf).group(1) - self.assertEqual(admin, launcher_var(self.prod_template, "db_owner")) + for env, _ in self.each_env(): + admin = re.search(r'administrator_login\s*=\s*"([^"]+)"', terraform(env)).group(1) + self.assertEqual(admin, launcher_var(launcher(env), "db_owner")) def test_the_migration_job_runs_as_that_owner_against_that_database(self): - job_user = re.search(r'name\s*=\s*"PGUSER"\s*value\s*=\s*"([^"]+)"', self.tf).group(1) - self.assertEqual(job_user, launcher_var(self.prod_template, "db_owner")) - self.assertRegex(self.tf, r'name\s*=\s*"PGDATABASE"\s*value\s*=\s*azurerm_postgresql_flexible_server_database\.\w+\.name', - "PGDATABASE must come from the database resource, not a second literal") + for env, _ in self.each_env(): + tf = terraform(env) + job_user = re.search(r'name\s*=\s*"PGUSER"\s*value\s*=\s*"([^"]+)"', tf).group(1) + self.assertEqual(job_user, launcher_var(launcher(env), "db_owner")) + self.assertRegex(tf, r'name\s*=\s*"PGDATABASE"\s*value\s*=\s*azurerm_postgresql_flexible_server_database\.\w+\.name', + "PGDATABASE must come from the database resource, not a second literal") + + def test_the_migration_job_is_given_the_app_role_password(self): + """Without it entrypoint.sh refuses to deploy, so a first run would fail.""" + for env, _ in self.each_env(): + tf = terraform(env) + self.assertRegex(tf, r'name\s*=\s*"APP_USER_PASSWORD"\s*secret_name\s*=\s*"app-user-password"') + self.assertRegex(tf, r'name\s*=\s*"app-user-password"[^}]*azurerm_key_vault_secret\.app_user_password\.id') def test_every_launcher_declares_both_so_the_deployer_can_read_them(self): for env in ("dev", "test", "staging", "prod"): - path = ENV_DIR / f"env_{env}.example.sql" - self.assertTrue(launcher_var(path, "db_name"), path) - self.assertTrue(launcher_var(path, "db_owner"), path) + with self.subTest(env=env): + self.assertTrue(launcher_var(launcher(env), "db_name"), launcher(env)) + self.assertTrue(launcher_var(launcher(env), "db_owner"), launcher(env)) def test_the_deployer_keeps_no_name_or_owner_of_its_own(self): """The hardcoded copies are what drifted.""" @@ -93,9 +123,11 @@ def test_the_entrypoint_generates_its_launcher_from_the_committed_template(self) self.assertIn("example.sql", self.entrypoint, "entrypoint.sh must build env_.sql from the template; a clean image has only the template") - def test_the_template_is_not_a_populated_launcher(self): + def test_no_template_is_a_populated_launcher(self): """RV-S4 (#174): the fix must not bake a real password into the image.""" - self.assertEqual(launcher_var(self.prod_template, "app_password"), "__INJECT_AT_DEPLOY__") + for env in ("dev", "test", "staging", "prod"): + with self.subTest(env=env): + self.assertEqual(launcher_var(launcher(env), "app_password"), "__INJECT_AT_DEPLOY__") @pytest.mark.integration @@ -106,7 +138,7 @@ class EntrypointOnAFreshServer(unittest.TestCase): names so the run cannot touch a real environment; the machinery under test (name and owner read from the launcher, launcher generated at runtime, owner role created when absent) is identical. The names themselves are pinned by - ``ProductionArtefactsAgree``. + ``AzureArtefactsAgree``. """ PASSWORD = "a'b\\c&d/e\"f g" # every character the generation step must survive diff --git a/tests/test_parity.py b/tests/test_parity.py index c5ba716..17222d7 100644 --- a/tests/test_parity.py +++ b/tests/test_parity.py @@ -43,8 +43,8 @@ _ENV_CONFIG = { "dev": ("te_mgmt_dev", "te_dev"), - "test": ("te_mgmt_test", "te_test"), - "staging": ("te_mgmt_staging", "te_staging"), + "test": ("te_test", "te_test"), + "staging": ("te_staging", "te_staging"), "prod": ("te_prod", "te_prod"), }