From 0d6cf1036f87ce8351ac694f80a8bd6e369ab6d7 Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Fri, 15 Mar 2024 14:56:59 +0100 Subject: [PATCH 01/21] chore: bootstrap fs-gen project Signed-off-by: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com> --- Cargo.toml | 2 +- src/fs-gen/Cargo.toml | 8 ++++++++ src/fs-gen/src/main.rs | 3 +++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 src/fs-gen/Cargo.toml create mode 100644 src/fs-gen/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 94d73a2..53031a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] -members = ["src/api", "src/vmm", "src/cli"] +members = ["src/api", "src/vmm", "src/cli", "src/fs-gen"] resolver = "2" diff --git a/src/fs-gen/Cargo.toml b/src/fs-gen/Cargo.toml new file mode 100644 index 0000000..e6c8ef2 --- /dev/null +++ b/src/fs-gen/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "fs-gen" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/src/fs-gen/src/main.rs b/src/fs-gen/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/src/fs-gen/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} From bc92ea80a9a853c0eefd454a23896aa1349eb5be Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Mon, 18 Mar 2024 08:46:49 +0100 Subject: [PATCH 02/21] feat(cli): basic argument handling Signed-off-by: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com> --- src/fs-gen/Cargo.toml | 1 + src/fs-gen/src/cli_args.rs | 23 +++++++++++++++++++++++ src/fs-gen/src/main.rs | 9 ++++++++- 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 src/fs-gen/src/cli_args.rs diff --git a/src/fs-gen/Cargo.toml b/src/fs-gen/Cargo.toml index e6c8ef2..23a50ac 100644 --- a/src/fs-gen/Cargo.toml +++ b/src/fs-gen/Cargo.toml @@ -6,3 +6,4 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +clap = { version = "4.5.3", features = ["derive", "wrap_help", "string"] } diff --git a/src/fs-gen/src/cli_args.rs b/src/fs-gen/src/cli_args.rs new file mode 100644 index 0000000..b7279aa --- /dev/null +++ b/src/fs-gen/src/cli_args.rs @@ -0,0 +1,23 @@ +use std::{env, path::PathBuf}; + +use clap::{command, Parser}; +/// Convert an OCI image into a CPIO file +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +pub struct CliArgs { + /// The name of the image to download + #[arg(short, long)] + pub image_name: String, + + /// The path to the output file + #[arg(short, long, default_value=get_default_log_path().into_os_string())] + pub ouput_file: PathBuf, +} + +/// Get the default output path for the cpio file. +fn get_default_log_path() -> PathBuf { + let mut path = env::current_exe().unwrap(); + path.pop(); + path.push("output.cpio"); + path +} diff --git a/src/fs-gen/src/main.rs b/src/fs-gen/src/main.rs index e7a11a9..8623f70 100644 --- a/src/fs-gen/src/main.rs +++ b/src/fs-gen/src/main.rs @@ -1,3 +1,10 @@ +use clap::Parser; + +use crate::cli_args::CliArgs; + +mod cli_args; + fn main() { - println!("Hello, world!"); + let args = CliArgs::parse(); + println!("Hello, world!, {:?}", args); } From 0cde34581aaf1fbbe970a0fcdc87221df20731e9 Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Thu, 28 Mar 2024 12:26:51 +0100 Subject: [PATCH 03/21] feat(validation): verify image name Signed-off-by: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com> --- src/fs-gen/Cargo.toml | 3 +++ src/fs-gen/src/cli_args.rs | 28 +++++++++++++++++++++++++++- src/fs-gen/src/main.rs | 2 +- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/fs-gen/Cargo.toml b/src/fs-gen/Cargo.toml index 23a50ac..0a0b17d 100644 --- a/src/fs-gen/Cargo.toml +++ b/src/fs-gen/Cargo.toml @@ -7,3 +7,6 @@ edition = "2021" [dependencies] clap = { version = "4.5.3", features = ["derive", "wrap_help", "string"] } +once_cell = "1.19.0" +regex = "1.10.4" +validator = { version = "0.17.0", features = ["derive"] } diff --git a/src/fs-gen/src/cli_args.rs b/src/fs-gen/src/cli_args.rs index b7279aa..350f6c7 100644 --- a/src/fs-gen/src/cli_args.rs +++ b/src/fs-gen/src/cli_args.rs @@ -1,12 +1,24 @@ use std::{env, path::PathBuf}; use clap::{command, Parser}; +use regex::Regex; + +use once_cell::sync::Lazy; +use validator::Validate; + +// So, for any of you who may be scared, this is the regex from the OCI Distribution Sepcification for the image name + the tag +static RE_IMAGE_NAME: Lazy = Lazy::new(|| { + Regex::new(r"[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*:[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}").unwrap() +}); + /// Convert an OCI image into a CPIO file -#[derive(Parser, Debug)] +#[derive(Parser, Debug, Validate)] #[command(version, about, long_about = None)] pub struct CliArgs { /// The name of the image to download + #[arg(short, long)] + #[validate(regex(path = *RE_IMAGE_NAME))] pub image_name: String, /// The path to the output file @@ -14,6 +26,20 @@ pub struct CliArgs { pub ouput_file: PathBuf, } +impl CliArgs { + /// Get the cli arguments with additional validation + pub fn get_args() -> Self { + let args = CliArgs::parse(); + + let validation = args.validate(); + if validation.is_err() { + panic!("Invalid arguments: {}", validation.expect_err("wut")); + } + + args + } +} + /// Get the default output path for the cpio file. fn get_default_log_path() -> PathBuf { let mut path = env::current_exe().unwrap(); diff --git a/src/fs-gen/src/main.rs b/src/fs-gen/src/main.rs index 8623f70..da6e973 100644 --- a/src/fs-gen/src/main.rs +++ b/src/fs-gen/src/main.rs @@ -5,6 +5,6 @@ use crate::cli_args::CliArgs; mod cli_args; fn main() { - let args = CliArgs::parse(); + let args = CliArgs::get_args(); println!("Hello, world!, {:?}", args); } From d7352e6d9a5acc1ebafde9da9f387d182929e43c Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Sat, 30 Mar 2024 17:43:14 +0100 Subject: [PATCH 04/21] Update Cargo.toml, image_builder.rs, and main.rs Signed-off-by: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com> --- src/fs-gen/Cargo.toml | 3 +++ src/fs-gen/src/image_builder.rs | 22 ++++++++++++++++++++++ src/fs-gen/src/main.rs | 13 ++++++++++++- 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 src/fs-gen/src/image_builder.rs diff --git a/src/fs-gen/Cargo.toml b/src/fs-gen/Cargo.toml index 0a0b17d..9790eb3 100644 --- a/src/fs-gen/Cargo.toml +++ b/src/fs-gen/Cargo.toml @@ -9,4 +9,7 @@ edition = "2021" clap = { version = "4.5.3", features = ["derive", "wrap_help", "string"] } once_cell = "1.19.0" regex = "1.10.4" +serde = { version = "1.0.197", features = ["derive"] } +serde_json = "1.0.115" validator = { version = "0.17.0", features = ["derive"] } +vfs = "0.12.0" diff --git a/src/fs-gen/src/image_builder.rs b/src/fs-gen/src/image_builder.rs new file mode 100644 index 0000000..8319048 --- /dev/null +++ b/src/fs-gen/src/image_builder.rs @@ -0,0 +1,22 @@ +use std::path::{Path, PathBuf}; + +use vfs::{FileSystem, OverlayFS, PhysicalFS, VfsPath}; + +pub fn build_new_image(blob_paths: &Vec, output_folder: &Path) { + let virtual_paths = blob_paths + .iter() + .map(|p| VfsPath::new(PhysicalFS::new(p))) + .collect::>(); + + let vfs = OverlayFS::new(&virtual_paths); + let toto: Vec = vfs.read_dir("/home").unwrap().collect(); + println!("{:?}", toto); + println!("{:?}", vfs); + let overlay_root: VfsPath = vfs.into(); + + let output_vpath = VfsPath::new(PhysicalFS::new(output_folder)); + + overlay_root + .copy_dir(&output_vpath) + .expect("Failed to copy the blobs !"); +} diff --git a/src/fs-gen/src/main.rs b/src/fs-gen/src/main.rs index da6e973..c827c2c 100644 --- a/src/fs-gen/src/main.rs +++ b/src/fs-gen/src/main.rs @@ -1,10 +1,21 @@ +use std::{ + path::{Path, PathBuf}, + str::FromStr, +}; + use clap::Parser; -use crate::cli_args::CliArgs; +use crate::{cli_args::CliArgs, image_builder::build_new_image}; mod cli_args; +mod image_builder; fn main() { let args = CliArgs::get_args(); println!("Hello, world!, {:?}", args); + + let paths: Vec = + vec![PathBuf::from_str("../../image-gen/blobs/sha256/layer_1").unwrap()]; + + build_new_image(&paths, &PathBuf::from_str("./titi").unwrap()); } From adb5c82d540dc2d2059afc58616966638cfe6ccd Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Wed, 3 Apr 2024 22:10:37 +0200 Subject: [PATCH 05/21] wip: fuse overlayfs test Signed-off-by: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com> --- src/fs-gen/Cargo.toml | 2 + src/fs-gen/src/image_builder.rs | 16 +++-- src/fs-gen/src/main.rs | 114 ++++++++++++++++++++++++++++++-- 3 files changed, 123 insertions(+), 9 deletions(-) diff --git a/src/fs-gen/Cargo.toml b/src/fs-gen/Cargo.toml index 9790eb3..2f711a4 100644 --- a/src/fs-gen/Cargo.toml +++ b/src/fs-gen/Cargo.toml @@ -7,9 +7,11 @@ edition = "2021" [dependencies] clap = { version = "4.5.3", features = ["derive", "wrap_help", "string"] } +fuse-backend-rs = "0.12.0" once_cell = "1.19.0" regex = "1.10.4" serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.115" +signal-hook = "0.3.17" validator = { version = "0.17.0", features = ["derive"] } vfs = "0.12.0" diff --git a/src/fs-gen/src/image_builder.rs b/src/fs-gen/src/image_builder.rs index 8319048..a6796aa 100644 --- a/src/fs-gen/src/image_builder.rs +++ b/src/fs-gen/src/image_builder.rs @@ -2,21 +2,29 @@ use std::path::{Path, PathBuf}; use vfs::{FileSystem, OverlayFS, PhysicalFS, VfsPath}; -pub fn build_new_image(blob_paths: &Vec, output_folder: &Path) { +pub fn build_new_image(blob_paths: &[PathBuf], output_folder: &Path) { let virtual_paths = blob_paths .iter() .map(|p| VfsPath::new(PhysicalFS::new(p))) .collect::>(); let vfs = OverlayFS::new(&virtual_paths); - let toto: Vec = vfs.read_dir("/home").unwrap().collect(); + + let toto: Vec = vfs.read_dir("").unwrap().collect(); + // copy from the overlay fs to the output folder + println!("{:?}", toto); println!("{:?}", vfs); let overlay_root: VfsPath = vfs.into(); - let output_vpath = VfsPath::new(PhysicalFS::new(output_folder)); + println!("{:?}", overlay_root); + + let output_vpath = VfsPath::new(PhysicalFS::new(".")); + println!("{:?}", output_vpath.as_str()); overlay_root - .copy_dir(&output_vpath) + .join("bin") + .unwrap() + .copy_dir(&output_vpath.join("titi").unwrap()) .expect("Failed to copy the blobs !"); } diff --git a/src/fs-gen/src/main.rs b/src/fs-gen/src/main.rs index c827c2c..4de6ff0 100644 --- a/src/fs-gen/src/main.rs +++ b/src/fs-gen/src/main.rs @@ -1,21 +1,125 @@ use std::{ + fs::{self, File}, + io::Result, path::{Path, PathBuf}, + process::exit, str::FromStr, + sync::Arc, + thread::{self, sleep}, + time::Duration, }; use clap::Parser; +use fuse_backend_rs::{ + api::{filesystem::Layer, server::Server}, + overlayfs::{config::Config, OverlayFs}, + passthrough::{self, PassthroughFs}, + transport::{FuseChannel, FuseSession}, +}; use crate::{cli_args::CliArgs, image_builder::build_new_image}; +use signal_hook::{self, consts::TERM_SIGNALS, iterator::Signals}; mod cli_args; mod image_builder; +pub struct FuseServer { + server: Arc>>, + ch: FuseChannel, +} + +type BoxedLayer = Box + Send + Sync>; + +fn new_passthroughfs_layer(rootdir: &str) -> Result { + let mut config = passthrough::Config::default(); + config.root_dir = String::from(rootdir); + // enable xattr + config.xattr = true; + config.do_import = true; + let fs = Box::new(PassthroughFs::<()>::new(config)?); + fs.import()?; + Ok(fs as BoxedLayer) +} + fn main() { - let args = CliArgs::get_args(); - println!("Hello, world!, {:?}", args); + // let args = CliArgs::get_args(); + // println!("Hello, world!, {:?}", args); + + // let paths: Vec = + // vec![PathBuf::from_str("/home/spse/Downloads/image-gen/layer").unwrap()]; + + // //let _ = fs::create_dir("./titi"); + + // build_new_image(&paths, &PathBuf::from_str("./titi").unwrap()); - let paths: Vec = - vec![PathBuf::from_str("../../image-gen/blobs/sha256/layer_1").unwrap()]; + let upper_layer = + Arc::new(new_passthroughfs_layer("/home/spse/Downloads/image-gen/layer_2").unwrap()); + let mut lower_layers = Vec::new(); + for lower in vec!["/home/spse/Downloads/image-gen/layer"] { + lower_layers.push(Arc::new(new_passthroughfs_layer(&lower).unwrap())); + } + + let mut config = Config::default(); + config.work = "/work".into(); + config.mountpoint = "/tmp/overlay_test2".into(); + config.do_import = true; + + print!("new overlay fs\n"); + let fs = OverlayFs::new(Some(upper_layer), lower_layers, config).unwrap(); + print!("init root inode\n"); + fs.import().unwrap(); + + print!("open fuse session\n"); + let mut se = + FuseSession::new(Path::new("/tmp/overlay_test2"), "toto_overlay2", "", false).unwrap(); + print!("session opened\n"); + se.mount().unwrap(); + + let mut server = FuseServer { + server: Arc::new(Server::new(Arc::new(fs))), + ch: se.new_channel().unwrap(), + }; + + let handle = thread::spawn(move || { + let _ = server.svc_loop(); + }); + + // main thread + let mut signals = Signals::new(TERM_SIGNALS).unwrap(); + for _sig in signals.forever() { + break; + } + + se.umount().unwrap(); + se.wake().unwrap(); + + let _ = handle.join(); +} - build_new_image(&paths, &PathBuf::from_str("./titi").unwrap()); +impl FuseServer { + pub fn svc_loop(&mut self) -> Result<()> { + print!("entering server loop\n"); + loop { + if let Some((reader, writer)) = self.ch.get_request().unwrap() { + if let Err(e) = self + .server + .handle_message(reader, writer.into(), None, None) + { + match e { + fuse_backend_rs::Error::EncodeMessage(_ebadf) => { + break; + } + _ => { + print!("Handling fuse message failed"); + continue; + } + } + } + } else { + print!("fuse server exits"); + break; + } + } + Ok(()) + } } From 778a1411ada70f84aa478b98982da0cca1f1f0f4 Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Fri, 5 Apr 2024 00:03:50 +0200 Subject: [PATCH 06/21] feat: extract layers into a single folder Signed-off-by: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com> --- src/fs-gen/Cargo.toml | 2 +- src/fs-gen/src/image_builder.rs | 154 +++++++++++++++++++++++++++----- src/fs-gen/src/main.rs | 119 ++---------------------- 3 files changed, 139 insertions(+), 136 deletions(-) diff --git a/src/fs-gen/Cargo.toml b/src/fs-gen/Cargo.toml index 2f711a4..1ac72bf 100644 --- a/src/fs-gen/Cargo.toml +++ b/src/fs-gen/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] clap = { version = "4.5.3", features = ["derive", "wrap_help", "string"] } +dircpy = "0.3.16" fuse-backend-rs = "0.12.0" once_cell = "1.19.0" regex = "1.10.4" @@ -14,4 +15,3 @@ serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.115" signal-hook = "0.3.17" validator = { version = "0.17.0", features = ["derive"] } -vfs = "0.12.0" diff --git a/src/fs-gen/src/image_builder.rs b/src/fs-gen/src/image_builder.rs index a6796aa..c58961e 100644 --- a/src/fs-gen/src/image_builder.rs +++ b/src/fs-gen/src/image_builder.rs @@ -1,30 +1,142 @@ -use std::path::{Path, PathBuf}; +use std::{ + fs::{self, File}, + io::Result, + option, + path::{Path, PathBuf}, + sync::Arc, + thread, +}; -use vfs::{FileSystem, OverlayFS, PhysicalFS, VfsPath}; +use fuse_backend_rs::{ + api::{filesystem::Layer, server::Server}, + overlayfs::{config::Config, OverlayFs}, + passthrough::{self, PassthroughFs}, + transport::{FuseChannel, FuseSession}, +}; +use signal_hook::{consts::TERM_SIGNALS, iterator::Signals}; -pub fn build_new_image(blob_paths: &[PathBuf], output_folder: &Path) { - let virtual_paths = blob_paths - .iter() - .map(|p| VfsPath::new(PhysicalFS::new(p))) - .collect::>(); +pub struct FuseServer { + server: Arc>>, + ch: FuseChannel, +} + +type BoxedLayer = Box + Send + Sync>; + +/// Initialiazes a passthrough fs for a given layer +fn new_passthroughfs_layer(rootdir: &str) -> Result { + let mut config = passthrough::Config::default(); + config.root_dir = String::from(rootdir); + // enable xattr + config.xattr = true; + config.do_import = true; + let fs = Box::new(PassthroughFs::<()>::new(config)?); + fs.import()?; + Ok(fs as BoxedLayer) +} + +/// Ensure a destination folder is created +fn ensure_folder_created(output_folder: &Path) -> Result<()> { + //TODO if there is already a folder, change names/delete it beforehand ? + let _ = fs::create_dir(output_folder); + // TODO actually make sure it works + Ok(()) +} + +/// Merges all the layers into a single folder for further manipulation +/// It works by instantiating an overlay fs via FUSE then copying the files to the desired target +/// # Usage +/// ``` +/// merge_layer(vec!["source/layer_1", "source/layer_2"], "/tmp/fused_layers") +/// ``` +pub fn merge_layer(blob_paths: &[PathBuf], output_folder: &Path) -> Result<()> { + // Stack all lower layers + let mut lower_layers = Vec::new(); + for lower in blob_paths { + lower_layers.push(Arc::new( + new_passthroughfs_layer(lower.to_str().unwrap()).unwrap(), + )); + } - let vfs = OverlayFS::new(&virtual_paths); + let mountpoint = Path::new("/tmp/cloudlet_internal"); + let fs_name = "cloudlet_overlay"; - let toto: Vec = vfs.read_dir("").unwrap().collect(); - // copy from the overlay fs to the output folder + let _ = ensure_folder_created(mountpoint); + let _ = ensure_folder_created(output_folder); - println!("{:?}", toto); - println!("{:?}", vfs); - let overlay_root: VfsPath = vfs.into(); + // Setup the overlay fs config + let mut config = Config::default(); + config.work = "/work".into(); + config.mountpoint = output_folder.to_str().unwrap().into(); + config.do_import = true; - println!("{:?}", overlay_root); + let fs = OverlayFs::new(None, lower_layers, config).unwrap(); + fs.import().unwrap(); - let output_vpath = VfsPath::new(PhysicalFS::new(".")); - println!("{:?}", output_vpath.as_str()); + // Enable a fuse session to make the fs available + let mut se = FuseSession::new(mountpoint, fs_name, "", true).unwrap(); + se.mount().unwrap(); + + // Fuse session + let mut server = FuseServer { + server: Arc::new(Server::new(Arc::new(fs))), + ch: se.new_channel().unwrap(), + }; + + let handle = thread::spawn(move || { + let _ = server.svc_loop(); + }); + + println!("copy starting !"); + //So now we need to copy the files + let copy_res = dircpy::copy_dir(mountpoint, output_folder); + println!("copy finished ?, {:?}", copy_res); + + // main thread + // let mut signals = Signals::new(TERM_SIGNALS).unwrap(); + // for _sig in signals.forever() { + // break; + // } + + // Unmount sessions so it can be re-used in later executions of the program + se.umount().unwrap(); + se.wake().unwrap(); + + let _ = handle.join(); + Ok(()) // TODO proper error handling +} - overlay_root - .join("bin") - .unwrap() - .copy_dir(&output_vpath.join("titi").unwrap()) - .expect("Failed to copy the blobs !"); +impl FuseServer { + pub fn svc_loop(&mut self) -> Result<()> { + print!("entering server loop\n"); + loop { + match self.ch.get_request() { + Ok(value) => { + if let Some((reader, writer)) = value { + if let Err(e) = + self.server + .handle_message(reader, writer.into(), None, None) + { + match e { + fuse_backend_rs::Error::EncodeMessage(_ebadf) => { + break; + } + _ => { + print!("Handling fuse message failed"); + continue; + } + } + } + } else { + print!("fuse server exits"); + break; + } + } + Err(err) => { + println!("{:?}", err); + break; + } + } + } + Ok(()) + } } diff --git a/src/fs-gen/src/main.rs b/src/fs-gen/src/main.rs index 4de6ff0..3fe6d8a 100644 --- a/src/fs-gen/src/main.rs +++ b/src/fs-gen/src/main.rs @@ -1,125 +1,16 @@ -use std::{ - fs::{self, File}, - io::Result, - path::{Path, PathBuf}, - process::exit, - str::FromStr, - sync::Arc, - thread::{self, sleep}, - time::Duration, -}; +use std::{fs, path::PathBuf, str::FromStr}; -use clap::Parser; -use fuse_backend_rs::{ - api::{filesystem::Layer, server::Server}, - overlayfs::{config::Config, OverlayFs}, - passthrough::{self, PassthroughFs}, - transport::{FuseChannel, FuseSession}, -}; - -use crate::{cli_args::CliArgs, image_builder::build_new_image}; -use signal_hook::{self, consts::TERM_SIGNALS, iterator::Signals}; +use image_builder::merge_layer; mod cli_args; mod image_builder; -pub struct FuseServer { - server: Arc>>, - ch: FuseChannel, -} - -type BoxedLayer = Box + Send + Sync>; - -fn new_passthroughfs_layer(rootdir: &str) -> Result { - let mut config = passthrough::Config::default(); - config.root_dir = String::from(rootdir); - // enable xattr - config.xattr = true; - config.do_import = true; - let fs = Box::new(PassthroughFs::<()>::new(config)?); - fs.import()?; - Ok(fs as BoxedLayer) -} - fn main() { // let args = CliArgs::get_args(); // println!("Hello, world!, {:?}", args); - // let paths: Vec = - // vec![PathBuf::from_str("/home/spse/Downloads/image-gen/layer").unwrap()]; - - // //let _ = fs::create_dir("./titi"); - - // build_new_image(&paths, &PathBuf::from_str("./titi").unwrap()); - - let upper_layer = - Arc::new(new_passthroughfs_layer("/home/spse/Downloads/image-gen/layer_2").unwrap()); - let mut lower_layers = Vec::new(); - for lower in vec!["/home/spse/Downloads/image-gen/layer"] { - lower_layers.push(Arc::new(new_passthroughfs_layer(&lower).unwrap())); - } - - let mut config = Config::default(); - config.work = "/work".into(); - config.mountpoint = "/tmp/overlay_test2".into(); - config.do_import = true; - - print!("new overlay fs\n"); - let fs = OverlayFs::new(Some(upper_layer), lower_layers, config).unwrap(); - print!("init root inode\n"); - fs.import().unwrap(); - - print!("open fuse session\n"); - let mut se = - FuseSession::new(Path::new("/tmp/overlay_test2"), "toto_overlay2", "", false).unwrap(); - print!("session opened\n"); - se.mount().unwrap(); - - let mut server = FuseServer { - server: Arc::new(Server::new(Arc::new(fs))), - ch: se.new_channel().unwrap(), - }; - - let handle = thread::spawn(move || { - let _ = server.svc_loop(); - }); - - // main thread - let mut signals = Signals::new(TERM_SIGNALS).unwrap(); - for _sig in signals.forever() { - break; - } - - se.umount().unwrap(); - se.wake().unwrap(); - - let _ = handle.join(); -} + let paths: Vec = + vec![PathBuf::from_str("/home/spse/Downloads/image-gen/layer").unwrap()]; -impl FuseServer { - pub fn svc_loop(&mut self) -> Result<()> { - print!("entering server loop\n"); - loop { - if let Some((reader, writer)) = self.ch.get_request().unwrap() { - if let Err(e) = self - .server - .handle_message(reader, writer.into(), None, None) - { - match e { - fuse_backend_rs::Error::EncodeMessage(_ebadf) => { - break; - } - _ => { - print!("Handling fuse message failed"); - continue; - } - } - } - } else { - print!("fuse server exits"); - break; - } - } - Ok(()) - } + merge_layer(&paths, &PathBuf::from_str("./titi").unwrap()); } From 3b160a0eff87e8653ca36bfafda2e473ce62222b Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Sat, 6 Apr 2024 20:26:01 +0200 Subject: [PATCH 07/21] add(args): agent_host_path and target path, better error display Signed-off-by: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com> --- src/fs-gen/src/cli_args.rs | 50 ++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/src/fs-gen/src/cli_args.rs b/src/fs-gen/src/cli_args.rs index 350f6c7..4f52e92 100644 --- a/src/fs-gen/src/cli_args.rs +++ b/src/fs-gen/src/cli_args.rs @@ -1,10 +1,9 @@ use std::{env, path::PathBuf}; -use clap::{command, Parser}; +use clap::{command, error::ErrorKind, CommandFactory, Parser}; use regex::Regex; use once_cell::sync::Lazy; -use validator::Validate; // So, for any of you who may be scared, this is the regex from the OCI Distribution Sepcification for the image name + the tag static RE_IMAGE_NAME: Lazy = Lazy::new(|| { @@ -12,18 +11,22 @@ static RE_IMAGE_NAME: Lazy = Lazy::new(|| { }); /// Convert an OCI image into a CPIO file -#[derive(Parser, Debug, Validate)] +#[derive(Parser, Debug)] #[command(version, about, long_about = None)] pub struct CliArgs { /// The name of the image to download - - #[arg(short, long)] - #[validate(regex(path = *RE_IMAGE_NAME))] pub image_name: String, /// The path to the output file #[arg(short, long, default_value=get_default_log_path().into_os_string())] pub ouput_file: PathBuf, + + /// The host path to the guest agent binary + pub agent_host_path: PathBuf, + + /// The target path of the guest agent binary + #[arg(short, long, default_value=get_default_target_agent_path().into_os_string())] + pub agent_target_path: PathBuf, } impl CliArgs { @@ -31,13 +34,36 @@ impl CliArgs { pub fn get_args() -> Self { let args = CliArgs::parse(); - let validation = args.validate(); - if validation.is_err() { - panic!("Invalid arguments: {}", validation.expect_err("wut")); - } + args.validate_image(); + args.validate_host_path(); args } + + fn validate_image(&self) { + if !RE_IMAGE_NAME.is_match(&self.image_name) { + let mut cmd = CliArgs::command(); + cmd.error( + ErrorKind::InvalidValue, + format!("Invalid image name: \"{}\"", self.image_name), + ) + .exit(); + } + } + + fn validate_host_path(&self) { + if !self.agent_host_path.exists() { + let mut cmd = CliArgs::command(); + cmd.error( + ErrorKind::InvalidValue, + format!( + "File not found for agent binary: \"{}\"", + self.agent_host_path.to_string_lossy() + ), + ) + .exit(); + } + } } /// Get the default output path for the cpio file. @@ -47,3 +73,7 @@ fn get_default_log_path() -> PathBuf { path.push("output.cpio"); path } + +fn get_default_target_agent_path() -> PathBuf { + PathBuf::from("/usr/bin/agent") +} From 8a64316aca9e15bd4e4c82fd683021df66bc7599 Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Mon, 15 Apr 2024 12:33:40 +0200 Subject: [PATCH 08/21] Fix: disallow other users to access the overlay fs Signed-off-by: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com> --- src/fs-gen/src/image_builder.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fs-gen/src/image_builder.rs b/src/fs-gen/src/image_builder.rs index c58961e..7fc6384 100644 --- a/src/fs-gen/src/image_builder.rs +++ b/src/fs-gen/src/image_builder.rs @@ -74,6 +74,7 @@ pub fn merge_layer(blob_paths: &[PathBuf], output_folder: &Path) -> Result<()> { // Enable a fuse session to make the fs available let mut se = FuseSession::new(mountpoint, fs_name, "", true).unwrap(); + se.set_allow_other(false); se.mount().unwrap(); // Fuse session From 25c5105f622d7d63be8dc44e8eeb08223a17cc57 Mon Sep 17 00:00:00 2001 From: DziyanaT Date: Sat, 13 Apr 2024 23:28:19 +0200 Subject: [PATCH 09/21] feat: download and unpack image layers from Docker Hub Signed-off-by: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com> --- src/fs-gen/Cargo.toml | 3 + src/fs-gen/src/cli_args.rs | 5 +- src/fs-gen/src/image_loader.rs | 135 +++++++++++++++++++++++++++++++++ src/fs-gen/src/main.rs | 18 +++-- 4 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 src/fs-gen/src/image_loader.rs diff --git a/src/fs-gen/Cargo.toml b/src/fs-gen/Cargo.toml index 1ac72bf..2dbeb48 100644 --- a/src/fs-gen/Cargo.toml +++ b/src/fs-gen/Cargo.toml @@ -9,9 +9,12 @@ edition = "2021" clap = { version = "4.5.3", features = ["derive", "wrap_help", "string"] } dircpy = "0.3.16" fuse-backend-rs = "0.12.0" +flate2 = "1.0.28" once_cell = "1.19.0" regex = "1.10.4" +reqwest = { version = "0.12.3", features = ["blocking", "json"] } serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.115" signal-hook = "0.3.17" +tar = "0.4.40" validator = { version = "0.17.0", features = ["derive"] } diff --git a/src/fs-gen/src/cli_args.rs b/src/fs-gen/src/cli_args.rs index 4f52e92..ef3a77c 100644 --- a/src/fs-gen/src/cli_args.rs +++ b/src/fs-gen/src/cli_args.rs @@ -19,7 +19,8 @@ pub struct CliArgs { /// The path to the output file #[arg(short, long, default_value=get_default_log_path().into_os_string())] - pub ouput_file: PathBuf, + + pub output_file: PathBuf, /// The host path to the guest agent binary pub agent_host_path: PathBuf, @@ -70,7 +71,7 @@ impl CliArgs { fn get_default_log_path() -> PathBuf { let mut path = env::current_exe().unwrap(); path.pop(); - path.push("output.cpio"); + path.push("output"); path } diff --git a/src/fs-gen/src/image_loader.rs b/src/fs-gen/src/image_loader.rs new file mode 100644 index 0000000..0b43ad9 --- /dev/null +++ b/src/fs-gen/src/image_loader.rs @@ -0,0 +1,135 @@ +use flate2::read::GzDecoder; +use reqwest::blocking::{Client, Response}; +use std::error::Error; +use std::fs::create_dir; +use std::path::PathBuf; +use tar::Archive; + +pub fn download_image_fs(image_name: &str, output_file: PathBuf) -> Result<(), Box> { + // Get image's name and tag + let image_and_tag: Vec<&str> = image_name.split(":").collect(); + let mut tag = ""; + if image_and_tag.len() < 2 { + tag = "latest" + } else { + tag = image_and_tag[1]; + } + let image_name = image_and_tag[0]; + + // Download image manifest + let mut manifest_json = download_manifest(image_name, tag)?; + + // Verify if it's a manifest or a manifest list + let mut layers = manifest_json["layers"].as_array(); + + if layers.is_none() { + let manifests = manifest_json["manifests"].as_array(); + match manifests { + None => eprintln!("This image's manifest format is not supported"), + Some(m) => { + // Get a manifest for amd64 architecture from the manifest list + let amd64_manifest = m.iter().find(|manifest| { + manifest["platform"].as_object().unwrap()["architecture"] + .as_str() + .unwrap() + == "amd64" + }); + + match amd64_manifest { + None => eprintln!("This image doesn't support amd64 architecture"), + Some(m) => { + manifest_json = + download_manifest(image_name, m["digest"].as_str().unwrap())?; + layers = manifest_json["layers"].as_array(); + if layers.is_none() { + eprintln!("Couldn't find image layers."); + return Ok(()); + } + } + } + } + } + } + + let _ = create_dir(&output_file); + + download_layers(layers.unwrap(), image_name, &output_file) +} + +fn download_manifest(image_name: &str, digest: &str) -> Result> { + // Create a reqwest HTTP client + let client = Client::new(); + + // Get a token for anonymous authentication to Docker Hub + let token_json: serde_json::Value = client + .get(format!("https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/{image_name}:pull")) + .send()?.json()?; + + let token = token_json["token"].as_str().unwrap(); + + // Query Docker Hub API to get the image manifest + let manifest_url = format!( + "https://registry-1.docker.io/v2/library/{}/manifests/{}", + image_name, digest + ); + println!("url: {manifest_url}"); + + let manifest_response = client + .get(&manifest_url) + .header( + "Accept", + "application/vnd.docker.distribution.manifest.v2+json", + ) + .header( + "Accept", + "application/vnd.docker.distribution.manifest.list.v2+json", + ) + .header("Accept", "application/vnd.oci.image.manifest.v1+json") + .bearer_auth(token) + .send()?; + + let manifest_json: serde_json::Value = manifest_response.json()?; + println!("manifest: {}", manifest_json); + Ok(manifest_json) +} + +fn unpack_tarball(tar: GzDecoder, output_dir: &PathBuf) -> Result<(), Box> { + let mut ar = Archive::new(tar); + ar.unpack(output_dir)?; + Ok(()) +} + +fn download_layers( + layers: &Vec, + image_name: &str, + output_dir: &PathBuf, +) -> Result<(), Box> { + let client = Client::new(); + + // Get a token for anonymous authentication to Docker Hub + let token_json: serde_json::Value = client + .get(format!("https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/{image_name}:pull")) + .send()?.json()?; + + let token = token_json["token"].as_str().unwrap(); + + // Download and unpack each layer + for layer in layers { + let digest = layer["digest"].as_str().unwrap(); + let layer_url = format!( + "https://registry-1.docker.io/v2/library/{}/blobs/{}", + image_name, digest + ); + + let response = client.get(&layer_url).bearer_auth(token).send()?; + + let tar = GzDecoder::new(response); + + let mut output_path = PathBuf::new(); + output_path.push(&output_dir); + output_path.push(digest); + + unpack_tarball(tar, &output_path)?; + } + Ok(()) +} diff --git a/src/fs-gen/src/main.rs b/src/fs-gen/src/main.rs index 3fe6d8a..1f1a501 100644 --- a/src/fs-gen/src/main.rs +++ b/src/fs-gen/src/main.rs @@ -4,13 +4,21 @@ use image_builder::merge_layer; mod cli_args; mod image_builder; +mod image_loader; fn main() { - // let args = CliArgs::get_args(); - // println!("Hello, world!, {:?}", args); + let args = cli_args::CliArgs::get_args(); + println!("Hello, world!, {:?}", args); - let paths: Vec = - vec![PathBuf::from_str("/home/spse/Downloads/image-gen/layer").unwrap()]; + // let paths: Vec = + // vec![PathBuf::from_str("/home/spse/Downloads/image-gen/layer").unwrap()]; - merge_layer(&paths, &PathBuf::from_str("./titi").unwrap()); + // merge_layer(&paths, &PathBuf::from_str("./titi").unwrap()); + + + if let Err(e) = image_loader::download_image_fs(&args.image_name, args.output_file) { + eprintln!("Error: {}", e); + } else { + println!("Image downloaded successfully!"); + } } From d4b9ef82728c98b8c1f96ab7c0e4386b17a896d2 Mon Sep 17 00:00:00 2001 From: DziyanaT Date: Sun, 14 Apr 2024 17:58:15 +0200 Subject: [PATCH 10/21] feat: return paths to layers in download_image_fs function, improve log messages Signed-off-by: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com> --- src/fs-gen/src/image_loader.rs | 33 +++++++++++++++++++++++---------- src/fs-gen/src/main.rs | 13 ++++++++----- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/fs-gen/src/image_loader.rs b/src/fs-gen/src/image_loader.rs index 0b43ad9..42bb209 100644 --- a/src/fs-gen/src/image_loader.rs +++ b/src/fs-gen/src/image_loader.rs @@ -5,10 +5,13 @@ use std::fs::create_dir; use std::path::PathBuf; use tar::Archive; -pub fn download_image_fs(image_name: &str, output_file: PathBuf) -> Result<(), Box> { +pub fn download_image_fs( + image_name: &str, + output_file: PathBuf, +) -> Result, Box> { // Get image's name and tag let image_and_tag: Vec<&str> = image_name.split(":").collect(); - let mut tag = ""; + let tag: &str; if image_and_tag.len() < 2 { tag = "latest" } else { @@ -19,14 +22,17 @@ pub fn download_image_fs(image_name: &str, output_file: PathBuf) -> Result<(), B // Download image manifest let mut manifest_json = download_manifest(image_name, tag)?; + println!("manifest: {}",manifest_json); + // Verify if it's a manifest or a manifest list let mut layers = manifest_json["layers"].as_array(); if layers.is_none() { let manifests = manifest_json["manifests"].as_array(); match manifests { - None => eprintln!("This image's manifest format is not supported"), + None => Err(format!("Couldn't find a Docker V2 or OCI manifest for {}:{}", image_name, tag))?, Some(m) => { + println!("Manifest list found. Looking for an amd64 manifest..."); // Get a manifest for amd64 architecture from the manifest list let amd64_manifest = m.iter().find(|manifest| { manifest["platform"].as_object().unwrap()["architecture"] @@ -36,14 +42,14 @@ pub fn download_image_fs(image_name: &str, output_file: PathBuf) -> Result<(), B }); match amd64_manifest { - None => eprintln!("This image doesn't support amd64 architecture"), + None => Err("This image doesn't support amd64 architecture")?, Some(m) => { + println!("Downloading manifest for amd64 architecture..."); manifest_json = download_manifest(image_name, m["digest"].as_str().unwrap())?; layers = manifest_json["layers"].as_array(); if layers.is_none() { - eprintln!("Couldn't find image layers."); - return Ok(()); + Err("Couldn't find image layers in the manifest.")? } } } @@ -72,7 +78,6 @@ fn download_manifest(image_name: &str, digest: &str) -> Result Result, image_name: &str, output_dir: &PathBuf, -) -> Result<(), Box> { +) -> Result, Box> { let client = Client::new(); // Get a token for anonymous authentication to Docker Hub @@ -113,6 +118,10 @@ fn download_layers( let token = token_json["token"].as_str().unwrap(); + let mut layer_paths = Vec::new(); + + println!("Downloading and unpacking layers:"); + // Download and unpack each layer for layer in layers { let digest = layer["digest"].as_str().unwrap(); @@ -123,6 +132,8 @@ fn download_layers( let response = client.get(&layer_url).bearer_auth(token).send()?; + print!(" - {}", digest); + let tar = GzDecoder::new(response); let mut output_path = PathBuf::new(); @@ -130,6 +141,8 @@ fn download_layers( output_path.push(digest); unpack_tarball(tar, &output_path)?; + println!(" - unpacked"); + layer_paths.push(output_path); } - Ok(()) + Ok(layer_paths) } diff --git a/src/fs-gen/src/main.rs b/src/fs-gen/src/main.rs index 1f1a501..0387510 100644 --- a/src/fs-gen/src/main.rs +++ b/src/fs-gen/src/main.rs @@ -15,10 +15,13 @@ fn main() { // merge_layer(&paths, &PathBuf::from_str("./titi").unwrap()); - - if let Err(e) = image_loader::download_image_fs(&args.image_name, args.output_file) { - eprintln!("Error: {}", e); - } else { - println!("Image downloaded successfully!"); + match image_loader::download_image_fs(&args.image_name, args.output_file) { + Err(e) => eprintln!("Error: {}", e), + Ok(layers_paths) => { + println!("Image downloaded successfully! Layers' paths:"); + for path in layers_paths { + println!(" - {}", path.display()); + } + } } } From c44c054887119f53ea6f1c8122761e5c1afd9cd7 Mon Sep 17 00:00:00 2001 From: DziyanaT Date: Mon, 15 Apr 2024 11:48:43 +0200 Subject: [PATCH 11/21] wip: trying to combine fs download and fuse Signed-off-by: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com> --- src/fs-gen/src/cli_args.rs | 3 +-- src/fs-gen/src/image_loader.rs | 9 ++++++--- src/fs-gen/src/main.rs | 12 ++++++++---- src/fs-gen/test | 0 4 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 src/fs-gen/test diff --git a/src/fs-gen/src/cli_args.rs b/src/fs-gen/src/cli_args.rs index ef3a77c..3eef5e0 100644 --- a/src/fs-gen/src/cli_args.rs +++ b/src/fs-gen/src/cli_args.rs @@ -19,7 +19,6 @@ pub struct CliArgs { /// The path to the output file #[arg(short, long, default_value=get_default_log_path().into_os_string())] - pub output_file: PathBuf, /// The host path to the guest agent binary @@ -71,7 +70,7 @@ impl CliArgs { fn get_default_log_path() -> PathBuf { let mut path = env::current_exe().unwrap(); path.pop(); - path.push("output"); + path.push("output.cpio"); path } diff --git a/src/fs-gen/src/image_loader.rs b/src/fs-gen/src/image_loader.rs index 42bb209..09c18a9 100644 --- a/src/fs-gen/src/image_loader.rs +++ b/src/fs-gen/src/image_loader.rs @@ -22,15 +22,16 @@ pub fn download_image_fs( // Download image manifest let mut manifest_json = download_manifest(image_name, tag)?; - println!("manifest: {}",manifest_json); - // Verify if it's a manifest or a manifest list let mut layers = manifest_json["layers"].as_array(); if layers.is_none() { let manifests = manifest_json["manifests"].as_array(); match manifests { - None => Err(format!("Couldn't find a Docker V2 or OCI manifest for {}:{}", image_name, tag))?, + None => Err(format!( + "Couldn't find a Docker V2 or OCI manifest for {}:{}", + image_name, tag + ))?, Some(m) => { println!("Manifest list found. Looking for an amd64 manifest..."); // Get a manifest for amd64 architecture from the manifest list @@ -95,6 +96,8 @@ fn download_manifest(image_name: &str, digest: &str) -> Result eprintln!("Error: {}", e), + match image_loader::download_image_fs(&args.image_name, args.output_file.clone()) { + Err(e) => { + eprintln!("Error: {}", e); + return; + }, Ok(layers_paths) => { println!("Image downloaded successfully! Layers' paths:"); - for path in layers_paths { + for path in &layers_paths { println!(" - {}", path.display()); } + merge_layer(&layers_paths, Path::new("/tmp/cloudlet")); } } } diff --git a/src/fs-gen/test b/src/fs-gen/test new file mode 100644 index 0000000..e69de29 From b4ab148c9e2eee704efa52f63f2a5ba63476ebc4 Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Mon, 15 Apr 2024 12:33:40 +0200 Subject: [PATCH 12/21] Fix: disallow other users to access the overlay fs Signed-off-by: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com> From 0a6c6a57ebe9b589dd8bb5752710728666891c9e Mon Sep 17 00:00:00 2001 From: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com> Date: Mon, 15 Apr 2024 17:15:16 +0200 Subject: [PATCH 13/21] feat: create init file Signed-off-by: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com> --- src/fs-gen/src/initramfs_generator.rs | 25 +++++++++++++++++++++++++ src/fs-gen/src/main.rs | 3 +++ 2 files changed, 28 insertions(+) create mode 100644 src/fs-gen/src/initramfs_generator.rs diff --git a/src/fs-gen/src/initramfs_generator.rs b/src/fs-gen/src/initramfs_generator.rs new file mode 100644 index 0000000..d9cfd26 --- /dev/null +++ b/src/fs-gen/src/initramfs_generator.rs @@ -0,0 +1,25 @@ +use std::fs::{File, Permissions}; +use std::os::unix::fs::PermissionsExt; +use std::io::Write; +use std::path::Path; + +const INIT_FILE: &[u8;220] = b"#! /bin/sh +# +# /init executable file in the initramfs +# +mount -t devtmpfs dev /dev +mount -t proc proc /proc +mount -t sysfs sysfs /sys +ip link set up dev lo + +exec /sbin/getty -n -l /bin/sh 115200 /dev/console +poweroff -f +"; + +pub fn create_init_file(path: &Path) { + let file_path = path.join("init"); + let mut file = File::create(file_path).unwrap(); + + file.write_all(INIT_FILE).expect("Could not write init file"); + file.set_permissions(Permissions::from_mode(0o755)).unwrap(); +} diff --git a/src/fs-gen/src/main.rs b/src/fs-gen/src/main.rs index 91faa78..2b05e67 100644 --- a/src/fs-gen/src/main.rs +++ b/src/fs-gen/src/main.rs @@ -1,10 +1,12 @@ use std::{fs, path::{Path, PathBuf}, str::FromStr}; use image_builder::merge_layer; +use crate::initramfs_generator::create_init_file; mod cli_args; mod image_builder; mod image_loader; +mod initramfs_generator; fn main() { let args = cli_args::CliArgs::get_args(); @@ -26,6 +28,7 @@ fn main() { println!(" - {}", path.display()); } merge_layer(&layers_paths, Path::new("/tmp/cloudlet")); + create_init_file(Path::new("/tmp/cloudlet")); } } } From d5b0196a77eac062bd869f977a518a192d73b49f Mon Sep 17 00:00:00 2001 From: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com> Date: Mon, 15 Apr 2024 18:27:56 +0200 Subject: [PATCH 14/21] feat: implement v0 with bash command Signed-off-by: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com> --- src/fs-gen/src/initramfs_generator.rs | 19 +++++++++++++++++++ src/fs-gen/src/main.rs | 10 +++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/fs-gen/src/initramfs_generator.rs b/src/fs-gen/src/initramfs_generator.rs index d9cfd26..b9078ac 100644 --- a/src/fs-gen/src/initramfs_generator.rs +++ b/src/fs-gen/src/initramfs_generator.rs @@ -2,6 +2,7 @@ use std::fs::{File, Permissions}; use std::os::unix::fs::PermissionsExt; use std::io::Write; use std::path::Path; +use std::process::{Command, Stdio}; const INIT_FILE: &[u8;220] = b"#! /bin/sh # @@ -23,3 +24,21 @@ pub fn create_init_file(path: &Path) { file.write_all(INIT_FILE).expect("Could not write init file"); file.set_permissions(Permissions::from_mode(0o755)).unwrap(); } + +pub fn generate_initramfs(root_directory: &Path, output: &Path) { + let file = File::create(output).unwrap(); + file.set_permissions(Permissions::from_mode(0o644)).expect("Could not set permissions"); + + println!("Generating initramfs..."); + + let mut command = Command::new("sh") + .current_dir(root_directory) + .stdout(Stdio::from(file)) + .arg("-c") + .arg("find . -print0 | cpio -0 --create --owner=root:root --format=newc | xz -9 --format=lzma") + .spawn() + .expect("Failed to package initramfs"); + command.wait().expect("Failed to wait for initramfs to finish"); + + println!("Initramfs generated!"); +} diff --git a/src/fs-gen/src/main.rs b/src/fs-gen/src/main.rs index 2b05e67..92f5a03 100644 --- a/src/fs-gen/src/main.rs +++ b/src/fs-gen/src/main.rs @@ -1,7 +1,7 @@ use std::{fs, path::{Path, PathBuf}, str::FromStr}; use image_builder::merge_layer; -use crate::initramfs_generator::create_init_file; +use crate::initramfs_generator::{create_init_file, generate_initramfs}; mod cli_args; mod image_builder; @@ -27,8 +27,12 @@ fn main() { for path in &layers_paths { println!(" - {}", path.display()); } - merge_layer(&layers_paths, Path::new("/tmp/cloudlet")); - create_init_file(Path::new("/tmp/cloudlet")); + + let path = Path::new("/tmp/cloudlet"); + + merge_layer(&layers_paths, path); + create_init_file(path); + generate_initramfs(path, Path::new("/tmp/rusty.img")); } } } From 33cce94df1b34a5f7787cc39cc16d635d6e754a3 Mon Sep 17 00:00:00 2001 From: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com> Date: Wed, 17 Apr 2024 19:52:12 +0200 Subject: [PATCH 15/21] feat: change cli arguments to be more modular Signed-off-by: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com> --- src/fs-gen/src/cli_args.rs | 23 ++++++++++++----------- src/fs-gen/src/main.rs | 16 ++++++++-------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/fs-gen/src/cli_args.rs b/src/fs-gen/src/cli_args.rs index 3eef5e0..dae89d6 100644 --- a/src/fs-gen/src/cli_args.rs +++ b/src/fs-gen/src/cli_args.rs @@ -18,15 +18,15 @@ pub struct CliArgs { pub image_name: String, /// The path to the output file - #[arg(short, long, default_value=get_default_log_path().into_os_string())] + #[arg(short='o', long="output", default_value=get_default_output_file().into_os_string())] pub output_file: PathBuf, + /// The path to the temporary folder + #[arg(short='t', long="tempdir", default_value=get_default_temp_directory().into_os_string())] + pub temp_directory: PathBuf, + /// The host path to the guest agent binary pub agent_host_path: PathBuf, - - /// The target path of the guest agent binary - #[arg(short, long, default_value=get_default_target_agent_path().into_os_string())] - pub agent_target_path: PathBuf, } impl CliArgs { @@ -67,13 +67,14 @@ impl CliArgs { } /// Get the default output path for the cpio file. -fn get_default_log_path() -> PathBuf { - let mut path = env::current_exe().unwrap(); - path.pop(); - path.push("output.cpio"); +fn get_default_temp_directory() -> PathBuf { + let mut path = env::current_dir().unwrap(); + path.push(".cloudlet_temp/"); path } -fn get_default_target_agent_path() -> PathBuf { - PathBuf::from("/usr/bin/agent") +fn get_default_output_file() -> PathBuf { + let mut path = env::current_dir().unwrap(); + path.push("initramfs.img"); + path } diff --git a/src/fs-gen/src/main.rs b/src/fs-gen/src/main.rs index 92f5a03..b7201b6 100644 --- a/src/fs-gen/src/main.rs +++ b/src/fs-gen/src/main.rs @@ -1,4 +1,4 @@ -use std::{fs, path::{Path, PathBuf}, str::FromStr}; +use std::{fs::remove_dir_all, path::Path, str::FromStr}; use image_builder::merge_layer; use crate::initramfs_generator::{create_init_file, generate_initramfs}; @@ -12,12 +12,8 @@ fn main() { let args = cli_args::CliArgs::get_args(); println!("Hello, world!, {:?}", args); - // let paths: Vec = - // vec![PathBuf::from_str("/home/spse/Downloads/image-gen/layer").unwrap()]; - - // merge_layer(&paths, &PathBuf::from_str("./titi").unwrap()); - - match image_loader::download_image_fs(&args.image_name, args.output_file.clone()) { + // TODO: better organise layers and OverlayFS build in the temp directory + match image_loader::download_image_fs(&args.image_name, args.temp_directory.clone()) { Err(e) => { eprintln!("Error: {}", e); return; @@ -28,11 +24,15 @@ fn main() { println!(" - {}", path.display()); } + // FIXME: use a subdir of the temp directory instead let path = Path::new("/tmp/cloudlet"); merge_layer(&layers_paths, path); create_init_file(path); - generate_initramfs(path, Path::new("/tmp/rusty.img")); + generate_initramfs(path, Path::new(args.output_file.as_path())); } } + + // cleanup of temporary directory + remove_dir_all(args.temp_directory.clone()).expect("Could not remove temporary directory"); } From bf7e50ef37f2ce955dc96897e48cac71eaa1965d Mon Sep 17 00:00:00 2001 From: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com> Date: Wed, 17 Apr 2024 19:56:32 +0200 Subject: [PATCH 16/21] refactor: use subdirs for layers and overlay Signed-off-by: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com> --- src/fs-gen/src/main.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/fs-gen/src/main.rs b/src/fs-gen/src/main.rs index b7201b6..6394ce5 100644 --- a/src/fs-gen/src/main.rs +++ b/src/fs-gen/src/main.rs @@ -12,8 +12,11 @@ fn main() { let args = cli_args::CliArgs::get_args(); println!("Hello, world!, {:?}", args); + let layers_subdir = args.temp_directory.clone().join("layers/"); + let overlay_subdir = args.temp_directory.clone().join("overlay/"); + // TODO: better organise layers and OverlayFS build in the temp directory - match image_loader::download_image_fs(&args.image_name, args.temp_directory.clone()) { + match image_loader::download_image_fs(&args.image_name, layers_subdir) { Err(e) => { eprintln!("Error: {}", e); return; @@ -25,7 +28,7 @@ fn main() { } // FIXME: use a subdir of the temp directory instead - let path = Path::new("/tmp/cloudlet"); + let path = Path::new(overlay_subdir.as_path()); merge_layer(&layers_paths, path); create_init_file(path); From 6a4c87d328b5680ec677f60e0299de816c19dbd9 Mon Sep 17 00:00:00 2001 From: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com> Date: Mon, 22 Apr 2024 13:50:42 +0200 Subject: [PATCH 17/21] refactor: add flair to signal the initramfs comes from cloudlet Signed-off-by: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com> --- src/fs-gen/src/initramfs_generator.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fs-gen/src/initramfs_generator.rs b/src/fs-gen/src/initramfs_generator.rs index b9078ac..3fedf8d 100644 --- a/src/fs-gen/src/initramfs_generator.rs +++ b/src/fs-gen/src/initramfs_generator.rs @@ -4,9 +4,9 @@ use std::io::Write; use std::path::Path; use std::process::{Command, Stdio}; -const INIT_FILE: &[u8;220] = b"#! /bin/sh +const INIT_FILE: &[u8;211] = b"#! /bin/sh # -# /init executable file in the initramfs +# Cloudlet initramfs generation # mount -t devtmpfs dev /dev mount -t proc proc /proc From 9026839a953b58de9309e62b2a2fec4a627f3179 Mon Sep 17 00:00:00 2001 From: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com> Date: Mon, 22 Apr 2024 14:24:54 +0200 Subject: [PATCH 18/21] lint: correct linting for crate Signed-off-by: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com> --- src/fs-gen/src/image_builder.rs | 4 +--- src/fs-gen/src/initramfs_generator.rs | 14 +++++++++----- src/fs-gen/src/main.rs | 8 ++++---- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/fs-gen/src/image_builder.rs b/src/fs-gen/src/image_builder.rs index 7fc6384..f723eab 100644 --- a/src/fs-gen/src/image_builder.rs +++ b/src/fs-gen/src/image_builder.rs @@ -1,7 +1,6 @@ use std::{ - fs::{self, File}, + fs::{self}, io::Result, - option, path::{Path, PathBuf}, sync::Arc, thread, @@ -13,7 +12,6 @@ use fuse_backend_rs::{ passthrough::{self, PassthroughFs}, transport::{FuseChannel, FuseSession}, }; -use signal_hook::{consts::TERM_SIGNALS, iterator::Signals}; pub struct FuseServer { server: Arc>>, diff --git a/src/fs-gen/src/initramfs_generator.rs b/src/fs-gen/src/initramfs_generator.rs index 3fedf8d..76027dc 100644 --- a/src/fs-gen/src/initramfs_generator.rs +++ b/src/fs-gen/src/initramfs_generator.rs @@ -1,10 +1,10 @@ use std::fs::{File, Permissions}; -use std::os::unix::fs::PermissionsExt; use std::io::Write; +use std::os::unix::fs::PermissionsExt; use std::path::Path; use std::process::{Command, Stdio}; -const INIT_FILE: &[u8;211] = b"#! /bin/sh +const INIT_FILE: &[u8; 211] = b"#! /bin/sh # # Cloudlet initramfs generation # @@ -21,13 +21,15 @@ pub fn create_init_file(path: &Path) { let file_path = path.join("init"); let mut file = File::create(file_path).unwrap(); - file.write_all(INIT_FILE).expect("Could not write init file"); + file.write_all(INIT_FILE) + .expect("Could not write init file"); file.set_permissions(Permissions::from_mode(0o755)).unwrap(); } pub fn generate_initramfs(root_directory: &Path, output: &Path) { let file = File::create(output).unwrap(); - file.set_permissions(Permissions::from_mode(0o644)).expect("Could not set permissions"); + file.set_permissions(Permissions::from_mode(0o644)) + .expect("Could not set permissions"); println!("Generating initramfs..."); @@ -38,7 +40,9 @@ pub fn generate_initramfs(root_directory: &Path, output: &Path) { .arg("find . -print0 | cpio -0 --create --owner=root:root --format=newc | xz -9 --format=lzma") .spawn() .expect("Failed to package initramfs"); - command.wait().expect("Failed to wait for initramfs to finish"); + command + .wait() + .expect("Failed to wait for initramfs to finish"); println!("Initramfs generated!"); } diff --git a/src/fs-gen/src/main.rs b/src/fs-gen/src/main.rs index 6394ce5..8229e85 100644 --- a/src/fs-gen/src/main.rs +++ b/src/fs-gen/src/main.rs @@ -1,7 +1,7 @@ -use std::{fs::remove_dir_all, path::Path, str::FromStr}; +use std::{fs::remove_dir_all, path::Path}; -use image_builder::merge_layer; use crate::initramfs_generator::{create_init_file, generate_initramfs}; +use image_builder::merge_layer; mod cli_args; mod image_builder; @@ -20,7 +20,7 @@ fn main() { Err(e) => { eprintln!("Error: {}", e); return; - }, + } Ok(layers_paths) => { println!("Image downloaded successfully! Layers' paths:"); for path in &layers_paths { @@ -30,7 +30,7 @@ fn main() { // FIXME: use a subdir of the temp directory instead let path = Path::new(overlay_subdir.as_path()); - merge_layer(&layers_paths, path); + merge_layer(&layers_paths, path).expect("Merging layers failed"); create_init_file(path); generate_initramfs(path, Path::new(args.output_file.as_path())); } From 38d9fb6888ae0936a86b9fa3fbd4db94b6eeca2b Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Mon, 22 Apr 2024 15:18:06 +0200 Subject: [PATCH 19/21] cleanup(image_builder): error handling with anyhow Signed-off-by: Mathias-Boulay --- src/fs-gen/Cargo.toml | 1 + src/fs-gen/src/image_builder.rs | 120 ++++++++++++++++++-------------- 2 files changed, 68 insertions(+), 53 deletions(-) diff --git a/src/fs-gen/Cargo.toml b/src/fs-gen/Cargo.toml index 2dbeb48..6bccfdb 100644 --- a/src/fs-gen/Cargo.toml +++ b/src/fs-gen/Cargo.toml @@ -18,3 +18,4 @@ serde_json = "1.0.115" signal-hook = "0.3.17" tar = "0.4.40" validator = { version = "0.17.0", features = ["derive"] } +anyhow = "1.0.82" diff --git a/src/fs-gen/src/image_builder.rs b/src/fs-gen/src/image_builder.rs index f723eab..6c5c67f 100644 --- a/src/fs-gen/src/image_builder.rs +++ b/src/fs-gen/src/image_builder.rs @@ -1,11 +1,11 @@ use std::{ fs::{self}, - io::Result, path::{Path, PathBuf}, sync::Arc, thread, }; +use anyhow::{Context, Result}; use fuse_backend_rs::{ api::{filesystem::Layer, server::Server}, overlayfs::{config::Config, OverlayFs}, @@ -21,23 +21,30 @@ pub struct FuseServer { type BoxedLayer = Box + Send + Sync>; /// Initialiazes a passthrough fs for a given layer +/// a passthrough fs is just a dummy implementation to map to the physical disk +/// # Usage +/// ``` +/// let passthrough_layer = new_passthroughfs_layer("/path/to/layer") +/// ``` fn new_passthroughfs_layer(rootdir: &str) -> Result { let mut config = passthrough::Config::default(); config.root_dir = String::from(rootdir); - // enable xattr config.xattr = true; config.do_import = true; let fs = Box::new(PassthroughFs::<()>::new(config)?); - fs.import()?; + fs.import() + .with_context(|| format!("Failed to create the passthrough layer: {}", rootdir))?; Ok(fs as BoxedLayer) } /// Ensure a destination folder is created fn ensure_folder_created(output_folder: &Path) -> Result<()> { - //TODO if there is already a folder, change names/delete it beforehand ? - let _ = fs::create_dir(output_folder); - // TODO actually make sure it works - Ok(()) + fs::create_dir(output_folder).with_context(|| { + format!( + "Failed to ensure folder creation: {}", + output_folder.to_string_lossy() + ) + }) } /// Merges all the layers into a single folder for further manipulation @@ -50,35 +57,39 @@ pub fn merge_layer(blob_paths: &[PathBuf], output_folder: &Path) -> Result<()> { // Stack all lower layers let mut lower_layers = Vec::new(); for lower in blob_paths { - lower_layers.push(Arc::new( - new_passthroughfs_layer(lower.to_str().unwrap()).unwrap(), - )); + lower_layers.push(Arc::new(new_passthroughfs_layer(&lower.to_string_lossy())?)); } let mountpoint = Path::new("/tmp/cloudlet_internal"); let fs_name = "cloudlet_overlay"; - let _ = ensure_folder_created(mountpoint); - let _ = ensure_folder_created(output_folder); + ensure_folder_created(mountpoint)?; + ensure_folder_created(output_folder)?; // Setup the overlay fs config let mut config = Config::default(); config.work = "/work".into(); - config.mountpoint = output_folder.to_str().unwrap().into(); + config.mountpoint = output_folder.to_string_lossy().into(); config.do_import = true; - let fs = OverlayFs::new(None, lower_layers, config).unwrap(); - fs.import().unwrap(); + let fs = OverlayFs::new(None, lower_layers, config) + .with_context(|| "Failed to construct the Overlay fs struct !".to_string())?; + fs.import() + .with_context(|| "Failed to initialize the overlay fs".to_string())?; // Enable a fuse session to make the fs available - let mut se = FuseSession::new(mountpoint, fs_name, "", true).unwrap(); + let mut se = FuseSession::new(mountpoint, fs_name, "", true) + .with_context(|| "Failed to construct the Fuse session")?; se.set_allow_other(false); - se.mount().unwrap(); + se.mount() + .with_context(|| "Failed to mount the overlay fs".to_string())?; // Fuse session let mut server = FuseServer { server: Arc::new(Server::new(Arc::new(fs))), - ch: se.new_channel().unwrap(), + ch: se + .new_channel() + .with_context(|| "Failed to create a new channel".to_string())?, }; let handle = thread::spawn(move || { @@ -87,52 +98,55 @@ pub fn merge_layer(blob_paths: &[PathBuf], output_folder: &Path) -> Result<()> { println!("copy starting !"); //So now we need to copy the files - let copy_res = dircpy::copy_dir(mountpoint, output_folder); - println!("copy finished ?, {:?}", copy_res); - - // main thread - // let mut signals = Signals::new(TERM_SIGNALS).unwrap(); - // for _sig in signals.forever() { - // break; - // } + dircpy::copy_dir(mountpoint, output_folder).with_context(|| { + format!( + "Failed to copy directories into the output folder: {}", + output_folder.to_string_lossy() + ) + })?; + println!("copy finished"); // Unmount sessions so it can be re-used in later executions of the program - se.umount().unwrap(); - se.wake().unwrap(); + se.wake() + .with_context(|| "Failed to exit the fuse session".to_string())?; + se.umount() + .with_context(|| "Failed to unmount the fuse session".to_string())?; let _ = handle.join(); - Ok(()) // TODO proper error handling + Ok(()) } impl FuseServer { + /// Run a loop to execute requests from the FUSE session + /// pub fn svc_loop(&mut self) -> Result<()> { - print!("entering server loop\n"); + println!("entering server loop"); loop { - match self.ch.get_request() { - Ok(value) => { - if let Some((reader, writer)) = value { - if let Err(e) = - self.server - .handle_message(reader, writer.into(), None, None) - { - match e { - fuse_backend_rs::Error::EncodeMessage(_ebadf) => { - break; - } - _ => { - print!("Handling fuse message failed"); - continue; - } - } - } - } else { - print!("fuse server exits"); + let value = self + .ch + .get_request() + .with_context(|| "Failed to get message from fuse session".to_string())?; + + if value.is_none() { + println!("fuse server exits"); + break; + } + + // Technically the unwrap is safe + let (reader, writer) = value.unwrap(); + + if let Err(e) = self + .server + .handle_message(reader, writer.into(), None, None) + { + match e { + fuse_backend_rs::Error::EncodeMessage(_ebadf) => { break; } - } - Err(err) => { - println!("{:?}", err); - break; + _ => { + print!("Handling fuse message failed"); + continue; + } } } } From 290f456b0cf41b76b848742852fc287d18d5d10c Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Mon, 22 Apr 2024 15:25:01 +0200 Subject: [PATCH 20/21] cleanup: clippy compliance Signed-off-by: Mathias-Boulay --- src/fs-gen/src/image_builder.rs | 20 ++++++++++++-------- src/fs-gen/src/image_loader.rs | 16 ++++++++-------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/fs-gen/src/image_builder.rs b/src/fs-gen/src/image_builder.rs index 6c5c67f..8d558ef 100644 --- a/src/fs-gen/src/image_builder.rs +++ b/src/fs-gen/src/image_builder.rs @@ -27,10 +27,12 @@ type BoxedLayer = Box + Send + Sync>; /// let passthrough_layer = new_passthroughfs_layer("/path/to/layer") /// ``` fn new_passthroughfs_layer(rootdir: &str) -> Result { - let mut config = passthrough::Config::default(); - config.root_dir = String::from(rootdir); - config.xattr = true; - config.do_import = true; + let config = passthrough::Config { + root_dir: String::from(rootdir), + xattr: true, + do_import: true, + ..Default::default() + }; let fs = Box::new(PassthroughFs::<()>::new(config)?); fs.import() .with_context(|| format!("Failed to create the passthrough layer: {}", rootdir))?; @@ -67,10 +69,12 @@ pub fn merge_layer(blob_paths: &[PathBuf], output_folder: &Path) -> Result<()> { ensure_folder_created(output_folder)?; // Setup the overlay fs config - let mut config = Config::default(); - config.work = "/work".into(); - config.mountpoint = output_folder.to_string_lossy().into(); - config.do_import = true; + let config = Config { + work: "/work".into(), + mountpoint: output_folder.to_string_lossy().into(), + do_import: true, + ..Default::default() + }; let fs = OverlayFs::new(None, lower_layers, config) .with_context(|| "Failed to construct the Overlay fs struct !".to_string())?; diff --git a/src/fs-gen/src/image_loader.rs b/src/fs-gen/src/image_loader.rs index 09c18a9..45c77bb 100644 --- a/src/fs-gen/src/image_loader.rs +++ b/src/fs-gen/src/image_loader.rs @@ -10,13 +10,13 @@ pub fn download_image_fs( output_file: PathBuf, ) -> Result, Box> { // Get image's name and tag - let image_and_tag: Vec<&str> = image_name.split(":").collect(); - let tag: &str; - if image_and_tag.len() < 2 { - tag = "latest" + let image_and_tag: Vec<&str> = image_name.split(':').collect(); + + let tag = if image_and_tag.len() < 2 { + "latest" } else { - tag = image_and_tag[1]; - } + image_and_tag[1] + }; let image_name = image_and_tag[0]; // Download image manifest @@ -81,7 +81,7 @@ fn download_manifest(image_name: &str, digest: &str) -> Result Date: Mon, 22 Apr 2024 16:51:11 +0200 Subject: [PATCH 21/21] fix: handle already existing temp folder Signed-off-by: Mathias-Boulay --- src/fs-gen/src/image_builder.rs | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/fs-gen/src/image_builder.rs b/src/fs-gen/src/image_builder.rs index 8d558ef..1d471f9 100644 --- a/src/fs-gen/src/image_builder.rs +++ b/src/fs-gen/src/image_builder.rs @@ -1,11 +1,12 @@ use std::{ - fs::{self}, + fs, path::{Path, PathBuf}, sync::Arc, thread, }; -use anyhow::{Context, Result}; +use anyhow::anyhow; +use anyhow::{Context, Ok, Result}; use fuse_backend_rs::{ api::{filesystem::Layer, server::Server}, overlayfs::{config::Config, OverlayFs}, @@ -13,6 +14,8 @@ use fuse_backend_rs::{ transport::{FuseChannel, FuseSession}, }; +static FILE_EXISTS_ERROR: i32 = 17; + pub struct FuseServer { server: Arc>>, ch: FuseChannel, @@ -41,12 +44,19 @@ fn new_passthroughfs_layer(rootdir: &str) -> Result { /// Ensure a destination folder is created fn ensure_folder_created(output_folder: &Path) -> Result<()> { - fs::create_dir(output_folder).with_context(|| { - format!( - "Failed to ensure folder creation: {}", - output_folder.to_string_lossy() - ) - }) + let result = fs::create_dir(output_folder); + + // If the file already exists, we're fine + if result.is_err() + && result + .unwrap_err() + .raw_os_error() + .is_some_and(|err_val| err_val != FILE_EXISTS_ERROR) + { + return Err(anyhow!("Failed to create folder")); + } + + Ok(()) } /// Merges all the layers into a single folder for further manipulation