mirror of
https://github.com/anthonyoteri/dredge.git
synced 2026-06-05 15:26:53 -04:00
Support detail view of a tagged image
Running the `dredge <REGISTRY> show <image> <tag>` now responds with additional output in the form: ```yaml name: foobar tag: latest architecture: amd64 fsLayers: - blobSum: sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4 - blobSum: sha256:d4228a16bba21ff0eabab548df8f4933103d1a83e5894216c7eb32e3058a8e5e - blobSum: sha256:1f25ec90921b9d511541f9d38ce2b339de2afffc586e55d75b7345b2057f1993 - blobSum: sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4 - blobSum: sha256:7d97e254a0461b0a30b3f443f1daa0d620a3cc6ff4e2714cc1cfd96ace5b7a7e digest: sha256:0259571889ac87efbfca5b79a0abe9baf626d058ec5f9a5744bace2229d9ed50 etag: sha256:0259571889ac87efbfca5b79a0abe9baf626d058ec5f9a5744bace2229d9ed50 ```
This commit is contained in:
@@ -25,10 +25,12 @@ async-std = { version = "1.12.0", features = ["async-attributes", "attributes",
|
|||||||
clap = { version = "4.4.3", features = ["derive", "env", "wrap_help"] }
|
clap = { version = "4.4.3", features = ["derive", "env", "wrap_help"] }
|
||||||
femme = "2.2.1"
|
femme = "2.2.1"
|
||||||
http = "0.2.9"
|
http = "0.2.9"
|
||||||
|
indoc = "2.0.4"
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
reqwest = { version = "0.11.20", features = ["json", "gzip", "multipart", "native-tls-vendored"] }
|
reqwest = { version = "0.11.20", features = ["json", "gzip", "multipart", "native-tls-vendored"] }
|
||||||
serde = { version = "1.0.188", features = ["derive"] }
|
serde = { version = "1.0.188", features = ["derive"] }
|
||||||
serde_toml = "0.0.1"
|
serde_toml = "0.0.1"
|
||||||
|
serde_yaml = "0.9.25"
|
||||||
thiserror = "1.0.48"
|
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"] }
|
||||||
|
|||||||
+142
-3
@@ -17,6 +17,7 @@
|
|||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::api;
|
use crate::api;
|
||||||
@@ -94,16 +95,66 @@ pub async fn tags_handler(
|
|||||||
///
|
///
|
||||||
/// Returns an `ApiError` if there is a problem fetching the manifest or if there
|
/// Returns an `ApiError` if there is a problem fetching the manifest or if there
|
||||||
/// is a problem parsing the response from the Docker Registry API.
|
/// is a problem parsing the response from the Docker Registry API.
|
||||||
#[allow(clippy::unused_async)]
|
#[allow(clippy::similar_names)]
|
||||||
pub async fn show_handler(
|
pub async fn show_handler(
|
||||||
_buf: &mut dyn Write,
|
buf: &mut dyn Write,
|
||||||
registry_url: &Url,
|
registry_url: &Url,
|
||||||
image: &str,
|
image: &str,
|
||||||
tag: &str,
|
tag: &str,
|
||||||
) -> Result<(), ApiError> {
|
) -> Result<(), ApiError> {
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct FsLayer {
|
||||||
|
blob_sum: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
name: String,
|
||||||
|
tag: String,
|
||||||
|
architecture: String,
|
||||||
|
|
||||||
|
#[serde(rename = "fsLayers")]
|
||||||
|
fslayers: Vec<FsLayer>,
|
||||||
|
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
digest: String,
|
||||||
|
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
etag: String,
|
||||||
|
}
|
||||||
log::trace!("show_handler(registry_url: {registry_url:?}, image: {image}, tag: {tag})");
|
log::trace!("show_handler(registry_url: {registry_url:?}, image: {image}, tag: {tag})");
|
||||||
let path = format!("/v2/{image}/manifests/{tag}");
|
let path = format!("/v2/{image}/manifests/{tag}");
|
||||||
let _url = registry_url.join(&path)?;
|
let url = registry_url.join(&path)?;
|
||||||
|
|
||||||
|
let resp = reqwest::get(url).await?;
|
||||||
|
let headers = resp.headers();
|
||||||
|
let digest: String = String::from(
|
||||||
|
headers
|
||||||
|
.get("docker-content-digest")
|
||||||
|
.ok_or(ApiError::UnexpectedResponse(String::from(
|
||||||
|
"Missing docker-content-digest header",
|
||||||
|
)))?
|
||||||
|
.to_str()?,
|
||||||
|
);
|
||||||
|
|
||||||
|
let etag: String = String::from(
|
||||||
|
headers
|
||||||
|
.get("etag")
|
||||||
|
.ok_or(ApiError::UnexpectedResponse(String::from(
|
||||||
|
"Missing etag header",
|
||||||
|
)))?
|
||||||
|
.to_str()?
|
||||||
|
.strip_prefix("'\"")
|
||||||
|
.and_then(|s| s.strip_suffix("\"'"))
|
||||||
|
.unwrap_or(&digest),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut body: Response = resp.json().await?;
|
||||||
|
body.digest = digest;
|
||||||
|
body.etag = etag;
|
||||||
|
|
||||||
|
serde_yaml::to_writer(buf, &body)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +200,7 @@ pub async fn check_handler(buf: &mut dyn Write, registry_url: &Url) -> Result<()
|
|||||||
mod tests {
|
mod tests {
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
|
use indoc::indoc;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::error;
|
use crate::error;
|
||||||
@@ -383,4 +435,91 @@ mod tests {
|
|||||||
mock_response.assert();
|
mock_response.assert();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validate the happy path for the show handler.
|
||||||
|
///
|
||||||
|
/// 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]
|
||||||
|
async fn test_show_handler() {
|
||||||
|
let mut server = mockito::Server::new_async().await;
|
||||||
|
let path = "/v2/foo/manifests/latest";
|
||||||
|
|
||||||
|
let response_body = r#"
|
||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"name": "foo",
|
||||||
|
"tag": "latest",
|
||||||
|
"architecture": "amd64",
|
||||||
|
"fsLayers": [
|
||||||
|
{
|
||||||
|
"blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"blobSum": "sha256:7d97e254a0461b0a30b3f443f1daa0d620a3cc6ff4e2714cc1cfd96ace5b7a7e"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"history": [
|
||||||
|
{
|
||||||
|
"v1Compatibility": "{\"id\":\"7fe38ce3fe63caeaacf6be64933d0d55adc5c5f48762b20ec6129d1a41691a84\",\"parent\":\"8ca907037d044ff942e9c95562b786f1913d3b05a4bda16ad3ed3e7ee67e8c76\",\"created\":\"2023-09-07T00:21:13.838729514Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) CMD [\\\"bash\\\"]\"]},\"throwaway\":true}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"v1Compatibility": "{\"id\":\"8ca907037d044ff942e9c95562b786f1913d3b05a4bda16ad3ed3e7ee67e8c76\",\"created\":\"2023-09-07T00:21:13.444807009Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ADD file:cb5fcc80c057b356a31492a20c6e3a75b70ed70a663506c8e97ad730ae32a02d in / \"]}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"jwk": {
|
||||||
|
"crv": "P-256",
|
||||||
|
"kid": "7ZLW:DJCO:GYG4:DCZD:TRO6:QW3Y:Q7Q3:PTXB:JDQX:4DLY:NB2B:4GJJ",
|
||||||
|
"kty": "EC",
|
||||||
|
"x": "LXquBoF1_XI3fawa-7UW9Y1Le7j7FiDGS3KB_4gF5hY",
|
||||||
|
"y": "UT5SniKpELMqL-j9YwL2fZLUHmRIFwori9rUBG18b_k"
|
||||||
|
},
|
||||||
|
"alg": "ES256"
|
||||||
|
},
|
||||||
|
"signature": "5_paRRhUCmwkAZJrjBfbvOJ341atEjUQuhG7i4kITyG3e_U2yuDqs9X7bHHMtmUTbChSp59NHi124uauAjoxIg",
|
||||||
|
"protected": "eyJmb3JtYXRMZW5ndGgiOjI3MDIsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAyMy0wOS0yN1QxMzoyMTo1MloifQ"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
// 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("GET", path)
|
||||||
|
.with_status(http::status::StatusCode::OK.as_u16().into())
|
||||||
|
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
||||||
|
.with_header(
|
||||||
|
"docker-content-digest",
|
||||||
|
"sha256:0259571889ac87efbfca5b79a0abe9baf626d058ec5f9a5744bace2229d9ed50",
|
||||||
|
)
|
||||||
|
.with_header(
|
||||||
|
"etag",
|
||||||
|
"sha256:0259571889ac87efbfca5b79a0abe9baf626d058ec5f9a5744bace2229d9ed50",
|
||||||
|
)
|
||||||
|
.with_body(response_body)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
|
let result = show_handler(&mut buf, ®istry_url, "foo", "latest").await;
|
||||||
|
|
||||||
|
let expected_body = indoc! {"
|
||||||
|
name: foo
|
||||||
|
tag: latest
|
||||||
|
architecture: amd64
|
||||||
|
fsLayers:
|
||||||
|
- blobSum: sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
|
||||||
|
- blobSum: sha256:7d97e254a0461b0a30b3f443f1daa0d620a3cc6ff4e2714cc1cfd96ace5b7a7e
|
||||||
|
digest: sha256:0259571889ac87efbfca5b79a0abe9baf626d058ec5f9a5744bace2229d9ed50
|
||||||
|
etag: sha256:0259571889ac87efbfca5b79a0abe9baf626d058ec5f9a5744bace2229d9ed50\n"
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(String::from_utf8(buf).unwrap(), *expected_body);
|
||||||
|
|
||||||
|
mock_response.assert();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ pub enum ApiError {
|
|||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
IOError(#[from] std::io::Error),
|
IOError(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
SerializerError(#[from] serde_yaml::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<reqwest::header::ToStrError> for ApiError {
|
impl From<reqwest::header::ToStrError> for ApiError {
|
||||||
|
|||||||
Reference in New Issue
Block a user