1
0
Fork 0
mirror of https://github.com/imjasonh/krust synced 2026-07-11 07:59:54 +00:00
krust/src/registry/mod.rs
Jason Hall 9aeedb133b
feat: add configurable multi-part uploads to oci-distribution
- Added `use_chunked_uploads` field to ClientConfig (default: true)
- Modified push_blob to respect the chunked upload configuration
- Configured krust to disable chunked uploads for better registry compatibility
- Added comprehensive tests for the new configuration option

This allows callers to control whether chunked/multi-part uploads are used,
which improves compatibility with registries that may not support them well.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-08 15:01:38 -04:00

256 lines
9 KiB
Rust

use anyhow::{Context, Result};
use oci_distribution::manifest::{OciDescriptor, OciImageManifest, OciManifest};
use oci_distribution::secrets::RegistryAuth;
use oci_distribution::{Client, Reference};
use std::str::FromStr;
use tracing::{debug, info};
#[cfg(test)]
mod tests;
pub struct RegistryClient {
client: Client,
}
impl RegistryClient {
pub fn new() -> Result<Self> {
let config = oci_distribution::client::ClientConfig {
use_chunked_uploads: false, // Disable chunked uploads for better compatibility with various registries
..Default::default()
};
let client = Client::new(config);
Ok(Self { client })
}
pub async fn push_image(
&mut self,
image_ref: &str,
config_data: Vec<u8>,
layers: Vec<(Vec<u8>, String)>,
auth: &RegistryAuth,
) -> Result<(String, usize)> {
let reference: Reference = image_ref
.parse()
.context("Failed to parse image reference")?;
info!("Pushing image to {}", reference);
// Authenticate with the registry
self.client
.auth(&reference, auth, oci_distribution::RegistryOperation::Push)
.await
.context("Failed to authenticate with registry")?;
// Push config blob
let config_digest = format!("sha256:{}", sha256::digest(&config_data));
debug!("Pushing config blob: {}", config_digest);
self.client
.push_blob(&reference, &config_data, &config_digest)
.await
.context("Failed to push config blob")?;
// Push layers
let mut manifest_layers = Vec::new();
for (layer_data, media_type) in layers {
let digest = format!("sha256:{}", sha256::digest(&layer_data));
debug!("Pushing layer: {}", digest);
self.client
.push_blob(&reference, &layer_data, &digest)
.await
.context("Failed to push layer")?;
manifest_layers.push(OciDescriptor {
media_type: media_type.clone(),
digest: digest.clone(),
size: layer_data.len() as i64,
urls: None,
annotations: None,
});
}
// Create and push manifest
let image_manifest = OciImageManifest {
schema_version: 2,
media_type: Some("application/vnd.oci.image.manifest.v1+json".to_string()),
artifact_type: None,
config: OciDescriptor {
media_type: "application/vnd.oci.image.config.v1+json".to_string(),
digest: config_digest,
size: config_data.len() as i64,
urls: None,
annotations: None,
},
layers: manifest_layers,
annotations: None,
};
// Wrap the image manifest in the OciManifest enum
let manifest = OciManifest::Image(image_manifest);
debug!("Pushing manifest");
let (manifest_url, digest) = self
.client
.push_manifest_and_get_digest(&reference, &manifest)
.await
.context("Failed to push manifest")?;
info!("Successfully pushed image to {}", manifest_url);
// Build the full image reference with digest
let registry = reference.registry();
let repository = reference.repository();
let digest_ref = format!("{}/{}@{}", registry, repository, digest);
// Return both the digest ref and the actual manifest size
let manifest_size = serde_json::to_vec(&manifest)?.len();
Ok((digest_ref, manifest_size))
}
pub async fn push_manifest_list(
&mut self,
image_ref: &str,
manifest_descriptors: Vec<crate::manifest::ManifestDescriptor>,
auth: &RegistryAuth,
) -> Result<String> {
let reference = Reference::from_str(image_ref)
.context(format!("Failed to parse image reference: {}", image_ref))?;
// Authenticate with the registry
self.client
.auth(&reference, auth, oci_distribution::RegistryOperation::Push)
.await
.context("Failed to authenticate with registry")?;
// Create the image index
let index = crate::manifest::ImageIndex::new(manifest_descriptors);
// Convert to OCI index
let oci_manifests: Vec<oci_distribution::manifest::ImageIndexEntry> = index
.manifests
.iter()
.map(|m| oci_distribution::manifest::ImageIndexEntry {
media_type: m.media_type.clone(),
digest: m.digest.clone(),
size: m.size,
platform: Some(oci_distribution::manifest::Platform {
architecture: m.platform.architecture.clone(),
os: m.platform.os.clone(),
os_version: None,
os_features: None,
variant: m.platform.variant.clone(),
features: None,
}),
annotations: None,
})
.collect();
let oci_index = oci_distribution::manifest::OciImageIndex {
schema_version: 2,
media_type: Some("application/vnd.oci.image.index.v1+json".to_string()),
manifests: oci_manifests,
annotations: None,
};
// Wrap in OciManifest enum
let manifest = oci_distribution::manifest::OciManifest::ImageIndex(oci_index);
debug!(
"Pushing manifest list with {} manifests",
index.manifests.len()
);
for m in &index.manifests {
debug!(
" - Platform: {}/{}, digest: {}",
m.platform.os, m.platform.architecture, m.digest
);
}
let (manifest_url, digest) = self
.client
.push_manifest_and_get_digest(&reference, &manifest)
.await
.context("Failed to push manifest list")?;
info!("Successfully pushed manifest list to {}", manifest_url);
// Build the full image reference with digest
let registry = reference.registry();
let repository = reference.repository();
let image_ref = format!("{}/{}@{}", registry, repository, digest);
Ok(image_ref)
}
/// Fetch the manifest for an image and extract available platforms
pub async fn get_image_platforms(
&mut self,
image_ref: &str,
auth: &RegistryAuth,
) -> Result<Vec<String>> {
let reference: Reference = image_ref
.parse()
.context("Failed to parse image reference")?;
debug!("Fetching platforms for {}", reference);
// Use the new get_image_platforms method from oci-distribution
let platforms = self
.client
.get_image_platforms(&reference, auth)
.await
.context("Failed to get image platforms")?;
// Convert (os, arch) tuples to platform strings and normalize
let mut platform_strings: Vec<String> = platforms
.into_iter()
.filter_map(|(os, arch)| {
// Filter out unknown/invalid platforms
if os == "unknown" || arch == "unknown" || os.is_empty() || arch.is_empty() {
return None;
}
let platform = format!("{}/{}", os, arch);
// Normalize and filter platforms
let normalized = match platform.as_str() {
"linux/amd64" => Some("linux/amd64".to_string()),
"linux/arm64" | "linux/arm64/v8" => Some("linux/arm64".to_string()),
"linux/arm/v7" => Some("linux/arm/v7".to_string()),
"linux/arm/v6" => Some("linux/arm/v6".to_string()),
"linux/386" => Some("linux/386".to_string()),
"linux/ppc64le" => Some("linux/ppc64le".to_string()),
"linux/s390x" => Some("linux/s390x".to_string()),
"linux/riscv64" => Some("linux/riscv64".to_string()),
_ => {
debug!("Skipping unsupported platform: {}", platform);
None
}
};
normalized
})
.collect();
// Deduplicate platforms
platform_strings.sort();
platform_strings.dedup();
if platform_strings.is_empty() {
debug!("No valid platforms found, defaulting to linux/amd64");
Ok(vec!["linux/amd64".to_string()])
} else {
info!("Found platforms: {:?}", platform_strings);
Ok(platform_strings)
}
}
}
pub fn parse_image_reference(image: &str) -> Result<(String, String, String)> {
let reference: Reference = image.parse().context("Failed to parse image reference")?;
let registry = reference.registry().to_string();
let repository = reference.repository().to_string();
let tag = reference.tag().unwrap_or("latest").to_string();
Ok((registry, repository, tag))
}