mod cloud;
mod local;
mod misconfigured;
mod profile;
pub(crate) mod sandbox;
pub(crate) mod volume;
pub use cloud::{CloudBackend, CloudBackendBuilder, DEFAULT_CLOUD_API_URL};
use futures::future::BoxFuture;
pub use local::{LocalBackend, LocalBackendBuilder};
pub use microsandbox_types::{
CloudCreateSandboxRequest, CloudCreateSandboxResponse, CloudErrorBody, CloudErrorDetails,
CloudMessageResponse, CloudPaginated, CloudSandboxStatus, CloudSandboxStatusReason,
};
pub use profile::{Profile, ProfileBackend, SdkConfig, load_sdk_config, resolve_default_backend};
pub use sandbox::{
SandboxBackend, SandboxCloudState, SandboxHandleCloudState, SandboxHandleInner,
SandboxHandleLocalState, SandboxInner, SandboxLocalState,
};
pub use volume::{
CloudVolumeKind, CloudVolumeStatus, VolumeBackend, VolumeCloudState, VolumeHandleCloudState,
VolumeHandleInner, VolumeHandleLocalState, VolumeInner, VolumeLocalState,
};
use std::{
sync::{Arc, OnceLock, RwLock},
time::Duration,
};
use serde::{Deserialize, Serialize};
use crate::MicrosandboxResult;
use crate::error::{Operation, UnsupportedReason};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum BackendKind {
Local,
Cloud,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BackendSelectionSource {
#[serde(rename = "programmatic")]
Programmatic,
#[serde(rename = "MSB_BACKEND")]
MsbBackend,
#[serde(rename = "MSB_API_KEY")]
MsbApiKey,
#[serde(rename = "MSB_PROFILE")]
MsbProfile,
#[serde(rename = "profile")]
Profile,
#[serde(rename = "active_profile")]
ActiveProfile,
#[serde(rename = "default")]
Default,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BackendInfo {
pub kind: BackendKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub api_url: Option<String>,
pub source: BackendSelectionSource,
#[serde(skip_serializing_if = "Option::is_none")]
pub profile: Option<String>,
}
impl BackendKind {
pub const fn as_str(self) -> &'static str {
match self {
Self::Local => "local",
Self::Cloud => "cloud",
}
}
}
impl BackendSelectionSource {
pub const fn as_str(self) -> &'static str {
match self {
Self::Programmatic => "programmatic",
Self::MsbBackend => "MSB_BACKEND",
Self::MsbApiKey => "MSB_API_KEY",
Self::MsbProfile => "MSB_PROFILE",
Self::Profile => "profile",
Self::ActiveProfile => "active_profile",
Self::Default => "default",
}
}
}
pub trait Backend: Send + Sync + 'static {
fn kind(&self) -> BackendKind;
fn info(&self) -> BackendInfo {
BackendInfo {
kind: self.kind(),
api_url: None,
source: BackendSelectionSource::Programmatic,
profile: None,
}
}
fn sandboxes(&self) -> &dyn SandboxBackend;
fn volumes(&self) -> &dyn VolumeBackend;
fn as_local(&self) -> Option<&LocalBackend> {
None
}
fn dial_agent<'a>(
&'a self,
_name: &'a str,
_timeout: Duration,
) -> BoxFuture<'a, MicrosandboxResult<crate::agent::AgentClient>> {
Box::pin(async {
Err(crate::MicrosandboxError::unsupported(
Operation::AgentConnect,
UnsupportedReason::NotAvailable(
"this backend does not provide agent connectivity".into(),
),
))
})
}
}
static DEFAULT: OnceLock<RwLock<Arc<dyn Backend>>> = OnceLock::new();
pub fn set_default_backend(backend: impl Into<Arc<dyn Backend>>) {
let cell = default_cell();
*cell.write().expect("DEFAULT backend RwLock poisoned") = backend.into();
}
pub fn swap_default_backend(backend: impl Into<Arc<dyn Backend>>) -> Arc<dyn Backend> {
let cell = default_cell();
let mut guard = cell.write().expect("DEFAULT backend RwLock poisoned");
std::mem::replace(&mut *guard, backend.into())
}
pub fn default_backend() -> Arc<dyn Backend> {
if let Ok(scoped) = SCOPED_BACKEND.try_with(|b| b.clone()) {
return scoped;
}
default_cell()
.read()
.expect("DEFAULT backend RwLock poisoned")
.clone()
}
pub fn default_backend_info() -> BackendInfo {
default_backend().info()
}
pub async fn with_backend<F, T>(backend: impl Into<Arc<dyn Backend>>, future: F) -> T
where
F: std::future::Future<Output = T>,
{
SCOPED_BACKEND.scope(backend.into(), future).await
}
fn default_cell() -> &'static RwLock<Arc<dyn Backend>> {
DEFAULT.get_or_init(|| {
let resolved = profile::resolve_default_backend().unwrap_or_else(|e| {
tracing::error!(error = %e, "default backend resolution failed");
Arc::new(misconfigured::ConfigurationErrorBackend::new(e))
});
RwLock::new(resolved)
})
}
tokio::task_local! {
static SCOPED_BACKEND: Arc<dyn Backend>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_backend_resolves_to_local_when_unset() {
let b = default_backend();
assert!(matches!(b.kind(), BackendKind::Local | BackendKind::Cloud));
}
#[tokio::test]
async fn with_backend_overrides_for_scope() {
struct Fake(BackendKind);
impl Backend for Fake {
fn kind(&self) -> BackendKind {
self.0
}
fn sandboxes(&self) -> &dyn SandboxBackend {
unimplemented!("fake backend only tests kind routing")
}
fn volumes(&self) -> &dyn VolumeBackend {
unimplemented!("fake backend only tests kind routing")
}
}
let fake: Arc<dyn Backend> = Arc::new(Fake(BackendKind::Cloud));
let observed = with_backend(fake, async { default_backend().kind() }).await;
assert_eq!(observed, BackendKind::Cloud);
let outside = default_backend().kind();
assert!(matches!(outside, BackendKind::Local | BackendKind::Cloud));
}
#[test]
fn swap_default_backend_restores_previous_backend() {
struct Fake(BackendKind);
impl Backend for Fake {
fn kind(&self) -> BackendKind {
self.0
}
fn sandboxes(&self) -> &dyn SandboxBackend {
unimplemented!("fake backend only tests kind routing")
}
fn volumes(&self) -> &dyn VolumeBackend {
unimplemented!("fake backend only tests kind routing")
}
}
let original = default_backend();
let fake: Arc<dyn Backend> = Arc::new(Fake(BackendKind::Cloud));
let previous = swap_default_backend(fake);
assert_eq!(default_backend().kind(), BackendKind::Cloud);
set_default_backend(previous);
assert_eq!(default_backend().kind(), original.kind());
}
}