/home/runner/work/lyquor/lyquor/oci/src/oci.rs
Line | Count | Source |
1 | | use crate::registry::PulledContent; |
2 | | |
3 | | use super::{ |
4 | | pack::*, |
5 | | registry::{Error, Registry, RegistryOptions}, |
6 | | }; |
7 | | use async_trait::async_trait; |
8 | | use bytes::Bytes; |
9 | | use docker_credential::{self, DockerCredential}; |
10 | | pub use oci_client::Reference as OCIReference; |
11 | | pub use oci_client::secrets::RegistryAuth as OCIRegistryAuth; |
12 | | use oci_client::{ |
13 | | Client, Reference, |
14 | | client::{ClientConfig, ClientProtocol, Config, ImageLayer}, |
15 | | manifest, |
16 | | secrets::RegistryAuth, |
17 | | }; |
18 | | use std::{collections::BTreeMap, net::IpAddr}; |
19 | | |
20 | | const LYQUID_PACK_ASSET_NAME_KEY: &str = "assetName"; |
21 | | const LYQUID_PACK_ASSET_TYPE_KEY: &str = "assetType"; |
22 | | pub const LYQUID_PACK_ASSET_TYPE_VALUE_SOLIDITY: &str = "solidity"; |
23 | | pub const LYQUID_PACK_ASSET_TYPE_VALUE_LYQUID: &str = "lyquid"; |
24 | | pub const LYQUID_PACK_ASSET_TYPE_VALUE_ASSETS: &str = "assets"; |
25 | | |
26 | | #[derive(Clone)] |
27 | | pub struct OCIRegistry { |
28 | | auth: RegistryAuth, |
29 | | repository_address: String, |
30 | | client: Client, |
31 | | } |
32 | | |
33 | | impl OCIRegistry { |
34 | 0 | pub fn new(auth: OCIRegistryAuth, repository_address: &str) -> Self { |
35 | 0 | let config = ClientConfig { |
36 | 0 | protocol: match Self::check_localhost(repository_address) { |
37 | 0 | true => ClientProtocol::Http, |
38 | 0 | _ => ClientProtocol::Https, |
39 | | }, |
40 | 0 | ..Default::default() |
41 | | }; |
42 | | |
43 | 0 | OCIRegistry { |
44 | 0 | auth, |
45 | 0 | repository_address: repository_address.to_owned(), |
46 | 0 | client: Client::new(config), |
47 | 0 | } |
48 | 0 | } |
49 | | |
50 | 1 | pub fn new_with_config(auth: OCIRegistryAuth, repository_address: &str, config: ClientConfig) -> Self { |
51 | 1 | OCIRegistry { |
52 | 1 | auth, |
53 | 1 | repository_address: repository_address.to_owned(), |
54 | 1 | client: Client::new(config), |
55 | 1 | } |
56 | 1 | } |
57 | | |
58 | 0 | fn check_localhost(addr: &str) -> bool { |
59 | | // Get host+port body |
60 | 0 | let host = addr.split('/').next().unwrap_or(addr); |
61 | | |
62 | | // Remove port get actual host |
63 | 0 | let host = if host.starts_with('[') { |
64 | 0 | host.trim_start_matches('[').split(']').next().unwrap_or(host) |
65 | | } else { |
66 | 0 | host.split(':').next().unwrap_or(host) |
67 | | }; |
68 | | // Check hostname |
69 | 0 | if host.eq_ignore_ascii_case("localhost") { |
70 | 0 | return true; |
71 | 0 | } |
72 | | |
73 | | // Check IP |
74 | 0 | if let Ok(ip) = host.parse::<IpAddr>() { |
75 | 0 | return ip.is_loopback(); |
76 | 0 | } |
77 | | |
78 | 0 | false |
79 | 0 | } |
80 | | |
81 | | #[inline] |
82 | 6 | fn build_reference( |
83 | 6 | &self, name: &str, tag: Option<&str>, digest: Option<&LyquidPackDigest>, |
84 | 6 | ) -> Result<Reference, Error> { |
85 | 6 | let mut address = format!("{}/{}", self.repository_address, name); |
86 | | |
87 | 6 | if let Some(t1 ) = tag { |
88 | 1 | address.push(':'); |
89 | 1 | address.push_str(t); |
90 | 5 | } |
91 | 6 | if let Some(d1 ) = digest { |
92 | 1 | address.push('@'); |
93 | 1 | address.push_str(&d.to_oci_digest()); |
94 | 5 | } |
95 | | |
96 | 6 | Reference::try_from(address).map_err(|_| Error::InvalidRegistryAddress) |
97 | 6 | } |
98 | | |
99 | 0 | pub fn get_docker_credential(repository_address: &str) -> Result<OCIRegistryAuth, Error> { |
100 | 0 | match docker_credential::get_credential(repository_address) { |
101 | 0 | Ok(DockerCredential::UsernamePassword(user, passwd)) => Ok(OCIRegistryAuth::Basic(user, passwd)), |
102 | 0 | Ok(DockerCredential::IdentityToken(token)) => Ok(OCIRegistryAuth::Bearer(token)), |
103 | 0 | Err(e) => Err(Error::DockerCredentialNotFound(e)), |
104 | | } |
105 | 0 | } |
106 | | |
107 | 0 | pub async fn resolve_digest(&self, name: &str, tag: Option<&str>) -> Result<LyquidPackDigest, Error> { |
108 | 0 | let reference = self.build_reference(name, tag, None)?; |
109 | 0 | let digest = self |
110 | 0 | .client |
111 | 0 | .fetch_manifest_digest(&reference, &self.auth) |
112 | 0 | .await |
113 | 0 | .map_err(|e| Error::OciDistributionError(e.to_string()))?; |
114 | 0 | LyquidPackDigest::from_oci_digest(&digest).map_err(|_| Error::BadDigest) |
115 | 0 | } |
116 | | |
117 | | #[inline] |
118 | 1 | async fn push_pack(&self, pack: LyquidPack, _options: Option<RegistryOptions>) -> Result<LyquidPackDigest, Error> { |
119 | 1 | let reference = |
120 | 1 | self.build_reference(&pack.metadata.name, Some(pack.metadata.tag.to_string().as_str()), None)?0 ; |
121 | | |
122 | | // Add wasm |
123 | 1 | let layer_lyquid = ImageLayer { |
124 | 1 | data: pack.wasm, |
125 | 1 | media_type: manifest::WASM_LAYER_MEDIA_TYPE.to_owned(), |
126 | 1 | annotations: Some(BTreeMap::from([( |
127 | 1 | LYQUID_PACK_ASSET_TYPE_KEY.to_owned(), |
128 | 1 | LYQUID_PACK_ASSET_TYPE_VALUE_LYQUID.to_owned(), |
129 | 1 | )])), |
130 | 1 | }; |
131 | 1 | let mut layers = vec![layer_lyquid]; |
132 | | |
133 | | // Add solidity |
134 | 1 | let layer_solidity = ImageLayer { |
135 | 1 | data: pack.solidity, |
136 | 1 | media_type: manifest::IMAGE_LAYER_MEDIA_TYPE.to_owned(), |
137 | 1 | annotations: Some(BTreeMap::from([( |
138 | 1 | LYQUID_PACK_ASSET_TYPE_KEY.to_owned(), |
139 | 1 | LYQUID_PACK_ASSET_TYPE_VALUE_SOLIDITY.to_owned(), |
140 | 1 | )])), |
141 | 1 | }; |
142 | 1 | layers.push(layer_solidity); |
143 | | |
144 | | // Add assets |
145 | 2 | for (key, val) in pack.assets1 .iter1 ().flat_map1 (|a| a1 .iter1 ()) { |
146 | 2 | let asset_layer = ImageLayer { |
147 | 2 | data: val.clone(), |
148 | 2 | media_type: manifest::IMAGE_LAYER_MEDIA_TYPE.to_owned(), |
149 | 2 | annotations: Some(BTreeMap::from([ |
150 | 2 | ( |
151 | 2 | LYQUID_PACK_ASSET_TYPE_KEY.to_owned(), |
152 | 2 | LYQUID_PACK_ASSET_TYPE_VALUE_ASSETS.to_owned(), |
153 | 2 | ), |
154 | 2 | (LYQUID_PACK_ASSET_NAME_KEY.to_owned(), key.to_owned()), |
155 | 2 | ])), |
156 | 2 | }; |
157 | 2 | layers.push(asset_layer); |
158 | 2 | } |
159 | | |
160 | | // Add config |
161 | 1 | let config_data = pack.metadata.to_json().map_err(|_| Error::BadMetadata)?0 ; |
162 | | |
163 | 1 | let config = Config { |
164 | 1 | data: config_data.into(), |
165 | 1 | media_type: manifest::WASM_CONFIG_MEDIA_TYPE.to_owned(), |
166 | 1 | annotations: None, |
167 | 1 | }; |
168 | | |
169 | 1 | let mut manifest = manifest::OciImageManifest::build(&layers, &config, None); |
170 | 1 | manifest.media_type = Some(manifest::OCI_IMAGE_MEDIA_TYPE.to_owned()); |
171 | | |
172 | 1 | let _ = self |
173 | 1 | .client |
174 | 1 | .push(&reference, &layers, config, &self.auth, Some(manifest)) |
175 | 1 | .await |
176 | 1 | .map_err(|e| Error::PushError(e0 .to_string0 ()))?0 ; |
177 | | |
178 | | // the only way the get the image digest in oci_client is to fetch it from the remote. |
179 | 1 | let digest = self |
180 | 1 | .client |
181 | 1 | .fetch_manifest_digest(&reference, &self.auth) |
182 | 1 | .await |
183 | 1 | .map_err(|e| Error::OciDistributionError(e0 .to_string0 ()))?0 ; |
184 | 1 | LyquidPackDigest::from_oci_digest(&digest).map_err(|_| Error::BadDigest) |
185 | 1 | } |
186 | | |
187 | | #[inline] |
188 | 2 | async fn pull_pack( |
189 | 2 | &self, name: &str, tag: Option<&str>, digest: Option<&LyquidPackDigest>, _options: Option<RegistryOptions>, |
190 | 2 | ) -> Result<super::pack::LyquidPack, Error> { |
191 | 2 | let reference = self.build_reference(name, tag, digest)?0 ; |
192 | 2 | let accepted_media_types = vec![ |
193 | | manifest::IMAGE_MANIFEST_MEDIA_TYPE, |
194 | 2 | manifest::OCI_IMAGE_MEDIA_TYPE, |
195 | 2 | manifest::WASM_LAYER_MEDIA_TYPE, |
196 | 2 | manifest::IMAGE_LAYER_MEDIA_TYPE, |
197 | | ]; |
198 | 2 | let image = self |
199 | 2 | .client |
200 | 2 | .pull(&reference, &self.auth, accepted_media_types) |
201 | 2 | .await |
202 | 2 | .map_err(|e| Error::PullError(e0 .to_string0 ()))?0 ; |
203 | | |
204 | 2 | let mut wasm = Bytes::new(); |
205 | 2 | let mut solidity = Bytes::new(); |
206 | 2 | let mut assets = BTreeMap::new(); |
207 | 8 | for layer in image.layers2 .into_iter2 () { |
208 | | // Find wasm |
209 | 8 | if layer.media_type == manifest::WASM_LAYER_MEDIA_TYPE { |
210 | 2 | wasm = Bytes::from(layer.data); |
211 | 2 | continue; |
212 | 6 | } |
213 | | |
214 | | // Other data should be in image layer |
215 | 6 | if layer.media_type != manifest::IMAGE_LAYER_MEDIA_TYPE { |
216 | 0 | continue; |
217 | 6 | } |
218 | | |
219 | 6 | let Some(annotations) = layer.annotations else { |
220 | 0 | continue; |
221 | | }; |
222 | | |
223 | | // Find solidity / assets |
224 | 6 | match annotations.get(LYQUID_PACK_ASSET_TYPE_KEY) { |
225 | 6 | Some(v) => match v.as_str() { |
226 | 6 | LYQUID_PACK_ASSET_TYPE_VALUE_SOLIDITY => { |
227 | 2 | solidity = Bytes::from(layer.data); |
228 | 2 | } |
229 | 4 | LYQUID_PACK_ASSET_TYPE_VALUE_ASSETS => { |
230 | 4 | match annotations.get(LYQUID_PACK_ASSET_NAME_KEY) { |
231 | 4 | Some(an) => { |
232 | 4 | assets.insert(an.to_owned(), Bytes::from(layer.data)); |
233 | 4 | } |
234 | 0 | _ => continue, |
235 | | }; |
236 | | } |
237 | 0 | _ => continue, |
238 | | }, |
239 | 0 | _ => continue, |
240 | | } |
241 | | } |
242 | | |
243 | 2 | if wasm.is_empty() { |
244 | 0 | return Err(Error::BadImage("Missing wasm binary.".into())); |
245 | 2 | } |
246 | | |
247 | 2 | let assets = if assets.is_empty() { None0 } else { Some(assets) }; |
248 | | |
249 | | // metadata |
250 | 2 | let metadata = LyquidPackMetadata::from_json(image.config.data.to_vec()).map_err(|_| Error::BadMetadata)?0 ; |
251 | | |
252 | | // digest |
253 | 2 | let digest = image.digest.ok_or(Error::BadDigest)?0 ; |
254 | 2 | let digest = LyquidPackDigest::from_oci_digest(&digest).map_err(|_| Error::BadDigest)?0 ; |
255 | | |
256 | 2 | Ok(LyquidPack { |
257 | 2 | wasm, |
258 | 2 | solidity, |
259 | 2 | assets, |
260 | 2 | metadata, |
261 | 2 | digest: Some(digest), |
262 | 2 | }) |
263 | 2 | } |
264 | | |
265 | | #[inline] |
266 | 2 | async fn pull_layer( |
267 | 2 | &self, name: &str, tag: Option<&str>, digest: Option<&LyquidPackDigest>, layer_type: &str, |
268 | 2 | layer_name: Option<&str>, _options: Option<RegistryOptions>, |
269 | 2 | ) -> Result<Bytes, Error> { |
270 | 2 | let mut layer_bytes = None; |
271 | 2 | let reference = self.build_reference(name, tag, digest)?0 ; |
272 | | |
273 | | // Get manifest |
274 | 2 | let (manifest, _) = self |
275 | 2 | .client |
276 | 2 | .pull_manifest(&reference, &self.auth) |
277 | 2 | .await |
278 | 2 | .map_err(|e| Error::PullError(e0 .to_string0 ()))?0 ; |
279 | 2 | let layers = match manifest { |
280 | 2 | manifest::OciManifest::Image(oim) => oim.layers, |
281 | 0 | manifest::OciManifest::ImageIndex(oii) => { |
282 | 0 | let mut found = None; |
283 | | // This part of code is for future-proof. |
284 | | // Currently we use os to identify major lyquor versions. |
285 | | // Refer: https://docs.rs/oci-client/latest/oci_client/manifest/struct.Platform.html |
286 | 0 | for manifest in oii.manifests { |
287 | 0 | if let Some(platform) = manifest.platform { |
288 | | // Found the platform we want |
289 | 0 | if platform.architecture.to_string() == LYQUID_PACK_METADATA_ARCHITECTURE_VALUE && |
290 | 0 | platform.os == oci_client::config::Os::Other(LYQUID_PACK_METADATA_OS_DEFAULT.to_owned()) && |
291 | 0 | check_lyquid_os_version_compat( |
292 | 0 | LYQUID_PACK_METADATA_OS_VERSION_DEFAULT, |
293 | 0 | &platform.os_version.unwrap_or_default(), |
294 | | ) |
295 | | { |
296 | 0 | let reference = reference.clone_with_digest(manifest.digest); |
297 | | // Pull manifest for this platform |
298 | 0 | let (manifest, _) = self |
299 | 0 | .client |
300 | 0 | .pull_manifest(&reference, &self.auth) |
301 | 0 | .await |
302 | 0 | .map_err(|e| Error::PullError(e.to_string()))?; |
303 | 0 | match manifest { |
304 | 0 | manifest::OciManifest::Image(oim) => found = Some(oim), |
305 | | manifest::OciManifest::ImageIndex(_) => { |
306 | 0 | return Err(Error::PullError("Unexpected nested index.".to_owned())); |
307 | | } |
308 | | }; |
309 | 0 | break; |
310 | 0 | } |
311 | | } else { |
312 | | // A multi-arch manifest must have a platform field |
313 | 0 | continue; |
314 | | } |
315 | | } |
316 | | |
317 | 0 | match found { |
318 | 0 | Some(manifest) => manifest.layers, |
319 | 0 | None => return Err(Error::PullError("Layer not found. No matching platform.".to_owned())), |
320 | | } |
321 | | } |
322 | | }; |
323 | | |
324 | | // Find layer digest and pull it |
325 | 5 | for od in layers2 { |
326 | 5 | if let Some(ref annotation) = od.annotations { |
327 | 5 | let Some(l_type) = annotation.get(LYQUID_PACK_ASSET_TYPE_KEY) else { |
328 | 0 | continue; |
329 | | }; |
330 | 5 | let l_name = annotation.get(LYQUID_PACK_ASSET_NAME_KEY); |
331 | | |
332 | 5 | if l_type != layer_type { |
333 | 3 | continue; |
334 | 2 | } |
335 | | |
336 | 2 | if let Some(layer_name1 ) = layer_name { |
337 | 1 | let Some(l_name) = l_name else { |
338 | 0 | continue; |
339 | | }; |
340 | 1 | if l_name != layer_name { |
341 | 0 | continue; |
342 | 1 | } |
343 | 1 | } |
344 | | |
345 | | // pull it |
346 | 2 | let mut out = Vec::with_capacity(od.size.clone().try_into().unwrap_or_default()); |
347 | 2 | self.client |
348 | 2 | .pull_blob(&reference, &od, &mut out) |
349 | 2 | .await |
350 | 2 | .map_err(|e| Error::PullError(e0 .to_string0 ()))?0 ; |
351 | | |
352 | 2 | layer_bytes = Some(Bytes::from(out)); |
353 | 2 | break; |
354 | | } else { |
355 | 0 | continue; |
356 | | } |
357 | | } |
358 | | |
359 | 2 | layer_bytes.ok_or(Error::PullError("Layer not found.".to_owned())) |
360 | 2 | } |
361 | | } |
362 | | |
363 | | #[async_trait] |
364 | | impl Registry for OCIRegistry { |
365 | 1 | async fn push(&self, pack: LyquidPack, options: Option<RegistryOptions>) -> Result<LyquidPackDigest, Error> { |
366 | | self.push_pack(pack, options).await |
367 | 1 | } |
368 | | |
369 | | async fn pull( |
370 | | &self, name: &str, tag: Option<&str>, digest: Option<&LyquidPackDigest>, layer_type: Option<&str>, |
371 | | layer_name: Option<&str>, options: Option<RegistryOptions>, |
372 | 4 | ) -> Result<PulledContent, Error> { |
373 | | if let Some(layer_type) = layer_type { |
374 | | let layer_byte = self |
375 | | .pull_layer(name, tag, digest, layer_type, layer_name, options) |
376 | | .await?; |
377 | | Ok(PulledContent::Layer(layer_byte)) |
378 | | } else { |
379 | | let pack = self.pull_pack(name, tag, digest, options).await?; |
380 | | Ok(PulledContent::LyquidPack(pack)) |
381 | | } |
382 | 4 | } |
383 | | |
384 | | async fn query( |
385 | | &self, name: &str, tag: Option<&str>, digest: Option<&LyquidPackDigest>, |
386 | 1 | ) -> Result<LyquidPackMetadata, Error> { |
387 | | let reference = self.build_reference(name, tag, digest)?; |
388 | | let (_, _, config) = self |
389 | | .client |
390 | | .pull_manifest_and_config(&reference, &self.auth) |
391 | | .await |
392 | 0 | .map_err(|e| Error::OciDistributionError(e.to_string()))?; |
393 | | |
394 | | let config = config.into_bytes(); |
395 | | LyquidPackMetadata::from_json(config).map_err(|_| Error::BadMetadata) |
396 | 1 | } |
397 | | } |