Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor #32

Merged
merged 12 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions Cargo.lock

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

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cup"
version = "2.3.1"
version = "2.4.0"
edition = "2021"

[dependencies]
Expand All @@ -20,6 +20,7 @@ reqwest = { version = "0.12.7", default-features = false, features = ["rustls-tl
futures = "0.3.30"
reqwest-retry = "0.6.1"
reqwest-middleware = "0.3.3"
rustc-hash = "2.0.0"

[features]
default = ["server", "cli"]
Expand Down
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ _If you like this project and/or use Cup, please consider starring the project

## Features

- Extremely fast. Cup takes full advantage of your CPU and is hightly optimized, resulting in lightning fast speed. On my test machine, it took ~12 seconds for ~95 images.
- Extremely fast. Cup takes full advantage of your CPU and is hightly optimized, resulting in lightning fast speed. On my Raspberry Pi 5, it took 3.7 seconds for 58 images!
- Supports most registries, including Docker Hub, ghcr.io, Quay, lscr.io and even Gitea (or derivatives)
- Doesn't exhaust any rate limits. This is the original reason I created Cup. It was inspired by [What's up docker?](https://github.com/fmartinou/whats-up-docker) which would always use it up.
- Beautiful CLI and web interface for checking on your containers any time.
- The binary is tiny! At the time of writing it's just 5.1 MB. No more pulling 100+ MB docker images for a such a simple program.
- The binary is tiny! At the time of writing it's just 5.2 MB. No more pulling 100+ MB docker images for a such a simple program.
- JSON output for both the CLI and web interface so you can connect Cup to integrations. It's easy to parse and makes webhooks and pretty dashboards simple to set up!

## Documentation
Expand All @@ -26,11 +26,10 @@ Take a look at https://sergi0g.github.io/cup/docs!

## Limitations

Cup is a work in progress. It might not have as many features as What's up Docker. If one of these features is really important for you, please consider using another tool.
Cup is a work in progress. It might not have as many features as other alternatives. If one of these features is really important for you, please consider using another tool.

- ~~Cup currently doesn't support registries which use repositories without slashes. This includes Azure. This problem may sound a bit weird, but it's due to the regex that's used at the moment. This will (hopefully) be fixed in the future.~~
- ~~Cup doesn't support private images. This is on the roadmap. Currently, it just returns unknown for those images.~~
- Cup cannot trigger your integrations. If you want that to happen automatically, please use What's up docker instead. Cup was created to be simple. The data is there, and it's up to you to retrieve it (e.g. by running `cup check -r` with a cronjob or periodically requesting the `/json` url from the server)
- Cup (currently) does not support semver.
- Cup cannot directly trigger your integrations. If you want that to happen automatically, please use What's up Docker instead. Cup was created to be simple. The data is there, and it's up to you to retrieve it (e.g. by running `cup check -r` with a cronjob or periodically requesting the `/json` url from the server).

## Roadmap
Take a sneak peek at what's coming up in future releases on the [roadmap](https://github.com/users/sergi0g/projects/2)!
Expand Down
5 changes: 3 additions & 2 deletions docs/pages/docs/configuration.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Steps, Callout, Card, Cards } from "nextra-theme-docs";
import { IconPaint, IconLockOpen, IconKey } from '@tabler/icons-react';
import { IconPaint, IconLockOpen, IconKey, IconPlug } from '@tabler/icons-react';

# Configuration

Expand All @@ -13,7 +13,8 @@ For example, if using Podman, you might do
$ cup -s /run/user/1000/podman/podman.sock check
```

This option will hopefully be moved to the configuration file soon.
This option is also available in the configuration file and it's best to put it there.
<Card icon={<IconPlug />} title="Custom Docker socket" href="/docs/configuration/socket" />

## Configuration file

Expand Down
10 changes: 10 additions & 0 deletions docs/pages/docs/configuration/socket.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Socket

If you need to specify a custom Docker socket (e.g. because you're using Podman), you can use the `socket` option. Here's an example:

```json
{
"socket": "/run/user/1000/podman/podman.sock"
// Other options
}
```
16 changes: 13 additions & 3 deletions docs/pages/docs/usage/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,22 @@ rockylinux:9-minimal Up to date
rabbitmq:3.11.9-management Up to date
...
some/deleted:image Unknown
[38:5:86mINFO ✨ Checked 58 images in 3772ms
```

### Check for updates to a specific image
### Check for updates to specific images
```ansi
$ cup check node:latest
node:latest Update available
[38:5:86mINFO ✨ Checked 1 images in 1310ms
```

```ansi
$ cup check node:latest
node:latest has an update available
nextcloud:30 Update available
postgres:14 Update available
mysql:8.0 Up to date
[38:5:86mINFO ✨ Checked 3 images in 1769ms
```

## Enable icons
Expand All @@ -46,7 +56,7 @@ $ cup check -r
Here is how it would look in Typescript:

```ts
type CupData = {
interface CupData {
metrics: {
monitored_images: number,
up_to_date: number,
Expand Down
Binary file modified screenshots/web_dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/web_light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
149 changes: 69 additions & 80 deletions src/check.rs
Original file line number Diff line number Diff line change
@@ -1,106 +1,95 @@
use std::collections::{HashMap, HashSet};
use futures::future::join_all;
use rustc_hash::{FxHashMap, FxHashSet};

use crate::{
debug,
docker::get_images_from_docker_daemon,
image::Image,
registry::{check_auth, get_latest_digests, get_token},
utils::{new_reqwest_client, unsplit_image, CliConfig},
config::Config, image::Image, registry::{check_auth, get_token}, utils::new_reqwest_client
};

#[cfg(feature = "cli")]
use crate::docker::get_image_from_docker_daemon;
#[cfg(feature = "cli")]
use crate::registry::get_latest_digest;

/// Trait for a type that implements a function `unique` that removes any duplicates.
/// In this case, it will be used for a Vec.
pub trait Unique<T> {
// So we can filter vecs for duplicates
fn unique(&mut self);
fn unique(&mut self) -> Vec<T>;
}

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();
/// Remove duplicates from Vec
fn unique(self: &mut Vec<T>) -> Self {
let mut seen: FxHashSet<T> = FxHashSet::default();
self.retain(|item| seen.insert(item.clone()));
self.to_vec()
}
}

pub async fn get_all_updates(options: &CliConfig) -> Vec<(String, Option<bool>)> {
let local_images = get_images_from_docker_daemon(options).await;
let mut image_map: HashMap<String, Option<String>> = HashMap::with_capacity(local_images.len());
for image in &local_images {
let img = unsplit_image(image);
image_map.insert(img, image.digest.clone());
}
let mut registries: Vec<&String> = local_images.iter().map(|image| &image.registry).collect();
registries.unique();
let mut remote_images: Vec<Image> = Vec::with_capacity(local_images.len());
/// Returns a list of updates for all images passed in.
pub async fn get_updates(images: &[Image], config: &Config) -> Vec<(String, Option<bool>)> {
// Get a list of unique registries our images belong to. We are unwrapping the registry because it's guaranteed to be there.
let registries: Vec<&String> = images
.iter()
.map(|image| image.registry.as_ref().unwrap())
.collect::<Vec<&String>>()
.unique();

// Create request client. All network requests share the same client for better performance.
// This client is also configured to retry a failed request up to 3 times with exponential backoff in between.
let client = new_reqwest_client();

// Create a map of images indexed by registry. This solution seems quite inefficient, since each iteration causes a key to be looked up. I can't find anything better at the moment.
let mut image_map: FxHashMap<&String, Vec<&Image>> = FxHashMap::default();

for image in images {
image_map
.entry(image.registry.as_ref().unwrap())
.or_default()
.push(image);
}

// Retrieve an authentication token (if required) for each registry.
let mut tokens: FxHashMap<&String, Option<String>> = FxHashMap::default();
for registry in registries {
if options.verbose {
debug!("Checking images from registry {}", registry)
}
let images: Vec<&Image> = local_images
.iter()
.filter(|image| &image.registry == registry)
.collect();
let credentials = options.config["authentication"][registry]
.clone()
.take_string()
.or(None);
let mut latest_images = match check_auth(registry, options, &client).await {
let credentials = config.authentication.get(registry);
match check_auth(registry, config, &client).await {
Some(auth_url) => {
let token = get_token(images.clone(), &auth_url, &credentials, &client).await;
if options.verbose {
debug!("Using token {}", token);
}
get_latest_digests(images, Some(&token), options, &client).await
let token = get_token(
image_map.get(registry).unwrap(),
&auth_url,
&credentials,
&client,
)
.await;
tokens.insert(registry, Some(token));
}
None => get_latest_digests(images, None, options, &client).await,
};
remote_images.append(&mut latest_images);
}
if options.verbose {
debug!("Collecting results")
}
let mut result: Vec<(String, Option<bool>)> = Vec::new();
remote_images.iter().for_each(|image| {
let img = unsplit_image(image);
match &image.digest {
Some(d) => {
let r = d != image_map.get(&img).unwrap().as_ref().unwrap();
result.push((img, Some(r)))
None => {
tokens.insert(registry, None);
}
None => result.push((img, None)),
}
});
result
}
}

#[cfg(feature = "cli")]
pub async fn get_update(image: &str, options: &CliConfig) -> Option<bool> {
let local_image = get_image_from_docker_daemon(options.socket.clone(), image).await;
let credentials = options.config["authentication"][&local_image.registry]
.clone()
.take_string()
.or(None);
let client = new_reqwest_client();
let token = match check_auth(&local_image.registry, options, &client).await {
Some(auth_url) => get_token(vec![&local_image], &auth_url, &credentials, &client).await,
None => String::new(),
};
if options.verbose {
debug!("Using token {}", token);
};
let remote_image = match token.as_str() {
"" => get_latest_digest(&local_image, None, options, &client).await,
_ => get_latest_digest(&local_image, Some(&token), options, &client).await,
};
match &remote_image.digest {
Some(d) => Some(d != &local_image.digest.unwrap()),
None => None,
// Create a Vec to store futures so we can await them all at once.
let mut handles = Vec::new();
// Loop through images and get the latest digest for each
for image in images {
let token = tokens.get(&image.registry.as_ref().unwrap()).unwrap();
let future = get_latest_digest(image, token.as_ref(), config, &client);
handles.push(future);
}
// Await all the futures
let final_images = join_all(handles).await;

let mut result: Vec<(String, Option<bool>)> = Vec::with_capacity(images.len());
final_images
.iter()
.for_each(|image| match &image.remote_digest {
Some(digest) => {
let has_update = !image.local_digests.as_ref().unwrap().contains(digest);
result.push((image.reference.clone(), Some(has_update)))
}
None => result.push((image.reference.clone(), None)),
});

result
}
Loading
Loading