2024-04-16 01:53:44 -07:00
|
|
|
use crate::{
|
|
|
|
progress::{OciBoundProgress, OciProgressPhase},
|
|
|
|
schema::OciSchema,
|
|
|
|
};
|
2024-04-12 11:09:26 -07:00
|
|
|
|
2024-03-25 02:37:02 +00:00
|
|
|
use super::{
|
|
|
|
name::ImageName,
|
2024-04-15 10:24:14 -07:00
|
|
|
registry::{OciPlatform, OciRegistryClient},
|
2024-03-25 02:37:02 +00:00
|
|
|
};
|
|
|
|
|
2024-03-13 14:20:22 +00:00
|
|
|
use std::{
|
2024-04-16 01:53:44 -07:00
|
|
|
fmt::Debug,
|
2024-04-16 09:29:54 -07:00
|
|
|
io::SeekFrom,
|
|
|
|
os::unix::fs::MetadataExt,
|
2024-03-13 14:20:22 +00:00
|
|
|
path::{Path, PathBuf},
|
|
|
|
pin::Pin,
|
|
|
|
};
|
2024-03-08 14:44:45 +00:00
|
|
|
|
2024-01-30 02:15:03 -08:00
|
|
|
use anyhow::{anyhow, Result};
|
2024-03-08 14:44:45 +00:00
|
|
|
use async_compression::tokio::bufread::{GzipDecoder, ZstdDecoder};
|
|
|
|
use log::debug;
|
2024-03-13 14:20:22 +00:00
|
|
|
use oci_spec::image::{
|
2024-04-16 09:29:54 -07:00
|
|
|
Descriptor, DescriptorBuilder, ImageConfiguration, ImageIndex, ImageManifest, MediaType,
|
|
|
|
ToDockerV2S2,
|
2024-03-13 14:20:22 +00:00
|
|
|
};
|
|
|
|
use serde::de::DeserializeOwned;
|
2024-03-08 14:44:45 +00:00
|
|
|
use tokio::{
|
2024-04-16 09:29:54 -07:00
|
|
|
fs::{self, File},
|
|
|
|
io::{AsyncRead, AsyncReadExt, AsyncSeekExt, BufReader, BufWriter},
|
2024-03-08 14:44:45 +00:00
|
|
|
};
|
2024-03-13 14:20:22 +00:00
|
|
|
use tokio_stream::StreamExt;
|
2024-03-08 14:44:45 +00:00
|
|
|
use tokio_tar::Archive;
|
|
|
|
|
2024-04-15 10:24:14 -07:00
|
|
|
pub struct OciImageFetcher {
|
2024-03-13 14:20:22 +00:00
|
|
|
seed: Option<PathBuf>,
|
2024-04-15 10:24:14 -07:00
|
|
|
platform: OciPlatform,
|
|
|
|
progress: OciBoundProgress,
|
2024-01-18 10:16:59 -08:00
|
|
|
}
|
|
|
|
|
2024-03-08 14:44:45 +00:00
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
|
|
pub enum OciImageLayerCompression {
|
|
|
|
None,
|
|
|
|
Gzip,
|
|
|
|
Zstd,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
pub struct OciImageLayer {
|
2024-04-16 09:29:54 -07:00
|
|
|
pub metadata: Descriptor,
|
2024-03-08 14:44:45 +00:00
|
|
|
pub path: PathBuf,
|
|
|
|
pub digest: String,
|
|
|
|
pub compression: OciImageLayerCompression,
|
|
|
|
}
|
2024-01-18 10:16:59 -08:00
|
|
|
|
2024-04-16 09:29:54 -07:00
|
|
|
#[async_trait::async_trait]
|
|
|
|
pub trait OciImageLayerReader: AsyncRead + Sync {
|
|
|
|
async fn position(&mut self) -> Result<u64>;
|
|
|
|
}
|
|
|
|
|
|
|
|
#[async_trait::async_trait]
|
|
|
|
impl OciImageLayerReader for BufReader<File> {
|
|
|
|
async fn position(&mut self) -> Result<u64> {
|
|
|
|
Ok(self.seek(SeekFrom::Current(0)).await?)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[async_trait::async_trait]
|
|
|
|
impl OciImageLayerReader for GzipDecoder<BufReader<File>> {
|
|
|
|
async fn position(&mut self) -> Result<u64> {
|
|
|
|
self.get_mut().position().await
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[async_trait::async_trait]
|
|
|
|
impl OciImageLayerReader for ZstdDecoder<BufReader<File>> {
|
|
|
|
async fn position(&mut self) -> Result<u64> {
|
|
|
|
self.get_mut().position().await
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-08 14:44:45 +00:00
|
|
|
impl OciImageLayer {
|
2024-04-16 09:29:54 -07:00
|
|
|
pub async fn decompress(&self) -> Result<Pin<Box<dyn OciImageLayerReader + Send>>> {
|
2024-03-08 14:44:45 +00:00
|
|
|
let file = File::open(&self.path).await?;
|
|
|
|
let reader = BufReader::new(file);
|
2024-04-16 09:29:54 -07:00
|
|
|
let reader: Pin<Box<dyn OciImageLayerReader + Send>> = match self.compression {
|
2024-03-08 14:44:45 +00:00
|
|
|
OciImageLayerCompression::None => Box::pin(reader),
|
|
|
|
OciImageLayerCompression::Gzip => Box::pin(GzipDecoder::new(reader)),
|
|
|
|
OciImageLayerCompression::Zstd => Box::pin(ZstdDecoder::new(reader)),
|
|
|
|
};
|
|
|
|
Ok(reader)
|
2024-01-18 10:16:59 -08:00
|
|
|
}
|
|
|
|
|
2024-04-16 09:29:54 -07:00
|
|
|
pub async fn archive(&self) -> Result<Archive<Pin<Box<dyn OciImageLayerReader + Send>>>> {
|
2024-03-08 14:44:45 +00:00
|
|
|
let decompress = self.decompress().await?;
|
|
|
|
Ok(Archive::new(decompress))
|
2024-01-18 10:16:59 -08:00
|
|
|
}
|
2024-03-08 14:44:45 +00:00
|
|
|
}
|
2024-01-18 10:16:59 -08:00
|
|
|
|
2024-03-08 14:44:45 +00:00
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
pub struct OciResolvedImage {
|
|
|
|
pub name: ImageName,
|
|
|
|
pub digest: String,
|
2024-04-16 09:29:54 -07:00
|
|
|
pub descriptor: Descriptor,
|
2024-04-16 01:53:44 -07:00
|
|
|
pub manifest: OciSchema<ImageManifest>,
|
2024-03-08 14:44:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
pub struct OciLocalImage {
|
|
|
|
pub image: OciResolvedImage,
|
2024-04-16 01:53:44 -07:00
|
|
|
pub config: OciSchema<ImageConfiguration>,
|
2024-03-08 14:44:45 +00:00
|
|
|
pub layers: Vec<OciImageLayer>,
|
|
|
|
}
|
|
|
|
|
2024-04-15 10:24:14 -07:00
|
|
|
impl OciImageFetcher {
|
2024-03-13 14:20:22 +00:00
|
|
|
pub fn new(
|
|
|
|
seed: Option<PathBuf>,
|
2024-04-15 10:24:14 -07:00
|
|
|
platform: OciPlatform,
|
|
|
|
progress: OciBoundProgress,
|
|
|
|
) -> OciImageFetcher {
|
|
|
|
OciImageFetcher {
|
2024-03-13 14:20:22 +00:00
|
|
|
seed,
|
|
|
|
platform,
|
2024-04-12 11:09:26 -07:00
|
|
|
progress,
|
2024-03-13 14:20:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-16 01:53:44 -07:00
|
|
|
async fn load_seed_json_blob<T: Clone + Debug + DeserializeOwned>(
|
2024-03-13 14:20:22 +00:00
|
|
|
&self,
|
|
|
|
descriptor: &Descriptor,
|
2024-04-16 01:53:44 -07:00
|
|
|
) -> Result<Option<OciSchema<T>>> {
|
2024-03-13 14:20:22 +00:00
|
|
|
let digest = descriptor.digest();
|
|
|
|
let Some((digest_type, digest_content)) = digest.split_once(':') else {
|
|
|
|
return Err(anyhow!("digest content was not properly formatted"));
|
|
|
|
};
|
|
|
|
let want = format!("blobs/{}/{}", digest_type, digest_content);
|
|
|
|
self.load_seed_json(&want).await
|
|
|
|
}
|
|
|
|
|
2024-04-16 01:53:44 -07:00
|
|
|
async fn load_seed_json<T: Clone + Debug + DeserializeOwned>(
|
|
|
|
&self,
|
|
|
|
want: &str,
|
|
|
|
) -> Result<Option<OciSchema<T>>> {
|
2024-03-13 14:20:22 +00:00
|
|
|
let Some(ref seed) = self.seed else {
|
|
|
|
return Ok(None);
|
|
|
|
};
|
|
|
|
|
|
|
|
let file = File::open(seed).await?;
|
|
|
|
let mut archive = Archive::new(file);
|
|
|
|
let mut entries = archive.entries()?;
|
|
|
|
while let Some(entry) = entries.next().await {
|
|
|
|
let mut entry = entry?;
|
|
|
|
let path = String::from_utf8(entry.path_bytes().to_vec())?;
|
|
|
|
if path == want {
|
2024-04-16 01:53:44 -07:00
|
|
|
let mut content = Vec::new();
|
|
|
|
entry.read_to_end(&mut content).await?;
|
|
|
|
let item = serde_json::from_slice::<T>(&content)?;
|
|
|
|
return Ok(Some(OciSchema::new(content, item)));
|
2024-03-13 14:20:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(None)
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn extract_seed_blob(&self, descriptor: &Descriptor, to: &Path) -> Result<bool> {
|
|
|
|
let Some(ref seed) = self.seed else {
|
|
|
|
return Ok(false);
|
|
|
|
};
|
|
|
|
|
|
|
|
let digest = descriptor.digest();
|
|
|
|
let Some((digest_type, digest_content)) = digest.split_once(':') else {
|
|
|
|
return Err(anyhow!("digest content was not properly formatted"));
|
|
|
|
};
|
|
|
|
let want = format!("blobs/{}/{}", digest_type, digest_content);
|
|
|
|
|
|
|
|
let seed = File::open(seed).await?;
|
|
|
|
let mut archive = Archive::new(seed);
|
|
|
|
let mut entries = archive.entries()?;
|
|
|
|
while let Some(entry) = entries.next().await {
|
|
|
|
let mut entry = entry?;
|
|
|
|
let path = String::from_utf8(entry.path_bytes().to_vec())?;
|
|
|
|
if path == want {
|
2024-03-25 09:39:06 +00:00
|
|
|
let file = File::create(to).await?;
|
|
|
|
let mut bufwrite = BufWriter::new(file);
|
|
|
|
tokio::io::copy(&mut entry, &mut bufwrite).await?;
|
2024-03-13 14:20:22 +00:00
|
|
|
return Ok(true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(false)
|
2024-02-25 05:38:23 +00:00
|
|
|
}
|
|
|
|
|
2024-03-08 14:44:45 +00:00
|
|
|
pub async fn resolve(&self, image: ImageName) -> Result<OciResolvedImage> {
|
2024-03-13 14:20:22 +00:00
|
|
|
debug!("resolve manifest image={}", image);
|
|
|
|
|
|
|
|
if let Some(index) = self.load_seed_json::<ImageIndex>("index.json").await? {
|
|
|
|
let mut found: Option<&Descriptor> = None;
|
2024-04-16 01:53:44 -07:00
|
|
|
for manifest in index.item().manifests() {
|
2024-03-13 14:20:22 +00:00
|
|
|
let Some(annotations) = manifest.annotations() else {
|
|
|
|
continue;
|
|
|
|
};
|
|
|
|
|
2024-03-13 15:13:20 +00:00
|
|
|
let mut image_name = annotations.get("io.containerd.image.name");
|
|
|
|
if image_name.is_none() {
|
|
|
|
image_name = annotations.get("org.opencontainers.image.ref.name");
|
|
|
|
}
|
|
|
|
|
|
|
|
let Some(image_name) = image_name else {
|
2024-03-13 14:20:22 +00:00
|
|
|
continue;
|
|
|
|
};
|
|
|
|
|
|
|
|
if *image_name != image.to_string() {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(platform) = manifest.platform() {
|
|
|
|
if *platform.architecture() != self.platform.arch
|
|
|
|
|| *platform.os() != self.platform.os
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
found = Some(manifest);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(found) = found {
|
|
|
|
if let Some(manifest) = self.load_seed_json_blob(found).await? {
|
|
|
|
debug!(
|
|
|
|
"found seeded manifest image={} manifest={}",
|
|
|
|
image,
|
|
|
|
found.digest()
|
|
|
|
);
|
|
|
|
return Ok(OciResolvedImage {
|
|
|
|
name: image,
|
2024-04-16 09:29:54 -07:00
|
|
|
descriptor: found.clone(),
|
2024-03-13 14:20:22 +00:00
|
|
|
digest: found.digest().clone(),
|
|
|
|
manifest,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-08 14:44:45 +00:00
|
|
|
let mut client = OciRegistryClient::new(image.registry_url()?, self.platform.clone())?;
|
2024-04-16 09:29:54 -07:00
|
|
|
let (manifest, descriptor, digest) = client
|
2024-03-08 14:44:45 +00:00
|
|
|
.get_manifest_with_digest(&image.name, &image.reference)
|
2024-02-25 05:38:23 +00:00
|
|
|
.await?;
|
2024-04-16 09:29:54 -07:00
|
|
|
let descriptor = descriptor.unwrap_or_else(|| {
|
|
|
|
DescriptorBuilder::default()
|
|
|
|
.media_type(MediaType::ImageManifest)
|
|
|
|
.size(manifest.raw().len() as i64)
|
|
|
|
.digest(digest.clone())
|
|
|
|
.build()
|
|
|
|
.unwrap()
|
|
|
|
});
|
2024-03-08 14:44:45 +00:00
|
|
|
Ok(OciResolvedImage {
|
|
|
|
name: image,
|
2024-04-16 09:29:54 -07:00
|
|
|
descriptor,
|
2024-03-08 14:44:45 +00:00
|
|
|
digest,
|
|
|
|
manifest,
|
|
|
|
})
|
2024-01-20 02:41:49 -08:00
|
|
|
}
|
|
|
|
|
2024-04-12 11:09:26 -07:00
|
|
|
pub async fn download(
|
|
|
|
&self,
|
2024-04-16 01:53:44 -07:00
|
|
|
image: &OciResolvedImage,
|
2024-04-15 10:24:14 -07:00
|
|
|
layer_dir: &Path,
|
2024-04-12 11:09:26 -07:00
|
|
|
) -> Result<OciLocalImage> {
|
2024-04-16 01:53:44 -07:00
|
|
|
let config: OciSchema<ImageConfiguration>;
|
2024-04-15 10:24:14 -07:00
|
|
|
self.progress
|
|
|
|
.update(|progress| {
|
2024-04-16 09:29:54 -07:00
|
|
|
progress.phase = OciProgressPhase::ConfigDownload;
|
2024-04-15 10:24:14 -07:00
|
|
|
})
|
|
|
|
.await;
|
2024-03-08 14:44:45 +00:00
|
|
|
let mut client = OciRegistryClient::new(image.name.registry_url()?, self.platform.clone())?;
|
2024-03-13 14:20:22 +00:00
|
|
|
if let Some(seeded) = self
|
2024-04-16 01:53:44 -07:00
|
|
|
.load_seed_json_blob::<ImageConfiguration>(image.manifest.item().config())
|
2024-03-13 14:20:22 +00:00
|
|
|
.await?
|
|
|
|
{
|
|
|
|
config = seeded;
|
|
|
|
} else {
|
|
|
|
let config_bytes = client
|
2024-04-16 01:53:44 -07:00
|
|
|
.get_blob(&image.name.name, image.manifest.item().config())
|
2024-03-13 14:20:22 +00:00
|
|
|
.await?;
|
2024-04-16 01:53:44 -07:00
|
|
|
config = OciSchema::new(
|
|
|
|
config_bytes.to_vec(),
|
|
|
|
serde_json::from_slice(&config_bytes)?,
|
|
|
|
);
|
2024-03-13 14:20:22 +00:00
|
|
|
}
|
2024-04-15 10:24:14 -07:00
|
|
|
self.progress
|
|
|
|
.update(|progress| {
|
2024-04-16 09:29:54 -07:00
|
|
|
progress.phase = OciProgressPhase::LayerDownload;
|
2024-04-15 10:24:14 -07:00
|
|
|
|
2024-04-16 01:53:44 -07:00
|
|
|
for layer in image.manifest.item().layers() {
|
2024-04-16 09:29:54 -07:00
|
|
|
progress.add_layer(layer.digest());
|
2024-04-15 10:24:14 -07:00
|
|
|
}
|
|
|
|
})
|
|
|
|
.await;
|
2024-03-08 14:44:45 +00:00
|
|
|
let mut layers = Vec::new();
|
2024-04-16 01:53:44 -07:00
|
|
|
for layer in image.manifest.item().layers() {
|
2024-04-15 10:24:14 -07:00
|
|
|
self.progress
|
|
|
|
.update(|progress| {
|
2024-04-16 09:29:54 -07:00
|
|
|
progress.downloading_layer(layer.digest(), 0, layer.size() as u64);
|
2024-04-15 10:24:14 -07:00
|
|
|
})
|
|
|
|
.await;
|
2024-04-12 11:09:26 -07:00
|
|
|
layers.push(
|
2024-04-15 10:24:14 -07:00
|
|
|
self.acquire_layer(&image.name, layer, layer_dir, &mut client)
|
2024-04-12 11:09:26 -07:00
|
|
|
.await?,
|
|
|
|
);
|
2024-04-15 10:24:14 -07:00
|
|
|
self.progress
|
|
|
|
.update(|progress| {
|
2024-04-16 09:29:54 -07:00
|
|
|
progress.downloaded_layer(layer.digest(), layer.size() as u64);
|
2024-04-15 10:24:14 -07:00
|
|
|
})
|
|
|
|
.await;
|
2024-01-18 10:16:59 -08:00
|
|
|
}
|
2024-03-08 14:44:45 +00:00
|
|
|
Ok(OciLocalImage {
|
2024-04-16 01:53:44 -07:00
|
|
|
image: image.clone(),
|
2024-03-08 14:44:45 +00:00
|
|
|
config,
|
|
|
|
layers,
|
|
|
|
})
|
2024-01-18 10:16:59 -08:00
|
|
|
}
|
|
|
|
|
2024-03-13 14:20:22 +00:00
|
|
|
async fn acquire_layer(
|
2024-03-08 14:44:45 +00:00
|
|
|
&self,
|
|
|
|
image: &ImageName,
|
|
|
|
layer: &Descriptor,
|
2024-04-15 10:24:14 -07:00
|
|
|
layer_dir: &Path,
|
2024-03-08 14:44:45 +00:00
|
|
|
client: &mut OciRegistryClient,
|
|
|
|
) -> Result<OciImageLayer> {
|
|
|
|
debug!(
|
2024-03-13 14:20:22 +00:00
|
|
|
"acquire layer digest={} size={}",
|
2024-03-08 14:44:45 +00:00
|
|
|
layer.digest(),
|
|
|
|
layer.size()
|
|
|
|
);
|
2024-04-15 10:24:14 -07:00
|
|
|
let mut layer_path = layer_dir.to_path_buf();
|
2024-03-08 14:44:45 +00:00
|
|
|
layer_path.push(format!("{}.layer", layer.digest()));
|
|
|
|
|
2024-03-13 14:20:22 +00:00
|
|
|
let seeded = self.extract_seed_blob(layer, &layer_path).await?;
|
|
|
|
if !seeded {
|
|
|
|
let file = File::create(&layer_path).await?;
|
2024-04-12 11:09:26 -07:00
|
|
|
let size = client
|
2024-04-15 10:24:14 -07:00
|
|
|
.write_blob_to_file(&image.name, layer, file, Some(self.progress.clone()))
|
2024-04-12 11:09:26 -07:00
|
|
|
.await?;
|
2024-03-08 14:44:45 +00:00
|
|
|
if layer.size() as u64 != size {
|
|
|
|
return Err(anyhow!(
|
|
|
|
"downloaded layer size differs from size in manifest",
|
|
|
|
));
|
2024-01-18 10:16:59 -08:00
|
|
|
}
|
|
|
|
}
|
2024-03-08 14:44:45 +00:00
|
|
|
|
2024-04-16 09:29:54 -07:00
|
|
|
let metadata = fs::metadata(&layer_path).await?;
|
|
|
|
|
|
|
|
if layer.size() as u64 != metadata.size() {
|
|
|
|
return Err(anyhow!("layer size differs from size in manifest",));
|
|
|
|
}
|
|
|
|
|
2024-03-08 14:44:45 +00:00
|
|
|
let mut media_type = layer.media_type().clone();
|
|
|
|
|
|
|
|
// docker layer compatibility
|
|
|
|
if media_type.to_string() == MediaType::ImageLayerGzip.to_docker_v2s2()? {
|
|
|
|
media_type = MediaType::ImageLayerGzip;
|
|
|
|
}
|
|
|
|
|
|
|
|
let compression = match media_type {
|
|
|
|
MediaType::ImageLayer => OciImageLayerCompression::None,
|
|
|
|
MediaType::ImageLayerGzip => OciImageLayerCompression::Gzip,
|
|
|
|
MediaType::ImageLayerZstd => OciImageLayerCompression::Zstd,
|
|
|
|
other => return Err(anyhow!("found layer with unknown media type: {}", other)),
|
|
|
|
};
|
|
|
|
Ok(OciImageLayer {
|
2024-04-16 09:29:54 -07:00
|
|
|
metadata: layer.clone(),
|
2024-03-08 14:44:45 +00:00
|
|
|
path: layer_path,
|
|
|
|
digest: layer.digest().clone(),
|
|
|
|
compression,
|
|
|
|
})
|
2024-01-18 10:16:59 -08:00
|
|
|
}
|
|
|
|
}
|