diff --git a/cmd/compose/remove.go b/cmd/compose/remove.go index f84621387..b4b61d580 100644 --- a/cmd/compose/remove.go +++ b/cmd/compose/remove.go @@ -65,7 +65,7 @@ func runRemove(ctx context.Context, backend api.Service, opts removeOptions, ser } if opts.stop { - err := backend.Stop(ctx, project, api.StopOptions{ + err := backend.Stop(ctx, project.Name, api.StopOptions{ Services: services, }) if err != nil { diff --git a/cmd/compose/restart.go b/cmd/compose/restart.go index 600d2b6fa..f691c31fb 100644 --- a/cmd/compose/restart.go +++ b/cmd/compose/restart.go @@ -49,13 +49,13 @@ func restartCommand(p *projectOptions, backend api.Service) *cobra.Command { } func runRestart(ctx context.Context, backend api.Service, opts restartOptions, services []string) error { - project, err := opts.toProject(services) + projectName, err := opts.toProjectName() if err != nil { return err } timeout := time.Duration(opts.timeout) * time.Second - return backend.Restart(ctx, project, api.RestartOptions{ + return backend.Restart(ctx, projectName, api.RestartOptions{ Timeout: &timeout, Services: services, }) diff --git a/cmd/compose/run.go b/cmd/compose/run.go index fc5f76421..575026204 100644 --- a/cmd/compose/run.go +++ b/cmd/compose/run.go @@ -240,5 +240,5 @@ func startDependencies(ctx context.Context, backend api.Service, project types.P if err := backend.Create(ctx, &project, api.CreateOptions{}); err != nil { return err } - return backend.Start(ctx, &project, api.StartOptions{}) + return backend.Start(ctx, project.Name, api.StartOptions{}) } diff --git a/cmd/compose/start.go b/cmd/compose/start.go index 8ad99c566..e9b67ae44 100644 --- a/cmd/compose/start.go +++ b/cmd/compose/start.go @@ -43,10 +43,12 @@ func startCommand(p *projectOptions, backend api.Service) *cobra.Command { } func runStart(ctx context.Context, backend api.Service, opts startOptions, services []string) error { - project, err := opts.toProject(services) + projectName, err := opts.toProjectName() if err != nil { return err } - return backend.Start(ctx, project, api.StartOptions{}) + return backend.Start(ctx, projectName, api.StartOptions{ + AttachTo: services, + }) } diff --git a/cmd/compose/stop.go b/cmd/compose/stop.go index d00d94f9d..39861cd68 100644 --- a/cmd/compose/stop.go +++ b/cmd/compose/stop.go @@ -53,7 +53,7 @@ func stopCommand(p *projectOptions, backend api.Service) *cobra.Command { } func runStop(ctx context.Context, backend api.Service, opts stopOptions, services []string) error { - project, err := opts.toProject(services) + projectName, err := opts.toProjectName() if err != nil { return err } @@ -63,7 +63,7 @@ func runStop(ctx context.Context, backend api.Service, opts stopOptions, service timeoutValue := time.Duration(opts.timeout) * time.Second timeout = &timeoutValue } - return backend.Stop(ctx, project, api.StopOptions{ + return backend.Stop(ctx, projectName, api.StopOptions{ Timeout: timeout, Services: services, }) diff --git a/pkg/api/api.go b/pkg/api/api.go index e28cf12a2..2c9e26802 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -37,11 +37,11 @@ type Service interface { // Create executes the equivalent to a `compose create` Create(ctx context.Context, project *types.Project, opts CreateOptions) error // Start executes the equivalent to a `compose start` - Start(ctx context.Context, project *types.Project, options StartOptions) error + Start(ctx context.Context, projectName string, options StartOptions) error // Restart restarts containers - Restart(ctx context.Context, project *types.Project, options RestartOptions) error + Restart(ctx context.Context, projectName string, options RestartOptions) error // Stop executes the equivalent to a `compose stop` - Stop(ctx context.Context, project *types.Project, options StopOptions) error + Stop(ctx context.Context, projectName string, options StopOptions) error // Up executes the equivalent to a `compose up` Up(ctx context.Context, project *types.Project, options UpOptions) error // Down executes the equivalent to a `compose down` diff --git a/pkg/api/proxy.go b/pkg/api/proxy.go index 6c3fe22f3..9a06b9cb4 100644 --- a/pkg/api/proxy.go +++ b/pkg/api/proxy.go @@ -28,9 +28,9 @@ type ServiceProxy struct { PushFn func(ctx context.Context, project *types.Project, options PushOptions) error PullFn func(ctx context.Context, project *types.Project, opts PullOptions) error CreateFn func(ctx context.Context, project *types.Project, opts CreateOptions) error - StartFn func(ctx context.Context, project *types.Project, options StartOptions) error - RestartFn func(ctx context.Context, project *types.Project, options RestartOptions) error - StopFn func(ctx context.Context, project *types.Project, options StopOptions) error + StartFn func(ctx context.Context, projectName string, options StartOptions) error + RestartFn func(ctx context.Context, projectName string, options RestartOptions) error + StopFn func(ctx context.Context, projectName string, options StopOptions) error UpFn func(ctx context.Context, project *types.Project, options UpOptions) error DownFn func(ctx context.Context, projectName string, options DownOptions) error LogsFn func(ctx context.Context, projectName string, consumer LogConsumer, options LogOptions) error @@ -141,36 +141,27 @@ func (s *ServiceProxy) Create(ctx context.Context, project *types.Project, optio } // Start implements Service interface -func (s *ServiceProxy) Start(ctx context.Context, project *types.Project, options StartOptions) error { +func (s *ServiceProxy) Start(ctx context.Context, projectName string, options StartOptions) error { if s.StartFn == nil { return ErrNotImplemented } - for _, i := range s.interceptors { - i(ctx, project) - } - return s.StartFn(ctx, project, options) + return s.StartFn(ctx, projectName, options) } // Restart implements Service interface -func (s *ServiceProxy) Restart(ctx context.Context, project *types.Project, options RestartOptions) error { +func (s *ServiceProxy) Restart(ctx context.Context, projectName string, options RestartOptions) error { if s.RestartFn == nil { return ErrNotImplemented } - for _, i := range s.interceptors { - i(ctx, project) - } - return s.RestartFn(ctx, project, options) + return s.RestartFn(ctx, projectName, options) } // Stop implements Service interface -func (s *ServiceProxy) Stop(ctx context.Context, project *types.Project, options StopOptions) error { +func (s *ServiceProxy) Stop(ctx context.Context, projectName string, options StopOptions) error { if s.StopFn == nil { return ErrNotImplemented } - for _, i := range s.interceptors { - i(ctx, project) - } - return s.StopFn(ctx, project, options) + return s.StopFn(ctx, projectName, options) } // Up implements Service interface diff --git a/pkg/compose/compose.go b/pkg/compose/compose.go index 7c31218cd..7c3c96a2b 100644 --- a/pkg/compose/compose.go +++ b/pkg/compose/compose.go @@ -24,6 +24,7 @@ import ( "strings" "github.com/docker/compose/v2/pkg/api" + "github.com/pkg/errors" "github.com/compose-spec/compose-go/types" "github.com/docker/cli/cli/config/configfile" @@ -92,3 +93,59 @@ func escapeDollarSign(marshal []byte) []byte { escDollar := []byte{'$', '$'} return bytes.ReplaceAll(marshal, dollar, escDollar) } + +// projectFromName builds a types.Project based on actual resources with compose labels set +func (s *composeService) projectFromName(containers Containers, projectName string, services ...string) (*types.Project, error) { + project := &types.Project{ + Name: projectName, + } + if len(containers) == 0 { + return project, errors.New("no such project: " + projectName) + } + set := map[string]*types.ServiceConfig{} + for _, c := range containers { + serviceLabel := c.Labels[api.ServiceLabel] + _, ok := set[serviceLabel] + if !ok { + set[serviceLabel] = &types.ServiceConfig{ + Name: serviceLabel, + Image: c.Image, + Labels: c.Labels, + } + } + set[serviceLabel].Scale++ + } + for _, service := range set { + dependencies := service.Labels[api.DependenciesLabel] + if len(dependencies) > 0 { + service.DependsOn = types.DependsOnConfig{} + for _, dc := range strings.Split(dependencies, ",") { + dcArr := strings.Split(dc, ":") + condition := ServiceConditionRunningOrHealthy + dependency := dcArr[0] + + // backward compatibility + if len(dcArr) > 1 { + condition = dcArr[1] + } + service.DependsOn[dependency] = types.ServiceDependency{Condition: condition} + } + } + project.Services = append(project.Services, *service) + } +SERVICES: + for _, qs := range services { + for _, es := range project.Services { + if es.Name == qs { + continue SERVICES + } + } + return project, errors.New("no such service: " + qs) + } + err := project.ForServices(services) + if err != nil { + return project, err + } + + return project, nil +} diff --git a/pkg/compose/create.go b/pkg/compose/create.go index d77e075db..ce2551251 100644 --- a/pkg/compose/create.go +++ b/pkg/compose/create.go @@ -432,8 +432,8 @@ func (s *composeService) prepareLabels(service types.ServiceConfig, number int) labels[api.ContainerNumberLabel] = strconv.Itoa(number) var dependencies []string - for s := range service.DependsOn { - dependencies = append(dependencies, s) + for s, d := range service.DependsOn { + dependencies = append(dependencies, s+":"+d.Condition) } labels[api.DependenciesLabel] = strings.Join(dependencies, ",") return labels, nil diff --git a/pkg/compose/down.go b/pkg/compose/down.go index fda34fbd9..60498ffa3 100644 --- a/pkg/compose/down.go +++ b/pkg/compose/down.go @@ -41,6 +41,7 @@ func (s *composeService) Down(ctx context.Context, projectName string, options a } func (s *composeService) down(ctx context.Context, projectName string, options api.DownOptions) error { + builtFromResources := options.Project == nil w := progress.ContextWriter(ctx) resourceToRemove := false @@ -50,8 +51,8 @@ func (s *composeService) down(ctx context.Context, projectName string, options a return err } - if options.Project == nil { - options.Project, err = s.projectFromLabels(ctx, containers.filter(isNotOneOff), projectName) + if builtFromResources { + options.Project, err = s.getProjectWithVolumes(ctx, containers, projectName) if err != nil { return err } @@ -232,34 +233,9 @@ func (s *composeService) removeContainers(ctx context.Context, w progress.Writer return eg.Wait() } -// projectFromLabels builds a types.Project based on actual resources with compose labels set -func (s *composeService) projectFromLabels(ctx context.Context, containers Containers, projectName string) (*types.Project, error) { - project := &types.Project{ - Name: projectName, - } - if len(containers) == 0 { - return project, nil - } - set := map[string]moby.Container{} - for _, c := range containers { - set[c.Labels[api.ServiceLabel]] = c - } - for s, c := range set { - service := types.ServiceConfig{ - Name: s, - Image: c.Image, - Labels: c.Labels, - } - dependencies := c.Labels[api.DependenciesLabel] - if len(dependencies) > 0 { - service.DependsOn = types.DependsOnConfig{} - for _, d := range strings.Split(dependencies, ",") { - service.DependsOn[d] = types.ServiceDependency{} - } - } - project.Services = append(project.Services, service) - } - +func (s *composeService) getProjectWithVolumes(ctx context.Context, containers Containers, projectName string) (*types.Project, error) { + containers = containers.filter(isNotOneOff) + project, _ := s.projectFromName(containers, projectName) volumes, err := s.apiClient.VolumeList(ctx, filters.NewArgs(projectFilter(projectName))) if err != nil { return nil, err @@ -273,6 +249,5 @@ func (s *composeService) projectFromLabels(ctx context.Context, containers Conta Labels: vol.Labels, } } - return project, nil } diff --git a/pkg/compose/restart.go b/pkg/compose/restart.go index 44592c62f..306b4d413 100644 --- a/pkg/compose/restart.go +++ b/pkg/compose/restart.go @@ -19,7 +19,6 @@ package compose import ( "context" - "github.com/compose-spec/compose-go/types" "github.com/docker/compose/v2/pkg/api" "golang.org/x/sync/errgroup" @@ -27,14 +26,20 @@ import ( "github.com/docker/compose/v2/pkg/utils" ) -func (s *composeService) Restart(ctx context.Context, project *types.Project, options api.RestartOptions) error { +func (s *composeService) Restart(ctx context.Context, projectName string, options api.RestartOptions) error { return progress.Run(ctx, func(ctx context.Context) error { - return s.restart(ctx, project, options) + return s.restart(ctx, projectName, options) }) } -func (s *composeService) restart(ctx context.Context, project *types.Project, options api.RestartOptions) error { - observedState, err := s.getContainers(ctx, project.Name, oneOffInclude, true) +func (s *composeService) restart(ctx context.Context, projectName string, options api.RestartOptions) error { + + observedState, err := s.getContainers(ctx, projectName, oneOffInclude, true) + if err != nil { + return err + } + + project, err := s.projectFromName(observedState, projectName, options.Services...) if err != nil { return err } diff --git a/pkg/compose/start.go b/pkg/compose/start.go index de31b3ea7..dca5358eb 100644 --- a/pkg/compose/start.go +++ b/pkg/compose/start.go @@ -28,15 +28,22 @@ import ( "github.com/docker/compose/v2/pkg/progress" ) -func (s *composeService) Start(ctx context.Context, project *types.Project, options api.StartOptions) error { +func (s *composeService) Start(ctx context.Context, projectName string, options api.StartOptions) error { return progress.Run(ctx, func(ctx context.Context) error { - return s.start(ctx, project, options, nil) + return s.start(ctx, projectName, options, nil) }) } -func (s *composeService) start(ctx context.Context, project *types.Project, options api.StartOptions, listener api.ContainerEventListener) error { - if len(options.AttachTo) == 0 { - options.AttachTo = project.ServiceNames() +func (s *composeService) start(ctx context.Context, projectName string, options api.StartOptions, listener api.ContainerEventListener) error { + var containers Containers + containers, err := s.getContainers(ctx, projectName, oneOffExclude, true) + if err != nil { + return err + } + + project, err := s.projectFromName(containers, projectName, options.AttachTo...) + if err != nil { + return err } eg, ctx := errgroup.WithContext(ctx) @@ -53,7 +60,7 @@ func (s *composeService) start(ctx context.Context, project *types.Project, opti }) } - err := InDependencyOrder(ctx, project, func(c context.Context, name string) error { + err = InDependencyOrder(ctx, project, func(c context.Context, name string) error { service, err := project.GetService(name) if err != nil { return err diff --git a/pkg/compose/stop.go b/pkg/compose/stop.go index 6655aa2b1..9f3943783 100644 --- a/pkg/compose/stop.go +++ b/pkg/compose/stop.go @@ -21,25 +21,29 @@ import ( "github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/progress" - - "github.com/compose-spec/compose-go/types" ) -func (s *composeService) Stop(ctx context.Context, project *types.Project, options api.StopOptions) error { +func (s *composeService) Stop(ctx context.Context, projectName string, options api.StopOptions) error { return progress.Run(ctx, func(ctx context.Context) error { - return s.stop(ctx, project, options) + return s.stop(ctx, projectName, options) }) } -func (s *composeService) stop(ctx context.Context, project *types.Project, options api.StopOptions) error { +func (s *composeService) stop(ctx context.Context, projectName string, options api.StopOptions) error { w := progress.ContextWriter(ctx) services := options.Services if len(services) == 0 { - services = project.ServiceNames() + services = []string{} } + var containers Containers - containers, err := s.getContainers(ctx, project.Name, oneOffInclude, true, services...) + containers, err := s.getContainers(ctx, projectName, oneOffInclude, true, services...) + if err != nil { + return err + } + + project, err := s.projectFromName(containers, projectName, services...) if err != nil { return err } diff --git a/pkg/compose/stop_test.go b/pkg/compose/stop_test.go index 4b8e345b6..29cecbd3f 100644 --- a/pkg/compose/stop_test.go +++ b/pkg/compose/stop_test.go @@ -25,7 +25,6 @@ import ( compose "github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/mocks" - "github.com/compose-spec/compose-go/types" moby "github.com/docker/docker/api/types" "github.com/golang/mock/gomock" "gotest.tools/v3/assert" @@ -50,13 +49,7 @@ func TestStopTimeout(t *testing.T) { api.EXPECT().ContainerStop(gomock.Any(), "456", &timeout).Return(nil) api.EXPECT().ContainerStop(gomock.Any(), "789", &timeout).Return(nil) - err := tested.Stop(ctx, &types.Project{ - Name: strings.ToLower(testProject), - Services: []types.ServiceConfig{ - {Name: "service1"}, - {Name: "service2"}, - }, - }, compose.StopOptions{ + err := tested.Stop(ctx, strings.ToLower(testProject), compose.StopOptions{ Timeout: &timeout, }) assert.NilError(t, err) diff --git a/pkg/compose/up.go b/pkg/compose/up.go index 88d0df1f8..3ee07f1cc 100644 --- a/pkg/compose/up.go +++ b/pkg/compose/up.go @@ -38,7 +38,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options return err } if options.Start.Attach == nil { - return s.start(ctx, project, options.Start, nil) + return s.start(ctx, project.Name, options.Start, nil) } return nil }) @@ -65,7 +65,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options }) }() - return s.Stop(ctx, project, api.StopOptions{ + return s.Stop(ctx, project.Name, api.StopOptions{ Services: options.Create.Services, }) }) @@ -85,7 +85,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options return err }) - err = s.start(ctx, project, options.Start, printer.HandleEvent) + err = s.start(ctx, project.Name, options.Start, printer.HandleEvent) if err != nil { return err }