Skip to content

Commit a06e71f

Browse files
committed
refactor(compute): negotiate gateway-managed lifecycle
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
1 parent 4bf4ede commit a06e71f

8 files changed

Lines changed: 93 additions & 41 deletions

File tree

.agents/skills/debug-openshell-cluster/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,8 @@ running intent should stop before the gateway exits and restart after it
199199
returns. Check for `Stopped sandbox during gateway shutdown` and `Started
200200
sandbox during gateway startup` in gateway logs. A sandbox explicitly stopped
201201
through the CLI remains stopped. Kubernetes sandboxes are cluster-owned and do
202-
not follow this local gateway lifecycle.
202+
not follow this local gateway lifecycle. An external driver follows it only
203+
when `GetCapabilities` advertises `GATEWAY_MANAGED_LIFECYCLE`.
203204

204205
### Step 5: Check Podman-Backed Gateways
205206

architecture/compute-runtimes.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ implementing `ComputeDriver` out of tree.
2727
At initialization the gateway snapshots additive feature values from
2828
`GetCapabilities`; unknown values are ignored. Post-initialization lifecycle
2929
and policy behavior does not depend on the configured or advertised driver
30-
name. Startup intent reconciliation and native process-identity defaults are
31-
enabled only by their corresponding features. Driver-requested listeners are
32-
structurally validated and remain restricted to sandbox callback RPCs.
30+
name. Gateway-managed shutdown/start lifecycle and native process-identity
31+
defaults are enabled only by their corresponding features. Driver-requested
32+
listeners are structurally validated and remain restricted to sandbox callback
33+
RPCs.
3334

3435
Drivers own runtime-specific platform event interpretation. When an event should
3536
drive client provisioning UI, the driver attaches the shared

crates/openshell-core/src/driver_utils.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -589,15 +589,15 @@ mod tests {
589589
"1.0.0",
590590
"sandbox:latest",
591591
[
592-
ComputeDriverFeature::GatewayStartReconciliation,
592+
ComputeDriverFeature::GatewayManagedLifecycle,
593593
ComputeDriverFeature::PreserveUnspecifiedProcessIdentity,
594594
],
595595
);
596596

597597
assert_eq!(
598598
capabilities.features,
599599
vec![
600-
i32::from(ComputeDriverFeature::GatewayStartReconciliation),
600+
i32::from(ComputeDriverFeature::GatewayManagedLifecycle),
601601
i32::from(ComputeDriverFeature::PreserveUnspecifiedProcessIdentity),
602602
]
603603
);

crates/openshell-driver-docker/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ impl DockerComputeDriver {
495495
&self.config.daemon_version,
496496
&self.config.default_image,
497497
[
498-
openshell_core::proto::compute::v1::ComputeDriverFeature::GatewayStartReconciliation,
498+
openshell_core::proto::compute::v1::ComputeDriverFeature::GatewayManagedLifecycle,
499499
openshell_core::proto::compute::v1::ComputeDriverFeature::PreserveUnspecifiedProcessIdentity,
500500
],
501501
)

crates/openshell-driver-podman/src/driver.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,7 @@ impl PodmanComputeDriver {
469469
openshell_core::VERSION,
470470
&self.config.default_image,
471471
[
472-
openshell_core::proto::compute::v1::ComputeDriverFeature::GatewayStartReconciliation,
472+
openshell_core::proto::compute::v1::ComputeDriverFeature::GatewayManagedLifecycle,
473473
openshell_core::proto::compute::v1::ComputeDriverFeature::PreserveUnspecifiedProcessIdentity,
474474
],
475475
))

crates/openshell-driver-vm/src/driver.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,7 @@ impl VmDriver {
520520
driver_name: DRIVER_NAME.to_string(),
521521
driver_version: openshell_core::VERSION.to_string(),
522522
default_image: self.config.default_image.clone(),
523-
features: vec![ComputeDriverFeature::GatewayStartReconciliation.into()],
523+
features: vec![ComputeDriverFeature::GatewayManagedLifecycle.into()],
524524
}
525525
}
526526

