-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathinstall-model-builder.sh
More file actions
executable file
·250 lines (229 loc) · 12.2 KB
/
Copy pathinstall-model-builder.sh
File metadata and controls
executable file
·250 lines (229 loc) · 12.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
#!/usr/bin/env bash
# Scripted install of Model Builder + claude-subscription-gateway on Kubernetes.
# Idempotent: safe to re-run; existing secrets and the Claude login are never
# overwritten.
#
# This file is self-contained: run it next to a deploy/k8s directory (a clone
# of transpara/model-builder or transpara/model-builder-install) and it uses
# those manifests; run it standalone and it fetches them from the public
# transpara/model-builder-install repo.
#
# Usage:
# ./install-model-builder.sh
#
# Images pull anonymously from registry.transpara.com — no registry
# credentials needed. Flags:
# --install-k3s install k3s first if there is no working kubectl (for a
# box that does not have Kubernetes yet)
# --skip-login do not offer the interactive `claude setup-token` step
set -euo pipefail
NS=model-builder
GATEWAY_URL_IN_CLUSTER="http://claude-subscription-gateway:8790"
MANIFEST_TARBALL="https://github.com/transpara/model-builder-install/archive/refs/heads/main.tar.gz"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INSTALL_K3S=false
SKIP_LOGIN=false
for arg in "$@"; do
case "$arg" in
--install-k3s) INSTALL_K3S=true ;;
--skip-login) SKIP_LOGIN=true ;;
*) echo "Unknown flag: $arg (supported: --install-k3s, --skip-login)" >&2; exit 1 ;;
esac
done
say() { echo; echo "==> $*"; }
warn() { echo "WARNING: $*" >&2; }
die() { echo "ERROR: $*" >&2; exit 1; }
# Manifests: prefer a deploy/k8s next to (or one level above) the script;
# otherwise fetch the public install repo tarball. No credentials needed.
REPO_ROOT=""
for cand in "$SCRIPT_DIR/.." "$SCRIPT_DIR"; do
if [ -d "$cand/deploy/k8s" ]; then REPO_ROOT="$(cd "$cand" && pwd)"; break; fi
done
if [ -z "$REPO_ROOT" ]; then
say "No local deploy/k8s next to the script; fetching manifests"
TMP_SRC="$(mktemp -d)"
trap 'rm -rf "$TMP_SRC"' EXIT
curl -fsSL "$MANIFEST_TARBALL" | tar xz -C "$TMP_SRC" --strip-components=1
[ -d "$TMP_SRC/deploy/k8s" ] || die "failed to fetch manifests from $MANIFEST_TARBALL"
REPO_ROOT="$TMP_SRC"
fi
# ── kubectl / cluster access ─────────────────────────────────────────────
say "Checking cluster access"
KUBECTL=""
if command -v kubectl >/dev/null 2>&1 && kubectl get nodes >/dev/null 2>&1; then
KUBECTL="kubectl"
elif sudo -n true 2>/dev/null && sudo kubectl get nodes >/dev/null 2>&1; then
# k3s installs kubectl with a root-owned kubeconfig
KUBECTL="sudo kubectl"
elif command -v kubectl >/dev/null 2>&1 && sudo kubectl get nodes >/dev/null 2>&1; then
KUBECTL="sudo kubectl"
fi
if [ -z "$KUBECTL" ]; then
if [ "$INSTALL_K3S" = true ]; then
say "No working kubectl; installing k3s"
curl -sfL https://get.k3s.io | sudo sh -
KUBECTL="sudo kubectl"
else
die "no working kubectl. If this box has no Kubernetes, re-run with --install-k3s"
fi
fi
# Right after a fresh k3s install the node object does not exist yet, and
# `kubectl wait --all` errors out on zero resources instead of waiting.
# Poll for registration first.
NODE_WAIT=0
until [ -n "$($KUBECTL get nodes --no-headers 2>/dev/null)" ]; do
NODE_WAIT=$((NODE_WAIT+2))
[ "$NODE_WAIT" -ge 120 ] && die "no node registered after 120s; check: sudo systemctl status k3s"
sleep 2
done
$KUBECTL wait --for=condition=Ready node --all --timeout=180s >/dev/null
echo "Cluster reachable: $($KUBECTL get nodes --no-headers | wc -l) node(s) Ready (using: $KUBECTL)"
if command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet firewalld 2>/dev/null; then
warn "firewalld is active. On RHEL-family systems it blocks the UI NodePort (30410) and can break pod networking. Disable it (sudo systemctl disable --now firewalld) or allow the k3s networks and port 30410; see the README's distribution notes."
fi
if ! $KUBECTL get storageclass 2>/dev/null | grep -q '(default)'; then
warn "no default StorageClass; PVCs will stay Pending. k3s ships one; on other clusters install a provisioner first."
fi
# ── namespace + secrets ──────────────────────────────────────────────────
say "Namespace and secrets"
$KUBECTL get namespace "$NS" >/dev/null 2>&1 || $KUBECTL create namespace "$NS"
# Images now pull anonymously from registry.transpara.com; a ghcr-pull
# secret from an older install is left alone (nothing references it).
if $KUBECTL -n "$NS" get secret gateway-secrets >/dev/null 2>&1; then
echo "gateway-secrets exists, keeping it (existing CSG_API_KEY stays valid)"
else
$KUBECTL -n "$NS" create secret generic gateway-secrets \
--from-literal=CSG_API_KEY="$(openssl rand -hex 32)" \
--from-literal=CSG_ADMIN_PASSWORD="$(openssl rand -hex 12)"
fi
CSG_API_KEY=$($KUBECTL -n "$NS" get secret gateway-secrets -o jsonpath='{.data.CSG_API_KEY}' | base64 -d)
# ── deploy ───────────────────────────────────────────────────────────────
say "Applying manifests"
$KUBECTL apply -k "$REPO_ROOT/deploy/k8s/"
say "Waiting for rollouts (first image pulls can take a few minutes)"
$KUBECTL -n "$NS" rollout status deploy/claude-subscription-gateway --timeout=300s
$KUBECTL -n "$NS" rollout status deploy/model-builder --timeout=300s
say "Health checks"
$KUBECTL -n "$NS" exec deploy/claude-subscription-gateway -- curl -sf http://localhost:8790/healthz >/dev/null \
&& echo "gateway /healthz ok" || die "gateway /healthz failed"
$KUBECTL -n "$NS" exec deploy/claude-subscription-gateway -- curl -sf http://localhost:8790/readyz >/dev/null \
&& echo "gateway /readyz ok" || die "gateway /readyz failed"
$KUBECTL -n "$NS" exec deploy/model-builder -- python -c \
"import urllib.request; urllib.request.urlopen('http://localhost:4010/health', timeout=10)" \
&& echo "model-builder /health ok" || die "model-builder /health failed"
# Self-heal: if a token is already stored but the deployment is not wired to
# read it (e.g. a cached manifest tarball lacked the env entry), wire it now.
if $KUBECTL -n "$NS" get secret gateway-secrets -o jsonpath='{.data.CSG_CLAUDE_OAUTH_TOKEN}' 2>/dev/null | grep -q . \
&& ! $KUBECTL -n "$NS" get deploy claude-subscription-gateway -o jsonpath='{.spec.template.spec.containers[0].env[*].name}' | grep -q CSG_CLAUDE_OAUTH_TOKEN; then
say "Wiring the stored subscription token into the gateway"
$KUBECTL -n "$NS" set env deploy/claude-subscription-gateway --from=secret/gateway-secrets --keys=CSG_CLAUDE_OAUTH_TOKEN
$KUBECTL -n "$NS" rollout status deploy/claude-subscription-gateway --timeout=180s
fi
# ── Claude subscription login ────────────────────────────────────────────
probe_login() {
$KUBECTL -n "$NS" exec deploy/claude-subscription-gateway -- \
curl -s -o /dev/null -w '%{http_code}' --max-time 120 \
-X POST http://localhost:8790/v1/messages \
-H "x-api-key: $CSG_API_KEY" -H 'content-type: application/json' \
-d '{"model":"sonnet","max_tokens":8,"messages":[{"role":"user","content":"Reply with exactly: ok"}]}' \
2>/dev/null || echo 000
}
say "Checking Claude subscription login"
LOGGED_IN=false
CODE=$(probe_login)
if [ "$CODE" = "200" ]; then
echo "Subscription login present and working"
LOGGED_IN=true
elif [ "$SKIP_LOGIN" = true ] || [ ! -t 0 ]; then
warn "gateway not logged in (probe returned $CODE); skipping interactive login"
else
echo "The gateway has no working Claude login yet (probe returned $CODE)."
read -rp "Run 'claude setup-token' in the gateway pod now? [y/N] " yn
if [ "${yn,,}" = "y" ]; then
$KUBECTL -n "$NS" exec -it deploy/claude-subscription-gateway -- claude setup-token || true
echo
echo "The CLI printed a long-lived token (sk-ant-oat01-...) but does NOT store it."
echo "Paste it below; it is saved into the gateway-secrets secret, which the"
echo "gateway injects into every claude call (survives restarts and updates)."
# Validate the paste: the token is ~108 chars and often wraps across two
# terminal lines when printed, so a scrollback copy can carry a line
# break and silently truncate at the hidden prompt (seen live: stored
# half-token -> 401 on every call).
while true; do
read -rsp "Token (hidden; Enter to skip): " OAUTH_TOKEN; echo
[ -z "$OAUTH_TOKEN" ] && break
OAUTH_TOKEN="$(printf %s "$OAUTH_TOKEN" | tr -d '[:space:]')"
case "$OAUTH_TOKEN" in
sk-ant-oat01-*)
if [ "${#OAUTH_TOKEN}" -ge 80 ]; then break; fi
echo "That looks truncated (${#OAUTH_TOKEN} chars, expected ~108). If the token"
echo "wrapped across two lines on screen, the clipboard split it: paste it into"
echo "a text editor, join it to one line, copy again, then retry."
;;
*)
echo "That does not look like a setup-token value (should start with sk-ant-oat01-). Try again."
;;
esac
done
if [ -n "$OAUTH_TOKEN" ]; then
$KUBECTL -n "$NS" patch secret gateway-secrets --type merge \
-p "{\"stringData\":{\"CSG_CLAUDE_OAUTH_TOKEN\":\"$OAUTH_TOKEN\"}}"
# Belt and suspenders: ensure the deployment reads the secret even if
# the applied manifest predates the token wiring.
$KUBECTL -n "$NS" set env deploy/claude-subscription-gateway \
--from=secret/gateway-secrets --keys=CSG_CLAUDE_OAUTH_TOKEN >/dev/null || true
$KUBECTL -n "$NS" rollout restart deploy/claude-subscription-gateway
$KUBECTL -n "$NS" rollout status deploy/claude-subscription-gateway --timeout=180s
fi
CODE=$(probe_login)
if [ "$CODE" = "200" ]; then
echo "Login verified end to end"
LOGGED_IN=true
else
warn "probe still returns $CODE; see the install guide, step 5"
fi
fi
fi
# ── wire Model Builder to the gateway (settings API) ─────────────────────
say "Configuring Model Builder's gateway settings"
$KUBECTL -n "$NS" exec deploy/model-builder -- \
env MB_KEY="$CSG_API_KEY" MB_URL="$GATEWAY_URL_IN_CLUSTER" python -c "
import json, os, urllib.request
body = json.dumps({
'gateway_url': os.environ['MB_URL'],
'gateway_api_key': os.environ['MB_KEY'],
'gateway_model': 'opus',
}).encode()
req = urllib.request.Request('http://localhost:4010/settings/gateway', data=body,
method='PUT', headers={'Content-Type': 'application/json'})
print(urllib.request.urlopen(req, timeout=30).read().decode())
"
if [ "$LOGGED_IN" = true ]; then
say "Running a real test completion through the saved settings"
RESULT=$($KUBECTL -n "$NS" exec deploy/model-builder -- python -c "
import urllib.request
req = urllib.request.Request('http://localhost:4010/settings/gateway/test', data=b'', method='POST')
print(urllib.request.urlopen(req, timeout=180).read().decode())
")
echo "$RESULT"
echo "$RESULT" | grep -q '\"ok\":[ ]*true' || die "test completion failed; check the gateway logs"
fi
# ── summary ──────────────────────────────────────────────────────────────
NODE_IP=$($KUBECTL get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
# Report WHICH build is now running. "unchanged" from kubectl apply is
# ambiguous: it means the manifest matched, which is also what you see when
# the fetched manifests were stale. git_sha is stamped into the image at
# build time, so this is the running commit rather than a tag.
BUILD="$($KUBECTL -n "$NS" exec deploy/model-builder -- curl -sf --max-time 5 http://localhost:4010/health 2>/dev/null || true)"
say "Done"
if [ -n "$BUILD" ]; then
echo "Running: $BUILD"
fi
echo "UI: http://$NODE_IP:30410"
echo "Gateway key: $KUBECTL -n $NS get secret gateway-secrets -o jsonpath='{.data.CSG_API_KEY}' | base64 -d"
echo "Admin UI: $KUBECTL -n $NS port-forward svc/claude-subscription-gateway 8790:8790 -> http://localhost:8790/admin"
if [ "$LOGGED_IN" != true ]; then
echo
echo "STILL TO DO: connect the Claude subscription. Re-run this script, answer y"
echo "at the login step, and paste the token the CLI prints when asked."
fi