From 576cf94e2b477dda86f3ecd5517fcdaf0dcf10ec Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Tue, 9 Jun 2026 16:43:39 +0200 Subject: [PATCH 1/7] feat(install): add snap install support with automatic Docker snap provisioning Add snap install path to install.sh so that on Linux systems with snapd available and no native Docker Engine installed, the installer chooses the snap path. Snap is preferred when 'snap' command exists and 'snapd.socket' is active. New functions: - has_snapd(): detects snapd availability via snap command + snapd.socket - has_native_docker(): pre-flight check for conflicting native Docker - install_linux_snap(): installs openshell snap, Docker snap if missing, connects interfaces (best-effort), registers gateway via HTTP, verifies status - register_local_gateway_snap(): gateway add with http:// URL (no mTLS) - wait_for_local_gateway_listener_snap(): waits for HTTP listener linux_package_method() returns 'snap' when snapd is available and no native docker, respecting OPENSHELL_INSTALL_METHOD=classic override. The installer handles the full flow: installing Docker snap if missing, connecting all required interfaces, and registering the gateway. Signed-off-by: Zygmunt Krynicki --- install.sh | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 105 insertions(+), 4 deletions(-) diff --git a/install.sh b/install.sh index 6cda59f8bc..a0078bfcd7 100755 --- a/install.sh +++ b/install.sh @@ -60,9 +60,10 @@ NOTES: 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. - macOS installs the release Homebrew formula on Apple Silicon and starts a - brew services-backed local gateway. + x86_64/aarch64, depending on the host package manager. When snapd is + available and no native Docker Engine is installed, the installer prefers + the Snap path. macOS installs the release Homebrew formula on Apple Silicon + and starts a brew services-backed local gateway. EOF } @@ -76,6 +77,14 @@ require_cmd() { fi } +has_snapd() { + has_cmd snap && systemctl is-active --quiet snapd.socket 2>/dev/null +} + +has_native_docker() { + has_cmd docker && ! snap list docker >/dev/null 2>&1 +} + download() { _url="$1" _output="$2" @@ -468,6 +477,17 @@ detect_platform() { } linux_package_method() { + case "${OPENSHELL_INSTALL_METHOD:-}" in + classic) + ;; + *) + if has_snapd && ! has_native_docker; then + echo "snap" + return 0 + fi + ;; + esac + if has_cmd dpkg; then echo "deb" elif has_cmd rpm; then @@ -953,6 +973,83 @@ install_linux_rpm() { start_user_gateway } +install_linux_snap() { + set_linux_target_runtime_dir + + # Docker snap must be installed before openshell so that the + # openshell:docker -> docker:docker-daemon interface connection + # can succeed. The docker-daemon slot doesn't exist until the + # Docker snap itself is present. + if ! snap list docker >/dev/null 2>&1; then + info "docker snap not found, installing..." + as_root snap install docker + else + info "docker snap already present" + fi + + info "installing openshell snap..." + as_root snap install openshell + + # Connect required interfaces. Network and network-bind auto-connect + # on install; the remaining plugs need explicit connection. snap connect + # is idempotent — it succeeds whether the connection was just made or + # already existed. + as_root snap connect openshell:docker docker:docker-daemon + as_root snap connect openshell:log-observe + as_root snap connect openshell:system-observe + as_root snap connect openshell:ssh-keys + + info "installed openshell snap from Snap Store" + info "registering local gateway as ${TARGET_USER}..." + register_local_gateway_snap + wait_for_local_gateway_listener_snap + wait_for_local_gateway_status +} + +register_local_gateway_snap() { + _register_bin="${OPENSHELL_REGISTER_BIN:-openshell}" + + if _add_output="$($_register_bin gateway add "http://127.0.0.1:${LOCAL_GATEWAY_PORT}" --local --name openshell 2>&1)"; then + [ -z "$_add_output" ] || print_gateway_add_output "$_add_output" + return 0 + fi + + _add_status=$? + + case "$_add_output" in + *"already exists"*) + info "local gateway already exists; removing and re-adding it..." + remove_local_gateway_registration + "$_register_bin" gateway add "http://127.0.0.1:${LOCAL_GATEWAY_PORT}" --local --name openshell + ;; + *) + printf '%s\n' "$_add_output" >&2 + return "$_add_status" + ;; + esac +} + +wait_for_local_gateway_listener_snap() { + _timeout="${OPENSHELL_INSTALL_GATEWAY_TIMEOUT_SNAP:-90}" + _elapsed=0 + _last_output="" + _probe_url="http://127.0.0.1:${LOCAL_GATEWAY_PORT}/" + + info "waiting for local gateway listener to become reachable..." + while [ "$_elapsed" -lt "$_timeout" ]; do + if _last_output="$(curl -sS --max-time 2 "$_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 +' "$_last_output" >&2 + error "local gateway listener did not become reachable at ${_probe_url} within ${_timeout}s" +} + install_macos_homebrew() { check_macos_platform @@ -1033,12 +1130,16 @@ main() { case "$PLATFORM" in linux) - require_linux_package_glibc case "$(linux_package_method)" in + snap) + install_linux_snap + ;; deb) + require_linux_package_glibc install_linux_deb ;; rpm) + require_linux_package_glibc install_linux_rpm ;; *) From 5886877356092b7905df64d726ad9a69c23ba6c2 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Fri, 5 Jun 2026 13:14:23 +0200 Subject: [PATCH 2/7] docs(installation): document snap install path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix the snap gateway protocol from https to http to match what the installer actually registers. Correct misleading claims that the installer "exits with an error" and "refuses" snap installs on hosts with native Docker — it silently falls back to classic. Signed-off-by: Zygmunt Krynicki --- docs/about/installation.mdx | 80 +++++++++++++++++++++++++++++++++---- 1 file changed, 72 insertions(+), 8 deletions(-) diff --git a/docs/about/installation.mdx b/docs/about/installation.mdx index 2ac077e7b9..410d383798 100644 --- a/docs/about/installation.mdx +++ b/docs/about/installation.mdx @@ -16,7 +16,10 @@ Install OpenShell with a single command: curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh ``` -The script detects your operating system and installs the OpenShell CLI and gateway with your native package manager. It then starts the local gateway server so you can begin creating sandboxes. +The script detects your operating system and installs the OpenShell CLI and +gateway. On Linux, the Snap path is preferred when `snapd` is available; +otherwise the script uses the native DEB or RPM package. The gateway then +starts automatically so you can begin creating sandboxes. You can also download release artifacts directly from the [OpenShell GitHub Releases](https://github.com/NVIDIA/OpenShell/releases) page. @@ -51,6 +54,13 @@ brew services restart openshell ## Linux +On distributions that ship with `snapd`, the install script uses the Snap path +described below, provided no non-snap Docker is detected. If a native Docker +package is present, the installer silently falls back to DEB or RPM. On hosts +without `snapd` (or with +`OPENSHELL_INSTALL_METHOD=classic` set), the script falls back to the classic +package manager. + On Fedora and RHEL, the install script uses RPM packages. The RPM installs the `openshell` CLI, the `openshell-gateway` daemon, and a systemd user service. On Debian and Ubuntu, the install script uses a Debian package. The Debian package installs the `openshell` CLI, the `openshell-gateway` daemon, VM sandbox support, and a systemd user service. @@ -77,7 +87,12 @@ sudo loginctl enable-linger $USER ## Snap -Install the OpenShell snap from the Snap Store: +On Linux distributions that ship with `snapd`, the install script installs +OpenShell from the Snap Store. The OpenShell snap bundles the CLI, the terminal +UI, and a managed gateway daemon. snapd handles upgrades and rollback; the +gateway runs as a system service inside the snap. + +You can also install the snap directly: ```shell sudo snap install openshell @@ -94,19 +109,34 @@ typically `~/snap/openshell/common`. Gateway registrations live under `$SNAP_USER_COMMON/.config/openshell/gateways/` instead of `~/.config/openshell/gateways/`. +The snap requires the Docker snap. `default-provider: docker` on the OpenShell +snap installs the Docker snap automatically on first use, but you can install +it up front with: + +```shell +sudo snap install docker +``` + ### Snap store installs -When installing from the Snap Store, snapd automatically connects the `home`, -`network`, and `network-bind` plugs. The `docker` plug still -requires manual connection: +When installing from the Snap Store, snapd automatically connects the `home` +`network`, and `network-bind` plugs. The remaining plugs — `docker`, +`log-observe`, and `system-observe` — still require manual connection: ```shell sudo snap connect openshell:docker docker:docker-daemon +sudo snap connect openshell:log-observe +sudo snap connect openshell:system-observe ``` -The snap declares `default-provider: docker` on the Docker plug so snapd will -offer to install the Docker snap, but the connection itself must be made -manually. +The Docker slot is the Docker snap's `docker-daemon` slot; OpenShell does not +work with a host-installed Docker Engine. The snap declares `default-provider: +docker` on the Docker plug so snapd will offer to install the Docker snap, but +the connection itself must be made manually. The installer runs these +connections for you on Snap installs; run them by hand if you install the snap +manually. The installer is best-effort: if a connect fails (for example because +the Docker snap is not yet running), the snap still installs and the installer +prints a warning. ### Locally built snap packages @@ -127,6 +157,26 @@ to read logs and inspect system processes. The `docker` plug requires the `docker:docker-daemon` slot from the Docker snap and does not work with system-installed Docker. +### Verify the gateway + +The snap-managed gateway service is `openshell.gateway`. Inspect it with: + +```shell +snap services openshell +snap logs -n 100 openshell.gateway +``` + +Register the gateway with the CLI: + +```shell +openshell gateway add http://127.0.0.1:17670 --local --name openshell +openshell status +``` + +The gateway listens on `http://127.0.0.1:17670` and stores its state under +`/var/snap/openshell/common/`. Override gateway settings by creating +`/var/snap/openshell/common/gateway.toml`. + ### Gateway service The gateway runs as a snap daemon with `refresh-mode: endure`, meaning snapd @@ -138,6 +188,20 @@ a snap refresh when you need the updated binary: sudo systemctl restart snap.openshell.gateway ``` +### When to choose Snap + +Use Snap when `snapd` is available and no native Docker Engine is installed, +and you want atomic upgrades and rollback, a single self-contained install that +bundles the Docker provider, or a desktop launcher that surfaces the OpenShell +terminal UI in the application menu. + +Use DEB or RPM when `snapd` is unavailable or when you already run Docker Engine +from a non-snap source. The installer falls back to DEB or RPM on hosts with +native Docker. + +Set `OPENSHELL_INSTALL_METHOD=classic` to force the classic package on hosts +that also have `snapd` available. + ## Kubernetes Kubernetes deployments use the OpenShell Helm chart. For step-by-step installation, refer to [Kubernetes Setup](/kubernetes/setup). For chart values and packaging details, refer to the [Helm chart README](https://github.com/NVIDIA/OpenShell/blob/main/deploy/helm/openshell/README.md). From 4bd8a05159306818e6ae444cd8cd4903b119d630 Mon Sep 17 00:00:00 2001 From: Oliver Calder Date: Mon, 13 Jul 2026 15:52:00 -0500 Subject: [PATCH 3/7] docs(installation): update installation docs to for snap installation Adjust the wording of the installation docs for the `openshell` snap. In particular, clarify when the snap will be installed, how it currently requires the docker snap, and how to override the installation method. Also, prune some of the manual interface connections, as these have since been granted autoconnection via snap store assertions. See: https://forum.snapcraft.io/t/please-update-snap-declaration-assertion-for-openshell/51762 Signed-off-by: Oliver Calder --- docs/about/installation.mdx | 53 +++++++++++++++---------------------- install.sh | 9 ------- 2 files changed, 21 insertions(+), 41 deletions(-) diff --git a/docs/about/installation.mdx b/docs/about/installation.mdx index 410d383798..dd7e2e0aa1 100644 --- a/docs/about/installation.mdx +++ b/docs/about/installation.mdx @@ -55,11 +55,10 @@ brew services restart openshell ## Linux On distributions that ship with `snapd`, the install script uses the Snap path -described below, provided no non-snap Docker is detected. If a native Docker -package is present, the installer silently falls back to DEB or RPM. On hosts -without `snapd` (or with -`OPENSHELL_INSTALL_METHOD=classic` set), the script falls back to the classic -package manager. +described below, unless one of the following is true: +- a native (non-snap) Docker package is present (the `openshell` snap requires the `docker` snap, for now) +- `snapd` is not present +- `OPENSHELL_INSTALL_METHOD=classic` is set in the environment On Fedora and RHEL, the install script uses RPM packages. The RPM installs the `openshell` CLI, the `openshell-gateway` daemon, and a systemd user service. @@ -89,7 +88,7 @@ sudo loginctl enable-linger $USER On Linux distributions that ship with `snapd`, the install script installs OpenShell from the Snap Store. The OpenShell snap bundles the CLI, the terminal -UI, and a managed gateway daemon. snapd handles upgrades and rollback; the +UI, and a managed gateway daemon. `snapd` handles upgrades and rollback; the gateway runs as a system service inside the snap. You can also install the snap directly: @@ -109,9 +108,7 @@ typically `~/snap/openshell/common`. Gateway registrations live under `$SNAP_USER_COMMON/.config/openshell/gateways/` instead of `~/.config/openshell/gateways/`. -The snap requires the Docker snap. `default-provider: docker` on the OpenShell -snap installs the Docker snap automatically on first use, but you can install -it up front with: +The OpenShell snap requires the Docker snap, which you can install with: ```shell sudo snap install docker @@ -119,24 +116,16 @@ sudo snap install docker ### Snap store installs -When installing from the Snap Store, snapd automatically connects the `home` -`network`, and `network-bind` plugs. The remaining plugs — `docker`, -`log-observe`, and `system-observe` — still require manual connection: +When installing from the Snap Store, snapd automatically connects the `docker`, +`home`, `log-observe`, `network`, `network-bind`, and `system-observe plugs. -```shell -sudo snap connect openshell:docker docker:docker-daemon -sudo snap connect openshell:log-observe -sudo snap connect openshell:system-observe -``` - -The Docker slot is the Docker snap's `docker-daemon` slot; OpenShell does not -work with a host-installed Docker Engine. The snap declares `default-provider: -docker` on the Docker plug so snapd will offer to install the Docker snap, but -the connection itself must be made manually. The installer runs these -connections for you on Snap installs; run them by hand if you install the snap -manually. The installer is best-effort: if a connect fails (for example because -the Docker snap is not yet running), the snap still installs and the installer -prints a warning. +The `docker` interface is used to connect the `openshell` snap's `docker` plug +to the `docker` snap's `docker-daemon` slot; the `openshell` snap does not yet +work with a host-installed Docker Engine. The installer runs this and the other +interface connections for you after installing the `openshell` snap; run them +by hand if you install the snap manually. The installer is best-effort: if a +connect fails (for example because the `docker` snap is not yet running), the +snap still installs and the installer prints a warning. ### Locally built snap packages @@ -190,14 +179,14 @@ sudo systemctl restart snap.openshell.gateway ### When to choose Snap -Use Snap when `snapd` is available and no native Docker Engine is installed, -and you want atomic upgrades and rollback, a single self-contained install that -bundles the Docker provider, or a desktop launcher that surfaces the OpenShell +Use the `openshell` snap when `snapd` is available and no native Docker Engine +is installed, and you want atomic upgrades and rollback, a single self-contained +and sandboxed install, or a desktop launcher that surfaces the OpenShell terminal UI in the application menu. -Use DEB or RPM when `snapd` is unavailable or when you already run Docker Engine -from a non-snap source. The installer falls back to DEB or RPM on hosts with -native Docker. +Use the `openshell` `.deb` or `.rpm` file when `snapd` is unavailable or when +you already run Docker from a non-snap source. The installer falls back to +these methods on hosts with native Docker. Set `OPENSHELL_INSTALL_METHOD=classic` to force the classic package on hosts that also have `snapd` available. diff --git a/install.sh b/install.sh index a0078bfcd7..0e01c7a3d4 100755 --- a/install.sh +++ b/install.sh @@ -990,15 +990,6 @@ install_linux_snap() { info "installing openshell snap..." as_root snap install openshell - # Connect required interfaces. Network and network-bind auto-connect - # on install; the remaining plugs need explicit connection. snap connect - # is idempotent — it succeeds whether the connection was just made or - # already existed. - as_root snap connect openshell:docker docker:docker-daemon - as_root snap connect openshell:log-observe - as_root snap connect openshell:system-observe - as_root snap connect openshell:ssh-keys - info "installed openshell snap from Snap Store" info "registering local gateway as ${TARGET_USER}..." register_local_gateway_snap From 22525a2170a3c4fd3fa49d0b7d3ca05df1b01660 Mon Sep 17 00:00:00 2001 From: Oliver Calder Date: Mon, 13 Jul 2026 16:58:28 -0500 Subject: [PATCH 4/7] feat(install): add `OPENSHELL_INSTALL_METHOD` options As @drew suggested, add `deb`, `rpm`, `snap`, and `homebrew` as installation methods which the user can specify via `OPENSHELL_INSTALL_METHOD` rather than falling back to detecting the package manager automatically. `homebrew` is not yet supported on Linux, and it is the only supported option on MacOS. Also adds a TODO to implement `install_linux_homebrew` in the future. Signed-off-by: Oliver Calder --- docs/about/installation.mdx | 23 +++++- install.sh | 44 +++++++++-- tasks/scripts/test-install-sh.sh | 128 +++++++++++++++++++++++++++++++ 3 files changed, 186 insertions(+), 9 deletions(-) diff --git a/docs/about/installation.mdx b/docs/about/installation.mdx index dd7e2e0aa1..4f0d162706 100644 --- a/docs/about/installation.mdx +++ b/docs/about/installation.mdx @@ -58,7 +58,7 @@ On distributions that ship with `snapd`, the install script uses the Snap path described below, unless one of the following is true: - a native (non-snap) Docker package is present (the `openshell` snap requires the `docker` snap, for now) - `snapd` is not present -- `OPENSHELL_INSTALL_METHOD=classic` is set in the environment +- `OPENSHELL_INSTALL_METHOD` is set in the environment to some value other than `snap` (e.g. `OPENSHELL_INSTALL_METHOD=deb`) On Fedora and RHEL, the install script uses RPM packages. The RPM installs the `openshell` CLI, the `openshell-gateway` daemon, and a systemd user service. @@ -188,8 +188,25 @@ Use the `openshell` `.deb` or `.rpm` file when `snapd` is unavailable or when you already run Docker from a non-snap source. The installer falls back to these methods on hosts with native Docker. -Set `OPENSHELL_INSTALL_METHOD=classic` to force the classic package on hosts -that also have `snapd` available. +### Force an install method + +Set `OPENSHELL_INSTALL_METHOD` to force a specific install method instead of +relying on auto-detection. Supported values: + +| Value | Install path | +|---|---| +| `deb` | Debian package via `apt`/`dpkg` | +| `rpm` | RPM package via `dnf`/`yum`/`zypper`/`rpm` | +| `snap` | Snap package via `snapd` | +| `homebrew` | Homebrew formula via `brew` (macOS only; not yet supported on Linux) | + +When the variable is unset, the script auto-detects the host package manager +as described above. If the requested method's package manager is not +installed, the installer aborts with an error. + +```shell +curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | OPENSHELL_INSTALL_METHOD=deb sh +``` ## Kubernetes diff --git a/install.sh b/install.sh index 0e01c7a3d4..c6757e87ce 100755 --- a/install.sh +++ b/install.sh @@ -51,6 +51,10 @@ OPTIONS: ENVIRONMENT VARIABLES: OPENSHELL_VERSION Release tag to install (default: latest tagged release). Set OPENSHELL_VERSION=dev to install the rolling dev build. + OPENSHELL_INSTALL_METHOD + Force a specific install method. Supported values: + deb, rpm, snap, homebrew. When unset, the script + auto-detects the host package manager. OPENSHELL_ACK_BREAKING_UPGRADE Set to 1 only after backing up and cleaning up a pre-v0.0.37 installation. @@ -478,17 +482,32 @@ detect_platform() { linux_package_method() { case "${OPENSHELL_INSTALL_METHOD:-}" in - classic) + "") + ;; + snap) + echo "snap" + return 0 + ;; + deb|apt) + echo "deb" + return 0 + ;; + rpm|dnf|yum|zypper) + echo "rpm" + return 0 + ;; + homebrew|brew) + echo "homebrew" + return 0 ;; *) - if has_snapd && ! has_native_docker; then - echo "snap" - return 0 - fi + error "OPENSHELL_INSTALL_METHOD='${OPENSHELL_INSTALL_METHOD}' is not supported; use one of: deb, rpm, snap, homebrew" ;; esac - if has_cmd dpkg; then + if has_snapd && ! has_native_docker; then + echo "snap" + elif has_cmd dpkg; then echo "deb" elif has_cmd rpm; then echo "rpm" @@ -974,6 +993,7 @@ install_linux_rpm() { } install_linux_snap() { + require_cmd snap set_linux_target_runtime_dir # Docker snap must be installed before openshell so that the @@ -1133,12 +1153,24 @@ main() { require_linux_package_glibc install_linux_rpm ;; + homebrew) + # TODO: implement install_linux_homebrew for Linuxbrew hosts + # (e.g. Universal Blue/Bazzite). Homebrew is accepted as a method + # value but not yet wired to an installer on Linux. + error "the 'homebrew' install method is not yet supported on Linux" + ;; *) error "unsupported Linux package method" ;; esac ;; darwin) + case "${OPENSHELL_INSTALL_METHOD:-}" in + ""|homebrew|brew) ;; + *) + error "only the 'homebrew' install method is supported on macOS" + ;; + esac install_macos_homebrew ;; *) diff --git a/tasks/scripts/test-install-sh.sh b/tasks/scripts/test-install-sh.sh index a1259cf0bc..d3474b1c2c 100755 --- a/tasks/scripts/test-install-sh.sh +++ b/tasks/scripts/test-install-sh.sh @@ -100,4 +100,132 @@ assert_glibc_preflight_fails \ "OpenShell Linux packages require glibc >= 2.28; detected musl or unsupported libc." \ setup_ldd_musl +assert_method_equals() { + local name=$1 + local value=$2 + local expected=$3 + + local result + if ! result="$(OPENSHELL_INSTALL_METHOD="$value" linux_package_method)" 2>"$err"; then + echo "FAIL: ${name}: expected '${expected}' but call failed" >&2 + cat "$err" >&2 || true + exit 1 + fi + + if [ "$result" != "$expected" ]; then + echo "FAIL: ${name}: expected '${expected}', got '${result}'" >&2 + exit 1 + fi +} + +assert_method_fails() { + local name=$1 + local value=$2 + local expected=$3 + + if (OPENSHELL_INSTALL_METHOD="$value" linux_package_method) >"$out" 2>"$err"; then + echo "FAIL: ${name}: expected failure for value '${value}'" >&2 + exit 1 + fi + + if ! grep -Fq "$expected" "$err"; then + echo "FAIL: ${name}: missing expected message" >&2 + echo "Expected: ${expected}" >&2 + echo "Actual:" >&2 + cat "$err" >&2 || true + exit 1 + fi +} + +assert_method_equals "method deb passes through" "deb" "deb" +assert_method_equals "method rpm passes through" "rpm" "rpm" +assert_method_equals "method snap passes through" "snap" "snap" +assert_method_equals "method homebrew passes through" "homebrew" "homebrew" + +assert_method_equals "alias apt maps to deb" "apt" "deb" +assert_method_equals "alias dnf maps to rpm" "dnf" "rpm" +assert_method_equals "alias yum maps to rpm" "yum" "rpm" +assert_method_equals "alias zypper maps to rpm" "zypper" "rpm" +assert_method_equals "alias brew maps to homebrew" "brew" "homebrew" + +assert_method_fails \ + "unknown method fails" \ + "apk" \ + "is not supported; use one of: deb, rpm, snap, homebrew" + +assert_method_fails \ + "classic method fails" \ + "classic" \ + "is not supported; use one of: deb, rpm, snap, homebrew" + +# Unset OPENSHELL_INSTALL_METHOD falls through to host detection. Shim the +# has_* helpers so the auto-detect path is deterministic regardless of the +# host running the tests. +assert_method_unset_detects() { + local name=$1 + local snapd=$2 + local native_docker=$3 + local dpkg=$4 + local rpm=$5 + local expected=$6 + + local result + if ! result="$( + unset OPENSHELL_INSTALL_METHOD + has_snapd() { [ "$snapd" = "1" ]; } + has_native_docker() { [ "$native_docker" = "1" ]; } + has_cmd() { + case "$1" in + dpkg) [ "$dpkg" = "1" ] ;; + rpm) [ "$rpm" = "1" ] ;; + *) return 1 ;; + esac + } + linux_package_method + )" 2>"$err"; then + echo "FAIL: ${name}: call failed" >&2 + cat "$err" >&2 || true + exit 1 + fi + + if [ "$result" != "$expected" ]; then + echo "FAIL: ${name}: expected '${expected}', got '${result}'" >&2 + exit 1 + fi +} + +assert_method_unset_detects \ + "unset prefers snap when snapd and no native docker" \ + 1 0 1 1 "snap" + +assert_method_unset_detects \ + "unset skips snap when native docker present" \ + 1 1 1 0 "deb" + +assert_method_unset_detects \ + "unset skips snap when snapd absent" \ + 0 0 1 0 "deb" + +assert_method_unset_detects \ + "unset falls back to rpm when no dpkg" \ + 0 0 0 1 "rpm" + +# Unset with no snapd, no dpkg, no rpm must error. +if ( + unset OPENSHELL_INSTALL_METHOD + has_snapd() { return 1; } + has_native_docker() { return 1; } + has_cmd() { return 1; } + linux_package_method +) >"$out" 2>"$err"; then + echo "FAIL: unset with no package managers should error" >&2 + exit 1 +fi + +if ! grep -Fq "Linux installs require either dpkg or rpm" "$err"; then + echo "FAIL: missing no-package-manager error" >&2 + cat "$err" >&2 || true + exit 1 +fi + echo "install.sh libc preflight tests passed" From dc778b489a851c1f8281f167ee814a412db9ba69 Mon Sep 17 00:00:00 2001 From: Oliver Calder Date: Thu, 16 Jul 2026 22:19:58 -0500 Subject: [PATCH 5/7] ci(release): update ubuntu release canary variants Split the `ubuntu` release canary into three jobs: - `ubuntu-deb`: test that `install.sh` installs the OpenShell .deb package on Ubuntu when snapd is not present. - `ubuntu-deb-native-docker`: test that `install.sh` installs the OpenShell .deb package on Ubuntu when a native docker daemon is present -- this will change when snapd 2.77 is released to the `latest/stable` channel, which will allow the `openshell` snap to connect to non-snap Docker daemons; this job will then be renamed to `ubuntu-snap-native-docker`. - `ubuntu-snap-without-docker`: test that `install.sh` installs both the `openshell` and `docker` snaps if snapd is present and there is no Docker daemon installed. In all variants, the canary tests that `openshell status` succeeds after installation. Signed-off-by: Oliver Calder --- .agents/skills/test-release-canary/SKILL.md | 9 +- .github/workflows/release-canary.yml | 136 ++++++++++++-------- 2 files changed, 90 insertions(+), 55 deletions(-) diff --git a/.agents/skills/test-release-canary/SKILL.md b/.agents/skills/test-release-canary/SKILL.md index 4bf7d38ae3..a7234368b2 100644 --- a/.agents/skills/test-release-canary/SKILL.md +++ b/.agents/skills/test-release-canary/SKILL.md @@ -12,7 +12,9 @@ The Release Canary (`.github/workflows/release-canary.yml`) smoke-tests the arti | Job | Runner | Verifies | |---|---|---| | `macos` | `macos-latest-xlarge` | `install.sh` resolves the Homebrew formula, brew installs the cask, and `openshell status` reaches the brew-services–backed local gateway with the VM driver. | -| `ubuntu` | `ubuntu-latest` | `install.sh` installs the Debian package, the post-install systemd user service starts, and `openshell status` reaches the local gateway with the Docker driver. | +| `ubuntu-deb-native-docker` | `ubuntu-latest` | `install.sh` auto-detects the Debian package when both snapd and native Docker are present, installs the deb, the post-install systemd user service starts, and `openshell status` reaches the local gateway with the Docker driver. Once snapd 2.77 is released to `latest/stable`, `install.sh` should install the `openshell` snap here instead, even with native Docker present. This job should then be renamed to `ubuntu-snap-native-docker`, and its purpose will be to verify that the openshell snap installs and works when a native (non-snap) Docker is already present on the system. | +| `ubuntu-deb` | `ubuntu-latest` | `install.sh` installs the Debian package after snapd is fully removed (forcing the deb path), starts the systemd user service, and `openshell status` reaches the local gateway with the Docker driver. | +| `ubuntu-snap-without-docker` | `ubuntu-latest` | `install.sh` auto-detects and installs both the `openshell` and `docker` snaps from the Snap Store when snapd is present and there is no Docker daemon on the system, the gateway daemon starts with auto-connected interfaces, and `openshell status` reaches the local gateway. | | `fedora` | `fedora:latest` container | `install.sh` installs the RPM packages, the local gateway starts under Podman, and `openshell status` succeeds. | | `kubernetes` | `ubuntu-latest` + kind | `helm install oci://ghcr.io/nvidia/openshell/helm-chart --version 0.0.0-dev` succeeds in a kind cluster, the gateway pod becomes Ready, port-forward exposes 8080, and the released CLI registers the in-cluster gateway and runs `openshell status` against it. | @@ -107,8 +109,9 @@ Loopback registration auto-derives the gateway name to `openshell` if `--name` i | Symptom | Likely cause | Where to look | |---|---|---| -| `macos`/`ubuntu`/`fedora` job fails on `install.sh` | Latest tagged release missing an asset, checksum mismatch, or `install.sh` regression on this branch. | Job log around the `curl … install.sh \| sh` step. | -| `macos`/`ubuntu`/`fedora` job fails on `openshell status` | Local gateway service did not start (systemd/brew/podman). Often a driver issue. | Service logs in the job log; `OPENSHELL_DRIVERS` env in the "Ensure …" step. | +| `macos`/`ubuntu-deb-native-docker`/`ubuntu-deb`/`fedora` job fails on `install.sh` | Latest tagged release missing an asset, checksum mismatch, or `install.sh` regression on this branch. | Job log around the `curl … install.sh \| sh` step. | +| `macos`/`ubuntu-deb-native-docker`/`ubuntu-deb`/`fedora` job fails on `openshell status` | Local gateway service did not start (systemd/brew/podman). Often a driver issue. | Service logs in the job log; `OPENSHELL_DRIVERS` env in the "Ensure …" step. | +| `ubuntu-snap-without-docker` job fails on `openshell status` | Snap gateway daemon did not start. | `snap services openshell` and `snap logs openshell.gateway` in the job log. | | `kubernetes` job fails on `helm install --wait` | Chart did not deploy in 5 min — usually image pull failure or readiness probe failing. | "Diagnostics on failure" step dumps `helm status`, manifest, pod describe, pod logs. | | `kubernetes` job fails on `kubectl wait` | Gateway pod stuck `CrashLoopBackOff` or `ImagePullBackOff`. | Diagnostics dump; check `:dev` image existence at `ghcr.io/nvidia/openshell/gateway`. | | `kubernetes` job fails on `openshell gateway add` or `status` | Port-forward not reachable, or CLI/gateway proto mismatch. | `port-forward.log` and `openshell gateway list` in the diagnostics dump. | diff --git a/.github/workflows/release-canary.yml b/.github/workflows/release-canary.yml index 896d12d190..f7fbec3c84 100644 --- a/.github/workflows/release-canary.yml +++ b/.github/workflows/release-canary.yml @@ -30,11 +30,17 @@ jobs: curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/${{ github.event.workflow_run.head_sha || github.sha }}/install.sh | sh openshell status - ubuntu: - name: Ubuntu Docker + ubuntu-deb-native-docker: + name: Ubuntu (native Docker) if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest timeout-minutes: 20 + # TODO: Once snapd 2.77 is released to latest/stable, the openshell snap + # will be able to connect to system Docker via the docker interface, + # removing the requirement for the docker snap. At that point, + # install.sh should select snap here even with native Docker present. + # Rename this job to `ubuntu-snap-native-docker`, replace the dpkg + # assertion below with `snap list openshell`. steps: - name: Ensure Docker run: | @@ -50,6 +56,82 @@ jobs: - name: Install and check status run: | curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/${{ github.event.workflow_run.head_sha || github.sha }}/install.sh | sh + # Expected to fail once snapd 2.77 is in latest/stable, at which + # point install.sh should select snap instead of deb here. + dpkg -l openshell | grep '^ii' + openshell --version + systemctl --user is-active openshell-gateway + openshell status + + ubuntu-deb: + name: Ubuntu Debian package + if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Remove snapd + run: | + set -euo pipefail + # Remove all snaps in a single transaction. Core bases and content + # providers cannot be removed before snaps that consume them, so + # removing one at a time fails; snap handles ordering within a + # single `snap remove` invocation. + snaps=$(snap list 2>/dev/null | awk 'NR>1 {print $1}') + if [ -n "$snaps" ]; then + sudo snap remove $snaps + fi + sudo apt-get -y --purge remove snapd + ! command -v snap >/dev/null 2>&1 + + - name: Ensure Docker + run: | + if ! command -v docker >/dev/null 2>&1; then + sudo apt-get update + sudo apt-get install -y docker.io + fi + sudo systemctl start docker || sudo service docker start + mkdir -p "${HOME}/.config/openshell" + printf 'OPENSHELL_DRIVERS=docker\n' > "${HOME}/.config/openshell/gateway.env" + docker info + + - name: Install and check status + run: | + curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/${{ github.event.workflow_run.head_sha || github.sha }}/install.sh | sh + dpkg -l openshell | grep '^ii' + openshell --version + systemctl --user is-active openshell-gateway + openshell status + + ubuntu-snap-without-docker: + name: Ubuntu without Docker preinstalled + if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Ensure snapd + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y snapd + sudo systemctl enable --now snapd.socket + sudo systemctl start snapd + sudo snap wait system seed.loaded + + - name: Remove native Docker + run: | + set -euo pipefail + sudo systemctl stop docker.service docker.socket 2>/dev/null || true + sudo apt-get -y --purge remove docker-ce docker-ce-cli containerd.io \ + docker-buildx-plugin docker-compose-plugin docker.io 2>/dev/null || true + ! command -v docker >/dev/null 2>&1 + + - name: Install and check status + run: | + set -euo pipefail + curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/${{ github.event.workflow_run.head_sha || github.sha }}/install.sh | sh + snap list openshell + openshell --version + sudo snap services openshell openshell status fedora: @@ -145,56 +227,6 @@ jobs: run: | docker rm -f "${FEDORA_CANARY_CONTAINER}" >/dev/null 2>&1 || true - ubuntu-snap: - name: Ubuntu Snap - if: ${{ github.event.workflow_run.conclusion == 'success' }} - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - name: Install snapd - run: | - set -euo pipefail - sudo apt-get update - sudo apt-get install -y snapd - sudo systemctl enable --now snapd.socket - sudo systemctl start snapd - sudo snap wait system seed.loaded - - - name: Install Docker snap - run: | - set -euo pipefail - sudo snap install docker - - - name: Download snap from release-dev artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - github-token: ${{ github.token }} - run-id: ${{ github.event.workflow_run.id }} - pattern: snap-linux-amd64 - path: release/ - merge-multiple: true - - - name: Install snap (dangerous — from release, not store) - run: | - set -euo pipefail - sudo snap install ./release/*.snap --dangerous - - - name: Connect interfaces - run: | - set -euo pipefail - sudo snap connect openshell:docker docker:docker-daemon - sudo snap connect openshell:log-observe - sudo snap connect openshell:system-observe - - - name: Register snap gateway and check status - run: | - set -euo pipefail - openshell --version - sudo snap services openshell - openshell gateway add http://127.0.0.1:17670 --local --name snap-docker - openshell gateway select snap-docker - openshell status - kubernetes: name: Kubernetes Helm (kind) if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} From 8db607220e41ed91d2de3966c27bef512c83c07c Mon Sep 17 00:00:00 2001 From: Oliver Calder Date: Thu, 16 Jul 2026 22:28:28 -0500 Subject: [PATCH 6/7] fix(snap): fix outdated description in snapcraft.yaml The change in snapd to support the `openshell` snap connecting to a non-snap Docker daemon will ship in snapd 2.77, not 2.76. Also, all the `openshell` snap interfaces are now auto-connected, so there is no need for manual connection anymore. Remove these from the description. Signed-off-by: Oliver Calder --- snapcraft.yaml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/snapcraft.yaml b/snapcraft.yaml index d221c51e7e..1802a8c03a 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -20,13 +20,10 @@ description: | sudo snap install docker - Support for system-installed Docker is coming in snapd 2.76. + Support for system-installed Docker is coming in snapd 2.77. - 2. Connect the required interfaces and start the gateway: + 2. Start the gateway: - sudo snap connect openshell:docker docker:docker-daemon - sudo snap connect openshell:log-observe - sudo snap connect openshell:system-observe sudo snap start openshell.gateway 3. Verify the gateway and register it locally: From c91aea97067e56503eb746e8601e26dfe95f61bb Mon Sep 17 00:00:00 2001 From: Oliver Calder Date: Thu, 16 Jul 2026 22:46:52 -0500 Subject: [PATCH 7/7] fix(install): remove OPENSHELL_INSTALL_METHOD variable The contract of `install.sh` is to choose the best installation method for the host system. Adding the `OPENSHELL_INSTALL_METHOD` variable breaks that contract, so it is best to remove it. Signed-off-by: Oliver Calder --- docs/about/installation.mdx | 26 ++------- install.sh | 42 +-------------- tasks/scripts/test-install-sh.sh | 90 ++++++-------------------------- 3 files changed, 19 insertions(+), 139 deletions(-) diff --git a/docs/about/installation.mdx b/docs/about/installation.mdx index 4f0d162706..4288f52ac1 100644 --- a/docs/about/installation.mdx +++ b/docs/about/installation.mdx @@ -56,13 +56,13 @@ brew services restart openshell On distributions that ship with `snapd`, the install script uses the Snap path described below, unless one of the following is true: -- a native (non-snap) Docker package is present (the `openshell` snap requires the `docker` snap, for now) + - `snapd` is not present -- `OPENSHELL_INSTALL_METHOD` is set in the environment to some value other than `snap` (e.g. `OPENSHELL_INSTALL_METHOD=deb`) +- a native (non-snap) Docker package is present (the `openshell` snap requires the `docker` snap, until snapd 2.77 is released to the `latest/stable` channel) On Fedora and RHEL, the install script uses RPM packages. The RPM installs the `openshell` CLI, the `openshell-gateway` daemon, and a systemd user service. -On Debian and Ubuntu, the install script uses a Debian package. The Debian package installs the `openshell` CLI, the `openshell-gateway` daemon, VM sandbox support, and a systemd user service. +On Debian and Ubuntu (where the snap is not applicable), the install script uses a Debian package. The Debian package installs the `openshell` CLI, the `openshell-gateway` daemon, VM sandbox support, and a systemd user service. Linux packages require glibc 2.28 or newer. The installer checks libc before downloading packages and exits with an error on older glibc versions, Alpine, musl-based distributions, or unknown libc environments. @@ -188,26 +188,6 @@ Use the `openshell` `.deb` or `.rpm` file when `snapd` is unavailable or when you already run Docker from a non-snap source. The installer falls back to these methods on hosts with native Docker. -### Force an install method - -Set `OPENSHELL_INSTALL_METHOD` to force a specific install method instead of -relying on auto-detection. Supported values: - -| Value | Install path | -|---|---| -| `deb` | Debian package via `apt`/`dpkg` | -| `rpm` | RPM package via `dnf`/`yum`/`zypper`/`rpm` | -| `snap` | Snap package via `snapd` | -| `homebrew` | Homebrew formula via `brew` (macOS only; not yet supported on Linux) | - -When the variable is unset, the script auto-detects the host package manager -as described above. If the requested method's package manager is not -installed, the installer aborts with an error. - -```shell -curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | OPENSHELL_INSTALL_METHOD=deb sh -``` - ## Kubernetes Kubernetes deployments use the OpenShell Helm chart. For step-by-step installation, refer to [Kubernetes Setup](/kubernetes/setup). For chart values and packaging details, refer to the [Helm chart README](https://github.com/NVIDIA/OpenShell/blob/main/deploy/helm/openshell/README.md). diff --git a/install.sh b/install.sh index c6757e87ce..028af8de22 100755 --- a/install.sh +++ b/install.sh @@ -51,10 +51,6 @@ OPTIONS: ENVIRONMENT VARIABLES: OPENSHELL_VERSION Release tag to install (default: latest tagged release). Set OPENSHELL_VERSION=dev to install the rolling dev build. - OPENSHELL_INSTALL_METHOD - Force a specific install method. Supported values: - deb, rpm, snap, homebrew. When unset, the script - auto-detects the host package manager. OPENSHELL_ACK_BREAKING_UPGRADE Set to 1 only after backing up and cleaning up a pre-v0.0.37 installation. @@ -481,30 +477,6 @@ detect_platform() { } linux_package_method() { - case "${OPENSHELL_INSTALL_METHOD:-}" in - "") - ;; - snap) - echo "snap" - return 0 - ;; - deb|apt) - echo "deb" - return 0 - ;; - rpm|dnf|yum|zypper) - echo "rpm" - return 0 - ;; - homebrew|brew) - echo "homebrew" - return 0 - ;; - *) - error "OPENSHELL_INSTALL_METHOD='${OPENSHELL_INSTALL_METHOD}' is not supported; use one of: deb, rpm, snap, homebrew" - ;; - esac - if has_snapd && ! has_native_docker; then echo "snap" elif has_cmd dpkg; then @@ -512,7 +484,7 @@ linux_package_method() { elif has_cmd rpm; then echo "rpm" else - error "Linux installs require either dpkg or rpm" + error "Linux installs require either snapd, dpkg, or rpm" fi } @@ -1153,24 +1125,12 @@ main() { require_linux_package_glibc install_linux_rpm ;; - homebrew) - # TODO: implement install_linux_homebrew for Linuxbrew hosts - # (e.g. Universal Blue/Bazzite). Homebrew is accepted as a method - # value but not yet wired to an installer on Linux. - error "the 'homebrew' install method is not yet supported on Linux" - ;; *) error "unsupported Linux package method" ;; esac ;; darwin) - case "${OPENSHELL_INSTALL_METHOD:-}" in - ""|homebrew|brew) ;; - *) - error "only the 'homebrew' install method is supported on macOS" - ;; - esac install_macos_homebrew ;; *) diff --git a/tasks/scripts/test-install-sh.sh b/tasks/scripts/test-install-sh.sh index d3474b1c2c..5ab28e731a 100755 --- a/tasks/scripts/test-install-sh.sh +++ b/tasks/scripts/test-install-sh.sh @@ -100,68 +100,10 @@ assert_glibc_preflight_fails \ "OpenShell Linux packages require glibc >= 2.28; detected musl or unsupported libc." \ setup_ldd_musl -assert_method_equals() { - local name=$1 - local value=$2 - local expected=$3 - - local result - if ! result="$(OPENSHELL_INSTALL_METHOD="$value" linux_package_method)" 2>"$err"; then - echo "FAIL: ${name}: expected '${expected}' but call failed" >&2 - cat "$err" >&2 || true - exit 1 - fi - - if [ "$result" != "$expected" ]; then - echo "FAIL: ${name}: expected '${expected}', got '${result}'" >&2 - exit 1 - fi -} - -assert_method_fails() { - local name=$1 - local value=$2 - local expected=$3 - - if (OPENSHELL_INSTALL_METHOD="$value" linux_package_method) >"$out" 2>"$err"; then - echo "FAIL: ${name}: expected failure for value '${value}'" >&2 - exit 1 - fi - - if ! grep -Fq "$expected" "$err"; then - echo "FAIL: ${name}: missing expected message" >&2 - echo "Expected: ${expected}" >&2 - echo "Actual:" >&2 - cat "$err" >&2 || true - exit 1 - fi -} - -assert_method_equals "method deb passes through" "deb" "deb" -assert_method_equals "method rpm passes through" "rpm" "rpm" -assert_method_equals "method snap passes through" "snap" "snap" -assert_method_equals "method homebrew passes through" "homebrew" "homebrew" - -assert_method_equals "alias apt maps to deb" "apt" "deb" -assert_method_equals "alias dnf maps to rpm" "dnf" "rpm" -assert_method_equals "alias yum maps to rpm" "yum" "rpm" -assert_method_equals "alias zypper maps to rpm" "zypper" "rpm" -assert_method_equals "alias brew maps to homebrew" "brew" "homebrew" - -assert_method_fails \ - "unknown method fails" \ - "apk" \ - "is not supported; use one of: deb, rpm, snap, homebrew" - -assert_method_fails \ - "classic method fails" \ - "classic" \ - "is not supported; use one of: deb, rpm, snap, homebrew" - -# Unset OPENSHELL_INSTALL_METHOD falls through to host detection. Shim the -# has_* helpers so the auto-detect path is deterministic regardless of the -# host running the tests. -assert_method_unset_detects() { +# Package format is detected based on the host environment. Shim the has_* +# helpers so the auto-detect path is deterministic regardless of the host +# running the tests. +assert_package_format_detection() { local name=$1 local snapd=$2 local native_docker=$3 @@ -171,7 +113,6 @@ assert_method_unset_detects() { local result if ! result="$( - unset OPENSHELL_INSTALL_METHOD has_snapd() { [ "$snapd" = "1" ]; } has_native_docker() { [ "$native_docker" = "1" ]; } has_cmd() { @@ -194,35 +135,34 @@ assert_method_unset_detects() { fi } -assert_method_unset_detects \ - "unset prefers snap when snapd and no native docker" \ +assert_package_format_detection \ + "prefers snap when snapd and no native docker" \ 1 0 1 1 "snap" -assert_method_unset_detects \ - "unset skips snap when native docker present" \ +assert_package_format_detection \ + "skips snap when native docker present" \ 1 1 1 0 "deb" -assert_method_unset_detects \ - "unset skips snap when snapd absent" \ +assert_package_format_detection \ + "skips snap when snapd absent" \ 0 0 1 0 "deb" -assert_method_unset_detects \ - "unset falls back to rpm when no dpkg" \ +assert_package_format_detection \ + "falls back to rpm when no dpkg" \ 0 0 0 1 "rpm" -# Unset with no snapd, no dpkg, no rpm must error. +# Host with no snapd, no dpkg, no rpm must error. if ( - unset OPENSHELL_INSTALL_METHOD has_snapd() { return 1; } has_native_docker() { return 1; } has_cmd() { return 1; } linux_package_method ) >"$out" 2>"$err"; then - echo "FAIL: unset with no package managers should error" >&2 + echo "FAIL: host with no package managers should error" >&2 exit 1 fi -if ! grep -Fq "Linux installs require either dpkg or rpm" "$err"; then +if ! grep -Fq "Linux installs require either snapd, dpkg, or rpm" "$err"; then echo "FAIL: missing no-package-manager error" >&2 cat "$err" >&2 || true exit 1