From 560a3f9fa7befc1a08f1969b6b781244f5c2ac9d Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Wed, 19 Nov 2025 01:08:19 +0100 Subject: [PATCH 1/3] feat: ignore remote tags based on the `.images.exclude` option --- src/main.rs | 20 ++++++++++++++++++++ src/registry.rs | 17 +++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/main.rs b/src/main.rs index 5bc4dc6..2090475 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use formatting::spinner::Spinner; #[cfg(feature = "cli")] use formatting::{print_raw_updates, print_updates}; use logging::Logger; +use rustc_hash::FxHashMap; #[cfg(feature = "server")] use server::serve; use std::path::PathBuf; @@ -70,6 +71,9 @@ enum Commands { pub struct Context { pub config: Config, pub logger: Logger, + + // Excluded tag prefixes per image reference. + pub excluded_tags: FxHashMap>, } #[tokio::main] @@ -86,7 +90,23 @@ async fn main() { let mut ctx = Context { config, logger: Logger::new(cli.debug, false), + excluded_tags: FxHashMap::default(), }; + + // Precompute excluded tag prefixes per image reference. Excluded images + // that don't specify a tag prefix are ignored here. + for excluded_image in &ctx.config.images.exclude { + let splits: Vec<&str> = excluded_image.splitn(2, ':').collect(); + let (image_name, tag_prefix) = match splits.len() { + 2 => (splits[0], splits[1]), + _ => continue, + }; + ctx.excluded_tags + .entry(image_name.to_string()) + .or_default() + .push(tag_prefix.to_string()); + } + match &cli.command { #[cfg(feature = "cli")] Some(Commands::Check { diff --git a/src/registry.rs b/src/registry.rs index c1b352f..787b09a 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -137,6 +137,16 @@ pub async fn get_latest_tag( ("Authorization", authorization.as_deref()), ]; + let image_name = image.reference.split(':').next().unwrap(); + let excluded_tags: &Vec = match ctx.excluded_tags.get(image_name) { + Some(tags) => tags, + None => &Vec::new(), + }; + ctx.logger.debug(format!( + "Excluded tag prefixes for {}: {:?}", + image_name, excluded_tags + )); + let mut tags: Vec = Vec::new(); let mut next_url = Some(url); @@ -153,6 +163,7 @@ pub async fn get_latest_tag( &image.version_info.as_ref().unwrap().format_str, ctx, client, + excluded_tags, ) .await { @@ -218,6 +229,7 @@ pub async fn get_extra_tags( format_str: &str, ctx: &Context, client: &Client, + excluded_tags: &[String], ) -> Result<(Vec, Option), String> { let response = client.get(url, headers, false).await; @@ -232,6 +244,11 @@ pub async fn get_extra_tags( .as_array() .unwrap() .iter() + .filter(|tag| { + !excluded_tags + .iter() + .any(|excluded| tag.as_str().unwrap().starts_with(excluded)) + }) .filter_map(|tag| Version::from_tag(tag.as_str().unwrap())) .filter(|(tag, format_string)| match (base.minor, tag.minor) { (Some(_), Some(_)) | (None, None) => { From 36cda686b86a9792c110f7ab4191d2fd80311175 Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Thu, 20 Nov 2025 10:08:37 +0100 Subject: [PATCH 2/3] refactor: implement requested changes --- src/check.rs | 23 +++++++++++++++++++++-- src/main.rs | 20 -------------------- src/registry.rs | 33 +++++++++++++++++---------------- src/structs/image.rs | 12 ++++++++++-- 4 files changed, 48 insertions(+), 40 deletions(-) diff --git a/src/check.rs b/src/check.rs index 27e188e..7eef206 100644 --- a/src/check.rs +++ b/src/check.rs @@ -7,7 +7,10 @@ use crate::{ http::Client, registry::{check_auth, get_token}, structs::{image::Image, update::Update}, - utils::request::{get_response_body, parse_json}, + utils::{ + reference::split, + request::{get_response_body, parse_json}, + }, Context, }; @@ -79,6 +82,21 @@ async fn get_remote_updates(ctx: &Context, client: &Client, refresh: bool) -> Ve remote_images } +/// Returns a list of excluded tag prefixes for the given image. +fn get_excluded_tags(image: &Image, ctx: &Context) -> Vec { + let image_name = image.reference.split(':').next().unwrap(); + ctx.config + .images + .exclude + .iter() + .filter(|item| item.starts_with(image_name)) + .filter_map(|excluded| { + let tag = split(excluded).2; + (tag != "latest").then_some(tag) + }) + .collect() +} + /// Returns a list of updates for all images passed in. pub async fn get_updates( references: &Option>, // If a user requested _specific_ references to be checked, this will have a value @@ -200,8 +218,9 @@ pub async fn get_updates( .iter() .any(|item| image.reference.starts_with(item)); if !is_ignored { + let excluded_tags = get_excluded_tags(image, ctx); let token = tokens.get(image.parts.registry.as_str()).unwrap(); - let future = image.check(token.as_deref(), ctx, &client); + let future = image.check(token.as_deref(), ctx, &client, excluded_tags); handles.push(future); } } diff --git a/src/main.rs b/src/main.rs index 2090475..5bc4dc6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,6 @@ use formatting::spinner::Spinner; #[cfg(feature = "cli")] use formatting::{print_raw_updates, print_updates}; use logging::Logger; -use rustc_hash::FxHashMap; #[cfg(feature = "server")] use server::serve; use std::path::PathBuf; @@ -71,9 +70,6 @@ enum Commands { pub struct Context { pub config: Config, pub logger: Logger, - - // Excluded tag prefixes per image reference. - pub excluded_tags: FxHashMap>, } #[tokio::main] @@ -90,23 +86,7 @@ async fn main() { let mut ctx = Context { config, logger: Logger::new(cli.debug, false), - excluded_tags: FxHashMap::default(), }; - - // Precompute excluded tag prefixes per image reference. Excluded images - // that don't specify a tag prefix are ignored here. - for excluded_image in &ctx.config.images.exclude { - let splits: Vec<&str> = excluded_image.splitn(2, ':').collect(); - let (image_name, tag_prefix) = match splits.len() { - 2 => (splits[0], splits[1]), - _ => continue, - }; - ctx.excluded_tags - .entry(image_name.to_string()) - .or_default() - .push(tag_prefix.to_string()); - } - match &cli.command { #[cfg(feature = "cli")] Some(Commands::Check { diff --git a/src/registry.rs b/src/registry.rs index 787b09a..bfe6857 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -122,6 +122,7 @@ pub async fn get_latest_tag( token: Option<&str>, ctx: &Context, client: &Client, + excluded_tags: Vec, ) -> Image { ctx.logger .debug(format!("Checking for tag update to {}", image.reference)); @@ -137,16 +138,6 @@ pub async fn get_latest_tag( ("Authorization", authorization.as_deref()), ]; - let image_name = image.reference.split(':').next().unwrap(); - let excluded_tags: &Vec = match ctx.excluded_tags.get(image_name) { - Some(tags) => tags, - None => &Vec::new(), - }; - ctx.logger.debug(format!( - "Excluded tag prefixes for {}: {:?}", - image_name, excluded_tags - )); - let mut tags: Vec = Vec::new(); let mut next_url = Some(url); @@ -163,7 +154,7 @@ pub async fn get_latest_tag( &image.version_info.as_ref().unwrap().format_str, ctx, client, - excluded_tags, + &excluded_tags, ) .await { @@ -222,6 +213,20 @@ pub async fn get_latest_tag( } } +/// Checks if a tag matches any of the excluded tag prefixes. +fn is_ignored_tag(tag: &str, excluded_tags: &[String], ctx: &Context) -> bool { + for excluded in excluded_tags { + if tag.starts_with(excluded) { + ctx.logger.debug(format!( + "Ignoring tag \"{}\" as it matches excluded prefix \"{}\"", + tag, excluded + )); + return true; + } + } + false +} + pub async fn get_extra_tags( url: &str, headers: &[(&str, Option<&str>)], @@ -244,11 +249,7 @@ pub async fn get_extra_tags( .as_array() .unwrap() .iter() - .filter(|tag| { - !excluded_tags - .iter() - .any(|excluded| tag.as_str().unwrap().starts_with(excluded)) - }) + .filter(|tag| !is_ignored_tag(tag.as_str().unwrap(), excluded_tags, ctx)) .filter_map(|tag| Version::from_tag(tag.as_str().unwrap())) .filter(|(tag, format_string)| match (base.minor, tag.minor) { (Some(_), Some(_)) | (None, None) => { diff --git a/src/structs/image.rs b/src/structs/image.rs index 17a19b7..a180bfa 100644 --- a/src/structs/image.rs +++ b/src/structs/image.rs @@ -228,9 +228,17 @@ impl Image { } /// Checks if the image has an update - pub async fn check(&self, token: Option<&str>, ctx: &Context, client: &Client) -> Self { + pub async fn check( + &self, + token: Option<&str>, + ctx: &Context, + client: &Client, + excluded_tags: Vec, + ) -> Self { match &self.version_info { - Some(data) => get_latest_tag(self, &data.current_tag, token, ctx, client).await, + Some(data) => { + get_latest_tag(self, &data.current_tag, token, ctx, client, excluded_tags).await + } None => match self.digest_info { Some(_) => get_latest_digest(self, token, ctx, client).await, None => unreachable!(), From f1f1e2c29845d07b28649f628f278c70c987bbde Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:03:58 +0100 Subject: [PATCH 3/3] chore: rename `is_ignored_tag` to `is_excluded_tag` --- src/registry.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registry.rs b/src/registry.rs index bfe6857..a1d74be 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -214,7 +214,7 @@ pub async fn get_latest_tag( } /// Checks if a tag matches any of the excluded tag prefixes. -fn is_ignored_tag(tag: &str, excluded_tags: &[String], ctx: &Context) -> bool { +fn is_excluded_tag(tag: &str, excluded_tags: &[String], ctx: &Context) -> bool { for excluded in excluded_tags { if tag.starts_with(excluded) { ctx.logger.debug(format!( @@ -249,7 +249,7 @@ pub async fn get_extra_tags( .as_array() .unwrap() .iter() - .filter(|tag| !is_ignored_tag(tag.as_str().unwrap(), excluded_tags, ctx)) + .filter(|tag| !is_excluded_tag(tag.as_str().unwrap(), excluded_tags, ctx)) .filter_map(|tag| Version::from_tag(tag.as_str().unwrap())) .filter(|(tag, format_string)| match (base.minor, tag.minor) { (Some(_), Some(_)) | (None, None) => {