Skip to content

Commit

Permalink
Added config, custom theming and moved update checking logic to check.rs
Browse files Browse the repository at this point in the history
  • Loading branch information
sergi0g committed Jul 15, 2024
1 parent b9278ca commit 30c762e
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 118 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ http-auth = { version = "0.1.9", features = [] }
termsize = { version = "0.1.8", optional = true }
regex = "1.10.5"
chrono = { version = "0.4.38", default-features = false, features = ["std", "alloc", "clock"] }
home = "0.5.9"

[features]
default = ["server", "cli"]
Expand Down
84 changes: 84 additions & 0 deletions src/check.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use std::{collections::{HashMap, HashSet}, sync::Mutex};

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};
#[cfg(feature = "cli")]
use crate::docker::get_image_from_docker_daemon;
#[cfg(feature = "cli")]
use crate::registry::get_latest_digest;

pub trait Unique<T> {
// So we can filter vecs for duplicates
fn unique(&mut self);
}

impl<T> Unique<T> for Vec<T>
where
T: Clone + Eq + std::hash::Hash,
{
fn unique(self: &mut Vec<T>) {
let mut seen: HashSet<T> = HashSet::new();
self.retain(|item| seen.insert(item.clone()));
}
}

pub async fn get_all_updates(socket: Option<String>) -> Vec<(String, Option<bool>)> {
let image_map_mutex: Mutex<HashMap<String, &Option<String>>> = Mutex::new(HashMap::new());
let local_images = get_images_from_docker_daemon(socket).await;
local_images.par_iter().for_each(|image| {
let img = unsplit_image(&image.registry, &image.repository, &image.tag);
image_map_mutex.lock().unwrap().insert(img, &image.digest);
});
let image_map = image_map_mutex.lock().unwrap().clone();
let mut registries: Vec<&String> = local_images
.par_iter()
.map(|image| &image.registry)
.collect();
registries.unique();
let mut remote_images: Vec<Image> = Vec::new();
for registry in registries {
let images: Vec<&Image> = local_images
.par_iter()
.filter(|image| &image.registry == registry)
.collect();
let mut latest_images = match check_auth(registry) {
Some(auth_url) => {
let token = get_token(images.clone(), &auth_url);
get_latest_digests(images, Some(&token))
}
None => get_latest_digests(images, None),
};
remote_images.append(&mut latest_images);
}
let result_mutex: Mutex<Vec<(String, Option<bool>)>> = Mutex::new(Vec::new());
remote_images.par_iter().for_each(|image| {
let img = unsplit_image(&image.registry, &image.repository, &image.tag);
match &image.digest {
Some(d) => {
let r = d != image_map.get(&img).unwrap().as_ref().unwrap();
result_mutex.lock().unwrap().push((img, Some(r)))
}
None => result_mutex.lock().unwrap().push((img, None)),
}
});
let result = result_mutex.lock().unwrap().clone();
result
}

#[cfg(feature = "cli")]
pub async fn get_update(image: &str, socket: Option<String>) -> Option<bool> {
let local_image = get_image_from_docker_daemon(socket, image).await;
let token = match check_auth(&local_image.registry) {
Some(auth_url) => get_token(vec![&local_image], &auth_url),
None => String::new(),
};
let remote_image = match token.as_str() {
"" => get_latest_digest(&local_image, None),
_ => get_latest_digest(&local_image, Some(&token)),
};
match &remote_image.digest {
Some(d) => Some(d != &local_image.digest.unwrap()),
None => None,
}
}
114 changes: 24 additions & 90 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,14 @@
use check::{get_all_updates, get_update};
use clap::{Parser, Subcommand};
#[cfg(feature = "cli")]
use docker::get_image_from_docker_daemon;
use docker::get_images_from_docker_daemon;
#[cfg(feature = "cli")]
use formatting::{print_raw_update, print_raw_updates, print_update, print_updates, Spinner};
use image::Image;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
#[cfg(feature = "cli")]
use registry::get_latest_digest;
use registry::{check_auth, get_latest_digests, get_token};
#[cfg(feature = "server")]
use server::serve;
use std::{
collections::{HashMap, HashSet},
sync::Mutex,
};
use utils::unsplit_image;
use std::path::PathBuf;
use utils::load_config;

