use std::sync::{Arc, OnceLock};
use crate::backend::{BackendProvider, SandboxBackend};
use crate::error::{Result, RightsizeError};
pub fn resolve(
providers: &[Box<dyn BackendProvider>],
requested: Option<&str>,
) -> Result<Box<dyn SandboxBackend>> {
if providers.is_empty() {
return Err(RightsizeError::Backend(
"No rightsize backends compiled in — add rightsize-msb and/or rightsize-docker \
(rightsize-backend-microsandbox / rightsize-backend-docker)"
.to_string(),
));
}
if let Some(requested) = requested {
let provider = providers
.iter()
.find(|p| p.name().eq_ignore_ascii_case(requested))
.ok_or_else(|| {
RightsizeError::Backend(format!(
"RIGHTSIZE_BACKEND='{requested}' — known backends are: {}",
provider_names(providers)
))
})?;
if !provider.is_supported() {
return Err(RightsizeError::Backend(format!(
"Requested backend '{}' unavailable: {}",
provider.name(),
provider.unsupported_reason()
)));
}
return provider.create();
}
let mut sorted: Vec<&Box<dyn BackendProvider>> = providers.iter().collect();
sorted.sort_by_key(|p| std::cmp::Reverse(p.priority()));
let supported = sorted
.into_iter()
.find(|p| p.is_supported())
.ok_or_else(|| {
RightsizeError::Backend(format!(
"No sandbox backend can run on this machine:\n{}",
unsupported_reasons(providers)
))
})?;
supported.create()
}
static PROVIDERS: std::sync::Mutex<Vec<Box<dyn BackendProvider>>> =
std::sync::Mutex::new(Vec::new());
pub fn register_provider(provider: Box<dyn BackendProvider>) {
let mut providers = PROVIDERS
.lock()
.expect("backend provider registry mutex poisoned");
if providers
.iter()
.any(|p| p.name().eq_ignore_ascii_case(provider.name()))
{
return;
}
providers.push(provider);
}
static ACTIVE: OnceLock<Arc<dyn SandboxBackend>> = OnceLock::new();
pub(crate) fn active() -> Arc<dyn SandboxBackend> {
ACTIVE
.get_or_init(|| {
let providers = PROVIDERS
.lock()
.expect("backend provider registry mutex poisoned");
let requested = std::env::var("RIGHTSIZE_BACKEND").ok();
let backend =
resolve(&providers, requested.as_deref()).unwrap_or_else(|e| panic!("{e}"));
let backend: Arc<dyn SandboxBackend> = Arc::from(backend);
crate::reaper::sweep(&backend);
backend
})
.clone()
}
pub fn active_name() -> String {
active().name().to_string()
}
fn provider_names(providers: &[Box<dyn BackendProvider>]) -> String {
providers
.iter()
.map(|p| p.name())
.collect::<Vec<_>>()
.join(", ")
}
fn unsupported_reasons(providers: &[Box<dyn BackendProvider>]) -> String {
providers
.iter()
.map(|p| format!(" - {}: {}", p.name(), p.unsupported_reason()))
.collect::<Vec<_>>()
.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
struct FakeProvider {
name: &'static str,
priority: u32,
supported: bool,
}
impl BackendProvider for FakeProvider {
fn name(&self) -> &str {
self.name
}
fn priority(&self) -> u32 {
self.priority
}
fn is_supported(&self) -> bool {
self.supported
}
fn unsupported_reason(&self) -> String {
format!("{} not supported on this host", self.name)
}
fn create(&self) -> Result<Box<dyn SandboxBackend>> {
panic!("not needed: {}", self.name)
}
}
fn provider(name: &'static str, priority: u32, supported: bool) -> Box<dyn BackendProvider> {
Box::new(FakeProvider {
name,
priority,
supported,
})
}
#[test]
fn register_provider_is_idempotent_by_name() {
register_provider(provider("dedup-fake", 1, false));
register_provider(provider("DEDUP-FAKE", 9, true));
let count = PROVIDERS
.lock()
.unwrap()
.iter()
.filter(|p| p.name().eq_ignore_ascii_case("dedup-fake"))
.count();
assert_eq!(count, 1);
}
fn expect_err(result: Result<Box<dyn SandboxBackend>>) -> RightsizeError {
match result {
Ok(_) => panic!("expected an error, got Ok"),
Err(e) => e,
}
}
#[test]
#[should_panic(expected = "not needed: microsandbox")]
fn picks_highest_priority_supported_provider() {
let providers = vec![
provider("docker", 10, true),
provider("microsandbox", 20, true),
];
let _ = resolve(&providers, None);
}
#[test]
#[should_panic(expected = "not needed: docker")]
fn env_override_wins_even_at_lower_priority() {
let providers = vec![
provider("docker", 10, true),
provider("microsandbox", 20, true),
];
let _ = resolve(&providers, Some("docker"));
}
#[test]
fn no_supported_provider_gives_every_reason() {
let providers = vec![
provider("microsandbox", 20, false),
provider("docker", 10, false),
];
let err = expect_err(resolve(&providers, None));
let msg = err.to_string();
assert!(msg.contains("microsandbox not supported"), "{msg}");
assert!(msg.contains("docker not supported"), "{msg}");
}
#[test]
fn unknown_requested_backend_lists_known_names() {
let providers = vec![provider("docker", 10, true)];
let err = expect_err(resolve(&providers, Some("podman")));
let msg = err.to_string();
assert!(msg.contains("podman"), "{msg}");
assert!(msg.contains("docker"), "{msg}");
}
#[test]
fn empty_provider_list_names_both_known_artifacts() {
let none_requested = expect_err(resolve(&[], None)).to_string();
assert!(none_requested.contains("rightsize-msb"), "{none_requested}");
assert!(
none_requested.contains("rightsize-docker"),
"{none_requested}"
);
let requested = expect_err(resolve(&[], Some("docker"))).to_string();
assert!(requested.contains("rightsize-msb"), "{requested}");
assert!(requested.contains("rightsize-docker"), "{requested}");
}
#[test]
fn requested_backend_that_is_unsupported_names_its_reason() {
let providers = vec![provider("docker", 10, false)];
let err = expect_err(resolve(&providers, Some("docker"))).to_string();
assert!(err.contains("unavailable:"), "{err}");
assert!(err.contains("docker not supported on this host"), "{err}");
}
#[test]
#[should_panic(expected = "not needed: docker")]
fn requested_backend_name_match_is_case_insensitive_upper() {
let providers = vec![provider("docker", 10, true)];
let _ = resolve(&providers, Some("DOCKER"));
}
#[test]
#[should_panic(expected = "not needed: docker")]
fn requested_backend_name_match_is_case_insensitive_mixed() {
let providers = vec![provider("docker", 10, true)];
let _ = resolve(&providers, Some("Docker"));
}
}