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