From 50974d2421d42d3ef6f200bf8a47662936439324 Mon Sep 17 00:00:00 2001 From: Chris Busillo Date: Wed, 3 Jun 2026 08:43:43 -0400 Subject: [PATCH] Declare Odoo prod runtime contracts --- docs/secrets.md | 12 +- .../launchplane/seed-imports/catalog.json | 134 ++++++++++++++++++ tests/test_product_onboarding.py | 85 ++++++----- 3 files changed, 186 insertions(+), 45 deletions(-) diff --git a/docs/secrets.md b/docs/secrets.md index 81982288..5ba32894 100644 --- a/docs/secrets.md +++ b/docs/secrets.md @@ -71,12 +71,12 @@ title: Secrets runtime managed-secret binding keys for the selected lane. Shared/global runtime records can provide values, but they are not synced to an unrelated product target unless that product profile declares the key. -- Odoo testing lanes declare only their stable compose runtime contract in - product onboarding seed material: `ODOO_DB_NAME`, `ODOO_DB_USER`, - `ODOO_DATA_VOLUME`, `ODOO_LOG_VOLUME`, `ODOO_DB_VOLUME`, and managed - testing-secret bindings for `ODOO_ADMIN_PASSWORD`, `ODOO_DB_PASSWORD`, and - `ODOO_MASTER_PASSWORD`. Odoo prod lanes stay undeclared until their production - runtime-secret ownership is validated explicitly. +- Odoo stable lanes declare their compose runtime contract in product onboarding + seed material: `ODOO_DB_NAME`, `ODOO_DB_USER`, `ODOO_DATA_VOLUME`, + `ODOO_LOG_VOLUME`, `ODOO_DB_VOLUME`, and managed secret bindings for + `ODOO_ADMIN_PASSWORD`, `ODOO_DB_PASSWORD`, and `ODOO_MASTER_PASSWORD`. CM prod + uses DB `cm` with `cm_prod_odoo_*` volumes; OPW prod uses DB `opw_prod` with + `opw_prod_odoo_*` volumes. - Gates fail closed when a required binding is missing, disabled, ambiguous, unclassified, or scoped outside the target context/instance. A target with an unknown environment class also fails closed. diff --git a/import-material/launchplane/seed-imports/catalog.json b/import-material/launchplane/seed-imports/catalog.json index 82981968..252e1db9 100644 --- a/import-material/launchplane/seed-imports/catalog.json +++ b/import-material/launchplane/seed-imports/catalog.json @@ -391,6 +391,18 @@ "ODOO_LOG_VOLUME": "cm_testing_odoo_logs", "ODOO_DB_VOLUME": "cm_testing_odoo_db" } + }, + { + "scope": "instance", + "context": "cm", + "instance": "prod", + "env": { + "ODOO_DB_NAME": "cm", + "ODOO_DB_USER": "odoo", + "ODOO_DATA_VOLUME": "cm_prod_odoo_data", + "ODOO_LOG_VOLUME": "cm_prod_odoo_logs", + "ODOO_DB_VOLUME": "cm_prod_odoo_db" + } } ], "secret_bindings": [ @@ -408,6 +420,21 @@ "binding_key": "ODOO_MASTER_PASSWORD", "context": "cm", "instance": "testing" + }, + { + "binding_key": "ODOO_ADMIN_PASSWORD", + "context": "cm", + "instance": "prod" + }, + { + "binding_key": "ODOO_DB_PASSWORD", + "context": "cm", + "instance": "prod" + }, + { + "binding_key": "ODOO_MASTER_PASSWORD", + "context": "cm", + "instance": "prod" } ], "expected_config": { @@ -436,6 +463,31 @@ "key": "ODOO_DB_VOLUME", "context": "cm", "instance": "testing" + }, + { + "key": "ODOO_DB_NAME", + "context": "cm", + "instance": "prod" + }, + { + "key": "ODOO_DB_USER", + "context": "cm", + "instance": "prod" + }, + { + "key": "ODOO_DATA_VOLUME", + "context": "cm", + "instance": "prod" + }, + { + "key": "ODOO_LOG_VOLUME", + "context": "cm", + "instance": "prod" + }, + { + "key": "ODOO_DB_VOLUME", + "context": "cm", + "instance": "prod" } ], "managed_secret_bindings": [ @@ -453,6 +505,21 @@ "binding_key": "ODOO_MASTER_PASSWORD", "context": "cm", "instance": "testing" + }, + { + "binding_key": "ODOO_ADMIN_PASSWORD", + "context": "cm", + "instance": "prod" + }, + { + "binding_key": "ODOO_DB_PASSWORD", + "context": "cm", + "instance": "prod" + }, + { + "binding_key": "ODOO_MASTER_PASSWORD", + "context": "cm", + "instance": "prod" } ] }, @@ -577,6 +644,18 @@ "ODOO_LOG_VOLUME": "opw_testing_odoo_logs", "ODOO_DB_VOLUME": "opw_testing_odoo_db" } + }, + { + "scope": "instance", + "context": "opw", + "instance": "prod", + "env": { + "ODOO_DB_NAME": "opw_prod", + "ODOO_DB_USER": "odoo", + "ODOO_DATA_VOLUME": "opw_prod_odoo_data", + "ODOO_LOG_VOLUME": "opw_prod_odoo_logs", + "ODOO_DB_VOLUME": "opw_prod_odoo_db" + } } ], "secret_bindings": [ @@ -594,6 +673,21 @@ "binding_key": "ODOO_MASTER_PASSWORD", "context": "opw", "instance": "testing" + }, + { + "binding_key": "ODOO_ADMIN_PASSWORD", + "context": "opw", + "instance": "prod" + }, + { + "binding_key": "ODOO_DB_PASSWORD", + "context": "opw", + "instance": "prod" + }, + { + "binding_key": "ODOO_MASTER_PASSWORD", + "context": "opw", + "instance": "prod" } ], "expected_config": { @@ -622,6 +716,31 @@ "key": "ODOO_DB_VOLUME", "context": "opw", "instance": "testing" + }, + { + "key": "ODOO_DB_NAME", + "context": "opw", + "instance": "prod" + }, + { + "key": "ODOO_DB_USER", + "context": "opw", + "instance": "prod" + }, + { + "key": "ODOO_DATA_VOLUME", + "context": "opw", + "instance": "prod" + }, + { + "key": "ODOO_LOG_VOLUME", + "context": "opw", + "instance": "prod" + }, + { + "key": "ODOO_DB_VOLUME", + "context": "opw", + "instance": "prod" } ], "managed_secret_bindings": [ @@ -639,6 +758,21 @@ "binding_key": "ODOO_MASTER_PASSWORD", "context": "opw", "instance": "testing" + }, + { + "binding_key": "ODOO_ADMIN_PASSWORD", + "context": "opw", + "instance": "prod" + }, + { + "binding_key": "ODOO_DB_PASSWORD", + "context": "opw", + "instance": "prod" + }, + { + "binding_key": "ODOO_MASTER_PASSWORD", + "context": "opw", + "instance": "prod" } ] }, diff --git a/tests/test_product_onboarding.py b/tests/test_product_onboarding.py index 59b554a5..7d557999 100644 --- a/tests/test_product_onboarding.py +++ b/tests/test_product_onboarding.py @@ -18,14 +18,14 @@ CLI_MAIN = cast(Command, main) -ODOO_TESTING_RUNTIME_KEYS = ( +ODOO_RUNTIME_KEYS = ( "ODOO_DB_NAME", "ODOO_DB_USER", "ODOO_DATA_VOLUME", "ODOO_LOG_VOLUME", "ODOO_DB_VOLUME", ) -ODOO_TESTING_SECRET_KEYS = ( +ODOO_SECRET_KEYS = ( "ODOO_ADMIN_PASSWORD", "ODOO_DB_PASSWORD", "ODOO_MASTER_PASSWORD", @@ -181,61 +181,68 @@ def _seed_import_manifest( return cast(dict[str, object], manifest_payload) -def _assert_odoo_testing_expected_runtime_contract( +def _assert_odoo_stable_lane_runtime_contract( test_case: unittest.TestCase, *, manifest: ProductOnboardingManifest, context: str, - expected_database_name: str, + expected_database_names: dict[str, str], ) -> None: - runtime_record = next( - record + runtime_records = { + record.instance: record for record in manifest.runtime_environments - if record.context == context and record.instance == "testing" - ) - test_case.assertEqual(runtime_record.scope, "instance") - test_case.assertEqual( - runtime_record.env, - { - "ODOO_DB_NAME": expected_database_name, - "ODOO_DB_USER": "odoo", - "ODOO_DATA_VOLUME": f"{context}_testing_odoo_data", - "ODOO_LOG_VOLUME": f"{context}_testing_odoo_logs", - "ODOO_DB_VOLUME": f"{context}_testing_odoo_db", - }, - ) - testing_secret_bindings = [ + if record.context == context + } + test_case.assertEqual(set(runtime_records), set(expected_database_names)) + for instance, expected_database_name in expected_database_names.items(): + runtime_record = runtime_records[instance] + test_case.assertEqual(runtime_record.scope, "instance") + volume_prefix = f"{context}_{instance}" + test_case.assertEqual( + runtime_record.env, + { + "ODOO_DB_NAME": expected_database_name, + "ODOO_DB_USER": "odoo", + "ODOO_DATA_VOLUME": f"{volume_prefix}_odoo_data", + "ODOO_LOG_VOLUME": f"{volume_prefix}_odoo_logs", + "ODOO_DB_VOLUME": f"{volume_prefix}_odoo_db", + }, + ) + + secret_bindings = [ (binding.context, binding.instance, binding.binding_key) for binding in manifest.secret_bindings - if binding.context == context and binding.instance == "testing" + if binding.context == context ] test_case.assertEqual( - testing_secret_bindings, - [(context, "testing", binding_key) for binding_key in ODOO_TESTING_SECRET_KEYS], + secret_bindings, + [ + (context, instance, binding_key) + for instance in expected_database_names + for binding_key in ODOO_SECRET_KEYS + ], ) - test_case.assertNotIn("prod", {record.instance for record in manifest.runtime_environments}) - test_case.assertNotIn("prod", {binding.instance for binding in manifest.secret_bindings}) test_case.assertEqual( [ (requirement.context, requirement.instance, requirement.key) for requirement in manifest.expected_config.runtime_environment_keys ], - [(context, "testing", key) for key in ODOO_TESTING_RUNTIME_KEYS], + [ + (context, instance, key) + for instance in expected_database_names + for key in ODOO_RUNTIME_KEYS + ], ) test_case.assertEqual( [ (requirement.context, requirement.instance, requirement.binding_key) for requirement in manifest.expected_config.managed_secret_bindings ], - [(context, "testing", binding_key) for binding_key in ODOO_TESTING_SECRET_KEYS], - ) - test_case.assertNotIn( - "prod", - {requirement.instance for requirement in manifest.expected_config.runtime_environment_keys}, - ) - test_case.assertNotIn( - "prod", - {requirement.instance for requirement in manifest.expected_config.managed_secret_bindings}, + [ + (context, instance, binding_key) + for instance in expected_database_names + for binding_key in ODOO_SECRET_KEYS + ], ) @@ -1571,11 +1578,11 @@ def test_seed_import_odoo_cm_onboarding_manifest_encodes_issue_backed_bootstrap_ ("cm", "prod", "compose", "compose-cm-prod"), ], ) - _assert_odoo_testing_expected_runtime_contract( + _assert_odoo_stable_lane_runtime_contract( self, manifest=manifest, context="cm", - expected_database_name="cm_testing", + expected_database_names={"testing": "cm_testing", "prod": "cm"}, ) self.assertEqual(manifest.source_label, "import-material:odoo-cm-product-onboarding") @@ -1623,11 +1630,11 @@ def test_seed_import_odoo_opw_onboarding_manifest_encodes_upstream_restore_polic policies["prod"].expected_domains, ("opw-prod.shinycomputers.com",), ) - _assert_odoo_testing_expected_runtime_contract( + _assert_odoo_stable_lane_runtime_contract( self, manifest=manifest, context="opw", - expected_database_name="opw_testing", + expected_database_names={"testing": "opw_testing", "prod": "opw_prod"}, ) def test_apply_product_onboarding_manifest_writes_canonical_records(self) -> None: