新增了包括密钥加解密、配置健康检查、原子写入、审计、解析器以及 scrubber 在内的完整 secrets 模块,实现了明文密钥检测替换、密钥引用解析和配置安全校验能力
138 lines
4.7 KiB
Python
138 lines
4.7 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass, field
|
|
from enum import StrEnum
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AtomicWritePhase(StrEnum):
|
|
PLAN = "plan"
|
|
PROJECT = "project"
|
|
VALIDATE = "validate"
|
|
APPLY = "apply"
|
|
|
|
|
|
@dataclass
|
|
class ConfigFileSnapshot:
|
|
path: str
|
|
raw: str
|
|
config_hash: str
|
|
|
|
@classmethod
|
|
def capture(cls, path: str, raw: str) -> ConfigFileSnapshot:
|
|
config_hash = hashlib.sha256(raw.encode()).hexdigest()[:12]
|
|
return cls(path=path, raw=raw, config_hash=config_hash)
|
|
|
|
|
|
@dataclass
|
|
class AtomicWritePlan:
|
|
phase: AtomicWritePhase = AtomicWritePhase.PLAN
|
|
target_state: dict[str, Any] = field(default_factory=dict)
|
|
changes: list[dict[str, Any]] = field(default_factory=list)
|
|
issues: list[str] = field(default_factory=list)
|
|
warnings: list[str] = field(default_factory=list)
|
|
snapshots: dict[str, ConfigFileSnapshot] = field(default_factory=dict)
|
|
dry_run: bool = True
|
|
|
|
@property
|
|
def ok(self) -> bool:
|
|
return len(self.issues) == 0
|
|
|
|
|
|
@dataclass
|
|
class AtomicWriteResult:
|
|
success: bool
|
|
phase: AtomicWritePhase
|
|
issues: list[str] = field(default_factory=list)
|
|
warnings: list[str] = field(default_factory=list)
|
|
applied_changes: int = 0
|
|
|
|
@classmethod
|
|
def ok(cls, phase: AtomicWritePhase, applied: int = 0, warnings: list[str] | None = None):
|
|
return cls(success=True, phase=phase, applied_changes=applied, warnings=warnings or [])
|
|
|
|
@classmethod
|
|
def failed(cls, phase: AtomicWritePhase, issues: list[str], warnings: list[str] | None = None):
|
|
return cls(success=False, phase=phase, issues=issues, warnings=warnings or [])
|
|
|
|
|
|
async def atomic_write_config(
|
|
plan: AtomicWritePlan,
|
|
apply_fn: Callable[[dict[str, Any]], Any],
|
|
validate_fn: Callable[[dict[str, Any]], list[str]] | None = None,
|
|
capture_fn: Callable[[], dict[str, ConfigFileSnapshot]] | None = None,
|
|
restore_fn: Callable[[dict[str, ConfigFileSnapshot]], Any] | None = None,
|
|
) -> AtomicWriteResult:
|
|
"""Plan → Project → Validate → Apply with snapshot/rollback."""
|
|
|
|
# Phase 1: Plan — generate target state
|
|
plan.phase = AtomicWritePhase.PLAN
|
|
if not plan.target_state:
|
|
return AtomicWriteResult.failed(AtomicWritePhase.PLAN, ["No target state provided"])
|
|
|
|
# Phase 2: Project — simulate changes, capture snapshots
|
|
plan.phase = AtomicWritePhase.PROJECT
|
|
if capture_fn:
|
|
plan.snapshots = capture_fn()
|
|
|
|
# Phase 3: Validate — pre-check target state
|
|
plan.phase = AtomicWritePhase.VALIDATE
|
|
if validate_fn:
|
|
validation_issues = validate_fn(plan.target_state)
|
|
if validation_issues:
|
|
plan.issues.extend(validation_issues)
|
|
return AtomicWriteResult.failed(
|
|
AtomicWritePhase.VALIDATE,
|
|
validation_issues,
|
|
plan.warnings,
|
|
)
|
|
|
|
if plan.dry_run:
|
|
logger.info(
|
|
"AtomicWrite: dry-run complete, planned %d changes, %d issues, %d warnings",
|
|
len(plan.changes),
|
|
len(plan.issues),
|
|
len(plan.warnings),
|
|
)
|
|
return AtomicWriteResult.ok(AtomicWritePhase.VALIDATE, warnings=plan.warnings)
|
|
|
|
# Phase 4: Apply — execute with rollback on failure
|
|
plan.phase = AtomicWritePhase.APPLY
|
|
try:
|
|
await _maybe_await(apply_fn, plan.target_state)
|
|
applied = len(plan.changes)
|
|
logger.info("AtomicWrite: applied %d changes successfully", applied)
|
|
return AtomicWriteResult.ok(AtomicWritePhase.APPLY, applied=applied, warnings=plan.warnings)
|
|
except Exception as e:
|
|
logger.exception("AtomicWrite: apply failed, rolling back")
|
|
apply_issues: list[str] = [f"Apply failed: {e}"]
|
|
if restore_fn and plan.snapshots:
|
|
try:
|
|
await _maybe_await(restore_fn, plan.snapshots)
|
|
logger.info("AtomicWrite: rollback successful")
|
|
except Exception as rollback_err:
|
|
logger.exception("AtomicWrite: rollback failed: %s", rollback_err)
|
|
apply_issues.insert(0, f"Rollback failed: {rollback_err}")
|
|
return AtomicWriteResult.failed(AtomicWritePhase.APPLY, apply_issues, plan.warnings)
|
|
|
|
|
|
async def _maybe_await(fn: Callable, *args, **kwargs) -> Any:
|
|
import inspect
|
|
|
|
result = fn(*args, **kwargs)
|
|
if inspect.isawaitable(result):
|
|
return await result
|
|
return result
|
|
|
|
|
|
def capture_db_snapshot(config: dict[str, Any]) -> ConfigFileSnapshot:
|
|
raw = json.dumps(config, sort_keys=True, default=str, ensure_ascii=False)
|
|
config_hash = hashlib.sha256(raw.encode()).hexdigest()[:12]
|
|
return ConfigFileSnapshot(path="db:channel_config", raw=raw, config_hash=config_hash)
|