diff --git a/aci/compose.go b/aci/compose.go index 83c74c5f5..6fe92db0f 100644 --- a/aci/compose.go +++ b/aci/compose.go @@ -87,6 +87,12 @@ func (cs *aciComposeService) Copy(ctx context.Context, project *types.Project, o } func (cs *aciComposeService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error { + return progress.Run(ctx, func(ctx context.Context) error { + return cs.up(ctx, project) + }) +} + +func (cs *aciComposeService) up(ctx context.Context, project *types.Project) error { logrus.Debugf("Up on project with name %q", project.Name) if err := autocreateFileshares(ctx, project); err != nil { diff --git a/api/compose/api.go b/api/compose/api.go index 979e51771..b63c244fc 100644 --- a/api/compose/api.go +++ b/api/compose/api.go @@ -112,10 +112,14 @@ type CreateOptions struct { // StartOptions group options of the Start API type StartOptions struct { - // Attach will attach to service containers and send container logs and events - Attach ContainerEventListener - // Services passed in the command line to be started - Services []string + // Attach to container and forward logs if not nil + Attach LogConsumer + // AttachTo set the services to attach to + AttachTo []string + // CascadeStop stops the application when a container stops + CascadeStop bool + // ExitCodeFrom return exit code from specified service + ExitCodeFrom string } // RestartOptions group options of the Restart API @@ -136,10 +140,8 @@ type StopOptions struct { // UpOptions group options of the Up API type UpOptions struct { - // Detach will create services and return immediately - Detach bool - // QuietPull makes the pulling process quiet - QuietPull bool + Create CreateOptions + Start StartOptions } // DownOptions group options of the Down API diff --git a/cli/cmd/compose/compose.go b/cli/cmd/compose/compose.go index c3bb0559d..2b724061b 100644 --- a/cli/cmd/compose/compose.go +++ b/cli/cmd/compose/compose.go @@ -21,6 +21,7 @@ import ( "fmt" "os" "os/signal" + "path/filepath" "strings" "syscall" @@ -90,14 +91,43 @@ type projectOptions struct { // ProjectFunc does stuff within a types.Project type ProjectFunc func(ctx context.Context, project *types.Project) error +// ProjectServicesFunc does stuff within a types.Project and a selection of services +type ProjectServicesFunc func(ctx context.Context, project *types.Project, services []string) error + // WithServices creates a cobra run command from a ProjectFunc based on configured project options and selected services -func (o *projectOptions) WithServices(services []string, fn ProjectFunc) func(cmd *cobra.Command, args []string) error { - return Adapt(func(ctx context.Context, strings []string) error { - project, err := o.toProject(services) +func (o *projectOptions) WithProject(fn ProjectFunc) func(cmd *cobra.Command, args []string) error { + return o.WithServices(func(ctx context.Context, project *types.Project, services []string) error { + return fn(ctx, project) + }) +} + +// WithServices creates a cobra run command from a ProjectFunc based on configured project options and selected services +func (o *projectOptions) WithServices(fn ProjectServicesFunc) func(cmd *cobra.Command, args []string) error { + return Adapt(func(ctx context.Context, args []string) error { + project, err := o.toProject(args) if err != nil { return err } - return fn(ctx, project) + + if o.EnvFile != "" { + var services types.Services + for _, s := range project.Services { + ef := o.EnvFile + if ef != "" { + if !filepath.IsAbs(ef) { + ef = filepath.Join(project.WorkingDir, o.EnvFile) + } + if s.Labels == nil { + s.Labels = make(map[string]string) + } + s.Labels[compose.EnvironmentFileLabel] = ef + services = append(services, s) + } + } + project.Services = services + } + + return fn(ctx, project, args) }) } @@ -217,7 +247,7 @@ func RootCommand(contextType string, backend compose.Service) *cobra.Command { } command.AddCommand( - upCommand(&opts, contextType, backend), + upCommand(&opts, backend), downCommand(&opts, backend), startCommand(&opts, backend), restartCommand(&opts, backend), diff --git a/cli/cmd/compose/create.go b/cli/cmd/compose/create.go index 75c55124b..2b3f5a774 100644 --- a/cli/cmd/compose/create.go +++ b/cli/cmd/compose/create.go @@ -19,22 +19,29 @@ package compose import ( "context" "fmt" + "time" + "github.com/compose-spec/compose-go/types" "github.com/spf13/cobra" "github.com/docker/compose-cli/api/compose" ) type createOptions struct { - *composeOptions + Build bool + noBuild bool + removeOrphans bool forceRecreate bool noRecreate bool + recreateDeps bool + noInherit bool + timeChanged bool + timeout int + quietPull bool } func createCommand(p *projectOptions, backend compose.Service) *cobra.Command { - opts := createOptions{ - composeOptions: &composeOptions{}, - } + opts := createOptions{} cmd := &cobra.Command{ Use: "create [SERVICE...]", Short: "Creates containers for a service.", @@ -47,17 +54,15 @@ func createCommand(p *projectOptions, backend compose.Service) *cobra.Command { } return nil }), - RunE: Adapt(func(ctx context.Context, args []string) error { - return runCreateStart(ctx, backend, upOptions{ - composeOptions: &composeOptions{ - projectOptions: p, - Build: opts.Build, - noBuild: opts.noBuild, - }, - noStart: true, - forceRecreate: opts.forceRecreate, - noRecreate: opts.noRecreate, - }, args) + RunE: p.WithProject(func(ctx context.Context, project *types.Project) error { + return backend.Create(ctx, project, compose.CreateOptions{ + RemoveOrphans: opts.removeOrphans, + Recreate: opts.recreateStrategy(), + RecreateDependencies: opts.dependenciesRecreateStrategy(), + Inherit: !opts.noInherit, + Timeout: opts.GetTimeout(), + QuietPull: false, + }) }), } flags := cmd.Flags() @@ -67,3 +72,46 @@ func createCommand(p *projectOptions, backend compose.Service) *cobra.Command { flags.BoolVar(&opts.noRecreate, "no-recreate", false, "If containers already exist, don't recreate them. Incompatible with --force-recreate.") return cmd } + +func (opts createOptions) recreateStrategy() string { + if opts.noRecreate { + return compose.RecreateNever + } + if opts.forceRecreate { + return compose.RecreateForce + } + return compose.RecreateDiverged +} + +func (opts createOptions) dependenciesRecreateStrategy() string { + if opts.noRecreate { + return compose.RecreateNever + } + if opts.recreateDeps { + return compose.RecreateForce + } + return compose.RecreateDiverged +} + +func (opts createOptions) GetTimeout() *time.Duration { + if opts.timeChanged { + t := time.Duration(opts.timeout) * time.Second + return &t + } + return nil +} + +func (opts createOptions) Apply(project *types.Project) { + if opts.Build { + for i, service := range project.Services { + service.PullPolicy = types.PullPolicyBuild + project.Services[i] = service + } + } + if opts.noBuild { + for i, service := range project.Services { + service.Build = nil + project.Services[i] = service + } + } +} diff --git a/cli/cmd/compose/kill.go b/cli/cmd/compose/kill.go index 9b6ab3f55..988c5da53 100644 --- a/cli/cmd/compose/kill.go +++ b/cli/cmd/compose/kill.go @@ -18,7 +18,6 @@ package compose import ( "context" - "os" "github.com/compose-spec/compose-go/types" "github.com/spf13/cobra" @@ -31,7 +30,7 @@ func killCommand(p *projectOptions, backend compose.Service) *cobra.Command { cmd := &cobra.Command{ Use: "kill [options] [SERVICE...]", Short: "Force stop service containers.", - RunE: p.WithServices(os.Args, func(ctx context.Context, project *types.Project) error { + RunE: p.WithProject(func(ctx context.Context, project *types.Project) error { return backend.Kill(ctx, project, opts) }), } diff --git a/cli/cmd/compose/run.go b/cli/cmd/compose/run.go index 7d619a4db..a1403d9ec 100644 --- a/cli/cmd/compose/run.go +++ b/cli/cmd/compose/run.go @@ -120,7 +120,11 @@ func runCommand(p *projectOptions, backend compose.Service) *cobra.Command { return nil }), RunE: Adapt(func(ctx context.Context, args []string) error { - return runRun(ctx, backend, opts) + project, err := p.toProject([]string{opts.Service}) + if err != nil { + return err + } + return runRun(ctx, backend, project, opts) }), } flags := cmd.Flags() @@ -143,13 +147,8 @@ func runCommand(p *projectOptions, backend compose.Service) *cobra.Command { return cmd } -func runRun(ctx context.Context, backend compose.Service, opts runOptions) error { - project, err := setup(*opts.composeOptions, []string{opts.Service}) - if err != nil { - return err - } - - err = opts.apply(project) +func runRun(ctx context.Context, backend compose.Service, project *types.Project, opts runOptions) error { + err := opts.apply(project) if err != nil { return err } diff --git a/cli/cmd/compose/up.go b/cli/cmd/compose/up.go index ec9e5dd3f..f4845b80d 100644 --- a/cli/cmd/compose/up.go +++ b/cli/cmd/compose/up.go @@ -20,40 +20,25 @@ import ( "context" "fmt" "os" - "os/signal" - "path/filepath" "strconv" "strings" - "syscall" - "time" "github.com/compose-spec/compose-go/types" - "github.com/docker/cli/cli" - "github.com/spf13/cobra" - "golang.org/x/sync/errgroup" - "github.com/docker/compose-cli/api/compose" - "github.com/docker/compose-cli/api/context/store" - "github.com/docker/compose-cli/api/progress" "github.com/docker/compose-cli/cli/formatter" "github.com/docker/compose-cli/utils" + "github.com/spf13/cobra" ) // composeOptions hold options common to `up` and `run` to run compose project type composeOptions struct { *projectOptions - Build bool - noBuild bool } type upOptions struct { *composeOptions Detach bool Environment []string - removeOrphans bool - forceRecreate bool - noRecreate bool - recreateDeps bool noStart bool noDeps bool cascadeStop bool @@ -61,39 +46,7 @@ type upOptions struct { scale []string noColor bool noPrefix bool - timeChanged bool - timeout int - noInherit bool attachDependencies bool - quietPull bool -} - -func (opts upOptions) recreateStrategy() string { - if opts.noRecreate { - return compose.RecreateNever - } - if opts.forceRecreate { - return compose.RecreateForce - } - return compose.RecreateDiverged -} - -func (opts upOptions) dependenciesRecreateStrategy() string { - if opts.noRecreate { - return compose.RecreateNever - } - if opts.recreateDeps { - return compose.RecreateForce - } - return compose.RecreateDiverged -} - -func (opts upOptions) GetTimeout() *time.Duration { - if opts.timeChanged { - t := time.Duration(opts.timeout) * time.Second - return &t - } - return nil } func (opts upOptions) apply(project *types.Project, services []string) error { @@ -136,192 +89,105 @@ func (opts upOptions) apply(project *types.Project, services []string) error { return nil } -func upCommand(p *projectOptions, contextType string, backend compose.Service) *cobra.Command { - opts := upOptions{ - composeOptions: &composeOptions{ - projectOptions: p, - }, - } +func upCommand(p *projectOptions, backend compose.Service) *cobra.Command { + up := upOptions{} + create := createOptions{} upCmd := &cobra.Command{ Use: "up [SERVICE...]", Short: "Create and start containers", PreRun: func(cmd *cobra.Command, args []string) { - opts.timeChanged = cmd.Flags().Changed("timeout") + create.timeChanged = cmd.Flags().Changed("timeout") }, PreRunE: Adapt(func(ctx context.Context, args []string) error { - if opts.exitCodeFrom != "" { - opts.cascadeStop = true + if up.exitCodeFrom != "" { + up.cascadeStop = true } - if opts.Build && opts.noBuild { + if create.Build && create.noBuild { return fmt.Errorf("--build and --no-build are incompatible") } - if opts.Detach && (opts.attachDependencies || opts.cascadeStop) { + if up.Detach && (up.attachDependencies || up.cascadeStop) { return fmt.Errorf("--detach cannot be combined with --abort-on-container-exit or --attach-dependencies") } - if opts.forceRecreate && opts.noRecreate { + if create.forceRecreate && create.noRecreate { return fmt.Errorf("--force-recreate and --no-recreate are incompatible") } - if opts.recreateDeps && opts.noRecreate { + if create.recreateDeps && create.noRecreate { return fmt.Errorf("--always-recreate-deps and --no-recreate are incompatible") } return nil }), - RunE: Adapt(func(ctx context.Context, args []string) error { - switch contextType { - case store.LocalContextType, store.DefaultContextType, store.EcsLocalSimulationContextType: - return runCreateStart(ctx, backend, opts, args) - default: - return runUp(ctx, backend, opts, args) - } + RunE: p.WithServices(func(ctx context.Context, project *types.Project, services []string) error { + return runUp(ctx, backend, create, up, project, services) }), } flags := upCmd.Flags() - flags.StringArrayVarP(&opts.Environment, "environment", "e", []string{}, "Environment variables") - flags.BoolVarP(&opts.Detach, "detach", "d", false, "Detached mode: Run containers in the background") - flags.BoolVar(&opts.Build, "build", false, "Build images before starting containers.") - flags.BoolVar(&opts.noBuild, "no-build", false, "Don't build an image, even if it's missing.") - flags.BoolVar(&opts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file.") - flags.StringArrayVar(&opts.scale, "scale", []string{}, "Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.") - flags.BoolVar(&opts.noColor, "no-color", false, "Produce monochrome output.") - flags.BoolVar(&opts.noPrefix, "no-log-prefix", false, "Don't print prefix in logs.") - - switch contextType { - case store.LocalContextType, store.DefaultContextType, store.EcsLocalSimulationContextType: - flags.BoolVar(&opts.forceRecreate, "force-recreate", false, "Recreate containers even if their configuration and image haven't changed.") - flags.BoolVar(&opts.noRecreate, "no-recreate", false, "If containers already exist, don't recreate them. Incompatible with --force-recreate.") - flags.BoolVar(&opts.noStart, "no-start", false, "Don't start the services after creating them.") - flags.BoolVar(&opts.cascadeStop, "abort-on-container-exit", false, "Stops all containers if any container was stopped. Incompatible with -d") - flags.StringVar(&opts.exitCodeFrom, "exit-code-from", "", "Return the exit code of the selected service container. Implies --abort-on-container-exit") - flags.IntVarP(&opts.timeout, "timeout", "t", 10, "Use this timeout in seconds for container shutdown when attached or when containers are already running.") - flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't start linked services.") - flags.BoolVar(&opts.recreateDeps, "always-recreate-deps", false, "Recreate dependent containers. Incompatible with --no-recreate.") - flags.BoolVarP(&opts.noInherit, "renew-anon-volumes", "V", false, "Recreate anonymous volumes instead of retrieving data from the previous containers.") - flags.BoolVar(&opts.attachDependencies, "attach-dependencies", false, "Attach to dependent containers.") - flags.BoolVar(&opts.quietPull, "quiet-pull", false, "Pull without printing progress information.") - } + flags.StringArrayVarP(&up.Environment, "environment", "e", []string{}, "Environment variables") + flags.BoolVarP(&up.Detach, "detach", "d", false, "Detached mode: Run containers in the background") + flags.BoolVar(&create.Build, "build", false, "Build images before starting containers.") + flags.BoolVar(&create.noBuild, "no-build", false, "Don't build an image, even if it's missing.") + flags.BoolVar(&create.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file.") + flags.StringArrayVar(&up.scale, "scale", []string{}, "Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.") + flags.BoolVar(&up.noColor, "no-color", false, "Produce monochrome output.") + flags.BoolVar(&up.noPrefix, "no-log-prefix", false, "Don't print prefix in logs.") + flags.BoolVar(&create.forceRecreate, "force-recreate", false, "Recreate containers even if their configuration and image haven't changed.") + flags.BoolVar(&create.noRecreate, "no-recreate", false, "If containers already exist, don't recreate them. Incompatible with --force-recreate.") + flags.BoolVar(&up.noStart, "no-start", false, "Don't start the services after creating them.") + flags.BoolVar(&up.cascadeStop, "abort-on-container-exit", false, "Stops all containers if any container was stopped. Incompatible with -d") + flags.StringVar(&up.exitCodeFrom, "exit-code-from", "", "Return the exit code of the selected service container. Implies --abort-on-container-exit") + flags.IntVarP(&create.timeout, "timeout", "t", 10, "Use this timeout in seconds for container shutdown when attached or when containers are already running.") + flags.BoolVar(&up.noDeps, "no-deps", false, "Don't start linked services.") + flags.BoolVar(&create.recreateDeps, "always-recreate-deps", false, "Recreate dependent containers. Incompatible with --no-recreate.") + flags.BoolVarP(&create.noInherit, "renew-anon-volumes", "V", false, "Recreate anonymous volumes instead of retrieving data from the previous containers.") + flags.BoolVar(&up.attachDependencies, "attach-dependencies", false, "Attach to dependent containers.") + flags.BoolVar(&create.quietPull, "quiet-pull", false, "Pull without printing progress information.") return upCmd } -func runUp(ctx context.Context, backend compose.Service, opts upOptions, services []string) error { - project, err := setup(*opts.composeOptions, services) - if err != nil { - return err - } - - err = opts.apply(project, services) - if err != nil { - return err - } - - return progress.Run(ctx, func(ctx context.Context) error { - return backend.Up(ctx, project, compose.UpOptions{ - Detach: opts.Detach, - QuietPull: opts.quietPull, - }) - }) -} - -func runCreateStart(ctx context.Context, backend compose.Service, opts upOptions, services []string) error { - project, err := setup(*opts.composeOptions, services) - if err != nil { - return err - } - - err = opts.apply(project, services) - if err != nil { - return err - } - +func runUp(ctx context.Context, backend compose.Service, createOptions createOptions, upOptions upOptions, project *types.Project, services []string) error { if len(project.Services) == 0 { return fmt.Errorf("no service selected") } - err = progress.Run(ctx, func(ctx context.Context) error { - err := backend.Create(ctx, project, compose.CreateOptions{ - Services: services, - RemoveOrphans: opts.removeOrphans, - Recreate: opts.recreateStrategy(), - RecreateDependencies: opts.dependenciesRecreateStrategy(), - Inherit: !opts.noInherit, - Timeout: opts.GetTimeout(), - QuietPull: opts.quietPull, - }) - if err != nil { - return err - } - if opts.Detach { - err = backend.Start(ctx, project, compose.StartOptions{ - Services: services, - }) - } - return err - }) + createOptions.Apply(project) + + err := upOptions.apply(project, services) if err != nil { return err } - if opts.noStart { - return nil + var consumer compose.LogConsumer + if !upOptions.Detach { + consumer = formatter.NewLogConsumer(ctx, os.Stdout, !upOptions.noColor, !upOptions.noPrefix) } - if opts.attachDependencies { - services = nil + attachTo := services + if upOptions.attachDependencies { + attachTo = project.ServiceNames() } - if opts.Detach { - return nil + create := compose.CreateOptions{ + RemoveOrphans: createOptions.removeOrphans, + Recreate: createOptions.recreateStrategy(), + RecreateDependencies: createOptions.dependenciesRecreateStrategy(), + Inherit: !createOptions.noInherit, + Timeout: createOptions.GetTimeout(), + QuietPull: createOptions.quietPull, } - consumer := formatter.NewLogConsumer(ctx, os.Stdout, !opts.noColor, !opts.noPrefix) - printer := compose.NewLogPrinter(consumer) - - signalChan := make(chan os.Signal, 1) - signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) - - stopFunc := func() error { - ctx := context.Background() - return progress.Run(ctx, func(ctx context.Context) error { - go func() { - <-signalChan - backend.Kill(ctx, project, compose.KillOptions{}) // nolint:errcheck - }() - - return backend.Stop(ctx, project, compose.StopOptions{}) - }) + if upOptions.noStart { + return backend.Create(ctx, project, create) } - go func() { - <-signalChan - printer.Cancel() - fmt.Println("Gracefully stopping... (press Ctrl+C again to force)") - stopFunc() // nolint:errcheck - }() - var exitCode int - eg, ctx := errgroup.WithContext(ctx) - eg.Go(func() error { - code, err := printer.Run(opts.cascadeStop, opts.exitCodeFrom, stopFunc) - exitCode = code - return err + return backend.Up(ctx, project, compose.UpOptions{ + Create: create, + Start: compose.StartOptions{ + Attach: consumer, + AttachTo: attachTo, + ExitCodeFrom: upOptions.exitCodeFrom, + CascadeStop: upOptions.cascadeStop, + }, }) - - err = backend.Start(ctx, project, compose.StartOptions{ - Attach: printer.HandleEvent, - Services: services, - }) - if err != nil { - return err - } - - err = eg.Wait() - if exitCode != 0 { - errMsg := "" - if err != nil { - errMsg = err.Error() - } - return cli.StatusError{StatusCode: exitCode, Status: errMsg} - } - return err } func setServiceScale(project *types.Project, name string, replicas int) error { @@ -338,43 +204,3 @@ func setServiceScale(project *types.Project, name string, replicas int) error { } return fmt.Errorf("unknown service %q", name) } - -func setup(opts composeOptions, services []string) (*types.Project, error) { - project, err := opts.toProject(services) - if err != nil { - return nil, err - } - - if opts.Build { - for i, service := range project.Services { - service.PullPolicy = types.PullPolicyBuild - project.Services[i] = service - } - } - if opts.noBuild { - for i, service := range project.Services { - service.Build = nil - project.Services[i] = service - } - } - - if opts.EnvFile != "" { - var services types.Services - for _, s := range project.Services { - ef := opts.EnvFile - if ef != "" { - if !filepath.IsAbs(ef) { - ef = filepath.Join(project.WorkingDir, opts.EnvFile) - } - if s.Labels == nil { - s.Labels = make(map[string]string) - } - s.Labels[compose.EnvironmentFileLabel] = ef - services = append(services, s) - } - } - project.Services = services - } - - return project, nil -} diff --git a/ecs/up.go b/ecs/up.go index 691a52c23..3829e680d 100644 --- a/ecs/up.go +++ b/ecs/up.go @@ -23,12 +23,12 @@ import ( "os/signal" "syscall" + "github.com/compose-spec/compose-go/types" "github.com/sirupsen/logrus" "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/errdefs" - - "github.com/compose-spec/compose-go/types" + "github.com/docker/compose-cli/api/progress" ) func (b *ecsAPIService) Build(ctx context.Context, project *types.Project, options compose.BuildOptions) error { @@ -80,6 +80,12 @@ func (b *ecsAPIService) Copy(ctx context.Context, project *types.Project, option } func (b *ecsAPIService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error { + return progress.Run(ctx, func(ctx context.Context) error { + return b.up(ctx, project, options) + }) +} + +func (b *ecsAPIService) up(ctx context.Context, project *types.Project, options compose.UpOptions) error { logrus.Debugf("deploying on AWS with region=%q", b.Region) err := b.aws.CheckRequirements(ctx, b.Region) if err != nil { @@ -124,7 +130,7 @@ func (b *ecsAPIService) Up(ctx context.Context, project *types.Project, options return err } } - if options.Detach { + if options.Start.Attach == nil { return nil } signalChan := make(chan os.Signal, 1) diff --git a/kube/compose.go b/kube/compose.go index 7b4e91330..b89ac0560 100644 --- a/kube/compose.go +++ b/kube/compose.go @@ -72,6 +72,12 @@ func NewComposeService() (compose.Service, error) { // Up executes the equivalent to a `compose up` func (s *composeService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error { + return progress.Run(ctx, func(ctx context.Context) error { + return s.up(ctx, project) + }) +} + +func (s *composeService) up(ctx context.Context, project *types.Project) error { w := progress.ContextWriter(ctx) eventName := "Convert Compose file to Helm charts" diff --git a/local/compose/compose.go b/local/compose/compose.go index 891b1d95c..f2567801f 100644 --- a/local/compose/compose.go +++ b/local/compose/compose.go @@ -23,7 +23,6 @@ import ( "strings" "github.com/docker/compose-cli/api/compose" - "github.com/docker/compose-cli/api/errdefs" "github.com/compose-spec/compose-go/types" "github.com/docker/cli/cli/config/configfile" @@ -45,10 +44,6 @@ type composeService struct { configFile *configfile.ConfigFile } -func (s *composeService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error { - return errdefs.ErrNotImplemented -} - func getCanonicalContainerName(c moby.Container) string { // Names return container canonical name /foo + link aliases /linked_by/foo for _, name := range c.Names { diff --git a/local/compose/create.go b/local/compose/create.go index ccf92818e..d1c79eef2 100644 --- a/local/compose/create.go +++ b/local/compose/create.go @@ -43,9 +43,15 @@ import ( "github.com/docker/compose-cli/utils" ) -func (s *composeService) Create(ctx context.Context, project *types.Project, opts compose.CreateOptions) error { - if len(opts.Services) == 0 { - opts.Services = project.ServiceNames() +func (s *composeService) Create(ctx context.Context, project *types.Project, options compose.CreateOptions) error { + return progress.Run(ctx, func(ctx context.Context) error { + return s.create(ctx, project, options) + }) +} + +func (s *composeService) create(ctx context.Context, project *types.Project, options compose.CreateOptions) error { + if len(options.Services) == 0 { + options.Services = project.ServiceNames() } var observedState Containers @@ -56,7 +62,7 @@ func (s *composeService) Create(ctx context.Context, project *types.Project, opt containerState := NewContainersState(observedState) ctx = context.WithValue(ctx, ContainersKey{}, containerState) - err = s.ensureImagesExists(ctx, project, observedState, opts.QuietPull) + err = s.ensureImagesExists(ctx, project, observedState, options.QuietPull) if err != nil { return err } @@ -83,7 +89,7 @@ func (s *composeService) Create(ctx context.Context, project *types.Project, opt } orphans := observedState.filter(isNotService(allServiceNames...)) if len(orphans) > 0 { - if opts.RemoveOrphans { + if options.RemoveOrphans { w := progress.ContextWriter(ctx) err := s.removeContainers(ctx, w, orphans, nil) if err != nil { @@ -100,10 +106,10 @@ func (s *composeService) Create(ctx context.Context, project *types.Project, opt prepareServicesDependsOn(project) return InDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error { - if utils.StringContains(opts.Services, service.Name) { - return s.ensureService(c, project, service, opts.Recreate, opts.Inherit, opts.Timeout) + if utils.StringContains(options.Services, service.Name) { + return s.ensureService(c, project, service, options.Recreate, options.Inherit, options.Timeout) } - return s.ensureService(c, project, service, opts.RecreateDependencies, opts.Inherit, opts.Timeout) + return s.ensureService(c, project, service, options.RecreateDependencies, options.Inherit, options.Timeout) }) } diff --git a/local/compose/kill.go b/local/compose/kill.go index 0ec788e89..4b951a5a4 100644 --- a/local/compose/kill.go +++ b/local/compose/kill.go @@ -28,6 +28,12 @@ import ( ) func (s *composeService) Kill(ctx context.Context, project *types.Project, options compose.KillOptions) error { + return progress.Run(ctx, func(ctx context.Context) error { + return s.kill(ctx, project, options) + }) +} + +func (s *composeService) kill(ctx context.Context, project *types.Project, options compose.KillOptions) error { w := progress.ContextWriter(ctx) var containers Containers diff --git a/local/compose/kill_test.go b/local/compose/kill_test.go index 58b2ec97d..6216a67d8 100644 --- a/local/compose/kill_test.go +++ b/local/compose/kill_test.go @@ -50,7 +50,7 @@ func TestKillAll(t *testing.T) { api.EXPECT().ContainerKill(anyCancellableContext(), "456", "").Return(nil) api.EXPECT().ContainerKill(anyCancellableContext(), "789", "").Return(nil) - err := tested.Kill(ctx, &project, compose.KillOptions{}) + err := tested.kill(ctx, &project, compose.KillOptions{}) assert.NilError(t, err) } @@ -66,7 +66,7 @@ func TestKillSignal(t *testing.T) { api.EXPECT().ContainerList(ctx, projectFilterListOpt()).Return([]apitypes.Container{testContainer("service1", "123")}, nil) api.EXPECT().ContainerKill(anyCancellableContext(), "123", "SIGTERM").Return(nil) - err := tested.Kill(ctx, &project, compose.KillOptions{Signal: "SIGTERM"}) + err := tested.kill(ctx, &project, compose.KillOptions{Signal: "SIGTERM"}) assert.NilError(t, err) } diff --git a/local/compose/start.go b/local/compose/start.go index 724aeeef2..a5c513072 100644 --- a/local/compose/start.go +++ b/local/compose/start.go @@ -19,45 +19,40 @@ package compose import ( "context" - "github.com/docker/compose-cli/api/compose" - "github.com/docker/compose-cli/api/progress" - "github.com/docker/compose-cli/utils" - "github.com/compose-spec/compose-go/types" moby "github.com/docker/docker/api/types" "github.com/pkg/errors" "golang.org/x/sync/errgroup" + + "github.com/docker/compose-cli/api/compose" + "github.com/docker/compose-cli/api/progress" ) func (s *composeService) Start(ctx context.Context, project *types.Project, options compose.StartOptions) error { return progress.Run(ctx, func(ctx context.Context) error { - return s.start(ctx, project, options) + return s.start(ctx, project, options, nil) }) } -func (s *composeService) start(ctx context.Context, project *types.Project, options compose.StartOptions) error { - listener := options.Attach - if len(options.Services) == 0 { - options.Services = project.ServiceNames() +func (s *composeService) start(ctx context.Context, project *types.Project, options compose.StartOptions, listener func(event compose.ContainerEvent)) error { + if len(options.AttachTo) == 0 { + options.AttachTo = project.ServiceNames() } eg, ctx := errgroup.WithContext(ctx) if listener != nil { - attached, err := s.attach(ctx, project, listener, options.Services) + attached, err := s.attach(ctx, project, listener, options.AttachTo) if err != nil { return err } eg.Go(func() error { - return s.watchContainers(project, options.Services, listener, attached) + return s.watchContainers(project, options.AttachTo, listener, attached) }) } err := InDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error { - if utils.StringContains(options.Services, service.Name) { - return s.startService(ctx, project, service) - } - return nil + return s.startService(ctx, project, service) }) if err != nil { return err diff --git a/local/compose/up.go b/local/compose/up.go new file mode 100644 index 000000000..72a30165b --- /dev/null +++ b/local/compose/up.go @@ -0,0 +1,95 @@ +/* + 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 ( + "context" + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/docker/compose-cli/api/compose" + "github.com/docker/compose-cli/api/progress" + + "github.com/compose-spec/compose-go/types" + "github.com/docker/cli/cli" + "golang.org/x/sync/errgroup" +) + +func (s *composeService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error { + err := progress.Run(ctx, func(ctx context.Context) error { + err := s.create(ctx, project, options.Create) + if err != nil { + return err + } + return s.start(ctx, project, options.Start, nil) + }) + if err != nil { + return err + } + + if options.Start.Attach == nil { + return err + } + + printer := compose.NewLogPrinter(options.Start.Attach) + + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) + + stopFunc := func() error { + ctx := context.Background() + return progress.Run(ctx, func(ctx context.Context) error { + go func() { + <-signalChan + s.Kill(ctx, project, compose.KillOptions{}) // nolint:errcheck + }() + + return s.Stop(ctx, project, compose.StopOptions{}) + }) + } + go func() { + <-signalChan + printer.Cancel() + fmt.Println("Gracefully stopping... (press Ctrl+C again to force)") + stopFunc() // nolint:errcheck + }() + + var exitCode int + eg, ctx := errgroup.WithContext(ctx) + eg.Go(func() error { + code, err := printer.Run(options.Start.CascadeStop, options.Start.ExitCodeFrom, stopFunc) + exitCode = code + return err + }) + + err = s.start(ctx, project, options.Start, printer.HandleEvent) + if err != nil { + return err + } + + err = eg.Wait() + if exitCode != 0 { + errMsg := "" + if err != nil { + errMsg = err.Error() + } + return cli.StatusError{StatusCode: exitCode, Status: errMsg} + } + return err +}