mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-02 07:34:45 +08:00
* fix(snap): require mTLS for the snap gateway Replace the installer opt-in with an authenticated snap gateway. The wrapper no longer forces plaintext, so the gateway serves TLS from the bundle it already generates in $SNAP_COMMON/tls. The install hook writes a config that enables mTLS user auth instead of unauthenticated access, and a new post-refresh hook migrates the exact legacy default on existing installs. install.sh waits for the gateway, detects whether it serves TLS, copies the client bundle into the target user's snap state directory, and registers the gateway over HTTPS. Older plaintext snap revisions still register over HTTP with a warning. The release canary asserts mTLS auth and HTTPS registration. Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(snap): pass config preflight and detect the mTLS gateway reliably An explicit [openshell.gateway.mtls_auth] table fails config preflight, which validates mTLS auth before the local TLS bundle supplies the client CA. Write a default that pins the Docker driver instead; with the wrapper's TLS bundle the gateway requires client certificates and enables mTLS user auth automatically, as the native packages do. The mTLS gateway rejects TLS handshakes without a client certificate, and it still answers plaintext loopback HTTP for sandbox service routing, so the installer could misdetect it as a legacy plaintext gateway. Probe HTTPS with the root-owned client bundle, and treat a gateway as legacy only when a plaintext gRPC Health call succeeds. Signed-off-by: Drew Newberry <anewberry@nvidia.com> * chore(snap): simplify install hook comment Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(snap): migrate insecure gateway configs on refresh Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(snap): simplify mTLS detection and config migration Detect the mTLS snap from the installed revision's post-refresh hook instead of probing plaintext gRPC, and drop the scheme global. Remove the installer's pre-hook config fallback, which is dead now that every channel ships the install hook and which wrote the insecure default. Give the install hook a single write path with a simple backup name, and shorten the manual client certificate steps. Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(snap): stop keeping a copy of replaced insecure configs Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(install): make the snap an opt-in install method Stop selecting the OpenShell snap just because the snap command exists. Linux installs default to the Debian or RPM package; OPENSHELL_INSTALL_METHOD=snap (or deb, rpm) selects the package explicitly. Hosts that already have the OpenShell snap keep refreshing it rather than gaining a second gateway on the same port. The release canary and snap repro script opt in explicitly. Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(snap): let the gateway auto-detect its compute driver Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(snap): restart the gateway after refresh Published revisions use refresh-mode: endure, and snapd honors the old revision's setting during a refresh, so the plaintext gateway kept running with the migrated config unused until a manual restart. Restart the gateway from the post-refresh hook so the mTLS config takes effect immediately. Signed-off-by: Drew Newberry <anewberry@nvidia.com> * docs(snap): drop refresh notes from the snap description Signed-off-by: Drew Newberry <anewberry@nvidia.com> * docs(snap): trim snap refresh notes from installation docs Signed-off-by: Drew Newberry <anewberry@nvidia.com> --------- Signed-off-by: Drew Newberry <anewberry@nvidia.com>
1498 lines
44 KiB
Bash
Executable File
1498 lines
44 KiB
Bash
Executable File
#!/bin/sh
|
|
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
# Install OpenShell from a GitHub release.
|
|
#
|
|
# Linux installs either the Debian or RPM packages from the selected release.
|
|
# Apple Silicon macOS installs the generated Homebrew formula, so Homebrew owns
|
|
# the binary layout and launchd service lifecycle.
|
|
#
|
|
set -e
|
|
|
|
APP_NAME="openshell"
|
|
REPO="NVIDIA/OpenShell"
|
|
GITHUB_URL="https://github.com/${REPO}"
|
|
RELEASE_TAG="${OPENSHELL_VERSION:-}"
|
|
RELEASE_ASSET_DIR=""
|
|
CHECKSUMS_NAME="openshell-checksums-sha256.txt"
|
|
LOCAL_GATEWAY_PORT="17670"
|
|
HOMEBREW_TAP="nvidia/openshell"
|
|
HOMEBREW_FORMULA_NAME="openshell"
|
|
HOMEBREW_CLI_ASSET="openshell-aarch64-apple-darwin.tar.gz"
|
|
HOMEBREW_GATEWAY_ASSET="openshell-gateway-aarch64-apple-darwin.tar.gz"
|
|
HOMEBREW_DRIVER_VM_ASSET="openshell-driver-vm-aarch64-apple-darwin.tar.gz"
|
|
HOMEBREW_PROVER_ASSET="openshell-prover-aarch64-apple-darwin.tar.gz"
|
|
BREAKING_RELEASE_VERSION="0.0.37"
|
|
LINUX_PACKAGE_GLIBC_MIN_VERSION="2.28"
|
|
UPGRADE_NOTICE_ACK="${OPENSHELL_ACK_BREAKING_UPGRADE:-}"
|
|
|
|
info() {
|
|
printf '%s: %s\n' "$APP_NAME" "$*" >&2
|
|
}
|
|
|
|
warn() {
|
|
printf '%s: warning: %s\n' "$APP_NAME" "$*" >&2
|
|
}
|
|
|
|
error() {
|
|
printf '%s: error: %s\n' "$APP_NAME" "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
install.sh - Install OpenShell
|
|
|
|
USAGE:
|
|
curl -fsSL https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh -o install.sh
|
|
sh install.sh
|
|
|
|
curl -fsSL https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh
|
|
|
|
OPTIONS:
|
|
--help Print this help message
|
|
|
|
ENVIRONMENT VARIABLES:
|
|
OPENSHELL_VERSION Release tag to install (default: latest tagged release).
|
|
Set OPENSHELL_VERSION=dev to install the rolling dev build.
|
|
Set OPENSHELL_VERSION=pre to install the latest prerelease.
|
|
Prereleases require an authenticated GitHub CLI session.
|
|
OPENSHELL_ACK_BREAKING_UPGRADE
|
|
Set to 1 only after backing up and cleaning up a
|
|
pre-v0.0.37 or non-snap installation.
|
|
OPENSHELL_INSTALL_METHOD
|
|
Linux package to install: snap, deb, or rpm. Unset
|
|
selects deb or rpm from the host package manager.
|
|
|
|
NOTES:
|
|
When OPENSHELL_VERSION is unset, this resolves the latest tagged release
|
|
from ${GITHUB_URL}/releases/latest.
|
|
|
|
Linux installs the Debian package on amd64/arm64 or the RPM packages on
|
|
x86_64/aarch64, depending on the host package manager. Set
|
|
OPENSHELL_INSTALL_METHOD=snap to install the OpenShell snap instead; hosts
|
|
that already have the OpenShell snap keep refreshing it. Snap installs use
|
|
latest/stable by default and latest/edge for dev, and do not support
|
|
explicit release tags or prereleases. The OpenShell snap requires a running
|
|
Docker Engine installed from a system package or Docker's package
|
|
repository. The Docker snap is not currently compatible with OpenShell.
|
|
macOS installs the release Homebrew formula on Apple Silicon and starts a
|
|
brew services-backed local gateway.
|
|
EOF
|
|
}
|
|
|
|
has_cmd() {
|
|
command -v "$1" >/dev/null 2>&1
|
|
}
|
|
|
|
require_cmd() {
|
|
if ! has_cmd "$1"; then
|
|
error "'$1' is required"
|
|
fi
|
|
}
|
|
|
|
download() {
|
|
_url="$1"
|
|
_output="$2"
|
|
curl -fLsS --retry 3 --max-redirs 5 -o "$_output" "$_url"
|
|
}
|
|
|
|
semver_core() {
|
|
_version="${1#v}"
|
|
_version="${_version%%[-+]*}"
|
|
printf '%s\n' "$_version"
|
|
}
|
|
|
|
semver_at_least() {
|
|
_version="$(semver_core "$1")"
|
|
_minimum="$(semver_core "$2")"
|
|
|
|
_major="${_version%%.*}"
|
|
_rest="${_version#*.}"
|
|
[ "$_rest" != "$_version" ] || return 1
|
|
_minor="${_rest%%.*}"
|
|
_patch="${_rest#*.}"
|
|
_patch="${_patch%%.*}"
|
|
|
|
_min_major="${_minimum%%.*}"
|
|
_min_rest="${_minimum#*.}"
|
|
[ "$_min_rest" != "$_minimum" ] || return 1
|
|
_min_minor="${_min_rest%%.*}"
|
|
_min_patch="${_min_rest#*.}"
|
|
_min_patch="${_min_patch%%.*}"
|
|
|
|
case "$_major:$_minor:$_patch:$_min_major:$_min_minor:$_min_patch" in
|
|
*[!0-9:]* | *::*)
|
|
return 1
|
|
;;
|
|
esac
|
|
|
|
[ "$_major" -gt "$_min_major" ] && return 0
|
|
[ "$_major" -lt "$_min_major" ] && return 1
|
|
[ "$_minor" -gt "$_min_minor" ] && return 0
|
|
[ "$_minor" -lt "$_min_minor" ] && return 1
|
|
[ "$_patch" -ge "$_min_patch" ]
|
|
}
|
|
|
|
version_at_least_major_minor() {
|
|
_version="$1"
|
|
_minimum="$2"
|
|
|
|
_major="${_version%%.*}"
|
|
_minor="${_version#*.}"
|
|
_minor="${_minor%%.*}"
|
|
|
|
_min_major="${_minimum%%.*}"
|
|
_min_minor="${_minimum#*.}"
|
|
_min_minor="${_min_minor%%.*}"
|
|
|
|
case "$_major:$_minor:$_min_major:$_min_minor" in
|
|
*[!0-9:]* | *::*)
|
|
return 1
|
|
;;
|
|
esac
|
|
|
|
[ "$_major" -gt "$_min_major" ] && return 0
|
|
[ "$_major" -lt "$_min_major" ] && return 1
|
|
[ "$_minor" -ge "$_min_minor" ]
|
|
}
|
|
|
|
getconf_gnu_libc_version() {
|
|
if [ "${OPENSHELL_INSTALL_SH_TEST:-0}" = "1" ] && [ "${OPENSHELL_TEST_GETCONF_UNAVAILABLE:-0}" = "1" ]; then
|
|
return 127
|
|
fi
|
|
|
|
if [ "${OPENSHELL_INSTALL_SH_TEST:-0}" = "1" ] && [ "${OPENSHELL_TEST_GETCONF_OUTPUT+x}" = "x" ]; then
|
|
printf '%s\n' "$OPENSHELL_TEST_GETCONF_OUTPUT"
|
|
return 0
|
|
fi
|
|
|
|
getconf GNU_LIBC_VERSION 2>/dev/null
|
|
}
|
|
|
|
ldd_version_output() {
|
|
if [ "${OPENSHELL_INSTALL_SH_TEST:-0}" = "1" ] && [ "${OPENSHELL_TEST_LDD_UNAVAILABLE:-0}" = "1" ]; then
|
|
return 127
|
|
fi
|
|
|
|
if [ "${OPENSHELL_INSTALL_SH_TEST:-0}" = "1" ] && [ "${OPENSHELL_TEST_LDD_OUTPUT+x}" = "x" ]; then
|
|
printf '%s\n' "$OPENSHELL_TEST_LDD_OUTPUT"
|
|
return 0
|
|
fi
|
|
|
|
ldd --version 2>&1
|
|
}
|
|
|
|
detect_glibc_version() {
|
|
_ldd_output="$(ldd_version_output 2>&1 || true)"
|
|
case "$_ldd_output" in
|
|
*[Mm][Uu][Ss][Ll]* | *[Aa][Ll][Pp][Ii][Nn][Ee]*)
|
|
return 2
|
|
;;
|
|
esac
|
|
|
|
_ldd_version="$(printf '%s\n' "$_ldd_output" | awk 'FNR == 1 && match($NF, /^[0-9]+\.[0-9]+/) { print substr($NF, RSTART, RLENGTH); exit }')"
|
|
if [ -n "$_ldd_version" ]; then
|
|
printf '%s\n' "$_ldd_version"
|
|
return 0
|
|
fi
|
|
|
|
_getconf_output="$(getconf_gnu_libc_version 2>/dev/null || true)"
|
|
case "$_getconf_output" in
|
|
glibc\ [0-9]*.[0-9]*)
|
|
printf '%s\n' "${_getconf_output#glibc }"
|
|
return 0
|
|
;;
|
|
*[Mm][Uu][Ss][Ll]* | *[Aa][Ll][Pp][Ii][Nn][Ee]*)
|
|
return 2
|
|
;;
|
|
esac
|
|
|
|
return 1
|
|
}
|
|
|
|
require_linux_package_glibc() {
|
|
_glibc_status=0
|
|
_glibc_version="$(detect_glibc_version)" || _glibc_status=$?
|
|
|
|
case "$_glibc_status" in
|
|
0)
|
|
;;
|
|
2)
|
|
error "OpenShell Linux packages require glibc >= ${LINUX_PACKAGE_GLIBC_MIN_VERSION}; detected musl or unsupported libc."
|
|
;;
|
|
*)
|
|
error "OpenShell Linux packages require glibc >= ${LINUX_PACKAGE_GLIBC_MIN_VERSION}; could not detect glibc."
|
|
;;
|
|
esac
|
|
|
|
if ! version_at_least_major_minor "$_glibc_version" "$LINUX_PACKAGE_GLIBC_MIN_VERSION"; then
|
|
error "OpenShell Linux packages require glibc >= ${LINUX_PACKAGE_GLIBC_MIN_VERSION}; detected glibc ${_glibc_version}.
|
|
Please use a newer distribution or container environment."
|
|
fi
|
|
}
|
|
|
|
target_uses_breaking_gateway_model() {
|
|
case "$RELEASE_TAG" in
|
|
dev)
|
|
return 0
|
|
;;
|
|
esac
|
|
|
|
semver_at_least "$RELEASE_TAG" "$BREAKING_RELEASE_VERSION"
|
|
}
|
|
|
|
installed_version_needs_breaking_upgrade_notice() {
|
|
_version="$1"
|
|
|
|
if [ -z "$_version" ]; then
|
|
return 0
|
|
fi
|
|
|
|
! semver_at_least "$_version" "$BREAKING_RELEASE_VERSION"
|
|
}
|
|
|
|
find_existing_native_openshell_bin() {
|
|
_path="$(command -v openshell 2>/dev/null || true)"
|
|
case "$_path" in
|
|
/snap/*) ;;
|
|
*)
|
|
if [ -n "$_path" ] && [ -x "$_path" ]; then
|
|
printf '%s\n' "$_path"
|
|
return 0
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
for _candidate in \
|
|
"${TARGET_HOME:-}/.local/bin/openshell" \
|
|
/usr/local/bin/openshell \
|
|
/usr/bin/openshell \
|
|
/opt/homebrew/bin/openshell; do
|
|
if [ -n "$_candidate" ] && [ -x "$_candidate" ]; then
|
|
printf '%s\n' "$_candidate"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
existing_openshell_version() {
|
|
_bin="$1"
|
|
_output="$("$_bin" --version 2>/dev/null | sed -n '1p' || true)"
|
|
printf '%s\n' "$_output" | awk '
|
|
{
|
|
for (i = 1; i <= NF; i++) {
|
|
if ($i ~ /^v?[0-9]+\.[0-9]+\.[0-9]+([-+][A-Za-z0-9.+~-]+)?$/) {
|
|
print $i
|
|
exit
|
|
}
|
|
}
|
|
}
|
|
'
|
|
}
|
|
|
|
print_breaking_upgrade_notice() {
|
|
_bin="$1"
|
|
_version="$2"
|
|
|
|
if [ -n "$_version" ]; then
|
|
warn "detected existing OpenShell ${_version} at ${_bin}"
|
|
else
|
|
warn "detected an existing OpenShell installation at ${_bin}"
|
|
fi
|
|
|
|
cat >&2 <<EOF
|
|
|
|
OpenShell ${BREAKING_RELEASE_VERSION} and later are incompatible with gateway
|
|
state created by earlier releases. Before installing OpenShell ${RELEASE_TAG}, back up
|
|
any files, artifacts, and configuration you need from existing sandboxes.
|
|
|
|
Then clean up the old runtime with the currently installed CLI:
|
|
|
|
openshell sandbox delete --all
|
|
openshell gateway destroy
|
|
|
|
Run these commands before upgrading because 'openshell gateway destroy' is not
|
|
available in OpenShell ${BREAKING_RELEASE_VERSION} and later.
|
|
|
|
After cleanup, rerun this installer or follow the installation guide:
|
|
|
|
https://docs.nvidia.com/openshell/latest/about/installation
|
|
|
|
If you have already backed up and cleaned up the old runtime, rerun with:
|
|
|
|
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | OPENSHELL_ACK_BREAKING_UPGRADE=1 sh
|
|
|
|
EOF
|
|
}
|
|
|
|
guard_breaking_upgrade() {
|
|
target_uses_breaking_gateway_model || return 0
|
|
|
|
_bin="$(find_existing_native_openshell_bin || true)"
|
|
[ -n "$_bin" ] || return 0
|
|
|
|
_version="$(existing_openshell_version "$_bin")"
|
|
installed_version_needs_breaking_upgrade_notice "$_version" || return 0
|
|
|
|
print_breaking_upgrade_notice "$_bin" "$_version"
|
|
|
|
if [ "$UPGRADE_NOTICE_ACK" = "1" ]; then
|
|
warn "continuing because OPENSHELL_ACK_BREAKING_UPGRADE=1 is set"
|
|
return 0
|
|
fi
|
|
|
|
error "manual cleanup is required before upgrading from this OpenShell installation"
|
|
}
|
|
|
|
print_native_to_snap_notice() {
|
|
_bin="$1"
|
|
_version="$2"
|
|
|
|
if [ -n "$_version" ]; then
|
|
warn "detected existing non-snap OpenShell ${_version} at ${_bin}"
|
|
else
|
|
warn "detected an existing non-snap OpenShell installation at ${_bin}"
|
|
fi
|
|
|
|
cat >&2 <<EOF
|
|
|
|
The OpenShell snap keeps gateway and CLI state in snap-specific directories and
|
|
does not import state from a non-snap installation. Before installing the snap,
|
|
back up anything you need and clean up sandboxes and runtime resources managed
|
|
by the existing installation.
|
|
|
|
For older installations that provide these commands, run:
|
|
|
|
${_bin} sandbox delete --all
|
|
${_bin} gateway destroy
|
|
|
|
Stop the non-snap gateway service and remove the native package or manual
|
|
installation before continuing. For package and service instructions, see:
|
|
|
|
https://docs.nvidia.com/openshell/latest/about/installation
|
|
|
|
If you have already backed up and cleaned up the non-snap installation, rerun with:
|
|
|
|
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | OPENSHELL_ACK_BREAKING_UPGRADE=1 sh
|
|
|
|
EOF
|
|
}
|
|
|
|
guard_native_to_snap_transition() {
|
|
_bin="$(find_existing_native_openshell_bin || true)"
|
|
[ -n "$_bin" ] || return 0
|
|
|
|
_version="$(existing_openshell_version "$_bin")"
|
|
print_native_to_snap_notice "$_bin" "$_version"
|
|
|
|
if [ "$UPGRADE_NOTICE_ACK" = "1" ]; then
|
|
warn "continuing because OPENSHELL_ACK_BREAKING_UPGRADE=1 is set"
|
|
return 0
|
|
fi
|
|
|
|
error "manual cleanup is required before replacing this non-snap OpenShell installation"
|
|
}
|
|
|
|
resolve_release_tag() {
|
|
if [ "${OPENSHELL_VERSION:-}" = "pre" ]; then
|
|
resolve_latest_prerelease_tag
|
|
return 0
|
|
fi
|
|
|
|
if [ -n "${OPENSHELL_VERSION:-}" ]; then
|
|
echo "$OPENSHELL_VERSION"
|
|
return 0
|
|
fi
|
|
|
|
info "resolving latest version..."
|
|
_latest_url="${GITHUB_URL}/releases/latest"
|
|
_resolved="$(curl -fLsS -o /dev/null -w '%{url_effective}' "$_latest_url")" || {
|
|
error "failed to resolve latest release from ${_latest_url}"
|
|
}
|
|
|
|
case "$_resolved" in
|
|
https://github.com/${REPO}/releases/*)
|
|
;;
|
|
*)
|
|
error "unexpected redirect target: ${_resolved} (expected https://github.com/${REPO}/releases/...)"
|
|
;;
|
|
esac
|
|
|
|
_version="${_resolved##*/}"
|
|
if [ -z "$_version" ] || [ "$_version" = "latest" ]; then
|
|
error "could not determine latest release version (resolved URL: ${_resolved})"
|
|
fi
|
|
|
|
echo "$_version"
|
|
}
|
|
|
|
resolve_latest_prerelease_tag() {
|
|
require_prerelease_github_access
|
|
|
|
info "resolving latest prerelease..."
|
|
_artifact_platform="$(prerelease_artifact_platform)"
|
|
_release_tags="$(gh api "repos/${REPO}/git/matching-refs/tags/v" --jq '
|
|
[.[].ref | sub("^refs/tags/"; "") |
|
|
select(test("^v[0-9]+\\.[0-9]+\\.[0-9]+-pre\\.[1-9][0-9]*$"))] |
|
|
sort_by(split("-pre.") as $parts |
|
|
($parts[0] | ltrimstr("v") | split(".") | map(tonumber)) +
|
|
[($parts[1] | tonumber)]) | reverse | .[]')" || {
|
|
error "failed to list prerelease tags"
|
|
}
|
|
|
|
for _tag in $_release_tags; do
|
|
_artifact_name="openshell-${_tag}-${_artifact_platform}"
|
|
info "checking ${_tag} for ${_artifact_platform}..."
|
|
_run_ids="$(gh api \
|
|
"repos/${REPO}/actions/artifacts?name=${_artifact_name}&per_page=100" \
|
|
--jq '[.artifacts[] | select(.expired == false)] |
|
|
sort_by(.created_at) | reverse | .[] | .workflow_run.id')" || {
|
|
error "failed to find prerelease artifact ${_artifact_name}"
|
|
}
|
|
|
|
for _run_id in $_run_ids; do
|
|
_successful="$(gh api "repos/${REPO}/actions/runs/${_run_id}" --jq '
|
|
.status == "completed" and .conclusion == "success" and
|
|
(.path | startswith(".github/workflows/release-tag.yml"))')" || {
|
|
error "failed to check prerelease workflow run ${_run_id}"
|
|
}
|
|
if [ "$_successful" = "true" ]; then
|
|
printf '%s\n' "$_tag"
|
|
return 0
|
|
fi
|
|
done
|
|
done
|
|
|
|
error "no unexpired prerelease artifacts found"
|
|
}
|
|
|
|
is_prerelease_tag() {
|
|
printf '%s\n' "$1" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+-pre\.[1-9][0-9]*$'
|
|
}
|
|
|
|
require_prerelease_github_access() {
|
|
require_cmd gh
|
|
if ! gh auth status --hostname github.com >/dev/null 2>&1; then
|
|
error "GitHub CLI authentication is required for prerelease artifacts; run 'gh auth login' and try again"
|
|
fi
|
|
}
|
|
|
|
prerelease_artifact_platform() {
|
|
case "${PLATFORM}:$(uname -m)" in
|
|
linux:x86_64 | linux:amd64)
|
|
case "$(linux_package_method)" in
|
|
deb) echo "linux-amd64-deb" ;;
|
|
rpm) echo "linux-x86_64-rpm" ;;
|
|
esac
|
|
;;
|
|
linux:aarch64 | linux:arm64)
|
|
case "$(linux_package_method)" in
|
|
deb) echo "linux-arm64-deb" ;;
|
|
rpm) echo "linux-aarch64-rpm" ;;
|
|
esac
|
|
;;
|
|
darwin:arm64 | darwin:aarch64) echo "macos-arm64" ;;
|
|
*) error "no prerelease artifact is available for ${PLATFORM}/$(uname -m)" ;;
|
|
esac
|
|
}
|
|
|
|
prepare_prerelease_assets() {
|
|
_destination="$1"
|
|
is_prerelease_tag "$RELEASE_TAG" || return 0
|
|
require_prerelease_github_access
|
|
|
|
_artifact_name="openshell-${RELEASE_TAG}-$(prerelease_artifact_platform)"
|
|
info "locating ${_artifact_name}..."
|
|
_run_id="$(gh api \
|
|
"repos/${REPO}/actions/artifacts?name=${_artifact_name}&per_page=100" \
|
|
--jq '[.artifacts[] | select(.expired == false)] | sort_by(.created_at) | last | .workflow_run.id')" || {
|
|
error "failed to find prerelease artifact ${_artifact_name}"
|
|
}
|
|
if [ -z "$_run_id" ] || [ "$_run_id" = "null" ] || ! printf '%s\n' "$_run_id" | grep -Eq '^[0-9]+$'; then
|
|
error "no unexpired prerelease artifact found for ${RELEASE_TAG} on this platform"
|
|
fi
|
|
|
|
RELEASE_ASSET_DIR="${_destination}/release"
|
|
info "downloading ${_artifact_name}..."
|
|
gh run download "$_run_id" \
|
|
--repo "$REPO" \
|
|
--name "$_artifact_name" \
|
|
--dir "$RELEASE_ASSET_DIR" || {
|
|
error "failed to download prerelease artifact ${_artifact_name}"
|
|
}
|
|
}
|
|
|
|
download_release_asset() {
|
|
_tag="$1"
|
|
_filename="$2"
|
|
_output="$3"
|
|
|
|
if [ -n "$RELEASE_ASSET_DIR" ]; then
|
|
_source="${RELEASE_ASSET_DIR}/${_filename}"
|
|
if [ -f "$_source" ]; then
|
|
cp "$_source" "$_output"
|
|
return 0
|
|
fi
|
|
return 1
|
|
fi
|
|
|
|
if curl -fLs --retry 3 --max-redirs 5 -o "$_output" \
|
|
"${GITHUB_URL}/releases/download/${_tag}/${_filename}"; then
|
|
return 0
|
|
fi
|
|
|
|
# GitHub normalizes `~` to `.` in release asset names, while checksum files
|
|
# can still record package filenames with `~dev` for correct version ordering.
|
|
# Download the normalized asset but verify it against the checksum entry for
|
|
# the original package filename.
|
|
_normalized="$(printf '%s' "$_filename" | tr '~' '.')"
|
|
if [ "$_normalized" != "$_filename" ]; then
|
|
if download "${GITHUB_URL}/releases/download/${_tag}/${_normalized}" "$_output"; then
|
|
info "using GitHub-normalized asset name ${_normalized}"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
as_root() {
|
|
if [ "$(id -u)" -eq 0 ]; then
|
|
"$@"
|
|
elif has_cmd sudo; then
|
|
sudo "$@"
|
|
else
|
|
error "this installer needs root privileges; rerun as root or install sudo"
|
|
fi
|
|
}
|
|
|
|
target_user() {
|
|
if [ "$(id -u)" -eq 0 ] && [ -n "${SUDO_USER:-}" ] && [ "${SUDO_USER}" != "root" ]; then
|
|
echo "$SUDO_USER"
|
|
else
|
|
id -un
|
|
fi
|
|
}
|
|
|
|
user_home() {
|
|
_user="$1"
|
|
if has_cmd getent; then
|
|
_home="$(getent passwd "$_user" | awk -F: '{ print $6 }')"
|
|
if [ -n "$_home" ]; then
|
|
echo "$_home"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
if [ "$(uname -s)" = "Darwin" ] && has_cmd dscl; then
|
|
_home="$(dscl . -read "/Users/${_user}" NFSHomeDirectory 2>/dev/null | awk '{ print $2 }')"
|
|
if [ -n "$_home" ]; then
|
|
echo "$_home"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
if [ "$(id -un)" = "$_user" ]; then
|
|
echo "${HOME:-}"
|
|
return 0
|
|
fi
|
|
|
|
if [ "$(uname -s)" = "Darwin" ]; then
|
|
echo "/Users/${_user}"
|
|
return 0
|
|
fi
|
|
|
|
echo "/home/${_user}"
|
|
}
|
|
|
|
as_target_user() {
|
|
if [ "${PLATFORM:-}" = "darwin" ]; then
|
|
if [ "$(id -u)" -eq "$TARGET_UID" ]; then
|
|
env HOME="$TARGET_HOME" "$@"
|
|
elif has_cmd sudo; then
|
|
sudo -u "$TARGET_USER" env HOME="$TARGET_HOME" "$@"
|
|
else
|
|
error "cannot run commands as ${TARGET_USER}; install sudo or run as ${TARGET_USER}"
|
|
fi
|
|
return
|
|
fi
|
|
|
|
_bus="unix:path=${TARGET_RUNTIME_DIR}/bus"
|
|
if [ "$(id -u)" -eq "$TARGET_UID" ]; then
|
|
env HOME="$TARGET_HOME" XDG_RUNTIME_DIR="$TARGET_RUNTIME_DIR" DBUS_SESSION_BUS_ADDRESS="$_bus" "$@"
|
|
elif has_cmd sudo; then
|
|
sudo -u "$TARGET_USER" env HOME="$TARGET_HOME" XDG_RUNTIME_DIR="$TARGET_RUNTIME_DIR" DBUS_SESSION_BUS_ADDRESS="$_bus" "$@"
|
|
elif has_cmd runuser; then
|
|
runuser -u "$TARGET_USER" -- env HOME="$TARGET_HOME" XDG_RUNTIME_DIR="$TARGET_RUNTIME_DIR" DBUS_SESSION_BUS_ADDRESS="$_bus" "$@"
|
|
else
|
|
error "cannot run user service commands as ${TARGET_USER}; install sudo or run as ${TARGET_USER}"
|
|
fi
|
|
}
|
|
|
|
detect_platform() {
|
|
case "$(uname -s)" in
|
|
Linux)
|
|
echo "linux"
|
|
;;
|
|
Darwin)
|
|
echo "darwin"
|
|
;;
|
|
*)
|
|
error "unsupported OS: $(uname -s); this installer supports Linux and macOS"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
local_gateway_endpoint() {
|
|
case "${PLATFORM:-$(detect_platform)}" in
|
|
darwin)
|
|
printf 'https://localhost:%s\n' "$LOCAL_GATEWAY_PORT"
|
|
;;
|
|
*)
|
|
printf 'https://127.0.0.1:%s\n' "$LOCAL_GATEWAY_PORT"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
linux_package_method() {
|
|
case "${OPENSHELL_INSTALL_METHOD:-}" in
|
|
snap | deb | rpm)
|
|
echo "$OPENSHELL_INSTALL_METHOD"
|
|
return 0
|
|
;;
|
|
'') ;;
|
|
*) error "unsupported OPENSHELL_INSTALL_METHOD=${OPENSHELL_INSTALL_METHOD}; use snap, deb, or rpm" ;;
|
|
esac
|
|
|
|
# Keep refreshing an existing snap install instead of adding a second
|
|
# gateway on the same port.
|
|
case "${OPENSHELL_VERSION:-}" in
|
|
'' | dev)
|
|
if has_cmd snap && snap list openshell >/dev/null 2>&1; then
|
|
echo "snap"
|
|
return 0
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
if has_cmd dpkg; then
|
|
echo "deb"
|
|
elif has_cmd rpm; then
|
|
echo "rpm"
|
|
else
|
|
error "Linux installs require either dpkg or rpm"
|
|
fi
|
|
}
|
|
|
|
set_linux_target_runtime_dir() {
|
|
if [ "$(id -u)" -eq "$TARGET_UID" ] && [ -n "${XDG_RUNTIME_DIR:-}" ]; then
|
|
TARGET_RUNTIME_DIR="$XDG_RUNTIME_DIR"
|
|
else
|
|
TARGET_RUNTIME_DIR="/run/user/${TARGET_UID}"
|
|
fi
|
|
}
|
|
|
|
check_linux_deb_platform() {
|
|
require_cmd dpkg
|
|
}
|
|
|
|
check_macos_platform() {
|
|
_arch="$(uname -m)"
|
|
|
|
case "$_arch" in
|
|
arm64|aarch64)
|
|
;;
|
|
x86_64|amd64)
|
|
error "Intel macOS is not supported because no x86_64-apple-darwin release assets are published"
|
|
;;
|
|
*)
|
|
error "no macOS release build is published for architecture: ${_arch}"
|
|
;;
|
|
esac
|
|
|
|
if ! as_target_user brew --version >/dev/null 2>&1; then
|
|
error "Homebrew is required for macOS installs; install it from https://brew.sh"
|
|
fi
|
|
}
|
|
|
|
get_deb_arch() {
|
|
_arch="$(dpkg --print-architecture)"
|
|
|
|
case "$_arch" in
|
|
amd64|arm64)
|
|
echo "$_arch"
|
|
;;
|
|
*)
|
|
error "no Debian package is published for architecture: ${_arch}"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
get_rpm_arch() {
|
|
if has_cmd rpm; then
|
|
_arch="$(rpm --eval '%{_arch}' 2>/dev/null || true)"
|
|
else
|
|
_arch=""
|
|
fi
|
|
|
|
if [ -z "$_arch" ]; then
|
|
_arch="$(uname -m)"
|
|
fi
|
|
|
|
case "$_arch" in
|
|
x86_64|amd64)
|
|
echo "x86_64"
|
|
;;
|
|
aarch64|arm64)
|
|
echo "aarch64"
|
|
;;
|
|
*)
|
|
error "no RPM package is published for architecture: ${_arch}"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
find_deb_asset() {
|
|
_checksums="$1"
|
|
_arch="$2"
|
|
|
|
awk -v arch="$_arch" '
|
|
$2 ~ "^\\*?openshell[-_].*[-_]" arch "\\.deb$" {
|
|
sub("^\\*", "", $2)
|
|
print $2
|
|
exit
|
|
}
|
|
' "$_checksums"
|
|
}
|
|
|
|
find_rpm_asset() {
|
|
_checksums="$1"
|
|
_arch="$2"
|
|
_package="$3"
|
|
|
|
case "$_package" in
|
|
openshell)
|
|
_dev_name="openshell-dev-${_arch}.rpm"
|
|
_fallback_re="^openshell-[0-9].*\\.${_arch}\\.rpm$"
|
|
;;
|
|
openshell-gateway)
|
|
_dev_name="openshell-gateway-dev-${_arch}.rpm"
|
|
_fallback_re="^openshell-gateway-[0-9].*\\.${_arch}\\.rpm$"
|
|
;;
|
|
openshell-prover)
|
|
_dev_name="openshell-prover-dev-${_arch}.rpm"
|
|
_fallback_re="^openshell-prover-[0-9].*\\.${_arch}\\.rpm$"
|
|
;;
|
|
*)
|
|
error "unknown RPM package selector: ${_package}"
|
|
;;
|
|
esac
|
|
|
|
awk -v dev_name="$_dev_name" -v fallback_re="$_fallback_re" '
|
|
{
|
|
name = $2
|
|
sub("^\\*", "", name)
|
|
|
|
if (name == dev_name) {
|
|
selected = name
|
|
found = 1
|
|
exit
|
|
}
|
|
|
|
if (fallback == "" && name ~ fallback_re) {
|
|
fallback = name
|
|
}
|
|
}
|
|
END {
|
|
if (found) {
|
|
print selected
|
|
} else if (fallback != "") {
|
|
print fallback
|
|
}
|
|
}
|
|
' "$_checksums"
|
|
}
|
|
|
|
verify_checksum() {
|
|
_archive="$1"
|
|
_checksums="$2"
|
|
_filename="$3"
|
|
|
|
if has_cmd sha256sum; then
|
|
_expected="$(awk -v name="$_filename" '($2 == name || $2 == "*" name) { print $1; exit }' "$_checksums")"
|
|
[ -n "$_expected" ] || error "no checksum entry found for ${_filename}"
|
|
echo "$_expected $_archive" | sha256sum -c --quiet
|
|
elif has_cmd shasum; then
|
|
_expected="$(awk -v name="$_filename" '($2 == name || $2 == "*" name) { print $1; exit }' "$_checksums")"
|
|
[ -n "$_expected" ] || error "no checksum entry found for ${_filename}"
|
|
echo "$_expected $_archive" | shasum -a 256 -c --quiet
|
|
else
|
|
error "neither 'sha256sum' nor 'shasum' found; cannot verify download integrity"
|
|
fi
|
|
}
|
|
|
|
install_deb_package() {
|
|
_deb_path="$1"
|
|
|
|
if has_cmd apt-get; then
|
|
as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
|
-o Dpkg::Options::=--force-confdef \
|
|
-o Dpkg::Options::=--force-confnew \
|
|
"$_deb_path"
|
|
elif has_cmd apt; then
|
|
as_root env DEBIAN_FRONTEND=noninteractive apt install -y \
|
|
-o Dpkg::Options::=--force-confdef \
|
|
-o Dpkg::Options::=--force-confnew \
|
|
"$_deb_path"
|
|
else
|
|
as_root dpkg --force-confdef --force-confnew -i "$_deb_path"
|
|
fi
|
|
}
|
|
|
|
install_rpm_packages() {
|
|
if has_cmd dnf; then
|
|
as_root dnf install -y "$@"
|
|
elif has_cmd yum; then
|
|
as_root yum install -y "$@"
|
|
elif has_cmd zypper; then
|
|
as_root zypper --non-interactive install --allow-unsigned-rpm "$@"
|
|
elif has_cmd rpm; then
|
|
warn "installing with rpm directly; dependencies must already be installed"
|
|
as_root rpm -Uvh --replacepkgs "$@"
|
|
else
|
|
error "'dnf', 'yum', 'zypper', or 'rpm' is required to install RPM packages"
|
|
fi
|
|
}
|
|
|
|
homebrew_formula_path() {
|
|
_tap="$1"
|
|
_formula="$2"
|
|
|
|
if ! as_target_user brew tap-info "$_tap" >/dev/null 2>&1; then
|
|
info "creating local Homebrew tap ${_tap}..."
|
|
as_target_user brew tap-new --no-git "$_tap" >/dev/null
|
|
fi
|
|
|
|
_tap_dir="$(as_target_user brew --repository "$_tap" 2>/dev/null || true)"
|
|
[ -n "$_tap_dir" ] || error "could not locate Homebrew tap ${_tap}"
|
|
|
|
_formula_dir="${_tap_dir}/Formula"
|
|
as_target_user mkdir -p "$_formula_dir"
|
|
printf '%s/%s.rb\n' "$_formula_dir" "$_formula"
|
|
}
|
|
|
|
patch_homebrew_formula() {
|
|
_formula_file="$1"
|
|
_patched_file="${_formula_file}.patched"
|
|
|
|
if grep -q 'entitlements.write <<~XML' "$_formula_file"; then
|
|
info "patching Homebrew formula for idempotent postinstall..."
|
|
sed 's/entitlements\.write <<~XML/entitlements.atomic_write <<~XML/' "$_formula_file" >"$_patched_file"
|
|
mv "$_patched_file" "$_formula_file"
|
|
fi
|
|
|
|
}
|
|
|
|
patch_prerelease_homebrew_formula_urls() {
|
|
_formula_file="$1"
|
|
[ -n "$RELEASE_ASSET_DIR" ] || return 0
|
|
|
|
for _asset in "$HOMEBREW_CLI_ASSET" "$HOMEBREW_GATEWAY_ASSET" "$HOMEBREW_DRIVER_VM_ASSET" "$HOMEBREW_PROVER_ASSET"; do
|
|
if [ ! -f "${RELEASE_ASSET_DIR}/${_asset}" ]; then
|
|
error "prerelease artifact is missing the required macOS asset: ${_asset}"
|
|
fi
|
|
done
|
|
|
|
_release_asset_url="${GITHUB_URL}/releases/download/${RELEASE_TAG}"
|
|
_local_asset_url="file://${RELEASE_ASSET_DIR}"
|
|
_patched_file="${_formula_file}.prerelease"
|
|
sed \
|
|
-e "s#${_release_asset_url}/${HOMEBREW_CLI_ASSET}#${_local_asset_url}/${HOMEBREW_CLI_ASSET}#g" \
|
|
-e "s#${_release_asset_url}/${HOMEBREW_GATEWAY_ASSET}#${_local_asset_url}/${HOMEBREW_GATEWAY_ASSET}#g" \
|
|
-e "s#${_release_asset_url}/${HOMEBREW_DRIVER_VM_ASSET}#${_local_asset_url}/${HOMEBREW_DRIVER_VM_ASSET}#g" \
|
|
-e "s#${_release_asset_url}/${HOMEBREW_PROVER_ASSET}#${_local_asset_url}/${HOMEBREW_PROVER_ASSET}#g" \
|
|
"$_formula_file" >"$_patched_file"
|
|
mv "$_patched_file" "$_formula_file"
|
|
}
|
|
|
|
start_user_gateway() {
|
|
info "restarting openshell-gateway user service as ${TARGET_USER}..."
|
|
|
|
if ! as_target_user systemctl --user daemon-reload; then
|
|
info "could not reach the user systemd manager for ${TARGET_USER}"
|
|
info "restart the gateway later with: systemctl --user enable openshell-gateway && systemctl --user restart openshell-gateway"
|
|
info "then register it with: openshell gateway add https://127.0.0.1:17670 --local --name openshell"
|
|
return 0
|
|
fi
|
|
|
|
as_target_user systemctl --user enable openshell-gateway
|
|
as_target_user systemctl --user restart openshell-gateway
|
|
as_target_user systemctl --user is-active --quiet openshell-gateway
|
|
|
|
info "registering local gateway as ${TARGET_USER}..."
|
|
register_local_gateway
|
|
wait_for_local_gateway_listener
|
|
wait_for_local_gateway_status
|
|
}
|
|
|
|
dump_local_gateway_diagnostics() {
|
|
_lines="${OPENSHELL_INSTALL_LOG_LINES:-80}"
|
|
case "$_lines" in
|
|
"" | *[!0-9]*)
|
|
_lines=80
|
|
;;
|
|
esac
|
|
|
|
info "dumping recent local gateway diagnostics..."
|
|
case "${PLATFORM:-}" in
|
|
darwin)
|
|
dump_homebrew_gateway_diagnostics "$_lines"
|
|
;;
|
|
linux)
|
|
if [ "${LINUX_INSTALL_METHOD:-}" = "snap" ]; then
|
|
dump_snap_gateway_diagnostics "$_lines"
|
|
else
|
|
dump_user_service_gateway_diagnostics "$_lines"
|
|
fi
|
|
;;
|
|
*)
|
|
info "no gateway log collector is available for platform: ${PLATFORM:-unknown}"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
dump_snap_gateway_diagnostics() {
|
|
_lines="$1"
|
|
|
|
info "OpenShell snap service status:"
|
|
as_root snap services openshell >&2 || true
|
|
info "OpenShell snap connections:"
|
|
as_root snap connections openshell >&2 || true
|
|
if has_cmd journalctl; then
|
|
info "last ${_lines} lines from the OpenShell snap gateway journal:"
|
|
as_root journalctl -b -u snap.openshell.gateway.service --no-pager -n "$_lines" >&2 || true
|
|
fi
|
|
as_root snap logs openshell.gateway -n="$_lines" >&2 || true
|
|
}
|
|
|
|
dump_homebrew_gateway_diagnostics() {
|
|
_lines="$1"
|
|
_brew_prefix="$(as_target_user brew --prefix 2>/dev/null || true)"
|
|
[ -n "$_brew_prefix" ] || _brew_prefix="/opt/homebrew"
|
|
|
|
info "Homebrew service status:"
|
|
as_target_user brew services info "${HOMEBREW_TAP}/${HOMEBREW_FORMULA_NAME}" >&2 || true
|
|
|
|
for _log_file in \
|
|
"${_brew_prefix}/var/log/openshell/openshell-gateway.err.log" \
|
|
"${_brew_prefix}/var/log/openshell/openshell-gateway.out.log"; do
|
|
if [ -f "$_log_file" ]; then
|
|
info "last ${_lines} lines from ${_log_file}:"
|
|
tail -n "$_lines" "$_log_file" >&2 || true
|
|
else
|
|
info "gateway log not found: ${_log_file}"
|
|
fi
|
|
done
|
|
}
|
|
|
|
dump_user_service_gateway_diagnostics() {
|
|
_lines="$1"
|
|
|
|
if has_cmd systemctl; then
|
|
info "openshell-gateway user service status:"
|
|
as_target_user systemctl --user status openshell-gateway --no-pager >&2 || true
|
|
fi
|
|
|
|
if has_cmd journalctl; then
|
|
info "last ${_lines} lines from openshell-gateway user journal:"
|
|
as_target_user journalctl --user -u openshell-gateway --no-pager -n "$_lines" >&2 || true
|
|
else
|
|
info "journalctl not found; cannot dump openshell-gateway user journal"
|
|
fi
|
|
}
|
|
|
|
wait_for_local_gateway_listener() {
|
|
_timeout="${OPENSHELL_INSTALL_GATEWAY_TIMEOUT:-30}"
|
|
_elapsed=0
|
|
_last_output=""
|
|
_probe_url="$(local_gateway_endpoint)/"
|
|
_mtls_dir="${TARGET_HOME}/.config/openshell/gateways/openshell/mtls"
|
|
|
|
info "waiting for local gateway listener to become reachable..."
|
|
while [ "$_elapsed" -lt "$_timeout" ]; do
|
|
if [ ! -f "${_mtls_dir}/ca.crt" ] || [ ! -f "${_mtls_dir}/tls.crt" ] || [ ! -f "${_mtls_dir}/tls.key" ]; then
|
|
_last_output="mTLS client bundle is not ready under ${_mtls_dir}"
|
|
elif _last_output="$(as_target_user curl -sS --max-time 2 --cacert "${_mtls_dir}/ca.crt" --cert "${_mtls_dir}/tls.crt" --key "${_mtls_dir}/tls.key" -o /dev/null "$_probe_url" 2>&1)"; then
|
|
info "local gateway listener is reachable"
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
_elapsed=$((_elapsed + 1))
|
|
done
|
|
|
|
[ -z "$_last_output" ] || printf '%s\n' "$_last_output" >&2
|
|
dump_local_gateway_diagnostics
|
|
error "local gateway listener did not become reachable at ${_probe_url} within ${_timeout}s"
|
|
}
|
|
|
|
wait_for_local_gateway_status() {
|
|
_timeout="${OPENSHELL_INSTALL_GATEWAY_TIMEOUT:-30}"
|
|
_elapsed=0
|
|
_status_output=""
|
|
_register_bin="${OPENSHELL_REGISTER_BIN:-openshell}"
|
|
|
|
info "waiting for openshell status to report connected..."
|
|
while [ "$_elapsed" -lt "$_timeout" ]; do
|
|
if _status_output="$(as_target_user env NO_COLOR=1 "$_register_bin" status 2>&1)"; then
|
|
case "$_status_output" in
|
|
*"Version:"*)
|
|
info "openshell status reports connected"
|
|
return 0
|
|
;;
|
|
esac
|
|
fi
|
|
sleep 1
|
|
_elapsed=$((_elapsed + 1))
|
|
done
|
|
|
|
[ -z "$_status_output" ] || printf '%s\n' "$_status_output" >&2
|
|
dump_local_gateway_diagnostics
|
|
error "openshell status did not report connected within ${_timeout}s"
|
|
}
|
|
|
|
remove_local_gateway_registration_from() {
|
|
_config_dir="$1"
|
|
[ -n "$TARGET_HOME" ] || error "cannot resolve home directory for ${TARGET_USER}"
|
|
|
|
# Replace the CLI registration directly instead of asking `gateway destroy`
|
|
# to tear down package-managed resources.
|
|
# shellcheck disable=SC2016
|
|
as_target_user sh -c '
|
|
config_dir=$1
|
|
rm -rf "${config_dir}/gateways/local"
|
|
mkdir -p "${config_dir}/gateways/openshell"
|
|
rm -f \
|
|
"${config_dir}/gateways/openshell/metadata.json" \
|
|
"${config_dir}/gateways/openshell/edge_token" \
|
|
"${config_dir}/gateways/openshell/cf_token" \
|
|
"${config_dir}/gateways/openshell/oidc_token.json"
|
|
active="${config_dir}/active_gateway"
|
|
active_name="$(cat "$active" 2>/dev/null || true)"
|
|
if [ "$active_name" = "local" ] || [ "$active_name" = "openshell" ]; then
|
|
rm -f "$active"
|
|
fi
|
|
' sh "$_config_dir"
|
|
}
|
|
|
|
remove_local_gateway_registration() {
|
|
remove_local_gateway_registration_from "${TARGET_HOME}/.config/openshell"
|
|
}
|
|
|
|
remove_snap_gateway_registration() {
|
|
remove_local_gateway_registration_from \
|
|
"${TARGET_HOME}/snap/openshell/common/.config/openshell"
|
|
}
|
|
|
|
register_local_gateway() {
|
|
_register_bin="${OPENSHELL_REGISTER_BIN:-openshell}"
|
|
_endpoint="$(local_gateway_endpoint)"
|
|
|
|
if _add_output="$(as_target_user "$_register_bin" gateway add "$_endpoint" --local --name openshell 2>&1)"; then
|
|
[ -z "$_add_output" ] || print_gateway_add_output "$_add_output"
|
|
return 0
|
|
else
|
|
_add_status=$?
|
|
fi
|
|
|
|
case "$_add_output" in
|
|
*"already exists"*)
|
|
info "local gateway already exists; removing and re-adding it..."
|
|
remove_local_gateway_registration
|
|
as_target_user "$_register_bin" gateway add "$_endpoint" --local --name openshell
|
|
;;
|
|
*)
|
|
printf '%s\n' "$_add_output" >&2
|
|
return "$_add_status"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
print_gateway_add_output() {
|
|
_endpoint="$(local_gateway_endpoint)"
|
|
printf '%s\n' "$1" | while IFS= read -r _line; do
|
|
case "$_line" in
|
|
*"Gateway is not reachable at ${_endpoint}"*) ;;
|
|
*"Verify the gateway is running and the endpoint is correct."*) ;;
|
|
*) printf '%s\n' "$_line" >&2 ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
install_linux_deb() {
|
|
check_linux_deb_platform
|
|
set_linux_target_runtime_dir
|
|
|
|
_arch="$(get_deb_arch)"
|
|
_tmpdir="$(mktemp -d)"
|
|
chmod 0755 "$_tmpdir"
|
|
trap 'rm -rf "$_tmpdir"' EXIT
|
|
prepare_prerelease_assets "$_tmpdir"
|
|
info "downloading ${RELEASE_TAG} release checksums..."
|
|
download_release_asset "$RELEASE_TAG" "$CHECKSUMS_NAME" "${_tmpdir}/${CHECKSUMS_NAME}" || {
|
|
error "failed to download ${CHECKSUMS_NAME} for ${RELEASE_TAG}"
|
|
}
|
|
|
|
_deb_file="$(find_deb_asset "${_tmpdir}/${CHECKSUMS_NAME}" "$_arch")"
|
|
if [ -z "$_deb_file" ]; then
|
|
error "no Debian package found for architecture: ${_arch}"
|
|
fi
|
|
|
|
_deb_path="${_tmpdir}/${_deb_file}"
|
|
|
|
info "selected ${_deb_file}"
|
|
|
|
info "downloading ${_deb_file}..."
|
|
download_release_asset "$RELEASE_TAG" "$_deb_file" "$_deb_path" || {
|
|
error "failed to download ${_deb_file} for ${RELEASE_TAG}"
|
|
}
|
|
chmod 0644 "$_deb_path"
|
|
|
|
info "verifying checksum..."
|
|
verify_checksum "$_deb_path" "${_tmpdir}/${CHECKSUMS_NAME}" "$_deb_file"
|
|
|
|
info "installing ${_deb_file}..."
|
|
install_deb_package "$_deb_path"
|
|
info "installed ${APP_NAME} package from ${RELEASE_TAG}"
|
|
start_user_gateway
|
|
}
|
|
|
|
install_linux_rpm() {
|
|
require_cmd rpm
|
|
set_linux_target_runtime_dir
|
|
|
|
_arch="$(get_rpm_arch)"
|
|
_tmpdir="$(mktemp -d)"
|
|
chmod 0755 "$_tmpdir"
|
|
trap 'rm -rf "$_tmpdir"' EXIT
|
|
prepare_prerelease_assets "$_tmpdir"
|
|
info "downloading ${RELEASE_TAG} release checksums..."
|
|
download_release_asset "$RELEASE_TAG" "$CHECKSUMS_NAME" "${_tmpdir}/${CHECKSUMS_NAME}" || {
|
|
error "failed to download ${CHECKSUMS_NAME} for ${RELEASE_TAG}"
|
|
}
|
|
|
|
_rpm_file="$(find_rpm_asset "${_tmpdir}/${CHECKSUMS_NAME}" "$_arch" openshell)"
|
|
if [ -z "$_rpm_file" ]; then
|
|
error "no openshell RPM package found for architecture: ${_arch}"
|
|
fi
|
|
|
|
_gateway_rpm_file="$(find_rpm_asset "${_tmpdir}/${CHECKSUMS_NAME}" "$_arch" openshell-gateway)"
|
|
if [ -z "$_gateway_rpm_file" ]; then
|
|
error "no openshell-gateway RPM package found for architecture: ${_arch}"
|
|
fi
|
|
|
|
_prover_rpm_file="$(find_rpm_asset "${_tmpdir}/${CHECKSUMS_NAME}" "$_arch" openshell-prover)"
|
|
if [ -z "$_prover_rpm_file" ]; then
|
|
error "no openshell-prover RPM package found for architecture: ${_arch}"
|
|
fi
|
|
|
|
info "selected ${_rpm_file}, ${_gateway_rpm_file}, and ${_prover_rpm_file}"
|
|
|
|
for _package_file in "$_rpm_file" "$_gateway_rpm_file" "$_prover_rpm_file"; do
|
|
_package_url="${GITHUB_URL}/releases/download/${RELEASE_TAG}/${_package_file}"
|
|
_package_path="${_tmpdir}/${_package_file}"
|
|
|
|
info "downloading ${_package_file}..."
|
|
download_release_asset "$RELEASE_TAG" "$_package_file" "$_package_path" || {
|
|
error "failed to download ${_package_file} for ${RELEASE_TAG}"
|
|
}
|
|
chmod 0644 "$_package_path"
|
|
|
|
info "verifying checksum for ${_package_file}..."
|
|
verify_checksum "$_package_path" "${_tmpdir}/${CHECKSUMS_NAME}" "$_package_file"
|
|
done
|
|
|
|
info "installing ${_rpm_file}, ${_gateway_rpm_file}, and ${_prover_rpm_file}..."
|
|
install_rpm_packages \
|
|
"${_tmpdir}/${_rpm_file}" \
|
|
"${_tmpdir}/${_gateway_rpm_file}" \
|
|
"${_tmpdir}/${_prover_rpm_file}"
|
|
info "installed ${APP_NAME} RPM packages from ${RELEASE_TAG}"
|
|
start_user_gateway
|
|
}
|
|
|
|
openshell_snap_channel() {
|
|
case "${OPENSHELL_VERSION:-}" in
|
|
dev) printf '%s\n' "latest/edge" ;;
|
|
'') printf '%s\n' "latest/stable" ;;
|
|
*) error "Snap installs do not support OPENSHELL_VERSION=${OPENSHELL_VERSION}; use a native package" ;;
|
|
esac
|
|
}
|
|
|
|
wait_for_docker_daemon() {
|
|
_timeout="${OPENSHELL_INSTALL_DOCKER_TIMEOUT:-30}"
|
|
_elapsed=0
|
|
_last_output=""
|
|
|
|
info "waiting for Docker daemon to become reachable..."
|
|
while [ "$_elapsed" -lt "$_timeout" ]; do
|
|
if _last_output="$(as_root docker info 2>&1)"; then
|
|
info "Docker daemon is reachable"
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
_elapsed=$((_elapsed + 1))
|
|
done
|
|
|
|
[ -z "$_last_output" ] || printf '%s\n' "$_last_output" >&2
|
|
if snap list docker >/dev/null 2>&1; then
|
|
as_root snap services docker >&2 || true
|
|
as_root snap changes >&2 || true
|
|
fi
|
|
error "Docker daemon did not become reachable within ${_timeout}s"
|
|
}
|
|
|
|
# Copy the snap gateway's client bundle into the target user's snap state
|
|
# directory, where `openshell gateway add --local` imports it. Root only reads
|
|
# the source files; the target user writes the copies into their own home.
|
|
copy_snap_client_bundle() {
|
|
_src="${OPENSHELL_SNAP_TLS_DIR:-/var/snap/openshell/common/tls}"
|
|
_dst="${TARGET_HOME}/snap/openshell/common/.local/state/openshell/tls"
|
|
|
|
as_target_user mkdir -p "${_dst}/client"
|
|
as_target_user chmod 700 "$_dst" "${_dst}/client"
|
|
for _file in ca.crt client/tls.crt client/tls.key; do
|
|
as_root cat "${_src}/${_file}" |
|
|
as_target_user sh -c 'umask 077; cat >"$1"' sh "${_dst}/${_file}"
|
|
as_target_user chmod 600 "${_dst}/${_file}"
|
|
done
|
|
}
|
|
|
|
# Snap revisions that require mTLS ship the post-refresh hook that migrates
|
|
# older plaintext configs.
|
|
snap_gateway_uses_mtls() {
|
|
[ -e "${OPENSHELL_SNAP_DIR:-/snap/openshell/current}/meta/hooks/post-refresh" ]
|
|
}
|
|
|
|
register_snap_gateway() {
|
|
_register_bin="${OPENSHELL_REGISTER_BIN:-/snap/bin/openshell}"
|
|
|
|
if snap_gateway_uses_mtls; then
|
|
_endpoint="https://127.0.0.1:${LOCAL_GATEWAY_PORT}"
|
|
info "copying the gateway client certificate for ${TARGET_USER}..."
|
|
copy_snap_client_bundle
|
|
else
|
|
_endpoint="http://127.0.0.1:${LOCAL_GATEWAY_PORT}"
|
|
warn "this OpenShell snap revision serves plaintext HTTP without client authentication; any local user can operate the gateway"
|
|
fi
|
|
|
|
if _add_output="$(as_target_user "$_register_bin" gateway add "$_endpoint" --local --name openshell 2>&1)"; then
|
|
[ -z "$_add_output" ] || print_gateway_add_output "$_add_output"
|
|
return 0
|
|
else
|
|
_add_status=$?
|
|
fi
|
|
|
|
case "$_add_output" in
|
|
*"already exists"*)
|
|
info "local gateway already exists; removing and re-adding it..."
|
|
remove_snap_gateway_registration
|
|
as_target_user "$_register_bin" gateway add "$_endpoint" --local --name openshell
|
|
;;
|
|
*)
|
|
printf '%s\n' "$_add_output" >&2
|
|
return "$_add_status"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# The mTLS gateway rejects TLS handshakes without a client certificate, so
|
|
# probe it with the root-owned client bundle.
|
|
wait_for_snap_gateway_listener() {
|
|
_timeout="${OPENSHELL_INSTALL_GATEWAY_TIMEOUT:-30}"
|
|
_elapsed=0
|
|
_last_output=""
|
|
_tls_dir="${OPENSHELL_SNAP_TLS_DIR:-/var/snap/openshell/common/tls}"
|
|
|
|
if snap_gateway_uses_mtls; then
|
|
_probe_url="https://127.0.0.1:${LOCAL_GATEWAY_PORT}/"
|
|
_probe_as=as_root
|
|
set -- --cacert "${_tls_dir}/ca.crt" \
|
|
--cert "${_tls_dir}/client/tls.crt" --key "${_tls_dir}/client/tls.key"
|
|
else
|
|
_probe_url="http://127.0.0.1:${LOCAL_GATEWAY_PORT}/"
|
|
_probe_as=""
|
|
set --
|
|
fi
|
|
|
|
info "waiting for local gateway listener to become reachable..."
|
|
while [ "$_elapsed" -lt "$_timeout" ]; do
|
|
if _last_output="$($_probe_as curl -sS --max-time 2 "$@" -o /dev/null "$_probe_url" 2>&1)"; then
|
|
info "local gateway listener is reachable"
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
_elapsed=$((_elapsed + 1))
|
|
done
|
|
|
|
[ -z "$_last_output" ] || printf '%s\n' "$_last_output" >&2
|
|
dump_local_gateway_diagnostics
|
|
error "local gateway listener did not become reachable at ${_probe_url} within ${_timeout}s"
|
|
}
|
|
|
|
install_linux_snap() {
|
|
require_cmd snap
|
|
set_linux_target_runtime_dir
|
|
|
|
if snap list docker >/dev/null 2>&1; then
|
|
error "the Docker snap is not currently compatible with OpenShell because its AppArmor confinement prevents OpenShell's hardened containers from starting.
|
|
Remove the Docker snap and install Docker Engine from a system package or Docker's package repository, then rerun this installer."
|
|
fi
|
|
if ! has_cmd docker; then
|
|
error "Docker is required before installing the OpenShell snap.
|
|
Install Docker Engine from a system package or Docker's package repository, then rerun this installer. The Docker snap is not currently compatible with OpenShell."
|
|
fi
|
|
info "using existing Docker installation"
|
|
wait_for_docker_daemon
|
|
|
|
_channel="$(openshell_snap_channel)"
|
|
if snap list openshell >/dev/null 2>&1; then
|
|
info "refreshing OpenShell snap from ${_channel}..."
|
|
as_root snap refresh openshell --channel="$_channel"
|
|
warn "restarting the OpenShell gateway to use the refreshed snap; active sandbox sessions will be interrupted"
|
|
else
|
|
info "installing OpenShell snap from ${_channel}..."
|
|
as_root snap install openshell --channel="$_channel"
|
|
fi
|
|
|
|
as_root snap restart openshell.gateway
|
|
|
|
info "installed OpenShell snap from ${_channel}"
|
|
wait_for_snap_gateway_listener
|
|
info "registering local gateway as ${TARGET_USER}..."
|
|
register_snap_gateway
|
|
OPENSHELL_REGISTER_BIN="/snap/bin/openshell"
|
|
wait_for_local_gateway_status
|
|
}
|
|
|
|
install_macos_homebrew() {
|
|
check_macos_platform
|
|
|
|
_tmpdir="$(mktemp -d)"
|
|
chmod 0755 "$_tmpdir"
|
|
trap 'rm -rf "$_tmpdir"' EXIT
|
|
prepare_prerelease_assets "$_tmpdir"
|
|
_formula_file="${_tmpdir}/openshell.rb"
|
|
_formula_url="${GITHUB_URL}/releases/download/${RELEASE_TAG}/openshell.rb"
|
|
|
|
info "downloading Homebrew formula from ${_formula_url}..."
|
|
download_release_asset "$RELEASE_TAG" "openshell.rb" "$_formula_file" || {
|
|
error "failed to download ${_formula_url}; the selected release may not include a Homebrew formula"
|
|
}
|
|
chmod 0644 "$_formula_file"
|
|
patch_prerelease_homebrew_formula_urls "$_formula_file"
|
|
patch_homebrew_formula "$_formula_file"
|
|
|
|
_tap_formula_file="$(homebrew_formula_path "$HOMEBREW_TAP" "$HOMEBREW_FORMULA_NAME")"
|
|
info "staging Homebrew formula in tap ${HOMEBREW_TAP}..."
|
|
cp "$_formula_file" "$_tap_formula_file"
|
|
chmod 0644 "$_tap_formula_file"
|
|
if [ "$(id -u)" -eq 0 ]; then
|
|
chown "$TARGET_USER" "$_tap_formula_file" 2>/dev/null || true
|
|
fi
|
|
|
|
_formula_ref="${HOMEBREW_TAP}/${HOMEBREW_FORMULA_NAME}"
|
|
|
|
if as_target_user brew list --formula openshell >/dev/null 2>&1; then
|
|
info "reinstalling OpenShell with Homebrew..."
|
|
as_target_user brew reinstall --formula "$_formula_ref"
|
|
else
|
|
info "installing OpenShell with Homebrew..."
|
|
as_target_user brew install --formula "$_formula_ref"
|
|
fi
|
|
|
|
info "restarting OpenShell Homebrew service..."
|
|
if ! as_target_user brew services restart "$_formula_ref"; then
|
|
warn "could not restart the OpenShell Homebrew service"
|
|
info "restart it later with: brew services restart ${_formula_ref}"
|
|
info "then register it with: openshell gateway add $(local_gateway_endpoint) --local --name openshell"
|
|
return 0
|
|
fi
|
|
|
|
_brew_prefix="$(as_target_user brew --prefix 2>/dev/null || true)"
|
|
if [ -n "$_brew_prefix" ] && [ -x "${_brew_prefix}/bin/openshell" ]; then
|
|
OPENSHELL_REGISTER_BIN="${_brew_prefix}/bin/openshell"
|
|
fi
|
|
|
|
info "registering local gateway as ${TARGET_USER}..."
|
|
register_local_gateway
|
|
wait_for_local_gateway_listener
|
|
wait_for_local_gateway_status
|
|
}
|
|
|
|
main() {
|
|
if [ "$#" -gt 0 ]; then
|
|
case "$1" in
|
|
--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
error "unknown option: $1"
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
require_cmd curl
|
|
PLATFORM="$(detect_platform)"
|
|
if [ "$PLATFORM" = "linux" ]; then
|
|
LINUX_INSTALL_METHOD="$(linux_package_method)"
|
|
fi
|
|
|
|
TARGET_USER="$(target_user)"
|
|
TARGET_UID="$(id -u "$TARGET_USER" 2>/dev/null || true)"
|
|
[ -n "$TARGET_UID" ] || error "cannot resolve uid for ${TARGET_USER}"
|
|
TARGET_HOME="$(user_home "$TARGET_USER")"
|
|
|
|
if [ "${LINUX_INSTALL_METHOD:-}" = "snap" ]; then
|
|
guard_native_to_snap_transition
|
|
else
|
|
RELEASE_TAG="$(resolve_release_tag)"
|
|
guard_breaking_upgrade
|
|
fi
|
|
|
|
case "$PLATFORM" in
|
|
linux)
|
|
case "$LINUX_INSTALL_METHOD" in
|
|
snap)
|
|
install_linux_snap
|
|
;;
|
|
deb)
|
|
require_linux_package_glibc
|
|
install_linux_deb
|
|
;;
|
|
rpm)
|
|
require_linux_package_glibc
|
|
install_linux_rpm
|
|
;;
|
|
*)
|
|
error "unsupported Linux package method"
|
|
;;
|
|
esac
|
|
;;
|
|
darwin)
|
|
install_macos_homebrew
|
|
;;
|
|
*)
|
|
error "unsupported platform: ${PLATFORM}"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
if [ "${OPENSHELL_INSTALL_SH_TEST:-0}" != "1" ]; then
|
|
main "$@"
|
|
fi
|