diff --git a/cmd/oci-catalog/src/providers/dockerhub.rs b/cmd/oci-catalog/src/providers/dockerhub.rs index 2a71736baf3..74e7f8b2f63 100644 --- a/cmd/oci-catalog/src/providers/dockerhub.rs +++ b/cmd/oci-catalog/src/providers/dockerhub.rs @@ -12,6 +12,7 @@ use tonic::Status; /// The default page size with which requests are sent to docker hub. const DEFAULT_PAGE_SIZE: u8 = 100; pub const PROVIDER_NAME: &str = "DockerHubAPI"; +pub const DOCKERHUB_URI: &str = "https://hub.docker.com"; #[derive(Serialize, Deserialize)] struct DockerHubV2Repository { @@ -29,6 +30,21 @@ struct DockerHubV2RepositoriesResult { results: Vec, } +#[derive(Serialize, Deserialize)] +struct DockerHubV2Tag { + name: String, + repository_type: Option, + content_type: String, +} + +#[derive(Serialize, Deserialize)] +struct DockerHubV2TagsResult { + count: u16, + next: Option, + previous: Option, + results: Vec, +} + #[derive(Debug, Default)] pub struct DockerHubAPI {} @@ -44,7 +60,7 @@ impl OCICatalogSender for DockerHubAPI { tx: mpsc::Sender>, request: &ListRepositoriesRequest, ) { - let mut url = url_for_request(request); + let mut url = url_for_request_repositories(request); let client = reqwest::Client::builder().build().unwrap(); @@ -105,25 +121,79 @@ impl OCICatalogSender for DockerHubAPI { } } - async fn send_tags(&self, tx: mpsc::Sender>, _request: &ListTagsRequest) { - for count in 0..10 { - tx.send(Ok(Tag { - name: format!("tag-{}", count), - })) - .await - .unwrap(); + async fn send_tags(&self, tx: mpsc::Sender>, request: &ListTagsRequest) { + let mut url = match url_for_request_tags(request) { + Ok(u) => u, + Err(e) => { + tx.send(Err(e)).await.unwrap(); + return; + } + }; + + let client = reqwest::Client::builder().build().unwrap(); + + loop { + log::debug!("requesting: {}", url); + let response = match client.get(url.clone()).send().await { + Ok(r) => r, + Err(e) => { + tx.send(Err(Status::failed_precondition(e.to_string()))) + .await + .unwrap(); + return; + } + }; + + if response.status() != StatusCode::OK { + tx.send(Err(Status::failed_precondition(format!( + "unexpected status code when requesting {}: {}", + url, + response.status() + )))) + .await + .unwrap(); + return; + } + + let body = match response.text().await { + Ok(b) => b, + Err(e) => { + tx.send(Err(Status::failed_precondition(format!( + "unable to extract body from response: {}", + e.to_string() + )))) + .await + .unwrap(); + return; + } + }; + log::trace!("response body: {}", body); + + let response: DockerHubV2TagsResult = serde_json::from_str(&body).unwrap(); + + for tag in response.results { + tx.send(Ok(Tag { name: tag.name })).await.unwrap(); + } + + if response.next.is_some() { + url = reqwest::Url::parse(&response.next.unwrap()).unwrap(); + } else { + break; + } } } } -fn url_for_request(request: &ListRepositoriesRequest) -> Url { - let mut url = reqwest::Url::parse("https://hub.docker.com/v2/repositories/").unwrap(); +fn url_for_request_repositories(request: &ListRepositoriesRequest) -> Url { + let mut url = reqwest::Url::parse(DOCKERHUB_URI).unwrap(); if !request.namespace.is_empty() { url.set_path(&format!( "/v2/namespaces/{}/repositories/", request.namespace )); + } else { + url.set_path("/v2/repositories/"); } // For now we use a default page size and default ordering. url.query_pairs_mut() @@ -137,6 +207,30 @@ fn url_for_request(request: &ListRepositoriesRequest) -> Url { url } +fn url_for_request_tags(request: &ListTagsRequest) -> Result { + let mut url = reqwest::Url::parse(DOCKERHUB_URI).unwrap(); + + let repo = match request.repository.clone() { + Some(r) => r, + None => { + return Err(Status::invalid_argument(format!( + "repository not set in request" + ))) + } + }; + + url.set_path(&format!( + "/v2/repositories/{}/{}/tags", + repo.namespace, repo.name + )); + + // For now we use a default page size. + url.query_pairs_mut() + .append_pair("page_size", &format!("{}", DEFAULT_PAGE_SIZE)); + + Ok(url) +} + #[cfg(test)] mod tests { use super::*; @@ -165,6 +259,35 @@ mod tests { ..Default::default() }, "https://hub.docker.com/v2/namespaces/bitnamicharts/repositories/?page_size=100&ordering=name&content_types=helm&content_types=image")] fn test_url_for_request(#[case] request: ListRepositoriesRequest, #[case] expected_url: Url) { - assert_eq!(url_for_request(&request), expected_url); + assert_eq!(url_for_request_repositories(&request), expected_url); + } + + #[rstest] + #[case::without_repository(ListTagsRequest{ + ..Default::default() + }, Err(Status::invalid_argument("bang")))] + #[case::with_repository(ListTagsRequest{ + repository: Some(Repository{ + namespace: "bitnamicharts".to_string(), + name: "apache".to_string(), + ..Default::default() + }), + ..Default::default() + }, Ok(reqwest::Url::parse("https://hub.docker.com/v2/repositories/bitnamicharts/apache/tags?page_size=100").unwrap()))] + fn test_url_for_request_tags( + #[case] request: ListTagsRequest, + #[case] expected_result: Result, + ) { + match expected_result { + Ok(url) => { + assert_eq!(url_for_request_tags(&request).unwrap(), url); + } + Err(e) => { + assert_eq!( + url_for_request_tags(&request).err().unwrap().code(), + e.code() + ) + } + } } }