diff --git a/src/fs-gen/Cargo.toml b/src/fs-gen/Cargo.toml index 6bccfdb..eb0b4eb 100644 --- a/src/fs-gen/Cargo.toml +++ b/src/fs-gen/Cargo.toml @@ -19,3 +19,6 @@ signal-hook = "0.3.17" tar = "0.4.40" validator = { version = "0.17.0", features = ["derive"] } anyhow = "1.0.82" +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +thiserror = "1.0.59" diff --git a/src/fs-gen/resources/initfile b/src/fs-gen/resources/initfile new file mode 100644 index 0000000..b405af9 --- /dev/null +++ b/src/fs-gen/resources/initfile @@ -0,0 +1,21 @@ +#! /bin/sh +# +# Cloudlet initramfs generation +# +mount -t devtmpfs dev /dev +mount -t proc proc /proc +mount -t sysfs sysfs /sys + +ip link set up dev lo + +slattach -L /dev/ttyS1& + +while ! ifconfig sl0 &> /dev/null; do + sleep 1 +done + +ifconfig sl0 172.30.0.11 netmask 255.255.0.0 up + +/agent + +reboot diff --git a/src/fs-gen/src/cli_args.rs b/src/fs-gen/src/cli_args.rs index dae89d6..a736113 100644 --- a/src/fs-gen/src/cli_args.rs +++ b/src/fs-gen/src/cli_args.rs @@ -1,22 +1,25 @@ use std::{env, path::PathBuf}; -use clap::{command, error::ErrorKind, CommandFactory, Parser}; +use clap::{command, error::ErrorKind, ArgAction, CommandFactory, Parser}; use regex::Regex; use once_cell::sync::Lazy; // 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() + 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, Clone)] #[command(version, about, long_about = None)] pub struct CliArgs { /// The name of the image to download pub image_name: String, + /// The host path to the guest agent binary + pub agent_host_path: PathBuf, + /// The path to the output file #[arg(short='o', long="output", default_value=get_default_output_file().into_os_string())] pub output_file: PathBuf, @@ -25,8 +28,14 @@ pub struct CliArgs { #[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, + #[arg(short='i', long="init", default_value=None)] + pub initfile_path: Option, + + #[arg(long = "arch", default_value = "amd64")] + pub architecture: String, + + #[arg(short='d', long="debug", action=ArgAction::SetTrue)] + pub debug: bool, } impl CliArgs { @@ -66,13 +75,12 @@ impl CliArgs { } } -/// Get the default output path for the cpio file. +/// Get the default temporary directory for the current execution. fn get_default_temp_directory() -> PathBuf { - let mut path = env::current_dir().unwrap(); - path.push(".cloudlet_temp/"); - path + PathBuf::from("/tmp/cloudlet-fs-gen") } +/// Get the default output file path for the generated initramfs. fn get_default_output_file() -> PathBuf { let mut path = env::current_dir().unwrap(); path.push("initramfs.img"); diff --git a/src/fs-gen/src/image_builder.rs b/src/fs-gen/src/image_builder.rs index 1d471f9..fa8803a 100644 --- a/src/fs-gen/src/image_builder.rs +++ b/src/fs-gen/src/image_builder.rs @@ -13,6 +13,7 @@ use fuse_backend_rs::{ passthrough::{self, PassthroughFs}, transport::{FuseChannel, FuseSession}, }; +use tracing::{debug, info, warn}; static FILE_EXISTS_ERROR: i32 = 17; @@ -44,16 +45,21 @@ fn new_passthroughfs_layer(rootdir: &str) -> Result { /// Ensure a destination folder is created fn ensure_folder_created(output_folder: &Path) -> Result<()> { - let result = fs::create_dir(output_folder); + let result = fs::create_dir_all(output_folder); // If the file already exists, we're fine - if result.is_err() - && result - .unwrap_err() + if result.is_err() { + let err = result.unwrap_err(); + + if err .raw_os_error() .is_some_and(|err_val| err_val != FILE_EXISTS_ERROR) - { - return Err(anyhow!("Failed to create folder")); + { + return Err(anyhow!(err)).context(format!( + "Failed to create folder: {}", + output_folder.to_string_lossy() + )); + } } Ok(()) @@ -63,16 +69,19 @@ fn ensure_folder_created(output_folder: &Path) -> Result<()> { /// 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") +/// merge_layer(vec!["source/layer_1", "source/layer_2"], "/tmp/fused_layers", "/tmp") /// ``` -pub fn merge_layer(blob_paths: &[PathBuf], output_folder: &Path) -> Result<()> { +pub fn merge_layer(blob_paths: &[PathBuf], output_folder: &Path, tmp_folder: &Path) -> Result<()> { + info!("Starting to merge layers..."); + // 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_string_lossy())?)); } - let mountpoint = Path::new("/tmp/cloudlet_internal"); + let binding = tmp_folder.join("overlayfs_mountpoint"); + let mountpoint = binding.as_path(); let fs_name = "cloudlet_overlay"; ensure_folder_created(mountpoint)?; @@ -80,7 +89,7 @@ pub fn merge_layer(blob_paths: &[PathBuf], output_folder: &Path) -> Result<()> { // Setup the overlay fs config let config = Config { - work: "/work".into(), + work: tmp_folder.join("work").to_string_lossy().into(), mountpoint: output_folder.to_string_lossy().into(), do_import: true, ..Default::default() @@ -110,7 +119,7 @@ pub fn merge_layer(blob_paths: &[PathBuf], output_folder: &Path) -> Result<()> { let _ = server.svc_loop(); }); - println!("copy starting !"); + debug!("Starting copy..."); //So now we need to copy the files dircpy::copy_dir(mountpoint, output_folder).with_context(|| { format!( @@ -118,7 +127,7 @@ pub fn merge_layer(blob_paths: &[PathBuf], output_folder: &Path) -> Result<()> { output_folder.to_string_lossy() ) })?; - println!("copy finished"); + debug!("Copy finished!"); // Unmount sessions so it can be re-used in later executions of the program se.wake() @@ -127,14 +136,15 @@ pub fn merge_layer(blob_paths: &[PathBuf], output_folder: &Path) -> Result<()> { .with_context(|| "Failed to unmount the fuse session".to_string())?; let _ = handle.join(); + + info!("Finished merging layers!"); Ok(()) } impl FuseServer { /// Run a loop to execute requests from the FUSE session - /// pub fn svc_loop(&mut self) -> Result<()> { - println!("entering server loop"); + debug!("entering server loop"); loop { let value = self .ch @@ -142,7 +152,7 @@ impl FuseServer { .with_context(|| "Failed to get message from fuse session".to_string())?; if value.is_none() { - println!("fuse server exits"); + debug!("fuse server exits"); break; } @@ -158,7 +168,7 @@ impl FuseServer { break; } _ => { - print!("Handling fuse message failed"); + warn!("Handling fuse message failed"); continue; } } diff --git a/src/fs-gen/src/image_loader.rs b/src/fs-gen/src/image_loader.rs deleted file mode 100644 index 45c77bb..0000000 --- a/src/fs-gen/src/image_loader.rs +++ /dev/null @@ -1,151 +0,0 @@ -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 tag = if image_and_tag.len() < 2 { - "latest" - } else { - 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 => 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"] - .as_str() - .unwrap() - == "amd64" - }); - - match amd64_manifest { - 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() { - Err("Couldn't find image layers in the manifest.")? - } - } - } - } - } - } - - 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 - ); - - 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_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(); - - 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(); - 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()?; - - print!(" - {}", digest); - - 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)?; - println!(" - unpacked"); - layer_paths.push(output_path); - } - Ok(layer_paths) -} diff --git a/src/fs-gen/src/initramfs_generator.rs b/src/fs-gen/src/initramfs_generator.rs index 76027dc..c216fa0 100644 --- a/src/fs-gen/src/initramfs_generator.rs +++ b/src/fs-gen/src/initramfs_generator.rs @@ -1,37 +1,61 @@ -use std::fs::{File, Permissions}; -use std::io::Write; +use anyhow::{Context, Result}; +use std::fs::{copy as fscopy, File, Permissions}; +use std::io::{copy as iocopy, Write}; use std::os::unix::fs::PermissionsExt; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; +use tracing::info; -const INIT_FILE: &[u8; 211] = b"#! /bin/sh -# -# Cloudlet initramfs generation -# -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(); +const INIT_FILE: &str = include_str!("../resources/initfile"); + +pub fn create_init_file(path: &Path, initfile: Option) -> Result<()> { + info!("Writing initfile..."); + + let destination = path.join("init"); + + if let Some(p) = initfile { + // if there is a given initfile, we copy it into the folder + fscopy(p, destination) + .with_context(|| "Failed to copy provided initfile to initramfs".to_string())?; + } else { + // if there is none, write the default init file + let mut file = File::create(destination).unwrap(); + file.set_permissions(Permissions::from_mode(0o755)).unwrap(); + + file.write_all(INIT_FILE.as_bytes()) + .with_context(|| "Failed to write default initfile to initramfs".to_string())?; + } + + info!("Initfile written!"); + + Ok(()) +} + +pub fn insert_agent(destination: &Path, agent_path: PathBuf) -> Result<()> { + info!("Inserting agent into fs..."); + + let mut file = File::create(destination.join("agent")) + .with_context(|| "Could not open agent file inside initramfs".to_string())?; + file.set_permissions(Permissions::from_mode(0o755)) + .with_context(|| "Failed to set permissions for agent file".to_string())?; + + let mut agent = + File::open(agent_path).with_context(|| "Could not open host agent file".to_string())?; + iocopy(&mut agent, &mut file) + .with_context(|| "Failed to copy agent contents from host to destination".to_string())?; + + info!("Agent inserted!"); + + Ok(()) } -pub fn generate_initramfs(root_directory: &Path, output: &Path) { - let file = File::create(output).unwrap(); +pub fn generate_initramfs(root_directory: &Path, output: &Path) -> Result<()> { + let file = File::create(output) + .with_context(|| "Could not open output file to write initramfs".to_string())?; file.set_permissions(Permissions::from_mode(0o644)) - .expect("Could not set permissions"); + .with_context(|| "Failed to set permissions for output file".to_string())?; - println!("Generating initramfs..."); + info!("Generating initramfs..."); let mut command = Command::new("sh") .current_dir(root_directory) @@ -39,10 +63,13 @@ pub fn generate_initramfs(root_directory: &Path, output: &Path) { .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"); + .with_context(|| "Failed to package initramfs into bundle".to_string())?; + + command.wait().with_context(|| { + "Encountered exception while waiting for bundling to finish".to_string() + })?; + + info!("Initramfs generated!"); - println!("Initramfs generated!"); + Ok(()) } diff --git a/src/fs-gen/src/loader/download.rs b/src/fs-gen/src/loader/download.rs new file mode 100644 index 0000000..1cc8c65 --- /dev/null +++ b/src/fs-gen/src/loader/download.rs @@ -0,0 +1,167 @@ +use crate::loader::errors::ImageLoaderError; +use crate::loader::utils::{get_docker_download_token, unpack_tarball}; +use anyhow::{Context, Result}; +use reqwest::blocking::Client; +use serde_json::Value; +use std::fs::create_dir_all; +use std::path::{Path, PathBuf}; +use tracing::{debug, info, warn}; + +pub(crate) fn download_image_fs( + image_name: &str, + architecture: &str, + output_file: PathBuf, +) -> Result, ImageLoaderError> { + info!("Downloading image..."); + + // Get image's name and tag + let image_and_tag: Vec<&str> = image_name.split(':').collect(); + let image_name = image_and_tag[0]; + let tag = if image_and_tag.len() < 2 { + "latest" + } else { + image_and_tag[1] + }; + + // Get download token and download manifest + let client = Client::new(); + let token = &get_docker_download_token(&client, image_name)?; + let manifest = download_manifest(&client, token, image_name, tag) + .map_err(|e| ImageLoaderError::Error { source: e })?; + + if let Some(layers) = manifest["layers"].as_array() { + // We have layers already, no need to look into sub-manifests. + info!("Found layers in manifest"); + warn!( + architecture, + "Manifest did not specify architecture, the initramfs may not work for the requested architecture" + ); + create_dir_all(&output_file) + .with_context(|| "Could not create output directory for image downloading")?; + return download_layers(layers, &client, token, image_name, &output_file) + .map_err(|e| ImageLoaderError::Error { source: e }); + } + + // Below, we assume there are no layers found. + // We dig into sub-manifests to try and find a suitable one to download, with hopefully layers inside. + + let manifest_list = match manifest["manifests"].as_array() { + // No sub-manifests found, we throw an error. + None => Err(ImageLoaderError::ManifestNotFound( + image_name.to_string(), + tag.to_string(), + ))?, + Some(m) => m, + }; + info!( + architecture, + "Manifest list found. Looking for an architecture-specific manifest..." + ); + + let arch_specific_manifest = manifest_list.iter().find(|manifest| { + manifest["platform"].as_object().unwrap()["architecture"] + .as_str() + .unwrap() + == architecture + }); + + let submanifest = match arch_specific_manifest { + None => Err(ImageLoaderError::UnsupportedArchitecture( + architecture.to_string(), + ))?, + Some(m) => { + debug!("Downloading architecture-specific manifest"); + + download_manifest(&client, token, image_name, m["digest"].as_str().unwrap()) + .map_err(|e| ImageLoaderError::Error { source: e })? + } + }; + + match submanifest["layers"].as_array() { + None => Err(ImageLoaderError::LayersNotFound)?, + Some(layers) => { + create_dir_all(&output_file) + .with_context(|| "Could not create output directory for image downloading")?; + download_layers(layers, &client, token, image_name, &output_file) + .map_err(|e| ImageLoaderError::Error { source: e }) + } + } +} + +fn download_manifest( + client: &Client, + token: &str, + image_name: &str, + digest: &str, +) -> Result { + // Query Docker Hub API to get the image manifest + let manifest_url = format!( + "https://registry-1.docker.io/v2/library/{}/manifests/{}", + image_name, digest + ); + + let manifest: Value = 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() + .with_context(|| "Could not send request to get manifest data".to_string())? + .json() + .with_context(|| "Failed to parse manifest to JSON".to_string())?; + + debug!( + manifest = ?manifest, + "downloaded manifest: " + ); + + Ok(manifest) +} + +fn download_layers( + layers: &Vec, + client: &Client, + token: &str, + image_name: &str, + output_dir: &Path, +) -> Result> { + info!("Downloading and unpacking layers..."); + + let mut layer_paths = Vec::new(); + + // Download and unpack each layer + for layer in layers { + let digest = layer["digest"] + .as_str() + .with_context(|| "Failed to get digest for layer".to_string())?; + 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() + .with_context(|| format!("Could not send request for layer digest '{digest}'"))?; + + debug!("starting to decode layer with digest '{}'", digest); + + let output_path = output_dir.join(digest); + + unpack_tarball(response, &output_path)?; + debug!("layer '{}' unpacked", digest); + layer_paths.push(output_path); + } + + info!("Layers downloaded successfully!"); + + Ok(layer_paths) +} diff --git a/src/fs-gen/src/loader/errors.rs b/src/fs-gen/src/loader/errors.rs new file mode 100644 index 0000000..d262c04 --- /dev/null +++ b/src/fs-gen/src/loader/errors.rs @@ -0,0 +1,27 @@ +use anyhow::Error; +use thiserror::Error; + +#[derive(Debug, Error)] +pub(crate) enum ImageLoaderError { + /// There is no existing manifest for the given image. + #[error("Could not find Docker v2 or OCI manifest for `{0}:{1}`")] + ManifestNotFound(String, String), + + /// Image doesn't support the requested architecture. + #[error("This image doesn't support {0} architecture")] + UnsupportedArchitecture(String), + + /// The manifest doesn't contain any layers to unpack. + #[error("Could not find image layers in the manifest")] + LayersNotFound, + + /// Encountered an error during the flow. + #[error("Image loading error: {}", .source)] + Error { source: anyhow::Error }, +} + +impl From for ImageLoaderError { + fn from(value: Error) -> Self { + Self::Error { source: value } + } +} diff --git a/src/fs-gen/src/loader/mod.rs b/src/fs-gen/src/loader/mod.rs new file mode 100644 index 0000000..f79903f --- /dev/null +++ b/src/fs-gen/src/loader/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod download; +pub(crate) mod errors; +mod utils; diff --git a/src/fs-gen/src/loader/utils.rs b/src/fs-gen/src/loader/utils.rs new file mode 100644 index 0000000..d65d247 --- /dev/null +++ b/src/fs-gen/src/loader/utils.rs @@ -0,0 +1,29 @@ +use anyhow::{Context, Result}; +use flate2::read::GzDecoder; +use reqwest::blocking::{Client, Response}; +use std::path::Path; +use tar::Archive; + +/// Unpack the tarball to a given directory. +pub(super) fn unpack_tarball(response: Response, output_dir: &Path) -> Result<()> { + Archive::new(GzDecoder::new(response)) + .unpack(output_dir) + .with_context(|| format!("Failed to unpack tarball to {}", output_dir.display()))?; + Ok(()) +} + +/// Get a token for anonymous authentication to Docker Hub. +pub(super) fn get_docker_download_token(client: &Client, image_name: &str) -> Result { + 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().with_context(|| "Could not send request for anonymous authentication".to_string())? + .json().with_context(|| "Failed to parse JSON response for anonymous authentication".to_string())?; + + match token_json["token"] + .as_str() + .with_context(|| "Failed to get token from anon auth response".to_string()) + { + Ok(t) => Ok(t.to_owned()), + Err(e) => Err(e), + } +} diff --git a/src/fs-gen/src/main.rs b/src/fs-gen/src/main.rs index 8229e85..045e1c0 100644 --- a/src/fs-gen/src/main.rs +++ b/src/fs-gen/src/main.rs @@ -1,41 +1,94 @@ +use anyhow::{bail, Context, Result}; use std::{fs::remove_dir_all, path::Path}; +use tracing::level_filters::LevelFilter; +use tracing::{debug, error, info}; +use tracing_subscriber::filter::EnvFilter; -use crate::initramfs_generator::{create_init_file, generate_initramfs}; -use image_builder::merge_layer; +use crate::cli_args::CliArgs; +use crate::image_builder::merge_layer; +use crate::initramfs_generator::{create_init_file, generate_initramfs, insert_agent}; +use crate::loader::download::download_image_fs; mod cli_args; mod image_builder; -mod image_loader; mod initramfs_generator; +mod loader; -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, layers_subdir) { - Err(e) => { - eprintln!("Error: {}", e); - return; - } - Ok(layers_paths) => { - println!("Image downloaded successfully! Layers' paths:"); - for path in &layers_paths { - println!(" - {}", path.display()); - } - - // FIXME: use a subdir of the temp directory instead - let path = Path::new(overlay_subdir.as_path()); - - merge_layer(&layers_paths, path).expect("Merging layers failed"); - create_init_file(path); - generate_initramfs(path, Path::new(args.output_file.as_path())); - } - } +fn run(args: CliArgs) -> Result<()> { + let layers_subdir = args.temp_directory.join("layers/"); + let overlay_subdir = args.temp_directory.join("overlay/"); + let _binding = args.temp_directory.join("output/"); + let output_subdir = _binding.as_path(); + + // image downloading and unpacking + let layers_paths = match download_image_fs(&args.image_name, &args.architecture, layers_subdir) + { + Err(e) => bail!(e), + Ok(e) => e, + }; + debug!("Layers' paths: {:?}", layers_paths); + + // reconstructing image with overlayfs + merge_layer(&layers_paths, output_subdir, &overlay_subdir)?; + + // building initramfs + create_init_file(output_subdir, args.initfile_path)?; + insert_agent(output_subdir, args.agent_host_path)?; + generate_initramfs(output_subdir, Path::new(args.output_file.as_path()))?; // cleanup of temporary directory - remove_dir_all(args.temp_directory.clone()).expect("Could not remove temporary directory"); + remove_dir_all(args.temp_directory.clone()) + .with_context(|| "Failed to remove temporary directory".to_string())?; + + Ok(()) +} + +fn main() -> Result<()> { + let args = CliArgs::get_args(); + + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::builder() + .with_default_directive( + (if args.debug { + LevelFilter::DEBUG + } else { + LevelFilter::INFO + }) + .into(), + ) + .from_env()? + .add_directive("fuse_backend_rs=warn".parse()?), + ) + .init(); + + // tracing_subscriber::fmt() + // .with_max_level(if args.debug { Level::DEBUG } else { Level::INFO }) + // .init(); + + info!( + "Cloudlet initramfs generator: '{}' v{}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + ); + info!("Generating for image '{}'", args.image_name); + + debug!( + image_name = args.image_name, + agent_host_path = ?args.agent_host_path, + output_file = ?args.output_file, + temp_dir = ?args.temp_directory, + initfile_path = ?args.initfile_path, + architecture = args.architecture, + debug = args.debug, + "arguments:", + ); + + if let Err(e) = run(args) { + error!(error = ?e, "encountered error while running"); + Err(e) + } else { + info!("Finished successfully!"); + Ok(()) + } } diff --git a/src/fs-gen/test b/src/fs-gen/test deleted file mode 100644 index e69de29..0000000