Merge pull request #9148 from arhemd/issue#9147

Using start, stop, restart from outside the working directory using --project-name (#9147)
This commit is contained in:
Guillaume Lours 2022-02-27 09:45:36 +01:00 committed by GitHub
commit 9d73cc88cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 125 additions and 91 deletions

View File

@ -65,7 +65,7 @@ func runRemove(ctx context.Context, backend api.Service, opts removeOptions, ser
} }
if opts.stop { if opts.stop {
err := backend.Stop(ctx, project, api.StopOptions{ err := backend.Stop(ctx, project.Name, api.StopOptions{
Services: services, Services: services,
}) })
if err != nil { if err != nil {

View File

@ -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 { 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 { if err != nil {
return err return err
} }
timeout := time.Duration(opts.timeout) * time.Second timeout := time.Duration(opts.timeout) * time.Second
return backend.Restart(ctx, project, api.RestartOptions{ return backend.Restart(ctx, projectName, api.RestartOptions{
Timeout: &timeout, Timeout: &timeout,
Services: services, Services: services,
}) })

View File

@ -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 { if err := backend.Create(ctx, &project, api.CreateOptions{}); err != nil {
return err return err
} }
return backend.Start(ctx, &project, api.StartOptions{}) return backend.Start(ctx, project.Name, api.StartOptions{})
} }

View File

@ -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 { 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 { if err != nil {
return err return err
} }
return backend.Start(ctx, project, api.StartOptions{}) return backend.Start(ctx, projectName, api.StartOptions{
AttachTo: services,
})
} }

View File

@ -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 { 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 { if err != nil {
return err return err
} }
@ -63,7 +63,7 @@ func runStop(ctx context.Context, backend api.Service, opts stopOptions, service
timeoutValue := time.Duration(opts.timeout) * time.Second timeoutValue := time.Duration(opts.timeout) * time.Second
timeout = &timeoutValue timeout = &timeoutValue
} }
return backend.Stop(ctx, project, api.StopOptions{ return backend.Stop(ctx, projectName, api.StopOptions{
Timeout: timeout, Timeout: timeout,
Services: services, Services: services,
}) })

View File

@ -37,11 +37,11 @@ type Service interface {
// Create executes the equivalent to a `compose create` // Create executes the equivalent to a `compose create`
Create(ctx context.Context, project *types.Project, opts CreateOptions) error Create(ctx context.Context, project *types.Project, opts CreateOptions) error
// Start executes the equivalent to a `compose start` // 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 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 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 executes the equivalent to a `compose up`
Up(ctx context.Context, project *types.Project, options UpOptions) error Up(ctx context.Context, project *types.Project, options UpOptions) error
// Down executes the equivalent to a `compose down` // Down executes the equivalent to a `compose down`

View File

@ -28,9 +28,9 @@ type ServiceProxy struct {
PushFn func(ctx context.Context, project *types.Project, options PushOptions) error PushFn func(ctx context.Context, project *types.Project, options PushOptions) error
PullFn func(ctx context.Context, project *types.Project, opts PullOptions) error PullFn func(ctx context.Context, project *types.Project, opts PullOptions) error
CreateFn func(ctx context.Context, project *types.Project, opts CreateOptions) error CreateFn func(ctx context.Context, project *types.Project, opts CreateOptions) error
StartFn func(ctx context.Context, project *types.Project, options StartOptions) error StartFn func(ctx context.Context, projectName string, options StartOptions) error
RestartFn func(ctx context.Context, project *types.Project, options RestartOptions) error RestartFn func(ctx context.Context, projectName string, options RestartOptions) error
StopFn func(ctx context.Context, project *types.Project, options StopOptions) error StopFn func(ctx context.Context, projectName string, options StopOptions) error
UpFn func(ctx context.Context, project *types.Project, options UpOptions) error UpFn func(ctx context.Context, project *types.Project, options UpOptions) error
DownFn func(ctx context.Context, projectName string, options DownOptions) error DownFn func(ctx context.Context, projectName string, options DownOptions) error
LogsFn func(ctx context.Context, projectName string, consumer LogConsumer, options LogOptions) 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 // 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 { if s.StartFn == nil {
return ErrNotImplemented return ErrNotImplemented
} }
for _, i := range s.interceptors { return s.StartFn(ctx, projectName, options)
i(ctx, project)
}
return s.StartFn(ctx, project, options)
} }
// Restart implements Service interface // 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 { if s.RestartFn == nil {
return ErrNotImplemented return ErrNotImplemented
} }
for _, i := range s.interceptors { return s.RestartFn(ctx, projectName, options)
i(ctx, project)
}
return s.RestartFn(ctx, project, options)
} }
// Stop implements Service interface // 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 { if s.StopFn == nil {
return ErrNotImplemented return ErrNotImplemented
} }
for _, i := range s.interceptors { return s.StopFn(ctx, projectName, options)
i(ctx, project)
}
return s.StopFn(ctx, project, options)
} }
// Up implements Service interface // Up implements Service interface

View File

