1use std::collections::{BTreeMap, HashMap};
3use std::convert::TryFrom;
4use std::hash::Hash;
5use std::pin::pin;
6use std::sync::Arc;
7use std::time::Duration;
8
9use futures_util::stream::{self, BoxStream, StreamExt, TryStreamExt};
10use futures_util::{future, Stream};
11use http::header::RANGE;
12use http::{HeaderValue, StatusCode};
13use http_auth::{parser::ChallengeParser, ChallengeRef};
14use oci_spec::image::{Arch, Os};
15use olpc_cjson::CanonicalFormatter;
16use reqwest::header::HeaderMap;
17use reqwest::{NoProxy, Proxy, RequestBuilder, Response, Url};
18use serde::{Deserialize, Deserializer, Serialize};
19use sha2::Digest as _;
20use tokio::io::{AsyncWrite, AsyncWriteExt};
21use tokio::sync::RwLock;
22use tracing::{debug, trace, warn};
23
24pub use crate::blob::*;
25use crate::config::ConfigFile;
26use crate::digest::{digest_header_value, validate_digest, Digest, Digester};
27use crate::errors::*;
28use crate::manifest::{
29 ImageIndexEntry, OciDescriptor, OciImageIndex, OciImageManifest, OciManifest, Versioned,
30 IMAGE_CONFIG_MEDIA_TYPE, IMAGE_LAYER_GZIP_MEDIA_TYPE, IMAGE_LAYER_MEDIA_TYPE,
31 IMAGE_MANIFEST_LIST_MEDIA_TYPE, IMAGE_MANIFEST_MEDIA_TYPE, OCI_IMAGE_INDEX_MEDIA_TYPE,
32 OCI_IMAGE_MEDIA_TYPE,
33};
34use crate::secrets::RegistryAuth;
35use crate::secrets::*;
36use crate::sha256_digest;
37use crate::token_cache::{RegistryOperation, RegistryToken, RegistryTokenType, TokenCache};
38use crate::Reference;
39
40const MIME_TYPES_DISTRIBUTION_MANIFEST: &[&str] = &[
41 IMAGE_MANIFEST_MEDIA_TYPE,
42 IMAGE_MANIFEST_LIST_MEDIA_TYPE,
43 OCI_IMAGE_MEDIA_TYPE,
44 OCI_IMAGE_INDEX_MEDIA_TYPE,
45];
46
47const PUSH_CHUNK_MAX_SIZE: usize = 4096 * 1024;
48
49pub const DEFAULT_MAX_CONCURRENT_UPLOAD: usize = 16;
51
52pub const DEFAULT_MAX_CONCURRENT_DOWNLOAD: usize = 16;
54
55pub const DEFAULT_TOKEN_EXPIRATION_SECS: usize = 60;
57
58static DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
59
60#[derive(Clone)]
62pub struct ImageData {
63 pub layers: Vec<ImageLayer>,
65 pub digest: Option<String>,
67 pub config: Config,
69 pub manifest: Option<OciImageManifest>,
71}
72
73pub struct PushResponse {
76 pub config_url: String,
78 pub manifest_url: String,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct PushBlobStreamChunkedResponse {
85 pub blob_url: String,
87 pub blob_digest: String,
89 pub size: u64,
91}
92
93#[derive(Deserialize, Debug)]
95pub struct TagResponse {
96 pub name: String,
98 #[serde(deserialize_with = "null_as_default")]
100 pub tags: Vec<String>,
101}
102
103fn null_as_default<'de, D, T>(d: D) -> std::result::Result<T, D::Error>
105where
106 D: Deserializer<'de>,
107 T: Default + Deserialize<'de>,
108{
109 let res = <Option<T>>::deserialize(d)?.unwrap_or_default();
110 Ok(res)
111}
112
113#[derive(Deserialize, Debug)]
115pub struct CatalogResponse {
116 pub repositories: Vec<String>,
118}
119
120pub struct LayerDescriptor<'a> {
122 pub digest: &'a str,
124 pub urls: &'a Option<Vec<String>>,
126}
127
128pub trait AsLayerDescriptor {
130 fn as_layer_descriptor(&self) -> LayerDescriptor<'_>;
132}
133
134impl<T: AsLayerDescriptor> AsLayerDescriptor for &T {
135 fn as_layer_descriptor(&self) -> LayerDescriptor<'_> {
136 (*self).as_layer_descriptor()
137 }
138}
139
140impl AsLayerDescriptor for &str {
141 fn as_layer_descriptor(&self) -> LayerDescriptor<'_> {
142 LayerDescriptor {
143 digest: self,
144 urls: &None,
145 }
146 }
147}
148
149impl AsLayerDescriptor for &OciDescriptor {
150 fn as_layer_descriptor(&self) -> LayerDescriptor<'_> {
151 LayerDescriptor {
152 digest: &self.digest,
153 urls: &self.urls,
154 }
155 }
156}
157
158impl AsLayerDescriptor for &LayerDescriptor<'_> {
159 fn as_layer_descriptor(&self) -> LayerDescriptor<'_> {
160 LayerDescriptor {
161 digest: self.digest,
162 urls: self.urls,
163 }
164 }
165}
166
167#[derive(Clone, Debug, Eq, Hash, PartialEq)]
169pub struct ImageLayer {
170 pub data: bytes::Bytes,
172 pub media_type: String,
174 pub annotations: Option<BTreeMap<String, String>>,
177}
178
179impl ImageLayer {
180 pub fn new(
182 data: impl Into<bytes::Bytes>,
183 media_type: String,
184 annotations: Option<BTreeMap<String, String>>,
185 ) -> Self {
186 ImageLayer {
187 data: data.into(),
188 media_type,
189 annotations,
190 }
191 }
192
193 pub fn oci_v1(
196 data: impl Into<bytes::Bytes>,
197 annotations: Option<BTreeMap<String, String>>,
198 ) -> Self {
199 Self::new(data, IMAGE_LAYER_MEDIA_TYPE.to_string(), annotations)
200 }
201 pub fn oci_v1_gzip(
204 data: impl Into<bytes::Bytes>,
205 annotations: Option<BTreeMap<String, String>>,
206 ) -> Self {
207 Self::new(data, IMAGE_LAYER_GZIP_MEDIA_TYPE.to_string(), annotations)
208 }
209
210 pub fn sha256_digest(&self) -> String {
212 sha256_digest(&self.data)
213 }
214}
215
216#[derive(Clone)]
218pub struct Config {
219 pub data: bytes::Bytes,
221 pub media_type: String,
223 pub annotations: Option<BTreeMap<String, String>>,
226}
227
228impl Config {
229 pub fn new(
231 data: impl Into<bytes::Bytes>,
232 media_type: String,
233 annotations: Option<BTreeMap<String, String>>,
234 ) -> Self {
235 Config {
236 data: data.into(),
237 media_type,
238 annotations,
239 }
240 }
241
242 pub fn oci_v1(
245 data: impl Into<bytes::Bytes>,
246 annotations: Option<BTreeMap<String, String>>,
247 ) -> Self {
248 Self::new(data, IMAGE_CONFIG_MEDIA_TYPE.to_string(), annotations)
249 }
250
251 pub fn oci_v1_from_config_file(
254 config_file: ConfigFile,
255 annotations: Option<BTreeMap<String, String>>,
256 ) -> Result<Self> {
257 let data = serde_json::to_vec(&config_file)?;
258 Ok(Self::new(
259 data,
260 IMAGE_CONFIG_MEDIA_TYPE.to_string(),
261 annotations,
262 ))
263 }
264
265 pub fn sha256_digest(&self) -> String {
267 sha256_digest(&self.data)
268 }
269}
270
271impl TryFrom<Config> for ConfigFile {
272 type Error = crate::errors::OciDistributionError;
273
274 fn try_from(config: Config) -> Result<Self> {
275 let config = String::from_utf8(config.data.into())
276 .map_err(|e| OciDistributionError::ConfigConversionError(e.to_string()))?;
277 let config_file: ConfigFile = serde_json::from_str(&config)
278 .map_err(|e| OciDistributionError::ConfigConversionError(e.to_string()))?;
279 Ok(config_file)
280 }
281}
282
283#[derive(Clone)]
298pub struct Client {
299 config: Arc<ClientConfig>,
300 auth_store: Arc<RwLock<HashMap<String, RegistryAuth>>>,
302 pub tokens: TokenCache,
304 client: reqwest::Client,
305 push_chunk_size: usize,
306}
307
308impl Default for Client {
309 fn default() -> Self {
310 Self {
311 config: Arc::default(),
312 auth_store: Arc::default(),
313 tokens: TokenCache::new(DEFAULT_TOKEN_EXPIRATION_SECS),
314 client: reqwest::Client::default(),
315 push_chunk_size: PUSH_CHUNK_MAX_SIZE,
316 }
317 }
318}
319
320pub trait ClientConfigSource {
324 fn client_config(&self) -> ClientConfig;
326}
327
328impl TryFrom<ClientConfig> for Client {
329 type Error = OciDistributionError;
330
331 fn try_from(config: ClientConfig) -> std::result::Result<Self, Self::Error> {
332 #[allow(unused_mut)]
333 let mut client_builder = reqwest::Client::builder();
334 #[cfg(not(target_arch = "wasm32"))]
335 let mut client_builder =
336 client_builder.danger_accept_invalid_certs(config.accept_invalid_certificates);
337
338 client_builder = match () {
339 #[cfg(all(feature = "native-tls", not(target_arch = "wasm32")))]
340 () => client_builder.danger_accept_invalid_hostnames(config.accept_invalid_hostnames),
341 #[cfg(any(not(feature = "native-tls"), target_arch = "wasm32"))]
342 () => client_builder,
343 };
344
345 #[cfg(not(target_arch = "wasm32"))]
346 {
347 if !config.tls_certs_only.is_empty() {
348 client_builder =
349 client_builder.tls_certs_only(convert_certificates(&config.tls_certs_only)?);
350 }
351 client_builder = client_builder
352 .tls_certs_merge(convert_certificates(&config.extra_root_certificates)?);
353 }
354
355 if let Some(timeout) = config.read_timeout {
356 client_builder = client_builder.read_timeout(timeout);
357 }
358 if let Some(timeout) = config.connect_timeout {
359 client_builder = client_builder.connect_timeout(timeout);
360 }
361
362 client_builder = client_builder.user_agent(config.user_agent);
363
364 if let Some(proxy_addr) = &config.https_proxy {
365 let no_proxy = config
366 .no_proxy
367 .as_ref()
368 .and_then(|no_proxy| NoProxy::from_string(no_proxy));
369 let proxy = Proxy::https(proxy_addr)?.no_proxy(no_proxy);
370 client_builder = client_builder.proxy(proxy);
371 }
372
373 if let Some(proxy_addr) = &config.http_proxy {
374 let no_proxy = config
375 .no_proxy
376 .as_ref()
377 .and_then(|no_proxy| NoProxy::from_string(no_proxy));
378 let proxy = Proxy::http(proxy_addr)?.no_proxy(no_proxy);
379 client_builder = client_builder.proxy(proxy);
380 }
381
382 let default_token_expiration_secs = config.default_token_expiration_secs;
383 Ok(Self {
384 config: Arc::new(config),
385 tokens: TokenCache::new(default_token_expiration_secs),
386 client: client_builder.build()?,
387 push_chunk_size: PUSH_CHUNK_MAX_SIZE,
388 ..Default::default()
389 })
390 }
391}
392
393impl Client {
394 pub fn new(config: ClientConfig) -> Self {
396 let default_token_expiration_secs = config.default_token_expiration_secs;
397 Client::try_from(config).unwrap_or_else(|err| {
398 warn!("Cannot create OCI client from config: {:?}", err);
399 warn!("Creating client with default configuration");
400 Self {
401 tokens: TokenCache::new(default_token_expiration_secs),
402 push_chunk_size: PUSH_CHUNK_MAX_SIZE,
403 ..Default::default()
404 }
405 })
406 }
407
408 pub fn from_source(config_source: &impl ClientConfigSource) -> Self {
410 Self::new(config_source.client_config())
411 }
412
413 async fn store_auth(&self, registry: &str, auth: RegistryAuth) {
414 self.auth_store
415 .write()
416 .await
417 .insert(registry.to_string(), auth);
418 }
419
420 async fn is_stored_auth(&self, registry: &str) -> bool {
421 self.auth_store.read().await.contains_key(registry)
422 }
423
424 pub async fn store_auth_if_needed(&self, registry: &str, auth: &RegistryAuth) {
433 if !self.is_stored_auth(registry).await {
434 self.store_auth(registry, auth.clone()).await;
435 }
436 }
437
438 async fn get_auth_token(
440 &self,
441 reference: &Reference,
442 op: RegistryOperation,
443 ) -> Option<RegistryTokenType> {
444 let registry = reference.resolve_registry();
445 let auth = self.auth_store.read().await.get(registry)?.clone();
446 match self.tokens.get(reference, op).await {
447 Some(token) => Some(token),
448 None => {
449 let token = self._auth(reference, &auth, op).await.ok()??;
450 self.tokens.insert(reference, op, token.clone()).await;
451 Some(token)
452 }
453 }
454 }
455
456 pub async fn list_tags(
461 &self,
462 image: &Reference,
463 auth: &RegistryAuth,
464 n: Option<usize>,
465 last: Option<&str>,
466 ) -> Result<TagResponse> {
467 let op = RegistryOperation::Pull;
468 let url = self.to_list_tags_url(image);
469
470 self.store_auth_if_needed(image.resolve_registry(), auth)
471 .await;
472
473 let request = self.client.get(&url);
474 let request = if let Some(num) = n {
475 request.query(&[("n", num)])
476 } else {
477 request
478 };
479 let request = if let Some(l) = last {
480 request.query(&[("last", l)])
481 } else {
482 request
483 };
484 let request = RequestBuilderWrapper {
485 client: self,
486 request_builder: request,
487 };
488 let res = request
489 .apply_auth(image, op)
490 .await?
491 .into_request_builder()
492 .send()
493 .await?;
494 let status = res.status();
495 let body = res.bytes().await?;
496
497 validate_registry_response(status, &body, &url)?;
498
499 Ok(serde_json::from_str(std::str::from_utf8(&body)?)?)
500 }
501
502 pub async fn pull(
507 &self,
508 image: &Reference,
509 auth: &RegistryAuth,
510 accepted_media_types: Vec<&str>,
511 ) -> Result<ImageData> {
512 debug!("Pulling image: {:?}", image);
513 self.store_auth_if_needed(image.resolve_registry(), auth)
514 .await;
515
516 let (manifest, digest, config) = self._pull_manifest_and_config(image).await?;
517
518 self.validate_layers(&manifest, accepted_media_types)
519 .await?;
520
521 let layers = stream::iter(&manifest.layers)
522 .map(|layer| {
523 let this = &self;
527 async move {
528 let mut out: Vec<u8> = Vec::new();
529 debug!("Pulling image layer");
530 this.pull_blob(image, layer, &mut out).await?;
531 Ok::<_, OciDistributionError>(ImageLayer::new(
532 out,
533 layer.media_type.clone(),
534 layer.annotations.clone(),
535 ))
536 }
537 })
538 .boxed() .buffer_unordered(self.config.max_concurrent_download)
540 .try_collect()
541 .await?;
542
543 Ok(ImageData {
544 layers,
545 manifest: Some(manifest),
546 config,
547 digest: Some(digest),
548 })
549 }
550
551 pub async fn blob_exists(&self, image: &Reference, digest: &str) -> Result<bool> {
553 let url = self.to_v2_blob_url(image, digest);
554 let request = RequestBuilderWrapper {
555 client: self,
556 request_builder: self.client.head(&url),
557 };
558
559 let res = request
560 .apply_auth(image, RegistryOperation::Pull)
561 .await?
562 .into_request_builder()
563 .send()
564 .await?;
565
566 match res.error_for_status() {
567 Ok(_) => Ok(true),
568 Err(err) => {
569 if err.status() == Some(StatusCode::NOT_FOUND) {
570 Ok(false)
571 } else {
572 Err(err.into())
573 }
574 }
575 }
576 }
577
578 pub async fn push(
588 &self,
589 image_ref: &Reference,
590 layers: &[ImageLayer],
591 config: Config,
592 auth: &RegistryAuth,
593 manifest: Option<OciImageManifest>,
594 ) -> Result<PushResponse> {
595 debug!("Pushing image: {:?}", image_ref);
596 self.store_auth_if_needed(image_ref.resolve_registry(), auth)
597 .await;
598
599 let manifest: OciImageManifest = match manifest {
600 Some(m) => m,
601 None => OciImageManifest::build(layers, &config, None),
602 };
603
604 let layer_digests: Vec<String> = if manifest.layers.len() == layers.len() {
614 manifest.layers.iter().map(|d| d.digest.clone()).collect()
615 } else {
616 layers.iter().map(|l| l.sha256_digest()).collect()
617 };
618 stream::iter(layers.iter().zip(layer_digests))
619 .map(|(layer, digest)| {
620 let this = &self;
624 async move {
625 this.push_blob(image_ref, layer.data.clone(), &digest)
626 .await?;
627 Result::Ok(())
628 }
629 })
630 .boxed() .buffer_unordered(self.config.max_concurrent_upload)
632 .try_for_each(future::ok)
633 .await?;
634
635 let config_url = self
636 .push_blob(image_ref, config.data, &manifest.config.digest)
637 .await?;
638 let manifest_url = self.push_manifest(image_ref, &manifest.into()).await?;
639
640 Ok(PushResponse {
641 config_url,
642 manifest_url,
643 })
644 }
645
646 pub async fn push_blob(
648 &self,
649 image_ref: &Reference,
650 data: impl Into<bytes::Bytes>,
651 digest: &str,
652 ) -> Result<String> {
653 if self.config.use_monolithic_push {
654 return self.push_blob_monolithically(image_ref, data, digest).await;
655 }
656 let data = data.into();
657 match self
661 .push_blob_chunked(image_ref, data.clone(), digest)
662 .await
663 {
664 Ok(url) => Ok(url),
665 Err(OciDistributionError::SpecViolationError(violation)) => {
666 warn!(?violation, "Registry is not respecting the OCI Distribution Specification when doing chunked push operations");
667 warn!("Attempting monolithic push");
668 self.push_blob_monolithically(image_ref, data, digest).await
669 }
670 Err(e) => Err(e),
671 }
672 }
673
674 async fn push_blob_monolithically(
678 &self,
679 image: &Reference,
680 blob_data: impl Into<bytes::Bytes>,
681 blob_digest: &str,
682 ) -> Result<String> {
683 let location = self.begin_push_monolithical_session(image).await?;
684 self.push_monolithically(&location, image, blob_data, blob_digest)
685 .await
686 }
687
688 async fn push_blob_chunked(
692 &self,
693 image: &Reference,
694 blob_data: impl Into<bytes::Bytes>,
695 blob_digest: &str,
696 ) -> Result<String> {
697 let mut location = self.begin_push_chunked_session(image).await?;
698 let mut start: usize = 0;
699
700 let mut blob_data: bytes::Bytes = blob_data.into();
701 while !blob_data.is_empty() {
702 let chunk_size = self.push_chunk_size.min(blob_data.len());
703 let chunk = blob_data.split_to(chunk_size);
704 (location, start) = self.push_chunk(&location, image, chunk, start).await?;
705 }
706 self.end_push_chunked_session(&location, image, blob_digest)
707 .await
708 }
709
710 pub async fn push_blob_stream<T: Stream<Item = Result<bytes::Bytes>> + Send + 'static>(
725 &self,
726 image: &Reference,
727 blob_data_stream: T,
728 blob_digest: &str,
729 size: Option<u64>,
730 ) -> Result<String> {
731 if self.config.use_monolithic_push {
732 let size = size.ok_or_else(|| {
733 OciDistributionError::GenericError(Some(
734 "size must be provided when use_monolithic_push is enabled".to_string(),
735 ))
736 })?;
737 let location = self.begin_push_monolithical_session(image).await?;
738 return self
739 .push_stream_monolithically(&location, image, blob_data_stream, size, blob_digest)
740 .await;
741 }
742
743 let location = self.begin_push_chunked_session(image).await?;
744 let (location, _size) = self
745 .push_stream_chunks(location, image, blob_data_stream, |_| {})
746 .await?;
747 self.end_push_chunked_session(&location, image, blob_digest)
748 .await
749 }
750
751 pub async fn push_blob_stream_chunked<
763 T: Stream<Item = Result<bytes::Bytes>> + Send + 'static,
764 >(
765 &self,
766 image: &Reference,
767 blob_data_stream: T,
768 ) -> Result<PushBlobStreamChunkedResponse> {
769 if self.config.use_monolithic_push {
770 debug!(
771 "use_monolithic_push is enabled, but push_blob_stream_chunked always uses chunked PATCH requests; ignoring"
772 );
773 }
774
775 let location = self.begin_push_chunked_session(image).await?;
776
777 let mut digester = Digester::Sha256(sha2::Sha256::new());
778 let (location, size) = self
779 .push_stream_chunks(location, image, blob_data_stream, |chunk| {
780 digester.update(chunk)
781 })
782 .await?;
783
784 if size == 0 {
785 return Err(OciDistributionError::PushNoDataError);
786 }
787
788 let blob_digest = digester.finalize();
789 let location = self
790 .end_push_chunked_session(&location, image, &blob_digest)
791 .await?;
792
793 Ok(PushBlobStreamChunkedResponse {
794 blob_url: location,
795 blob_digest,
796 size,
797 })
798 }
799
800 pub async fn auth(
805 &self,
806 image: &Reference,
807 authentication: &RegistryAuth,
808 operation: RegistryOperation,
809 ) -> Result<Option<String>> {
810 self.store_auth_if_needed(image.resolve_registry(), authentication)
811 .await;
812 match self._auth(image, authentication, operation).await {
814 Ok(Some(RegistryTokenType::Bearer(token))) => {
815 self.tokens
816 .insert(image, operation, RegistryTokenType::Bearer(token.clone()))
817 .await;
818 Ok(Some(token.token().to_string()))
819 }
820 Ok(Some(RegistryTokenType::Basic(username, password))) => {
821 self.tokens
822 .insert(
823 image,
824 operation,
825 RegistryTokenType::Basic(username, password),
826 )
827 .await;
828 Ok(None)
829 }
830 Ok(None) => Ok(None),
831 Err(e) => Err(e),
832 }
833 }
834
835 async fn _auth(
837 &self,
838 image: &Reference,
839 authentication: &RegistryAuth,
840 operation: RegistryOperation,
841 ) -> Result<Option<RegistryTokenType>> {
842 debug!("Authorizing for image: {:?}", image);
843 let url = format!(
845 "{}://{}/v2/",
846 self.config.protocol.scheme_for(image.resolve_registry()),
847 image.resolve_registry()
848 );
849 debug!(?url);
850
851 if let RegistryAuth::Bearer(token) = authentication {
852 return Ok(Some(RegistryTokenType::Bearer(RegistryToken::Token {
853 token: token.clone(),
854 })));
855 }
856
857 let res = self.client.get(&url).send().await?;
858 let dist_hdr = match res.headers().get(reqwest::header::WWW_AUTHENTICATE) {
859 Some(h) => h,
860 None => return Ok(None),
861 };
862
863 let challenge = match BearerChallenge::try_from(dist_hdr) {
864 Ok(c) => c,
865 Err(e) => {
866 debug!(error = ?e, "Falling back to HTTP Basic Auth");
867 if let RegistryAuth::Basic(username, password) = authentication {
868 return Ok(Some(RegistryTokenType::Basic(
869 username.to_string(),
870 password.to_string(),
871 )));
872 }
873 return Ok(None);
874 }
875 };
876
877 let scope = match operation {
879 RegistryOperation::Pull => format!("repository:{}:pull", image.repository()),
880 RegistryOperation::Push => format!("repository:{}:pull,push", image.repository()),
881 };
882
883 let realm = challenge.realm.as_ref();
884 let service = challenge.service.as_ref();
885 let mut query = vec![("scope", &scope)];
886
887 if let Some(s) = service {
888 query.push(("service", s))
889 }
890
891 debug!(?realm, ?service, ?scope, "Making authentication call");
894
895 let auth_res = self
896 .client
897 .get(realm)
898 .query(&query)
899 .apply_authentication(authentication)
900 .send()
901 .await?;
902
903 match auth_res.status() {
904 reqwest::StatusCode::OK => {
905 let text = auth_res.text().await?;
906 debug!("Received response from auth request");
907 let token: RegistryToken = serde_json::from_str(&text)
908 .map_err(|e| OciDistributionError::RegistryTokenDecodeError(e.to_string()))?;
909 debug!("Successfully authorized for image '{:?}'", image);
910 Ok(Some(RegistryTokenType::Bearer(token)))
911 }
912 _ => {
913 let reason = auth_res.text().await?;
914 debug!("Failed to authenticate for image '{:?}': {}", image, reason);
915 Err(OciDistributionError::AuthenticationFailure(reason))
916 }
917 }
918 }
919
920 pub async fn fetch_manifest_digest(
929 &self,
930 image: &Reference,
931 auth: &RegistryAuth,
932 ) -> Result<String> {
933 self.store_auth_if_needed(image.resolve_registry(), auth)
934 .await;
935
936 let url = self.to_v2_manifest_url(image);
937 debug!("HEAD image manifest from {}", url);
938 let res = RequestBuilderWrapper::from_client(self, |client| client.head(&url))
939 .apply_accept(MIME_TYPES_DISTRIBUTION_MANIFEST)?
940 .apply_auth(image, RegistryOperation::Pull)
941 .await?
942 .into_request_builder()
943 .send()
944 .await?;
945
946 if let Some(digest) = digest_header_value(res.headers().clone())? {
947 let status = res.status();
948 let body = res.bytes().await?;
949 validate_registry_response(status, &body, &url)?;
950
951 if let Some(img_digest) = image.digest() {
954 let header_digest = Digest::new(&digest)?;
955 let image_digest = Digest::new(img_digest)?;
956 if header_digest.algorithm == image_digest.algorithm
957 && header_digest != image_digest
958 {
959 return Err(DigestError::VerificationError {
960 expected: img_digest.to_string(),
961 actual: digest,
962 }
963 .into());
964 }
965 }
966
967 Ok(digest)
968 } else {
969 debug!("GET image manifest from {}", url);
970 let res = RequestBuilderWrapper::from_client(self, |client| client.get(&url))
971 .apply_accept(MIME_TYPES_DISTRIBUTION_MANIFEST)?
972 .apply_auth(image, RegistryOperation::Pull)
973 .await?
974 .into_request_builder()
975 .send()
976 .await?;
977 let status = res.status();
978 trace!(headers = ?res.headers(), "Got Headers");
979 let headers = res.headers().clone();
980 let body = res.bytes().await?;
981 validate_registry_response(status, &body, &url)?;
982
983 validate_digest(&body, digest_header_value(headers)?, image.digest())
984 .map_err(OciDistributionError::from)
985 }
986 }
987
988 async fn validate_layers(
989 &self,
990 manifest: &OciImageManifest,
991 accepted_media_types: Vec<&str>,
992 ) -> Result<()> {
993 if manifest.layers.is_empty() {
994 return Err(OciDistributionError::PullNoLayersError);
995 }
996
997 for layer in &manifest.layers {
998 if !accepted_media_types.iter().any(|i| i.eq(&layer.media_type)) {
999 return Err(OciDistributionError::IncompatibleLayerMediaTypeError(
1000 layer.media_type.clone(),
1001 ));
1002 }
1003 }
1004
1005 Ok(())
1006 }
1007
1008 pub async fn pull_image_manifest(
1019 &self,
1020 image: &Reference,
1021 auth: &RegistryAuth,
1022 ) -> Result<(OciImageManifest, String)> {
1023 self.store_auth_if_needed(image.resolve_registry(), auth)
1024 .await;
1025
1026 self._pull_image_manifest(image).await
1027 }
1028
1029 pub async fn pull_image_manifest_and_list_digest(
1042 &self,
1043 image: &Reference,
1044 auth: &RegistryAuth,
1045 ) -> Result<(OciImageManifest, String, Option<String>)> {
1046 self.store_auth_if_needed(image.resolve_registry(), auth)
1047 .await;
1048
1049 self._pull_image_manifest_and_list_digest(image).await
1050 }
1051
1052 pub async fn pull_manifest_raw(
1060 &self,
1061 image: &Reference,
1062 auth: &RegistryAuth,
1063 accepted_media_types: &[&str],
1064 ) -> Result<(bytes::Bytes, String)> {
1065 self.store_auth_if_needed(image.resolve_registry(), auth)
1066 .await;
1067
1068 self._pull_manifest_raw(image, accepted_media_types).await
1069 }
1070
1071 pub async fn pull_manifest(
1079 &self,
1080 image: &Reference,
1081 auth: &RegistryAuth,
1082 ) -> Result<(OciManifest, String)> {
1083 self.store_auth_if_needed(image.resolve_registry(), auth)
1084 .await;
1085
1086 self._pull_manifest(image).await
1087 }
1088
1089 async fn _pull_image_manifest(&self, image: &Reference) -> Result<(OciImageManifest, String)> {
1097 let (manifest, digest, _list_digest) =
1098 self._pull_image_manifest_and_list_digest(image).await?;
1099 Ok((manifest, digest))
1100 }
1101
1102 async fn _pull_image_manifest_and_list_digest(
1113 &self,
1114 image: &Reference,
1115 ) -> Result<(OciImageManifest, String, Option<String>)> {
1116 let (manifest, digest) = self._pull_manifest(image).await?;
1117 match manifest {
1118 OciManifest::Image(image_manifest) => Ok((image_manifest, digest, None)),
1119 OciManifest::ImageIndex(image_index_manifest) => {
1120 let list_digest = digest;
1121 debug!("Inspecting Image Index Manifest");
1122 let platform_digest = if let Some(resolver) = &self.config.platform_resolver {
1123 resolver(&image_index_manifest.manifests)
1124 } else {
1125 return Err(OciDistributionError::ImageIndexParsingNoPlatformResolverError);
1126 };
1127
1128 match platform_digest {
1129 Some(platform_digest) => {
1130 debug!("Selected manifest entry with digest: {}", platform_digest);
1131 let manifest_entry_reference =
1132 image.clone_with_digest(platform_digest.clone());
1133 self._pull_manifest(&manifest_entry_reference)
1134 .await
1135 .and_then(|(manifest, _digest)| match manifest {
1136 OciManifest::Image(manifest) => {
1137 Ok((manifest, platform_digest, Some(list_digest)))
1138 }
1139 OciManifest::ImageIndex(_) => {
1140 Err(OciDistributionError::ImageManifestNotFoundError(
1141 "received Image Index manifest instead".to_string(),
1142 ))
1143 }
1144 })
1145 }
1146 None => Err(OciDistributionError::ImageManifestNotFoundError(
1147 "no entry found in image index manifest matching client's default platform"
1148 .to_string(),
1149 )),
1150 }
1151 }
1152 }
1153 }
1154
1155 async fn _pull_manifest_raw(
1160 &self,
1161 image: &Reference,
1162 accepted_media_types: &[&str],
1163 ) -> Result<(bytes::Bytes, String)> {
1164 let url = self.to_v2_manifest_url(image);
1165 debug!("Pulling image manifest from {}", url);
1166
1167 let res = RequestBuilderWrapper::from_client(self, |client| client.get(&url))
1168 .apply_accept(accepted_media_types)?
1169 .apply_auth(image, RegistryOperation::Pull)
1170 .await?
1171 .into_request_builder()
1172 .send()
1173 .await?;
1174 let status = res.status();
1175 let headers = res.headers().clone();
1176 let body = res.bytes().await?;
1177
1178 validate_registry_response(status, &body, &url)?;
1179
1180 let digest_header = digest_header_value(headers)?;
1181 let digest = validate_digest(&body, digest_header, image.digest())?;
1182
1183 Ok((body, digest))
1184 }
1185
1186 async fn _pull_manifest(&self, image: &Reference) -> Result<(OciManifest, String)> {
1191 let (body, digest) = self
1192 ._pull_manifest_raw(image, MIME_TYPES_DISTRIBUTION_MANIFEST)
1193 .await?;
1194
1195 self.validate_image_manifest(&body).await?;
1196
1197 debug!("Parsing response as Manifest");
1198 let manifest = serde_json::from_slice(&body)
1199 .map_err(|e| OciDistributionError::ManifestParsingError(e.to_string()))?;
1200 Ok((manifest, digest))
1201 }
1202
1203 async fn validate_image_manifest(&self, body: &[u8]) -> Result<()> {
1204 let versioned: Versioned = serde_json::from_slice(body)
1205 .map_err(|e| OciDistributionError::VersionedParsingError(e.to_string()))?;
1206 debug!(?versioned, "validating manifest");
1207 if versioned.schema_version != 2 {
1208 return Err(OciDistributionError::UnsupportedSchemaVersionError(
1209 versioned.schema_version,
1210 ));
1211 }
1212 if let Some(media_type) = versioned.media_type {
1213 if media_type != IMAGE_MANIFEST_MEDIA_TYPE
1214 && media_type != OCI_IMAGE_MEDIA_TYPE
1215 && media_type != IMAGE_MANIFEST_LIST_MEDIA_TYPE
1216 && media_type != OCI_IMAGE_INDEX_MEDIA_TYPE
1217 {
1218 return Err(OciDistributionError::UnsupportedMediaTypeError(media_type));
1219 }
1220 }
1221
1222 Ok(())
1223 }
1224
1225 pub async fn pull_manifest_and_config(
1234 &self,
1235 image: &Reference,
1236 auth: &RegistryAuth,
1237 ) -> Result<(OciImageManifest, String, String)> {
1238 self.store_auth_if_needed(image.resolve_registry(), auth)
1239 .await;
1240
1241 self._pull_manifest_and_config(image)
1242 .await
1243 .and_then(|(manifest, digest, config)| {
1244 Ok((
1245 manifest,
1246 digest,
1247 String::from_utf8(config.data.into()).map_err(|e| {
1248 OciDistributionError::GenericError(Some(format!(
1249 "Cannot parse config as UTF-8 string: {e}"
1250 )))
1251 })?,
1252 ))
1253 })
1254 }
1255
1256 pub async fn pull_manifest_and_config_and_list_digest(
1269 &self,
1270 image: &Reference,
1271 auth: &RegistryAuth,
1272 ) -> Result<(OciImageManifest, String, String, Option<String>)> {
1273 self.store_auth_if_needed(image.resolve_registry(), auth)
1274 .await;
1275
1276 self._pull_manifest_and_config_and_list_digest(image)
1277 .await
1278 .and_then(|(manifest, digest, config, list_digest)| {
1279 Ok((
1280 manifest,
1281 digest,
1282 String::from_utf8(config.data.into()).map_err(|e| {
1283 OciDistributionError::GenericError(Some(format!(
1284 "Cannot parse config as UTF-8 string: {e}"
1285 )))
1286 })?,
1287 list_digest,
1288 ))
1289 })
1290 }
1291
1292 async fn _pull_manifest_and_config(
1293 &self,
1294 image: &Reference,
1295 ) -> Result<(OciImageManifest, String, Config)> {
1296 let (manifest, digest, config, _list_digest) = self
1297 ._pull_manifest_and_config_and_list_digest(image)
1298 .await?;
1299 Ok((manifest, digest, config))
1300 }
1301
1302 async fn _pull_manifest_and_config_and_list_digest(
1303 &self,
1304 image: &Reference,
1305 ) -> Result<(OciImageManifest, String, Config, Option<String>)> {
1306 let (manifest, digest, list_digest) =
1307 self._pull_image_manifest_and_list_digest(image).await?;
1308
1309 let mut out: Vec<u8> = Vec::new();
1310 debug!("Pulling config layer");
1311 self.pull_blob(image, &manifest.config, &mut out).await?;
1312 let media_type = manifest.config.media_type.clone();
1313 let annotations = manifest.annotations.clone();
1314 Ok((
1315 manifest,
1316 digest,
1317 Config::new(out, media_type, annotations),
1318 list_digest,
1319 ))
1320 }
1321
1322 pub async fn push_manifest_list(
1326 &self,
1327 reference: &Reference,
1328 auth: &RegistryAuth,
1329 manifest: OciImageIndex,
1330 ) -> Result<String> {
1331 self.store_auth_if_needed(reference.resolve_registry(), auth)
1332 .await;
1333 self.push_manifest(reference, &OciManifest::ImageIndex(manifest))
1334 .await
1335 }
1336
1337 pub async fn pull_blob<T: AsyncWrite>(
1345 &self,
1346 image: &Reference,
1347 layer: impl AsLayerDescriptor,
1348 out: T,
1349 ) -> Result<()> {
1350 let response = self.pull_blob_response(image, &layer, None, None).await?;
1351
1352 let mut maybe_header_digester = digest_header_value(response.headers().clone())?
1353 .map(|digest| Digester::new(&digest).map(|d| (d, digest)))
1354 .transpose()?;
1355
1356 let layer_digest = layer.as_layer_descriptor().digest.to_string();
1358 let mut layer_digester = Digester::new(&layer_digest)?;
1359
1360 let status = response.status();
1361 let url = response.url().to_string();
1362 if !status.is_success() {
1363 let body = response.bytes().await?;
1364 return validate_registry_response(status, &body, &url);
1365 }
1366 let mut stream = response.bytes_stream();
1367
1368 let mut out = pin!(out);
1369
1370 while let Some(bytes) = stream.next().await {
1371 let bytes = bytes?;
1372 if let Some((ref mut digester, _)) = maybe_header_digester.as_mut() {
1373 digester.update(&bytes);
1374 }
1375 layer_digester.update(&bytes);
1376 out.write_all(&bytes).await?;
1377 }
1378
1379 out.flush().await?;
1381
1382 if let Some((mut digester, expected)) = maybe_header_digester.take() {
1383 let digest = digester.finalize();
1384
1385 if digest != expected {
1386 return Err(DigestError::VerificationError {
1387 expected,
1388 actual: digest,
1389 }
1390 .into());
1391 }
1392 }
1393
1394 let digest = layer_digester.finalize();
1395 if digest != layer_digest {
1396 return Err(DigestError::VerificationError {
1397 expected: layer_digest,
1398 actual: digest,
1399 }
1400 .into());
1401 }
1402
1403 Ok(())
1404 }
1405
1406 pub async fn pull_blob_stream(
1436 &self,
1437 image: &Reference,
1438 layer: impl AsLayerDescriptor,
1439 ) -> Result<SizedStream> {
1440 stream_from_response(
1441 self.pull_blob_response(image, &layer, None, None).await?,
1442 layer,
1443 true,
1444 )
1445 .await
1446 }
1447
1448 pub async fn pull_blob_stream_partial(
1458 &self,
1459 image: &Reference,
1460 layer: impl AsLayerDescriptor,
1461 offset: u64,
1462 length: Option<u64>,
1463 ) -> Result<BlobResponse> {
1464 let response = self
1465 .pull_blob_response(image, &layer, Some(offset), length)
1466 .await?;
1467
1468 let status = response.status();
1469 match status {
1470 StatusCode::OK => Ok(BlobResponse::Full(
1471 stream_from_response(response, &layer, true).await?,
1472 )),
1473 StatusCode::PARTIAL_CONTENT => Ok(BlobResponse::Partial(
1474 stream_from_response(response, &layer, false).await?,
1475 )),
1476 _ => {
1477 let url = response.url().to_string();
1478 let body = response.bytes().await?;
1479 Err(validate_registry_response(status, &body, &url).expect_err("validate_registry_response should return an error for non-success status codes"))
1480 }
1481 }
1482 }
1483
1484 async fn pull_blob_response(
1486 &self,
1487 image: &Reference,
1488 layer: impl AsLayerDescriptor,
1489 offset: Option<u64>,
1490 length: Option<u64>,
1491 ) -> Result<Response> {
1492 let layer = layer.as_layer_descriptor();
1493 let url = self.to_v2_blob_url(image, layer.digest);
1494
1495 let mut request = RequestBuilderWrapper::from_client(self, |client| client.get(&url))
1496 .apply_accept(MIME_TYPES_DISTRIBUTION_MANIFEST)?
1497 .apply_auth(image, RegistryOperation::Pull)
1498 .await?
1499 .into_request_builder();
1500 if let (Some(off), Some(len)) = (offset, length) {
1501 let end = (off + len).saturating_sub(1);
1502 request = request.header(
1503 RANGE,
1504 HeaderValue::from_str(&format!("bytes={off}-{end}")).unwrap(),
1505 );
1506 } else if let Some(offset) = offset {
1507 request = request.header(
1508 RANGE,
1509 HeaderValue::from_str(&format!("bytes={offset}-")).unwrap(),
1510 );
1511 }
1512 let mut response = request.send().await?;
1513
1514 if let Some(urls) = &layer.urls {
1515 for url in urls {
1516 if response.error_for_status_ref().is_ok() {
1517 break;
1518 }
1519
1520 let url = Url::parse(url)
1521 .map_err(|e| OciDistributionError::UrlParseError(e.to_string()))?;
1522
1523 if url.scheme() == "http" || url.scheme() == "https" {
1524 request =
1528 RequestBuilderWrapper::from_client(self, |client| client.get(url.clone()))
1529 .apply_accept(MIME_TYPES_DISTRIBUTION_MANIFEST)?
1530 .into_request_builder();
1531 if let Some(offset) = offset {
1532 request = request.header(
1533 RANGE,
1534 HeaderValue::from_str(&format!("bytes={offset}-")).unwrap(),
1535 );
1536 }
1537 response = request.send().await?
1538 }
1539 }
1540 }
1541
1542 Ok(response)
1543 }
1544
1545 async fn begin_push_monolithical_session(&self, image: &Reference) -> Result<String> {
1549 let url = &self.to_v2_blob_upload_url(image);
1550 debug!(?url, "begin_push_monolithical_session");
1551 let res = RequestBuilderWrapper::from_client(self, |client| client.post(url))
1552 .apply_auth(image, RegistryOperation::Push)
1553 .await?
1554 .into_request_builder()
1555 .header("Content-Length", 0)
1560 .send()
1561 .await?;
1562
1563 self.extract_location_header(image, res, &reqwest::StatusCode::ACCEPTED)
1565 .await
1566 }
1567
1568 async fn begin_push_chunked_session(&self, image: &Reference) -> Result<String> {
1572 let url = &self.to_v2_blob_upload_url(image);
1573 debug!(?url, "begin_push_session");
1574 let res = RequestBuilderWrapper::from_client(self, |client| client.post(url))
1575 .apply_auth(image, RegistryOperation::Push)
1576 .await?
1577 .into_request_builder()
1578 .header("Content-Length", 0)
1579 .send()
1580 .await?;
1581
1582 self.extract_location_header(image, res, &reqwest::StatusCode::ACCEPTED)
1584 .await
1585 }
1586
1587 async fn end_push_chunked_session(
1591 &self,
1592 location: &str,
1593 image: &Reference,
1594 digest: &str,
1595 ) -> Result<String> {
1596 let url = Url::parse_with_params(location, &[("digest", digest)])
1597 .map_err(|e| OciDistributionError::GenericError(Some(e.to_string())))?;
1598 let res = RequestBuilderWrapper::from_client(self, |client| client.put(url.clone()))
1599 .apply_auth(image, RegistryOperation::Push)
1600 .await?
1601 .into_request_builder()
1602 .header("Content-Length", 0)
1603 .send()
1604 .await?;
1605 self.extract_location_header(image, res, &reqwest::StatusCode::CREATED)
1606 .await
1607 }
1608
1609 async fn push_stream_monolithically(
1613 &self,
1614 location: &str,
1615 image: &Reference,
1616 layer: impl Stream<Item = Result<bytes::Bytes>> + Send + 'static,
1617 size: u64,
1618 blob_digest: &str,
1619 ) -> Result<String> {
1620 let mut url =
1621 Url::parse(location).map_err(|e| OciDistributionError::UrlParseError(e.to_string()))?;
1622 url.query_pairs_mut().append_pair("digest", blob_digest);
1623 let url = url.to_string();
1624
1625 debug!(size, location = ?url, "Pushing monolithically");
1626 let mut headers = HeaderMap::new();
1627 headers.insert(
1628 "Content-Length",
1629 format!("{}", size)
1630 .parse()
1631 .map_err(|e: reqwest::header::InvalidHeaderValue| {
1632 OciDistributionError::GenericError(Some(e.to_string()))
1633 })?,
1634 );
1635 headers.insert("Content-Type", "application/octet-stream".parse().unwrap());
1636
1637 let res = RequestBuilderWrapper::from_client(self, |client| client.put(&url))
1638 .apply_auth(image, RegistryOperation::Push)
1639 .await?
1640 .into_request_builder()
1641 .headers(headers)
1642 .body(reqwest::Body::wrap_stream(layer))
1643 .send()
1644 .await?;
1645
1646 self.extract_location_header(image, res, &reqwest::StatusCode::CREATED)
1648 .await
1649 }
1650
1651 async fn push_monolithically(
1655 &self,
1656 location: &str,
1657 image: &Reference,
1658 layer: impl Into<bytes::Bytes>,
1659 blob_digest: &str,
1660 ) -> Result<String> {
1661 let mut url = Url::parse(location).unwrap();
1662 url.query_pairs_mut().append_pair("digest", blob_digest);
1663 let url = url.to_string();
1664
1665 let layer = layer.into();
1666 debug!(size = layer.len(), location = ?url, "Pushing monolithically");
1667 if layer.is_empty() {
1668 return Err(OciDistributionError::PushNoDataError);
1669 };
1670 let mut headers = HeaderMap::new();
1671 headers.insert(
1672 "Content-Length",
1673 format!("{}", layer.len()).parse().unwrap(),
1674 );
1675 headers.insert("Content-Type", "application/octet-stream".parse().unwrap());
1676
1677 let res = RequestBuilderWrapper::from_client(self, |client| client.put(&url))
1678 .apply_auth(image, RegistryOperation::Push)
1679 .await?
1680 .into_request_builder()
1681 .headers(headers)
1682 .body(layer)
1683 .send()
1684 .await?;
1685
1686 self.extract_location_header(image, res, &reqwest::StatusCode::CREATED)
1688 .await
1689 }
1690
1691 async fn push_chunk(
1696 &self,
1697 location: &str,
1698 image: &Reference,
1699 blob_chunk: bytes::Bytes,
1700 range_start: usize,
1701 ) -> Result<(String, usize)> {
1702 if blob_chunk.is_empty() {
1703 return Err(OciDistributionError::PushNoDataError);
1704 };
1705
1706 let chunk_size = blob_chunk.len();
1707 let end_range_inclusive = range_start + chunk_size - 1;
1708
1709 let mut headers = HeaderMap::new();
1710 headers.insert(
1711 "Content-Range",
1712 format!("{range_start}-{end_range_inclusive}")
1713 .parse()
1714 .unwrap(),
1715 );
1716
1717 headers.insert("Content-Length", format!("{chunk_size}").parse().unwrap());
1718 headers.insert("Content-Type", "application/octet-stream".parse().unwrap());
1719
1720 debug!(
1721 ?range_start,
1722 ?end_range_inclusive,
1723 chunk_size,
1724 ?location,
1725 ?headers,
1726 "Pushing chunk"
1727 );
1728
1729 let res = RequestBuilderWrapper::from_client(self, |client| client.patch(location))
1730 .apply_auth(image, RegistryOperation::Push)
1731 .await?
1732 .into_request_builder()
1733 .headers(headers)
1734 .body(blob_chunk)
1735 .send()
1736 .await?;
1737
1738 Ok((
1740 self.extract_location_header(image, res, &reqwest::StatusCode::ACCEPTED)
1741 .await?,
1742 end_range_inclusive + 1,
1743 ))
1744 }
1745
1746 async fn push_stream_chunks(
1754 &self,
1755 location: String,
1756 image: &Reference,
1757 blob_data_stream: impl Stream<Item = Result<bytes::Bytes>> + Send + 'static,
1758 mut on_chunk: impl FnMut(&bytes::Bytes),
1759 ) -> Result<(String, u64)> {
1760 let mut location = location;
1761 let mut range_start = 0;
1762 let mut size = 0u64;
1763
1764 let mut blob_data_stream = pin!(blob_data_stream);
1765
1766 while let Some(blob_data) = blob_data_stream.next().await {
1767 let mut blob_data = blob_data?;
1768 while !blob_data.is_empty() {
1769 let chunk = blob_data.split_to(self.push_chunk_size.min(blob_data.len()));
1770 size += chunk.len() as u64;
1771 on_chunk(&chunk);
1772 (location, range_start) = self
1773 .push_chunk(&location, image, chunk, range_start)
1774 .await?;
1775 }
1776 }
1777
1778 Ok((location, size))
1779 }
1780
1781 pub async fn mount_blob(
1783 &self,
1784 image: &Reference,
1785 source: &Reference,
1786 digest: &str,
1787 ) -> Result<()> {
1788 let base_url = self.to_v2_blob_upload_url(image);
1789 let url = Url::parse_with_params(
1790 &base_url,
1791 &[("mount", digest), ("from", source.repository())],
1792 )
1793 .map_err(|e| OciDistributionError::UrlParseError(e.to_string()))?;
1794
1795 let res = RequestBuilderWrapper::from_client(self, |client| client.post(url.clone()))
1796 .apply_auth(image, RegistryOperation::Push)
1797 .await?
1798 .into_request_builder()
1799 .send()
1800 .await?;
1801
1802 self.extract_location_header(image, res, &reqwest::StatusCode::CREATED)
1803 .await?;
1804
1805 Ok(())
1806 }
1807
1808 pub async fn push_manifest(&self, image: &Reference, manifest: &OciManifest) -> Result<String> {
1812 let mut headers = HeaderMap::new();
1813 let content_type = manifest.content_type();
1814 headers.insert("Content-Type", content_type.parse().unwrap());
1815
1816 let mut body = Vec::new();
1819 let mut ser = serde_json::Serializer::with_formatter(&mut body, CanonicalFormatter::new());
1820 manifest.serialize(&mut ser).unwrap();
1821
1822 self.push_manifest_raw(image, body, manifest.content_type().parse().unwrap())
1823 .await
1824 }
1825
1826 pub async fn push_manifest_raw(
1830 &self,
1831 image: &Reference,
1832 body: impl Into<bytes::Bytes>,
1833 content_type: HeaderValue,
1834 ) -> Result<String> {
1835 let url = self.to_v2_manifest_url(image);
1836 debug!(?url, ?content_type, "push manifest");
1837
1838 let mut headers = HeaderMap::new();
1839 headers.insert("Content-Type", content_type);
1840
1841 let body = body.into();
1842
1843 let manifest_hash = sha256_digest(&body);
1847
1848 let res = RequestBuilderWrapper::from_client(self, |client| client.put(url.clone()))
1849 .apply_auth(image, RegistryOperation::Push)
1850 .await?
1851 .into_request_builder()
1852 .headers(headers)
1853 .body(body)
1854 .send()
1855 .await?;
1856
1857 let ret = self
1858 .extract_location_header(image, res, &reqwest::StatusCode::CREATED)
1859 .await;
1860
1861 if matches!(ret, Err(OciDistributionError::RegistryNoLocationError)) {
1862 warn!("Registry is not respecting the OCI Distribution Specification: it didn't return the Location of the uploaded Manifest inside of the response headers. Working around this issue...");
1870
1871 let url_base = url
1872 .strip_suffix(image.tag().unwrap_or("latest"))
1873 .expect("The manifest URL always ends with the image tag suffix");
1874 let url_by_digest = format!("{url_base}{manifest_hash}");
1875
1876 return Ok(url_by_digest);
1877 }
1878
1879 ret
1880 }
1881
1882 pub async fn pull_referrers(
1904 &self,
1905 image: &Reference,
1906 artifact_type: Option<&str>,
1907 ) -> Result<OciImageIndex> {
1908 let url = self.to_v2_referrers_url(image, artifact_type)?;
1909 debug!("Pulling referrers from {}", url);
1910
1911 let res = RequestBuilderWrapper::from_client(self, |client| client.get(&url))
1912 .apply_accept(MIME_TYPES_DISTRIBUTION_MANIFEST)?
1913 .apply_auth(image, RegistryOperation::Pull)
1914 .await?
1915 .into_request_builder()
1916 .send()
1917 .await?;
1918 let status = res.status();
1919 let body = res.bytes().await?;
1920
1921 if status == reqwest::StatusCode::NOT_FOUND {
1924 debug!(
1925 url = %url,
1926 "Native referrers API returned 404; falling back to OCI referrers tag schema"
1927 );
1928 return self
1929 .pull_referrers_via_tag_schema(image, artifact_type)
1930 .await;
1931 }
1932
1933 validate_registry_response(status, &body, &url)?;
1934 let manifest = serde_json::from_slice(&body)
1935 .map_err(|e| OciDistributionError::ManifestParsingError(e.to_string()))?;
1936
1937 Ok(manifest)
1938 }
1939
1940 async fn pull_referrers_via_tag_schema(
1951 &self,
1952 image: &Reference,
1953 artifact_type: Option<&str>,
1954 ) -> Result<OciImageIndex> {
1955 let digest = image.digest().ok_or_else(|| {
1956 OciDistributionError::GenericError(Some(
1957 "Getting referrers for a tag is not supported".into(),
1958 ))
1959 })?;
1960
1961 let fallback_tag = digest.replace(':', "-");
1962 let fallback_ref = Reference::with_tag(
1963 image.resolve_registry().to_string(),
1964 image.repository().to_string(),
1965 fallback_tag.clone(),
1966 );
1967
1968 debug!(
1969 tag = %fallback_tag,
1970 "Pulling referrers via tag schema"
1971 );
1972
1973 let manifest = match self._pull_manifest(&fallback_ref).await {
1974 Ok((manifest, _digest)) => manifest,
1975 Err(e) => match &e {
1976 OciDistributionError::ImageManifestNotFoundError(_)
1977 | OciDistributionError::RegistryError { .. }
1978 | OciDistributionError::ServerError { code: 404, .. } => {
1979 debug!(
1980 error = ?e,
1981 "Referrers tag schema not found; assuming no referrers"
1982 );
1983 return Ok(empty_image_index());
1984 }
1985 _ => return Err(e),
1986 },
1987 };
1988
1989 let mut index = match manifest {
1990 OciManifest::ImageIndex(idx) => idx,
1991 OciManifest::Image(_) => {
1992 return Err(OciDistributionError::SpecViolationError(format!(
1993 "referrers tag schema: tag '{fallback_tag}' contains an Image manifest; \
1994 expected an OCI Image Index"
1995 )));
1996 }
1997 };
1998
1999 if let Some(at) = artifact_type {
2002 index.manifests.retain(|entry| {
2003 entry
2004 .artifact_type
2005 .as_deref()
2006 .map(|t| t == at)
2007 .unwrap_or(false)
2008 });
2009 }
2010
2011 Ok(index)
2012 }
2013
2014 pub async fn catalog(
2020 &self,
2021 image: &Reference,
2022 auth: &RegistryAuth,
2023 n: Option<usize>,
2024 last: Option<&str>,
2025 ) -> Result<CatalogResponse> {
2026 let op = RegistryOperation::Pull;
2027 let url = self.to_catalog_url(image);
2028
2029 self.store_auth_if_needed(image.resolve_registry(), auth)
2030 .await;
2031
2032 let request = self.client.get(&url);
2033 let request = if let Some(num) = n {
2034 request.query(&[("n", num)])
2035 } else {
2036 request
2037 };
2038 let request = if let Some(l) = last {
2039 request.query(&[("last", l)])
2040 } else {
2041 request
2042 };
2043 let request = RequestBuilderWrapper {
2044 client: self,
2045 request_builder: request,
2046 };
2047 let res = request
2048 .apply_auth(image, op)
2049 .await?
2050 .into_request_builder()
2051 .send()
2052 .await?;
2053 let status = res.status();
2054 let body = res.bytes().await?;
2055
2056 validate_registry_response(status, &body, &url)?;
2057
2058 Ok(serde_json::from_str(std::str::from_utf8(&body)?)?)
2059 }
2060
2061 async fn extract_location_header(
2062 &self,
2063 image: &Reference,
2064 res: reqwest::Response,
2065 expected_status: &reqwest::StatusCode,
2066 ) -> Result<String> {
2067 debug!(expected_status_code=?expected_status.as_u16(),
2068 status_code=?res.status().as_u16(),
2069 "extract location header");
2070 if res.status().eq(expected_status) {
2071 let location_header = res.headers().get("Location");
2072 debug!(location=?location_header, "Location header");
2073 match location_header {
2074 None => Err(OciDistributionError::RegistryNoLocationError),
2075 Some(lh) => self.location_header_to_url(image, lh),
2076 }
2077 } else if res.status().is_success() && expected_status.is_success() {
2078 Err(OciDistributionError::SpecViolationError(format!(
2079 "Expected HTTP Status {}, got {} instead",
2080 expected_status,
2081 res.status(),
2082 )))
2083 } else {
2084 let url = res.url().to_string();
2085 let code = res.status().as_u16();
2086 let message = res.text().await?;
2087 Err(OciDistributionError::ServerError { url, code, message })
2088 }
2089 }
2090
2091 fn location_header_to_url(
2100 &self,
2101 image: &Reference,
2102 location_header: &reqwest::header::HeaderValue,
2103 ) -> Result<String> {
2104 let lh = location_header.to_str()?;
2105 if lh.starts_with("/") {
2106 let registry = image.resolve_registry();
2107 Ok(format!(
2108 "{scheme}://{registry}{lh}",
2109 scheme = self.config.protocol.scheme_for(registry)
2110 ))
2111 } else {
2112 Ok(lh.to_string())
2113 }
2114 }
2115
2116 fn to_v2_manifest_url(&self, reference: &Reference) -> String {
2118 let registry = reference.resolve_registry();
2119 format!(
2120 "{scheme}://{registry}/v2/{repository}/manifests/{reference}{ns}",
2121 scheme = self.config.protocol.scheme_for(registry),
2122 repository = reference.repository(),
2123 reference = if let Some(digest) = reference.digest() {
2124 digest
2125 } else {
2126 reference.tag().unwrap_or("latest")
2127 },
2128 ns = reference
2129 .namespace()
2130 .map(|ns| format!("?ns={ns}"))
2131 .unwrap_or_default(),
2132 )
2133 }
2134
2135 fn to_v2_blob_url(&self, reference: &Reference, digest: &str) -> String {
2137 let registry = reference.resolve_registry();
2138 format!(
2139 "{scheme}://{registry}/v2/{repository}/blobs/{digest}{ns}",
2140 scheme = self.config.protocol.scheme_for(registry),
2141 repository = reference.repository(),
2142 ns = reference
2143 .namespace()
2144 .map(|ns| format!("?ns={ns}"))
2145 .unwrap_or_default(),
2146 )
2147 }
2148
2149 fn to_v2_blob_upload_url(&self, reference: &Reference) -> String {
2151 self.to_v2_blob_url(reference, "uploads/")
2152 }
2153
2154 fn to_list_tags_url(&self, reference: &Reference) -> String {
2155 let registry = reference.resolve_registry();
2156 format!(
2157 "{scheme}://{registry}/v2/{repository}/tags/list{ns}",
2158 scheme = self.config.protocol.scheme_for(registry),
2159 repository = reference.repository(),
2160 ns = reference
2161 .namespace()
2162 .map(|ns| format!("?ns={ns}"))
2163 .unwrap_or_default(),
2164 )
2165 }
2166
2167 fn to_catalog_url(&self, reference: &Reference) -> String {
2168 let registry = reference.resolve_registry();
2169 format!(
2170 "{scheme}://{registry}/v2/_catalog",
2171 scheme = self.config.protocol.scheme_for(registry),
2172 )
2173 }
2174
2175 fn to_v2_referrers_url(
2177 &self,
2178 reference: &Reference,
2179 artifact_type: Option<&str>,
2180 ) -> Result<String> {
2181 let digest = reference.digest().ok_or_else(|| {
2182 OciDistributionError::GenericError(Some(
2183 "Getting referrers for a tag is not supported".into(),
2184 ))
2185 })?;
2186
2187 let registry = reference.resolve_registry();
2188 let base = format!(
2189 "{scheme}://{registry}",
2190 scheme = self.config.protocol.scheme_for(registry),
2191 );
2192 let mut url =
2193 Url::parse(&base).map_err(|e| OciDistributionError::UrlParseError(e.to_string()))?;
2194 url.path_segments_mut()
2195 .map_err(|_| {
2196 OciDistributionError::GenericError(Some(
2197 "cannot build referrers URL: base URL is cannot-be-a-base".into(),
2198 ))
2199 })?
2200 .push("v2")
2201 .extend(reference.repository().split('/'))
2202 .push("referrers")
2203 .push(digest);
2204 if let Some(at) = artifact_type {
2205 url.query_pairs_mut().append_pair("artifactType", at);
2206 }
2207 Ok(url.into())
2208 }
2209}
2210
2211fn validate_registry_response(status: reqwest::StatusCode, body: &[u8], url: &str) -> Result<()> {
2215 match status {
2216 reqwest::StatusCode::OK => Ok(()),
2217 reqwest::StatusCode::UNAUTHORIZED => Err(OciDistributionError::UnauthorizedError {
2218 url: url.to_string(),
2219 }),
2220 s if s.is_success() => Err(OciDistributionError::SpecViolationError(format!(
2221 "Expected HTTP Status {}, got {} instead",
2222 reqwest::StatusCode::OK,
2223 status,
2224 ))),
2225 s if s.is_client_error() => {
2226 match serde_json::from_slice::<OciEnvelope>(body) {
2227 Ok(envelope) => Err(OciDistributionError::RegistryError {
2229 envelope,
2230 url: url.to_string(),
2231 }),
2232 Err(_) => Err(OciDistributionError::ServerError {
2234 code: s.as_u16(),
2235 url: url.to_string(),
2236 message: String::from_utf8_lossy(body).to_string(),
2237 }),
2238 }
2239 }
2240 s => Err(OciDistributionError::ServerError {
2243 code: s.as_u16(),
2244 url: url.to_string(),
2245 message: String::from_utf8_lossy(body).to_string(),
2246 }),
2247 }
2248}
2249
2250fn empty_image_index() -> OciImageIndex {
2252 OciImageIndex {
2253 schema_version: 2,
2254 media_type: Some(crate::manifest::OCI_IMAGE_INDEX_MEDIA_TYPE.to_string()),
2255 artifact_type: None,
2256 annotations: None,
2257 manifests: vec![],
2258 }
2259}
2260
2261async fn stream_from_response(
2263 response: Response,
2264 layer: impl AsLayerDescriptor,
2265 verify: bool,
2266) -> Result<SizedStream> {
2267 let status = response.status();
2268 let url = response.url().to_string();
2269 let content_length = response.content_length();
2270 let headers = response.headers().clone();
2271 if !status.is_success() {
2272 let body = response.bytes().await?;
2273 return Err(validate_registry_response(status, &body, &url).expect_err(
2274 "validate_registry_response should return an error for non-success status codes",
2275 ));
2276 }
2277 let stream = response.bytes_stream().map_err(std::io::Error::other);
2278
2279 let expected_layer_digest = layer.as_layer_descriptor().digest.to_string();
2280 let layer_digester = Digester::new(&expected_layer_digest)?;
2281 let header_digester_and_digest = match digest_header_value(headers)? {
2282 Some(digest) if digest == expected_layer_digest => None,
2284 Some(digest) => Some((Digester::new(&digest)?, digest)),
2285 None => None,
2286 };
2287 let header_digest = header_digester_and_digest
2288 .as_ref()
2289 .map(|(_, digest)| digest.to_owned());
2290 let stream: BoxStream<'static, std::result::Result<bytes::Bytes, std::io::Error>> = if verify {
2291 Box::pin(VerifyingStream::new(
2292 Box::pin(stream),
2293 layer_digester,
2294 expected_layer_digest,
2295 header_digester_and_digest,
2296 ))
2297 } else {
2298 Box::pin(stream)
2299 };
2300 Ok(SizedStream {
2301 content_length,
2302 digest_header_value: header_digest,
2303 stream,
2304 })
2305}
2306
2307struct RequestBuilderWrapper<'a> {
2311 client: &'a Client,
2312 request_builder: RequestBuilder,
2313}
2314
2315impl<'a> RequestBuilderWrapper<'a> {
2317 fn from_client(
2321 client: &'a Client,
2322 f: impl Fn(&reqwest::Client) -> RequestBuilder,
2323 ) -> RequestBuilderWrapper<'a> {
2324 let request_builder = f(&client.client);
2325 RequestBuilderWrapper {
2326 client,
2327 request_builder,
2328 }
2329 }
2330
2331 fn into_request_builder(self) -> RequestBuilder {
2333 self.request_builder
2334 }
2335}
2336
2337impl<'a> RequestBuilderWrapper<'a> {
2339 fn cloned_request_builder(&self) -> Result<RequestBuilder> {
2344 self.request_builder.try_clone().ok_or_else(|| {
2345 OciDistributionError::GenericError(Some("could not clone request builder".to_string()))
2346 })
2347 }
2348
2349 fn apply_accept(&self, accept: &[&str]) -> Result<RequestBuilderWrapper<'_>> {
2350 let request_builder = self
2351 .cloned_request_builder()?
2352 .header("Accept", Vec::from(accept).join(", "));
2353
2354 Ok(RequestBuilderWrapper {
2355 client: self.client,
2356 request_builder,
2357 })
2358 }
2359
2360 fn targets_credential_registry(&self, image: &Reference) -> Result<bool> {
2373 let request = self.cloned_request_builder()?.build()?;
2374 let target = request.url();
2375
2376 let registry = image.resolve_registry();
2377 let registry_url = Url::parse(&format!(
2378 "{scheme}://{registry}",
2379 scheme = self.client.config.protocol.scheme_for(registry)
2380 ))
2381 .map_err(|e| OciDistributionError::UrlParseError(e.to_string()))?;
2382
2383 if target.host_str() != registry_url.host_str() {
2384 return Ok(false);
2385 }
2386 if target.port_or_known_default() != registry_url.port_or_known_default() {
2387 return Ok(false);
2388 }
2389 if registry_url.scheme() == "https" && target.scheme() != "https" {
2392 return Ok(false);
2393 }
2394
2395 Ok(true)
2396 }
2397
2398 async fn apply_auth(
2409 &self,
2410 image: &Reference,
2411 op: RegistryOperation,
2412 ) -> Result<RequestBuilderWrapper<'_>> {
2413 if !self.targets_credential_registry(image)? {
2417 debug!(
2418 registry = image.resolve_registry(),
2419 "Not authenticating a request addressed outside of the registry"
2420 );
2421 return Ok(RequestBuilderWrapper {
2422 client: self.client,
2423 request_builder: self.cloned_request_builder()?,
2424 });
2425 }
2426
2427 let mut headers = HeaderMap::new();
2428 if let Some(token) = self.client.get_auth_token(image, op).await {
2429 match token {
2430 RegistryTokenType::Bearer(token) => {
2431 debug!("Using bearer token authentication.");
2432 headers.insert("Authorization", token.bearer_token().parse().unwrap());
2433 }
2434 RegistryTokenType::Basic(username, password) => {
2435 debug!("Using HTTP basic authentication.");
2436 return Ok(RequestBuilderWrapper {
2437 client: self.client,
2438 request_builder: self
2439 .cloned_request_builder()?
2440 .headers(headers)
2441 .basic_auth(username.to_string(), Some(password.to_string())),
2442 });
2443 }
2444 }
2445 }
2446 Ok(RequestBuilderWrapper {
2447 client: self.client,
2448 request_builder: self.cloned_request_builder()?.headers(headers),
2449 })
2450 }
2451}
2452
2453#[derive(Debug, Clone)]
2455pub enum CertificateEncoding {
2456 #[allow(missing_docs)]
2457 Der,
2458 #[allow(missing_docs)]
2459 Pem,
2460}
2461
2462#[derive(Debug, Clone)]
2464pub struct Certificate {
2465 pub encoding: CertificateEncoding,
2467
2468 pub data: Vec<u8>,
2470}
2471
2472impl TryFrom<&Certificate> for reqwest::Certificate {
2473 type Error = OciDistributionError;
2474
2475 fn try_from(cert: &Certificate) -> Result<Self> {
2476 match cert.encoding {
2477 CertificateEncoding::Der => Ok(reqwest::Certificate::from_der(cert.data.as_slice())?),
2478 CertificateEncoding::Pem => Ok(reqwest::Certificate::from_pem(cert.data.as_slice())?),
2479 }
2480 }
2481}
2482
2483fn convert_certificates(certs: &[Certificate]) -> Result<Vec<reqwest::Certificate>> {
2484 certs.iter().map(reqwest::Certificate::try_from).collect()
2485}
2486
2487pub struct ClientConfig {
2489 pub protocol: ClientProtocol,
2491
2492 #[cfg(feature = "native-tls")]
2494 pub accept_invalid_hostnames: bool,
2495
2496 pub accept_invalid_certificates: bool,
2498
2499 pub use_monolithic_push: bool,
2501
2502 pub tls_certs_only: Vec<Certificate>,
2507
2508 pub extra_root_certificates: Vec<Certificate>,
2511
2512 pub platform_resolver: Option<Box<PlatformResolverFn>>,
2520
2521 pub max_concurrent_upload: usize,
2526
2527 pub max_concurrent_download: usize,
2532
2533 pub default_token_expiration_secs: usize,
2538
2539 pub read_timeout: Option<Duration>,
2543
2544 pub connect_timeout: Option<Duration>,
2548
2549 pub user_agent: &'static str,
2553
2554 pub https_proxy: Option<String>,
2558
2559 pub http_proxy: Option<String>,
2563
2564 pub no_proxy: Option<String>,
2568}
2569
2570impl Default for ClientConfig {
2571 fn default() -> Self {
2572 Self {
2573 protocol: ClientProtocol::default(),
2574 #[cfg(feature = "native-tls")]
2575 accept_invalid_hostnames: false,
2576 accept_invalid_certificates: false,
2577 use_monolithic_push: false,
2578 tls_certs_only: Vec::new(),
2579 extra_root_certificates: Vec::new(),
2580 platform_resolver: Some(Box::new(current_platform_resolver)),
2581 max_concurrent_upload: DEFAULT_MAX_CONCURRENT_UPLOAD,
2582 max_concurrent_download: DEFAULT_MAX_CONCURRENT_DOWNLOAD,
2583 default_token_expiration_secs: DEFAULT_TOKEN_EXPIRATION_SECS,
2584 read_timeout: None,
2585 connect_timeout: None,
2586 user_agent: DEFAULT_USER_AGENT,
2587 https_proxy: None,
2588 http_proxy: None,
2589 no_proxy: None,
2590 }
2591 }
2592}
2593
2594type PlatformResolverFn = dyn Fn(&[ImageIndexEntry]) -> Option<String> + Send + Sync;
2598
2599pub fn linux_amd64_resolver(manifests: &[ImageIndexEntry]) -> Option<String> {
2601 manifests
2602 .iter()
2603 .find(|entry| {
2604 entry.platform.as_ref().is_some_and(|platform| {
2605 platform.os == Os::Linux && platform.architecture == Arch::Amd64
2606 })
2607 })
2608 .map(|entry| entry.digest.clone())
2609}
2610
2611pub fn windows_amd64_resolver(manifests: &[ImageIndexEntry]) -> Option<String> {
2613 manifests
2614 .iter()
2615 .find(|entry| {
2616 entry.platform.as_ref().is_some_and(|platform| {
2617 platform.os == Os::Windows && platform.architecture == Arch::Amd64
2618 })
2619 })
2620 .map(|entry| entry.digest.clone())
2621}
2622
2623pub fn current_platform_resolver(manifests: &[ImageIndexEntry]) -> Option<String> {
2626 manifests
2627 .iter()
2628 .find(|entry| {
2629 entry.platform.as_ref().is_some_and(|platform| {
2630 platform.os == Os::default() && platform.architecture == Arch::default()
2631 })
2632 })
2633 .map(|entry| entry.digest.clone())
2634}
2635
2636#[derive(Debug, Clone, PartialEq, Eq, Default)]
2638pub enum ClientProtocol {
2639 #[allow(missing_docs)]
2640 Http,
2641 #[allow(missing_docs)]
2642 #[default]
2643 Https,
2644 #[allow(missing_docs)]
2645 HttpsExcept(Vec<String>),
2646}
2647
2648impl ClientProtocol {
2649 fn scheme_for(&self, registry: &str) -> &str {
2650 match self {
2651 ClientProtocol::Https => "https",
2652 ClientProtocol::Http => "http",
2653 ClientProtocol::HttpsExcept(exceptions) => {
2654 if exceptions.contains(®istry.to_owned()) {
2655 "http"
2656 } else {
2657 "https"
2658 }
2659 }
2660 }
2661 }
2662}
2663
2664#[derive(Clone, Debug)]
2665struct BearerChallenge {
2666 pub realm: Box<str>,
2667 pub service: Option<String>,
2668}
2669
2670impl TryFrom<&HeaderValue> for BearerChallenge {
2671 type Error = String;
2672
2673 fn try_from(value: &HeaderValue) -> std::result::Result<Self, Self::Error> {
2674 let parser = ChallengeParser::new(
2675 value
2676 .to_str()
2677 .map_err(|e| format!("cannot convert header value to string: {e:?}"))?,
2678 );
2679 parser
2680 .filter_map(|parser_res| {
2681 if let Ok(chalenge_ref) = parser_res {
2682 let bearer_challenge = BearerChallenge::try_from(&chalenge_ref);
2683 bearer_challenge.ok()
2684 } else {
2685 None
2686 }
2687 })
2688 .next()
2689 .ok_or_else(|| "Cannot find Bearer challenge".to_string())
2690 }
2691}
2692
2693impl TryFrom<&ChallengeRef<'_>> for BearerChallenge {
2694 type Error = String;
2695
2696 fn try_from(value: &ChallengeRef<'_>) -> std::result::Result<Self, Self::Error> {
2697 if !value.scheme.eq_ignore_ascii_case("Bearer") {
2698 return Err(format!(
2699 "BearerChallenge doesn't support challenge scheme {:?}",
2700 value.scheme
2701 ));
2702 }
2703 let mut realm = None;
2704 let mut service = None;
2705 for (k, v) in &value.params {
2706 if k.eq_ignore_ascii_case("realm") {
2707 realm = Some(v.to_unescaped());
2708 }
2709
2710 if k.eq_ignore_ascii_case("service") {
2711 service = Some(v.to_unescaped());
2712 }
2713 }
2714
2715 let realm = realm.ok_or("missing required parameter realm")?;
2716
2717 Ok(BearerChallenge {
2718 realm: realm.into_boxed_str(),
2719 service,
2720 })
2721 }
2722}
2723
2724#[cfg(test)]
2725mod test {
2726 use super::*;
2727 use std::convert::TryFrom;
2728 use std::fs;
2729 use std::path;
2730 use std::result::Result;
2731
2732 use bytes::Bytes;
2733 use rstest::rstest;
2734 use tempfile::TempDir;
2735 use tokio::io::AsyncReadExt;
2736 use tokio_util::io::StreamReader;
2737
2738 use crate::errors::OciErrorCode;
2739 use crate::manifest::{self, IMAGE_DOCKER_LAYER_GZIP_MEDIA_TYPE};
2740
2741 #[test]
2742 fn test_validate_registry_response_server_error_non_utf8_body() {
2743 let err = validate_registry_response(
2744 reqwest::StatusCode::BAD_GATEWAY,
2745 &[0xff, 0xfe, b'o', b'k'],
2746 "https://example.com",
2747 )
2748 .expect_err("5xx should be an error");
2749 match err {
2750 OciDistributionError::ServerError { code, message, .. } => {
2751 assert_eq!(502, code);
2752 assert_eq!("\u{fffd}\u{fffd}ok", message);
2753 }
2754 e => panic!("expected ServerError, got {e:?}"),
2755 }
2756 }
2757
2758 #[test]
2759 fn test_validate_registry_response_unknown_error_code() {
2760 let err = validate_registry_response(
2761 reqwest::StatusCode::CONFLICT,
2762 br#"{"errors":[{"code":"ARTIFACT_LOCKED","message":"artifact is locked"}]}"#,
2763 "https://example.com",
2764 )
2765 .expect_err("4xx should be an error");
2766 match err {
2767 OciDistributionError::RegistryError { envelope, .. } => {
2768 assert_eq!(
2769 OciErrorCode::Other("ARTIFACT_LOCKED".to_string()),
2770 envelope.errors[0].code
2771 );
2772 assert_eq!("artifact is locked", envelope.errors[0].message);
2773 }
2774 e => panic!("expected RegistryError, got {e:?}"),
2775 }
2776 }
2777
2778 #[cfg(feature = "test-registry")]
2779 use testcontainers::{
2780 core::{Mount, WaitFor},
2781 runners::AsyncRunner,
2782 ContainerRequest, GenericImage, ImageExt,
2783 };
2784
2785 const HELLO_IMAGE_NO_TAG: &str = "webassembly.azurecr.io/hello-wasm";
2786 const HELLO_IMAGE_TAG: &str = "webassembly.azurecr.io/hello-wasm:v1";
2787 const HELLO_IMAGE_DIGEST: &str = "webassembly.azurecr.io/hello-wasm@sha256:51d9b231d5129e3ffc267c9d455c49d789bf3167b611a07ab6e4b3304c96b0e7";
2788 const HELLO_IMAGE_TAG_AND_DIGEST: &str = "webassembly.azurecr.io/hello-wasm:v1@sha256:51d9b231d5129e3ffc267c9d455c49d789bf3167b611a07ab6e4b3304c96b0e7";
2789 const TEST_IMAGES: &[&str] = &[
2790 HELLO_IMAGE_TAG,
2795 HELLO_IMAGE_DIGEST,
2796 HELLO_IMAGE_TAG_AND_DIGEST,
2797 ];
2798 const GHCR_IO_IMAGE: &str = "ghcr.io/krustlet/oci-distribution/hello-wasm:v1";
2799 const DOCKER_IO_IMAGE: &str = "docker.io/library/hello-world@sha256:37a0b92b08d4919615c3ee023f7ddb068d12b8387475d64c622ac30f45c29c51";
2800 const HTPASSWD: &str = "testuser:$2y$05$8/q2bfRcX74EuxGf0qOcSuhWDQJXrgWiy6Fi73/JM2tKC66qSrLve";
2801 const HTPASSWD_USERNAME: &str = "testuser";
2802 const HTPASSWD_PASSWORD: &str = "testpassword";
2803
2804 const EMPTY_JSON_BLOB: &str = "{}";
2805 const EMPTY_JSON_DIGEST: &str =
2806 "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a";
2807
2808 #[test]
2809 fn test_apply_accept() -> anyhow::Result<()> {
2810 assert_eq!(
2811 RequestBuilderWrapper::from_client(&Client::default(), |client| client
2812 .get("https://example.com/some/module.wasm"))
2813 .apply_accept(&["*/*"])?
2814 .into_request_builder()
2815 .build()?
2816 .headers()["Accept"],
2817 "*/*"
2818 );
2819
2820 assert_eq!(
2821 RequestBuilderWrapper::from_client(&Client::default(), |client| client
2822 .get("https://example.com/some/module.wasm"))
2823 .apply_accept(MIME_TYPES_DISTRIBUTION_MANIFEST)?
2824 .into_request_builder()
2825 .build()?
2826 .headers()["Accept"],
2827 MIME_TYPES_DISTRIBUTION_MANIFEST.join(", ")
2828 );
2829
2830 Ok(())
2831 }
2832
2833 #[tokio::test]
2834 async fn test_apply_auth_no_token() -> anyhow::Result<()> {
2835 assert!(
2836 !RequestBuilderWrapper::from_client(&Client::default(), |client| client
2837 .get("https://example.com/some/module.wasm"))
2838 .apply_auth(
2839 &Reference::try_from(HELLO_IMAGE_TAG)?,
2840 RegistryOperation::Pull
2841 )
2842 .await?
2843 .into_request_builder()
2844 .build()?
2845 .headers()
2846 .contains_key("Authorization")
2847 );
2848
2849 Ok(())
2850 }
2851
2852 #[tokio::test]
2853 async fn test_apply_auth_bearer_token() -> anyhow::Result<()> {
2854 let _ = tracing_subscriber::fmt::try_init();
2855 let client = Client::default();
2856 let token = "e30.e30.signature".to_string();
2858
2859 client
2861 .store_auth(
2862 Reference::try_from(HELLO_IMAGE_TAG)?.resolve_registry(),
2863 RegistryAuth::Anonymous,
2864 )
2865 .await;
2866
2867 client
2868 .tokens
2869 .insert(
2870 &Reference::try_from(HELLO_IMAGE_TAG)?,
2871 RegistryOperation::Pull,
2872 RegistryTokenType::Bearer(RegistryToken::Token {
2873 token: token.clone(),
2874 }),
2875 )
2876 .await;
2877
2878 assert_eq!(
2879 RequestBuilderWrapper::from_client(&client, |client| client
2880 .get("https://webassembly.azurecr.io/v2/hello-wasm/blobs/sha256:deadbeef"))
2881 .apply_auth(
2882 &Reference::try_from(HELLO_IMAGE_TAG)?,
2883 RegistryOperation::Pull
2884 )
2885 .await?
2886 .into_request_builder()
2887 .build()?
2888 .headers()["Authorization"],
2889 format!("Bearer {}", &token)
2890 );
2891
2892 assert!(!RequestBuilderWrapper::from_client(&client, |client| client
2895 .get("https://example.com/some/module.wasm"))
2896 .apply_auth(
2897 &Reference::try_from(HELLO_IMAGE_TAG)?,
2898 RegistryOperation::Pull
2899 )
2900 .await?
2901 .into_request_builder()
2902 .build()?
2903 .headers()
2904 .contains_key("Authorization"));
2905
2906 Ok(())
2907 }
2908
2909 #[tokio::test]
2910 async fn test_apply_auth_basic_not_forwarded_cross_host() -> anyhow::Result<()> {
2911 let client = Client::default();
2912 let image = Reference::try_from(HELLO_IMAGE_TAG)?;
2913 client
2914 .store_auth(
2915 image.resolve_registry(),
2916 RegistryAuth::Basic("user".to_string(), "pass".to_string()),
2917 )
2918 .await;
2919 client
2920 .tokens
2921 .insert(
2922 &image,
2923 RegistryOperation::Push,
2924 RegistryTokenType::Basic("user".to_string(), "pass".to_string()),
2925 )
2926 .await;
2927
2928 assert!(RequestBuilderWrapper::from_client(&client, |client| client
2929 .patch("https://webassembly.azurecr.io/v2/hello-wasm/blobs/uploads/abc"))
2930 .apply_auth(&image, RegistryOperation::Push)
2931 .await?
2932 .into_request_builder()
2933 .build()?
2934 .headers()
2935 .contains_key("Authorization"));
2936
2937 assert!(!RequestBuilderWrapper::from_client(&client, |client| client
2938 .patch("https://elsewhere.example.com/upload/abc"))
2939 .apply_auth(&image, RegistryOperation::Push)
2940 .await?
2941 .into_request_builder()
2942 .build()?
2943 .headers()
2944 .contains_key("Authorization"));
2945
2946 Ok(())
2947 }
2948
2949 #[rstest]
2950 #[case::registry("https://webassembly.azurecr.io/v2/hello-wasm/blobs/uploads/abc", true)]
2951 #[case::explicit_default_port(
2952 "https://webassembly.azurecr.io:443/v2/hello-wasm/blobs/uploads/abc",
2953 true
2954 )]
2955 #[case::another_port(
2956 "https://webassembly.azurecr.io:8443/v2/hello-wasm/blobs/uploads/abc",
2957 false
2958 )]
2959 #[case::another_host("https://elsewhere.example.com/v2/hello-wasm/blobs/uploads/abc", false)]
2960 #[case::subdomain(
2961 "https://evil.webassembly.azurecr.io/v2/hello-wasm/blobs/uploads/abc",
2962 false
2963 )]
2964 #[case::downgraded_to_http(
2965 "http://webassembly.azurecr.io/v2/hello-wasm/blobs/uploads/abc",
2966 false
2967 )]
2968 fn test_targets_credential_registry(#[case] location: &str, #[case] expected: bool) {
2969 let image = Reference::try_from(HELLO_IMAGE_TAG).expect("failed to parse reference");
2970 let client = Client::default();
2971 let request = RequestBuilderWrapper::from_client(&client, |c| c.patch(location));
2972
2973 assert_eq!(
2974 request
2975 .targets_credential_registry(&image)
2976 .expect("failed to compare the location with the registry"),
2977 expected
2978 );
2979 }
2980
2981 #[rstest]
2982 #[case::mirror("https://docker.mirror.io/v2/hello-wasm/blobs/uploads/abc", true)]
2983 #[case::upstream_registry(
2984 "https://webassembly.azurecr.io/v2/hello-wasm/blobs/uploads/abc",
2985 false
2986 )]
2987 fn test_targets_credential_registry_with_mirror(
2988 #[case] location: &str,
2989 #[case] expected: bool,
2990 ) {
2991 let mut image = Reference::try_from(HELLO_IMAGE_TAG).expect("failed to parse reference");
2992 image.set_mirror_registry("docker.mirror.io".to_owned());
2993 let client = Client::default();
2994 let request = RequestBuilderWrapper::from_client(&client, |c| c.patch(location));
2995
2996 assert_eq!(
2997 request
2998 .targets_credential_registry(&image)
2999 .expect("failed to compare the location with the registry"),
3000 expected
3001 );
3002 }
3003
3004 #[rstest]
3005 #[case::http_registry("http://localhost:5000/v2/hello-wasm/blobs/uploads/abc", true)]
3006 #[case::upgraded_to_https("https://localhost:5000/v2/hello-wasm/blobs/uploads/abc", true)]
3007 #[case::another_port("http://localhost:5001/v2/hello-wasm/blobs/uploads/abc", false)]
3008 fn test_targets_credential_registry_plain_http(#[case] location: &str, #[case] expected: bool) {
3009 let image =
3010 Reference::try_from("localhost:5000/hello-wasm:v1").expect("failed to parse reference");
3011 let client = Client::new(ClientConfig {
3012 protocol: ClientProtocol::Http,
3013 ..Default::default()
3014 });
3015 let request = RequestBuilderWrapper::from_client(&client, |c| c.patch(location));
3016
3017 assert_eq!(
3018 request
3019 .targets_credential_registry(&image)
3020 .expect("failed to compare the location with the registry"),
3021 expected
3022 );
3023 }
3024
3025 #[test]
3026 fn test_to_v2_blob_url() {
3027 let mut image = Reference::try_from(HELLO_IMAGE_TAG).expect("failed to parse reference");
3028 let c = Client::default();
3029
3030 assert_eq!(
3031 c.to_v2_blob_url(&image, "sha256:deadbeef"),
3032 "https://webassembly.azurecr.io/v2/hello-wasm/blobs/sha256:deadbeef"
3033 );
3034
3035 image.set_mirror_registry("docker.mirror.io".to_owned());
3036 assert_eq!(
3037 c.to_v2_blob_url(&image, "sha256:deadbeef"),
3038 "https://docker.mirror.io/v2/hello-wasm/blobs/sha256:deadbeef?ns=webassembly.azurecr.io"
3039 );
3040 }
3041
3042 #[rstest(image, expected_uri, expected_mirror_uri,
3043 case(HELLO_IMAGE_NO_TAG, "https://webassembly.azurecr.io/v2/hello-wasm/manifests/latest", "https://docker.mirror.io/v2/hello-wasm/manifests/latest?ns=webassembly.azurecr.io"), case(HELLO_IMAGE_TAG, "https://webassembly.azurecr.io/v2/hello-wasm/manifests/v1", "https://docker.mirror.io/v2/hello-wasm/manifests/v1?ns=webassembly.azurecr.io"),
3045 case(HELLO_IMAGE_DIGEST, "https://webassembly.azurecr.io/v2/hello-wasm/manifests/sha256:51d9b231d5129e3ffc267c9d455c49d789bf3167b611a07ab6e4b3304c96b0e7", "https://docker.mirror.io/v2/hello-wasm/manifests/sha256:51d9b231d5129e3ffc267c9d455c49d789bf3167b611a07ab6e4b3304c96b0e7?ns=webassembly.azurecr.io"),
3046 case(HELLO_IMAGE_TAG_AND_DIGEST, "https://webassembly.azurecr.io/v2/hello-wasm/manifests/sha256:51d9b231d5129e3ffc267c9d455c49d789bf3167b611a07ab6e4b3304c96b0e7", "https://docker.mirror.io/v2/hello-wasm/manifests/sha256:51d9b231d5129e3ffc267c9d455c49d789bf3167b611a07ab6e4b3304c96b0e7?ns=webassembly.azurecr.io"),
3047 )]
3048 fn test_to_v2_manifest(image: &str, expected_uri: &str, expected_mirror_uri: &str) {
3049 let mut reference = Reference::try_from(image).expect("failed to parse reference");
3050 let c = Client::default();
3051 assert_eq!(c.to_v2_manifest_url(&reference), expected_uri);
3052
3053 reference.set_mirror_registry("docker.mirror.io".to_owned());
3054 assert_eq!(c.to_v2_manifest_url(&reference), expected_mirror_uri);
3055 }
3056
3057 #[test]
3058 fn test_to_v2_blob_upload_url() {
3059 let image = Reference::try_from(HELLO_IMAGE_TAG).expect("failed to parse reference");
3060 let blob_url = Client::default().to_v2_blob_upload_url(&image);
3061
3062 assert_eq!(
3063 blob_url,
3064 "https://webassembly.azurecr.io/v2/hello-wasm/blobs/uploads/"
3065 )
3066 }
3067
3068 #[test]
3069 fn test_to_list_tags_url() {
3070 let mut image = Reference::try_from(HELLO_IMAGE_TAG).expect("failed to parse reference");
3071 let c = Client::default();
3072
3073 assert_eq!(
3074 c.to_list_tags_url(&image),
3075 "https://webassembly.azurecr.io/v2/hello-wasm/tags/list"
3076 );
3077
3078 image.set_mirror_registry("docker.mirror.io".to_owned());
3079 assert_eq!(
3080 c.to_list_tags_url(&image),
3081 "https://docker.mirror.io/v2/hello-wasm/tags/list?ns=webassembly.azurecr.io"
3082 );
3083 }
3084
3085 #[test]
3086 fn test_to_catalog_url() {
3087 let mut image = Reference::try_from(HELLO_IMAGE_TAG).expect("failed to parse reference");
3088 let c = Client::default();
3089
3090 assert_eq!(
3091 c.to_catalog_url(&image),
3092 "https://webassembly.azurecr.io/v2/_catalog"
3093 );
3094
3095 image.set_mirror_registry("docker.mirror.io".to_owned());
3096 assert_eq!(
3097 c.to_catalog_url(&image),
3098 "https://docker.mirror.io/v2/_catalog"
3099 );
3100 }
3101
3102 #[test]
3103 fn test_to_v2_referrers_url() {
3104 let image = Reference::try_from(HELLO_IMAGE_DIGEST).expect("failed to parse reference");
3105 let c = Client::default();
3106
3107 assert_eq!(
3109 c.to_v2_referrers_url(&image, None).unwrap(),
3110 "https://webassembly.azurecr.io/v2/hello-wasm/referrers/sha256:51d9b231d5129e3ffc267c9d455c49d789bf3167b611a07ab6e4b3304c96b0e7"
3111 );
3112
3113 assert_eq!(
3117 c.to_v2_referrers_url(&image, Some("application/spdx+json")).unwrap(),
3118 "https://webassembly.azurecr.io/v2/hello-wasm/referrers/sha256:51d9b231d5129e3ffc267c9d455c49d789bf3167b611a07ab6e4b3304c96b0e7?artifactType=application%2Fspdx%2Bjson"
3119 );
3120 }
3121
3122 #[test]
3123 fn manifest_url_generation_respects_http_protocol() {
3124 let c = Client::new(ClientConfig {
3125 protocol: ClientProtocol::Http,
3126 ..Default::default()
3127 });
3128 let reference = Reference::try_from("webassembly.azurecr.io/hello:v1".to_owned())
3129 .expect("Could not parse reference");
3130 assert_eq!(
3131 "http://webassembly.azurecr.io/v2/hello/manifests/v1",
3132 c.to_v2_manifest_url(&reference)
3133 );
3134 }
3135
3136 #[test]
3137 fn blob_url_generation_respects_http_protocol() {
3138 let c = Client::new(ClientConfig {
3139 protocol: ClientProtocol::Http,
3140 ..Default::default()
3141 });
3142 let reference = Reference::try_from("webassembly.azurecr.io/hello@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".to_owned())
3143 .expect("Could not parse reference");
3144 assert_eq!(
3145 "http://webassembly.azurecr.io/v2/hello/blobs/sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
3146 c.to_v2_blob_url(&reference, reference.digest().unwrap())
3147 );
3148 }
3149
3150 #[test]
3151 fn manifest_url_generation_uses_https_if_not_on_exception_list() {
3152 let insecure_registries = vec!["localhost".to_owned(), "oci.registry.local".to_owned()];
3153 let protocol = ClientProtocol::HttpsExcept(insecure_registries);
3154 let c = Client::new(ClientConfig {
3155 protocol,
3156 ..Default::default()
3157 });
3158 let reference = Reference::try_from("webassembly.azurecr.io/hello:v1".to_owned())
3159 .expect("Could not parse reference");
3160 assert_eq!(
3161 "https://webassembly.azurecr.io/v2/hello/manifests/v1",
3162 c.to_v2_manifest_url(&reference)
3163 );
3164 }
3165
3166 #[test]
3167 fn manifest_url_generation_uses_http_if_on_exception_list() {
3168 let insecure_registries = vec!["localhost".to_owned(), "oci.registry.local".to_owned()];
3169 let protocol = ClientProtocol::HttpsExcept(insecure_registries);
3170 let c = Client::new(ClientConfig {
3171 protocol,
3172 ..Default::default()
3173 });
3174 let reference = Reference::try_from("oci.registry.local/hello:v1".to_owned())
3175 .expect("Could not parse reference");
3176 assert_eq!(
3177 "http://oci.registry.local/v2/hello/manifests/v1",
3178 c.to_v2_manifest_url(&reference)
3179 );
3180 }
3181
3182 #[test]
3183 fn blob_url_generation_uses_https_if_not_on_exception_list() {
3184 let insecure_registries = vec!["localhost".to_owned(), "oci.registry.local".to_owned()];
3185 let protocol = ClientProtocol::HttpsExcept(insecure_registries);
3186 let c = Client::new(ClientConfig {
3187 protocol,
3188 ..Default::default()
3189 });
3190 let reference = Reference::try_from("webassembly.azurecr.io/hello@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".to_owned())
3191 .expect("Could not parse reference");
3192 assert_eq!(
3193 "https://webassembly.azurecr.io/v2/hello/blobs/sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
3194 c.to_v2_blob_url(&reference, reference.digest().unwrap())
3195 );
3196 }
3197
3198 #[test]
3199 fn blob_url_generation_uses_http_if_on_exception_list() {
3200 let insecure_registries = vec!["localhost".to_owned(), "oci.registry.local".to_owned()];
3201 let protocol = ClientProtocol::HttpsExcept(insecure_registries);
3202 let c = Client::new(ClientConfig {
3203 protocol,
3204 ..Default::default()
3205 });
3206 let reference = Reference::try_from("oci.registry.local/hello@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".to_owned())
3207 .expect("Could not parse reference");
3208 assert_eq!(
3209 "http://oci.registry.local/v2/hello/blobs/sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
3210 c.to_v2_blob_url(&reference, reference.digest().unwrap())
3211 );
3212 }
3213
3214 #[test]
3215 fn can_generate_valid_digest() {
3216 let bytes = b"hellobytes";
3217 let hash = sha256_digest(bytes);
3218
3219 let combination = vec![b"hello".to_vec(), b"bytes".to_vec()];
3220 let combination_hash =
3221 sha256_digest(&combination.into_iter().flatten().collect::<Vec<u8>>());
3222
3223 assert_eq!(
3224 hash,
3225 "sha256:fdbd95aafcbc814a2600fcc54c1e1706f52d2f9bf45cf53254f25bcd7599ce99"
3226 );
3227 assert_eq!(
3228 combination_hash,
3229 "sha256:fdbd95aafcbc814a2600fcc54c1e1706f52d2f9bf45cf53254f25bcd7599ce99"
3230 );
3231 }
3232
3233 #[test]
3234 fn test_registry_token_deserialize() {
3235 let text = r#"{"token": "abc"}"#;
3237 let res: Result<RegistryToken, serde_json::Error> = serde_json::from_str(text);
3238 assert!(res.is_ok());
3239 let rt = res.unwrap();
3240 assert_eq!(rt.token(), "abc");
3241
3242 let text = r#"{"access_token": "xyz"}"#;
3244 let res: Result<RegistryToken, serde_json::Error> = serde_json::from_str(text);
3245 assert!(res.is_ok());
3246 let rt = res.unwrap();
3247 assert_eq!(rt.token(), "xyz");
3248
3249 let text = r#"{"access_token": "xyz", "token": "abc"}"#;
3251 let res: Result<RegistryToken, serde_json::Error> = serde_json::from_str(text);
3252 assert!(res.is_ok());
3253 let rt = res.unwrap();
3254 assert_eq!(rt.token(), "abc");
3255
3256 let text = r#"{"token": "abc", "access_token": "xyz"}"#;
3258 let res: Result<RegistryToken, serde_json::Error> = serde_json::from_str(text);
3259 assert!(res.is_ok());
3260 let rt = res.unwrap();
3261 assert_eq!(rt.token(), "abc");
3262
3263 let text = r#"{"aaa": 300, "access_token": "xyz", "token": "abc", "zzz": 600}"#;
3265 let res: Result<RegistryToken, serde_json::Error> = serde_json::from_str(text);
3266 assert!(res.is_ok());
3267
3268 let text = r#"{"access_token": 300, "token": "abc"}"#;
3273 let res: Result<RegistryToken, serde_json::Error> = serde_json::from_str(text);
3274 assert!(res.is_ok());
3275 let rt = res.unwrap();
3276 assert_eq!(rt.token(), "abc");
3277
3278 let text = r#"{"access_token": "xyz", "token": 300}"#;
3280 let res: Result<RegistryToken, serde_json::Error> = serde_json::from_str(text);
3281 assert!(res.is_ok());
3282 let rt = res.unwrap();
3283 assert_eq!(rt.token(), "xyz");
3284
3285 let text = r#"{"token": 300}"#;
3287 let res: Result<RegistryToken, serde_json::Error> = serde_json::from_str(text);
3288 assert!(res.is_err());
3289
3290 let text = r#"{"access_token": 300}"#;
3292 let res: Result<RegistryToken, serde_json::Error> = serde_json::from_str(text);
3293 assert!(res.is_err());
3294
3295 let text = r#"{"token": {"some": "thing"}}"#;
3297 let res: Result<RegistryToken, serde_json::Error> = serde_json::from_str(text);
3298 assert!(res.is_err());
3299
3300 let text = r#"{"access_token": {"some": "thing"}}"#;
3302 let res: Result<RegistryToken, serde_json::Error> = serde_json::from_str(text);
3303 assert!(res.is_err());
3304
3305 let text = r#"{"some": "thing"}"#;
3307 let res: Result<RegistryToken, serde_json::Error> = serde_json::from_str(text);
3308 assert!(res.is_err());
3309
3310 let text = r#"{"token": "abc""#;
3312 let res: Result<RegistryToken, serde_json::Error> = serde_json::from_str(text);
3313 assert!(res.is_err());
3314
3315 let text = r#"_ _ _ kjbwef??98{9898 }} }}"#;
3317 let res: Result<RegistryToken, serde_json::Error> = serde_json::from_str(text);
3318 assert!(res.is_err());
3319 }
3320
3321 fn check_auth_token(token: &str) {
3322 assert!(token.len() > 64);
3324 }
3325
3326 #[tokio::test]
3327 async fn test_auth() {
3328 let _ = tracing_subscriber::fmt::try_init();
3329 for &image in TEST_IMAGES {
3330 let reference = Reference::try_from(image).expect("failed to parse reference");
3331 let c = Client::default();
3332 let token = c
3333 .auth(
3334 &reference,
3335 &RegistryAuth::Anonymous,
3336 RegistryOperation::Pull,
3337 )
3338 .await
3339 .expect("result from auth request");
3340
3341 assert!(token.is_some());
3342 check_auth_token(token.unwrap().as_ref());
3343
3344 let tok = c
3345 .tokens
3346 .get(&reference, RegistryOperation::Pull)
3347 .await
3348 .expect("token is available");
3349 if let RegistryTokenType::Bearer(tok) = tok {
3351 check_auth_token(tok.token());
3352 } else {
3353 panic!("Unexpeted Basic Auth Token");
3354 }
3355 }
3356 }
3357
3358 #[cfg(feature = "test-registry")]
3359 #[tokio::test]
3360 async fn test_list_tags() {
3361 let test_container = registry_image_edge()
3362 .start()
3363 .await
3364 .expect("Failed to start registry container");
3365 let port = test_container
3366 .get_host_port_ipv4(5000)
3367 .await
3368 .expect("Failed to get port");
3369 let auth =
3370 RegistryAuth::Basic(HTPASSWD_USERNAME.to_string(), HTPASSWD_PASSWORD.to_string());
3371
3372 let client = Client::new(ClientConfig {
3373 protocol: ClientProtocol::HttpsExcept(vec![format!("localhost:{}", port)]),
3374 ..Default::default()
3375 });
3376
3377 let image: Reference = HELLO_IMAGE_TAG_AND_DIGEST.parse().unwrap();
3378 client
3379 .auth(&image, &RegistryAuth::Anonymous, RegistryOperation::Pull)
3380 .await
3381 .expect("cannot authenticate against registry for pull operation");
3382
3383 let (manifest, _digest) = client
3384 ._pull_image_manifest(&image)
3385 .await
3386 .expect("failed to pull manifest");
3387
3388 let image_data = client
3389 .pull(&image, &auth, vec![manifest::WASM_LAYER_MEDIA_TYPE])
3390 .await
3391 .expect("failed to pull image");
3392
3393 for i in 0..=3 {
3394 let push_image: Reference = format!("localhost:{port}/hello-wasm:1.0.{i}")
3395 .parse()
3396 .unwrap();
3397 client
3398 .auth(&push_image, &auth, RegistryOperation::Push)
3399 .await
3400 .expect("authenticated");
3401 client
3402 .push(
3403 &push_image,
3404 &image_data.layers,
3405 image_data.config.clone(),
3406 &auth,
3407 Some(manifest.clone()),
3408 )
3409 .await
3410 .expect("Failed to push Image");
3411 }
3412
3413 let image: Reference = format!("localhost:{port}/hello-wasm:1.0.1")
3414 .parse()
3415 .unwrap();
3416 let response = client
3417 .list_tags(&image, &RegistryAuth::Anonymous, Some(2), Some("1.0.1"))
3418 .await
3419 .expect("Cannot list Tags");
3420 assert_eq!(response.tags, vec!["1.0.2", "1.0.3"])
3421 }
3422
3423 #[cfg(feature = "test-registry")]
3424 #[tokio::test]
3425 async fn test_catalog() {
3426 let test_container = registry_image_edge()
3427 .start()
3428 .await
3429 .expect("Failed to start registry container");
3430 let port = test_container
3431 .get_host_port_ipv4(5000)
3432 .await
3433 .expect("Failed to get port");
3434 let auth =
3435 RegistryAuth::Basic(HTPASSWD_USERNAME.to_string(), HTPASSWD_PASSWORD.to_string());
3436
3437 let client = Client::new(ClientConfig {
3438 protocol: ClientProtocol::HttpsExcept(vec![format!("localhost:{}", port)]),
3439 ..Default::default()
3440 });
3441
3442 let image: Reference = HELLO_IMAGE_TAG_AND_DIGEST.parse().unwrap();
3443 client
3444 .auth(&image, &RegistryAuth::Anonymous, RegistryOperation::Pull)
3445 .await
3446 .expect("cannot authenticate against registry for pull operation");
3447
3448 let (manifest, _digest) = client
3449 ._pull_image_manifest(&image)
3450 .await
3451 .expect("failed to pull manifest");
3452
3453 let image_data = client
3454 .pull(&image, &auth, vec![manifest::WASM_LAYER_MEDIA_TYPE])
3455 .await
3456 .expect("failed to pull image");
3457
3458 for repo in &["hello-catalog-a", "hello-catalog-b"] {
3460 let push_image: Reference = format!("localhost:{port}/{repo}:latest").parse().unwrap();
3461 client
3462 .auth(&push_image, &auth, RegistryOperation::Push)
3463 .await
3464 .expect("authenticated");
3465 client
3466 .push(
3467 &push_image,
3468 &image_data.layers,
3469 image_data.config.clone(),
3470 &auth,
3471 Some(manifest.clone()),
3472 )
3473 .await
3474 .expect("Failed to push Image");
3475 }
3476
3477 let catalog_ref: Reference = format!("localhost:{port}/hello-catalog-a:latest")
3479 .parse()
3480 .unwrap();
3481 let response = client
3482 .catalog(&catalog_ref, &RegistryAuth::Anonymous, None, None)
3483 .await
3484 .expect("Cannot list catalog");
3485 assert!(response
3486 .repositories
3487 .contains(&"hello-catalog-a".to_string()));
3488 assert!(response
3489 .repositories
3490 .contains(&"hello-catalog-b".to_string()));
3491
3492 let page1 = client
3494 .catalog(&catalog_ref, &RegistryAuth::Anonymous, Some(1), None)
3495 .await
3496 .expect("Cannot list catalog page 1");
3497 assert_eq!(page1.repositories.len(), 1);
3498
3499 let page2 = client
3500 .catalog(
3501 &catalog_ref,
3502 &RegistryAuth::Anonymous,
3503 Some(1),
3504 Some(&page1.repositories[0]),
3505 )
3506 .await
3507 .expect("Cannot list catalog page 2");
3508 assert_eq!(page2.repositories.len(), 1);
3509 assert_ne!(page1.repositories[0], page2.repositories[0]);
3510 }
3511
3512 #[tokio::test]
3513 async fn test_pull_manifest_private() {
3514 for &image in TEST_IMAGES {
3515 let reference = Reference::try_from(image).expect("failed to parse reference");
3516 let c = Client::default();
3518 c._pull_image_manifest(&reference)
3519 .await
3520 .expect_err("pull manifest should fail");
3521
3522 let c = Client::default();
3524 c.auth(
3525 &reference,
3526 &RegistryAuth::Anonymous,
3527 RegistryOperation::Pull,
3528 )
3529 .await
3530 .expect("authenticated");
3531 let (manifest, _) = c
3532 ._pull_image_manifest(&reference)
3533 .await
3534 .expect("pull manifest should not fail");
3535
3536 assert_eq!(manifest.schema_version, 2);
3538 assert!(!manifest.layers.is_empty());
3539 }
3540 }
3541
3542 #[tokio::test]
3543 async fn test_pull_manifest_public() {
3544 for &image in TEST_IMAGES {
3545 let reference = Reference::try_from(image).expect("failed to parse reference");
3546 let c = Client::default();
3547 let (manifest, _) = c
3548 .pull_image_manifest(&reference, &RegistryAuth::Anonymous)
3549 .await
3550 .expect("pull manifest should not fail");
3551
3552 assert_eq!(manifest.schema_version, 2);
3554 assert!(!manifest.layers.is_empty());
3555 }
3556 }
3557
3558 #[tokio::test]
3559 async fn pull_manifest_and_config_public() {
3560 for &image in TEST_IMAGES {
3561 let reference = Reference::try_from(image).expect("failed to parse reference");
3562 let c = Client::default();
3563 let (manifest, _, config) = c
3564 .pull_manifest_and_config(&reference, &RegistryAuth::Anonymous)
3565 .await
3566 .expect("pull manifest and config should not fail");
3567
3568 assert_eq!(manifest.schema_version, 2);
3570 assert!(!manifest.layers.is_empty());
3571 assert!(!config.is_empty());
3572 }
3573 }
3574
3575 #[tokio::test]
3576 async fn test_fetch_digest() {
3577 let c = Client::default();
3578
3579 for &image in TEST_IMAGES {
3580 let reference = Reference::try_from(image).expect("failed to parse reference");
3581 c.fetch_manifest_digest(&reference, &RegistryAuth::Anonymous)
3582 .await
3583 .expect("pull manifest should not fail");
3584
3585 let reference = Reference::try_from(image).expect("failed to parse reference");
3587 let c = Client::default();
3588 c.auth(
3589 &reference,
3590 &RegistryAuth::Anonymous,
3591 RegistryOperation::Pull,
3592 )
3593 .await
3594 .expect("authenticated");
3595 let digest = c
3596 .fetch_manifest_digest(&reference, &RegistryAuth::Anonymous)
3597 .await
3598 .expect("pull manifest should not fail");
3599
3600 assert_eq!(
3601 digest,
3602 "sha256:51d9b231d5129e3ffc267c9d455c49d789bf3167b611a07ab6e4b3304c96b0e7"
3603 );
3604 }
3605 }
3606
3607 #[tokio::test]
3608 async fn test_pull_blob() {
3609 let c = Client::default();
3610
3611 for &image in TEST_IMAGES {
3612 let reference = Reference::try_from(image).expect("failed to parse reference");
3613 c.auth(
3614 &reference,
3615 &RegistryAuth::Anonymous,
3616 RegistryOperation::Pull,
3617 )
3618 .await
3619 .expect("authenticated");
3620 let (manifest, _) = c
3621 ._pull_image_manifest(&reference)
3622 .await
3623 .expect("failed to pull manifest");
3624
3625 let mut file: Vec<u8> = Vec::new();
3627 let layer0 = &manifest.layers[0];
3628
3629 let mut last_error = None;
3631 for i in 1..6 {
3632 if let Err(e) = c.pull_blob(&reference, layer0, &mut file).await {
3633 println!("Got error on pull_blob call attempt {i}. Will retry in 1s: {e:?}");
3634 last_error.replace(e);
3635 tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
3636 } else {
3637 last_error = None;
3638 break;
3639 }
3640 }
3641
3642 if let Some(e) = last_error {
3643 panic!("Unable to pull layer: {e:?}");
3644 }
3645
3646 assert_eq!(file.len(), layer0.size as usize);
3648 }
3649 }
3650
3651 #[tokio::test]
3652 async fn test_pull_blob_stream() {
3653 let c = Client::default();
3654
3655 for &image in TEST_IMAGES {
3656 let reference = Reference::try_from(image).expect("failed to parse reference");
3657 c.auth(
3658 &reference,
3659 &RegistryAuth::Anonymous,
3660 RegistryOperation::Pull,
3661 )
3662 .await
3663 .expect("authenticated");
3664 let (manifest, _) = c
3665 ._pull_image_manifest(&reference)
3666 .await
3667 .expect("failed to pull manifest");
3668
3669 let mut file: Vec<u8> = Vec::new();
3671 let layer0 = &manifest.layers[0];
3672
3673 let layer_stream = c
3674 .pull_blob_stream(&reference, layer0)
3675 .await
3676 .expect("failed to pull blob stream");
3677
3678 assert_eq!(layer_stream.content_length, Some(layer0.size as u64));
3679 AsyncReadExt::read_to_end(&mut StreamReader::new(layer_stream.stream), &mut file)
3680 .await
3681 .unwrap();
3682
3683 assert_eq!(file.len(), layer0.size as usize);
3685 }
3686 }
3687
3688 #[tokio::test]
3689 async fn test_pull_blob_stream_partial() {
3690 let c = Client::default();
3691
3692 for &image in TEST_IMAGES {
3693 let reference = Reference::try_from(image).expect("failed to parse reference");
3694 c.auth(
3695 &reference,
3696 &RegistryAuth::Anonymous,
3697 RegistryOperation::Pull,
3698 )
3699 .await
3700 .expect("authenticated");
3701 let (manifest, _) = c
3702 ._pull_image_manifest(&reference)
3703 .await
3704 .expect("failed to pull manifest");
3705
3706 let mut partial_file: Vec<u8> = Vec::new();
3708 let layer0 = &manifest.layers[0];
3709 let (offset, length) = (10, 6);
3710
3711 let partial_response = c
3712 .pull_blob_stream_partial(&reference, layer0, offset, Some(length))
3713 .await
3714 .expect("failed to pull blob stream");
3715 let full_response = c
3716 .pull_blob_stream_partial(&reference, layer0, 0, Some(layer0.size as u64))
3717 .await
3718 .expect("failed to pull blob stream");
3719
3720 let layer_stream_partial = match partial_response {
3721 BlobResponse::Full(_stream) => panic!("expected partial response"),
3722 BlobResponse::Partial(stream) => stream,
3723 };
3724 assert_eq!(layer_stream_partial.content_length, Some(length));
3725 AsyncReadExt::read_to_end(
3726 &mut StreamReader::new(layer_stream_partial.stream),
3727 &mut partial_file,
3728 )
3729 .await
3730 .unwrap();
3731
3732 let mut full_file: Vec<u8> = Vec::new();
3734 let layer_stream_full = match full_response {
3735 BlobResponse::Full(_stream) => panic!("expected partial response"),
3736 BlobResponse::Partial(stream) => stream,
3737 };
3738 assert_eq!(layer_stream_full.content_length, Some(layer0.size as u64));
3739 AsyncReadExt::read_to_end(
3740 &mut StreamReader::new(layer_stream_full.stream),
3741 &mut full_file,
3742 )
3743 .await
3744 .unwrap();
3745
3746 assert_eq!(partial_file.len(), length as usize);
3748 assert_eq!(full_file.len(), layer0.size as usize);
3750 let end: usize = (offset + length) as usize;
3752 assert_eq!(partial_file, full_file[offset as usize..end]);
3753 }
3754 }
3755
3756 #[tokio::test]
3757 async fn test_pull() {
3758 for &image in TEST_IMAGES {
3759 let reference = Reference::try_from(image).expect("failed to parse reference");
3760
3761 let mut last_error = None;
3763 let mut image_data = None;
3764 for i in 1..6 {
3765 match Client::default()
3766 .pull(
3767 &reference,
3768 &RegistryAuth::Anonymous,
3769 vec![manifest::WASM_LAYER_MEDIA_TYPE],
3770 )
3771 .await
3772 {
3773 Ok(data) => {
3774 image_data = Some(data);
3775 last_error = None;
3776 break;
3777 }
3778 Err(e) => {
3779 println!("Got error on pull call attempt {i}. Will retry in 1s: {e:?}");
3780 last_error.replace(e);
3781 tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
3782 }
3783 }
3784 }
3785
3786 if let Some(e) = last_error {
3787 panic!("Unable to pull layer: {e:?}");
3788 }
3789
3790 assert!(image_data.is_some());
3791 let image_data = image_data.unwrap();
3792 assert!(!image_data.layers.is_empty());
3793 assert!(image_data.digest.is_some());
3794 }
3795 }
3796
3797 #[tokio::test]
3799 async fn test_pull_without_layer_validation() {
3800 for &image in TEST_IMAGES {
3801 let reference = Reference::try_from(image).expect("failed to parse reference");
3802 assert!(Client::default()
3803 .pull(&reference, &RegistryAuth::Anonymous, vec![],)
3804 .await
3805 .is_err());
3806 }
3807 }
3808
3809 #[tokio::test]
3811 async fn test_pull_wrong_layer_validation() {
3812 for &image in TEST_IMAGES {
3813 let reference = Reference::try_from(image).expect("failed to parse reference");
3814 assert!(Client::default()
3815 .pull(&reference, &RegistryAuth::Anonymous, vec!["text/plain"],)
3816 .await
3817 .is_err());
3818 }
3819 }
3820
3821 #[cfg(feature = "test-registry")]
3827 fn registry_image_edge() -> GenericImage {
3828 GenericImage::new("distribution/distribution", "edge")
3829 .with_wait_for(WaitFor::message_on_stderr("listening on "))
3830 }
3831
3832 #[cfg(feature = "test-registry")]
3833 fn registry_image() -> GenericImage {
3834 GenericImage::new("docker.io/library/registry", "2")
3835 .with_wait_for(WaitFor::message_on_stderr("listening on "))
3836 }
3837
3838 #[cfg(feature = "test-registry")]
3839 fn registry_image_basic_auth(auth_path: &str) -> ContainerRequest<GenericImage> {
3840 GenericImage::new("docker.io/library/registry", "2")
3841 .with_wait_for(WaitFor::message_on_stderr("listening on "))
3842 .with_env_var("REGISTRY_AUTH", "htpasswd")
3843 .with_env_var("REGISTRY_AUTH_HTPASSWD_REALM", "Registry Realm")
3844 .with_env_var("REGISTRY_AUTH_HTPASSWD_PATH", "/auth/htpasswd")
3845 .with_mount(Mount::bind_mount(auth_path, "/auth"))
3846 }
3847
3848 #[tokio::test]
3849 #[cfg(feature = "test-registry")]
3850 async fn can_push_chunk() {
3851 let test_container = registry_image()
3852 .start()
3853 .await
3854 .expect("Failed to start registry container");
3855 let port = test_container
3856 .get_host_port_ipv4(5000)
3857 .await
3858 .expect("Failed to get port");
3859
3860 let c = Client::new(ClientConfig {
3861 protocol: ClientProtocol::Http,
3862 ..Default::default()
3863 });
3864 let url = format!("localhost:{port}/hello-wasm:v1");
3865 let image: Reference = url.parse().unwrap();
3866
3867 c.auth(&image, &RegistryAuth::Anonymous, RegistryOperation::Push)
3868 .await
3869 .expect("result from auth request");
3870
3871 let location = c
3872 .begin_push_chunked_session(&image)
3873 .await
3874 .expect("failed to begin push session");
3875
3876 let image_data = Bytes::from(b"iamawebassemblymodule".to_vec());
3877 let (next_location, next_byte) = c
3878 .push_chunk(&location, &image, image_data.clone(), 0)
3879 .await
3880 .expect("failed to push layer");
3881
3882 assert!(next_location.len() >= url.len() + "6987887f-0196-45ee-91a1-2dfad901bea0".len());
3884 assert_eq!(next_byte, image_data.len());
3885
3886 let layer_location = c
3887 .end_push_chunked_session(&next_location, &image, &sha256_digest(&image_data))
3888 .await
3889 .expect("failed to end push session");
3890
3891 assert_eq!(layer_location, format!("http://localhost:{port}/v2/hello-wasm/blobs/sha256:6165c4ad43c0803798b6f2e49d6348c915d52c999a5f890846cee77ea65d230b"));
3892 }
3893
3894 #[tokio::test]
3895 #[cfg(feature = "test-registry")]
3896 async fn can_push_multiple_chunks() {
3897 let test_container = registry_image()
3898 .start()
3899 .await
3900 .expect("Failed to start registry container");
3901 let port = test_container
3902 .get_host_port_ipv4(5000)
3903 .await
3904 .expect("Failed to get port");
3905
3906 let mut c = Client::new(ClientConfig {
3907 protocol: ClientProtocol::Http,
3908 ..Default::default()
3909 });
3910 c.push_chunk_size = 3;
3912 let url = format!("localhost:{port}/hello-wasm:v1");
3913 let image: Reference = url.parse().unwrap();
3914
3915 c.auth(&image, &RegistryAuth::Anonymous, RegistryOperation::Push)
3916 .await
3917 .expect("result from auth request");
3918
3919 let image_data: Vec<u8> =
3920 b"i am a big webassembly mode that needs chunked uploads".to_vec();
3921 let image_digest = sha256_digest(&image_data);
3922
3923 let location = c
3924 .push_blob_chunked(&image, image_data, &image_digest)
3925 .await
3926 .expect("failed to begin push session");
3927
3928 assert_eq!(
3929 location,
3930 format!("http://localhost:{port}/v2/hello-wasm/blobs/{image_digest}")
3931 );
3932 }
3933
3934 #[tokio::test]
3935 #[cfg(feature = "test-registry")]
3936 async fn test_image_roundtrip_anon_auth() {
3937 let test_container = registry_image()
3938 .start()
3939 .await
3940 .expect("Failed to start registry container");
3941
3942 test_image_roundtrip(&RegistryAuth::Anonymous, &test_container).await;
3943 }
3944
3945 #[tokio::test]
3946 #[cfg(feature = "test-registry")]
3947 async fn test_image_roundtrip_basic_auth() {
3948 let auth_dir = TempDir::new().expect("cannot create tmp directory");
3949 let htpasswd_path = path::Path::join(auth_dir.path(), "htpasswd");
3950 fs::write(htpasswd_path, HTPASSWD).expect("cannot write htpasswd file");
3951
3952 let image = registry_image_basic_auth(
3953 auth_dir
3954 .path()
3955 .to_str()
3956 .expect("cannot convert htpasswd_path to string"),
3957 );
3958 let test_container = image.start().await.expect("cannot registry container");
3959
3960 let auth =
3961 RegistryAuth::Basic(HTPASSWD_USERNAME.to_string(), HTPASSWD_PASSWORD.to_string());
3962
3963 test_image_roundtrip(&auth, &test_container).await;
3964 }
3965
3966 #[cfg(feature = "test-registry")]
3967 async fn test_image_roundtrip(
3968 registry_auth: &RegistryAuth,
3969 test_container: &testcontainers::ContainerAsync<GenericImage>,
3970 ) {
3971 let _ = tracing_subscriber::fmt::try_init();
3972 let port = test_container
3973 .get_host_port_ipv4(5000)
3974 .await
3975 .expect("Failed to get port");
3976
3977 let c = Client::new(ClientConfig {
3978 protocol: ClientProtocol::HttpsExcept(vec![format!("localhost:{}", port)]),
3979 ..Default::default()
3980 });
3981
3982 let image: Reference = HELLO_IMAGE_TAG_AND_DIGEST.parse().unwrap();
3984 c.auth(&image, &RegistryAuth::Anonymous, RegistryOperation::Pull)
3985 .await
3986 .expect("cannot authenticate against registry for pull operation");
3987
3988 let (manifest, _digest) = c
3989 ._pull_image_manifest(&image)
3990 .await
3991 .expect("failed to pull manifest");
3992
3993 let image_data = c
3994 .pull(&image, registry_auth, vec![manifest::WASM_LAYER_MEDIA_TYPE])
3995 .await
3996 .expect("failed to pull image");
3997
3998 let push_image: Reference = format!("localhost:{port}/hello-wasm:v1").parse().unwrap();
3999 c.auth(&push_image, registry_auth, RegistryOperation::Push)
4000 .await
4001 .expect("authenticated");
4002
4003 c.push(
4004 &push_image,
4005 &image_data.layers,
4006 image_data.config.clone(),
4007 registry_auth,
4008 Some(manifest.clone()),
4009 )
4010 .await
4011 .expect("failed to push image");
4012
4013 let pulled_image_data = c
4014 .pull(
4015 &push_image,
4016 registry_auth,
4017 vec![manifest::WASM_LAYER_MEDIA_TYPE],
4018 )
4019 .await
4020 .expect("failed to pull pushed image");
4021
4022 let (pulled_manifest, _digest) = c
4023 ._pull_image_manifest(&push_image)
4024 .await
4025 .expect("failed to pull pushed image manifest");
4026
4027 assert!(image_data.layers.len() == 1);
4028 assert!(pulled_image_data.layers.len() == 1);
4029 assert_eq!(
4030 image_data.layers[0].data.len(),
4031 pulled_image_data.layers[0].data.len()
4032 );
4033 assert_eq!(image_data.layers[0].data, pulled_image_data.layers[0].data);
4034
4035 assert_eq!(manifest.media_type, pulled_manifest.media_type);
4036 assert_eq!(manifest.schema_version, pulled_manifest.schema_version);
4037 assert_eq!(manifest.config.digest, pulled_manifest.config.digest);
4038 }
4039
4040 #[tokio::test]
4041 async fn test_raw_manifest_digest() {
4042 let _ = tracing_subscriber::fmt::try_init();
4043
4044 let c = Client::default();
4045
4046 let image: Reference = HELLO_IMAGE_TAG_AND_DIGEST.parse().unwrap();
4048 c.auth(&image, &RegistryAuth::Anonymous, RegistryOperation::Pull)
4049 .await
4050 .expect("cannot authenticate against registry for pull operation");
4051
4052 let (manifest, _) = c
4053 .pull_manifest_raw(
4054 &image,
4055 &RegistryAuth::Anonymous,
4056 MIME_TYPES_DISTRIBUTION_MANIFEST,
4057 )
4058 .await
4059 .expect("failed to pull manifest");
4060
4061 let digest = sha2::Sha256::digest(manifest);
4063 let hex = format!("sha256:{}", hex::encode(digest));
4064
4065 assert_eq!(image.digest().unwrap(), hex);
4067 }
4068
4069 #[tokio::test]
4070 #[cfg(feature = "test-registry")]
4071 async fn test_mount() {
4072 let test_container = registry_image()
4074 .start()
4075 .await
4076 .expect("Failed to start registry");
4077 let port = test_container
4078 .get_host_port_ipv4(5000)
4079 .await
4080 .expect("Failed to get port");
4081
4082 let c = Client::new(ClientConfig {
4083 protocol: ClientProtocol::HttpsExcept(vec![format!("localhost:{}", port)]),
4084 ..Default::default()
4085 });
4086
4087 let layer_reference: Reference = format!("localhost:{port}/layer-repository")
4089 .parse()
4090 .unwrap();
4091 let layer_data = vec![1u8, 2, 3, 4];
4092 let layer = OciDescriptor {
4093 digest: sha256_digest(&layer_data),
4094 ..Default::default()
4095 };
4096 c.push_blob(
4097 &layer_reference,
4098 Bytes::copy_from_slice(&layer_data),
4099 &layer.digest,
4100 )
4101 .await
4102 .expect("Failed to push");
4103
4104 let image_reference: Reference = format!("localhost:{port}/image-repository")
4106 .parse()
4107 .unwrap();
4108 c.mount_blob(&image_reference, &layer_reference, &layer.digest)
4109 .await
4110 .expect("Failed to mount");
4111
4112 let mut buf = Vec::new();
4114 c.pull_blob(&image_reference, &layer, &mut buf)
4115 .await
4116 .expect("Failed to pull");
4117
4118 assert_eq!(layer_data, buf);
4119 }
4120
4121 #[tokio::test]
4122 async fn test_platform_resolution() {
4123 let reference = Reference::try_from(DOCKER_IO_IMAGE).expect("failed to parse reference");
4125 let mut c = Client::new(ClientConfig {
4126 platform_resolver: None,
4127 ..Default::default()
4128 });
4129 let err = c
4130 .pull_image_manifest(&reference, &RegistryAuth::Anonymous)
4131 .await
4132 .unwrap_err();
4133 assert_eq!(
4134 format!("{err}"),
4135 "Received Image Index/Manifest List, but platform_resolver was not defined on the client config. Consider setting platform_resolver"
4136 );
4137
4138 c = Client::new(ClientConfig {
4139 platform_resolver: Some(Box::new(linux_amd64_resolver)),
4140 ..Default::default()
4141 });
4142 let (_manifest, digest) = c
4143 .pull_image_manifest(&reference, &RegistryAuth::Anonymous)
4144 .await
4145 .expect("Couldn't pull manifest");
4146 assert_eq!(
4147 digest,
4148 "sha256:f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4"
4149 );
4150 }
4151
4152 #[tokio::test]
4153 async fn test_pull_ghcr_io() {
4154 let reference = Reference::try_from(GHCR_IO_IMAGE).expect("failed to parse reference");
4155 let c = Client::default();
4156 let (manifest, _manifest_str) = c
4157 .pull_image_manifest(&reference, &RegistryAuth::Anonymous)
4158 .await
4159 .unwrap();
4160 assert_eq!(manifest.config.media_type, manifest::WASM_CONFIG_MEDIA_TYPE);
4161 }
4162
4163 #[tokio::test]
4164 async fn test_list_all_tags_ghcr_io() {
4165 const MAX_TAGS_PER_LIST: usize = 100;
4166 const MAX_TAG_REQUESTS: usize = 10;
4167
4168 let reference = Reference::try_from(GHCR_IO_IMAGE).expect("failed to parse reference");
4169 let c = Client::default();
4170
4171 let mut last_tag = None;
4175 for _ in 0..MAX_TAG_REQUESTS {
4176 let mut response = c
4177 .list_tags(
4178 &reference,
4179 &RegistryAuth::Anonymous,
4180 Some(MAX_TAGS_PER_LIST),
4181 last_tag.as_deref(),
4182 )
4183 .await
4184 .expect("failed to list tags in registry");
4185
4186 if let Some(tag) = response.tags.pop() {
4187 last_tag = Some(tag);
4188 } else {
4189 return;
4190 }
4191 }
4192
4193 panic!("failed to list all tags for {GHCR_IO_IMAGE} in {MAX_TAG_REQUESTS} requests");
4194 }
4195
4196 #[tokio::test]
4197 #[ignore]
4198 async fn test_roundtrip_multiple_layers() {
4199 let _ = tracing_subscriber::fmt::try_init();
4200 let c = Client::new(ClientConfig {
4201 protocol: ClientProtocol::HttpsExcept(vec!["oci.registry.local".to_string()]),
4202 ..Default::default()
4203 });
4204 let src_image = Reference::try_from("registry:2.7.1").expect("failed to parse reference");
4205 let dest_image = Reference::try_from("oci.registry.local/registry:roundtrip-test")
4206 .expect("failed to parse reference");
4207
4208 let image = c
4209 .pull(
4210 &src_image,
4211 &RegistryAuth::Anonymous,
4212 vec![IMAGE_DOCKER_LAYER_GZIP_MEDIA_TYPE],
4213 )
4214 .await
4215 .expect("Failed to pull manifest");
4216 assert!(image.layers.len() > 1);
4217
4218 let ImageData {
4219 layers,
4220 config,
4221 manifest,
4222 ..
4223 } = image;
4224 c.push(
4225 &dest_image,
4226 &layers,
4227 config,
4228 &RegistryAuth::Anonymous,
4229 manifest,
4230 )
4231 .await
4232 .expect("Failed to pull manifest");
4233
4234 c.pull_image_manifest(&dest_image, &RegistryAuth::Anonymous)
4235 .await
4236 .expect("Failed to pull manifest");
4237 }
4238
4239 #[tokio::test]
4240 async fn test_hashable_image_layer() {
4241 use itertools::Itertools;
4242
4243 let image_layers = Vec::from([
4245 ImageLayer {
4246 data: Bytes::from_static(&[0, 1, 2, 3]),
4247 media_type: "media_type".to_owned(),
4248 annotations: Some(BTreeMap::from([
4249 ("0".to_owned(), "1".to_owned()),
4250 ("2".to_owned(), "3".to_owned()),
4251 ])),
4252 },
4253 ImageLayer {
4254 data: Bytes::from_static(&[0, 1, 2, 3]),
4255 media_type: "media_type".to_owned(),
4256 annotations: Some(BTreeMap::from([
4257 ("2".to_owned(), "3".to_owned()),
4258 ("0".to_owned(), "1".to_owned()),
4259 ])),
4260 },
4261 ImageLayer {
4262 data: Bytes::from_static(&[0, 1, 2, 3]),
4263 media_type: "different_media_type".to_owned(),
4264 annotations: Some(BTreeMap::from([
4265 ("0".to_owned(), "1".to_owned()),
4266 ("2".to_owned(), "3".to_owned()),
4267 ])),
4268 },
4269 ImageLayer {
4270 data: Bytes::from_static(&[0, 1, 2]),
4271 media_type: "media_type".to_owned(),
4272 annotations: Some(BTreeMap::from([
4273 ("0".to_owned(), "1".to_owned()),
4274 ("2".to_owned(), "3".to_owned()),
4275 ])),
4276 },
4277 ImageLayer {
4278 data: Bytes::from_static(&[0, 1, 2, 3]),
4279 media_type: "media_type".to_owned(),
4280 annotations: Some(BTreeMap::from([
4281 ("1".to_owned(), "0".to_owned()),
4282 ("2".to_owned(), "3".to_owned()),
4283 ])),
4284 },
4285 ]);
4286
4287 assert_eq!(
4288 &image_layers[0], &image_layers[1],
4289 "image_layers[0] should equal image_layers[1]"
4290 );
4291 assert_ne!(
4292 &image_layers[0], &image_layers[2],
4293 "image_layers[0] should not equal image_layers[2]"
4294 );
4295 assert_ne!(
4296 &image_layers[0], &image_layers[3],
4297 "image_layers[0] should not equal image_layers[3]"
4298 );
4299 assert_ne!(
4300 &image_layers[0], &image_layers[4],
4301 "image_layers[0] should not equal image_layers[4]"
4302 );
4303 assert_ne!(
4304 &image_layers[2], &image_layers[3],
4305 "image_layers[2] should not equal image_layers[3]"
4306 );
4307 assert_ne!(
4308 &image_layers[2], &image_layers[4],
4309 "image_layers[2] should not equal image_layers[4]"
4310 );
4311 assert_ne!(
4312 &image_layers[3], &image_layers[4],
4313 "image_layers[3] should not equal image_layers[4]"
4314 );
4315
4316 let deduped: Vec<ImageLayer> = image_layers.clone().into_iter().unique().collect();
4317 assert_eq!(
4318 image_layers.len() - 1,
4319 deduped.len(),
4320 "after deduplication, there should be one less image layer"
4321 );
4322 }
4323
4324 #[tokio::test]
4325 #[cfg(feature = "test-registry")]
4326 async fn test_blob_exists() {
4327 let real_registry = registry_image_edge()
4328 .start()
4329 .await
4330 .expect("Failed to start registry container");
4331
4332 let server_port = real_registry
4333 .get_host_port_ipv4(5000)
4334 .await
4335 .expect("Failed to get port");
4336
4337 let client = Client::new(ClientConfig {
4338 protocol: ClientProtocol::HttpsExcept(vec![format!("localhost:{}", server_port)]),
4339 ..Default::default()
4340 });
4341
4342 let reference = Reference::try_from(format!("localhost:{server_port}/empty"))
4343 .expect("failed to parse reference");
4344
4345 assert!(!client
4346 .blob_exists(&reference, EMPTY_JSON_DIGEST)
4347 .await
4348 .expect("failed to check blob existence"));
4349 client
4350 .push_blob(&reference, EMPTY_JSON_BLOB.as_bytes(), EMPTY_JSON_DIGEST)
4351 .await
4352 .expect("failed to push empty json blob");
4353 assert!(client
4354 .blob_exists(&reference, EMPTY_JSON_DIGEST)
4355 .await
4356 .expect("failed to check blob existence"));
4357 }
4358
4359 #[rstest]
4360 #[case::chunked(false)]
4361 #[case::monolithic(true)]
4362 #[tokio::test]
4363 #[cfg(feature = "test-registry")]
4364 async fn test_push_stream(#[case] use_monolithic_push: bool) {
4365 let real_registry = registry_image_edge()
4366 .start()
4367 .await
4368 .expect("Failed to start registry container");
4369
4370 let server_port = real_registry
4371 .get_host_port_ipv4(5000)
4372 .await
4373 .expect("Failed to get port");
4374
4375 let mut client = Client::new(ClientConfig {
4376 protocol: ClientProtocol::HttpsExcept(vec![format!("localhost:{}", server_port)]),
4377 use_monolithic_push,
4378 ..Default::default()
4379 });
4380 client.push_chunk_size = 253;
4381
4382 let data_hash = "sha256:c8f5d0341d54d951a71b136e6e2afcb14d11ed8489a7ae126a8fee0df6ecf193";
4384 let repeat = 16usize;
4385 let chunk_size = 256usize; let data_stream = |n| {
4387 futures_util::stream::repeat(Bytes::from_iter(0u8..=255))
4388 .take(n)
4389 .map(Ok)
4390 };
4391 let size = Some((repeat * chunk_size) as u64);
4392
4393 let reference = Reference::try_from(format!("localhost:{server_port}/test-push-stream"))
4394 .expect("failed to parse reference");
4395
4396 client
4398 .push_blob_stream(&reference, data_stream(1), data_hash, None)
4399 .await
4400 .expect_err("expected push to fail with mismatched digest");
4401
4402 client
4404 .push_blob_stream(&reference, data_stream(repeat), data_hash, size)
4405 .await
4406 .expect("failed to push stream");
4407
4408 assert!(client
4409 .blob_exists(&reference, data_hash)
4410 .await
4411 .expect("failed to check blob existence"));
4412 }
4413
4414 #[tokio::test]
4415 #[cfg(feature = "test-registry")]
4416 async fn test_push_stream_monolithic_requires_size() {
4417 let real_registry = registry_image_edge()
4418 .start()
4419 .await
4420 .expect("Failed to start registry container");
4421
4422 let server_port = real_registry
4423 .get_host_port_ipv4(5000)
4424 .await
4425 .expect("Failed to get port");
4426
4427 let client = Client::new(ClientConfig {
4428 protocol: ClientProtocol::HttpsExcept(vec![format!("localhost:{}", server_port)]),
4429 use_monolithic_push: true,
4430 ..Default::default()
4431 });
4432
4433 let data_hash = "sha256:c8f5d0341d54d951a71b136e6e2afcb14d11ed8489a7ae126a8fee0df6ecf193";
4434 let data_stream = futures_util::stream::repeat(Bytes::from_iter(0u8..=255))
4435 .take(16)
4436 .map(Ok);
4437
4438 let reference = Reference::try_from(format!("localhost:{server_port}/test-push-stream"))
4439 .expect("failed to parse reference");
4440
4441 client
4442 .push_blob_stream(&reference, data_stream, data_hash, None)
4443 .await
4444 .expect_err("expected error when use_monolithic_push is true but size is None");
4445 }
4446
4447 #[tokio::test]
4448 #[cfg(feature = "test-registry")]
4449 async fn test_push_stream_chunked_without_digest() {
4450 let real_registry = registry_image_edge()
4451 .start()
4452 .await
4453 .expect("Failed to start registry container");
4454
4455 let server_port = real_registry
4456 .get_host_port_ipv4(5000)
4457 .await
4458 .expect("Failed to get port");
4459
4460 let mut client = Client::new(ClientConfig {
4461 protocol: ClientProtocol::HttpsExcept(vec![format!("localhost:{}", server_port)]),
4462 use_monolithic_push: true,
4463 ..Default::default()
4464 });
4465 client.push_chunk_size = 253;
4466
4467 let data_hash = "sha256:c8f5d0341d54d951a71b136e6e2afcb14d11ed8489a7ae126a8fee0df6ecf193";
4469 let repeat = 16usize;
4470 let data_stream = futures_util::stream::repeat(Bytes::from_iter(0u8..=255))
4471 .take(repeat)
4472 .map(Ok);
4473
4474 let reference =
4475 Reference::try_from(format!("localhost:{server_port}/test-push-stream-chunked"))
4476 .expect("failed to parse reference");
4477
4478 let response = client
4479 .push_blob_stream_chunked(&reference, data_stream)
4480 .await
4481 .expect("failed to push stream with chunked-only API");
4482
4483 assert_eq!(response.blob_digest, data_hash);
4484 assert_eq!(response.size, (repeat * 256) as u64);
4485 assert!(response.blob_url.ends_with(data_hash));
4486
4487 assert!(client
4488 .blob_exists(&reference, data_hash)
4489 .await
4490 .expect("failed to check blob existence"));
4491 }
4492
4493 #[tokio::test]
4494 #[cfg(feature = "test-registry")]
4495 async fn test_push_stream_chunked_empty_stream() {
4496 let real_registry = registry_image_edge()
4497 .start()
4498 .await
4499 .expect("Failed to start registry container");
4500
4501 let server_port = real_registry
4502 .get_host_port_ipv4(5000)
4503 .await
4504 .expect("Failed to get port");
4505
4506 let client = Client::new(ClientConfig {
4507 protocol: ClientProtocol::HttpsExcept(vec![format!("localhost:{}", server_port)]),
4508 ..Default::default()
4509 });
4510
4511 let data_stream = futures_util::stream::empty::<crate::errors::Result<Bytes>>();
4512
4513 let reference = Reference::try_from(format!(
4514 "localhost:{server_port}/test-push-stream-chunked-empty"
4515 ))
4516 .expect("failed to parse reference");
4517
4518 let err = client
4519 .push_blob_stream_chunked(&reference, data_stream)
4520 .await
4521 .expect_err("expected error when pushing an empty stream");
4522
4523 assert!(matches!(err, OciDistributionError::PushNoDataError));
4524 }
4525
4526 #[cfg(feature = "test-registry")]
4532 async fn push_minimal_manifest(
4533 client: &Client,
4534 reference: &Reference,
4535 artifact_type: Option<&str>,
4536 ) -> String {
4537 let config_data = b"{}";
4539 let config_digest = sha256_digest(config_data);
4540 client
4541 .push_blob(reference, config_data.as_slice(), &config_digest)
4542 .await
4543 .expect("failed to push config blob");
4544
4545 let manifest = OciImageManifest {
4546 schema_version: 2,
4547 media_type: Some(manifest::OCI_IMAGE_MEDIA_TYPE.to_string()),
4548 artifact_type: artifact_type.map(str::to_string),
4549 config: OciDescriptor {
4550 media_type: manifest::IMAGE_CONFIG_MEDIA_TYPE.to_string(),
4551 digest: config_digest.clone(),
4552 size: config_data.len() as i64,
4553 ..Default::default()
4554 },
4555 layers: vec![],
4556 subject: None,
4557 annotations: None,
4558 };
4559
4560 let oci_manifest = OciManifest::Image(manifest);
4561 client
4562 .push_manifest(reference, &oci_manifest)
4563 .await
4564 .expect("failed to push manifest")
4565 .rsplit('/')
4567 .next()
4568 .expect("manifest URL has no digest component")
4569 .to_string()
4570 }
4571
4572 #[tokio::test]
4586 #[cfg(feature = "test-registry")]
4587 async fn test_pull_referrers_with_tag_schema_fallback() {
4588 let test_container = registry_image()
4589 .start()
4590 .await
4591 .expect("Failed to start registry container");
4592 let port = test_container
4593 .get_host_port_ipv4(5000)
4594 .await
4595 .expect("Failed to get port");
4596
4597 let client = Client::new(ClientConfig {
4598 protocol: ClientProtocol::HttpsExcept(vec![format!("localhost:{port}")]),
4599 ..Default::default()
4600 });
4601
4602 let repo = format!("localhost:{port}/referrers-test");
4603
4604 let target_ref: Reference = format!("{repo}:target").parse().unwrap();
4606 client
4607 .auth(
4608 &target_ref,
4609 &RegistryAuth::Anonymous,
4610 RegistryOperation::Push,
4611 )
4612 .await
4613 .expect("failed to authenticate for push");
4614 let target_digest = push_minimal_manifest(&client, &target_ref, None).await;
4615
4616 const SIG_ARTIFACT_TYPE: &str = "application/vnd.test.sig";
4623 const SBOM_ARTIFACT_TYPE: &str = "application/vnd.test.sbom";
4624
4625 let sig_ref: Reference = format!("{repo}:sig").parse().unwrap();
4627 let sig_digest = push_minimal_manifest(&client, &sig_ref, Some(SIG_ARTIFACT_TYPE)).await;
4628
4629 let sbom_ref: Reference = format!("{repo}:sbom").parse().unwrap();
4630 let sbom_digest = push_minimal_manifest(&client, &sbom_ref, Some(SBOM_ARTIFACT_TYPE)).await;
4631
4632 let (sig_raw, _) = client
4634 .pull_manifest_raw(
4635 &sig_ref,
4636 &RegistryAuth::Anonymous,
4637 MIME_TYPES_DISTRIBUTION_MANIFEST,
4638 )
4639 .await
4640 .expect("failed to pull sig manifest raw");
4641 let sig_size = sig_raw.len() as i64;
4642
4643 let (sbom_raw, _) = client
4644 .pull_manifest_raw(
4645 &sbom_ref,
4646 &RegistryAuth::Anonymous,
4647 MIME_TYPES_DISTRIBUTION_MANIFEST,
4648 )
4649 .await
4650 .expect("failed to pull sbom manifest raw");
4651 let sbom_size = sbom_raw.len() as i64;
4652
4653 let referrers_index = OciImageIndex {
4654 schema_version: 2,
4655 media_type: Some(manifest::OCI_IMAGE_INDEX_MEDIA_TYPE.to_string()),
4656 artifact_type: None,
4657 annotations: None,
4658 manifests: vec![
4659 ImageIndexEntry {
4660 media_type: manifest::OCI_IMAGE_MEDIA_TYPE.to_string(),
4661 digest: sig_digest,
4662 size: sig_size,
4663 artifact_type: Some(SIG_ARTIFACT_TYPE.to_string()),
4664 platform: None,
4665 annotations: None,
4666 },
4667 ImageIndexEntry {
4668 media_type: manifest::OCI_IMAGE_MEDIA_TYPE.to_string(),
4669 digest: sbom_digest,
4670 size: sbom_size,
4671 artifact_type: Some(SBOM_ARTIFACT_TYPE.to_string()),
4672 platform: None,
4673 annotations: None,
4674 },
4675 ],
4676 };
4677
4678 let fallback_tag = target_digest.replace(':', "-");
4679 let tag_ref: Reference = format!("{repo}:{fallback_tag}").parse().unwrap();
4680 client
4681 .push_manifest(&tag_ref, &OciManifest::ImageIndex(referrers_index))
4682 .await
4683 .expect("failed to push referrers tag index");
4684
4685 let digest_ref = Reference::with_digest(
4687 format!("localhost:{port}"),
4688 "referrers-test".to_string(),
4689 target_digest.clone(),
4690 );
4691 client
4692 .auth(
4693 &digest_ref,
4694 &RegistryAuth::Anonymous,
4695 RegistryOperation::Pull,
4696 )
4697 .await
4698 .expect("failed to authenticate for pull");
4699
4700 let index = client
4701 .pull_referrers(&digest_ref, None)
4702 .await
4703 .expect("pull_referrers failed");
4704 assert_eq!(
4705 index.manifests.len(),
4706 2,
4707 "expected 2 referrers (unfiltered), got {:?}",
4708 index.manifests
4709 );
4710
4711 let index_filtered = client
4713 .pull_referrers(&digest_ref, Some(SIG_ARTIFACT_TYPE))
4714 .await
4715 .expect("pull_referrers with artifact_type filter failed");
4716 assert_eq!(
4717 index_filtered.manifests.len(),
4718 1,
4719 "expected 1 referrer after filtering by {SIG_ARTIFACT_TYPE}, got {:?}",
4720 index_filtered.manifests
4721 );
4722 assert_eq!(
4723 index_filtered.manifests[0].artifact_type.as_deref(),
4724 Some(SIG_ARTIFACT_TYPE),
4725 );
4726 }
4727
4728 #[tokio::test]
4732 #[cfg(feature = "test-registry")]
4733 async fn test_pull_referrers_no_tag_schema() {
4734 let test_container = registry_image()
4735 .start()
4736 .await
4737 .expect("Failed to start registry container");
4738 let port = test_container
4739 .get_host_port_ipv4(5000)
4740 .await
4741 .expect("Failed to get port");
4742
4743 let client = Client::new(ClientConfig {
4744 protocol: ClientProtocol::HttpsExcept(vec![format!("localhost:{port}")]),
4745 ..Default::default()
4746 });
4747
4748 let repo = format!("localhost:{port}/referrers-none-test");
4749
4750 let target_ref: Reference = format!("{repo}:target").parse().unwrap();
4752 client
4753 .auth(
4754 &target_ref,
4755 &RegistryAuth::Anonymous,
4756 RegistryOperation::Push,
4757 )
4758 .await
4759 .expect("failed to authenticate for push");
4760 let target_digest = push_minimal_manifest(&client, &target_ref, None).await;
4761
4762 let digest_ref = Reference::with_digest(
4763 format!("localhost:{port}"),
4764 "referrers-none-test".to_string(),
4765 target_digest,
4766 );
4767 client
4768 .auth(
4769 &digest_ref,
4770 &RegistryAuth::Anonymous,
4771 RegistryOperation::Pull,
4772 )
4773 .await
4774 .expect("failed to authenticate for pull");
4775
4776 let index = client
4777 .pull_referrers(&digest_ref, None)
4778 .await
4779 .expect("pull_referrers should succeed (returning empty index)");
4780 assert!(
4781 index.manifests.is_empty(),
4782 "expected empty referrers index, got {:?}",
4783 index.manifests
4784 );
4785 }
4786}