Skip to main content

dkregistry/v2/
tags.rs

1use crate::errors::Result;
2use crate::v2::*;
3use async_stream::try_stream;
4use reqwest::{self, header, Url};
5use std::fmt::Debug;
6
7/// A chunk of tags for an image.
8///
9/// This contains a non-strict subset of the whole list of tags
10/// for an image, depending on pagination option at request time.
11#[derive(Debug, Default, Deserialize, Serialize)]
12struct TagsChunk {
13    /// Image repository name.
14    name: String,
15    /// Subset of tags.
16    tags: Vec<String>,
17}
18
19impl Client {
20    /// List existing tags for an image.
21    pub fn get_tags<'a, 'b: 'a, 'c: 'a>(
22        &'b self,
23        name: &'c str,
24        paginate: Option<u32>,
25    ) -> impl Stream<Item = Result<String>> + 'a {
26        let base_url = format!("{}/v2/{}/tags/list", self.base_url, name);
27        let mut link: Option<String> = None;
28
29        try_stream! {
30            loop {
31                let (tags_chunk, last) = self.fetch_tags_chunk(paginate, &base_url, &link).await?;
32                for tag in tags_chunk.tags {
33                    yield tag;
34                }
35
36                link = match last {
37                    None => break,
38                    Some(ref s) if s == "" => None,
39                    s => s,
40                };
41            }
42        }
43    }
44
45    async fn fetch_tags_chunk(
46        &self,
47        paginate: Option<u32>,
48        base_url: &str,
49        link: &Option<String>,
50    ) -> Result<(TagsChunk, Option<String>)> {
51        let url_paginated = match (paginate, link) {
52            (Some(p), None) => format!("{}?n={}", base_url, p),
53            (None, Some(l)) => format!("{}?next_page={}", base_url, l),
54            (Some(p), Some(l)) => format!("{}?n={}&next_page={}", base_url, p, l),
55            _ => base_url.to_string(),
56        };
57        let url = Url::parse(&url_paginated)?;
58
59        let resp = self
60            .build_reqwest(Method::GET, url.clone())
61            .header(header::ACCEPT, "application/json")
62            .send()
63            .await?
64            .error_for_status()?;
65
66        // ensure the CONTENT_TYPE header is application/json
67        let ct_hdr = resp.headers().get(header::CONTENT_TYPE).cloned();
68
69        trace!("page url {:?}", ct_hdr);
70
71        let ok = match ct_hdr {
72            None => false,
73            Some(ref ct) => ct.to_str()?.starts_with("application/json"),
74        };
75        if !ok {
76            // TODO:(steveeJ): Make this an error once Satellite
77            // returns the content type correctly
78            debug!("get_tags: wrong content type '{:?}', ignoring...", ct_hdr);
79        }
80
81        // extract the response body and parse the LINK header
82        let next = parse_link(resp.headers().get(header::LINK));
83        trace!("next_page {:?}", next);
84
85        let tags_chunk = resp.json::<TagsChunk>().await?;
86        Ok((tags_chunk, next))
87    }
88}
89
90/// Parse a `Link` header.
91///
92/// Format is described at https://docs.docker.com/registry/spec/api/#listing-image-tags#pagination.
93fn parse_link(hdr: Option<&header::HeaderValue>) -> Option<String> {
94    // TODO(lucab): this a brittle string-matching parser. Investigate
95    // whether there is a a common library to do this, in the future.
96
97    // Raw Header value bytes.
98    let hval = match hdr {
99        Some(v) => v,
100        None => return None,
101    };
102
103    // Header value string.
104    let sval = match hval.to_str() {
105        Ok(v) => v.to_owned(),
106        _ => return None,
107    };
108
109    // Query parameters for next page URL.
110    let uri = sval.trim_end_matches(">; rel=\"next\"");
111    let query: Vec<&str> = uri.splitn(2, "next_page=").collect();
112    let params = match query.get(1) {
113        Some(v) if *v != "" => v,
114        _ => return None,
115    };
116
117    // Last item in current page (pagination parameter).
118    let last: Vec<&str> = params.splitn(2, '&').collect();
119    match last.get(0).cloned() {
120        Some(v) if v != "" => Some(v.to_string()),
121        _ => None,
122    }
123}