1use std::{iter::FromIterator, str::FromStr};
2
3use log::{debug, trace};
4use reqwest::{self, StatusCode, Url, header};
5
6use crate::{
7 errors::{Error, Result},
8 mediatypes,
9 v2::*,
10};
11
12mod manifest_schema1;
13pub use self::manifest_schema1::*;
14
15mod manifest_schema2;
16pub use self::manifest_schema2::{
17 ConfigBlob, ManifestList, ManifestObj, ManifestSchema2, ManifestSchema2Spec, Platform,
18};
19
20impl Client {
21 pub async fn get_manifest(&self, name: &str, reference: &str) -> Result<Manifest> {
26 self
27 .get_manifest_and_ref(name, reference)
28 .await
29 .map(|(manifest, _)| manifest)
30 }
31
32 pub async fn get_manifest_and_ref(&self, name: &str, reference: &str) -> Result<(Manifest, Option<String>)> {
37 let url = self.build_url(name, reference)?;
38
39 let accept_headers = build_accept_headers(&self.accepted_types);
40
41 let client_spare0 = self.clone();
42
43 let res = self
44 .build_reqwest(Method::GET, url.clone())
45 .headers(accept_headers)
46 .send()
47 .await?;
48
49 let status = res.status();
50 trace!("GET '{}' status: {:?}", res.url(), status);
51
52 match status {
53 StatusCode::OK => {}
54 _ => return Err(ApiErrors::from(res).await),
55 }
56
57 let headers = res.headers();
58 let content_digest = match headers.get("docker-content-digest") {
59 Some(content_digest_value) => Some(content_digest_value.to_str()?.to_string()),
60 None => {
61 debug!("cannot find manifestref in headers");
62 None
63 }
64 };
65
66 let header_content_type = headers.get(header::CONTENT_TYPE);
67 let media_type = evaluate_media_type(header_content_type, &url)?;
68
69 trace!("content-type: {header_content_type:?}, media-type: {media_type:?}");
70
71 match media_type {
72 mediatypes::MediaTypes::ManifestV2S1Signed => Ok((
73 res.json::<ManifestSchema1Signed>().await.map(Manifest::S1Signed)?,
74 content_digest,
75 )),
76 mediatypes::MediaTypes::ManifestV2S2 | mediatypes::MediaTypes::OciImageManifest => {
77 let m = res.json::<ManifestSchema2Spec>().await?;
78 Ok((
79 m.fetch_config_blob(client_spare0, name.to_string())
80 .await
81 .map(Manifest::S2)?,
82 content_digest,
83 ))
84 }
85 mediatypes::MediaTypes::ManifestList | mediatypes::MediaTypes::OciImageIndexV1 => {
86 Ok((res.json::<ManifestList>().await.map(Manifest::ML)?, content_digest))
87 }
88 unsupported => Err(Error::UnsupportedMediaType(unsupported)),
89 }
90 }
91
92 fn build_url(&self, name: &str, reference: &str) -> Result<Url> {
93 let ep = format!("{}/v2/{}/manifests/{}", self.base_url.clone(), name, reference);
94 reqwest::Url::parse(&ep).map_err(Error::from)
95 }
96
97 pub async fn get_manifestref(&self, name: &str, reference: &str) -> Result<Option<String>> {
99 let url = self.build_url(name, reference)?;
100
101 let accept_headers = build_accept_headers(&self.accepted_types);
102
103 let res = self
104 .build_reqwest(Method::HEAD, url)
105 .headers(accept_headers)
106 .send()
107 .await?;
108
109 let status = res.status();
110 trace!("HEAD '{}' status: {:?}", res.url(), status);
111
112 match status {
113 StatusCode::OK => {}
114 _ => return Err(ApiErrors::from(res).await),
115 }
116
117 let headers = res.headers();
118 let content_digest = match headers.get("docker-content-digest") {
119 Some(content_digest_value) => Some(content_digest_value.to_str()?.to_string()),
120 None => {
121 debug!("cannot find manifestref in headers");
122 None
123 }
124 };
125 Ok(content_digest)
126 }
127
128 pub async fn has_manifest(
133 &self,
134 name: &str,
135 reference: &str,
136 mediatypes: Option<&[&str]>,
137 ) -> Result<Option<mediatypes::MediaTypes>> {
138 let url = self.build_url(name, reference)?;
139 let accept_types = match mediatypes {
140 None => {
141 let m = mediatypes::MediaTypes::ManifestV2S2.to_mime();
142 vec![m]
143 }
144 Some(v) => to_mimes(v),
145 };
146
147 let mut accept_headers = header::HeaderMap::with_capacity(accept_types.len());
148 for accept_type in accept_types {
149 let header_value =
150 header::HeaderValue::from_str(accept_type.as_ref()).expect("mime type is always valid header value");
151 accept_headers.insert(header::ACCEPT, header_value);
152 }
153
154 trace!("HEAD {url:?}");
155
156 let r = self
157 .build_reqwest(Method::HEAD, url.clone())
158 .headers(accept_headers)
159 .send()
160 .await
161 .map_err(Error::from)?;
162
163 let status = r.status();
164
165 trace!("Manifest check status '{:?}', headers '{:?}", r.status(), r.headers(),);
166
167 match status {
168 StatusCode::MOVED_PERMANENTLY | StatusCode::TEMPORARY_REDIRECT | StatusCode::FOUND | StatusCode::OK => {
169 let media_type = evaluate_media_type(r.headers().get(header::CONTENT_TYPE), r.url())?;
170 trace!("Manifest media-type: {media_type:?}");
171 Ok(Some(media_type))
172 }
173 StatusCode::NOT_FOUND => Ok(None),
174 _ => Err(ApiErrors::from(r).await),
175 }
176 }
177}
178
179fn to_mimes(v: &[&str]) -> Vec<mime::Mime> {
180 v.iter()
181 .filter_map(|x| {
182 let mtype = mediatypes::MediaTypes::from_str(x);
183 match mtype {
184 Ok(m) => Some(m.to_mime()),
185 _ => None,
186 }
187 })
188 .collect()
189}
190
191fn evaluate_media_type(
193 content_type: Option<&reqwest::header::HeaderValue>,
194 url: &Url,
195) -> Result<mediatypes::MediaTypes> {
196 let header_content_type = content_type
197 .map(|hv| hv.to_str())
198 .map(std::result::Result::unwrap_or_default);
199
200 let is_pulp_based = url.path().starts_with("/pulp/docker/v2");
201
202 match (header_content_type, is_pulp_based) {
203 (Some(header_value), false) => mediatypes::MediaTypes::from_str(header_value).map_err(strum::ParseError::into),
204 (None, false) => Err(Error::MediaTypeSniff),
205 (Some(header_value), true) => {
206 match header_value {
208 "application/x-troff-man" => {
209 trace!("Applying workaround for pulp-based registries, e.g. Satellite");
210 mediatypes::MediaTypes::from_str("application/vnd.docker.distribution.manifest.v1+prettyjws")
211 .map_err(strum::ParseError::into)
212 }
213 _ => {
214 debug!(
215 "Received content-type '{header_value}' from pulp-based registry. Feeling lucky and trying to parse it...",
216 );
217 mediatypes::MediaTypes::from_str(header_value).map_err(strum::ParseError::into)
218 }
219 }
220 }
221 (None, true) => {
222 trace!("Applying workaround for pulp-based registries, e.g. Satellite");
223 mediatypes::MediaTypes::from_str("application/vnd.docker.distribution.manifest.v1+prettyjws")
224 .map_err(strum::ParseError::into)
225 }
226 }
227}
228
229fn build_accept_headers(accepted_types: &[(MediaTypes, Option<f64>)]) -> header::HeaderMap {
230 let accepted_types_string = accepted_types
231 .iter()
232 .map(|(ty, q)| {
233 format!(
234 "{}{}",
235 ty,
236 match q {
237 None => String::default(),
238 Some(v) => format!("; q={v}"),
239 }
240 )
241 })
242 .collect::<Vec<_>>()
243 .join(",");
244
245 header::HeaderMap::from_iter(vec![(
246 header::ACCEPT,
247 header::HeaderValue::from_str(&accepted_types_string)
248 .expect("should be always valid because both float and mime type only use allowed ASCII chard"),
249 )])
250}
251
252#[derive(Debug)]
254pub enum Manifest {
255 S1Signed(manifest_schema1::ManifestSchema1Signed),
256 S2(manifest_schema2::ManifestSchema2),
257 ML(manifest_schema2::ManifestList),
258}
259
260#[derive(Debug, thiserror::Error)]
261pub enum ManifestError {
262 #[error("no architecture in manifest")]
263 NoArchitecture,
264 #[error("architecture mismatch")]
265 ArchitectureMismatch,
266 #[error("manifest {0} does not support the 'layer_digests' method")]
267 LayerDigestsUnsupported(String),
268 #[error("manifest {0} does not support the 'architecture' method")]
269 ArchitectureNotSupported(String),
270}
271
272impl Manifest {
273 pub fn layers_digests(&self, architecture: Option<&str>) -> Result<Vec<String>> {
289 match (self, self.architectures(), architecture) {
290 (Manifest::S1Signed(m), _, None) => Ok(m.get_layers()),
291 (Manifest::S2(m), _, None) => Ok(m.get_layers()),
292 (Manifest::S1Signed(m), Ok(ref self_architectures), Some(ref a)) => {
293 let self_a = self_architectures.first().ok_or(ManifestError::NoArchitecture)?;
294 if self_a != a {
295 return Err(ManifestError::ArchitectureMismatch.into());
296 }
297 Ok(m.get_layers())
298 }
299 (Manifest::S2(m), Ok(ref self_architectures), Some(ref a)) => {
300 let self_a = self_architectures.first().ok_or(ManifestError::NoArchitecture)?;
301 if self_a != a {
302 return Err(ManifestError::ArchitectureMismatch.into());
303 }
304 Ok(m.get_layers())
305 }
306 (Manifest::ML(m), _, _) => Ok(m.get_digests()),
307 _ => Err(ManifestError::LayerDigestsUnsupported(format!("{self:?}")).into()),
308 }
309 }
310
311 pub fn architectures(&self) -> Result<Vec<String>> {
313 match self {
314 Manifest::S1Signed(m) => Ok([m.architecture.clone()].to_vec()),
315 Manifest::S2(m) => Ok([m.architecture()].to_vec()),
316 Manifest::ML(m) => Ok(m.architectures()),
317 }
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use test_case::test_case;
324
325 use super::*;
326 use crate::v2::Client;
327
328 #[test_case("not-gcr.io" => "application/vnd.docker.distribution.manifest.v2+json; q=0.5,application/vnd.docker.distribution.manifest.v1+prettyjws; q=0.4,application/vnd.docker.distribution.manifest.list.v2+json; q=0.5,application/vnd.oci.image.manifest.v1+json; q=0.5,application/vnd.oci.image.index.v1+json; q=0.5"; "Not gcr registry")]
329 #[test_case("gcr.io" => "application/vnd.docker.distribution.manifest.v2+json,application/vnd.docker.distribution.manifest.v1+prettyjws,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.oci.image.index.v1+json"; "gcr.io")]
330 #[test_case("foobar.gcr.io" => "application/vnd.docker.distribution.manifest.v2+json,application/vnd.docker.distribution.manifest.v1+prettyjws,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.oci.image.index.v1+json"; "Custom gcr.io registry")]
331 #[test_case("foobar.k8s.io" => "application/vnd.docker.distribution.manifest.v2+json,application/vnd.docker.distribution.manifest.v1+prettyjws,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.oci.image.index.v1+json"; "Custom k8s.io registry")]
332 fn gcr_io_accept_headers(registry: &str) -> String {
333 let client_builder = Client::configure().registry(registry);
334 let client = client_builder.build().unwrap();
335 let header_map = build_accept_headers(&client.accepted_types);
336 header_map.get(header::ACCEPT).unwrap().to_str().unwrap().to_string()
337 }
338
339 #[test_case(None => "application/vnd.docker.distribution.manifest.v2+json; q=0.5,application/vnd.docker.distribution.manifest.v1+prettyjws; q=0.4,application/vnd.docker.distribution.manifest.list.v2+json; q=0.5,application/vnd.oci.image.manifest.v1+json; q=0.5,application/vnd.oci.image.index.v1+json; q=0.5"; "Default settings")]
340 #[test_case(Some(vec![
341 (MediaTypes::ManifestV2S2, Some(0.5)),
342 (MediaTypes::ManifestV2S1Signed, Some(0.2)),
343 (MediaTypes::ManifestList, Some(0.5)),
344 (MediaTypes::OciImageManifest, Some(0.2)),
345 ]) => "application/vnd.docker.distribution.manifest.v2+json; q=0.5,application/vnd.docker.distribution.manifest.v1+prettyjws; q=0.2,application/vnd.docker.distribution.manifest.list.v2+json; q=0.5,application/vnd.oci.image.manifest.v1+json; q=0.2"; "Custom accept types with weight")]
346 #[test_case(Some(vec![
347 (MediaTypes::ManifestV2S2, None),
348 (MediaTypes::ManifestList, None),
349 (MediaTypes::OciImageIndexV1, None),
350 ]) => "application/vnd.docker.distribution.manifest.v2+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.index.v1+json"; "Custom accept types, no weight")]
351 fn custom_accept_headers(accept_headers: Option<Vec<(MediaTypes, Option<f64>)>>) -> String {
352 let registry = "https://example.com";
353
354 let client_builder = Client::configure().registry(registry).accepted_types(accept_headers);
355 let client = client_builder.build().unwrap();
356 let header_map = build_accept_headers(&client.accepted_types);
357 header_map.get(header::ACCEPT).unwrap().to_str().unwrap().to_string()
358 }
359}