Enforce readiness tiers and reconcile blocked workplans
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0e75a-fc5c-7913-9dba-9846210c766d
This commit is contained in:
parent
370e1f84c7
commit
36445ae679
14 changed files with 652 additions and 39 deletions
|
|
@ -8,18 +8,20 @@ and records metadata-only evidence.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import getpass
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import UTC, datetime
|
||||
from datetime import UTC, date, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
from ops_mason.plan import ConstructionPlan
|
||||
from ops_mason.readiness import inspect_readiness
|
||||
|
||||
|
||||
class PlaneError(RuntimeError):
|
||||
|
|
@ -93,6 +95,7 @@ class PlaneBundle:
|
|||
dependencies: tuple[Dependency, ...]
|
||||
evidence_path: Path
|
||||
documents: tuple[dict[str, Any], ...]
|
||||
readiness: dict[str, Any] | None = None
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: str | Path) -> "PlaneBundle":
|
||||
|
|
@ -172,6 +175,7 @@ class PlaneBundle:
|
|||
dependencies=dependencies,
|
||||
evidence_path=local_path(str(raw["evidence_path"])),
|
||||
documents=tuple(documents),
|
||||
readiness=raw.get("readiness"),
|
||||
)
|
||||
bundle.validate()
|
||||
return bundle
|
||||
|
|
@ -185,6 +189,12 @@ class PlaneBundle:
|
|||
return digest.hexdigest()
|
||||
|
||||
def validate(self) -> None:
|
||||
if self.readiness is not None and (
|
||||
not isinstance(self.readiness, dict)
|
||||
or not isinstance(self.readiness.get("target"), dict)
|
||||
or not isinstance(self.readiness.get("source"), dict)
|
||||
):
|
||||
raise PlaneError("readiness needs target and source mappings")
|
||||
actual_refs = tuple(_document_ref(doc, self.allowed_objects) for doc in self.documents)
|
||||
if len(set(actual_refs)) != len(actual_refs):
|
||||
raise PlaneRefused("bundle contains duplicate Kubernetes object identities")
|
||||
|
|
@ -233,6 +243,7 @@ class PlaneBundle:
|
|||
"source_revision": self.source_revision,
|
||||
"implementation_revision": self.implementation_revision,
|
||||
"expected_context": self.expected_context,
|
||||
"readiness": self.readiness,
|
||||
"objects": [asdict(ref) | {"display": ref.display} for ref in self.allowed_objects],
|
||||
"forbidden_kinds": sorted(self.forbidden_kinds),
|
||||
"plan_id": self.plan().id,
|
||||
|
|
@ -363,7 +374,10 @@ def _check_inputs_clean(bundle: PlaneBundle, runner: Runner) -> None:
|
|||
raise PlaneRefused("repository must be committed and clean before Kubernetes mutation")
|
||||
|
||||
|
||||
def preflight(bundle: PlaneBundle, runner: Runner = subprocess_runner) -> dict[str, Any]:
|
||||
def preflight(
|
||||
bundle: PlaneBundle, runner: Runner = subprocess_runner, *,
|
||||
readiness_repo: Path | None = None, activation: str = "APPROVED", today: date | None = None,
|
||||
) -> dict[str, Any]:
|
||||
context = _run(runner, ["kubectl", "config", "current-context"]).stdout.strip()
|
||||
if context != bundle.expected_context:
|
||||
raise PlaneRefused(
|
||||
|
|
@ -464,6 +478,7 @@ def preflight(bundle: PlaneBundle, runner: Runner = subprocess_runner) -> dict[s
|
|||
if str(item.path.relative_to(bundle.repo_root)) not in server_validated
|
||||
],
|
||||
"dependencies": dependency_evidence,
|
||||
"readiness": inspect_readiness(bundle, runner, readiness_repo, activation, today),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -489,9 +504,9 @@ def verify(bundle: PlaneBundle, runner: Runner = subprocess_runner) -> dict[str,
|
|||
for resource in ("pods", "secrets"):
|
||||
result = _run(
|
||||
runner,
|
||||
["kubectl", "-n", bundle.expected_namespace, "get", resource, "-o", "json"],
|
||||
["kubectl", "-n", bundle.expected_namespace, "get", resource, "-o", "name"],
|
||||
)
|
||||
count = len(json.loads(result.stdout).get("items", []))
|
||||
count = len(result.stdout.splitlines())
|
||||
if count:
|
||||
raise PlaneError(
|
||||
f"negative-scope check failed: {count} {resource} exist in "
|
||||
|
|
@ -538,6 +553,10 @@ def apply(
|
|||
confirm_plan_id: str,
|
||||
expected_digest: str,
|
||||
runner: Runner = subprocess_runner,
|
||||
readiness_repo: Path | None = None,
|
||||
activation: str = "APPROVED",
|
||||
break_glass_reason: str | None = None,
|
||||
today: date | None = None,
|
||||
) -> dict[str, Any]:
|
||||
plan = bundle.plan()
|
||||
if not plan.is_approved():
|
||||
|
|
@ -552,8 +571,17 @@ def apply(
|
|||
raise PlaneRefused(
|
||||
f"bundle digest confirmation mismatch: expected {bundle.digest}"
|
||||
)
|
||||
if activation not in {"APPROVED", "BREAK_GLASS"}:
|
||||
raise PlaneRefused("unknown activation")
|
||||
if activation == "BREAK_GLASS" and not (break_glass_reason or "").strip():
|
||||
raise PlaneRefused("BREAK_GLASS requires a non-empty reason")
|
||||
_check_inputs_clean(bundle, runner)
|
||||
before = preflight(bundle, runner)
|
||||
readiness = inspect_readiness(bundle, runner, readiness_repo, activation, today)
|
||||
if not readiness["direct_apply_allowed"]:
|
||||
raise PlaneRefused(f"production tier requires GitOps or BREAK_GLASS: {readiness['reason']}")
|
||||
before = preflight(bundle, runner, readiness_repo=readiness_repo, activation=activation, today=today)
|
||||
if not before["readiness"]["direct_apply_allowed"]:
|
||||
raise PlaneRefused("readiness changed during preflight; refusing mutation")
|
||||
|
||||
server_validated = list(before["server_validated_manifests"])
|
||||
persisted: list[str] = []
|
||||
|
|
@ -607,6 +635,14 @@ def apply(
|
|||
"server_validated_manifests": server_validated,
|
||||
"persisted_manifests": persisted,
|
||||
},
|
||||
"readiness": before["readiness"],
|
||||
"activation": activation,
|
||||
"break_glass": {
|
||||
"reason": break_glass_reason.strip(),
|
||||
"actor": getpass.getuser(),
|
||||
"recorded_at": datetime.now(UTC).isoformat(),
|
||||
"follow_up": "Commit the same change to the manifest repository that ArgoCD reconciles.",
|
||||
} if activation == "BREAK_GLASS" else None,
|
||||
"preflight": before,
|
||||
"verification": verified,
|
||||
"rollback": rollback_plan(bundle),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue