Skip to content
Merged
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
116 changes: 109 additions & 7 deletions crates/kit/src/to_disk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,11 @@ impl ToDiskOpts {
}

/// Generate the complete bootc installation command arguments for SSH execution
fn generate_bootc_install_command(&self, disk_size: u64) -> Result<Vec<String>> {
fn generate_bootc_install_command(
&self,
disk_size: u64,
use_oci_layout: bool,
) -> Result<Vec<String>> {
let source_imgref = format!("containers-storage:{}", self.source_image);

// Quote each bootc argument individually to prevent shell injection
Expand Down Expand Up @@ -251,7 +255,27 @@ impl ToDiskOpts {
.map_err(|e| eyre!("Failed to quote tmpfs size: {}", e))?
.to_string();

// Create the complete script by substituting variables directly
// For composefs-backend installs, mount the OCI layout in the VM
// and pass --source-imgref so bootc reads from it directly,
// preserving the correct manifest with compressed layer digests (#307).
let (oci_setup, oci_volume, oci_source_arg) = if use_oci_layout {
(
indoc! {r#"
# Ensure OCI layout virtiofs mount is available (#307)
OCI=/run/virtiofs-mnt-ociimage
if ! mountpoint -q ${OCI} &>/dev/null; then
mkdir -p ${OCI}
mount -t virtiofs mount_ociimage ${OCI} -o ro
fi
"#}
.to_string(),
"-v /run/virtiofs-mnt-ociimage:/run/virtiofs-mnt-ociimage:ro".to_string(),
"--source-imgref oci:/run/virtiofs-mnt-ociimage:latest".to_string(),
)
} else {
(String::new(), String::new(), String::new())
};

let script = indoc! {r#"
set -euo pipefail

Expand All @@ -271,6 +295,8 @@ impl ToDiskOpts {
mount -t virtiofs mount_hoststorage ${AIS} -o ro
fi

{OCI_SETUP}

echo "Starting bootc installation..."
echo "Source image: {SOURCE_IMGREF}"
echo "Additional args: {BOOTC_ARGS}"
Expand All @@ -290,6 +316,7 @@ impl ToDiskOpts {
ERROR_LOG=$(mktemp)
podman run --rm -i ${tty} --privileged --pid=host --net=none -v /sys:/sys:ro \
-v /var/lib/containers:/var/lib/containers -v /var/tmp:/var/tmp -v /dev:/dev -v "${AIS}:${AIS}" \
{OCI_VOLUME} \
--security-opt label=type:unconfined_t \
--env=STORAGE_OPTS \
{INSTALL_LOG} \
Expand All @@ -298,6 +325,7 @@ impl ToDiskOpts {
bootc install to-disk \
--generic-image \
--skip-fetch-check \
{OCI_SOURCE_ARG} \
{BOOTC_ARGS} \
/dev/disk/by-id/virtio-output 2> "$ERROR_LOG"
BOOTC_EXIT=$?
Expand Down Expand Up @@ -329,6 +357,7 @@ EOF
# Retry bootc install with the unsigned local copy
podman run --rm -i ${tty} --privileged --pid=host --net=none -v /sys:/sys:ro \
-v /var/lib/containers:/var/lib/containers -v /var/tmp:/var/tmp -v /dev:/dev -v "${AIS}:${AIS}" \
{OCI_VOLUME} \
--security-opt label=type:unconfined_t \
--env=STORAGE_OPTS \
{INSTALL_LOG} \
Expand All @@ -337,6 +366,7 @@ EOF
bootc install to-disk \
--generic-image \
--skip-fetch-check \
{OCI_SOURCE_ARG} \
{BOOTC_ARGS} \
/dev/disk/by-id/virtio-output
elif [ $BOOTC_EXIT -ne 0 ]; then
Expand All @@ -351,6 +381,9 @@ EOF
echo "Installation completed successfully!"
"#}
.replace("{TMPFS_SIZE}", &tmpfs_size_quoted)
.replace("{OCI_SETUP}", &oci_setup)
.replace("{OCI_VOLUME}", &oci_volume)
.replace("{OCI_SOURCE_ARG}", &oci_source_arg)
.replace("{SOURCE_IMGREF}", &quoted_source_imgref)
.replace("{SOURCE_IMAGE}", &quoted_source_image)
.replace("{INSTALL_LOG}", &install_log)
Expand Down Expand Up @@ -403,6 +436,52 @@ pub enum RunOutcome {
DryRunWouldRegenerate,
}

/// Export a container image to a temporary OCI layout directory.
///
/// This preserves the original manifest with correct compressed layer digests.
/// The containers-storage `additionalimagestore` reconstructs manifests with
/// uncompressed layer digests when layers are accessed via virtiofs, producing
/// incorrect manifest digests.
///
/// Note: `podman push` to the `oci:` transport will convert Docker v2s2
/// manifests to OCI format, changing the digest. Bootc images use OCI
/// manifests natively, so this is not an issue in practice.
///
/// See <https://github.com/bootc-dev/bcvk/issues/307>
fn export_to_oci_layout(source_image: &str) -> Result<tempfile::TempDir> {
// Use /var/tmp rather than /tmp because /tmp is often tmpfs (RAM-backed)
// on Fedora/RHEL, and OCI layouts for bootc images can be several GB.
let tmpdir = tempfile::Builder::new()
.prefix("bcvk-oci-")
.tempdir_in("/var/tmp")
.context("Failed to create temp directory in /var/tmp for OCI layout")?;

let dst = format!("oci:{}:latest", tmpdir.path().display());

debug!("Exporting image to OCI layout: {} -> {}", source_image, dst);

let output = std::process::Command::new("podman")
.args(["push", source_image, &dst])
.output()
.context("Failed to run 'podman push'")?;

if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(eyre!("Failed to export image to OCI layout: {}", stderr));
}

let stdout = String::from_utf8_lossy(&output.stdout);
if !stdout.is_empty() {
debug!("podman push stdout: {}", stdout);
}
let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.is_empty() {
debug!("podman push stderr: {}", stderr);
}

Ok(tmpdir)
}

/// Execute a bootc installation using an ephemeral VM with SSH
///
/// Main entry point for the bootc installation process. See module-level documentation
Expand Down Expand Up @@ -517,9 +596,27 @@ pub fn run(mut opts: ToDiskOpts) -> Result<RunOutcome> {
}
}

// For composefs-backend installs, export the image to an OCI layout
// on the host. This preserves the correct manifest with compressed
// layer digests, working around containers-storage additionalimagestore
// reconstructing manifests with uncompressed digests via virtiofs (#307).
let use_oci_layout = opts.install.composefs_backend;
let oci_tmpdir = if use_oci_layout {
tracing::info!("Exporting image to OCI layout...");
match export_to_oci_layout(&opts.source_image) {
Ok(tmpdir) => Some(tmpdir),
Err(e) => {
let _ = std::fs::remove_file(&opts.target_disk);
return Err(e);
}
}
} else {
None
};

// Phase 3: Installation command generation
// Generate complete script including storage setup and bootc install
let bootc_install_command = opts.generate_bootc_install_command(disk_size)?;
let bootc_install_command = opts.generate_bootc_install_command(disk_size, use_oci_layout)?;

// Phase 4: Ephemeral VM configuration
let mut common_opts = opts.additional.common.clone();
Expand All @@ -533,6 +630,11 @@ pub fn run(mut opts: ToDiskOpts) -> Result<RunOutcome> {
// - Mount host storage read-only for image access
// - Attach target disk via virtio-blk
// - Disable networking (using local storage only)
let mut ro_bind_mounts = Vec::new();
if let Some(ref tmpdir) = oci_tmpdir {
ro_bind_mounts.push(format!("{}:ociimage", tmpdir.path().display()));
}

let ephemeral_opts = RunEphemeralOpts {
host_dns_servers: None,
image: opts.get_installer_image().to_string(),
Expand All @@ -548,10 +650,10 @@ pub fn run(mut opts: ToDiskOpts) -> Result<RunOutcome> {
// Basically containers-libs allocates a tempfile for a whole serialization of a layer as a tarball
// when fetching, so we need enough memory to do so.
add_swap: Some(format!("{disk_size}")),
bind_mounts: Vec::new(), // No additional bind mounts needed
ro_bind_mounts: Vec::new(), // No additional ro bind mounts needed
systemd_units_dir: None, // No custom systemd units
bind_storage_ro: true, // Mount host container storage read-only
bind_mounts: Vec::new(), // No additional bind mounts needed
ro_bind_mounts,
systemd_units_dir: None, // No custom systemd units
bind_storage_ro: true, // Mount host container storage read-only
mount_disk_files: vec![format!(
"{}:output:{}",
opts.target_disk,
Expand Down