attemp to develop rig in spr without an actual use case
This commit is contained in:
7
rig/.gitignore
vendored
7
rig/.gitignore
vendored
@@ -11,10 +11,9 @@ ctrl/.env
|
||||
arch/*.dot
|
||||
ctrl/Tiltfile.gen
|
||||
|
||||
# binaries pulled by `make deps-bundle` for the air-gapped wizard image
|
||||
# binaries pulled by `make deps-bundle` for the air-gapped installer image
|
||||
vendor
|
||||
|
||||
# Client rigs are NOT ignored here. A copy is a SIBLING of this directory
|
||||
# (spr/acme-rig), so a rule in this file cannot see it — the rules live in
|
||||
# spr/.gitignore, anchored at spr's root, where `*-rig/` matches the siblings and
|
||||
# `!rig/sample-rig/` keeps the committed stand-in.
|
||||
# (../acme-rig), so a rule in this file cannot see it — the rules live in the
|
||||
# parent repo's .gitignore, anchored at its root, where `*-rig/` matches them.
|
||||
|
||||
@@ -4,9 +4,8 @@ The README says the prerequisite is Docker and nothing else. This is what that
|
||||
actually looks like end to end: a bare Linux box, and a new project running under
|
||||
Tilt at the end of it.
|
||||
|
||||
rig lives inside soleprint, at `spr/rig` — it is soleprint's cluster half, and
|
||||
a client copy is a sibling (`spr/acme-rig`). Paths below are relative to
|
||||
soleprint's checkout.
|
||||
A copy of this directory is a sibling of it, named after the environment it
|
||||
models (`acme-rig`). Paths below are relative to the parent checkout.
|
||||
|
||||
It spans three repos because the work does. **rig** prepares the machine — the
|
||||
pinned toolchain, the cluster, the port arithmetic. **all** owns the shape a
|
||||
@@ -50,7 +49,7 @@ isn't.
|
||||
## Read the docs before installing anything
|
||||
|
||||
```bash
|
||||
cd spr/rig
|
||||
cd rig
|
||||
make docs
|
||||
```
|
||||
|
||||
@@ -67,11 +66,11 @@ persists; ctrl-c ends it.
|
||||
## Ask what is wrong with this machine
|
||||
|
||||
```bash
|
||||
make station
|
||||
make check
|
||||
cp ctrl/.env.example ctrl/.env
|
||||
```
|
||||
|
||||
`station.sh` reports and instructs, and fixes nothing. It runs bare rather than
|
||||
`check.sh` reports and instructs, and fixes nothing. It runs bare rather than
|
||||
in a container because host detection only ever reads `/proc` and `/etc` — no
|
||||
dependency beyond coreutils.
|
||||
|
||||
@@ -80,7 +79,7 @@ port rig binds derives from this directory's name, so the answer is specific to
|
||||
this copy, and a clash here surfaces as an opaque `failed to bind host port` in
|
||||
the middle of cluster creation if you skip it.
|
||||
|
||||
Copy the `.env` even though station only warns about it. It is gitignored, it is
|
||||
Copy the `.env` even though the check only warns about it. It is gitignored, it is
|
||||
where a machine-local override goes, and `ports.sh persist` expects it to exist.
|
||||
|
||||
|
||||
@@ -88,33 +87,33 @@ where a machine-local override goes, and `ports.sh persist` expects it to exist.
|
||||
|
||||
This is the step where "nothing installed" stops being rhetorical.
|
||||
|
||||
`make deps` runs `ctrl/wizard.sh install` directly on the host, and the wizard
|
||||
`make deps` runs `ctrl/deps.sh install` directly on the host, and the installer
|
||||
fetches with `curl`. A stock `debian:trixie-slim` has no curl — detection runs
|
||||
fine, then the first download dies with `curl: command not found` and an exit
|
||||
code of 127. That is the bootstrap paradox `ctrl/Dockerfile.wizard` exists
|
||||
to kill — the wizard carries its own toolchain so the host needs only Docker —
|
||||
code of 127. That is the bootstrap paradox `ctrl/Dockerfile.deps` exists
|
||||
to kill — the installer carries its own toolchain so the host needs only Docker —
|
||||
but building the image and running it are two different things, and only the
|
||||
build has a Makefile target today. **On a genuinely bare machine, run it by
|
||||
hand:**
|
||||
|
||||
```bash
|
||||
make wizard # builds rig-wizard:wizard
|
||||
make deps-image # builds rig-deps:deps
|
||||
mkdir -p ~/.local/bin
|
||||
docker run --rm \
|
||||
-v /:/host:ro \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v "$HOME/.local/bin:/out/bin" \
|
||||
-e HOST_UID="$(id -u)" -e HOST_GID="$(id -g)" \
|
||||
rig-wizard:wizard install dev
|
||||
rig-deps:deps install dev
|
||||
```
|
||||
|
||||
The image name follows the directory, like everything else here: in `spr/rig`
|
||||
it is `rig-wizard`, in a copy called `spr/acme-rig` it is `acme-rig-wizard`. The
|
||||
tag is `wizard` (or `full`, below), not `latest`.
|
||||
The image name follows the directory, like everything else here: in `rig`
|
||||
it is `rig-deps`, in a copy called `acme-rig` it is `acme-rig-deps`. The
|
||||
tag is `deps` (or `full`, below), not `latest`.
|
||||
|
||||
None of the four arguments are guessable, so:
|
||||
|
||||
- **`/:/host:ro`** — the wizard reads the *host's* `/etc/os-release` and
|
||||
- **`/:/host:ro`** — the installer reads the *host's* `/etc/os-release` and
|
||||
`/etc/wsl.conf`, not the container's. `HOST_ROOT=/host` is already baked into
|
||||
the image; this is what it points at. Read-only, and it is the only reason
|
||||
detection inside a container tells you anything about the machine.
|
||||
@@ -122,7 +121,7 @@ None of the four arguments are guessable, so:
|
||||
and how it counts kind clusters already running.
|
||||
- **`/out/bin`** — the image's `OUT_BIN`. Whatever you mount here is where the
|
||||
four binaries land.
|
||||
- **`HOST_UID` / `HOST_GID`** — the wizard runs as root so it can reach that
|
||||
- **`HOST_UID` / `HOST_GID`** — the installer runs as root so it can reach that
|
||||
socket, which means everything it writes into a mounted volume is root-owned
|
||||
and useless to you. These drive the `chown` back. Omit them and the install
|
||||
looks like it worked.
|
||||
@@ -131,18 +130,18 @@ None of the four arguments are guessable, so:
|
||||
tooling — which is the right answer on a managed or corporate-issued machine and
|
||||
is why the split exists.
|
||||
|
||||
Then put them on PATH, which the wizard will remind you about because it cannot
|
||||
Then put them on PATH, which the installer will remind you about because it cannot
|
||||
edit your shell for you:
|
||||
|
||||
```bash
|
||||
export PATH="$HOME/.local/bin:$PATH" # and add the same line to ~/.bashrc
|
||||
```
|
||||
|
||||
If something else on this machine already provides `kubectl`, the wizard says so
|
||||
If something else on this machine already provides `kubectl`, the installer says so
|
||||
by name rather than shadowing it quietly. `OUT_BIN=$PWD/def/bin` installs
|
||||
somewhere private instead.
|
||||
|
||||
**Two variants worth knowing before you need them.** `make wizard full` bakes
|
||||
**Two variants worth knowing before you need them.** `make deps-image full` bakes
|
||||
every pinned binary into the image at build time (`DEPS_SOURCE=baked`), so
|
||||
`docker save` gives you the entire installer as one file to carry into an
|
||||
air-gapped network. And `DEPS_SOURCE=artifactory` with `DEPS_ARTIFACTORY_URL`
|
||||
|
||||
21
rig/Makefile
21
rig/Makefile
@@ -20,7 +20,7 @@ SLUG := $(shell echo '$(notdir $(CURDIR))' | tr '[:upper:]' '[:lower:]' | t
|
||||
CLUSTER := $(or $(shell sed -n 's/^CLUSTER=//p' ctrl/.env 2>/dev/null),$(SLUG))
|
||||
KCTX := --context kind-$(CLUSTER)
|
||||
TILT_PORT := $(shell sed -n 's/^TILT_PORT=//p' ctrl/.env 2>/dev/null)
|
||||
WIZARD := $(SLUG)-wizard
|
||||
DEPSIMG := $(SLUG)-deps
|
||||
|
||||
# Words after the target become the script's subcommand. Make would otherwise
|
||||
# treat them as goals of their own, so each gets a no-op rule.
|
||||
@@ -35,7 +35,7 @@ $(eval $(ARGS):;@:)
|
||||
.PHONY: $(ARGS)
|
||||
endif
|
||||
|
||||
.PHONY: help setup station deps wizard cluster registry addons ports \
|
||||
.PHONY: help setup check mem deps deps-image cluster registry addons ports \
|
||||
newbox dockerhost docs tilt \
|
||||
kind-up kind-down kind-reset tilt-up tilt-down
|
||||
|
||||
@@ -47,16 +47,19 @@ help: ## list targets
|
||||
setup: ## prepare this machine [core] [--share-docker] [--cluster]
|
||||
bash ctrl/setup.sh $(ARGS)
|
||||
|
||||
station: ## is this workstation ready? reports, never fixes
|
||||
bash ctrl/station.sh
|
||||
check: ## is this machine ready? reports, never fixes
|
||||
bash ctrl/check.sh
|
||||
|
||||
mem: ## memory, and any cap holding it [status|backup|restore]
|
||||
bash ctrl/mem.sh $(or $(ARGS),status)
|
||||
|
||||
deps: ## install the toolchain [core|dev] (default dev)
|
||||
bash ctrl/wizard.sh install $(or $(ARGS),dev)
|
||||
bash ctrl/deps.sh install $(or $(ARGS),dev)
|
||||
|
||||
wizard: ## build the installer image [full]
|
||||
docker build -f ctrl/Dockerfile.wizard \
|
||||
--target $(if $(filter full,$(ARGS)),wizard-full,wizard) \
|
||||
-t $(WIZARD):$(if $(filter full,$(ARGS)),full,wizard) .
|
||||
deps-image: ## build the installer image [full]
|
||||
docker build -f ctrl/Dockerfile.deps \
|
||||
--target $(if $(filter full,$(ARGS)),deps-full,deps) \
|
||||
-t $(DEPSIMG):$(if $(filter full,$(ARGS)),full,deps) .
|
||||
|
||||
# ── cluster ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -9,6 +9,46 @@ topology, not the workloads.
|
||||
|
||||
**Docker.** Nothing else — no curl, no jq, no python, no apt repositories.
|
||||
|
||||
### Starting from plain Windows
|
||||
|
||||
Everything here is bash and runs *inside* a Linux shell, so on a Windows machine
|
||||
that means WSL. Nothing in rig installs WSL, and nothing will: `wsl --install`
|
||||
enables Windows features and requires a reboot, which is not something a script
|
||||
should do to a machine on your behalf — and there is no tested undo for it.
|
||||
|
||||
From an elevated PowerShell or Command Prompt, once:
|
||||
|
||||
```powershell
|
||||
wsl --install
|
||||
```
|
||||
|
||||
Then reboot and open the Linux shell it installed.
|
||||
|
||||
**If you cloned this on the Windows side, copy it into WSL before carrying on.**
|
||||
WSL can reach the Windows drives at `/mnt/c`, and working from there mostly
|
||||
functions — slowly — but file watching does not: that filesystem raises no
|
||||
inotify events, so anything watching for edits silently stops seeing them.
|
||||
|
||||
```bash
|
||||
cp -r /mnt/c/Users/<you>/rig ~/rig
|
||||
cd ~/rig
|
||||
```
|
||||
|
||||
`make deps` reports it if you are running from `/mnt/...`. Then carry on below.
|
||||
|
||||
If it fails, the usual causes give unhelpful messages:
|
||||
|
||||
| symptom | cause |
|
||||
| --- | --- |
|
||||
| "the virtual machine could not be started" | virtualization disabled in BIOS/UEFI |
|
||||
| the command is not recognised | Windows build too old — needs 2004 or later |
|
||||
| the install starts, then nothing works | a reboot is still pending |
|
||||
|
||||
Running the scripts from **Git Bash, MSYS or Cygwin does not work** — those look
|
||||
close enough to a Linux shell to get started and then fail without `/proc` or a
|
||||
docker socket. `ctrl/deps.sh` detects that and says so rather than letting you
|
||||
find out the slow way.
|
||||
|
||||
## Read the docs first
|
||||
|
||||
```bash
|
||||
@@ -21,16 +61,43 @@ instructions for everything else. No cluster and no toolchain required.
|
||||
## Then
|
||||
|
||||
```bash
|
||||
make station # report host and config problems; changes nothing
|
||||
make check # report host and config problems; changes nothing
|
||||
make deps # install the toolchain (add `core` on a managed machine)
|
||||
make cluster up # build the cluster for the active profile
|
||||
```
|
||||
|
||||
`make cluster up` also starts this environment's local registry and wires it
|
||||
into the node, so an image built locally is pullable by the cluster without
|
||||
going near docker.io:
|
||||
|
||||
```bash
|
||||
make registry status # prints: endpoint localhost:<port>
|
||||
docker build -t localhost:<port>/app:1 .
|
||||
docker push localhost:<port>/app:1
|
||||
kubectl --context kind-$(basename $PWD) run app --image=localhost:<port>/app:1
|
||||
```
|
||||
|
||||
The port block is derived from the directory name, so two copies of rig never
|
||||
collide:
|
||||
|
||||
```bash
|
||||
make ports show # HTTP / HTTPS / TILT / REGISTRY
|
||||
make cluster list # every cluster on this machine, with memory
|
||||
make cluster free # stop the others if memory is tight
|
||||
make cluster down # remove this cluster and its registry
|
||||
```
|
||||
|
||||
**`make tilt` has nothing to run yet.** The target and its `tilt-up` / `tilt-down`
|
||||
aliases exist so rig answers to the same spelling as every other project here,
|
||||
but rig ships no `Tiltfile` — it builds the estate, it is not itself a service
|
||||
with a dev loop. Add a `ctrl/Tiltfile` and the target works; until then it fails
|
||||
on the missing file, not on anything rig did.
|
||||
|
||||
`make help` lists every target.
|
||||
|
||||
On a machine where Docker really is the only thing installed, `make deps` has
|
||||
nothing to download with — see [BOOTSTRAP.md](BOOTSTRAP.md), which runs the
|
||||
toolchain through the wizard container and carries on to scaffolding and running
|
||||
toolchain through the installer container and carries on to scaffolding and running
|
||||
a new project.
|
||||
|
||||
## One directory is one environment
|
||||
@@ -39,10 +106,10 @@ Copy this directory, rename it, run it. Cluster name, kubectl context, image
|
||||
tags and the host port block all derive from the directory name, so copies never
|
||||
collide and neither one's teardown can touch the other.
|
||||
|
||||
rig lives inside soleprint, at `spr/rig` — it is soleprint's cluster half, and a
|
||||
copy is a **sibling**: `spr/acme-rig`. That is why the ignore rules for client
|
||||
rigs sit in `spr/.gitignore` rather than here; a rule in this directory cannot
|
||||
see a directory beside it.
|
||||
A copy of this directory is a **sibling** of it, named after the environment it
|
||||
models (`acme-rig`). That is why the ignore rules for copies sit in the *parent*
|
||||
repo's `.gitignore` rather than here: a rule in this directory cannot see a
|
||||
directory beside it.
|
||||
|
||||
## Profiles
|
||||
|
||||
@@ -54,7 +121,7 @@ apiserver audits. They live in `ctrl/env.d/`, and the active one is `PROFILE`.
|
||||
| `minimal` | the default. One node, no addons, boots fast. |
|
||||
| `client` | the regulated-estate shape — multi-node, audit on, registry mirror. |
|
||||
| `offline` | air-gapped: everything from a preloaded local registry. |
|
||||
| `data` | the dependency containers a soleprint room asks for. |
|
||||
| `data` | the cabinets an environment asks for. |
|
||||
|
||||
```bash
|
||||
PROFILE=data make cluster up
|
||||
@@ -98,10 +165,10 @@ cluster does.
|
||||
| `redis` | cache and broker |
|
||||
| `airflow` | scheduled pipelines; needs postgres and redis |
|
||||
|
||||
The last three are the cluster half of **soleprint's cabinets**. A room declares
|
||||
what it needs once, in `cfg/<room>/data/cabinets.json`; soleprint's `build.py`
|
||||
composes those services into `docker-compose.yml` for a laptop, and these
|
||||
install the same ones here. The names match on purpose — each cabinet carries a
|
||||
The last three are **cabinets**: a public service dropped in as-is, the upstream
|
||||
image unmodified, reachable at a known address. A cabinet is declared once and
|
||||
installs on either target — a `service.yml` composes it for a laptop, and these
|
||||
install the same one here. The names match on purpose: each cabinet carries a
|
||||
`rig_addon` field pointing at `ctrl/addons/<name>.sh`.
|
||||
|
||||
Plain manifests rather than helm charts, like every other addon: a chart repo is
|
||||
|
||||
@@ -26,10 +26,10 @@ PROFILE=minimal
|
||||
# MANIFESTS_DIR=../platform-manifests/overlays/dev
|
||||
MANIFESTS_DIR=ctrl/k8s/overlays/dev
|
||||
|
||||
# Where the wizard fetches the pinned binaries from.
|
||||
# Where the installer fetches the pinned binaries from.
|
||||
# upstream GitHub releases / dl.k8s.io (needs internet)
|
||||
# artifactory a generic repo — what a locked-down client usually allows
|
||||
# baked already inside the wizard image; no network at all
|
||||
# baked already inside the installer image; no network at all
|
||||
DEPS_SOURCE=upstream
|
||||
DEPS_ARTIFACTORY_URL=
|
||||
|
||||
@@ -43,7 +43,7 @@ REGISTRY_PASSWORD=
|
||||
# Corporate root CA, if Artifactory is fronted by an internal CA (it usually is).
|
||||
# Trust has to reach THREE places and nothing does it for you: the host docker
|
||||
# daemon, every kind node's containerd, and any in-cluster client. registry.sh
|
||||
# handles the first two; station.sh reports when it's configured but not trusted.
|
||||
# handles the first two; check.sh reports when it's configured but not trusted.
|
||||
# Symptom when missing: x509: certificate signed by unknown authority
|
||||
REGISTRY_CA_FILE=
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
# The installation wizard. It does NOT run the cluster — it installs a toolchain
|
||||
# The toolchain installer image. It does NOT run the cluster — it installs a toolchain
|
||||
# onto the host and gets out of the way.
|
||||
#
|
||||
# This exists to kill a bootstrap paradox: a plain bash installer needs curl, jq
|
||||
# and sha256sum to already be present, and a minimal Debian has none of them.
|
||||
# The wizard carries its own toolchain, so the only host prerequisite is Docker.
|
||||
# It carries its own toolchain, so the only host prerequisite is Docker.
|
||||
#
|
||||
# Two variants from one file:
|
||||
# docker build -f ctrl/Dockerfile.wizard --target wizard -t <slug>-wizard .
|
||||
# docker build -f ctrl/Dockerfile.wizard --target wizard-full -t <slug>-wizard:full .
|
||||
# docker build -f ctrl/Dockerfile.deps --target deps -t <slug>-deps .
|
||||
# docker build -f ctrl/Dockerfile.deps --target deps-full -t <slug>-deps:full .
|
||||
#
|
||||
# wizard-full bakes every pinned binary in at build time. `docker save` it and
|
||||
# deps-full bakes every pinned binary in at build time. `docker save` it and
|
||||
# you have the whole installer as one file to carry into an air-gapped network.
|
||||
|
||||
FROM debian:trixie-slim AS wizard
|
||||
FROM debian:trixie-slim AS deps
|
||||
|
||||
# ca-certificates + curl: fetch and verify. graphviz + python3: render diagrams
|
||||
# and validate the arch model, so the host never needs an apt package.
|
||||
@@ -26,21 +26,21 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
|
||||
WORKDIR /work
|
||||
COPY ctrl/versions.env /work/ctrl/versions.env
|
||||
COPY ctrl/wizard.sh /work/ctrl/wizard.sh
|
||||
RUN chmod +x /work/ctrl/wizard.sh
|
||||
COPY ctrl/deps.sh /work/ctrl/deps.sh
|
||||
RUN chmod +x /work/ctrl/deps.sh
|
||||
|
||||
# Defaults; every one is overridable with -e at run time.
|
||||
ENV DEPS_SOURCE=upstream \
|
||||
OUT_BIN=/out/bin \
|
||||
HOST_ROOT=/host
|
||||
|
||||
ENTRYPOINT ["/work/ctrl/wizard.sh"]
|
||||
ENTRYPOINT ["/work/ctrl/deps.sh"]
|
||||
CMD ["install"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# wizard-full — same wizard, binaries baked in, works with no network at all.
|
||||
FROM wizard AS wizard-full
|
||||
RUN /work/ctrl/wizard.sh fetch --to /opt/rig/bin
|
||||
# deps-full — same image, binaries baked in, works with no network at all.
|
||||
FROM deps AS deps-full
|
||||
RUN /work/ctrl/deps.sh fetch --to /opt/rig/bin
|
||||
ENV DEPS_SOURCE=baked \
|
||||
BAKED_BIN=/opt/rig/bin
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Apache Airflow — the cluster half of soleprint's airflow cabinet.
|
||||
# Apache Airflow — the cluster half of the airflow cabinet.
|
||||
#
|
||||
# Airflow needs a metadata database before it will start at all, so this refuses
|
||||
# rather than rolls a pod that will CrashLoopBackOff while the real problem
|
||||
@@ -7,7 +7,7 @@
|
||||
#
|
||||
# One pod on `standalone`, matching the compose cabinet: migration, admin user,
|
||||
# scheduler and webserver in a single container. The official chart's five
|
||||
# deployments model an installation; a room switching this on wants pipelines.
|
||||
# deployments model an installation; switching this on means wanting pipelines.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
# PostgreSQL — the cluster half of soleprint's postgres cabinet.
|
||||
# PostgreSQL — the cluster half of the postgres cabinet.
|
||||
#
|
||||
# A room declares the dependency once, in cfg/<room>/data/cabinets.json. On a
|
||||
# laptop `build.py` composes it into docker-compose.yml; here it becomes a pod,
|
||||
# so the same declaration works either way and nothing has to be remembered
|
||||
# A cabinet is a public service dropped into the environment as-is — the
|
||||
# upstream image, unmodified, reachable at a known address. This is the cluster
|
||||
# half of it; the compose half is a `service.yml` beside a `cabinet.json`. The
|
||||
# declaration is made once and both paths read it, so nothing is remembered
|
||||
# twice.
|
||||
#
|
||||
# Plain manifests rather than a helm chart, matching the other addons: a chart
|
||||
@@ -32,8 +33,8 @@ if $K get secret -n "$NS" postgres >/dev/null 2>&1; then
|
||||
else
|
||||
password=$(head -c 18 /dev/urandom | base64 | tr -d '/+=' | head -c 24)
|
||||
$K create secret generic postgres -n "$NS" \
|
||||
--from-literal=POSTGRES_DB="${POSTGRES_DB:-soleprint}" \
|
||||
--from-literal=POSTGRES_USER="${POSTGRES_USER:-soleprint}" \
|
||||
--from-literal=POSTGRES_DB="${POSTGRES_DB:-postgres}" \
|
||||
--from-literal=POSTGRES_USER="${POSTGRES_USER:-postgres}" \
|
||||
--from-literal=POSTGRES_PASSWORD="$password" >/dev/null
|
||||
echo " generated a password (read it back with the command printed below)"
|
||||
fi
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Redis — the cluster half of soleprint's redis cabinet.
|
||||
# Redis — the cluster half of the redis cabinet.
|
||||
#
|
||||
# Cache, and the broker anything queue-shaped runs on. No persistence: a broker
|
||||
# that loses its queue on restart is the honest local model, and a PVC here buys
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# Station check: is this workstation ready to run rig?
|
||||
# Readiness check: is this machine ready to run rig?
|
||||
#
|
||||
# Reports and instructs; never silently fixes anything. Everything it finds is
|
||||
# either already fine, or something a human has to decide on.
|
||||
#
|
||||
# Runs the wizard's host detection in a container when Docker is the only thing
|
||||
# Runs ctrl/deps.sh host detection in a container when Docker is the only thing
|
||||
# installed, or directly when the toolchain is already present. Then adds the
|
||||
# checks that need this repo's config: profile sanity, CA trust, port clashes.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
WIZARD_IMAGE="${WIZARD_IMAGE:-$(basename "$(cd .. && pwd)")-wizard}"
|
||||
DEPS_IMAGE="${DEPS_IMAGE:-$(basename "$(cd .. && pwd)")-deps}"
|
||||
|
||||
# Host detection. Prefer running it bare — it needs no dependencies beyond
|
||||
# coreutils — and fall back to the container only if this shell can't.
|
||||
bash ./wizard.sh detect
|
||||
bash ./deps.sh detect
|
||||
|
||||
# ── repo-level checks ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# The installation wizard: detect the host, install a pinned toolchain onto it,
|
||||
# then report what it could not do. It never runs the cluster and never mutates
|
||||
# the host outside the directories mounted into it.
|
||||
# Toolchain installer: detect the host, install a pinned toolchain onto it, then
|
||||
# report what it could not do.
|
||||
#
|
||||
# Usage (normally via `make station` / `make deps`, or directly):
|
||||
# wizard.sh detect # report host facts only, change nothing
|
||||
# wizard.sh fetch [core|dev] [--to DIR] # download + verify into DIR
|
||||
# wizard.sh install [core|dev] # detect, fetch, install, report
|
||||
# It never runs the cluster, never uses sudo or apt, and writes only into
|
||||
# $OUT_BIN (default ~/.local/bin). Everything that would touch the host proper —
|
||||
# systemd, inotify limits, .wslconfig, docker group — is REPORTED for a human to
|
||||
# decide on, never performed. That is what makes it safe to run on a machine that
|
||||
# already has a working setup.
|
||||
#
|
||||
# Usage (normally via `make deps`, or directly):
|
||||
# deps.sh detect # report host facts only, change nothing
|
||||
# deps.sh fetch [core|dev] [--to DIR] # download + verify into DIR
|
||||
# deps.sh install [core|dev] # detect, fetch, install, report
|
||||
#
|
||||
# Tiers: 'core' is kubectl + jq (talk to a cluster); 'dev' adds kind and tilt
|
||||
# Default is dev.
|
||||
#
|
||||
# Runs both inside the wizard container and bare on a host. Inside the
|
||||
# Runs both inside the installer container and bare on a host. Inside the
|
||||
# container, host files are read through $HOST_ROOT (mount / as :ro); bare, it
|
||||
# falls back to /.
|
||||
|
||||
@@ -55,6 +60,29 @@ host_file() {
|
||||
|
||||
# ── detect ─────────────────────────────────────────────────────────────────
|
||||
|
||||
# Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and
|
||||
# then fails in a pile of confusing ways: no /proc, no docker socket, none of
|
||||
# the tooling. Detectable, so name it instead.
|
||||
require_linux() {
|
||||
case "$(uname -s)" in
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
cat >&2 <<'EOF'
|
||||
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
||||
|
||||
If WSL is not installed yet, from an elevated PowerShell or Command Prompt:
|
||||
|
||||
wsl --install
|
||||
|
||||
That enables Windows features and needs a reboot, so it is not something this
|
||||
script will do for you. Afterwards, open the Linux shell it installs and run
|
||||
this from there.
|
||||
|
||||
See "Starting from plain Windows" in README.md.
|
||||
EOF
|
||||
exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; }
|
||||
|
||||
detect() {
|
||||
@@ -76,6 +104,7 @@ detect() {
|
||||
fi
|
||||
|
||||
detect_wsl
|
||||
detect_filesystem
|
||||
detect_docker
|
||||
detect_inotify
|
||||
}
|
||||
@@ -115,18 +144,46 @@ detect_wsl() {
|
||||
if [ -n "$wcfg" ] && grep -qE '^\s*memory\s*=' "$wcfg"; then
|
||||
echo " wslconfig memory set: $(grep -E '^\s*memory\s*=' "$wcfg" | tr -d ' ')"
|
||||
else
|
||||
MANUAL+=("Cap/raise the WSL VM memory — in %USERPROFILE%\\.wslconfig on Windows:
|
||||
[wsl2]
|
||||
memory=8GB
|
||||
then from a WINDOWS terminal: wsl --shutdown")
|
||||
MANUAL+=("Cap/raise the WSL VM memory — see what is set versus what booted:
|
||||
make mem status
|
||||
It prints the edit to make and the command to apply it.")
|
||||
fi
|
||||
}
|
||||
|
||||
# Not a path check: /mnt is an ordinary mount point and an ext4 disk mounted
|
||||
# there is perfectly fine. What matters is the filesystem. The Windows drives
|
||||
# arrive as 9p (WSL2) or drvfs (WSL1); network and fuse mounts behave the same
|
||||
# way. None of them deliver inotify events, so anything watching files goes
|
||||
# quiet without saying why.
|
||||
watch_hostile_fs() {
|
||||
local dir="$1" fstype
|
||||
fstype=$(findmnt -no FSTYPE --target "$dir" 2>/dev/null || true)
|
||||
[ -n "$fstype" ] || fstype=$(stat -f -c %T "$dir" 2>/dev/null || true)
|
||||
case "$fstype" in
|
||||
9p|v9fs|drvfs|cifs|smb3|nfs|nfs4|fuse.sshfs|fuseblk) echo "$fstype" ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
detect_filesystem() {
|
||||
local root fstype
|
||||
root=$(cd .. && pwd -P)
|
||||
fstype=$(watch_hostile_fs "$root")
|
||||
if [ -n "$fstype" ]; then
|
||||
echo " ! this directory is on $fstype — file watching will not work"
|
||||
MANUAL+=("Move this onto the local disk. Nothing watching files sees changes
|
||||
on a $fstype mount, and everything else is slower:
|
||||
cp -r \"$root\" ~/ && cd ~/$(basename "$root")")
|
||||
else
|
||||
echo " filesystem $root ($(findmnt -no FSTYPE --target "$root" 2>/dev/null || echo local))"
|
||||
fi
|
||||
}
|
||||
|
||||
detect_docker() {
|
||||
# Reachability of the daemon is the real question, and the CLI is only how
|
||||
# we ask it. Note that when this runs inside the wizard container, Docker
|
||||
# we ask it. Note that when this runs inside the installer container, Docker
|
||||
# necessarily exists on the host — otherwise nothing would be executing —
|
||||
# so a missing CLI in here is a wizard packaging bug, not a host problem.
|
||||
# so a missing CLI in here is an installer packaging bug, not a host problem.
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
if [ -S /var/run/docker.sock ]; then
|
||||
echo " docker socket present (no cli in this context)"
|
||||
@@ -230,7 +287,7 @@ fetch_tgz() {
|
||||
chmod +x "$dest/$name"
|
||||
}
|
||||
|
||||
# The wizard runs as root so it can reach the docker socket, which means
|
||||
# The installer runs as root so it can reach the docker socket, which means
|
||||
# everything it writes into a mounted volume lands root-owned and unusable from
|
||||
# the host. Hand it back to whoever owns the mount point (the host user created
|
||||
# that directory before mounting it).
|
||||
@@ -257,7 +314,13 @@ fix_ownership() {
|
||||
CORE_TOOLS="kubectl jq"
|
||||
# No helm: every addon installs with `kubectl apply -f <url>`, so nothing here
|
||||
# has ever invoked it. Add it back the day something actually needs a chart.
|
||||
DEV_TOOLS="kind tilt"
|
||||
#
|
||||
# ctlptl is 'dev' rather than 'core' for the same reason kind is: core is "talk
|
||||
# to a cluster someone else runs", and ctlptl builds them. It earns its place
|
||||
# because it is what wires a cluster to a local registry — without one, an
|
||||
# unqualified image name resolves to docker.io/library/<name> and there is
|
||||
# nothing structural stopping a push there.
|
||||
DEV_TOOLS="kind tilt ctlptl"
|
||||
|
||||
fetch() {
|
||||
local dest="$OUT_BIN" tier="${TIER:-dev}"
|
||||
@@ -284,7 +347,8 @@ fetch() {
|
||||
fetch_bin jq "$JQ_URL" "$JQ_SHA256" "$dest"
|
||||
if [ "$tier" = "dev" ]; then
|
||||
fetch_bin kind "$KIND_URL" "$KIND_SHA256" "$dest"
|
||||
fetch_tgz tilt "$TILT_URL" "$TILT_SHA256" "$dest" tilt 0
|
||||
fetch_tgz tilt "$TILT_URL" "$TILT_SHA256" "$dest" tilt 0
|
||||
fetch_tgz ctlptl "$CTLPTL_URL" "$CTLPTL_SHA256" "$dest" ctlptl 0
|
||||
fi
|
||||
|
||||
fix_ownership "$dest"
|
||||
@@ -301,7 +365,7 @@ report_manual() {
|
||||
echo "nothing left to do by hand."
|
||||
return
|
||||
fi
|
||||
echo "host actions the wizard cannot perform (${#MANUAL[@]}):"
|
||||
echo "host actions this cannot perform (${#MANUAL[@]}):"
|
||||
echo
|
||||
local n=1
|
||||
for m in "${MANUAL[@]}"; do
|
||||
@@ -373,6 +437,8 @@ install() {
|
||||
|
||||
# ── main ───────────────────────────────────────────────────────────────────
|
||||
|
||||
require_linux
|
||||
|
||||
case "${1:-install}" in
|
||||
detect) detect; report_manual ;;
|
||||
fetch) shift; fetch "$@" ;;
|
||||
@@ -19,7 +19,7 @@ DNS_MODE=hosts
|
||||
# Opt in to the real ports below only when this is the ONLY environment and
|
||||
# nothing else owns :80. They fail to bind otherwise, and docker reports it as an
|
||||
# opaque "failed to bind host port 0.0.0.0:80/tcp: address already in use"
|
||||
# halfway through cluster creation. `make station` checks before you spend the
|
||||
# halfway through cluster creation. `make check` checks before you spend the
|
||||
# time. Uncommenting also means only one environment can exist at a time.
|
||||
# HTTP_PORT=80
|
||||
# HTTPS_PORT=443
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
# data — a cluster with the dependency containers a soleprint room asks for.
|
||||
# data — a cluster with the cabinets an environment asks for.
|
||||
#
|
||||
# The point of this profile is that a room declares what it needs once, in
|
||||
# cfg/<room>/data/cabinets.json, and gets it on either target: `build.py`
|
||||
# composes those services into docker-compose.yml for a laptop, and the addons
|
||||
# below install the same ones here. The names match deliberately —
|
||||
# soleprint/station/cabinets/<name>/cabinet.json carries a `rig_addon` field
|
||||
# pointing at ctrl/addons/<name>.sh.
|
||||
# A cabinet is a public service dropped in as-is — the upstream image,
|
||||
# unmodified, reachable at a known address. It is declared once and installs on
|
||||
# either target: a `service.yml` composes it for a laptop, and the addons below
|
||||
# install the same one here. The names match deliberately — each cabinet.json
|
||||
# carries a `rig_addon` field pointing at ctrl/addons/<name>.sh.
|
||||
#
|
||||
# Everything lands in the `data` namespace (DATA_NAMESPACE to move it), so
|
||||
# `make cluster reset` on the app namespace leaves the databases alone.
|
||||
@@ -30,8 +29,8 @@ DATA_NAMESPACE=data
|
||||
# Postgres identity. The password is not here: postgres.sh generates one on
|
||||
# first install and keeps it across re-runs, so re-running the addon never
|
||||
# rotates the credential out from under whatever is already connected.
|
||||
POSTGRES_DB=soleprint
|
||||
POSTGRES_USER=soleprint
|
||||
POSTGRES_DB=app
|
||||
POSTGRES_USER=app
|
||||
POSTGRES_STORAGE=2Gi
|
||||
|
||||
AIRFLOW_ADMIN_USER=admin
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# offline — air-gapped. Everything comes from a local registry that was loaded
|
||||
# ahead of time; nothing reaches the internet. Pair with the wizard-full image
|
||||
# ahead of time; nothing reaches the internet. Pair with the deps-full image
|
||||
# (DEPS_SOURCE=baked) so the toolchain install is offline too.
|
||||
#
|
||||
# The heavier addons are left out to keep first boot viable.
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
# `ctrl/k8s` — cluster shape, and what runs on it
|
||||
|
||||
Same layout as every other project here (`unt`, `nvi`, `eth`, `mpr`, and
|
||||
soleprint's generated rooms): a kind config, a kustomize `base/`, and an
|
||||
`overlays/dev/` that patches it. See ALL `projects/templates/conventions.md`.
|
||||
Same layout as every other project here: a kind config, a kustomize `base/`,
|
||||
and an `overlays/dev/` that patches it.
|
||||
|
||||
```
|
||||
kind-config*.yaml.tpl the cluster itself — nodes, ports, audit
|
||||
|
||||
@@ -44,7 +44,7 @@ nodes:
|
||||
- role: control-plane
|
||||
image: ${NODE_IMAGE}
|
||||
# hostPath is resolved by the HOST dockerd, so this must be a host path even
|
||||
# when cluster.sh runs inside the wizard container. HOST_WORKDIR says where
|
||||
# when cluster.sh runs inside the installer container. HOST_WORKDIR says where
|
||||
# this rig lives on the host; bare on a host it is just the repo root.
|
||||
extraMounts:
|
||||
- hostPath: ${HOST_WORKDIR}/ctrl/k8s/audit-policy.yaml
|
||||
|
||||
@@ -112,7 +112,7 @@ load_config() {
|
||||
fi
|
||||
|
||||
# Read the shape back out of the YAML rather than trusting a profile to
|
||||
# restate it. station.sh sizes the memory warning on NODES, and cluster.sh
|
||||
# restate it. check.sh sizes the memory warning on NODES, and cluster.sh
|
||||
# prints AUDIT before spending minutes building something that cannot be
|
||||
# changed afterwards — both would mislead if the numbers drifted.
|
||||
NODES=$(grep -c '^ - role:' "$KIND_CONFIG_PATH")
|
||||
@@ -125,7 +125,7 @@ load_config() {
|
||||
# depending on something the caller does not set.
|
||||
#
|
||||
# hostPath entries are resolved by the HOST dockerd, so HOST_WORKDIR must stay a
|
||||
# host path even when this runs inside the wizard container.
|
||||
# host path even when this runs inside the installer container.
|
||||
render_kind_config() {
|
||||
local host_workdir="${HOST_WORKDIR:-$(cd .. && pwd)}"
|
||||
sed -e "s|\${CLUSTER}|${CLUSTER}|g" \
|
||||
|
||||
233
rig/ctrl/mem.sh
Executable file
233
rig/ctrl/mem.sh
Executable file
@@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env bash
|
||||
# What memory this machine has, what is left, and — where there is one — what
|
||||
# cap is holding it there.
|
||||
#
|
||||
# Runs on native Linux and under WSL, because rig is developed on one and used
|
||||
# on the other. The difference is not cosmetic: on WSL the memory you see is a
|
||||
# VM allocation that can be raised, and the commonest failure is raising it
|
||||
# without restarting, so the number on disk and the number in /proc disagree.
|
||||
# On native Linux there is no such cap and pretending otherwise sends you to a
|
||||
# file that does not exist.
|
||||
#
|
||||
# This reports and instructs. It never writes a .wslconfig — applying one costs
|
||||
# a full VM restart that takes every shell, mount and container with it, and
|
||||
# choosing that moment is yours.
|
||||
#
|
||||
# `backup` exists so `restore` has something to read: back up, hand-edit
|
||||
# following the printed instruction, restore if it goes wrong. Both are
|
||||
# WSL-only, because .wslconfig is the only thing here worth backing up.
|
||||
#
|
||||
# Usage: mem.sh status | backup | restore
|
||||
set -euo pipefail
|
||||
|
||||
# Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and
|
||||
# then fails in a pile of confusing ways: no /proc, no docker socket, none of
|
||||
# the tooling. Detectable, so name it instead.
|
||||
require_linux() {
|
||||
case "$(uname -s)" in
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
cat >&2 <<'EOF'
|
||||
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
||||
|
||||
If WSL is not installed yet, from an elevated PowerShell or Command Prompt:
|
||||
|
||||
wsl --install
|
||||
|
||||
That enables Windows features and needs a reboot, so it is not something this
|
||||
script will do for you. Afterwards, open the Linux shell it installs and run
|
||||
this from there.
|
||||
|
||||
See "Starting from plain Windows" in README.md.
|
||||
EOF
|
||||
exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; }
|
||||
|
||||
require_wsl() {
|
||||
if ! is_wsl; then
|
||||
echo "$1 acts on .wslconfig, which only exists under WSL." >&2
|
||||
echo "This is native Linux — there is no VM allocation to save or roll back." >&2
|
||||
echo "Use 'mem.sh status' to see what the machine actually has." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
mb() { echo $(( $(awk "/^$1:/{print \$2}" /proc/meminfo) / 1024 )); }
|
||||
|
||||
# /mnt/c/Users can hold several real accounts — a renamed login leaves the old
|
||||
# directory behind — so picking the first alphabetically is a coin toss. Ask
|
||||
# Windows, then fall back to whichever profile actually owns a config.
|
||||
wslconfig_path() {
|
||||
local profile winpath found
|
||||
profile=$(cmd.exe /c "echo %USERPROFILE%" 2>/dev/null | tr -d "\r\n" || true)
|
||||
case "$profile" in
|
||||
""|*%*) ;;
|
||||
*) winpath=$(wslpath -u "$profile" 2>/dev/null || true)
|
||||
if [ -n "$winpath" ] && [ -d "$winpath" ]; then
|
||||
echo "$winpath/.wslconfig"; return
|
||||
fi ;;
|
||||
esac
|
||||
|
||||
found=$(ls -d /mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true)
|
||||
if [ -n "$found" ]; then echo "$found"; return; fi
|
||||
|
||||
echo "cannot tell which Windows profile owns .wslconfig. Candidates:" >&2
|
||||
ls -d /mnt/c/Users/*/ 2>/dev/null \
|
||||
| grep -viE "/(All Users|Default|Default User|Public)/$" | sed "s/^/ /" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
configured_memory() {
|
||||
[ -r "$1" ] || { echo ""; return; }
|
||||
sed -n 's/^[[:space:]]*memory[[:space:]]*=[[:space:]]*//p' "$1" | tail -1 | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
# "9GB" / "8192MB" / "9G" -> MB, so it can be compared with /proc/meminfo.
|
||||
to_mb() {
|
||||
local v="${1^^}" n
|
||||
n=$(echo "$v" | tr -dc '0-9')
|
||||
[ -n "$n" ] || { echo ""; return; }
|
||||
case "$v" in
|
||||
*GB|*G) echo $(( n * 1024 )) ;;
|
||||
*MB|*M) echo "$n" ;;
|
||||
*) echo $(( n / 1024 / 1024 )) ;;
|
||||
esac
|
||||
}
|
||||
|
||||
hogs() {
|
||||
echo "holding the most:"
|
||||
ps -eo rss,comm --sort=-rss 2>/dev/null | awk 'NR>1 && NR<=6 {printf " %6.0f MB %s\n", $1/1024, $2}'
|
||||
}
|
||||
|
||||
status() {
|
||||
local total avail swap_total swap_free
|
||||
total=$(mb MemTotal); avail=$(mb MemAvailable)
|
||||
swap_total=$(mb SwapTotal); swap_free=$(mb SwapFree)
|
||||
|
||||
if is_wsl; then
|
||||
local cfg conf conf_mb
|
||||
cfg=$(wslconfig_path)
|
||||
conf=$(configured_memory "$cfg")
|
||||
echo "platform WSL"
|
||||
echo "config $cfg"
|
||||
if [ -n "$conf" ]; then
|
||||
conf_mb=$(to_mb "$conf")
|
||||
echo "configured $conf (${conf_mb} MB)"
|
||||
else
|
||||
conf_mb=""
|
||||
echo "configured (no memory= set — WSL defaults to 50% of host RAM, or 8GB, whichever is less)"
|
||||
fi
|
||||
echo "booted ${total} MB"
|
||||
echo "available ${avail} MB"
|
||||
echo "swap ${swap_total} MB ($(( swap_total - swap_free )) MB used)"
|
||||
|
||||
if [ -n "$conf_mb" ]; then
|
||||
# The VM reports a little less than allocated; 15% covers the kernel
|
||||
# without calling every healthy machine a mismatch.
|
||||
if [ "$total" -lt $(( conf_mb * 85 / 100 )) ]; then
|
||||
echo
|
||||
echo "! configured ${conf_mb} MB but booted ${total} MB."
|
||||
echo " The change has not been applied. From a WINDOWS terminal:"
|
||||
echo
|
||||
echo " wsl --shutdown"
|
||||
echo
|
||||
echo " then start the distro again."
|
||||
fi
|
||||
else
|
||||
echo
|
||||
echo "To raise it, add to $cfg on the Windows side:"
|
||||
echo
|
||||
echo " [wsl2]"
|
||||
echo " memory=8GB"
|
||||
echo
|
||||
echo "then, from a WINDOWS terminal: wsl --shutdown"
|
||||
fi
|
||||
|
||||
local n
|
||||
n=$(ls "$cfg".*.bak 2>/dev/null | wc -l)
|
||||
[ "$n" -gt 0 ] && echo "backups $n (newest: $(ls -t "$cfg".*.bak 2>/dev/null | head -1))"
|
||||
else
|
||||
echo "platform native linux"
|
||||
echo "total ${total} MB"
|
||||
echo "available ${avail} MB"
|
||||
echo "swap ${swap_total} MB ($(( swap_total - swap_free )) MB used)"
|
||||
echo
|
||||
echo "No VM allocation to raise here — this is the machine's own memory."
|
||||
echo "If it is tight the levers are freeing something or adding swap."
|
||||
fi
|
||||
|
||||
# Under a fifth left is worth naming wherever you are running.
|
||||
if [ "$avail" -lt $(( total / 5 )) ]; then
|
||||
echo
|
||||
hogs
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
backup() {
|
||||
require_wsl backup
|
||||
local cfg dest
|
||||
cfg=$(wslconfig_path)
|
||||
[ -r "$cfg" ] || { echo "nothing to back up: $cfg does not exist" >&2; exit 1; }
|
||||
# Timestamped and never overwritten: a backup that can destroy itself on a
|
||||
# second run is not a backup.
|
||||
dest="${cfg}.$(date +%Y%m%d-%H%M%S).bak"
|
||||
cp "$cfg" "$dest"
|
||||
echo "backed up $dest"
|
||||
echo
|
||||
echo "Edit $cfg by hand, then from a WINDOWS terminal: wsl --shutdown"
|
||||
}
|
||||
|
||||
restore() {
|
||||
require_wsl restore
|
||||
local cfg newest count
|
||||
cfg=$(wslconfig_path)
|
||||
newest=$(ls -t "$cfg".*.bak 2>/dev/null | head -1 || true)
|
||||
[ -n "$newest" ] || { echo "no backups found beside $cfg" >&2; exit 1; }
|
||||
|
||||
echo "restoring $newest"
|
||||
echo " -> $cfg"
|
||||
echo
|
||||
|
||||
# Newest is the right default — undo the last edit — but if you backed up
|
||||
# *after* editing, the state you want is older. Show the rest so a no-op
|
||||
# restore is obviously a no-op rather than a mystery.
|
||||
count=$(ls "$cfg".*.bak 2>/dev/null | wc -l)
|
||||
if [ "$count" -gt 1 ]; then
|
||||
echo "$count backups exist, newest first:"
|
||||
ls -t "$cfg".*.bak | sed 's/^/ /'
|
||||
echo " (restoring the newest; copy another by hand to pick an older one)"
|
||||
echo
|
||||
fi
|
||||
|
||||
if [ -r "$cfg" ]; then
|
||||
echo "what changes:"
|
||||
if diff "$cfg" "$newest" > /tmp/mem.diff 2>&1 && [ ! -s /tmp/mem.diff ]; then
|
||||
echo " nothing — that backup is identical to the current config"
|
||||
else
|
||||
sed 's/^/ /' /tmp/mem.diff
|
||||
fi
|
||||
rm -f /tmp/mem.diff
|
||||
echo
|
||||
fi
|
||||
|
||||
printf "proceed? [y/N] "
|
||||
read -r reply
|
||||
case "$reply" in
|
||||
y|Y|yes|Yes) ;;
|
||||
*) echo "left alone"; return 0 ;;
|
||||
esac
|
||||
cp "$newest" "$cfg"
|
||||
echo "restored. From a WINDOWS terminal: wsl --shutdown"
|
||||
}
|
||||
|
||||
require_linux
|
||||
|
||||
case "${1:-status}" in
|
||||
status) status ;;
|
||||
backup) backup ;;
|
||||
restore) restore ;;
|
||||
*) echo "usage: $0 [status|backup|restore]" >&2; exit 1 ;;
|
||||
esac
|
||||
@@ -58,10 +58,13 @@ require_wsl() {
|
||||
cat >&2 <<'EOF'
|
||||
newbox is WSL-only for now.
|
||||
|
||||
If WSL is not installed, run `wsl --install` from an elevated Windows prompt
|
||||
first — see "Starting from plain Windows" in README.md.
|
||||
|
||||
On native Linux you do not need it: rig already isolates environments by
|
||||
directory (own cluster, context, images and port block), so a second copy in a
|
||||
second directory is the clean slate. To validate the installer itself against a
|
||||
bare system, run the wizard against a stock Debian container instead.
|
||||
bare system, run ctrl/deps.sh against a stock Debian container instead.
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
@@ -250,7 +253,7 @@ create() {
|
||||
echo
|
||||
echo "next:"
|
||||
echo " make newbox shell # a shell inside it"
|
||||
echo " then: cd ~/rig && make station && make deps && make cluster up"
|
||||
echo " then: cd ~/rig && make check && make deps && make cluster up"
|
||||
echo
|
||||
echo "For a browser on Windows to resolve the hostnames, paste this into"
|
||||
echo "C:\\Windows\\System32\\drivers\\etc\\hosts (it has no wildcard support):"
|
||||
|
||||
@@ -43,7 +43,7 @@ K="kubectl --context ${KUBECONTEXT}"
|
||||
# 2. every kind node's containerd — nodes do NOT inherit host trust
|
||||
# 3. anything doing HTTPS from inside the cluster, in its own trust store
|
||||
#
|
||||
# We handle (2) here because it's ours to handle. (1) is reported by station.sh
|
||||
# We handle (2) here because it's ours to handle. (1) is reported by check.sh
|
||||
# since it needs root. (3) belongs to the workload.
|
||||
install_ca_into_nodes() {
|
||||
[ -n "${REGISTRY_CA_FILE:-}" ] || return 0
|
||||
|
||||
@@ -73,11 +73,11 @@ record() {
|
||||
|
||||
step_host() {
|
||||
local out
|
||||
if ! out=$(bash ./wizard.sh detect 2>&1); then
|
||||
if ! out=$(bash ./deps.sh detect 2>&1); then
|
||||
record host fail "detection failed"
|
||||
return
|
||||
fi
|
||||
# Anything the wizard flagged with '!' needs a human; surface the count here
|
||||
# Anything flagged with '!' needs a human; surface the count here
|
||||
# and the detail below rather than burying it.
|
||||
local warns; warns=$(echo "$out" | grep -c '^\s*!' || true)
|
||||
HOST_DETAIL="$out"
|
||||
@@ -102,7 +102,7 @@ step_toolchain() {
|
||||
return
|
||||
fi
|
||||
|
||||
if bash ./wizard.sh install "$TIER" >/tmp/rig-deps.$$ 2>&1; then
|
||||
if bash ./deps.sh install "$TIER" >/tmp/rig-deps.$$ 2>&1; then
|
||||
local still=""
|
||||
for b in $want; do
|
||||
[ -x "${OUT_BIN:-$HOME/.local/bin}/$b" ] || still="$still $b"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Pinned toolchain — the single manifest the wizard installs from.
|
||||
# Pinned toolchain — the single manifest ctrl/deps.sh installs from.
|
||||
# Every entry is a single binary; none of them needs an apt repo.
|
||||
# kubectl fully static
|
||||
# kind libc only
|
||||
@@ -6,8 +6,19 @@
|
||||
# jq upstream static build (Debian's is linked against libjq/libonig)
|
||||
#
|
||||
# Checksums are the upstream-published SHA256 of the linux/amd64 artifact.
|
||||
# To bump: change the version, then re-run `bash ctrl/versions-refresh.sh` and
|
||||
# commit the result — never hand-edit a checksum.
|
||||
#
|
||||
# To bump: change the version, then take the checksum from the release's own
|
||||
# published list — never hand-edit or hand-copy one from a download you did.
|
||||
# For anything hosted on GitHub releases that is:
|
||||
#
|
||||
# curl -sSL https://github.com/<org>/<repo>/releases/download/<tag>/checksums.txt \
|
||||
# | grep linux.x86_64
|
||||
#
|
||||
# (kubectl publishes its own instead: <KUBECTL_URL>.sha256.)
|
||||
#
|
||||
# There was a `ctrl/versions-refresh.sh` named here that has never existed. If
|
||||
# bumping stops being rare enough to do by hand, write it — but a comment
|
||||
# pointing at a missing script is worse than no comment.
|
||||
|
||||
KIND_VERSION=v0.32.0
|
||||
KIND_SHA256=50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54
|
||||
@@ -21,6 +32,14 @@ TILT_VERSION=0.37.6
|
||||
TILT_SHA256=e9672b8a18d43501f35dcfe98465969a7db0e436b36cf0c50c7e6f8d40de5fe6
|
||||
TILT_URL=https://github.com/tilt-dev/tilt/releases/download/v${TILT_VERSION}/tilt.${TILT_VERSION}.linux.x86_64.tar.gz
|
||||
|
||||
# ctlptl — creates a kind cluster WITH a local registry wired in, which is what
|
||||
# keeps images off docker.io (an unqualified name means docker.io/library/<name>).
|
||||
# Same publisher and same archive shape as tilt: binary at the archive root, so
|
||||
# fetch_tgz handles it with strip=0 and no special case.
|
||||
CTLPTL_VERSION=0.9.4
|
||||
CTLPTL_SHA256=c63a1ec28e60bc3faf6becb76f53355c5cf5e0143dafdd27ad85db5584fa6b1e
|
||||
CTLPTL_URL=https://github.com/tilt-dev/ctlptl/releases/download/v${CTLPTL_VERSION}/ctlptl.${CTLPTL_VERSION}.linux.x86_64.tar.gz
|
||||
|
||||
JQ_VERSION=1.8.2
|
||||
JQ_SHA256=b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f
|
||||
JQ_URL=https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/jq-linux-amd64
|
||||
@@ -44,9 +63,9 @@ CERT_MANAGER_VERSION=v1.21.1
|
||||
METRICS_SERVER_VERSION=v0.9.0
|
||||
METALLB_VERSION=v0.16.0
|
||||
|
||||
# Dependency containers. These mirror soleprint's cabinets
|
||||
# (soleprint/station/cabinets/), so a room that declares postgres gets the same
|
||||
# thing whether it runs on compose or in the cluster. Pinned by tag rather than
|
||||
# Cabinets — public services dropped in as-is, the upstream image unmodified.
|
||||
# The same declaration installs on compose or in the cluster, so a dependency is
|
||||
# named once and works either way. Pinned by tag rather than
|
||||
# digest because they are ordinary upstream images with no supply chain claim
|
||||
# attached — bump freely, and preload them for the offline profile.
|
||||
POSTGRES_IMAGE=postgres:16-alpine
|
||||
|
||||
@@ -20,13 +20,13 @@ digraph rig_install {
|
||||
bin [label="~/.local/bin\nkind · kubectl · tilt\njq" fillcolor="#121829" shape=cylinder]
|
||||
}
|
||||
|
||||
subgraph cluster_wizard {
|
||||
subgraph cluster_installer {
|
||||
label="Installer container (transient)"
|
||||
style=dashed
|
||||
color="#1e2a4a"
|
||||
fontcolor="#8892a8"
|
||||
|
||||
wizard [label="wizard\ncurl · jq · python · graphviz" fillcolor="#121829"]
|
||||
installer [label="deps installer\ncurl · jq · python · graphviz" fillcolor="#121829"]
|
||||
detect [label="detect host\nWSL · memory · inotify · docker" fillcolor="#121829"]
|
||||
fetch [label="fetch + verify\nSHA256, pinned versions" fillcolor="#121829"]
|
||||
}
|
||||
@@ -34,14 +34,14 @@ digraph rig_install {
|
||||
upstream [label="upstream\nreleases / corporate mirror" fillcolor="#1a3a1a" fontcolor="#00c853" shape=octagon]
|
||||
report [label="report what it\nCANNOT do" fillcolor="#3a1a1a" fontcolor="#ffc107"]
|
||||
|
||||
docker -> wizard [label="docker run"]
|
||||
wizard -> detect
|
||||
docker -> installer [label="docker run"]
|
||||
installer -> detect
|
||||
detect -> fetch
|
||||
fetch -> upstream [label="pinned + checksummed" color="#00c853"]
|
||||
fetch -> bin [label="install"]
|
||||
detect -> report [style=dashed label="sudo / Windows-side steps" color="#ffc107"]
|
||||
|
||||
// The container is gone after this; nothing depends on it at run time.
|
||||
wizard -> gone [style=dotted label="exits"]
|
||||
installer -> gone [style=dotted label="exits"]
|
||||
gone [label="(container discarded)" fillcolor="#0a0e17" fontcolor="#4a5568" color="#1e2a4a" style="filled,dashed"]
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<text xml:space="preserve" text-anchor="middle" x="1011.15" y="-170.8" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">Your machine</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_wizard</title>
|
||||
<title>cluster_installer</title>
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="8,-95 8,-175 745.5,-175 745.5,-95 8,-95"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="376.75" y="-155.8" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">Installer container (transient)</text>
|
||||
</g>
|
||||
@@ -27,16 +27,16 @@
|
||||
<text xml:space="preserve" text-anchor="middle" x="1011.15" y="-46.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#0066ff">Docker</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1011.15" y="-32.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#0066ff">(the one prerequisite)</text>
|
||||
</g>
|
||||
<!-- wizard -->
|
||||
<!-- installer -->
|
||||
<g id="node3" class="node">
|
||||
<title>wizard</title>
|
||||
<title>installer</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="181.25,-139 16,-139 16,-103 181.25,-103 181.25,-139"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="98.62" y="-124.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">wizard</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="98.62" y="-124.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">deps installer</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="98.62" y="-110.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">curl · jq · python · graphviz</text>
|
||||
</g>
|
||||
<!-- docker->wizard -->
|
||||
<!-- docker->installer -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>docker->wizard</title>
|
||||
<title>docker->installer</title>
|
||||
<path fill="none" stroke="#4a5568" d="M906.12,-45.7C757.47,-50.51 475.98,-63.12 238.25,-94 223.38,-95.93 207.7,-98.48 192.45,-101.24"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="192.13,-97.74 182.93,-103.01 193.4,-104.63 192.13,-97.74"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="506.62" y="-74.39" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">docker run</text>
|
||||
@@ -57,9 +57,9 @@
|
||||
<text xml:space="preserve" text-anchor="middle" x="333.62" y="-124.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">detect host</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="333.62" y="-110.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">WSL · memory · inotify · docker</text>
|
||||
</g>
|
||||
<!-- wizard->detect -->
|
||||
<!-- installer->detect -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>wizard->detect</title>
|
||||
<title>installer->detect</title>
|
||||
<path fill="none" stroke="#4a5568" d="M181.44,-121C195.97,-121 211.28,-121 226.37,-121"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="226.33,-124.5 236.33,-121 226.33,-117.5 226.33,-124.5"/>
|
||||
</g>
|
||||
@@ -69,9 +69,9 @@
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="400.5,-47 266.75,-47 266.75,-11 400.5,-11 400.5,-47"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="333.62" y="-25.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#4a5568">(container discarded)</text>
|
||||
</g>
|
||||
<!-- wizard->gone -->
|
||||
<!-- installer->gone -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>wizard->gone</title>
|
||||
<title>installer->gone</title>
|
||||
<path fill="none" stroke="#4a5568" stroke-dasharray="1,5" d="M119.29,-102.62C138.26,-86 168.54,-62.29 199.25,-49.75 216.76,-42.6 236.49,-37.9 255.29,-34.81"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="255.67,-38.29 265.04,-33.35 254.64,-31.37 255.67,-38.29"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="209.75" y="-52.45" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">exits</text>
|
||||
|
||||
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
@@ -20,7 +20,7 @@ digraph rig_environment {
|
||||
|
||||
cname [label="cluster name\nacmebank" fillcolor="#121829"]
|
||||
ctx [label="kubectl context\nkind-acmebank" fillcolor="#121829"]
|
||||
img [label="image tag\nacmebank-wizard" fillcolor="#121829"]
|
||||
img [label="image tag\nacmebank-deps" fillcolor="#121829"]
|
||||
ports [label="port block\n21300–21309" fillcolor="#121829"]
|
||||
reg [label="registry container\nacmebank-registry" fillcolor="#121829"]
|
||||
}
|
||||
|
||||
@@ -4,28 +4,28 @@
|
||||
<!-- Generated by graphviz version 14.1.2 (0)
|
||||
-->
|
||||
<!-- Title: rig_environment Pages: 1 -->
|
||||
<svg width="971pt" height="481pt"
|
||||
viewBox="0.00 0.00 971.00 481.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<svg width="962pt" height="481pt"
|
||||
viewBox="0.00 0.00 962.00 481.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 476.83)">
|
||||
<title>rig_environment</title>
|
||||
<polygon fill="#0a0e17" stroke="none" points="-4,4 -4,-476.83 967,-476.83 967,4 -4,4"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="481.5" y="-453.63" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#0066ff">One environment per directory — copies never collide</text>
|
||||
<polygon fill="#0a0e17" stroke="none" points="-4,4 -4,-476.83 958,-476.83 958,4 -4,4"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="477" y="-453.63" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#0066ff">One environment per directory — copies never collide</text>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_derived</title>
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="8,-65 8,-144.5 605,-144.5 605,-65 8,-65"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="306.5" y="-125.3" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">Everything below is derived from it</text>
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="8,-65 8,-144.5 596,-144.5 596,-65 8,-65"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="302" y="-125.3" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">Everything below is derived from it</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_config</title>
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="613,-65 613,-437.33 955,-437.33 955,-65 613,-65"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="784" y="-418.13" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">Configuration — weakest first, later wins</text>
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="604,-65 604,-437.33 946,-437.33 946,-65 604,-65"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="775" y="-418.13" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">Configuration — weakest first, later wins</text>
|
||||
</g>
|
||||
<!-- dirname -->
|
||||
<g id="node1" class="node">
|
||||
<title>dirname</title>
|
||||
<polygon fill="#1f6feb" stroke="#1e2a4a" points="375.11,-197.44 375.11,-219.63 329.94,-235.33 266.06,-235.33 220.89,-219.63 220.89,-197.44 266.06,-181.75 329.94,-181.75 375.11,-197.44"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="298" y="-211.59" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffffff">directory name</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="298" y="-198.09" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffffff">e.g. acmebank/</text>
|
||||
<polygon fill="#1f6feb" stroke="#1e2a4a" points="370.11,-197.44 370.11,-219.63 324.94,-235.33 261.06,-235.33 215.89,-219.63 215.89,-197.44 261.06,-181.75 324.94,-181.75 370.11,-197.44"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="293" y="-211.59" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffffff">directory name</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="293" y="-198.09" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffffff">e.g. acmebank/</text>
|
||||
</g>
|
||||
<!-- cname -->
|
||||
<g id="node2" class="node">
|
||||
@@ -37,8 +37,8 @@
|
||||
<!-- dirname->cname -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>dirname->cname</title>
|
||||
<path fill="none" stroke="#4a5568" d="M232.76,-193.05C195.81,-183.01 149.78,-167.28 113,-144.5 101.37,-137.29 90.31,-127.09 81.33,-117.6"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="84.04,-115.37 74.73,-110.3 78.85,-120.07 84.04,-115.37"/>
|
||||
<path fill="none" stroke="#4a5568" d="M228.68,-192.51C192.88,-182.36 148.52,-166.7 113,-144.5 101.4,-137.25 90.34,-127.04 81.36,-117.55"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="84.07,-115.32 74.76,-110.26 78.88,-120.02 84.07,-115.32"/>
|
||||
</g>
|
||||
<!-- ctx -->
|
||||
<g id="node3" class="node">
|
||||
@@ -50,120 +50,120 @@
|
||||
<!-- dirname->ctx -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>dirname->ctx</title>
|
||||
<path fill="none" stroke="#4a5568" d="M269.64,-181.32C248.71,-161.98 220.42,-135.83 199.86,-116.83"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="202.4,-114.41 192.68,-110.19 197.65,-119.55 202.4,-114.41"/>
|
||||
<path fill="none" stroke="#4a5568" d="M265.77,-181.32C245.77,-162.07 218.77,-136.06 199.05,-117.08"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="201.55,-114.63 191.91,-110.21 196.69,-119.67 201.55,-114.63"/>
|
||||
</g>
|
||||
<!-- img -->
|
||||
<g id="node4" class="node">
|
||||
<title>img</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="354,-109 242,-109 242,-73 354,-73 354,-109"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="298" y="-94.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">image tag</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="298" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">acmebank-wizard</text>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="344.12,-109 241.88,-109 241.88,-73 344.12,-73 344.12,-109"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="293" y="-94.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">image tag</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="293" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">acmebank-deps</text>
|
||||
</g>
|
||||
<!-- dirname->img -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>dirname->img</title>
|
||||
<path fill="none" stroke="#4a5568" d="M298,-181.32C298,-163.19 298,-139.07 298,-120.47"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="301.5,-120.67 298,-110.67 294.5,-120.67 301.5,-120.67"/>
|
||||
<path fill="none" stroke="#4a5568" d="M293,-181.32C293,-163.19 293,-139.07 293,-120.47"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="296.5,-120.67 293,-110.67 289.5,-120.67 296.5,-120.67"/>
|
||||
</g>
|
||||
<!-- ports -->
|
||||
<g id="node5" class="node">
|
||||
<title>ports</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="460.38,-109 371.62,-109 371.62,-73 460.38,-73 460.38,-109"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="416" y="-94.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">port block</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="416" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">21300–21309</text>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="451.38,-109 362.62,-109 362.62,-73 451.38,-73 451.38,-109"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="407" y="-94.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">port block</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="407" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">21300–21309</text>
|
||||
</g>
|
||||
<!-- dirname->ports -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>dirname->ports</title>
|
||||
<path fill="none" stroke="#4a5568" d="M324.94,-181.53C336.66,-170.19 350.54,-156.71 363,-144.5 372.11,-135.57 382.06,-125.74 390.85,-117.02"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="393.15,-119.67 397.79,-110.14 388.22,-114.7 393.15,-119.67"/>
|
||||
<path fill="none" stroke="#4a5568" d="M318.87,-181.32C337.78,-162.15 363.29,-136.3 382,-117.34"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="384.47,-119.81 389,-110.24 379.49,-114.9 384.47,-119.81"/>
|
||||
</g>
|
||||
<!-- reg -->
|
||||
<g id="node6" class="node">
|
||||
<title>reg</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="597.38,-109 478.62,-109 478.62,-73 597.38,-73 597.38,-109"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="538" y="-94.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">registry container</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="538" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">acmebank-registry</text>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="588.38,-109 469.62,-109 469.62,-73 588.38,-73 588.38,-109"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="529" y="-94.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">registry container</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="529" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">acmebank-registry</text>
|
||||
</g>
|
||||
<!-- dirname->reg -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>dirname->reg</title>
|
||||
<path fill="none" stroke="#4a5568" d="M356.87,-190.63C390.74,-179.74 433.49,-163.98 469,-144.5 483.3,-136.65 497.86,-126.04 509.9,-116.41"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="511.88,-119.31 517.39,-110.26 507.44,-113.9 511.88,-119.31"/>
|
||||
<path fill="none" stroke="#4a5568" d="M350.86,-190.3C383.87,-179.35 425.43,-163.64 460,-144.5 474.27,-136.6 488.83,-125.98 500.87,-116.36"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="502.85,-119.26 508.36,-110.21 498.41,-113.84 502.85,-119.26"/>
|
||||
</g>
|
||||
<!-- cluster -->
|
||||
<g id="node11" class="node">
|
||||
<title>cluster</title>
|
||||
<polygon fill="#1a1a3a" stroke="#1e2a4a" points="469.81,-10.54 469.81,-25.46 438.29,-36 393.71,-36 362.19,-25.46 362.19,-10.54 393.71,0 438.29,0 469.81,-10.54"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="416" y="-14.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#0066ff">kind cluster</text>
|
||||
<polygon fill="#1a1a3a" stroke="#1e2a4a" points="460.81,-10.54 460.81,-25.46 429.29,-36 384.71,-36 353.19,-25.46 353.19,-10.54 384.71,0 429.29,0 460.81,-10.54"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="407" y="-14.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#0066ff">kind cluster</text>
|
||||
</g>
|
||||
<!-- cname->cluster -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>cname->cluster</title>
|
||||
<path fill="none" stroke="#4a5568" d="M93.06,-72.62C99.54,-69.72 106.38,-67.02 113,-65 192.78,-40.7 288.45,-28.91 350.65,-23.42"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="350.69,-26.93 360.36,-22.6 350.1,-19.96 350.69,-26.93"/>
|
||||
<path fill="none" stroke="#4a5568" d="M93.35,-72.51C99.75,-69.67 106.48,-67 113,-65 189.55,-41.49 281.19,-29.58 341.58,-23.85"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="341.71,-27.35 351.35,-22.96 341.07,-20.38 341.71,-27.35"/>
|
||||
</g>
|
||||
<!-- ports->cluster -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>ports->cluster</title>
|
||||
<path fill="none" stroke="#4a5568" d="M416,-72.81C416,-65.23 416,-56.1 416,-47.54"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="419.5,-47.54 416,-37.54 412.5,-47.54 419.5,-47.54"/>
|
||||
<path fill="none" stroke="#4a5568" d="M407,-72.81C407,-65.23 407,-56.1 407,-47.54"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="410.5,-47.54 407,-37.54 403.5,-47.54 410.5,-47.54"/>
|
||||
</g>
|
||||
<!-- versions -->
|
||||
<g id="node7" class="node">
|
||||
<title>versions</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="759.38,-401.83 652.62,-401.83 652.62,-365.83 759.38,-365.83 759.38,-401.83"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="706" y="-386.88" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">versions.env</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="706" y="-373.38" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">pinned toolchain</text>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="750.38,-401.83 643.62,-401.83 643.62,-365.83 750.38,-365.83 750.38,-401.83"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="697" y="-386.88" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">versions.env</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="697" y="-373.38" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">pinned toolchain</text>
|
||||
</g>
|
||||
<!-- profile -->
|
||||
<g id="node8" class="node">
|
||||
<title>profile</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="790.5,-318.58 621.5,-318.58 621.5,-282.58 790.5,-282.58 790.5,-318.58"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="706" y="-303.63" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">env.d/<profile>.env</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="706" y="-290.13" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">nodes · CNI · audit · addons</text>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="781.5,-318.58 612.5,-318.58 612.5,-282.58 781.5,-282.58 781.5,-318.58"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="697" y="-303.63" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">env.d/<profile>.env</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="697" y="-290.13" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">nodes · CNI · audit · addons</text>
|
||||
</g>
|
||||
<!-- versions->profile -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>versions->profile</title>
|
||||
<path fill="none" stroke="#4a5568" d="M706,-365.59C706,-355.32 706,-342.03 706,-330.21"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="709.5,-330.58 706,-320.58 702.5,-330.58 709.5,-330.58"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="737.5" y="-339.28" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">overridden by</text>
|
||||
<path fill="none" stroke="#4a5568" d="M697,-365.59C697,-355.32 697,-342.03 697,-330.21"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="700.5,-330.58 697,-320.58 693.5,-330.58 700.5,-330.58"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="728.5" y="-339.28" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">overridden by</text>
|
||||
</g>
|
||||
<!-- localenv -->
|
||||
<g id="node9" class="node">
|
||||
<title>localenv</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="761.88,-226.54 646.12,-226.54 646.12,-190.54 761.88,-190.54 761.88,-226.54"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="704" y="-211.59" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">ctrl/.env</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="704" y="-198.09" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">secrets, overrides</text>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="752.88,-226.54 637.12,-226.54 637.12,-190.54 752.88,-190.54 752.88,-226.54"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="695" y="-211.59" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">ctrl/.env</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="695" y="-198.09" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">secrets, overrides</text>
|
||||
</g>
|
||||
<!-- profile->localenv -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>profile->localenv</title>
|
||||
<path fill="none" stroke="#4a5568" d="M705.61,-282.22C705.34,-269.76 704.96,-252.69 704.64,-238.23"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="708.14,-238.28 704.42,-228.36 701.14,-238.43 708.14,-238.28"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="736.68" y="-256.03" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">overridden by</text>
|
||||
<path fill="none" stroke="#4a5568" d="M696.61,-282.22C696.34,-269.76 695.96,-252.69 695.64,-238.23"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="699.14,-238.28 695.42,-228.36 692.14,-238.43 699.14,-238.28"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="727.68" y="-256.03" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">overridden by</text>
|
||||
</g>
|
||||
<!-- shell -->
|
||||
<g id="node10" class="node">
|
||||
<title>shell</title>
|
||||
<polygon fill="#1a3a1a" stroke="#1e2a4a" points="772.38,-109 623.62,-109 623.62,-73 772.38,-73 772.38,-109"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="698" y="-94.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">the environment</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="698" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">PROFILE=client make …</text>
|
||||
<polygon fill="#1a3a1a" stroke="#1e2a4a" points="763.38,-109 614.62,-109 614.62,-73 763.38,-73 763.38,-109"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="689" y="-94.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">the environment</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="689" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">PROFILE=client make …</text>
|
||||
</g>
|
||||
<!-- localenv->shell -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>localenv->shell</title>
|
||||
<path fill="none" stroke="#00c853" d="M703.11,-190.49C702.16,-172.16 700.63,-142.72 699.49,-120.79"/>
|
||||
<polygon fill="#00c853" stroke="#00c853" points="703,-120.81 698.99,-111.01 696.01,-121.18 703,-120.81"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="733.21" y="-155.2" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">overridden by</text>
|
||||
<path fill="none" stroke="#00c853" d="M694.11,-190.49C693.16,-172.16 691.63,-142.72 690.49,-120.79"/>
|
||||
<polygon fill="#00c853" stroke="#00c853" points="694,-120.81 689.99,-111.01 687.01,-121.18 694,-120.81"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="724.21" y="-155.2" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">overridden by</text>
|
||||
</g>
|
||||
<!-- shell->cluster -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>shell->cluster</title>
|
||||
<path fill="none" stroke="#4a5568" stroke-dasharray="5,2" d="M637.29,-72.6C627.83,-69.99 618.17,-67.38 609,-65 562.72,-52.99 509.89,-40.48 471.22,-31.54"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="472.2,-28.18 461.67,-29.35 470.63,-35 472.2,-28.18"/>
|
||||
<path fill="none" stroke="#4a5568" stroke-dasharray="5,2" d="M628.29,-72.6C618.83,-69.99 609.17,-67.38 600,-65 553.72,-52.99 500.89,-40.48 462.22,-31.54"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="463.2,-28.18 452.67,-29.35 461.63,-35 463.2,-28.18"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@@ -265,11 +265,11 @@
|
||||
<h3>The only prerequisite</h3>
|
||||
<p><b>Docker.</b> No curl, no jq, no python, no apt repositories to configure.</p>
|
||||
<pre><code><span class="c"># then, in the environment directory:</span>
|
||||
make station <span class="c"># is this workstation ready? reports, never fixes</span>
|
||||
make check <span class="c"># is this machine ready? reports, never fixes</span>
|
||||
make deps <span class="c"># install the pinned toolchain</span>
|
||||
make cluster up <span class="c"># build the cluster for the active profile</span>
|
||||
</code></pre>
|
||||
<p>Read <code>make station</code> before <code>make deps</code>. It never changes
|
||||
<p>Read <code>make check</code> before <code>make deps</code>. It never changes
|
||||
anything — it prints what it found and, at the end, the steps it cannot perform
|
||||
for you.</p>
|
||||
</div>
|
||||
@@ -280,13 +280,13 @@ make cluster up <span class="c"># build the cluster for the active profile</spa
|
||||
<p class="lede">Start to finish, in order, with what each one actually does.</p>
|
||||
<div class="prose">
|
||||
|
||||
<h3>1 · make station</h3>
|
||||
<p>Asks whether this workstation is ready. It <b>changes nothing</b> — it
|
||||
<h3>1 · make check</h3>
|
||||
<p>Asks whether this machine is ready. It <b>changes nothing</b> — it
|
||||
reports what it found and, at the end, the things only a human can do
|
||||
(anything needing <code>sudo</code>, or a Windows-side restart). Read it
|
||||
before installing anything; it is faster than discovering the same problems
|
||||
one failure at a time.</p>
|
||||
<pre><code>make station</code></pre>
|
||||
<pre><code>make check</code></pre>
|
||||
|
||||
<h3>2 · make setup</h3>
|
||||
<p>Does the preparation that can be automated: installs the pinned
|
||||
@@ -381,8 +381,8 @@ make setup core <span class="c"># same distinction, via setup</span>
|
||||
wants only Docker.</p>
|
||||
|
||||
<h3>Air-gapped</h3>
|
||||
<pre><code>make wizard full <span class="c"># bakes every binary into the image</span>
|
||||
docker save …-wizard:full | gzip > rig.tgz
|
||||
<pre><code>make deps-image full <span class="c"># bakes every binary into the image</span>
|
||||
docker save …-deps:full | gzip > rig.tgz
|
||||
<span class="c"># carry that one file in, then:</span>
|
||||
docker load < rig.tgz && make cluster up PROFILE=offline
|
||||
</code></pre>
|
||||
@@ -485,7 +485,7 @@ docker load < rig.tgz && make cluster up PROFILE=offline
|
||||
registry is usually behind an internal CA, and trust has to reach
|
||||
<b>three</b> places: the host Docker daemon, every cluster node's containerd
|
||||
(nodes do <i>not</i> inherit host trust), and any in-cluster client. Set
|
||||
<code>REGISTRY_CA_FILE</code> and <code>make station</code> reports which is
|
||||
<code>REGISTRY_CA_FILE</code> and <code>make check</code> reports which is
|
||||
still missing. The symptom otherwise is an opaque
|
||||
<code>x509: certificate signed by unknown authority</code>.</p></div>
|
||||
|
||||
@@ -528,11 +528,11 @@ docker load < rig.tgz && make cluster up PROFILE=offline
|
||||
<h3>Tilt stops noticing file changes</h3>
|
||||
<p>Almost always <code>inotify</code> limits, and it fails <i>silently</i> —
|
||||
nothing errors, changes just stop being picked up. Defaults on WSL are far too
|
||||
low. <code>make station</code> reports it and prints the fix.</p>
|
||||
low. <code>make check</code> reports it and prints the fix.</p>
|
||||
|
||||
<h3>Cluster creation dies halfway with a port error</h3>
|
||||
<p>Docker reports <code>failed to bind host port … address already in use</code>
|
||||
partway through creating the cluster. Run <code>make station</code> first — it
|
||||
partway through creating the cluster. Run <code>make check</code> first — it
|
||||
checks every port in this environment's block before anything is built.</p>
|
||||
|
||||
<h3>Every node stays NotReady</h3>
|
||||
|
||||
9
rig/sample-rig/.gitignore
vendored
9
rig/sample-rig/.gitignore
vendored
@@ -1,9 +0,0 @@
|
||||
# NOTE: generated/ is deliberately NOT ignored. The artifact IS the deliverable —
|
||||
# the whole point is a folder you copy, apply and boot without generating
|
||||
# anything first. Regenerate it with `make manifest` after editing bundle.json
|
||||
# or app/serve.py, and commit the result.
|
||||
#
|
||||
# (This differs from rig's ctrl/k8s/generated, which is a local build artifact.)
|
||||
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -1,50 +0,0 @@
|
||||
# Thin control Makefile — one target per ctrl/ script, subcommand as an
|
||||
# argument. Same shape as rig's, for the same reason: the logic lives in the
|
||||
# script, never here.
|
||||
#
|
||||
# make up -> ctrl/bundle.sh up
|
||||
# make bundle down
|
||||
#
|
||||
# This directory is a BUNDLE, not an installer. It needs a cluster, which rig
|
||||
# owns:
|
||||
#
|
||||
# cd .. && make cluster up # kind cluster for this environment
|
||||
# make up # then deploy this bundle into it
|
||||
#
|
||||
# Start with: make up
|
||||
|
||||
ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
|
||||
ifneq ($(ARGS),)
|
||||
$(eval $(ARGS):;@:)
|
||||
endif
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
.PHONY: help bundle manifest up down status url list dev
|
||||
|
||||
help: ## list targets
|
||||
@grep -hE '^[a-z]+:.*?##' $(MAKEFILE_LIST) | sed 's/:.*##/\t/' | expand -t16
|
||||
|
||||
bundle: ## the bundle [manifest|up|down|status|url|list|dev]
|
||||
bash ctrl/bundle.sh $(or $(ARGS),status)
|
||||
|
||||
# Shorthands for the ones used constantly.
|
||||
manifest: ## regenerate generated/<slug>.yaml — no cluster needed
|
||||
bash ctrl/bundle.sh manifest
|
||||
|
||||
up: ## deploy this rig (installs MetalLB if absent)
|
||||
bash ctrl/bundle.sh up
|
||||
|
||||
down: ## remove this rig (leaves cluster, MetalLB, siblings)
|
||||
bash ctrl/bundle.sh down
|
||||
|
||||
status: ## what is deployed for this rig
|
||||
bash ctrl/bundle.sh status
|
||||
|
||||
url: ## the address MetalLB assigned
|
||||
bash ctrl/bundle.sh url
|
||||
|
||||
list: ## every rig in this cluster, with addresses
|
||||
bash ctrl/bundle.sh list
|
||||
|
||||
dev: ## run the UI locally with vite — no cluster needed
|
||||
bash ctrl/bundle.sh dev
|
||||
@@ -1,140 +0,0 @@
|
||||
# sample-rig
|
||||
|
||||
A minimal, non-sensitive bundle that proves an installation works and shows what
|
||||
shipped. Copy it, rename it, and you have another rig.
|
||||
|
||||
```bash
|
||||
make manifest # generate the artifact — no cluster, no kubectl needed
|
||||
make up # deploy it into the local cluster
|
||||
make list # every rig in this cluster, with addresses
|
||||
make dev # run the UI locally with vite, no cluster at all
|
||||
```
|
||||
|
||||
`make up` prints an address. Open it and the page says **IT WORKS**, then lists
|
||||
the tools and rigs in the bundle.
|
||||
|
||||
## What it is for
|
||||
|
||||
Three jobs, in the order you hit them:
|
||||
|
||||
1. **Prove the install.** kind is there, a cluster exists, MetalLB hands out an
|
||||
address, a `type: LoadBalancer` Service actually resolves, and a pod serves.
|
||||
If all of that works, the environment is sound.
|
||||
2. **Say what shipped.** The page renders [`bundle.json`](bundle.json) —
|
||||
standalone tools and rigs, flat, with none of soleprint's internal hierarchy.
|
||||
Editing that file is the only step needed to change the listing.
|
||||
3. **Stand in for the real thing.** Nothing here is sensitive. The real
|
||||
architecture connects separately, against a setup already known to work.
|
||||
|
||||
## The UI is a complement, not the product
|
||||
|
||||
`rig-ui/` is just a vite app. It complements a rig; a rig is complete and useful
|
||||
without it, and nothing depends on it being there. It is deliberately **not**
|
||||
generated by kind or tilt — you copy the folder into a rig after that rig is
|
||||
pulled, and apply one manifest:
|
||||
|
||||
```bash
|
||||
kubectl apply -n <namespace> -f rig-ui/k8s.yaml
|
||||
```
|
||||
|
||||
That file is the whole integration: one Pod running `npm run dev` on
|
||||
`node:22-alpine`, one Service. A bare Pod rather than a Deployment because this
|
||||
is a dev-loop convenience, not a workload to keep alive.
|
||||
|
||||
The app and `bundle.json` arrive as a ConfigMap, so nothing is baked into an
|
||||
image and editing the bundle is the entire update cycle. The container runs
|
||||
`npm install` at start, which needs egress to a registry — on a locked-down
|
||||
cluster point npm at the internal one, or bake an image instead. Nothing else
|
||||
changes if you do.
|
||||
|
||||
## One artifact, two destinations
|
||||
|
||||
`ctrl/manifest.py` emits `generated/<slug>.yaml` — namespace, the app and
|
||||
bundle embedded in a ConfigMap, Pod, Service. It is self-contained and applies
|
||||
unmodified anywhere:
|
||||
|
||||
```bash
|
||||
kubectl apply -f generated/sample-rig.yaml # local kind, or an external cluster
|
||||
```
|
||||
|
||||
`make up` applies **that same file**. There is no separate local path, so what
|
||||
works here cannot quietly differ from what is applied elsewhere.
|
||||
|
||||
This is what `type: LoadBalancer` buys. MetalLB answers it on kind; the AWS load
|
||||
balancer controller answers it on EKS. NodePort would not survive the trip — it
|
||||
is a single cluster-wide port range, so two rigs would have to negotiate numbers.
|
||||
|
||||
**VPC-agnostic on purpose.** The target is EKS, but the Service carries no
|
||||
annotations — no `aws-load-balancer-subnets`, no security groups, no `-scheme`,
|
||||
no `-type: nlb`. Each of those encodes a specific network layout, and one of them
|
||||
appearing here would pin the artifact to the account and VPC it was written
|
||||
against, which is precisely what stops it also working on kind. Subnet discovery
|
||||
is the cluster's business: EKS resolves it from the tags its own subnets carry.
|
||||
|
||||
That leaves one thing genuinely environment-specific — internal versus
|
||||
internet-facing. A bare `LoadBalancer` provisions internet-facing, which a
|
||||
regulated account will usually refuse, and should. That belongs in a
|
||||
per-environment overlay applied on top, never inlined into this artifact.
|
||||
|
||||
**MetalLB only — no ingress-nginx.** Its controller supports a narrow window of
|
||||
Kubernetes versions, so depending on it constrains which k8s a rig can be built
|
||||
with. That undercuts running trailing-edge control planes to model a legacy
|
||||
estate, which is the reason `versions.env` pins v1_33..v1_36. MetalLB carries no
|
||||
such constraint, so reachability costs nothing in version coverage.
|
||||
|
||||
## Several rigs, one cluster
|
||||
|
||||
Identity follows the **folder name**, the same rule rig uses for cluster
|
||||
identity. The namespace is the folder; resource names are generic, and names only
|
||||
have to be unique within a namespace.
|
||||
|
||||
```bash
|
||||
cp -r sample-rig corporate-rig
|
||||
cd corporate-rig && make up # its own namespace, its own address
|
||||
```
|
||||
|
||||
No edits, no collisions, both in the same local cluster. `make list` shows them
|
||||
together. `make down` removes only this one — siblings, MetalLB and the cluster
|
||||
are left alone.
|
||||
|
||||
Client rigs are gitignored (`*-rig/`, with `sample-rig/` the deliberate
|
||||
exception): a rig's k8s files spell out a real architecture, and that is exactly
|
||||
what must not land in this repo.
|
||||
|
||||
## Staging workstations
|
||||
|
||||
`ctrl/manifest.py` is stdlib-only on purpose: it runs on a bare machine before
|
||||
anything is installed. The toolchain itself is rig's job — `make deps` installs
|
||||
the pinned kind and tilt binaries, which is what makes a staging AWS workspace
|
||||
reachable from the same commands as a laptop.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
sample-rig/
|
||||
├── Makefile # thin — one target per ctrl/ script
|
||||
├── bundle.json # what shipped; the UI renders THIS
|
||||
├── rig-ui/ # the vite app — optional, copied into a rig to enable it
|
||||
│ ├── k8s.yaml # how to plug it in: one Pod, one Service
|
||||
│ ├── index.html
|
||||
│ ├── package.json
|
||||
│ ├── vite.config.js
|
||||
│ └── src/{main.js,style.css}
|
||||
├── ctrl/
|
||||
│ ├── manifest.py # emits the artifact
|
||||
│ └── bundle.sh # generate / deploy / inspect
|
||||
└── generated/ # the artifact — committed, this is the deliverable
|
||||
```
|
||||
|
||||
Editing `bundle.json` or anything in `rig-ui/` means re-running `make manifest`.
|
||||
The ConfigMap carries a checksum of everything embedded, so a stale deployment is
|
||||
visible rather than silent.
|
||||
|
||||
## Not built, but not foreclosed
|
||||
|
||||
Everything derives from `bundle.json` plus a target namespace. A Pulumi or
|
||||
Terraform emitter would sit beside `ctrl/manifest.py` consuming the same inputs;
|
||||
nothing above it assumes the artifact is YAML.
|
||||
|
||||
Licence terms for the compiled UI component belong in the soleprint-generated
|
||||
bundle, not here — this sample carries no proprietary component.
|
||||
@@ -1,51 +0,0 @@
|
||||
{
|
||||
"_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."
|
||||
]
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"_comment": "MOCKED cluster state. Nothing here is read from a live cluster — it exists so the UI can be shown when there is no cluster at all (a locked-down machine, a laptop with no memory to spare, a demo where kind will not start). The page labels it as mocked; a demo that looks live but is not is worse than one that says so. When a real cluster is present the same shapes come from kubectl.",
|
||||
|
||||
"mocked": true,
|
||||
|
||||
"cluster": {
|
||||
"name": "sample-rig",
|
||||
"context": "kind-sample-rig",
|
||||
"provider": "kind",
|
||||
"profile": "minimal",
|
||||
"k8s": "v1.36.1",
|
||||
"nodes": 1
|
||||
},
|
||||
|
||||
"workloads": [
|
||||
{
|
||||
"name": "rig-ui",
|
||||
"summary": "Pod · node:22-alpine · vite on :5173",
|
||||
"state": "Running"
|
||||
},
|
||||
{
|
||||
"name": "metallb-system/controller",
|
||||
"summary": "Deployment · assigns LoadBalancer addresses",
|
||||
"state": "Running"
|
||||
},
|
||||
{
|
||||
"name": "metallb-system/speaker",
|
||||
"summary": "DaemonSet · answers ARP in layer 2 mode",
|
||||
"state": "Running"
|
||||
}
|
||||
],
|
||||
|
||||
"services": [
|
||||
{
|
||||
"name": "rig-ui",
|
||||
"summary": "LoadBalancer · 80 -> 5173 · no annotations, so it resolves on kind and on EKS alike",
|
||||
"state": "172.18.255.200"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# The rig bundle: generate it, deploy it, tear it down, find it.
|
||||
#
|
||||
# Usage: bundle.sh manifest | up | down | status | url | list | dev
|
||||
#
|
||||
# What `up` proves, in order: kind installed and a cluster exists, MetalLB can
|
||||
# hand out an address, a Service of type LoadBalancer actually resolves, and a
|
||||
# pod serves the bundle listing. If all of that works the installation is sound,
|
||||
# and the only thing missing is the real architecture.
|
||||
#
|
||||
# ONE ARTIFACT
|
||||
# `up` applies generated/<slug>.yaml — the same self-contained file you would
|
||||
# hand to an external cluster. There is no separate local path, so what works
|
||||
# here cannot quietly differ from the master deployment applied elsewhere.
|
||||
#
|
||||
# ONE CLUSTER, SEVERAL RIGS
|
||||
# Identity follows the FOLDER NAME, exactly as rig's cluster identity does. This
|
||||
# directory deploys into a namespace named after itself, so copying it to
|
||||
# corporate-rig/ yields a second rig in the SAME local cluster with no edits and
|
||||
# no collisions — different namespace, its own MetalLB address. `list` shows all
|
||||
# of them. The cluster itself is rig's business; this only ever owns a namespace.
|
||||
#
|
||||
# MetalLB is installed by calling rig's own addon script rather than
|
||||
# reimplementing it — deriving the pool from the kind Docker network is the
|
||||
# fiddly part and there should be exactly one copy of it.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
BUNDLE_ROOT="$(pwd)"
|
||||
RIG_CTRL="$(cd .. && pwd)/ctrl"
|
||||
|
||||
# The containing folder's name, reduced to a DNS label (same rule as rig's
|
||||
# default_cluster_name and ctrl/manifest.py, so all three agree on the slug).
|
||||
slug() {
|
||||
local n
|
||||
n=$(basename "$BUNDLE_ROOT")
|
||||
n=$(echo "$n" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-')
|
||||
n=$(echo "$n" | sed 's/^-*//; s/-*$//')
|
||||
echo "${n:-rig-bundle}"
|
||||
}
|
||||
NS="$(slug)"
|
||||
ARTIFACT="generated/${NS}.yaml"
|
||||
|
||||
# Resolved lazily, not at load time: `manifest` and `dev` deliberately work
|
||||
# with no cluster and no kubectl at all, and a top-level check would break that.
|
||||
#
|
||||
# Follows whatever context rig's cluster.sh selected, so this bundle works in a
|
||||
# copied-and-renamed environment without being told which cluster it is in.
|
||||
init_kube() {
|
||||
KUBECONTEXT="${KUBECONTEXT:-$(kubectl config current-context 2>/dev/null || true)}"
|
||||
if [ -z "$KUBECONTEXT" ]; then
|
||||
echo "no kubectl context — bring a cluster up first: (cd .. && make cluster up)" >&2
|
||||
exit 1
|
||||
fi
|
||||
KCTX="kubectl --context ${KUBECONTEXT}"
|
||||
K="kubectl --context ${KUBECONTEXT} --namespace ${NS}"
|
||||
}
|
||||
|
||||
require_cluster() {
|
||||
if ! $KCTX cluster-info >/dev/null 2>&1; then
|
||||
echo "context '$KUBECONTEXT' does not reach a cluster" >&2
|
||||
echo "bring one up: (cd .. && make cluster up)" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_metallb() {
|
||||
if $KCTX get deployment -n metallb-system controller >/dev/null 2>&1; then
|
||||
echo "metallb: present"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Only kind needs it. On a real cluster the cloud load balancer answers a
|
||||
# `type: LoadBalancer` Service, and installing MetalLB there would be wrong.
|
||||
case "$KUBECONTEXT" in
|
||||
kind-*) ;;
|
||||
*)
|
||||
echo "metallb: skipped — '$KUBECONTEXT' is not a kind context"
|
||||
echo " (a cloud load balancer answers LoadBalancer services there)"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ ! -f "$RIG_CTRL/addons/metallb.sh" ]; then
|
||||
echo "metallb is not installed and rig's addon script was not found at" >&2
|
||||
echo " $RIG_CTRL/addons/metallb.sh" >&2
|
||||
echo "a Service of type LoadBalancer will sit at <pending> without it." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# rig's addons derive their target cluster from RIG'S OWN folder name via
|
||||
# load_config, so left alone this bundle would install into `kind-rig` —
|
||||
# a cluster that need not exist — while deploying everything else into the
|
||||
# context actually selected. CLUSTER is in load_config's overridable set,
|
||||
# so passing it here points the addon at the same cluster we are using.
|
||||
local target="${KUBECONTEXT#kind-}"
|
||||
echo "metallb: installing via rig's addon into '$target'"
|
||||
CLUSTER="$target" bash "$RIG_CTRL/addons/metallb.sh"
|
||||
}
|
||||
|
||||
# Regenerate the artifact. No cluster and no kubectl required — this is the step
|
||||
# a staging workstation runs before anything is installed.
|
||||
manifest() {
|
||||
mkdir -p generated
|
||||
python3 ctrl/manifest.py "$NS" > "$ARTIFACT"
|
||||
echo "wrote $ARTIFACT ($(wc -l < "$ARTIFACT") lines)"
|
||||
echo " applies as-is anywhere: kubectl apply -f ${BUNDLE_ROOT}/${ARTIFACT}"
|
||||
}
|
||||
|
||||
up() {
|
||||
manifest
|
||||
init_kube
|
||||
require_cluster
|
||||
ensure_metallb
|
||||
|
||||
echo
|
||||
echo "applying '${NS}' to context '${KUBECONTEXT}'"
|
||||
$KCTX apply -f "$ARTIFACT"
|
||||
|
||||
# `rollout status` does not work on a bare Pod — it only understands
|
||||
# Deployments, StatefulSets and DaemonSets. Wait on the condition instead.
|
||||
# This is the slow step: the container npm-installs before vite serves.
|
||||
echo "waiting for the pod to be ready (npm install runs first)..."
|
||||
$K wait --for=condition=Ready pod/rig-ui --timeout=300s
|
||||
echo
|
||||
url
|
||||
}
|
||||
|
||||
down() {
|
||||
init_kube
|
||||
# Delete the namespace and everything in it goes with it. Scoped to THIS
|
||||
# rig — a sibling rig in the same cluster is untouched.
|
||||
$KCTX delete namespace "$NS" --ignore-not-found
|
||||
echo "'${NS}' removed (cluster, metallb and any sibling rig are left alone)"
|
||||
}
|
||||
|
||||
status() {
|
||||
init_kube
|
||||
require_cluster
|
||||
if ! $KCTX get namespace "$NS" >/dev/null 2>&1; then
|
||||
echo "'${NS}' is not deployed — run: make up"
|
||||
return 0
|
||||
fi
|
||||
$K get pod,svc,configmap -o wide
|
||||
}
|
||||
|
||||
# Every rig in this cluster, not just this one — the point of the namespace
|
||||
# split is that several coexist, so there has to be a way to see them together.
|
||||
list() {
|
||||
init_kube
|
||||
require_cluster
|
||||
local names
|
||||
names=$($KCTX get namespace -l rig.bundle/name \
|
||||
-o jsonpath='{.items[*].metadata.name}' 2>/dev/null || true)
|
||||
if [ -z "$names" ]; then
|
||||
echo "no rigs deployed in context '${KUBECONTEXT}'"
|
||||
return 0
|
||||
fi
|
||||
printf "%-20s %-16s %s\n" RIG ADDRESS ""
|
||||
local n ip
|
||||
for n in $names; do
|
||||
ip=$($KCTX -n "$n" get svc rig-ui \
|
||||
-o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || true)
|
||||
printf "%-20s %-16s %s\n" "$n" "${ip:-<pending>}" \
|
||||
"$([ "$n" = "$NS" ] && echo '<- this one')"
|
||||
done
|
||||
}
|
||||
|
||||
# The address MetalLB (or a cloud load balancer) assigned. <pending> here is the
|
||||
# classic silent failure: everything reports healthy and nothing is reachable.
|
||||
url() {
|
||||
init_kube
|
||||
local ip
|
||||
ip=$($K get svc rig-ui \
|
||||
-o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || true)
|
||||
if [ -z "$ip" ]; then
|
||||
ip=$($K get svc rig-ui \
|
||||
-o jsonpath='{.status.loadBalancer.ingress[0].hostname}' 2>/dev/null || true)
|
||||
fi
|
||||
if [ -z "$ip" ]; then
|
||||
echo "no external address yet — nothing has assigned one."
|
||||
echo "on kind: kubectl --context $KUBECONTEXT -n metallb-system get pods"
|
||||
return 1
|
||||
fi
|
||||
echo "IT WORKS -> http://${ip}/"
|
||||
echo " bundle http://${ip}/bundle.json"
|
||||
}
|
||||
|
||||
# Run the UI locally with no cluster at all — the fast way to iterate on
|
||||
# bundle.json. Same vite command the pod runs, so what you see here is what
|
||||
# gets served there.
|
||||
dev() {
|
||||
if ! command -v npm >/dev/null 2>&1; then
|
||||
echo "npm not found — the UI needs node locally for this." >&2
|
||||
echo "(in-cluster it runs on the node:22-alpine image instead)" >&2
|
||||
exit 1
|
||||
fi
|
||||
# bundle.json lives one level up so it stays the rig's data rather than the
|
||||
# app's; vite serves public/ at the root, which is where the app fetches it.
|
||||
mkdir -p rig-ui/public
|
||||
cp bundle.json rig-ui/public/bundle.json
|
||||
|
||||
# The mocked cluster is a DEMO asset and is deliberately not embedded in the
|
||||
# deployed artifact — on a real rig the UI would then show canned values
|
||||
# beside a live cluster, which is precisely the lie its banner warns about.
|
||||
# It is served here, and in the static build for the public UI-only page.
|
||||
cp cluster.mock.json rig-ui/public/cluster.mock.json
|
||||
|
||||
cd rig-ui
|
||||
[ -d node_modules ] || npm install --no-audit --no-fund
|
||||
VITE_RIG_NAME="$NS" npm run dev
|
||||
}
|
||||
|
||||
case "${1:-status}" in
|
||||
manifest) manifest ;;
|
||||
up) up ;;
|
||||
down) down ;;
|
||||
status) status ;;
|
||||
url) url ;;
|
||||
list) list ;;
|
||||
dev) dev ;;
|
||||
*) echo "usage: $0 [manifest|up|down|status|url|list|dev]" >&2; exit 1 ;;
|
||||
esac
|
||||
@@ -1,141 +0,0 @@
|
||||
#!/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))
|
||||
@@ -1,406 +0,0 @@
|
||||
# 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
|
||||
3
rig/sample-rig/rig-ui/.gitignore
vendored
3
rig/sample-rig/rig-ui/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
node_modules/
|
||||
public/
|
||||
dist/
|
||||
@@ -1,12 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,108 +0,0 @@
|
||||
# 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
|
||||
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: __RIG_NAME__
|
||||
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
|
||||
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
|
||||
1164
rig/sample-rig/rig-ui/package-lock.json
generated
1164
rig/sample-rig/rig-ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
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.
|
||||
*
|
||||
* Laid out like soleprint's templated vein pages, because it does the same job:
|
||||
* name each component, list what it exposes, show what comes back. Tool chrome
|
||||
* and output are styled apart on purpose (see style.css) — that separation is
|
||||
* what tells you whether you are reading the tool or its result.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const esc = (s) =>
|
||||
String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
|
||||
const tag = (text, on = false) =>
|
||||
`<span class="tag${on ? " on" : ""}">${esc(text)}</span>`;
|
||||
|
||||
/* Tool chrome: one bordered card per component. */
|
||||
function components(list, activeKey) {
|
||||
if (!list?.length)
|
||||
return `<div class="component"><p>nothing listed</p></div>`;
|
||||
return list
|
||||
.map((it) => {
|
||||
const tags = [
|
||||
it.standalone ? tag("standalone") : "",
|
||||
it.state ? tag(it.state) : "",
|
||||
activeKey && it[activeKey] ? tag("active", true) : "",
|
||||
].join("");
|
||||
return `<div class="component">
|
||||
<h4>${esc(it.name ?? "?")} ${tags}</h4>
|
||||
<p>${esc(it.summary ?? "")}</p>
|
||||
</div>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
/* Endpoint rows: path on the left, what it returns on the right. */
|
||||
function endpoints(list) {
|
||||
return list
|
||||
.map(
|
||||
(e) => `<li><code>${esc(e.path)}</code>
|
||||
<span class="desc">${esc(e.desc)}</span></li>`
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
/* Output: what the endpoint above actually returns, so the page demonstrates
|
||||
itself rather than describing what a demonstration would look like. */
|
||||
function example(bundle) {
|
||||
const sample = {
|
||||
bundle: bundle.bundle?.name,
|
||||
tools: (bundle.tools ?? []).map((t) => t.name),
|
||||
rigs: (bundle.rigs ?? []).map((r) => r.name),
|
||||
};
|
||||
return `<pre class="output">${esc(JSON.stringify(sample, null, 2))}</pre>`;
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
function clusterSection(c) {
|
||||
if (!c) return "";
|
||||
const m = c.cluster ?? {};
|
||||
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>
|
||||
${c.mocked ? `<p class="mock">mocked — no cluster was queried; these are canned values</p>` : ""}
|
||||
${meta ? `<p class="tagline">${esc(meta)}</p>` : ""}
|
||||
<div class="components">${components(c.workloads)}</div>
|
||||
|
||||
<h2>Services</h2>
|
||||
<div class="components">${components(c.services)}</div>`;
|
||||
}
|
||||
|
||||
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="tagline">${esc(meta.description ?? "")}</p>
|
||||
|
||||
<h2>Tools (${b.tools?.length ?? 0})</h2>
|
||||
<div class="components">${components(b.tools)}</div>
|
||||
|
||||
<h2>Rigs (${b.rigs?.length ?? 0})</h2>
|
||||
<div class="components">${components(b.rigs, "active")}</div>
|
||||
|
||||
<h2>Endpoints</h2>
|
||||
<ul class="endpoints">${endpoints([
|
||||
{ path: "/", desc: "this page" },
|
||||
{ path: "/bundle.json", desc: "the manifest it renders" },
|
||||
])}</ul>
|
||||
|
||||
<h2>Example — GET /bundle.json</h2>
|
||||
${example(b)}
|
||||
|
||||
${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="tagline">${esc(err.message)}</p>`;
|
||||
});
|
||||
@@ -1,139 +0,0 @@
|
||||
/* Minimal and self-contained — no framework dependency.
|
||||
*
|
||||
* The visual language follows soleprint's templated vein pages, because this
|
||||
* page does the same job: say what a component is, list what it exposes, and
|
||||
* show what comes back. Two treatments, deliberately distinct:
|
||||
*
|
||||
* TOOL CHROME bordered cards on the darker background, accent-coloured
|
||||
* titles, endpoint rows separated by rules.
|
||||
* OUTPUT a lighter raised block, monospace, pre-wrap and selectable —
|
||||
* it is data, not furniture, and should read as a payload.
|
||||
*
|
||||
* Keeping them apart matters more than either looks: it is what tells you at a
|
||||
* glance whether you are reading the tool or the thing it produced. */
|
||||
|
||||
:root {
|
||||
--bg: #0d0d0f;
|
||||
--surface: #16161a;
|
||||
--surface-raised: #1e1e24;
|
||||
--border: #2e2e38;
|
||||
--border-strong: #3d3d4a;
|
||||
--text: #e8e8f0;
|
||||
--muted: #8888a0;
|
||||
--accent: #3ecf8e;
|
||||
--accent-dim: #f5a623;
|
||||
--mono: "JetBrains Mono", "Cascadia Mono", Consolas, ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 2.5rem 1.5rem;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font: 14px/1.6 var(--mono);
|
||||
}
|
||||
main { max-width: 56rem; margin: 0 auto; }
|
||||
|
||||
h1 { margin: 0; font-size: 1.6rem; letter-spacing: 0.02em; }
|
||||
h1 .ok { color: var(--accent); }
|
||||
h1.err { color: #f06565; }
|
||||
.tagline { color: var(--muted); margin: 0.35rem 0 2.25rem; }
|
||||
|
||||
h2 {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--muted);
|
||||
margin: 2.25rem 0 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── tool chrome ────────────────────────────────────────────────────────── */
|
||||
|
||||
.components {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.component {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
.component h4 {
|
||||
margin: 0 0 0.25rem;
|
||||
font-size: 0.95rem;
|
||||
color: var(--accent);
|
||||
}
|
||||
.component p {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.endpoints { list-style: none; margin: 0; padding: 0; }
|
||||
.endpoints li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
padding: 0.6rem 0;
|
||||
border-bottom: 1px solid var(--border-strong);
|
||||
}
|
||||
.endpoints li:last-child { border-bottom: none; }
|
||||
.endpoints code {
|
||||
background: var(--surface);
|
||||
color: var(--accent);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.endpoints .desc { color: var(--muted); font-size: 0.9rem; }
|
||||
|
||||
.tag {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.1rem 0.45rem;
|
||||
border-radius: 3px;
|
||||
background: var(--border);
|
||||
color: var(--muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tag.on { background: var(--accent); color: var(--bg); }
|
||||
|
||||
/* ── output ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
.output {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
font-size: 0.85rem;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
user-select: text;
|
||||
color: var(--text);
|
||||
margin: 0;
|
||||
}
|
||||
.output .k { color: var(--accent); }
|
||||
|
||||
/* Mocked-data banner. Loud on purpose: it only appears when the 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 var(--accent-dim);
|
||||
border-radius: 6px;
|
||||
color: var(--accent-dim);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.next {
|
||||
color: #555568;
|
||||
font-size: 0.8rem;
|
||||
margin-top: 2.5rem;
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
.next ul { margin: 0; padding: 0 0 0 1.1rem; }
|
||||
.next li { list-style: disc; padding: 0.15rem 0; }
|
||||
@@ -1,9 +0,0 @@
|
||||
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 },
|
||||
});
|
||||
Reference in New Issue
Block a user