mirror of
https://github.com/sergi0g/cup.git
synced 2025-11-11 14:43:49 -05:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e26f941c59 | ||
|
|
c411fc4bad | ||
|
|
e965380133 | ||
|
|
ef849b624f | ||
|
|
8db7e2e12b | ||
|
|
54e1998032 | ||
|
|
9f142ab81c | ||
|
|
ffd4d6267c | ||
|
|
242029db22 | ||
|
|
b6562ef76f | ||
|
|
846b24bf2d | ||
|
|
d7f766f1f5 |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -355,7 +355,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cup"
|
||||
version = "3.1.0"
|
||||
version = "3.2.0"
|
||||
dependencies = [
|
||||
"bollard",
|
||||
"chrono",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cup"
|
||||
version = "3.1.0"
|
||||
version = "3.2.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||

|
||||

|
||||

|
||||

|
||||
[](https://discord.gg/jmh5ctzwNG)
|
||||
|
||||
|
||||
Cup is the easiest way to check for container image updates.
|
||||
@@ -23,7 +23,7 @@ _If you like this project and/or use Cup, please consider starring the project
|
||||
|
||||
- Extremely fast. Cup takes full advantage of your CPU and is hightly optimized, resulting in lightning fast speed. On my Raspberry Pi 5, it took 3.7 seconds for 58 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/getwud/wud) which would always use it up.
|
||||
- Doesn't exhaust any rate limits. This is the original reason I created Cup. I feel that this feature is especially relevant now with [Docker Hub reducing its pull limits for unauthenticated users](https://docs.docker.com/docker-hub/usage/).
|
||||
- Beautiful CLI and web interface for checking on your containers any time.
|
||||
- The binary is tiny! At the time of writing it's just 5.4 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!
|
||||
@@ -56,7 +56,7 @@ For more information, check the [docs](https://cup.sergi0g.dev/docs/contributing
|
||||
|
||||
## Support
|
||||
|
||||
If you have any questions about Cup, feel free to ask in the [discussions](https://github.com/sergi0g/cup/discussions)!
|
||||
If you have any questions about Cup, feel free to ask in the [discussions](https://github.com/sergi0g/cup/discussions)! You can also join our [discord server](https://discord.gg/jmh5ctzwNG).
|
||||
|
||||
If you find a bug, or want to propose a feature, search for it in the [issues](https://github.com/sergi0g/cup/issues). If there isn't already an open issue, please open one.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ Cup can automatically refresh the results when running in server mode. Simply ad
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"refresh_interval": "0 0,30 * 0 0" // Check twice an hour
|
||||
"refresh_interval": "0 0,30 * * * *" // Check twice an hour
|
||||
// Other options
|
||||
}
|
||||
```
|
||||
|
||||
@@ -42,7 +42,26 @@ pub async fn get_images_from_docker_daemon(
|
||||
references: &Option<Vec<String>>,
|
||||
) -> Vec<Image> {
|
||||
let client: Docker = create_docker_client(ctx.config.socket.as_deref());
|
||||
match references {
|
||||
let mut swarm_images = match client.list_services::<String>(None).await {
|
||||
Ok(services) => services
|
||||
.iter()
|
||||
.filter_map(|service| match &service.spec {
|
||||
Some(service_spec) => match &service_spec.task_template {
|
||||
Some(task_spec) => match &task_spec.container_spec {
|
||||
Some(container_spec) => match &container_spec.image {
|
||||
Some(image) => Image::from_inspect_data(ctx, image),
|
||||
None => None,
|
||||
},
|
||||
None => None,
|
||||
},
|
||||
None => None,
|
||||
},
|
||||
None => None,
|
||||
})
|
||||
.collect(),
|
||||
Err(_) => Vec::new(),
|
||||
};
|
||||
let mut local_images = match references {
|
||||
Some(refs) => {
|
||||
let mut inspect_handles = Vec::with_capacity(refs.len());
|
||||
for reference in refs {
|
||||
@@ -56,7 +75,7 @@ pub async fn get_images_from_docker_daemon(
|
||||
.collect();
|
||||
inspects
|
||||
.iter()
|
||||
.filter_map(|inspect| Image::from_inspect_data(inspect.clone()))
|
||||
.filter_map(|inspect| Image::from_inspect_data(ctx, inspect.clone()))
|
||||
.collect()
|
||||
}
|
||||
None => {
|
||||
@@ -68,8 +87,10 @@ pub async fn get_images_from_docker_daemon(
|
||||
};
|
||||
images
|
||||
.iter()
|
||||
.filter_map(|image| Image::from_inspect_data(image.clone()))
|
||||
.collect()
|
||||
.filter_map(|image| Image::from_inspect_data(ctx, image.clone()))
|
||||
.collect::<Vec<Image>>()
|
||||
}
|
||||
}
|
||||
};
|
||||
local_images.append(&mut swarm_images);
|
||||
local_images
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ pub async fn get_latest_tag(
|
||||
}
|
||||
}
|
||||
}
|
||||
None => unreachable!("{:?}", tags),
|
||||
None => error!("Image {} has no remote version tags! Local tag: {}", image.reference, image.parts.tag),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ use xitca_web::{
|
||||
use crate::{
|
||||
check::get_updates,
|
||||
config::Theme,
|
||||
error,
|
||||
structs::update::Update,
|
||||
utils::{
|
||||
json::{to_full_json, to_simple_json},
|
||||
@@ -55,13 +56,24 @@ pub async fn serve(port: &u16, ctx: &Context) -> std::io::Result<()> {
|
||||
if let Some(interval) = &ctx.config.refresh_interval {
|
||||
scheduler
|
||||
.add(
|
||||
Job::new_async(interval, move |_uuid, _lock| {
|
||||
match Job::new_async(interval, move |_uuid, _lock| {
|
||||
let data_copy = data_copy.clone();
|
||||
Box::pin(async move {
|
||||
data_copy.lock().await.refresh().await;
|
||||
})
|
||||
})
|
||||
.unwrap(),
|
||||
}) {
|
||||
Ok(job) => job,
|
||||
Err(e) => match e {
|
||||
tokio_cron_scheduler::JobSchedulerError::ParseSchedule => error!(
|
||||
"Failed to parse cron schedule: {}. Please ensure it is valid!",
|
||||
interval
|
||||
),
|
||||
e => error!(
|
||||
"An unexpected error occured while scheduling automatic refresh: {}",
|
||||
e
|
||||
),
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -79,12 +91,16 @@ pub async fn serve(port: &u16, ctx: &Context) -> std::io::Result<()> {
|
||||
.at("/", get(handler_service(_static)))
|
||||
.at("/*", get(handler_service(_static)));
|
||||
}
|
||||
app_builder
|
||||
match app_builder
|
||||
.enclosed_fn(logger)
|
||||
.serve()
|
||||
.bind(format!("0.0.0.0:{}", port))?
|
||||
.run()
|
||||
.wait()
|
||||
.bind(format!("0.0.0.0:{}", port))
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(_) => error!("Failed to bind to port {}. Is it in use?", port),
|
||||
}
|
||||
.run()
|
||||
.wait()
|
||||
}
|
||||
|
||||
async fn _static(data: StateRef<'_, Arc<Mutex<ServerData>>>, path: PathRef<'_>) -> WebResponse {
|
||||
|
||||
@@ -44,16 +44,30 @@ pub struct Image {
|
||||
|
||||
impl Image {
|
||||
/// Creates and populates the fields of an Image object based on the ImageSummary from the Docker daemon
|
||||
pub fn from_inspect_data<T: InspectData>(image: T) -> Option<Self> {
|
||||
pub fn from_inspect_data<T: InspectData>(ctx: &Context, image: T) -> Option<Self> {
|
||||
let tags = image.tags().unwrap();
|
||||
let digests = image.digests().unwrap();
|
||||
if !tags.is_empty() && !digests.is_empty() {
|
||||
let reference = tags[0].clone();
|
||||
if reference.contains('@') {
|
||||
return None; // As far as I know, references that contain @ are either manually pulled by the user or automatically created because of swarm. In the first case AFAICT we can't know what tag was originally pulled, so we'd have to make assumptions and I've decided to remove this. The other case is already handled seperately, so this also ensures images aren't displayed twice, once with and once without a digest.
|
||||
};
|
||||
let (registry, repository, tag) = split(&reference);
|
||||
let version_tag = Version::from_tag(&tag);
|
||||
let local_digests = digests
|
||||
.iter()
|
||||
.map(|digest| digest.split('@').collect::<Vec<&str>>()[1].to_string())
|
||||
.filter_map(
|
||||
|digest| match digest.split('@').collect::<Vec<&str>>().get(1) {
|
||||
Some(digest) => Some(digest.to_string()),
|
||||
None => {
|
||||
ctx.logger.warn(format!(
|
||||
"Ignoring invalid digest {} for image {}!",
|
||||
digest, reference
|
||||
));
|
||||
None
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
Some(Self {
|
||||
reference,
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
use bollard::secret::{ImageInspect, ImageSummary};
|
||||
|
||||
pub trait InspectData {
|
||||
fn tags(&self) -> Option<&Vec<String>>;
|
||||
fn digests(&self) -> Option<&Vec<String>>;
|
||||
fn tags(&self) -> Option<Vec<String>>;
|
||||
fn digests(&self) -> Option<Vec<String>>;
|
||||
fn url(&self) -> Option<String>;
|
||||
}
|
||||
|
||||
impl InspectData for ImageInspect {
|
||||
fn tags(&self) -> Option<&Vec<String>> {
|
||||
self.repo_tags.as_ref()
|
||||
fn tags(&self) -> Option<Vec<String>> {
|
||||
self.repo_tags.clone()
|
||||
}
|
||||
|
||||
fn digests(&self) -> Option<&Vec<String>> {
|
||||
self.repo_digests.as_ref()
|
||||
fn digests(&self) -> Option<Vec<String>> {
|
||||
self.repo_digests.clone()
|
||||
}
|
||||
|
||||
fn url(&self) -> Option<String> {
|
||||
@@ -27,15 +27,36 @@ impl InspectData for ImageInspect {
|
||||
}
|
||||
|
||||
impl InspectData for ImageSummary {
|
||||
fn tags(&self) -> Option<&Vec<String>> {
|
||||
Some(&self.repo_tags)
|
||||
fn tags(&self) -> Option<Vec<String>> {
|
||||
Some(self.repo_tags.clone())
|
||||
}
|
||||
|
||||
fn digests(&self) -> Option<&Vec<String>> {
|
||||
Some(&self.repo_digests)
|
||||
fn digests(&self) -> Option<Vec<String>> {
|
||||
Some(self.repo_digests.clone())
|
||||
}
|
||||
|
||||
fn url(&self) -> Option<String> {
|
||||
self.labels.get("org.opencontainers.image.url").cloned()
|
||||
}
|
||||
}
|
||||
|
||||
impl InspectData for &String {
|
||||
fn tags(&self) -> Option<Vec<String>> {
|
||||
self.split('@').next().map(|tag| vec![tag.to_string()])
|
||||
}
|
||||
|
||||
fn digests(&self) -> Option<Vec<String>> {
|
||||
match self.split_once('@') {
|
||||
Some((reference, digest)) => Some(vec![format!(
|
||||
"{}@{}",
|
||||
reference.split(':').next().unwrap(),
|
||||
digest
|
||||
)]),
|
||||
None => Some(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
fn url(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,10 @@ pub fn parse_www_authenticate(www_auth: &str) -> String {
|
||||
.fold(String::new(), |acc, (key, value)| {
|
||||
if *key == "realm" {
|
||||
acc.to_owned() + value.as_escaped() + "?"
|
||||
} else {
|
||||
} else if value.unescaped_len() != 0 {
|
||||
format!("{}&{}={}", acc, key, value.as_escaped())
|
||||
} else {
|
||||
acc
|
||||
}
|
||||
})
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user