diff --git a/docs/.tool-versions b/docs/.tool-versions new file mode 100644 index 0000000..cc1993f --- /dev/null +++ b/docs/.tool-versions @@ -0,0 +1 @@ +nodejs 21.6.2 diff --git a/docs/pages/docs/configuration.mdx b/docs/pages/docs/configuration.mdx index 6ff2f73..ee3128f 100644 --- a/docs/pages/docs/configuration.mdx +++ b/docs/pages/docs/configuration.mdx @@ -1,7 +1,5 @@ -import Image from "next/image"; -import { Steps, Callout } from "nextra-theme-docs"; -import blue from "../../assets/blue_theme.png" -import gray from "../../assets/gray_theme.png" +import { Steps, Callout, Card, Cards } from "nextra-theme-docs"; +import { IconPaint, IconLockOpen, IconKey } from '@tabler/icons-react'; # Configuration @@ -15,6 +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. + ## Configuration file Cup has an option to be configured from a configuration file named `cup.json`. @@ -25,16 +25,23 @@ Create a `cup.json` file somewhere on your system. For binary installs, a path l If you're running with Docker, you can create a `cup.json` in the directory you're running cup and mount it into the container. _In the next section you will need to use the path where you **mounted** the file_ ### Configure Cup from the configuration file -Follow the guides below (Theme and Authentication) to make your `cup.json` +Follow the guides below to customize your `cup.json` + + + } title="Authentication" href="/docs/configuration/authentication" /> + } title="Insecure registries" href="/docs/configuration/insecure-registries" /> + } title="Theme" href="/docs/configuration/theme" /> + Here's a full example: ```json { - authentication: { + "authentication": { "ghcr.io": "", "registry-1.docker.io": "" }, - theme: "blue" + "theme": "blue", + "insecure_registries": ["localhost:5000", "my-insecure-registry.example.com"] } ``` @@ -48,51 +55,4 @@ $ cup -c /home/sergio/.config/cup.json check ```bash $ docker run -tv /var/run/docker.sock:/var/run/docker.sock -v /home/sergio/.config/cup.json:/config/cup.json ghcr.io/sergi0g/cup -c /config/cup.json serve ``` - - -## Theme (server only) - -Cup initially had a blue theme which looked like this: - -Screenshot of blue theme - -This was replaced by a more neutral theme which is now the default: - -Screenshot of neutral theme - -However, you can get the old theme back by adding the `theme` key to your `cup.json` -Available values are `default` and `blue`. - -Here's an example: - -```json -{ - "theme": "blue", - // Other options -} -``` - -## Authentication - - -The features described in this section have not been implemented yet. - - -Some registries (or specific images) may require you to be authenticated. For those, you can modify `cup.json` like this: - -```json -{ - "authentication": { - "": "", - "": "" - // ... - }, - // Other options -} -``` - -You can use any registry, like `ghcr.io`, `quay.io`, `gcr.io`, etc. - - -For Docker Hub, use `registry-1.docker.io` - \ No newline at end of file + \ No newline at end of file diff --git a/docs/pages/docs/configuration/authentication.mdx b/docs/pages/docs/configuration/authentication.mdx new file mode 100644 index 0000000..5399c76 --- /dev/null +++ b/docs/pages/docs/configuration/authentication.mdx @@ -0,0 +1,22 @@ +import { Callout } from 'nextra-theme-docs' + +# Authentication + +Some registries (or specific images) may require you to be authenticated. For those, you can modify `cup.json` like this: + +```json +{ + "authentication": { + "": "", + "": "" + // ... + }, + // Other options +} +``` + +You can use any registry, like `ghcr.io`, `quay.io`, `gcr.io`, etc. + + +For Docker Hub, use `registry-1.docker.io` + \ No newline at end of file diff --git a/docs/pages/docs/configuration/insecure-registries.mdx b/docs/pages/docs/configuration/insecure-registries.mdx new file mode 100644 index 0000000..77533fe --- /dev/null +++ b/docs/pages/docs/configuration/insecure-registries.mdx @@ -0,0 +1,20 @@ +import { Callout } from 'nextra-theme-docs' + +# Insecure registries + +For the best security, Cup only connects to registries over SSL (HTTPS) by default. However, for people running a local registry that haven't configured SSL, this may be a problem. + +To solve this problem, `cup.json` has an `"insecure_registries"` option which allows you to specify exceptions + +Here's what it looks like: + +```json +{ + "insecure_registries": ["", ""], + // Other options +} +``` + + +When configuring an insecure registry that doesn't run on port 80, don't forget to specify it (i.e. use `localhost:5000` instead of `localhost` if your registry is running on port `5000`) + \ No newline at end of file diff --git a/docs/pages/docs/configuration/theme.mdx b/docs/pages/docs/configuration/theme.mdx new file mode 100644 index 0000000..33fa623 --- /dev/null +++ b/docs/pages/docs/configuration/theme.mdx @@ -0,0 +1,31 @@ +import { Callout } from "nextra-theme-docs"; +import Image from "next/image"; + +import blue from "../../../assets/blue_theme.png"; +import gray from "../../../assets/gray_theme.png"; + +# Theme + + +This configuration option is only for the server + + +Cup initially had a blue theme which looked like this: + +Screenshot of blue theme + +This was replaced by a more neutral theme which is now the default: + +Screenshot of neutral theme + +However, you can get the old theme back by adding the `theme` key to your `cup.json` +Available values are `default` and `blue`. + +Here's an example: + +```json +{ + "theme": "blue", + // Other options +} +``` \ No newline at end of file diff --git a/src/check.rs b/src/check.rs index 95851a5..f2ba76d 100644 --- a/src/check.rs +++ b/src/check.rs @@ -1,8 +1,10 @@ use std::{collections::{HashMap, HashSet}, sync::Mutex}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use json::JsonValue; use crate::{docker::get_images_from_docker_daemon, image::Image, registry::{check_auth, get_token, get_latest_digests}, utils::unsplit_image}; + #[cfg(feature = "cli")] use crate::docker::get_image_from_docker_daemon; #[cfg(feature = "cli")] @@ -23,7 +25,7 @@ where } } -pub async fn get_all_updates(socket: Option) -> Vec<(String, Option)> { +pub async fn get_all_updates(socket: Option, config: &JsonValue) -> Vec<(String, Option)> { let image_map_mutex: Mutex>> = Mutex::new(HashMap::new()); let local_images = get_images_from_docker_daemon(socket).await; local_images.par_iter().for_each(|image| { @@ -42,12 +44,13 @@ pub async fn get_all_updates(socket: Option) -> Vec<(String, Option { - let token = get_token(images.clone(), &auth_url); - get_latest_digests(images, Some(&token)) + let token = get_token(images.clone(), &auth_url, &credentials); + get_latest_digests(images, Some(&token), config) } - None => get_latest_digests(images, None), + None => get_latest_digests(images, None, config), }; remote_images.append(&mut latest_images); } @@ -67,15 +70,16 @@ pub async fn get_all_updates(socket: Option) -> Vec<(String, Option) -> Option { +pub async fn get_update(image: &str, socket: Option, config: &JsonValue) -> Option { let local_image = get_image_from_docker_daemon(socket, image).await; - let token = match check_auth(&local_image.registry) { - Some(auth_url) => get_token(vec![&local_image], &auth_url), + 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), - _ => get_latest_digest(&local_image, Some(&token)), + "" => 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()), diff --git a/src/main.rs b/src/main.rs index 4cbb5e2..17298c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -69,7 +69,7 @@ async fn main() { #[cfg(feature = "cli")] Some(Commands::Check { image, icons, raw }) => match image { Some(name) => { - let has_update = get_update(name, cli.socket).await; + 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), @@ -77,10 +77,10 @@ async fn main() { } None => { match raw { - true => print_raw_updates(&get_all_updates(cli.socket).await), + true => print_raw_updates(&get_all_updates(cli.socket, &config).await), false => { let spinner = Spinner::new(); - let updates = get_all_updates(cli.socket).await; + let updates = get_all_updates(cli.socket, &config).await; spinner.succeed(); print_updates(&updates, icons); } diff --git a/src/registry.rs b/src/registry.rs index 77359a1..289b7fd 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -6,24 +6,26 @@ use ureq::Error; use http_auth::parse_challenges; -use crate::{error, image::Image}; +use crate::{error, image::Image, warn}; -pub fn check_auth(registry: &str) -> Option { - let response = ureq::get(&format!("https://{}/v2/", registry)).call(); +pub fn check_auth(registry: &str, config: &JsonValue) -> Option { + let protocol = if config["insecure_registries"].contains(registry) { "http" } else { "https" }; + let response = ureq::get(&format!("{}://{}/v2/", protocol, registry)).call(); match response { Ok(_) => None, Err(Error::Status(401, response)) => match response.header("www-authenticate") { Some(challenge) => Some(parse_www_authenticate(challenge)), - None => error!("Server returned invalid response!"), + None => error!("Unauthorized to access registry {} and no way to authenticate was provided", registry), }, Err(e) => error!("{}", e), } } -pub fn get_latest_digest(image: &Image, token: Option<&String>) -> Image { +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!( - "https://{}/v2/{}/manifests/{}", - &image.registry, &image.repository, &image.tag + "{}://{}/v2/{}/manifests/{}", + protocol, &image.registry, &image.repository, &image.tag )); if let Some(t) = token { request = request.set("Authorization", &format!("Bearer {}", t)); @@ -35,14 +37,17 @@ pub fn get_latest_digest(image: &Image, token: Option<&String>) -> Image { Ok(response) => response, Err(Error::Status(401, response)) => { if token.is_some() { - error!("Failed to authenticate to registry {} with given token!\n{}", &image.registry, token.unwrap()) + warn!("Failed to authenticate to registry {} with given token!\n{}", &image.registry, token.unwrap()); + 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 ); } } @@ -63,10 +68,10 @@ pub fn get_latest_digest(image: &Image, token: Option<&String>) -> Image { } } -pub fn get_latest_digests(images: Vec<&Image>, token: Option<&String>) -> Vec { +pub fn get_latest_digests(images: Vec<&Image>, token: Option<&String>, config: &JsonValue) -> Vec { let result: Mutex> = Mutex::new(Vec::new()); images.par_iter().for_each(|&image| { - let digest = get_latest_digest(image, token).digest; + let digest = get_latest_digest(image, token, config).digest; result.lock().unwrap().push(Image { digest, ..image.clone() @@ -76,14 +81,17 @@ pub fn get_latest_digests(images: Vec<&Image>, token: Option<&String>) -> Vec, auth_url: &str) -> String { +pub fn get_token(images: Vec<&Image>, auth_url: &str, credentials: &Option) -> String { let mut final_url = auth_url.to_owned(); for image in images { final_url = format!("{}&scope=repository:{}:pull", final_url, image.repository); } - let raw_response = match ureq::get(&final_url) - .set("Accept", "application/vnd.oci.image.index.v1+json") // Seems to be unnecesarry. Will probably remove in the future - .call() + 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 + base_request = match credentials { + Some(creds) => base_request.set("Authorization", &format!("Basic {}", creds)), + None => base_request + }; + let raw_response = match base_request.call() { Ok(response) => match response.into_string() { Ok(res) => res, @@ -118,6 +126,6 @@ fn parse_www_authenticate(www_auth: &str) -> String { error!("Unsupported scheme {}", &challenge.scheme) } } else { - error!("No challenge provided"); + error!("No challenge provided by the server"); } } diff --git a/src/server.rs b/src/server.rs index 3044df0..30753e1 100644 --- a/src/server.rs +++ b/src/server.rs @@ -94,7 +94,7 @@ impl ServerData { s } async fn refresh(&mut self) { - let updates = sort_update_vec(&get_all_updates(self.socket.clone()).await); + let updates = sort_update_vec(&get_all_updates(self.socket.clone(), &self.config["authentication"]).await); self.raw_updates = updates; let template = liquid::ParserBuilder::with_stdlib() .build() diff --git a/src/utils.rs b/src/utils.rs index 94f8cbd..d1cea88 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -13,6 +13,14 @@ macro_rules! error { }) } +// 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 = Lazy::new(|| { @@ -23,15 +31,16 @@ pub fn split_image(image: &str) -> (String, String, String) { }); match RE.captures(image) { Some(c) => { - return ( - match c.name("registry") { + 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('/') { + if !repo.contains('/') && registry == "registry-1.docker.io" { format!("library/{}", repo) } else { repo