#[derive(Debug, thiserror::Error)]
pub enum RightsizeError {
#[error("{}", format_unsupported(feature, backend, remedy))]
UnsupportedByBackend {
feature: String,
backend: String,
remedy: Option<String>,
},
#[error("{message}")]
PortBindConflict {
message: String,
#[source]
source: Option<Box<RightsizeError>>,
},
#[error("{0}")]
ContainerLaunch(String),
#[error("{0}")]
Backend(String),
#[error("{message}")]
NameConflict {
message: String,
#[source]
source: Option<Box<RightsizeError>>,
},
#[error(
"Container reuse cannot be combined with a custom network ('{network_id}') — reuse \
identity does not cover network topology; drop either .reuse(true) or \
.with_network(...)"
)]
ReuseNetworkConflict {
network_id: String,
},
#[error(
"Container reuse cannot be combined with Container::from_checkpoint(...) — reuse \
identity does not cover a checkpoint reference; drop either .reuse(true) or start from \
an ordinary image instead"
)]
ReuseCheckpointConflict,
#[error("{}", format_isolation_required(backend))]
IsolationRequired {
backend: String,
},
#[error("{}", format_checkpoint_unsupported(backend))]
CheckpointUnsupported {
backend: String,
},
#[error(
"{}",
format_checkpoint_backend_mismatch(active_backend, checkpoint_backend)
)]
CheckpointBackendMismatch {
active_backend: String,
checkpoint_backend: String,
},
#[error("checkpoint name '{name}' is invalid — names must match ^[a-z0-9][a-z0-9-]{{0,40}}$")]
InvalidCheckpointName {
name: String,
},
#[error("{}", format_checkpoint_artifact_missing(checkpoint_ref, backend))]
CheckpointArtifactMissing {
checkpoint_ref: String,
backend: String,
},
#[error("{}", format_malformed_archive(path, reason))]
MalformedArchive {
path: std::path::PathBuf,
reason: String,
},
#[error("{0}")]
Provision(String),
#[error(transparent)]
Io(#[from] std::io::Error),
}
fn format_unsupported(feature: &str, backend: &str, remedy: &Option<String>) -> String {
let base = format!("Feature '{feature}' is not supported by the '{backend}' backend");
match remedy {
Some(r) => format!("{base} — {r}"),
None => base,
}
}
fn format_isolation_required(backend: &str) -> String {
format!(
"Hardware isolation was required (.require_isolation(true)) but the active '{backend}' \
backend does not provide it — set RIGHTSIZE_BACKEND=microsandbox to run on a hardware-isolated \
backend, or drop .require_isolation(true) if this workload does not need it"
)
}
fn format_checkpoint_unsupported(backend: &str) -> String {
format!(
"Checkpoint/restore was requested but the active '{backend}' backend does not support \
it — checkpointing needs a backend whose capabilities().checkpoint is true (see the \
checkpoints docs)"
)
}
fn format_checkpoint_backend_mismatch(active_backend: &str, checkpoint_backend: &str) -> String {
format!(
"the active backend is '{active_backend}', but this checkpoint was created by the \
'{checkpoint_backend}' backend — set RIGHTSIZE_BACKEND={checkpoint_backend} to use it, \
or take a fresh checkpoint under '{active_backend}' instead"
)
}
fn format_checkpoint_artifact_missing(checkpoint_ref: &str, backend: &str) -> String {
format!(
"checkpoint '{checkpoint_ref}' has no backend-native artifact left on the '{backend}' \
backend — it may already have been removed; there is nothing left to export"
)
}
fn format_malformed_archive(path: &std::path::Path, reason: &str) -> String {
format!(
"the checkpoint archive at '{}' is not a usable rightsize archive — {reason}",
path.display()
)
}
impl RightsizeError {
pub fn unsupported(feature: impl Into<String>, backend: impl Into<String>) -> Self {
Self::UnsupportedByBackend {
feature: feature.into(),
backend: backend.into(),
remedy: None,
}
}
pub fn unsupported_with_remedy(
feature: impl Into<String>,
backend: impl Into<String>,
remedy: impl Into<String>,
) -> Self {
Self::UnsupportedByBackend {
feature: feature.into(),
backend: backend.into(),
remedy: Some(remedy.into()),
}
}
}
pub type Result<T> = std::result::Result<T, RightsizeError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unsupported_without_remedy_renders_base_sentence_only() {
let e = RightsizeError::unsupported("network alias 'bad'", "microsandbox");
assert_eq!(
e.to_string(),
"Feature 'network alias 'bad'' is not supported by the 'microsandbox' backend"
);
}
#[test]
fn unsupported_with_remedy_appends_it_after_an_em_dash() {
let e = RightsizeError::unsupported_with_remedy(
"network links (no nc/busybox in consumer image 'X')",
"microsandbox",
"run this test with RIGHTSIZE_BACKEND=docker instead",
);
assert_eq!(
e.to_string(),
"Feature 'network links (no nc/busybox in consumer image 'X')' is not supported by \
the 'microsandbox' backend — run this test with RIGHTSIZE_BACKEND=docker instead"
);
}
#[test]
fn port_bind_conflict_displays_its_message() {
let e = RightsizeError::PortBindConflict {
message: "address already in use".into(),
source: None,
};
assert_eq!(e.to_string(), "address already in use");
}
#[test]
fn container_launch_and_backend_display_their_payload() {
let e = RightsizeError::ContainerLaunch("timed out waiting for port 6379".into());
assert_eq!(e.to_string(), "timed out waiting for port 6379");
let e = RightsizeError::Backend("500: already allocated".into());
assert_eq!(e.to_string(), "500: already allocated");
}
#[test]
fn name_conflict_displays_its_message() {
let e = RightsizeError::NameConflict {
message: "container name 'rz-reuse-abc' is already in use".into(),
source: None,
};
assert_eq!(
e.to_string(),
"container name 'rz-reuse-abc' is already in use"
);
}
#[test]
fn isolation_required_names_the_backend_and_the_remedy() {
let e = RightsizeError::IsolationRequired {
backend: "docker".to_string(),
};
let msg = e.to_string();
assert!(msg.contains("docker"), "{msg}");
assert!(msg.contains("RIGHTSIZE_BACKEND=microsandbox"), "{msg}");
assert!(msg.contains(".require_isolation(true)"), "{msg}");
}
#[test]
fn checkpoint_unsupported_names_the_backend_without_steering_to_a_specific_other_one() {
let e = RightsizeError::CheckpointUnsupported {
backend: "test-double".to_string(),
};
let msg = e.to_string();
assert!(msg.contains("test-double"), "{msg}");
assert!(msg.contains("capabilities().checkpoint"), "{msg}");
assert!(
!msg.contains("RIGHTSIZE_BACKEND=docker"),
"must not steer toward a specific backend — both real backends support it now: {msg}"
);
}
#[test]
fn checkpoint_backend_mismatch_names_both_backends_and_the_remedy() {
let e = RightsizeError::CheckpointBackendMismatch {
active_backend: "docker".to_string(),
checkpoint_backend: "microsandbox".to_string(),
};
let msg = e.to_string();
assert!(msg.contains("docker"), "{msg}");
assert!(msg.contains("microsandbox"), "{msg}");
assert!(msg.contains("RIGHTSIZE_BACKEND=microsandbox"), "{msg}");
}
#[test]
fn reuse_network_conflict_names_the_network_and_both_knobs() {
let e = RightsizeError::ReuseNetworkConflict {
network_id: "rz-net-1".to_string(),
};
let msg = e.to_string();
assert!(msg.contains("rz-net-1"), "{msg}");
assert!(msg.contains(".reuse(true)"), "{msg}");
assert!(msg.contains(".with_network(...)"), "{msg}");
}
#[test]
fn reuse_checkpoint_conflict_names_both_knobs() {
let e = RightsizeError::ReuseCheckpointConflict;
let msg = e.to_string();
assert!(msg.contains(".reuse(true)"), "{msg}");
assert!(msg.contains("from_checkpoint"), "{msg}");
}
#[test]
fn invalid_checkpoint_name_names_the_rejected_name_and_the_pattern() {
let e = RightsizeError::InvalidCheckpointName {
name: "Bad Name!".to_string(),
};
let msg = e.to_string();
assert!(msg.contains("Bad Name!"), "{msg}");
assert!(msg.contains("^[a-z0-9][a-z0-9-]{0,40}$"), "{msg}");
}
#[test]
fn checkpoint_backend_mismatch_message_is_call_site_neutral() {
let e = RightsizeError::CheckpointBackendMismatch {
active_backend: "docker".to_string(),
checkpoint_backend: "microsandbox".to_string(),
};
let msg = e.to_string();
assert!(!msg.contains("from_checkpoint"), "{msg}");
assert!(!msg.contains("export_to"), "{msg}");
assert!(!msg.contains("import_from"), "{msg}");
assert!(msg.contains("docker"), "{msg}");
assert!(msg.contains("microsandbox"), "{msg}");
assert!(msg.contains("RIGHTSIZE_BACKEND=microsandbox"), "{msg}");
}
#[test]
fn checkpoint_artifact_missing_names_the_ref_and_the_backend() {
let e = RightsizeError::CheckpointArtifactMissing {
checkpoint_ref: "rz-ckpt-deadbeefcafe".to_string(),
backend: "microsandbox".to_string(),
};
let msg = e.to_string();
assert!(msg.contains("rz-ckpt-deadbeefcafe"), "{msg}");
assert!(msg.contains("microsandbox"), "{msg}");
}
#[test]
fn malformed_archive_names_the_path_and_the_reason() {
let e = RightsizeError::MalformedArchive {
path: std::path::PathBuf::from("/tmp/cp.archive"),
reason: "unsupported rightsizeArchive version 2 (expected 1)".to_string(),
};
let msg = e.to_string();
assert!(msg.contains("/tmp/cp.archive"), "{msg}");
assert!(
msg.contains("unsupported rightsizeArchive version 2 (expected 1)"),
"{msg}"
);
}
}