use crate::version::{VersionPattern, CompareOutcome}; use crate::docker::DockerRef; use crate::db::{Db, Image, Tag}; use crate::registry::Registries; use crate::metrics; use chrono::{DateTime, Utc}; use std::ops::Range; #[derive(Debug)] pub struct FilePatch { pub position: Range, pub content: String, } pub struct FileInput { pub path: std::path::PathBuf, pub content: String, pub images: Vec>, } pub fn update_images(now: &DateTime, db: &dyn Db, reg: &dyn Registries, files: &[FileInput], outcomes: &mut Vec, String>>) { let mut file_idxs: Vec = vec![]; let mut parsed: Vec = vec![]; let mut images: Vec = vec![]; for (file_idx, file) in files.iter().enumerate() { for image in &file.images { let img = DockerRef::parse(&file.content, image); let registry = img.registry.as_ref() .map(|x| &file.content[x.clone()]) .unwrap_or("registry.hub.docker.com"); let image_name = &file.content[img.image.clone()]; let image_record = match db.get_image(registry, image_name) { Some(x) => x, None => { let mut new_images = [Image { id: 0, registry: registry.to_string(), image: image_name.to_string(), expires_at: DateTime::::UNIX_EPOCH, }]; db.insert_images(&mut new_images); new_images[0].clone() }, }; println!("Image {} with id {}", image_record.image, image_record.id); file_idxs.push(file_idx); parsed.push(img); images.push(image_record); } } // Refresh all expired images in the database 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); match reg.get_tags(&expired.registry, &expired.image) { Some(mut fetched_tags) => { fetched_tags.sort(); let mut to_delete: Vec = vec![]; let mut to_insert: Vec = vec![]; let mut existing = cached_tags.iter().peekable(); for t in &fetched_tags { while existing.peek().is_some_and(|e| e.tag.as_str() < t.as_str()) { let gone = existing.next().unwrap(); to_delete.push(gone.clone()); } if existing.peek().is_some_and(|e| e.tag.as_str() == t.as_str()) { existing.next(); } else { to_insert.push(Tag { id: 0, image_id: expired.id, tag: t.clone(), digest: None }); } } for gone in existing { to_delete.push(gone.clone()); } db.delete_tags(&to_delete); db.insert_tags(&mut to_insert); expired.expires_at = *now + cache_max_age; db.update_images(&[expired]); } None => { expired.expires_at = *now + cache_max_age; db.update_images(&[expired]); } } } let mut file_patches: Vec> = Vec::with_capacity(files.len()); let mut file_errors: Vec> = Vec::with_capacity(files.len()); for _ in 0..files.len() { file_patches.push(vec![]); file_errors.push(None); } for i in 0..parsed.len() { let file_idx = file_idxs[i]; let file = &files[file_idx]; if file_errors[file_idx].is_some() { continue; } let img = &parsed[i]; let mut tag = img.tag.as_ref().map(|x| file.content[x.clone()].to_string()); let image_record = &images[i]; println!("Checking image {}", image_record.image); metrics::get().images_checked.with_label_values(&[&image_record.registry]).inc(); let tags = db.get_tags_sorted(image_record.id); if image_record.expires_at <= *now && tags.is_empty() { file_errors[file_idx] = Some(format!("{}/{} was not refreshed", image_record.registry, image_record.image)); continue; } if let Some(ref tag_str) = tag { let mut current = VersionPattern::parse(&tag_str); let mut new_tag = None; for candidate in &tags { let candidate_pattern = VersionPattern::parse(&candidate.tag); match current.compare(&candidate_pattern) { CompareOutcome::Higher => { new_tag = Some(candidate.tag.clone()); current = candidate_pattern; }, CompareOutcome::Lower => {}, CompareOutcome::Incompatible => {}, CompareOutcome::Identical => {}, } } if let Some(new_tag) = new_tag { tag = Some(new_tag.clone()); file_patches[file_idx].push(FilePatch { position: img.tag.clone().unwrap(), content: new_tag, }); metrics::get().images_updated.with_label_values(&[&image_record.registry]).inc(); } } if let Some(ref digest) = img.digest { let tag_for_digest = tag.as_deref().unwrap_or("latest"); let cached_digest = if image_record.expires_at > *now { tags.iter().find(|t| t.tag == tag_for_digest).and_then(|t| t.digest.clone()) } else { None }; let digest_result = match cached_digest { Some(d) => Ok(d), None => { match reg.get_digest(&image_record.registry, &image_record.image, tag_for_digest) { Some(fetched) => { if let Some(mut tag_record) = tags.iter().find(|t| t.tag == tag_for_digest).cloned() { tag_record.digest = Some(fetched.clone()); db.update_tags(&[tag_record]); } Ok(fetched) } None => Err(format!("digest not found for {}:{}", image_record.image, tag_for_digest)), } } }; let digest_string = match digest_result { Ok(d) => d, Err(msg) => { file_errors[file_idx] = Some(msg); continue; } }; if file.content[digest.clone()] != digest_string { file_patches[file_idx].push(FilePatch { position: img.digest.clone().unwrap(), content: digest_string, }); metrics::get().images_updated.with_label_values(&[&image_record.registry]).inc(); } } } for file_idx in 0..files.len() { if let Some(msg) = file_errors[file_idx].take() { outcomes.push(Err(msg)); } else { let mut patches = std::mem::take(&mut file_patches[file_idx]); patches.sort_by_key(|e| e.position.start); outcomes.push(Ok(patches)); } } } #[cfg(test)] mod tests { use super::*; use crate::db::StubDb; use crate::registry::{StubRegistry, StubTag}; use chrono::TimeZone; #[test] fn stale_cache_fetches_from_registry() { crate::metrics::init(); let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); let db = StubDb::default(); let reg = StubRegistry::new(vec![ StubTag { registry: "registry.hub.docker.com".into(), image: "nginx".into(), tag: "1.21".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000001".into() }, StubTag { registry: "registry.hub.docker.com".into(), image: "nginx".into(), tag: "1.22".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000002".into() }, ]); let past_expiry = now - chrono::Duration::days(1); 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.20".into(), digest: None }, Tag { id: 0, image_id: images[0].id, tag: "1.21".into(), digest: None }, ]); let files = vec![FileInput { path: "/test".into(), content: "image: nginx:1.21".into(), images: vec![7..17] }]; let mut outcomes = vec![]; update_images(&now, &db, ®, &files, &mut outcomes); 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); } #[test] fn fresh_cache_uses_cached_tags() { crate::metrics::init(); let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); let db = StubDb::default(); let reg = StubRegistry::new(vec![]); let future_expiry = now + chrono::Duration::minutes(30); 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(), content: "image: nginx:1.21".into(), images: vec![7..17] }]; let mut outcomes = vec![]; update_images(&now, &db, ®, &files, &mut outcomes); assert!(outcomes[0].as_ref().unwrap().is_empty()); } #[test] fn no_patch_when_at_highest() { crate::metrics::init(); let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); let db = StubDb::default(); let reg = StubRegistry::new(vec![ StubTag { registry: "registry.hub.docker.com".into(), image: "nginx".into(), tag: "1.21".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000001".into() }, ]); let files = vec![FileInput { path: "/test".into(), content: "image: nginx:1.21".into(), images: vec![7..17] }]; let mut outcomes = vec![]; update_images(&now, &db, ®, &files, &mut outcomes); assert!(outcomes[0].as_ref().unwrap().is_empty()); } #[test] fn patch_to_higher_version() { crate::metrics::init(); let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); let db = StubDb::default(); let reg = StubRegistry::new(vec![ StubTag { registry: "registry.hub.docker.com".into(), image: "nginx".into(), tag: "1.21".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000001".into() }, StubTag { registry: "registry.hub.docker.com".into(), image: "nginx".into(), tag: "1.22".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000002".into() }, ]); let files = vec![FileInput { path: "/test".into(), content: "image: nginx:1.21".into(), images: vec![7..17] }]; let mut outcomes = vec![]; update_images(&now, &db, ®, &files, &mut outcomes); assert_eq!(outcomes[0].as_ref().unwrap()[0].content, "1.22"); } #[test] fn picks_highest_of_multiple() { crate::metrics::init(); let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); let db = StubDb::default(); let reg = StubRegistry::new(vec![ StubTag { registry: "registry.hub.docker.com".into(), image: "nginx".into(), tag: "1.21".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000001".into() }, StubTag { registry: "registry.hub.docker.com".into(), image: "nginx".into(), tag: "1.22".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000002".into() }, StubTag { registry: "registry.hub.docker.com".into(), image: "nginx".into(), tag: "1.25".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000003".into() }, ]); let files = vec![FileInput { path: "/test".into(), content: "image: nginx:1.21".into(), images: vec![7..17] }]; let mut outcomes = vec![]; update_images(&now, &db, ®, &files, &mut outcomes); assert_eq!(outcomes[0].as_ref().unwrap()[0].content, "1.25"); } #[test] fn no_tag_skips_comparison() { crate::metrics::init(); let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); let db = StubDb::default(); let reg = StubRegistry::new(vec![ StubTag { registry: "registry.hub.docker.com".into(), image: "nginx".into(), tag: "latest".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000001".into() }, ]); let files = vec![FileInput { path: "/test".into(), content: "image: nginx@sha256:0000000000000000000000000000000000000000000000000000000000000002".into(), images: vec![7..84] }]; let mut outcomes = vec![]; update_images(&now, &db, ®, &files, &mut outcomes); let patches = outcomes[0].as_ref().unwrap(); assert_eq!(patches.len(), 1); assert_eq!(patches[0].content, "sha256:0000000000000000000000000000000000000000000000000000000000000001"); } #[test] fn uses_cached_digest_when_fresh() { crate::metrics::init(); let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); let db = StubDb::default(); let reg = StubRegistry::new(vec![]); let future_expiry = now + chrono::Duration::minutes(30); 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(), content: "image: nginx:1.21@sha256:0000000000000000000000000000000000000000000000000000000000000001".into(), images: vec![7..89] }]; let mut outcomes = vec![]; update_images(&now, &db, ®, &files, &mut outcomes); let patches = outcomes[0].as_ref().unwrap(); assert_eq!(patches.len(), 1); assert_eq!(patches[0].content, "sha256:0000000000000000000000000000000000000000000000000000000000000003"); } #[test] fn fetches_digest_when_not_cached() { crate::metrics::init(); let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); let db = StubDb::default(); let reg = StubRegistry::new(vec![ StubTag { registry: "registry.hub.docker.com".into(), image: "nginx".into(), tag: "1.21".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000004".into() }, ]); let files = vec![FileInput { path: "/test".into(), content: "image: nginx:1.21@sha256:0000000000000000000000000000000000000000000000000000000000000001".into(), images: vec![7..89] }]; let mut outcomes = vec![]; update_images(&now, &db, ®, &files, &mut outcomes); let patches = outcomes[0].as_ref().unwrap(); assert_eq!(patches.len(), 1); assert_eq!(patches[0].content, "sha256:0000000000000000000000000000000000000000000000000000000000000004"); } #[test] fn no_patch_when_digest_matches() { crate::metrics::init(); let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); let db = StubDb::default(); let reg = StubRegistry::new(vec![ StubTag { registry: "registry.hub.docker.com".into(), image: "nginx".into(), tag: "1.21".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000002".into() }, ]); let files = vec![FileInput { path: "/test".into(), content: "image: nginx:1.21@sha256:0000000000000000000000000000000000000000000000000000000000000002".into(), images: vec![7..89] }]; let mut outcomes = vec![]; update_images(&now, &db, ®, &files, &mut outcomes); assert!(outcomes[0].as_ref().unwrap().is_empty()); } #[test] fn patch_when_digest_differs() { crate::metrics::init(); let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); let db = StubDb::default(); let reg = StubRegistry::new(vec![ StubTag { registry: "registry.hub.docker.com".into(), image: "nginx".into(), tag: "1.21".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000002".into() }, ]); let files = vec![FileInput { path: "/test".into(), content: "image: nginx:1.21@sha256:0000000000000000000000000000000000000000000000000000000000000001".into(), images: vec![7..89] }]; let mut outcomes = vec![]; update_images(&now, &db, ®, &files, &mut outcomes); let patches = outcomes[0].as_ref().unwrap(); assert_eq!(patches.len(), 1); assert_eq!(patches[0].content, "sha256:0000000000000000000000000000000000000000000000000000000000000002"); } #[test] fn digest_uses_updated_tag() { crate::metrics::init(); let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); let db = StubDb::default(); let reg = StubRegistry::new(vec![ StubTag { registry: "registry.hub.docker.com".into(), image: "nginx".into(), tag: "1.21".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000001".into() }, StubTag { registry: "registry.hub.docker.com".into(), image: "nginx".into(), tag: "1.22".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000002".into() }, ]); let files = vec![FileInput { path: "/test".into(), content: "image: nginx:1.21@sha256:0000000000000000000000000000000000000000000000000000000000000001".into(), images: vec![7..89] }]; let mut outcomes = vec![]; update_images(&now, &db, ®, &files, &mut outcomes); let patches = outcomes[0].as_ref().unwrap(); assert_eq!(patches.len(), 2); assert_eq!(patches[0].content, "1.22"); assert_eq!(patches[1].content, "sha256:0000000000000000000000000000000000000000000000000000000000000002"); } #[test] fn error_when_image_not_found() { crate::metrics::init(); let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); let db = StubDb::default(); let reg = StubRegistry::new(vec![]); let files = vec![FileInput { path: "/test".into(), content: "image: nginx:1.21".into(), images: vec![7..17] }]; let mut outcomes = vec![]; update_images(&now, &db, ®, &files, &mut outcomes); assert!(outcomes[0].is_err()); let got = db.get_image("registry.hub.docker.com", "nginx").unwrap(); assert!(got.expires_at > now); } #[test] fn error_when_digest_not_found() { crate::metrics::init(); let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); let db = StubDb::default(); let reg = StubRegistry::new(vec![ StubTag { registry: "registry.hub.docker.com".into(), image: "nginx".into(), tag: "1.21".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000001".into() }, ]); let files = vec![FileInput { path: "/test".into(), content: "image: nginx:1.22@sha256:0000000000000000000000000000000000000000000000000000000000000001".into(), images: vec![7..89] }]; let mut outcomes = vec![]; update_images(&now, &db, ®, &files, &mut outcomes); assert!(outcomes[0].is_err()); } #[test] fn multiple_images_produce_patches() { crate::metrics::init(); let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); let db = StubDb::default(); let reg = StubRegistry::new(vec![ StubTag { registry: "registry.hub.docker.com".into(), image: "nginx".into(), tag: "1.21".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000001".into() }, StubTag { registry: "registry.hub.docker.com".into(), image: "nginx".into(), tag: "1.22".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000002".into() }, StubTag { registry: "registry.hub.docker.com".into(), image: "redis".into(), tag: "6.0".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000003".into() }, StubTag { registry: "registry.hub.docker.com".into(), image: "redis".into(), tag: "6.2".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000004".into() }, ]); let files = vec![FileInput { path: "/test".into(), content: "image: nginx:1.21\nimage: redis:6.0".into(), images: vec![7..17, 25..34] }]; let mut outcomes = vec![]; update_images(&now, &db, ®, &files, &mut outcomes); let patches = outcomes[0].as_ref().unwrap(); assert_eq!(patches.len(), 2); } #[test] fn patches_sorted_by_position() { crate::metrics::init(); let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); let db = StubDb::default(); let reg = StubRegistry::new(vec![ StubTag { registry: "registry.hub.docker.com".into(), image: "nginx".into(), tag: "1.21".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000001".into() }, StubTag { registry: "registry.hub.docker.com".into(), image: "nginx".into(), tag: "1.22".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000002".into() }, StubTag { registry: "registry.hub.docker.com".into(), image: "redis".into(), tag: "6.0".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000003".into() }, StubTag { registry: "registry.hub.docker.com".into(), image: "redis".into(), tag: "6.2".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000004".into() }, ]); let files = vec![FileInput { path: "/test".into(), content: "image: nginx:1.21\nimage: redis:6.0".into(), images: vec![7..17, 25..34] }]; let mut outcomes = vec![]; update_images(&now, &db, ®, &files, &mut outcomes); let patches = outcomes[0].as_ref().unwrap(); assert!(patches[0].position.start < patches[1].position.start); } #[test] fn multiple_files_independent() { crate::metrics::init(); let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); let db = StubDb::default(); let reg = StubRegistry::new(vec![ StubTag { registry: "registry.hub.docker.com".into(), image: "nginx".into(), tag: "1.21".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000001".into() }, StubTag { registry: "registry.hub.docker.com".into(), image: "nginx".into(), tag: "1.22".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000002".into() }, ]); let files = vec![ FileInput { path: "/a".into(), content: "image: missing:1.0".into(), images: vec![7..18] }, FileInput { path: "/b".into(), content: "image: nginx:1.21".into(), images: vec![7..17] }, ]; let mut outcomes = vec![]; update_images(&now, &db, ®, &files, &mut outcomes); assert!(outcomes[0].is_err()); assert!(outcomes[1].is_ok()); assert_eq!(outcomes[1].as_ref().unwrap()[0].content, "1.22"); } #[test] fn stale_cache_ignores_cached_digest() { crate::metrics::init(); let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); let db = StubDb::default(); let reg = StubRegistry::new(vec![ StubTag { registry: "registry.hub.docker.com".into(), image: "nginx".into(), tag: "1.21".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000005".into() }, ]); let past_expiry = now - chrono::Duration::days(1); 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(), content: "image: nginx:1.21@sha256:0000000000000000000000000000000000000000000000000000000000000001".into(), images: vec![7..89] }]; let mut outcomes = vec![]; update_images(&now, &db, ®, &files, &mut outcomes); let patches = outcomes[0].as_ref().unwrap(); assert_eq!(patches[0].content, "sha256:0000000000000000000000000000000000000000000000000000000000000005"); } #[test] fn error_in_first_image_skips_remaining() { crate::metrics::init(); let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); let db = StubDb::default(); let reg = StubRegistry::new(vec![ StubTag { registry: "registry.hub.docker.com".into(), image: "redis".into(), tag: "6.0".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000001".into() }, StubTag { registry: "registry.hub.docker.com".into(), image: "redis".into(), tag: "6.2".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000002".into() }, ]); let files = vec![FileInput { path: "/test".into(), content: "image: nginx:1.21\nimage: redis:6.0".into(), images: vec![7..17, 25..34] }]; let mut outcomes = vec![]; update_images(&now, &db, ®, &files, &mut outcomes); assert!(outcomes[0].is_err()); assert!(db.get_image("registry.hub.docker.com", "redis").is_some()); // Redis tags ARE populated (cache refresh happens before processing), but no patches are produced let tags: Vec = db.get_tags_sorted(2).into_iter().map(|t| t.tag).collect(); assert_eq!(tags, vec!["6.0", "6.2"]); } #[test] fn same_image_multiple_files_fetches_twice() { crate::metrics::init(); let now = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap(); let db = StubDb::default(); let reg = StubRegistry::new(vec![ StubTag { registry: "registry.hub.docker.com".into(), image: "nginx".into(), tag: "1.21".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000001".into() }, StubTag { registry: "registry.hub.docker.com".into(), image: "nginx".into(), tag: "1.22".into(), digest: "sha256:0000000000000000000000000000000000000000000000000000000000000002".into() }, ]); let files = vec![ FileInput { path: "/a".into(), content: "image: nginx:1.21".into(), images: vec![7..17] }, FileInput { path: "/b".into(), content: "image: nginx:1.21".into(), images: vec![7..17] }, ]; let mut outcomes = vec![]; update_images(&now, &db, ®, &files, &mut outcomes); assert_eq!(outcomes[0].as_ref().unwrap()[0].content, "1.22"); assert_eq!(outcomes[1].as_ref().unwrap()[0].content, "1.22"); assert_eq!(reg.tags_calls.borrow()[&("registry.hub.docker.com".to_string(), "nginx".to_string())], 1); } }