Skip to content

Commit abb58c1

Browse files
kobuttonvrutkovs
authored andcommitted
Modified Tag Pagination to get all query parameters from Link Header.
This ensures that this library will work with registries that do not use a "next_page" query parameter as next_page seems to be non-standard and does not align with the dockerv2 api spec as defined: https://docs.docker.com/registry/spec/api/#pagination-1
1 parent 37acecb commit abb58c1

File tree

4 files changed

+158
-17
lines changed

4 files changed

+158
-17
lines changed

src/v2/tags.rs

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ impl Client {
5050
) -> Result<(TagsChunk, Option<String>)> {
5151
let url_paginated = match (paginate, link) {
5252
(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),
53+
(None, Some(l)) => format!("{}?{}", base_url, l),
54+
(Some(_p), Some(l)) => format!("{}?{}", base_url, l),
5555
_ => base_url.to_string(),
5656
};
5757
let url = Url::parse(&url_paginated)?;
@@ -108,16 +108,9 @@ fn parse_link(hdr: Option<&header::HeaderValue>) -> Option<String> {
108108

109109
// Query parameters for next page URL.
110110
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.is_empty() => 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.first().cloned() {
120-
Some(v) if !v.is_empty() => Some(v.to_string()),
111+
let query: Vec<&str> = uri.splitn(2, "?").collect();
112+
match query.get(1) { //use the entire query param string since some registries have different ways of pagination
113+
Some(v) if !v.is_empty() => Some(v.to_string()),
121114
_ => None,
122115
}
123116
}

tests/mock/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ mod api_version;
22
mod base_client;
33
mod blobs_download;
44
mod catalog;
5-
mod tags;
5+
mod tags_dockerv2;
6+
mod tags_quay;

tests/mock/tags_dockerv2.rs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
extern crate dkregistry;
2+
extern crate futures;
3+
extern crate mockito;
4+
extern crate tokio;
5+
6+
use self::futures::StreamExt;
7+
use self::mockito::mock;
8+
use self::tokio::runtime::Runtime;
9+
10+
#[test]
11+
fn test_dockerv2_tags_simple() {
12+
let name = "repo";
13+
let tags = r#"{"name": "repo", "tags": [ "t1", "t2" ]}"#;
14+
15+
let ep = format!("/v2/{}/tags/list", name);
16+
let addr = mockito::server_address().to_string();
17+
let _m = mock("GET", ep.as_str())
18+
.with_status(200)
19+
.with_header("Content-Type", "application/json")
20+
.with_body(tags)
21+
.create();
22+
23+
let runtime = Runtime::new().unwrap();
24+
let dclient = dkregistry::v2::Client::configure()
25+
.registry(&addr)
26+
.insecure_registry(true)
27+
.username(None)
28+
.password(None)
29+
.build()
30+
.unwrap();
31+
32+
let futcheck = dclient.get_tags(name, None);
33+
34+
let res = runtime.block_on(futcheck.map(Result::unwrap).collect::<Vec<_>>());
35+
assert_eq!(res.get(0).unwrap(), &String::from("t1"));
36+
assert_eq!(res.get(1).unwrap(), &String::from("t2"));
37+
38+
mockito::reset();
39+
}
40+
41+
#[test]
42+
fn test_dockerv2_tags_paginate() {
43+
let name = "repo";
44+
let tags_p1 = r#"{"name": "repo", "tags": [ "t1" ]}"#;
45+
let tags_p2 = r#"{"name": "repo", "tags": [ "t2" ]}"#;
46+
47+
let ep1 = format!("/v2/{}/tags/list?n=1", name);
48+
let ep2 = format!("/v2/{}/tags/list?n=1&last=t1", name);
49+
let addr = mockito::server_address().to_string();
50+
let _m1 = mock("GET", ep1.as_str())
51+
.with_status(200)
52+
.with_header(
53+
"Link",
54+
&format!(
55+
r#"<{}/v2/_tags?n=1&last=t1>; rel="next""#,
56+
mockito::server_url()
57+
),
58+
)
59+
.with_header("Content-Type", "application/json")
60+
.with_body(tags_p1)
61+
.create();
62+
let _m2 = mock("GET", ep2.as_str())
63+
.with_status(200)
64+
.with_header("Content-Type", "application/json")
65+
.with_body(tags_p2)
66+
.create();
67+
68+
let runtime = Runtime::new().unwrap();
69+
let dclient = dkregistry::v2::Client::configure()
70+
.registry(&addr)
71+
.insecure_registry(true)
72+
.username(None)
73+
.password(None)
74+
.build()
75+
.unwrap();
76+
77+
let next = Box::pin(dclient.get_tags(name, Some(1)).map(Result::unwrap));
78+
79+
let (first_tag, stream_rest) = runtime.block_on(next.into_future());
80+
assert_eq!(first_tag.unwrap(), "t1".to_owned());
81+
82+
let (second_tag, stream_rest) = runtime.block_on(stream_rest.into_future());
83+
assert_eq!(second_tag.unwrap(), "t2".to_owned());
84+
85+
let (end, _) = runtime.block_on(stream_rest.into_future());
86+
if end.is_some() {
87+
panic!("end is some: {:?}", end);
88+
}
89+
90+
mockito::reset();
91+
}
92+
93+
#[test]
94+
fn test_dockerv2_tags_404() {
95+
let name = "repo";
96+
let ep = format!("/v2/{}/tags/list", name);
97+
let addr = mockito::server_address().to_string();
98+
let _m = mock("GET", ep.as_str())
99+
.with_status(404)
100+
.with_header("Content-Type", "application/json")
101+
.create();
102+
103+
let runtime = Runtime::new().unwrap();
104+
let dclient = dkregistry::v2::Client::configure()
105+
.registry(&addr)
106+
.insecure_registry(true)
107+
.username(None)
108+
.password(None)
109+
.build()
110+
.unwrap();
111+
112+
let futcheck = dclient.get_tags(name, None);
113+
114+
let res = runtime.block_on(futcheck.collect::<Vec<_>>());
115+
assert!(res.get(0).unwrap().is_err());
116+
117+
mockito::reset();
118+
}
119+
120+
#[test]
121+
fn test_dockerv2_tags_missing_header() {
122+
let name = "repo";
123+
let tags = r#"{"name": "repo", "tags": [ "t1", "t2" ]}"#;
124+
let ep = format!("/v2/{}/tags/list", name);
125+
126+
let addr = mockito::server_address().to_string();
127+
let _m = mock("GET", ep.as_str())
128+
.with_status(200)
129+
.with_body(tags)
130+
.create();
131+
132+
let runtime = Runtime::new().unwrap();
133+
let dclient = dkregistry::v2::Client::configure()
134+
.registry(&addr)
135+
.insecure_registry(true)
136+
.username(None)
137+
.password(None)
138+
.build()
139+
.unwrap();
140+
141+
let futcheck = dclient.get_tags(name, None);
142+
143+
let res = runtime.block_on(futcheck.map(Result::unwrap).collect::<Vec<_>>());
144+
assert_eq!(vec!["t1", "t2"], res);
145+
146+
mockito::reset();
147+
}

tests/mock/tags.rs renamed to tests/mock/tags_quay.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use self::mockito::mock;
88
use self::tokio::runtime::Runtime;
99

1010
#[test]
11-
fn test_tags_simple() {
11+
fn test_quay_tags_simple() {
1212
let name = "repo";
1313
let tags = r#"{"name": "repo", "tags": [ "t1", "t2" ]}"#;
1414

@@ -39,7 +39,7 @@ fn test_tags_simple() {
3939
}
4040

4141
#[test]
42-
fn test_tags_paginate() {
42+
fn test_quay_tags_paginate() {
4343
let name = "repo";
4444
let tags_p1 = r#"{"name": "repo", "tags": [ "t1" ]}"#;
4545
let tags_p2 = r#"{"name": "repo", "tags": [ "t2" ]}"#;
@@ -91,7 +91,7 @@ fn test_tags_paginate() {
9191
}
9292

9393
#[test]
94-
fn test_tags_404() {
94+
fn test_quay_tags_404() {
9595
let name = "repo";
9696
let ep = format!("/v2/{}/tags/list", name);
9797
let addr = mockito::server_address().to_string();
@@ -118,7 +118,7 @@ fn test_tags_404() {
118118
}
119119

120120
#[test]
121-
fn test_tags_missing_header() {
121+
fn test_quay_tags_missing_header() {
122122
let name = "repo";
123123
let tags = r#"{"name": "repo", "tags": [ "t1", "t2" ]}"#;
124124
let ep = format!("/v2/{}/tags/list", name);

0 commit comments

Comments
 (0)