#!/usr/bin/env python3 """Fixed Test-slot identity and database-refresh operations for D3 deployment. This module deliberately owns no application build, pack apply, process start, or deployment CLI. The reviewed serviceification controller is the sole remote Test deployment path; these helpers only fence the selected slot and implement the separately authorized production-snapshot refresh operation. """ from __future__ import annotations import os from pathlib import Path import deploy_staging_env as staging from common import ( DeployError, db_exec, db_query_scalar, ensure_tidb_database, load_env_file, log, require_env, reset_tidb_branch_if_requested, ) from deployment_plan import environment_config, topology_target_config from deployment_runtime_state import ( append_stage_summary, runtime_state, update_env_file, ) ENV_NAME = "test" RESET_SNAPSHOT_CONNECTOR_QUEUE_HYGIENE_PENDING = ( "TEST_RESET_SNAPSHOT_CONNECTOR_QUEUE_HYGIENE_PENDING" ) PROVIDER_CREDENTIAL_SECRET_VALUE_TABLES = ( staging.STAGING_PROVIDER_CREDENTIAL_SECRET_VALUE_TABLES ) def test_slots() -> list[str]: env_config = environment_config(ENV_NAME, error_type=DeployError) topology = env_config.get("topology") targets = topology.get("targets") if isinstance(topology, dict) else None if not isinstance(targets, list) or not targets: raise DeployError("test topology has no targets") return [str(item).strip() for item in targets if str(item).strip()] def slot_topology(slot: str) -> dict[str, str]: target_config = topology_target_config(ENV_NAME, slot, error_type=DeployError) tidb = target_config.get("tidb") if not isinstance(tidb, dict): raise DeployError(f"test slot {slot} has no tidb topology config") return { "TIDB_HOST": str(tidb["host"]).strip(), "TIDB_PORT": str(tidb["port"]).strip(), "TIDB_USER": str(tidb["user"]).strip(), "TIDB_CLOUD_CLUSTER_ID": str(tidb["cluster_id"]).strip(), "TIDB_CLOUD_BRANCH_ID": str(tidb["branch_id"]).strip(), "TIDB_PASSWORD_ENV": str(tidb["password_env"]).strip(), } def require_test_clone_scrub_target_identity( envv: dict[str, str], *, subject: str, ) -> str: target_slot = str(envv.get("DEPLOY_TARGET", "")).strip() if target_slot not in set(test_slots()): raise DeployError(f"refusing {subject} outside test1/test2/test3") expected = slot_topology(target_slot) required_identity = { "ENV_NAME": f"test-{target_slot}", "TIDB_DATABASE": "nexuscrm", "TIDB_CLOUD_CLUSTER_ID": expected["TIDB_CLOUD_CLUSTER_ID"], "TIDB_CLOUD_BRANCH_ID": expected["TIDB_CLOUD_BRANCH_ID"], } if not expected["TIDB_USER"].endswith(".root") or not expected[ "TIDB_CLOUD_BRANCH_ID" ].startswith("bran-"): raise DeployError(f"refusing {subject} with invalid committed test topology") for key, expected_value in required_identity.items(): if str(envv.get(key, "")).strip() != expected_value: raise DeployError( f"refusing {subject} because the runtime " f"does not match committed test topology: {key}" ) return envv["TIDB_DATABASE"] def reconcile_test_runtime_connection_binding( env_file: Path, *, process_env: dict[str, str] | None = None, ) -> dict[str, str]: """Converge rotatable connection material after proving slot identity.""" resolved_env_file = env_file.expanduser().resolve() envv = load_env_file(resolved_env_file) target_slot = str(envv.get("DEPLOY_TARGET", "")).strip() if target_slot not in set(test_slots()): raise DeployError("refusing to reconcile a runtime outside test1/test2/test3") require_test_clone_scrub_target_identity( envv, subject=f"{target_slot} runtime connection reconciliation", ) topology = slot_topology(target_slot) password_env = topology["TIDB_PASSWORD_ENV"] source = process_env if process_env is not None else dict(os.environ) password = str( source.get("TARGET_TIDB_PASSWORD", "") or source.get(password_env, "") ).strip() if not password: raise DeployError( f"missing required secret for {target_slot} connection reconciliation: " f"TARGET_TIDB_PASSWORD or {password_env}" ) updates = { "TIDB_HOST": topology["TIDB_HOST"], "TIDB_PORT": topology["TIDB_PORT"], "TIDB_USER": topology["TIDB_USER"], "TIDB_PASSWORD": password, "TIDB_DATABASE": "nexuscrm", "TIDB_CLOUD_CLUSTER_ID": topology["TIDB_CLOUD_CLUSTER_ID"], "TIDB_CLOUD_BRANCH_ID": topology["TIDB_CLOUD_BRANCH_ID"], } update_env_file(resolved_env_file, updates) reconciled = load_env_file(resolved_env_file) require_test_clone_scrub_target_identity( reconciled, subject=f"{target_slot} reconciled runtime connection", ) for key, value in updates.items(): if str(reconciled.get(key, "")).strip() != value: raise DeployError(f"failed to persist {target_slot} runtime binding: {key}") return reconciled def scrub_test_cloned_provider_credential_secrets( envv: dict[str, str], ) -> dict[str, dict[str, int]]: database = require_test_clone_scrub_target_identity( envv, subject="provider credential secret scrub", ) summary: dict[str, dict[str, int]] = {} for table_name in PROVIDER_CREDENTIAL_SECRET_VALUE_TABLES: before_raw = db_query_scalar( envv, f"SELECT COUNT(*) FROM `{table_name}`;", database=database ) try: before = int(before_raw) except ValueError as exc: raise DeployError( f"invalid provider credential secret count for {table_name}: {before_raw!r}" ) from exc db_exec(envv, f"DELETE FROM `{table_name}`;", database=database) remaining_raw = db_query_scalar( envv, f"SELECT COUNT(*) FROM `{table_name}`;", database=database ) try: remaining = int(remaining_raw) except ValueError as exc: raise DeployError( f"invalid provider credential secret readback for {table_name}: {remaining_raw!r}" ) from exc if remaining != 0: raise DeployError( f"test provider credential secret scrub failed for {table_name}: " f"remaining={remaining}" ) summary[table_name] = {"deleted": before, "remaining": remaining} log( "scrubbed cloned test provider credential secrets: " f"slot={envv['DEPLOY_TARGET']} table={table_name} deleted={before}" ) return summary def slot_from_env_name(env_name: str) -> str: for slot in test_slots(): if env_name.endswith(f"-{slot}"): return slot raise DeployError(f"unable to infer test slot from ENV_NAME: {env_name}") def run_test_reset_branch_snapshot_stage( env_file: Path, target_slot: str ) -> dict[str, object]: """Refresh one fixed Test database without owning application deployment.""" state = runtime_state(env_file) envv = state["envv"] assert isinstance(envv, dict) actual_slot = slot_from_env_name(str(envv.get("ENV_NAME", ""))) if actual_slot != target_slot: raise DeployError( f"test reset target mismatch: runtime env selects {actual_slot}, requested {target_slot}" ) require_test_clone_scrub_target_identity(envv, subject="test branch reset") envv["RESET_TIDB_BRANCH"] = "1" update_env_file(env_file, {"RESET_TIDB_BRANCH": "1"}) reset_result = reset_tidb_branch_if_requested(envv) if not isinstance(reset_result, dict): raise DeployError("test reset stage did not return TiDB provider reset evidence") ensure_tidb_database(envv) secret_scrub = scrub_test_cloned_provider_credential_secrets(envv) envv["RESET_TIDB_BRANCH"] = "0" update_env_file( env_file, { "RESET_TIDB_BRANCH": "0", RESET_SNAPSHOT_CONNECTOR_QUEUE_HYGIENE_PENDING: "1", }, ) summary: dict[str, object] = { "runtime_env_file": str(env_file), "target_slot": target_slot, "provider_reset_epoch": { "field": "parentTimestamp", "previous": str(reset_result["previous_parent_timestamp"]), "value": str(reset_result["parent_timestamp"]), }, "reset_http_status": reset_result["reset_http_status"], "reset_retry_http_status": reset_result["reset_retry_http_status"], "provider_credential_secret_scrub": secret_scrub, "connector_queue_hygiene_pending": True, "status": "passed", } append_stage_summary(envv, "reset-branch-snapshot", summary) return summary