Files
soleprint/rig/sample-rig/ctrl/manifest.py
2026-08-20 11:24:42 -03:00

142 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""Emit the complete, self-contained deployment for this rig.
python3 ctrl/manifest.py [namespace] > generated/<slug>.yaml
The output is the ARTIFACT. It carries everything — namespace, the vite app and
bundle.json embedded in a ConfigMap, the Pod and the Service — so it applies
unmodified to any cluster:
kubectl apply -f generated/sample-rig.yaml
On kind, MetalLB answers the `type: LoadBalancer` Service. On a real external
cluster the cloud load balancer does. Same file, no edits, no branch — which is
the point: what runs locally is byte-identical to the deployment applied
elsewhere, so local success actually means something.
`ctrl/bundle.sh up` applies this same generated output rather than a separate
code path, so the local convenience wrapper can never drift from the artifact.
Stdlib only, deliberately: this must run on a bare staging workstation before
anything is installed, so it cannot depend on PyYAML or a template engine.
Open seam — not built: everything here derives from bundle.json plus a target
namespace. A Pulumi or Terraform emitter would sit beside this file consuming the
same inputs; nothing above it assumes the artifact is YAML.
"""
import json
import re
import sys
import zlib
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
UI = ROOT / "rig-ui"
# Files embedded into the ConfigMap, mounted read-only at /src in the pod and
# copied into vite's layout at start (see rig-ui/k8s.yaml). Flat on purpose:
# ConfigMap keys cannot contain '/'.
EMBEDDED = {
"bundle.json": ROOT / "bundle.json",
"package.json": UI / "package.json",
"vite.config.js": UI / "vite.config.js",
"index.html": UI / "index.html",
"main.js": UI / "src" / "main.js",
"style.css": UI / "src" / "style.css",
}
def slug(name: str) -> str:
"""Reduce a folder name to a DNS label, matching ctrl/bundle.sh's rule."""
out = re.sub(r"[^a-z0-9-]", "-", name.lower()).strip("-")
return out or "rig-bundle"
def block(text: str, indent: int) -> str:
"""Indent a file's contents for a YAML literal block scalar.
Blank lines are emitted truly empty rather than as whitespace: trailing
spaces on an otherwise blank line are legal YAML but show up as diff noise
in a committed artifact.
"""
pad = " " * indent
return "\n".join(pad + line if line.strip() else "" for line in text.splitlines())
def checksum(parts: list[str]) -> str:
"""Stable content hash of everything embedded, stamped as a label.
A mounted ConfigMap updates in place without restarting anything, so without
a visible change nothing signals that the pod is serving stale content.
"""
return str(zlib.crc32("".join(parts).encode()) & 0xFFFFFFFF)
def build(namespace: str) -> str:
contents = {}
for key, path in EMBEDDED.items():
if not path.exists():
sys.exit(f"missing input: {path}")
contents[key] = path.read_text()
# Fail loudly here rather than shipping an artifact that renders an error.
try:
json.loads(contents["bundle.json"])
except json.JSONDecodeError as exc:
sys.exit(f"bundle.json is not valid JSON: {exc}")
app = (UI / "k8s.yaml").read_text()
app = app.replace("__RIG_NAME__", namespace)
data = "\n".join(
f" {key}: |\n{block(text, 4)}" for key, text in sorted(contents.items())
)
return f"""# GENERATED by ctrl/manifest.py — do not edit.
# Regenerate with: make manifest
#
# Self-contained: applies as-is to any cluster, local kind or external.
# kubectl apply -f this-file.yaml
#
# Namespace carries the identity, so several rigs coexist in one cluster.
apiVersion: v1
kind: Namespace
metadata:
name: {namespace}
labels:
rig.bundle/name: {namespace}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: rig-ui
namespace: {namespace}
labels:
rig.bundle/checksum: "{checksum(list(contents.values()))}"
data:
{data}
---
{_namespaced(app, namespace)}
"""
def _namespaced(doc: str, namespace: str) -> str:
"""Add `namespace:` to each resource so the artifact applies without -n.
rig-ui/k8s.yaml omits it on purpose — applied by hand it should land in
whatever namespace you choose. Pinning it belongs to the generated artifact,
which has to be self-contained.
"""
return re.sub(
r"^(metadata:\n(?:[ \t]+.*\n)*?)([ \t]+)(name: rig-ui)$",
lambda m: f"{m.group(1)}{m.group(2)}{m.group(3)}\n{m.group(2)}namespace: {namespace}",
doc.strip(),
flags=re.MULTILINE,
)
if __name__ == "__main__":
target = sys.argv[1] if len(sys.argv) > 1 else slug(ROOT.name)
sys.stdout.write(build(target))