railiance-platform/scripts/capture-reef-deployables.py

144 lines
4.1 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Capture live reef-railiance deployables for the family-declaration validator.
The validator in railiance-master consumes this file via
`--inventory` and must not grow a cluster dependency of its own.
Substrate namespaces and Knative revision Deployments are omitted so
the coverage check sees workload units, not rail machinery.
"""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
from datetime import datetime, timezone
from typing import Any
DEFAULT_REMOTE = "railiance01"
DEFAULT_REEF = "reef-railiance"
SOURCE = "railiance-platform"
SUBSTRATE_NAMESPACES = {
"cert-manager",
"cert-manager-test",
"cnpg-system",
"default",
"external-secrets",
"knative-serving",
"kube-node-lease",
"kube-public",
"kube-system",
"kourier-system",
"local-path-storage",
}
EPHEMERAL_NAMESPACE_SUFFIXES = ("-drill", "-test")
KNATIVE_REVISION_DEPLOY = re.compile(r".*-\d{5}-deployment$")
RESOURCE_SPECS = (
("Deployment", "deploy"),
("StatefulSet", "sts"),
("Cluster", "cluster.postgresql.cnpg.io"),
("KnativeService", "service.serving.knative.dev"),
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n", 1)[0])
parser.add_argument(
"--remote",
default=DEFAULT_REMOTE,
help=f"SSH host that can reach the reef API (default: {DEFAULT_REMOTE})",
)
parser.add_argument(
"--reef",
default=DEFAULT_REEF,
help=f"Reef identifier recorded in the inventory (default: {DEFAULT_REEF})",
)
parser.add_argument(
"-o",
"--output",
help="Write JSON to this path instead of stdout",
)
return parser.parse_args()
def remote_get(remote: str, resource: str) -> dict[str, Any]:
cmd = [
"ssh",
"-o",
"BatchMode=yes",
remote,
"kubectl",
"get",
resource,
"-A",
"-o",
"json",
]
result = subprocess.run(cmd, check=False, capture_output=True, text=True)
if result.returncode != 0:
stderr = (result.stderr or "").strip()
if "the server doesn't have a resource type" in stderr:
return {"items": []}
raise RuntimeError(f"{' '.join(cmd)} failed ({result.returncode}): {stderr}")
return json.loads(result.stdout)
def skip_namespace(name: str) -> bool:
if name in SUBSTRATE_NAMESPACES:
return True
return any(name.endswith(suffix) for suffix in EPHEMERAL_NAMESPACE_SUFFIXES)
def collect(remote: str) -> list[dict[str, str]]:
seen: set[tuple[str, str, str]] = set()
deployables: list[dict[str, str]] = []
for kind, resource in RESOURCE_SPECS:
payload = remote_get(remote, resource)
for item in payload.get("items") or []:
meta = item.get("metadata") or {}
name = meta.get("name")
namespace = meta.get("namespace") or ""
if not isinstance(name, str) or not name:
continue
if skip_namespace(namespace):
continue
if kind == "Deployment" and KNATIVE_REVISION_DEPLOY.fullmatch(name):
continue
key = (name, namespace, kind)
if key in seen:
continue
seen.add(key)
deployables.append({"name": name, "namespace": namespace, "kind": kind})
deployables.sort(key=lambda row: (row["namespace"], row["name"], row["kind"]))
return deployables
def main() -> int:
args = parse_args()
try:
deployables = collect(args.remote)
except (RuntimeError, json.JSONDecodeError) as exc:
print(f"capture failed: {exc}", file=sys.stderr)
return 1
inventory = {
"source": SOURCE,
"captured_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"reef": args.reef,
"deployables": deployables,
}
text = json.dumps(inventory, indent=2) + "\n"
if args.output:
with open(args.output, "w", encoding="utf-8") as fh:
fh.write(text)
else:
sys.stdout.write(text)
return 0
if __name__ == "__main__":
sys.exit(main())