#!/usr/bin/env bash # # demo.sh — the neuronetz-gateway one-command presentation. # # Brings up the demo stack (postgres + redis + mock-ollama + gateway) with NO # GPU and NO model downloads, creates a demo tenant + API key via the bootstrap # CLI *inside the gateway container*, and prints a clean summary with the key, # the playground URL, and ready-to-paste curl commands. # # Usage: # ./demo.sh # build + start, bootstrap a tenant/key, print summary # ./demo.sh --down # tear the whole stack down (and remove volumes) # ./demo.sh --help # this help # # Re-runnable: existing tenant/key are handled gracefully. The full API key is # only ever printed once at creation (SPEC §11), so on a re-run where the key # already exists this script creates a fresh, uniquely-named key and prints it. set -euo pipefail # ────────────────────────────────────────────────────────────────────────── # Configuration # ────────────────────────────────────────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.demo.yml" COMPOSE=(docker compose -f "${COMPOSE_FILE}") GATEWAY_URL="http://localhost:8080" PLAYGROUND_URL="${GATEWAY_URL}/playground" TENANT_NAME="demo" KEY_NAME="demo-key" # Colours (disabled when stdout is not a TTY). if [ -t 1 ]; then BOLD="$(printf '\033[1m')"; DIM="$(printf '\033[2m')"; RESET="$(printf '\033[0m')" CYAN="$(printf '\033[36m')"; GREEN="$(printf '\033[32m')"; YELLOW="$(printf '\033[33m')" else BOLD=""; DIM=""; RESET=""; CYAN=""; GREEN=""; YELLOW="" fi log() { printf '%s\n' "${CYAN}==>${RESET} ${BOLD}$*${RESET}"; } warn() { printf '%s\n' "${YELLOW}!!${RESET} $*" >&2; } die() { printf '%s\n' "${YELLOW}xx${RESET} $*" >&2; exit 1; } # ────────────────────────────────────────────────────────────────────────── # Subcommands # ────────────────────────────────────────────────────────────────────────── usage() { sed -n '3,18p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' } down() { log "Tearing down the demo stack (containers + volumes)…" "${COMPOSE[@]}" down --volumes --remove-orphans log "Done. The demo stack is gone." } # Run the bootstrap CLI inside the running gateway container. gw_cli() { "${COMPOSE[@]}" exec -T gateway neuronetz-gateway "$@" } wait_for_health() { log "Waiting for the gateway to become healthy at ${GATEWAY_URL}/healthz …" local deadline=$(( $(date +%s) + 180 )) until curl -fsS "${GATEWAY_URL}/healthz" >/dev/null 2>&1; do if [ "$(date +%s)" -ge "${deadline}" ]; then warn "Gateway did not become healthy in time. Recent gateway logs:" "${COMPOSE[@]}" logs --tail=50 gateway >&2 || true die "Aborting." fi sleep 2 done log "Gateway is up." } # Create the demo tenant if it does not already exist (idempotent). ensure_tenant() { log "Creating demo tenant '${TENANT_NAME}' (allow-all-models) …" local out if out="$(gw_cli create-tenant --name "${TENANT_NAME}" --allow-all-models 2>&1)"; then printf '%s\n' "${DIM}${out}${RESET}" else # Already-exists (or similar) is fine — surface it but keep going. if printf '%s' "${out}" | grep -qiE 'exist|duplicate|unique'; then log "Tenant '${TENANT_NAME}' already exists — reusing it." else warn "create-tenant reported:" printf '%s\n' "${out}" >&2 warn "Continuing; the tenant may already be present." fi fi } # Create a fresh API key and capture the printed key. The key is printed once. # We give each created key a unique name so re-runs always succeed and always # yield a usable key to print. create_key() { local unique_name="${KEY_NAME}-$(date +%Y%m%d-%H%M%S)" log "Creating API key '${unique_name}' for tenant '${TENANT_NAME}' …" >&2 local out if ! out="$(gw_cli create-key --tenant "${TENANT_NAME}" --name "${unique_name}" 2>&1)"; then warn "create-key failed:" >&2 printf '%s\n' "${out}" >&2 return 1 fi # The CLI prints both the 12-char prefix (e.g. "prefix nz_abc12345Yz") AND the # full key on a later line. Both match /nz_[A-Za-z0-9]+/, so pick the longest # match — that's the full key (44 chars), never the prefix (12). local key key="$(printf '%s' "${out}" | grep -oE 'nz_[A-Za-z0-9]+' \ | awk '{ if (length($0) > maxlen) { maxlen = length($0); k = $0 } } END { print k }' \ || true)" if [ -z "${key}" ]; then warn "Could not parse an API key from create-key output:" >&2 printf '%s\n' "${out}" >&2 return 1 fi printf '%s' "${key}" } print_summary() { local key="$1" local cl='application/json' cat </dev/null 2>&1 || die "docker is required but not found on PATH." command -v curl >/dev/null 2>&1 || die "curl is required but not found on PATH." [ -f "${COMPOSE_FILE}" ] || die "Missing ${COMPOSE_FILE}" log "Building and starting the demo stack (postgres + redis + mock-ollama + gateway) …" "${COMPOSE[@]}" up --build -d wait_for_health ensure_tenant local key if ! key="$(create_key)"; then die "Could not create/parse an API key. See logs above." fi print_summary "${key}" } # ────────────────────────────────────────────────────────────────────────── # Entry point # ────────────────────────────────────────────────────────────────────────── main() { case "${1:-}" in --down|-d|down) down ;; --help|-h|help) usage ;; "") up ;; *) die "Unknown argument: $1 (try --help)" ;; esac } main "$@"