From 5cde25ccf51d7bafa67c4bab89758dbe9928492a Mon Sep 17 00:00:00 2001 From: Chris Kyrouac Date: Thu, 11 Jul 2024 13:25:51 -0400 Subject: [PATCH] deploy: Retrieve bound images when staging new image This parses any file pointed to by a symlink with a .container or .image extension found in /usr/lib/bootc/bound-images.d. An error is thrown if a systemd specifier is found in the parsed fields. It currently only supports the Image and AuthFile fields. Some known shortcomings are that each image is pulled synchronously. It does not do any cleanup during a rollback or if the switch fails after pulling an image. The install path also needs to pull bound images. Signed-off-by: Chris Kyrouac --- Cargo.lock | 72 ++++++++++++++++++ lib/Cargo.toml | 3 +- lib/src/boundimage.rs | 171 ++++++++++++++++++++++++++++++++++++++++++ lib/src/deploy.rs | 6 +- lib/src/install.rs | 16 ++++ lib/src/lib.rs | 1 + lib/src/task.rs | 4 +- 7 files changed, 268 insertions(+), 5 deletions(-) create mode 100644 lib/src/boundimage.rs 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..efb2493bb --- /dev/null +++ b/lib/src/boundimage.rs @@ -0,0 +1,171 @@ +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/bound-images.d"; + +pub(crate) struct BoundImageManager { + deployment_root: String, // full path to the deployment root + spec_dir: String, //full path to the bound image spec dir +} + +impl BoundImageManager { + pub(crate) fn new(deployment: &Deployment, sysroot: &SysrootLock) -> Result { + let deployment_root = sysroot.deployment_dirpath(&deployment); + + let bound_image_manager = BoundImageManager { + deployment_root: deployment_root.to_string(), + spec_dir: format!("/{}/{BOUND_IMAGE_DIR}", deployment_root), + }; + + Ok(bound_image_manager) + } + + pub(crate) fn run(&self) -> Result<()> { + if Path::new(&self.spec_dir).exists() { + let bound_images = self.parse_spec_dir()?; + self.pull_images(bound_images)?; + } + + Ok(()) + } + + #[context("parse bound image spec dir")] + fn parse_spec_dir(&self) -> Result> { + let entries = fs::read_dir(&self.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 {}", + &self.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!("/{}", &self.deployment_root))?; + + let root_dir = File::open(&self.deployment_root)?; + let root_fd = root_dir.as_fd(); + + let fd = rustix::fs::openat2( + root_fd, + file_path, + OFlags::empty(), + rustix::fs::Mode::empty(), + ResolveFlags::IN_ROOT, + )?; + + let mut file = { File::from(fd) }; + 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") => self.parse_image_file(file_name, &file_ini), + Some("container") => self.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(&self, file_name: &str, file_contents: &ini::Ini) -> Result { + let image = if let Some(img) = file_contents.get_from(Some("Image"), "Image") { + img + } else { + anyhow::bail!("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( + &self, + file_name: &str, + file_contents: &ini::Ini, + ) -> Result { + let image = if let Some(img) = file_contents.get_from(Some("Container"), "Image") { + img + } else { + anyhow::bail!("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(&self, 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..1a2fa9ff2 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,10 @@ pub(crate) async fn stage( opts, ) .await?; + + let bound_image_manager = crate::boundimage::BoundImageManager::new(&deployment, sysroot)?; + bound_image_manager.run()?; + 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};