mirror of
https://github.com/anthonyoteri/dredge.git
synced 2026-06-05 15:26:53 -04:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c7305f8cc4 | |||
| 02dd2ec90a | |||
| b60d433508 | |||
| 0712af9d23 | |||
| 42f8f46bd3 | |||
| 80d1acf295 | |||
| 12dd298706 | |||
| de42860be8 | |||
| b4d6002a20 | |||
| fbe43f03f1 | |||
| 48070cff1f | |||
| 13ae092b91 |
+11
-5
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dredge"
|
name = "dredge-tool"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Anthony Oteri"]
|
authors = ["Anthony Oteri"]
|
||||||
description = "A Command Line tool for interracting with the Docker Registry API"
|
description = "A Command Line tool for interracting with the Docker Registry API"
|
||||||
@@ -18,12 +18,16 @@ categories = [
|
|||||||
"api-bindings",
|
"api-bindings",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
path = "src/main.rs"
|
||||||
|
name = "dredge"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-std = { version = "1.12.0", features = ["async-attributes", "attributes", "tokio1"] }
|
|
||||||
clap = { version = "4.4.3", features = ["derive", "env", "wrap_help"] }
|
clap = { version = "4.4.3", features = ["derive", "env", "wrap_help"] }
|
||||||
femme = "2.2.1"
|
simple_logger = { version = "4.2.0", features = ["timestamps", "colors", "stderr"] }
|
||||||
http = "0.2.9"
|
http = "0.2.9"
|
||||||
indoc = "2.0.4"
|
indoc = "2.0.4"
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
@@ -35,6 +39,8 @@ thiserror = "1.0.48"
|
|||||||
toml = "0.8.0"
|
toml = "0.8.0"
|
||||||
url = { version = "2.4.1", features = ["serde"] }
|
url = { version = "2.4.1", features = ["serde"] }
|
||||||
xdg = "2.5.2"
|
xdg = "2.5.2"
|
||||||
|
tokio = { version = "1.32.0", features = ["macros"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
mockito = "1.2.0"
|
mockito = "1.2.0"
|
||||||
|
env_logger = "0.10.0"
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ Options:
|
|||||||
-V, --version
|
-V, --version
|
||||||
Print version
|
Print version
|
||||||
```
|
```
|
||||||
|
|
||||||
### Checking the API Version
|
### Checking the API Version
|
||||||
|
|
||||||
Perform a simple API Version check towards the registry endpoint
|
Perform a simple API Version check towards the registry endpoint
|
||||||
@@ -87,6 +88,15 @@ Options:
|
|||||||
|
|
||||||
Delete a tagged image from the registry
|
Delete a tagged image from the registry
|
||||||
|
|
||||||
|
Note! This requires that the registry has storage delete rights enabled. For
|
||||||
|
example, when creating the registry, setting the environment variable
|
||||||
|
`REGISTRY_STORAGE_DELETE_ENABLED=true` to enable that feature. If that is not
|
||||||
|
enabled, a `MethodNotAllowed` error will be returned.
|
||||||
|
|
||||||
|
Note! This will only remove the tag from the registry, it will not remove
|
||||||
|
orphaned digests. For that, the garbage collector on the registry service must
|
||||||
|
be run separately.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
Usage: dredge <REGISTRY> delete <IMAGE> <TAG>
|
Usage: dredge <REGISTRY> delete <IMAGE> <TAG>
|
||||||
|
|
||||||
|
|||||||
+12
-2
@@ -2,12 +2,22 @@
|
|||||||
|
|
||||||
## Known Issues
|
## Known Issues
|
||||||
|
|
||||||
* The delete command is currently not implemented and will return an error
|
|
||||||
if called.
|
|
||||||
* Docker authentication is not currently supported, and attempts to query a
|
* Docker authentication is not currently supported, and attempts to query a
|
||||||
registry which requires authentication will fail.
|
registry which requires authentication will fail.
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
- v1.0.0
|
||||||
|
|
||||||
|
- Rename project to dredge-tool by Anthony Oteri b60d433
|
||||||
|
- Replace async_std::test with tokio::test by Anthony Oteri 42f8f46
|
||||||
|
- Replace async-std with tokio by Anthony Oteri 80d1acf
|
||||||
|
- Update known issues in release notes by Anthony Oteri 12dd298
|
||||||
|
|
||||||
|
- v0.2.0
|
||||||
|
|
||||||
|
- Support deleting an image tag by Anthony Oteri fbe43f0
|
||||||
|
- Replace femme logger with simple_logger by Anthony Oteri 13ae092
|
||||||
|
|
||||||
- v0.1.0
|
- v0.1.0
|
||||||
|
|
||||||
- Additional scripts for managing the release process by Anthony Oteri cfdefb2
|
- Additional scripts for managing the release process by Anthony Oteri cfdefb2
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- v0.2.0
|
||||||
|
|
||||||
|
- Support deleting an image tag by Anthony Oteri fbe43f0
|
||||||
|
- Replace femme logger with simple_logger by Anthony Oteri 13ae092
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
- v1.0.0
|
||||||
|
|
||||||
|
- Rename project to dredge-tool by Anthony Oteri b60d433
|
||||||
|
- Replace async_std::test with tokio::test by Anthony Oteri 42f8f46
|
||||||
|
- Replace async-std with tokio by Anthony Oteri 80d1acf
|
||||||
|
- Update known issues in release notes by Anthony Oteri 12dd298
|
||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
## Known Issues
|
## Known Issues
|
||||||
|
|
||||||
* The delete command is currently not implemented and will return an error
|
|
||||||
if called.
|
|
||||||
* Docker authentication is not currently supported, and attempts to query a
|
* Docker authentication is not currently supported, and attempts to query a
|
||||||
registry which requires authentication will fail.
|
registry which requires authentication will fail.
|
||||||
|
|
||||||
|
|||||||
+87
-7
@@ -14,11 +14,14 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use http::header;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::error::ApiError;
|
use crate::error::ApiError;
|
||||||
|
|
||||||
|
const MANIFEST_V2: &str = "application/vnd.docker.distribution.manifest.v2+json";
|
||||||
|
|
||||||
/// Iterate over a paginated result set, collecting and returning the response
|
/// Iterate over a paginated result set, collecting and returning the response
|
||||||
/// set.
|
/// set.
|
||||||
///
|
///
|
||||||
@@ -48,8 +51,13 @@ pub async fn fetch_paginated<T: for<'de> Deserialize<'de>>(
|
|||||||
let url = origin.join(&next_path)?;
|
let url = origin.join(&next_path)?;
|
||||||
|
|
||||||
let resp = reqwest::get(url).await?;
|
let resp = reqwest::get(url).await?;
|
||||||
|
parse_response_status(&resp)?;
|
||||||
|
|
||||||
let headers = resp.headers().clone();
|
let headers = resp.headers().clone();
|
||||||
responses.push(resp.json().await?);
|
|
||||||
|
if let Ok(json) = resp.json().await {
|
||||||
|
responses.push(json);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(p) = parse_rfc5988(headers.get(http::header::LINK))? {
|
if let Some(p) = parse_rfc5988(headers.get(http::header::LINK))? {
|
||||||
next_path = p;
|
next_path = p;
|
||||||
@@ -126,7 +134,7 @@ pub fn parse_response_status(response: &reqwest::Response) -> Result<(), ApiErro
|
|||||||
log::trace!("parse_response_status(response: {response:?})");
|
log::trace!("parse_response_status(response: {response:?})");
|
||||||
|
|
||||||
match response.status() {
|
match response.status() {
|
||||||
http::StatusCode::OK => {
|
http::StatusCode::OK | http::StatusCode::ACCEPTED => {
|
||||||
let headers = response.headers();
|
let headers = response.headers();
|
||||||
if let Some(header_value) = headers.get("Docker-Distribution-API-Version") {
|
if let Some(header_value) = headers.get("Docker-Distribution-API-Version") {
|
||||||
if header_value.to_str()? == "registry/2.0" {
|
if header_value.to_str()? == "registry/2.0" {
|
||||||
@@ -140,6 +148,7 @@ pub fn parse_response_status(response: &reqwest::Response) -> Result<(), ApiErro
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
http::StatusCode::METHOD_NOT_ALLOWED => Err(ApiError::MethodNotAllowed),
|
||||||
http::StatusCode::UNAUTHORIZED => {
|
http::StatusCode::UNAUTHORIZED => {
|
||||||
let headers = response.headers();
|
let headers = response.headers();
|
||||||
if let Some(header_value) = headers.get("Docker-Distribution-API-Version") {
|
if let Some(header_value) = headers.get("Docker-Distribution-API-Version") {
|
||||||
@@ -155,12 +164,39 @@ pub fn parse_response_status(response: &reqwest::Response) -> Result<(), ApiErro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
http::StatusCode::NOT_FOUND => Err(ApiError::NotFound),
|
http::StatusCode::NOT_FOUND => Err(ApiError::NotFound),
|
||||||
_ => Err(ApiError::UnexpectedResponse(
|
e => Err(ApiError::UnexpectedResponse(format!(
|
||||||
"Undocumented status code".into(),
|
"Undocumented status code: {e:?}"
|
||||||
)),
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch the V2 Registry Digest for the specific manifest referenced in the
|
||||||
|
/// provided `url`.
|
||||||
|
///
|
||||||
|
/// # Errors:
|
||||||
|
///
|
||||||
|
/// This will return an `ApiError` if there is a problem fetching the manifest
|
||||||
|
/// headers.
|
||||||
|
pub async fn get_digest(client: &reqwest::Client, url: &Url) -> Result<String, ApiError> {
|
||||||
|
log::trace!("get_manifest(client: {client:?}, url: {url}");
|
||||||
|
let resp = client
|
||||||
|
.head(url.as_ref())
|
||||||
|
.header(header::ACCEPT, MANIFEST_V2)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
parse_response_status(&resp)?;
|
||||||
|
|
||||||
|
let headers = resp.headers();
|
||||||
|
Ok(String::from(
|
||||||
|
headers
|
||||||
|
.get("docker-content-digest")
|
||||||
|
.ok_or(ApiError::UnexpectedResponse(String::from(
|
||||||
|
"Missing docker-content-digest header",
|
||||||
|
)))?
|
||||||
|
.to_str()?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use http::header::HeaderValue;
|
use http::header::HeaderValue;
|
||||||
@@ -171,7 +207,7 @@ mod tests {
|
|||||||
///
|
///
|
||||||
/// Attempt to parse a valid RFC5988 header value, and ensure that the
|
/// Attempt to parse a valid RFC5988 header value, and ensure that the
|
||||||
/// parsed URL was returned as expected.
|
/// parsed URL was returned as expected.
|
||||||
#[async_std::test]
|
#[tokio::test]
|
||||||
async fn test_parse_rfc5988_valid() {
|
async fn test_parse_rfc5988_valid() {
|
||||||
// Mock a valid RFC5988 header value
|
// Mock a valid RFC5988 header value
|
||||||
let valid_header_value =
|
let valid_header_value =
|
||||||
@@ -189,7 +225,7 @@ mod tests {
|
|||||||
///
|
///
|
||||||
/// Attempt to parse an invalid string as RFC5988, ensuring that the `None`
|
/// Attempt to parse an invalid string as RFC5988, ensuring that the `None`
|
||||||
/// variant is returned.
|
/// variant is returned.
|
||||||
#[async_std::test]
|
#[tokio::test]
|
||||||
async fn test_parse_rfc5988_invalid() {
|
async fn test_parse_rfc5988_invalid() {
|
||||||
// Mock a valid RFC5988 header value
|
// Mock a valid RFC5988 header value
|
||||||
let invalid_header_value = HeaderValue::from_str(r#"invalid header value"#)
|
let invalid_header_value = HeaderValue::from_str(r#"invalid header value"#)
|
||||||
@@ -201,4 +237,48 @@ mod tests {
|
|||||||
// Assert that the function returned the expected URL as Some(String)
|
// Assert that the function returned the expected URL as Some(String)
|
||||||
assert_eq!(result, None);
|
assert_eq!(result, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validates the happy path for the get_digest function
|
||||||
|
///
|
||||||
|
/// This tests starts up a mock server, and the client makes a request for
|
||||||
|
/// the digest with the proper headers set. The test then validates that
|
||||||
|
/// the correct digest is returned and that the mock server had the expected
|
||||||
|
/// interactions.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_digest() -> Result<(), ApiError> {
|
||||||
|
let mut server = mockito::Server::new_async().await;
|
||||||
|
let path = "/v2/foo/manifests/latest";
|
||||||
|
|
||||||
|
// Mock the HTTP response for the Docker Registry API
|
||||||
|
let registry_url = Url::parse(&server.url()).expect("Failed to parse registry URL");
|
||||||
|
let mock_response = server
|
||||||
|
.mock("HEAD", path)
|
||||||
|
.match_header(http::header::ACCEPT.as_str(), MANIFEST_V2)
|
||||||
|
.with_status(http::status::StatusCode::OK.as_u16().into())
|
||||||
|
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
||||||
|
.with_header("Docker-Distribution-API-Version", "registry/2.0")
|
||||||
|
.with_header(
|
||||||
|
"docker-content-digest",
|
||||||
|
"sha256:0259571889ac87efbfca5b79a0abe9baf626d058ec5f9a5744bace2229d9ed50",
|
||||||
|
)
|
||||||
|
.with_header(
|
||||||
|
"etag",
|
||||||
|
"sha256:0259571889ac87efbfca5b79a0abe9baf626d058ec5f9a5744bace2229d9ed50",
|
||||||
|
)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let url = registry_url.join(path)?;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let result = get_digest(&client, &url).await;
|
||||||
|
|
||||||
|
assert!(result.is_ok(), "{:?}", result.unwrap_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap(),
|
||||||
|
*"sha256:0259571889ac87efbfca5b79a0abe9baf626d058ec5f9a5744bace2229d9ed50"
|
||||||
|
);
|
||||||
|
|
||||||
|
mock_response.assert();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-17
@@ -128,6 +128,8 @@ pub async fn show_handler(
|
|||||||
let url = registry_url.join(&path)?;
|
let url = registry_url.join(&path)?;
|
||||||
|
|
||||||
let resp = reqwest::get(url).await?;
|
let resp = reqwest::get(url).await?;
|
||||||
|
api::parse_response_status(&resp)?;
|
||||||
|
|
||||||
let headers = resp.headers();
|
let headers = resp.headers();
|
||||||
let digest: String = String::from(
|
let digest: String = String::from(
|
||||||
headers
|
headers
|
||||||
@@ -173,7 +175,17 @@ pub async fn delete_handler(
|
|||||||
tag: &str,
|
tag: &str,
|
||||||
) -> Result<(), ApiError> {
|
) -> Result<(), ApiError> {
|
||||||
log::trace!("delete_handler(registry_url: {registry_url:?}, image: {image}, tag: {tag})");
|
log::trace!("delete_handler(registry_url: {registry_url:?}, image: {image}, tag: {tag})");
|
||||||
todo!()
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let url = registry_url.join(&format!("/v2/{image}/manifests/{tag}"))?;
|
||||||
|
let digest = api::get_digest(&client, &url).await?;
|
||||||
|
|
||||||
|
log::debug!("Deleting digest {digest}");
|
||||||
|
let url = registry_url.join(&format!("/v2/{image}/manifests/{digest}"))?;
|
||||||
|
let resp = client.delete(url).send().await?;
|
||||||
|
api::parse_response_status(&resp)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path to the Docker Registry APIs "api version check" endpoint.
|
// Path to the Docker Registry APIs "api version check" endpoint.
|
||||||
@@ -190,8 +202,8 @@ pub async fn check_handler(buf: &mut dyn Write, registry_url: &Url) -> Result<()
|
|||||||
let path = "/v2";
|
let path = "/v2";
|
||||||
let url = registry_url.join(path)?;
|
let url = registry_url.join(path)?;
|
||||||
|
|
||||||
let response = reqwest::get(url).await?;
|
let resp = reqwest::get(url).await?;
|
||||||
api::parse_response_status(&response)?;
|
api::parse_response_status(&resp)?;
|
||||||
writeln!(buf, "Ok")?;
|
writeln!(buf, "Ok")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -212,7 +224,7 @@ mod tests {
|
|||||||
/// This test spins up a mock server, and makes a request to the catalog
|
/// This test spins up a mock server, and makes a request to the catalog
|
||||||
/// endpoint. It checks that the handler both called the request the
|
/// endpoint. It checks that the handler both called the request the
|
||||||
/// expected number of times, and did not return an error.
|
/// expected number of times, and did not return an error.
|
||||||
#[async_std::test]
|
#[tokio::test]
|
||||||
async fn test_catalog_handler() {
|
async fn test_catalog_handler() {
|
||||||
let mut server = mockito::Server::new_async().await;
|
let mut server = mockito::Server::new_async().await;
|
||||||
let path = "/v2/_catalog";
|
let path = "/v2/_catalog";
|
||||||
@@ -222,12 +234,13 @@ mod tests {
|
|||||||
.mock("GET", path)
|
.mock("GET", path)
|
||||||
.with_status(http::status::StatusCode::OK.as_u16().into())
|
.with_status(http::status::StatusCode::OK.as_u16().into())
|
||||||
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
||||||
|
.with_header("Docker-Distribution-API-Version", "registry/2.0")
|
||||||
.with_body(r#"{"repositories": ["image1", "image2", "image3"]}"#)
|
.with_body(r#"{"repositories": ["image1", "image2", "image3"]}"#)
|
||||||
.create();
|
.create();
|
||||||
|
|
||||||
let mut buf: Vec<u8> = Vec::new();
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
let result = catalog_handler(&mut buf, ®istry_url).await;
|
let result = catalog_handler(&mut buf, ®istry_url).await;
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok(), "{:?}", result.unwrap_err());
|
||||||
assert_eq!(String::from_utf8(buf).unwrap(), *"image1\nimage2\nimage3\n");
|
assert_eq!(String::from_utf8(buf).unwrap(), *"image1\nimage2\nimage3\n");
|
||||||
|
|
||||||
mock_response.assert();
|
mock_response.assert();
|
||||||
@@ -240,7 +253,7 @@ mod tests {
|
|||||||
/// should follow, resulting in the combined list. It checks that the
|
/// should follow, resulting in the combined list. It checks that the
|
||||||
/// handler both called the request the expected number of times, and did
|
/// handler both called the request the expected number of times, and did
|
||||||
/// not return an error.
|
/// not return an error.
|
||||||
#[async_std::test]
|
#[tokio::test]
|
||||||
async fn test_catalog_handler_with_pagination() {
|
async fn test_catalog_handler_with_pagination() {
|
||||||
let mut server = mockito::Server::new_async().await;
|
let mut server = mockito::Server::new_async().await;
|
||||||
let path = "/v2/_catalog";
|
let path = "/v2/_catalog";
|
||||||
@@ -251,6 +264,7 @@ mod tests {
|
|||||||
.mock("GET", path)
|
.mock("GET", path)
|
||||||
.with_status(http::status::StatusCode::OK.as_u16().into())
|
.with_status(http::status::StatusCode::OK.as_u16().into())
|
||||||
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
||||||
|
.with_header("Docker-Distribution-API-Version", "registry/2.0")
|
||||||
.with_header(
|
.with_header(
|
||||||
http::header::LINK.as_str(),
|
http::header::LINK.as_str(),
|
||||||
&format!(r#"<{path2}>; rel=next"#),
|
&format!(r#"<{path2}>; rel=next"#),
|
||||||
@@ -262,12 +276,13 @@ mod tests {
|
|||||||
.mock("GET", path2)
|
.mock("GET", path2)
|
||||||
.with_status(http::status::StatusCode::OK.as_u16().into())
|
.with_status(http::status::StatusCode::OK.as_u16().into())
|
||||||
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
||||||
|
.with_header("Docker-Distribution-API-Version", "registry/2.0")
|
||||||
.with_body(r#"{"repositories": ["image3"]}"#)
|
.with_body(r#"{"repositories": ["image3"]}"#)
|
||||||
.create();
|
.create();
|
||||||
|
|
||||||
let mut buf: Vec<u8> = Vec::new();
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
let result = catalog_handler(&mut buf, ®istry_url).await;
|
let result = catalog_handler(&mut buf, ®istry_url).await;
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok(), "{:?}", result.unwrap_err());
|
||||||
assert_eq!(String::from_utf8(buf).unwrap(), *"image1\nimage2\nimage3\n");
|
assert_eq!(String::from_utf8(buf).unwrap(), *"image1\nimage2\nimage3\n");
|
||||||
|
|
||||||
mock_response.assert();
|
mock_response.assert();
|
||||||
@@ -279,7 +294,7 @@ mod tests {
|
|||||||
/// This test spins up a mock server, and makes a request to the tags
|
/// This test spins up a mock server, and makes a request to the tags
|
||||||
/// endpoint. It checks that the handler both called the request the
|
/// endpoint. It checks that the handler both called the request the
|
||||||
/// expected number of times, and did not return an error.
|
/// expected number of times, and did not return an error.
|
||||||
#[async_std::test]
|
#[tokio::test]
|
||||||
async fn test_tags_handler() {
|
async fn test_tags_handler() {
|
||||||
let mut server = mockito::Server::new_async().await;
|
let mut server = mockito::Server::new_async().await;
|
||||||
let path = "/v2/some_image/tags/list";
|
let path = "/v2/some_image/tags/list";
|
||||||
@@ -290,12 +305,13 @@ mod tests {
|
|||||||
.mock("GET", path)
|
.mock("GET", path)
|
||||||
.with_status(http::status::StatusCode::OK.as_u16().into())
|
.with_status(http::status::StatusCode::OK.as_u16().into())
|
||||||
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
||||||
|
.with_header("Docker-Distribution-API-Version", "registry/2.0")
|
||||||
.with_body(r#"{"tags": ["tag1", "tag2", "tag3"]}"#)
|
.with_body(r#"{"tags": ["tag1", "tag2", "tag3"]}"#)
|
||||||
.create();
|
.create();
|
||||||
|
|
||||||
let mut buf: Vec<u8> = Vec::new();
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
let result = tags_handler(&mut buf, ®istry_url, "some_image").await;
|
let result = tags_handler(&mut buf, ®istry_url, "some_image").await;
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok(), "{:?}", result.unwrap_err());
|
||||||
assert_eq!(String::from_utf8(buf).unwrap(), *"tag1\ntag2\ntag3\n");
|
assert_eq!(String::from_utf8(buf).unwrap(), *"tag1\ntag2\ntag3\n");
|
||||||
|
|
||||||
mock_response.assert();
|
mock_response.assert();
|
||||||
@@ -308,7 +324,7 @@ mod tests {
|
|||||||
/// should follow, resulting in the combined list. It checks that the
|
/// should follow, resulting in the combined list. It checks that the
|
||||||
/// handler both called the request the expected number of times, and did
|
/// handler both called the request the expected number of times, and did
|
||||||
/// not return an error.
|
/// not return an error.
|
||||||
#[async_std::test]
|
#[tokio::test]
|
||||||
async fn test_tags_handler_with_pagination() {
|
async fn test_tags_handler_with_pagination() {
|
||||||
let mut server = mockito::Server::new_async().await;
|
let mut server = mockito::Server::new_async().await;
|
||||||
let path = "/v2/some_image/tags/list";
|
let path = "/v2/some_image/tags/list";
|
||||||
@@ -320,6 +336,7 @@ mod tests {
|
|||||||
.mock("GET", path)
|
.mock("GET", path)
|
||||||
.with_status(http::status::StatusCode::OK.as_u16().into())
|
.with_status(http::status::StatusCode::OK.as_u16().into())
|
||||||
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
||||||
|
.with_header("Docker-Distribution-API-Version", "registry/2.0")
|
||||||
.with_header(
|
.with_header(
|
||||||
http::header::LINK.as_str(),
|
http::header::LINK.as_str(),
|
||||||
&format!(r#"<{path2}>; rel=next"#),
|
&format!(r#"<{path2}>; rel=next"#),
|
||||||
@@ -331,12 +348,13 @@ mod tests {
|
|||||||
.mock("GET", path2)
|
.mock("GET", path2)
|
||||||
.with_status(http::status::StatusCode::OK.as_u16().into())
|
.with_status(http::status::StatusCode::OK.as_u16().into())
|
||||||
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
||||||
|
.with_header("Docker-Distribution-API-Version", "registry/2.0")
|
||||||
.with_body(r#"{"tags": ["tag3"]}"#)
|
.with_body(r#"{"tags": ["tag3"]}"#)
|
||||||
.create();
|
.create();
|
||||||
|
|
||||||
let mut buf: Vec<u8> = Vec::new();
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
let result = tags_handler(&mut buf, ®istry_url, "some_image").await;
|
let result = tags_handler(&mut buf, ®istry_url, "some_image").await;
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok(), "{:?}", result.unwrap_err());
|
||||||
assert_eq!(String::from_utf8(buf).unwrap(), *"tag1\ntag2\ntag3\n");
|
assert_eq!(String::from_utf8(buf).unwrap(), *"tag1\ntag2\ntag3\n");
|
||||||
|
|
||||||
mock_response.assert();
|
mock_response.assert();
|
||||||
@@ -348,7 +366,7 @@ mod tests {
|
|||||||
/// This test spins up a mock server, and makes a request to the check
|
/// This test spins up a mock server, and makes a request to the check
|
||||||
/// endpoint. It checks that the handler both called the request the
|
/// endpoint. It checks that the handler both called the request the
|
||||||
/// expected number of times, and did not return an error.
|
/// expected number of times, and did not return an error.
|
||||||
#[async_std::test]
|
#[tokio::test]
|
||||||
async fn test_check_handler() {
|
async fn test_check_handler() {
|
||||||
let mut server = mockito::Server::new_async().await;
|
let mut server = mockito::Server::new_async().await;
|
||||||
let path = "/v2";
|
let path = "/v2";
|
||||||
@@ -364,7 +382,7 @@ mod tests {
|
|||||||
|
|
||||||
let mut buf: Vec<u8> = Vec::new();
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
let result = check_handler(&mut buf, ®istry_url).await;
|
let result = check_handler(&mut buf, ®istry_url).await;
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok(), "{:?}", result.unwrap_err());
|
||||||
assert_eq!(String::from_utf8(buf).unwrap(), *"Ok\n");
|
assert_eq!(String::from_utf8(buf).unwrap(), *"Ok\n");
|
||||||
|
|
||||||
mock_response.assert();
|
mock_response.assert();
|
||||||
@@ -374,7 +392,7 @@ mod tests {
|
|||||||
///
|
///
|
||||||
/// This validates that if the "Docker-Distribution-API-Version" header
|
/// This validates that if the "Docker-Distribution-API-Version" header
|
||||||
/// is missing in the response, the appropriate error is returned.
|
/// is missing in the response, the appropriate error is returned.
|
||||||
#[async_std::test]
|
#[tokio::test]
|
||||||
async fn test_check_handler_missing_api_version() -> Result<(), Box<dyn Error>> {
|
async fn test_check_handler_missing_api_version() -> Result<(), Box<dyn Error>> {
|
||||||
let mut server = mockito::Server::new_async().await;
|
let mut server = mockito::Server::new_async().await;
|
||||||
let path = "/v2";
|
let path = "/v2";
|
||||||
@@ -407,7 +425,7 @@ mod tests {
|
|||||||
/// This validates that if the "Docker-Distribution-API-Version" header
|
/// This validates that if the "Docker-Distribution-API-Version" header
|
||||||
/// is present in the response but contains an unexpected value, the
|
/// is present in the response but contains an unexpected value, the
|
||||||
/// appropriate error is returned.
|
/// appropriate error is returned.
|
||||||
#[async_std::test]
|
#[tokio::test]
|
||||||
async fn test_check_handler_invalid_api_version() -> Result<(), Box<dyn Error>> {
|
async fn test_check_handler_invalid_api_version() -> Result<(), Box<dyn Error>> {
|
||||||
let mut server = mockito::Server::new_async().await;
|
let mut server = mockito::Server::new_async().await;
|
||||||
let path = "/v2";
|
let path = "/v2";
|
||||||
@@ -441,7 +459,7 @@ mod tests {
|
|||||||
/// This test spins up a mock server, and makes a request to the image
|
/// This test spins up a mock server, and makes a request to the image
|
||||||
/// manifests endpoint. It checks that the handler both called the request
|
/// manifests endpoint. It checks that the handler both called the request
|
||||||
/// the expected number of times, and did not return an error.
|
/// the expected number of times, and did not return an error.
|
||||||
#[async_std::test]
|
#[tokio::test]
|
||||||
async fn test_show_handler() {
|
async fn test_show_handler() {
|
||||||
let mut server = mockito::Server::new_async().await;
|
let mut server = mockito::Server::new_async().await;
|
||||||
let path = "/v2/foo/manifests/latest";
|
let path = "/v2/foo/manifests/latest";
|
||||||
@@ -492,6 +510,7 @@ mod tests {
|
|||||||
.mock("GET", path)
|
.mock("GET", path)
|
||||||
.with_status(http::status::StatusCode::OK.as_u16().into())
|
.with_status(http::status::StatusCode::OK.as_u16().into())
|
||||||
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
||||||
|
.with_header("Docker-Distribution-API-Version", "registry/2.0")
|
||||||
.with_header(
|
.with_header(
|
||||||
"docker-content-digest",
|
"docker-content-digest",
|
||||||
"sha256:0259571889ac87efbfca5b79a0abe9baf626d058ec5f9a5744bace2229d9ed50",
|
"sha256:0259571889ac87efbfca5b79a0abe9baf626d058ec5f9a5744bace2229d9ed50",
|
||||||
@@ -517,7 +536,7 @@ mod tests {
|
|||||||
etag: sha256:0259571889ac87efbfca5b79a0abe9baf626d058ec5f9a5744bace2229d9ed50\n"
|
etag: sha256:0259571889ac87efbfca5b79a0abe9baf626d058ec5f9a5744bace2229d9ed50\n"
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok(), "{:?}", result.unwrap_err());
|
||||||
assert_eq!(String::from_utf8(buf).unwrap(), *expected_body);
|
assert_eq!(String::from_utf8(buf).unwrap(), *expected_body);
|
||||||
|
|
||||||
mock_response.assert();
|
mock_response.assert();
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ pub enum DredgeError {
|
|||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
IOError(#[from] std::io::Error),
|
IOError(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
LoggerError(#[from] log::SetLoggerError),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An error related to the communication with the registry API.
|
/// An error related to the communication with the registry API.
|
||||||
@@ -65,6 +68,9 @@ pub enum ApiError {
|
|||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
SerializerError(#[from] serde_yaml::Error),
|
SerializerError(#[from] serde_yaml::Error),
|
||||||
|
|
||||||
|
#[error("Method not allowed")]
|
||||||
|
MethodNotAllowed,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<reqwest::header::ToStrError> for ApiError {
|
impl From<reqwest::header::ToStrError> for ApiError {
|
||||||
|
|||||||
+8
-2
@@ -19,6 +19,7 @@
|
|||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use simple_logger::SimpleLogger;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::cli::Cli;
|
use crate::cli::Cli;
|
||||||
@@ -56,13 +57,18 @@ fn parse_registry_arg(host: &str) -> Result<Url, DredgeError> {
|
|||||||
Url::parse(&host).or(Err(DredgeError::RegistryUrlError(host.to_string())))
|
Url::parse(&host).or(Err(DredgeError::RegistryUrlError(host.to_string())))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_std::main]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
async fn main() -> Result<(), DredgeError> {
|
async fn main() -> Result<(), DredgeError> {
|
||||||
let args = Cli::parse();
|
let args = Cli::parse();
|
||||||
|
|
||||||
// -- Initialize logging
|
// -- Initialize logging
|
||||||
let log_level = args.log_level;
|
let log_level = args.log_level;
|
||||||
femme::with_level(log::LevelFilter::from(log_level));
|
SimpleLogger::new()
|
||||||
|
.with_colors(true)
|
||||||
|
.with_utc_timestamps()
|
||||||
|
.with_level(log_level.into())
|
||||||
|
.env()
|
||||||
|
.init()?;
|
||||||
|
|
||||||
// -- Parse the given <REGISTRY> argument into a complete URL
|
// -- Parse the given <REGISTRY> argument into a complete URL
|
||||||
let registry_url: Url = parse_registry_arg(&args.registry)?;
|
let registry_url: Url = parse_registry_arg(&args.registry)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user