mirror of
https://github.com/sergi0g/cup.git
synced 2025-11-13 07:33:48 -05:00
Compare commits
29 Commits
v3.0.0
...
v3.2.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6562ef76f | ||
|
|
846b24bf2d | ||
|
|
d7f766f1f5 | ||
|
|
60096792a9 | ||
|
|
a599d4e084 | ||
|
|
a5a1f12899 | ||
|
|
766a20ccac | ||
|
|
fe66ba842a | ||
|
|
c06c20394f | ||
|
|
98dafb8ba4 | ||
|
|
2addfca1b4 | ||
|
|
e3b05923ae | ||
|
|
aa4195f8d6 | ||
|
|
1b94629c79 | ||
|
|
8cd9cce94e | ||
|
|
ddabd8c102 | ||
|
|
0b0028ab6d | ||
|
|
75509550b1 | ||
|
|
9716d1a351 | ||
|
|
d5a2556768 | ||
|
|
e7f1921620 | ||
|
|
7ea6ae6de5 | ||
|
|
d7c2e6436c | ||
|
|
fde61ee07d | ||
|
|
c4de3961a0 | ||
|
|
404c574c2c | ||
|
|
6d4df20f54 | ||
|
|
7b3745d095 | ||
|
|
f9aa516da7 |
12
.github/actions/build-image/Dockerfile
vendored
Normal file
12
.github/actions/build-image/Dockerfile
vendored
Normal 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
52
.github/actions/build-image/action.yml
vendored
Normal 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
|
||||||
36
.github/workflows/nightly.yml
vendored
36
.github/workflows/nightly.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
|||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
- name: Set up Bun
|
- name: Set up Bun
|
||||||
uses: oven-sh/setup-bun@v1
|
uses: oven-sh/setup-bun@v1
|
||||||
|
|
||||||
@@ -62,38 +62,24 @@ jobs:
|
|||||||
cup-linux-arm64
|
cup-linux-arm64
|
||||||
|
|
||||||
build-image:
|
build-image:
|
||||||
needs: get-tag
|
needs:
|
||||||
|
- get-tag
|
||||||
|
- build-binaries
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
- uses: ./.github/actions/build-image
|
||||||
- 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
|
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
tags: |
|
||||||
username: ${{ github.repository_owner }}
|
${{ needs.get-tag.outputs.tag }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
gh-token: ${{ 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
|
|
||||||
|
|
||||||
nightly-release:
|
nightly-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [build-binaries, get-tag]
|
needs:
|
||||||
|
- build-binaries
|
||||||
|
- build-image
|
||||||
steps:
|
steps:
|
||||||
- name: Download binaries
|
- name: Download binaries
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
|
|||||||
33
.github/workflows/release.yml
vendored
33
.github/workflows/release.yml
vendored
@@ -60,34 +60,19 @@ jobs:
|
|||||||
cup-linux-arm64
|
cup-linux-arm64
|
||||||
|
|
||||||
build-image:
|
build-image:
|
||||||
needs: get-tag
|
needs:
|
||||||
|
- get-tag
|
||||||
|
- build-binaries
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
- uses: ./.github/actions/build-image
|
||||||
- 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
|
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
tags: |
|
||||||
username: ${{ github.repository_owner }}
|
${{ needs.get-tag.outputs.tag }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
latest
|
||||||
|
gh-token: ${{ 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
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -107,4 +92,4 @@ jobs:
|
|||||||
prerelease: true
|
prerelease: true
|
||||||
tag_name: ${{ needs.get-tag.outputs.tag }}
|
tag_name: ${{ needs.get-tag.outputs.tag }}
|
||||||
name: ${{ needs.get-tag.outputs.tag }}
|
name: ${{ needs.get-tag.outputs.tag }}
|
||||||
files: binaries/*
|
files: binaries/*
|
||||||
|
|||||||
1167
Cargo.lock
generated
1167
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
10
Cargo.toml
10
Cargo.toml
@@ -1,15 +1,15 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cup"
|
name = "cup"
|
||||||
version = "3.0.0"
|
version = "3.2.0-alpha.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.5.7", features = ["derive"] }
|
clap = { version = "4.5.7", features = ["derive"] }
|
||||||
indicatif = { version = "0.17.8", optional = true }
|
indicatif = { version = "0.17.8", optional = true }
|
||||||
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
|
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 }
|
liquid = { version = "0.26.6", optional = true }
|
||||||
bollard = "0.16.1"
|
bollard = "0.18.1"
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
http-auth = { version = "0.1.9", default-features = false }
|
http-auth = { version = "0.1.9", default-features = false }
|
||||||
termsize = { version = "0.1.8", optional = true }
|
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 }
|
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"] }
|
reqwest = { version = "0.12.7", default-features = false, features = ["rustls-tls"] }
|
||||||
futures = "0.3.30"
|
futures = "0.3.30"
|
||||||
reqwest-retry = "0.6.1"
|
reqwest-retry = "0.7.0"
|
||||||
reqwest-middleware = "0.3.3"
|
reqwest-middleware = "0.3.3"
|
||||||
rustc-hash = "2.0.0"
|
rustc-hash = "2.0.0"
|
||||||
http-link = "1.0.1"
|
http-link = "1.0.1"
|
||||||
itertools = "0.13.0"
|
itertools = "0.14.0"
|
||||||
serde_json = "1.0.133"
|
serde_json = "1.0.133"
|
||||||
serde = "1.0.215"
|
serde = "1.0.215"
|
||||||
tokio-cron-scheduler = { version = "0.13.0", default-features = false, optional = true }
|
tokio-cron-scheduler = { version = "0.13.0", default-features = false, optional = true }
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ RUN ~/.bun/bin/bun install
|
|||||||
RUN ~/.bun/bin/bun run build
|
RUN ~/.bun/bin/bun run build
|
||||||
|
|
||||||
### Build Cup ###
|
### Build Cup ###
|
||||||
FROM rust:1.80.1-alpine AS build
|
FROM rust:1-alpine AS build
|
||||||
|
|
||||||
# Requirements
|
# Requirements
|
||||||
RUN apk add musl-dev
|
RUN apk add musl-dev
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# Cup 🥤
|
# Cup 🥤
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
[](https://discord.gg/jmh5ctzwNG)
|
||||||
|
|
||||||
|
|
||||||
Cup is the easiest way to check for container image updates.
|
Cup is the easiest way to check for container image updates.
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
BIN
docs/bun.lockb
BIN
docs/bun.lockb
Binary file not shown.
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build && pagefind --site out --output-path out/_pagefind",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"fmt": "bun prettier --write ."
|
"fmt": "bun prettier --write ."
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
"@types/react-dom": "^19.0.3",
|
"@types/react-dom": "^19.0.3",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-config-next": "15.1.5",
|
"eslint-config-next": "15.1.5",
|
||||||
|
"pagefind": "^1.3.0",
|
||||||
"postcss": "^8.5.1",
|
"postcss": "^8.5.1",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
|
|||||||
BIN
docs/src/app/assets/ha-cup-component.png
Normal file
BIN
docs/src/app/assets/ha-cup-component.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
47
docs/src/content/docs/community-resources/home-assistant.mdx
Normal file
47
docs/src/content/docs/community-resources/home-assistant.mdx
Normal 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
|
||||||
|
|
||||||
|
[](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:
|
||||||
|
[](https://www.buymeacoffee.com/bastgau)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Callout, Cards } from "nextra/components";
|
import { Callout, Cards } from "nextra/components";
|
||||||
import { IconServer, IconTerminal } from "@tabler/icons-react"
|
import { IconServer, IconTerminal } from "@tabler/icons-react";
|
||||||
|
|
||||||
# Integrations
|
# 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",
|
"repository": "sergi0g/cup",
|
||||||
"tag": "latest",
|
"tag": "latest",
|
||||||
},
|
},
|
||||||
|
"url": "https://github.com/sergi0g/cup", // The URL specified in the "org.opencontainers.image.url" label, otherwise null
|
||||||
"result": {
|
"result": {
|
||||||
"has_update": true, // `true` when an image has an update of any kind, `false` when up to date and `null` when unknown.
|
"has_update": true, // `true` when an image has an update of any kind, `false` when up to date and `null` when unknown.
|
||||||
"info": {
|
"info": {
|
||||||
@@ -77,3 +78,7 @@ For retrieving the above data, refer to the CLI and server pages:
|
|||||||
href="/docs/usage/server"
|
href="/docs/usage/server"
|
||||||
/>
|
/>
|
||||||
</Cards>
|
</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.
|
||||||
|
|||||||
27
src/check.rs
27
src/check.rs
@@ -120,6 +120,16 @@ pub async fn get_updates(
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|image| &image.parts.registry)
|
.map(|image| &image.parts.registry)
|
||||||
.unique()
|
.unique()
|
||||||
|
.filter(|®istry| match ctx.config.registries.get(registry) {
|
||||||
|
Some(config) => {
|
||||||
|
if config.ignore {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => true,
|
||||||
|
})
|
||||||
.collect::<Vec<&String>>();
|
.collect::<Vec<&String>>();
|
||||||
|
|
||||||
// Create request client. All network requests share the same client for better performance.
|
// 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.
|
// Retrieve an authentication token (if required) for each registry.
|
||||||
let mut tokens: FxHashMap<&str, Option<String>> = FxHashMap::default();
|
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) {
|
let credentials = if let Some(registry_config) = ctx.config.registries.get(registry) {
|
||||||
®istry_config.authentication
|
®istry_config.authentication
|
||||||
} else {
|
} else {
|
||||||
@@ -163,24 +173,11 @@ pub async fn get_updates(
|
|||||||
|
|
||||||
ctx.logger.debug(format!("Tokens: {:?}", tokens));
|
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());
|
let mut handles = Vec::with_capacity(images.len());
|
||||||
|
|
||||||
// Loop through images check for updates
|
// Loop through images check for updates
|
||||||
for image in &images {
|
for image in &images {
|
||||||
let is_ignored = ignored_registries.contains(&&image.parts.registry)
|
let is_ignored = !registries.contains(&&image.parts.registry)
|
||||||
|| ctx
|
|| ctx
|
||||||
.config
|
.config
|
||||||
.images
|
.images
|
||||||
|
|||||||
@@ -42,7 +42,26 @@ pub async fn get_images_from_docker_daemon(
|
|||||||
references: &Option<Vec<String>>,
|
references: &Option<Vec<String>>,
|
||||||
) -> Vec<Image> {
|
) -> Vec<Image> {
|
||||||
let client: Docker = create_docker_client(ctx.config.socket.as_deref());
|
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(image),
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
Err(_) => Vec::new(),
|
||||||
|
};
|
||||||
|
let mut local_images = match references {
|
||||||
Some(refs) => {
|
Some(refs) => {
|
||||||
let mut inspect_handles = Vec::with_capacity(refs.len());
|
let mut inspect_handles = Vec::with_capacity(refs.len());
|
||||||
for reference in refs {
|
for reference in refs {
|
||||||
@@ -69,7 +88,9 @@ pub async fn get_images_from_docker_daemon(
|
|||||||
images
|
images
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|image| Image::from_inspect_data(image.clone()))
|
.filter_map(|image| Image::from_inspect_data(image.clone()))
|
||||||
.collect()
|
.collect::<Vec<Image>>()
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
local_images.append(&mut swarm_images);
|
||||||
|
local_images
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,10 @@ impl Client {
|
|||||||
let message = format!("{} {}: Connection timed out!", method, url);
|
let message = format!("{} {}: Connection timed out!", method, url);
|
||||||
self.ctx.logger.warn(&message);
|
self.ctx.logger.warn(&message);
|
||||||
Err(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 {
|
} else {
|
||||||
error!(
|
error!(
|
||||||
"{} {}: Unexpected error: {}",
|
"{} {}: Unexpected error: {}",
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ pub struct VersionInfo {
|
|||||||
pub struct Image {
|
pub struct Image {
|
||||||
pub reference: String,
|
pub reference: String,
|
||||||
pub parts: Parts,
|
pub parts: Parts,
|
||||||
|
pub url: Option<String>,
|
||||||
pub digest_info: Option<DigestInfo>,
|
pub digest_info: Option<DigestInfo>,
|
||||||
pub version_info: Option<VersionInfo>,
|
pub version_info: Option<VersionInfo>,
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
@@ -48,6 +49,9 @@ impl Image {
|
|||||||
let digests = image.digests().unwrap();
|
let digests = image.digests().unwrap();
|
||||||
if !tags.is_empty() && !digests.is_empty() {
|
if !tags.is_empty() && !digests.is_empty() {
|
||||||
let reference = tags[0].clone();
|
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 (registry, repository, tag) = split(&reference);
|
||||||
let version_tag = Version::from_tag(&tag);
|
let version_tag = Version::from_tag(&tag);
|
||||||
let local_digests = digests
|
let local_digests = digests
|
||||||
@@ -61,6 +65,7 @@ impl Image {
|
|||||||
repository,
|
repository,
|
||||||
tag,
|
tag,
|
||||||
},
|
},
|
||||||
|
url: image.url(),
|
||||||
digest_info: Some(DigestInfo {
|
digest_info: Some(DigestInfo {
|
||||||
local_digests,
|
local_digests,
|
||||||
remote_digest: None,
|
remote_digest: None,
|
||||||
@@ -141,6 +146,7 @@ impl Image {
|
|||||||
Update {
|
Update {
|
||||||
reference: self.reference.clone(),
|
reference: self.reference.clone(),
|
||||||
parts: self.parts.clone(),
|
parts: self.parts.clone(),
|
||||||
|
url: self.url.clone(),
|
||||||
result: UpdateResult {
|
result: UpdateResult {
|
||||||
has_update: has_update.to_option_bool(),
|
has_update: has_update.to_option_bool(),
|
||||||
info: match has_update {
|
info: match has_update {
|
||||||
|
|||||||
@@ -1,26 +1,62 @@
|
|||||||
use bollard::secret::{ImageInspect, ImageSummary};
|
use bollard::secret::{ImageInspect, ImageSummary};
|
||||||
|
|
||||||
pub trait InspectData {
|
pub trait InspectData {
|
||||||
fn tags(&self) -> Option<&Vec<String>>;
|
fn tags(&self) -> Option<Vec<String>>;
|
||||||
fn digests(&self) -> Option<&Vec<String>>;
|
fn digests(&self) -> Option<Vec<String>>;
|
||||||
|
fn url(&self) -> Option<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InspectData for ImageInspect {
|
impl InspectData for ImageInspect {
|
||||||
fn tags(&self) -> Option<&Vec<String>> {
|
fn tags(&self) -> Option<Vec<String>> {
|
||||||
self.repo_tags.as_ref()
|
self.repo_tags.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn digests(&self) -> Option<&Vec<String>> {
|
fn digests(&self) -> Option<Vec<String>> {
|
||||||
self.repo_digests.as_ref()
|
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 {
|
impl InspectData for ImageSummary {
|
||||||
fn tags(&self) -> Option<&Vec<String>> {
|
fn tags(&self) -> Option<Vec<String>> {
|
||||||
Some(&self.repo_tags)
|
Some(self.repo_tags.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn digests(&self) -> Option<&Vec<String>> {
|
fn digests(&self) -> Option<Vec<String>> {
|
||||||
Some(&self.repo_digests)
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use super::{parts::Parts, status::Status};
|
|||||||
pub struct Update {
|
pub struct Update {
|
||||||
pub reference: String,
|
pub reference: String,
|
||||||
pub parts: Parts,
|
pub parts: Parts,
|
||||||
|
pub url: Option<String>,
|
||||||
pub result: UpdateResult,
|
pub result: UpdateResult,
|
||||||
pub time: u32,
|
pub time: u32,
|
||||||
pub server: Option<String>,
|
pub server: Option<String>,
|
||||||
|
|||||||
@@ -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() {
|
let (repository, tag) = match splits.len() {
|
||||||
1 | 2 => {
|
1 | 2 => {
|
||||||
let repository_components = splits[0].split('/').collect::<Vec<&str>>();
|
let repository_components = splits[0].split('/').collect::<Vec<&str>>();
|
||||||
@@ -38,7 +38,7 @@ pub fn split(reference: &str) -> (String, String, String) {
|
|||||||
};
|
};
|
||||||
(repository, tag)
|
(repository, tag)
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => {dbg!(splits); panic!()},
|
||||||
};
|
};
|
||||||
(registry.to_string(), repository, tag.to_string())
|
(registry.to_string(), repository, tag.to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
nodejs 22.8.0
|
|
||||||
@@ -21,6 +21,8 @@ export function CodeBlock({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const copyText = children instanceof Array ? children.join("") : children;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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`}
|
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
|
<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`}
|
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" />
|
<Clipboard className="size-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ export default function Image({ data }: { data: Image }) {
|
|||||||
: data.reference;
|
: data.reference;
|
||||||
const info = getInfo(data)!;
|
const info = getInfo(data)!;
|
||||||
let url: string | null = null;
|
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) {
|
switch (data.parts.registry) {
|
||||||
case "registry-1.docker.io":
|
case "registry-1.docker.io":
|
||||||
url = `https://hub.docker.com/r/${data.parts.repository}`;
|
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">
|
<div className="mb-4 flex items-center gap-3">
|
||||||
<Box className={`size-6 shrink-0 text-${theme}-500`} />
|
<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 ? (
|
{url ? (
|
||||||
<>
|
<>
|
||||||
<a
|
<a
|
||||||
href={url}
|
href={url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className={`group w-fit text-black hover:underline dark:text-white`}
|
className={`group w-fit hover:underline`}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{data.reference}
|
{data.reference}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export interface Image {
|
|||||||
repository: string;
|
repository: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
};
|
};
|
||||||
|
url: string | null;
|
||||||
result: {
|
result: {
|
||||||
has_update: boolean | null;
|
has_update: boolean | null;
|
||||||
info: VersionInfo | DigestInfo | null;
|
info: VersionInfo | DigestInfo | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user