@ -24,6 +24,7 @@ import (
"strings" "strings"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/pkg/errors"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
@ -92,3 +93,59 @@ func escapeDollarSign(marshal []byte) []byte {
escDollar := []byte{'$', '$'} escDollar := []byte{'$', '$'}
return bytes.ReplaceAll(marshal, dollar, escDollar) 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
}

View File

@ -432,8 +432,8 @@ func (s *composeService) prepareLabels(service types.ServiceConfig, number int)
labels[api.ContainerNumberLabel] = strconv.Itoa(number) labels[api.ContainerNumberLabel] = strconv.Itoa(number)
var dependencies []string var dependencies []string
for s := range service.DependsOn { for s, d := range service.DependsOn {
dependencies = append(dependencies, s) dependencies = append(dependencies, s+":"+d.Condition)
} }
labels[api.DependenciesLabel] = strings.Join(dependencies, ",") labels[api.DependenciesLabel] = strings.Join(dependencies, ",")
return labels, nil return labels, nil

View File

@ -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 { func (s *composeService) down(ctx context.Context, projectName string, options api.DownOptions) error {
builtFromResources := options.Project == nil
w := progress.ContextWriter(ctx) w := progress.ContextWriter(ctx)
resourceToRemove := false resourceToRemove := false
@ -50,8 +51,8 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
return err return err
} }
if options.Project == nil { if builtFromResources {
options.Project, err = s.projectFromLabels(ctx, containers.filter(isNotOneOff), projectName) options.Project, err = s.getProjectWithVolumes(ctx, containers, projectName)
if err != nil { if err != nil {
return err return err
} }
@ -232,34 +233,9 @@ func (s *composeService) removeContainers(ctx context.Context, w progress.Writer
return eg.Wait() return eg.Wait()
} }
// projectFromLabels builds a types.Project based on actual resources with compose labels set func (s *composeService) getProjectWithVolumes(ctx context.Context, containers Containers, projectName string) (*types.Project, error) {
func (s *composeService) projectFromLabels(ctx context.Context, containers Containers, projectName string) (*types.Project, error) { containers = containers.filter(isNotOneOff)
project := &types.Project{ project, _ := s.projectFromName(containers, projectName)
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)
}
volumes, err := s.apiClient.VolumeList(ctx, filters.NewArgs(projectFilter(projectName))) volumes, err := s.apiClient.VolumeList(ctx, filters.NewArgs(projectFilter(projectName)))
if err != nil { if err != nil {
return nil, err return nil, err
@ -273,6 +249,5 @@ func (s *composeService) projectFromLabels(ctx context.Context, containers Conta
Labels: vol.Labels, Labels: vol.Labels,
} }
} }
return project, nil return project, nil
} }

View File

@ -19,7 +19,6 @@ package compose
import ( import (
"context" "context"
"github.com/compose-spec/compose-go/types"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
@ -27,14 +26,20 @@ import (
"github.com/docker/compose/v2/pkg/utils" "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 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 { func (s *composeService) restart(ctx context.Context, projectName string, options api.RestartOptions) error {
observedState, err := s.getContainers(ctx, project.Name, oneOffInclude, true)
observedState, err := s.getContainers(ctx, projectName, oneOffInclude, true)
if err != nil {
return err
}
project, err := s.projectFromName(observedState, projectName, options.Services...)
if err != nil { if err != nil {
return err return err
} }

View File

@ -28,15 +28,22 @@ import (
"github.com/docker/compose/v2/pkg/progress" "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 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 { func (s *composeService) start(ctx context.Context, projectName string, options api.StartOptions, listener api.ContainerEventListener) error {
if len(options.AttachTo) == 0 { var containers Containers
options.AttachTo = project.ServiceNames() 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) 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) service, err := project.GetService(name)
if err != nil { if err != nil {
return err return err

View File

@ -21,25 +21,29 @@ import (
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress" "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 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) w := progress.ContextWriter(ctx)
services := options.Services services := options.Services
if len(services) == 0 { if len(services) == 0 {
services = project.ServiceNames() services = []string{}
} }
var containers Containers 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 { if err != nil {
return err return err
} }

View File

@ -25,7 +25,6 @@ import (
compose "github.com/docker/compose/v2/pkg/api" compose "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/mocks" "github.com/docker/compose/v2/pkg/mocks"
"github.com/compose-spec/compose-go/types"
moby "github.com/docker/docker/api/types" moby "github.com/docker/docker/api/types"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"gotest.tools/v3/assert" "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(), "456", &timeout).Return(nil)
api.EXPECT().ContainerStop(gomock.Any(), "789", &timeout).Return(nil) api.EXPECT().ContainerStop(gomock.Any(), "789", &timeout).Return(nil)
err := tested.Stop(ctx, &types.Project{ err := tested.Stop(ctx, strings.ToLower(testProject), compose.StopOptions{
Name: strings.ToLower(testProject),
Services: []types.ServiceConfig{
{Name: "service1"},
{Name: "service2"},
},
}, compose.StopOptions{
Timeout: &timeout, Timeout: &timeout,
}) })
assert.NilError(t, err) assert.NilError(t, err)

View File

@ -38,7 +38,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
return err return err
} }
if options.Start.Attach == nil { if options.Start.Attach == nil {
return s.start(ctx, project, options.Start, nil) return s.start(ctx, project.Name, options.Start, nil)
} }
return 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, Services: options.Create.Services,
}) })
}) })
@ -85,7 +85,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
return err return err
}) })
err = s.start(ctx, project, options.Start, printer.HandleEvent) err = s.start(ctx, project.Name, options.Start, printer.HandleEvent)
if err != nil { if err != nil {
return err return err
} }