Compare commits
95 Commits
547d418401
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d0da37e36 | ||
|
|
780d7a088d | ||
|
|
bcb9f63735 | ||
|
|
4d691dd5fa | ||
|
|
685219ea62 | ||
|
|
756462cd7c | ||
|
|
f020ac0906 | ||
|
|
4b03a48d88 | ||
|
|
ba1cfac64b | ||
|
|
05d4c7c630 | ||
|
|
cf22ec300f | ||
|
|
5b428dbf67 | ||
|
|
787a730ab5 | ||
|
|
925989fd80 | ||
|
|
5656003058 | ||
|
|
f79d7ff03a | ||
|
|
550fb955a3 | ||
|
|
6ae95bf83b | ||
|
|
2262df0355 | ||
|
|
1beb7dc020 | ||
|
|
a0de565367 | ||
|
|
0314ef2f05 | ||
|
|
f1c8a45122 | ||
|
|
ce3f8176f1 | ||
|
|
8b520182ed | ||
|
|
e8fee79d20 | ||
|
|
24f160803a | ||
|
|
2ef77c9a55 | ||
|
|
a5bbdd0e33 | ||
|
|
b5aa0309ee | ||
|
|
4bbb53cd67 | ||
|
|
3ac6fb57e9 | ||
|
|
ead74dadd6 | ||
|
|
6e6afdb757 | ||
|
|
0c10134829 | ||
|
|
c0c7f7c0e9 | ||
|
|
aeeffaccba | ||
|
|
a1711b7ac8 | ||
|
|
9d628e3ab2 | ||
|
|
d3b18a6587 | ||
|
|
76a812f52f | ||
|
|
fe779c9c4e | ||
|
|
84609d5189 | ||
|
|
ded441cf75 | ||
|
|
0a8295fff4 | ||
|
|
9c8e6ccdea | ||
|
|
f1e1bcbf1c | ||
|
|
31f7bfbbcb | ||
|
|
15eb553e50 | ||
|
|
359147770f | ||
|
|
0a4e302322 | ||
|
|
5ed64c92fd | ||
|
|
6d08d75ac3 | ||
|
|
dc38b84e87 | ||
|
|
09b6880295 | ||
|
|
4f1075b2b2 | ||
|
|
c84270603f | ||
|
|
4aa28f2cc5 | ||
|
|
eadda5f776 | ||
|
|
622b156eed | ||
|
|
dca19b5ae2 | ||
|
|
f6ac43aac0 | ||
|
|
e5e60c4abc | ||
|
|
33a72c8c0d | ||
|
|
e544ef6ca5 | ||
|
|
afc34a0847 | ||
|
|
ce08e00bb4 | ||
|
|
6a77b85141 | ||
|
|
215e88ae0f | ||
|
|
178acfb2f6 | ||
|
|
59894343de | ||
|
|
61bc60493f | ||
|
|
be7d55d126 | ||
|
|
36a3a13c04 | ||
|
|
d85fadfb39 | ||
|
|
0f95be26dc | ||
|
|
0b7e064980 | ||
|
|
9e9bb78db7 | ||
|
|
88d346b480 | ||
|
|
4519c534a1 | ||
|
|
6b83f51749 | ||
|
|
0c3f293fa8 | ||
|
|
d94abecf35 | ||
|
|
c11b5e6432 | ||
|
|
022dc0b2cb | ||
|
|
51609da4ff | ||
|
|
3ed79e69bd | ||
|
|
078a51c4fa | ||
|
|
8d70d7ae4d | ||
|
|
6d45409928 | ||
|
|
bcfb9ef27a | ||
|
|
5c4de36052 | ||
|
|
eda30229e2 | ||
|
|
8fd012efbe | ||
|
|
8ab073d562 |
3
.github/workflows/ci.yml
vendored
@@ -28,6 +28,9 @@ jobs:
|
||||
- name: Build
|
||||
run: ./build.sh cargo build --verbose
|
||||
|
||||
- name: Test
|
||||
run: cargo test
|
||||
|
||||
build-image:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
15
.github/workflows/docs.yml
vendored
@@ -13,22 +13,23 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: latest
|
||||
- name: Set up Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
run: bun install
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
run: bun run build
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: docs/out/
|
||||
deploy:
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
needs: build
|
||||
permissions:
|
||||
pages: write
|
||||
@@ -40,4 +41,4 @@ jobs:
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
2
.github/workflows/nightly.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
||||
- name: Install cross
|
||||
run: cargo install cross --git https://github.com/cross-rs/cross
|
||||
run: RUSTFLAGS="" cargo install cross --git https://github.com/cross-rs/cross
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
2
.github/workflows/release.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
||||
- name: Install cross
|
||||
run: cargo install cross --git https://github.com/cross-rs/cross
|
||||
run: RUSTFLAGS="" cargo install cross --git https://github.com/cross-rs/cross
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
5
.gitignore
vendored
@@ -5,4 +5,7 @@
|
||||
/src/static
|
||||
|
||||
# In case I accidentally commit mine...
|
||||
cup.json
|
||||
cup.json
|
||||
|
||||
# Profiling results don't need to be present in the repo
|
||||
profile.json
|
||||
161
Cargo.lock
generated
@@ -248,8 +248,10 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
@@ -327,6 +329,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "croner"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38fd53511eaf0b00a185613875fee58b208dfce016577d0ad4bb548e1c4fb3ee"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
@@ -339,15 +350,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cup"
|
||||
version = "2.4.0"
|
||||
version = "3.0.0"
|
||||
dependencies = [
|
||||
"bollard",
|
||||
"chrono",
|
||||
"clap",
|
||||
"futures",
|
||||
"http-auth",
|
||||
"http-link",
|
||||
"indicatif",
|
||||
"json",
|
||||
"itertools",
|
||||
"liquid",
|
||||
"once_cell",
|
||||
"regex",
|
||||
@@ -355,8 +367,11 @@ dependencies = [
|
||||
"reqwest-middleware",
|
||||
"reqwest-retry",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"termsize",
|
||||
"tokio",
|
||||
"tokio-cron-scheduler",
|
||||
"xitca-web",
|
||||
]
|
||||
|
||||
@@ -619,6 +634,16 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-link"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "500f1fc191bab8d956904c49818a167fd19534dbd529d93bd030bdc3bf9117a0"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.9.4"
|
||||
@@ -834,12 +859,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "json"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd"
|
||||
|
||||
[[package]]
|
||||
name = "kstring"
|
||||
version = "2.0.2"
|
||||
@@ -967,22 +986,23 @@ 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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -1023,12 +1043,6 @@ 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 = "parking_lot"
|
||||
version = "0.11.2"
|
||||
@@ -1160,9 +1174,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.86"
|
||||
version = "1.0.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -1461,18 +1475,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.210"
|
||||
version = "1.0.215"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
|
||||
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.210"
|
||||
version = "1.0.215"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
||||
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1481,9 +1495,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.128"
|
||||
version = "1.0.133"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
|
||||
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -1542,15 +1556,6 @@ 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"
|
||||
@@ -1611,9 +1616,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.68"
|
||||
version = "2.0.90"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9"
|
||||
checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1660,16 +1665,6 @@ 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"
|
||||
@@ -1734,6 +1729,21 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-cron-scheduler"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a5597b569b4712cf78aa0c9ae29742461b7bda1e49c2a5fdad1d79bf022f8f0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"croner",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.3.0"
|
||||
@@ -1825,32 +1835,6 @@ 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]]
|
||||
@@ -1900,9 +1884,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.13"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
@@ -1928,10 +1912,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
name = "uuid"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
@@ -2047,9 +2034,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.5"
|
||||
version = "0.26.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a"
|
||||
checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
@@ -2333,8 +2320,6 @@ dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"xitca-http",
|
||||
"xitca-server",
|
||||
"xitca-service",
|
||||
|
||||
18
Cargo.toml
@@ -1,30 +1,34 @@
|
||||
[package]
|
||||
name = "cup"
|
||||
version = "2.4.0"
|
||||
version = "3.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.7", features = ["derive"] }
|
||||
indicatif = { version = "0.17.8", optional = true }
|
||||
tokio = {version = "1.38.0", features = ["macros"]}
|
||||
xitca-web = { version = "0.5.0", optional = true, features = ["logger"] }
|
||||
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
|
||||
xitca-web = { version = "0.5.0", optional = true }
|
||||
liquid = { version = "0.26.6", optional = true }
|
||||
bollard = "0.16.1"
|
||||
once_cell = "1.19.0"
|
||||
http-auth = { version = "0.1.9", default-features = false, features = [] }
|
||||
http-auth = { version = "0.1.9", default-features = false }
|
||||
termsize = { version = "0.1.8", optional = true }
|
||||
regex = "1.10.5"
|
||||
regex = { version = "1.10.5", default-features = false, features = ["perf"] }
|
||||
chrono = { version = "0.4.38", default-features = false, features = ["std", "alloc", "clock"], optional = true }
|
||||
json = "0.12.4"
|
||||
reqwest = { version = "0.12.7", default-features = false, features = ["rustls-tls"] }
|
||||
futures = "0.3.30"
|
||||
reqwest-retry = "0.6.1"
|
||||
reqwest-middleware = "0.3.3"
|
||||
rustc-hash = "2.0.0"
|
||||
http-link = "1.0.1"
|
||||
itertools = "0.13.0"
|
||||
serde_json = "1.0.133"
|
||||
serde = "1.0.215"
|
||||
tokio-cron-scheduler = { version = "0.13.0", default-features = false, optional = true }
|
||||
|
||||
[features]
|
||||
default = ["server", "cli"]
|
||||
server = ["dep:xitca-web", "dep:liquid", "dep:chrono"]
|
||||
server = ["dep:xitca-web", "dep:liquid", "dep:chrono", "dep:tokio-cron-scheduler"]
|
||||
cli = ["dep:indicatif", "dep:termsize"]
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -17,7 +17,7 @@ _If you like this project and/or use Cup, please consider starring the project
|
||||
- Supports most registries, including Docker Hub, ghcr.io, Quay, lscr.io and even Gitea (or derivatives)
|
||||
- Doesn't exhaust any rate limits. This is the original reason I created Cup. It was inspired by [What's up docker?](https://github.com/getwud/wud) which would always use it up.
|
||||
- Beautiful CLI and web interface for checking on your containers any time.
|
||||
- The binary is tiny! At the time of writing it's just 5.2 MB. No more pulling 100+ MB docker images for a such a simple program.
|
||||
- The binary is tiny! At the time of writing it's just 5.1 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!
|
||||
|
||||
## Documentation 📘
|
||||
@@ -28,7 +28,6 @@ Take a look at https://sergi0g.github.io/cup/docs!
|
||||
|
||||
Cup is a work in progress. It might not have as many features as other alternatives. If one of these features is really important for you, please consider using another tool.
|
||||
|
||||
- Cup (currently) does not support semver.
|
||||
- Cup cannot directly 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
|
||||
|
||||
3
build.sh
@@ -17,6 +17,9 @@ bun run build
|
||||
# Copy UI to src folder
|
||||
cp -r dist/ ../src/static
|
||||
|
||||
# Go back
|
||||
cd ../
|
||||
|
||||
# Run command from argv
|
||||
|
||||
$@
|
||||
87
cup.schema.json
Normal file
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://raw.githubusercontent.com/sergi0g/cup/main/cup.schema.json",
|
||||
"title": "Cup",
|
||||
"description": "A schema for Cup's config file",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "integer",
|
||||
"minimum": 3,
|
||||
"maximum": 3
|
||||
},
|
||||
"agent": {
|
||||
"type": "boolean",
|
||||
"description": "Whether or not to enable agent mode. When agent mode is enabled, the server only exposes the API and the web interface is unavailable."
|
||||
},
|
||||
"images": {
|
||||
"type": "object",
|
||||
"description": "Configuration options for specific images",
|
||||
"properties": {
|
||||
"extra": {
|
||||
"type": "array",
|
||||
"description": "Extra image references you want Cup to check",
|
||||
"minItems": 1
|
||||
},
|
||||
"exclude": {
|
||||
"type": "array",
|
||||
"description": "Image references that should be excluded from the check",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"refresh_interval": {
|
||||
"type": "string",
|
||||
"description": "The interval at which Cup should check for updates. Must be a valid cron expression. Reference: https://github.com/Hexagon/croner-rust#pattern",
|
||||
"minLength": 11
|
||||
},
|
||||
"registries": {
|
||||
"type": "object",
|
||||
"description": "Configuration options for specific registries",
|
||||
"additionalProperties": {
|
||||
"authentication": {
|
||||
"description": "An authentication token provided by the registry",
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"insecure": {
|
||||
"description": "Whether Cup should connect to the registry insecurely (HTTP) or not. Enable this only if you really need to.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"ignore": {
|
||||
"description": "Whether or not the registry should be ignored when running Cup",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"socket": {
|
||||
"description": "The path to the unix socket you would like Cup to use for communication with the Docker daemon. Useful if you're trying to use Cup with Podman.",
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"servers": {
|
||||
"type": "object",
|
||||
"description": "Additional servers to connect to and fetch update data from",
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"minProperties": 1
|
||||
},
|
||||
"theme": {
|
||||
"description": "The theme used by the web UI",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"default",
|
||||
"blue"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"version"
|
||||
]
|
||||
}
|
||||
2
docs/.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
||||
.next
|
||||
.node_modules
|
||||
10
docs/.prettierrc
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"overrides": [
|
||||
{
|
||||
"files": "src/content/docs/integrations.mdx",
|
||||
"options": {
|
||||
"tabWidth": 4
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
nodejs 22.8.0
|
||||
@@ -1 +1,37 @@
|
||||
This is where Cup's documentation lives. It's created with [Nextra](https://nextra.site).
|
||||
# Cup Documentation
|
||||
|
||||
## Architecture
|
||||
|
||||
The docs are built with [Nextra](https://nextra.site). We use [Bun](https://bun.sh) as a package manager and Node.js as a runtime (Next.js and Bun don't play well together at the moment). Docs pages are written in [MDX](https://mdxjs.com) and any custom components are written in TypeScript with TSX.
|
||||
|
||||
## Development
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- A recent Node.js version (22 recommended)
|
||||
- [Bun](https://bun.sh)
|
||||
|
||||
```bash
|
||||
git clone https://github.com/sergi0g/cup
|
||||
cd cup/docs
|
||||
bun install
|
||||
```
|
||||
|
||||
You're ready to go!
|
||||
|
||||
## Scripts
|
||||
|
||||
The available scripts are:
|
||||
|
||||
- `bun dev` starts the development server. Note that making changes to MDX pages will probably require a full reload.
|
||||
- `bun run build` creates a static production build, ready to be deployed.
|
||||
- `bun lint` checks for errors in your code.
|
||||
- `bun fmt` formats your code with Prettier, so it becomes... prettier.
|
||||
|
||||
## Contributing
|
||||
|
||||
Our documentation is always evolving, so, we constantly need to update this repository with new guides and configuration options. If you have any ideas of a guide or suggestions on how to improve them, feel free to open a pull request or create an issue. All contributions are welcome!
|
||||
|
||||
## License
|
||||
|
||||
The documentation is licensed under the MIT License. TL;DR — You are free to use, copy, modify, merge, publish, distribute, sublicense, and sell copies of the software. However, the software is provided "as is," without warranty of any kind. You must include the original license in all copies or substantial portions of the software.
|
||||
|
||||
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 64 KiB |
BIN
docs/bun.lockb
Executable file
21
docs/eslint.config.mjs
Normal file
@@ -0,0 +1,21 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
{
|
||||
rules: {
|
||||
"import/no-anonymous-default-export": "off",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
5
docs/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -1,17 +0,0 @@
|
||||
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/next.config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import nextra from "nextra";
|
||||
|
||||
const withNextra = nextra({
|
||||
defaultShowCopyCode: true,
|
||||
});
|
||||
|
||||
export default withNextra({
|
||||
output: "export",
|
||||
transpilePackages: ["geist"],
|
||||
images: {
|
||||
unoptimized: true,
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "raw.githubusercontent.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
basePath: "",
|
||||
});
|
||||
@@ -1,21 +1,36 @@
|
||||
{
|
||||
"name": "cup-docs",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next",
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"fmt": "bun prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@tabler/icons-react": "^3.11.0",
|
||||
"next": "^14.2.10",
|
||||
"nextra": "^2.13.4",
|
||||
"nextra-theme-docs": "^2.13.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
"@tabler/icons-react": "^3.29.0",
|
||||
"geist": "^1.3.1",
|
||||
"next": "15.1.5",
|
||||
"nextra": "^4.1.0",
|
||||
"nextra-theme-docs": "^4.1.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.39",
|
||||
"tailwindcss": "^3.4.5"
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@tailwindcss/postcss": "^4.0.1",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/react": "^19.0.7",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-next": "15.1.5",
|
||||
"postcss": "^8.5.1",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"tailwindcss": "^4.0.1",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"packageManager": "pnpm@9.10.0+sha512.73a29afa36a0d092ece5271de5177ecbf8318d454ecd701343131b8ebc0c1a91c487da46ab77c8e596d6acf1461e3594ced4becedf8921b074fbd8653ed7051c"
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import '../styles.css';
|
||||
import 'nextra-theme-docs/style.css';
|
||||
|
||||
export default function App({ Component, pageProps }) {
|
||||
return (
|
||||
<main>
|
||||
<Component {...pageProps} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"docs": {
|
||||
"title": "Documentation",
|
||||
"type": "page"
|
||||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
"type": "page",
|
||||
"theme": {
|
||||
"typesetting": "article"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
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/getwud/wud) (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/getwud/wud/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!
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"index": {
|
||||
"title": "Introduction"
|
||||
},
|
||||
"installation": {
|
||||
"title": "Installation"
|
||||
},
|
||||
"configuration": {
|
||||
"title": "Configuration"
|
||||
},
|
||||
"usage": {
|
||||
"title": "Usage"
|
||||
},
|
||||
"community-resources": {
|
||||
"title": "Community Resources"
|
||||
},
|
||||
"nightly": {
|
||||
"title": "Using the latest version"
|
||||
},
|
||||
"contributing": {
|
||||
"title": "Contributing"
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Callout } from 'nextra-theme-docs'
|
||||
|
||||
# Authentication
|
||||
|
||||
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>
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Callout } from 'nextra-theme-docs'
|
||||
|
||||
# Insecure registries
|
||||
|
||||
For the best security, Cup only connects to registries over SSL (HTTPS) by default. However, for people running a local registry that haven't configured SSL, this may be a problem.
|
||||
|
||||
To solve this problem, `cup.json` has an `"insecure_registries"` option which allows you to specify exceptions
|
||||
|
||||
Here's what it looks like:
|
||||
|
||||
```json
|
||||
{
|
||||
"insecure_registries": ["<INSECURE_REGISTRY_1>", "<INSECURE_REGISTRY_2>"],
|
||||
// Other options
|
||||
}
|
||||
```
|
||||
|
||||
<Callout emoji="⚠️">
|
||||
When configuring an insecure registry that doesn't run on port 80, don't forget to specify it (i.e. use `localhost:5000` instead of `localhost` if your registry is running on port `5000`)
|
||||
</Callout>
|
||||
@@ -1,10 +0,0 @@
|
||||
# Socket
|
||||
|
||||
If you need to specify a custom Docker socket (e.g. because you're using Podman), you can use the `socket` option. Here's an example:
|
||||
|
||||
```json
|
||||
{
|
||||
"socket": "/run/user/1000/podman/podman.sock"
|
||||
// Other options
|
||||
}
|
||||
```
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Callout } from "nextra-theme-docs";
|
||||
import Image from "next/image";
|
||||
|
||||
import blue from "../../../assets/blue_theme.png";
|
||||
import gray from "../../../assets/gray_theme.png";
|
||||
|
||||
# Theme
|
||||
|
||||
<Callout emoji="⚠️">
|
||||
This configuration option is only for the server
|
||||
</Callout>
|
||||
|
||||
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
|
||||
}
|
||||
```
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"docker": {
|
||||
"title": "With Docker"
|
||||
},
|
||||
"binary": {
|
||||
"title": "As a binary"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
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>
|
||||
@@ -1,83 +0,0 @@
|
||||
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
|
||||
[32mnginx:alpine Update available
|
||||
redis:7 Update available
|
||||
redis:alpine Update available
|
||||
[0m...
|
||||
[34mcentos: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
|
||||
[0m...
|
||||
[90msome/deleted:image Unknown
|
||||
[38:5:86mINFO ✨ Checked 58 images in 3772ms
|
||||
```
|
||||
|
||||
### Check for updates to specific images
|
||||
```ansi
|
||||
$ cup check node:latest
|
||||
[32mnode:latest Update available
|
||||
[38:5:86mINFO ✨ Checked 1 images in 1310ms
|
||||
```
|
||||
|
||||
```ansi
|
||||
$ cup check node:latest
|
||||
[32mnextcloud:30 Update available
|
||||
postgres:14 Update available
|
||||
[34mmysql:8.0 Up to date
|
||||
[38:5:86mINFO ✨ Checked 3 images in 1769ms
|
||||
```
|
||||
|
||||
## 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
|
||||
interface 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
|
||||
```
|
||||
@@ -1,53 +0,0 @@
|
||||
import { Callout } from "nextra-theme-docs";
|
||||
|
||||
# Server
|
||||
|
||||
The server provides the `cup serve` command.
|
||||
|
||||
## Basic usage
|
||||
|
||||
```ansi
|
||||
$ cup serve
|
||||
[2m2024-07-17T09:08:38.724922Z [0m [32m INFO [0m [2mxitca_server::net [0m [2m: [0m Started Tcp listening on: Some(0.0.0.0:8000)
|
||||
[2m2024-07-17T09:08:38.725076Z [0m [33m WARN [0m [2mxitca_server::server::future [0m [2m: [0m ServerFuture::wait is called from within tokio context. It would block current thread from handling async tasks
|
||||
[2m2024-07-17T09:08:38.725248Z [0m [32m INFO [0m [2mxitca_server::worker [0m [2m: [0m Started xitca-server-worker-0
|
||||
[2m2024-07-17T09:08:38.725343Z [0m [32m INFO [0m [2mxitca_server::worker [0m [2m: [0m Started xitca-server-worker-1
|
||||
[2m2024-07-17T09:08:38.725580Z [0m [32m INFO [0m [2mxitca_server::worker [0m [2m: [0m Started xitca-server-worker-2
|
||||
[2m2024-07-17T09:08:38.725607Z [0m [32m INFO [0m [2mxitca_server::worker [0m [2m: [0m Started xitca-server-worker-3
|
||||
[2m2024-07-17T09:08:41.390783Z [0m [32m INFO [0m [1mrequest [0m [1m{ [0m [3mmethod [0m [2m= [0mGET [3muri [0m [2m= [0m/ [1m} [0m [2m: [0m [2mon_request [0m [2m: [0m serving request
|
||||
[2m2024-07-17T09:08:41.390905Z [0m [32m INFO [0m [1mrequest [0m [1m{ [0m [3mmethod [0m [2m= [0mGET [3muri [0m [2m= [0m/ [1m} [0m [2m: [0m [2mon_response [0m [2m: [0m 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
|
||||
[2m2024-07-17T09:08:38.724922Z [0m [32m INFO [0m [2mxitca_server::net [0m [2m: [0m Started Tcp listening on: Some(0.0.0.0:9000)
|
||||
[2m2024-07-17T09:08:38.725076Z [0m [33m WARN [0m [2mxitca_server::server::future [0m [2m: [0m ServerFuture::wait is called from within tokio context. It would block current thread from handling async tasks
|
||||
[2m2024-07-17T09:08:38.725248Z [0m [32m INFO [0m [2mxitca_server::worker [0m [2m: [0m Started xitca-server-worker-0
|
||||
[2m2024-07-17T09:08:38.725343Z [0m [32m INFO [0m [2mxitca_server::worker [0m [2m: [0m Started xitca-server-worker-1
|
||||
[2m2024-07-17T09:08:38.725580Z [0m [32m INFO [0m [2mxitca_server::worker [0m [2m: [0m Started xitca-server-worker-2
|
||||
[2m2024-07-17T09:08:38.725607Z [0m [32m INFO [0m [2mxitca_server::worker [0m [2m: [0m Started xitca-server-worker-3
|
||||
[2m2024-07-17T09:08:41.390783Z [0m [32m INFO [0m [1mrequest [0m [1m{ [0m [3mmethod [0m [2m= [0mGET [3muri [0m [2m= [0m/ [1m} [0m [2m: [0m [2mon_request [0m [2m: [0m serving request
|
||||
[2m2024-07-17T09:08:41.390905Z [0m [32m INFO [0m [1mrequest [0m [1m{ [0m [3mmethod [0m [2m= [0mGET [3muri [0m [2m= [0m/ [1m} [0m [2m: [0m [2mon_response [0m [2m: [0m 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
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
8
docs/postcss.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
BIN
docs/public/cup-og.png
Normal file
|
After Width: | Height: | Size: 136 KiB |
29
docs/public/favicon.svg
Normal 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 |
27
docs/src/app/[...mdxPath]/page.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { generateStaticParamsFor, importPage } from "nextra/pages";
|
||||
import { useMDXComponents } from "@/mdx-components";
|
||||
|
||||
export const generateStaticParams = generateStaticParamsFor("mdxPath");
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ mdxPath: string[] }>;
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: Props) {
|
||||
const params = await props.params;
|
||||
const { metadata } = await importPage(params.mdxPath);
|
||||
return metadata;
|
||||
}
|
||||
/* eslint-disable-next-line */
|
||||
const Wrapper = useMDXComponents({}).wrapper;
|
||||
|
||||
export default async function Page(props: Props) {
|
||||
const params = await props.params;
|
||||
const result = await importPage(params.mdxPath);
|
||||
const { default: MDXContent, toc, metadata } = result;
|
||||
return (
|
||||
<Wrapper toc={toc} metadata={metadata}>
|
||||
<MDXContent {...props} params={params} />
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
BIN
docs/src/app/apple-icon.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
15
docs/src/app/assets/GitHubIcon.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
|
||||
export function GitHubIcon({ className }: { className?: string | undefined }) {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
viewBox="3 3 18 18"
|
||||
className={className}
|
||||
>
|
||||
<path d="M12 3C7.0275 3 3 7.12937 3 12.2276C3 16.3109 5.57625 19.7597 9.15374 20.9824C9.60374 21.0631 9.77249 20.7863 9.77249 20.5441C9.77249 20.3249 9.76125 19.5982 9.76125 18.8254C7.5 19.2522 6.915 18.2602 6.735 17.7412C6.63375 17.4759 6.19499 16.6569 5.8125 16.4378C5.4975 16.2647 5.0475 15.838 5.80124 15.8264C6.51 15.8149 7.01625 16.4954 7.18499 16.7723C7.99499 18.1679 9.28875 17.7758 9.80625 17.5335C9.885 16.9337 10.1212 16.53 10.38 16.2993C8.3775 16.0687 6.285 15.2728 6.285 11.7432C6.285 10.7397 6.63375 9.9092 7.20749 9.26326C7.1175 9.03257 6.8025 8.08674 7.2975 6.81794C7.2975 6.81794 8.05125 6.57571 9.77249 7.76377C10.4925 7.55615 11.2575 7.45234 12.0225 7.45234C12.7875 7.45234 13.5525 7.55615 14.2725 7.76377C15.9937 6.56418 16.7475 6.81794 16.7475 6.81794C17.2424 8.08674 16.9275 9.03257 16.8375 9.26326C17.4113 9.9092 17.76 10.7281 17.76 11.7432C17.76 15.2843 15.6563 16.0687 13.6537 16.2993C13.98 16.5877 14.2613 17.1414 14.2613 18.0065C14.2613 19.2407 14.25 20.2326 14.25 20.5441C14.25 20.7863 14.4188 21.0746 14.8688 20.9824C16.6554 20.364 18.2079 19.1866 19.3078 17.6162C20.4077 16.0457 20.9995 14.1611 21 12.2276C21 7.12937 16.9725 3 12 3Z"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
BIN
docs/src/app/assets/blue_theme.png
Normal file
|
After Width: | Height: | Size: 242 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 136 KiB |
BIN
docs/src/app/assets/hero-dark.png
Normal file
|
After Width: | Height: | Size: 232 KiB |
BIN
docs/src/app/assets/hero-mobile-dark.png
Normal file
|
After Width: | Height: | Size: 240 KiB |
BIN
docs/src/app/assets/hero-mobile.png
Normal file
|
After Width: | Height: | Size: 242 KiB |
BIN
docs/src/app/assets/hero.png
Normal file
|
After Width: | Height: | Size: 233 KiB |
252
docs/src/app/components/Browser.tsx
Normal file
23
docs/src/app/components/Card.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Icon as IconType } from "@tabler/icons-react";
|
||||
|
||||
export function Card({
|
||||
name,
|
||||
icon: Icon,
|
||||
description,
|
||||
}: {
|
||||
name: string;
|
||||
icon: IconType;
|
||||
description: string;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<Icon className="text-black size-7 dark:text-white inline mr-2" />
|
||||
<span className="align-middle text-2xl font-bold text-black dark:text-white">
|
||||
{name}
|
||||
</span>
|
||||
<p className="text-2xl font-semibold text-neutral-500 dark:text-neutral-500">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
28
docs/src/app/components/CopyableCode.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { IconCopy, IconCopyCheck } from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function CopyableCode({ children }: { children: string }) {
|
||||
const [success, setSuccess] = useState(false);
|
||||
const handleClick = () => {
|
||||
navigator.clipboard.writeText(children);
|
||||
setSuccess(true);
|
||||
setTimeout(() => setSuccess(false), 3000);
|
||||
};
|
||||
return (
|
||||
<div className="relative rounded-md xl:w-auto">
|
||||
<button
|
||||
className="hover:bg-black/10 dark:hover:bg-black/60 flex w-full items-center justify-center gap-4 rounded-md border border-black/10 bg-black/5 px-8 py-3 font-mono text-sm font-medium text-black/70 transition-colors duration-200 md:px-10 md:py-3 md:text-base md:leading-6 dark:border-white/15 dark:bg-black dark:text-gray-300 backdrop-blur-md"
|
||||
onClick={handleClick}
|
||||
>
|
||||
{children}
|
||||
{success ? (
|
||||
<IconCopyCheck className="stroke-black/40 dark:stroke-white/50" />
|
||||
) : (
|
||||
<IconCopy className="stroke-black/40 dark:stroke-white/50" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
33
docs/src/app/components/GradientText.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from "react";
|
||||
import { clsx } from "clsx";
|
||||
|
||||
export function GradientText({
|
||||
text,
|
||||
innerClassName,
|
||||
className,
|
||||
blur,
|
||||
}: {
|
||||
text: string;
|
||||
innerClassName: string;
|
||||
className?: string;
|
||||
blur: number;
|
||||
}) {
|
||||
return (
|
||||
<div className={clsx("relative", className)}>
|
||||
<p
|
||||
className={clsx("bg-clip-text text-transparent w-fit", innerClassName)}
|
||||
>
|
||||
{text}
|
||||
</p>
|
||||
<p
|
||||
className={clsx(
|
||||
"pointer-events-none absolute top-0 hidden select-none bg-clip-text text-transparent dark:block",
|
||||
innerClassName,
|
||||
)}
|
||||
style={{ filter: `blur(${blur}px)` }}
|
||||
>
|
||||
{text}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
docs/src/app/components/GridPattern.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useId } from "react";
|
||||
|
||||
const SIZE = 36;
|
||||
|
||||
export function GridPattern() {
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 bottom-0 left-0 right-0 top-0 -z-10 h-full w-full stroke-neutral-200 dark:stroke-neutral-600/30"
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id={id}
|
||||
width={SIZE}
|
||||
height={SIZE}
|
||||
patternUnits="userSpaceOnUse"
|
||||
x={-1}
|
||||
y={-1}
|
||||
>
|
||||
<path
|
||||
d={`M.5 ${SIZE}V.5H${SIZE}`}
|
||||
fill="none"
|
||||
strokeDasharray={"4 2"}
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
29
docs/src/app/components/Head.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { Head as NextraHead } from "nextra/components";
|
||||
|
||||
export function Head() {
|
||||
return (
|
||||
<NextraHead>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<meta
|
||||
name="theme-color"
|
||||
media="(prefers-color-scheme: light)"
|
||||
content="#ffffff"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
content="#111111"
|
||||
/>
|
||||
<meta
|
||||
name="og:image"
|
||||
content="https://raw.githubusercontent.com/sergi0g/cup/main/docs/public/cup-og.png"
|
||||
/>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="https://cup.sergi0g.dev" />
|
||||
<meta name="apple-mobile-web-app-title" content="Cup" />
|
||||
</NextraHead>
|
||||
);
|
||||
}
|
||||
57
docs/src/app/components/Logo.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
export default 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>
|
||||
);
|
||||
}
|
||||
28
docs/src/app/components/Section.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { ReactNode } from "react";
|
||||
import { GradientText } from "./GradientText";
|
||||
|
||||
export function Section({
|
||||
title,
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
className: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="border-t border-t-neutral-300 bg-neutral-50 py-32 dark:border-t-neutral-600/30 dark:bg-neutral-950">
|
||||
<div className="mx-auto w-full max-w-screen-xl">
|
||||
<GradientText
|
||||
text={title}
|
||||
className="mx-auto mb-20 w-fit text-center text-4xl font-bold tracking-tighter"
|
||||
innerClassName={className}
|
||||
blur={12}
|
||||
/>
|
||||
<div className="m-2 grid w-full auto-cols-fr gap-20 lg:grid-cols-3">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
136
docs/src/app/components/pages/home.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import React from "react";
|
||||
import "./styles.css"
|
||||
|
||||
import CopyableCode from "../CopyableCode";
|
||||
import { Browser } from "../Browser";
|
||||
import { Card } from "../Card";
|
||||
import {
|
||||
IconAdjustments,
|
||||
IconArrowRight,
|
||||
IconBolt,
|
||||
IconBraces,
|
||||
IconDevices,
|
||||
IconFeather,
|
||||
IconLockCheck,
|
||||
} from "@tabler/icons-react";
|
||||
import { GitHubIcon } from "nextra/icons";
|
||||
import { GridPattern } from "../GridPattern";
|
||||
import { GradientText } from "../GradientText";
|
||||
import { Section } from "../Section";
|
||||
import { Steps } from "nextra/components";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function Home() {
|
||||
return (
|
||||
<>
|
||||
<div className="relative home">
|
||||
<GridPattern />
|
||||
<div className="px-4 pt-16 pb-8 sm:pt-24 lg:px-8">
|
||||
<div className="flex w-full flex-col items-center justify-between">
|
||||
<div>
|
||||
<h1 className="mx-auto max-w-2xl text-center text-6xl leading-none font-extrabold tracking-tighter text-black sm:text-7xl dark:text-white">
|
||||
The easiest way to manage your
|
||||
<GradientText
|
||||
text="container updates."
|
||||
className="mx-auto w-fit"
|
||||
innerClassName="bg-linear-to-r/oklch from-blue-500 to-green-500"
|
||||
blur={30}
|
||||
/>
|
||||
</h1>
|
||||
<h3 className="mx-auto mt-6 max-w-3xl text-center text-xl leading-tight font-medium text-gray-400">
|
||||
Cup is a small utility with a big impact. Simplify your
|
||||
container management workflow with fast and efficient update
|
||||
checking, a full-featured CLI and web interface, and more.
|
||||
</h3>
|
||||
</div>
|
||||
<div className="mt-8 grid w-fit grid-cols-2 gap-4 *:flex *:items-center *:gap-2 *:rounded-lg *:px-3 *:py-2">
|
||||
<Link
|
||||
href="/docs"
|
||||
className="hide-focus group h-full bg-black text-white dark:bg-white dark:text-black"
|
||||
>
|
||||
Get started
|
||||
<IconArrowRight className="ml-auto mr-1 transition-transform duration-300 ease-out group-hover:translate-x-1 group-focus:translate-x-1 dark:!text-black" />
|
||||
</Link>
|
||||
<a
|
||||
href="https://github.com/sergi0g/cup"
|
||||
target="_blank"
|
||||
className="hide-focus h-full text-nowrap border border-neutral-400 transition-colors duration-200 ease-in-out hover:border-neutral-600 focus:border-neutral-600 dark:border-neutral-600 hover:dark:border-neutral-400 hover:dark:shadow-sm hover:dark:shadow-neutral-600 focus:dark:border-neutral-400"
|
||||
>
|
||||
Star on GitHub
|
||||
<GitHubIcon className="ml-auto size-4 md:size-5" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="py-10 flex translate-y-32 justify-center" id="hero">
|
||||
<Browser />
|
||||
</div>
|
||||
</div>
|
||||
<Section
|
||||
title="Powerful at its core."
|
||||
className="bg-gradient-to-r from-red-500 to-amber-500"
|
||||
>
|
||||
<Card
|
||||
name="100% Safe Code"
|
||||
icon={IconLockCheck}
|
||||
description="Built with safe Rust and Typescript to ensure security and reliability."
|
||||
/>
|
||||
<Card
|
||||
name="Lightning Fast Performance"
|
||||
icon={IconBolt}
|
||||
description="Heavily optimized to squeeze out every last drop of performance. Each release is extensively benchmarked and profiled so that you'll never have to stare at a loading spinner for long."
|
||||
/>
|
||||
<Card
|
||||
name="Lightweight"
|
||||
icon={IconFeather}
|
||||
description="No runtimes or libraries are needed. All you need is the 5.1 MB static binary that works out of the box on any system."
|
||||
/>
|
||||
</Section>
|
||||
<Section
|
||||
title="Efficient, yet flexible."
|
||||
className="bg-gradient-to-r from-blue-500 to-indigo-500"
|
||||
>
|
||||
<Card
|
||||
name="JSON output"
|
||||
description="Connect Cup to your favorite intergrations with JSON output for the CLI and an API for the server. Now go make that cool dashboard you've been dreaming of!"
|
||||
icon={IconBraces}
|
||||
/>
|
||||
<Card
|
||||
name="Both CLI and web interface"
|
||||
description="Whether you prefer the command line or the web, Cup runs wherever you choose."
|
||||
icon={IconDevices}
|
||||
/>
|
||||
<Card
|
||||
name="Configurable"
|
||||
description="The simple configuration file provides you with all the tools you need to specify a custom Docker socket, manage registry connection options, choose a theme for the web interface and more."
|
||||
icon={IconAdjustments}
|
||||
/>
|
||||
</Section>
|
||||
<div className="relative py-24 border-t border-t-neutral-300 dark:border-t-neutral-600/30 text-black dark:text-white">
|
||||
<GridPattern />
|
||||
<div className="mx-auto flex w-full max-w-screen-xl flex-col items-center">
|
||||
<p className="mb-8 text-center text-3xl font-bold">
|
||||
Still not convinced? Try it out now!
|
||||
</p>
|
||||
<div>
|
||||
<Steps>
|
||||
<h3 className="mb-2">Open a terminal and run</h3>
|
||||
<CopyableCode>
|
||||
docker run --rm -t -v /var/run/docker.sock:/var/run/docker.sock
|
||||
-p 8000:8000 ghcr.io/sergi0g/cup serve
|
||||
</CopyableCode>
|
||||
<h3 className="mb-2">Open the dashboard in your browser</h3>
|
||||
<p>
|
||||
Visit{" "}
|
||||
<a href="http://localhost:8000" className="underline">
|
||||
http://localhost:8000
|
||||
</a>{" "}
|
||||
in your browser to try it out!
|
||||
</p>
|
||||
</Steps>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
26
docs/src/app/components/pages/styles.css
Normal file
@@ -0,0 +1,26 @@
|
||||
article:has(.home) {
|
||||
padding-inline: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
article div.x\:mt-16:last-child:empty {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#hero {
|
||||
animation-name: hero;
|
||||
animation-duration: 1500ms;
|
||||
animation-delay: 500ms;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes hero {
|
||||
from {
|
||||
translate: 0 8rem;
|
||||
}
|
||||
to {
|
||||
translate: 0 0;
|
||||
}
|
||||
}
|
||||
BIN
docs/src/app/favicon.ico
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
17
docs/src/app/globals.css
Normal file
@@ -0,0 +1,17 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@variant dark (&:where(.dark, .dark *));
|
||||
|
||||
.nextra-card .tabler-icon:hover {
|
||||
color: rgb(17 24 39 / var(--tw-text-opacity));
|
||||
}
|
||||
.nextra-card .tabler-icon {
|
||||
color: rgb(55 65 81 / var(--tw-text-opacity));
|
||||
}
|
||||
.nextra-card .tabler-icon:is(.dark *) {
|
||||
color: rgb(229 229 229 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.nextra-card .tabler-icon:is(.dark *):hover {
|
||||
color: rgb(250 250 250 / var(--tw-text-opacity));
|
||||
}
|
||||
55
docs/src/app/layout.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Footer, Layout, Navbar, ThemeSwitch } from "nextra-theme-docs";
|
||||
import { getPageMap } from "nextra/page-map";
|
||||
import { GeistSans } from "geist/font/sans";
|
||||
import "nextra-theme-docs/style.css";
|
||||
import "./globals.css";
|
||||
import { Head } from "./components/Head";
|
||||
import Logo from "./components/Logo";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Cup",
|
||||
description: "The easiest way to manage your container updates",
|
||||
};
|
||||
|
||||
const logo = (
|
||||
<div className="flex items-center">
|
||||
<Logo />
|
||||
<h1 className="ml-2 font-bold">Cup</h1>
|
||||
</div>
|
||||
);
|
||||
|
||||
const navbar = (
|
||||
<Navbar logo={logo} projectLink="https://github.com/sergi0g/cup">
|
||||
<ThemeSwitch lite className="cursor-pointer" />
|
||||
</Navbar>
|
||||
);
|
||||
|
||||
const footer = <Footer> </Footer>;
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
dir="ltr"
|
||||
suppressHydrationWarning
|
||||
className={`${GeistSans.className} antialiased`}
|
||||
>
|
||||
<Head />
|
||||
<body>
|
||||
<Layout
|
||||
navbar={navbar}
|
||||
pageMap={await getPageMap()}
|
||||
footer={footer}
|
||||
docsRepositoryBase="https://github.com/sergi0g/cup"
|
||||
>
|
||||
<div>{children}</div>
|
||||
</Layout>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
23
docs/src/app/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useMDXComponents } from "@/mdx-components";
|
||||
import { Heading, NextraMetadata } from "nextra";
|
||||
import Home from "./components/pages/home";
|
||||
|
||||
/* eslint-disable-next-line */
|
||||
const Wrapper = useMDXComponents({}).wrapper;
|
||||
|
||||
const toc: Heading[] = [];
|
||||
|
||||
export const metadata: NextraMetadata = {
|
||||
title: "Cup - The easiest way to manage your container updates",
|
||||
description: "Simple, fast, efficient Docker image update checking",
|
||||
filePath: "",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
// @ts-expect-error This component passes all extra props to the underlying component, but that possibility does not exist in the type declarations. A comment there indicates that passing extra props is intended functionality.
|
||||
<Wrapper toc={toc} metadata={metadata} className={"x:mx-auto x:flex"}>
|
||||
<Home />
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
17
docs/src/content/_meta.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export default {
|
||||
index: {
|
||||
theme: {
|
||||
sidebar: false,
|
||||
toc: false,
|
||||
breadcrumb: false,
|
||||
pagination: false,
|
||||
timestamp: false,
|
||||
layout: "full",
|
||||
},
|
||||
display: "hidden",
|
||||
},
|
||||
docs: {
|
||||
type: "page",
|
||||
title: "Documentation",
|
||||
},
|
||||
};
|
||||
5
docs/src/content/docs/_meta.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
installation: {},
|
||||
usage: {},
|
||||
configuration: {},
|
||||
};
|
||||
@@ -20,9 +20,24 @@ services:
|
||||
- ./cup.json:/config/cup.json
|
||||
```
|
||||
|
||||
If you don't have a config, you can use this instead:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
cup:
|
||||
image: ghcr.io/sergi0g/cup:latest
|
||||
container_name: cup # Optional
|
||||
restart: unless-stopped
|
||||
command: serve
|
||||
ports:
|
||||
- 8000:8000
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
```
|
||||
|
||||
This can be customized further of course, if you choose to use a different port, another config location, or would like to change something else.
|
||||
|
||||
Cup can run with a non-root user, but needs to be in a docker group. Assuming user id of 1000 and `docker` group id of 999 you can add this to the docker compose:
|
||||
```yaml
|
||||
user: "1000:999"
|
||||
```
|
||||
|
||||
This can be customized further of course, if you choose to use a different port, another config location, or would like to change something else. Have fun!
|
||||
@@ -1,6 +1,6 @@
|
||||
import Image from 'next/image';
|
||||
import widget1 from '../../../assets/350767810-42eccc89-bdfd-426a-a113-653abe7483d8.png'
|
||||
import widget2 from '../../../assets/358304960-e9f26767-51f7-4b5a-8b74-a5811019497b.jpeg'
|
||||
import Image from "next/image";
|
||||
import widget1 from "@/app/assets/350767810-42eccc89-bdfd-426a-a113-653abe7483d8.png";
|
||||
import widget2 from "@/app/assets/358304960-e9f26767-51f7-4b5a-8b74-a5811019497b.jpeg";
|
||||
|
||||
# Homepage Widget
|
||||
|
||||
@@ -28,7 +28,7 @@ services:
|
||||
homepage.ping: http://myserver:8000
|
||||
homepage.description: Checks for container updates
|
||||
homepage.widget.type: customapi
|
||||
homepage.widget.url: http://myserver:8000/json
|
||||
homepage.widget.url: http://myserver:8000/api/v3/json
|
||||
homepage.widget.mappings[0].label: Monitoring
|
||||
homepage.widget.mappings[0].field.metrics: monitored_images
|
||||
homepage.widget.mappings[0].format: number
|
||||
@@ -39,8 +39,10 @@ services:
|
||||
homepage.widget.mappings[2].field.metrics: update_available
|
||||
homepage.widget.mappings[2].format: number
|
||||
```
|
||||
|
||||
Preview:
|
||||
<Image src={widget1}/>
|
||||
|
||||
<Image src={widget1} />
|
||||
|
||||
Credit: [@agrmohit](https://github.com/agrmohit)
|
||||
|
||||
@@ -49,7 +51,7 @@ Credit: [@agrmohit](https://github.com/agrmohit)
|
||||
```yaml
|
||||
widget:
|
||||
type: customapi
|
||||
url: http://<SERVER_IP>:9000/json
|
||||
url: http://<SERVER_IP>:9000/api/v3/json
|
||||
refreshInterval: 10000
|
||||
method: GET
|
||||
mappings:
|
||||
@@ -57,19 +59,21 @@ widget:
|
||||
metrics: monitored_images
|
||||
label: Monitored images
|
||||
format: number
|
||||
- field:
|
||||
- field:
|
||||
metrics: up_to_date
|
||||
label: Up to date
|
||||
format: number
|
||||
- field:
|
||||
- field:
|
||||
metrics: update_available
|
||||
label: Available updates
|
||||
format: number
|
||||
- field:
|
||||
- field:
|
||||
metrics: unknown
|
||||
label: Unknown
|
||||
format: number
|
||||
```
|
||||
|
||||
Preview:
|
||||
<Image src={widget2}/>
|
||||
Credit: [@remussamoila](https://github.com/remussamoila)
|
||||
|
||||
<Image src={widget2} />
|
||||
Credit: [@remussamoila](https://github.com/remussamoila)
|
||||
26
docs/src/content/docs/configuration/authentication.mdx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Callout } from "nextra/components";
|
||||
|
||||
# Authentication
|
||||
|
||||
Some registries (or specific images) may require you to be authenticated. For those, you can modify `cup.json` like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"registries": {
|
||||
"<YOUR_REGISTRY_DOMAIN_1>": {
|
||||
"authentication": "<YOUR_TOKEN_1>"
|
||||
// Other options
|
||||
},
|
||||
"<YOUR_REGISTRY_DOMAIN_2>" {
|
||||
"authentication": "<YOUR_TOKEN_2>"
|
||||
// Other options
|
||||
},
|
||||
// ...
|
||||
}
|
||||
// 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>
|
||||
@@ -1,5 +1,15 @@
|
||||
import { Steps, Callout, Card, Cards } from "nextra-theme-docs";
|
||||
import { IconPaint, IconLockOpen, IconKey, IconPlug } from '@tabler/icons-react';
|
||||
---
|
||||
asIndexPage: true
|
||||
---
|
||||
|
||||
import { Steps, Callout, Cards } from "nextra/components";
|
||||
import {
|
||||
IconPaint,
|
||||
IconLockOpen,
|
||||
IconKey,
|
||||
IconPlug,
|
||||
IconServer
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
# Configuration
|
||||
|
||||
@@ -9,12 +19,17 @@ Sometimes, there may be a need to specify a custom docker socket. Cup provides t
|
||||
|
||||
For example, if using Podman, you might do
|
||||
|
||||
```
|
||||
```bash
|
||||
$ cup -s /run/user/1000/podman/podman.sock check
|
||||
```
|
||||
|
||||
This option is also available in the configuration file and it's best to put it there.
|
||||
<Card icon={<IconPlug />} title="Custom Docker socket" href="/docs/configuration/socket" />
|
||||
|
||||
<Cards.Card
|
||||
icon={<IconPlug />}
|
||||
title="Custom Docker socket"
|
||||
href="/docs/configuration/socket"
|
||||
/>
|
||||
|
||||
## Configuration file
|
||||
|
||||
@@ -23,30 +38,57 @@ 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_
|
||||
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 to customize your `cup.json`
|
||||
|
||||
<Cards>
|
||||
<Card icon={<IconKey />} title="Authentication" href="/docs/configuration/authentication" />
|
||||
<Card icon={<IconLockOpen />} title="Insecure registries" href="/docs/configuration/insecure-registries" />
|
||||
<Card icon={<IconPaint />} title="Theme" href="/docs/configuration/theme" />
|
||||
<Cards.Card
|
||||
icon={<IconKey />}
|
||||
title="Authentication"
|
||||
href="/docs/configuration/authentication"
|
||||
/>
|
||||
<Cards.Card
|
||||
icon={<IconLockOpen />}
|
||||
title="Insecure registries"
|
||||
href="/docs/configuration/insecure-registries"
|
||||
/>
|
||||
<Cards.Card
|
||||
icon={<IconPaint />}
|
||||
title="Theme"
|
||||
href="/docs/configuration/theme"
|
||||
/>
|
||||
<Cards.Card
|
||||
icon={<IconServer />}
|
||||
title="Multiple servers"
|
||||
href="/docs/configuration/servers"
|
||||
/>
|
||||
</Cards>
|
||||
|
||||
Here's a full example:
|
||||
|
||||
```json
|
||||
{
|
||||
"authentication": {
|
||||
"ghcr.io": "<YOUR_TOKEN_HERE>",
|
||||
"registry-1.docker.io": "<YOUR_TOKEN_HERE>"
|
||||
},
|
||||
"theme": "blue",
|
||||
"insecure_registries": ["localhost:5000", "my-insecure-registry.example.com"]
|
||||
"authentication": {
|
||||
"ghcr.io": "<YOUR_TOKEN_HERE>",
|
||||
"registry-1.docker.io": "<YOUR_TOKEN_HERE>"
|
||||
},
|
||||
"theme": "blue",
|
||||
"insecure_registries": ["localhost:5000", "my-insecure-registry.example.com"]
|
||||
}
|
||||
```
|
||||
|
||||
<Callout>
|
||||
If you want autocompletions and error checking for your editor, there is a
|
||||
JSON schema available. Use it by adding a `"$schema":
|
||||
"https://raw.githubusercontent.com/sergi0g/cup/main/cup.schema.json"` entry in
|
||||
your `cup.json` file.
|
||||
</Callout>
|
||||
|
||||
### 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
|
||||
@@ -56,4 +98,5 @@ $ 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>
|
||||
|
||||
</Steps>
|
||||
32
docs/src/content/docs/configuration/insecure-registries.mdx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Callout } from "nextra/components";
|
||||
|
||||
# Insecure registries
|
||||
|
||||
For the best security, Cup only connects to registries over SSL (HTTPS) by default. However, for people running a local registry that doesn't support SSL, this may be a problem.
|
||||
|
||||
To solve this problem, you can specify exceptions in your `cup.json`.
|
||||
|
||||
Here's what it looks like:
|
||||
|
||||
```json
|
||||
{
|
||||
"registries": {
|
||||
"<INSECURE_REGISTRY_1>": {
|
||||
"insecure": true
|
||||
// Other options
|
||||
},
|
||||
"<INSECURE_REGISTRY_2>" {
|
||||
"insecure": true
|
||||
// Other options
|
||||
},
|
||||
// ...
|
||||
}
|
||||
// Other options
|
||||
}
|
||||
```
|
||||
|
||||
<Callout emoji="⚠️">
|
||||
When configuring an insecure registry that doesn't run on port 80, don't
|
||||
forget to specify the port (i.e. use `localhost:5000` instead of `localhost`
|
||||
if your registry is running on port `5000`)
|
||||
</Callout>
|
||||
15
docs/src/content/docs/configuration/servers.mdx
Normal file
@@ -0,0 +1,15 @@
|
||||
# Multiple servers
|
||||
|
||||
Besides checking for local image updates, you might want to be able to view update stats for all your servers running Cup in a central place. If you choose to add more servers to your Cup configuration, Cup will retrieve the current list of updates from your other servers and it will be included in the results.
|
||||
|
||||
Just add something like this to your config:
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": {
|
||||
"Cool server 1": "http://your-other-server-running-cup:8000",
|
||||
"Other server": "http://and-another-one:9000"
|
||||
}
|
||||
// Other options
|
||||
}
|
||||
```
|
||||
19
docs/src/content/docs/configuration/socket.mdx
Normal file
@@ -0,0 +1,19 @@
|
||||
# Socket
|
||||
|
||||
If you need to specify a custom Docker socket (e.g. because you're using Podman), you can use the `socket` option. Here's an example:
|
||||
|
||||
```json
|
||||
{
|
||||
"socket": "/run/user/1000/podman/podman.sock"
|
||||
// Other options
|
||||
}
|
||||
```
|
||||
|
||||
You can also specify a TCP socket if you're using a remote Docker host or a [proxy](https://github.com/Tecnativa/docker-socket-proxy):
|
||||
|
||||
```json
|
||||
{
|
||||
"socket": "tcp://localhost:2375"
|
||||
// Other options
|
||||
}
|
||||
```
|
||||
31
docs/src/content/docs/configuration/theme.mdx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Callout } from "nextra/components";
|
||||
import Image from "next/image";
|
||||
|
||||
import blue from "@/app/assets/blue_theme.png";
|
||||
import neutral from "@/app/assets/hero-dark.png";
|
||||
|
||||
# Theme
|
||||
|
||||
<Callout emoji="⚠️">This configuration option is only for the server</Callout>
|
||||
|
||||
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={neutral} />
|
||||
|
||||
However, you can get the old theme back by adding the `theme` key to your `cup.json`
|
||||
Available options are `default` and `blue`.
|
||||
|
||||
Here's an example:
|
||||
|
||||
```json
|
||||
{
|
||||
"theme": "blue"
|
||||
// Other options
|
||||
}
|
||||
```
|
||||
|
||||
Note that the difference between the 2 themes is almost impossible to perceive when your system is in light mode.
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Steps } from "nextra/components";
|
||||
|
||||
# Contributing
|
||||
|
||||
First of all, thanks for taking time to contribute to Cup! This guide will help you set up a development environment and make your first contribution.
|
||||
@@ -5,14 +7,31 @@ First of all, thanks for taking time to contribute to Cup! This guide will help
|
||||
## Setting up a development environment
|
||||
|
||||
Requirements:
|
||||
|
||||
- A computer running Linux
|
||||
- Rust (usually installed from https://rustup.rs/)
|
||||
- Node.js 22+ and Bun 1+
|
||||
|
||||
1. Fork the repository. This is where you'll be pushing your changes before you create a pull request. Make sure to _create a new branch_ for your changes.
|
||||
2. Clone your fork with `git clone https://github.com/<YOUR_USERNAME>/cup` (if you use SSH, `git clone git@github.com:<YOUR_USERNAME>/cup`) and open your editor
|
||||
3. Switch to your newly created branch (e.g. if your branch is called `improve-logging`, run `git checkout improve-logging`)
|
||||
4. Run `bun install` in `web/` and `./build.sh` to set up the frontend
|
||||
<Steps>
|
||||
### Fork the repository
|
||||
This is where you'll be pushing your changes before you create a pull request. Make sure to _create a new branch_ for your changes.
|
||||
### Clone your fork
|
||||
```bash
|
||||
git clone https://github.com/<YOUR_USERNAME>/cup
|
||||
```
|
||||
If you use SSH:
|
||||
```bash
|
||||
git clone git@github.com:<YOUR_USERNAME>/cup`)
|
||||
```
|
||||
### Switch to your newly created branch (e.g. if your branch is called `improve-logging`, run `git checkout improve-logging`)
|
||||
### Set up the frontend
|
||||
```bash
|
||||
$ cd web
|
||||
$ bun install
|
||||
$ cd ..
|
||||
$ ./build.sh
|
||||
```
|
||||
</Steps>
|
||||
|
||||
You're ready to go!
|
||||
|
||||
@@ -27,8 +46,9 @@ All server specific functionality is located in `src/server.rs` and `web/`.
|
||||
## Important notes
|
||||
|
||||
- When making any changes, always make sure to write optimize your code for:
|
||||
+ Performance: You should always benchmark Cup before making changes and after your changes to make sure there is none (or a very small) difference in time. Profiling old and new code is also good.
|
||||
+ Readability: Include comments describing any new functions you create, give descriptive names to variables and when making a design decision or a compromise, ALWAYS include a comment explaining what you did and why.
|
||||
|
||||
- Performance: You should always benchmark Cup before making changes and after your changes to make sure there is none (or a very small) difference in time. Profiling old and new code is also good.
|
||||
- Readability: Include comments describing any new functions you create, give descriptive names to variables and when making a design decision or a compromise, ALWAYS include a comment explaining what you did and why.
|
||||
|
||||
- If you plan on developing the frontend without making backend changes, it is highly recommended to run `cup serve` in the background and start the frontend in development mode from `web/` with `bun dev`.
|
||||
|
||||
@@ -48,4 +68,4 @@ After you're done with that, commit your changes and push them to your branch.
|
||||
|
||||
Next, open your fork on Github and create a pull request. Make sure to include the changes you made, which issues it addresses (if any) and any other info you think is important.
|
||||
|
||||
Happy contributing!
|
||||
Happy contributing!
|
||||
@@ -1,11 +1,11 @@
|
||||
import Image from "next/image";
|
||||
import cup from "../../assets/cup.gif";
|
||||
import { Cards, Card } from "nextra-theme-docs";
|
||||
import cup from "@/app/assets/cup.gif";
|
||||
import { Cards } from "nextra/components";
|
||||
import { IconBrandDocker, IconPackage } from "@tabler/icons-react";
|
||||
|
||||
# Introduction
|
||||
|
||||
<Image src={cup} unoptimized />
|
||||
<Image src={cup} alt="Animated GIF of Cup's CLI in action" unoptimized />
|
||||
|
||||
Cup is a lightweight alternative to [What's up Docker?](https://github.com/getwud/wud) written in Rust.
|
||||
|
||||
@@ -15,12 +15,20 @@ Cup is a lightweight alternative to [What's up Docker?](https://github.com/getwu
|
||||
- Supports most registries, including Docker Hub, ghcr.io, Quay, lscr.io and even Gitea (or derivatives)
|
||||
- Doesn't exhaust any rate limits. This is the original reason I created Cup. It was inspired by [What's up docker?](https://github.com/getwud/wud) which would always use it up.
|
||||
- Beautiful CLI and web interface for checking on your containers any time.
|
||||
- The binary is tiny! At the time of writing it's just 5.2 MB. No more pulling 100+ MB docker images for a such a simple program.
|
||||
- The binary is tiny! At the time of writing it's just 5.1 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.Card
|
||||
icon={<IconBrandDocker />}
|
||||
title="With Docker"
|
||||
href="/docs/installation/docker"
|
||||
/>
|
||||
<Cards.Card
|
||||
icon={<IconPackage />}
|
||||
title="As a binary"
|
||||
href="/docs/installation/binary"
|
||||
/>
|
||||
</Cards>
|
||||
8
docs/src/content/docs/installation/_meta.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export default {
|
||||
docker: {
|
||||
title: "With Docker",
|
||||
},
|
||||
binary: {
|
||||
title: "As a binary",
|
||||
},
|
||||
};
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Callout, Card, Steps } from "nextra-theme-docs";
|
||||
import { Callout, Cards, Steps } from "nextra/components";
|
||||
import { IconFileDescription } from "@tabler/icons-react";
|
||||
|
||||
# As a binary
|
||||
|
||||
## Introduction
|
||||
|
||||
This guide will help you install Cup from a binary.
|
||||
@@ -13,6 +14,7 @@ This guide will help you install Cup from a 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>
|
||||
@@ -21,5 +23,6 @@ Move the binary you downloaded to a directory in your path. You can usually get
|
||||
</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" />
|
||||
<Cards.Card icon={<IconFileDescription />} title="Usage" href="/docs/usage" />
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Callout, Card } from "nextra-theme-docs";
|
||||
import { Callout, Cards } from "nextra/components";
|
||||
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.
|
||||
@@ -9,13 +10,18 @@ This guide will help you install Cup as a Docker container. It is the easiest in
|
||||
## 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`
|
||||
If you aren't a member of the `docker` group, please ensure you run all
|
||||
commands as a user who is. 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" />
|
||||
<Cards.Card icon={<IconFileDescription />} title="Usage" href="/docs/usage" />
|
||||
79
docs/src/content/docs/integrations.mdx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Callout, Cards } from "nextra/components";
|
||||
import { IconServer, IconTerminal } from "@tabler/icons-react"
|
||||
|
||||
# Integrations
|
||||
|
||||
At the moment, Cup has no built-in integrations, but it provides an API for the server and JSON output for the CLI, which can enable you to connect Cup to your own integrations.
|
||||
|
||||
## JSON data
|
||||
|
||||
The data returned from the API or from the CLI is in JSON and looks like this:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
// Statistics useful for displaying on dashboards.
|
||||
// You could calculate these yourself based on the rest of the data,
|
||||
// but they're provided for easier integration with other systems.
|
||||
"metrics": {
|
||||
"monitored_images": 5,
|
||||
"up_to_date": 2,
|
||||
"updates_available": 3,
|
||||
"major_updates": 1,
|
||||
"minor_updates": 0,
|
||||
"patch_updates": 0,
|
||||
"other_updates": 2,
|
||||
"unknown": 0,
|
||||
},
|
||||
// A list of image objects with all related information.
|
||||
"images": [
|
||||
{
|
||||
"reference": "ghcr.io/sergi0g/cup:latest",
|
||||
"parts": {
|
||||
// The information Cup extracted about the image from the reference. Mostly useful for debugging and the way the web interface works.
|
||||
"registry": "ghcr.io",
|
||||
"repository": "sergi0g/cup",
|
||||
"tag": "latest",
|
||||
},
|
||||
"result": {
|
||||
"has_update": true, // `true` when an image has an update of any kind, `false` when up to date and `null` when unknown.
|
||||
"info": {
|
||||
// `null` if up to date
|
||||
"type": "digest", // Can also be `version` when Cup detects the tag contains a version.
|
||||
// If `type` is "digest":
|
||||
"local_digests": [
|
||||
// A list of local digests present for the image
|
||||
"sha256:b7168e5f6828cbbd3622fa19965007e4611cf42b5f3c603008377ffd45a4fe00",
|
||||
],
|
||||
"remote_digest": "sha256:170f1974d8fc8ca245bcfae5590bc326de347b19719972bf122400fb13dfa42c", // Latest digest available in the registry
|
||||
// If `type` is "version":
|
||||
"version_update_type": "major", // Loosely corresponds to SemVer versioning. Can also be `minor` or `patch`.
|
||||
"new_tag": "v3.3.3", // The tag of the latest image.
|
||||
},
|
||||
"error": null, // If checking for the image fails, will be a string with an error message.
|
||||
},
|
||||
"time": 869, // Time in milliseconds it took to check for the update. Useful for debugging.
|
||||
"server": "Lithium", // The name of the server which the image was checked for updates on. `null` if from the current machine.
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
<Callout emoji="⚠️">
|
||||
Please keep in mind that the above may not always be up to date. New fields
|
||||
may be added, or some types extended. If you notice that, just open an issue
|
||||
and they'll be updated. Changes to the JSON data schema will _always_ happen
|
||||
in a backwards-compatible way. In case backwards-incompatible changes are
|
||||
made, these docs will be updated. For something more up-to-date, you can
|
||||
take a look at https://github.com/sergi0g/cup/blob/main/web/src/types.ts
|
||||
</Callout>
|
||||
|
||||
For retrieving the above data, refer to the CLI and server pages:
|
||||
|
||||
<Cards>
|
||||
<Cards.Card icon={<IconTerminal />} title="CLI" href="/docs/usage/cli" />
|
||||
<Cards.Card
|
||||
icon={<IconServer />}
|
||||
title="Server"
|
||||
href="/docs/usage/server"
|
||||
/>
|
||||
</Cards>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Callout } from "nextra-theme-docs"
|
||||
import { Callout } from "nextra/components";
|
||||
|
||||
# Using the latest version
|
||||
|
||||
@@ -9,7 +9,11 @@ However, it is only updated when a new release is created, so if you want the la
|
||||
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
|
||||
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
|
||||
@@ -18,4 +22,4 @@ 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.
|
||||
Go to a [nightly workflow run](https://github.com/sergi0g/cup/actions/workflows/nightly.yml) and download the artifact for your system.
|
||||
82
docs/src/content/docs/usage/cli.mdx
Normal file
@@ -0,0 +1,82 @@
|
||||
import Image from "next/image";
|
||||
import cup from "@/app/assets/cup.gif";
|
||||
import { Callout } from "nextra/components";
|
||||
|
||||
# CLI
|
||||
|
||||
Cup's CLI provides the `cup check` command.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Check for all updates
|
||||
|
||||
```ansi
|
||||
$ cup check
|
||||
[38;5;1m
|
||||
mysql:8.0 Major update
|
||||
node:20 Major update
|
||||
postgres:16-alpine Major update[0m[38;5;3m
|
||||
rust:1.80.1-alpine Minor update[0m[38;5;12m
|
||||
redis:7.4.0 Patch update
|
||||
nginx:alpine Update available
|
||||
redis:alpine Update available
|
||||
ubuntu:latest Update available[0m[38;5;2m
|
||||
node:iron Up to date
|
||||
2fauth/2fauth:latest Up to date
|
||||
c1982/sdns:latest Up to date[0m[38;5;8m
|
||||
registry.acme.com/acme-server:latest Unknown
|
||||
[36;1mINFO [0m✨ Checked 58 images in 3772ms
|
||||
```
|
||||
|
||||
### Check for updates to specific images
|
||||
|
||||
```ansi
|
||||
$ cup check node:latest[38;5;12m
|
||||
node:latest Update available
|
||||
[36;1mINFO [0m✨ Checked 1 images in 1310ms
|
||||
```
|
||||
|
||||
```ansi
|
||||
$ cup check nextcloud:30 postgres:14 mysql:8.0[38;5;12m
|
||||
nextcloud:30 Update available
|
||||
postgres:14 Update available[38;5;2m
|
||||
mysql:8.0 Up to date
|
||||
[36;1mINFO [0m✨ Checked 3 images in 1769ms
|
||||
```
|
||||
|
||||
## Enable icons
|
||||
|
||||
You can also enable icons if you have a [Nerd Font](https://nerdfonts.com) installed.
|
||||
|
||||
<Image src={cup} alt="GIF of Cup's CLI" 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. Note that at the moment it does not match the detailed API the server provides.
|
||||
|
||||
```
|
||||
$ cup check -r
|
||||
{"metrics":{"monitored_images":26,"up_to_date":2,"updates_available":23,"major_updates":8,"minor_updates":6,"patch_updates":2,"other_updates":7,"unknown":1},"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,...}}
|
||||
```
|
||||
|
||||
<Callout emoji="⚠️">
|
||||
When parsing Cup's output, capture only `stdout`, otherwise you might not get valid JSON (if there are warnings)
|
||||
</Callout>
|
||||
|
||||
## 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
|
||||
$ cup check node:latest
|
||||
```
|
||||
|
||||
becomes:
|
||||
|
||||
```bash
|
||||
$ docker run -tv /var/run/docker.sock:/var/run/docker.sock ghcr.io/sergi0g/cup check node:latest
|
||||
```
|
||||
15
docs/src/content/docs/usage/index.mdx
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
asIndexPage: true
|
||||
---
|
||||
|
||||
import { IconServer, IconTerminal } from "@tabler/icons-react";
|
||||
import { Cards } from "nextra/components";
|
||||
|
||||
# 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>
|
||||
<Cards.Card icon={<IconTerminal />} title="CLI" href="/docs/usage/cli" />
|
||||
<Cards.Card icon={<IconServer />} title="Server" href="/docs/usage/server" />
|
||||
</Cards>
|
||||
55
docs/src/content/docs/usage/server.mdx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Callout } from "nextra/components";
|
||||
|
||||
# Server
|
||||
|
||||
The server provides the `cup serve` command.
|
||||
|
||||
## Basic usage
|
||||
|
||||
```ansi
|
||||
$ cup serve
|
||||
[36;1mINFO [0mStarting server, please wait...
|
||||
[36;1mINFO [0m✨ Checked 8 images in 8862ms
|
||||
[36;1mINFO [0mReady to start!
|
||||
[94;1mHTTP [0m[32mGET[0m / [32m200[0m in 0ms
|
||||
[94;1mHTTP [0m[32mGET[0m /assets/index.js [32m200[0m in 3ms
|
||||
[94;1mHTTP [0m[32mGET[0m /assets/index.css [32m200[0m in 0ms
|
||||
[94;1mHTTP [0m[32mGET[0m /api/v3/json [32m200[0m in 0ms
|
||||
```
|
||||
|
||||
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/api/v3/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
|
||||
[36;1mINFO [0mStarting server, please wait...
|
||||
[36;1mINFO [0m✨ Checked 8 images in 8862ms
|
||||
[36;1mINFO [0mReady to start!
|
||||
[94;1mHTTP [0m[32mGET[0m / [32m200[0m in 0ms
|
||||
[94;1mHTTP [0m[32mGET[0m /assets/index.js [32m200[0m in 3ms
|
||||
[94;1mHTTP [0m[32mGET[0m /assets/index.css [32m200[0m in 0ms
|
||||
[94;1mHTTP [0m[32mGET[0m /api/v3/json [32m200[0m in 0ms
|
||||
```
|
||||
|
||||
## 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
|
||||
$ cup serve -p 9000
|
||||
```
|
||||
|
||||
becomes:
|
||||
|
||||
```bash
|
||||
$ docker run -tv /var/run/docker.sock:/var/run/docker.sock -p 9000:9000 ghcr.io/sergi0g/cup serve -p 9000
|
||||
```
|
||||
13
docs/src/mdx-components.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useMDXComponents as getThemeComponents } from "nextra-theme-docs";
|
||||
import { MDXComponents } from "nextra/mdx-components";
|
||||
|
||||
// Get the default MDX components
|
||||
const themeComponents = getThemeComponents();
|
||||
|
||||
// Merge components
|
||||
export function useMDXComponents(components: MDXComponents) {
|
||||
return {
|
||||
...themeComponents,
|
||||
...components,
|
||||
};
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.tabler-icon {
|
||||
color: rgb(250 250 250 / var(--tw-text-opacity)) !important
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"theme.config.jsx"
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
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/docs/' +
|
||||
(`/${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: "https://sergi0g.github.io/cup/docs/",
|
||||
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>
|
||||
);
|
||||
}
|
||||
27
docs/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
200
src/check.rs
@@ -1,63 +1,153 @@
|
||||
use futures::future::join_all;
|
||||
use itertools::Itertools;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
use crate::{
|
||||
config::Config, image::Image, registry::{check_auth, get_token}, utils::new_reqwest_client
|
||||
docker::get_images_from_docker_daemon,
|
||||
http::Client,
|
||||
registry::{check_auth, get_token},
|
||||
structs::{image::Image, update::Update},
|
||||
utils::request::{get_response_body, parse_json},
|
||||
Context,
|
||||
};
|
||||
|
||||
use crate::registry::get_latest_digest;
|
||||
/// Fetches image data from other Cup instances
|
||||
async fn get_remote_updates(ctx: &Context, client: &Client, refresh: bool) -> Vec<Update> {
|
||||
let mut remote_images = Vec::new();
|
||||
|
||||
/// Trait for a type that implements a function `unique` that removes any duplicates.
|
||||
/// In this case, it will be used for a Vec.
|
||||
pub trait Unique<T> {
|
||||
fn unique(&mut self) -> Vec<T>;
|
||||
}
|
||||
let handles: Vec<_> = ctx.config.servers
|
||||
.iter()
|
||||
.map(|(name, url)| async {
|
||||
let base_url = if url.starts_with("http://") || url.starts_with("https://") {
|
||||
format!("{}/api/v3/", url.trim_end_matches('/'))
|
||||
} else {
|
||||
format!("https://{}/api/v3/", url.trim_end_matches('/'))
|
||||
};
|
||||
let json_url = base_url.clone() + "json";
|
||||
if refresh {
|
||||
let refresh_url = base_url + "refresh";
|
||||
match client.get(&(&refresh_url), vec![], false).await {
|
||||
Ok(response) => {
|
||||
if response.status() != 200 {
|
||||
ctx.logger.warn(format!("GET {}: Failed to refresh server. Server returned invalid response code: {}",refresh_url,response.status()));
|
||||
return Vec::new();
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
ctx.logger.warn(format!("GET {}: Failed to refresh server. {}", refresh_url, e));
|
||||
return Vec::new();
|
||||
},
|
||||
}
|
||||
|
||||
impl<T> Unique<T> for Vec<T>
|
||||
where
|
||||
T: Clone + Eq + std::hash::Hash,
|
||||
{
|
||||
/// Remove duplicates from Vec
|
||||
fn unique(self: &mut Vec<T>) -> Self {
|
||||
let mut seen: FxHashSet<T> = FxHashSet::default();
|
||||
self.retain(|item| seen.insert(item.clone()));
|
||||
self.to_vec()
|
||||
}
|
||||
match client.get(&json_url, vec![], false).await {
|
||||
Ok(response) => {
|
||||
if response.status() != 200 {
|
||||
ctx.logger.warn(format!("GET {}: Failed to fetch updates from server. Server returned invalid response code: {}",json_url,response.status()));
|
||||
return Vec::new();
|
||||
}
|
||||
let json = parse_json(&get_response_body(response).await);
|
||||
if let Some(updates) = json["images"].as_array() {
|
||||
let mut server_updates: Vec<Update> = updates
|
||||
.iter()
|
||||
.filter_map(|img| serde_json::from_value(img.clone()).ok())
|
||||
.collect();
|
||||
// Add server origin to each image
|
||||
for update in &mut server_updates {
|
||||
update.server = Some(name.clone());
|
||||
update.status = update.get_status();
|
||||
}
|
||||
return server_updates;
|
||||
}
|
||||
|
||||
Vec::new()
|
||||
}
|
||||
Err(e) => {
|
||||
ctx.logger.warn(format!("GET {}: Failed to fetch updates from server. {}", json_url, e));
|
||||
Vec::new()
|
||||
},
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
for mut images in join_all(handles).await {
|
||||
remote_images.append(&mut images);
|
||||
}
|
||||
|
||||
remote_images
|
||||
}
|
||||
|
||||
/// Returns a list of updates for all images passed in.
|
||||
pub async fn get_updates(images: &[Image], config: &Config) -> Vec<(String, Option<bool>)> {
|
||||
pub async fn get_updates(
|
||||
references: &Option<Vec<String>>,
|
||||
refresh: bool,
|
||||
ctx: &Context,
|
||||
) -> Vec<Update> {
|
||||
let client = Client::new(ctx);
|
||||
|
||||
// Get local images
|
||||
ctx.logger.debug("Retrieving images to be checked");
|
||||
let mut images = get_images_from_docker_daemon(ctx, references).await;
|
||||
|
||||
// Add extra images from references
|
||||
if let Some(refs) = references {
|
||||
let image_refs: FxHashSet<&String> = images.iter().map(|image| &image.reference).collect();
|
||||
let extra = refs
|
||||
.iter()
|
||||
.filter(|&reference| !image_refs.contains(reference))
|
||||
.map(|reference| Image::from_reference(reference))
|
||||
.collect::<Vec<Image>>();
|
||||
images.extend(extra);
|
||||
}
|
||||
|
||||
// Get remote images from other servers
|
||||
let remote_updates = if !ctx.config.servers.is_empty() {
|
||||
ctx.logger.debug("Fetching updates from remote servers");
|
||||
get_remote_updates(ctx, &client, refresh).await
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
ctx.logger.debug(format!(
|
||||
"Checking {:?}",
|
||||
images.iter().map(|image| &image.reference).collect_vec()
|
||||
));
|
||||
|
||||
// Get a list of unique registries our images belong to. We are unwrapping the registry because it's guaranteed to be there.
|
||||
let registries: Vec<&String> = images
|
||||
.iter()
|
||||
.map(|image| image.registry.as_ref().unwrap())
|
||||
.collect::<Vec<&String>>()
|
||||
.unique();
|
||||
.map(|image| &image.parts.registry)
|
||||
.unique()
|
||||
.collect::<Vec<&String>>();
|
||||
|
||||
// Create request client. All network requests share the same client for better performance.
|
||||
// This client is also configured to retry a failed request up to 3 times with exponential backoff in between.
|
||||
let client = new_reqwest_client();
|
||||
let client = Client::new(ctx);
|
||||
|
||||
// Create a map of images indexed by registry. This solution seems quite inefficient, since each iteration causes a key to be looked up. I can't find anything better at the moment.
|
||||
let mut image_map: FxHashMap<&String, Vec<&Image>> = FxHashMap::default();
|
||||
|
||||
for image in images {
|
||||
for image in &images {
|
||||
image_map
|
||||
.entry(image.registry.as_ref().unwrap())
|
||||
.entry(&image.parts.registry)
|
||||
.or_default()
|
||||
.push(image);
|
||||
}
|
||||
|
||||
// Retrieve an authentication token (if required) for each registry.
|
||||
let mut tokens: FxHashMap<&String, Option<String>> = FxHashMap::default();
|
||||
let mut tokens: FxHashMap<&str, Option<String>> = FxHashMap::default();
|
||||
for registry in registries {
|
||||
let credentials = config.authentication.get(registry);
|
||||
match check_auth(registry, config, &client).await {
|
||||
let credentials = if let Some(registry_config) = ctx.config.registries.get(registry) {
|
||||
®istry_config.authentication
|
||||
} else {
|
||||
&None
|
||||
};
|
||||
match check_auth(registry, ctx, &client).await {
|
||||
Some(auth_url) => {
|
||||
let token = get_token(
|
||||
image_map.get(registry).unwrap(),
|
||||
&auth_url,
|
||||
&credentials,
|
||||
credentials,
|
||||
&client,
|
||||
)
|
||||
.await;
|
||||
@@ -69,27 +159,41 @@ pub async fn get_updates(images: &[Image], config: &Config) -> Vec<(String, Opti
|
||||
}
|
||||
}
|
||||
|
||||
// Create a Vec to store futures so we can await them all at once.
|
||||
let mut handles = Vec::new();
|
||||
// Loop through images and get the latest digest for each
|
||||
for image in images {
|
||||
let token = tokens.get(&image.registry.as_ref().unwrap()).unwrap();
|
||||
let future = get_latest_digest(image, token.as_ref(), config, &client);
|
||||
handles.push(future);
|
||||
ctx.logger.debug(format!("Tokens: {:?}", tokens));
|
||||
|
||||
let ignored_registries = ctx
|
||||
.config
|
||||
.registries
|
||||
.iter()
|
||||
.filter_map(|(registry, registry_config)| {
|
||||
if registry_config.ignore {
|
||||
Some(registry)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<&String>>();
|
||||
|
||||
let mut handles = Vec::with_capacity(images.len());
|
||||
|
||||
// Loop through images check for updates
|
||||
for image in &images {
|
||||
let is_ignored = ignored_registries.contains(&&image.parts.registry)
|
||||
|| ctx
|
||||
.config
|
||||
.images
|
||||
.exclude
|
||||
.iter()
|
||||
.any(|item| image.reference.starts_with(item));
|
||||
if !is_ignored {
|
||||
let token = tokens.get(image.parts.registry.as_str()).unwrap();
|
||||
let future = image.check(token.as_deref(), ctx, &client);
|
||||
handles.push(future);
|
||||
}
|
||||
}
|
||||
// Await all the futures
|
||||
let final_images = join_all(handles).await;
|
||||
|
||||
let mut result: Vec<(String, Option<bool>)> = Vec::with_capacity(images.len());
|
||||
final_images
|
||||
.iter()
|
||||
.for_each(|image| match &image.remote_digest {
|
||||
Some(digest) => {
|
||||
let has_update = !image.local_digests.as_ref().unwrap().contains(digest);
|
||||
result.push((image.reference.clone(), Some(has_update)))
|
||||
}
|
||||
None => result.push((image.reference.clone(), None)),
|
||||
});
|
||||
|
||||
result
|
||||
let images = join_all(handles).await;
|
||||
let mut updates: Vec<Update> = images.iter().map(|image| image.to_update()).collect();
|
||||
updates.extend_from_slice(&remote_updates);
|
||||
updates
|
||||
}
|
||||
|
||||
150
src/config.rs
@@ -1,130 +1,96 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::error;
|
||||
|
||||
const VALID_KEYS: [&str; 4] = ["authentication", "theme", "insecure_registries", "socket"];
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub enum Theme {
|
||||
#[serde(rename = "default")]
|
||||
Default,
|
||||
#[serde(rename = "blue")]
|
||||
Blue,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
impl Default for Theme {
|
||||
fn default() -> Self {
|
||||
Self::Default
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[serde(default)]
|
||||
pub struct RegistryConfig {
|
||||
pub authentication: Option<String>,
|
||||
pub insecure: bool,
|
||||
pub ignore: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[serde(default)]
|
||||
pub struct ImageConfig {
|
||||
pub extra: Vec<String>,
|
||||
pub exclude: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Config {
|
||||
pub authentication: FxHashMap<String, String>,
|
||||
pub theme: Theme,
|
||||
pub insecure_registries: Vec<String>,
|
||||
version: u8,
|
||||
pub agent: bool,
|
||||
pub images: ImageConfig,
|
||||
pub refresh_interval: Option<String>,
|
||||
pub registries: FxHashMap<String, RegistryConfig>,
|
||||
pub servers: FxHashMap<String, String>,
|
||||
pub socket: Option<String>,
|
||||
pub theme: Theme,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// A stupid new function that exists just so calling `load` doesn't require a self argument
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
authentication: FxHashMap::default(),
|
||||
version: 3,
|
||||
agent: false,
|
||||
images: ImageConfig::default(),
|
||||
refresh_interval: None,
|
||||
registries: FxHashMap::default(),
|
||||
servers: FxHashMap::default(),
|
||||
socket: None,
|
||||
theme: Theme::Default,
|
||||
insecure_registries: Vec::with_capacity(0),
|
||||
socket: None
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads the config from the file path provided and returns the parsed result.
|
||||
pub fn load(&self, path: Option<PathBuf>) -> Self {
|
||||
let raw_config = match &path {
|
||||
Some(path) => std::fs::read_to_string(path),
|
||||
None => Ok(String::from("{}")), // Empty config
|
||||
None => return Self::new(), // Empty config
|
||||
};
|
||||
if raw_config.is_err() {
|
||||
panic!(
|
||||
error!(
|
||||
"Failed to read config file from {}. Are you sure the file exists?",
|
||||
&path.unwrap().to_str().unwrap()
|
||||
)
|
||||
};
|
||||
self.parse(&raw_config.unwrap()) // We can safely unwrap here
|
||||
}
|
||||
/// Parses and validates the config. The process is quite manual and I would rather use a library, but I don't want to grow the dependency tree, for a config as simple as this one.
|
||||
/// Many of these checks are stupid, but we either validate the config properly, or we don't at all, so... this is the result. I _am not_ proud of this code.
|
||||
/// Parses and validates the config.
|
||||
pub fn parse(&self, raw_config: &str) -> Self {
|
||||
let json = match json::parse(raw_config) {
|
||||
Ok(v) => v,
|
||||
Err(e) => panic!("Failed to parse config!\n{}", e),
|
||||
let config: Self = match serde_json::from_str(raw_config) {
|
||||
Ok(config) => config,
|
||||
Err(e) => error!("Unexpected error occured while parsing config: {}", e),
|
||||
};
|
||||
// In the code, raw_<key> means the JsonValue from the parsed config, before it's validated.
|
||||
|
||||
// Authentication
|
||||
let raw_authentication = &json["authentication"];
|
||||
if !raw_authentication.is_null() && !raw_authentication.is_object() {
|
||||
error!("Config key `authentication` must be an object!");
|
||||
}
|
||||
let mut authentication: FxHashMap<String, String> = FxHashMap::default();
|
||||
raw_authentication.entries().for_each(|(registry, key)| {
|
||||
if !key.is_string() {
|
||||
error!("Config key `authentication.{}` must be a string!", registry);
|
||||
}
|
||||
authentication.insert(registry.to_string(), key.to_string());
|
||||
});
|
||||
|
||||
// Theme
|
||||
let raw_theme = &json["theme"];
|
||||
if !raw_theme.is_null() && !raw_theme.is_string() {
|
||||
error!("Config key `theme` must be a string!");
|
||||
}
|
||||
let theme: Theme = {
|
||||
if raw_theme.is_null() {
|
||||
Theme::Default
|
||||
} else {
|
||||
match raw_theme.as_str().unwrap() {
|
||||
"default" => Theme::Default,
|
||||
"blue" => Theme::Blue,
|
||||
_ => {
|
||||
error!("Config key `theme` must be one of: `default`, `blue`!");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Insecure registries
|
||||
let raw_insecure_registries = &json["insecure_registries"];
|
||||
if !raw_insecure_registries.is_null() && !raw_insecure_registries.is_array() {
|
||||
error!("Config key `insecure_registries` must be an array!");
|
||||
}
|
||||
let insecure_registries: Vec<String> = raw_insecure_registries
|
||||
.members()
|
||||
.map(|registry| {
|
||||
if !registry.is_string() {
|
||||
error!("Config key `insecure_registries` must only consist of strings!");
|
||||
} else {
|
||||
registry.as_str().unwrap().to_owned()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Socket
|
||||
let raw_socket = &json["socket"];
|
||||
if !raw_socket.is_null() && !raw_socket.is_string() {
|
||||
error!("Config key `socket` must be a string!");
|
||||
}
|
||||
let socket: Option<String> = if raw_socket.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(raw_socket.to_string())
|
||||
};
|
||||
|
||||
// Check for extra keys
|
||||
json.entries().for_each(|(key, _)| {
|
||||
if !VALID_KEYS.contains(&key) {
|
||||
error!("Invalid key `{}`", key)
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
authentication,
|
||||
theme,
|
||||
insecure_registries,
|
||||
socket,
|
||||
if config.version != 3 {
|
||||
error!("You are trying to run Cup with an incompatible config file! Please migrate your config file to the version 3, or if you have already done so, add a `version` key with the value `3`.")
|
||||
}
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,62 +2,46 @@ use bollard::{models::ImageInspect, ClientVersion, Docker};
|
||||
|
||||
use futures::future::join_all;
|
||||
|
||||
use crate::{error, image::Image, config::Config};
|
||||
use crate::{error, structs::image::Image, Context};
|
||||
|
||||
fn create_docker_client(socket: Option<String>) -> Docker {
|
||||
fn create_docker_client(socket: Option<&str>) -> Docker {
|
||||
let client: Result<Docker, bollard::errors::Error> = match socket {
|
||||
Some(sock) => Docker::connect_with_local(
|
||||
&sock,
|
||||
120,
|
||||
&ClientVersion {
|
||||
major_version: 1,
|
||||
minor_version: 44,
|
||||
},
|
||||
),
|
||||
None => Docker::connect_with_local_defaults(),
|
||||
Some(sock) => {
|
||||
if sock.starts_with("unix://") {
|
||||
Docker::connect_with_unix(
|
||||
sock,
|
||||
120,
|
||||
&ClientVersion {
|
||||
major_version: 1,
|
||||
minor_version: 44,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Docker::connect_with_http(
|
||||
sock,
|
||||
120,
|
||||
&ClientVersion {
|
||||
major_version: 1,
|
||||
minor_version: 44,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
None => Docker::connect_with_unix_defaults(),
|
||||
};
|
||||
|
||||
match client {
|
||||
Ok(d) => d,
|
||||
Err(e) => error!("Failed to connect to docker socket!\n{}", e),
|
||||
Err(e) => error!("Failed to connect to docker daemon!\n{}", e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves images from Docker daemon. If `references` is Some, return only the images whose references match the ones specified.
|
||||
pub async fn get_images_from_docker_daemon(
|
||||
config: &Config,
|
||||
ctx: &Context,
|
||||
references: &Option<Vec<String>>,
|
||||
) -> Vec<Image> {
|
||||
let client: Docker = create_docker_client(config.socket.clone());
|
||||
// If https://github.com/moby/moby/issues/48612 is fixed, this code should be faster. For now a workaround will be used.
|
||||
// let mut filters = HashMap::with_capacity(1);
|
||||
// match references {
|
||||
// Some(refs) => {
|
||||
// filters.insert("reference".to_string(), refs.clone());
|
||||
// }
|
||||
// None => (),
|
||||
// }
|
||||
// let images: Vec<ImageSummary> = match client
|
||||
// .list_images::<String>(Some(ListImagesOptions {
|
||||
// filters,
|
||||
// ..Default::default()
|
||||
// }))
|
||||
// .await
|
||||
// {
|
||||
// Ok(images) => images,
|
||||
// Err(e) => {
|
||||
// error!("Failed to retrieve list of images available!\n{}", e)
|
||||
// }
|
||||
// };
|
||||
// let mut handles = Vec::new();
|
||||
// for image in images {
|
||||
// handles.push(Image::from(image, options))
|
||||
// }
|
||||
// join_all(handles)
|
||||
// .await
|
||||
// .iter()
|
||||
// .filter_map(|img| img.clone())
|
||||
// .collect()
|
||||
let client: Docker = create_docker_client(ctx.config.socket.as_deref());
|
||||
match references {
|
||||
Some(refs) => {
|
||||
let mut inspect_handles = Vec::with_capacity(refs.len());
|
||||
@@ -70,14 +54,9 @@ pub async fn get_images_from_docker_daemon(
|
||||
.filter(|inspect| inspect.is_ok())
|
||||
.map(|inspect| inspect.as_ref().unwrap().clone())
|
||||
.collect();
|
||||
let mut image_handles = Vec::with_capacity(inspects.len());
|
||||
for inspect in inspects {
|
||||
image_handles.push(Image::from_inspect(inspect.clone()));
|
||||
}
|
||||
join_all(image_handles)
|
||||
.await
|
||||
inspects
|
||||
.iter()
|
||||
.filter_map(|img| img.clone())
|
||||
.filter_map(|inspect| Image::from_inspect_data(inspect.clone()))
|
||||
.collect()
|
||||
}
|
||||
None => {
|
||||
@@ -87,14 +66,9 @@ pub async fn get_images_from_docker_daemon(
|
||||
error!("Failed to retrieve list of images available!\n{}", e)
|
||||
}
|
||||
};
|
||||
let mut handles = Vec::new();
|
||||
for image in images {
|
||||
handles.push(Image::from_summary(image))
|
||||
}
|
||||
join_all(handles)
|
||||
.await
|
||||
images
|
||||
.iter()
|
||||
.filter_map(|img| img.clone())
|
||||
.filter_map(|image| Image::from_inspect_data(image.clone()))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
|
||||
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);
|
||||
let term_width: usize = termsize::get()
|
||||
.unwrap_or(termsize::Size { rows: 24, cols: 80 })
|
||||
.cols as usize;
|
||||
for update in sorted_updates {
|
||||
let description = match update.1 {
|
||||
Some(true) => "Update available",
|
||||
Some(false) => "Up to date",
|
||||
None => "Unknown",
|
||||
};
|
||||
let icon = if *icons {
|
||||
match update.1 {
|
||||
Some(true) => "\u{f0aa} ",
|
||||
Some(false) => "\u{f058} ",
|
||||
None => "\u{f059} ",
|
||||
}
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let color = match update.1 {
|
||||
Some(true) => "\u{001b}[38;5;12m",
|
||||
Some(false) => "\u{001b}[38;5;2m",
|
||||
None => "\u{001b}[38;5;8m",
|
||||
};
|
||||
let dynamic_space =
|
||||
" ".repeat(term_width - description.len() - icon.len() - update.0.len());
|
||||
println!(
|
||||
"{}{}{}{}{}\u{001b}[0m",
|
||||
color, icon, update.0, dynamic_space, description
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_raw_updates(updates: &[(String, Option<bool>)]) {
|
||||
println!("{}", json::stringify(to_json(updates)));
|
||||
}
|
||||
|
||||
pub struct Spinner {
|
||||
spinner: ProgressBar,
|
||||
}
|
||||
|
||||
impl Spinner {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Spinner {
|
||||
let spinner = ProgressBar::new_spinner();
|
||||
let style: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||
let progress_style = ProgressStyle::default_spinner();
|
||||
|
||||
spinner.set_style(ProgressStyle::tick_strings(progress_style, style));
|
||||
|
||||
spinner.set_message("Checking...");
|
||||
spinner.enable_steady_tick(Duration::from_millis(50));
|
||||
|
||||
Spinner { spinner }
|
||||
}
|
||||
pub fn succeed(&self) {
|
||||
const CHECKMARK: &str = "\u{001b}[32;1m\u{2713}\u{001b}[0m";
|
||||
|
||||
let success_message = format!("{} Done!", CHECKMARK);
|
||||
self.spinner
|
||||
.set_style(ProgressStyle::with_template("{msg}").unwrap());
|
||||
self.spinner.finish_with_message(success_message);
|
||||
}
|
||||
}
|
||||
168
src/formatting/mod.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
pub mod spinner;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::{
|
||||
structs::{
|
||||
status::Status,
|
||||
update::{Update, UpdateInfo},
|
||||
},
|
||||
utils::{json::to_simple_json, sort_update_vec::sort_update_vec},
|
||||
};
|
||||
|
||||
pub fn print_updates(updates: &[Update], icons: &bool) {
|
||||
let sorted_updates = sort_update_vec(updates);
|
||||
let updates_by_server = {
|
||||
let mut servers: FxHashMap<&str, Vec<&Update>> = FxHashMap::default();
|
||||
sorted_updates.iter().for_each(|update| {
|
||||
let key = update.server.as_deref().unwrap_or("");
|
||||
match servers.get_mut(&key) {
|
||||
Some(server) => server.push(update),
|
||||
None => {
|
||||
let _ = servers.insert(key, vec![update]);
|
||||
}
|
||||
}
|
||||
});
|
||||
servers
|
||||
};
|
||||
for (server, updates) in updates_by_server {
|
||||
if server.is_empty() {
|
||||
println!("\x1b[90;1m~ Local images\x1b[0m")
|
||||
} else {
|
||||
println!("\x1b[90;1m~ {}\x1b[0m", server)
|
||||
}
|
||||
let (reference_width, status_width, time_width) =
|
||||
updates.iter().fold((9, 6, 9), |acc, update| {
|
||||
let reference_length = update.reference.len();
|
||||
let status_length = update.get_status().to_string().len()
|
||||
+ match &update.result.info {
|
||||
UpdateInfo::Version(info) => {
|
||||
info.current_version.len() + info.new_version.len() + 6
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
let time_length = update.time.to_string().len();
|
||||
(
|
||||
if reference_length > acc.0 {
|
||||
reference_length
|
||||
} else {
|
||||
acc.0
|
||||
},
|
||||
if status_length > acc.1 {
|
||||
status_length
|
||||
} else {
|
||||
acc.1
|
||||
},
|
||||
if time_length > acc.2 {
|
||||
time_length
|
||||
} else {
|
||||
acc.2
|
||||
},
|
||||
)
|
||||
});
|
||||
println!(
|
||||
" \x1b[90;1m╭{:─<rw$}┬{:─<sw$}┬{:─<tw$}╮\x1b[0m",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
rw = reference_width,
|
||||
sw = status_width + {
|
||||
if *icons {
|
||||
2
|
||||
} else {
|
||||
0
|
||||
}
|
||||
},
|
||||
tw = time_width
|
||||
);
|
||||
println!(
|
||||
" \x1b[90;1m│\x1b[36;1m{:<rw$}\x1b[90;1m│\x1b[36;1m{:<sw$}\x1b[90;1m│\x1b[36;1m{:<tw$}\x1b[90;1m│\x1b[0m",
|
||||
"Reference",
|
||||
"Status",
|
||||
"Time (ms)",
|
||||
rw = reference_width,
|
||||
sw = status_width + {
|
||||
if *icons {
|
||||
2
|
||||
} else {
|
||||
0
|
||||
}
|
||||
},
|
||||
tw = time_width
|
||||
);
|
||||
println!(
|
||||
" \x1b[90;1m├{:─<rw$}┼{:─<sw$}┼{:─<tw$}┤\x1b[0m",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
rw = reference_width,
|
||||
sw = status_width + {
|
||||
if *icons {
|
||||
2
|
||||
} else {
|
||||
0
|
||||
}
|
||||
},
|
||||
tw = time_width
|
||||
);
|
||||
for update in updates {
|
||||
let status = update.get_status();
|
||||
let icon = if *icons {
|
||||
match status {
|
||||
Status::UpToDate => "\u{f058} ",
|
||||
Status::Unknown(_) => "\u{f059} ",
|
||||
_ => "\u{f0aa} ",
|
||||
}
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let color = match status {
|
||||
Status::UpdateAvailable | Status::UpdatePatch => "\x1b[34m",
|
||||
Status::UpdateMinor => "\x1b[33m",
|
||||
Status::UpdateMajor => "\x1b[31m",
|
||||
Status::UpToDate => "\x1b[32m",
|
||||
Status::Unknown(_) => "\x1b[90m",
|
||||
};
|
||||
let description = format!(
|
||||
"{} {}",
|
||||
status,
|
||||
match &update.result.info {
|
||||
UpdateInfo::Version(info) => {
|
||||
format!("({} → {})", info.current_version, info.new_version)
|
||||
}
|
||||
_ => String::new(),
|
||||
}
|
||||
);
|
||||
println!(
|
||||
" \x1b[90;1m│\x1b[0m{:<rw$}\x1b[90;1m│\x1b[0m{}{}{:<sw$}\x1b[0m\x1b[90;1m│\x1b[0m{:<tw$}\x1b[90;1m│\x1b[0m",
|
||||
update.reference,
|
||||
color,
|
||||
icon,
|
||||
description,
|
||||
update.time,
|
||||
rw = reference_width,
|
||||
sw = status_width,
|
||||
tw = time_width
|
||||
);
|
||||
}
|
||||
println!(
|
||||
" \x1b[90;1m╰{:─<rw$}┴{:─<sw$}┴{:─<tw$}╯\x1b[0m",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
rw = reference_width,
|
||||
sw = status_width + {
|
||||
if *icons {
|
||||
2
|
||||
} else {
|
||||
0
|
||||
}
|
||||
},
|
||||
tw = time_width
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_raw_updates(updates: &[Update]) {
|
||||
println!("{}", to_simple_json(updates));
|
||||
}
|
||||
31
src/formatting/spinner.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
|
||||
pub struct Spinner {
|
||||
spinner: ProgressBar,
|
||||
}
|
||||
|
||||
impl Spinner {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Spinner {
|
||||
let spinner = ProgressBar::new_spinner();
|
||||
let style: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||
let progress_style = ProgressStyle::default_spinner();
|
||||
|
||||
spinner.set_style(ProgressStyle::tick_strings(progress_style, style));
|
||||
|
||||
spinner.set_message("Checking...");
|
||||
spinner.enable_steady_tick(Duration::from_millis(50));
|
||||
|
||||
Spinner { spinner }
|
||||
}
|
||||
pub fn succeed(&self) {
|
||||
const CHECKMARK: &str = "\u{001b}[32;1m\u{2713}\u{001b}[0m";
|
||||
|
||||
let success_message = format!("{} Done!", CHECKMARK);
|
||||
self.spinner
|
||||
.set_style(ProgressStyle::with_template("{msg}").unwrap());
|
||||
self.spinner.finish_with_message(success_message);
|
||||
}
|
||||
}
|
||||
127
src/http.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use reqwest::Response;
|
||||
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
|
||||
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
|
||||
|
||||
use crate::{error, Context};
|
||||
|
||||
pub enum RequestMethod {
|
||||
GET,
|
||||
HEAD,
|
||||
}
|
||||
|
||||
impl Display for RequestMethod {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
RequestMethod::GET => "GET",
|
||||
RequestMethod::HEAD => "HEAD",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A struct for handling HTTP requests. Takes care of the repetitive work of checking for errors, etc and exposes a simple interface
|
||||
pub struct Client {
|
||||
inner: ClientWithMiddleware,
|
||||
ctx: Context,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(ctx: &Context) -> Self {
|
||||
Self {
|
||||
inner: ClientBuilder::new(reqwest::Client::new())
|
||||
.with(RetryTransientMiddleware::new_with_policy(
|
||||
ExponentialBackoff::builder().build_with_max_retries(3),
|
||||
))
|
||||
.build(),
|
||||
ctx: ctx.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn request(
|
||||
&self,
|
||||
url: &str,
|
||||
method: RequestMethod,
|
||||
headers: Vec<(&str, Option<&str>)>,
|
||||
ignore_401: bool,
|
||||
) -> Result<Response, String> {
|
||||
let mut request = match method {
|
||||
RequestMethod::GET => self.inner.get(url),
|
||||
RequestMethod::HEAD => self.inner.head(url),
|
||||
};
|
||||
for (name, value) in headers {
|
||||
if let Some(v) = value {
|
||||
request = request.header(name, v)
|
||||
}
|
||||
}
|
||||
match request.send().await {
|
||||
Ok(response) => {
|
||||
let status = response.status();
|
||||
if status == 404 {
|
||||
let message = format!("{} {}: Not found!", method, url);
|
||||
self.ctx.logger.warn(&message);
|
||||
Err(message)
|
||||
} else if status == 401 {
|
||||
if ignore_401 {
|
||||
Ok(response)
|
||||
} else {
|
||||
let message = format!("{} {}: Unauthorized! Please configure authentication for this registry or if you have already done so, please make sure it is correct.", method, url);
|
||||
self.ctx.logger.warn(&message);
|
||||
Err(message)
|
||||
}
|
||||
} else if status.as_u16() <= 400 {
|
||||
Ok(response)
|
||||
} else {
|
||||
match method {
|
||||
RequestMethod::GET => error!(
|
||||
"{} {}: Unexpected error: {}",
|
||||
method,
|
||||
url,
|
||||
response.text().await.unwrap()
|
||||
),
|
||||
RequestMethod::HEAD => error!(
|
||||
"{} {}: Unexpected error: Recieved status code {}",
|
||||
method, url, status
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
if error.is_connect() {
|
||||
let message = format!("{} {}: Connection failed!", method, url);
|
||||
self.ctx.logger.warn(&message);
|
||||
Err(message)
|
||||
} else if error.is_timeout() {
|
||||
let message = format!("{} {}: Connection timed out!", method, url);
|
||||
self.ctx.logger.warn(&message);
|
||||
Err(message)
|
||||
} else {
|
||||
error!(
|
||||
"{} {}: Unexpected error: {}",
|
||||
method,
|
||||
url,
|
||||
error.to_string()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get(
|
||||
&self,
|
||||
url: &str,
|
||||
headers: Vec<(&str, Option<&str>)>,
|
||||
ignore_401: bool,
|
||||
) -> Result<Response, String> {
|
||||
self.request(url, RequestMethod::GET, headers, ignore_401)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn head(
|
||||
&self,
|
||||
url: &str,
|
||||
headers: Vec<(&str, Option<&str>)>,
|
||||
) -> Result<Response, String> {
|
||||
self.request(url, RequestMethod::HEAD, headers, false).await
|
||||
}
|
||||
}
|
||||
119
src/image.rs
@@ -1,119 +0,0 @@
|
||||
use bollard::models::{ImageInspect, ImageSummary};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::error;
|
||||
|
||||
/// Image struct that contains all information that may be needed by a function.
|
||||
/// It's designed to be passed around between functions
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Image {
|
||||
pub reference: String,
|
||||
pub registry: Option<String>,
|
||||
pub repository: Option<String>,
|
||||
pub tag: Option<String>,
|
||||
pub local_digests: Option<Vec<String>>,
|
||||
pub remote_digest: Option<String>,
|
||||
}
|
||||
|
||||
impl Image {
|
||||
/// Creates an populates the fields of an Image object based on the ImageSummary from the Docker daemon
|
||||
pub async fn from_summary(image: ImageSummary) -> Option<Self> {
|
||||
if !image.repo_tags.is_empty() && !image.repo_digests.is_empty() {
|
||||
let mut image = Image {
|
||||
reference: image.repo_tags[0].clone(),
|
||||
registry: None,
|
||||
repository: None,
|
||||
tag: None,
|
||||
local_digests: Some(
|
||||
image
|
||||
.repo_digests
|
||||
.clone()
|
||||
.iter()
|
||||
.map(|digest| digest.split('@').collect::<Vec<&str>>()[1].to_string())
|
||||
.collect(),
|
||||
),
|
||||
remote_digest: None,
|
||||
};
|
||||
let (registry, repository, tag) = image.split();
|
||||
image.registry = Some(registry);
|
||||
image.repository = Some(repository);
|
||||
image.tag = Some(tag);
|
||||
|
||||
return Some(image);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn from_inspect(image: ImageInspect) -> Option<Self> {
|
||||
if image.repo_tags.is_some()
|
||||
&& !image.repo_tags.as_ref().unwrap().is_empty()
|
||||
&& image.repo_digests.is_some()
|
||||
&& !image.repo_digests.as_ref().unwrap().is_empty()
|
||||
{
|
||||
let mut image = Image {
|
||||
reference: image.repo_tags.as_ref().unwrap()[0].clone(),
|
||||
registry: None,
|
||||
repository: None,
|
||||
tag: None,
|
||||
local_digests: Some(
|
||||
image
|
||||
.repo_digests
|
||||
.unwrap()
|
||||
.clone()
|
||||
.iter()
|
||||
.map(|digest| digest.split('@').collect::<Vec<&str>>()[1].to_string())
|
||||
.collect(),
|
||||
),
|
||||
remote_digest: None,
|
||||
};
|
||||
let (registry, repository, tag) = image.split();
|
||||
image.registry = Some(registry);
|
||||
image.repository = Some(repository);
|
||||
image.tag = Some(tag);
|
||||
|
||||
return Some(image);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Takes an image and splits it into registry, repository and tag, based on the reference.
|
||||
/// For example, `ghcr.io/sergi0g/cup:latest` becomes `['ghcr.io', 'sergi0g/cup', 'latest']`.
|
||||
pub fn split(&self) -> (String, String, String) {
|
||||
match RE.captures(&self.reference) {
|
||||
Some(c) => {
|
||||
let registry = match c.name("registry") {
|
||||
Some(registry) => registry.as_str().to_owned(),
|
||||
None => String::from("registry-1.docker.io"),
|
||||
};
|
||||
return (
|
||||
registry.clone(),
|
||||
match c.name("repository") {
|
||||
Some(repository) => {
|
||||
let repo = repository.as_str().to_owned();
|
||||
if !repo.contains('/') && registry == "registry-1.docker.io" {
|
||||
format!("library/{}", repo)
|
||||
} else {
|
||||
repo
|
||||
}
|
||||
}
|
||||
None => error!("Failed to parse image {}", &self.reference),
|
||||
},
|
||||
match c.name("tag") {
|
||||
Some(tag) => tag.as_str().to_owned(),
|
||||
None => String::from("latest"),
|
||||
},
|
||||
);
|
||||
}
|
||||
None => error!("Failed to parse image {}", &self.reference),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Regex to match Docker image references against, so registry, repository and tag can be extracted.
|
||||
static RE: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(
|
||||
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()
|
||||
});
|
||||
42
src/logging.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
#[macro_export]
|
||||
macro_rules! error {
|
||||
($($arg:tt)*) => ({
|
||||
eprintln!("\x1b[31;1mERROR\x1b[0m {}", format!($($arg)*));
|
||||
std::process::exit(1);
|
||||
})
|
||||
}
|
||||
|
||||
/// This struct mostly exists so we can print stuff without passing debug or raw every time.
|
||||
#[derive(Clone)]
|
||||
pub struct Logger {
|
||||
debug: bool,
|
||||
raw: bool,
|
||||
}
|
||||
|
||||
impl Logger {
|
||||
pub fn new(debug: bool, raw: bool) -> Self {
|
||||
Self { debug, raw }
|
||||
}
|
||||
|
||||
pub fn warn(&self, msg: impl AsRef<str>) {
|
||||
if !self.raw {
|
||||
eprintln!("\x1b[33;1m WARN\x1b[0m {}", msg.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn info(&self, msg: impl AsRef<str>) {
|
||||
if !self.raw {
|
||||
println!("\x1b[36;1m INFO\x1b[0m {}", msg.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn debug(&self, msg: impl AsRef<str>) {
|
||||
if self.debug {
|
||||
println!("\x1b[35;1mDEBUG\x1b[0m {}", msg.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_raw(&mut self, raw: bool) {
|
||||
self.raw = raw
|
||||
}
|
||||
}
|
||||
51
src/main.rs
@@ -1,23 +1,26 @@
|
||||
use check::get_updates;
|
||||
use chrono::Local;
|
||||
use clap::{Parser, Subcommand};
|
||||
use config::Config;
|
||||
use docker::get_images_from_docker_daemon;
|
||||
use formatting::spinner::Spinner;
|
||||
#[cfg(feature = "cli")]
|
||||
use formatting::{print_raw_updates, print_updates, Spinner};
|
||||
use formatting::{print_raw_updates, print_updates};
|
||||
use logging::Logger;
|
||||
#[cfg(feature = "server")]
|
||||
use server::serve;
|
||||
use std::path::PathBuf;
|
||||
use std::time::SystemTime;
|
||||
|
||||
pub mod check;
|
||||
pub mod config;
|
||||
pub mod docker;
|
||||
#[cfg(feature = "cli")]
|
||||
pub mod formatting;
|
||||
pub mod image;
|
||||
pub mod http;
|
||||
pub mod logging;
|
||||
pub mod registry;
|
||||
#[cfg(feature = "server")]
|
||||
pub mod server;
|
||||
pub mod structs;
|
||||
pub mod utils;
|
||||
|
||||
#[derive(Parser)]
|
||||
@@ -29,13 +32,17 @@ struct Cli {
|
||||
config_path: String,
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
#[arg(short, long)]
|
||||
debug: bool,
|
||||
#[arg(long)]
|
||||
refresh: bool,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
#[cfg(feature = "cli")]
|
||||
Check {
|
||||
#[arg(name = "Images", default_value = None)]
|
||||
#[arg(name = "images", default_value = None)]
|
||||
references: Option<Vec<String>>,
|
||||
#[arg(short, long, default_value_t = false, help = "Enable icons")]
|
||||
icons: bool,
|
||||
@@ -59,6 +66,12 @@ enum Commands {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Context {
|
||||
pub config: Config,
|
||||
pub logger: Logger,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let cli = Cli::parse();
|
||||
@@ -67,10 +80,13 @@ async fn main() {
|
||||
path => Some(PathBuf::from(path)),
|
||||
};
|
||||
let mut config = Config::new().load(cfg_path);
|
||||
match cli.socket {
|
||||
Some(socket) => config.socket = Some(socket),
|
||||
None => ()
|
||||
if let Some(socket) = cli.socket {
|
||||
config.socket = Some(socket)
|
||||
}
|
||||
let mut ctx = Context {
|
||||
config,
|
||||
logger: Logger::new(cli.debug, false),
|
||||
};
|
||||
match &cli.command {
|
||||
#[cfg(feature = "cli")]
|
||||
Some(Commands::Check {
|
||||
@@ -78,27 +94,28 @@ async fn main() {
|
||||
icons,
|
||||
raw,
|
||||
}) => {
|
||||
let start = Local::now().timestamp_millis();
|
||||
let images = get_images_from_docker_daemon(&config, references).await;
|
||||
match raw {
|
||||
let start = SystemTime::now();
|
||||
if *raw {
|
||||
ctx.logger.set_raw(true);
|
||||
}
|
||||
match *raw || cli.debug {
|
||||
true => {
|
||||
let updates = get_updates(&images, &config).await;
|
||||
let updates = get_updates(references, cli.refresh, &ctx).await;
|
||||
print_raw_updates(&updates);
|
||||
}
|
||||
false => {
|
||||
let spinner = Spinner::new();
|
||||
let updates = get_updates(&images, &config).await;
|
||||
let updates = get_updates(references, cli.refresh, &ctx).await;
|
||||
spinner.succeed();
|
||||
let end = Local::now().timestamp_millis();
|
||||
print_updates(&updates, icons);
|
||||
info!("✨ Checked {} images in {}ms", updates.len(), end - start);
|
||||
ctx.logger.info(format!("✨ Checked {} images in {}ms", updates.len(), start.elapsed().unwrap().as_millis()));
|
||||
}
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "server")]
|
||||
Some(Commands::Serve { port }) => {
|
||||
let _ = serve(port, &config).await;
|
||||
let _ = serve(port, &ctx).await;
|
||||
}
|
||||
None => (),
|
||||
None => error!("Whoops! It looks like you haven't specified a command to run! Try `cup help` to see available options."),
|
||||
}
|
||||
}
|
||||
|
||||