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#[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 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}