diff --git a/cmd/compose/backend.go b/cmd/compose/backend.go new file mode 100644 index 0000000000..cc7d07ac77 --- /dev/null +++ b/cmd/compose/backend.go @@ -0,0 +1,45 @@ +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package compose + +import ( + "time" + + "github.com/docker/cli/cli/command" + + "github.com/docker/compose/v5/pkg/api" + "github.com/docker/compose/v5/pkg/compose" +) + +// withBackend creates a compose backend and passes it to fn. +func withBackend(dockerCli command.Cli, opts *BackendOptions, fn func(api.Compose) error) error { + backend, err := compose.NewComposeService(dockerCli, opts.Options...) + if err != nil { + return err + } + return fn(backend) +} + +// optionalTimeout converts an integer timeout (in seconds) into a *time.Duration. +// If changed is false, nil is returned (no timeout was explicitly set). +func optionalTimeout(t int, changed bool) *time.Duration { + if !changed { + return nil + } + d := time.Duration(t) * time.Second + return &d +} diff --git a/cmd/compose/kill.go b/cmd/compose/kill.go index ee488d2ec6..1ec83153bc 100644 --- a/cmd/compose/kill.go +++ b/cmd/compose/kill.go @@ -26,7 +26,6 @@ import ( "github.com/spf13/cobra" "github.com/docker/compose/v5/pkg/api" - "github.com/docker/compose/v5/pkg/compose" "github.com/docker/compose/v5/pkg/utils" ) @@ -63,19 +62,17 @@ func runKill(ctx context.Context, dockerCli command.Cli, backendOptions *Backend return err } - backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...) - if err != nil { + return withBackend(dockerCli, backendOptions, func(backend api.Compose) error { + err := backend.Kill(ctx, name, api.KillOptions{ + RemoveOrphans: opts.removeOrphans, + Project: project, + Services: services, + Signal: opts.signal, + }) + if errors.Is(err, api.ErrNoResources) { + _, _ = fmt.Fprintln(stdinfo(dockerCli), "No container to kill") + return nil + } return err - } - err = backend.Kill(ctx, name, api.KillOptions{ - RemoveOrphans: opts.removeOrphans, - Project: project, - Services: services, - Signal: opts.signal, }) - if errors.Is(err, api.ErrNoResources) { - _, _ = fmt.Fprintln(stdinfo(dockerCli), "No container to kill") - return nil - } - return err } diff --git a/cmd/compose/pause.go b/cmd/compose/pause.go index bb4cedba2a..8b8fb4a5fa 100644 --- a/cmd/compose/pause.go +++ b/cmd/compose/pause.go @@ -23,7 +23,6 @@ import ( "github.com/spf13/cobra" "github.com/docker/compose/v5/pkg/api" - "github.com/docker/compose/v5/pkg/compose" ) type pauseOptions struct { @@ -50,14 +49,11 @@ func runPause(ctx context.Context, dockerCli command.Cli, backendOptions *Backen if err != nil { return err } - - backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...) - if err != nil { - return err - } - return backend.Pause(ctx, name, api.PauseOptions{ - Services: services, - Project: project, + return withBackend(dockerCli, backendOptions, func(backend api.Compose) error { + return backend.Pause(ctx, name, api.PauseOptions{ + Services: services, + Project: project, + }) }) } @@ -85,13 +81,10 @@ func runUnPause(ctx context.Context, dockerCli command.Cli, backendOptions *Back if err != nil { return err } - - backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...) - if err != nil { - return err - } - return backend.UnPause(ctx, name, api.PauseOptions{ - Services: services, - Project: project, + return withBackend(dockerCli, backendOptions, func(backend api.Compose) error { + return backend.UnPause(ctx, name, api.PauseOptions{ + Services: services, + Project: project, + }) }) } diff --git a/cmd/compose/restart.go b/cmd/compose/restart.go index e014b2a8ef..a9d97c5026 100644 --- a/cmd/compose/restart.go +++ b/cmd/compose/restart.go @@ -18,13 +18,11 @@ package compose import ( "context" - "time" "github.com/docker/cli/cli/command" "github.com/spf13/cobra" "github.com/docker/compose/v5/pkg/api" - "github.com/docker/compose/v5/pkg/compose" ) type restartOptions struct { @@ -69,20 +67,12 @@ func runRestart(ctx context.Context, dockerCli command.Cli, backendOptions *Back } } - var timeout *time.Duration - if opts.timeChanged { - timeoutValue := time.Duration(opts.timeout) * time.Second - timeout = &timeoutValue - } - - backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...) - if err != nil { - return err - } - return backend.Restart(ctx, name, api.RestartOptions{ - Timeout: timeout, - Services: services, - Project: project, - NoDeps: opts.noDeps, + return withBackend(dockerCli, backendOptions, func(backend api.Compose) error { + return backend.Restart(ctx, name, api.RestartOptions{ + Timeout: optionalTimeout(opts.timeout, opts.timeChanged), + Services: services, + Project: project, + NoDeps: opts.noDeps, + }) }) } diff --git a/cmd/compose/start.go b/cmd/compose/start.go index bd5f10c463..062efb680d 100644 --- a/cmd/compose/start.go +++ b/cmd/compose/start.go @@ -24,7 +24,6 @@ import ( "github.com/spf13/cobra" "github.com/docker/compose/v5/pkg/api" - "github.com/docker/compose/v5/pkg/compose" ) type startOptions struct { @@ -58,20 +57,17 @@ func runStart(ctx context.Context, dockerCli command.Cli, backendOptions *Backen return err } - backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...) - if err != nil { - return err - } - var timeout time.Duration if opts.waitTimeout > 0 { timeout = time.Duration(opts.waitTimeout) * time.Second } - return backend.Start(ctx, name, api.StartOptions{ - AttachTo: services, - Project: project, - Services: services, - Wait: opts.wait, - WaitTimeout: timeout, + return withBackend(dockerCli, backendOptions, func(backend api.Compose) error { + return backend.Start(ctx, name, api.StartOptions{ + AttachTo: services, + Project: project, + Services: services, + Wait: opts.wait, + WaitTimeout: timeout, + }) }) } diff --git a/cmd/compose/stop.go b/cmd/compose/stop.go index 6bc3faaa96..be5dec26fc 100644 --- a/cmd/compose/stop.go +++ b/cmd/compose/stop.go @@ -18,13 +18,11 @@ package compose import ( "context" - "time" "github.com/docker/cli/cli/command" "github.com/spf13/cobra" "github.com/docker/compose/v5/pkg/api" - "github.com/docker/compose/v5/pkg/compose" ) type stopOptions struct { @@ -59,19 +57,11 @@ func runStop(ctx context.Context, dockerCli command.Cli, backendOptions *Backend if err != nil { return err } - - var timeout *time.Duration - if opts.timeChanged { - timeoutValue := time.Duration(opts.timeout) * time.Second - timeout = &timeoutValue - } - backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...) - if err != nil { - return err - } - return backend.Stop(ctx, name, api.StopOptions{ - Timeout: timeout, - Services: services, - Project: project, + return withBackend(dockerCli, backendOptions, func(backend api.Compose) error { + return backend.Stop(ctx, name, api.StopOptions{ + Timeout: optionalTimeout(opts.timeout, opts.timeChanged), + Services: services, + Project: project, + }) }) } diff --git a/pkg/compose/containers.go b/pkg/compose/containers.go index f1a54fa1a7..b2f6b97486 100644 --- a/pkg/compose/containers.go +++ b/pkg/compose/containers.go @@ -26,6 +26,7 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/moby/moby/api/types/container" "github.com/moby/moby/client" + "golang.org/x/sync/errgroup" "github.com/docker/compose/v5/pkg/api" ) @@ -61,7 +62,7 @@ func getDefaultFilters(projectName string, oneOff oneOff, selectedServices ...st if len(selectedServices) == 1 { f.Add("label", serviceFilter(selectedServices[0])) } - f.Add("label", hasConfigHashLabel()) + f.Add("label", api.ConfigHashLabel) switch oneOff { case oneOffOnly: f.Add("label", oneOffFilter(true)) @@ -166,12 +167,18 @@ func (containers Containers) names() []string { return names } -func (containers Containers) forEach(fn func(container.Summary)) { - for _, c := range containers { - fn(c) +// forEachContainerConcurrent runs fn for every container concurrently and waits for all goroutines. +func forEachContainerConcurrent(ctx context.Context, containers Containers, fn func(context.Context, container.Summary) error) error { + eg, ctx := errgroup.WithContext(ctx) + for _, ctr := range containers { + eg.Go(func() error { + return fn(ctx, ctr) + }) } + return eg.Wait() } +// sorted sorts containers in place by canonical name and returns the (same) slice. func (containers Containers) sorted() Containers { sort.Slice(containers, func(i, j int) bool { return getCanonicalContainerName(containers[i]) < getCanonicalContainerName(containers[j]) diff --git a/pkg/compose/convergence.go b/pkg/compose/convergence.go index 609f803949..6a5d950070 100644 --- a/pkg/compose/convergence.go +++ b/pkg/compose/convergence.go @@ -193,7 +193,7 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project, name := getContainerProgressName(ctr) switch ctr.State { case container.StateRunning: - c.compose.events.On(runningEvent(name)) + c.compose.events.On(newEvent(name, api.Done, api.StatusRunning)) case container.StateCreated: case container.StateRestarting: case container.StateExited: @@ -303,34 +303,24 @@ func (c *convergence) resolveVolumeFrom(service *types.ServiceConfig) error { } func (c *convergence) resolveSharedNamespaces(service *types.ServiceConfig) error { - str := service.NetworkMode - if name := getDependentServiceFromMode(str); name != "" { - dependencies := c.getObservedState(name) - if len(dependencies) == 0 { - return fmt.Errorf("cannot share network namespace with service %s: container missing", name) + resolve := func(field *string, noun string) error { + if name := getDependentServiceFromMode(*field); name != "" { + dependencies := c.getObservedState(name) + if len(dependencies) == 0 { + return fmt.Errorf("cannot share %s namespace with service %s: container missing", noun, name) + } + *field = types.ContainerPrefix + dependencies.sorted()[0].ID } - service.NetworkMode = types.ContainerPrefix + dependencies.sorted()[0].ID + return nil } - str = service.Ipc - if name := getDependentServiceFromMode(str); name != "" { - dependencies := c.getObservedState(name) - if len(dependencies) == 0 { - return fmt.Errorf("cannot share IPC namespace with service %s: container missing", name) - } - service.Ipc = types.ContainerPrefix + dependencies.sorted()[0].ID + if err := resolve(&service.NetworkMode, "network"); err != nil { + return err } - - str = service.Pid - if name := getDependentServiceFromMode(str); name != "" { - dependencies := c.getObservedState(name) - if len(dependencies) == 0 { - return fmt.Errorf("cannot share PID namespace with service %s: container missing", name) - } - service.Pid = types.ContainerPrefix + dependencies.sorted()[0].ID + if err := resolve(&service.Ipc, "IPC"); err != nil { + return err } - - return nil + return resolve(&service.Pid, "PID") } func (c *convergence) mustRecreate(expected types.ServiceConfig, actual container.Summary, policy string) (bool, error) { @@ -927,7 +917,7 @@ func (s *composeService) startService(ctx context.Context, } eventName := getContainerProgressName(ctr) - s.events.On(startingEvent(eventName)) + s.events.On(newEvent(eventName, api.Working, api.StatusStarting)) _, err = s.apiClient().ContainerStart(ctx, ctr.ID, client.ContainerStartOptions{}) if err != nil { return err @@ -940,7 +930,7 @@ func (s *composeService) startService(ctx context.Context, } } - s.events.On(startedEvent(eventName)) + s.events.On(newEvent(eventName, api.Done, api.StatusStarted)) } return nil } diff --git a/pkg/compose/convergence_test.go b/pkg/compose/convergence_test.go index 2f7c31cf81..441356959b 100644 --- a/pkg/compose/convergence_test.go +++ b/pkg/compose/convergence_test.go @@ -73,7 +73,7 @@ func TestServiceLinks(t *testing.T) { Filters: projectFilter(testProject).Add("label", serviceFilter("db"), oneOffFilter(false), - hasConfigHashLabel(), + api.ConfigHashLabel, ), All: true, } @@ -201,7 +201,7 @@ func TestServiceLinks(t *testing.T) { Filters: projectFilter(testProject).Add("label", serviceFilter("web"), oneOffFilter(false), - hasConfigHashLabel(), + api.ConfigHashLabel, ), All: true, } diff --git a/pkg/compose/dependencies.go b/pkg/compose/dependencies.go index c448fd5a17..84565c46c6 100644 --- a/pkg/compose/dependencies.go +++ b/pkg/compose/dependencies.go @@ -43,9 +43,9 @@ type graphTraversal struct { seen map[string]struct{} ignored map[string]struct{} - extremityNodesFn func(*Graph) []*Vertex // leaves or roots - adjacentNodesFn func(*Vertex) []*Vertex // getParents or getChildren - filterAdjacentByStatusFn func(*Graph, string, ServiceStatus) []*Vertex // filterChildren or filterParents + extremityNodesFn func(*Graph) []*Vertex // Leaves or Roots + adjacentNodesFn func(*Vertex) []*Vertex // GetParents or GetChildren + filterAdjacentByStatusFn func(*Graph, string, ServiceStatus) []*Vertex // FilterChildren or FilterParents targetServiceStatus ServiceStatus adjacentServiceStatusToSkip ServiceStatus @@ -55,9 +55,9 @@ type graphTraversal struct { func upDirectionTraversal(visitorFn func(context.Context, string) error) *graphTraversal { return &graphTraversal{ - extremityNodesFn: leaves, - adjacentNodesFn: getParents, - filterAdjacentByStatusFn: filterChildren, + extremityNodesFn: (*Graph).Leaves, + adjacentNodesFn: (*Vertex).GetParents, + filterAdjacentByStatusFn: (*Graph).FilterChildren, adjacentServiceStatusToSkip: ServiceStopped, targetServiceStatus: ServiceStarted, visitorFn: visitorFn, @@ -66,9 +66,9 @@ func upDirectionTraversal(visitorFn func(context.Context, string) error) *graphT func downDirectionTraversal(visitorFn func(context.Context, string) error) *graphTraversal { return &graphTraversal{ - extremityNodesFn: roots, - adjacentNodesFn: getChildren, - filterAdjacentByStatusFn: filterParents, + extremityNodesFn: (*Graph).Roots, + adjacentNodesFn: (*Vertex).GetChildren, + filterAdjacentByStatusFn: (*Graph).FilterParents, adjacentServiceStatusToSkip: ServiceStarted, targetServiceStatus: ServiceStopped, visitorFn: visitorFn, @@ -219,10 +219,6 @@ type Vertex struct { Parents map[string]*Vertex } -func getParents(v *Vertex) []*Vertex { - return v.GetParents() -} - // GetParents returns a slice with the parent vertices of the Vertex func (v *Vertex) GetParents() []*Vertex { var res []*Vertex @@ -232,10 +228,6 @@ func (v *Vertex) GetParents() []*Vertex { return res } -func getChildren(v *Vertex) []*Vertex { - return v.GetChildren() -} - // getAncestors return all descendents for a vertex, might contain duplicates func getAncestors(v *Vertex) []*Vertex { var descendents []*Vertex @@ -339,10 +331,6 @@ func (g *Graph) AddEdge(source string, destination string) error { return nil } -func leaves(g *Graph) []*Vertex { - return g.Leaves() -} - // Leaves returns the slice of leaves of the graph func (g *Graph) Leaves() []*Vertex { g.lock.Lock() @@ -358,10 +346,6 @@ func (g *Graph) Leaves() []*Vertex { return res } -func roots(g *Graph) []*Vertex { - return g.Roots() -} - // Roots returns the slice of "Roots" of the graph func (g *Graph) Roots() []*Vertex { g.lock.Lock() @@ -383,10 +367,6 @@ func (g *Graph) UpdateStatus(key string, status ServiceStatus) { g.Vertices[key].Status = status } -func filterChildren(g *Graph, k string, s ServiceStatus) []*Vertex { - return g.FilterChildren(k, s) -} - // FilterChildren returns children of a certain vertex that are in a certain status func (g *Graph) FilterChildren(key string, status ServiceStatus) []*Vertex { g.lock.Lock() @@ -404,10 +384,6 @@ func (g *Graph) FilterChildren(key string, status ServiceStatus) []*Vertex { return res } -func filterParents(g *Graph, k string, s ServiceStatus) []*Vertex { - return g.FilterParents(k, s) -} - // FilterParents returns the parents of a certain vertex that are in a certain status func (g *Graph) FilterParents(key string, status ServiceStatus) []*Vertex { g.lock.Lock() @@ -462,17 +438,7 @@ func (g *Graph) visit(key string, path []string, discovered []string, finished [ } } - discovered = remove(discovered, key) + discovered = slices.DeleteFunc(discovered, func(s string) bool { return s == key }) finished = append(finished, key) return discovered, finished, nil } - -func remove(slice []string, item string) []string { - var s []string - for _, i := range slice { - if i != item { - s = append(s, i) - } - } - return s -} diff --git a/pkg/compose/down.go b/pkg/compose/down.go index 758b9868a2..abc6f3a826 100644 --- a/pkg/compose/down.go +++ b/pkg/compose/down.go @@ -252,21 +252,10 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s func (s *composeService) removeImage(ctx context.Context, image string) error { id := fmt.Sprintf("Image %s", image) - s.events.On(newEvent(id, api.Working, "Removing")) - _, err := s.apiClient().ImageRemove(ctx, image, client.ImageRemoveOptions{}) - if err == nil { - s.events.On(newEvent(id, api.Done, "Removed")) - return nil - } - if errdefs.IsConflict(err) { - s.events.On(newEvent(id, api.Warning, "Resource is still in use")) - return nil - } - if errdefs.IsNotFound(err) { - s.events.On(newEvent(id, api.Done, "Warning: No resource found to remove")) - return nil - } - return err + return s.removeResource(id, func() error { + _, err := s.apiClient().ImageRemove(ctx, image, client.ImageRemoveOptions{}) + return err + }) } func (s *composeService) removeVolume(ctx context.Context, id string) error { @@ -278,20 +267,29 @@ func (s *composeService) removeVolume(ctx context.Context, id string) error { return nil } - s.events.On(newEvent(resource, api.Working, "Removing")) - _, err = s.apiClient().VolumeRemove(ctx, id, client.VolumeRemoveOptions{ - Force: true, + return s.removeResource(resource, func() error { + _, err := s.apiClient().VolumeRemove(ctx, id, client.VolumeRemoveOptions{ + Force: true, + }) + return err }) +} + +// removeResource emits a "Removing" progress event, calls op, then emits the appropriate +// completion event based on the error: nil→Removed, conflict→still-in-use warning, not-found→gone warning. +func (s *composeService) removeResource(eventID string, op func() error) error { + s.events.On(newEvent(eventID, api.Working, "Removing")) + err := op() if err == nil { - s.events.On(newEvent(resource, api.Done, "Removed")) + s.events.On(newEvent(eventID, api.Done, "Removed")) return nil } if errdefs.IsConflict(err) { - s.events.On(newEvent(resource, api.Warning, "Resource is still in use")) + s.events.On(newEvent(eventID, api.Warning, "Resource is still in use")) return nil } if errdefs.IsNotFound(err) { - s.events.On(newEvent(resource, api.Done, "Warning: No resource found to remove")) + s.events.On(newEvent(eventID, api.Done, "Warning: No resource found to remove")) return nil } return err @@ -299,7 +297,7 @@ func (s *composeService) removeVolume(ctx context.Context, id string) error { func (s *composeService) stopContainer(ctx context.Context, service *types.ServiceConfig, ctr containerType.Summary, timeout *time.Duration, listener api.ContainerEventListener) error { eventName := getContainerProgressName(ctr) - s.events.On(stoppingEvent(eventName)) + s.events.On(newEvent(eventName, api.Working, api.StatusStopping)) if service != nil { for _, hook := range service.PreStop { @@ -321,7 +319,7 @@ func (s *composeService) stopContainer(ctx context.Context, service *types.Servi s.events.On(errorEvent(eventName, "Error while Stopping")) return err } - s.events.On(stoppedEvent(eventName)) + s.events.On(newEvent(eventName, api.Done, api.StatusStopped)) return nil } diff --git a/pkg/compose/filters.go b/pkg/compose/filters.go index ebac9eb82d..27ecd03118 100644 --- a/pkg/compose/filters.go +++ b/pkg/compose/filters.go @@ -24,16 +24,21 @@ import ( "github.com/docker/compose/v5/pkg/api" ) +// labelFilter returns a label filter string of the form "key=value". +func labelFilter(key, value string) string { + return fmt.Sprintf("%s=%s", key, value) +} + func projectFilter(projectName string) client.Filters { - return make(client.Filters).Add("label", fmt.Sprintf("%s=%s", api.ProjectLabel, projectName)) + return make(client.Filters).Add("label", labelFilter(api.ProjectLabel, projectName)) } func serviceFilter(serviceName string) string { - return fmt.Sprintf("%s=%s", api.ServiceLabel, serviceName) + return labelFilter(api.ServiceLabel, serviceName) } func networkFilter(name string) string { - return fmt.Sprintf("%s=%s", api.NetworkLabel, name) + return labelFilter(api.NetworkLabel, name) } func oneOffFilter(b bool) string { @@ -45,9 +50,5 @@ func oneOffFilter(b bool) string { } func containerNumberFilter(index int) string { - return fmt.Sprintf("%s=%d", api.ContainerNumberLabel, index) -} - -func hasConfigHashLabel() string { - return api.ConfigHashLabel + return labelFilter(api.ContainerNumberLabel, fmt.Sprintf("%d", index)) } diff --git a/pkg/compose/kill.go b/pkg/compose/kill.go index 3fc7444080..c6caffc230 100644 --- a/pkg/compose/kill.go +++ b/pkg/compose/kill.go @@ -22,7 +22,6 @@ import ( "github.com/moby/moby/api/types/container" "github.com/moby/moby/client" - "golang.org/x/sync/errgroup" "github.com/docker/compose/v5/pkg/api" ) @@ -57,21 +56,17 @@ func (s *composeService) kill(ctx context.Context, projectName string, options a return api.ErrNoResources } - eg, ctx := errgroup.WithContext(ctx) - containers.forEach(func(ctr container.Summary) { - eg.Go(func() error { - eventName := getContainerProgressName(ctr) - s.events.On(killingEvent(eventName)) - _, err := s.apiClient().ContainerKill(ctx, ctr.ID, client.ContainerKillOptions{ - Signal: options.Signal, - }) - if err != nil { - s.events.On(errorEvent(eventName, "Error while Killing")) - return err - } - s.events.On(killedEvent(eventName)) - return nil + return forEachContainerConcurrent(ctx, containers, func(ctx context.Context, ctr container.Summary) error { + eventName := getContainerProgressName(ctr) + s.events.On(newEvent(eventName, api.Working, api.StatusKilling)) + _, err := s.apiClient().ContainerKill(ctx, ctr.ID, client.ContainerKillOptions{ + Signal: options.Signal, }) + if err != nil { + s.events.On(errorEvent(eventName, "Error while Killing")) + return err + } + s.events.On(newEvent(eventName, api.Done, api.StatusKilled)) + return nil }) - return eg.Wait() } diff --git a/pkg/compose/kill_test.go b/pkg/compose/kill_test.go index cc71ed7ccf..a55ea6f2d9 100644 --- a/pkg/compose/kill_test.go +++ b/pkg/compose/kill_test.go @@ -44,7 +44,7 @@ func TestKillAll(t *testing.T) { name := strings.ToLower(testProject) api.EXPECT().ContainerList(t.Context(), client.ContainerListOptions{ - Filters: projectFilter(name).Add("label", hasConfigHashLabel()), + Filters: projectFilter(name).Add("label", compose.ConfigHashLabel), }).Return(client.ContainerListResult{ Items: []container.Summary{ testContainer("service1", "123", false), @@ -83,7 +83,7 @@ func TestKillSignal(t *testing.T) { name := strings.ToLower(testProject) listOptions := client.ContainerListOptions{ - Filters: projectFilter(name).Add("label", serviceFilter(serviceName), hasConfigHashLabel()), + Filters: projectFilter(name).Add("label", serviceFilter(serviceName), compose.ConfigHashLabel), } api.EXPECT().ContainerList(t.Context(), listOptions).Return(client.ContainerListResult{ @@ -145,7 +145,7 @@ func anyCancellableContext() gomock.Matcher { } func projectFilterListOpt(withOneOff bool) client.ContainerListOptions { - filter := projectFilter(strings.ToLower(testProject)).Add("label", hasConfigHashLabel()) + filter := projectFilter(strings.ToLower(testProject)).Add("label", compose.ConfigHashLabel) if !withOneOff { filter.Add("label", oneOffFilter(false)) } diff --git a/pkg/compose/logs_test.go b/pkg/compose/logs_test.go index 3b02ef965e..b0499f1560 100644 --- a/pkg/compose/logs_test.go +++ b/pkg/compose/logs_test.go @@ -98,7 +98,7 @@ func TestComposeService_Logs_Demux(t *testing.T) { api.EXPECT().ContainerList(t.Context(), client.ContainerListOptions{ All: true, - Filters: projectFilter(name).Add("label", oneOffFilter(false), hasConfigHashLabel()), + Filters: projectFilter(name).Add("label", oneOffFilter(false), compose.ConfigHashLabel), }).Return( client.ContainerListResult{ Items: []containerType.Summary{ @@ -166,7 +166,7 @@ func TestComposeService_Logs_ServiceFiltering(t *testing.T) { api.EXPECT().ContainerList(t.Context(), client.ContainerListOptions{ All: true, - Filters: projectFilter(name).Add("label", oneOffFilter(false), hasConfigHashLabel()), + Filters: projectFilter(name).Add("label", oneOffFilter(false), compose.ConfigHashLabel), }).Return( client.ContainerListResult{ Items: []containerType.Summary{ diff --git a/pkg/compose/monitor.go b/pkg/compose/monitor.go index d544d5620a..f405a5d5b4 100644 --- a/pkg/compose/monitor.go +++ b/pkg/compose/monitor.go @@ -60,7 +60,7 @@ func (c *monitor) Start(ctx context.Context) error { All: true, Filters: projectFilter(c.project).Add("label", oneOffFilter(false), - hasConfigHashLabel(), + api.ConfigHashLabel, ), }) if err != nil { diff --git a/pkg/compose/pause.go b/pkg/compose/pause.go index 2ec644a44e..83d9937411 100644 --- a/pkg/compose/pause.go +++ b/pkg/compose/pause.go @@ -22,7 +22,6 @@ import ( "github.com/moby/moby/api/types/container" "github.com/moby/moby/client" - "golang.org/x/sync/errgroup" "github.com/docker/compose/v5/pkg/api" ) @@ -43,18 +42,13 @@ func (s *composeService) pause(ctx context.Context, projectName string, options containers = containers.filter(isService(options.Project.ServiceNames()...)) } - eg, ctx := errgroup.WithContext(ctx) - containers.forEach(func(container container.Summary) { - eg.Go(func() error { - _, err := s.apiClient().ContainerPause(ctx, container.ID, client.ContainerPauseOptions{}) - if err == nil { - eventName := getContainerProgressName(container) - s.events.On(newEvent(eventName, api.Done, "Paused")) - } - return err - }) + return forEachContainerConcurrent(ctx, containers, func(ctx context.Context, ctr container.Summary) error { + _, err := s.apiClient().ContainerPause(ctx, ctr.ID, client.ContainerPauseOptions{}) + if err == nil { + s.events.On(newEvent(getContainerProgressName(ctr), api.Done, "Paused")) + } + return err }) - return eg.Wait() } func (s *composeService) UnPause(ctx context.Context, projectName string, options api.PauseOptions) error { @@ -73,16 +67,11 @@ func (s *composeService) unPause(ctx context.Context, projectName string, option containers = containers.filter(isService(options.Project.ServiceNames()...)) } - eg, ctx := errgroup.WithContext(ctx) - containers.forEach(func(ctr container.Summary) { - eg.Go(func() error { - _, err = s.apiClient().ContainerUnpause(ctx, ctr.ID, client.ContainerUnpauseOptions{}) - if err == nil { - eventName := getContainerProgressName(ctr) - s.events.On(newEvent(eventName, api.Done, "Unpaused")) - } - return err - }) + return forEachContainerConcurrent(ctx, containers, func(ctx context.Context, ctr container.Summary) error { + _, err := s.apiClient().ContainerUnpause(ctx, ctr.ID, client.ContainerUnpauseOptions{}) + if err == nil { + s.events.On(newEvent(getContainerProgressName(ctr), api.Done, "Unpaused")) + } + return err }) - return eg.Wait() } diff --git a/pkg/compose/progress.go b/pkg/compose/progress.go index 26f9b5d859..6e691cf0c9 100644 --- a/pkg/compose/progress.go +++ b/pkg/compose/progress.go @@ -52,66 +52,11 @@ func creatingEvent(id string) api.Resource { return newEvent(id, api.Working, api.StatusCreating) } -// startingEvent creates a new Starting in progress Resource -func startingEvent(id string) api.Resource { - return newEvent(id, api.Working, api.StatusStarting) -} - -// startedEvent creates a new Started in progress Resource -func startedEvent(id string) api.Resource { - return newEvent(id, api.Done, api.StatusStarted) -} - -// waiting creates a new waiting event -func waiting(id string) api.Resource { - return newEvent(id, api.Working, api.StatusWaiting) -} - -// healthy creates a new healthy event -func healthy(id string) api.Resource { - return newEvent(id, api.Done, api.StatusHealthy) -} - -// exited creates a new exited event -func exited(id string) api.Resource { - return newEvent(id, api.Done, api.StatusExited) -} - -// restartingEvent creates a new Restarting in progress Resource -func restartingEvent(id string) api.Resource { - return newEvent(id, api.Working, api.StatusRestarting) -} - -// runningEvent creates a new Running in progress Resource -func runningEvent(id string) api.Resource { - return newEvent(id, api.Done, api.StatusRunning) -} - // createdEvent creates a new Created (done) Resource func createdEvent(id string) api.Resource { return newEvent(id, api.Done, api.StatusCreated) } -// stoppingEvent creates a new Stopping in progress Resource -func stoppingEvent(id string) api.Resource { - return newEvent(id, api.Working, api.StatusStopping) -} - -// stoppedEvent creates a new Stopping in progress Resource -func stoppedEvent(id string) api.Resource { - return newEvent(id, api.Done, api.StatusStopped) -} - -// killingEvent creates a new Killing in progress Resource -func killingEvent(id string) api.Resource { - return newEvent(id, api.Working, api.StatusKilling) -} - -// killedEvent creates a new Killed in progress Resource -func killedEvent(id string) api.Resource { - return newEvent(id, api.Done, api.StatusKilled) -} - // removingEvent creates a new Removing in progress Resource func removingEvent(id string) api.Resource { return newEvent(id, api.Working, api.StatusRemoving) @@ -132,17 +77,22 @@ func builtEvent(id string) api.Resource { return newEvent("Image "+id, api.Done, api.StatusBuilt) } -// pullingEvent creates a new pulling (in progress) Resource -func pullingEvent(id string) api.Resource { - return newEvent("Image "+id, api.Working, api.StatusPulling) +// waiting creates a new waiting event; kept as a named func for use as a function value. +func waiting(id string) api.Resource { + return newEvent(id, api.Working, api.StatusWaiting) } -// pulledEvent creates a new pulled (done) Resource -func pulledEvent(id string) api.Resource { - return newEvent("Image "+id, api.Done, api.StatusPulled) +// healthy creates a new healthy event; kept as a named func for use as a function value. +func healthy(id string) api.Resource { + return newEvent(id, api.Done, api.StatusHealthy) +} + +// exited creates a new exited event; kept as a named func for use as a function value. +func exited(id string) api.Resource { + return newEvent(id, api.Done, api.StatusExited) } -// skippedEvent creates a new Skipped Resource +// skippedEvent creates a new Skipped Resource; kept as a named func for use as a function value. func skippedEvent(id string, reason string) api.Resource { return api.Resource{ ID: id, diff --git a/pkg/compose/ps_test.go b/pkg/compose/ps_test.go index 1bca7bcc6f..390da28353 100644 --- a/pkg/compose/ps_test.go +++ b/pkg/compose/ps_test.go @@ -38,7 +38,7 @@ func TestPs(t *testing.T) { assert.NilError(t, err) listOpts := client.ContainerListOptions{ - Filters: projectFilter(strings.ToLower(testProject)).Add("label", hasConfigHashLabel(), oneOffFilter(false)), + Filters: projectFilter(strings.ToLower(testProject)).Add("label", compose.ConfigHashLabel, oneOffFilter(false)), All: false, } c1, inspect1 := containerDetails("service1", "123", containerType.StateRunning, containerType.Healthy, 0) diff --git a/pkg/compose/pull.go b/pkg/compose/pull.go index 8a02dc719b..f1b6a64a12 100644 --- a/pkg/compose/pull.go +++ b/pkg/compose/pull.go @@ -175,7 +175,7 @@ func getUnwrappedErrorMessage(err error) string { func (s *composeService) pullServiceImage(ctx context.Context, service types.ServiceConfig, quietPull bool, defaultPlatform string) (string, error) { resource := "Image " + service.Image - s.events.On(pullingEvent(service.Image)) + s.events.On(newEvent(resource, api.Working, api.StatusPulling)) ref, err := reference.ParseNormalizedNamed(service.Image) if err != nil { return "", err @@ -246,7 +246,7 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser toPullProgressEvent(resource, jm, s.events) } } - s.events.On(pulledEvent(service.Image)) + s.events.On(newEvent(resource, api.Done, api.StatusPulled)) inspected, err := s.apiClient().ImageInspect(ctx, service.Image) if err != nil { diff --git a/pkg/compose/remove.go b/pkg/compose/remove.go index 8b81a61f8b..017de4a009 100644 --- a/pkg/compose/remove.go +++ b/pkg/compose/remove.go @@ -21,14 +21,13 @@ import ( "fmt" "strings" - "github.com/moby/moby/api/types/container" "github.com/moby/moby/client" "golang.org/x/sync/errgroup" "github.com/docker/compose/v5/pkg/api" ) -func (s *composeService) Remove(ctx context.Context, projectName string, options api.RemoveOptions) error { +func (s *composeService) Remove(ctx context.Context, projectName string, options api.RemoveOptions) error { //nolint:gocyclo projectName = strings.ToLower(projectName) if options.Stop { @@ -71,9 +70,9 @@ func (s *composeService) Remove(ctx context.Context, projectName string, options } var names []string - stoppedContainers.forEach(func(c container.Summary) { + for _, c := range stoppedContainers { names = append(names, getCanonicalContainerName(c)) - }) + } if len(names) == 0 { return api.ErrNoResources diff --git a/pkg/compose/restart.go b/pkg/compose/restart.go index 9d83bff9a4..97d10b2dcb 100644 --- a/pkg/compose/restart.go +++ b/pkg/compose/restart.go @@ -93,14 +93,14 @@ func (s *composeService) restart(ctx context.Context, projectName string, option } } eventName := getContainerProgressName(ctr) - s.events.On(restartingEvent(eventName)) + s.events.On(newEvent(eventName, api.Working, api.StatusRestarting)) _, err = s.apiClient().ContainerRestart(ctx, ctr.ID, client.ContainerRestartOptions{ Timeout: utils.DurationSecondToInt(options.Timeout), }) if err != nil { return err } - s.events.On(startedEvent(eventName)) + s.events.On(newEvent(eventName, api.Done, api.StatusStarted)) for _, hook := range def.PostStart { err = s.runHook(ctx, ctr, def, hook, nil) if err != nil { diff --git a/pkg/utils/set.go b/pkg/utils/set.go index 5a092d7c26..5d763bf4ca 100644 --- a/pkg/utils/set.go +++ b/pkg/utils/set.go @@ -51,9 +51,9 @@ func (s Set[T]) Remove(v T) bool { return ok } -func (s Set[T]) Clear() { - for v := range s { - delete(s, v) +func (s Set[T]) RemoveAll(elements ...T) { + for _, e := range elements { + s.Remove(e) } } @@ -65,12 +65,6 @@ func (s Set[T]) Elements() []T { return elements } -func (s Set[T]) RemoveAll(elements ...T) { - for _, e := range elements { - s.Remove(e) - } -} - func (s Set[T]) Diff(other Set[T]) Set[T] { out := make(Set[T]) for k := range s { @@ -81,6 +75,18 @@ func (s Set[T]) Diff(other Set[T]) Set[T] { return out } +// Clear removes all elements from the set. +// +// Deprecated: Clear is retained for API compatibility; prefer re-assigning a new Set. +func (s Set[T]) Clear() { + for v := range s { + delete(s, v) + } +} + +// Union returns a new set containing all elements from both s and other. +// +// Deprecated: Union is retained for API compatibility. func (s Set[T]) Union(other Set[T]) Set[T] { out := make(Set[T]) for k := range s { diff --git a/pkg/utils/set_test.go b/pkg/utils/set_test.go index 452bacc1c7..3a18961972 100644 --- a/pkg/utils/set_test.go +++ b/pkg/utils/set_test.go @@ -15,7 +15,6 @@ package utils import ( - "slices" "testing" "gotest.tools/v3/assert" @@ -33,16 +32,3 @@ func TestSet_Diff(t *testing.T) { assert.DeepEqual(t, []int{1}, a.Diff(b).Elements()) assert.DeepEqual(t, []int{3}, b.Diff(a).Elements()) } - -func TestSet_Union(t *testing.T) { - a := NewSet[int](1, 2) - b := NewSet[int](2, 3) - - actual := a.Union(b).Elements() - slices.Sort(actual) - assert.DeepEqual(t, []int{1, 2, 3}, actual) - - actual = b.Union(a).Elements() - slices.Sort(actual) - assert.DeepEqual(t, []int{1, 2, 3}, actual) -}