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

43 Commits

Author SHA1 Message Date
Sergio
90239f83e9 New version
Some checks are pending
Nightly Release / build-binary (map[bin:cup command:build name:cup-linux-aarch64 os:ubuntu-latest release_for:linux-aarch64 target:aarch64-unknown-linux-musl]) (push) Waiting to run
Nightly Release / build-binary (map[bin:cup command:build name:cup-linux-x86_64 os:ubuntu-latest release_for:linux-x86_64 target:x86_64-unknown-linux-musl]) (push) Waiting to run
Nightly Release / build-image (push) Waiting to run
2024-08-30 19:30:28 +03:00
Sergio
dc7a981930 Merge pull request #22 from sergi0g/set-correct-headers
Set correct headers when requesting a manifest to prevent getting a v1 manifest
2024-08-30 19:19:11 +03:00
Sergio
8d2740dc7d Set correct headers when requesting a manifest to prevent getting a v1 manifest 2024-08-30 19:17:43 +03:00
Sergio
fb674acf96 Update gitignore
Some checks are pending
Nightly Release / build-binary (map[bin:cup command:build name:cup-linux-aarch64 os:ubuntu-latest release_for:linux-aarch64 target:aarch64-unknown-linux-musl]) (push) Waiting to run
Nightly Release / build-binary (map[bin:cup command:build name:cup-linux-x86_64 os:ubuntu-latest release_for:linux-x86_64 target:x86_64-unknown-linux-musl]) (push) Waiting to run
Nightly Release / build-image (push) Waiting to run
2024-08-30 11:32:33 +03:00
Sergio
e9160334d9 Update README.md
Some checks failed
Nightly Release / build-binary (map[bin:cup command:build name:cup-linux-aarch64 os:ubuntu-latest release_for:linux-aarch64 target:aarch64-unknown-linux-musl]) (push) Has been cancelled
Nightly Release / build-binary (map[bin:cup command:build name:cup-linux-x86_64 os:ubuntu-latest release_for:linux-x86_64 target:x86_64-unknown-linux-musl]) (push) Has been cancelled
Nightly Release / build-image (push) Has been cancelled
Fix broken docs link
2024-08-21 16:23:28 +03:00
Sergio
ca6ffea29c Fix sort order in docs and a typo
Some checks failed
Deploy github pages / build (push) Has been cancelled
Nightly Release / build-binary (map[bin:cup command:build name:cup-linux-aarch64 os:ubuntu-latest release_for:linux-aarch64 target:aarch64-unknown-linux-musl]) (push) Has been cancelled
Nightly Release / build-binary (map[bin:cup command:build name:cup-linux-x86_64 os:ubuntu-latest release_for:linux-x86_64 target:x86_64-unknown-linux-musl]) (push) Has been cancelled
Nightly Release / build-image (push) Has been cancelled
Deploy github pages / deploy (push) Has been cancelled
2024-07-17 17:28:05 +03:00
Sergio
923e81d75d Update release workflow and fix bug with binaries not being uploaded
Some checks are pending
Deploy github pages / build (push) Waiting to run
Deploy github pages / deploy (push) Blocked by required conditions
Nightly Release / build-binary (map[bin:cup command:build name:cup-linux-aarch64 os:ubuntu-latest release_for:linux-aarch64 target:aarch64-unknown-linux-musl]) (push) Waiting to run
Nightly Release / build-binary (map[bin:cup command:build name:cup-linux-x86_64 os:ubuntu-latest release_for:linux-x86_64 target:x86_64-unknown-linux-musl]) (push) Waiting to run
Nightly Release / build-image (push) Waiting to run
2024-07-17 16:15:26 +03:00
Sergio
1cf4cf2394 Switch back to json 2024-07-17 15:28:32 +03:00
Sergio
d1cb62304d docs: Change basePath only in production 2024-07-17 15:10:18 +03:00
Sergio
935517d3f8 Fix docs basepath 2024-07-17 14:57:18 +03:00
Sergio
710abd5277 Make final documentation changes 2024-07-17 14:50:56 +03:00
Sergio
7b4bf6c8e3 Update docs.yml 2024-07-17 14:40:06 +03:00
Sergio
03e260bf56 Update docs.yml 2024-07-17 14:38:19 +03:00
Sergio
0d4aa17a4f Update docs workflow 2024-07-17 14:36:00 +03:00
Sergio
bea4e67021 Add manual trigger to docs workflow 2024-07-17 14:32:19 +03:00
Sergio
35f96d1e3c Fix documentation workflow to setup pnpm 2024-07-17 14:31:24 +03:00
Sergio
a822a616be Add documentation 2024-07-17 14:28:49 +03:00
Sergio
0fac554fae Added metrics to JSON, switched from json to serde_json, extracted all JSON related functionality into a function in utils.rs
Some checks are pending
Nightly Release / build-binary (map[bin:cup command:build name:cup-linux-aarch64 os:ubuntu-latest release_for:linux-aarch64 target:aarch64-unknown-linux-musl]) (push) Waiting to run
Nightly Release / build-binary (map[bin:cup command:build name:cup-linux-x86_64 os:ubuntu-latest release_for:linux-x86_64 target:x86_64-unknown-linux-musl]) (push) Waiting to run
Nightly Release / build-image (push) Waiting to run
2024-07-16 19:40:34 +03:00
Sergio
f09f8762c2 Switch stats to 2 columns on smaller screens (see #5)
Some checks failed
Nightly Release / build-binary (map[bin:cup command:build name:cup-linux-aarch64 os:ubuntu-latest release_for:linux-aarch64 target:aarch64-unknown-linux-musl]) (push) Has been cancelled
Nightly Release / build-binary (map[bin:cup command:build name:cup-linux-x86_64 os:ubuntu-latest release_for:linux-x86_64 target:x86_64-unknown-linux-musl]) (push) Has been cancelled
Nightly Release / build-image (push) Has been cancelled
2024-07-15 14:25:12 +03:00
Sergio
a84b3eab02 Update README.md
Add section about roadmap
2024-07-15 14:16:18 +03:00
Sergio
30c762ea83 Added config, custom theming and moved update checking logic to check.rs 2024-07-15 14:11:46 +03:00
Sergio
b9278ca010 Refactor and simplify server code, UI updates requested in #5 and #6
Some checks failed
Nightly Release / build-binary (map[bin:cup command:build name:cup-linux-aarch64 os:ubuntu-latest release_for:linux-aarch64 target:aarch64-unknown-linux-musl]) (push) Has been cancelled
Nightly Release / build-binary (map[bin:cup command:build name:cup-linux-x86_64 os:ubuntu-latest release_for:linux-x86_64 target:x86_64-unknown-linux-musl]) (push) Has been cancelled
Nightly Release / build-image (push) Has been cancelled
2024-07-13 15:25:39 +03:00
Sergio
814e0b8133 Add favicon.ico to head as well
Some checks are pending
Nightly Release / build-binary (map[bin:cup command:build name:cup-linux-aarch64 os:ubuntu-latest release_for:linux-aarch64 target:aarch64-unknown-linux-musl]) (push) Waiting to run
Nightly Release / build-binary (map[bin:cup command:build name:cup-linux-x86_64 os:ubuntu-latest release_for:linux-x86_64 target:x86_64-unknown-linux-musl]) (push) Waiting to run
Nightly Release / build-image (push) Waiting to run
2024-07-13 11:37:15 +03:00
Sergio
75ebda03db Simplify nightly workflow 2024-07-12 14:30:12 +03:00
Sergio
698061e8c8 Update package version 2024-07-12 13:30:24 +03:00
Sergio
0a01c667da Fix UI scaling and update docs 2024-07-12 13:26:40 +03:00
Sergio
271e299017 Moved all web assets to static folder, added xitca-web's logging feature and added favicons 2024-07-11 18:21:31 +03:00
Sergio
e0cad9d3ea Make scrollbar a bit nicer 2024-07-11 17:32:59 +03:00
Sergio
ae0c6ed50f Add caching 2024-07-11 13:32:43 +03:00
Sergio
f88f7d90f9 Fix release workflow 2024-07-11 13:29:11 +03:00
Sergio
75700cd692 Update nightly workflow to upload the correct binaries 2024-07-11 12:36:57 +03:00
Sergio
6573b43ce0 I know I'm not supposed to do this, but I have to. 2024-07-11 12:32:44 +03:00
Sergio
bee808e577 FINALLY fix Dockerfile 2024-07-11 12:27:02 +03:00
Sergio
ac8f97fbcd Update workflows 2024-07-10 19:44:55 +03:00
Sergio
4ec661183f Update nightly.yml
Make jq output unquoted string
2024-07-10 15:20:08 +03:00
Sergio
39b2355c04 Update nightly.yml
Try to fix workflow
2024-07-10 15:15:56 +03:00
Sergio
867ab4ba10 Update Cargo.toml
I have to automate this
2024-07-10 14:49:01 +03:00
Sergio
9a75f5101f Update Dockerfile
Experimental dependency caching
2024-07-10 14:41:21 +03:00
Sergio
54eed353aa Add nightly workflow 2024-07-10 14:39:03 +03:00
Sergio
e3319d9371 Update Dockerfile
Hotfix
2024-07-10 14:23:05 +03:00
Sergio
38d775688a Update Dockerfile
Fix broken build process (attempt 1)
2024-07-10 14:07:22 +03:00
Sergio
31dd605efb Update Cargo.toml
I forgot to change the version
2024-07-10 12:08:00 +03:00
Sergio
06ec23520d Updated image regex to support any image. Removed dependency on fancy_regex in favour of the regex crate 2024-07-10 09:59:00 +03:00
47 changed files with 5698 additions and 512 deletions

View File

@@ -1,27 +1,52 @@
name: Build docker image
name: Release
on:
release:
types:
- published
env:
CARGO_TERM_COLOR: always
push:
tags: ["v*.*.*"]
jobs:
build:
get-tag:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.tag.outputs.tag }}
steps:
- name: Get current tag
id: tag
run: |
TAG=$(echo "$GITHUB_REF" | sed 's/^refs\/tags\///')
echo "Current tag: $TAG"
echo "tag=$TAG" >> $GITHUB_OUTPUT
build-binary:
strategy:
matrix:
arch:
- aarch64
- x86_64
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Checkout
uses: actions/checkout@v4
- name: Get latest release tag
id: release
uses: pozetroninc/github-action-get-latest-release@master
- name: Setup rust
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Install cross
run: cargo install cross --git https://github.com/cross-rs/cross
- name: Build binary
run: cross build --target ${{ matrix.arch }}-unknown-linux-musl --release
- name: Upload binary
uses: actions/upload-artifact@v4
with:
owner: sergi0g
repo: cup
excludes: prerelease, draft
name: cup-linux-${{ matrix.arch }}
path: target/${{ matrix.arch }}-unknown-linux-musl/release/cup
build-image:
needs: get-tag
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -33,17 +58,48 @@ jobs:
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64, linux/arm64
push: true
platforms: linux/amd64,linux/arm64
tags: |
ghcr.io/sergi0g/cup:latest
ghcr.io/sergi0g/cup:${{ steps.release.outputs.release }}
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
cache-to: type=gha,mode=max
release:
runs-on: ubuntu-latest
needs: [get-tag, build-image, build-binary]
steps:
- name: Download arm64 binary
uses: actions/download-artifact@v4
with:
name: cup-linux-aarch64
path: cup-linux-aarch64
- name: Download x86 binary
uses: actions/download-artifact@v4
with:
name: cup-linux-x86_64
path: cup-linux-x86_64
- name: Extract and rename binaries
run: |
unzip cup-linux-aarch64.zip && mv cup cup-linux-aarch64
unzip cup-linux-x86_64.zip && mv cup cup-linux-x86_64
- name: Create release
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
prerelease: true
tag_name: ${{ needs.get-tag.outputs.tag }}
name: ${{ needs.get-tag.outputs.tag }}
files: |
cup-linux-aarch64
cup-linux-x86_64

