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 content_digest;
55pub(crate) use self::content_digest::ContentDigest;
56pub use self::content_digest::ContentDigestError;
57
58/// A Client to make outgoing API requests to a registry.
59#[derive(Clone, Debug)]
60pub struct Client {
61  base_url: String,
62  credentials: Option<(String, String)>,
63  user_agent: Option<String>,
64  auth: Option<auth::Auth>,
65  client: reqwest::Client,
66  accepted_types: Vec<(MediaTypes, Option<f64>)>,
67}
68
69impl Client {
70  pub fn configure() -> Config {
71    Config::default()
72  }
73
74  /// Ensure remote registry supports v2 API.
75  pub async fn ensure_v2_registry(self) -> Result<Self> {
76    if !self.is_v2_supported().await? {
77      Err(Error::V2NotSupported)
78    } else {
79      Ok(self)
80    }
81  }
82
83  /// Check whether remote registry supports v2 API.
84  pub async fn is_v2_supported(&self) -> Result<bool> {
85    match self.is_v2_supported_and_authorized().await {
86      Ok((v2_supported, _)) => Ok(v2_supported),
87      Err(crate::Error::UnexpectedHttpStatus(_)) => Ok(false),
88      Err(e) => Err(e),
89    }
90  }
91
92  /// Check whether remote registry supports v2 API and `self` is authorized.
93  /// Authorized means to successfully GET the `/v2` endpoint on the remote registry.
94  pub async fn is_v2_supported_and_authorized(&self) -> Result<(bool, bool)> {
95    let api_header = "Docker-Distribution-API-Version";
96    let api_version = "registry/2.0";
97
98    // GET request to bare v2 endpoint.
99    let v2_endpoint = format!("{}/v2/", self.base_url);
100    let request = reqwest::Url::parse(&v2_endpoint).map(|url| {
101      trace!("GET {url:?}");
102      self.build_reqwest(Method::GET, url)
103    })?;
104
105    let response = request.send().await?;
106
107    match (response.status(), response.headers().get(api_header)) {
108      (StatusCode::OK, Some(x)) => Ok((x == api_version, true)),
109      (StatusCode::UNAUTHORIZED, Some(x)) => Ok((x == api_version, false)),
110      (s, v) => {
111        trace!("Got unexpected status {s}, header version {v:?}");
112        Err(crate::Error::UnexpectedHttpStatus(s))
113      }
114    }
115  }
116
117  /// Takes reqwest's async RequestBuilder and injects an authentication header if a token is present
118  fn build_reqwest(&self, method: Method, url: Url) -> reqwest::RequestBuilder {
119    let mut builder = self.client.request(method, url);
120
121    if let Some(auth) = &self.auth {
122      builder = auth.add_auth_headers(builder);
123    };
124
125    if let Some(ua) = &self.user_agent {
126      builder = builder.header(reqwest::header::USER_AGENT, ua.as_str());
127    };
128
129    builder
130  }
131}
132
133#[derive(Debug, Default, Deserialize, Serialize)]
134pub struct ApiError {
135  code: String,
136  message: Option<String>,
137  detail: Option<Box<serde_json::value::RawValue>>,
138}
139
140#[derive(Debug, Default, Deserialize, Serialize, thiserror::Error)]
141pub struct ApiErrors {
142  errors: Option<Vec<ApiError>>,
143}
144
145impl ApiError {
146  /// Return the API error code.
147  pub fn code(&self) -> &str {
148    &self.code
149  }
150
151  pub fn message(&self) -> Option<&str> {
152    self.message.as_deref()
153  }
154}
155impl fmt::Display for ApiError {
156  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157    write!(f, "({})", self.code)?;
158    if let Some(message) = &self.message {
159      write!(f, ", message: {message}")?;
160    }
161    if let Some(detail) = &self.detail {
162      write!(f, ", detail: {detail}")?;
163    }
164    Ok(())
165  }
166}
167
168impl ApiErrors {
169  /// Create a new ApiErrors from a API Json response.
170  /// Returns an ApiError if the content is a valid per
171  /// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#error-codes
172  pub async fn from(r: Response) -> errors::Error {
173    match r.json::<ApiErrors>().await {
174      Ok(e) => errors::Error::Api(e),
175      Err(e) => errors::Error::Reqwest(e),
176    }
177  }
178
179  /// Returns the errors returned by the API.
180  pub fn errors(&self) -> &Option<Vec<ApiError>> {
181    &self.errors
182  }
183}
184
185impl fmt::Display for ApiErrors {
186  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187    if self.errors.is_none() {
188      return Ok(());
189    }
190    for error in self.errors.as_ref().unwrap().iter() {
191      write!(f, "({error})")?
192    }
193    Ok(())
194  }
195}