407 lines
14 KiB
YAML
407 lines
14 KiB
YAML
# 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: |
|
|
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>IT WORKS</title>
|
|
</head>
|
|
<body>
|
|
<main id="app"></main>
|
|
<script type="module" src="/src/main.js"></script>
|
|
</body>
|
|
</html>
|
|
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, "<").replace(/>/g, ">");
|
|
|
|
const tag = (text, on = false) =>
|
|
`<span class="tag${on ? " on" : ""}">${esc(text)}</span>`;
|
|
|
|
function items(list, activeKey) {
|
|
if (!list?.length) return `<li><span class="summary">nothing listed</span></li>`;
|
|
return list
|
|
.map((it) => {
|
|
const tags = [
|
|
it.standalone ? tag("standalone") : "",
|
|
it.state ? tag(it.state) : "",
|
|
activeKey && it[activeKey] ? tag("active", true) : "",
|
|
].join("");
|
|
return `<li><span class="name">${esc(it.name ?? "?")}</span>
|
|
<span class="summary">${esc(it.summary ?? "")}</span>${tags}</li>`;
|
|
})
|
|
.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
|
|
? `<p class="mock">mocked — no cluster was queried; these are canned values</p>`
|
|
: "";
|
|
const meta = [m.context, m.k8s, m.profile ? `profile ${m.profile}` : "",
|
|
m.nodes ? `${m.nodes} node${m.nodes > 1 ? "s" : ""}` : ""]
|
|
.filter(Boolean).join(" · ");
|
|
|
|
return `
|
|
<h2>Cluster${c.mocked ? " (mocked)" : ""}</h2>
|
|
${banner}
|
|
${meta ? `<p class="sub">${esc(meta)}</p>` : ""}
|
|
<ul>${items(c.workloads)}</ul>
|
|
<h2>Services (${c.services?.length ?? 0})</h2>
|
|
<ul>${items(c.services)}</ul>`;
|
|
}
|
|
|
|
function render(b, name, cluster) {
|
|
const meta = b.bundle ?? {};
|
|
const next = (b.next ?? []).map((n) => `<li>${esc(n)}</li>`).join("");
|
|
return `
|
|
<h1><span class="ok">IT WORKS</span> — ${esc(name || meta.name || "rig")}</h1>
|
|
<p class="sub">${esc(meta.description ?? "")}</p>
|
|
|
|
<h2>Tools (${b.tools?.length ?? 0})</h2>
|
|
<ul>${items(b.tools)}</ul>
|
|
|
|
<h2>Rigs (${b.rigs?.length ?? 0})</h2>
|
|
<ul>${items(b.rigs, "active")}</ul>
|
|
|
|
${clusterSection(cluster)}
|
|
|
|
${next ? `<div class="next"><ul>${next}</ul></div>` : ""}`;
|
|
}
|
|
|
|
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 = `<h1 class="err">bundle unavailable</h1>
|
|
<p class="sub">${esc(err.message)}</p>`;
|
|
});
|
|
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 <your-namespace> -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
|