From b6edebd10308c5b01e616da4566325d81534bf99 Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Mon, 31 Aug 2026 12:07:48 +0200 Subject: [PATCH 1/4] test(e2e): remove unused capbset probe Signed-off-by: Evan Lezar --- e2e/support/capbset-probe.c | 113 ------------------------------------ 1 file changed, 113 deletions(-) delete mode 100644 e2e/support/capbset-probe.c diff --git a/e2e/support/capbset-probe.c b/e2e/support/capbset-probe.c deleted file mode 100644 index bc93d766db..0000000000 --- a/e2e/support/capbset-probe.c +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// Verify the capability-bounding-set condition behind issue #2069 from -// inside a rootless Podman container. - -#include -#include -#include -#include -#include -#include - -static unsigned long long status_capability(const char *field) { - FILE *status = fopen("/proc/self/status", "r"); - if (status == NULL) { - perror("fopen(/proc/self/status)"); - exit(EXIT_FAILURE); - } - - char line[256]; - unsigned long long value = 0; - int found = 0; - while (fgets(line, sizeof(line), status) != NULL) { - char name[32]; - unsigned long long candidate; - if (sscanf(line, "%31[^:]:%llx", name, &candidate) == 2 && - strcmp(name, field) == 0) { - value = candidate; - found = 1; - break; - } - } - fclose(status); - - if (!found) { - fprintf(stderr, "missing %s in /proc/self/status\n", field); - exit(EXIT_FAILURE); - } - return value; -} - -static void print_apparmor_profile(void) { - FILE *profile = fopen("/proc/self/attr/current", "r"); - if (profile == NULL) { - perror("fopen(/proc/self/attr/current)"); - return; - } - - char line[256]; - if (fgets(line, sizeof(line), profile) != NULL) { - printf("apparmor_profile=%s", line); - if (strchr(line, '\n') == NULL) { - putchar('\n'); - } - } - fclose(profile); -} - -int main(int argc, char **argv) { - if (argc != 1) { - fprintf(stderr, "usage: %s\n", argv[0]); - return EXIT_FAILURE; - } - - const unsigned long long setpcap_mask = 1ULL << CAP_SETPCAP; - const unsigned long long cap_bnd_before = status_capability("CapBnd"); - const unsigned long long cap_eff_before = status_capability("CapEff"); - const int setpcap_before = prctl(PR_CAPBSET_READ, CAP_SETPCAP, 0, 0, 0); - if (setpcap_before == -1) { - perror("prctl(PR_CAPBSET_READ) before drop"); - return EXIT_FAILURE; - } - - print_apparmor_profile(); - printf("cap_bnd_before=%016llx\n", cap_bnd_before); - printf("cap_eff_before=%016llx\n", cap_eff_before); - printf("setpcap_bounding_before=%d\n", setpcap_before); - - if (cap_bnd_before == 0 || (cap_bnd_before & setpcap_mask) == 0 || - (cap_eff_before & setpcap_mask) == 0 || setpcap_before != 1) { - fprintf(stderr, "CAP_SETPCAP must be effective and present in a non-empty bounding set\n"); - return EXIT_FAILURE; - } - - errno = 0; - const int drop_result = prctl(PR_CAPBSET_DROP, CAP_SETPCAP, 0, 0, 0); - const int drop_errno = errno; - const unsigned long long cap_bnd_after = status_capability("CapBnd"); - const int setpcap_after = prctl(PR_CAPBSET_READ, CAP_SETPCAP, 0, 0, 0); - - printf("drop_result=%d\n", drop_result); - printf("drop_errno=%d (%s)\n", drop_errno, strerror(drop_errno)); - printf("cap_bnd_after=%016llx\n", cap_bnd_after); - printf("setpcap_bounding_after=%d\n", setpcap_after); - - if (drop_result == 0) { - if (setpcap_after != 0 || (cap_bnd_after & setpcap_mask) != 0) { - fprintf(stderr, "CAP_SETPCAP remained in the bounding set after a successful drop\n"); - return EXIT_FAILURE; - } - } else if (drop_errno == EPERM) { - if (setpcap_after != 1 || (cap_bnd_after & setpcap_mask) == 0) { - fprintf(stderr, "CAP_SETPCAP changed in the bounding set after EPERM\n"); - return EXIT_FAILURE; - } - } else { - fprintf(stderr, "unexpected PR_CAPBSET_DROP result\n"); - return EXIT_FAILURE; - } - - return EXIT_SUCCESS; -} From adf705702208f30fa5adf6e6bac1338d1f16322a Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Mon, 31 Aug 2026 12:07:55 +0200 Subject: [PATCH 2/4] test(guest): add Ubuntu 26 rootless Podman guest Signed-off-by: Evan Lezar --- nix/test-guest/README.md | 44 ++++----- nix/test-guest/cache.sh | 4 +- .../configuration/podman-rootless.yml | 89 +++++++++++++++++++ nix/test-guest/default.nix | 4 +- .../distros/{ubuntu.nix => ubuntu-24-04.nix} | 0 nix/test-guest/distros/ubuntu-26-04.nix | 25 ++++++ nix/test-guest/run.sh | 6 +- 7 files changed, 146 insertions(+), 26 deletions(-) create mode 100644 nix/test-guest/configuration/podman-rootless.yml rename nix/test-guest/distros/{ubuntu.nix => ubuntu-24-04.nix} (100%) create mode 100644 nix/test-guest/distros/ubuntu-26-04.nix diff --git a/nix/test-guest/README.md b/nix/test-guest/README.md index b608cb19fe..a65baf02a3 100644 --- a/nix/test-guest/README.md +++ b/nix/test-guest/README.md @@ -33,12 +33,14 @@ nix/test-guest/ ├── cache-lib.sh ├── cache-seal.sh ├── distros/ -│ ├── ubuntu.nix +│ ├── ubuntu-24-04.nix +│ ├── ubuntu-26-04.nix │ ├── centos.nix │ ├── fedora.nix │ └── rocky.nix └── configuration/ ├── docker.yml + ├── podman-rootless.yml ├── podman.yml └── selinux.yml ``` @@ -56,12 +58,13 @@ The root [`flake.nix`](../../flake.nix) exposes this directory as the `test-gues ## Supported configurations -| Distro | Docker | Podman | SELinux | Package format | -| --- | --- | --- | --- | --- | -| Ubuntu 24.04 | Yes | Yes | No | `.deb` | -| CentOS Stream 10 | No | Yes | Yes | `.rpm` | -| Fedora 44 | No | Yes | Yes | `.rpm` | -| Rocky Linux 9 | Yes | Yes | Yes | `.rpm` | +| Distro | Docker | Podman | Rootless Podman | SELinux | Package format | +| --- | --- | --- | --- | --- | --- | +| Ubuntu 24.04 | Yes | Yes | No | No | `.deb` | +| Ubuntu 26.04 | Yes | Yes | Yes | No | `.deb` | +| CentOS Stream 10 | No | Yes | No | Yes | `.rpm` | +| Fedora 44 | No | Yes | No | Yes | `.rpm` | +| Rocky Linux 9 | Yes | Yes | No | Yes | `.rpm` | The `snapd` configuration is available for Ubuntu and prepares snapd for local Snap lifecycle experiments. It does not install Docker, because the Snap @@ -70,8 +73,8 @@ interface rather than the host-package Docker configuration. The Ubuntu 24.04 Podman configuration is available for runtime and packaging checks, but its Podman 4 release does not provide the `pasta` rootless network -helper required by OpenShell sandbox callbacks. OpenShell Podman E2E runs use -the Fedora guest, which provides Podman 5 and `pasta`. +helper required by OpenShell sandbox callbacks. Rootless Podman E2E uses the +Ubuntu 26.04 guest with `--with podman-rootless`. List the available distros and configurations: @@ -84,13 +87,13 @@ nix run .#test-guest -- --list Boot a base Ubuntu VM: ```shell -nix run .#test-guest -- --distro ubuntu +nix run .#test-guest -- --distro ubuntu-24-04 ``` Apply the Docker configuration before opening the SSH session: ```shell -nix run .#test-guest -- --distro ubuntu --with docker +nix run .#test-guest -- --distro ubuntu-24-04 --with docker ``` Other combinations use the same interface: @@ -99,13 +102,14 @@ Other combinations use the same interface: nix run .#test-guest -- --distro rocky --with docker nix run .#test-guest -- --distro centos --with podman nix run .#test-guest -- --distro fedora --with podman +nix run .#test-guest -- --distro ubuntu-26-04 --with podman-rootless ``` Configurations are repeatable: ```shell nix run .#test-guest -- \ - --distro ubuntu \ + --distro ubuntu-24-04 \ --with docker \ --with podman ``` @@ -138,7 +142,7 @@ The `test-guest-cache` app ensures a prepared disk exists for one exact distro, ```shell nix run .#test-guest-cache -- \ - --distro ubuntu \ + --distro ubuntu-24-04 \ --with docker ``` @@ -147,7 +151,7 @@ backing cache: ```shell nix run .#test-guest-cache -- \ - --distro ubuntu \ + --distro ubuntu-24-04 \ --with docker \ --repository ghcr.io/nvidia/openshell/test-guest-cache \ --digest sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef @@ -157,7 +161,7 @@ The command never publishes implicitly. Add `--push` after authenticating ORAS t ```shell nix run .#test-guest-cache -- \ - --distro ubuntu \ + --distro ubuntu-24-04 \ --with docker \ --repository ghcr.io/nvidia/openshell/test-guest-cache \ --push @@ -184,7 +188,7 @@ The default cache directory is `${XDG_CACHE_HOME:-$HOME/.cache}/openshell/test-g Cache command options: ```text ---distro NAME Base distro: ubuntu, centos, fedora, or rocky +--distro NAME Base distro: ubuntu-24-04, ubuntu-26-04, centos, fedora, or rocky --with NAME Apply docker, podman, or selinux; repeatable --repository REF OCI repository without a tag --digest DIGEST Trusted OCI manifest digest required for pulls @@ -209,7 +213,7 @@ Install the package in an Ubuntu VM and run a command: ```shell nix run .#test-guest -- \ - --distro ubuntu \ + --distro ubuntu-24-04 \ --with docker \ --install artifacts/openshell_0.0.0-local_arm64.deb \ -- openshell --version @@ -226,7 +230,7 @@ guest file preserves the source's ordinary permission bits: ```shell nix run .#test-guest -- \ - --distro ubuntu \ + --distro ubuntu-24-04 \ --copy ./openshell:/usr/local/bin/openshell \ -- openshell --version ``` @@ -241,7 +245,7 @@ gateway. On each failure it prints snapd and gateway journals. ```shell nix run .#test-guest -- \ - --distro ubuntu \ + --distro ubuntu-24-04 \ --with snapd \ --keep \ --copy ./openshell_*.snap:/tmp/openshell.snap \ @@ -259,7 +263,7 @@ The destination must be an absolute guest path. Copied files are installed with ## Runner options ```text ---distro NAME Base distro: ubuntu, centos, fedora, or rocky +--distro NAME Base distro: ubuntu-24-04, ubuntu-26-04, centos, fedora, or rocky --with NAME Apply docker, podman, or selinux; repeatable --install PATH Install a .deb or .rpm package; repeatable --copy SRC:DEST Copy a regular file into the guest, preserving its host mode; diff --git a/nix/test-guest/cache.sh b/nix/test-guest/cache.sh index 1103b3c289..35e3ac441c 100644 --- a/nix/test-guest/cache.sh +++ b/nix/test-guest/cache.sh @@ -12,8 +12,8 @@ Usage: nix run .#test-guest-cache -- --distro DISTRO [OPTIONS] Options: - --distro NAME Base distro: ubuntu, centos, fedora, or rocky - --with NAME Apply a configuration; repeatable (docker, podman, selinux) + --distro NAME Base distro: ubuntu-24-04, ubuntu-26-04, centos, fedora, or rocky + --with NAME Apply a configuration; repeatable (docker, podman, podman-rootless, selinux) --repository REF OCI repository without a tag --digest DIGEST Trusted OCI manifest digest required for pulls --cache-dir PATH Override the local prepared-disk cache directory diff --git a/nix/test-guest/configuration/podman-rootless.yml b/nix/test-guest/configuration/podman-rootless.yml new file mode 100644 index 0000000000..dc99934f74 --- /dev/null +++ b/nix/test-guest/configuration/podman-rootless.yml @@ -0,0 +1,89 @@ +--- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# PROTOTYPE: Configure rootless Podman in a disposable test guest. + +- name: Configure rootless Podman + hosts: test_vm + become: true + gather_facts: true + + tasks: + - name: Validate rootless Podman support + ansible.builtin.assert: + that: + - ansible_facts.distribution == "Ubuntu" + - ansible_facts.distribution_version is version("26.04", ">=") + fail_msg: >- + Rootless Podman requires Ubuntu 26.04 or newer, not + {{ ansible_facts.distribution }} {{ ansible_facts.distribution_version }}. + + - name: Refresh Ubuntu package metadata + ansible.builtin.apt: + update_cache: true + + - name: Install Ubuntu rootless Podman + ansible.builtin.apt: + name: + - apparmor + - fuse-overlayfs + - passt + - podman + - uidmap + state: present + + - name: Allow pasta to receive Podman stop signals + ansible.builtin.lineinfile: + path: /etc/apparmor.d/usr.bin.pasta + insertafter: "^ include $" + line: " signal (receive) peer=podman," + state: present + register: pasta_apparmor_profile + + - name: Reload pasta AppArmor profile + ansible.builtin.command: + argv: + - apparmor_parser + - --replace + - /etc/apparmor.d/usr.bin.pasta + when: pasta_apparmor_profile.changed + changed_when: pasta_apparmor_profile.changed + + - name: Enable the rootless Podman API socket + ansible.builtin.systemd_service: + name: podman.socket + scope: user + enabled: true + state: started + become: false + + - name: Verify rootless Podman + ansible.builtin.command: + cmd: podman info + become: false + changed_when: false + + - name: Verify rootless Podman mode + ansible.builtin.command: + argv: + - podman + - info + - --format + - "{% raw %}{{.Host.Security.Rootless}}{% endraw %}" + become: false + changed_when: false + register: podman_rootless + failed_when: podman_rootless.stdout != "true" + + - name: Verify rootless Podman uses pasta + ansible.builtin.command: + argv: + - podman + - info + - --format + - "{% raw %}{{.Host.RootlessNetworkCmd}}{% endraw %}" + become: false + changed_when: false + register: podman_rootless_network + failed_when: podman_rootless_network.stdout != "pasta" diff --git a/nix/test-guest/default.nix b/nix/test-guest/default.nix index 753f266ab4..b2249e1f4e 100644 --- a/nix/test-guest/default.nix +++ b/nix/test-guest/default.nix @@ -18,7 +18,8 @@ let if isAarch64 then "${qemu}/bin/qemu-system-aarch64" else "${qemu}/bin/qemu-system-x86_64"; distros = { - ubuntu = import ./distros/ubuntu.nix { inherit pkgs architecture; }; + ubuntu-24-04 = import ./distros/ubuntu-24-04.nix { inherit pkgs architecture; }; + ubuntu-26-04 = import ./distros/ubuntu-26-04.nix { inherit pkgs architecture; }; centos = import ./distros/centos.nix { inherit pkgs architecture; }; fedora = import ./distros/fedora.nix { inherit pkgs architecture; }; rocky = import ./distros/rocky.nix { inherit pkgs architecture; }; @@ -27,6 +28,7 @@ let configurations = { docker = ./configuration/docker.yml; podman = ./configuration/podman.yml; + podman-rootless = ./configuration/podman-rootless.yml; selinux = ./configuration/selinux.yml; snapd = ./configuration/snapd.yml; }; diff --git a/nix/test-guest/distros/ubuntu.nix b/nix/test-guest/distros/ubuntu-24-04.nix similarity index 100% rename from nix/test-guest/distros/ubuntu.nix rename to nix/test-guest/distros/ubuntu-24-04.nix diff --git a/nix/test-guest/distros/ubuntu-26-04.nix b/nix/test-guest/distros/ubuntu-26-04.nix new file mode 100644 index 0000000000..7f37fec138 --- /dev/null +++ b/nix/test-guest/distros/ubuntu-26-04.nix @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +{ pkgs, architecture }: + +let + imageArchitecture = if architecture == "aarch64" then "arm64" else "amd64"; + imageUrl = "https://cloud-images.ubuntu.com/releases/releases/26.04/release/ubuntu-26.04-server-cloudimg-${imageArchitecture}.img"; + imageHash = + if architecture == "aarch64" then + "sha256-PhE/3UHznhNyk3UXO7KueT+H3G20KU5SUf8kdpcXiLo=" + else + "sha256-gZa+nXlYBZy1bGx1yA/fbO6KiIW8FJ6nkdfbHH75MDU="; +in +{ + osId = "ubuntu"; + osVersion = "26.04"; + packageFamily = "deb"; + inherit imageUrl imageHash; + image = pkgs.fetchurl { + name = "ubuntu-26.04-server-cloudimg-${imageArchitecture}.img"; + url = imageUrl; + hash = imageHash; + }; +} diff --git a/nix/test-guest/run.sh b/nix/test-guest/run.sh index 6be58eb235..9e5d19baef 100644 --- a/nix/test-guest/run.sh +++ b/nix/test-guest/run.sh @@ -12,8 +12,8 @@ Usage: nix run .#test-guest -- --distro DISTRO [OPTIONS] [-- COMMAND...] Options: - --distro NAME Base distro: ubuntu, centos, fedora, or rocky - --with NAME Apply a configuration; repeatable (docker, podman, selinux, snapd) + --distro NAME Base distro: ubuntu-24-04, ubuntu-26-04, centos, fedora, or rocky + --with NAME Apply a configuration; repeatable (docker, podman, podman-rootless, selinux, snapd) --install PATH Install a .deb or .rpm package; repeatable --copy SRC:DEST Copy a regular file to an absolute guest path, preserving its host mode; repeatable @@ -49,7 +49,7 @@ preserved_file_mode() { local source_mode if [ "$(uname -s)" = Darwin ]; then - if ! source_mode=$(stat -f '%Lp' "${source_path}"); then + if ! source_mode=$(/usr/bin/stat -f '%Lp' "${source_path}"); then echo "could not determine mode for --copy source: ${source_path}" >&2 return 1 fi From 4d5ea7e168e49389acbda2989b77e3cb5fa6bd8b Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Mon, 31 Aug 2026 12:08:03 +0200 Subject: [PATCH 3/4] test(e2e): run Podman E2E tests inside test guest Signed-off-by: Evan Lezar --- TESTING.md | 6 + e2e/run.sh | 479 +++++++++++++++++++++++---- e2e/rust/Cargo.toml | 1 + e2e/rust/tests/live_policy_update.rs | 26 +- e2e/rust/tests/transparent_tcp.rs | 7 + tasks/test.toml | 4 + 6 files changed, 450 insertions(+), 73 deletions(-) diff --git a/TESTING.md b/TESTING.md index 6c0829060d..94e457d6ba 100644 --- a/TESTING.md +++ b/TESTING.md @@ -175,6 +175,12 @@ Run the Podman-backed Rust CLI e2e suite: mise run e2e:podman ``` +Run the rootless Podman suite in an Ubuntu 26.04 Nix test guest: + +```shell +mise run e2e:podman:rootless +``` + Run the VM-backed Rust CLI e2e suite: ```shell diff --git a/e2e/run.sh b/e2e/run.sh index 0505730f05..bf364f6176 100755 --- a/e2e/run.sh +++ b/e2e/run.sh @@ -3,7 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 # Build the current checkout, run its gateway on the host or in a disposable -# Nix test guest, and execute one named host-side E2E suite against that gateway. +# Nix test guest, and execute E2E tests against that gateway. set -Eeuo pipefail @@ -20,19 +20,27 @@ usage() { cat <<'EOF' Usage: e2e/run.sh [--vm DISTRO] [--with CONFIG ...] \ - --gateway-config PATH --suite NAME + --gateway-config PATH [--features FEATURES] [--suite NAME] Options: --vm DISTRO Run the gateway in a Nix test guest --with CONFIG Apply a Nix test-guest configuration; repeatable + --tests-in-vm Prebuild Linux Rust E2E test binaries on the host, + copy them into the Nix test guest, and run them there + --cli-bin PATH Use a prebuilt openshell CLI instead of building it + --gateway-bin PATH Use a prebuilt openshell-gateway instead of building it + --sandbox-bin PATH Use a prebuilt openshell-sandbox instead of building it --gateway-config PATH Fully resolved gateway TOML + --features FEATURES Rust e2e feature set to enable (default: e2e) --suite NAME Rust suite at e2e/rust/tests/NAME.rs -h, --help Show this help Omit --vm and --with to run the gateway on the host. Supplying --with without --vm selects Fedora for the Podman driver and Ubuntu otherwise. Set -OPENSHELL_E2E_KEEP=1 to retain state. +OPENSHELL_CLI_BIN for a default --cli-bin; otherwise --tests-in-vm +cross-builds the guest CLI with cargo-zigbuild. Set OPENSHELL_E2E_KEEP=1 to +retain state. EOF } @@ -92,7 +100,12 @@ catalog_has_entry() { vm= gateway_config= +gateway_bin= +cli_bin= +sandbox_bin= +e2e_features=e2e suite_name= +tests_in_vm=0 with_configurations=() while [ "$#" -gt 0 ]; do @@ -107,11 +120,35 @@ while [ "$#" -gt 0 ]; do with_configurations+=("$2") shift 2 ;; + --tests-in-vm) + tests_in_vm=1 + shift + ;; + --cli-bin) + require_value "$1" "$#" "${2:-}" + cli_bin="$(resolve_file "$2")" || die "--cli-bin does not name a file: $2" + shift 2 + ;; + --gateway-bin) + require_value "$1" "$#" "${2:-}" + gateway_bin="$(resolve_file "$2")" || die "--gateway-bin does not name a file: $2" + shift 2 + ;; + --sandbox-bin) + require_value "$1" "$#" "${2:-}" + sandbox_bin="$(resolve_file "$2")" || die "--sandbox-bin does not name a file: $2" + shift 2 + ;; --gateway-config) require_value "$1" "$#" "${2:-}" gateway_config=$2 shift 2 ;; + --features) + require_value "$1" "$#" "${2:-}" + e2e_features=$2 + shift 2 + ;; --suite) require_value "$1" "$#" "${2:-}" suite_name=$2 @@ -130,26 +167,31 @@ done if [ -z "${gateway_config}" ]; then die "--gateway-config is required" fi -if [ -z "${suite_name}" ]; then - die "--suite is required" -fi if ! command -v python3 >/dev/null 2>&1; then die "python3 is required" fi +if ! command -v mise >/dev/null 2>&1; then + die "mise is required to build OpenShell" +fi gateway_config_source=${gateway_config} if ! gateway_config="$(resolve_file "${gateway_config_source}")"; then die "gateway config does not exist: ${gateway_config_source}" fi -gateway_driver="$(python3 -c ' +gateway_driver="$(mise x -- python3 -c ' import sys, tomllib print(tomllib.load(open(sys.argv[1], "rb"))["openshell"]["gateway"]["compute_drivers"][0]) ' "${gateway_config}")" -if [[ ! ${suite_name} =~ ^[a-z0-9][a-z0-9-]*$ ]]; then - die "suite name must contain only lowercase letters, digits, and hyphens: ${suite_name}" +if [ -z "${e2e_features}" ]; then + die "--features must not be empty" fi -suite_path="${ROOT}/e2e/rust/tests/${suite_name}.rs" -if [ ! -f "${suite_path}" ]; then - die "unknown suite: ${suite_name}" +if [ -n "${suite_name}" ]; then + if [[ ! ${suite_name} =~ ^[a-z0-9][a-z0-9_-]*$ ]]; then + die "suite name must contain only lowercase letters, digits, underscores, and hyphens: ${suite_name}" + fi + suite_path="${ROOT}/e2e/rust/tests/${suite_name}.rs" + if [ ! -f "${suite_path}" ]; then + die "unknown suite: ${suite_name}" + fi fi mode=host if [ -n "${vm}" ] || [ "${#with_configurations[@]}" -gt 0 ]; then @@ -158,7 +200,7 @@ if [ -n "${vm}" ] || [ "${#with_configurations[@]}" -gt 0 ]; then if [ "${gateway_driver}" = podman ]; then vm=fedora else - vm=ubuntu + vm=ubuntu-24-04 fi fi fi @@ -171,9 +213,6 @@ if [ "${mode}" = vm ]; then die "invalid VM configuration name: ${configuration}" fi done - if [ "${gateway_driver}" = podman ] && [ "${vm}" = ubuntu ]; then - die "the Ubuntu 24.04 guest lacks the Podman 5 pasta helper required for sandbox callbacks; use --vm fedora --with podman" - fi if ! command -v nix >/dev/null 2>&1; then die "Nix is required for VM mode" fi @@ -191,15 +230,17 @@ if [ "${mode}" = vm ]; then die "unknown VM configuration in the Nix test-guest catalog: ${configuration}" fi done +elif [ "${tests_in_vm}" -eq 1 ]; then + die "--tests-in-vm requires --vm" +fi +if [ -z "${cli_bin}" ] && [ -n "${OPENSHELL_CLI_BIN:-}" ]; then + cli_bin="$(resolve_file "${OPENSHELL_CLI_BIN}")" || + die "OPENSHELL_CLI_BIN does not name a file: ${OPENSHELL_CLI_BIN}" fi - gateway_ready_timeout=${OPENSHELL_E2E_GATEWAY_READY_TIMEOUT:-600} if [[ ! ${gateway_ready_timeout} =~ ^[1-9][0-9]*$ ]]; then die "OPENSHELL_E2E_GATEWAY_READY_TIMEOUT must be a positive integer" fi -if ! command -v mise >/dev/null 2>&1; then - die "mise is required to build OpenShell" -fi if ! command -v openssl >/dev/null 2>&1; then die "OpenSSL is required to generate sandbox JWT keys" fi @@ -230,56 +271,93 @@ target_dir="$(e2e_cargo_target_dir "${ROOT}" mise x -- cargo)" ensure_build_nofile_limit -echo "==> Building native host openshell CLI" -mise x -- cargo build "${cargo_jobs[@]}" -p openshell-cli --bin openshell -host_cli_bin="${target_dir}/debug/openshell" - -echo "==> Preparing ${linux_musl_target} build target" -mise x -- rustup target add "${linux_musl_target}" >/dev/null +if [ "${tests_in_vm}" -eq 1 ]; then + if [ -n "${cli_bin}" ]; then + echo "==> Using Linux guest openshell CLI: ${cli_bin}" + else + echo "==> Building Linux guest openshell CLI (${linux_musl_target})" + mise x -- rustup target add "${linux_musl_target}" >/dev/null + ( + export CXXSTDLIB=c++ + mise x -- cargo zigbuild "${cargo_jobs[@]+"${cargo_jobs[@]}"}" \ + --release \ + --target "${linux_musl_target}" \ + -p openshell-cli \ + --bin openshell + ) + cli_bin="${target_dir}/${linux_musl_target}/release/openshell" + fi +elif [ -n "${cli_bin}" ]; then + echo "==> Using host openshell CLI: ${cli_bin}" +else + echo "==> Building native host openshell CLI" + mise x -- cargo build "${cargo_jobs[@]+"${cargo_jobs[@]}"}" -p openshell-cli --bin openshell + cli_bin="${target_dir}/debug/openshell" +fi -echo "==> Building Linux openshell-sandbox (${linux_musl_target})" -mise x -- cargo zigbuild "${cargo_jobs[@]}" \ - --release \ - --target "${linux_musl_target}" \ - -p openshell-sandbox \ - --bin openshell-sandbox -linux_sandbox_bin="${target_dir}/${linux_musl_target}/release/openshell-sandbox" +if [ -n "${sandbox_bin}" ]; then + echo "==> Using Linux openshell-sandbox: ${sandbox_bin}" + linux_sandbox_bin="${sandbox_bin}" +else + echo "==> Preparing ${linux_musl_target} build target" + mise x -- rustup target add "${linux_musl_target}" >/dev/null + + echo "==> Building Linux openshell-sandbox (${linux_musl_target})" + mise x -- cargo zigbuild "${cargo_jobs[@]+"${cargo_jobs[@]}"}" \ + --release \ + --target "${linux_musl_target}" \ + -p openshell-sandbox \ + --bin openshell-sandbox + linux_sandbox_bin="${target_dir}/${linux_musl_target}/release/openshell-sandbox" +fi host_gateway_bin= guest_gateway_bin= if [ "${mode}" = host ]; then - echo "==> Building native host openshell-gateway" - mise x -- cargo build "${cargo_jobs[@]}" \ - -p openshell-server \ - --bin openshell-gateway \ - --features bundled-z3 - host_gateway_bin="${target_dir}/debug/openshell-gateway" -else - echo "==> Preparing ${linux_gateway_rust_target} build target" - mise x -- rustup target add "${linux_gateway_rust_target}" >/dev/null - echo "==> Building Linux openshell-gateway (${linux_gateway_zig_target})" - ( - eval "$( - "${ROOT}/tasks/scripts/setup-zig-cc-wrapper.sh" \ - "${linux_gateway_zig_target}" \ - "${linux_gateway_zig_target}" \ - "${target_dir}/zig-gnu-wrapper/e2e" - )" - mise x -- cargo zigbuild "${cargo_jobs[@]}" \ - --release \ - --target "${linux_gateway_zig_target}" \ + if [ -n "${gateway_bin}" ]; then + echo "==> Using host openshell-gateway: ${gateway_bin}" + host_gateway_bin="${gateway_bin}" + else + echo "==> Building native host openshell-gateway" + mise x -- cargo build "${cargo_jobs[@]+"${cargo_jobs[@]}"}" \ -p openshell-server \ --bin openshell-gateway \ --features bundled-z3 - ) - guest_gateway_bin="${target_dir}/${linux_gateway_rust_target}/release/openshell-gateway" + host_gateway_bin="${target_dir}/debug/openshell-gateway" + fi +else + if [ -n "${gateway_bin}" ]; then + echo "==> Using Linux openshell-gateway: ${gateway_bin}" + guest_gateway_bin="${gateway_bin}" + else + echo "==> Preparing ${linux_gateway_rust_target} build target" + mise x -- rustup target add "${linux_gateway_rust_target}" >/dev/null + echo "==> Building Linux openshell-gateway (${linux_gateway_zig_target})" + ( + eval "$( + "${ROOT}/tasks/scripts/setup-zig-cc-wrapper.sh" \ + "${linux_gateway_zig_target}" \ + "${linux_gateway_zig_target}" \ + "${target_dir}/zig-gnu-wrapper/e2e" + )" + mise x -- cargo zigbuild "${cargo_jobs[@]+"${cargo_jobs[@]}"}" \ + --release \ + --target "${linux_gateway_zig_target}" \ + -p openshell-server \ + --bin openshell-gateway \ + --features bundled-z3 + ) + guest_gateway_bin="${target_dir}/${linux_gateway_rust_target}/release/openshell-gateway" + fi fi -expected_binaries=("${host_cli_bin}" "${linux_sandbox_bin}") -if [ "${mode}" = host ]; then - expected_binaries+=("${host_gateway_bin}") +expected_binaries=("${linux_sandbox_bin}") +if [ "${tests_in_vm}" -eq 1 ]; then + expected_binaries+=("${cli_bin}" "${guest_gateway_bin}") +elif [ "${mode}" = host ]; then + expected_binaries+=("${cli_bin}" "${host_gateway_bin}") else - expected_binaries+=("${guest_gateway_bin}") + expected_binaries+=("${cli_bin}" "${guest_gateway_bin}") fi for binary in "${expected_binaries[@]}"; do if [ ! -x "${binary}" ]; then @@ -300,6 +378,8 @@ supervisor_archive="${run_dir}/supervisor.tar" mkdir -p "${supervisor_rootfs}" install -m 0555 "${linux_sandbox_bin}" "${supervisor_rootfs}/openshell-sandbox" tar -C "${supervisor_rootfs}" -cf "${supervisor_archive}" openshell-sandbox +chmod 0644 "${supervisor_archive}" +test_artifacts=() child_pid= runtime_log= keep=0 @@ -321,6 +401,65 @@ start_child() { child_pid=$! } +build_e2e_test_artifacts() { + local build_log="${run_dir}/e2e-test-build.jsonl" + local artifacts_file="${run_dir}/e2e-test-artifacts.txt" + local build_args=( + mise x -- cargo zigbuild + --manifest-path e2e/rust/Cargo.toml + --features "${e2e_features}" + --target "${linux_gateway_zig_target}" + --message-format=json + ) + if [ -n "${suite_name}" ]; then + build_args+=(--test "${suite_name}") + else + build_args+=(--tests) + fi + + echo "==> Prebuilding E2E test artifacts for guest execution (${linux_gateway_rust_target})" + if ! ( + eval "$( + "${ROOT}/tasks/scripts/setup-zig-cc-wrapper.sh" \ + "${linux_gateway_zig_target}" \ + "${linux_gateway_zig_target}" \ + "${target_dir}/zig-gnu-wrapper/e2e-tests" + )" + "${build_args[@]}" + ) >"${build_log}"; then + echo "=== E2E test artifact build output ===" >&2 + cat "${build_log}" >&2 + echo "=== end E2E test artifact build output ===" >&2 + return 1 + fi + python3 - "${build_log}" >"${artifacts_file}" <<'PY' +import json +import sys + +for line in open(sys.argv[1], encoding="utf-8"): + try: + message = json.loads(line) + except json.JSONDecodeError: + continue + if message.get("reason") != "compiler-artifact": + continue + target = message.get("target") or {} + if "test" not in (target.get("kind") or []): + continue + executable = message.get("executable") + if executable: + print(executable) +PY + while IFS= read -r artifact; do + if [ -n "${artifact}" ]; then + test_artifacts+=("${artifact}") + fi + done <"${artifacts_file}" + if [ "${#test_artifacts[@]}" -eq 0 ]; then + die "cargo did not report any E2E test executables" + fi +} + # Invoked by the EXIT trap through cleanup. # shellcheck disable=SC2329 stop_child() { @@ -367,6 +506,10 @@ trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM +if [ "${tests_in_vm}" -eq 1 ]; then + build_e2e_test_artifacts +fi + jwt_source_dir="${run_dir}/gateway-jwt" host_runtime_dir= if [ "${mode}" = host ]; then @@ -390,7 +533,7 @@ gateway_name="openshell-e2e-${mode}-${host_port}" gateway_endpoint="http://127.0.0.1:${host_port}" export OPENSHELL_GATEWAY_ENDPOINT="${gateway_endpoint}" export OPENSHELL_GATEWAY="${gateway_name}" -export OPENSHELL_BIN="${host_cli_bin}" +export OPENSHELL_BIN="${cli_bin}" if [ "${mode}" = host ]; then case "${gateway_driver}" in @@ -424,6 +567,23 @@ else guest_launcher="${run_dir}/launch-gateway.sh" guest_launcher_path=/home/openshell/.cache/openshell-e2e/bin/launch-gateway guest_supervisor_archive_path=/home/openshell/.cache/openshell-e2e/supervisor.tar + guest_test_artifact_dir=/home/openshell/.cache/openshell-e2e/tests + guest_test_manifest="${run_dir}/test-artifacts.txt" + guest_test_manifest_path=/home/openshell/.cache/openshell-e2e/test-artifacts.txt + if [ "${tests_in_vm}" -eq 1 ]; then + : >"${guest_test_manifest}" + for artifact in "${test_artifacts[@]}"; do + printf '%s/%s\n' "${guest_test_artifact_dir}" "${artifact##*/}" >>"${guest_test_manifest}" + done + chmod 0644 "${guest_test_manifest}" + fi + guest_e2e_network_name="$(mise x -- python3 - "${gateway_config}" <<'PY' +import sys, tomllib + +config = tomllib.load(open(sys.argv[1], "rb")) +print(config.get("openshell", {}).get("drivers", {}).get("podman", {}).get("network_name", "openshell-e2e")) +PY +)" config_payload="$(base64 <"${gateway_config}" | tr -d '\r\n')" jwt_signing_payload="$(base64 <"${jwt_source_dir}/signing.pem" | tr -d '\r\n')" jwt_public_payload="$(base64 <"${jwt_source_dir}/public.pem" | tr -d '\r\n')" @@ -475,13 +635,178 @@ podman) esac report_timing "${gateway_driver} supervisor import" "\${phase_started_at}" cd /home/openshell + +if [ '${tests_in_vm}' = 1 ]; then + gateway_log=\${state_root}/gateway.log + gateway_pid_file=\${state_root}/gateway.pid + gateway_args_file=\${state_root}/gateway.args + spiffe_root=\${state_root}/spiffe + mkdir -p "\${spiffe_root}" "${guest_test_artifact_dir}" + + toml_string() { + python3 - "\$1" <<'PY' +import json +import sys + +print(json.dumps(sys.argv[1])) +PY + } + + pick_free_port() { + python3 - <<'PY' +import socket + +sock = socket.socket() +sock.bind(("0.0.0.0", 0)) +print(sock.getsockname()[1]) +sock.close() +PY + } + + insert_podman_config_key() { + local key=\$1 + local value=\$2 + + python3 - "\${config_path}" "\${key}" "\${value}" <<'PY' +import pathlib +import sys + +path = pathlib.Path(sys.argv[1]) +key = sys.argv[2] +value = sys.argv[3] +section = "[openshell.drivers.podman]" +lines = path.read_text(encoding="utf-8").splitlines() +try: + start = next(index for index, line in enumerate(lines) if line.strip() == section) +except StopIteration: + raise SystemExit(f"{section} not found in {path}") +end = len(lines) +for index in range(start + 1, len(lines)): + if lines[index].lstrip().startswith("["): + end = index + break +for line in lines[start + 1:end]: + if line.split("=", 1)[0].strip() == key: + raise SystemExit(0) +lines.insert(end, f"{key} = {value}") +path.write_text("\\n".join(lines) + "\\n", encoding="utf-8") +PY + } + + write_gateway_args_file() { + : >"\${gateway_args_file}" + for arg in "\$@"; do + printf '%s\0' "\${arg}" >>"\${gateway_args_file}" + done + } + + stop_gateway() { + local gateway_pid= + if [ -f "\${gateway_pid_file}" ]; then + gateway_pid=\$(cat "\${gateway_pid_file}" 2>/dev/null || true) + fi + if [ -n "\${gateway_pid}" ] && kill -0 "\${gateway_pid}" 2>/dev/null; then + kill "\${gateway_pid}" 2>/dev/null || true + for _ in \$(seq 1 60); do + kill -0 "\${gateway_pid}" 2>/dev/null || break + sleep 0.5 + done + kill -KILL "\${gateway_pid}" 2>/dev/null || true + wait "\${gateway_pid}" 2>/dev/null || true + fi + rm -f "\${gateway_pid_file}" 2>/dev/null || true + } + + cleanup_guest_tests() { + local status=\$? + trap - EXIT INT TERM + stop_gateway + if [ "\${status}" -ne 0 ] && [ -f "\${gateway_log}" ]; then + echo "=== guest gateway log ===" >&2 + cat "\${gateway_log}" >&2 + echo "=== end guest gateway log ===" >&2 + fi + exit "\${status}" + } + trap cleanup_guest_tests EXIT + trap 'exit 130' INT + trap 'exit 143' TERM + + export OPENSHELL_BIN=/usr/local/bin/openshell + export OPENSHELL_GATEWAY_ENDPOINT=http://127.0.0.1:${guest_port} + export OPENSHELL_GATEWAY=openshell-e2e-vm-${guest_port} + export OPENSHELL_PROVISION_TIMEOUT=\${OPENSHELL_PROVISION_TIMEOUT:-300} + export OPENSHELL_E2E_TESTS_IN_VM=1 + if [ '${gateway_driver}' = podman ]; then + export CONTAINER_ENGINE=podman + export OPENSHELL_E2E_DRIVER=podman + export OPENSHELL_E2E_NETWORK_NAME='${guest_e2e_network_name}' + export OPENSHELL_E2E_SANDBOX_NAMESPACE='${guest_e2e_network_name}' + export XDG_RUNTIME_DIR="\${XDG_RUNTIME_DIR:-/run/user/\$(id -u)}" + export OPENSHELL_PODMAN_SOCKET="\${XDG_RUNTIME_DIR}/podman/podman.sock" + export CONTAINER_HOST="unix://\${OPENSHELL_PODMAN_SOCKET}" + export OPENSHELL_E2E_CONTAINER_ENGINE_UNSET_XDG_CONFIG_HOME=1 + insert_podman_config_key socket_path "\$(toml_string "\${OPENSHELL_PODMAN_SOCKET}")" + insert_podman_config_key enable_bind_mounts true + + provider_spiffe_port=\$(pick_free_port) + export OPENSHELL_E2E_GATEWAY_SPIFFE_SOCKET="\${spiffe_root}/gateway.sock" + export OPENSHELL_GATEWAY_SPIFFE_WORKLOAD_API_SOCKET="\${OPENSHELL_E2E_GATEWAY_SPIFFE_SOCKET}" + export OPENSHELL_E2E_PROVIDER_SPIFFE_LISTEN="0.0.0.0:\${provider_spiffe_port}" + export OPENSHELL_E2E_PROVIDER_SPIFFE_SOCKET="tcp:169.254.1.2:\${provider_spiffe_port}" + insert_podman_config_key provider_spiffe_workload_api_socket "\$(toml_string "\${OPENSHELL_E2E_PROVIDER_SPIFFE_SOCKET}")" + fi + + gateway_args=( + --config "\${config_path}" + --bind-address 127.0.0.1 + --port ${guest_port} + --disable-tls + ) + write_gateway_args_file "\${gateway_args[@]}" + export OPENSHELL_E2E_GATEWAY_BIN=/usr/local/bin/openshell-gateway + export OPENSHELL_E2E_GATEWAY_ARGS_FILE="\${gateway_args_file}" + export OPENSHELL_E2E_GATEWAY_LOG="\${gateway_log}" + export OPENSHELL_E2E_GATEWAY_PID_FILE="\${gateway_pid_file}" + + /usr/local/bin/openshell-gateway "\${gateway_args[@]}" >"\${gateway_log}" 2>&1 & + printf '%s\n' "\$!" >"\${gateway_pid_file}" + + echo "==> Waiting for guest gateway readiness" + gateway_ready=0 + for _ in \$(seq 1 "${gateway_ready_timeout}"); do + if ! kill -0 "\$(cat "\${gateway_pid_file}")" 2>/dev/null; then + echo "ERROR: guest gateway exited before becoming ready" >&2 + exit 1 + fi + if NO_COLOR=1 /usr/local/bin/openshell status >/tmp/openshell-e2e-status.log 2>&1 && + grep -q "Connected" /tmp/openshell-e2e-status.log; then + gateway_ready=1 + break + fi + sleep 1 + done + if [ "\${gateway_ready}" -ne 1 ]; then + echo "ERROR: guest gateway did not become ready" >&2 + cat /tmp/openshell-e2e-status.log >&2 || true + exit 1 + fi + + while IFS= read -r test_bin <&3; do + [ -n "\${test_bin}" ] || continue + echo "==> Running guest E2E artifact: \${test_bin##*/}" + "\${test_bin}" --nocapture Running prebuilt E2E test artifacts inside ${vm} test guest" + "${vm_args[@]}" + exit $? + fi + echo "==> Starting ${vm} test guest gateway at ${gateway_endpoint}" start_child "${ROOT}" "${runtime_log}" "${vm_args[@]}" fi @@ -588,10 +926,17 @@ wait_for_gateway() { wait_for_gateway -echo "==> Running E2E suite: ${suite_name}" +test_args=( + cargo test + --manifest-path e2e/rust/Cargo.toml + --features "${e2e_features}" +) +echo "==> Running E2E features: ${e2e_features}" +if [ -n "${suite_name}" ]; then + echo "==> Running E2E suite: ${suite_name}" + test_args+=(--test "${suite_name}") +fi +test_args+=(-- --nocapture) + cd "${ROOT}" -cargo test \ - --manifest-path e2e/rust/Cargo.toml \ - --features e2e \ - --test "${suite_name}" \ - -- --nocapture +"${test_args[@]}" diff --git a/e2e/rust/Cargo.toml b/e2e/rust/Cargo.toml index 44881e1682..b0941eee08 100644 --- a/e2e/rust/Cargo.toml +++ b/e2e/rust/Cargo.toml @@ -32,6 +32,7 @@ e2e-kubernetes-credential-drivers = ["e2e-kubernetes"] e2e-kubernetes-workspace-managed = ["e2e-kubernetes"] e2e-kubernetes-workspace-operator = ["e2e-kubernetes"] e2e-podman = ["e2e", "e2e-host-gateway", "e2e-local-container-driver"] +e2e_podman = ["e2e-podman"] e2e-podman-gpu = ["e2e-podman", "e2e-gpu"] e2e-oidc-pkce = [] e2e-provider-refresh-keycloak = [] diff --git a/e2e/rust/tests/live_policy_update.rs b/e2e/rust/tests/live_policy_update.rs index 7a1e12923a..4cded2354b 100644 --- a/e2e/rust/tests/live_policy_update.rs +++ b/e2e/rust/tests/live_policy_update.rs @@ -56,6 +56,11 @@ ENV OPENSHELL_POLICY_POLL_INTERVAL_SECS=1 CMD ["sleep", "infinity"] "#; +const SPARSE_POLICY: &str = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../examples/policy-advisor/sandbox-policy.yaml" +)); + // --------------------------------------------------------------------------- // Policy YAML builders // --------------------------------------------------------------------------- @@ -146,6 +151,15 @@ landlock: Ok(file) } +fn write_sparse_policy() -> Result { + let mut file = NamedTempFile::new().map_err(|e| format!("create temp policy file: {e}"))?; + file.write_all(SPARSE_POLICY.as_bytes()) + .map_err(|e| format!("write temp policy file: {e}"))?; + file.flush() + .map_err(|e| format!("flush temp policy file: {e}"))?; + Ok(file) +} + #[cfg(feature = "e2e-docker")] fn write_local_override_image() -> Result { let dir = tempfile::tempdir().map_err(|e| format!("create image context: {e}"))?; @@ -521,18 +535,18 @@ async fn live_policy_update_from_empty_network_policies() { /// no revision remaining `Pending` once the acknowledgement lands. #[tokio::test] async fn initial_sparse_policy_is_acknowledged_as_loaded() { - // Repo-relative path to the sparse network-only policy fixture. - let sparse_policy = concat!( - env!("CARGO_MANIFEST_DIR"), - "/../../examples/policy-advisor/sandbox-policy.yaml" - ); + let sparse_policy = write_sparse_policy().expect("write sparse policy fixture"); + let sparse_policy_path = sparse_policy + .path() + .to_str() + .expect("sparse policy path is not UTF-8"); let mut guard = SandboxGuard::create_keep_with_args( &[ "--name", "e2e-sparse-enrich", "--policy", - sparse_policy, + sparse_policy_path, "--no-tty", ], &["sh", "-c", "echo Ready && sleep infinity"], diff --git a/e2e/rust/tests/transparent_tcp.rs b/e2e/rust/tests/transparent_tcp.rs index 1027654641..aef5a39d87 100644 --- a/e2e/rust/tests/transparent_tcp.rs +++ b/e2e/rust/tests/transparent_tcp.rs @@ -141,6 +141,13 @@ async fn rootless_podman_musl_getaddrinfo_uses_udp_policy_dns() { if !is_e2e_driver("podman") { return; } + if std::env::var_os("OPENSHELL_E2E_TESTS_IN_VM").is_some() { + eprintln!( + "skipping musl DNS probe test in guest prebuilt-artifact mode; \ + restore with a prebuilt probe artifact tracked in #3009" + ); + return; + } let probe = MuslDnsProbe::build().expect("build static musl DNS probe"); let fixture = SupportContainer::start_python( diff --git a/tasks/test.toml b/tasks/test.toml index 29e06b826a..3c31aa7eee 100644 --- a/tasks/test.toml +++ b/tasks/test.toml @@ -136,6 +136,10 @@ run = [ "CONTAINER_RUNTIME=docker e2e/with-keycloak.sh env OPENSHELL_E2E_OIDC_GATEWAY=1 e2e/with-docker-gateway.sh uv run pytest -m 'not gpu' e2e/python/oidc", ] +["e2e:podman:rootless"] +description = "Run Rust Podman e2e inside a rootless Ubuntu 26.04 Nix test guest" +run = "e2e/run.sh --vm ubuntu-26-04 --with podman-rootless --tests-in-vm --gateway-config e2e/configs/gateway/podman.toml --features e2e_podman" + ["e2e:podman:gpu"] description = "Run GPU e2e against a standalone gateway with the Podman compute driver" env = { OPENSHELL_E2E_PODMAN_GPU = "1", OPENSHELL_E2E_PODMAN_TEST = "gpu", OPENSHELL_E2E_PODMAN_FEATURES = "e2e-podman-gpu" } From 0e126be0a9350355b531e0d2e1b4e3af05d4ec82 Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Mon, 31 Aug 2026 12:08:10 +0200 Subject: [PATCH 4/4] ci(e2e): run rootless Podman guest lane Signed-off-by: Evan Lezar --- .github/actions/setup-e2e-sandbox/action.yml | 27 ++++++ .github/workflows/branch-e2e.yml | 11 ++- .../workflows/e2e-podman-rootless-test.yml | 89 +++++++++++++++++++ .github/workflows/release-dev.yml | 10 ++- .github/workflows/release-tag.yml | 12 ++- 5 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 .github/actions/setup-e2e-sandbox/action.yml create mode 100644 .github/workflows/e2e-podman-rootless-test.yml diff --git a/.github/actions/setup-e2e-sandbox/action.yml b/.github/actions/setup-e2e-sandbox/action.yml new file mode 100644 index 0000000000..2fa814bea1 --- /dev/null +++ b/.github/actions/setup-e2e-sandbox/action.yml @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Setup E2E Sandbox +description: Download an architecture-matched prebuilt OpenShell sandbox binary for E2E tests + +runs: + using: composite + steps: + - name: Download prebuilt sandbox + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ runner.arch == 'X64' && 'openshell-sandbox-x86_64-unknown-linux-musl' || 'openshell-sandbox-aarch64-unknown-linux-musl' }} + path: .e2e/prebuilt-sandbox + + - name: Configure prebuilt sandbox + shell: bash + run: | + set -euo pipefail + sandbox="$GITHUB_WORKSPACE/.e2e/prebuilt-sandbox/openshell-sandbox" + if [[ ! -f "$sandbox" ]]; then + echo "downloaded artifact is missing $sandbox" >&2 + exit 1 + fi + chmod +x "$sandbox" + "$sandbox" --version + echo "OPENSHELL_SANDBOX_BIN=$sandbox" >> "$GITHUB_ENV" diff --git a/.github/workflows/branch-e2e.yml b/.github/workflows/branch-e2e.yml index 0b050d0e51..abe91721da 100644 --- a/.github/workflows/branch-e2e.yml +++ b/.github/workflows/branch-e2e.yml @@ -263,6 +263,15 @@ jobs: with: image-tag: ${{ github.sha }} + podman-rootless-e2e: + needs: [pr_metadata, build-cli, build-gateway, build-sandbox] + if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' + permissions: + actions: read + contents: read + packages: read + uses: ./.github/workflows/e2e-podman-rootless-test.yml + vm-e2e: needs: [pr_metadata, build-cli, build-gateway, build-vm-driver] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' @@ -428,7 +437,7 @@ jobs: core-e2e-result: name: Core E2E result - needs: [pr_metadata, docker-e2e, podman-e2e, vm-e2e, docker-external-driver-e2e, podman-external-driver-e2e, vm-external-driver-e2e, kubernetes-e2e, kubernetes-external-driver-e2e, kubernetes-workspace-managed-e2e, kubernetes-workspace-operator-e2e] + needs: [pr_metadata, docker-e2e, podman-e2e, podman-rootless-e2e, vm-e2e, docker-external-driver-e2e, podman-external-driver-e2e, vm-external-driver-e2e, kubernetes-e2e, kubernetes-external-driver-e2e, kubernetes-workspace-managed-e2e, kubernetes-workspace-operator-e2e] if: always() && needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' runs-on: ubuntu-latest steps: diff --git a/.github/workflows/e2e-podman-rootless-test.yml b/.github/workflows/e2e-podman-rootless-test.yml new file mode 100644 index 0000000000..6ca2686452 --- /dev/null +++ b/.github/workflows/e2e-podman-rootless-test.yml @@ -0,0 +1,89 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Rootless Podman E2E Test + +on: + workflow_call: + inputs: + checkout-ref: + required: false + type: string + default: "" + gateway-artifact: + required: false + type: string + default: "" + +permissions: + actions: read + contents: read + packages: read + +jobs: + e2e: + name: E2E (rust-podman-rootless, Ubuntu 26.04 Nix VM) + # Run rootless Podman inside a Nix-managed Ubuntu guest so Podman, pasta, + # and user-namespace setup are provisioned by versioned repository tooling + # rather than mutable hosted-runner packages. + runs-on: ubuntu-26.04 + timeout-minutes: 60 + env: + MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.checkout-ref || github.sha }} + persist-credentials: false + + - uses: ./.github/actions/setup-e2e-cli + + - uses: ./.github/actions/setup-e2e-gateway + with: + artifact-name: ${{ inputs.gateway-artifact }} + + - uses: ./.github/actions/setup-e2e-sandbox + + - uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1 + with: + github_access_token: ${{ secrets.GITHUB_TOKEN }} + + - uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17 + with: + name: openshell + + - name: Install mise + run: | + curl https://mise.run | MISE_VERSION=v2026.4.25 sh + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + echo "$HOME/.local/share/mise/shims" >> "$GITHUB_PATH" + + - name: Install tools + run: mise install --locked + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + build-essential \ + clang \ + cmake \ + libclang-dev \ + libssl-dev \ + libz3-dev \ + openssh-client \ + pkg-config + + - name: Run rootless Podman E2E + run: | + set -euo pipefail + + mise x -- e2e/run.sh \ + --vm ubuntu-26-04 \ + --with podman-rootless \ + --tests-in-vm \ + --cli-bin "$OPENSHELL_BIN" \ + --gateway-bin "$OPENSHELL_GATEWAY_BIN" \ + --sandbox-bin "$OPENSHELL_SANDBOX_BIN" \ + --gateway-config e2e/configs/gateway/podman.toml \ + --features e2e_podman diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index 7982a81643..a38cccd569 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -145,6 +145,14 @@ jobs: with: image-tag: ${{ github.sha }} + podman-rootless-e2e: + needs: [build-cli, build-gateway, build-sandbox] + permissions: + actions: read + contents: read + packages: read + uses: ./.github/workflows/e2e-podman-rootless-test.yml + vm-e2e: needs: [build-cli, build-gateway, build-vm-driver] permissions: @@ -339,7 +347,7 @@ jobs: # --------------------------------------------------------------------------- release-dev: name: Release Dev - needs: [compute-versions, package-binaries, build-python-wheel, docker-e2e, podman-e2e, vm-e2e, build-deb, build-rpm, build-snap, smoke-linux-dev-artifacts] + needs: [compute-versions, package-binaries, build-python-wheel, docker-e2e, podman-e2e, podman-rootless-e2e, vm-e2e, build-deb, build-rpm, build-snap, smoke-linux-dev-artifacts] runs-on: linux-amd64-cpu8 timeout-minutes: 10 permissions: diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 92eb4adf14..ba6a851cf4 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -175,6 +175,16 @@ jobs: image-tag: ${{ needs.compute-versions.outputs.source_sha }} checkout-ref: ${{ inputs.tag || github.ref }} + podman-rootless-e2e: + needs: [compute-versions, build-cli, build-gateway, build-sandbox] + permissions: + actions: read + contents: read + packages: read + uses: ./.github/workflows/e2e-podman-rootless-test.yml + with: + checkout-ref: ${{ inputs.tag || github.ref }} + vm-e2e: needs: [compute-versions, build-cli, build-gateway, build-vm-driver] permissions: @@ -420,7 +430,7 @@ jobs: # --------------------------------------------------------------------------- release: name: Release - needs: [compute-versions, package-binaries, build-python-wheel, docker-e2e, podman-e2e, vm-e2e, build-deb, build-rpm, build-snap, smoke-linux-release-artifacts] + needs: [compute-versions, package-binaries, build-python-wheel, docker-e2e, podman-e2e, podman-rootless-e2e, vm-e2e, build-deb, build-rpm, build-snap, smoke-linux-release-artifacts] runs-on: linux-amd64-cpu8 timeout-minutes: 10 permissions: