diff --git a/Cargo.lock b/Cargo.lock index c6f2e6a..c881a02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -183,6 +183,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.38" @@ -894,6 +900,18 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -968,6 +986,7 @@ dependencies = [ "home", "lazy_static", "log", + "nix", "oci-client", "rand", "regex", diff --git a/Cargo.toml b/Cargo.toml index ce2175c..ce2c2b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ fs2 = "0.4.3" home = "0.5.9" lazy_static = "1.5.0" log = "0.4.22" +nix = { version = "0.29.0", features = ["mount", "user"] } oci-client = "0.13.0" rand = "0.8.5" regex = "1.10.6" diff --git a/README.md b/README.md index 40e2ed9..36f90a4 100644 --- a/README.md +++ b/README.md @@ -25,5 +25,7 @@ ocibuilder config --port 4444/tcp $ctr1 | containers | List the working containers and their base images. | from | Creates a new working container either from scratch or using an image. | images | List images in local storage. +| mount | Mounts a working container's root filesystem for manipulation. +| umount | Unmounts the root file system of the specified working containers. | pull | Pull an image from the specified registry. | reset | Reset local storage. diff --git a/src/builder/mod.rs b/src/builder/mod.rs index e9174d9..a41d761 100644 --- a/src/builder/mod.rs +++ b/src/builder/mod.rs @@ -1,6 +1,7 @@ pub mod config; pub mod dist_client; pub mod from; +pub mod mount; pub mod oci; pub mod pull; pub mod reset; diff --git a/src/builder/mount.rs b/src/builder/mount.rs new file mode 100644 index 0000000..da6c19a --- /dev/null +++ b/src/builder/mount.rs @@ -0,0 +1,140 @@ +use std::{ + ffi::CStr, + fs::File, + io::{BufRead, BufReader}, + path::{Path, PathBuf}, +}; + +use log::debug; + +use crate::{ + error::{BuilderError, BuilderResult}, + utils, +}; + +use super::oci::OCIBuilder; + +impl OCIBuilder { + pub fn mount(&self, container: &str) -> BuilderResult { + if nix::unistd::geteuid().as_raw() != 0 { + return Err(BuilderError::MountRootlessError()); + } + + self.lock()?; + + let cnt: crate::container::containers::Container = + self.container_store().container_exist(container)?; + + let top_layer = cnt.top_layer(); + let top_layer_digest = utils::digest::Digest::new(&format!("sha256:{}", top_layer))?; + let mount_point = self.layer_store().overlay_merged_path(&top_layer_digest); + debug!("container {:.12} mount point: {:?}", cnt.id(), mount_point); + + let workdir_path = self.layer_store().overlay_work_path(&top_layer_digest); + debug!( + "container {:.12} work directory: {:?}", + cnt.id(), + workdir_path + ); + + let upperdir_path = self.layer_store().overlay_diff_path(&top_layer_digest); + debug!( + "container {:.12} upper directory: {:?}", + cnt.id(), + upperdir_path + ); + + let mut lowerdir_paths: Vec = Vec::new(); + + for layer in cnt.rootfs_diff() { + let layer_digest = utils::digest::Digest::new(&layer)?; + let layer_diff_path = self + .layer_store() + .overlay_diff_path(&layer_digest) + .display() + .to_string(); + lowerdir_paths.push(layer_diff_path); + } + + if is_mounted(&mount_point)? { + self.umount(container)?; + } + + let mount_options = format!( + "lowerdir={},upperdir={},workdir={}", + lowerdir_paths.join(":"), + upperdir_path.display(), + workdir_path.display(), + ); + + debug!( + "container {:.12} mount options: {:?}", + cnt.id(), + mount_options + ); + + match nix::mount::mount( + Some(CStr::from_bytes_with_nul(b"overlay\0").unwrap()), + mount_point.display().to_string().as_bytes(), + Some(CStr::from_bytes_with_nul(b"overlay\0").unwrap()), + nix::mount::MsFlags::empty(), + Some(mount_options.as_bytes()), + ) { + Ok(_) => {} + Err(err) => return Err(BuilderError::MountUmountError(err.to_string())), + } + + self.unlock()?; + Ok(mount_point) + } + + pub fn umount(&self, container: &str) -> BuilderResult<()> { + self.lock()?; + + let cnt = self.container_store().container_exist(container)?; + let top_layer = cnt.top_layer(); + let top_layer_digest = utils::digest::Digest::new(&format!("sha256:{}", top_layer))?; + let mount_point = self.layer_store().overlay_merged_path(&top_layer_digest); + + if is_mounted(&mount_point)? { + debug!( + "container {:.12} filesystem umount from {:?}", + cnt.id(), + mount_point + ); + + match nix::mount::umount(mount_point.display().to_string().as_bytes()) { + Ok(_) => {} + Err(err) => return Err(BuilderError::MountUmountError(err.to_string())), + } + } + + self.unlock()?; + Ok(()) + } +} + +fn is_mounted(source: &Path) -> BuilderResult { + let proc_mounts_file = "/proc/mounts"; + let proc_mounts = match File::open(proc_mounts_file) { + Ok(pm) => pm, + Err(err) => return Err(BuilderError::IoError(PathBuf::from(proc_mounts_file), err)), + }; + + for line_result in BufReader::new(proc_mounts).lines() { + match line_result { + Ok(line) => { + let mount_info: Vec<&str> = line.split_whitespace().collect(); + if !mount_info.is_empty() + && mount_info.len() == 6 + && mount_info[1] == source.display().to_string() + { + return Ok(true); + } + } + Err(err) => return Err(BuilderError::IoError(PathBuf::from(proc_mounts_file), err)), + } + } + + Ok(false) +} diff --git a/src/commands/from.rs b/src/commands/from.rs index 7d74a90..63658aa 100644 --- a/src/commands/from.rs +++ b/src/commands/from.rs @@ -6,22 +6,23 @@ use crate::{builder, error::BuilderResult, utils}; #[derive(Parser, Debug)] pub struct From { - image_name: String, + // image name or ID + image: String, /// container name #[clap(short, long)] name: Option, } impl From { - pub fn new(image_name: String, name: Option) -> Self { - Self { image_name, name } + pub fn new(image: String, name: Option) -> Self { + Self { image, name } } pub async fn exec(&self, root_dir: Option) -> BuilderResult<()> { let root_dir_path = utils::get_root_dir(root_dir); let builder = builder::oci::OCIBuilder::new(root_dir_path)?; - let cnt_name = builder.from(&self.image_name, self.name.clone()).await?; + let cnt_name = builder.from(&self.image, self.name.clone()).await?; println!("{}", cnt_name); Ok(()) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index a11ba0b..dec5367 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -5,3 +5,4 @@ pub mod images; pub mod mount; pub mod pull; pub mod reset; +pub mod umount; diff --git a/src/commands/mount.rs b/src/commands/mount.rs index a1cb05a..69a1293 100644 --- a/src/commands/mount.rs +++ b/src/commands/mount.rs @@ -7,18 +7,20 @@ use crate::{builder, error::BuilderResult, utils}; #[derive(Parser, Debug)] pub struct Mount { /// container name or ID - #[clap(short, long)] - name: Option, + container: String, } impl Mount { - pub fn new(name: Option) -> Self { - Self { name } + pub fn new(container: String) -> Self { + Self { container } } pub fn exec(&self, root_dir: Option) -> BuilderResult<()> { let root_dir_path = utils::get_root_dir(root_dir); - let _builder = builder::oci::OCIBuilder::new(root_dir_path)?; + let builder = builder::oci::OCIBuilder::new(root_dir_path)?; + + let mount_point = builder.mount(&self.container)?; + println!("mount point: {:?}", mount_point); Ok(()) } diff --git a/src/commands/pull.rs b/src/commands/pull.rs index 3e948a9..3a4d70e 100644 --- a/src/commands/pull.rs +++ b/src/commands/pull.rs @@ -7,18 +7,16 @@ use crate::{builder, error::BuilderResult, utils}; #[derive(Parser, Debug)] pub struct Pull { - image_name: String, + /// image name + image: String, /// Using http insecure connection instead of https #[clap(short, long)] insecure: bool, } impl Pull { - pub fn new(image_name: String, insecure: bool) -> Self { - Self { - image_name, - insecure, - } + pub fn new(image: String, insecure: bool) -> Self { + Self { image, insecure } } pub async fn exec(&self, root_dir: Option) -> BuilderResult<()> { @@ -27,7 +25,7 @@ impl Pull { let root_dir_path = utils::get_root_dir(root_dir); let builder = builder::oci::OCIBuilder::new(root_dir_path)?; - builder.pull(&self.image_name, &self.insecure).await?; + builder.pull(&self.image, &self.insecure).await?; Ok(()) } diff --git a/src/commands/umount.rs b/src/commands/umount.rs new file mode 100644 index 0000000..aabc5c8 --- /dev/null +++ b/src/commands/umount.rs @@ -0,0 +1,26 @@ +use std::ffi::OsString; + +use clap::Parser; + +use crate::{builder, error::BuilderResult, utils}; + +#[derive(Parser, Debug)] +pub struct Umount { + /// container name or ID + container: String, +} + +impl Umount { + pub fn new(container: String) -> Self { + Self { container } + } + + pub fn exec(&self, root_dir: Option) -> BuilderResult<()> { + let root_dir_path = utils::get_root_dir(root_dir); + let builder = builder::oci::OCIBuilder::new(root_dir_path)?; + + builder.umount(&self.container)?; + + Ok(()) + } +} diff --git a/src/container/containers.rs b/src/container/containers.rs index f02b38d..8a2ea05 100644 --- a/src/container/containers.rs +++ b/src/container/containers.rs @@ -142,6 +142,19 @@ impl ContainerStore { Err(BuilderError::ContainerNotFound(name_or_id.to_string())) } + pub fn container_exist(&self, name_or_id: &str) -> BuilderResult { + let cnt_id = self.container_digest(name_or_id)?; + let cnt_list = self.containers()?; + + for cnt in cnt_list { + if cnt_id.encoded == cnt.id { + return Ok(cnt); + } + } + + Err(BuilderError::ContainerNotFound(name_or_id.to_string())) + } + pub fn containers_path(&self) -> PathBuf { let mut containers_file = self.cstore_path().clone(); containers_file.push(CONTAINERS_FILENAME); diff --git a/src/error/mod.rs b/src/error/mod.rs index c4d152f..9d5f5c6 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -35,6 +35,12 @@ pub enum BuilderError { #[error("invalid digest: {0}")] InvalidDigest(String), + #[error("mount/umount error: {0}")] + MountUmountError(String), + + #[error("cannot mount using driver overlay in rootless mode.")] + MountRootlessError(), + // container store errors #[error("container store error: {0}")] ContainerStoreError(String), diff --git a/src/main.rs b/src/main.rs index e1e42a0..95f3d6b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use std::ffi::OsString; use clap::{Parser, Subcommand}; -use ocibuilder::commands::{config, containers, from, images, mount, pull, reset}; +use ocibuilder::commands::{config, containers, from, images, mount, pull, reset, umount}; #[derive(Parser, Debug)] #[clap(version = env!("CARGO_PKG_VERSION"), about)] @@ -38,6 +38,9 @@ enum SubCommand { /// Mounts a working container's root filesystem for manipulation Mount(mount::Mount), + + /// Unmounts the root file system of the specified working containers + Umount(umount::Umount), } #[tokio::main] @@ -55,6 +58,7 @@ async fn main() { SubCommand::Pull(pull) => pull.exec(root_dir).await, SubCommand::Reset(reset) => reset.exec(root_dir), SubCommand::Mount(mount) => mount.exec(root_dir), + SubCommand::Umount(umount) => umount.exec(root_dir), }; match result {