m/cup
1
0
mirror of https://github.com/sergi0g/cup.git synced 2025-11-17 17:43:37 -05:00

9 Commits

Author SHA1 Message Date
Sergio
6dc1030a3b chore: bump project version 2025-05-21 19:30:30 +03:00
Sergio
d2c1651761 docs: update automatic refresh docs to mention TZ configuration 2025-05-21 11:39:41 +03:00
Sergio
8b3cf73f65 docs: add docs for environment variables 2025-05-21 11:34:16 +03:00
Sergio
6d88036914 feat: add timezone support 2025-05-21 11:13:27 +03:00
Sergio
a06266264d refactor: avoid a clone if extra images are empty 2025-05-20 17:46:09 +03:00
Sergio
c260874459 feat: add support for configuring through environment variables 2025-05-20 17:03:36 +03:00
Sergio
3e42ac338a fix: errors in http.rs 2025-05-10 20:55:14 +03:00
Sergio
15784eb4f1 fix: errors in http.rs 2025-05-10 20:52:04 +03:00
Sergio
2623f52a20 fix: handle 502 gracefully
Fixes #104
2025-05-10 20:48:22 +03:00
8 changed files with 198 additions and 19 deletions

85
Cargo.lock generated
View File

@@ -260,6 +260,27 @@ dependencies = [
"windows-link",
]
[[package]]
name = "chrono-tz"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efdce149c370f133a071ca8ef6ea340b7b88748ab0810097a9e2976eaa34b4f3"
dependencies = [
"chrono",
"chrono-tz-build",
"phf",
]
[[package]]
name = "chrono-tz-build"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f10f8c9340e31fc120ff885fcdb54a0b48e474bbd77cab557f0c30a3e569402"
dependencies = [
"parse-zoneinfo",
"phf_codegen",
]
[[package]]
name = "clap"
version = "4.5.31"
@@ -359,7 +380,9 @@ version = "3.3.0"
dependencies = [
"bollard",
"chrono",
"chrono-tz",
"clap",
"envy",
"futures",
"http-auth",
"http-link",
@@ -423,6 +446,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
name = "envy"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965"
dependencies = [
"serde",
]
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -1192,6 +1224,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "parse-zoneinfo"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24"
dependencies = [
"regex",
]
[[package]]
name = "percent-encoding"
version = "2.3.1"
@@ -1243,6 +1284,44 @@ dependencies = [
"sha2",
]
[[package]]
name = "phf"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
dependencies = [
"phf_shared",
]
[[package]]
name = "phf_codegen"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
dependencies = [
"phf_generator",
"phf_shared",
]
[[package]]
name = "phf_generator"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared",
"rand",
]
[[package]]
name = "phf_shared"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
dependencies = [
"siphasher",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
@@ -1688,6 +1767,12 @@ dependencies = [
"libc",
]
[[package]]
name = "siphasher"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "slab"
version = "0.4.9"

View File

@@ -1,6 +1,6 @@
[package]
name = "cup"
version = "3.3.0"
version = "3.4.0"
edition = "2021"
[dependencies]
@@ -25,6 +25,8 @@ itertools = "0.14.0"
serde_json = "1.0.133"
serde = "1.0.215"
tokio-cron-scheduler = { version = "0.13.0", default-features = false, optional = true }
envy = "0.4.2"
chrono-tz = "0.10.3"
[features]
default = ["server", "cli"]

View File

@@ -1,3 +1,5 @@
import { Callout } from "nextra/components";
# Automatic refresh
Cup can automatically refresh the results when running in server mode. Simply add this to your config:
@@ -9,4 +11,8 @@ Cup can automatically refresh the results when running in server mode. Simply ad
}
```
You can use a cron expression to specify the refresh interval. Note that seconds are not optional. The reference is [here](https://github.com/Hexagon/croner-rust#pattern)
You can use a cron expression to specify the refresh interval. Note that seconds are not optional. The reference is [here](https://github.com/Hexagon/croner-rust#pattern).
<Callout>
If you use a schedule with absolute time (e.g. every day at 6 AM), make sure to set the `TZ` environment variable to your [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List).
</Callout>

View File

@@ -109,3 +109,36 @@ $ docker run -tv /var/run/docker.sock:/var/run/docker.sock -v /home/sergio/.conf
```
</Steps>
## Environment Variables
Want to make a quick change without editing your `config.json`? Cup also supports some configuration options from environment variables.
Here are the ones currently available:
- `CUP_AGENT` - Agent mode
- `CUP_IGNORE_UPDATE_TYPE` - Ignoring specific update types
- `CUP_REFRESH_INTERVAL` - Automatic refresh
- `CUP_SOCKET` - Socket
- `CUP_THEME` - Theme
Refer to the configuration page for more information on each of these.
Here's an example of a Docker Compose file using them:
```yaml
services:
cup:
image: ghcr.io/sergi0g/cup:latest
command: serve
ports:
- 8000:8000
environment:
- CUP_AGENT: true
- CUP_IGNORE_UPDATE_TYPE: major
- CUP_REFRESH_INTERVAL: "0 */30 * * * *"
- CUP_SOCKET: tcp://localhost:2375
- CUP_THEME: blue
```
<Callout>
Heads up!
Any configuration option you set with environment variables **always** overrides anything in your `cup.json`.
</Callout>

View File

