15 Commits

Author SHA1 Message Date
Anthony Oteri 5e7d0e4e1e Release version 1.1.0 2023-10-03 09:55:09 -04:00
Anthony Oteri 600c2d86ac Merge pull request #52 from anthonyoteri/license
Change License to MIT/Apache
2023-10-03 09:48:38 -04:00
Anthony Oteri 0e4219b405 Change License
This changes the effective license from a simple Apache-2.0 license to a
dual license consisting of both Apache-2.0 and the MIT license to be more
compatible for use in GPL Code.

The MIT license requires reproducing countless copies of the same copyright
header with diffferent names in the copyright field, for every MIT library
in use.  The Apache license does not have this drawback.  However, this is not
the primary motivation for creating these issues.  The Apache license also has
protections from patent trolls and explicit contribuition licensing clause.
However the Apache license is incompatible with GPLv2.  This is why Rust is
dual-licensed as MIT/Apache (the "primary" license being Apache, MIT only for
GPLv2 compat), and doing so would be wise for this project.  This also makes
this crate suitable for inclusion and unrestricted sharing in the Rust
standard distribution and other projects using dual MIT/Apache.
2023-10-03 09:36:43 -04:00
Anthony Oteri c7305f8cc4 Release version 1.0.0 2023-10-02 13:44:53 -04:00
Anthony Oteri 02dd2ec90a Merge pull request #50 from anthonyoteri/rename-project
Rename project to dredge-tool
2023-10-02 13:43:32 -04:00
Anthony Oteri b60d433508 Rename project to dredge-tool
The name "dredge" alreay exists on crates.io, but "dredge-tool" does not.
The name "dredge-tool" is more clear that this is a binary tool anyway.
2023-10-02 13:33:50 -04:00
Anthony Oteri 0712af9d23 Merge pull request #49 from anthonyoteri/replace-async-std-with-tokio
Use better async runtime
2023-10-02 13:15:56 -04:00
Anthony Oteri 42f8f46bd3 Replace async_std::test with tokio::test 2023-10-02 13:10:55 -04:00
Anthony Oteri 80d1acf295 Replace async-std with tokio
Tokio is a far more mature async runtime.
2023-10-02 13:01:58 -04:00
Anthony Oteri 12dd298706 Update known issues in release notes 2023-09-29 18:18:47 -04:00
Anthony Oteri de42860be8 Release version 0.2.0 2023-09-29 18:09:46 -04:00
Anthony Oteri b4d6002a20 Merge pull request #46 from anthonyoteri/support-delete
Support delete command
2023-09-29 18:08:40 -04:00
Anthony Oteri fbe43f03f1 Support deleting an image tag
If the Docker Registry API allows for images to be deleted, issuing the
command `delete <image> <tag>` will result in the tag being removed from
the server.

A limitation of this change is that it will cause any unreferenced tags
to become orphaned, requiring the garbage collector within the docker
registry server to clean these up.  This is because there is no way to
efficiently determine if each of the layers are reused by other tags or
images on the server.

Implement delete logic

