Skip to content

Commit

Permalink
Add support for (weakly) lifecycle bound podman images
Browse files Browse the repository at this point in the history
This is a working PoC implementation of part of
#128

Demo:

```
$ cat Containerfile
FROM localhost/bootc
COPY *.image /usr/share/containers/systemd
$ cat foo.image
[Container]
Image=quay.io/centos/centos:stream9
$ podman build -t localhost/testbootc .
$ podman-bootc run localhost/testbootc
...
[root@ibm-p8-kvm-03-guest-02 ~]# podman images
REPOSITORY             TAG         IMAGE ID      CREATED       SIZE
quay.io/centos/centos  stream9     75a875ea6cd8  43 hours ago  163 MB
[root@ibm-p8-kvm-03-guest-02 ~]#
```

Signed-off-by: Colin Walters <[email protected]>
  • Loading branch information
cgwalters committed May 22, 2024
1 parent 41cca13 commit 9f77320
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 24 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.

1 change: 1 addition & 0 deletions lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
81 changes: 70 additions & 11 deletions lib/src/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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;
Expand Down Expand Up @@ -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>,
Expand Down Expand Up @@ -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")]
Expand All @@ -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,
Expand All @@ -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() {
Expand Down
51 changes: 40 additions & 11 deletions lib/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();

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

Expand Down
Loading

0 comments on commit 9f77320

Please sign in to comment.