Skip to content

Commit efccb2b

Browse files
committed
compose: fix app image pruning and add all-unused-images mode
Fix image pruning in only-app-images mode. The previous implementation used the Docker image prune API with dangling=true, which only removes untagged dangling images and did not prune unused images that were still referenced by tags or digests. Uninstall now explicitly detects images not used by any container and, in only-app-images mode, removes only those images related to the apps being uninstalled. Add a new all-unused-images mode that removes every image not used by any container, regardless of whether it is related to compose apps. This makes uninstall cleanup more accurate for app-specific pruning while providing an optional broader system cleanup mode. Signed-off-by: Mike Sul <mike.sul@foundries.io>
1 parent 73528f3 commit efccb2b

1 file changed

Lines changed: 86 additions & 14 deletions

File tree

pkg/compose/uninstall.go

Lines changed: 86 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@ package compose
33
import (
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

1015
type (
@@ -18,7 +23,8 @@ type (
1823
)
1924

2025
var (
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

Comments
 (0)