Skip to content

Commit f155eeb

Browse files
committed
test(podman): add real-daemon coverage for resource limits and daemon failure
Neither the Podman driver's resource-limit enforcement nor its behavior when the Podman daemon is unreachable had any test coverage against a real daemon; both were only exercised through unit tests against a mocked Podman client. podman_resource_limits.rs creates a sandbox with --cpu/--memory flags and reads /sys/fs/cgroup/memory.max and cpu.max from inside the sandbox itself, verifying the limit is actually enforced rather than just echoed back by the template API. Expected values are cross-checked against the driver's own parse_cpu_to_microseconds/parse_memory_to_bytes and against a real local `podman run --cpus/--memory` container. podman_preflight.rs spawns the standalone openshell-driver-podman binary against a guaranteed-nonexistent Podman socket and asserts it exits non-zero within its bounded retry window with an actionable error naming the socket path, rather than hanging or failing silently. Signed-off-by: politerealism <burdcat17@gmail.com>
1 parent df67cac commit f155eeb

3 files changed

Lines changed: 195 additions & 0 deletions

File tree

‎e2e/rust/Cargo.toml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ name = "podman_gateway_start"
8888
path = "tests/podman_gateway_start.rs"
8989
required-features = ["e2e-podman"]
9090

91+
[[test]]
92+
name = "podman_preflight"
93+
path = "tests/podman_preflight.rs"
94+
required-features = ["e2e-podman"]
95+
9196
[[test]]
9297
name = "podman_corporate_proxy"
9398
path = "tests/podman_corporate_proxy.rs"
@@ -98,6 +103,11 @@ name = "podman_oci_identity"
98103
path = "tests/podman_oci_identity.rs"
99104
required-features = ["e2e-podman"]
100105

106+
[[test]]
107+
name = "podman_resource_limits"
108+
path = "tests/podman_resource_limits.rs"
109+
required-features = ["e2e-podman"]
110+
101111
[[test]]
102112
name = "podman_userns"
103113
path = "tests/podman_userns.rs"

