From c1380b1095180fc1fcdfe14c727f4d3a0b274d5593002d2cede895fd623f4909 Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Sun, 2 Nov 2025 18:38:56 +0100 Subject: [PATCH] Add local `file://` source support for orchestrator image preparation This commit enhances image handling in the orchestrator by adding support for `file://` sources. It introduces logic to handle both local file copying and decompression options, complementing the existing `http(s)://` download functionality. --- crates/orchestrator/src/config.rs | 104 +++++++++++++++++---------- examples/orchestrator-image-map.yaml | 1 + 2 files changed, 68 insertions(+), 37 deletions(-) diff --git a/crates/orchestrator/src/config.rs b/crates/orchestrator/src/config.rs index 6449a69..3a787ff 100644 --- a/crates/orchestrator/src/config.rs +++ b/crates/orchestrator/src/config.rs @@ -100,8 +100,9 @@ fn default_example_path() -> PathBuf { PathBuf::from("examples/orchestrator-image-map.yaml") } -/// Ensure images referenced in config exist at local_path. If missing, download -/// from `source` and optionally decompress according to `decompress`. +/// Ensure images referenced in config exist at local_path. If missing, fetch +/// from `source` (supports http(s):// and file://) and optionally decompress +/// according to `decompress`. pub async fn ensure_images(cfg: &OrchestratorConfig) -> Result<()> { for (label, image) in cfg.images.iter() { if image.local_path.exists() { @@ -111,46 +112,75 @@ pub async fn ensure_images(cfg: &OrchestratorConfig) -> Result<()> { if let Some(parent) = image.local_path.parent() { tokio::fs::create_dir_all(parent).await.into_diagnostic()?; } - // Download to temporary file - let tmp_path = image.local_path.with_extension("part"); - tracing::info!(label = %label, url = %image.source, local = ?image.local_path, "downloading base image"); - let resp = reqwest::get(&image.source).await.into_diagnostic()?; - let status = resp.status(); - if !status.is_success() { - miette::bail!( - "failed to download {url}: {status}", - url = image.source, - status = status - ); - } - let bytes = resp.bytes().await.into_diagnostic()?; - tokio::fs::write(&tmp_path, &bytes) - .await - .into_diagnostic()?; - // Decompress or move into place - match image.decompress.unwrap_or(Decompress::None) { - Decompress::None => { - tokio::fs::rename(&tmp_path, &image.local_path) + let source = image.source.as_str(); + let is_file = source.starts_with("file://"); + let tmp_path = image.local_path.with_extension("part"); + + if is_file { + // Local file source: copy or decompress from local path + let src_path = PathBuf::from(&source[7..]); // naive parse; paths should be absolute + tracing::info!(label = %label, src = ?src_path, local = ?image.local_path, "preparing base image from file:// source"); + + match image.decompress.unwrap_or(Decompress::None) { + Decompress::None => { + // Copy to temporary then atomically move into place + tokio::fs::copy(&src_path, &tmp_path).await.into_diagnostic()?; + tokio::fs::rename(&tmp_path, &image.local_path).await.into_diagnostic()?; + } + Decompress::Zstd => { + let src = src_path.clone(); + let tmp_out = tmp_path.clone(); + task::spawn_blocking(move || -> miette::Result<()> { + let infile = fs::File::open(&src).into_diagnostic()?; + let mut decoder = zstd::stream::read::Decoder::new(infile).into_diagnostic()?; + let mut outfile = fs::File::create(&tmp_out).into_diagnostic()?; + std::io::copy(&mut decoder, &mut outfile).into_diagnostic()?; + Ok(()) + }) .await - .into_diagnostic()?; + .into_diagnostic()??; + tokio::fs::rename(&tmp_path, &image.local_path).await.into_diagnostic()?; + } } - Decompress::Zstd => { - let src = tmp_path.clone(); - let dst = image.local_path.clone(); - task::spawn_blocking(move || -> miette::Result<()> { - let infile = fs::File::open(&src).into_diagnostic()?; - let mut decoder = zstd::stream::read::Decoder::new(infile).into_diagnostic()?; - let mut outfile = fs::File::create(&dst).into_diagnostic()?; - std::io::copy(&mut decoder, &mut outfile).into_diagnostic()?; - // remove compressed temp - std::fs::remove_file(&src).ok(); - Ok(()) - }) - .await - .into_diagnostic()??; + } else { + // Remote URL (HTTP/HTTPS): download to temporary file first + tracing::info!(label = %label, url = %image.source, local = ?image.local_path, "downloading base image"); + let resp = reqwest::get(&image.source).await.into_diagnostic()?; + let status = resp.status(); + if !status.is_success() { + miette::bail!( + "failed to download {url}: {status}", + url = image.source, + status = status + ); + } + let bytes = resp.bytes().await.into_diagnostic()?; + tokio::fs::write(&tmp_path, &bytes).await.into_diagnostic()?; + + // Decompress or move into place + match image.decompress.unwrap_or(Decompress::None) { + Decompress::None => { + tokio::fs::rename(&tmp_path, &image.local_path).await.into_diagnostic()?; + } + Decompress::Zstd => { + let src = tmp_path.clone(); + let dst = image.local_path.clone(); + task::spawn_blocking(move || -> miette::Result<()> { + let infile = fs::File::open(&src).into_diagnostic()?; + let mut decoder = zstd::stream::read::Decoder::new(infile).into_diagnostic()?; + let mut outfile = fs::File::create(&dst).into_diagnostic()?; + std::io::copy(&mut decoder, &mut outfile).into_diagnostic()?; + // remove compressed temp + std::fs::remove_file(&src).ok(); + Ok(()) + }) + .await + .into_diagnostic()??; + } } } + tracing::info!(label = %label, local = ?image.local_path, "image ready"); } Ok(()) diff --git a/examples/orchestrator-image-map.yaml b/examples/orchestrator-image-map.yaml index 86fde8f..5429bfb 100644 --- a/examples/orchestrator-image-map.yaml +++ b/examples/orchestrator-image-map.yaml @@ -32,6 +32,7 @@ images: openindiana-hipster: # All images are backend-agnostic and must support NoCloud. Backends are chosen by host. source: https://dlc.openindiana.org/isos/hipster/20250402/OI-hipster-cloudimage.img.zstd + #source: file:///home/toasty/ws/illumos/installer/cloudimage-ttya-openindiana-hipster.raw.zst # Local path (raw .img) target after download/decompression. Adjust per host. local_path: /var/lib/solstice/images/openindiana-hipster.img decompress: zstd # if omitted, assumed already uncompressed raw or qcow2