use std::path::Path;
use crate::compose::types::{Service, VolumeMount, VolumeType};
use crate::libpod::types::container::{Mount, NamedVolume};
mod spec;
use spec::{
access_opts, extend_bind_opts_str, extend_volume_opts_str, parse_bind_string,
parse_volume_string,
};
pub(crate) fn build_mounts_all(
service: &Service,
base_dir: &Path,
secret_binds: &[String],
config_binds: &[String],
) -> (Vec<Mount>, Vec<NamedVolume>) {
let mut mounts = Vec::new();
let mut named = Vec::new();
for v in &service.volumes {
match v {
VolumeMount::Short(s) => {
if let Some((m, n)) = parse_volume_string(s) {
match n {
Some(nv) => named.push(nv),
None => mounts.push(m.unwrap()),
}
}
}
VolumeMount::Long {
volume_type,
source,
target,
read_only,
bind,
volume,
tmpfs,
..
} => match volume_type {
VolumeType::Tmpfs => {
let mut opts: Vec<String> = Vec::new();
if let Some(t) = tmpfs {
if let Some(size) = t.size {
opts.push(format!("size={size}"));
}
if let Some(mode) = t.mode {
opts.push(format!("mode={mode:o}"));
}
}
if read_only.unwrap_or(false) {
opts.push("ro".into());
}
mounts.push(Mount {
mount_type: "tmpfs".into(),
source: None,
destination: target.clone(),
options: opts,
});
}
VolumeType::Bind => {
let src = source.as_deref().unwrap_or("");
if let Some(b) = bind {
if b.create_host_path.unwrap_or(false) && !src.is_empty() {
let abs = super::container::resolve_bind_source(src, base_dir);
if let Err(e) = std::fs::create_dir_all(&abs) {
tracing::warn!("create_host_path: failed to create {abs}: {e}");
}
}
}
let mut opts = access_opts(*read_only);
extend_bind_opts_str(&mut opts, bind.as_ref());
mounts.push(Mount {
mount_type: "bind".into(),
source: Some(src.to_string()),
destination: target.clone(),
options: opts,
});
}
VolumeType::Volume => {
let mut opts = access_opts(*read_only);
extend_volume_opts_str(&mut opts, volume.as_ref());
named.push(NamedVolume {
name: source.clone().unwrap_or_default(),
dest: target.clone(),
options: opts,
sub_path: volume.as_ref().and_then(|v| v.subpath.clone()),
});
}
VolumeType::Npipe => {
mounts.push(Mount {
mount_type: "npipe".into(),
source: source.clone(),
destination: target.clone(),
options: vec![],
});
}
VolumeType::Cluster => {
mounts.push(Mount {
mount_type: "cluster".into(),
source: source.clone(),
destination: target.clone(),
options: vec![],
});
}
},
}
}
for path in service.tmpfs.to_list() {
mounts.push(Mount {
mount_type: "tmpfs".into(),
source: None,
destination: path,
options: vec![],
});
}
for bind in secret_binds.iter().chain(config_binds.iter()) {
if let Some(m) = parse_bind_string(bind) {
mounts.push(m);
}
}
(mounts, named)
}
#[cfg(test)]
mod tests {
use super::build_mounts_all;
use crate::compose::types::{BindOptions, Service, VolumeMount, VolumeOptions, VolumeType};
use std::path::Path;
fn svc_with_volumes(vols: Vec<VolumeMount>) -> Service {
Service {
volumes: vols,
..Default::default()
}
}
#[test]
fn short_form_bind_passthrough() {
let svc = svc_with_volumes(vec![VolumeMount::Short("./data:/app/data".into())]);
let (mounts, named) = build_mounts_all(&svc, Path::new("/base"), &[], &[]);
assert_eq!(mounts.len(), 1);
assert!(named.is_empty());
assert_eq!(mounts[0].mount_type, "bind");
assert_eq!(mounts[0].destination, "/app/data");
}
#[test]
fn short_form_named_volume() {
let svc = svc_with_volumes(vec![VolumeMount::Short("myvolume:/data".into())]);
let (mounts, named) = build_mounts_all(&svc, Path::new("/base"), &[], &[]);
assert!(mounts.is_empty());
assert_eq!(named.len(), 1);
assert_eq!(named[0].name, "myvolume");
assert_eq!(named[0].dest, "/data");
}
#[test]
fn long_form_bind_read_only() {
let svc = svc_with_volumes(vec![VolumeMount::Long {
volume_type: VolumeType::Bind,
source: Some("/host/path".into()),
target: "/container/path".into(),
read_only: Some(true),
bind: None,
volume: None,
tmpfs: None,
consistency: None,
}]);
let (mounts, _) = build_mounts_all(&svc, Path::new("/base"), &[], &[]);
assert_eq!(mounts.len(), 1);
assert_eq!(mounts[0].mount_type, "bind");
assert!(mounts[0].options.contains(&"ro".to_string()));
assert_eq!(mounts[0].destination, "/container/path");
}
#[test]
fn long_form_bind_with_propagation() {
let svc = svc_with_volumes(vec![VolumeMount::Long {
volume_type: VolumeType::Bind,
source: Some("/host".into()),
target: "/cont".into(),
read_only: Some(false),
bind: Some(BindOptions {
propagation: Some("rshared".into()),
create_host_path: None,
selinux: None,
}),
volume: None,
tmpfs: None,
consistency: None,
}]);
let (mounts, _) = build_mounts_all(&svc, Path::new("/base"), &[], &[]);
assert!(mounts[0].options.contains(&"rshared".to_string()));
}
#[test]
fn long_form_volume_nocopy() {
let svc = svc_with_volumes(vec![VolumeMount::Long {
volume_type: VolumeType::Volume,
source: Some("myvolume".into()),
target: "/data".into(),
read_only: None,
bind: None,
volume: Some(VolumeOptions {
nocopy: Some(true),
..Default::default()
}),
tmpfs: None,
consistency: None,
}]);
let (mounts, named) = build_mounts_all(&svc, Path::new("/base"), &[], &[]);
assert!(mounts.is_empty());
assert_eq!(named.len(), 1);
assert_eq!(named[0].name, "myvolume");
assert!(named[0].options.contains(&"nocopy".to_string()));
}
#[test]
fn long_form_volume_subpath_forwarded() {
let svc = svc_with_volumes(vec![VolumeMount::Long {
volume_type: VolumeType::Volume,
source: Some("myvolume".into()),
target: "/data".into(),
read_only: None,
bind: None,
volume: Some(VolumeOptions {
subpath: Some("nested/dir".into()),
..Default::default()
}),
tmpfs: None,
consistency: None,
}]);
let (_, named) = build_mounts_all(&svc, Path::new("/base"), &[], &[]);
assert_eq!(named.len(), 1);
assert_eq!(named[0].sub_path.as_deref(), Some("nested/dir"));
}
#[test]
fn tmpfs_type_becomes_tmpfs_mount() {
use crate::compose::types::TmpfsOptions;
let svc = svc_with_volumes(vec![VolumeMount::Long {
volume_type: VolumeType::Tmpfs,
source: None,
target: "/tmp/cache".into(),
read_only: None,
bind: None,
volume: None,
tmpfs: Some(TmpfsOptions {
size: Some(65536),
mode: Some(0o700),
}),
consistency: None,
}]);
let (mounts, _) = build_mounts_all(&svc, Path::new("/base"), &[], &[]);
assert_eq!(mounts.len(), 1);
assert_eq!(mounts[0].mount_type, "tmpfs");
assert_eq!(mounts[0].destination, "/tmp/cache");
assert!(mounts[0].options.iter().any(|o| o.starts_with("size=")));
assert!(mounts[0].options.iter().any(|o| o.starts_with("mode=")));
}
#[test]
fn secret_binds_appended() {
let svc = svc_with_volumes(vec![]);
let secret = "/run/secrets/mydb:/run/secrets/mydb:ro".to_string();
let (mounts, _) = build_mounts_all(&svc, Path::new("/base"), &[secret], &[]);
assert_eq!(mounts.len(), 1);
assert_eq!(mounts[0].destination, "/run/secrets/mydb");
}
#[test]
fn create_host_path_creates_directory() {
let dir = tempfile::tempdir().unwrap();
let rel = "subdir/nested";
let svc = svc_with_volumes(vec![VolumeMount::Long {
volume_type: VolumeType::Bind,
source: Some(rel.into()),
target: "/cont".into(),
read_only: None,
bind: Some(BindOptions {
propagation: None,
create_host_path: Some(true),
selinux: None,
}),
volume: None,
tmpfs: None,
consistency: None,
}]);
build_mounts_all(&svc, dir.path(), &[], &[]);
assert!(dir.path().join(rel).exists());
}
#[test]
fn top_level_tmpfs_shorthand() {
use crate::compose::types::StringOrList;
let svc = Service {
tmpfs: StringOrList::List(vec!["/tmp".into(), "/run".into()]),
..Default::default()
};
let (mounts, _) = build_mounts_all(&svc, Path::new("/base"), &[], &[]);
assert_eq!(mounts.len(), 2);
assert_eq!(mounts[0].mount_type, "tmpfs");
assert_eq!(mounts[0].destination, "/tmp");
assert_eq!(mounts[1].destination, "/run");
}
#[test]
fn top_level_tmpfs_single_string() {
use crate::compose::types::StringOrList;
let svc = Service {
tmpfs: StringOrList::Single("/tmp".into()),
..Default::default()
};
let (mounts, _) = build_mounts_all(&svc, Path::new("/base"), &[], &[]);
assert_eq!(mounts.len(), 1);
assert_eq!(mounts[0].mount_type, "tmpfs");
assert_eq!(mounts[0].destination, "/tmp");
}
}