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:
tegwick 2026-09-28 11:40:57 +02:00
parent 370e1f84c7
commit 36445ae679
14 changed files with 652 additions and 39 deletions

View file

@ -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),