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:
-
-
-
-This was replaced by a more neutral theme which is now the default:
-
-
-
-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:
+
+
+
+This was replaced by a more neutral theme which is now the default:
+
+
+
+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