diff --git a/Cargo.lock b/Cargo.lock index e3f395a41..655405cb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,6 +187,7 @@ dependencies = [ "openssl", "ostree-ext", "regex", + "rust-ini", "rustix", "schemars", "serde", @@ -391,6 +392,26 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + [[package]] name = "containers-image-proxy" version = "0.6.0" @@ -434,6 +455,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -527,6 +554,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "dyn-clone" version = "1.0.16" @@ -1328,6 +1364,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown", +] + [[package]] name = "ostree" version = "0.19.1" @@ -1627,6 +1673,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" +[[package]] +name = "rust-ini" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d625ed57d8f49af6cfa514c42e1a71fadcff60eb0b1c517ff82fe41aa025b41" +dependencies = [ + "cfg-if", + "ordered-multimap", + "trim-in-place", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1977,6 +2034,15 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -2151,6 +2217,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "trim-in-place" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" + [[package]] name = "typenum" version = "1.17.0" diff --git a/lib/Cargo.toml b/lib/Cargo.toml index d184271f4..9ace0e1fb 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -30,7 +30,7 @@ liboverdrop = "0.1.0" libsystemd = "0.7" openssl = "^0.10.64" # TODO drop this in favor of rustix -nix = { version = "0.29", features = ["ioctl", "sched"] } +nix = { version = "0.29", features = ["ioctl", "sched", "fs"] } regex = "1.10.4" rustix = { "version" = "0.38.34", features = ["thread", "fs", "system", "process"] } schemars = { version = "0.8.17", features = ["chrono"] } @@ -45,6 +45,7 @@ tempfile = "3.10.1" toml = "0.8.12" xshell = { version = "0.2.6", optional = true } uuid = { version = "1.8.0", features = ["v4"] } +rust-ini = "0.21.0" [features] default = ["install"] diff --git a/lib/src/boundimage.rs b/lib/src/boundimage.rs new file mode 100644 index 000000000..1167bbaaf --- /dev/null +++ b/lib/src/boundimage.rs @@ -0,0 +1,145 @@ +use crate::task::Task; +use anyhow::{Context, Result}; +use camino::Utf8Path; +use fn_error_context::context; +use ostree_ext::ostree::Deployment; +use ostree_ext::sysroot::SysrootLock; +use regex::Regex; +use rustix::fs::{OFlags, ResolveFlags}; +use std::fs; +use std::fs::File; +use std::io::Read; +use std::os::unix::io::AsFd; +use std::path::Path; + +const BOUND_IMAGE_DIR: &'static str = "usr/lib/bootc-experimental/bound-images.d"; + +pub(crate) fn pull_bound_images(sysroot: &SysrootLock, deployment: &Deployment) -> Result<()> { + let deployment_root = sysroot.deployment_dirpath(&deployment).to_string(); + let spec_dir = format!("/{}/{BOUND_IMAGE_DIR}", deployment_root); + + if Path::new(&spec_dir).exists() { + let bound_images = parse_spec_dir(&spec_dir, &deployment_root)?; + pull_images(bound_images)?; + } + + Ok(()) +} + +#[context("parse bound image spec dir")] +fn parse_spec_dir(spec_dir: &String, deployment_root: &String) -> Result> { + let entries = fs::read_dir(spec_dir)?; + let mut bound_images = Vec::new(); + + for entry in entries { + //validate entry is a symlink with correct extension + let entry = entry?; + let file_name = entry.file_name(); + let file_name = if let Some(n) = file_name.to_str() { + n + } else { + anyhow::bail!("Invalid non-UTF8 filename: {file_name:?} in {}", spec_dir); + }; + + if !entry.file_type()?.is_symlink() { + anyhow::bail!("Not a symlink: {file_name}"); + } + + //parse the file contents + let file_path = entry.path(); + let file_path = file_path.strip_prefix(format!("/{}", deployment_root))?; + + let root_dir = File::open(deployment_root)?; + let root_fd = root_dir.as_fd(); + + let mut file: File = rustix::fs::openat2( + root_fd, + file_path, + OFlags::empty(), + rustix::fs::Mode::empty(), + ResolveFlags::IN_ROOT, + )? + .into(); + + let mut file_contents = String::new(); + file.read_to_string(&mut file_contents)?; + + let file_ini = ini::Ini::load_from_str(&file_contents).context("Parse to ini")?; + let file_extension = Utf8Path::new(file_name).extension(); + let bound_image = match file_extension { + Some("image") => parse_image_file(file_name, &file_ini), + Some("container") => parse_container_file(file_name, &file_ini), + _ => anyhow::bail!("Invalid file extension: {file_name}"), + }?; + + bound_images.push(bound_image); + } + + Ok(bound_images) +} + +#[context("parse image file {file_name}")] +fn parse_image_file(file_name: &str, file_contents: &ini::Ini) -> Result { + let image = file_contents + .get_from(Some("Image"), "Image") + .ok_or_else(|| anyhow::anyhow!("Missing Image field in {file_name}"))?; + + let auth_file = file_contents + .get_from(Some("Image"), "AuthFile") + .map(|s| s.to_string()); + + let bound_image = BoundImage::new(image.to_string(), auth_file)?; + Ok(bound_image) +} + +#[context("parse container file {file_name}")] +fn parse_container_file(file_name: &str, file_contents: &ini::Ini) -> Result { + let image = file_contents + .get_from(Some("Container"), "Image") + .ok_or_else(|| anyhow::anyhow!("Missing Image field in {file_name}"))?; + + let bound_image = BoundImage::new(image.to_string(), None)?; + Ok(bound_image) +} + +#[context("pull bound images")] +fn pull_images(bound_images: Vec) -> Result<()> { + //TODO: do this in parallel + for bound_image in bound_images { + let mut task = Task::new("Pulling bound image", "/usr/bin/podman") + .arg("pull") + .arg(&bound_image.image); + if let Some(auth_file) = &bound_image.auth_file { + task = task.arg("--authfile").arg(auth_file); + } + task.run()?; + } + + Ok(()) +} + +struct BoundImage { + image: String, + auth_file: Option, +} + +impl BoundImage { + fn new(image: String, auth_file: Option) -> Result { + validate_spec_value(&image).context("Invalid image value")?; + + if let Some(auth_file) = &auth_file { + validate_spec_value(auth_file).context("Invalid auth_file value")?; + } + + Ok(BoundImage { image, auth_file }) + } +} + +fn validate_spec_value(value: &String) -> Result<()> { + let r = Regex::new(r"%[^%]").unwrap(); + if r.is_match(&value) { + anyhow::bail!("Systemd specifiers are not supported by bound bootc images: {value}"); + } + + Ok(()) +} diff --git a/lib/src/deploy.rs b/lib/src/deploy.rs index 1f63d33ec..1b95a596a 100644 --- a/lib/src/deploy.rs +++ b/lib/src/deploy.rs @@ -318,7 +318,7 @@ pub(crate) async fn stage( ) -> Result<()> { let merge_deployment = sysroot.merge_deployment(Some(stateroot)); let origin = origin_from_imageref(spec.image)?; - crate::deploy::deploy( + let deployment = crate::deploy::deploy( sysroot, merge_deployment.as_ref(), stateroot, @@ -327,6 +327,9 @@ pub(crate) async fn stage( opts, ) .await?; + + crate::boundimage::pull_bound_images(sysroot, &deployment)?; + crate::deploy::cleanup(sysroot).await?; println!("Queued for next boot: {:#}", spec.image); if let Some(version) = image.version.as_deref() { diff --git a/lib/src/install.rs b/lib/src/install.rs index 4369a4fa2..15bd6b1b2 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -1204,6 +1204,22 @@ async fn install_to_filesystem_impl(state: &State, rootfs: &mut RootSetup) -> Re anyhow::Ok(()) }) .context("Writing aleph version")?; + + // TODO: add code to run quadlet/systemd against the bootc-bound-image directory + // let bound = query_bound_state(&inst.deployment)?; + // bound.print(); + // if !bound.is_empty() { + // println!(); + // Task::new("Mounting deployment /var", "mount") + // .args(["--bind", ".", "/var"]) + // .cwd(&inst.var)? + // .run()?; + // // podman needs this + // Task::new("Initializing /var/tmp", "systemd-tmpfiles") + // .args(["--create", "--boot", "--prefix=/var/tmp"]) + // .verbose() + // .run()?; + // crate::deploy::fetch_bound_state(&bound).await?; } crate::bootloader::install_via_bootupd(&rootfs.device, &rootfs.rootfs, &state.config_opts)?; diff --git a/lib/src/lib.rs b/lib/src/lib.rs index f2f2c60d2..f7ae40049 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -18,6 +18,7 @@ #![allow(clippy::needless_borrows_for_generic_args)] pub mod cli; +mod boundimage; pub(crate) mod deploy; pub(crate) mod generator; pub(crate) mod journal; diff --git a/lib/src/task.rs b/lib/src/task.rs index 19ebc4474..fbd37e07f 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -1,7 +1,5 @@ use std::{ - ffi::OsStr, - io::{Seek, Write}, - process::{Command, Stdio}, + ffi::OsStr, io::{Seek, Write}, process::{Command, Stdio} }; use anyhow::{Context, Result};