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: ControlRequest) -> 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(request).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),
Memory(MemoryState),
Cpu(CpuState),
Secrets(SecretsResult),
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::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::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: ControlRequest) -> Response {
let wire = serde_json::to_value(&request).expect("control request serializes");
let command = serde_json::from_value(wire).expect("shared control operation");
let result = self.executor.execute_legacy(command);
let json = serde_json::from_value(
serde_json::to_value(&result).expect("control response serializes"),
)
.expect("shared control response");
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 matches!(
request,
ControlRequest::MemoryTarget { .. } | ControlRequest::CpuTarget { .. }
) {
ErrorEffect::Unknown
} else {
ErrorEffect::None
},
};
let framed = if let Some(secrets) = result.secret_result {
Reply::Secrets(secrets)
} else if !result.ok {
Reply::Error(error())
} else if let Some(caps) = result.capabilities {
Reply::Capabilities(Capabilities {
root_disk_grow: caps.root_disk_grow,
cpu_resize: caps.cpu_resize,
memory_resize: caps.memory_resize,
secrets_update: caps.secrets_update,
})
} 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 {
Reply::Error(ControlError::rejected(
"invalid_response",
"runtime omitted control result",
))
};
Response { json, framed }
}
}
#[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 }),
}
}