pub mod check;
pub mod docker;
#[cfg(feature = "cli")]
pub mod formatting;
Expand All @@ -31,6 +23,8 @@ pub mod utils;
struct Cli {
#[arg(short, long, default_value = None)]
socket: Option<String>,
#[arg(short, long, default_value_t = String::new(), help = "Config file path")]
config_path: String,
#[command(subcommand)]
command: Option<Commands>,
}
Expand All @@ -43,34 +37,34 @@ enum Commands {
image: Option<String>,
#[arg(short, long, default_value_t = false, help = "Enable icons")]
icons: bool,
#[arg(short, long, default_value_t = false, help = "Output JSON instead of formatted text")]
#[arg(
short,
long,
default_value_t = false,
help = "Output JSON instead of formatted text"
)]
raw: bool,
},
#[cfg(feature = "server")]
Serve {
#[arg(short, long, default_value_t = 8000, help = "Use a different port for the server")]
#[arg(
short,
long,
default_value_t = 8000,
help = "Use a different port for the server"
)]
port: u16,
},
}

pub trait Unique<T> {
// So we can filter vecs for duplicates
fn unique(&mut self);
}

impl<T> Unique<T> for Vec<T>
where
T: Clone + Eq + std::hash::Hash,
{
fn unique(self: &mut Vec<T>) {
let mut seen: HashSet<T> = HashSet::new();
self.retain(|item| seen.insert(item.clone()));
}
}

