m/cup
1
0
mirror of https://github.com/sergi0g/cup.git synced 2025-11-08 13:13:49 -05:00

18 Commits

Author SHA1 Message Date
Sergio
e26f941c59 chore: bump project version 2025-03-16 18:42:06 +02:00
Sergio
c411fc4bad fix: ignore invalid digests instead of panicking 2025-03-16 18:40:10 +02:00
Sergio
e965380133 fix: improve error handling in get_latest_tag when an image has no tags
This commit is mostly for debugging #68, but it's good to have more
error info just in case.
2025-03-16 18:33:19 +02:00
Sergio
ef849b624f fix: don't pass empty parameters when making auth request (#69) 2025-03-16 18:26:04 +02:00
Sergio
8db7e2e12b fix: improve error handling when scheduling automatic refresh 2025-03-15 12:28:39 +02:00
Sergio
54e1998032 docs: fix incorrect cron schedule example 2025-03-15 12:28:39 +02:00
Sergio
9f142ab81c fix: add error message when app fails to bind to port 2025-03-15 12:28:39 +02:00
Sergio
ffd4d6267c chore: update readme
forgot to add a link
2025-03-14 10:20:51 +02:00
Sergio
242029db22 chore: update readme 2025-03-14 10:15:38 +02:00
Sergio
b6562ef76f chore: bump project version 2025-03-13 17:16:22 +02:00
Sergio
846b24bf2d feat: add experimental docker swarm support 2025-03-13 17:09:20 +02:00
Sergio
d7f766f1f5 chore(readme): Add discord link 2025-03-11 17:15:11 +02:00
Sergio
60096792a9 chore(docs): update JSON API reference schema 2025-03-11 17:08:42 +02:00
Sergio
a599d4e084 feat: detect image url from label 2025-03-11 17:06:00 +02:00
Sergio
a5a1f12899 chore: remove unneeded file 2025-03-11 17:05:46 +02:00
Sergio
766a20ccac chore: add badges to readme 2025-03-11 16:32:59 +02:00
Sergio
fe66ba842a docs: add docs about refresh endpoint 2025-03-11 16:26:48 +02:00
Sergio
c06c20394f docs: add page about home assistant integration 2025-03-11 16:26:31 +02:00
18 changed files with 192 additions and 36 deletions

View File

@@ -8,7 +8,7 @@ inputs:
required: true
runs:
using: 'composite'
using: "composite"
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -47,5 +47,6 @@ runs:
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

2
Cargo.lock generated
View File

