diff --git a/scripts/wire-multi-backend.sh b/scripts/wire-multi-backend.sh new file mode 100755 index 0000000..ddbd028 --- /dev/null +++ b/scripts/wire-multi-backend.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env bash +# scripts/wire-multi-backend.sh +# +# One-shot bootstrap for multi-backend Ollama wiring. +# +# Walks every running ollama/ollama container on this host, classifies them +# (which is the gateway's embedded one, which are extras from other compose +# stacks), checks each one's auth state, rewrites OLLAMA_BACKENDS in this +# repo's .env atomically, recreates the gateway, and probes everything. +# +# Refuses to half-wire. If any Ollama has OLLAMA_AUTH=true with an empty +# OLLAMA_AUTH_TOKEN — no caller can ever succeed in that state — the script +# exits with the exact fix instead of silently leaving a dead backend. +# +# Idempotent. Re-running it produces the same OLLAMA_BACKENDS line. +# +# Run from the gateway repo directory: +# cd ~/docker-public/neuro-gateway && ./scripts/wire-multi-backend.sh + +set -euo pipefail + +GATEWAY=${GATEWAY_CONTAINER:-neuronetz-gateway} +ENV_FILE=${ENV_FILE:-.env} + +RED=$(printf '\033[31m') +GRN=$(printf '\033[32m') +YEL=$(printf '\033[33m') +BLD=$(printf '\033[1m') +RST=$(printf '\033[0m') + +step() { printf '\n%s==> %s%s\n' "$BLD" "$1" "$RST"; } +fail() { printf '%sERROR:%s %s\n' "$RED" "$RST" "$1" >&2; exit 1; } + +# ── preflight ────────────────────────────────────────────────────────────── +command -v docker >/dev/null || fail "docker not on PATH." +[ -f "$ENV_FILE" ] || fail "no $ENV_FILE in $(pwd); cd to the gateway repo first." +docker ps --format '{{.Names}}' | grep -qx "$GATEWAY" \ + || fail "$GATEWAY not running. Bring it up first: docker compose up -d gateway" + +# ── discover every running Ollama ────────────────────────────────────────── +step "Discovering running Ollama containers" +mapfile -t OLLAMAS < <(docker ps --filter ancestor=ollama/ollama --format '{{.Names}}') +[ ${#OLLAMAS[@]} -gt 0 ] || fail "no ollama/ollama containers running on this host." +printf ' - %s\n' "${OLLAMAS[@]}" + +# Networks the gateway is attached to — each Ollama needs to share at least one. +GATEWAY_NETS=$(docker inspect "$GATEWAY" \ + --format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}') + +# ── classify + validate each backend ─────────────────────────────────────── +step "Inspecting each backend" + +EMBEDDED="" # the ollama on the gateway's `internal` network +declare -a EXTRAS=() # ollamas on the `proxy` network (other stacks) +declare -A TOKEN_OF=() + +for c in "${OLLAMAS[@]}"; do + # find a network this container shares with the gateway + shared="" + for net in $GATEWAY_NETS; do + if docker network inspect "$net" \ + --format '{{range .Containers}}{{.Name}}{{println}}{{end}}' 2>/dev/null \ + | grep -qx "$c"; then + shared=$net + break + fi + done + if [ -z "$shared" ]; then + printf ' %s!%s %s not on any gateway network; attaching to '\''proxy'\''...\n' \ + "$YEL" "$RST" "$c" + docker network connect proxy "$c" 2>/dev/null \ + || fail "failed to attach $c to 'proxy' network" + shared=proxy + fi + + # read auth config from the running container's effective env + envs=$(docker inspect "$c" --format '{{range .Config.Env}}{{println .}}{{end}}') + auth=$(printf '%s\n' "$envs" | sed -n 's/^OLLAMA_AUTH=//p' | head -1) + token=$(printf '%s\n' "$envs" | sed -n 's/^OLLAMA_AUTH_TOKEN=//p' | head -1) + + if [ "$auth" = "true" ] && [ -z "$token" ]; then + cat <&2 +${RED}ERROR:${RST} ${BLD}$c${RST} has OLLAMA_AUTH=true but OLLAMA_AUTH_TOKEN is empty. + No caller can succeed in this state. Fix one of these on the $c stack + (whichever directory contains its docker-compose.yml), then re-run me: + + A) Set a real token in that stack's .env, e.g.: + echo "OLLAMA_AUTH_TOKEN=\$(openssl rand -hex 32)" >> .env + docker compose up -d + (re-run this script after — it'll pick up the new token via + \`docker inspect\` automatically.) + + B) Or just disable auth (this container is on an internal Docker + network and doesn't publish a port, so it isn't externally + reachable anyway): + # change in its docker-compose.yml: + # OLLAMA_AUTH: "false" + docker compose up -d +EOF + exit 1 + fi + + TOKEN_OF[$c]=$token + + case "$shared" in + *internal*) + EMBEDDED=$c + printf ' %s✓%s %-30s [embedded] auth=%s token=%s net=%s\n' \ + "$GRN" "$RST" "$c" "${auth:-false}" \ + "$([ -n "$token" ] && echo set || echo empty)" "$shared" + ;; + *) + EXTRAS+=("$c") + printf ' %s✓%s %-30s [extra] auth=%s token=%s net=%s\n' \ + "$GRN" "$RST" "$c" "${auth:-false}" \ + "$([ -n "$token" ] && echo set || echo empty)" "$shared" + ;; + esac +done + +# ── build the OLLAMA_BACKENDS JSON ───────────────────────────────────────── +step "Building OLLAMA_BACKENDS" + +build_entry() { + local name=$1 url=$2 token=$3 + if [ -n "$token" ]; then + printf '{"name":"%s","base_url":"%s","auth_token":"%s"}' "$name" "$url" "$token" + else + printf '{"name":"%s","base_url":"%s"}' "$name" "$url" + fi +} + +entries=() +# Embedded first (highest routing priority). Use the in-stack service name +# "ollama" — compose's DNS resolves it on the internal network. +if [ -n "$EMBEDDED" ]; then + entries+=("$(build_entry embedded http://ollama:11434 "${TOKEN_OF[$EMBEDDED]:-}")") +fi +for c in "${EXTRAS[@]}"; do + entries+=("$(build_entry "$c" "http://$c:11434" "${TOKEN_OF[$c]:-}")") +done + +JSON='[' +for i in "${!entries[@]}"; do + [ "$i" = "0" ] || JSON+=',' + JSON+="${entries[$i]}" +done +JSON+=']' + +# Echo the line with the token redacted so we don't print secrets to the terminal. +REDACTED=$(printf '%s' "$JSON" | sed 's/"auth_token":"[^"]*"/"auth_token":"***"/g') +printf ' Result line (token redacted):\n OLLAMA_BACKENDS=%s\n' "$REDACTED" + +# ── write .env atomically (on the HOST — no container perms involved) ────── +step "Writing $ENV_FILE" +TMP=$(mktemp "${ENV_FILE}.XXXXXX") || fail "mktemp failed" +trap 'rm -f "$TMP"' EXIT +grep -v '^OLLAMA_BACKENDS=' "$ENV_FILE" > "$TMP" || true +printf 'OLLAMA_BACKENDS=%s\n' "$JSON" >> "$TMP" +mv "$TMP" "$ENV_FILE" +trap - EXIT +printf ' %s✓%s %s updated\n' "$GRN" "$RST" "$ENV_FILE" + +# ── recreate gateway + probe ────────────────────────────────────────────── +step "Recreating the gateway" +docker compose up -d gateway + +printf 'Waiting for /healthz' +for _ in $(seq 1 30); do + if docker exec "$GATEWAY" curl -sf http://127.0.0.1:8080/healthz >/dev/null 2>&1; then + printf ' %sOK%s\n' "$GRN" "$RST" + break + fi + printf '.' + sleep 1 +done + +step "Probing all backends" +docker exec "$GATEWAY" neuronetz-gateway probe-ollama + +step "Final gateway view" +docker exec "$GATEWAY" neuronetz-gateway list-backends + +printf '\n%s%s✓ Multi-backend wiring complete.%s\n' "$GRN" "$BLD" "$RST" +printf ' Hit /v1/models on the gateway to see the union of all backends.\n'