From 8254289c4f4ab69e23dedb51976e2632b757afc4 Mon Sep 17 00:00:00 2001 From: Jesper Jensen Date: Mon, 9 Feb 2026 17:56:23 +0100 Subject: Same database pattern for images --- src/db.rs | 166 ++++++++++++++++++++++++++++++--------------------------- src/updater.rs | 46 ++++++++-------- 2 files changed, 110 insertions(+), 102 deletions(-) (limited to 'src') diff --git a/src/db.rs b/src/db.rs index f1a65fd..263d88d 100644 --- a/src/db.rs +++ b/src/db.rs @@ -19,15 +19,15 @@ pub struct Tag { } pub trait Db { - fn insert_image(&self, image: &mut Image); - fn get_image(&self, registry: &str, image: &str) -> Option; fn get_expired_images(&self, now: &DateTime) -> Vec; + fn insert_images(&self, images: &mut [Image]); + fn update_images(&self, images: &[Image]); + fn get_tags_sorted(&self, image_id: i64) -> Vec; fn insert_tags(&self, tags: &mut [Tag]); fn delete_tags(&self, tag_ids: &[i64]); fn update_tags(&self, tags: &[Tag]); - fn set_expires_at(&self, image_id: i64, expires_at: &DateTime); } pub struct SqliteDb { @@ -143,12 +143,22 @@ impl Db for SqliteDb { })).optional().unwrap(); } - fn insert_image(&self, image: &mut Image) { + fn insert_images(&self, images: &mut [Image]) { + let _timer = crate::metrics::get().db_query_duration.start_timer(); + for image in images { + self.conn.execute(" + INSERT INTO images(registry, image, expires_at) VALUES (?1, ?2, ?3) + ", (&image.registry, &image.image, &image.expires_at)).unwrap(); + image.id = self.conn.last_insert_rowid(); + } + } + + fn update_images(&self, images: &[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) - ", (&image.registry, &image.image, &image.expires_at)).unwrap(); - image.id = self.conn.last_insert_rowid(); + for image in images { + self.conn.execute("UPDATE images SET expires_at = ?1 WHERE id = ?2", + (&image.expires_at, &image.id)).unwrap(); + } } fn get_expired_images(&self, now: &DateTime) -> Vec { @@ -209,10 +219,6 @@ impl Db for SqliteDb { } } - fn set_expires_at(&self, image_id: i64, expires_at: &DateTime) { - let _timer = crate::metrics::get().db_query_duration.start_timer(); - self.conn.execute("UPDATE images SET expires_at = ?1 WHERE id = ?2", (expires_at, image_id)).unwrap(); - } } #[cfg(test)] @@ -249,16 +255,24 @@ impl Db for StubDb { return None; } - fn insert_image(&self, image: &mut Image) { - let id = *self.next_id.borrow(); - *self.next_id.borrow_mut() += 1; - image.id = id; - self.images.borrow_mut().push(Image { - id, - registry: image.registry.clone(), - image: image.image.clone(), - expires_at: image.expires_at, - }); + fn insert_images(&self, images: &mut [Image]) { + for image in images { + let id = *self.next_id.borrow(); + *self.next_id.borrow_mut() += 1; + image.id = id; + self.images.borrow_mut().push(image.clone()); + } + } + + fn update_images(&self, images: &[Image]) { + for image in images { + for img in self.images.borrow_mut().iter_mut() { + if img.id == image.id { + img.expires_at = image.expires_at; + break; + } + } + } } fn get_expired_images(&self, now: &DateTime) -> Vec { @@ -311,15 +325,6 @@ impl Db for StubDb { } } } - - fn set_expires_at(&self, image_id: i64, expires_at: &DateTime) { - for img in self.images.borrow_mut().iter_mut() { - if img.id == image_id { - img.expires_at = *expires_at; - return; - } - } - } } #[cfg(test)] @@ -327,13 +332,14 @@ mod tests { use super::*; use chrono::TimeZone; - fn test_insert_image_returns_incrementing_ids(db: &dyn Db) { + fn test_insert_images_returns_incrementing_ids(db: &dyn Db) { let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); - 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); + let mut images = [ + Image { id: 0, registry: "docker.io".into(), image: "nginx".into(), expires_at: now }, + Image { id: 0, registry: "docker.io".into(), image: "redis".into(), expires_at: now }, + ]; + db.insert_images(&mut images); + assert!(images[1].id > images[0].id); } fn test_get_image_returns_none_for_unknown(db: &dyn Db) { @@ -342,99 +348,101 @@ 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 mut img = Image { id: 0, registry: "docker.io".into(), image: "nginx".into(), expires_at: now }; - db.insert_image(&mut img); + let mut images = [Image { id: 0, registry: "docker.io".into(), image: "nginx".into(), expires_at: now }]; + db.insert_images(&mut images); let got = db.get_image("docker.io", "nginx").unwrap(); - assert_eq!(got.id, img.id); + assert_eq!(got.id, images[0].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 mut img = Image { id: 0, registry: "docker.io".into(), image: "nginx".into(), expires_at: now }; - db.insert_image(&mut img); - assert!(db.get_tags_sorted(img.id).is_empty()); + let mut images = [Image { id: 0, registry: "docker.io".into(), image: "nginx".into(), expires_at: now }]; + db.insert_images(&mut images); + assert!(db.get_tags_sorted(images[0].id).is_empty()); } fn test_get_tags_returns_sorted(db: &dyn Db) { let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); - let mut img = Image { id: 0, registry: "docker.io".into(), image: "nginx".into(), expires_at: now }; - db.insert_image(&mut img); + let mut images = [Image { id: 0, registry: "docker.io".into(), image: "nginx".into(), expires_at: now }]; + db.insert_images(&mut images); db.insert_tags(&mut [ - Tag { id: 0, image_id: img.id, tag: "2.0".into(), digest: None }, - Tag { id: 0, image_id: img.id, tag: "1.0".into(), digest: None }, - Tag { id: 0, image_id: img.id, tag: "latest".into(), digest: None }, + Tag { id: 0, image_id: images[0].id, tag: "2.0".into(), digest: None }, + Tag { id: 0, image_id: images[0].id, tag: "1.0".into(), digest: None }, + Tag { id: 0, image_id: images[0].id, tag: "latest".into(), digest: None }, ]); - let tags: Vec = db.get_tags_sorted(img.id).into_iter().map(|t| t.tag).collect(); + let tags: Vec = db.get_tags_sorted(images[0].id).into_iter().map(|t| t.tag).collect(); assert_eq!(tags, vec!["1.0", "2.0", "latest"]); } fn test_delete_tags_removes_tags(db: &dyn Db) { let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); - let mut img = Image { id: 0, registry: "docker.io".into(), image: "nginx".into(), expires_at: now }; - db.insert_image(&mut img); + let mut images = [Image { id: 0, registry: "docker.io".into(), image: "nginx".into(), expires_at: now }]; + db.insert_images(&mut images); let mut tags = [ - Tag { id: 0, image_id: img.id, tag: "1.0".into(), digest: None }, - Tag { id: 0, image_id: img.id, tag: "2.0".into(), digest: None }, + Tag { id: 0, image_id: images[0].id, tag: "1.0".into(), digest: None }, + Tag { id: 0, image_id: images[0].id, tag: "2.0".into(), digest: None }, ]; db.insert_tags(&mut tags); db.delete_tags(&[tags[0].id]); - let result: Vec = db.get_tags_sorted(img.id).into_iter().map(|t| t.tag).collect(); + let result: Vec = db.get_tags_sorted(images[0].id).into_iter().map(|t| t.tag).collect(); assert_eq!(result, vec!["2.0"]); } - fn test_set_expires_at(db: &dyn Db) { + fn test_update_images(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 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); + let mut images = [Image { id: 0, registry: "docker.io".into(), image: "nginx".into(), expires_at: t1 }]; + db.insert_images(&mut images); + images[0].expires_at = t2; + db.update_images(&images); assert_eq!(db.get_image("docker.io", "nginx").unwrap().expires_at, t2); } fn test_tag_digest_none_when_unset(db: &dyn Db) { let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); - let mut img = Image { id: 0, registry: "docker.io".into(), image: "nginx".into(), expires_at: now }; - db.insert_image(&mut img); - db.insert_tags(&mut [Tag { id: 0, image_id: img.id, tag: "1.0".into(), digest: None }]); - assert_eq!(db.get_tags_sorted(img.id)[0].digest, None); + let mut images = [Image { id: 0, registry: "docker.io".into(), image: "nginx".into(), expires_at: now }]; + db.insert_images(&mut images); + db.insert_tags(&mut [Tag { id: 0, image_id: images[0].id, tag: "1.0".into(), digest: None }]); + assert_eq!(db.get_tags_sorted(images[0].id)[0].digest, None); } fn test_update_tags(db: &dyn Db) { let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); - let mut img = Image { id: 0, registry: "docker.io".into(), image: "nginx".into(), expires_at: now }; - db.insert_image(&mut img); - let mut tags = [Tag { id: 0, image_id: img.id, tag: "1.0".into(), digest: None }]; + let mut images = [Image { id: 0, registry: "docker.io".into(), image: "nginx".into(), expires_at: now }]; + db.insert_images(&mut images); + let mut tags = [Tag { id: 0, image_id: images[0].id, tag: "1.0".into(), digest: None }]; db.insert_tags(&mut tags); tags[0].digest = Some("sha256:abc".into()); db.update_tags(&tags); - assert_eq!(db.get_tags_sorted(img.id)[0].digest, Some("sha256:abc".to_string())); + assert_eq!(db.get_tags_sorted(images[0].id)[0].digest, 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 mut images = [ + Image { id: 0, registry: "docker.io".into(), image: "nginx".into(), expires_at: past }, + Image { id: 0, registry: "docker.io".into(), image: "redis".into(), expires_at: future }, + ]; + db.insert_images(&mut images); let result = db.get_expired_images(&now); assert_eq!(result.len(), 1); - assert_eq!(result[0].id, expired.id); + assert_eq!(result[0].id, images[0].id); assert_eq!(result[0].image, "nginx"); } #[test] - fn conformance_stub_insert_image_ids() { - test_insert_image_returns_incrementing_ids(&StubDb::default()); + fn conformance_stub_insert_images() { + test_insert_images_returns_incrementing_ids(&StubDb::default()); } #[test] - fn conformance_sqlite_insert_image_ids() { + fn conformance_sqlite_insert_images() { crate::metrics::init(); let dir = tempfile::tempdir().unwrap(); - test_insert_image_returns_incrementing_ids(&SqliteDb::new(&dir.path().join("db.sqlite"))); + test_insert_images_returns_incrementing_ids(&SqliteDb::new(&dir.path().join("db.sqlite"))); } #[test] @@ -498,15 +506,15 @@ mod tests { } #[test] - fn conformance_stub_set_expires_at() { - test_set_expires_at(&StubDb::default()); + fn conformance_stub_update_images() { + test_update_images(&StubDb::default()); } #[test] - fn conformance_sqlite_set_expires_at() { + fn conformance_sqlite_update_images() { crate::metrics::init(); let dir = tempfile::tempdir().unwrap(); - test_set_expires_at(&SqliteDb::new(&dir.path().join("db.sqlite"))); + test_update_images(&SqliteDb::new(&dir.path().join("db.sqlite"))); } #[test] diff --git a/src/updater.rs b/src/updater.rs index 7ab6483..b9dc555 100644 --- a/src/updater.rs +++ b/src/updater.rs @@ -34,14 +34,14 @@ pub fn update_images(now: &DateTime, db: &dyn Db, reg: &dyn Registries, fil let image_record = match db.get_image(registry, image_name) { Some(x) => x, None => { - let mut new_image = Image { + let mut new_images = [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 + }]; + db.insert_images(&mut new_images); + new_images[0].clone() }, }; @@ -52,7 +52,7 @@ pub fn update_images(now: &DateTime, db: &dyn Db, reg: &dyn Registries, fil } // Refresh all expired images in the database - for expired in db.get_expired_images(now) { + for mut expired in db.get_expired_images(now) { let cache_max_age = reg.get_cache_ttl(&expired.registry); let cached_tags = db.get_tags_sorted(expired.id); @@ -78,12 +78,12 @@ pub fn update_images(now: &DateTime, db: &dyn Db, reg: &dyn Registries, fil } db.delete_tags(&to_delete); db.insert_tags(&mut to_insert); - let new_expires_at = *now + cache_max_age; - db.set_expires_at(expired.id, &new_expires_at); + expired.expires_at = *now + cache_max_age; + db.update_images(&[expired]); } None => { - let new_expires_at = *now + cache_max_age; - db.set_expires_at(expired.id, &new_expires_at); + expired.expires_at = *now + cache_max_age; + db.update_images(&[expired]); } } } @@ -215,11 +215,11 @@ mod tests { ]); let past_expiry = now - chrono::Duration::days(1); - let mut img = Image { id: 0, registry: "registry.hub.docker.com".into(), image: "nginx".into(), expires_at: past_expiry }; - db.insert_image(&mut img); + let mut images = [Image { id: 0, registry: "registry.hub.docker.com".into(), image: "nginx".into(), expires_at: past_expiry }]; + db.insert_images(&mut images); db.insert_tags(&mut [ - Tag { id: 0, image_id: img.id, tag: "1.20".into(), digest: None }, - Tag { id: 0, image_id: img.id, tag: "1.21".into(), digest: None }, + Tag { id: 0, image_id: images[0].id, tag: "1.20".into(), digest: None }, + Tag { id: 0, image_id: images[0].id, tag: "1.21".into(), digest: None }, ]); let files = vec![FileInput { @@ -230,7 +230,7 @@ mod tests { let mut outcomes = vec![]; update_images(&now, &db, ®, &files, &mut outcomes); - let tags: Vec = db.get_tags_sorted(img.id).into_iter().map(|t| t.tag).collect(); + let tags: Vec = db.get_tags_sorted(images[0].id).into_iter().map(|t| t.tag).collect(); assert_eq!(tags, vec!["1.21", "1.22"]); let got = db.get_image("registry.hub.docker.com", "nginx").unwrap(); assert!(got.expires_at > now); @@ -244,9 +244,9 @@ mod tests { let reg = StubRegistry::new(vec![]); let future_expiry = now + chrono::Duration::minutes(30); - 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_tags(&mut [Tag { id: 0, image_id: img.id, tag: "1.21".into(), digest: None }]); + let mut images = [Image { id: 0, registry: "registry.hub.docker.com".into(), image: "nginx".into(), expires_at: future_expiry }]; + db.insert_images(&mut images); + db.insert_tags(&mut [Tag { id: 0, image_id: images[0].id, tag: "1.21".into(), digest: None }]); let files = vec![FileInput { path: "/test".into(), @@ -352,9 +352,9 @@ mod tests { let reg = StubRegistry::new(vec![]); let future_expiry = now + chrono::Duration::minutes(30); - 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_tags(&mut [Tag { id: 0, image_id: img.id, tag: "1.21".into(), digest: Some("sha256:0000000000000000000000000000000000000000000000000000000000000003".into()) }]); + let mut images = [Image { id: 0, registry: "registry.hub.docker.com".into(), image: "nginx".into(), expires_at: future_expiry }]; + db.insert_images(&mut images); + db.insert_tags(&mut [Tag { id: 0, image_id: images[0].id, tag: "1.21".into(), digest: Some("sha256:0000000000000000000000000000000000000000000000000000000000000003".into()) }]); let files = vec![FileInput { path: "/test".into(), @@ -585,9 +585,9 @@ mod tests { ]); let past_expiry = now - chrono::Duration::days(1); - 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_tags(&mut [Tag { id: 0, image_id: img.id, tag: "1.21".into(), digest: Some("sha256:0000000000000000000000000000000000000000000000000000000000000003".into()) }]); + let mut images = [Image { id: 0, registry: "registry.hub.docker.com".into(), image: "nginx".into(), expires_at: past_expiry }]; + db.insert_images(&mut images); + db.insert_tags(&mut [Tag { id: 0, image_id: images[0].id, tag: "1.21".into(), digest: Some("sha256:0000000000000000000000000000000000000000000000000000000000000003".into()) }]); let files = vec![FileInput { path: "/test".into(), -- cgit v1.2.3