From 6cd2b38a5b372ab09f2bf80151ecb7e50ee6b8c7 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 | 145 ++++++++++++++++++++++++++++++++++++++++++ lib/src/deploy.rs | 5 +- lib/src/install.rs | 16 +++++ lib/src/lib.rs | 1 + lib/src/task.rs | 4 +- 7 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 lib/src/boundimage.rs diff --git a/Cargo.lock b/Cargo.lock index e3f395a4..655405cb 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 d184271f..9ace0e1f 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 00000000..1167bbaa --- /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 1f63d33e..1b95a596 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 4369a4fa..15bd6b1b 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 f2f2c60d..f7ae4004 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 19ebc447..fbd37e07 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};