m/cup
1
0
mirror of https://github.com/sergi0g/cup.git synced 2025-11-15 00:23:48 -05:00

Change config, add schema

This commit is contained in:
Sergio
2024-12-07 16:21:01 +02:00
parent e544ef6ca5
commit 33a72c8c0d
5 changed files with 132 additions and 25 deletions

61
cup.schema.json Normal file
View File

@@ -0,0 +1,61 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://raw.githubusercontent.com/sergi0g/cup/main/cup.schema.json",
"title": "Cup",
"description": "A schema for Cup's config file",
"type": "object",
"properties": {
"registries": {
"type": "object",
"description": "Configuration options for specific registries",
"additionalProperties": {
"authentication": {
"description": "An authentication token provided by the registry",
"type": "string",
"minLength": 1
},
"insecure": {
"description": "Whether Cup should connect to the registry insecurely (HTTP) or not. Enable this only if you really need to.",
"type": "boolean"
},
"ignore": {
"description": "Whether or not the registry should be ignored when running Cup",
"type": "boolean"
}
}
},
"images": {
"type": "object",
"description": "Configuration options for specific images",
"properties": {
"extra": {
"type": "array",
"description": "Extra image references you want Cup to check",
"minItems": 1
},
"exclude": {
"type": "array",
"description": "Image references that should be excluded from the check",
"minItems": 1,
"items": {
"type": "string",
"minLength": 1
}
}
}
},
"theme": {
"description": "The theme used by the web UI",
"type": "string",
"enum": [
"default",
"blue"
]
},
"socket": {
"description": "The path to the unix socket you would like Cup to use for communication with the Docker daemon. Useful if you're trying to use Cup with Podman.",
"type": "string",
"minLength": 1
}
}
}

View File

