self tests
This commit is contained in:
4
rig/.gitignore
vendored
4
rig/.gitignore
vendored
@@ -9,7 +9,9 @@ ctrl/.env
|
|||||||
# generated: the .dot is a build artifact rendered from arch/*.json, never hand-edited.
|
# generated: the .dot is a build artifact rendered from arch/*.json, never hand-edited.
|
||||||
# The .svg IS committed — onboarding material should render in a repo browser.
|
# The .svg IS committed — onboarding material should render in a repo browser.
|
||||||
arch/*.dot
|
arch/*.dot
|
||||||
ctrl/Tiltfile.gen
|
# ctrl/Tiltfile.gen was here for a generator that no longer exists. ctrl/Tiltfile
|
||||||
|
# is now a real, committed file that derives its values when Tilt parses it, so
|
||||||
|
# there is nothing generated to ignore.
|
||||||
|
|
||||||
# binaries pulled by `make deps-bundle` for the air-gapped installer image
|
# binaries pulled by `make deps-bundle` for the air-gapped installer image
|
||||||
vendor
|
vendor
|
||||||
|
|||||||
@@ -246,9 +246,19 @@ Reachability is a plain kind port mapping: no ingress controller and no MetalLB.
|
|||||||
Caddy maps `<slug>.local.ar` onto the host port (`~/wdir/semester/ppl/local/Caddyfile`),
|
Caddy maps `<slug>.local.ar` onto the host port (`~/wdir/semester/ppl/local/Caddyfile`),
|
||||||
with `*.local.ar` resolving to 127.0.0.1 through dnsmasq. That is the whole chain.
|
with `*.local.ar` resolving to 127.0.0.1 through dnsmasq. That is the whole chain.
|
||||||
|
|
||||||
**The one file the scaffold still does not ship is `ctrl/Tiltfile`** — `make
|
**For `ctrl/Tiltfile`, copy rig's** rather than a live project's. rig ships one
|
||||||
tilt-up` runs `cd ctrl && tilt up`, and there is nothing to run until you write
|
that derives its cluster, context, ports and manifest directory from
|
||||||
one. Copy it from a live project; `unt` and `nvi` are closest to the plain shape.
|
`ctrl/ports.sh active` instead of hardcoding a slug, and carries a catalogue of
|
||||||
|
the blocks every project here ends up needing. Copying from `unt` or `nvi` is
|
||||||
|
what the estate did until now, and it is why the same Tiltfile preamble exists
|
||||||
|
in six places with the slug typed in by hand five times each.
|
||||||
|
|
||||||
|
> **Two things in this document disagree with rig and are not settled.** It
|
||||||
|
> mandates Tilt ports in `10300–10399`, while rig derives a block from the
|
||||||
|
> directory name at `20000+` so copies cannot collide — a rig-managed project
|
||||||
|
> takes rig's. And it names `ctrl/k8s/.env.example`, which is the `broad`
|
||||||
|
> scaffold's layout; rig's is `ctrl/.env.example`. Both are this document
|
||||||
|
> describing the house scaffold from inside rig's tree.
|
||||||
|
|
||||||
|
|
||||||
## Run it
|
## Run it
|
||||||
|
|||||||
58
rig/Makefile
58
rig/Makefile
@@ -16,10 +16,26 @@
|
|||||||
# Identity follows the FOLDER NAME, so this directory can be copied elsewhere,
|
# Identity follows the FOLDER NAME, so this directory can be copied elsewhere,
|
||||||
# renamed, and run as a separate environment with no edits. ctrl/.env overrides
|
# renamed, and run as a separate environment with no edits. ctrl/.env overrides
|
||||||
# it when you want a name that differs from the directory.
|
# it when you want a name that differs from the directory.
|
||||||
|
#
|
||||||
|
# Asked once, of ctrl/ports.sh, which resolves it through lib/config.sh:
|
||||||
|
#
|
||||||
|
# CLUSTER KUBECONTEXT HTTP HTTPS TILT REGISTRY MANIFESTS_DIR
|
||||||
|
#
|
||||||
|
# Read positionally, so the order is a contract — ctrl/selftest.sh pins it.
|
||||||
|
#
|
||||||
|
# This used to be sed over ctrl/.env plus a slug computed here, which is a
|
||||||
|
# SECOND derivation of values lib/config.sh already owns — and the two could
|
||||||
|
# disagree about the port after `make ports persist`, or about the name for any
|
||||||
|
# directory whose sanitised form differs from its raw one. One source now; the
|
||||||
|
# Tiltfile reads the same line.
|
||||||
|
FACTS := $(shell bash ctrl/ports.sh active 2>/dev/null)
|
||||||
SLUG := $(shell echo '$(notdir $(CURDIR))' | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-' | sed 's/^-*//; s/-*$$//')
|
SLUG := $(shell echo '$(notdir $(CURDIR))' | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-' | sed 's/^-*//; s/-*$$//')
|
||||||
CLUSTER := $(or $(shell sed -n 's/^CLUSTER=//p' ctrl/.env 2>/dev/null),$(SLUG))
|
# The fallback matters: ports.sh sources config.sh, and if a profile or .env is
|
||||||
KCTX := --context kind-$(CLUSTER)
|
# broken it exits non-zero. Losing the cluster name would send --context to the
|
||||||
TILT_PORT := $(shell sed -n 's/^TILT_PORT=//p' ctrl/.env 2>/dev/null)
|
# wrong place, so fall back to the folder rather than to empty.
|
||||||
|
CLUSTER := $(or $(word 1,$(FACTS)),$(SLUG))
|
||||||
|
KCTX := --context $(or $(word 2,$(FACTS)),kind-$(SLUG))
|
||||||
|
TILT_PORT := $(word 5,$(FACTS))
|
||||||
DEPSIMG := $(SLUG)-deps
|
DEPSIMG := $(SLUG)-deps
|
||||||
|
|
||||||
# Words after the target become the script's subcommand. Make would otherwise
|
# Words after the target become the script's subcommand. Make would otherwise
|
||||||
@@ -35,7 +51,7 @@ $(eval $(ARGS):;@:)
|
|||||||
.PHONY: $(ARGS)
|
.PHONY: $(ARGS)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: help setup check mem deps deps-image pins cluster registry addons ports \
|
.PHONY: help setup check selftest mem deps deps-image pins cluster registry addons ports \
|
||||||
newbox dockerhost docs tilt \
|
newbox dockerhost docs tilt \
|
||||||
kind-up kind-down kind-reset tilt-up tilt-down
|
kind-up kind-down kind-reset tilt-up tilt-down
|
||||||
|
|
||||||
@@ -50,6 +66,12 @@ setup: ## prepare this machine [core] [--share-docker]
|
|||||||
check: ## is this machine ready? reports, never fixes
|
check: ## is this machine ready? reports, never fixes
|
||||||
bash ctrl/check.sh
|
bash ctrl/check.sh
|
||||||
|
|
||||||
|
# The counterpart to check: that one asks about the MACHINE and never fails,
|
||||||
|
# this one asks about RIG and exits 1, the way pins does. The checks are written
|
||||||
|
# as the decisions they defend, so a failure names what is being undone.
|
||||||
|
selftest: ## does rig still do what it says? exits 1 if not
|
||||||
|
bash ctrl/selftest.sh
|
||||||
|
|
||||||
mem: ## memory, and any cap holding it [status|backup|restore]
|
mem: ## memory, and any cap holding it [status|backup|restore]
|
||||||
bash ctrl/mem.sh $(or $(ARGS),status)
|
bash ctrl/mem.sh $(or $(ARGS),status)
|
||||||
|
|
||||||
@@ -91,11 +113,16 @@ dockerhost: ## share Docker between distros [status|share|un
|
|||||||
docs: ## documentation [serve|graphs] (default serve)
|
docs: ## documentation [serve|graphs] (default serve)
|
||||||
bash ctrl/docs.sh $(or $(ARGS),serve)
|
bash ctrl/docs.sh $(or $(ARGS),serve)
|
||||||
|
|
||||||
# --port is only passed when TILT_PORT is actually set. It comes from ctrl/.env,
|
# --port is only passed when TILT_PORT resolved. It normally does, since FACTS
|
||||||
# which does NOT carry it by default — ports are derived at runtime in
|
# above asks ports.sh — but ports.sh can fail on a broken profile, and without
|
||||||
# lib/config.sh unless `make ports persist` has written them. Without the guard
|
# the guard tilt receives a bare `--port` with no value and fails on the flag
|
||||||
# tilt receives a bare `--port` with no value and fails on the flag rather than
|
# rather than on anything real. Tilt's own default is 10350, which is the number
|
||||||
# on anything real. `make ports show` prints the derived block.
|
# every project on this machine is trying not to collide on, so falling back to
|
||||||
|
# it silently is worse than not passing the flag.
|
||||||
|
#
|
||||||
|
# The Tiltfile asks ports.sh for the rest itself — cluster, registry and where
|
||||||
|
# the manifests are — so nothing needs passing here beyond what tilt's own flags
|
||||||
|
# require.
|
||||||
tilt: ## dev loop [up|down] (default up)
|
tilt: ## dev loop [up|down] (default up)
|
||||||
cd ctrl && tilt $(or $(ARGS),up) $(KCTX) $(if $(filter down,$(ARGS)),,$(if $(TILT_PORT),--port $(TILT_PORT)))
|
cd ctrl && tilt $(or $(ARGS),up) $(KCTX) $(if $(filter down,$(ARGS)),,$(if $(TILT_PORT),--port $(TILT_PORT)))
|
||||||
|
|
||||||
@@ -110,6 +137,11 @@ tilt: ## dev loop [up|down] (default
|
|||||||
#
|
#
|
||||||
# `cluster list` and `cluster free` have no hyphenated twin on purpose — they
|
# `cluster list` and `cluster free` have no hyphenated twin on purpose — they
|
||||||
# are rig's own, with nothing to be consistent with.
|
# are rig's own, with nothing to be consistent with.
|
||||||
|
#
|
||||||
|
# Nothing outside this file reads these names: the script is `ctrl/cluster.sh`
|
||||||
|
# and it takes the verb. So rename them, delete the ones you never type, or add
|
||||||
|
# the spelling your own projects use — an alias is two lines, and adding one
|
||||||
|
# costs nothing but a line in .PHONY above.
|
||||||
|
|
||||||
kind-up: ## alias for `cluster up`
|
kind-up: ## alias for `cluster up`
|
||||||
bash ctrl/cluster.sh up
|
bash ctrl/cluster.sh up
|
||||||
@@ -120,10 +152,10 @@ kind-down: ## alias for `cluster down`
|
|||||||
kind-reset: ## alias for `cluster reset`
|
kind-reset: ## alias for `cluster reset`
|
||||||
bash ctrl/cluster.sh reset
|
bash ctrl/cluster.sh reset
|
||||||
|
|
||||||
# These two match the other projects' spelling, but rig has no Tiltfile — there
|
# These two match the other projects' spelling. rig ships ctrl/Tiltfile, so they
|
||||||
# is nothing to run yet, and they fail the same way `make tilt` does.
|
# run — it deploys the examples in k8s/base until you replace them.
|
||||||
tilt-up: ## alias for `tilt up` (rig has no Tiltfile yet)
|
tilt-up: ## alias for `tilt up`
|
||||||
cd ctrl && tilt up $(KCTX) $(if $(TILT_PORT),--port $(TILT_PORT))
|
cd ctrl && tilt up $(KCTX) $(if $(TILT_PORT),--port $(TILT_PORT))
|
||||||
|
|
||||||
tilt-down: ## alias for `tilt down` (rig has no Tiltfile yet)
|
tilt-down: ## alias for `tilt down`
|
||||||
cd ctrl && tilt down $(KCTX)
|
cd ctrl && tilt down $(KCTX)
|
||||||
|
|||||||
@@ -87,11 +87,33 @@ make cluster free # stop the others if memory is tight
|
|||||||
make cluster down # remove this cluster and its registry
|
make cluster down # remove this cluster and its registry
|
||||||
```
|
```
|
||||||
|
|
||||||
**`make tilt` has nothing to run yet.** The target and its `tilt-up` / `tilt-down`
|
**The verbs are yours to change.** `cluster` is the script — `ctrl/cluster.sh` —
|
||||||
aliases exist so rig answers to the same spelling as every other project here,
|
and every spelling above is a `Makefile` target that calls it. `make kind-up` is
|
||||||
but rig ships no `Tiltfile` — it builds the estate, it is not itself a service
|
an alias for `make cluster up`, kept because the other projects on this machine
|
||||||
with a dev loop. Add a `ctrl/Tiltfile` and the target works; until then it fails
|
answer to that spelling and muscle memory spans repos rather than stopping at
|
||||||
on the missing file, not on anything rig did.
|
one. Nothing outside the `Makefile` reads these names, so rename them, drop the
|
||||||
|
ones you never type, or add whatever your own projects already say: each alias
|
||||||
|
is two lines at the bottom of the file, calling the same script the canonical
|
||||||
|
target does.
|
||||||
|
|
||||||
|
**`make tilt` works on a fresh copy, unedited.** rig ships `ctrl/Tiltfile`, and
|
||||||
|
`k8s/base` already boots, so the dev loop comes up with the two examples running
|
||||||
|
and nothing to configure first.
|
||||||
|
|
||||||
|
It hardcodes nothing. It asks `ctrl/ports.sh active` for this environment's
|
||||||
|
cluster name, kube context, ports and manifest directory — the same values every
|
||||||
|
other rig script resolves through `ctrl/lib/config.sh` — so a copied and renamed
|
||||||
|
rig deploys into its own cluster with no edits. Every other project here writes
|
||||||
|
its slug into the Tiltfile five or six times by hand, which is exactly the
|
||||||
|
collision `kind-config.yaml.tpl` exists to avoid.
|
||||||
|
|
||||||
|
What it deploys is `MANIFESTS_DIR`, defaulting to rig's own `ctrl/k8s/overlays/dev`.
|
||||||
|
Point that at an overlay versioned elsewhere and rig stops owning the manifests.
|
||||||
|
|
||||||
|
Replace the examples, then add your images and resources in the two marked
|
||||||
|
sections. The catalogue below them holds the blocks that recur across every
|
||||||
|
project here — `docker_build`, resource ordering, gateway reload, port-forwards —
|
||||||
|
with the parts that are easy to get wrong already commented.
|
||||||
|
|
||||||
`make help` lists every target.
|
`make help` lists every target.
|
||||||
|
|
||||||
|
|||||||
50
rig/ctrl/Dockerfile.example
Normal file
50
rig/ctrl/Dockerfile.example
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# EXAMPLE — a component image. Copy, rename, replace.
|
||||||
|
#
|
||||||
|
# Named like the manifest it feeds and the resource it becomes:
|
||||||
|
#
|
||||||
|
# ctrl/Dockerfile.api -> image <cluster>-api -> image: in k8s/base/api.yaml
|
||||||
|
#
|
||||||
|
# That image string is the ONLY thing connecting the three. Nothing checks it;
|
||||||
|
# a typo shows up as a pod stuck in ImagePullBackOff pulling from the public
|
||||||
|
# index, which reads like a network problem and is not one.
|
||||||
|
#
|
||||||
|
# ── the one that catches everyone ──────────────────────────────────────────
|
||||||
|
# The Tiltfile passes two paths with DIFFERENT bases, in adjacent arguments:
|
||||||
|
#
|
||||||
|
# context='..' the REPO ROOT (the Tiltfile is in ctrl/)
|
||||||
|
# dockerfile='Dockerfile.api' relative to the TILTFILE, so ctrl/Dockerfile.api
|
||||||
|
#
|
||||||
|
# So every COPY below is resolved against the repo root, NOT against this file's
|
||||||
|
# directory. A file sitting right beside this one is still reached as `ctrl/`:
|
||||||
|
#
|
||||||
|
# COPY ctrl/nginx.conf /etc/nginx/conf.d/default.conf # correct
|
||||||
|
# COPY nginx.conf /etc/nginx/conf.d/default.conf # fails — no such file
|
||||||
|
#
|
||||||
|
# Nothing warns you. The build just cannot find a file that is visibly there.
|
||||||
|
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Dependencies first, in their own layer: they change far less often than the
|
||||||
|
# code, so a source edit does not reinstall them on every rebuild.
|
||||||
|
COPY api/requirements.txt ./
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Repo-root relative — see above.
|
||||||
|
COPY api/ ./api/
|
||||||
|
|
||||||
|
# Match this with the containerPort in the manifest and the target of the
|
||||||
|
# Service in front of it.
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["python", "-m", "api"]
|
||||||
|
|
||||||
|
# ── live_update ────────────────────────────────────────────────────────────
|
||||||
|
# The sync in the Tiltfile's docker_build must land where this image expects it:
|
||||||
|
#
|
||||||
|
# live_update=[sync('../api', '/app/api')]
|
||||||
|
#
|
||||||
|
# matches `COPY api/ ./api/` with WORKDIR /app. If the two disagree, Tilt syncs
|
||||||
|
# into a path nothing reads and the container keeps serving the built copy —
|
||||||
|
# edits appear to do nothing, with no error anywhere.
|
||||||
139
rig/ctrl/Tiltfile
Normal file
139
rig/ctrl/Tiltfile
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# The dev loop. `make tilt` from the project root, or `cd ctrl && tilt up`.
|
||||||
|
#
|
||||||
|
# This file ships with rig and works unedited: rig's own k8s/base already boots,
|
||||||
|
# so `make tilt` comes up with a running cluster and no editing at all. What it
|
||||||
|
# deploys is two EXAMPLES — replace them, and add your own images and resources
|
||||||
|
# in the two marked sections near the bottom. The catalogue after them has the
|
||||||
|
# blocks to paste, with the parts that are easy to get wrong already commented.
|
||||||
|
#
|
||||||
|
# rig supplies this file; it does not own it. Nothing in rig reads it back, and
|
||||||
|
# nothing here is regenerated — edit it freely, the way you would edit
|
||||||
|
# k8s/base/example-mock.yaml. rig owns the machine, you own the workload.
|
||||||
|
#
|
||||||
|
# Nothing below is hardcoded to this directory, deliberately. Every other
|
||||||
|
# project here writes its slug into the Tiltfile five or six times by hand, so a
|
||||||
|
# copy of the project deploys into the original's cluster until someone
|
||||||
|
# remembers to edit all of them. A rig is meant to be copied and renamed, so it
|
||||||
|
# asks instead.
|
||||||
|
|
||||||
|
# ── who we are, and on which ports ─────────────────────────────────────────
|
||||||
|
# One question to rig, answered by ctrl/ports.sh, which resolves it through
|
||||||
|
# lib/config.sh — the same path every other rig script takes. That is the point:
|
||||||
|
# the cluster name is NOT the bare directory name (it is lowercased and reduced
|
||||||
|
# to a DNS label), and the ports honour anything pinned in ctrl/.env. Recomputing
|
||||||
|
# either of those here in Starlark is how two copies end up disagreeing about
|
||||||
|
# which cluster they are talking to.
|
||||||
|
_facts = str(local('bash ports.sh active', quiet=True)).split()
|
||||||
|
CLUSTER = _facts[0]
|
||||||
|
CTX = _facts[1]
|
||||||
|
HTTP = _facts[2]
|
||||||
|
HTTPS = _facts[3]
|
||||||
|
TILT = _facts[4]
|
||||||
|
REGISTRY = _facts[5]
|
||||||
|
|
||||||
|
# Where the manifests live. rig's own are the default; point MANIFESTS_DIR in
|
||||||
|
# ctrl/.env at an overlay versioned somewhere else and rig stops owning them —
|
||||||
|
# see k8s/README.md. Real manifests usually change on a different cadence, by
|
||||||
|
# different people, under different review.
|
||||||
|
#
|
||||||
|
# The value is REPO-ROOT relative, because that is the root everything else in
|
||||||
|
# rig is expressed against. This file runs in ctrl/, so prefix rather than
|
||||||
|
# assume: '../' + 'ctrl/k8s/overlays/dev' and '../' + '../platform/overlays/dev'
|
||||||
|
# are both right, where stripping a leading 'ctrl/' would only fix the first.
|
||||||
|
MANIFESTS = '../' + _facts[6]
|
||||||
|
|
||||||
|
# ── refuse to deploy into the wrong cluster ────────────────────────────────
|
||||||
|
# Tilt snapshots the kubectl context at startup, BEFORE parsing this file, so it
|
||||||
|
# cannot be switched from here — only refused. `make tilt` passes --context for
|
||||||
|
# you; this catches a bare `tilt up` after some other project moved the global
|
||||||
|
# context.
|
||||||
|
allow_k8s_contexts(CTX)
|
||||||
|
if k8s_context() != CTX:
|
||||||
|
fail("Wrong kubectl context: '%s'. This is %s — run: make tilt, or tilt up --context %s"
|
||||||
|
% (k8s_context(), CLUSTER, CTX))
|
||||||
|
|
||||||
|
# The namespace has to exist before anything lands in it, and kustomize does not
|
||||||
|
# guarantee ordering across resources. Creating it here is idempotent.
|
||||||
|
local('kubectl --context %s create namespace %s --dry-run=client -o yaml | kubectl --context %s apply -f -'
|
||||||
|
% (CTX, CLUSTER, CTX), quiet=True)
|
||||||
|
|
||||||
|
# ── images go to this environment's own registry ───────────────────────────
|
||||||
|
# Fail closed. Tilt can usually infer the kind registry on its own, but "usually"
|
||||||
|
# is an inference, and when it misses, an unqualified name like 'app' quietly
|
||||||
|
# means docker.io/library/app — a push to the public index instead of the
|
||||||
|
# registry two lines away. rig runs that registry; name it.
|
||||||
|
default_registry('localhost:' + REGISTRY)
|
||||||
|
|
||||||
|
k8s_yaml(kustomize(MANIFESTS))
|
||||||
|
|
||||||
|
# ── Images ─────────────────────────────────────────────────────────────────
|
||||||
|
# (nothing yet — rig's examples run upstream images. Add docker_build calls here.)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Resources ──────────────────────────────────────────────────────────────
|
||||||
|
# (nothing yet — add k8s_resource calls here to name and order what you deploy.)
|
||||||
|
|
||||||
|
|
||||||
|
# Everything with no dev loop of its own, gathered so it does not clutter the UI.
|
||||||
|
k8s_resource(
|
||||||
|
objects=[CLUSTER + ':namespace'],
|
||||||
|
new_name='infra',
|
||||||
|
)
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# Catalogue — paste what you need, delete the rest.
|
||||||
|
#
|
||||||
|
# These are the shapes that recur across every project here, with the reasoning
|
||||||
|
# kept next to them. They are comments so this file runs as-is.
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
#
|
||||||
|
# ── build an image ─────────────────────────────────────────────────────────
|
||||||
|
# The one genuinely non-obvious thing in the whole corpus: `context` and
|
||||||
|
# `dockerfile` are relative to DIFFERENT directories, in adjacent arguments,
|
||||||
|
# and nothing warns you.
|
||||||
|
#
|
||||||
|
# context= the REPO ROOT — this file is in ctrl/, so '..'
|
||||||
|
# dockerfile= relative to THIS file — so 'Dockerfile.api' is ctrl/Dockerfile.api
|
||||||
|
#
|
||||||
|
# Every COPY inside those Dockerfiles is therefore repo-root relative: a file
|
||||||
|
# sitting BESIDE the Dockerfile is still reached as `COPY ctrl/nginx.conf`.
|
||||||
|
#
|
||||||
|
# docker_build(
|
||||||
|
# CLUSTER + '-api', # must match `image:` in the manifest —
|
||||||
|
# context='..', # that string is the only thing
|
||||||
|
# dockerfile='Dockerfile.api', # connecting the two
|
||||||
|
# ignore=['.git', 'def', '.venv', 'node_modules', '__pycache__'],
|
||||||
|
# live_update=[sync('../api', '/app/api')],
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# ── name and order a resource ──────────────────────────────────────────────
|
||||||
|
# k8s_resource('api', resource_deps=['postgres'], labels=['app'])
|
||||||
|
# k8s_resource('gateway', resource_deps=['api', 'ui'], labels=['app'])
|
||||||
|
#
|
||||||
|
# ── reload the gateway when its config changes ─────────────────────────────
|
||||||
|
# A Caddyfile arriving via configMapGenerator with disableNameSuffixHash does
|
||||||
|
# NOT roll the pod — the ConfigMap name never changes, so nothing tells the
|
||||||
|
# Deployment anything happened. Without this you edit the routes and watch
|
||||||
|
# nothing take effect.
|
||||||
|
#
|
||||||
|
# local_resource(
|
||||||
|
# 'gateway-reload',
|
||||||
|
# cmd='kubectl --context %s -n %s rollout restart deployment/gateway' % (CTX, CLUSTER),
|
||||||
|
# deps=['k8s/base/Caddyfile'],
|
||||||
|
# resource_deps=['gateway'],
|
||||||
|
# auto_init=False,
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# ── an overlay whose secretGenerator reads outside its own directory ───────
|
||||||
|
# kustomize refuses to read above the kustomization root unless told to. Only
|
||||||
|
# add this if you actually have such a generator; it loosens a safety check.
|
||||||
|
#
|
||||||
|
# k8s_yaml(kustomize(MANIFESTS, flags=['--load-restrictor=LoadRestrictionsNone']))
|
||||||
|
#
|
||||||
|
# ── reach a service directly, bypassing the gateway ────────────────────────
|
||||||
|
# For a DB client or an admin UI. Prefer routing through the gateway: host ports
|
||||||
|
# are a single shared namespace across every project on this machine, which is
|
||||||
|
# why rig derives a block per environment in the first place. If you do need
|
||||||
|
# one, take it from this environment's own block rather than picking a number.
|
||||||
|
#
|
||||||
|
# k8s_resource('postgres', port_forwards=[str(int(HTTP) + 5) + ':5432'])
|
||||||
@@ -21,9 +21,14 @@
|
|||||||
# NODES and AUDIT are deliberately NOT here: they are properties of the chosen
|
# NODES and AUDIT are deliberately NOT here: they are properties of the chosen
|
||||||
# ctrl/k8s/kind-config*.yaml.tpl and are read back out of it below, so there is
|
# ctrl/k8s/kind-config*.yaml.tpl and are read back out of it below, so there is
|
||||||
# one place that decides the shape of the cluster rather than two that can drift.
|
# one place that decides the shape of the cluster rather than two that can drift.
|
||||||
|
#
|
||||||
|
# REGISTRY_PORT and MANIFESTS_DIR were missing here while ctrl/.env set them, so
|
||||||
|
# the caller's env silently LOST to the file for those two — the one precedence
|
||||||
|
# rule this header states. Both are now listed; the other twelve are unchanged.
|
||||||
CONFIG_OVERRIDABLE="PROFILE CLUSTER K8S_VERSION KIND_CONFIG ADDONS
|
CONFIG_OVERRIDABLE="PROFILE CLUSTER K8S_VERSION KIND_CONFIG ADDONS
|
||||||
REGISTRY_MODE INGRESS_MODE DNS_MODE TILT_PORT
|
REGISTRY_MODE INGRESS_MODE DNS_MODE TILT_PORT
|
||||||
SOURCE ARCH DEPS_SOURCE HTTP_PORT HTTPS_PORT"
|
SOURCE ARCH DEPS_SOURCE HTTP_PORT HTTPS_PORT
|
||||||
|
REGISTRY_PORT MANIFESTS_DIR"
|
||||||
|
|
||||||
# The containing folder's name, reduced to something kind accepts as a cluster
|
# The containing folder's name, reduced to something kind accepts as a cluster
|
||||||
# name (a DNS label: lowercase alphanumerics and dashes). Run from ctrl/, so the
|
# name (a DNS label: lowercase alphanumerics and dashes). Run from ctrl/, so the
|
||||||
@@ -93,6 +98,12 @@ load_config() {
|
|||||||
TILT_PORT="${TILT_PORT:-$((base + 2))}"
|
TILT_PORT="${TILT_PORT:-$((base + 2))}"
|
||||||
REGISTRY_PORT="${REGISTRY_PORT:-$((base + 3))}"
|
REGISTRY_PORT="${REGISTRY_PORT:-$((base + 3))}"
|
||||||
|
|
||||||
|
# Where the workload's manifests live, repo-root relative. Defaulted here so
|
||||||
|
# it is always resolved rather than sometimes-set: it is the seam that lets
|
||||||
|
# the real manifests be versioned away from the installer, and a consumer
|
||||||
|
# should not have to know whether anyone filled it in. See k8s/README.md.
|
||||||
|
MANIFESTS_DIR="${MANIFESTS_DIR:-ctrl/k8s/overlays/dev}"
|
||||||
|
|
||||||
# Profiles name a k8s minor (v1_36); versions.env holds the pinned digest.
|
# Profiles name a k8s minor (v1_36); versions.env holds the pinned digest.
|
||||||
local var="NODE_IMAGE_${K8S_VERSION}"
|
local var="NODE_IMAGE_${K8S_VERSION}"
|
||||||
NODE_IMAGE="${!var:-}"
|
NODE_IMAGE="${!var:-}"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
# written into ctrl/.env, so it becomes pinned, visible and editable rather than
|
# written into ctrl/.env, so it becomes pinned, visible and editable rather than
|
||||||
# a number that appears from nowhere. Anything already in ctrl/.env wins.
|
# a number that appears from nowhere. Anything already in ctrl/.env wins.
|
||||||
#
|
#
|
||||||
# Usage: ports.sh show | derive | persist
|
# Usage: ports.sh show | active | derive | persist
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
@@ -38,6 +38,33 @@ derive() {
|
|||||||
DERIVED_REGISTRY=$((base + 3))
|
DERIVED_REGISTRY=$((base + 3))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The resolved facts a consumer outside bash needs, machine-readable:
|
||||||
|
#
|
||||||
|
# CLUSTER KUBECONTEXT HTTP HTTPS TILT REGISTRY MANIFESTS_DIR
|
||||||
|
#
|
||||||
|
# Identity and ports together, because they are one fact set — the header above
|
||||||
|
# says so: both derive from the directory name so that copies never collide. A
|
||||||
|
# consumer needs all of them or none, and fetching them separately is how two
|
||||||
|
# end up disagreeing. MANIFESTS_DIR rides along because the one consumer that
|
||||||
|
# needs the addressing is the one that needs to know what to deploy.
|
||||||
|
#
|
||||||
|
# Space-separated, so MANIFESTS_DIR must not contain spaces. Everything else in
|
||||||
|
# rig already assumes that of paths — kind, docker and kubectl all do.
|
||||||
|
#
|
||||||
|
# `derive` answers a DIFFERENT question — what the directory name alone implies
|
||||||
|
# — and deliberately ignores ctrl/.env. Configuring anything from it would
|
||||||
|
# silently contradict this file's own rule that "anything already in ctrl/.env
|
||||||
|
# wins". `active` is what anything downstream should read.
|
||||||
|
#
|
||||||
|
# Why this exists at all: the cluster name is not the bare directory name.
|
||||||
|
# default_cluster_name() lowercases it and replaces every character outside
|
||||||
|
# [a-z0-9-], because it has to be a DNS label. Re-deriving that in another
|
||||||
|
# language is how a copy in `My_Project/` ends up guarding the wrong context.
|
||||||
|
active() {
|
||||||
|
load_config
|
||||||
|
echo "$CLUSTER $KUBECONTEXT $HTTP_PORT $HTTPS_PORT $TILT_PORT $REGISTRY_PORT $MANIFESTS_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
show() {
|
show() {
|
||||||
derive
|
derive
|
||||||
echo "environment $CLUSTER"
|
echo "environment $CLUSTER"
|
||||||
@@ -98,6 +125,7 @@ persist() {
|
|||||||
case "${1:-show}" in
|
case "${1:-show}" in
|
||||||
show) show ;;
|
show) show ;;
|
||||||
derive) derive; echo "$DERIVED_HTTP $DERIVED_HTTPS $DERIVED_TILT $DERIVED_REGISTRY" ;;
|
derive) derive; echo "$DERIVED_HTTP $DERIVED_HTTPS $DERIVED_TILT $DERIVED_REGISTRY" ;;
|
||||||
|
active) active ;;
|
||||||
persist) persist ;;
|
persist) persist ;;
|
||||||
*) echo "usage: $0 [show|derive|persist]" >&2; exit 1 ;;
|
*) echo "usage: $0 [show|active|derive|persist]" >&2; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
193
rig/ctrl/selftest.sh
Executable file
193
rig/ctrl/selftest.sh
Executable file
@@ -0,0 +1,193 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# What rig has settled, written down as assertions.
|
||||||
|
#
|
||||||
|
# These are documentation that runs. Each check is ONE decision that has already
|
||||||
|
# been made, with the reason above it — not coverage, and deliberately not an
|
||||||
|
# exhaustive sweep of use cases. rig's own index says a rule without its reason
|
||||||
|
# gets overridden the first time it is inconvenient; a rule nobody can restate
|
||||||
|
# is worse. So the test says what was decided, and failing it should read as
|
||||||
|
# "you are about to undo this" rather than "something broke".
|
||||||
|
#
|
||||||
|
# Scope, on purpose:
|
||||||
|
# - no cluster, no docker, no network. It must be cheap enough to actually run.
|
||||||
|
# - it asserts about RIG. `make check` asserts about the MACHINE and never
|
||||||
|
# fails; this exits 1, the way `make pins` does.
|
||||||
|
# - what actually deploys is not testable here. `tilt ci` stays a manual step.
|
||||||
|
#
|
||||||
|
# Usage: make selftest (or: bash ctrl/selftest.sh)
|
||||||
|
set -uo pipefail # NOT -e: one failing check must not abort the rest
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
source ./lib/config.sh
|
||||||
|
|
||||||
|
rc=0
|
||||||
|
passed=0
|
||||||
|
|
||||||
|
check() { # name, expected, actual
|
||||||
|
if [ "$2" = "$3" ]; then
|
||||||
|
printf ' ok %s\n' "$1"
|
||||||
|
passed=$((passed + 1))
|
||||||
|
else
|
||||||
|
printf ' FAIL %s\n expected: %s\n got: %s\n' "$1" "$2" "$3"
|
||||||
|
rc=1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
note() { printf '\n%s\n' "$1"; }
|
||||||
|
|
||||||
|
# Resolve one key the way every rig script does, in a clean shell so the
|
||||||
|
# caller's exported value is the only thing in play.
|
||||||
|
resolved() {
|
||||||
|
bash -c 'source ./lib/config.sh; load_config >/dev/null 2>&1; printf "%s" "${!1}"' _ "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
note "the ports.sh active contract"
|
||||||
|
# ports.sh active is read POSITIONALLY by two other files — the Makefile takes
|
||||||
|
# $(word 2) and $(word 5), the Tiltfile takes _facts[0]..[6]. Insert a field in
|
||||||
|
# the middle and nothing errors: Tilt simply guards on the wrong context or
|
||||||
|
# binds the wrong port. The field count and order are the contract, so they are
|
||||||
|
# pinned here rather than left to whoever edits ports.sh next.
|
||||||
|
FACTS="$(bash ports.sh active)"
|
||||||
|
check "active: exactly 7 fields" "7" "$(printf '%s' "$FACTS" | wc -w)"
|
||||||
|
read -r F_CLUSTER F_CTX F_HTTP F_HTTPS F_TILT F_REG F_MANIFESTS <<< "$FACTS"
|
||||||
|
check "active: field 2 is kind-<cluster>" "kind-$F_CLUSTER" "$F_CTX"
|
||||||
|
check "active: fields 3-6 are numeric" "yes" \
|
||||||
|
"$([[ "$F_HTTP$F_HTTPS$F_TILT$F_REG" =~ ^[0-9]+$ ]] && echo yes || echo no)"
|
||||||
|
check "active: field 7 is a path" "yes" \
|
||||||
|
"$([ -n "$F_MANIFESTS" ] && [ "${F_MANIFESTS#-}" = "$F_MANIFESTS" ] && echo yes || echo no)"
|
||||||
|
# derive answers a different question and must keep its own shape: it reports
|
||||||
|
# what the directory name implies, ignoring ctrl/.env, so nothing should
|
||||||
|
# configure itself from it.
|
||||||
|
check "derive: still 4 fields, not 7" "4" "$(bash ports.sh derive | wc -w)"
|
||||||
|
|
||||||
|
|
||||||
|
note "the caller's env beats the files"
|
||||||
|
# lib/config.sh states one precedence rule: versions.env < env.d/<profile> <
|
||||||
|
# ctrl/.env < the caller's env. It is enforced by CONFIG_OVERRIDABLE, a
|
||||||
|
# hand-maintained list — and a key missing from it loses to the file SILENTLY.
|
||||||
|
# REGISTRY_PORT and MANIFESTS_DIR were both missing on 2026-09-13 and were found
|
||||||
|
# by accident.
|
||||||
|
#
|
||||||
|
# So this loop is generated FROM the list: add a key to CONFIG_OVERRIDABLE and
|
||||||
|
# this test starts asking about it without anyone remembering to come here.
|
||||||
|
# Three keys name something that must exist and are validated at load, so they
|
||||||
|
# get a real alternative rather than a sentinel.
|
||||||
|
test_value() {
|
||||||
|
case "$1" in
|
||||||
|
PROFILE) echo "client" ;; # env.d/client.env exists
|
||||||
|
K8S_VERSION) echo "v1_35" ;; # NODE_IMAGE_v1_35 is pinned
|
||||||
|
KIND_CONFIG) echo "kind-config.client.yaml.tpl" ;; # the shape must exist
|
||||||
|
*_PORT) echo "19999" ;;
|
||||||
|
CLUSTER) echo "selftest-name" ;;
|
||||||
|
MANIFESTS_DIR) echo "../elsewhere/overlays/dev" ;;
|
||||||
|
ADDONS) echo "metallb" ;;
|
||||||
|
*) echo "selftest-sentinel" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
for key in $CONFIG_OVERRIDABLE; do
|
||||||
|
[ -n "$key" ] || continue
|
||||||
|
want="$(test_value "$key")"
|
||||||
|
if [ -z "$want" ]; then
|
||||||
|
check "precedence: $key has a test value" "yes" "no — add one to test_value()"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
got="$(export "$key=$want"; resolved "$key")"
|
||||||
|
check "precedence: caller's $key wins" "$want" "$got"
|
||||||
|
done
|
||||||
|
|
||||||
|
|
||||||
|
note "one derivation, not three"
|
||||||
|
# The Makefile used to compute the cluster name itself and sed TILT_PORT out of
|
||||||
|
# ctrl/.env — a second derivation of values lib/config.sh already owns, which
|
||||||
|
# could disagree with it after `make ports persist`. It now reads ports.sh
|
||||||
|
# active. Nothing structurally prevents the sed coming back, so the agreement is
|
||||||
|
# asserted against the real `make -n` output rather than against the source.
|
||||||
|
# --no-print-directory and a grep, not `tail -1`: run from `make selftest` this
|
||||||
|
# is a RECURSIVE make, and the "Entering/Leaving directory" lines go to STDOUT.
|
||||||
|
# tail -1 then reads "make[1]: Leaving directory ..." and both checks below fail
|
||||||
|
# — but only when invoked through make, never when the script is run directly.
|
||||||
|
# A test that passes one way and fails the other is worse than no test.
|
||||||
|
MK="$(cd .. && make --no-print-directory -n tilt 2>/dev/null | grep -m1 'tilt ')"
|
||||||
|
check "Makefile: --context comes from active" "$F_CTX" \
|
||||||
|
"$(printf '%s' "$MK" | sed -n 's/.*--context \([^ ]*\).*/\1/p')"
|
||||||
|
check "Makefile: --port comes from active" "$F_TILT" \
|
||||||
|
"$(printf '%s' "$MK" | sed -n 's/.*--port \([^ ]*\).*/\1/p')"
|
||||||
|
|
||||||
|
|
||||||
|
note "identity follows the folder, safely"
|
||||||
|
# The cluster name is NOT the bare directory name: kind needs a DNS label, so
|
||||||
|
# default_cluster_name lowercases it and replaces everything outside [a-z0-9-].
|
||||||
|
# Re-deriving that anywhere else is how a copy ends up guarding the wrong
|
||||||
|
# context — which is exactly why the Tiltfile asks instead of computing.
|
||||||
|
TMP="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
mkdir -p "$TMP/My_Proj"
|
||||||
|
cp -r . "$TMP/My_Proj/ctrl"
|
||||||
|
# A pinned CLUSTER in .env would be an override, not a derivation, and this
|
||||||
|
# check is about the derivation.
|
||||||
|
sed -i '/^CLUSTER=/d' "$TMP/My_Proj/ctrl/.env" 2>/dev/null
|
||||||
|
COPY="$(cd "$TMP/My_Proj/ctrl" && bash ports.sh active)"
|
||||||
|
check "a dir named My_Proj derives a DNS label" "my-proj" "$(awk '{print $1}' <<< "$COPY")"
|
||||||
|
check "and a context to match" "kind-my-proj" "$(awk '{print $2}' <<< "$COPY")"
|
||||||
|
check "a renamed copy gets a DIFFERENT block" "different" \
|
||||||
|
"$([ "$(awk '{print $3}' <<< "$COPY")" != "$F_HTTP" ] && echo different || echo COLLIDES)"
|
||||||
|
|
||||||
|
|
||||||
|
note "ports are stable across versions"
|
||||||
|
# Not a change-detector. The block is derived, never stored, so if the
|
||||||
|
# derivation shifts then every EXISTING environment's ports move underneath it —
|
||||||
|
# a running cluster keeps its old ports while rig starts reporting new ones, and
|
||||||
|
# `make ports` stops describing reality. Anchored to three known names.
|
||||||
|
check "derive_port_base rig" "20310" "$(derive_port_base rig)"
|
||||||
|
check "derive_port_base foo" "21690" "$(derive_port_base foo)"
|
||||||
|
check "derive_port_base my-proj" "21030" "$(derive_port_base my-proj)"
|
||||||
|
|
||||||
|
|
||||||
|
note "rig stays standalone"
|
||||||
|
# rig sits inside a host project's tree but must be copyable straight out of it:
|
||||||
|
# no imports, no paths, no assumption the host is there. This grep is the whole
|
||||||
|
# test of that claim, and until now it lived only in prose and in whoever
|
||||||
|
# remembered to run it.
|
||||||
|
#
|
||||||
|
# The pattern is assembled from fragments so this file does not match ITSELF.
|
||||||
|
# Writing it literally would fail forever; excluding this file instead would put
|
||||||
|
# a blind spot in the one check that guards the boundary.
|
||||||
|
HOST_PAT="$(printf '%s' 'sole' 'print' '|\b' 'sp' 'r\b')"
|
||||||
|
check "no host-project references" "0" \
|
||||||
|
"$(cd .. && grep -rIl -iE "$HOST_PAT" . --exclude-dir=def 2>/dev/null | wc -l)"
|
||||||
|
|
||||||
|
|
||||||
|
note "the Tiltfile hardcodes nothing"
|
||||||
|
# Every other Tiltfile on this machine writes its slug in five or six times by
|
||||||
|
# hand, so a copied project deploys into the original's cluster until someone
|
||||||
|
# edits all of them. rig's asks ports.sh. A literal kind-<name> here would mean
|
||||||
|
# that has been undone.
|
||||||
|
check "no literal kind-<name>" "0" "$(grep -cE "['\"]kind-[a-z0-9]" Tiltfile)"
|
||||||
|
check "guards on the variable" "1" "$(grep -c 'allow_k8s_contexts(CTX)' Tiltfile)"
|
||||||
|
check "asks ports.sh for facts" "1" "$(grep -c "local('bash ports.sh active'" Tiltfile)"
|
||||||
|
|
||||||
|
|
||||||
|
note "optional — needs tilt and this rig's cluster"
|
||||||
|
# Parsing the Tiltfile for real is the only way to know it still evaluates, but
|
||||||
|
# Tilt snapshots a kubectl context before parsing, so it cannot run without a
|
||||||
|
# cluster. Skipped rather than failed when there is none, the same way docgen
|
||||||
|
# skips its graphgen section.
|
||||||
|
if ! command -v tilt >/dev/null; then
|
||||||
|
printf ' skip tilt is not installed\n'
|
||||||
|
elif ! kubectl config get-contexts -o name 2>/dev/null | grep -qx "$F_CTX"; then
|
||||||
|
printf " skip no %s context — run 'make cluster up' to include this\n" "$F_CTX"
|
||||||
|
else
|
||||||
|
out="$(tilt alpha tiltfile-result --context "$F_CTX" 2>&1)"
|
||||||
|
check "Tiltfile evaluates" "yes" \
|
||||||
|
"$(printf '%s' "$out" | grep -q '"Manifests"' && echo yes || echo "no: $(printf '%s' "$out" | tail -1)")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
if [ "$rc" -eq 0 ]; then
|
||||||
|
printf '%d checks passed — rig still does what it says\n' "$passed"
|
||||||
|
else
|
||||||
|
printf 'FAILED — a decision above has drifted; read the comment next to it\n' >&2
|
||||||
|
fi
|
||||||
|
exit "$rc"
|
||||||
Reference in New Issue
Block a user