Skip to content

Commit 8c2398e

Browse files
committed
fix(driver-vm): resolve lifecycle requests on sandbox_id alone
Signed-off-by: Artem Lytvyn <alytvyn@redhat.com>
1 parent 484f076 commit 8c2398e

1 file changed

Lines changed: 231 additions & 25 deletions

File tree

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

Lines changed: 231 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,35 @@ struct SandboxRecord {
572572
deleting: bool,
573573
}
574574

575+
/// Resolve a lifecycle request to a registry key.
576+
///
577+
/// A non-empty `sandbox_id` is authoritative: resolution uses that id alone and
578+
/// never falls back to the name, so a request for an already-removed sandbox
579+
/// reports absence instead of matching a same-named sandbox in another
580+
/// workspace. Only a caller that supplies no id resolves by name, and because
581+
/// sandbox names are unique per workspace rather than globally, a name matching
582+
/// more than one record is rejected instead of decided by iteration order.
583+
fn resolve_record_id(
584+
registry: &HashMap<String, SandboxRecord>,
585+
sandbox_id: &str,
586+
sandbox_name: &str,
587+
) -> Result<Option<String>, Status> {
588+
if !sandbox_id.is_empty() {
589+
return Ok(registry.get_key_value(sandbox_id).map(|(id, _)| id.clone()));
590+
}
591+
592+
let mut matches = registry
593+
.iter()
594+
.filter(|(_, record)| record.snapshot.name == sandbox_name);
595+
let first = matches.next().map(|(id, _)| id.clone());
596+
if matches.next().is_some() {
597+
return Err(Status::failed_precondition(format!(
598+
"sandbox_name {sandbox_name} matched more than one sandbox; supply sandbox_id"
599+
)));
600+
}
601+
Ok(first)
602+
}
603+
575604
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
576605
enum OverlayPreparation {
577606
Fresh,
@@ -1697,14 +1726,7 @@ impl VmDriver {
16971726
}
16981727
let record_id = {
16991728
let registry = self.registry.lock().await;
1700-
if registry.contains_key(sandbox_id) {
1701-
Some(sandbox_id.to_string())
1702-
} else {
1703-
registry
1704-
.iter()
1705-
.find(|(_, record)| record.snapshot.name == sandbox_name)
1706-
.map(|(id, _)| id.clone())
1707-
}
1729+
resolve_record_id(&registry, sandbox_id, sandbox_name)?
17081730
}
17091731
.ok_or_else(|| Status::not_found("sandbox not found"))?;
17101732

@@ -1789,16 +1811,13 @@ impl VmDriver {
17891811
.map_err(|error| Status::invalid_argument(error.to_string()))?;
17901812
let (record_id, state_dir, already_running) = {
17911813
let registry = self.registry.lock().await;
1792-
let (id, record) = if let Some(entry) = registry.get_key_value(sandbox_id) {
1793-
entry
1794-
} else {
1795-
registry
1796-
.iter()
1797-
.find(|(_, record)| record.snapshot.name == sandbox_name)
1798-
.ok_or_else(|| Status::not_found("sandbox not found"))?
1799-
};
1814+
let id = resolve_record_id(&registry, sandbox_id, sandbox_name)?
1815+
.ok_or_else(|| Status::not_found("sandbox not found"))?;
1816+
let record = registry
1817+
.get(&id)
1818+
.ok_or_else(|| Status::not_found("sandbox not found"))?;
18001819
(
1801-
id.clone(),
1820+
id,
18021821
record.state_dir.clone(),
18031822
record.process.is_some() || record.provisioning_task.is_some(),
18041823
)
@@ -1911,14 +1930,7 @@ impl VmDriver {
19111930

19121931
let record_id = {
19131932
let registry = self.registry.lock().await;
1914-
if let Some((id, _record)) = registry.get_key_value(sandbox_id) {
1915-
Some(id.clone())
1916-
} else {
1917-
registry
1918-
.iter()
1919-
.find(|(_, record)| record.snapshot.name == sandbox_name)
1920-
.map(|(id, _)| id.clone())
1921-
}
1933+
resolve_record_id(&registry, sandbox_id, sandbox_name)?
19221934
};
19231935

19241936
let Some(record_id) = record_id else {
@@ -10662,4 +10674,198 @@ mod tests {
1066210674
assert!(gpu.default_selection_supported);
1066310675
assert!(gpu.count_selection_supported);
1066410676
}
10677+
10678+
/// Register a stopped, process-free record whose id, name, and workspace are
10679+
/// set independently.
10680+
///
10681+
/// The other test helpers reuse one string for both id and name, so a test
10682+
/// built on them cannot express the cross-workspace name collision that
10683+
/// lifecycle resolution has to tolerate.
10684+
async fn insert_named_record(
10685+
driver: &VmDriver,
10686+
id: &str,
10687+
name: &str,
10688+
workspace: &str,
10689+
) -> PathBuf {
10690+
let state_dir = sandbox_state_dir(&driver.config.state_dir, id).unwrap();
10691+
create_private_dir_all(&state_dir).await.unwrap();
10692+
driver.registry.lock().await.insert(
10693+
id.to_string(),
10694+
SandboxRecord {
10695+
snapshot: Sandbox {
10696+
id: id.to_string(),
10697+
name: name.to_string(),
10698+
workspace: workspace.to_string(),
10699+
..Default::default()
10700+
},
10701+
state_dir: state_dir.clone(),
10702+
process: None,
10703+
provisioning_task: None,
10704+
gpu_bdf: None,
10705+
deleting: false,
10706+
},
10707+
);
10708+
state_dir
10709+
}
10710+
10711+
fn resolution_test_driver(state_dir: &Path) -> VmDriver {
10712+
let mut driver = test_driver_with_extensions(LifecycleExtensionRegistry::new());
10713+
driver.config.state_dir = state_dir.to_path_buf();
10714+
driver
10715+
}
10716+
10717+
#[tokio::test]
10718+
async fn stop_targets_the_requested_id_when_two_workspaces_share_a_name() {
10719+
let temp = tempfile::tempdir().unwrap();
10720+
let driver = resolution_test_driver(temp.path());
10721+
let alpha = insert_named_record(&driver, "vm-alpha", "demo", "alpha").await;
10722+
let beta = insert_named_record(&driver, "vm-beta", "demo", "beta").await;
10723+
10724+
driver
10725+
.stop_sandbox("vm-beta", "demo")
10726+
.await
10727+
.expect("stop by id should be accepted");
10728+
10729+
assert!(
10730+
beta.join(SANDBOX_STOPPED_FILE).exists(),
10731+
"the requested sandbox should have been stopped"
10732+
);
10733+
assert!(
10734+
!alpha.join(SANDBOX_STOPPED_FILE).exists(),
10735+
"the same-named sandbox in another workspace must be untouched"
10736+
);
10737+
}
10738+
10739+
#[tokio::test]
10740+
async fn delete_targets_the_requested_id_when_two_workspaces_share_a_name() {
10741+
let temp = tempfile::tempdir().unwrap();
10742+
let driver = resolution_test_driver(temp.path());
10743+
let alpha = insert_named_record(&driver, "vm-alpha", "demo", "alpha").await;
10744+
insert_named_record(&driver, "vm-beta", "demo", "beta").await;
10745+
10746+
let response = driver
10747+
.delete_sandbox("vm-beta", "demo")
10748+
.await
10749+
.expect("delete by id should be accepted");
10750+
10751+
assert!(response.deleted);
10752+
let registry = driver.registry.lock().await;
10753+
assert!(!registry.contains_key("vm-beta"));
10754+
assert!(
10755+
registry.contains_key("vm-alpha"),
10756+
"the same-named sandbox in another workspace must survive"
10757+
);
10758+
assert!(alpha.exists(), "the surviving sandbox must keep its state");
10759+
}
10760+
10761+
#[tokio::test]
10762+
async fn a_repeated_delete_does_not_fall_back_to_a_same_named_sandbox() {
10763+
let temp = tempfile::tempdir().unwrap();
10764+
let driver = resolution_test_driver(temp.path());
10765+
let alpha = insert_named_record(&driver, "vm-alpha", "demo", "alpha").await;
10766+
insert_named_record(&driver, "vm-beta", "demo", "beta").await;
10767+
10768+
let first = driver
10769+
.delete_sandbox("vm-beta", "demo")
10770+
.await
10771+
.expect("the first delete should be accepted");
10772+
assert!(first.deleted);
10773+
10774+
// Delete is idempotent, so a retry carrying the same id and name is
10775+
// ordinary caller behavior. The id is gone from the registry by now, and
10776+
// resolving it by name instead would destroy the sandbox in `alpha`.
10777+
let second = driver
10778+
.delete_sandbox("vm-beta", "demo")
10779+
.await
10780+
.expect("a repeated delete should be accepted");
10781+
10782+
assert!(
10783+
!second.deleted,
10784+
"a repeated delete must report that nothing was removed"
10785+
);
10786+
assert!(
10787+
driver.registry.lock().await.contains_key("vm-alpha"),
10788+
"a repeated delete must not remove a same-named sandbox in another workspace"
10789+
);
10790+
assert!(alpha.exists(), "the surviving sandbox must keep its state");
10791+
}
10792+
10793+
#[tokio::test]
10794+
async fn stop_and_start_reject_an_absent_id_that_shares_a_name() {
10795+
let temp = tempfile::tempdir().unwrap();
10796+
let driver = resolution_test_driver(temp.path());
10797+
let alpha = insert_named_record(&driver, "vm-alpha", "demo", "alpha").await;
10798+
10799+
let stop_error = driver
10800+
.stop_sandbox("vm-absent", "demo")
10801+
.await
10802+
.expect_err("a supplied id that is not registered must not resolve by name");
10803+
assert_eq!(stop_error.code(), Code::NotFound);
10804+
10805+
let (start_authentication, _) = test_launch_authentication("absent");
10806+
let start_error = driver
10807+
.start_sandbox(
10808+
"vm-absent",
10809+
"demo",
10810+
"g0000000000000001",
10811+
start_authentication,
10812+
)
10813+
.await
10814+
.expect_err("a supplied id that is not registered must not resolve by name");
10815+
assert_eq!(start_error.code(), Code::NotFound);
10816+
10817+
assert!(
10818+
!alpha.join(SANDBOX_STOPPED_FILE).exists(),
10819+
"the same-named sandbox must not have been touched"
10820+
);
10821+
}
10822+
10823+
#[tokio::test]
10824+
async fn a_name_only_request_matching_two_sandboxes_is_rejected() {
10825+
let temp = tempfile::tempdir().unwrap();
10826+
let driver = resolution_test_driver(temp.path());
10827+
let alpha = insert_named_record(&driver, "vm-alpha", "demo", "alpha").await;
10828+
let beta = insert_named_record(&driver, "vm-beta", "demo", "beta").await;
10829+
10830+
// Without an id the driver has nothing to disambiguate with: the request
10831+
// carries no workspace, and picking by iteration order would stop or
10832+
// delete an arbitrary one of the two.
10833+
for error in [
10834+
driver.stop_sandbox("", "demo").await.unwrap_err(),
10835+
driver
10836+
.start_sandbox(
10837+
"",
10838+
"demo",
10839+
"g0000000000000001",
10840+
test_launch_authentication("ambiguous").0,
10841+
)
10842+
.await
10843+
.unwrap_err(),
10844+
driver.delete_sandbox("", "demo").await.unwrap_err(),
10845+
] {
10846+
assert_eq!(error.code(), Code::FailedPrecondition);
10847+
assert!(error.message().contains("matched more than one sandbox"));
10848+
}
10849+
10850+
let registry = driver.registry.lock().await;
10851+
assert!(registry.contains_key("vm-alpha"));
10852+
assert!(registry.contains_key("vm-beta"));
10853+
assert!(!alpha.join(SANDBOX_STOPPED_FILE).exists());
10854+
assert!(!beta.join(SANDBOX_STOPPED_FILE).exists());
10855+
}
10856+
10857+
#[tokio::test]
10858+
async fn a_name_only_request_still_resolves_a_unique_name() {
10859+
let temp = tempfile::tempdir().unwrap();
10860+
let driver = resolution_test_driver(temp.path());
10861+
let alpha = insert_named_record(&driver, "vm-alpha", "demo", "alpha").await;
10862+
insert_named_record(&driver, "vm-beta", "other", "beta").await;
10863+
10864+
driver
10865+
.stop_sandbox("", "demo")
10866+
.await
10867+
.expect("an unambiguous name-only stop should still resolve");
10868+
10869+
assert!(alpha.join(SANDBOX_STOPPED_FILE).exists());
10870+
}
1066510871
}

0 commit comments

Comments
 (0)