# 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: sample-rig labels: rig.bundle/name: sample-rig --- apiVersion: v1 kind: ConfigMap metadata: name: rig-ui namespace: sample-rig labels: rig.bundle/checksum: "2074194964" data: bundle.json: | { "_comment": "What this bundle contains. Single source of truth — the landing page renders THIS file, so adding an entry here is the only edit needed. Deliberately FLAT: standalone tools and rigs, with none of soleprint's internal hierarchy (no artery/atlas/station layering). Nothing here is sensitive; the real architecture connects separately.", "bundle": { "name": "sample-rig", "description": "Non-sensitive sample bundle. Proves the kind install works and shows what ships.", "sensitive": false }, "tools": [ { "name": "modelgen", "summary": "Generate models from config", "standalone": true }, { "name": "datagen", "summary": "Generate test data from rig-owned generators", "standalone": true }, { "name": "graphgen", "summary": "Generate navigable model graphs", "standalone": true }, { "name": "tester", "summary": "HTTP contract test runner — one suite, any environment", "standalone": true }, { "name": "databrowse", "summary": "SQL data browser", "standalone": true }, { "name": "sbwrapper", "summary": "Sandbox wrapper", "standalone": true } ], "rigs": [ { "name": "sample-rig", "summary": "This bundle — a minimal, copyable environment", "active": true } ], "next": [ "Point MANIFESTS_DIR at the real manifests to connect the actual architecture.", "Real k8s files are versioned separately and are not part of this bundle." ] } index.html: | IT WORKS
main.js: | import "./style.css"; /* The IT WORKS page: renders bundle.json as the list of what shipped. * * Plain vite, no framework — this is a complement to the rig, not part of it, * and it should stay small enough that nobody has to adopt a stack to read it. * * bundle.json is fetched at runtime rather than imported, so the same built app * serves whatever rig it was copied into. Editing the ConfigMap changes the page * without rebuilding. * * Styling is a handful of rules on purpose. The real visual identity lives in * the soleprint UI package; nothing here should grow into a theme. */ const esc = (s) => String(s).replace(/&/g, "&").replace(//g, ">"); const tag = (text, on = false) => `${esc(text)}`; function items(list, activeKey) { if (!list?.length) return `
  • nothing listed
  • `; return list .map((it) => { const tags = [ it.standalone ? tag("standalone") : "", it.state ? tag(it.state) : "", activeKey && it[activeKey] ? tag("active", true) : "", ].join(""); return `
  • ${esc(it.name ?? "?")} ${esc(it.summary ?? "")}${tags}
  • `; }) .join(""); } /* Cluster state, when there is any to show. * * Fetched separately and allowed to fail: the bundle listing is the point, and a * rig with no cluster reachable is a normal state, not an error. Renders nothing * at all when absent. * * When the payload says `mocked`, say so loudly. This exists to demo the UI on a * machine where kind will not run — and a demo that looks live but is not is * worse than one that admits it. */ function clusterSection(c) { if (!c) return ""; const m = c.cluster ?? {}; const banner = c.mocked ? `

    mocked — no cluster was queried; these are canned values

    ` : ""; const meta = [m.context, m.k8s, m.profile ? `profile ${m.profile}` : "", m.nodes ? `${m.nodes} node${m.nodes > 1 ? "s" : ""}` : ""] .filter(Boolean).join(" · "); return `

    Cluster${c.mocked ? " (mocked)" : ""}

    ${banner} ${meta ? `

    ${esc(meta)}

    ` : ""}

    Services (${c.services?.length ?? 0})

    `; } function render(b, name, cluster) { const meta = b.bundle ?? {}; const next = (b.next ?? []).map((n) => `
  • ${esc(n)}
  • `).join(""); return `

    IT WORKS — ${esc(name || meta.name || "rig")}

    ${esc(meta.description ?? "")}

    Tools (${b.tools?.length ?? 0})

    Rigs (${b.rigs?.length ?? 0})

    ${clusterSection(cluster)} ${next ? `` : ""}`; } const app = document.getElementById("app"); const json = (path, required) => fetch(path).then((r) => { if (r.ok) return r.json(); if (required) throw new Error(`${path} -> HTTP ${r.status}`); return null; // optional: absent is a normal state, not an error }).catch((err) => { if (required) throw err; return null; }); Promise.all([json("/bundle.json", true), json("/cluster.mock.json", false)]) // RIG_NAME is injected by vite from the pod env, so two rigs sharing a // cluster are distinguishable even if a copied bundle.json kept its old name. .then(([b, cluster]) => { app.innerHTML = render(b, import.meta.env.VITE_RIG_NAME, cluster); }) .catch((err) => { app.innerHTML = `

    bundle unavailable

    ${esc(err.message)}

    `; }); package.json: | { "name": "rig-ui", "private": true, "version": "0.1.0", "type": "module", "scripts": { "dev": "vite --host 0.0.0.0 --port 5173", "build": "vite build", "preview": "vite preview --host 0.0.0.0 --port 5173" }, "devDependencies": { "vite": "^6" } } style.css: | /* Minimal, self-contained. The real visual identity ships with the soleprint UI package, which is a separate artifact — nothing here should grow into a theme. */ body { margin: 0; padding: 2.5rem 1.5rem; background: #0d0d0f; color: #e8e8f0; font: 14px/1.6 ui-monospace, "JetBrains Mono", Menlo, monospace; } main { max-width: 52rem; margin: 0 auto; } h1 { margin: 0; font-size: 1.6rem; letter-spacing: 0.02em; } h1 .ok { color: #3ecf8e; } h1.err { color: #f06565; } .sub { color: #8888a0; margin: 0.35rem 0 2.25rem; } h2 { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.12em; color: #8888a0; margin: 2rem 0 0.75rem; font-weight: 600; } ul { list-style: none; margin: 0; padding: 0; } li { display: flex; gap: 0.75rem; align-items: baseline; padding: 0.5rem 0.75rem; border: 1px solid #2e2e38; border-radius: 6px; margin-bottom: 0.4rem; background: #16161a; } .name { font-weight: 600; min-width: 9rem; } .summary { color: #8888a0; flex: 1; } .tag { font-size: 0.7rem; padding: 0.1rem 0.45rem; border-radius: 3px; background: #26262f; color: #8888a0; white-space: nowrap; } .tag.on { background: #3ecf8e; color: #0d0d0f; } .next { color: #555568; font-size: 0.8rem; margin-top: 2.5rem; border-top: 1px solid #2e2e38; padding-top: 1rem; } .next li { display: list-item; border: 0; background: none; padding: 0.15rem 0; margin: 0 0 0 1.1rem; list-style: disc; } /* Mocked-data banner. Deliberately loud: this only appears when the cluster payload is canned, and a demo that looks live but is not is worse than one that says so. */ .mock { margin: 0 0 0.75rem; padding: 0.4rem 0.75rem; border: 1px dashed #f5a623; border-radius: 6px; color: #f5a623; font-size: 0.8rem; } vite.config.js: | import { defineConfig } from "vite"; /* Serves on 0.0.0.0 so the pod is reachable through the Service, and allows any * Host header because the address is assigned at runtime (MetalLB locally, a * cloud load balancer on EKS) and is never known at build time. */ export default defineConfig({ server: { host: "0.0.0.0", port: 5173, strictPort: true, allowedHosts: true }, preview: { host: "0.0.0.0", port: 5173, strictPort: true, allowedHosts: true }, }); --- # How to plug the UI into whatever k8s you generated. THIS IS THE WHOLE THING: # one Pod running the vite app, one Service to reach it. # # Optional by design. The UI complements a rig; it is not part of the end # product, and a rig is complete and useful without it. Apply this only when you # want the listing: # # kubectl apply -n -f rig-ui/k8s.yaml # # A bare Pod, not a Deployment — this is a dev-loop convenience, not a workload # to keep alive. If it dies you re-apply it; nothing depends on it staying up. # # The app and bundle.json arrive as a ConfigMap named `rig-ui`, which # ctrl/manifest.py generates from the folder. Nothing is baked into an image, so # editing bundle.json and re-applying is the whole update cycle. apiVersion: v1 kind: Pod metadata: name: rig-ui namespace: sample-rig labels: app: rig-ui spec: containers: - name: vite image: node:22-alpine workingDir: /app # npm install at start: no image to build and no registry to publish to, # which is the point of a minimal plug-in. It needs egress to a registry — # on a locked-down cluster point npm at the internal one, or bake an image # instead. Nothing else here changes if you do. command: ["sh", "-c"] # A ConfigMap mounts flat (keys cannot contain '/'), so the files are # placed into vite's expected layout here. bundle.json goes to public/ # because that is what vite serves at /bundle.json, which is where the # app fetches it. args: - | mkdir -p /app/src /app/public && cp /src/package.json /src/vite.config.js /src/index.html /app/ && cp /src/main.js /src/style.css /app/src/ && cp /src/bundle.json /app/public/ && npm install --no-audit --no-fund && npm run dev env: # Rendered in the heading so two rigs sharing a cluster stay # distinguishable. Set from the namespace by ctrl/manifest.py. - name: VITE_RIG_NAME value: sample-rig ports: - name: http containerPort: 5173 volumeMounts: # /src is read-only from the ConfigMap; the app is copied to a writable # /app because npm install has to create node_modules. - name: rig-ui mountPath: /src - name: app mountPath: /app readinessProbe: httpGet: { path: /, port: 5173 } # npm install decides how long this takes, and it is the slow part. initialDelaySeconds: 15 periodSeconds: 5 failureThreshold: 30 resources: requests: { memory: 128Mi, cpu: 50m } limits: { memory: 512Mi } volumes: - name: rig-ui configMap: name: rig-ui - name: app emptyDir: {} --- apiVersion: v1 kind: Service metadata: name: rig-ui namespace: sample-rig labels: app: rig-ui # No annotations, deliberately — see k8s/app.yaml. The target is EKS but this # stays VPC-agnostic: no subnets, no security groups, no -scheme, no -type. # A bare LoadBalancer is what lets one manifest work on kind and on EKS. spec: type: LoadBalancer selector: app: rig-ui ports: - name: http port: 80 targetPort: 5173 protocol: TCP # Pinned, because a LoadBalancer Service also allocates a NodePort and # this is the only address that works everywhere. # # On WSL the MetalLB address is on a docker bridge INSIDE the Linux VM, # and Windows has no route to it — the page looks broken while the # cluster is perfectly healthy. 30080 is what rig's `hostport` ingress # mode publishes to the host, so this is reachable at # localhost:$HTTP_PORT from a Windows browser with nothing configured. # # Costs nothing elsewhere: MetalLB still assigns an external IP on Linux, # and on EKS the load balancer targets this NodePort anyway. One Service, # no per-environment branch. # # A pinned NodePort is cluster-unique, so two rigs must live in separate # clusters — which is how they are run anyway. nodePort: 30080