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

38 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
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
28 changed files with 995 additions and 582 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.2.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)](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.

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

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

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

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

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

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

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

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

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