@@ -70,9 +70,9 @@ pub async fn image_exists(
7070 }
7171}
7272
73- /// Remove all images whose tags or digests contain the provided name fragment
73+ /// Remove all images whose tags, digests, or labels match the provided name fragment
7474///
75- /// Returns the number of image references removed.
75+ /// Returns the number of images removed.
7676pub async fn remove_images_by_name (
7777 client : & DockerClient ,
7878 name_fragment : & str ,
@@ -82,8 +82,8 @@ pub async fn remove_images_by_name(
8282
8383 let images = list_docker_images ( client) . await ?;
8484
85- let references = collect_image_references ( & images, name_fragment) ;
86- remove_image_references ( client, references , force) . await
85+ let image_ids = collect_image_ids ( & images, name_fragment) ;
86+ remove_image_ids ( client, image_ids , force) . await
8787}
8888
8989/// List all local Docker images (including intermediate layers).
@@ -98,47 +98,87 @@ async fn list_docker_images(
9898 . map_err ( |e| DockerError :: Image ( format ! ( "Failed to list images: {e}" ) ) )
9999}
100100
101- /// Collect tags and digests that contain the provided name fragment.
102- fn collect_image_references (
101+ const LABEL_TITLE : & str = "org.opencontainers.image.title" ;
102+ const LABEL_SOURCE : & str = "org.opencontainers.image.source" ;
103+ const LABEL_URL : & str = "org.opencontainers.image.url" ;
104+
105+ const LABEL_TITLE_VALUE : & str = "opencode-cloud" ;
106+ const LABEL_SOURCE_VALUE : & str = "https://github.com/pRizz/opencode-cloud" ;
107+ const LABEL_URL_VALUE : & str = "https://github.com/pRizz/opencode-cloud" ;
108+
109+ /// Collect image IDs that contain the provided name fragment or match opencode labels.
110+ fn collect_image_ids (
103111 images : & [ bollard:: models:: ImageSummary ] ,
104112 name_fragment : & str ,
105113) -> HashSet < String > {
106- let mut references = HashSet :: new ( ) ;
114+ let mut image_ids = HashSet :: new ( ) ;
107115 for image in images {
108- for tag in & image. repo_tags {
109- if tag != "<none>:<none>" && tag. contains ( name_fragment) {
110- references. insert ( tag. to_string ( ) ) ;
111- }
112- }
113-
114- for digest in & image. repo_digests {
115- if digest. contains ( name_fragment) {
116- references. insert ( digest. to_string ( ) ) ;
117- }
116+ if image_matches_fragment_or_labels ( image, name_fragment) {
117+ image_ids. insert ( image. id . clone ( ) ) ;
118118 }
119119 }
120- references
120+ image_ids
121+ }
122+
123+ fn image_matches_fragment_or_labels (
124+ image : & bollard:: models:: ImageSummary ,
125+ name_fragment : & str ,
126+ ) -> bool {
127+ let tag_match = image
128+ . repo_tags
129+ . iter ( )
130+ . any ( |tag| tag != "<none>:<none>" && tag. contains ( name_fragment) ) ;
131+ let digest_match = image
132+ . repo_digests
133+ . iter ( )
134+ . any ( |digest| digest. contains ( name_fragment) ) ;
135+ let label_match = image_labels_match ( & image. labels ) ;
136+
137+ tag_match || digest_match || label_match
121138}
122139
123- /// Remove image references (tags/digests), returning the number removed.
124- async fn remove_image_references (
140+ fn image_labels_match ( labels : & HashMap < String , String > ) -> bool {
141+ labels
142+ . get ( LABEL_SOURCE )
143+ . is_some_and ( |value| value == LABEL_SOURCE_VALUE )
144+ || labels
145+ . get ( LABEL_URL )
146+ . is_some_and ( |value| value == LABEL_URL_VALUE )
147+ || labels
148+ . get ( LABEL_TITLE )
149+ . is_some_and ( |value| value == LABEL_TITLE_VALUE )
150+ }
151+
152+ /// Remove image IDs, returning the number removed.
153+ async fn remove_image_ids (
125154 client : & DockerClient ,
126- references : HashSet < String > ,
155+ image_ids : HashSet < String > ,
127156 force : bool ,
128157) -> Result < usize , DockerError > {
129- if references . is_empty ( ) {
158+ if image_ids . is_empty ( ) {
130159 return Ok ( 0 ) ;
131160 }
132161
133162 let remove_options = RemoveImageOptionsBuilder :: new ( ) . force ( force) . build ( ) ;
134163 let mut removed = 0usize ;
135- for reference in references {
136- client
164+ for image_id in image_ids {
165+ let result = client
137166 . inner ( )
138- . remove_image ( & reference, Some ( remove_options. clone ( ) ) , None )
139- . await
140- . map_err ( |e| DockerError :: Image ( format ! ( "Failed to remove image {reference}: {e}" ) ) ) ?;
141- removed += 1 ;
167+ . remove_image ( & image_id, Some ( remove_options. clone ( ) ) , None )
168+ . await ;
169+ match result {
170+ Ok ( _) => removed += 1 ,
171+ Err ( bollard:: errors:: Error :: DockerResponseServerError {
172+ status_code : 404 , ..
173+ } ) => {
174+ debug ! ( "Docker image already removed: {}" , image_id) ;
175+ }
176+ Err ( err) => {
177+ return Err ( DockerError :: Image ( format ! (
178+ "Failed to remove image {image_id}: {err}"
179+ ) ) ) ;
180+ }
181+ }
142182 }
143183
144184 Ok ( removed)
@@ -867,6 +907,32 @@ fn create_build_context() -> Result<Vec<u8>, std::io::Error> {
867907#[ cfg( test) ]
868908mod tests {
869909 use super :: * ;
910+ use bollard:: models:: ImageSummary ;
911+ use std:: collections:: HashMap ;
912+
913+ fn make_image_summary (
914+ id : & str ,
915+ tags : Vec < & str > ,
916+ digests : Vec < & str > ,
917+ labels : HashMap < String , String > ,
918+ ) -> ImageSummary {
919+ ImageSummary {
920+ id : id. to_string ( ) ,
921+ parent_id : String :: new ( ) ,
922+ repo_tags : tags. into_iter ( ) . map ( |tag| tag. to_string ( ) ) . collect ( ) ,
923+ repo_digests : digests
924+ . into_iter ( )
925+ . map ( |digest| digest. to_string ( ) )
926+ . collect ( ) ,
927+ created : 0 ,
928+ size : 0 ,
929+ shared_size : -1 ,
930+ labels,
931+ containers : 0 ,
932+ manifests : None ,
933+ descriptor : None ,
934+ }
935+ }
870936
871937 #[ test]
872938 fn create_build_context_succeeds ( ) {
@@ -980,4 +1046,24 @@ mod tests {
9801046 assert ! ( !is_error_line( "Compiling foo v1.0" ) ) ;
9811047 assert ! ( !is_error_line( "Successfully installed" ) ) ;
9821048 }
1049+
1050+ #[ test]
1051+ fn collect_image_ids_matches_labels ( ) {
1052+ let mut labels = HashMap :: new ( ) ;
1053+ labels. insert ( LABEL_SOURCE . to_string ( ) , LABEL_SOURCE_VALUE . to_string ( ) ) ;
1054+
1055+ let images = vec ! [
1056+ make_image_summary( "sha256:opencode" , vec![ ] , vec![ ] , labels) ,
1057+ make_image_summary(
1058+ "sha256:other" ,
1059+ vec![ "busybox:latest" ] ,
1060+ vec![ ] ,
1061+ HashMap :: new( ) ,
1062+ ) ,
1063+ ] ;
1064+
1065+ let ids = collect_image_ids ( & images, "opencode-cloud-sandbox" ) ;
1066+ assert ! ( ids. contains( "sha256:opencode" ) ) ;
1067+ assert ! ( !ids. contains( "sha256:other" ) ) ;
1068+ }
9831069}
0 commit comments