m/cup
1
0
mirror of https://github.com/sergi0g/cup.git synced 2025-11-14 16:13:48 -05:00
This commit is contained in:
Sergio
2024-09-01 19:57:15 +03:00
parent 2f195f611c
commit 1ba67c8af0
7 changed files with 110 additions and 54 deletions

View File

@@ -1,9 +1,17 @@
use std::{collections::{HashMap, HashSet}, sync::Mutex}; use std::{
collections::{HashMap, HashSet},
sync::Mutex,
};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use json::JsonValue; use json::JsonValue;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use crate::{docker::get_images_from_docker_daemon, image::Image, registry::{check_auth, get_token, get_latest_digests}, utils::unsplit_image}; use crate::{
docker::get_images_from_docker_daemon,
image::Image,
registry::{check_auth, get_latest_digests, get_token},
utils::unsplit_image,
};
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
use crate::docker::get_image_from_docker_daemon; use crate::docker::get_image_from_docker_daemon;
@@ -25,7 +33,10 @@ where
} }
} }
pub async fn get_all_updates(socket: Option<String>, config: &JsonValue) -> Vec<(String, Option<bool>)> { pub async fn get_all_updates(
socket: Option<String>,
config: &JsonValue,
) -> Vec<(String, Option<bool>)> {
let image_map_mutex: Mutex<HashMap<String, &Option<String>>> = Mutex::new(HashMap::new()); let image_map_mutex: Mutex<HashMap<String, &Option<String>>> = Mutex::new(HashMap::new());
let local_images = get_images_from_docker_daemon(socket).await; let local_images = get_images_from_docker_daemon(socket).await;
local_images.par_iter().for_each(|image| { local_images.par_iter().for_each(|image| {
@@ -44,7 +55,10 @@ pub async fn get_all_updates(socket: Option<String>, config: &JsonValue) -> Vec<
.par_iter() .par_iter()
.filter(|image| &image.registry == registry) .filter(|image| &image.registry == registry)
.collect(); .collect();
let credentials = config["authentication"][registry].clone().take_string().or(None); let credentials = config["authentication"][registry]
.clone()
.take_string()
.or(None);
let mut latest_images = match check_auth(registry, config) { let mut latest_images = match check_auth(registry, config) {
Some(auth_url) => { Some(auth_url) => {
let token = get_token(images.clone(), &auth_url, &credentials); let token = get_token(images.clone(), &auth_url, &credentials);
@@ -72,7 +86,10 @@ pub async fn get_all_updates(socket: Option<String>, config: &JsonValue) -> Vec<
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
pub async fn get_update(image: &str, socket: Option<String>, config: &JsonValue) -> Option<bool> { pub async fn get_update(image: &str, socket: Option<String>, config: &JsonValue) -> Option<bool> {
let local_image = get_image_from_docker_daemon(socket, image).await; let local_image = get_image_from_docker_daemon(socket, image).await;
let credentials = config["authentication"][&local_image.registry].clone().take_string().or(None); let credentials = config["authentication"][&local_image.registry]
.clone()
.take_string()
.or(None);
let token = match check_auth(&local_image.registry, config) { let token = match check_auth(&local_image.registry, config) {
Some(auth_url) => get_token(vec![&local_image], &auth_url, &credentials), Some(auth_url) => get_token(vec![&local_image], &auth_url, &credentials),
None => String::new(), None => String::new(),
@@ -85,4 +102,4 @@ pub async fn get_update(image: &str, socket: Option<String>, config: &JsonValue)
Some(d) => Some(d != &local_image.digest.unwrap()), Some(d) => Some(d != &local_image.digest.unwrap()),
None => None, None => None,
} }
} }

View File

@@ -1,7 +1,4 @@
use bollard::{ use bollard::{secret::ImageSummary, ClientVersion, Docker};
secret::ImageSummary,
ClientVersion, Docker,
};
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
use bollard::secret::ImageInspect; use bollard::secret::ImageInspect;

View File

@@ -58,7 +58,7 @@ pub fn print_update(name: &str, has_update: &Option<bool>) {
} }
pub fn print_raw_update(name: &str, has_update: &Option<bool>) { pub fn print_raw_update(name: &str, has_update: &Option<bool>) {
let result = object! {images: {[name]: *has_update}} ; let result = object! {images: {[name]: *has_update}};
println!("{}", result); println!("{}", result);
} }

View File

@@ -1,8 +1,8 @@
#[cfg(feature = "cli")]
use check::{get_all_updates, get_update};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
use formatting::{print_raw_update, print_raw_updates, print_update, print_updates, Spinner}; use formatting::{print_raw_update, print_raw_updates, print_update, print_updates, Spinner};
#[cfg(feature = "cli")]
use check::{get_all_updates, get_update};
#[cfg(feature = "server")] #[cfg(feature = "server")]
use server::serve; use server::serve;
use std::path::PathBuf; use std::path::PathBuf;
@@ -10,13 +10,13 @@ use utils::load_config;
pub mod check; pub mod check;
pub mod docker; pub mod docker;
pub mod image;
pub mod registry;
pub mod utils;
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
pub mod formatting; pub mod formatting;
pub mod image;
pub mod registry;
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub mod server; pub mod server;
pub mod utils;
#[derive(Parser)] #[derive(Parser)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]

View File

@@ -9,33 +9,45 @@ use http_auth::parse_challenges;
use crate::{error, image::Image, warn}; use crate::{error, image::Image, warn};
pub fn check_auth(registry: &str, config: &JsonValue) -> Option<String> { pub fn check_auth(registry: &str, config: &JsonValue) -> Option<String> {
let protocol = if config["insecure_registries"].contains(registry) { "http" } else { "https" }; let protocol = if config["insecure_registries"].contains(registry) {
"http"
} else {
"https"
};
let response = ureq::get(&format!("{}://{}/v2/", protocol, registry)).call(); let response = ureq::get(&format!("{}://{}/v2/", protocol, registry)).call();
match response { match response {
Ok(_) => None, Ok(_) => None,
Err(Error::Status(401, response)) => match response.header("www-authenticate") { Err(Error::Status(401, response)) => match response.header("www-authenticate") {
Some(challenge) => Some(parse_www_authenticate(challenge)), Some(challenge) => Some(parse_www_authenticate(challenge)),
None => error!("Unauthorized to access registry {} and no way to authenticate was provided", registry), None => error!(
"Unauthorized to access registry {} and no way to authenticate was provided",
registry
),
}, },
Err(Error::Transport(error)) => { Err(Error::Transport(error)) => {
match error.kind() { match error.kind() {
ErrorKind::Dns => { ErrorKind::Dns => {
warn!("Failed to lookup the IP of the registry, retrying."); warn!("Failed to lookup the IP of the registry, retrying.");
return check_auth(registry, config) return check_auth(registry, config);
}, // If something goes really wrong, this can get stuck in a loop } // If something goes really wrong, this can get stuck in a loop
ErrorKind::ConnectionFailed => { ErrorKind::ConnectionFailed => {
warn!("Connection probably timed out, retrying."); warn!("Connection probably timed out, retrying.");
return check_auth(registry, config) return check_auth(registry, config);
}, // Same here } // Same here
_ => error!("{}", error) _ => error!("{}", error),
} }
}, }
Err(e) => error!("{}", e), Err(e) => error!("{}", e),
} }
} }
pub fn get_latest_digest(image: &Image, token: Option<&String>, config: &JsonValue) -> Image { pub fn get_latest_digest(image: &Image, token: Option<&String>, config: &JsonValue) -> Image {
let protocol = if config["insecure_registries"].contains(json::JsonValue::from(image.registry.clone())) { "http" } else { "https" }; let protocol =
if config["insecure_registries"].contains(json::JsonValue::from(image.registry.clone())) {
"http"
} else {
"https"
};
let mut request = ureq::head(&format!( let mut request = ureq::head(&format!(
"{}://{}/v2/{}/manifests/{}", "{}://{}/v2/{}/manifests/{}",
protocol, &image.registry, &image.repository, &image.tag protocol, &image.registry, &image.repository, &image.tag
@@ -93,7 +105,11 @@ pub fn get_latest_digest(image: &Image, token: Option<&String>, config: &JsonVal
} }
} }
pub fn get_latest_digests(images: Vec<&Image>, token: Option<&String>, config: &JsonValue) -> Vec<Image> { pub fn get_latest_digests(
images: Vec<&Image>,
token: Option<&String>,
config: &JsonValue,
) -> Vec<Image> {
let result: Mutex<Vec<Image>> = Mutex::new(Vec::new()); let result: Mutex<Vec<Image>> = Mutex::new(Vec::new());
images.par_iter().for_each(|&image| { images.par_iter().for_each(|&image| {
let digest = get_latest_digest(image, token, config).digest; let digest = get_latest_digest(image, token, config).digest;
@@ -111,13 +127,13 @@ pub fn get_token(images: Vec<&Image>, auth_url: &str, credentials: &Option<Strin
for image in &images { for image in &images {
final_url = format!("{}&scope=repository:{}:pull", final_url, image.repository); final_url = format!("{}&scope=repository:{}:pull", final_url, image.repository);
} }
let mut base_request = ureq::get(&final_url).set("Accept", "application/vnd.oci.image.index.v1+json"); // Seems to be unnecesarry. Will probably remove in the future let mut base_request =
ureq::get(&final_url).set("Accept", "application/vnd.oci.image.index.v1+json"); // Seems to be unnecesarry. Will probably remove in the future
base_request = match credentials { base_request = match credentials {
Some(creds) => base_request.set("Authorization", &format!("Basic {}", creds)), Some(creds) => base_request.set("Authorization", &format!("Basic {}", creds)),
None => base_request None => base_request,
}; };
let raw_response = match base_request.call() let raw_response = match base_request.call() {
{
Ok(response) => match response.into_string() { Ok(response) => match response.into_string() {
Ok(res) => res, Ok(res) => res,
Err(e) => { Err(e) => {
@@ -128,15 +144,15 @@ pub fn get_token(images: Vec<&Image>, auth_url: &str, credentials: &Option<Strin
match error.kind() { match error.kind() {
ErrorKind::Dns => { ErrorKind::Dns => {
warn!("Failed to lookup the IP of the registry, retrying."); warn!("Failed to lookup the IP of the registry, retrying.");
return get_token(images, auth_url, credentials) return get_token(images, auth_url, credentials);
}, // If something goes really wrong, this can get stuck in a loop } // If something goes really wrong, this can get stuck in a loop
ErrorKind::ConnectionFailed => { ErrorKind::ConnectionFailed => {
warn!("Connection probably timed out, retrying."); warn!("Connection probably timed out, retrying.");
return get_token(images, auth_url, credentials) return get_token(images, auth_url, credentials);
}, // Same here } // Same here
_ => error!("Token request failed\n{}!", error) _ => error!("Token request failed\n{}!", error),
} }
}, }
Err(e) => { Err(e) => {
error!("Token request failed!\n{}", e) error!("Token request failed!\n{}", e)
} }

View File

@@ -43,20 +43,44 @@ pub async fn serve(port: &u16, socket: Option<String>, config: JsonValue) -> std
async fn _static(data: StateRef<'_, Arc<Mutex<ServerData>>>, path: PathRef<'_>) -> WebResponse { async fn _static(data: StateRef<'_, Arc<Mutex<ServerData>>>, path: PathRef<'_>) -> WebResponse {
match path.0 { match path.0 {
"/" => WebResponse::builder().header("Content-Type", "text/html").body(ResponseBody::from(HTML)).unwrap(), "/" => WebResponse::builder()
"/assets/index.js" => WebResponse::builder().header("Content-Type", "text/javascript").body(ResponseBody::from(JS.replace("=\"neutral\"", &format!("=\"{}\"", data.lock().await.theme)))).unwrap(), .header("Content-Type", "text/html")
"/assets/index.css" => WebResponse::builder().header("Content-Type", "text/css").body(ResponseBody::from(CSS)).unwrap(), .body(ResponseBody::from(HTML))
"/favicon.ico" => WebResponse::builder().header("Content-Type", "image/vnd.microsoft.icon").body(ResponseBody::from(FAVICON_ICO)).unwrap(), .unwrap(),
"/favicon.svg" => WebResponse::builder().header("Content-Type", "image/svg+xml").body(ResponseBody::from(FAVICON_SVG)).unwrap(), "/assets/index.js" => WebResponse::builder()
"/apple-touch-icon.png" => WebResponse::builder().header("Content-Type", "image/png").body(ResponseBody::from(APPLE_TOUCH_ICON)).unwrap(), .header("Content-Type", "text/javascript")
_ => WebResponse::builder().status(404).body(ResponseBody::from("Not found")).unwrap() .body(ResponseBody::from(JS.replace(
"=\"neutral\"",
&format!("=\"{}\"", data.lock().await.theme),
)))
.unwrap(),
"/assets/index.css" => WebResponse::builder()
.header("Content-Type", "text/css")
.body(ResponseBody::from(CSS))
.unwrap(),
"/favicon.ico" => WebResponse::builder()
.header("Content-Type", "image/vnd.microsoft.icon")
.body(ResponseBody::from(FAVICON_ICO))
.unwrap(),
"/favicon.svg" => WebResponse::builder()
.header("Content-Type", "image/svg+xml")
.body(ResponseBody::from(FAVICON_SVG))
.unwrap(),
"/apple-touch-icon.png" => WebResponse::builder()
.header("Content-Type", "image/png")
.body(ResponseBody::from(APPLE_TOUCH_ICON))
.unwrap(),
_ => WebResponse::builder()
.status(404)
.body(ResponseBody::from("Not found"))
.unwrap(),
} }
} }
async fn json(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse { async fn json(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
WebResponse::new(ResponseBody::from( WebResponse::new(ResponseBody::from(json::stringify(
json::stringify(data.lock().await.json.clone()) data.lock().await.json.clone(),
)) )))
} }
async fn refresh(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse { async fn refresh(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
@@ -69,7 +93,7 @@ struct ServerData {
json: JsonValue, json: JsonValue,
socket: Option<String>, socket: Option<String>,
config: JsonValue, config: JsonValue,
theme: &'static str theme: &'static str,
} }
impl ServerData { impl ServerData {
@@ -82,13 +106,15 @@ impl ServerData {
}, },
raw_updates: Vec::new(), raw_updates: Vec::new(),
config, config,
theme: "neutral" theme: "neutral",
}; };
s.refresh().await; s.refresh().await;
s s
} }
async fn refresh(&mut self) { async fn refresh(&mut self) {
let updates = sort_update_vec(&get_all_updates(self.socket.clone(), &self.config["authentication"]).await); let updates = sort_update_vec(
&get_all_updates(self.socket.clone(), &self.config["authentication"]).await,
);
self.raw_updates = updates; self.raw_updates = updates;
self.json = to_json(&self.raw_updates); self.json = to_json(&self.raw_updates);
let last_updated = Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true); let last_updated = Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);

View File

@@ -32,9 +32,9 @@ pub fn split_image(image: &str) -> (String, String, String) {
match RE.captures(image) { match RE.captures(image) {
Some(c) => { Some(c) => {
let registry = match c.name("registry") { let registry = match c.name("registry") {
Some(registry) => registry.as_str().to_owned(), Some(registry) => registry.as_str().to_owned(),
None => String::from("registry-1.docker.io"), None => String::from("registry-1.docker.io"),
}; };
return ( return (
registry.clone(), registry.clone(),
match c.name("repository") { match c.name("repository") {
@@ -52,7 +52,7 @@ pub fn split_image(image: &str) -> (String, String, String) {
Some(tag) => tag.as_str().to_owned(), Some(tag) => tag.as_str().to_owned(),
None => String::from("latest"), None => String::from("latest"),
}, },
) );
} }
None => error!("Failed to parse image {}", image), None => error!("Failed to parse image {}", image),
} }