‎e2e/rust/tests/podman_preflight.rs‎

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#![cfg(feature = "e2e-podman")]
5+
6+
//! Podman driver daemon-unavailable e2e tests.
7+
//!
8+
//! These tests verify that `openshell-driver-podman` fails fast with an
9+
//! actionable error when it cannot reach a Podman API socket, instead of
10+
//! hanging or silently serving gRPC against a dead connection.
11+
//!
12+
//! The tests do NOT require a running Podman daemon or gateway — they point
13+
//! `--podman-socket` at a path that is guaranteed not to exist to simulate
14+
//! the daemon being unavailable.
15+
16+
use std::path::{Path, PathBuf};
17+
use std::time::{Duration, Instant};
18+
19+
use openshell_e2e::harness::output::strip_ansi;
20+
21+
/// Locate the workspace root by walking up from this crate's manifest directory.
22+
fn workspace_root() -> PathBuf {
23+
Path::new(env!("CARGO_MANIFEST_DIR"))
24+
.ancestors()
25+
.nth(2)
26+
.expect("failed to resolve workspace root from CARGO_MANIFEST_DIR")
27+
.to_path_buf()
28+
}
29+
30+
/// Return the path to the `openshell-driver-podman` binary.
31+
///
32+
/// Uses `OPENSHELL_EXTERNAL_DRIVER_BIN` when set (the same env var the shell
33+
/// e2e harness uses for prebuilt standalone driver artifacts), otherwise
34+
/// expects the binary at `<workspace>/target/debug/openshell-driver-podman`.
35+
fn driver_podman_bin() -> PathBuf {
36+
let bin = std::env::var_os("OPENSHELL_EXTERNAL_DRIVER_BIN").map_or_else(
37+
|| workspace_root().join("target/debug/openshell-driver-podman"),
38+
PathBuf::from,
39+
);
40+
assert!(
41+
bin.is_file(),
42+
"openshell-driver-podman binary not found at {} — set OPENSHELL_EXTERNAL_DRIVER_BIN \
43+
or run `cargo build -p openshell-driver-podman` first",
44+
bin.display()
45+
);
46+
bin
47+
}
48+
49+
/// Run `openshell-driver-podman` pointed at a Podman socket that does not
50+
/// exist, and wait for it to exit.
51+
///
52+
/// The driver retries a handful of times before giving up (to tolerate the
53+
/// socket briefly re-activating), so this can take several seconds.
54+
async fn run_with_unreachable_podman_socket() -> (String, i32, Duration, PathBuf) {
55+
let tmpdir = tempfile::tempdir().expect("create isolated socket dir");
56+
let missing_socket = tmpdir.path().join("openshell-e2e-nonexistent-podman.sock");
57+
58+
let start = Instant::now();
59+
let mut cmd = tokio::process::Command::new(driver_podman_bin());
60+
cmd.arg("--podman-socket")
61+
.arg(&missing_socket)
62+
.kill_on_drop(true)
63+
.stdout(std::process::Stdio::piped())
64+
.stderr(std::process::Stdio::piped());
65+
66+
let output = tokio::time::timeout(Duration::from_secs(60), cmd.output())
67+
.await
68+
.expect("openshell-driver-podman should exit instead of hanging")
69+
.expect("spawn openshell-driver-podman");
70+
let elapsed = start.elapsed();
71+
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
72+
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
73+
let combined = format!("{stdout}{stderr}");
74+
let code = output.status.code().unwrap_or(-1);
75+
(combined, code, elapsed, missing_socket)
76+
}
77+
78+
/// `openshell-driver-podman` should exit non-zero, not hang, when its
79+
/// configured Podman socket does not exist.
80+
#[tokio::test]
81+
async fn driver_exits_when_podman_socket_unreachable() {
82+
let (output, code, elapsed, _) = run_with_unreachable_podman_socket().await;
83+
84+
assert_ne!(
85+
code, 0,
86+
"driver should exit non-zero when Podman is unreachable, output:\n{output}"
87+
);
88+
89+
assert!(
90+
elapsed < Duration::from_secs(30),
91+
"driver should give up retrying and exit within its bounded retry \
92+
window (took {}s), output:\n{output}",
93+
elapsed.as_secs()
94+
);
95+
}
96+
97+
/// The error surfaced when the Podman socket is unreachable should name the
98+
/// configured socket path and describe a connection failure, not a generic
99+
/// panic or timeout with no actionable detail.
100+
#[tokio::test]
101+
async fn driver_error_names_unreachable_socket() {
102+
let (output, code, _, missing_socket) = run_with_unreachable_podman_socket().await;
103+
104+
assert_ne!(code, 0);
105+
let clean = strip_ansi(&output);
106+
107+
assert!(
108+
clean.contains("connection error"),
109+
"driver error should describe a connection failure:\n{clean}"
110+
);
111+
assert!(
112+
clean.contains(missing_socket.to_str().expect("socket path is utf-8")),
113+
"driver error should name the unreachable socket path {}:\n{clean}",
114+
missing_socket.display()
115+
);
116+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#![cfg(feature = "e2e-podman")]
5+
6+
//! Podman-specific E2E coverage verifying that declared sandbox CPU/memory
7+
//! resource limits are actually enforced by cgroups inside the workload, not
8+
//! just echoed back by the API/template layer.
9+
//!
10+
//! `e2e/rust/tests/sandbox_templates.rs` already verifies that
11+
//! `sandbox template create --cpu ... --memory ...` is stored and returned
12+
//! correctly, but it never inspects the resulting container's real resource
13+
//! state. This test creates a sandbox with those flags directly and reads the
14+
//! cgroup v2 interface files from inside the sandbox itself, so it exercises
15+
//! the actual enforcement boundary the workload experiences.
16+
17+
use openshell_e2e::harness::sandbox::SandboxGuard;
18+
19+
const CPU_REQUEST: &str = "500m";
20+
const MEMORY_REQUEST: &str = "512Mi";
21+
22+
// "500m" (500 millicores) becomes a 50000us quota over Podman's 100000us
23+
// (100ms) CFS period — see
24+
// crates/openshell-driver-podman/src/container.rs's parse_cpu_to_microseconds.
25+
// Verified directly against a real `podman run --cpus=0.5` container, whose
26+
// cpu.max reads "50000 100000".
27+
const EXPECTED_CPU_MAX: &str = "50000 100000";
28+
29+
// "512Mi" (mebibytes) becomes exactly 512 * 1024 * 1024 bytes — see
30+
// crates/openshell-driver-podman/src/container.rs's parse_memory_to_bytes.
31+
// Verified directly against a real `podman run --memory=512m` container,
32+
// whose memory.max reads "536870912".
33+
const EXPECTED_MEMORY_MAX: &str = "536870912";
34+
35+
#[tokio::test]
36+
async fn sandbox_resource_limits_are_enforced_via_cgroups() {
37+
if std::env::var("OPENSHELL_E2E_DRIVER").as_deref() != Ok("podman") {
38+
eprintln!("Skipping Podman resource-limit test: e2e driver is not podman");
39+
return;
40+
}
41+
42+
let mut sandbox = SandboxGuard::create(&["--cpu", CPU_REQUEST, "--memory", MEMORY_REQUEST])
43+
.await
44+
.expect("sandbox create with resource limits should succeed");
45+
46+
let memory_max = sandbox
47+
.exec(&["cat", "/sys/fs/cgroup/memory.max"])
48+
.await
49+
.expect("read memory.max from sandbox cgroup")
50+
.trim()
51+
.to_string();
52+
assert_eq!(
53+
memory_max, EXPECTED_MEMORY_MAX,
54+
"sandbox cgroup should enforce the declared {MEMORY_REQUEST} memory limit, got {memory_max}"
55+
);
56+
57+
let cpu_max = sandbox
58+
.exec(&["cat", "/sys/fs/cgroup/cpu.max"])
59+
.await
60+
.expect("read cpu.max from sandbox cgroup")
61+
.trim()
62+
.to_string();
63+
assert_eq!(
64+
cpu_max, EXPECTED_CPU_MAX,
65+
"sandbox cgroup should enforce the declared {CPU_REQUEST} CPU limit, got {cpu_max}"
66+
);
67+
68+
sandbox.cleanup().await;
69+
}

0 commit comments

Comments
 (0)