Skip to main content

docker_registry/v2/
mod.rs

1//! Client library for Docker Registry API v2.
2//!
3//! This module provides a `Client` which can be used to list
4//! images and tags, to check for the presence of blobs (manifests,
5//! layers and other objects) by digest, and to retrieve them.
6//!
7//! ## Example
8//!
9//! ```rust,no_run
10//! # use tokio;
11//!
12//! # #[tokio::main]
13//! # async fn main() {
14//! # async fn run() -> docker_registry::errors::Result<()> {
15//! #
16//! use docker_registry::v2::Client;
17//!
18//! // Retrieve an image manifest.
19//! let client = Client::configure().registry("quay.io").build()?;
20//! let manifest = client.get_manifest("coreos/etcd", "v3.1.0").await?;
21//! #
22//! # Ok(())
23//! # };
24//! # run().await.unwrap();
25//! # }
26//! ```
27
28use std::fmt;
29
30use futures::prelude::*;
31use log::trace;
32use reqwest::{Method, Response, StatusCode, Url};
33use serde::{Deserialize, Serialize};
34
35use crate::{
36  errors::{self, *},
37  mediatypes::MediaTypes,
38};
39
40mod config;
41pub use self::config::Config;
42
43mod catalog;
44
45mod auth;
46pub use auth::WwwHeaderParseError;
47
48pub mod manifest;
49
50mod tags;
51
52mod blobs;
53
54mod referrers;
55
56mod content_digest;
57pub(crate) use self::content_digest::ContentDigest;
58pub use self::content_digest::ContentDigestError;
59
60/// A Client to make outgoing API requests to a registry.
61#[derive(Clone, Debug)]
62pub struct Client {
63  base_url: String,
64  credentials: Option<(String, String)>,
65  user_agent: Option<String>,
66  auth: Option<auth::Auth>,
67  client: reqwest::Client,
68  accepted_types: Vec<(MediaTypes, Option<f64>)>,
69}
70
71impl Client {
72  pub fn configure() -> Config {
73    Config::default()
74  }
75
76  /// Ensure remote registry supports v2 API.
77  pub async fn ensure_v2_registry(self) -> Result<Self> {
78    if !self.is_v2_supported().await? {
79      Err(Error::V2NotSupported)
80    } else {
81      Ok(self)
82    }
83  }
84
85  /// Check whether remote registry supports v2 API.
86  pub async fn is_v2_supported(&self) -> Result<bool> {
87    match self.is_v2_supported_and_authorized().await {
88      Ok((v2_supported, _)) => Ok(v2_supported),
89      Err(crate::Error::UnexpectedHttpStatus(_)) => Ok(false),
90      Err(e) => Err(e),
91    }
92  }
93
94  /// Check whether remote registry supports v2 API and `self` is authorized.
95  /// Authorized means to successfully GET the `/v2` endpoint on the remote registry.
96  pub async fn is_v2_supported_and_authorized(&self) -> Result<(bool, bool)> {
97    let api_header = "Docker-Distribution-API-Version";
98    let api_version = "registry/2.0";
99
100    // GET request to bare v2 endpoint.
101    let v2_endpoint = format!("{}/v2/", self.base_url);
102    let request = reqwest::Url::parse(&v2_endpoint).map(|url| {
103      trace!("GET {url:?}");
104      self.build_reqwest(Method::GET, url)
105    })?;
106
107    let response = request.send().await?;
108
109    match (response.status(), response.headers().get(api_header)) {
110      (StatusCode::OK, Some(x)) => Ok((x == api_version, true)),
111      (StatusCode::UNAUTHORIZED, Some(x)) => Ok((x == api_version, false)),
112      (s, v) => {
113        trace!("Got unexpected status {s}, header version {v:?}");
114        Err(crate::Error::UnexpectedHttpStatus(s))
115      }
116    }
117  }
118
119  /// Takes reqwest's async RequestBuilder and injects an authentication header if a token is present
120  fn build_reqwest(&self, method: Method, url: Url) -> reqwest::RequestBuilder {
121    let mut builder = self.client.request(method, url);
122
123    if let Some(auth) = &self.auth {
124      builder = auth.add_auth_headers(builder);
125    };
126
127    if let Some(ua) = &self.user_agent {
128      builder = builder.header(reqwest::header::USER_AGENT, ua.as_str());
129    };
130
131    builder
132  }
133}
134
135#[derive(Clone, Debug, Default, Deserialize, Serialize)]
136pub struct ApiError {
137  code: String,
138  message: Option<String>,
139  detail: Option<Box<serde_json::value::RawValue>>,
140}
141
142#[derive(Clone, Debug, Default, Deserialize, Serialize, thiserror::Error)]
143pub struct ApiErrors {
144  errors: Option<Vec<ApiError>>,
145}
146
147impl ApiError {
148  /// Return the API error code.
149  pub fn code(&self) -> &str {
150    &self.code
151  }
152
153  pub fn message(&self) -> Option<&str> {
154    self.message.as_deref()
155  }
156}
157impl fmt::Display for ApiError {
158  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159    write!(f, "({})", self.code)?;
160    if let Some(message) = &self.message {
161      write!(f, ", message: {message}")?;
162    }
163    if let Some(detail) = &self.detail {
164      write!(f, ", detail: {detail}")?;
165    }
166    Ok(())
167  }
168}
169
170impl ApiErrors {
171  /// Create a new ApiErrors from a API Json response.
172  /// Returns an ApiError if the content is a valid per
173  /// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#error-codes
174  pub async fn from(r: Response) -> errors::Error {
175    match r.json::<ApiErrors>().await {
176      Ok(e) => errors::Error::Api(e),
177      Err(e) => errors::Error::Reqwest(e),
178    }
179  }
180
181  /// Returns the errors returned by the API.
182  pub fn errors(&self) -> &Option<Vec<ApiError>> {
183    &self.errors
184  }
185}
186
187impl fmt::Display for ApiErrors {
188  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189    if self.errors.is_none() {
190      return Ok(());
191    }
192    for error in self.errors.as_ref().unwrap().iter() {
193      write!(f, "({error})")?
194    }
195    Ok(())
196  }
197}