use crate::cli::args::RunArgs;
use crate::config::Config;
use crate::error::MinoResult;
use crate::home::{self, HomeVolume};
use crate::orchestration::ContainerRuntime;
use std::path::Path;
use tracing::debug;
use super::image::LAYER_BASE_IMAGE;
pub(super) async fn setup_home_volume(
runtime: &dyn ContainerRuntime,
args: &RunArgs,
config: &Config,
project_dir: &Path,
image: &str,
) -> MinoResult<Option<String>> {
if args.no_home || !config.home.enabled {
debug!("Home volume disabled by flag/config");
return Ok(None);
}
if !is_mino_image(image) {
debug!("Skipping home volume for custom image: {}", image);
return Ok(None);
}
if has_home_mount(&args.volume, &config.container.volumes) {
debug!("Skipping home volume: user-specified mount at /home/developer");
return Ok(None);
}
let volume_name = home::home_volume_name(project_dir);
let existing = runtime.volume_inspect(&volume_name).await?;
if existing.is_some() {
debug!("Reusing existing home volume: {}", volume_name);
} else {
debug!("Creating home volume: {}", volume_name);
let labels = HomeVolume::labels(project_dir);
runtime.volume_create(&volume_name, &labels).await?;
}
Ok(Some(format!("{}:/home/developer", volume_name)))
}
pub(super) fn is_mino_image(image: &str) -> bool {
image == LAYER_BASE_IMAGE || image.starts_with("mino-composed-")
}
pub(super) fn has_home_mount(cli_volumes: &[String], config_volumes: &[String]) -> bool {
cli_volumes
.iter()
.chain(config_volumes.iter())
.any(|v| v.contains(":/home/developer"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::orchestration::mock::MockRuntime;
use crate::orchestration::VolumeInfo;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
fn test_args() -> RunArgs {
RunArgs {
name: None,
project: None,
aws: false,
gcp: false,
azure: false,
all_clouds: false,
no_ssh_agent: false,
no_github: false,
strict_credentials: false,
image: None,
layers: vec![],
env: vec![],
volume: vec![],
detach: false,
read_only: false,
no_cache: false,
no_home: false,
cache_fresh: false,
network: None,
network_allow: vec![],
network_preset: None,
command: vec![],
}
}
#[tokio::test]
async fn setup_creates_volume_on_miss() {
let mock = Arc::new(MockRuntime::new());
let args = test_args();
let config = Config::default();
let project = PathBuf::from("/tmp/test-project");
let result = setup_home_volume(&*mock, &args, &config, &project, LAYER_BASE_IMAGE)
.await
.unwrap();
assert!(result.is_some());
assert!(result.unwrap().contains(":/home/developer"));
mock.assert_called("volume_inspect", 1);
mock.assert_called("volume_create", 1);
}
#[tokio::test]
async fn setup_reuses_existing_volume() {
use crate::orchestration::mock::MockResponse;
let vol = VolumeInfo {
name: "mino-home-existing".to_string(),
labels: HashMap::new(),
mountpoint: None,
created_at: None,
size_bytes: None,
};
let mock = Arc::new(MockRuntime::new().on(
"volume_inspect",
Ok(MockResponse::OptionalVolumeInfo(Some(vol))),
));
let args = test_args();
let config = Config::default();
let project = PathBuf::from("/tmp/test-project");
let result = setup_home_volume(&*mock, &args, &config, &project, LAYER_BASE_IMAGE)
.await
.unwrap();
assert!(result.is_some());
mock.assert_called("volume_inspect", 1);
mock.assert_called("volume_create", 0);
}
#[tokio::test]
async fn setup_disabled_by_no_home_flag() {
let mock = Arc::new(MockRuntime::new());
let mut args = test_args();
args.no_home = true;
let config = Config::default();
let project = PathBuf::from("/tmp/test-project");
let result = setup_home_volume(&*mock, &args, &config, &project, LAYER_BASE_IMAGE)
.await
.unwrap();
assert!(result.is_none());
mock.assert_called("volume_inspect", 0);
mock.assert_called("volume_create", 0);
}
#[tokio::test]
async fn setup_disabled_by_config() {
let mock = Arc::new(MockRuntime::new());
let args = test_args();
let mut config = Config::default();
config.home.enabled = false;
let project = PathBuf::from("/tmp/test-project");
let result = setup_home_volume(&*mock, &args, &config, &project, LAYER_BASE_IMAGE)
.await
.unwrap();
assert!(result.is_none());
mock.assert_called("volume_inspect", 0);
}
#[tokio::test]
async fn setup_skips_custom_image() {
let mock = Arc::new(MockRuntime::new());
let args = test_args();
let config = Config::default();
let project = PathBuf::from("/tmp/test-project");
let result = setup_home_volume(&*mock, &args, &config, &project, "fedora:43")
.await
.unwrap();
assert!(result.is_none());
mock.assert_called("volume_inspect", 0);
}
#[tokio::test]
async fn setup_works_with_composed_image() {
let mock = Arc::new(MockRuntime::new());
let args = test_args();
let config = Config::default();
let project = PathBuf::from("/tmp/test-project");
let result = setup_home_volume(
&*mock,
&args,
&config,
&project,
"mino-composed-abc123def456",
)
.await
.unwrap();
assert!(result.is_some());
mock.assert_called("volume_create", 1);
}
#[tokio::test]
async fn setup_skips_when_user_volume_at_home() {
let mock = Arc::new(MockRuntime::new());
let mut args = test_args();
args.volume = vec!["/my/dir:/home/developer".to_string()];
let config = Config::default();
let project = PathBuf::from("/tmp/test-project");
let result = setup_home_volume(&*mock, &args, &config, &project, LAYER_BASE_IMAGE)
.await
.unwrap();
assert!(result.is_none());
mock.assert_called("volume_inspect", 0);
}
#[test]
fn is_mino_image_base() {
assert!(is_mino_image(LAYER_BASE_IMAGE));
}
#[test]
fn is_mino_image_composed() {
assert!(is_mino_image("mino-composed-abc123def456"));
}
#[test]
fn is_mino_image_custom_false() {
assert!(!is_mino_image("fedora:43"));
assert!(!is_mino_image("custom:tag"));
assert!(!is_mino_image("ghcr.io/other/image:latest"));
}
#[test]
fn has_home_mount_cli_volume() {
let cli = vec!["/my/dir:/home/developer".to_string()];
assert!(has_home_mount(&cli, &[]));
}
#[test]
fn has_home_mount_cli_volume_with_options() {
let cli = vec!["/my/dir:/home/developer:rw".to_string()];
assert!(has_home_mount(&cli, &[]));
}
#[test]
fn has_home_mount_config_volume() {
let config = vec!["/my/dir:/home/developer".to_string()];
assert!(has_home_mount(&[], &config));
}
#[test]
fn has_home_mount_no_match() {
let cli = vec!["/my/dir:/workspace".to_string()];
let config = vec!["/my/dir:/home/other".to_string()];
assert!(!has_home_mount(&cli, &config));
}
#[test]
fn has_home_mount_empty() {
assert!(!has_home_mount(&[], &[]));
}
}