From e2fa32736187621b58628e4cbbd1f14765aa41cb Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Sun, 28 Jun 2026 17:26:46 +0200 Subject: [PATCH] =?UTF-8?q?feat(multi-env):=20Phase=20D3=20=E2=80=94=20erp?= =?UTF-8?q?=20iac=20creates=20erp-sandbox=20Vault=20auth=20+=20creds=20+?= =?UTF-8?q?=20KV?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-0002 Phase D, erp-repo layer. iac/main.tf iterates `envs = ["prod", "sandbox"]` so the app_roles module + the admin-bootstrap resources are materialised per environment: - module.app_roles["sandbox"] → auth/kubernetes/role/erp-sandbox + postgres/creds/erp-sandbox (dynamic role GRANTs erp_sandbox_role — the snake-case owner role created in factory#17 — and REVOKEs on DATABASE erp-sandbox; token_policies ["default","erp-sandbox"] = the policy from tools#3). - random_password.admin_initial_password["sandbox"], random_uuid.dolibarr_id ["sandbox"], and vault_kv_secret_v2.dolibarr_admin_setup["sandbox"] (kvv2/erp-sandbox/config) → the sandbox Dolibarr's own admin password + encryption id. State migration via `moved` blocks: the pre-existing single-env resources are re-keyed into the for_each map under "prod" so introducing for_each does NOT destroy+recreate them. Critical for random_uuid.dolibarr_id (prevent_destroy = true — prod encryption id + paid-module binding): a wrong/absent moved would HARD-FAIL the apply rather than lose it. The module's internal moved (role -> role[0]) chains with the module re-key. Verified the exact compound (old-module state → new module count+moved + for_each, one apply) with two standalone tofu plans: both show "X moved", sandbox created, 0 destroyed. env=prod renders byte-identical to the single-env baseline (module elision rule), so the prod erp Vault auth role, dynamic creds, admin secret + KV are unchanged. D3 of Phase D. D1 = factory#17 (DB+role, merged). D2 = tools#3 (Vault policies, merged). D4 (ArgoCD Application) is next. Co-Authored-By: Claude Opus 4.7 (1M context) --- iac/main.tf | 58 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/iac/main.tf b/iac/main.tf index e7339d3..edd0630 100644 --- a/iac/main.tf +++ b/iac/main.tf @@ -1,32 +1,66 @@ locals { app = { - name = "erp" + name = "erp" product_name = "dolibarr" # unused } + + # Environments this app is deployed to. By the elision rule (factory runbook + # conventions.md / ADR-0002) env=prod renders identical to the single-env + # baseline; non-prod envs get a "-" instance id from the module. + envs = toset(["prod", "sandbox"]) } module "app_roles" { - source = "git::ssh://git@192.168.1.202:2222/arcodange-org/tools.git//hashicorp-vault/iac/modules/app_roles?depth=1&ref=main" - name = local.app.name + source = "git::ssh://git@192.168.1.202:2222/arcodange-org/tools.git//hashicorp-vault/iac/modules/app_roles?depth=1&ref=main" + for_each = local.envs + name = local.app.name + env = each.key } resource "random_password" "admin_initial_password" { - length = 32 + for_each = local.envs + length = 32 } resource "random_uuid" "dolibarr_id" { # used for encryption as well as when buying modules + for_each = local.envs lifecycle { prevent_destroy = true } } resource "vault_kv_secret_v2" "dolibarr_admin_setup" { - mount = module.app_roles.mount_paths.kvv2 - name = format("%sconfig", module.app_roles.kvv2_path_prefix) - data_json = jsonencode( - { - DOLI_ADMIN_LOGIN = "admin", - DOLI_ADMIN_PASSWORD = random_password.admin_initial_password.result - DOLI_INSTANCE_UNIQUE_ID = random_uuid.dolibarr_id.result - } + for_each = local.envs + mount = module.app_roles[each.key].mount_paths.kvv2 + name = format("%sconfig", module.app_roles[each.key].kvv2_path_prefix) + data_json = jsonencode( + { + DOLI_ADMIN_LOGIN = "admin", + DOLI_ADMIN_PASSWORD = random_password.admin_initial_password[each.key].result + DOLI_INSTANCE_UNIQUE_ID = random_uuid.dolibarr_id[each.key].result + } ) } + +# State migration (ADR-0002 Phase D): re-key the pre-existing single-env resources +# into the for_each map under the "prod" key so that introducing for_each does NOT +# plan a destroy+create. This is critical for random_uuid.dolibarr_id — it carries +# prevent_destroy = true (it is the prod Dolibarr encryption id + paid-module +# binding), so a missing moved block would HARD-FAIL the apply rather than silently +# losing it. The module's own internal moved (role -> role[0]) chains with the +# module re-key here. +moved { + from = module.app_roles + to = module.app_roles["prod"] +} +moved { + from = random_password.admin_initial_password + to = random_password.admin_initial_password["prod"] +} +moved { + from = random_uuid.dolibarr_id + to = random_uuid.dolibarr_id["prod"] +} +moved { + from = vault_kv_secret_v2.dolibarr_admin_setup + to = vault_kv_secret_v2.dolibarr_admin_setup["prod"] +} -- 2.49.1