use oci_client::client::ClientConfig;
use oci_client::manifest::OciManifest;
use oci_client::secrets::RegistryAuth;
use oci_client::{Client, Reference};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::auth::OciAuth;
use crate::error::{OciError, Result};
use crate::r#ref::OciRef;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OciInspectResult {
pub registry: String,
pub repository: String,
pub tag: Option<String>,
pub digest: Option<String>,
pub resolved_digest: Option<String>,
pub manifest_media_type: Option<String>,
pub config_digest: Option<String>,
pub layer_count: Option<usize>,
pub size: Option<u64>,
pub annotations: Option<Value>,
pub auth_source: String,
}
#[derive(Clone)]
pub struct OciClient {
auth: OciAuth,
client: Client,
}
impl OciClient {
pub fn new(auth: OciAuth) -> Self {
Self {
auth,
client: Client::new(ClientConfig::default()),
}
}
pub fn auth(&self) -> &OciAuth {
&self.auth
}
fn registry_auth(&self) -> RegistryAuth {
if self.auth.anonymous || self.auth.token.is_none() {
return RegistryAuth::Anonymous;
}
let username = self
.auth
.username
.clone()
.unwrap_or_else(|| "x-access-token".into());
let password = self.auth.token.clone().unwrap_or_default();
RegistryAuth::Basic(username, password)
}
fn to_reference(image: &OciRef) -> Result<Reference> {
image
.reference()
.parse::<Reference>()
.map_err(|e| OciError::InvalidRef(format!("{}: {e}", image.reference())))
}
pub async fn pull_manifest(&self, image: &OciRef) -> Result<(OciManifest, String)> {
let reference = Self::to_reference(image)?;
let auth = self.registry_auth();
self.client
.pull_manifest(&reference, &auth)
.await
.map_err(|e| OciError::from_registry_message(e.to_string()))
}
pub async fn inspect(&self, image: &OciRef) -> Result<OciInspectResult> {
let (manifest, digest) = self.pull_manifest(image).await?;
let resolved_digest = if digest.is_empty() {
image.digest.clone()
} else {
Some(digest)
};
let (manifest_media_type, config_digest, layer_count, size, annotations) = match &manifest {
OciManifest::Image(img) => {
let layer_count = Some(img.layers.len());
let size = Some(
img.layers
.iter()
.map(|l| l.size as u64)
.fold(0u64, |acc, s| acc.saturating_add(s)),
);
(
img.media_type.clone(),
Some(img.config.digest.clone()),
layer_count,
size,
img.annotations
.as_ref()
.and_then(|a| serde_json::to_value(a).ok()),
)
}
OciManifest::ImageIndex(idx) => (
idx.media_type.clone(),
None,
Some(idx.manifests.len()),
None,
idx.annotations
.as_ref()
.and_then(|a| serde_json::to_value(a).ok()),
),
};
Ok(OciInspectResult {
registry: image.registry.clone(),
repository: image.repository.clone(),
tag: image.tag.clone(),
digest: image.digest.clone(),
resolved_digest,
manifest_media_type,
config_digest,
layer_count,
size,
annotations,
auth_source: self.auth.source.clone(),
})
}
pub async fn resolve_digest(&self, image: &OciRef) -> Result<String> {
self.inspect(image)
.await?
.resolved_digest
.ok_or_else(|| OciError::Other(format!("could not resolve digest for {image}")))
}
pub async fn exists(&self, image: &OciRef) -> Result<bool> {
match self.resolve_digest(image).await {
Ok(_) => Ok(true),
Err(OciError::NotFound(_)) => Ok(false),
Err(e) => Err(e),
}
}
pub async fn list_tags(&self, image: &OciRef, limit: Option<usize>) -> Result<Vec<String>> {
let repo_ref = image.repository_path();
let reference = repo_ref
.parse::<Reference>()
.map_err(|e| OciError::InvalidRef(format!("{repo_ref}: {e}")))?;
let auth = self.registry_auth();
let response = self
.client
.list_tags(&reference, &auth, None, None)
.await
.map_err(|e| OciError::from_registry_message(e.to_string()))?;
let mut tags = response.tags;
if let Some(limit) = limit {
tags.truncate(limit);
}
Ok(tags)
}
pub async fn push_manifest(&self, target: &OciRef, manifest: &OciManifest) -> Result<String> {
let reference = Self::to_reference(target)?;
match manifest {
OciManifest::Image(img) => {
let wrapped = OciManifest::Image(img.clone());
self.client
.push_manifest(&reference, &wrapped)
.await
.map_err(|e| OciError::from_registry_message(e.to_string()))
}
OciManifest::ImageIndex(idx) => {
let auth = self.registry_auth();
self.client
.push_manifest_list(&reference, &auth, idx.clone())
.await
.map_err(|e| OciError::from_registry_message(e.to_string()))
}
}
}
pub async fn promote(
&self,
source: &OciRef,
target: &OciRef,
force: bool,
) -> Result<String> {
let digest = self.resolve_digest(source).await?;
if !force {
if self.exists(target).await? {
return Err(OciError::TargetExists(target.reference()));
}
}
let source_digest_ref = source.with_digest(&digest);
let (manifest, _) = self.pull_manifest(&source_digest_ref).await?;
self.push_manifest(target, &manifest).await?;
Ok(digest)
}
}