use microsandbox_protocol::{
control::*,
wire::{Envelope, WireError},
};
#[derive(Clone)]
pub struct ControlContext {
pub executor: std::sync::Arc<super::executor::RuntimeControlExecutor>,
}
pub(crate) trait Handler: Send + Sync + 'static {
fn handle(&self, request: ControlOperation, generation: u8) -> Response;
fn handle_json_with_memory(
&self,
value: serde_json::Value,
memory: Option<std::fs::File>,
) -> Vec<u8> {
if memory.is_some() {
return b"{\"ok\":false,\"error\":\"unexpected control descriptor\"}\n".to_vec();
}
self.handle_json(value)
}
fn handle_json(&self, value: serde_json::Value) -> Vec<u8> {
let response = match serde_json::from_value(value) {
Ok(request) => {
self.handle(ControlOperation::GenerationOne(request), 1)
.json
}
Err(_) => JsonControlResponse {
ok: false,
error: Some("invalid control request".into()),
..Default::default()
},
};
let mut bytes = serde_json::to_vec(&response).unwrap_or_default();
bytes.push(b'\n');
bytes
}
}
pub(crate) struct Response {
pub json: JsonControlResponse,
pub framed: Reply,
}
pub(crate) enum Reply {
Capabilities(Capabilities),
RuntimeCapabilities(RuntimeCapabilities),
Memory(MemoryState),
Cpu(CpuState),
Secrets(SecretsResult),
Checkpoint(CheckpointResult),
DiskCheckpoint(DiskCheckpointState),
Branch(BranchResult),
Pause(PauseState),
RootDisk(RootDiskState),
DiskCompact(DiskCompactionResult),
Error(ControlError),
}
impl Response {
pub(crate) fn error(error: ControlError, legacy: impl Into<String>) -> Self {
Self {
json: JsonControlResponse {
ok: false,
error: Some(legacy.into()),
..Default::default()
},
framed: Reply::Error(error),
}
}
}
impl Reply {
pub(crate) fn envelope(&self, generation: u8) -> Result<Envelope, WireError> {
match self {
Self::Capabilities(value) => {
Envelope::new(generation, "control.capabilities.result", value)
}
Self::RuntimeCapabilities(value) => {
Envelope::new(generation, "control.capabilities.result", value)
}
Self::Memory(value) => Envelope::new(generation, "control.memory.state", value),
Self::Cpu(value) => Envelope::new(generation, "control.cpu.state", value),
Self::Secrets(value) => Envelope::new(generation, "control.secrets.result", value),
Self::Checkpoint(value) => {
Envelope::new(generation, "control.checkpoint.result", value)
}
Self::DiskCheckpoint(value) => {
Envelope::new(generation, "control.disk.checkpoint.result", value)
}
Self::Branch(value) => Envelope::new(generation, "control.branch.result", value),
Self::Pause(value) => Envelope::new(generation, "control.pause.state", value),
Self::RootDisk(value) => Envelope::new(generation, "control.root-disk.state", value),
Self::DiskCompact(value) => {
Envelope::new(generation, "control.disk.compact.result", value)
}
Self::Error(value) => Envelope::new(generation, "control.error", value),
}
}
}
impl Handler for ControlContext {
fn handle_json_with_memory(
&self,
value: serde_json::Value,
memory: Option<std::fs::File>,
) -> Vec<u8> {
super::legacy::respond_with_memory(&value.to_string(), self, memory)
}
fn handle_json(&self, value: serde_json::Value) -> Vec<u8> {
super::legacy::respond_to_line(&value.to_string(), self)
}
fn handle(&self, request: ControlOperation, generation: u8) -> Response {
let mutation = !matches!(
request,
ControlOperation::GenerationOne(
ControlRequest::Capabilities
| ControlRequest::MemoryState
| ControlRequest::CpuState
) | ControlOperation::PauseState
);
let command = match operation_to_legacy(request) {
Ok(command) => command,
Err(error) => return Response::error(error, "invalid control request payload"),
};
let result = self.executor.execute_legacy(command);
let error = || ControlError {
code: result
.error_code
.clone()
.unwrap_or_else(|| "operation_failed".into()),
message: result
.error
.clone()
.unwrap_or_else(|| "control operation failed".into()),
effect: if mutation {
ErrorEffect::Unknown
} else {
ErrorEffect::None
},
};
let framed = if let Some(secrets) = result.secret_result.clone() {
Reply::Secrets(secrets)
} else if let Some(checkpoint) = result.checkpoint.clone() {
Reply::Checkpoint(CheckpointResult {
checkpoint: Some(CheckpointState {
checkpoint_id: checkpoint.checkpoint_id,
checkpoint_root: checkpoint.checkpoint_root,
path: checkpoint.path,
memory_mode: checkpoint.memory_mode,
memory_logical_bytes: checkpoint.memory_logical_bytes,
memory_emitted_bytes: checkpoint.memory_emitted_bytes,
}),
recovery_error: (!result.ok).then(|| result.error.clone()).flatten(),
})
} else if !result.ok {
Reply::Error(error())
} else if let Some(caps) = result.capabilities {
let capabilities = RuntimeCapabilities {
root_disk_grow: caps.root_disk_grow,
guest_flush_policy: caps.guest_flush_policy,
optional_disk_integrity: caps.optional_disk_integrity,
branch_create: caps.branch_create,
branch_memfd: caps.branch_memfd,
pause_resume: caps.pause_resume,
disk_compact: caps.disk_compact,
disk_compact_owned: caps.disk_compact_owned,
cpu_resize: caps.cpu_resize,
memory_resize: caps.memory_resize,
secrets_update: caps.secrets_update,
checkpoint_create: caps.checkpoint_create,
disk_checkpoint_create: caps.disk_checkpoint_create,
};
if generation >= 2 {
Reply::RuntimeCapabilities(capabilities)
} else {
Reply::Capabilities(capabilities.generation_one())
}
} else if let Some(state) = result.memory {
Reply::Memory(MemoryState {
boot_mib: state.boot_mib,
target_mib: state.target_mib,
current_mib: state.current_mib,
max_mib: state.max_mib,
})
} else if let Some(state) = result.cpu {
Reply::Cpu(CpuState {
possible: state.possible,
requested_online: state.requested_online,
actual_online: state.actual_online,
enforced: state.enforced,
})
} else if let Some(state) = result.disk_checkpoint {
Reply::DiskCheckpoint(DiskCheckpointState {
checkpoint_id: state.checkpoint_id,
path: state.path,
disk: state.disk,
owned_volumes: state.owned_volumes,
})
} else if let Some(path) = result.branch {
Reply::Branch(BranchResult { path })
} else if let Some(state) = result.pause {
Reply::Pause(PauseState {
paused: state.paused,
recovery_required: state.recovery_required,
capture_unavailable: state.capture_unavailable,
})
} else if let Some(state) = result.root_disk {
Reply::RootDisk(RootDiskState {
filesystem_bytes: state.filesystem_bytes,
device_bytes: state.device_bytes,
total_us: state.total_us,
pause_us: state.pause_us,
guest_us: state.guest_us,
})
} else if let Some(result) = result.compaction {
Reply::DiskCompact(result)
} else {
Reply::Error(ControlError::rejected(
"invalid_response",
"runtime omitted control result",
))
};
Response {
json: JsonControlResponse::default(),
framed,
}
}
}
fn operation_to_legacy(
operation: ControlOperation,
) -> Result<crate::control::ControlRequest, ControlError> {
use crate::control::{
CheckpointCaptureIntent as LegacyIntent, ControlRequest as Legacy,
SecretLiveChange as LegacySecret, SecretValue as LegacyValue,
};
Ok(match operation {
ControlOperation::GenerationOne(request) => match request {
ControlRequest::Capabilities => Legacy::Capabilities,
ControlRequest::MemoryTarget { total_mib } => Legacy::MemoryTarget { total_mib },
ControlRequest::MemoryState => Legacy::MemoryState,
ControlRequest::CpuTarget { online } => Legacy::CpuTarget { online },
ControlRequest::CpuState => Legacy::CpuState,
ControlRequest::SecretsUpdate { changes } => Legacy::SecretsUpdate {
changes: changes
.into_iter()
.map(|change| match change {
SecretChange::Rotate { name, value } => LegacySecret::Rotate {
name,
value: LegacyValue(value.0.clone()),
},
SecretChange::Remove { name } => LegacySecret::Remove { name },
SecretChange::SetAllowedHosts { name, hosts } => {
LegacySecret::SetAllowedHosts { name, hosts }
}
})
.collect(),
},
},
ControlOperation::CheckpointCreate(request) => Legacy::CheckpointCreate {
guest_flush: request.guest_flush,
record_integrity: request.record_integrity,
checkpoint_id: request.checkpoint_id,
intent: match request.intent {
CheckpointCaptureIntent::FullSnapshot => LegacyIntent::FullSnapshot,
CheckpointCaptureIntent::Park => LegacyIntent::Park,
CheckpointCaptureIntent::TransparentTransfer => LegacyIntent::TransparentTransfer,
},
},
ControlOperation::DiskCheckpointCreate(request) => Legacy::DiskCheckpointCreate {
guest_flush: request.guest_flush,
checkpoint_id: request.checkpoint_id,
},
ControlOperation::BranchCreate(request) => Legacy::BranchCreate {
guest_flush: request.guest_flush,
record_integrity: request.record_integrity,
branch_id: request.branch_id,
child_name: request.child_name,
memory_cache_dir: request.memory_cache_dir,
},
ControlOperation::Pause(request) => match request.guest_flush {
Some(guest_flush) => Legacy::PauseWithGuestFlush { guest_flush },
None => Legacy::Pause,
},
ControlOperation::Resume => Legacy::Resume,
ControlOperation::PauseState => Legacy::PauseState,
ControlOperation::RootDiskGrow(request) => Legacy::RootDiskGrow {
size_bytes: request.size_bytes,
},
ControlOperation::DiskCompact(request) => Legacy::DiskCompact {
target: request.target,
layers: request
.layers
.map(usize::try_from)
.transpose()
.map_err(|_| {
ControlError::rejected("invalid_request", "layer count exceeds the host range")
})?,
dry_run: request.dry_run,
},
})
}
#[cfg(feature = "net")]
pub(super) fn apply_secret_changes(
secrets: Option<µsandbox_network::secrets::handle::SecretsHandle>,
changes: Vec<SecretChange>,
) -> Response {
use microsandbox_network::secrets::handle::SecretsUpdateError;
let Some(secrets) = secrets else {
return Response::error(
ControlError::rejected("unsupported_operation", "secret updates are unavailable"),
"live secret reconfiguration is not available for this sandbox",
);
};
let mut applied_count = 0u32;
for change in changes {
let result = match change {
SecretChange::Rotate { name, value } => secrets.rotate_value(&name, value.0.clone()),
SecretChange::Remove { name } => {
secrets.remove(&name);
Ok(())
}
SecretChange::SetAllowedHosts { name, hosts } => {
secrets.set_allowed_hosts(&name, &hosts)
}
};
if let Err(error) = result {
let structured = match &error {
SecretsUpdateError::UnknownSecret { .. } => {
ControlError::rejected("unknown_secret", "the secret does not exist")
}
SecretsUpdateError::MissingAllowedHosts { .. } => ControlError::rejected(
"invalid_secret_hosts",
"at least one allowed host is required",
),
};
return Response {
json: JsonControlResponse {
ok: false,
error: Some(error.to_string()),
..Default::default()
},
framed: Reply::Secrets(SecretsResult::Failed {
applied_count,
failed_index: applied_count,
error: structured,
}),
};
}
applied_count = applied_count.saturating_add(1);
}
Response {
json: JsonControlResponse {
ok: true,
..Default::default()
},
framed: Reply::Secrets(SecretsResult::Complete { applied_count }),
}
}