m/cup
1
0
mirror of https://github.com/sergi0g/cup.git synced 2025-11-19 10:23:42 -05:00

Nearly complete versioning support. Fixed old bugs where not all tags were fetched.

This commit is contained in:
Sergio
2024-11-15 13:21:30 +02:00
parent c11b5e6432
commit d94abecf35
30 changed files with 1288 additions and 962 deletions

View File

@@ -1,60 +1,42 @@
use json::JsonValue;
use http_auth::parse_challenges;
use reqwest_middleware::ClientWithMiddleware;
use itertools::Itertools;
use crate::{
config::Config,
error,
image::{get_version, Image, SemVer},
utils::timestamp,
warn,
http::Client,
structs::{
image::{DigestInfo, Image, VersionInfo},
version::Version,
},
utils::{
link::parse_link,
misc::timestamp,
request::{
get_protocol, get_response_body, parse_json, parse_www_authenticate, to_bearer_string,
},
},
};
pub async fn check_auth(
registry: &str,
config: &Config,
client: &ClientWithMiddleware,
) -> Option<String> {
let protocol = if config.insecure_registries.contains(&registry.to_string()) {
"http"
} else {
"https"
};
let response = client
.get(format!("{}://{}/v2/", protocol, registry))
.send()
.await;
pub async fn check_auth(registry: &str, config: &Config, client: &Client) -> Option<String> {
let protocol = get_protocol(&registry.to_string(), &config.insecure_registries);
let url = format!("{}://{}/v2/", protocol, registry);
let response = client.get(&url, Vec::new(), true).await;
match response {
Ok(r) => {
let status = r.status().as_u16();
Ok(response) => {
let status = response.status();
if status == 401 {
match r.headers().get("www-authenticate") {
Some(challenge) => Some(parse_www_authenticate(challenge.to_str().unwrap())),
None => error!(
"Unauthorized to access registry {} and no way to authenticate was provided",
registry
),
}
} else if status == 200 {
None
match response.headers().get("www-authenticate") {
Some(challenge) => Some(parse_www_authenticate(challenge.to_str().unwrap())),
None => error!(
"Unauthorized to access registry {} and no way to authenticate was provided",
registry
),
}
} else {
warn!(
"Received unexpected status code {}\nResponse: {}",
status,
r.text().await.unwrap()
);
None
}
}
Err(e) => {
if e.is_connect() {
warn!("Connection to registry {} failed.", &registry);
None
} else {
error!("Unexpected error: {}", e.to_string())
}
}
Err(_) => None,
}
}
@@ -62,67 +44,44 @@ pub async fn get_latest_digest(
image: &Image,
token: Option<&String>,
config: &Config,
client: &ClientWithMiddleware,
client: &Client,
) -> Image {
let start = timestamp();
let protocol = if config
.insecure_registries
.contains(&image.registry.clone().unwrap())
{
"http"
} else {
"https"
};
let mut request = client.head(format!(
let protocol = get_protocol(&image.registry, &config.insecure_registries);
let url = format!(
"{}://{}/v2/{}/manifests/{}",
protocol,
&image.registry.as_ref().unwrap(),
&image.repository.as_ref().unwrap(),
&image.tag.as_ref().unwrap()
));
if let Some(t) = token {
request = request.header("Authorization", &format!("Bearer {}", t));
}
let raw_response = match request
.header("Accept", "application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.index.v1+json")
.send().await
{
Ok(response) => {
let status = response.status();
if status == 401 {
if token.is_some() {
warn!("Failed to authenticate to registry {} with token provided!\n{}", image.registry.as_ref().unwrap(), token.unwrap());
return Image { error: Some(format!("Authentication token \"{}\" was not accepted", token.unwrap())), time_ms: timestamp() - start, ..image.clone() }
} else {
warn!("Registry {} requires authentication", image.registry.as_ref().unwrap());
return Image { error: Some("Registry requires authentication".to_string()), time_ms: timestamp() - start, ..image.clone() }
protocol, &image.registry, &image.repository, &image.tag
);
let authorization = to_bearer_string(&token);
let headers = vec![("Accept", Some("application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.index.v1+json")), ("Authorization", authorization.as_deref())];
let response = client.head(&url, headers).await;
match response {
Ok(res) => match res.headers().get("docker-content-digest") {
Some(digest) => {
let local_digests = match &image.digest_info {
Some(data) => data.local_digests.clone(),
None => return image.clone(),
};
Image {
digest_info: Some(DigestInfo {
remote_digest: Some(digest.to_str().unwrap().to_string()),
local_digests,
}),
time_ms: image.time_ms + (timestamp() - start),
..image.clone()
}
} else if status == 404 {
warn!("Image {:?} not found", &image);
return Image { error: Some("Image not found".to_string()), time_ms: timestamp() - start, ..image.clone() }
} else {
response
}
None => error!(
"Server returned invalid response! No docker-content-digest!\n{:#?}",
res
),
},
Err(e) => {
if e.is_connect() {
warn!("Connection to registry {} failed.", image.registry.as_ref().unwrap());
return Image { error: Some("Connection to registry failed".to_string()), time_ms: timestamp() - start, ..image.clone() }
} else {
error!("Unexpected error: {}", e.to_string())
}
},
};
match raw_response.headers().get("docker-content-digest") {
Some(digest) => Image {
remote_digest: Some(digest.to_str().unwrap().to_string()),
time_ms: timestamp() - start,
Err(error) => Image {
error: Some(error),
time_ms: image.time_ms + (timestamp() - start),
..image.clone()
},
None => error!(
"Server returned invalid response! No docker-content-digest!\n{:#?}",
raw_response
),
}
}
@@ -130,182 +89,128 @@ pub async fn get_token(
images: &Vec<&Image>,
auth_url: &str,
credentials: &Option<&String>,
client: &ClientWithMiddleware,
client: &Client,
) -> String {
let mut final_url = auth_url.to_owned();
let mut url = auth_url.to_owned();
for image in images {
final_url = format!(
"{}&scope=repository:{}:pull",
final_url,
image.repository.as_ref().unwrap()
);
url = format!("{}&scope=repository:{}:pull", url, image.repository);
}
let mut base_request = client.get(&final_url);
base_request = match credentials {
Some(creds) => base_request.header("Authorization", &format!("Basic {}", creds)),
None => base_request,
let authorization = credentials.as_ref().map(|creds| format!("Basic {}", creds));
let headers = vec![("Authorization", authorization.as_deref())];
let response = client.get(&url, headers, false).await;
let response_json = match response {
Ok(response) => parse_json(&get_response_body(response).await),
Err(_) => error!("GET {}: Request failed!", url),
};
let raw_response = match base_request.send().await {
Ok(response) => match response.text().await {
Ok(res) => res,
Err(e) => {
error!("Failed to parse registry response into string!\n{}", e)
}
},
Err(e) => {
if e.is_connect() {
error!("Connection to registry failed.");
} else {
error!("Token request failed!\n{}", e.to_string())
}
}
};
let parsed_token_response: JsonValue = match json::parse(&raw_response) {
Ok(parsed) => parsed,
Err(e) => {
error!("Failed to parse server response\n{}", e)
}
};
parsed_token_response["token"].to_string()
response_json["token"].to_string()
}
pub async fn get_latest_tag(
image: &Image,
base: &SemVer,
base: &Version,
token: Option<&String>,
config: &Config,
client: &ClientWithMiddleware,
client: &Client,
) -> Image {
let start = timestamp();
// Start creating request
let protocol = if config
.insecure_registries
.contains(&image.registry.clone().unwrap())
{
"http"
} else {
"https"
};
let mut request = client.get(format!(
let protocol = get_protocol(&image.registry, &config.insecure_registries);
let url = format!(
"{}://{}/v2/{}/tags/list",
protocol,
&image.registry.as_ref().unwrap(),
&image.repository.as_ref().unwrap(),
));
if let Some(t) = token {
request = request.header("Authorization", &format!("Bearer {}", t));
}
protocol, &image.registry, &image.repository,
);
let authorization = to_bearer_string(&token);
let headers = vec![
("Accept", Some("application/json")),
("Authorization", authorization.as_deref()),
];
// Send request
let raw_response = match request.header("Accept", "application/json").send().await {
Ok(response) => {
let status = response.status();
if status == 401 {
if token.is_some() {
warn!(
"Failed to authenticate to registry {} with token provided!\n{}",
image.registry.as_ref().unwrap(),
token.unwrap()
);
let mut tags: Vec<Version> = Vec::new();
let mut next_url = Some(url);
while next_url.is_some() {
let mut new_tags = Vec::new();
(new_tags, next_url) =
match get_extra_tags(&next_url.unwrap(), headers.clone(), base, client).await {
Ok(t) => t,
Err(message) => {
return Image {
error: Some(format!(
"Authentication token \"{}\" was not accepted",
token.unwrap()
)),
time_ms: timestamp() - start,
error: Some(message),
time_ms: image.time_ms + (timestamp() - start),
..image.clone()
};
} else {
warn!(
"Registry {} requires authentication",
image.registry.as_ref().unwrap()
);
return Image {
error: Some("Registry requires authentication".to_string()),
time_ms: timestamp() - start,
..image.clone()
};
}
} else if status == 404 {
warn!("Image {:?} not found", &image);
return Image {
error: Some("Image not found".to_string()),
time_ms: timestamp() - start,
..image.clone()
};
} else {
match response.text().await {
Ok(res) => res,
Err(e) => {
error!("Failed to parse registry response into string!\n{}", e)
}
}
}
}
Err(e) => {
if e.is_connect() {
warn!(
"Connection to registry {} failed.",
image.registry.as_ref().unwrap()
);
return Image {
error: Some("Connection to registry failed".to_string()),
time_ms: timestamp() - start,
..image.clone()
};
} else {
error!("Unexpected error: {}", e.to_string())
}
}
};
let parsed_response: JsonValue = match json::parse(&raw_response) {
Ok(parsed) => parsed,
Err(e) => {
error!("Failed to parse server response\n{}", e)
}
};
let tag = parsed_response["tags"]
.members()
.filter_map(|tag| get_version(&tag.to_string()))
.filter(|tag| match (base.minor, tag.minor) {
(Some(_), Some(_)) | (None, None) => {
matches!((base.patch, tag.patch), (Some(_), Some(_)) | (None, None))
}
_ => false,
})
};
tags.append(&mut new_tags);
}
let tag = tags
.iter()
.max();
let current_tag = match &image.version_info {
Some(data) => data.current_tag.clone(),
_ => unreachable!(),
};
match tag {
Some(t) => {
if t == *base {
if t == base {
// Tags are equal so we'll compare digests
get_latest_digest(image, token, config, client).await
get_latest_digest(
&Image {
version_info: Some(VersionInfo {
current_tag,
latest_remote_tag: Some(t.clone()),
}),
time_ms: image.time_ms + (timestamp() - start),
..image.clone()
},
token,
config,
client,
)
.await
} else {
Image {
latest_remote_tag: Some(t),
time_ms: timestamp() - start,
version_info: Some(VersionInfo {
current_tag,
latest_remote_tag: Some(t.clone()),
}),
time_ms: image.time_ms + (timestamp() - start),
..image.clone()
}
}
}
None => unreachable!(),
None => unreachable!("{:?}", tags),
}
}
fn parse_www_authenticate(www_auth: &str) -> String {
let challenges = parse_challenges(www_auth).unwrap();
if !challenges.is_empty() {
let challenge = &challenges[0];
if challenge.scheme == "Bearer" {
format!(
"{}?service={}",
challenge.params[0].1.as_escaped(),
challenge.params[1].1.as_escaped()
)
} else {
error!("Unsupported scheme {}", &challenge.scheme)
pub async fn get_extra_tags(
url: &str,
headers: Vec<(&str, Option<&str>)>,
base: &Version,
client: &Client,
) -> Result<(Vec<Version>, Option<String>), String> {
let response = client.get(&url, headers, false).await;
match response {
Ok(res) => {
let next_url = match res.headers().get("Link") {
Some(link) => Some(parse_link(link.to_str().unwrap(), &url)),
None => None,
};
let response_json = parse_json(&get_response_body(res).await);
let result = response_json["tags"]
.members()
.filter_map(|tag| Version::from_tag(&tag.to_string()))
.filter(|tag| match (base.minor, tag.minor) {
(Some(_), Some(_)) | (None, None) => {
matches!((base.patch, tag.patch), (Some(_), Some(_)) | (None, None))
}
_ => false,
})
.dedup()
.collect();
Ok((result, next_url))
}
} else {
error!("No challenge provided by the server");
Err(message) => Err(message),
}
}