diff --git a/.image-garden.mk b/.image-garden.mk new file mode 100644 index 000000000..5e5bc8599 --- /dev/null +++ b/.image-garden.mk @@ -0,0 +1,63 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# TODO: image-garden maps systems to cloud-init templates by (system, arch) but +# cannot select different templates for the same system name. Until that is +# resolved, we use different cloud-image versions to give each docker +# configuration a distinct cloud-init profile. 22.04, 24.04, 26.04, 12 (bookworm), +# 13 (trixie), and 44 (Fedora). Once spread supports more flexible per-system +# overrides, consolidate back to a single ubuntu-cloud-26.04 definition. + +# snapd-only: snapd is pre-installed, no docker at all. +# install.sh will choose the "snap" install path. +define UBUNTU_22.04_CLOUD_INIT_USER_DATA_TEMPLATE +$(CLOUD_INIT_USER_DATA_TEMPLATE) +- snap wait system seed.loaded +- snap install snapd +packages: +- snapd +endef + +# docker-snap: snapd + docker snap pre-installed. +# install.sh will choose the "snap" install path. +define UBUNTU_24.04_CLOUD_INIT_USER_DATA_TEMPLATE +$(CLOUD_INIT_USER_DATA_TEMPLATE) +- snap wait system seed.loaded +- snap install docker +packages: +- snapd +endef + +# native-docker: snapd + docker installed via apt (deb package). +# install.sh will choose the "classic" install path because native +# docker is detected and snapd is present. +define UBUNTU_26.04_CLOUD_INIT_USER_DATA_TEMPLATE +$(CLOUD_INIT_USER_DATA_TEMPLATE) +packages: +- docker.io +endef + +# snapd + docker snap pre-installed. install.sh detects snapd and finds docker +# snap, choosing the snap install path. Debian users who install snapd to run +# docker as a snap get this configuration. +define DEBIAN_12_CLOUD_INIT_USER_DATA_TEMPLATE +$(CLOUD_INIT_USER_DATA_TEMPLATE) +- snap wait system seed.loaded +- snap install snapd +- snap install docker +packages: +- snapd +endef + +# native docker, no snapd — classic install path. +define DEBIAN_13_CLOUD_INIT_USER_DATA_TEMPLATE +$(CLOUD_INIT_USER_DATA_TEMPLATE) +packages: +- docker.io +endef + +# Clean Fedora image: no snapd, no docker. +# install.sh chooses the classic (rpm) install path. +define FEDORA_44_CLOUD_INIT_USER_DATA_TEMPLATE +$(CLOUD_INIT_USER_DATA_TEMPLATE) +endef diff --git a/.image-garden/.gitignore b/.image-garden/.gitignore new file mode 100644 index 000000000..5e4f04d29 --- /dev/null +++ b/.image-garden/.gitignore @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +*.img +*.iso +*.lock +*.log +*.meta-data +*.qcow2 +*.run +*.user-data diff --git a/docs/about/installation.mdx b/docs/about/installation.mdx index cd9973f13..494ada001 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 exits with an error and recommends installing +via DEB or RPM instead. 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. @@ -75,6 +85,81 @@ To keep the user service running after logout, enable linger: sudo loginctl enable-linger $USER ``` +## Snap + +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 +``` + +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 +``` + +### Connect the required interfaces + +Strict confinement requires explicit `snap connect` for several interfaces. The +installer runs these for you on Snap installs; run them by hand if you install +the snap manually: + +```shell +sudo snap connect openshell:docker docker:docker-daemon +sudo snap connect openshell:log-observe +sudo snap connect openshell:ssh-keys +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 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. + +### 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 https://127.0.0.1:17670 --local --name openshell +openshell status +``` + +The gateway listens on `https://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`. + +### 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 presents temporary limitations +awaiting snapd 2.76 release: `systemd --user` integration for the gateway, or +when you already run Docker Engine from a non-snap source. The installer will +refuse Snap installs on hosts with native Docker — use the DEB or RPM package +instead. + +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). diff --git a/install.sh b/install.sh index 6a8bc3029..c2493fc4e 100755 --- a/install.sh +++ b/install.sh @@ -4,9 +4,10 @@ # # 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. +# Linux prefers the Snap path when snapd is available. Otherwise Linux installs +# 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 @@ -16,11 +17,18 @@ GITHUB_URL="https://github.com/${REPO}" RELEASE_TAG="${OPENSHELL_VERSION:-}" CHECKSUMS_NAME="openshell-checksums-sha256.txt" LOCAL_GATEWAY_PORT="17670" +LOCAL_GATEWAY_PROTOCOL="https" HOMEBREW_TAP="nvidia/openshell" HOMEBREW_FORMULA_NAME="openshell" BREAKING_RELEASE_VERSION="0.0.37" LINUX_PACKAGE_GLIBC_MIN_VERSION="2.31" UPGRADE_NOTICE_ACK="${OPENSHELL_ACK_BREAKING_UPGRADE:-}" +SNAP_PACKAGE_NAME="openshell" +SNAP_DOCKER_SLOT="docker:docker-daemon" +SNAP_DEFAULT_CHANNEL="latest/stable" +SNAP_DEV_CHANNEL="latest/edge" +LINUX_INSTALL_METHOD_SNAP="snap" +LINUX_INSTALL_METHOD_CLASSIC="classic" info() { printf '%s: %s\n' "$APP_NAME" "$*" >&2 @@ -54,13 +62,27 @@ ENVIRONMENT VARIABLES: OPENSHELL_ACK_BREAKING_UPGRADE Set to 1 only after backing up and cleaning up a pre-v0.0.37 installation. + OPENSHELL_INSTALL_METHOD + Linux only. Selects snap or classic. Accepted values: + snap install from the Snap Store + classic install via dpkg or rpm + When unset, snap is used when snapd is available and + running, otherwise the classic package manager is used. + Set OPENSHELL_INSTALL_METHOD=classic on distros where + snapd is not desired. 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. + Linux prefers the snap method when snapd is available. The Snap channel + follows OPENSHELL_VERSION: 'dev' selects ${SNAP_DEV_CHANNEL:-latest/edge}, + tagged releases select ${SNAP_DEFAULT_CHANNEL:-latest/stable}. snapd + manages the gateway as the 'openshell.gateway' service. + + On systems without snapd, 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. EOF @@ -477,6 +499,71 @@ linux_package_method() { fi } +has_snapd() { + if [ "${OPENSHELL_INSTALL_SH_TEST:-0}" = "1" ]; then + case "${OPENSHELL_TEST_SNAPD_AVAILABLE:-0}" in + 1) return 0 ;; + 0) return 1 ;; + esac + fi + + command -v snap >/dev/null 2>&1 || return 1 + [ -S /run/snapd.socket ] || [ -S /var/run/snapd.socket ] || return 1 + return 0 +} + +host_supports_native_package() { + has_cmd dpkg || has_cmd rpm +} + +has_native_docker() { + if [ "${OPENSHELL_INSTALL_SH_TEST:-0}" = "1" ] && [ -n "${OPENSHELL_TEST_NATIVE_DOCKER+x}" ]; then + case "${OPENSHELL_TEST_NATIVE_DOCKER}" in + 1) return 0 ;; + *) return 1 ;; + esac + fi + + command -v docker >/dev/null 2>&1 || return 1 + case "$(command -v docker)" in + */snap/bin/docker | */var/lib/snapd/snap/bin/docker) + return 1 + ;; + esac + return 0 +} + +resolve_linux_install_method() { + _method="${OPENSHELL_INSTALL_METHOD:-}" + if [ -n "$_method" ]; then + case "$_method" in + "$LINUX_INSTALL_METHOD_SNAP" | "$LINUX_INSTALL_METHOD_CLASSIC") + echo "$_method" + return 0 + ;; + *) + error "OPENSHELL_INSTALL_METHOD must be '${LINUX_INSTALL_METHOD_SNAP}' or '${LINUX_INSTALL_METHOD_CLASSIC}' (got: ${_method})" + ;; + esac + fi + + if has_snapd && ! has_native_docker; then + echo "$LINUX_INSTALL_METHOD_SNAP" + elif host_supports_native_package; then + echo "$LINUX_INSTALL_METHOD_CLASSIC" + else + error "OpenShell Linux installs require either snapd or dpkg/rpm" + fi +} + +resolve_snap_channel() { + if [ "${RELEASE_TAG:-}" = "dev" ]; then + echo "$SNAP_DEV_CHANNEL" + else + echo "$SNAP_DEFAULT_CHANNEL" + fi +} + set_linux_target_runtime_dir() { if [ "$(id -u)" -eq "$TARGET_UID" ] && [ -n "${XDG_RUNTIME_DIR:-}" ]; then TARGET_RUNTIME_DIR="$XDG_RUNTIME_DIR" @@ -688,7 +775,7 @@ start_user_gateway() { 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" + info "then register it with: openshell gateway add ${LOCAL_GATEWAY_PROTOCOL}://127.0.0.1:${LOCAL_GATEWAY_PORT} --local --name openshell" return 0 fi @@ -764,14 +851,19 @@ wait_for_local_gateway_listener() { _timeout="${OPENSHELL_INSTALL_GATEWAY_TIMEOUT:-30}" _elapsed=0 _last_output="" - _probe_url="https://127.0.0.1:${LOCAL_GATEWAY_PORT}/" + _probe_url="${LOCAL_GATEWAY_PROTOCOL}://127.0.0.1:${LOCAL_GATEWAY_PORT}/" _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 + if [ "$LOCAL_GATEWAY_PROTOCOL" = "https" ]; then + 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 + elif _last_output="$(as_target_user curl -sS --max-time 2 -o /dev/null "$_probe_url" 2>&1)"; then info "local gateway listener is reachable" return 0 fi @@ -836,7 +928,7 @@ remove_local_gateway_registration() { register_local_gateway() { _register_bin="${OPENSHELL_REGISTER_BIN:-openshell}" - if _add_output="$(as_target_user "$_register_bin" gateway add "https://127.0.0.1:${LOCAL_GATEWAY_PORT}" --local --name openshell 2>&1)"; then + if _add_output="$(as_target_user "$_register_bin" gateway add "${LOCAL_GATEWAY_PROTOCOL}://127.0.0.1:${LOCAL_GATEWAY_PORT}" --local --name openshell 2>&1)"; then [ -z "$_add_output" ] || print_gateway_add_output "$_add_output" return 0 else @@ -847,7 +939,7 @@ register_local_gateway() { *"already exists"*) info "local gateway already exists; removing and re-adding it..." remove_local_gateway_registration - as_target_user "$_register_bin" gateway add "https://127.0.0.1:${LOCAL_GATEWAY_PORT}" --local --name openshell + as_target_user "$_register_bin" gateway add "${LOCAL_GATEWAY_PROTOCOL}://127.0.0.1:${LOCAL_GATEWAY_PORT}" --local --name openshell ;; *) printf '%s\n' "$_add_output" >&2 @@ -859,13 +951,83 @@ register_local_gateway() { print_gateway_add_output() { printf '%s\n' "$1" | while IFS= read -r _line; do case "$_line" in - *"Gateway is not reachable at https://127.0.0.1:${LOCAL_GATEWAY_PORT}"*) ;; + *"Gateway is not reachable at ${LOCAL_GATEWAY_PROTOCOL}://127.0.0.1:${LOCAL_GATEWAY_PORT}"*) ;; *"Verify the gateway is running and the endpoint is correct."*) ;; *) printf '%s\n' "$_line" >&2 ;; esac done } +start_snap_gateway() { + info "waiting for the OpenShell gateway (snap.openshell.gateway) to become ready..." + + _timeout="${OPENSHELL_INSTALL_GATEWAY_TIMEOUT:-60}" + _elapsed=0 + + while [ "$_elapsed" -lt "$_timeout" ]; do + if systemctl is-active --quiet snap.openshell.gateway 2>/dev/null; then + info "openshell.gateway service is running" + break + fi + sleep 1 + _elapsed=$((_elapsed + 1)) + done + + if [ "$_elapsed" -ge "$_timeout" ]; then + info "snap.openshell.gateway did not become ready within ${_timeout}s" + info "check its status with: systemctl status snap.openshell.gateway" + info "then register the gateway with:" + info " openshell gateway add ${LOCAL_GATEWAY_PROTOCOL}://127.0.0.1:${LOCAL_GATEWAY_PORT} --local --name openshell" + return 1 + fi + + info "registering local gateway as ${TARGET_USER}..." + register_local_gateway + wait_for_local_gateway_listener + wait_for_local_gateway_status +} + +install_linux_snap() { + _channel="$(resolve_snap_channel)" + local _snap_installed=false + + _snap_clean() { + if [ "$_snap_installed" = true ]; then + warn "removing ${APP_NAME} snap due to interrupted install..." + as_root snap remove --purge "$SNAP_PACKAGE_NAME" || \ + warn "could not remove ${APP_NAME} snap; please remove it manually" + fi + } + trap '_snap_clean' EXIT INT TERM + + info "pre-installing the Docker snap..." + as_root snap install docker + + info "installing ${APP_NAME} from the Snap Store (channel: ${_channel})..." + as_root snap install "$SNAP_PACKAGE_NAME" --channel="$_channel" + _snap_installed=true + + info "connecting required interfaces..." + as_root snap connect "${SNAP_PACKAGE_NAME}:docker" "$SNAP_DOCKER_SLOT" || \ + warn "could not connect ${SNAP_PACKAGE_NAME}:docker to ${SNAP_DOCKER_SLOT}; the Docker driver will not work until this is fixed" + for _plug in log-observe system-observe ssh-keys; do + as_root snap connect "${SNAP_PACKAGE_NAME}:${_plug}" || \ + warn "could not connect ${SNAP_PACKAGE_NAME}:${_plug}; some features may be limited" + done + + info "restarting OpenShell gateway (snap.openshell.gateway) after interface connections" + systemctl stop snap.openshell.gateway + systemctl start snap.openshell.gateway + + trap - EXIT INT TERM + info "installed ${APP_NAME} from the Snap Store (${_channel})" + info "snapd manages the OpenShell gateway as the 'snap.openshell.gateway' service." + + set_linux_target_runtime_dir + LOCAL_GATEWAY_PROTOCOL="http" + start_snap_gateway || return $? +} + install_linux_deb() { check_linux_deb_platform set_linux_target_runtime_dir @@ -992,7 +1154,7 @@ install_macos_homebrew() { 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 https://127.0.0.1:${LOCAL_GATEWAY_PORT} --local --name openshell" + info "then register it with: openshell gateway add ${LOCAL_GATEWAY_PROTOCOL}://127.0.0.1:${LOCAL_GATEWAY_PORT} --local --name openshell" return 0 fi @@ -1033,16 +1195,23 @@ main() { case "$PLATFORM" in linux) - require_linux_package_glibc - case "$(linux_package_method)" in - deb) - install_linux_deb - ;; - rpm) - install_linux_rpm + case "$(resolve_linux_install_method)" in + "$LINUX_INSTALL_METHOD_SNAP") + install_linux_snap ;; - *) - error "unsupported Linux package method" + "$LINUX_INSTALL_METHOD_CLASSIC") + require_linux_package_glibc + case "$(linux_package_method)" in + deb) + install_linux_deb + ;; + rpm) + install_linux_rpm + ;; + *) + error "unsupported Linux package method" + ;; + esac ;; esac ;; diff --git a/spread.yaml b/spread.yaml new file mode 100644 index 000000000..cf93198b3 --- /dev/null +++ b/spread.yaml @@ -0,0 +1,62 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +project: openshell + +backends: + garden: + type: adhoc + allocate: | + if [ -n "${SPREAD_HOST_PATH-}" ]; then + PATH="${SPREAD_HOST_PATH}" + fi + exec image-garden allocate --spread "$SPREAD_SYSTEM"."$(uname -m)" + discard: | + if [ -n "${SPREAD_HOST_PATH-}" ]; then + PATH="${SPREAD_HOST_PATH}" + fi + image-garden discard "$SPREAD_SYSTEM_ADDRESS" + systems: + - ubuntu-cloud-22.04: + username: root + password: root + - ubuntu-cloud-24.04: + username: root + password: root + - ubuntu-cloud-26.04: + username: root + password: root + - debian-cloud-12: + username: root + password: root + - debian-cloud-13: + username: root + password: root + - fedora-cloud-44: + username: root + password: root + +exclude: + - ".cache/*" + - ".image-garden/*" + - "target/*" + - ".spread-reuse.*.yaml" + +path: /root/openshell + +debug-each: | + echo "Kernel and architecture:" + uname -a + + echo "OS release info:" + cat /etc/os-release + + echo "Docker availability:" + command -v docker >/dev/null 2>&1 && { + docker version --format '{{.Server.Version}}' 2>/dev/null || true + } + command -v snap >/dev/null 2>&1 && snap list docker 2>/dev/null || true + +suites: + tests/install/: + summary: Install smoke tests for OpenShell diff --git a/tasks/scripts/test-install-sh.sh b/tasks/scripts/test-install-sh.sh index 5bf98071a..506c12ac2 100755 --- a/tasks/scripts/test-install-sh.sh +++ b/tasks/scripts/test-install-sh.sh @@ -99,4 +99,135 @@ assert_glibc_preflight_fails \ "OpenShell Linux packages require glibc >= 2.31; detected musl or unsupported libc." \ setup_ldd_musl +assert_eq() { + local name=$1 + local expected=$2 + local actual=$3 + if [ "$expected" != "$actual" ]; then + echo "FAIL: ${name}: expected '${expected}', got '${actual}'" >&2 + exit 1 + fi +} + +# has_snapd: with the test-mode override, OPENSHELL_TEST_SNAPD_AVAILABLE selects +# the result. In real environments the function probes for `snap` and the snapd +# socket, which the test cannot exercise without root or a real snapd. +(export OPENSHELL_TEST_SNAPD_AVAILABLE=1; has_snapd) || { + echo "FAIL: has_snapd with OPENSHELL_TEST_SNAPD_AVAILABLE=1" >&2 + exit 1 +} +(export OPENSHELL_TEST_SNAPD_AVAILABLE=0; has_snapd) && { + echo "FAIL: has_snapd with OPENSHELL_TEST_SNAPD_AVAILABLE=0 should fail" >&2 + exit 1 +} + +# has_native_docker: uses PATH to resolve docker, so we create a mock directory +# with symlinks that make command -v return the desired path. +# Make sure test-mode override is cleared so PATH-based detection is exercised. +unset OPENSHELL_TEST_NATIVE_DOCKER +_real_path="$PATH" +_mock_dir="${tmpdir}/mock-bin" +mkdir -p "$_mock_dir/snap/bin" \ + "$_mock_dir/var/lib/snapd/snap/bin" \ + "$_mock_dir/usr/bin" \ + "${tmpdir}/empty-dir" +# The actual no-op executable lives in the "native" directory +touch "$_mock_dir/usr/bin/docker" && chmod +x "$_mock_dir/usr/bin/docker" +# Symlinks in snap paths so command -v resolves them correctly +ln -s "$_mock_dir/usr/bin/docker" "$_mock_dir/snap/bin/docker" +ln -s "$_mock_dir/usr/bin/docker" "$_mock_dir/var/lib/snapd/snap/bin/docker" + +# No docker in PATH → has_native_docker should fail (return 1) +PATH="${tmpdir}/empty-dir" +if has_native_docker; then + echo "FAIL: has_native_docker without docker should return 1" >&2 + exit 1 +fi + +# docker from /snap/bin → has_native_docker should fail (return 1) +PATH="$_mock_dir/snap/bin" +if has_native_docker; then + echo "FAIL: has_native_docker with snap docker should return 1" >&2 + exit 1 +fi + +# docker from non-snap path → has_native_docker should succeed (return 0) +PATH="$_mock_dir/usr/bin" +if ! has_native_docker; then + echo "FAIL: has_native_docker with native docker should return 0" >&2 + exit 1 +fi + +# docker from /var/lib/snapd/snap/bin → has_native_docker should fail (return 1) +PATH="$_mock_dir/var/lib/snapd/snap/bin" +if has_native_docker; then + echo "FAIL: has_native_docker with snap docker (var path) should return 1" >&2 + exit 1 +fi + +PATH="$_real_path" + +# resolve_snap_channel +export RELEASE_TAG="" +assert_eq "resolve_snap_channel default" "latest/stable" "$(resolve_snap_channel)" +export RELEASE_TAG="dev" +assert_eq "resolve_snap_channel dev" "latest/edge" "$(resolve_snap_channel)" +export RELEASE_TAG="v0.0.37" +assert_eq "resolve_snap_channel tagged" "latest/stable" "$(resolve_snap_channel)" +export RELEASE_TAG="" +unset RELEASE_TAG + +# resolve_linux_install_method: explicit env var wins over the probe +export OPENSHELL_TEST_SNAPD_AVAILABLE=1 +export OPENSHELL_INSTALL_METHOD=snap +assert_eq "resolve_linux_install_method snap explicit" "snap" "$(resolve_linux_install_method)" +export OPENSHELL_INSTALL_METHOD=classic +assert_eq "resolve_linux_install_method classic explicit overrides snap probe" "classic" "$(resolve_linux_install_method)" +export OPENSHELL_TEST_SNAPD_AVAILABLE=0 +export OPENSHELL_INSTALL_METHOD=classic +assert_eq "resolve_linux_install_method classic explicit" "classic" "$(resolve_linux_install_method)" +unset OPENSHELL_INSTALL_METHOD + +# resolve_linux_install_method: snap when available, no native docker +export OPENSHELL_TEST_SNAPD_AVAILABLE=1 +unset OPENSHELL_TEST_NATIVE_DOCKER OPENSHELL_INSTALL_METHOD +assert_eq "resolve_linux_install_method snap auto" "snap" "$(resolve_linux_install_method)" + +# resolve_linux_install_method: snapd present + native docker → fallback to classic +export OPENSHELL_TEST_SNAPD_AVAILABLE=1 +export OPENSHELL_TEST_NATIVE_DOCKER=1 +unset OPENSHELL_INSTALL_METHOD +assert_eq "resolve_linux_install_method snapd + native docker → classic" "classic" "$(resolve_linux_install_method)" +unset OPENSHELL_TEST_NATIVE_DOCKER + +# resolve_linux_install_method: invalid env var exits non-zero +export OPENSHELL_INSTALL_METHOD=invalid +_snap_err="${tmpdir}/snap-err" +if (resolve_linux_install_method) >/dev/null 2>"$_snap_err"; then + echo "FAIL: resolve_linux_install_method should fail on invalid value" >&2 + exit 1 +fi +if ! grep -Fq "OPENSHELL_INSTALL_METHOD must be" "$_snap_err"; then + echo "FAIL: resolve_linux_install_method: missing expected error message" >&2 + cat "$_snap_err" >&2 || true + exit 1 +fi +unset OPENSHELL_INSTALL_METHOD + +# resolve_linux_install_method: snap absent + clean PATH (no dpkg/rpm) exits non-zero +export OPENSHELL_TEST_SNAPD_AVAILABLE=0 +_real_path="$PATH" +_clean_path="${tmpdir}/clean-path" +mkdir -p "$_clean_path" +PATH="$_clean_path" +unset OPENSHELL_INSTALL_METHOD +if (resolve_linux_install_method) >/dev/null 2>"$_snap_err"; then + PATH="$_real_path" + echo "FAIL: resolve_linux_install_method should fail without snap or native" >&2 + exit 1 +fi +PATH="$_real_path" +export OPENSHELL_TEST_SNAPD_AVAILABLE=1 +unset OPENSHELL_INSTALL_METHOD + echo "install.sh libc preflight tests passed" diff --git a/tests/install/install-with-classic-docker/task.yaml b/tests/install/install-with-classic-docker/task.yaml new file mode 100644 index 000000000..13fc5039a --- /dev/null +++ b/tests/install/install-with-classic-docker/task.yaml @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +summary: Install via deb or rpm (classic install path) +details: | + This test installs OpenShell via the classic install path (deb or rpm + packages, not snap). The installer chooses this path when snapd is + absent or when snapd is present alongside native docker. On Ubuntu + 26.04 the system has snapd and native docker, choosing the deb path. + Debian 13 has native docker but no snapd, also choosing the deb path. + Fedora 44 has neither snapd nor docker and installs via RPM packages + using dnf. This verifies that the classic install path, user gateway + service, and gateway registration work correctly across all three + package ecosystems. + +systems: + # Cloud-init templates are assigned per system name by image-garden. + # Each system must have a unique cloud-init profile, so we pin each + # test to the system whose cloud-init matches its requirements. + - ubuntu-cloud-26.04 + - debian-cloud-13 + - fedora-cloud-44 + +prepare: | + mkdir -p "$HOME/.local/bin" + export PATH="$HOME/.local/bin:$PATH" + cat "$SPREAD_PATH/install.sh" | sh + +execute: | + which openshell | NOMATCH "/snap/" + openshell status | sed 's/\x1b\[[0-9;]*m//g' | MATCH "Status: Connected" diff --git a/tests/install/install-with-docker-snap/task.yaml b/tests/install/install-with-docker-snap/task.yaml new file mode 100644 index 000000000..44b1872be --- /dev/null +++ b/tests/install/install-with-docker-snap/task.yaml @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +summary: Install via snap with docker snap pre-installed +details: | + This test installs OpenShell on a system with snapd and the docker + snap pre-installed. The installer detects snapd and chooses the snap + install path (the docker snap is already present, so no snap install + of docker is needed). This verifies the same snap install path but + with a pre-existing docker snap that the openshell snap can connect + to. Runs on Ubuntu 24.04 and Debian 12 (where snapd is installed via + apt so users can run docker as a snap). + +systems: + # Cloud-init templates are assigned per system name by image-garden. + # Each system must have a unique cloud-init profile, so we pin each + # test to the system whose cloud-init matches its requirements. + - ubuntu-cloud-24.04 + - debian-cloud-12 + +prepare: | + cat "$SPREAD_PATH/install.sh" | sh + +execute: | + which openshell | MATCH "/snap/bin/openshell" + openshell status | sed 's/\x1b\[[0-9;]*m//g' | MATCH "Status: Connected" diff --git a/tests/install/install-without-docker/task.yaml b/tests/install/install-without-docker/task.yaml new file mode 100644 index 000000000..da61d8ce8 --- /dev/null +++ b/tests/install/install-without-docker/task.yaml @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +summary: Install via snap on snapd-only system +details: | + This test installs OpenShell on a system with only snapd (no docker). + The installer detects snapd and no classic docker, choosing the snap + install path. This verifies that the snap can be installed, its + interfaces connected, and the gateway registered. + +# Each task runs on one or more systems because cloud-init profiles are +# assigned per system name by image-garden. We cannot have multiple +# cloud-init variants for the same system definition, so each test pins +# to the system whose cloud-init profile matches its requirements. +systems: + - ubuntu-cloud-22.04 + +prepare: | + cat "$SPREAD_PATH/install.sh" | sh + +execute: | + which openshell | MATCH "/snap/bin/openshell" + openshell status | sed 's/\x1b\[[0-9;]*m//g' | MATCH "Status: Connected"