Fix broken unit tests
2023-09-29 18:04:08 -04:00
Anthony Oteri 48070cff1f Merge pull request #45 from anthonyoteri/replace-femme-with-simple-logger
Replace Femme with SimpleLogger
2023-09-29 15:13:45 -04:00
Anthony Oteri 13ae092b91 Replace femme logger with simple_logger 2023-09-29 15:05:23 -04:00
19 changed files with 296 additions and 140 deletions
+6
View File
@@ -0,0 +1,6 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="Copyright 2023 Anthony Oteri&#10;&#10;Licensed under the Apache License, Version 2.0, &lt;LICENSE-APACHE or&#10;http://apache.org/licenses/LICENSE-2.0&gt; or the MIT license &lt;LICENSE-MIT or&#10;http://opensource.org/licenses/MIT&gt;, at your option. This file may not be&#10;copied, modified, or distributed except according to those terms." />
<option name="myName" value="MIT OR Apache-2.0 (personal)" />
</copyright>
</component>
+1 -1
View File
@@ -1,7 +1,7 @@
<component name="CopyrightManager">
<settings>
<module2copyright>
<element module="Project Files" copyright="Apache License (personal)" />
<element module="Project Files" copyright="MIT OR Apache-2.0 (personal)" />
</module2copyright>
</settings>
</component>
+11 -5
View File
@@ -1,13 +1,13 @@
[package]
name = "dredge"
version = "0.1.0"
name = "dredge-tool"
version = "1.1.0"
edition = "2021"
authors = ["Anthony Oteri"]
description = "A Command Line tool for interracting with the Docker Registry API"
readme = "README.md"
repository = "https://github.com/anthonyoteri/dredge"
rust-version = "1.72"
license-file = "LICENSE"
license = "MIT OR Apache-2.0"
keywords = [
"docker",
"registry",
@@ -18,12 +18,16 @@ categories = [
"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
[dependencies]
async-std = { version = "1.12.0", features = ["async-attributes", "attributes", "tokio1"] }
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"
indoc = "2.0.4"
log = "0.4.20"
@@ -35,6 +39,8 @@ thiserror = "1.0.48"
toml = "0.8.0"
url = { version = "2.4.1", features = ["serde"] }
xdg = "2.5.2"
tokio = { version = "1.32.0", features = ["macros"] }
[dev-dependencies]
mockito = "1.2.0"
env_logger = "0.10.0"
View File
+24
View File
@@ -0,0 +1,24 @@
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
+26
View File
@@ -32,6 +32,7 @@ Options:
-V, --version
Print version
```
### Checking the API Version
Perform a simple API Version check towards the registry endpoint
@@ -87,6 +88,15 @@ Options:
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
Usage: dredge <REGISTRY> delete <IMAGE> <TAG>
@@ -97,3 +107,19 @@ Arguments:
Options:
-h, --help Print help
```
## License
Licensed under either of
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any
additional terms or conditions.
+21 -2
View File
@@ -1,13 +1,32 @@
# Dredge Release Notes
## Legal
As of version 1.1.0, this software license has been changed from Apache-2.0
to a dual-licensed Apache-2.0 OR MIT license.
## 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
registry which requires authentication will fail.
## Changelog
- v1.1.0
- Change License by Anthony Oteri 0e4219b
- 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
- Additional scripts for managing the release process by Anthony Oteri cfdefb2
+4
View File
@@ -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
+6
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
- v1.1.0
- Change License by Anthony Oteri 0e4219b
+5 -2
View File
@@ -1,9 +1,12 @@
# Dredge Release Notes
## Legal
As of version 1.1.0, this software license has been changed from Apache-2.0
to a dual-licensed Apache-2.0 OR MIT license.
## 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
registry which requires authentication will fail.
+6 -13
View File
@@ -1,20 +1,13 @@
#!/usr/bin/bash
#
# Copyright 2023 Anthony Oteri
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
# http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
# http://opensource.org/licenses/MIT>, at your option. This file may not be
# copied, modified, or distributed except according to those terms.
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#!/usr/bin/bash
set -e
+7 -12
View File
@@ -1,20 +1,15 @@
#!/usr/bin/bash
#
# Copyright 2023 Anthony Oteri
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
# http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
# http://opensource.org/licenses/MIT>, at your option. This file may not be
# copied, modified, or distributed except according to those terms.
#
#!/usr/bin/bash -e
set -e
version=$1
previous=$2
+7 -12
View File
@@ -1,20 +1,15 @@
#!/usr/bin/bash
#
# Copyright 2023 Anthony Oteri
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
# http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
# http://opensource.org/licenses/MIT>, at your option. This file may not be
# copied, modified, or distributed except according to those terms.
#
#!/usr/bin/bash
set -e
REPO_ROOT=$(git rev-parse --show-toplevel)
+91 -18
View File
@@ -1,24 +1,20 @@
/*
* Copyright 2023 Anthony Oteri
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
* http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
* http://opensource.org/licenses/MIT>, at your option. This file may not be
* copied, modified, or distributed except according to those terms.
*/
use http::header;
use serde::Deserialize;
use url::Url;
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
/// set.
///
@@ -48,8 +44,13 @@ pub async fn fetch_paginated<T: for<'de> Deserialize<'de>>(
let url = origin.join(&next_path)?;
let resp = reqwest::get(url).await?;
parse_response_status(&resp)?;
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))? {
next_path = p;
@@ -126,7 +127,7 @@ pub fn parse_response_status(response: &reqwest::Response) -> Result<(), ApiErro
log::trace!("parse_response_status(response: {response:?})");
match response.status() {
http::StatusCode::OK => {
http::StatusCode::OK | http::StatusCode::ACCEPTED => {
let headers = response.headers();
if let Some(header_value) = headers.get("Docker-Distribution-API-Version") {
if header_value.to_str()? == "registry/2.0" {
@@ -140,6 +141,7 @@ pub fn parse_response_status(response: &reqwest::Response) -> Result<(), ApiErro
))
}
}
http::StatusCode::METHOD_NOT_ALLOWED => Err(ApiError::MethodNotAllowed),
http::StatusCode::UNAUTHORIZED => {
let headers = response.headers();
if let Some(header_value) = headers.get("Docker-Distribution-API-Version") {
@@ -155,12 +157,39 @@ pub fn parse_response_status(response: &reqwest::Response) -> Result<(), ApiErro
}
}
http::StatusCode::NOT_FOUND => Err(ApiError::NotFound),
_ => Err(ApiError::UnexpectedResponse(
"Undocumented status code".into(),
)),
e => Err(ApiError::UnexpectedResponse(format!(
"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)]
mod tests {
use http::header::HeaderValue;
@@ -171,7 +200,7 @@ mod tests {
///
/// Attempt to parse a valid RFC5988 header value, and ensure that the
/// parsed URL was returned as expected.
#[async_std::test]
#[tokio::test]
async fn test_parse_rfc5988_valid() {
// Mock a valid RFC5988 header value
let valid_header_value =
@@ -189,7 +218,7 @@ mod tests {
///
/// Attempt to parse an invalid string as RFC5988, ensuring that the `None`
/// variant is returned.
#[async_std::test]
#[tokio::test]
async fn test_parse_rfc5988_invalid() {
// Mock a valid RFC5988 header value
let invalid_header_value = HeaderValue::from_str(r#"invalid header value"#)
@@ -201,4 +230,48 @@ mod tests {
// Assert that the function returned the expected URL as Some(String)
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(())
}
}
+4 -11
View File
@@ -1,17 +1,10 @@
/*
* Copyright 2023 Anthony Oteri
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
* http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
* http://opensource.org/licenses/MIT>, at your option. This file may not be
* copied, modified, or distributed except according to those terms.
*/
#![allow(unused_imports)]
+40 -28
View File
@@ -1,17 +1,10 @@
/*
* Copyright 2023 Anthony Oteri
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
* http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
* http://opensource.org/licenses/MIT>, at your option. This file may not be
* copied, modified, or distributed except according to those terms.
*/
use std::io::Write;
@@ -128,6 +121,8 @@ pub async fn show_handler(
let url = registry_url.join(&path)?;
let resp = reqwest::get(url).await?;
api::parse_response_status(&resp)?;
let headers = resp.headers();
let digest: String = String::from(
headers
@@ -173,7 +168,17 @@ pub async fn delete_handler(
tag: &str,
) -> Result<(), ApiError> {
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.
@@ -190,8 +195,8 @@ pub async fn check_handler(buf: &mut dyn Write, registry_url: &Url) -> Result<()
let path = "/v2";
let url = registry_url.join(path)?;
let response = reqwest::get(url).await?;
api::parse_response_status(&response)?;
let resp = reqwest::get(url).await?;
api::parse_response_status(&resp)?;
writeln!(buf, "Ok")?;
Ok(())
}
@@ -212,7 +217,7 @@ mod tests {
/// 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
/// expected number of times, and did not return an error.
#[async_std::test]
#[tokio::test]
async fn test_catalog_handler() {
let mut server = mockito::Server::new_async().await;
let path = "/v2/_catalog";
@@ -222,12 +227,13 @@ mod tests {
.mock("GET", path)
.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_body(r#"{"repositories": ["image1", "image2", "image3"]}"#)
.create();
let mut buf: Vec<u8> = Vec::new();
let result = catalog_handler(&mut buf, &registry_url).await;
assert!(result.is_ok());
assert!(result.is_ok(), "{:?}", result.unwrap_err());
assert_eq!(String::from_utf8(buf).unwrap(), *"image1\nimage2\nimage3\n");
mock_response.assert();
@@ -240,7 +246,7 @@ mod tests {
/// should follow, resulting in the combined list. It checks that the
/// handler both called the request the expected number of times, and did
/// not return an error.
#[async_std::test]
#[tokio::test]
async fn test_catalog_handler_with_pagination() {
let mut server = mockito::Server::new_async().await;
let path = "/v2/_catalog";
@@ -251,6 +257,7 @@ mod tests {
.mock("GET", path)
.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(
http::header::LINK.as_str(),
&format!(r#"<{path2}>; rel=next"#),
@@ -262,12 +269,13 @@ mod tests {
.mock("GET", path2)
.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_body(r#"{"repositories": ["image3"]}"#)
.create();
let mut buf: Vec<u8> = Vec::new();
let result = catalog_handler(&mut buf, &registry_url).await;
assert!(result.is_ok());
assert!(result.is_ok(), "{:?}", result.unwrap_err());
assert_eq!(String::from_utf8(buf).unwrap(), *"image1\nimage2\nimage3\n");
mock_response.assert();
@@ -279,7 +287,7 @@ mod tests {
/// 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
/// expected number of times, and did not return an error.
#[async_std::test]
#[tokio::test]
async fn test_tags_handler() {
let mut server = mockito::Server::new_async().await;
let path = "/v2/some_image/tags/list";
@@ -290,12 +298,13 @@ mod tests {
.mock("GET", path)
.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_body(r#"{"tags": ["tag1", "tag2", "tag3"]}"#)
.create();
let mut buf: Vec<u8> = Vec::new();
let result = tags_handler(&mut buf, &registry_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");
mock_response.assert();
@@ -308,7 +317,7 @@ mod tests {
/// should follow, resulting in the combined list. It checks that the
/// handler both called the request the expected number of times, and did
/// not return an error.
#[async_std::test]
#[tokio::test]
async fn test_tags_handler_with_pagination() {
let mut server = mockito::Server::new_async().await;
let path = "/v2/some_image/tags/list";
@@ -320,6 +329,7 @@ mod tests {
.mock("GET", path)
.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(
http::header::LINK.as_str(),
&format!(r#"<{path2}>; rel=next"#),
@@ -331,12 +341,13 @@ mod tests {
.mock("GET", path2)
.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_body(r#"{"tags": ["tag3"]}"#)
.create();
let mut buf: Vec<u8> = Vec::new();
let result = tags_handler(&mut buf, &registry_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");
mock_response.assert();
@@ -348,7 +359,7 @@ mod tests {
/// 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
/// expected number of times, and did not return an error.
#[async_std::test]
#[tokio::test]
async fn test_check_handler() {
let mut server = mockito::Server::new_async().await;
let path = "/v2";
@@ -364,7 +375,7 @@ mod tests {
let mut buf: Vec<u8> = Vec::new();
let result = check_handler(&mut buf, &registry_url).await;
assert!(result.is_ok());
assert!(result.is_ok(), "{:?}", result.unwrap_err());
assert_eq!(String::from_utf8(buf).unwrap(), *"Ok\n");
mock_response.assert();
@@ -374,7 +385,7 @@ mod tests {
///
/// This validates that if the "Docker-Distribution-API-Version" header
/// 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>> {
let mut server = mockito::Server::new_async().await;
let path = "/v2";
@@ -407,7 +418,7 @@ mod tests {
/// This validates that if the "Docker-Distribution-API-Version" header
/// is present in the response but contains an unexpected value, the
/// appropriate error is returned.
#[async_std::test]
#[tokio::test]
async fn test_check_handler_invalid_api_version() -> Result<(), Box<dyn Error>> {
let mut server = mockito::Server::new_async().await;
let path = "/v2";
@@ -441,7 +452,7 @@ mod tests {
/// 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
/// the expected number of times, and did not return an error.
#[async_std::test]
#[tokio::test]
async fn test_show_handler() {
let mut server = mockito::Server::new_async().await;
let path = "/v2/foo/manifests/latest";
@@ -492,6 +503,7 @@ mod tests {
.mock("GET", path)
.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",
@@ -517,7 +529,7 @@ mod tests {
etag: sha256:0259571889ac87efbfca5b79a0abe9baf626d058ec5f9a5744bace2229d9ed50\n"
};
assert!(result.is_ok());
assert!(result.is_ok(), "{:?}", result.unwrap_err());
assert_eq!(String::from_utf8(buf).unwrap(), *expected_body);
mock_response.assert();
+10 -11
View File
@@ -1,17 +1,10 @@
/*
* Copyright 2023 Anthony Oteri
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
* http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
* http://opensource.org/licenses/MIT>, at your option. This file may not be
* copied, modified, or distributed except according to those terms.
*/
#![allow(clippy::enum_variant_names)]
@@ -32,6 +25,9 @@ pub enum DredgeError {
#[error(transparent)]
IOError(#[from] std::io::Error),
#[error(transparent)]
LoggerError(#[from] log::SetLoggerError),
}
/// An error related to the communication with the registry API.
@@ -65,6 +61,9 @@ pub enum ApiError {
#[error(transparent)]
SerializerError(#[from] serde_yaml::Error),
#[error("Method not allowed")]
MethodNotAllowed,
}
impl From<reqwest::header::ToStrError> for ApiError {
+12 -13
View File
@@ -1,17 +1,10 @@
/*
* Copyright 2023 Anthony Oteri
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
* http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
* http://opensource.org/licenses/MIT>, at your option. This file may not be
* copied, modified, or distributed except according to those terms.
*/
#![deny(clippy::pedantic)]
@@ -19,6 +12,7 @@
use std::io::{self, Write};
use clap::Parser;
use simple_logger::SimpleLogger;
use url::Url;
use crate::cli::Cli;
@@ -56,13 +50,18 @@ fn parse_registry_arg(host: &str) -> Result<Url, DredgeError> {
Url::parse(&host).or(Err(DredgeError::RegistryUrlError(host.to_string())))
}
#[async_std::main]
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), DredgeError> {
let args = Cli::parse();
// -- Initialize logging
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
let registry_url: Url = parse_registry_arg(&args.registry)?;