fix(images): surface runtime prune error in dangling-only mode
When PruneRegistry is false, return the PruneRuntime error to the caller instead of swallowing it into a warning log. Both-scopes mode keeps the existing behavior of logging and continuing to registry cleanup.
- commit
- 12bb5253deb191d126034b8cafde8228a2635811
- author
- brice · · 2026-02-13 06:45 UTC
- parent
- 9de4117
- signature
- unsigned
2 files · +18 -4
M internal/usecase/images/service.go +5 -4
| @@ -4,6 +4,7 @@ package images | |||
| 4 | 4 | import ( | |
| 5 | 5 | "context" | |
| 6 | 6 | "encoding/json" | |
| 7 | + | "fmt" | |
| 7 | 8 | "sort" | |
| 8 | 9 | "strings" | |
| 9 | 10 | "time" | |
| @@ -347,11 +348,11 @@ func (s *Service) Prune(ctx context.Context, opts domain.ImagePruneOptions) (dom | |||
| 347 | 348 | if opts.PruneDangling { | |
| 348 | 349 | runtimeReport, err := s.PruneRuntime(ctx) | |
| 349 | 350 | if err != nil { | |
| 350 | - | msg := "runtime prune failed" | |
| 351 | - | if opts.PruneRegistry { | |
| 352 | - | msg += "; continuing with registry prune" | |
| 351 | + | if !opts.PruneRegistry { | |
| 352 | + | // Dangling-only: surface the error to the caller. | |
| 353 | + | return report, fmt.Errorf("runtime prune failed: %w", err) | |
| 353 | 354 | } | |
| 354 | - | log.Warn().Err(err).Msg(msg) | |
| 355 | + | log.Warn().Err(err).Msg("runtime prune failed; continuing with registry prune") | |
| 355 | 356 | } else { | |
| 356 | 357 | report.Runtime = runtimeReport.Runtime | |
| 357 | 358 | } | |
| @@ -4,6 +4,7 @@ package images | |||||
| 4 | import ( | 4 | import ( | ||
| 5 | "context" | 5 | "context" | ||
| 6 | "encoding/json" | 6 | "encoding/json" | ||
| 7 | + | "fmt" | |||
| 7 | "sort" | 8 | "sort" | ||
| 8 | "strings" | 9 | "strings" | ||
| 9 | "time" | 10 | "time" | ||
| @@ -347,11 +348,11 @@ func (s *Service) Prune(ctx context.Context, opts domain.ImagePruneOptions) (dom | |||||
| 347 | if opts.PruneDangling { | 348 | if opts.PruneDangling { | ||
| 348 | runtimeReport, err := s.PruneRuntime(ctx) | 349 | runtimeReport, err := s.PruneRuntime(ctx) | ||
| 349 | if err != nil { | 350 | if err != nil { | ||
| 350 | - | msg := "runtime prune failed" | 351 | + | if !opts.PruneRegistry { |
| 351 | - | if opts.PruneRegistry { | 352 | + | // Dangling-only: surface the error to the caller. |
| 352 | - | msg += "; continuing with registry prune" | 353 | + | return report, fmt.Errorf("runtime prune failed: %w", err) |
| 353 | } | 354 | } | ||
| 354 | - | log.Warn().Err(err).Msg(msg) | 355 | + | log.Warn().Err(err).Msg("runtime prune failed; continuing with registry prune") |
| 355 | } else { | 356 | } else { | ||
| 356 | report.Runtime = runtimeReport.Runtime | 357 | report.Runtime = runtimeReport.Runtime | ||
| 357 | } | 358 | } | ||
M internal/usecase/images/service_test.go +13 -0
| @@ -529,6 +529,19 @@ func TestService_Prune_DanglingOnlySkipsRegistry(t *testing.T) { | |||
| 529 | 529 | assert.Equal(t, 0, manifestStorage.listRepositoriesCalls, "should not list repositories when registry disabled") | |
| 530 | 530 | } | |
| 531 | 531 | ||
| 532 | + | func TestService_Prune_DanglingOnlyRuntimeFailureReturnsError(t *testing.T) { | |
| 533 | + | rt := &fakeRuntime{pruneErr: errors.New("docker daemon unavailable")} | |
| 534 | + | svc := NewService(rt, newFakeManifestStorage(), &fakeBlobStorage{}, zerowrap.Default()) | |
| 535 | + | ||
| 536 | + | _, err := svc.Prune(context.Background(), domain.ImagePruneOptions{ | |
| 537 | + | KeepLast: 3, PruneDangling: true, PruneRegistry: false, | |
| 538 | + | }) | |
| 539 | + | ||
| 540 | + | require.Error(t, err) | |
| 541 | + | assert.Contains(t, err.Error(), "runtime prune failed") | |
| 542 | + | assert.Contains(t, err.Error(), "docker daemon unavailable") | |
| 543 | + | } | |
| 544 | + | ||
| 532 | 545 | func TestService_Prune_RegistryOnlySkipsRuntime(t *testing.T) { | |
| 533 | 546 | manifestStorage := newFakeManifestStorage() | |
| 534 | 547 | manifestStorage.repositories = []string{"gordon/api"} | |
| @@ -529,6 +529,19 @@ func TestService_Prune_DanglingOnlySkipsRegistry(t *testing.T) { | |||||
| 529 | assert.Equal(t, 0, manifestStorage.listRepositoriesCalls, "should not list repositories when registry disabled") | 529 | assert.Equal(t, 0, manifestStorage.listRepositoriesCalls, "should not list repositories when registry disabled") | ||
| 530 | } | 530 | } | ||
| 531 | 531 | ||||
| 532 | + | func TestService_Prune_DanglingOnlyRuntimeFailureReturnsError(t *testing.T) { | |||
| 533 | + | rt := &fakeRuntime{pruneErr: errors.New("docker daemon unavailable")} | |||
| 534 | + | svc := NewService(rt, newFakeManifestStorage(), &fakeBlobStorage{}, zerowrap.Default()) | |||
| 535 | + | ||||
| 536 | + | _, err := svc.Prune(context.Background(), domain.ImagePruneOptions{ | |||
| 537 | + | KeepLast: 3, PruneDangling: true, PruneRegistry: false, | |||
| 538 | + | }) | |||
| 539 | + | ||||
| 540 | + | require.Error(t, err) | |||
| 541 | + | assert.Contains(t, err.Error(), "runtime prune failed") | |||
| 542 | + | assert.Contains(t, err.Error(), "docker daemon unavailable") | |||
| 543 | + | } | |||
| 544 | + | ||||
| 532 | func TestService_Prune_RegistryOnlySkipsRuntime(t *testing.T) { | 545 | func TestService_Prune_RegistryOnlySkipsRuntime(t *testing.T) { | ||
| 533 | manifestStorage := newFakeManifestStorage() | 546 | manifestStorage := newFakeManifestStorage() | ||
| 534 | manifestStorage.repositories = []string{"gordon/api"} | 547 | manifestStorage.repositories = []string{"gordon/api"} | ||