Files
neuronetz-gateway/scripts/wire-multi-backend.sh
Stephan Berbig 27b7012ec9
Some checks failed
CI / ruff (push) Has been cancelled
CI / mypy --strict (push) Has been cancelled
CI / pytest (push) Has been cancelled
CI / bandit (push) Has been cancelled
CI / pip-audit (push) Has been cancelled
scripts: wire-multi-backend.sh — one-shot multi-backend bootstrap
Drops the multi-step paste-by-paste setup down to one command. Detects
every running ollama/ollama container on the host, classifies them
(embedded vs extras based on which Docker network they share with the
gateway), checks each one's auth config, rewrites OLLAMA_BACKENDS in
.env atomically, recreates the gateway, and probes everything.

What it does, in order:
  1. preflight: docker on PATH, .env present, gateway container running
  2. `docker ps --filter ancestor=ollama/ollama` → every running Ollama
  3. for each one:
       - find the network it shares with the gateway; attach to `proxy`
         if it's on no shared network
       - read OLLAMA_AUTH and OLLAMA_AUTH_TOKEN from `docker inspect`
       - FAIL LOUD if auth=true with empty token (with the exact fix)
       - classify: on a `*internal*` net = the gateway's embedded backend;
         otherwise an extra
  4. build the OLLAMA_BACKENDS JSON: embedded first (so it gets routing
     priority), then extras in discovery order, with per-backend tokens
     where present
  5. write .env atomically (host-side temp + rename — no in-container
     perm issues to worry about)
  6. `docker compose up -d gateway` to pick up the new env
  7. wait for /healthz, then run probe-ollama and list-backends so the
     operator sees the end state immediately

Token is redacted before echoing the resulting OLLAMA_BACKENDS line, so
the script can run safely with terminal logging on.

Idempotent: re-running produces the same OLLAMA_BACKENDS line. No
half-states possible — every error path exits before .env is touched.
2026-05-27 23:15:59 +02:00

186 lines
7.1 KiB
Bash
Executable File

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