use super::super::super::RefUpdate;
use super::super::{async_io, LocalRegistry};
use super::oci_dir::OciDirImport;
use crate::artifact::{
manifest::blob_descriptors_by_digest,
media_types, remote_error,
remote_transport::{blob_transfer_concurrency, bounded_map, RemoteTransport},
ImageRef, OCI_IMAGE_MANIFEST_MEDIA_TYPE,
};
use anyhow::{Context, Result};
use oci_client::RegistryOperation;
use oci_spec::image::{Descriptor, DescriptorBuilder, Digest, ImageManifest, MediaType};
use std::str::FromStr;
impl LocalRegistry {
pub fn pull_image(&self, image_name: &ImageRef) -> Result<OciDirImport> {
RemotePull::new(self, image_name).run()
}
}
struct RemotePull<'reg, 'name> {
registry: &'reg LocalRegistry,
image_name: &'name ImageRef,
}
impl<'reg, 'name> RemotePull<'reg, 'name> {
fn new(registry: &'reg LocalRegistry, image_name: &'name ImageRef) -> Self {
Self {
registry,
image_name,
}
}
fn run(&self) -> Result<OciDirImport> {
if let Some(cached) = self.cached_ref()? {
return Ok(cached);
}
let transfer_concurrency = blob_transfer_concurrency()?;
let transport = self.remote_manifest_result(RemoteTransport::new(self.image_name))?;
self.remote_manifest_result(transport.auth_for(self.image_name, RegistryOperation::Pull))?;
tracing::info!("Pulling {} into the v3 Local Registry", self.image_name);
let (manifest_bytes, manifest_digest) =
self.remote_manifest_result(transport.pull_manifest_raw(
self.image_name,
&[
OCI_IMAGE_MANIFEST_MEDIA_TYPE,
"application/vnd.oci.image.index.v1+json",
],
))?;
let manifest: ImageManifest = self.invalid_remote_result(
serde_json::from_slice(&manifest_bytes)
.context("Failed to parse OCI image manifest pulled from the remote registry"),
)?;
self.invalid_remote_result(Self::ensure_ommx_image_manifest(&manifest))?;
let manifest_digest = self.invalid_remote_result(
Digest::from_str(&manifest_digest)
.with_context(|| format!("Invalid remote manifest digest: {manifest_digest}")),
)?;
let manifest_descriptor = self.invalid_remote_result(
DescriptorBuilder::default()
.media_type(MediaType::ImageManifest)
.digest(manifest_digest.clone())
.size(manifest_bytes.len() as u64)
.build()
.context("Failed to build remote manifest descriptor"),
)?;
let descriptors = std::iter::once(manifest.config())
.chain(manifest.layers())
.cloned()
.collect::<Vec<_>>();
let unique_descriptors =
self.invalid_remote_result(blob_descriptors_by_digest(&descriptors))?;
let deduplicated = 1 + manifest.layers().len() - unique_descriptors.len();
let transferred = unique_descriptors.len();
let transport_ref = &transport;
transport.block_on(async {
bounded_map(
unique_descriptors.into_values(),
transfer_concurrency,
|descriptor| async move {
self.pull_descriptor_blob_async(transport_ref, descriptor)
.await
},
)
.await
})?;
tracing::info!(
transferred,
deduplicated,
concurrency = transfer_concurrency.get(),
"Completed remote blob downloads"
);
self.store_manifest_blob(&manifest_descriptor, &manifest_bytes)?;
let experiment_record = self
.registry
.experiment_manifest_record(self.image_name, &manifest_digest)?;
let ref_update = if let Some(record) = experiment_record.as_ref() {
self.registry.index.publish_experiment_ref(
self.image_name,
&manifest_descriptor,
record,
)?
} else {
let artifact_record = self.registry.artifact_manifest_record(&manifest_digest)?;
self.registry.index.publish_artifact_ref(
self.image_name,
&manifest_descriptor,
&artifact_record,
)?
};
self.reject_conflicting_ref(&ref_update)?;
Ok(OciDirImport {
manifest_digest,
image_name: self.image_name.clone(),
ref_update,
})
}
fn remote_manifest_result<T>(&self, result: Result<T>) -> Result<T> {
result.map_err(|source| {
crate::error!(remote_error::classify_manifest(self.image_name, source))
})
}
fn remote_blob_result<T>(&self, result: Result<T>) -> Result<T> {
result.map_err(|source| crate::error!(remote_error::classify_blob(self.image_name, source)))
}
fn invalid_remote_result<T>(&self, result: Result<T>) -> Result<T> {
result.map_err(|source| {
crate::error!(remote_error::invalid_artifact(self.image_name, source))
})
}
fn cached_ref(&self) -> Result<Option<OciDirImport>> {
let Some(manifest_digest) = self.registry.index.resolve_image_name(self.image_name)? else {
return Ok(None);
};
if self.cached_manifest_closure_is_present(&manifest_digest)? {
return Ok(Some(OciDirImport {
manifest_digest,
image_name: self.image_name.clone(),
ref_update: RefUpdate::Unchanged,
}));
}
tracing::warn!(
"SQLite ref resolves {} → {manifest_digest}, but the manifest closure \
is incomplete in the registry; falling through to a fresh remote pull \
to repopulate the registry",
self.image_name,
);
Ok(None)
}
fn cached_manifest_closure_is_present(&self, manifest_digest: &Digest) -> Result<bool> {
if !self.registry.contains_blob(manifest_digest)? {
return Ok(false);
}
let manifest_bytes = self.registry.read_blob(manifest_digest)?;
let manifest: ImageManifest = serde_json::from_slice(&manifest_bytes)
.with_context(|| format!("Failed to parse cached manifest {manifest_digest}"))?;
Self::ensure_ommx_image_manifest(&manifest)?;
if !self.cached_descriptor_blob_is_present(manifest.config())? {
return Ok(false);
}
for layer in manifest.layers() {
if !self.cached_descriptor_blob_is_present(layer)? {
return Ok(false);
}
}
Ok(true)
}
fn cached_descriptor_blob_is_present(&self, descriptor: &Descriptor) -> Result<bool> {
if !self.registry.contains_blob(descriptor.digest())? {
return Ok(false);
}
let size = self.registry.blob_size(descriptor.digest())?;
if size != descriptor.size() {
tracing::warn!(
"Cached blob {} has size {}; expected {}",
descriptor.digest(),
size,
descriptor.size(),
);
return Ok(false);
}
Ok(true)
}
fn ensure_ommx_image_manifest(manifest: &ImageManifest) -> Result<()> {
let artifact_type = manifest
.artifact_type()
.as_ref()
.context("Remote manifest is not an OMMX artifact: artifactType is missing")?;
anyhow::ensure!(
media_types::is_ommx_artifact_type(artifact_type),
"Remote manifest is not an OMMX artifact: {artifact_type}"
);
if let Some(media_type) = manifest.media_type() {
anyhow::ensure!(
media_type == &MediaType::ImageManifest,
"Remote manifest media type must be OCI Image Manifest, got {media_type}"
);
}
Ok(())
}
async fn pull_descriptor_blob_async(
&self,
transport: &RemoteTransport,
descriptor: &Descriptor,
) -> Result<()> {
let digest = descriptor.digest().to_string();
let bytes = self.remote_blob_result(
transport
.pull_blob_to_vec_async(self.image_name, &digest, descriptor.size())
.await
.with_context(|| format!("Failed while downloading blob {digest}")),
)?;
if bytes.len() as u64 != descriptor.size() {
let source = crate::error!(
"Blob size mismatch for {digest}: descriptor={}, actual={}",
descriptor.size(),
bytes.len()
);
return Err(crate::error!(remote_error::invalid_artifact(
self.image_name,
source,
)));
}
async_io::store_descriptor_blob(self.registry, descriptor.clone(), bytes).await?;
Ok(())
}
fn store_manifest_blob(&self, descriptor: &Descriptor, manifest_bytes: &[u8]) -> Result<()> {
self.registry
.store_blob(descriptor.clone(), manifest_bytes)?;
Ok(())
}
fn reject_conflicting_ref(&self, ref_update: &RefUpdate) -> Result<()> {
if let RefUpdate::Conflicted {
existing_manifest_digest,
incoming_manifest_digest,
} = ref_update
{
anyhow::bail!(
"Local registry ref conflict for {}: existing manifest \
{existing_manifest_digest}, incoming manifest {incoming_manifest_digest}. \
The remote serves a different manifest than the one cached locally.",
self.image_name
);
}
Ok(())
}
}