Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions .github/workflows/quality-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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_<env> owned by postgres. The owners must match each
# build/environments/env_<env>.example.sql, or deploy_all.sh warns.
# Test, staging and prod databases are named and owned as in Azure
# (te_<env>, owner pgadmin; #172); dev stays te_mgmt_dev owned by postgres.
# The owners must match each build/environments/env_<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"
Expand Down
27 changes: 17 additions & 10 deletions DECISIONS/2026-11-09-prod-db-naming.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Decision: Production database is `te_prod`, owned by `pgadmin`
# Decision: Azure databases are `te_<env>`, owned by `pgadmin`

Status: Decided and applied
Date: 2026-11-09
Expand All @@ -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.
Expand All @@ -36,8 +40,9 @@ canonical name on 9 Nov 2026.

## Trade-offs accepted

- Prod now differs from dev, test and staging (`te_mgmt_<env>`, owner `postgres`). The
reading code does not care; the asymmetry is in the data.
- The Azure environments (`te_<env>`, 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.
Expand All @@ -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.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
4 changes: 2 additions & 2 deletions build/config.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=""
Expand Down
6 changes: 4 additions & 2 deletions build/environments/env_staging.example.sql
Original file line number Diff line number Diff line change
@@ -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__'
Expand Down
6 changes: 4 additions & 2 deletions build/environments/env_test.example.sql
Original file line number Diff line number Diff line change
@@ -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__'
Expand Down
9 changes: 5 additions & 4 deletions docs/DATABASE_ENVIRONMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_<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-<env>` 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_<env>` 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
Expand Down
2 changes: 1 addition & 1 deletion docs/ENV_VARIABLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)_ |
Expand Down
4 changes: 2 additions & 2 deletions evals/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
]

Expand Down
2 changes: 1 addition & 1 deletion infra/terraform-prod/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
36 changes: 36 additions & 0 deletions infra/terraform-staging/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
}
}
}

Expand All @@ -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 {
Expand Down
36 changes: 36 additions & 0 deletions infra/terraform-test/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
}
}
}

Expand All @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions scripts/provision_full_test_env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/environments/test_env_staging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion tests/environments/test_env_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions tests/run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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=(
Expand Down
Loading
Loading