From d34ff9220d880ccc9f61fb31356d427cbe945a3f Mon Sep 17 00:00:00 2001 From: Paul Payne Date: Wed, 1 Jul 2026 16:33:42 -0700 Subject: [PATCH] install.sh: bootstrap Castle's own control plane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A fresh clone + ./install.sh set up the infra (Docker, Caddy, MQTT, Postgres) but left the registry empty — so `castle deploy && castle start` brought up nothing. install.sh now gets a person all the way to a running Castle: - ensure_uv + install_cli: install uv and the `castle` CLI (editable from ./cli) so the command exists on a fresh machine. - seed_control_plane: register castle-gateway, castle-api, and the castle dashboard from a new bootstrap/ seed dir, and add `repo:` to castle.yaml so `source: repo:` resolves. Never clobbers existing entries. - build_dashboard: build app/dist/ so the gateway has a UI to serve (best-effort; warns if pnpm is absent). The gateway seed uses a `__SPECS_DIR__` placeholder (no machine-specific paths in the repo), substituted with this machine's specs dir at seed time. .gitignore negates bootstrap/**/castle.yaml so the seeds are tracked despite the user-registry ignore rule. Verified: seeded config loads, derives kinds service/service/static, and renders an off-mode Caddyfile serving the dashboard at :9000/ with /api/* proxied to castle-api. --- .gitignore | 1 + bootstrap/deployments/castle-api.yaml | 18 +++++ bootstrap/deployments/castle-gateway.yaml | 22 ++++++ bootstrap/deployments/castle.yaml | 3 + bootstrap/programs/castle-api.yaml | 3 + bootstrap/programs/castle.yaml | 6 ++ install.sh | 94 ++++++++++++++++++++++- 7 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 bootstrap/deployments/castle-api.yaml create mode 100644 bootstrap/deployments/castle-gateway.yaml create mode 100644 bootstrap/deployments/castle.yaml create mode 100644 bootstrap/programs/castle-api.yaml create mode 100644 bootstrap/programs/castle.yaml diff --git a/.gitignore b/.gitignore index a5e4fa6..097ad84 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ __pycache__ .claude node_modules castle.yaml +!bootstrap/**/castle.yaml .working diff --git a/bootstrap/deployments/castle-api.yaml b/bootstrap/deployments/castle-api.yaml new file mode 100644 index 0000000..3c190f0 --- /dev/null +++ b/bootstrap/deployments/castle-api.yaml @@ -0,0 +1,18 @@ +program: castle-api +description: Castle API +manager: systemd +run: + launcher: python + program: castle-api +expose: + http: + internal: + port: 9020 + health_path: /health +proxy: true +manage: + systemd: {} +defaults: + env: + CASTLE_API_PORT: ${port} + CASTLE_API_DATA_DIR: ${data_dir} diff --git a/bootstrap/deployments/castle-gateway.yaml b/bootstrap/deployments/castle-gateway.yaml new file mode 100644 index 0000000..06d2041 --- /dev/null +++ b/bootstrap/deployments/castle-gateway.yaml @@ -0,0 +1,22 @@ +# The Caddy gateway. `__SPECS_DIR__` is substituted with $CASTLE_HOME/artifacts/specs +# at install time (install.sh seed_control_plane). For acme TLS, add a +# CLOUDFLARE_API_TOKEN to defaults.env later — see docs/dns-and-tls.md. +description: Caddy reverse proxy gateway +manager: systemd +run: + launcher: command + argv: + - caddy + - run + - --config + - __SPECS_DIR__/Caddyfile + - --adapter + - caddyfile +expose: + http: + internal: + port: 9000 + health_path: / +manage: + systemd: + exec_reload: caddy reload --config __SPECS_DIR__/Caddyfile --adapter caddyfile diff --git a/bootstrap/deployments/castle.yaml b/bootstrap/deployments/castle.yaml new file mode 100644 index 0000000..d0d0550 --- /dev/null +++ b/bootstrap/deployments/castle.yaml @@ -0,0 +1,3 @@ +program: castle +description: Castle web app +manager: caddy diff --git a/bootstrap/programs/castle-api.yaml b/bootstrap/programs/castle-api.yaml new file mode 100644 index 0000000..ae9a404 --- /dev/null +++ b/bootstrap/programs/castle-api.yaml @@ -0,0 +1,3 @@ +description: Castle API +source: repo:castle-api +stack: python-fastapi diff --git a/bootstrap/programs/castle.yaml b/bootstrap/programs/castle.yaml new file mode 100644 index 0000000..7aef44e --- /dev/null +++ b/bootstrap/programs/castle.yaml @@ -0,0 +1,6 @@ +description: Castle web app +source: repo:app +stack: react-vite +build: + outputs: + - dist/ diff --git a/install.sh b/install.sh index 3533f85..9b69239 100755 --- a/install.sh +++ b/install.sh @@ -215,6 +215,34 @@ ensure_caddy_dns_plugin() { log_ok } +# --------------------------------------------------------------------------- +# Castle CLI +# --------------------------------------------------------------------------- + +ensure_uv() { + log_step "Ensuring uv (Python package manager)" + if cmd_exists uv; then + log_skip "already installed" + return + fi + curl -LsSf https://astral.sh/uv/install.sh | sh >/dev/null 2>&1 || log_fail "uv install failed" + # The installer drops uv in ~/.local/bin — make it visible for the rest of this run. + export PATH="${HOME}/.local/bin:${PATH}" + cmd_exists uv || log_fail "uv installed but not on PATH (expected ~/.local/bin)" + log_ok +} + +# Install the `castle` CLI from this repo as an editable uv tool, so a fresh +# clone becomes a working `castle` command. Idempotent — reinstall is cheap and +# keeps the entry point pointed at the current checkout. +install_cli() { + log_step "Installing the castle CLI" + ( cd "$CASTLE_ROOT" && uv tool install --editable ./cli >/dev/null 2>&1 ) \ + || log_fail "uv tool install of ./cli failed" + cmd_exists castle || log_info "NOTE: ensure ~/.local/bin is on your PATH to use 'castle'" + log_ok +} + # --------------------------------------------------------------------------- # Directory structure # --------------------------------------------------------------------------- @@ -247,6 +275,41 @@ create_directories() { fi } +# --------------------------------------------------------------------------- +# Control plane (Castle's own gateway + API + dashboard) +# --------------------------------------------------------------------------- + +# Register Castle's own control-plane programs/deployments from bootstrap/ so a +# fresh registry is not empty. Without this, `castle deploy && castle start` +# would bring up nothing. Never clobbers existing entries (idempotent). The +# gateway deployment carries a `__SPECS_DIR__` placeholder (the source repo has +# no machine-specific paths) that we substitute with this machine's specs dir. +seed_control_plane() { + log_step "Registering Castle's control plane" + local specs="${CASTLE_HOME}/artifacts/specs" + + # The `repo:` field lets `source: repo:` resolve castle's own programs. + if ! grep -q "^repo:" "${CASTLE_HOME}/castle.yaml" 2>/dev/null; then + printf 'repo: %s\n' "$CASTLE_ROOT" >> "${CASTLE_HOME}/castle.yaml" + fi + + local seeded=0 f dst + for f in "${CASTLE_ROOT}"/bootstrap/programs/*.yaml; do + dst="${CASTLE_HOME}/programs/$(basename "$f")" + [ -f "$dst" ] || { cp "$f" "$dst"; seeded=1; } + done + for f in "${CASTLE_ROOT}"/bootstrap/deployments/*.yaml; do + dst="${CASTLE_HOME}/deployments/$(basename "$f")" + [ -f "$dst" ] || { sed "s#__SPECS_DIR__#${specs}#g" "$f" > "$dst"; seeded=1; } + done + + if [ "$seeded" = "1" ]; then + log_ok "castle-gateway, castle-api, castle (dashboard)" + else + log_skip "already registered" + fi +} + # --------------------------------------------------------------------------- # Systemd lingering # --------------------------------------------------------------------------- @@ -435,6 +498,28 @@ maybe_setup_neo4j() { fi } +# --------------------------------------------------------------------------- +# Dashboard build +# --------------------------------------------------------------------------- + +# Build the dashboard SPA so the gateway has something to serve at :9000. The +# `castle` program serves `app/dist/` in place; without a build there's no UI. +# Best-effort: a missing pnpm is a warning (the CLI/API still work), not a failure. +build_dashboard() { + log_step "Building the dashboard" + if [ ! -d "${CASTLE_ROOT}/app" ]; then + log_skip "no app/ directory" + return + fi + if ! cmd_exists pnpm; then + log_skip "pnpm not found — build later with 'castle program build castle'" + return + fi + ( cd "${CASTLE_ROOT}/app" && pnpm install --silent >/dev/null 2>&1 && pnpm build >/dev/null 2>&1 ) \ + || { log_skip "build failed — retry later with 'castle program build castle'"; return; } + log_ok "app/dist/" +} + # --------------------------------------------------------------------------- # Summary # --------------------------------------------------------------------------- @@ -468,8 +553,9 @@ print_summary() { printf "\n" printf "Next steps:\n" - printf " castle deploy # Generate registry, systemd units, Caddyfile\n" - printf " castle start # Start all deployments and the gateway\n" + printf " castle deploy # Generate registry, systemd units, Caddyfile\n" + printf " castle start # Start the gateway, API, and all deployments\n" + printf " open http://localhost:9000 # the dashboard\n" } # --------------------------------------------------------------------------- @@ -500,13 +586,17 @@ main() { ensure_docker ensure_caddy [ -n "$WITH_DNS_PLUGIN" ] && ensure_caddy_dns_plugin "$WITH_DNS_PLUGIN" + ensure_uv + install_cli create_directories + seed_control_plane enable_lingering seed_caddyfile migrate_old_containers setup_mqtt setup_postgres maybe_setup_neo4j + build_dashboard print_summary }