Promote v0.1.13 to production
This commit is contained in:
@@ -41,6 +41,10 @@ set -eu
|
|||||||
# host. Unset (the real install) they are exactly the systemd defaults workerd uses
|
# host. Unset (the real install) they are exactly the systemd defaults workerd uses
|
||||||
# (apps/worker/src/module.rs, apps/worker/src/identity.rs).
|
# (apps/worker/src/module.rs, apps/worker/src/identity.rs).
|
||||||
BIN_DEST="${GPUK_BIN_DEST:-/usr/local/bin/gpu-kitchen-worker}"
|
BIN_DEST="${GPUK_BIN_DEST:-/usr/local/bin/gpu-kitchen-worker}"
|
||||||
|
# This CLI, installed next to the daemon by `gpuk install`: it carries the
|
||||||
|
# release key pinned at install time, and install.sh hands a plain re-run to its
|
||||||
|
# `update` (INS-03, OPS-20).
|
||||||
|
CLI_DEST="${GPUK_CLI_DEST:-$(dirname "$BIN_DEST")/gpuk}"
|
||||||
ETC_DIR="${GPUK_ETC_DIR:-/etc/gpu-kitchen}"
|
ETC_DIR="${GPUK_ETC_DIR:-/etc/gpu-kitchen}"
|
||||||
MANIFEST="$ETC_DIR/manifest.json"
|
MANIFEST="$ETC_DIR/manifest.json"
|
||||||
IDENTITY_DIR="${GPUK_IDENTITY_DIR:-$ETC_DIR/identity}"
|
IDENTITY_DIR="${GPUK_IDENTITY_DIR:-$ETC_DIR/identity}"
|
||||||
@@ -95,6 +99,19 @@ install_binary() { # [local-path]
|
|||||||
# Not -s: the binary is tens of MB and a silent download reads as a hang.
|
# Not -s: the binary is tens of MB and a silent download reads as a hang.
|
||||||
curl -fL --progress-bar "$_url" -o "$BIN_DEST.new" \
|
curl -fL --progress-bar "$_url" -o "$BIN_DEST.new" \
|
||||||
|| { rm -f "$BIN_DEST.new"; die "cannot download $_url"; }
|
|| { rm -f "$BIN_DEST.new"; die "cannot download $_url"; }
|
||||||
|
# The root daemon is swapped in only once its minisign signature verifies
|
||||||
|
# against the release key pinned in this gpuk (OPS-20, INS-05).
|
||||||
|
if [ "${GPUK_CHANNEL_INSECURE:-}" = "1" ]; then
|
||||||
|
echo "WARNING: GPUK_CHANNEL_INSECURE=1 — $_url NOT verified" >&2
|
||||||
|
else
|
||||||
|
require_release_key
|
||||||
|
curl -fsSL "$_url.minisig" -o "$BIN_DEST.new.minisig" \
|
||||||
|
|| { rm -f "$BIN_DEST.new" "$BIN_DEST.new.minisig"; die "no signature at $_url.minisig — refusing an unsigned daemon binary"; }
|
||||||
|
"$MINISIGN" -Vq -m "$BIN_DEST.new" -x "$BIN_DEST.new.minisig" -P "$CHANNEL_PUBKEY" >/dev/null 2>&1 \
|
||||||
|
|| { rm -f "$BIN_DEST.new" "$BIN_DEST.new.minisig"; die "$_url: signature verification FAILED — refusing it (OPS-20)"; }
|
||||||
|
rm -f "$BIN_DEST.new.minisig"
|
||||||
|
echo "==> signature verified"
|
||||||
|
fi
|
||||||
chmod 0755 "$BIN_DEST.new"
|
chmod 0755 "$BIN_DEST.new"
|
||||||
mv "$BIN_DEST.new" "$BIN_DEST"
|
mv "$BIN_DEST.new" "$BIN_DEST"
|
||||||
elif [ -x "$BIN_DEST" ]; then
|
elif [ -x "$BIN_DEST" ]; then
|
||||||
@@ -104,6 +121,18 @@ install_binary() { # [local-path]
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Leave this very gpuk on the host, next to the daemon: `gpuk status`, `gpuk
|
||||||
|
# update` and install.sh's plain re-run all use it. It is the copy that carries
|
||||||
|
# the release key this install pinned (substituted at publish time), so later
|
||||||
|
# updates are verified against the key the first install trusted.
|
||||||
|
install_cli() {
|
||||||
|
# Already this copy (gpuk re-run from its installed path, or identical bytes).
|
||||||
|
if [ -e "$CLI_DEST" ] && cmp -s "$0" "$CLI_DEST"; then return 0; fi
|
||||||
|
install -m 0755 "$0" "$CLI_DEST.new" && mv "$CLI_DEST.new" "$CLI_DEST" \
|
||||||
|
|| die "cannot install the gpuk CLI at $CLI_DEST"
|
||||||
|
echo "==> installed $CLI_DEST"
|
||||||
|
}
|
||||||
|
|
||||||
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"),
|
# The host ports a leftover app container is reached on ("ui mtls inference"),
|
||||||
@@ -202,7 +231,9 @@ prepare_claim_code() {
|
|||||||
# The file is the operator's recoverable proof of machine possession. Create
|
# The file is the operator's recoverable proof of machine possession. Create
|
||||||
# it once, preserve it across reinstalls, and let the backend unlink it after
|
# it once, preserve it across reinstalls, and let the backend unlink it after
|
||||||
# the atomic first-account claim. A missing file beside an existing manifest
|
# the atomic first-account claim. A missing file beside an existing manifest
|
||||||
# therefore means "consumed", never "rotate the credential".
|
# therefore means "consumed", never "rotate the credential". homelab and studio
|
||||||
|
# have no claim window at all (PRF-12, SEC-53): no code is made for them.
|
||||||
|
case "$PROFILE" in homelab|studio) return 0 ;; esac
|
||||||
if [ -f "$CLAIM_CODE_FILE" ]; then
|
if [ -f "$CLAIM_CODE_FILE" ]; then
|
||||||
chmod 0600 "$CLAIM_CODE_FILE"
|
chmod 0600 "$CLAIM_CODE_FILE"
|
||||||
elif [ ! -f "$MANIFEST" ]; then
|
elif [ ! -f "$MANIFEST" ]; then
|
||||||
@@ -256,7 +287,7 @@ UNIT
|
|||||||
}
|
}
|
||||||
|
|
||||||
# TLS reverse-proxy example, FILLED with the operator's domain (INS-47) — written
|
# TLS reverse-proxy example, FILLED with the operator's domain (INS-47) — written
|
||||||
# only under `--profile public --domain <d>`. Kept aligned with
|
# under `--domain <d>`, whatever the profile. Kept aligned with
|
||||||
# deployments/controller/Caddyfile.example (the compose variant); this copy
|
# deployments/controller/Caddyfile.example (the compose variant); this copy
|
||||||
# targets the all-in-one image, where nginx on the UI port is the single front
|
# targets the all-in-one image, where nginx on the UI port is the single front
|
||||||
# door (INS-09) so one upstream carries pages, /api and the /ws upgrade alike.
|
# door (INS-09) so one upstream carries pages, /api and the /ws upgrade alike.
|
||||||
@@ -266,15 +297,18 @@ UNIT
|
|||||||
write_caddyfile() {
|
write_caddyfile() {
|
||||||
mkdir -p "$DATA_ROOT/caddy"
|
mkdir -p "$DATA_ROOT/caddy"
|
||||||
cat > "$DATA_ROOT/caddy/Caddyfile" <<CADDY
|
cat > "$DATA_ROOT/caddy/Caddyfile" <<CADDY
|
||||||
# TLS in front of GPU Kitchen — generated by the installer for --profile public
|
# TLS in front of GPU Kitchen — generated by the installer for --domain $DOMAIN
|
||||||
# (specs/plateforme/installation.md INS-47). Caddy provisions and renews the
|
# (specs/plateforme/installation.md INS-47). Caddy provisions and renews the
|
||||||
# certificate itself once DNS for $DOMAIN points at this machine.
|
# certificate itself once DNS for $DOMAIN points at this machine.
|
||||||
#
|
#
|
||||||
# sudo cp $DATA_ROOT/caddy/Caddyfile /etc/caddy/Caddyfile
|
# sudo cp $DATA_ROOT/caddy/Caddyfile /etc/caddy/Caddyfile
|
||||||
# sudo systemctl reload caddy
|
# sudo systemctl reload caddy
|
||||||
#
|
#
|
||||||
# The app already runs with GPUK_HSTS=true and GPUK_SESSION_COOKIE_SECURE=true
|
# The app already knows $DOMAIN as one of its names (GPUK_ALLOWED_HOSTS) and reads
|
||||||
# (set by the public profile). What does NOT go through this proxy:
|
# the real client address from this proxy's X-Forwarded-For (GPUK_TRUST_PROXY) —
|
||||||
|
# without it every visitor would share Caddy's address, and one login throttle.
|
||||||
|
# Under --profile public it also runs with GPUK_HSTS=true and
|
||||||
|
# GPUK_SESSION_COOKIE_SECURE=true. What does NOT go through this proxy:
|
||||||
# - the worker mTLS channel (:$MTLS_PORT): 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).
|
||||||
@@ -406,8 +440,6 @@ cmd_install() {
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
if [ -n "$DOMAIN" ]; then
|
if [ -n "$DOMAIN" ]; then
|
||||||
[ "$PROFILE" = "public" ] \
|
|
||||||
|| die "--domain applies only to --profile public (it fills the TLS reverse-proxy example)"
|
|
||||||
case "$DOMAIN" in
|
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
|
||||||
@@ -438,7 +470,10 @@ cmd_install() {
|
|||||||
CLAIM_CODE_AVAILABLE=0
|
CLAIM_CODE_AVAILABLE=0
|
||||||
|
|
||||||
if [ "$DRY_RUN" -eq 1 ]; then
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
[ "$MODE" != "controller" ] || CLAIM_CODE_AVAILABLE=1
|
case "$MODE:$PROFILE" in
|
||||||
|
controller:homelab|controller:studio) ;;
|
||||||
|
controller:*) CLAIM_CODE_AVAILABLE=1 ;;
|
||||||
|
esac
|
||||||
echo "==> dry run: no file, service or container was changed"
|
echo "==> dry run: no file, service or container was changed"
|
||||||
[ ! -f "$MANIFEST" ] \
|
[ ! -f "$MANIFEST" ] \
|
||||||
|| echo "==> dry run: $MANIFEST exists — this render would be MERGED into it, controller-owned settings kept"
|
|| echo "==> dry run: $MANIFEST exists — this render would be MERGED into it, controller-owned settings kept"
|
||||||
@@ -508,6 +543,7 @@ cmd_install() {
|
|||||||
|
|
||||||
need_root
|
need_root
|
||||||
install_binary "$BIN_SRC"
|
install_binary "$BIN_SRC"
|
||||||
|
install_cli
|
||||||
|
|
||||||
mkdir -p "$ETC_DIR" "$DATA_ROOT" "$CACHE_DIR"
|
mkdir -p "$ETC_DIR" "$DATA_ROOT" "$CACHE_DIR"
|
||||||
seed_secrets "$DATA_ROOT"
|
seed_secrets "$DATA_ROOT"
|
||||||
@@ -711,6 +747,14 @@ render_controller_manifest() {
|
|||||||
EXTRA_ENV="$EXTRA_ENV,\"GPUK_HSTS\":\"true\""
|
EXTRA_ENV="$EXTRA_ENV,\"GPUK_HSTS\":\"true\""
|
||||||
EXTRA_ENV="$EXTRA_ENV,\"GPUK_SESSION_COOKIE_SECURE\":\"true\""
|
EXTRA_ENV="$EXTRA_ENV,\"GPUK_SESSION_COOKIE_SECURE\":\"true\""
|
||||||
fi
|
fi
|
||||||
|
# SEC-16: the DNS-rebinding Host guard stays on during the anonymous first run even
|
||||||
|
# under `enforced`, so the operator's own name must be a known host of the install
|
||||||
|
# before the wizard can load through it — whatever the profile. SEC-15: that name
|
||||||
|
# is served through a reverse proxy, whose X-Forwarded-For carries the real client.
|
||||||
|
if [ -n "$DOMAIN" ]; then
|
||||||
|
EXTRA_ENV="$EXTRA_ENV,\"GPUK_ALLOWED_HOSTS\":\"$(json_str "$DOMAIN")\""
|
||||||
|
EXTRA_ENV="$EXTRA_ENV,\"GPUK_TRUST_PROXY\":\"true\""
|
||||||
|
fi
|
||||||
BOOTSTRAP_SECRET_JSON=""
|
BOOTSTRAP_SECRET_JSON=""
|
||||||
if [ -s "$BOOTSTRAP_PASSWORD_FILE" ]; then
|
if [ -s "$BOOTSTRAP_PASSWORD_FILE" ]; then
|
||||||
EXTRA_ENV="$EXTRA_ENV,\"GPUK_BOOTSTRAP_MUST_CHANGE\":\"1\""
|
EXTRA_ENV="$EXTRA_ENV,\"GPUK_BOOTSTRAP_MUST_CHANGE\":\"1\""
|
||||||
@@ -837,6 +881,58 @@ CHANNEL_URL="${GPUK_CHANNEL_URL:-https://repo.byterain.io/gpukitchen/channel/raw
|
|||||||
# no longer pick what an existing install runs.
|
# no longer pick what an existing install runs.
|
||||||
CHANNEL_PUBKEY="${GPUK_UPDATE_PUBKEY:-RWQ7BKXJqGX2jdKXu1GxeSPVAN3JDRTefpImM/mFRjtFwq4E7mhnQtA7}"
|
CHANNEL_PUBKEY="${GPUK_UPDATE_PUBKEY:-RWQ7BKXJqGX2jdKXu1GxeSPVAN3JDRTefpImM/mFRjtFwq4E7mhnQtA7}"
|
||||||
|
|
||||||
|
# The pinned key and the minisign CLI every verification needs (channel
|
||||||
|
# document, downloaded daemon binary); fail-closed.
|
||||||
|
require_release_key() {
|
||||||
|
case "$CHANNEL_PUBKEY" in
|
||||||
|
# The unstamped placeholder, matched by its prefix only: the release stamps
|
||||||
|
# the key by substituting the whole placeholder wherever it appears, and a
|
||||||
|
# guard spelling it in full would become one refusing the very key it pinned.
|
||||||
|
""|__GPUK_UPDATE_*)
|
||||||
|
die "this gpuk carries no pinned release public key — set GPUK_UPDATE_PUBKEY (the minisign public-key line), or GPUK_CHANNEL_INSECURE=1 to skip verification" ;;
|
||||||
|
esac
|
||||||
|
ensure_minisign 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# The minisign CLI is what verifies the release; a host without it gets it from
|
||||||
|
# its own package manager — detected, non-interactive, quiet — before anything
|
||||||
|
# else changes. No known manager, or an install that fails: stop, naming the
|
||||||
|
# manual command. GPUK_MINISIGN names another verifier binary (test seam).
|
||||||
|
MINISIGN="${GPUK_MINISIGN:-minisign}"
|
||||||
|
minisign_manual_command() {
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then echo "apt-get install minisign"
|
||||||
|
elif command -v dnf >/dev/null 2>&1; then echo "dnf install minisign (EPEL on RHEL)"
|
||||||
|
elif command -v yum >/dev/null 2>&1; then echo "yum install minisign (EPEL)"
|
||||||
|
elif command -v zypper >/dev/null 2>&1; then echo "zypper install minisign"
|
||||||
|
elif command -v apk >/dev/null 2>&1; then echo "apk add minisign"
|
||||||
|
elif command -v pacman >/dev/null 2>&1; then echo "pacman -S minisign"
|
||||||
|
else echo "install minisign from https://jedisct1.github.io/minisign/"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
ensure_minisign() { # $1 = 1 when nothing may be installed (dry run)
|
||||||
|
command -v "$MINISIGN" >/dev/null 2>&1 && return 0
|
||||||
|
[ "${1:-0}" -eq 0 ] \
|
||||||
|
|| die "minisign is required to verify the release and a dry run installs nothing: $(minisign_manual_command), or set GPUK_CHANNEL_INSECURE=1"
|
||||||
|
_mlog=$(mktemp)
|
||||||
|
for _pm in apt-get dnf yum zypper apk pacman; do
|
||||||
|
command -v "$_pm" >/dev/null 2>&1 || continue
|
||||||
|
echo "==> minisign is missing — installing it with $_pm"
|
||||||
|
case "$_pm" in
|
||||||
|
apt-get) { DEBIAN_FRONTEND=noninteractive apt-get update -qq \
|
||||||
|
&& DEBIAN_FRONTEND=noninteractive apt-get install -y -qq minisign; } ;;
|
||||||
|
dnf) dnf install -y -q minisign ;;
|
||||||
|
yum) yum install -y -q minisign ;;
|
||||||
|
zypper) zypper --non-interactive --quiet install minisign ;;
|
||||||
|
apk) apk add --quiet minisign ;;
|
||||||
|
pacman) pacman -S --noconfirm --needed --quiet minisign ;;
|
||||||
|
esac >"$_mlog" 2>&1 || true
|
||||||
|
if command -v "$MINISIGN" >/dev/null 2>&1; then rm -f "$_mlog"; return 0; fi
|
||||||
|
done
|
||||||
|
tail -5 "$_mlog" >&2 2>/dev/null || true
|
||||||
|
rm -f "$_mlog"
|
||||||
|
die "minisign is required to verify the release and could not be installed automatically. Nothing was changed: run '$(minisign_manual_command)' as root, then re-run — or set GPUK_CHANNEL_INSECURE=1"
|
||||||
|
}
|
||||||
|
|
||||||
# Fetch latest.json AND its minisign signature, verify, and leave the verified
|
# Fetch latest.json AND its minisign signature, verify, and leave the verified
|
||||||
# document at $CHANNEL_DOC. Fail-closed: no signature, bad signature, no
|
# document at $CHANNEL_DOC. Fail-closed: no signature, bad signature, no
|
||||||
# minisign CLI or no pinned key are all fatal — GPUK_CHANNEL_INSECURE=1 is the
|
# minisign CLI or no pinned key are all fatal — GPUK_CHANNEL_INSECURE=1 is the
|
||||||
@@ -850,18 +946,13 @@ channel_fetch() {
|
|||||||
echo "WARNING: GPUK_CHANNEL_INSECURE=1 — release channel signature NOT verified" >&2
|
echo "WARNING: GPUK_CHANNEL_INSECURE=1 — release channel signature NOT verified" >&2
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
case "$CHANNEL_PUBKEY" in
|
require_release_key
|
||||||
""|RWQ7BKXJqGX2jdKXu1GxeSPVAN3JDRTefpImM/mFRjtFwq4E7mhnQtA7*)
|
|
||||||
die "this gpuk carries no pinned release public key — set GPUK_UPDATE_PUBKEY (the minisign public-key line), or GPUK_CHANNEL_INSECURE=1 to skip verification" ;;
|
|
||||||
esac
|
|
||||||
command -v minisign >/dev/null 2>&1 \
|
|
||||||
|| die "minisign is required to verify the release channel (apt install minisign), or set GPUK_CHANNEL_INSECURE=1"
|
|
||||||
_sig=$(mktemp)
|
_sig=$(mktemp)
|
||||||
if ! curl -fsSL --max-time 20 "${CHANNEL_URL}.minisig" -o "$_sig" 2>/dev/null; then
|
if ! curl -fsSL --max-time 20 "${CHANNEL_URL}.minisig" -o "$_sig" 2>/dev/null; then
|
||||||
rm -f "$_sig"
|
rm -f "$_sig"
|
||||||
die "no signature at ${CHANNEL_URL}.minisig — refusing an unsigned channel document (OPS-20)"
|
die "no signature at ${CHANNEL_URL}.minisig — refusing an unsigned channel document (OPS-20)"
|
||||||
fi
|
fi
|
||||||
if ! minisign -Vq -m "$CHANNEL_DOC" -x "$_sig" -P "$CHANNEL_PUBKEY" >/dev/null 2>&1; then
|
if ! "$MINISIGN" -Vq -m "$CHANNEL_DOC" -x "$_sig" -P "$CHANNEL_PUBKEY" >/dev/null 2>&1; then
|
||||||
rm -f "$_sig"
|
rm -f "$_sig"
|
||||||
die "latest.json signature verification FAILED — refusing the channel document (OPS-20)"
|
die "latest.json signature verification FAILED — refusing the channel document (OPS-20)"
|
||||||
fi
|
fi
|
||||||
@@ -1054,25 +1145,38 @@ cmd_update() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
_digest=""
|
_digest=""
|
||||||
if [ -z "$_want" ]; then
|
# channel_fetch runs in THIS shell (not a $(…) subshell) so a signature
|
||||||
# channel_fetch runs in THIS shell (not a $(…) subshell) so a signature
|
# failure is fatal here — fail-closed — and $CHANNEL_DOC survives. The
|
||||||
# failure is fatal here — fail-closed — and $CHANNEL_DOC survives. The
|
# verified signature closes the document half of OPS-20; the digest read
|
||||||
# verified signature closes the document half of OPS-20; the digest read
|
# from it pins CONTENT, closing the mutable-tag half. A named --version goes
|
||||||
# from it pins CONTENT, closing the mutable-tag half.
|
# through the same document: it vouches for ONE release, the one it names.
|
||||||
if channel_fetch; then
|
_latest=""
|
||||||
_want=$(channel_field "$CHANNEL_DOC" version)
|
if channel_fetch; then
|
||||||
fi
|
_latest=$(channel_field "$CHANNEL_DOC" version)
|
||||||
if [ -z "$_want" ]; then
|
fi
|
||||||
echo "==> cannot reach $CHANNEL_URL — re-applying the pinned $_current"
|
if [ -z "$_latest" ]; then
|
||||||
GPUK_MANIFEST_PATH="$MANIFEST" "$BIN_DEST" apply
|
[ -z "$_want" ] \
|
||||||
return 0
|
|| die "cannot read the release channel at $CHANNEL_URL — refusing to pin $_want unverified"
|
||||||
fi
|
echo "==> cannot reach $CHANNEL_URL — re-applying the pinned $_current"
|
||||||
|
GPUK_MANIFEST_PATH="$MANIFEST" "$BIN_DEST" apply
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [ -n "$_want" ] && [ "$_want" != "$_latest" ]; then
|
||||||
|
[ "${GPUK_CHANNEL_INSECURE:-}" = "1" ] \
|
||||||
|
|| die "the signed channel vouches for $_latest only, not $_want — refusing an unpinned tag (OPS-20)"
|
||||||
|
else
|
||||||
|
_want="$_latest"
|
||||||
# Pick the digest matching the installed edition by image basename — the
|
# Pick the digest matching the installed edition by image basename — the
|
||||||
# repo itself may be a mirror, the basename is the edition marker.
|
# repo itself may be a mirror, the basename is the edition marker.
|
||||||
case "${_repo##*/}" in
|
case "${_repo##*/}" in
|
||||||
*-ee) _digest=$(channel_field "$CHANNEL_DOC" controllerImageDigestEnterprise) ;;
|
*-ee) _digest=$(channel_field "$CHANNEL_DOC" controllerImageDigestEnterprise) ;;
|
||||||
*) _digest=$(channel_field "$CHANNEL_DOC" controllerImageDigest) ;;
|
*) _digest=$(channel_field "$CHANNEL_DOC" controllerImageDigest) ;;
|
||||||
esac
|
esac
|
||||||
|
case "$_digest" in
|
||||||
|
sha256:*) ;;
|
||||||
|
*) [ "${GPUK_CHANNEL_INSECURE:-}" = "1" ] \
|
||||||
|
|| die "the signed release channel names no image digest for $_want — refusing to pin a mutable tag (OPS-20)" ;;
|
||||||
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
_new="$_repo:$_want"
|
_new="$_repo:$_want"
|
||||||
@@ -1089,7 +1193,10 @@ cmd_update() {
|
|||||||
else
|
else
|
||||||
echo "==> already on $_current — re-pulling and recreating"
|
echo "==> already on $_current — re-pulling and recreating"
|
||||||
fi
|
fi
|
||||||
GPUK_MANIFEST_PATH="$MANIFEST" "$BIN_DEST" apply
|
GPUK_MANIFEST_PATH="$MANIFEST" "$BIN_DEST" apply \
|
||||||
|
|| die "the update did not apply — the [worker] lines above say why. A manifest refused for a field
|
||||||
|
an older release wrote is repaired by re-running the same install command with --reset-manifest:
|
||||||
|
the file is archived beside itself and rebuilt from this install's settings."
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── uninstall ──────────────────────────────────────────────────────────────────
|
# ── uninstall ──────────────────────────────────────────────────────────────────
|
||||||
@@ -1131,6 +1238,7 @@ cmd_uninstall() {
|
|||||||
done
|
done
|
||||||
[ ! -e "$MACHINE_ID_FILE" ] || rm -f "$MACHINE_ID_FILE"
|
[ ! -e "$MACHINE_ID_FILE" ] || rm -f "$MACHINE_ID_FILE"
|
||||||
[ ! -e "$BIN_DEST" ] || { rm -f "$BIN_DEST"; echo "==> removed $BIN_DEST"; }
|
[ ! -e "$BIN_DEST" ] || { rm -f "$BIN_DEST"; echo "==> removed $BIN_DEST"; }
|
||||||
|
[ ! -e "$CLI_DEST" ] || { rm -f "$CLI_DEST"; echo "==> removed $CLI_DEST"; }
|
||||||
echo
|
echo
|
||||||
echo "Kept: the data root${_root:+ $_root} — database, models and secrets (ENCRYPTION_KEY)."
|
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>}"
|
echo "A new install over it reuses them. To delete it too, knowingly: rm -rf ${_root:-<data-root>}"
|
||||||
@@ -1141,12 +1249,14 @@ 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] [--data-root P]
|
[--domain D] [--cluster N] [--cache-dir P] [--data-root P]
|
||||||
[--http-port P] [--mtls-port P] [--inference-port P]
|
[--http-port P] [--mtls-port P] [--inference-port P]
|
||||||
[--network bridge|host|<net>] [--binary <path>] [--reset-manifest]
|
[--network bridge|host|<net>] [--binary <path>] [--reset-manifest]
|
||||||
--network: bridge by default — the container publishes its three
|
--network: bridge by default — the container publishes its three
|
||||||
ports and nothing else it listens on touches the host; host makes
|
ports and nothing else it listens on touches the host; host makes
|
||||||
every listener a host-wide claim (a re-run keeps the mode installed)
|
every listener a host-wide claim (a re-run keeps the mode installed)
|
||||||
|
--domain: the name the UI is reached by through a reverse proxy, any
|
||||||
|
profile — allowed as a host, trusted proxy on, filled Caddyfile written
|
||||||
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]
|
[--health-port P] [--data-port P] [--transfer-port P] [--handover-port P]
|
||||||
@@ -1163,7 +1273,7 @@ gpuk — GPU Kitchen host daemon (gpu-kitchen-worker)
|
|||||||
gpuk logs Follow the app container logs (controller) or the daemon journal;
|
gpuk logs Follow the app container logs (controller) or the daemon journal;
|
||||||
after a failed start, the kept <container>-failed log
|
after a failed start, the kept <container>-failed log
|
||||||
gpuk uninstall Remove the systemd service, leave everything else in place
|
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
|
gpuk uninstall --purge Also remove the app container, /etc/gpu-kitchen, the binary and gpuk
|
||||||
(never the data root: database, models, secrets)
|
(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
|
||||||
|
|||||||
+303
-59
@@ -25,8 +25,11 @@
|
|||||||
# 5. hand over workerd pulls the pinned all-in-one controller image and starts it
|
# 5. hand over workerd pulls the pinned all-in-one controller image and starts it
|
||||||
# 6. print the UI URL on the real host, plus the profile-specific next step
|
# 6. print the UI URL on the real host, plus the profile-specific next step
|
||||||
#
|
#
|
||||||
# Re-running is how you UPDATE: same command, newer tag, `docker pull` + recreate,
|
# Re-running is how you UPDATE: on a host that already runs GPU Kitchen, the same
|
||||||
# data untouched (it lives in the data root, not the container).
|
# command hands over to the installed `gpuk update` — the signed channel checked
|
||||||
|
# against the key that install pinned, the image pinned by digest, data untouched
|
||||||
|
# (it lives in the data root, not the container). A flag that changes a setting,
|
||||||
|
# --reinstall or --reset-manifest reinstall over it instead (INS-03).
|
||||||
#
|
#
|
||||||
# This installs a CONTROLLER node (the full app + UI). A headless compute node is
|
# This installs a CONTROLLER node (the full app + UI). A headless compute node is
|
||||||
# `gpuk install --mode worker …` — see deployments/install/gpuk and
|
# `gpuk install --mode worker …` — see deployments/install/gpuk and
|
||||||
@@ -35,8 +38,14 @@
|
|||||||
# Design note — this script starts nothing itself. workerd owns the container's
|
# Design note — this script starts nothing itself. workerd owns the container's
|
||||||
# lifecycle (create, health-gate, roll back, update); the UI's update button and
|
# lifecycle (create, health-gate, roll back, update); the UI's update button and
|
||||||
# `gpuk update` drive that same daemon. One updater, three front doors.
|
# `gpuk update` drive that same daemon. One updater, three front doors.
|
||||||
|
#
|
||||||
|
# The whole script is the body of main(), called on its very last line: `sh`
|
||||||
|
# reads a piped script as it arrives, so a download cut short would otherwise
|
||||||
|
# run as root up to wherever it stopped. Truncated, main is never called.
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
|
main() {
|
||||||
|
|
||||||
# ── Defaults (every one overridable by flag or env) ───────────────────────────
|
# ── Defaults (every one overridable by flag or env) ───────────────────────────
|
||||||
# The channel is the single source of truth for "what is the current release":
|
# The channel is the single source of truth for "what is the current release":
|
||||||
# it is served next to this script, and the backend's update check reads the SAME
|
# it is served next to this script, and the backend's update check reads the SAME
|
||||||
@@ -75,8 +84,21 @@ ETC_DIR="${GPUK_ETC_DIR:-/etc/gpu-kitchen}"
|
|||||||
MANIFEST="$ETC_DIR/manifest.json"
|
MANIFEST="$ETC_DIR/manifest.json"
|
||||||
UNIT_DEST="${GPUK_UNIT_DEST:-/etc/systemd/system/gpu-kitchen-worker.service}"
|
UNIT_DEST="${GPUK_UNIT_DEST:-/etc/systemd/system/gpu-kitchen-worker.service}"
|
||||||
BIN_DEST="${GPUK_BIN_DEST:-/usr/local/bin/gpu-kitchen-worker}"
|
BIN_DEST="${GPUK_BIN_DEST:-/usr/local/bin/gpu-kitchen-worker}"
|
||||||
|
# The gpuk CLI `gpuk install` leaves on the host, next to the daemon binary: the
|
||||||
|
# one that carries the release key this install pinned, and the one a re-run
|
||||||
|
# hands the update to (same override as gpuk's).
|
||||||
|
GPUK_CLI="${GPUK_CLI_DEST:-$(dirname "$BIN_DEST")/gpuk}"
|
||||||
|
# The release public key pinned in THIS copy of install.sh (OPS-20). The channel
|
||||||
|
# copy gets the real key substituted at publish time, exactly like gpuk's; the
|
||||||
|
# operator override covers a self-hosted channel with its own keypair. The
|
||||||
|
# script itself arrives over HTTPS (trust on first use); everything it then
|
||||||
|
# takes from the channel — latest.json, the gpuk installer, the daemon binary,
|
||||||
|
# the image digest — is verified against this key before it is used.
|
||||||
|
CHANNEL_PUBKEY="${GPUK_UPDATE_PUBKEY:-RWQ7BKXJqGX2jdKXu1GxeSPVAN3JDRTefpImM/mFRjtFwq4E7mhnQtA7}"
|
||||||
|
EDITION_GIVEN=0; [ -z "${GPUK_EDITION:-}" ] || EDITION_GIVEN=1
|
||||||
PROFILE=""
|
PROFILE=""
|
||||||
RESET_MANIFEST=0
|
RESET_MANIFEST=0
|
||||||
|
REINSTALL=0
|
||||||
DOMAIN=""
|
DOMAIN=""
|
||||||
VERSION=""
|
VERSION=""
|
||||||
IMAGE=""
|
IMAGE=""
|
||||||
@@ -121,7 +143,7 @@ GPU Kitchen installer
|
|||||||
curl -fsSL https://repo.byterain.io/gpukitchen/channel/raw/branch/main/install.sh | sudo sh -s -- [options]
|
curl -fsSL https://repo.byterain.io/gpukitchen/channel/raw/branch/main/install.sh | sudo sh -s -- [options]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--version <tag> Install this release instead of the channel's current one
|
--version <tag> The release the channel names (another one: --image <ref>@sha256:…)
|
||||||
--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)
|
||||||
@@ -135,8 +157,13 @@ Options:
|
|||||||
host-wide claim. An existing install keeps its mode.
|
host-wide claim. An existing install keeps its mode.
|
||||||
--profile <p> homelab | studio | enterprise | public (no flag + a terminal
|
--profile <p> homelab | studio | enterprise | public (no flag + a terminal
|
||||||
= the script asks; no flag + no terminal = first-run asks)
|
= the script asks; no flag + no terminal = first-run asks)
|
||||||
--domain <d> Domain for the public profile: writes a filled TLS
|
--domain <d> Name the UI is reached by through a reverse proxy, any
|
||||||
reverse-proxy example to <data-root>/caddy/Caddyfile
|
profile: allowed as a host of the install, proxy trusted for
|
||||||
|
the client address, and a filled TLS reverse-proxy example
|
||||||
|
written to <data-root>/caddy/Caddyfile
|
||||||
|
--reinstall On a host that already runs GPU Kitchen, reinstall over it
|
||||||
|
(settings kept, merged into its manifest) instead of the
|
||||||
|
default re-run, which is 'gpuk update'
|
||||||
--reset-manifest Rebuild /etc/gpu-kitchen/manifest.json from this run's settings
|
--reset-manifest Rebuild /etc/gpu-kitchen/manifest.json from this run's settings
|
||||||
when the daemon refuses the existing one (a field an older
|
when the daemon refuses the existing one (a field an older
|
||||||
release wrote); the old file is archived beside it and what
|
release wrote); the old file is archived beside it and what
|
||||||
@@ -156,7 +183,7 @@ while [ $# -gt 0 ]; do
|
|||||||
case "$1" in
|
case "$1" in
|
||||||
--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"; EDITION_GIVEN=1; shift 2 ;;
|
||||||
--port) PORT="$2"; PORT_GIVEN=1; shift 2 ;;
|
--port) PORT="$2"; PORT_GIVEN=1; shift 2 ;;
|
||||||
--mtls-port) MTLS_PORT="$2"; MTLS_GIVEN=1; shift 2 ;;
|
--mtls-port) MTLS_PORT="$2"; MTLS_GIVEN=1; shift 2 ;;
|
||||||
--inference-port) INFERENCE_PORT="$2"; INFERENCE_GIVEN=1; shift 2 ;;
|
--inference-port) INFERENCE_PORT="$2"; INFERENCE_GIVEN=1; shift 2 ;;
|
||||||
@@ -168,6 +195,7 @@ while [ $# -gt 0 ]; do
|
|||||||
--domain) DOMAIN="$2"; shift 2 ;;
|
--domain) DOMAIN="$2"; shift 2 ;;
|
||||||
--non-interactive) NON_INTERACTIVE=1; shift ;;
|
--non-interactive) NON_INTERACTIVE=1; shift ;;
|
||||||
--reset-manifest) RESET_MANIFEST=1; shift ;;
|
--reset-manifest) RESET_MANIFEST=1; shift ;;
|
||||||
|
--reinstall) REINSTALL=1; shift ;;
|
||||||
--worker-binary) WORKER_BINARY="$2"; shift 2 ;;
|
--worker-binary) WORKER_BINARY="$2"; shift 2 ;;
|
||||||
--gpuk-script) GPUK_SCRIPT="$2"; shift 2 ;;
|
--gpuk-script) GPUK_SCRIPT="$2"; shift 2 ;;
|
||||||
--skip-gpu-check) SKIP_GPU_CHECK=1; shift ;;
|
--skip-gpu-check) SKIP_GPU_CHECK=1; shift ;;
|
||||||
@@ -260,7 +288,8 @@ published_ports() { # $1 = network mode → OURS_UI OURS_MTLS OURS_INF
|
|||||||
fi
|
fi
|
||||||
[ -n "$OURS_INF" ] || OURS_INF=8200
|
[ -n "$OURS_INF" ] || OURS_INF=8200
|
||||||
}
|
}
|
||||||
EXISTING=""; OUR_PORTS=""; CONTAINER_NAME="gpu-kitchen"
|
EXISTING=""; OUR_PORTS=""; CONTAINER_NAME="gpu-kitchen"; RECONFIGURE=""
|
||||||
|
reconfigure() { RECONFIGURE="${RECONFIGURE:+$RECONFIGURE, }$1"; }
|
||||||
OURS_UI=""; OURS_MTLS=""; OURS_INF=""
|
OURS_UI=""; OURS_MTLS=""; OURS_INF=""
|
||||||
if [ -f "$MANIFEST" ] && [ ! -r "$MANIFEST" ]; then
|
if [ -f "$MANIFEST" ] && [ ! -r "$MANIFEST" ]; then
|
||||||
EXISTING="unreadable"
|
EXISTING="unreadable"
|
||||||
@@ -290,6 +319,28 @@ case "$EXISTING" in
|
|||||||
ok "image $(manifest_str image)"
|
ok "image $(manifest_str image)"
|
||||||
_root=$(manifest_str dataRoot); _cache=$(manifest_str hostPath)
|
_root=$(manifest_str dataRoot); _cache=$(manifest_str hostPath)
|
||||||
_cluster=$(manifest_str GPUK_CLUSTER); _profile=$(manifest_str GPUK_INSTALL_PROFILE)
|
_cluster=$(manifest_str GPUK_CLUSTER); _profile=$(manifest_str GPUK_INSTALL_PROFILE)
|
||||||
|
# The edition an install runs is its image's basename (gpukitchen-controller-ee).
|
||||||
|
_edition=$(manifest_str image); _edition=${_edition%@*}; _edition=${_edition##*/}
|
||||||
|
case "${_edition%%:*}" in *-ee) _edition=enterprise ;; *) _edition=community ;; esac
|
||||||
|
# What this run asks to CHANGE, before anything is inherited: a flag restating
|
||||||
|
# the installed value changes nothing, so the same command line re-run later
|
||||||
|
# is still an update. Anything else is an explicit reinstall.
|
||||||
|
[ "$PORT_GIVEN" -eq 0 ] || [ "$PORT" = "$OURS_UI" ] || reconfigure "--port"
|
||||||
|
[ "$MTLS_GIVEN" -eq 0 ] || [ "$MTLS_PORT" = "$OURS_MTLS" ] || reconfigure "--mtls-port"
|
||||||
|
[ "$INFERENCE_GIVEN" -eq 0 ] || [ "$INFERENCE_PORT" = "$OURS_INF" ] || reconfigure "--inference-port"
|
||||||
|
[ "$DATA_ROOT_GIVEN" -eq 0 ] || [ "$DATA_ROOT" = "$_root" ] || reconfigure "--data-root"
|
||||||
|
[ "$CACHE_GIVEN" -eq 0 ] || [ "$CACHE_DIR" = "$_cache" ] || reconfigure "--cache-dir"
|
||||||
|
[ "$CLUSTER_GIVEN" -eq 0 ] || [ "$CLUSTER" = "$_cluster" ] || reconfigure "--cluster"
|
||||||
|
[ "$NETWORK_GIVEN" -eq 0 ] || [ "$NETWORK" = "${C_NETMODE:-host}" ] || reconfigure "--network"
|
||||||
|
[ "$EDITION_GIVEN" -eq 0 ] || [ "$EDITION" = "$_edition" ] || reconfigure "--edition"
|
||||||
|
[ -z "$PROFILE" ] || [ "$PROFILE" = "$_profile" ] || reconfigure "--profile"
|
||||||
|
[ -z "$DOMAIN" ] || reconfigure "--domain"
|
||||||
|
[ -z "$IMAGE" ] || reconfigure "--image"
|
||||||
|
[ -z "$VERSION" ] || reconfigure "--version"
|
||||||
|
[ -z "$WORKER_BINARY" ] || reconfigure "--worker-binary"
|
||||||
|
[ -z "$GPUK_SCRIPT" ] || reconfigure "--gpuk-script"
|
||||||
|
[ "$REINSTALL" -eq 0 ] || reconfigure "--reinstall"
|
||||||
|
[ "$RESET_MANIFEST" -eq 0 ] || reconfigure "--reset-manifest"
|
||||||
[ "$DATA_ROOT_GIVEN" -eq 1 ] || [ -z "$_root" ] || DATA_ROOT="$_root"
|
[ "$DATA_ROOT_GIVEN" -eq 1 ] || [ -z "$_root" ] || DATA_ROOT="$_root"
|
||||||
[ "$CACHE_GIVEN" -eq 1 ] || [ -z "$_cache" ] || CACHE_DIR="$_cache"
|
[ "$CACHE_GIVEN" -eq 1 ] || [ -z "$_cache" ] || CACHE_DIR="$_cache"
|
||||||
[ "$CLUSTER_GIVEN" -eq 1 ] || [ -z "$_cluster" ] || CLUSTER="$_cluster"
|
[ "$CLUSTER_GIVEN" -eq 1 ] || [ -z "$_cluster" ] || CLUSTER="$_cluster"
|
||||||
@@ -308,7 +359,11 @@ case "$EXISTING" in
|
|||||||
ok "ports UI $OURS_UI, worker channel $OURS_MTLS, inference $OURS_INF"
|
ok "ports UI $OURS_UI, worker channel $OURS_MTLS, inference $OURS_INF"
|
||||||
ok "network ${C_NETMODE:-host}"
|
ok "network ${C_NETMODE:-host}"
|
||||||
[ -z "$_profile" ] || ok "profile $_profile"
|
[ -z "$_profile" ] || ok "profile $_profile"
|
||||||
ok "re-running updates it in place. Its settings are kept unless a flag says otherwise."
|
if [ -n "$RECONFIGURE" ]; then
|
||||||
|
ok "reinstalling over it ($RECONFIGURE). Its other settings are kept."
|
||||||
|
else
|
||||||
|
ok "re-running updates it in place, through the installed gpuk. Its settings are kept."
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
unreadable)
|
unreadable)
|
||||||
step "Existing install — $MANIFEST"
|
step "Existing install — $MANIFEST"
|
||||||
@@ -327,6 +382,33 @@ case "$EXISTING" in
|
|||||||
esac
|
esac
|
||||||
[ -n "$CACHE_DIR" ] || CACHE_DIR="$DATA_ROOT/hf"
|
[ -n "$CACHE_DIR" ] || CACHE_DIR="$DATA_ROOT/hf"
|
||||||
|
|
||||||
|
# ── 0b. A plain re-run is `gpuk update` (INS-03, OPS-20) ──────────────────────
|
||||||
|
# The gpuk this install left on the host carries the release key pinned when it
|
||||||
|
# was installed: it verifies the signed channel against THAT key, pins the image
|
||||||
|
# by digest, and lets the daemon pull, health-gate and roll back. A freshly
|
||||||
|
# downloaded install.sh is trust-on-first-use again; handing the update to the
|
||||||
|
# installed gpuk keeps a re-run inside the chain the first install established.
|
||||||
|
if [ "$EXISTING" = "manifest" ] && [ -z "$RECONFIGURE" ]; then
|
||||||
|
step "Update — handed to the installed gpuk"
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
step "Dry run — stopping here"
|
||||||
|
echo " would run : $GPUK_CLI update"
|
||||||
|
echo " existing : yes — updated in place"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
[ -x "$GPUK_CLI" ] \
|
||||||
|
|| die "this install has no $GPUK_CLI to update it with. Re-run with --reinstall: it reinstalls
|
||||||
|
over the existing install, keeps its settings, and leaves gpuk on the host for the next update."
|
||||||
|
"$GPUK_CLI" update || die "the update failed — gpuk reported the cause just above"
|
||||||
|
echo
|
||||||
|
echo "${BOLD}${GREEN}GPU Kitchen is up to date.${RESET}"
|
||||||
|
echo
|
||||||
|
echo " Status : gpuk status Logs: gpuk logs"
|
||||||
|
echo " Change a setting: re-run with its flag (a reinstall), or use Settings in the UI"
|
||||||
|
echo
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$PORT" = "$MTLS_PORT" ] || [ "$PORT" = "$INFERENCE_PORT" ] || [ "$MTLS_PORT" = "$INFERENCE_PORT" ]; then
|
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)"
|
die "the UI, worker channel and inference ports must differ (got $PORT, $MTLS_PORT, $INFERENCE_PORT)"
|
||||||
fi
|
fi
|
||||||
@@ -549,6 +631,71 @@ fi
|
|||||||
# ── 2. Resolve the release ───────────────────────────────────────────────────
|
# ── 2. Resolve the release ───────────────────────────────────────────────────
|
||||||
step "Release — resolving the version to install"
|
step "Release — resolving the version to install"
|
||||||
|
|
||||||
|
# Everything fetched from the channel lands here, and is verified here, before use.
|
||||||
|
TMP=$(mktemp -d)
|
||||||
|
# shellcheck disable=SC2064 # expand TMP now: it must be removed even if it changes
|
||||||
|
trap "rm -rf '$TMP'" EXIT INT TERM
|
||||||
|
|
||||||
|
# The trust chain (OPS-20): latest.json is signed with the release key, and so is
|
||||||
|
# every daemon binary; latest.json names the gpuk installer's sha256 and the
|
||||||
|
# image digests. Fail-closed: no pinned key, no minisign, no or a bad signature
|
||||||
|
# are all fatal. GPUK_CHANNEL_INSECURE=1 is the explicit, loudly reported
|
||||||
|
# opt-out for a private mirror that does not sign — gpuk's own opt-out.
|
||||||
|
INSECURE=0; [ "${GPUK_CHANNEL_INSECURE:-}" != "1" ] || INSECURE=1
|
||||||
|
# The minisign CLI is what verifies the release; a host without it gets it from
|
||||||
|
# its own package manager — detected, non-interactive, quiet — before anything
|
||||||
|
# else changes. No known manager, or an install that fails: stop, naming the
|
||||||
|
# manual command. GPUK_MINISIGN names another verifier binary (test seam).
|
||||||
|
MINISIGN="${GPUK_MINISIGN:-minisign}"
|
||||||
|
minisign_manual_command() {
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then echo "apt-get install minisign"
|
||||||
|
elif command -v dnf >/dev/null 2>&1; then echo "dnf install minisign (EPEL on RHEL)"
|
||||||
|
elif command -v yum >/dev/null 2>&1; then echo "yum install minisign (EPEL)"
|
||||||
|
elif command -v zypper >/dev/null 2>&1; then echo "zypper install minisign"
|
||||||
|
elif command -v apk >/dev/null 2>&1; then echo "apk add minisign"
|
||||||
|
elif command -v pacman >/dev/null 2>&1; then echo "pacman -S minisign"
|
||||||
|
else echo "install minisign from https://jedisct1.github.io/minisign/"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
ensure_minisign() { # $1 = 1 when nothing may be installed (dry run)
|
||||||
|
command -v "$MINISIGN" >/dev/null 2>&1 && return 0
|
||||||
|
[ "${1:-0}" -eq 0 ] \
|
||||||
|
|| die "minisign is required to verify the release and a dry run installs nothing: $(minisign_manual_command), or set GPUK_CHANNEL_INSECURE=1"
|
||||||
|
_mlog=$(mktemp)
|
||||||
|
for _pm in apt-get dnf yum zypper apk pacman; do
|
||||||
|
command -v "$_pm" >/dev/null 2>&1 || continue
|
||||||
|
ok "minisign is missing — installing it with $_pm"
|
||||||
|
case "$_pm" in
|
||||||
|
apt-get) { DEBIAN_FRONTEND=noninteractive apt-get update -qq \
|
||||||
|
&& DEBIAN_FRONTEND=noninteractive apt-get install -y -qq minisign; } ;;
|
||||||
|
dnf) dnf install -y -q minisign ;;
|
||||||
|
yum) yum install -y -q minisign ;;
|
||||||
|
zypper) zypper --non-interactive --quiet install minisign ;;
|
||||||
|
apk) apk add --quiet minisign ;;
|
||||||
|
pacman) pacman -S --noconfirm --needed --quiet minisign ;;
|
||||||
|
esac >"$_mlog" 2>&1 || true
|
||||||
|
if command -v "$MINISIGN" >/dev/null 2>&1; then rm -f "$_mlog"; return 0; fi
|
||||||
|
done
|
||||||
|
tail -5 "$_mlog" >&2 2>/dev/null || true
|
||||||
|
rm -f "$_mlog"
|
||||||
|
die "minisign is required to verify the release and could not be installed automatically. Nothing was changed: run '$(minisign_manual_command)' as root, then re-run — or set GPUK_CHANNEL_INSECURE=1"
|
||||||
|
}
|
||||||
|
require_verifier() {
|
||||||
|
case "$CHANNEL_PUBKEY" in
|
||||||
|
# The unstamped placeholder, matched by its prefix only: the release stamps
|
||||||
|
# the key by substituting the whole placeholder wherever it appears, and a
|
||||||
|
# guard spelling it in full would become one refusing the very key it pinned.
|
||||||
|
""|__GPUK_UPDATE_*)
|
||||||
|
die "this install.sh carries no pinned release public key: use the channel's copy, set
|
||||||
|
GPUK_UPDATE_PUBKEY (the minisign public-key line), or GPUK_CHANNEL_INSECURE=1 to skip verification" ;;
|
||||||
|
esac
|
||||||
|
ensure_minisign "$DRY_RUN"
|
||||||
|
}
|
||||||
|
verify_signature() { # $1 = file, $2 = its .minisig, $3 = what it is
|
||||||
|
"$MINISIGN" -Vq -m "$1" -x "$2" -P "$CHANNEL_PUBKEY" >/dev/null 2>&1 \
|
||||||
|
|| die "$3: signature verification FAILED against the pinned release key — refusing it (OPS-20)"
|
||||||
|
}
|
||||||
|
|
||||||
# One tiny JSON document, fetched over TLS, holding what the current release IS.
|
# One tiny JSON document, fetched over TLS, holding what the current release IS.
|
||||||
# Parsed with sed rather than jq: `curl … | sudo sh` cannot assume jq exists, and
|
# Parsed with sed rather than jq: `curl … | sudo sh` cannot assume jq exists, and
|
||||||
# the document is ours and flat.
|
# the document is ours and flat.
|
||||||
@@ -576,30 +723,65 @@ if [ -n "$IMAGE" ]; then
|
|||||||
ok "using the image given on the command line: $IMAGE"
|
ok "using the image given on the command line: $IMAGE"
|
||||||
[ -n "$VERSION" ] || VERSION="(pinned by --image)"
|
[ -n "$VERSION" ] || VERSION="(pinned by --image)"
|
||||||
else
|
else
|
||||||
CHANNEL=$(curl -fsSL --max-time 20 "$CHANNEL_URL" 2>/dev/null) \
|
CHANNEL_DOC="$TMP/latest.json"
|
||||||
|
curl -fsSL --max-time 20 "$CHANNEL_URL" -o "$CHANNEL_DOC" 2>/dev/null \
|
||||||
|| die "cannot reach the release channel at $CHANNEL_URL
|
|| die "cannot reach the release channel at $CHANNEL_URL
|
||||||
If this host is offline, pass --image <ref> to install a specific image directly."
|
If this host is offline, pass --image <ref> to install a specific image directly."
|
||||||
|
if [ "$INSECURE" -eq 1 ]; then
|
||||||
|
warn "GPUK_CHANNEL_INSECURE=1 — the release channel and what it names are NOT verified"
|
||||||
|
else
|
||||||
|
require_verifier
|
||||||
|
curl -fsSL --max-time 20 "$CHANNEL_URL.minisig" -o "$CHANNEL_DOC.minisig" 2>/dev/null \
|
||||||
|
|| die "no signature at $CHANNEL_URL.minisig — refusing an unsigned channel document (OPS-20)"
|
||||||
|
verify_signature "$CHANNEL_DOC" "$CHANNEL_DOC.minisig" "the release channel ($CHANNEL_URL)"
|
||||||
|
ok "release channel signature verified"
|
||||||
|
fi
|
||||||
|
|
||||||
[ -n "$VERSION" ] || VERSION=$(echo "$CHANNEL" | json_field version)
|
CHANNEL_VERSION=$(json_field version < "$CHANNEL_DOC")
|
||||||
[ -n "$VERSION" ] || die "the release channel returned no version: $CHANNEL_URL"
|
[ -n "$CHANNEL_VERSION" ] || die "the release channel returned no version: $CHANNEL_URL"
|
||||||
|
|
||||||
if [ "$EDITION" = "enterprise" ]; then
|
if [ "$EDITION" = "enterprise" ]; then
|
||||||
IMAGE_TEMPLATE=$(echo "$CHANNEL" | json_field controllerImageEnterprise)
|
IMAGE_TEMPLATE=$(json_field controllerImageEnterprise < "$CHANNEL_DOC")
|
||||||
|
IMAGE_DIGEST=$(json_field controllerImageDigestEnterprise < "$CHANNEL_DOC")
|
||||||
else
|
else
|
||||||
IMAGE_TEMPLATE=$(echo "$CHANNEL" | json_field controllerImage)
|
IMAGE_TEMPLATE=$(json_field controllerImage < "$CHANNEL_DOC")
|
||||||
|
IMAGE_DIGEST=$(json_field controllerImageDigest < "$CHANNEL_DOC")
|
||||||
fi
|
fi
|
||||||
[ -n "$IMAGE_TEMPLATE" ] \
|
[ -n "$IMAGE_TEMPLATE" ] \
|
||||||
|| die "the release channel names no $EDITION controller image: $CHANNEL_URL"
|
|| die "the release channel names no $EDITION controller image: $CHANNEL_URL"
|
||||||
|
|
||||||
# The channel gives the repository; WE pin the tag. A floating `:latest` would
|
# The signed document vouches for ONE release: its digest belongs to that
|
||||||
# make every container recreate a silent, unrequested upgrade. Strip any tag the
|
# version and no other. Another release is installed by its full reference.
|
||||||
# channel already carries with image_repo (host:port-safe), then pin OUR version.
|
if [ -n "$VERSION" ] && [ "$VERSION" != "$CHANNEL_VERSION" ]; then
|
||||||
IMAGE="$(image_repo "$IMAGE_TEMPLATE"):${VERSION}"
|
[ "$INSECURE" -eq 1 ] \
|
||||||
|
|| die "the signed channel vouches for $CHANNEL_VERSION only, not $VERSION. To install another
|
||||||
|
release, pass its full reference: --image <repo>:$VERSION@sha256:<digest>"
|
||||||
|
IMAGE_DIGEST=""
|
||||||
|
fi
|
||||||
|
[ -n "$VERSION" ] || VERSION="$CHANNEL_VERSION"
|
||||||
|
case "$IMAGE_DIGEST" in
|
||||||
|
sha256:*) _hex=${IMAGE_DIGEST#sha256:}
|
||||||
|
case "$_hex" in *[!0-9a-f]*) die "the release channel carries a malformed image digest: $IMAGE_DIGEST" ;; esac
|
||||||
|
[ "${#_hex}" -eq 64 ] || die "the release channel carries a malformed image digest: $IMAGE_DIGEST" ;;
|
||||||
|
"") [ "$INSECURE" -eq 1 ] \
|
||||||
|
|| die "the release channel names no $EDITION image digest: refusing to pin a mutable tag (OPS-20)" ;;
|
||||||
|
*) die "the release channel carries a malformed image digest: $IMAGE_DIGEST" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# The channel gives the repository; WE pin the tag and, from the signed
|
||||||
|
# document, the content: `repo:tag@sha256:…` keeps the tag readable while
|
||||||
|
# docker resolves by digest, so a registry cannot swap what the tag points at.
|
||||||
|
# A floating `:latest` would make every recreate a silent, unrequested upgrade.
|
||||||
|
IMAGE="$(image_repo "$IMAGE_TEMPLATE"):${VERSION}${IMAGE_DIGEST:+@$IMAGE_DIGEST}"
|
||||||
ok "release $VERSION"
|
ok "release $VERSION"
|
||||||
ok "image $IMAGE"
|
ok "image $IMAGE"
|
||||||
|
[ -n "$IMAGE_DIGEST" ] || warn "the image is pinned by tag only (no digest in an unverified channel)"
|
||||||
|
|
||||||
[ -n "$WORKER_BINARY" ] || WORKER_RELEASE_BASE=$(echo "$CHANNEL" | json_field workerBase)
|
[ -n "$WORKER_BINARY" ] || WORKER_RELEASE_BASE=$(json_field workerBase < "$CHANNEL_DOC")
|
||||||
[ -n "${GPUK_SCRIPT}" ] || GPUK_SCRIPT_URL=$(echo "$CHANNEL" | json_field gpukScript)
|
if [ -z "${GPUK_SCRIPT}" ]; then
|
||||||
|
GPUK_SCRIPT_URL=$(json_field gpukScript < "$CHANNEL_DOC")
|
||||||
|
GPUK_SCRIPT_SHA256=$(json_field gpukScriptSha256 < "$CHANNEL_DOC")
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# A direct --image and a channel version are held to the same immutable-image
|
# A direct --image and a channel version are held to the same immutable-image
|
||||||
@@ -616,10 +798,6 @@ case "$IMAGE" in
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [ -n "$DOMAIN" ] && [ -n "$PROFILE" ] && [ "$PROFILE" != "public" ]; then
|
|
||||||
die "--domain applies only to --profile public (it fills the TLS reverse-proxy example)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$DRY_RUN" -eq 1 ]; then
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
step "Dry run — stopping here"
|
step "Dry run — stopping here"
|
||||||
if [ "$SKIP_PREFLIGHT" -eq 1 ]; then
|
if [ "$SKIP_PREFLIGHT" -eq 1 ]; then
|
||||||
@@ -714,31 +892,45 @@ if [ "$PROFILE" = "public" ] && [ -z "$DOMAIN" ] && can_prompt; then
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$DOMAIN" ] && [ "$PROFILE" != "public" ]; then
|
|
||||||
die "--domain applies only to --profile public (it fills the TLS reverse-proxy example)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 4. The host daemon ───────────────────────────────────────────────────────
|
# ── 4. The host daemon ───────────────────────────────────────────────────────
|
||||||
step "Host daemon — gpu-kitchen-worker"
|
step "Host daemon — gpu-kitchen-worker"
|
||||||
|
|
||||||
TMP=$(mktemp -d)
|
|
||||||
# shellcheck disable=SC2064 # expand TMP now: it must be removed even if it changes
|
|
||||||
trap "rm -rf '$TMP'" EXIT INT TERM
|
|
||||||
|
|
||||||
if [ -z "$GPUK_SCRIPT" ]; then
|
if [ -z "$GPUK_SCRIPT" ]; then
|
||||||
# A checkout right here beats a download (that is how contributors run it).
|
# A checkout right here beats a download (that is how contributors run it) —
|
||||||
_local="$(dirname "$0")/gpuk"
|
# but ONLY a checkout. Piped into `sh`, $0 is `sh` and dirname "$0" is the
|
||||||
if [ -f "$_local" ]; then
|
# current directory: a stray `./gpuk` in /tmp or a shared folder would run as
|
||||||
|
# root, unverified. So the local copy counts only when this script was run by
|
||||||
|
# its path, next to its gpuk, inside a repository checkout (dev.sh two levels
|
||||||
|
# up, deployments/install/); everything else downloads (INS-03).
|
||||||
|
_local=""
|
||||||
|
case "$0" in
|
||||||
|
install.sh|*/install.sh)
|
||||||
|
_here=$(dirname "$0")
|
||||||
|
[ -f "$_here/gpuk" ] && [ -f "$_here/../../dev.sh" ] && _local="$_here/gpuk"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
if [ -n "$_local" ]; then
|
||||||
GPUK_SCRIPT="$_local"
|
GPUK_SCRIPT="$_local"
|
||||||
ok "using the gpuk installer from this checkout"
|
ok "using the gpuk installer from this checkout ($_local)"
|
||||||
else
|
else
|
||||||
[ -n "${GPUK_SCRIPT_URL:-}" ] \
|
[ -n "${GPUK_SCRIPT_URL:-}" ] \
|
||||||
|| die "the release channel names no gpuk installer, and none was found locally"
|
|| die "the release channel names no gpuk installer, and none was found locally"
|
||||||
curl -fsSL --max-time 60 "$GPUK_SCRIPT_URL" -o "$TMP/gpuk" \
|
curl -fsSL --max-time 60 "$GPUK_SCRIPT_URL" -o "$TMP/gpuk" \
|
||||||
|| die "cannot download the gpuk installer from $GPUK_SCRIPT_URL"
|
|| die "cannot download the gpuk installer from $GPUK_SCRIPT_URL"
|
||||||
|
# The signed channel names the installer's sha256: the bytes that run as root
|
||||||
|
# next are the ones the release signed for, wherever they were served from.
|
||||||
|
if [ -n "${GPUK_SCRIPT_SHA256:-}" ]; then
|
||||||
|
_sum=$(sha256sum "$TMP/gpuk" | cut -d' ' -f1)
|
||||||
|
[ "$_sum" = "$GPUK_SCRIPT_SHA256" ] \
|
||||||
|
|| die "the gpuk installer from $GPUK_SCRIPT_URL does not match the signed channel (sha256 $_sum)"
|
||||||
|
ok "downloaded the gpuk installer (sha256 matches the signed channel)"
|
||||||
|
else
|
||||||
|
[ "$INSECURE" -eq 1 ] \
|
||||||
|
|| die "the release channel names no gpukScriptSha256: refusing an unverified gpuk installer (OPS-20)"
|
||||||
|
warn "downloaded the gpuk installer, NOT verified (GPUK_CHANNEL_INSECURE=1)"
|
||||||
|
fi
|
||||||
chmod +x "$TMP/gpuk"
|
chmod +x "$TMP/gpuk"
|
||||||
GPUK_SCRIPT="$TMP/gpuk"
|
GPUK_SCRIPT="$TMP/gpuk"
|
||||||
ok "downloaded the gpuk installer"
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -767,9 +959,45 @@ if [ -n "$WORKER_BINARY" ]; then
|
|||||||
set -- "$@" --binary "$WORKER_BINARY"
|
set -- "$@" --binary "$WORKER_BINARY"
|
||||||
ok "using a locally-built gpu-kitchen-worker"
|
ok "using a locally-built gpu-kitchen-worker"
|
||||||
elif [ -n "${WORKER_RELEASE_BASE:-}" ]; then
|
elif [ -n "${WORKER_RELEASE_BASE:-}" ]; then
|
||||||
GPUK_RELEASE_BASE="$WORKER_RELEASE_BASE"
|
# The privileged daemon: downloaded here, its minisign signature checked
|
||||||
export GPUK_RELEASE_BASE
|
# against the pinned release key, and only then handed to gpuk (OPS-20).
|
||||||
ok "gpu-kitchen-worker will be downloaded from the release"
|
case "$(uname -m)" in
|
||||||
|
x86_64|amd64) _asset="gpu-kitchen-worker-x86_64"; _build_key="workerBuildIdX86_64" ;;
|
||||||
|
aarch64|arm64) _asset="gpu-kitchen-worker-aarch64"; _build_key="workerBuildIdAarch64" ;;
|
||||||
|
*) die "unsupported architecture: $(uname -m)" ;;
|
||||||
|
esac
|
||||||
|
ok "downloading $_asset from the release"
|
||||||
|
curl -fL --progress-bar "$WORKER_RELEASE_BASE/$_asset" -o "$TMP/gpu-kitchen-worker" \
|
||||||
|
|| die "cannot download $WORKER_RELEASE_BASE/$_asset"
|
||||||
|
if [ "$INSECURE" -eq 1 ]; then
|
||||||
|
warn "gpu-kitchen-worker NOT verified (GPUK_CHANNEL_INSECURE=1)"
|
||||||
|
else
|
||||||
|
curl -fsSL --max-time 60 "$WORKER_RELEASE_BASE/$_asset.minisig" -o "$TMP/gpu-kitchen-worker.minisig" \
|
||||||
|
|| die "no signature at $WORKER_RELEASE_BASE/$_asset.minisig — refusing an unsigned daemon binary"
|
||||||
|
verify_signature "$TMP/gpu-kitchen-worker" "$TMP/gpu-kitchen-worker.minisig" "gpu-kitchen-worker ($_asset)"
|
||||||
|
# The signature binds artifact AND build (trusted comment
|
||||||
|
# `gpu-kitchen-worker@<buildId>`, P2-29): the build must be the one the signed
|
||||||
|
# channel names for this release, so an older signed binary served in its
|
||||||
|
# place is refused, and never older than the daemon already installed.
|
||||||
|
_build=$(json_field "$_build_key" < "$CHANNEL_DOC")
|
||||||
|
[ -n "$_build" ] \
|
||||||
|
|| die "the release channel names no $_build_key: cannot tell which gpu-kitchen-worker build it vouches for (OPS-20)"
|
||||||
|
_comment=$("$MINISIGN" -V -m "$TMP/gpu-kitchen-worker" -x "$TMP/gpu-kitchen-worker.minisig" -P "$CHANNEL_PUBKEY" 2>/dev/null \
|
||||||
|
| sed -n 's/^Trusted comment: //p' | head -1)
|
||||||
|
[ "$_comment" = "gpu-kitchen-worker@$_build" ] \
|
||||||
|
|| die "gpu-kitchen-worker ($_asset) is signed for '${_comment:-nothing}', not gpu-kitchen-worker@$_build — refusing it (OPS-20)"
|
||||||
|
if [ -x "$BIN_DEST" ]; then
|
||||||
|
_installed=$("$BIN_DEST" --version 2>/dev/null | sed -n 's/^gpu-kitchen-worker //p' | head -1)
|
||||||
|
case "$_installed" in
|
||||||
|
[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-*)
|
||||||
|
# buildId = YYYYMMDDHHMMSS-<commit>: the timestamp orders builds.
|
||||||
|
[ "${_build%%-*}" -ge "${_installed%%-*}" ] \
|
||||||
|
|| die "gpu-kitchen-worker $_build is OLDER than the installed $_installed — refusing a downgrade" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
ok "gpu-kitchen-worker signature verified (build $_build)"
|
||||||
|
fi
|
||||||
|
set -- "$@" --binary "$TMP/gpu-kitchen-worker"
|
||||||
fi
|
fi
|
||||||
# else: gpuk reuses an already-installed binary, or fails with its own message.
|
# else: gpuk reuses an already-installed binary, or fails with its own message.
|
||||||
|
|
||||||
@@ -803,25 +1031,32 @@ echo
|
|||||||
echo " ${BOLD}Open:${RESET} http://${HOSTNAME_FQDN}:${PORT}"
|
echo " ${BOLD}Open:${RESET} http://${HOSTNAME_FQDN}:${PORT}"
|
||||||
[ -n "$LAN_IP" ] && echo " http://${LAN_IP}:${PORT}"
|
[ -n "$LAN_IP" ] && echo " http://${LAN_IP}:${PORT}"
|
||||||
echo
|
echo
|
||||||
CLAIM_CODE_FILE="$DATA_ROOT/secrets/claim_code"
|
# [PRF-12] [SEC-53] homelab and studio open straight into the app: the controller
|
||||||
CLAIM_CODE=""
|
# closed their claim window at first boot, so there is no code to show. Every other
|
||||||
[ ! -s "$CLAIM_CODE_FILE" ] || CLAIM_CODE=$(cat "$CLAIM_CODE_FILE" 2>/dev/null || true)
|
# profile — or none yet — starts with the claim code (SEC-52).
|
||||||
if [ -n "$CLAIM_CODE" ]; then
|
case "$PROFILE" in
|
||||||
echo " ${BOLD}Claim code:${RESET} $CLAIM_CODE"
|
homelab|studio)
|
||||||
echo " ${BOLD}Read again:${RESET} $CLAIM_CODE_FILE (mode 0600; removed after claim)"
|
echo " ${BOLD}Next:${RESET} open the URL above: GPU Kitchen opens straight away."
|
||||||
else
|
;;
|
||||||
echo " ${BOLD}Claim code:${RESET} consumed (the first account already exists)"
|
*)
|
||||||
fi
|
CLAIM_CODE_FILE="$DATA_ROOT/secrets/claim_code"
|
||||||
echo
|
CLAIM_CODE=""
|
||||||
# The wizard's first step asks for a Kitchen ACCOUNT key (CPT-04). It is the
|
[ ! -s "$CLAIM_CODE_FILE" ] || CLAIM_CODE=$(cat "$CLAIM_CODE_FILE" 2>/dev/null || true)
|
||||||
# person's credential, never the machine's — this script cannot create or print
|
if [ -n "$CLAIM_CODE" ]; then
|
||||||
# it, and the browser must not receive it in a URL (CPT-05/06). What it can do
|
echo " ${BOLD}Claim code:${RESET} $CLAIM_CODE"
|
||||||
# is say so, and say where the key comes from, before the page does.
|
echo " ${BOLD}Read again:${RESET} $CLAIM_CODE_FILE (mode 0600; removed after claim)"
|
||||||
echo " ${BOLD}Next:${RESET} open the URL above. Its first step asks for your GPU Kitchen account key"
|
else
|
||||||
echo " (gpuk_…). Sign in to your GPU Kitchen account and create one under Install keys:"
|
echo " ${BOLD}Claim code:${RESET} consumed (the first account already exists)"
|
||||||
echo " https://gpu.kitchen/account#install-keys"
|
fi
|
||||||
echo " That key is yours, not this machine's: the installer never sees it,"
|
echo
|
||||||
echo " and the page exchanges it for a revocable installation token."
|
echo " ${BOLD}Next:${RESET} open the URL above and enter the claim code."
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
# [INS-50] Linking to a GPU Kitchen account is never a first-run step: a banner in
|
||||||
|
# the app offers it, and the person approves on gpu.kitchen — no key is copied here,
|
||||||
|
# none ever travels in a URL (CPT-05/06).
|
||||||
|
echo " A banner in the app connects this installation to your GPU Kitchen account:"
|
||||||
|
echo " \"Connect to my account\" or \"Create an account\" on gpu.kitchen — nothing to copy."
|
||||||
|
|
||||||
case "$PROFILE" in
|
case "$PROFILE" in
|
||||||
public)
|
public)
|
||||||
@@ -849,7 +1084,6 @@ case "$PROFILE" in
|
|||||||
echo
|
echo
|
||||||
;;
|
;;
|
||||||
homelab|studio)
|
homelab|studio)
|
||||||
echo " ${BOLD}Then:${RESET} finish first-run in the UI"
|
|
||||||
echo
|
echo
|
||||||
;;
|
;;
|
||||||
"")
|
"")
|
||||||
@@ -857,6 +1091,13 @@ case "$PROFILE" in
|
|||||||
echo
|
echo
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
# Any profile may be reached by a name through a reverse proxy (INS-47); the
|
||||||
|
# public case above already printed its steps.
|
||||||
|
if [ -n "$DOMAIN" ] && [ "$PROFILE" != "public" ]; then
|
||||||
|
echo " ${BOLD}HTTPS:${RESET} https://${DOMAIN} once your reverse proxy serves it; a filled"
|
||||||
|
echo " Caddy example was written to $DATA_ROOT/caddy/Caddyfile."
|
||||||
|
echo
|
||||||
|
fi
|
||||||
echo " Inference endpoint : http://${HOSTNAME_FQDN}:${INFERENCE_PORT}/v1"
|
echo " Inference endpoint : http://${HOSTNAME_FQDN}:${INFERENCE_PORT}/v1"
|
||||||
echo " Version : ${VERSION}"
|
echo " Version : ${VERSION}"
|
||||||
echo
|
echo
|
||||||
@@ -888,3 +1129,6 @@ echo " Update : re-run this command, or press Update in the UI, or: gpuk updat
|
|||||||
echo " Status : gpuk status Logs: gpuk logs"
|
echo " Status : gpuk status Logs: gpuk logs"
|
||||||
echo " Remove : gpuk uninstall (service only) or gpuk uninstall --purge (all but the data root)"
|
echo " Remove : gpuk uninstall (service only) or gpuk uninstall --purge (all but the data root)"
|
||||||
echo
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
|
|||||||
+9
-6
@@ -1,11 +1,14 @@
|
|||||||
{
|
{
|
||||||
"version": "v0.1.11",
|
"version": "v0.1.13",
|
||||||
"semver": "0.1.11",
|
"semver": "0.1.13",
|
||||||
"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:358a66cc790d2da9b2fce98593e323747f7638945414ba7a2965090369a5470d",
|
"controllerImageDigest": "sha256:e48971d9f7872b7a3cfbf82260ddaf4fbe0a493496f484894def74442e90cd92",
|
||||||
"controllerImageDigestEnterprise": "sha256:71f254ec52894e1634e9fa7b976dff40efd69ebf1121be0c7c80b5748aea75e8",
|
"controllerImageDigestEnterprise": "sha256:a469c7e5c8e624239a0203574790681d6698d4dbd8574ecfb451f5c5a320f537",
|
||||||
"workerBase": "https://repo.byterain.io/api/packages/gpukitchen/generic/gpu-kitchen-worker/v0.1.11",
|
"gpukScriptSha256": "40286416f58d4807c18d62b8dda988ef87714e32c35fe1343e8b4de3fad36dcf",
|
||||||
"gpukScript": "https://repo.byterain.io/api/packages/gpukitchen/generic/gpu-kitchen-channel/v0.1.11/gpuk",
|
"workerBuildIdX86_64": "20260928160010-d6ea38b659d912eec5a2b47b3b08f9eeb09aea01",
|
||||||
|
"workerBuildIdAarch64": "20260928160010-d6ea38b659d912eec5a2b47b3b08f9eeb09aea01",
|
||||||
|
"workerBase": "https://repo.byterain.io/api/packages/gpukitchen/generic/gpu-kitchen-worker/v0.1.13",
|
||||||
|
"gpukScript": "https://repo.byterain.io/api/packages/gpukitchen/generic/gpu-kitchen-channel/v0.1.13/gpuk",
|
||||||
"releaseNotes": "https://repo.byterain.io/gpukitchen/channel"
|
"releaseNotes": "https://repo.byterain.io/gpukitchen/channel"
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
untrusted comment: signature from minisign secret key
|
untrusted comment: signature from minisign secret key
|
||||||
RUQ7BKXJqGX2jbXzspPlihd5bObC3/Ex5EYCJWKoVGhMg6nHMqDO/AeCTvOs6eC1Oy+lUIkOLX7aHDVtqJr3NK5ELVlXFGxhOQE=
|
RUQ7BKXJqGX2jesWQLAgf8TOhEtR7LdTE/9W4xWixCA2CMU5EjcA73hQkqwF1pPaP53Yrx35nu8dGLVqoDAYcminSfEpwakTxwQ=
|
||||||
trusted comment: gpu-kitchen channel v0.1.11
|
trusted comment: gpu-kitchen channel v0.1.13
|
||||||
S2dq0xcF2sO3uSP3KCS9m6Tq8X9bPmfrLjY1WUxhoB9HtBrwBiV5lSUXEtsBX2KKfo0FVisfuyFPM2mDPDKCAA==
|
8JLHZg2cxj2RjgCyUp6S6tO909AmdxZ9wLScByCswPpzV5Vc8aYyM6DcKAmDRyVPyrPmdy+yKpQOZrOFq2DfCA==
|
||||||
|
|||||||
Reference in New Issue
Block a user