Skip to content
Open
Show file tree
Hide file tree
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
34 changes: 34 additions & 0 deletions crates/kit/src/images.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,40 @@ pub fn inspect(name: &str) -> Result<ImageInspect> {
r.pop().ok_or_else(|| eyre!("No such image"))
}

pub fn get_image_digest(name: &str) -> Result<String> {
// Use podman to inspect if the image to be installed is from containers-storage
if !img_has_transport(name) || img_from_containers_storage(name) {
let name = name.strip_prefix("containers-storage:").unwrap_or(name);
let i = inspect(name)?;
return Ok(i.digest.to_string());
}

let r = Command::new("skopeo")
.args(["inspect", "--format", "{{.Digest}}", name])
.run_get_string()
.map_err(|e| eyre!("{e}"))?;

Ok(r.trim().into())
}

pub fn img_has_transport(name: &str) -> bool {
["docker://", "containers-storage:", "oci:", "dir:"]
.iter()
.any(|t| name.starts_with(t))
}

pub fn img_from_containers_storage(name: &str) -> bool {
name.starts_with("containers-storage:")
}

pub fn prepend_transport_to_img(name: &str) -> String {
if img_has_transport(name) {
return name.into();
}

return format!("containers-storage:{name}");
}

/// Get container image size in bytes for disk space planning.
pub fn get_image_size(name: &str) -> Result<u64> {
tracing::debug!("Getting size for image: {}", name);
Expand Down
10 changes: 7 additions & 3 deletions crates/kit/src/libvirt/base_disks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ use tracing::{debug, info};
/// Find or create a base disk for the given parameters
pub fn find_or_create_base_disk(
source_image: &str,
image_to_install: &str,
image_digest: &str,
install_options: &InstallOptions,
connect_uri: Option<&str>,
virtiofsd_binary: Option<&str>,
) -> Result<Utf8PathBuf> {
let metadata = DiskImageMetadata::from(install_options, image_digest, source_image);
let metadata = DiskImageMetadata::from(install_options, image_digest, image_to_install);
let cache_hash = metadata.compute_cache_hash();

// Extract short hash for filename (first 16 chars after "sha256:")
Expand All @@ -44,7 +45,7 @@ pub fn find_or_create_base_disk(
if crate::cache_metadata::check_cached_disk(
base_disk_path.as_std_path(),
image_digest,
source_image,
image_to_install,
install_options,
)?
.is_ok()
Expand All @@ -65,6 +66,7 @@ pub fn find_or_create_base_disk(
create_base_disk(
&base_disk_path,
source_image,
image_to_install,
image_digest,
install_options,
connect_uri,
Expand All @@ -78,6 +80,7 @@ pub fn find_or_create_base_disk(
fn create_base_disk(
base_disk_path: &Utf8Path,
source_image: &str,
image_to_install: &str,
image_digest: &str,
install_options: &InstallOptions,
connect_uri: Option<&str>,
Expand Down Expand Up @@ -107,6 +110,7 @@ fn create_base_disk(
// Create the disk using to_disk at temporary location
let to_disk_opts = ToDiskOpts {
source_image: source_image.to_string(),
image_to_install: Some(image_to_install.to_string()),
target_disk: temp_disk_path.clone(),
install: install_options.clone(),
additional: ToDiskAdditionalOpts {
Expand Down Expand Up @@ -135,7 +139,7 @@ fn create_base_disk(
let metadata_valid = crate::cache_metadata::check_cached_disk(
temp_disk_path.as_std_path(),
image_digest,
source_image,
image_to_install,
install_options,
)
.context("Querying cached disk")?;
Expand Down
14 changes: 11 additions & 3 deletions crates/kit/src/libvirt/base_disks_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use serde_json;

use super::base_disks::{find_or_create_base_disk, list_base_disks, prune_base_disks};
use super::OutputFormat;
use crate::images;
use crate::images::{get_image_digest, prepend_transport_to_img};
use crate::install_options::InstallOptions;

/// Options for base-disks command
Expand All @@ -24,6 +24,12 @@ pub struct LibvirtBaseDisksOpts {
#[derive(Debug, Parser)]
pub struct CreateBaseDiskOpts {
pub source_image: String,
/// The image to use for creating the base disk
/// If None, the `source_image` cli option is used for installation
///
/// Ex. docker://quay.io/fedora/fedora-bootc:44
#[clap(long)]
pub image_to_install: Option<String>,
#[clap(flatten)]
pub install_options: InstallOptions,
}
Expand Down Expand Up @@ -69,11 +75,13 @@ pub fn run_create(
opts: CreateBaseDiskOpts,
) -> Result<()> {
let connect_uri = global_opts.connect.as_deref();
let inspect = images::inspect(&opts.source_image)?;
let image_digest = inspect.digest.to_string();
let image_to_install = opts.image_to_install.unwrap_or(opts.source_image.clone());
let image_to_install = prepend_transport_to_img(&image_to_install);
let image_digest = get_image_digest(&image_to_install)?;

let path = find_or_create_base_disk(
&opts.source_image,
&image_to_install,
&image_digest,
&opts.install_options,
connect_uri,
Expand Down
40 changes: 31 additions & 9 deletions crates/kit/src/libvirt/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ use tracing::{debug, info};

use crate::common_opts::MemoryOpts;
use crate::domain_list::DomainLister;
use crate::images::{
get_image_digest, img_from_containers_storage, img_has_transport, prepend_transport_to_img,
};
use crate::install_options::InstallOptions;
use crate::libvirt::domain::VirtiofsFilesystem;
use crate::utils::parse_memory_to_mb;
Expand Down Expand Up @@ -340,6 +343,13 @@ pub struct LibvirtRunOpts {
/// `--log-dir=journal=/tmp/logs/`
#[clap(long, value_name = "STREAMS=DIR")]
pub log_dir: Option<crate::run_ephemeral::LogDir>,

/// The image to use for creating the base disk
/// If None, the `image` cli option is used for installation
///
/// Ex. docker://quay.io/fedora/fedora-bootc:44
#[clap(long)]
pub image_to_install: Option<String>,
}

impl LibvirtRunOpts {
Expand Down Expand Up @@ -429,8 +439,6 @@ fn wait_for_ssh_ready(

/// Execute the libvirt run command
pub fn run(global_opts: &crate::libvirt::LibvirtOptions, mut opts: LibvirtRunOpts) -> Result<()> {
use crate::images;

// Validate labels don't contain commas
opts.validate_labels()?;

Expand Down Expand Up @@ -478,25 +486,27 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, mut opts: LibvirtRunOpt
None => generate_unique_vm_name(&opts.image, &existing_domains),
};

let image_to_install = opts.image_to_install.as_ref().unwrap_or(&opts.image);
let image_to_install = prepend_transport_to_img(&image_to_install);

println!(
"Creating libvirt domain '{}' (install source container image: {})",
vm_name, opts.image
vm_name, image_to_install,
);

// Get the image digest for caching
let inspect = images::inspect(&opts.image)?;
let image_digest = inspect.digest.to_string();
let image_digest = get_image_digest(&image_to_install)?;
debug!("Image digest: {}", image_digest);

// Check Ignition support and validate config file path early
if let Some(ref ignition_path) = opts.ignition_config {
let has_ignition = check_ignition_support(&opts.image)?;
let has_ignition = check_ignition_support(&image_to_install)?;
if !has_ignition {
return Err(eyre!(
"Image does not support Ignition. See man bcvk-libvirt-run for details."
));
}
debug!("Image {} supports Ignition", opts.image);
debug!("Image {} supports Ignition", image_to_install);

// Validate that the Ignition config file exists before proceeding
if !ignition_path.try_exists()? {
Expand All @@ -521,6 +531,7 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, mut opts: LibvirtRunOpt
// Phase 1: Find or create a base disk image
let base_disk_path = crate::libvirt::base_disks::find_or_create_base_disk(
&opts.image,
&image_to_install,
&image_digest,
&opts.install,
connect_uri,
Expand Down Expand Up @@ -1107,9 +1118,20 @@ fn check_ignition_support(image: &str) -> Result<bool> {
use std::collections::HashMap;
use std::process::Stdio;

let mut args = vec!["inspect", "--format", "{{json .Labels}}"];

let cmd = if img_from_containers_storage(image) || !img_has_transport(image) {
args.insert(0, "image");
args.push(image.strip_prefix("containers-storage:").unwrap_or(image));
"podman"
} else {
args.push(image);
"skopeo"
};

// Fetch all labels with a single podman inspect call
let output = std::process::Command::new("podman")
.args(["image", "inspect", "--format", "{{json .Labels}}", image])
let output = std::process::Command::new(cmd)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
Expand Down
17 changes: 15 additions & 2 deletions crates/kit/src/libvirt/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! to libvirt storage pools, maintaining container image metadata as libvirt annotations.

use crate::common_opts::MemoryOpts;
use crate::images::get_image_digest;
use crate::install_options::InstallOptions;
use crate::to_disk::{run as to_disk, ToDiskAdditionalOpts, ToDiskOpts};
use crate::{images, utils};
Expand All @@ -20,6 +21,13 @@ pub struct LibvirtUploadOpts {
/// Container image to install and upload
pub source_image: String,

/// The image to use for creating the base disk
/// If None, the `image` cli option is used for installation
///
/// Ex. docker://quay.io/fedora/fedora-bootc:44
#[clap(long)]
pub image_to_install: Option<String>,

/// Name for the libvirt volume (defaults to sanitized image name)
#[clap(long)]
pub volume_name: Option<String>,
Expand Down Expand Up @@ -183,9 +191,10 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtUploadOpts
opts.source_image
);

let image_to_install = opts.image_to_install.as_ref().unwrap_or(&opts.source_image);

// Phase 1: Extract image digest for caching
let inspect = images::inspect(&opts.source_image)?;
let image_digest = &inspect.digest.to_string();
let image_digest = get_image_digest(image_to_install)?;
debug!("Container image digest: {}", image_digest);

// Phase 2: Calculate disk size to use
Expand All @@ -194,6 +203,9 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtUploadOpts
utils::parse_size(size_str)?
} else {
// Use same logic as to_disk: 2x source image size with 4GB minimum
// NOTE: Using opts.source_image to estimate the required size here
// as getting the size from registry images might not be always accurate
// as we will get the compressed size and not the final on-disk size
let image_size = images::get_image_size(&opts.source_image)?;

std::cmp::max(image_size * 2, 4u64 * 1024 * 1024 * 1024)
Expand All @@ -208,6 +220,7 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtUploadOpts

let install_opts = ToDiskOpts {
source_image: opts.source_image.clone(),
image_to_install: opts.image_to_install.clone(),
target_disk: temp_disk_path.clone(),
install: opts.install.clone(),
additional: ToDiskAdditionalOpts {
Expand Down
1 change: 1 addition & 0 deletions crates/kit/src/libvirt_upload_disk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ pub fn run(opts: LibvirtUploadDiskOpts) -> Result<()> {

let install_opts = ToDiskOpts {
source_image: opts.source_image.clone(),
image_to_install: None,
target_disk: temp_disk.clone(),
install: opts.install.clone(),
additional: ToDiskAdditionalOpts {
Expand Down
22 changes: 18 additions & 4 deletions crates/kit/src/run_ephemeral.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ pub fn default_vcpus() -> u32 {
.unwrap_or(2)
}

use crate::images::{img_from_containers_storage, img_has_transport};
use crate::qemu::{self, QemuConfigExt};
use crate::{
boot_progress,
Expand Down Expand Up @@ -195,7 +196,7 @@ impl std::str::FromStr for LogDir {
other => {
return Err(color_eyre::eyre::eyre!(
"--log-dir unknown stream name {other:?}; expected `journal` or `console`"
))
));
}
}
}
Expand Down Expand Up @@ -1239,9 +1240,20 @@ fn check_ignition_support(image: &str) -> Result<bool> {
use std::collections::HashMap;
use std::process::Stdio;

let mut args = vec!["inspect", "--format", "{{json .Labels}}"];

let cmd = if img_from_containers_storage(image) || !img_has_transport(image) {
args.insert(0, "image");
args.push(image.strip_prefix("containers-storage:").unwrap_or(image));
"podman"
} else {
args.push(image);
"skopeo"
};

// Fetch all labels with a single podman inspect call
let output = Command::new("podman")
.args(["image", "inspect", "--format", "{{json .Labels}}", image])
let output = Command::new(cmd)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
Expand Down Expand Up @@ -1994,7 +2006,9 @@ Options=
if let Some(ref dns_servers) = opts.host_dns_servers {
debug!("DNS servers configured for QEMU slirp: {:?}", dns_servers);
} else {
warn!("No host DNS servers available, QEMU slirp will use container's resolv.conf which may not work");
warn!(
"No host DNS servers available, QEMU slirp will use container's resolv.conf which may not work"
);
}

if opts.common.ssh_keygen {
Expand Down
Loading
Loading