@@ -3,8 +3,13 @@ package compose
33import (
44 "context"
55 "errors"
6- "github.com/docker/docker/api/types/filters "
6+ "fmt "
77 "os"
8+
9+ "github.com/docker/docker/api/types"
10+ "github.com/docker/docker/api/types/container"
11+ "github.com/docker/docker/api/types/image"
12+ "github.com/docker/docker/client"
813)
914
1015type (
1823)
1924
2025var (
21- ErrUninstallRunningApps = errors .New ("failed to uninstall apps: some apps are still running, please stop them first" )
26+ ErrUninstallRunningApps = errors .New ("failed to uninstall apps: some apps are still running, please stop them first" )
27+
2228 PruneTypeAllUnusedImages PruneType = "all-unused-images"
2329 PruneTypeOnlyAppImages PruneType = "only-app-images"
2430)
@@ -72,30 +78,96 @@ func UninstallApps(ctx context.Context, cfg *Config, appRefs []string, options .
7278 continue
7379 }
7480 }
75- err = os .RemoveAll (cfg .GetAppComposeDir (app .Name ()))
76- if err != nil {
81+ if err = os .RemoveAll (cfg .GetAppComposeDir (app .Name ())); err != nil {
7782 return err
7883 }
7984 }
8085
8186 if opts .Prune {
8287 cli , errClient := GetDockerClient (cfg .DockerHost )
8388 if errClient != nil {
84- return errClient
89+ return fmt .Errorf ("failed to create docker client: %w" , errClient )
90+ }
91+
92+ var err error
93+ var allImages []image.Summary
94+ if allImages , err = cli .ImageList (ctx , types.ImageListOptions {All : true }); err != nil {
95+ return fmt .Errorf ("failed to list images: %w" , err )
96+ }
97+
98+ imagesNotInUse := make (map [string ]image.Summary )
99+ for _ , img := range allImages {
100+ imagesNotInUse [img .ID ] = img
101+ }
102+
103+ var allContainers []types.Container
104+ if allContainers , err = cli .ContainerList (ctx , container.ListOptions {All : true }); err != nil {
105+ return fmt .Errorf ("failed to list containers: %w" , err )
85106 }
86- // Prune only dangling images.
87- // The dangling images are the ones that are not tagged and not referenced by any container.
88- // TODO: consider pruning volumes and networks if needed.
89- // TODO: consider pruning only those images that are related to the uninstalled apps,
90- // otherwise it prunes all dangling images including those that are not managed by composectl
91- var dangling string
107+ for _ , ctr := range allContainers {
108+ delete (imagesNotInUse , ctr .ImageID )
109+ }
110+
92111 switch opts .PruneType {
93112 case PruneTypeAllUnusedImages :
94- dangling = "false"
113+ // Remove all images that are not in use by any container.
114+ for imgID := range imagesNotInUse {
115+ // TODO: print debug message about which image is being removed and any error that occurs during removal.
116+ _ , _ = cli .ImageRemove (ctx , imgID , types.ImageRemoveOptions {Force : true , PruneChildren : true })
117+ }
95118 case PruneTypeOnlyAppImages :
96- dangling = "true"
119+ // Build a map of image refs to image summary for images that are not in use by any container.
120+ // We will use this map to check if an image ref related to the uninstalled apps is used by any container before removing it.
121+ imageRefsNotInUse := make (map [string ]image.Summary )
122+ for _ , img := range imagesNotInUse {
123+ for _ , ref := range img .RepoDigests {
124+ imageRefsNotInUse [ref ] = img
125+ }
126+ for _ , ref := range img .RepoTags {
127+ imageRefsNotInUse [ref ] = img
128+ }
129+ }
130+ // Remove images that are referenced by the apps being uninstalled and are not in use by any container.
131+ removeAppImageRefs (ctx , status .Apps , cli , imageRefsNotInUse )
97132 }
98- _ , err = cli .ImagesPrune (ctx , filters .NewArgs (filters .Arg ("dangling" , dangling )))
99133 }
100134 return err
101135}
136+
137+ func removeAppImageRefs (ctx context.Context , apps []App , cli * client.Client , imageRefsNotInUse map [string ]image.Summary ) {
138+ // Collect image refs related to the uninstalled apps.
139+ var imageRefsToPrune []string
140+ for _ , app := range apps {
141+ for _ , imageRoot := range app .GetComposeRoot ().Children {
142+ curImageRoot := imageRoot
143+ for {
144+ imageRef := curImageRoot .Ref ()
145+ // Add a digest ref
146+ imageRefsToPrune = append (imageRefsToPrune , imageRef )
147+ if ref , err := ParseImageRef (imageRef ); err == nil {
148+ // Add a tag ref
149+ imageRefsToPrune = append (imageRefsToPrune , ref .GetTagRef ())
150+ }
151+ if curImageRoot .Type == BlobTypeImageManifest || len (curImageRoot .Children ) == 0 {
152+ break
153+ }
154+ // the image root points to an image index, let's add refs that point to the image manifest
155+ curImageRoot = curImageRoot .Children [0 ]
156+ }
157+ }
158+ }
159+ // Remove image refs related to the uninstalled apps and images the refs point to are not in use by any container.
160+ // If the removed ref is the only ref for the image, the image will also be removed;
161+ // if there are other refs for the image, only the removed ref will be removed.
162+ // This is the best effort to remove images related to the uninstalled apps without
163+ // affecting other apps that may share the same images.
164+ // In some case it can remove an image for which there is no container but some other utility reference it
165+ // by the same reference as the uninstalled app, but that is an acceptable edge case and best effort
166+ // to clean up images related to the uninstalled apps.
167+ for _ , ref := range imageRefsToPrune {
168+ if _ , notInUse := imageRefsNotInUse [ref ]; notInUse {
169+ // TODO: print debug message about which image is being removed and any error that occurs during removal.
170+ _ , _ = cli .ImageRemove (ctx , ref , types.ImageRemoveOptions {Force : false , PruneChildren : true })
171+ }
172+ }
173+ }
0 commit comments