From 5932649083b3c6324ca82bf4d387fd37334560be Mon Sep 17 00:00:00 2001 From: bxvtr <148579658+bxvtr@users.noreply.github.com> Date: Tue, 18 Aug 2026 17:43:58 +0200 Subject: [PATCH] refactor: retire v1 repository paths --- .github/workflows/repository-validation.yml | 193 ++++++++---------- CHANGELOG.md | 4 + CONTRIBUTING.md | 2 +- README.md | 80 +++----- ansible/README.md | 13 +- .../private_runtime_config/defaults/main.yml | 6 +- apps/mlflow/overlays/v1/kustomization.yaml | 7 - apps/mlflow/overlays/v1/secrets.yaml | 23 --- .../monitoring/overlays/v1/kustomization.yaml | 7 - apps/monitoring/overlays/v1/secrets.yaml | 27 --- apps/postgres/overlays/v1/kustomization.yaml | 7 - apps/postgres/overlays/v1/secrets.yaml | 43 ---- argocd/README.md | 9 +- infrastructure/oci-provider/clusterrole.yaml | 11 - .../oci-provider/clusterrolebinding.yaml | 12 -- infrastructure/oci-provider/daemonset.yaml | 47 ----- .../oci-provider/kustomization.yaml | 5 - .../oci-provider/serviceaccount.yaml | 5 - scripts/01-system.sh | 16 -- scripts/02-microk8s.sh | 14 -- scripts/03-storage.sh | 33 --- scripts/04-secrets.sh | 24 --- scripts/05-monitoring.sh | 21 -- scripts/06-argocd.sh | 33 --- scripts/07-apps.sh | 17 -- scripts/08-runtime.sh | 8 - scripts/bootstrap-cluster.sh | 15 -- scripts/inject-runtime-values.sh | 109 ---------- .../unit/test_ansible_k8s_runtime_contract.sh | 10 +- tests/unit/test_retired_v1_paths_contract.sh | 50 +++++ 30 files changed, 189 insertions(+), 662 deletions(-) delete mode 100644 apps/mlflow/overlays/v1/kustomization.yaml delete mode 100644 apps/mlflow/overlays/v1/secrets.yaml delete mode 100644 apps/monitoring/overlays/v1/kustomization.yaml delete mode 100644 apps/monitoring/overlays/v1/secrets.yaml delete mode 100644 apps/postgres/overlays/v1/kustomization.yaml delete mode 100644 apps/postgres/overlays/v1/secrets.yaml delete mode 100644 infrastructure/oci-provider/clusterrole.yaml delete mode 100644 infrastructure/oci-provider/clusterrolebinding.yaml delete mode 100644 infrastructure/oci-provider/daemonset.yaml delete mode 100644 infrastructure/oci-provider/kustomization.yaml delete mode 100644 infrastructure/oci-provider/serviceaccount.yaml delete mode 100755 scripts/01-system.sh delete mode 100755 scripts/02-microk8s.sh delete mode 100755 scripts/03-storage.sh delete mode 100755 scripts/04-secrets.sh delete mode 100755 scripts/05-monitoring.sh delete mode 100755 scripts/06-argocd.sh delete mode 100755 scripts/07-apps.sh delete mode 100755 scripts/08-runtime.sh delete mode 100644 scripts/bootstrap-cluster.sh delete mode 100755 scripts/inject-runtime-values.sh create mode 100755 tests/unit/test_retired_v1_paths_contract.sh diff --git a/.github/workflows/repository-validation.yml b/.github/workflows/repository-validation.yml index f43e989..fd3d9e8 100644 --- a/.github/workflows/repository-validation.yml +++ b/.github/workflows/repository-validation.yml @@ -41,6 +41,9 @@ jobs: # Stubbed/static only. Does not contact OCI, SSH, Kubernetes, or download Terraform. run: ./tests/unit/test_clean_room_automation_contract.sh + - name: Run retired V1 path contract tests + run: ./tests/unit/test_retired_v1_paths_contract.sh + - name: Run safe repository validation run: ./tools/validate-safe @@ -639,14 +642,73 @@ jobs: mlflow_v2_patch = ( root / "apps/mlflow/overlays/v2/aws-default-region-patch.yaml" ) - v1_spc_paths = { - "postgres-secret-bundle": root - / "apps/postgres/overlays/v1/secrets.yaml", - "mlflow-secret-bundle": root / "apps/mlflow/overlays/v1/secrets.yaml", - "monitoring-secret-bundle": root - / "apps/monitoring/overlays/v1/secrets.yaml", + expected_spc_namespaces = { + "postgres-secret-bundle": "postgres", + "mlflow-secret-bundle": "mlflow", + "monitoring-secret-bundle": "monitoring", + } + expected_spc_secrets = { + "postgres-secret-bundle": [ + {"name": "postgresdb-naming", "fileName": "postgresdb-naming"}, + {"name": "postgres-user", "fileName": "postgres-user"}, + {"name": "postgres-password", "fileName": "postgres-password"}, + {"name": "mlflowdb-naming", "fileName": "mlflowdb-naming"}, + {"name": "mlflow-user", "fileName": "mlflow-user"}, + {"name": "mlflow-password", "fileName": "mlflow-password"}, + ], + "mlflow-secret-bundle": [ + {"name": "mlflow-db-uri", "fileName": "mlflow-db-uri"}, + ], + "monitoring-secret-bundle": [ + {"name": "grafana-login-user", "fileName": "grafana-login-user"}, + {"name": "grafana-login-password", "fileName": "grafana-login-password"}, + ], + } + expected_spc_objects = { + "postgres-secret-bundle": [ + { + "secretName": "postgres-secret", + "type": "Opaque", + "data": [ + {"objectName": "postgresdb-naming", "key": "POSTGRES_DB"}, + {"objectName": "postgres-user", "key": "POSTGRES_USER"}, + {"objectName": "postgres-password", "key": "POSTGRES_PASSWORD"}, + {"objectName": "mlflowdb-naming", "key": "MLFLOW_DB"}, + {"objectName": "mlflow-user", "key": "MLFLOW_USER"}, + {"objectName": "mlflow-password", "key": "MLFLOW_PASSWORD"}, + ], + } + ], + "mlflow-secret-bundle": [ + { + "secretName": "mlflow-secret", + "type": "Opaque", + "data": [ + {"objectName": "mlflow-db-uri", "key": "MLFLOW_DB_URI"}, + ], + } + ], + "monitoring-secret-bundle": [ + { + "secretName": "monitoring-secret", + "type": "Opaque", + "data": [ + {"objectName": "grafana-login-user", "key": "GF_USER"}, + {"objectName": "grafana-login-password", "key": "GF_PASSWORD"}, + ], + } + ], } + for rel in ( + "apps/postgres/overlays/v1", + "apps/mlflow/overlays/v1", + "apps/monitoring/overlays/v1", + ): + if (root / rel).exists(): + print(f"FAIL: retired V1 overlay path must not exist: {rel}") + sys.exit(1) + defaults = yaml.safe_load(defaults_path.read_text(encoding="utf-8")) spcs = defaults.get("private_runtime_config_secret_provider_classes") or [] if len(spcs) != 3: @@ -654,7 +716,7 @@ jobs: sys.exit(1) by_name = {spc["name"]: spc for spc in spcs} - expected_names = set(v1_spc_paths) + expected_names = set(expected_spc_namespaces) if set(by_name) != expected_names: print(f"FAIL: SPC names {sorted(by_name)} != {sorted(expected_names)}") sys.exit(1) @@ -702,51 +764,30 @@ jobs: for item in parsed ] - for name, path in v1_spc_paths.items(): - doc = yaml.safe_load(path.read_text(encoding="utf-8")) + for name, namespace in expected_spc_namespaces.items(): role_spc = by_name[name] - if doc.get("kind") != "SecretProviderClass": - print(f"FAIL: {path} is not a SecretProviderClass") - sys.exit(1) - if doc["metadata"]["name"] != name: - print(f"FAIL: {path} metadata.name mismatch") - sys.exit(1) - if doc["metadata"]["namespace"] != role_spc["namespace"]: + if role_spc.get("namespace") != namespace: print( f"FAIL: {name} namespace drift: " - f"V1={doc['metadata']['namespace']} role={role_spc['namespace']}" - ) - sys.exit(1) - if doc["spec"].get("provider") != "oci": - print(f"FAIL: {name} provider must remain oci") - sys.exit(1) - if doc["spec"]["parameters"].get("authType") != "instance": - print(f"FAIL: {name} authType must remain instance") - sys.exit(1) - if doc["spec"]["parameters"].get("vaultId") != "${{ '${VAULT_ID}' }}": - print( - f"FAIL: active V1 {name} must keep vaultId placeholder " - f"${{ '${VAULT_ID}' }}" + f"role={role_spc.get('namespace')!r} expected={namespace!r}" ) sys.exit(1) - v1_secrets = normalize_secrets_list(doc["spec"]["parameters"].get("secrets")) role_secrets = normalize_secrets_list(role_spc.get("secrets")) - if v1_secrets != role_secrets: - print(f"FAIL: {name} secrets mapping drift between V1 and role defaults") - print(f" V1={v1_secrets}") + if role_secrets != expected_spc_secrets[name]: + print(f"FAIL: {name} secrets mapping drift vs pinned V2 contract") print(f" role={role_secrets}") + print(f" expected={expected_spc_secrets[name]}") sys.exit(1) - v1_objects = normalize_secret_objects(doc["spec"].get("secretObjects") or []) role_objects = normalize_secret_objects(role_spc.get("secret_objects") or []) - if v1_objects != role_objects: - print(f"FAIL: {name} secretObjects drift between V1 and role defaults") - print(f" V1={v1_objects}") + if role_objects != expected_spc_objects[name]: + print(f"FAIL: {name} secretObjects drift vs pinned V2 contract") print(f" role={role_objects}") + print(f" expected={expected_spc_objects[name]}") sys.exit(1) - print(f"PASS: {name} matches V1 functional SPC contract (vaultId source excluded).") + print(f"PASS: {name} matches Ansible-owned SPC contract.") if defaults.get("private_runtime_config_secret_name") != "tradingchassis-runtime-config": print("FAIL: runtime Secret name must be tradingchassis-runtime-config") @@ -1469,80 +1510,8 @@ jobs: print("PASS: Manifest includes linux/amd64 and linux/arm64.") PY - - name: Validate historical V1 application overlays - run: | - set -euo pipefail - # Enable Helm for monitoring (kube-prometheus-stack via Kustomize). - export KUSTOMIZE_ENABLE_HELM=true - - python3 <<'PY' - from pathlib import Path - import subprocess - import sys - import yaml - - # kube-prometheus-stack CRDs emit bare "=" enum values. PyYAML 1.1 maps - # those to tag:yaml.org,2002:value without a SafeLoader constructor. - yaml.SafeLoader.add_constructor( - "tag:yaml.org,2002:value", - lambda loader, node: loader.construct_scalar(node), - ) - - expected = { - "apps/postgres/overlays/v1": "postgres-secret-bundle", - "apps/mlflow/overlays/v1": "mlflow-secret-bundle", - "apps/monitoring/overlays/v1": "monitoring-secret-bundle", - } - - def load_docs(text: str): - return [ - doc for doc in yaml.safe_load_all(text) - if isinstance(doc, dict) - ] - - for path, spc_name in expected.items(): - proc = subprocess.run( - ["kustomize", "build", "--enable-helm", path], - check=True, - capture_output=True, - text=True, - ) - docs = load_docs(proc.stdout) - spcs = [ - d for d in docs - if d.get("kind") == "SecretProviderClass" - and d.get("metadata", {}).get("name") == spc_name - ] - if len(spcs) != 1: - print(f"FAIL: {path} expected one SecretProviderClass named {spc_name}, found {len(spcs)}") - sys.exit(1) - vault_id = ( - spcs[0].get("spec", {}) - .get("parameters", {}) - .get("vaultId") - ) - # Escape so Actions emits the literal placeholder string for Python. - expected_vault_id = "${{ '${VAULT_ID}' }}" - if vault_id != expected_vault_id: - print(f"FAIL: {path} vaultId={vault_id!r}, expected {expected_vault_id!r} placeholder for historical V1") - sys.exit(1) - sync_options = ( - spcs[0] - .get("metadata", {}) - .get("annotations", {}) - .get("argocd.argoproj.io/sync-options") - ) - if sync_options != "Prune=false": - print( - f"FAIL: {path} SPC {spc_name} must carry " - f"argocd.argoproj.io/sync-options=Prune=false " - f"(got {sync_options!r})" - ) - sys.exit(1) - print(f"PASS: historical {path} retains V1 SPC fallback contract {spc_name}.") - - print("PASS: Historical V1 overlays remain renderable fallback artifacts.") - PY + - name: Reject retired V1 executable paths + run: ./tests/unit/test_retired_v1_paths_contract.sh - name: Validate active V2 application overlays run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 56e5f64..cbfbc37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/). - Documented the current GitHub `main-protection` required checks as present external policy, not future work - Documented one explicitly authorized finalization task after PRE-COMMIT review, with stop-on-failure and a mandatory exact-head CI gate +### Removed + +- Retired historical V1 application overlays (`apps/*/overlays/v1`), the legacy Bash bootstrap and runtime-injection path under `scripts/`, and the unused `infrastructure/oci-provider` manifests. Canonical operator automation remains `tools/bootstrap-cloud-shell` → `tools/deploy-clean-room` → `tools/verify-clean-room`. + ### Fixed - Replaced the invalid Terraform-provided scratch device-path handoff with fail-closed host-side Ansible device discovery after live OCI paravirtualized attachments returned no device path. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e34300f..1e15fcb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,7 @@ Contributions should preserve clarity, explicitness, and reproducibility. - GitOps-first for long-lived Kubernetes resources reconciled by Argo CD - No secrets in Git (ever) -- Prefer declarative manifests for application state; Version 1 still includes imperative host bootstrap scripts under `scripts/` +- Prefer declarative manifests for application state; do not reintroduce Version 1 imperative host bootstrap scripts - Keep changes minimal and documented - Single source of truth for each owned resource - Avoid ad-hoc live cluster mutations outside an explicit architecture decision diff --git a/README.md b/README.md index 24a186d..b693cab 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Single-node infrastructure for quantitative research and backtesting on OCI. | Generation | Role today | Path | | --- | --- | --- | | **Version 2** | **Current target** — Terraform → Ansible → Argo CD clean-room deploy | [`docs/V2_CLEAN_ROOM_DEPLOYMENT.md`](docs/V2_CLEAN_ROOM_DEPLOYMENT.md) | -| **Version 1** | Historical / fallback — Bash bootstrap on an existing VM | `scripts/`, [`VERSION_1_BASELINE.md`](VERSION_1_BASELINE.md) | +| **Version 1** | Historical record — not an executable repository path | [`VERSION_1_BASELINE.md`](VERSION_1_BASELINE.md) | V2 ownership: @@ -38,7 +38,6 @@ is a fallback procedure, not the primary V2 path. - GitOps-driven application reconciliation from manifests in this repository - OCI Vault-backed secret delivery through Secrets Store CSI + OCI provider - Predefined workloads: PostgreSQL, MLflow, monitoring stack, Argo Workflows, and scratch PVC overlays -- Legacy V1 Bash bootstrap scripts retained as historical fallback until V2 clean-room proof ## Architecture Overview @@ -48,18 +47,18 @@ See [`docs/V2_CLEAN_ROOM_DEPLOYMENT.md`](docs/V2_CLEAN_ROOM_DEPLOYMENT.md) for t deterministic operator sequence (Terraform outputs → Ansible inventory → `site.yml` → Argo → private runtime → acceptance). -### Version 1 host bootstrap layer (`scripts/`) — historical / fallback +### Version 1 (historical record only) -`scripts/bootstrap-cluster.sh` runs these stages in order: +Version 1 Bash bootstrap and runtime-injection scripts are retired from this +repository. The historical generation is recorded in +[`VERSION_1_BASELINE.md`](VERSION_1_BASELINE.md). It is not an executable +deployment path. -1. `01-system.sh`: flushes host iptables rules and sets ACCEPT policies -2. `02-microk8s.sh`: installs MicroK8s (`1.29/stable`) via snap and enables addons -3. `03-storage.sh`: validates/formats/mounts `/dev/oracleoci/oraclevds` at `/mnt/scratch` -4. `04-secrets.sh`: installs Secrets Store CSI Driver and OCI provider manifests -5. `05-monitoring.sh`: installs Prometheus Operator CRDs -6. `06-argocd.sh`: installs Argo CD and enables `--enable-helm` in Argo CD Kustomize build options -7. `07-apps.sh`: applies all Argo CD Application manifests from `argocd/` -8. `08-runtime.sh`: historical V1 runtime injection only (not part of the V2 clean-room path) +Canonical operator automation: + +```text +tools/bootstrap-cloud-shell → tools/deploy-clean-room → tools/verify-clean-room +``` ### GitOps application layer (`apps/` and `argocd/`) @@ -81,9 +80,6 @@ argocd/ │ ├── postgres/ # PostgreSQL deployment/pvc/service + DB init job │ └── scratch/ # scratch PVC overlays for dev/prod ├── argocd/ # Argo CD Application definitions -├── infrastructure/ -│ └── oci-provider/ # OCI CSI provider DaemonSet/RBAC -├── scripts/ # Bootstrap and runtime-injection scripts ├── VERSION_1_BASELINE.md # Version 1 ownership, limits, and V2 direction ├── CONTRIBUTING.md ├── SECURITY.md @@ -123,25 +119,24 @@ set +a | Variable | Required | Purpose | Used by | | --- | --- | --- | --- | -| `VAULT_ID` | Yes | OCI Vault OCID used in `SecretProviderClass` patches | `.env.example`, `scripts/inject-runtime-values.sh` | -| `OCI_REGION` | Yes | Region value injected as `AWS_DEFAULT_REGION` into MLflow deployment patch | `.env.example`, `scripts/inject-runtime-values.sh` | -| `ARGO_NS` | No (default: `default`) | Namespace where Argo CD `Application` resources are patched | `scripts/inject-runtime-values.sh` | +| `VAULT_ID` | Yes | OCI Vault OCID used when materializing SecretProviderClass resources | `.env.example`, Ansible `private_runtime_config` extra-vars | +| `OCI_REGION` | Yes | Region value stored in `tradingchassis-runtime-config` / `OCI_REGION` for MLflow | `.env.example`, Ansible `private_runtime_config` extra-vars | ## Required OCI Vault Secrets The following secret names are referenced directly by `SecretProviderClass` manifests and must exist in OCI Vault before bootstrap. -| Secret name | Used by | Purpose / expected value type | Source manifest | +| Secret name | Used by | Purpose / expected value type | Source contract | | --- | --- | --- | --- | -| `postgresdb-naming` | PostgreSQL | PostgreSQL database name (string) | `apps/postgres/overlays/v1/secrets.yaml` | -| `postgres-user` | PostgreSQL | PostgreSQL username (string) | `apps/postgres/overlays/v1/secrets.yaml` | -| `postgres-password` | PostgreSQL | PostgreSQL password (secret string) | `apps/postgres/overlays/v1/secrets.yaml` | -| `mlflowdb-naming` | PostgreSQL init job | MLflow database name in PostgreSQL (string) | `apps/postgres/overlays/v1/secrets.yaml` | -| `mlflow-user` | PostgreSQL init job | MLflow DB user (string) | `apps/postgres/overlays/v1/secrets.yaml` | -| `mlflow-password` | PostgreSQL init job | MLflow DB user password (secret string) | `apps/postgres/overlays/v1/secrets.yaml` | -| `mlflow-db-uri` | MLflow | Full backend store URI (secret string/URI) | `apps/mlflow/overlays/v1/secrets.yaml` | -| `grafana-login-user` | Monitoring / Grafana | Grafana admin username (string) | `apps/monitoring/overlays/v1/secrets.yaml` | -| `grafana-login-password` | Monitoring / Grafana | Grafana admin password (secret string) | `apps/monitoring/overlays/v1/secrets.yaml` | +| `postgresdb-naming` | PostgreSQL | PostgreSQL database name (string) | `ansible/roles/private_runtime_config/defaults/main.yml` | +| `postgres-user` | PostgreSQL | PostgreSQL username (string) | `ansible/roles/private_runtime_config/defaults/main.yml` | +| `postgres-password` | PostgreSQL | PostgreSQL password (secret string) | `ansible/roles/private_runtime_config/defaults/main.yml` | +| `mlflowdb-naming` | PostgreSQL init job | MLflow database name in PostgreSQL (string) | `ansible/roles/private_runtime_config/defaults/main.yml` | +| `mlflow-user` | PostgreSQL init job | MLflow DB user (string) | `ansible/roles/private_runtime_config/defaults/main.yml` | +| `mlflow-password` | PostgreSQL init job | MLflow DB user password (secret string) | `ansible/roles/private_runtime_config/defaults/main.yml` | +| `mlflow-db-uri` | MLflow | Full backend store URI (secret string/URI) | `ansible/roles/private_runtime_config/defaults/main.yml` | +| `grafana-login-user` | Monitoring / Grafana | Grafana admin username (string) | `ansible/roles/private_runtime_config/defaults/main.yml` | +| `grafana-login-password` | Monitoring / Grafana | Grafana admin password (secret string) | `ansible/roles/private_runtime_config/defaults/main.yml` | Do not commit secret values to Git. @@ -151,7 +146,7 @@ All `SecretProviderClass` resources in this repository use `authType: instance`. This repository does not include OCI IAM policy text. You must configure OCI IAM policies so the instance principal can read the required vault secrets. -## V1 historical / fallback bootstrap +## V1 historical record For **new V2 deployments**, use the canonical clean-room runbook: @@ -159,26 +154,15 @@ For **new V2 deployments**, use the canonical clean-room runbook: docs/V2_CLEAN_ROOM_DEPLOYMENT.md ``` -The script-based bootstrap below is retained only as the **historical V1 / fallback** -path until V2 clean-room validation is complete. It is not the primary setup path -and is not claimed live-validated as the V2 workflow. - -> V1 bootstrap is intended for a fresh VM under the legacy model. -> Re-running on an existing cluster is not supported by this repository flow. +Canonical operator automation: -Run (V1 fallback only): - -```bash -chmod +x scripts/* -./scripts/bootstrap-cluster.sh +```text +tools/bootstrap-cloud-shell → tools/deploy-clean-room → tools/verify-clean-room ``` -Important operational behavior during bootstrap: - -- `01-system.sh` flushes iptables and sets default ACCEPT policies -- `03-storage.sh` may format `/dev/oracleoci/oraclevds` if it has no filesystem -- `03-storage.sh` appends an `/etc/fstab` mount entry for the scratch device -- `07-apps.sh` waits only for Argo CD `Application` objects `postgres` and `mlflow` to exist (not all applications to become healthy) +The historical Version 1 Bash bootstrap is retired from this repository. See +[`VERSION_1_BASELINE.md`](VERSION_1_BASELINE.md) for that generation's +ownership and limits. It is not an executable fallback path. ## GitOps Source of Truth @@ -225,7 +209,7 @@ Access is typically done through SSH local port forwarding. Cloud firewall expos (`ansible/roles/argocd_bootstrap`, `argocd/*.yaml`). **V1 (historical Bash bootstrap):** Application objects were commonly managed in -namespace `default` (`scripts/inject-runtime-values.sh` defaults `ARGO_NS=default`). +namespace `default`. See [`VERSION_1_BASELINE.md`](VERSION_1_BASELINE.md). Check where `argocd-server` service exists: @@ -265,7 +249,7 @@ Argo CD → StorageClass tradingchassis-scratch + static hostPath PVs + namesp ### V1 historical note -Legacy Bash `scripts/03-storage.sh` mounted `/mnt/scratch` while scratch PVCs used `microk8s-hostpath`. That gap is closed in the V2 Git manifests above and still requires live clean-room validation. +Legacy V1 Bash storage bootstrap mounted `/mnt/scratch` while scratch PVCs used `microk8s-hostpath`. That gap is closed in the V2 Git manifests above and still requires live clean-room validation. ## Post-Install Verification diff --git a/ansible/README.md b/ansible/README.md index 4669dbc..3f288de 100644 --- a/ansible/README.md +++ b/ansible/README.md @@ -533,10 +533,10 @@ ANSIBLE_CONFIG=ansible/ansible.cfg \ ``` Do **not** run that playbook against a cluster while Argo CD is still -reconciling historical V1 SecretProviderClass resources as the active desired +reconciling Git-owned SecretProviderClass resources as the active desired state. Active V2 overlays omit those Git-owned SPCs; the same SPC identities (`postgres-secret-bundle`, `mlflow-secret-bundle`, -`monitoring-secret-bundle`) remain only in inactive V1 overlays. +`monitoring-secret-bundle`) are owned by Ansible `private_runtime_config`. ### Preparation (current repository state) @@ -545,9 +545,8 @@ site.yml unchanged (role not auto-run) active apps/*/kustomization.yaml → overlays/v2 Ansible owns the three SecretProviderClass resources and the runtime Secret after private-runtime-config.yml is executed -historical overlays/v1 remain inactive fallback artifacts private-runtime-config playbook exists but is not executed automatically -V1 scripts/08-runtime.sh and inject-runtime-values.sh are historical only +V1 Bash runtime injection is retired from the repository ``` ### Future ownership notes @@ -606,7 +605,6 @@ volume remount/reload behavior is not claimed without live evidence. ```text Prometheus Operator CRD ownership / monitoring app ownership cleanup canonical site.yml activation of private_runtime_config -post-proof V1 overlay and runtime-injection script cleanup ``` Kubernetes scratch binding to `/mnt/scratch` is implemented under `apps/scratch/**` (Argo-owned StorageClass/PVs/PVCs) with Ansible creating `/mnt/scratch/dev` and @@ -646,13 +644,14 @@ Canonical sequence, Cloud Shell auth, wait gates, and acceptance checklist: Active application shims point at `overlays/v2`. The private-runtime playbook supplies the SecretProviderClass resources and `tradingchassis-runtime-config` -consumed by those overlays. Do not treat V1 `scripts/inject-runtime-values.sh` +consumed by those overlays. Do not reintroduce V1 runtime injection as part of the desired V2 path. ## V1 migration strategy ```text -Existing Bash scripts remain the V1 fallback until each corresponding Ansible/Argo CD replacement has independent validation evidence. +Existing V1 Bash bootstrap scripts are retired from the repository. +Historical ownership for that generation is recorded in VERSION_1_BASELINE.md. ``` ```text diff --git a/ansible/roles/private_runtime_config/defaults/main.yml b/ansible/roles/private_runtime_config/defaults/main.yml index 1497a23..9e45131 100644 --- a/ansible/roles/private_runtime_config/defaults/main.yml +++ b/ansible/roles/private_runtime_config/defaults/main.yml @@ -58,9 +58,9 @@ private_runtime_config_forbidden_input_substrings: - "replace-me" - "not-a-secret" -# Functional SecretProviderClass contracts mirrored from -# apps/*/overlays/v1/secrets.yaml. Only vaultId sourcing differs for V2: -# Ansible renders a literal private_runtime_config_vault_id instead of ${VAULT_ID}. +# Functional SecretProviderClass contracts owned by Ansible +# private_runtime_config. vaultId is rendered from +# private_runtime_config_vault_id (not a Git ${VAULT_ID} placeholder). private_runtime_config_secret_provider_classes: - name: postgres-secret-bundle namespace: postgres diff --git a/apps/mlflow/overlays/v1/kustomization.yaml b/apps/mlflow/overlays/v1/kustomization.yaml deleted file mode 100644 index 972b9d1..0000000 --- a/apps/mlflow/overlays/v1/kustomization.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: - - ../../base - - secrets.yaml diff --git a/apps/mlflow/overlays/v1/secrets.yaml b/apps/mlflow/overlays/v1/secrets.yaml deleted file mode 100644 index 9e1c954..0000000 --- a/apps/mlflow/overlays/v1/secrets.yaml +++ /dev/null @@ -1,23 +0,0 @@ -apiVersion: secrets-store.csi.x-k8s.io/v1 -kind: SecretProviderClass -metadata: - name: mlflow-secret-bundle - namespace: mlflow - annotations: - argocd.argoproj.io/sync-options: Prune=false -spec: - provider: oci - parameters: - authType: instance - vaultId: ${VAULT_ID} - - secrets: | - - name: "mlflow-db-uri" - fileName: "mlflow-db-uri" - - secretObjects: - - secretName: mlflow-secret - type: Opaque - data: - - objectName: mlflow-db-uri - key: MLFLOW_DB_URI diff --git a/apps/monitoring/overlays/v1/kustomization.yaml b/apps/monitoring/overlays/v1/kustomization.yaml deleted file mode 100644 index 972b9d1..0000000 --- a/apps/monitoring/overlays/v1/kustomization.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: - - ../../base - - secrets.yaml diff --git a/apps/monitoring/overlays/v1/secrets.yaml b/apps/monitoring/overlays/v1/secrets.yaml deleted file mode 100644 index ed49c70..0000000 --- a/apps/monitoring/overlays/v1/secrets.yaml +++ /dev/null @@ -1,27 +0,0 @@ -apiVersion: secrets-store.csi.x-k8s.io/v1 -kind: SecretProviderClass -metadata: - name: monitoring-secret-bundle - namespace: monitoring - annotations: - argocd.argoproj.io/sync-options: Prune=false -spec: - provider: oci - parameters: - authType: instance - vaultId: ${VAULT_ID} - - secrets: | - - name: "grafana-login-user" - fileName: "grafana-login-user" - - name: "grafana-login-password" - fileName: "grafana-login-password" - - secretObjects: - - secretName: monitoring-secret - type: Opaque - data: - - objectName: "grafana-login-user" - key: GF_USER - - objectName: "grafana-login-password" - key: GF_PASSWORD diff --git a/apps/postgres/overlays/v1/kustomization.yaml b/apps/postgres/overlays/v1/kustomization.yaml deleted file mode 100644 index 972b9d1..0000000 --- a/apps/postgres/overlays/v1/kustomization.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: - - ../../base - - secrets.yaml diff --git a/apps/postgres/overlays/v1/secrets.yaml b/apps/postgres/overlays/v1/secrets.yaml deleted file mode 100644 index a52702a..0000000 --- a/apps/postgres/overlays/v1/secrets.yaml +++ /dev/null @@ -1,43 +0,0 @@ -apiVersion: secrets-store.csi.x-k8s.io/v1 -kind: SecretProviderClass -metadata: - name: postgres-secret-bundle - namespace: postgres - annotations: - argocd.argoproj.io/sync-options: Prune=false -spec: - provider: oci - parameters: - authType: instance - vaultId: ${VAULT_ID} - - secrets: | - - name: "postgresdb-naming" - fileName: "postgresdb-naming" - - name: "postgres-user" - fileName: "postgres-user" - - name: "postgres-password" - fileName: "postgres-password" - - name: "mlflowdb-naming" - fileName: "mlflowdb-naming" - - name: "mlflow-user" - fileName: "mlflow-user" - - name: "mlflow-password" - fileName: "mlflow-password" - - secretObjects: - - secretName: postgres-secret - type: Opaque - data: - - objectName: postgresdb-naming - key: POSTGRES_DB - - objectName: postgres-user - key: POSTGRES_USER - - objectName: postgres-password - key: POSTGRES_PASSWORD - - objectName: "mlflowdb-naming" - key: MLFLOW_DB - - objectName: mlflow-user - key: MLFLOW_USER - - objectName: mlflow-password - key: MLFLOW_PASSWORD diff --git a/argocd/README.md b/argocd/README.md index 9a719b5..1241e69 100644 --- a/argocd/README.md +++ b/argocd/README.md @@ -14,7 +14,6 @@ CD paths at `apps/`. Those entry points are shims that now render the ```text apps//kustomization.yaml → overlays/v2 (active) -apps//overlays/v1 → historical fallback (inactive) ``` Active V2 overlays intentionally omit SecretProviderClass resources. @@ -47,9 +46,9 @@ Historical in-place cutover sequencing (fallback only; not the Greenfield path): docs/RUNTIME_SPC_OWNERSHIP_CUTOVER.md ``` -V1 overlays and `scripts/inject-runtime-values.sh` / `scripts/08-runtime.sh` -remain in the repository as historical fallback artifacts. They are **not** -required by the canonical clean-room sequence. +Canonical operator automation is `tools/bootstrap-cloud-shell` → +`tools/deploy-clean-room` → `tools/verify-clean-room`. Historical V1 overlays +and Bash runtime-injection scripts are retired from the repository. MLflow V2 sets `AWS_DEFAULT_REGION` via `secretKeyRef` to `tradingchassis-runtime-config` / `OCI_REGION` in the `mlflow` namespace. @@ -171,5 +170,3 @@ Canonical operator sequence: `docs/V2_CLEAN_ROOM_DEPLOYMENT.md`. ### Deferred - Activation of `private_runtime_config` inside the canonical Ansible converge -- Runtime VAULT_ID / OCI_REGION script retirement after clean-room proof -- Post-proof deletion of inactive V1 overlays and historical injection scripts diff --git a/infrastructure/oci-provider/clusterrole.yaml b/infrastructure/oci-provider/clusterrole.yaml deleted file mode 100644 index 9e4ac05..0000000 --- a/infrastructure/oci-provider/clusterrole.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: oci-secrets-store-csi-driver-provider-cluster-role -rules: -- apiGroups: [""] - resources: ["secrets"] - verbs: ["get"] -- apiGroups: [""] - resources: ["serviceaccounts/token"] - verbs: ["create"] diff --git a/infrastructure/oci-provider/clusterrolebinding.yaml b/infrastructure/oci-provider/clusterrolebinding.yaml deleted file mode 100644 index 4d437c3..0000000 --- a/infrastructure/oci-provider/clusterrolebinding.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: oci-secrets-store-csi-driver-provider-cluster-rolebinding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: oci-secrets-store-csi-driver-provider-cluster-role -subjects: -- kind: ServiceAccount - name: oci-secrets-store-csi-driver-provider-sa - namespace: kube-system diff --git a/infrastructure/oci-provider/daemonset.yaml b/infrastructure/oci-provider/daemonset.yaml deleted file mode 100644 index c55700e..0000000 --- a/infrastructure/oci-provider/daemonset.yaml +++ /dev/null @@ -1,47 +0,0 @@ -apiVersion: apps/v1 -kind: DaemonSet -metadata: - name: oci-secrets-store-csi-driver-provider - namespace: kube-system - labels: - app.kubernetes.io/name: oci-secrets-store-csi-driver-provider -spec: - selector: - matchLabels: - app.kubernetes.io/name: oci-secrets-store-csi-driver-provider - updateStrategy: - type: RollingUpdate - template: - metadata: - labels: - app.kubernetes.io/name: oci-secrets-store-csi-driver-provider - spec: - serviceAccountName: oci-secrets-store-csi-driver-provider-sa - containers: - - name: oci-secrets-store-csi-driver-provider - image: ghcr.io/tradingchassis/oci-secrets-store-csi-driver-provider:latest - imagePullPolicy: Always - args: - - --endpoint=unix:///opt/provider/sockets/oci.sock - - --endpoint-permissions=0755 - - --healthz-port=8098 - - --metrics-port=8198 - env: - - name: OCI_RESOURCE_PRINCIPAL_VERSION - value: "2.2" - securityContext: - seccompProfile: - type: RuntimeDefault - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - runAsUser: 0 - capabilities: - drop: - - ALL - volumeMounts: - - mountPath: "/opt/provider/sockets" - name: socket-volume - volumes: - - name: socket-volume - hostPath: - path: /var/run/secrets-store-csi-providers diff --git a/infrastructure/oci-provider/kustomization.yaml b/infrastructure/oci-provider/kustomization.yaml deleted file mode 100644 index c3f95a4..0000000 --- a/infrastructure/oci-provider/kustomization.yaml +++ /dev/null @@ -1,5 +0,0 @@ -resources: - - clusterrole.yaml - - clusterrolebinding.yaml - - daemonset.yaml - - serviceaccount.yaml diff --git a/infrastructure/oci-provider/serviceaccount.yaml b/infrastructure/oci-provider/serviceaccount.yaml deleted file mode 100644 index 6a0a008..0000000 --- a/infrastructure/oci-provider/serviceaccount.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: oci-secrets-store-csi-driver-provider-sa - namespace: kube-system diff --git a/scripts/01-system.sh b/scripts/01-system.sh deleted file mode 100755 index f828b76..0000000 --- a/scripts/01-system.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -set -euo pipefail - -############################ -# Flush iptables for MicroK8s -############################ -echo "🧹 Flushing iptables..." -sudo iptables -F -sudo iptables -X -sudo iptables -t nat -F -sudo iptables -t nat -X -sudo iptables -t mangle -F -sudo iptables -t mangle -X -sudo iptables -P INPUT ACCEPT -sudo iptables -P FORWARD ACCEPT -sudo iptables -P OUTPUT ACCEPT diff --git a/scripts/02-microk8s.sh b/scripts/02-microk8s.sh deleted file mode 100755 index badc433..0000000 --- a/scripts/02-microk8s.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -set -euo pipefail - -############################ -# Install MicroK8s -############################ -echo "📦 Installing MicroK8s..." -sudo snap install microk8s --classic --channel=1.29/stable || true -sudo microk8s start -sudo microk8s status --wait-ready -sudo microk8s enable dns -sudo microk8s enable hostpath-storage -sudo microk8s enable metrics-server -sudo microk8s enable helm diff --git a/scripts/03-storage.sh b/scripts/03-storage.sh deleted file mode 100755 index d0efaa3..0000000 --- a/scripts/03-storage.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -set -euo pipefail - -############################ -# Prepare Scratch Volume -############################ -echo "💾 Preparing scratch disk..." - -DEVICE="/dev/oracleoci/oraclevds" -MOUNTPOINT="/mnt/scratch" - -if [ ! -b "$DEVICE" ]; then - echo "❌ Device $DEVICE not found" - exit 1 -fi - -if ! sudo blkid "$DEVICE" >/dev/null 2>&1; then - echo "📀 Formatting scratch disk..." - sudo mkfs.ext4 "$DEVICE" -fi - -sudo mkdir -p "$MOUNTPOINT" - -if ! mountpoint -q "$MOUNTPOINT"; then - sudo mount "$DEVICE" "$MOUNTPOINT" -fi - -sudo mkdir -p "$MOUNTPOINT/data" -sudo chown -R 1000:1000 "$MOUNTPOINT/data" - -if ! grep -q "$DEVICE" /etc/fstab; then - echo "$DEVICE $MOUNTPOINT ext4 defaults,nofail 0 2" | sudo tee -a /etc/fstab -fi diff --git a/scripts/04-secrets.sh b/scripts/04-secrets.sh deleted file mode 100755 index f55bf87..0000000 --- a/scripts/04-secrets.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -set -euo pipefail - -############################ -# Install Secrets Store CSI Driver -############################ -echo "🔐 Installing CSI Driver..." -sudo microk8s helm repo add secrets-store-csi-driver \ - https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts - -sudo microk8s helm repo update - -sudo microk8s helm install csi-secrets-store \ - secrets-store-csi-driver/secrets-store-csi-driver \ - --version 1.4.8 \ - -n kube-system \ - --set syncSecret.enabled=true \ - --set enableSecretRotation=true - -############################ -# Install OCI Provider -############################ -echo "☁️ Installing OCI Provider..." -sudo microk8s kubectl apply -k ./infrastructure/oci-provider/ diff --git a/scripts/05-monitoring.sh b/scripts/05-monitoring.sh deleted file mode 100755 index 8f91781..0000000 --- a/scripts/05-monitoring.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -set -euo pipefail - -############################ -# Install Prometheus Operator CRDs (REQUIRED) -############################ -echo "📊 Installing Prometheus CRDs..." - -CRD_BASE="https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/v0.76.1/example/prometheus-operator-crd" - -# Delete if partially broken -sudo microk8s kubectl delete crd prometheuses.monitoring.coreos.com --ignore-not-found -sudo microk8s kubectl delete crd alertmanagers.monitoring.coreos.com --ignore-not-found -sudo microk8s kubectl delete crd servicemonitors.monitoring.coreos.com --ignore-not-found -sudo microk8s kubectl delete crd podmonitors.monitoring.coreos.com --ignore-not-found - -# IMPORTANT: use create (NOT apply) -sudo microk8s kubectl create -f $CRD_BASE/monitoring.coreos.com_prometheuses.yaml -sudo microk8s kubectl create -f $CRD_BASE/monitoring.coreos.com_alertmanagers.yaml -sudo microk8s kubectl create -f $CRD_BASE/monitoring.coreos.com_servicemonitors.yaml -sudo microk8s kubectl create -f $CRD_BASE/monitoring.coreos.com_podmonitors.yaml diff --git a/scripts/06-argocd.sh b/scripts/06-argocd.sh deleted file mode 100755 index f2a934c..0000000 --- a/scripts/06-argocd.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -set -euo pipefail - -############################ -# Install Argo CD -############################ -echo "🚀 Installing Argo CD..." - -sudo microk8s kubectl delete crd applications.argoproj.io --ignore-not-found -sudo microk8s kubectl delete crd appprojects.argoproj.io --ignore-not-found -sudo microk8s kubectl delete crd applicationsets.argoproj.io --ignore-not-found - -sudo microk8s kubectl apply --server-side --force-conflicts \ - -f https://raw.githubusercontent.com/argoproj/argo-cd/v3.3.0/manifests/install.yaml - -echo "⏳ Waiting for ArgoCD components..." - -sudo microk8s kubectl rollout status statefulset/argocd-application-controller -n default -sudo microk8s kubectl rollout status deployment/argocd-repo-server -n default -sudo microk8s kubectl rollout status deployment/argocd-server -n default - -############################ -# Enable Helm in Kustomize -############################ -echo "🧩 Enabling Helm support..." - -sudo microk8s kubectl patch configmap argocd-cm -n default \ - --type merge \ - -p '{"data":{"kustomize.buildOptions":"--enable-helm"}}' - -sudo microk8s kubectl rollout restart deployment argocd-repo-server -n default - -sudo microk8s kubectl rollout status deployment/argocd-repo-server -n default diff --git a/scripts/07-apps.sh b/scripts/07-apps.sh deleted file mode 100755 index 979a080..0000000 --- a/scripts/07-apps.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -set -euo pipefail - -############################ -# Register Applications -############################ -echo "🔗 Applying Argo Applications..." - -sudo microk8s kubectl apply -f ./argocd/ - -until sudo microk8s kubectl -n default get application postgres >/dev/null 2>&1; do - sleep 2 -done - -until sudo microk8s kubectl -n default get application mlflow >/dev/null 2>&1; do - sleep 2 -done diff --git a/scripts/08-runtime.sh b/scripts/08-runtime.sh deleted file mode 100755 index 526d248..0000000 --- a/scripts/08-runtime.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -set -euo pipefail - -############################ -# Inject Runtime Values -############################ -echo "🔧 Injecting runtime values..." -./scripts/inject-runtime-values.sh diff --git a/scripts/bootstrap-cluster.sh b/scripts/bootstrap-cluster.sh deleted file mode 100644 index 7643b77..0000000 --- a/scripts/bootstrap-cluster.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -set -euo pipefail - -echo "### 🚀 Bootstrap MicroK8s Cluster ###" - -./scripts/01-system.sh -./scripts/02-microk8s.sh -./scripts/03-storage.sh -./scripts/04-secrets.sh -./scripts/05-monitoring.sh -./scripts/06-argocd.sh -./scripts/07-apps.sh -./scripts/08-runtime.sh - -echo "### ✅ Bootstrap Complete! ###" \ No newline at end of file diff --git a/scripts/inject-runtime-values.sh b/scripts/inject-runtime-values.sh deleted file mode 100755 index 8f7d218..0000000 --- a/scripts/inject-runtime-values.sh +++ /dev/null @@ -1,109 +0,0 @@ -#!/bin/bash -set -euo pipefail - -echo "🔧 Loading environment config..." -: "${VAULT_ID:?VAULT_ID not set}" -: "${OCI_REGION:?OCI_REGION not set}" - -ARGO_NS="${ARGO_NS:-default}" - -wait_for_app() { - local app_name=$1 - echo "⏳ Waiting for application ${app_name} in namespace ${ARGO_NS}..." - until sudo microk8s kubectl -n "${ARGO_NS}" get application "${app_name}" >/dev/null 2>&1; do - sleep 2 - done -} - -inject_app() { - local app_name=$1 - local spc_name=${2:-} # SecretProviderClass name (optional if needs_vault=false) - local deployment_name=${3:-} # Deployment name (optional if needs_region=false) - local needs_vault=${4:-false} - local needs_region=${5:-false} - - # Optional: wait for the Application to exist (comment out if you prefer "skip") - wait_for_app "${app_name}" - - echo "🔧 Injecting runtime values into ${app_name}..." - - # Start building the YAML merge patch for the Application spec - local PATCH - PATCH=$(cat < - -inject_app "postgres" "postgres-secret-bundle" "postgres" true false -inject_app "mlflow" "mlflow-secret-bundle" "mlflow" true true -inject_app "monitoring" "monitoring-secret-bundle" "monitoring" true false diff --git a/tests/unit/test_ansible_k8s_runtime_contract.sh b/tests/unit/test_ansible_k8s_runtime_contract.sh index b0699cc..6f9ee20 100755 --- a/tests/unit/test_ansible_k8s_runtime_contract.sh +++ b/tests/unit/test_ansible_k8s_runtime_contract.sh @@ -302,8 +302,16 @@ print("PASS: CI enforces the Kubernetes runtime contract") forbidden_v1 = ( "scripts/bootstrap-cluster.sh", - "scripts/06-argocd.sh", + "scripts/inject-runtime-values.sh", + "scripts/01-system.sh", + "scripts/02-microk8s.sh", "scripts/03-storage.sh", + "scripts/04-secrets.sh", + "scripts/05-monitoring.sh", + "scripts/06-argocd.sh", + "scripts/07-apps.sh", + "scripts/08-runtime.sh", + "infrastructure/oci-provider", ) for token in forbidden_v1: if token in argo_tasks or token in runtime_tasks: diff --git a/tests/unit/test_retired_v1_paths_contract.sh b/tests/unit/test_retired_v1_paths_contract.sh new file mode 100755 index 0000000..ae80741 --- /dev/null +++ b/tests/unit/test_retired_v1_paths_contract.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Fail-closed contract: retired V1 executable repository paths must stay gone. +# Static only. Does not mutate infrastructure. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT" + +python3 - "$ROOT" <<'PY' +from pathlib import Path +import re +import sys + +ROOT = Path(sys.argv[1]).resolve() + +retired = ( + "apps/mlflow/overlays/v1", + "apps/monitoring/overlays/v1", + "apps/postgres/overlays/v1", + "infrastructure/oci-provider", + "scripts/01-system.sh", + "scripts/02-microk8s.sh", + "scripts/03-storage.sh", + "scripts/04-secrets.sh", + "scripts/05-monitoring.sh", + "scripts/06-argocd.sh", + "scripts/07-apps.sh", + "scripts/08-runtime.sh", + "scripts/bootstrap-cluster.sh", + "scripts/inject-runtime-values.sh", +) + +found = [rel for rel in retired if (ROOT / rel).exists()] +if found: + raise SystemExit(f"retired V1 executable paths must not reappear: {found}") +print("PASS: retired V1 overlay, bootstrap, and oci-provider paths are absent") + +for shim in ( + "apps/postgres/kustomization.yaml", + "apps/mlflow/kustomization.yaml", + "apps/monitoring/kustomization.yaml", +): + text = (ROOT / shim).read_text(encoding="utf-8") + if "overlays/v2" not in text: + raise SystemExit(f"{shim} must select overlays/v2") + if re.search(r"(?m)^\s*-\s+overlays/v1\s*$", text): + raise SystemExit(f"{shim} must not keep overlays/v1 as the active resource") +print("PASS: active application entry points remain V2-only") +print("PASS: retired V1 path contract") +PY