@@ -90,7 +90,9 @@ pub async fn get_updates(
// Merge references argument with references from config
let all_references = match &references {
Some(refs) => {
refs.clone().extend_from_slice(&ctx.config.images.extra);
if !ctx.config.images.extra.is_empty() {
refs.clone().extend_from_slice(&ctx.config.images.extra);
}
refs
}
None => &ctx.config.images.extra,
@@ -100,7 +102,8 @@ pub async fn get_updates(
ctx.logger.debug("Retrieving images to be checked");
let mut images = get_images_from_docker_daemon(ctx, references).await;
let in_use_images = get_in_use_images(ctx).await;
ctx.logger.debug(format!("Found {} images in use", in_use_images.len()));
ctx.logger
.debug(format!("Found {} images in use", in_use_images.len()));
// Complete in_use field
images.iter_mut().for_each(|image| {
@@ -204,10 +207,7 @@ pub async fn get_updates(
}
// Await all the futures
let images = join_all(handles).await;
let mut updates: Vec<Update> = images
.iter()
.map(|image| image.to_update())
.collect();
let mut updates: Vec<Update> = images.iter().map(|image| image.to_update()).collect();
updates.extend_from_slice(&remote_updates);
updates
}

View File

@@ -1,10 +1,18 @@
use std::path::PathBuf;
use rustc_hash::FxHashMap;
use serde::Deserialize;
use std::env;
use std::mem;
use std::path::PathBuf;
use crate::error;
// We can't assign `a` to `b` in the loop in `Config::load`, so we'll have to use swap. It looks ugly so now we have a macro for it.
macro_rules! swap {
($a:expr, $b:expr) => {
mem::swap(&mut $a, &mut $b)
};
}
#[derive(Clone, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Theme {
@@ -78,8 +86,41 @@ impl Config {
}
}
/// Loads file and env config and merges them
pub fn load(&mut self, path: Option<PathBuf>) -> Self {
let mut config = self.load_file(path);
// Get environment variables with CUP_ prefix
let env_vars: FxHashMap<String, String> =
env::vars().filter(|(k, _)| k.starts_with("CUP_")).collect();
if !env_vars.is_empty() {
if let Ok(mut cfg) = envy::prefixed("CUP_").from_env::<Config>() {
// If we have environment variables, override config.json options
for (key, _) in env_vars {
match key.as_str() {
"CUP_AGENT" => config.agent = cfg.agent,
#[rustfmt::skip]
"CUP_IGNORE_UPDATE_TYPE" => swap!(config.ignore_update_type, cfg.ignore_update_type),
#[rustfmt::skip]
"CUP_REFRESH_INTERVAL" => swap!(config.refresh_interval, cfg.refresh_interval),
"CUP_SOCKET" => swap!(config.socket, cfg.socket),
"CUP_THEME" => swap!(config.theme, cfg.theme),
// The syntax for these is slightly more complicated, not sure if they should be enabled or not. Let's stick to simple types for now.
// "CUP_IMAGES" => swap!(config.images, cfg.images),
// "CUP_REGISTRIES" => swap!(config.registries, cfg.registries),
// "CUP_SERVERS" => swap!(config.servers, cfg.servers),
_ => (), // Maybe print a warning if other CUP_ variables are detected
}
}
}
}
config
}
/// Reads the config from the file path provided and returns the parsed result.
pub fn load(&self, path: Option<PathBuf>) -> Self {
fn load_file(&self, path: Option<PathBuf>) -> Self {
let raw_config = match &path {
Some(path) => std::fs::read_to_string(path),
None => return Self::new(), // Empty config
@@ -93,7 +134,7 @@ impl Config {
self.parse(&raw_config.unwrap()) // We can safely unwrap here
}
/// Parses and validates the config.
pub fn parse(&self, raw_config: &str) -> Self {
fn parse(&self, raw_config: &str) -> Self {
let config: Self = match serde_json::from_str(raw_config) {
Ok(config) => config,
Err(e) => error!("Unexpected error occured while parsing config: {}", e),

View File

@@ -69,6 +69,10 @@ impl Client {
self.ctx.logger.warn(&message);
Err(message)
}
} else if status == 502 {
let message = format!("{} {}: The registry is currently unavailabile (returned status code 502).", method, url);
self.ctx.logger.warn(&message);
Err(message)
} else if status.as_u16() <= 400 {
Ok(response)
} else {

View File

@@ -1,6 +1,7 @@
use std::sync::Arc;
use std::{env, sync::Arc};
use chrono::Local;
use chrono_tz::Tz;
use liquid::{object, Object, ValueView};
use rustc_hash::FxHashMap;
use serde_json::Value;
@@ -54,15 +55,22 @@ pub async fn serve(port: &u16, ctx: &Context) -> std::io::Result<()> {
let scheduler = JobScheduler::new().await.unwrap();
let data = Arc::new(Mutex::new(data));
let data_copy = data.clone();
let tz = env::var("TZ")
.map(|tz| tz.parse().unwrap_or(Tz::UTC))
.unwrap_or(Tz::UTC);
if let Some(interval) = &ctx.config.refresh_interval {
scheduler
.add(
match Job::new_async(interval, move |_uuid, _lock| {
let data_copy = data_copy.clone();
Box::pin(async move {
data_copy.lock().await.refresh().await;
})
}) {
match Job::new_async_tz(
interval,
tz,
move |_uuid, _lock| {
let data_copy = data_copy.clone();
Box::pin(async move {
data_copy.lock().await.refresh().await;
})
},
) {
Ok(job) => job,
Err(e) => match e {
tokio_cron_scheduler::JobSchedulerError::ParseSchedule => error!(