use super::super::super::RefUpdate;
use super::super::LocalRegistry;
use super::oci_dir::OciDirImport;
use crate::artifact::{
media_types, remote_transport::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 transport = RemoteTransport::new(self.image_name)?;
transport.auth_for(self.image_name, RegistryOperation::Pull)?;
tracing::info!("Pulling {} into the v3 Local Registry", self.image_name);
let (manifest_bytes, manifest_digest) = transport.pull_manifest_raw(
self.image_name,
&[
OCI_IMAGE_MANIFEST_MEDIA_TYPE,
"application/vnd.oci.image.index.v1+json",
],
)?;
let manifest: ImageManifest = serde_json::from_slice(&manifest_bytes)
.context("Failed to parse OCI image manifest pulled from the remote registry")?;
Self::ensure_ommx_image_manifest(&manifest)?;
let manifest_digest = Digest::from_str(&manifest_digest)
.with_context(|| format!("Invalid remote manifest digest: {manifest_digest}"))?;
let manifest_descriptor = DescriptorBuilder::default()
.media_type(MediaType::ImageManifest)
.digest(manifest_digest.clone())
.size(manifest_bytes.len() as u64)
.build()
.context("Failed to build remote manifest descriptor")?;
self.pull_descriptor_blob(&transport, manifest.config())?;
for layer in manifest.layers() {
self.pull_descriptor_blob(&transport, layer)?;
}
self.store_manifest_blob(&manifest_descriptor, &manifest_bytes, &manifest_digest)?;
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 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(())
}
fn pull_descriptor_blob(
&self,
transport: &RemoteTransport,
descriptor: &Descriptor,
) -> Result<()> {
let digest = descriptor.digest().to_string();
let bytes = transport.pull_blob_to_vec(self.image_name, &digest, descriptor.size())?;
anyhow::ensure!(
bytes.len() as u64 == descriptor.size(),
"Blob size mismatch for {digest}: descriptor={}, actual={}",
descriptor.size(),
bytes.len()
);
self.registry.store_blob(descriptor.clone(), &bytes)?;
Ok(())
}
fn store_manifest_blob(
&self,
descriptor: &Descriptor,
manifest_bytes: &[u8],
expected_digest: &Digest,
) -> Result<()> {
anyhow::ensure!(
descriptor.digest() == expected_digest,
"Manifest descriptor digest mismatch: descriptor={}, registry reported {}",
descriptor.digest(),
expected_digest
);
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(())
}
}