mirror of
https://github.com/sergi0g/cup.git
synced 2025-11-08 13:13:49 -05:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5867cb375f | ||
|
|
65b2bece03 | ||
|
|
6b15d8dfad | ||
|
|
5bf7269aca | ||
|
|
0136850200 | ||
|
|
2afce016f3 | ||
|
|
bc06c06cac | ||
|
|
bc86364e68 | ||
|
|
663ca64cd7 | ||
|
|
330b70752e | ||
|
|
0c9ad61a4d |
@@ -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!
|
||||
574
Cargo.lock
generated
574
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,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cup"
|
||||
version = "2.2.2"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"bollard",
|
||||
"chrono",
|
||||
"clap",
|
||||
"futures",
|
||||
"http-auth",
|
||||
"indicatif",
|
||||
"json",
|
||||
"liquid",
|
||||
"once_cell",
|
||||
"rayon",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"reqwest-middleware",
|
||||
"reqwest-retry",
|
||||
"termsize",
|
||||
"tokio",
|
||||
"ureq",
|
||||
"xitca-web",
|
||||
]
|
||||
|
||||
@@ -412,16 +403,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 +418,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 +440,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -452,6 +449,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 +495,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 +524,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 +592,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 +664,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 +792,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 +918,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 +1028,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 +1150,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 +1166,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 +1254,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 +1291,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 +1400,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 +1420,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",
|
||||
@@ -1238,6 +1452,12 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.204"
|
||||
@@ -1298,7 +1518,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 +1618,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 +1743,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 +1892,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 +1908,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 +1977,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 +2019,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 +2083,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 +2339,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"
|
||||
|
||||
12
Cargo.toml
12
Cargo.toml
@@ -1,23 +1,25 @@
|
||||
[package]
|
||||
name = "cup"
|
||||
version = "2.2.2"
|
||||
version = "2.3.1"
|
||||
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"
|
||||
|
||||
[features]
|
||||
default = ["server", "cli"]
|
||||
|
||||
@@ -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 test machine, it took ~12 seconds for ~95 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.1 MB. No more pulling 100+ MB docker images for a such a simple program.
|
||||
- JSON output for both the CLI and web interface so you can connect Cup to integrations. It's easy to parse and makes webhooks and pretty dashboards simple to set up!
|
||||
|
||||
## Documentation
|
||||
@@ -44,7 +46,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
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
nodejs 21.6.2
|
||||
nodejs 22.8.0
|
||||
|
||||
@@ -16,5 +16,6 @@
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.39",
|
||||
"tailwindcss": "^3.4.5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@9.10.0+sha512.73a29afa36a0d092ece5271de5177ecbf8318d454ecd701343131b8ebc0c1a91c487da46ab77c8e596d6acf1461e3594ced4becedf8921b074fbd8653ed7051c"
|
||||
}
|
||||
|
||||
@@ -16,5 +16,8 @@
|
||||
},
|
||||
"nightly": {
|
||||
"title": "Using the latest version"
|
||||
},
|
||||
"contributing": {
|
||||
"title": "Contributing"
|
||||
}
|
||||
}
|
||||
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!
|
||||
87
src/check.rs
87
src/check.rs
@@ -1,16 +1,11 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
use json::JsonValue;
|
||||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::{
|
||||
debug,
|
||||
docker::get_images_from_docker_daemon,
|
||||
image::Image,
|
||||
registry::{check_auth, get_latest_digests, get_token},
|
||||
utils::unsplit_image,
|
||||
utils::{new_reqwest_client, unsplit_image, CliConfig},
|
||||
};
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
@@ -33,70 +28,76 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
pub async fn get_all_updates(options: &CliConfig) -> Vec<(String, Option<bool>)> {
|
||||
let local_images = get_images_from_docker_daemon(options).await;
|
||||
let mut image_map: HashMap<String, Option<String>> = HashMap::with_capacity(local_images.len());
|
||||
for image in &local_images {
|
||||
let img = unsplit_image(image);
|
||||
image_map.insert(img, image.digest.clone());
|
||||
}
|
||||
let mut registries: Vec<&String> = local_images.iter().map(|image| &image.registry).collect();
|
||||
registries.unique();
|
||||
let mut remote_images: Vec<Image> = Vec::new();
|
||||
let mut remote_images: Vec<Image> = Vec::with_capacity(local_images.len());
|
||||
let client = new_reqwest_client();
|
||||
for registry in registries {
|
||||
if options.verbose {
|
||||
debug!("Checking images from registry {}", registry)
|
||||
}
|
||||
let images: Vec<&Image> = local_images
|
||||
.par_iter()
|
||||
.iter()
|
||||
.filter(|image| &image.registry == registry)
|
||||
.collect();
|
||||
let credentials = config["authentication"][registry]
|
||||
let credentials = options.config["authentication"][registry]
|
||||
.clone()
|
||||
.take_string()
|
||||
.or(None);
|
||||
let mut latest_images = match check_auth(registry, config) {
|
||||
let mut latest_images = match check_auth(registry, options, &client).await {
|
||||
Some(auth_url) => {
|
||||
let token = get_token(images.clone(), &auth_url, &credentials);
|
||||
get_latest_digests(images, Some(&token), config)
|
||||
let token = get_token(images.clone(), &auth_url, &credentials, &client).await;
|
||||
if options.verbose {
|
||||
debug!("Using token {}", token);
|
||||
}
|
||||
get_latest_digests(images, Some(&token), options, &client).await
|
||||
}
|
||||
None => get_latest_digests(images, None, config),
|
||||
None => get_latest_digests(images, None, options, &client).await,
|
||||
};
|
||||
remote_images.append(&mut latest_images);
|
||||
}
|
||||
let result_mutex: Mutex<Vec<(String, Option<bool>)>> = Mutex::new(Vec::new());
|
||||
remote_images.par_iter().for_each(|image| {
|
||||
let img = unsplit_image(&image.registry, &image.repository, &image.tag);
|
||||
if options.verbose {
|
||||
debug!("Collecting results")
|
||||
}
|
||||
let mut result: Vec<(String, Option<bool>)> = Vec::new();
|
||||
remote_images.iter().for_each(|image| {
|
||||
let img = unsplit_image(image);
|
||||
match &image.digest {
|
||||
Some(d) => {
|
||||
let r = d != image_map.get(&img).unwrap().as_ref().unwrap();
|
||||
result_mutex.lock().unwrap().push((img, Some(r)))
|
||||
result.push((img, Some(r)))
|
||||
}
|
||||
None => result_mutex.lock().unwrap().push((img, None)),
|
||||
None => result.push((img, None)),
|
||||
}
|
||||
});
|
||||
let result = result_mutex.lock().unwrap().clone();
|
||||
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]
|
||||
pub async fn get_update(image: &str, options: &CliConfig) -> Option<bool> {
|
||||
let local_image = get_image_from_docker_daemon(options.socket.clone(), image).await;
|
||||
let credentials = options.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),
|
||||
let client = new_reqwest_client();
|
||||
let token = match check_auth(&local_image.registry, options, &client).await {
|
||||
Some(auth_url) => get_token(vec![&local_image], &auth_url, &credentials, &client).await,
|
||||
None => String::new(),
|
||||
};
|
||||
if options.verbose {
|
||||
debug!("Using token {}", token);
|
||||
};
|
||||
let remote_image = match token.as_str() {
|
||||
"" => get_latest_digest(&local_image, None, config),
|
||||
_ => get_latest_digest(&local_image, Some(&token), config),
|
||||
"" => get_latest_digest(&local_image, None, options, &client).await,
|
||||
_ => get_latest_digest(&local_image, Some(&token), options, &client).await,
|
||||
};
|
||||
match &remote_image.digest {
|
||||
Some(d) => Some(d != &local_image.digest.unwrap()),
|
||||
|
||||
@@ -2,8 +2,13 @@ use bollard::{secret::ImageSummary, 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,
|
||||
utils::{split_image, CliConfig},
|
||||
};
|
||||
|
||||
fn create_docker_client(socket: Option<String>) -> Docker {
|
||||
let client: Result<Docker, bollard::errors::Error> = match socket {
|
||||
@@ -24,35 +29,24 @@ 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);
|
||||
pub async fn get_images_from_docker_daemon(options: &CliConfig) -> Vec<Image> {
|
||||
let client: Docker = create_docker_client(options.socket.clone());
|
||||
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();
|
||||
let mut handles = 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(),
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
handles.push(Image::from(image, options))
|
||||
}
|
||||
result
|
||||
join_all(handles)
|
||||
.await
|
||||
.iter()
|
||||
.filter(|img| img.is_some())
|
||||
.map(|img| img.clone().unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
|
||||
34
src/image.rs
34
src/image.rs
@@ -1,3 +1,10 @@
|
||||
use bollard::secret::ImageSummary;
|
||||
|
||||
use crate::{
|
||||
debug,
|
||||
utils::{split_image, CliConfig},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Image {
|
||||
pub registry: String,
|
||||
@@ -5,3 +12,30 @@ pub struct Image {
|
||||
pub tag: String,
|
||||
pub digest: Option<String>,
|
||||
}
|
||||
|
||||
impl Image {
|
||||
pub async fn from(image: ImageSummary, options: &CliConfig) -> Option<Self> {
|
||||
if !image.repo_tags.is_empty() && !image.repo_digests.is_empty() {
|
||||
let (registry, repository, tag) = split_image(&image.repo_tags[0]);
|
||||
let image = Image {
|
||||
registry,
|
||||
repository,
|
||||
tag,
|
||||
digest: Some(
|
||||
image.repo_digests[0]
|
||||
.clone()
|
||||
.split('@')
|
||||
.collect::<Vec<&str>>()[1]
|
||||
.to_string(),
|
||||
),
|
||||
};
|
||||
return Some(image);
|
||||
} else if options.verbose {
|
||||
debug!(
|
||||
"Skipped an image\nTags: {:#?}\nDigests: {:#?}",
|
||||
image.repo_tags, image.repo_digests
|
||||
)
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
38
src/main.rs
38
src/main.rs
@@ -1,12 +1,13 @@
|
||||
#[cfg(feature = "cli")]
|
||||
use check::{get_all_updates, get_update};
|
||||
use chrono::Local;
|
||||
use clap::{Parser, Subcommand};
|
||||
#[cfg(feature = "cli")]
|
||||
use formatting::{print_raw_update, print_raw_updates, print_update, print_updates, Spinner};
|
||||
#[cfg(feature = "server")]
|
||||
use server::serve;
|
||||
use std::path::PathBuf;
|
||||
use utils::load_config;
|
||||
use utils::{load_config, CliConfig};
|
||||
|
||||
pub mod check;
|
||||
pub mod docker;
|
||||
@@ -25,6 +26,13 @@ struct Cli {
|
||||
socket: Option<String>,
|
||||
#[arg(short, long, default_value_t = String::new(), help = "Config file path")]
|
||||
config_path: String,
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
default_value_t = false,
|
||||
help = "Enable verbose (debug) logging"
|
||||
)]
|
||||
verbose: bool,
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
}
|
||||
@@ -64,32 +72,50 @@ async fn main() {
|
||||
"" => None,
|
||||
path => Some(PathBuf::from(path)),
|
||||
};
|
||||
let config = load_config(cfg_path);
|
||||
if cli.verbose {
|
||||
debug!("CLI options:");
|
||||
debug!("Config path: {:?}", cfg_path);
|
||||
debug!("Socket: {:?}", &cli.socket)
|
||||
}
|
||||
let cli_config = CliConfig {
|
||||
socket: cli.socket,
|
||||
verbose: cli.verbose,
|
||||
config: load_config(cfg_path),
|
||||
};
|
||||
if cli.verbose {
|
||||
debug!("Config: {}", cli_config.config)
|
||||
}
|
||||
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;
|
||||
let has_update = get_update(name, &cli_config).await;
|
||||
match raw {
|
||||
true => print_raw_update(name, &has_update),
|
||||
false => print_update(name, &has_update),
|
||||
};
|
||||
}
|
||||
None => {
|
||||
let start = Local::now().timestamp_millis();
|
||||
match raw {
|
||||
true => print_raw_updates(&get_all_updates(cli.socket, &config).await),
|
||||
true => {
|
||||
let updates = get_all_updates(&cli_config).await;
|
||||
print_raw_updates(&updates);
|
||||
}
|
||||
false => {
|
||||
let spinner = Spinner::new();
|
||||
let updates = get_all_updates(cli.socket, &config).await;
|
||||
let updates = get_all_updates(&cli_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, &cli_config).await;
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
|
||||
216
src/registry.rs
216
src/registry.rs
@@ -1,160 +1,168 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use futures::future::join_all;
|
||||
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::{debug, error, image::Image, utils::CliConfig, 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,
|
||||
options: &CliConfig,
|
||||
client: &ClientWithMiddleware,
|
||||
) -> Option<String> {
|
||||
let protocol = if options.config["insecure_registries"].contains(registry) {
|
||||
if options.verbose {
|
||||
debug!(
|
||||
"{} is configured as an insecure registry. Downgrading to HTTP",
|
||||
registry
|
||||
);
|
||||
};
|
||||
"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>,
|
||||
options: &CliConfig,
|
||||
client: &ClientWithMiddleware,
|
||||
) -> Image {
|
||||
let protocol = if options.config["insecure_registries"]
|
||||
.contains(json::JsonValue::from(image.registry.clone()))
|
||||
{
|
||||
"http"
|
||||
} else {
|
||||
"https"
|
||||
};
|
||||
let mut request = client.head(format!(
|
||||
"{}://{}/v2/{}/manifests/{}",
|
||||
protocol, &image.registry, &image.repository, &image.tag
|
||||
));
|
||||
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());
|
||||
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, token.unwrap());
|
||||
} else {
|
||||
warn!("Registry requires authentication");
|
||||
}
|
||||
return Image { digest: None, ..image.clone() }
|
||||
} else if status == 404 {
|
||||
warn!("Image {:?} not found", &image);
|
||||
return Image { 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 { 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()),
|
||||
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(
|
||||
pub async fn get_latest_digests(
|
||||
images: Vec<&Image>,
|
||||
token: Option<&String>,
|
||||
config: &JsonValue,
|
||||
options: &CliConfig,
|
||||
client: &ClientWithMiddleware,
|
||||
) -> 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
|
||||
let mut handles = Vec::new();
|
||||
for image in images {
|
||||
handles.push(get_latest_digest(image, token, options, client))
|
||||
}
|
||||
join_all(handles).await
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -15,8 +15,8 @@ use xitca_web::{
|
||||
|
||||
use crate::{
|
||||
check::get_all_updates,
|
||||
error,
|
||||
utils::{sort_update_vec, to_json},
|
||||
error, info,
|
||||
utils::{sort_update_vec, to_json, CliConfig},
|
||||
};
|
||||
|
||||
const HTML: &str = include_str!("static/index.html");
|
||||
@@ -26,9 +26,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, options: &CliConfig) -> std::io::Result<()> {
|
||||
info!("Starting server, please wait...");
|
||||
let data = ServerData::new(options).await;
|
||||
info!("Ready to start!");
|
||||
App::new()
|
||||
.with_state(Arc::new(Mutex::new(data)))
|
||||
.at("/", get(handler_service(_static)))
|
||||
@@ -93,31 +94,33 @@ struct ServerData {
|
||||
template: String,
|
||||
raw_updates: Vec<(String, Option<bool>)>,
|
||||
json: JsonValue,
|
||||
socket: Option<String>,
|
||||
config: JsonValue,
|
||||
options: CliConfig,
|
||||
theme: &'static str,
|
||||
}
|
||||
|
||||
impl ServerData {
|
||||
async fn new(socket: Option<String>, config: JsonValue) -> Self {
|
||||
async fn new(options: &CliConfig) -> Self {
|
||||
let mut s = Self {
|
||||
socket,
|
||||
options: options.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 updates = sort_update_vec(&get_all_updates(&self.options).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,8 +137,11 @@ 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() {
|
||||
self.json["last_updated"] = last_updated
|
||||
.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
|
||||
.to_string()
|
||||
.into();
|
||||
self.theme = match &self.options.config["theme"].as_str() {
|
||||
Some(t) => match *t {
|
||||
"default" => "neutral",
|
||||
"blue" => "gray",
|
||||
|
||||
106
src/utils.rs
106
src/utils.rs
@@ -1,34 +1,21 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::{error, image::Image};
|
||||
use json::{object, JsonValue};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
|
||||
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
|
||||
|
||||
/// 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)*));
|
||||
})
|
||||
}
|
||||
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()
|
||||
});
|
||||
|
||||
/// 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") {
|
||||
@@ -59,22 +46,22 @@ pub fn split_image(image: &str) -> (String, String, String) {
|
||||
}
|
||||
|
||||
/// Given an image's parts which were previously created by split_image, recreate a reference that docker would use. This means removing the registry part, if it's Docker Hub and removing "library" if the image is official
|
||||
pub fn unsplit_image(registry: &str, repository: &str, tag: &str) -> String {
|
||||
let reg = match registry {
|
||||
pub fn unsplit_image(image: &Image) -> String {
|
||||
let reg = match image.registry.as_str() {
|
||||
"registry-1.docker.io" => String::new(),
|
||||
r => format!("{}/", r),
|
||||
};
|
||||
let repo = match repository.split('/').collect::<Vec<&str>>()[0] {
|
||||
let repo = match image.repository.split('/').collect::<Vec<&str>>()[0] {
|
||||
"library" => {
|
||||
if reg.is_empty() {
|
||||
repository.strip_prefix("library/").unwrap()
|
||||
image.repository.strip_prefix("library/").unwrap()
|
||||
} else {
|
||||
repository
|
||||
image.repository.as_str()
|
||||
}
|
||||
}
|
||||
_ => repository,
|
||||
_ => image.repository.as_str(),
|
||||
};
|
||||
format!("{}{}:{}", reg, repo, tag)
|
||||
format!("{}{}:{}", reg, repo, image.tag)
|
||||
}
|
||||
|
||||
/// Sorts the update vector alphabetically and where Some(true) > Some(false) > None
|
||||
@@ -124,21 +111,64 @@ 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
|
||||
}
|
||||
|
||||
/// Struct to hold some config values to avoid having to pass them all the time
|
||||
#[derive(Clone)]
|
||||
pub struct CliConfig {
|
||||
pub socket: Option<String>,
|
||||
pub verbose: bool,
|
||||
pub config: JsonValue,
|
||||
}
|
||||
|
||||
// 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[41m ERROR \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[103m WARN \x1b[0m {}", format!($($arg)*));
|
||||
})
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! info {
|
||||
($($arg:tt)*) => ({
|
||||
println!("\x1b[44m INFO \x1b[0m {}", format!($($arg)*));
|
||||
})
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! debug {
|
||||
($($arg:tt)*) => ({
|
||||
println!("\x1b[48:5:57m DEBUG \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
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user