crates/openshell-server/src/compute/mod.rs

Lines changed: 79 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,11 @@ impl ComputeRuntime {
837837
self.driver_info.features.contains(&i32::from(feature))
838838
}
839839

840+
#[must_use]
841+
pub(crate) fn uses_gateway_managed_lifecycle(&self) -> bool {
842+
self.supports_feature(ComputeDriverFeature::GatewayManagedLifecycle)
843+
}
844+
840845
#[must_use]
841846
pub(crate) fn preserves_unspecified_process_identity(&self) -> bool {
842847
self.supports_feature(ComputeDriverFeature::PreserveUnspecifiedProcessIdentity)
@@ -2055,14 +2060,10 @@ impl ComputeRuntime {
20552060
/// persisted lifecycle intent.
20562061
///
20572062
/// An explicit sandbox stop persists `Stopped`; gateway shutdown does not.
2058-
/// Docker, Podman, and VM compute is stopped through the same public driver
2059-
/// RPC and restarted from the retained running-intent phase on gateway
2060-
/// startup. Kubernetes compute remains cluster-owned and is excluded.
2063+
/// Drivers opt into stop/start ownership through their public capability
2064+
/// snapshot. Kubernetes and legacy extension drivers omit that capability.
20612065
async fn stop_persisted_sandboxes_on_shutdown(&self) -> Result<(), String> {
2062-
if !matches!(
2063-
self.driver_kind(),
2064-
Some(ComputeDriverKind::Docker | ComputeDriverKind::Podman | ComputeDriverKind::Vm)
2065-
) {
2066+
if !self.uses_gateway_managed_lifecycle() {
20662067
return Ok(());
20672068
}
20682069

@@ -2158,7 +2159,7 @@ impl ComputeRuntime {
21582159

21592160
/// Reconcile running intent for local compute after a gateway restart.
21602161
///
2161-
/// Drivers opt into this sweep through their startup capability snapshot.
2162+
/// Drivers opt into this sweep through their lifecycle capability snapshot.
21622163
/// `StartSandbox` is idempotent, so call it for every persisted phase that
21632164
/// requires running compute. Stable stopped, deleting, and error states are
21642165
/// deliberately left alone.
@@ -2167,7 +2168,7 @@ impl ComputeRuntime {
21672168
/// so the watch loop sees the post-start state on its first poll.
21682169
pub async fn start_persisted_sandboxes(&self) -> Result<(), String> {
21692170
self.recover_persisted_lifecycle_transitions().await?;
2170-
if !self.supports_feature(ComputeDriverFeature::GatewayStartReconciliation) {
2171+
if !self.uses_gateway_managed_lifecycle() {
21712172
return Ok(());
21722173
}
21732174

@@ -4075,11 +4076,11 @@ pub async fn new_test_runtime_with_driver(
40754076
) -> ComputeRuntime {
40764077
let features = match driver_name.parse::<ComputeDriverKind>().ok() {
40774078
Some(ComputeDriverKind::Docker | ComputeDriverKind::Podman) => vec![
4078-
i32::from(ComputeDriverFeature::GatewayStartReconciliation),
4079+
i32::from(ComputeDriverFeature::GatewayManagedLifecycle),
40794080
i32::from(ComputeDriverFeature::PreserveUnspecifiedProcessIdentity),
40804081
],
40814082
Some(ComputeDriverKind::Vm) => {
4082-
vec![i32::from(ComputeDriverFeature::GatewayStartReconciliation)]
4083+
vec![i32::from(ComputeDriverFeature::GatewayManagedLifecycle)]
40834084
}
40844085
_ => Vec::new(),
40854086
};
@@ -8177,7 +8178,12 @@ mod tests {
81778178
#[tokio::test]
81788179
async fn shutdown_stops_running_intent_without_changing_persisted_phase() {
81798180
let driver = ControlledDriver::new();
8180-
let runtime = test_runtime_for_driver(driver.clone(), "docker").await;
8181+
let runtime = test_runtime_for_driver_with_features(
8182+
driver.clone(),
8183+
"extension",
8184+
vec![ComputeDriverFeature::GatewayManagedLifecycle],
8185+
)
8186+
.await;
81818187

81828188
for (id, name, phase) in [
81838189
("sb-unspecified", "unspecified", SandboxPhase::Unspecified),
@@ -8236,7 +8242,12 @@ mod tests {
82368242
async fn shutdown_stop_sweep_continues_after_driver_errors() {
82378243
let driver = ControlledDriver::new();
82388244
driver.set_stop_outcome(ControlledLifecycleOutcome::Error("runtime angry"));
8239-
let runtime = test_runtime_for_driver(driver.clone(), "podman").await;
8245+
let runtime = test_runtime_for_driver_with_features(
8246+
driver.clone(),
8247+
"extension",
8248+
vec![ComputeDriverFeature::GatewayManagedLifecycle],
8249+
)
8250+
.await;
82408251
for (id, name) in [("sb-1", "one"), ("sb-2", "two")] {
82418252
runtime
82428253
.store
@@ -8258,7 +8269,12 @@ mod tests {
82588269
async fn shutdown_stop_sweep_uses_bounded_concurrency() {
82598270
let driver = ControlledDriver::new();
82608271
driver.block_stop();
8261-
let runtime = test_runtime_for_driver(driver.clone(), "docker").await;
8272+
let runtime = test_runtime_for_driver_with_features(
8273+
driver.clone(),
8274+
"extension",
8275+
vec![ComputeDriverFeature::GatewayManagedLifecycle],
8276+
)
8277+
.await;
82628278
let sandbox_count = SHUTDOWN_STOP_CONCURRENCY + 1;
82638279
for index in 0..sandbox_count {
82648280
runtime
@@ -8296,16 +8312,15 @@ mod tests {
82968312
}
82978313

82988314
#[tokio::test]
8299-
async fn shutdown_stop_sweep_runs_for_each_local_driver_only() {
8300-
for (driver_name, expected_calls) in [
8301-
("docker", 1),
8302-
("podman", 1),
8303-
("vm", 1),
8304-
("kubernetes", 0),
8305-
("extension", 0),
8306-
] {
8315+
async fn shutdown_stop_sweep_runs_for_any_capable_driver() {
8316+
for driver_name in ["docker", "podman", "vm", "extension"] {
83078317
let driver = ControlledDriver::new();
8308-
let runtime = test_runtime_for_driver(driver.clone(), driver_name).await;
8318+
let runtime = test_runtime_for_driver_with_features(
8319+
driver.clone(),
8320+
driver_name,
8321+
vec![ComputeDriverFeature::GatewayManagedLifecycle],
8322+
)
8323+
.await;
83098324
runtime
83108325
.store
83118326
.put_message(&sandbox_record("sb-1", "sandbox", SandboxPhase::Ready))
@@ -8319,19 +8334,43 @@ mod tests {
83198334

83208335
assert_eq!(
83218336
driver.stop_calls(),
8322-
expected_calls,
8337+
1,
83238338
"unexpected shutdown behavior for {driver_name}"
83248339
);
83258340
}
83268341
}
83278342

8343+
#[tokio::test]
8344+
async fn shutdown_stop_sweep_skips_drivers_without_feature() {
8345+
for driver_name in ["kubernetes", "docker", "extension"] {
8346+
let driver = ControlledDriver::new();
8347+
let runtime = test_runtime_for_driver(driver.clone(), driver_name).await;
8348+
runtime
8349+
.store
8350+
.put_message(&sandbox_record("sb-1", "sandbox", SandboxPhase::Ready))
8351+
.await
8352+
.unwrap();
8353+
8354+
runtime
8355+
.stop_persisted_sandboxes_on_shutdown()
8356+
.await
8357+
.unwrap();
8358+
8359+
assert_eq!(
8360+
driver.stop_calls(),
8361+
0,
8362+
"{driver_name} should retain operator-owned lifecycle"
8363+
);
8364+
}
8365+
}
8366+
83288367
#[tokio::test]
83298368
async fn start_persisted_sandboxes_starts_running_phases() {
83308369
let driver = ControlledDriver::new();
83318370
let runtime = test_runtime_for_driver_with_features(
83328371
driver.clone(),
83338372
"extension",
8334-
vec![ComputeDriverFeature::GatewayStartReconciliation],
8373+
vec![ComputeDriverFeature::GatewayManagedLifecycle],
83358374
)
83368375
.await;
83378376

@@ -8375,7 +8414,7 @@ mod tests {
83758414
let runtime = test_runtime_for_driver_with_features(
83768415
driver,
83778416
"extension",
8378-
vec![ComputeDriverFeature::GatewayStartReconciliation],
8417+
vec![ComputeDriverFeature::GatewayManagedLifecycle],
83798418
)
83808419
.await;
83818420

@@ -8410,7 +8449,7 @@ mod tests {
84108449
let runtime = test_runtime_for_driver_with_features(
84118450
driver,
84128451
"extension",
8413-
vec![ComputeDriverFeature::GatewayStartReconciliation],
8452+
vec![ComputeDriverFeature::GatewayManagedLifecycle],
84148453
)
84158454
.await;
84168455

@@ -8445,7 +8484,7 @@ mod tests {
84458484
let runtime = test_runtime_for_driver_with_features(
84468485
driver.clone(),
84478486
driver_name,
8448-
vec![ComputeDriverFeature::GatewayStartReconciliation],
8487+
vec![ComputeDriverFeature::GatewayManagedLifecycle],
84498488
)
84508489
.await;
84518490
let sandbox = sandbox_record("sb-1", "local", SandboxPhase::Ready);
@@ -8691,7 +8730,7 @@ mod tests {
86918730
.with_driver_name("fake-remote-driver")
86928731
.with_default_image("openshell/sandbox:remote")
86938732
.with_features([
8694-
ComputeDriverFeature::GatewayStartReconciliation,
8733+
ComputeDriverFeature::GatewayManagedLifecycle,
86958734
ComputeDriverFeature::PreserveUnspecifiedProcessIdentity,
86968735
])
86978736
.with_gateway_listener_requirement(
@@ -8767,6 +8806,17 @@ mod tests {
87678806
assert!(driver_config.fields.contains_key("pool"));
87688807
assert!(!driver_config.fields.contains_key("network_mode"));
87698808

8809+
driver.clear_calls();
8810+
runtime
8811+
.stop_persisted_sandboxes_on_shutdown()
8812+
.await
8813+
.unwrap();
8814+
assert!(matches!(
8815+
driver.calls().as_slice(),
8816+
[FakeComputeDriverCall::StopSandbox { sandbox_id, sandbox_name }]
8817+
if sandbox_id == "sb-uds" && sandbox_name == "uds-sandbox"
8818+
));
8819+
87708820
driver.clear_calls();
87718821
runtime.start_persisted_sandboxes().await.unwrap();
87728822
assert!(matches!(

proto/compute_driver.proto

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,9 @@ message GetCapabilitiesResponse {
8282

8383
enum ComputeDriverFeature {
8484
COMPUTE_DRIVER_FEATURE_UNSPECIFIED = 0;
85-
// Reconcile persisted running intent by calling StartSandbox when the
86-
// gateway starts.
87-
COMPUTE_DRIVER_FEATURE_GATEWAY_START_RECONCILIATION = 1;
85+
// Let the gateway stop persisted running intent during graceful shutdown
86+
// and restart that intent when the gateway starts.
87+
COMPUTE_DRIVER_FEATURE_GATEWAY_MANAGED_LIFECYCLE = 1;
8888
// Preserve omitted process user/group fields so the runtime can apply its
8989
// native image or OCI identity defaults.
9090
COMPUTE_DRIVER_FEATURE_PRESERVE_UNSPECIFIED_PROCESS_IDENTITY = 2;

0 commit comments

Comments
 (0)