#![cfg(target_os = "windows")]
use std::io;
use std::path::{Path, PathBuf};
use zlayer_agent::windows::scratch::WritableLayer;
use zlayer_agent::windows::unpacker::{self, ResolvedLayerDescriptor, UnpackedImage};
use zlayer_agent::windows::wclayer::LayerChain;
use zlayer_registry::image_config::ImageConfig;
use zlayer_registry::{ImagePuller, OciImageManifest, RegistryAuth};
#[derive(Debug)]
pub struct BaseChainArtifacts {
pub parent_chain: LayerChain,
pub layer_blobs: Vec<BaseLayerBlob>,
pub base_config: Option<ImageConfig>,
pub os_version: Option<String>,
#[allow(dead_code)]
pub unpacked_root: PathBuf,
}
#[derive(Debug, Clone)]
pub struct BaseLayerBlob {
pub media_type: String,
pub digest: String,
pub bytes: Vec<u8>,
pub urls: Vec<String>,
}
pub async fn prepare_base_chain(
puller: &ImagePuller,
image: &str,
dest_root: &Path,
) -> io::Result<BaseChainArtifacts> {
std::fs::create_dir_all(dest_root)?;
let auth = RegistryAuth::Anonymous;
let (manifest, _manifest_digest) = puller
.pull_manifest(image, &auth)
.await
.map_err(|e| io::Error::other(format!("pull manifest {image}: {e}")))?;
let (base_config, os_version) = fetch_base_config_and_version(puller, image, &auth, &manifest)
.await
.unwrap_or((None, None));
let descriptors: Vec<ResolvedLayerDescriptor> = manifest
.layers
.iter()
.map(|layer| ResolvedLayerDescriptor {
digest: layer.digest.clone(),
media_type: layer.media_type.clone(),
size: layer.size,
urls: layer.urls.clone().unwrap_or_default(),
})
.collect();
let unpacked_root = dest_root.join("unpacked");
std::fs::create_dir_all(&unpacked_root)?;
let UnpackedImage { chain, root } =
unpacker::unpack_windows_image(puller, image, &auth, &descriptors, &unpacked_root).await?;
let mut layer_blobs = Vec::with_capacity(descriptors.len());
for desc in &descriptors {
let bytes = puller
.pull_blob_with_urls(image, &desc.digest, &auth, &desc.urls, Some(desc.size))
.await
.map_err(|e| io::Error::other(format!("refetch base layer {}: {e}", desc.digest)))?;
layer_blobs.push(BaseLayerBlob {
media_type: desc.media_type.clone(),
digest: desc.digest.clone(),
bytes,
urls: desc.urls.clone(),
});
}
Ok(BaseChainArtifacts {
parent_chain: chain,
layer_blobs,
base_config,
os_version,
unpacked_root: root,
})
}
pub fn create_writable_layer(
layer_path: &Path,
parent_chain: &LayerChain,
size_gb: u64,
) -> io::Result<WritableLayer> {
if parent_chain.0.is_empty() {
return Err(io::Error::other(
"HCS scratch layer requires a non-empty parent chain; \
FROM scratch is not supported by the HCS builder",
));
}
zlayer_agent::windows::layer::enable_backup_restore_privileges()?;
zlayer_agent::windows::scratch::create(
layer_path,
parent_chain,
size_gb,
false,
)
}
async fn fetch_base_config_and_version(
puller: &ImagePuller,
image: &str,
auth: &RegistryAuth,
manifest: &OciImageManifest,
) -> io::Result<(Option<ImageConfig>, Option<String>)> {
let config_blob = puller
.pull_blob(image, &manifest.config.digest, auth)
.await
.map_err(|e| io::Error::other(format!("pull base config: {e}")))?;
let value: serde_json::Value = serde_json::from_slice(&config_blob)?;
let os_version = value
.get("os.version")
.and_then(serde_json::Value::as_str)
.map(ToString::to_string);
let image_config = value
.get("config")
.cloned()
.and_then(|v| serde_json::from_value::<ImageConfig>(v).ok());
Ok((image_config, os_version))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_parent_chain_rejected_before_hcs_call() {
let tmp = tempfile::tempdir().expect("tmpdir");
let chain = LayerChain::default();
let err = create_writable_layer(tmp.path(), &chain, 20)
.expect_err("empty chain must fail before calling HCS");
assert!(
err.to_string().contains("non-empty parent chain"),
"unexpected error: {err}"
);
}
}