#!/usr/bin/env python3 # 2026-04-28 ogt + Claude Opus 4.7: P2-1 ConfigMap vs code default drift checker # 來源:tool-expert 統一治理方案 # 目的:CI / pre-commit 階段驗證 k8s ConfigMap 與 apps/api/src/core/config.py default 一致 # 違反「事實驅動」紅線案例:AI_FALLBACK_ORDER、ARGOCD_URL 都曾發生 drift """ ConfigMap vs Code Default Drift Checker 用法: python3 scripts/check_config_drift.py 退出碼: 0 = 全部對齊 1 = 至少一項 drift(CI 應 fail) 可加進 .pre-commit-config.yaml: - repo: local hooks: - id: config-drift-check name: ConfigMap vs code default drift entry: python3 scripts/check_config_drift.py language: python pass_filenames: false additional_dependencies: [pyyaml] """ from __future__ import annotations import json import re import sys from pathlib import Path import yaml # noqa: F401 pre-commit 會經 additional_dependencies 安裝 ROOT = Path(__file__).resolve().parent.parent CONFIGMAP_PATH = ROOT / "k8s" / "awoooi-prod" / "04-configmap.yaml" CONFIG_PY_PATH = ROOT / "apps" / "api" / "src" / "core" / "config.py" # 需要比對的欄位 # code_default_pattern: 在 config.py 找 default=... 用的 regex(DOTALL) CHECK_FIELDS: dict[str, dict[str, str]] = { "AI_FALLBACK_ORDER": { "configmap_key": "AI_FALLBACK_ORDER", "code_pattern": r"AI_FALLBACK_ORDER:\s*list\[str\]\s*=\s*Field\([^)]*?default=(\[[^\]]+\])", }, "ARGOCD_URL": { "configmap_key": "ARGOCD_URL", "code_pattern": r"ARGOCD_URL[^\n]*?\n[^)]*?default=[\"']([^\"']+)[\"']", }, "PROMETHEUS_URL": { "configmap_key": "PROMETHEUS_URL", "code_pattern": r"PROMETHEUS_URL[^\n]*?\n[^)]*?default=[\"']([^\"']+)[\"']", }, "OLLAMA_URL": { "configmap_key": "OLLAMA_URL", "code_pattern": r"OLLAMA_URL[^\n]*?\n[^)]*?default=[\"']([^\"']+)[\"']", }, } def _normalize(raw: str) -> object: """嘗試把字串解析成 list/dict,失敗就回原字串。""" raw_strip = raw.strip().strip("'\"") if raw_strip.startswith("["): try: return json.loads(raw_strip.replace("'", '"')) except json.JSONDecodeError: return raw_strip return raw_strip def main() -> int: if not CONFIGMAP_PATH.exists(): print(f"[ERROR] ConfigMap not found: {CONFIGMAP_PATH}") return 2 if not CONFIG_PY_PATH.exists(): print(f"[ERROR] config.py not found: {CONFIG_PY_PATH}") return 2 with CONFIGMAP_PATH.open() as fh: cm_data: dict = yaml.safe_load(fh).get("data", {}) or {} py_src = CONFIG_PY_PATH.read_text() exit_code = 0 print("=== ConfigMap ↔ code.default Drift Check ===") for field, spec in CHECK_FIELDS.items(): cm_raw = cm_data.get(spec["configmap_key"], "") m = re.search(spec["code_pattern"], py_src, re.DOTALL) py_raw = m.group(1) if m else "" cm_val = _normalize(cm_raw) py_val = _normalize(py_raw) if cm_val == py_val: print(f"[OK] {field}: {cm_val}") else: print(f"[DRIFT] {field}:") print(f" ConfigMap = {cm_val}") print(f" config.py = {py_val}") exit_code = 1 if exit_code == 0: print("=== All drift-check fields aligned ===") else: print("=== DRIFT detected, fix the inconsistency ===") return exit_code if __name__ == "__main__": sys.exit(main())