#[tokio::main]
async fn main() {
let cli = Cli::parse();
let cfg_path = match cli.config_path.as_str() {
"" => None,
path => Some(PathBuf::from(path)),
};
let config = load_config(cfg_path);
match &cli.command {
#[cfg(feature = "cli")]
Some(Commands::Check { image, icons, raw }) => match image {
Expand All @@ -95,68 +89,8 @@ async fn main() {
},
#[cfg(feature = "server")]
Some(Commands::Serve { port }) => {
let _ = serve(port, cli.socket).await;
let _ = serve(port, cli.socket, config).await;
}
None => (),
}
}

async fn get_all_updates(socket: Option<String>) -> Vec<(String, Option<bool>)> {
let image_map_mutex: Mutex<HashMap<String, &Option<String>>> = Mutex::new(HashMap::new());
let local_images = get_images_from_docker_daemon(socket).await;
local_images.par_iter().for_each(|image| {
let img = unsplit_image(&image.registry, &image.repository, &image.tag);
image_map_mutex.lock().unwrap().insert(img, &image.digest);
});
let image_map = image_map_mutex.lock().unwrap().clone();
let mut registries: Vec<&String> = local_images
.par_iter()
.map(|image| &image.registry)
.collect();
registries.unique();
let mut remote_images: Vec<Image> = Vec::new();
for registry in registries {
let images: Vec<&Image> = local_images
.par_iter()
.filter(|image| &image.registry == registry)
.collect();
let mut latest_images = match check_auth(registry) {
Some(auth_url) => {
let token = get_token(images.clone(), &auth_url);
get_latest_digests(images, Some(&token))
}
None => get_latest_digests(images, None),
};
remote_images.append(&mut latest_images);
}
let result_mutex: Mutex<Vec<(String, Option<bool>)>> = Mutex::new(Vec::new());
remote_images.par_iter().for_each(|image| {
let img = unsplit_image(&image.registry, &image.repository, &image.tag);
match &image.digest {
Some(d) => {
let r = d != image_map.get(&img).unwrap().as_ref().unwrap();
result_mutex.lock().unwrap().push((img, Some(r)))
}
None => result_mutex.lock().unwrap().push((img, None)),
}
});
let result = result_mutex.lock().unwrap().clone();
result
}

#[cfg(feature = "cli")]
async fn get_update(image: &str, socket: Option<String>) -> Option<bool> {
let local_image = get_image_from_docker_daemon(socket, image).await;
let token = match check_auth(&local_image.registry) {
Some(auth_url) => get_token(vec![&local_image], &auth_url),
None => String::new(),
};
let remote_image = match token.as_str() {
"" => get_latest_digest(&local_image, None),
_ => get_latest_digest(&local_image, Some(&token)),
};
match &remote_image.digest {
Some(d) => Some(d != &local_image.digest.unwrap()),
None => None,
}
}
33 changes: 18 additions & 15 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ use xitca_web::{
App,
};

use crate::{get_all_updates, utils::sort_update_vec};
use crate::{check::get_all_updates, utils::{sort_update_vec, Config}};

const RAW_TEMPLATE: &str = include_str!("static/template.liquid");
const STYLE: &str = include_str!("static/index.css");
const FAVICON_ICO: &[u8] = include_bytes!("static/favicon.ico");
const FAVICON_SVG: &[u8] = include_bytes!("static/favicon.svg");
const APPLE_TOUCH_ICON: &[u8] = include_bytes!("static/apple-touch-icon.png");

pub async fn serve(port: &u16, socket: Option<String>) -> std::io::Result<()> {
let mut data = UpdateData::new(socket).await;
pub async fn serve(port: &u16, socket: Option<String>, config: Config) -> std::io::Result<()> {
let mut data = ServerData::new(socket, config).await;
data.refresh().await;
App::new()
.with_state(Arc::new(Mutex::new(data)))
Expand All @@ -38,15 +38,15 @@ pub async fn serve(port: &u16, socket: Option<String>) -> std::io::Result<()> {
.wait()
}

async fn home(data: StateRef<'_, Arc<Mutex<UpdateData>>>) -> WebResponse {
async fn home(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
WebResponse::new(ResponseBody::from(data.lock().unwrap().template.clone()))
}

async fn json(data: StateRef<'_, Arc<Mutex<UpdateData>>>) -> WebResponse {
async fn json(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
WebResponse::new(ResponseBody::from(data.lock().unwrap().json.clone()))
}

async fn refresh(data: StateRef<'_, Arc<Mutex<UpdateData>>>) -> WebResponse {
async fn refresh(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
data.lock().unwrap().refresh().await;
return WebResponse::new(ResponseBody::from("OK"));
}
Expand All @@ -63,32 +63,34 @@ async fn apple_touch_icon() -> WebResponse {
WebResponse::new(ResponseBody::from(APPLE_TOUCH_ICON))
}

struct UpdateData {
struct ServerData {
template: String,
raw: Vec<(String, Option<bool>)>,
raw_updates: Vec<(String, Option<bool>)>,
json: String,
socket: Option<String>,
config: Config,
}

impl UpdateData {
async fn new(socket: Option<String>) -> Self {
impl ServerData {
async fn new(socket: Option<String>, config: Config) -> Self {
return Self {
socket,
template: String::new(),
json: String::new(),
raw: Vec::new(),
raw_updates: Vec::new(),
config,
};
}
async fn refresh(self: &mut Self) {
let updates = sort_update_vec(&get_all_updates(self.socket.clone()).await);
self.raw = updates;
self.raw_updates = updates;
let template = liquid::ParserBuilder::with_stdlib()
.build()
.unwrap()
.parse(RAW_TEMPLATE)
.unwrap();
let images = self
.raw
.raw_updates
.iter()
.map(|(name, image)| match image {
Some(value) => {
Expand Down Expand Up @@ -121,11 +123,12 @@ impl UpdateData {
"metrics": [{"name": "Monitored images", "value": images.len()}, {"name": "Up to date", "value": uptodate}, {"name": "Updates available", "value": updatable}, {"name": "Unknown", "value": unknown}],
"images": images,
"style": STYLE,
"last_updated": last_updated.to_string()
"last_updated": last_updated.to_string(),
"theme": self.config.theme
});
self.template = template.render(&globals).unwrap();
let json_data: Mutex<json::object::Object> = Mutex::new(json::object::Object::new());
self.raw.par_iter().for_each(|image| match image.1 {
self.raw_updates.par_iter().for_each(|image| match image.1 {
Some(b) => json_data.lock().unwrap().insert(&image.0, json::from(b)),
None => json_data.lock().unwrap().insert(&image.0, json::Null),
});
Expand Down
Loading

0 comments on commit 30c762e

Please sign in to comment.