mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-17 05:31:16 +08:00
* feat(podman): add Podman compute driver for rootless sandbox management Adds openshell-driver-podman, a new compute driver that manages OpenShell sandboxes as rootless Podman containers via the Podman REST API over a Unix socket. Enables local workstation sandboxes without Kubernetes. Driver features: - Bridge networking with ephemeral host-port mapping for rootless SSH reachability - Named volumes for workspace storage, Podman native health checks, GPU via CDI - Supervisor binary sideloaded via image volume mount (BYOC-compatible) - SSH handshake secret injected via Podman secrets API (not plaintext env) - Typed ContainerSpec structs, input validation, and path-traversal guards - Cgroups v2 required; fails fast on v1 hosts - Bounded event stream buffer; watch stream reconnection handled by server watch_loop - Graceful shutdown and standalone driver binary with gRPC bridge Rootless-specific fixes: - Skip drop_privileges when user namespace lacks SETUID/SETGID/DAC_READ_SEARCH caps - Add /run/netns tmpfs mount for ip netns in rootless containers - Use secret_env map (not secrets array) for env-var injection in libpod API - Resolve SSH endpoint to 127.0.0.1:<host_port> instead of unreachable bridge IP Server/sandbox hardening: - Split loopback and link-local SSRF gates; Podman/VM drivers allow loopback - Close SSRF bypass in SSH tunnel Host path by resolving DNS before connecting - Prevent OPENSHELL_* env var override by user-supplied spec environment maps - Disable SQLite pool idle_timeout/max_lifetime for in-memory databases - Emit deleted_event on 404-during-inspect instead of regressing sandbox phase - Key delete cleanup by stable sandbox_id to survive container label drift CLI fixes: - Restore --name as a named flag on sandbox create (not positional) - Fix exec command arg parsing to not consume sandboxed-command flags - Propagate SSH verbosity via OPENSHELL_SSH_LOG_LEVEL Build tooling: - Add tasks/scripts/container-engine.sh: auto-detects Podman or Docker, exposes unified ce_* helpers; all build/cluster/VM scripts updated to use it - Add docker:build:supervisor mise task for standalone supervisor image - Add openshell-driver-podman to Dockerfile.images pre-fetch/build stages - Add e2e/rust/e2e-podman.sh and e2e:podman mise task for full lifecycle testing Signed-off-by: Adam Miller <admiller@redhat.com> * fix(driver-podman): derive grpc endpoint from server bind port When a user starts the gateway on a non-default port (e.g. --port 8081), sandbox containers were receiving OPENSHELL_ENDPOINT pointing at the default port 8080. The driver's auto-detection fallback read OPENSHELL_BIND_ADDRESS from the environment, which was stale or unset, and fell back to DEFAULT_SERVER_PORT. Add gateway_port to PodmanComputeConfig and thread config.bind_address.port() from the server into the driver so the fallback uses the actual listening port. Remove the OPENSHELL_BIND_ADDRESS env var read and the extract_port_from_bind_address helper which are no longer needed. Add --gateway-port / OPENSHELL_GATEWAY_PORT to the standalone driver binary for parity when the driver is run outside the embedded server path. Signed-off-by: Adam Miller <admiller@redhat.com> * fix(driver-podman): address PR feedback on env test safety and cluster DNS docs Replace hand-rolled unsafe TempEnvVar RAII guard with temp_env::with_vars and a static ENV_LOCK mutex, fixing a data race in parallel test execution. The prior safety comment incorrectly claimed Cargo runs tests single-threaded. Update debug-openshell-cluster skill to accurately document the DNS proxy strategy (setup_dns_proxy + public DNS fallback) and clarify the separation between cluster DNS and sandbox agent DNS enforcement. Signed-off-by: Adam Miller <admiller@redhat.com> * fix(e2e): resolve CI failures in auth timeout, test harness, and formatting - Short-circuit browser_auth_flow when OPENSHELL_NO_BROWSER=1 instead of waiting the full 120s AUTH_TIMEOUT for a callback that never arrives - Add timeout to SandboxGuard::create() and create_with_upload() to prevent indefinite hangs (matches create_keep() which already had one) - Add missing '--' separator in no_proxy test before command args - Add #![cfg(feature = "e2e")] gate to sandbox_lifecycle.rs - Run cargo fmt on openshell-driver-podman - Refine cluster DNS docs for Podman in debug-openshell-cluster skill Signed-off-by: Adam Miller <admiller@redhat.com> * refactor(server): remove allows_loopback_endpoints from ComputeRuntime SSRF protection is now handled at the network and proxy layers (openshell-core net.rs, openshell-sandbox proxy.rs) rather than requiring per-driver flags on ComputeRuntime. Update architecture docs to reflect supervisor relay SSH transport and add rootless networking deep-dive. Signed-off-by: Adam Miller <admiller@redhat.com> --------- Signed-off-by: Adam Miller <admiller@redhat.com>
131 lines
4.2 KiB
Rust
131 lines
4.2 KiB
Rust
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
#![cfg(feature = "e2e")]
|
|
|
|
use std::process::Stdio;
|
|
use std::time::Duration;
|
|
|
|
use openshell_e2e::harness::binary::{openshell_cmd, openshell_tty_cmd};
|
|
use openshell_e2e::harness::output::{extract_field, strip_ansi};
|
|
use tokio::time::sleep;
|
|
|
|
fn normalize_output(output: &str) -> String {
|
|
let stripped = strip_ansi(output).replace('\r', "");
|
|
let mut cleaned = String::with_capacity(stripped.len());
|
|
|
|
for ch in stripped.chars() {
|
|
match ch {
|
|
'\u{8}' => {
|
|
cleaned.pop();
|
|
}
|
|
'\u{4}' => {}
|
|
_ => cleaned.push(ch),
|
|
}
|
|
}
|
|
|
|
cleaned
|
|
}
|
|
|
|
fn extract_sandbox_name(output: &str) -> Option<String> {
|
|
if let Some((_, rest)) = output.split_once("Created sandbox:") {
|
|
return rest.split_whitespace().next().map(ToOwned::to_owned);
|
|
}
|
|
|
|
extract_field(output, "Created sandbox").or_else(|| extract_field(output, "Name"))
|
|
}
|
|
|
|
async fn sandbox_list_names() -> Vec<String> {
|
|
let mut cmd = openshell_cmd();
|
|
cmd.args(["sandbox", "list", "--names"])
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped());
|
|
|
|
let output = cmd.output().await.expect("spawn openshell sandbox list");
|
|
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
|
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
|
let combined = normalize_output(&format!("{stdout}{stderr}"));
|
|
assert!(
|
|
output.status.success(),
|
|
"sandbox list should succeed (exit {:?}):\n{combined}",
|
|
output.status.code()
|
|
);
|
|
|
|
combined
|
|
.lines()
|
|
.map(str::trim)
|
|
.filter(|line| !line.is_empty())
|
|
.map(ToOwned::to_owned)
|
|
.collect()
|
|
}
|
|
|
|
async fn delete_sandbox(name: &str) {
|
|
let mut cmd = openshell_cmd();
|
|
cmd.args(["sandbox", "delete", name])
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null());
|
|
let _ = cmd.status().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn sandbox_create_keeps_sandbox_after_tty_command_by_default() {
|
|
let mut cmd = openshell_tty_cmd(&["sandbox", "create", "--", "echo", "OK"]);
|
|
cmd.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped());
|
|
|
|
let output = cmd.output().await.expect("spawn openshell sandbox create");
|
|
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
|
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
|
let combined = normalize_output(&format!("{stdout}{stderr}"));
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"sandbox create should succeed (exit {:?}):\n{combined}",
|
|
output.status.code()
|
|
);
|
|
assert!(combined.contains("OK"), "expected command output in:\n{combined}");
|
|
|
|
let sandbox_name = extract_sandbox_name(&combined).expect("sandbox name should be present in output");
|
|
|
|
for _ in 0..20 {
|
|
if sandbox_list_names().await.contains(&sandbox_name) {
|
|
delete_sandbox(&sandbox_name).await;
|
|
return;
|
|
}
|
|
sleep(Duration::from_millis(500)).await;
|
|
}
|
|
|
|
panic!("sandbox {sandbox_name} should still exist by default");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn sandbox_create_with_no_keep_cleans_up_after_tty_command() {
|
|
let mut cmd = openshell_tty_cmd(&["sandbox", "create", "--no-keep", "--", "echo", "OK"]);
|
|
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
|
|
|
let output = cmd.output().await.expect("spawn openshell sandbox create");
|
|
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
|
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
|
let combined = normalize_output(&format!("{stdout}{stderr}"));
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"sandbox create should succeed (exit {:?}):\n{combined}",
|
|
output.status.code()
|
|
);
|
|
assert!(combined.contains("OK"), "expected command output in:\n{combined}");
|
|
|
|
let sandbox_name =
|
|
extract_sandbox_name(&combined).expect("sandbox name should be present in output");
|
|
|
|
for _ in 0..20 {
|
|
if !sandbox_list_names().await.contains(&sandbox_name) {
|
|
return;
|
|
}
|
|
sleep(Duration::from_millis(500)).await;
|
|
}
|
|
|
|
delete_sandbox(&sandbox_name).await;
|
|
panic!("sandbox {sandbox_name} should have been deleted automatically");
|
|
}
|