-
Notifications
You must be signed in to change notification settings - Fork 86
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 <[email protected]>
- Loading branch information
Showing
7 changed files
with
241 additions
and
5 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 = format!("/{}", 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(deployment_root, bound_images)?; | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
#[context("parse bound image spec dir")] | ||
fn parse_spec_dir(spec_dir: &String, deployment_root: &String) -> Result<Vec<BoundImage>> { | ||
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)).context("Prefix not found in file path")?; | ||
|
||
let root_dir = File::open(deployment_root).context("Unable to 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).context("Unable to read 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<BoundImage> { | ||
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<BoundImage> { | ||
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(deployment_root: String, 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(format!("/{deployment_root}/{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(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters