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
|
||||
# (apps/worker/src/module.rs, apps/worker/src/identity.rs).
|
||||
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}"
|
||||
MANIFEST="$ETC_DIR/manifest.json"
|
||||
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.
|
||||
curl -fL --progress-bar "$_url" -o "$BIN_DEST.new" \
|
||||
|| { 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"
|
||||
mv "$BIN_DEST.new" "$BIN_DEST"
|
||||
elif [ -x "$BIN_DEST" ]; then
|
||||
@@ -104,6 +121,18 @@ install_binary() { # [local-path]
|
||||
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 '='; }
|
||||
|
||||
# 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
|
||||
# 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
|
||||
# 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
|
||||
chmod 0600 "$CLAIM_CODE_FILE"
|
||||
elif [ ! -f "$MANIFEST" ]; then
|
||||
@@ -256,7 +287,7 @@ UNIT
|
||||
}
|
||||
|
||||
# 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
|
||||
# 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.
|
||||
@@ -266,15 +297,18 @@ UNIT
|
||||
write_caddyfile() {
|
||||
mkdir -p "$DATA_ROOT/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
|
||||
# certificate itself once DNS for $DOMAIN points at this machine.
|
||||
#
|
||||
# sudo cp $DATA_ROOT/caddy/Caddyfile /etc/caddy/Caddyfile
|
||||
# sudo systemctl reload caddy
|
||||
#
|
||||
# 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:
|
||||
# The app already knows $DOMAIN as one of its names (GPUK_ALLOWED_HOSTS) and reads
|
||||
# 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
|
||||
# reach it DIRECTLY — terminating it here would break the pin.
|
||||
# - worker<->worker data transfers (:8300): LAN-only by contract (OPS-68).
|
||||
@@ -406,8 +440,6 @@ cmd_install() {
|
||||
esac
|
||||
|
||||
if [ -n "$DOMAIN" ]; then
|
||||
[ "$PROFILE" = "public" ] \
|
||||
|| die "--domain applies only to --profile public (it fills the TLS reverse-proxy example)"
|
||||
case "$DOMAIN" in
|
||||
*[!A-Za-z0-9.-]*) die "--domain must be a bare domain name (got '$DOMAIN')" ;;
|
||||
esac
|
||||
@@ -438,7 +470,10 @@ cmd_install() {
|
||||
CLAIM_CODE_AVAILABLE=0
|
||||
|
||||
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"
|
||||
[ ! -f "$MANIFEST" ] \
|
||||
|| echo "==> dry run: $MANIFEST exists — this render would be MERGED into it, controller-owned settings kept"
|
||||
@@ -508,6 +543,7 @@ cmd_install() {
|
||||
|
||||
need_root
|
||||
install_binary "$BIN_SRC"
|
||||
install_cli
|
||||
|
||||
mkdir -p "$ETC_DIR" "$DATA_ROOT" "$CACHE_DIR"
|
||||
seed_secrets "$DATA_ROOT"
|
||||
@@ -711,6 +747,14 @@ render_controller_manifest() {
|
||||
EXTRA_ENV="$EXTRA_ENV,\"GPUK_HSTS\":\"true\""
|
||||
EXTRA_ENV="$EXTRA_ENV,\"GPUK_SESSION_COOKIE_SECURE\":\"true\""
|
||||
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=""
|
||||
if [ -s "$BOOTSTRAP_PASSWORD_FILE" ]; then
|
||||
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.
|
||||
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
|
||||
# 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
|
||||
@@ -850,18 +946,13 @@ channel_fetch() {
|
||||
echo "WARNING: GPUK_CHANNEL_INSECURE=1 — release channel signature NOT verified" >&2
|
||||
return 0
|
||||
fi
|
||||
case "$CHANNEL_PUBKEY" in
|
||||
""|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"
|
||||
require_release_key
|
||||
_sig=$(mktemp)
|
||||
if ! curl -fsSL --max-time 20 "${CHANNEL_URL}.minisig" -o "$_sig" 2>/dev/null; then
|
||||
rm -f "$_sig"
|
||||
die "no signature at ${CHANNEL_URL}.minisig — refusing an unsigned channel document (OPS-20)"
|
||||
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"
|
||||
die "latest.json signature verification FAILED — refusing the channel document (OPS-20)"
|
||||
fi
|
||||
@@ -1054,25 +1145,38 @@ cmd_update() {
|
||||
fi
|
||||
|
||||
_digest=""
|
||||
if [ -z "$_want" ]; then
|
||||
# channel_fetch runs in THIS shell (not a $(…) subshell) so a signature
|
||||
# failure is fatal here — fail-closed — and $CHANNEL_DOC survives. The
|
||||
# verified signature closes the document half of OPS-20; the digest read
|
||||
# from it pins CONTENT, closing the mutable-tag half.
|
||||
if channel_fetch; then
|
||||
_want=$(channel_field "$CHANNEL_DOC" version)
|
||||
fi
|
||||
if [ -z "$_want" ]; then
|
||||
echo "==> cannot reach $CHANNEL_URL — re-applying the pinned $_current"
|
||||
GPUK_MANIFEST_PATH="$MANIFEST" "$BIN_DEST" apply
|
||||
return 0
|
||||
fi
|
||||
# channel_fetch runs in THIS shell (not a $(…) subshell) so a signature
|
||||
# failure is fatal here — fail-closed — and $CHANNEL_DOC survives. The
|
||||
# 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
|
||||
# through the same document: it vouches for ONE release, the one it names.
|
||||
_latest=""
|
||||
if channel_fetch; then
|
||||
_latest=$(channel_field "$CHANNEL_DOC" version)
|
||||
fi
|
||||
if [ -z "$_latest" ]; then
|
||||
[ -z "$_want" ] \
|
||||
|| die "cannot read the release channel at $CHANNEL_URL — refusing to pin $_want unverified"
|
||||
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
|
||||
# repo itself may be a mirror, the basename is the edition marker.
|
||||
case "${_repo##*/}" in
|
||||
*-ee) _digest=$(channel_field "$CHANNEL_DOC" controllerImageDigestEnterprise) ;;
|
||||
*) _digest=$(channel_field "$CHANNEL_DOC" controllerImageDigest) ;;
|
||||
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
|
||||
|
||||
_new="$_repo:$_want"
|
||||
@@ -1089,7 +1193,10 @@ cmd_update() {
|
||||
else
|
||||
echo "==> already on $_current — re-pulling and recreating"
|
||||
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 ──────────────────────────────────────────────────────────────────
|
||||
@@ -1131,6 +1238,7 @@ cmd_uninstall() {
|
||||
done
|
||||
[ ! -e "$MACHINE_ID_FILE" ] || rm -f "$MACHINE_ID_FILE"
|
||||
[ ! -e "$BIN_DEST" ] || { rm -f "$BIN_DEST"; echo "==> removed $BIN_DEST"; }
|
||||
[ ! -e "$CLI_DEST" ] || { rm -f "$CLI_DEST"; echo "==> removed $CLI_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>}"
|
||||
@@ -1141,12 +1249,14 @@ usage() {
|
||||
gpuk — GPU Kitchen host daemon (gpu-kitchen-worker)
|
||||
|
||||
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]
|
||||
[--network bridge|host|<net>] [--binary <path>] [--reset-manifest]
|
||||
--network: bridge by default — the container publishes its three
|
||||
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)
|
||||
--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_...
|
||||
[--cluster N] [--cache-dir P] [--binary <path>]
|
||||
[--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;
|
||||
after a failed start, the kept <container>-failed log
|
||||
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)
|
||||
|
||||
Most people never run this directly: the channel's install.sh installs it
|
||||
|
||||
Reference in New Issue
Block a user