Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions cmd/compose/down.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,8 @@ func downCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backe
Short: "Stop and remove containers, networks",
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
opts.timeChanged = cmd.Flags().Changed("timeout")
if opts.images != "" {
if opts.images != "all" && opts.images != "local" {
return fmt.Errorf("invalid value for --rmi: %q", opts.images)
}
if !api.ImagePruneMode(opts.images).Valid() {
return fmt.Errorf("invalid value for --rmi: %q (legal values are %q, %q)", opts.images, api.ImagePruneLocal, api.ImagePruneAll)
}
return nil
}),
Expand Down Expand Up @@ -97,7 +95,7 @@ func runDown(ctx context.Context, dockerCli command.Cli, backendOptions *Backend
RemoveOrphans: opts.removeOrphans,
Project: project,
Timeout: timeout,
Images: opts.images,
Images: api.ImagePruneMode(opts.images),
Volumes: opts.volumes,
Services: services,
})
Expand Down
32 changes: 30 additions & 2 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,31 @@ type UpOptions struct {
Start StartOptions
}

// ImagePruneMode controls how aggressively `down` removes the images
// associated with the project.
type ImagePruneMode string

const (
// ImagePruneNone keeps all project images.
ImagePruneNone ImagePruneMode = ""
// ImagePruneLocal removes only the images built locally by Compose
// (no custom tag).
ImagePruneLocal ImagePruneMode = "local"
// ImagePruneAll removes every project-associated image, remote ones
// included.
ImagePruneAll ImagePruneMode = "all"
)

// Valid reports whether the mode is one of the declared values.
func (m ImagePruneMode) Valid() bool {
switch m {
case ImagePruneNone, ImagePruneLocal, ImagePruneAll:
return true
default:
return false
}
}

// DownOptions group options of the Down API
type DownOptions struct {
// RemoveOrphans will cleanup containers that are not declared on the compose model but own the same labels
Expand All @@ -385,8 +410,11 @@ type DownOptions struct {
Project *types.Project
// Timeout override container stop timeout
Timeout *time.Duration
// Images remove image used by services. 'all': Remove all images. 'local': Remove only images that don't have a tag
Images string
// Images removes images used by the services: ImagePruneAll removes
// every project image, ImagePruneLocal only those without a custom tag,
// ImagePruneNone (the zero value) keeps them all. Down rejects any other
// value before touching a single resource.
Images ImagePruneMode
// Volumes remove volumes, both declared in the `volumes` section and anonymous ones
Volumes bool
// Services passed in the command line to be stopped
Expand Down
10 changes: 8 additions & 2 deletions pkg/compose/down.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ func (s *composeService) Down(ctx context.Context, projectName string, options a
}

func (s *composeService) down(ctx context.Context, projectName string, options api.DownOptions) error {
// validate before touching anything: failing on a bad image prune mode
// after containers are already removed would leave the teardown half done
if !options.Images.Valid() {
return fmt.Errorf("invalid image prune mode %q: legal values are %q, %q", options.Images, api.ImagePruneLocal, api.ImagePruneAll)
}

resourceToRemove := false

include := oneOffExclude
Expand Down Expand Up @@ -113,7 +119,7 @@ func (s *composeService) down(ctx context.Context, projectName string, options a

ops := s.ensureNetworksDown(ctx, project)

if options.Images != "" {
if options.Images != api.ImagePruneNone {
imgOps, err := s.ensureImagesDown(ctx, project, options)
if err != nil {
return err
Expand Down Expand Up @@ -154,7 +160,7 @@ func (s *composeService) ensureVolumesDown(ctx context.Context, project *types.P
func (s *composeService) ensureImagesDown(ctx context.Context, project *types.Project, options api.DownOptions) ([]downOp, error) {
imagePruner := NewImagePruner(s.apiClient(), project)
pruneOpts := ImagePruneOptions{
Mode: ImagePruneMode(options.Images),
Mode: options.Images,
RemoveOrphans: options.RemoveOrphans,
}
images, err := imagePruner.ImagesToPrune(ctx, pruneOpts)
Expand Down
18 changes: 18 additions & 0 deletions pkg/compose/down_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,24 @@ import (
"github.com/docker/compose/v5/pkg/mocks"
)

// An invalid image prune mode must be rejected before any resource is
// touched: the mocks carry no expectation, so a single daemon call would
// fail the test. Guards the down() precondition — validating mid-down,
// after containers were removed, would leave the teardown half done.
func TestDownRejectsInvalidImagePruneModeUpfront(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

_, cli := prepareMocks(mockCtrl)
tested, err := NewComposeService(cli)
assert.NilError(t, err)

err = tested.Down(t.Context(), strings.ToLower(testProject), compose.DownOptions{
Images: "bogus",
})
assert.ErrorContains(t, err, `invalid image prune mode "bogus"`)
}

func TestDown(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
Expand Down
13 changes: 7 additions & 6 deletions pkg/compose/image_pruner.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,20 @@ import (
"github.com/docker/compose/v5/pkg/api"
)

// ImagePruneMode controls how aggressively images associated with the project
// are removed from the engine.
type ImagePruneMode string
// ImagePruneMode aliases the pkg/api definition: the legal values of
// DownOptions.Images belong to the SDK surface, kept aliased here for the
// existing consumers of this package.
type ImagePruneMode = api.ImagePruneMode

const (
// ImagePruneNone indicates that no project images should be removed.
ImagePruneNone ImagePruneMode = ""
ImagePruneNone = api.ImagePruneNone
// ImagePruneLocal indicates that only images built locally by Compose
// should be removed.
ImagePruneLocal ImagePruneMode = "local"
ImagePruneLocal = api.ImagePruneLocal
// ImagePruneAll indicates that all project-associated images, including
// remote images should be removed.
ImagePruneAll ImagePruneMode = "all"
ImagePruneAll = api.ImagePruneAll
)

// ImagePruneOptions controls the behavior of image pruning.
Expand Down
Loading