xbp-oci 10.46.0

OCI registry primitives and traits for XBP deploy.
Documentation
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,
}

/// Thin wrapper around `oci-client`.
#[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)?;
        // oci-client 0.17: push_manifest(image, manifest) — auth via client config/session.
        // Some versions still take RegistryAuth on push_manifest_list only.
        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)
    }
}