diff --git a/.github/workflows/branch-checks.yml b/.github/workflows/branch-checks.yml index 1615a94925..c4e8dca1a5 100644 --- a/.github/workflows/branch-checks.yml +++ b/.github/workflows/branch-checks.yml @@ -143,6 +143,7 @@ jobs: cargo fmt --all -- --check cargo fmt --manifest-path e2e/support/rust/Cargo.toml --all -- --check cargo fmt --manifest-path e2e/suites/conformance/Cargo.toml --all -- --check + cargo fmt --manifest-path e2e/suites/drivers/Cargo.toml --all -- --check cargo fmt --manifest-path e2e/rust/Cargo.toml --all -- --check cargo fmt --manifest-path examples/governance-interceptor/Cargo.toml --all -- --check cargo fmt --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml --all -- --check @@ -200,6 +201,7 @@ jobs: cargo clippy --locked --workspace --all-targets -- -D warnings cargo clippy --locked --manifest-path e2e/support/rust/Cargo.toml --workspace --all-targets -- -D warnings cargo clippy --locked --manifest-path e2e/suites/conformance/Cargo.toml --workspace --all-targets -- -D warnings + cargo clippy --locked --manifest-path e2e/suites/drivers/Cargo.toml --workspace --all-targets -- -D warnings cargo clippy --locked --manifest-path e2e/rust/Cargo.toml --all-targets -- -D warnings cargo clippy --locked --manifest-path examples/governance-interceptor/Cargo.toml --all-targets -- -D warnings cargo clippy --locked --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml --all-targets -- -D warnings @@ -241,6 +243,7 @@ jobs: cargo nextest run --locked --profile ci --workspace --features openshell-server/test-support cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path e2e/support/rust/Cargo.toml cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path e2e/suites/conformance/Cargo.toml --package openshell-test-conformance-cli -E 'binary(policy_advisor) & test(draft_assertion::tests::)' + cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path e2e/suites/drivers/Cargo.toml --package openshell-test-suite-podman -E 'test(::tests::)' cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml rust-build-modes: diff --git a/TESTING.md b/TESTING.md index 015280350f..ae1bc2f395 100644 --- a/TESTING.md +++ b/TESTING.md @@ -336,7 +336,14 @@ The `e2e-podman` testsuite runs a nextest archive built with the corresponding Rust feature and preloads its Python workload image into the rootless Podman store. The separate `driver-podman` testsuite compares OpenShell and direct Podman user-namespace mappings for the default, `auto`, `keep-id`, and private -profiles. The E2E archive excludes binaries that still depend on wrapper-owned +profiles. Each profile is a named Rust test. The suite runs serially against +the shared gateway; a guest-side file lock also protects separate invocations. +The Rust fixture restores the original gateway configuration, restarts and +checks gateway health, and verifies sandbox cleanup after each test, including +assertion failures. A dirty marker stops later tests after interruption or failed +restoration; start a fresh tmachine invocation to recover. + +The E2E archive excludes binaries that still depend on wrapper-owned gateway controls, host fixtures, missing guest tools, or nondeterministic relay setup. The `driver-podman` suite replaces the removed `podman_userns` E2E binary. `tests/artifacts.nix` keeps the follow-up exclusions explicit and uses diff --git a/e2e/suites/drivers/Cargo.lock b/e2e/suites/drivers/Cargo.lock index 17daf87176..29c3ac91f3 100644 --- a/e2e/suites/drivers/Cargo.lock +++ b/e2e/suites/drivers/Cargo.lock @@ -20,6 +20,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.14" @@ -36,6 +42,30 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -48,6 +78,22 @@ dependencies = [ "wasip2", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "itoa" version = "1.0.18" @@ -104,9 +150,11 @@ dependencies = [ name = "openshell-test-suite-podman" version = "0.0.0" dependencies = [ + "futures-util", "openshell-e2e-support", "serde", "tokio", + "toml_edit", ] [[package]] @@ -243,6 +291,12 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "socket2" version = "0.6.5" @@ -315,6 +369,30 @@ dependencies = [ "syn 3.0.5", ] +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -351,6 +429,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.57.1" diff --git a/e2e/suites/drivers/podman/Cargo.toml b/e2e/suites/drivers/podman/Cargo.toml index f828ebb1dd..7fb84d98a1 100644 --- a/e2e/suites/drivers/podman/Cargo.toml +++ b/e2e/suites/drivers/podman/Cargo.toml @@ -12,4 +12,11 @@ repository = "https://github.com/NVIDIA/OpenShell" [dependencies] openshell-e2e-support = { path = "../../../support/rust" } serde = { version = "1", features = ["derive"] } -tokio = { version = "1.43", features = ["macros", "rt"] } +tokio = { version = "1.43", features = ["macros", "rt", "process", "time", "io-util"] } + +[dev-dependencies] +futures-util = { version = "0.3", default-features = false, features = ["std"] } +toml_edit = "0.22" + +[features] +fixture-validation = [] diff --git a/e2e/suites/drivers/podman/README.md b/e2e/suites/drivers/podman/README.md new file mode 100644 index 0000000000..4f6f9f0ea4 --- /dev/null +++ b/e2e/suites/drivers/podman/README.md @@ -0,0 +1,56 @@ +# Podman driver tests + +These tests run inside a disposable tmachine guest against its shared Podman +gateway. Rootful and rootless Podman are separate tmachine environments. + +The `user_namespaces` Cargo test target contains four story modules: `default`, +`auto`, `keep_id`, and `private`. Each compares OpenShell's UID mapping with a +direct Podman container in the gateway service user's execution context, and +checks that the non-root workload owns and can write to its managed workspace. +Both commands use the same workload image. + +## Gateway fixture + +The Rust fixture requires `/etc/openshell/gateway.toml`, the +`openshell-gateway.service` systemd unit, a registered candidate CLI, and +passwordless sudo in the guest. tmachine prepares the writable fixture directory +at `/var/lib/openshell-driver-tests/podman`. This is a dedicated test gateway: +its baseline must omit `userns`, `uidmap`, and `gidmap` and contain no sandboxes. + +Ansible installs the archive and invokes nextest once with `--test-threads 1`. +A guest-side file lock also serializes separate Cargo or nextest invocations. +Under the lock, Rust captures the original config, applies the selected profile, +and restarts and checks gateway readiness. After success, an error, or an +assertion panic, it deletes tracked sandboxes, restores the exact original +configuration, restarts the gateway, and verifies health and sandbox absence. +Scenario failures and restoration failures remain test failures. + +Before changing the gateway, the fixture writes a dirty marker. It removes that +marker only after successful restoration. An interrupted process or failed +restoration therefore prevents subsequent stories from treating modified state +as their baseline. Start a fresh tmachine invocation to recover; each invocation +uses a disposable overlay over the cached installed guest image. + +## Run + +Build the candidate runtime inputs and driver archive, then run either environment: + +```shell +nix run .#build-podman-driver-test-archive +nix run .#tmachine -- test fedora-podman-rootful binaries driver-podman +nix run .#tmachine -- test fedora-podman-rootless binaries driver-podman +``` + +The tests may restart the gateway and require guest privileges. Run the pure +configuration and UID-map assertions on the development host with: + +```shell +cargo test --locked --manifest-path e2e/suites/drivers/Cargo.toml \ + --package openshell-test-suite-podman --test user_namespaces ::tests:: +``` + +Inside a prepared guest, a nextest test-name filter selects one profile. The +`fixture-validation` Cargo feature adds a deliberately failing panic scenario +for validating cleanup; it is excluded from normal archives. Execute that +scenario explicitly, require failure and successful restoration, then run a +normal profile in the same guest to verify recovery. diff --git a/e2e/suites/drivers/podman/fixtures/userns-auto.toml b/e2e/suites/drivers/podman/fixtures/userns-auto.toml deleted file mode 100644 index 3701222828..0000000000 --- a/e2e/suites/drivers/podman/fixtures/userns-auto.toml +++ /dev/null @@ -1,4 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -userns = "auto" diff --git a/e2e/suites/drivers/podman/fixtures/userns-keep-id.toml b/e2e/suites/drivers/podman/fixtures/userns-keep-id.toml deleted file mode 100644 index c52cdb528f..0000000000 --- a/e2e/suites/drivers/podman/fixtures/userns-keep-id.toml +++ /dev/null @@ -1,4 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -userns = "keep-id" diff --git a/e2e/suites/drivers/podman/fixtures/userns-private.toml b/e2e/suites/drivers/podman/fixtures/userns-private.toml deleted file mode 100644 index 48a7968d13..0000000000 --- a/e2e/suites/drivers/podman/fixtures/userns-private.toml +++ /dev/null @@ -1,6 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -userns = "private" -uidmap = ["0:0:1", "1:1:65535"] -gidmap = ["0:0:1", "1:1:65535"] diff --git a/e2e/suites/drivers/podman/tests/default_userns.rs b/e2e/suites/drivers/podman/tests/default_userns.rs deleted file mode 100644 index ce5f81ab20..0000000000 --- a/e2e/suites/drivers/podman/tests/default_userns.rs +++ /dev/null @@ -1,140 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Podman-driver user-namespace integration tests. - -mod support; - -use openshell_e2e_support::OpenShellRunner; -use std::fs; -use std::path::PathBuf; -use std::time::Duration; - -use support::assert_podman_gateway; - -const SANDBOX_TIMEOUT: Duration = Duration::from_secs(300); -const PODMAN_TEST_INPUT_DIR_ENV: &str = "OPENSHELL_TEST_INPUT_DIR"; -const PODMAN_TEST_IMAGE_ENV: &str = "OPENSHELL_PODMAN_TEST_IMAGE"; - -const WORKSPACE_AND_UID_MAP_PROBE: &str = r#"set -eu -workload_owner="$(id -u):$(id -g)" -workspace_owner="$(stat -c '%u:%g' /sandbox)" -printf 'workload-owner=%s\nworkspace-owner=%s\n' "$workload_owner" "$workspace_owner" -test "$(id -u)" -ne 0 -test "$workspace_owner" = "$workload_owner" -probe=$(mktemp /sandbox/userns-probe.XXXXXX) -printf 'workspace probe\n' > "$probe" -rm "$probe" -echo podman-userns-workspace-ok -cat /proc/self/uid_map -"#; - -/// Verify that the gateway's user-namespace configuration matches Podman's -/// direct behavior for the same profile and preserves workspace access. -/// -/// The test runs a short-lived sandbox command and compares its user-namespace -/// mapping with the direct-Podman reference stored at -/// `OPENSHELL_TEST_INPUT_DIR/reference-uid-map`. The tmachine pre-test -/// playbook creates that reference in the same gateway-user context. This deliberately -/// avoids baking a particular Podman mapping into OpenShell's test contract. -/// The workload also verifies that the managed workspace is owned by its -/// non-root UID/GID and that it can create, write, and remove a file there. -#[tokio::test] -async fn configured_userns_matches_podman_reference() { - let mut runner = - OpenShellRunner::from_env("podman-userns").expect("candidate openshell CLI is available"); - let result = async { - runner.check_gateway_status().await?; - assert_podman_gateway(&runner).await?; - - let test_input_dir = std::env::var_os(PODMAN_TEST_INPUT_DIR_ENV) - .map(PathBuf::from) - .ok_or_else(|| format!("{PODMAN_TEST_INPUT_DIR_ENV} must name the Podman test-input directory"))?; - let expected_path = test_input_dir.join("reference-uid-map"); - let expected_uid_map = fs::read_to_string(&expected_path).map_err(|error| { - format!( - "could not read Podman reference UID map {}: {error}", - expected_path.display() - ) - })?; - let expected_uid_map = normalize_uid_map(&expected_uid_map).ok_or_else(|| { - format!( - "Podman reference UID map {} contains no mappings", - expected_path.display() - ) - })?; - - let workload_image = std::env::var(PODMAN_TEST_IMAGE_ENV) - .ok() - .filter(|image| !image.trim().is_empty()); - let sandbox_name = format!("pu-{}", runner.id()); - runner.track_sandbox(&sandbox_name); - let mut create_args = vec!["sandbox", "create", "--name", &sandbox_name]; - if let Some(image) = workload_image.as_deref() { - create_args.extend(["--from", image]); - } - create_args.extend(["--no-tty", "--", "sh", "-c", WORKSPACE_AND_UID_MAP_PROBE]); - let run = runner - .step("userns/workspace-and-uid-map") - .description("sandbox can write to its owned workspace and exposes its UID map") - .with_timeout(SANDBOX_TIMEOUT) - .run(&create_args) - .await - .map_err(|error| error.to_string())?; - run.require_success()?; - if !run.stdout().contains("podman-userns-workspace-ok") { - return Err(run.failure_diagnostic("non-root workload owns and can write to /sandbox")); - } - let sandbox_uid_map = normalize_uid_map(run.stdout()).ok_or_else(|| { - run.failure_diagnostic("sandbox returns a non-empty UID map") - })?; - if sandbox_uid_map != expected_uid_map { - return Err(format!( - "sandbox UID map differs from the direct Podman reference:\nexpected:\n{expected_uid_map}\nactual:\n{sandbox_uid_map}" - )); - } - Ok(()) - } - .await; - - if let Err(error) = runner.finish(result).await { - panic!("Podman userns test failed:\n{error}"); - } -} - -fn normalize_uid_map(value: &str) -> Option { - let mut mappings: Vec<(u64, u64, u64)> = Vec::new(); - for line in value.lines() { - let fields = line - .split_whitespace() - .map(str::parse::) - .collect::, _>>(); - let Ok(fields) = fields else { continue }; - let [inside, outside, length] = fields.as_slice() else { - continue; - }; - if let Some(previous) = mappings.last_mut() - && previous.0.checked_add(previous.2) == Some(*inside) - && previous.1.checked_add(previous.2) == Some(*outside) - { - previous.2 += *length; - } else { - mappings.push((*inside, *outside, *length)); - } - } - (!mappings.is_empty()).then(|| { - mappings - .iter() - .map(|(inside, outside, length)| format!("{inside} {outside} {length}")) - .collect::>() - .join("\n") - }) -} - -#[test] -fn adjacent_uid_ranges_match_a_combined_mapping() { - assert_eq!( - normalize_uid_map("0 0 1\n1 1 65535\n"), - normalize_uid_map("0 0 65536\n") - ); -} diff --git a/e2e/suites/drivers/podman/tests/user_namespaces/auto.rs b/e2e/suites/drivers/podman/tests/user_namespaces/auto.rs new file mode 100644 index 0000000000..08bf2ba3ed --- /dev/null +++ b/e2e/suites/drivers/podman/tests/user_namespaces/auto.rs @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use super::gateway::{Profile, with_profile}; +use super::helpers::assert_workspace_and_uid_map; + +#[tokio::test] +async fn mapping_matches_podman_and_preserves_workspace_access() { + with_profile(Profile::Auto, async |gateway, runner| { + assert_workspace_and_uid_map(gateway, runner).await + }) + .await + .expect("auto Podman user namespace preserves workspace access"); +} diff --git a/e2e/suites/drivers/podman/tests/user_namespaces/default.rs b/e2e/suites/drivers/podman/tests/user_namespaces/default.rs new file mode 100644 index 0000000000..ef621982aa --- /dev/null +++ b/e2e/suites/drivers/podman/tests/user_namespaces/default.rs @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use super::gateway::{Profile, with_profile}; +use super::helpers::assert_workspace_and_uid_map; + +#[tokio::test] +async fn mapping_matches_podman_and_preserves_workspace_access() { + with_profile(Profile::Default, async |gateway, runner| { + assert_workspace_and_uid_map(gateway, runner).await + }) + .await + .expect("default Podman user namespace preserves workspace access"); +} diff --git a/e2e/suites/drivers/podman/tests/user_namespaces/gateway.rs b/e2e/suites/drivers/podman/tests/user_namespaces/gateway.rs new file mode 100644 index 0000000000..327014f73e --- /dev/null +++ b/e2e/suites/drivers/podman/tests/user_namespaces/gateway.rs @@ -0,0 +1,414 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! The caller must use a disposable tmachine guest with passwordless sudo. +//! A lock spans profile setup, assertions, sandbox cleanup, and restoration. +//! A dirty marker survives panic/termination or failed restoration: later tests +//! refuse to adopt a changed configuration as their baseline. + +use futures_util::FutureExt; +use openshell_e2e_support::OpenShellRunner; +use serde::Deserialize; +use std::fs::{self, File, OpenOptions, TryLockError}; +use std::panic::AssertUnwindSafe; +use std::path::PathBuf; +use std::process::Stdio; +use std::time::{Duration, Instant}; +use tokio::io::AsyncWriteExt; +use tokio::process::Command; +use tokio::time::{sleep, timeout}; +use toml_edit::{DocumentMut, value}; + +use super::support::assert_podman_gateway; + +const CONFIG: &str = "/etc/openshell/gateway.toml"; +const SERVICE: &str = "openshell-gateway.service"; +const FIXTURE_DIR: &str = "/var/lib/openshell-driver-tests/podman"; +const DEFAULT_IMAGE: &str = "nvcr.io/nvidia/base/ubuntu:24.04"; +const COMMAND_TIMEOUT: Duration = Duration::from_secs(120); + +#[derive(Clone, Copy, Debug)] +pub enum Profile { + Default, + Auto, + KeepId, + Private, +} + +impl Profile { + fn arguments(self) -> &'static [&'static str] { + match self { + Self::Default => &[], + Self::Auto => &["--userns", "auto"], + Self::KeepId => &["--userns", "keep-id"], + // Podman infers private from explicit maps, and rejects combining + // --userns private with --uidmap/--gidmap. + Self::Private => &[ + "--uidmap", + "0:0:1", + "--uidmap", + "1:1:65535", + "--gidmap", + "0:0:1", + "--gidmap", + "1:1:65535", + ], + } + } + + fn configuration(self, original: &str) -> Result { + let mut config = original + .parse::() + .map_err(|error| error.to_string())?; + let driver = &mut config["openshell"]["drivers"]["podman"]; + if !driver.is_table() { + return Err("gateway configuration has no Podman driver table".into()); + } + match self { + Self::Default => {} + Self::Auto => driver["userns"] = value("auto"), + Self::KeepId => driver["userns"] = value("keep-id"), + Self::Private => { + driver["userns"] = value("private"); + driver["uidmap"] = value( + ["0:0:1", "1:1:65535"] + .into_iter() + .collect::(), + ); + driver["gidmap"] = value( + ["0:0:1", "1:1:65535"] + .into_iter() + .collect::(), + ); + } + } + Ok(config.to_string()) + } +} + +pub struct GatewayFixture { + _lock: File, + original: String, + dirty: PathBuf, + profile: Profile, + user: String, + home: String, + uid: String, + image: String, +} + +impl GatewayFixture { + async fn acquire(profile: Profile) -> Result { + let lock = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(PathBuf::from(FIXTURE_DIR).join("gateway.lock")) + .map_err(|error| { + format!("open gateway lock (tmachine preparation required): {error}") + })?; + let started = Instant::now(); + loop { + match lock.try_lock() { + Ok(()) => break, + Err(TryLockError::WouldBlock) if started.elapsed() < COMMAND_TIMEOUT => { + sleep(Duration::from_millis(100)).await; + } + Err(error) => return Err(format!("acquire shared gateway lock: {error}")), + } + } + let dirty = PathBuf::from(FIXTURE_DIR).join("gateway-dirty"); + if dirty.exists() { + return Err( + "previous test did not restore the gateway; start a fresh tmachine invocation" + .into(), + ); + } + let original = fs::read_to_string(CONFIG).map_err(|error| error.to_string())?; + let parsed = original + .parse::() + .map_err(|error| error.to_string())?; + let driver = &parsed["openshell"]["drivers"]["podman"]; + if !driver.is_table() + || ["userns", "uidmap", "gidmap"] + .iter() + .any(|key| driver.get(key).is_some()) + { + return Err("shared gateway baseline must omit userns, uidmap, and gidmap".into()); + } + let user = command( + "systemctl", + &["show", SERVICE, "--property=User", "--value"], + None, + ) + .await?; + let user: String = if user.trim().is_empty() { + "root".into() + } else { + user.trim().into() + }; + let account = command("getent", &["passwd", &user], None).await?; + let fields: Vec<_> = account.trim().split(':').collect(); + if fields.len() != 7 { + return Err("gateway service user has no valid passwd entry".into()); + } + let fixture = Self { + _lock: lock, + original, + dirty, + profile, + uid: fields[2].into(), + home: fields[5].into(), + user, + image: std::env::var("OPENSHELL_PODMAN_TEST_IMAGE") + .ok() + .filter(|image| !image.trim().is_empty()) + .unwrap_or_else(|| DEFAULT_IMAGE.into()), + }; + normal_gateway().await?; + Ok(fixture) + } + + async fn apply(&self, runner: &mut OpenShellRunner) -> Result<(), String> { + // Set the marker before changing the gateway. The lock releases on + // process exit, but this marker must not be cleared by Drop. + fs::write(&self.dirty, format!("{:?}\n", self.profile)) + .map_err(|error| error.to_string())?; + let config = self.profile.configuration(&self.original)?; + command("sudo", &["-n", "tee", CONFIG], Some(&config)).await?; + restart_gateway(runner).await + } + + pub fn image(&self) -> &str { + &self.image + } + + async fn podman(&self, arguments: &[&str]) -> Result { + let home = format!("HOME={}", self.home); + let runtime = format!("XDG_RUNTIME_DIR=/run/user/{}", self.uid); + let mut args = vec!["-n", "-u", &self.user, "env", &home, &runtime, "podman"]; + args.extend_from_slice(arguments); + command("sudo", &args, None).await + } + + pub async fn reference_uid_map(&self) -> Result { + self.podman(&["pull", self.image()]).await?; + let mut args = vec!["run", "--rm", "--pull", "never"]; + args.extend_from_slice(self.profile.arguments()); + args.extend([ + "--entrypoint", + "/bin/cat", + self.image(), + "/proc/self/uid_map", + ]); + self.podman(&args).await + } + + async fn restore(&self) -> Result<(), String> { + // Deletion may be accepted while the gateway's cleanup worker is still + // running. Let it finish under the scenario profile before restarting. + // Even if that wait fails, attempt configuration restoration below. + if let Err(error) = normal_gateway().await { + eprintln!("gateway before restoration: {error}"); + } + command("sudo", &["-n", "tee", CONFIG], Some(&self.original)).await?; + command("sudo", &["-n", "systemctl", "restart", SERVICE], None).await?; + if fs::read_to_string(CONFIG).map_err(|error| error.to_string())? != self.original { + return Err("restored gateway configuration differs from the original".into()); + } + normal_gateway().await?; + fs::remove_file(&self.dirty).map_err(|error| error.to_string())?; + eprintln!("gateway restored: original configuration, healthy Podman gateway, no sandboxes"); + Ok(()) + } +} + +pub async fn with_profile(profile: Profile, scenario: F) -> Result<(), String> +where + F: AsyncFnOnce(&GatewayFixture, &mut OpenShellRunner) -> Result<(), String>, +{ + let fixture = GatewayFixture::acquire(profile).await?; + let mut runner = OpenShellRunner::from_env(&format!("podman-userns/{profile:?}")) + .map_err(|error| error.to_string())?; + let scenario_result = AssertUnwindSafe(async { + fixture.apply(&mut runner).await?; + scenario(&fixture, &mut runner).await + }) + .catch_unwind() + .await + .unwrap_or_else(|panic| { + let message = panic + .downcast_ref::() + .map(String::as_str) + .or_else(|| panic.downcast_ref::<&str>().copied()) + .unwrap_or("non-string panic"); + Err(format!("scenario panicked: {message}")) + }); + let result = runner.finish(scenario_result).await; + let restored = fixture.restore().await; + if result.is_err() || restored.is_err() { + let journal = command( + "sudo", + &[ + "-n", + "journalctl", + "--unit", + SERVICE, + "--no-pager", + "--lines", + "100", + ], + None, + ) + .await; + eprintln!( + "gateway failure diagnostics:\n{}", + journal.unwrap_or_else(|error| error) + ); + } + match (result, restored) { + (Ok(()), Ok(())) => Ok(()), + (Err(error), Ok(())) => Err(error), + (Ok(()), Err(error)) => Err(format!("gateway restoration failed: {error}")), + (Err(error), Err(restore)) => Err(format!( + "{error}\ngateway restoration also failed: {restore}" + )), + } +} + +async fn restart_gateway(runner: &mut OpenShellRunner) -> Result<(), String> { + command("sudo", &["-n", "systemctl", "restart", SERVICE], None).await?; + runner.check_gateway_status().await?; + assert_podman_gateway(runner).await +} + +#[derive(Deserialize)] +struct SandboxPage { + sandboxes: Vec, + next_page_token: String, +} + +async fn normal_gateway() -> Result<(), String> { + let mut runner = + OpenShellRunner::from_env("podman-userns/baseline").map_err(|error| error.to_string())?; + let result = async { + runner.check_gateway_status().await?; + assert_podman_gateway(&runner).await?; + let deadline = Instant::now() + Duration::from_secs(60); + loop { + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + return Err("timed out waiting for dedicated Podman test gateway to have no remaining sandboxes".into()); + } + let result = runner + .step("baseline/sandboxes") + .with_timeout(remaining.min(Duration::from_secs(10))) + .run(&["sandbox", "list", "--output", "json"]) + .await + .map_err(|error| error.to_string())?; + result.require_success()?; + let page = result + .json::() + .map_err(|error| error.to_string())?; + if page.sandboxes.is_empty() && page.next_page_token.is_empty() { + break; + } + eprintln!("waiting for sandbox cleanup: {} entries remain on the first page", page.sandboxes.len()); + sleep(Duration::from_millis(250)).await; + } + Ok(()) + } + .await; + runner.finish(result).await +} + +async fn command(program: &str, arguments: &[&str], input: Option<&str>) -> Result { + let operation = async { + let mut child = Command::new(program) + .args(arguments) + .kill_on_drop(true) + .stdin(if input.is_some() { + Stdio::piped() + } else { + Stdio::null() + }) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|error| error.to_string())?; + if let Some(input) = input { + let mut stdin = child.stdin.take().ok_or("command stdin was not piped")?; + stdin + .write_all(input.as_bytes()) + .await + .map_err(|error| error.to_string())?; + } + let output = child + .wait_with_output() + .await + .map_err(|error| error.to_string())?; + if !output.status.success() { + return Err(format!( + "{program} {arguments:?} exited {}:\n{}", + output.status, + String::from_utf8_lossy(&output.stderr) + )); + } + Ok(String::from_utf8_lossy(&output.stdout).into_owned()) + }; + timeout(COMMAND_TIMEOUT, operation) + .await + .map_err(|_| format!("{program} {arguments:?} timed out"))? +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn profiles_only_change_podman_user_namespace_settings() { + let baseline = "[openshell.drivers.podman]\nsocket_path = '/run/podman/podman.sock'\n[other]\nvalue = 7\n"; + for profile in [ + Profile::Default, + Profile::Auto, + Profile::KeepId, + Profile::Private, + ] { + let config = profile + .configuration(baseline) + .unwrap() + .parse::() + .unwrap(); + assert_eq!(config["other"]["value"].as_integer(), Some(7)); + assert_eq!( + config["openshell"]["drivers"]["podman"]["socket_path"].as_str(), + Some("/run/podman/podman.sock") + ); + } + let private = Profile::Private + .configuration(baseline) + .unwrap() + .parse::() + .unwrap(); + assert_eq!( + private["openshell"]["drivers"]["podman"]["uidmap"] + .as_array() + .unwrap() + .len(), + 2 + ); + } +} + +// Executed explicitly during fixture validation, not normal qualification. +#[cfg(feature = "fixture-validation")] +#[tokio::test] +async fn deliberate_panic_after_sandbox_creation() { + with_profile(Profile::Auto, async |gateway, runner| { + super::helpers::assert_workspace_and_uid_map(gateway, runner).await?; + panic!("injected scenario assertion failure"); + }) + .await + .expect("injected failure must remain a failed test"); +} diff --git a/e2e/suites/drivers/podman/tests/user_namespaces/helpers.rs b/e2e/suites/drivers/podman/tests/user_namespaces/helpers.rs new file mode 100644 index 0000000000..eefede4a6c --- /dev/null +++ b/e2e/suites/drivers/podman/tests/user_namespaces/helpers.rs @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use openshell_e2e_support::OpenShellRunner; +use std::time::Duration; + +use super::gateway::GatewayFixture; + +const WORKSPACE_AND_UID_MAP_PROBE: &str = r#"set -eu +workload_owner="$(id -u):$(id -g)" +workspace_owner="$(stat -c '%u:%g' /sandbox)" +printf 'workload-owner=%s\nworkspace-owner=%s\n' "$workload_owner" "$workspace_owner" +test "$(id -u)" -ne 0 +test "$workspace_owner" = "$workload_owner" +probe=$(mktemp /sandbox/userns-probe.XXXXXX) +printf 'workspace probe\n' > "$probe" +rm "$probe" +echo podman-userns-workspace-ok +cat /proc/self/uid_map +"#; + +pub async fn assert_workspace_and_uid_map( + gateway: &GatewayFixture, + runner: &mut OpenShellRunner, +) -> Result<(), String> { + let reference = gateway.reference_uid_map().await?; + let expected = normalize_uid_map(&reference) + .ok_or_else(|| "direct Podman returned no UID mappings".to_string())?; + let sandbox_name = format!("pu-{}", runner.id()); + runner.track_sandbox(&sandbox_name); + let result = runner + .step("userns/workspace-and-uid-map") + .description("non-root sandbox owns and can write to its workspace") + .with_timeout(Duration::from_secs(300)) + .run(&[ + "sandbox", + "create", + "--name", + &sandbox_name, + "--from", + gateway.image(), + "--no-tty", + "--", + "sh", + "-c", + WORKSPACE_AND_UID_MAP_PROBE, + ]) + .await + .map_err(|error| error.to_string())?; + result.require_success()?; + if !result.stdout().contains("podman-userns-workspace-ok") { + return Err(result.failure_diagnostic("non-root workload owns and can write to /sandbox")); + } + let actual = normalize_uid_map(result.stdout()) + .ok_or_else(|| result.failure_diagnostic("sandbox returns a non-empty UID map"))?; + if actual != expected { + return Err(format!( + "UID map differs from direct Podman:\nexpected:\n{expected}\nactual:\n{actual}" + )); + } + Ok(()) +} + +fn normalize_uid_map(value: &str) -> Option { + let mut mappings: Vec<(u64, u64, u64)> = Vec::new(); + for line in value.lines() { + let fields = line + .split_whitespace() + .map(str::parse::) + .collect::, _>>(); + let Ok(fields) = fields else { continue }; + let [inside, outside, length] = fields.as_slice() else { + continue; + }; + if let Some(previous) = mappings.last_mut() + && previous.0.checked_add(previous.2) == Some(*inside) + && previous.1.checked_add(previous.2) == Some(*outside) + { + previous.2 += *length; + } else { + mappings.push((*inside, *outside, *length)); + } + } + (!mappings.is_empty()).then(|| { + mappings + .iter() + .map(|(inside, outside, length)| format!("{inside} {outside} {length}")) + .collect::>() + .join("\n") + }) +} + +#[cfg(test)] +mod tests { + use super::normalize_uid_map; + + #[test] + fn adjacent_uid_ranges_match_a_combined_mapping() { + assert_eq!( + normalize_uid_map("0 0 1\n1 1 65535\n"), + normalize_uid_map("0 0 65536\n") + ); + } +} diff --git a/e2e/suites/drivers/podman/tests/user_namespaces/keep_id.rs b/e2e/suites/drivers/podman/tests/user_namespaces/keep_id.rs new file mode 100644 index 0000000000..b01dfec512 --- /dev/null +++ b/e2e/suites/drivers/podman/tests/user_namespaces/keep_id.rs @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use super::gateway::{Profile, with_profile}; +use super::helpers::assert_workspace_and_uid_map; + +#[tokio::test] +async fn mapping_matches_podman_and_preserves_workspace_access() { + with_profile(Profile::KeepId, async |gateway, runner| { + assert_workspace_and_uid_map(gateway, runner).await + }) + .await + .expect("keep_id Podman user namespace preserves workspace access"); +} diff --git a/e2e/suites/drivers/podman/tests/user_namespaces/main.rs b/e2e/suites/drivers/podman/tests/user_namespaces/main.rs new file mode 100644 index 0000000000..ca7c6199f1 --- /dev/null +++ b/e2e/suites/drivers/podman/tests/user_namespaces/main.rs @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Podman profiles run serially against the tmachine guest's shared gateway. + +mod auto; +mod default; +mod gateway; +mod helpers; +mod keep_id; +mod private; +#[path = "../support/mod.rs"] +mod support; diff --git a/e2e/suites/drivers/podman/tests/user_namespaces/private.rs b/e2e/suites/drivers/podman/tests/user_namespaces/private.rs new file mode 100644 index 0000000000..e531f12012 --- /dev/null +++ b/e2e/suites/drivers/podman/tests/user_namespaces/private.rs @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use super::gateway::{Profile, with_profile}; +use super::helpers::assert_workspace_and_uid_map; + +#[tokio::test] +async fn mapping_matches_podman_and_preserves_workspace_access() { + with_profile(Profile::Private, async |gateway, runner| { + assert_workspace_and_uid_map(gateway, runner).await + }) + .await + .expect("private Podman user namespace preserves workspace access"); +} diff --git a/tasks/rust.toml b/tasks/rust.toml index a89a2313eb..7937955b42 100644 --- a/tasks/rust.toml +++ b/tasks/rust.toml @@ -14,6 +14,7 @@ run = [ "cargo check --workspace", "cargo check --locked --manifest-path e2e/support/rust/Cargo.toml --workspace --all-targets", "cargo check --locked --manifest-path e2e/suites/conformance/Cargo.toml --workspace --all-targets", + "cargo check --locked --manifest-path e2e/suites/drivers/Cargo.toml --workspace --all-targets", "cargo check -p openshell-sandbox --all-targets --features perf-harness", ] run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-msvc.ps1 check native" @@ -32,6 +33,7 @@ run = [ "cargo clippy --workspace --all-targets -- -D warnings", "cargo clippy --locked --manifest-path e2e/support/rust/Cargo.toml --workspace --all-targets -- -D warnings", "cargo clippy --locked --manifest-path e2e/suites/conformance/Cargo.toml --workspace --all-targets -- -D warnings", + "cargo clippy --locked --manifest-path e2e/suites/drivers/Cargo.toml --workspace --all-targets -- -D warnings", "cargo clippy -p openshell-sandbox --all-targets --features perf-harness -- -D warnings", "cargo clippy --manifest-path e2e/rust/Cargo.toml --all-targets -- -D warnings", "cargo clippy --manifest-path examples/governance-interceptor/Cargo.toml --all-targets -- -D warnings", @@ -46,6 +48,7 @@ run = [ "cargo fmt --all", "cargo fmt --manifest-path e2e/support/rust/Cargo.toml --all", "cargo fmt --manifest-path e2e/suites/conformance/Cargo.toml --all", + "cargo fmt --manifest-path e2e/suites/drivers/Cargo.toml --all", "cargo fmt --manifest-path e2e/rust/Cargo.toml --all", "cargo fmt --manifest-path examples/governance-interceptor/Cargo.toml --all", "cargo fmt --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml --all", @@ -58,6 +61,7 @@ run = [ "cargo fmt --all -- --check", "cargo fmt --manifest-path e2e/support/rust/Cargo.toml --all -- --check", "cargo fmt --manifest-path e2e/suites/conformance/Cargo.toml --all -- --check", + "cargo fmt --manifest-path e2e/suites/drivers/Cargo.toml --all -- --check", "cargo fmt --manifest-path e2e/rust/Cargo.toml --all -- --check", "cargo fmt --manifest-path examples/governance-interceptor/Cargo.toml --all -- --check", "cargo fmt --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml --all -- --check", diff --git a/tasks/test.toml b/tasks/test.toml index 2c95d18b6b..3649750cbb 100644 --- a/tasks/test.toml +++ b/tasks/test.toml @@ -117,6 +117,7 @@ run = [ "cargo test -p openshell-server --features test-support", "cargo test --locked --manifest-path e2e/support/rust/Cargo.toml", "cargo test --locked --manifest-path e2e/suites/conformance/Cargo.toml --package openshell-test-conformance-cli --test policy_advisor draft_assertion::tests::", + "cargo test --locked --manifest-path e2e/suites/drivers/Cargo.toml --package openshell-test-suite-podman --test user_namespaces ::tests::", "cargo nextest run --config-file .config/nextest.toml --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml", ] run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-msvc.ps1 test-precommit native" diff --git a/tests/ansible/playbooks/drivers/podman/default-userns-baseline.yaml b/tests/ansible/playbooks/drivers/podman/default-userns-baseline.yaml deleted file mode 100644 index 7356ae3c5e..0000000000 --- a/tests/ansible/playbooks/drivers/podman/default-userns-baseline.yaml +++ /dev/null @@ -1,106 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - ---- -- name: Capture the default Podman user-namespace mapping - hosts: all - gather_facts: false - vars: - podman_reference_image: "{{ openshell_podman_reference_image }}" - podman_reference_uid_map: /var/lib/openshell-test-inputs/podman/reference-uid-map - tasks: - - name: Wait for SSH - ansible.builtin.wait_for_connection: - - - name: Detect tmachine container runtime - ansible.builtin.include_role: - name: tmachine_container_runtime - - - name: Require a Podman gateway - ansible.builtin.assert: - that: - - tmachine_container_runtime_name == "podman" - fail_msg: >- - The Podman default-userns suite requires a Podman gateway, not - {{ tmachine_container_runtime_name }} - - # podman_reference_user only selects who runs the direct `podman` - # reference commands below; it must match the daemon's rootful/rootless - # mode. The captured reference file itself stays owned by tmachine - # (the inventory's ansible_user), since the archived test binary that - # later reads it always runs unprivileged as tmachine, in both modes. - - name: Resolve the Podman reference execution context - ansible.builtin.set_fact: - podman_reference_user: "{{ 'tmachine' if tmachine_container_runtime_is_rootless else 'root' }}" - podman_reference_home: "{{ '/home/tmachine' if tmachine_container_runtime_is_rootless else '/root' }}" - podman_reference_uid: "{{ tmachine_container_runtime_tmachine_uid.stdout if tmachine_container_runtime_is_rootless else '0' }}" - - - name: Read OpenShell gateway configuration - become: true - ansible.builtin.slurp: - src: /etc/openshell/gateway.toml - register: openshell_gateway_config - - - name: Require unconfigured Podman user namespaces - ansible.builtin.assert: - that: - - >- - (openshell_gateway_config.content | b64decode) - is not regex('(?m)^\\s*(userns|uidmap|gidmap)\\s*=') - fail_msg: >- - The Podman default-userns suite requires gateway.toml to omit userns, - uidmap, and gidmap. - - - name: Create Podman test-input directory - become: true - ansible.builtin.file: - path: "{{ podman_reference_uid_map | dirname }}" - state: directory - owner: tmachine - group: tmachine - mode: "0700" - - - name: Pull the Podman reference image - become: true - become_user: "{{ podman_reference_user }}" - ansible.builtin.command: - argv: [podman, pull, "{{ podman_reference_image }}"] - environment: - HOME: "{{ podman_reference_home }}" - XDG_RUNTIME_DIR: "/run/user/{{ podman_reference_uid }}" - changed_when: false - - - name: Capture direct Podman default UID mapping - become: true - become_user: "{{ podman_reference_user }}" - ansible.builtin.command: - argv: - - podman - - run - - --rm - - --pull - - never - - --entrypoint - - /bin/cat - - "{{ podman_reference_image }}" - - /proc/self/uid_map - environment: - HOME: "{{ podman_reference_home }}" - XDG_RUNTIME_DIR: "/run/user/{{ podman_reference_uid }}" - changed_when: false - register: podman_default_uid_map - - - name: Require a direct Podman UID mapping - ansible.builtin.assert: - that: - - podman_default_uid_map.stdout | trim | length > 0 - fail_msg: Direct Podman reference container returned no UID mapping - - - name: Store direct Podman default UID mapping - become: true - ansible.builtin.copy: - content: "{{ podman_default_uid_map.stdout | trim }}\n" - dest: "{{ podman_reference_uid_map }}" - owner: tmachine - group: tmachine - mode: "0600" diff --git a/tests/ansible/playbooks/drivers/podman/tests.yaml b/tests/ansible/playbooks/drivers/podman/tests.yaml index abe2f6e553..9a73ecc5d7 100644 --- a/tests/ansible/playbooks/drivers/podman/tests.yaml +++ b/tests/ansible/playbooks/drivers/podman/tests.yaml @@ -7,7 +7,6 @@ gather_facts: false vars: podman_test_root: /var/lib/openshell-driver-tests/podman - podman_test_input_dir: /var/lib/openshell-test-inputs/podman tasks: - name: Wait for SSH ansible.builtin.wait_for_connection: @@ -51,11 +50,10 @@ - name: Run Podman archive tests ansible.builtin.command: - argv: [cargo-nextest, nextest, run, --archive-file, "{{ podman_test_root }}/tests.tar.zst", --workspace-remap, "{{ podman_test_root }}", --no-capture] + argv: [cargo-nextest, nextest, run, --archive-file, "{{ podman_test_root }}/tests.tar.zst", --workspace-remap, "{{ podman_test_root }}", --test-threads, "1", --success-output, immediate, --failure-output, immediate] environment: OPENSHELL_BIN: /usr/local/bin/openshell - OPENSHELL_TEST_INPUT_DIR: "{{ podman_test_input_dir }}" - OPENSHELL_PODMAN_TEST_IMAGE: "{{ openshell_podman_test_image | default('') }}" + OPENSHELL_PODMAN_TEST_IMAGE: "{{ openshell_podman_test_image | default(openshell_podman_reference_image) }}" register: podman_test_result changed_when: false failed_when: false diff --git a/tests/ansible/playbooks/drivers/podman/userns-auto.yaml b/tests/ansible/playbooks/drivers/podman/userns-auto.yaml deleted file mode 100644 index 353f902c2c..0000000000 --- a/tests/ansible/playbooks/drivers/podman/userns-auto.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - ---- -- import_playbook: userns-profile.yaml - vars: - podman_userns_profile: auto - podman_userns_config: "{{ openshell_podman_userns_auto_config }}" - podman_userns_reference_args: [--userns, auto] diff --git a/tests/ansible/playbooks/drivers/podman/userns-keep-id.yaml b/tests/ansible/playbooks/drivers/podman/userns-keep-id.yaml deleted file mode 100644 index e7ec08cce4..0000000000 --- a/tests/ansible/playbooks/drivers/podman/userns-keep-id.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - ---- -- import_playbook: userns-profile.yaml - vars: - podman_userns_profile: keep-id - podman_userns_config: "{{ openshell_podman_userns_keep_id_config }}" - podman_userns_reference_args: [--userns, keep-id] diff --git a/tests/ansible/playbooks/drivers/podman/userns-private.yaml b/tests/ansible/playbooks/drivers/podman/userns-private.yaml deleted file mode 100644 index d24c0f38bc..0000000000 --- a/tests/ansible/playbooks/drivers/podman/userns-private.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - ---- -- import_playbook: userns-profile.yaml - vars: - podman_userns_profile: private - podman_userns_config: "{{ openshell_podman_userns_private_config }}" - podman_userns_reference_args: - # Podman infers a private namespace from explicit maps; its CLI rejects - # combining --userns private with --uidmap/--gidmap. - - --uidmap - - 0:0:1 - - --uidmap - - 1:1:65535 - - --gidmap - - 0:0:1 - - --gidmap - - 1:1:65535 diff --git a/tests/ansible/playbooks/drivers/podman/userns-profile.yaml b/tests/ansible/playbooks/drivers/podman/userns-profile.yaml deleted file mode 100644 index a3071c2833..0000000000 --- a/tests/ansible/playbooks/drivers/podman/userns-profile.yaml +++ /dev/null @@ -1,92 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - ---- -- name: Configure and capture a Podman user-namespace reference - hosts: all - gather_facts: false - vars: - podman_reference_image: "{{ openshell_podman_reference_image }}" - podman_reference_uid_map: /var/lib/openshell-test-inputs/podman/reference-uid-map - tasks: - - name: Wait for SSH - ansible.builtin.wait_for_connection: - - - name: Detect tmachine container runtime - ansible.builtin.include_role: - name: tmachine_container_runtime - - - name: Require a Podman gateway - ansible.builtin.assert: - that: - - tmachine_container_runtime_name == "podman" - fail_msg: >- - The Podman {{ podman_userns_profile }} suite requires a Podman gateway, not - {{ tmachine_container_runtime_name }} - - # podman_reference_user only selects who runs the direct `podman` - # reference command below; it must match the daemon's rootful/rootless - # mode. The captured reference file itself stays owned by tmachine - # (the inventory's ansible_user), since the archived test binary that - # later reads it always runs unprivileged as tmachine, in both modes. - - name: Resolve the Podman reference execution context - ansible.builtin.set_fact: - podman_reference_user: "{{ 'tmachine' if tmachine_container_runtime_is_rootless else 'root' }}" - podman_reference_home: "{{ '/home/tmachine' if tmachine_container_runtime_is_rootless else '/root' }}" - podman_reference_uid: "{{ tmachine_container_runtime_tmachine_uid.stdout if tmachine_container_runtime_is_rootless else '0' }}" - - - name: Apply the Podman user-namespace fixture - become: true - ansible.builtin.blockinfile: - path: /etc/openshell/gateway.toml - marker: "# {mark} OpenShell Podman userns test fixture" - block: "{{ lookup('ansible.builtin.file', podman_userns_config) | trim }}" - - - name: Restart OpenShell gateway with the Podman user-namespace fixture - become: true - ansible.builtin.systemd_service: - name: openshell-gateway.service - state: restarted - - - name: Wait for the configured OpenShell gateway - ansible.builtin.wait_for: - host: 127.0.0.1 - port: 17670 - timeout: 60 - - - name: Capture direct Podman user-namespace mapping - become: true - become_user: "{{ podman_reference_user }}" - ansible.builtin.command: - argv: "{{ [\"podman\", \"run\", \"--rm\", \"--pull\", \"never\"] + podman_userns_reference_args + [\"--entrypoint\", \"/bin/cat\", podman_reference_image, \"/proc/self/uid_map\"] }}" - environment: - HOME: "{{ podman_reference_home }}" - XDG_RUNTIME_DIR: "/run/user/{{ podman_reference_uid }}" - changed_when: false - register: podman_userns_uid_map - - - name: Require a direct Podman user-namespace mapping - ansible.builtin.assert: - that: - - podman_userns_uid_map.stdout | trim | length > 0 - fail_msg: >- - Direct Podman {{ podman_userns_profile }} reference container - returned no UID mapping - - - name: Create Podman test-input directory - become: true - ansible.builtin.file: - path: "{{ podman_reference_uid_map | dirname }}" - state: directory - owner: tmachine - group: tmachine - mode: "0700" - - - name: Store direct Podman user-namespace mapping - become: true - ansible.builtin.copy: - content: "{{ podman_userns_uid_map.stdout | trim }}\n" - dest: "{{ podman_reference_uid_map }}" - owner: tmachine - group: tmachine - mode: "0600" diff --git a/tests/config.nix b/tests/config.nix index d37f0dee81..90445c6ed3 100644 --- a/tests/config.nix +++ b/tests/config.nix @@ -187,24 +187,12 @@ let } { name = "driver-podman"; - playbooks = [ - "ansible/playbooks/drivers/podman/default-userns-baseline.yaml" - "ansible/playbooks/drivers/podman/tests.yaml" - "ansible/playbooks/drivers/podman/userns-auto.yaml" - "ansible/playbooks/drivers/podman/tests.yaml" - "ansible/playbooks/drivers/podman/userns-keep-id.yaml" - "ansible/playbooks/drivers/podman/tests.yaml" - "ansible/playbooks/drivers/podman/userns-private.yaml" - "ansible/playbooks/drivers/podman/tests.yaml" - ]; + playbooks = [ "ansible/playbooks/drivers/podman/tests.yaml" ]; inputs = { openshell_podman_test_bundle = "../artifacts/test-archives/${muslTarget}/openshell-podman-tests.tar"; # Match OpenShell's compiled-in default so direct Podman and # OpenShell containers resolve the same workload image metadata. openshell_podman_reference_image = "nvcr.io/nvidia/base/ubuntu:24.04"; - openshell_podman_userns_auto_config = "../e2e/suites/drivers/podman/fixtures/userns-auto.toml"; - openshell_podman_userns_keep_id_config = "../e2e/suites/drivers/podman/fixtures/userns-keep-id.toml"; - openshell_podman_userns_private_config = "../e2e/suites/drivers/podman/fixtures/userns-private.toml"; }; } ];