Complete renamed image source and verify bounded consumer migration readiness
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
This commit is contained in:
parent
dd15d8cf3f
commit
16ca97b277
13 changed files with 455 additions and 19 deletions
37
tools/publish-runtime-images.sh
Normal file
37
tools/publish-runtime-images.sh
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#!/usr/bin/env bash
|
||||
# Uses the enclosing Forge runner's existing registry login. No credential reads.
|
||||
set -euo pipefail
|
||||
revision=${1:?full source revision required}
|
||||
mode=${2:-legacy}
|
||||
context=${3:-.}
|
||||
[[ "$revision" =~ ^[a-f0-9]{40}$ ]] || { echo 'full immutable source revision required' >&2; exit 2; }
|
||||
[[ "$mode" == legacy || "$mode" == paired ]] || { echo 'unknown publication mode' >&2; exit 2; }
|
||||
short=${revision:0:7}
|
||||
registry=forgejo.coulomb.social
|
||||
legacy="$registry/coulomb/flex-auth"
|
||||
renamed="$registry/coulomb/access-engine"
|
||||
docker build -f "$context/Containerfile" --label "org.opencontainers.image.revision=$revision" -t "$legacy:main-$short" "$context"
|
||||
images=("$legacy")
|
||||
if [[ "$mode" == paired ]]; then
|
||||
docker tag "$legacy:main-$short" "$renamed:main-$short"
|
||||
images+=("$renamed")
|
||||
fi
|
||||
for image in "${images[@]}"; do
|
||||
docker tag "$image:main-$short" "$image:latest"
|
||||
docker push "$image:main-$short"
|
||||
docker push "$image:latest"
|
||||
done
|
||||
# Pull each published immutable tag through the runner's existing registry login.
|
||||
# RepoDigests then records the registry's top-level digest, including OCI indexes
|
||||
# and attestations; comparing a local image/config ID would not prove publication.
|
||||
digests=()
|
||||
for image in "${images[@]}"; do
|
||||
docker pull "$image:main-$short"
|
||||
raw=$(docker image inspect --format '{{json .RepoDigests}}' "$image:main-$short")
|
||||
digest=$(printf '%s' "$raw" | python3 -c 'import json,re,sys; x=json.load(sys.stdin); assert isinstance(x,list); matches=[v.removeprefix(sys.argv[1]+"@") for v in x if isinstance(v,str) and v.startswith(sys.argv[1]+"@")]; assert len(matches)==1 and re.fullmatch(r"sha256:[a-f0-9]{64}",matches[0]); print(matches[0])' "$image")
|
||||
digests+=("$digest")
|
||||
done
|
||||
if [[ "$mode" == paired && "${digests[0]}" != "${digests[1]}" ]]; then
|
||||
echo 'published registry manifest digests differ' >&2; exit 1
|
||||
fi
|
||||
printf 'publication mode=%s revision=%s manifest_digest=%s verified_repositories=%s\n' "$mode" "$revision" "${digests[0]}" "${#images[@]}"
|
||||
139
tools/verify_runtime_consumer_migration.py
Normal file
139
tools/verify_runtime_consumer_migration.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Deterministic T06 source classification. No credentials, network or mutation.
|
||||
|
||||
This reports source preparation, NEVER native readiness or workplan closure.
|
||||
Owner-authenticated live allow/deny, predecessor and no-traffic/soak evidence
|
||||
must be independently collected and reviewed by the migration coordinator.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import ast
|
||||
import tomllib
|
||||
import yaml
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Consumer:
|
||||
owner: str
|
||||
repos: tuple
|
||||
pins: tuple
|
||||
paths: tuple
|
||||
network_policy: bool
|
||||
|
||||
CONSUMERS = (
|
||||
Consumer("ops-warden", ("ops-warden", "ops-bridge"), ("ops-warden", "ops-warden-fetch"), ("ops-warden/examples/warden.production.example.yaml",), False),
|
||||
Consumer("secrets-engine", ("secrets-engine",), ("secrets-engine",), ("secrets-engine/tools/exercise_approval_identity.py",), False),
|
||||
Consumer("user-engine", ("user-engine", "net-kingdom"), ("user-engine", "application-readback"), ("net-kingdom/sso-mfa/k8s/user-engine/runtime.yaml",), True),
|
||||
Consumer("tenant-engine", ("tenant-engine",), ("tenant-engine",), ("tenant-engine/deploy/base/tenant-engine.yaml", "tenant-engine/railiance/app.toml"), True),
|
||||
Consumer("informed-decision", ("informed-decision",), ("informed-decision-lifecycle", "informed-decision-sitting", "informed-decision-t03"), ("informed-decision/deploy/sitting-admission.json", "informed-decision/deploy/t03-admission.json", "informed-decision/deploy/lifecycle/admission.candidate.json", "informed-decision/deploy/lifecycle/service.candidate.json", "informed-decision/tools/render_deployment.py"), True),
|
||||
)
|
||||
|
||||
class MigrationReadiness:
|
||||
def __init__(self, estate_root):
|
||||
self.root = Path(estate_root).resolve()
|
||||
|
||||
def classify(self, consumer):
|
||||
sources = []; texts = []; documents = []
|
||||
for relative in consumer.paths:
|
||||
path = self.root / relative
|
||||
# Never follow a redirected public path into a secret/private tree.
|
||||
if path.is_symlink() or (path.exists() and not path.resolve().is_relative_to(self.root)):
|
||||
raise ValueError("public source path escapes bounded estate root")
|
||||
if not path.is_file():
|
||||
sources.append({"path": relative, "state": "missing"}); continue
|
||||
raw = path.read_bytes(); text = raw.decode()
|
||||
source = {"path": relative, "state": "present", "sha256": hashlib.sha256(raw).hexdigest()}
|
||||
try:
|
||||
if path.suffix == ".py":
|
||||
# Literal configuration strings only, never execute source.
|
||||
values = [n.value for n in ast.walk(ast.parse(text)) if isinstance(n, ast.Constant) and isinstance(n.value, str)]
|
||||
texts.extend(values)
|
||||
for value in values:
|
||||
try:
|
||||
documents.extend(doc for doc in yaml.safe_load_all(value) if isinstance(doc, (dict, list)))
|
||||
except yaml.YAMLError:
|
||||
pass
|
||||
elif path.suffix == ".toml":
|
||||
doc = tomllib.loads(text); documents.append(doc)
|
||||
texts.append(json.dumps(doc, sort_keys=True))
|
||||
else:
|
||||
# JSON is a YAML subset; parse documents to exclude comments.
|
||||
docs = [doc for doc in yaml.safe_load_all(text) if doc is not None]
|
||||
documents.extend(docs)
|
||||
texts.extend(json.dumps(doc, sort_keys=True) for doc in docs)
|
||||
except (ValueError, SyntaxError, yaml.YAMLError):
|
||||
source["state"] = "invalid"
|
||||
sources.append(source)
|
||||
text = "\n".join(texts)
|
||||
old_urls = []; new_urls = []
|
||||
endpoints = []
|
||||
for pin in consumer.pins:
|
||||
old = f"flex-auth-{pin}.flex-auth.svc"
|
||||
new = f"access-engine-{pin}.access-engine.svc"
|
||||
observed = []
|
||||
for value in re.findall(r"https?://[^\s\"'<>]+", text):
|
||||
try:
|
||||
observed.append(urlparse(value.rstrip(".,;")))
|
||||
except ValueError:
|
||||
continue # templates/malformed URLs are not endpoint evidence
|
||||
def has_endpoint(host):
|
||||
for value in observed:
|
||||
try:
|
||||
if value.hostname in (host, host + ".cluster.local") and value.port == 8080:
|
||||
return True
|
||||
except ValueError:
|
||||
continue
|
||||
return False
|
||||
if has_endpoint(old): old_urls.append(pin)
|
||||
if has_endpoint(new): new_urls.append(pin)
|
||||
endpoints.append({"pin": pin, "old_origin": f"http://{old}.cluster.local:8080", "new_origin": f"http://{new}.cluster.local:8080", "audience": "access-engine"})
|
||||
# Only runtime audience and namespace-selector literals count. Env names,
|
||||
# caller mount paths, prose and historical package names do not.
|
||||
audiences = set(); namespaces = set()
|
||||
def collect(value):
|
||||
if isinstance(value, dict):
|
||||
audience = value.get("audience")
|
||||
if isinstance(audience, str): audiences.add(audience)
|
||||
if value.get("kind") == "NetworkPolicy":
|
||||
spec = value.get("spec", {})
|
||||
for rule in spec.get("egress", []) if isinstance(spec, dict) else []:
|
||||
if not isinstance(rule, dict): continue
|
||||
for peer in rule.get("to", []):
|
||||
if not isinstance(peer, dict): continue
|
||||
selector = peer.get("namespaceSelector", {})
|
||||
labels = selector.get("matchLabels", {}) if isinstance(selector, dict) else {}
|
||||
if isinstance(labels, dict) and isinstance(labels.get("kubernetes.io/metadata.name"), str):
|
||||
namespaces.add(labels["kubernetes.io/metadata.name"])
|
||||
for child in value.values(): collect(child)
|
||||
elif isinstance(value, list):
|
||||
for child in value: collect(child)
|
||||
for doc in documents: collect(doc)
|
||||
audience_old = "flex-auth" in audiences
|
||||
audience_new = "access-engine" in audiences
|
||||
egress_old = "flex-auth" in namespaces
|
||||
egress_new = "access-engine" in namespaces
|
||||
reasons = []
|
||||
if any(s["state"] != "present" for s in sources): reasons.append("inventory source files missing/invalid; owner must reconcile")
|
||||
if len(new_urls) != len(consumer.pins): reasons.append("not every new pin endpoint appears in bounded public source")
|
||||
if not audience_new: reasons.append("new projected-token audience not observed in public source")
|
||||
if consumer.network_policy and not egress_new: reasons.append("new namespace-selector egress not observed; add before switching")
|
||||
if old_urls and consumer.network_policy and egress_new and not egress_old: reasons.append("old endpoint remains but old namespace-selector removed; unsafe migration order")
|
||||
if reasons: phase = "NEEDS_OWNER_SOURCE_WORK"
|
||||
elif old_urls or audience_old: phase = "SOURCE_DUAL_PREPARATION_REVIEW_REQUIRED"
|
||||
else: phase = "SOURCE_SWITCHED_NATIVE_PROOF_REQUIRED"
|
||||
return {"owner": consumer.owner, "source_owners": list(consumer.repos), "phase": phase, "native_ready": False, "sources": sources, "old_endpoint_pins": old_urls, "new_endpoint_pins": new_urls, "audience": {"old": audience_old, "new": audience_new}, "egress_namespace": {"old": egress_old, "new": egress_new}, "reasons": reasons, "handoff": {"endpoints": endpoints, "order": ["verify T02/T05 and ordered predecessor live proof", "owner adds new egress while retaining old", "owner switches URL and projected audience for one consumer", "prove live allow and deny against every new pin", "observe old-twin logs with bounded no-traffic/soak evidence", "owner removes old egress only after evidence review"], "required_live_evidence": ["owner identity and exact deployed revision", "old/new image digest and package correspondence", "authenticated allow and deny exact decision bindings", "old-twin log observation interval and no-traffic results; silence/source strings alone are insufficient"], "special": "ops-warden operator config and ops-bridge ClusterIP tunnels require owner evidence; no new self resource.system grant may be inferred from policy routing lane" if consumer.owner == "ops-warden" else "CLI/operator runtime config requires owner evidence" if consumer.owner == "secrets-engine" else "public source does not establish live deployment"}}
|
||||
|
||||
def report(self):
|
||||
return {"schema": "runtime-consumer-migration-source-v1", "workplan": "FLEX-WP-0036", "task": "FLEX-WP-0036-T06", "scope": "deterministic public-source readiness classification, no native admission", "dependency_state": "T02/T05 and ordered live proof require coordinator verification; this tool cannot assert completion", "consumers": [self.classify(c) for c in CONSUMERS]}
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--estate-root", type=Path, default=Path(__file__).resolve().parents[2])
|
||||
args = parser.parse_args()
|
||||
print(json.dumps(MigrationReadiness(args.estate_root).report(), indent=2, sort_keys=True))
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue