Skip to content

Commit

Permalink
Modified Tag Pagination to get all query parameters from Link Header.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
kobutton authored and vrutkovs committed Mar 30, 2023
1 parent 37acecb commit abb58c1
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 17 deletions.
17 changes: 5 additions & 12 deletions src/v2/tags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ impl Client {
) -> Result<(TagsChunk, Option<String>)> {
let url_paginated = match (paginate, link) {
(Some(p), None) => format!("{}?n={}", base_url, p),
(None, Some(l)) => format!("{}?next_page={}", base_url, l),
(Some(p), Some(l)) => format!("{}?n={}&next_page={}", base_url, p, l),
(None, Some(l)) => format!("{}?{}", base_url, l),
(Some(_p), Some(l)) => format!("{}?{}", base_url, l),
_ => base_url.to_string(),
};
let url = Url::parse(&url_paginated)?;
Expand Down Expand Up @@ -108,16 +108,9 @@ fn parse_link(hdr: Option<&header::HeaderValue>) -> Option<String> {

// Query parameters for next page URL.
let uri = sval.trim_end_matches(">; rel=\"next\"");
let query: Vec<&str> = uri.splitn(2, "next_page=").collect();
let params = match query.get(1) {
Some(v) if !v.is_empty() => v,
_ => return None,
};

// Last item in current page (pagination parameter).
let last: Vec<&str> = params.splitn(2, '&').collect();
match last.first().cloned() {
Some(v) if !v.is_empty() => Some(v.to_string()),
let query: Vec<&str> = uri.splitn(2, "?").collect();
match query.get(1) { //use the entire query param string since some registries have different ways of pagination
Some(v) if !v.is_empty() => Some(v.to_string()),
_ => None,
}
}
3 changes: 2 additions & 1 deletion tests/mock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ mod api_version;
mod base_client;
mod blobs_download;
mod catalog;
mod tags;
mod tags_dockerv2;
mod tags_quay;
147 changes: 147 additions & 0 deletions tests/mock/tags_dockerv2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
extern crate dkregistry;
extern crate futures;
extern crate mockito;
extern crate tokio;

use self::futures::StreamExt;
use self::mockito::mock;
use self::tokio::runtime::Runtime;

#[test]
fn test_dockerv2_tags_simple() {
let name = "repo";
let tags = r#"{"name": "repo", "tags": [ "t1", "t2" ]}"#;

let ep = format!("/v2/{}/tags/list", name);
let addr = mockito::server_address().to_string();
let _m = mock("GET", ep.as_str())
.with_status(200)
.with_header("Content-Type", "application/json")
.with_body(tags)
.create();

let runtime = Runtime::new().unwrap();
let dclient = dkregistry::v2::Client::configure()
.registry(&addr)
.insecure_registry(true)
.username(None)
.password(None)
.build()
.unwrap();

let futcheck = dclient.get_tags(name, None);

let res = runtime.block_on(futcheck.map(Result::unwrap).collect::<Vec<_>>());
assert_eq!(res.get(0).unwrap(), &String::from("t1"));
assert_eq!(res.get(1).unwrap(), &String::from("t2"));

mockito::reset();
}

#[test]
fn test_dockerv2_tags_paginate() {
let name = "repo";
let tags_p1 = r#"{"name": "repo", "tags": [ "t1" ]}"#;
let tags_p2 = r#"{"name": "repo", "tags": [ "t2" ]}"#;

let ep1 = format!("/v2/{}/tags/list?n=1", name);
let ep2 = format!("/v2/{}/tags/list?n=1&last=t1", name);
let addr = mockito::server_address().to_string();
let _m1 = mock("GET", ep1.as_str())
.with_status(200)
.with_header(
"Link",
&format!(
r#"<{}/v2/_tags?n=1&last=t1>; rel="next""#,
mockito::server_url()
),
)
.with_header("Content-Type", "application/json")
.with_body(tags_p1)
.create();
let _m2 = mock("GET", ep2.as_str())
.with_status(200)
.with_header("Content-Type", "application/json")
.with_body(tags_p2)
.create();

let runtime = Runtime::new().unwrap();
let dclient = dkregistry::v2::Client::configure()
.registry(&addr)
.insecure_registry(true)
.username(None)
.password(None)
.build()
.unwrap();

let next = Box::pin(dclient.get_tags(name, Some(1)).map(Result::unwrap));

let (first_tag, stream_rest) = runtime.block_on(next.into_future());
assert_eq!(first_tag.unwrap(), "t1".to_owned());

let (second_tag, stream_rest) = runtime.block_on(stream_rest.into_future());
assert_eq!(second_tag.unwrap(), "t2".to_owned());

let (end, _) = runtime.block_on(stream_rest.into_future());
if end.is_some() {
panic!("end is some: {:?}", end);
}

mockito::reset();
}

#[test]
fn test_dockerv2_tags_404() {
let name = "repo";
let ep = format!("/v2/{}/tags/list", name);
let addr = mockito::server_address().to_string();
let _m = mock("GET", ep.as_str())
.with_status(404)
.with_header("Content-Type", "application/json")
.create();

let runtime = Runtime::new().unwrap();
let dclient = dkregistry::v2::Client::configure()
.registry(&addr)
.insecure_registry(true)
.username(None)
.password(None)
.build()
.unwrap();

let futcheck = dclient.get_tags(name, None);

let res = runtime.block_on(futcheck.collect::<Vec<_>>());
assert!(res.get(0).unwrap().is_err());

mockito::reset();
}

#[test]
fn test_dockerv2_tags_missing_header() {
let name = "repo";
let tags = r#"{"name": "repo", "tags": [ "t1", "t2" ]}"#;
let ep = format!("/v2/{}/tags/list", name);

let addr = mockito::server_address().to_string();
let _m = mock("GET", ep.as_str())
.with_status(200)
.with_body(tags)
.create();

let runtime = Runtime::new().unwrap();
let dclient = dkregistry::v2::Client::configure()
.registry(&addr)
.insecure_registry(true)
.username(None)
.password(None)
.build()
.unwrap();

let futcheck = dclient.get_tags(name, None);

let res = runtime.block_on(futcheck.map(Result::unwrap).collect::<Vec<_>>());
assert_eq!(vec!["t1", "t2"], res);

mockito::reset();
}
8 changes: 4 additions & 4 deletions tests/mock/tags.rs → tests/mock/tags_quay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use self::mockito::mock;
use self::tokio::runtime::Runtime;

#[test]
fn test_tags_simple() {
fn test_quay_tags_simple() {
let name = "repo";
let tags = r#"{"name": "repo", "tags": [ "t1", "t2" ]}"#;

Expand Down Expand Up @@ -39,7 +39,7 @@ fn test_tags_simple() {
}

#[test]
fn test_tags_paginate() {
fn test_quay_tags_paginate() {
let name = "repo";
let tags_p1 = r#"{"name": "repo", "tags": [ "t1" ]}"#;
let tags_p2 = r#"{"name": "repo", "tags": [ "t2" ]}"#;
Expand Down Expand Up @@ -91,7 +91,7 @@ fn test_tags_paginate() {
}

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

#[test]
fn test_tags_missing_header() {
fn test_quay_tags_missing_header() {
let name = "repo";
let tags = r#"{"name": "repo", "tags": [ "t1", "t2" ]}"#;
let ep = format!("/v2/{}/tags/list", name);
Expand Down

0 comments on commit abb58c1

Please sign in to comment.