use microsandbox_image::RegistryAuth;
#[cfg(feature = "net")]
use microsandbox_network::builder::{NetworkBuilder, SecretBuilder};
#[cfg(feature = "net")]
use microsandbox_network::config::{PortProtocol, PublishedPort};
#[cfg(feature = "net")]
use std::net::{IpAddr, Ipv4Addr};
use super::{
config::SandboxConfig,
types::{ImageBuilder, IntoImage, MountBuilder, Patch, PatchBuilder, RootfsSource},
};
use crate::{LogLevel, MicrosandboxResult, size::Mebibytes};
pub struct SandboxBuilder {
config: SandboxConfig,
build_error: Option<crate::MicrosandboxError>,
}
impl SandboxBuilder {
pub fn new(name: impl Into<String>) -> Self {
Self {
config: SandboxConfig {
name: name.into(),
..Default::default()
},
build_error: None,
}
}
pub fn image(mut self, image: impl IntoImage) -> Self {
match image.into_rootfs_source() {
Ok(rootfs) => self.config.image = rootfs,
Err(e) => {
if self.build_error.is_none() {
self.build_error = Some(e);
}
}
}
self
}
pub fn image_with(mut self, f: impl FnOnce(ImageBuilder) -> ImageBuilder) -> Self {
match f(ImageBuilder::new()).build() {
Ok(rootfs) => self.config.image = rootfs,
Err(e) => {
if self.build_error.is_none() {
self.build_error = Some(e);
}
}
}
self
}
pub fn cpus(mut self, count: u8) -> Self {
self.config.cpus = count;
self
}
pub fn memory(mut self, size: impl Into<Mebibytes>) -> Self {
self.config.memory_mib = size.into().as_u32();
self
}
pub fn log_level(mut self, level: LogLevel) -> Self {
self.config.log_level = Some(level);
self
}
pub fn quiet_logs(mut self) -> Self {
self.config.log_level = None;
self
}
pub fn workdir(mut self, path: impl Into<String>) -> Self {
self.config.workdir = Some(path.into());
self
}
pub fn shell(mut self, shell: impl Into<String>) -> Self {
self.config.shell = Some(shell.into());
self
}
pub fn registry_auth(mut self, auth: RegistryAuth) -> Self {
self.config.registry_auth = Some(auth);
self
}
pub fn replace(mut self) -> Self {
self.config.replace_existing = true;
self
}
pub fn entrypoint(mut self, cmd: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.config.entrypoint = Some(cmd.into_iter().map(Into::into).collect());
self
}
pub fn hostname(mut self, hostname: impl Into<String>) -> Self {
self.config.hostname = Some(hostname.into());
self
}
pub fn user(mut self, user: impl Into<String>) -> Self {
self.config.user = Some(user.into());
self
}
pub fn pull_policy(mut self, policy: microsandbox_image::PullPolicy) -> Self {
self.config.pull_policy = policy;
self
}
#[cfg(feature = "net")]
pub fn disable_network(mut self) -> Self {
self.config.network.enabled = false;
self.config.network.policy = microsandbox_network::policy::NetworkPolicy::none();
self
}
#[cfg(feature = "net")]
pub fn network(mut self, f: impl FnOnce(NetworkBuilder) -> NetworkBuilder) -> Self {
let network = std::mem::take(&mut self.config.network);
self.config.network = f(NetworkBuilder::from_config(network)).build();
self
}
#[cfg(feature = "net")]
pub fn port(mut self, host_port: u16, guest_port: u16) -> Self {
self.config.network.ports.push(PublishedPort {
host_port,
guest_port,
protocol: PortProtocol::Tcp,
host_bind: IpAddr::V4(Ipv4Addr::LOCALHOST),
});
self
}
#[cfg(feature = "net")]
pub fn port_udp(mut self, host_port: u16, guest_port: u16) -> Self {
self.config.network.ports.push(PublishedPort {
host_port,
guest_port,
protocol: PortProtocol::Udp,
host_bind: IpAddr::V4(Ipv4Addr::LOCALHOST),
});
self
}
#[cfg(feature = "net")]
pub fn secret(mut self, f: impl FnOnce(SecretBuilder) -> SecretBuilder) -> Self {
let entry = f(SecretBuilder::new()).build();
self.config.network.secrets.secrets.push(entry);
if !self.config.network.tls.enabled {
self.config.network.tls.enabled = true;
}
self
}
#[cfg(feature = "net")]
pub fn secret_env(
self,
env_var: impl Into<String>,
value: impl Into<String>,
allowed_host: impl Into<String>,
) -> Self {
let env_var = env_var.into();
let value = value.into();
let allowed_host = allowed_host.into();
self.secret(|s| s.env(&env_var).value(value).allow_host(allowed_host))
}
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.config.env.push((key.into(), value.into()));
self
}
pub fn envs(
mut self,
vars: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
) -> Self {
for (k, v) in vars {
self.config.env.push((k.into(), v.into()));
}
self
}
pub fn script(mut self, name: impl Into<String>, content: impl Into<String>) -> Self {
self.config.scripts.insert(name.into(), content.into());
self
}
pub fn scripts(
mut self,
scripts: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
) -> Self {
for (name, content) in scripts {
self.config.scripts.insert(name.into(), content.into());
}
self
}
pub fn max_duration(mut self, secs: u64) -> Self {
self.config.policy.max_duration_secs = Some(secs);
self
}
pub fn idle_timeout(mut self, secs: u64) -> Self {
self.config.policy.idle_timeout_secs = Some(secs);
self
}
pub fn volume(
mut self,
guest_path: impl Into<String>,
f: impl FnOnce(MountBuilder) -> MountBuilder,
) -> Self {
match f(MountBuilder::new(guest_path)).build() {
Ok(mount) => self.config.mounts.push(mount),
Err(e) => {
if self.build_error.is_none() {
self.build_error = Some(e);
}
}
}
self
}
pub fn patch(mut self, f: impl FnOnce(PatchBuilder) -> PatchBuilder) -> Self {
self.config.patches.extend(f(PatchBuilder::new()).build());
self
}
pub fn add_patch(mut self, patch: Patch) -> Self {
self.config.patches.push(patch);
self
}
pub fn build(mut self) -> MicrosandboxResult<SandboxConfig> {
self.validate()?;
Ok(self.config)
}
pub async fn create(self) -> MicrosandboxResult<super::Sandbox> {
let config = self.build()?;
super::Sandbox::create(config).await
}
pub async fn create_detached(self) -> MicrosandboxResult<super::Sandbox> {
let config = self.build()?;
super::Sandbox::create_detached(config).await
}
pub fn create_with_pull_progress(
self,
) -> crate::MicrosandboxResult<(
microsandbox_image::PullProgressHandle,
tokio::task::JoinHandle<crate::MicrosandboxResult<super::Sandbox>>,
)> {
let config = self.build()?;
Ok(super::Sandbox::create_with_pull_progress(config))
}
pub fn create_detached_with_pull_progress(
self,
) -> crate::MicrosandboxResult<(
microsandbox_image::PullProgressHandle,
tokio::task::JoinHandle<crate::MicrosandboxResult<super::Sandbox>>,
)> {
let config = self.build()?;
Ok(super::Sandbox::create_detached_with_pull_progress(config))
}
}
impl SandboxBuilder {
fn validate(&mut self) -> MicrosandboxResult<()> {
if let Some(err) = self.build_error.take() {
return Err(err);
}
if self.config.name.is_empty() {
return Err(crate::MicrosandboxError::InvalidConfig(
"sandbox name is required".into(),
));
}
match &self.config.image {
RootfsSource::Oci(s) if s.is_empty() => {
return Err(crate::MicrosandboxError::InvalidConfig(
"image source is required".into(),
));
}
RootfsSource::DiskImage { .. } if !self.config.patches.is_empty() => {
return Err(crate::MicrosandboxError::InvalidConfig(
"patches are not compatible with disk image rootfs".into(),
));
}
_ => {}
}
Ok(())
}
}
impl From<SandboxConfig> for SandboxBuilder {
fn from(config: SandboxConfig) -> Self {
Self {
config,
build_error: None,
}
}
}
#[cfg(test)]
mod tests {
use super::SandboxBuilder;
use crate::LogLevel;
#[cfg(feature = "net")]
use microsandbox_network::config::PortProtocol;
#[test]
fn test_builder_sets_runtime_log_level() {
let config = SandboxBuilder::new("test")
.image("alpine")
.log_level(LogLevel::Debug)
.build()
.unwrap();
assert_eq!(config.log_level, Some(LogLevel::Debug));
}
#[test]
fn test_builder_quiet_logs_clears_runtime_log_level() {
let config = SandboxBuilder::new("test")
.image("alpine")
.log_level(LogLevel::Trace)
.quiet_logs()
.build()
.unwrap();
assert_eq!(config.log_level, None);
}
#[test]
fn test_builder_replace_sets_replace_existing() {
let config = SandboxBuilder::new("test")
.image("alpine")
.replace()
.build()
.unwrap();
assert!(config.replace_existing);
}
#[cfg(feature = "net")]
#[test]
fn test_builder_ports_are_repeatable() {
let config = SandboxBuilder::new("test")
.image("alpine")
.port(8080, 80)
.port(3000, 3000)
.port_udp(5353, 53)
.build()
.unwrap();
assert_eq!(config.network.ports.len(), 3);
assert_eq!(config.network.ports[0].host_port, 8080);
assert_eq!(config.network.ports[0].guest_port, 80);
assert_eq!(config.network.ports[0].protocol, PortProtocol::Tcp);
assert_eq!(config.network.ports[1].host_port, 3000);
assert_eq!(config.network.ports[1].guest_port, 3000);
assert_eq!(config.network.ports[1].protocol, PortProtocol::Tcp);
assert_eq!(config.network.ports[2].host_port, 5353);
assert_eq!(config.network.ports[2].guest_port, 53);
assert_eq!(config.network.ports[2].protocol, PortProtocol::Udp);
}
#[cfg(feature = "net")]
#[test]
fn test_builder_disable_network_denies_all() {
use microsandbox_network::policy::Action;
let config = SandboxBuilder::new("test")
.image("alpine")
.disable_network()
.build()
.unwrap();
assert!(!config.network.enabled);
assert_eq!(config.network.policy.default_action, Action::Deny);
assert!(config.network.policy.rules.is_empty());
}
#[cfg(feature = "net")]
#[test]
fn test_builder_network_preserves_top_level_settings() {
let config = SandboxBuilder::new("test")
.image("alpine")
.port(8080, 80)
.secret_env("OPENAI_API_KEY", "secret", "api.openai.com")
.network(|n| n.max_connections(128))
.build()
.unwrap();
assert_eq!(config.network.ports.len(), 1);
assert_eq!(config.network.ports[0].host_port, 8080);
assert_eq!(config.network.ports[0].guest_port, 80);
assert_eq!(config.network.ports[0].protocol, PortProtocol::Tcp);
assert_eq!(config.network.secrets.secrets.len(), 1);
assert_eq!(config.network.max_connections, Some(128));
}
}