Skip to main content

xbp_oci/
client.rs

1use oci_client::client::ClientConfig;
2use oci_client::manifest::OciManifest;
3use oci_client::secrets::RegistryAuth;
4use oci_client::{Client, Reference};
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8use crate::auth::OciAuth;
9use crate::error::{OciError, Result};
10use crate::r#ref::OciRef;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct OciInspectResult {
14    pub registry: String,
15    pub repository: String,
16    pub tag: Option<String>,
17    pub digest: Option<String>,
18    pub resolved_digest: Option<String>,
19    pub manifest_media_type: Option<String>,
20    pub config_digest: Option<String>,
21    pub layer_count: Option<usize>,
22    pub size: Option<u64>,
23    pub annotations: Option<Value>,
24    pub auth_source: String,
25}
26
27/// Thin wrapper around `oci-client`.
28#[derive(Clone)]
29pub struct OciClient {
30    auth: OciAuth,
31    client: Client,
32}
33
34impl OciClient {
35    pub fn new(auth: OciAuth) -> Self {
36        Self {
37            auth,
38            client: Client::new(ClientConfig::default()),
39        }
40    }
41
42    pub fn auth(&self) -> &OciAuth {
43        &self.auth
44    }
45
46    fn registry_auth(&self) -> RegistryAuth {
47        if self.auth.anonymous || self.auth.token.is_none() {
48            return RegistryAuth::Anonymous;
49        }
50        let username = self
51            .auth
52            .username
53            .clone()
54            .unwrap_or_else(|| "x-access-token".into());
55        let password = self.auth.token.clone().unwrap_or_default();
56        RegistryAuth::Basic(username, password)
57    }
58
59    fn to_reference(image: &OciRef) -> Result<Reference> {
60        image
61            .reference()
62            .parse::<Reference>()
63            .map_err(|e| OciError::InvalidRef(format!("{}: {e}", image.reference())))
64    }
65
66    pub async fn pull_manifest(&self, image: &OciRef) -> Result<(OciManifest, String)> {
67        let reference = Self::to_reference(image)?;
68        let auth = self.registry_auth();
69        self.client
70            .pull_manifest(&reference, &auth)
71            .await
72            .map_err(|e| OciError::from_registry_message(e.to_string()))
73    }
74
75    pub async fn inspect(&self, image: &OciRef) -> Result<OciInspectResult> {
76        let (manifest, digest) = self.pull_manifest(image).await?;
77        let resolved_digest = if digest.is_empty() {
78            image.digest.clone()
79        } else {
80            Some(digest)
81        };
82        let (manifest_media_type, config_digest, layer_count, size, annotations) = match &manifest {
83            OciManifest::Image(img) => {
84                let layer_count = Some(img.layers.len());
85                let size = Some(
86                    img.layers
87                        .iter()
88                        .map(|l| l.size as u64)
89                        .fold(0u64, |acc, s| acc.saturating_add(s)),
90                );
91                (
92                    img.media_type.clone(),
93                    Some(img.config.digest.clone()),
94                    layer_count,
95                    size,
96                    img.annotations
97                        .as_ref()
98                        .and_then(|a| serde_json::to_value(a).ok()),
99                )
100            }
101            OciManifest::ImageIndex(idx) => (
102                idx.media_type.clone(),
103                None,
104                Some(idx.manifests.len()),
105                None,
106                idx.annotations
107                    .as_ref()
108                    .and_then(|a| serde_json::to_value(a).ok()),
109            ),
110        };
111
112        Ok(OciInspectResult {
113            registry: image.registry.clone(),
114            repository: image.repository.clone(),
115            tag: image.tag.clone(),
116            digest: image.digest.clone(),
117            resolved_digest,
118            manifest_media_type,
119            config_digest,
120            layer_count,
121            size,
122            annotations,
123            auth_source: self.auth.source.clone(),
124        })
125    }
126
127    pub async fn resolve_digest(&self, image: &OciRef) -> Result<String> {
128        self.inspect(image)
129            .await?
130            .resolved_digest
131            .ok_or_else(|| OciError::Other(format!("could not resolve digest for {image}")))
132    }
133
134    pub async fn exists(&self, image: &OciRef) -> Result<bool> {
135        match self.resolve_digest(image).await {
136            Ok(_) => Ok(true),
137            Err(OciError::NotFound(_)) => Ok(false),
138            Err(e) => Err(e),
139        }
140    }
141
142    pub async fn list_tags(&self, image: &OciRef, limit: Option<usize>) -> Result<Vec<String>> {
143        let repo_ref = image.repository_path();
144        let reference = repo_ref
145            .parse::<Reference>()
146            .map_err(|e| OciError::InvalidRef(format!("{repo_ref}: {e}")))?;
147        let auth = self.registry_auth();
148        let response = self
149            .client
150            .list_tags(&reference, &auth, None, None)
151            .await
152            .map_err(|e| OciError::from_registry_message(e.to_string()))?;
153        let mut tags = response.tags;
154        if let Some(limit) = limit {
155            tags.truncate(limit);
156        }
157        Ok(tags)
158    }
159
160    pub async fn push_manifest(&self, target: &OciRef, manifest: &OciManifest) -> Result<String> {
161        let reference = Self::to_reference(target)?;
162        // oci-client 0.17: push_manifest(image, manifest) — auth via client config/session.
163        // Some versions still take RegistryAuth on push_manifest_list only.
164        match manifest {
165            OciManifest::Image(img) => {
166                let wrapped = OciManifest::Image(img.clone());
167                self.client
168                    .push_manifest(&reference, &wrapped)
169                    .await
170                    .map_err(|e| OciError::from_registry_message(e.to_string()))
171            }
172            OciManifest::ImageIndex(idx) => {
173                let auth = self.registry_auth();
174                self.client
175                    .push_manifest_list(&reference, &auth, idx.clone())
176                    .await
177                    .map_err(|e| OciError::from_registry_message(e.to_string()))
178            }
179        }
180    }
181
182    pub async fn promote(
183        &self,
184        source: &OciRef,
185        target: &OciRef,
186        force: bool,
187    ) -> Result<String> {
188        let digest = self.resolve_digest(source).await?;
189        if !force {
190            if self.exists(target).await? {
191                return Err(OciError::TargetExists(target.reference()));
192            }
193        }
194        let source_digest_ref = source.with_digest(&digest);
195        let (manifest, _) = self.pull_manifest(&source_digest_ref).await?;
196        self.push_manifest(target, &manifest).await?;
197        Ok(digest)
198    }
199}