mirror of
https://github.com/sergi0g/cup.git
synced 2025-11-08 13:13:49 -05:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
875fcce0d5 | ||
|
|
f40af342ec | ||
|
|
88885aa1dd | ||
|
|
5867cb375f | ||
|
|
65b2bece03 | ||
|
|
6b15d8dfad | ||
|
|
5bf7269aca | ||
|
|
0136850200 | ||
|
|
2afce016f3 | ||
|
|
bc06c06cac | ||
|
|
bc86364e68 | ||
|
|
663ca64cd7 | ||
|
|
330b70752e | ||
|
|
0c9ad61a4d | ||
|
|
38bf187a4a | ||
|
|
2c120ffaff | ||
|
|
572ca8858a | ||
|
|
2c4f2a1e05 |
@@ -1 +0,0 @@
|
||||
rust 1.79.0
|
||||
51
CONTRIBUTING.md
Normal file
51
CONTRIBUTING.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# 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.
|
||||
|
||||
## 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
|
||||
|
||||
You're ready to go!
|
||||
|
||||
## Project architecture
|
||||
|
||||
Cup can be run in 2 modes: CLI and server.
|
||||
|
||||
All CLI specific functionality is located in `src/formatting.rs` and some other files in functions prefixed with `#[cfg(feature = "cli")]`.
|
||||
|
||||
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.
|
||||
|
||||
- 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`.
|
||||
|
||||
- If you make changes to the frontend, always remember to prefix your build command with the `build.sh` script which takes care of rebuilding the frontend. For example: `./build.sh cargo build -r`
|
||||
|
||||
- When adding new features to Cup (e.g. configuration options), make sure to update the documentation (located in `docs/`). Refer to other pages in the documentation, or to the [official docs](https://nextra.site) for any questions you may have. The docs use `pnpm` as their package manager.
|
||||
|
||||
- If you need help with finishing something (e.g. you've made some commits and need help with writing docs, you want some feedback about a design decision, etc.), you can open a draft PR and ask for help there.
|
||||
|
||||
## Submitting a PR
|
||||
|
||||
To have your changes included in Cup, you will need to create a pull request.
|
||||
|
||||
Before doing so, please make sure you have run `cargo clippy` and resolved all warnings related to your changes and have formatted your code with `cargo fmt`. This ensures Cup's codebase is consistent and uses good practices for code.
|
||||
|
||||
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!
|
||||
590
Cargo.lock
generated
590
Cargo.lock
generated
@@ -43,9 +43,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.14"
|
||||
version = "0.6.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
|
||||
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
@@ -64,38 +64,55 @@ checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.4"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
|
||||
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
|
||||
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.3"
|
||||
version = "3.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
|
||||
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
|
||||
|
||||
[[package]]
|
||||
name = "anymap2"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
@@ -128,18 +145,18 @@ dependencies = [
|
||||
"rustc-demangle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
@@ -155,7 +172,7 @@ version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0aed08d3adb6ebe0eff737115056652670ae290f177759aac19c30456135f94c"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"base64",
|
||||
"bollard-stubs",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
@@ -199,6 +216,12 @@ version = "3.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.6.0"
|
||||
@@ -272,9 +295,9 @@ checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
|
||||
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
@@ -304,40 +327,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
@@ -350,21 +339,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cup"
|
||||
version = "2.2.1"
|
||||
version = "2.4.0"
|
||||
dependencies = [
|
||||
"bollard",
|
||||
"chrono",
|
||||
"clap",
|
||||
"futures",
|
||||
"http-auth",
|
||||
"indicatif",
|
||||
"json",
|
||||
"liquid",
|
||||
"once_cell",
|
||||
"rayon",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"reqwest-middleware",
|
||||
"reqwest-retry",
|
||||
"rustc-hash",
|
||||
"termsize",
|
||||
"tokio",
|
||||
"ureq",
|
||||
"xitca-web",
|
||||
]
|
||||
|
||||
@@ -412,16 +404,6 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@@ -437,6 +419,21 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.30"
|
||||
@@ -444,6 +441,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -452,6 +450,23 @@ version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.30"
|
||||
@@ -481,9 +496,13 @@ version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
@@ -506,8 +525,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -572,13 +593,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "643c9bbf6a4ea8a656d6b4cd53d34f79e3f841ad5203c1a55fb7d761923bc255"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"digest",
|
||||
"hex",
|
||||
"md-5",
|
||||
"memchr",
|
||||
"rand",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -650,6 +665,24 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.6"
|
||||
@@ -760,13 +793,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.0"
|
||||
name = "ipnet"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
|
||||
checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
@@ -877,28 +919,34 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.7.4"
|
||||
@@ -981,6 +1029,31 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
|
||||
dependencies = [
|
||||
"instant",
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"instant",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
@@ -1078,9 +1151,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.17"
|
||||
version = "0.2.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
@@ -1091,6 +1167,54 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"rand",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"slab",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8bffec3605b73c6f1754535084a85229fa8a30f86014e6c81aeec4abb68b0285"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.36"
|
||||
@@ -1131,23 +1255,12 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.10.0"
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
|
||||
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1179,6 +1292,93 @@ version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest-middleware"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562ceb5a604d3f7c885a792d42c199fd8af239d0a51b2fa6a78aafa092452b04"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"http",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest-retry"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a83df1aaec00176d0fabb65dea13f832d2a446ca99107afc17c5d2d4981221d0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"futures",
|
||||
"getrandom",
|
||||
"http",
|
||||
"hyper",
|
||||
"parking_lot",
|
||||
"reqwest",
|
||||
"reqwest-middleware",
|
||||
"retry-policies",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"wasm-timer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "retry-policies"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5875471e6cab2871bc150ecb8c727db5113c9338cc3354dc5ee3425b6aa40a1c"
|
||||
dependencies = [
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.8"
|
||||
@@ -1201,12 +1401,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.10"
|
||||
name = "rustc-hash"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402"
|
||||
checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -1216,16 +1421,26 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.7.0"
|
||||
name = "rustls-pemfile"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
|
||||
checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.102.5"
|
||||
version = "0.102.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78"
|
||||
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -1239,19 +1454,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.204"
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.210"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.204"
|
||||
version = "1.0.210"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
|
||||
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1260,11 +1481,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.120"
|
||||
version = "1.0.128"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5"
|
||||
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
@@ -1298,7 +1520,7 @@ version = "3.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e73139bc5ec2d45e6c5fd85be5a46949c1c39a4c18e56915f5eb4c12f975e377"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"base64",
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap 1.9.3",
|
||||
@@ -1398,6 +1620,15 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termsize"
|
||||
version = "0.1.8"
|
||||
@@ -1514,6 +1745,17 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.11"
|
||||
@@ -1652,9 +1894,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.11.0"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
@@ -1668,22 +1910,6 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72139d247e5f97a3eff96229a7ae85ead5328a39efe76f8bf5a06313d505b6ea"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"flate2",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"url",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.2"
|
||||
@@ -1753,6 +1979,18 @@ dependencies = [
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.92"
|
||||
@@ -1783,10 +2021,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.3"
|
||||
name = "wasm-timer"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd"
|
||||
checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"js-sys",
|
||||
"parking_lot",
|
||||
"pin-utils",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
@@ -1822,6 +2085,36 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
|
||||
dependencies = [
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
||||
dependencies = [
|
||||
"windows-result",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
@@ -2048,6 +2341,27 @@ dependencies = [
|
||||
"xitca-unsafe-collection",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.1"
|
||||
|
||||
13
Cargo.toml
13
Cargo.toml
@@ -1,23 +1,26 @@
|
||||
[package]
|
||||
name = "cup"
|
||||
version = "2.2.1"
|
||||
version = "2.4.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.7", features = ["derive"] }
|
||||
indicatif = { version = "0.17.8", optional = true }
|
||||
tokio = {version = "1.38.0", features = ["rt", "rt-multi-thread", "macros"]}
|
||||
ureq = { version = "2.9.7", features = ["tls"] }
|
||||
rayon = "1.10.0"
|
||||
tokio = {version = "1.38.0", features = ["macros"]}
|
||||
xitca-web = { version = "0.5.0", optional = true, features = ["logger"] }
|
||||
liquid = { version = "0.26.6", optional = true }
|
||||
bollard = "0.16.1"
|
||||
once_cell = "1.19.0"
|
||||
http-auth = { version = "0.1.9", features = [] }
|
||||
http-auth = { version = "0.1.9", default-features = false, features = [] }
|
||||
termsize = { version = "0.1.8", optional = true }
|
||||
regex = "1.10.5"
|
||||
chrono = { version = "0.4.38", default-features = false, features = ["std", "alloc", "clock"], optional = true }
|
||||
json = "0.12.4"
|
||||
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"
|
||||
|
||||
[features]
|
||||
default = ["server", "cli"]
|
||||
|
||||
15
README.md
15
README.md
@@ -4,6 +4,8 @@ Cup is the easiest way to check for container image updates.
|
||||
|
||||

|
||||
|
||||
_If you like this project and/or use Cup, please consider starring the project ⭐. It motivates me to continue working on it and imrpoving it. Plus, you get updates for new releases!_
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
@@ -11,11 +13,11 @@ Cup is the easiest way to check for container image updates.
|
||||
|
||||
## Features
|
||||
|
||||
- Extremely fast. Cup takes full advantage of your CPU and is hightly optimized, resulting in lightning fast speed. On my test machine, it took ~6 seconds for 70 images.
|
||||
- Extremely fast. Cup takes full advantage of your CPU and is hightly optimized, resulting in lightning fast speed. On my Raspberry Pi 5, it took 3.7 seconds for 58 images!
|
||||
- Supports most registries, including Docker Hub, ghcr.io, Quay, lscr.io and even Gitea (or derivatives)
|
||||
- Doesn't exhaust any rate limits. This is the original reason I created Cup. It was inspired by [What's up docker?](https://github.com/fmartinou/whats-up-docker) which would always use it up.
|
||||
- Beautiful CLI and web interface for checking on your containers any time.
|
||||
- The binary is tiny! At the time of writing it's just 4.7 MB. No more pulling 100+ MB docker images for a such a simple program.
|
||||
- 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.
|
||||
- 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
|
||||
@@ -24,11 +26,10 @@ Take a look at https://sergi0g.github.io/cup/docs!
|
||||
|
||||
## Limitations
|
||||
|
||||
Cup is a work in progress. It might not have as many features as What's up Docker. If one of these features is really important for you, please consider using another tool.
|
||||
Cup 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 doesn't support registries which use repositories without slashes. This includes Azure. This problem may sound a bit weird, but it's due to the regex that's used at the moment. This will (hopefully) be fixed in the future.~~
|
||||
- ~~Cup doesn't support private images. This is on the roadmap. Currently, it just returns unknown for those images.~~
|
||||
- Cup cannot trigger your integrations. If you want that to happen automatically, please use What's up docker instead. Cup was created to be simple. The data is there, and it's up to you to retrieve it (e.g. by running `cup check -r` with a cronjob or periodically requesting the `/json` url from the server)
|
||||
- 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
|
||||
Take a sneak peek at what's coming up in future releases on the [roadmap](https://github.com/users/sergi0g/projects/2)!
|
||||
@@ -44,7 +45,7 @@ Here are some ideas to get you started:
|
||||
- Help optimize Cup and make it even better!
|
||||
- Add more features to the web UI
|
||||
|
||||
To contribute, fork the repository, make your changes and the submit a pull request.
|
||||
For more information, check the [docs](https://sergi0g.github.io/cup/docs/contributing)!
|
||||
|
||||
## Support
|
||||
|
||||
|
||||
6
build.sh
6
build.sh
@@ -1,7 +1,13 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Exit on error
|
||||
set -e
|
||||
|
||||
# This is kind of like a shim that makes sure the frontend is rebuilt when running a build. For example you can run `./build.sh cargo build --release`
|
||||
|
||||
# Remove old files
|
||||
rm -rf src/static
|
||||
|
||||
# Frontend
|
||||
cd web/
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
nodejs 21.6.2
|
||||
nodejs 22.8.0
|
||||
|
||||
BIN
docs/assets/350767810-42eccc89-bdfd-426a-a113-653abe7483d8.png
Normal file
BIN
docs/assets/350767810-42eccc89-bdfd-426a-a113-653abe7483d8.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 176 KiB |
BIN
docs/assets/358304960-e9f26767-51f7-4b5a-8b74-a5811019497b.jpeg
Normal file
BIN
docs/assets/358304960-e9f26767-51f7-4b5a-8b74-a5811019497b.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tabler/icons-react": "^3.11.0",
|
||||
"next": "^14.2.5",
|
||||
"next": "^14.2.10",
|
||||
"nextra": "^2.13.4",
|
||||
"nextra-theme-docs": "^2.13.4",
|
||||
"react": "^18.3.1",
|
||||
@@ -16,5 +16,6 @@
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.39",
|
||||
"tailwindcss": "^3.4.5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@9.10.0+sha512.73a29afa36a0d092ece5271de5177ecbf8318d454ecd701343131b8ebc0c1a91c487da46ab77c8e596d6acf1461e3594ced4becedf8921b074fbd8653ed7051c"
|
||||
}
|
||||
|
||||
@@ -11,7 +11,13 @@
|
||||
"usage": {
|
||||
"title": "Usage"
|
||||
},
|
||||
"community-resources": {
|
||||
"title": "Community Resources"
|
||||
},
|
||||
"nightly": {
|
||||
"title": "Using the latest version"
|
||||
},
|
||||
"contributing": {
|
||||
"title": "Contributing"
|
||||
}
|
||||
}
|
||||
23
docs/pages/docs/community-resources/docker-compose.mdx
Normal file
23
docs/pages/docs/community-resources/docker-compose.mdx
Normal file
@@ -0,0 +1,23 @@
|
||||
# Docker Compose
|
||||
|
||||
Many users find it useful to run Cup with Docker Compose, as it enables them to have it constantly running in the background and easily control it. Cup's lightweight resource usae makes it ideal for this use case.
|
||||
|
||||
There have been requests for an official Docker Compose file, but I believe you should customize it to your needs.
|
||||
|
||||
Here is an example of what I would use (by [@ioverho](https://github.com/ioverho)):
|
||||
|
||||
```yaml
|
||||
services:
|
||||
cup:
|
||||
image: ghcr.io/sergi0g/cup:latest
|
||||
container_name: cup # Optional
|
||||
restart: unless-stopped
|
||||
command: -c /config/cup.json serve
|
||||
ports:
|
||||
- 8000:8000
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./cup.json:/config/cup.json
|
||||
```
|
||||
|
||||
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!
|
||||
75
docs/pages/docs/community-resources/homepage-widget.mdx
Normal file
75
docs/pages/docs/community-resources/homepage-widget.mdx
Normal file
@@ -0,0 +1,75 @@
|
||||
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'
|
||||
|
||||
# Homepage Widget
|
||||
|
||||
Some users have asked for a homepage widget.
|
||||
|
||||
## Docker Compose with the widget configured via labels:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
cup:
|
||||
image: ghcr.io/sergi0g/cup
|
||||
container_name: cup
|
||||
command: -c /config/cup.json serve -p 8000
|
||||
volumes:
|
||||
- ./config/cup.json:/config/cup.json
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
ports:
|
||||
- 8000:8000
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
homepage.group: Network
|
||||
homepage.name: Cup
|
||||
homepage.icon: /icons/cup-with-straw.png
|
||||
homepage.href: http://myserver:8000
|
||||
homepage.ping: http://myserver:8000
|
||||
homepage.description: Checks for container updates
|
||||
homepage.widget.type: customapi
|
||||
homepage.widget.url: http://myserver:8000/json
|
||||
homepage.widget.mappings[0].label: Monitoring
|
||||
homepage.widget.mappings[0].field.metrics: monitored_images
|
||||
homepage.widget.mappings[0].format: number
|
||||
homepage.widget.mappings[1].label: Up to date
|
||||
homepage.widget.mappings[1].field.metrics: up_to_date
|
||||
homepage.widget.mappings[1].format: number
|
||||
homepage.widget.mappings[2].label: Updates
|
||||
homepage.widget.mappings[2].field.metrics: update_available
|
||||
homepage.widget.mappings[2].format: number
|
||||
```
|
||||
Preview:
|
||||
<Image src={widget1}/>
|
||||
|
||||
Credit: [@agrmohit](https://github.com/agrmohit)
|
||||
|
||||
## Widget in Homepage's config file format:
|
||||
|
||||
```yaml
|
||||
widget:
|
||||
type: customapi
|
||||
url: http://<SERVER_IP>:9000/json
|
||||
refreshInterval: 10000
|
||||
method: GET
|
||||
mappings:
|
||||
- field:
|
||||
metrics: monitored_images
|
||||
label: Monitored images
|
||||
format: number
|
||||
- field:
|
||||
metrics: up_to_date
|
||||
label: Up to date
|
||||
format: number
|
||||
- field:
|
||||
metrics: update_available
|
||||
label: Available updates
|
||||
format: number
|
||||
- field:
|
||||
metrics: unknown
|
||||
label: Unknown
|
||||
format: number
|
||||
```
|
||||
Preview:
|
||||
<Image src={widget2}/>
|
||||
Credit: [@remussamoila](https://github.com/remussamoila)
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Steps, Callout, Card, Cards } from "nextra-theme-docs";
|
||||
import { IconPaint, IconLockOpen, IconKey } from '@tabler/icons-react';
|
||||
import { IconPaint, IconLockOpen, IconKey, IconPlug } from '@tabler/icons-react';
|
||||
|
||||
# Configuration
|
||||
|
||||
@@ -13,7 +13,8 @@ For example, if using Podman, you might do
|
||||
$ cup -s /run/user/1000/podman/podman.sock check
|
||||
```
|
||||
|
||||
This option will hopefully be moved to the configuration file soon.
|
||||
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" />
|
||||
|
||||
## Configuration file
|
||||
|
||||
|
||||
10
docs/pages/docs/configuration/socket.mdx
Normal file
10
docs/pages/docs/configuration/socket.mdx
Normal file
@@ -0,0 +1,10 @@
|
||||
# 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
|
||||
}
|
||||
```
|
||||
51
docs/pages/docs/contributing.mdx
Normal file
51
docs/pages/docs/contributing.mdx
Normal file
@@ -0,0 +1,51 @@
|
||||
# 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.
|
||||
|
||||
## 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
|
||||
|
||||
You're ready to go!
|
||||
|
||||
## Project architecture
|
||||
|
||||
Cup can be run in 2 modes: CLI and server.
|
||||
|
||||
All CLI specific functionality is located in `src/formatting.rs` and some other files in functions prefixed with `#[cfg(feature = "cli")]`.
|
||||
|
||||
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.
|
||||
|
||||
- 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`.
|
||||
|
||||
- If you make changes to the frontend, always remember to prefix your build command with the `build.sh` script which takes care of rebuilding the frontend. For example: `./build.sh cargo build -r`
|
||||
|
||||
- When adding new features to Cup (e.g. configuration options), make sure to update the documentation (located in `docs/`). Refer to other pages in the documentation, or to the [official docs](https://nextra.site) for any questions you may have. The docs use `pnpm` as their package manager.
|
||||
|
||||
- If you need help with finishing something (e.g. you've made some commits and need help with writing docs, you want some feedback about a design decision, etc.), you can open a draft PR and ask for help there.
|
||||
|
||||
## Submitting a PR
|
||||
|
||||
To have your changes included in Cup, you will need to create a pull request.
|
||||
|
||||
Before doing so, please make sure you have run `cargo clippy` and resolved all warnings related to your changes and have formatted your code with `cargo fmt`. This ensures Cup's codebase is consistent and uses good practices for code.
|
||||
|
||||
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!
|
||||
@@ -20,12 +20,22 @@ 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 a specific image
|
||||
```
|
||||
### Check for updates to specific images
|
||||
```ansi
|
||||
$ cup check node:latest
|
||||
node:latest has an update available
|
||||
[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
|
||||
@@ -46,7 +56,7 @@ $ cup check -r
|
||||
Here is how it would look in Typescript:
|
||||
|
||||
```ts
|
||||
type CupData = {
|
||||
interface CupData {
|
||||
metrics: {
|
||||
monitored_images: number,
|
||||
up_to_date: number,
|
||||
|
||||
126
docs/pnpm-lock.yaml
generated
126
docs/pnpm-lock.yaml
generated
@@ -12,14 +12,14 @@ importers:
|
||||
specifier: ^3.11.0
|
||||
version: 3.11.0(react@18.3.1)
|
||||
next:
|
||||
specifier: ^14.2.5
|
||||
version: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
specifier: ^14.2.10
|
||||
version: 14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
nextra:
|
||||
specifier: ^2.13.4
|
||||
version: 2.13.4(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
version: 2.13.4(next@14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
nextra-theme-docs:
|
||||
specifier: ^2.13.4
|
||||
version: 2.13.4(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@2.13.4(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
version: 2.13.4(next@14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@2.13.4(next@14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react:
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.1
|
||||
@@ -175,59 +175,59 @@ packages:
|
||||
resolution: {integrity: sha512-lH8bYk2kqfbKsht/Gejd8K+y069ZXPHBfrlcj1ptS6xlJbHhncHxpFyy57W+PTuCcN+MPGVjs+3CiufG8EUrCQ==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@next/env@14.2.5':
|
||||
resolution: {integrity: sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==}
|
||||
'@next/env@14.2.10':
|
||||
resolution: {integrity: sha512-dZIu93Bf5LUtluBXIv4woQw2cZVZ2DJTjax5/5DOs3lzEOeKLy7GxRSr4caK9/SCPdaW6bCgpye6+n4Dh9oJPw==}
|
||||
|
||||
'@next/swc-darwin-arm64@14.2.5':
|
||||
resolution: {integrity: sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==}
|
||||
'@next/swc-darwin-arm64@14.2.10':
|
||||
resolution: {integrity: sha512-V3z10NV+cvMAfxQUMhKgfQnPbjw+Ew3cnr64b0lr8MDiBJs3eLnM6RpGC46nhfMZsiXgQngCJKWGTC/yDcgrDQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-x64@14.2.5':
|
||||
resolution: {integrity: sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==}
|
||||
'@next/swc-darwin-x64@14.2.10':
|
||||
resolution: {integrity: sha512-Y0TC+FXbFUQ2MQgimJ/7Ina2mXIKhE7F+GUe1SgnzRmwFY3hX2z8nyVCxE82I2RicspdkZnSWMn4oTjIKz4uzA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@14.2.5':
|
||||
resolution: {integrity: sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==}
|
||||
'@next/swc-linux-arm64-gnu@14.2.10':
|
||||
resolution: {integrity: sha512-ZfQ7yOy5zyskSj9rFpa0Yd7gkrBnJTkYVSya95hX3zeBG9E55Z6OTNPn1j2BTFWvOVVj65C3T+qsjOyVI9DQpA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-arm64-musl@14.2.5':
|
||||
resolution: {integrity: sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==}
|
||||
'@next/swc-linux-arm64-musl@14.2.10':
|
||||
resolution: {integrity: sha512-n2i5o3y2jpBfXFRxDREr342BGIQCJbdAUi/K4q6Env3aSx8erM9VuKXHw5KNROK9ejFSPf0LhoSkU/ZiNdacpQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-gnu@14.2.5':
|
||||
resolution: {integrity: sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==}
|
||||
'@next/swc-linux-x64-gnu@14.2.10':
|
||||
resolution: {integrity: sha512-GXvajAWh2woTT0GKEDlkVhFNxhJS/XdDmrVHrPOA83pLzlGPQnixqxD8u3bBB9oATBKB//5e4vpACnx5Vaxdqg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-musl@14.2.5':
|
||||
resolution: {integrity: sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==}
|
||||
'@next/swc-linux-x64-musl@14.2.10':
|
||||
resolution: {integrity: sha512-opFFN5B0SnO+HTz4Wq4HaylXGFV+iHrVxd3YvREUX9K+xfc4ePbRrxqOuPOFjtSuiVouwe6uLeDtabjEIbkmDA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@14.2.5':
|
||||
resolution: {integrity: sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==}
|
||||
'@next/swc-win32-arm64-msvc@14.2.10':
|
||||
resolution: {integrity: sha512-9NUzZuR8WiXTvv+EiU/MXdcQ1XUvFixbLIMNQiVHuzs7ZIFrJDLJDaOF1KaqttoTujpcxljM/RNAOmw1GhPPQQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-ia32-msvc@14.2.5':
|
||||
resolution: {integrity: sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==}
|
||||
'@next/swc-win32-ia32-msvc@14.2.10':
|
||||
resolution: {integrity: sha512-fr3aEbSd1GeW3YUMBkWAu4hcdjZ6g4NBl1uku4gAn661tcxd1bHs1THWYzdsbTRLcCKLjrDZlNp6j2HTfrw+Bg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-x64-msvc@14.2.5':
|
||||
resolution: {integrity: sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==}
|
||||
'@next/swc-win32-x64-msvc@14.2.10':
|
||||
resolution: {integrity: sha512-UjeVoRGKNL2zfbcQ6fscmgjBAS/inHBh63mjIlfPg/NG8Yn2ztqylXt5qilYb6hoHIwaU2ogHknHWWmahJjgZQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -1249,8 +1249,8 @@ packages:
|
||||
micromark@3.2.0:
|
||||
resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==}
|
||||
|
||||
micromatch@4.0.7:
|
||||
resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==}
|
||||
micromatch@4.0.8:
|
||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
minimatch@9.0.5:
|
||||
@@ -1297,8 +1297,8 @@ packages:
|
||||
react: '*'
|
||||
react-dom: '*'
|
||||
|
||||
next@14.2.5:
|
||||
resolution: {integrity: sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==}
|
||||
next@14.2.10:
|
||||
resolution: {integrity: sha512-sDDExXnh33cY3RkS9JuFEKaS4HmlWmDKP1VJioucCG6z5KuA008DPsDZOzi8UfqEk3Ii+2NCQSJrfbEWtZZfww==}
|
||||
engines: {node: '>=18.17.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -1991,33 +1991,33 @@ snapshots:
|
||||
'@napi-rs/simple-git-win32-arm64-msvc': 0.1.17
|
||||
'@napi-rs/simple-git-win32-x64-msvc': 0.1.17
|
||||
|
||||
'@next/env@14.2.5': {}
|
||||
'@next/env@14.2.10': {}
|
||||
|
||||
'@next/swc-darwin-arm64@14.2.5':
|
||||
'@next/swc-darwin-arm64@14.2.10':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@14.2.5':
|
||||
'@next/swc-darwin-x64@14.2.10':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@14.2.5':
|
||||
'@next/swc-linux-arm64-gnu@14.2.10':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@14.2.5':
|
||||
'@next/swc-linux-arm64-musl@14.2.10':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@14.2.5':
|
||||
'@next/swc-linux-x64-gnu@14.2.10':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@14.2.5':
|
||||
'@next/swc-linux-x64-musl@14.2.10':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@14.2.5':
|
||||
'@next/swc-win32-arm64-msvc@14.2.10':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-ia32-msvc@14.2.5':
|
||||
'@next/swc-win32-ia32-msvc@14.2.10':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@14.2.5':
|
||||
'@next/swc-win32-x64-msvc@14.2.10':
|
||||
optional: true
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
@@ -2572,7 +2572,7 @@ snapshots:
|
||||
'@nodelib/fs.walk': 1.2.8
|
||||
glob-parent: 5.1.2
|
||||
merge2: 1.4.1
|
||||
micromatch: 4.0.7
|
||||
micromatch: 4.0.8
|
||||
|
||||
fastq@1.17.1:
|
||||
dependencies:
|
||||
@@ -3371,7 +3371,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
micromatch@4.0.7:
|
||||
micromatch@4.0.8:
|
||||
dependencies:
|
||||
braces: 3.0.3
|
||||
picomatch: 2.3.1
|
||||
@@ -3405,21 +3405,21 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
next-seo@6.5.0(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
next-seo@6.5.0(next@14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
next: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
next: 14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
next-themes@0.2.1(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
next-themes@0.2.1(next@14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
next: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
next: 14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
next@14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@next/env': 14.2.5
|
||||
'@next/env': 14.2.10
|
||||
'@swc/helpers': 0.5.5
|
||||
busboy: 1.6.0
|
||||
caniuse-lite: 1.0.30001642
|
||||
@@ -3429,20 +3429,20 @@ snapshots:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
styled-jsx: 5.1.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 14.2.5
|
||||
'@next/swc-darwin-x64': 14.2.5
|
||||
'@next/swc-linux-arm64-gnu': 14.2.5
|
||||
'@next/swc-linux-arm64-musl': 14.2.5
|
||||
'@next/swc-linux-x64-gnu': 14.2.5
|
||||
'@next/swc-linux-x64-musl': 14.2.5
|
||||
'@next/swc-win32-arm64-msvc': 14.2.5
|
||||
'@next/swc-win32-ia32-msvc': 14.2.5
|
||||
'@next/swc-win32-x64-msvc': 14.2.5
|
||||
'@next/swc-darwin-arm64': 14.2.10
|
||||
'@next/swc-darwin-x64': 14.2.10
|
||||
'@next/swc-linux-arm64-gnu': 14.2.10
|
||||
'@next/swc-linux-arm64-musl': 14.2.10
|
||||
'@next/swc-linux-x64-gnu': 14.2.10
|
||||
'@next/swc-linux-x64-musl': 14.2.10
|
||||
'@next/swc-win32-arm64-msvc': 14.2.10
|
||||
'@next/swc-win32-ia32-msvc': 14.2.10
|
||||
'@next/swc-win32-x64-msvc': 14.2.10
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
nextra-theme-docs@2.13.4(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@2.13.4(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
nextra-theme-docs@2.13.4(next@14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@2.13.4(next@14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@headlessui/react': 1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@popperjs/core': 2.11.8
|
||||
@@ -3453,16 +3453,16 @@ snapshots:
|
||||
git-url-parse: 13.1.1
|
||||
intersection-observer: 0.12.2
|
||||
match-sorter: 6.3.4
|
||||
next: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
next-seo: 6.5.0(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
next-themes: 0.2.1(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
nextra: 2.13.4(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
next: 14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
next-seo: 6.5.0(next@14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
next-themes: 0.2.1(next@14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
nextra: 2.13.4(next@14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
scroll-into-view-if-needed: 3.1.0
|
||||
zod: 3.23.8
|
||||
|
||||
nextra@2.13.4(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
nextra@2.13.4(next@14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@headlessui/react': 1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@mdx-js/mdx': 2.3.0
|
||||
@@ -3476,7 +3476,7 @@ snapshots:
|
||||
gray-matter: 4.0.3
|
||||
katex: 0.16.11
|
||||
lodash.get: 4.4.2
|
||||
next: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
next: 14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
next-mdx-remote: 4.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
p-limit: 3.1.0
|
||||
react: 18.3.1
|
||||
@@ -3857,7 +3857,7 @@ snapshots:
|
||||
is-glob: 4.0.3
|
||||
jiti: 1.21.6
|
||||
lilconfig: 2.1.0
|
||||
micromatch: 4.0.7
|
||||
micromatch: 4.0.8
|
||||
normalize-path: 3.0.0
|
||||
object-hash: 3.0.0
|
||||
picocolors: 1.0.1
|
||||
|
||||
@@ -16,7 +16,7 @@ export default {
|
||||
const { asPath } = useRouter()
|
||||
const { frontMatter } = useConfig()
|
||||
const url =
|
||||
'https://sergi0g.github.io/cup' +
|
||||
'https://sergi0g.github.io/cup/docs/' +
|
||||
(`/${asPath}`);
|
||||
|
||||
return (
|
||||
@@ -36,9 +36,9 @@ export default {
|
||||
<h1 className="font-bold ml-2">Cup</h1>
|
||||
</div>
|
||||
),
|
||||
logoLink: "sergi0g.github.io/cup",
|
||||
logoLink: "https://sergi0g.github.io/cup/docs/",
|
||||
project: {
|
||||
link: "https://github.com/sergi0g/cup",
|
||||
link: "https://github.com/sergi0g/cup/",
|
||||
},
|
||||
navbar: {
|
||||
extraContent: <ThemeSwitch lite className="[&_span]:hidden" />,
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 90 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 90 KiB |
156
src/check.rs
156
src/check.rs
@@ -1,105 +1,95 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
use json::JsonValue;
|
||||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
||||
use futures::future::join_all;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
use crate::{
|
||||
docker::get_images_from_docker_daemon,
|
||||
image::Image,
|
||||
registry::{check_auth, get_latest_digests, get_token},
|
||||
utils::unsplit_image,
|
||||
config::Config, image::Image, registry::{check_auth, get_token}, utils::new_reqwest_client
|
||||
};
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
use crate::docker::get_image_from_docker_daemon;
|
||||
#[cfg(feature = "cli")]
|
||||
use crate::registry::get_latest_digest;
|
||||
|
||||
/// 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> {
|
||||
// So we can filter vecs for duplicates
|
||||
fn unique(&mut self);
|
||||
fn unique(&mut self) -> Vec<T>;
|
||||
}
|
||||
|
||||
impl<T> Unique<T> for Vec<T>
|
||||
where
|
||||
T: Clone + Eq + std::hash::Hash,
|
||||
{
|
||||
fn unique(self: &mut Vec<T>) {
|
||||
let mut seen: HashSet<T> = HashSet::new();
|
||||
/// 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()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_all_updates(
|
||||
socket: Option<String>,
|
||||
config: &JsonValue,
|
||||
) -> Vec<(String, Option<bool>)> {
|
||||
let image_map_mutex: Mutex<HashMap<String, &Option<String>>> = Mutex::new(HashMap::new());
|
||||
let local_images = get_images_from_docker_daemon(socket).await;
|
||||
local_images.par_iter().for_each(|image| {
|
||||
let img = unsplit_image(&image.registry, &image.repository, &image.tag);
|
||||
image_map_mutex.lock().unwrap().insert(img, &image.digest);
|
||||
});
|
||||
let image_map = image_map_mutex.lock().unwrap().clone();
|
||||
let mut registries: Vec<&String> = local_images
|
||||
.par_iter()
|
||||
.map(|image| &image.registry)
|
||||
.collect();
|
||||
registries.unique();
|
||||
let mut remote_images: Vec<Image> = Vec::new();
|
||||
for registry in registries {
|
||||
let images: Vec<&Image> = local_images
|
||||
.par_iter()
|
||||
.filter(|image| &image.registry == registry)
|
||||
.collect();
|
||||
let credentials = config["authentication"][registry]
|
||||
.clone()
|
||||
.take_string()
|
||||
.or(None);
|
||||
let mut latest_images = match check_auth(registry, config) {
|
||||
Some(auth_url) => {
|
||||
let token = get_token(images.clone(), &auth_url, &credentials);
|
||||
get_latest_digests(images, Some(&token), config)
|
||||
}
|
||||
None => get_latest_digests(images, None, config),
|
||||
};
|
||||
remote_images.append(&mut latest_images);
|
||||
/// Returns a list of updates for all images passed in.
|
||||
pub async fn get_updates(images: &[Image], config: &Config) -> Vec<(String, Option<bool>)> {
|
||||
// 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();
|
||||
|
||||
// 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();
|
||||
|
||||
// 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 {
|
||||
image_map
|
||||
.entry(image.registry.as_ref().unwrap())
|
||||
.or_default()
|
||||
.push(image);
|
||||
}
|
||||
let result_mutex: Mutex<Vec<(String, Option<bool>)>> = Mutex::new(Vec::new());
|
||||
remote_images.par_iter().for_each(|image| {
|
||||
let img = unsplit_image(&image.registry, &image.repository, &image.tag);
|
||||
match &image.digest {
|
||||
Some(d) => {
|
||||
let r = d != image_map.get(&img).unwrap().as_ref().unwrap();
|
||||
result_mutex.lock().unwrap().push((img, Some(r)))
|
||||
|
||||
// Retrieve an authentication token (if required) for each registry.
|
||||
let mut tokens: FxHashMap<&String, Option<String>> = FxHashMap::default();
|
||||
for registry in registries {
|
||||
let credentials = config.authentication.get(registry);
|
||||
match check_auth(registry, config, &client).await {
|
||||
Some(auth_url) => {
|
||||
let token = get_token(
|
||||
image_map.get(registry).unwrap(),
|
||||
&auth_url,
|
||||
&credentials,
|
||||
&client,
|
||||
)
|
||||
.await;
|
||||
tokens.insert(registry, Some(token));
|
||||
}
|
||||
None => {
|
||||
tokens.insert(registry, None);
|
||||
}
|
||||
None => result_mutex.lock().unwrap().push((img, None)),
|
||||
}
|
||||
});
|
||||
let result = result_mutex.lock().unwrap().clone();
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
pub async fn get_update(image: &str, socket: Option<String>, config: &JsonValue) -> Option<bool> {
|
||||
let local_image = get_image_from_docker_daemon(socket, image).await;
|
||||
let credentials = config["authentication"][&local_image.registry]
|
||||
.clone()
|
||||
.take_string()
|
||||
.or(None);
|
||||
let token = match check_auth(&local_image.registry, config) {
|
||||
Some(auth_url) => get_token(vec![&local_image], &auth_url, &credentials),
|
||||
None => String::new(),
|
||||
};
|
||||
let remote_image = match token.as_str() {
|
||||
"" => get_latest_digest(&local_image, None, config),
|
||||
_ => get_latest_digest(&local_image, Some(&token), config),
|
||||
};
|
||||
match &remote_image.digest {
|
||||
Some(d) => Some(d != &local_image.digest.unwrap()),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
130
src/config.rs
Normal file
130
src/config.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::error;
|
||||
|
||||
const VALID_KEYS: [&str; 4] = ["authentication", "theme", "insecure_registries", "socket"];
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Theme {
|
||||
Default,
|
||||
Blue,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Config {
|
||||
pub authentication: FxHashMap<String, String>,
|
||||
pub theme: Theme,
|
||||
pub insecure_registries: Vec<String>,
|
||||
pub socket: Option<String>,
|
||||
}
|
||||
|
||||
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(),
|
||||
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
|
||||
};
|
||||
if raw_config.is_err() {
|
||||
panic!(
|
||||
"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.
|
||||
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),
|
||||
};
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
128
src/docker.rs
128
src/docker.rs
@@ -1,9 +1,8 @@
|
||||
use bollard::{secret::ImageSummary, ClientVersion, Docker};
|
||||
use bollard::{models::ImageInspect, ClientVersion, Docker};
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
use bollard::secret::ImageInspect;
|
||||
use futures::future::join_all;
|
||||
|
||||
use crate::{error, image::Image, utils::split_image};
|
||||
use crate::{error, image::Image, config::Config};
|
||||
|
||||
fn create_docker_client(socket: Option<String>) -> Docker {
|
||||
let client: Result<Docker, bollard::errors::Error> = match socket {
|
||||
@@ -24,58 +23,79 @@ fn create_docker_client(socket: Option<String>) -> Docker {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_images_from_docker_daemon(socket: Option<String>) -> Vec<Image> {
|
||||
let client: Docker = create_docker_client(socket);
|
||||
let images: Vec<ImageSummary> = match client.list_images::<String>(None).await {
|
||||
Ok(images) => images,
|
||||
Err(e) => {
|
||||
error!("Failed to retrieve list of images available!\n{}", e)
|
||||
}
|
||||
};
|
||||
let mut result: Vec<Image> = Vec::new();
|
||||
for image in images {
|
||||
if !image.repo_tags.is_empty() && !image.repo_digests.is_empty() {
|
||||
for t in &image.repo_tags {
|
||||
let (registry, repository, tag) = split_image(t);
|
||||
result.push(Image {
|
||||
registry,
|
||||
repository,
|
||||
tag,
|
||||
digest: Some(
|
||||
image.repo_digests[0]
|
||||
.clone()
|
||||
.split('@')
|
||||
.collect::<Vec<&str>>()[1]
|
||||
.to_string(),
|
||||
),
|
||||
});
|
||||
/// 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,
|
||||
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()
|
||||
match references {
|
||||
Some(refs) => {
|
||||
let mut inspect_handles = Vec::with_capacity(refs.len());
|
||||
for reference in refs {
|
||||
inspect_handles.push(client.inspect_image(reference));
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
pub async fn get_image_from_docker_daemon(socket: Option<String>, name: &str) -> Image {
|
||||
let client: Docker = create_docker_client(socket);
|
||||
let image: ImageInspect = match client.inspect_image(name).await {
|
||||
Ok(i) => i,
|
||||
Err(e) => error!("Failed to retrieve image {} from daemon\n{}", name, e),
|
||||
};
|
||||
match image.repo_tags {
|
||||
Some(_) => (),
|
||||
None => error!("Image has no tags"), // I think this is actually unreachable
|
||||
}
|
||||
match image.repo_digests {
|
||||
Some(d) => {
|
||||
let (registry, repository, tag) = split_image(&image.repo_tags.unwrap()[0]);
|
||||
Image {
|
||||
registry,
|
||||
repository,
|
||||
tag,
|
||||
digest: Some(d[0].clone().split('@').collect::<Vec<&str>>()[1].to_string()),
|
||||
let inspects: Vec<ImageInspect> = join_all(inspect_handles)
|
||||
.await
|
||||
.iter()
|
||||
.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
|
||||
.iter()
|
||||
.filter_map(|img| img.clone())
|
||||
.collect()
|
||||
}
|
||||
None => {
|
||||
let images = match client.list_images::<String>(None).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_summary(image))
|
||||
}
|
||||
join_all(handles)
|
||||
.await
|
||||
.iter()
|
||||
.filter_map(|img| img.clone())
|
||||
.collect()
|
||||
}
|
||||
None => error!("No digests found for image {}", name),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use json::object;
|
||||
|
||||
use crate::utils::{sort_update_vec, to_json};
|
||||
|
||||
@@ -33,7 +32,7 @@ pub fn print_updates(updates: &[(String, Option<bool>)], icons: &bool) {
|
||||
let dynamic_space =
|
||||
" ".repeat(term_width - description.len() - icon.len() - update.0.len());
|
||||
println!(
|
||||
"{}{}{}{}{}",
|
||||
"{}{}{}{}{}\u{001b}[0m",
|
||||
color, icon, update.0, dynamic_space, description
|
||||
);
|
||||
}
|
||||
@@ -43,30 +42,12 @@ pub fn print_raw_updates(updates: &[(String, Option<bool>)]) {
|
||||
println!("{}", json::stringify(to_json(updates)));
|
||||
}
|
||||
|
||||
pub fn print_update(name: &str, has_update: &Option<bool>) {
|
||||
let color = match has_update {
|
||||
Some(true) => "\u{001b}[38;5;12m",
|
||||
Some(false) => "\u{001b}[38;5;2m",
|
||||
None => "\u{001b}[38;5;8m",
|
||||
};
|
||||
let description = match has_update {
|
||||
Some(true) => "has an update available",
|
||||
Some(false) => "is up to date",
|
||||
None => "wasn't found",
|
||||
};
|
||||
println!("{}{} {}", color, name, description);
|
||||
}
|
||||
|
||||
pub fn print_raw_update(name: &str, has_update: &Option<bool>) {
|
||||
let result = object! {images: {[name]: *has_update}};
|
||||
println!("{}", result);
|
||||
}
|
||||
|
||||
pub struct Spinner {
|
||||
spinner: ProgressBar,
|
||||
}
|
||||
|
||||
impl Spinner {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Spinner {
|
||||
let spinner = ProgressBar::new_spinner();
|
||||
let style: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||
|
||||
120
src/image.rs
120
src/image.rs
@@ -1,7 +1,119 @@
|
||||
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 registry: String,
|
||||
pub repository: String,
|
||||
pub tag: String,
|
||||
pub digest: Option<String>,
|
||||
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()
|
||||
});
|
||||
|
||||
64
src/main.rs
64
src/main.rs
@@ -1,14 +1,16 @@
|
||||
#[cfg(feature = "cli")]
|
||||
use check::{get_all_updates, get_update};
|
||||
use check::get_updates;
|
||||
use chrono::Local;
|
||||
use clap::{Parser, Subcommand};
|
||||
use config::Config;
|
||||
use docker::get_images_from_docker_daemon;
|
||||
#[cfg(feature = "cli")]
|
||||
use formatting::{print_raw_update, print_raw_updates, print_update, print_updates, Spinner};
|
||||
use formatting::{print_raw_updates, print_updates, Spinner};
|
||||
#[cfg(feature = "server")]
|
||||
use server::serve;
|
||||
use std::path::PathBuf;
|
||||
use utils::load_config;
|
||||
|
||||
pub mod check;
|
||||
pub mod config;
|
||||
pub mod docker;
|
||||
#[cfg(feature = "cli")]
|
||||
pub mod formatting;
|
||||
@@ -33,8 +35,8 @@ struct Cli {
|
||||
enum Commands {
|
||||
#[cfg(feature = "cli")]
|
||||
Check {
|
||||
#[arg(default_value = None)]
|
||||
image: Option<String>,
|
||||
#[arg(name = "Images", default_value = None)]
|
||||
references: Option<Vec<String>>,
|
||||
#[arg(short, long, default_value_t = false, help = "Enable icons")]
|
||||
icons: bool,
|
||||
#[arg(
|
||||
@@ -64,32 +66,38 @@ async fn main() {
|
||||
"" => None,
|
||||
path => Some(PathBuf::from(path)),
|
||||
};
|
||||
let config = load_config(cfg_path);
|
||||
let mut config = Config::new().load(cfg_path);
|
||||
match cli.socket {
|
||||
Some(socket) => config.socket = Some(socket),
|
||||
None => ()
|
||||
}
|
||||
match &cli.command {
|
||||
#[cfg(feature = "cli")]
|
||||
Some(Commands::Check { image, icons, raw }) => match image {
|
||||
Some(name) => {
|
||||
let has_update = get_update(name, cli.socket, &config).await;
|
||||
match raw {
|
||||
true => print_raw_update(name, &has_update),
|
||||
false => print_update(name, &has_update),
|
||||
};
|
||||
}
|
||||
None => {
|
||||
match raw {
|
||||
true => print_raw_updates(&get_all_updates(cli.socket, &config).await),
|
||||
false => {
|
||||
let spinner = Spinner::new();
|
||||
let updates = get_all_updates(cli.socket, &config).await;
|
||||
spinner.succeed();
|
||||
print_updates(&updates, icons);
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
Some(Commands::Check {
|
||||
references,
|
||||
icons,
|
||||
raw,
|
||||
}) => {
|
||||
let start = Local::now().timestamp_millis();
|
||||
let images = get_images_from_docker_daemon(&config, references).await;
|
||||
match raw {
|
||||
true => {
|
||||
let updates = get_updates(&images, &config).await;
|
||||
print_raw_updates(&updates);
|
||||
}
|
||||
false => {
|
||||
let spinner = Spinner::new();
|
||||
let updates = get_updates(&images, &config).await;
|
||||
spinner.succeed();
|
||||
let end = Local::now().timestamp_millis();
|
||||
print_updates(&updates, icons);
|
||||
info!("✨ Checked {} images in {}ms", updates.len(), end - start);
|
||||
}
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "server")]
|
||||
Some(Commands::Serve { port }) => {
|
||||
let _ = serve(port, cli.socket, config).await;
|
||||
let _ = serve(port, &config).await;
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
|
||||
220
src/registry.rs
220
src/registry.rs
@@ -1,160 +1,154 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use json::JsonValue;
|
||||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
||||
use ureq::{Error, ErrorKind};
|
||||
|
||||
use http_auth::parse_challenges;
|
||||
use reqwest_middleware::ClientWithMiddleware;
|
||||
|
||||
use crate::{error, image::Image, warn};
|
||||
use crate::{config::Config, error, image::Image, warn};
|
||||
|
||||
pub fn check_auth(registry: &str, config: &JsonValue) -> Option<String> {
|
||||
let protocol = if config["insecure_registries"].contains(registry) {
|
||||
pub async fn check_auth(
|
||||
registry: &str,
|
||||
config: &Config,
|
||||
client: &ClientWithMiddleware,
|
||||
) -> Option<String> {
|
||||
let protocol = if config.insecure_registries.contains(®istry.to_string()) {
|
||||
"http"
|
||||
} else {
|
||||
"https"
|
||||
};
|
||||
let response = ureq::get(&format!("{}://{}/v2/", protocol, registry)).call();
|
||||
let response = client
|
||||
.get(format!("{}://{}/v2/", protocol, registry))
|
||||
.send()
|
||||
.await;
|
||||
match response {
|
||||
Ok(_) => None,
|
||||
Err(Error::Status(401, response)) => match response.header("www-authenticate") {
|
||||
Some(challenge) => Some(parse_www_authenticate(challenge)),
|
||||
None => error!(
|
||||
"Unauthorized to access registry {} and no way to authenticate was provided",
|
||||
registry
|
||||
),
|
||||
},
|
||||
Err(Error::Transport(error)) => {
|
||||
match error.kind() {
|
||||
ErrorKind::Dns => {
|
||||
warn!("Failed to lookup the IP of the registry, retrying.");
|
||||
return check_auth(registry, config);
|
||||
} // If something goes really wrong, this can get stuck in a loop
|
||||
ErrorKind::ConnectionFailed => {
|
||||
warn!("Connection probably timed out, retrying.");
|
||||
return check_auth(registry, config);
|
||||
} // Same here
|
||||
_ => error!("{}", error),
|
||||
Ok(r) => {
|
||||
let status = r.status().as_u16();
|
||||
if status == 401 {
|
||||
match r.headers().get("www-authenticate") {
|
||||
Some(challenge) => Some(parse_www_authenticate(challenge.to_str().unwrap())),
|
||||
None => error!(
|
||||
"Unauthorized to access registry {} and no way to authenticate was provided",
|
||||
registry
|
||||
),
|
||||
}
|
||||
} else if status == 200 {
|
||||
None
|
||||
} else {
|
||||
warn!(
|
||||
"Received unexpected status code {}\nResponse: {}",
|
||||
status,
|
||||
r.text().await.unwrap()
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if e.is_connect() {
|
||||
warn!("Connection to registry {} failed.", ®istry);
|
||||
None
|
||||
} else {
|
||||
error!("Unexpected error: {}", e.to_string())
|
||||
}
|
||||
}
|
||||
Err(e) => error!("{}", e),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_latest_digest(image: &Image, token: Option<&String>, config: &JsonValue) -> Image {
|
||||
let protocol =
|
||||
if config["insecure_registries"].contains(json::JsonValue::from(image.registry.clone())) {
|
||||
"http"
|
||||
} else {
|
||||
"https"
|
||||
};
|
||||
let mut request = ureq::head(&format!(
|
||||
pub async fn get_latest_digest(
|
||||
image: &Image,
|
||||
token: Option<&String>,
|
||||
config: &Config,
|
||||
client: &ClientWithMiddleware,
|
||||
) -> Image {
|
||||
let protocol = if config.insecure_registries.contains(&image.registry.clone().unwrap())
|
||||
{
|
||||
"http"
|
||||
} else {
|
||||
"https"
|
||||
};
|
||||
let mut request = client.head(format!(
|
||||
"{}://{}/v2/{}/manifests/{}",
|
||||
protocol, &image.registry, &image.repository, &image.tag
|
||||
protocol,
|
||||
&image.registry.as_ref().unwrap(),
|
||||
&image.repository.as_ref().unwrap(),
|
||||
&image.tag.as_ref().unwrap()
|
||||
));
|
||||
if let Some(t) = token {
|
||||
request = request.set("Authorization", &format!("Bearer {}", t));
|
||||
request = request.header("Authorization", &format!("Bearer {}", t));
|
||||
}
|
||||
let raw_response = match request
|
||||
.set("Accept", "application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.index.v1+json")
|
||||
.call()
|
||||
.header("Accept", "application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.index.v1+json")
|
||||
.send().await
|
||||
{
|
||||
Ok(response) => response,
|
||||
Err(Error::Status(401, response)) => {
|
||||
if token.is_some() {
|
||||
warn!("Failed to authenticate to registry {} with given token!\n{}", &image.registry, token.unwrap());
|
||||
return Image { digest: None, ..image.clone() }
|
||||
Ok(response) => {
|
||||
let status = response.status();
|
||||
if status == 401 {
|
||||
if token.is_some() {
|
||||
warn!("Failed to authenticate to registry {} with given token!\n{}", &image.registry.as_ref().unwrap(), token.unwrap());
|
||||
} else {
|
||||
warn!("Registry requires authentication");
|
||||
}
|
||||
return Image { remote_digest: None, ..image.clone() }
|
||||
} else if status == 404 {
|
||||
warn!("Image {:?} not found", &image);
|
||||
return Image { remote_digest: None, ..image.clone() }
|
||||
} else {
|
||||
return get_latest_digest(
|
||||
image,
|
||||
Some(&get_token(
|
||||
vec![image],
|
||||
&parse_www_authenticate(response.header("www-authenticate").unwrap()),
|
||||
&None // I think?
|
||||
)),
|
||||
config
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(Error::Status(_, _)) => {
|
||||
return Image {
|
||||
digest: None,
|
||||
..image.clone()
|
||||
response
|
||||
}
|
||||
},
|
||||
Err(Error::Transport(error)) => {
|
||||
match error.kind() {
|
||||
ErrorKind::Dns => {
|
||||
warn!("Failed to lookup the IP of the registry, retrying.");
|
||||
return get_latest_digest(image, token, config)
|
||||
}, // If something goes really wrong, this can get stuck in a loop
|
||||
ErrorKind::ConnectionFailed => {
|
||||
warn!("Connection probably timed out, retrying.");
|
||||
return get_latest_digest(image, token, config)
|
||||
}, // Same here
|
||||
_ => error!("Failed to retrieve image digest\n{}!", error)
|
||||
Err(e) => {
|
||||
if e.is_connect() {
|
||||
warn!("Connection to registry failed.");
|
||||
return Image { remote_digest: None, ..image.clone() }
|
||||
} else {
|
||||
error!("Unexpected error: {}", e.to_string())
|
||||
}
|
||||
},
|
||||
};
|
||||
match raw_response.header("docker-content-digest") {
|
||||
match raw_response.headers().get("docker-content-digest") {
|
||||
Some(digest) => Image {
|
||||
digest: Some(digest.to_string()),
|
||||
remote_digest: Some(digest.to_str().unwrap().to_string()),
|
||||
..image.clone()
|
||||
},
|
||||
None => error!("Server returned invalid response! No docker-content-digest!"),
|
||||
None => error!(
|
||||
"Server returned invalid response! No docker-content-digest!\n{:#?}",
|
||||
raw_response
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_latest_digests(
|
||||
images: Vec<&Image>,
|
||||
token: Option<&String>,
|
||||
config: &JsonValue,
|
||||
) -> Vec<Image> {
|
||||
let result: Mutex<Vec<Image>> = Mutex::new(Vec::new());
|
||||
images.par_iter().for_each(|&image| {
|
||||
let digest = get_latest_digest(image, token, config).digest;
|
||||
result.lock().unwrap().push(Image {
|
||||
digest,
|
||||
..image.clone()
|
||||
});
|
||||
});
|
||||
let r = result.lock().unwrap().clone();
|
||||
r
|
||||
}
|
||||
|
||||
pub fn get_token(images: Vec<&Image>, auth_url: &str, credentials: &Option<String>) -> String {
|
||||
pub async fn get_token(
|
||||
images: &Vec<&Image>,
|
||||
auth_url: &str,
|
||||
credentials: &Option<&String>,
|
||||
client: &ClientWithMiddleware,
|
||||
) -> String {
|
||||
let mut final_url = auth_url.to_owned();
|
||||
for image in &images {
|
||||
final_url = format!("{}&scope=repository:{}:pull", final_url, image.repository);
|
||||
for image in images {
|
||||
final_url = format!(
|
||||
"{}&scope=repository:{}:pull",
|
||||
final_url,
|
||||
image.repository.as_ref().unwrap()
|
||||
);
|
||||
}
|
||||
let mut base_request =
|
||||
ureq::get(&final_url).set("Accept", "application/vnd.oci.image.index.v1+json"); // Seems to be unnecesarry. Will probably remove in the future
|
||||
let mut base_request = client
|
||||
.get(&final_url)
|
||||
.header("Accept", "application/vnd.oci.image.index.v1+json"); // Seems to be unnecessary. Will probably remove in the future
|
||||
base_request = match credentials {
|
||||
Some(creds) => base_request.set("Authorization", &format!("Basic {}", creds)),
|
||||
Some(creds) => base_request.header("Authorization", &format!("Basic {}", creds)),
|
||||
None => base_request,
|
||||
};
|
||||
let raw_response = match base_request.call() {
|
||||
Ok(response) => match response.into_string() {
|
||||
let raw_response = match base_request.send().await {
|
||||
Ok(response) => match response.text().await {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
error!("Failed to parse response into string!\n{}", e)
|
||||
}
|
||||
},
|
||||
Err(Error::Transport(error)) => {
|
||||
match error.kind() {
|
||||
ErrorKind::Dns => {
|
||||
warn!("Failed to lookup the IP of the registry, retrying.");
|
||||
return get_token(images, auth_url, credentials);
|
||||
} // If something goes really wrong, this can get stuck in a loop
|
||||
ErrorKind::ConnectionFailed => {
|
||||
warn!("Connection probably timed out, retrying.");
|
||||
return get_token(images, auth_url, credentials);
|
||||
} // Same here
|
||||
_ => error!("Token request failed\n{}!", error),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Token request failed!\n{}", e)
|
||||
if e.is_connect() {
|
||||
error!("Connection to registry failed.");
|
||||
} else {
|
||||
error!("Token request failed!\n{}", e.to_string())
|
||||
}
|
||||
}
|
||||
};
|
||||
let parsed_token_response: JsonValue = match json::parse(&raw_response) {
|
||||
|
||||
@@ -14,9 +14,7 @@ use xitca_web::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
check::get_all_updates,
|
||||
error,
|
||||
utils::{sort_update_vec, to_json},
|
||||
check::get_updates, config::{Config, Theme}, docker::get_images_from_docker_daemon, info, utils::{sort_update_vec, to_json}
|
||||
};
|
||||
|
||||
const HTML: &str = include_str!("static/index.html");
|
||||
@@ -26,9 +24,10 @@ const FAVICON_ICO: &[u8] = include_bytes!("static/favicon.ico");
|
||||
const FAVICON_SVG: &[u8] = include_bytes!("static/favicon.svg");
|
||||
const APPLE_TOUCH_ICON: &[u8] = include_bytes!("static/apple-touch-icon.png");
|
||||
|
||||
pub async fn serve(port: &u16, socket: Option<String>, config: JsonValue) -> std::io::Result<()> {
|
||||
println!("Starting server, please wait...");
|
||||
let data = ServerData::new(socket, config).await;
|
||||
pub async fn serve(port: &u16, config: &Config) -> std::io::Result<()> {
|
||||
info!("Starting server, please wait...");
|
||||
let data = ServerData::new(config).await;
|
||||
info!("Ready to start!");
|
||||
App::new()
|
||||
.with_state(Arc::new(Mutex::new(data)))
|
||||
.at("/", get(handler_service(_static)))
|
||||
@@ -93,31 +92,34 @@ struct ServerData {
|
||||
template: String,
|
||||
raw_updates: Vec<(String, Option<bool>)>,
|
||||
json: JsonValue,
|
||||
socket: Option<String>,
|
||||
config: JsonValue,
|
||||
config: Config,
|
||||
theme: &'static str,
|
||||
}
|
||||
|
||||
impl ServerData {
|
||||
async fn new(socket: Option<String>, config: JsonValue) -> Self {
|
||||
async fn new(config: &Config) -> Self {
|
||||
let mut s = Self {
|
||||
socket,
|
||||
config: config.clone(),
|
||||
template: String::new(),
|
||||
json: json::object! {
|
||||
metrics: json::object! {},
|
||||
images: json::object! {},
|
||||
},
|
||||
raw_updates: Vec::new(),
|
||||
config,
|
||||
theme: "neutral",
|
||||
};
|
||||
s.refresh().await;
|
||||
s
|
||||
}
|
||||
async fn refresh(&mut self) {
|
||||
let updates = sort_update_vec(
|
||||
&get_all_updates(self.socket.clone(), &self.config["authentication"]).await,
|
||||
);
|
||||
let start = Local::now().timestamp_millis();
|
||||
if !self.raw_updates.is_empty() {
|
||||
info!("Refreshing data");
|
||||
}
|
||||
let images = get_images_from_docker_daemon(&self.config, &None).await;
|
||||
let updates = sort_update_vec(&get_updates(&images, &self.config).await);
|
||||
let end = Local::now().timestamp_millis();
|
||||
info!("✨ Checked {} images in {}ms", updates.len(), end - start);
|
||||
self.raw_updates = updates;
|
||||
let template = liquid::ParserBuilder::with_stdlib()
|
||||
.build()
|
||||
@@ -134,17 +136,13 @@ impl ServerData {
|
||||
.collect::<Vec<Object>>();
|
||||
self.json = to_json(&self.raw_updates);
|
||||
let last_updated = Local::now();
|
||||
self.json["last_updated"] = last_updated.to_rfc3339_opts(chrono::SecondsFormat::Secs, true).to_string().into();
|
||||
self.theme = match &self.config["theme"].as_str() {
|
||||
Some(t) => match *t {
|
||||
"default" => "neutral",
|
||||
"blue" => "gray",
|
||||
_ => error!(
|
||||
"Invalid theme {} specified! Please choose between 'default' and 'blue'",
|
||||
t
|
||||
),
|
||||
},
|
||||
None => "neutral",
|
||||
self.json["last_updated"] = last_updated
|
||||
.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
|
||||
.to_string()
|
||||
.into();
|
||||
self.theme = match &self.config.theme {
|
||||
Theme::Default => "neutral",
|
||||
Theme::Blue => "gray"
|
||||
};
|
||||
let globals = object!({
|
||||
"metrics": [{"name": "Monitored images", "value": self.json["metrics"]["monitored_images"].as_usize()}, {"name": "Up to date", "value": self.json["metrics"]["up_to_date"].as_usize()}, {"name": "Updates available", "value": self.json["metrics"]["update_available"].as_usize()}, {"name": "Unknown", "value": self.json["metrics"]["unknown"].as_usize()}],
|
||||
|
||||
150
src/utils.rs
150
src/utils.rs
@@ -1,81 +1,6 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use json::{object, JsonValue};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
/// This macro is an alternative to panic. It prints the message you give it and exits the process with code 1, without printing a stack trace. Useful for when the program has to exit due to a user error or something unexpected which is unrelated to the program (e.g. a failed web request)
|
||||
#[macro_export]
|
||||
macro_rules! error {
|
||||
($($arg:tt)*) => ({
|
||||
eprintln!($($arg)*);
|
||||
std::process::exit(1);
|
||||
})
|
||||
}
|
||||
|
||||
// A small macro to print in yellow as a warning
|
||||
#[macro_export]
|
||||
macro_rules! warn {
|
||||
($($arg:tt)*) => ({
|
||||
eprintln!("\x1b[93m{}\x1b[0m", format!($($arg)*));
|
||||
})
|
||||
}
|
||||
|
||||
/// Takes an image and splits it into registry, repository and tag. For example ghcr.io/sergi0g/cup:latest becomes ['ghcr.io', 'sergi0g/cup', 'latest'].
|
||||
pub fn split_image(image: &str) -> (String, String, String) {
|
||||
static RE: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(
|
||||
r#"^(?P<name>(?:(?P<registry>(?:(?:localhost|[\w-]+(?:\.[\w-]+)+)(?::\d+)?)|[\w]+:\d+)/)?(?P<repository>[a-z0-9_.-]+(?:/[a-z0-9_.-]+)*))(?::(?P<tag>[\w][\w.-]{0,127}))?$"#, // From https://regex101.com/r/nmSDPA/1
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
match RE.captures(image) {
|
||||
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 {}", image),
|
||||
},
|
||||
match c.name("tag") {
|
||||
Some(tag) => tag.as_str().to_owned(),
|
||||
None => String::from("latest"),
|
||||
},
|
||||
);
|
||||
}
|
||||
None => error!("Failed to parse image {}", image),
|
||||
}
|
||||
}
|
||||
|
||||
/// Given an image's parts which were previously created by split_image, recreate a reference that docker would use. This means removing the registry part, if it's Docker Hub and removing "library" if the image is official
|
||||
pub fn unsplit_image(registry: &str, repository: &str, tag: &str) -> String {
|
||||
let reg = match registry {
|
||||
"registry-1.docker.io" => String::new(),
|
||||
r => format!("{}/", r),
|
||||
};
|
||||
let repo = match repository.split('/').collect::<Vec<&str>>()[0] {
|
||||
"library" => {
|
||||
if reg.is_empty() {
|
||||
repository.strip_prefix("library/").unwrap()
|
||||
} else {
|
||||
repository
|
||||
}
|
||||
}
|
||||
_ => repository,
|
||||
};
|
||||
format!("{}{}:{}", reg, repo, tag)
|
||||
}
|
||||
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
|
||||
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
|
||||
|
||||
/// Sorts the update vector alphabetically and where Some(true) > Some(false) > None
|
||||
pub fn sort_update_vec(updates: &[(String, Option<bool>)]) -> Vec<(String, Option<bool>)> {
|
||||
@@ -95,24 +20,6 @@ pub fn sort_update_vec(updates: &[(String, Option<bool>)]) -> Vec<(String, Optio
|
||||
sorted_updates.to_vec()
|
||||
}
|
||||
|
||||
/// Tries to load the config from the path provided and perform basic validation
|
||||
pub fn load_config(config_path: Option<PathBuf>) -> JsonValue {
|
||||
let raw_config = match &config_path {
|
||||
Some(path) => std::fs::read_to_string(path),
|
||||
None => Ok(String::from("{\"theme\":\"default\"}")),
|
||||
};
|
||||
if raw_config.is_err() {
|
||||
panic!(
|
||||
"Failed to read config file from {}. Are you sure the file exists?",
|
||||
&config_path.unwrap().to_str().unwrap()
|
||||
)
|
||||
};
|
||||
match json::parse(&raw_config.unwrap()) {
|
||||
Ok(v) => v,
|
||||
Err(e) => panic!("Failed to parse config!\n{}", e),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_json(updates: &[(String, Option<bool>)]) -> JsonValue {
|
||||
let mut json_data: JsonValue = object! {
|
||||
metrics: object! {},
|
||||
@@ -124,21 +31,56 @@ pub fn to_json(updates: &[(String, Option<bool>)]) -> JsonValue {
|
||||
let up_to_date = updates
|
||||
.iter()
|
||||
.filter(|&(_, value)| *value == Some(false))
|
||||
.collect::<Vec<&(String, Option<bool>)>>()
|
||||
.len();
|
||||
.count();
|
||||
let update_available = updates
|
||||
.iter()
|
||||
.filter(|&(_, value)| *value == Some(true))
|
||||
.collect::<Vec<&(String, Option<bool>)>>()
|
||||
.len();
|
||||
let unknown = updates
|
||||
.iter()
|
||||
.filter(|&(_, value)| value.is_none())
|
||||
.collect::<Vec<&(String, Option<bool>)>>()
|
||||
.len();
|
||||
.count();
|
||||
let unknown = updates.iter().filter(|&(_, value)| value.is_none()).count();
|
||||
let _ = json_data["metrics"].insert("monitored_images", updates.len());
|
||||
let _ = json_data["metrics"].insert("up_to_date", up_to_date);
|
||||
let _ = json_data["metrics"].insert("update_available", update_available);
|
||||
let _ = json_data["metrics"].insert("unknown", unknown);
|
||||
json_data
|
||||
}
|
||||
|
||||
// Logging
|
||||
|
||||
/// This macro is an alternative to panic. It prints the message you give it and exits the process with code 1, without printing a stack trace. Useful for when the program has to exit due to a user error or something unexpected which is unrelated to the program (e.g. a failed web request)
|
||||
#[macro_export]
|
||||
macro_rules! error {
|
||||
($($arg:tt)*) => ({
|
||||
eprintln!("\x1b[38:5:204mERROR \x1b[0m {}", format!($($arg)*));
|
||||
std::process::exit(1);
|
||||
})
|
||||
}
|
||||
|
||||
// A small macro to print in yellow as a warning
|
||||
#[macro_export]
|
||||
macro_rules! warn {
|
||||
($($arg:tt)*) => ({
|
||||
eprintln!("\x1b[38:5:192mWARN \x1b[0m {}", format!($($arg)*));
|
||||
})
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! info {
|
||||
($($arg:tt)*) => ({
|
||||
println!("\x1b[38:5:86mINFO \x1b[0m {}", format!($($arg)*));
|
||||
})
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! debug {
|
||||
($($arg:tt)*) => ({
|
||||
println!("\x1b[38:5:63mDEBUG \x1b[0m {}", format!($($arg)*));
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_reqwest_client() -> ClientWithMiddleware {
|
||||
ClientBuilder::new(reqwest::Client::new())
|
||||
.with(RetryTransientMiddleware::new_with_policy(
|
||||
ExponentialBackoff::builder().build_with_max_retries(3),
|
||||
))
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
nodejs 21.6.2
|
||||
nodejs 22.8.0
|
||||
|
||||
BIN
web/bun.lockb
BIN
web/bun.lockb
Binary file not shown.
@@ -89,7 +89,7 @@
|
||||
class="lg:grid-cols-4 grid-cols-2 gap-1 grid overflow-hidden *:relative"
|
||||
>
|
||||
{% for metric in metrics %}
|
||||
<div class="gi">
|
||||
<div class="before:bg-{{ theme }}-200 before:dark:bg-{{ theme }}-800 after:bg-{{ theme }}-200 after:dark:bg-{{ theme }}-800 gi">
|
||||
<div
|
||||
class="xl:px-8 px-6 py-4 gap-y-2 gap-x-4 justify-between align-baseline flex flex-col h-full"
|
||||
>
|
||||
|
||||
@@ -11,10 +11,14 @@
|
||||
"fmt": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@tabler/icons-react": "^3.14.0",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
"react-dom": "^18.3.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.0",
|
||||
|
||||
@@ -1,33 +1,18 @@
|
||||
import { MouseEvent, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import Logo from "./components/Logo";
|
||||
import Statistic from "./components/Statistic";
|
||||
import Image from "./components/Image";
|
||||
import { LastChecked } from "./components/LastChecked";
|
||||
import Loading from "./components/Loading";
|
||||
import { Data } from "./types";
|
||||
import { theme } from "./theme";
|
||||
import RefreshButton from "./components/RefreshButton";
|
||||
import Search from "./components/Search";
|
||||
|
||||
function App() {
|
||||
const [data, setData] = useState<Data | null>(null);
|
||||
const theme = "neutral"; // Stupid, I know but I want both the dev and prod to work easily.
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
if (!data) return <Loading onLoad={setData} />;
|
||||
const refresh = (event: MouseEvent) => {
|
||||
const btn = event.currentTarget as HTMLButtonElement;
|
||||
btn.disabled = true;
|
||||
|
||||
let request = new XMLHttpRequest();
|
||||
request.onload = () => {
|
||||
if (request.status === 200) {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
request.open(
|
||||
"GET",
|
||||
process.env.NODE_ENV === "production"
|
||||
? "/refresh"
|
||||
: `http://${window.location.hostname}:8000/refresh`
|
||||
);
|
||||
request.send();
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={`flex justify-center min-h-screen bg-${theme}-50 dark:bg-${theme}-950`}
|
||||
@@ -56,29 +41,13 @@ function App() {
|
||||
className={`flex justify-between items-center px-6 py-4 text-${theme}-500`}
|
||||
>
|
||||
<LastChecked datetime={data.last_updated} />
|
||||
<button className="group" onClick={refresh}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
className="group-disabled:animate-spin"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M4,11A8.1,8.1 0 0 1 19.5,9M20,5v4h-4" />
|
||||
<path d="M20,13A8.1,8.1 0 0 1 4.5,15M4,19v-4h4" />
|
||||
</svg>
|
||||
</button>
|
||||
<RefreshButton />
|
||||
</div>
|
||||
<Search onChange={setSearchQuery}/>
|
||||
<ul
|
||||
className={`*:py-4 *:px-6 *:flex *:items-center *:gap-3 dark:divide-${theme}-800 divide-y dark:text-white`}
|
||||
>
|
||||
{Object.entries(data.images).map(([name, status]) => (
|
||||
{Object.entries(data.images).filter(([name]) => name.includes(searchQuery)).map(([name, status]) => (
|
||||
<Image name={name} status={status} key={name} />
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
IconCube,
|
||||
IconHelpCircleFilled,
|
||||
} from "@tabler/icons-react";
|
||||
import { WithTooltip } from "./Tooltip";
|
||||
|
||||
export default function Image({
|
||||
name,
|
||||
@@ -17,13 +18,28 @@ export default function Image({
|
||||
<IconCube className="size-6 shrink-0" />
|
||||
{name}
|
||||
{status == false && (
|
||||
<IconCircleCheckFilled className="text-green-500 ml-auto size-6 shrink-0" />
|
||||
<WithTooltip
|
||||
text="Up to date"
|
||||
className="text-green-500 ml-auto size-6 shrink-0"
|
||||
>
|
||||
<IconCircleCheckFilled />
|
||||
</WithTooltip>
|
||||
)}
|
||||
{status == true && (
|
||||
<IconCircleArrowUpFilled className="text-blue-500 ml-auto size-6 shrink-0" />
|
||||
<WithTooltip
|
||||
text="Update available"
|
||||
className="text-blue-500 ml-auto size-6 shrink-0"
|
||||
>
|
||||
<IconCircleArrowUpFilled />
|
||||
</WithTooltip>
|
||||
)}
|
||||
{status == null && (
|
||||
<IconHelpCircleFilled className="text-gray-500 ml-auto size-6 shrink-0" />
|
||||
<WithTooltip
|
||||
text="Unknown"
|
||||
className="text-gray-500 ml-auto size-6 shrink-0"
|
||||
>
|
||||
<IconHelpCircleFilled />
|
||||
</WithTooltip>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { IconLoader2 } from "@tabler/icons-react";
|
||||
import { Data } from "../types";
|
||||
import Logo from "./Logo";
|
||||
import { theme } from "../theme";
|
||||
|
||||
export default function Loading({ onLoad }: { onLoad: (data: Data) => void }) {
|
||||
const theme = "neutral";
|
||||
fetch(
|
||||
process.env.NODE_ENV === "production"
|
||||
? "/json"
|
||||
: `http://${window.location.hostname}:8000/json`,
|
||||
).then((response) => response.json().then((data) => onLoad(data)));
|
||||
).then((response) => response.json().then((data) => {onLoad(data as Data)}));
|
||||
return (
|
||||
<div
|
||||
className={`flex justify-center min-h-screen bg-${theme}-50 dark:bg-${theme}-950`}
|
||||
|
||||
45
web/src/components/RefreshButton.tsx
Normal file
45
web/src/components/RefreshButton.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { MouseEvent } from "react";
|
||||
import { WithTooltip } from "./Tooltip";
|
||||
|
||||
export default function RefreshButton() {
|
||||
const refresh = (event: MouseEvent) => {
|
||||
const btn = event.currentTarget as HTMLButtonElement;
|
||||
btn.disabled = true;
|
||||
|
||||
const request = new XMLHttpRequest();
|
||||
request.onload = () => {
|
||||
if (request.status === 200) {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
request.open(
|
||||
"GET",
|
||||
process.env.NODE_ENV === "production"
|
||||
? "/refresh"
|
||||
: `http://${window.location.hostname}:8000/refresh`,
|
||||
);
|
||||
request.send();
|
||||
};
|
||||
return (
|
||||
<WithTooltip text="Reload">
|
||||
<button className="group" onClick={refresh}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="group-disabled:animate-spin"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M4,11A8.1,8.1 0 0 1 19.5,9M20,5v4h-4" />
|
||||
<path d="M20,13A8.1,8.1 0 0 1 4.5,15M4,19v-4h4" />
|
||||
</svg>
|
||||
</button>
|
||||
</WithTooltip>
|
||||
);
|
||||
}
|
||||
51
web/src/components/Search.tsx
Normal file
51
web/src/components/Search.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ChangeEvent, useState } from "react";
|
||||
import { theme } from "../theme";
|
||||
import { IconSearch, IconX } from "@tabler/icons-react";
|
||||
|
||||
export default function Search({
|
||||
onChange,
|
||||
}: {
|
||||
onChange: (value: string) => void;
|
||||
}) {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [showClear, setShowClear] = useState(false);
|
||||
const handleChange = (event: ChangeEvent) => {
|
||||
const value = (event.target as HTMLInputElement).value;
|
||||
setSearchQuery(value);
|
||||
onChange(value);
|
||||
if (value !== "") {
|
||||
setShowClear(true);
|
||||
} else setShowClear(false);
|
||||
};
|
||||
const handleClear = () => {
|
||||
setShowClear(false);
|
||||
setSearchQuery("");
|
||||
onChange("");
|
||||
};
|
||||
return (
|
||||
<div className={`w-full px-6 text-${theme}-500`}>
|
||||
<div
|
||||
className={`flex items-center w-full rounded-md border border-${theme}-300 dark:border-${theme}-700 px-2 gap-1 bg-${theme}-200 dark:bg-${theme}-800 flex-nowrap peer`}
|
||||
>
|
||||
<IconSearch className="size-5" />
|
||||
<div className="w-full">
|
||||
<input
|
||||
className={`w-full h-10 text-sm text-${theme}-600 dark:text-${theme}-400 focus:outline-none peer bg-transparent placeholder:text-${theme}-500`}
|
||||
placeholder="Search"
|
||||
onChange={handleChange}
|
||||
value={searchQuery}
|
||||
></input>
|
||||
</div>
|
||||
{showClear && (
|
||||
<button onClick={handleClear} className={`hover:text-${theme}-600 dark:hover:text-${theme}-400`}>
|
||||
<IconX className="size-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="relative -translate-y-[8px] h-[8px] border-b-blue-600 border-b-2 w-0 peer-has-[:focus]:w-full transition-all duration-200 rounded-md left-1/2 -translate-x-1/2"
|
||||
style={{ clipPath: "inset(calc(100% - 2px) 0 0 0)" }}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
IconEyeFilled,
|
||||
IconHelpCircleFilled,
|
||||
} from "@tabler/icons-react";
|
||||
import { theme } from "../theme";
|
||||
|
||||
export default function Statistic({
|
||||
name,
|
||||
@@ -12,7 +13,6 @@ export default function Statistic({
|
||||
name: string;
|
||||
value: number;
|
||||
}) {
|
||||
const theme = "neutral";
|
||||
name = name.replaceAll("_", " ");
|
||||
name = name.slice(0, 1).toUpperCase() + name.slice(1); // Capitalize name
|
||||
return (
|
||||
|
||||
46
web/src/components/Tooltip.tsx
Normal file
46
web/src/components/Tooltip.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Provider, Root, Trigger, Content } from "@radix-ui/react-tooltip";
|
||||
import { cn } from "../utils";
|
||||
import { forwardRef, ReactNode } from "react";
|
||||
import { theme } from "../theme";
|
||||
|
||||
const TooltipContent = forwardRef<
|
||||
React.ElementRef<typeof Content>,
|
||||
React.ComponentPropsWithoutRef<typeof Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
`z-50 overflow-hidden rounded-md border border-${theme}-200 bg-white px-3 py-1.5 text-sm text-${theme}-950 shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-${theme}-800 dark:bg-${theme}-950 dark:text-${theme}-50`,
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
TooltipContent.displayName = Content.displayName;
|
||||
|
||||
const WithTooltip = ({
|
||||
children,
|
||||
text,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
text: string;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<Provider>
|
||||
<Root>
|
||||
<Trigger className={className} asChild>
|
||||
{children}
|
||||
</Trigger>
|
||||
<TooltipContent>
|
||||
<p className="text-black dark:text-white">{text}</p>
|
||||
</TooltipContent>
|
||||
</Root>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { WithTooltip };
|
||||
1
web/src/theme.ts
Normal file
1
web/src/theme.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const theme = "neutral"; // Will be modified by server at runtime
|
||||
@@ -1,12 +1,10 @@
|
||||
export type Data = {
|
||||
export interface Data {
|
||||
metrics: {
|
||||
monitored_images: number;
|
||||
up_to_date: number;
|
||||
update_available: number;
|
||||
unknown: number;
|
||||
};
|
||||
images: {
|
||||
[key: string]: boolean | null;
|
||||
};
|
||||
images: Record<string, boolean | null>;
|
||||
last_updated: string;
|
||||
};
|
||||
|
||||
6
web/src/utils.ts
Normal file
6
web/src/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -4,7 +4,7 @@ export default {
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
safelist: [
|
||||
// Generate minimum extra CSS
|
||||
{
|
||||
@@ -20,18 +20,30 @@ export default {
|
||||
},
|
||||
{
|
||||
pattern: /bg-(gray|neutral)-800/,
|
||||
variants: ["before:dark", "after:dark"],
|
||||
variants: ["before:dark", "after:dark", "dark"],
|
||||
},
|
||||
{
|
||||
pattern: /text-(gray|neutral)-600/,
|
||||
variants: ["hover"]
|
||||
},
|
||||
{
|
||||
pattern: /text-(gray|neutral)-400/,
|
||||
variants: ["hover", "dark", "dark:hover"]
|
||||
},
|
||||
{
|
||||
pattern: /text-(gray|neutral)-500/,
|
||||
variants: ["dark"],
|
||||
variants: ["dark", "placeholder"],
|
||||
},
|
||||
{
|
||||
pattern: /divide-(gray|neutral)-800/,
|
||||
variants: ["dark"],
|
||||
},
|
||||
{
|
||||
pattern: /border-(gray|neutral)-300/,
|
||||
},
|
||||
{
|
||||
pattern: /border-(gray|neutral)-700/,
|
||||
variants: ["dark"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -5,7 +5,8 @@ import react from "@vitejs/plugin-react-swc";
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
rollupOptions: { // https://stackoverflow.com/q/69614671/vite-without-hash-in-filename#75344943
|
||||
rollupOptions: {
|
||||
// https://stackoverflow.com/q/69614671/vite-without-hash-in-filename#75344943
|
||||
output: {
|
||||
entryFileNames: `assets/[name].js`,
|
||||
chunkFileNames: `assets/[name].js`,
|
||||
|
||||
Reference in New Issue
Block a user