@@ -355,7 +355,7 @@ dependencies = [
[[package]]
name = "cup"
version = "3.0.4"
version = "3.2.0"
dependencies = [
"bollard",
"chrono",

View File

@@ -1,6 +1,6 @@
[package]
name = "cup"
version = "3.0.4"
version = "3.2.0"
edition = "2021"
[dependencies]

View File

@@ -1,5 +1,13 @@
# Cup 🥤
![GitHub License](https://img.shields.io/github/license/sergi0g/cup)
![CI Status](https://img.shields.io/github/actions/workflow/status/sergi0g/cup/.github%2Fworkflows%2Fci.yml?label=CI)
![GitHub last commit](https://img.shields.io/github/last-commit/sergi0g/cup)
![GitHub Release](https://img.shields.io/github/v/release/sergi0g/cup)
![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/sergi0g/cup)
[![Discord](https://img.shields.io/discord/1337705080518086658)](https://discord.gg/jmh5ctzwNG)
Cup is the easiest way to check for container image updates.
![Cup web in dark mode](screenshots/web_dark.png)
@@ -15,7 +23,7 @@ _If you like this project and/or use Cup, please consider starring the project
- 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/getwud/wud) which would always use it up.
- Doesn't exhaust any rate limits. This is the original reason I created Cup. I feel that this feature is especially relevant now with [Docker Hub reducing its pull limits for unauthenticated users](https://docs.docker.com/docker-hub/usage/).
- 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.4 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!
@@ -48,7 +56,7 @@ For more information, check the [docs](https://cup.sergi0g.dev/docs/contributing
## Support
If you have any questions about Cup, feel free to ask in the [discussions](https://github.com/sergi0g/cup/discussions)!
If you have any questions about Cup, feel free to ask in the [discussions](https://github.com/sergi0g/cup/discussions)! You can also join our [discord server](https://discord.gg/jmh5ctzwNG).
If you find a bug, or want to propose a feature, search for it in the [issues](https://github.com/sergi0g/cup/issues). If there isn't already an open issue, please open one.

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -0,0 +1,47 @@
import Image from "next/image";
import screenshot from "@/app/assets/ha-cup-component.png";
# Home Assistant integration
Many thanks to [@bastgau](https://github.com/bastgau) for creating this integration.
## About
The **HA Cup Component** integration for Home Assistant allows you to retrieve update statistics for Docker containers directly from your Home Assistant interface.
With this integration, you can easily track the status of your Docker containers and receive notifications when updates are available.
The following sensors are currently implemented:
<Image
src={screenshot}
alt="Screenshot of Home Assistant showing a card with update information provided by Cup"
/>
## Installation
### Via HACS
1. Open Home Assistant and go to HACS
2. Navigate to "Integrations" and click on "Add a custom repository".
3. Use https://github.com/bastgau/ha-cup-component as the URL
4. Search for "HA Cup Component" and install it.
5. Restart Home Assistant.
### One-click install
[![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=bastgau&repository=ha-cup-component&category=Integration)
### Manual Installation
1. Download the integration files from the GitHub repository.
2. Place the integration folder in the custom_components directory of Home Assistant.
3. Restart Home Assistant.
## Support & Contributions
If you encounter any issues or wish to contribute to improving this integration, feel free to open an issue or a pull request in the [GitHub repository](https://github.com/bastgau/ha-cup-component).
Support the author:
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/bastgau)

View File

@@ -4,7 +4,7 @@ Cup can automatically refresh the results when running in server mode. Simply ad
```jsonc
{
"refresh_interval": "0 0,30 * 0 0" // Check twice an hour
"refresh_interval": "0 0,30 * * * *" // Check twice an hour
// Other options
}
```

View File

@@ -1,5 +1,5 @@
import { Callout, Cards } from "nextra/components";
import { IconServer, IconTerminal } from "@tabler/icons-react"
import { IconServer, IconTerminal } from "@tabler/icons-react";
# Integrations
@@ -34,6 +34,7 @@ The data returned from the API or from the CLI is in JSON and looks like this:
"repository": "sergi0g/cup",
"tag": "latest",
},
"url": "https://github.com/sergi0g/cup", // The URL specified in the "org.opencontainers.image.url" label, otherwise null
"result": {
"has_update": true, // `true` when an image has an update of any kind, `false` when up to date and `null` when unknown.
"info": {
@@ -77,3 +78,7 @@ For retrieving the above data, refer to the CLI and server pages:
href="/docs/usage/server"
/>
</Cards>
## Refresh Cup
If you'd like to fetch the latest information, you can manually trigger a refresh by making a `GET` request to the `/api/v3/refresh` endpoint. Once the request completes, you can fetch the data as described above.

View File

@@ -42,7 +42,26 @@ pub async fn get_images_from_docker_daemon(
references: &Option<Vec<String>>,
) -> Vec<Image> {
let client: Docker = create_docker_client(ctx.config.socket.as_deref());
match references {
let mut swarm_images = match client.list_services::<String>(None).await {
Ok(services) => services
.iter()
.filter_map(|service| match &service.spec {
Some(service_spec) => match &service_spec.task_template {
Some(task_spec) => match &task_spec.container_spec {
Some(container_spec) => match &container_spec.image {
Some(image) => Image::from_inspect_data(ctx, image),
None => None,
},
None => None,
},
None => None,
},
None => None,
})
.collect(),
Err(_) => Vec::new(),
};
let mut local_images = match references {
Some(refs) => {
let mut inspect_handles = Vec::with_capacity(refs.len());
for reference in refs {
@@ -56,7 +75,7 @@ pub async fn get_images_from_docker_daemon(
.collect();
inspects
.iter()
.filter_map(|inspect| Image::from_inspect_data(inspect.clone()))
.filter_map(|inspect| Image::from_inspect_data(ctx, inspect.clone()))
.collect()
}
None => {
@@ -68,8 +87,10 @@ pub async fn get_images_from_docker_daemon(
};
images
.iter()
.filter_map(|image| Image::from_inspect_data(image.clone()))
.collect()
.filter_map(|image| Image::from_inspect_data(ctx, image.clone()))
.collect::<Vec<Image>>()
}
}
};
local_images.append(&mut swarm_images);
local_images
}

View File

@@ -205,7 +205,7 @@ pub async fn get_latest_tag(
}
}
}
None => unreachable!("{:?}", tags),
None => error!("Image {} has no remote version tags! Local tag: {}", image.reference, image.parts.tag),
}
}

View File

@@ -19,6 +19,7 @@ use xitca_web::{
use crate::{
check::get_updates,
config::Theme,
error,
structs::update::Update,
utils::{
json::{to_full_json, to_simple_json},
@@ -55,13 +56,24 @@ pub async fn serve(port: &u16, ctx: &Context) -> std::io::Result<()> {
if let Some(interval) = &ctx.config.refresh_interval {
scheduler
.add(
Job::new_async(interval, move |_uuid, _lock| {
match Job::new_async(interval, move |_uuid, _lock| {
let data_copy = data_copy.clone();
Box::pin(async move {
data_copy.lock().await.refresh().await;
})
})
.unwrap(),
}) {
Ok(job) => job,
Err(e) => match e {
tokio_cron_scheduler::JobSchedulerError::ParseSchedule => error!(
"Failed to parse cron schedule: {}. Please ensure it is valid!",
interval
),
e => error!(
"An unexpected error occured while scheduling automatic refresh: {}",
e
),
},
},
)
.await
.unwrap();
@@ -79,12 +91,16 @@ pub async fn serve(port: &u16, ctx: &Context) -> std::io::Result<()> {
.at("/", get(handler_service(_static)))
.at("/*", get(handler_service(_static)));
}
app_builder
match app_builder
.enclosed_fn(logger)
.serve()
.bind(format!("0.0.0.0:{}", port))?
.run()
.wait()
.bind(format!("0.0.0.0:{}", port))
{
Ok(r) => r,
Err(_) => error!("Failed to bind to port {}. Is it in use?", port),
}
.run()
.wait()
}
async fn _static(data: StateRef<'_, Arc<Mutex<ServerData>>>, path: PathRef<'_>) -> WebResponse {

View File

@@ -35,6 +35,7 @@ pub struct VersionInfo {
pub struct Image {
pub reference: String,
pub parts: Parts,
pub url: Option<String>,
pub digest_info: Option<DigestInfo>,
pub version_info: Option<VersionInfo>,
pub error: Option<String>,
@@ -43,16 +44,30 @@ pub struct Image {
impl Image {
/// Creates and populates the fields of an Image object based on the ImageSummary from the Docker daemon
pub fn from_inspect_data<T: InspectData>(image: T) -> Option<Self> {
pub fn from_inspect_data<T: InspectData>(ctx: &Context, image: T) -> Option<Self> {
let tags = image.tags().unwrap();
let digests = image.digests().unwrap();
if !tags.is_empty() && !digests.is_empty() {
let reference = tags[0].clone();
if reference.contains('@') {
return None; // As far as I know, references that contain @ are either manually pulled by the user or automatically created because of swarm. In the first case AFAICT we can't know what tag was originally pulled, so we'd have to make assumptions and I've decided to remove this. The other case is already handled seperately, so this also ensures images aren't displayed twice, once with and once without a digest.
};
let (registry, repository, tag) = split(&reference);
let version_tag = Version::from_tag(&tag);
let local_digests = digests
.iter()
.map(|digest| digest.split('@').collect::<Vec<&str>>()[1].to_string())
.filter_map(
|digest| match digest.split('@').collect::<Vec<&str>>().get(1) {
Some(digest) => Some(digest.to_string()),
None => {
ctx.logger.warn(format!(
"Ignoring invalid digest {} for image {}!",
digest, reference
));
None
}
},
)
.collect();
Some(Self {
reference,
@@ -61,6 +76,7 @@ impl Image {
repository,
tag,
},
url: image.url(),
digest_info: Some(DigestInfo {
local_digests,
remote_digest: None,
@@ -141,6 +157,7 @@ impl Image {
Update {
reference: self.reference.clone(),
parts: self.parts.clone(),
url: self.url.clone(),
result: UpdateResult {
has_update: has_update.to_option_bool(),
info: match has_update {

View File

@@ -1,26 +1,62 @@
use bollard::secret::{ImageInspect, ImageSummary};
pub trait InspectData {
fn tags(&self) -> Option<&Vec<String>>;
fn digests(&self) -> Option<&Vec<String>>;
fn tags(&self) -> Option<Vec<String>>;
fn digests(&self) -> Option<Vec<String>>;
fn url(&self) -> Option<String>;
}
impl InspectData for ImageInspect {
fn tags(&self) -> Option<&Vec<String>> {
self.repo_tags.as_ref()
fn tags(&self) -> Option<Vec<String>> {
self.repo_tags.clone()
}
fn digests(&self) -> Option<&Vec<String>> {
self.repo_digests.as_ref()
fn digests(&self) -> Option<Vec<String>> {
self.repo_digests.clone()
}
fn url(&self) -> Option<String> {
match &self.config {
Some(config) => match &config.labels {
Some(labels) => labels.get("org.opencontainers.image.url").cloned(),
None => None,
},
None => None,
}
}
}
impl InspectData for ImageSummary {
fn tags(&self) -> Option<&Vec<String>> {
Some(&self.repo_tags)
fn tags(&self) -> Option<Vec<String>> {
Some(self.repo_tags.clone())
}
fn digests(&self) -> Option<&Vec<String>> {
Some(&self.repo_digests)
fn digests(&self) -> Option<Vec<String>> {
Some(self.repo_digests.clone())
}
fn url(&self) -> Option<String> {
self.labels.get("org.opencontainers.image.url").cloned()
}
}
impl InspectData for &String {
fn tags(&self) -> Option<Vec<String>> {
self.split('@').next().map(|tag| vec![tag.to_string()])
}
fn digests(&self) -> Option<Vec<String>> {
match self.split_once('@') {
Some((reference, digest)) => Some(vec![format!(
"{}@{}",
reference.split(':').next().unwrap(),
digest
)]),
None => Some(vec![]),
}
}
fn url(&self) -> Option<String> {
None
}
}

View File

@@ -7,6 +7,7 @@ use super::{parts::Parts, status::Status};
pub struct Update {
pub reference: String,
pub parts: Parts,
pub url: Option<String>,
pub result: UpdateResult,
pub time: u32,
pub server: Option<String>,

View File

@@ -17,8 +17,10 @@ pub fn parse_www_authenticate(www_auth: &str) -> String {
.fold(String::new(), |acc, (key, value)| {
if *key == "realm" {
acc.to_owned() + value.as_escaped() + "?"
} else {
} else if value.unescaped_len() != 0 {
format!("{}&{}={}", acc, key, value.as_escaped())
} else {
acc
}
})
} else {

View File

@@ -1 +0,0 @@
nodejs 22.8.0

View File

@@ -40,7 +40,9 @@ export default function Image({ data }: { data: Image }) {
: data.reference;
const info = getInfo(data)!;
let url: string | null = null;
if (clickable_registries.includes(data.parts.registry)) {
if (data.url) {
url = data.url;
} else if (clickable_registries.includes(data.parts.registry)) {
switch (data.parts.registry) {
case "registry-1.docker.io":
url = `https://hub.docker.com/r/${data.parts.repository}`;
@@ -82,7 +84,7 @@ export default function Image({ data }: { data: Image }) {
>
<div className="mb-4 flex items-center gap-3">
<Box className={`size-6 shrink-0 text-${theme}-500`} />
<DialogTitle className="font-mono text-black dark:text-white break-all">
<DialogTitle className="break-all font-mono text-black dark:text-white">
{url ? (
<>
<a

View File

@@ -20,6 +20,7 @@ export interface Image {
repository: string;
tag: string;
};
url: string | null;
result: {
has_update: boolean | null;
info: VersionInfo | DigestInfo | null;