use std::sync::Arc;
use std::time::Duration;
use crate::control::{
ControlPlane, CreateImageRequest, Image, Microvm, RunHookPayload, RunMicrovmRequest, WaitOpts,
};
use crate::error::{Error, ErrorKind};
use crate::region::Region;
use crate::session::{Session, TokenMinter};
pub const DEFAULT_READY_TIMEOUT: Duration = Duration::from_secs(300);
pub const DEFAULT_LIFECYCLE_TIMEOUT: Duration = Duration::from_secs(300);
pub const DEFAULT_DELETE_ATTEMPTS: u32 = 20;
pub const DEFAULT_DELETE_BACKOFF: Duration = Duration::from_secs(15);
const LIFECYCLE_POLL_INTERVAL: Duration = Duration::from_secs(5);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Lifecycle {
Pending,
Running,
Suspending,
Suspended,
Terminating,
Terminated,
}
impl Lifecycle {
pub fn as_str(self) -> &'static str {
match self {
Lifecycle::Pending => "PENDING",
Lifecycle::Running => "RUNNING",
Lifecycle::Suspending => "SUSPENDING",
Lifecycle::Suspended => "SUSPENDED",
Lifecycle::Terminating => "TERMINATING",
Lifecycle::Terminated => "TERMINATED",
}
}
pub fn is_live(self) -> bool {
matches!(
self,
Lifecycle::Pending | Lifecycle::Running | Lifecycle::Suspending | Lifecycle::Suspended
)
}
}
impl std::fmt::Display for Lifecycle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug)]
pub struct RunRequest {
pub image_identifier: Option<String>,
pub image_version: Option<String>,
pub execution_role_arn: Option<String>,
pub agent_token: Option<String>,
pub launch_env: std::collections::HashMap<String, String>,
pub egress: bool,
pub max_idle_sec: u32,
pub suspended_sec: u32,
pub auto_resume: bool,
pub max_duration_sec: u32,
pub ready_timeout: Duration,
pub token_scope: Option<String>,
}
impl Default for RunRequest {
fn default() -> Self {
Self {
image_identifier: None,
image_version: None,
execution_role_arn: None,
agent_token: None,
launch_env: std::collections::HashMap::new(),
egress: false,
max_idle_sec: 600,
suspended_sec: 600,
auto_resume: false,
max_duration_sec: 3_600,
ready_timeout: DEFAULT_READY_TIMEOUT,
token_scope: None,
}
}
}
impl RunRequest {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_image(mut self, identifier: impl Into<String>) -> Self {
self.image_identifier = Some(identifier.into());
self
}
#[must_use]
pub fn with_egress(mut self) -> Self {
self.egress = true;
self
}
#[must_use]
pub fn with_image_version(mut self, version: impl Into<String>) -> Self {
self.image_version = Some(version.into());
self
}
#[must_use]
pub fn with_suspended_sec(mut self, seconds: u32) -> Self {
self.suspended_sec = seconds;
self
}
#[must_use]
pub fn with_agent_token(mut self, token: impl Into<String>) -> Self {
self.agent_token = Some(token.into());
self
}
#[must_use]
pub fn with_launch_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.launch_env.insert(key.into(), value.into());
self
}
}
#[derive(Clone, Copy, Debug)]
pub struct TeardownOpts {
pub delete_image: bool,
pub delete_log_group: bool,
pub delete_attempts: u32,
pub delete_backoff: Duration,
pub wait_for_terminated: Option<Duration>,
}
impl Default for TeardownOpts {
fn default() -> Self {
Self {
delete_image: false,
delete_log_group: false,
delete_attempts: DEFAULT_DELETE_ATTEMPTS,
delete_backoff: DEFAULT_DELETE_BACKOFF,
wait_for_terminated: None,
}
}
}
impl TeardownOpts {
#[must_use]
pub fn deleting_image(mut self) -> Self {
self.delete_image = true;
self
}
#[must_use]
pub fn deleting_log_group(mut self) -> Self {
self.delete_log_group = true;
self
}
#[must_use]
pub fn waiting_for_terminated(mut self) -> Self {
self.wait_for_terminated = Some(DEFAULT_LIFECYCLE_TIMEOUT);
self
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct TeardownReport {
pub undeleted: Vec<String>,
pub terminate_accepted: bool,
pub image_deleted: Option<bool>,
pub lifecycle: Option<Lifecycle>,
pub failures: Vec<String>,
}
impl TeardownReport {
pub fn leaked(&self) -> bool {
!self.undeleted.is_empty()
}
}
struct ControlPlaneMinter {
control: Arc<ControlPlane>,
microvm_id: String,
}
impl TokenMinter for ControlPlaneMinter {
fn mint(
&self,
) -> futures_util::future::BoxFuture<'_, Result<crate::session::ProxyToken, Error>> {
Box::pin(async move {
let minted = self.control.mint_auth_token(&self.microvm_id).await?;
Ok(minted.into())
})
}
fn mint_for_ports(
&self,
ports: &[u16],
) -> futures_util::future::BoxFuture<'_, Result<crate::session::ProxyToken, Error>> {
let specs: Vec<crate::control::ops::PortSpecification> = ports
.iter()
.map(|port| crate::control::ops::PortSpecification::port(*port))
.collect();
Box::pin(async move {
let minted = self
.control
.mint_auth_token_for(&self.microvm_id, &specs)
.await?;
Ok(minted.into())
})
}
}
pub struct Sandbox {
control: Arc<ControlPlane>,
image: Option<Image>,
microvm: Option<Microvm>,
session: Option<Session>,
lifecycle: Lifecycle,
token_installed: bool,
image_exists: bool,
was_terminated: bool,
bootstrap_count: u32,
suspended_window: Option<Duration>,
suspended_at: Option<Duration>,
torn_down: bool,
}
impl std::fmt::Debug for Sandbox {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Sandbox")
.field("lifecycle", &self.lifecycle)
.field("microvm", &self.microvm.as_ref().map(|vm| &vm.id))
.field("image", &self.image.as_ref().map(|image| &image.identifier))
.field("token_installed", &self.token_installed)
.field("bootstrap_count", &self.bootstrap_count)
.field("was_terminated", &self.was_terminated)
.field("suspended_window", &self.suspended_window)
.finish_non_exhaustive()
}
}
impl Sandbox {
pub async fn new(region: Region) -> Result<Self, Error> {
Ok(Self::with_control_plane(ControlPlane::new(region).await?))
}
pub fn with_control_plane(control: ControlPlane) -> Self {
Self {
control: Arc::new(control),
image: None,
microvm: None,
session: None,
lifecycle: Lifecycle::Pending,
token_installed: false,
image_exists: false,
was_terminated: false,
bootstrap_count: 0,
suspended_window: None,
suspended_at: None,
torn_down: false,
}
}
pub fn lifecycle(&self) -> Lifecycle {
self.lifecycle
}
pub fn token_installed(&self) -> bool {
self.token_installed
}
pub fn image_exists(&self) -> bool {
self.image_exists
}
pub fn was_terminated(&self) -> bool {
self.was_terminated
}
pub fn bootstrap_count(&self) -> u32 {
self.bootstrap_count
}
pub fn image(&self) -> Option<&Image> {
self.image.as_ref()
}
pub fn microvm(&self) -> Option<&Microvm> {
self.microvm.as_ref()
}
pub fn session(&self) -> Option<&Session> {
self.session.as_ref()
}
pub fn suspended_window(&self) -> Option<Duration> {
self.suspended_window
}
pub async fn build_image(&mut self, request: CreateImageRequest) -> Result<&Image, Error> {
let size = request.size;
let created = self.control.create_image(request).await?;
let built = self
.control
.wait_for_image(&created.identifier, size, WaitOpts::default())
.await?;
self.image_exists = true;
self.image = Some(built);
Ok(self.image.as_ref().expect("just assigned"))
}
pub fn build_artifact_for(&self, request: &CreateImageRequest) -> Result<Vec<u8>, Error> {
self.control.build_artifact_for(request)
}
pub fn preflight(&self, request: &CreateImageRequest) -> Result<(), Error> {
self.control.preflight(request)
}
pub async fn resolve_image_arn(&self, identifier: &str) -> Result<String, Error> {
self.control.resolve_image_arn(identifier).await
}
pub async fn find_image_by_name(
&self,
name: &str,
) -> Result<Option<crate::control::ops::MicrovmImageSummaryWire>, Error> {
self.control.find_image_by_name(name).await
}
pub fn artifact_content_hash_for(&self, request: &CreateImageRequest) -> String {
self.control.artifact_content_hash_for(request)
}
pub async fn managed_base_versions(
&self,
base_image_arn: &str,
) -> Result<Vec<crate::control::ops::ManagedMicrovmImageVersionWire>, Error> {
self.control.managed_base_versions(base_image_arn).await
}
pub async fn image_version(
&self,
identifier: &str,
version: &str,
) -> Result<crate::control::ops::MicrovmImageVersionSummaryWire, Error> {
self.control.get_image_version(identifier, version).await
}
pub async fn run(&mut self, request: RunRequest) -> Result<&mut Session, Error> {
if self.bootstrap_count > 0 || self.microvm.is_some() {
return Err(Error::invalid_arg(format!(
"this sandbox has already launched a VM ({} bootstrap(s), lifecycle {}), and the \
agent token is installed at most once per VM lifetime (STATE-3). A second VM \
needs a second Sandbox — reusing this one would either re-deliver a run-hook \
payload to a daemon whose one-shot bootstrap refuses it, or silently address \
two guests through one handle.",
self.bootstrap_count, self.lifecycle,
)));
}
let Some(identifier) = request
.image_identifier
.clone()
.or_else(|| self.image.as_ref().map(|image| image.identifier.clone()))
else {
return Err(Error::new(
ErrorKind::Precondition,
"no image to launch: pass RunRequest::with_image or call build_image first."
.to_string(),
));
};
let agent_token = request.agent_token.clone().unwrap_or_else(mint_agent_token);
let payload = RunHookPayload::for_launch(&agent_token, &request.launch_env)?;
let mut wire = RunMicrovmRequest::new(&identifier, payload);
wire.image_version = request.image_version.clone();
wire.execution_role_arn = request.execution_role_arn.clone();
wire.max_idle_sec = request.max_idle_sec;
wire.suspended_sec = request.suspended_sec;
wire.auto_resume = request.auto_resume;
wire.max_duration_sec = request.max_duration_sec;
wire.token_scope = request.token_scope.clone();
if request.egress {
wire = wire.with_egress();
}
let launched = self.control.run_microvm(wire).await?;
self.lifecycle = Lifecycle::Pending;
self.image_exists = true;
self.suspended_window = Some(Duration::from_secs(u64::from(request.suspended_sec)));
let id = launched.id.clone();
self.microvm = Some(launched);
let running = self
.control
.wait_for_running(
&id,
WaitOpts {
timeout: request.ready_timeout,
poll_interval: LIFECYCLE_POLL_INTERVAL,
stall_grace: Duration::MAX,
},
)
.await?;
self.lifecycle = Lifecycle::Running;
self.token_installed = true;
self.bootstrap_count += 1;
let endpoint = running.endpoint.clone();
self.microvm = Some(running);
let minter = Arc::new(ControlPlaneMinter {
control: Arc::clone(&self.control),
microvm_id: id,
});
self.session = Some(
Session::builder(endpoint, agent_token)
.with_minter(minter)
.with_port(self.control.port())
.build()?,
);
Ok(self.session.as_mut().expect("just assigned"))
}
pub async fn suspend(&mut self) -> Result<(), Error> {
let id = self.require_microvm("suspend")?;
if self.lifecycle != Lifecycle::Running {
return Err(Error::invalid_arg(format!(
"microvm {id} is {} and a suspend is only issued from RUNNING (STATE-5). Refused \
here rather than by the service, because the service's answer about a \
non-running id does not say which of the two things went wrong — and a suspend \
issued from SUSPENDED is a caller who believes they resumed.",
self.lifecycle,
)));
}
self.control.suspend(&id).await?;
self.lifecycle = Lifecycle::Suspending;
self.suspended_at = Some(self.control.clock().elapsed());
let settled = self
.control
.wait_for_state(
&id,
&crate::control::microvm::SUSPEND_WANTED,
&[],
self.lifecycle_wait(),
)
.await?;
self.lifecycle = match settled.state.as_str() {
"SUSPENDED" => Lifecycle::Suspended,
"TERMINATED" => {
self.was_terminated = true;
Lifecycle::Terminated
}
other => {
return Err(Error::new(
ErrorKind::Platform,
format!(
"the suspend wait returned {other}, which is neither SUSPENDED nor \
TERMINATED — and those two are what this client asked for."
),
));
}
};
self.microvm = Some(settled);
Ok(())
}
pub async fn resume(&mut self) -> Result<&mut Session, Error> {
let id = self.require_microvm("resume")?;
if self.was_terminated || self.lifecycle == Lifecycle::Terminated {
return Err(Error::invalid_arg(format!(
"microvm {id} was terminated, and a terminated VM never returns to RUNNING \
(STATE-11). There is nothing to resume: the guest's memory is gone, so even a \
call the service accepted would hand back a different machine."
)));
}
if self.lifecycle != Lifecycle::Suspended {
return Err(Error::invalid_arg(format!(
"microvm {id} is {} and a resume is only issued from SUSPENDED (STATE-7).",
self.lifecycle,
)));
}
self.require_open_suspended_window(&id)?;
self.control.resume(&id).await?;
let running = self
.control
.wait_for_state(
&id,
&["RUNNING"],
&crate::constants::DEAD_STATES,
self.lifecycle_wait(),
)
.await?;
self.lifecycle = Lifecycle::Running;
let endpoint = running.endpoint.clone();
self.microvm = Some(running);
if let Some(session) = self.session.as_mut() {
session.rebind(endpoint);
}
self.suspended_at = None;
self.session.as_mut().ok_or_else(|| {
Error::new(
ErrorKind::Unexpected,
"the VM resumed but this sandbox holds no session, which run() always builds"
.to_string(),
)
})
}
fn require_open_suspended_window(&self, id: &str) -> Result<(), Error> {
let (Some(window), Some(since)) = (self.suspended_window, self.suspended_at) else {
return Ok(());
};
let elapsed = self.control.clock().elapsed().saturating_sub(since);
if elapsed <= window {
return Ok(());
}
Err(Error::new(
ErrorKind::WindowClosed,
format!(
"microvm {id} has been suspended {}s, past the {}s suspendedDurationSeconds \
window set at launch — the idlePolicy terminates a suspended VM once that window \
passes, so there is nothing left to resume (docs/PLATFORM.md, '`idlePolicy`'). \
Refused before ResumeMicrovm because suspendedDurationSeconds exists only in the \
RunMicrovm request: GetMicrovm does not return it, so this client is the only \
party that can name the number. A longer window has to be set at launch on the \
next VM; there is no call that extends this one.",
elapsed.as_secs(),
window.as_secs(),
),
))
}
pub async fn terminate(&mut self, opts: TeardownOpts) -> TeardownReport {
let mut report = TeardownReport::default();
self.torn_down = true;
self.session = None;
if let Some(id) = self.microvm.as_ref().map(|vm| vm.id.clone()) {
self.lifecycle = Lifecycle::Terminating;
self.was_terminated = true;
match self.control.terminate(&id).await {
Ok(()) => report.terminate_accepted = true,
Err(error) => {
report.failures.push(format!("terminate {id}: {error}"));
report.undeleted.push(id.clone());
}
}
if report.terminate_accepted
&& let Some(timeout) = opts.wait_for_terminated
{
let wait = WaitOpts {
timeout,
poll_interval: LIFECYCLE_POLL_INTERVAL,
stall_grace: Duration::MAX,
};
match self
.control
.wait_for_state(&id, &["TERMINATED"], &[], wait)
.await
{
Ok(settled) => {
self.lifecycle = Lifecycle::Terminated;
self.microvm = Some(settled);
}
Err(error) => report
.failures
.push(format!("waiting for {id} to reach TERMINATED: {error}")),
}
}
}
if opts.delete_image {
match self.image.as_ref().map(|image| image.identifier.clone()) {
Some(identifier) => {
let deleted = self
.control
.delete_image(&identifier, opts.delete_attempts, opts.delete_backoff)
.await;
report.image_deleted = Some(deleted);
if deleted {
self.image_exists = false;
} else {
report.failures.push(format!(
"the image {identifier} survived {} delete attempts",
opts.delete_attempts.max(1)
));
report.undeleted.push(identifier);
}
}
None => report.image_deleted = Some(false),
}
}
if opts.delete_log_group
&& let Some(group) = self.image.as_ref().map(Image::build_log_group)
{
report.failures.push(format!(
"the build log group {group} was not deleted: CloudWatch Logs is not in this \
crate's dependency set, so it is reported rather than removed. It is \
service-created, which means no Terraform stack owns it and `terraform destroy` \
leaves it behind — six accumulated before anyone noticed."
));
report.undeleted.push(group);
}
report.lifecycle = Some(self.lifecycle);
report
}
fn require_microvm(&self, what: &str) -> Result<String, Error> {
self.microvm
.as_ref()
.map(|vm| vm.id.clone())
.ok_or_else(|| {
Error::new(
ErrorKind::Precondition,
format!("nothing to {what}: this sandbox has not launched a VM."),
)
})
}
fn lifecycle_wait(&self) -> WaitOpts {
WaitOpts {
timeout: DEFAULT_LIFECYCLE_TIMEOUT,
poll_interval: LIFECYCLE_POLL_INTERVAL,
stall_grace: Duration::MAX,
}
}
}
impl Drop for Sandbox {
fn drop(&mut self) {
if self.torn_down {
return;
}
if let Some(vm) = self.microvm.as_ref()
&& self.lifecycle.is_live()
{
eprintln!(
"warning: the Sandbox for microvm {} was dropped in {} without terminate(). \
Nothing was torn down — Drop cannot await, so a teardown here would deadlock \
inside a runtime. The VM bills until its maximumDurationInSeconds ceiling: \
terminate it with `microvm terminate {}`.",
vm.id, self.lifecycle, vm.id,
);
}
}
}
fn mint_agent_token() -> String {
use std::fmt::Write as _;
use std::io::Read as _;
let mut bytes = [0u8; 32];
let read = std::fs::File::open("/dev/urandom")
.ok()
.and_then(|mut file| file.read_exact(&mut bytes).ok())
.is_some();
if !read {
use std::collections::hash_map::RandomState;
use std::hash::{BuildHasher as _, Hasher as _};
for chunk in bytes.chunks_mut(8) {
let mut hasher = RandomState::new().build_hasher();
hasher.write_usize(chunk.as_ptr() as usize);
hasher.write_u128(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|since| since.as_nanos())
.unwrap_or_default(),
);
let mixed = hasher.finish().to_le_bytes();
chunk.copy_from_slice(&mixed[..chunk.len()]);
}
}
let mut token = String::with_capacity(bytes.len() * 2);
for byte in bytes {
let _ = write!(token, "{byte:02x}");
}
token
}
#[cfg(test)]
mod tests {
use super::*;
use crate::control::fake::{self as fake, Answer, FakeControlPlane, TestClock};
use crate::control::transport::Method;
fn planted() -> (Sandbox, Arc<FakeControlPlane>, Arc<TestClock>) {
let recorder = Arc::new(FakeControlPlane::new());
let clock = Arc::new(TestClock::new());
let plane = ControlPlane::with_transport(
Arc::clone(&recorder) as Arc<dyn crate::control::transport::Transport>,
Region::UsEast1,
Arc::clone(&clock) as Arc<dyn crate::control::Clock>,
);
(Sandbox::with_control_plane(plane), recorder, clock)
}
fn answer_launch(recorder: &FakeControlPlane) {
recorder
.answer(
"RunMicrovm",
Answer::ok(fake::microvm_response("PENDING", None)),
)
.answer(
"GetMicrovm",
Answer::ok(fake::microvm_response("RUNNING", None)),
)
.answer(
"CreateMicrovmAuthToken",
Answer::ok(fake::auth_token_response("proxy-token")),
);
}
async fn launched() -> (Sandbox, Arc<FakeControlPlane>, Arc<TestClock>) {
let (mut sandbox, recorder, clock) = planted();
answer_launch(&recorder);
sandbox
.run(RunRequest::new().with_image("arn:image"))
.await
.expect("the launch reaches RUNNING");
(sandbox, recorder, clock)
}
async fn suspended_with_window(
suspended_sec: u32,
) -> (Sandbox, Arc<FakeControlPlane>, Arc<TestClock>) {
let (mut sandbox, recorder, clock) = planted();
recorder
.answer(
"RunMicrovm",
Answer::ok(fake::microvm_response("PENDING", None)),
)
.answer(
"GetMicrovm",
Answer::ok(fake::microvm_response("RUNNING", None)),
)
.answer(
"GetMicrovm",
Answer::ok(fake::microvm_response("SUSPENDED", None)),
)
.answer(
"CreateMicrovmAuthToken",
Answer::ok(fake::auth_token_response("proxy-token")),
)
.answer("SuspendMicrovm", Answer::ok(fake::empty_response()));
sandbox
.run(
RunRequest::new()
.with_image("arn:image")
.with_suspended_sec(suspended_sec),
)
.await
.expect("launches");
sandbox.suspend().await.expect("suspends");
assert_eq!(
clock.now(),
Duration::ZERO,
"no wait may have slept, or the window arithmetic below is measuring the fake's \
poll interval as well as the elapsed suspension"
);
(sandbox, recorder, clock)
}
async fn suspended() -> (Sandbox, Arc<FakeControlPlane>, Arc<TestClock>) {
suspended_with_window(600).await
}
#[tokio::test]
async fn a_launch_records_the_image_and_the_running_report_installs_the_token() {
let (mut sandbox, recorder, _) = planted();
assert_eq!(sandbox.lifecycle(), Lifecycle::Pending);
assert!(
!sandbox.image_exists(),
"nothing is recorded before a launch"
);
assert!(!sandbox.token_installed());
answer_launch(&recorder);
let session = sandbox
.run(RunRequest::new().with_image("arn:image"))
.await
.expect("launches");
assert_eq!(
session.endpoint(),
"https://mvm-abc123.microvm.us-east-1.amazonaws.com"
);
assert_eq!(sandbox.lifecycle(), Lifecycle::Running);
assert!(sandbox.image_exists(), "STATE-1: the image is recorded");
assert!(sandbox.token_installed(), "STATE-2: the token is installed");
assert_eq!(sandbox.bootstrap_count(), 1);
assert_eq!(sandbox.microvm().expect("launched").id, "mvm-abc123");
let body = recorder.first_body("RunMicrovm");
let payload = body["runHookPayload"].as_str().expect("a string");
assert!(payload.starts_with(r#"{"agent_token":"#), "{payload}");
assert_eq!(body["idlePolicy"]["suspendedDurationSeconds"], 600);
}
#[tokio::test]
async fn a_launch_env_reaches_the_run_hook_payload_on_the_wire() {
let (mut sandbox, recorder, _) = planted();
answer_launch(&recorder);
sandbox
.run(
RunRequest::new()
.with_image("arn:image")
.with_launch_env("ANTHROPIC_BASE_URL", "https://gateway.example")
.with_launch_env("PATH", "/usr/local/bin:/usr/bin:/bin"),
)
.await
.expect("launches");
let body = recorder.first_body("RunMicrovm");
let payload = body["runHookPayload"].as_str().expect("a string");
let inner: serde_json::Value =
serde_json::from_str(payload).expect("the payload is itself JSON");
assert!(
inner["agent_token"].as_str().is_some_and(|t| !t.is_empty()),
"the token still rides alongside: {payload}"
);
assert_eq!(
inner["env"]["ANTHROPIC_BASE_URL"],
"https://gateway.example"
);
assert_eq!(inner["env"]["PATH"], "/usr/local/bin:/usr/bin:/bin");
}
#[tokio::test]
async fn a_pinned_image_version_is_forwarded_to_the_launch_request() {
let (mut sandbox, recorder, _) = planted();
answer_launch(&recorder);
sandbox
.run(
RunRequest::new()
.with_image("arn:image")
.with_image_version("2.0"),
)
.await
.expect("launches");
assert_eq!(
recorder.first_body("RunMicrovm")["imageVersion"],
"2.0",
"a canary has to launch against the version it means to test"
);
let (mut sandbox, recorder, _) = planted();
answer_launch(&recorder);
sandbox
.run(RunRequest::new().with_image("arn:image"))
.await
.expect("launches");
assert!(
recorder
.first_body("RunMicrovm")
.get("imageVersion")
.is_none(),
"an unpinned launch sends what this client always sent"
);
}
#[tokio::test]
async fn an_over_budget_launch_env_is_refused_before_any_control_plane_call() {
let (mut sandbox, recorder, _) = planted();
answer_launch(&recorder);
let error = sandbox
.run(
RunRequest::new()
.with_image("arn:image")
.with_launch_env("AWS_SESSION_TOKEN", "t".repeat(4096)),
)
.await
.expect_err("a credential-scale launch env does not fit the payload");
assert_eq!(error.kind(), ErrorKind::InvalidArg);
let message = error.to_string();
assert!(message.contains("ceiling of 4096"), "{message}");
assert!(message.contains("launch env contributed"), "{message}");
assert_eq!(
recorder.calls().len(),
0,
"the refusal has to be local: a launch that reached AWS costs a VM and a bill"
);
assert_eq!(sandbox.lifecycle(), Lifecycle::Pending);
assert_eq!(sandbox.bootstrap_count(), 0);
assert!(sandbox.microvm().is_none());
}
#[tokio::test]
async fn the_bindings_layer_refuses_an_illegal_launch_with_zero_calls() {
let cases: [(&str, RunRequest, &str); 3] = [
(
"max_idle_sec=59, which is what a Python keyword argument passes",
RunRequest {
max_idle_sec: 59,
..RunRequest::new().with_image("arn:image")
},
"maxIdleDurationSeconds",
),
(
"an execution role that is a bare name",
RunRequest {
execution_role_arn: Some("execution-role".to_string()),
..RunRequest::new().with_image("arn:image")
},
"role *name*",
),
(
"a pinned version with a trailing newline",
RunRequest::new()
.with_image("arn:image")
.with_image_version("2.0\n"),
"contains whitespace",
),
];
for (label, request, expected) in cases {
let (mut sandbox, recorder, _) = planted();
answer_launch(&recorder);
let Err(error) = sandbox.run(request).await else {
panic!("{label} must be refused");
};
assert_eq!(error.kind(), ErrorKind::InvalidArg, "{label}");
assert!(
error.to_string().contains(expected),
"{label}: wanted {expected:?}, got {error}"
);
assert_eq!(
recorder.calls().len(),
0,
"{label}: the refusal has to be local, because a launch that reached AWS costs a \
VM and a bill"
);
assert_eq!(sandbox.lifecycle(), Lifecycle::Pending);
assert!(sandbox.microvm().is_none());
}
}
#[tokio::test]
async fn a_launch_that_dies_during_startup_installs_no_token() {
let (mut sandbox, recorder, _) = planted();
recorder
.answer(
"RunMicrovm",
Answer::ok(fake::microvm_response("PENDING", None)),
)
.answer(
"GetMicrovm",
Answer::ok(fake::microvm_response(
"TERMINATED",
Some("run hook returned 500"),
)),
);
let error = sandbox
.run(RunRequest::new().with_image("arn:image"))
.await
.expect_err("a VM that died during startup is not a launch");
assert_eq!(error.kind(), ErrorKind::LaunchDied);
assert!(
error.to_string().contains("run hook returned 500"),
"{error}"
);
assert!(
!sandbox.token_installed(),
"the run hook never answered, so nothing was installed"
);
assert_eq!(
sandbox.bootstrap_count(),
0,
"a bootstrap counted here would make STATE-3's ceiling unreachable for a retry"
);
assert_eq!(sandbox.lifecycle(), Lifecycle::Pending);
assert!(
sandbox.image_exists(),
"STATE-1 still holds: the launch was accepted against a real image"
);
}
#[tokio::test]
async fn a_second_run_on_one_sandbox_is_refused_before_any_call() {
let (mut sandbox, recorder, _) = launched().await;
assert_eq!(sandbox.bootstrap_count(), 1);
let before = recorder.calls().len();
let error = sandbox
.run(RunRequest::new().with_image("arn:image"))
.await
.expect_err("the token is installed at most once per VM lifetime");
assert_eq!(error.kind(), ErrorKind::InvalidArg);
assert!(error.to_string().contains("STATE-3"), "{error}");
assert!(
error.to_string().contains("needs a second Sandbox"),
"the remedy has to be nameable: {error}"
);
assert_eq!(
sandbox.bootstrap_count(),
1,
"bootstrap_count <= 1 is the Z3-proved invariant"
);
assert_eq!(
recorder.calls().len(),
before,
"the refusal must cost no control-plane call"
);
assert_eq!(recorder.call_count("RunMicrovm"), 1);
}
#[tokio::test]
async fn a_suspend_from_running_passes_through_suspending_to_suspended() {
let (sandbox, recorder, _) = suspended().await;
assert_eq!(sandbox.lifecycle(), Lifecycle::Suspended);
assert!(
sandbox.token_installed(),
"a freeze keeps the guest's memory, so the token survives"
);
assert_eq!(sandbox.bootstrap_count(), 1, "no re-bootstrap on a freeze");
let calls = recorder.calls();
let suspend = calls
.iter()
.find(|call| call.operation == "SuspendMicrovm")
.expect("the suspend went out");
assert_eq!(suspend.method, Method::Post);
assert_eq!(suspend.path, "/2025-09-09/microvms/mvm-abc123/suspend");
}
#[tokio::test]
async fn a_vm_that_dies_while_suspending_is_recorded_rather_than_raised() {
let (mut sandbox, recorder, _) = launched().await;
recorder
.answer("SuspendMicrovm", Answer::ok(fake::empty_response()))
.answer(
"GetMicrovm",
Answer::ok(fake::microvm_response("TERMINATED", Some("idle policy"))),
);
sandbox
.suspend()
.await
.expect("a death mid-suspend is a state, not an error");
assert_eq!(sandbox.lifecycle(), Lifecycle::Terminated);
assert!(
sandbox.was_terminated(),
"STATE-11's precondition is recorded"
);
}
#[tokio::test]
async fn a_suspend_whose_call_fails_stays_running_and_can_be_retried() {
let (mut sandbox, recorder, _) = planted();
recorder
.answer(
"RunMicrovm",
Answer::ok(fake::microvm_response("PENDING", None)),
)
.answer(
"GetMicrovm",
Answer::ok(fake::microvm_response("RUNNING", None)),
)
.answer(
"GetMicrovm",
Answer::ok(fake::microvm_response("SUSPENDED", None)),
)
.answer(
"CreateMicrovmAuthToken",
Answer::ok(fake::auth_token_response("proxy-token")),
)
.answer("SuspendMicrovm", Answer::failure(409, "ConflictException"))
.answer("SuspendMicrovm", Answer::ok(fake::empty_response()));
sandbox
.run(RunRequest::new().with_image("arn:image"))
.await
.expect("launches");
sandbox
.suspend()
.await
.expect_err("the control plane refused the suspend");
assert_eq!(
sandbox.lifecycle(),
Lifecycle::Running,
"a refused suspend must not move the lifecycle: SUSPENDING accepts neither a \
suspend nor a resume, so recording it here would brick the handle"
);
sandbox.suspend().await.expect("the retry suspends");
assert_eq!(sandbox.lifecycle(), Lifecycle::Suspended);
assert_eq!(recorder.call_count("SuspendMicrovm"), 2);
}
#[tokio::test]
async fn a_suspend_from_a_non_running_state_reaches_no_control_plane_call() {
let (mut sandbox, recorder, _) = suspended().await;
let before = recorder.call_count("SuspendMicrovm");
let error = sandbox
.suspend()
.await
.expect_err("a suspend is only issued from RUNNING");
assert_eq!(error.kind(), ErrorKind::InvalidArg);
assert!(error.to_string().contains("STATE-5"), "{error}");
assert!(error.to_string().contains("SUSPENDED"), "{error}");
assert_eq!(
recorder.call_count("SuspendMicrovm"),
before,
"the refusal must be local: no second suspend went to the wire"
);
let (mut fresh, fresh_recorder, _) = planted();
let error = fresh.suspend().await.expect_err("nothing to suspend");
assert_eq!(error.kind(), ErrorKind::Precondition);
assert_eq!(fresh_recorder.calls().len(), 0);
}
#[tokio::test]
async fn a_resume_reuses_the_token_redelivers_nothing_and_drops_the_proxy_token() {
let (mut sandbox, recorder, _) = suspended().await;
let auth = Arc::clone(
sandbox
.session()
.expect("launched")
.proxy_auth()
.expect("the launch wired a minter"),
);
auth.headers().await.expect("mints");
assert!(
auth.is_cached(),
"there must be a token for the drop to matter"
);
assert_eq!(auth.mint_count(), 1);
recorder
.answer("ResumeMicrovm", Answer::ok(fake::empty_response()))
.answer(
"GetMicrovm",
Answer::ok(fake::microvm_response("RUNNING", None)),
);
let session = sandbox.resume().await.expect("resumes");
assert_eq!(
session.endpoint(),
"https://mvm-abc123.microvm.us-east-1.amazonaws.com"
);
assert_eq!(sandbox.lifecycle(), Lifecycle::Running);
assert!(
sandbox.token_installed(),
"STATE-7: the installed token is reused"
);
assert_eq!(
sandbox.bootstrap_count(),
1,
"STATE-7: a resume re-delivers no run-hook payload"
);
assert_eq!(
recorder.call_count("RunMicrovm"),
1,
"a resume is not a second launch"
);
assert!(
!auth.is_cached(),
"STATE-8: the cached proxy token survived the resume"
);
}
#[tokio::test]
async fn the_request_after_a_resume_mints_a_fresh_proxy_token() {
let (mut sandbox, recorder, _) = suspended().await;
let auth = Arc::clone(
sandbox
.session()
.expect("launched")
.proxy_auth()
.expect("wired"),
);
auth.headers().await.expect("mints");
assert_eq!(auth.mint_count(), 1);
recorder
.answer("ResumeMicrovm", Answer::ok(fake::empty_response()))
.answer(
"GetMicrovm",
Answer::ok(fake::microvm_response("RUNNING", None)),
);
sandbox.resume().await.expect("resumes");
auth.headers().await.expect("re-mints");
assert_eq!(
auth.mint_count(),
2,
"STATE-8: the resume did not force a fresh mint"
);
}
#[tokio::test]
async fn a_resume_past_the_suspended_window_is_refused_before_the_wire() {
let (mut sandbox, recorder, clock) = suspended_with_window(60).await;
assert_eq!(sandbox.suspended_window(), Some(Duration::from_secs(60)));
clock.advance(Duration::from_secs(61));
recorder
.answer("ResumeMicrovm", Answer::ok(fake::empty_response()))
.answer(
"GetMicrovm",
Answer::ok(fake::microvm_response(
"TERMINATED",
Some("suspended window elapsed"),
)),
);
let elapsed_before = clock.now();
let resumes_before = recorder.call_count("ResumeMicrovm");
let error = sandbox
.resume()
.await
.expect_err("the window the launch set has closed");
assert_eq!(error.kind(), ErrorKind::WindowClosed);
assert_eq!(error.code(), "ERR_WINDOW_CLOSED");
let message = error.to_string();
assert!(message.contains("61s"), "the elapsed time: {message}");
assert!(
message.contains("60s suspendedDurationSeconds"),
"the window has to be named: {message}"
);
assert!(message.contains("idlePolicy"), "the finding: {message}");
assert!(
message.contains("GetMicrovm does not return it"),
"why the client is the only party that can say this: {message}"
);
assert_eq!(
recorder.call_count("ResumeMicrovm"),
resumes_before,
"the refusal must come before ResumeMicrovm"
);
assert_eq!(
clock.now(),
elapsed_before,
"the refusal must be immediate rather than polled to the deadline"
);
}
#[tokio::test]
async fn a_resume_inside_the_window_is_allowed_and_the_boundary_is_inclusive() {
let (mut sandbox, recorder, clock) = suspended_with_window(60).await;
clock.advance(Duration::from_secs(60));
recorder
.answer("ResumeMicrovm", Answer::ok(fake::empty_response()))
.answer(
"GetMicrovm",
Answer::ok(fake::microvm_response("RUNNING", None)),
);
sandbox
.resume()
.await
.expect("60s == the 60s window is open");
assert_eq!(sandbox.lifecycle(), Lifecycle::Running);
assert_eq!(recorder.call_count("ResumeMicrovm"), 1);
}
#[tokio::test]
async fn a_successful_resume_clears_the_stamp_so_a_second_cycle_measures_its_own_window() {
let (mut sandbox, recorder, clock) = planted();
recorder
.answer(
"RunMicrovm",
Answer::ok(fake::microvm_response("PENDING", None)),
)
.answer(
"CreateMicrovmAuthToken",
Answer::ok(fake::auth_token_response("proxy-token")),
)
.answer("SuspendMicrovm", Answer::ok(fake::empty_response()))
.answer("ResumeMicrovm", Answer::ok(fake::empty_response()))
.answer(
"GetMicrovm",
Answer::ok(fake::microvm_response("RUNNING", None)),
)
.answer(
"GetMicrovm",
Answer::ok(fake::microvm_response("SUSPENDED", None)),
)
.answer(
"GetMicrovm",
Answer::ok(fake::microvm_response("RUNNING", None)),
)
.answer(
"GetMicrovm",
Answer::ok(fake::microvm_response("SUSPENDED", None)),
)
.answer(
"GetMicrovm",
Answer::ok(fake::microvm_response("RUNNING", None)),
);
sandbox
.run(
RunRequest::new()
.with_image("arn:image")
.with_suspended_sec(60),
)
.await
.expect("launches");
for cycle in 0..2 {
sandbox
.suspend()
.await
.unwrap_or_else(|error| panic!("cycle {cycle} suspend: {error}"));
assert_eq!(sandbox.lifecycle(), Lifecycle::Suspended, "cycle {cycle}");
clock.advance(Duration::from_secs(45));
sandbox.resume().await.unwrap_or_else(|error| {
panic!("cycle {cycle} resume, whose own window is wide open: {error}")
});
assert_eq!(sandbox.lifecycle(), Lifecycle::Running, "cycle {cycle}");
assert_eq!(
sandbox.bootstrap_count(),
1,
"cycle {cycle}: no re-bootstrap"
);
}
assert_eq!(
clock.now(),
Duration::from_secs(90),
"ninety seconds of accumulated suspension against a sixty-second window: this is \
the total an implementation that never cleared the stamp would have compared"
);
assert_eq!(
recorder.call_count("ResumeMicrovm"),
2,
"both cycles resumed"
);
}
#[tokio::test]
async fn a_resume_after_terminate_records_zero_resume_calls() {
let (mut sandbox, recorder, _) = launched().await;
recorder.answer("TerminateMicrovm", Answer::ok(fake::empty_response()));
let report = sandbox.terminate(TeardownOpts::default()).await;
assert!(report.terminate_accepted);
assert!(sandbox.was_terminated());
let error = sandbox
.resume()
.await
.expect_err("a terminated VM never returns to RUNNING");
assert_eq!(error.kind(), ErrorKind::InvalidArg);
assert!(error.to_string().contains("STATE-11"), "{error}");
assert_eq!(
recorder.call_count("ResumeMicrovm"),
0,
"no resume may reach the wire once the VM was terminated"
);
assert_ne!(
sandbox.lifecycle(),
Lifecycle::Running,
"the lifecycle must not return to RUNNING"
);
}
#[tokio::test]
async fn a_terminate_records_terminating_and_the_completion_report_marks_terminated() {
let (mut sandbox, recorder, _) = launched().await;
recorder.answer("TerminateMicrovm", Answer::ok(fake::empty_response()));
let report = sandbox.terminate(TeardownOpts::default()).await;
assert!(report.terminate_accepted, "STATE-9: the call was accepted");
assert!(sandbox.was_terminated(), "STATE-9: recorded terminated");
assert_eq!(
report.lifecycle,
Some(Lifecycle::Terminating),
"the default teardown does not wait, so TERMINATED is not claimed"
);
assert!(!report.leaked(), "{report:?}");
let (mut sandbox, recorder, _) = launched().await;
recorder
.answer("TerminateMicrovm", Answer::ok(fake::empty_response()))
.answer(
"GetMicrovm",
Answer::ok(fake::microvm_response("TERMINATED", None)),
);
let report = sandbox
.terminate(TeardownOpts::default().waiting_for_terminated())
.await;
assert_eq!(report.lifecycle, Some(Lifecycle::Terminated));
assert_eq!(sandbox.lifecycle(), Lifecycle::Terminated);
}
#[tokio::test]
async fn teardown_deletes_the_vm_then_the_image_and_names_the_log_group_last() {
let (mut sandbox, recorder, _) = planted();
recorder
.answer(
"CreateMicrovmImage",
Answer::created(fake::create_image_response("agentd-conformance")),
)
.answer(
"GetMicrovmImage",
Answer::ok(fake::get_image_response("agentd-conformance", "CREATED")),
);
sandbox
.build_image(CreateImageRequest::new(
"agentd-conformance",
b"binary".to_vec(),
"s3://bucket/img.zip",
"arn:aws:iam::123456789012:role/build",
))
.await
.expect("builds");
answer_launch(&recorder);
sandbox
.run(RunRequest::new())
.await
.expect("launches from the built image");
recorder
.answer("TerminateMicrovm", Answer::ok(fake::empty_response()))
.answer(
"ListMicrovmImageVersions",
Answer::ok(fake::list_versions_response("1")),
)
.answer(
"DeleteMicrovmImage",
Answer::ok(fake::delete_image_response()),
);
let report = sandbox
.terminate(
TeardownOpts::default()
.deleting_image()
.deleting_log_group(),
)
.await;
assert!(report.terminate_accepted);
assert_eq!(report.image_deleted, Some(true));
let order: Vec<&str> = recorder
.operations()
.into_iter()
.filter(|operation| matches!(*operation, "TerminateMicrovm" | "DeleteMicrovmImage"))
.collect();
assert_eq!(
order,
["TerminateMicrovm", "DeleteMicrovmImage"],
"the VM goes before the image: a terminating VM holds a reference to it"
);
assert_eq!(
report.undeleted,
["/aws/lambda-microvms/agentd-conformance"],
"{report:?}"
);
assert!(report.leaked());
assert!(
report
.failures
.last()
.expect("a failure")
.contains("log group"),
"the log group's failure has to be the last one recorded: {report:?}"
);
assert!(
report
.failures
.last()
.expect("a failure")
.contains("terraform destroy"),
"the reason it leaks has to be named: {report:?}"
);
}
#[tokio::test]
async fn a_failing_image_delete_names_what_it_left_behind() {
let (mut sandbox, recorder, _) = planted();
recorder
.answer(
"CreateMicrovmImage",
Answer::created(fake::create_image_response("img")),
)
.answer(
"GetMicrovmImage",
Answer::ok(fake::get_image_response("img", "CREATED")),
);
sandbox
.build_image(CreateImageRequest::new(
"img",
b"binary".to_vec(),
"s3://bucket/img.zip",
"arn:aws:iam::123456789012:role/build",
))
.await
.expect("builds");
answer_launch(&recorder);
sandbox.run(RunRequest::new()).await.expect("launches");
recorder
.answer("TerminateMicrovm", Answer::ok(fake::empty_response()))
.answer(
"ListMicrovmImageVersions",
Answer::ok(fake::list_versions_response("1")),
)
.answer(
"DeleteMicrovmImage",
Answer::failure(409, "image is in CREATING"),
);
let mut opts = TeardownOpts::default().deleting_image();
opts.delete_attempts = 3;
opts.delete_backoff = Duration::from_secs(1);
let report = sandbox.terminate(opts).await;
assert_eq!(report.image_deleted, Some(false));
assert_eq!(
report.undeleted,
["arn:aws:lambda:us-east-1:123456789012:microvm-image:img"],
"{report:?}"
);
assert!(
sandbox.image_exists(),
"the image really is still there, so the flag must say so"
);
assert_eq!(
recorder.call_count("DeleteMicrovmImage"),
3,
"every attempt ran"
);
}
#[tokio::test]
async fn a_terminate_whose_call_fails_still_records_the_intent_and_the_leak() {
let (mut sandbox, recorder, _) = launched().await;
recorder.answer(
"TerminateMicrovm",
Answer::failure(500, "InternalServerException"),
);
let report = sandbox.terminate(TeardownOpts::default()).await;
assert!(!report.terminate_accepted);
assert_eq!(report.undeleted, ["mvm-abc123"], "{report:?}");
assert!(sandbox.was_terminated());
assert_eq!(report.lifecycle, Some(Lifecycle::Terminating));
assert!(
!report.failures.is_empty(),
"the swallowed failure is recorded"
);
}
#[tokio::test]
async fn a_teardown_before_any_launch_is_an_empty_report() {
let (mut sandbox, recorder, _) = planted();
let report = sandbox.terminate(TeardownOpts::default()).await;
assert!(!report.terminate_accepted);
assert!(!report.leaked());
assert_eq!(report.lifecycle, Some(Lifecycle::Pending));
assert_eq!(recorder.calls().len(), 0);
}
#[tokio::test]
async fn a_run_with_no_image_is_refused_before_any_call() {
let (mut sandbox, recorder, _) = planted();
let error = sandbox
.run(RunRequest::new())
.await
.expect_err("there is nothing to launch");
assert_eq!(error.kind(), ErrorKind::Precondition);
assert!(error.to_string().contains("build_image first"), "{error}");
assert_eq!(recorder.calls().len(), 0);
}
#[test]
fn the_lifecycle_states_are_the_state_models_domain_in_the_services_spelling() {
let all = [
Lifecycle::Pending,
Lifecycle::Running,
Lifecycle::Suspending,
Lifecycle::Suspended,
Lifecycle::Terminating,
Lifecycle::Terminated,
];
assert_eq!(
all.map(Lifecycle::as_str),
[
"PENDING",
"RUNNING",
"SUSPENDING",
"SUSPENDED",
"TERMINATING",
"TERMINATED"
]
);
for state in all {
assert_eq!(state.to_string(), state.as_str());
}
assert_eq!(
all.iter().filter(|state| state.is_live()).count(),
4,
"a state added to the enum defaults to not-live, which would silence the warning"
);
assert!(!Lifecycle::Terminating.is_live());
assert!(!Lifecycle::Terminated.is_live());
}
#[test]
fn a_minted_agent_token_is_sixty_four_hex_characters_and_never_repeats() {
let minted: std::collections::HashSet<String> =
(0..200).map(|_| mint_agent_token()).collect();
assert_eq!(minted.len(), 200, "a repeated token is a shared guest");
for token in &minted {
assert_eq!(token.len(), 64, "{token}");
assert!(token.chars().all(|c| c.is_ascii_hexdigit()), "{token}");
assert_ne!(
token,
&"0".repeat(64),
"an all-zero draw is a read that did nothing"
);
}
}
#[tokio::test]
async fn a_sandbox_debug_does_not_print_the_agent_token() {
let (mut sandbox, recorder, _) = planted();
answer_launch(&recorder);
sandbox
.run(
RunRequest::new()
.with_image("arn:image")
.with_agent_token("super-secret-agent-token"),
)
.await
.expect("launches");
let rendered = format!("{sandbox:?}");
assert!(rendered.contains("mvm-abc123"), "{rendered}");
assert!(rendered.contains("Running"), "{rendered}");
assert!(
!rendered.contains("super-secret"),
"the agent token reached a Debug string: {rendered}"
);
}
#[tokio::test]
async fn a_torn_down_sandbox_is_distinguishable_from_an_abandoned_one() {
let (mut sandbox, recorder, _) = launched().await;
assert!(
sandbox.lifecycle().is_live() && !sandbox.torn_down,
"an abandoned live VM is what the drop warning is for"
);
recorder.answer("TerminateMicrovm", Answer::ok(fake::empty_response()));
sandbox.terminate(TeardownOpts::default()).await;
assert!(
sandbox.torn_down,
"terminate must mark the sandbox torn down"
);
assert!(
!sandbox.lifecycle().is_live(),
"and the lifecycle must no longer read as live"
);
}
}