Skip to main content
Glama

microsandbox

by microsandbox
config.rs24.5 kB
//! Configuration management for the Microsandbox runtime. //! //! This module provides structures and utilities for modifying Microsandbox //! configuration. use microsandbox_utils::{DEFAULT_SHELL, MICROSANDBOX_CONFIG_FILENAME}; use nondestructive::yaml; use sqlx::{Pool, Sqlite}; use std::{ collections::HashMap, path::{Path, PathBuf}, }; use tokio::fs; use typed_path::Utf8UnixPathBuf; use crate::{ config::{EnvPair, Microsandbox, PathSegment, PortPair, Sandbox}, oci::Reference, MicrosandboxError, MicrosandboxResult, }; use super::db; //-------------------------------------------------------------------------------------------------- // Types //-------------------------------------------------------------------------------------------------- #[derive(Debug, Clone)] /// The component to add to the Microsandbox configuration. pub enum Component { /// A sandbox component. Sandbox { /// The image to use for the sandbox. image: String, /// The amount of memory in MiB to use. memory: Option<u32>, /// The number of CPUs to use. cpus: Option<u32>, /// The volumes to mount. volumes: Vec<String>, /// The ports to expose. ports: Vec<String>, /// The environment variables to use. envs: Vec<String>, /// The environment file to use. env_file: Option<Utf8UnixPathBuf>, /// The dependencies to use for the sandbox. depends_on: Vec<String>, /// The working directory to use for the sandbox. workdir: Option<Utf8UnixPathBuf>, /// The shell to use for the sandbox. shell: Option<String>, /// The scripts to use for the sandbox. scripts: HashMap<String, String>, /// The imports to use for the sandbox. imports: HashMap<String, Utf8UnixPathBuf>, /// The exports to use for the sandbox. exports: HashMap<String, Utf8UnixPathBuf>, /// The network scope to use for the sandbox. scope: Option<String>, }, /// A build component. Build {}, /// A group component. Group {}, } /// The type of component to add to the Microsandbox configuration. #[derive(Debug, Clone)] pub enum ComponentType { /// A sandbox component. Sandbox, /// A build component. Build, /// A group component. Group, } //-------------------------------------------------------------------------------------------------- // Functions //-------------------------------------------------------------------------------------------------- /// Adds one or more components to the Microsandbox configuration. /// /// Modifies the Microsandbox configuration file by adding new components while preserving /// the existing formatting and structure. /// /// ## Arguments /// /// * `names` - Names for the components to add /// * `component` - The component specification to add /// * `project_dir` - Optional project directory path (defaults to current directory) /// * `config_file` - Optional config file path (defaults to standard filename) /// /// ## Returns /// /// * `Ok(())` on success, or error if the file cannot be found/read/written, /// contains invalid YAML, or a component with the same name already exists pub async fn add( names: &[String], component: &Component, project_dir: Option<&Path>, config_file: Option<&str>, ) -> MicrosandboxResult<()> { let (_, _, full_config_path) = resolve_config_paths(project_dir, config_file).await?; // Read the configuration file content let config_contents = fs::read_to_string(&full_config_path).await?; // Parse the YAML document using nondestructive let mut doc = yaml::from_slice(config_contents.as_bytes()) .map_err(|e| MicrosandboxError::ConfigParseError(e.to_string()))?; for name in names { match component { Component::Sandbox { image, memory, cpus, volumes, ports, envs, env_file, depends_on, workdir, shell, scripts, imports, exports, scope, } => { let doc_mut = doc.as_mut(); let mut root_mapping = doc_mut.make_mapping(); // Ensure the "sandboxes" key exists in the root mapping let mut sandboxes_mapping = if let Some(sandboxes_mut) = root_mapping.get_mut("sandboxes") { // Get the existing sandboxes mapping sandboxes_mut.make_mapping() } else { // Create a new sandboxes mapping if it doesn't exist root_mapping .insert("sandboxes", yaml::Separator::Auto) .make_mapping() }; // Check if the sandbox already exists by trying to get it if sandboxes_mapping.get_mut(name).is_some() { return Err(MicrosandboxError::ConfigValidation(format!( "Sandbox with name '{}' already exists", name ))); } // Create a new sandbox mapping let mut sandbox_mapping = sandboxes_mapping .insert(name, yaml::Separator::Auto) .make_mapping(); // Add image field (required) sandbox_mapping.insert_str("image", image.to_string()); // Add optional fields if let Some(memory_value) = memory { sandbox_mapping.insert_u32("memory", *memory_value); } if let Some(cpus_value) = cpus { sandbox_mapping.insert_u32("cpus", *cpus_value as u32); } // Add shell (default if not provided) if let Some(shell_value) = shell { sandbox_mapping.insert_str("shell", shell_value); } else if sandbox_mapping.get_mut("shell").is_none() { sandbox_mapping.insert_str("shell", DEFAULT_SHELL); } // Add volumes if any if !volumes.is_empty() { let mut volumes_sequence = sandbox_mapping .insert("volumes", yaml::Separator::Auto) .make_sequence(); for volume in volumes { volumes_sequence.push_string(volume); } } // Add ports if any if !ports.is_empty() { let mut ports_sequence = sandbox_mapping .insert("ports", yaml::Separator::Auto) .make_sequence(); for port in ports { ports_sequence.push_string(port); } } // Add env vars if any if !envs.is_empty() { let mut envs_sequence = sandbox_mapping .insert("envs", yaml::Separator::Auto) .make_sequence(); for env in envs { envs_sequence.push_string(env); } } // Add env_file if provided if let Some(env_file_path) = env_file { sandbox_mapping.insert_str("env_file", env_file_path.to_string()); } // Add depends_on if any if !depends_on.is_empty() { let mut depends_on_sequence = sandbox_mapping .insert("depends_on", yaml::Separator::Auto) .make_sequence(); for dep in depends_on { depends_on_sequence.push_string(dep); } } // Add workdir if provided if let Some(workdir_path) = workdir { sandbox_mapping.insert_str("workdir", workdir_path.to_string()); } // Add scripts if any if !scripts.is_empty() { let mut scripts_mapping = sandbox_mapping .insert("scripts", yaml::Separator::Auto) .make_mapping(); for (script_name, script_content) in scripts { scripts_mapping.insert_str(script_name, script_content); } } // Add imports if any if !imports.is_empty() { let mut imports_mapping = sandbox_mapping .insert("imports", yaml::Separator::Auto) .make_mapping(); for (import_name, import_path) in imports { imports_mapping.insert_str(import_name, import_path.to_string()); } } // Add exports if any if !exports.is_empty() { let mut exports_mapping = sandbox_mapping .insert("exports", yaml::Separator::Auto) .make_mapping(); for (export_name, export_path) in exports { exports_mapping.insert_str(export_name, export_path.to_string()); } } // Add network scope if provided if let Some(scope_value) = scope { let mut network_mapping = sandbox_mapping .insert("network", yaml::Separator::Auto) .make_mapping(); network_mapping.insert_str("scope", scope_value); } } Component::Build {} => {} Component::Group {} => {} } } // Write the modified YAML back to the file, preserving formatting let modified_content = doc.to_string(); // TODO: Validate config before writing fs::write(full_config_path, modified_content).await?; Ok(()) } /// Removes a component from the Microsandbox configuration. /// /// Modifies the Microsandbox configuration file by removing an existing component /// while preserving the existing formatting and structure. /// /// ## Arguments /// /// * `component` - The component to remove from the configuration /// /// ## Returns /// /// * `Ok(())` on success, or error if the file cannot be found/read/written, /// contains invalid YAML, or the component does not exist /// /// Note: This function is currently a placeholder and needs to be implemented. pub async fn remove( component_type: ComponentType, names: &[String], project_dir: Option<&Path>, config_file: Option<&str>, ) -> MicrosandboxResult<()> { let (_, _, full_config_path) = resolve_config_paths(project_dir, config_file).await?; // Read the configuration file content let config_contents = fs::read_to_string(&full_config_path).await?; let mut doc = yaml::from_slice(config_contents.as_bytes()) .map_err(|e| MicrosandboxError::ConfigParseError(e.to_string()))?; match component_type { ComponentType::Sandbox => { let doc_mut = doc.as_mut(); let mut root_mapping = doc_mut .into_mapping_mut() .ok_or(MicrosandboxError::ConfigParseError( "config is not valid. expected an object".to_string(), ))?; // Ensure the "sandboxes" key exists in the root mapping let mut sandboxes_mapping = if let Some(sandboxes_mut) = root_mapping.get_mut("sandboxes") { // Get the existing sandboxes mapping sandboxes_mut .into_mapping_mut() .ok_or(MicrosandboxError::ConfigParseError( "sandboxes is not a valid mapping".to_string(), ))? } else { // Create a new sandboxes mapping if it doesn't exist root_mapping .insert("sandboxes", yaml::Separator::Auto) .make_mapping() }; for name in names { sandboxes_mapping.remove(name); } } _ => (), } // Write the modified YAML back to the file, preserving formatting let modified_content = doc.to_string(); // TODO: Validate config before writing fs::write(full_config_path, modified_content).await?; Ok(()) } /// Lists components in the Microsandbox configuration. /// /// Retrieves and displays information about components defined in the Microsandbox configuration. /// /// ## Arguments /// /// * `component_type` - The type of component to list /// * `project_dir` - Optional path to the project directory. If None, defaults to current directory /// * `config_file` - Optional path to the Microsandbox config file. If None, uses default filename /// /// ## Returns /// /// * `Ok(())` on success, or error if the file cannot be found/read/written, /// contains invalid YAML, or the component does not exist pub async fn list( component_type: ComponentType, project_dir: Option<&Path>, config_file: Option<&str>, ) -> MicrosandboxResult<Vec<String>> { let (config, _, _) = load_config(project_dir, config_file).await?; match component_type { ComponentType::Sandbox => { return Ok(config.get_sandboxes().keys().cloned().collect()); } _ => return Ok(vec![]), } } //-------------------------------------------------------------------------------------------------- // Functions: Helpers //-------------------------------------------------------------------------------------------------- /// Loads a Microsandbox configuration from a file. /// /// This function handles all the common steps for loading a Microsandbox configuration, including: /// - Resolving the project directory and config file path /// - Validating the config file path /// - Checking if the config file exists /// - Reading and parsing the config file /// /// ## Arguments /// /// * `project_dir` - Optional path to the project directory. If None, defaults to current directory /// * `config_file` - Optional path to the Microsandbox config file. If None, uses default filename /// /// ## Returns /// /// Returns a tuple containing: /// - The loaded Microsandbox configuration /// - The canonical project directory path /// - The config file name /// /// Or a `MicrosandboxError` if: /// - The config file path is invalid /// - The config file does not exist /// - The config file cannot be read /// - The config file contains invalid YAML pub async fn load_config( project_dir: Option<&Path>, config_file: Option<&str>, ) -> MicrosandboxResult<(Microsandbox, PathBuf, String)> { // Get the target path, defaulting to current directory if none specified let project_dir = project_dir.unwrap_or_else(|| Path::new(".")); let canonical_project_dir = fs::canonicalize(project_dir).await?; // Validate the config file path let config_file = config_file.unwrap_or_else(|| MICROSANDBOX_CONFIG_FILENAME); let _ = PathSegment::try_from(config_file)?; let full_config_path = canonical_project_dir.join(config_file); // Check if config file exists if !full_config_path.exists() { return Err(MicrosandboxError::MicrosandboxConfigNotFound( project_dir.display().to_string(), )); } // Read and parse the config file let config_contents = fs::read_to_string(&full_config_path).await?; let config: Microsandbox = serde_yaml::from_str(&config_contents)?; Ok((config, canonical_project_dir, config_file.to_string())) } /// Resolves the paths for a Microsandbox configuration. /// /// This function is similar to `load_config` but without actually loading the file. /// It just resolves the paths that would be used. /// /// ## Arguments /// /// * `project_dir` - Optional path to the project directory. If None, defaults to current directory /// * `config_file` - Optional path to the Microsandbox config file. If None, uses default filename /// /// ## Returns /// /// Returns a tuple containing: /// - The canonical project directory path /// - The config file name /// - The full config file path pub async fn resolve_config_paths( project_dir: Option<&Path>, config_file: Option<&str>, ) -> MicrosandboxResult<(PathBuf, String, PathBuf)> { // Get the target path, defaulting to current directory if none specified let project_dir = project_dir.unwrap_or_else(|| Path::new(".")); let canonical_project_dir = fs::canonicalize(project_dir).await?; // Validate the config file path let config_file = config_file.unwrap_or_else(|| MICROSANDBOX_CONFIG_FILENAME); let _ = PathSegment::try_from(config_file)?; let full_config_path = canonical_project_dir.join(config_file); // Check if config file exists if !full_config_path.exists() { return Err(MicrosandboxError::MicrosandboxConfigNotFound( project_dir.display().to_string(), )); } Ok(( canonical_project_dir, config_file.to_string(), full_config_path, )) } /// Applies defaults from an OCI image configuration to a sandbox configuration. /// /// This function enhances the sandbox configuration with defaults from the OCI image /// configuration when they are not explicitly defined in the sandbox config. /// /// The following defaults are applied: /// - Script: Uses the entrypoint and cmd from the image if a script is missing /// - Environment variables: Combines image env variables with sandbox env variables /// - Working directory: Uses the image's working directory if not specified /// - Exposed ports: Combines image exposed ports with sandbox ports /// /// ## Arguments /// /// * `sandbox_config` - Mutable reference to the sandbox configuration to enhance /// * `reference` - OCI image reference to get defaults from /// * `script_name` - The name of the script we're trying to run /// /// ## Returns /// /// Returns `Ok(())` if defaults were successfully applied, or a `MicrosandboxError` if: /// - The image configuration could not be retrieved /// - Any conversion or parsing operations fail pub async fn apply_image_defaults( sandbox_config: &mut Sandbox, reference: &Reference, oci_db: &Pool<Sqlite>, ) -> MicrosandboxResult<()> { // Get the image configuration if let Some(config) = db::get_image_config(&oci_db, &reference.to_string()).await? { tracing::info!("applying defaults from image configuration"); // Apply working directory if not set in sandbox if sandbox_config.get_workdir().is_none() && config.config_working_dir.is_some() { let workdir = config.config_working_dir.unwrap(); tracing::debug!("using image working directory: {}", workdir); let workdir_path = Utf8UnixPathBuf::from(workdir); sandbox_config.workdir = Some(workdir_path); } // Combine environment variables if let Some(config_env_json) = config.config_env_json { if let Ok(image_env_vars) = serde_json::from_str::<Vec<String>>(&config_env_json) { let mut image_env_pairs = Vec::new(); for env_var in image_env_vars { if let Ok(env_pair) = env_var.parse::<EnvPair>() { image_env_pairs.push(env_pair); } } tracing::debug!("image env vars: {:#?}", image_env_pairs); // Combine image env vars with sandbox env vars (image vars come first) let mut combined_env = image_env_pairs; combined_env.extend_from_slice(sandbox_config.get_envs()); sandbox_config.envs = combined_env; } } // Apply entrypoint and cmd as command if no command is defined if sandbox_config.get_command().is_empty() { let mut command_vec: Vec<String> = Vec::new(); let mut has_entrypoint_or_cmd = false; // Try to use entrypoint and cmd from image config if let Some(entrypoint_json) = &config.config_entrypoint_json { if let Ok(entrypoint) = serde_json::from_str::<Vec<String>>(entrypoint_json) { if !entrypoint.is_empty() { has_entrypoint_or_cmd = true; command_vec = entrypoint; // Add CMD args if they exist if let Some(cmd_json) = &config.config_cmd_json { if let Ok(cmd) = serde_json::from_str::<Vec<String>>(cmd_json) { if !cmd.is_empty() { command_vec.extend(cmd); } } } tracing::debug!("entrypoint exec content: {:?}", command_vec); } } } else if let Some(cmd_json) = &config.config_cmd_json { if let Ok(cmd) = serde_json::from_str::<Vec<String>>(cmd_json) { if !cmd.is_empty() { has_entrypoint_or_cmd = true; command_vec = cmd; tracing::debug!("cmd exec content: {:?}", command_vec); } } } // If we found an entrypoint or cmd, set it as the command if has_entrypoint_or_cmd { tracing::debug!("setting command to: {:?}", command_vec); sandbox_config.command = command_vec; } else if let Some(shell_value) = &sandbox_config.shell { // If no entrypoint or cmd, use shell as fallback command tracing::debug!("using shell as fallback command"); sandbox_config.command = vec![shell_value.clone()]; } } // Combine exposed ports if let Some(exposed_ports_json) = &config.config_exposed_ports_json { if let Ok(exposed_ports_map) = serde_json::from_str::<serde_json::Value>(exposed_ports_json) { if let Some(exposed_ports_obj) = exposed_ports_map.as_object() { let mut additional_ports = Vec::new(); for port_key in exposed_ports_obj.keys() { // Port keys in OCI format are like "80/tcp" if let Some(container_port) = port_key.split('/').next() { if let Ok(port_num) = container_port.parse::<u16>() { // Create a port mapping from host port to container port // We'll use the same port on both sides let port_pair = format!("{}:{}", port_num, port_num).parse::<PortPair>(); if let Ok(port_pair) = port_pair { // Only add if not already defined in sandbox config let existing_ports = sandbox_config.get_ports(); if !existing_ports .iter() .any(|p| p.get_guest() == port_pair.get_guest()) { additional_ports.push(port_pair); } } } } } tracing::debug!("additional ports: {:?}", additional_ports); // Add new ports to existing ones let mut combined_ports = sandbox_config.get_ports().to_vec(); combined_ports.extend(additional_ports); sandbox_config.ports = combined_ports; } } } } Ok(()) }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/microsandbox/microsandbox'

If you have feedback or need assistance with the MCP directory API, please join our Discord server