43
.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
name: Deploy github pages
on:
push:
paths:
- "docs/**"
workflow_dispatch:
jobs:
build:
defaults:
run:
working-directory: docs
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: latest
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/out/
deploy:
needs: build
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

72
.github/workflows/nightly.yml vendored Normal file
View File

@@ -0,0 +1,72 @@
name: Nightly Release
on:
push:
branches: main
jobs:
build-binary:
strategy:
matrix:
platform:
- release_for: linux-aarch64
os: ubuntu-latest
target: aarch64-unknown-linux-musl
bin: cup
name: cup-linux-aarch64
command: build
- release_for: linux-x86_64
os: ubuntu-latest
target: x86_64-unknown-linux-musl
bin: cup
name: cup-linux-x86_64
command: build
runs-on: ${{ matrix.platform.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup rust
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Install cross
run: cargo install cross --git https://github.com/cross-rs/cross
- name: Build binary
run: cross ${{ matrix.platform.command }} --target ${{ matrix.platform.target }}
- name: Upload CLI
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.platform.name }}
path: target/${{ matrix.platform.target }}/debug/${{ matrix.platform.bin }}
build-image:
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
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:nightly
cache-from: type=gha
cache-to: type=gha,mode=max

6
.gitignore vendored
View File

@@ -1 +1,7 @@
/target
/docs/.next
/docs/node_modules
/docs/out
# In case I accidentally commit mine...
cup.json

120
Cargo.lock generated
View File

