fix: report every tagged-image removal failure, not just one

removeTaggedImagesOp switched to a plain errgroup.Group so one
image's failure can't cancel the others still in flight, but
Wait() only ever surfaces the first error returned to it,
silently dropping any other concurrent failures. Aggregate every
failure instead, the same way removeImages already separates
tolerated outcomes (still-in-use, already-gone) from real errors
before joining them.

Also give a tagged-image listing failure its own "Tagged images"
label instead of the generic Compose-wide one, matching the
dedicated label the dangling-image path already has.

Signed-off-by: Guillaume Lours <[email protected]>
This commit is contained in:
Guillaume Lours
2026-09-29 16:19:33 +02:00
committed by Nicolas De loof
parent 2ae716faac
commit a5eb970da5
2 changed files with 15 additions and 5 deletions
+14 -4
View File
@@ -18,8 +18,10 @@ package compose
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"time"
"github.com/compose-spec/compose-go/v2/types"
@@ -186,22 +188,30 @@ func (s *composeService) ensureImagesDown(ctx context.Context, project *types.Pr
func (s *composeService) removeTaggedImagesOp(ctx context.Context, project *types.Project, pruneOpts ImagePruneOptions) error {
images, err := NewImagePruner(s.apiClient(), project).ImagesToPrune(ctx, pruneOpts)
if err != nil {
s.events.On(errorEvent(api.ResourceCompose, err.Error()))
s.events.On(errorEvent("Tagged images", err.Error()))
return err
}
var mu sync.Mutex
var errs []error
var eg errgroup.Group
eg.SetLimit(s.maxConcurrency)
for i := range images {
img := images[i]
eg.Go(func() error {
return s.removeResource("Image "+img, func() error {
if err := s.removeResource("Image "+img, func() error {
_, err := s.apiClient().ImageRemove(ctx, img, client.ImageRemoveOptions{})
return err
})
}); err != nil {
mu.Lock()
errs = append(errs, err)
mu.Unlock()
}
return nil
})
}
return eg.Wait()
_ = eg.Wait() // errgroup is only used for fan-out here; goroutines never return an error
return errors.Join(errs...)
}
// removeDanglingImagesOp lists a project's dangling images and removes those
+1 -1
View File
@@ -506,7 +506,7 @@ func TestEnsureImagesDown_TaggedImageListingFailureStaysVisibleWithoutAbortingDo
assert.NilError(t, ops[1]()) // dangling-images op is independent, still runs fine
assert.Equal(t, len(rec.resources), 1)
assert.Equal(t, rec.resources[0].ID, compose.ResourceCompose)
assert.Equal(t, rec.resources[0].ID, "Tagged images")
assert.Equal(t, rec.resources[0].Status, compose.Error)
assert.Equal(t, rec.resources[0].Details, listErr.Error())
}