Files
awoooi/apps/api/src/utils/k8s_naming.py
OG T 938df7f291 fix(api): 全面清除假信心分數 - 遵循 feedback_confidence_truthfulness.md
🔴 違規修正: 規則匹配/Expert System 不是 AI 分析,confidence 必須 = 0.0

修正檔案:
- agents/action_planner.py: 0.9 → 0.0
- agents/blast_radius.py: 0.85/0.5/0.9 → 0.0
- agents/security.py: 計算公式 → 0.0
- signoz_webhook.py: 0.7 → 0.0
- auto_approve.py: default 0.5 → 0.0
- ci_auto_repair.py: 整個計算函數 → return 0.0
- error_analyzer_service.py: default 0.5 → 0.0
- intent_classifier.py: 計算公式 → 0.0
- openclaw.py: default 0.5 → 0.0
- resource_resolver.py: 0.8 → 0.0
- k8s_naming.py: 0.9/0.7 → 0.0

只有 LLM 真實分析返回的 confidence 才能 > 0

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-29 16:00:46 +08:00

302 lines
8.2 KiB
Python

"""
K8s Resource Naming Utilities - ADR-016 資源名稱規範
=====================================================
提供 K8s 資源名稱正規化與驗證功能:
1. URL/域名 → 有效 K8s 名稱
2. 格式驗證 (RFC 1123)
3. 靜態映射表查詢
K8s 命名規則 (RFC 1123):
- 最多 63 字元
- 只能包含小寫字母、數字、連字號
- 必須以字母或數字開頭和結尾
版本: v1.0
建立: 2026-03-26 (台北時區)
建立者: Claude Code (首席架構師)
@see docs/adr/ADR-016-k8s-resource-naming.md
"""
import re
from dataclasses import dataclass
from enum import Enum
from typing import Final
import structlog
logger = structlog.get_logger(__name__)
# =============================================================================
# Constants
# =============================================================================
# K8s 名稱正則 (RFC 1123 subdomain)
K8S_NAME_PATTERN: Final[re.Pattern] = re.compile(
r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$"
)
# 最大長度
K8S_NAME_MAX_LENGTH: Final[int] = 63
# =============================================================================
# Static Mapping Table (Fallback)
# =============================================================================
# URL/域名 → K8s Deployment 映射
# 當動態查詢失敗時使用
RESOURCE_MAPPING: Final[dict[str, tuple[str, str]]] = {
# 域名 → (deployment_name, namespace)
"api.awoooi.wooo.work": ("awoooi-api", "awoooi-prod"),
"awoooi.wooo.work": ("awoooi-web", "awoooi-prod"),
"wooo.work": ("awoooi-web", "awoooi-prod"),
# 服務別名
"awoooi-api": ("awoooi-api", "awoooi-prod"),
"awoooi-web": ("awoooi-web", "awoooi-prod"),
"openclaw": ("openclaw", "awoooi-prod"),
# 內部服務
"signoz": ("signoz-otel-collector", "signoz"),
"langfuse": ("langfuse-web", "langfuse"),
}
# 非 K8s 資源標記 (這些主機不在 K8s 中)
NON_K8S_HOSTS: Final[set[str]] = {
"prod-docker-188",
"192.168.0.188",
"192.168.0.110",
"192.168.0.112",
}
# =============================================================================
# Types
# =============================================================================
class ResourceType(str, Enum):
"""資源類型"""
DEPLOYMENT = "deployment"
STATEFULSET = "statefulset"
POD = "pod"
SERVICE = "service"
UNKNOWN = "unknown"
@dataclass
class NormalizeResult:
"""正規化結果"""
success: bool
original: str
normalized: str | None
namespace: str | None
resource_type: ResourceType
is_k8s_resource: bool
confidence: float # 0.0 - 1.0
note: str | None = None
# =============================================================================
# Normalization Functions
# =============================================================================
def is_valid_k8s_name(name: str) -> bool:
"""
檢查是否為有效的 K8s 資源名稱 (RFC 1123)
Args:
name: 資源名稱
Returns:
bool: 是否有效
"""
if not name:
return False
if len(name) > K8S_NAME_MAX_LENGTH:
return False
return bool(K8S_NAME_PATTERN.match(name))
def strip_url_scheme(raw: str) -> str:
"""
移除 URL scheme 和路徑
Examples:
https://api.awoooi.wooo.work/v1/health → api.awoooi.wooo.work
http://192.168.0.188:8000 → 192.168.0.188
"""
# 移除 scheme
result = re.sub(r"^https?://", "", raw)
# 移除 port
result = re.sub(r":\d+.*$", "", result)
# 移除路徑
result = result.split("/")[0]
return result.strip()
def to_k8s_safe_name(raw: str) -> str:
"""
轉換為 K8s 安全名稱
Examples:
api.awoooi.wooo.work → api-awoooi-wooo-work
My_Service_Name → my-service-name
"""
# 轉小寫
result = raw.lower()
# 替換不允許的字元為連字號
result = re.sub(r"[^a-z0-9-]", "-", result)
# 合併多個連字號
result = re.sub(r"-+", "-", result)
# 移除開頭和結尾的連字號
result = result.strip("-")
# 截斷到最大長度
if len(result) > K8S_NAME_MAX_LENGTH:
result = result[:K8S_NAME_MAX_LENGTH].rstrip("-")
return result
def normalize_resource_name(raw: str, default_namespace: str = "awoooi-prod") -> NormalizeResult:
"""
正規化資源名稱 - 主入口函數
流程:
1. 檢查是否為非 K8s 資源
2. 移除 URL scheme
3. 查詢靜態映射表
4. 轉換為 K8s 安全名稱
5. 驗證格式
Args:
raw: 原始資源名稱 (可能是 URL、域名、或 K8s 名稱)
default_namespace: 預設命名空間
Returns:
NormalizeResult: 正規化結果
"""
if not raw:
return NormalizeResult(
success=False,
original=raw,
normalized=None,
namespace=None,
resource_type=ResourceType.UNKNOWN,
is_k8s_resource=False,
confidence=0.0,
note="Empty resource name",
)
# Step 1: 檢查非 K8s 資源
stripped = strip_url_scheme(raw)
if stripped in NON_K8S_HOSTS or raw in NON_K8S_HOSTS:
logger.info(
"resource_is_non_k8s",
original=raw,
stripped=stripped,
)
return NormalizeResult(
success=True,
original=raw,
normalized=stripped,
namespace=None,
resource_type=ResourceType.UNKNOWN,
is_k8s_resource=False,
confidence=1.0,
note="Non-K8s host (VM/Container)",
)
# Step 2: 查詢靜態映射表
lookup_key = stripped.lower()
if lookup_key in RESOURCE_MAPPING:
deployment, namespace = RESOURCE_MAPPING[lookup_key]
logger.info(
"resource_mapped_from_table",
original=raw,
deployment=deployment,
namespace=namespace,
)
return NormalizeResult(
success=True,
original=raw,
normalized=deployment,
namespace=namespace,
resource_type=ResourceType.DEPLOYMENT,
is_k8s_resource=True,
confidence=1.0,
note="Mapped from static table",
)
# Step 3: 檢查是否已經是有效的 K8s 名稱
if is_valid_k8s_name(raw):
logger.info(
"resource_already_valid",
original=raw,
)
return NormalizeResult(
success=True,
original=raw,
normalized=raw,
namespace=default_namespace,
resource_type=ResourceType.DEPLOYMENT,
is_k8s_resource=True,
confidence=0.0, # 🔴 規則驗證,非 AI
note="Already valid K8s name",
)
# Step 4: 嘗試轉換
converted = to_k8s_safe_name(stripped)
if is_valid_k8s_name(converted):
logger.info(
"resource_converted",
original=raw,
converted=converted,
)
return NormalizeResult(
success=True,
original=raw,
normalized=converted,
namespace=default_namespace,
resource_type=ResourceType.DEPLOYMENT,
is_k8s_resource=True,
confidence=0.0, # 🔴 規則轉換,非 AI
note=f"Converted from '{raw}' (requires validation)",
)
# Step 5: 無法處理
logger.warning(
"resource_normalization_failed",
original=raw,
attempted=converted,
)
return NormalizeResult(
success=False,
original=raw,
normalized=None,
namespace=None,
resource_type=ResourceType.UNKNOWN,
is_k8s_resource=False,
confidence=0.0,
note=f"Cannot normalize '{raw}' to valid K8s name",
)
def extract_resource_hints(raw: str) -> list[str]:
"""
從原始名稱提取可能的資源關鍵字
用於模糊匹配時的候選生成
Examples:
https://api.awoooi.wooo.work → ["api", "awoooi", "wooo", "work"]
prod-docker-188 → ["prod", "docker", "188"]
"""
stripped = strip_url_scheme(raw)
# 分割所有非字母數字字元
parts = re.split(r"[^a-z0-9]+", stripped.lower())
# 過濾空字串和太短的詞
return [p for p in parts if len(p) >= 2]