#!/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'