Skip to content

Commit

Permalink
deploy: Retrieve bound images when staging new image
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
ckyrouac committed Jul 11, 2024
1 parent 98b2903 commit 5cde25c
Show file tree
Hide file tree
Showing 7 changed files with 268 additions and 5 deletions.
72 changes: 72 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand All @@ -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"]
Expand Down
171 changes: 171 additions & 0 deletions lib/src/boundimage.rs
Original file line number Diff line number Diff line change
@@ -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<BoundImageManager> {
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<Vec<BoundImage>> {
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<BoundImage> {
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<BoundImage> {
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<BoundImage>) -> 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<String>,
}

impl BoundImage {
fn new(image: String, auth_file: Option<String>) -> Result<BoundImage> {
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(())
}
6 changes: 5 additions & 1 deletion lib/src/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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() {
Expand Down
16 changes: 16 additions & 0 deletions lib/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down
1 change: 1 addition & 0 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 1 addition & 3 deletions lib/src/task.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down

0 comments on commit 5cde25c

Please sign in to comment.