pub type MicrosandboxResult<T> = Result<T, MicrosandboxError>;
#[derive(Debug, thiserror::Error)]
pub enum MicrosandboxError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("http error: {0}")]
Http(#[from] reqwest::Error),
#[error("cloud HTTP {status}: {message}")]
CloudHttp {
status: u16,
code: Option<String>,
message: String,
},
#[error("libkrunfw not found: {0}")]
LibkrunfwNotFound(String),
#[error("database error: {0}")]
Database(#[from] sea_orm::DbErr),
#[error("invalid config: {0}")]
InvalidConfig(String),
#[error("sandbox not found: {0}")]
SandboxNotFound(String),
#[error("sandbox already exists: {0}")]
SandboxAlreadyExists(String),
#[error("sandbox still running: {0}")]
SandboxStillRunning(String),
#[error("sandbox {0}")]
SandboxNotRunning(String),
#[error("runtime error: {0}")]
Runtime(String),
#[error("failed to start {name:?}: {}", .err.message)]
BootStart {
name: String,
err: microsandbox_runtime::boot_error::BootError,
},
#[error("json error: {0}")]
Json(#[from] serde_json::Error),
#[error("protocol error: {0}")]
Protocol(#[from] microsandbox_protocol::ProtocolError),
#[error("agent client error: {0}")]
AgentClient(#[from] crate::agent::AgentClientError),
#[cfg(unix)]
#[error("nix error: {0}")]
Nix(#[from] nix::errno::Errno),
#[cfg(windows)]
#[error("{0}")]
WindowsHostSetup(#[from] crate::setup::WindowsHostSetupError),
#[error("exec timed out after {0:?}")]
ExecTimeout(std::time::Duration),
#[error("exec failed: {}", .0.message)]
ExecFailed(microsandbox_protocol::exec::ExecFailed),
#[error("terminal error: {0}")]
Terminal(String),
#[error("sandbox fs error: {0}")]
SandboxFsOps(String),
#[error("image not found: {0}")]
ImageNotFound(String),
#[error("image in use by sandbox(es): {0}")]
ImageInUse(String),
#[error("volume not found: {0}")]
VolumeNotFound(String),
#[error("volume already exists: {0}")]
VolumeAlreadyExists(String),
#[error("image error: {0}")]
Image(#[from] microsandbox_image::ImageError),
#[cfg(feature = "net")]
#[error("network builder: {0}")]
NetworkBuilder(#[from] microsandbox_network::policy::BuildError),
#[error("patch failed: {0}")]
PatchFailed(String),
#[error("snapshot not found: {0}")]
SnapshotNotFound(String),
#[error("snapshot already exists: {0}")]
SnapshotAlreadyExists(String),
#[error("snapshot source sandbox '{0}' is not stopped")]
SnapshotSandboxRunning(String),
#[error("snapshot image missing from cache: {0}")]
SnapshotImageMissing(String),
#[error("snapshot integrity check failed: {0}")]
SnapshotIntegrity(String),
#[error("snapshot artifact migration failed for {artifact} during {phase}: {code}: {detail}")]
SnapshotMigration {
code: String,
phase: String,
artifact: String,
detail: String,
},
#[error("metrics disabled for sandbox: {0}")]
MetricsDisabled(String),
#[error("metrics unavailable for sandbox: {0}")]
MetricsUnavailable(String),
#[error("log stream missed rotation (dropped from offset {dropped_from_offset})")]
MissedRotation {
dropped_from_offset: u64,
},
#[error("invalid cursor: {0}")]
InvalidCursor(String),
#[error("{} is not supported by this backend: {}", .op.api_path(), .reason.hint())]
Unsupported {
op: Operation,
reason: UnsupportedReason,
},
#[error("{0}")]
Custom(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Operation {
SandboxCreate,
SandboxStart,
SandboxStop,
SandboxRemove,
SandboxRemovePersisted,
SandboxKill,
SandboxDrain,
SandboxPing,
SandboxTouch,
SandboxStopAndWait,
SandboxWait,
SandboxLogs,
SandboxLogStream,
SandboxLogStreamNoFollow,
SandboxLogStreamFollow,
SandboxLogger,
SandboxMetrics,
SandboxMetricsStream,
SandboxModify,
SandboxFs,
AllSandboxMetrics,
AgentConnect,
SandboxHandleConfig,
SandboxHandleConnect,
SandboxHandleMetrics,
SandboxHandleRemove,
SandboxHandleSnapshot,
SandboxHandleSnapshotTo,
SandboxFsOpenFile,
SandboxFsOpenDir,
SandboxFsCloseHandle,
SandboxFsReadHandle,
SandboxFsReadHandleStream,
SandboxFsWriteHandle,
SandboxFsWriteHandleStream,
SandboxFsReadDirHandle,
SandboxFsStatHandle,
SandboxFsSetStatHandle,
SandboxSshServer,
SshServerServe,
VolumeCreate,
VolumeGet,
VolumeList,
VolumeRemove,
VolumePath,
VolumeFsRead,
VolumeFsReadToString,
VolumeFsWrite,
VolumeFsList,
VolumeFsStat,
VolumeFsMkdir,
VolumeFsRemove,
VolumeFsCopy,
VolumeFsRename,
VolumeFsExists,
VolumeFsReadStream,
VolumeFsWriteStream,
ImageGet,
ImageList,
ImageInspect,
ImageRemove,
ImagePrune,
ImageLoad,
ImageSave,
SnapshotOps,
Config,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum UnsupportedReason {
LocalOnly,
CloudOnly,
UseInstead(Operation),
RequiresUnixHost,
RequiresCrateFeature(&'static str),
ConfigField(&'static str),
MountIntoSandbox,
NotAvailable(String),
}
impl Operation {
pub fn api_path(&self) -> &'static str {
match self {
Operation::SandboxCreate => "Sandbox::create",
Operation::SandboxStart => "Sandbox::start",
Operation::SandboxStop => "Sandbox::stop",
Operation::SandboxRemove => "Sandbox::remove",
Operation::SandboxRemovePersisted => "Sandbox::remove_persisted",
Operation::SandboxKill => "Sandbox::kill",
Operation::SandboxDrain => "Sandbox::drain",
Operation::SandboxPing => "Sandbox::ping",
Operation::SandboxTouch => "Sandbox::touch",
Operation::SandboxStopAndWait => "Sandbox::stop_and_wait",
Operation::SandboxWait => "Sandbox::wait",
Operation::SandboxLogs => "Sandbox::logs",
Operation::SandboxLogStream => "Sandbox::log_stream",
Operation::SandboxLogStreamNoFollow => "Sandbox::log_stream(follow=false)",
Operation::SandboxLogStreamFollow => "Sandbox::log_stream(follow=true)",
Operation::SandboxLogger => "Sandbox::logger",
Operation::SandboxMetrics => "Sandbox::metrics",
Operation::SandboxMetricsStream => "Sandbox::metrics_stream",
Operation::SandboxModify => "Sandbox::modify",
Operation::SandboxFs => "Sandbox::fs",
Operation::AllSandboxMetrics => "all_sandbox_metrics",
Operation::AgentConnect => "agent connections",
Operation::SandboxHandleConfig => "SandboxHandle::config",
Operation::SandboxHandleConnect => "SandboxHandle::connect",
Operation::SandboxHandleMetrics => "SandboxHandle::metrics",
Operation::SandboxHandleRemove => "SandboxHandle::remove",
Operation::SandboxHandleSnapshot => "SandboxHandle::snapshot",
Operation::SandboxHandleSnapshotTo => "SandboxHandle::snapshot_to",
Operation::SandboxFsOpenFile => "SandboxFsOps::open_file",
Operation::SandboxFsOpenDir => "SandboxFsOps::open_dir",
Operation::SandboxFsCloseHandle => "SandboxFsOps::close_handle",
Operation::SandboxFsReadHandle => "SandboxFsOps::read_handle",
Operation::SandboxFsReadHandleStream => "SandboxFsOps::read_handle_stream",
Operation::SandboxFsWriteHandle => "SandboxFsOps::write_handle",
Operation::SandboxFsWriteHandleStream => "SandboxFsOps::write_handle_stream",
Operation::SandboxFsReadDirHandle => "SandboxFsOps::read_dir_handle",
Operation::SandboxFsStatHandle => "SandboxFsOps::stat_handle",
Operation::SandboxFsSetStatHandle => "SandboxFsOps::set_stat_handle",
Operation::SandboxSshServer => "SandboxSshOps::server",
Operation::SshServerServe => "SshServer::serve",
Operation::VolumeCreate => "Volume::create",
Operation::VolumeGet => "Volume::get",
Operation::VolumeList => "Volume::list",
Operation::VolumeRemove => "Volume::remove",
Operation::VolumePath => "Volume::path",
Operation::VolumeFsRead => "VolumeFs::read",
Operation::VolumeFsReadToString => "VolumeFs::read_to_string",
Operation::VolumeFsWrite => "VolumeFs::write",
Operation::VolumeFsList => "VolumeFs::list",
Operation::VolumeFsStat => "VolumeFs::stat",
Operation::VolumeFsMkdir => "VolumeFs::mkdir",
Operation::VolumeFsRemove => "VolumeFs::remove",
Operation::VolumeFsCopy => "VolumeFs::copy",
Operation::VolumeFsRename => "VolumeFs::rename",
Operation::VolumeFsExists => "VolumeFs::exists",
Operation::VolumeFsReadStream => "VolumeFs::read_stream",
Operation::VolumeFsWriteStream => "VolumeFs::write_stream",
Operation::ImageGet => "Image::get",
Operation::ImageList => "Image::list",
Operation::ImageInspect => "Image::inspect",
Operation::ImageRemove => "Image::remove",
Operation::ImagePrune => "Image::prune",
Operation::ImageLoad => "Image::load",
Operation::ImageSave => "Image::save",
Operation::SnapshotOps => "snapshot operations",
Operation::Config => "config",
}
}
}
impl UnsupportedReason {
pub fn hint(&self) -> String {
match self {
UnsupportedReason::LocalOnly => "use a local backend".to_string(),
UnsupportedReason::CloudOnly => "use a cloud backend".to_string(),
UnsupportedReason::UseInstead(op) => format!("use {}", op.api_path()),
UnsupportedReason::RequiresUnixHost => "unix hosts only".to_string(),
UnsupportedReason::RequiresCrateFeature(feature) => {
format!("enable the {feature} feature")
}
UnsupportedReason::ConfigField(field) => {
format!("the {field} option is not accepted here")
}
UnsupportedReason::MountIntoSandbox => "mount the volume into a sandbox".to_string(),
UnsupportedReason::NotAvailable(reason) => reason.clone(),
}
}
}
impl MicrosandboxError {
pub fn unsupported(op: Operation, reason: UnsupportedReason) -> MicrosandboxError {
MicrosandboxError::Unsupported { op, reason }
}
pub fn local_only(op: Operation) -> MicrosandboxError {
Self::unsupported(op, UnsupportedReason::LocalOnly)
}
pub fn cloud_only(op: Operation) -> MicrosandboxError {
Self::unsupported(op, UnsupportedReason::CloudOnly)
}
}
impl From<microsandbox_types::TypesError> for MicrosandboxError {
fn from(value: microsandbox_types::TypesError) -> Self {
match value {
microsandbox_types::TypesError::InvalidConfig(message) => Self::InvalidConfig(message),
}
}
}
impl microsandbox_db::retry::IsSqliteBusy for MicrosandboxError {
fn is_sqlite_busy(&self) -> bool {
matches!(self, MicrosandboxError::Database(db_err) if microsandbox_db::retry::is_sqlite_busy(db_err))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unsupported_renders_operation_and_reason() {
let err = MicrosandboxError::unsupported(
Operation::SandboxKill,
UnsupportedReason::UseInstead(Operation::SandboxStop),
);
assert_eq!(
err.to_string(),
"Sandbox::kill is not supported by this backend: use Sandbox::stop"
);
let err = MicrosandboxError::local_only(Operation::ImagePrune);
assert_eq!(
err.to_string(),
"Image::prune is not supported by this backend: use a local backend"
);
let err = MicrosandboxError::unsupported(
Operation::SandboxCreate,
UnsupportedReason::ConfigField("ca_certs"),
);
assert_eq!(
err.to_string(),
"Sandbox::create is not supported by this backend: the ca_certs option is not accepted here"
);
}
}