@@ -140,21 +140,6 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bit-set"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -365,17 +350,18 @@ dependencies = [
[[package]]
name = "cup"
version = "0.1.0"
version = "2.0.1"
dependencies = [
"bollard",
"chrono",
"clap",
"fancy-regex",
"http-auth",
"indicatif",
"json",
"liquid",
"once_cell",
"rayon",
"regex",
"termsize",
"tokio",
"ureq",
@@ -426,17 +412,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "fancy-regex"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
dependencies = [
"bit-set",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "flate2"
version = "1.0.30"
@@ -944,6 +919,16 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "num-conv"
version = "0.1.0"
@@ -990,6 +975,12 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "percent-encoding"
version = "2.3.1"
@@ -1249,18 +1240,18 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
version = "1.0.203"
version = "1.0.204"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.203"
version = "1.0.204"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
dependencies = [
"proc-macro2",
"quote",
@@ -1329,6 +1320,15 @@ dependencies = [
"digest",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
@@ -1429,6 +1429,16 @@ dependencies = [
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]]
name = "time"
version = "0.3.36"
@@ -1551,9 +1561,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.32"
@@ -1561,6 +1583,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
dependencies = [
"nu-ansi-term",
"sharded-slab",
"smallvec",
"thread_local",
"tracing-core",
"tracing-log",
]
[[package]]
@@ -1653,6 +1701,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "version_check"
version = "0.9.4"
@@ -1986,6 +2040,8 @@ dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
"tracing",
"tracing-subscriber",
"xitca-http",
"xitca-server",
"xitca-service",

View File

@@ -1,6 +1,6 @@
[package]
name = "cup"
version = "0.1.0"
version = "2.0.1"
edition = "2021"
[dependencies]
@@ -8,19 +8,20 @@ clap = { version = "4.5.7", features = ["derive"] }
indicatif = { version = "0.17.8", optional = true }
tokio = {version = "1.38.0", features = ["rt", "rt-multi-thread", "macros"]}
ureq = { version = "2.9.7", features = ["tls"] }
json = "0.12.4"
rayon = "1.10.0"
xitca-web = { version = "0.5.0", optional = true }
xitca-web = { version = "0.5.0", optional = true, features = ["logger"] }
liquid = { version = "0.26.6", optional = true }
bollard = "0.16.1"
once_cell = "1.19.0"
fancy-regex = "0.13.0"
http-auth = { version = "0.1.9", features = [] }
termsize = { version = "0.1.8", optional = true }
regex = "1.10.5"
chrono = { version = "0.4.38", default-features = false, features = ["std", "alloc", "clock"], optional = true }
json = "0.12.4"
[features]
default = ["server", "cli"]
server = ["dep:xitca-web", "dep:liquid"]
server = ["dep:xitca-web", "dep:liquid", "dep:chrono"]
cli = ["dep:indicatif", "dep:termsize"]
[profile.release]
@@ -28,4 +29,4 @@ opt-level = "z"
strip = "symbols"
panic = "abort"
lto = "fat"
codegen-units = 1
codegen-units = 1

View File

@@ -3,12 +3,16 @@ WORKDIR /
RUN apk add musl-dev
RUN USER=root cargo new cup
RUN USER=root cargo new --bin cup
WORKDIR /cup
COPY Cargo.toml Cargo.lock ./
RUN cargo build --release
COPY src ./src
COPY Cargo.toml Cargo.lock .
RUN cargo build --release
RUN rm -rf src/
COPY src src
# This is a very bad workaround, but cargo only triggers a rebuild this way for some reason
RUN printf "\n" >> src/main.rs
RUN cargo build --release
FROM scratch

128
README.md
View File

@@ -18,135 +18,21 @@ Cup is the easiest way to check for container image updates.
- The binary is tiny! At the time of writing it's just 4.7 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!
## Installation
## Documentation
You can install Cup in 2 ways: as a docker container (recommended) or as a binary.
### With docker
Requirements: [Docker](https://docs.docker.com/engine/install/)
Difficulty: Easy
- Open a terminal and run `docker pull ghcr.io/sergi0g/cup`. If you're not in the `docker` group, make sure to prefix the command as root.
That's it! Now head over to the Usage section!
### From source
Requirements: [Rust](https://rustup.rs)
Difficulty: Moderate
1. Open a terminal and clone the git repository
```
$ git clone https://github.com/sergi0g/cup
```
2. Change your directory to the repository you just cloned
```
$ cd cup
```
3. Build Cup
```
$ cargo build --release
```
This will build cup with all features by default. If you want to build just the server, you can append `--no-default-features --features cli` or `--no-default-features --features server` depending one what you need.
4. Add the binary to your path for easy access
Consult your shell's documentation on how to do this. For Bash, the following will likely work:
```
$ mv /target/release/cup ~/.local/bin
```
Make sure to reload your shell.
You're now ready to use Cup!
## Usage
### CLI
Cup's CLI provides the `cup check` command.
Basic usage:
```
$ cup check
nginx:alpine Update available
redis:7 Update available
redis:alpine Update available
...
centos:7 Up to date
mcr.microsoft.com/devcontainers/go:0-1.19-bullseye Up to date
rockylinux:9-minimal Up to date
rabbitmq:3.11.9-management Up to date
...
some/deleted:image Unknown
```
If you have a [Nerd Font](https://nerdfonts.com) installed, you can also add icons with the `-i` flag: `cup check -i`
If you want the output to be JSON, use the `-r` flag:
```
$ cup check -r
[{"image":"nginx:alpine","has_update":true},{"image":"rockylinux:9-minimal","has_update":false},{"image":"some/deleted:image","has_update":null}]
```
Checking a single image is also possible:
```
$ cup check node:latest
node:latest has an update available
```
If using the docker image:
```
$ docker run -v /var/run/docker.sock:/var/run/docker.sock cup check
```
### Server
The server provides the `cup serve` command.
Basic usage:
```
$ cup serve
Serving on http://0.0.0.0:8000
Received GET request on /
Received GET request on /json
```
This will launch the server on port 8000. To access it, visit `http://<YOUR_IP>:8000` (replace `<YOUR_IP>` with the IP address of the machine running Cup.)
The URL `http://<YOUR_IP>:8000/json` is also available for usage with integrations.
If you want to use a custom port, use the `-p` flag:
```
$ cup check -p 9000
Serving on http://0.0.0.0:9000
Received GET request on /
Received GET request on /json
```
If using the docker image (replace `<PORT>` with the port you're using (default 8000)):
```
$ docker run -v /var/run/docker.sock:/var/run/docker.sock -p <PORT>:<PORT> cup serve
```
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 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 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)
## Roadmap
Take a sneak peek at what's coming up in future releases on the [roadmap](https://github.com/users/sergi0g/projects/2)!
## Contributing
All contributions are welcome!
@@ -160,7 +46,7 @@ Here are some ideas to get you started:
To contribute, fork the repository, make your changes and the submit a pull request.
Note: If you update the UI, please make sure to recompile the CSS with `tailwindcss -mo src/index.css`. You need to have the Tailwind CSS CLI installed ([instructions here](https://tailwindcss.com/docs/installation))
Note: If you update the UI, please make sure to recompile the CSS with `tailwindcss -mo src/static/index.css`. You need to have the Tailwind CSS CLI installed ([instructions here](https://tailwindcss.com/docs/installation))
## Support

1
docs/README.md Normal file
View File

@@ -0,0 +1 @@
This is where Cup's documentation lives. It's created with [Nextra](https://nextra.site).

BIN
docs/assets/blue_theme.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
docs/assets/cup.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

BIN
docs/assets/gray_theme.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
docs/assets/old_cup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

17
docs/next.config.js Normal file
View File

@@ -0,0 +1,17 @@
const withNextra = require("nextra")({
theme: "nextra-theme-docs",
themeConfig: "./theme.config.jsx",
});
module.exports = withNextra(
{
output: "export",
images: {
unoptimized: true
},
basePath: process.env.NODE_ENV == 'production' ? '/cup' : ''
}
);
// If you have other Next.js configurations, you can pass them as the parameter:
// module.exports = withNextra({ /* other next.js config */ })

20
docs/package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@tabler/icons-react": "^3.11.0",
"next": "^14.2.5",
"nextra": "^2.13.4",
"nextra-theme-docs": "^2.13.4",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"autoprefixer": "^10.4.19",
"postcss": "^8.4.39",
"tailwindcss": "^3.4.5"
}
}

10
docs/pages/_app.mdx Normal file
View File

@@ -0,0 +1,10 @@
import '../styles.css';
import 'nextra-theme-docs/style.css';
export default function App({ Component, pageProps }) {
return (
<main>
<Component {...pageProps} />
</main>
);
}

13
docs/pages/_meta.json Normal file
View File

@@ -0,0 +1,13 @@
{
"docs": {
"title": "Documentation",
"type": "page"
},
"about": {
"title": "About",
"type": "page",
"theme": {
"typesetting": "article"
}
}
}

110
docs/pages/about.mdx Normal file
View File

@@ -0,0 +1,110 @@
import Image from "next/image";
import old_cup from "../assets/old_cup.png"
import web_ui from "../assets/blue_theme.png"
# About
Cup is a small utility that checks for updates to Docker containers. The logic is simple: Cup checks the locally pulled images' digests against the latest ones in their registry. It then presents the results in a pretty interface. Here's the story:
## How it started
I got the basic idea for Cup a long time ago. I was looking at [Homepage's list of widgets](https://gethomepage.dev/latest/widgets/) when I discovered [What's Up Docker?](https://github.com/fmartinou/whats-up-docker) (referred to as WUD from now on).
According to the docs:
> What's up Docker ( aka WUD ) gets you notified when a new version of your Docker Container is available.
It supports the most common registries, has integrations with IFTTT, Slack, Telegram and other apps/services for notifications or triggering workflows and also has the option to automatically update containers, like [Watchtower](https://github.com/containrrr/watchtower).
I was managing my homelab myself at that time and the only way to check if I had updates was log in to the server and manually try to pull the images for *every single compose file*. WUD seemed to solve the problem nicely, so I decided to give it a try. I never used automatic updates or notifications, but I configured it and let it run.
After deploying it and setting up my reverse proxy, I was greeted with this dashboard:
<Image src="https://github.com/fmartinou/whats-up-docker/blob/master/docs/ui/ui.png?raw=true" alt="A screenshot of WUD's web UI, from the docs" />
It was working fine, but... the UI was not what I expected. It really reminds me of some really old Android app (I hope I didn't offend anyone). That was strike one. Nevertheless, I left it running. It was useful after all.
A few days later I was pulling some docker images, when I got this error message:
> You have reached your pull rate limit. You may increase the limit by authenticating and upgrading: https://www.docker.com/increase-rate-limits.
Wait a minute. What was that? I'd never encountered a message like this before. I thought "Weird. Maybe I pulled too many images today?". So I decided to finish those updates another day.
Next time I tried, same issue. "What the heck is happening?" I thought. The only change I'd made to my homelab at that time was installing WUD. So I stopped it. And that's where the problems ended.
The problem was clearly related to WUD, so I started trying to find what was going wrong. That was when I came upon [this page from Docker's documentation](https://docs.docker.com/docker-hub/download-rate-limit/). I noticed 2 things:
> A pull request is defined as up to two `GET` requests on registry manifest URLs (`/v2/*/manifests/*`)
> `HEAD` requests aren't counted.
There were also helpful instructions on how to check the rate limit:
```
sergio@desktop:~ $ TOKEN=$(curl "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | jq -r .token)
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 5429 0 5429 0 0 7431 0 --:--:-- --:--:-- --:--:-- 7426
sergio@desktop:~ $ curl --head -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest
HTTP/1.1 200 OK
content-length: 2782
content-type: application/vnd.docker.distribution.manifest.v1+prettyjws
docker-content-digest: sha256:767a3815c34823b355bed31760d5fa3daca0aec2ce15b217c9cd83229e0e2020
docker-distribution-api-version: registry/2.0
etag: "sha256:767a3815c34823b355bed31760d5fa3daca0aec2ce15b217c9cd83229e0e2020"
date: Tue, 16 Jul 2024 12:13:17 GMT
strict-transport-security: max-age=31536000
ratelimit-limit: 100;w=21600
ratelimit-remaining: 100;w=21600
docker-ratelimit-source: <REDACTED>
```
The rate limit is there, just like in the docs, but do you see something else interesting? Look at this header: `docker-content-digest: sha256:767a3815c34823b355bed31760d5fa3daca0aec2ce15b217c9cd83229e0e2020`
This is an image's digest. Can we check for updates by making `HEAD` requests to Docker Hub?
The answer is yes:
```
$ set TOKEN $(curl -H "Accept: application/vnd.docker.distribution.manifest.list.v2+json" "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/busybox:pull" | jq -r .token)
$ curl --head -H "Authorization: Bearer $TOKEN" -H "Accept: application/vnd.docker.distribution.manifest.v2.list+json" https://registry-1.docker.io/v2/library/busybox/manifests/latest
HTTP/1.1 200 OK
content-length: 6761
content-type: application/vnd.oci.image.index.v1+json
docker-content-digest: sha256:9ae97d36d26566ff84e8893c64a6dc4fe8ca6d1144bf5b87b2b85a32def253c7
docker-distribution-api-version: registry/2.0
etag: "sha256:9ae97d36d26566ff84e8893c64a6dc4fe8ca6d1144bf5b87b2b85a32def253c7"
date: Tue, 16 Jul 2024 12:17:49 GMT
strict-transport-security: max-age=31536000
ratelimit-limit: 100;w=21600
ratelimit-remaining: 100;w=21600
docker-ratelimit-source: <REDACTED>
```
And then we can compare that with the digest of the image stored locally:
```
$ docker inspect busybox:latest | jq -r '.[0].RepoDigests[0]'
busybox@sha256:9ae97d36d26566ff84e8893c64a6dc4fe8ca6d1144bf5b87b2b85a32def253c7
```
Notice how the 2 digests are the same. We can check for image updates without using up the rate limit!
That's when I got the idea of writing a program to do this automatically.
## The birth of Cup
I initially intended to write a simple bash script but I chose not to for the following reasons:
- I wanted something more than a simple script. WUD has a web UI and support for so many integrations! I had to match that some way!
- Bash is slow and I was learning Rust at the time, so I wanted to practice (and make a proper project)
It started out as a small CLI that could either check a single image, or check all the images.
<Image src={old_cup} alt="The initial version of Cup" />
It also couldn't check for updates to images not from Docker Hub, lacked a web UI and generally had many limitations. But it proved it could be done, quickly and efficiently. The binary was just 5 MB and took about 5 seconds for ~90 images on my development machine. That's insane!
A few days later, I decided to completely rewrite it. I tried to write clean code, split it in files and fix every limitation from the previous version. I'm quite close. Here's what it looks like now:
<Image src="https://github.com/sergi0g/cup/blob/main/screenshots/cup.gif?raw=true" alt="Cup's old CLI" />
It also has a statically rendered web UI making it ideal for self hosting.
<Image src={web_ui} alt="Cup's web UI"/>
With some optimization (well ok, maybe a lot), the binary is 5 MB and that means I finally don't have to wait forever to pull the Docker image! Finally something that works nicely with my 1.5 MB/s internet connection! (Thank you powerline!)
Now go ahead and try it out!

View File

@@ -0,0 +1,17 @@
{
"index": {
"title": "Introduction"
},
"installation": {
"title": "Installation"
},
"configuration": {
"title": "Configuration"
},
"usage": {
"title": "Usage"
},
"nightly": {
"title": "Using the latest version"
}
}

View File

@@ -0,0 +1,98 @@
import Image from "next/image";
import { Steps, Callout } from "nextra-theme-docs";
import blue from "../../assets/blue_theme.png"
import gray from "../../assets/gray_theme.png"
# Configuration
## Custom docker socket
Sometimes, there may be a need to specify a custom docker socket. Cup provides the `-s` option for this.
For example, if using Podman, you might do
```
$ cup -s /run/user/1000/podman/podman.sock check
```
## Configuration file
Cup has an option to be configured from a configuration file named `cup.json`.
<Steps>
### Create the configuration file
Create a `cup.json` file somewhere on your system. For binary installs, a path like `~/.config/cup.json` is recommended.
If you're running with Docker, you can create a `cup.json` in the directory you're running cup and mount it into the container. _In the next section you will need to use the path where you **mounted** the file_
### Configure Cup from the configuration file
Follow the guides below (Theme and Authentication) to make your `cup.json`
Here's a full example:
```json
{
authentication: {
"ghcr.io": "<YOUR_TOKEN_HERE>",
"registry-1.docker.io": "<YOUR_TOKEN_HERE>"
},
theme: "blue"
}
```
### Run Cup with the new configuration file
To let Cup know that you'd like it to use a custom configuration file, you can use the `-c` flag, followed by the _absolute_ path of the file.
```bash
$ cup -c /home/sergio/.config/cup.json check
```
```bash
$ docker run -tv /var/run/docker.sock:/var/run/docker.sock -v /home/sergio/.config/cup.json:/config/cup.json ghcr.io/sergi0g/cup -c /config/cup.json serve
```
</Steps>
## Theme (server only)
Cup initially had a blue theme which looked like this:
<Image alt="Screenshot of blue theme" src={blue} />
This was replaced by a more neutral theme which is now the default:
<Image alt="Screenshot of neutral theme" src={gray} />
However, you can get the old theme back by adding the `theme` key to your `cup.json`
Available values are `default` and `blue`.
Here's an example:
```json
{
"theme": "blue",
// Other options
}
```
## Authentication
<Callout emoji="⛔">
The features described in this section have not been implemented yet.
</Callout>
Some registries (or specific images) may require you to be authenticated. For those, you can modify `cup.json` like this:
```json
{
"authentication": {
"<YOUR_REGISTRY_DOMAIN_1>": "<YOUR_TOKEN_1>",
"<YOUR_REGISTRY_DOMAIN_2>": "<YOUR_TOKEN_2>"
// ...
},
// Other options
}
```
You can use any registry, like `ghcr.io`, `quay.io`, `gcr.io`, etc.
<Callout emoji="⚠️">
For Docker Hub, use `registry-1.docker.io`
</Callout>

26
docs/pages/docs/index.mdx Normal file
View File

@@ -0,0 +1,26 @@
import Image from "next/image";
import cup from "../../assets/cup.gif";
import { Cards, Card } from "nextra-theme-docs";
import { IconBrandDocker, IconPackage } from "@tabler/icons-react";
# Introduction
<Image src={cup} unoptimized />
Cup is a lightweight alternative to [What's up Docker?](https://github.com/fmartinou/whats-up-docker) written in Rust.
# 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 ~6 seconds for 70 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? 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 4.7 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!
# Installation
<Cards>
<Card icon={<IconBrandDocker />} title="With Docker" href="/docs/installation/docker" />
<Card icon={<IconPackage />} title="As a binary" href="/docs/installation/binary" />
</Cards>

View File

@@ -0,0 +1,8 @@
{
"docker": {
"title": "With Docker"
},
"binary": {
"title": "As a binary"
}
}

View File

@@ -0,0 +1,25 @@
import { Callout, Card, Steps } from "nextra-theme-docs";
import { IconFileDescription } from "@tabler/icons-react";
# As a binary
## Introduction
This guide will help you install Cup from a binary.
## Installation
<Steps>
### Download binary
Go to https://github.com/sergi0g/cup/releases/latest.
Depending on your system's architecture, choose the binary for your system. For example, for an `x86_64` machine, you should download `cup-x86_64-unknown-linux-musl`
<Callout>
You can use the command `uname -i` to find this
</Callout>
### Add binary to path
Move the binary you downloaded to a directory in your path. You can usually get a list those directories by running `echo $PATH`. On most Linux systems, moving it to `~/.local/bin` is usually enough.
</Steps>
That's it! Cup is ready to be used. Head over to the Usage page to get started.
<br />
<Card icon={<IconFileDescription />} title="Usage" href="/docs/usage" />

View File

@@ -0,0 +1,21 @@
import { Callout, Card } from "nextra-theme-docs";
import { IconFileDescription } from "@tabler/icons-react";
# With Docker
## Introduction
This guide will help you install Cup as a Docker container. It is the easiest installation method and also makes updating Cup very easy.
## Installation
To get started, open up a terminal and run the following command.
```bash
$ docker pull ghcr.io/sergi0g/cup
```
<Callout emoji="⚠️">
If you aren't in the `docker` group, please ensure you run all commands as a user who does. In most cases, you'll just need to prefix the `docker` commands with `sudo`
</Callout>
That's it! Cup is ready to be used. Head over to the Usage page to get started.
<br />
<Card icon={<IconFileDescription />} title="Usage" href="/docs/usage" />

View File

@@ -0,0 +1,21 @@
import { Callout } from "nextra-theme-docs"
# Using the latest version
The installation instructions you previously followed describe how to install Cup's stable version.
However, it is only updated when a new release is created, so if you want the latest features, you'll need to install Cup's nightly version.
Cup's nightly version always contains the latest changes in the main branch.
<Callout emoji="⚠️">
There is no guarantee that the nightly version will always work. There may be breaking changes or a bad commit and it may not work properly. Install nightly only if you know what you are doing. These instructions will assume you have the technical know-how to follow them. If you do not, please use the stable release
</Callout>
## With Docker
Instead of `ghcr.io/sergi0g/cup`, use `ghcr.io/sergi0g/cup:nightly`
## As a binary
Go to a [nightly workflow run](https://github.com/sergi0g/cup/actions/workflows/nightly.yml) and download the artifact for your system.

11
docs/pages/docs/usage.mdx Normal file
View File

@@ -0,0 +1,11 @@
import { IconServer, IconTerminal } from "@tabler/icons-react";
import { Cards, Card } from "nextra-theme-docs";
# Usage
You can use Cup in 2 different ways. As a CLI or as a server. You can learn more about each mode in its corresponding page
<Cards>
<Card icon={<IconTerminal />} title="CLI" href="/docs/usage/cli" />
<Card icon={<IconServer />} title="Server" href="/docs/usage/server" />
</Cards>

View File

@@ -0,0 +1,73 @@
import Image from "next/image";
import cup from "../../../assets/cup.gif";
# CLI
Cup's CLI provides the `cup check` command.
## Basic Usage
### Check for all updates
```ansi
$ cup check
nginx:alpine Update available
redis:7 Update available
redis:alpine Update available
...
centos:7 Up to date
mcr.microsoft.com/devcontainers/go:0-1.19-bullseye Up to date
rockylinux:9-minimal Up to date
rabbitmq:3.11.9-management Up to date
...
some/deleted:image Unknown
```
### Check for updates to a specific image
```
$ cup check node:latest
node:latest has an update available
```
## Enable icons
You can also enable icons if you have a [Nerd Font](https://nerdfonts.com) installed.
<Image src={cup} unoptimized />
## JSON output
When integrating Cup with other services (e.g. webhooks or a dashboard), you may find Cup's JSON output functionality useful.
It provides some useful metrics (see [server](/docs/usage/server) for more information), along with a list of images and whether they have an update or not.
```
$ cup check -r
{"metrics":{"update_available":4,"monitored_images":25,"unknown":1,"up_to_date":20},"images":{"ghcr.io/immich-app/immich-server:v1.106.4":false,"portainer/portainer-ce:2.20.3-alpine":false,"ghcr.io/runtipi/runtipi:v3.4.1":false,...}}
```
Here is how it would look in Typescript:
```ts
type CupData = {
metrics: {
monitored_images: number,
up_to_date: number,
update_available: number,
unknown: number
},
images: {
[image: string]: boolean | null
}
}
```
## Usage with Docker
If you're using the Docker image, just replace all occurences of `cup` in the examples with `docker run -tv /var/run/docker.sock:/var/run/docker.sock ghcr.io/sergi0g/cup`.
For example, this:
```bash /check node:latest/
$ cup check node:latest
```
becomes:
```bash /check node:latest/
$ docker run -tv /var/run/docker.sock:/var/run/docker.sock ghcr.io/sergi0g/cup check node:latest
```

View File

@@ -0,0 +1,53 @@
import { Callout } from "nextra-theme-docs";
# Server
The server provides the `cup serve` command.
## Basic usage
```ansi
$ cup serve
2024-07-17T09:08:38.724922Z   INFO  xitca_server::net  :  Started Tcp listening on: Some(0.0.0.0:8000)
2024-07-17T09:08:38.725076Z   WARN  xitca_server::server::future  :  ServerFuture::wait is called from within tokio context. It would block current thread from handling async tasks
2024-07-17T09:08:38.725248Z   INFO  xitca_server::worker  :  Started xitca-server-worker-0
2024-07-17T09:08:38.725343Z   INFO  xitca_server::worker  :  Started xitca-server-worker-1
2024-07-17T09:08:38.725580Z   INFO  xitca_server::worker  :  Started xitca-server-worker-2
2024-07-17T09:08:38.725607Z   INFO  xitca_server::worker  :  Started xitca-server-worker-3
2024-07-17T09:08:41.390783Z   INFO  request  {  method  = GET uri  = / }  :  on_request  :  serving request
2024-07-17T09:08:41.390905Z   INFO  request  {  method  = GET uri  = / }  :  on_response  :  sending response
```
This will launch the server on port `8000`. To access it, visit `http://<YOUR_IP>:8000` (replace `<YOUR_IP>` with the IP address of the machine running Cup.)
<Callout>
The URL `http://<YOUR_IP>:8000/json` is also available for usage with integrations.
</Callout>
## Use a different port
Pass the `-p` argument with the port you want to use
```ansi
$ cup serve -p 9000
2024-07-17T09:08:38.724922Z   INFO  xitca_server::net  :  Started Tcp listening on: Some(0.0.0.0:9000)
2024-07-17T09:08:38.725076Z   WARN  xitca_server::server::future  :  ServerFuture::wait is called from within tokio context. It would block current thread from handling async tasks
2024-07-17T09:08:38.725248Z   INFO  xitca_server::worker  :  Started xitca-server-worker-0
2024-07-17T09:08:38.725343Z   INFO  xitca_server::worker  :  Started xitca-server-worker-1
2024-07-17T09:08:38.725580Z   INFO  xitca_server::worker  :  Started xitca-server-worker-2
2024-07-17T09:08:38.725607Z   INFO  xitca_server::worker  :  Started xitca-server-worker-3
2024-07-17T09:08:41.390783Z   INFO  request  {  method  = GET uri  = / }  :  on_request  :  serving request
2024-07-17T09:08:41.390905Z   INFO  request  {  method  = GET uri  = / }  :  on_response  :  sending response
```
## Usage with Docker
If you're using the Docker image, just replace all occurences of `cup` in the examples with `docker run -tv /var/run/docker.sock:/var/run/docker.sock -p <PORT>:<PORT> ghcr.io/sergi0g/cup`, where `<PORT>` is the port Cup will be using.
For example, this:
```bash /serve -p 9000/
$ cup serve -p 9000
```
becomes:
```bash /serve -p 9000/
$ docker run -tv /var/run/docker.sock:/var/run/docker.sock -p 9000:9000 ghcr.io/sergi0g/cup serve -p 9000
```

4089
docs/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

6
docs/postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

7
docs/styles.css Normal file
View File

@@ -0,0 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.tabler-icon {
color: rgb(250 250 250 / var(--tw-text-opacity)) !important
}

11
docs/tailwind.config.js Normal file
View File

@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"theme.config.jsx"
],
theme: {
extend: {},
},
plugins: [],
}

111
docs/theme.config.jsx Normal file
View File

@@ -0,0 +1,111 @@
import { ThemeSwitch } from "nextra-theme-docs";
import { useRouter } from "next/router";
import { useConfig } from "nextra-theme-docs";
export default {
docsRepositoryBase: "https://github.com/sergi0g/cup/tree/main/docs",
useNextSeoProps() {
const { asPath } = useRouter()
if (asPath !== '/') {
return {
titleTemplate: '%s Cup'
}
}
},
head: () => {
const { asPath } = useRouter()
const { frontMatter } = useConfig()
const url =
'https://sergi0g.github.io/cup' +
(`/${asPath}`);
return (
<>
<meta property="og:url" content={url} />
<meta property="og:title" content={frontMatter.title || 'Cup'} />
<meta
property="og:description"
content={frontMatter.description || 'The easiest way to manage your container updates'}
/>
</>
)
},
logo: (
<div className="flex items-center">
<Logo />
<h1 className="font-bold ml-2">Cup</h1>
</div>
),
logoLink: "sergi0g.github.io/cup",
project: {
link: "https://github.com/sergi0g/cup",
},
navbar: {
extraContent: <ThemeSwitch lite className="[&_span]:hidden" />,
},
toc: {
backToTop: true,
},
footer: {
text: null,
},
navigation: false,
};
function Logo() {
return (
<svg
viewBox="0 0 128 128"
style={{ height: "calc(var(--nextra-navbar-height) * 0.6)" }}
>
<path
style={{ fill: "#A6CFD6" }}
d="M65.12,17.55c-17.6-0.53-34.75,5.6-34.83,14.36c-0.04,5.2,1.37,18.6,3.62,48.68s2.25,33.58,3.5,34.95
c1.25,1.37,10.02,8.8,25.75,8.8s25.93-6.43,26.93-8.05c0.48-0.78,1.83-17.89,3.5-37.07c1.81-20.84,3.91-43.9,3.99-45.06
C97.82,30.66,94.2,18.43,65.12,17.55z"
/>
<path
style={{ fill: "#DCEDF6" }}
d="M41.4,45.29c-0.12,0.62,1.23,24.16,2.32,27.94c1.99,6.92,9.29,7.38,10.23,4.16
c0.9-3.07-0.38-29.29-0.38-29.29s-3.66-0.3-6.43-0.84C44,46.63,41.4,45.29,41.4,45.29z"
/>
<path
style={{ fill: "#6CA4AE" }}
d="M33.74,32.61c-0.26,8.83,20.02,12.28,30.19,12.22c13.56-0.09,29.48-4.29,29.8-11.7
S79.53,21.1,63.35,21.1C49.6,21.1,33.96,25.19,33.74,32.61z"
/>
<path
style={{ fill: "#DC0D27" }}
d="M84.85,13.1c-0.58,0.64-9.67,30.75-9.67,30.75s2.01-0.33,4-0.79c2.63-0.61,3.76-1.06,3.76-1.06
s7.19-22.19,7.64-23.09c0.45-0.9,21.61-7.61,22.31-7.93c0.7-0.32,1.39-0.4,1.46-0.78c0.06-0.38-2.34-6.73-3.11-6.73
C110.47,3.47,86.08,11.74,84.85,13.1z"
/>
<path
style={{ fill: "#8A1F0F" }}
d="M110.55,7.79c1.04,2.73,2.8,3.09,3.55,2.77c0.45-0.19,1.25-1.84,0.01-4.47
c-0.99-2.09-2.17-2.74-2.93-2.61C110.42,3.6,109.69,5.53,110.55,7.79z"
/>
<g>
<path
style={{ fill: "#8A1F0F" }}
d="M91.94,18.34c-0.22,0-0.44-0.11-0.58-0.3l-3.99-5.77c-0.22-0.32-0.14-0.75,0.18-0.97
c0.32-0.22,0.76-0.14,0.97,0.18l3.99,5.77c0.22,0.32,0.14,0.75-0.18,0.97C92.21,18.3,92.07,18.34,91.94,18.34z"
/>
</g>
<g>
<path
style={{ fill: "#8A1F0F" }}
d="M90.28,19.43c-0.18,0-0.35-0.07-0.49-0.2l-5.26-5.12c-0.28-0.27-0.28-0.71-0.01-0.99
c0.27-0.28,0.71-0.28,0.99-0.01l5.26,5.12c0.28,0.27,0.28,0.71,0.01,0.99C90.64,19.36,90.46,19.43,90.28,19.43z"
/>
</g>
<g>
<path
style={{ fill: "#8A1F0F" }}
d="M89.35,21.22c-0.12,0-0.25-0.03-0.36-0.1l-5.6-3.39c-0.33-0.2-0.44-0.63-0.24-0.96
c0.2-0.33,0.63-0.44,0.96-0.24l5.6,3.39c0.33,0.2,0.44,0.63,0.24,0.96C89.82,21.1,89.59,21.22,89.35,21.22z"
/>
</g>
</svg>
);
}

84
src/check.rs Normal file
View File

@@ -0,0 +1,84 @@
use std::{collections::{HashMap, HashSet}, sync::Mutex};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use crate::{docker::get_images_from_docker_daemon, image::Image, registry::{check_auth, get_token, get_latest_digests}, utils::unsplit_image};
#[cfg(feature = "cli")]
use crate::docker::get_image_from_docker_daemon;
#[cfg(feature = "cli")]
use crate::registry::get_latest_digest;
pub trait Unique<T> {
// So we can filter vecs for duplicates
fn unique(&mut self);
}
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();
self.retain(|item| seen.insert(item.clone()));
}
}
pub async fn get_all_updates(socket: Option<String>) -> Vec<(String, Option<bool>)> {
let image_map_mutex: Mutex<HashMap<String, &Option<String>>> = Mutex::new(HashMap::new());
let local_images = get_images_from_docker_daemon(socket).await;
local_images.par_iter().for_each(|image| {
let img = unsplit_image(&image.registry, &image.repository, &image.tag);
image_map_mutex.lock().unwrap().insert(img, &image.digest);
});
let image_map = image_map_mutex.lock().unwrap().clone();
let mut registries: Vec<&String> = local_images
.par_iter()
.map(|image| &image.registry)
.collect();
registries.unique();
let mut remote_images: Vec<Image> = Vec::new();
for registry in registries {
let images: Vec<&Image> = local_images
.par_iter()
.filter(|image| &image.registry == registry)
.collect();
let mut latest_images = match check_auth(registry) {
Some(auth_url) => {
let token = get_token(images.clone(), &auth_url);
get_latest_digests(images, Some(&token))
}
None => get_latest_digests(images, None),
};
remote_images.append(&mut latest_images);
}
let result_mutex: Mutex<Vec<(String, Option<bool>)>> = Mutex::new(Vec::new());
remote_images.par_iter().for_each(|image| {
let img = unsplit_image(&image.registry, &image.repository, &image.tag);
match &image.digest {
Some(d) => {
let r = d != image_map.get(&img).unwrap().as_ref().unwrap();
result_mutex.lock().unwrap().push((img, Some(r)))
}
None => result_mutex.lock().unwrap().push((img, None)),
}
});
let result = result_mutex.lock().unwrap().clone();
result
}
#[cfg(feature = "cli")]
pub async fn get_update(image: &str, socket: Option<String>) -> Option<bool> {
let local_image = get_image_from_docker_daemon(socket, image).await;
let token = match check_auth(&local_image.registry) {
Some(auth_url) => get_token(vec![&local_image], &auth_url),
None => String::new(),
};
let remote_image = match token.as_str() {
"" => get_latest_digest(&local_image, None),
_ => get_latest_digest(&local_image, Some(&token)),
};
match &remote_image.digest {
Some(d) => Some(d != &local_image.digest.unwrap()),
None => None,
}
}

View File

@@ -3,7 +3,7 @@ use std::time::Duration;
use indicatif::{ProgressBar, ProgressStyle};
use json::object;
use crate::utils::sort_update_vec;
use crate::utils::{sort_update_vec, to_json};
pub fn print_updates(updates: &[(String, Option<bool>)], icons: &bool) {
let sorted_updates = sort_update_vec(updates);
@@ -40,11 +40,7 @@ pub fn print_updates(updates: &[(String, Option<bool>)], icons: &bool) {
}
pub fn print_raw_updates(updates: &[(String, Option<bool>)]) {
let mut result = json::Array::new();
for update in updates {
result.push(object! {image: update.0.clone(), has_update: update.1});
}
println!("{}", json::stringify(result));
println!("{}", json::stringify(to_json(updates)));
}
pub fn print_update(name: &str, has_update: &Option<bool>) {
@@ -62,8 +58,8 @@ pub fn print_update(name: &str, has_update: &Option<bool>) {
}
pub fn print_raw_update(name: &str, has_update: &Option<bool>) {
let result = object!{image: name, has_update: *has_update};
println!("{}", json::stringify(result));
let result = object! {images: {[name]: *has_update}} ;
println!("{}", result);
}
pub struct Spinner {

File diff suppressed because one or more lines are too long

View File

@@ -1,36 +1,30 @@
use clap::{Parser, Subcommand};
#[cfg(feature = "cli")]
use docker::get_image_from_docker_daemon;
use docker::get_images_from_docker_daemon;
#[cfg(feature = "cli")]
use formatting::{print_raw_update, print_raw_updates, print_update, print_updates, Spinner};
use image::Image;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
#[cfg(feature = "cli")]
use registry::get_latest_digest;
use registry::{check_auth, get_latest_digests, get_token};
use check::{get_all_updates, get_update};
#[cfg(feature = "server")]
use server::serve;
use std::{
collections::{HashMap, HashSet},
sync::Mutex,
};
use utils::unsplit_image;
use std::path::PathBuf;
use utils::load_config;
pub mod check;
pub mod docker;
#[cfg(feature = "cli")]
pub mod formatting;
pub mod image;
pub mod registry;
pub mod utils;
#[cfg(feature = "cli")]
pub mod formatting;
#[cfg(feature = "server")]
pub mod server;
pub mod utils;
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
#[arg(short, long, default_value = None)]
socket: Option<String>,
#[arg(short, long, default_value_t = String::new(), help = "Config file path")]
config_path: String,
#[command(subcommand)]
command: Option<Commands>,
}
@@ -43,34 +37,34 @@ enum Commands {
image: Option<String>,
#[arg(short, long, default_value_t = false, help = "Enable icons")]
icons: bool,
#[arg(short, long, default_value_t = false, help = "Output JSON instead of formatted text")]
#[arg(
short,
long,
default_value_t = false,
help = "Output JSON instead of formatted text"
)]
raw: bool,
},
#[cfg(feature = "server")]
Serve {
#[arg(short, long, default_value_t = 8000, help = "Use a different port for the server")]
#[arg(
short,
long,
default_value_t = 8000,
help = "Use a different port for the server"
)]
port: u16,
},
}
pub trait Unique<T> {
// So we can filter vecs for duplicates
fn unique(&mut self);
}
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();
self.retain(|item| seen.insert(item.clone()));
}
}
#[tokio::main]
async fn main() {
let cli = Cli::parse();
let cfg_path = match cli.config_path.as_str() {
"" => None,
path => Some(PathBuf::from(path)),
};
let config = load_config(cfg_path);
match &cli.command {
#[cfg(feature = "cli")]
Some(Commands::Check { image, icons, raw }) => match image {
@@ -95,69 +89,8 @@ async fn main() {
},
#[cfg(feature = "server")]
Some(Commands::Serve { port }) => {
let updates = get_all_updates(cli.socket).await;
let _ = serve(port, &updates).await;
let _ = serve(port, cli.socket, config).await;
}
None => (),
}
}
async fn get_all_updates(socket: Option<String>) -> Vec<(String, Option<bool>)> {
let image_map_mutex: Mutex<HashMap<String, &Option<String>>> = Mutex::new(HashMap::new());
let local_images = get_images_from_docker_daemon(socket).await;
local_images.par_iter().for_each(|image| {
let img = unsplit_image(&image.registry, &image.repository, &image.tag);
image_map_mutex.lock().unwrap().insert(img, &image.digest);
});
let image_map = image_map_mutex.lock().unwrap().clone();
let mut registries: Vec<&String> = local_images
.par_iter()
.map(|image| &image.registry)
.collect();
registries.unique();
let mut remote_images: Vec<Image> = Vec::new();
for registry in registries {
let images: Vec<&Image> = local_images
.par_iter()
.filter(|image| &image.registry == registry)
.collect();
let mut latest_images = match check_auth(registry) {
Some(auth_url) => {
let token = get_token(images.clone(), &auth_url);
get_latest_digests(images, Some(&token))
}
None => get_latest_digests(images, None),
};
remote_images.append(&mut latest_images);
}
let result_mutex: Mutex<Vec<(String, Option<bool>)>> = Mutex::new(Vec::new());
remote_images.par_iter().for_each(|image| {
let img = unsplit_image(&image.registry, &image.repository, &image.tag);
match &image.digest {
Some(d) => {
let r = d != image_map.get(&img).unwrap().as_ref().unwrap();
result_mutex.lock().unwrap().push((img, Some(r)))
}
None => result_mutex.lock().unwrap().push((img, None)),
}
});
let result = result_mutex.lock().unwrap().clone();
result
}
#[cfg(feature = "cli")]
async fn get_update(image: &str, socket: Option<String>) -> Option<bool> {
let local_image = get_image_from_docker_daemon(socket, image).await;
let token = match check_auth(&local_image.registry) {
Some(auth_url) => get_token(vec![&local_image], &auth_url),
None => String::new(),
};
let remote_image = match token.as_str() {
"" => get_latest_digest(&local_image, None),
_ => get_latest_digest(&local_image, Some(&token)),
};
match &remote_image.digest {
Some(d) => Some(d != &local_image.digest.unwrap()),
None => None,
}
}

View File

@@ -1,5 +1,6 @@
use std::sync::Mutex;
use json::JsonValue;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use ureq::Error;
@@ -28,13 +29,13 @@ pub fn get_latest_digest(image: &Image, token: Option<&String>) -> Image {
request = request.set("Authorization", &format!("Bearer {}", t));
}
let raw_response = match request
.set("Accept", "application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.index.v1+json")
.set("Accept", "application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.index.v1+json")
.call()
{
Ok(response) => response,
Err(Error::Status(401, response)) => {
if token.is_some() {
error!("Failed to authenticate with given token!\n{}", token.unwrap())
error!("Failed to authenticate to registry {} with given token!\n{}", &image.registry, token.unwrap())
} else {
return get_latest_digest(
image,
@@ -81,7 +82,7 @@ pub fn get_token(images: Vec<&Image>, auth_url: &str) -> String {
final_url = format!("{}&scope=repository:{}:pull", final_url, image.repository);
}
let raw_response = match ureq::get(&final_url)
.set("Accept", "application/vnd.oci.image.index.v1+json")
.set("Accept", "application/vnd.oci.image.index.v1+json") // Seems to be unnecesarry. Will probably remove in the future
.call()
{
Ok(response) => match response.into_string() {
@@ -94,7 +95,7 @@ pub fn get_token(images: Vec<&Image>, auth_url: &str) -> String {
error!("Token request failed!\n{}", e)
}
};
let parsed_token_response = match json::parse(&raw_response) {
let parsed_token_response: JsonValue = match json::parse(&raw_response) {
Ok(parsed) => parsed,
Err(e) => {
error!("Failed to parse server response\n{}", e)

View File

@@ -1,90 +1,134 @@
use std::sync::Mutex;
use std::sync::Arc;
use chrono::Local;
use json::JsonValue;
use liquid::{object, Object};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use tokio::sync::Mutex;
use xitca_web::{
body::ResponseBody,
handler::{handler_service, path::PathOwn, state::StateOwn},
http::{Method, WebResponse},
handler::{handler_service, state::StateRef},
http::WebResponse,
middleware::Logger,
route::get,
App,
};
const RAW_TEMPLATE: &str = include_str!("template.liquid");
const STYLE: &str = include_str!("index.css");
use crate::{
check::get_all_updates,
error,
utils::{sort_update_vec, to_json},
};
pub async fn serve(port: &u16, updates: &[(String, Option<bool>)]) -> std::io::Result<()> {
println!("Serving on http://0.0.0.0:{}", port);
const RAW_TEMPLATE: &str = include_str!("static/template.liquid");
const STYLE: &str = include_str!("static/index.css");
const FAVICON_ICO: &[u8] = include_bytes!("static/favicon.ico");
const FAVICON_SVG: &[u8] = include_bytes!("static/favicon.svg");
const APPLE_TOUCH_ICON: &[u8] = include_bytes!("static/apple-touch-icon.png");
pub async fn serve(port: &u16, socket: Option<String>, config: JsonValue) -> std::io::Result<()> {
let mut data = ServerData::new(socket, config).await;
data.refresh().await;
App::new()
.with_state(updates.to_owned())
.with_state(Arc::new(Mutex::new(data)))
.at("/", get(handler_service(home)))
.at("/json", get(handler_service(json)))
.at("/refresh", get(handler_service(refresh)))
.at("/favicon.ico", handler_service(favicon_ico)) // These aren't pretty but this is xitca-web...
.at("/favicon.svg", handler_service(favicon_svg))
.at("/apple-touch-icon.png", handler_service(apple_touch_icon))
.enclosed(Logger::new())
.serve()
.bind(format!("0.0.0.0:{}", port))?
.run()
.wait()
}
async fn home(
updates: StateOwn<Vec<(String, Option<bool>)>>,
method: Method,
path: PathOwn,
) -> WebResponse {
let template = liquid::ParserBuilder::with_stdlib()
.build()
.unwrap()
.parse(RAW_TEMPLATE)
.unwrap();
let images = updates
.0
.par_iter()
.map(|(name, image)| match image {
Some(value) => {
if *value {
object!({"name": name, "status": "update-available"})
} else {
object!({"name": name, "status": "up-to-date"})
}
}
None => object!({"name": name, "status": "unknown"}),
})
.collect::<Vec<Object>>();
let uptodate = images
.par_iter()
.filter(|&o| o["status"] == "up-to-date")
.collect::<Vec<&Object>>()
.len();
let updatable = images
.par_iter()
.filter(|&o| o["status"] == "update-available")
.collect::<Vec<&Object>>()
.len();
let unknown = images
.par_iter()
.filter(|&o| o["status"] == "unknown")
.collect::<Vec<&Object>>()
.len();
let globals = object!({
"metrics": [{"name": "Monitored images", "value": images.len()}, {"name": "Up to date", "value": uptodate}, {"name": "Updates available", "value": updatable}, {"name": "Unknown", "value": unknown}],
"images": images,
"style": STYLE
});
let result = template.render(&globals).unwrap();
println!("Received {} request on {}", method, path.0);
WebResponse::new(ResponseBody::from(result))
async fn home(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
WebResponse::new(ResponseBody::from(data.lock().await.template.clone()))
}
async fn json(
updates: StateOwn<Vec<(String, Option<bool>)>>,
method: Method,
path: PathOwn,
) -> WebResponse {
let result_mutex: Mutex<json::object::Object> = Mutex::new(json::object::Object::new());
updates.par_iter().for_each(|image| match image.1 {
Some(b) => result_mutex.lock().unwrap().insert(&image.0, json::from(b)),
None => result_mutex.lock().unwrap().insert(&image.0, json::Null),
});
let result = json::stringify(result_mutex.lock().unwrap().clone());
println!("Received {} request on {}", method, path.0);
WebResponse::new(ResponseBody::from(result))
async fn json(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
WebResponse::new(ResponseBody::from(
json::stringify(data.lock().await.json.clone())
))
}
async fn refresh(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
data.lock().await.refresh().await;
WebResponse::new(ResponseBody::from("OK"))
}
async fn favicon_ico() -> WebResponse {
WebResponse::new(ResponseBody::from(FAVICON_ICO))
}
async fn favicon_svg() -> WebResponse {
WebResponse::new(ResponseBody::from(FAVICON_SVG))
}
async fn apple_touch_icon() -> WebResponse {
WebResponse::new(ResponseBody::from(APPLE_TOUCH_ICON))
}
struct ServerData {
template: String,
raw_updates: Vec<(String, Option<bool>)>,
json: JsonValue,
socket: Option<String>,
config: JsonValue,
}
impl ServerData {
async fn new(socket: Option<String>, config: JsonValue) -> Self {
let mut s = Self {
socket,
template: String::new(),
json: json::object! {
metrics: json::object! {},
images: json::object! {},
},
raw_updates: Vec::new(),
config,
};
s.refresh().await;
s
}
async fn refresh(&mut self) {
let updates = sort_update_vec(&get_all_updates(self.socket.clone()).await);
self.raw_updates = updates;
let template = liquid::ParserBuilder::with_stdlib()
.build()
.unwrap()
.parse(RAW_TEMPLATE)
.unwrap();
let images = self
.raw_updates
.iter()
.map(|(name, has_update)| match has_update {
Some(v) => object!({"name": name, "has_update": v.to_string()}), // Liquid kinda thinks false == nil, so we'll be comparing strings from now on
None => object!({"name": name, "has_update": "null"}),
})
.collect::<Vec<Object>>();
self.json = to_json(&self.raw_updates);
let last_updated = Local::now().format("%Y-%m-%d %H:%M:%S");
let theme = match &self.config["theme"].as_str() {
Some(t) => match *t {
"default" => "neutral",
"blue" => "gray",
_ => error!(
"Invalid theme {} specified! Please choose between 'default' and 'blue'",
t
),
},
None => "neutral",
};
let globals = object!({
"metrics": [{"name": "Monitored images", "value": self.json["metrics"]["monitored_images"].as_usize()}, {"name": "Up to date", "value": self.json["metrics"]["up_to_date"].as_usize()}, {"name": "Updates available", "value": self.json["metrics"]["update_available"].as_usize()}, {"name": "Unknown", "value": self.json["metrics"]["unknown"].as_usize()}],
"images": images,
"style": STYLE,
"last_updated": last_updated.to_string(),
"theme": theme
});
self.template = template.render(&globals).unwrap();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
src/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

29
src/static/favicon.svg Normal file
View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve">
<path style="fill:#A6CFD6;" d="M65.12,17.55c-17.6-0.53-34.75,5.6-34.83,14.36c-0.04,5.2,1.37,18.6,3.62,48.68s2.25,33.58,3.5,34.95
c1.25,1.37,10.02,8.8,25.75,8.8s25.93-6.43,26.93-8.05c0.48-0.78,1.83-17.89,3.5-37.07c1.81-20.84,3.91-43.9,3.99-45.06
C97.82,30.66,94.2,18.43,65.12,17.55z"/>
<path style="fill:#DCEDF6;" d="M41.4,45.29c-0.12,0.62,1.23,24.16,2.32,27.94c1.99,6.92,9.29,7.38,10.23,4.16
c0.9-3.07-0.38-29.29-0.38-29.29s-3.66-0.3-6.43-0.84C44,46.63,41.4,45.29,41.4,45.29z"/>
<path style="fill:#6CA4AE;" d="M33.74,32.61c-0.26,8.83,20.02,12.28,30.19,12.22c13.56-0.09,29.48-4.29,29.8-11.7
S79.53,21.1,63.35,21.1C49.6,21.1,33.96,25.19,33.74,32.61z"/>
<path style="fill:#DC0D27;" d="M84.85,13.1c-0.58,0.64-9.67,30.75-9.67,30.75s2.01-0.33,4-0.79c2.63-0.61,3.76-1.06,3.76-1.06
s7.19-22.19,7.64-23.09c0.45-0.9,21.61-7.61,22.31-7.93c0.7-0.32,1.39-0.4,1.46-0.78c0.06-0.38-2.34-6.73-3.11-6.73
C110.47,3.47,86.08,11.74,84.85,13.1z"/>
<path style="fill:#8A1F0F;" d="M110.55,7.79c1.04,2.73,2.8,3.09,3.55,2.77c0.45-0.19,1.25-1.84,0.01-4.47
c-0.99-2.09-2.17-2.74-2.93-2.61C110.42,3.6,109.69,5.53,110.55,7.79z"/>
<g>
<path style="fill:#8A1F0F;" d="M91.94,18.34c-0.22,0-0.44-0.11-0.58-0.3l-3.99-5.77c-0.22-0.32-0.14-0.75,0.18-0.97
c0.32-0.22,0.76-0.14,0.97,0.18l3.99,5.77c0.22,0.32,0.14,0.75-0.18,0.97C92.21,18.3,92.07,18.34,91.94,18.34z"/>
</g>
<g>
<path style="fill:#8A1F0F;" d="M90.28,19.43c-0.18,0-0.35-0.07-0.49-0.2l-5.26-5.12c-0.28-0.27-0.28-0.71-0.01-0.99
c0.27-0.28,0.71-0.28,0.99-0.01l5.26,5.12c0.28,0.27,0.28,0.71,0.01,0.99C90.64,19.36,90.46,19.43,90.28,19.43z"/>
</g>
<g>
<path style="fill:#8A1F0F;" d="M89.35,21.22c-0.12,0-0.25-0.03-0.36-0.1l-5.6-3.39c-0.33-0.2-0.44-0.63-0.24-0.96
c0.2-0.33,0.63-0.44,0.96-0.24l5.6,3.39c0.33,0.2,0.44,0.63,0.24,0.96C89.82,21.1,89.59,21.22,89.35,21.22z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

1
src/static/index.css Normal file

File diff suppressed because one or more lines are too long

292
src/static/template.liquid Normal file
View File

@@ -0,0 +1,292 @@
<!doctype html>
<html>
<head>
<meta charset="utf8">
<meta name="theme-color" content="#ffffff">
<link rel="icon" type="image/svg+xml" href="favicon.svg">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="apple-touch-icon" href="apple-touch-icon.png">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Cup</title>
<style>
{{ style }}
</style>
<style>
/* Kinda hacky, but thank you https://geary.co/internal-borders-css-grid/ */
.gi {
position: relative;
height: 100%;
}
.gi::before,
.gi::after {
content: '';
position: absolute;
z-index: 1;
{% if theme == "neutral" %}
background-color: #e5e5e5;
{% elsif theme == "gray" %}
background-color: #e5e7eb
{% endif %}
}
@media (prefers-color-scheme: dark) {
.gi::before,
.gi::after {
{% if theme == "neutral" %}
background-color: #262626;
{% elsif theme == "gray" %}
background-color: #1f2937
{% endif %}
}
}
.gi::before {
inline-size: 1px;
block-size: 100vh;
inset-inline-start: -0.125rem;
}
.gi::after {
inline-size: 100vw;
block-size: 1px;
inset-inline-start: 0;
inset-block-start: -0.12rem;
}
@supports (scrollbar-color: auto) {
html {
scrollbar-color: #707070 #343840;
}
}
@supports selector(::-webkit-scrollbar) {
html::-webkit-scrollbar {
width: 10px;
}
html::-webkit-scrollbar-track {
background: #343840;
}
html::-webkit-scrollbar-thumb {
background: #707070;
border-radius: 0.375rem;
}
html::-webkit-scrollbar-thumb:hover {
background: #b5b5b5;
}
}
</style>
<script>
function refresh(event) {
var button = event.currentTarget;
button.disabled = true;
let request = new XMLHttpRequest();
request.onload = function () {
if (request.status === 200) {
window.location.reload();
}
};
request.open('GET', `${window.location.origin}/refresh`);
request.send();
}
</script>
</head>
<body>
<div class="flex justify-center items-center min-h-screen bg-{{ theme }}-50 dark:bg-{{ theme }}-950">
<div class="lg:px-8 sm:px-6 px-4 max-w-[80rem] mx-auto h-full w-full">
<div class="max-w-[48rem] mx-auto h-full my-8">
<div class="flex items-center gap-1">
<h1 class="text-5xl lg:text-6xl font-bold dark:text-white">Cup</h1>
<svg
version="1.1"
id="Layer_2"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 128 128"
style="enable-background:new 0 0 128 128;"
xml:space="preserve"
class="size-16"
>
<path style="fill:#A6CFD6;" d="M65.12,17.55c-17.6-0.53-34.75,5.6-34.83,14.36c-0.04,5.2,1.37,18.6,3.62,48.68s2.25,33.58,3.5,34.95
c1.25,1.37,10.02,8.8,25.75,8.8s25.93-6.43,26.93-8.05c0.48-0.78,1.83-17.89,3.5-37.07c1.81-20.84,3.91-43.9,3.99-45.06
C97.82,30.66,94.2,18.43,65.12,17.55z"/>
<path style="fill:#DCEDF6;" d="M41.4,45.29c-0.12,0.62,1.23,24.16,2.32,27.94c1.99,6.92,9.29,7.38,10.23,4.16
c0.9-3.07-0.38-29.29-0.38-29.29s-3.66-0.3-6.43-0.84C44,46.63,41.4,45.29,41.4,45.29z"/>
<path style="fill:#6CA4AE;" d="M33.74,32.61c-0.26,8.83,20.02,12.28,30.19,12.22c13.56-0.09,29.48-4.29,29.8-11.7
S79.53,21.1,63.35,21.1C49.6,21.1,33.96,25.19,33.74,32.61z"/>
<path style="fill:#DC0D27;" d="M84.85,13.1c-0.58,0.64-9.67,30.75-9.67,30.75s2.01-0.33,4-0.79c2.63-0.61,3.76-1.06,3.76-1.06
s7.19-22.19,7.64-23.09c0.45-0.9,21.61-7.61,22.31-7.93c0.7-0.32,1.39-0.4,1.46-0.78c0.06-0.38-2.34-6.73-3.11-6.73
C110.47,3.47,86.08,11.74,84.85,13.1z"/>
<path style="fill:#8A1F0F;" d="M110.55,7.79c1.04,2.73,2.8,3.09,3.55,2.77c0.45-0.19,1.25-1.84,0.01-4.47
c-0.99-2.09-2.17-2.74-2.93-2.61C110.42,3.6,109.69,5.53,110.55,7.79z"/>
<g>
<path style="fill:#8A1F0F;" d="M91.94,18.34c-0.22,0-0.44-0.11-0.58-0.3l-3.99-5.77c-0.22-0.32-0.14-0.75,0.18-0.97
c0.32-0.22,0.76-0.14,0.97,0.18l3.99,5.77c0.22,0.32,0.14,0.75-0.18,0.97C92.21,18.3,92.07,18.34,91.94,18.34z"/>
</g>
<g>
<path style="fill:#8A1F0F;" d="M90.28,19.43c-0.18,0-0.35-0.07-0.49-0.2l-5.26-5.12c-0.28-0.27-0.28-0.71-0.01-0.99
c0.27-0.28,0.71-0.28,0.99-0.01l5.26,5.12c0.28,0.27,0.28,0.71,0.01,0.99C90.64,19.36,90.46,19.43,90.28,19.43z"/>
</g>
<g>
<path style="fill:#8A1F0F;" d="M89.35,21.22c-0.12,0-0.25-0.03-0.36-0.1l-5.6-3.39c-0.33-0.2-0.44-0.63-0.24-0.96
c0.2-0.33,0.63-0.44,0.96-0.24l5.6,3.39c0.33,0.2,0.44,0.63,0.24,0.96C89.82,21.1,89.59,21.22,89.35,21.22z"/>
</g>
</svg>
</div>
<div class="shadow-sm bg-white dark:bg-{{ theme }}-900 rounded-md my-8">
<dl class="lg:grid-cols-4 grid-cols-2 gap-1 grid overflow-hidden *:relative">
{% for metric in metrics %}
<div class="gi">
<div class="xl:px-8 px-6 py-4 gap-y-2 gap-x-4 justify-between align-baseline flex flex-col h-full">
<dt class="text-{{ theme }}-500 dark:text-{{ theme }}-400 leading-6 font-medium flex gap-1 justify-between">
{{ metric.name }}
{% if metric.name == 'Monitored images' %}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
class="size-6 text-black dark:text-white shrink-0"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 4c4.29 0 7.863 2.429 10.665 7.154l.22 .379l.045 .1l.03 .083l.014 .055l.014 .082l.011 .1v.11l-.014 .111a.992 .992 0 0 1 -.026 .11l-.039 .108l-.036 .075l-.016 .03c-2.764 4.836 -6.3 7.38 -10.555 7.499l-.313 .004c-4.396 0 -8.037 -2.549 -10.868 -7.504a1 1 0 0 1 0 -.992c2.831 -4.955 6.472 -7.504 10.868 -7.504zm0 5a3 3 0 1 0 0 6a3 3 0 0 0 0 -6z" />
</svg>
{% elsif metric.name == 'Up to date' %}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
class="size-6 text-green-500 shrink-0"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-1.293 5.953a1 1 0 0 0 -1.32 -.083l-.094 .083l-3.293 3.292l-1.293 -1.292l-.094 -.083a1 1 0 0 0 -1.403 1.403l.083 .094l2 2l.094 .083a1 1 0 0 0 1.226 0l.094 -.083l4 -4l.083 -.094a1 1 0 0 0 -.083 -1.32z" />
</svg>
{% elsif metric.name == 'Updates available' %}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
class="size-6 text-blue-500 shrink-0"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-4.98 3.66l-.163 .01l-.086 .016l-.142 .045l-.113 .054l-.07 .043l-.095 .071l-.058 .054l-4 4l-.083 .094a1 1 0 0 0 1.497 1.32l2.293 -2.293v5.586l.007 .117a1 1 0 0 0 1.993 -.117v-5.585l2.293 2.292l.094 .083a1 1 0 0 0 1.32 -1.497l-4 -4l-.082 -.073l-.089 -.064l-.113 -.062l-.081 -.034l-.113 -.034l-.112 -.02l-.098 -.006z" />
</svg>
{% elsif metric.name == 'Unknown' %}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
class="size-6 text-{{ theme }}-500 shrink-0"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1 -19.995 .324l-.005 -.324l.004 -.28c.148 -5.393 4.566 -9.72 9.996 -9.72zm0 13a1 1 0 0 0 -.993 .883l-.007 .117l.007 .127a1 1 0 0 0 1.986 0l.007 -.117l-.007 -.127a1 1 0 0 0 -.993 -.883zm1.368 -6.673a2.98 2.98 0 0 0 -3.631 .728a1 1 0 0 0 1.44 1.383l.171 -.18a.98 .98 0 0 1 1.11 -.15a1 1 0 0 1 -.34 1.886l-.232 .012a1 1 0 0 0 .111 1.994a3 3 0 0 0 1.371 -5.673z" />
</svg>
{% endif %}
</dt>
<dd class="text-black dark:text-white tracking-tight leading-10 font-medium text-3xl flex-none w-full">
{{ metric.value }}
</dd>
</div>
</div>
{% endfor %}
</dl>
</div>
<div class="shadow-sm bg-white dark:bg-{{ theme }}-900 rounded-md my-8">
<div class="flex justify-between items-center px-6 py-4 text-{{ theme }}-500">
<h3>Last checked: {{ last_updated }}</h3>
<button class="group" onclick="refresh(event)">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="group-disabled:animate-spin"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4,11A8.1,8.1 0 0 1 19.5,9M20,5v4h-4" /><path d="M20,13A8.1,8.1 0 0 1 4.5,15M4,19v-4h4" />
</svg>
</button>
</div>
<ul class="*:py-4 *:px-6 *:flex *:items-center *:gap-3 dark:divide-{{ theme }}-800 divide-y dark:text-white">
{% for image in images %}
<li>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M21 16.008v-8.018a1.98 1.98 0 0 0 -1 -1.717l-7 -4.008a2.016 2.016 0 0 0 -2 0l-7 4.008c-.619 .355 -1 1.01 -1 1.718v8.018c0 .709 .381 1.363 1 1.717l7 4.008a2.016 2.016 0 0 0 2 0l7 -4.008c.619 -.355 1 -1.01 1 -1.718z" />
<path d="M12 22v-10" />
<path d="M12 12l8.73 -5.04" />
<path d="M3.27 6.96l8.73 5.04" />
</svg>
{{ image.name }}
{% if image.has_update == 'false' %}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
class="text-green-500 ml-auto"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-1.293 5.953a1 1 0 0 0 -1.32 -.083l-.094 .083l-3.293 3.292l-1.293 -1.292l-.094 -.083a1 1 0 0 0 -1.403 1.403l.083 .094l2 2l.094 .083a1 1 0 0 0 1.226 0l.094 -.083l4 -4l.083 -.094a1 1 0 0 0 -.083 -1.32z" />
</svg>
{% elsif image.has_update == 'true' %}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
class="text-blue-500 ml-auto"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-4.98 3.66l-.163 .01l-.086 .016l-.142 .045l-.113 .054l-.07 .043l-.095 .071l-.058 .054l-4 4l-.083 .094a1 1 0 0 0 1.497 1.32l2.293 -2.293v5.586l.007 .117a1 1 0 0 0 1.993 -.117v-5.585l2.293 2.292l.094 .083a1 1 0 0 0 1.32 -1.497l-4 -4l-.082 -.073l-.089 -.064l-.113 -.062l-.081 -.034l-.113 -.034l-.112 -.02l-.098 -.006z" />
</svg>
{% elsif image.has_update == 'null' %}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
class="text-{{ theme }}-500 ml-auto"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1 -19.995 .324l-.005 -.324l.004 -.28c.148 -5.393 4.566 -9.72 9.996 -9.72zm0 13a1 1 0 0 0 -.993 .883l-.007 .117l.007 .127a1 1 0 0 0 1.986 0l.007 -.117l-.007 -.127a1 1 0 0 0 -.993 -.883zm1.368 -6.673a2.98 2.98 0 0 0 -3.631 .728a1 1 0 0 0 1.44 1.383l.171 -.18a.98 .98 0 0 1 1.11 -.15a1 1 0 0 1 -.34 1.886l-.232 .012a1 1 0 0 0 .111 1.994a3 3 0 0 0 1.371 -5.673z" />
</svg>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,129 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf8">
<style>
{{ style }}
</style>
<style>
/* Kinda hacky, but thank you https://geary.co/internal-borders-css-grid/ */
.gi {
position: relative;
height: 100%;
}
.gi::before,
.gi::after {
content: '';
position: absolute;
background-color: #e5e7eb;
z-index: 1;
}
@media (prefers-color-scheme: dark) {
.gi::before, .gi::after {
background-color: #1f2937;
}
}
.gi::before {
inline-size: 1px;
block-size: 100vh;
inset-inline-start: -0.125rem;
}
.gi::after {
inline-size: 100vw;
block-size: 1px;
inset-inline-start: 0;
inset-block-start: -0.12rem;
}
</style>
</head>
<body>
<div class="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-gray-950">
<div class="lg:px-8 sm:px-6 px-4 max-w-[80rem] mx-auto h-full w-full">
<div class="max-w-[48rem] mx-auto h-full my-8">
<h1 class="text-6xl font-bold dark:text-white">Cup🥤</h1>
<div class="shadow-sm bg-white dark:bg-gray-900 rounded-md my-8">
<dl class="lg:grid-cols-4 md:grid-cols-2 gap-1 grid-cols-1 grid overflow-hidden *:relative">
{% for metric in metrics %}
<div class="gi">
<div class="xl:px-8 px-6 py-4 gap-y-2 gap-x-4 justify-between align-baseline flex flex-col h-full">
<dt class="text-gray-500 dark:text-gray-400 leading-6 font-medium">{{ metric.name }}</dt>
<dd class="text-black dark:text-white tracking-tight leading-10 font-medium text-3xl flex-none w-full">
{{ metric.value }}
</dd>
</div>
</div>
{% endfor %}
</dl>
</div>
<div class="shadow-sm bg-white dark:bg-gray-900 rounded-md my-8">
<ul class="*:py-4 *:px-6 *:flex *:items-center *:gap-3 dark:divide-gray-800 divide-y dark:text-white">
{% for image in images %}
<li>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M21 16.008v-8.018a1.98 1.98 0 0 0 -1 -1.717l-7 -4.008a2.016 2.016 0 0 0 -2 0l-7 4.008c-.619 .355 -1 1.01 -1 1.718v8.018c0 .709 .381 1.363 1 1.717l7 4.008a2.016 2.016 0 0 0 2 0l7 -4.008c.619 -.355 1 -1.01 1 -1.718z" />
<path d="M12 22v-10" />
<path d="M12 12l8.73 -5.04" />
<path d="M3.27 6.96l8.73 5.04" />
</svg>
{{ image.name }}
{% if image.status == 'up-to-date' %}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
class="text-green-500 ml-auto"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-1.293 5.953a1 1 0 0 0 -1.32 -.083l-.094 .083l-3.293 3.292l-1.293 -1.292l-.094 -.083a1 1 0 0 0 -1.403 1.403l.083 .094l2 2l.094 .083a1 1 0 0 0 1.226 0l.094 -.083l4 -4l.083 -.094a1 1 0 0 0 -.083 -1.32z" />
</svg>
{% elsif image.status == 'update-available' %}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
class="text-blue-500 ml-auto"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-4.98 3.66l-.163 .01l-.086 .016l-.142 .045l-.113 .054l-.07 .043l-.095 .071l-.058 .054l-4 4l-.083 .094a1 1 0 0 0 1.497 1.32l2.293 -2.293v5.586l.007 .117a1 1 0 0 0 1.993 -.117v-5.585l2.293 2.292l.094 .083a1 1 0 0 0 1.32 -1.497l-4 -4l-.082 -.073l-.089 -.064l-.113 -.062l-.081 -.034l-.113 -.034l-.112 -.02l-.098 -.006z" />
</svg>
{% elsif image.status == 'unknown' %}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
class="text-gray-500 ml-auto"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1 -19.995 .324l-.005 -.324l.004 -.28c.148 -5.393 4.566 -9.72 9.996 -9.72zm0 13a1 1 0 0 0 -.993 .883l-.007 .117l.007 .127a1 1 0 0 0 1.986 0l.007 -.117l-.007 -.127a1 1 0 0 0 -.993 -.883zm1.368 -6.673a2.98 2.98 0 0 0 -3.631 .728a1 1 0 0 0 1.44 1.383l.171 -.18a.98 .98 0 0 1 1.11 -.15a1 1 0 0 1 -.34 1.886l-.232 .012a1 1 0 0 0 .111 1.994a3 3 0 0 0 1.371 -5.673z" />
</svg>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,6 +1,10 @@
use fancy_regex::Regex;
use once_cell::sync::Lazy;
use std::path::PathBuf;
use json::{object, JsonValue};
use once_cell::sync::Lazy;
use regex::Regex;
/// This macro is an alternative to panic. It prints the message you give it and exits the process with code 1, without printing a stack trace. Useful for when the program has to exit due to a user error or something unexpected which is unrelated to the program (e.g. a failed web request)
#[macro_export]
macro_rules! error {
($($arg:tt)*) => ({
@@ -9,27 +13,20 @@ macro_rules! error {
})
}
// Takes an image and splits it into registry, repository and tag. For example ghcr.io/sergi0g/cup:latest becomes ['ghcr.io', 'sergi0g/cup', 'latest']. ONLY REGISTRIES THAT USE A / IN THE REPOSITORY ARE SUPPORTED CURRENTLY. THAT MEANS AZURE WILL NOT WORK.
/// Takes an image and splits it into registry, repository and tag. For example ghcr.io/sergi0g/cup:latest becomes ['ghcr.io', 'sergi0g/cup', 'latest'].
pub fn split_image(image: &str) -> (String, String, String) {
static RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r#"^(?P<registry>[\w.\-_]+((?::\d+|)(?=/[a-z0-9._-]+/[a-z0-9._-]+))|)(?:/|)(?P<repository>[a-z0-9.\-_]+(?:/[a-z0-9.\-_]+|))(:(?P<tag>[\w.\-_]{1,127})|)$"#, // From https://regex101.com/r/a98UqN/1
r#"^(?P<name>(?:(?P<registry>(?:(?:localhost|[\w-]+(?:\.[\w-]+)+)(?::\d+)?)|[\w]+:\d+)/)?(?P<repository>[a-z0-9_.-]+(?:/[a-z0-9_.-]+)*))(?::(?P<tag>[\w][\w.-]{0,127}))?$"#, // From https://regex101.com/r/nmSDPA/1
)
.unwrap()
});
match RE.captures(image).unwrap() {
match RE.captures(image) {
Some(c) => {
return (
match c.name("registry") {
Some(registry) => {
let reg = registry.as_str().to_owned();
if reg.is_empty() {
String::from("registry-1.docker.io")
} else {
reg
}
}
None => error!("Failed to parse image {}", image),
Some(registry) => registry.as_str().to_owned(),
None => String::from("registry-1.docker.io"),
},
match c.name("repository") {
Some(repository) => {
@@ -52,26 +49,87 @@ pub fn split_image(image: &str) -> (String, String, String) {
}
}
/// Given an image's parts which were previously created by split_image, recreate a reference that docker would use. This means removing the registry part, if it's Docker Hub and removing "library" if the image is official
pub fn unsplit_image(registry: &str, repository: &str, tag: &str) -> String {
let reg = match registry {
"registry-1.docker.io" => String::new(),
r => format!("{}/", r),
};
let repo = match repository.split('/').collect::<Vec<&str>>()[0] {
"library" => repository.strip_prefix("library/").unwrap(),
"library" => {
if reg.is_empty() {
repository.strip_prefix("library/").unwrap()
} else {
repository
}
}
_ => repository,
};
format!("{}{}:{}", reg, repo, tag)
}
#[cfg(feature = "cli")]
/// Sorts the update vector alphabetically and where Some(true) > Some(false) > None
pub fn sort_update_vec(updates: &[(String, Option<bool>)]) -> Vec<(String, Option<bool>)> {
let mut sorted_updates = updates.to_vec();
sorted_updates.sort_unstable_by(|a, b| match (a.1, b.1) {
(Some(a), Some(b)) => (!a).cmp(&!b),
(Some(c), Some(d)) => {
if c == d {
a.0.cmp(&b.0)
} else {
(!c).cmp(&!d)
}
}
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => std::cmp::Ordering::Equal,
(None, None) => a.0.cmp(&b.0),
});
sorted_updates.to_vec()
}
/// Tries to load the config from the path provided and perform basic validation
pub fn load_config(config_path: Option<PathBuf>) -> JsonValue {
let raw_config = match &config_path {
Some(path) => std::fs::read_to_string(path),
None => Ok(String::from("{\"theme\":\"default\"}")),
};
if raw_config.is_err() {
panic!(
"Failed to read config file from {}. Are you sure the file exists?",
&config_path.unwrap().to_str().unwrap()
)
};
match json::parse(&raw_config.unwrap()) {
Ok(v) => v,
Err(e) => panic!("Failed to parse config!\n{}", e),
}
}
pub fn to_json(updates: &[(String, Option<bool>)]) -> JsonValue {
let mut json_data: JsonValue = object! {
metrics: object! {},
images: object! {}
};
updates.iter().for_each(|(image, has_update)| {
let _ = json_data["images"].insert(image, *has_update);
});
let up_to_date = updates
.iter()
.filter(|&(_, value)| *value == Some(false))
.collect::<Vec<&(String, Option<bool>)>>()
.len();
let update_available = updates
.iter()
.filter(|&(_, value)| *value == Some(true))
.collect::<Vec<&(String, Option<bool>)>>()
.len();
let unknown = updates
.iter()
.filter(|&(_, value)| value.is_none())
.collect::<Vec<&(String, Option<bool>)>>()
.len();
let _ = json_data["metrics"].insert("monitored_images", updates.len());
let _ = json_data["metrics"].insert("up_to_date", up_to_date);
let _ = json_data["metrics"].insert("update_available", update_available);
let _ = json_data["metrics"].insert("unknown", unknown);
json_data
}

View File

@@ -1,11 +1,17 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"src/template.liquid"
"src/static/template.liquid"
],
theme: {
extend: {},
},
plugins: [],
safelist: [
{
pattern: /(bg|text|divide)-(gray|neutral)-.+/,
variants: ["dark"]
}
]
}