@@ -63,7 +63,11 @@ pub async fn get_updates(references: &Option<Vec<String>>, config: &Config) -> V
// Retrieve an authentication token (if required) for each registry. // Retrieve an authentication token (if required) for each registry.
let mut tokens: FxHashMap<&str, Option<String>> = FxHashMap::default(); let mut tokens: FxHashMap<&str, Option<String>> = FxHashMap::default();
for registry in registries { for registry in registries {
let credentials = config.authentication.get(registry); let credentials = if let Some(registry_config) = config.registries.get(registry) {
&registry_config.authentication
} else {
&None
};
match check_auth(registry, config, &client).await { match check_auth(registry, config, &client).await {
Some(auth_url) => { Some(auth_url) => {
let token = get_token( let token = get_token(
@@ -85,11 +89,27 @@ pub async fn get_updates(references: &Option<Vec<String>>, config: &Config) -> V
// Create a Vec to store futures so we can await them all at once. // Create a Vec to store futures so we can await them all at once.
let mut handles = Vec::with_capacity(images.len()); let mut handles = Vec::with_capacity(images.len());
let ignored_registries = config
.registries
.iter()
.filter_map(|(registry, registry_config)| {
if registry_config.ignore {
Some(registry)
} else {
None
}
})
.collect::<Vec<&String>>();
// Loop through images and get the latest digest for each // Loop through images and get the latest digest for each
for image in &images { for image in &images {
let token = tokens.get(image.registry.as_str()).unwrap(); let is_ignored = ignored_registries.contains(&&image.registry) || config.images.exclude.iter().any(|item| image.reference.starts_with(item));
let future = image.check(token.as_ref(), config, &client); if !is_ignored {
handles.push(future); let token = tokens.get(image.registry.as_str()).unwrap();
let future = image.check(token.as_ref(), config, &client);
handles.push(future);
}
} }
// Await all the futures // Await all the futures
join_all(handles).await join_all(handles).await

View File

@@ -19,32 +19,44 @@ impl Default for Theme {
} }
} }
#[derive(Clone, Deserialize)] #[derive(Clone, Deserialize, Default)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
#[serde(default)]
pub struct RegistryConfig {
pub authentication: Option<String>,
pub insecure: bool,
pub ignore: bool,
}
#[derive(Clone, Deserialize, Default)]
#[serde(default)]
pub struct ImageConfig {
pub extra: Vec<String>,
pub exclude: Vec<String>
}
#[derive(Clone, Deserialize)]
#[serde(default)]
pub struct Config { pub struct Config {
#[serde(default = "FxHashMap::default")] pub registries: FxHashMap<String, RegistryConfig>,
pub authentication: FxHashMap<String, String>, pub images: ImageConfig,
#[serde(default = "Theme::default")]
pub theme: Theme, pub theme: Theme,
#[serde(default = "Vec::default")]
pub insecure_registries: Vec<String>,
pub socket: Option<String>, pub socket: Option<String>,
#[serde(skip_deserializing)] #[serde(skip_deserializing)]
pub debug: bool, pub debug: bool,
} }
impl Config { 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 { pub fn new() -> Self {
Self { Self {
authentication: FxHashMap::default(), registries: FxHashMap::default(),
images: ImageConfig::default(),
theme: Theme::Default, theme: Theme::Default,
insecure_registries: Vec::with_capacity(0),
socket: None, socket: None,
debug: false, debug: false,
} }
} }
/// Reads the config from the file path provided and returns the parsed result. /// Reads the config from the file path provided and returns the parsed result.
pub fn load(&self, path: Option<PathBuf>) -> Self { pub fn load(&self, path: Option<PathBuf>) -> Self {
let raw_config = match &path { let raw_config = match &path {
@@ -67,3 +79,9 @@ impl Config {
} }
} }
} }
impl Default for Config {
fn default() -> Self {
Self::new()
}
}

View File

@@ -18,7 +18,7 @@ use crate::{
}; };
pub async fn check_auth(registry: &str, config: &Config, client: &Client) -> Option<String> { pub async fn check_auth(registry: &str, config: &Config, client: &Client) -> Option<String> {
let protocol = get_protocol(&registry.to_string(), &config.insecure_registries); let protocol = get_protocol(registry, &config.registries);
let url = format!("{}://{}/v2/", protocol, registry); let url = format!("{}://{}/v2/", protocol, registry);
let response = client.get(&url, Vec::new(), true).await; let response = client.get(&url, Vec::new(), true).await;
match response { match response {
@@ -51,7 +51,7 @@ pub async fn get_latest_digest(
"Checking for digest update to {}", image.reference "Checking for digest update to {}", image.reference
); );
let start = timestamp(); let start = timestamp();
let protocol = get_protocol(&image.registry, &config.insecure_registries); let protocol = get_protocol(&image.registry, &config.registries);
let url = format!( let url = format!(
"{}://{}/v2/{}/manifests/{}", "{}://{}/v2/{}/manifests/{}",
protocol, &image.registry, &image.repository, &image.tag protocol, &image.registry, &image.repository, &image.tag
@@ -97,7 +97,7 @@ pub async fn get_latest_digest(
pub async fn get_token( pub async fn get_token(
images: &Vec<&Image>, images: &Vec<&Image>,
auth_url: &str, auth_url: &str,
credentials: &Option<&String>, credentials: &Option<String>,
client: &Client, client: &Client,
) -> String { ) -> String {
let mut url = auth_url.to_owned(); let mut url = auth_url.to_owned();
@@ -127,7 +127,7 @@ pub async fn get_latest_tag(
"Checking for tag update to {}", image.reference "Checking for tag update to {}", image.reference
); );
let start = timestamp(); let start = timestamp();
let protocol = get_protocol(&image.registry, &config.insecure_registries); let protocol = get_protocol(&image.registry, &config.registries);
let url = format!( let url = format!(
"{}://{}/v2/{}/tags/list", "{}://{}/v2/{}/tags/list",
protocol, &image.registry, &image.repository, protocol, &image.registry, &image.repository,

View File

@@ -1,8 +1,9 @@
use http_auth::parse_challenges; use http_auth::parse_challenges;
use json::JsonValue; use json::JsonValue;
use reqwest::Response; use reqwest::Response;
use rustc_hash::FxHashMap;
use crate::error; use crate::{config::RegistryConfig, error};
/// Parses the www-authenticate header the registry sends into a challenge URL /// Parses the www-authenticate header the registry sends into a challenge URL
pub fn parse_www_authenticate(www_auth: &str) -> String { pub fn parse_www_authenticate(www_auth: &str) -> String {
@@ -23,13 +24,20 @@ pub fn parse_www_authenticate(www_auth: &str) -> String {
} }
} }
pub fn get_protocol(registry: &String, insecure_registries: &[String]) -> String { pub fn get_protocol(
if insecure_registries.contains(registry) { registry: &str,
"http" registry_config: &FxHashMap<String, RegistryConfig>,
} else { ) -> &'static str {
"https" match registry_config.get(registry) {
Some(config) => {
if config.insecure {
"http"
} else {
"https"
}
},
None => "https"
} }
.to_string()
} }
pub fn to_bearer_string(token: &Option<&String>) -> Option<String> { pub fn to_bearer_string(token: &Option<&String>) -> Option<String> {