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 content_digest;
55pub(crate) use self::content_digest::ContentDigest;
56pub use self::content_digest::ContentDigestError;
57
58#[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 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 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 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 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 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 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 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 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}