use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateMicrovmImageWire {
pub name: String,
pub base_image_arn: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub base_image_version: Option<String>,
pub build_role_arn: String,
pub code_artifact: CodeArtifact,
pub cpu_configurations: Vec<CpuConfiguration>,
pub resources: Vec<Resources>,
pub hooks: Hooks,
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_os_capabilities: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<std::collections::BTreeMap<String, String>>,
pub(crate) client_token: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeArtifact {
pub uri: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CpuConfiguration {
pub architecture: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Resources {
#[serde(rename = "minimumMemoryInMiB")]
pub minimum_memory_in_mib: u32,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Hooks {
pub port: u16,
pub microvm_hooks: MicrovmHooks,
pub microvm_image_hooks: MicrovmImageHooks,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum HookState {
#[serde(rename = "DISABLED")]
Disabled,
#[serde(rename = "ENABLED")]
Enabled,
}
impl HookState {
pub fn as_str(self) -> &'static str {
match self {
HookState::Disabled => "DISABLED",
HookState::Enabled => "ENABLED",
}
}
}
impl std::fmt::Display for HookState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MicrovmHooks {
pub run: HookState,
pub run_timeout_in_seconds: u32,
pub resume: HookState,
pub resume_timeout_in_seconds: u32,
pub suspend: HookState,
pub suspend_timeout_in_seconds: u32,
pub terminate: HookState,
pub terminate_timeout_in_seconds: u32,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MicrovmImageHooks {
pub ready: HookState,
pub ready_timeout_in_seconds: u32,
pub validate: HookState,
pub validate_timeout_in_seconds: u32,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RunMicrovmWire {
pub image_identifier: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub image_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub execution_role_arn: Option<String>,
pub ingress_network_connectors: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub egress_network_connectors: Option<Vec<String>>,
pub idle_policy: IdlePolicy,
pub maximum_duration_in_seconds: u32,
pub run_hook_payload: String,
pub(crate) client_token: String,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct IdlePolicy {
pub max_idle_duration_seconds: u32,
pub suspended_duration_seconds: u32,
pub auto_resume_enabled: bool,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateImageVersionWire {
pub status: VersionStatus,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
pub enum VersionStatus {
#[serde(rename = "ACTIVE")]
Active,
#[serde(rename = "INACTIVE")]
Inactive,
}
impl VersionStatus {
pub fn as_str(self) -> &'static str {
match self {
VersionStatus::Active => "ACTIVE",
VersionStatus::Inactive => "INACTIVE",
}
}
}
impl std::fmt::Display for VersionStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateAuthTokenWire {
pub expiration_in_minutes: u32,
pub allowed_ports: Vec<PortSpecification>,
}
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(untagged)]
pub enum PortSpecification {
One {
#[serde(rename = "port")]
port: u16,
},
Range {
#[serde(rename = "range")]
range: PortRange,
},
All {
#[serde(rename = "allPorts")]
all_ports: AllPorts,
},
}
impl PortSpecification {
pub fn port(port: u16) -> Self {
Self::One { port }
}
pub fn range(start: u16, end: u16) -> Self {
Self::Range {
range: PortRange {
start_port: start,
end_port: end,
},
}
}
pub fn all() -> Self {
Self::All {
all_ports: AllPorts {},
}
}
}
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PortRange {
pub start_port: u16,
pub end_port: u16,
}
#[derive(Clone, Copy, Debug, Serialize)]
pub struct AllPorts {}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateMicrovmImageResponseWire {
pub image_arn: String,
pub name: String,
pub state: String,
pub image_version: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetMicrovmImageResponseWire {
pub image_arn: String,
pub name: String,
pub state: String,
pub latest_active_image_version: Option<String>,
pub latest_failed_image_version: Option<String>,
pub created_at: f64,
pub updated_at: Option<f64>,
pub tags: Option<std::collections::BTreeMap<String, String>>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListImageVersionsResponseWire {
pub items: Vec<MicrovmImageVersionSummaryWire>,
pub next_token: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MicrovmImageVersionSummaryWire {
pub base_image_arn: String,
pub build_role_arn: String,
pub code_artifact: CodeArtifact,
pub image_arn: String,
pub image_version: String,
pub state: String,
pub status: String,
pub created_at: f64,
pub base_image_version: Option<String>,
pub description: Option<String>,
pub resources: Option<Vec<Resources>>,
pub cpu_configurations: Option<Vec<CpuConfiguration>>,
pub egress_network_connectors: Option<Vec<String>>,
pub additional_os_capabilities: Option<Vec<String>>,
pub environment_variables: Option<std::collections::BTreeMap<String, String>>,
pub updated_at: Option<f64>,
pub state_reason: Option<String>,
pub tags: Option<std::collections::BTreeMap<String, String>>,
}
impl MicrovmImageVersionSummaryWire {
pub fn is_active(&self) -> bool {
self.status == VersionStatus::Active.as_str()
}
pub fn describe(&self) -> String {
match self.state_reason.as_deref() {
Some(reason) => format!(
"{} {} / {} ({reason})",
self.image_version, self.state, self.status
),
None => format!("{} {} / {}", self.image_version, self.state, self.status),
}
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListImageBuildsResponseWire {
pub items: Vec<MicrovmImageBuildSummaryWire>,
pub next_token: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MicrovmImageBuildSummaryWire {
pub image_arn: String,
pub image_version: String,
pub build_id: String,
pub build_state: String,
pub architecture: String,
pub chipset: String,
pub chipset_generation: String,
pub created_at: f64,
pub state_reason: Option<String>,
}
impl MicrovmImageBuildSummaryWire {
pub fn describe(&self) -> String {
match self.state_reason.as_deref() {
Some(reason) => format!("{} {} ({reason})", self.build_id, self.build_state),
None => format!("{} {}", self.build_id, self.build_state),
}
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetImageBuildResponseWire {
pub image_arn: String,
pub image_version: String,
pub build_id: String,
pub build_state: String,
pub architecture: String,
pub chipset: String,
pub chipset_generation: String,
pub created_at: f64,
pub state_reason: Option<String>,
pub snapshot_build: Option<SnapshotBuild>,
}
impl GetImageBuildResponseWire {
pub fn describe(&self) -> String {
let mut line = match self.state_reason.as_deref() {
Some(reason) => format!(
"{} {} (chipset generation {}) — {reason}",
self.build_id, self.build_state, self.chipset_generation
),
None => format!(
"{} {} (chipset generation {})",
self.build_id, self.build_state, self.chipset_generation
),
};
if let Some(sizes) = &self.snapshot_build
&& let Some(rendered) = sizes.describe()
{
line.push_str(&format!(", {rendered}"));
}
line
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SnapshotBuild {
pub memory_snapshot_size_in_bytes: Option<u64>,
pub code_install_size_in_bytes: Option<u64>,
pub disk_snapshot_size_in_bytes: Option<u64>,
}
impl SnapshotBuild {
pub fn describe(&self) -> Option<String> {
let parts: Vec<String> = [
("memory", self.memory_snapshot_size_in_bytes),
("code", self.code_install_size_in_bytes),
("disk", self.disk_snapshot_size_in_bytes),
]
.into_iter()
.filter_map(|(label, bytes)| bytes.map(|bytes| format!("{label} {bytes} bytes")))
.collect();
(!parts.is_empty()).then(|| parts.join(", "))
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListManagedVersionsResponseWire {
pub items: Vec<ManagedMicrovmImageVersionWire>,
pub next_token: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ManagedMicrovmImageVersionWire {
pub image_arn: String,
pub image_version: String,
pub created_at: f64,
pub updated_at: Option<f64>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListManagedImagesResponseWire {
pub items: Vec<ManagedMicrovmImageSummaryWire>,
pub next_token: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ManagedMicrovmImageSummaryWire {
pub image_arn: String,
pub created_at: f64,
pub updated_at: Option<f64>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MicrovmResponseWire {
pub microvm_id: String,
pub state: String,
pub endpoint: String,
pub image_arn: String,
pub image_version: String,
pub state_reason: Option<String>,
pub idle_policy: Option<IdlePolicy>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateAuthTokenResponseWire {
pub auth_token: std::collections::BTreeMap<String, String>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListMicrovmsResponseWire {
pub items: Vec<MicrovmItemWire>,
pub next_token: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MicrovmItemWire {
pub microvm_id: String,
pub state: String,
pub image_arn: String,
pub image_version: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeleteImageResponseWire {
pub image_identifier: String,
pub state: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListImagesResponseWire {
pub items: Vec<MicrovmImageSummaryWire>,
pub next_token: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MicrovmImageSummaryWire {
pub image_arn: String,
pub name: String,
pub state: String,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ServiceErrorWire {
pub message: Option<String>,
pub quota_code: Option<String>,
pub service_code: Option<String>,
pub resource_type: Option<String>,
pub resource_id: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_build_summary_deserialises_from_the_models_own_spelling() {
let json = r#"{
"imageArn": "arn:aws:lambda:us-east-1:123456789012:microvm-image:img",
"imageVersion": "1",
"buildId": "build-abc",
"buildState": "PENDING",
"architecture": "ARM_64",
"chipset": "GRAVITON",
"chipsetGeneration": "1",
"createdAt": 1754524800
}"#;
let parsed: MicrovmImageBuildSummaryWire =
serde_json::from_str(json).expect("the model's own spelling parses");
assert_eq!(parsed.build_state, "PENDING");
assert_eq!(parsed.build_id, "build-abc");
assert_eq!(parsed.image_version, "1");
assert_eq!(parsed.state_reason, None);
assert_eq!(
parsed.image_arn,
"arn:aws:lambda:us-east-1:123456789012:microvm-image:img"
);
assert_eq!(parsed.architecture, "ARM_64");
assert_eq!(parsed.chipset, "GRAVITON");
assert_eq!(parsed.chipset_generation, "1");
assert_eq!(parsed.created_at, 1_754_524_800.0);
}
#[test]
fn a_build_summary_missing_a_required_member_fails_to_deserialise() {
let members = [
(
"imageArn",
r#""arn:aws:lambda:us-east-1:1:microvm-image:i""#,
),
("imageVersion", r#""1""#),
("buildId", r#""build-abc""#),
("buildState", r#""PENDING""#),
("architecture", r#""ARM_64""#),
("chipset", r#""GRAVITON""#),
("chipsetGeneration", r#""1""#),
("createdAt", "1754524800"),
];
for omitted in members.iter().map(|(name, _)| *name) {
let body: Vec<String> = members
.iter()
.filter(|(name, _)| *name != omitted)
.map(|(name, value)| format!(r#""{name}": {value}"#))
.collect();
let json = format!("{{{}}}", body.join(", "));
let error = serde_json::from_str::<MicrovmImageBuildSummaryWire>(&json)
.expect_err(&format!("{omitted} is required by the model"));
assert!(
error.to_string().contains(omitted),
"the error must name the member that is missing ({omitted}): {error}"
);
}
let all: Vec<String> = members
.iter()
.map(|(name, value)| format!(r#""{name}": {value}"#))
.collect();
let parsed: MicrovmImageBuildSummaryWire =
serde_json::from_str(&format!("{{{}}}", all.join(", ")))
.expect("stateReason is optional");
assert_eq!(parsed.state_reason, None);
}
#[test]
fn a_build_describes_itself_with_its_id_and_only_a_reason_it_has() {
let with_reason: MicrovmImageBuildSummaryWire = serde_json::from_str(
r#"{
"imageArn": "arn:aws:lambda:us-east-1:1:microvm-image:i",
"imageVersion": "1",
"buildId": "build-abc",
"buildState": "FAILED",
"architecture": "ARM_64",
"chipset": "GRAVITON",
"chipsetGeneration": "1",
"createdAt": 1754524800,
"stateReason": "no space left on device"
}"#,
)
.expect("parses");
assert_eq!(
with_reason.describe(),
"build-abc FAILED (no space left on device)"
);
let without: MicrovmImageBuildSummaryWire = serde_json::from_str(
r#"{
"imageArn": "arn:aws:lambda:us-east-1:1:microvm-image:i",
"imageVersion": "1",
"buildId": "build-abc",
"buildState": "PENDING",
"architecture": "ARM_64",
"chipset": "GRAVITON",
"chipsetGeneration": "1",
"createdAt": 1754524800
}"#,
)
.expect("parses");
assert_eq!(without.describe(), "build-abc PENDING");
assert!(
!without.describe().contains("()"),
"an absent reason renders nothing, not an empty parenthesis: {}",
without.describe()
);
}
#[test]
fn a_summary_spelled_state_instead_of_build_state_fails_to_deserialise() {
let json = r#"{
"imageArn": "arn:aws:lambda:us-east-1:123456789012:microvm-image:img",
"imageVersion": "1",
"buildId": "build-abc",
"state": "PENDING",
"architecture": "ARM_64",
"chipset": "GRAVITON",
"chipsetGeneration": "1",
"createdAt": 1754524800
}"#;
let parsed: Result<MicrovmImageBuildSummaryWire, _> = serde_json::from_str(json);
let error = parsed.expect_err("MicrovmImageBuildSummary has no member called state");
assert!(
error.to_string().contains("buildState"),
"the error must name the member that is actually missing: {error}"
);
}
#[test]
fn a_version_summary_does_use_state_which_is_why_the_build_one_confuses() {
let json = r#"{
"baseImageArn": "arn:aws:lambda:us-east-1:aws:microvm-image:al2023-1",
"buildRoleArn": "arn:aws:iam::123456789012:role/build",
"codeArtifact": {"uri": "s3://bucket/img.zip"},
"imageArn": "arn:aws:lambda:us-east-1:123456789012:microvm-image:img",
"imageVersion": "1",
"state": "SUCCESSFUL",
"status": "ACTIVE",
"createdAt": 1754524800
}"#;
let parsed: MicrovmImageVersionSummaryWire =
serde_json::from_str(json).expect("the version summary's member is state");
assert_eq!(parsed.state, "SUCCESSFUL");
assert_eq!(parsed.status, "ACTIVE");
assert!(parsed.is_active(), "ACTIVE is what RunMicrovm will launch");
}
#[test]
fn a_version_summary_carries_the_state_reason_get_image_cannot() {
let json = r#"{
"baseImageArn": "arn:aws:lambda:us-east-1:aws:microvm-image:al2023-1",
"buildRoleArn": "arn:aws:iam::123456789012:role/build",
"codeArtifact": {"uri": "s3://bucket/img.zip"},
"imageArn": "arn:aws:lambda:us-east-1:123456789012:microvm-image:img",
"imageVersion": "2",
"state": "FAILED",
"status": "INACTIVE",
"createdAt": 1754524800,
"stateReason": "one or more builds failed"
}"#;
let parsed: MicrovmImageVersionSummaryWire = serde_json::from_str(json).expect("parses");
assert_eq!(parsed.state, "FAILED");
assert_eq!(
parsed.state_reason.as_deref(),
Some("one or more builds failed")
);
let image: GetMicrovmImageResponseWire = serde_json::from_str(
r#"{"imageArn": "a", "name": "n", "state": "CREATE_FAILED",
"createdAt": 1754524800, "stateReason": "ignored"}"#,
)
.expect("an unknown member is dropped");
assert_eq!(image.state, "CREATE_FAILED");
}
#[test]
fn a_get_image_response_carries_its_tags_and_timestamps() {
let json = r#"{
"imageArn": "arn:aws:lambda:us-east-1:123456789012:microvm-image:img",
"name": "img",
"state": "CREATED",
"latestActiveImageVersion": "1.0",
"createdAt": 1754524800,
"updatedAt": 1754528400,
"tags": {"owner": "conformance", "cost-centre": "agents"}
}"#;
let parsed: GetMicrovmImageResponseWire = serde_json::from_str(json).expect("parses");
assert_eq!(parsed.created_at, 1_754_524_800.0);
assert_eq!(parsed.updated_at, Some(1_754_528_400.0));
let tags = parsed.tags.expect("the map was sent");
assert_eq!(tags.get("owner").map(String::as_str), Some("conformance"));
assert_eq!(tags.len(), 2);
let empty: GetMicrovmImageResponseWire = serde_json::from_str(
r#"{"imageArn": "a", "name": "n", "state": "CREATED",
"createdAt": 1754524800, "updatedAt": 1754528400, "tags": {}}"#,
)
.expect("parses");
assert_eq!(empty.tags, Some(std::collections::BTreeMap::new()));
let absent: GetMicrovmImageResponseWire = serde_json::from_str(
r#"{"imageArn": "a", "name": "n", "state": "CREATED", "createdAt": 1754524800}"#,
)
.expect("tags is optional");
assert_eq!(absent.tags, None);
assert_eq!(absent.updated_at, None);
}
#[test]
fn a_get_image_response_without_created_at_fails_to_deserialise() {
let error = serde_json::from_str::<GetMicrovmImageResponseWire>(
r#"{"imageArn": "a", "name": "n", "state": "CREATED"}"#,
)
.expect_err("createdAt is one of the model's four required members");
assert!(error.to_string().contains("createdAt"), "{error}");
}
#[test]
fn an_idle_policy_comes_back_on_a_response_with_all_three_members() {
let json = r#"{
"microvmId": "mvm-1",
"state": "RUNNING",
"endpoint": "https://e",
"imageArn": "arn:aws:lambda:us-east-1:123456789012:microvm-image:img",
"imageVersion": "1.0",
"idlePolicy": {
"maxIdleDurationSeconds": 1800,
"suspendedDurationSeconds": 600,
"autoResumeEnabled": false
},
"maximumDurationInSeconds": 10800
}"#;
let parsed: MicrovmResponseWire = serde_json::from_str(json).expect("parses");
let policy = parsed.idle_policy.expect("the service sent one");
assert_eq!(policy.max_idle_duration_seconds, 1800);
assert_eq!(
policy.suspended_duration_seconds, 600,
"the member the comment claimed the response omits"
);
assert!(!policy.auto_resume_enabled);
}
#[test]
fn a_delete_readback_distinguishes_failure_from_deleting_and_deleted() {
for state in ["DELETING", "DELETED", "DELETE_FAILED"] {
let json = format!(
r#"{{"imageIdentifier": "arn:aws:lambda:us-east-1:1:microvm-image:img",
"state": "{state}"}}"#
);
let parsed: DeleteImageResponseWire = serde_json::from_str(&json).expect("parses");
assert_eq!(parsed.state, state);
assert_eq!(
parsed.image_identifier,
"arn:aws:lambda:us-east-1:1:microvm-image:img"
);
}
}
#[test]
fn an_image_listing_deserialises_from_the_models_own_spelling() {
let json = r#"{
"items": [{
"imageArn": "arn:aws:lambda:us-east-1:123456789012:microvm-image:img",
"name": "img",
"state": "ACTIVE",
"latestActiveImageVersion": "1",
"createdAt": 1754524800
}],
"nextToken": "opaque-page-2-token"
}"#;
let parsed: ListImagesResponseWire =
serde_json::from_str(json).expect("the model's own spelling parses");
assert_eq!(parsed.items.len(), 1);
assert_eq!(parsed.items[0].name, "img");
assert_eq!(
parsed.items[0].image_arn,
"arn:aws:lambda:us-east-1:123456789012:microvm-image:img"
);
assert_eq!(parsed.items[0].state, "ACTIVE");
assert_eq!(parsed.next_token.as_deref(), Some("opaque-page-2-token"));
let last: ListImagesResponseWire =
serde_json::from_str(r#"{"items": []}"#).expect("an absent nextToken parses");
assert!(last.items.is_empty());
assert_eq!(last.next_token, None);
}
#[test]
fn a_run_response_deserialises_with_its_state_reason() {
let json = r#"{
"microvmId": "mvm-123",
"state": "TERMINATED",
"endpoint": "https://mvm-123.microvm.amazonaws.com",
"imageArn": "arn:aws:lambda:us-east-1:123456789012:microvm-image:img",
"imageVersion": "1",
"maximumDurationInSeconds": 3600,
"startedAt": 1754524800,
"stateReason": "run hook returned 500"
}"#;
let parsed: MicrovmResponseWire = serde_json::from_str(json).expect("parses");
assert_eq!(parsed.state, "TERMINATED");
assert_eq!(
parsed.state_reason.as_deref(),
Some("run hook returned 500")
);
assert_eq!(parsed.microvm_id, "mvm-123");
}
#[test]
fn an_absent_state_reason_is_none_rather_than_an_empty_string() {
let json = r#"{
"microvmId": "mvm-123",
"state": "RUNNING",
"endpoint": "https://mvm-123.microvm.amazonaws.com",
"imageArn": "arn:aws:lambda:us-east-1:123456789012:microvm-image:img",
"imageVersion": "1",
"maximumDurationInSeconds": 3600,
"startedAt": 1754524800
}"#;
let parsed: MicrovmResponseWire = serde_json::from_str(json).expect("parses");
assert_eq!(parsed.state_reason, None);
}
#[test]
fn an_auth_token_response_is_a_header_map_not_a_string() {
let json = r#"{"authToken": {"X-aws-proxy-auth": "opaque-token-value"}}"#;
let parsed: CreateAuthTokenResponseWire = serde_json::from_str(json).expect("parses");
assert_eq!(
parsed
.auth_token
.get("X-aws-proxy-auth")
.map(String::as_str),
Some("opaque-token-value")
);
}
#[test]
fn a_string_auth_token_fails_to_deserialise() {
let json = r#"{"authToken": "opaque-token-value"}"#;
let parsed: Result<CreateAuthTokenResponseWire, _> = serde_json::from_str(json);
assert!(parsed.is_err(), "TokenParts is a map shape");
}
#[test]
fn an_access_denied_with_a_null_message_still_deserialises() {
let parsed: ServiceErrorWire =
serde_json::from_str(r#"{"message": null}"#).expect("a null message must parse");
assert_eq!(parsed.message, None);
let empty: ServiceErrorWire = serde_json::from_str("{}").expect("an absent message parses");
assert_eq!(empty.message, None);
}
#[test]
fn an_unknown_response_member_is_ignored() {
let json = r#"{
"microvmId": "mvm-123",
"state": "RUNNING",
"endpoint": "https://e",
"imageArn": "arn",
"imageVersion": "1",
"somethingAwsAddedLater": {"nested": [1, 2, 3]}
}"#;
let parsed: MicrovmResponseWire =
serde_json::from_str(json).expect("additive change is safe");
assert_eq!(parsed.microvm_id, "mvm-123");
}
#[test]
fn the_create_request_serialises_to_the_models_member_names() {
let wire = CreateMicrovmImageWire {
name: "img".to_string(),
base_image_arn: "arn:base".to_string(),
base_image_version: None,
build_role_arn: "arn:role".to_string(),
code_artifact: CodeArtifact {
uri: "s3://bucket/img.zip".to_string(),
},
cpu_configurations: vec![CpuConfiguration {
architecture: "ARM_64".to_string(),
}],
resources: vec![Resources {
minimum_memory_in_mib: 2048,
}],
hooks: Hooks {
port: 9000,
microvm_hooks: MicrovmHooks {
run: HookState::Enabled,
run_timeout_in_seconds: 30,
resume: HookState::Enabled,
resume_timeout_in_seconds: 30,
suspend: HookState::Enabled,
suspend_timeout_in_seconds: 30,
terminate: HookState::Enabled,
terminate_timeout_in_seconds: 30,
},
microvm_image_hooks: MicrovmImageHooks {
ready: HookState::Enabled,
ready_timeout_in_seconds: 300,
validate: HookState::Enabled,
validate_timeout_in_seconds: 300,
},
},
additional_os_capabilities: Some(vec!["ALL".to_string()]),
tags: None,
client_token: "create-img-0011223344556677".to_string(),
};
let value = serde_json::to_value(&wire).expect("serialises");
let object = value.as_object().expect("an object");
assert_eq!(value["resources"][0]["minimumMemoryInMiB"], 2048);
assert_eq!(value["baseImageArn"], "arn:base");
assert_eq!(value["buildRoleArn"], "arn:role");
assert_eq!(value["codeArtifact"]["uri"], "s3://bucket/img.zip");
assert_eq!(value["cpuConfigurations"][0]["architecture"], "ARM_64");
assert_eq!(
value["hooks"]["microvmImageHooks"]["readyTimeoutInSeconds"],
300
);
assert_eq!(value["hooks"]["microvmHooks"]["runTimeoutInSeconds"], 30);
assert_eq!(value["additionalOsCapabilities"][0], "ALL");
assert_eq!(value["clientToken"], "create-img-0011223344556677");
assert!(!object.contains_key("tags"), "an absent tag map is omitted");
assert!(
!object.contains_key("egressNetworkConnectors"),
"the client sends no image-level egress list. If you are adding one, the ceiling is \
MAX_IMAGE_EGRESS_CONNECTORS (1), NOT MAX_NETWORK_CONNECTORS (10) — the two differ by \
10x and the permissive direction is the one that reaches the wire."
);
}
#[test]
fn an_unrequested_capability_list_is_omitted_not_empty() {
let mut wire = minimal_create_wire();
wire.additional_os_capabilities = None;
let value = serde_json::to_value(&wire).expect("serialises");
assert!(
!value
.as_object()
.expect("object")
.contains_key("additionalOsCapabilities"),
"omitted, not empty: {value}"
);
}
#[test]
fn the_run_request_serialises_to_the_models_member_names() {
let wire = RunMicrovmWire {
image_identifier: "arn:image".to_string(),
image_version: None,
execution_role_arn: Some("arn:aws:iam::123456789012:role/exec".to_string()),
ingress_network_connectors: vec!["arn:connector:ALL_INGRESS".to_string()],
egress_network_connectors: None,
idle_policy: IdlePolicy {
max_idle_duration_seconds: 600,
suspended_duration_seconds: 600,
auto_resume_enabled: false,
},
maximum_duration_in_seconds: 3600,
run_hook_payload: r#"{"agent_token":"t"}"#.to_string(),
client_token: "run-arn-0011223344556677".to_string(),
};
let value = serde_json::to_value(&wire).expect("serialises");
assert_eq!(value["imageIdentifier"], "arn:image");
assert_eq!(
value["ingressNetworkConnectors"][0],
"arn:connector:ALL_INGRESS"
);
assert_eq!(value["idlePolicy"]["maxIdleDurationSeconds"], 600);
assert_eq!(value["idlePolicy"]["suspendedDurationSeconds"], 600);
assert_eq!(value["idlePolicy"]["autoResumeEnabled"], false);
assert_eq!(value["maximumDurationInSeconds"], 3600);
assert_eq!(value["runHookPayload"], r#"{"agent_token":"t"}"#);
assert!(
!value
.as_object()
.expect("object")
.contains_key("egressNetworkConnectors"),
"omitting egress is how you get no outbound network"
);
}
#[test]
fn the_auth_token_request_body_carries_no_uri_parameter() {
let wire = CreateAuthTokenWire {
expiration_in_minutes: 60,
allowed_ports: vec![PortSpecification::port(9000)],
};
let value = serde_json::to_value(&wire).expect("serialises");
assert_eq!(value["expirationInMinutes"], 60);
assert_eq!(value["allowedPorts"][0]["port"], 9000);
assert!(
!value
.as_object()
.expect("object")
.contains_key("microvmIdentifier"),
"it is a uri-located member: {value}"
);
}
#[test]
fn each_port_specification_variant_serialises_as_its_model_member() {
let cases = [
(
PortSpecification::port(8080),
"port",
serde_json::json!(8080),
),
(
PortSpecification::range(8000, 9100),
"range",
serde_json::json!({ "startPort": 8000, "endPort": 9100 }),
),
(PortSpecification::all(), "allPorts", serde_json::json!({})),
];
for (spec, key, expected) in cases {
let value = serde_json::to_value(spec).expect("serialises");
let object = value.as_object().expect("a union member is an object");
assert_eq!(
object.keys().collect::<Vec<_>>(),
vec![key],
"a union serialises as exactly one member named by the model: {value}"
);
assert_eq!(object[key], expected, "{key} carries the model's shape");
}
}
#[test]
fn a_build_get_response_carries_the_snapshot_sizes_the_listing_has_no_member_for() {
let json = r#"{
"imageArn": "arn:aws:lambda:us-east-1:392583147479:microvm-image:coding-agents-on-bedrock",
"imageVersion": "1.0",
"buildId": "d39847fa-4c6b-43c6-ba69-e25e3b55197b",
"buildState": "SUCCESSFUL",
"architecture": "ARM_64",
"chipset": "GRAVITON",
"chipsetGeneration": "4",
"createdAt": 1755150457.035,
"snapshotBuild": {
"memorySnapshotSizeInBytes": 582238208,
"codeInstallSizeInBytes": 2355486720,
"diskSnapshotSizeInBytes": 23760896
}
}"#;
let parsed: GetImageBuildResponseWire =
serde_json::from_str(json).expect("the model's own spelling parses");
assert_eq!(parsed.build_state, "SUCCESSFUL");
assert_eq!(parsed.build_id, "d39847fa-4c6b-43c6-ba69-e25e3b55197b");
assert_eq!(parsed.chipset_generation, "4");
assert_eq!(parsed.state_reason, None);
let sizes = parsed.snapshot_build.expect("a successful build has sizes");
assert_eq!(sizes.memory_snapshot_size_in_bytes, Some(582_238_208));
assert_eq!(sizes.code_install_size_in_bytes, Some(2_355_486_720));
assert_eq!(sizes.disk_snapshot_size_in_bytes, Some(23_760_896));
assert_eq!(
sizes.describe().as_deref(),
Some("memory 582238208 bytes, code 2355486720 bytes, disk 23760896 bytes")
);
}
#[test]
fn a_failed_builds_snapshot_sizes_are_partial_rather_than_zeroed() {
let json = r#"{
"imageArn": "arn:aws:lambda:us-east-1:392583147479:microvm-image:bonk-sandbox-v4",
"imageVersion": "1.0",
"buildId": "4a4c5e30-811f-47fa-9893-260ea6a37a8f",
"buildState": "FAILED",
"architecture": "ARM_64",
"chipset": "GRAVITON",
"chipsetGeneration": "4",
"stateReason": "Ready hook invocation timed out after PT5M",
"createdAt": 1751042593.476,
"snapshotBuild": {"codeInstallSizeInBytes": 1724940288}
}"#;
let parsed: GetImageBuildResponseWire = serde_json::from_str(json).expect("parses");
let sizes = parsed
.snapshot_build
.expect("the service sent a partial breakdown");
assert_eq!(sizes.code_install_size_in_bytes, Some(1_724_940_288));
assert_eq!(
sizes.memory_snapshot_size_in_bytes, None,
"no memory snapshot was produced, which is the finding — not a size of zero"
);
assert_eq!(sizes.disk_snapshot_size_in_bytes, None);
let described = parsed.describe();
assert!(
described.contains("Ready hook invocation timed out"),
"{described}"
);
assert!(
described.contains("chipset generation 4"),
"one CreateMicrovmImage fans out per generation, so the line has to name which: \
{described}"
);
assert!(described.contains("code 1724940288 bytes"), "{described}");
assert!(
!described.contains("memory"),
"an absent size must not render at all: {described}"
);
}
#[test]
fn a_build_with_no_snapshot_block_renders_no_size_clause() {
let json = r#"{
"imageArn": "arn:aws:lambda:us-east-1:1:microvm-image:img",
"imageVersion": "1",
"buildId": "build-abc",
"buildState": "PENDING",
"architecture": "ARM_64",
"chipset": "GRAVITON",
"chipsetGeneration": "3",
"createdAt": 1754524800
}"#;
let parsed: GetImageBuildResponseWire = serde_json::from_str(json).expect("parses");
assert_eq!(parsed.snapshot_build, None);
assert_eq!(
parsed.describe(),
"build-abc PENDING (chipset generation 3)"
);
assert!(!parsed.describe().ends_with(", "), "{}", parsed.describe());
let empty: GetImageBuildResponseWire = serde_json::from_str(
r#"{"imageArn": "a", "imageVersion": "1", "buildId": "b", "buildState": "PENDING",
"architecture": "ARM_64", "chipset": "GRAVITON", "chipsetGeneration": "3",
"createdAt": 1754524800, "snapshotBuild": {}}"#,
)
.expect("an empty block parses");
assert_eq!(empty.snapshot_build.expect("Some").describe(), None);
assert!(!empty.describe().contains("bytes"), "{}", empty.describe());
}
#[test]
fn a_build_get_response_missing_a_required_member_fails_to_deserialise() {
let members = [
(
"imageArn",
r#""arn:aws:lambda:us-east-1:1:microvm-image:i""#,
),
("imageVersion", r#""1""#),
("buildId", r#""build-abc""#),
("buildState", r#""SUCCESSFUL""#),
("architecture", r#""ARM_64""#),
("chipset", r#""GRAVITON""#),
("chipsetGeneration", r#""4""#),
("createdAt", "1754524800"),
];
for omitted in members.iter().map(|(name, _)| *name) {
let body: Vec<String> = members
.iter()
.filter(|(name, _)| *name != omitted)
.map(|(name, value)| format!(r#""{name}": {value}"#))
.collect();
let error = serde_json::from_str::<GetImageBuildResponseWire>(&format!(
"{{{}}}",
body.join(", ")
))
.expect_err(&format!("{omitted} is required by the model"));
assert!(error.to_string().contains(omitted), "{omitted}: {error}");
}
}
#[test]
fn a_get_image_version_response_parses_as_the_summary_with_its_whole_config() {
let json = r#"{
"baseImageArn": "arn:aws:lambda:us-east-1:aws:microvm-image:al2023-1",
"baseImageVersion": "1.0",
"buildRoleArn": "arn:aws:iam::392583147479:role/agentd-conformance-build-b2111c56",
"codeArtifact": {"uri": "s3://agentd-conformance-392583147479-b2111c56/coding-agents-on-bedrock.zip"},
"egressNetworkConnectors": [
"arn:aws:lambda:us-east-1:aws:network-connector:aws-network-connector:INTERNET_EGRESS"
],
"cpuConfigurations": [{"architecture": "ARM_64"}],
"resources": [{"minimumMemoryInMiB": 2048}],
"hooks": {
"port": 9000,
"microvmHooks": {
"run": "ENABLED", "runTimeoutInSeconds": 30,
"resume": "ENABLED", "resumeTimeoutInSeconds": 30,
"suspend": "ENABLED", "suspendTimeoutInSeconds": 30,
"terminate": "ENABLED", "terminateTimeoutInSeconds": 30
},
"microvmImageHooks": {
"ready": "ENABLED", "readyTimeoutInSeconds": 30,
"validate": "ENABLED", "validateTimeoutInSeconds": 30
}
},
"imageArn": "arn:aws:lambda:us-east-1:392583147479:microvm-image:coding-agents-on-bedrock",
"imageVersion": "1.0",
"state": "SUCCESSFUL",
"status": "ACTIVE",
"createdAt": 1755150457.035,
"updatedAt": 1755150629.117
}"#;
let parsed: MicrovmImageVersionSummaryWire =
serde_json::from_str(json).expect("a real GetMicrovmImageVersion response parses");
assert_eq!(parsed.image_version, "1.0");
assert_eq!(parsed.state, "SUCCESSFUL");
assert_eq!(parsed.status, "ACTIVE");
assert!(parsed.is_active());
assert_eq!(
parsed.base_image_arn,
"arn:aws:lambda:us-east-1:aws:microvm-image:al2023-1"
);
assert_eq!(parsed.base_image_version.as_deref(), Some("1.0"));
assert!(
parsed
.code_artifact
.uri
.ends_with("coding-agents-on-bedrock.zip")
);
let resources = parsed
.resources
.as_ref()
.expect("the readback carries the resources");
assert_eq!(resources.len(), 1);
assert_eq!(resources[0].minimum_memory_in_mib, 2048);
let egress = parsed
.egress_network_connectors
.as_ref()
.expect("one connector");
assert_eq!(egress.len(), 1);
assert!(egress.len() <= crate::constants::MAX_IMAGE_EGRESS_CONNECTORS);
assert!(
egress.len() < crate::constants::MAX_NETWORK_CONNECTORS,
"the two ceilings are different numbers; see the constants test that holds the \
inequality"
);
assert_eq!(parsed.state_reason, None, "a successful version has none");
assert_eq!(parsed.updated_at, Some(1_755_150_629.117));
assert_eq!(parsed.describe(), "1.0 SUCCESSFUL / ACTIVE");
}
#[test]
fn a_version_summary_missing_a_required_member_fails_to_deserialise() {
let members = [
(
"baseImageArn",
r#""arn:aws:lambda:us-east-1:aws:microvm-image:al2023-1""#,
),
("buildRoleArn", r#""arn:aws:iam::1:role/build""#),
("codeArtifact", r#"{"uri": "s3://b/k"}"#),
(
"imageArn",
r#""arn:aws:lambda:us-east-1:1:microvm-image:i""#,
),
("imageVersion", r#""1.0""#),
("state", r#""SUCCESSFUL""#),
("status", r#""ACTIVE""#),
("createdAt", "1754524800"),
];
for omitted in members.iter().map(|(name, _)| *name) {
let body: Vec<String> = members
.iter()
.filter(|(name, _)| *name != omitted)
.map(|(name, value)| format!(r#""{name}": {value}"#))
.collect();
let error = serde_json::from_str::<MicrovmImageVersionSummaryWire>(&format!(
"{{{}}}",
body.join(", ")
))
.expect_err(&format!("{omitted} is required by the model"));
assert!(error.to_string().contains(omitted), "{omitted}: {error}");
}
}
#[test]
fn a_versions_hooks_block_is_not_read_because_a_real_one_omits_members() {
let json = r#"{
"baseImageArn": "arn:aws:lambda:us-east-1:aws:microvm-image:al2023-1",
"baseImageVersion": "0.0",
"buildRoleArn": "arn:aws:iam::392583147479:role/bonk-sandbox-microvm-build",
"codeArtifact": {"uri": "s3://bucket/omnigent-host-overlay.zip"},
"resources": [{"minimumMemoryInMiB": 2048}],
"hooks": {
"port": 9000,
"microvmHooks": {"run": "ENABLED", "runTimeoutInSeconds": 30},
"microvmImageHooks": {"ready": "ENABLED", "readyTimeoutInSeconds": 120}
},
"imageArn": "arn:aws:lambda:us-east-1:392583147479:microvm-image:omnigent-host-vpc",
"imageVersion": "3.0",
"state": "SUCCESSFUL",
"status": "ACTIVE",
"createdAt": 1752205430.83
}"#;
let parsed: MicrovmImageVersionSummaryWire = serde_json::from_str(json)
.expect("a sparse hooks block parses precisely because nothing reads it");
assert_eq!(parsed.image_version, "3.0");
let refused = serde_json::from_str::<Hooks>(
r#"{"port": 9000,
"microvmHooks": {"run": "ENABLED", "runTimeoutInSeconds": 30},
"microvmImageHooks": {"ready": "ENABLED", "readyTimeoutInSeconds": 120}}"#,
)
.expect_err("the request-side Hooks declares all six enabled flags as required");
assert!(refused.to_string().contains("resume"), "{refused}");
}
#[test]
fn an_inactive_version_reads_back_as_not_launchable() {
let json = r#"{
"baseImageArn": "arn:aws:lambda:us-east-1:aws:microvm-image:al2023-1",
"buildRoleArn": "arn:aws:iam::1:role/build",
"codeArtifact": {"uri": "s3://b/k"},
"imageArn": "arn:aws:lambda:us-east-1:1:microvm-image:img",
"imageVersion": "2.0",
"state": "SUCCESSFUL",
"status": "INACTIVE",
"createdAt": 1754524800,
"updatedAt": 1754528400
}"#;
let parsed: MicrovmImageVersionSummaryWire = serde_json::from_str(json).expect("parses");
assert!(
!parsed.is_active(),
"INACTIVE is the version RunMicrovm refuses to launch"
);
assert_eq!(parsed.describe(), "2.0 SUCCESSFUL / INACTIVE");
assert_eq!(
parsed.state, "SUCCESSFUL",
"the state and the status are different questions: a SUCCESSFUL build can be \
retired, which is the whole point of the status member"
);
}
#[test]
fn a_version_describes_itself_with_only_a_reason_it_has() {
let json = r#"{
"baseImageArn": "arn:aws:lambda:us-east-1:aws:microvm-image:al2023-1",
"buildRoleArn": "arn:aws:iam::1:role/build",
"codeArtifact": {"uri": "s3://b/k"},
"imageArn": "arn:aws:lambda:us-east-1:1:microvm-image:img",
"imageVersion": "2.0",
"state": "FAILED",
"status": "INACTIVE",
"createdAt": 1754524800,
"stateReason": "one or more builds failed"
}"#;
let parsed: MicrovmImageVersionSummaryWire = serde_json::from_str(json).expect("parses");
assert_eq!(
parsed.describe(),
"2.0 FAILED / INACTIVE (one or more builds failed)"
);
assert!(!parsed.describe().contains("()"), "{}", parsed.describe());
}
#[test]
fn the_update_version_request_is_one_member_spelled_as_the_model_spells_it() {
for (status, expected) in [
(VersionStatus::Active, "ACTIVE"),
(VersionStatus::Inactive, "INACTIVE"),
] {
let value =
serde_json::to_value(UpdateImageVersionWire { status }).expect("serialises");
let object = value.as_object().expect("an object");
assert_eq!(
object.keys().collect::<Vec<_>>(),
vec!["status"],
"imageIdentifier and imageVersion are uri-located: {value}"
);
assert_eq!(object["status"], expected);
assert_eq!(status.as_str(), expected);
assert_eq!(status.to_string(), expected);
}
}
#[test]
fn the_typed_statuses_are_exactly_the_published_ones() {
let typed: Vec<&str> = [VersionStatus::Active, VersionStatus::Inactive]
.iter()
.map(|status| status.as_str())
.collect();
assert_eq!(typed, crate::constants::IMAGE_VERSION_STATUSES);
}
#[test]
fn a_managed_version_listing_carries_bare_integer_versions_newest_first() {
let json = r#"{
"items": [
{
"imageArn": "arn:aws:lambda:us-east-1:aws:microvm-image:al2023-1",
"imageVersion": "1",
"createdAt": 1753119464.231,
"updatedAt": 1753119644.78
},
{
"imageArn": "arn:aws:lambda:us-east-1:aws:microvm-image:al2023-1",
"imageVersion": "0",
"createdAt": 1750180266.531,
"updatedAt": 1750180537.679
}
]
}"#;
let parsed: ListManagedVersionsResponseWire =
serde_json::from_str(json).expect("the model's own spelling parses");
assert_eq!(parsed.items.len(), 2);
assert_eq!(parsed.items[0].image_version, "1");
assert_eq!(parsed.items[1].image_version, "0");
assert_eq!(parsed.next_token, None, "one page, so the loop stops");
assert_eq!(
parsed.items[0].image_arn,
"arn:aws:lambda:us-east-1:aws:microvm-image:al2023-1"
);
assert_eq!(parsed.items[0].updated_at, Some(1_753_119_644.78));
for item in &parsed.items {
assert!(
!item.image_version.contains('.'),
"a managed base's versions are bare integers: {}",
item.image_version
);
}
}
#[test]
fn a_managed_image_summary_carries_no_registry_ref_so_it_cannot_build_a_base_image() {
let json = r#"{
"items": [{
"imageArn": "arn:aws:lambda:us-east-1:aws:microvm-image:al2023-1",
"createdAt": 1750180266.531,
"updatedAt": 1753119644.78
}]
}"#;
let parsed: ListManagedImagesResponseWire =
serde_json::from_str(json).expect("the model's own spelling parses");
assert_eq!(parsed.items.len(), 1, "measured 2026-08-16: exactly one");
assert_eq!(
parsed.items[0].image_arn,
"arn:aws:lambda:us-east-1:aws:microvm-image:al2023-1"
);
let base = crate::control::BaseImage::al2023();
assert_eq!(
base.docker_ref, "public.ecr.aws/amazonlinux/amazonlinux:2023-minimal",
"the pairing a discovered base cannot supply"
);
assert!(
!parsed.items[0].image_arn.contains(&base.docker_ref),
"the ARN does not carry the registry ref, so discovery cannot derive it"
);
let error = serde_json::from_str::<ManagedMicrovmImageSummaryWire>(
r#"{"createdAt": 1750180266.531}"#,
)
.expect_err("imageArn is required");
assert!(error.to_string().contains("imageArn"), "{error}");
}
#[test]
fn a_pinned_base_image_version_reaches_the_wire_and_an_unpinned_one_is_omitted() {
let mut wire = minimal_create_wire();
wire.base_image_version = Some("1".to_string());
let value = serde_json::to_value(&wire).expect("serialises");
assert_eq!(value["baseImageVersion"], "1");
let mut unpinned = minimal_create_wire();
unpinned.base_image_version = None;
let value = serde_json::to_value(&unpinned).expect("serialises");
assert!(
!value
.as_object()
.expect("object")
.contains_key("baseImageVersion"),
"omitted, not null: an absent member takes the service default, and a blank one \
is a ValidationException: {value}"
);
}
#[test]
fn a_pinned_launch_version_reaches_the_wire_and_an_unpinned_one_is_omitted() {
let mut wire = minimal_run_wire();
wire.image_version = Some("2.0".to_string());
let value = serde_json::to_value(&wire).expect("serialises");
assert_eq!(value["imageVersion"], "2.0");
assert_eq!(
value["imageIdentifier"], "arn:image",
"the version does not replace the identifier; both are sent"
);
let value = serde_json::to_value(minimal_run_wire()).expect("serialises");
assert!(
!value
.as_object()
.expect("object")
.contains_key("imageVersion"),
"an unpinned launch must emit byte-for-byte what this client always sent: {value}"
);
}
fn minimal_run_wire() -> RunMicrovmWire {
RunMicrovmWire {
image_identifier: "arn:image".to_string(),
image_version: None,
execution_role_arn: None,
ingress_network_connectors: Vec::new(),
egress_network_connectors: None,
idle_policy: IdlePolicy {
max_idle_duration_seconds: 600,
suspended_duration_seconds: 600,
auto_resume_enabled: false,
},
maximum_duration_in_seconds: 3_600,
run_hook_payload: String::new(),
client_token: "run-arn-0011223344556677".to_string(),
}
}
fn minimal_create_wire() -> CreateMicrovmImageWire {
CreateMicrovmImageWire {
name: "img".to_string(),
base_image_arn: "arn:base".to_string(),
base_image_version: None,
build_role_arn: "arn:role".to_string(),
code_artifact: CodeArtifact {
uri: "s3://b/k".to_string(),
},
cpu_configurations: vec![CpuConfiguration {
architecture: "ARM_64".to_string(),
}],
resources: vec![Resources {
minimum_memory_in_mib: 2048,
}],
hooks: Hooks {
port: 9000,
microvm_hooks: MicrovmHooks {
run: HookState::Enabled,
run_timeout_in_seconds: 30,
resume: HookState::Enabled,
resume_timeout_in_seconds: 30,
suspend: HookState::Enabled,
suspend_timeout_in_seconds: 30,
terminate: HookState::Enabled,
terminate_timeout_in_seconds: 30,
},
microvm_image_hooks: MicrovmImageHooks {
ready: HookState::Enabled,
ready_timeout_in_seconds: 300,
validate: HookState::Enabled,
validate_timeout_in_seconds: 300,
},
},
additional_os_capabilities: None,
tags: None,
client_token: "create-img-0011223344556677".to_string(),
}
}
}