🔴 違規修正: 規則匹配/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>
302 lines
8.2 KiB
Python
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]
|