Files
OpenShell/tasks/scripts/test-install-sh.sh
Oliver Calder 82bc756545 feat(snap): run gateway as a user service by default (#4099)
* feat(snap): run gateway as a user service by default

Replace the existing system `gateway` service with separate
`user-gateway` and `system-gateway` services. For new installs, only the
`user-gateway` service is enabled. For existing installs, only the
`system-gateway` service is enabled.

Existing installs continue to have a one-time migration which removes
legacy insecure configurations. It is up to `install.sh` or users to
manually copy mTLS credentials from the root-owned `$SNAP_COMMON/tls` to
the invoking user's OpenShell snap directory. This is explained in the
snap description in `snapcraft.yaml`, visible in the Snap Store listing
and via `snap info openshell`.

Service enablement is now managed by a new snap configuration option
named `gateway-mode`, so users can switch from one mode to another via
e.g. `sudo snap set openshell gateway-mode=user`. The `install` and
`post-refresh` hooks select which service to start by setting this mode.
If the `gateway-mode` is already set, then we know the one-time
migration has already taken place.

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* fix(snap): simplify the snap gateway wrapper

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* fix(snap): use `snap start --user` when running as non-root

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* fix(snap): ensure install.sh waits for the correct docker daemon

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* feat(snap): document how to safely switch between gateway modes

New installs run the gateway as a user service by default, while
upgrades of existing installs from prior to the user gateway continue to
use the existing system gateway. The user and system gateway are
fundamentally different services and there is no way to migrate state
between them.

However, it may be desirable for a user to run the gateway as a system
service after a fresh install. To support this, this commit adds
documentation for how to safely transition from the user gateway mode to
the system gateway mode, and vice versa.

Also, clarifies when it is necessary to manually copy TLS credentials
from the system gateway state directory to the user's home directory:
when upgrading a pre-TLS snap installation, or when switching from user
to system gateway mode. When upgrading an existing install with an HTTPS
gateway running as a system service, copying credentials again is not
necessary.

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

---------

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
2026-10-09 11:39:08 +00:00

988 lines
31 KiB
Bash
Executable File

#!/usr/bin/env bash
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
out="${tmpdir}/out"
err="${tmpdir}/err"
export OPENSHELL_INSTALL_SH_TEST=1
# shellcheck source=../../install.sh
. "${ROOT}/install.sh"
assert_glibc_preflight_passes() {
local name=$1
local ldd_output=$2
if ! (export OPENSHELL_TEST_GETCONF_UNAVAILABLE=1 OPENSHELL_TEST_LDD_OUTPUT="$ldd_output"; require_linux_package_glibc) >"$out" 2>"$err"; then
echo "FAIL: ${name}" >&2
cat "$err" >&2 || true
exit 1
fi
}
assert_glibc_preflight_fails() {
local name=$1
local expected=$2
local setup=$3
if ("$setup"; require_linux_package_glibc) >"$out" 2>"$err"; then
echo "FAIL: ${name}: expected failure" >&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
}
setup_glibc_227() {
export OPENSHELL_TEST_GETCONF_UNAVAILABLE=1
export OPENSHELL_TEST_LDD_OUTPUT="ldd (GNU libc) 2.27"
}
setup_missing_glibc() {
export OPENSHELL_TEST_GETCONF_UNAVAILABLE=1
export OPENSHELL_TEST_LDD_UNAVAILABLE=1
}
setup_getconf_musl() {
export OPENSHELL_TEST_LDD_UNAVAILABLE=1
export OPENSHELL_TEST_GETCONF_OUTPUT="musl libc"
}
setup_ldd_musl() {
export OPENSHELL_TEST_GETCONF_UNAVAILABLE=1
export OPENSHELL_TEST_LDD_OUTPUT="musl libc (x86_64)"
}
assert_glibc_preflight_passes "glibc 2.28 passes" "glibc 2.28"
assert_glibc_preflight_passes "glibc 2.31 passes" "glibc 2.31"
assert_glibc_preflight_passes "glibc 2.35 passes" "ldd (GNU libc) 2.35"
if ! (export OPENSHELL_TEST_LDD_UNAVAILABLE=1 OPENSHELL_TEST_GETCONF_OUTPUT="glibc 2.35"; require_linux_package_glibc) >"$out" 2>"$err"; then
echo "FAIL: getconf glibc fallback passes" >&2
cat "$err" >&2 || true
exit 1
fi
if ! (export OPENSHELL_TEST_LDD_OUTPUT="not ldd" OPENSHELL_TEST_GETCONF_OUTPUT="glibc 2.35"; require_linux_package_glibc) >"$out" 2>"$err"; then
echo "FAIL: unparseable ldd output falls back to getconf" >&2
cat "$err" >&2 || true
exit 1
fi
assert_glibc_preflight_fails \
"glibc 2.27 fails" \
"OpenShell Linux packages require glibc >= 2.28; detected glibc 2.27." \
setup_glibc_227
assert_glibc_preflight_fails \
"missing glibc detection fails" \
"OpenShell Linux packages require glibc >= 2.28; could not detect glibc." \
setup_missing_glibc
assert_glibc_preflight_fails \
"musl detection fails" \
"OpenShell Linux packages require glibc >= 2.28; detected musl or unsupported libc." \
setup_getconf_musl
assert_glibc_preflight_fails \
"ldd musl fallback fails" \
"OpenShell Linux packages require glibc >= 2.28; detected musl or unsupported libc." \
setup_ldd_musl
# snap_state: 0 = no snap command, 1 = snap command only,
# installed = the OpenShell snap is already installed.
assert_linux_package_method() {
local name=$1
local install_method=$2
local requested_version=$3
local snap_state=$4
local dpkg_present=$5
local rpm_present=$6
local expected=$7
local actual
(
export OPENSHELL_INSTALL_METHOD="$install_method"
export OPENSHELL_VERSION="$requested_version"
has_cmd() {
case "$1" in
snap) [ "$snap_state" != "0" ] ;;
dpkg) [ "$dpkg_present" = "1" ] ;;
rpm) [ "$rpm_present" = "1" ] ;;
*) return 1 ;;
esac
}
snap() { [ "$*" = "list openshell" ] && [ "$snap_state" = "installed" ]; }
linux_package_method
) >"$out"
actual="$(cat "$out")"
if [ "$actual" != "$expected" ]; then
echo "FAIL: ${name}: expected ${expected}, got ${actual}" >&2
exit 1
fi
}
assert_linux_package_method "deb is the default despite snap" "" "" 1 1 1 deb
assert_linux_package_method "rpm is the default despite snap" "" "" 1 0 1 rpm
assert_linux_package_method "dev uses deb despite snap" "" dev 1 1 1 deb
assert_linux_package_method "snap is opt-in" snap "" 1 1 1 snap
assert_linux_package_method "snap opt-in with dev" snap dev 1 1 1 snap
assert_linux_package_method "explicit deb" deb "" installed 0 1 deb
assert_linux_package_method "explicit rpm" rpm "" 1 1 1 rpm
assert_linux_package_method "existing snap install keeps refreshing" "" "" installed 1 1 snap
assert_linux_package_method "existing snap install keeps refreshing dev" "" dev installed 1 1 snap
assert_linux_package_method "pre uses deb despite existing snap" "" pre installed 1 1 deb
assert_linux_package_method "numbered prerelease uses deb despite existing snap" "" v0.1.0-pre.3 installed 1 1 deb
assert_linux_package_method "pinned stable uses rpm despite existing snap" "" v1.2.3 installed 0 1 rpm
assert_linux_package_method "deb is selected without snap" "" "" 0 1 1 deb
assert_linux_package_method "rpm is selected without snap or deb" "" "" 0 0 1 rpm
if (OPENSHELL_INSTALL_METHOD=flatpak linux_package_method) >"$out" 2>"$err"; then
echo "FAIL: unsupported OPENSHELL_INSTALL_METHOD should be rejected" >&2
exit 1
fi
if ! grep -Fq "unsupported OPENSHELL_INSTALL_METHOD=flatpak" "$err"; then
echo "FAIL: unsupported OPENSHELL_INSTALL_METHOD was not explained" >&2
cat "$err" >&2
exit 1
fi
if ! (
find_existing_native_openshell_bin() { return 1; }
guard_native_to_snap_transition
) >"$out" 2>"$err"; then
echo "FAIL: Snap install without an existing native installation should continue" >&2
cat "$err" >&2 || true
exit 1
fi
assert_native_to_snap_blocked() {
local name=$1
local version=$2
if (
find_existing_native_openshell_bin() { printf '%s\n' /usr/bin/openshell; }
existing_openshell_version() { printf '%s\n' "$version"; }
UPGRADE_NOTICE_ACK=""
guard_native_to_snap_transition
) >"$out" 2>"$err"; then
echo "FAIL: ${name}: expected native-to-Snap transition to be blocked" >&2
exit 1
fi
if ! grep -Fq "detected existing non-snap OpenShell ${version} at /usr/bin/openshell" "$err"; then
echo "FAIL: ${name}: missing native installation warning" >&2
cat "$err" >&2 || true
exit 1
fi
if ! grep -Fq "does not import state from a non-snap installation" "$err"; then
echo "FAIL: ${name}: missing isolated Snap state explanation" >&2
cat "$err" >&2 || true
exit 1
fi
}
assert_native_to_snap_blocked "old native install" v0.0.36
assert_native_to_snap_blocked "current native install" v1.2.3
if ! (
find_existing_native_openshell_bin() { printf '%s\n' /usr/local/bin/openshell; }
existing_openshell_version() { printf '%s\n' v1.2.3; }
UPGRADE_NOTICE_ACK=1
guard_native_to_snap_transition
) >"$out" 2>"$err"; then
echo "FAIL: acknowledged native-to-Snap transition should continue" >&2
cat "$err" >&2 || true
exit 1
fi
if ! grep -Fq "continuing because OPENSHELL_ACK_BREAKING_UPGRADE=1 is set" "$err"; then
echo "FAIL: acknowledged native-to-Snap transition was not reported" >&2
cat "$err" >&2 || true
exit 1
fi
if (
RELEASE_TAG=""
target_uses_breaking_gateway_model
); then
echo "FAIL: Snap targets must not use the version-boundary upgrade guard" >&2
exit 1
fi
out="$(mktemp)"
err="$(mktemp)"
if ! OPENSHELL_VERSION=dev openshell_snap_channel >"$out" 2>"$err"; then
echo "FAIL: dev must select the latest/edge Snap channel" >&2
cat "$err" >&2 || true
exit 1
fi
if [ "$(cat "$out")" != "latest/edge" ]; then
echo "FAIL: dev must select the latest/edge Snap channel" >&2
exit 1
fi
if [ -s "$err" ]; then
echo "FAIL: dev must not warn about an ignored version" >&2
cat "$err" >&2 || true
exit 1
fi
if ! OPENSHELL_VERSION="" openshell_snap_channel >"$out" 2>"$err"; then
echo "FAIL: unset OPENSHELL_VERSION must select the latest/stable Snap channel" >&2
cat "$err" >&2 || true
exit 1
fi
if [ "$(cat "$out")" != "latest/stable" ]; then
echo "FAIL: unset OPENSHELL_VERSION must select the latest/stable Snap channel" >&2
exit 1
fi
if [ -s "$err" ]; then
echo "FAIL: unset OPENSHELL_VERSION must not warn about an ignored version" >&2
cat "$err" >&2 || true
exit 1
fi
if (OPENSHELL_VERSION=v1.2.3 openshell_snap_channel) >"$out" 2>"$err"; then
echo "FAIL: pinned release must not select a Snap channel" >&2
exit 1
fi
if ! grep -Fq "Snap installs do not support OPENSHELL_VERSION=v1.2.3" "$err"; then
echo "FAIL: pinned release Snap rejection was not explained" >&2
cat "$err" >&2
exit 1
fi
rm -f "$out" "$err"
assert_snap_install_flow() {
local name=$1
local docker_present=$2
local openshell_present=$3
local requested_version=$4
local expected=$5
local calls
(
has_cmd() {
case "$1" in
snap) return 0 ;;
docker) [ "$docker_present" = "1" ] ;;
*) command -v "$1" >/dev/null 2>&1 ;;
esac
}
snap() {
case "${1:-}:${2:-}" in
list:docker) return 1 ;;
list:openshell) [ "$openshell_present" = "1" ] ;;
get:openshell) printf '%s\n' system ;;
*) command snap "$@" ;;
esac
}
as_root() {
if [ "${1:-}:${2:-}" = snap:get ]; then
shift
snap "$@"
else
printf 'root:%s\n' "$*"
fi
}
as_target_user() { printf 'target:%s\n' "$*"; }
set_linux_target_runtime_dir() { :; }
wait_for_docker_daemon() { printf '%s\n' "wait:docker"; }
wait_for_user_docker_daemon() { printf '%s\n' "wait:user-docker"; }
snap_gateway_mode() {
if [ "$openshell_present" = 1 ]; then
printf '%s\n' system
else
printf '%s\n' user
fi
}
register_system_snap_gateway() { printf '%s\n' "register:system-gateway"; }
register_user_snap_gateway() { printf '%s\n' "register:user-gateway"; }
wait_for_system_snap_gateway_listener() { printf '%s\n' "wait:system-gateway-listener"; }
wait_for_user_snap_gateway_listener() { printf '%s\n' "wait:user-gateway-listener"; }
wait_for_local_gateway_status() { printf '%s\n' "wait:gateway-status"; }
info() { :; }
export TARGET_USER=test-user
export OPENSHELL_VERSION="$requested_version"
install_linux_snap
) >"$out"
calls="$(cat "$out")"
if [ "$calls" != "$expected" ]; then
echo "FAIL: ${name}: unexpected command sequence" >&2
echo "Expected:" >&2
printf '%s\n' "$expected" >&2
echo "Actual:" >&2
printf '%s\n' "$calls" >&2
exit 1
fi
}
assert_snap_user_refresh_flow() {
local calls
(
has_cmd() {
case "$1" in
snap | docker) return 0 ;;
*) command -v "$1" >/dev/null 2>&1 ;;
esac
}
snap() {
case "${1:-}:${2:-}" in
list:docker) return 1 ;;
list:openshell) return 0 ;;
get:openshell) printf '%s\n' user ;;
*) command snap "$@" ;;
esac
}
as_root() {
if [ "${1:-}:${2:-}" = snap:get ]; then
shift
snap "$@"
else
printf 'root:%s\n' "$*"
fi
}
as_target_user() { printf 'target:%s\n' "$*"; }
set_linux_target_runtime_dir() { :; }
wait_for_user_docker_daemon() { printf '%s\n' "wait:user-docker"; }
snap_gateway_mode() { printf '%s\n' user; }
wait_for_user_snap_gateway_listener() { printf '%s\n' "wait:user-gateway-listener"; }
register_user_snap_gateway() { printf '%s\n' "register:user-gateway"; }
wait_for_local_gateway_status() { printf '%s\n' "wait:gateway-status"; }
info() { :; }
export TARGET_USER=test-user
install_linux_snap
) >"$out"
calls="$(cat "$out")"
expected="wait:user-docker
root:snap refresh openshell --channel=latest/stable
root:snap restart openshell.user-gateway
wait:user-gateway-listener
register:user-gateway
wait:gateway-status"
if [ "$calls" != "$expected" ]; then
echo "FAIL: existing user-mode Snap refresh used the wrong service" >&2
printf 'Expected:\n%s\nActual:\n%s\n' "$expected" "$calls" >&2
exit 1
fi
}
assert_snap_install_flow \
"existing Docker is reused" \
1 0 "" \
"wait:user-docker
root:snap install openshell --channel=latest/stable
wait:user-gateway-listener
register:user-gateway
wait:gateway-status"
assert_snap_user_refresh_flow
assert_snap_install_flow \
"existing OpenShell snap is refreshed" \
1 1 "" \
"wait:docker
root:snap refresh openshell --channel=latest/stable
root:snap restart openshell.system-gateway
wait:system-gateway-listener
register:system-gateway
wait:gateway-status"
if (
has_cmd() {
case "$1" in
snap) return 0 ;;
docker) return 1 ;;
*) command -v "$1" >/dev/null 2>&1 ;;
esac
}
snap() {
case "${1:-}:${2:-}" in
list:docker) return 1 ;;
list:openshell) return 0 ;;
get:openshell) printf '%s\n' disable ;;
*) echo "FAIL: disabled mode reached snap command: $*" >&2; return 99 ;;
esac
}
as_root() {
if [ "${1:-}:${2:-}" = snap:get ]; then
shift
snap "$@"
else
echo "FAIL: disabled mode reached root command: $*" >&2
return 99
fi
}
set_linux_target_runtime_dir() { :; }
install_linux_snap
) >"$out" 2>"$err"; then
echo "FAIL: disabled Snap gateway mode should block installation" >&2
exit 1
fi
if ! grep -Fq "OpenShell snap gateway mode is disabled" "$err"; then
echo "FAIL: disabled Snap gateway mode missing recovery guidance" >&2
cat "$err" >&2 || true
exit 1
fi
if grep -Fq "disabled mode reached" "$err"; then
cat "$err" >&2 || true
exit 1
fi
assert_snap_install_rejected() {
local name=$1
local docker_present=$2
local docker_snap_present=$3
local expected=$4
if (
has_cmd() {
case "$1" in
snap) return 0 ;;
docker) [ "$docker_present" = "1" ] ;;
*) command -v "$1" >/dev/null 2>&1 ;;
esac
}
snap() {
if [ "${1:-}:${2:-}" = list:docker ]; then
[ "$docker_snap_present" = "1" ]
else
echo "FAIL: ${name}: unexpected snap command: $*" >&2
return 99
fi
}
as_root() {
echo "FAIL: ${name}: installation reached root command: $*" >&2
return 99
}
set_linux_target_runtime_dir() { :; }
install_linux_snap
) >"$out" 2>"$err"; then
echo "FAIL: ${name}: Snap installation should have been rejected" >&2
exit 1
fi
if ! grep -Fq "$expected" "$err"; then
echo "FAIL: ${name}: missing rejection message" >&2
cat "$err" >&2 || true
exit 1
fi
if grep -Fq "installation reached root command" "$err"; then
cat "$err" >&2 || true
exit 1
fi
}
assert_snap_install_rejected \
"missing Docker" \
0 0 \
"Docker is required before installing the OpenShell snap"
assert_snap_install_rejected \
"Docker snap" \
1 1 \
"the Docker snap is not currently compatible with OpenShell"
attempts_file="${tmpdir}/docker-attempts"
root_probes_file="${tmpdir}/docker-root-probes"
printf '0\n' >"$attempts_file"
printf '0\n' >"$root_probes_file"
if ! (
env() {
if [ "$*" != "-u DOCKER_HOST -u DOCKER_CONTEXT docker --host unix:///var/run/docker.sock info" ]; then
echo "unexpected Docker readiness probe: $*" >&2
return 1
fi
attempts="$(cat "$attempts_file")"
attempts=$((attempts + 1))
printf '%s\n' "$attempts" >"$attempts_file"
[ "$attempts" -ge 3 ]
}
as_root() {
root_probes="$(cat "$root_probes_file")"
printf '%s\n' "$((root_probes + 1))" >"$root_probes_file"
"$@"
}
sleep() { :; }
info() { :; }
OPENSHELL_INSTALL_DOCKER_TIMEOUT=3 wait_for_docker_daemon
) >"$out" 2>"$err"; then
echo "FAIL: Docker readiness should succeed after retries" >&2
cat "$err" >&2 || true
exit 1
fi
if [ "$(cat "$attempts_file")" != "3" ]; then
echo "FAIL: Docker readiness did not retry three times" >&2
exit 1
fi
if [ "$(cat "$root_probes_file")" != "3" ]; then
echo "FAIL: Docker readiness probes did not run through as_root" >&2
exit 1
fi
if (
env() { return 1; }
as_root() { "$@"; }
snap() { return 1; }
sleep() { :; }
info() { :; }
OPENSHELL_INSTALL_DOCKER_TIMEOUT=2 wait_for_docker_daemon
) >"$out" 2>"$err"; then
echo "FAIL: Docker readiness timeout should fail" >&2
exit 1
fi
if ! grep -Fq "Docker daemon at /var/run/docker.sock did not become reachable within 2s" "$err"; then
echo "FAIL: missing Docker readiness timeout message" >&2
cat "$err" >&2 || true
exit 1
fi
user_attempts_file="${tmpdir}/docker-user-attempts"
user_probes_file="${tmpdir}/docker-user-probes"
printf '0\n' >"$user_attempts_file"
printf '0\n' >"$user_probes_file"
if ! (
env() {
if [ "$*" != "-u DOCKER_HOST -u DOCKER_CONTEXT docker --host unix:///var/run/docker.sock info" ]; then
echo "unexpected user Docker readiness probe: $*" >&2
return 1
fi
attempts="$(cat "$user_attempts_file")"
attempts=$((attempts + 1))
printf '%s\n' "$attempts" >"$user_attempts_file"
[ "$attempts" -ge 3 ]
}
as_target_user() {
user_probes="$(cat "$user_probes_file")"
printf '%s\n' "$((user_probes + 1))" >"$user_probes_file"
"$@"
}
sleep() { :; }
info() { :; }
TARGET_USER=test-user
export DOCKER_HOST=tcp://remote.example:2376
export DOCKER_CONTEXT=remote
OPENSHELL_INSTALL_DOCKER_TIMEOUT=3 wait_for_user_docker_daemon
) >"$out" 2>"$err"; then
echo "FAIL: user Docker readiness should succeed after retries" >&2
cat "$err" >&2 || true
exit 1
fi
if [ "$(cat "$user_attempts_file")" != "3" ]; then
echo "FAIL: user Docker readiness did not retry three times" >&2
exit 1
fi
if [ "$(cat "$user_probes_file")" != "3" ]; then
echo "FAIL: user Docker readiness probes did not run through as_target_user" >&2
exit 1
fi
if (
env() { return 1; }
as_target_user() { "$@"; }
sleep() { :; }
info() { :; }
TARGET_USER=test-user
OPENSHELL_INSTALL_DOCKER_TIMEOUT=2 wait_for_user_docker_daemon
) >"$out" 2>"$err"; then
echo "FAIL: user Docker readiness timeout should fail" >&2
exit 1
fi
if ! grep -Fq "Docker daemon at /var/run/docker.sock did not become reachable as test-user within 2s" "$err"; then
echo "FAIL: missing user Docker readiness timeout message" >&2
cat "$err" >&2 || true
exit 1
fi
if ! grep -Fq '/snap/bin/openshell' "${ROOT}/install.sh"; then
echo "FAIL: Snap installs must use the Snap CLI explicitly" >&2
exit 1
fi
registration_calls_file="${tmpdir}/registration-calls"
: >"$registration_calls_file"
if ! (
as_target_user() { printf 'target:%s\n' "$*" >>"$registration_calls_file"; }
copy_snap_client_bundle() { printf 'copy:client-bundle\n' >>"$registration_calls_file"; }
print_gateway_add_output() { :; }
info() { :; }
TARGET_USER=test-user
snap_gateway_uses_mtls() { return 0; }
register_system_snap_gateway
) >"$out" 2>"$err"; then
echo "FAIL: Snap gateway registration should succeed" >&2
cat "$err" >&2 || true
exit 1
fi
registration_calls="$(cat "$registration_calls_file")"
if [ "$registration_calls" != "copy:client-bundle
target:/snap/bin/openshell gateway add https://127.0.0.1:17670 --local --name openshell" ]; then
echo "FAIL: mTLS Snap gateway registration must copy the client bundle and use HTTPS" >&2
printf '%s\n' "$registration_calls" >&2
exit 1
fi
: >"$registration_calls_file"
if ! (
as_target_user() { printf 'target:%s\n' "$*" >>"$registration_calls_file"; }
copy_snap_client_bundle() { printf 'copy:client-bundle\n' >>"$registration_calls_file"; }
print_gateway_add_output() { :; }
snap_gateway_uses_mtls() { return 1; }
register_system_snap_gateway
) >"$out" 2>"$err"; then
echo "FAIL: legacy plaintext Snap gateway registration should succeed" >&2
cat "$err" >&2 || true
exit 1
fi
registration_calls="$(cat "$registration_calls_file")"
if [ "$registration_calls" != "target:/snap/bin/openshell gateway add http://127.0.0.1:17670 --local --name openshell" ]; then
echo "FAIL: legacy Snap gateway registration must use HTTP without copying certificates" >&2
printf '%s\n' "$registration_calls" >&2
exit 1
fi
if ! grep -Fq "without client authentication" "$err"; then
echo "FAIL: legacy Snap gateway registration must warn about unauthenticated access" >&2
exit 1
fi
assert_snap_listener_probe() {
local name=$1
local uses_mtls=$2
local expected=$3
local actual
actual="$(
as_root() { printf 'root:'; "$@"; }
curl() { printf '%s\n' "$*"; }
snap_gateway_uses_mtls() { [ "$uses_mtls" = "1" ]; }
info() { :; }
OPENSHELL_SNAP_TLS_DIR=/tls
wait_for_system_snap_gateway_listener >/dev/null
printf '%s\n' "$_last_output"
)"
if [ "$actual" != "$expected" ]; then
echo "FAIL: ${name}: expected ${expected}, got ${actual}" >&2
exit 1
fi
}
assert_snap_listener_probe "mTLS snap probes HTTPS with the client bundle as root" 1 \
"root:-sS --max-time 2 --cacert /tls/ca.crt --cert /tls/client/tls.crt --key /tls/client/tls.key -o /dev/null https://127.0.0.1:17670/"
assert_snap_listener_probe "legacy snap probes plaintext HTTP" 0 \
"-sS --max-time 2 -o /dev/null http://127.0.0.1:17670/"
snap_tls_src="${tmpdir}/snap-tls"
mkdir -p "${snap_tls_src}/client"
printf 'ca\n' >"${snap_tls_src}/ca.crt"
printf 'cert\n' >"${snap_tls_src}/client/tls.crt"
printf 'key\n' >"${snap_tls_src}/client/tls.key"
snap_user_home="${tmpdir}/snap-user-home"
mkdir -p "${snap_user_home}/snap/openshell/common/.local/state/openshell/tls/client"
printf 'old key\n' >"${snap_user_home}/snap/openshell/common/.local/state/openshell/tls/client/tls.key"
chmod 644 "${snap_user_home}/snap/openshell/common/.local/state/openshell/tls/client/tls.key"
(
as_root() { "$@"; }
as_target_user() { "$@"; }
TARGET_HOME="$snap_user_home"
OPENSHELL_SNAP_TLS_DIR="$snap_tls_src" copy_snap_client_bundle
)
snap_user_tls="${snap_user_home}/snap/openshell/common/.local/state/openshell/tls"
for file in ca.crt client/tls.crt client/tls.key; do
if ! cmp -s "${snap_tls_src}/${file}" "${snap_user_tls}/${file}"; then
echo "FAIL: Snap client bundle copy missing ${file}" >&2
exit 1
fi
if [[ -z $(find "${snap_user_tls}/${file}" -perm 600) ]]; then
echo "FAIL: Snap client bundle ${file} must be mode 0600" >&2
exit 1
fi
done
if [[ -z $(find "$snap_user_tls" -maxdepth 0 -perm 700) ]]; then
echo "FAIL: Snap client bundle directory must be mode 0700" >&2
exit 1
fi
assert_user_gateway_service_failed() {
local name=$1
local unit_state=$2
local expected=$3
local actual=0
(
as_target_user() { printf '%s\n' "$unit_state"; }
user_gateway_service_failed
) >/dev/null || actual=$?
if [ "$actual" != "$expected" ]; then
echo "FAIL: ${name}: expected status ${expected}, got ${actual}" >&2
exit 1
fi
}
assert_user_gateway_service_failed "failed unit" $'ActiveState=failed\nSubState=failed' 0
assert_user_gateway_service_failed "unit restarting after a failure" $'ActiveState=activating\nSubState=auto-restart' 0
assert_user_gateway_service_failed "unit starting after a failure" $'ActiveState=activating\nSubState=start' 1
assert_user_gateway_service_failed "running unit" $'ActiveState=active\nSubState=running' 1
if (
as_target_user() { return 1; }
user_gateway_service_failed
); then
echo "FAIL: unreachable user systemd must not count as a failed unit" >&2
exit 1
fi
# Runs the listener wait against an unreachable gateway and prints the number
# of one second waits. The wait is expected to fail.
run_listener_wait() {
local unit_state=$1
local sleeps_file="${tmpdir}/listener-sleeps"
shift
: >"$sleeps_file"
(
as_target_user() {
case "$1" in
systemctl) printf '%s\n' "$unit_state" ;;
*) return 7 ;;
esac
}
sleep() { printf '.' >>"$sleeps_file"; }
info() { :; }
dump_local_gateway_diagnostics() { echo "gateway diagnostics" >&2; }
TARGET_HOME="${tmpdir}/listener-home"
PLATFORM=linux
OPENSHELL_INSTALL_GATEWAY_TIMEOUT=5 wait_for_local_gateway_listener "$@"
) >"$out" 2>"$err" && return 1
wc -c <"$sleeps_file" | tr -d ' '
}
listener_mtls_dir="${tmpdir}/listener-home/.config/openshell/gateways/openshell/mtls"
mkdir -p "$listener_mtls_dir"
: >"${listener_mtls_dir}/ca.crt"
: >"${listener_mtls_dir}/tls.crt"
: >"${listener_mtls_dir}/tls.key"
restarting_unit=$'ActiveState=activating\nSubState=auto-restart'
running_unit=$'ActiveState=active\nSubState=running'
if [ "$(run_listener_wait "$restarting_unit" user_gateway_service_failed)" != "0" ]; then
echo "FAIL: a failed gateway service must stop the listener wait immediately" >&2
exit 1
fi
if [ "$(tail -n 1 "$err")" != "openshell: error: the openshell-gateway service failed to start; fix the cause shown above, then run: systemctl --user restart openshell-gateway" ]; then
echo "FAIL: a failed gateway service must end with the service error" >&2
cat "$err" >&2
exit 1
fi
if ! grep -Fq "gateway diagnostics" "$err"; then
echo "FAIL: a failed gateway service must dump diagnostics" >&2
exit 1
fi
if [ "$(run_listener_wait "$running_unit" user_gateway_service_failed)" != "5" ]; then
echo "FAIL: a running gateway service must not stop the listener wait" >&2
exit 1
fi
if ! grep -Fq "did not become reachable" "$err"; then
echo "FAIL: a running gateway service must end with the listener timeout" >&2
cat "$err" >&2
exit 1
fi
if [ "$(run_listener_wait "$restarting_unit")" != "5" ]; then
echo "FAIL: the listener wait must ignore the service state without a check" >&2
exit 1
fi
: >"$registration_calls_file"
if ! (
as_target_user() { printf 'target:%s\n' "$*" >>"$registration_calls_file"; }
print_gateway_add_output() { :; }
register_user_snap_gateway
) >"$out" 2>"$err"; then
echo "FAIL: user Snap gateway registration should succeed" >&2
cat "$err" >&2 || true
exit 1
fi
if [[ $(cat "$registration_calls_file") != "target:/snap/bin/openshell gateway add https://127.0.0.1:17670 --local --name openshell" ]]; then
echo "FAIL: user Snap gateway registration must use its own local TLS bundle" >&2
cat "$registration_calls_file" >&2
exit 1
fi
if [ "$(PLATFORM=darwin local_gateway_endpoint)" != "https://localhost:17670" ]; then
echo "FAIL: macOS local gateway endpoint must use a TLS-compatible loopback hostname" >&2
exit 1
fi
if [ "$(PLATFORM=linux local_gateway_endpoint)" != "https://127.0.0.1:17670" ]; then
echo "FAIL: Linux local gateway endpoint must use IPv4 loopback" >&2
exit 1
fi
cat >"${tmpdir}/checksums" <<'EOF'
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa openshell-dev-x86_64.rpm
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb openshell-gateway-dev-x86_64.rpm
cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc openshell-prover-dev-x86_64.rpm
EOF
if [ "$(find_rpm_asset "${tmpdir}/checksums" x86_64 openshell-prover)" != "openshell-prover-dev-x86_64.rpm" ]; then
echo "FAIL: RPM prover package selection" >&2
exit 1
fi
mock_gh_log="${tmpdir}/gh.log"
PLATFORM=linux
linux_package_method() {
printf 'deb\n'
}
uname() {
case "${1:-}" in
-m) printf 'x86_64\n' ;;
*) command uname "$@" ;;
esac
}
gh() {
printf '%s\n' "$*" >>"$mock_gh_log"
case "$1:$2" in
auth:status)
return 0
;;
api:*)
case "$*" in
*"git/matching-refs/tags/v"*)
printf '%s\n' v2.0.0-pre.1 v1.0.0-pre.2 v0.1.0-pre.9
;;
*"?name=openshell-v2.0.0-pre.1-linux-amd64-deb"*)
[ "${MOCK_NO_PRERELEASE:-0}" = "1" ] || printf '999\n'
;;
*"?name=openshell-v1.0.0-pre.2-linux-amd64-deb"*)
[ "${MOCK_NO_PRERELEASE:-0}" = "1" ] || printf '101\n'
;;
*"?name=openshell-v0.1.0-pre.9-linux-amd64-deb"*)
[ "${MOCK_NO_PRERELEASE:-0}" = "1" ] || printf '123456\n'
;;
*"actions/runs/999"*) printf 'false\n' ;;
*"actions/runs/101"*) printf 'true\n' ;;
*)
;;
esac
;;
run:download)
while [ "$#" -gt 0 ]; do
if [ "$1" = "--dir" ]; then
shift
mkdir -p "$1"
printf 'checksums\n' >"$1/$CHECKSUMS_NAME"
return 0
fi
shift
done
return 1
;;
*) return 1 ;;
esac
}
resolved_prerelease="$(OPENSHELL_VERSION=pre resolve_release_tag)"
if [ "$resolved_prerelease" != "v1.0.0-pre.2" ]; then
echo "FAIL: pre alias resolved to ${resolved_prerelease}, expected v1.0.0-pre.2" >&2
exit 1
fi
if ! grep -Fq 'git/matching-refs/tags/v' "$mock_gh_log"; then
echo "FAIL: pre alias did not query prerelease tags" >&2
cat "$mock_gh_log" >&2
exit 1
fi
if ! grep -Fq 'actions/artifacts?name=openshell-v1.0.0-pre.2-linux-amd64-deb' "$mock_gh_log"; then
echo "FAIL: pre alias did not query the platform artifact by name" >&2
cat "$mock_gh_log" >&2
exit 1
fi
if ! grep -Fq 'actions/runs/999' "$mock_gh_log" ||
! grep -Fq 'actions/runs/101' "$mock_gh_log"; then
echo "FAIL: pre alias did not skip an unsuccessful run" >&2
cat "$mock_gh_log" >&2
exit 1
fi
if ! grep -Fq '.conclusion == "success"' "$mock_gh_log"; then
echo "FAIL: pre alias did not require a successful workflow run" >&2
cat "$mock_gh_log" >&2
exit 1
fi
if grep -Fq -- '--paginate' "$mock_gh_log"; then
echo "FAIL: pre alias must not scan every workflow run or artifact" >&2
cat "$mock_gh_log" >&2
exit 1
fi
if (MOCK_NO_PRERELEASE=1 OPENSHELL_VERSION=pre resolve_release_tag) >"$out" 2>"$err"; then
echo "FAIL: pre alias should fail when no unexpired prerelease artifacts exist" >&2
exit 1
fi
if ! grep -Fq 'no unexpired prerelease artifacts found' "$err"; then
echo "FAIL: missing prerelease resolution failure was not explained" >&2
cat "$err" >&2
exit 1
fi
RELEASE_TAG=v0.1.0-pre.9
prerelease_tmp="${tmpdir}/prerelease"
prepare_prerelease_assets "$prerelease_tmp"
if [ "$RELEASE_ASSET_DIR" != "${prerelease_tmp}/release" ]; then
echo "FAIL: prerelease artifact directory was not recorded" >&2
exit 1
fi
if ! grep -Fq 'select(.expired == false)' "$mock_gh_log"; then
echo "FAIL: prerelease lookup did not filter expired artifacts" >&2
cat "$mock_gh_log" >&2
exit 1
fi
if ! grep -Fq 'actions/artifacts?name=openshell-v0.1.0-pre.9-linux-amd64-deb' "$mock_gh_log"; then
echo "FAIL: prerelease lookup did not select the current platform artifact" >&2
cat "$mock_gh_log" >&2
exit 1
fi
if ! grep -Fq 'run download 123456 --repo NVIDIA/OpenShell --name openshell-v0.1.0-pre.9-linux-amd64-deb' "$mock_gh_log"; then
echo "FAIL: prerelease download did not select the current platform artifact" >&2
cat "$mock_gh_log" >&2
exit 1
fi
downloaded_checksum="${tmpdir}/downloaded-checksums.txt"
download_release_asset "$RELEASE_TAG" "$CHECKSUMS_NAME" "$downloaded_checksum"
if [ "$(cat "$downloaded_checksum")" != "checksums" ]; then
echo "FAIL: prerelease checksum was not copied from the platform artifact" >&2
exit 1
fi
for asset in "$HOMEBREW_CLI_ASSET" "$HOMEBREW_GATEWAY_ASSET" "$HOMEBREW_DRIVER_VM_ASSET" "$HOMEBREW_PROVER_ASSET"; do
: >"${RELEASE_ASSET_DIR}/${asset}"
done
prerelease_formula="${tmpdir}/openshell.rb"
printf '%s\n' \
" url \"${GITHUB_URL}/releases/download/${RELEASE_TAG}/${HOMEBREW_CLI_ASSET}\"" \
" url \"${GITHUB_URL}/releases/download/${RELEASE_TAG}/${HOMEBREW_GATEWAY_ASSET}\"" \
" url \"${GITHUB_URL}/releases/download/${RELEASE_TAG}/${HOMEBREW_DRIVER_VM_ASSET}\"" \
" url \"${GITHUB_URL}/releases/download/${RELEASE_TAG}/${HOMEBREW_PROVER_ASSET}\"" \
>"$prerelease_formula"
patch_prerelease_homebrew_formula_urls "$prerelease_formula"
for asset in "$HOMEBREW_CLI_ASSET" "$HOMEBREW_GATEWAY_ASSET" "$HOMEBREW_DRIVER_VM_ASSET" "$HOMEBREW_PROVER_ASSET"; do
if ! grep -Fq "file://${RELEASE_ASSET_DIR}/${asset}" "$prerelease_formula"; then
echo "FAIL: prerelease formula did not use local asset ${asset}" >&2
exit 1
fi
done
unset -f gh uname linux_package_method
echo "install.sh focused tests passed"