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;
#[derive(Debug, Clone)]
pub enum Component {
Sandbox {
image: String,
memory: Option<u32>,
cpus: Option<u32>,
volumes: Vec<String>,
ports: Vec<String>,
envs: Vec<String>,
env_file: Option<Utf8UnixPathBuf>,
depends_on: Vec<String>,
workdir: Option<Utf8UnixPathBuf>,
shell: Option<String>,
scripts: HashMap<String, String>,
imports: HashMap<String, Utf8UnixPathBuf>,
exports: HashMap<String, Utf8UnixPathBuf>,
scope: Option<String>,
},
Build {},
Group {},
}
#[derive(Debug, Clone)]
pub enum ComponentType {
Sandbox,
Build,
Group,
}
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?;
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()))?;
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();
let mut sandboxes_mapping =
if let Some(sandboxes_mut) = root_mapping.get_mut("sandboxes") {
sandboxes_mut.make_mapping()
} else {
root_mapping
.insert("sandboxes", yaml::Separator::Auto)
.make_mapping()
};
if sandboxes_mapping.get_mut(name).is_some() {
return Err(MicrosandboxError::ConfigValidation(format!(
"Sandbox with name '{}' already exists",
name
)));
}
let mut sandbox_mapping = sandboxes_mapping
.insert(name, yaml::Separator::Auto)
.make_mapping();
sandbox_mapping.insert_str("image", image.to_string());
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);
}
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);
}
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);
}
}
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);
}
}
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);
}
}
if let Some(env_file_path) = env_file {
sandbox_mapping.insert_str("env_file", env_file_path.to_string());
}
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);
}
}
if let Some(workdir_path) = workdir {
sandbox_mapping.insert_str("workdir", workdir_path.to_string());
}
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);
}
}
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());
}
}
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());
}
}
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 {} => {}
}
}
let modified_content = doc.to_string();
fs::write(full_config_path, modified_content).await?;
Ok(())
}
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?;
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(),
))?;
let mut sandboxes_mapping =
if let Some(sandboxes_mut) = root_mapping.get_mut("sandboxes") {
sandboxes_mut
.into_mapping_mut()
.ok_or(MicrosandboxError::ConfigParseError(
"sandboxes is not a valid mapping".to_string(),
))?
} else {
root_mapping
.insert("sandboxes", yaml::Separator::Auto)
.make_mapping()
};
for name in names {
sandboxes_mapping.remove(name);
}
}
_ => (),
}
let modified_content = doc.to_string();
fs::write(full_config_path, modified_content).await?;
Ok(())
}
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![]),
}
}
pub async fn load_config(
project_dir: Option<&Path>,
config_file: Option<&str>,
) -> MicrosandboxResult<(Microsandbox, PathBuf, String)> {
let project_dir = project_dir.unwrap_or_else(|| Path::new("."));
let canonical_project_dir = fs::canonicalize(project_dir).await?;
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);
if !full_config_path.exists() {
return Err(MicrosandboxError::MicrosandboxConfigNotFound(
project_dir.display().to_string(),
));
}
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()))
}
pub async fn resolve_config_paths(
project_dir: Option<&Path>,
config_file: Option<&str>,
) -> MicrosandboxResult<(PathBuf, String, PathBuf)> {
let project_dir = project_dir.unwrap_or_else(|| Path::new("."));
let canonical_project_dir = fs::canonicalize(project_dir).await?;
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);
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,
))
}
pub async fn apply_image_defaults(
sandbox_config: &mut Sandbox,
reference: &Reference,
oci_db: &Pool<Sqlite>,
) -> MicrosandboxResult<()> {
if let Some(config) = db::get_image_config(&oci_db, &reference.to_string()).await? {
tracing::info!("applying defaults from image configuration");
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);
}
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);
let mut combined_env = image_env_pairs;
combined_env.extend_from_slice(sandbox_config.get_envs());
sandbox_config.envs = combined_env;
}
}
if sandbox_config.get_command().is_empty() {
let mut command_vec: Vec<String> = Vec::new();
let mut has_entrypoint_or_cmd = false;
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;
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 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 {
tracing::debug!("using shell as fallback command");
sandbox_config.command = vec![shell_value.clone()];
}
}
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() {
if let Some(container_port) = port_key.split('/').next() {
if let Ok(port_num) = container_port.parse::<u16>() {
let port_pair =
format!("{}:{}", port_num, port_num).parse::<PortPair>();
if let Ok(port_pair) = port_pair {
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);
let mut combined_ports = sandbox_config.get_ports().to_vec();
combined_ports.extend(additional_ports);
sandbox_config.ports = combined_ports;
}
}
}
}
Ok(())
}