mirror of
https://github.com/docker/compose.git
synced 2026-10-11 22:39:56 +00:00
api: DownOptions.Images becomes a typed ImagePruneMode, validated upfront
The legal values of DownOptions.Images lived in pkg/compose (image_pruner.go) while the field itself was a bare string in pkg/api, and the only validation fired inside ImagesToPrune — after down had already removed the project's containers, leaving the teardown half done on a typo. The CLI validated its --rmi flag separately, so only SDK callers were exposed. ImagePruneMode and its three values now live in pkg/api next to the field they constrain (type-aliased in pkg/compose for existing consumers), down() rejects any other value before touching a single resource, and the CLI check reuses the same definition. A unit test runs Down with expectation-free mocks: one daemon call would fail it. Epic #14074, B.6. Signed-off-by: Nicolas De Loof <[email protected]>
This commit is contained in:
committed by
Guillaume Lours
parent
42f48072bb
commit
53198ab17a
+3
-5
@@ -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
|
||||
}),
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
+30
-2
@@ -403,6 +403,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
|
||||
@@ -411,8 +436,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
|
||||
|
||||
+8
-2
@@ -46,6 +46,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
|
||||
@@ -124,7 +130,7 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
|
||||
ops := s.ensureNetworksDown(ctx, project, limiter)
|
||||
ops = append(ops, s.ensureRelayLinkNetworksDown(ctx, project, limiter)...)
|
||||
|
||||
if options.Images != "" {
|
||||
if options.Images != api.ImagePruneNone {
|
||||
ops = append(ops, s.ensureImagesDown(ctx, project, options, limiter)...)
|
||||
}
|
||||
|
||||
@@ -171,7 +177,7 @@ func (s *composeService) ensureVolumesDown(ctx context.Context, project *types.P
|
||||
|
||||
func (s *composeService) ensureImagesDown(ctx context.Context, project *types.Project, options api.DownOptions, limiter *semaphore.Weighted) []downOp {
|
||||
pruneOpts := ImagePruneOptions{
|
||||
Mode: ImagePruneMode(options.Images),
|
||||
Mode: options.Images,
|
||||
RemoveOrphans: options.RemoveOrphans,
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,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()
|
||||
|
||||
@@ -34,19 +34,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.
|
||||
|
||||
Reference in New Issue
Block a user