Skip to content

Add support for (weakly) "lifecycle bound" podman images #559

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

1 change: 1 addition & 0 deletions lib/Cargo.toml
Original file line number Diff line number Diff line change
@@ -47,6 +47,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"]
81 changes: 70 additions & 11 deletions lib/src/deploy.rs
Original file line number Diff line number Diff line change
@@ -2,7 +2,9 @@
//!
//! Create a merged filesystem tree with the image and mounted configmaps.
use std::collections::HashSet;
use std::io::{BufRead, Write};
use std::process::Command;

use anyhow::Ok;
use anyhow::{anyhow, Context, Result};
@@ -18,7 +20,9 @@ use ostree_ext::container::store::PrepareResult;
use ostree_ext::ostree;
use ostree_ext::ostree::Deployment;
use ostree_ext::sysroot::SysrootLock;
use rustix::fd::BorrowedFd;

use crate::podman;
use crate::spec::ImageReference;
use crate::spec::{BootOrder, HostSpec};
use crate::status::labels_of_config;
@@ -113,6 +117,53 @@ pub(crate) fn check_bootc_label(config: &ostree_ext::oci_spec::image::ImageConfi
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct BoundState {
pub(crate) total_images: usize,
pub(crate) bound_images: HashSet<String>,
}

impl BoundState {
pub(crate) fn is_empty(&self) -> bool {
self.bound_images.is_empty()
}

pub(crate) fn print(&self) {
if self.total_images == 0 {
println!("No podman .image definitions found");
} else {
println!("podman systemd .image entries: {}", self.total_images);
println!("Bound images: {}", self.bound_images.len());
}
}
}

pub(crate) fn query_bound_state(root: &Dir) -> Result<BoundState> {
let (total_images, bound_images) = podman::list_container_images(root)?;
tracing::debug!("images={total_images} bound={}", bound_images.len());
Ok(BoundState {
total_images,
bound_images,
})
}

/// Pre-fetch e.g. podman `.image` files which reference external images. This
/// expects that podman sees e.g. `/var` set up as the deployment root.
#[context("Fetching bound state")]
pub(crate) async fn fetch_bound_state(state: &BoundState) -> Result<()> {
for image in state.bound_images.iter() {
let mut cmd = Command::new("podman");
cmd.args(["pull", image.as_str()]);
let mut cmd = tokio::process::Command::from(cmd);
cmd.kill_on_drop(true);
let status = cmd.status().await.context("bound podman pull")?;
if !status.success() {
anyhow::bail!("Failed to pull {image}");
}
}
Ok(())
}

/// Write container fetch progress to standard output.
async fn handle_layer_progress_print(
mut layers: tokio::sync::mpsc::Receiver<ostree_container::store::ImportProgress>,
@@ -278,19 +329,20 @@ async fn deploy(
stateroot: &str,
image: &ImageState,
origin: &glib::KeyFile,
) -> Result<()> {
) -> Result<Deployment> {
let stateroot = Some(stateroot);
// Copy to move into thread
let cancellable = gio::Cancellable::NONE;
let _new_deployment = sysroot.stage_tree_with_options(
stateroot,
image.ostree_commit.as_str(),
Some(origin),
merge_deployment,
&Default::default(),
cancellable,
)?;
Ok(())
sysroot
.stage_tree_with_options(
stateroot,
image.ostree_commit.as_str(),
Some(origin),
merge_deployment,
&Default::default(),
cancellable,
)
.map_err(Into::into)
}

#[context("Generating origin")]
@@ -307,6 +359,7 @@ fn origin_from_imageref(imgref: &ImageReference) -> Result<glib::KeyFile> {

/// Stage (queue deployment of) a fetched container image.
#[context("Staging")]
#[allow(unsafe_code)]
pub(crate) async fn stage(
sysroot: &SysrootLock,
stateroot: &str,
@@ -315,14 +368,20 @@ 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,
image,
&origin,
)
.await?;
let sysroot_fd = Dir::reopen_dir(unsafe { &BorrowedFd::borrow_raw(sysroot.fd()) })?;
let deployment_dir = sysroot_fd.open_dir(sysroot.deployment_dirpath(&deployment))?;
// TODO: Make things atomic here by not completing the staging unless we can fetch
// the new images.
let bound = query_bound_state(&deployment_dir)?;
fetch_bound_state(&bound).await?;
crate::deploy::cleanup(sysroot).await?;
println!("Queued for next boot: {:#}", spec.image);
if let Some(version) = image.version.as_deref() {
51 changes: 40 additions & 11 deletions lib/src/install.rs
Original file line number Diff line number Diff line change
@@ -40,6 +40,7 @@ use serde::{Deserialize, Serialize};

use self::baseline::InstallBlockDeviceOpts;
use crate::containerenv::ContainerExecutionInfo;
use crate::deploy::query_bound_state;
use crate::mount::Filesystem;
use crate::task::Task;
use crate::utils::sigpolicy_from_opts;
@@ -530,11 +531,17 @@ pub(crate) fn print_configuration() -> Result<()> {
serde_json::to_writer(stdout, &install_config).map_err(Into::into)
}

pub(crate) struct InitializedRoot {
aleph: InstallAleph,
deployment: Dir,
var: Dir,
}

#[context("Creating ostree deployment")]
async fn initialize_ostree_root_from_self(
state: &State,
root_setup: &RootSetup,
) -> Result<InstallAleph> {
) -> Result<InitializedRoot> {
let sepolicy = state.load_policy()?;
let sepolicy = sepolicy.as_ref();

@@ -667,6 +674,10 @@ async fn initialize_ostree_root_from_self(
let root = rootfs_dir
.open_dir(path.as_str())
.context("Opening deployment dir")?;
let varpath = format!("ostree/deploy/{stateroot}/var");
let var = rootfs_dir
.open_dir(&varpath)
.with_context(|| format!("Opening {varpath}"))?;

// And do another recursive relabeling pass over the ostree-owned directories
// but avoid recursing into the deployment root (because that's a *distinct*
@@ -715,7 +726,11 @@ async fn initialize_ostree_root_from_self(
selinux: state.selinux_state.to_aleph().to_string(),
};

Ok(aleph)
Ok(InitializedRoot {
aleph,
deployment: root,
var: var,
})
}

/// Run a command in the host mount namespace
@@ -1180,15 +1195,29 @@ async fn install_to_filesystem_impl(state: &State, rootfs: &mut RootSetup) -> Re
tracing::debug!("boot uuid={boot_uuid}");

// Write the aleph data that captures the system state at the time of provisioning for aid in future debugging.
{
let aleph = initialize_ostree_root_from_self(state, rootfs).await?;
rootfs
.rootfs_fd
.atomic_replace_with(BOOTC_ALEPH_PATH, |f| {
serde_json::to_writer(f, &aleph)?;
anyhow::Ok(())
})
.context("Writing aleph version")?;
let inst = initialize_ostree_root_from_self(state, rootfs).await?;
rootfs
.rootfs_fd
.atomic_replace_with(BOOTC_ALEPH_PATH, |f| {
serde_json::to_writer(f, &inst.aleph)?;
anyhow::Ok(())
})
.context("Writing aleph version")?;

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)?;
1 change: 0 additions & 1 deletion lib/src/lib.rs
Original file line number Diff line number Diff line change
@@ -45,7 +45,6 @@ mod k8sapitypes;
mod kernel;
#[cfg(feature = "install")]
pub(crate) mod mount;
#[cfg(feature = "install")]
mod podman;
pub mod spec;

69 changes: 68 additions & 1 deletion lib/src/podman.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
use anyhow::{anyhow, Result};
use std::collections::HashSet;
use std::io::{BufReader, Read};

use anyhow::{anyhow, Context, Result};
use camino::Utf8Path;
use cap_std_ext::cap_std::fs::Dir;
use cap_std_ext::dirext::CapStdExtDirExt;
use fn_error_context::context;
use serde::Deserialize;

use crate::install::run_in_host_mountns;
@@ -7,6 +14,9 @@ use crate::task::Task;
/// Where we look inside our container to find our own image
/// for use with `bootc install`.
pub(crate) const CONTAINER_STORAGE: &str = "/var/lib/containers";
/// Currently a magic comment which instructs bootc it should pull these
/// images.
pub(crate) const BOOTC_BOUND_FLAG: &str = "# bootc: bound";

#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
@@ -27,3 +37,60 @@ pub(crate) fn imageid_to_digest(imgid: &str) -> Result<String> {
.ok_or_else(|| anyhow!("No images returned for inspect"))?;
Ok(i.digest)
}

/// List all container `.image` files described in the target root
#[context("Listing .image files")]
pub(crate) fn list_container_images(root: &Dir) -> Result<(usize, HashSet<String>)> {
const ETC_ROOT: &str = "etc/containers/systemd";
const USR_ROOT: &str = "usr/share/containers/systemd";

let mut found_image_files = 0;
let mut r = HashSet::new();
for d in [ETC_ROOT, USR_ROOT] {
let imagedir = if let Some(d) = root.open_dir_optional(d)? {
d
} else {
tracing::debug!("No {d} found");
continue;
};
for entry in imagedir.entries()? {
let entry = entry?;
if !entry.file_type()?.is_file() {
continue;
}
let name = entry.file_name();
let name = if let Some(n) = name.to_str() {
n
} else {
anyhow::bail!("Invalid non-UTF8 filename: {name:?} in {d}");
};
if !matches!(Utf8Path::new(name).extension(), Some("image")) {
continue;
}
found_image_files += 1;
let mut buf = String::new();
entry.open().map(BufReader::new)?.read_to_string(&mut buf)?;
let mut is_bound = false;
for line in buf.lines() {
if line.starts_with(BOOTC_BOUND_FLAG) {
is_bound = true;
break;
}
}
if !is_bound {
tracing::trace!("{name}: Did not find {BOOTC_BOUND_FLAG}");
continue;
}
let config = ini::Ini::load_from_str(&buf).with_context(|| format!("{name}:"))?;
let image = if let Some(img) = config.get_from(Some("Container"), "Image") {
img
} else {
tracing::debug!("{name}: Missing Container/Image key");
continue;
};
tracing::trace!("{name}: Bound {image}");
r.insert(image.to_string());
}
}
Ok((found_image_files, r))
}