From 047a4387efdd097f949bd2cfdb79f6e5425dc931 Mon Sep 17 00:00:00 2001 From: Jesper Jensen Date: Sun, 8 Feb 2026 17:21:34 +0100 Subject: Add struct to represent image row --- src/db.rs | 191 ++++++++++++++++++++++++++++++++++++++++++--------------- src/main.rs | 6 +- src/updater.rs | 51 ++++++++------- 3 files changed, 173 insertions(+), 75 deletions(-) (limited to 'src') diff --git a/src/db.rs b/src/db.rs index c914769..fc00621 100644 --- a/src/db.rs +++ b/src/db.rs @@ -2,9 +2,18 @@ use chrono::{DateTime, Utc}; use rusqlite::Connection; use rusqlite::OptionalExtension; +#[derive(Debug, PartialEq)] +pub struct Image { + pub id: i64, + pub registry: String, + pub image: String, + pub expires_at: DateTime, +} + pub trait Db { - fn get_image(&self, registry: &str, image: &str) -> Option<(i64, DateTime)>; - fn insert_image(&self, registry: &str, image: &str, expires_at: &DateTime) -> i64; + fn get_image(&self, registry: &str, image: &str) -> Option; + fn insert_image(&self, image: &mut Image); + fn get_expired_images(&self, now: &DateTime) -> Vec; fn get_tags_sorted(&self, image_id: i64) -> Vec; fn delete_tag(&self, image_id: i64, tag: &str); fn insert_tag(&self, image_id: i64, tag: &str, fetched_at: &DateTime); @@ -108,23 +117,43 @@ impl SqliteDb { } impl Db for SqliteDb { - fn get_image(&self, registry: &str, image: &str) -> Option<(i64, DateTime)> { + fn get_image(&self, registry: &str, image: &str) -> Option { let _timer = crate::metrics::get().db_query_duration.start_timer(); return self.conn.query_row(" - SELECT id, expires_at FROM images + SELECT id, registry, image, expires_at FROM images WHERE registry = ?1 AND image = ?2 - ", (registry, image), |row| Ok(( - row.get::<_, i64>(0)?, - row.get::<_, DateTime>(1)?, - ))).optional().unwrap(); + ", (registry, image), |row| Ok(Image { + id: row.get(0)?, + registry: row.get(1)?, + image: row.get(2)?, + expires_at: row.get(3)?, + })).optional().unwrap(); } - fn insert_image(&self, registry: &str, image: &str, expires_at: &DateTime) -> i64 { + fn insert_image(&self, image: &mut Image) { let _timer = crate::metrics::get().db_query_duration.start_timer(); self.conn.execute(" INSERT INTO images(registry, image, expires_at) VALUES (?1, ?2, ?3) - ", (registry, image, expires_at)).unwrap(); - return self.conn.last_insert_rowid(); + ", (&image.registry, &image.image, &image.expires_at)).unwrap(); + image.id = self.conn.last_insert_rowid(); + } + + fn get_expired_images(&self, now: &DateTime) -> Vec { + let _timer = crate::metrics::get().db_query_duration.start_timer(); + let mut stmt = self.conn.prepare(" + SELECT id, registry, image, expires_at FROM images WHERE expires_at <= ?1 + ").unwrap(); + let mut rows = stmt.query((now,)).unwrap(); + let mut images = vec![]; + while let Some(row) = rows.next().unwrap() { + images.push(Image { + id: row.get(0).unwrap(), + registry: row.get(1).unwrap(), + image: row.get(2).unwrap(), + expires_at: row.get(3).unwrap(), + }); + } + return images; } fn get_tags_sorted(&self, image_id: i64) -> Vec { @@ -172,7 +201,7 @@ impl Db for SqliteDb { #[cfg(test)] pub struct StubDb { next_id: std::cell::RefCell, - images: std::cell::RefCell)>>, + images: std::cell::RefCell>, tags: std::cell::RefCell, DateTime)>>, } @@ -189,20 +218,45 @@ impl Default for StubDb { #[cfg(test)] impl Db for StubDb { - fn get_image(&self, registry: &str, image: &str) -> Option<(i64, DateTime)> { - for (r, i, id, expires_at) in self.images.borrow().iter() { - if r == registry && i == image { - return Some((*id, *expires_at)); + fn get_image(&self, registry: &str, image: &str) -> Option { + for img in self.images.borrow().iter() { + if img.registry == registry && img.image == image { + return Some(Image { + id: img.id, + registry: img.registry.clone(), + image: img.image.clone(), + expires_at: img.expires_at, + }); } } return None; } - fn insert_image(&self, registry: &str, image: &str, expires_at: &DateTime) -> i64 { + fn insert_image(&self, image: &mut Image) { let id = *self.next_id.borrow(); *self.next_id.borrow_mut() += 1; - self.images.borrow_mut().push((registry.to_string(), image.to_string(), id, expires_at.clone())); - return id; + image.id = id; + self.images.borrow_mut().push(Image { + id, + registry: image.registry.clone(), + image: image.image.clone(), + expires_at: image.expires_at, + }); + } + + fn get_expired_images(&self, now: &DateTime) -> Vec { + let mut result = vec![]; + for img in self.images.borrow().iter() { + if img.expires_at <= *now { + result.push(Image { + id: img.id, + registry: img.registry.clone(), + image: img.image.clone(), + expires_at: img.expires_at, + }); + } + } + return result; } fn get_tags_sorted(&self, image_id: i64) -> Vec { @@ -225,9 +279,9 @@ impl Db for StubDb { } fn set_expires_at(&self, image_id: i64, expires_at: &DateTime) { - for (_, _, id, ea) in self.images.borrow_mut().iter_mut() { - if *id == image_id { - *ea = expires_at.clone(); + for img in self.images.borrow_mut().iter_mut() { + if img.id == image_id { + img.expires_at = *expires_at; return; } } @@ -260,9 +314,11 @@ mod tests { fn test_insert_image_returns_incrementing_ids(db: &dyn Db) { let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); - let id1 = db.insert_image("docker.io", "nginx", &now); - let id2 = db.insert_image("docker.io", "redis", &now); - assert!(id2 > id1); + let mut img1 = Image { id: 0, registry: "docker.io".into(), image: "nginx".into(), expires_at: now }; + let mut img2 = Image { id: 0, registry: "docker.io".into(), image: "redis".into(), expires_at: now }; + db.insert_image(&mut img1); + db.insert_image(&mut img2); + assert!(img2.id > img1.id); } fn test_get_image_returns_none_for_unknown(db: &dyn Db) { @@ -271,57 +327,78 @@ mod tests { fn test_get_image_returns_inserted(db: &dyn Db) { let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); - let id = db.insert_image("docker.io", "nginx", &now); - let (got_id, got_time) = db.get_image("docker.io", "nginx").unwrap(); - assert_eq!(got_id, id); - assert_eq!(got_time, now); + let mut img = Image { id: 0, registry: "docker.io".into(), image: "nginx".into(), expires_at: now }; + db.insert_image(&mut img); + let got = db.get_image("docker.io", "nginx").unwrap(); + assert_eq!(got.id, img.id); + assert_eq!(got.expires_at, now); } fn test_get_tags_returns_empty_for_no_tags(db: &dyn Db) { let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); - let id = db.insert_image("docker.io", "nginx", &now); - assert_eq!(db.get_tags_sorted(id), Vec::::new()); + let mut img = Image { id: 0, registry: "docker.io".into(), image: "nginx".into(), expires_at: now }; + db.insert_image(&mut img); + assert_eq!(db.get_tags_sorted(img.id), Vec::::new()); } fn test_get_tags_returns_sorted(db: &dyn Db) { let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); - let id = db.insert_image("docker.io", "nginx", &now); - db.insert_tag(id, "2.0", &now); - db.insert_tag(id, "1.0", &now); - db.insert_tag(id, "latest", &now); - assert_eq!(db.get_tags_sorted(id), vec!["1.0", "2.0", "latest"]); + let mut img = Image { id: 0, registry: "docker.io".into(), image: "nginx".into(), expires_at: now }; + db.insert_image(&mut img); + db.insert_tag(img.id, "2.0", &now); + db.insert_tag(img.id, "1.0", &now); + db.insert_tag(img.id, "latest", &now); + assert_eq!(db.get_tags_sorted(img.id), vec!["1.0", "2.0", "latest"]); } fn test_delete_tag_removes_tag(db: &dyn Db) { let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); - let id = db.insert_image("docker.io", "nginx", &now); - db.insert_tag(id, "1.0", &now); - db.insert_tag(id, "2.0", &now); - db.delete_tag(id, "1.0"); - assert_eq!(db.get_tags_sorted(id), vec!["2.0"]); + let mut img = Image { id: 0, registry: "docker.io".into(), image: "nginx".into(), expires_at: now }; + db.insert_image(&mut img); + db.insert_tag(img.id, "1.0", &now); + db.insert_tag(img.id, "2.0", &now); + db.delete_tag(img.id, "1.0"); + assert_eq!(db.get_tags_sorted(img.id), vec!["2.0"]); } fn test_set_expires_at(db: &dyn Db) { let t1 = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); let t2 = Utc.with_ymd_and_hms(2000, 1, 1, 1, 0, 0).unwrap(); - let id = db.insert_image("docker.io", "nginx", &t1); - db.set_expires_at(id, &t2); - assert_eq!(db.get_image("docker.io", "nginx").unwrap().1, t2); + let mut img = Image { id: 0, registry: "docker.io".into(), image: "nginx".into(), expires_at: t1 }; + db.insert_image(&mut img); + db.set_expires_at(img.id, &t2); + assert_eq!(db.get_image("docker.io", "nginx").unwrap().expires_at, t2); } fn test_get_tag_digest_returns_none_when_unset(db: &dyn Db) { let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); - let id = db.insert_image("docker.io", "nginx", &now); - db.insert_tag(id, "1.0", &now); - assert_eq!(db.get_tag_digest(id, "1.0"), None); + let mut img = Image { id: 0, registry: "docker.io".into(), image: "nginx".into(), expires_at: now }; + db.insert_image(&mut img); + db.insert_tag(img.id, "1.0", &now); + assert_eq!(db.get_tag_digest(img.id, "1.0"), None); } fn test_update_tag_digest(db: &dyn Db) { let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); - let id = db.insert_image("docker.io", "nginx", &now); - db.insert_tag(id, "1.0", &now); - db.update_tag_digest(id, "1.0", "sha256:abc", &now); - assert_eq!(db.get_tag_digest(id, "1.0"), Some("sha256:abc".to_string())); + let mut img = Image { id: 0, registry: "docker.io".into(), image: "nginx".into(), expires_at: now }; + db.insert_image(&mut img); + db.insert_tag(img.id, "1.0", &now); + db.update_tag_digest(img.id, "1.0", "sha256:abc", &now); + assert_eq!(db.get_tag_digest(img.id, "1.0"), Some("sha256:abc".to_string())); + } + + fn test_get_expired_images(db: &dyn Db) { + let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); + let past = now - chrono::Duration::days(1); + let future = now + chrono::Duration::days(1); + let mut expired = Image { id: 0, registry: "docker.io".into(), image: "nginx".into(), expires_at: past }; + let mut fresh = Image { id: 0, registry: "docker.io".into(), image: "redis".into(), expires_at: future }; + db.insert_image(&mut expired); + db.insert_image(&mut fresh); + let result = db.get_expired_images(&now); + assert_eq!(result.len(), 1); + assert_eq!(result[0].id, expired.id); + assert_eq!(result[0].image, "nginx"); } #[test] @@ -431,4 +508,16 @@ mod tests { let dir = tempfile::tempdir().unwrap(); test_update_tag_digest(&SqliteDb::new(&dir.path().join("db.sqlite"))); } + + #[test] + fn conformance_stub_get_expired_images() { + test_get_expired_images(&StubDb::default()); + } + + #[test] + fn conformance_sqlite_get_expired_images() { + crate::metrics::init(); + let dir = tempfile::tempdir().unwrap(); + test_get_expired_images(&SqliteDb::new(&dir.path().join("db.sqlite"))); + } } diff --git a/src/main.rs b/src/main.rs index 6eb2cb7..24a1b01 100644 --- a/src/main.rs +++ b/src/main.rs @@ -331,9 +331,9 @@ fn run_tool(db: &dyn Db, reg: &dyn Registries, repos: &Vec, mut infi .unwrap_or("registry.hub.docker.com"); let image_name = &file.content[img.image.clone()]; - if let Some((_, expires_at)) = db.get_image(registry, image_name) { - if expires_at < min_expiry { - min_expiry = expires_at; + if let Some(img) = db.get_image(registry, image_name) { + if img.expires_at < min_expiry { + min_expiry = img.expires_at; } } } diff --git a/src/updater.rs b/src/updater.rs index e407a2f..9f6c7df 100644 --- a/src/updater.rs +++ b/src/updater.rs @@ -1,6 +1,6 @@ use crate::version::{VersionPattern, CompareOutcome}; use crate::docker::DockerRef; -use crate::db::Db; +use crate::db::{Db, Image}; use crate::registry::Registries; use crate::metrics; use chrono::{DateTime, Utc}; @@ -34,11 +34,16 @@ pub fn update_images(now: &DateTime, db: &dyn Db, reg: &dyn Registries, fil let image_name = &file.content[img.image.clone()]; let (image_id, expires_at) = match db.get_image(registry, image_name) { - Some(x) => x, + Some(img) => (img.id, img.expires_at), None => { - let epoch = DateTime::::UNIX_EPOCH; - let id = db.insert_image(registry, image_name, &epoch); - (id, epoch) + let mut new_image = Image { + id: 0, + registry: registry.to_string(), + image: image_name.to_string(), + expires_at: DateTime::::UNIX_EPOCH, + }; + db.insert_image(&mut new_image); + (new_image.id, new_image.expires_at) }, }; @@ -230,9 +235,10 @@ mod tests { let reg = StubRegistry::default(); let past_expiry = now - chrono::Duration::days(1); - let id = db.insert_image("registry.hub.docker.com", "nginx", &past_expiry); - db.insert_tag(id, "1.20", &past_expiry); - db.insert_tag(id, "1.21", &past_expiry); + let mut img = Image { id: 0, registry: "registry.hub.docker.com".into(), image: "nginx".into(), expires_at: past_expiry }; + db.insert_image(&mut img); + db.insert_tag(img.id, "1.20", &past_expiry); + db.insert_tag(img.id, "1.21", &past_expiry); reg.add_tag("registry.hub.docker.com", "nginx", "1.21", "sha256:0000000000000000000000000000000000000000000000000000000000000001"); reg.add_tag("registry.hub.docker.com", "nginx", "1.22", "sha256:0000000000000000000000000000000000000000000000000000000000000002"); @@ -244,9 +250,9 @@ mod tests { let mut outcomes = vec![]; update_images(&now, &db, ®, &files, &mut outcomes); - assert_eq!(db.get_tags_sorted(id), vec!["1.21", "1.22"]); - let (_, expires_at) = db.get_image("registry.hub.docker.com", "nginx").unwrap(); - assert!(expires_at > now); + assert_eq!(db.get_tags_sorted(img.id), vec!["1.21", "1.22"]); + let got = db.get_image("registry.hub.docker.com", "nginx").unwrap(); + assert!(got.expires_at > now); } #[test] @@ -257,8 +263,9 @@ mod tests { let reg = StubRegistry::default(); let future_expiry = now + chrono::Duration::minutes(30); - let id = db.insert_image("registry.hub.docker.com", "nginx", &future_expiry); - db.insert_tag(id, "1.21", &now); + let mut img = Image { id: 0, registry: "registry.hub.docker.com".into(), image: "nginx".into(), expires_at: future_expiry }; + db.insert_image(&mut img); + db.insert_tag(img.id, "1.21", &now); let files = vec![FileInput { path: "/test".into(), @@ -364,9 +371,10 @@ mod tests { let reg = StubRegistry::default(); let future_expiry = now + chrono::Duration::minutes(30); - let id = db.insert_image("registry.hub.docker.com", "nginx", &future_expiry); - db.insert_tag(id, "1.21", &now); - db.update_tag_digest(id, "1.21", "sha256:0000000000000000000000000000000000000000000000000000000000000003", &now); + let mut img = Image { id: 0, registry: "registry.hub.docker.com".into(), image: "nginx".into(), expires_at: future_expiry }; + db.insert_image(&mut img); + db.insert_tag(img.id, "1.21", &now); + db.update_tag_digest(img.id, "1.21", "sha256:0000000000000000000000000000000000000000000000000000000000000003", &now); let files = vec![FileInput { path: "/test".into(), @@ -486,8 +494,8 @@ mod tests { assert!(outcomes[0].is_err()); assert!(outcomes[0].as_ref().unwrap_err().contains("image not found")); - let (_, expires_at) = db.get_image("registry.hub.docker.com", "nginx").unwrap(); - assert!(expires_at > now); + let got = db.get_image("registry.hub.docker.com", "nginx").unwrap(); + assert!(got.expires_at > now); } #[test] @@ -596,9 +604,10 @@ mod tests { let reg = StubRegistry::default(); let past_expiry = now - chrono::Duration::days(1); - let id = db.insert_image("registry.hub.docker.com", "nginx", &past_expiry); - db.insert_tag(id, "1.21", &past_expiry); - db.update_tag_digest(id, "1.21", "sha256:0000000000000000000000000000000000000000000000000000000000000003", &past_expiry); + let mut img = Image { id: 0, registry: "registry.hub.docker.com".into(), image: "nginx".into(), expires_at: past_expiry }; + db.insert_image(&mut img); + db.insert_tag(img.id, "1.21", &past_expiry); + db.update_tag_digest(img.id, "1.21", "sha256:0000000000000000000000000000000000000000000000000000000000000003", &past_expiry); reg.add_tag("registry.hub.docker.com", "nginx", "1.21", "sha256:0000000000000000000000000000000000000000000000000000000000000005"); -- cgit v1.2.3