use std::fs::File;
use std::io::{BufWriter, ErrorKind, Write};
use std::path::{Path, PathBuf};
use anyhow::{Result, anyhow};
use libcgroups::common::{CgroupSetup, get_cgroup_setup};
use libcontainer::oci_spec::runtime::{
LinuxBuilder, LinuxDeviceCgroupBuilder, LinuxIdMappingBuilder, LinuxNamespace,
LinuxNamespaceBuilder, LinuxNamespaceType, Mount, Spec,
};
use libcontainer::syscall::syscall::Syscall;
use serde_json::to_writer_pretty;
pub fn get_default() -> Result<Spec> {
let spec = Spec::default();
normalize_spec(spec)
}
pub fn get_rootless(syscall: &dyn Syscall) -> Result<Spec> {
let mut namespaces: Vec<LinuxNamespace> =
libcontainer::oci_spec::runtime::get_default_namespaces()
.into_iter()
.filter(|ns| {
ns.typ() != LinuxNamespaceType::Network && ns.typ() != LinuxNamespaceType::User
})
.collect();
namespaces.push(
LinuxNamespaceBuilder::default()
.typ(LinuxNamespaceType::User)
.build()?,
);
let uid = syscall.get_euid().as_raw();
let gid = syscall.get_egid().as_raw();
let linux = LinuxBuilder::default()
.namespaces(namespaces)
.uid_mappings(vec![
LinuxIdMappingBuilder::default()
.host_id(uid)
.container_id(0_u32)
.size(1_u32)
.build()?,
])
.gid_mappings(vec![
LinuxIdMappingBuilder::default()
.host_id(gid)
.container_id(0_u32)
.size(1_u32)
.build()?,
])
.build()?;
let mut mounts: Vec<Mount> = libcontainer::oci_spec::runtime::get_default_mounts();
for mount in &mut mounts {
if mount.destination().eq(Path::new("/sys")) {
mount
.set_source(Some(PathBuf::from("/sys")))
.set_typ(Some(String::from("none")))
.set_options(Some(vec![
"rbind".to_string(),
"nosuid".to_string(),
"noexec".to_string(),
"nodev".to_string(),
"ro".to_string(),
]));
} else {
let options: Vec<String> = mount
.options()
.as_ref()
.unwrap_or(&vec![])
.iter()
.filter(|&o| !o.starts_with("gid=") && !o.starts_with("uid="))
.map(|o| o.to_string())
.collect();
mount.set_options(Some(options));
}
}
let mut spec = get_default()?;
spec.set_linux(Some(linux)).set_mounts(Some(mounts));
normalize_spec(spec)
}
fn normalize_spec(mut spec: Spec) -> Result<Spec> {
if let Some(process) = spec.process_mut()
&& let Some(mut capabilities) = process.capabilities().clone()
{
capabilities.set_inheritable(None);
capabilities.set_ambient(None);
process.set_capabilities(Some(capabilities));
}
if let Some(linux) = spec.linux_mut() {
if let Some(resources) = linux.resources_mut() {
let default_device = LinuxDeviceCgroupBuilder::default()
.allow(false)
.access("rwm".to_string())
.build()?;
resources.set_devices(Some(vec![default_device]));
}
if let Some(namespaces) = linux.namespaces_mut() {
let setup = get_cgroup_setup().unwrap_or(CgroupSetup::Legacy);
if !matches!(setup, CgroupSetup::Unified) {
namespaces.retain(|ns| ns.typ() != LinuxNamespaceType::Cgroup);
}
}
}
Ok(spec)
}
pub fn spec(args: liboci_cli::Spec, syscall: &dyn Syscall) -> Result<()> {
let spec = if args.rootless {
get_rootless(syscall)?
} else {
get_default()?
};
let path = match args.bundle {
Some(bundle) => bundle.join("config.json"),
None => PathBuf::from("config.json"),
};
let file = File::create_new(&path).map_err(|e| match e.kind() {
ErrorKind::AlreadyExists => anyhow!("File `config.json` already exists"),
_ => anyhow!(e),
})?;
let mut writer = BufWriter::new(file);
to_writer_pretty(&mut writer, &spec)?;
writer.flush()?;
Ok(())
}
#[cfg(test)]
mod tests {
use libcontainer::syscall::syscall::create_syscall;
use serial_test::serial;
use super::*;
#[test]
#[serial]
fn test_spec_json() -> Result<()> {
let tmpdir = tempfile::tempdir().expect("failed to create temp dir");
let args = liboci_cli::Spec {
bundle: Some(tmpdir.path().to_path_buf()),
rootless: true,
};
let syscall = create_syscall();
spec(args, syscall.as_ref()).expect("failed to run spec subcommand");
let config_path = tmpdir.path().join("config.json");
assert!(config_path.is_file());
Ok(())
}
#[test]
#[serial]
fn test_spec_json_already_exists() -> Result<()> {
let tmpdir = tempfile::tempdir().expect("failed to create temp dir");
let args = liboci_cli::Spec {
bundle: Some(tmpdir.path().to_path_buf()),
rootless: true,
};
let syscall = create_syscall();
let config_path = tmpdir.path().join("config.json");
File::create(config_path).expect("failed to create initial config.json");
let result = spec(args, syscall.as_ref());
assert!(
result.is_err(),
"spec subcommand should fail if config.json already exists"
);
let err_msg = result.unwrap_err().to_string();
assert_eq!(err_msg, "File `config.json` already exists");
Ok(())
}
}