diff --git a/Cargo.lock b/Cargo.lock index 5069f7a..5ff9954 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1459,11 +1459,13 @@ dependencies = [ "backhand", "bytes", "env_logger", + "indexmap 2.2.6", "krata-tokio-tar", "log", "oci-spec", "path-clean", "reqwest", + "scopeguard", "serde", "serde_json", "sha256", diff --git a/Cargo.toml b/Cargo.toml index c75d3af..7b395f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ fancy-duration = "0.9.2" flate2 = "1.0" futures = "0.3.30" human_bytes = "0.4" +indexmap = "2.2.6" indicatif = "0.17.8" ipnetwork = "0.20.0" libc = "0.2" diff --git a/DEV.md b/DEV.md index 9898701..aff5e3f 100644 --- a/DEV.md +++ b/DEV.md @@ -28,7 +28,12 @@ it's corresponding code path from the above table. 1. Install the specified Debian version on a x86_64 host _capable_ of KVM (NOTE: KVM is not used, Xen is a type-1 hypervisor). -2. Install required packages: `apt install git xen-system-amd64 flex bison libelf-dev libssl-dev bc` +2. Install required packages: + +```sh +$ apt install git xen-system-amd64 flex bison libelf-dev libssl-dev bc protobuf-compiler libprotobuf-dev squashfs-tools erofs-utils +``` + 3. Install [rustup](https://rustup.rs) for managing a Rust environment. diff --git a/crates/ctl/src/cli/launch.rs b/crates/ctl/src/cli/launch.rs index 360a7eb..83f2364 100644 --- a/crates/ctl/src/cli/launch.rs +++ b/crates/ctl/src/cli/launch.rs @@ -182,8 +182,7 @@ async fn wait_guest_started(id: &str, events: EventStream) -> Result<()> { for layer in &oci.layers { let bar = ProgressBar::new(layer.total); bar.set_style( - ProgressStyle::with_template("{msg} {wide_bar} {pos}/{len}") - .unwrap(), + ProgressStyle::with_template("{msg} {wide_bar}").unwrap(), ); progresses.insert(layer.id.clone(), bar.clone()); multi_progress.add(bar); @@ -204,35 +203,54 @@ async fn wait_guest_started(id: &str, events: EventStream) -> Result<()> { _ => "unknown", }; - progress.set_message(format!("{} {}", layer.id, phase)); - progress.set_length(layer.total); - progress.set_position(layer.value); + let simple = if let Some((_, hash)) = layer.id.split_once(':') { + hash + } else { + id + }; + let simple = if simple.len() > 10 { + &simple[0..10] + } else { + simple + }; + let message = format!("{:width$} {}", simple, phase, width = 10); + + if message != progress.message() { + progress.set_message(message); + } + + progress.update(|state| { + state.set_len(layer.total); + state.set_pos(layer.value); + }); } } OciProgressEventPhase::Packing => { - for (key, progress) in &mut *progresses { + for (key, bar) in &mut *progresses { if key == "packing" { continue; } - progress.finish_and_clear(); - multi_progress.remove(progress); + bar.finish_and_clear(); + multi_progress.remove(bar); } progresses.retain(|k, _| k == "packing"); if progresses.is_empty() { let progress = ProgressBar::new(100); + progress.set_message("packing"); progress.set_style( - ProgressStyle::with_template("{msg} {wide_bar} {pos}/{len}") - .unwrap(), + ProgressStyle::with_template("{msg} {wide_bar}").unwrap(), ); progresses.insert("packing".to_string(), progress); } let Some(progress) = progresses.get("packing") else { continue; }; - progress.set_message("packing image"); - progress.set_length(oci.total); - progress.set_position(oci.value); + + progress.update(|state| { + state.set_len(oci.total); + state.set_pos(oci.value); + }); } _ => {} diff --git a/crates/guest/src/init.rs b/crates/guest/src/init.rs index ca7cc25..1768035 100644 --- a/crates/guest/src/init.rs +++ b/crates/guest/src/init.rs @@ -98,11 +98,19 @@ impl GuestInit { if result != 0 { warn!("failed to set hostname: {}", result); } + + let etc = PathBuf::from_str("/etc")?; + if !etc.exists() { + fs::create_dir(&etc).await?; + } + let mut etc_hostname = etc; + etc_hostname.push("hostname"); + fs::write(&etc_hostname, hostname + "\n").await?; } if let Some(network) = &launch.network { trace!("initializing network"); - if let Err(error) = self.network_setup(network).await { + if let Err(error) = self.network_setup(&launch, network).await { warn!("failed to initialize network: {}", error); } } @@ -287,7 +295,7 @@ impl GuestInit { Ok(()) } - async fn network_setup(&mut self, network: &LaunchNetwork) -> Result<()> { + async fn network_setup(&mut self, cfg: &LaunchInfo, network: &LaunchNetwork) -> Result<()> { trace!("setting up network for link"); let etc = PathBuf::from_str("/etc")?; @@ -295,14 +303,33 @@ impl GuestInit { fs::create_dir(etc).await?; } let resolv = PathBuf::from_str("/etc/resolv.conf")?; - let mut lines = vec!["# krata resolver configuration".to_string()]; - for nameserver in &network.resolver.nameservers { - lines.push(format!("nameserver {}", nameserver)); + + { + let mut lines = vec!["# krata resolver configuration".to_string()]; + for nameserver in &network.resolver.nameservers { + lines.push(format!("nameserver {}", nameserver)); + } + + let mut conf = lines.join("\n"); + conf.push('\n'); + fs::write(resolv, conf).await?; + } + + let hosts = PathBuf::from_str("/etc/hosts")?; + if let Some(ref hostname) = cfg.hostname { + let mut lines = if hosts.exists() { + fs::read_to_string(&hosts) + .await? + .lines() + .map(|x| x.to_string()) + .collect::>() + } else { + vec!["127.0.0.1 localhost".to_string()] + }; + lines.push(format!("127.0.1.1 {}", hostname)); + fs::write(&hosts, lines.join("\n") + "\n").await?; } - let mut conf = lines.join("\n"); - conf.push('\n'); - fs::write(resolv, conf).await?; self.network_configure_ethtool(network).await?; self.network_configure_link(network).await?; Ok(()) diff --git a/crates/oci/Cargo.toml b/crates/oci/Cargo.toml index 108f477..e5983fe 100644 --- a/crates/oci/Cargo.toml +++ b/crates/oci/Cargo.toml @@ -14,11 +14,13 @@ async-compression = { workspace = true, features = ["tokio", "gzip", "zstd"] } async-trait = { workspace = true } backhand = { workspace = true } bytes = { workspace = true } +indexmap = { workspace = true } krata-tokio-tar = { workspace = true } log = { workspace = true } oci-spec = { workspace = true } path-clean = { workspace = true } reqwest = { workspace = true } +scopeguard = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } sha256 = { workspace = true } diff --git a/crates/oci/examples/squashify.rs b/crates/oci/examples/squashify.rs index 8ad0f5e..77068ba 100644 --- a/crates/oci/examples/squashify.rs +++ b/crates/oci/examples/squashify.rs @@ -3,7 +3,11 @@ use std::{env::args, path::PathBuf}; use anyhow::Result; use env_logger::Env; use krataoci::{ - cache::ImageCache, compiler::ImageCompiler, name::ImageName, progress::OciProgressContext, + cache::ImageCache, + compiler::OciImageCompiler, + name::ImageName, + packer::OciPackerFormat, + progress::{OciProgress, OciProgressContext}, }; use tokio::{fs, sync::broadcast}; @@ -21,17 +25,26 @@ async fn main() -> Result<()> { let cache = ImageCache::new(&cache_dir)?; - let (sender, mut receiver) = broadcast::channel(1000); + let (sender, mut receiver) = broadcast::channel::(1000); tokio::task::spawn(async move { loop { - let Some(_) = receiver.recv().await.ok() else { + let Some(progress) = receiver.recv().await.ok() else { break; }; + println!("phase {:?}", progress.phase); + for (id, layer) in progress.layers { + println!( + "{} {:?} {} of {}", + id, layer.phase, layer.value, layer.total + ) + } } }); let context = OciProgressContext::new(sender); - let compiler = ImageCompiler::new(&cache, seed, context)?; - let info = compiler.compile(&image.to_string(), &image).await?; + let compiler = OciImageCompiler::new(&cache, seed, context)?; + let info = compiler + .compile(&image.to_string(), &image, OciPackerFormat::Squashfs) + .await?; println!( "generated squashfs of {} to {}", image, diff --git a/crates/oci/src/cache.rs b/crates/oci/src/cache.rs index 7f6e673..bddb9cf 100644 --- a/crates/oci/src/cache.rs +++ b/crates/oci/src/cache.rs @@ -1,3 +1,5 @@ +use crate::packer::OciPackerFormat; + use super::compiler::ImageInfo; use anyhow::Result; use log::debug; @@ -17,16 +19,16 @@ impl ImageCache { }) } - pub async fn recall(&self, digest: &str) -> Result> { - let mut squashfs_path = self.cache_dir.clone(); + pub async fn recall(&self, digest: &str, format: OciPackerFormat) -> Result> { + let mut fs_path = self.cache_dir.clone(); let mut config_path = self.cache_dir.clone(); let mut manifest_path = self.cache_dir.clone(); - squashfs_path.push(format!("{}.squashfs", digest)); + fs_path.push(format!("{}.{}", digest, format.extension())); manifest_path.push(format!("{}.manifest.json", digest)); config_path.push(format!("{}.config.json", digest)); Ok( - if squashfs_path.exists() && manifest_path.exists() && config_path.exists() { - let squashfs_metadata = fs::metadata(&squashfs_path).await?; + if fs_path.exists() && manifest_path.exists() && config_path.exists() { + let squashfs_metadata = fs::metadata(&fs_path).await?; let manifest_metadata = fs::metadata(&manifest_path).await?; let config_metadata = fs::metadata(&config_path).await?; if squashfs_metadata.is_file() @@ -38,7 +40,7 @@ impl ImageCache { let config_text = fs::read_to_string(&config_path).await?; let config: ImageConfiguration = serde_json::from_str(&config_text)?; debug!("cache hit digest={}", digest); - Some(ImageInfo::new(squashfs_path.clone(), manifest, config)?) + Some(ImageInfo::new(fs_path.clone(), manifest, config)?) } else { None } @@ -49,23 +51,24 @@ impl ImageCache { ) } - pub async fn store(&self, digest: &str, info: &ImageInfo) -> Result { + pub async fn store( + &self, + digest: &str, + info: &ImageInfo, + format: OciPackerFormat, + ) -> Result { debug!("cache store digest={}", digest); - let mut squashfs_path = self.cache_dir.clone(); + let mut fs_path = self.cache_dir.clone(); let mut manifest_path = self.cache_dir.clone(); let mut config_path = self.cache_dir.clone(); - squashfs_path.push(format!("{}.squashfs", digest)); + fs_path.push(format!("{}.{}", digest, format.extension())); manifest_path.push(format!("{}.manifest.json", digest)); config_path.push(format!("{}.config.json", digest)); - fs::copy(&info.image_squashfs, &squashfs_path).await?; + fs::copy(&info.image_squashfs, &fs_path).await?; let manifest_text = serde_json::to_string_pretty(&info.manifest)?; fs::write(&manifest_path, manifest_text).await?; let config_text = serde_json::to_string_pretty(&info.config)?; fs::write(&config_path, config_text).await?; - ImageInfo::new( - squashfs_path.clone(), - info.manifest.clone(), - info.config.clone(), - ) + ImageInfo::new(fs_path.clone(), info.manifest.clone(), info.config.clone()) } } diff --git a/crates/oci/src/compiler.rs b/crates/oci/src/compiler.rs index 5d3a406..94b800b 100644 --- a/crates/oci/src/compiler.rs +++ b/crates/oci/src/compiler.rs @@ -1,18 +1,14 @@ use crate::cache::ImageCache; use crate::fetch::{OciImageDownloader, OciImageLayer}; use crate::name::ImageName; +use crate::packer::OciPackerFormat; use crate::progress::{OciProgress, OciProgressContext, OciProgressPhase}; use crate::registry::OciRegistryPlatform; use anyhow::{anyhow, Result}; -use backhand::compression::Compressor; -use backhand::{FilesystemCompressor, FilesystemWriter, NodeHeader}; -use log::{debug, trace, warn}; +use indexmap::IndexMap; +use log::{debug, trace}; use oci_spec::image::{ImageConfiguration, ImageManifest}; use std::borrow::Cow; -use std::collections::BTreeMap; -use std::fs::File; -use std::io::{BufWriter, ErrorKind, Read}; -use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; use std::path::{Path, PathBuf}; use std::pin::Pin; use tokio::fs; @@ -20,9 +16,8 @@ use tokio::io::AsyncRead; use tokio_stream::StreamExt; use tokio_tar::{Archive, Entry}; use uuid::Uuid; -use walkdir::WalkDir; -pub const IMAGE_SQUASHFS_VERSION: u64 = 2; +pub const IMAGE_PACKER_VERSION: u64 = 2; pub struct ImageInfo { pub image_squashfs: PathBuf, @@ -44,27 +39,32 @@ impl ImageInfo { } } -pub struct ImageCompiler<'a> { +pub struct OciImageCompiler<'a> { cache: &'a ImageCache, seed: Option, progress: OciProgressContext, } -impl ImageCompiler<'_> { +impl OciImageCompiler<'_> { pub fn new( cache: &ImageCache, seed: Option, progress: OciProgressContext, - ) -> Result { - Ok(ImageCompiler { + ) -> Result { + Ok(OciImageCompiler { cache, seed, progress, }) } - pub async fn compile(&self, id: &str, image: &ImageName) -> Result { - debug!("compile image={image}"); + pub async fn compile( + &self, + id: &str, + image: &ImageName, + format: OciPackerFormat, + ) -> Result { + debug!("compile image={image} format={:?}", format); let mut tmp_dir = std::env::temp_dir().clone(); tmp_dir.push(format!("krata-compile-{}", Uuid::new_v4())); @@ -78,10 +78,15 @@ impl ImageCompiler<'_> { let mut squash_file = tmp_dir.clone(); squash_file.push("image.squashfs"); + + let _guard = scopeguard::guard(tmp_dir.to_path_buf(), |delete| { + tokio::task::spawn(async move { + let _ = fs::remove_dir_all(delete).await; + }); + }); let info = self - .download_and_compile(id, image, &layer_dir, &image_dir, &squash_file) + .download_and_compile(id, image, &layer_dir, &image_dir, &squash_file, format) .await?; - fs::remove_dir_all(&tmp_dir).await?; Ok(info) } @@ -92,11 +97,12 @@ impl ImageCompiler<'_> { layer_dir: &Path, image_dir: &Path, squash_file: &Path, + format: OciPackerFormat, ) -> Result { let mut progress = OciProgress { id: id.to_string(), phase: OciProgressPhase::Resolving, - layers: BTreeMap::new(), + layers: IndexMap::new(), value: 0, total: 0, }; @@ -109,14 +115,14 @@ impl ImageCompiler<'_> { ); let resolved = downloader.resolve(image.clone()).await?; let cache_key = format!( - "manifest={}:squashfs-version={}\n", - resolved.digest, IMAGE_SQUASHFS_VERSION + "manifest={}:version={}:format={}\n", + resolved.digest, + IMAGE_PACKER_VERSION, + format.id(), ); let cache_digest = sha256::digest(cache_key); - progress.phase = OciProgressPhase::Complete; - self.progress.update(&progress); - if let Some(cached) = self.cache.recall(&cache_digest).await? { + if let Some(cached) = self.cache.recall(&cache_digest, format).await? { return Ok(cached); } @@ -132,7 +138,7 @@ impl ImageCompiler<'_> { "process layer digest={} compression={:?}", &layer.digest, layer.compression, ); - progress.extracting_layer(&layer.digest, 0, 0); + progress.extracting_layer(&layer.digest, 0, 1); self.progress.update(&progress); let (whiteouts, count) = self.process_layer_whiteout(layer, image_dir).await?; progress.extracting_layer(&layer.digest, 0, count); @@ -149,7 +155,9 @@ impl ImageCompiler<'_> { let path = entry.path()?; let mut maybe_whiteout_path_str = path.to_str().map(|x| x.to_string()).unwrap_or_default(); - progress.extracting_layer(&layer.digest, completed, count); + if (completed % 10) == 0 { + progress.extracting_layer(&layer.digest, completed, count); + } completed += 1; self.progress.update(&progress); if whiteouts.contains(&maybe_whiteout_path_str) { @@ -188,7 +196,8 @@ impl ImageCompiler<'_> { let progress_squash = progress.clone(); let progress_context = self.progress.clone(); progress = tokio::task::spawn_blocking(move || { - ImageCompiler::squash( + OciImageCompiler::pack( + OciPackerFormat::Squashfs, &image_dir_squash, &squash_file_squash, progress_squash, @@ -202,7 +211,7 @@ impl ImageCompiler<'_> { local.image.manifest, local.config, )?; - let info = self.cache.store(&cache_digest, &info).await?; + let info = self.cache.store(&cache_digest, &info, format).await?; progress.phase = OciProgressPhase::Complete; progress.value = 0; progress.total = 0; @@ -359,128 +368,21 @@ impl ImageCompiler<'_> { Ok(()) } - fn squash( + fn pack( + format: OciPackerFormat, image_dir: &Path, squash_file: &Path, mut progress: OciProgress, progress_context: OciProgressContext, ) -> Result { - progress.phase = OciProgressPhase::Packing; - progress.total = 2; - progress.value = 0; progress_context.update(&progress); - let mut writer = FilesystemWriter::default(); - writer.set_compressor(FilesystemCompressor::new(Compressor::Gzip, None)?); - let walk = WalkDir::new(image_dir).follow_links(false); - for entry in walk { - let entry = entry?; - let rel = entry - .path() - .strip_prefix(image_dir)? - .to_str() - .ok_or_else(|| anyhow!("failed to strip prefix of tmpdir"))?; - let rel = format!("/{}", rel); - trace!("squash write {}", rel); - let typ = entry.file_type(); - let metadata = std::fs::symlink_metadata(entry.path())?; - let uid = metadata.uid(); - let gid = metadata.gid(); - let mode = metadata.permissions().mode(); - let mtime = metadata.mtime(); - - if rel == "/" { - writer.set_root_uid(uid); - writer.set_root_gid(gid); - writer.set_root_mode(mode as u16); - continue; - } - - let header = NodeHeader { - permissions: mode as u16, - uid, - gid, - mtime: mtime as u32, - }; - if typ.is_symlink() { - let symlink = std::fs::read_link(entry.path())?; - let symlink = symlink - .to_str() - .ok_or_else(|| anyhow!("failed to read symlink"))?; - writer.push_symlink(symlink, rel, header)?; - } else if typ.is_dir() { - writer.push_dir(rel, header)?; - } else if typ.is_file() { - writer.push_file(ConsumingFileReader::new(entry.path()), rel, header)?; - } else if typ.is_block_device() { - let device = metadata.dev(); - writer.push_block_device(device as u32, rel, header)?; - } else if typ.is_char_device() { - let device = metadata.dev(); - writer.push_char_device(device as u32, rel, header)?; - } else if typ.is_fifo() { - writer.push_fifo(rel, header)?; - } else if typ.is_socket() { - writer.push_socket(rel, header)?; - } else { - return Err(anyhow!("invalid file type")); - } - } - - progress.phase = OciProgressPhase::Packing; - progress.value = 1; - progress_context.update(&progress); - - let squash_file_path = squash_file - .to_str() - .ok_or_else(|| anyhow!("failed to convert squashfs string"))?; - - let file = File::create(squash_file)?; - let mut bufwrite = BufWriter::new(file); - trace!("squash generate: {}", squash_file_path); - writer.write(&mut bufwrite)?; + let backend = format.detect_best_backend(); + let backend = backend.create(); + backend.pack(&mut progress, &progress_context, image_dir, squash_file)?; std::fs::remove_dir_all(image_dir)?; progress.phase = OciProgressPhase::Packing; - progress.value = 2; + progress.value = progress.total; progress_context.update(&progress); Ok(progress) } } - -struct ConsumingFileReader { - path: PathBuf, - file: Option, -} - -impl ConsumingFileReader { - fn new(path: &Path) -> ConsumingFileReader { - ConsumingFileReader { - path: path.to_path_buf(), - file: None, - } - } -} - -impl Read for ConsumingFileReader { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - if self.file.is_none() { - self.file = Some(File::open(&self.path)?); - } - let Some(ref mut file) = self.file else { - return Err(std::io::Error::new( - ErrorKind::NotFound, - "file was not opened", - )); - }; - file.read(buf) - } -} - -impl Drop for ConsumingFileReader { - fn drop(&mut self) { - let file = self.file.take(); - drop(file); - if let Err(error) = std::fs::remove_file(&self.path) { - warn!("failed to delete consuming file {:?}: {}", self.path, error); - } - } -} diff --git a/crates/oci/src/lib.rs b/crates/oci/src/lib.rs index ac3471a..1b117a7 100644 --- a/crates/oci/src/lib.rs +++ b/crates/oci/src/lib.rs @@ -2,5 +2,6 @@ pub mod cache; pub mod compiler; pub mod fetch; pub mod name; +pub mod packer; pub mod progress; pub mod registry; diff --git a/crates/oci/src/packer.rs b/crates/oci/src/packer.rs new file mode 100644 index 0000000..2022ec0 --- /dev/null +++ b/crates/oci/src/packer.rs @@ -0,0 +1,307 @@ +use std::{ + fs::File, + io::{BufWriter, ErrorKind, Read}, + os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}, + path::{Path, PathBuf}, + process::{Command, Stdio}, +}; + +use anyhow::{anyhow, Result}; +use backhand::{compression::Compressor, FilesystemCompressor, FilesystemWriter, NodeHeader}; +use log::{trace, warn}; +use walkdir::WalkDir; + +use crate::progress::{OciProgress, OciProgressContext, OciProgressPhase}; + +#[derive(Debug, Default, Clone, Copy)] +pub enum OciPackerFormat { + #[default] + Squashfs, + Erofs, +} + +#[derive(Debug, Clone, Copy)] +pub enum OciPackerBackendType { + Backhand, + MkSquashfs, + MkfsErofs, +} + +impl OciPackerFormat { + pub fn id(&self) -> u8 { + match self { + OciPackerFormat::Squashfs => 0, + OciPackerFormat::Erofs => 1, + } + } + + pub fn extension(&self) -> &str { + match self { + OciPackerFormat::Squashfs => "erofs", + OciPackerFormat::Erofs => "erofs", + } + } + + pub fn detect_best_backend(&self) -> OciPackerBackendType { + match self { + OciPackerFormat::Squashfs => { + let status = Command::new("mksquashfs") + .arg("-version") + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .stdout(Stdio::null()) + .status() + .ok(); + + let Some(code) = status.and_then(|x| x.code()) else { + return OciPackerBackendType::Backhand; + }; + + if code == 0 { + OciPackerBackendType::MkSquashfs + } else { + OciPackerBackendType::Backhand + } + } + OciPackerFormat::Erofs => OciPackerBackendType::MkfsErofs, + } + } +} + +impl OciPackerBackendType { + pub fn format(&self) -> OciPackerFormat { + match self { + OciPackerBackendType::Backhand => OciPackerFormat::Squashfs, + OciPackerBackendType::MkSquashfs => OciPackerFormat::Squashfs, + OciPackerBackendType::MkfsErofs => OciPackerFormat::Erofs, + } + } + + pub fn create(&self) -> Box { + match self { + OciPackerBackendType::Backhand => { + Box::new(OciPackerBackhand {}) as Box + } + OciPackerBackendType::MkSquashfs => { + Box::new(OciPackerMkSquashfs {}) as Box + } + OciPackerBackendType::MkfsErofs => { + Box::new(OciPackerMkfsErofs {}) as Box + } + } + } +} + +pub trait OciPackerBackend { + fn pack( + &self, + progress: &mut OciProgress, + progress_context: &OciProgressContext, + directory: &Path, + file: &Path, + ) -> Result<()>; +} + +pub struct OciPackerBackhand {} + +impl OciPackerBackend for OciPackerBackhand { + fn pack( + &self, + progress: &mut OciProgress, + progress_context: &OciProgressContext, + directory: &Path, + file: &Path, + ) -> Result<()> { + progress.phase = OciProgressPhase::Packing; + progress.total = 1; + progress.value = 0; + progress_context.update(progress); + let mut writer = FilesystemWriter::default(); + writer.set_compressor(FilesystemCompressor::new(Compressor::Gzip, None)?); + let walk = WalkDir::new(directory).follow_links(false); + for entry in walk { + let entry = entry?; + let rel = entry + .path() + .strip_prefix(directory)? + .to_str() + .ok_or_else(|| anyhow!("failed to strip prefix of tmpdir"))?; + let rel = format!("/{}", rel); + trace!("squash write {}", rel); + let typ = entry.file_type(); + let metadata = std::fs::symlink_metadata(entry.path())?; + let uid = metadata.uid(); + let gid = metadata.gid(); + let mode = metadata.permissions().mode(); + let mtime = metadata.mtime(); + + if rel == "/" { + writer.set_root_uid(uid); + writer.set_root_gid(gid); + writer.set_root_mode(mode as u16); + continue; + } + + let header = NodeHeader { + permissions: mode as u16, + uid, + gid, + mtime: mtime as u32, + }; + if typ.is_symlink() { + let symlink = std::fs::read_link(entry.path())?; + let symlink = symlink + .to_str() + .ok_or_else(|| anyhow!("failed to read symlink"))?; + writer.push_symlink(symlink, rel, header)?; + } else if typ.is_dir() { + writer.push_dir(rel, header)?; + } else if typ.is_file() { + writer.push_file(ConsumingFileReader::new(entry.path()), rel, header)?; + } else if typ.is_block_device() { + let device = metadata.dev(); + writer.push_block_device(device as u32, rel, header)?; + } else if typ.is_char_device() { + let device = metadata.dev(); + writer.push_char_device(device as u32, rel, header)?; + } else if typ.is_fifo() { + writer.push_fifo(rel, header)?; + } else if typ.is_socket() { + writer.push_socket(rel, header)?; + } else { + return Err(anyhow!("invalid file type")); + } + } + + progress.phase = OciProgressPhase::Packing; + progress.value = 1; + progress_context.update(progress); + + let squash_file_path = file + .to_str() + .ok_or_else(|| anyhow!("failed to convert squashfs string"))?; + + let file = File::create(file)?; + let mut bufwrite = BufWriter::new(file); + trace!("squash generate: {}", squash_file_path); + writer.write(&mut bufwrite)?; + Ok(()) + } +} + +struct ConsumingFileReader { + path: PathBuf, + file: Option, +} + +impl ConsumingFileReader { + fn new(path: &Path) -> ConsumingFileReader { + ConsumingFileReader { + path: path.to_path_buf(), + file: None, + } + } +} + +impl Read for ConsumingFileReader { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + if self.file.is_none() { + self.file = Some(File::open(&self.path)?); + } + let Some(ref mut file) = self.file else { + return Err(std::io::Error::new( + ErrorKind::NotFound, + "file was not opened", + )); + }; + file.read(buf) + } +} + +impl Drop for ConsumingFileReader { + fn drop(&mut self) { + let file = self.file.take(); + drop(file); + if let Err(error) = std::fs::remove_file(&self.path) { + warn!("failed to delete consuming file {:?}: {}", self.path, error); + } + } +} + +pub struct OciPackerMkSquashfs {} + +impl OciPackerBackend for OciPackerMkSquashfs { + fn pack( + &self, + progress: &mut OciProgress, + progress_context: &OciProgressContext, + directory: &Path, + file: &Path, + ) -> Result<()> { + progress.phase = OciProgressPhase::Packing; + progress.total = 1; + progress.value = 0; + progress_context.update(progress); + let mut child = Command::new("mksquashfs") + .arg(directory) + .arg(file) + .arg("-comp") + .arg("gzip") + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .stdout(Stdio::null()) + .spawn()?; + let status = child.wait()?; + if !status.success() { + Err(anyhow!( + "mksquashfs failed with exit code: {}", + status.code().unwrap() + )) + } else { + progress.phase = OciProgressPhase::Packing; + progress.total = 1; + progress.value = 1; + progress_context.update(progress); + Ok(()) + } + } +} + +pub struct OciPackerMkfsErofs {} + +impl OciPackerBackend for OciPackerMkfsErofs { + fn pack( + &self, + progress: &mut OciProgress, + progress_context: &OciProgressContext, + directory: &Path, + file: &Path, + ) -> Result<()> { + progress.phase = OciProgressPhase::Packing; + progress.total = 1; + progress.value = 0; + progress_context.update(progress); + let mut child = Command::new("mkfs.erofs") + .arg("-L") + .arg("root") + .arg(file) + .arg(directory) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .stdout(Stdio::null()) + .spawn()?; + let status = child.wait()?; + if !status.success() { + Err(anyhow!( + "mkfs.erofs failed with exit code: {}", + status.code().unwrap() + )) + } else { + progress.phase = OciProgressPhase::Packing; + progress.total = 1; + progress.value = 1; + progress_context.update(progress); + Ok(()) + } + } +} diff --git a/crates/oci/src/progress.rs b/crates/oci/src/progress.rs index ef5a554..905a12f 100644 --- a/crates/oci/src/progress.rs +++ b/crates/oci/src/progress.rs @@ -1,12 +1,11 @@ -use std::collections::BTreeMap; - +use indexmap::IndexMap; use tokio::sync::broadcast::Sender; #[derive(Clone, Debug)] pub struct OciProgress { pub id: String, pub phase: OciProgressPhase, - pub layers: BTreeMap, + pub layers: IndexMap, pub value: u64, pub total: u64, } diff --git a/crates/runtime/src/launch.rs b/crates/runtime/src/launch.rs index 131931d..5c7cd12 100644 --- a/crates/runtime/src/launch.rs +++ b/crates/runtime/src/launch.rs @@ -9,6 +9,7 @@ use ipnetwork::{IpNetwork, Ipv4Network}; use krata::launchcfg::{ LaunchInfo, LaunchNetwork, LaunchNetworkIpv4, LaunchNetworkIpv6, LaunchNetworkResolver, }; +use krataoci::packer::OciPackerFormat; use krataoci::progress::OciProgressContext; use tokio::sync::Semaphore; use uuid::Uuid; @@ -19,7 +20,7 @@ use crate::cfgblk::ConfigBlock; use crate::RuntimeContext; use krataoci::{ cache::ImageCache, - compiler::{ImageCompiler, ImageInfo}, + compiler::{ImageInfo, OciImageCompiler}, name::ImageName, }; @@ -58,6 +59,7 @@ impl GuestLauncher { request.image, &context.image_cache, &context.oci_progress_context, + OciPackerFormat::Squashfs, ) .await?; @@ -257,10 +259,11 @@ impl GuestLauncher { image: &str, image_cache: &ImageCache, progress: &OciProgressContext, + format: OciPackerFormat, ) -> Result { let image = ImageName::parse(image)?; - let compiler = ImageCompiler::new(image_cache, None, progress.clone())?; - compiler.compile(id, &image).await + let compiler = OciImageCompiler::new(image_cache, None, progress.clone())?; + compiler.compile(id, &image, format).await } async fn allocate_ipv4(&self, context: &RuntimeContext) -> Result { diff --git a/hack/dist/apk.sh b/hack/dist/apk.sh index 3402332..ef7747d 100755 --- a/hack/dist/apk.sh +++ b/hack/dist/apk.sh @@ -19,6 +19,8 @@ fpm -s tar -t apk \ --license agpl3 \ --version "${KRATA_VERSION}" \ --architecture "${TARGET_ARCH}" \ + --depends "squashfs-tools" \ + --depends "erofs-utils" \ --description "Krata Hypervisor" \ --url "https://krata.dev" \ --maintainer "Edera Team " \ diff --git a/hack/dist/deb.sh b/hack/dist/deb.sh index db2c107..fbf97b4 100755 --- a/hack/dist/deb.sh +++ b/hack/dist/deb.sh @@ -20,6 +20,8 @@ fpm -s tar -t deb \ --version "${KRATA_VERSION}" \ --architecture "${TARGET_ARCH_DEBIAN}" \ --depends "xen-system-${TARGET_ARCH_DEBIAN}" \ + --depends "squashfs-tools" \ + --depends "erofs-utils" \ --description "Krata Hypervisor" \ --url "https://krata.dev" \ --maintainer "Edera Team " \