docker_registry/v2/
mod.rs1use 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#[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 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 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 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 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 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 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 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 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}