use std::collections::HashMap;
use std::path::PathBuf;
use zlayer_spec::StorageSpec;
use crate::error::{AgentError, Result};
use crate::storage_manager::StorageManager;
#[cfg_attr(all(target_os = "windows", feature = "wsl"), allow(dead_code))]
pub(crate) async fn prepare_storage_volumes(
storage_manager: &mut StorageManager,
container_id: &str,
storage: &[StorageSpec],
) -> Result<HashMap<String, PathBuf>> {
let mut volume_paths = HashMap::new();
for spec in storage {
match spec {
StorageSpec::Named { name, .. } => {
let path = storage_manager
.ensure_volume_with_sync(name)
.await
.map_err(|e| AgentError::CreateFailed {
id: container_id.to_string(),
reason: format!("failed to ensure volume '{name}': {e}"),
})?;
storage_manager
.attach_volume(name, container_id)
.map_err(|e| AgentError::CreateFailed {
id: container_id.to_string(),
reason: format!("failed to attach volume '{name}': {e}"),
})?;
volume_paths.insert(name.clone(), path);
}
StorageSpec::Anonymous { target, .. } => {
let path = storage_manager
.create_anonymous(container_id, target)
.map_err(|e| AgentError::CreateFailed {
id: container_id.to_string(),
reason: format!("failed to create anonymous volume for '{target}': {e}"),
})?;
volume_paths.insert(anon_key(target), path);
}
StorageSpec::Bind { .. } | StorageSpec::Tmpfs { .. } => {}
StorageSpec::S3 {
bucket,
prefix,
endpoint,
..
} => {
let path = storage_manager
.mount_s3(bucket, prefix.as_deref(), endpoint.as_deref(), container_id)
.map_err(|e| AgentError::CreateFailed {
id: container_id.to_string(),
reason: format!("failed to mount S3 bucket '{bucket}': {e}"),
})?;
volume_paths.insert(s3_key(bucket, prefix.as_deref()), path);
}
}
}
Ok(volume_paths)
}
#[cfg(target_os = "macos")]
pub(crate) fn cleanup_storage_volumes(
storage_manager: &mut StorageManager,
container_id: &str,
storage: &[StorageSpec],
) {
for spec in storage {
match spec {
StorageSpec::Named { name, .. } => {
if let Err(e) = storage_manager.detach_volume(name, container_id) {
tracing::warn!(
volume = %name,
container = %container_id,
error = %e,
"failed to detach volume"
);
}
}
StorageSpec::S3 { bucket, prefix, .. } => {
if let Err(e) = storage_manager.unmount_s3(bucket, prefix.as_deref(), container_id)
{
tracing::warn!(
bucket = %bucket,
container = %container_id,
error = %e,
"failed to unmount S3 bucket"
);
}
}
StorageSpec::Anonymous { .. }
| StorageSpec::Bind { .. }
| StorageSpec::Tmpfs { .. } => {}
}
}
if let Err(e) = storage_manager.cleanup_anonymous(container_id) {
tracing::warn!(
container = %container_id,
error = %e,
"failed to cleanup anonymous volumes"
);
}
}
#[cfg_attr(all(target_os = "windows", feature = "wsl"), allow(dead_code))]
#[must_use]
pub(crate) fn anon_key(target: &str) -> String {
format!("_anon_{}", target.trim_start_matches('/').replace('/', "_"))
}
#[cfg_attr(all(target_os = "windows", feature = "wsl"), allow(dead_code))]
#[must_use]
pub(crate) fn s3_key(bucket: &str, prefix: Option<&str>) -> String {
format!("_s3_{}_{}", bucket, prefix.unwrap_or(""))
}
#[cfg(target_os = "macos")]
pub(crate) struct ResolvedMount {
pub host: PathBuf,
pub target: String,
pub readonly: bool,
}
#[cfg(target_os = "macos")]
#[must_use]
pub(crate) fn resolve_host_source(
spec: &StorageSpec,
volume_paths: &HashMap<String, PathBuf>,
) -> Option<ResolvedMount> {
match spec {
StorageSpec::Bind {
source,
target,
readonly,
} => Some(ResolvedMount {
host: PathBuf::from(source),
target: target.clone(),
readonly: *readonly,
}),
StorageSpec::Named {
name,
target,
readonly,
..
} => volume_paths.get(name).map(|host| ResolvedMount {
host: host.clone(),
target: target.clone(),
readonly: *readonly,
}),
StorageSpec::Anonymous { target, .. } => {
volume_paths
.get(&anon_key(target))
.map(|host| ResolvedMount {
host: host.clone(),
target: target.clone(),
readonly: false,
})
}
StorageSpec::S3 {
bucket,
prefix,
target,
readonly,
..
} => volume_paths
.get(&s3_key(bucket, prefix.as_deref()))
.map(|host| ResolvedMount {
host: host.clone(),
target: target.clone(),
readonly: *readonly,
}),
StorageSpec::Tmpfs { .. } => None,
}
}