Skip to content

Commit 55129bc

Browse files
committed
test(podman): align restart check with session credentials
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
1 parent 2cabec7 commit 55129bc

2 files changed

Lines changed: 59 additions & 25 deletions

File tree

‎architecture/gateway.md‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -357,10 +357,12 @@ cannot authorize ordinary RPCs or choose another successor. Advancing the
357357
successor removes that retry path across every gateway replica. Short
358358
`gateway_jwt.ttl_secs` lifetimes still bound the exposure of a current bearer
359359
that has not yet been refreshed.
360-
Omitting `gateway_jwt.ttl_secs` selects non-expiring tokens for local
361-
single-player Docker, Podman, and VM gateways; those tokens carry `exp = 0`.
362-
Kubernetes and other shared deployments should set a positive TTL. Explicit
363-
zero is rejected.
360+
Omitting `gateway_jwt.ttl_secs` selects non-expiring legacy sandbox JWTs for
361+
local single-player Docker, Podman, and VM gateways; those tokens carry
362+
`exp = 0`. Launch-scoped sandbox session JWTs still have a finite lifetime,
363+
defaulting to 15 minutes when the field is omitted, and are renewed while the
364+
sandbox is active. Kubernetes and other shared deployments should set a
365+
positive TTL. Explicit zero is rejected.
364366

365367
Gateway JWT signing-key rotation is currently an offline operator action. The
366368
runtime loads one active signing key and one matching public verification key

‎e2e/rust/tests/local_driver_token_restart.rs‎

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
#![cfg(feature = "e2e")]
55

66
//! Local-driver E2E regression for sandbox bootstrap JWT material and terminal
7-
//! main-process semantics. Local single-player gateways mint non-expiring
8-
//! bootstrap tokens. Stopping a Docker or Podman sandbox container is terminal
7+
//! main-process semantics. Launch-scoped session tokens have finite lifetimes.
8+
//! Stopping a Docker or Podman sandbox container is terminal
99
//! and must not relaunch its canonical process; restarting the VM gateway still
1010
//! exercises driver recovery without deliberately stopping the sandbox.
1111
1212
use std::fs;
1313
use std::path::PathBuf;
1414
use std::process::Stdio;
15-
use std::time::{Duration, Instant};
15+
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
1616

1717
use base64::Engine as _;
1818
use openshell_e2e::harness::cli::{wait_for_healthy, wait_for_sandbox_exec_contains};
@@ -63,16 +63,17 @@ impl LocalDriver {
6363
matches!(self, Self::Docker | Self::Podman)
6464
}
6565

