wrap compose cobra command to set exitcode according to metrics status

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2021-04-15 12:43:18 +02:00
parent 1fce1623ee
commit d8aa00a766
26 changed files with 117 additions and 79 deletions

View File

@ -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.")

View File

@ -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()

View File

@ -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]")

View File

@ -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.")

View File

@ -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.")

View File

@ -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")

View File

@ -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.")

View File

@ -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

View File

@ -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()

View File

@ -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.")

View File

@ -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.")

View File

@ -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
}

View File

@ -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")

View File

@ -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")

View File

@ -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")

View File

@ -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")

View File

@ -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")

View File

@ -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")

View File

@ -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")

View File

@ -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
}

View File

@ -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")

View File

@ -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
}

View File

@ -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")

View File

@ -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)
}

View File

@ -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 {

14
main.go
View File

@ -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{