Compare commits
14 Commits
38c2cfe50f
...
rig-fold
| Author | SHA1 | Date | |
|---|---|---|---|
| 83b6cbebe3 | |||
| a65c92257d | |||
| 78595f1bd9 | |||
| 9bcd439266 | |||
| 74f03566f1 | |||
| 2d9bc9289c | |||
| 910927993e | |||
| 9a6337e493 | |||
| 0b04516cbb | |||
| ef63b02554 | |||
| 33f0559268 | |||
| dfb1991ae3 | |||
| 9fc4c23143 | |||
| 973f0a01c9 |
27
.gitattributes
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# Copied verbatim from rig/.gitattributes, and deliberately duplicated rather
|
||||
# than shared: rig/ must carry its own so it survives being handed over on its
|
||||
# own, and spr had none at all despite shipping ctrl/*.sh and generating
|
||||
# gen/<room>/ctrl/*.sh.
|
||||
#
|
||||
# Line endings are normalised to LF in the repository and on checkout, on every
|
||||
# platform. Without this, a checkout on Windows/WSL rewrites files to CRLF and
|
||||
# every one of them shows up as modified without anyone having touched it.
|
||||
#
|
||||
# For the scripts it is not cosmetic: a shell script with CRLF fails on Linux
|
||||
# with `bad interpreter: /usr/bin/env bash^M`, which reads as a broken installer
|
||||
# rather than a line-ending problem — the worst possible first impression on a
|
||||
# machine where nothing has been proven yet.
|
||||
* text=auto eol=lf
|
||||
|
||||
*.sh text eol=lf
|
||||
*.py text eol=lf
|
||||
*.env text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.yml text eol=lf
|
||||
|
||||
# Never touch binaries.
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.zip binary
|
||||
*.tar binary
|
||||
*.gz binary
|
||||
26
.gitignore
vendored
@@ -9,12 +9,38 @@ __pycache__/
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# Secrets. There was no rule here, which is how a real API key ended up tracked
|
||||
# in station/tools/tester/.env. Templates still ship.
|
||||
.env
|
||||
!.env.example
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
|
||||
# Built library bundles (regenerate with `pnpm build` in the package).
|
||||
# The dist is what a container boots; it is an artifact, not source.
|
||||
dist/
|
||||
|
||||
# Generated runnable instance (entirely gitignored - regenerate with build.py)
|
||||
gen/
|
||||
|
||||
# Specs and sheets uploaded through shuntgen's UI. Inputs someone dropped in a
|
||||
# browser, not source — and often a client's real data.
|
||||
soleprint/station/tools/shuntgen/uploads/
|
||||
|
||||
# Room configurations (separate repo - contains credentials and room-specific data)
|
||||
# Keep cfg/standalone/ and cfg/sample/ as templates, ignore actual rooms
|
||||
cfg/amar/
|
||||
cfg/dlt/
|
||||
# Add new rooms here as they are created
|
||||
# cfg/<room>/
|
||||
|
||||
# Client rigs. A rig is a copy of rig/ renamed after the environment it models,
|
||||
# so its k8s files spell out a real architecture — exactly the thing that must
|
||||
# not land here. They are versioned in their own repo.
|
||||
#
|
||||
# Anchored at the ROOT on purpose: a copy is a SIBLING of rig/, so a rule inside
|
||||
# rig/.gitignore cannot see it. The negation must name the full path for the same
|
||||
# reason — `*-rig/` is unanchored and matches at any depth, including rig/sample-rig.
|
||||
*-rig/
|
||||
!rig/sample-rig/
|
||||
|
||||
147
CLAUDE.md
@@ -51,20 +51,19 @@ spr/
|
||||
│ └── amar/ # Amar room config
|
||||
│ ├── config.json
|
||||
│ ├── data/
|
||||
│ ├── artery/ # Amar-specific (merged into output)
|
||||
│ │ └── shunts/amar/
|
||||
│ ├── atlas/ # Amar-specific books
|
||||
│ │ └── books/
|
||||
│ ├── station/ # Amar-specific tools config
|
||||
│ │ └── tools/datagen/
|
||||
│ ├── soleprint/ # Room overlay — merged over soleprint/ on build
|
||||
│ │ ├── artery/ # room shunts, pulses
|
||||
│ │ │ └── shunts/amar/
|
||||
│ │ ├── atlas/ # room books
|
||||
│ │ │ └── books/
|
||||
│ │ ├── station/ # room tool configs
|
||||
│ │ │ └── tools/datagen/
|
||||
│ │ └── nginx/
|
||||
│ ├── ctrl/ # Room lifecycle scripts (copied into gen/<room>/)
|
||||
│ ├── link/ # Bridge to managed app
|
||||
│ ├── soleprint/ # Soleprint docker config
|
||||
│ ├── databrowse/
|
||||
│ ├── tester/
|
||||
│ ├── monitors/
|
||||
│ └── models/
|
||||
│ └── amar/ # The managed app itself
|
||||
│
|
||||
├── ctrl/ # Build/run scripts
|
||||
├── ctrl/ # Build/run scripts (see Build & Run)
|
||||
│
|
||||
└── gen/ # Built instances (gitignored)
|
||||
├── standalone/
|
||||
@@ -100,26 +99,38 @@ Each room in `cfg/` has:
|
||||
- `config.json` - Framework branding/terminology
|
||||
- `data/` - Data files (veins.json, shunts.json, etc.)
|
||||
|
||||
Room-specific system configs (merged into output):
|
||||
- `artery/` - Room-specific shunts, pulses
|
||||
- `atlas/` - Room-specific books
|
||||
- `station/` - Room-specific tool configs (datagen, tester tests, etc.)
|
||||
Room-specific system configs live under `cfg/<room>/soleprint/` and are merged over
|
||||
the core `soleprint/` tree at build time:
|
||||
- `soleprint/artery/` - Room-specific shunts, pulses
|
||||
- `soleprint/atlas/` - Room-specific books
|
||||
- `soleprint/station/` - Room-specific tool configs (datagen generators, tester tests)
|
||||
|
||||
A room's own lifecycle scripts live in `cfg/<room>/ctrl/` and land in
|
||||
`gen/<room>/ctrl/` — that is what `make start <room>` dispatches to.
|
||||
|
||||
## Build & Run
|
||||
|
||||
`make` is the front door — one target per `ctrl/` script, with the subcommand as an
|
||||
argument (`make cluster up`, not `make cluster-up`). The logic lives in the scripts,
|
||||
never in the Makefile. `make help` lists every target.
|
||||
|
||||
```bash
|
||||
# Build
|
||||
python build.py # -> gen/standalone/
|
||||
python build.py --cfg amar # -> gen/amar/
|
||||
python build.py --all # -> all rooms
|
||||
make # = make help
|
||||
make build [room|all|models] # -> gen/<room>/ (default: standalone)
|
||||
make start [room] [-d] # dispatches to gen/<room>/ctrl/start.sh
|
||||
make stop [room]
|
||||
make cluster [up|down|status] # the shared `spr` kind cluster
|
||||
make component [list|sync|watch|publish|diff]
|
||||
make deploy [--build|--sync-only]
|
||||
```
|
||||
|
||||
# Run bare-metal
|
||||
cd gen/standalone && python run.py
|
||||
Every script stays runnable on its own — the standalone rule holds:
|
||||
|
||||
# Using ctrl scripts
|
||||
./ctrl/build.sh [room]
|
||||
./ctrl/start.sh [room] [-d]
|
||||
./ctrl/stop.sh [room]
|
||||
```bash
|
||||
python build.py --cfg amar # -> gen/amar/
|
||||
cd gen/standalone && python run.py # bare-metal
|
||||
./ctrl/kind-up.sh # still works directly
|
||||
cd gen/<room> && ./ctrl/start.sh # each room owns its lifecycle scripts
|
||||
```
|
||||
|
||||
## Adding a New Room
|
||||
@@ -129,12 +140,12 @@ mkdir -p cfg/newroom/data
|
||||
cp cfg/standalone/config.json cfg/newroom/
|
||||
cp -r cfg/standalone/data/* cfg/newroom/data/
|
||||
|
||||
# Add room-specific configs as needed:
|
||||
# cfg/newroom/artery/shunts/...
|
||||
# cfg/newroom/atlas/books/...
|
||||
# cfg/newroom/station/tools/...
|
||||
# Add room-specific configs as needed (note the soleprint/ overlay level):
|
||||
# cfg/newroom/soleprint/artery/shunts/...
|
||||
# cfg/newroom/soleprint/atlas/books/...
|
||||
# cfg/newroom/soleprint/station/tools/datagen/<name>.py
|
||||
|
||||
python build.py --cfg newroom
|
||||
make build newroom
|
||||
```
|
||||
|
||||
## Ports
|
||||
@@ -149,10 +160,51 @@ python build.py --cfg newroom
|
||||
|------|---------|
|
||||
| modelgen | Generate models from config |
|
||||
| datagen | Generate test data (uses faker) |
|
||||
| tester | BDD/playwright test runner |
|
||||
| tester | HTTP contract test runner |
|
||||
| graphgen | Generate navigable model graphs |
|
||||
| databrowse | SQL data browser |
|
||||
|
||||
## Shared UI (`soleprint/common/ui`)
|
||||
|
||||
Upstream copy of the `soleprint-ui` package. `mpr/ui/framework` and
|
||||
`meetus/ui/framework` are **downstream copies** synced by `ctrl/spr.py` — edit here,
|
||||
never there, or the next sync overwrites the change.
|
||||
|
||||
```bash
|
||||
cd soleprint/common/ui
|
||||
pnpm install && pnpm typecheck && pnpm test
|
||||
pnpm build # -> dist/soleprint-ui.js + dist/style.css
|
||||
```
|
||||
|
||||
**The theme ships with the bundle.** Every component styles itself with
|
||||
`var(--surface-0)` and friends from `src/tokens.css`, so `src/index.ts` imports
|
||||
`src/theme.css` (tokens + `base.css`). A bundle without it renders broken, not merely
|
||||
unbranded. Retheme by overriding the variables in `tokens.css`; don't inline colours
|
||||
in components.
|
||||
|
||||
`dist/` and `node_modules/` are gitignored — the dist is an artifact a container
|
||||
boots, not source. To hand someone a running artifact without the sources:
|
||||
|
||||
```bash
|
||||
python ctrl/spr.py publish soleprint-ui <dest> --dist # bundle + manifest only
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
**No test bodies are committed to core.** `soleprint/station/tools/tester/` ships the
|
||||
base class, runner and UI; tests belong to a room at
|
||||
`cfg/<room>/soleprint/station/tools/tester/tests/`.
|
||||
|
||||
Two rules, both in `tester/tests/test_template.py`:
|
||||
|
||||
1. **One suite, any environment** — a test states what the API promises, never who
|
||||
implements it or where it runs.
|
||||
2. **No helper framework tools** — stdlib `unittest` + `httpx`. No pytest fixtures,
|
||||
no framework test client, no factories, no ORM or database access.
|
||||
|
||||
Django, where it appears, is an optional private DB editor via its admin — never the
|
||||
framework, and never something a test reaches into.
|
||||
|
||||
## External Paths
|
||||
|
||||
| What | Path |
|
||||
@@ -161,6 +213,35 @@ python build.py --cfg newroom
|
||||
|
||||
## Files Ignored
|
||||
|
||||
- `gen/` - Regenerate with `python build.py`
|
||||
- `gen/` - Regenerate with `make build [room]`
|
||||
- `dist/` - Build artifact; regenerate with `pnpm build`
|
||||
- `node_modules/`
|
||||
- `fails/`, `def/` - Drafts
|
||||
- `__pycache__/`, `.venv/`
|
||||
|
||||
## Known Broken (found, not yet fixed)
|
||||
|
||||
- `tester/tests/example/test_health.py` imports `pytest`, which is neither a
|
||||
dependency nor allowed here — `tests/README.md` says stdlib `unittest` and
|
||||
`httpx`, no pytest. It fails to import, so `python -m tester discover` reports
|
||||
it as `_FailedTest`. It also duplicates `test_template.py`, which is the
|
||||
sanctioned example. Removing it is probably the fix; core ships no tests.
|
||||
- Old vocabulary survives in prose — `album`, `larder`, `ward`, `nest` and
|
||||
`pawprint` still appear in READMEs, `docs/`, and this file's history. Live code
|
||||
is clean; the docs lag.
|
||||
|
||||
## Fixed
|
||||
|
||||
- **A real API key was tracked** in `station/tools/tester/.env`. Untracked, `.env`
|
||||
added to `.gitignore`, `.env.example` added. **The key is still in git history
|
||||
and must be rotated** — untracking does not unpublish it.
|
||||
- Client vocabulary is out of live code. `atlas/main.py` was written entirely as
|
||||
`Album`/`larder`/`PAWPRINT_URL`; `databrowse` docs described a `larder/` the
|
||||
code stopped using; `get-api-key.sh` defaulted to the client's database name.
|
||||
- Two dead back-links, found while renaming: `atlas/main.py` passed
|
||||
`pawprint_url` and `artery/index.html` read `pawprint_url`, while `run.py`
|
||||
passes `soleprint_url`. Neither "← Soleprint" link had ever rendered.
|
||||
- `atlas/main.py` fetched `/api/data/album`; `main.py` serves `/api/data/atlas`.
|
||||
`get_data()` had been failing and returning empty lists.
|
||||
- `tester/tests/_dev/test_health.py` imported a non-existent `..endpoints`. The
|
||||
import was unused; removing it makes the module discoverable (2 tests).
|
||||
|
||||
80
Makefile
Normal file
@@ -0,0 +1,80 @@
|
||||
# Thin control Makefile — one target per ctrl/ script, and the subcommand is an
|
||||
# argument rather than a second target: `make cluster down`, not `make cluster-down`.
|
||||
#
|
||||
# The logic lives in the scripts, never here. Each target maps to exactly one
|
||||
# file, and that file holds the variants:
|
||||
#
|
||||
# make cluster up -> ctrl/cluster.sh up
|
||||
# make build amar -> ctrl/build.sh amar
|
||||
#
|
||||
# Bare words pass straight through. Anything starting with a dash would be
|
||||
# swallowed by make itself, so pass those via ARGS instead:
|
||||
#
|
||||
# make component ARGS="publish soleprint-ui /tmp/out --dist"
|
||||
# make deploy ARGS="--build"
|
||||
#
|
||||
# Every script stays runnable on its own (./ctrl/kind-up.sh still works, and each
|
||||
# built room keeps its own gen/<room>/ctrl/*.sh) — the standalone rule holds, and
|
||||
# this only saves typing.
|
||||
#
|
||||
# Start with: make build && make start
|
||||
|
||||
# The room to act on when none is named. Rooms live in cfg/ and build into gen/.
|
||||
ROOM ?= standalone
|
||||
PYTHON ?= python3
|
||||
export PYTHON
|
||||
|
||||
# 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.
|
||||
ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
|
||||
ifneq ($(ARGS),)
|
||||
# Declares each extra word as a target that does nothing: `:` is an empty rule
|
||||
# body and `@` silences it. Without this, `make build sample` runs the build and
|
||||
# then fails with "No rule to make target 'sample'", because make reads every
|
||||
# word on the line as something it has been asked to build.
|
||||
$(eval $(ARGS):;@:)
|
||||
# ...and as PHONY, because some of those words name real directories. `cfg`,
|
||||
# `ctrl`, `docs`, `gen` and `init` all exist at this level, and make considers a
|
||||
# target that is an existing directory already built — so `make build ctrl` ran
|
||||
# the build and then printed "make: 'ctrl' is up to date". The empty rule above
|
||||
# is not enough on its own; only .PHONY stops make consulting the filesystem.
|
||||
.PHONY: $(ARGS)
|
||||
endif
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
.PHONY: help build start stop dist docs cluster deploy component
|
||||
|
||||
help: ## list targets
|
||||
@grep -hE '^[a-z]+:.*?##' $(MAKEFILE_LIST) | sed 's/:.*##/\t/' | expand -t16
|
||||
|
||||
# ── rooms ──────────────────────────────────────────────────────────────────
|
||||
|
||||
build: ## build a room into gen/ [<room>|all|models]
|
||||
bash ctrl/build.sh $(or $(ARGS),$(ROOM))
|
||||
|
||||
start: ## run a built room [<room>] [-d] [--build]
|
||||
bash ctrl/start.sh $(or $(ARGS),$(ROOM))
|
||||
|
||||
stop: ## stop a running room [<room>]
|
||||
bash ctrl/stop.sh $(or $(ARGS),$(ROOM))
|
||||
|
||||
dist: ## compile the plexus UIs to single files [<room>]
|
||||
bash ctrl/dist.sh $(or $(ARGS),$(ROOM))
|
||||
|
||||
# ── docs ───────────────────────────────────────────────────────────────────
|
||||
|
||||
docs: ## documentation [serve [port]|graphs [theme]] (default serve)
|
||||
bash ctrl/docs.sh $(or $(ARGS),serve)
|
||||
|
||||
# ── cluster ────────────────────────────────────────────────────────────────
|
||||
|
||||
cluster: ## shared kind cluster [up|down|status] (default status)
|
||||
bash ctrl/cluster.sh $(or $(ARGS),status)
|
||||
|
||||
# ── distribution ───────────────────────────────────────────────────────────
|
||||
|
||||
component: ## publish components [list|sync|watch|publish|diff]
|
||||
$(PYTHON) ctrl/spr.py $(or $(ARGS),list)
|
||||
|
||||
deploy: ## push standalone to the server [--build|--sync-only]
|
||||
bash ctrl/deploy.sh $(ARGS)
|
||||
36
README.md
@@ -2,44 +2,14 @@
|
||||
|
||||
Development workflow platform. Wraps existing applications with tools, testing, and documentation — without touching source code.
|
||||
|
||||
Cada paso deja huella.
|
||||
*Cada paso deja huella.*
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Create a room
|
||||
python -m init.cli myroom
|
||||
|
||||
# Build
|
||||
python build.py --cfg myroom
|
||||
|
||||
# Run
|
||||
cd gen/myroom/soleprint && docker compose up
|
||||
|
||||
# Visit http://localhost:12000
|
||||
```
|
||||
|
||||
Or use the browser wizard: `python -m init.web` → http://localhost:9000
|
||||
---
|
||||
|
||||
## Docs
|
||||
|
||||
```bash
|
||||
cd docs && python -m http.server 8080
|
||||
# http://localhost:8080
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
spr/
|
||||
├── soleprint/ # Core framework
|
||||
│ ├── artery/ # Connectors (veins, shunts, pulses)
|
||||
│ ├── atlas/ # Documentation (books, templates)
|
||||
│ └── station/ # Tools (tester, datagen, modelgen)
|
||||
├── cfg/ # Room configurations
|
||||
├── init/ # Room setup (CLI + web wizard)
|
||||
├── docs/ # Documentation site
|
||||
├── ctrl/ # Build/deploy scripts
|
||||
├── build.py # Build tool: cfg/ → gen/
|
||||
└── gen/ # Built instances (gitignored)
|
||||
```
|
||||
Visit `http://localhost:8080`. Start with [Concepts](docs/data/en/concepts.md) for the mental model, then [Quickstart](docs/data/en/quickstart.md) to run your first room.
|
||||
|
||||
439
build.py
@@ -23,6 +23,7 @@ Generated structure for managed rooms:
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import logging
|
||||
import shutil
|
||||
@@ -79,6 +80,30 @@ def _rmtree_resilient(path: Path):
|
||||
)
|
||||
|
||||
|
||||
# Never swept into a built room, wherever they appear in a source tree.
|
||||
#
|
||||
# This is a SECURITY boundary, not tidiness. gen/<room>/ is the docker build
|
||||
# context, soleprint/Dockerfile is `COPY . .`, and there is no .dockerignore —
|
||||
# so anything that reaches gen/ reaches an image layer, and registry.mcrn.ar is
|
||||
# public-read. That is how station/tools/tester/.env, gitignored since the last
|
||||
# incident, still ended up baked into soleprint_localtest-soleprint:latest with
|
||||
# its API key intact. .gitignore does not bind shutil.
|
||||
#
|
||||
# Applied to bulk directory copies only. A caller naming a single file is making
|
||||
# an explicit request (cfg/<room>/.env.example is the one that matters) and is
|
||||
# left alone.
|
||||
ALWAYS_IGNORE = {".git", "__pycache__", "node_modules", ".venv", "venv", ".env"}
|
||||
ALWAYS_IGNORE_SUFFIXES = (".pyc", ".pyo")
|
||||
|
||||
|
||||
def is_ignored(name: str) -> bool:
|
||||
return name in ALWAYS_IGNORE or name.endswith(ALWAYS_IGNORE_SUFFIXES)
|
||||
|
||||
|
||||
def _copytree_ignore(directory, files):
|
||||
return {f for f in files if is_ignored(f)}
|
||||
|
||||
|
||||
def copy_path(source: Path, target: Path, quiet: bool = False):
|
||||
"""Copy file or directory, resolving symlinks."""
|
||||
if target.is_symlink():
|
||||
@@ -90,7 +115,7 @@ def copy_path(source: Path, target: Path, quiet: bool = False):
|
||||
target.unlink()
|
||||
|
||||
if source.is_dir():
|
||||
shutil.copytree(source, target, symlinks=False)
|
||||
shutil.copytree(source, target, symlinks=False, ignore=_copytree_ignore)
|
||||
if not quiet:
|
||||
log.info(f" {target.name}/")
|
||||
else:
|
||||
@@ -110,6 +135,8 @@ def merge_into(source: Path, target: Path):
|
||||
for item in source.rglob("*"):
|
||||
if item.is_file():
|
||||
rel = item.relative_to(source)
|
||||
if any(is_ignored(part) for part in rel.parts):
|
||||
continue
|
||||
dest = target / rel
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(item, dest)
|
||||
@@ -317,6 +344,363 @@ def copy_cfg(output_dir: Path, room: str):
|
||||
copy_path(item, output_dir / item.name)
|
||||
|
||||
|
||||
def load_cabinets(room: str) -> list[dict]:
|
||||
"""The dependency containers a room asked for, in the order it listed them.
|
||||
|
||||
Read from cfg/<room>/data/cabinets.json — the same shape and the same place
|
||||
as its sibling data/*.json files, so nothing new has to know about it.
|
||||
Entries are {"name": "postgres"} and may carry an "env" override.
|
||||
"""
|
||||
path = SPR_ROOT / "cfg" / room / "data" / "cabinets.json"
|
||||
if not path.exists():
|
||||
return []
|
||||
try:
|
||||
entries = json.loads(path.read_text())
|
||||
except ValueError as e:
|
||||
log.warning(f" cabinets.json is not valid JSON, ignoring: {e}")
|
||||
return []
|
||||
|
||||
out = []
|
||||
for entry in entries if isinstance(entries, list) else []:
|
||||
if isinstance(entry, str):
|
||||
entry = {"name": entry}
|
||||
if isinstance(entry, dict) and entry.get("name"):
|
||||
out.append(entry)
|
||||
return out
|
||||
|
||||
|
||||
def resolve_cabinets(requested: list[dict]) -> list[dict]:
|
||||
"""Expand each request into its definition, pulling in what it depends on.
|
||||
|
||||
Airflow without postgres is a container that exits on boot, so a cabinet's
|
||||
depends_on is added for you rather than left as something to remember.
|
||||
"""
|
||||
cabinets_dir = SPR_ROOT / "soleprint" / "station" / "cabinets"
|
||||
resolved: dict[str, dict] = {}
|
||||
|
||||
def add(name: str, overrides: dict) -> None:
|
||||
if name in resolved:
|
||||
# Already pulled in as somebody's dependency. The room asking for it
|
||||
# by name is the more specific statement, so its env still applies —
|
||||
# otherwise declaring airflow before postgres would silently drop
|
||||
# postgres's settings.
|
||||
if overrides.get("env"):
|
||||
resolved[name]["env"] = {
|
||||
**resolved[name].get("env", {}),
|
||||
**overrides["env"],
|
||||
}
|
||||
return
|
||||
definition_path = cabinets_dir / name / "cabinet.json"
|
||||
if not definition_path.exists():
|
||||
available = sorted(
|
||||
p.name for p in cabinets_dir.iterdir() if p.is_dir()
|
||||
) if cabinets_dir.exists() else []
|
||||
log.warning(f" no such cabinet: {name} (available: {', '.join(available) or 'none'})")
|
||||
return
|
||||
try:
|
||||
definition = json.loads(definition_path.read_text())
|
||||
except ValueError as e:
|
||||
log.warning(f" cabinet {name} has invalid cabinet.json: {e}")
|
||||
return
|
||||
|
||||
# Mark it claimed before recursing, so a dependency cycle terminates.
|
||||
resolved[name] = definition
|
||||
for dependency in definition.get("depends_on", []) or []:
|
||||
add(dependency, {})
|
||||
|
||||
definition["env"] = {**definition.get("env", {}), **overrides.get("env", {})}
|
||||
|
||||
for entry in requested:
|
||||
add(entry["name"], entry)
|
||||
|
||||
# Dependencies first, so compose reads in the order things start.
|
||||
ordered: list[dict] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
def emit(name: str) -> None:
|
||||
if name in seen or name not in resolved:
|
||||
return
|
||||
seen.add(name)
|
||||
for dependency in resolved[name].get("depends_on", []) or []:
|
||||
emit(dependency)
|
||||
ordered.append(resolved[name])
|
||||
|
||||
for name in resolved:
|
||||
emit(name)
|
||||
return ordered
|
||||
|
||||
|
||||
def compose_cabinets(output_dir: Path, room: str):
|
||||
"""Merge the room's cabinets into its docker-compose.yml and .env.example.
|
||||
|
||||
This is the compile step for dependencies: a room declares postgres, and the
|
||||
built instance comes out with postgres in its compose file rather than with
|
||||
instructions for adding it.
|
||||
"""
|
||||
requested = load_cabinets(room)
|
||||
if not requested:
|
||||
return
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
log.warning(
|
||||
" cabinets need PyYAML to merge into docker-compose.yml "
|
||||
"(pip install pyyaml) — skipping"
|
||||
)
|
||||
return
|
||||
|
||||
cabinets = resolve_cabinets(requested)
|
||||
if not cabinets:
|
||||
return
|
||||
|
||||
compose_path = output_dir / "docker-compose.yml"
|
||||
if not compose_path.exists():
|
||||
log.warning(
|
||||
f" no docker-compose.yml in {output_dir.name}, "
|
||||
f"so there is nothing to merge {len(cabinets)} cabinet(s) into"
|
||||
)
|
||||
return
|
||||
|
||||
original = compose_path.read_text()
|
||||
# A YAML round-trip drops every comment, and the room's compose file leads
|
||||
# with the one that says how to run it. Keep the header block; the rest is
|
||||
# generated anyway.
|
||||
header = []
|
||||
for line in original.splitlines():
|
||||
if line.startswith("#") or not line.strip():
|
||||
header.append(line)
|
||||
else:
|
||||
break
|
||||
while header and not header[-1].strip():
|
||||
header.pop()
|
||||
|
||||
compose = yaml.safe_load(original) or {}
|
||||
services = compose.setdefault("services", {})
|
||||
volumes = compose.setdefault("volumes", {}) or {}
|
||||
cabinets_dir = SPR_ROOT / "soleprint" / "station" / "cabinets"
|
||||
|
||||
added, skipped = [], []
|
||||
for cabinet in cabinets:
|
||||
name = cabinet["name"]
|
||||
service_name = cabinet.get("service", name)
|
||||
|
||||
# The room's own compose file is the authority. A room that already
|
||||
# declares `db` has arranged it deliberately, and silently replacing it
|
||||
# would be the worst possible outcome of switching a cabinet on.
|
||||
if service_name in services:
|
||||
skipped.append(service_name)
|
||||
continue
|
||||
|
||||
fragment_path = cabinets_dir / name / "service.yml"
|
||||
if not fragment_path.exists():
|
||||
log.warning(f" cabinet {name} has no service.yml")
|
||||
continue
|
||||
|
||||
fragment = yaml.safe_load(fragment_path.read_text()) or {}
|
||||
for key, value in fragment.items():
|
||||
if key in services:
|
||||
skipped.append(key)
|
||||
continue
|
||||
services[key] = value
|
||||
added.append(key)
|
||||
|
||||
for volume in cabinet.get("volumes", []) or []:
|
||||
volumes.setdefault(volume, None)
|
||||
|
||||
if volumes:
|
||||
compose["volumes"] = volumes
|
||||
|
||||
rendered = yaml.safe_dump(compose, sort_keys=False, default_flow_style=False)
|
||||
banner = f"# Cabinets merged in by build.py: {', '.join(c['name'] for c in cabinets)}.\n"
|
||||
preamble = ("\n".join(header) + "\n" + banner + "\n") if header else banner + "\n"
|
||||
compose_path.write_text(preamble + rendered)
|
||||
|
||||
if added:
|
||||
log.info(f" cabinets: {', '.join(added)}")
|
||||
if skipped:
|
||||
log.info(f" cabinets already declared by the room, left alone: {', '.join(skipped)}")
|
||||
|
||||
_append_cabinet_env(output_dir, cabinets)
|
||||
|
||||
|
||||
def _append_cabinet_env(output_dir: Path, cabinets: list[dict]):
|
||||
"""Add each cabinet's settings to .env.example, without touching .env."""
|
||||
example = output_dir / ".env.example"
|
||||
existing = example.read_text() if example.exists() else ""
|
||||
# Match whole settings, not substrings: `POSTGRES_DB=` appears inside
|
||||
# `MY_POSTGRES_DB=`, and a substring test would decide the setting was
|
||||
# already there and skip it.
|
||||
declared = {
|
||||
line.split("=", 1)[0].strip()
|
||||
for line in existing.splitlines()
|
||||
if "=" in line and not line.lstrip().startswith("#")
|
||||
}
|
||||
|
||||
lines = []
|
||||
for cabinet in cabinets:
|
||||
env = cabinet.get("env", {})
|
||||
if not env:
|
||||
continue
|
||||
block = [f"\n# ── {cabinet.get('title', cabinet['name'])} (cabinet) ──"]
|
||||
for note in cabinet.get("notes", []) or []:
|
||||
block.append(f"# {note}")
|
||||
wrote = False
|
||||
for key, value in env.items():
|
||||
if key in declared:
|
||||
continue
|
||||
block.append(f"{key}={value}")
|
||||
declared.add(key)
|
||||
wrote = True
|
||||
if wrote:
|
||||
lines.extend(block)
|
||||
|
||||
if lines:
|
||||
example.write_text(existing.rstrip("\n") + "\n" + "\n".join(lines) + "\n")
|
||||
|
||||
|
||||
def load_plexuses(room: str) -> list[dict]:
|
||||
"""The plexuses a room asked for. Same shape as its sibling data/*.json."""
|
||||
path = SPR_ROOT / "cfg" / room / "data" / "plexuses.json"
|
||||
if not path.exists():
|
||||
return []
|
||||
try:
|
||||
raw = json.loads(path.read_text())
|
||||
except ValueError as e:
|
||||
log.warning(f" plexuses.json is not valid JSON, ignoring: {e}")
|
||||
return []
|
||||
|
||||
entries = raw.get("items", raw) if isinstance(raw, dict) else raw
|
||||
out = []
|
||||
for entry in entries if isinstance(entries, list) else []:
|
||||
if isinstance(entry, str):
|
||||
entry = {"name": entry}
|
||||
if isinstance(entry, dict) and entry.get("name"):
|
||||
out.append(entry)
|
||||
return out
|
||||
|
||||
|
||||
def _theme_css(theme: str) -> str:
|
||||
"""The token contract plus one theme, flattened for inlining.
|
||||
|
||||
Only the named theme ships alongside the others it can switch to, because
|
||||
the export has to work with no server: there is no /theme.css to fetch.
|
||||
"""
|
||||
theme_dir = SPR_ROOT / "soleprint" / "common" / "theme"
|
||||
parts = []
|
||||
tokens = theme_dir / "tokens.css"
|
||||
if tokens.exists():
|
||||
parts.append(tokens.read_text())
|
||||
# Every theme, so the switcher in the page has something to switch to.
|
||||
for sheet in sorted((theme_dir / "themes").glob("*.css")):
|
||||
parts.append(sheet.read_text())
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def _inline_svg(name: str, theme: str) -> str:
|
||||
"""A rendered graph, stripped of its XML prolog so it can sit in HTML.
|
||||
|
||||
Inlined rather than <img>-linked so the page's CSS can recolour it when the
|
||||
theme switches — graphviz writes class="node accent" into the SVG, and CSS
|
||||
outranks the presentation attributes it bakes in.
|
||||
"""
|
||||
graphs = SPR_ROOT / "docs" / "graphs"
|
||||
for candidate in (graphs / f"{name}.{theme}.svg", graphs / f"{name}.svg"):
|
||||
if candidate.exists():
|
||||
svg = candidate.read_text()
|
||||
start = svg.find("<svg")
|
||||
return svg[start:] if start >= 0 else svg
|
||||
log.warning(f" no rendered graph '{name}' — run docs/graphs/render.sh")
|
||||
return "<p>diagram not rendered</p>"
|
||||
|
||||
|
||||
def build_plexuses(output_dir: Path, room: str):
|
||||
"""Export each plexus the room declared to a single self-contained file.
|
||||
|
||||
A plexus is exported, not served. The output is one index.html carrying its
|
||||
theme, its data and its diagram, so it survives a locked-down machine, a zip
|
||||
attachment and a double-click — which is the whole point of the format.
|
||||
"""
|
||||
requested = load_plexuses(room)
|
||||
if not requested:
|
||||
return
|
||||
|
||||
source_root = SPR_ROOT / "soleprint" / "artery" / "plexuses"
|
||||
built = []
|
||||
|
||||
for entry in requested:
|
||||
name = entry["name"]
|
||||
source = source_root / name
|
||||
manifest_path = source / "plexus.json"
|
||||
if not manifest_path.exists():
|
||||
available = sorted(
|
||||
p.name for p in source_root.iterdir() if p.is_dir()
|
||||
) if source_root.exists() else []
|
||||
log.warning(
|
||||
f" no such plexus: {name} (available: {', '.join(available) or 'none'})"
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
manifest = json.loads(manifest_path.read_text())
|
||||
except ValueError as e:
|
||||
log.warning(f" plexus {name} has invalid plexus.json: {e}")
|
||||
continue
|
||||
|
||||
# The room may override anything the plexus declares — theme first.
|
||||
manifest.update({k: v for k, v in entry.items() if k != "name"})
|
||||
|
||||
template_path = source / "app" / "index.html"
|
||||
if not template_path.exists():
|
||||
log.warning(f" plexus {name} has no app/index.html")
|
||||
continue
|
||||
|
||||
theme = manifest.get("theme", "soleprint")
|
||||
data = {k: v for k, v in manifest.items() if not k.startswith("_")}
|
||||
|
||||
# A plexus may ship a showcase.py exposing collect(): anything it returns
|
||||
# is merged into the page's data. The bundle uses it to run the real
|
||||
# tools over the real fixtures at build time, so what the page shows
|
||||
# cannot drift from what the tools do.
|
||||
collector = source / "showcase.py"
|
||||
if collector.exists():
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
f"plexus_{name}_showcase", collector
|
||||
)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
data["showcase"] = module.collect()
|
||||
except Exception as e:
|
||||
log.warning(f" {name}: showcase.py failed ({type(e).__name__}: {e})")
|
||||
|
||||
page = template_path.read_text()
|
||||
for token, value in (
|
||||
("%%TITLE%%", manifest.get("title", name)),
|
||||
("%%DESCRIPTION%%", manifest.get("description", "")),
|
||||
("%%DEFAULT_THEME%%", theme),
|
||||
("%%BUILT%%", f"{room} · built by soleprint build.py"),
|
||||
("%%THEME_CSS%%", _theme_css(theme)),
|
||||
("%%GRAPH%%", _inline_svg(manifest.get("graph", "system_overview"), theme)),
|
||||
("%%BUNDLE%%", json.dumps(data, indent=2)),
|
||||
):
|
||||
page = page.replace(token, value)
|
||||
|
||||
target = output_dir / "plexuses" / name
|
||||
ensure_dir(target)
|
||||
(target / "index.html").write_text(page)
|
||||
|
||||
# Anything else in app/ rides along, for a plexus that outgrows one file.
|
||||
for extra in (source / "app").iterdir():
|
||||
if extra.name != "index.html":
|
||||
copy_path(extra, target / extra.name, quiet=True)
|
||||
|
||||
built.append(f"{name} ({theme})")
|
||||
|
||||
if built:
|
||||
log.info(f" plexuses: {', '.join(built)}")
|
||||
|
||||
|
||||
def build_soleprint(output_dir: Path, room: str):
|
||||
"""Build soleprint folder with core + room config merged."""
|
||||
soleprint = SPR_ROOT / "soleprint"
|
||||
@@ -329,6 +713,7 @@ def build_soleprint(output_dir: Path, room: str):
|
||||
"index.html",
|
||||
"requirements.txt",
|
||||
"Dockerfile",
|
||||
".dockerignore",
|
||||
]:
|
||||
if (soleprint / name).exists():
|
||||
copy_path(soleprint / name, output_dir / name)
|
||||
@@ -348,6 +733,16 @@ def build_soleprint(output_dir: Path, room: str):
|
||||
# Room config (includes merging room-specific artery/atlas/station)
|
||||
copy_cfg(output_dir, room)
|
||||
|
||||
# Dependency containers the room asked for, merged into its compose file.
|
||||
# After copy_cfg, because the compose file being merged into is the room's.
|
||||
log.info("Composing cabinets...")
|
||||
compose_cabinets(output_dir, room)
|
||||
|
||||
# Plexuses are exported rather than served, so this is a compile step like
|
||||
# the cabinet merge above — not something run.py does at request time.
|
||||
log.info("Exporting plexuses...")
|
||||
build_plexuses(output_dir, room)
|
||||
|
||||
# Generate models
|
||||
log.info("Generating models...")
|
||||
if not generate_models(output_dir, room):
|
||||
@@ -404,6 +799,33 @@ def build_models_only():
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def build_plexuses_only(room: str):
|
||||
"""Compile just the plexus UIs, without rebuilding the room around them.
|
||||
|
||||
The equivalent of `vite build` for this repo: the iteration loop when you
|
||||
are working on the UI itself is edit, compile, reopen the file — and a full
|
||||
room build to see a CSS change is a slow way to do that.
|
||||
"""
|
||||
output_dir = SPR_ROOT / "gen" / room
|
||||
if not output_dir.exists():
|
||||
log.error(f"Room '{room}' is not built — run: python build.py --cfg {room}")
|
||||
sys.exit(1)
|
||||
|
||||
log.info(f"Compiling plexus UIs for {room}...")
|
||||
build_plexuses(output_dir, room)
|
||||
|
||||
built = sorted((output_dir / "plexuses").glob("*/index.html"))
|
||||
if not built:
|
||||
log.warning(
|
||||
f" nothing compiled — does cfg/{room}/data/plexuses.json list one?"
|
||||
)
|
||||
return
|
||||
for page in built:
|
||||
log.info(f" {page.relative_to(SPR_ROOT)} ({page.stat().st_size // 1024} KB)")
|
||||
log.info("\n✓ Open directly — no server needed:")
|
||||
log.info(f" xdg-open {built[0]}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Soleprint Build Tool")
|
||||
|
||||
@@ -411,15 +833,26 @@ def main():
|
||||
parser.add_argument("--cfg", "-c", type=str, help="Room config name")
|
||||
parser.add_argument("--all", action="store_true", help="Build all rooms")
|
||||
parser.add_argument("--models", action="store_true", help="Only regenerate models")
|
||||
parser.add_argument(
|
||||
"--plexuses",
|
||||
action="store_true",
|
||||
help="Only compile the plexus UIs into an already-built room",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.models:
|
||||
if args.plexuses:
|
||||
build_plexuses_only(args.cfg or "standalone")
|
||||
elif args.models:
|
||||
build_models_only()
|
||||
elif args.all:
|
||||
build(SPR_ROOT / "gen" / "standalone", None)
|
||||
for room in (SPR_ROOT / "cfg").iterdir():
|
||||
if room.is_dir() and room.name not in ("__pycache__", "standalone"):
|
||||
# cfg/ is itself a git repo and rooms may carry dot-dirs — skip them,
|
||||
# or --all tries to build ".git" as a room.
|
||||
if room.name.startswith(".") or room.name == "__pycache__":
|
||||
continue
|
||||
if room.is_dir() and room.name != "standalone":
|
||||
build(SPR_ROOT / "gen" / room.name, room.name)
|
||||
else:
|
||||
if args.output:
|
||||
|
||||
4
cfg/.gitignore
vendored
@@ -9,5 +9,9 @@ __pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
|
||||
standalone
|
||||
sample
|
||||
|
||||
|
||||
# standalone/ and sample/ are kept in the main soleprint repo as templates.
|
||||
# Other rooms (amar/, dlt/, etc.) are listed in the repo-root .gitignore.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Pawprint Data Layer
|
||||
Soleprint Data Layer
|
||||
|
||||
JSON file storage (future: MongoDB)
|
||||
"""
|
||||
|
||||
@@ -1,14 +1,3 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"name": "feature-flow",
|
||||
"slug": "feature-flow",
|
||||
"title": "Feature Flow Pipeline",
|
||||
"status": "ready",
|
||||
"template": null,
|
||||
"larder": null,
|
||||
"output_larder": null,
|
||||
"system": "atlas"
|
||||
}
|
||||
]
|
||||
"items": []
|
||||
}
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"name": "feature-form",
|
||||
"slug": "feature-form",
|
||||
"title": "Feature Forms",
|
||||
"status": "ready",
|
||||
"source_template": "feature-form",
|
||||
"data_path": "album/book/feature-form-samples/feature-form"
|
||||
}
|
||||
]
|
||||
"items": []
|
||||
}
|
||||
|
||||
@@ -1,22 +1,3 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"name": "turnos",
|
||||
"slug": "turnos",
|
||||
"title": "Turnos Monitor",
|
||||
"status": "dev",
|
||||
"system": "ward",
|
||||
"description": "Pipeline view of requests → turnos. Shows vet-petowner at a glance.",
|
||||
"path": "ward/monitor/turnos"
|
||||
},
|
||||
{
|
||||
"name": "data_browse",
|
||||
"slug": "data-browse",
|
||||
"title": "Data Browse",
|
||||
"status": "ready",
|
||||
"system": "ward",
|
||||
"description": "Quick navigation to test users and data states. Book/larder pattern with SQL mode for manual testing workflows.",
|
||||
"path": "ward/monitor/data_browse"
|
||||
}
|
||||
]
|
||||
"items": []
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
{
|
||||
"items": [
|
||||
{"name": "pawprint-local", "slug": "pawprint-local", "title": "Pawprint Local", "status": "dev", "config_path": "deploy/pawprint-local"}
|
||||
]
|
||||
"items": []
|
||||
}
|
||||
|
||||
@@ -1,18 +1,3 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"name": "mercadopago",
|
||||
"slug": "mercadopago",
|
||||
"title": "MercadoPago",
|
||||
"status": "ready",
|
||||
"description": "Mock payment API for testing"
|
||||
},
|
||||
{
|
||||
"name": "example",
|
||||
"slug": "example",
|
||||
"title": "Example",
|
||||
"status": "ready",
|
||||
"description": "Example shunt template"
|
||||
}
|
||||
]
|
||||
"items": []
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
# {{nombre_flujo}}
|
||||
|
||||
## Tipo de usuario
|
||||
{{tipo_usuario}}
|
||||
|
||||
## Donde empieza
|
||||
{{punto_entrada}}
|
||||
|
||||
## Que quiere hacer el usuario
|
||||
{{objetivo}}
|
||||
|
||||
## Pasos
|
||||
|
||||
1. {{paso_1}}
|
||||
2. {{paso_2}}
|
||||
3. {{paso_3}}
|
||||
|
||||
## Que deberia pasar
|
||||
|
||||
- {{resultado_1}}
|
||||
- {{resultado_2}}
|
||||
|
||||
## Problemas comunes
|
||||
|
||||
- {{problema_1}}
|
||||
- {{problema_2}}
|
||||
|
||||
## Casos especiales
|
||||
|
||||
- {{caso_especial_1}}
|
||||
|
||||
## Flujos relacionados
|
||||
|
||||
- {{flujo_relacionado_1}}
|
||||
|
||||
## Notas tecnicas
|
||||
|
||||
- {{nota_tecnica_1}}
|
||||
@@ -1,12 +1,3 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"name": "feature-form",
|
||||
"slug": "feature-form",
|
||||
"title": "Feature Form Template",
|
||||
"status": "ready",
|
||||
"template_path": "data/template/feature-form",
|
||||
"system": "album"
|
||||
}
|
||||
]
|
||||
"items": []
|
||||
}
|
||||
|
||||
@@ -1,47 +1,36 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"name": "tester",
|
||||
"slug": "tester",
|
||||
"title": "Contract Tests",
|
||||
"status": "live",
|
||||
"system": "ward",
|
||||
"type": "app",
|
||||
"description": "HTTP contract test runner with multi-environment support. Filter, run, and track tests against dev/stage/prod.",
|
||||
"path": "ward/tools/tester",
|
||||
"url": "/tools/tester/"
|
||||
},
|
||||
{
|
||||
"name": "datagen",
|
||||
"slug": "datagen",
|
||||
"title": "Test Data Generator",
|
||||
"title": "Datagen",
|
||||
"status": "live",
|
||||
"system": "ward",
|
||||
"type": "cli",
|
||||
"description": "Generate realistic test data for Amar domain (users, pets, services) and MercadoPago API responses. Used by mock veins and test seeders.",
|
||||
"path": "ward/tools/datagen",
|
||||
"cli": "python -m datagen"
|
||||
"system": "station",
|
||||
"type": "app",
|
||||
"description": "Generate realistic test data via faker-backed generators.",
|
||||
"path": "station/tools/datagen",
|
||||
"url": "/station/tools/datagen/"
|
||||
},
|
||||
{
|
||||
"name": "generate_test_data",
|
||||
"slug": "generate-test-data",
|
||||
"title": "DB Test Data Extractor",
|
||||
"status": "dev",
|
||||
"system": "ward",
|
||||
"type": "cli",
|
||||
"description": "Extract representative subsets from PostgreSQL dumps for testing/development.",
|
||||
"path": "ward/tools/generate_test_data",
|
||||
"cli": "python -m generate_test_data"
|
||||
"name": "graphgen",
|
||||
"slug": "graphgen",
|
||||
"title": "Graphgen",
|
||||
"status": "live",
|
||||
"system": "station",
|
||||
"type": "app",
|
||||
"description": "Render the data model as an interactive graph (entities + FK edges).",
|
||||
"path": "station/tools/graphgen",
|
||||
"url": "/station/tools/graphgen/"
|
||||
},
|
||||
{
|
||||
"name": "modelgen",
|
||||
"slug": "modelgen",
|
||||
"title": "Model Generator",
|
||||
"status": "dev",
|
||||
"system": "ward",
|
||||
"title": "Modelgen",
|
||||
"status": "live",
|
||||
"system": "station",
|
||||
"type": "cli",
|
||||
"description": "Generate platform-specific models (Pydantic, Django, Prisma) from JSON Schema.",
|
||||
"path": "ward/tools/modelgen",
|
||||
"path": "station/tools/modelgen",
|
||||
"cli": "python -m modelgen"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,60 +1,12 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"name": "jira",
|
||||
"slug": "jira",
|
||||
"title": "Jira",
|
||||
"status": "live",
|
||||
"system": "artery"
|
||||
},
|
||||
{
|
||||
"name": "slack",
|
||||
"slug": "slack",
|
||||
"title": "Slack",
|
||||
"status": "building",
|
||||
"system": "artery"
|
||||
},
|
||||
{
|
||||
"name": "google",
|
||||
"slug": "google",
|
||||
"title": "Google",
|
||||
"status": "building",
|
||||
"system": "artery"
|
||||
},
|
||||
{
|
||||
"name": "maps",
|
||||
"slug": "maps",
|
||||
"title": "Maps",
|
||||
"status": "planned",
|
||||
"system": "artery"
|
||||
},
|
||||
{
|
||||
"name": "whatsapp",
|
||||
"slug": "whatsapp",
|
||||
"title": "WhatsApp",
|
||||
"status": "planned",
|
||||
"system": "artery"
|
||||
},
|
||||
{
|
||||
"name": "gnucash",
|
||||
"slug": "gnucash",
|
||||
"title": "GNUCash",
|
||||
"status": "planned",
|
||||
"system": "artery"
|
||||
},
|
||||
{
|
||||
"name": "vpn",
|
||||
"slug": "vpn",
|
||||
"title": "VPN",
|
||||
"status": "planned",
|
||||
"system": "artery"
|
||||
},
|
||||
{
|
||||
"name": "ia",
|
||||
"slug": "ia",
|
||||
"title": "IA",
|
||||
"status": "live",
|
||||
"system": "artery"
|
||||
"system": "artery",
|
||||
"description": "Google OAuth login + Drive/Calendar/Gmail APIs."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# Fixture Invoicing — Soleprint Demo
|
||||
|
||||
> **This book describes a deliberately-fake invoicing app used as a test
|
||||
> fixture for the Soleprint framework.** It is not a real product.
|
||||
|
||||
## Purpose
|
||||
|
||||
The fixture exercises every soleprint tool end-to-end so we can iterate on
|
||||
the framework without needing a real managed app. See
|
||||
[`examples/fixture-invoicing/`](../../../../../../examples/fixture-invoicing/)
|
||||
for the app itself.
|
||||
|
||||
## Data model
|
||||
|
||||
```
|
||||
Customer ──< Invoice ──< LineItem
|
||||
└──────< Payment
|
||||
```
|
||||
|
||||
| Table | Key fields |
|
||||
|-------------|-------------------------------------------------|
|
||||
| `customer` | id · name · email · created_at |
|
||||
| `invoice` | id · number · customer_id · issued_at · status |
|
||||
| `line_item` | id · invoice_id · description · qty · unit_price|
|
||||
| `payment` | id · invoice_id · amount · method · paid_at |
|
||||
|
||||
## Happy-path flow
|
||||
|
||||
1. Create a customer (`POST /api/customers`)
|
||||
2. Create a draft invoice for them (`POST /api/invoices`)
|
||||
3. Add one or more line items (`POST /api/line-items/invoices/{id}`)
|
||||
4. Record a payment (`POST /api/payments/invoices/{id}`) — if the total
|
||||
paid ≥ total billed, the invoice auto-transitions to `paid`.
|
||||
|
||||
## How this connects to Soleprint
|
||||
|
||||
| Soleprint tool | Fixture hook |
|
||||
|----------------|--------------|
|
||||
| datagen | `station/tools/datagen/fixture.py` — FixtureInvoicingGenerator |
|
||||
| graphgen | `station/tools/graphgen/schema.json` — 4 models + FK edges |
|
||||
| databrowse | `station/tools/databrowse/depot/{schema,views}.json` |
|
||||
| tester | `station/tools/tester/tests/fixture/` |
|
||||
| link | SQLAlchemy reflection against `customer`, `invoice`, etc. |
|
||||
| sbwrapper | Injected into fixture's Vue frontend by nginx |
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Pawprint Data Layer
|
||||
Soleprint Data Layer
|
||||
|
||||
JSON file storage (future: MongoDB)
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
{
|
||||
"items": []
|
||||
"items": [
|
||||
{
|
||||
"name": "bundle"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"items": [
|
||||
{"name": "pawprint-local", "slug": "pawprint-local", "title": "Pawprint Local", "status": "dev", "config_path": "deploy/pawprint-local"}
|
||||
{"name": "standalone-local", "slug": "standalone-local", "title": "Standalone Local", "status": "dev", "config_path": "deploy/standalone-local"}
|
||||
]
|
||||
}
|
||||
|
||||
37
ctrl/build.sh
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
# Build a room into gen/ — thin wrapper over build.py.
|
||||
#
|
||||
# Usage:
|
||||
# ./ctrl/build.sh # standalone -> gen/standalone/
|
||||
# ./ctrl/build.sh amar # amar -> gen/amar/
|
||||
# ./ctrl/build.sh all # every room under cfg/
|
||||
# ./ctrl/build.sh models # only regenerate models
|
||||
#
|
||||
# build.py holds the logic; this exists so `make build` has exactly one
|
||||
# script to call, and so the room name is a plain argument.
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
PYTHON="${PYTHON:-python3}"
|
||||
ROOM="${1:-standalone}"
|
||||
|
||||
case "$ROOM" in
|
||||
all) exec "$PYTHON" build.py --all ;;
|
||||
models) exec "$PYTHON" build.py --models ;;
|
||||
esac
|
||||
|
||||
if [[ ! -d "cfg/$ROOM" ]]; then
|
||||
echo "No such room: cfg/$ROOM" >&2
|
||||
echo "Available: $(find cfg -mindepth 1 -maxdepth 1 -type d -not -name '.*' -printf '%f ')" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# standalone is build.py's default and takes no --cfg
|
||||
if [[ "$ROOM" == "standalone" ]]; then
|
||||
exec "$PYTHON" build.py
|
||||
fi
|
||||
|
||||
exec "$PYTHON" build.py --cfg "$ROOM"
|
||||
24
ctrl/cluster.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
# The shared `spr` kind cluster [up|down|status] (default status).
|
||||
#
|
||||
# Usage:
|
||||
# ./ctrl/cluster.sh up # create the cluster (no-op if it exists)
|
||||
# ./ctrl/cluster.sh down # delete it (drops every room's namespace)
|
||||
# ./ctrl/cluster.sh status # what's running on it
|
||||
#
|
||||
# One target, one script — the variants live here. The kind-*.sh files stay
|
||||
# exactly as they are and remain runnable on their own; this only dispatches.
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
case "${1:-status}" in
|
||||
up) exec "$SCRIPT_DIR/kind-up.sh" ;;
|
||||
down) exec "$SCRIPT_DIR/kind-down.sh" ;;
|
||||
status) exec "$SCRIPT_DIR/kind-status.sh" ;;
|
||||
*)
|
||||
echo "Unknown subcommand: $1" >&2
|
||||
echo "Usage: cluster.sh [up|down|status]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -41,6 +41,14 @@ if [ "$SYNC_ONLY" = true ]; then
|
||||
fi
|
||||
|
||||
echo "Restarting soleprint on server..."
|
||||
ssh "$SERVER" "cd $REMOTE_DIR && docker compose up -d --build"
|
||||
# The compose file runs the container as ${UID:-1000}:${GID:-1000} and bind-mounts
|
||||
# the deployed tree at /app. Those have to be the ids that OWN the tree, and they
|
||||
# are not 1000 on every host — mcrn.ar's user is 1001. Without this the container
|
||||
# starts fine and then 500s on the first file it reads, which reads as an app bug
|
||||
# rather than a permissions one.
|
||||
#
|
||||
# `env` rather than a prefix assignment: UID is readonly in bash, so
|
||||
# `UID=$(id -u) docker ...` fails outright.
|
||||
ssh "$SERVER" "cd $REMOTE_DIR && env UID=\$(id -u) GID=\$(id -g) docker compose up -d --build"
|
||||
|
||||
echo "Deploy complete"
|
||||
|
||||
31
ctrl/dist.sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
# Compile the plexus UIs to distributable files — this repo's `vite build`.
|
||||
#
|
||||
# Usage:
|
||||
# ./ctrl/dist.sh # standalone
|
||||
# ./ctrl/dist.sh sample # a named room
|
||||
#
|
||||
# A plexus is a UI that gets EXPORTED, not served. The output is a single
|
||||
# index.html carrying its theme, its data and its diagrams inline, so it opens
|
||||
# from a double-click on a machine with no server, no node and no network — the
|
||||
# state a regulated Windows box is usually in.
|
||||
#
|
||||
# `make build` runs this as one of its steps. This exists for the loop where the
|
||||
# UI is what you are working on: rebuilding a whole room to see a CSS change is
|
||||
# a slow way to iterate.
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
PYTHON="${PYTHON:-python3}"
|
||||
ROOM="${1:-standalone}"
|
||||
|
||||
if [[ ! -d "cfg/$ROOM" ]]; then
|
||||
echo "No such room: cfg/$ROOM" >&2
|
||||
echo "Available: $(find cfg -mindepth 1 -maxdepth 1 -type d -not -name '.*' -printf '%f ')" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec "$PYTHON" build.py --plexuses --cfg "$ROOM"
|
||||
47
ctrl/docs.sh
Executable file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
# Documentation: serve the pages, and re-render the diagrams.
|
||||
#
|
||||
# Usage: docs.sh serve [port] | graphs [theme]
|
||||
#
|
||||
# The docs are a static SPA — index.html plus data/*.md read at runtime — so
|
||||
# they need a server only because fetch() refuses file:// origins. Any static
|
||||
# server does; python is already a hard dependency here (build.py is python), so
|
||||
# there is no reason to reach for docker the way rig does.
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
DOCS_DIR="$ROOT_DIR/docs"
|
||||
|
||||
PYTHON="${PYTHON:-python3}"
|
||||
PORT="${DOCS_PORT:-8080}"
|
||||
|
||||
serve() {
|
||||
[ -n "${1:-}" ] && PORT="$1"
|
||||
if [ ! -f "$DOCS_DIR/index.html" ]; then
|
||||
echo "no docs/index.html" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "docs on http://localhost:${PORT}/"
|
||||
echo " (ctrl-c to stop; nothing is installed and nothing persists)"
|
||||
cd "$DOCS_DIR"
|
||||
exec "$PYTHON" -m http.server "$PORT"
|
||||
}
|
||||
|
||||
# Re-render docs/graphs/*.dot through every theme. The sources carry structure;
|
||||
# the palette lives in docs/graphs/themes/*.gvpr. See docs/graphs/README.md.
|
||||
graphs() {
|
||||
if [ ! -x "$DOCS_DIR/graphs/render.sh" ]; then
|
||||
echo "no docs/graphs/render.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
exec bash "$DOCS_DIR/graphs/render.sh" "$@"
|
||||
}
|
||||
|
||||
case "${1:-serve}" in
|
||||
serve) shift || true; serve "$@" ;;
|
||||
graphs) shift || true; graphs "$@" ;;
|
||||
# `make docs 8090` is the obvious thing to type, so take it.
|
||||
''|*[!0-9]*) echo "usage: $0 [serve [port]|graphs [theme]]" >&2; exit 1 ;;
|
||||
*) serve "$1" ;;
|
||||
esac
|
||||
49
ctrl/spr.py
@@ -11,6 +11,7 @@ Usage:
|
||||
python ctrl/spr.py sync soleprint-ui ~/wdir/unt/ui/framework
|
||||
python ctrl/spr.py watch soleprint-ui ~/wdir/unt/ui/framework # ctrl+c to stop
|
||||
python ctrl/spr.py publish soleprint-ui ~/wdir/mpr/ui/framework
|
||||
python ctrl/spr.py publish soleprint-ui /tmp/out --dist # built bundle only
|
||||
python ctrl/spr.py diff soleprint-ui ~/wdir/mpr/ui/framework
|
||||
"""
|
||||
|
||||
@@ -194,6 +195,37 @@ def cmd_list(args):
|
||||
log.info(" %s %s v%-10s %s", f"{name:<25}", f"{comp_type:<5}", version, entry["path"])
|
||||
|
||||
|
||||
def publish_dist(source, dest):
|
||||
"""Copy only the built bundle — dist/ plus the manifest and any docs.
|
||||
|
||||
The artifact-only form: a consumer gets something that runs, not the
|
||||
sources. Deliberately NOT a variant of copy_tree, which walks the whole
|
||||
tree and strips dist; here dist is the entire point.
|
||||
"""
|
||||
dist = source / "dist"
|
||||
if not dist.is_dir() or not any(dist.iterdir()):
|
||||
log.error("no build at %s", dist)
|
||||
log.info("build it first: cd %s && pnpm build", source)
|
||||
sys.exit(1)
|
||||
|
||||
count = 0
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
for item in dist.rglob("*"):
|
||||
if item.is_file():
|
||||
target = dest / "dist" / item.relative_to(dist)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(item, target)
|
||||
count += 1
|
||||
|
||||
# The manifest travels too, or the bundle's entry points aren't resolvable.
|
||||
for name in ("package.json", "README.md", "LICENSE"):
|
||||
if (source / name).is_file():
|
||||
shutil.copy2(source / name, dest / name)
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
|
||||
def cmd_publish(args):
|
||||
registry = load_registry()
|
||||
comp_type, source = resolve_component(registry, args.component)
|
||||
@@ -202,12 +234,17 @@ def cmd_publish(args):
|
||||
if dest.exists():
|
||||
shutil.rmtree(dest)
|
||||
|
||||
count = copy_tree(source, dest)
|
||||
write_stamp(dest, args.component, comp_type, source, "published")
|
||||
if args.dist:
|
||||
count = publish_dist(source, dest)
|
||||
mode = "published-dist"
|
||||
else:
|
||||
count = copy_tree(source, dest)
|
||||
mode = "published"
|
||||
write_stamp(dest, args.component, comp_type, source, mode)
|
||||
|
||||
version = get_version(comp_type, source)
|
||||
sha = get_sha()
|
||||
log.info("%s v%s (%s) -> %s (%d files)", args.component, version, sha, dest, count)
|
||||
log.info("%s v%s (%s) -> %s (%d files, %s)", args.component, version, sha, dest, count, mode)
|
||||
|
||||
|
||||
def cmd_sync(args):
|
||||
@@ -306,6 +343,12 @@ def main():
|
||||
p = sub.add_parser(cmd)
|
||||
p.add_argument("component", help="component name")
|
||||
p.add_argument("dest", help="target folder path")
|
||||
if cmd == "publish":
|
||||
p.add_argument(
|
||||
"--dist",
|
||||
action="store_true",
|
||||
help="ship only the built bundle (dist/ + manifest), not the sources",
|
||||
)
|
||||
|
||||
p = sub.add_parser("watch", help="continuous two-way sync (foreground, ctrl+c to stop)")
|
||||
p.add_argument("component", help="component name")
|
||||
|
||||
35
ctrl/start.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
# Start a built room by dispatching to its own ctrl/start.sh in gen/.
|
||||
#
|
||||
# Usage:
|
||||
# ./ctrl/start.sh # standalone, foreground
|
||||
# ./ctrl/start.sh amar -d # amar, detached
|
||||
# ./ctrl/start.sh sample --build # flags pass straight through
|
||||
#
|
||||
# Every room ships its own start script (gen/<room>/ctrl/start.sh) and stays
|
||||
# runnable on its own — this only saves cd'ing there and picks the default room.
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
ROOM="standalone"
|
||||
if [[ $# -gt 0 && "$1" != -* ]]; then
|
||||
ROOM="$1"
|
||||
shift
|
||||
fi
|
||||
|
||||
ROOM_DIR="$ROOT_DIR/gen/$ROOM"
|
||||
|
||||
if [[ ! -d "$ROOM_DIR" ]]; then
|
||||
echo "Room '$ROOM' is not built — run: ./ctrl/build.sh $ROOM" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -x "$ROOM_DIR/ctrl/start.sh" ]]; then
|
||||
echo "No start script at gen/$ROOM/ctrl/start.sh" >&2
|
||||
echo "(rebuild the room, or start it by hand from $ROOM_DIR)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec "$ROOM_DIR/ctrl/start.sh" "$@"
|
||||
27
ctrl/stop.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# Stop a running room by dispatching to its own ctrl/stop.sh in gen/.
|
||||
#
|
||||
# Usage:
|
||||
# ./ctrl/stop.sh # standalone
|
||||
# ./ctrl/stop.sh amar # a named room
|
||||
#
|
||||
# Mirror of ctrl/start.sh — the room's own script does the work.
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
ROOM="standalone"
|
||||
if [[ $# -gt 0 && "$1" != -* ]]; then
|
||||
ROOM="$1"
|
||||
shift
|
||||
fi
|
||||
|
||||
ROOM_DIR="$ROOT_DIR/gen/$ROOM"
|
||||
|
||||
if [[ ! -x "$ROOM_DIR/ctrl/stop.sh" ]]; then
|
||||
echo "No stop script at gen/$ROOM/ctrl/stop.sh — nothing to stop." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exec "$ROOM_DIR/ctrl/stop.sh" "$@"
|
||||
93
docs/data/en/artery-plexuses.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Plexuses
|
||||
|
||||
A **plexus** is a full app — the vocabulary has always said so
|
||||
(*"full app with backend, frontend and DB"*). What was missing is that a plexus
|
||||
is **exported, not served**. It is compiled to a distributable file the way vite
|
||||
builds for production, and that compile is the whole point of the format.
|
||||
|
||||
```bash
|
||||
make dist # compile the plexus UIs for standalone
|
||||
make dist sample # for a named room
|
||||
```
|
||||
|
||||
Output is one `index.html` per plexus under `gen/<room>/plexuses/<name>/`,
|
||||
carrying its theme, its data and its diagrams inline. No server, no node, no
|
||||
network. Zip it, mail it, double-click it.
|
||||
|
||||
`make build` runs the same step as part of a room build. `make dist` exists for
|
||||
the loop where the UI is what you are working on — rebuilding a whole room to
|
||||
see a CSS change is a slow way to iterate.
|
||||
|
||||
## The constraint that shapes it
|
||||
|
||||
It has to open from a **double-clicked file on a machine with no egress**. That
|
||||
is the state a regulated Windows box is usually in, and it rules out three
|
||||
things a normal web app does:
|
||||
|
||||
| Ruled out | Because |
|
||||
| --- | --- |
|
||||
| `fetch("bundle.json")` | `file://` treats every sibling file as cross-origin |
|
||||
| `<link href="/theme.css">` | an absolute path assumes a server at the root |
|
||||
| a webfont `@import` | a blocked stylesheet is a stall, not a fallback |
|
||||
|
||||
So the data is a JS object, the theme is inlined at compile time, and the fonts
|
||||
are stacks. The test that matters is opening the output with the network off and
|
||||
seeing zero failed requests — everything else is cosmetic.
|
||||
|
||||
## Declaring one
|
||||
|
||||
A room opts in through `cfg/<room>/data/plexuses.json`, the same shape and the
|
||||
same place as its sibling `data/*.json` files:
|
||||
|
||||
```json
|
||||
{ "items": [ { "name": "bundle" } ] }
|
||||
```
|
||||
|
||||
A room may override anything the plexus declares — most usefully the theme:
|
||||
|
||||
```json
|
||||
{ "items": [ { "name": "bundle", "theme": "mcrn" } ] }
|
||||
```
|
||||
|
||||
## Writing one
|
||||
|
||||
```
|
||||
soleprint/artery/plexuses/<name>/
|
||||
plexus.json identity, theme, the data the page renders
|
||||
app/index.html the template
|
||||
```
|
||||
|
||||
`build.py` fills these placeholders and writes one file:
|
||||
|
||||
| Placeholder | Becomes |
|
||||
| --- | --- |
|
||||
| `%%THEME_CSS%%` | tokens plus every theme, so the switcher has something to switch to |
|
||||
| `%%BUNDLE%%` | `plexus.json` as a JS object |
|
||||
| `%%GRAPH%%` | a rendered SVG from `docs/graphs/`, inlined |
|
||||
| `%%TITLE%%` `%%DESCRIPTION%%` `%%DEFAULT_THEME%%` `%%BUILT%%` | from the manifest |
|
||||
|
||||
Anything else in `app/` is copied alongside, for a plexus that outgrows one file.
|
||||
|
||||
## The bundle plexus
|
||||
|
||||
The one that ships. It answers "what does a rig installation have at its
|
||||
disposal" — tools, cabinets, veins, themes — and embeds the system diagram.
|
||||
|
||||
Because the SVG is **inlined** rather than `<img>`-linked, the theme switch
|
||||
recolours the diagram too: graphviz writes `class="node accent"` into the SVG,
|
||||
and CSS outranks the presentation attributes it bakes in. Switching to `lucid`
|
||||
turns both the page and the diagram into something printable, which is the
|
||||
demonstration the format exists for.
|
||||
|
||||
## Not the same as rig's bundle
|
||||
|
||||
Two artifacts, both called bundle, generated by different repos:
|
||||
|
||||
| | soleprint | rig |
|
||||
| --- | --- | --- |
|
||||
| Command | `make dist` | `make manifest` in `sample-rig` |
|
||||
| Artifact | `plexuses/<name>/index.html` | `generated/<slug>.yaml` |
|
||||
| Needs | nothing | kind + MetalLB |
|
||||
| Answers | what shipped, on any machine | whether this cluster install is sound |
|
||||
|
||||
Complementary. One proves the environment, the other travels.
|
||||
@@ -31,10 +31,10 @@ Core books live in `soleprint/atlas/books/`. Room-specific books live in `cfg/<r
|
||||
At build time, room-specific books are merged into the output:
|
||||
|
||||
```
|
||||
soleprint/atlas/books/ # Core books (all rooms)
|
||||
cfg/amar/atlas/books/ # Amar-specific books
|
||||
soleprint/atlas/books/ # Core books (all rooms)
|
||||
cfg/<room>/atlas/books/ # Room-specific books
|
||||
↓ build.py
|
||||
gen/amar/soleprint/atlas/books/ # Merged output
|
||||
gen/<room>/soleprint/atlas/books/ # Merged output
|
||||
```
|
||||
|
||||
Core books ship with every room. Room books add to or override them. The build copies the core first, then overlays the room-specific content.
|
||||
|
||||
@@ -11,7 +11,6 @@ A room is an isolated configuration. Each room lives in `cfg/<room>/` and contai
|
||||
```
|
||||
cfg/
|
||||
standalone/ # Soleprint only, no managed app
|
||||
amar/ # Soleprint wrapping the Amar application
|
||||
myroom/ # Your room
|
||||
```
|
||||
|
||||
|
||||
144
docs/data/en/export.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# Export / Compile
|
||||
|
||||
Soleprint's source tree is not what runs. `build.py` compiles the framework plus
|
||||
a room's configuration into a self-contained instance under `gen/<room>/`, and
|
||||
that directory is what a container boots, what `deploy.sh` rsyncs, and what the
|
||||
cluster manifests point at.
|
||||
|
||||
Everything below is a `make` target, and every target is one script in `ctrl/`.
|
||||
The logic lives in the scripts, never in the Makefile.
|
||||
|
||||
```bash
|
||||
make build # cfg/standalone -> gen/standalone
|
||||
make build sample # cfg/sample -> gen/sample
|
||||
make start # run it
|
||||
```
|
||||
|
||||
## The targets
|
||||
|
||||
| Command | Runs | Does |
|
||||
| --- | --- | --- |
|
||||
| `make build [<room>\|all\|models]` | `ctrl/build.sh` | compile a room into `gen/` |
|
||||
| `make dist [<room>]` | `ctrl/dist.sh` | compile just the plexus UIs to single files |
|
||||
| `make docs [serve\|graphs]` | `ctrl/docs.sh` | serve the docs, or re-render the diagrams |
|
||||
| `make start [<room>] [-d]` | `ctrl/start.sh` | run a built room's compose stack |
|
||||
| `make stop [<room>]` | `ctrl/stop.sh` | stop it |
|
||||
| `make cluster [up\|down\|status]` | `ctrl/cluster.sh` | the shared kind cluster |
|
||||
| `make component [list\|publish\|sync\|watch\|diff]` | `ctrl/spr.py` | publish a distributable component |
|
||||
| `make deploy` | `ctrl/deploy.sh` | rsync `gen/standalone` to the server and restart |
|
||||
|
||||
Bare words pass straight through, so `make build sample` becomes
|
||||
`ctrl/build.sh sample`. Anything starting with a dash would be eaten by make
|
||||
itself, so those go through `ARGS`:
|
||||
|
||||
```bash
|
||||
make deploy ARGS="--build"
|
||||
make component ARGS="publish soleprint-ui /tmp/out --dist"
|
||||
```
|
||||
|
||||
## What a build does
|
||||
|
||||
`python build.py --cfg <room>` runs these in order:
|
||||
|
||||
1. **Clean** `gen/<room>/`. A build is not incremental — a stale file left
|
||||
behind is worse than a slow build.
|
||||
2. **Copy the framework.** `main.py`, `run.py`, `index.html`, `Dockerfile`,
|
||||
`requirements.txt`, `dataloader/`, `common/`, and the three systems
|
||||
(`artery/`, `atlas/`, `station/`).
|
||||
3. **Merge the room** (`copy_cfg`). `cfg/<room>/config.json` lands in `cfg/`,
|
||||
`data/*.json` in `data/`, and anything under `cfg/<room>/soleprint/artery|atlas|station/`
|
||||
is merged *over* the framework copy — which is how a room adds its own vein,
|
||||
shunt, tool or generator without forking the tree.
|
||||
4. **Compose cabinets.** The dependency containers the room declared in
|
||||
`data/cabinets.json` are merged into its `docker-compose.yml`. See
|
||||
[Cabinets](#station-cabinets).
|
||||
5. **Export plexuses.** Each plexus the room declared in `data/plexuses.json` is
|
||||
compiled to a single self-contained `index.html` — theme, data and diagrams
|
||||
inlined, so it opens with no server. See [Plexuses](#artery-plexuses).
|
||||
6. **Generate models.** modelgen reads the room's `config.json` and writes
|
||||
`models/pydantic/__init__.py`.
|
||||
7. **Render k8s** (optional). When the room's config enables it,
|
||||
`soleprint/ctrl/k8s/` writes manifests and lifecycle scripts.
|
||||
|
||||
## What comes out
|
||||
|
||||
A standalone room is a flat instance:
|
||||
|
||||
```
|
||||
gen/standalone/
|
||||
run.py main.py Dockerfile docker-compose.yml
|
||||
artery/ atlas/ station/ common/
|
||||
cfg/config.json
|
||||
data/*.json
|
||||
models/pydantic/
|
||||
plexuses/<name>/index.html # one file each, opens with no server
|
||||
```
|
||||
|
||||
A **managed** room — one that wraps an existing application — is three folders
|
||||
instead, because soleprint sits beside the app rather than containing it:
|
||||
|
||||
```
|
||||
gen/<room>/
|
||||
<app>/ the application's repos, plus its ctrl scripts
|
||||
link/ bridge code between the two
|
||||
soleprint/ the instance, exactly as above
|
||||
```
|
||||
|
||||
`build.py` picks between them on whether the room's `config.json` has a
|
||||
`managed` block. `gen/` is gitignored in full: it is an artifact, and the way to
|
||||
change it is to change `cfg/<room>/` and rebuild.
|
||||
|
||||
## Distributing components
|
||||
|
||||
Rooms are compiled; *components* are published. `registry.json` lists what can
|
||||
be shipped out of this repo on its own:
|
||||
|
||||
```bash
|
||||
make component # list
|
||||
make component ARGS="publish soleprint-ui /tmp/out --dist"
|
||||
make component ARGS="watch soleprint-ui ../unt/ui/framework"
|
||||
```
|
||||
|
||||
`--dist` copies only the built bundle — `dist/**` plus `package.json`,
|
||||
`README.md` and `LICENSE` — rather than the source. It refuses to publish an
|
||||
empty `dist/`, because a component whose bundle was never built is the failure
|
||||
that shows up later as a container that starts and renders nothing:
|
||||
|
||||
```
|
||||
no build at soleprint/common/ui
|
||||
build it first: cd soleprint/common/ui && pnpm build
|
||||
```
|
||||
|
||||
Each publish leaves a `.spr` stamp in the destination recording name, version,
|
||||
type, source and mode, so a copy can say where it came from.
|
||||
|
||||
## Deploying
|
||||
|
||||
```bash
|
||||
make deploy ARGS="--build" # rebuild, sync, restart
|
||||
make deploy ARGS="--sync-only" # sync, leave it running
|
||||
```
|
||||
|
||||
`deploy.sh` rsyncs `gen/standalone/` and runs `docker compose up -d --build` on
|
||||
the far side. `.env` is excluded, so server secrets stay on the server.
|
||||
|
||||
## Running without building
|
||||
|
||||
`python run.py` from `soleprint/` serves every subsystem on one port (12000 by
|
||||
default) straight from the source tree. It is for developing the framework
|
||||
itself; a room's `cfg/config.json` does not exist there, so the landing pages
|
||||
fall back to their defaults. Rooms use docker.
|
||||
|
||||
## Diagrams
|
||||
|
||||
The `.dot` sources under `docs/graphs/` carry structure; the palette lives in
|
||||
`docs/graphs/themes/*.gvpr` and is applied at render time, so one source renders
|
||||
in every theme.
|
||||
|
||||
```bash
|
||||
make docs graphs # every graph, every theme
|
||||
make docs graphs lucid # one theme
|
||||
```
|
||||
|
||||
`<name>.svg` is the dark default the docs link to; other themes write
|
||||
`<name>.<theme>.svg`. See [Themes](#themes).
|
||||
107
docs/data/en/station-cabinets.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Cabinets
|
||||
|
||||
A cabinet is a **dependency container** a room can switch on: postgres, redis,
|
||||
airflow. The vocabulary already had the word — `execution.container` in every
|
||||
room's `config.json` is *"Cabinet — tool container"* — and until now nothing
|
||||
stood behind it.
|
||||
|
||||
The problem it solves: a generated artifact knows what it needs and had no way
|
||||
to say so. A shunt built from a client's spreadsheets holds its rows in memory
|
||||
happily, but the moment you want them to survive a restart you need postgres,
|
||||
and wiring postgres in meant hand-editing a room's `docker-compose.yml` and then
|
||||
hand-editing the cluster too. A cabinet is that declaration, made once and read
|
||||
by both paths.
|
||||
|
||||
## Switching one on
|
||||
|
||||
Add `cfg/<room>/data/cabinets.json`, the same shape as its sibling `data/*.json`
|
||||
files:
|
||||
|
||||
```json
|
||||
[
|
||||
{ "name": "postgres" },
|
||||
{ "name": "redis" },
|
||||
{ "name": "airflow", "env": { "AIRFLOW_ADMIN_PASSWORD": "change-me" } }
|
||||
]
|
||||
```
|
||||
|
||||
Then build. The compose merge is a step in [Export / Compile](#export):
|
||||
|
||||
```bash
|
||||
python build.py --cfg sample
|
||||
cd gen/sample && docker compose up -d
|
||||
```
|
||||
|
||||
`build.py` merges each cabinet's compose fragment into the room's
|
||||
`docker-compose.yml`, declares its named volumes, and appends its settings to
|
||||
`.env.example` — never to `.env`.
|
||||
|
||||
**A service the room already declares wins.** `cfg/amar/docker-compose.yml`
|
||||
ships its own `db`; switching the postgres cabinet on will not replace it. The
|
||||
build says so when it skips one:
|
||||
|
||||
```
|
||||
Composing cabinets...
|
||||
cabinets: redis, airflow
|
||||
cabinets already declared by the room, left alone: postgres
|
||||
```
|
||||
|
||||
Dependencies come along automatically. Airflow without a metadata database is a
|
||||
container that exits on boot, so asking for `airflow` brings `postgres` and
|
||||
`redis` with it, ordered so compose reads them before the thing that needs them.
|
||||
|
||||
## On a cluster
|
||||
|
||||
Every cabinet names a `rig_addon`. Where a room runs on kind rather than
|
||||
compose, the same dependency installs as a rig addon of that name:
|
||||
|
||||
```bash
|
||||
cd rig
|
||||
PROFILE=data make cluster up
|
||||
PROFILE=data make addons install
|
||||
|
||||
kubectl -n data port-forward svc/postgres 5432:5432
|
||||
kubectl -n data port-forward svc/airflow 8080:8080
|
||||
```
|
||||
|
||||
The two paths are deliberately separate — compose for a laptop, manifests for a
|
||||
cluster — and `rig_addon` is the thread between them, so the room declares the
|
||||
dependency once either way. The addons generate their own passwords on first
|
||||
install and keep them across re-runs, so re-running never rotates a credential
|
||||
out from under something already connected.
|
||||
|
||||
## What ships
|
||||
|
||||
| Cabinet | Image | Notes |
|
||||
| --- | --- | --- |
|
||||
| `postgres` | `postgres:16-alpine` | healthcheck wired, so `depends_on: service_healthy` works |
|
||||
| `redis` | `redis:7-alpine` | cache, and the broker for anything queue-shaped |
|
||||
| `airflow` | `apache/airflow:2.10.4` | one container on `standalone`; needs postgres and redis |
|
||||
|
||||
## Writing one
|
||||
|
||||
```
|
||||
soleprint/station/cabinets/<name>/
|
||||
cabinet.json what it is, what it needs, what it exports
|
||||
service.yml the compose service, verbatim
|
||||
```
|
||||
|
||||
`cabinet.json`:
|
||||
|
||||
| Key | Purpose |
|
||||
| --- | --- |
|
||||
| `name` | must match the directory |
|
||||
| `title`, `description` | shown on the station index |
|
||||
| `service` | the key to merge under in `services:` (defaults to `name`) |
|
||||
| `env` | settings and defaults, written to `.env.example` |
|
||||
| `volumes` | named volumes to declare at the top level |
|
||||
| `depends_on` | other cabinets that must come with it |
|
||||
| `rig_addon` | the matching `rig/ctrl/addons/<name>.sh`, if there is one |
|
||||
| `notes` | lines written into `.env.example` as comments |
|
||||
|
||||
`service.yml` is a plain compose fragment with one top-level key — the service
|
||||
name. It stays YAML rather than being generated from JSON so it reads like the
|
||||
file it becomes, and so anything compose supports is available without this tool
|
||||
learning about it first.
|
||||
|
||||
Adding a cabinet is adding a directory. Nothing dispatches on the name.
|
||||
@@ -1,6 +1,8 @@
|
||||
# Datagen
|
||||
|
||||
Test data generator using faker. Produces realistic, domain-specific data for testing and development.
|
||||
Test data generator. Produces realistic, domain-specific records for testing and
|
||||
development, from generators a room writes or that
|
||||
[modelgen](#station-modelgen) writes for it.
|
||||
|
||||
**Status:** live
|
||||
|
||||
@@ -8,50 +10,95 @@ Test data generator using faker. Produces realistic, domain-specific data for te
|
||||
|
||||
## What It Does
|
||||
|
||||
Datagen generates fake but realistic data. Names, emails, addresses, transactions -- whatever your domain needs. It uses Python's faker library under the hood.
|
||||
Datagen hands out instances of a room's models. Core ships the base class, the
|
||||
discovery, the HTTP API and the browser UI; the generators themselves belong to
|
||||
a room, because what counts as realistic is a property of the domain.
|
||||
|
||||
Core datagen is a placeholder. The real work happens in room-specific generators.
|
||||
Generation is stdlib `random`, `uuid` and `datetime` — **not** faker, which is
|
||||
not a dependency of this repo.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
soleprint/station/tools/datagen/ # Core (base classes, placeholder)
|
||||
cfg/<room>/soleprint/station/tools/datagen/ # Room-specific generators
|
||||
soleprint/station/tools/datagen/ # base class, api, UI
|
||||
cfg/<room>/soleprint/station/tools/datagen/ # the room's generators
|
||||
```
|
||||
|
||||
After build, both merge into `gen/<room>/station/tools/datagen/`.
|
||||
|
||||
## Pattern
|
||||
## The contract
|
||||
|
||||
Rooms subclass a base generator and provide domain-specific data factories:
|
||||
A generator subclasses `BaseDataGenerator` and defines **one method per model,
|
||||
named after it**. There is no registration step: the method name *is* the model
|
||||
name.
|
||||
|
||||
```python
|
||||
from station.tools.datagen.base import BaseGenerator
|
||||
from station.tools.datagen.base import BaseDataGenerator
|
||||
|
||||
class AmarDataGenerator(BaseGenerator):
|
||||
def generate_customers(self, count=10):
|
||||
return [self.fake_customer() for _ in range(count)]
|
||||
class RoomDataGenerator(BaseDataGenerator):
|
||||
def customer(self, **kwargs):
|
||||
return {"id": str(uuid4()), "name": ..., "email": ..., **kwargs}
|
||||
|
||||
def fake_customer(self):
|
||||
return {
|
||||
"name": self.faker.name(),
|
||||
"email": self.faker.email(),
|
||||
"phone": self.faker.phone_number(),
|
||||
}
|
||||
def invoice(self, customer_id=None, **kwargs):
|
||||
return {"id": str(uuid4()), "customer_id": customer_id, **kwargs}
|
||||
```
|
||||
|
||||
The base class provides:
|
||||
|
||||
| Method | Does |
|
||||
| --- | --- |
|
||||
| `generate(model, count=1, **kwargs)` | calls the matching method `count` times; `kwargs` pass through to every call |
|
||||
| `available_models()` | the method names, which are the model names |
|
||||
| `schema()` | override to return a graphgen-compatible schema |
|
||||
|
||||
Discovery is by convention too: any `*.py` in the datagen directory whose first
|
||||
class ends in `Generator` is loaded and keyed by its filename.
|
||||
|
||||
## Generating a generator
|
||||
|
||||
Writing one by hand is optional. modelgen's `datagen` target emits the whole
|
||||
class from a schema — and when the schema came from spreadsheets, the generated
|
||||
class **samples the real rows** rather than inventing values:
|
||||
|
||||
```bash
|
||||
python -m station.tools.modelgen from-tabular -s ./sheets -o out/ -t datagen
|
||||
python -m station.tools.modelgen from-openapi -s api.yaml -o out/ -t datagen
|
||||
```
|
||||
|
||||
Each room defines what data it needs. Core provides the faker instance and base class. Rooms provide the factories.
|
||||
This is how [shuntgen](#station-shuntgen) fills a generated shunt.
|
||||
|
||||
## HTTP API
|
||||
|
||||
Mounted under `/station/tools/datagen/`:
|
||||
|
||||
| Route | Returns |
|
||||
| --- | --- |
|
||||
| `GET /` | the browser UI |
|
||||
| `GET /api/generators` | loaded generator files and their models |
|
||||
| `GET /api/models?generator=` | model names |
|
||||
| `POST /api/generate` | `{model, count, generator?, kwargs?}` → the records |
|
||||
| `GET /api/schema?generator=` | the generator's schema, if it exposes one |
|
||||
|
||||
## Room Configuration
|
||||
## Feeding graphgen
|
||||
|
||||
Room generators live in `cfg/<room>/soleprint/station/tools/datagen/`. They are fully self-contained -- they define their own models, factories, and output formats.
|
||||
A generator that overrides `schema()` is surfaced at `/api/schema` in the format
|
||||
[graphgen](#station-graphgen) reads, so the same definition draws the diagram:
|
||||
|
||||
The core module provides:
|
||||
- Base generator class with faker instance
|
||||
- CLI entry point
|
||||
- Output formatting (JSON, CSV)
|
||||
```python
|
||||
def schema(self):
|
||||
return {
|
||||
"models": {
|
||||
"Invoice": {
|
||||
"doc": "A billed order.",
|
||||
"fields": {
|
||||
"id": {"type": "UUID", "pk": True},
|
||||
"customer_id": {"type": "FK:Customer"},
|
||||
"total": {"type": "float"},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Rooms provide:
|
||||
- Domain-specific generator subclasses
|
||||
- Field definitions and relationships
|
||||
- Volume and distribution configuration
|
||||
`FK:<Model>` and `M2M:<Model>` are how relations are written. modelgen's
|
||||
`datagen` target emits this method for you.
|
||||
|
||||
@@ -1,54 +1,111 @@
|
||||
# Modelgen
|
||||
|
||||
Generates platform-specific models from JSON Schema. Reads schema once, writes models for multiple targets.
|
||||
Multi-source, multi-target model generator. Reads a schema from wherever it
|
||||
already lives, and writes it out for every stack that needs it.
|
||||
|
||||
**Status:** dev
|
||||
**Status:** live
|
||||
|
||||
---
|
||||
|
||||
## What It Does
|
||||
|
||||
Modelgen takes a JSON Schema definition and produces model code for different platforms:
|
||||
Everything passes through one intermediate representation — `ModelDefinition`,
|
||||
`FieldDefinition`, `EnumDefinition`. **Loaders** fill it, **generators** emit
|
||||
from it, and the two sides do not know about each other. Adding an input means
|
||||
one extractor and every output comes with it; adding an output means one
|
||||
generator and every input already feeds it.
|
||||
|
||||
- **Pydantic** -- Python data validation models
|
||||
- **Django ORM** -- Django model classes
|
||||
- **Prisma** -- Prisma schema definitions
|
||||
```
|
||||
dataclasses ─┐ ┌─ pydantic
|
||||
Django │ ├─ django
|
||||
SQLAlchemy ├──▶ ModelDefinition ──▶├─ sqlmodel
|
||||
a live DB │ FieldDefinition ├─ typescript
|
||||
OpenAPI │ EnumDefinition ├─ protobuf
|
||||
CSV/ODS ─┘ ├─ prisma
|
||||
├─ strawberry
|
||||
├─ schema (graphgen)
|
||||
└─ datagen
|
||||
```
|
||||
|
||||
One schema, multiple outputs.
|
||||
Core is **pure standard library**. It is published as `soleprint-modelgen` and
|
||||
installs with no dependencies; live-database extraction is an extra
|
||||
(`pip install "soleprint-modelgen[db]"`), and YAML specs need PyYAML.
|
||||
|
||||
## Extractors
|
||||
## Sources
|
||||
|
||||
Modelgen also works in reverse. Extractors read existing codebases and produce a normalized schema representation:
|
||||
| Command | Reads |
|
||||
| --- | --- |
|
||||
| `from-schema` | Python dataclasses in a `schema/` folder |
|
||||
| `from-config` | a room's `config.json` |
|
||||
| `extract` | a Django or SQLAlchemy codebase (`--framework auto` detects) |
|
||||
| `from-db` | a live database, any SQLAlchemy dialect |
|
||||
| `from-openapi` | an OpenAPI 3.x / Swagger 2.0 document |
|
||||
| `from-tabular` | a directory of `.csv` / `.tsv` / `.ods` spreadsheets |
|
||||
|
||||
- **Django extractor** -- reads Django model files
|
||||
- **SQLAlchemy extractor** -- reads SQLAlchemy model files
|
||||
- **Prisma extractor** -- reads Prisma schema files
|
||||
```bash
|
||||
python -m station.tools.modelgen from-openapi -s api.yaml -o out/ -t pydantic,typescript,schema
|
||||
python -m station.tools.modelgen from-tabular -s ./sheets -o out/ -t pydantic,datagen
|
||||
python -m station.tools.modelgen extract -s /path/to/django -o out/ -t prisma
|
||||
python -m station.tools.modelgen from-db -u postgresql://… -o out/ -t typescript
|
||||
python -m station.tools.modelgen list-formats
|
||||
```
|
||||
|
||||
Extractors feed into graphgen for visualization.
|
||||
### From a spec
|
||||
|
||||
## Output
|
||||
`components.schemas` (or Swagger's `definitions`) become models. `$ref` chains
|
||||
and `allOf` are resolved, enums are materialised as real `Enum` classes so every
|
||||
target names them properly, and a referenced object becomes a relation rather
|
||||
than a nested type — the same call the database extractor makes, and what keeps
|
||||
the generated code valid for every target.
|
||||
|
||||
Generated models are written to `gen/<room>/models/`.
|
||||
The parse also yields the *operations*, which is what
|
||||
[shuntgen](#station-shuntgen) turns into routes.
|
||||
|
||||
```
|
||||
gen/<room>/models/
|
||||
├── pydantic/
|
||||
├── django/
|
||||
└── prisma/
|
||||
```
|
||||
### From spreadsheets
|
||||
|
||||
## CLI
|
||||
One model per CSV file, one per sheet in an ODS workbook. Column types are
|
||||
inferred from the values actually present, and a blank cell makes the column
|
||||
optional. Keys and relations are inferred by name and then confirmed against the
|
||||
data: an `id` column that is not unique is not treated as a key, and
|
||||
`customer_id` is only a foreign key if a `customers` sheet came with it.
|
||||
|
||||
```bash
|
||||
python -m modelgen
|
||||
```
|
||||
The rows are kept, not just the shape — which is what lets the `datagen` target
|
||||
sample real values instead of inventing them.
|
||||
|
||||
Reads from `schema.json` (the project source of truth) and writes to the configured output directory.
|
||||
ODS is read with `zipfile` and `ElementTree`. No odfpy, no pandas: the
|
||||
dependency-free promise is what makes this package publishable on its own.
|
||||
|
||||
## Shared Distribution
|
||||
## Targets
|
||||
|
||||
Modelgen is also distributed as a shared component via `ctrl/spr.py`. This allows other projects to use model generation without running full soleprint.
|
||||
`pydantic`, `django`, `sqlmodel`, `typescript` (`ts`), `protobuf` (`proto`),
|
||||
`prisma`, `strawberry`, `schema` (`jsonschema`), `datagen`.
|
||||
|
||||
## Schema Source
|
||||
Two are worth calling out:
|
||||
|
||||
- **`schema`** writes the graphgen-compatible `schema.json` — the portable
|
||||
artifact [graphgen](#station-graphgen) and databrowse read directly.
|
||||
Relations come out as `FK:<Model>` and `M2M:<Model>`.
|
||||
- **`datagen`** writes a `BaseDataGenerator` subclass for
|
||||
[datagen](#station-datagen), including its `schema()` override. Given
|
||||
spreadsheet rows it samples them; otherwise it synthesises from the types.
|
||||
|
||||
Multiple targets in one run get one file each, named `models_<target><ext>`.
|
||||
|
||||
## In a build
|
||||
|
||||
`build.py` calls modelgen during every room build, writing
|
||||
`gen/<room>/models/pydantic/__init__.py` from the room's `config.json`. See
|
||||
[Export / Compile](#export).
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
cd soleprint/station/tools
|
||||
python -m unittest modelgen.tests.test_extractors
|
||||
```
|
||||
|
||||
The source of truth is `schema.json` at the project root. All model generation starts from this file. Room-specific schema extensions live in `cfg/<room>/models/`.
|
||||
stdlib `unittest`, no pytest, and every input is built in a temp directory — the
|
||||
tests have to pass with nothing installed and nothing else in the tree. Run them
|
||||
from `station/tools/`, not from inside `modelgen/`: the package ships a
|
||||
`types.py`, and putting its own directory on `sys.path` shadows the standard
|
||||
library module of that name.
|
||||
|
||||
129
docs/data/en/station-shuntgen.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# Shuntgen
|
||||
|
||||
Generates runnable [shunts](#artery-shunts) from the two things people actually
|
||||
have: a service contract, or a folder of spreadsheets.
|
||||
|
||||
Writing a shunt by hand means copying `artery/shunts/example/` and filling in
|
||||
`responses.json` entry by entry. That is fine for three endpoints and untenable
|
||||
for eighty — and it is the wrong work anyway, because the endpoints are already
|
||||
described in the spec somebody handed you.
|
||||
|
||||
```bash
|
||||
# a spec you were handed
|
||||
python -m station.tools.shuntgen from-openapi -s api.yaml -o artery/shunts/petstore
|
||||
|
||||
# sheets a client sent
|
||||
python -m station.tools.shuntgen from-tabular -s ./sheets -o artery/shunts/books
|
||||
|
||||
python -m station.tools.shuntgen list
|
||||
```
|
||||
|
||||
Run from `soleprint/`. Also in the browser at `/station/tools/shuntgen/`, where
|
||||
you can upload a spec, preview the routes it would serve, and generate.
|
||||
|
||||
## What comes out
|
||||
|
||||
```
|
||||
artery/shunts/<name>/
|
||||
main.py builds the app from the spec
|
||||
run.py uvicorn entry point (PORT, or depot/config.json)
|
||||
shunt_runtime.py vendored runtime — no soleprint import
|
||||
models.py pydantic, via modelgen
|
||||
datagen_<name>.py BaseDataGenerator subclass, via modelgen
|
||||
depot/spec.json routes, collections and schema
|
||||
depot/responses.json pinned overrides — yours, never overwritten
|
||||
depot/config.json delays, error rate, prefill — yours, never overwritten
|
||||
depot/data.json imported rows
|
||||
templates/index.html config UI
|
||||
README.md
|
||||
```
|
||||
|
||||
```bash
|
||||
cd artery/shunts/books && python run.py
|
||||
curl localhost:8098/customers
|
||||
```
|
||||
|
||||
The routes are built at startup from `spec.json` rather than written out as
|
||||
source. That keeps the generated code short enough to read, and puts the
|
||||
behaviour in one reviewable place: fixing `runtime.py` fixes every shunt, and
|
||||
regenerating is a copy.
|
||||
|
||||
## Where a response comes from
|
||||
|
||||
First hit wins:
|
||||
|
||||
1. `depot/responses.json` — a pinned override, keyed `"METHOD /path"`
|
||||
2. the store — rows imported from sheets, plus anything POSTed since
|
||||
3. the spec's `example`, if the source document carried one
|
||||
4. `datagen_<name>.py`, synthesising from the schema
|
||||
5. `{}`
|
||||
|
||||
The store is what makes it behave like a service rather than a random-value
|
||||
faucet: POST something and GET it back, ask for `/pets/7` and get the pet whose
|
||||
id is 7. Collections that arrived with no rows are prefilled with generated
|
||||
ones, so the first call answers with something.
|
||||
|
||||
## Two sources, one pipeline
|
||||
|
||||
Both inputs are [modelgen](#station-modelgen) extractors, so the same shapes
|
||||
also generate pydantic, TypeScript, prisma and a
|
||||
[graphgen](#station-graphgen) schema:
|
||||
|
||||
```bash
|
||||
python -m station.tools.modelgen from-openapi -s api.yaml -o out/ -t pydantic,typescript
|
||||
python -m station.tools.modelgen from-tabular -s ./sheets -o out/ -t schema,datagen
|
||||
```
|
||||
|
||||
| Source | Becomes | Routes |
|
||||
| --- | --- | --- |
|
||||
| OpenAPI 3.x / Swagger 2.0 | one model per schema; enums become real Enums, `$ref` becomes a relation | the operations the document declares |
|
||||
| `.csv` / `.tsv` / `.ods` | one model per file or sheet, types inferred per column | five CRUD routes per table |
|
||||
|
||||
Keys and relations are inferred by name and then **checked against the data**:
|
||||
an `id` column that is not unique is not treated as a key, and `customer_id` is
|
||||
only a foreign key if a `customers` sheet came with it.
|
||||
|
||||
ODS is read with `zipfile` and `ElementTree` — no odfpy, no pandas — which is
|
||||
what lets modelgen stay dependency-free and publishable on its own.
|
||||
|
||||
## Control endpoints
|
||||
|
||||
Every generated shunt serves these:
|
||||
|
||||
| Endpoint | Purpose |
|
||||
| --- | --- |
|
||||
| `GET /health` | liveness |
|
||||
| `GET /mock/spec` | the routes it was built from |
|
||||
| `GET /mock/stats` | call counts and row counts |
|
||||
| `POST /mock/reset` | restore the imported rows, clear counters |
|
||||
| `GET,POST /mock/config` | delays, error rate, `unknown_id`, page size |
|
||||
| `GET,POST /mock/responses` | pin an override; set a key to `null` to drop it |
|
||||
|
||||
```bash
|
||||
# make it slow and flaky, the way the real thing is
|
||||
curl -X POST localhost:8098/mock/config \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{"enable_random_delays": true, "error_rate": 0.2}'
|
||||
|
||||
# make one call answer something specific
|
||||
curl -X POST localhost:8098/mock/responses \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{"GET /customers/1": {"id": 1, "name": "PINNED"}}'
|
||||
```
|
||||
|
||||
`unknown_id` decides what an unknown key does: `generate` (the default) invents
|
||||
a record wearing the id that was asked for; `404` refuses it. Generate by
|
||||
default, because a client pointed at a fresh shunt should just work — flip it
|
||||
when the error path is what you are testing.
|
||||
|
||||
## Dependency containers
|
||||
|
||||
`--cabinet postgres,redis` writes a `cabinet.json` declaring what the shunt
|
||||
expects. `build.py` composes those services into the room's compose file, and on
|
||||
a cluster they install as rig addons of the same name. See
|
||||
[Cabinets](#station-cabinets).
|
||||
|
||||
## Regenerating
|
||||
|
||||
Everything is overwritten except `depot/responses.json` and `depot/config.json`.
|
||||
Those two are yours.
|
||||
110
docs/data/en/themes.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Themes
|
||||
|
||||
Three themes ship, and the same palettes drive the diagrams as well as the
|
||||
pages. `soleprint` is the default; the others are switched to on purpose.
|
||||
|
||||
| Theme | Reads as | For |
|
||||
| --- | --- | --- |
|
||||
| `soleprint` | dark, rounded, amber | the default — tools and dev chrome |
|
||||
| `mcrn` | dark, square, monospace, orange glow | the terminal look, matched to mariano.mcrn.ar |
|
||||
| `lucid` | light, hairline, printable | regulated documents, and sitting beside a real lucid.app export |
|
||||
|
||||
Switch with `?theme=lucid`, or the toggle in the corner. The choice persists.
|
||||
|
||||
## Where it lives
|
||||
|
||||
```
|
||||
soleprint/common/theme/
|
||||
tokens.css the vocabulary + neutral defaults
|
||||
themes/*.css one file per theme
|
||||
theme.js resolve, apply, remember
|
||||
bake.py inline the defaults into pages
|
||||
```
|
||||
|
||||
Served together at `/theme.css` — tokens first, then every theme, each scoped to
|
||||
`[data-theme="..."]`. Adding a theme is adding a file: `run.py` lists the
|
||||
directory rather than carrying a hardcoded list.
|
||||
|
||||
## Two naming families, one set of values
|
||||
|
||||
Both are answered, because both were already in use and renaming across a dozen
|
||||
templates would have been the larger change:
|
||||
|
||||
- `--bg` / `--surface` / `--border` / `--text` / `--muted` / `--accent` — the
|
||||
station tools and the docs site
|
||||
- `--surface-0..3` / `--text-primary` / `--panel-radius` — `common/ui`'s Vue
|
||||
components
|
||||
|
||||
The second family is derived from the first in `tokens.css`, so a page using
|
||||
either name gets the same colour and a theme author fills in one set.
|
||||
|
||||
## Order matters
|
||||
|
||||
```html
|
||||
<!-- baked defaults --> <style>:root { … }</style>
|
||||
<link rel="stylesheet" href="/theme.css">
|
||||
<style> …the page's own rules… </style>
|
||||
```
|
||||
|
||||
The theme has to load **before** the page's styles, so its element defaults
|
||||
underpin the page rather than override it. Get this backwards and
|
||||
`tokens.css`'s `body { background: var(--bg) }` flattens whatever the page
|
||||
wanted — which is exactly how artery, atlas and station briefly lost their
|
||||
coloured content columns.
|
||||
|
||||
## Baked defaults, and why
|
||||
|
||||
`/theme.css` is an absolute path, and soleprint is not always at the root — in a
|
||||
room's nginx it sits under `/spr/` while `location /` goes to the frontend. A
|
||||
page that says `background: var(--bg)` and never receives `--bg` does not fall
|
||||
back to something plainer: the declaration is invalid at computed-value time, so
|
||||
the background goes transparent and the text goes initial-black on a design that
|
||||
assumed dark. Unstyled, not merely unbranded.
|
||||
|
||||
So every page carries a generated `:root` block **before** the link. Both are
|
||||
`:root`, so document order decides: the served stylesheet wins when it loads,
|
||||
and the baked block is what is left when it does not.
|
||||
|
||||
```bash
|
||||
python3 common/theme/bake.py # regenerate
|
||||
python3 common/theme/bake.py --check # fail if a page is stale
|
||||
```
|
||||
|
||||
Only the variables a page actually uses are emitted, so the blocks stay small.
|
||||
|
||||
## No webfonts
|
||||
|
||||
The stacks name faces that exist on the target rather than fetching any:
|
||||
|
||||
```css
|
||||
--font-ui: "Segoe UI", Inter, system-ui, -apple-system, Arial, sans-serif;
|
||||
--font-mono: "Cascadia Mono", "JetBrains Mono", Consolas, "SF Mono", monospace;
|
||||
```
|
||||
|
||||
Segoe UI and Consolas ship with Windows. A regulated network blocks
|
||||
`fonts.googleapis.com` and `file://` stalls on it, and neither failure looks
|
||||
like a missing font — they look like a broken page.
|
||||
|
||||
## Diagrams follow
|
||||
|
||||
`docs/graphs/themes/*.gvpr` carry the same palettes for graphviz, so a diagram
|
||||
and the page around it are one visual language. See
|
||||
[the graphs README](https://git.mcrn.ar/mariano/soleprint/src/branch/main/docs/graphs/README.md)
|
||||
and [Export / Compile](#export).
|
||||
|
||||
```bash
|
||||
make docs graphs # every graph, every theme
|
||||
```
|
||||
|
||||
Diagrams are baked per theme rather than styled by CSS, because the docs embed
|
||||
them with `<img src=…>` — which makes the SVG a separate document the page's
|
||||
stylesheet cannot reach. A plexus that **inlines** the SVG can style it live,
|
||||
and the bundle plexus does exactly that.
|
||||
|
||||
## Contrast
|
||||
|
||||
`lucid` is the first light theme, and the palette was measured rather than
|
||||
guessed — against both `#ffffff` and the `#f5f7fa` panel, at the sizes actually
|
||||
used. `--dim` drives 11px notes and `--status-warn` drives 10px labels, so both
|
||||
need 4.5:1 rather than the 3:1 large text gets away with. The obvious lighter
|
||||
greys came in at 3.4–3.8 and were dropped for that reason.
|
||||
@@ -1,29 +1,188 @@
|
||||
[
|
||||
{"id": "intro", "title": {"en": "Introduction"}},
|
||||
{"id": "quickstart", "title": {"en": "Quick Start"}},
|
||||
{"id": "concepts", "title": {"en": "Concepts"}},
|
||||
{"id": "room-setup", "title": {"en": "↳ Room Setup"}, "sub": true},
|
||||
{"id": "standalone", "title": {"en": "↳ Standalone"}, "sub": true},
|
||||
{"id": "managed", "title": {"en": "↳ Managed"}, "sub": true},
|
||||
|
||||
{"id": "artery", "title": {"en": "Artery"}},
|
||||
{"id": "artery-jira", "title": {"en": "↳ Jira"}, "sub": true},
|
||||
{"id": "artery-google", "title": {"en": "↳ Google"}, "sub": true},
|
||||
{"id": "artery-slack", "title": {"en": "↳ Slack"}, "sub": true},
|
||||
{"id": "artery-ia", "title": {"en": "↳ IA"}, "sub": true},
|
||||
{"id": "artery-shunts", "title": {"en": "↳ Shunts"}, "sub": true},
|
||||
|
||||
{"id": "atlas", "title": {"en": "Atlas"}},
|
||||
{"id": "atlas-books", "title": {"en": "↳ Books"}, "sub": true},
|
||||
{"id": "atlas-templates", "title": {"en": "↳ Templates"}, "sub": true},
|
||||
|
||||
{"id": "station", "title": {"en": "Station"}},
|
||||
{"id": "station-tester", "title": {"en": "↳ Tester"}, "sub": true},
|
||||
{"id": "station-datagen", "title": {"en": "↳ Datagen"}, "sub": true},
|
||||
{"id": "station-modelgen", "title": {"en": "↳ Modelgen"}, "sub": true},
|
||||
{"id": "station-graphgen", "title": {"en": "↳ Graphgen"}, "sub": true},
|
||||
{"id": "station-databrowse", "title": {"en": "↳ Databrowse"}, "sub": true},
|
||||
|
||||
{"id": "components", "title": {"en": "Shared Components"}},
|
||||
{"id": "deployment", "title": {"en": "Deployment"}}
|
||||
{
|
||||
"id": "intro",
|
||||
"title": {
|
||||
"en": "Introduction"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "quickstart",
|
||||
"title": {
|
||||
"en": "Quick Start"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "concepts",
|
||||
"title": {
|
||||
"en": "Concepts"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "room-setup",
|
||||
"title": {
|
||||
"en": "↳ Room Setup"
|
||||
},
|
||||
"sub": true
|
||||
},
|
||||
{
|
||||
"id": "standalone",
|
||||
"title": {
|
||||
"en": "↳ Standalone"
|
||||
},
|
||||
"sub": true
|
||||
},
|
||||
{
|
||||
"id": "managed",
|
||||
"title": {
|
||||
"en": "↳ Managed"
|
||||
},
|
||||
"sub": true
|
||||
},
|
||||
{
|
||||
"id": "artery",
|
||||
"title": {
|
||||
"en": "Artery"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "artery-jira",
|
||||
"title": {
|
||||
"en": "↳ Jira"
|
||||
},
|
||||
"sub": true
|
||||
},
|
||||
{
|
||||
"id": "artery-google",
|
||||
"title": {
|
||||
"en": "↳ Google"
|
||||
},
|
||||
"sub": true
|
||||
},
|
||||
{
|
||||
"id": "artery-slack",
|
||||
"title": {
|
||||
"en": "↳ Slack"
|
||||
},
|
||||
"sub": true
|
||||
},
|
||||
{
|
||||
"id": "artery-ia",
|
||||
"title": {
|
||||
"en": "↳ IA"
|
||||
},
|
||||
"sub": true
|
||||
},
|
||||
{
|
||||
"id": "artery-shunts",
|
||||
"title": {
|
||||
"en": "↳ Shunts"
|
||||
},
|
||||
"sub": true
|
||||
},
|
||||
{
|
||||
"id": "artery-plexuses",
|
||||
"title": {
|
||||
"en": "↳ Plexuses"
|
||||
},
|
||||
"sub": true
|
||||
},
|
||||
{
|
||||
"id": "atlas",
|
||||
"title": {
|
||||
"en": "Atlas"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "atlas-books",
|
||||
"title": {
|
||||
"en": "↳ Books"
|
||||
},
|
||||
"sub": true
|
||||
},
|
||||
{
|
||||
"id": "atlas-templates",
|
||||
"title": {
|
||||
"en": "↳ Templates"
|
||||
},
|
||||
"sub": true
|
||||
},
|
||||
{
|
||||
"id": "station",
|
||||
"title": {
|
||||
"en": "Station"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "station-tester",
|
||||
"title": {
|
||||
"en": "↳ Tester"
|
||||
},
|
||||
"sub": true
|
||||
},
|
||||
{
|
||||
"id": "station-datagen",
|
||||
"title": {
|
||||
"en": "↳ Datagen"
|
||||
},
|
||||
"sub": true
|
||||
},
|
||||
{
|
||||
"id": "station-modelgen",
|
||||
"title": {
|
||||
"en": "↳ Modelgen"
|
||||
},
|
||||
"sub": true
|
||||
},
|
||||
{
|
||||
"id": "station-graphgen",
|
||||
"title": {
|
||||
"en": "↳ Graphgen"
|
||||
},
|
||||
"sub": true
|
||||
},
|
||||
{
|
||||
"id": "station-shuntgen",
|
||||
"title": {
|
||||
"en": "↳ Shuntgen"
|
||||
},
|
||||
"sub": true
|
||||
},
|
||||
{
|
||||
"id": "station-databrowse",
|
||||
"title": {
|
||||
"en": "↳ Databrowse"
|
||||
},
|
||||
"sub": true
|
||||
},
|
||||
{
|
||||
"id": "station-cabinets",
|
||||
"title": {
|
||||
"en": "↳ Cabinets"
|
||||
},
|
||||
"sub": true
|
||||
},
|
||||
{
|
||||
"id": "components",
|
||||
"title": {
|
||||
"en": "Shared Components"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "export",
|
||||
"title": {
|
||||
"en": "Export / Compile"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "themes",
|
||||
"title": {
|
||||
"en": "Themes"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "deployment",
|
||||
"title": {
|
||||
"en": "Deployment"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
67
docs/graphs/README.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Graphs
|
||||
|
||||
The `.dot` files carry **structure and meaning**. The palette lives in
|
||||
`themes/*.gvpr` and is applied at render time.
|
||||
|
||||
```bash
|
||||
./render.sh # every graph, every theme
|
||||
./render.sh lucid # one theme
|
||||
./render.sh dark system_overview
|
||||
```
|
||||
|
||||
Needs graphviz (`sudo apt install graphviz`). The committed SVGs work without
|
||||
it; this is only needed to re-render.
|
||||
|
||||
## Why themes, and why gvpr
|
||||
|
||||
The sources used to hardcode the dark palette inline, which meant one look and
|
||||
no way to get another. `gvpr` rewrites the *parsed* graph, so it overrides
|
||||
whatever a `.dot` set — one source renders in any theme without being edited.
|
||||
|
||||
Command-line `-G`/`-N`/`-E` flags would not do: those are defaults, and an
|
||||
attribute written in the file beats them.
|
||||
|
||||
Colour is **baked into each SVG** rather than driven by CSS, because both docs
|
||||
sites embed graphs with `<img src=...>`. That makes the SVG a separate document
|
||||
the page's stylesheet cannot reach.
|
||||
|
||||
## Output
|
||||
|
||||
| Theme | Writes | For |
|
||||
| --- | --- | --- |
|
||||
| `dark` | `<name>.svg` | the docs site — this is the default, and what `docs/data/en/*.md` links to |
|
||||
| `lucid` | `<name>.lucid.svg` | regulated documents, print, and sitting beside a real lucid.app export |
|
||||
|
||||
## Classes
|
||||
|
||||
Nodes, edges, clusters and the graph itself may carry a `class`. Anything
|
||||
untagged gets the theme's neutral treatment.
|
||||
|
||||
| Class | Means | dark | lucid |
|
||||
| --- | --- | --- | --- |
|
||||
| `accent` | the emphasised thing | amber outline | blue outline, pale blue fill |
|
||||
| `accent-text` | emphasised *label*, not a box | amber text | blue text |
|
||||
| `ok` | live, working | green text | green outline, pale green fill |
|
||||
| `artery` / `atlas` / `station` | belongs to that system | that system's colour | its pale equivalent |
|
||||
| `muted` | present but not the point | grey text | grey fill |
|
||||
|
||||
`class` survives into the SVG (`<g class="node accent">`), so an **inlined** SVG
|
||||
can also be styled by page CSS. That is not how the docs embed them, but it is
|
||||
there if a page wants it.
|
||||
|
||||
## What themes never touch
|
||||
|
||||
- **`shape`** — a cylinder is a datastore, not a decoration.
|
||||
- **`style=invis`** — layout scaffolding. Filling it would draw it.
|
||||
- **`style=dashed`** — a weaker relationship; the theme preserves it and adds to it.
|
||||
- **`label`, `rankdir`, `rank`, `fontsize`** — content and layout.
|
||||
|
||||
## Adding a theme
|
||||
|
||||
Drop a `themes/<name>.gvpr` in beside the others; `render.sh` picks it up with no
|
||||
edit. Match the palette to `soleprint/common/theme/themes/<name>.css` so a
|
||||
diagram and the page around it are the same visual language.
|
||||
|
||||
Name fonts that exist on the target. `lucid.gvpr` uses Arial deliberately: it is
|
||||
on every Windows box and fontconfig aliases it to Liberation Sans on Linux, so
|
||||
the SVG measures the same on both and text does not reflow out of its box.
|
||||
@@ -1,43 +1,37 @@
|
||||
digraph artery_hierarchy {
|
||||
bgcolor="#0a0a0a"
|
||||
rankdir=LR
|
||||
fontname="Helvetica"
|
||||
node [fontname="Helvetica" fontsize=11 style=filled color="#333" fontcolor="#e5e5e5" shape=box]
|
||||
edge [fontname="Helvetica" fontsize=9 fontcolor="#a3a3a3" color="#b91c1c"]
|
||||
node [fontname="Helvetica" fontsize=11 style=filled shape=box]
|
||||
edge [class="artery" fontname="Helvetica" fontsize=9]
|
||||
|
||||
label="Artery — Component Hierarchy"
|
||||
labelloc=t
|
||||
fontsize=14
|
||||
fontcolor="#fca5a5"
|
||||
|
||||
vein [label="Vein\nstateless API connector" fillcolor="#1a1a1a"]
|
||||
pulse [label="Pulse\nVein + Room + Depot" fillcolor="#1a1a1a"]
|
||||
plexus [label="Plexus\nfull app: backend\n+ frontend + DB" fillcolor="#1a1a1a"]
|
||||
shunt [label="Shunt\nfake connector\nfor testing" fillcolor="#1a1a1a" color="#d4a574"]
|
||||
vein [label="Vein\nstateless API connector"]
|
||||
pulse [label="Pulse\nVein + Room + Depot"]
|
||||
plexus [label="Plexus\nfull app: backend\n+ frontend + DB"]
|
||||
shunt [class="accent" label="Shunt\nfake connector\nfor testing"]
|
||||
|
||||
vein -> pulse [label="compose"]
|
||||
pulse -> plexus [label="extend"]
|
||||
shunt -> vein [label="replaces" style=dashed color="#d4a574" fontcolor="#d4a574"]
|
||||
shunt -> vein [class="accent" label="replaces" style=dashed]
|
||||
|
||||
// Examples
|
||||
subgraph cluster_examples {
|
||||
label="Live Veins"
|
||||
style=dashed
|
||||
color="#333"
|
||||
fontcolor="#666"
|
||||
|
||||
jira [label="Jira" fillcolor="#1a1a1a" fontcolor="#15803d" fontsize=9]
|
||||
google [label="Google" fillcolor="#1a1a1a" fontcolor="#d4a574" fontsize=9]
|
||||
ia [label="IA" fillcolor="#1a1a1a" fontcolor="#15803d" fontsize=9]
|
||||
jira [class="ok" label="Jira" fontsize=9]
|
||||
google [class="accent-text" label="Google" fontsize=9]
|
||||
ia [class="ok" label="IA" fontsize=9]
|
||||
}
|
||||
|
||||
subgraph cluster_shunts {
|
||||
label="Shunts"
|
||||
style=dashed
|
||||
color="#333"
|
||||
fontcolor="#666"
|
||||
|
||||
mp [label="MercadoPago" fillcolor="#1a1a1a" fontcolor="#d4a574" fontsize=9]
|
||||
mp [class="accent-text" label="MercadoPago" fontsize=9]
|
||||
}
|
||||
|
||||
jira -> vein [style=invis]
|
||||
|
||||
101
docs/graphs/artery_hierarchy.lucid.svg
Normal file
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 14.1.2 (0)
|
||||
-->
|
||||
<!-- Title: artery_hierarchy Pages: 1 -->
|
||||
<svg width="776pt" height="292pt"
|
||||
viewBox="0.00 0.00 776.00 292.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 287.75)">
|
||||
<title>artery_hierarchy</title>
|
||||
<polygon fill="#ffffff" stroke="none" points="-4,4 -4,-287.75 772.25,-287.75 772.25,4 -4,4"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="384.12" y="-266.45" font-family="Arial" font-size="14.00" fill="#1f2933">Artery — Component Hierarchy</text>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_examples</title>
|
||||
<path fill="#f5f7fa" stroke="#cbd2d9" stroke-dasharray="5,2" d="M143.75,-68C143.75,-68 197.25,-68 197.25,-68 203.25,-68 209.25,-74 209.25,-80 209.25,-80 209.25,-240 209.25,-240 209.25,-246 203.25,-252 197.25,-252 197.25,-252 143.75,-252 143.75,-252 137.75,-252 131.75,-246 131.75,-240 131.75,-240 131.75,-80 131.75,-80 131.75,-74 137.75,-68 143.75,-68"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="170.5" y="-234.7" font-family="Arial" font-size="14.00" fill="#616e7c">Live Veins</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_shunts</title>
|
||||
<path fill="#f5f7fa" stroke="#cbd2d9" stroke-dasharray="5,2" d="M20,-8C20,-8 85.75,-8 85.75,-8 91.75,-8 97.75,-14 97.75,-20 97.75,-20 97.75,-72 97.75,-72 97.75,-78 91.75,-84 85.75,-84 85.75,-84 20,-84 20,-84 14,-84 8,-78 8,-72 8,-72 8,-20 8,-20 8,-14 14,-8 20,-8"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="52.88" y="-66.7" font-family="Arial" font-size="14.00" fill="#616e7c">Shunts</text>
|
||||
</g>
|
||||
<!-- jira -->
|
||||
<g id="node1" class="node ok">
|
||||
<title>jira</title>
|
||||
<path fill="#e6f5ec" stroke="#1a7f45" d="M185,-112C185,-112 155,-112 155,-112 149,-112 143,-106 143,-100 143,-100 143,-88 143,-88 143,-82 149,-76 155,-76 155,-76 185,-76 185,-76 191,-76 197,-82 197,-88 197,-88 197,-100 197,-100 197,-106 191,-112 185,-112"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="170" y="-90.7" font-family="Arial" font-size="9.00" fill="#1f2933">Jira</text>
|
||||
</g>
|
||||
<!-- vein -->
|
||||
<g id="node5" class="node">
|
||||
<title>vein</title>
|
||||
<path fill="#ffffff" stroke="#9aa5b1" d="M401,-82C401,-82 296.5,-82 296.5,-82 290.5,-82 284.5,-76 284.5,-70 284.5,-70 284.5,-58 284.5,-58 284.5,-52 290.5,-46 296.5,-46 296.5,-46 401,-46 401,-46 407,-46 413,-52 413,-58 413,-58 413,-70 413,-70 413,-76 407,-82 401,-82"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="348.75" y="-67.05" font-family="Arial" font-size="11.00" fill="#1f2933">Vein</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="348.75" y="-53.55" font-family="Arial" font-size="11.00" fill="#1f2933">stateless API connector</text>
|
||||
</g>
|
||||
<!-- jira->vein -->
|
||||
<!-- google -->
|
||||
<g id="node2" class="node accent-text">
|
||||
<title>google</title>
|
||||
<path fill="#ffffff" stroke="#9aa5b1" d="M185,-166C185,-166 155,-166 155,-166 149,-166 143,-160 143,-154 143,-154 143,-142 143,-142 143,-136 149,-130 155,-130 155,-130 185,-130 185,-130 191,-130 197,-136 197,-142 197,-142 197,-154 197,-154 197,-160 191,-166 185,-166"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="170" y="-144.7" font-family="Arial" font-size="9.00" fill="#3a7dff">Google</text>
|
||||
</g>
|
||||
<!-- ia -->
|
||||
<g id="node3" class="node ok">
|
||||
<title>ia</title>
|
||||
<path fill="#e6f5ec" stroke="#1a7f45" d="M185,-220C185,-220 155,-220 155,-220 149,-220 143,-214 143,-208 143,-208 143,-196 143,-196 143,-190 149,-184 155,-184 155,-184 185,-184 185,-184 191,-184 197,-190 197,-196 197,-196 197,-208 197,-208 197,-214 191,-220 185,-220"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="170" y="-198.7" font-family="Arial" font-size="9.00" fill="#1f2933">IA</text>
|
||||
</g>
|
||||
<!-- mp -->
|
||||
<g id="node4" class="node accent-text">
|
||||
<title>mp</title>
|
||||
<path fill="#ffffff" stroke="#9aa5b1" d="M77.75,-52C77.75,-52 28,-52 28,-52 22,-52 16,-46 16,-40 16,-40 16,-28 16,-28 16,-22 22,-16 28,-16 28,-16 77.75,-16 77.75,-16 83.75,-16 89.75,-22 89.75,-28 89.75,-28 89.75,-40 89.75,-40 89.75,-46 83.75,-52 77.75,-52"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="52.88" y="-30.7" font-family="Arial" font-size="9.00" fill="#3a7dff">MercadoPago</text>
|
||||
</g>
|
||||
<!-- shunt -->
|
||||
<g id="node8" class="node accent">
|
||||
<title>shunt</title>
|
||||
<path fill="#d6e4ff" stroke="#3a7dff" d="M201.25,-58.25C201.25,-58.25 138.75,-58.25 138.75,-58.25 132.75,-58.25 126.75,-52.25 126.75,-46.25 126.75,-46.25 126.75,-21.75 126.75,-21.75 126.75,-15.75 132.75,-9.75 138.75,-9.75 138.75,-9.75 201.25,-9.75 201.25,-9.75 207.25,-9.75 213.25,-15.75 213.25,-21.75 213.25,-21.75 213.25,-46.25 213.25,-46.25 213.25,-52.25 207.25,-58.25 201.25,-58.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="170" y="-43.8" font-family="Arial" font-size="11.00" fill="#1f2933">Shunt</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="170" y="-30.3" font-family="Arial" font-size="11.00" fill="#1f2933">fake connector</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="170" y="-16.8" font-family="Arial" font-size="11.00" fill="#1f2933">for testing</text>
|
||||
</g>
|
||||
<!-- mp->shunt -->
|
||||
<!-- pulse -->
|
||||
<g id="node6" class="node">
|
||||
<title>pulse</title>
|
||||
<path fill="#ffffff" stroke="#9aa5b1" d="M595.5,-82C595.5,-82 498.5,-82 498.5,-82 492.5,-82 486.5,-76 486.5,-70 486.5,-70 486.5,-58 486.5,-58 486.5,-52 492.5,-46 498.5,-46 498.5,-46 595.5,-46 595.5,-46 601.5,-46 607.5,-52 607.5,-58 607.5,-58 607.5,-70 607.5,-70 607.5,-76 601.5,-82 595.5,-82"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="547" y="-67.05" font-family="Arial" font-size="11.00" fill="#1f2933">Pulse</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="547" y="-53.55" font-family="Arial" font-size="11.00" fill="#1f2933">Vein + Room + Depot</text>
|
||||
</g>
|
||||
<!-- vein->pulse -->
|
||||
<g id="edge1" class="edge artery">
|
||||
<title>vein->pulse</title>
|
||||
<path fill="none" stroke="#c0392b" d="M413.47,-64C434.03,-64 456.89,-64 477.76,-64"/>
|
||||
<polygon fill="#c0392b" stroke="#c0392b" points="477.75,-66.45 484.75,-64 477.75,-61.55 477.75,-66.45"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="449.75" y="-65.95" font-family="Arial" font-size="9.00" fill="#c0392b">compose</text>
|
||||
</g>
|
||||
<!-- plexus -->
|
||||
<g id="node7" class="node">
|
||||
<title>plexus</title>
|
||||
<path fill="#ffffff" stroke="#9aa5b1" d="M756.25,-88.25C756.25,-88.25 683.25,-88.25 683.25,-88.25 677.25,-88.25 671.25,-82.25 671.25,-76.25 671.25,-76.25 671.25,-51.75 671.25,-51.75 671.25,-45.75 677.25,-39.75 683.25,-39.75 683.25,-39.75 756.25,-39.75 756.25,-39.75 762.25,-39.75 768.25,-45.75 768.25,-51.75 768.25,-51.75 768.25,-76.25 768.25,-76.25 768.25,-82.25 762.25,-88.25 756.25,-88.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="719.75" y="-73.8" font-family="Arial" font-size="11.00" fill="#1f2933">Plexus</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="719.75" y="-60.3" font-family="Arial" font-size="11.00" fill="#1f2933">full app: backend</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="719.75" y="-46.8" font-family="Arial" font-size="11.00" fill="#1f2933">+ frontend + DB</text>
|
||||
</g>
|
||||
<!-- pulse->plexus -->
|
||||
<g id="edge2" class="edge artery">
|
||||
<title>pulse->plexus</title>
|
||||
<path fill="none" stroke="#c0392b" d="M607.72,-64C625.49,-64 644.89,-64 662.44,-64"/>
|
||||
<polygon fill="#c0392b" stroke="#c0392b" points="662.37,-66.45 669.37,-64 662.37,-61.55 662.37,-66.45"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="639.38" y="-65.95" font-family="Arial" font-size="9.00" fill="#c0392b">extend</text>
|
||||
</g>
|
||||
<!-- shunt->vein -->
|
||||
<g id="edge3" class="edge accent">
|
||||
<title>shunt->vein</title>
|
||||
<path fill="none" stroke="#3a7dff" stroke-dasharray="5,2" d="M213.34,-41.19C232.09,-44.37 254.68,-48.2 275.94,-51.81"/>
|
||||
<polygon fill="#3a7dff" stroke="#3a7dff" points="275.53,-54.23 282.84,-52.98 276.35,-49.4 275.53,-54.23"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="248.88" y="-51.85" font-family="Arial" font-size="9.00" fill="#3a7dff">replaces</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.9 KiB |
@@ -4,98 +4,98 @@
|
||||
<!-- Generated by graphviz version 14.1.2 (0)
|
||||
-->
|
||||
<!-- Title: artery_hierarchy Pages: 1 -->
|
||||
<svg width="845pt" height="317pt"
|
||||
viewBox="0.00 0.00 845.00 317.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 313.25)">
|
||||
<svg width="845pt" height="294pt"
|
||||
viewBox="0.00 0.00 845.00 294.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 290.25)">
|
||||
<title>artery_hierarchy</title>
|
||||
<polygon fill="#0a0a0a" stroke="none" points="-4,4 -4,-313.25 840.5,-313.25 840.5,4 -4,4"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="418.25" y="-291.95" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#fca5a5">Artery — Component Hierarchy</text>
|
||||
<polygon fill="#0a0a0a" stroke="none" points="-4,4 -4,-290.25 840.5,-290.25 840.5,4 -4,4"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="418.25" y="-268.95" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d4a574">Artery — Component Hierarchy</text>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_examples</title>
|
||||
<polygon fill="#0a0a0a" stroke="#333333" stroke-dasharray="5,2" points="135.88,-8 135.88,-193 220.88,-193 220.88,-8 135.88,-8"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="178.38" y="-175.7" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#666666">Live Veins</text>
|
||||
<polygon fill="#0a0a0a" stroke="#333333" stroke-dasharray="5,2" points="135.88,-68 135.88,-253 220.88,-253 220.88,-68 135.88,-68"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="178.38" y="-235.7" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#666666">Live Veins</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_shunts</title>
|
||||
<polygon fill="#0a0a0a" stroke="#333333" stroke-dasharray="5,2" points="8,-199 8,-276 100,-276 100,-199 8,-199"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="54" y="-258.7" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#666666">Shunts</text>
|
||||
<polygon fill="#0a0a0a" stroke="#333333" stroke-dasharray="5,2" points="8,-8 8,-85 100,-85 100,-8 8,-8"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="54" y="-67.7" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#666666">Shunts</text>
|
||||
</g>
|
||||
<!-- jira -->
|
||||
<g id="node1" class="node ok">
|
||||
<title>jira</title>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="204.88,-112 150.88,-112 150.88,-76 204.88,-76 204.88,-112"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="177.88" y="-91.08" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#86efac">Jira</text>
|
||||
</g>
|
||||
<!-- vein -->
|
||||
<g id="node1" class="node">
|
||||
<g id="node5" class="node">
|
||||
<title>vein</title>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="446,-167 300.25,-167 300.25,-131 446,-131 446,-167"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="373.12" y="-152.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">Vein</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="373.12" y="-138.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">stateless API connector</text>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="446,-82 300.25,-82 300.25,-46 446,-46 446,-82"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="373.12" y="-67.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">Vein</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="373.12" y="-53.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">stateless API connector</text>
|
||||
</g>
|
||||
<!-- jira->vein -->
|
||||
<!-- google -->
|
||||
<g id="node2" class="node accent-text">
|
||||
<title>google</title>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="204.88,-166 150.88,-166 150.88,-130 204.88,-130 204.88,-166"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="177.88" y="-145.07" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#d4a574">Google</text>
|
||||
</g>
|
||||
<!-- ia -->
|
||||
<g id="node3" class="node ok">
|
||||
<title>ia</title>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="204.88,-220 150.88,-220 150.88,-184 204.88,-184 204.88,-220"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="177.88" y="-199.07" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#86efac">IA</text>
|
||||
</g>
|
||||
<!-- mp -->
|
||||
<g id="node4" class="node accent-text">
|
||||
<title>mp</title>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="92,-52 16,-52 16,-16 92,-16 92,-52"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="54" y="-31.07" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#d4a574">MercadoPago</text>
|
||||
</g>
|
||||
<!-- shunt -->
|
||||
<g id="node8" class="node accent">
|
||||
<title>shunt</title>
|
||||
<polygon fill="#1a1a1a" stroke="#d4a574" points="226.75,-58.25 129,-58.25 129,-9.75 226.75,-9.75 226.75,-58.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="177.88" y="-43.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">Shunt</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="177.88" y="-30.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">fake connector</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="177.88" y="-16.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">for testing</text>
|
||||
</g>
|
||||
<!-- mp->shunt -->
|
||||
<!-- pulse -->
|
||||
<g id="node2" class="node">
|
||||
<g id="node6" class="node">
|
||||
<title>pulse</title>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="659.25,-167 522.5,-167 522.5,-131 659.25,-131 659.25,-167"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="590.88" y="-152.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">Pulse</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="590.88" y="-138.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">Vein + Room + Depot</text>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="659.25,-82 522.5,-82 522.5,-46 659.25,-46 659.25,-82"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="590.88" y="-67.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">Pulse</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="590.88" y="-53.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">Vein + Room + Depot</text>
|
||||
</g>
|
||||
<!-- vein->pulse -->
|
||||
<g id="edge1" class="edge">
|
||||
<g id="edge1" class="edge artery">
|
||||
<title>vein->pulse</title>
|
||||
<path fill="none" stroke="#b91c1c" d="M446.27,-149C467.03,-149 489.79,-149 510.95,-149"/>
|
||||
<polygon fill="#b91c1c" stroke="#b91c1c" points="510.67,-152.5 520.67,-149 510.67,-145.5 510.67,-152.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="484.25" y="-151.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#a3a3a3">compose</text>
|
||||
<path fill="none" stroke="#b91c1c" d="M446.27,-64C467.03,-64 489.79,-64 510.95,-64"/>
|
||||
<polygon fill="#b91c1c" stroke="#b91c1c" points="510.67,-67.5 520.67,-64 510.67,-60.5 510.67,-67.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="484.25" y="-66.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#fca5a5">compose</text>
|
||||
</g>
|
||||
<!-- plexus -->
|
||||
<g id="node3" class="node">
|
||||
<g id="node7" class="node">
|
||||
<title>plexus</title>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="836.5,-173.25 726.75,-173.25 726.75,-124.75 836.5,-124.75 836.5,-173.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="781.62" y="-158.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">Plexus</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="781.62" y="-145.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">full app: backend</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="781.62" y="-131.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">+ frontend + DB</text>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="836.5,-88.25 726.75,-88.25 726.75,-39.75 836.5,-39.75 836.5,-88.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="781.62" y="-73.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">Plexus</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="781.62" y="-60.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">full app: backend</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="781.62" y="-46.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">+ frontend + DB</text>
|
||||
</g>
|
||||
<!-- pulse->plexus -->
|
||||
<g id="edge2" class="edge">
|
||||
<g id="edge2" class="edge artery">
|
||||
<title>pulse->plexus</title>
|
||||
<path fill="none" stroke="#b91c1c" d="M659.48,-149C677.62,-149 697.19,-149 715.21,-149"/>
|
||||
<polygon fill="#b91c1c" stroke="#b91c1c" points="714.98,-152.5 724.98,-149 714.98,-145.5 714.98,-152.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="693" y="-151.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#a3a3a3">extend</text>
|
||||
</g>
|
||||
<!-- shunt -->
|
||||
<g id="node4" class="node">
|
||||
<title>shunt</title>
|
||||
<polygon fill="#1a1a1a" stroke="#d4a574" points="226.75,-249.25 129,-249.25 129,-200.75 226.75,-200.75 226.75,-249.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="177.88" y="-234.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">Shunt</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="177.88" y="-221.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">fake connector</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="177.88" y="-207.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">for testing</text>
|
||||
<path fill="none" stroke="#b91c1c" d="M659.48,-64C677.62,-64 697.19,-64 715.21,-64"/>
|
||||
<polygon fill="#b91c1c" stroke="#b91c1c" points="714.98,-67.5 724.98,-64 714.98,-60.5 714.98,-67.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="693" y="-66.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#fca5a5">extend</text>
|
||||
</g>
|
||||
<!-- shunt->vein -->
|
||||
<g id="edge3" class="edge">
|
||||
<g id="edge3" class="edge accent">
|
||||
<title>shunt->vein</title>
|
||||
<path fill="none" stroke="#d4a574" stroke-dasharray="5,2" d="M227.14,-206.02C253.71,-195.57 286.85,-182.54 314.73,-171.57"/>
|
||||
<polygon fill="#d4a574" stroke="#d4a574" points="315.71,-174.95 323.73,-168.03 313.14,-168.43 315.71,-174.95"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="263.5" y="-200.95" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#d4a574">replaces</text>
|
||||
</g>
|
||||
<!-- jira -->
|
||||
<g id="node5" class="node">
|
||||
<title>jira</title>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="204.88,-52 150.88,-52 150.88,-16 204.88,-16 204.88,-52"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="177.88" y="-31.07" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#15803d">Jira</text>
|
||||
</g>
|
||||
<!-- jira->vein -->
|
||||
<!-- google -->
|
||||
<g id="node6" class="node">
|
||||
<title>google</title>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="204.88,-106 150.88,-106 150.88,-70 204.88,-70 204.88,-106"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="177.88" y="-85.08" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#d4a574">Google</text>
|
||||
</g>
|
||||
<!-- ia -->
|
||||
<g id="node7" class="node">
|
||||
<title>ia</title>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="204.88,-160 150.88,-160 150.88,-124 204.88,-124 204.88,-160"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="177.88" y="-139.07" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#15803d">IA</text>
|
||||
<path fill="none" stroke="#d4a574" stroke-dasharray="5,2" d="M227.14,-41.49C245.81,-44.39 267.72,-47.79 288.78,-51.06"/>
|
||||
<polygon fill="#d4a574" stroke="#d4a574" points="288.11,-54.5 298.53,-52.58 289.18,-47.58 288.11,-54.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="263.5" y="-52.6" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#d4a574">replaces</text>
|
||||
</g>
|
||||
<!-- mp -->
|
||||
<g id="node8" class="node">
|
||||
<title>mp</title>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="92,-243 16,-243 16,-207 92,-207 92,-243"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="54" y="-222.07" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#d4a574">MercadoPago</text>
|
||||
</g>
|
||||
<!-- mp->shunt -->
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
@@ -1,43 +1,37 @@
|
||||
digraph cfg_gen_flow {
|
||||
bgcolor="#0a0a0a"
|
||||
rankdir=LR
|
||||
fontname="Helvetica"
|
||||
node [fontname="Helvetica" fontsize=11 style=filled color="#333" fontcolor="#e5e5e5" shape=box]
|
||||
edge [fontname="Helvetica" fontsize=9 fontcolor="#a3a3a3" color="#d4a574"]
|
||||
node [fontname="Helvetica" fontsize=11 style=filled shape=box]
|
||||
edge [class="accent" fontname="Helvetica" fontsize=9]
|
||||
|
||||
label="Build Flow — cfg/ to gen/"
|
||||
labelloc=t
|
||||
fontsize=14
|
||||
fontcolor="#d4a574"
|
||||
|
||||
// Source
|
||||
subgraph cluster_source {
|
||||
label="Source (committed)"
|
||||
style=dashed
|
||||
color="#333"
|
||||
fontcolor="#666"
|
||||
|
||||
core [label="soleprint/\ncore framework" fillcolor="#1a1a1a"]
|
||||
cfg [label="cfg/<room>/\nroom config" fillcolor="#1a1a1a"]
|
||||
core [label="soleprint/\ncore framework"]
|
||||
cfg [label="cfg/<room>/\nroom config"]
|
||||
}
|
||||
|
||||
// Build
|
||||
build [label="build.py\n--cfg <room>" fillcolor="#1a1a1a" color="#d4a574" shape=component]
|
||||
build [class="accent" label="build.py\n--cfg <room>" shape=component]
|
||||
|
||||
// Output
|
||||
subgraph cluster_output {
|
||||
label="Output (generated, gitignored)"
|
||||
style=dashed
|
||||
color="#333"
|
||||
fontcolor="#666"
|
||||
|
||||
gen_spr [label="gen/<room>/soleprint/\ncore + room merged" fillcolor="#1a1a1a"]
|
||||
gen_app [label="gen/<room>/<app>/\ncloned repos" fillcolor="#1a1a1a"]
|
||||
gen_link [label="gen/<room>/link/\nDB bridge" fillcolor="#1a1a1a"]
|
||||
gen_spr [label="gen/<room>/soleprint/\ncore + room merged"]
|
||||
gen_app [label="gen/<room>/<app>/\ncloned repos"]
|
||||
gen_link [label="gen/<room>/link/\nDB bridge"]
|
||||
}
|
||||
|
||||
// Run
|
||||
docker [label="docker compose up" fillcolor="#1a1a1a" shape=component]
|
||||
docker [label="docker compose up" shape=component]
|
||||
|
||||
// Flow
|
||||
core -> build
|
||||
|
||||
114
docs/graphs/cfg_gen_flow.lucid.svg
Normal file
@@ -0,0 +1,114 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 14.1.2 (0)
|
||||
-->
|
||||
<!-- Title: cfg_gen_flow Pages: 1 -->
|
||||
<svg width="661pt" height="232pt"
|
||||
viewBox="0.00 0.00 661.00 232.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 227.75)">
|
||||
<title>cfg_gen_flow</title>
|
||||
<polygon fill="#ffffff" stroke="none" points="-4,4 -4,-227.75 657.25,-227.75 657.25,4 -4,4"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="326.62" y="-206.45" font-family="Arial" font-size="14.00" fill="#1f2933">Build Flow — cfg/ to gen/</text>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_source</title>
|
||||
<path fill="#f5f7fa" stroke="#cbd2d9" stroke-dasharray="5,2" d="M12,-62C12,-62 123.25,-62 123.25,-62 129.25,-62 135.25,-68 135.25,-74 135.25,-74 135.25,-180 135.25,-180 135.25,-186 129.25,-192 123.25,-192 123.25,-192 12,-192 12,-192 6,-192 0,-186 0,-180 0,-180 0,-74 0,-74 0,-68 6,-62 12,-62"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="67.62" y="-174.7" font-family="Arial" font-size="14.00" fill="#616e7c">Source (committed)</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_output</title>
|
||||
<path fill="#f5f7fa" stroke="#cbd2d9" stroke-dasharray="5,2" d="M329,-8C329,-8 502.5,-8 502.5,-8 508.5,-8 514.5,-14 514.5,-20 514.5,-20 514.5,-180 514.5,-180 514.5,-186 508.5,-192 502.5,-192 502.5,-192 329,-192 329,-192 323,-192 317,-186 317,-180 317,-180 317,-20 317,-20 317,-14 323,-8 329,-8"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="415.75" y="-174.7" font-family="Arial" font-size="14.00" fill="#616e7c">Output (generated, gitignored)</text>
|
||||
</g>
|
||||
<!-- core -->
|
||||
<g id="node1" class="node">
|
||||
<title>core</title>
|
||||
<path fill="#ffffff" stroke="#9aa5b1" d="M100.62,-160C100.62,-160 33.62,-160 33.62,-160 27.62,-160 21.62,-154 21.62,-148 21.62,-148 21.62,-136 21.62,-136 21.62,-130 27.62,-124 33.62,-124 33.62,-124 100.62,-124 100.62,-124 106.62,-124 112.62,-130 112.62,-136 112.62,-136 112.62,-148 112.62,-148 112.62,-154 106.62,-160 100.62,-160"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="67.12" y="-145.05" font-family="Arial" font-size="11.00" fill="#1f2933">soleprint/</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="67.12" y="-131.55" font-family="Arial" font-size="11.00" fill="#1f2933">core framework</text>
|
||||
</g>
|
||||
<!-- build -->
|
||||
<g id="node6" class="node accent">
|
||||
<title>build</title>
|
||||
<polygon fill="#d6e4ff" stroke="#3a7dff" points="243.25,-106 164.25,-106 164.25,-102 160.25,-102 160.25,-98 164.25,-98 164.25,-78 160.25,-78 160.25,-74 164.25,-74 164.25,-70 243.25,-70 243.25,-106"/>
|
||||
<polyline fill="none" stroke="#3a7dff" points="164.25,-102 168.25,-102 168.25,-98 164.25,-98"/>
|
||||
<polyline fill="none" stroke="#3a7dff" points="164.25,-78 168.25,-78 168.25,-74 164.25,-74"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="203.75" y="-91.05" font-family="Arial" font-size="11.00" fill="#1f2933">build.py</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="203.75" y="-77.55" font-family="Arial" font-size="11.00" fill="#1f2933">--cfg <room></text>
|
||||
</g>
|
||||
<!-- core->build -->
|
||||
<g id="edge1" class="edge accent">
|
||||
<title>core->build</title>
|
||||
<path fill="none" stroke="#3a7dff" d="M112.95,-124.02C126.89,-118.43 142.29,-112.25 156.33,-106.62"/>
|
||||
<polygon fill="#3a7dff" stroke="#3a7dff" points="156.99,-108.99 162.58,-104.11 155.17,-104.45 156.99,-108.99"/>
|
||||
</g>
|
||||
<!-- cfg -->
|
||||
<g id="node2" class="node">
|
||||
<title>cfg</title>
|
||||
<path fill="#ffffff" stroke="#9aa5b1" d="M92.38,-106C92.38,-106 41.88,-106 41.88,-106 35.88,-106 29.88,-100 29.88,-94 29.88,-94 29.88,-82 29.88,-82 29.88,-76 35.88,-70 41.88,-70 41.88,-70 92.38,-70 92.38,-70 98.38,-70 104.38,-76 104.38,-82 104.38,-82 104.38,-94 104.38,-94 104.38,-100 98.38,-106 92.38,-106"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="67.12" y="-91.05" font-family="Arial" font-size="11.00" fill="#1f2933">cfg/<room>/</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="67.12" y="-77.55" font-family="Arial" font-size="11.00" fill="#1f2933">room config</text>
|
||||
</g>
|
||||
<!-- cfg->build -->
|
||||
<g id="edge2" class="edge accent">
|
||||
<title>cfg->build</title>
|
||||
<path fill="none" stroke="#3a7dff" d="M104.56,-88C120.35,-88 139.04,-88 155.85,-88"/>
|
||||
<polygon fill="#3a7dff" stroke="#3a7dff" points="155.42,-90.45 162.42,-88 155.42,-85.55 155.42,-90.45"/>
|
||||
</g>
|
||||
<!-- gen_spr -->
|
||||
<g id="node3" class="node">
|
||||
<title>gen_spr</title>
|
||||
<path fill="#ffffff" stroke="#9aa5b1" d="M464.12,-160C464.12,-160 366.38,-160 366.38,-160 360.38,-160 354.38,-154 354.38,-148 354.38,-148 354.38,-136 354.38,-136 354.38,-130 360.38,-124 366.38,-124 366.38,-124 464.12,-124 464.12,-124 470.12,-124 476.12,-130 476.12,-136 476.12,-136 476.12,-148 476.12,-148 476.12,-154 470.12,-160 464.12,-160"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="415.25" y="-145.05" font-family="Arial" font-size="11.00" fill="#1f2933">gen/<room>/soleprint/</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="415.25" y="-131.55" font-family="Arial" font-size="11.00" fill="#1f2933">core + room merged</text>
|
||||
</g>
|
||||
<!-- docker -->
|
||||
<g id="node7" class="node">
|
||||
<title>docker</title>
|
||||
<polygon fill="#ffffff" stroke="#9aa5b1" points="653.25,-160 543.5,-160 543.5,-156 539.5,-156 539.5,-152 543.5,-152 543.5,-132 539.5,-132 539.5,-128 543.5,-128 543.5,-124 653.25,-124 653.25,-160"/>
|
||||
<polyline fill="none" stroke="#9aa5b1" points="543.5,-156 547.5,-156 547.5,-152 543.5,-152"/>
|
||||
<polyline fill="none" stroke="#9aa5b1" points="543.5,-132 547.5,-132 547.5,-128 543.5,-128"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="598.38" y="-138.3" font-family="Arial" font-size="11.00" fill="#1f2933">docker compose up</text>
|
||||
</g>
|
||||
<!-- gen_spr->docker -->
|
||||
<g id="edge6" class="edge accent">
|
||||
<title>gen_spr->docker</title>
|
||||
<path fill="none" stroke="#3a7dff" d="M476.56,-142C495.21,-142 515.8,-142 534.61,-142"/>
|
||||
<polygon fill="#3a7dff" stroke="#3a7dff" points="534.6,-144.45 541.6,-142 534.6,-139.55 534.6,-144.45"/>
|
||||
</g>
|
||||
<!-- gen_app -->
|
||||
<g id="node4" class="node">
|
||||
<title>gen_app</title>
|
||||
<path fill="#ffffff" stroke="#9aa5b1" d="M459.62,-106C459.62,-106 370.88,-106 370.88,-106 364.88,-106 358.88,-100 358.88,-94 358.88,-94 358.88,-82 358.88,-82 358.88,-76 364.88,-70 370.88,-70 370.88,-70 459.62,-70 459.62,-70 465.62,-70 471.62,-76 471.62,-82 471.62,-82 471.62,-94 471.62,-94 471.62,-100 465.62,-106 459.62,-106"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="415.25" y="-91.05" font-family="Arial" font-size="11.00" fill="#1f2933">gen/<room>/<app>/</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="415.25" y="-77.55" font-family="Arial" font-size="11.00" fill="#1f2933">cloned repos</text>
|
||||
</g>
|
||||
<!-- gen_link -->
|
||||
<g id="node5" class="node">
|
||||
<title>gen_link</title>
|
||||
<path fill="#ffffff" stroke="#9aa5b1" d="M451.75,-52C451.75,-52 378.75,-52 378.75,-52 372.75,-52 366.75,-46 366.75,-40 366.75,-40 366.75,-28 366.75,-28 366.75,-22 372.75,-16 378.75,-16 378.75,-16 451.75,-16 451.75,-16 457.75,-16 463.75,-22 463.75,-28 463.75,-28 463.75,-40 463.75,-40 463.75,-46 457.75,-52 451.75,-52"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="415.25" y="-37.05" font-family="Arial" font-size="11.00" fill="#1f2933">gen/<room>/link/</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="415.25" y="-23.55" font-family="Arial" font-size="11.00" fill="#1f2933">DB bridge</text>
|
||||
</g>
|
||||
<!-- build->gen_spr -->
|
||||
<g id="edge3" class="edge accent">
|
||||
<title>build->gen_spr</title>
|
||||
<path fill="none" stroke="#3a7dff" d="M243.64,-98.38C249.54,-99.94 255.55,-101.52 261.25,-103 288.9,-110.19 319.39,-117.99 345.83,-124.71"/>
|
||||
<polygon fill="#3a7dff" stroke="#3a7dff" points="345.05,-127.04 352.43,-126.39 346.25,-122.29 345.05,-127.04"/>
|
||||
</g>
|
||||
<!-- build->gen_app -->
|
||||
<g id="edge4" class="edge accent">
|
||||
<title>build->gen_app</title>
|
||||
<path fill="none" stroke="#3a7dff" stroke-dasharray="5,2" d="M243.66,-88C273.57,-88 315.52,-88 350.33,-88"/>
|
||||
<polygon fill="#3a7dff" stroke="#3a7dff" points="350.04,-90.45 357.04,-88 350.04,-85.55 350.04,-90.45"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="284.12" y="-89.95" font-family="Arial" font-size="9.00" fill="#3a7dff">if managed</text>
|
||||
</g>
|
||||
<!-- build->gen_link -->
|
||||
<g id="edge5" class="edge accent">
|
||||
<title>build->gen_link</title>
|
||||
<path fill="none" stroke="#3a7dff" stroke-dasharray="5,2" d="M243.66,-77.97C275.9,-69.66 322.12,-57.75 358.32,-48.42"/>
|
||||
<polygon fill="#3a7dff" stroke="#3a7dff" points="358.72,-50.84 364.88,-46.72 357.49,-46.1 358.72,-50.84"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="284.12" y="-73.99" font-family="Arial" font-size="9.00" fill="#3a7dff">if managed</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.6 KiB |
@@ -28,7 +28,7 @@
|
||||
<text xml:space="preserve" text-anchor="middle" x="75.75" y="-131.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">core framework</text>
|
||||
</g>
|
||||
<!-- build -->
|
||||
<g id="node3" class="node">
|
||||
<g id="node6" class="node accent">
|
||||
<title>build</title>
|
||||
<polygon fill="#1a1a1a" stroke="#d4a574" points="271.75,-106 181.5,-106 181.5,-102 177.5,-102 177.5,-98 181.5,-98 181.5,-78 177.5,-78 177.5,-74 181.5,-74 181.5,-70 271.75,-70 271.75,-106"/>
|
||||
<polyline fill="none" stroke="#d4a574" points="181.5,-102 185.5,-102 185.5,-98 181.5,-98"/>
|
||||
@@ -37,7 +37,7 @@
|
||||
<text xml:space="preserve" text-anchor="middle" x="226.62" y="-77.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">--cfg <room></text>
|
||||
</g>
|
||||
<!-- core->build -->
|
||||
<g id="edge1" class="edge">
|
||||
<g id="edge1" class="edge accent">
|
||||
<title>core->build</title>
|
||||
<path fill="none" stroke="#d4a574" d="M127.3,-124.07C135.72,-121.06 144.36,-117.95 152.5,-115 158.3,-112.89 164.36,-110.68 170.4,-108.47"/>
|
||||
<polygon fill="#d4a574" stroke="#d4a574" points="171.59,-111.76 179.76,-105.02 169.17,-105.19 171.59,-111.76"/>
|
||||
@@ -50,65 +50,65 @@
|
||||
<text xml:space="preserve" text-anchor="middle" x="75.75" y="-77.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">room config</text>
|
||||
</g>
|
||||
<!-- cfg->build -->
|
||||
<g id="edge2" class="edge">
|
||||
<g id="edge2" class="edge accent">
|
||||
<title>cfg->build</title>
|
||||
<path fill="none" stroke="#d4a574" d="M119.41,-88C135.12,-88 153.15,-88 169.84,-88"/>
|
||||
<polygon fill="#d4a574" stroke="#d4a574" points="169.63,-91.5 179.63,-88 169.63,-84.5 169.63,-91.5"/>
|
||||
</g>
|
||||
<!-- gen_spr -->
|
||||
<g id="node4" class="node">
|
||||
<g id="node3" class="node">
|
||||
<title>gen_spr</title>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="536.5,-160 394.5,-160 394.5,-124 536.5,-124 536.5,-160"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="465.5" y="-145.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">gen/<room>/soleprint/</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="465.5" y="-131.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">core + room merged</text>
|
||||
</g>
|
||||
<!-- build->gen_spr -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>build->gen_spr</title>
|
||||
<path fill="none" stroke="#d4a574" d="M272.06,-98.83C278,-100.25 284.02,-101.67 289.75,-103 320.22,-110.08 353.74,-117.62 383.21,-124.16"/>
|
||||
<polygon fill="#d4a574" stroke="#d4a574" points="382.29,-127.55 392.81,-126.29 383.8,-120.71 382.29,-127.55"/>
|
||||
<!-- docker -->
|
||||
<g id="node7" class="node">
|
||||
<title>docker</title>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="733.5,-160 610.25,-160 610.25,-156 606.25,-156 606.25,-152 610.25,-152 610.25,-132 606.25,-132 606.25,-128 610.25,-128 610.25,-124 733.5,-124 733.5,-160"/>
|
||||
<polyline fill="none" stroke="#333333" points="610.25,-156 614.25,-156 614.25,-152 610.25,-152"/>
|
||||
<polyline fill="none" stroke="#333333" points="610.25,-132 614.25,-132 614.25,-128 610.25,-128"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="671.88" y="-138.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">docker compose up</text>
|
||||
</g>
|
||||
<!-- gen_spr->docker -->
|
||||
<g id="edge6" class="edge accent">
|
||||
<title>gen_spr->docker</title>
|
||||
<path fill="none" stroke="#d4a574" d="M536.83,-142C556.81,-142 578.59,-142 598.68,-142"/>
|
||||
<polygon fill="#d4a574" stroke="#d4a574" points="598.44,-145.5 608.44,-142 598.44,-138.5 598.44,-145.5"/>
|
||||
</g>
|
||||
<!-- gen_app -->
|
||||
<g id="node5" class="node">
|
||||
<g id="node4" class="node">
|
||||
<title>gen_app</title>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="531.62,-106 399.38,-106 399.38,-70 531.62,-70 531.62,-106"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="465.5" y="-91.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">gen/<room>/<app>/</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="465.5" y="-77.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">cloned repos</text>
|
||||
</g>
|
||||
<!-- build->gen_app -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>build->gen_app</title>
|
||||
<path fill="none" stroke="#d4a574" stroke-dasharray="5,2" d="M272.18,-88C304.75,-88 349.72,-88 388,-88"/>
|
||||
<polygon fill="#d4a574" stroke="#d4a574" points="387.6,-91.5 397.6,-88 387.6,-84.5 387.6,-91.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="315.25" y="-90.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#a3a3a3">if managed</text>
|
||||
</g>
|
||||
<!-- gen_link -->
|
||||
<g id="node6" class="node">
|
||||
<g id="node5" class="node">
|
||||
<title>gen_link</title>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="521.88,-52 409.12,-52 409.12,-16 521.88,-16 521.88,-52"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="465.5" y="-37.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">gen/<room>/link/</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="465.5" y="-23.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">DB bridge</text>
|
||||
</g>
|
||||
<!-- build->gen_spr -->
|
||||
<g id="edge3" class="edge accent">
|
||||
<title>build->gen_spr</title>
|
||||
<path fill="none" stroke="#d4a574" d="M272.06,-98.83C278,-100.25 284.02,-101.67 289.75,-103 320.22,-110.08 353.74,-117.62 383.21,-124.16"/>
|
||||
<polygon fill="#d4a574" stroke="#d4a574" points="382.29,-127.55 392.81,-126.29 383.8,-120.71 382.29,-127.55"/>
|
||||
</g>
|
||||
<!-- build->gen_app -->
|
||||
<g id="edge4" class="edge accent">
|
||||
<title>build->gen_app</title>
|
||||
<path fill="none" stroke="#d4a574" stroke-dasharray="5,2" d="M272.18,-88C304.75,-88 349.72,-88 388,-88"/>
|
||||
<polygon fill="#d4a574" stroke="#d4a574" points="387.6,-91.5 397.6,-88 387.6,-84.5 387.6,-91.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="315.25" y="-90.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#d4a574">if managed</text>
|
||||
</g>
|
||||
<!-- build->gen_link -->
|
||||
<g id="edge5" class="edge">
|
||||
<g id="edge5" class="edge accent">
|
||||
<title>build->gen_link</title>
|
||||
<path fill="none" stroke="#d4a574" stroke-dasharray="5,2" d="M272.05,-76.97C278,-75.53 284.02,-74.09 289.75,-72.75 325.35,-64.44 365.13,-55.58 397.73,-48.44"/>
|
||||
<polygon fill="#d4a574" stroke="#d4a574" points="398.44,-51.86 407.46,-46.31 396.95,-45.03 398.44,-51.86"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="315.25" y="-75.45" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#a3a3a3">if managed</text>
|
||||
</g>
|
||||
<!-- docker -->
|
||||
<g id="node7" class="node">
|
||||
<title>docker</title>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="733.5,-160 610.25,-160 610.25,-156 606.25,-156 606.25,-152 610.25,-152 610.25,-132 606.25,-132 606.25,-128 610.25,-128 610.25,-124 733.5,-124 733.5,-160"/>
|
||||
<polyline fill="none" stroke="#333333" points="610.25,-156 614.25,-156 614.25,-152 610.25,-152"/>
|
||||
<polyline fill="none" stroke="#333333" points="610.25,-132 614.25,-132 614.25,-128 610.25,-128"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="671.88" y="-138.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">docker compose up</text>
|
||||
</g>
|
||||
<!-- gen_spr->docker -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>gen_spr->docker</title>
|
||||
<path fill="none" stroke="#d4a574" d="M536.83,-142C556.81,-142 578.59,-142 598.68,-142"/>
|
||||
<polygon fill="#d4a574" stroke="#d4a574" points="598.44,-145.5 608.44,-142 598.44,-138.5 598.44,-145.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="315.25" y="-75.45" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#d4a574">if managed</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
69
docs/graphs/render.sh
Executable file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
# Render every .dot through every theme.
|
||||
#
|
||||
# ./render.sh # all graphs, all themes
|
||||
# ./render.sh lucid # one theme
|
||||
# ./render.sh dark system_overview
|
||||
#
|
||||
# The sources carry structure and meaning; themes/*.gvpr carry palette. gvpr
|
||||
# rewrites the parsed graph, so it wins over anything a .dot sets inline — which
|
||||
# is what lets one source render in several looks without being edited.
|
||||
#
|
||||
# Output naming: the default theme writes <name>.svg, because that is what
|
||||
# docs/data/en/*.md already links to and those links should keep working. Every
|
||||
# other theme writes <name>.<theme>.svg.
|
||||
#
|
||||
# Why baked rather than CSS: both docs sites embed graphs with <img src=...>,
|
||||
# which makes the SVG a separate document that the page's stylesheet cannot
|
||||
# reach. Colour has to be in the file.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
DEFAULT_THEME=dark
|
||||
|
||||
if ! command -v dot >/dev/null 2>&1 || ! command -v gvpr >/dev/null 2>&1; then
|
||||
echo "graphviz not found — install with: sudo apt install graphviz" >&2
|
||||
echo "(the committed .svg files already work; this is only needed to re-render)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
theme_arg="${1:-}"
|
||||
graph_arg="${2:-}"
|
||||
|
||||
themes=()
|
||||
if [ -n "$theme_arg" ]; then
|
||||
if [ ! -f "themes/${theme_arg}.gvpr" ]; then
|
||||
echo "no such theme: themes/${theme_arg}.gvpr" >&2
|
||||
echo "available: $(ls themes/*.gvpr 2>/dev/null | xargs -n1 basename | sed 's/\.gvpr$//' | tr '\n' ' ')" >&2
|
||||
exit 1
|
||||
fi
|
||||
themes=("$theme_arg")
|
||||
else
|
||||
for t in themes/*.gvpr; do themes+=("$(basename "$t" .gvpr)"); done
|
||||
fi
|
||||
|
||||
shopt -s nullglob
|
||||
sources=(*.dot)
|
||||
if [ -n "$graph_arg" ]; then
|
||||
sources=("${graph_arg%.dot}.dot")
|
||||
[ -f "${sources[0]}" ] || { echo "no such graph: ${sources[0]}" >&2; exit 1; }
|
||||
fi
|
||||
[ ${#sources[@]} -gt 0 ] || { echo "no .dot files here"; exit 0; }
|
||||
|
||||
for theme in "${themes[@]}"; do
|
||||
for src in "${sources[@]}"; do
|
||||
base="${src%.dot}"
|
||||
if [ "$theme" = "$DEFAULT_THEME" ]; then
|
||||
out="${base}.svg"
|
||||
else
|
||||
out="${base}.${theme}.svg"
|
||||
fi
|
||||
# One pipeline, so a gvpr failure fails the whole render rather than
|
||||
# silently writing a half-themed file.
|
||||
gvpr -c -f "themes/${theme}.gvpr" "$src" | dot -Tsvg -o "$out"
|
||||
printf " %-14s %s\n" "$theme" "$out"
|
||||
done
|
||||
done
|
||||
|
||||
echo
|
||||
echo "done — ${#sources[@]} graph(s) x ${#themes[@]} theme(s)"
|
||||
@@ -1,22 +1,20 @@
|
||||
digraph room_layers {
|
||||
bgcolor="#0a0a0a"
|
||||
rankdir=TB
|
||||
fontname="Helvetica"
|
||||
node [fontname="Helvetica" fontsize=10 style=filled color="#333" fontcolor="#e5e5e5" shape=record]
|
||||
edge [fontname="Helvetica" fontsize=9 fontcolor="#a3a3a3" color="#666"]
|
||||
node [fontname="Helvetica" fontsize=10 style=filled shape=record]
|
||||
edge [fontname="Helvetica" fontsize=9]
|
||||
|
||||
label="Room Layers — init wizard"
|
||||
labelloc=t
|
||||
fontsize=14
|
||||
fontcolor="#d4a574"
|
||||
|
||||
l0 [label="{Layer 0 | Config + Data | config.json · data/*.json}" fillcolor="#1a1a1a" color="#d4a574"]
|
||||
l1 [label="{Layer 1 | Docker | soleprint/docker-compose.yml · .env}" fillcolor="#1a1a1a"]
|
||||
l2 [label="{Layer 2 | Managed App | docker-compose.yml · Dockerfiles · .env}" fillcolor="#1a1a1a"]
|
||||
l3 [label="{Layer 3 | Link | link/main.py · adapters/ · Dockerfile}" fillcolor="#1a1a1a"]
|
||||
l4 [label="{Layer 4 | Scripts | ctrl/start.sh · stop.sh · status.sh · logs.sh}" fillcolor="#1a1a1a"]
|
||||
l5 [label="{Layer 5 | Systems | tester/environments.json · tests/}" fillcolor="#1a1a1a"]
|
||||
l6 [label="{Layer 6 | Nginx | nginx/local.conf · docker-compose.nginx.yml}" fillcolor="#1a1a1a"]
|
||||
l0 [class="accent" label="{Layer 0 | Config + Data | config.json · data/*.json}"]
|
||||
l1 [label="{Layer 1 | Docker | soleprint/docker-compose.yml · .env}"]
|
||||
l2 [label="{Layer 2 | Managed App | docker-compose.yml · Dockerfiles · .env}"]
|
||||
l3 [label="{Layer 3 | Link | link/main.py · adapters/ · Dockerfile}"]
|
||||
l4 [label="{Layer 4 | Scripts | ctrl/start.sh · stop.sh · status.sh · logs.sh}"]
|
||||
l5 [label="{Layer 5 | Systems | tester/environments.json · tests/}"]
|
||||
l6 [label="{Layer 6 | Nginx | nginx/local.conf · docker-compose.nginx.yml}"]
|
||||
|
||||
l0 -> l1 [label="required"]
|
||||
l1 -> l2 [label="if managed"]
|
||||
@@ -26,6 +24,6 @@ digraph room_layers {
|
||||
l5 -> l6 [label="if frontend"]
|
||||
|
||||
// Annotations
|
||||
note_req [label="every room" fillcolor="#0a0a0a" fontcolor="#d4a574" color="#0a0a0a" shape=plaintext fontsize=9]
|
||||
note_req [class="accent" label="every room" shape=plaintext fontsize=9]
|
||||
note_req -> l0 [style=invis]
|
||||
}
|
||||
|
||||
133
docs/graphs/room_layers.lucid.svg
Normal file
@@ -0,0 +1,133 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 14.1.2 (0)
|
||||
-->
|
||||
<!-- Title: room_layers Pages: 1 -->
|
||||
<svg width="423pt" height="596pt"
|
||||
viewBox="0.00 0.00 423.00 596.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 591.75)">
|
||||
<title>room_layers</title>
|
||||
<polygon fill="#ffffff" stroke="none" points="-4,4 -4,-591.75 418.62,-591.75 418.62,4 -4,4"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="207.31" y="-570.45" font-family="Arial" font-size="14.00" fill="#1f2933">Room Layers — init wizard</text>
|
||||
<!-- l0 -->
|
||||
<g id="node1" class="node accent">
|
||||
<title>l0</title>
|
||||
<polygon fill="#d6e4ff" stroke="#3a7dff" points="143.12,-430.5 143.12,-490.5 261.88,-490.5 261.88,-430.5 143.12,-430.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="202.5" y="-477" font-family="Arial" font-size="10.00" fill="#1f2933">Layer 0</text>
|
||||
<polyline fill="none" stroke="#3a7dff" points="143.12,-470.5 261.88,-470.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="202.5" y="-457" font-family="Arial" font-size="10.00" fill="#1f2933">Config + Data</text>
|
||||
<polyline fill="none" stroke="#3a7dff" points="143.12,-450.5 261.88,-450.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="202.5" y="-437" font-family="Arial" font-size="10.00" fill="#1f2933">config.json · data/*.json</text>
|
||||
</g>
|
||||
<!-- l1 -->
|
||||
<g id="node2" class="node">
|
||||
<title>l1</title>
|
||||
<polygon fill="#ffffff" stroke="#9aa5b1" points="115.38,-323 115.38,-383 289.62,-383 289.62,-323 115.38,-323"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="202.5" y="-369.5" font-family="Arial" font-size="10.00" fill="#1f2933">Layer 1</text>
|
||||
<polyline fill="none" stroke="#9aa5b1" points="115.38,-363 289.62,-363"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="202.5" y="-349.5" font-family="Arial" font-size="10.00" fill="#1f2933">Docker</text>
|
||||
<polyline fill="none" stroke="#9aa5b1" points="115.38,-343 289.62,-343"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="202.5" y="-329.5" font-family="Arial" font-size="10.00" fill="#1f2933">soleprint/docker-compose.yml · .env</text>
|
||||
</g>
|
||||
<!-- l0->l1 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>l0->l1</title>
|
||||
<path fill="none" stroke="#9aa5b1" d="M202.5,-430.11C202.5,-418.22 202.5,-404.35 202.5,-391.73"/>
|
||||
<polygon fill="#9aa5b1" stroke="#9aa5b1" points="204.95,-391.74 202.5,-384.74 200.05,-391.74 204.95,-391.74"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="219.75" y="-403.45" font-family="Arial" font-size="9.00" fill="#616e7c">required</text>
|
||||
</g>
|
||||
<!-- l2 -->
|
||||
<g id="node3" class="node">
|
||||
<title>l2</title>
|
||||
<polygon fill="#ffffff" stroke="#9aa5b1" points="0,-215.5 0,-275.5 193,-275.5 193,-215.5 0,-215.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="96.5" y="-262" font-family="Arial" font-size="10.00" fill="#1f2933">Layer 2</text>
|
||||
<polyline fill="none" stroke="#9aa5b1" points="0,-255.5 193,-255.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="96.5" y="-242" font-family="Arial" font-size="10.00" fill="#1f2933">Managed App</text>
|
||||
<polyline fill="none" stroke="#9aa5b1" points="0,-235.5 193,-235.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="96.5" y="-222" font-family="Arial" font-size="10.00" fill="#1f2933">docker-compose.yml · Dockerfiles · .env</text>
|
||||
</g>
|
||||
<!-- l1->l2 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>l1->l2</title>
|
||||
<path fill="none" stroke="#9aa5b1" d="M172.97,-322.61C160.22,-309.92 145.2,-294.97 131.86,-281.7"/>
|
||||
<polygon fill="#9aa5b1" stroke="#9aa5b1" points="133.63,-280 126.94,-276.8 130.17,-283.47 133.63,-280"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="175.55" y="-295.95" font-family="Arial" font-size="9.00" fill="#616e7c">if managed</text>
|
||||
</g>
|
||||
<!-- l4 -->
|
||||
<g id="node4" class="node">
|
||||
<title>l4</title>
|
||||
<polygon fill="#ffffff" stroke="#9aa5b1" points="211.12,-215.5 211.12,-275.5 407.88,-275.5 407.88,-215.5 211.12,-215.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="309.5" y="-262" font-family="Arial" font-size="10.00" fill="#1f2933">Layer 4</text>
|
||||
<polyline fill="none" stroke="#9aa5b1" points="211.12,-255.5 407.88,-255.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="309.5" y="-242" font-family="Arial" font-size="10.00" fill="#1f2933">Scripts</text>
|
||||
<polyline fill="none" stroke="#9aa5b1" points="211.12,-235.5 407.88,-235.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="309.5" y="-222" font-family="Arial" font-size="10.00" fill="#1f2933">ctrl/start.sh · stop.sh · status.sh · logs.sh</text>
|
||||
</g>
|
||||
<!-- l1->l4 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>l1->l4</title>
|
||||
<path fill="none" stroke="#9aa5b1" d="M232.31,-322.61C245.18,-309.92 260.34,-294.97 273.8,-281.7"/>
|
||||
<polygon fill="#9aa5b1" stroke="#9aa5b1" points="275.51,-283.45 278.78,-276.79 272.07,-279.96 275.51,-283.45"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="275.71" y="-295.95" font-family="Arial" font-size="9.00" fill="#616e7c">optional</text>
|
||||
</g>
|
||||
<!-- l3 -->
|
||||
<g id="node5" class="node">
|
||||
<title>l3</title>
|
||||
<polygon fill="#ffffff" stroke="#9aa5b1" points="10.88,-108 10.88,-168 182.12,-168 182.12,-108 10.88,-108"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="96.5" y="-154.5" font-family="Arial" font-size="10.00" fill="#1f2933">Layer 3</text>
|
||||
<polyline fill="none" stroke="#9aa5b1" points="10.88,-148 182.12,-148"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="96.5" y="-134.5" font-family="Arial" font-size="10.00" fill="#1f2933">Link</text>
|
||||
<polyline fill="none" stroke="#9aa5b1" points="10.88,-128 182.12,-128"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="96.5" y="-114.5" font-family="Arial" font-size="10.00" fill="#1f2933">link/main.py · adapters/ · Dockerfile</text>
|
||||
</g>
|
||||
<!-- l2->l3 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>l2->l3</title>
|
||||
<path fill="none" stroke="#9aa5b1" d="M96.5,-215.11C96.5,-203.22 96.5,-189.35 96.5,-176.73"/>
|
||||
<polygon fill="#9aa5b1" stroke="#9aa5b1" points="98.95,-176.74 96.5,-169.74 94.05,-176.74 98.95,-176.74"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="113" y="-188.45" font-family="Arial" font-size="9.00" fill="#616e7c">optional</text>
|
||||
</g>
|
||||
<!-- l5 -->
|
||||
<g id="node6" class="node">
|
||||
<title>l5</title>
|
||||
<polygon fill="#ffffff" stroke="#9aa5b1" points="231,-108 231,-168 388,-168 388,-108 231,-108"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="309.5" y="-154.5" font-family="Arial" font-size="10.00" fill="#1f2933">Layer 5</text>
|
||||
<polyline fill="none" stroke="#9aa5b1" points="231,-148 388,-148"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="309.5" y="-134.5" font-family="Arial" font-size="10.00" fill="#1f2933">Systems</text>
|
||||
<polyline fill="none" stroke="#9aa5b1" points="231,-128 388,-128"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="309.5" y="-114.5" font-family="Arial" font-size="10.00" fill="#1f2933">tester/environments.json · tests/</text>
|
||||
</g>
|
||||
<!-- l4->l5 -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>l4->l5</title>
|
||||
<path fill="none" stroke="#9aa5b1" d="M309.5,-215.11C309.5,-203.22 309.5,-189.35 309.5,-176.73"/>
|
||||
<polygon fill="#9aa5b1" stroke="#9aa5b1" points="311.95,-176.74 309.5,-169.74 307.05,-176.74 311.95,-176.74"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="326" y="-188.45" font-family="Arial" font-size="9.00" fill="#616e7c">optional</text>
|
||||
</g>
|
||||
<!-- l6 -->
|
||||
<g id="node7" class="node">
|
||||
<title>l6</title>
|
||||
<polygon fill="#ffffff" stroke="#9aa5b1" points="204.38,-0.5 204.38,-60.5 414.62,-60.5 414.62,-0.5 204.38,-0.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="309.5" y="-47" font-family="Arial" font-size="10.00" fill="#1f2933">Layer 6</text>
|
||||
<polyline fill="none" stroke="#9aa5b1" points="204.38,-40.5 414.62,-40.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="309.5" y="-27" font-family="Arial" font-size="10.00" fill="#1f2933">Nginx</text>
|
||||
<polyline fill="none" stroke="#9aa5b1" points="204.38,-20.5 414.62,-20.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="309.5" y="-7" font-family="Arial" font-size="10.00" fill="#1f2933">nginx/local.conf · docker-compose.nginx.yml</text>
|
||||
</g>
|
||||
<!-- l5->l6 -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>l5->l6</title>
|
||||
<path fill="none" stroke="#9aa5b1" d="M309.5,-107.61C309.5,-95.72 309.5,-81.85 309.5,-69.23"/>
|
||||
<polygon fill="#9aa5b1" stroke="#9aa5b1" points="311.95,-69.24 309.5,-62.24 307.05,-69.24 311.95,-69.24"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="329.75" y="-80.95" font-family="Arial" font-size="9.00" fill="#616e7c">if frontend</text>
|
||||
</g>
|
||||
<!-- note_req -->
|
||||
<g id="node8" class="node accent">
|
||||
<title>note_req</title>
|
||||
<polygon fill="#d6e4ff" stroke="none" points="233.38,-564 171.62,-564 171.62,-528 233.38,-528 233.38,-564"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="202.5" y="-542.7" font-family="Arial" font-size="9.00" fill="#1f2933">every room</text>
|
||||
</g>
|
||||
<!-- note_req->l0 -->
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.8 KiB |
@@ -11,7 +11,7 @@
|
||||
<polygon fill="#0a0a0a" stroke="none" points="-4,4 -4,-607.5 456.62,-607.5 456.62,4 -4,4"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="226.31" y="-586.2" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d4a574">Room Layers — init wizard</text>
|
||||
<!-- l0 -->
|
||||
<g id="node1" class="node">
|
||||
<g id="node1" class="node accent">
|
||||
<title>l0</title>
|
||||
<polygon fill="#1a1a1a" stroke="#d4a574" points="155.12,-442.5 155.12,-504.75 285.88,-504.75 285.88,-442.5 155.12,-442.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="220.5" y="-491.25" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#e5e5e5">Layer 0</text>
|
||||
@@ -55,7 +55,7 @@
|
||||
<text xml:space="preserve" text-anchor="middle" x="191.95" y="-304.95" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#a3a3a3">if managed</text>
|
||||
</g>
|
||||
<!-- l4 -->
|
||||
<g id="node5" class="node">
|
||||
<g id="node4" class="node">
|
||||
<title>l4</title>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="229.25,-221.5 229.25,-283.75 441.75,-283.75 441.75,-221.5 229.25,-221.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="335.5" y="-270.25" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#e5e5e5">Layer 4</text>
|
||||
@@ -65,14 +65,14 @@
|
||||
<text xml:space="preserve" text-anchor="middle" x="335.5" y="-228.75" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#e5e5e5">ctrl/start.sh · stop.sh · status.sh · logs.sh</text>
|
||||
</g>
|
||||
<!-- l1->l4 -->
|
||||
<g id="edge4" class="edge">
|
||||
<g id="edge3" class="edge">
|
||||
<title>l1->l4</title>
|
||||
<path fill="none" stroke="#666666" d="M252.84,-331.61C266.06,-319.14 281.48,-304.59 295.41,-291.45"/>
|
||||
<polygon fill="#666666" stroke="#666666" points="297.43,-294.36 302.3,-284.95 292.63,-289.26 297.43,-294.36"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="299.45" y="-304.95" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#a3a3a3">optional</text>
|
||||
</g>
|
||||
<!-- l3 -->
|
||||
<g id="node4" class="node">
|
||||
<g id="node5" class="node">
|
||||
<title>l3</title>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="10.88,-111 10.88,-173.25 200.12,-173.25 200.12,-111 10.88,-111"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="105.5" y="-159.75" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#e5e5e5">Layer 3</text>
|
||||
@@ -82,7 +82,7 @@
|
||||
<text xml:space="preserve" text-anchor="middle" x="105.5" y="-118.25" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#e5e5e5">link/main.py · adapters/ · Dockerfile</text>
|
||||
</g>
|
||||
<!-- l2->l3 -->
|
||||
<g id="edge3" class="edge">
|
||||
<g id="edge4" class="edge">
|
||||
<title>l2->l3</title>
|
||||
<path fill="none" stroke="#666666" d="M105.5,-221.11C105.5,-209.81 105.5,-196.79 105.5,-184.67"/>
|
||||
<polygon fill="#666666" stroke="#666666" points="109,-184.92 105.5,-174.92 102,-184.92 109,-184.92"/>
|
||||
@@ -123,10 +123,10 @@
|
||||
<text xml:space="preserve" text-anchor="middle" x="359.12" y="-83.95" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#a3a3a3">if frontend</text>
|
||||
</g>
|
||||
<!-- note_req -->
|
||||
<g id="node8" class="node">
|
||||
<g id="node8" class="node accent">
|
||||
<title>note_req</title>
|
||||
<polygon fill="#0a0a0a" stroke="none" points="254,-578.25 187,-578.25 187,-542.25 254,-542.25 254,-578.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="220.5" y="-557.33" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#d4a574">every room</text>
|
||||
<polygon fill="#1a1a1a" stroke="none" points="254,-578.25 187,-578.25 187,-542.25 254,-542.25 254,-578.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="220.5" y="-557.33" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#e5e5e5">every room</text>
|
||||
</g>
|
||||
<!-- note_req->l0 -->
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
@@ -1,88 +1,78 @@
|
||||
digraph system_overview {
|
||||
bgcolor="#0a0a0a"
|
||||
rankdir=TB
|
||||
compound=true
|
||||
fontname="Helvetica"
|
||||
node [fontname="Helvetica" fontsize=11 style=filled color="#333" fontcolor="#e5e5e5"]
|
||||
edge [fontname="Helvetica" fontsize=9 fontcolor="#a3a3a3" color="#666"]
|
||||
node [fontname="Helvetica" fontsize=11 style=filled]
|
||||
edge [fontname="Helvetica" fontsize=9]
|
||||
|
||||
label="Soleprint — System Overview"
|
||||
labelloc=t
|
||||
fontsize=14
|
||||
fontcolor="#d4a574"
|
||||
|
||||
// Core
|
||||
subgraph cluster_core {
|
||||
label="Soleprint Hub"
|
||||
style=dashed
|
||||
color="#d4a574"
|
||||
fontcolor="#d4a574"
|
||||
class="accent"
|
||||
|
||||
hub [label="soleprint\ncore coordinator\nport 12000" fillcolor="#1a1a1a" shape=box]
|
||||
hub [label="soleprint\ncore coordinator\nport 12000" shape=box]
|
||||
}
|
||||
|
||||
// Artery
|
||||
subgraph cluster_artery {
|
||||
label="Artery — Todo lo vital"
|
||||
style=dashed
|
||||
color="#b91c1c"
|
||||
fontcolor="#fca5a5"
|
||||
class="artery"
|
||||
|
||||
veins [label="Veins\nstateless connectors" fillcolor="#1a1a1a"]
|
||||
shunts [label="Shunts\nmock connectors" fillcolor="#1a1a1a"]
|
||||
pulses [label="Pulses\ncomposed flows" fillcolor="#1a1a1a"]
|
||||
veins [label="Veins\nstateless connectors"]
|
||||
shunts [label="Shunts\nmock connectors"]
|
||||
pulses [label="Pulses\ncomposed flows"]
|
||||
}
|
||||
|
||||
// Atlas
|
||||
subgraph cluster_atlas {
|
||||
label="Atlas — Documentacion accionable"
|
||||
style=dashed
|
||||
color="#15803d"
|
||||
fontcolor="#86efac"
|
||||
class="atlas"
|
||||
|
||||
books [label="Books\ndocumentation" fillcolor="#1a1a1a"]
|
||||
templates [label="Templates\npatterns" fillcolor="#1a1a1a"]
|
||||
books [label="Books\ndocumentation"]
|
||||
templates [label="Templates\npatterns"]
|
||||
}
|
||||
|
||||
// Station
|
||||
subgraph cluster_station {
|
||||
label="Station — Centro de control"
|
||||
style=dashed
|
||||
color="#1d4ed8"
|
||||
fontcolor="#93c5fd"
|
||||
class="station"
|
||||
|
||||
tools [label="Tools\ntester · datagen · modelgen" fillcolor="#1a1a1a"]
|
||||
monitors [label="Monitors\ndatabrowse" fillcolor="#1a1a1a"]
|
||||
tools [label="Tools\ntester · datagen · modelgen"]
|
||||
monitors [label="Monitors\ndatabrowse"]
|
||||
}
|
||||
|
||||
// External
|
||||
subgraph cluster_external {
|
||||
label="External APIs"
|
||||
style=dashed
|
||||
color="#333"
|
||||
fontcolor="#666"
|
||||
|
||||
jira [label="Jira" fillcolor="#1a1a1a" fontcolor="#a3a3a3"]
|
||||
google [label="Google" fillcolor="#1a1a1a" fontcolor="#a3a3a3"]
|
||||
slack [label="Slack" fillcolor="#1a1a1a" fontcolor="#a3a3a3"]
|
||||
jira [label="Jira"]
|
||||
google [label="Google"]
|
||||
slack [label="Slack"]
|
||||
}
|
||||
|
||||
// Managed app
|
||||
subgraph cluster_managed {
|
||||
label="Managed App"
|
||||
style=dashed
|
||||
color="#333"
|
||||
fontcolor="#666"
|
||||
|
||||
app_fe [label="Frontend" fillcolor="#1a1a1a" fontcolor="#a3a3a3"]
|
||||
app_be [label="Backend" fillcolor="#1a1a1a" fontcolor="#a3a3a3"]
|
||||
app_db [label="Database" fillcolor="#1a1a1a" fontcolor="#a3a3a3" shape=cylinder]
|
||||
app_fe [label="Frontend"]
|
||||
app_be [label="Backend"]
|
||||
app_db [label="Database" shape=cylinder]
|
||||
}
|
||||
|
||||
// Connections
|
||||
hub -> veins [label="routes" color="#b91c1c"]
|
||||
hub -> books [label="routes" color="#15803d"]
|
||||
hub -> tools [label="routes" color="#1d4ed8"]
|
||||
hub -> veins [class="artery" label="routes"]
|
||||
hub -> books [class="atlas" label="routes"]
|
||||
hub -> tools [class="station" label="routes"]
|
||||
|
||||
veins -> jira [label="API"]
|
||||
veins -> google [label="OAuth"]
|
||||
@@ -93,5 +83,5 @@ digraph system_overview {
|
||||
monitors -> app_db [label="browse" style=dashed]
|
||||
|
||||
// Sidebar injection
|
||||
hub -> app_fe [label="sidebar\ninjection" color="#d4a574" style=dashed]
|
||||
hub -> app_fe [class="accent" label="sidebar\ninjection" style=dashed]
|
||||
}
|
||||
|
||||
209
docs/graphs/system_overview.lucid.svg
Normal file
@@ -0,0 +1,209 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 14.1.2 (0)
|
||||
-->
|
||||
<!-- Title: system_overview Pages: 1 -->
|
||||
<svg width="1038pt" height="365pt"
|
||||
viewBox="0.00 0.00 1038.00 365.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 360.75)">
|
||||
<title>system_overview</title>
|
||||
<polygon fill="#ffffff" stroke="none" points="-4,4 -4,-360.75 1034,-360.75 1034,4 -4,4"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="515" y="-339.45" font-family="Arial" font-size="14.00" fill="#1f2933">Soleprint — System Overview</text>
|
||||
<g id="clust1" class="cluster accent">
|
||||
<title>cluster_core</title>
|
||||
<path fill="#d6e4ff" stroke="#3a7dff" stroke-dasharray="5,2" d="M493,-236.75C493,-236.75 579,-236.75 579,-236.75 585,-236.75 591,-242.75 591,-248.75 591,-248.75 591,-313 591,-313 591,-319 585,-325 579,-325 579,-325 493,-325 493,-325 487,-325 481,-319 481,-313 481,-313 481,-248.75 481,-248.75 481,-242.75 487,-236.75 493,-236.75"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="536" y="-307.7" font-family="Arial" font-size="14.00" fill="#616e7c">Soleprint Hub</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster artery">
|
||||
<title>cluster_artery</title>
|
||||
<path fill="#fdeaea" stroke="#c0392b" stroke-dasharray="5,2" d="M20,-8C20,-8 328,-8 328,-8 334,-8 340,-14 340,-20 340,-20 340,-196.25 340,-196.25 340,-202.25 334,-208.25 328,-208.25 328,-208.25 20,-208.25 20,-208.25 14,-208.25 8,-202.25 8,-196.25 8,-196.25 8,-20 8,-20 8,-14 14,-8 20,-8"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="174" y="-190.95" font-family="Arial" font-size="14.00" fill="#616e7c">Artery — Todo lo vital</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster atlas">
|
||||
<title>cluster_atlas</title>
|
||||
<path fill="#e6f5ec" stroke="#1a7f45" stroke-dasharray="5,2" d="M360,-119C360,-119 584,-119 584,-119 590,-119 596,-125 596,-131 596,-131 596,-196.25 596,-196.25 596,-202.25 590,-208.25 584,-208.25 584,-208.25 360,-208.25 360,-208.25 354,-208.25 348,-202.25 348,-196.25 348,-196.25 348,-131 348,-131 348,-125 354,-119 360,-119"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="472" y="-190.95" font-family="Arial" font-size="14.00" fill="#616e7c">Atlas — Documentacion accionable</text>
|
||||
</g>
|
||||
<g id="clust4" class="cluster station">
|
||||
<title>cluster_station</title>
|
||||
<path fill="#e8effd" stroke="#2b5fd9" stroke-dasharray="5,2" d="M616,-119C616,-119 938,-119 938,-119 944,-119 950,-125 950,-131 950,-131 950,-196.25 950,-196.25 950,-202.25 944,-208.25 938,-208.25 938,-208.25 616,-208.25 616,-208.25 610,-208.25 604,-202.25 604,-196.25 604,-196.25 604,-131 604,-131 604,-125 610,-119 616,-119"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="777" y="-190.95" font-family="Arial" font-size="14.00" fill="#616e7c">Station — Centro de control</text>
|
||||
</g>
|
||||
<g id="clust5" class="cluster">
|
||||
<title>cluster_external</title>
|
||||
<path fill="#f5f7fa" stroke="#cbd2d9" stroke-dasharray="5,2" d="M360,-14.75C360,-14.75 558,-14.75 558,-14.75 564,-14.75 570,-20.75 570,-26.75 570,-26.75 570,-78.5 570,-78.5 570,-84.5 564,-90.5 558,-90.5 558,-90.5 360,-90.5 360,-90.5 354,-90.5 348,-84.5 348,-78.5 348,-78.5 348,-26.75 348,-26.75 348,-20.75 354,-14.75 360,-14.75"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="459" y="-73.2" font-family="Arial" font-size="14.00" fill="#616e7c">External APIs</text>
|
||||
</g>
|
||||
<g id="clust6" class="cluster">
|
||||
<title>cluster_managed</title>
|
||||
<path fill="#f5f7fa" stroke="#cbd2d9" stroke-dasharray="5,2" d="M774,-14.75C774,-14.75 1010,-14.75 1010,-14.75 1016,-14.75 1022,-20.75 1022,-26.75 1022,-26.75 1022,-78.5 1022,-78.5 1022,-84.5 1016,-90.5 1010,-90.5 1010,-90.5 774,-90.5 774,-90.5 768,-90.5 762,-84.5 762,-78.5 762,-78.5 762,-26.75 762,-26.75 762,-20.75 768,-14.75 774,-14.75"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="892" y="-73.2" font-family="Arial" font-size="14.00" fill="#616e7c">Managed App</text>
|
||||
</g>
|
||||
<!-- hub -->
|
||||
<g id="node1" class="node">
|
||||
<title>hub</title>
|
||||
<path fill="#ffffff" stroke="#9aa5b1" d="M571,-293.25C571,-293.25 501,-293.25 501,-293.25 495,-293.25 489,-287.25 489,-281.25 489,-281.25 489,-256.75 489,-256.75 489,-250.75 495,-244.75 501,-244.75 501,-244.75 571,-244.75 571,-244.75 577,-244.75 583,-250.75 583,-256.75 583,-256.75 583,-281.25 583,-281.25 583,-287.25 577,-293.25 571,-293.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="536" y="-278.8" font-family="Arial" font-size="11.00" fill="#1f2933">soleprint</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="536" y="-265.3" font-family="Arial" font-size="11.00" fill="#1f2933">core coordinator</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="536" y="-251.8" font-family="Arial" font-size="11.00" fill="#1f2933">port 12000</text>
|
||||
</g>
|
||||
<!-- veins -->
|
||||
<g id="node2" class="node">
|
||||
<title>veins</title>
|
||||
<ellipse fill="#ffffff" stroke="#9aa5b1" cx="96" cy="-151.75" rx="80.26" ry="24.75"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="96" y="-154.8" font-family="Arial" font-size="11.00" fill="#1f2933">Veins</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="96" y="-141.3" font-family="Arial" font-size="11.00" fill="#1f2933">stateless connectors</text>
|
||||
</g>
|
||||
<!-- hub->veins -->
|
||||
<g id="edge1" class="edge artery">
|
||||
<title>hub->veins</title>
|
||||
<path fill="none" stroke="#c0392b" d="M488.78,-265.75C419.92,-261.21 288.87,-247.71 185,-208.25 166.55,-201.24 147.77,-190.21 132.24,-179.83"/>
|
||||
<polygon fill="#c0392b" stroke="#c0392b" points="133.76,-177.9 126.6,-175.98 131,-181.95 133.76,-177.9"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="252.94" y="-218.2" font-family="Arial" font-size="9.00" fill="#c0392b">routes</text>
|
||||
</g>
|
||||
<!-- books -->
|
||||
<g id="node5" class="node">
|
||||
<title>books</title>
|
||||
<ellipse fill="#ffffff" stroke="#9aa5b1" cx="417" cy="-151.75" rx="61.16" ry="24.75"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="417" y="-154.8" font-family="Arial" font-size="11.00" fill="#1f2933">Books</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="417" y="-141.3" font-family="Arial" font-size="11.00" fill="#1f2933">documentation</text>
|
||||
</g>
|
||||
<!-- hub->books -->
|
||||
<g id="edge2" class="edge atlas">
|
||||
<title>hub->books</title>
|
||||
<path fill="none" stroke="#1a7f45" d="M511.63,-244.4C492.65,-226.01 466.24,-200.44 446.14,-180.97"/>
|
||||
<polygon fill="#1a7f45" stroke="#1a7f45" points="447.94,-179.3 441.21,-176.19 444.53,-182.82 447.94,-179.3"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="503.41" y="-218.2" font-family="Arial" font-size="9.00" fill="#1a7f45">routes</text>
|
||||
</g>
|
||||
<!-- tools -->
|
||||
<g id="node7" class="node">
|
||||
<title>tools</title>
|
||||
<ellipse fill="#ffffff" stroke="#9aa5b1" cx="717" cy="-151.75" rx="105.18" ry="24.75"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="717" y="-154.8" font-family="Arial" font-size="11.00" fill="#1f2933">Tools</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="717" y="-141.3" font-family="Arial" font-size="11.00" fill="#1f2933">tester · datagen · modelgen</text>
|
||||
</g>
|
||||
<!-- hub->tools -->
|
||||
<g id="edge3" class="edge station">
|
||||
<title>hub->tools</title>
|
||||
<path fill="none" stroke="#2b5fd9" d="M573.06,-244.4C602.58,-225.6 643.92,-199.28 674.73,-179.67"/>
|
||||
<polygon fill="#2b5fd9" stroke="#2b5fd9" points="675.66,-181.98 680.25,-176.15 673.03,-177.84 675.66,-181.98"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="628.41" y="-218.2" font-family="Arial" font-size="9.00" fill="#2b5fd9">routes</text>
|
||||
</g>
|
||||
<!-- app_fe -->
|
||||
<g id="node12" class="node">
|
||||
<title>app_fe</title>
|
||||
<ellipse fill="#ffffff" stroke="#9aa5b1" cx="977" cy="-40.75" rx="37.09" ry="18"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="977" y="-37.05" font-family="Arial" font-size="11.00" fill="#1f2933">Frontend</text>
|
||||
</g>
|
||||
<!-- hub->app_fe -->
|
||||
<g id="edge4" class="edge accent">
|
||||
<title>hub->app_fe</title>
|
||||
<path fill="none" stroke="#3a7dff" stroke-dasharray="5,2" d="M583.13,-268.1C681.31,-267.28 902.87,-259.62 954,-208.25 991.07,-171.01 987.37,-103.78 981.91,-67.18"/>
|
||||
<polygon fill="#3a7dff" stroke="#3a7dff" points="984.37,-67.01 980.83,-60.49 979.53,-67.79 984.37,-67.01"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1002.46" y="-153.7" font-family="Arial" font-size="9.00" fill="#3a7dff">sidebar</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1002.46" y="-143.2" font-family="Arial" font-size="9.00" fill="#3a7dff">injection</text>
|
||||
</g>
|
||||
<!-- pulses -->
|
||||
<g id="node4" class="node">
|
||||
<title>pulses</title>
|
||||
<ellipse fill="#ffffff" stroke="#9aa5b1" cx="96" cy="-40.75" rx="65.94" ry="24.75"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="96" y="-43.8" font-family="Arial" font-size="11.00" fill="#1f2933">Pulses</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="96" y="-30.3" font-family="Arial" font-size="11.00" fill="#1f2933">composed flows</text>
|
||||
</g>
|
||||
<!-- veins->pulses -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>veins->pulses</title>
|
||||
<path fill="none" stroke="#9aa5b1" d="M96,-126.56C96,-111.18 96,-91.07 96,-74.3"/>
|
||||
<polygon fill="#9aa5b1" stroke="#9aa5b1" points="98.45,-74.38 96,-67.39 93.55,-74.39 98.45,-74.38"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="114.75" y="-100.45" font-family="Arial" font-size="9.00" fill="#616e7c">compose</text>
|
||||
</g>
|
||||
<!-- jira -->
|
||||
<g id="node9" class="node">
|
||||
<title>jira</title>
|
||||
<ellipse fill="#ffffff" stroke="#9aa5b1" cx="383" cy="-40.75" rx="27" ry="18"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="383" y="-37.05" font-family="Arial" font-size="11.00" fill="#1f2933">Jira</text>
|
||||
</g>
|
||||
<!-- veins->jira -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>veins->jira</title>
|
||||
<path fill="none" stroke="#9aa5b1" d="M144.51,-131.81C157.52,-127.18 171.68,-122.54 185,-119 254.38,-100.54 282.3,-127.21 344,-90.5 354.39,-84.32 363.13,-74.26 369.69,-64.89"/>
|
||||
<polygon fill="#9aa5b1" stroke="#9aa5b1" points="371.64,-66.39 373.44,-59.19 367.54,-63.69 371.64,-66.39"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="333.55" y="-100.45" font-family="Arial" font-size="9.00" fill="#616e7c">API</text>
|
||||
</g>
|
||||
<!-- google -->
|
||||
<g id="node10" class="node">
|
||||
<title>google</title>
|
||||
<ellipse fill="#ffffff" stroke="#9aa5b1" cx="459" cy="-40.75" rx="31.48" ry="18"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="459" y="-37.05" font-family="Arial" font-size="11.00" fill="#1f2933">Google</text>
|
||||
</g>
|
||||
<!-- veins->google -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>veins->google</title>
|
||||
<path fill="none" stroke="#9aa5b1" d="M142.87,-131.26C156.26,-126.46 171.02,-121.87 185,-119 287.63,-97.92 330.48,-146.53 419,-90.5 429.17,-84.06 437.99,-74.16 444.73,-64.95"/>
|
||||
<polygon fill="#9aa5b1" stroke="#9aa5b1" points="446.63,-66.51 448.61,-59.37 442.61,-63.72 446.63,-66.51"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="416.29" y="-100.45" font-family="Arial" font-size="9.00" fill="#616e7c">OAuth</text>
|
||||
</g>
|
||||
<!-- slack -->
|
||||
<g id="node11" class="node">
|
||||
<title>slack</title>
|
||||
<ellipse fill="#ffffff" stroke="#9aa5b1" cx="535" cy="-40.75" rx="27" ry="18"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="535" y="-37.05" font-family="Arial" font-size="11.00" fill="#1f2933">Slack</text>
|
||||
</g>
|
||||
<!-- veins->slack -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>veins->slack</title>
|
||||
<path fill="none" stroke="#9aa5b1" d="M142.37,-131.22C155.88,-126.36 170.83,-121.75 185,-119 293.3,-98.01 324.04,-126.22 433,-109 463.09,-104.24 473.93,-107.8 499,-90.5 508.43,-83.99 516.35,-74.2 522.34,-65.1"/>
|
||||
<polygon fill="#9aa5b1" stroke="#9aa5b1" points="524.32,-66.55 525.93,-59.3 520.16,-63.97 524.32,-66.55"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="492.52" y="-100.45" font-family="Arial" font-size="9.00" fill="#616e7c">API</text>
|
||||
</g>
|
||||
<!-- shunts -->
|
||||
<g id="node3" class="node">
|
||||
<title>shunts</title>
|
||||
<ellipse fill="#ffffff" stroke="#9aa5b1" cx="263" cy="-151.75" rx="68.59" ry="24.75"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="263" y="-154.8" font-family="Arial" font-size="11.00" fill="#1f2933">Shunts</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="263" y="-141.3" font-family="Arial" font-size="11.00" fill="#1f2933">mock connectors</text>
|
||||
</g>
|
||||
<!-- templates -->
|
||||
<g id="node6" class="node">
|
||||
<title>templates</title>
|
||||
<ellipse fill="#ffffff" stroke="#9aa5b1" cx="542" cy="-151.75" rx="45.79" ry="24.75"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="542" y="-154.8" font-family="Arial" font-size="11.00" fill="#1f2933">Templates</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="542" y="-141.3" font-family="Arial" font-size="11.00" fill="#1f2933">patterns</text>
|
||||
</g>
|
||||
<!-- app_be -->
|
||||
<g id="node13" class="node">
|
||||
<title>app_be</title>
|
||||
<ellipse fill="#ffffff" stroke="#9aa5b1" cx="806" cy="-40.75" rx="36.16" ry="18"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="806" y="-37.05" font-family="Arial" font-size="11.00" fill="#1f2933">Backend</text>
|
||||
</g>
|
||||
<!-- tools->app_be -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>tools->app_be</title>
|
||||
<path fill="none" stroke="#9aa5b1" stroke-dasharray="5,2" d="M736.31,-127.1C751.52,-108.47 772.58,-82.68 787.56,-64.33"/>
|
||||
<polygon fill="#9aa5b1" stroke="#9aa5b1" points="789.29,-66.08 791.82,-59.11 785.5,-62.99 789.29,-66.08"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="765.96" y="-100.45" font-family="Arial" font-size="9.00" fill="#616e7c">test</text>
|
||||
</g>
|
||||
<!-- monitors -->
|
||||
<g id="node8" class="node">
|
||||
<title>monitors</title>
|
||||
<ellipse fill="#ffffff" stroke="#9aa5b1" cx="891" cy="-151.75" rx="51.09" ry="24.75"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="891" y="-154.8" font-family="Arial" font-size="11.00" fill="#1f2933">Monitors</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="891" y="-141.3" font-family="Arial" font-size="11.00" fill="#1f2933">databrowse</text>
|
||||
</g>
|
||||
<!-- app_db -->
|
||||
<g id="node14" class="node">
|
||||
<title>app_db</title>
|
||||
<path fill="#ffffff" stroke="#9aa5b1" d="M922.25,-55.48C922.25,-57.28 908.24,-58.75 891,-58.75 873.76,-58.75 859.75,-57.28 859.75,-55.48 859.75,-55.48 859.75,-26.02 859.75,-26.02 859.75,-24.22 873.76,-22.75 891,-22.75 908.24,-22.75 922.25,-24.22 922.25,-26.02 922.25,-26.02 922.25,-55.48 922.25,-55.48"/>
|
||||
<path fill="none" stroke="#9aa5b1" d="M922.25,-55.48C922.25,-53.67 908.24,-52.2 891,-52.2 873.76,-52.2 859.75,-53.67 859.75,-55.48"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="891" y="-37.05" font-family="Arial" font-size="11.00" fill="#1f2933">Database</text>
|
||||
</g>
|
||||
<!-- monitors->app_db -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>monitors->app_db</title>
|
||||
<path fill="none" stroke="#9aa5b1" stroke-dasharray="5,2" d="M891,-126.56C891,-108.99 891,-85.25 891,-67.36"/>
|
||||
<polygon fill="#9aa5b1" stroke="#9aa5b1" points="893.45,-67.61 891,-60.61 888.55,-67.61 893.45,-67.61"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="906" y="-100.45" font-family="Arial" font-size="9.00" fill="#616e7c">browse</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
@@ -10,22 +10,22 @@
|
||||
<title>system_overview</title>
|
||||
<polygon fill="#0a0a0a" stroke="none" points="-4,4 -4,-368.25 1149,-368.25 1149,4 -4,4"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="572.5" y="-346.95" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d4a574">Soleprint — System Overview</text>
|
||||
<g id="clust1" class="cluster">
|
||||
<g id="clust1" class="cluster accent">
|
||||
<title>cluster_core</title>
|
||||
<polygon fill="#0a0a0a" stroke="#d4a574" stroke-dasharray="5,2" points="538,-241.25 538,-331 660,-331 660,-241.25 538,-241.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="599" y="-313.7" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d4a574">Soleprint Hub</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<g id="clust2" class="cluster artery">
|
||||
<title>cluster_artery</title>
|
||||
<polygon fill="#0a0a0a" stroke="#b91c1c" stroke-dasharray="5,2" points="8,-8 8,-212 382,-212 382,-8 8,-8"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="195" y="-194.7" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#fca5a5">Artery — Todo lo vital</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster">
|
||||
<g id="clust3" class="cluster atlas">
|
||||
<title>cluster_atlas</title>
|
||||
<polygon fill="#0a0a0a" stroke="#15803d" stroke-dasharray="5,2" points="390,-121.25 390,-212 665,-212 665,-121.25 390,-121.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="527.5" y="-194.7" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#86efac">Atlas — Documentacion accionable</text>
|
||||
</g>
|
||||
<g id="clust4" class="cluster">
|
||||
<g id="clust4" class="cluster station">
|
||||
<title>cluster_station</title>
|
||||
<polygon fill="#0a0a0a" stroke="#1d4ed8" stroke-dasharray="5,2" points="673,-121.25 673,-212 1062,-212 1062,-121.25 673,-121.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="867.5" y="-194.7" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#93c5fd">Station — Centro de control</text>
|
||||
@@ -56,11 +56,11 @@
|
||||
<text xml:space="preserve" text-anchor="middle" x="108" y="-143.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">stateless connectors</text>
|
||||
</g>
|
||||
<!-- hub->veins -->
|
||||
<g id="edge1" class="edge">
|
||||
<g id="edge1" class="edge artery">
|
||||
<title>hub->veins</title>
|
||||
<path fill="none" stroke="#b91c1c" d="M545.18,-270.24C468.57,-265.68 324.55,-252.07 209,-212 188.62,-204.93 167.6,-193.79 150.05,-183.23"/>
|
||||
<polygon fill="#b91c1c" stroke="#b91c1c" points="152.32,-180.52 141.98,-178.24 148.65,-186.47 152.32,-180.52"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="284.95" y="-222.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#a3a3a3">routes</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="284.95" y="-222.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#fca5a5">routes</text>
|
||||
</g>
|
||||
<!-- books -->
|
||||
<g id="node5" class="node">
|
||||
@@ -70,11 +70,11 @@
|
||||
<text xml:space="preserve" text-anchor="middle" x="468" y="-143.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">documentation</text>
|
||||
</g>
|
||||
<!-- hub->books -->
|
||||
<g id="edge2" class="edge">
|
||||
<g id="edge2" class="edge atlas">
|
||||
<title>hub->books</title>
|
||||
<path fill="none" stroke="#15803d" d="M572.81,-249C552.24,-230.55 523.47,-204.75 501.27,-184.84"/>
|
||||
<polygon fill="#15803d" stroke="#15803d" points="503.73,-182.34 493.94,-178.27 499.05,-187.55 503.73,-182.34"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="567.14" y="-222.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#a3a3a3">routes</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="567.14" y="-222.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#86efac">routes</text>
|
||||
</g>
|
||||
<!-- tools -->
|
||||
<g id="node7" class="node">
|
||||
@@ -84,25 +84,25 @@
|
||||
<text xml:space="preserve" text-anchor="middle" x="802" y="-143.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">tester · datagen · modelgen</text>
|
||||
</g>
|
||||
<!-- hub->tools -->
|
||||
<g id="edge3" class="edge">
|
||||
<g id="edge3" class="edge station">
|
||||
<title>hub->tools</title>
|
||||
<path fill="none" stroke="#1d4ed8" d="M639.59,-249C672.13,-230.17 717.93,-203.66 752.6,-183.59"/>
|
||||
<polygon fill="#1d4ed8" stroke="#1d4ed8" points="754.3,-186.65 761.2,-178.61 750.8,-180.59 754.3,-186.65"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="702.6" y="-222.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#a3a3a3">routes</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="702.6" y="-222.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#93c5fd">routes</text>
|
||||
</g>
|
||||
<!-- app_fe -->
|
||||
<g id="node12" class="node">
|
||||
<title>app_fe</title>
|
||||
<ellipse fill="#1a1a1a" stroke="#333333" cx="1089" cy="-40.75" rx="39.9" ry="18"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1089" y="-37.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#a3a3a3">Frontend</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1089" y="-37.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">Frontend</text>
|
||||
</g>
|
||||
<!-- hub->app_fe -->
|
||||
<g id="edge10" class="edge">
|
||||
<g id="edge4" class="edge accent">
|
||||
<title>hub->app_fe</title>
|
||||
<path fill="none" stroke="#d4a574" stroke-dasharray="5,2" d="M652.46,-273.38C762.51,-274.01 1008.83,-268.85 1066,-212 1103.21,-174.99 1100.13,-108.42 1094.57,-70.42"/>
|
||||
<polygon fill="#d4a574" stroke="#d4a574" points="1098.04,-69.95 1092.97,-60.65 1091.14,-71.08 1098.04,-69.95"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1116.94" y="-156.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#a3a3a3">sidebar</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1116.94" y="-145.45" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#a3a3a3">injection</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1116.94" y="-156.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#d4a574">sidebar</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1116.94" y="-145.45" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#d4a574">injection</text>
|
||||
</g>
|
||||
<!-- pulses -->
|
||||
<g id="node4" class="node">
|
||||
@@ -112,7 +112,7 @@
|
||||
<text xml:space="preserve" text-anchor="middle" x="108" y="-30.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">composed flows</text>
|
||||
</g>
|
||||
<!-- veins->pulses -->
|
||||
<g id="edge7" class="edge">
|
||||
<g id="edge5" class="edge">
|
||||
<title>veins->pulses</title>
|
||||
<path fill="none" stroke="#666666" d="M108,-128.86C108,-113.7 108,-93.88 108,-76.98"/>
|
||||
<polygon fill="#666666" stroke="#666666" points="111.5,-77.35 108,-67.35 104.5,-77.35 111.5,-77.35"/>
|
||||
@@ -122,10 +122,10 @@
|
||||
<g id="node9" class="node">
|
||||
<title>jira</title>
|
||||
<ellipse fill="#1a1a1a" stroke="#333333" cx="425" cy="-40.75" rx="27" ry="18"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="425" y="-37.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#a3a3a3">Jira</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="425" y="-37.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">Jira</text>
|
||||
</g>
|
||||
<!-- veins->jira -->
|
||||
<g id="edge4" class="edge">
|
||||
<g id="edge6" class="edge">
|
||||
<title>veins->jira</title>
|
||||
<path fill="none" stroke="#666666" d="M163.97,-133.86C178.52,-129.32 194.25,-124.77 209,-121.25 286.56,-102.75 317.5,-132.81 386,-92 395.89,-86.1 404.2,-76.67 410.57,-67.57"/>
|
||||
<polygon fill="#666666" stroke="#666666" points="413.44,-69.58 415.88,-59.27 407.54,-65.8 413.44,-69.58"/>
|
||||
@@ -135,10 +135,10 @@
|
||||
<g id="node10" class="node">
|
||||
<title>google</title>
|
||||
<ellipse fill="#1a1a1a" stroke="#333333" cx="504" cy="-40.75" rx="33.82" ry="18"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="504" y="-37.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#a3a3a3">Google</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="504" y="-37.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">Google</text>
|
||||
</g>
|
||||
<!-- veins->google -->
|
||||
<g id="edge5" class="edge">
|
||||
<g id="edge7" class="edge">
|
||||
<title>veins->google</title>
|
||||
<path fill="none" stroke="#666666" d="M161.63,-133.42C176.76,-128.65 193.36,-124.1 209,-121.25 287.39,-106.97 309.3,-123.69 388,-111.25 421.14,-106.01 432.58,-109.83 461,-92 471.11,-85.66 480.09,-76.19 487.2,-67.17"/>
|
||||
<polygon fill="#666666" stroke="#666666" points="489.88,-69.43 493,-59.31 484.25,-65.28 489.88,-69.43"/>
|
||||
@@ -148,10 +148,10 @@
|
||||
<g id="node11" class="node">
|
||||
<title>slack</title>
|
||||
<ellipse fill="#1a1a1a" stroke="#333333" cx="584" cy="-40.75" rx="27.74" ry="18"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="584" y="-37.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#a3a3a3">Slack</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="584" y="-37.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">Slack</text>
|
||||
</g>
|
||||
<!-- veins->slack -->
|
||||
<g id="edge6" class="edge">
|
||||
<g id="edge8" class="edge">
|
||||
<title>veins->slack</title>
|
||||
<path fill="none" stroke="#666666" d="M161.07,-133.39C176.34,-128.56 193.15,-123.98 209,-121.25 326.9,-100.91 359.79,-129.67 478,-111.25 509.46,-106.35 520.8,-110.08 547,-92 556.07,-85.74 563.77,-76.52 569.76,-67.68"/>
|
||||
<polygon fill="#666666" stroke="#666666" points="572.65,-69.66 574.98,-59.33 566.72,-65.96 572.65,-69.66"/>
|
||||
@@ -175,10 +175,10 @@
|
||||
<g id="node13" class="node">
|
||||
<title>app_be</title>
|
||||
<ellipse fill="#1a1a1a" stroke="#333333" cx="906" cy="-40.75" rx="38.96" ry="18"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="906" y="-37.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#a3a3a3">Backend</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="906" y="-37.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">Backend</text>
|
||||
</g>
|
||||
<!-- tools->app_be -->
|
||||
<g id="edge8" class="edge">
|
||||
<g id="edge9" class="edge">
|
||||
<title>tools->app_be</title>
|
||||
<path fill="none" stroke="#666666" stroke-dasharray="5,2" d="M824.06,-129.4C841.45,-110.79 865.64,-84.93 883.22,-66.12"/>
|
||||
<polygon fill="#666666" stroke="#666666" points="885.72,-68.57 889.99,-58.88 880.6,-63.79 885.72,-68.57"/>
|
||||
@@ -196,10 +196,10 @@
|
||||
<title>app_db</title>
|
||||
<path fill="#1a1a1a" stroke="#333333" d="M1031.25,-55.48C1031.25,-57.28 1015.9,-58.75 997,-58.75 978.1,-58.75 962.75,-57.28 962.75,-55.48 962.75,-55.48 962.75,-26.02 962.75,-26.02 962.75,-24.22 978.1,-22.75 997,-22.75 1015.9,-22.75 1031.25,-24.22 1031.25,-26.02 1031.25,-26.02 1031.25,-55.48 1031.25,-55.48"/>
|
||||
<path fill="none" stroke="#333333" d="M1031.25,-55.48C1031.25,-53.67 1015.9,-52.2 997,-52.2 978.1,-52.2 962.75,-53.67 962.75,-55.48"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="997" y="-37.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#a3a3a3">Database</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="997" y="-37.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">Database</text>
|
||||
</g>
|
||||
<!-- monitors->app_db -->
|
||||
<g id="edge9" class="edge">
|
||||
<g id="edge10" class="edge">
|
||||
<title>monitors->app_db</title>
|
||||
<path fill="none" stroke="#666666" stroke-dasharray="5,2" d="M997,-128.86C997,-111.64 997,-88.42 997,-70.29"/>
|
||||
<polygon fill="#666666" stroke="#666666" points="1000.5,-70.46 997,-60.46 993.5,-70.46 1000.5,-70.46"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
92
docs/graphs/themes/dark.gvpr
Normal file
@@ -0,0 +1,92 @@
|
||||
// Dark — the palette the docs have always used, restated as a theme.
|
||||
//
|
||||
// This exists so the .dot sources can stop carrying colour. It reproduces what
|
||||
// the committed SVGs look like today, which also makes it the regression
|
||||
// oracle: strip the inline colours from a source, render it through this, and
|
||||
// the result should be the diagram you had before. If it is not, the strip was
|
||||
// wrong.
|
||||
//
|
||||
// Palette matches common/theme/themes/soleprint.css, so a diagram and the page
|
||||
// around it are the same visual language.
|
||||
|
||||
BEGIN {
|
||||
string BG = "#0a0a0a";
|
||||
string FILL = "#1a1a1a";
|
||||
string LINE = "#333333";
|
||||
string INK = "#e5e5e5";
|
||||
string MUTED = "#a3a3a3";
|
||||
string DIM = "#666666";
|
||||
string FONT = "Helvetica";
|
||||
graph_t sg;
|
||||
|
||||
// The soleprint amber, and the three system colours, each with the lighter
|
||||
// text variant the docs use for labels on dark.
|
||||
string ACC_S = "#d4a574"; string ACC_T = "#d4a574";
|
||||
string ART_S = "#b91c1c"; string ART_T = "#fca5a5";
|
||||
string ATL_S = "#15803d"; string ATL_T = "#86efac";
|
||||
string STA_S = "#1d4ed8"; string STA_T = "#93c5fd";
|
||||
}
|
||||
|
||||
BEG_G {
|
||||
$G.bgcolor = BG;
|
||||
setDflt($G, "N", "fontname", FONT);
|
||||
setDflt($G, "E", "fontname", FONT);
|
||||
// Declare `class` so reading it on an untagged object is defined
|
||||
// rather than a warning — most nodes carry no class by design.
|
||||
setDflt($G, "G", "class", "");
|
||||
setDflt($G, "N", "class", "");
|
||||
setDflt($G, "E", "class", "");
|
||||
$G.fontname = FONT;
|
||||
// The graph title takes the accent of whatever system the graph is about;
|
||||
// graphs declare that with a class on the digraph itself.
|
||||
if ($G.class == "artery") { $G.fontcolor = ART_T; }
|
||||
else if ($G.class == "atlas") { $G.fontcolor = ATL_T; }
|
||||
else if ($G.class == "station") { $G.fontcolor = STA_T; }
|
||||
else { $G.fontcolor = ACC_T; }
|
||||
|
||||
|
||||
for (sg = fstsubg($G); sg; sg = nxtsubg(sg)) {
|
||||
if (index(sg.name, "cluster") == 0) {
|
||||
sg.fontname = FONT;
|
||||
sg.color = LINE;
|
||||
sg.fontcolor = DIM;
|
||||
if (sg.class == "artery") { sg.color = ART_S; sg.fontcolor = ART_T; }
|
||||
else if (sg.class == "atlas") { sg.color = ATL_S; sg.fontcolor = ATL_T; }
|
||||
else if (sg.class == "station") { sg.color = STA_S; sg.fontcolor = STA_T; }
|
||||
else if (sg.class == "accent") { sg.color = ACC_S; sg.fontcolor = ACC_T; }
|
||||
else if (sg.class == "ok") { sg.color = ATL_S; sg.fontcolor = ATL_T; }
|
||||
if (index(sg.style, "dashed") < 0) { sg.style = "dashed"; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
N {
|
||||
if (index($.style, "invis") >= 0) { continue; }
|
||||
|
||||
$.fontname = FONT;
|
||||
$.fontcolor = INK;
|
||||
$.color = LINE;
|
||||
$.fillcolor = FILL;
|
||||
|
||||
if ($.class == "accent") { $.color = ACC_S; }
|
||||
else if ($.class == "artery") { $.color = ART_S; }
|
||||
else if ($.class == "atlas") { $.color = ATL_S; }
|
||||
else if ($.class == "station") { $.color = STA_S; }
|
||||
else if ($.class == "accent-text") { $.fontcolor = ACC_T; }
|
||||
else if ($.class == "ok") { $.fontcolor = ATL_T; }
|
||||
else if ($.class == "muted") { $.fontcolor = MUTED; }
|
||||
|
||||
if (index($.style, "dashed") >= 0) { $.style = "filled,dashed"; }
|
||||
else { $.style = "filled"; }
|
||||
}
|
||||
|
||||
E {
|
||||
$.fontname = FONT;
|
||||
$.fontcolor = MUTED;
|
||||
$.color = DIM;
|
||||
|
||||
if ($.class == "accent") { $.color = ACC_S; $.fontcolor = ACC_T; }
|
||||
else if ($.class == "artery") { $.color = ART_S; $.fontcolor = ART_T; }
|
||||
else if ($.class == "atlas") { $.color = ATL_S; $.fontcolor = ATL_T; }
|
||||
else if ($.class == "station") { $.color = STA_S; $.fontcolor = STA_T; }
|
||||
}
|
||||
114
docs/graphs/themes/lucid.gvpr
Normal file
@@ -0,0 +1,114 @@
|
||||
// Lucid — graphviz output shaped after a lucid.app export.
|
||||
//
|
||||
// Run through gvpr, not as .dot attributes, because gvpr rewrites the parsed
|
||||
// graph: it wins over whatever the source set inline, so one .dot renders in
|
||||
// any theme without being edited. See render.sh.
|
||||
//
|
||||
// The look, and why each part is what it is:
|
||||
// white canvas this ends up in a document and gets printed
|
||||
// rounded box, hairline Lucid's default shape is a rounded rect with a grey
|
||||
// 1px stroke, not a coloured fill
|
||||
// pale-blue accent #d6e4ff on #3a7dff is Lucid's own selected-shape pair
|
||||
// Arial the one face guaranteed on Windows, and aliased to
|
||||
// Liberation Sans by fontconfig on Linux — so the SVG
|
||||
// measures the same on both and the text does not
|
||||
// reflow out of its box
|
||||
//
|
||||
// Structure is preserved, only palette is replaced. Two styles carry meaning
|
||||
// rather than decoration and are explicitly kept: `invis` (a layout spacer —
|
||||
// overwriting it makes hidden scaffolding visible) and `dashed` (a weaker
|
||||
// relationship). Shapes are never touched: a cylinder is a datastore.
|
||||
|
||||
BEGIN {
|
||||
string BG = "#ffffff";
|
||||
string INK = "#1f2933";
|
||||
string MUTED = "#616e7c";
|
||||
string LINE = "#9aa5b1";
|
||||
string SOFT = "#cbd2d9";
|
||||
string FONT = "Arial";
|
||||
graph_t sg;
|
||||
|
||||
string FILL = "#ffffff";
|
||||
string FILL_ALT = "#f5f7fa";
|
||||
|
||||
// class -> (stroke, fill). Lucid's palette for emphasised shapes.
|
||||
string ACC_S = "#3a7dff"; string ACC_F = "#d6e4ff";
|
||||
string ART_S = "#c0392b"; string ART_F = "#fdeaea";
|
||||
string ATL_S = "#1a7f45"; string ATL_F = "#e6f5ec";
|
||||
string STA_S = "#2b5fd9"; string STA_F = "#e8effd";
|
||||
}
|
||||
|
||||
BEG_G {
|
||||
$G.bgcolor = BG;
|
||||
$G.fontname = FONT;
|
||||
$G.fontcolor = INK;
|
||||
|
||||
// Defaults for anything the per-node block does not reach.
|
||||
setDflt($G, "N", "fontname", FONT);
|
||||
setDflt($G, "E", "fontname", FONT);
|
||||
// Declare `class` so reading it on an untagged object is defined
|
||||
// rather than a warning — most nodes carry no class by design.
|
||||
setDflt($G, "G", "class", "");
|
||||
setDflt($G, "N", "class", "");
|
||||
setDflt($G, "E", "class", "");
|
||||
|
||||
// Clusters: a pale container, the way Lucid draws a grouping box.
|
||||
for (sg = fstsubg($G); sg; sg = nxtsubg(sg)) {
|
||||
if (index(sg.name, "cluster") == 0) {
|
||||
sg.fontname = FONT;
|
||||
sg.color = SOFT;
|
||||
sg.fontcolor = MUTED;
|
||||
sg.bgcolor = FILL_ALT;
|
||||
// A tagged container keeps its identity, the way a Lucid swimlane
|
||||
// is tinted by what it holds.
|
||||
if (sg.class == "artery") { sg.color = ART_S; sg.bgcolor = ART_F; }
|
||||
else if (sg.class == "atlas") { sg.color = ATL_S; sg.bgcolor = ATL_F; }
|
||||
else if (sg.class == "station") { sg.color = STA_S; sg.bgcolor = STA_F; }
|
||||
else if (sg.class == "accent") { sg.color = ACC_S; sg.bgcolor = ACC_F; }
|
||||
// Keep dashed where the source chose it; it reads as "logical
|
||||
// grouping" rather than "deployed boundary".
|
||||
if (index(sg.style, "dashed") < 0) { sg.style = "rounded"; }
|
||||
else { sg.style = "dashed,rounded"; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
N {
|
||||
// An invisible node is layout scaffolding. Filling it would draw it.
|
||||
if (index($.style, "invis") >= 0) { continue; }
|
||||
|
||||
$.fontname = FONT;
|
||||
$.fontcolor = INK;
|
||||
$.color = LINE;
|
||||
$.fillcolor = FILL;
|
||||
$.penwidth = 1;
|
||||
|
||||
if ($.class == "accent") { $.color = ACC_S; $.fillcolor = ACC_F; }
|
||||
else if ($.class == "artery") { $.color = ART_S; $.fillcolor = ART_F; }
|
||||
else if ($.class == "atlas") { $.color = ATL_S; $.fillcolor = ATL_F; }
|
||||
else if ($.class == "station") { $.color = STA_S; $.fillcolor = STA_F; }
|
||||
else if ($.class == "accent-text") { $.fontcolor = ACC_S; }
|
||||
else if ($.class == "ok") { $.color = ATL_S; $.fillcolor = ATL_F; }
|
||||
else if ($.class == "muted") { $.fillcolor = FILL_ALT; $.fontcolor = MUTED; }
|
||||
|
||||
// `record` ignores rounding, and plaintext has no box to round.
|
||||
if ($.shape == "record" || $.shape == "Mrecord" || $.shape == "plaintext") {
|
||||
$.style = "filled";
|
||||
} else if (index($.style, "dashed") >= 0) {
|
||||
$.style = "filled,rounded,dashed";
|
||||
} else {
|
||||
$.style = "filled,rounded";
|
||||
}
|
||||
}
|
||||
|
||||
E {
|
||||
$.fontname = FONT;
|
||||
$.fontcolor = MUTED;
|
||||
$.color = LINE;
|
||||
$.arrowsize = 0.7;
|
||||
|
||||
if ($.class == "accent") { $.color = ACC_S; $.fontcolor = ACC_S; }
|
||||
else if ($.class == "artery") { $.color = ART_S; $.fontcolor = ART_S; }
|
||||
else if ($.class == "atlas") { $.color = ATL_S; $.fontcolor = ATL_S; }
|
||||
else if ($.class == "station") { $.color = STA_S; $.fontcolor = STA_S; }
|
||||
}
|
||||
@@ -1,45 +1,39 @@
|
||||
digraph wrapping {
|
||||
bgcolor="#0a0a0a"
|
||||
rankdir=LR
|
||||
fontname="Helvetica"
|
||||
node [fontname="Helvetica" fontsize=11 style=filled color="#333" fontcolor="#e5e5e5" shape=box]
|
||||
edge [fontname="Helvetica" fontsize=9 fontcolor="#a3a3a3" color="#666"]
|
||||
node [fontname="Helvetica" fontsize=11 style=filled shape=box]
|
||||
edge [fontname="Helvetica" fontsize=9]
|
||||
|
||||
label="Sidebar Injection — How Wrapping Works"
|
||||
labelloc=t
|
||||
fontsize=14
|
||||
fontcolor="#d4a574"
|
||||
|
||||
browser [label="Browser" fillcolor="#1a1a1a" shape=oval]
|
||||
browser [label="Browser" shape=oval]
|
||||
|
||||
subgraph cluster_nginx {
|
||||
label="Nginx (reverse proxy)"
|
||||
style=dashed
|
||||
color="#d4a574"
|
||||
fontcolor="#d4a574"
|
||||
class="accent"
|
||||
|
||||
proxy [label="proxy_pass\n+\nsub_filter\ninjects sidebar" fillcolor="#1a1a1a" shape=component]
|
||||
proxy [label="proxy_pass\n+\nsub_filter\ninjects sidebar" shape=component]
|
||||
}
|
||||
|
||||
subgraph cluster_app {
|
||||
label="Managed App"
|
||||
style=dashed
|
||||
color="#333"
|
||||
fontcolor="#666"
|
||||
|
||||
frontend [label="Frontend\n(React/Next/Vue)" fillcolor="#1a1a1a"]
|
||||
backend [label="Backend API" fillcolor="#1a1a1a"]
|
||||
frontend [label="Frontend\n(React/Next/Vue)"]
|
||||
backend [label="Backend API"]
|
||||
}
|
||||
|
||||
subgraph cluster_spr {
|
||||
label="Soleprint"
|
||||
style=dashed
|
||||
color="#d4a574"
|
||||
fontcolor="#d4a574"
|
||||
class="accent"
|
||||
|
||||
sidebar_css [label="sidebar.css" fillcolor="#1a1a1a"]
|
||||
sidebar_js [label="sidebar.js" fillcolor="#1a1a1a"]
|
||||
hub [label="Hub API\n/api/sidebar/config" fillcolor="#1a1a1a"]
|
||||
sidebar_css [label="sidebar.css"]
|
||||
sidebar_js [label="sidebar.js"]
|
||||
hub [label="Hub API\n/api/sidebar/config"]
|
||||
}
|
||||
|
||||
browser -> proxy [label="myroom.spr.local.ar"]
|
||||
@@ -47,8 +41,8 @@ digraph wrapping {
|
||||
proxy -> hub [label="/spr/ → soleprint"]
|
||||
|
||||
// The injection
|
||||
proxy -> sidebar_css [label="injects into </head>" color="#d4a574" style=dashed]
|
||||
proxy -> sidebar_js [color="#d4a574" style=dashed]
|
||||
proxy -> sidebar_css [class="accent" label="injects into </head>" style=dashed]
|
||||
proxy -> sidebar_js [class="accent" style=dashed]
|
||||
|
||||
sidebar_js -> hub [label="loads config" color="#d4a574"]
|
||||
sidebar_js -> hub [class="accent" label="loads config"]
|
||||
}
|
||||
|
||||
119
docs/graphs/wrapping.lucid.svg
Normal file
@@ -0,0 +1,119 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 14.1.2 (0)
|
||||
-->
|
||||
<!-- Title: wrapping Pages: 1 -->
|
||||
<svg width="742pt" height="325pt"
|
||||
viewBox="0.00 0.00 742.00 325.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 321.19)">
|
||||
<title>wrapping</title>
|
||||
<polygon fill="#ffffff" stroke="none" points="-4,4 -4,-321.19 738.19,-321.19 738.19,4 -4,4"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="367.1" y="-299.89" font-family="Arial" font-size="14.00" fill="#1f2933">Sidebar Injection — How Wrapping Works</text>
|
||||
<g id="clust1" class="cluster accent">
|
||||
<title>cluster_nginx</title>
|
||||
<path fill="#d6e4ff" stroke="#3a7dff" stroke-dasharray="5,2" d="M189.94,-24.44C189.94,-24.44 310.94,-24.44 310.94,-24.44 316.94,-24.44 322.94,-30.44 322.94,-36.44 322.94,-36.44 322.94,-114.44 322.94,-114.44 322.94,-120.44 316.94,-126.44 310.94,-126.44 310.94,-126.44 189.94,-126.44 189.94,-126.44 183.94,-126.44 177.94,-120.44 177.94,-114.44 177.94,-114.44 177.94,-36.44 177.94,-36.44 177.94,-30.44 183.94,-24.44 189.94,-24.44"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="250.44" y="-109.14" font-family="Arial" font-size="14.00" fill="#616e7c">Nginx (reverse proxy)</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_app</title>
|
||||
<path fill="#f5f7fa" stroke="#cbd2d9" stroke-dasharray="5,2" d="M434.44,-155.44C434.44,-155.44 525.69,-155.44 525.69,-155.44 531.69,-155.44 537.69,-161.44 537.69,-167.44 537.69,-167.44 537.69,-273.44 537.69,-273.44 537.69,-279.44 531.69,-285.44 525.69,-285.44 525.69,-285.44 434.44,-285.44 434.44,-285.44 428.44,-285.44 422.44,-279.44 422.44,-273.44 422.44,-273.44 422.44,-167.44 422.44,-167.44 422.44,-161.44 428.44,-155.44 434.44,-155.44"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="480.07" y="-268.14" font-family="Arial" font-size="14.00" fill="#616e7c">Managed App</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster accent">
|
||||
<title>cluster_spr</title>
|
||||
<path fill="#d6e4ff" stroke="#3a7dff" stroke-dasharray="5,2" d="M449.44,-17.44C449.44,-17.44 714.19,-17.44 714.19,-17.44 720.19,-17.44 726.19,-23.44 726.19,-29.44 726.19,-29.44 726.19,-135.44 726.19,-135.44 726.19,-141.44 720.19,-147.44 714.19,-147.44 714.19,-147.44 449.44,-147.44 449.44,-147.44 443.44,-147.44 437.44,-141.44 437.44,-135.44 437.44,-135.44 437.44,-29.44 437.44,-29.44 437.44,-23.44 443.44,-17.44 449.44,-17.44"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="581.82" y="-130.14" font-family="Arial" font-size="14.00" fill="#616e7c">Soleprint</text>
|
||||
</g>
|
||||
<!-- proxy -->
|
||||
<g id="node1" class="node">
|
||||
<title>proxy</title>
|
||||
<polygon fill="#ffffff" stroke="#9aa5b1" points="292.07,-94.44 207.82,-94.44 207.82,-90.44 203.82,-90.44 203.82,-86.44 207.82,-86.44 207.82,-40.44 203.82,-40.44 203.82,-36.44 207.82,-36.44 207.82,-32.44 292.07,-32.44 292.07,-94.44"/>
|
||||
<polyline fill="none" stroke="#9aa5b1" points="207.82,-90.44 211.82,-90.44 211.82,-86.44 207.82,-86.44"/>
|
||||
<polyline fill="none" stroke="#9aa5b1" points="207.82,-40.44 211.82,-40.44 211.82,-36.44 207.82,-36.44"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="249.94" y="-79.99" font-family="Arial" font-size="11.00" fill="#1f2933">proxy_pass</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="249.94" y="-66.49" font-family="Arial" font-size="11.00" fill="#1f2933">+</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="249.94" y="-52.99" font-family="Arial" font-size="11.00" fill="#1f2933">sub_filter</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="249.94" y="-39.49" font-family="Arial" font-size="11.00" fill="#1f2933">injects sidebar</text>
|
||||
</g>
|
||||
<!-- frontend -->
|
||||
<g id="node2" class="node">
|
||||
<title>frontend</title>
|
||||
<path fill="#ffffff" stroke="#9aa5b1" d="M517.69,-199.44C517.69,-199.44 442.44,-199.44 442.44,-199.44 436.44,-199.44 430.44,-193.44 430.44,-187.44 430.44,-187.44 430.44,-175.44 430.44,-175.44 430.44,-169.44 436.44,-163.44 442.44,-163.44 442.44,-163.44 517.69,-163.44 517.69,-163.44 523.69,-163.44 529.69,-169.44 529.69,-175.44 529.69,-175.44 529.69,-187.44 529.69,-187.44 529.69,-193.44 523.69,-199.44 517.69,-199.44"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="480.07" y="-184.49" font-family="Arial" font-size="11.00" fill="#1f2933">Frontend</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="480.07" y="-170.99" font-family="Arial" font-size="11.00" fill="#1f2933">(React/Next/Vue)</text>
|
||||
</g>
|
||||
<!-- proxy->frontend -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>proxy->frontend</title>
|
||||
<path fill="none" stroke="#9aa5b1" d="M292.52,-84.95C333.17,-105.98 394.79,-137.85 435.96,-159.15"/>
|
||||
<polygon fill="#9aa5b1" stroke="#9aa5b1" points="434.71,-161.26 442.05,-162.3 436.96,-156.91 434.71,-161.26"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="372.69" y="-148.53" font-family="Arial" font-size="9.00" fill="#616e7c">/ → app</text>
|
||||
</g>
|
||||
<!-- sidebar_css -->
|
||||
<g id="node4" class="node">
|
||||
<title>sidebar_css</title>
|
||||
<path fill="#ffffff" stroke="#9aa5b1" d="M502.69,-115.44C502.69,-115.44 457.44,-115.44 457.44,-115.44 451.44,-115.44 445.44,-109.44 445.44,-103.44 445.44,-103.44 445.44,-91.44 445.44,-91.44 445.44,-85.44 451.44,-79.44 457.44,-79.44 457.44,-79.44 502.69,-79.44 502.69,-79.44 508.69,-79.44 514.69,-85.44 514.69,-91.44 514.69,-91.44 514.69,-103.44 514.69,-103.44 514.69,-109.44 508.69,-115.44 502.69,-115.44"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="480.07" y="-93.74" font-family="Arial" font-size="11.00" fill="#1f2933">sidebar.css</text>
|
||||
</g>
|
||||
<!-- proxy->sidebar_css -->
|
||||
<g id="edge3" class="edge accent">
|
||||
<title>proxy->sidebar_css</title>
|
||||
<path fill="none" stroke="#3a7dff" stroke-dasharray="5,2" d="M292.52,-69.64C333.52,-75.75 395.85,-85.04 437.01,-91.18"/>
|
||||
<polygon fill="#3a7dff" stroke="#3a7dff" points="436.42,-93.56 443.71,-92.17 437.15,-88.72 436.42,-93.56"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="372.69" y="-89.35" font-family="Arial" font-size="9.00" fill="#3a7dff">injects into </head></text>
|
||||
</g>
|
||||
<!-- sidebar_js -->
|
||||
<g id="node5" class="node">
|
||||
<title>sidebar_js</title>
|
||||
<path fill="#ffffff" stroke="#9aa5b1" d="M498.57,-61.44C498.57,-61.44 461.57,-61.44 461.57,-61.44 455.57,-61.44 449.57,-55.44 449.57,-49.44 449.57,-49.44 449.57,-37.44 449.57,-37.44 449.57,-31.44 455.57,-25.44 461.57,-25.44 461.57,-25.44 498.57,-25.44 498.57,-25.44 504.57,-25.44 510.57,-31.44 510.57,-37.44 510.57,-37.44 510.57,-49.44 510.57,-49.44 510.57,-55.44 504.57,-61.44 498.57,-61.44"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="480.07" y="-39.74" font-family="Arial" font-size="11.00" fill="#1f2933">sidebar.js</text>
|
||||
</g>
|
||||
<!-- proxy->sidebar_js -->
|
||||
<g id="edge4" class="edge accent">
|
||||
<title>proxy->sidebar_js</title>
|
||||
<path fill="none" stroke="#3a7dff" stroke-dasharray="5,2" d="M292.52,-59.8C334.87,-56.08 399.99,-50.38 441.02,-46.78"/>
|
||||
<polygon fill="#3a7dff" stroke="#3a7dff" points="441.13,-49.23 447.89,-46.18 440.7,-44.35 441.13,-49.23"/>
|
||||
</g>
|
||||
<!-- hub -->
|
||||
<g id="node6" class="node">
|
||||
<title>hub</title>
|
||||
<path fill="#ffffff" stroke="#9aa5b1" d="M706.19,-61.44C706.19,-61.44 627.19,-61.44 627.19,-61.44 621.19,-61.44 615.19,-55.44 615.19,-49.44 615.19,-49.44 615.19,-37.44 615.19,-37.44 615.19,-31.44 621.19,-25.44 627.19,-25.44 627.19,-25.44 706.19,-25.44 706.19,-25.44 712.19,-25.44 718.19,-31.44 718.19,-37.44 718.19,-37.44 718.19,-49.44 718.19,-49.44 718.19,-55.44 712.19,-61.44 706.19,-61.44"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="666.69" y="-46.49" font-family="Arial" font-size="11.00" fill="#1f2933">Hub API</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="666.69" y="-32.99" font-family="Arial" font-size="11.00" fill="#1f2933">/api/sidebar/config</text>
|
||||
</g>
|
||||
<!-- proxy->hub -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>proxy->hub</title>
|
||||
<path fill="none" stroke="#9aa5b1" d="M292.27,-45.37C326.42,-31.48 376.57,-13.43 422.44,-5.94 473,2.31 486.99,1.33 537.69,-5.94 560.76,-9.25 585.47,-15.76 606.97,-22.43"/>
|
||||
<polygon fill="#9aa5b1" stroke="#9aa5b1" points="606.19,-24.75 613.6,-24.53 607.67,-20.08 606.19,-24.75"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="480.07" y="-7.89" font-family="Arial" font-size="9.00" fill="#616e7c">/spr/ → soleprint</text>
|
||||
</g>
|
||||
<!-- backend -->
|
||||
<g id="node3" class="node">
|
||||
<title>backend</title>
|
||||
<path fill="#ffffff" stroke="#9aa5b1" d="M507.19,-253.44C507.19,-253.44 452.94,-253.44 452.94,-253.44 446.94,-253.44 440.94,-247.44 440.94,-241.44 440.94,-241.44 440.94,-229.44 440.94,-229.44 440.94,-223.44 446.94,-217.44 452.94,-217.44 452.94,-217.44 507.19,-217.44 507.19,-217.44 513.19,-217.44 519.19,-223.44 519.19,-229.44 519.19,-229.44 519.19,-241.44 519.19,-241.44 519.19,-247.44 513.19,-253.44 507.19,-253.44"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="480.07" y="-231.74" font-family="Arial" font-size="11.00" fill="#1f2933">Backend API</text>
|
||||
</g>
|
||||
<!-- sidebar_js->hub -->
|
||||
<g id="edge6" class="edge accent">
|
||||
<title>sidebar_js->hub</title>
|
||||
<path fill="none" stroke="#3a7dff" d="M510.73,-43.44C536.46,-43.44 574.49,-43.44 606.44,-43.44"/>
|
||||
<polygon fill="#3a7dff" stroke="#3a7dff" points="606.33,-45.89 613.33,-43.44 606.33,-40.99 606.33,-45.89"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="572.44" y="-45.39" font-family="Arial" font-size="9.00" fill="#3a7dff">loads config</text>
|
||||
</g>
|
||||
<!-- browser -->
|
||||
<g id="node7" class="node">
|
||||
<title>browser</title>
|
||||
<ellipse fill="#ffffff" stroke="#9aa5b1" cx="35.22" cy="-63.44" rx="35.22" ry="18"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="35.22" y="-59.74" font-family="Arial" font-size="11.00" fill="#1f2933">Browser</text>
|
||||
</g>
|
||||
<!-- browser->proxy -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>browser->proxy</title>
|
||||
<path fill="none" stroke="#9aa5b1" d="M70.87,-63.44C105.71,-63.44 159.77,-63.44 199.19,-63.44"/>
|
||||
<polygon fill="#9aa5b1" stroke="#9aa5b1" points="198.85,-65.89 205.85,-63.44 198.85,-60.99 198.85,-65.89"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="128.19" y="-65.39" font-family="Arial" font-size="9.00" fill="#616e7c">myroom.spr.local.ar</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
@@ -10,7 +10,7 @@
|
||||
<title>wrapping</title>
|
||||
<polygon fill="#0a0a0a" stroke="none" points="-4,4 -4,-325.41 819.55,-325.41 819.55,4 -4,4"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="407.78" y="-304.11" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d4a574">Sidebar Injection — How Wrapping Works</text>
|
||||
<g id="clust1" class="cluster">
|
||||
<g id="clust1" class="cluster accent">
|
||||
<title>cluster_nginx</title>
|
||||
<polygon fill="#0a0a0a" stroke="#d4a574" stroke-dasharray="5,2" points="193.3,-25.16 193.3,-128.16 360.05,-128.16 360.05,-25.16 193.3,-25.16"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="276.68" y="-110.86" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d4a574">Nginx (reverse proxy)</text>
|
||||
@@ -20,19 +20,13 @@
|
||||
<polygon fill="#0a0a0a" stroke="#333333" stroke-dasharray="5,2" points="473.05,-157.16 473.05,-288.16 599.55,-288.16 599.55,-157.16 473.05,-157.16"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="536.3" y="-270.86" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#666666">Managed App</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster">
|
||||
<g id="clust3" class="cluster accent">
|
||||
<title>cluster_spr</title>
|
||||
<polygon fill="#0a0a0a" stroke="#d4a574" stroke-dasharray="5,2" points="489.55,-18.16 489.55,-149.16 807.55,-149.16 807.55,-18.16 489.55,-18.16"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="648.55" y="-131.86" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d4a574">Soleprint</text>
|
||||
</g>
|
||||
<!-- browser -->
|
||||
<g id="node1" class="node">
|
||||
<title>browser</title>
|
||||
<ellipse fill="#1a1a1a" stroke="#333333" cx="38.03" cy="-64.16" rx="38.03" ry="18"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="38.03" y="-60.46" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">Browser</text>
|
||||
</g>
|
||||
<!-- proxy -->
|
||||
<g id="node2" class="node">
|
||||
<g id="node1" class="node">
|
||||
<title>proxy</title>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="324.3,-95.16 228.05,-95.16 228.05,-91.16 224.05,-91.16 224.05,-87.16 228.05,-87.16 228.05,-41.16 224.05,-41.16 224.05,-37.16 228.05,-37.16 228.05,-33.16 324.3,-33.16 324.3,-95.16"/>
|
||||
<polyline fill="none" stroke="#333333" points="228.05,-91.16 232.05,-91.16 232.05,-87.16 228.05,-87.16"/>
|
||||
@@ -42,15 +36,8 @@
|
||||
<text xml:space="preserve" text-anchor="middle" x="276.18" y="-53.71" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">sub_filter</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="276.18" y="-40.21" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">injects sidebar</text>
|
||||
</g>
|
||||
<!-- browser->proxy -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>browser->proxy</title>
|
||||
<path fill="none" stroke="#666666" d="M76.45,-64.16C114.05,-64.16 172.57,-64.16 216.38,-64.16"/>
|
||||
<polygon fill="#666666" stroke="#666666" points="216.24,-67.66 226.24,-64.16 216.24,-60.66 216.24,-67.66"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="138.68" y="-66.86" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#a3a3a3">myroom.spr.local.ar</text>
|
||||
</g>
|
||||
<!-- frontend -->
|
||||
<g id="node3" class="node">
|
||||
<g id="node2" class="node">
|
||||
<title>frontend</title>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="591.55,-201.16 481.05,-201.16 481.05,-165.16 591.55,-165.16 591.55,-201.16"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="536.3" y="-186.21" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">Frontend</text>
|
||||
@@ -64,56 +51,69 @@
|
||||
<text xml:space="preserve" text-anchor="middle" x="416.55" y="-150.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#a3a3a3">/ → app</text>
|
||||
</g>
|
||||
<!-- sidebar_css -->
|
||||
<g id="node5" class="node">
|
||||
<g id="node4" class="node">
|
||||
<title>sidebar_css</title>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="575.05,-116.16 497.55,-116.16 497.55,-80.16 575.05,-80.16 575.05,-116.16"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="536.3" y="-94.46" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">sidebar.css</text>
|
||||
</g>
|
||||
<!-- proxy->sidebar_css -->
|
||||
<g id="edge4" class="edge">
|
||||
<g id="edge3" class="edge accent">
|
||||
<title>proxy->sidebar_css</title>
|
||||
<path fill="none" stroke="#d4a574" stroke-dasharray="5,2" d="M324.52,-70.4C370.27,-76.43 439.3,-85.52 485.78,-91.64"/>
|
||||
<polygon fill="#d4a574" stroke="#d4a574" points="485.21,-95.09 495.58,-92.93 486.12,-88.15 485.21,-95.09"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="416.55" y="-90.82" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#a3a3a3">injects into </head></text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="416.55" y="-90.82" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#d4a574">injects into </head></text>
|
||||
</g>
|
||||
<!-- sidebar_js -->
|
||||
<g id="node6" class="node">
|
||||
<g id="node5" class="node">
|
||||
<title>sidebar_js</title>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="570.55,-62.16 502.05,-62.16 502.05,-26.16 570.55,-26.16 570.55,-62.16"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="536.3" y="-40.46" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">sidebar.js</text>
|
||||
</g>
|
||||
<!-- proxy->sidebar_js -->
|
||||
<g id="edge5" class="edge">
|
||||
<g id="edge4" class="edge accent">
|
||||
<title>proxy->sidebar_js</title>
|
||||
<path fill="none" stroke="#d4a574" stroke-dasharray="5,2" d="M324.52,-60.49C371.88,-56.82 444.19,-51.22 490.6,-47.63"/>
|
||||
<polygon fill="#d4a574" stroke="#d4a574" points="490.6,-51.14 500.3,-46.87 490.06,-44.16 490.6,-51.14"/>
|
||||
</g>
|
||||
<!-- hub -->
|
||||
<g id="node7" class="node">
|
||||
<g id="node6" class="node">
|
||||
<title>hub</title>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="799.55,-62.16 682.3,-62.16 682.3,-26.16 799.55,-26.16 799.55,-62.16"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="740.93" y="-47.21" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">Hub API</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="740.93" y="-33.71" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">/api/sidebar/config</text>
|
||||
</g>
|
||||
<!-- proxy->hub -->
|
||||
<g id="edge3" class="edge">
|
||||
<g id="edge5" class="edge">
|
||||
<title>proxy->hub</title>
|
||||
<path fill="none" stroke="#666666" d="M324.6,-45.85C363.74,-31.76 421.09,-13.48 473.05,-5.91 528.69,2.18 543.83,1.54 599.55,-5.91 624.52,-9.25 651.33,-15.79 674.76,-22.54"/>
|
||||
<polygon fill="#666666" stroke="#666666" points="673.59,-25.84 684.17,-25.31 675.57,-19.12 673.59,-25.84"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="536.3" y="-8.61" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#a3a3a3">/spr/ → soleprint</text>
|
||||
</g>
|
||||
<!-- backend -->
|
||||
<g id="node4" class="node">
|
||||
<g id="node3" class="node">
|
||||
<title>backend</title>
|
||||
<polygon fill="#1a1a1a" stroke="#333333" points="578.05,-255.16 494.55,-255.16 494.55,-219.16 578.05,-219.16 578.05,-255.16"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="536.3" y="-233.46" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">Backend API</text>
|
||||
</g>
|
||||
<!-- sidebar_js->hub -->
|
||||
<g id="edge6" class="edge">
|
||||
<g id="edge6" class="edge accent">
|
||||
<title>sidebar_js->hub</title>
|
||||
<path fill="none" stroke="#d4a574" d="M570.76,-44.16C597.83,-44.16 636.85,-44.16 670.6,-44.16"/>
|
||||
<polygon fill="#d4a574" stroke="#d4a574" points="670.3,-47.66 680.3,-44.16 670.3,-40.66 670.3,-47.66"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="636.93" y="-46.86" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#a3a3a3">loads config</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="636.93" y="-46.86" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#d4a574">loads config</text>
|
||||
</g>
|
||||
<!-- browser -->
|
||||
<g id="node7" class="node">
|
||||
<title>browser</title>
|
||||
<ellipse fill="#1a1a1a" stroke="#333333" cx="38.03" cy="-64.16" rx="38.03" ry="18"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="38.03" y="-60.46" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e5e5e5">Browser</text>
|
||||
</g>
|
||||
<!-- browser->proxy -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>browser->proxy</title>
|
||||
<path fill="none" stroke="#666666" d="M76.45,-64.16C114.05,-64.16 172.57,-64.16 216.38,-64.16"/>
|
||||
<polygon fill="#666666" stroke="#666666" points="216.24,-67.66 226.24,-64.16 216.24,-60.66 216.24,-67.66"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="138.68" y="-66.86" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#a3a3a3">myroom.spr.local.ar</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.1 KiB |
22
rig/.gitattributes
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
# Line endings are normalised to LF in the repository and on checkout, on every
|
||||
# platform. Without this, a checkout on Windows/WSL rewrites files to CRLF and
|
||||
# every one of them shows up as modified without anyone having touched it.
|
||||
#
|
||||
# For the scripts it is not cosmetic: a shell script with CRLF fails on Linux
|
||||
# with `bad interpreter: /usr/bin/env bash^M`, which reads as a broken installer
|
||||
# rather than a line-ending problem — the worst possible first impression on a
|
||||
# machine where nothing has been proven yet.
|
||||
* text=auto eol=lf
|
||||
|
||||
*.sh text eol=lf
|
||||
*.py text eol=lf
|
||||
*.env text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.yml text eol=lf
|
||||
|
||||
# Never touch binaries.
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.zip binary
|
||||
*.tar binary
|
||||
*.gz binary
|
||||
20
rig/.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# def/ — the "default" scratch bucket: always gitignored, never versioned
|
||||
def
|
||||
|
||||
# local env (commit the .env.example, never the .env)
|
||||
.env
|
||||
.env.local
|
||||
ctrl/.env
|
||||
|
||||
# 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.
|
||||
arch/*.dot
|
||||
ctrl/Tiltfile.gen
|
||||
|
||||
# binaries pulled by `make deps-bundle` for the air-gapped wizard 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.
|
||||
278
rig/BOOTSTRAP.md
Normal file
@@ -0,0 +1,278 @@
|
||||
# From a machine with nothing on it to a project you can work in
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
project takes, in `all/projects/templates/conventions.md` and the `broad`
|
||||
scaffold beside it. **ppl** owns everything after local, and is where this
|
||||
document stops.
|
||||
|
||||
Read it once before running anything. Three of the steps below need root and one
|
||||
needs a logout, so knowing about them in advance is cheaper than meeting them
|
||||
halfway through.
|
||||
|
||||
|
||||
## Docker, and the two sysctls Tilt depends on
|
||||
|
||||
rig installs a toolchain; it does not install Docker. That line is not modesty —
|
||||
Docker is a daemon, a group membership and usually a logout, and a script that
|
||||
did it would have to be trusted with root on a machine it knows nothing about.
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y docker.io && sudo usermod -aG docker "$USER"
|
||||
```
|
||||
|
||||
Then log out and back in, and check `docker info` answers. Until it does, nothing
|
||||
below works and everything below reports the same failure.
|
||||
|
||||
While you have root, raise the inotify limits:
|
||||
|
||||
```bash
|
||||
echo -e 'fs.inotify.max_user_watches=524288\nfs.inotify.max_user_instances=512' \
|
||||
| sudo tee /etc/sysctl.d/99-rig.conf
|
||||
sudo sysctl --system
|
||||
```
|
||||
|
||||
kind and Tilt both watch large trees, and WSL ships 8192 watches and 128
|
||||
instances — far too low. The failure mode is the reason this is here at step
|
||||
zero rather than mentioned later: Tilt does not error, it simply stops noticing
|
||||
that files changed, and you lose an afternoon to a hot reload that silently
|
||||
isn't.
|
||||
|
||||
|
||||
## Read the docs before installing anything
|
||||
|
||||
```bash
|
||||
cd spr/rig
|
||||
make docs
|
||||
```
|
||||
|
||||
`ctrl/docs.sh` runs a throwaway `nginx:alpine` over a read-only bind mount of
|
||||
`docs/` and prints the URL. That is deliberate: the docs are the instructions for
|
||||
building everything else, so they cannot live in the cluster and cannot need
|
||||
`python3 -m http.server` either — a minimal Debian has no python. What it has,
|
||||
by definition, is Docker.
|
||||
|
||||
The port is this environment's `HTTP_PORT + 4`. Nothing is installed and nothing
|
||||
persists; ctrl-c ends it.
|
||||
|
||||
|
||||
## Ask what is wrong with this machine
|
||||
|
||||
```bash
|
||||
make station
|
||||
cp ctrl/.env.example ctrl/.env
|
||||
```
|
||||
|
||||
`station.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.
|
||||
|
||||
Read the whole output, but the `ports` block is the one to read carefully. Every
|
||||
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
|
||||
where a machine-local override goes, and `ports.sh persist` expects it to exist.
|
||||
|
||||
|
||||
## Install the toolchain — through the container
|
||||
|
||||
This is the step where "nothing installed" stops being rhetorical.
|
||||
|
||||
`make deps` runs `ctrl/wizard.sh install` directly on the host, and the wizard
|
||||
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 —
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
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`.
|
||||
|
||||
None of the four arguments are guessable, so:
|
||||
|
||||
- **`/:/host:ro`** — the wizard 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.
|
||||
- **the docker socket** — how detection reaches the daemon it is reporting on,
|
||||
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
|
||||
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.
|
||||
|
||||
`dev` is kubectl, jq, kind and tilt. `core` is kubectl and jq alone — no cluster
|
||||
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
|
||||
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
|
||||
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
|
||||
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`
|
||||
pulls from a generic internal repo, which is usually the only thing a locked-down
|
||||
client allows.
|
||||
|
||||
From here on this machine has curl, so **`make deps` is the short form** for
|
||||
every later run and every later copy of this directory. The container path is
|
||||
the first-time path.
|
||||
|
||||
|
||||
## Prove the machine before blaming the project
|
||||
|
||||
```bash
|
||||
make setup
|
||||
make cluster up
|
||||
kubectl get nodes
|
||||
```
|
||||
|
||||
`make setup` re-runs every check as a group. It is idempotent and it deliberately
|
||||
does not abort on the first failure — a setup script that dies at step two hides
|
||||
the fact that steps four and five were also going to fail. Run now, it should be
|
||||
`ok` and `done` all the way down, and that is the point: it is the scoreboard,
|
||||
not the installer.
|
||||
|
||||
`make cluster up` builds the default `minimal` profile — one node, no addons,
|
||||
boots fast. You do not need it to develop anything, but you do want to know that
|
||||
kind, the kubeconfig context and the derived port block work *before* a new
|
||||
project has any problems of its own to confuse them with. `make cluster down`
|
||||
when you are done looking.
|
||||
|
||||
Before starting a second cluster, and it will not be long:
|
||||
|
||||
```bash
|
||||
make cluster list
|
||||
```
|
||||
|
||||
Available memory, per-cluster usage and each cluster's port block. On a 16 GiB
|
||||
box four single-node clusters are comfortable and six push into swap, so this is
|
||||
worth reading before rather than after. `make cluster free <names>` stops
|
||||
clusters without deleting them; `docker start` brings them back untouched.
|
||||
|
||||
|
||||
## Scaffold the project
|
||||
|
||||
The canonical layout is [`all/projects/templates/conventions.md`](../all/projects/templates/conventions.md).
|
||||
Read it — it is short, opinionated, and exists precisely so nobody
|
||||
reverse-engineers a layout from whichever repo they happened to open. What
|
||||
follows is only the mechanical part.
|
||||
|
||||
```bash
|
||||
SLUG=<slug> # short, lowercase, no separators
|
||||
cp -r ~/wdir/all/projects/templates/broad ~/wdir/"$SLUG"
|
||||
cd ~/wdir/"$SLUG"
|
||||
grep -rl '<slug>' ctrl | xargs sed -i "s/<slug>/$SLUG/g"
|
||||
cp ctrl/k8s/.env.example ctrl/k8s/.env
|
||||
git init && git add -A && git commit -m "scaffold $SLUG from broad"
|
||||
```
|
||||
|
||||
`<slug>` is the only placeholder and it lives only under `ctrl/` — cluster name,
|
||||
namespace, ConfigMap name, and the `NAME=` in `kind-up.sh` / `kind-down.sh`. One
|
||||
sed does all of it.
|
||||
|
||||
The slug is the folder name, lowercase and short — `mpr`, `unt`, `nvi`. The
|
||||
cluster takes that name and the context becomes `kind-<slug>`, derived by the
|
||||
scaffold's Makefile from the directory, so there is nothing to edit for either.
|
||||
|
||||
**Pick the Tilt port deliberately.** `ctrl/k8s/.env.example` ships a value that
|
||||
is already in use, so copying it unchanged puts two projects on one port:
|
||||
|
||||
```bash
|
||||
grep -h '^TILT_PORT=' ~/wdir/*/ctrl/k8s/.env 2>/dev/null | sort
|
||||
```
|
||||
|
||||
Choose a free one in `10300–10399` — the range ALL reserves in
|
||||
`projects/index.json` under `policy` — avoiding `10350`, which is Tilt's own
|
||||
default. Currently taken: `nvi` 10330, `unt` 10340, `mpr` 10360, `mlv` 10370,
|
||||
`eth` 10380, `lng` 10390. This is the Tilt *web UI* port, not a service port;
|
||||
each project owns its own service ports separately. The scaffold ships it blank
|
||||
on purpose, so there is nothing to collide with until you choose.
|
||||
|
||||
The scaffold's `ctrl/k8s/` is the same shape as every other project here, and it
|
||||
builds as shipped:
|
||||
|
||||
```
|
||||
kind-config.yaml one node; gateway NodePort 30080 -> hostPort 8080
|
||||
base/ namespace, configmap, app (Deployment + Service)
|
||||
overlays/dev/ promotes the app Service to NodePort 30080
|
||||
```
|
||||
|
||||
Check it before `kind` spends minutes on anything — this renders the whole tree
|
||||
without a cluster and catches a broken patch immediately:
|
||||
|
||||
```bash
|
||||
kubectl kustomize ctrl/k8s/overlays/dev
|
||||
```
|
||||
|
||||
The workload is an nginx placeholder so a fresh copy reaches something that
|
||||
answers; replace it. Keep `30080` in step between the overlay patch and
|
||||
`kind-config.yaml`'s `containerPort` — the hostPort is this project's to pick.
|
||||
Reachability is a plain kind port mapping: no ingress controller and no MetalLB.
|
||||
Caddy maps `<slug>.local.ar` onto the host port (`~/wdir/ppl/local/Caddyfile`),
|
||||
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
|
||||
tilt-up` runs `cd ctrl && tilt up`, and there is nothing to run until you write
|
||||
one. Copy it from a live project; `unt` and `nvi` are closest to the plain shape.
|
||||
|
||||
|
||||
## Run it
|
||||
|
||||
```bash
|
||||
make kind-up # idempotent create, then selects the context
|
||||
make tilt-up # context + your assigned port
|
||||
```
|
||||
|
||||
`tilt-up` passes `--context kind-<slug>` every time, which is the point of going
|
||||
through `make` at all: tilt cannot deploy into whichever cluster you last looked
|
||||
at.
|
||||
|
||||
`make tilt-down` and `make kind-down` close the loop, and `make kind-reset` is
|
||||
delete-and-recreate for when a cluster wedges.
|
||||
|
||||
|
||||
## Register it
|
||||
|
||||
The project exists; now it is findable. Add an entry to
|
||||
`~/wdir/all/projects/index.json` and write its `projects/<slug>.md` beside the
|
||||
others. Structured fields in the index, prose in the markdown.
|
||||
|
||||
Putting it on the CI server and deploying it is `ppl`'s half, and it starts at
|
||||
`~/wdir/ppl/ctrl/init-repo.sh` — gitea remote, then Woodpecker. That is a
|
||||
different document.
|
||||
123
rig/Makefile
Normal file
@@ -0,0 +1,123 @@
|
||||
# Thin control Makefile — one target per ctrl/ script, and the subcommand is an
|
||||
# argument rather than a second target: `make cluster down`, not `make cluster-down`.
|
||||
#
|
||||
# The logic lives in the scripts, never here. Each target maps to exactly one
|
||||
# bash file, and that file holds the variants:
|
||||
#
|
||||
# make cluster up -> ctrl/cluster.sh up
|
||||
# make newbox destroy -> ctrl/newbox.sh destroy
|
||||
#
|
||||
# Config layers, weakest first: ctrl/versions.env (pinned toolchain) <
|
||||
# ctrl/env.d/<profile>.env (cluster shape) < ctrl/.env (local, gitignored) <
|
||||
# the environment. So `make cluster up PROFILE=client` beats everything.
|
||||
#
|
||||
# Start with: make setup (then: make cluster up && make docs)
|
||||
|
||||
# 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
|
||||
# it when you want a name that differs from the directory.
|
||||
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))
|
||||
KCTX := --context kind-$(CLUSTER)
|
||||
TILT_PORT := $(shell sed -n 's/^TILT_PORT=//p' ctrl/.env 2>/dev/null)
|
||||
WIZARD := $(SLUG)-wizard
|
||||
|
||||
# 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.
|
||||
ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
|
||||
ifneq ($(ARGS),)
|
||||
$(eval $(ARGS):;@:)
|
||||
# ...and as PHONY, because some of those words name real directories. `cfg`,
|
||||
# `ctrl`, `docs`, `gen` and `init` all exist at this level, and make considers a
|
||||
# target that is an existing directory already built — so `make build ctrl` ran
|
||||
# the build and then printed "make: 'ctrl' is up to date". The empty rule above
|
||||
# is not enough on its own; only .PHONY stops make consulting the filesystem.
|
||||
.PHONY: $(ARGS)
|
||||
endif
|
||||
|
||||
.PHONY: help setup station deps wizard cluster registry addons ports \
|
||||
newbox dockerhost docs tilt \
|
||||
kind-up kind-down kind-reset tilt-up tilt-down
|
||||
|
||||
help: ## list targets
|
||||
@grep -hE '^[a-z][a-z-]*:.*?##' $(MAKEFILE_LIST) | sed 's/:.*##/\t/' | expand -t16
|
||||
|
||||
# ── setup ──────────────────────────────────────────────────────────────────
|
||||
|
||||
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
|
||||
|
||||
deps: ## install the toolchain [core|dev] (default dev)
|
||||
bash ctrl/wizard.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) .
|
||||
|
||||
# ── cluster ────────────────────────────────────────────────────────────────
|
||||
|
||||
cluster: ## this env + the machine [up|down|reset|list|free]
|
||||
bash ctrl/cluster.sh $(or $(ARGS),up)
|
||||
|
||||
registry: ## registry wiring [up|down|status] (default status)
|
||||
bash ctrl/registry.sh $(or $(ARGS),status)
|
||||
|
||||
addons: ## profile addons [install|list] (default list)
|
||||
bash ctrl/addons.sh $(or $(ARGS),list)
|
||||
|
||||
ports: ## this environment's port block [show|persist]
|
||||
bash ctrl/ports.sh $(or $(ARGS),show)
|
||||
|
||||
# ── host ───────────────────────────────────────────────────────────────────
|
||||
|
||||
newbox: ## throwaway environment [create|status|shell|destroy]
|
||||
bash ctrl/newbox.sh $(or $(ARGS),status)
|
||||
|
||||
dockerhost: ## share Docker between distros [status|share|unshare]
|
||||
$(if $(filter share unshare,$(ARGS)),sudo ,)bash ctrl/dockerhost.sh $(or $(ARGS),status)
|
||||
|
||||
# ── docs + dev loop ────────────────────────────────────────────────────────
|
||||
|
||||
docs: ## documentation [serve|graphs] (default serve)
|
||||
bash ctrl/docs.sh $(or $(ARGS),serve)
|
||||
|
||||
# --port is only passed when TILT_PORT is actually set. It comes from ctrl/.env,
|
||||
# which does NOT carry it by default — ports are derived at runtime in
|
||||
# lib/config.sh unless `make ports persist` has written them. Without the guard
|
||||
# tilt receives a bare `--port` with no value and fails on the flag rather than
|
||||
# on anything real. `make ports show` prints the derived block.
|
||||
tilt: ## dev loop [up|down] (default up)
|
||||
cd ctrl && tilt $(or $(ARGS),up) $(KCTX) $(if $(filter down,$(ARGS)),,$(if $(TILT_PORT),--port $(TILT_PORT)))
|
||||
|
||||
# ── the shape every other project uses ─────────────────────────────────────
|
||||
# Aliases, not a second implementation: each one calls the same script the
|
||||
# canonical target does.
|
||||
#
|
||||
# The header above argues for `make cluster down` over `make cluster-down`, and
|
||||
# that still holds *within* this file. But rig is one repo among several on the
|
||||
# same machine, and every other one answers to kind-up / tilt-up. Muscle memory
|
||||
# spanning six projects beats internal tidiness in one, so both spellings work.
|
||||
#
|
||||
# `cluster list` and `cluster free` have no hyphenated twin on purpose — they
|
||||
# are rig's own, with nothing to be consistent with.
|
||||
|
||||
kind-up: ## alias for `cluster up`
|
||||
bash ctrl/cluster.sh up
|
||||
|
||||
kind-down: ## alias for `cluster down`
|
||||
bash ctrl/cluster.sh down
|
||||
|
||||
kind-reset: ## alias for `cluster reset`
|
||||
bash ctrl/cluster.sh reset
|
||||
|
||||
# These two match the other projects' spelling, but rig has no Tiltfile — there
|
||||
# is nothing to run yet, and they fail the same way `make tilt` does.
|
||||
tilt-up: ## alias for `tilt up` (rig has no Tiltfile yet)
|
||||
cd ctrl && tilt up $(KCTX) $(if $(TILT_PORT),--port $(TILT_PORT))
|
||||
|
||||
tilt-down: ## alias for `tilt down` (rig has no Tiltfile yet)
|
||||
cd ctrl && tilt down $(KCTX)
|
||||
117
rig/README.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# rig
|
||||
|
||||
A runnable local model of a large, regulated estate — legacy and new side by
|
||||
side. Its job is onboarding and exploration, not a production replica: most
|
||||
services are deliberately mocked, because what has to be faithful is the
|
||||
topology, not the workloads.
|
||||
|
||||
## Prerequisite
|
||||
|
||||
**Docker.** Nothing else — no curl, no jq, no python, no apt repositories.
|
||||
|
||||
## Read the docs first
|
||||
|
||||
```bash
|
||||
make docs # serves on localhost, prints the URL
|
||||
```
|
||||
|
||||
They run before anything is installed, which matters because they are the
|
||||
instructions for everything else. No cluster and no toolchain required.
|
||||
|
||||
## Then
|
||||
|
||||
```bash
|
||||
make station # 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 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
|
||||
a new project.
|
||||
|
||||
## One directory is one environment
|
||||
|
||||
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.
|
||||
|
||||
## Profiles
|
||||
|
||||
A profile is the shape of the cluster: how many nodes, which addons, whether the
|
||||
apiserver audits. They live in `ctrl/env.d/`, and the active one is `PROFILE`.
|
||||
|
||||
| Profile | For |
|
||||
| --- | --- |
|
||||
| `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. |
|
||||
|
||||
```bash
|
||||
PROFILE=data make cluster up
|
||||
PROFILE=data make addons install
|
||||
make addons # what the active profile wants, and what exists
|
||||
```
|
||||
|
||||
A profile names a **cluster shape** — a file in `ctrl/k8s/` — rather than
|
||||
restating node count and audit as variables:
|
||||
|
||||
| shape | nodes | audit | used by |
|
||||
| --- | --- | --- | --- |
|
||||
| `kind-config.yaml.tpl` | 1 | off | `minimal`, `data` |
|
||||
| `kind-config.audit.yaml.tpl` | 1 | on | `offline` |
|
||||
| `kind-config.client.yaml.tpl` | 3 | on | `client` |
|
||||
|
||||
Both numbers are read back out of the chosen file, so the YAML is the only place
|
||||
that decides and there is nothing to drift. The layout under `ctrl/k8s/` is the
|
||||
same as every other project here — a kind config, a kustomize `base/`, an
|
||||
`overlays/dev/` — see [`ctrl/k8s/README.md`](ctrl/k8s/README.md).
|
||||
|
||||
## Addons
|
||||
|
||||
Each addon is its own idempotent script in `ctrl/addons/`, and a profile names
|
||||
the ones it wants in `ADDONS`. Adding one is adding a file — there is no
|
||||
dispatcher to edit.
|
||||
|
||||
**There is no ingress controller, deliberately.** They pin a narrow window of
|
||||
Kubernetes versions, so depending on one would constrain which k8s a rig can be
|
||||
built with — and running a trailing-edge control plane to model a legacy estate
|
||||
is the whole point. Services are reached through MetalLB and
|
||||
`type: LoadBalancer`, which carries no such constraint and is also what a real
|
||||
cluster does.
|
||||
|
||||
| Addon | Does |
|
||||
| --- | --- |
|
||||
| `metallb` | gives `type: LoadBalancer` an address it can actually reach |
|
||||
| `cert-manager` | a local CA, so TLS works offline |
|
||||
| `metrics-server` | makes `kubectl top` work on kind |
|
||||
| `postgres` | database, in the `data` namespace |
|
||||
| `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
|
||||
`rig_addon` field pointing at `ctrl/addons/<name>.sh`.
|
||||
|
||||
Plain manifests rather than helm charts, like every other addon: a chart repo is
|
||||
a network dependency, and the `offline` profile exists precisely so there is a
|
||||
path with none. Images are pinned in `ctrl/versions.env` and can be preloaded.
|
||||
|
||||
Passwords are generated on first install and kept across re-runs, so re-running
|
||||
an addon never rotates a credential out from under something already connected:
|
||||
|
||||
```bash
|
||||
kubectl -n data get secret postgres -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d
|
||||
kubectl -n data port-forward svc/airflow 8080:8080
|
||||
```
|
||||
50
rig/ctrl/.env.example
Normal file
@@ -0,0 +1,50 @@
|
||||
# Machine-local config. Copy to ctrl/.env (gitignored) and edit.
|
||||
# Cluster SHAPE lives in ctrl/env.d/<profile>.env — not here.
|
||||
# The architecture MODEL lives in arch/<name>.json — not here either.
|
||||
|
||||
# Which profile in ctrl/env.d/ to build. minimal | client | offline
|
||||
PROFILE=minimal
|
||||
|
||||
# Cluster name; the kubectl context becomes kind-<CLUSTER>.
|
||||
# LEAVE THIS UNSET unless you need a name that differs from the directory —
|
||||
# it defaults to this folder's name, which is what makes the folder copyable:
|
||||
# copy it, rename it, and you get a separate environment with no edits.
|
||||
# CLUSTER=
|
||||
|
||||
# Host ports. LEAVE UNSET — they derive from the directory name so several
|
||||
# environments coexist without negotiating (see ctrl/ports.sh). `make ports`
|
||||
# shows this environment's block; `make ports persist` writes it here so it stops
|
||||
# being derived and becomes fixed. Set a value only to override.
|
||||
# HTTP_PORT=
|
||||
# HTTPS_PORT=
|
||||
# TILT_PORT=
|
||||
# REGISTRY_PORT=
|
||||
|
||||
# Where the application manifests live. The real ones are expected to be
|
||||
# versioned separately from this installer — they change on a different cadence,
|
||||
# by different people. Repoint this at their repo and rig stops owning them:
|
||||
# MANIFESTS_DIR=../platform-manifests/overlays/dev
|
||||
MANIFESTS_DIR=ctrl/k8s/overlays/dev
|
||||
|
||||
# Where the wizard 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
|
||||
DEPS_SOURCE=upstream
|
||||
DEPS_ARTIFACTORY_URL=
|
||||
|
||||
# --- Registry -------------------------------------------------------------
|
||||
# Mode comes from the profile (REGISTRY_MODE). These are the secrets it needs.
|
||||
# Required for mirror/remote:
|
||||
REGISTRY_REMOTE_URL=
|
||||
REGISTRY_USER=
|
||||
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.
|
||||
# Symptom when missing: x509: certificate signed by unknown authority
|
||||
REGISTRY_CA_FILE=
|
||||
|
||||
# (The local registry's host port is part of the derived block above.)
|
||||
46
rig/ctrl/Dockerfile.wizard
Normal file
@@ -0,0 +1,46 @@
|
||||
# The installation wizard. 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.
|
||||
#
|
||||
# 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 .
|
||||
#
|
||||
# wizard-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
|
||||
|
||||
# ca-certificates + curl: fetch and verify. graphviz + python3: render diagrams
|
||||
# and validate the arch model, so the host never needs an apt package.
|
||||
#
|
||||
# docker-cli, NOT docker.io: we only ever talk to the host's daemon through the
|
||||
# mounted socket, and under --no-install-recommends the docker.io package ships
|
||||
# docker-init without the actual `docker` binary.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl jq graphviz python3 docker-cli \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
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
|
||||
|
||||
# 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"]
|
||||
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
|
||||
ENV DEPS_SOURCE=baked \
|
||||
BAKED_BIN=/opt/rig/bin
|
||||
39
rig/ctrl/addons.sh
Executable file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install the addons the active profile asked for, in the order listed.
|
||||
# Each addon is its own idempotent script in ctrl/addons/ — adding one is adding
|
||||
# a file, not editing a dispatcher.
|
||||
#
|
||||
# Usage: addons.sh install | list
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
|
||||
install() {
|
||||
if [ -z "${ADDONS// /}" ]; then
|
||||
echo "no addons in profile '$PROFILE_NAME'"
|
||||
return
|
||||
fi
|
||||
local a
|
||||
for a in $ADDONS; do
|
||||
if [ ! -f "addons/${a}.sh" ]; then
|
||||
echo "no such addon: addons/${a}.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "addon: $a"
|
||||
bash "addons/${a}.sh"
|
||||
done
|
||||
}
|
||||
|
||||
list() {
|
||||
echo "profile '$PROFILE_NAME' wants: ${ADDONS:-none}"
|
||||
echo "available:"
|
||||
ls addons/*.sh 2>/dev/null | xargs -n1 basename | sed 's/\.sh$//' | sed 's/^/ /'
|
||||
}
|
||||
|
||||
case "${1:-list}" in
|
||||
install) install ;;
|
||||
list) list ;;
|
||||
*) echo "usage: $0 [install|list]" >&2; exit 1 ;;
|
||||
esac
|
||||
115
rig/ctrl/addons/airflow.sh
Executable file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env bash
|
||||
# Apache Airflow — the cluster half of soleprint's 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
|
||||
# (postgres missing from ADDONS) stays invisible in the logs.
|
||||
#
|
||||
# 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.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
|
||||
K="kubectl --context ${KUBECONTEXT}"
|
||||
NS="${DATA_NAMESPACE:-data}"
|
||||
|
||||
if ! $K get deployment -n "$NS" postgres >/dev/null 2>&1; then
|
||||
echo " ! airflow needs the postgres addon, and it is not installed" >&2
|
||||
echo " add it before airflow in the profile's ADDONS:" >&2
|
||||
echo " ADDONS=\"... postgres airflow\"" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Reuse the credential postgres generated rather than storing a second copy.
|
||||
db_user=$($K get secret -n "$NS" postgres -o jsonpath='{.data.POSTGRES_USER}' | base64 -d)
|
||||
db_pass=$($K get secret -n "$NS" postgres -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d)
|
||||
db_name=$($K get secret -n "$NS" postgres -o jsonpath='{.data.POSTGRES_DB}' | base64 -d)
|
||||
|
||||
if $K get secret -n "$NS" airflow >/dev/null 2>&1; then
|
||||
echo " secret exists, keeping the current admin password and fernet key"
|
||||
else
|
||||
admin_password=$(head -c 18 /dev/urandom | base64 | tr -d '/+=' | head -c 24)
|
||||
# Airflow requires a 32-byte urlsafe-base64 key; without a fixed one every
|
||||
# restart invalidates every stored connection.
|
||||
fernet_key=$(head -c 32 /dev/urandom | base64 | tr '+/' '-_')
|
||||
$K create secret generic airflow -n "$NS" \
|
||||
--from-literal=ADMIN_USER="${AIRFLOW_ADMIN_USER:-admin}" \
|
||||
--from-literal=ADMIN_PASSWORD="$admin_password" \
|
||||
--from-literal=FERNET_KEY="$fernet_key" \
|
||||
--from-literal=SQL_ALCHEMY_CONN="postgresql+psycopg2://${db_user}:${db_pass}@postgres:5432/${db_name}" \
|
||||
>/dev/null
|
||||
echo " generated an admin password (read it back with the command below)"
|
||||
fi
|
||||
|
||||
echo " applying manifests"
|
||||
$K apply -n "$NS" -f - >/dev/null <<YAML
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: airflow
|
||||
spec:
|
||||
selector:
|
||||
app: airflow
|
||||
ports:
|
||||
- port: 8080
|
||||
targetPort: 8080
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: airflow
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: airflow
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: airflow
|
||||
spec:
|
||||
containers:
|
||||
- name: airflow
|
||||
image: ${AIRFLOW_IMAGE}
|
||||
args: ["standalone"]
|
||||
env:
|
||||
- name: AIRFLOW__CORE__EXECUTOR
|
||||
value: LocalExecutor
|
||||
- name: AIRFLOW__CORE__LOAD_EXAMPLES
|
||||
value: "false"
|
||||
- name: AIRFLOW__DATABASE__SQL_ALCHEMY_CONN
|
||||
valueFrom:
|
||||
secretKeyRef: {name: airflow, key: SQL_ALCHEMY_CONN}
|
||||
- name: AIRFLOW__CORE__FERNET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef: {name: airflow, key: FERNET_KEY}
|
||||
- name: _AIRFLOW_WWW_USER_USERNAME
|
||||
valueFrom:
|
||||
secretKeyRef: {name: airflow, key: ADMIN_USER}
|
||||
- name: _AIRFLOW_WWW_USER_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef: {name: airflow, key: ADMIN_PASSWORD}
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
# First boot runs the whole migration before it serves anything.
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 15
|
||||
failureThreshold: 20
|
||||
YAML
|
||||
|
||||
echo " waiting for airflow (the first boot migrates the database, so this is slow)..."
|
||||
$K rollout status deployment/airflow -n "$NS" --timeout=600s
|
||||
|
||||
echo " in-cluster: http://airflow.${NS}.svc.cluster.local:8080"
|
||||
echo " reach it: kubectl --context ${KUBECONTEXT} -n ${NS} port-forward svc/airflow 8080:8080"
|
||||
echo " password: kubectl --context ${KUBECONTEXT} -n ${NS} get secret airflow -o jsonpath='{.data.ADMIN_PASSWORD}' | base64 -d"
|
||||
65
rig/ctrl/addons/cert-manager.sh
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
# cert-manager plus a self-signed cluster issuer.
|
||||
#
|
||||
# In a regulated estate almost everything is TLS, so the interesting question
|
||||
# during onboarding is "does this service present a cert my client trusts" — not
|
||||
# "can I reach a public ACME server". A local CA answers that offline, which is
|
||||
# also what makes the air-gapped profile usable.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
|
||||
K="kubectl --context ${KUBECONTEXT}"
|
||||
|
||||
if $K get deployment -n cert-manager cert-manager >/dev/null 2>&1; then
|
||||
echo " already installed"
|
||||
else
|
||||
$K apply -f "https://github.com/cert-manager/cert-manager/releases/download/${CERT_MANAGER_VERSION}/cert-manager.yaml"
|
||||
fi
|
||||
|
||||
echo " waiting for cert-manager..."
|
||||
$K wait --namespace cert-manager \
|
||||
--for=condition=ready pod --selector=app.kubernetes.io/instance=cert-manager \
|
||||
--timeout=240s
|
||||
|
||||
# A self-signed root, then a CA issuer chained off it. Workloads reference
|
||||
# ClusterIssuer/local-ca and get a cert from a CA you can actually distribute.
|
||||
echo " creating local CA issuer"
|
||||
$K apply -f - <<'YAML' >/dev/null
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: selfsigned-root
|
||||
spec:
|
||||
selfSigned: {}
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: local-ca
|
||||
namespace: cert-manager
|
||||
spec:
|
||||
isCA: true
|
||||
commonName: rig-local-ca
|
||||
secretName: local-ca-key-pair
|
||||
duration: 87600h
|
||||
privateKey:
|
||||
algorithm: ECDSA
|
||||
size: 256
|
||||
issuerRef:
|
||||
name: selfsigned-root
|
||||
kind: ClusterIssuer
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: local-ca
|
||||
spec:
|
||||
ca:
|
||||
secretName: local-ca-key-pair
|
||||
YAML
|
||||
|
||||
echo " export the CA for your browser/client with:"
|
||||
echo " kubectl --context ${KUBECONTEXT} -n cert-manager get secret local-ca-key-pair -o jsonpath='{.data.tls\\.crt}' | base64 -d"
|
||||
103
rig/ctrl/addons/metallb.sh
Executable file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env bash
|
||||
# MetalLB — makes `Service type: LoadBalancer` actually get an address.
|
||||
#
|
||||
# Why it matters here: real manifests use LoadBalancer, because a real cluster
|
||||
# has one. On a bare kind cluster those Services sit at EXTERNAL-IP <pending>
|
||||
# forever with no error anywhere — the deployment looks fine and simply is not
|
||||
# reachable. Without this, every such Service has to be edited to NodePort,
|
||||
# which means the local manifests stop matching the ones being modelled.
|
||||
#
|
||||
# The address pool is derived from the kind Docker network at install time, not
|
||||
# hardcoded: Docker picks that subnet, it differs between machines, and a pool
|
||||
# outside it is silently unroutable.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
|
||||
K="kubectl --context ${KUBECONTEXT}"
|
||||
|
||||
# ── work out an address range ──────────────────────────────────────────────
|
||||
# kind hands node addresses out from the bottom of the subnet, so the top is
|
||||
# free. Taking a slice there avoids collisions with current and future nodes.
|
||||
subnet=$(docker network inspect kind \
|
||||
-f '{{range .IPAM.Config}}{{.Subnet}} {{end}}' 2>/dev/null \
|
||||
| tr ' ' '\n' | grep -E '^[0-9]+\.' | head -1)
|
||||
|
||||
if [ -z "$subnet" ]; then
|
||||
echo " ! could not read the kind Docker network subnet" >&2
|
||||
echo " (is the cluster up? MetalLB needs the network to exist first)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
base="${subnet%/*}"; prefix="${subnet#*/}"
|
||||
o1=$(echo "$base" | cut -d. -f1); o2=$(echo "$base" | cut -d. -f2)
|
||||
o3=$(echo "$base" | cut -d. -f3)
|
||||
|
||||
case "$prefix" in
|
||||
16) pool_start="${o1}.${o2}.255.200"; pool_end="${o1}.${o2}.255.250" ;;
|
||||
24) pool_start="${o1}.${o2}.${o3}.200"; pool_end="${o1}.${o2}.${o3}.250" ;;
|
||||
*)
|
||||
# Guessing a range inside an unexpected prefix risks handing out
|
||||
# addresses that belong to something else. Say so instead.
|
||||
echo " ! kind network is $subnet — only /16 and /24 are handled" >&2
|
||||
echo " set the pool by hand in ctrl/addons/metallb.sh" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo " kind network $subnet → pool ${pool_start}-${pool_end}"
|
||||
|
||||
# ── install ────────────────────────────────────────────────────────────────
|
||||
|
||||
if $K get deployment -n metallb-system controller >/dev/null 2>&1; then
|
||||
echo " already installed"
|
||||
else
|
||||
$K apply -f "https://raw.githubusercontent.com/metallb/metallb/${METALLB_VERSION}/config/manifests/metallb-native.yaml"
|
||||
fi
|
||||
|
||||
# `kubectl wait` on a selector errors out immediately when nothing matches yet,
|
||||
# and right after apply the ReplicaSet has not created the pod — so it loses a
|
||||
# race it looks like it should win. `rollout status` waits for the Deployment
|
||||
# itself and handles the not-yet-created case.
|
||||
echo " waiting for the controller..."
|
||||
$K rollout status deployment/controller -n metallb-system --timeout=240s
|
||||
$K rollout status daemonset/speaker -n metallb-system --timeout=240s
|
||||
|
||||
# The webhook rejects IPAddressPools until it is actually serving, and it comes
|
||||
# up a moment after the pod is Ready — so retry rather than fail the whole run
|
||||
# on a race that resolves itself in seconds.
|
||||
echo " configuring the address pool"
|
||||
for attempt in 1 2 3 4 5 6 7 8 9 10; do
|
||||
if $K apply -f - >/dev/null 2>&1 <<YAML
|
||||
apiVersion: metallb.io/v1beta1
|
||||
kind: IPAddressPool
|
||||
metadata:
|
||||
name: default
|
||||
namespace: metallb-system
|
||||
spec:
|
||||
addresses:
|
||||
- ${pool_start}-${pool_end}
|
||||
---
|
||||
# Layer 2 mode: one node answers ARP for each address. No BGP peer needed, which
|
||||
# is what makes this work on a laptop.
|
||||
apiVersion: metallb.io/v1beta1
|
||||
kind: L2Advertisement
|
||||
metadata:
|
||||
name: default
|
||||
namespace: metallb-system
|
||||
spec:
|
||||
ipAddressPools:
|
||||
- default
|
||||
YAML
|
||||
then
|
||||
echo " pool ready: ${pool_start}-${pool_end}"
|
||||
exit 0
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
|
||||
echo " ! the pool was rejected after 10 attempts — is the webhook up?" >&2
|
||||
$K get pods -n metallb-system >&2
|
||||
exit 1
|
||||
25
rig/ctrl/addons/metrics-server.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# metrics-server — makes `kubectl top` work.
|
||||
#
|
||||
# kind nodes serve kubelet metrics over a self-signed cert, so the standard
|
||||
# manifest never becomes ready without --kubelet-insecure-tls. That is fine here
|
||||
# (it is a local cluster) and is the single most common reason metrics-server
|
||||
# sits at 0/1 on kind.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
|
||||
K="kubectl --context ${KUBECONTEXT}"
|
||||
|
||||
if ! $K get deployment -n kube-system metrics-server >/dev/null 2>&1; then
|
||||
$K apply -f "https://github.com/kubernetes-sigs/metrics-server/releases/download/${METRICS_SERVER_VERSION}/components.yaml"
|
||||
fi
|
||||
|
||||
$K patch deployment metrics-server -n kube-system --type=json \
|
||||
-p '[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]' \
|
||||
>/dev/null 2>&1 || true
|
||||
|
||||
echo " waiting for metrics-server..."
|
||||
$K rollout status deployment/metrics-server -n kube-system --timeout=180s
|
||||
118
rig/ctrl/addons/postgres.sh
Executable file
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env bash
|
||||
# PostgreSQL — the cluster half of soleprint's 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
|
||||
# twice.
|
||||
#
|
||||
# Plain manifests rather than a helm chart, matching the other addons: a chart
|
||||
# repo is a network dependency, and the offline profile exists precisely so
|
||||
# there is a path with none. The image is pinned in ctrl/versions.env and can be
|
||||
# preloaded into a local registry like every other image here.
|
||||
#
|
||||
# One replica on a PVC. This models a dependency for local work, not a
|
||||
# highly-available database, and pretending otherwise on a kind node would be a
|
||||
# more elaborate lie rather than a more useful one.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
|
||||
K="kubectl --context ${KUBECONTEXT}"
|
||||
NS="${DATA_NAMESPACE:-data}"
|
||||
|
||||
$K get namespace "$NS" >/dev/null 2>&1 || $K create namespace "$NS"
|
||||
|
||||
# The password is generated once and then left alone, so re-running this does
|
||||
# not rotate the credential out from under whatever is already connected.
|
||||
if $K get secret -n "$NS" postgres >/dev/null 2>&1; then
|
||||
echo " secret exists, keeping the current password"
|
||||
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_PASSWORD="$password" >/dev/null
|
||||
echo " generated a password (read it back with the command printed below)"
|
||||
fi
|
||||
|
||||
echo " applying manifests"
|
||||
$K apply -n "$NS" -f - >/dev/null <<YAML
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: postgres-data
|
||||
spec:
|
||||
accessModes: [ReadWriteOnce]
|
||||
resources:
|
||||
requests:
|
||||
storage: ${POSTGRES_STORAGE:-2Gi}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres
|
||||
spec:
|
||||
selector:
|
||||
app: postgres
|
||||
ports:
|
||||
- port: 5432
|
||||
targetPort: 5432
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: postgres
|
||||
spec:
|
||||
replicas: 1
|
||||
# One volume, one writer. Rolling would start a second pod against the same
|
||||
# PVC before the first exits, and Postgres refuses to share a data directory.
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: ${POSTGRES_IMAGE}
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: postgres
|
||||
env:
|
||||
# The image initialises into the volume root otherwise, and a
|
||||
# lost+found from the PVC makes it refuse to initdb.
|
||||
- name: PGDATA
|
||||
value: /var/lib/postgresql/data/pgdata
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["sh", "-c", "pg_isready -U \$POSTGRES_USER -d \$POSTGRES_DB"]
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: ["sh", "-c", "pg_isready -U \$POSTGRES_USER -d \$POSTGRES_DB"]
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 15
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: postgres-data
|
||||
YAML
|
||||
|
||||
echo " waiting for postgres..."
|
||||
$K rollout status deployment/postgres -n "$NS" --timeout=240s
|
||||
|
||||
echo " in-cluster: postgres.${NS}.svc.cluster.local:5432"
|
||||
echo " password: kubectl --context ${KUBECONTEXT} -n ${NS} get secret postgres -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d"
|
||||
60
rig/ctrl/addons/redis.sh
Executable file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
# Redis — the cluster half of soleprint's 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
|
||||
# nothing but a volume to clean up.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
|
||||
K="kubectl --context ${KUBECONTEXT}"
|
||||
NS="${DATA_NAMESPACE:-data}"
|
||||
|
||||
$K get namespace "$NS" >/dev/null 2>&1 || $K create namespace "$NS"
|
||||
|
||||
echo " applying manifests"
|
||||
$K apply -n "$NS" -f - >/dev/null <<YAML
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis
|
||||
spec:
|
||||
selector:
|
||||
app: redis
|
||||
ports:
|
||||
- port: 6379
|
||||
targetPort: 6379
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: ${REDIS_IMAGE}
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["redis-cli", "ping"]
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 5
|
||||
YAML
|
||||
|
||||
echo " waiting for redis..."
|
||||
$K rollout status deployment/redis -n "$NS" --timeout=180s
|
||||
|
||||
echo " in-cluster: redis://redis.${NS}.svc.cluster.local:6379/0"
|
||||
149
rig/ctrl/cluster.sh
Executable file
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env bash
|
||||
# Cluster lifecycle, plus what else is running on this machine.
|
||||
#
|
||||
# `list` and `free` live here rather than in a separate script because a
|
||||
# near-identical second name (cluster / clusters) is a trap — you reach for one
|
||||
# and get the other. One target, one file, unambiguous subcommands.
|
||||
#
|
||||
# "Idempotent" here means CONVERGENT, not "exits early if the cluster exists".
|
||||
# That distinction matters: an interrupted first run can leave a cluster created
|
||||
# but not finished, and returning early on the re-run would strand it there.
|
||||
# The create step is conditional; every step after it always runs, and each one
|
||||
# is individually idempotent.
|
||||
#
|
||||
# Usage: cluster.sh up | down | reset | list | free
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
|
||||
up() {
|
||||
if kind get clusters 2>/dev/null | grep -qx "$CLUSTER"; then
|
||||
echo "cluster '$CLUSTER' exists — converging"
|
||||
else
|
||||
# Say what this profile locks in BEFORE spending minutes building it:
|
||||
# the audit policy is an apiserver flag and cannot be changed later.
|
||||
echo "creating cluster '$CLUSTER' from profile '$PROFILE_NAME'"
|
||||
echo " shape ctrl/k8s/$KIND_CONFIG"
|
||||
echo " nodes $NODES"
|
||||
echo " image $NODE_IMAGE"
|
||||
echo " audit $AUDIT"
|
||||
echo " ingress $INGRESS_MODE"
|
||||
echo " registry $REGISTRY_MODE"
|
||||
echo " (audit is fixed at creation — 'make cluster reset' to change it)"
|
||||
echo
|
||||
|
||||
render_kind_config | kind create cluster --config -
|
||||
fi
|
||||
|
||||
# The cluster can exist while its context does not — a reset or a switched
|
||||
# KUBECONFIG loses it, and then nothing works despite a healthy cluster.
|
||||
if ! kubectl config get-contexts -o name 2>/dev/null | grep -qx "$KUBECONTEXT"; then
|
||||
echo "context '$KUBECONTEXT' missing from kubeconfig — re-exporting"
|
||||
kind export kubeconfig --name "$CLUSTER"
|
||||
fi
|
||||
kubectl config use-context "$KUBECONTEXT" >/dev/null
|
||||
|
||||
bash registry.sh up
|
||||
|
||||
if [ -n "${ADDONS// /}" ]; then
|
||||
bash addons.sh install
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "cluster '$CLUSTER' ready (context $KUBECONTEXT)"
|
||||
}
|
||||
|
||||
down() {
|
||||
# The registry is a standalone container outside the cluster; take it down
|
||||
# first so a reset doesn't leave it orphaned and holding a port.
|
||||
bash registry.sh down || true
|
||||
|
||||
if kind get clusters 2>/dev/null | grep -qx "$CLUSTER"; then
|
||||
echo "deleting cluster '$CLUSTER'..."
|
||||
kind delete cluster --name "$CLUSTER"
|
||||
else
|
||||
echo "no cluster '$CLUSTER' to delete"
|
||||
fi
|
||||
}
|
||||
|
||||
# The escape hatch for a wedged cluster, and the only way to change a
|
||||
# creation-time setting such as the audit policy.
|
||||
reset() {
|
||||
down
|
||||
echo
|
||||
up
|
||||
}
|
||||
|
||||
# ── the whole machine ──────────────────────────────────────────────────────
|
||||
# Every cluster is a running container tree whether or not you are using it, and
|
||||
# an idle one is the usual reason a new one will not fit.
|
||||
|
||||
list() {
|
||||
local total avail
|
||||
total=$(awk '/^MemTotal:/{printf "%.1f", $2/1024/1024}' /proc/meminfo)
|
||||
avail=$(awk '/^MemAvailable:/{printf "%.1f", $2/1024/1024}' /proc/meminfo)
|
||||
echo "memory: ${avail} GB available of ${total} GB"
|
||||
echo
|
||||
|
||||
local names; names=$(kind get clusters 2>/dev/null || true)
|
||||
if [ -z "$names" ]; then
|
||||
echo "no clusters"
|
||||
return
|
||||
fi
|
||||
|
||||
printf "%-16s %-10s %8s %6s %-13s %s\n" CLUSTER STATE MEM NODES PORTS ""
|
||||
local c nodes state mem base
|
||||
for c in $names; do
|
||||
nodes=$(docker ps -a --filter "label=io.x-k8s.kind.cluster=$c" --format '{{.Names}}' | wc -l)
|
||||
state=$(docker inspect -f '{{.State.Status}}' "${c}-control-plane" 2>/dev/null || echo unknown)
|
||||
if [ "$state" = "running" ]; then
|
||||
mem=$(docker stats --no-stream --format '{{.MemUsage}}' \
|
||||
$(docker ps --filter "label=io.x-k8s.kind.cluster=$c" -q) 2>/dev/null \
|
||||
| awk '{gsub(/GiB/,"");gsub(/MiB/,"e-3");s+=$1} END {printf "%.1fG", s}')
|
||||
else
|
||||
mem="-"
|
||||
fi
|
||||
# A cluster's name is its directory slug, so its port block is derivable
|
||||
# here without reading that directory's config.
|
||||
base=$(derive_port_base "$c")
|
||||
printf "%-16s %-10s %8s %6s %-13s %s\n" "$c" "$state" "$mem" "$nodes" \
|
||||
"${base}-$((base + 3))" \
|
||||
"$([ "$c" = "$CLUSTER" ] && echo "<- this one")"
|
||||
done
|
||||
}
|
||||
|
||||
# Stop the OTHER clusters to free memory. Stops, never deletes — a stopped
|
||||
# cluster restarts with `docker start`, so nothing is lost.
|
||||
free() {
|
||||
local targets=("$@")
|
||||
if [ ${#targets[@]} -eq 0 ]; then
|
||||
mapfile -t targets < <(kind get clusters 2>/dev/null | grep -vx "$CLUSTER" || true)
|
||||
fi
|
||||
if [ ${#targets[@]} -eq 0 ]; then
|
||||
echo "nothing to stop"
|
||||
return
|
||||
fi
|
||||
|
||||
local c ids
|
||||
for c in "${targets[@]}"; do
|
||||
ids=$(docker ps --filter "label=io.x-k8s.kind.cluster=$c" -q)
|
||||
if [ -z "$ids" ]; then
|
||||
echo "cluster '$c' is not running"
|
||||
continue
|
||||
fi
|
||||
echo "stopping '$c' (restart with: docker start \$(docker ps -aq -f label=io.x-k8s.kind.cluster=$c))"
|
||||
# shellcheck disable=SC2086
|
||||
docker stop $ids >/dev/null
|
||||
done
|
||||
}
|
||||
|
||||
case "${1:-up}" in
|
||||
up) up ;;
|
||||
down) down ;;
|
||||
reset) reset ;;
|
||||
list) list ;;
|
||||
free) shift; free "$@" ;;
|
||||
*) echo "usage: $0 [up|down|reset|list|free]" >&2; exit 1 ;;
|
||||
esac
|
||||
272
rig/ctrl/dockerhost.sh
Executable file
@@ -0,0 +1,272 @@
|
||||
#!/usr/bin/env bash
|
||||
# Share ONE Docker daemon across WSL distros, instead of running one per distro.
|
||||
#
|
||||
# Why this exists
|
||||
# ---------------
|
||||
# WSL2 distros share a kernel and a network stack. Two dockerd instances then
|
||||
# contend over docker0 and iptables, which can disturb the daemon you actually
|
||||
# depend on. Docker Desktop avoids this by running a single daemon in a
|
||||
# dedicated distro and sharing its socket — this is the same idea, without
|
||||
# Docker Desktop.
|
||||
#
|
||||
# So a throwaway rig box does NOT install Docker. It borrows the daemon from
|
||||
# whichever distro is the designated host. That also makes the test more honest:
|
||||
# rig never installs Docker anyway — Docker is its documented prerequisite.
|
||||
#
|
||||
# How
|
||||
# ---
|
||||
# /mnt/wsl is a tmpfs with `shared` mount propagation, visible to every distro
|
||||
# in the WSL VM. The owning distro exposes its socket there; guests point
|
||||
# DOCKER_HOST at it. Two ways, with different costs:
|
||||
#
|
||||
# share bind-mount the existing socket onto the shared tmpfs.
|
||||
# Instant, and dockerd is NEVER restarted. Lasts until the
|
||||
# next WSL shutdown.
|
||||
# share --persist additionally install a systemd drop-in so dockerd listens
|
||||
# there itself. Survives restarts, but requires one Docker
|
||||
# restart now — which stops every container that has no
|
||||
# restart policy, since live-restore is off by default.
|
||||
#
|
||||
# The bind mount is the default precisely because the persistent version's cost
|
||||
# is paid on a machine that is already working.
|
||||
#
|
||||
# Reversibility is the whole design
|
||||
# ---------------------------------
|
||||
# `unshare` removes the bind mount (no restart) and, if present, the drop-in.
|
||||
# The original systemd unit is never edited — only an additive drop-in file is
|
||||
# ever created — so undoing is deletion, not repair. `status` always states
|
||||
# which of the three roles a distro is in, in those words.
|
||||
#
|
||||
# Nothing here runs automatically. It does nothing until invoked.
|
||||
#
|
||||
# Usage:
|
||||
# dockerhost.sh status # which distro owns Docker; what this one uses
|
||||
# dockerhost.sh share # share it (bind mount, no daemon restart)
|
||||
# dockerhost.sh share --persist # ...and survive WSL restarts (restarts Docker)
|
||||
# dockerhost.sh unshare # undo it; this distro owns its Docker again
|
||||
# dockerhost.sh use [--persist] # point THIS distro at the shared socket
|
||||
set -euo pipefail
|
||||
|
||||
SHARED_DIR=/mnt/wsl/shared-docker
|
||||
SHARED_SOCK="$SHARED_DIR/docker.sock"
|
||||
OWNER_FILE="$SHARED_DIR/OWNER"
|
||||
DROPIN=/etc/systemd/system/docker.service.d/10-rig-shared-socket.conf
|
||||
PROFILE_D=/etc/profile.d/rig-docker-host.sh
|
||||
|
||||
distro_name() { echo "${WSL_DISTRO_NAME:-$(hostname)}"; }
|
||||
|
||||
require_wsl() {
|
||||
grep -qi microsoft /proc/version 2>/dev/null && return 0
|
||||
echo "dockerhost is WSL-only: it relies on /mnt/wsl being shared between distros." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ── status ─────────────────────────────────────────────────────────────────
|
||||
|
||||
status() {
|
||||
require_wsl
|
||||
echo "distro $(distro_name)"
|
||||
|
||||
if [ -f "$DROPIN" ] || mountpoint -q "$SHARED_SOCK" 2>/dev/null; then
|
||||
echo "role SHARING — this distro's Docker is offered to other distros"
|
||||
elif [ -n "${DOCKER_HOST:-}" ] && [ "${DOCKER_HOST}" = "unix://$SHARED_SOCK" ]; then
|
||||
echo "role BORROWING — using another distro's Docker"
|
||||
else
|
||||
echo "role standalone — this WSL installation has the main host Docker"
|
||||
fi
|
||||
|
||||
echo
|
||||
if [ -S "$SHARED_SOCK" ]; then
|
||||
echo "shared sock $SHARED_SOCK (present)"
|
||||
[ -f "$OWNER_FILE" ] && sed 's/^/ /' "$OWNER_FILE"
|
||||
else
|
||||
echo "shared sock none — no distro is sharing right now"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "DOCKER_HOST ${DOCKER_HOST:-(unset — using /var/run/docker.sock)}"
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
echo "docker $(docker version --format '{{.Server.Version}}' 2>/dev/null || echo unreachable)"
|
||||
else
|
||||
echo "docker cli not installed"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── share / unshare (run on the host distro) ───────────────────────────────
|
||||
|
||||
# Default: expose the EXISTING socket by bind-mounting it onto the shared tmpfs.
|
||||
# /mnt/wsl has `shared` propagation, so the mount is visible in other distros.
|
||||
#
|
||||
# The point of doing it this way is that dockerd is never restarted. Restarting
|
||||
# it stops every container that has no restart policy (live-restore is off by
|
||||
# default), which on a working machine means quietly killing whatever you had
|
||||
# running. Not a trade worth making just to expose a socket.
|
||||
#
|
||||
# Cost: a bind mount does not survive a WSL VM shutdown. `--persist` adds the
|
||||
# systemd drop-in as well, which does survive but needs that one restart.
|
||||
share_bind() {
|
||||
mkdir -p "$SHARED_DIR"
|
||||
chmod 0755 "$SHARED_DIR"
|
||||
|
||||
if mountpoint -q "$SHARED_SOCK" 2>/dev/null; then
|
||||
echo "already bind-mounted at $SHARED_SOCK"
|
||||
else
|
||||
[ -S /var/run/docker.sock ] || { echo "no /var/run/docker.sock here" >&2; exit 1; }
|
||||
# The target must exist as a file for a bind mount onto it.
|
||||
[ -e "$SHARED_SOCK" ] || : > "$SHARED_SOCK"
|
||||
mount --bind /var/run/docker.sock "$SHARED_SOCK"
|
||||
echo "bind-mounted /var/run/docker.sock -> $SHARED_SOCK (no daemon restart)"
|
||||
fi
|
||||
|
||||
cat > "$OWNER_FILE" <<EOF
|
||||
owner distro: $(distro_name)
|
||||
docker gid: $(getent group docker | cut -d: -f3)
|
||||
socket: $SHARED_SOCK
|
||||
method: bind-mount (until the next WSL shutdown)
|
||||
EOF
|
||||
}
|
||||
|
||||
share() {
|
||||
require_wsl
|
||||
[ "$(id -u)" -eq 0 ] || { echo "run with sudo: sudo bash ctrl/dockerhost.sh share" >&2; exit 1; }
|
||||
|
||||
share_bind
|
||||
|
||||
if [ "${1:-}" != "--persist" ]; then
|
||||
echo
|
||||
echo "This lasts until the next WSL shutdown. To make it survive, re-run with"
|
||||
echo "--persist — but note that adds a systemd drop-in and RESTARTS Docker,"
|
||||
echo "which stops any container that has no restart policy."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -f "$DROPIN" ]; then
|
||||
echo "drop-in already present — sharing persists across restarts."
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "--persist: installing a systemd drop-in and restarting Docker."
|
||||
echo "Containers without a restart policy will stop and will NOT come back."
|
||||
docker ps --format ' {{.Names}} restart={{.HostConfig.RestartPolicy.Name}}' 2>/dev/null \
|
||||
|| docker ps --format ' {{.Names}}' 2>/dev/null || true
|
||||
echo
|
||||
|
||||
local exec_line
|
||||
exec_line=$(systemctl cat docker.service | grep -m1 '^ExecStart=')
|
||||
if [ -z "$exec_line" ]; then
|
||||
echo "could not read docker.service ExecStart — refusing to guess" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$DROPIN")" "$SHARED_DIR"
|
||||
# Additive only: blank the inherited ExecStart, then restate it verbatim
|
||||
# with one extra -H. Nothing about the original unit is edited.
|
||||
cat > "$DROPIN" <<EOF
|
||||
# Added by rig (ctrl/dockerhost.sh share).
|
||||
#
|
||||
# Adds a SECOND listening socket on the WSL-shared tmpfs so other distros can
|
||||
# use this daemon instead of running their own. The original socket is
|
||||
# untouched, so this distro behaves exactly as before.
|
||||
#
|
||||
# To undo: sudo bash ctrl/dockerhost.sh unshare
|
||||
[Service]
|
||||
ExecStartPre=-/bin/mkdir -p $SHARED_DIR
|
||||
ExecStartPre=-/bin/chmod 0755 $SHARED_DIR
|
||||
ExecStart=
|
||||
${exec_line} -H unix://$SHARED_SOCK
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl restart docker
|
||||
|
||||
# Guests need a group with a MATCHING GID to use the socket; GIDs are not
|
||||
# consistent across distros, so record ours rather than assume.
|
||||
cat > "$OWNER_FILE" <<EOF
|
||||
owner distro: $(distro_name)
|
||||
docker gid: $(getent group docker | cut -d: -f3)
|
||||
socket: $SHARED_SOCK
|
||||
EOF
|
||||
|
||||
echo "sharing from '$(distro_name)'"
|
||||
echo " guests: export DOCKER_HOST=unix://$SHARED_SOCK"
|
||||
echo " undo: sudo bash ctrl/dockerhost.sh unshare"
|
||||
echo
|
||||
echo "NOTE: /mnt/wsl is tmpfs and is cleared when the WSL VM shuts down."
|
||||
echo " The drop-in recreates the directory on the next Docker start."
|
||||
}
|
||||
|
||||
unshare_() {
|
||||
require_wsl
|
||||
[ "$(id -u)" -eq 0 ] || { echo "run with sudo: sudo bash ctrl/dockerhost.sh unshare" >&2; exit 1; }
|
||||
|
||||
local did=0
|
||||
|
||||
# The bind mount first: undoing it needs no restart, so a plain `share`
|
||||
# is fully reversible without disturbing anything.
|
||||
if mountpoint -q "$SHARED_SOCK" 2>/dev/null; then
|
||||
umount "$SHARED_SOCK"
|
||||
rm -f "$SHARED_SOCK"
|
||||
echo " removed the bind mount (no restart needed)"
|
||||
did=1
|
||||
fi
|
||||
rm -f "$OWNER_FILE"
|
||||
rmdir "$SHARED_DIR" 2>/dev/null || true
|
||||
|
||||
if [ -f "$DROPIN" ]; then
|
||||
rm -f "$DROPIN"
|
||||
rmdir "$(dirname "$DROPIN")" 2>/dev/null || true
|
||||
systemctl daemon-reload
|
||||
systemctl restart docker
|
||||
echo " removed the systemd drop-in and restarted Docker"
|
||||
did=1
|
||||
fi
|
||||
|
||||
if [ "$did" -eq 0 ]; then
|
||||
echo "not sharing — this WSL installation already has the main host Docker."
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "restored: this WSL installation has the main host Docker again."
|
||||
echo " (nothing else was changed; the original unit was never edited)"
|
||||
}
|
||||
|
||||
# ── use (run on a guest distro) ────────────────────────────────────────────
|
||||
|
||||
use() {
|
||||
require_wsl
|
||||
if [ ! -S "$SHARED_SOCK" ]; then
|
||||
echo "no shared socket at $SHARED_SOCK" >&2
|
||||
echo "Run 'sudo bash ctrl/dockerhost.sh share' in the distro that owns Docker." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Align the local docker group GID with the owner's, or the socket is
|
||||
# unreadable here even though it is visible.
|
||||
if [ -f "$OWNER_FILE" ] && [ "$(id -u)" -eq 0 ]; then
|
||||
local gid; gid=$(awk '/docker gid:/ {print $3}' "$OWNER_FILE")
|
||||
if [ -n "$gid" ]; then
|
||||
if getent group docker >/dev/null; then
|
||||
[ "$(getent group docker | cut -d: -f3)" = "$gid" ] || groupmod -g "$gid" docker
|
||||
else
|
||||
groupadd -g "$gid" docker
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${1:-}" = "--persist" ]; then
|
||||
[ "$(id -u)" -eq 0 ] || { echo "--persist needs root" >&2; exit 1; }
|
||||
echo "export DOCKER_HOST=unix://$SHARED_SOCK" > "$PROFILE_D"
|
||||
echo "persisted in $PROFILE_D"
|
||||
fi
|
||||
|
||||
echo "export DOCKER_HOST=unix://$SHARED_SOCK"
|
||||
}
|
||||
|
||||
case "${1:-status}" in
|
||||
status) status ;;
|
||||
share) shift; share "${1:-}" ;;
|
||||
unshare) unshare_ ;;
|
||||
use) shift; use "${1:-}" ;;
|
||||
*) echo "usage: $0 [status|share|unshare|use [--persist]]" >&2; exit 1 ;;
|
||||
esac
|
||||
58
rig/ctrl/docs.sh
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
# Documentation: render the diagrams, and serve the pages.
|
||||
#
|
||||
# The docs are the instructions for building the cluster, so they must work
|
||||
# BEFORE anything else exists. That rules out serving them from the cluster, and
|
||||
# it rules out python -m http.server too — a minimal Debian has no python3. What
|
||||
# it does have, by definition, is Docker: the single prerequisite rig already
|
||||
# demands. So a throwaway nginx container serves a read-only bind mount.
|
||||
#
|
||||
# Rendered SVGs are committed alongside their .dot sources for the same reason:
|
||||
# the pages have to read on a machine with no Graphviz installed.
|
||||
#
|
||||
# Usage: docs.sh serve | graphs
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
|
||||
REPO="$(cd .. && pwd)"
|
||||
DOCS_PORT="${DOCS_PORT:-$((HTTP_PORT + 4))}" # +4 sits inside this env's block
|
||||
|
||||
serve() {
|
||||
if [ ! -f "$REPO/docs/index.html" ]; then
|
||||
echo "no docs/index.html" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "docs for '$CLUSTER' on http://localhost:${DOCS_PORT}"
|
||||
echo " (ctrl-c to stop; nothing is installed and nothing persists)"
|
||||
docker run --rm \
|
||||
--name "${CLUSTER}-docs" \
|
||||
-p "${DOCS_PORT}:80" \
|
||||
-v "$REPO/docs:/usr/share/nginx/html:ro" \
|
||||
nginx:alpine
|
||||
}
|
||||
|
||||
graphs() {
|
||||
if ! command -v dot >/dev/null 2>&1; then
|
||||
echo "graphviz not found — install with: sudo apt install graphviz" >&2
|
||||
echo "(only needed to re-render; the committed .svg files already work)" >&2
|
||||
exit 1
|
||||
fi
|
||||
shopt -s nullglob
|
||||
local found=0 f out
|
||||
for f in "$REPO"/docs/graphs/*.dot; do
|
||||
out="${f%.dot}.svg"
|
||||
echo " graphviz $(basename "$f") → $(basename "$out")"
|
||||
dot -Tsvg "$f" -o "$out"
|
||||
found=1
|
||||
done
|
||||
[ "$found" -eq 1 ] || echo " no .dot files in docs/graphs/"
|
||||
}
|
||||
|
||||
case "${1:-serve}" in
|
||||
serve) serve ;;
|
||||
graphs) graphs ;;
|
||||
*) echo "usage: $0 [serve|graphs]" >&2; exit 1 ;;
|
||||
esac
|
||||
30
rig/ctrl/env.d/client.env
Normal file
@@ -0,0 +1,30 @@
|
||||
# client — the regulated-estate shape. Multi-node so taints, affinity and
|
||||
# topology are real; apiserver audit on; images through a pull-through cache of
|
||||
# the corporate registry.
|
||||
#
|
||||
# Costs roughly 4-6 GB. Check `make cluster list` before starting this alongside
|
||||
# other work — see the memory note in the README.
|
||||
|
||||
PROFILE_NAME=client
|
||||
K8S_VERSION=v1_36
|
||||
KIND_CONFIG=kind-config.client.yaml.tpl
|
||||
ADDONS="metallb cert-manager metrics-server"
|
||||
REGISTRY_MODE=mirror
|
||||
INGRESS_MODE=hostport
|
||||
DNS_MODE=hosts
|
||||
|
||||
# Ports derive from the directory name by default (see ctrl/ports.sh), so
|
||||
# several environments run side by side.
|
||||
#
|
||||
# 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
|
||||
# time. Uncommenting also means only one environment can exist at a time.
|
||||
# HTTP_PORT=80
|
||||
# HTTPS_PORT=443
|
||||
|
||||
# Set these in ctrl/.env (gitignored), not here:
|
||||
# REGISTRY_REMOTE_URL=https://artifactory.corp.example/artifactory/api/docker/docker-virtual
|
||||
# REGISTRY_USER / REGISTRY_PASSWORD
|
||||
# REGISTRY_CA_FILE=/path/to/corp-root-ca.crt
|
||||
42
rig/ctrl/env.d/data.env
Normal file
@@ -0,0 +1,42 @@
|
||||
# data — a cluster with the dependency containers a soleprint room 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.
|
||||
#
|
||||
# Everything lands in the `data` namespace (DATA_NAMESPACE to move it), so
|
||||
# `make cluster reset` on the app namespace leaves the databases alone.
|
||||
#
|
||||
# Costs roughly 2-3 GB with airflow, under 1 without. Airflow's first boot runs
|
||||
# the whole metadata migration, so expect a few minutes before it is ready.
|
||||
|
||||
PROFILE_NAME=data
|
||||
K8S_VERSION=v1_36
|
||||
KIND_CONFIG=kind-config.yaml.tpl
|
||||
# Order matters: addons.sh installs in the order listed, and airflow refuses to
|
||||
# start without a metadata database, so postgres comes first.
|
||||
ADDONS="metallb postgres redis airflow"
|
||||
# local, not none — see minimal.env: `none` has no outward-push guard.
|
||||
REGISTRY_MODE=local
|
||||
INGRESS_MODE=hostport
|
||||
DNS_MODE=hosts
|
||||
|
||||
# Namespace for the dependency containers.
|
||||
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_STORAGE=2Gi
|
||||
|
||||
AIRFLOW_ADMIN_USER=admin
|
||||
|
||||
# Ports derive from the directory name by default — see ctrl/ports.sh. Reach
|
||||
# the databases with port-forward rather than binding more host ports:
|
||||
# kubectl -n data port-forward svc/postgres 5432:5432
|
||||
# kubectl -n data port-forward svc/airflow 8080:8080
|
||||
21
rig/ctrl/env.d/minimal.env
Normal file
@@ -0,0 +1,21 @@
|
||||
# minimal — the default. One node, no addons, no registry.
|
||||
# Assumes nothing and boots fast. Start here; move to client.env when you need
|
||||
# the regulated behaviours.
|
||||
#
|
||||
|
||||
PROFILE_NAME=minimal
|
||||
K8S_VERSION=v1_36
|
||||
KIND_CONFIG=kind-config.yaml.tpl
|
||||
ADDONS=""
|
||||
# local, not none: `none` leaves the cluster with no registry to push to, and an
|
||||
# unqualified image name then means docker.io/library/<name>. In a regulated
|
||||
# estate that is a disclosure risk, not a convenience trade — so the default
|
||||
# carries the guard even though it costs one container.
|
||||
REGISTRY_MODE=local
|
||||
INGRESS_MODE=hostport
|
||||
DNS_MODE=hosts
|
||||
|
||||
# Ports are deliberately NOT set here. They derive from the directory name so
|
||||
# several environments coexist — see ctrl/ports.sh, and `make ports` to see the
|
||||
# block this one gets. A fixed default here would collide with whatever else the
|
||||
# machine happens to be running; 8080 in particular is rarely free.
|
||||
18
rig/ctrl/env.d/offline.env
Normal file
@@ -0,0 +1,18 @@
|
||||
# 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
|
||||
# (DEPS_SOURCE=baked) so the toolchain install is offline too.
|
||||
#
|
||||
# The heavier addons are left out to keep first boot viable.
|
||||
|
||||
PROFILE_NAME=offline
|
||||
K8S_VERSION=v1_36
|
||||
KIND_CONFIG=kind-config.audit.yaml.tpl
|
||||
ADDONS="metallb"
|
||||
REGISTRY_MODE=local
|
||||
INGRESS_MODE=hostport
|
||||
DNS_MODE=hosts
|
||||
|
||||
# Derived from the directory name by default — see ctrl/ports.sh.
|
||||
# Uncomment for the real ports, but only if this is the only environment.
|
||||
# HTTP_PORT=80
|
||||
# HTTPS_PORT=443
|
||||
15
rig/ctrl/hosts.tmpl
Normal file
@@ -0,0 +1,15 @@
|
||||
# /etc/hosts block for this environment. Rendered by newbox.sh; ${CLUSTER} and
|
||||
# ${HTTP_PORT} are substituted.
|
||||
#
|
||||
# Hostnames are a convenience, not a requirement — every service is reachable at
|
||||
# localhost:<port> without any of this, which is why DNS is not touched by
|
||||
# default. Add entries here as the model grows.
|
||||
#
|
||||
# On Windows the same block has to go in
|
||||
# C:\Windows\System32\drivers\etc\hosts for a browser to resolve these. That
|
||||
# file does NOT support wildcards, so every name must be listed explicitly.
|
||||
# newbox.sh prints the block for you to paste rather than editing it.
|
||||
|
||||
127.0.0.1 ${CLUSTER}.local
|
||||
127.0.0.1 api.${CLUSTER}.local
|
||||
127.0.0.1 docs.${CLUSTER}.local
|
||||
71
rig/ctrl/k8s/README.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# `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`.
|
||||
|
||||
```
|
||||
kind-config*.yaml.tpl the cluster itself — nodes, ports, audit
|
||||
base/ the components, as plain manifests
|
||||
overlays/dev/ how this rig differs from the base
|
||||
audit-policy.yaml mounted into the apiserver by the audit shapes
|
||||
```
|
||||
|
||||
## Why the cluster config is a template
|
||||
|
||||
Every other project checks in a literal `kind-config.yaml`, because there is
|
||||
exactly one `unt` and one `nvi`. A rig is copied and renamed to make a second
|
||||
environment, and both the cluster name and the host port block follow the
|
||||
directory name — so a literal would make every copy collide on both.
|
||||
|
||||
`ctrl/cluster.sh` renders it with `sed`, substituting `${CLUSTER}`,
|
||||
`${NODE_IMAGE}`, `${HTTP_PORT}` and `${HOST_WORKDIR}`. Not `envsubst`: that is
|
||||
`gettext-base`, which a minimal Debian does not have, and Docker being the only
|
||||
prerequisite is the one promise rig makes.
|
||||
|
||||
**The chosen file is the source of truth for node count and audit.**
|
||||
`lib/config.sh` reads both back out of it, so a profile names a shape and does
|
||||
not restate what the YAML already says.
|
||||
|
||||
| file | nodes | audit | profiles |
|
||||
| --- | --- | --- | --- |
|
||||
| `kind-config.yaml.tpl` | 1 | off | `minimal`, `data` |
|
||||
| `kind-config.audit.yaml.tpl` | 1 | on | `offline` |
|
||||
| `kind-config.client.yaml.tpl` | 3 | on | `client` |
|
||||
|
||||
A profile picks one with `KIND_CONFIG` in `ctrl/env.d/<profile>.env`. Adding a
|
||||
shape is adding a file — there is no dispatcher to edit.
|
||||
|
||||
Audit is an apiserver flag and therefore fixed at creation: changing it is
|
||||
`make cluster reset`, not a re-apply.
|
||||
|
||||
## `base/` — replace these
|
||||
|
||||
**The two components in `base/` are examples, not the system.** They exist so
|
||||
the real manifests have a shape to be written against.
|
||||
|
||||
The real ones are expected to be versioned **separately from the installer** —
|
||||
they change on a different cadence, by different people, under different review.
|
||||
Point `MANIFESTS_DIR` in `ctrl/.env` at their overlay and rig stops owning them:
|
||||
|
||||
```
|
||||
MANIFESTS_DIR=../platform-manifests/overlays/dev
|
||||
```
|
||||
|
||||
Until then it defaults to `ctrl/k8s/overlays/dev`.
|
||||
|
||||
### The three states a component can be in
|
||||
|
||||
Switching between them should be a one-line change, never a rewrite. The DNS
|
||||
name stays the same in every case, so callers never know the difference:
|
||||
|
||||
| state | what exists | when |
|
||||
| --- | --- | --- |
|
||||
| **real** | an image built from source, hot-reloaded | the one thing you are working on |
|
||||
| **mock** | a stub returning canned responses (`example-mock.yaml`) | everything else — most of the estate |
|
||||
| **remote** | no pod at all, just a Service (`example-remote.yaml`) | when the real system is reachable and you want it |
|
||||
|
||||
Most components should be **mock**. What has to be faithful is the topology —
|
||||
names, ports, dependency order, who can reach whom, how it fails. The workloads
|
||||
are noise, and mocking them is what makes several copies of a large estate fit
|
||||
on one laptop.
|
||||
44
rig/ctrl/k8s/audit-policy.yaml
Normal file
@@ -0,0 +1,44 @@
|
||||
# Apiserver audit policy. Mounted into the control plane at creation when a
|
||||
# profile sets AUDIT=on — an apiserver flag, so it cannot be added to a running
|
||||
# cluster without recreating it.
|
||||
#
|
||||
# Deliberately modest: enough to make "who changed what, and when" answerable
|
||||
# during onboarding without filling the disk. Read the log with:
|
||||
# docker exec <cluster>-control-plane cat /var/log/kubernetes/audit.log
|
||||
apiVersion: audit.k8s.io/v1
|
||||
kind: Policy
|
||||
|
||||
# Never log the request body for these — they contain credentials.
|
||||
omitStages:
|
||||
- RequestReceived
|
||||
|
||||
rules:
|
||||
# Secrets/configmaps: record that access happened, never the contents.
|
||||
- level: Metadata
|
||||
resources:
|
||||
- group: ""
|
||||
resources: ["secrets", "configmaps"]
|
||||
|
||||
# Authn/authz decisions — the part an auditor actually asks about.
|
||||
- level: Metadata
|
||||
nonResourceURLs:
|
||||
- /apis*
|
||||
- /api*
|
||||
|
||||
# Mutations to workloads and policy: full request, so a diff is reconstructable.
|
||||
- level: Request
|
||||
verbs: ["create", "update", "patch", "delete"]
|
||||
resources:
|
||||
- group: ""
|
||||
resources: ["pods", "services", "serviceaccounts", "namespaces"]
|
||||
- group: "apps"
|
||||
- group: "networking.k8s.io"
|
||||
- group: "rbac.authorization.k8s.io"
|
||||
|
||||
# Everything else that changes state: metadata only.
|
||||
- level: Metadata
|
||||
verbs: ["create", "update", "patch", "delete"]
|
||||
|
||||
# Reads are dropped entirely — otherwise controller polling drowns the log.
|
||||
- level: None
|
||||
verbs: ["get", "list", "watch"]
|
||||
104
rig/ctrl/k8s/base/example-mock.yaml
Normal file
@@ -0,0 +1,104 @@
|
||||
# EXAMPLE — a mocked component. Copy, rename, replace.
|
||||
#
|
||||
# A stub that answers on the right name and port with canned responses. No image
|
||||
# to build: the script is mounted from the ConfigMap, so changing the behaviour
|
||||
# is a kubectl apply, not a rebuild.
|
||||
#
|
||||
# Deliberately boring and readable. This is onboarding material — someone should
|
||||
# be able to read the generated object and recognise what it is.
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: example-service-stub
|
||||
data:
|
||||
# Canned responses by path. Add entries as the contract becomes clear;
|
||||
# anything unmatched returns 404 so a missing route is visible, not silent.
|
||||
routes.json: |
|
||||
{
|
||||
"/health": {"status": 200, "body": {"status": "ok"}},
|
||||
"/v1/example": {"status": 200, "body": {"items": [], "mocked": true}}
|
||||
}
|
||||
serve.py: |
|
||||
import json, os
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
ROUTES = json.load(open("/etc/stub/routes.json"))
|
||||
NAME = os.environ.get("STUB_NAME", "stub")
|
||||
|
||||
class H(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
r = ROUTES.get(self.path)
|
||||
if r is None:
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
# Say which stub rejected it — with everything mocked, "404"
|
||||
# alone tells you nothing about where the call actually landed.
|
||||
self.wfile.write(json.dumps(
|
||||
{"error": "no canned route", "stub": NAME, "path": self.path}
|
||||
).encode())
|
||||
return
|
||||
body = json.dumps(r["body"]).encode()
|
||||
self.send_response(r["status"])
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("X-Mocked-By", NAME)
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
print("%s %s" % (NAME, fmt % args), flush=True)
|
||||
|
||||
HTTPServer(("0.0.0.0", 8080), H).serve_forever()
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: example-service
|
||||
labels:
|
||||
app: example-service
|
||||
rig.component/impl: mock # so `kubectl get deploy -L rig.component/impl`
|
||||
# shows at a glance what is real and what is not
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: example-service
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: example-service
|
||||
spec:
|
||||
containers:
|
||||
- name: stub
|
||||
image: python:3.12-slim
|
||||
command: ["python3", "/etc/stub/serve.py"]
|
||||
env:
|
||||
- name: STUB_NAME
|
||||
value: example-service
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
volumeMounts:
|
||||
- name: stub
|
||||
mountPath: /etc/stub
|
||||
readinessProbe:
|
||||
httpGet: { path: /health, port: 8080 }
|
||||
initialDelaySeconds: 2
|
||||
# Small enough that a whole estate of these fits alongside the real
|
||||
# thing you are working on.
|
||||
resources:
|
||||
requests: { memory: 32Mi, cpu: 10m }
|
||||
limits: { memory: 64Mi }
|
||||
volumes:
|
||||
- name: stub
|
||||
configMap:
|
||||
name: example-service-stub
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: example-service
|
||||
spec:
|
||||
selector:
|
||||
app: example-service
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8080
|
||||
46
rig/ctrl/k8s/base/example-remote.yaml
Normal file
@@ -0,0 +1,46 @@
|
||||
# EXAMPLE — a component that is NOT simulated, pointed at the real system.
|
||||
#
|
||||
# This is the payoff of keeping the topology honest: there is no pod here at
|
||||
# all, yet `example-remote.<namespace>.svc.cluster.local` resolves exactly as it
|
||||
# does when the same component is mocked. Callers are identical in both cases,
|
||||
# so moving a dependency from mocked to real is a one-line change and nothing
|
||||
# downstream is touched.
|
||||
#
|
||||
# Use this when the real system is reachable and you want it in the loop.
|
||||
# Note that reachability depends on where you are running: systems restricted to
|
||||
# a managed workspace will not resolve from a laptop at all, which is the whole
|
||||
# reason most components should stay mocked.
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: example-remote
|
||||
labels:
|
||||
rig.component/impl: remote
|
||||
spec:
|
||||
type: ExternalName
|
||||
externalName: real-system.internal.example.com
|
||||
---
|
||||
# If the real system has no DNS name — only an IP, which is common for legacy
|
||||
# hosts — ExternalName cannot express it. Use a bare Service plus manual
|
||||
# Endpoints instead, and delete the block above.
|
||||
#
|
||||
# apiVersion: v1
|
||||
# kind: Service
|
||||
# metadata:
|
||||
# name: example-remote
|
||||
# labels:
|
||||
# rig.component/impl: remote
|
||||
# spec:
|
||||
# ports:
|
||||
# - port: 80
|
||||
# targetPort: 8080
|
||||
# ---
|
||||
# apiVersion: v1
|
||||
# kind: Endpoints
|
||||
# metadata:
|
||||
# name: example-remote # must match the Service name exactly
|
||||
# subsets:
|
||||
# - addresses:
|
||||
# - ip: 10.0.0.42
|
||||
# ports:
|
||||
# - port: 8080
|
||||
11
rig/ctrl/k8s/base/kustomization.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
# The namespace every component lands in. The overlay overrides it, so a rig
|
||||
# modelling two estates can apply the same base twice under different names.
|
||||
namespace: rig
|
||||
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- example-mock.yaml
|
||||
- example-remote.yaml
|
||||
4
rig/ctrl/k8s/base/namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: rig
|
||||
57
rig/ctrl/k8s/kind-config.audit.yaml.tpl
Normal file
@@ -0,0 +1,57 @@
|
||||
# Cluster shape: one node, apiserver audit ON. Used by the `offline` profile.
|
||||
#
|
||||
# Audit is an apiserver flag, so it is fixed when the cluster is created —
|
||||
# changing it means `make cluster reset`, not a re-apply. That is why it is a
|
||||
# property of the cluster file rather than something switched at runtime.
|
||||
#
|
||||
# k8s >= 1.31 uses kubeadm v1beta4, where extraArgs is a LIST of name/value
|
||||
# pairs. The older map form is silently ignored — it does not error, audit
|
||||
# simply never turns on.
|
||||
#
|
||||
# Substituted by ctrl/cluster.sh: CLUSTER, NODE_IMAGE, HTTP_PORT, HOST_WORKDIR
|
||||
# (named without the ${...} braces so this line survives the substitution)
|
||||
kind: Cluster
|
||||
apiVersion: kind.x-k8s.io/v1alpha4
|
||||
name: ${CLUSTER}
|
||||
|
||||
containerdConfigPatches:
|
||||
- |-
|
||||
[plugins."io.containerd.grpc.v1.cri".registry]
|
||||
config_path = "/etc/containerd/certs.d"
|
||||
|
||||
kubeadmConfigPatches:
|
||||
- |
|
||||
kind: ClusterConfiguration
|
||||
apiServer:
|
||||
extraArgs:
|
||||
- name: audit-policy-file
|
||||
value: /etc/kubernetes/audit/policy.yaml
|
||||
- name: audit-log-path
|
||||
value: /var/log/kubernetes/audit.log
|
||||
- name: audit-log-maxage
|
||||
value: "7"
|
||||
extraVolumes:
|
||||
- name: audit-policy
|
||||
hostPath: /etc/kubernetes/audit
|
||||
mountPath: /etc/kubernetes/audit
|
||||
readOnly: true
|
||||
- name: audit-log
|
||||
hostPath: /var/log/kubernetes
|
||||
mountPath: /var/log/kubernetes
|
||||
readOnly: false
|
||||
|
||||
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
|
||||
# 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
|
||||
containerPath: /etc/kubernetes/audit/policy.yaml
|
||||
readOnly: true
|
||||
extraPortMappings:
|
||||
- containerPort: 30080
|
||||
hostPort: ${HTTP_PORT}
|
||||
listenAddress: "0.0.0.0"
|
||||
protocol: TCP
|
||||
55
rig/ctrl/k8s/kind-config.client.yaml.tpl
Normal file
@@ -0,0 +1,55 @@
|
||||
# Cluster shape: three nodes, apiserver audit ON. Used by the `client` profile —
|
||||
# the regulated-estate shape.
|
||||
#
|
||||
# Multi-node so taints, affinity and topology spread are real rather than
|
||||
# vacuously satisfied by a single node. It costs roughly 4-6 GB; run
|
||||
# `make cluster list` before starting this alongside other work.
|
||||
#
|
||||
# Substituted by ctrl/cluster.sh: CLUSTER, NODE_IMAGE, HTTP_PORT, HOST_WORKDIR
|
||||
# (named without the ${...} braces so this line survives the substitution)
|
||||
kind: Cluster
|
||||
apiVersion: kind.x-k8s.io/v1alpha4
|
||||
name: ${CLUSTER}
|
||||
|
||||
containerdConfigPatches:
|
||||
- |-
|
||||
[plugins."io.containerd.grpc.v1.cri".registry]
|
||||
config_path = "/etc/containerd/certs.d"
|
||||
|
||||
kubeadmConfigPatches:
|
||||
- |
|
||||
kind: ClusterConfiguration
|
||||
apiServer:
|
||||
extraArgs:
|
||||
- name: audit-policy-file
|
||||
value: /etc/kubernetes/audit/policy.yaml
|
||||
- name: audit-log-path
|
||||
value: /var/log/kubernetes/audit.log
|
||||
- name: audit-log-maxage
|
||||
value: "7"
|
||||
extraVolumes:
|
||||
- name: audit-policy
|
||||
hostPath: /etc/kubernetes/audit
|
||||
mountPath: /etc/kubernetes/audit
|
||||
readOnly: true
|
||||
- name: audit-log
|
||||
hostPath: /var/log/kubernetes
|
||||
mountPath: /var/log/kubernetes
|
||||
readOnly: false
|
||||
|
||||
nodes:
|
||||
- role: control-plane
|
||||
image: ${NODE_IMAGE}
|
||||
extraMounts:
|
||||
- hostPath: ${HOST_WORKDIR}/ctrl/k8s/audit-policy.yaml
|
||||
containerPath: /etc/kubernetes/audit/policy.yaml
|
||||
readOnly: true
|
||||
extraPortMappings:
|
||||
- containerPort: 30080
|
||||
hostPort: ${HTTP_PORT}
|
||||
listenAddress: "0.0.0.0"
|
||||
protocol: TCP
|
||||
- role: worker
|
||||
image: ${NODE_IMAGE}
|
||||
- role: worker
|
||||
image: ${NODE_IMAGE}
|
||||
36
rig/ctrl/k8s/kind-config.yaml.tpl
Normal file
@@ -0,0 +1,36 @@
|
||||
# Cluster shape: one node, no audit. Used by the `minimal` and `data` profiles.
|
||||
#
|
||||
# A TEMPLATE rather than a plain kind-config.yaml because a rig is copied and
|
||||
# renamed to make a second environment, and both the cluster name and the host
|
||||
# port follow the directory. A checked-in literal would make every copy collide
|
||||
# on both. ctrl/cluster.sh renders it with sed — not envsubst, which is
|
||||
# gettext-base and absent from a minimal Debian, and rig's whole premise is that
|
||||
# Docker is the only prerequisite.
|
||||
#
|
||||
# Substituted by ctrl/cluster.sh: CLUSTER, NODE_IMAGE, HTTP_PORT, HOST_WORKDIR
|
||||
# (named without the ${...} braces so this line survives the substitution)
|
||||
# Node count and audit are READ BACK from this file by lib/config.sh, so this
|
||||
# YAML is the source of truth for both — there is no second place to update.
|
||||
kind: Cluster
|
||||
apiVersion: kind.x-k8s.io/v1alpha4
|
||||
name: ${CLUSTER}
|
||||
|
||||
# Point containerd at a certs.d directory. registry.sh drops per-host hosts.toml
|
||||
# files in there afterwards, so switching registry mode never requires
|
||||
# recreating the cluster.
|
||||
containerdConfigPatches:
|
||||
- |-
|
||||
[plugins."io.containerd.grpc.v1.cri".registry]
|
||||
config_path = "/etc/containerd/certs.d"
|
||||
|
||||
nodes:
|
||||
- role: control-plane
|
||||
image: ${NODE_IMAGE}
|
||||
# One NodePort bridged to the host; an in-cluster gateway owns it. There is
|
||||
# deliberately no ingress controller — they pin a narrow window of k8s
|
||||
# versions, and running a trailing-edge control plane is the point.
|
||||
extraPortMappings:
|
||||
- containerPort: 30080
|
||||
hostPort: ${HTTP_PORT}
|
||||
listenAddress: "0.0.0.0"
|
||||
protocol: TCP
|
||||
22
rig/ctrl/k8s/overlays/dev/kustomization.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- ../../base
|
||||
|
||||
# The dev overlay is where a rig says how its estate differs from the base —
|
||||
# which components are real, which are mocked, which point at a live system.
|
||||
# Kept empty on purpose: the base already boots, and an overlay full of examples
|
||||
# is harder to read than one that starts blank.
|
||||
#
|
||||
# The shape a patch takes, for when the first one is needed:
|
||||
#
|
||||
# patches:
|
||||
# - target: {kind: Service, name: example-service}
|
||||
# patch: |
|
||||
# - op: replace
|
||||
# path: /spec/type
|
||||
# value: NodePort
|
||||
# - op: add
|
||||
# path: /spec/ports/0/nodePort
|
||||
# value: 30080
|
||||
148
rig/ctrl/lib/config.sh
Normal file
@@ -0,0 +1,148 @@
|
||||
# Shared config loading. Sourced, never executed.
|
||||
#
|
||||
# The ecosystem convention is that scripts are standalone with no shared log
|
||||
# library — that still holds. This file is not a logging lib; it is the single
|
||||
# definition of how the config layers compose, which every script has to agree
|
||||
# on exactly. Precedence, weakest first:
|
||||
#
|
||||
# ctrl/versions.env pinned toolchain + image digests (committed)
|
||||
# ctrl/env.d/<profile> cluster shape (committed)
|
||||
# ctrl/.env machine-local values and secrets (gitignored)
|
||||
# the caller's env `make cluster up PROFILE=client` (always wins)
|
||||
#
|
||||
# That last rule is why this is more than a few `source` lines: .env sets
|
||||
# PROFILE, so without snapshotting it would silently override the PROFILE the
|
||||
# user just typed on the command line.
|
||||
#
|
||||
# Run from ctrl/.
|
||||
|
||||
# Values a user can reasonably override per-invocation. Anything set in the
|
||||
# environment when load_config runs is restored after the files are read.
|
||||
# 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
|
||||
# one place that decides the shape of the cluster rather than two that can drift.
|
||||
CONFIG_OVERRIDABLE="PROFILE CLUSTER K8S_VERSION KIND_CONFIG ADDONS
|
||||
REGISTRY_MODE INGRESS_MODE DNS_MODE TILT_PORT
|
||||
SOURCE ARCH DEPS_SOURCE HTTP_PORT HTTPS_PORT"
|
||||
|
||||
# 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
|
||||
# repo root is the parent.
|
||||
default_cluster_name() {
|
||||
local n
|
||||
n=$(basename "$(cd .. && pwd)")
|
||||
n=$(echo "$n" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-')
|
||||
n=$(echo "$n" | sed 's/^-*//; s/-*$//')
|
||||
echo "${n:-rig}"
|
||||
}
|
||||
|
||||
# Base of this environment's 10-port block. cksum is used rather than $RANDOM or
|
||||
# bash hashing because it is POSIX and returns the same value on every machine,
|
||||
# which is what makes the block reproducible instead of merely unique.
|
||||
derive_port_base() {
|
||||
local h; h=$(printf '%s' "$1" | cksum | awk '{print $1}')
|
||||
echo $((20000 + (h % 200) * 10))
|
||||
}
|
||||
|
||||
load_config() {
|
||||
local k saved=""
|
||||
for k in $CONFIG_OVERRIDABLE; do
|
||||
# ${!k+x} distinguishes "set but empty" from "unset" — an explicit
|
||||
# FOO= on the command line is a real choice and must survive.
|
||||
if [ -n "${!k+x}" ]; then
|
||||
saved+="$k=$(printf '%q' "${!k}")"$'\n'
|
||||
fi
|
||||
done
|
||||
|
||||
set -a
|
||||
source ./versions.env
|
||||
[ -f ./.env ] && source ./.env
|
||||
set +a
|
||||
|
||||
# Re-apply overrides now so PROFILE is the caller's before we pick the file.
|
||||
_config_restore "$saved"
|
||||
|
||||
local profile="${PROFILE:-minimal}"
|
||||
if [ ! -f "./env.d/${profile}.env" ]; then
|
||||
echo "no such profile: env.d/${profile}.env" >&2
|
||||
echo "available: $(ls env.d/*.env 2>/dev/null | xargs -n1 basename | sed 's/\.env$//' | tr '\n' ' ')" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -a
|
||||
source "./env.d/${profile}.env"
|
||||
[ -f ./.env ] && source ./.env
|
||||
set +a
|
||||
|
||||
_config_restore "$saved"
|
||||
|
||||
# Identity follows the FOLDER, so copying this directory somewhere else and
|
||||
# renaming it yields a distinct environment with no further edits. Without
|
||||
# this, two copies would share one cluster and `make cluster down` in either
|
||||
# would destroy the other's.
|
||||
CLUSTER="${CLUSTER:-$(default_cluster_name)}"
|
||||
KUBECONTEXT="kind-${CLUSTER}"
|
||||
|
||||
# Host ports are a single shared namespace, so unlike the cluster name they
|
||||
# cannot just follow the directory — they have to be spread out. Anything
|
||||
# already set (ctrl/.env, a profile, the command line) wins; only the gaps
|
||||
# are filled. See ports.sh for the reasoning.
|
||||
local base; base=$(derive_port_base "$CLUSTER")
|
||||
HTTP_PORT="${HTTP_PORT:-$base}"
|
||||
HTTPS_PORT="${HTTPS_PORT:-$((base + 1))}"
|
||||
TILT_PORT="${TILT_PORT:-$((base + 2))}"
|
||||
REGISTRY_PORT="${REGISTRY_PORT:-$((base + 3))}"
|
||||
|
||||
# Profiles name a k8s minor (v1_36); versions.env holds the pinned digest.
|
||||
local var="NODE_IMAGE_${K8S_VERSION}"
|
||||
NODE_IMAGE="${!var:-}"
|
||||
if [ -z "$NODE_IMAGE" ]; then
|
||||
echo "K8S_VERSION='${K8S_VERSION}' has no NODE_IMAGE_${K8S_VERSION} in versions.env" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The cluster's shape is a file in ctrl/k8s/, named by the profile. Adding a
|
||||
# shape is adding a file; there is no dispatcher to edit.
|
||||
KIND_CONFIG="${KIND_CONFIG:-kind-config.yaml.tpl}"
|
||||
KIND_CONFIG_PATH="./k8s/${KIND_CONFIG}"
|
||||
if [ ! -f "$KIND_CONFIG_PATH" ]; then
|
||||
echo "no such cluster shape: ctrl/k8s/${KIND_CONFIG}" >&2
|
||||
echo "available: $(ls k8s/kind-config*.yaml.tpl 2>/dev/null | xargs -n1 basename | tr '\n' ' ')" >&2
|
||||
exit 1
|
||||
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
|
||||
# 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")
|
||||
if grep -q 'audit-policy-file' "$KIND_CONFIG_PATH"; then AUDIT=on; else AUDIT=off; fi
|
||||
}
|
||||
|
||||
# Render a cluster shape to stdout. sed rather than envsubst: envsubst is
|
||||
# gettext-base, absent from a minimal Debian, and Docker is meant to be the only
|
||||
# prerequisite. The variable list is explicit so a template cannot quietly start
|
||||
# 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.
|
||||
render_kind_config() {
|
||||
local host_workdir="${HOST_WORKDIR:-$(cd .. && pwd)}"
|
||||
sed -e "s|\${CLUSTER}|${CLUSTER}|g" \
|
||||
-e "s|\${NODE_IMAGE}|${NODE_IMAGE}|g" \
|
||||
-e "s|\${HTTP_PORT}|${HTTP_PORT}|g" \
|
||||
-e "s|\${HOST_WORKDIR}|${host_workdir}|g" \
|
||||
"$KIND_CONFIG_PATH"
|
||||
}
|
||||
|
||||
_config_restore() {
|
||||
local line
|
||||
while IFS= read -r line; do
|
||||
if [ -n "$line" ]; then
|
||||
eval "export $line"
|
||||
fi
|
||||
done <<< "$1"
|
||||
# A while loop returns its last body command's status; the trailing empty
|
||||
# line would otherwise make this return 1 and trip `set -e` in the caller.
|
||||
return 0
|
||||
}
|
||||
313
rig/ctrl/newbox.sh
Executable file
@@ -0,0 +1,313 @@
|
||||
#!/usr/bin/env bash
|
||||
# Create a disposable Linux environment to validate the installer from a
|
||||
# genuinely clean slate — one that can be thrown away without touching the
|
||||
# environment you actually work in.
|
||||
#
|
||||
# This is the ONLY host-aware file in the tree. Everything else needs just a
|
||||
# Linux with Docker, which is what keeps other host types a later addition
|
||||
# rather than a rewrite.
|
||||
#
|
||||
# On WSL it creates a second distro. There is no .bat and no PowerShell script:
|
||||
# wsl.exe is callable from inside WSL, and wslpath converts the paths it wants.
|
||||
# A machine with no WSL at all needs `wsl --install` run once by hand first —
|
||||
# scripting a reboot-requiring Windows feature install is not worth it.
|
||||
#
|
||||
# Docker: borrowed by default, never installed twice
|
||||
# --------------------------------------------------
|
||||
# WSL2 distros share one kernel and one network stack, so two dockerd instances
|
||||
# contend over docker0 and iptables and can disturb the daemon you depend on.
|
||||
# (That is why Docker Desktop runs one daemon in a dedicated distro and shares
|
||||
# its socket rather than installing one per distro.)
|
||||
#
|
||||
# REUSE_DOCKER=1 (default) borrow the host distro's daemon over /mnt/wsl.
|
||||
# Nothing is installed; nothing can conflict.
|
||||
# Requires `ctrl/dockerhost.sh share` once on the
|
||||
# distro that owns Docker.
|
||||
# REUSE_DOCKER=0 install a second daemon in the new distro. Only
|
||||
# if you specifically want to test a from-scratch
|
||||
# Docker install, and not on a machine you need.
|
||||
#
|
||||
# Borrowing is also the more honest test: rig never installs Docker anyway — it
|
||||
# is the documented prerequisite — so a clean box does not need its own to
|
||||
# exercise everything rig actually does.
|
||||
#
|
||||
# Usage: newbox.sh create | destroy [--purge] | status | shell
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
|
||||
REPO="$(cd .. && pwd)"
|
||||
|
||||
# The distro is named after this environment, and that derived name is the ONLY
|
||||
# thing this script will ever destroy. See guard_name().
|
||||
BOX="${BOX:-${CLUSTER}box}"
|
||||
BOX_USER="${BOX_USER:-dev}"
|
||||
|
||||
# Borrow the host distro's Docker rather than installing a second daemon.
|
||||
REUSE_DOCKER="${REUSE_DOCKER:-1}"
|
||||
SHARED_SOCK=/mnt/wsl/shared-docker/docker.sock
|
||||
|
||||
WSL_EXE=/mnt/c/Windows/System32/wsl.exe
|
||||
|
||||
# ── host detection ─────────────────────────────────────────────────────────
|
||||
|
||||
require_wsl() {
|
||||
if ! grep -qi microsoft /proc/version 2>/dev/null; then
|
||||
cat >&2 <<'EOF'
|
||||
newbox is WSL-only for now.
|
||||
|
||||
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.
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -x "$WSL_EXE" ]; then
|
||||
echo "wsl.exe not found at $WSL_EXE" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
wsl_list() { "$WSL_EXE" -l -q 2>/dev/null | tr -d '\0\r'; }
|
||||
box_exists() { wsl_list | grep -qx "$BOX"; }
|
||||
|
||||
# `wsl --unregister` permanently deletes a distro's filesystem. The whole safety
|
||||
# story is this function: only the name derived from this directory can ever be
|
||||
# a target, so a typo or a stray argument cannot destroy the distro you work in.
|
||||
guard_name() {
|
||||
local derived="${CLUSTER}box"
|
||||
if [ "$BOX" != "$derived" ]; then
|
||||
echo "refusing: BOX='$BOX' is not the name derived from this directory ('$derived')." >&2
|
||||
echo "That guard exists because --unregister is irreversible." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$CLUSTER" ] || [ "$BOX" = "box" ]; then
|
||||
echo "refusing: empty environment name" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ── create ─────────────────────────────────────────────────────────────────
|
||||
|
||||
rootfs_path() {
|
||||
local win_home; win_home=$(wslpath "$("$WSL_EXE" -d "$(wsl_list | head -1)" -e printf '%s' "$USERPROFILE" 2>/dev/null || true)" 2>/dev/null || true)
|
||||
# Simpler and reliable: use the current user's Windows home via /mnt/c.
|
||||
ls -d /mnt/c/Users/*/ 2>/dev/null | grep -viE '/(All Users|Default|Default User|Public)/$' | head -1
|
||||
}
|
||||
|
||||
build_rootfs() {
|
||||
local tar="$1"
|
||||
if [ -f "$tar" ]; then
|
||||
echo " rootfs cached: $(basename "$tar")"
|
||||
return
|
||||
fi
|
||||
echo " exporting a stock Debian rootfs (cached for next time)"
|
||||
local cid; cid=$(docker create debian:trixie-slim)
|
||||
docker export "$cid" > "$tar"
|
||||
docker rm -f "$cid" >/dev/null
|
||||
}
|
||||
|
||||
provision() {
|
||||
echo " provisioning (root)"
|
||||
local hosts_block
|
||||
hosts_block=$(CLUSTER="$CLUSTER" HTTP_PORT="$HTTP_PORT" \
|
||||
envsubst < ./hosts.tmpl 2>/dev/null || sed "s/\${CLUSTER}/$CLUSTER/g" ./hosts.tmpl)
|
||||
|
||||
# Piped as stdin rather than a second script file, the same shape as any
|
||||
# remote provisioning heredoc. Everything here is idempotent so a failed run
|
||||
# can simply be repeated.
|
||||
"$WSL_EXE" -d "$BOX" -u root -- bash -s <<PROVISION
|
||||
set -euo pipefail
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq ca-certificates curl gnupg sudo >/dev/null
|
||||
|
||||
if [ "$REUSE_DOCKER" = "1" ]; then
|
||||
# Borrow the host distro's daemon: CLI only, no dockerd, nothing to
|
||||
# conflict with. The GID must match the owner's or the shared socket is
|
||||
# unreadable here even though it is visible.
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
if [ ! -f /etc/apt/keyrings/docker.asc ]; then
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
|
||||
chmod a+r /etc/apt/keyrings/docker.asc
|
||||
fi
|
||||
echo "deb [arch=\$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \$(. /etc/os-release && echo \$VERSION_CODENAME) stable" \
|
||||
> /etc/apt/sources.list.d/docker.list
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq docker-ce-cli >/dev/null
|
||||
|
||||
echo "export DOCKER_HOST=unix://$SHARED_SOCK" > /etc/profile.d/rig-docker-host.sh
|
||||
|
||||
if [ -f /mnt/wsl/shared-docker/OWNER ]; then
|
||||
gid=\$(awk '/docker gid:/ {print \$3}' /mnt/wsl/shared-docker/OWNER)
|
||||
if [ -n "\$gid" ]; then
|
||||
getent group docker >/dev/null && groupmod -g "\$gid" docker || groupadd -g "\$gid" docker
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# A second daemon. Only when deliberately testing a from-scratch install.
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
if [ ! -f /etc/apt/keyrings/docker.asc ]; then
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
|
||||
chmod a+r /etc/apt/keyrings/docker.asc
|
||||
fi
|
||||
echo "deb [arch=\$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \$(. /etc/os-release && echo \$VERSION_CODENAME) stable" \
|
||||
> /etc/apt/sources.list.d/docker.list
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq docker-ce docker-ce-cli containerd.io >/dev/null
|
||||
fi
|
||||
|
||||
id -u "$BOX_USER" >/dev/null 2>&1 || useradd -m -s /bin/bash "$BOX_USER"
|
||||
usermod -aG sudo,docker "$BOX_USER"
|
||||
echo "$BOX_USER ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/90-$BOX_USER
|
||||
chmod 0440 /etc/sudoers.d/90-$BOX_USER
|
||||
|
||||
# systemd is off by default in WSL, and Docker needs it. Takes effect on the
|
||||
# next start of this distro, which is why create() terminates it below.
|
||||
cat > /etc/wsl.conf <<WSLCONF
|
||||
[boot]
|
||||
systemd=true
|
||||
|
||||
[user]
|
||||
default=$BOX_USER
|
||||
WSLCONF
|
||||
|
||||
# The default inotify limits are low enough that file watching silently stops
|
||||
# working — no error, changes just stop being noticed. Fix it before it bites.
|
||||
cat > /etc/sysctl.d/99-rig.conf <<SYSCTL
|
||||
fs.inotify.max_user_watches=524288
|
||||
fs.inotify.max_user_instances=512
|
||||
SYSCTL
|
||||
|
||||
if ! grep -q 'rig environment' /etc/hosts 2>/dev/null; then
|
||||
{ echo ""; echo "# rig environment"; cat <<'HOSTS'
|
||||
$hosts_block
|
||||
HOSTS
|
||||
} >> /etc/hosts
|
||||
fi
|
||||
|
||||
touch /etc/rig-provisioned
|
||||
PROVISION
|
||||
}
|
||||
|
||||
create() {
|
||||
require_wsl
|
||||
guard_name
|
||||
|
||||
local winhome; winhome=$(rootfs_path)
|
||||
[ -n "$winhome" ] || { echo "could not locate the Windows user directory" >&2; exit 1; }
|
||||
local tar="${winhome}rig-rootfs.tar"
|
||||
local installdir="${winhome}WSL/${BOX}"
|
||||
|
||||
echo "creating '$BOX'"
|
||||
if [ "$REUSE_DOCKER" = "1" ]; then
|
||||
echo " docker: borrowing the host distro's daemon (nothing installed)"
|
||||
if [ ! -S "$SHARED_SOCK" ]; then
|
||||
echo
|
||||
echo " No shared socket yet. In the distro that owns Docker, run once:"
|
||||
echo " sudo bash ctrl/dockerhost.sh share"
|
||||
echo " That adds one systemd drop-in and nothing else; undo with 'unshare'."
|
||||
echo " Continuing — the box will be created, but Docker won't work in it"
|
||||
echo " until you do that."
|
||||
fi
|
||||
else
|
||||
echo
|
||||
echo " REUSE_DOCKER=0: installing a SECOND Docker daemon."
|
||||
echo " WSL distros share a network stack, so this can disturb Docker in"
|
||||
echo " the distro you work in. Ctrl-C now if that is a bad trade today."
|
||||
echo
|
||||
sleep 4
|
||||
fi
|
||||
echo
|
||||
|
||||
if box_exists; then
|
||||
echo " distro already registered"
|
||||
else
|
||||
build_rootfs "$tar"
|
||||
mkdir -p "$installdir"
|
||||
"$WSL_EXE" --import "$BOX" "$(wslpath -w "$installdir")" "$(wslpath -w "$tar")" --version 2
|
||||
fi
|
||||
|
||||
# Resumable: a partially-created box is finished rather than restarted.
|
||||
if "$WSL_EXE" -d "$BOX" -u root -- test -f /etc/rig-provisioned 2>/dev/null; then
|
||||
echo " already provisioned"
|
||||
else
|
||||
provision
|
||||
echo " restarting the distro so systemd and group membership apply"
|
||||
"$WSL_EXE" --terminate "$BOX" # ONLY this distro; never --shutdown
|
||||
fi
|
||||
|
||||
echo " copying rig in"
|
||||
tar c -C "$REPO" --exclude=def --exclude=.git --exclude=ctrl/.env . \
|
||||
| "$WSL_EXE" -d "$BOX" -u "$BOX_USER" -- bash -lc "mkdir -p ~/rig && tar x -C ~/rig"
|
||||
|
||||
echo
|
||||
echo " docker: $("$WSL_EXE" -d "$BOX" -u "$BOX_USER" -- bash -lc 'systemctl is-active docker 2>/dev/null || echo inactive')"
|
||||
echo
|
||||
echo "next:"
|
||||
echo " make newbox shell # a shell inside it"
|
||||
echo " then: cd ~/rig && make station && 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):"
|
||||
CLUSTER="$CLUSTER" envsubst < ./hosts.tmpl 2>/dev/null | grep -v '^#' | grep -v '^$' | sed 's/^/ /'
|
||||
}
|
||||
|
||||
# ── the rest ───────────────────────────────────────────────────────────────
|
||||
|
||||
destroy() {
|
||||
require_wsl
|
||||
guard_name
|
||||
|
||||
if ! box_exists; then
|
||||
echo "no distro '$BOX' to remove"
|
||||
else
|
||||
echo "about to PERMANENTLY delete the distro '$BOX' and its filesystem."
|
||||
"$WSL_EXE" --terminate "$BOX" 2>/dev/null || true
|
||||
"$WSL_EXE" --unregister "$BOX"
|
||||
echo " unregistered"
|
||||
fi
|
||||
|
||||
local winhome; winhome=$(rootfs_path)
|
||||
rm -rf "${winhome}WSL/${BOX}" 2>/dev/null || true
|
||||
|
||||
if [ "${1:-}" = "--purge" ]; then
|
||||
rm -f "${winhome}rig-rootfs.tar"
|
||||
echo " cached rootfs removed"
|
||||
fi
|
||||
}
|
||||
|
||||
status() {
|
||||
require_wsl
|
||||
echo "environment $CLUSTER"
|
||||
echo "distro $BOX"
|
||||
if box_exists; then
|
||||
echo "registered yes"
|
||||
echo "provisioned $("$WSL_EXE" -d "$BOX" -u root -- test -f /etc/rig-provisioned 2>/dev/null && echo yes || echo no)"
|
||||
echo "docker $("$WSL_EXE" -d "$BOX" -u root -- bash -lc 'systemctl is-active docker 2>/dev/null' || echo unknown)"
|
||||
echo "rig copied $("$WSL_EXE" -d "$BOX" -u "$BOX_USER" -- bash -lc 'test -f ~/rig/Makefile && echo yes || echo no' 2>/dev/null)"
|
||||
else
|
||||
echo "registered no"
|
||||
fi
|
||||
echo
|
||||
echo "all distros (this one is never touched unless it is '$BOX'):"
|
||||
wsl_list | sed 's/^/ /'
|
||||
}
|
||||
|
||||
shell() {
|
||||
require_wsl
|
||||
box_exists || { echo "no distro '$BOX' — run 'make newbox' first" >&2; exit 1; }
|
||||
"$WSL_EXE" -d "$BOX" -u "$BOX_USER" --cd '~'
|
||||
}
|
||||
|
||||
case "${1:-status}" in
|
||||
create) create ;;
|
||||
destroy) shift; destroy "${1:-}" ;;
|
||||
status) status ;;
|
||||
shell) shell ;;
|
||||
*) echo "usage: $0 [create|destroy [--purge]|status|shell]" >&2; exit 1 ;;
|
||||
esac
|
||||
103
rig/ctrl/ports.sh
Executable file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env bash
|
||||
# Give each environment its own block of host ports.
|
||||
#
|
||||
# New versions of a system mean new clusters on ONE machine, not new machines.
|
||||
# Cluster name, kubectl context, registry container and image tag already derive
|
||||
# from the directory name, so two copies never collide there — but host ports are
|
||||
# a single shared namespace and would.
|
||||
#
|
||||
# The block is derived from the directory name: stateless, stable, and requiring
|
||||
# no coordination between copies that know nothing about each other.
|
||||
#
|
||||
# base = 20000 + (hash(slug) % 200) * 10
|
||||
# +0 HTTP +1 HTTPS +2 TILT +3 REGISTRY (+4..9 reserved)
|
||||
#
|
||||
# 20000+ deliberately avoids the ports something is already likely to hold: 80,
|
||||
# 443, 3000, 5432, 8000, 8080.
|
||||
#
|
||||
# Derivation is a default, not a decision. On first use the resolved block is
|
||||
# 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.
|
||||
#
|
||||
# Usage: ports.sh show | derive | persist
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
source ./lib/config.sh
|
||||
|
||||
# derive_port_base lives in lib/config.sh so every script resolves the same block
|
||||
# without going through this one.
|
||||
derive_base() { derive_port_base "$1"; }
|
||||
|
||||
derive() {
|
||||
load_config
|
||||
local base; base=$(derive_base "$CLUSTER")
|
||||
DERIVED_HTTP=$base
|
||||
DERIVED_HTTPS=$((base + 1))
|
||||
DERIVED_TILT=$((base + 2))
|
||||
DERIVED_REGISTRY=$((base + 3))
|
||||
}
|
||||
|
||||
show() {
|
||||
derive
|
||||
echo "environment $CLUSTER"
|
||||
echo "derived base $(derive_base "$CLUSTER")"
|
||||
echo
|
||||
printf " %-14s %-8s %-8s %s\n" KEY DERIVED ACTIVE SOURCE
|
||||
_row HTTP_PORT "$DERIVED_HTTP"
|
||||
_row HTTPS_PORT "$DERIVED_HTTPS"
|
||||
_row TILT_PORT "$DERIVED_TILT"
|
||||
_row REGISTRY_PORT "$DERIVED_REGISTRY"
|
||||
}
|
||||
|
||||
_row() {
|
||||
local key="$1" derived="$2" active="${!1:-}" src="derived"
|
||||
if [ -n "$active" ] && [ "$active" != "$derived" ]; then
|
||||
src="override"
|
||||
elif [ -z "$active" ]; then
|
||||
active="$derived"
|
||||
fi
|
||||
printf " %-14s %-8s %-8s %s\n" "$key" "$derived" "$active" "$src"
|
||||
}
|
||||
|
||||
# Write the derived block into ctrl/.env, once. Existing keys are never
|
||||
# rewritten — an override stays an override.
|
||||
persist() {
|
||||
derive
|
||||
[ -f ./.env ] || cp ./.env.example ./.env
|
||||
|
||||
local wrote=0 key val
|
||||
for key in HTTP_PORT:$DERIVED_HTTP \
|
||||
HTTPS_PORT:$DERIVED_HTTPS \
|
||||
TILT_PORT:$DERIVED_TILT \
|
||||
REGISTRY_PORT:$DERIVED_REGISTRY; do
|
||||
val="${key#*:}"; key="${key%%:*}"
|
||||
if grep -qE "^${key}=[0-9]" ./.env 2>/dev/null; then
|
||||
continue
|
||||
fi
|
||||
if [ "$wrote" -eq 0 ]; then
|
||||
{
|
||||
echo ""
|
||||
echo "# Port block for this environment, derived from the directory name"
|
||||
echo "# so copies never collide. Pinned here on first use — edit freely."
|
||||
} >> ./.env
|
||||
wrote=1
|
||||
fi
|
||||
# Replace a commented/empty placeholder if present, else append.
|
||||
if grep -qE "^#?\s*${key}=" ./.env 2>/dev/null; then
|
||||
sed -i "s|^#\?\s*${key}=.*|${key}=${val}|" ./.env
|
||||
else
|
||||
echo "${key}=${val}" >> ./.env
|
||||
fi
|
||||
done
|
||||
|
||||
[ "$wrote" -eq 1 ] && echo "pinned port block into ctrl/.env" || echo "ports already set in ctrl/.env"
|
||||
return 0
|
||||
}
|
||||
|
||||
case "${1:-show}" in
|
||||
show) show ;;
|
||||
derive) derive; echo "$DERIVED_HTTP $DERIVED_HTTPS $DERIVED_TILT $DERIVED_REGISTRY" ;;
|
||||
persist) persist ;;
|
||||
*) echo "usage: $0 [show|derive|persist]" >&2; exit 1 ;;
|
||||
esac
|
||||
216
rig/ctrl/registry.sh
Executable file
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env bash
|
||||
# Registry plumbing. THIS is the seam — not a tool.
|
||||
#
|
||||
# Four modes, selected by REGISTRY_MODE in the active profile:
|
||||
#
|
||||
# none Tilt builds straight into the node. No registry at all — and so no
|
||||
# guard against an outward push: an unqualified image name means
|
||||
# docker.io/library/<name>, and only Tilt's kind detection stands
|
||||
# between that and a real push. Throwaway use only; every profile
|
||||
# here now defaults to `local` instead.
|
||||
# local a registry:2 container wired into the cluster.
|
||||
# mirror the same container, but configured as a pull-through CACHE of the
|
||||
# corporate registry. What a locked-down client actually looks like:
|
||||
# images originate from corp, you don't hammer it, and you keep
|
||||
# working when the VPN drops.
|
||||
# remote no local container; pull straight from the corporate registry using
|
||||
# an imagePullSecret.
|
||||
#
|
||||
# Deliberately a script rather than a tool. ctlptl collapses the `local` wiring
|
||||
# into one line, but its Registry spec only accepts name/port/image/listenAddress
|
||||
# — there is no way to set REGISTRY_PROXY_REMOTEURL, so it cannot express
|
||||
# `mirror` at all. Keeping the seam here is what keeps the corporate registry
|
||||
# swappable.
|
||||
#
|
||||
# Usage: registry.sh up | down | status
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
|
||||
REG_NAME="${CLUSTER}-registry"
|
||||
REG_PORT="${REGISTRY_PORT:-5005}"
|
||||
K="kubectl --context ${KUBECONTEXT}"
|
||||
|
||||
# ── CA trust ───────────────────────────────────────────────────────────────
|
||||
# A corporate registry is almost always fronted by an internal CA, and trust has
|
||||
# to reach three separate places. Nothing does this for you, and the symptom when
|
||||
# it's missing is an opaque:
|
||||
# x509: certificate signed by unknown authority
|
||||
#
|
||||
# 1. the host docker daemon — /etc/docker/certs.d/<host>/ca.crt (needs root)
|
||||
# 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
|
||||
# since it needs root. (3) belongs to the workload.
|
||||
install_ca_into_nodes() {
|
||||
[ -n "${REGISTRY_CA_FILE:-}" ] || return 0
|
||||
|
||||
if [ ! -r "$REGISTRY_CA_FILE" ]; then
|
||||
echo "REGISTRY_CA_FILE is set but not readable: $REGISTRY_CA_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " distributing CA to kind nodes"
|
||||
local node
|
||||
for node in $(kind get nodes --name "$CLUSTER"); do
|
||||
docker cp "$REGISTRY_CA_FILE" "$node:/usr/local/share/ca-certificates/corp-registry.crt"
|
||||
docker exec "$node" update-ca-certificates >/dev/null 2>&1
|
||||
docker exec "$node" systemctl restart containerd
|
||||
done
|
||||
}
|
||||
|
||||
# Point containerd at a registry host. The cluster config already set
|
||||
# config_path=/etc/containerd/certs.d, so this is a per-node drop-in and needs no
|
||||
# cluster recreate — which is what lets registry mode change on a live cluster.
|
||||
write_hosts_toml() {
|
||||
local host="$1" upstream="$2" skip_verify="${3:-false}"
|
||||
local node
|
||||
for node in $(kind get nodes --name "$CLUSTER"); do
|
||||
docker exec "$node" mkdir -p "/etc/containerd/certs.d/${host}"
|
||||
docker exec -i "$node" cp /dev/stdin "/etc/containerd/certs.d/${host}/hosts.toml" <<TOML
|
||||
server = "${upstream}"
|
||||
|
||||
[host."${upstream}"]
|
||||
capabilities = ["pull", "resolve"]
|
||||
skip_verify = ${skip_verify}
|
||||
TOML
|
||||
done
|
||||
}
|
||||
|
||||
# ── the local container (local + mirror) ───────────────────────────────────
|
||||
|
||||
start_registry_container() {
|
||||
if [ "$(docker inspect -f '{{.State.Running}}' "$REG_NAME" 2>/dev/null || true)" = "true" ]; then
|
||||
echo " registry container '$REG_NAME' already running"
|
||||
return
|
||||
fi
|
||||
docker rm -f "$REG_NAME" >/dev/null 2>&1 || true
|
||||
|
||||
local args=(-d --restart=always --name "$REG_NAME"
|
||||
-p "127.0.0.1:${REG_PORT}:5000")
|
||||
|
||||
if [ "$REGISTRY_MODE" = "mirror" ]; then
|
||||
if [ -z "${REGISTRY_REMOTE_URL:-}" ]; then
|
||||
echo "REGISTRY_MODE=mirror needs REGISTRY_REMOTE_URL in ctrl/.env" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " starting pull-through cache of ${REGISTRY_REMOTE_URL}"
|
||||
args+=(-e "REGISTRY_PROXY_REMOTEURL=${REGISTRY_REMOTE_URL}")
|
||||
[ -n "${REGISTRY_USER:-}" ] && args+=(-e "REGISTRY_PROXY_USERNAME=${REGISTRY_USER}")
|
||||
[ -n "${REGISTRY_PASSWORD:-}" ] && args+=(-e "REGISTRY_PROXY_PASSWORD=${REGISTRY_PASSWORD}")
|
||||
if [ -n "${REGISTRY_CA_FILE:-}" ]; then
|
||||
args+=(-v "$(readlink -f "$REGISTRY_CA_FILE"):/etc/ssl/certs/corp-ca.crt:ro")
|
||||
fi
|
||||
else
|
||||
echo " starting local registry"
|
||||
fi
|
||||
|
||||
docker run "${args[@]}" "$REGISTRY_IMAGE" >/dev/null
|
||||
}
|
||||
|
||||
# The registry must share a network with the nodes so they can resolve it by
|
||||
# container name; localhost inside a node is the node, not the host.
|
||||
join_kind_network() {
|
||||
if docker inspect -f '{{json .NetworkSettings.Networks}}' "$REG_NAME" | grep -q '"kind"'; then
|
||||
return
|
||||
fi
|
||||
docker network connect kind "$REG_NAME" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# The documented contract that tells tooling (Tilt, skaffold) where the local
|
||||
# registry is, so they don't have to be configured separately.
|
||||
apply_hosting_configmap() {
|
||||
$K apply -f - <<YAML >/dev/null
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: local-registry-hosting
|
||||
namespace: kube-public
|
||||
data:
|
||||
localRegistryHosting.v1: |
|
||||
host: "localhost:${REG_PORT}"
|
||||
help: "https://kind.sigs.k8s.io/docs/user/local-registry/"
|
||||
YAML
|
||||
}
|
||||
|
||||
# ── modes ──────────────────────────────────────────────────────────────────
|
||||
|
||||
up() {
|
||||
echo "registry: ${REGISTRY_MODE}"
|
||||
case "$REGISTRY_MODE" in
|
||||
none)
|
||||
echo " no registry — images are built straight into the node"
|
||||
;;
|
||||
|
||||
local|mirror)
|
||||
start_registry_container
|
||||
join_kind_network
|
||||
install_ca_into_nodes
|
||||
# Nodes reach the registry by container name on the shared network;
|
||||
# the host reaches it on localhost:PORT. Both names must resolve.
|
||||
write_hosts_toml "localhost:${REG_PORT}" "http://${REG_NAME}:5000"
|
||||
if [ "$REGISTRY_MODE" = "mirror" ]; then
|
||||
# Anything asking for docker.io transparently goes to the cache.
|
||||
write_hosts_toml "docker.io" "http://${REG_NAME}:5000"
|
||||
fi
|
||||
apply_hosting_configmap
|
||||
echo " ready at localhost:${REG_PORT}"
|
||||
;;
|
||||
|
||||
remote)
|
||||
if [ -z "${REGISTRY_REMOTE_URL:-}" ]; then
|
||||
echo "REGISTRY_MODE=remote needs REGISTRY_REMOTE_URL in ctrl/.env" >&2
|
||||
exit 1
|
||||
fi
|
||||
install_ca_into_nodes
|
||||
local host="${REGISTRY_REMOTE_URL#*://}"; host="${host%%/*}"
|
||||
if [ -n "${REGISTRY_USER:-}" ]; then
|
||||
echo " creating imagePullSecret for ${host}"
|
||||
$K create secret docker-registry regcred \
|
||||
--docker-server="$host" \
|
||||
--docker-username="$REGISTRY_USER" \
|
||||
--docker-password="$REGISTRY_PASSWORD" \
|
||||
--dry-run=client -o yaml | $K apply -f - >/dev/null
|
||||
# Attach to the default ServiceAccount so plain pods inherit it.
|
||||
$K patch serviceaccount default \
|
||||
-p '{"imagePullSecrets":[{"name":"regcred"}]}' >/dev/null
|
||||
fi
|
||||
echo " pulling directly from ${host}"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "unknown REGISTRY_MODE '$REGISTRY_MODE' (expected none|local|mirror|remote)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
down() {
|
||||
if docker inspect "$REG_NAME" >/dev/null 2>&1; then
|
||||
echo "removing registry container '$REG_NAME'"
|
||||
docker rm -f "$REG_NAME" >/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
status() {
|
||||
echo "mode ${REGISTRY_MODE}"
|
||||
if docker inspect "$REG_NAME" >/dev/null 2>&1; then
|
||||
echo "container ${REG_NAME} $(docker inspect -f '{{.State.Status}}' "$REG_NAME")"
|
||||
echo "endpoint localhost:${REG_PORT}"
|
||||
else
|
||||
echo "container none"
|
||||
fi
|
||||
[ -n "${REGISTRY_REMOTE_URL:-}" ] && echo "upstream ${REGISTRY_REMOTE_URL}"
|
||||
[ -n "${REGISTRY_CA_FILE:-}" ] && echo "ca ${REGISTRY_CA_FILE}"
|
||||
return 0
|
||||
}
|
||||
|
||||
case "${1:-status}" in
|
||||
up) up ;;
|
||||
down) down ;;
|
||||
status) status ;;
|
||||
*) echo "usage: $0 [up|down|status]" >&2; exit 1 ;;
|
||||
esac
|
||||
249
rig/ctrl/setup.sh
Executable file
@@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prepare a machine to run rig, and say plainly what worked, what was already
|
||||
# done, and what is left for a human.
|
||||
#
|
||||
# This is the grouped entry point: `make setup`. Every step is idempotent and
|
||||
# independently checked, so running it twice is safe and running it on a
|
||||
# half-configured machine finishes the job rather than starting over.
|
||||
#
|
||||
# It deliberately does NOT abort on the first failure. A setup script that dies
|
||||
# at step 2 hides the fact that steps 4 and 5 were also going to fail — and on
|
||||
# an unfamiliar machine, the full picture is the whole point. Failures are
|
||||
# collected and reported together, and the exit code reflects the worst outcome.
|
||||
#
|
||||
# The same script runs inside a fresh throwaway distro (newbox), so the
|
||||
# provisioning path and the everyday path cannot drift apart.
|
||||
#
|
||||
# Usage:
|
||||
# setup.sh # host checks + the dev toolchain
|
||||
# setup.sh core # kubectl and jq only — no cluster tooling
|
||||
# setup.sh --share-docker # ...and offer this distro's Docker to others
|
||||
# setup.sh --cluster # ...and bring the cluster up
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
|
||||
WITH_SHARE=0
|
||||
WITH_CLUSTER=0
|
||||
# Cluster tooling is not wanted everywhere: a managed or corporate-issued
|
||||
# machine may legitimately want kubectl and nothing that builds clusters.
|
||||
TIER=dev
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
core|dev) TIER="$a" ;;
|
||||
--share-docker) WITH_SHARE=1 ;;
|
||||
--cluster) WITH_CLUSTER=1 ;;
|
||||
*) echo "unknown option: $a" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$TIER" = "core" ] && [ "$WITH_CLUSTER" -eq 1 ]; then
|
||||
echo "core tier installs no cluster tooling, so --cluster cannot work" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── step framework ─────────────────────────────────────────────────────────
|
||||
# Statuses are deliberately distinct: "already" and "done" both mean success but
|
||||
# tell you very different things about the machine you are on.
|
||||
STEP_NAMES=()
|
||||
STEP_STATUS=()
|
||||
STEP_NOTE=()
|
||||
WORST=0
|
||||
|
||||
record() {
|
||||
STEP_NAMES+=("$1"); STEP_STATUS+=("$2"); STEP_NOTE+=("${3:-}")
|
||||
# Only a genuine failure is a non-zero exit. "manual" means the machine is
|
||||
# fine and you have something to do — reporting that as an error makes the
|
||||
# whole run look broken and trains people to ignore the output.
|
||||
[ "$2" = "fail" ] && WORST=1 || true
|
||||
local mark
|
||||
case "$2" in
|
||||
already) mark=" ok " ;;
|
||||
done) mark=" done " ;;
|
||||
skip) mark=" skip " ;;
|
||||
manual) mark="MANUAL" ;;
|
||||
fail) mark=" FAIL " ;;
|
||||
esac
|
||||
printf "[%s] %-22s %s\n" "$mark" "$1" "${3:-}"
|
||||
}
|
||||
|
||||
# ── steps ──────────────────────────────────────────────────────────────────
|
||||
|
||||
step_host() {
|
||||
local out
|
||||
if ! out=$(bash ./wizard.sh detect 2>&1); then
|
||||
record host fail "detection failed"
|
||||
return
|
||||
fi
|
||||
# Anything the wizard 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"
|
||||
if [ "$warns" -gt 0 ]; then
|
||||
record host manual "$warns item(s) need attention — see below"
|
||||
else
|
||||
record host already "no problems detected"
|
||||
fi
|
||||
}
|
||||
|
||||
step_toolchain() {
|
||||
local want="kubectl jq"
|
||||
[ "$TIER" = "dev" ] && want="$want kind tilt"
|
||||
|
||||
local missing=""
|
||||
for b in $want; do
|
||||
command -v "$b" >/dev/null 2>&1 || missing="$missing $b"
|
||||
done
|
||||
|
||||
if [ -z "$missing" ]; then
|
||||
record toolchain already "$TIER: $want"
|
||||
return
|
||||
fi
|
||||
|
||||
if bash ./wizard.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"
|
||||
done
|
||||
if [ -n "$still" ]; then
|
||||
record toolchain fail "still missing:$still (see /tmp/rig-deps.$$)"
|
||||
else
|
||||
record toolchain done "$TIER, installed:$missing"
|
||||
rm -f "/tmp/rig-deps.$$"
|
||||
fi
|
||||
else
|
||||
record toolchain fail "install failed — see /tmp/rig-deps.$$"
|
||||
fi
|
||||
}
|
||||
|
||||
step_path() {
|
||||
local bin="${OUT_BIN:-$HOME/.local/bin}"
|
||||
case ":$PATH:" in
|
||||
*":$bin:"*) ;;
|
||||
*) record path manual "add to ~/.bashrc: export PATH=\"$bin:\$PATH\""; return ;;
|
||||
esac
|
||||
if grep -qs "$bin" "$HOME/.bashrc" "$HOME/.profile" 2>/dev/null; then
|
||||
record path already "$bin on PATH and persisted"
|
||||
else
|
||||
record path manual "on PATH now, but not persisted in ~/.bashrc"
|
||||
fi
|
||||
}
|
||||
|
||||
step_docker() {
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
record docker fail "no docker cli — this is the one prerequisite rig cannot install"
|
||||
return
|
||||
fi
|
||||
if docker info >/dev/null 2>&1; then
|
||||
record docker already "$(docker version --format '{{.Server.Version}}' 2>/dev/null)"
|
||||
else
|
||||
record docker fail "daemon unreachable (in the docker group? logged out and back in?)"
|
||||
fi
|
||||
}
|
||||
|
||||
step_share_docker() {
|
||||
if [ "$WITH_SHARE" -ne 1 ]; then
|
||||
record docker-share skip "not requested (--share-docker)"
|
||||
return
|
||||
fi
|
||||
if ! grep -qi microsoft /proc/version 2>/dev/null; then
|
||||
record docker-share skip "not WSL — sharing only applies between WSL distros"
|
||||
return
|
||||
fi
|
||||
if [ -f /etc/systemd/system/docker.service.d/10-rig-shared-socket.conf ]; then
|
||||
record docker-share already "this distro is offering its Docker to others"
|
||||
return
|
||||
fi
|
||||
# Needs root, and asking mid-script is worse than telling the user the
|
||||
# single command to run.
|
||||
if [ "$(id -u)" -ne 0 ] && ! sudo -n true 2>/dev/null; then
|
||||
record docker-share manual "run: sudo bash ctrl/dockerhost.sh share"
|
||||
return
|
||||
fi
|
||||
if sudo bash ./dockerhost.sh share >/tmp/rig-share.$$ 2>&1; then
|
||||
record docker-share done "this distro now owns the shared Docker"
|
||||
rm -f "/tmp/rig-share.$$"
|
||||
else
|
||||
record docker-share fail "see /tmp/rig-share.$$"
|
||||
fi
|
||||
}
|
||||
|
||||
step_ports() {
|
||||
local busy=""
|
||||
for entry in "HTTP:$HTTP_PORT" "HTTPS:$HTTPS_PORT" "TILT:$TILT_PORT" "REGISTRY:$REGISTRY_PORT"; do
|
||||
local p="${entry#*:}"
|
||||
if command -v ss >/dev/null 2>&1 && ss -ltn "sport = :$p" 2>/dev/null | grep -q LISTEN; then
|
||||
busy="$busy ${entry%%:*}($p)"
|
||||
fi
|
||||
done
|
||||
if [ -n "$busy" ]; then
|
||||
record ports fail "in use:$busy — override in ctrl/.env or rename the directory"
|
||||
else
|
||||
record ports already "$HTTP_PORT-$REGISTRY_PORT free"
|
||||
fi
|
||||
}
|
||||
|
||||
step_cluster() {
|
||||
if [ "$TIER" = "core" ]; then
|
||||
record cluster skip "core tier — no cluster tooling on this machine"
|
||||
return
|
||||
fi
|
||||
if [ "$WITH_CLUSTER" -ne 1 ]; then
|
||||
record cluster skip "not requested (--cluster)"
|
||||
return
|
||||
fi
|
||||
if kind get clusters 2>/dev/null | grep -qx "$CLUSTER"; then
|
||||
record cluster already "'$CLUSTER' exists"
|
||||
return
|
||||
fi
|
||||
if bash ./cluster.sh up >/tmp/rig-cluster.$$ 2>&1; then
|
||||
record cluster done "'$CLUSTER' created"
|
||||
rm -f "/tmp/rig-cluster.$$"
|
||||
else
|
||||
record cluster fail "see /tmp/rig-cluster.$$"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── run ────────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "setting up '$CLUSTER'"
|
||||
echo
|
||||
HOST_DETAIL=""
|
||||
step_host
|
||||
step_toolchain
|
||||
step_path
|
||||
step_docker
|
||||
step_share_docker
|
||||
step_ports
|
||||
step_cluster
|
||||
|
||||
echo
|
||||
if [ -n "$HOST_DETAIL" ]; then
|
||||
echo "host detail"
|
||||
echo "$HOST_DETAIL" | sed 's/^/ /'
|
||||
echo
|
||||
fi
|
||||
|
||||
# Repeat only what still needs action, so the tail of the output is a to-do list
|
||||
# rather than a transcript.
|
||||
outstanding=0
|
||||
for i in "${!STEP_NAMES[@]}"; do
|
||||
case "${STEP_STATUS[$i]}" in
|
||||
fail|manual)
|
||||
[ "$outstanding" -eq 0 ] && echo "outstanding:"
|
||||
outstanding=1
|
||||
printf " %-8s %-16s %s\n" "${STEP_STATUS[$i]}" "${STEP_NAMES[$i]}" "${STEP_NOTE[$i]}"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$outstanding" -eq 0 ]; then
|
||||
echo "ready. next: make cluster up && make docs"
|
||||
else
|
||||
echo
|
||||
echo "(nothing was aborted — every step ran so the list above is complete)"
|
||||
fi
|
||||
|
||||
exit "$WORST"
|
||||
106
rig/ctrl/station.sh
Executable file
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env bash
|
||||
# Station check: is this workstation 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
|
||||
# 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}"
|
||||
|
||||
# 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
|
||||
|
||||
# ── repo-level checks ──────────────────────────────────────────────────────
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
|
||||
echo
|
||||
echo "config"
|
||||
echo " profile ${PROFILE_NAME} (nodes=${NODES} audit=${AUDIT})"
|
||||
echo " cluster ${CLUSTER} (context ${KUBECONTEXT})"
|
||||
echo " registry ${REGISTRY_MODE}"
|
||||
echo " ingress ${INGRESS_MODE}"
|
||||
|
||||
if [ ! -f ./.env ]; then
|
||||
echo " ! ctrl/.env missing — copy it: cp ctrl/.env.example ctrl/.env"
|
||||
fi
|
||||
|
||||
# A 3-node profile on a box that's already full is the most common first
|
||||
# failure, and it presents as pods stuck Pending rather than anything obvious.
|
||||
avail=$(awk '/^MemAvailable:/{printf "%d", $2/1024/1024}' /proc/meminfo)
|
||||
need=$((NODES * 2))
|
||||
if [ "$avail" -lt "$need" ]; then
|
||||
echo " ! profile '${PROFILE_NAME}' wants ~${need} GB, ${avail} GB available"
|
||||
echo " 'make cluster list' shows what else is running; 'make cluster free' stops it"
|
||||
fi
|
||||
|
||||
# The CA reaches three places and only one of them is ours. Report the other two.
|
||||
if [ -n "${REGISTRY_CA_FILE:-}" ]; then
|
||||
echo
|
||||
echo "registry CA"
|
||||
if [ ! -r "$REGISTRY_CA_FILE" ]; then
|
||||
echo " ! REGISTRY_CA_FILE not readable: $REGISTRY_CA_FILE"
|
||||
else
|
||||
echo " file $REGISTRY_CA_FILE"
|
||||
host="${REGISTRY_REMOTE_URL#*://}"; host="${host%%/*}"
|
||||
if [ -n "$host" ] && [ ! -f "/etc/docker/certs.d/${host}/ca.crt" ]; then
|
||||
echo " ! the HOST docker daemon does not trust it yet:"
|
||||
echo " sudo mkdir -p /etc/docker/certs.d/${host}"
|
||||
echo " sudo cp ${REGISTRY_CA_FILE} /etc/docker/certs.d/${host}/ca.crt"
|
||||
echo " (kind nodes are handled by registry.sh; in-cluster clients are the workload's job)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Host ports this environment will try to bind. Checked before cluster creation
|
||||
# because docker reports a clash halfway through, as an opaque
|
||||
# "failed to bind host port ...: address already in use".
|
||||
echo
|
||||
echo "ports (block derived from the directory name — see 'make ports')"
|
||||
|
||||
port_busy() {
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
ss -ltn "sport = :$1" 2>/dev/null | grep -q LISTEN && return 0 || return 1
|
||||
fi
|
||||
# iproute2 is absent from a minimal Debian, so fall back to procfs rather
|
||||
# than silently reporting everything as free.
|
||||
local hex; hex=$(printf ':%04X' "$1")
|
||||
grep -qi "^ *[0-9]*: [0-9A-F]*$hex " /proc/net/tcp /proc/net/tcp6 2>/dev/null
|
||||
}
|
||||
|
||||
# A port held by THIS environment's own cluster is not a clash — it is the thing
|
||||
# working. Reporting it as a problem every time the cluster is up would train
|
||||
# people to ignore this section, which is the opposite of the point.
|
||||
# Extract with a second grep rather than `tr -d ':->'`: in tr, ':->' is the
|
||||
# character RANGE ':' to '>', which does not contain '-', so the trailing dash
|
||||
# survives and nothing ever matches.
|
||||
ours=$(docker ps --filter "label=io.x-k8s.kind.cluster=${CLUSTER}" \
|
||||
--format '{{.Ports}}' 2>/dev/null | tr ',' '\n' \
|
||||
| grep -oE ':[0-9]+->' | grep -oE '[0-9]+' || true)
|
||||
|
||||
clash=0
|
||||
for entry in "HTTP:${HTTP_PORT}" "HTTPS:${HTTPS_PORT}" \
|
||||
"TILT:${TILT_PORT}" "REGISTRY:${REGISTRY_PORT}"; do
|
||||
name="${entry%%:*}"; p="${entry#*:}"
|
||||
[ -n "$p" ] || continue
|
||||
if ! port_busy "$p"; then
|
||||
printf " %-9s %-6s free\n" "$name" "$p"
|
||||
elif echo "$ours" | grep -qx "$p"; then
|
||||
printf " %-9s %-6s in use by this environment's cluster\n" "$name" "$p"
|
||||
else
|
||||
printf " ! %-9s %-6s IN USE by something else\n" "$name" "$p"
|
||||
clash=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$clash" -eq 1 ]; then
|
||||
echo " override the clashing one in ctrl/.env, e.g. HTTP_PORT=21080"
|
||||
echo " (or rename this directory — the whole block follows the name)"
|
||||
fi
|
||||
54
rig/ctrl/versions.env
Normal file
@@ -0,0 +1,54 @@
|
||||
# Pinned toolchain — the single manifest the wizard installs from.
|
||||
# Every entry is a single binary; none of them needs an apt repo.
|
||||
# kubectl fully static
|
||||
# kind libc only
|
||||
# tilt libc + libstdc++ + libgcc (present in base Debian)
|
||||
# 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.
|
||||
|
||||
KIND_VERSION=v0.32.0
|
||||
KIND_SHA256=50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54
|
||||
KIND_URL=https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/kind-linux-amd64
|
||||
|
||||
KUBECTL_VERSION=v1.36.3
|
||||
KUBECTL_SHA256=ebbd080e7c2e275093b55915722043257eb24004363e20acb3c4d71919f88336
|
||||
KUBECTL_URL=https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl
|
||||
|
||||
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
|
||||
|
||||
JQ_VERSION=1.8.2
|
||||
JQ_SHA256=b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f
|
||||
JQ_URL=https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/jq-linux-amd64
|
||||
|
||||
# Node images shipped with KIND_VERSION above, pinned by digest so a kind upgrade
|
||||
# can never silently move the k8s version. Profiles select one via K8S_VERSION.
|
||||
# Older entries are kept deliberately: running a trailing-edge control plane is
|
||||
# part of simulating a legacy estate.
|
||||
NODE_IMAGE_v1_36=kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5
|
||||
NODE_IMAGE_v1_35=kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95
|
||||
NODE_IMAGE_v1_34=kindest/node:v1.34.8@sha256:02722c2dedddcfc00febf5d27fbeb9b7b2c14294c82109ff4a85d89ac9ba3256
|
||||
NODE_IMAGE_v1_33=kindest/node:v1.33.12@sha256:3f5c8443c620245e4d355cfe09e96a91ead32ceaa569d3f1ca9edf0cb2fe2ff4
|
||||
|
||||
# Images pulled at runtime (registry, mocks). Pinned by tag; the registry mode
|
||||
# decides where they are pulled FROM.
|
||||
REGISTRY_IMAGE=registry:2
|
||||
STUB_IMAGE=python:3.12-slim
|
||||
|
||||
# Addons, installed by ctrl/addons/<name>.sh when listed in a profile's ADDONS.
|
||||
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
|
||||
# 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
|
||||
REDIS_IMAGE=redis:7-alpine
|
||||
AIRFLOW_IMAGE=apache/airflow:2.10.4
|
||||
381
rig/ctrl/wizard.sh
Executable file
@@ -0,0 +1,381 @@
|
||||
#!/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.
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# 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
|
||||
# container, host files are read through $HOST_ROOT (mount / as :ro); bare, it
|
||||
# falls back to /.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Keep the caller's cwd so a relative --to resolves where the user expects,
|
||||
# not against ctrl/ once we've moved.
|
||||
INVOKED_FROM="$PWD"
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
source ./versions.env
|
||||
|
||||
# Resolve a possibly-relative path against the caller's original directory.
|
||||
abspath() {
|
||||
case "$1" in
|
||||
/*) echo "$1" ;;
|
||||
*) echo "$INVOKED_FROM/$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
OUT_BIN="${OUT_BIN:-$HOME/.local/bin}"
|
||||
HOST_ROOT="${HOST_ROOT:-/}"
|
||||
DEPS_SOURCE="${DEPS_SOURCE:-upstream}"
|
||||
DEPS_ARTIFACTORY_URL="${DEPS_ARTIFACTORY_URL:-}"
|
||||
BAKED_BIN="${BAKED_BIN:-/opt/rig/bin}"
|
||||
|
||||
# Collected by detect(), printed by report_manual() at the very end.
|
||||
MANUAL=()
|
||||
|
||||
# Host FILES (/etc/..., /mnt/c/...) must be read through the mount. Kernel-level
|
||||
# facts (kernel version, meminfo, inotify) are shared with the container, so the
|
||||
# container's own view is already the host's.
|
||||
host_file() {
|
||||
local p="${1#/}"
|
||||
if [ "$HOST_ROOT" != "/" ] && [ -e "$HOST_ROOT/$p" ]; then
|
||||
echo "$HOST_ROOT/$p"
|
||||
else
|
||||
echo "/$p"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── detect ─────────────────────────────────────────────────────────────────
|
||||
|
||||
is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; }
|
||||
|
||||
detect() {
|
||||
echo "host"
|
||||
echo " kernel $(uname -r)"
|
||||
|
||||
local osr; osr=$(host_file /etc/os-release)
|
||||
[ -r "$osr" ] && echo " distro $(sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' "$osr")"
|
||||
|
||||
local total_kb avail_kb
|
||||
total_kb=$(awk '/^MemTotal:/{print $2}' /proc/meminfo)
|
||||
avail_kb=$(awk '/^MemAvailable:/{print $2}' /proc/meminfo)
|
||||
printf " memory %d GB total, %d GB available\n" \
|
||||
$((total_kb / 1024 / 1024)) $((avail_kb / 1024 / 1024))
|
||||
|
||||
if [ $((avail_kb / 1024 / 1024)) -lt 4 ]; then
|
||||
echo " ! under 4 GB available — a multi-node profile will struggle."
|
||||
echo " 'make cluster list' shows the others; 'make cluster free' stops them."
|
||||
fi
|
||||
|
||||
detect_wsl
|
||||
detect_docker
|
||||
detect_inotify
|
||||
}
|
||||
|
||||
detect_wsl() {
|
||||
if ! is_wsl; then
|
||||
echo " platform native linux"
|
||||
return
|
||||
fi
|
||||
|
||||
echo " platform WSL"
|
||||
|
||||
# systemd is off by default in WSL, and the ingress/DNS paths that use a
|
||||
# host service need it. Enabling it requires a Windows-side restart, which
|
||||
# cannot be issued from inside the distro.
|
||||
local wc; wc=$(host_file /etc/wsl.conf)
|
||||
if [ -r "$wc" ] && grep -qE '^\s*systemd\s*=\s*true' "$wc"; then
|
||||
echo " systemd enabled in wsl.conf"
|
||||
else
|
||||
echo " ! systemd not enabled in /etc/wsl.conf"
|
||||
MANUAL+=("Enable systemd — add to /etc/wsl.conf:
|
||||
[boot]
|
||||
systemd=true
|
||||
then from a WINDOWS terminal (not this shell): wsl --shutdown")
|
||||
fi
|
||||
|
||||
# WSL regenerates /etc/resolv.conf on every boot, which silently reverts any
|
||||
# local DNS setup.
|
||||
if [ -r "$wc" ] && grep -qE '^\s*generateResolvConf\s*=\s*false' "$wc"; then
|
||||
echo " resolv.conf pinned (generateResolvConf=false)"
|
||||
else
|
||||
echo " - resolv.conf is WSL-generated; DNS_MODE=dnsmasq would be reverted on reboot"
|
||||
fi
|
||||
|
||||
local wcfg
|
||||
wcfg=$(ls "$HOST_ROOT"/mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true)
|
||||
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")
|
||||
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
|
||||
# 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.
|
||||
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)"
|
||||
else
|
||||
echo " ! docker not found and no socket at /var/run/docker.sock"
|
||||
MANUAL+=("Install Docker — the one true prerequisite:
|
||||
sudo apt-get install -y docker.io && sudo usermod -aG docker \"\$USER\"
|
||||
then log out and back in.")
|
||||
fi
|
||||
return
|
||||
fi
|
||||
if docker info >/dev/null 2>&1; then
|
||||
echo " docker $(docker version --format '{{.Server.Version}}' 2>/dev/null)"
|
||||
local n
|
||||
n=$(docker ps --filter "label=io.x-k8s.kind.cluster" --format '{{.Names}}' 2>/dev/null | wc -l)
|
||||
# Must be an `if`, not `[ ] && echo`: as the last statement in this
|
||||
# function the latter returns 1 when the count is zero, and `set -e`
|
||||
# then kills the caller. That is the fresh-machine case — no clusters
|
||||
# yet — so the bug only ever shows up where it does most harm.
|
||||
if [ "$n" -gt 0 ]; then
|
||||
echo " - $n kind node container(s) already running; see 'make cluster list'"
|
||||
fi
|
||||
else
|
||||
echo " ! docker cli present but the daemon is unreachable"
|
||||
MANUAL+=("Start Docker, or add yourself to the docker group:
|
||||
sudo usermod -aG docker \"\$USER\" # then log out and back in")
|
||||
fi
|
||||
}
|
||||
|
||||
# kind and Tilt both watch large trees. WSL ships defaults (8192/128) far too low,
|
||||
# and the failure mode is silent: Tilt simply stops noticing file changes.
|
||||
detect_inotify() {
|
||||
local w i
|
||||
w=$(cat /proc/sys/fs/inotify/max_user_watches 2>/dev/null || echo 0)
|
||||
i=$(cat /proc/sys/fs/inotify/max_user_instances 2>/dev/null || echo 0)
|
||||
echo " inotify watches=$w instances=$i"
|
||||
|
||||
if [ "$w" -lt 524288 ] || [ "$i" -lt 512 ]; then
|
||||
echo " ! inotify limits are low — Tilt will silently stop noticing file changes"
|
||||
MANUAL+=("Raise inotify limits (needs root on the host):
|
||||
echo -e 'fs.inotify.max_user_watches=524288\\nfs.inotify.max_user_instances=512' \\
|
||||
| sudo tee /etc/sysctl.d/99-rig.conf
|
||||
sudo sysctl --system")
|
||||
fi
|
||||
}
|
||||
|
||||
# ── fetch ──────────────────────────────────────────────────────────────────
|
||||
|
||||
# Resolve where a given artifact comes from, honouring DEPS_SOURCE.
|
||||
resolve_url() {
|
||||
local upstream="$1"
|
||||
case "$DEPS_SOURCE" in
|
||||
upstream) echo "$upstream" ;;
|
||||
artifactory)
|
||||
if [ -z "$DEPS_ARTIFACTORY_URL" ]; then
|
||||
echo "DEPS_SOURCE=artifactory but DEPS_ARTIFACTORY_URL is empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "${DEPS_ARTIFACTORY_URL%/}/$(basename "$upstream")"
|
||||
;;
|
||||
*) echo "unsupported DEPS_SOURCE '$DEPS_SOURCE' for a download" >&2; exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
verify() {
|
||||
local file="$1" want="$2" name="$3" got
|
||||
got=$(sha256sum "$file" | awk '{print $1}')
|
||||
if [ "$got" != "$want" ]; then
|
||||
echo "checksum mismatch for $name" >&2
|
||||
echo " expected $want" >&2
|
||||
echo " got $got" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# fetch_bin <name> <url> <sha256> <dest-dir> — a bare binary
|
||||
fetch_bin() {
|
||||
local name="$1" url="$2" sha="$3" dest="$4"
|
||||
local tmp="$dest/.$name.tmp"
|
||||
echo " fetching $name"
|
||||
curl -fsSL --retry 3 -o "$tmp" "$(resolve_url "$url")"
|
||||
verify "$tmp" "$sha" "$name"
|
||||
mv "$tmp" "$dest/$name"
|
||||
chmod +x "$dest/$name"
|
||||
}
|
||||
|
||||
# fetch_tgz <name> <url> <sha256> <dest-dir> <path-inside-archive> <strip>
|
||||
# Archive layouts differ — tilt's is flat (the binary at the root, strip=0),
|
||||
# others nest it a directory down — so the caller says which.
|
||||
fetch_tgz() {
|
||||
local name="$1" url="$2" sha="$3" dest="$4" inner="$5" strip="$6"
|
||||
local tmp="$dest/.$name.tgz"
|
||||
echo " fetching $name"
|
||||
curl -fsSL --retry 3 -o "$tmp" "$(resolve_url "$url")"
|
||||
verify "$tmp" "$sha" "$name"
|
||||
# --no-same-owner: extracting as root would otherwise restore the uid/gid
|
||||
# baked into the archive (some ship as uid 1001), leaving a binary the host
|
||||
# user does not own.
|
||||
tar -xzf "$tmp" -C "$dest" --strip-components="$strip" --no-same-owner "$inner"
|
||||
rm -f "$tmp"
|
||||
chmod +x "$dest/$name"
|
||||
}
|
||||
|
||||
# The wizard 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).
|
||||
fix_ownership() {
|
||||
local dir="$1"
|
||||
[ -d "$dir" ] || return 0
|
||||
local owner="${HOST_UID:-}:${HOST_GID:-}"
|
||||
if [ "$owner" = ":" ]; then
|
||||
owner=$(stat -c '%u:%g' "$dir")
|
||||
fi
|
||||
[ "$owner" = "0:0" ] && return 0
|
||||
chown -R "$owner" "$dir" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Two tiers, because not every machine should get cluster tooling.
|
||||
#
|
||||
# core kubectl, jq — talk to a cluster someone else runs. Nothing that
|
||||
# creates one. Appropriate on a managed or corporate-issued machine
|
||||
# where development tools are not wanted by default.
|
||||
# dev core plus kind and tilt — build clusters and hot-reload into them.
|
||||
#
|
||||
# The split exists because "install the toolchain" is not one decision: on a
|
||||
# managed workspace the right answer is kubectl and nothing else.
|
||||
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"
|
||||
|
||||
fetch() {
|
||||
local dest="$OUT_BIN" tier="${TIER:-dev}"
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--to) dest="$2"; shift 2 ;;
|
||||
core|dev) tier="$1"; shift ;;
|
||||
*) echo "unknown argument: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
dest="$(abspath "$dest")"
|
||||
mkdir -p "$dest"
|
||||
TIER="$tier"
|
||||
|
||||
if [ "$DEPS_SOURCE" = "baked" ]; then
|
||||
echo "installing baked binaries from $BAKED_BIN"
|
||||
cp -a "$BAKED_BIN"/. "$dest"/
|
||||
fix_ownership "$dest"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "fetching '$tier' toolchain (source: $DEPS_SOURCE)"
|
||||
fetch_bin kubectl "$KUBECTL_URL" "$KUBECTL_SHA256" "$dest"
|
||||
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
|
||||
fi
|
||||
|
||||
fix_ownership "$dest"
|
||||
# kind writes the kubeconfig as root too; hand that back as well when it's
|
||||
# a mounted host directory rather than container-local state.
|
||||
fix_ownership "${KUBE_DIR:-/out/kube}"
|
||||
}
|
||||
|
||||
# ── install ────────────────────────────────────────────────────────────────
|
||||
|
||||
report_manual() {
|
||||
echo
|
||||
if [ ${#MANUAL[@]} -eq 0 ]; then
|
||||
echo "nothing left to do by hand."
|
||||
return
|
||||
fi
|
||||
echo "host actions the wizard cannot perform (${#MANUAL[@]}):"
|
||||
echo
|
||||
local n=1
|
||||
for m in "${MANUAL[@]}"; do
|
||||
echo " $n. $m"
|
||||
echo
|
||||
n=$((n + 1))
|
||||
done
|
||||
}
|
||||
|
||||
# Installing into a directory that sits early in PATH silently replaces whatever
|
||||
# the machine was already using — which on a shared or client machine can break
|
||||
# unrelated work (kubectl more than one minor away from a cluster is the common
|
||||
# one). Say so; never decide it for them.
|
||||
tier_tools() { [ "$1" = "core" ] && echo "$CORE_TOOLS" || echo "$CORE_TOOLS $DEV_TOOLS"; }
|
||||
|
||||
warn_shadowing() {
|
||||
local b existing shadowed="" tier="${1:-dev}"
|
||||
for b in $(tier_tools "$tier"); do
|
||||
[ -x "$OUT_BIN/$b" ] || continue
|
||||
# Where would this resolve if OUT_BIN weren't in the way?
|
||||
existing=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -vx "$OUT_BIN" | paste -sd:) \
|
||||
command -v "$b" 2>/dev/null || true)
|
||||
[ -n "$existing" ] || continue
|
||||
[ "$existing" = "$OUT_BIN/$b" ] && continue
|
||||
shadowed+=" $b $existing"$'\n'
|
||||
done
|
||||
|
||||
[ -n "$shadowed" ] || return 0
|
||||
|
||||
case ":${PATH}:" in
|
||||
*":$OUT_BIN:"*) ;;
|
||||
*) return 0 ;; # not on PATH yet, so nothing is being shadowed
|
||||
esac
|
||||
|
||||
echo
|
||||
echo " ! these were already installed elsewhere and are now shadowed by $OUT_BIN:"
|
||||
printf '%s' "$shadowed"
|
||||
echo " Other projects on this machine will pick up the new versions."
|
||||
MANUAL+=("Decide which toolchain wins. To keep the previous one, remove what
|
||||
was just installed:
|
||||
rm -f $(for b in $(tier_tools "$tier"); do printf '%s ' "$OUT_BIN/$b"; done)
|
||||
Or install somewhere private instead:
|
||||
OUT_BIN=\$PWD/def/bin make deps # then put that dir first in PATH")
|
||||
}
|
||||
|
||||
install() {
|
||||
local tier="${1:-dev}"
|
||||
detect
|
||||
echo
|
||||
fetch "$tier"
|
||||
echo
|
||||
echo "installed to $OUT_BIN ($tier):"
|
||||
for b in $(tier_tools "$tier"); do
|
||||
[ -x "$OUT_BIN/$b" ] && echo " $b"
|
||||
done
|
||||
if [ "$tier" = "core" ]; then
|
||||
echo " (no kind/tilt — 'make deps dev' adds them)"
|
||||
fi
|
||||
warn_shadowing "$tier"
|
||||
|
||||
case ":${PATH}:" in
|
||||
*":$OUT_BIN:"*) ;;
|
||||
*) MANUAL+=("Put the toolchain on your PATH — add to ~/.bashrc:
|
||||
export PATH=\"${OUT_BIN}:\$PATH\"") ;;
|
||||
esac
|
||||
|
||||
report_manual
|
||||
}
|
||||
|
||||
# ── main ───────────────────────────────────────────────────────────────────
|
||||
|
||||
case "${1:-install}" in
|
||||
detect) detect; report_manual ;;
|
||||
fetch) shift; fetch "$@" ;;
|
||||
install) shift; install "${1:-dev}" ;;
|
||||
*) echo "usage: $0 [detect|fetch|install]" >&2; exit 1 ;;
|
||||
esac
|
||||
47
rig/docs/graphs/01-install.dot
Normal file
@@ -0,0 +1,47 @@
|
||||
digraph rig_install {
|
||||
rankdir=LR
|
||||
bgcolor="#0a0e17"
|
||||
fontname="Helvetica"
|
||||
node [fontname="Helvetica" fontsize=11 style=filled color="#1e2a4a" fontcolor="#e8eaf0" shape=box]
|
||||
edge [fontname="Helvetica" fontsize=9 fontcolor="#8892a8" color="#4a5568"]
|
||||
|
||||
label="Installation — the only host prerequisite is Docker"
|
||||
labelloc=t
|
||||
fontsize=16
|
||||
fontcolor="#0066ff"
|
||||
|
||||
subgraph cluster_host {
|
||||
label="Your machine"
|
||||
style=dashed
|
||||
color="#1e2a4a"
|
||||
fontcolor="#8892a8"
|
||||
|
||||
docker [label="Docker\n(the one prerequisite)" fillcolor="#1a1a3a" fontcolor="#0066ff" shape=octagon]
|
||||
bin [label="~/.local/bin\nkind · kubectl · tilt\njq" fillcolor="#121829" shape=cylinder]
|
||||
}
|
||||
|
||||
subgraph cluster_wizard {
|
||||
label="Installer container (transient)"
|
||||
style=dashed
|
||||
color="#1e2a4a"
|
||||
fontcolor="#8892a8"
|
||||
|
||||
wizard [label="wizard\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"]
|
||||
}
|
||||
|
||||
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
|
||||
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"]
|
||||
gone [label="(container discarded)" fillcolor="#0a0e17" fontcolor="#4a5568" color="#1e2a4a" style="filled,dashed"]
|
||||
}
|
||||
128
rig/docs/graphs/01-install.svg
Normal file
@@ -0,0 +1,128 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 14.1.2 (0)
|
||||
-->
|
||||
<!-- Title: rig_install Pages: 1 -->
|
||||
<svg width="1145pt" height="287pt"
|
||||
viewBox="0.00 0.00 1145.00 287.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 283.29)">
|
||||
<title>rig_install</title>
|
||||
<polygon fill="#0a0e17" stroke="none" points="-4,4 -4,-283.29 1141.06,-283.29 1141.06,4 -4,4"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="568.53" y="-260.09" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#0066ff">Installation — the only host prerequisite is Docker</text>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_host</title>
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="898.49,-8 898.49,-190 1123.82,-190 1123.82,-8 898.49,-8"/>
|
||||
<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>
|
||||
<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>
|
||||
<!-- docker -->
|
||||
<g id="node1" class="node">
|
||||
<title>docker</title>
|
||||
<polygon fill="#1a1a3a" stroke="#1e2a4a" points="1115.82,-31.9 1115.82,-54.1 1054.51,-69.79 967.8,-69.79 906.49,-54.1 906.49,-31.9 967.8,-16.21 1054.51,-16.21 1115.82,-31.9"/>
|
||||
<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 -->
|
||||
<g id="node3" class="node">
|
||||
<title>wizard</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="-110.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">curl · jq · python · graphviz</text>
|
||||
</g>
|
||||
<!-- docker->wizard -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>docker->wizard</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>
|
||||
</g>
|
||||
<!-- bin -->
|
||||
<g id="node2" class="node">
|
||||
<title>bin</title>
|
||||
<path fill="#121829" stroke="#1e2a4a" d="M1069.03,-148.28C1069.03,-151.63 1043.09,-154.34 1011.15,-154.34 979.22,-154.34 953.28,-151.63 953.28,-148.28 953.28,-148.28 953.28,-93.72 953.28,-93.72 953.28,-90.37 979.22,-87.66 1011.15,-87.66 1043.09,-87.66 1069.03,-90.37 1069.03,-93.72 1069.03,-93.72 1069.03,-148.28 1069.03,-148.28"/>
|
||||
<path fill="none" stroke="#1e2a4a" d="M1069.03,-148.28C1069.03,-144.94 1043.09,-142.22 1011.15,-142.22 979.22,-142.22 953.28,-144.94 953.28,-148.28"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1011.15" y="-130.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">~/.local/bin</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1011.15" y="-117.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">kind · kubectl · tilt</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1011.15" y="-103.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">jq</text>
|
||||
</g>
|
||||
<!-- detect -->
|
||||
<g id="node4" class="node">
|
||||
<title>detect</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="429,-139 238.25,-139 238.25,-103 429,-103 429,-139"/>
|
||||
<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 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>wizard->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>
|
||||
<!-- gone -->
|
||||
<g id="node8" class="node">
|
||||
<title>gone</title>
|
||||
<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 -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>wizard->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>
|
||||
</g>
|
||||
<!-- fetch -->
|
||||
<g id="node5" class="node">
|
||||
<title>fetch</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="737.5,-139 584.25,-139 584.25,-103 737.5,-103 737.5,-139"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="660.88" y="-124.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">fetch + verify</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="660.88" y="-110.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">SHA256, pinned versions</text>
|
||||
</g>
|
||||
<!-- detect->fetch -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>detect->fetch</title>
|
||||
<path fill="none" stroke="#4a5568" d="M429.14,-121C474.43,-121 528.32,-121 572.63,-121"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="572.46,-124.5 582.46,-121 572.46,-117.5 572.46,-124.5"/>
|
||||
</g>
|
||||
<!-- report -->
|
||||
<g id="node7" class="node">
|
||||
<title>report</title>
|
||||
<polygon fill="#3a1a1a" stroke="#1e2a4a" points="706.75,-219 615,-219 615,-183 706.75,-183 706.75,-219"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="660.88" y="-204.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">report what it</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="660.88" y="-190.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">CANNOT do</text>
|
||||
</g>
|
||||
<!-- detect->report -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>detect->report</title>
|
||||
<path fill="none" stroke="#ffc107" stroke-dasharray="5,2" d="M409.65,-139.45C468.85,-154.02 550.13,-174.01 603.78,-187.2"/>
|
||||
<polygon fill="#ffc107" stroke="#ffc107" points="602.77,-190.56 613.32,-189.55 604.45,-183.76 602.77,-190.56"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="506.62" y="-180.06" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">sudo / Windows-side steps</text>
|
||||
</g>
|
||||
<!-- fetch->bin -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>fetch->bin</title>
|
||||
<path fill="none" stroke="#4a5568" d="M737.88,-121C798.58,-121 882.9,-121 941.56,-121"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="941.43,-124.5 951.43,-121 941.43,-117.5 941.43,-124.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="811.38" y="-123.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">install</text>
|
||||
</g>
|
||||
<!-- upstream -->
|
||||
<g id="node6" class="node">
|
||||
<title>upstream</title>
|
||||
<polygon fill="#1a3a1a" stroke="#1e2a4a" points="1137.06,-213.9 1137.06,-236.1 1063.3,-251.79 959,-251.79 885.25,-236.1 885.25,-213.9 959,-198.21 1063.3,-198.21 1137.06,-213.9"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1011.15" y="-228.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">upstream</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1011.15" y="-214.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">releases / corporate mirror</text>
|
||||
</g>
|
||||
<!-- fetch->upstream -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>fetch->upstream</title>
|
||||
<path fill="none" stroke="#00c853" d="M714.8,-139.5C759.85,-154.95 826.43,-177.11 885.25,-194 894.79,-196.74 904.78,-199.46 914.78,-202.09"/>
|
||||
<polygon fill="#00c853" stroke="#00c853" points="913.59,-205.4 924.15,-204.52 915.35,-198.62 913.59,-205.4"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="811.38" y="-191.36" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">pinned + checksummed</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.0 KiB |