66-
fn container_filters(self, namespace: &str, sandbox_name: &str) -> Vec<String> {
66+
fn container_filters(self, namespace: &str, sandbox_name: &str, role: &str) -> Vec<String> {
6767
match self {
6868
Self::Docker => vec![
6969
"label=openshell.ai/managed-by=openshell".to_string(),
70-
"label=openshell.ai/isolation-role=sandbox".to_string(),
70+
format!("label=openshell.ai/isolation-role={role}"),
7171
format!("label=openshell.ai/sandbox-namespace={namespace}"),
7272
format!("label=openshell.ai/sandbox-name={sandbox_name}"),
7373
],
7474
Self::Podman => vec![
7575
"label=openshell.managed=true".to_string(),
76+
format!("label=openshell.io/isolation-role={role}"),
7677
format!("label=openshell.ai/sandbox-name={sandbox_name}"),
7778
],
7879
Self::Vm => Vec::new(),
@@ -107,9 +108,10 @@ fn sandbox_container_id(
107108
driver: LocalDriver,
108109
namespace: &str,
109110
sandbox_name: &str,
111+
role: &str,
110112
) -> Result<String, String> {
111113
let mut args = vec!["ps".to_string(), "-aq".to_string()];
112-
for filter in driver.container_filters(namespace, sandbox_name) {
114+
for filter in driver.container_filters(namespace, sandbox_name, role) {
113115
args.push("--filter".to_string());
114116
args.push(filter);
115117
}
@@ -176,15 +178,19 @@ async fn wait_for_container_running(
176178
}
177179

178180
fn read_bootstrap_token(engine: &ContainerEngine, container_id: &str) -> Result<String, String> {
181+
// The Podman supervisor image has no shell tools. Copy its private secret
182+
// out through Podman's archive API rather than execing `cat` inside it.
183+
let temporary = tempfile::tempdir().map_err(|err| format!("create token tempdir: {err}"))?;
184+
let destination = temporary.path().join("sandbox.jwt");
179185
run_engine(
180186
engine,
181187
&[
182-
"exec".to_string(),
183-
container_id.to_string(),
184-
"cat".to_string(),
185-
CONTAINER_TOKEN_MOUNT_PATH.to_string(),
188+
"cp".to_string(),
189+
format!("{container_id}:{CONTAINER_TOKEN_MOUNT_PATH}"),
190+
destination.to_string_lossy().into_owned(),
186191
],
187-
)
192+
)?;
193+
fs::read_to_string(&destination).map_err(|err| format!("read copied bootstrap token: {err}"))
188194
}
189195

190196
fn read_vm_bootstrap_token(sandbox_name: &str) -> Result<String, String> {
@@ -260,10 +266,16 @@ fn token_exp_claim(token: &str) -> Result<i64, String> {
260266
.ok_or_else(|| format!("sandbox JWT missing integer exp claim: {claims}"))
261267
}
262268

263-
fn require_non_expiring_token(token: &str, context: &str) -> Result<(), String> {
269+
fn require_finite_session_token(token: &str, context: &str) -> Result<(), String> {
264270
let exp = token_exp_claim(token)?;
265-
if exp != 0 {
266-
return Err(format!("{context} should use exp=0, got exp={exp}"));
271+
let now = SystemTime::now()
272+
.duration_since(UNIX_EPOCH)
273+
.map_err(|err| format!("read current time: {err}"))?
274+
.as_secs() as i64;
275+
if exp <= now || exp > now + 3600 {
276+
return Err(format!(
277+
"{context} should expire within one hour, got exp={exp}, now={now}"
278+
));
267279
}
268280
Ok(())
269281
}
@@ -274,8 +286,25 @@ async fn stop_container_sandbox(
274286
namespace: &str,
275287
sandbox_name: &str,
276288
) -> Result<(), String> {
277-
let container_id = sandbox_container_id(engine, driver, namespace, sandbox_name)?;
278-
if driver == LocalDriver::Docker {
289+
let container_id = sandbox_container_id(engine, driver, namespace, sandbox_name, "sandbox")?;
290+
if driver == LocalDriver::Podman {
291+
let temporary =
292+
tempfile::tempdir().map_err(|err| format!("create token tempdir: {err}"))?;
293+
let destination = temporary.path().join("workload-sandbox.jwt");
294+
let copy_result = run_engine(
295+
engine,
296+
&[
297+
"cp".to_string(),
298+
format!("{container_id}:{CONTAINER_TOKEN_MOUNT_PATH}"),
299+
destination.to_string_lossy().into_owned(),
300+
],
301+
);
302+
match copy_result {
303+
Err(error) if error.contains("no such file or directory") => {}
304+
Err(error) => return Err(format!("inspect workload bootstrap JWT path: {error}")),
305+
Ok(_) => return Err("sandbox workload contains its bootstrap JWT".to_string()),
306+
}
307+
} else {
279308
run_engine(
280309
engine,
281310
&[
@@ -287,11 +316,14 @@ async fn stop_container_sandbox(
287316
],
288317
)
289318
.map_err(|error| {
290-
format!("Docker sandbox workload must not be able to read its bootstrap JWT: {error}")
319+
format!("sandbox workload must not be able to read its bootstrap JWT: {error}")
291320
})?;
292-
} else {
293-
let token = read_bootstrap_token(engine, &container_id)?;
294-
require_non_expiring_token(&token, "local-driver bootstrap JWT")?;
321+
}
322+
if driver == LocalDriver::Podman {
323+
let supervisor_id =
324+
sandbox_container_id(engine, driver, namespace, sandbox_name, "supervisor")?;
325+
let token = read_bootstrap_token(engine, &supervisor_id)?;
326+
require_finite_session_token(&token, "local-driver session JWT")?;
295327
}
296328

297329
run_engine(engine, &["stop".to_string(), container_id.clone()])?;
@@ -327,7 +359,7 @@ async fn wait_for_sandbox_error(sandbox_name: &str, timeout: Duration) -> Result
327359

328360
async fn restart_vm_sandbox(gateway: &ManagedGateway, sandbox_name: &str) -> Result<(), String> {
329361
let token = read_vm_bootstrap_token(sandbox_name)?;
330-
require_non_expiring_token(&token, "VM bootstrap JWT")?;
362+
require_finite_session_token(&token, "VM session JWT")?;
331363

332364
gateway.stop()?;
333365
gateway.start()?;

0 commit comments

Comments
 (0)