release v0.1.5

This commit is contained in:
gpuk-release
2026-09-17 00:02:06 +00:00
parent 406d457b8d
commit 983733003b
4 changed files with 524 additions and 128 deletions
+235 -33
View File
@@ -104,6 +104,72 @@ install_binary() { # [local-path]
gen_secret() { head -c 32 /dev/urandom | base64 | tr '+/' '-_' | tr -d '='; } gen_secret() { head -c 32 /dev/urandom | base64 | tr '+/' '-_' | tr -d '='; }
# The host ports a leftover app container is reached on ("ui mtls inference"),
# read off the container itself with the precedence the controller applies
# (core/published-ports.ts): host networking moves the listeners (GPUK_PORT,
# GPUK_MTLS_PORT, GPUK_LISTEN_ADDR); anything else keeps the image's fixed
# listeners and publishes them (GPUK_PUBLIC_*, then the port bindings). Empty
# when there is no such container or no docker.
leftover_container_ports() { # $1 = container name
command -v docker >/dev/null 2>&1 || return 0
# Same format string as install.sh's container_facts — one reading, two scripts.
_f=$(docker inspect -f '{{.State.Status}}|{{.HostConfig.NetworkMode}}|{{range $p, $b := .HostConfig.PortBindings}}{{range $b}}{{$p}}={{.HostPort}} {{end}}{{end}}{{"\n"}}{{range .Config.Env}}{{.}}{{"\n"}}{{end}}' "$1" 2>/dev/null) \
|| return 0
[ -n "$_f" ] || return 0
_head=$(printf '%s\n' "$_f" | head -1)
_r=${_head#*|}; _net=${_r%%|*}; _bind=${_r#*|}
_env() { printf '%s\n' "$_f" | sed -n "s/^$1=//p" | head -1; }
_pub() { printf '%s\n' "$_bind" | tr ' ' '\n' | sed -n "s/^$1\/tcp=//p" | head -1; }
if [ "$_net" = "host" ]; then
_ui=$(_env GPUK_PORT); _mtls=$(_env GPUK_MTLS_PORT)
_inf=$(_env GPUK_PROXY_PUBLIC_PORT)
[ -n "$_inf" ] || { _la=$(_env GPUK_LISTEN_ADDR); _inf=${_la##*:}; }
else
_ui=$(_env GPUK_PUBLIC_PORT); [ -n "$_ui" ] || _ui=$(_pub 8080)
_mtls=$(_env GPUK_PUBLIC_MTLS_PORT); [ -n "$_mtls" ] || _mtls=$(_pub 8443)
_inf=$(_env GPUK_PROXY_PUBLIC_PORT); [ -n "$_inf" ] || _inf=$(_pub 8200)
fi
printf '%s %s %s' "${_ui:-8080}" "${_mtls:-8443}" "${_inf:-8200}"
}
# Who holds a port: "process", "process in container NAME", or "". The process's
# cgroup names its container (host networking); a bridged publication is found
# as the container's port mapping in `docker ps` (the host-side holder is
# docker-proxy, which says nothing by itself). Same reading as install.sh.
port_owner() { # $1 = ss|netstat, $2 = port
_proc=""; _pid=""
case "$1" in
ss)
_line=$(ss -ltnpH "sport = :$2" 2>/dev/null | head -1)
_proc=$(printf '%s' "$_line" | sed -n 's/.*users:((\"\([^"]*\)\".*/\1/p')
_pid=$(printf '%s' "$_line" | sed -n 's/.*pid=\([0-9]*\).*/\1/p')
;;
netstat)
_field=$(netstat -ltnp 2>/dev/null \
| awk -v p="$2" '{n=split($4,a,":"); if (a[n]==p) {print $NF; exit}}')
case "$_field" in
*/*) _pid=${_field%%/*}; _proc=${_field#*/} ;;
*) _proc="$_field" ;;
esac
;;
esac
case "$_pid" in *[!0-9]*|"") _pid="" ;; esac
_ctr=""
if command -v docker >/dev/null 2>&1; then
if [ -n "$_pid" ] && [ -r "/proc/$_pid/cgroup" ]; then
_cid=$(sed -n 's#.*docker[-/]\([0-9a-f]\{64\}\).*#\1#p' "/proc/$_pid/cgroup" 2>/dev/null | head -1)
[ -z "$_cid" ] || _ctr=$(docker inspect -f '{{.Name}}' "$_cid" 2>/dev/null | sed 's|^/||')
fi
[ -n "$_ctr" ] || _ctr=$(docker ps --format '{{.Names}} {{.Ports}}' 2>/dev/null \
| awk -v p=":$2->" 'index($0, p) { print $1; exit }')
fi
if [ -n "$_ctr" ]; then
printf '%s' "${_proc:-a process} in container $_ctr"
else
printf '%s' "$_proc"
fi
}
seed_secrets() { # DATA_ROOT seed_secrets() { # DATA_ROOT
_sd="$1/secrets" _sd="$1/secrets"
mkdir -p "$_sd"; chmod 0700 "$_sd" mkdir -p "$_sd"; chmod 0700 "$_sd"
@@ -191,7 +257,7 @@ write_caddyfile() {
# #
# The app already runs with GPUK_HSTS=true and GPUK_SESSION_COOKIE_SECURE=true # The app already runs with GPUK_HSTS=true and GPUK_SESSION_COOKIE_SECURE=true
# (set by the public profile). What does NOT go through this proxy: # (set by the public profile). What does NOT go through this proxy:
# - the worker mTLS channel (:8443): workers pin the controller CA and must # - the worker mTLS channel (:$MTLS_PORT): workers pin the controller CA and must
# reach it DIRECTLY — terminating it here would break the pin. # reach it DIRECTLY — terminating it here would break the pin.
# - worker<->worker data transfers (:8300): LAN-only by contract (OPS-68). # - worker<->worker data transfers (:8300): LAN-only by contract (OPS-68).
@@ -205,7 +271,7 @@ $DOMAIN {
# #
# inference.$DOMAIN { # inference.$DOMAIN {
# encode zstd gzip # encode zstd gzip
# reverse_proxy localhost:8200 # reverse_proxy localhost:$INFERENCE_PORT
# } # }
CADDY CADDY
chmod 0644 "$DATA_ROOT/caddy/Caddyfile" chmod 0644 "$DATA_ROOT/caddy/Caddyfile"
@@ -242,8 +308,14 @@ hint_existing_caches() {
cmd_install() { cmd_install() {
IMAGE=""; MODE="worker"; CLUSTER="default"; DATA_ROOT="/var/lib/gpu-kitchen" IMAGE=""; MODE="worker"; CLUSTER="default"; DATA_ROOT="/var/lib/gpu-kitchen"
CACHE_DIR=""; NETWORK="host"; CONTROLLER_URL=""; BIN_SRC=""; HEALTH_PORT="8001" CACHE_DIR=""; NETWORK="host"; CONTROLLER_URL=""; BIN_SRC=""; HEALTH_PORT="8001"
# 1337, not 8080: kept in lockstep with install.sh's default (INS-01). # 1337, not 8080: kept in lockstep with install.sh's default (INS-01). The
ENROLL_TOKEN=""; HTTP_PORT="1337"; PROFILE=""; DOMAIN=""; DRY_RUN=0 # worker channel and the inference endpoint move the same way (INS-46).
ENROLL_TOKEN=""; HTTP_PORT="1337"; MTLS_PORT="8443"; INFERENCE_PORT="8200"
# Worker data plane (OPS-68): the daemon reads GPUK_DATA_PORT,
# GPUK_MODEL_TRANSFER_PORT and GPUK_HANDOVER_DATA_PORT from worker.env and
# announces the first two to the controller, so moving them here is complete.
DATA_PORT="8300"; TRANSFER_PORT="8301"; HANDOVER_PORT="8302"
PROFILE=""; DOMAIN=""; DRY_RUN=0
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
--image) IMAGE="$2"; shift 2 ;; --image) IMAGE="$2"; shift 2 ;;
@@ -261,6 +333,11 @@ cmd_install() {
--enroll-token|--token) ENROLL_TOKEN="$2"; shift 2 ;; --enroll-token|--token) ENROLL_TOKEN="$2"; shift 2 ;;
--health-port) HEALTH_PORT="$2"; shift 2 ;; --health-port) HEALTH_PORT="$2"; shift 2 ;;
--http-port) HTTP_PORT="$2"; shift 2 ;; --http-port) HTTP_PORT="$2"; shift 2 ;;
--mtls-port) MTLS_PORT="$2"; shift 2 ;;
--inference-port) INFERENCE_PORT="$2"; shift 2 ;;
--data-port) DATA_PORT="$2"; shift 2 ;;
--transfer-port) TRANSFER_PORT="$2"; shift 2 ;;
--handover-port) HANDOVER_PORT="$2"; shift 2 ;;
--binary) BIN_SRC="$2"; shift 2 ;; --binary) BIN_SRC="$2"; shift 2 ;;
--dry-run) DRY_RUN=1; shift ;; --dry-run) DRY_RUN=1; shift ;;
*) die "unknown install option: $1" ;; *) die "unknown install option: $1" ;;
@@ -279,6 +356,26 @@ cmd_install() {
if [ "$MODE" = "worker" ] && [ -n "$PROFILE" ]; then if [ "$MODE" = "worker" ] && [ -n "$PROFILE" ]; then
die "--profile applies only to --mode controller; workers do not have an installation profile" die "--profile applies only to --mode controller; workers do not have an installation profile"
fi fi
for _pv in "$HTTP_PORT" "$MTLS_PORT" "$INFERENCE_PORT" "$HEALTH_PORT" \
"$DATA_PORT" "$TRANSFER_PORT" "$HANDOVER_PORT"; do
case "$_pv" in
''|*[!0-9]*) die "not a port number: '$_pv'" ;;
esac
[ "$_pv" -ge 1 ] && [ "$_pv" -le 65535 ] || die "port out of range: $_pv"
done
if [ "$HTTP_PORT" = "$MTLS_PORT" ] || [ "$HTTP_PORT" = "$INFERENCE_PORT" ] || [ "$MTLS_PORT" = "$INFERENCE_PORT" ]; then
die "--http-port, --mtls-port and --inference-port must differ (got $HTTP_PORT, $MTLS_PORT, $INFERENCE_PORT)"
fi
# The worker's four listeners (health + data plane) must differ too; the
# handover port is the data port's temporary twin (WRK-173), never the same.
_seen=""
for _pv in "$HEALTH_PORT" "$DATA_PORT" "$TRANSFER_PORT" "$HANDOVER_PORT"; do
case " $_seen " in
*" $_pv "*) die "--health-port, --data-port, --transfer-port and --handover-port must differ (got $HEALTH_PORT, $DATA_PORT, $TRANSFER_PORT, $HANDOVER_PORT)" ;;
esac
_seen="$_seen $_pv"
done
case "$PROFILE" in case "$PROFILE" in
""|homelab|studio|enterprise|public) ;; ""|homelab|studio|enterprise|public) ;;
*) die "--profile must be homelab, studio, enterprise or public (got '$PROFILE')" ;; *) die "--profile must be homelab, studio, enterprise or public (got '$PROFILE')" ;;
@@ -319,32 +416,54 @@ cmd_install() {
if [ "$DRY_RUN" -eq 1 ]; then if [ "$DRY_RUN" -eq 1 ]; then
[ "$MODE" != "controller" ] || CLAIM_CODE_AVAILABLE=1 [ "$MODE" != "controller" ] || CLAIM_CODE_AVAILABLE=1
echo "==> dry run: no file, service or container was changed" echo "==> dry run: no file, service or container was changed"
[ ! -f "$MANIFEST" ] \
|| echo "==> dry run: $MANIFEST exists — this render would be MERGED into it, controller-owned settings kept"
if [ "$MODE" = "worker" ]; then render_worker_manifest; else render_controller_manifest; fi if [ "$MODE" = "worker" ]; then render_worker_manifest; else render_controller_manifest; fi
[ -z "$DOMAIN" ] || echo "==> dry run: would write $DATA_ROOT/caddy/Caddyfile for $DOMAIN" [ -z "$DOMAIN" ] || echo "==> dry run: would write $DATA_ROOT/caddy/Caddyfile for $DOMAIN"
return 0 return 0
fi fi
# ── Port conflicts (INS-46) — the mutator's own guard ──────────────────────── # ── Port conflicts (INS-46) — the mutator's own guard ────────────────────────
# install.sh's preflight already checks these, and on a terminal it can offer # install.sh's preflight already checks these, and on a terminal it offers an
# an alternative port. gpuk is the actual mutator and contributors call it # alternative port. gpuk is the actual mutator and contributors call it
# DIRECTLY, so it re-checks and refuses, non-interactively. Same helpers as # DIRECTLY, so it re-checks and refuses, non-interactively, naming the flag
# install.sh (both scripts ship standalone from the channel). A listener owned # that moves the port. Same helpers as install.sh (both scripts ship standalone
# by an existing install is not a conflict: a manifest on disk means the # from the channel). A listener owned by an existing install is not a conflict:
# re-run is the update path, and every checked port is then our own. # a manifest on disk means the re-run is the update path, and every checked
# port is then our own; without a manifest, a leftover app container still
# holds OUR ports — apply() renames and stops it before the new one starts.
if [ ! -f "$MANIFEST" ]; then if [ ! -f "$MANIFEST" ]; then
_port_tool="" _port_tool="${GPUK_PORT_CHECK_TOOL:-}"
case "$_port_tool" in
""|ss|netstat) ;;
*) die "GPUK_PORT_CHECK_TOOL must be ss or netstat (got '$_port_tool')" ;;
esac
if [ -z "$_port_tool" ]; then
if command -v ss >/dev/null 2>&1; then _port_tool="ss" if command -v ss >/dev/null 2>&1; then _port_tool="ss"
elif command -v netstat >/dev/null 2>&1; then _port_tool="netstat"; fi elif command -v netstat >/dev/null 2>&1; then _port_tool="netstat"; fi
fi
_ours=""
if [ "$MODE" = "controller" ]; then
_ours=$(leftover_container_ports gpu-kitchen)
[ -z "$_ours" ] || echo "==> existing app container gpu-kitchen found (ports $_ours): the install replaces it"
fi
if [ -z "$_port_tool" ]; then if [ -z "$_port_tool" ]; then
echo "==> warning: cannot check for port conflicts (no ss or netstat)" echo "==> warning: cannot check for port conflicts (no ss or netstat)"
else else
if [ "$MODE" = "controller" ]; then if [ "$MODE" = "controller" ]; then
set -- "$HTTP_PORT" 8443 8200 set -- "$HTTP_PORT:UI:--http-port" \
"$MTLS_PORT:worker channel:--mtls-port" \
"$INFERENCE_PORT:inference endpoint:--inference-port"
else else
# Worker data-plane ports (OPS-68) plus the local health listener. # Worker data-plane ports (OPS-68) plus the local health listener.
set -- "$HEALTH_PORT" 8300 8301 8302 set -- "$HEALTH_PORT:worker health:--health-port" \
"$DATA_PORT:worker data plane:--data-port" \
"$TRANSFER_PORT:model transfers:--transfer-port" \
"$HANDOVER_PORT:handover data plane:--handover-port"
fi fi
for _p in "$@"; do for _spec in "$@"; do
_p=${_spec%%:*}; _rest=${_spec#*:}; _label=${_rest%%:*}; _flag=${_rest#*:}
case " $_ours " in *" $_p "*) continue ;; esac
_busy=1 _busy=1
case "$_port_tool" in case "$_port_tool" in
ss) [ -n "$(ss -ltnH "sport = :$_p" 2>/dev/null)" ] || _busy=0 ;; ss) [ -n "$(ss -ltnH "sport = :$_p" 2>/dev/null)" ] || _busy=0 ;;
@@ -354,7 +473,11 @@ cmd_install() {
|| _busy=0 || _busy=0
;; ;;
esac esac
[ "$_busy" -eq 0 ] || die "port $_p is already in use. Free it first, then run the install again." [ "$_busy" -eq 0 ] && continue
_owner=$(port_owner "$_port_tool" "$_p")
_hint="Free it first, then run the install again."
[ -z "$_flag" ] || _hint="Pass $_flag <p> to choose another port, or free it first."
die "port $_p ($_label) is already in use by ${_owner:-an unknown process}. $_hint"
done done
fi fi
fi fi
@@ -378,6 +501,9 @@ cmd_install() {
{ {
echo "GPUK_CLUSTER=$CLUSTER" echo "GPUK_CLUSTER=$CLUSTER"
echo "GPUK_WORKER_HEALTH_PORT=$HEALTH_PORT" echo "GPUK_WORKER_HEALTH_PORT=$HEALTH_PORT"
echo "GPUK_DATA_PORT=$DATA_PORT"
echo "GPUK_MODEL_TRANSFER_PORT=$TRANSFER_PORT"
echo "GPUK_HANDOVER_DATA_PORT=$HANDOVER_PORT"
echo "GPUK_MACHINE_ID_FILE=$MACHINE_ID_FILE" echo "GPUK_MACHINE_ID_FILE=$MACHINE_ID_FILE"
echo "NODE_DISPLAY_NAME=$(hostname)" echo "NODE_DISPLAY_NAME=$(hostname)"
[ -n "$CONTROLLER_URL" ] && echo "GPUK_CONTROLLER_URLS=$CONTROLLER_URL" [ -n "$CONTROLLER_URL" ] && echo "GPUK_CONTROLLER_URLS=$CONTROLLER_URL"
@@ -392,7 +518,7 @@ cmd_install() {
echo "GPUK_CLUSTER=$CLUSTER" echo "GPUK_CLUSTER=$CLUSTER"
echo "GPUK_WORKER_HEALTH_PORT=$HEALTH_PORT" echo "GPUK_WORKER_HEALTH_PORT=$HEALTH_PORT"
echo "GPUK_MACHINE_ID_FILE=$MACHINE_ID_FILE" echo "GPUK_MACHINE_ID_FILE=$MACHINE_ID_FILE"
echo "GPUK_CONTROLLER_URLS=wss://127.0.0.1:8443" echo "GPUK_CONTROLLER_URLS=wss://127.0.0.1:$MTLS_PORT"
echo "GPUK_SELF_ENROLL_FILE=$SELF_ENROLL_FILE" echo "GPUK_SELF_ENROLL_FILE=$SELF_ENROLL_FILE"
echo "NODE_DISPLAY_NAME=$(hostname)" echo "NODE_DISPLAY_NAME=$(hostname)"
} > "$WORKER_ENV" } > "$WORKER_ENV"
@@ -439,7 +565,7 @@ cmd_install() {
# No unix socket any more: workerd reconciles the app container in-process from # No unix socket any more: workerd reconciles the app container in-process from
# the on-disk manifest (there is no backend to relay through on the very first # the on-disk manifest (there is no backend to relay through on the very first
# boot). Steady-state updates go through the daemon over WS. # boot). Steady-state updates go through the daemon over WS.
"$BIN_DEST" apply || die "apply failed. Check: gpuk logs" GPUK_MANIFEST_PATH="$MANIFEST" "$BIN_DEST" apply || die "apply failed. Check: gpuk logs"
echo echo
echo "Done. The worker daemon is running and the app container is up." echo "Done. The worker daemon is running and the app container is up."
echo " Status : gpuk status App logs: gpuk logs Daemon: journalctl -u $SERVICE_NAME" echo " Status : gpuk status App logs: gpuk logs Daemon: journalctl -u $SERVICE_NAME"
@@ -463,7 +589,36 @@ render_worker_manifest() {
JSON JSON
} }
write_worker_manifest() { render_worker_manifest > "$MANIFEST"; } # Every env key gpuk may ever write into a manifest — conditional ones included.
# On a re-run the merge sets each of them to the fresh value or DELETES it when the
# fresh render no longer carries it (a profile change drops GPUK_HSTS); any other
# key was pushed by the controller and is kept. Keep this list in step with
# render_controller_manifest.
INSTALL_ENV_KEYS="NODE_ENV,GPUK_MODE,GPUK_CLUSTER,GPUK_SELF_ENROLL_FILE,NODE_DISPLAY_NAME,HF_HOME,GPUK_DATA_ROOT,GPUK_INSTALL_PROFILE,GPUK_HSTS,GPUK_SESSION_COOKIE_SECURE,GPUK_BOOTSTRAP_MUST_CHANGE,BACKEND_DOCKER_NETWORK,GPUK_PORT,GPUK_PUBLIC_PORT,GPUK_MTLS_PORT,GPUK_PUBLIC_MTLS_PORT,GPUK_LISTEN_ADDR,GPUK_PROXY_PUBLIC_PORT"
# Write the manifest — or, when one exists, MERGE into it (INS-03). Re-running the
# installer is the update path, and the manifest is not ours alone: since the first
# install the controller has patched it (extra cache disks, shared origin, eviction,
# LED binary, ports moved from Settings -> Network…). Rewriting it from flags would
# silently undo all of that, so the fresh render goes through the daemon's own
# `install-manifest`, which only replaces what the installer owns (image, mode,
# cluster, network, data root, ports, secret references, the primary cache disk,
# the env keys above) and keeps the rest. The first write is the render as-is.
write_manifest() { # $1 = render function
if [ -f "$MANIFEST" ]; then
"$1" > "$MANIFEST.new"
chmod 0600 "$MANIFEST.new"
GPUK_MANIFEST_PATH="$MANIFEST" "$BIN_DEST" install-manifest \
--from "$MANIFEST.new" --own-env "$INSTALL_ENV_KEYS" >/dev/null \
|| { rm -f "$MANIFEST.new"; die "could not merge the new settings into $MANIFEST"; }
rm -f "$MANIFEST.new"
echo "==> merged into the existing $MANIFEST (controller-owned settings kept)"
else
"$1" > "$MANIFEST"
fi
}
write_worker_manifest() { write_manifest render_worker_manifest; }
# Controller manifest: the declarative app-container description workerd applies. # Controller manifest: the declarative app-container description workerd applies.
render_controller_manifest() { render_controller_manifest() {
@@ -490,18 +645,25 @@ render_controller_manifest() {
[ "$NETWORK" = "host" ] || EXTRA_ENV="$EXTRA_ENV,\"BACKEND_DOCKER_NETWORK\":\"$(json_str "$NETWORK")\"" [ "$NETWORK" = "host" ] || EXTRA_ENV="$EXTRA_ENV,\"BACKEND_DOCKER_NETWORK\":\"$(json_str "$NETWORK")\""
# Published != bound (INS-43). On a bridged network the container keeps the image's # Published != bound (INS-43). On a bridged network the container keeps the image's
# FIXED listeners — nginx 8080, worker mTLS 8443 — and --http-port only moves the HOST # FIXED listeners — nginx 8080, worker mTLS 8443, gpuk-proxy 8200 — and the port
# side of the publication; Settings -> Network moves it later by patching this same # flags only move the HOST side of the publication; Settings -> Network moves it
# manifest, so the container port must never become a variable. With host networking # later by patching this same manifest, so the container port must never become a
# nothing is published and the listener itself takes the port. # variable. With host networking nothing is published and the listeners themselves
# take the ports (core/published-ports.ts reads this back with the same rules; the
# proxy port is GPUK_LISTEN_ADDR for the proxy, GPUK_PROXY_PUBLIC_PORT for what the
# backend shows — core/cluster-settings.ts proxyPublicPort).
PORTS_JSON="{}" PORTS_JSON="{}"
if [ "$NETWORK" = "host" ]; then if [ "$NETWORK" = "host" ]; then
EXTRA_ENV="$EXTRA_ENV,\"GPUK_PORT\":\"$(json_str "$HTTP_PORT")\"" EXTRA_ENV="$EXTRA_ENV,\"GPUK_PORT\":\"$(json_str "$HTTP_PORT")\""
EXTRA_ENV="$EXTRA_ENV,\"GPUK_MTLS_PORT\":\"$(json_str "$MTLS_PORT")\""
EXTRA_ENV="$EXTRA_ENV,\"GPUK_LISTEN_ADDR\":\"0.0.0.0:$(json_str "$INFERENCE_PORT")\""
else else
EXTRA_ENV="$EXTRA_ENV,\"GPUK_PORT\":\"8080\"" EXTRA_ENV="$EXTRA_ENV,\"GPUK_PORT\":\"8080\""
EXTRA_ENV="$EXTRA_ENV,\"GPUK_PUBLIC_PORT\":\"$(json_str "$HTTP_PORT")\"" EXTRA_ENV="$EXTRA_ENV,\"GPUK_PUBLIC_PORT\":\"$(json_str "$HTTP_PORT")\""
PORTS_JSON="{\"8080\":$HTTP_PORT,\"8200\":8200,\"8443\":8443}" EXTRA_ENV="$EXTRA_ENV,\"GPUK_PUBLIC_MTLS_PORT\":\"$(json_str "$MTLS_PORT")\""
PORTS_JSON="{\"8080\":$HTTP_PORT,\"8200\":$INFERENCE_PORT,\"8443\":$MTLS_PORT}"
fi fi
EXTRA_ENV="$EXTRA_ENV,\"GPUK_PROXY_PUBLIC_PORT\":\"$(json_str "$INFERENCE_PORT")\""
cat <<JSON cat <<JSON
{ {
@@ -532,7 +694,7 @@ render_controller_manifest() {
JSON JSON
} }
write_controller_manifest() { render_controller_manifest > "$MANIFEST"; } write_controller_manifest() { write_manifest render_controller_manifest; }
# ── control subcommands ──────────────────────────────────────────────────────── # ── control subcommands ────────────────────────────────────────────────────────
container_name() { container_name() {
@@ -770,7 +932,7 @@ cmd_enroll() {
# has no app container — `apply` there is a no-op with a clear message. # has no app container — `apply` there is a no-op with a clear message.
cmd_apply() { cmd_apply() {
need_root need_root
"$BIN_DEST" apply GPUK_MANIFEST_PATH="$MANIFEST" "$BIN_DEST" apply
} }
# ── update ───────────────────────────────────────────────────────────────────── # ── update ─────────────────────────────────────────────────────────────────────
@@ -824,7 +986,7 @@ cmd_update() {
fi fi
if [ -z "$_want" ]; then if [ -z "$_want" ]; then
echo "==> cannot reach $CHANNEL_URL — re-applying the pinned $_current" echo "==> cannot reach $CHANNEL_URL — re-applying the pinned $_current"
"$BIN_DEST" apply GPUK_MANIFEST_PATH="$MANIFEST" "$BIN_DEST" apply
return 0 return 0
fi fi
# Pick the digest matching the installed edition by image basename — the # Pick the digest matching the installed edition by image basename — the
@@ -849,15 +1011,51 @@ cmd_update() {
else else
echo "==> already on $_current — re-pulling and recreating" echo "==> already on $_current — re-pulling and recreating"
fi fi
"$BIN_DEST" apply GPUK_MANIFEST_PATH="$MANIFEST" "$BIN_DEST" apply
} }
# ── uninstall ──────────────────────────────────────────────────────────────────
# Plain: stop and remove the service, touch nothing else (a pause, reversible by
# `gpuk install`). --purge: everything the installer created goes — the app
# container (and a leftover -old twin), $ETC_DIR with the manifest and the
# enrolled identity, the binary — EXCEPT the data root: database, models and the
# secrets (ENCRYPTION_KEY above all, OPS-04) are the operator's to delete, by
# hand, knowingly. After a purge the next install is a first install (INS-03);
# without it, install.sh sees the manifest and treats the re-run as an update.
cmd_uninstall() { cmd_uninstall() {
need_root need_root
_purge=0
while [ $# -gt 0 ]; do
case "$1" in
--purge) _purge=1; shift ;;
*) die "unknown uninstall option: $1 (expected: --purge)" ;;
esac
done
_cn=$(container_name)
_root=$(manifest_data_root)
systemctl disable --now "$SERVICE_NAME" 2>/dev/null || true systemctl disable --now "$SERVICE_NAME" 2>/dev/null || true
rm -f "$UNIT_DEST"; systemctl daemon-reload 2>/dev/null || true rm -f "$UNIT_DEST"; systemctl daemon-reload 2>/dev/null || true
echo "Removed the systemd service. Left in place: $BIN_DEST, $ETC_DIR (incl. identity)," echo "==> removed the systemd service"
echo "the data root and any app container. Delete them manually for a full cleanup." if [ "$_purge" -eq 0 ]; then
echo "Left in place: $BIN_DEST, $ETC_DIR (manifest, identity), the data root and any"
echo "app container — 'gpuk install' brings the service back on them."
echo "For a clean slate (everything but the data root): gpuk uninstall --purge"
return 0
fi
if [ -n "$_cn" ] && command -v docker >/dev/null 2>&1; then
for _c in "$_cn" "$_cn-old"; do
docker rm -f "$_c" >/dev/null 2>&1 && echo "==> removed container $_c"
done
fi
for _d in "$ETC_DIR" "$IDENTITY_DIR"; do
case "$_d" in ""|/|/etc|/usr|/var) die "refusing to remove $_d" ;; esac
[ ! -e "$_d" ] || { rm -rf "$_d"; echo "==> removed $_d"; }
done
[ ! -e "$MACHINE_ID_FILE" ] || rm -f "$MACHINE_ID_FILE"
[ ! -e "$BIN_DEST" ] || { rm -f "$BIN_DEST"; echo "==> removed $BIN_DEST"; }
echo
echo "Kept: the data root${_root:+ $_root} — database, models and secrets (ENCRYPTION_KEY)."
echo "A new install over it reuses them. To delete it too, knowingly: rm -rf ${_root:-<data-root>}"
} }
usage() { usage() {
@@ -865,10 +1063,12 @@ usage() {
gpuk — GPU Kitchen host daemon (gpu-kitchen-worker) gpuk — GPU Kitchen host daemon (gpu-kitchen-worker)
gpuk install --mode controller --image <ref> [--profile homelab|studio|enterprise|public] gpuk install --mode controller --image <ref> [--profile homelab|studio|enterprise|public]
[--cluster N] [--cache-dir P] [--cluster N] [--cache-dir P] [--data-root P]
[--data-root P] [--http-port P] [--network host|bridge|<net>] [--binary <path>] [--http-port P] [--mtls-port P] [--inference-port P]
[--network host|bridge|<net>] [--binary <path>]
gpuk install --mode worker --controller wss://<host>:<port> --enroll-token gk_enroll_... gpuk install --mode worker --controller wss://<host>:<port> --enroll-token gk_enroll_...
[--cluster N] [--cache-dir P] [--binary <path>] [--cluster N] [--cache-dir P] [--binary <path>]
[--health-port P] [--data-port P] [--transfer-port P] [--handover-port P]
gpuk install ... --dry-run Validate inputs and print the manifest without changing the host gpuk install ... --dry-run Validate inputs and print the manifest without changing the host
gpuk status Service state, enrollment, /health, app container status gpuk status Service state, enrollment, /health, app container status
gpuk enroll --controller wss://<host>:<port> --token gk_enroll_... gpuk enroll --controller wss://<host>:<port> --token gk_enroll_...
@@ -880,7 +1080,9 @@ gpuk — GPU Kitchen host daemon (gpu-kitchen-worker)
gpuk backup [DIR] (controller) Cold snapshot of the embedded pgdata (stop → tar → restart) gpuk backup [DIR] (controller) Cold snapshot of the embedded pgdata (stop → tar → restart)
gpuk manifest Print the current manifest gpuk manifest Print the current manifest
gpuk logs Follow the app container logs (controller) or the daemon journal gpuk logs Follow the app container logs (controller) or the daemon journal
gpuk uninstall Remove the systemd service gpuk uninstall Remove the systemd service, leave everything else in place
gpuk uninstall --purge Also remove the app container, /etc/gpu-kitchen and the binary
(never the data root: database, models, secrets)
Most people never run this directly: the channel's install.sh installs it Most people never run this directly: the channel's install.sh installs it
(https://repo.byterain.io/gpukitchen/channel/raw/branch/main/install.sh). (https://repo.byterain.io/gpukitchen/channel/raw/branch/main/install.sh).
@@ -902,7 +1104,7 @@ case "$cmd" in
_img=$(manifest_image) _img=$(manifest_image)
if [ -n "$_img" ]; then exec docker logs -f "$(container_name)"; else exec journalctl -u "$SERVICE_NAME" -f; fi if [ -n "$_img" ]; then exec docker logs -f "$(container_name)"; else exec journalctl -u "$SERVICE_NAME" -f; fi
;; ;;
uninstall) cmd_uninstall ;; uninstall) cmd_uninstall "$@" ;;
help|-h|--help) usage ;; help|-h|--help) usage ;;
*) usage; exit 1 ;; *) usage; exit 1 ;;
esac esac
+279 -85
View File
@@ -7,9 +7,12 @@
# channel repo is written by .gitea/workflows/release.yml, see specs/developpement/ci-cd.md.) # channel repo is written by .gitea/workflows/release.yml, see specs/developpement/ci-cd.md.)
# #
# What it does, and nothing more: # What it does, and nothing more:
# 0. existing detect an install already on this host (manifest, or the traces
# a previous one left) and KEEP its settings — ports, data root,
# profile — unless a flag says otherwise (INS-03)
# 1. preflight docker, the NVIDIA driver, a REAL `--gpus all` smoke test, and # 1. preflight docker, the NVIDIA driver, a REAL `--gpus all` smoke test, and
# the listening ports (INS-46 — a taken port fails HERE, not three # the listening ports (INS-46 — a taken port is resolved HERE, on
# minutes later in a health-check timeout) # the terminal, not three minutes later in a health-check timeout)
# 2. resolve the current release from the channel (a TAG — never a floating # 2. resolve the current release from the channel (a TAG — never a floating
# `latest`: an install that silently changes version under you is # `latest`: an install that silently changes version under you is
# not an install, it is a surprise) # not an install, it is a surprise)
@@ -47,12 +50,26 @@ CACHE_DIR="${GPUK_CACHE_DIR:-}"
# 1337, not 8080: the single most-squatted port in existence would make the # 1337, not 8080: the single most-squatted port in existence would make the
# conflict preflight fire on half the lab boxes out there (INS-01). # conflict preflight fire on half the lab boxes out there (INS-01).
PORT="${GPUK_PORT:-1337}" PORT="${GPUK_PORT:-1337}"
# Fixed listeners: mirror of the gpuk-proxy default (core/cluster-settings.ts # The worker mTLS channel and the gpuk-proxy inference endpoint (defaults mirror
# proxyPublicPort) and of the worker mTLS channel — no install-time flag moves # core/cluster-settings.ts). Movable at install time like the UI port (INS-46):
# them (INS-46). # 8443 is every second appliance's HTTPS alias and 8200 is HashiCorp Vault's.
INFERENCE_PORT=8200 MTLS_PORT="${GPUK_MTLS_PORT:-8443}"
MTLS_PORT=8443 INFERENCE_PORT="${GPUK_INFERENCE_PORT:-8200}"
CLUSTER="${GPUK_CLUSTER:-default}" CLUSTER="${GPUK_CLUSTER:-default}"
# Which of those came from the operator (flag or env) — an existing install keeps
# its own value for everything the operator did not ask to change (INS-03).
PORT_GIVEN=0; [ -z "${GPUK_PORT:-}" ] || PORT_GIVEN=1
MTLS_GIVEN=0; [ -z "${GPUK_MTLS_PORT:-}" ] || MTLS_GIVEN=1
INFERENCE_GIVEN=0; [ -z "${GPUK_INFERENCE_PORT:-}" ] || INFERENCE_GIVEN=1
DATA_ROOT_GIVEN=0; [ -z "${GPUK_DATA_ROOT:-}" ] || DATA_ROOT_GIVEN=1
CACHE_GIVEN=0; [ -z "${GPUK_CACHE_DIR:-}" ] || CACHE_GIVEN=1
CLUSTER_GIVEN=0; [ -z "${GPUK_CLUSTER:-}" ] || CLUSTER_GIVEN=1
# Where an existing install keeps its manifest. Same override as gpuk's, and for
# the same reason: it is the only way to exercise the re-run path without root.
ETC_DIR="${GPUK_ETC_DIR:-/etc/gpu-kitchen}"
MANIFEST="$ETC_DIR/manifest.json"
UNIT_DEST="${GPUK_UNIT_DEST:-/etc/systemd/system/gpu-kitchen-worker.service}"
BIN_DEST="${GPUK_BIN_DEST:-/usr/local/bin/gpu-kitchen-worker}"
PROFILE="" PROFILE=""
DOMAIN="" DOMAIN=""
VERSION="" VERSION=""
@@ -102,6 +119,8 @@ Options:
--image <ref> Use this controller image outright (implies --version none) --image <ref> Use this controller image outright (implies --version none)
--edition <ed> community (default) | enterprise --edition <ed> community (default) | enterprise
--port <p> Port the UI listens on (default 1337) --port <p> Port the UI listens on (default 1337)
--mtls-port <p> Port workers dial to join this controller (default 8443)
--inference-port <p> Port of the OpenAI-compatible inference endpoint (default 8200)
--data-root <path> Where the database and secrets live (default /var/lib/gpu-kitchen) --data-root <path> Where the database and secrets live (default /var/lib/gpu-kitchen)
--cache-dir <path> Model cache (default <data-root>/hf) --cache-dir <path> Model cache (default <data-root>/hf)
--cluster <name> Cluster name workers join (default "default") --cluster <name> Cluster name workers join (default "default")
@@ -113,7 +132,8 @@ Options:
--worker-binary <p> Use a locally-built gpu-kitchen-worker instead of downloading one --worker-binary <p> Use a locally-built gpu-kitchen-worker instead of downloading one
--gpuk-script <p> Use a local copy of the gpuk installer --gpuk-script <p> Use a local copy of the gpuk installer
--skip-gpu-check Skip the 'docker run --gpus all' smoke test --skip-gpu-check Skip the 'docker run --gpus all' smoke test
--skip-preflight Skip the host checks entirely (CI: no docker, no GPU) --skip-preflight Skip the docker, driver, GPU and disk checks (CI: no docker,
no GPU); the listening ports are still checked
--dry-run Run the preflight and resolve the release, change nothing --dry-run Run the preflight and resolve the release, change nothing
-h, --help This -h, --help This
EOF EOF
@@ -124,10 +144,12 @@ while [ $# -gt 0 ]; do
--version) VERSION="$2"; shift 2 ;; --version) VERSION="$2"; shift 2 ;;
--image) IMAGE="$2"; shift 2 ;; --image) IMAGE="$2"; shift 2 ;;
--edition) EDITION="$2"; shift 2 ;; --edition) EDITION="$2"; shift 2 ;;
--port) PORT="$2"; shift 2 ;; --port) PORT="$2"; PORT_GIVEN=1; shift 2 ;;
--data-root) DATA_ROOT="$2"; shift 2 ;; --mtls-port) MTLS_PORT="$2"; MTLS_GIVEN=1; shift 2 ;;
--cache-dir) CACHE_DIR="$2"; shift 2 ;; --inference-port) INFERENCE_PORT="$2"; INFERENCE_GIVEN=1; shift 2 ;;
--cluster) CLUSTER="$2"; shift 2 ;; --data-root) DATA_ROOT="$2"; DATA_ROOT_GIVEN=1; shift 2 ;;
--cache-dir) CACHE_DIR="$2"; CACHE_GIVEN=1; shift 2 ;;
--cluster) CLUSTER="$2"; CLUSTER_GIVEN=1; shift 2 ;;
--profile) PROFILE="$2"; shift 2 ;; --profile) PROFILE="$2"; shift 2 ;;
--domain) DOMAIN="$2"; shift 2 ;; --domain) DOMAIN="$2"; shift 2 ;;
--non-interactive) NON_INTERACTIVE=1; shift ;; --non-interactive) NON_INTERACTIVE=1; shift ;;
@@ -141,8 +163,6 @@ while [ $# -gt 0 ]; do
esac esac
done done
[ -n "$CACHE_DIR" ] || CACHE_DIR="$DATA_ROOT/hf"
case "$EDITION" in case "$EDITION" in
community|enterprise) ;; community|enterprise) ;;
*) die "--edition must be community or enterprise (got '$EDITION')" ;; *) die "--edition must be community or enterprise (got '$EDITION')" ;;
@@ -157,9 +177,132 @@ case "$DOMAIN" in
*[!A-Za-z0-9.-]*) die "--domain must be a bare domain name (got '$DOMAIN')" ;; *[!A-Za-z0-9.-]*) die "--domain must be a bare domain name (got '$DOMAIN')" ;;
esac esac
for _pv in "$PORT" "$MTLS_PORT" "$INFERENCE_PORT"; do
case "$_pv" in
''|*[!0-9]*) die "not a port number: '$_pv'" ;;
esac
[ "$_pv" -ge 1 ] && [ "$_pv" -le 65535 ] || die "port out of range: $_pv"
done
echo echo
echo "${BOLD}GPU Kitchen — installing the $EDITION controller${RESET}" echo "${BOLD}GPU Kitchen — installing the $EDITION controller${RESET}"
# ── 0. An existing install (INS-03) ──────────────────────────────────────────
# Re-running this script is the update path, so before checking anything it
# reads what is already here — and KEEPS it. An update that silently moved the
# UI to another port, re-asked the profile (Enter = homelab would downgrade a
# public install) or pointed at a fresh data root beside the real one is not an
# update. Read-only. The manifest is the authority (WRK-55); without it, the
# traces a previous install leaves (container, unit, binary, data root) still
# mean "take over", never "start beside", and the container's own ports are ours.
manifest_str() { # $1 = key of a string field, anywhere in the manifest
sed -n "s/.*\"$1\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" "$MANIFEST" 2>/dev/null | head -1
}
manifest_binding() { # $1 = container port → the host port the ports table publishes it on
sed -n "s/.*\"$1\(\/tcp\)\{0,1\}\"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\2/p" "$MANIFEST" 2>/dev/null | head -1
}
C_STATUS=""; C_NETMODE=""
E_PORT=""; E_MTLS_PORT=""; E_LISTEN_ADDR=""
E_PUBLIC_PORT=""; E_PUBLIC_MTLS_PORT=""; E_PROXY_PUBLIC_PORT=""
B_8080=""; B_8443=""; B_8200=""
container_facts() { # $1 = name → C_*, E_*, B_* from docker; 1 when absent
command -v docker >/dev/null 2>&1 || return 1
_f=$(docker inspect -f '{{.State.Status}}|{{.HostConfig.NetworkMode}}|{{range $p, $b := .HostConfig.PortBindings}}{{range $b}}{{$p}}={{.HostPort}} {{end}}{{end}}{{"\n"}}{{range .Config.Env}}{{.}}{{"\n"}}{{end}}' "$1" 2>/dev/null) \
|| return 1
[ -n "$_f" ] || return 1
_head=$(printf '%s\n' "$_f" | head -1)
C_STATUS=${_head%%|*}; _r=${_head#*|}; C_NETMODE=${_r%%|*}; _bind=${_r#*|}
E_PORT=$(printf '%s\n' "$_f" | sed -n 's/^GPUK_PORT=//p' | head -1)
E_MTLS_PORT=$(printf '%s\n' "$_f" | sed -n 's/^GPUK_MTLS_PORT=//p' | head -1)
E_LISTEN_ADDR=$(printf '%s\n' "$_f" | sed -n 's/^GPUK_LISTEN_ADDR=//p' | head -1)
E_PUBLIC_PORT=$(printf '%s\n' "$_f" | sed -n 's/^GPUK_PUBLIC_PORT=//p' | head -1)
E_PUBLIC_MTLS_PORT=$(printf '%s\n' "$_f" | sed -n 's/^GPUK_PUBLIC_MTLS_PORT=//p' | head -1)
E_PROXY_PUBLIC_PORT=$(printf '%s\n' "$_f" | sed -n 's/^GPUK_PROXY_PUBLIC_PORT=//p' | head -1)
B_8080=$(printf '%s\n' "$_bind" | tr ' ' '\n' | sed -n 's/^8080\/tcp=//p' | head -1)
B_8443=$(printf '%s\n' "$_bind" | tr ' ' '\n' | sed -n 's/^8443\/tcp=//p' | head -1)
B_8200=$(printf '%s\n' "$_bind" | tr ' ' '\n' | sed -n 's/^8200\/tcp=//p' | head -1)
}
# The ports an install is REACHED on, with the precedence the controller itself
# applies (core/published-ports.ts, INS-43): host networking moves the listeners
# (GPUK_PORT, GPUK_MTLS_PORT, GPUK_LISTEN_ADDR); anything else keeps the image's
# fixed listeners and publishes them (GPUK_PUBLIC_*, then the ports table).
published_ports() { # $1 = network mode → OURS_UI OURS_MTLS OURS_INF
if [ "$1" = "host" ]; then
OURS_UI="${E_PORT:-8080}"
OURS_MTLS="${E_MTLS_PORT:-8443}"
OURS_INF="${E_PROXY_PUBLIC_PORT:-${E_LISTEN_ADDR##*:}}"
else
OURS_UI="${E_PUBLIC_PORT:-${B_8080:-8080}}"
OURS_MTLS="${E_PUBLIC_MTLS_PORT:-${B_8443:-8443}}"
OURS_INF="${E_PROXY_PUBLIC_PORT:-${B_8200:-8200}}"
fi
[ -n "$OURS_INF" ] || OURS_INF=8200
}
EXISTING=""; OUR_PORTS=""; CONTAINER_NAME="gpu-kitchen"
OURS_UI=""; OURS_MTLS=""; OURS_INF=""
if [ -f "$MANIFEST" ] && [ ! -r "$MANIFEST" ]; then
EXISTING="unreadable"
elif [ -f "$MANIFEST" ]; then
EXISTING="manifest"
_cn=$(manifest_str containerName); [ -z "$_cn" ] || CONTAINER_NAME="$_cn"
C_NETMODE=$(manifest_str networkMode)
E_PORT=$(manifest_str GPUK_PORT); E_MTLS_PORT=$(manifest_str GPUK_MTLS_PORT)
E_LISTEN_ADDR=$(manifest_str GPUK_LISTEN_ADDR)
E_PUBLIC_PORT=$(manifest_str GPUK_PUBLIC_PORT)
E_PUBLIC_MTLS_PORT=$(manifest_str GPUK_PUBLIC_MTLS_PORT)
E_PROXY_PUBLIC_PORT=$(manifest_str GPUK_PROXY_PUBLIC_PORT)
B_8080=$(manifest_binding 8080); B_8443=$(manifest_binding 8443); B_8200=$(manifest_binding 8200)
published_ports "${C_NETMODE:-host}"
OUR_PORTS="$OURS_UI $OURS_MTLS $OURS_INF"
elif container_facts "$CONTAINER_NAME"; then
EXISTING="leftovers"
published_ports "$C_NETMODE"
OUR_PORTS="$OURS_UI $OURS_MTLS $OURS_INF"
elif [ -f "$UNIT_DEST" ] || [ -x "$BIN_DEST" ] || [ -d "$DATA_ROOT/secrets" ]; then
EXISTING="leftovers"
fi
case "$EXISTING" in
manifest)
step "Existing install — $MANIFEST"
ok "image $(manifest_str image)"
_root=$(manifest_str dataRoot); _cache=$(manifest_str hostPath)
_cluster=$(manifest_str GPUK_CLUSTER); _profile=$(manifest_str GPUK_INSTALL_PROFILE)
[ "$DATA_ROOT_GIVEN" -eq 1 ] || [ -z "$_root" ] || DATA_ROOT="$_root"
[ "$CACHE_GIVEN" -eq 1 ] || [ -z "$_cache" ] || CACHE_DIR="$_cache"
[ "$CLUSTER_GIVEN" -eq 1 ] || [ -z "$_cluster" ] || CLUSTER="$_cluster"
[ "$PORT_GIVEN" -eq 1 ] || PORT="$OURS_UI"
[ "$MTLS_GIVEN" -eq 1 ] || MTLS_PORT="$OURS_MTLS"
[ "$INFERENCE_GIVEN" -eq 1 ] || INFERENCE_PORT="$OURS_INF"
case "$_profile" in
homelab|studio|enterprise|public) [ -n "$PROFILE" ] || PROFILE="$_profile" ;;
esac
ok "data root $DATA_ROOT"
ok "ports UI $OURS_UI, worker channel $OURS_MTLS, inference $OURS_INF"
[ -z "$_profile" ] || ok "profile $_profile"
ok "re-running updates it in place. Its settings are kept unless a flag says otherwise."
;;
unreadable)
step "Existing install — $MANIFEST"
warn "present, but not readable from here: run as root to keep its settings"
;;
leftovers)
step "Existing install — traces of a previous install, no manifest"
if [ -n "$C_STATUS" ]; then
ok "container $CONTAINER_NAME ($C_STATUS; UI $OURS_UI, worker channel $OURS_MTLS, inference $OURS_INF) — the install replaces it"
fi
[ ! -f "$UNIT_DEST" ] || ok "systemd unit $UNIT_DEST — rewritten"
[ ! -x "$BIN_DEST" ] || ok "daemon binary $BIN_DEST — replaced"
[ ! -d "$DATA_ROOT/secrets" ] || ok "data root $DATA_ROOT — reused, nothing in it is touched"
warn "without $MANIFEST no setting can be kept: the flags and the defaults apply"
;;
esac
[ -n "$CACHE_DIR" ] || CACHE_DIR="$DATA_ROOT/hf"
if [ "$PORT" = "$MTLS_PORT" ] || [ "$PORT" = "$INFERENCE_PORT" ] || [ "$MTLS_PORT" = "$INFERENCE_PORT" ]; then
die "the UI, worker channel and inference ports must differ (got $PORT, $MTLS_PORT, $INFERENCE_PORT)"
fi
# ── 1. Preflight ───────────────────────────────────────────────────────────── # ── 1. Preflight ─────────────────────────────────────────────────────────────
# The same checks tools/provision-feeder.sh makes, minus the compose ones: the # The same checks tools/provision-feeder.sh makes, minus the compose ones: the
# all-in-one image is driven by workerd through the plain docker CLI, so there is # all-in-one image is driven by workerd through the plain docker CLI, so there is
@@ -173,7 +316,10 @@ else
step "Preflight — docker, NVIDIA driver, container toolkit" step "Preflight — docker, NVIDIA driver, container toolkit"
[ "$DRY_RUN" -eq 1 ] || [ "$(id -u)" -eq 0 ] \ # Root, or a user-owned prefix (GPUK_ETC_DIR) — the same rule as gpuk's
# need_root, and the only way the full path is testable without handing root
# to a test suite.
[ "$DRY_RUN" -eq 1 ] || [ "$(id -u)" -eq 0 ] || [ -w "$ETC_DIR" ] \
|| die "run as root: curl -fsSL … | sudo sh" || die "run as root: curl -fsSL … | sudo sh"
command -v curl >/dev/null 2>&1 || die "curl not found. Install curl first." command -v curl >/dev/null 2>&1 || die "curl not found. Install curl first."
@@ -230,7 +376,16 @@ else
warn "only ${FREE_GB:-?}G free under $CACHE_PARENT. Model weights need 100G or more." warn "only ${FREE_GB:-?}G free under $CACHE_PARENT. Model weights need 100G or more."
fi fi
# ── Port conflicts (INS-46) ── fi # end preflight
# ── Listening ports (INS-46) ──────────────────────────────────────────────────
# Deliberately OUTSIDE the preflight branch: --skip-preflight skips docker, the
# driver, the GPU smoke test and the disk (things a runner or a VM cannot have),
# but a taken port is exactly as fatal there, and checking it costs nothing.
# Skipping it here only moved the failure to gpuk's non-interactive refusal.
if [ "$SKIP_PREFLIGHT" -eq 1 ]; then
step "Listening ports — checked even without the preflight"
fi
# A taken port must fail HERE, before anything mutates the host — today's # A taken port must fail HERE, before anything mutates the host — today's
# alternative is a 3-minute health-check timeout with zero diagnosis. Best-effort # alternative is a 3-minute health-check timeout with zero diagnosis. Best-effort
# detection (ss, then netstat); neither present is a warn, never a false red. # detection (ss, then netstat); neither present is a warn, never a false red.
@@ -258,82 +413,111 @@ port_busy() { # $1 = port → 0 iff something listens on TCP :$1
esac esac
} }
port_owner() { # $1 = port → best-effort process name (needs root for -p) # Which container a listener belongs to, if any: the process's cgroup names the
case "$PORT_TOOL" in # container id (host networking — the listener IS the container's process), and a
ss) ss -ltnpH "sport = :$1" 2>/dev/null \ # bridged publication shows up as the container's port mapping in `docker ps`
| sed -n 's/.*users:((\"\([^"]*\)\".*/\1/p' | head -1 ;; # (the host-side holder is docker-proxy, which says nothing by itself). "nginx"
netstat) netstat -ltnp 2>/dev/null \ # is a riddle; "nginx in container gpu-kitchen-dev" is the answer.
| awk -v p="$1" '{n=split($4,a,":"); if (a[n]==p) {print $NF; exit}}' \ port_container() { # $1 = port, $2 = pid ("" if unknown) → container name or ""
| sed 's|^[0-9]*/||' ;; command -v docker >/dev/null 2>&1 || return 0
esac if [ -n "$2" ] && [ -r "/proc/$2/cgroup" ]; then
_cid=$(sed -n 's#.*docker[-/]\([0-9a-f]\{64\}\).*#\1#p' "/proc/$2/cgroup" 2>/dev/null | head -1)
if [ -n "$_cid" ]; then
docker inspect -f '{{.Name}}' "$_cid" 2>/dev/null | sed 's|^/||'
return 0
fi
fi
docker ps --format '{{.Names}} {{.Ports}}' 2>/dev/null \
| awk -v p=":$1->" 'index($0, p) { print $1; exit }'
} }
manifest_ui_port() { # the port an existing install already owns, if any port_owner() { # $1 = port → best-effort "process", "process in container NAME", or ""
[ -f /etc/gpu-kitchen/manifest.json ] || return 0 _proc=""; _pid=""
# Bridge publishes GPUK_PUBLIC_PORT over the fixed container 8080; host case "$PORT_TOOL" in
# networking moves the listener itself (GPUK_PORT). Same precedence as gpuk. ss)
_p=$(sed -n 's/.*"GPUK_PUBLIC_PORT"[[:space:]]*:[[:space:]]*"\([0-9]*\)".*/\1/p' \ _line=$(ss -ltnpH "sport = :$1" 2>/dev/null | head -1)
/etc/gpu-kitchen/manifest.json | head -1) _proc=$(printf '%s' "$_line" | sed -n 's/.*users:((\"\([^"]*\)\".*/\1/p')
[ -n "$_p" ] || _p=$(sed -n 's/.*"GPUK_PORT"[[:space:]]*:[[:space:]]*"\([0-9]*\)".*/\1/p' \ _pid=$(printf '%s' "$_line" | sed -n 's/.*pid=\([0-9]*\).*/\1/p')
/etc/gpu-kitchen/manifest.json | head -1) ;;
printf '%s' "$_p" netstat)
_field=$(netstat -ltnp 2>/dev/null \
| awk -v p="$1" '{n=split($4,a,":"); if (a[n]==p) {print $NF; exit}}')
case "$_field" in
*/*) _pid=${_field%%/*}; _proc=${_field#*/} ;;
*) _proc="$_field" ;;
esac
;;
esac
case "$_pid" in *[!0-9]*|"") _pid="" ;; esac
_ctr=$(port_container "$1" "$_pid")
if [ -n "$_ctr" ]; then
printf '%s' "${_proc:-a process} in container $_ctr"
else
printf '%s' "$_proc"
fi
}
# A port is ours when the existing install (step 0) already holds it: the
# re-run replaces that container, so what it listens on is not a conflict.
port_is_ours() { case " $OUR_PORTS " in *" $1 "*) return 0 ;; esac; return 1; }
# Ports this run may not hand out twice: the three requested ones, plus every
# alternative already accepted. Without it, a busy 8442 would be offered 8443
# and collide with the worker channel one question later.
RESERVED_PORTS="$PORT $MTLS_PORT $INFERENCE_PORT"
port_available() { # $1 → free on the host AND not reserved by this run
case " $RESERVED_PORTS " in *" $1 "*) return 1 ;; esac
port_is_ours "$1" && return 1
! port_busy "$1"
}
# resolve_port <label> <port> <flag> → RESOLVED. Same rules for all three
# listeners: ours = fine; busy on a terminal = propose the next free port
# (Enter accepts, a number picks, q aborts) — never auto-pick silently, the URL
# printed at the end and the idempotent re-run both need the operator to KNOW
# the port; busy without a terminal = fail now, naming the process and the flag.
resolve_port() {
_label="$1"; _want="$2"; _flag="$3"
if port_is_ours "$_want"; then
ok "$_label port $_want — already ours (re-running is how you update)"
elif port_busy "$_want"; then
OWNER=$(port_owner "$_want")
OWNER="${OWNER:-an unknown process}"
if can_prompt; then
ALT=$((_want + 1))
while ! port_available "$ALT"; do ALT=$((ALT + 1)); done
ask " ${YELLOW}!${RESET} $_label port $_want is busy ($OWNER). Use $ALT instead? [$ALT], another port, or 'q' to abort: "
case "$REPLY" in
q|Q) die "$_label port $_want is in use by $OWNER. Run the install again with $_flag <p>." ;;
"") _want="$ALT" ;;
*)
case "$REPLY" in
*[!0-9]*) die "not a port number: $REPLY" ;;
esac
port_available "$REPLY" \
|| die "port $REPLY is busy, or already taken by another GPU Kitchen listener. Run the install again with $_flag <p>."
_want="$REPLY"
;;
esac
RESERVED_PORTS="$RESERVED_PORTS $_want"
ok "$_label port $_want is free"
else
die "$_label port $_want is already in use by $OWNER. Pass $_flag <p> to choose another port."
fi
else
ok "$_label port $_want is free"
fi
RESOLVED="$_want"
} }
if [ -z "$PORT_TOOL" ]; then if [ -z "$PORT_TOOL" ]; then
warn "cannot check for port conflicts (neither ss nor netstat found)" warn "cannot check for port conflicts (neither ss nor netstat found)"
else else
HAVE_MANIFEST=0 resolve_port "UI" "$PORT" "--port"; PORT="$RESOLVED"
[ ! -f /etc/gpu-kitchen/manifest.json ] || HAVE_MANIFEST=1 resolve_port "worker channel" "$MTLS_PORT" "--mtls-port"; MTLS_PORT="$RESOLVED"
if [ "$HAVE_MANIFEST" -eq 1 ] && [ "$(manifest_ui_port)" = "$PORT" ]; then resolve_port "inference" "$INFERENCE_PORT" "--inference-port"; INFERENCE_PORT="$RESOLVED"
ok "UI port $PORT — already ours (re-running is how you update)"
elif port_busy "$PORT"; then
OWNER=$(port_owner "$PORT")
OWNER="${OWNER:-an unknown process}"
if can_prompt; then
ALT=$((PORT + 1))
while port_busy "$ALT"; do ALT=$((ALT + 1)); done
# Propose, never auto-pick: the URL printed at the end and the idempotent
# re-run both need the operator to KNOW which port they chose.
ask " ${YELLOW}!${RESET} port $PORT is busy ($OWNER). Use $ALT instead? [$ALT], another port, or 'q' to abort: "
case "$REPLY" in
q|Q) die "port $PORT is in use by $OWNER. Run the install again with --port <p>." ;;
"") PORT="$ALT" ;;
*)
case "$REPLY" in
*[!0-9]*) die "not a port number: $REPLY" ;;
esac
if port_busy "$REPLY"; then
die "port $REPLY is busy too. Run the install again with --port <p>."
fi
PORT="$REPLY"
;;
esac
ok "UI port $PORT is free"
else
die "port $PORT is already in use by $OWNER. Pass --port <p> to choose another port."
fi
else
ok "UI port $PORT is free"
fi
# The mTLS and inference listeners have no install-time flag — assumed
# limitation (INS-46): the published mTLS port moves later via
# Settings -> Network. With a manifest present they are our own listeners.
if [ "$HAVE_MANIFEST" -eq 0 ]; then
if port_busy "$MTLS_PORT"; then
OWNER=$(port_owner "$MTLS_PORT")
die "port $MTLS_PORT (worker channel) is in use by ${OWNER:-an unknown process}. Free it first."
fi
if port_busy "$INFERENCE_PORT"; then
OWNER=$(port_owner "$INFERENCE_PORT")
die "port $INFERENCE_PORT (inference endpoint) is in use by ${OWNER:-an unknown process}. Free it first.
($INFERENCE_PORT is also HashiCorp Vault's default port.)"
fi
ok "worker channel port $MTLS_PORT and inference port $INFERENCE_PORT are free"
fi
fi fi
fi # end preflight
# ── 2. Resolve the release ─────────────────────────────────────────────────── # ── 2. Resolve the release ───────────────────────────────────────────────────
step "Release — resolving the version to install" step "Release — resolving the version to install"
@@ -420,6 +604,13 @@ if [ "$DRY_RUN" -eq 1 ]; then
echo " data root : $DATA_ROOT" echo " data root : $DATA_ROOT"
echo " model cache : $CACHE_DIR" echo " model cache : $CACHE_DIR"
echo " UI port : $PORT" echo " UI port : $PORT"
echo " worker channel: $MTLS_PORT"
echo " inference : $INFERENCE_PORT"
case "$EXISTING" in
manifest) echo " existing : yes — updated in place" ;;
leftovers) echo " existing : traces of a previous install — taken over" ;;
*) echo " existing : no" ;;
esac
if [ -n "$PROFILE" ]; then if [ -n "$PROFILE" ]; then
echo " profile : $PROFILE" echo " profile : $PROFILE"
else else
@@ -531,7 +722,9 @@ set -- install \
--cluster "$CLUSTER" \ --cluster "$CLUSTER" \
--data-root "$DATA_ROOT" \ --data-root "$DATA_ROOT" \
--cache-dir "$CACHE_DIR" \ --cache-dir "$CACHE_DIR" \
--http-port "$PORT" --http-port "$PORT" \
--mtls-port "$MTLS_PORT" \
--inference-port "$INFERENCE_PORT"
[ -z "$PROFILE" ] || set -- "$@" --profile "$PROFILE" [ -z "$PROFILE" ] || set -- "$@" --profile "$PROFILE"
[ -z "$DOMAIN" ] || set -- "$@" --domain "$DOMAIN" [ -z "$DOMAIN" ] || set -- "$@" --domain "$DOMAIN"
@@ -646,5 +839,6 @@ if [ -n "$EXISTING_CACHE" ]; then
echo echo
fi fi
echo " Update : re-run this command, or press Update in the UI, or: gpuk update" echo " Update : re-run this command, or press Update in the UI, or: gpuk update"
echo " Status : gpuk status Logs: gpuk logs Remove: gpuk uninstall" echo " Status : gpuk status Logs: gpuk logs"
echo " Remove : gpuk uninstall (service only) or gpuk uninstall --purge (all but the data root)"
echo echo
+5 -5
View File
@@ -1,11 +1,11 @@
{ {
"version": "v0.1.3", "version": "v0.1.5",
"semver": "0.1.3", "semver": "0.1.5",
"controllerImage": "repo.byterain.io/gpukitchen/gpukitchen-controller", "controllerImage": "repo.byterain.io/gpukitchen/gpukitchen-controller",
"controllerImageEnterprise": "repo.byterain.io/gpukitchen-private/gpukitchen-controller-ee", "controllerImageEnterprise": "repo.byterain.io/gpukitchen-private/gpukitchen-controller-ee",
"controllerImageDigest": "sha256:609bfdf5e9abcebdae56d868217b7d6400bd6fa8c44737b2275bfc3d4db6a100", "controllerImageDigest": "sha256:5a126247a2f24a29338ba2a84414a3ef8e270a1696dd7a32a3894889836594b1",
"controllerImageDigestEnterprise": "sha256:9fe855a97932cc134948ace4d03de592652e5872749c27273e9e7f6e930cd7ab", "controllerImageDigestEnterprise": "sha256:a8f55534aa09a1f077dc5d72116db2b1f2f153b0e2f3c6299d777a1d80c3dfab",
"workerBase": "https://repo.byterain.io/api/packages/gpukitchen/generic/gpu-kitchen-worker/v0.1.3", "workerBase": "https://repo.byterain.io/api/packages/gpukitchen/generic/gpu-kitchen-worker/v0.1.5",
"gpukScript": "https://repo.byterain.io/gpukitchen/channel/raw/branch/main/gpuk", "gpukScript": "https://repo.byterain.io/gpukitchen/channel/raw/branch/main/gpuk",
"releaseNotes": "https://repo.byterain.io/gpukitchen/channel" "releaseNotes": "https://repo.byterain.io/gpukitchen/channel"
} }
+3 -3
View File
@@ -1,4 +1,4 @@
untrusted comment: signature from minisign secret key untrusted comment: signature from minisign secret key
RUQ7BKXJqGX2je3pLbIHpqUv94Xn863QAziuHoQdpUFKIG0QvVOjHN2rPz7ooHA1Nh6hjttCUfpvGsuH4IOLfsVgTB9KyA0TDQo= RUQ7BKXJqGX2jabBqcSYwUGmejKh6OS+qKIyVqxpzwJ4PwIBVDAAuoxgF6CkfyxiNgQvpS63RUIRE485PVdRvHTW8UQd10fuIAQ=
trusted comment: gpu-kitchen channel v0.1.3 trusted comment: gpu-kitchen channel v0.1.5
dcB01BhY0qvL9wGDhVtTG3q0IimAB1Kfh/6yd/lZhov3cQ38cM2sLelVOoE6DpkXyYZ2OXzTA0tJJEDkt68nAQ== f+6uK6GJBgurIcd7L2l53/SPV/VqRv9DfJfRaFbc5tJTBVRL5I2zgzEF66YytLXHUHScAM7MZsJi9VUCeoGxAw==