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

26 Commits

Author SHA1 Message Date
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
Sergio
98dafb8ba4 chore: bump project version 2025-03-10 16:49:07 +02:00
Sergio
2addfca1b4 fix: code block component copying was working incorrectly (#62) 2025-03-10 16:47:47 +02:00
dependabot[bot]
e3b05923ae Bump ring from 0.17.11 to 0.17.13 (#60)
Bumps [ring](https://github.com/briansmith/ring) from 0.17.11 to 0.17.13.
- [Changelog](https://github.com/briansmith/ring/blob/main/RELEASES.md)
- [Commits](https://github.com/briansmith/ring/commits)

---
updated-dependencies:
- dependency-name: ring
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-07 19:26:28 +02:00
Sergio
aa4195f8d6 chore: bump project version 2025-03-07 18:25:00 +02:00
Sergio
1b94629c79 fix: overflowing image references in web UI 2025-03-07 18:01:32 +02:00
Sergio
8cd9cce94e chore: bump project version 2025-03-07 17:38:20 +02:00
Sergio
ddabd8c102 fix: strip hash when parsing image references 2025-03-07 17:37:56 +02:00
Sergio
0b0028ab6d Fix search in docs 2025-03-07 16:40:59 +02:00
Sergio
75509550b1 Fix CI Dockerfile 2025-03-03 11:35:58 +02:00
Sergio
9716d1a351 Fix CI Dockerfile 2025-03-03 11:30:09 +02:00
Sergio
d5a2556768 Fix CI workflows 2025-03-03 11:23:26 +02:00
Sergio
e7f1921620 I'm stupid 2025-03-03 11:14:16 +02:00
Sergio
7ea6ae6de5 Let's try again 2025-03-03 10:31:42 +02:00
Sergio
d7c2e6436c Fix CI workflows 2025-03-03 10:23:08 +02:00
Sergio
fde61ee07d Fix CI workflows 2025-03-03 10:18:11 +02:00
Sergio
c4de3961a0 Optimize CI for docker image 2025-03-03 10:05:18 +02:00
Sergio
404c574c2c Update Rust version in Dockerfile 2025-03-02 13:08:43 +02:00
Sergio
6d4df20f54 Ignore registries before retrieving auth tokens 2025-03-02 13:05:23 +02:00
Sergio
7b3745d095 Fix errors and revert reqwest-middleware to v0.3.3 2025-03-02 13:04:31 +02:00
Sergio
f9aa516da7 Update dependencies. Also fixes a security vulnerability in rustls 2025-03-02 12:45:29 +02:00
23 changed files with 892 additions and 553 deletions

12
.github/actions/build-image/Dockerfile vendored Normal file
View File

@@ -0,0 +1,12 @@
FROM --platform=$BUILDPLATFORM alpine AS builder
ARG TARGETARCH
ARG TARGETOS
COPY binaries/* /
RUN mv cup-$TARGETOS-$TARGETARCH cup
RUN chmod +x cup
FROM scratch
COPY --from=builder /cup /cup
ENTRYPOINT ["/cup"]

52
.github/actions/build-image/action.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
name: Build Image
inputs:
tags:
description: "Docker image tags"
required: true
gh-token:
description: "Github token"
required: true
runs:
using: "composite"
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download binaries
uses: actions/download-artifact@v4
with:
path: .
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/sergi0g/cup
tags: ${{ inputs.tags }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: sergi0g
password: ${{ inputs.gh-token }}
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
file: ./.github/actions/build-image/Dockerfile
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

View File

@@ -36,7 +36,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 20
- name: Set up Bun
uses: oven-sh/setup-bun@v1
@@ -62,38 +62,24 @@ jobs:
cup-linux-arm64
build-image:
needs: get-tag
needs:
- get-tag
- build-binaries
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
- uses: ./.github/actions/build-image
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64, linux/arm64
push: true
tags: ghcr.io/sergi0g/cup:${{ needs.get-tag.outputs.tag }}
cache-from: type=gha
cache-to: type=gha,mode=max
tags: |
${{ needs.get-tag.outputs.tag }}
gh-token: ${{ secrets.GITHUB_TOKEN }}
nightly-release:
runs-on: ubuntu-latest
needs: [build-binaries, get-tag]
needs:
- build-binaries
- build-image
steps:
- name: Download binaries
uses: actions/download-artifact@v4

View File

@@ -60,34 +60,19 @@ jobs:
cup-linux-arm64
build-image:
needs: get-tag
needs:
- get-tag
- build-binaries
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
- uses: ./.github/actions/build-image
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64, linux/arm64
push: true
tags: ghcr.io/sergi0g/cup:${{ needs.get-tag.outputs.tag }},ghcr.io/sergi0g/cup:latest
cache-from: type=gha
cache-to: type=gha,mode=max
tags: |
${{ needs.get-tag.outputs.tag }}
latest
gh-token: ${{ secrets.GITHUB_TOKEN }}
release:
runs-on: ubuntu-latest
@@ -107,4 +92,4 @@ jobs:
prerelease: true
tag_name: ${{ needs.get-tag.outputs.tag }}
name: ${{ needs.get-tag.outputs.tag }}
files: binaries/*
files: binaries/*

1167
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,15 @@
[package]
name = "cup"
version = "3.0.0"
version = "3.1.0"
edition = "2021"
[dependencies]
clap = { version = "4.5.7", features = ["derive"] }
indicatif = { version = "0.17.8", optional = true }
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
xitca-web = { version = "0.5.0", optional = true }
xitca-web = { version = "0.6.2", optional = true }
liquid = { version = "0.26.6", optional = true }
bollard = "0.16.1"
bollard = "0.18.1"
once_cell = "1.19.0"
http-auth = { version = "0.1.9", default-features = false }
termsize = { version = "0.1.8", optional = true }
@@ -17,11 +17,11 @@ regex = { version = "1.10.5", default-features = false, features = ["perf"] }
chrono = { version = "0.4.38", default-features = false, features = ["std", "alloc", "clock"], optional = true }
reqwest = { version = "0.12.7", default-features = false, features = ["rustls-tls"] }
futures = "0.3.30"
reqwest-retry = "0.6.1"
reqwest-retry = "0.7.0"
reqwest-middleware = "0.3.3"
rustc-hash = "2.0.0"
http-link = "1.0.1"
itertools = "0.13.0"
itertools = "0.14.0"
serde_json = "1.0.133"
serde = "1.0.215"
tokio-cron-scheduler = { version = "0.13.0", default-features = false, optional = true }

View File

@@ -15,7 +15,7 @@ RUN ~/.bun/bin/bun install
RUN ~/.bun/bin/bun run build
### Build Cup ###
FROM rust:1.80.1-alpine AS build
FROM rust:1-alpine AS build
# Requirements
RUN apk add musl-dev

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)
Cup is the easiest way to check for container image updates.
![Cup web in dark mode](screenshots/web_dark.png)

Binary file not shown.

View File

@@ -4,7 +4,7 @@
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"build": "next build && pagefind --site out --output-path out/_pagefind",
"start": "next start",
"lint": "next lint",
"fmt": "bun prettier --write ."
@@ -26,6 +26,7 @@
"@types/react-dom": "^19.0.3",
"eslint": "^9.18.0",
"eslint-config-next": "15.1.5",
"pagefind": "^1.3.0",
"postcss": "^8.5.1",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.11",

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

@@ -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

@@ -120,6 +120,16 @@ pub async fn get_updates(
.iter()
.map(|image| &image.parts.registry)
.unique()
.filter(|&registry| match ctx.config.registries.get(registry) {
Some(config) => {
if config.ignore {
false
} else {
true
}
}
None => true,
})
.collect::<Vec<&String>>();
// Create request client. All network requests share the same client for better performance.
@@ -138,7 +148,7 @@ pub async fn get_updates(
// Retrieve an authentication token (if required) for each registry.
let mut tokens: FxHashMap<&str, Option<String>> = FxHashMap::default();
for registry in registries {
for registry in registries.clone() {
let credentials = if let Some(registry_config) = ctx.config.registries.get(registry) {
&registry_config.authentication
} else {
@@ -163,24 +173,11 @@ pub async fn get_updates(
ctx.logger.debug(format!("Tokens: {:?}", tokens));
let ignored_registries = ctx
.config
.registries
.iter()
.filter_map(|(registry, registry_config)| {
if registry_config.ignore {
Some(registry)
} else {
None
}
})
.collect::<Vec<&String>>();
let mut handles = Vec::with_capacity(images.len());
// Loop through images check for updates
for image in &images {
let is_ignored = ignored_registries.contains(&&image.parts.registry)
let is_ignored = !registries.contains(&&image.parts.registry)
|| ctx
.config
.images

View File

@@ -95,6 +95,10 @@ impl Client {
let message = format!("{} {}: Connection timed out!", method, url);
self.ctx.logger.warn(&message);
Err(message)
} else if error.is_middleware() {
let message = format!("{} {}: Connection failed after 3 retries!", method, url);
self.ctx.logger.warn(&message);
Err(message)
} else {
error!(
"{} {}: Unexpected error: {}",

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>,
@@ -61,6 +62,7 @@ impl Image {
repository,
tag,
},
url: image.url(),
digest_info: Some(DigestInfo {
local_digests,
remote_digest: None,
@@ -141,6 +143,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

@@ -3,6 +3,7 @@ use bollard::secret::{ImageInspect, ImageSummary};
pub trait InspectData {
fn tags(&self) -> Option<&Vec<String>>;
fn digests(&self) -> Option<&Vec<String>>;
fn url(&self) -> Option<String>;
}
impl InspectData for ImageInspect {
@@ -13,6 +14,16 @@ impl InspectData for ImageInspect {
fn digests(&self) -> Option<&Vec<String>> {
self.repo_digests.as_ref()
}
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 {
@@ -23,4 +34,8 @@ impl InspectData for ImageSummary {
fn digests(&self) -> Option<&Vec<String>> {
Some(&self.repo_digests)
}
fn url(&self) -> Option<String> {
self.labels.get("org.opencontainers.image.url").cloned()
}
}

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

@@ -16,7 +16,7 @@ pub fn split(reference: &str) -> (String, String, String) {
}
}
};
let splits = repository_and_tag.split(':').collect::<Vec<&str>>();
let splits = repository_and_tag.split('@').next().unwrap().split(':').collect::<Vec<&str>>();
let (repository, tag) = match splits.len() {
1 | 2 => {
let repository_components = splits[0].split('/').collect::<Vec<&str>>();
@@ -38,7 +38,7 @@ pub fn split(reference: &str) -> (String, String, String) {
};
(repository, tag)
}
_ => unreachable!(),
_ => {dbg!(splits); panic!()},
};
(registry.to_string(), repository, tag.to_string())
}

View File

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

View File

@@ -21,6 +21,8 @@ export function CodeBlock({
};
};
const copyText = children instanceof Array ? children.join("") : children;
return (
<div
className={`group relative flex w-full items-center rounded-lg bg-${theme}-100 px-3 py-2 font-mono text-${theme}-700 dark:bg-${theme}-950 dark:text-${theme}-300`}
@@ -35,7 +37,7 @@ export function CodeBlock({
) : (
<button
className={`duration-50 absolute right-3 bg-${theme}-100 py-1 pl-2 opacity-0 transition-opacity group-hover:opacity-100 dark:bg-${theme}-950`}
onClick={handleCopy(`docker pull ${children}`)}
onClick={handleCopy(`${copyText}`)}
>
<Clipboard className="size-5" />
</button>

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,14 +84,14 @@ 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">
<DialogTitle className="break-all font-mono text-black dark:text-white">
{url ? (
<>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className={`group w-fit text-black hover:underline dark:text-white`}
className={`group w-fit hover:underline`}
>
<span>
{data.reference}

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;