From 0bdad7e5511c2d4413923e565e3eb6bcc9b805d8 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Wed, 14 Apr 2021 11:47:02 +0200 Subject: [PATCH 1/9] pass compose.Service to cobra commands, dependency-injection style Signed-off-by: Nicolas De Loof --- cli/cmd/compose/build.go | 14 ++++------- cli/cmd/compose/compose.go | 49 +++++++++++++++++++------------------- cli/cmd/compose/convert.go | 14 ++++------- cli/cmd/compose/create.go | 6 +++-- cli/cmd/compose/down.go | 16 ++++--------- cli/cmd/compose/events.go | 14 ++++------- cli/cmd/compose/exec.go | 14 ++++------- cli/cmd/compose/images.go | 14 ++++------- cli/cmd/compose/kill.go | 13 ++++------ cli/cmd/compose/list.go | 13 ++++------ cli/cmd/compose/logs.go | 14 ++++------- cli/cmd/compose/pause.go | 27 +++++++-------------- cli/cmd/compose/port.go | 14 ++++------- cli/cmd/compose/ps.go | 14 ++++------- cli/cmd/compose/pull.go | 16 ++++--------- cli/cmd/compose/push.go | 14 ++++------- cli/cmd/compose/remove.go | 20 ++++++---------- cli/cmd/compose/restart.go | 14 ++++------- cli/cmd/compose/run.go | 19 +++++++-------- cli/cmd/compose/start.go | 14 ++++------- cli/cmd/compose/stop.go | 16 ++++--------- cli/cmd/compose/top.go | 14 ++++------- cli/cmd/compose/up.go | 38 +++++++++++++---------------- cli/main.go | 2 +- docs/yaml/main/generate.go | 2 +- 25 files changed, 143 insertions(+), 262 deletions(-) diff --git a/cli/cmd/compose/build.go b/cli/cmd/compose/build.go index 4f5e83e70..ecacacbd8 100644 --- a/cli/cmd/compose/build.go +++ b/cli/cmd/compose/build.go @@ -24,7 +24,6 @@ import ( "github.com/compose-spec/compose-go/types" "github.com/spf13/cobra" - "github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/progress" ) @@ -40,7 +39,7 @@ type buildOptions struct { memory string } -func buildCommand(p *projectOptions) *cobra.Command { +func buildCommand(p *projectOptions, backend compose.Service) *cobra.Command { opts := buildOptions{ projectOptions: p, } @@ -58,7 +57,7 @@ func buildCommand(p *projectOptions) *cobra.Command { } os.Stdout = devnull } - return runBuild(cmd.Context(), opts, args) + return runBuild(cmd.Context(), backend, opts, args) }, } cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false, "Don't print anything to STDOUT") @@ -80,19 +79,14 @@ func buildCommand(p *projectOptions) *cobra.Command { return cmd } -func runBuild(ctx context.Context, opts buildOptions, services []string) error { - c, err := client.New(ctx) - if err != nil { - return err - } - +func runBuild(ctx context.Context, backend compose.Service, opts buildOptions, services []string) error { project, err := opts.toProject(services) if err != nil { return err } _, err = progress.Run(ctx, func(ctx context.Context) (string, error) { - return "", c.ComposeService().Build(ctx, project, compose.BuildOptions{ + return "", backend.Build(ctx, project, compose.BuildOptions{ Pull: opts.pull, Progress: opts.progress, Args: types.NewMapping(opts.args), diff --git a/cli/cmd/compose/compose.go b/cli/cmd/compose/compose.go index 4b7754c97..49bf3e9e1 100644 --- a/cli/cmd/compose/compose.go +++ b/cli/cmd/compose/compose.go @@ -28,6 +28,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" + "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/context/store" "github.com/docker/compose-cli/cli/formatter" "github.com/docker/compose-cli/cli/metrics" @@ -105,7 +106,7 @@ func (o *projectOptions) toProjectOptions(po ...cli.ProjectOptionsFn) (*cli.Proj } // Command returns the compose command with its child commands -func Command(contextType string) *cobra.Command { +func Command(contextType string, backend compose.Service) *cobra.Command { opts := projectOptions{} var ansi string var noAnsi bool @@ -146,34 +147,34 @@ func Command(contextType string) *cobra.Command { } command.AddCommand( - upCommand(&opts, contextType), - downCommand(&opts, contextType), - startCommand(&opts), - restartCommand(&opts), - stopCommand(&opts), - psCommand(&opts), - listCommand(contextType), - logsCommand(&opts, contextType), - convertCommand(&opts), - killCommand(&opts), - runCommand(&opts), - removeCommand(&opts), - execCommand(&opts), - pauseCommand(&opts), - unpauseCommand(&opts), - topCommand(&opts), - eventsCommand(&opts), - portCommand(&opts), - imagesCommand(&opts), + upCommand(&opts, contextType, backend), + downCommand(&opts, contextType, backend), + startCommand(&opts, backend), + restartCommand(&opts, backend), + stopCommand(&opts, backend), + psCommand(&opts, backend), + listCommand(contextType, backend), + logsCommand(&opts, contextType, backend), + convertCommand(&opts, backend), + killCommand(&opts, backend), + runCommand(&opts, backend), + removeCommand(&opts, backend), + execCommand(&opts, backend), + pauseCommand(&opts, backend), + unpauseCommand(&opts, backend), + topCommand(&opts, backend), + eventsCommand(&opts, backend), + portCommand(&opts, backend), + imagesCommand(&opts, backend), versionCommand(), ) if contextType == store.LocalContextType || contextType == store.DefaultContextType { command.AddCommand( - buildCommand(&opts), - pushCommand(&opts), - pullCommand(&opts), - createCommand(&opts), + buildCommand(&opts, backend), + pushCommand(&opts, backend), + pullCommand(&opts, backend), + createCommand(&opts, backend), ) } command.Flags().SetInterspersed(false) diff --git a/cli/cmd/compose/convert.go b/cli/cmd/compose/convert.go index 42453bbe2..a01d10be7 100644 --- a/cli/cmd/compose/convert.go +++ b/cli/cmd/compose/convert.go @@ -31,7 +31,6 @@ import ( "github.com/opencontainers/go-digest" "github.com/spf13/cobra" - "github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/config" "github.com/docker/compose-cli/utils" @@ -52,7 +51,7 @@ type convertOptions struct { var addFlagsFuncs []func(cmd *cobra.Command, opts *convertOptions) -func convertCommand(p *projectOptions) *cobra.Command { +func convertCommand(p *projectOptions, backend compose.Service) *cobra.Command { opts := convertOptions{ projectOptions: p, } @@ -81,7 +80,7 @@ func convertCommand(p *projectOptions) *cobra.Command { return runProfiles(opts, args) } - return runConvert(cmd.Context(), opts, args) + return runConvert(cmd.Context(), backend, opts, args) }, } flags := cmd.Flags() @@ -102,13 +101,8 @@ func convertCommand(p *projectOptions) *cobra.Command { return cmd } -func runConvert(ctx context.Context, opts convertOptions, services []string) error { +func runConvert(ctx context.Context, backend compose.Service, opts convertOptions, services []string) error { var json []byte - c, err := client.New(ctx) - if err != nil { - return err - } - project, err := opts.toProject(services, cli.WithInterpolation(!opts.noInterpolate)) if err != nil { return err @@ -130,7 +124,7 @@ func runConvert(ctx context.Context, opts convertOptions, services []string) err } } - json, err = c.ComposeService().Convert(ctx, project, compose.ConvertOptions{ + json, err = backend.Convert(ctx, project, compose.ConvertOptions{ Format: opts.Format, Output: opts.Output, }) diff --git a/cli/cmd/compose/create.go b/cli/cmd/compose/create.go index 87ef50894..ef6080852 100644 --- a/cli/cmd/compose/create.go +++ b/cli/cmd/compose/create.go @@ -20,6 +20,8 @@ import ( "fmt" "github.com/spf13/cobra" + + "github.com/docker/compose-cli/api/compose" ) type createOptions struct { @@ -28,7 +30,7 @@ type createOptions struct { noRecreate bool } -func createCommand(p *projectOptions) *cobra.Command { +func createCommand(p *projectOptions, backend compose.Service) *cobra.Command { opts := createOptions{ composeOptions: &composeOptions{}, } @@ -42,7 +44,7 @@ func createCommand(p *projectOptions) *cobra.Command { if opts.forceRecreate && opts.noRecreate { return fmt.Errorf("--force-recreate and --no-recreate are incompatible") } - return runCreateStart(cmd.Context(), upOptions{ + return runCreateStart(cmd.Context(), backend, upOptions{ composeOptions: &composeOptions{ projectOptions: p, Build: opts.Build, diff --git a/cli/cmd/compose/down.go b/cli/cmd/compose/down.go index edc669682..07e377443 100644 --- a/cli/cmd/compose/down.go +++ b/cli/cmd/compose/down.go @@ -24,7 +24,6 @@ import ( "github.com/compose-spec/compose-go/types" "github.com/spf13/cobra" - "github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/context/store" "github.com/docker/compose-cli/api/progress" @@ -39,7 +38,7 @@ type downOptions struct { images string } -func downCommand(p *projectOptions, contextType string) *cobra.Command { +func downCommand(p *projectOptions, contextType string, backend compose.Service) *cobra.Command { opts := downOptions{ projectOptions: p, } @@ -53,7 +52,7 @@ func downCommand(p *projectOptions, contextType string) *cobra.Command { return fmt.Errorf("invalid value for --rmi: %q", opts.images) } } - return runDown(cmd.Context(), opts) + return runDown(cmd.Context(), backend, opts) }, } flags := downCmd.Flags() @@ -68,13 +67,8 @@ func downCommand(p *projectOptions, contextType string) *cobra.Command { return downCmd } -func runDown(ctx context.Context, opts downOptions) error { - c, err := client.New(ctx) - if err != nil { - return err - } - - _, err = progress.Run(ctx, func(ctx context.Context) (string, error) { +func runDown(ctx context.Context, backend compose.Service, opts downOptions) error { + _, err := progress.Run(ctx, func(ctx context.Context) (string, error) { name := opts.ProjectName var project *types.Project if opts.ProjectName == "" { @@ -91,7 +85,7 @@ func runDown(ctx context.Context, opts downOptions) error { timeoutValue := time.Duration(opts.timeout) * time.Second timeout = &timeoutValue } - return name, c.ComposeService().Down(ctx, name, compose.DownOptions{ + return name, backend.Down(ctx, name, compose.DownOptions{ RemoveOrphans: opts.removeOrphans, Project: project, Timeout: timeout, diff --git a/cli/cmd/compose/events.go b/cli/cmd/compose/events.go index 0628f9708..da496d0dd 100644 --- a/cli/cmd/compose/events.go +++ b/cli/cmd/compose/events.go @@ -21,7 +21,6 @@ import ( "encoding/json" "fmt" - "github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/compose" "github.com/spf13/cobra" @@ -32,7 +31,7 @@ type eventsOpts struct { json bool } -func eventsCommand(p *projectOptions) *cobra.Command { +func eventsCommand(p *projectOptions, backend compose.Service) *cobra.Command { opts := eventsOpts{ composeOptions: &composeOptions{ projectOptions: p, @@ -42,7 +41,7 @@ func eventsCommand(p *projectOptions) *cobra.Command { Use: "events [options] [--] [SERVICE...]", Short: "Receive real time events from containers.", RunE: func(cmd *cobra.Command, args []string) error { - return runEvents(cmd.Context(), opts, args) + return runEvents(cmd.Context(), backend, opts, args) }, } @@ -50,18 +49,13 @@ func eventsCommand(p *projectOptions) *cobra.Command { return cmd } -func runEvents(ctx context.Context, opts eventsOpts, services []string) error { - c, err := client.New(ctx) - if err != nil { - return err - } - +func runEvents(ctx context.Context, backend compose.Service, opts eventsOpts, services []string) error { project, err := opts.toProjectName() if err != nil { return err } - return c.ComposeService().Events(ctx, project, compose.EventsOptions{ + return backend.Events(ctx, project, compose.EventsOptions{ Services: services, Consumer: func(event compose.Event) error { if opts.json { diff --git a/cli/cmd/compose/exec.go b/cli/cmd/compose/exec.go index 1cd0170d3..4592448e9 100644 --- a/cli/cmd/compose/exec.go +++ b/cli/cmd/compose/exec.go @@ -24,7 +24,6 @@ import ( "github.com/containerd/console" "github.com/spf13/cobra" - "github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/compose" ) @@ -43,7 +42,7 @@ type execOpts struct { privileged bool } -func execCommand(p *projectOptions) *cobra.Command { +func execCommand(p *projectOptions, backend compose.Service) *cobra.Command { opts := execOpts{ composeOptions: &composeOptions{ projectOptions: p, @@ -58,7 +57,7 @@ func execCommand(p *projectOptions) *cobra.Command { opts.command = args[1:] } opts.service = args[0] - return runExec(cmd.Context(), opts) + return runExec(cmd.Context(), backend, opts) }, } @@ -74,12 +73,7 @@ func execCommand(p *projectOptions) *cobra.Command { return runCmd } -func runExec(ctx context.Context, opts execOpts) error { - c, err := client.New(ctx) - if err != nil { - return err - } - +func runExec(ctx context.Context, backend compose.Service, opts execOpts) error { project, err := opts.toProject(nil) if err != nil { return err @@ -114,5 +108,5 @@ func runExec(ctx context.Context, opts execOpts) error { execOpts.Writer = con execOpts.Reader = con } - return c.ComposeService().Exec(ctx, project, execOpts) + return backend.Exec(ctx, project, execOpts) } diff --git a/cli/cmd/compose/images.go b/cli/cmd/compose/images.go index 29f8ff0f0..072a6a083 100644 --- a/cli/cmd/compose/images.go +++ b/cli/cmd/compose/images.go @@ -26,7 +26,6 @@ import ( "github.com/spf13/cobra" - "github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/cli/formatter" "github.com/docker/compose-cli/utils" @@ -40,7 +39,7 @@ type imageOptions struct { Quiet bool } -func imagesCommand(p *projectOptions) *cobra.Command { +func imagesCommand(p *projectOptions, backend compose.Service) *cobra.Command { opts := imageOptions{ projectOptions: p, } @@ -48,25 +47,20 @@ func imagesCommand(p *projectOptions) *cobra.Command { Use: "images [SERVICE...]", Short: "List images used by the created containers", RunE: func(cmd *cobra.Command, args []string) error { - return runImages(cmd.Context(), opts, args) + return runImages(cmd.Context(), backend, opts, args) }, } imgCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs") return imgCmd } -func runImages(ctx context.Context, opts imageOptions, services []string) error { - c, err := client.New(ctx) - if err != nil { - return err - } - +func runImages(ctx context.Context, backend compose.Service, opts imageOptions, services []string) error { projectName, err := opts.toProjectName() if err != nil { return err } - images, err := c.ComposeService().Images(ctx, projectName, compose.ImagesOptions{ + images, err := backend.Images(ctx, projectName, compose.ImagesOptions{ Services: services, }) if err != nil { diff --git a/cli/cmd/compose/kill.go b/cli/cmd/compose/kill.go index 1ffb99dd6..97cb9c9d7 100644 --- a/cli/cmd/compose/kill.go +++ b/cli/cmd/compose/kill.go @@ -21,7 +21,6 @@ import ( "github.com/spf13/cobra" - "github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/compose" ) @@ -30,7 +29,7 @@ type killOptions struct { Signal string } -func killCommand(p *projectOptions) *cobra.Command { +func killCommand(p *projectOptions, backend compose.Service) *cobra.Command { opts := killOptions{ projectOptions: p, } @@ -38,7 +37,7 @@ func killCommand(p *projectOptions) *cobra.Command { Use: "kill [options] [SERVICE...]", Short: "Force stop service containers.", RunE: func(cmd *cobra.Command, args []string) error { - return runKill(cmd.Context(), opts, args) + return runKill(cmd.Context(), backend, opts, args) }, } @@ -48,16 +47,12 @@ func killCommand(p *projectOptions) *cobra.Command { return cmd } -func runKill(ctx context.Context, opts killOptions, services []string) error { - c, err := client.New(ctx) - if err != nil { - return err - } +func runKill(ctx context.Context, backend compose.Service, opts killOptions, services []string) error { project, err := opts.toProject(services) if err != nil { return err } - return c.ComposeService().Kill(ctx, project, compose.KillOptions{ + return backend.Kill(ctx, project, compose.KillOptions{ Signal: opts.Signal, }) } diff --git a/cli/cmd/compose/list.go b/cli/cmd/compose/list.go index 8ae601d87..b75887d00 100644 --- a/cli/cmd/compose/list.go +++ b/cli/cmd/compose/list.go @@ -26,7 +26,6 @@ import ( "github.com/docker/cli/opts" "github.com/spf13/cobra" - "github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/context/store" "github.com/docker/compose-cli/cli/formatter" @@ -39,13 +38,13 @@ type lsOptions struct { Filter opts.FilterOpt } -func listCommand(contextType string) *cobra.Command { +func listCommand(contextType string, backend compose.Service) *cobra.Command { opts := lsOptions{Filter: opts.NewFilterOpt()} lsCmd := &cobra.Command{ Use: "ls", Short: "List running compose projects", RunE: func(cmd *cobra.Command, args []string) error { - return runList(cmd.Context(), opts) + return runList(cmd.Context(), backend, opts) }, } lsCmd.Flags().StringVar(&opts.Format, "format", "pretty", "Format the output. Values: [pretty | json].") @@ -62,18 +61,14 @@ var acceptedListFilters = map[string]bool{ "name": true, } -func runList(ctx context.Context, opts lsOptions) error { +func runList(ctx context.Context, backend compose.Service, opts lsOptions) error { filters := opts.Filter.Value() err := filters.Validate(acceptedListFilters) if err != nil { return err } - c, err := client.New(ctx) - if err != nil { - return err - } - stackList, err := c.ComposeService().List(ctx, compose.ListOptions{All: opts.All}) + stackList, err := backend.List(ctx, compose.ListOptions{All: opts.All}) if err != nil { return err } diff --git a/cli/cmd/compose/logs.go b/cli/cmd/compose/logs.go index 75ec48039..e2249721f 100644 --- a/cli/cmd/compose/logs.go +++ b/cli/cmd/compose/logs.go @@ -22,7 +22,6 @@ import ( "github.com/spf13/cobra" - "github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/context/store" "github.com/docker/compose-cli/cli/formatter" @@ -38,7 +37,7 @@ type logsOptions struct { timestamps bool } -func logsCommand(p *projectOptions, contextType string) *cobra.Command { +func logsCommand(p *projectOptions, contextType string, backend compose.Service) *cobra.Command { opts := logsOptions{ projectOptions: p, } @@ -46,7 +45,7 @@ func logsCommand(p *projectOptions, contextType string) *cobra.Command { Use: "logs [service...]", Short: "View output from containers", RunE: func(cmd *cobra.Command, args []string) error { - return runLogs(cmd.Context(), opts, args) + return runLogs(cmd.Context(), backend, opts, args) }, } flags := logsCmd.Flags() @@ -61,18 +60,13 @@ func logsCommand(p *projectOptions, contextType string) *cobra.Command { return logsCmd } -func runLogs(ctx context.Context, opts logsOptions, services []string) error { - c, err := client.New(ctx) - if err != nil { - return err - } - +func runLogs(ctx context.Context, backend compose.Service, opts logsOptions, services []string) error { projectName, err := opts.toProjectName() if err != nil { return err } consumer := formatter.NewLogConsumer(ctx, os.Stdout, !opts.noColor, !opts.noPrefix) - return c.ComposeService().Logs(ctx, projectName, consumer, compose.LogOptions{ + return backend.Logs(ctx, projectName, consumer, compose.LogOptions{ Services: services, Follow: opts.follow, Tail: opts.tail, diff --git a/cli/cmd/compose/pause.go b/cli/cmd/compose/pause.go index b32c7571e..0265b4ea3 100644 --- a/cli/cmd/compose/pause.go +++ b/cli/cmd/compose/pause.go @@ -21,7 +21,6 @@ import ( "github.com/spf13/cobra" - "github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/progress" ) @@ -30,7 +29,7 @@ type pauseOptions struct { *projectOptions } -func pauseCommand(p *projectOptions) *cobra.Command { +func pauseCommand(p *projectOptions, backend compose.Service) *cobra.Command { opts := pauseOptions{ projectOptions: p, } @@ -38,25 +37,20 @@ func pauseCommand(p *projectOptions) *cobra.Command { Use: "pause [SERVICE...]", Short: "pause services", RunE: func(cmd *cobra.Command, args []string) error { - return runPause(cmd.Context(), opts, args) + return runPause(cmd.Context(), backend, opts, args) }, } return cmd } -func runPause(ctx context.Context, opts pauseOptions, services []string) error { - c, err := client.New(ctx) - if err != nil { - return err - } - +func runPause(ctx context.Context, backend compose.Service, opts pauseOptions, services []string) error { project, err := opts.toProjectName() if err != nil { return err } _, err = progress.Run(ctx, func(ctx context.Context) (string, error) { - return "", c.ComposeService().Pause(ctx, project, compose.PauseOptions{ + return "", backend.Pause(ctx, project, compose.PauseOptions{ Services: services, }) }) @@ -67,7 +61,7 @@ type unpauseOptions struct { *projectOptions } -func unpauseCommand(p *projectOptions) *cobra.Command { +func unpauseCommand(p *projectOptions, backend compose.Service) *cobra.Command { opts := unpauseOptions{ projectOptions: p, } @@ -75,25 +69,20 @@ func unpauseCommand(p *projectOptions) *cobra.Command { Use: "unpause [SERVICE...]", Short: "unpause services", RunE: func(cmd *cobra.Command, args []string) error { - return runUnPause(cmd.Context(), opts, args) + return runUnPause(cmd.Context(), backend, opts, args) }, } return cmd } -func runUnPause(ctx context.Context, opts unpauseOptions, services []string) error { - c, err := client.New(ctx) - if err != nil { - return err - } - +func runUnPause(ctx context.Context, backend compose.Service, opts unpauseOptions, services []string) error { project, err := opts.toProjectName() if err != nil { return err } _, err = progress.Run(ctx, func(ctx context.Context) (string, error) { - return "", c.ComposeService().UnPause(ctx, project, compose.PauseOptions{ + return "", backend.UnPause(ctx, project, compose.PauseOptions{ Services: services, }) }) diff --git a/cli/cmd/compose/port.go b/cli/cmd/compose/port.go index c556893ca..ecb8c8e58 100644 --- a/cli/cmd/compose/port.go +++ b/cli/cmd/compose/port.go @@ -23,7 +23,6 @@ import ( "github.com/spf13/cobra" - "github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/compose" ) @@ -33,7 +32,7 @@ type portOptions struct { index int } -func portCommand(p *projectOptions) *cobra.Command { +func portCommand(p *projectOptions, backend compose.Service) *cobra.Command { opts := portOptions{ projectOptions: p, } @@ -46,7 +45,7 @@ func portCommand(p *projectOptions) *cobra.Command { if err != nil { return err } - return runPort(cmd.Context(), opts, args[0], port) + return runPort(cmd.Context(), backend, opts, args[0], port) }, } cmd.Flags().StringVar(&opts.protocol, "protocol", "tcp", "tcp or udp") @@ -54,17 +53,12 @@ func portCommand(p *projectOptions) *cobra.Command { return cmd } -func runPort(ctx context.Context, opts portOptions, service string, port int) error { - c, err := client.New(ctx) - if err != nil { - return err - } - +func runPort(ctx context.Context, backend compose.Service, opts portOptions, service string, port int) error { projectName, err := opts.toProjectName() if err != nil { return err } - ip, port, err := c.ComposeService().Port(ctx, projectName, service, port, compose.PortOptions{ + ip, port, err := backend.Port(ctx, projectName, service, port, compose.PortOptions{ Protocol: opts.protocol, Index: opts.index, }) diff --git a/cli/cmd/compose/ps.go b/cli/cmd/compose/ps.go index 5e91b5be3..985b16c3e 100644 --- a/cli/cmd/compose/ps.go +++ b/cli/cmd/compose/ps.go @@ -26,7 +26,6 @@ import ( "github.com/spf13/cobra" - "github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/cli/formatter" "github.com/docker/compose-cli/utils" @@ -40,7 +39,7 @@ type psOptions struct { Services bool } -func psCommand(p *projectOptions) *cobra.Command { +func psCommand(p *projectOptions, backend compose.Service) *cobra.Command { opts := psOptions{ projectOptions: p, } @@ -48,7 +47,7 @@ func psCommand(p *projectOptions) *cobra.Command { Use: "ps", Short: "List containers", RunE: func(cmd *cobra.Command, args []string) error { - return runPs(cmd.Context(), opts) + return runPs(cmd.Context(), backend, opts) }, } psCmd.Flags().StringVar(&opts.Format, "format", "pretty", "Format the output. Values: [pretty | json].") @@ -58,17 +57,12 @@ func psCommand(p *projectOptions) *cobra.Command { return psCmd } -func runPs(ctx context.Context, opts psOptions) error { - c, err := client.New(ctx) - if err != nil { - return err - } - +func runPs(ctx context.Context, backend compose.Service, opts psOptions) error { projectName, err := opts.toProjectName() if err != nil { return err } - containers, err := c.ComposeService().Ps(ctx, projectName, compose.PsOptions{ + containers, err := backend.Ps(ctx, projectName, compose.PsOptions{ All: opts.All, }) if err != nil { diff --git a/cli/cmd/compose/pull.go b/cli/cmd/compose/pull.go index c84dbb052..9fb4df138 100644 --- a/cli/cmd/compose/pull.go +++ b/cli/cmd/compose/pull.go @@ -24,7 +24,6 @@ import ( "github.com/morikuni/aec" "github.com/spf13/cobra" - "github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/progress" "github.com/docker/compose-cli/utils" @@ -40,7 +39,7 @@ type pullOptions struct { ignorePullFailures bool } -func pullCommand(p *projectOptions) *cobra.Command { +func pullCommand(p *projectOptions, backend compose.Service) *cobra.Command { opts := pullOptions{ projectOptions: p, } @@ -51,7 +50,7 @@ func pullCommand(p *projectOptions) *cobra.Command { if opts.noParallel { fmt.Fprint(os.Stderr, aec.Apply("option '--no-parallel' is DEPRECATED and will be ignored.\n", aec.RedF)) } - return runPull(cmd.Context(), opts, args) + return runPull(cmd.Context(), backend, opts, args) }, } flags := cmd.Flags() @@ -65,12 +64,7 @@ func pullCommand(p *projectOptions) *cobra.Command { return cmd } -func runPull(ctx context.Context, opts pullOptions, services []string) error { - c, err := client.New(ctx) - if err != nil { - return err - } - +func runPull(ctx context.Context, backend compose.Service, opts pullOptions, services []string) error { project, err := opts.toProject(services) if err != nil { return err @@ -94,11 +88,11 @@ func runPull(ctx context.Context, opts pullOptions, services []string) error { } if opts.quiet { - return c.ComposeService().Pull(ctx, project, apiOpts) + return backend.Pull(ctx, project, apiOpts) } _, err = progress.Run(ctx, func(ctx context.Context) (string, error) { - return "", c.ComposeService().Pull(ctx, project, apiOpts) + return "", backend.Pull(ctx, project, apiOpts) }) return err } diff --git a/cli/cmd/compose/push.go b/cli/cmd/compose/push.go index 9c1b6a0d2..3f1fb6e2f 100644 --- a/cli/cmd/compose/push.go +++ b/cli/cmd/compose/push.go @@ -21,7 +21,6 @@ import ( "github.com/spf13/cobra" - "github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/progress" ) @@ -33,7 +32,7 @@ type pushOptions struct { Ignorefailures bool } -func pushCommand(p *projectOptions) *cobra.Command { +func pushCommand(p *projectOptions, backend compose.Service) *cobra.Command { opts := pushOptions{ projectOptions: p, } @@ -41,7 +40,7 @@ func pushCommand(p *projectOptions) *cobra.Command { Use: "push [SERVICE...]", Short: "Push service images", RunE: func(cmd *cobra.Command, args []string) error { - return runPush(cmd.Context(), opts, args) + return runPush(cmd.Context(), backend, opts, args) }, } pushCmd.Flags().BoolVar(&opts.Ignorefailures, "ignore-push-failures", false, "Push what it can and ignores images with push failures") @@ -49,19 +48,14 @@ func pushCommand(p *projectOptions) *cobra.Command { return pushCmd } -func runPush(ctx context.Context, opts pushOptions, services []string) error { - c, err := client.New(ctx) - if err != nil { - return err - } - +func runPush(ctx context.Context, backend compose.Service, opts pushOptions, services []string) error { project, err := opts.toProject(services) if err != nil { return err } _, err = progress.Run(ctx, func(ctx context.Context) (string, error) { - return "", c.ComposeService().Push(ctx, project, compose.PushOptions{ + return "", backend.Push(ctx, project, compose.PushOptions{ IgnoreFailures: opts.Ignorefailures, }) }) diff --git a/cli/cmd/compose/remove.go b/cli/cmd/compose/remove.go index 9f3814384..786e0feed 100644 --- a/cli/cmd/compose/remove.go +++ b/cli/cmd/compose/remove.go @@ -21,7 +21,6 @@ import ( "fmt" "strings" - "github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/progress" "github.com/docker/compose-cli/utils/prompt" @@ -36,7 +35,7 @@ type removeOptions struct { volumes bool } -func removeCommand(p *projectOptions) *cobra.Command { +func removeCommand(p *projectOptions, backend compose.Service) *cobra.Command { opts := removeOptions{ projectOptions: p, } @@ -50,7 +49,7 @@ can override this with -v. To list all volumes, use "docker volume ls". Any data which is not in a volume will be lost.`, RunE: func(cmd *cobra.Command, args []string) error { - return runRemove(cmd.Context(), opts, args) + return runRemove(cmd.Context(), backend, opts, args) }, } f := cmd.Flags() @@ -60,12 +59,7 @@ Any data which is not in a volume will be lost.`, return cmd } -func runRemove(ctx context.Context, opts removeOptions, services []string) error { - c, err := client.New(ctx) - if err != nil { - return err - } - +func runRemove(ctx context.Context, backend compose.Service, opts removeOptions, services []string) error { project, err := opts.toProject(services) if err != nil { return err @@ -73,7 +67,7 @@ func runRemove(ctx context.Context, opts removeOptions, services []string) error if opts.stop { _, err = progress.Run(ctx, func(ctx context.Context) (string, error) { - err := c.ComposeService().Stop(ctx, project, compose.StopOptions{ + err := backend.Stop(ctx, project, compose.StopOptions{ Services: services, }) return "", err @@ -83,8 +77,8 @@ func runRemove(ctx context.Context, opts removeOptions, services []string) error } } - reosurces, err := c.ComposeService().Remove(ctx, project, compose.RemoveOptions{ - DryRun: true, + reosurces, err := backend.Remove(ctx, project, compose.RemoveOptions{ + DryRun: true, Services: services, }) if err != nil { @@ -109,7 +103,7 @@ func runRemove(ctx context.Context, opts removeOptions, services []string) error } _, err = progress.Run(ctx, func(ctx context.Context) (string, error) { - _, err = c.ComposeService().Remove(ctx, project, compose.RemoveOptions{ + _, err = backend.Remove(ctx, project, compose.RemoveOptions{ Volumes: opts.volumes, Force: opts.force, }) diff --git a/cli/cmd/compose/restart.go b/cli/cmd/compose/restart.go index 4d10568b6..b6dd7dc0e 100644 --- a/cli/cmd/compose/restart.go +++ b/cli/cmd/compose/restart.go @@ -22,7 +22,6 @@ import ( "github.com/spf13/cobra" - "github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/progress" ) @@ -32,7 +31,7 @@ type restartOptions struct { timeout int } -func restartCommand(p *projectOptions) *cobra.Command { +func restartCommand(p *projectOptions, backend compose.Service) *cobra.Command { opts := restartOptions{ projectOptions: p, } @@ -40,7 +39,7 @@ func restartCommand(p *projectOptions) *cobra.Command { Use: "restart", Short: "Restart containers", RunE: func(cmd *cobra.Command, args []string) error { - return runRestart(cmd.Context(), opts, args) + return runRestart(cmd.Context(), backend, opts, args) }, } flags := restartCmd.Flags() @@ -49,12 +48,7 @@ func restartCommand(p *projectOptions) *cobra.Command { return restartCmd } -func runRestart(ctx context.Context, opts restartOptions, services []string) error { - c, err := client.New(ctx) - if err != nil { - return err - } - +func runRestart(ctx context.Context, backend compose.Service, opts restartOptions, services []string) error { project, err := opts.toProject(services) if err != nil { return err @@ -62,7 +56,7 @@ func runRestart(ctx context.Context, opts restartOptions, services []string) err timeout := time.Duration(opts.timeout) * time.Second _, err = progress.Run(ctx, func(ctx context.Context) (string, error) { - return "", c.ComposeService().Restart(ctx, project, compose.RestartOptions{ + return "", backend.Restart(ctx, project, compose.RestartOptions{ Timeout: &timeout, }) }) diff --git a/cli/cmd/compose/run.go b/cli/cmd/compose/run.go index b3aaab2ba..79028e6e6 100644 --- a/cli/cmd/compose/run.go +++ b/cli/cmd/compose/run.go @@ -28,7 +28,6 @@ import ( "github.com/spf13/cobra" "github.com/docker/cli/cli" - "github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/progress" ) @@ -100,7 +99,7 @@ func (opts runOptions) apply(project *types.Project) error { return nil } -func runCommand(p *projectOptions) *cobra.Command { +func runCommand(p *projectOptions, backend compose.Service) *cobra.Command { opts := runOptions{ composeOptions: &composeOptions{ projectOptions: p, @@ -118,7 +117,7 @@ func runCommand(p *projectOptions) *cobra.Command { if len(opts.publish) > 0 && opts.servicePorts { return fmt.Errorf("--service-ports and --publish are incompatible") } - return runRun(cmd.Context(), opts) + return runRun(cmd.Context(), backend, opts) }, } flags := cmd.Flags() @@ -141,8 +140,8 @@ func runCommand(p *projectOptions) *cobra.Command { return cmd } -func runRun(ctx context.Context, opts runOptions) error { - c, project, err := setup(ctx, *opts.composeOptions, []string{opts.Service}) +func runRun(ctx context.Context, backend compose.Service, opts runOptions) error { + project, err := setup(*opts.composeOptions, []string{opts.Service}) if err != nil { return err } @@ -153,7 +152,7 @@ func runRun(ctx context.Context, opts runOptions) error { } _, err = progress.Run(ctx, func(ctx context.Context) (string, error) { - return "", startDependencies(ctx, c, *project, opts.Service) + return "", startDependencies(ctx, backend, *project, opts.Service) }) if err != nil { return err @@ -194,7 +193,7 @@ func runRun(ctx context.Context, opts runOptions) error { UseNetworkAliases: opts.useAliases, Index: 0, } - exitCode, err := c.ComposeService().RunOneOffContainer(ctx, project, runOpts) + exitCode, err := backend.RunOneOffContainer(ctx, project, runOpts) if exitCode != 0 { errMsg := "" if err != nil { @@ -205,7 +204,7 @@ func runRun(ctx context.Context, opts runOptions) error { return err } -func startDependencies(ctx context.Context, c *client.Client, project types.Project, requestedServiceName string) error { +func startDependencies(ctx context.Context, backend compose.Service, project types.Project, requestedServiceName string) error { dependencies := types.Services{} var requestedService types.ServiceConfig for _, service := range project.Services { @@ -218,10 +217,10 @@ func startDependencies(ctx context.Context, c *client.Client, project types.Proj project.Services = dependencies project.DisabledServices = append(project.DisabledServices, requestedService) - if err := c.ComposeService().Create(ctx, &project, compose.CreateOptions{}); err != nil { + if err := backend.Create(ctx, &project, compose.CreateOptions{}); err != nil { return err } - if err := c.ComposeService().Start(ctx, &project, compose.StartOptions{}); err != nil { + if err := backend.Start(ctx, &project, compose.StartOptions{}); err != nil { return err } return nil diff --git a/cli/cmd/compose/start.go b/cli/cmd/compose/start.go index ac50b7199..f6632a7ba 100644 --- a/cli/cmd/compose/start.go +++ b/cli/cmd/compose/start.go @@ -19,7 +19,6 @@ package compose import ( "context" - "github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/progress" @@ -30,7 +29,7 @@ type startOptions struct { *projectOptions } -func startCommand(p *projectOptions) *cobra.Command { +func startCommand(p *projectOptions, backend compose.Service) *cobra.Command { opts := startOptions{ projectOptions: p, } @@ -38,25 +37,20 @@ func startCommand(p *projectOptions) *cobra.Command { Use: "start [SERVICE...]", Short: "Start services", RunE: func(cmd *cobra.Command, args []string) error { - return runStart(cmd.Context(), opts, args) + return runStart(cmd.Context(), backend, opts, args) }, } return startCmd } -func runStart(ctx context.Context, opts startOptions, services []string) error { - c, err := client.New(ctx) - if err != nil { - return err - } - +func runStart(ctx context.Context, backend compose.Service, opts startOptions, services []string) error { project, err := opts.toProject(services) if err != nil { return err } _, err = progress.Run(ctx, func(ctx context.Context) (string, error) { - return "", c.ComposeService().Start(ctx, project, compose.StartOptions{}) + return "", backend.Start(ctx, project, compose.StartOptions{}) }) return err } diff --git a/cli/cmd/compose/stop.go b/cli/cmd/compose/stop.go index 1884cf1bb..494ac9079 100644 --- a/cli/cmd/compose/stop.go +++ b/cli/cmd/compose/stop.go @@ -22,7 +22,6 @@ import ( "github.com/spf13/cobra" - "github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/progress" ) @@ -33,7 +32,7 @@ type stopOptions struct { timeout int } -func stopCommand(p *projectOptions) *cobra.Command { +func stopCommand(p *projectOptions, backend compose.Service) *cobra.Command { opts := stopOptions{ projectOptions: p, } @@ -42,7 +41,7 @@ func stopCommand(p *projectOptions) *cobra.Command { Short: "Stop services", RunE: func(cmd *cobra.Command, args []string) error { opts.timeChanged = cmd.Flags().Changed("timeout") - return runStop(cmd.Context(), opts, args) + return runStop(cmd.Context(), backend, opts, args) }, } flags := cmd.Flags() @@ -51,12 +50,7 @@ func stopCommand(p *projectOptions) *cobra.Command { return cmd } -func runStop(ctx context.Context, opts stopOptions, services []string) error { - c, err := client.New(ctx) - if err != nil { - return err - } - +func runStop(ctx context.Context, backend compose.Service, opts stopOptions, services []string) error { project, err := opts.toProject(services) if err != nil { return err @@ -68,8 +62,8 @@ func runStop(ctx context.Context, opts stopOptions, services []string) error { timeout = &timeoutValue } _, err = progress.Run(ctx, func(ctx context.Context) (string, error) { - return "", c.ComposeService().Stop(ctx, project, compose.StopOptions{ - Timeout: timeout, + return "", backend.Stop(ctx, project, compose.StopOptions{ + Timeout: timeout, Services: services, }) }) diff --git a/cli/cmd/compose/top.go b/cli/cmd/compose/top.go index cc5719baf..cd5a12bc0 100644 --- a/cli/cmd/compose/top.go +++ b/cli/cmd/compose/top.go @@ -27,14 +27,14 @@ import ( "github.com/spf13/cobra" - "github.com/docker/compose-cli/api/client" + "github.com/docker/compose-cli/api/compose" ) type topOptions struct { *projectOptions } -func topCommand(p *projectOptions) *cobra.Command { +func topCommand(p *projectOptions, backend compose.Service) *cobra.Command { opts := topOptions{ projectOptions: p, } @@ -42,22 +42,18 @@ func topCommand(p *projectOptions) *cobra.Command { Use: "top", Short: "Display the running processes", RunE: func(cmd *cobra.Command, args []string) error { - return runTop(cmd.Context(), opts, args) + return runTop(cmd.Context(), backend, opts, args) }, } return topCmd } -func runTop(ctx context.Context, opts topOptions, services []string) error { - c, err := client.New(ctx) - if err != nil { - return err - } +func runTop(ctx context.Context, backend compose.Service, opts topOptions, services []string) error { projectName, err := opts.toProjectName() if err != nil { return err } - containers, err := c.ComposeService().Top(ctx, projectName, services) + containers, err := backend.Top(ctx, projectName, services) if err != nil { return err } diff --git a/cli/cmd/compose/up.go b/cli/cmd/compose/up.go index 5424d18a7..b6df226d9 100644 --- a/cli/cmd/compose/up.go +++ b/cli/cmd/compose/up.go @@ -33,7 +33,6 @@ import ( "github.com/spf13/cobra" "golang.org/x/sync/errgroup" - "github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/context/store" "github.com/docker/compose-cli/api/progress" @@ -140,7 +139,7 @@ func (opts upOptions) apply(project *types.Project, services []string) error { return nil } -func upCommand(p *projectOptions, contextType string) *cobra.Command { +func upCommand(p *projectOptions, contextType string, backend compose.Service) *cobra.Command { opts := upOptions{ composeOptions: &composeOptions{ projectOptions: p, @@ -168,9 +167,9 @@ func upCommand(p *projectOptions, contextType string) *cobra.Command { if opts.recreateDeps && opts.noRecreate { return fmt.Errorf("--always-recreate-deps and --no-recreate are incompatible") } - return runCreateStart(cmd.Context(), opts, args) + return runCreateStart(cmd.Context(), backend, opts, args) default: - return runUp(cmd.Context(), opts, args) + return runUp(cmd.Context(), backend, opts, args) } }, } @@ -204,8 +203,8 @@ func upCommand(p *projectOptions, contextType string) *cobra.Command { return upCmd } -func runUp(ctx context.Context, opts upOptions, services []string) error { - c, project, err := setup(ctx, *opts.composeOptions, services) +func runUp(ctx context.Context, backend compose.Service, opts upOptions, services []string) error { + project, err := setup(*opts.composeOptions, services) if err != nil { return err } @@ -216,7 +215,7 @@ func runUp(ctx context.Context, opts upOptions, services []string) error { } _, err = progress.Run(ctx, func(ctx context.Context) (string, error) { - return "", c.ComposeService().Up(ctx, project, compose.UpOptions{ + return "", backend.Up(ctx, project, compose.UpOptions{ Detach: opts.Detach, QuietPull: opts.quietPull, }) @@ -224,8 +223,8 @@ func runUp(ctx context.Context, opts upOptions, services []string) error { return err } -func runCreateStart(ctx context.Context, opts upOptions, services []string) error { - c, project, err := setup(ctx, *opts.composeOptions, services) +func runCreateStart(ctx context.Context, backend compose.Service, opts upOptions, services []string) error { + project, err := setup(*opts.composeOptions, services) if err != nil { return err } @@ -240,7 +239,7 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro } _, err = progress.Run(ctx, func(ctx context.Context) (string, error) { - err := c.ComposeService().Create(ctx, project, compose.CreateOptions{ + err := backend.Create(ctx, project, compose.CreateOptions{ Services: services, RemoveOrphans: opts.removeOrphans, Recreate: opts.recreateStrategy(), @@ -253,7 +252,7 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro return "", err } if opts.Detach { - err = c.ComposeService().Start(ctx, project, compose.StartOptions{}) + err = backend.Start(ctx, project, compose.StartOptions{}) } return "", err }) @@ -285,10 +284,10 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro _, err := progress.Run(ctx, func(ctx context.Context) (string, error) { go func() { <-signalChan - c.ComposeService().Kill(ctx, project, compose.KillOptions{}) // nolint:errcheck + backend.Kill(ctx, project, compose.KillOptions{}) // nolint:errcheck }() - return "", c.ComposeService().Stop(ctx, project, compose.StopOptions{}) + return "", backend.Stop(ctx, project, compose.StopOptions{}) }) return err } @@ -311,7 +310,7 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro return err }) - err = c.ComposeService().Start(ctx, project, compose.StartOptions{ + err = backend.Start(ctx, project, compose.StartOptions{ Attach: func(event compose.ContainerEvent) { queue <- event }, @@ -351,15 +350,10 @@ func setServiceScale(project *types.Project, name string, replicas int) error { return fmt.Errorf("unknown service %q", name) } -func setup(ctx context.Context, opts composeOptions, services []string) (*client.Client, *types.Project, error) { - c, err := client.New(ctx) - if err != nil { - return nil, nil, err - } - +func setup(opts composeOptions, services []string) (*types.Project, error) { project, err := opts.toProject(services) if err != nil { - return nil, nil, err + return nil, err } if opts.DomainName != "" { @@ -397,7 +391,7 @@ func setup(ctx context.Context, opts composeOptions, services []string) (*client project.Services = services } - return c, project, nil + return project, nil } type printer struct { diff --git a/cli/main.go b/cli/main.go index d5aba3938..2e8055a89 100644 --- a/cli/main.go +++ b/cli/main.go @@ -221,7 +221,7 @@ func main() { root.AddCommand( run.Command(ctype), - compose.Command(ctype), + compose.Command(ctype, service.ComposeService()), volume.Command(ctype), ) diff --git a/docs/yaml/main/generate.go b/docs/yaml/main/generate.go index 63792115c..4ca4a16bb 100644 --- a/docs/yaml/main/generate.go +++ b/docs/yaml/main/generate.go @@ -35,7 +35,7 @@ const descriptionSourcePath = "docs/reference/" func generateCliYaml(opts *options) error { cmd := &cobra.Command{Use: "docker"} - cmd.AddCommand(compose.Command("local")) + cmd.AddCommand(compose.Command("local", nil)) disableFlagsInUseLine(cmd) source := filepath.Join(opts.source, descriptionSourcePath) if err := loadLongDescription(cmd, source); err != nil { From 85af8cdaaa1f5d170e41449493284b0167b2c721 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Thu, 15 Apr 2021 09:09:35 +0200 Subject: [PATCH 2/9] local backend to rely on dockerCli's LoadDefaultConfigFile Signed-off-by: Nicolas De Loof --- cli/cmd/compose/convert.go | 6 +----- ecs/local/backend.go | 6 ++++-- local/backend.go | 6 +++++- local/compose/build.go | 9 +-------- local/compose/compose.go | 9 ++++++--- local/compose/pull.go | 8 +------- 6 files changed, 18 insertions(+), 26 deletions(-) diff --git a/cli/cmd/compose/convert.go b/cli/cmd/compose/convert.go index a01d10be7..01b0fbe52 100644 --- a/cli/cmd/compose/convert.go +++ b/cli/cmd/compose/convert.go @@ -32,7 +32,6 @@ import ( "github.com/spf13/cobra" "github.com/docker/compose-cli/api/compose" - "github.com/docker/compose-cli/api/config" "github.com/docker/compose-cli/utils" ) @@ -109,10 +108,7 @@ func runConvert(ctx context.Context, backend compose.Service, opts convertOption } if opts.resolve { - configFile, err := cliconfig.Load(config.Dir()) - if err != nil { - return err - } + configFile := cliconfig.LoadDefaultConfigFile(os.Stderr) resolver := remotes.CreateResolver(configFile) err = project.ResolveImages(func(named reference.Named) (digest.Digest, error) { diff --git a/ecs/local/backend.go b/ecs/local/backend.go index 8e26b768e..0b521ff1e 100644 --- a/ecs/local/backend.go +++ b/ecs/local/backend.go @@ -17,8 +17,9 @@ package local import ( - local_compose "github.com/docker/compose-cli/local/compose" + "os" + cliconfig "github.com/docker/cli/cli/config" "github.com/docker/docker/client" "github.com/docker/compose-cli/api/backend" @@ -29,6 +30,7 @@ import ( "github.com/docker/compose-cli/api/resources" "github.com/docker/compose-cli/api/secrets" "github.com/docker/compose-cli/api/volumes" + local_compose "github.com/docker/compose-cli/local/compose" ) const backendType = store.EcsLocalSimulationContextType @@ -50,7 +52,7 @@ func service() (backend.Service, error) { return &ecsLocalSimulation{ moby: apiClient, - compose: local_compose.NewComposeService(apiClient), + compose: local_compose.NewComposeService(apiClient, cliconfig.LoadDefaultConfigFile(os.Stderr)), }, nil } diff --git a/local/backend.go b/local/backend.go index e315f5a08..7050180c4 100644 --- a/local/backend.go +++ b/local/backend.go @@ -17,6 +17,9 @@ package local import ( + "os" + + cliconfig "github.com/docker/cli/cli/config" "github.com/docker/docker/client" "github.com/docker/compose-cli/api/backend" @@ -36,10 +39,11 @@ type local struct { // NewService build a backend for "local" context, using Docker API client func NewService(apiClient client.APIClient) backend.Service { + file := cliconfig.LoadDefaultConfigFile(os.Stderr) return &local{ containerService: &containerService{apiClient}, volumeService: &volumeService{apiClient}, - composeService: local_compose.NewComposeService(apiClient), + composeService: local_compose.NewComposeService(apiClient, file), } } diff --git a/local/compose/build.go b/local/compose/build.go index 5afb17116..9b2b1339f 100644 --- a/local/compose/build.go +++ b/local/compose/build.go @@ -28,13 +28,11 @@ import ( "github.com/docker/buildx/driver" _ "github.com/docker/buildx/driver/docker" // required to get default driver registered "github.com/docker/buildx/util/progress" - cliconfig "github.com/docker/cli/cli/config" moby "github.com/docker/docker/api/types" bclient "github.com/moby/buildkit/client" specs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/docker/compose-cli/api/compose" - "github.com/docker/compose-cli/api/config" composeprogress "github.com/docker/compose-cli/api/progress" "github.com/docker/compose-cli/cli/metrics" "github.com/docker/compose-cli/utils" @@ -195,12 +193,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opts } const drivername = "default" - configFile, err := cliconfig.Load(config.Dir()) - if err != nil { - return nil, err - } - - d, err := driver.GetDriver(ctx, drivername, nil, s.apiClient, configFile, nil, nil, "", nil, nil, project.WorkingDir) + d, err := driver.GetDriver(ctx, drivername, nil, s.apiClient, s.configFile, nil, nil, "", nil, nil, project.WorkingDir) if err != nil { return nil, err } diff --git a/local/compose/compose.go b/local/compose/compose.go index 038ac615c..db60cbf5e 100644 --- a/local/compose/compose.go +++ b/local/compose/compose.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/docker/cli/cli/config/configfile" "strings" "github.com/docker/compose-cli/api/compose" @@ -32,14 +33,16 @@ import ( ) // NewComposeService create a local implementation of the compose.Service API -func NewComposeService(apiClient client.APIClient) compose.Service { +func NewComposeService(apiClient client.APIClient, configFile *configfile.ConfigFile) compose.Service { return &composeService{ - apiClient: apiClient, + apiClient: apiClient, + configFile: configFile, } } type composeService struct { - apiClient client.APIClient + apiClient client.APIClient + configFile *configfile.ConfigFile } func (s *composeService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error { diff --git a/local/compose/pull.go b/local/compose/pull.go index 7e114b487..0df5e45c7 100644 --- a/local/compose/pull.go +++ b/local/compose/pull.go @@ -27,23 +27,17 @@ import ( "github.com/compose-spec/compose-go/types" "github.com/distribution/distribution/v3/reference" "github.com/docker/buildx/driver" - cliconfig "github.com/docker/cli/cli/config" moby "github.com/docker/docker/api/types" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/registry" "golang.org/x/sync/errgroup" "github.com/docker/compose-cli/api/compose" - "github.com/docker/compose-cli/api/config" "github.com/docker/compose-cli/api/progress" "github.com/docker/compose-cli/cli/metrics" ) func (s *composeService) Pull(ctx context.Context, project *types.Project, opts compose.PullOptions) error { - configFile, err := cliconfig.Load(config.Dir()) - if err != nil { - return err - } info, err := s.apiClient.Info(ctx) if err != nil { return err @@ -67,7 +61,7 @@ func (s *composeService) Pull(ctx context.Context, project *types.Project, opts continue } eg.Go(func() error { - err := s.pullServiceImage(ctx, service, info, configFile, w) + err := s.pullServiceImage(ctx, service, info, s.configFile, w) if err != nil { if !opts.IgnoreFailures { return err From 1dc97e8c4b3a60ccc574dd6f06c04447255fdc7e Mon Sep 17 00:00:00 2001 From: Guillaume Tardif Date: Thu, 4 Mar 2021 19:21:42 +0100 Subject: [PATCH 3/9] Compose as a cli plugin Signed-off-by: Guillaume Tardif Signed-off-by: Nicolas De Loof --- api/compose/delegator.go | 143 +++++++++++++++++++++++++++++++++++++ api/compose/noimpl.go | 143 +++++++++++++++++++++++++++++++++++++ builder.Makefile | 4 ++ cli/cmd/compose/compose.go | 8 +++ cli/config/flags.go | 35 +++++++++ cli/config/flags_test.go | 61 ++++++++++++++++ cli/main.go | 62 ++-------------- cli/main_test.go | 36 ---------- local/backend.go | 28 ++++++++ main.go | 58 +++++++++++++++ utils/e2e/framework.go | 13 +++- 11 files changed, 494 insertions(+), 97 deletions(-) create mode 100644 api/compose/delegator.go create mode 100644 api/compose/noimpl.go create mode 100644 cli/config/flags_test.go create mode 100644 main.go diff --git a/api/compose/delegator.go b/api/compose/delegator.go new file mode 100644 index 000000000..c79ded64f --- /dev/null +++ b/api/compose/delegator.go @@ -0,0 +1,143 @@ +/* + 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" + + "github.com/compose-spec/compose-go/types" +) + +// ServiceDelegator implements Service by delegating to another implementation. This allows lazy init +type ServiceDelegator struct { + Delegate Service +} + +//Build implements Service interface +func (s *ServiceDelegator) Build(ctx context.Context, project *types.Project, options BuildOptions) error { + return s.Delegate.Build(ctx, project, options) +} + +//Push implements Service interface +func (s *ServiceDelegator) Push(ctx context.Context, project *types.Project, options PushOptions) error { + return s.Delegate.Push(ctx, project, options) +} + +//Pull implements Service interface +func (s *ServiceDelegator) Pull(ctx context.Context, project *types.Project, options PullOptions) error { + return s.Delegate.Pull(ctx, project, options) +} + +//Create implements Service interface +func (s *ServiceDelegator) Create(ctx context.Context, project *types.Project, options CreateOptions) error { + return s.Delegate.Create(ctx, project, options) +} + +//Start implements Service interface +func (s *ServiceDelegator) Start(ctx context.Context, project *types.Project, options StartOptions) error { + return s.Delegate.Start(ctx, project, options) +} + +//Restart implements Service interface +func (s *ServiceDelegator) Restart(ctx context.Context, project *types.Project, options RestartOptions) error { + return s.Delegate.Restart(ctx, project, options) +} + +//Stop implements Service interface +func (s *ServiceDelegator) Stop(ctx context.Context, project *types.Project, options StopOptions) error { + return s.Delegate.Stop(ctx, project, options) +} + +//Up implements Service interface +func (s *ServiceDelegator) Up(ctx context.Context, project *types.Project, options UpOptions) error { + return s.Delegate.Up(ctx, project, options) +} + +//Down implements Service interface +func (s *ServiceDelegator) Down(ctx context.Context, project string, options DownOptions) error { + return s.Delegate.Down(ctx, project, options) +} + +//Logs implements Service interface +func (s *ServiceDelegator) Logs(ctx context.Context, project string, consumer LogConsumer, options LogOptions) error { + return s.Delegate.Logs(ctx, project, consumer, options) +} + +//Ps implements Service interface +func (s *ServiceDelegator) Ps(ctx context.Context, project string, options PsOptions) ([]ContainerSummary, error) { + return s.Delegate.Ps(ctx, project, options) +} + +//List implements Service interface +func (s *ServiceDelegator) List(ctx context.Context, options ListOptions) ([]Stack, error) { + return s.Delegate.List(ctx, options) +} + +//Convert implements Service interface +func (s *ServiceDelegator) Convert(ctx context.Context, project *types.Project, options ConvertOptions) ([]byte, error) { + return s.Delegate.Convert(ctx, project, options) +} + +//Kill implements Service interface +func (s *ServiceDelegator) Kill(ctx context.Context, project *types.Project, options KillOptions) error { + return s.Delegate.Kill(ctx, project, options) +} + +//RunOneOffContainer implements Service interface +func (s *ServiceDelegator) RunOneOffContainer(ctx context.Context, project *types.Project, options RunOptions) (int, error) { + return s.Delegate.RunOneOffContainer(ctx, project, options) +} + +//Remove implements Service interface +func (s *ServiceDelegator) Remove(ctx context.Context, project *types.Project, options RemoveOptions) ([]string, error) { + return s.Delegate.Remove(ctx, project, options) +} + +//Exec implements Service interface +func (s *ServiceDelegator) Exec(ctx context.Context, project *types.Project, options RunOptions) error { + return s.Delegate.Exec(ctx, project, options) +} + +//Pause implements Service interface +func (s *ServiceDelegator) Pause(ctx context.Context, project string, options PauseOptions) error { + return s.Delegate.Pause(ctx, project, options) +} + +//UnPause implements Service interface +func (s *ServiceDelegator) UnPause(ctx context.Context, project string, options PauseOptions) error { + return s.Delegate.UnPause(ctx, project, options) +} + +//Top implements Service interface +func (s *ServiceDelegator) Top(ctx context.Context, project string, services []string) ([]ContainerProcSummary, error) { + return s.Delegate.Top(ctx, project, services) +} + +//Events implements Service interface +func (s *ServiceDelegator) Events(ctx context.Context, project string, options EventsOptions) error { + return s.Delegate.Events(ctx, project, options) +} + +//Port implements Service interface +func (s *ServiceDelegator) Port(ctx context.Context, project string, service string, port int, options PortOptions) (string, int, error) { + return s.Delegate.Port(ctx, project, service, port, options) +} + +//Images implements Service interface +func (s *ServiceDelegator) Images(ctx context.Context, project string, options ImagesOptions) ([]ImageSummary, error) { + return s.Delegate.Images(ctx, project, options) +} diff --git a/api/compose/noimpl.go b/api/compose/noimpl.go new file mode 100644 index 000000000..172af3e47 --- /dev/null +++ b/api/compose/noimpl.go @@ -0,0 +1,143 @@ +/* + 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" + + "github.com/compose-spec/compose-go/types" + + "github.com/docker/compose-cli/api/errdefs" +) + +// NoImpl implements Service to return ErrNotImplemented +type NoImpl struct{} + +//Build implements Service interface +func (s NoImpl) Build(ctx context.Context, project *types.Project, options BuildOptions) error { + return errdefs.ErrNotImplemented +} + +//Push implements Service interface +func (s NoImpl) Push(ctx context.Context, project *types.Project, options PushOptions) error { + return errdefs.ErrNotImplemented +} + +//Pull implements Service interface +func (s NoImpl) Pull(ctx context.Context, project *types.Project, options PullOptions) error { + return errdefs.ErrNotImplemented +} + +//Create implements Service interface +func (s NoImpl) Create(ctx context.Context, project *types.Project, options CreateOptions) error { + return errdefs.ErrNotImplemented +} + +//Start implements Service interface +func (s NoImpl) Start(ctx context.Context, project *types.Project, options StartOptions) error { + return errdefs.ErrNotImplemented +} + +//Restart implements Service interface +func (s NoImpl) Restart(ctx context.Context, project *types.Project, options RestartOptions) error { + return errdefs.ErrNotImplemented +} + +//Stop implements Service interface +func (s NoImpl) Stop(ctx context.Context, project *types.Project, options StopOptions) error { + return errdefs.ErrNotImplemented +} + +//Up implements Service interface +func (s NoImpl) Up(ctx context.Context, project *types.Project, options UpOptions) error { + return errdefs.ErrNotImplemented +} + +//Down implements Service interface +func (s NoImpl) Down(ctx context.Context, project string, options DownOptions) error { + return errdefs.ErrNotImplemented +} + +//Logs implements Service interface +func (s NoImpl) Logs(ctx context.Context, project string, consumer LogConsumer, options LogOptions) error { + return errdefs.ErrNotImplemented +} + +//Ps implements Service interface +func (s NoImpl) Ps(ctx context.Context, project string, options PsOptions) ([]ContainerSummary, error) { + return nil, errdefs.ErrNotImplemented +} + +//List implements Service interface +func (s NoImpl) List(ctx context.Context, options ListOptions) ([]Stack, error) { + return nil, errdefs.ErrNotImplemented +} + +//Convert implements Service interface +func (s NoImpl) Convert(ctx context.Context, project *types.Project, options ConvertOptions) ([]byte, error) { + return nil, errdefs.ErrNotImplemented +} + +//Kill implements Service interface +func (s NoImpl) Kill(ctx context.Context, project *types.Project, options KillOptions) error { + return errdefs.ErrNotImplemented +} + +//RunOneOffContainer implements Service interface +func (s NoImpl) RunOneOffContainer(ctx context.Context, project *types.Project, options RunOptions) (int, error) { + return 0, errdefs.ErrNotImplemented +} + +//Remove implements Service interface +func (s NoImpl) Remove(ctx context.Context, project *types.Project, options RemoveOptions) ([]string, error) { + return nil, errdefs.ErrNotImplemented +} + +//Exec implements Service interface +func (s NoImpl) Exec(ctx context.Context, project *types.Project, options RunOptions) error { + return errdefs.ErrNotImplemented +} + +//Pause implements Service interface +func (s NoImpl) Pause(ctx context.Context, project string, options PauseOptions) error { + return errdefs.ErrNotImplemented +} + +//UnPause implements Service interface +func (s NoImpl) UnPause(ctx context.Context, project string, options PauseOptions) error { + return errdefs.ErrNotImplemented +} + +//Top implements Service interface +func (s NoImpl) Top(ctx context.Context, project string, services []string) ([]ContainerProcSummary, error) { + return nil, errdefs.ErrNotImplemented +} + +//Events implements Service interface +func (s NoImpl) Events(ctx context.Context, project string, options EventsOptions) error { + return errdefs.ErrNotImplemented +} + +//Port implements Service interface +func (s NoImpl) Port(ctx context.Context, project string, service string, port int, options PortOptions) (string, int, error) { + return "", 0, errdefs.ErrNotImplemented +} + +//Images implements Service interface +func (s NoImpl) Images(ctx context.Context, project string, options ImagesOptions) ([]ImageSummary, error) { + return nil, errdefs.ErrNotImplemented +} diff --git a/builder.Makefile b/builder.Makefile index 4e1983dc8..079da2bc1 100644 --- a/builder.Makefile +++ b/builder.Makefile @@ -57,6 +57,10 @@ protos: cli: GOOS=${GOOS} GOARCH=${GOARCH} $(GO_BUILD) $(TAGS) -o $(BINARY_WITH_EXTENSION) ./cli +.PHONY: compose-plugin +compose-plugin: + GOOS=${GOOS} GOARCH=${GOARCH} $(GO_BUILD) $(TAGS) -o ./bin/docker-compose . + .PHONY: cross cross: GOOS=linux GOARCH=amd64 $(GO_BUILD) $(TAGS) -o $(BINARY)-linux-amd64 ./cli diff --git a/cli/cmd/compose/compose.go b/cli/cmd/compose/compose.go index 49bf3e9e1..da105b9c4 100644 --- a/cli/cmd/compose/compose.go +++ b/cli/cmd/compose/compose.go @@ -123,6 +123,14 @@ func Command(contextType string, backend compose.Service) *cobra.Command { return fmt.Errorf("unknown docker command: %q", "compose "+args[0]) }, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + parent := cmd.Root() + parentPrerun := parent.PersistentPreRunE + if parentPrerun != nil { + err := parentPrerun(cmd, args) + if err != nil { + return err + } + } if noAnsi { if ansi != "auto" { return errors.New(`cannot specify DEPRECATED "--no-ansi" and "--ansi". Please use only "--ansi"`) diff --git a/cli/config/flags.go b/cli/config/flags.go index 2a5265b27..f38d967e2 100644 --- a/cli/config/flags.go +++ b/cli/config/flags.go @@ -17,9 +17,11 @@ package config import ( + "fmt" "os" "path/filepath" + "github.com/pkg/errors" "github.com/spf13/pflag" "github.com/docker/compose-cli/api/config" @@ -44,3 +46,36 @@ func confDir() string { home, _ := os.UserHomeDir() return filepath.Join(home, config.ConfigFileDir) } + +// GetCurrentContext get current context based on opts, env vars +func GetCurrentContext(contextOpt string, configDir string, hosts []string) string { + // host and context flags cannot be both set at the same time -- the local backend enforces this when resolving hostname + // -H flag disables context --> set default as current + if len(hosts) > 0 { + return "default" + } + // DOCKER_HOST disables context --> set default as current + if _, present := os.LookupEnv("DOCKER_HOST"); present { + return "default" + } + res := contextOpt + if res == "" { + // check if DOCKER_CONTEXT env variable was set + if _, present := os.LookupEnv("DOCKER_CONTEXT"); present { + res = os.Getenv("DOCKER_CONTEXT") + } + + if res == "" { + config, err := config.LoadFile(configDir) + if err != nil { + fmt.Fprintln(os.Stderr, errors.Wrap(err, "WARNING")) + return "default" + } + res = config.CurrentContext + } + } + if res == "" { + res = "default" + } + return res +} diff --git a/cli/config/flags_test.go b/cli/config/flags_test.go new file mode 100644 index 000000000..3e6890215 --- /dev/null +++ b/cli/config/flags_test.go @@ -0,0 +1,61 @@ +/* + 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 config + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "gotest.tools/v3/assert" + + "github.com/docker/compose-cli/api/config" +) + +var contextSetConfig = []byte(`{ + "currentContext": "some-context" +}`) + +func TestDetermineCurrentContext(t *testing.T) { + d, err := ioutil.TempDir("", "") + // nolint errcheck + defer os.RemoveAll(d) + assert.NilError(t, err) + err = ioutil.WriteFile(filepath.Join(d, config.ConfigFileName), contextSetConfig, 0644) + assert.NilError(t, err) + + // If nothing set, fallback to default + c := GetCurrentContext("", "", []string{}) + assert.Equal(t, c, "default") + + // If context flag set, use that + c = GetCurrentContext("other-context", "", []string{}) + assert.Equal(t, c, "other-context") + + // If no context flag, use config + c = GetCurrentContext("", d, []string{}) + assert.Equal(t, c, "some-context") + + // Ensure context flag overrides config + c = GetCurrentContext("other-context", d, []string{}) + assert.Equal(t, "other-context", c) + + // Ensure host flag overrides context + c = GetCurrentContext("other-context", d, []string{"hostname"}) + assert.Equal(t, "default", c) +} diff --git a/cli/main.go b/cli/main.go index 2e8055a89..ae6ab7c11 100644 --- a/cli/main.go +++ b/cli/main.go @@ -29,9 +29,6 @@ import ( "time" "github.com/docker/cli/cli" - "github.com/docker/cli/cli/command" - cliconfig "github.com/docker/cli/cli/config" - cliflags "github.com/docker/cli/cli/flags" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -48,6 +45,7 @@ import ( "github.com/docker/compose-cli/cli/cmd/logout" "github.com/docker/compose-cli/cli/cmd/run" "github.com/docker/compose-cli/cli/cmd/volume" + cliconfig "github.com/docker/compose-cli/cli/config" "github.com/docker/compose-cli/cli/metrics" "github.com/docker/compose-cli/cli/mobycli" cliopts "github.com/docker/compose-cli/cli/options" @@ -62,7 +60,6 @@ import ( var ( contextAgnosticCommands = map[string]struct{}{ - "compose": {}, "context": {}, "login": {}, "logout": {}, @@ -198,7 +195,7 @@ func main() { configDir := opts.Config config.WithDir(configDir) - currentContext := determineCurrentContext(opts.Context, configDir, opts.Hosts) + currentContext := cliconfig.GetCurrentContext(opts.Context, configDir, opts.Hosts) apicontext.WithCurrentContext(currentContext) s, err := store.New(configDir) @@ -234,27 +231,7 @@ func main() { func getBackend(ctype string, configDir string, opts cliopts.GlobalOpts) (backend.Service, error) { switch ctype { case store.DefaultContextType, store.LocalContextType: - configFile, err := cliconfig.Load(configDir) - if err != nil { - return nil, err - } - options := cliflags.CommonOptions{ - Context: opts.Context, - Debug: opts.Debug, - Hosts: opts.Hosts, - LogLevel: opts.LogLevel, - } - - if opts.TLSVerify { - options.TLS = opts.TLS - options.TLSVerify = opts.TLSVerify - options.TLSOptions = opts.TLSOptions - } - apiClient, err := command.NewAPIClientFromFlags(&options, configFile) - if err != nil { - return nil, err - } - return local.NewService(apiClient), nil + return local.GetLocalBackend(configDir, opts) } service, err := backend.Get(ctype) if errdefs.IsNotFoundError(err) { @@ -311,6 +288,7 @@ func exit(ctx string, err error, ctype string) { } if compose.Warning != "" { + logrus.Warn(err) fmt.Fprintln(os.Stderr, compose.Warning) } @@ -354,38 +332,6 @@ func newSigContext() (context.Context, func()) { return ctx, cancel } -func determineCurrentContext(flag string, configDir string, hosts []string) string { - // host and context flags cannot be both set at the same time -- the local backend enforces this when resolving hostname - // -H flag disables context --> set default as current - if len(hosts) > 0 { - return "default" - } - // DOCKER_HOST disables context --> set default as current - if _, present := os.LookupEnv("DOCKER_HOST"); present { - return "default" - } - res := flag - if res == "" { - // check if DOCKER_CONTEXT env variable was set - if _, present := os.LookupEnv("DOCKER_CONTEXT"); present { - res = os.Getenv("DOCKER_CONTEXT") - } - - if res == "" { - config, err := config.LoadFile(configDir) - if err != nil { - fmt.Fprintln(os.Stderr, errors.Wrap(err, "WARNING")) - return "default" - } - res = config.CurrentContext - } - } - if res == "" { - res = "default" - } - return res -} - func walk(c *cobra.Command, f func(*cobra.Command)) { f(c) for _, c := range c.Commands() { diff --git a/cli/main_test.go b/cli/main_test.go index 3a0ee145c..85caf2034 100644 --- a/cli/main_test.go +++ b/cli/main_test.go @@ -17,53 +17,17 @@ package main import ( - "io/ioutil" "os" - "path/filepath" "testing" "gotest.tools/v3/assert" - "github.com/docker/compose-cli/api/config" "github.com/docker/compose-cli/cli/cmd" "github.com/docker/compose-cli/cli/cmd/context" "github.com/docker/compose-cli/cli/cmd/login" "github.com/docker/compose-cli/cli/cmd/run" ) -var contextSetConfig = []byte(`{ - "currentContext": "some-context" -}`) - -func TestDetermineCurrentContext(t *testing.T) { - d, err := ioutil.TempDir("", "") - // nolint errcheck - defer os.RemoveAll(d) - assert.NilError(t, err) - err = ioutil.WriteFile(filepath.Join(d, config.ConfigFileName), contextSetConfig, 0644) - assert.NilError(t, err) - - // If nothing set, fallback to default - c := determineCurrentContext("", "", []string{}) - assert.Equal(t, c, "default") - - // If context flag set, use that - c = determineCurrentContext("other-context", "", []string{}) - assert.Equal(t, c, "other-context") - - // If no context flag, use config - c = determineCurrentContext("", d, []string{}) - assert.Equal(t, c, "some-context") - - // Ensure context flag overrides config - c = determineCurrentContext("other-context", d, []string{}) - assert.Equal(t, "other-context", c) - - // Ensure host flag overrides context - c = determineCurrentContext("other-context", d, []string{"hostname"}) - assert.Equal(t, "default", c) -} - func TestCheckOwnCommand(t *testing.T) { assert.Assert(t, isContextAgnosticCommand(login.Command())) assert.Assert(t, isContextAgnosticCommand(context.Command())) diff --git a/local/backend.go b/local/backend.go index 7050180c4..95ab2f573 100644 --- a/local/backend.go +++ b/local/backend.go @@ -19,7 +19,9 @@ package local import ( "os" + "github.com/docker/cli/cli/command" cliconfig "github.com/docker/cli/cli/config" + cliflags "github.com/docker/cli/cli/flags" "github.com/docker/docker/client" "github.com/docker/compose-cli/api/backend" @@ -28,6 +30,7 @@ import ( "github.com/docker/compose-cli/api/resources" "github.com/docker/compose-cli/api/secrets" "github.com/docker/compose-cli/api/volumes" + cliopts "github.com/docker/compose-cli/cli/options" local_compose "github.com/docker/compose-cli/local/compose" ) @@ -47,6 +50,31 @@ func NewService(apiClient client.APIClient) backend.Service { } } +// GetLocalBackend initialize local backend +func GetLocalBackend(configDir string, opts cliopts.GlobalOpts) (backend.Service, error) { + configFile, err := cliconfig.Load(configDir) + if err != nil { + return nil, err + } + options := cliflags.CommonOptions{ + Context: opts.Context, + Debug: opts.Debug, + Hosts: opts.Hosts, + LogLevel: opts.LogLevel, + } + + if opts.TLSVerify { + options.TLS = opts.TLS + options.TLSVerify = opts.TLSVerify + options.TLSOptions = opts.TLSOptions + } + apiClient, err := command.NewAPIClientFromFlags(&options, configFile) + if err != nil { + return nil, err + } + return NewService(apiClient), nil +} + func (s *local) ContainerService() containers.Service { return s.containerService } diff --git a/main.go b/main.go new file mode 100644 index 000000000..fa801fa65 --- /dev/null +++ b/main.go @@ -0,0 +1,58 @@ +/* + 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 main + +import ( + "strings" + + "github.com/spf13/cobra" + + "github.com/docker/cli/cli-plugins/manager" + "github.com/docker/cli/cli-plugins/plugin" + "github.com/docker/cli/cli/command" + api "github.com/docker/compose-cli/api/compose" + "github.com/docker/compose-cli/api/context/store" + "github.com/docker/compose-cli/cli/cmd/compose" + "github.com/docker/compose-cli/internal" + impl "github.com/docker/compose-cli/local/compose" +) + +func main() { + plugin.Run(func(dockerCli command.Cli) *cobra.Command { + lazyInit := api.ServiceDelegator{ + Delegate: api.NoImpl{}, + } + cmd := compose.Command(store.DefaultContextType, &lazyInit) + originalPreRun := cmd.PersistentPreRunE + cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + if err := plugin.PersistentPreRunE(cmd, args); err != nil { + return err + } + lazyInit.Delegate = impl.NewComposeService(dockerCli.Client(), dockerCli.ConfigFile()) + if originalPreRun != nil { + return originalPreRun(cmd, args) + } + return nil + } + return cmd + }, + manager.Metadata{ + SchemaVersion: "0.1.0", + Vendor: "Docker Inc.", + Version: strings.TrimPrefix(internal.Version, "v"), + }) +} diff --git a/utils/e2e/framework.go b/utils/e2e/framework.go index ce9360e4a..5ce8da67c 100644 --- a/utils/e2e/framework.go +++ b/utils/e2e/framework.go @@ -85,6 +85,13 @@ func newE2eCLI(t *testing.T, binDir string) *E2eCLI { _ = os.RemoveAll(d) }) + _ = os.MkdirAll(filepath.Join(d, "cli-plugins"), 0755) + composePlugin, _ := findExecutable("docker-compose", []string{"../../bin", "../../../bin"}) + err = CopyFile(composePlugin, filepath.Join(d, "cli-plugins", "docker-compose")) + if err != nil { + panic(err) + } + return &E2eCLI{binDir, d, t} } @@ -117,7 +124,7 @@ func SetupExistingCLI() (string, func(), error) { return "", nil, err } - bin, err := findExecutable([]string{"../../bin", "../../../bin"}) + bin, err := findExecutable(DockerExecutableName, []string{"../../bin", "../../../bin"}) if err != nil { return "", nil, err } @@ -133,9 +140,9 @@ func SetupExistingCLI() (string, func(), error) { return d, cleanup, nil } -func findExecutable(paths []string) (string, error) { +func findExecutable(executableName string, paths []string) (string, error) { for _, p := range paths { - bin, err := filepath.Abs(path.Join(p, DockerExecutableName)) + bin, err := filepath.Abs(path.Join(p, executableName)) if err != nil { return "", err } From c7dc12d310eb2e9baac31c3456b673f956b9c0b8 Mon Sep 17 00:00:00 2001 From: Guillaume Tardif Date: Fri, 5 Mar 2021 10:50:06 +0100 Subject: [PATCH 4/9] E2E tests using compose CLI plugin Signed-off-by: Guillaume Tardif --- builder.Makefile | 7 +++++-- local/e2e/compose/compose_test.go | 10 ++++++++++ utils/e2e/framework.go | 8 ++++++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/builder.Makefile b/builder.Makefile index 079da2bc1..ca8736a46 100644 --- a/builder.Makefile +++ b/builder.Makefile @@ -34,6 +34,9 @@ GO_BUILD=$(STATIC_FLAGS) go build -trimpath -ldflags=$(LDFLAGS) BINARY?=bin/docker BINARY_WITH_EXTENSION=$(BINARY)$(EXTENSION) +COMPOSE_BINARY?=bin/docker-compose +COMPOSE_BINARY_WITH_EXTENSION=$(COMPOSE_BINARY)$(EXTENSION) + WORK_DIR:=$(shell mktemp -d) TAGS:= @@ -54,12 +57,12 @@ protos: protoc -I. --go_out=plugins=grpc,paths=source_relative:. ${PROTOS} .PHONY: cli -cli: +cli: compose-plugin GOOS=${GOOS} GOARCH=${GOARCH} $(GO_BUILD) $(TAGS) -o $(BINARY_WITH_EXTENSION) ./cli .PHONY: compose-plugin compose-plugin: - GOOS=${GOOS} GOARCH=${GOARCH} $(GO_BUILD) $(TAGS) -o ./bin/docker-compose . + GOOS=${GOOS} GOARCH=${GOARCH} $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY_WITH_EXTENSION) ./compose_plugin .PHONY: cross cross: diff --git a/local/e2e/compose/compose_test.go b/local/e2e/compose/compose_test.go index d62c5b3b6..732b743f1 100644 --- a/local/e2e/compose/compose_test.go +++ b/local/e2e/compose/compose_test.go @@ -20,6 +20,7 @@ import ( "fmt" "net/http" "os" + "path/filepath" "regexp" "strings" "testing" @@ -131,6 +132,15 @@ func TestLocalComposeUp(t *testing.T) { }) } +func TestComposeUsingCliPlugin(t *testing.T) { + c := NewParallelE2eCLI(t, binDir) + + err := os.Remove(filepath.Join(c.ConfigDir, "cli-plugins", "docker-compose")) + assert.NilError(t, err) + res := c.RunDockerOrExitError("compose", "ls") + res.Assert(t, icmd.Expected{Err: "'compose' is not a docker command", ExitCode: 1}) +} + func TestComposePull(t *testing.T) { c := NewParallelE2eCLI(t, binDir) diff --git a/utils/e2e/framework.go b/utils/e2e/framework.go index 5ce8da67c..a194f5049 100644 --- a/utils/e2e/framework.go +++ b/utils/e2e/framework.go @@ -86,8 +86,12 @@ func newE2eCLI(t *testing.T, binDir string) *E2eCLI { }) _ = os.MkdirAll(filepath.Join(d, "cli-plugins"), 0755) - composePlugin, _ := findExecutable("docker-compose", []string{"../../bin", "../../../bin"}) - err = CopyFile(composePlugin, filepath.Join(d, "cli-plugins", "docker-compose")) + composePluginFile := "docker-compose" + if runtime.GOOS == "windows" { + composePluginFile += ".exe" + } + composePlugin, _ := findExecutable(composePluginFile, []string{"../../bin", "../../../bin"}) + err = CopyFile(composePlugin, filepath.Join(d, "cli-plugins", composePluginFile)) if err != nil { panic(err) } From 1fce1623ee17f6c7069af98327f46501e903f411 Mon Sep 17 00:00:00 2001 From: Guillaume Tardif Date: Mon, 22 Mar 2021 14:12:57 +0100 Subject: [PATCH 5/9] Cross build & package compose-cli plugin Signed-off-by: Guillaume Tardif Signed-off-by: Nicolas De Loof --- Dockerfile | 4 ++-- builder.Makefile | 46 ++++++++++++++++++++++++++++++---------- local/compose/compose.go | 2 +- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/Dockerfile b/Dockerfile index a6ea9e02e..726992a4a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -73,7 +73,7 @@ RUN --mount=target=. \ GOARCH=${TARGETARCH} \ BUILD_TAGS=${BUILD_TAGS} \ GIT_TAG=${GIT_TAG} \ - make BINARY=/out/docker -f builder.Makefile cli + make BINARY=/out/docker COMPOSE_BINARY=/out/docker-compose -f builder.Makefile cli FROM base AS make-cross ARG BUILD_TAGS @@ -83,7 +83,7 @@ RUN --mount=target=. \ --mount=type=cache,target=/root/.cache/go-build \ BUILD_TAGS=${BUILD_TAGS} \ GIT_TAG=${GIT_TAG} \ - make BINARY=/out/docker -f builder.Makefile cross + make BINARY=/out/docker COMPOSE_BINARY=/out/docker-compose -f builder.Makefile cross FROM scratch AS protos COPY --from=make-protos /compose-cli/cli/server/protos . diff --git a/builder.Makefile b/builder.Makefile index ca8736a46..0f9398809 100644 --- a/builder.Makefile +++ b/builder.Makefile @@ -45,9 +45,22 @@ ifdef BUILD_TAGS LINT_TAGS=--build-tags $(BUILD_TAGS) endif -TAR_TRANSFORM:=--transform s/packaging/docker/ --transform s/bin/docker/ --transform s/docker-linux-amd64/docker/ --transform s/docker-darwin-amd64/docker/ --transform s/docker-linux-arm64/docker/ --transform s/docker-linux-armv6/docker/ --transform s/docker-linux-armv7/docker/ --transform s/docker-darwin-arm64/docker/ +TAR_TRANSFORM:=--transform s/packaging/docker/ --transform s/bin/docker/ \ + --transform s/docker-linux-amd64/docker/ --transform s/docker-linux-arm64/docker/ \ + --transform s/docker-linux-armv6/docker/ --transform s/docker-linux-armv7/docker/ \ + --transform s/docker-darwin-amd64/docker/ --transform s/docker-darwin-arm64/docker/ \ + --transform s/docker-compose-linux-amd64/docker-compose/ --transform s/docker-compose-linux-arm64/docker-compose/ \ + --transform s/docker-compose-linux-armv6/docker-compose/ --transform s/docker-compose-linux-armv7/docker-compose/ \ + --transform s/docker-compose-darwin-amd64/docker-compose/ --transform s/docker-compose-darwin-arm64/docker-compose/ + ifneq ($(findstring bsd,$(shell tar --version)),) - TAR_TRANSFORM=-s /packaging/docker/ -s /bin/docker/ -s /docker-linux-amd64/docker/ -s /docker-darwin-amd64/docker/ -s /docker-linux-arm64/docker/ -s /docker-linux-armv6/docker/ -s /docker-linux-armv7/docker/ -s /docker-darwin-arm64/docker/ + TAR_TRANSFORM=-s /packaging/docker/ -s /bin/docker/ \ + -s /docker-linux-amd64/docker/ -s /docker-linux-arm64/docker/ \ + -s /docker-linux-armv6/docker/ -s /docker-linux-armv7/docker/ \ + -s /docker-darwin-amd64/docker/ -s /docker-darwin-arm64/docker/ \ + -s /docker-compose-linux-amd64/docker-compose/ -s /docker-compose-linux-arm64/docker-compose/ \ + -s /docker-compose-linux-armv6/docker-compose/ -s /docker-compose-linux-armv7/docker-compose/ \ + -s /docker-compose-darwin-amd64/docker-compose/ -s /docker-compose-darwin-arm64/docker-compose/ endif all: cli @@ -62,10 +75,10 @@ cli: compose-plugin .PHONY: compose-plugin compose-plugin: - GOOS=${GOOS} GOARCH=${GOARCH} $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY_WITH_EXTENSION) ./compose_plugin + GOOS=${GOOS} GOARCH=${GOARCH} $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY_WITH_EXTENSION) . .PHONY: cross -cross: +cross: cross-compose-plugin GOOS=linux GOARCH=amd64 $(GO_BUILD) $(TAGS) -o $(BINARY)-linux-amd64 ./cli GOOS=linux GOARCH=arm64 $(GO_BUILD) $(TAGS) -o $(BINARY)-linux-arm64 ./cli GOOS=linux GOARM=6 GOARCH=arm $(GO_BUILD) $(TAGS) -o $(BINARY)-linux-armv6 ./cli @@ -74,6 +87,16 @@ cross: GOOS=darwin GOARCH=arm64 $(GO_BUILD) $(TAGS) -o $(BINARY)-darwin-arm64 ./cli GOOS=windows GOARCH=amd64 $(GO_BUILD) $(TAGS) -o $(BINARY)-windows-amd64.exe ./cli +.PHONY: cross-compose-plugin +cross-compose-plugin: + GOOS=linux GOARCH=amd64 $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-linux-amd64 . + GOOS=linux GOARCH=arm64 $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-linux-arm64 . + GOOS=linux GOARM=6 GOARCH=arm $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-linux-armv6 . + GOOS=linux GOARM=7 GOARCH=arm $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-linux-armv7 . + GOOS=darwin GOARCH=amd64 $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-darwin-amd64 . + GOOS=darwin GOARCH=arm64 $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-darwin-arm64 . + GOOS=windows GOARCH=amd64 $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-windows-amd64.exe . + .PHONY: test test: go test $(TAGS) -cover $(shell go list $(TAGS) ./... | grep -vE 'e2e') @@ -97,14 +120,15 @@ check-go-mod: .PHONY: package package: cross mkdir -p dist - tar -czf dist/docker-linux-amd64.tar.gz $(TAR_TRANSFORM) packaging/LICENSE $(BINARY)-linux-amd64 - tar -czf dist/docker-linux-arm64.tar.gz $(TAR_TRANSFORM) packaging/LICENSE $(BINARY)-linux-arm64 - tar -czf dist/docker-linux-armv6.tar.gz $(TAR_TRANSFORM) packaging/LICENSE $(BINARY)-linux-armv6 - tar -czf dist/docker-linux-armv7.tar.gz $(TAR_TRANSFORM) packaging/LICENSE $(BINARY)-linux-armv7 - tar -czf dist/docker-darwin-amd64.tar.gz $(TAR_TRANSFORM) packaging/LICENSE $(BINARY)-darwin-amd64 - tar -czf dist/docker-darwin-arm64.tar.gz $(TAR_TRANSFORM) packaging/LICENSE $(BINARY)-darwin-arm64 + tar -czf dist/docker-linux-amd64.tar.gz $(TAR_TRANSFORM) packaging/LICENSE $(BINARY)-linux-amd64 $(COMPOSE_BINARY)-linux-amd64 + tar -czf dist/docker-linux-arm64.tar.gz $(TAR_TRANSFORM) packaging/LICENSE $(BINARY)-linux-arm64 $(COMPOSE_BINARY)-linux-arm64 + tar -czf dist/docker-linux-armv6.tar.gz $(TAR_TRANSFORM) packaging/LICENSE $(BINARY)-linux-armv6 $(COMPOSE_BINARY)-linux-armv6 + tar -czf dist/docker-linux-armv7.tar.gz $(TAR_TRANSFORM) packaging/LICENSE $(BINARY)-linux-armv7 $(COMPOSE_BINARY)-linux-armv7 + tar -czf dist/docker-darwin-amd64.tar.gz $(TAR_TRANSFORM) packaging/LICENSE $(BINARY)-darwin-amd64 $(COMPOSE_BINARY)-darwin-amd64 + tar -czf dist/docker-darwin-arm64.tar.gz $(TAR_TRANSFORM) packaging/LICENSE $(BINARY)-darwin-arm64 $(COMPOSE_BINARY)-darwin-arm64 cp $(BINARY)-windows-amd64.exe $(WORK_DIR)/docker.exe - rm -f dist/docker-windows-amd64.zip && zip dist/docker-windows-amd64.zip -j packaging/LICENSE $(WORK_DIR)/docker.exe + cp $(COMPOSE_BINARY)-windows-amd64.exe $(WORK_DIR)/docker-compose.exe + rm -f dist/docker-windows-amd64.zip && zip dist/docker-windows-amd64.zip -j packaging/LICENSE $(WORK_DIR)/docker.exe $(WORK_DIR)/docker-compose.exe rm -r $(WORK_DIR) .PHONY: yamldocs diff --git a/local/compose/compose.go b/local/compose/compose.go index db60cbf5e..891b1d95c 100644 --- a/local/compose/compose.go +++ b/local/compose/compose.go @@ -20,13 +20,13 @@ import ( "context" "encoding/json" "fmt" - "github.com/docker/cli/cli/config/configfile" "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" moby "github.com/docker/docker/api/types" "github.com/docker/docker/client" "github.com/sanathkr/go-yaml" From d8aa00a766ad7c7c4e21a54953405dd37051b6c3 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Thu, 15 Apr 2021 12:43:18 +0200 Subject: [PATCH 6/9] wrap compose cobra command to set exitcode according to metrics status Signed-off-by: Nicolas De Loof --- cli/cmd/compose/build.go | 6 +++--- cli/cmd/compose/compose.go | 29 ++++++++++++++++++++++++++--- cli/cmd/compose/convert.go | 6 +++--- cli/cmd/compose/create.go | 7 ++++--- cli/cmd/compose/down.go | 8 +++++--- cli/cmd/compose/events.go | 6 +++--- cli/cmd/compose/exec.go | 6 +++--- cli/cmd/compose/images.go | 6 +++--- cli/cmd/compose/kill.go | 6 +++--- cli/cmd/compose/list.go | 6 +++--- cli/cmd/compose/logs.go | 6 +++--- cli/cmd/compose/pause.go | 12 ++++++------ cli/cmd/compose/port.go | 6 +++--- cli/cmd/compose/ps.go | 6 +++--- cli/cmd/compose/pull.go | 6 +++--- cli/cmd/compose/push.go | 6 +++--- cli/cmd/compose/remove.go | 6 +++--- cli/cmd/compose/restart.go | 6 +++--- cli/cmd/compose/run.go | 6 +++--- cli/cmd/compose/start.go | 6 +++--- cli/cmd/compose/stop.go | 6 ++++-- cli/cmd/compose/top.go | 6 +++--- cli/cmd/compose/up.go | 10 ++++++---- cli/main.go | 6 +++--- docs/yaml/main/generate.go | 2 +- main.go | 14 +++++++++++--- 26 files changed, 117 insertions(+), 79 deletions(-) diff --git a/cli/cmd/compose/build.go b/cli/cmd/compose/build.go index ecacacbd8..bc2bb5836 100644 --- a/cli/cmd/compose/build.go +++ b/cli/cmd/compose/build.go @@ -46,7 +46,7 @@ func buildCommand(p *projectOptions, backend compose.Service) *cobra.Command { cmd := &cobra.Command{ Use: "build [SERVICE...]", Short: "Build or rebuild services", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: Adapt(func(ctx context.Context, args []string) error { if opts.memory != "" { fmt.Println("WARNING --memory is ignored as not supported in buildkit.") } @@ -57,8 +57,8 @@ func buildCommand(p *projectOptions, backend compose.Service) *cobra.Command { } os.Stdout = devnull } - return runBuild(cmd.Context(), backend, opts, args) - }, + return runBuild(ctx, backend, opts, args) + }), } cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false, "Don't print anything to STDOUT") cmd.Flags().BoolVar(&opts.pull, "pull", false, "Always attempt to pull a newer version of the image.") diff --git a/cli/cmd/compose/compose.go b/cli/cmd/compose/compose.go index da105b9c4..6722527ac 100644 --- a/cli/cmd/compose/compose.go +++ b/cli/cmd/compose/compose.go @@ -17,12 +17,14 @@ package compose import ( + "context" "fmt" "os" "strings" "github.com/compose-spec/compose-go/cli" "github.com/compose-spec/compose-go/types" + dockercli "github.com/docker/cli/cli" "github.com/morikuni/aec" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -34,6 +36,24 @@ import ( "github.com/docker/compose-cli/cli/metrics" ) +//Command defines a compose CLI command as a func with args +type Command func(context.Context, []string) error + +//Adapt a Command func to cobra library +func Adapt(fn Command) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + err := fn(cmd.Context(), args) + var composeErr metrics.ComposeError + if errors.As(err, &composeErr) { + err = dockercli.StatusError{ + StatusCode: composeErr.GetMetricsFailureCategory().ExitCode, + Status: err.Error(), + } + } + return err + } +} + // Warning is a global warning to be displayed to user on command failure var Warning string @@ -105,8 +125,8 @@ func (o *projectOptions) toProjectOptions(po ...cli.ProjectOptionsFn) (*cli.Proj cli.WithName(o.ProjectName))...) } -// Command returns the compose command with its child commands -func Command(contextType string, backend compose.Service) *cobra.Command { +// RootCommand returns the compose command with its child commands +func RootCommand(contextType string, backend compose.Service) *cobra.Command { opts := projectOptions{} var ansi string var noAnsi bool @@ -120,7 +140,10 @@ func Command(contextType string, backend compose.Service) *cobra.Command { return cmd.Help() } _ = cmd.Help() - return fmt.Errorf("unknown docker command: %q", "compose "+args[0]) + return dockercli.StatusError{ + StatusCode: metrics.CommandSyntaxFailure.ExitCode, + Status: fmt.Sprintf("unknown docker command: %q", "compose "+args[0]), + } }, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { parent := cmd.Root() diff --git a/cli/cmd/compose/convert.go b/cli/cmd/compose/convert.go index 01b0fbe52..359bef47b 100644 --- a/cli/cmd/compose/convert.go +++ b/cli/cmd/compose/convert.go @@ -58,7 +58,7 @@ func convertCommand(p *projectOptions, backend compose.Service) *cobra.Command { Aliases: []string{"config"}, Use: "convert SERVICES", Short: "Converts the compose file to platform's canonical format", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: Adapt(func(ctx context.Context, args []string) error { if opts.quiet { devnull, err := os.Open(os.DevNull) if err != nil { @@ -79,8 +79,8 @@ func convertCommand(p *projectOptions, backend compose.Service) *cobra.Command { return runProfiles(opts, args) } - return runConvert(cmd.Context(), backend, opts, args) - }, + return runConvert(ctx, backend, opts, args) + }), } flags := cmd.Flags() flags.StringVar(&opts.Format, "format", "yaml", "Format the output. Values: [yaml | json]") diff --git a/cli/cmd/compose/create.go b/cli/cmd/compose/create.go index ef6080852..8d790ca23 100644 --- a/cli/cmd/compose/create.go +++ b/cli/cmd/compose/create.go @@ -17,6 +17,7 @@ package compose import ( + "context" "fmt" "github.com/spf13/cobra" @@ -37,14 +38,14 @@ func createCommand(p *projectOptions, backend compose.Service) *cobra.Command { cmd := &cobra.Command{ Use: "create [SERVICE...]", Short: "Creates containers for a service.", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: Adapt(func(ctx context.Context, args []string) error { if opts.Build && opts.noBuild { return fmt.Errorf("--build and --no-build are incompatible") } if opts.forceRecreate && opts.noRecreate { return fmt.Errorf("--force-recreate and --no-recreate are incompatible") } - return runCreateStart(cmd.Context(), backend, upOptions{ + return runCreateStart(ctx, backend, upOptions{ composeOptions: &composeOptions{ projectOptions: p, Build: opts.Build, @@ -54,7 +55,7 @@ func createCommand(p *projectOptions, backend compose.Service) *cobra.Command { forceRecreate: opts.forceRecreate, noRecreate: opts.noRecreate, }, args) - }, + }), } flags := cmd.Flags() flags.BoolVar(&opts.Build, "build", false, "Build images before starting containers.") diff --git a/cli/cmd/compose/down.go b/cli/cmd/compose/down.go index 07e377443..ba6dac25e 100644 --- a/cli/cmd/compose/down.go +++ b/cli/cmd/compose/down.go @@ -45,15 +45,17 @@ func downCommand(p *projectOptions, contextType string, backend compose.Service) downCmd := &cobra.Command{ Use: "down", Short: "Stop and remove containers, networks", - RunE: func(cmd *cobra.Command, args []string) error { + PreRun: func(cmd *cobra.Command, args []string) { opts.timeChanged = cmd.Flags().Changed("timeout") + }, + RunE: Adapt(func(ctx context.Context, args []string) error { if opts.images != "" { if opts.images != "all" && opts.images != "local" { return fmt.Errorf("invalid value for --rmi: %q", opts.images) } } - return runDown(cmd.Context(), backend, opts) - }, + return runDown(ctx, backend, opts) + }), } flags := downCmd.Flags() flags.BoolVar(&opts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file.") diff --git a/cli/cmd/compose/events.go b/cli/cmd/compose/events.go index da496d0dd..a66486876 100644 --- a/cli/cmd/compose/events.go +++ b/cli/cmd/compose/events.go @@ -40,9 +40,9 @@ func eventsCommand(p *projectOptions, backend compose.Service) *cobra.Command { cmd := &cobra.Command{ Use: "events [options] [--] [SERVICE...]", Short: "Receive real time events from containers.", - RunE: func(cmd *cobra.Command, args []string) error { - return runEvents(cmd.Context(), backend, opts, args) - }, + RunE: Adapt(func(ctx context.Context, args []string) error { + return runEvents(ctx, backend, opts, args) + }), } cmd.Flags().BoolVar(&opts.json, "json", false, "Output events as a stream of json objects") diff --git a/cli/cmd/compose/exec.go b/cli/cmd/compose/exec.go index 4592448e9..c2a5b908d 100644 --- a/cli/cmd/compose/exec.go +++ b/cli/cmd/compose/exec.go @@ -52,13 +52,13 @@ func execCommand(p *projectOptions, backend compose.Service) *cobra.Command { Use: "exec [options] [-e KEY=VAL...] [--] SERVICE COMMAND [ARGS...]", Short: "Execute a command in a running container.", Args: cobra.MinimumNArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: Adapt(func(ctx context.Context, args []string) error { if len(args) > 1 { opts.command = args[1:] } opts.service = args[0] - return runExec(cmd.Context(), backend, opts) - }, + return runExec(ctx, backend, opts) + }), } runCmd.Flags().BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: Run command in the background.") diff --git a/cli/cmd/compose/images.go b/cli/cmd/compose/images.go index 072a6a083..39520cbe0 100644 --- a/cli/cmd/compose/images.go +++ b/cli/cmd/compose/images.go @@ -46,9 +46,9 @@ func imagesCommand(p *projectOptions, backend compose.Service) *cobra.Command { imgCmd := &cobra.Command{ Use: "images [SERVICE...]", Short: "List images used by the created containers", - RunE: func(cmd *cobra.Command, args []string) error { - return runImages(cmd.Context(), backend, opts, args) - }, + RunE: Adapt(func(ctx context.Context, args []string) error { + return runImages(ctx, backend, opts, args) + }), } imgCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs") return imgCmd diff --git a/cli/cmd/compose/kill.go b/cli/cmd/compose/kill.go index 97cb9c9d7..986dce0fd 100644 --- a/cli/cmd/compose/kill.go +++ b/cli/cmd/compose/kill.go @@ -36,9 +36,9 @@ func killCommand(p *projectOptions, backend compose.Service) *cobra.Command { cmd := &cobra.Command{ Use: "kill [options] [SERVICE...]", Short: "Force stop service containers.", - RunE: func(cmd *cobra.Command, args []string) error { - return runKill(cmd.Context(), backend, opts, args) - }, + RunE: Adapt(func(ctx context.Context, args []string) error { + return runKill(ctx, backend, opts, args) + }), } flags := cmd.Flags() diff --git a/cli/cmd/compose/list.go b/cli/cmd/compose/list.go index b75887d00..bcf8d3641 100644 --- a/cli/cmd/compose/list.go +++ b/cli/cmd/compose/list.go @@ -43,9 +43,9 @@ func listCommand(contextType string, backend compose.Service) *cobra.Command { lsCmd := &cobra.Command{ Use: "ls", Short: "List running compose projects", - RunE: func(cmd *cobra.Command, args []string) error { - return runList(cmd.Context(), backend, opts) - }, + RunE: Adapt(func(ctx context.Context, args []string) error { + return runList(ctx, backend, opts) + }), } lsCmd.Flags().StringVar(&opts.Format, "format", "pretty", "Format the output. Values: [pretty | json].") lsCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs.") diff --git a/cli/cmd/compose/logs.go b/cli/cmd/compose/logs.go index e2249721f..f89b13b4e 100644 --- a/cli/cmd/compose/logs.go +++ b/cli/cmd/compose/logs.go @@ -44,9 +44,9 @@ func logsCommand(p *projectOptions, contextType string, backend compose.Service) logsCmd := &cobra.Command{ Use: "logs [service...]", Short: "View output from containers", - RunE: func(cmd *cobra.Command, args []string) error { - return runLogs(cmd.Context(), backend, opts, args) - }, + RunE: Adapt(func(ctx context.Context, args []string) error { + return runLogs(ctx, backend, opts, args) + }), } flags := logsCmd.Flags() flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output.") diff --git a/cli/cmd/compose/pause.go b/cli/cmd/compose/pause.go index 0265b4ea3..13287f9a5 100644 --- a/cli/cmd/compose/pause.go +++ b/cli/cmd/compose/pause.go @@ -36,9 +36,9 @@ func pauseCommand(p *projectOptions, backend compose.Service) *cobra.Command { cmd := &cobra.Command{ Use: "pause [SERVICE...]", Short: "pause services", - RunE: func(cmd *cobra.Command, args []string) error { - return runPause(cmd.Context(), backend, opts, args) - }, + RunE: Adapt(func(ctx context.Context, args []string) error { + return runPause(ctx, backend, opts, args) + }), } return cmd } @@ -68,9 +68,9 @@ func unpauseCommand(p *projectOptions, backend compose.Service) *cobra.Command { cmd := &cobra.Command{ Use: "unpause [SERVICE...]", Short: "unpause services", - RunE: func(cmd *cobra.Command, args []string) error { - return runUnPause(cmd.Context(), backend, opts, args) - }, + RunE: Adapt(func(ctx context.Context, args []string) error { + return runUnPause(ctx, backend, opts, args) + }), } return cmd } diff --git a/cli/cmd/compose/port.go b/cli/cmd/compose/port.go index ecb8c8e58..1d00b9c15 100644 --- a/cli/cmd/compose/port.go +++ b/cli/cmd/compose/port.go @@ -40,13 +40,13 @@ func portCommand(p *projectOptions, backend compose.Service) *cobra.Command { Use: "port [options] [--] SERVICE PRIVATE_PORT", Short: "Print the public port for a port binding.", Args: cobra.MinimumNArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: Adapt(func(ctx context.Context, args []string) error { port, err := strconv.Atoi(args[1]) if err != nil { return err } - return runPort(cmd.Context(), backend, opts, args[0], port) - }, + return runPort(ctx, backend, opts, args[0], port) + }), } cmd.Flags().StringVar(&opts.protocol, "protocol", "tcp", "tcp or udp") cmd.Flags().IntVar(&opts.index, "index", 1, "index of the container if service has multiple replicas") diff --git a/cli/cmd/compose/ps.go b/cli/cmd/compose/ps.go index 985b16c3e..20e99c32a 100644 --- a/cli/cmd/compose/ps.go +++ b/cli/cmd/compose/ps.go @@ -46,9 +46,9 @@ func psCommand(p *projectOptions, backend compose.Service) *cobra.Command { psCmd := &cobra.Command{ Use: "ps", Short: "List containers", - RunE: func(cmd *cobra.Command, args []string) error { - return runPs(cmd.Context(), backend, opts) - }, + RunE: Adapt(func(ctx context.Context, args []string) error { + return runPs(ctx, backend, opts) + }), } psCmd.Flags().StringVar(&opts.Format, "format", "pretty", "Format the output. Values: [pretty | json].") psCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs") diff --git a/cli/cmd/compose/pull.go b/cli/cmd/compose/pull.go index 9fb4df138..8917da744 100644 --- a/cli/cmd/compose/pull.go +++ b/cli/cmd/compose/pull.go @@ -46,12 +46,12 @@ func pullCommand(p *projectOptions, backend compose.Service) *cobra.Command { cmd := &cobra.Command{ Use: "pull [SERVICE...]", Short: "Pull service images", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: Adapt(func(ctx context.Context, args []string) error { if opts.noParallel { fmt.Fprint(os.Stderr, aec.Apply("option '--no-parallel' is DEPRECATED and will be ignored.\n", aec.RedF)) } - return runPull(cmd.Context(), backend, opts, args) - }, + return runPull(ctx, backend, opts, args) + }), } flags := cmd.Flags() flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Pull without printing progress information") diff --git a/cli/cmd/compose/push.go b/cli/cmd/compose/push.go index 3f1fb6e2f..91531793c 100644 --- a/cli/cmd/compose/push.go +++ b/cli/cmd/compose/push.go @@ -39,9 +39,9 @@ func pushCommand(p *projectOptions, backend compose.Service) *cobra.Command { pushCmd := &cobra.Command{ Use: "push [SERVICE...]", Short: "Push service images", - RunE: func(cmd *cobra.Command, args []string) error { - return runPush(cmd.Context(), backend, opts, args) - }, + RunE: Adapt(func(ctx context.Context, args []string) error { + return runPush(ctx, backend, opts, args) + }), } pushCmd.Flags().BoolVar(&opts.Ignorefailures, "ignore-push-failures", false, "Push what it can and ignores images with push failures") diff --git a/cli/cmd/compose/remove.go b/cli/cmd/compose/remove.go index 786e0feed..2757f9fcd 100644 --- a/cli/cmd/compose/remove.go +++ b/cli/cmd/compose/remove.go @@ -48,9 +48,9 @@ By default, anonymous volumes attached to containers will not be removed. You can override this with -v. To list all volumes, use "docker volume ls". Any data which is not in a volume will be lost.`, - RunE: func(cmd *cobra.Command, args []string) error { - return runRemove(cmd.Context(), backend, opts, args) - }, + RunE: Adapt(func(ctx context.Context, args []string) error { + return runRemove(ctx, backend, opts, args) + }), } f := cmd.Flags() f.BoolVarP(&opts.force, "force", "f", false, "Don't ask to confirm removal") diff --git a/cli/cmd/compose/restart.go b/cli/cmd/compose/restart.go index b6dd7dc0e..f1e031afa 100644 --- a/cli/cmd/compose/restart.go +++ b/cli/cmd/compose/restart.go @@ -38,9 +38,9 @@ func restartCommand(p *projectOptions, backend compose.Service) *cobra.Command { restartCmd := &cobra.Command{ Use: "restart", Short: "Restart containers", - RunE: func(cmd *cobra.Command, args []string) error { - return runRestart(cmd.Context(), backend, opts, args) - }, + RunE: Adapt(func(ctx context.Context, args []string) error { + return runRestart(ctx, backend, opts, args) + }), } flags := restartCmd.Flags() flags.IntVarP(&opts.timeout, "timeout", "t", 10, "Specify a shutdown timeout in seconds") diff --git a/cli/cmd/compose/run.go b/cli/cmd/compose/run.go index 79028e6e6..ca2582cc3 100644 --- a/cli/cmd/compose/run.go +++ b/cli/cmd/compose/run.go @@ -109,7 +109,7 @@ func runCommand(p *projectOptions, backend compose.Service) *cobra.Command { Use: "run [options] [-v VOLUME...] [-p PORT...] [-e KEY=VAL...] [-l KEY=VALUE...] SERVICE [COMMAND] [ARGS...]", Short: "Run a one-off command on a service.", Args: cobra.MinimumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: Adapt(func(ctx context.Context, args []string) error { if len(args) > 1 { opts.Command = args[1:] } @@ -117,8 +117,8 @@ func runCommand(p *projectOptions, backend compose.Service) *cobra.Command { if len(opts.publish) > 0 && opts.servicePorts { return fmt.Errorf("--service-ports and --publish are incompatible") } - return runRun(cmd.Context(), backend, opts) - }, + return runRun(ctx, backend, opts) + }), } flags := cmd.Flags() flags.BoolVarP(&opts.Detach, "detach", "d", false, "Run container in background and print container ID") diff --git a/cli/cmd/compose/start.go b/cli/cmd/compose/start.go index f6632a7ba..3e8b0b4e8 100644 --- a/cli/cmd/compose/start.go +++ b/cli/cmd/compose/start.go @@ -36,9 +36,9 @@ func startCommand(p *projectOptions, backend compose.Service) *cobra.Command { startCmd := &cobra.Command{ Use: "start [SERVICE...]", Short: "Start services", - RunE: func(cmd *cobra.Command, args []string) error { - return runStart(cmd.Context(), backend, opts, args) - }, + RunE: Adapt(func(ctx context.Context, args []string) error { + return runStart(ctx, backend, opts, args) + }), } return startCmd } diff --git a/cli/cmd/compose/stop.go b/cli/cmd/compose/stop.go index 494ac9079..73e811562 100644 --- a/cli/cmd/compose/stop.go +++ b/cli/cmd/compose/stop.go @@ -39,10 +39,12 @@ func stopCommand(p *projectOptions, backend compose.Service) *cobra.Command { cmd := &cobra.Command{ Use: "stop [SERVICE...]", Short: "Stop services", - RunE: func(cmd *cobra.Command, args []string) error { + PreRun: func(cmd *cobra.Command, args []string) { opts.timeChanged = cmd.Flags().Changed("timeout") - return runStop(cmd.Context(), backend, opts, args) }, + RunE: Adapt(func(ctx context.Context, args []string) error { + return runStop(ctx, backend, opts, args) + }), } flags := cmd.Flags() flags.IntVarP(&opts.timeout, "timeout", "t", 10, "Specify a shutdown timeout in seconds") diff --git a/cli/cmd/compose/top.go b/cli/cmd/compose/top.go index cd5a12bc0..1d6518ea6 100644 --- a/cli/cmd/compose/top.go +++ b/cli/cmd/compose/top.go @@ -41,9 +41,9 @@ func topCommand(p *projectOptions, backend compose.Service) *cobra.Command { topCmd := &cobra.Command{ Use: "top", Short: "Display the running processes", - RunE: func(cmd *cobra.Command, args []string) error { - return runTop(cmd.Context(), backend, opts, args) - }, + RunE: Adapt(func(ctx context.Context, args []string) error { + return runTop(ctx, backend, opts, args) + }), } return topCmd } diff --git a/cli/cmd/compose/up.go b/cli/cmd/compose/up.go index b6df226d9..6c926fdbf 100644 --- a/cli/cmd/compose/up.go +++ b/cli/cmd/compose/up.go @@ -148,8 +148,10 @@ func upCommand(p *projectOptions, contextType string, backend compose.Service) * upCmd := &cobra.Command{ Use: "up [SERVICE...]", Short: "Create and start containers", - RunE: func(cmd *cobra.Command, args []string) error { + PreRun: func(cmd *cobra.Command, args []string) { opts.timeChanged = cmd.Flags().Changed("timeout") + }, + RunE: Adapt(func(ctx context.Context, args []string) error { switch contextType { case store.LocalContextType, store.DefaultContextType, store.EcsLocalSimulationContextType: if opts.exitCodeFrom != "" { @@ -167,11 +169,11 @@ func upCommand(p *projectOptions, contextType string, backend compose.Service) * if opts.recreateDeps && opts.noRecreate { return fmt.Errorf("--always-recreate-deps and --no-recreate are incompatible") } - return runCreateStart(cmd.Context(), backend, opts, args) + return runCreateStart(ctx, backend, opts, args) default: - return runUp(cmd.Context(), backend, opts, args) + return runUp(ctx, backend, opts, args) } - }, + }), } flags := upCmd.Flags() flags.StringArrayVarP(&opts.Environment, "environment", "e", []string{}, "Environment variables") diff --git a/cli/main.go b/cli/main.go index ae6ab7c11..ffcdc64f7 100644 --- a/cli/main.go +++ b/cli/main.go @@ -218,7 +218,7 @@ func main() { root.AddCommand( run.Command(ctype), - compose.Command(ctype, service.ComposeService()), + compose.RootCommand(ctype, service.ComposeService()), volume.Command(ctype), ) @@ -294,7 +294,7 @@ func exit(ctx string, err error, ctype string) { if errors.Is(err, errdefs.ErrNotImplemented) { name := metrics.GetCommand(os.Args[1:]) - fmt.Fprintf(os.Stderr, "Command %q not available in current context (%s)\n", name, ctx) + fmt.Fprintf(os.Stderr, "RootCommand %q not available in current context (%s)\n", name, ctx) os.Exit(1) } @@ -314,7 +314,7 @@ func checkIfUnknownCommandExistInDefaultContext(err error, currentContext string dockerCommand := string(submatch[1]) if mobycli.IsDefaultContextCommand(dockerCommand) { - fmt.Fprintf(os.Stderr, "Command %q not available in current context (%s), you can use the \"default\" context to run this command\n", dockerCommand, currentContext) + fmt.Fprintf(os.Stderr, "RootCommand %q not available in current context (%s), you can use the \"default\" context to run this command\n", dockerCommand, currentContext) metrics.Track(contextType, os.Args[1:], metrics.FailureStatus) os.Exit(1) } diff --git a/docs/yaml/main/generate.go b/docs/yaml/main/generate.go index 4ca4a16bb..19353dc75 100644 --- a/docs/yaml/main/generate.go +++ b/docs/yaml/main/generate.go @@ -35,7 +35,7 @@ const descriptionSourcePath = "docs/reference/" func generateCliYaml(opts *options) error { cmd := &cobra.Command{Use: "docker"} - cmd.AddCommand(compose.Command("local", nil)) + cmd.AddCommand(compose.RootCommand("local", nil)) disableFlagsInUseLine(cmd) source := filepath.Join(opts.source, descriptionSourcePath) if err := loadLongDescription(cmd, source); err != nil { diff --git a/main.go b/main.go index fa801fa65..1686408eb 100644 --- a/main.go +++ b/main.go @@ -19,14 +19,16 @@ package main import ( "strings" - "github.com/spf13/cobra" - + dockercli "github.com/docker/cli/cli" "github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli-plugins/plugin" "github.com/docker/cli/cli/command" + "github.com/spf13/cobra" + api "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/context/store" "github.com/docker/compose-cli/cli/cmd/compose" + "github.com/docker/compose-cli/cli/metrics" "github.com/docker/compose-cli/internal" impl "github.com/docker/compose-cli/local/compose" ) @@ -36,7 +38,7 @@ func main() { lazyInit := api.ServiceDelegator{ Delegate: api.NoImpl{}, } - cmd := compose.Command(store.DefaultContextType, &lazyInit) + cmd := compose.RootCommand(store.DefaultContextType, &lazyInit) originalPreRun := cmd.PersistentPreRunE cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { if err := plugin.PersistentPreRunE(cmd, args); err != nil { @@ -48,6 +50,12 @@ func main() { } return nil } + cmd.SetFlagErrorFunc(func(c *cobra.Command, err error) error { + return dockercli.StatusError{ + StatusCode: metrics.CommandSyntaxFailure.ExitCode, + Status: err.Error(), + } + }) return cmd }, manager.Metadata{ From db38d1244cbf91c098a1bd062efdd34b3b081cd4 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Thu, 15 Apr 2021 14:19:59 +0200 Subject: [PATCH 7/9] Retrieve compose failure category by exit code Signed-off-by: Nicolas De Loof --- cli/cmd/compose/remove.go | 2 +- cli/cmd/compose/stop.go | 2 +- cli/main.go | 4 ++-- cli/metrics/definitions.go | 23 +++++++++++++++++++++++ cli/mobycli/exec.go | 8 ++------ 5 files changed, 29 insertions(+), 10 deletions(-) diff --git a/cli/cmd/compose/remove.go b/cli/cmd/compose/remove.go index 2757f9fcd..eda63a4fd 100644 --- a/cli/cmd/compose/remove.go +++ b/cli/cmd/compose/remove.go @@ -78,7 +78,7 @@ func runRemove(ctx context.Context, backend compose.Service, opts removeOptions, } reosurces, err := backend.Remove(ctx, project, compose.RemoveOptions{ - DryRun: true, + DryRun: true, Services: services, }) if err != nil { diff --git a/cli/cmd/compose/stop.go b/cli/cmd/compose/stop.go index 73e811562..2a12a55ba 100644 --- a/cli/cmd/compose/stop.go +++ b/cli/cmd/compose/stop.go @@ -65,7 +65,7 @@ func runStop(ctx context.Context, backend compose.Service, opts stopOptions, ser } _, err = progress.Run(ctx, func(ctx context.Context) (string, error) { return "", backend.Stop(ctx, project, compose.StopOptions{ - Timeout: timeout, + Timeout: timeout, Services: services, }) }) diff --git a/cli/main.go b/cli/main.go index ffcdc64f7..6b49bba7c 100644 --- a/cli/main.go +++ b/cli/main.go @@ -294,7 +294,7 @@ func exit(ctx string, err error, ctype string) { if errors.Is(err, errdefs.ErrNotImplemented) { name := metrics.GetCommand(os.Args[1:]) - fmt.Fprintf(os.Stderr, "RootCommand %q not available in current context (%s)\n", name, ctx) + fmt.Fprintf(os.Stderr, "Command %q not available in current context (%s)\n", name, ctx) os.Exit(1) } @@ -314,7 +314,7 @@ func checkIfUnknownCommandExistInDefaultContext(err error, currentContext string dockerCommand := string(submatch[1]) if mobycli.IsDefaultContextCommand(dockerCommand) { - fmt.Fprintf(os.Stderr, "RootCommand %q not available in current context (%s), you can use the \"default\" context to run this command\n", dockerCommand, currentContext) + fmt.Fprintf(os.Stderr, "Command %q not available in current context (%s), you can use the \"default\" context to run this command\n", dockerCommand, currentContext) metrics.Track(contextType, os.Args[1:], metrics.FailureStatus) os.Exit(1) } diff --git a/cli/metrics/definitions.go b/cli/metrics/definitions.go index 707af1af0..228e42aea 100644 --- a/cli/metrics/definitions.go +++ b/cli/metrics/definitions.go @@ -55,3 +55,26 @@ var ( // PullFailure failure while pulling image PullFailure = FailureCategory{MetricsStatus: PullFailureStatus, ExitCode: 18} ) + +//ByExitCode retrieve FailureCategory based on command exit code +func ByExitCode(exitCode int) FailureCategory { + switch exitCode { + case 0: + return FailureCategory{MetricsStatus: SuccessStatus, ExitCode: 0} + case 14: + return FileNotFoundFailure + case 15: + return ComposeParseFailure + case 16: + return CommandSyntaxFailure + case 17: + return BuildFailure + case 18: + return PullFailure + case 130: + return FailureCategory{MetricsStatus: CanceledStatus, ExitCode: exitCode} + default: + return FailureCategory{MetricsStatus: FailureStatus, ExitCode: exitCode} + } + +} diff --git a/cli/mobycli/exec.go b/cli/mobycli/exec.go index fb0c7750f..e4fe42175 100644 --- a/cli/mobycli/exec.go +++ b/cli/mobycli/exec.go @@ -68,12 +68,8 @@ func Exec(root *cobra.Command) { if err != nil { if exiterr, ok := err.(*exec.ExitError); ok { exitCode := exiterr.ExitCode() - if exitCode == 130 { - metrics.Track(store.DefaultContextType, os.Args[1:], metrics.CanceledStatus) - } else { - metrics.Track(store.DefaultContextType, os.Args[1:], metrics.FailureStatus) - } - os.Exit(exiterr.ExitCode()) + metrics.Track(store.DefaultContextType, os.Args[1:], metrics.ByExitCode(exitCode).MetricsStatus) + os.Exit(exitCode) } metrics.Track(store.DefaultContextType, os.Args[1:], metrics.FailureStatus) fmt.Fprintln(os.Stderr, err) From 0785114b90132058dbe4580b3c72f17ac3ee3929 Mon Sep 17 00:00:00 2001 From: Guillaume Tardif Date: Thu, 22 Apr 2021 22:03:52 +0200 Subject: [PATCH 8/9] =?UTF-8?q?Also=20ensure=20we=20don=E2=80=99t=20need?= =?UTF-8?q?=20compose=20cloud=20integration=20to=20run=20compose=20CLI=20p?= =?UTF-8?q?lugin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Guillaume Tardif --- local/e2e/compose/compose_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/local/e2e/compose/compose_test.go b/local/e2e/compose/compose_test.go index 732b743f1..6af94b483 100644 --- a/local/e2e/compose/compose_test.go +++ b/local/e2e/compose/compose_test.go @@ -141,6 +141,17 @@ func TestComposeUsingCliPlugin(t *testing.T) { res.Assert(t, icmd.Expected{Err: "'compose' is not a docker command", ExitCode: 1}) } +func TestComposeCliPluginWithoutCloudIntegration(t *testing.T) { + c := NewParallelE2eCLI(t, binDir) + + err := os.Remove(filepath.Join(binDir, "docker")) + assert.NilError(t, err) + err = os.Rename(filepath.Join(binDir, "com.docker.cli"), filepath.Join(binDir, "docker")) + assert.NilError(t, err) + res := c.RunDockerOrExitError("compose", "ls") + res.Assert(t, icmd.Expected{Out: "NAME STATUS", ExitCode: 0}) +} + func TestComposePull(t *testing.T) { c := NewParallelE2eCLI(t, binDir) From de3fa40bae802116e69848e009ba5119e240756c Mon Sep 17 00:00:00 2001 From: Guillaume Tardif Date: Thu, 22 Apr 2021 22:05:01 +0200 Subject: [PATCH 9/9] =?UTF-8?q?Handle=20Ctrl+C=20for=20compose=20CLI=20plu?= =?UTF-8?q?gin.=20Could=20do=20something=20nicer=20passing=20the=20context?= =?UTF-8?q?=20to=20the=20compose=20command,=20rather=20than=20intercepting?= =?UTF-8?q?=20it=20and=20checking=20if=20it=E2=80=99s=20=E2=80=9C.WithCanc?= =?UTF-8?q?el=E2=80=9D=20or=20not...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Guillaume Tardif --- cli/cmd/compose/compose.go | 23 +++++- .../build-infinite/docker-compose.yml | 3 + .../build-infinite/service1/Dockerfile | 17 +++++ local/e2e/compose/metrics_test.go | 70 +++++++++++++++++++ utils/e2e/framework.go | 12 ++++ 5 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 local/e2e/compose/fixtures/build-infinite/docker-compose.yml create mode 100644 local/e2e/compose/fixtures/build-infinite/service1/Dockerfile diff --git a/cli/cmd/compose/compose.go b/cli/cmd/compose/compose.go index 6722527ac..9a03f4d20 100644 --- a/cli/cmd/compose/compose.go +++ b/cli/cmd/compose/compose.go @@ -20,7 +20,9 @@ import ( "context" "fmt" "os" + "os/signal" "strings" + "syscall" "github.com/compose-spec/compose-go/cli" "github.com/compose-spec/compose-go/types" @@ -32,6 +34,7 @@ import ( "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/context/store" + "github.com/docker/compose-cli/api/errdefs" "github.com/docker/compose-cli/cli/formatter" "github.com/docker/compose-cli/cli/metrics" ) @@ -42,8 +45,26 @@ type Command func(context.Context, []string) error //Adapt a Command func to cobra library func Adapt(fn Command) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { - err := fn(cmd.Context(), args) + ctx := cmd.Context() + contextString := fmt.Sprintf("%s", ctx) + if !strings.HasSuffix(contextString, ".WithCancel") { // need to handle cancel + cancellableCtx, cancel := context.WithCancel(cmd.Context()) + ctx = cancellableCtx + s := make(chan os.Signal, 1) + signal.Notify(s, syscall.SIGTERM, syscall.SIGINT) + go func() { + <-s + cancel() + }() + } + err := fn(ctx, args) var composeErr metrics.ComposeError + if errdefs.IsErrCanceled(err) || errors.Is(ctx.Err(), context.Canceled) { + err = dockercli.StatusError{ + StatusCode: 130, + Status: metrics.CanceledStatus, + } + } if errors.As(err, &composeErr) { err = dockercli.StatusError{ StatusCode: composeErr.GetMetricsFailureCategory().ExitCode, diff --git a/local/e2e/compose/fixtures/build-infinite/docker-compose.yml b/local/e2e/compose/fixtures/build-infinite/docker-compose.yml new file mode 100644 index 000000000..cdc1e1693 --- /dev/null +++ b/local/e2e/compose/fixtures/build-infinite/docker-compose.yml @@ -0,0 +1,3 @@ +services: + service1: + build: service1 \ No newline at end of file diff --git a/local/e2e/compose/fixtures/build-infinite/service1/Dockerfile b/local/e2e/compose/fixtures/build-infinite/service1/Dockerfile new file mode 100644 index 000000000..3fd64e7b6 --- /dev/null +++ b/local/e2e/compose/fixtures/build-infinite/service1/Dockerfile @@ -0,0 +1,17 @@ +# 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. + +FROM busybox + +RUN sleep infinity \ No newline at end of file diff --git a/local/e2e/compose/metrics_test.go b/local/e2e/compose/metrics_test.go index 1600d6ea3..a760bb1f6 100644 --- a/local/e2e/compose/metrics_test.go +++ b/local/e2e/compose/metrics_test.go @@ -17,7 +17,11 @@ package e2e import ( + "bytes" "fmt" + "os/exec" + "strings" + "syscall" "testing" "time" @@ -84,3 +88,69 @@ func TestComposeMetrics(t *testing.T) { }, usage) }) } + +func TestComposeCancel(t *testing.T) { + c := NewParallelE2eCLI(t, binDir) + s := NewMetricsServer(c.MetricsSocket()) + s.Start() + defer s.Stop() + + started := false + + for i := 0; i < 30; i++ { + c.RunDockerCmd("help", "ps") + if len(s.GetUsage()) > 0 { + started = true + fmt.Printf(" [%s] Server up in %d ms\n", t.Name(), i*100) + break + } + time.Sleep(100 * time.Millisecond) + } + assert.Assert(t, started, "Metrics mock server not available after 3 secs") + + t.Run("metrics on cancel Compose build", func(t *testing.T) { + s.ResetUsage() + + c.RunDockerCmd("compose", "ls") + buildProjectPath := "../compose/fixtures/build-infinite/docker-compose.yml" + + // require a separate groupID from the process running tests, in order to simulate ctrl+C from a terminal. + // sending kill signal + cmd, stdout, stderr, err := StartWithNewGroupID(c.NewDockerCmd("compose", "-f", buildProjectPath, "build", "--progress", "plain")) + assert.NilError(t, err) + + c.WaitForCondition(func() (bool, string) { + out := stdout.String() + errors := stderr.String() + return strings.Contains(out, "RUN sleep infinity"), fmt.Sprintf("'RUN sleep infinity' not found in : \n%s\nStderr: \n%s\n", out, errors) + }, 30*time.Second, 1*time.Second) + + err = syscall.Kill(-cmd.Process.Pid, syscall.SIGINT) // simulate Ctrl-C : send signal to processGroup, children will have same groupId by default + + assert.NilError(t, err) + c.WaitForCondition(func() (bool, string) { + out := stdout.String() + errors := stderr.String() + return strings.Contains(out, "CANCELED"), fmt.Sprintf("'CANCELED' not found in : \n%s\nStderr: \n%s\n", out, errors) + }, 10*time.Second, 1*time.Second) + + usage := s.GetUsage() + assert.DeepEqual(t, []string{ + `{"command":"compose ls","context":"moby","source":"cli","status":"success"}`, + `{"command":"compose build","context":"moby","source":"cli","status":"canceled"}`, + }, usage) + }) +} + +func StartWithNewGroupID(command icmd.Cmd) (*exec.Cmd, *bytes.Buffer, *bytes.Buffer, error) { + cmd := exec.Command(command.Command[0], command.Command[1:]...) + cmd.Env = command.Env + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Start() + return cmd, &stdout, &stderr, err +} diff --git a/utils/e2e/framework.go b/utils/e2e/framework.go index a194f5049..f6fca2d24 100644 --- a/utils/e2e/framework.go +++ b/utils/e2e/framework.go @@ -252,6 +252,18 @@ func (c *E2eCLI) WaitForCmdResult(command icmd.Cmd, predicate func(*icmd.Result) poll.WaitOn(c.test, checkStopped, poll.WithDelay(delay), poll.WithTimeout(timeout)) } +// WaitForCondition wait for predicate to execute to true +func (c *E2eCLI) WaitForCondition(predicate func() (bool, string), timeout time.Duration, delay time.Duration) { + checkStopped := func(logt poll.LogT) poll.Result { + pass, description := predicate() + if !pass { + return poll.Continue("Condition not met: %q", description) + } + return poll.Success() + } + poll.WaitOn(c.test, checkStopped, poll.WithDelay(delay), poll.WithTimeout(timeout)) +} + // PathEnvVar returns path (os sensitive) for running test func (c *E2eCLI) PathEnvVar() string { path := c.BinDir + ":" + os.Getenv("PATH")