diff --git a/aci/containers.go b/aci/containers.go index 570f6afd7..93fb03eb9 100644 --- a/aci/containers.go +++ b/aci/containers.go @@ -53,7 +53,7 @@ func (cs *aciContainerService) List(ctx context.Context, all bool) ([]containers if err != nil { return nil, err } - var res []containers.Container + res := []containers.Container{} for _, group := range containerGroups { if group.Containers == nil || len(*group.Containers) == 0 { return nil, fmt.Errorf("no containers found in ACI container group %s", *group.Name) diff --git a/cli/cmd/compose/compose.go b/cli/cmd/compose/compose.go index 8e8211208..30248d25d 100644 --- a/cli/cmd/compose/compose.go +++ b/cli/cmd/compose/compose.go @@ -33,6 +33,7 @@ type composeOptions struct { WorkingDir string ConfigPaths []string Environment []string + Format string } func (o *composeOptions) toProjectName() (string, error) { diff --git a/cli/cmd/compose/list.go b/cli/cmd/compose/list.go index d44e05bb5..806ee5356 100644 --- a/cli/cmd/compose/list.go +++ b/cli/cmd/compose/list.go @@ -23,8 +23,11 @@ import ( "os" "github.com/spf13/cobra" + "github.com/spf13/pflag" "github.com/docker/compose-cli/api/client" + "github.com/docker/compose-cli/api/compose" + "github.com/docker/compose-cli/formatter" ) func listCommand() *cobra.Command { @@ -35,10 +38,15 @@ func listCommand() *cobra.Command { return runList(cmd.Context(), opts) }, } - lsCmd.Flags().StringVarP(&opts.Name, "project-name", "p", "", "Project name") + addComposeCommonFlags(lsCmd.Flags(), &opts) return lsCmd } +func addComposeCommonFlags(f *pflag.FlagSet, opts *composeOptions) { + f.StringVarP(&opts.Name, "project-name", "p", "", "Project name") + f.StringVar(&opts.Format, "format", "", "Format the output. Values: [pretty | json]. (Default: pretty)") +} + func runList(ctx context.Context, opts composeOptions) error { c, err := client.New(ctx) if err != nil { @@ -49,10 +57,26 @@ func runList(ctx context.Context, opts composeOptions) error { return err } - err = printSection(os.Stdout, func(w io.Writer) { - for _, stack := range stackList { - fmt.Fprintf(w, "%s\t%s\n", stack.Name, stack.Status) + view := viewFromStackList(stackList) + return formatter.Print(view, opts.Format, os.Stdout, func(w io.Writer) { + for _, stack := range view { + _, _ = fmt.Fprintf(w, "%s\t%s\n", stack.Name, stack.Status) } }, "NAME", "STATUS") - return err +} + +type stackView struct { + Name string + Status string +} + +func viewFromStackList(stackList []compose.Stack) []stackView { + retList := make([]stackView, len(stackList)) + for i, s := range stackList { + retList[i] = stackView{ + Name: s.Name, + Status: s.Status, + } + } + return retList } diff --git a/cli/cmd/compose/ps.go b/cli/cmd/compose/ps.go index 8ad400554..13327f839 100644 --- a/cli/cmd/compose/ps.go +++ b/cli/cmd/compose/ps.go @@ -22,11 +22,12 @@ import ( "io" "os" "strings" - "text/tabwriter" "github.com/spf13/cobra" "github.com/docker/compose-cli/api/client" + "github.com/docker/compose-cli/api/compose" + "github.com/docker/compose-cli/formatter" ) func psCommand() *cobra.Command { @@ -37,10 +38,9 @@ func psCommand() *cobra.Command { return runPs(cmd.Context(), opts) }, } - psCmd.Flags().StringVarP(&opts.Name, "project-name", "p", "", "Project name") psCmd.Flags().StringVar(&opts.WorkingDir, "workdir", "", "Work dir") psCmd.Flags().StringArrayVarP(&opts.ConfigPaths, "file", "f", []string{}, "Compose configuration files") - + addComposeCommonFlags(psCmd.Flags(), &opts) return psCmd } @@ -59,17 +59,34 @@ func runPs(ctx context.Context, opts composeOptions) error { return err } - err = printSection(os.Stdout, func(w io.Writer) { - for _, service := range serviceList { - fmt.Fprintf(w, "%s\t%s\t%d/%d\t%s\n", service.ID, service.Name, service.Replicas, service.Desired, strings.Join(service.Ports, ", ")) - } - }, "ID", "NAME", "REPLICAS", "PORTS") - return err + view := viewFromServiceStatusList(serviceList) + return formatter.Print(view, opts.Format, os.Stdout, + func(w io.Writer) { + for _, service := range view { + _, _ = fmt.Fprintf(w, "%s\t%s\t%d/%d\t%s\n", service.ID, service.Name, service.Replicas, service.Desired, strings.Join(service.Ports, ", ")) + } + }, + "ID", "NAME", "REPLICAS", "PORTS") } -func printSection(out io.Writer, printer func(io.Writer), headers ...string) error { - w := tabwriter.NewWriter(out, 20, 1, 3, ' ', 0) - fmt.Fprintln(w, strings.Join(headers, "\t")) - printer(w) - return w.Flush() +type serviceStatusView struct { + ID string + Name string + Replicas int + Desired int + Ports []string +} + +func viewFromServiceStatusList(serviceStatusList []compose.ServiceStatus) []serviceStatusView { + retList := make([]serviceStatusView, len(serviceStatusList)) + for i, s := range serviceStatusList { + retList[i] = serviceStatusView{ + ID: s.ID, + Name: s.Name, + Replicas: s.Replicas, + Desired: s.Desired, + Ports: s.Ports, + } + } + return retList } diff --git a/cli/cmd/context/ls.go b/cli/cmd/context/ls.go index 2cad5c7ba..47ca3dea9 100644 --- a/cli/cmd/context/ls.go +++ b/cli/cmd/context/ls.go @@ -17,13 +17,13 @@ package context import ( - "errors" "fmt" + "io" "os" "sort" "strings" - "text/tabwriter" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/docker/compose-cli/cli/mobycli" @@ -58,7 +58,8 @@ func listCommand() *cobra.Command { } cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false, "Only show context names") cmd.Flags().BoolVar(&opts.json, "json", false, "Format output as JSON") - cmd.Flags().StringVar(&opts.format, "format", "", "Format output as JSON") + cmd.Flags().StringVar(&opts.format, "format", "", "Format the output. Values: [pretty | json]. (Default: pretty)") + _ = cmd.Flags().MarkHidden("json") return cmd } @@ -68,7 +69,7 @@ func runList(cmd *cobra.Command, opts lsOpts) error { if err != nil { return err } - if opts.format != "" { + if opts.format != "" && opts.format != formatter.JSON && opts.format != formatter.PRETTY { mobycli.Exec(cmd.Root()) return nil } @@ -93,35 +94,27 @@ func runList(cmd *cobra.Command, opts lsOpts) error { } if opts.json { - j, err := formatter.ToStandardJSON(contexts) - if err != nil { - return err - } - fmt.Println(j) - return nil + opts.format = formatter.JSON } - w := tabwriter.NewWriter(os.Stdout, 20, 1, 3, ' ', 0) - fmt.Fprintln(w, "NAME\tTYPE\tDESCRIPTION\tDOCKER ENDPOINT\tKUBERNETES ENDPOINT\tORCHESTRATOR") - format := "%s\t%s\t%s\t%s\t%s\t%s\n" - - for _, c := range contexts { - contextName := c.Name - if c.Name == currentContext { - contextName += " *" - } - - fmt.Fprintf(w, - format, - contextName, - c.Type(), - c.Metadata.Description, - getEndpoint("docker", c.Endpoints), - getEndpoint("kubernetes", c.Endpoints), - c.Metadata.StackOrchestrator) - } - - return w.Flush() + view := viewFromContextList(contexts, currentContext) + return formatter.Print(view, opts.format, os.Stdout, + func(w io.Writer) { + for _, c := range view { + contextName := c.Name + if c.Current { + contextName += " *" + } + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", + contextName, + c.Type, + c.Description, + c.DockerEndpoint, + c.KubernetesEndpoint, + c.StackOrchestrator) + } + }, + "NAME", "TYPE", "DESCRIPTION", "DOCKER ENDPOINT", "KUBERNETES ENDPOINT", "ORCHESTRATOR") } func getEndpoint(name string, meta map[string]interface{}) string { @@ -141,3 +134,29 @@ func getEndpoint(name string, meta map[string]interface{}) string { return result } + +type contextView struct { + Current bool + Description string + DockerEndpoint string + KubernetesEndpoint string + Type string + Name string + StackOrchestrator string +} + +func viewFromContextList(contextList []*store.DockerContext, currentContext string) []contextView { + retList := make([]contextView, len(contextList)) + for i, c := range contextList { + retList[i] = contextView{ + Current: c.Name == currentContext, + Description: c.Metadata.Description, + DockerEndpoint: getEndpoint("docker", c.Endpoints), + KubernetesEndpoint: getEndpoint("kubernetes", c.Endpoints), + Name: c.Name, + Type: c.Type(), + StackOrchestrator: c.Metadata.StackOrchestrator, + } + } + return retList +} diff --git a/cli/cmd/inspect.go b/cli/cmd/inspect.go index 20997d869..2e9647996 100644 --- a/cli/cmd/inspect.go +++ b/cli/cmd/inspect.go @@ -56,7 +56,7 @@ func runInspect(ctx context.Context, id string) error { if err != nil { return err } - fmt.Println(j) + fmt.Print(j) return nil } diff --git a/cli/cmd/ps.go b/cli/cmd/ps.go index 815284dec..56d6ab720 100644 --- a/cli/cmd/ps.go +++ b/cli/cmd/ps.go @@ -19,9 +19,9 @@ package cmd import ( "context" "fmt" + "io" "os" "strings" - "text/tabwriter" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -33,16 +33,10 @@ import ( ) type psOpts struct { - all bool - quiet bool - json bool -} - -func (o psOpts) validate() error { - if o.quiet && o.json { - return errors.New(`cannot combine "quiet" and "json" options`) - } - return nil + all bool + quiet bool + json bool + format string } // PsCommand lists containers @@ -59,50 +53,52 @@ func PsCommand() *cobra.Command { cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") cmd.Flags().BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)") cmd.Flags().BoolVar(&opts.json, "json", false, "Format output as JSON") + cmd.Flags().StringVar(&opts.format, "format", "", "Format the output. Values: [pretty | json]. (Default: pretty)") + _ = cmd.Flags().MarkHidden("json") // Legacy. This is used by VSCode Docker extension return cmd } +func (o psOpts) validate() error { + if o.quiet && o.json { + return errors.New(`cannot combine "quiet" and "json" options`) + } + return nil +} + func runPs(ctx context.Context, opts psOpts) error { err := opts.validate() if err != nil { return err } - c, err := client.New(ctx) if err != nil { return errors.Wrap(err, "cannot connect to backend") } - containers, err := c.ContainerService().List(ctx, opts.all) + containerList, err := c.ContainerService().List(ctx, opts.all) if err != nil { return errors.Wrap(err, "fetch containers") } if opts.quiet { - for _, c := range containers { + for _, c := range containerList { fmt.Println(c.ID) } return nil } if opts.json { - j, err := formatter2.ToStandardJSON(containers) - if err != nil { - return err + opts.format = formatter2.JSON + } + + view := viewFromContainerList(containerList) + return formatter2.Print(view, opts.format, os.Stdout, func(w io.Writer) { + for _, c := range view { + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", c.ID, c.Image, c.Command, c.Status, + strings.Join(c.Ports, ", ")) } - fmt.Println(j) - return nil - } - - w := tabwriter.NewWriter(os.Stdout, 20, 1, 3, ' ', 0) - fmt.Fprintf(w, "CONTAINER ID\tIMAGE\tCOMMAND\tSTATUS\tPORTS\n") - format := "%s\t%s\t%s\t%s\t%s\n" - for _, container := range containers { - fmt.Fprintf(w, format, container.ID, container.Image, container.Command, container.Status, strings.Join(formatter.PortsToStrings(container.Ports, fqdn(container)), ", ")) - } - - return w.Flush() + }, "CONTAINER ID", "IMAGE", "COMMAND", "STATUS", "PORTS") } func fqdn(container containers.Container) string { @@ -112,3 +108,25 @@ func fqdn(container containers.Container) string { } return fqdn } + +type containerView struct { + ID string + Image string + Status string + Command string + Ports []string +} + +func viewFromContainerList(containerList []containers.Container) []containerView { + retList := make([]containerView, len(containerList)) + for i, c := range containerList { + retList[i] = containerView{ + ID: c.ID, + Image: c.Image, + Status: c.Status, + Command: c.Command, + Ports: formatter.PortsToStrings(c.Ports, fqdn(c)), + } + } + return retList +} diff --git a/cli/cmd/secrets.go b/cli/cmd/secrets.go index cce9141f4..f372376bd 100644 --- a/cli/cmd/secrets.go +++ b/cli/cmd/secrets.go @@ -20,13 +20,12 @@ import ( "fmt" "io" "os" - "strings" - "text/tabwriter" "github.com/spf13/cobra" "github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/secrets" + "github.com/docker/compose-cli/formatter" ) type createSecretOptions struct { @@ -105,7 +104,12 @@ func inspectSecret() *cobra.Command { return cmd } +type listSecretsOpts struct { + format string +} + func listSecrets() *cobra.Command { + var opts listSecretsOpts cmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, @@ -115,17 +119,40 @@ func listSecrets() *cobra.Command { if err != nil { return err } - list, err := c.SecretsService().ListSecrets(cmd.Context()) + secretsList, err := c.SecretsService().ListSecrets(cmd.Context()) if err != nil { return err } - printList(os.Stdout, list) - return nil + view := viewFromSecretList(secretsList) + return formatter.Print(view, opts.format, os.Stdout, func(w io.Writer) { + for _, secret := range view { + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", secret.ID, secret.Name, secret.Description) + } + }, "ID", "NAME", "DESCRIPTION") }, } + cmd.Flags().StringVar(&opts.format, "format", "", "Format the output. Values: [pretty | json]. (Default: pretty)") return cmd } +type secretView struct { + ID string + Name string + Description string +} + +func viewFromSecretList(secretList []secrets.Secret) []secretView { + retList := make([]secretView, len(secretList)) + for i, s := range secretList { + retList[i] = secretView{ + ID: s.ID, + Name: s.Name, + Description: s.Description, + } + } + return retList +} + type deleteSecretOptions struct { recover bool } @@ -148,18 +175,3 @@ func deleteSecret() *cobra.Command { cmd.Flags().BoolVar(&opts.recover, "recover", false, "Enable recovery.") return cmd } - -func printList(out io.Writer, secrets []secrets.Secret) { - printSection(out, func(w io.Writer) { - for _, secret := range secrets { - fmt.Fprintf(w, "%s\t%s\t%s\n", secret.ID, secret.Name, secret.Description) // nolint:errcheck - } - }, "ID", "NAME", "DESCRIPTION") -} - -func printSection(out io.Writer, printer func(io.Writer), headers ...string) { - w := tabwriter.NewWriter(out, 20, 1, 3, ' ', 0) - fmt.Fprintln(w, strings.Join(headers, "\t")) // nolint:errcheck - printer(w) - w.Flush() // nolint:errcheck -} diff --git a/cli/cmd/version.go b/cli/cmd/version.go index 70a7cd0ad..3a50f1790 100644 --- a/cli/cmd/version.go +++ b/cli/cmd/version.go @@ -18,43 +18,96 @@ package cmd import ( "fmt" + "os" "strings" "github.com/spf13/cobra" "github.com/docker/compose-cli/cli/cmd/mobyflags" "github.com/docker/compose-cli/cli/mobycli" + "github.com/docker/compose-cli/formatter" ) +const formatOpt = "format" + // VersionCommand command to display version func VersionCommand(version string) *cobra.Command { cmd := &cobra.Command{ Use: "version", Short: "Show the Docker version information", Args: cobra.MaximumNArgs(0), - RunE: func(cmd *cobra.Command, _ []string) error { - return runVersion(cmd, version) + Run: func(cmd *cobra.Command, _ []string) { + runVersion(cmd, version) }, } // define flags for backward compatibility with com.docker.cli flags := cmd.Flags() - flags.StringP("format", "f", "", "Format the output using the given Go template") + flags.StringP(formatOpt, "f", "", "Format the output. Values: [pretty | json]. (Default: pretty)") flags.String("kubeconfig", "", "Kubernetes config file") mobyflags.AddMobyFlagsForRetrocompatibility(flags) return cmd } -func runVersion(cmd *cobra.Command, version string) error { +func runVersion(cmd *cobra.Command, version string) { + var versionString string + format := strings.ToLower(strings.ReplaceAll(cmd.Flag(formatOpt).Value.String(), " ", "")) displayedVersion := strings.TrimPrefix(version, "v") - versionResult, _ := mobycli.ExecSilent(cmd.Context()) + // Replace is preferred in this case to keep the order. + switch format { + case formatter.PRETTY, "": + versionString = strings.Replace(getOutFromMoby(cmd, fixedPrettyArgs(os.Args[1:])...), + "\n Version:", "\n Cloud integration: "+displayedVersion+"\n Version:", 1) + case formatter.JSON, "{{json.}}": // Try to catch full JSON formats + versionString = strings.Replace(getOutFromMoby(cmd, fixedJSONArgs(os.Args[1:])...), + `"Version":`, fmt.Sprintf(`"CloudIntegration":%q,"Version":`, displayedVersion), 1) + default: + versionString = getOutFromMoby(cmd) + } + + fmt.Print(versionString) +} + +func getOutFromMoby(cmd *cobra.Command, args ...string) string { + versionResult, _ := mobycli.ExecSilent(cmd.Context(), args...) // we don't want to fail on error, there is an error if the engine is not available but it displays client version info // Still, technically the [] byte versionResult could be nil, just let the original command display what it has to display if versionResult == nil { mobycli.Exec(cmd.Root()) - return nil + return "" } - var s string = string(versionResult) - fmt.Print(strings.Replace(s, "\n Version:", "\n Cloud integration "+displayedVersion+"\n Version:", 1)) - return nil + return string(versionResult) +} + +func fixedPrettyArgs(oArgs []string) []string { + args := make([]string, 0) + for i := 0; i < len(oArgs); i++ { + if isFormatOpt(oArgs[i]) && + len(oArgs) > i && + (strings.ToLower(oArgs[i+1]) == formatter.PRETTY || oArgs[i+1] == "") { + i++ + continue + } + args = append(args, oArgs[i]) + } + return args +} + +func fixedJSONArgs(oArgs []string) []string { + args := make([]string, 0) + for i := 0; i < len(oArgs); i++ { + if isFormatOpt(oArgs[i]) && + len(oArgs) > i && + strings.ToLower(oArgs[i+1]) == formatter.JSON { + args = append(args, oArgs[i], "{{json .}}") + i++ + continue + } + args = append(args, oArgs[i]) + } + return args +} + +func isFormatOpt(o string) bool { + return o == "--format" || o == "-f" } diff --git a/cli/cmd/version_test.go b/cli/cmd/version_test.go new file mode 100644 index 000000000..bdcf24359 --- /dev/null +++ b/cli/cmd/version_test.go @@ -0,0 +1,190 @@ +/* + 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 cmd + +import ( + "testing" + + "gotest.tools/assert" +) + +type caze struct { + Actual []string + Expected []string +} + +func TestVersionFormat(t *testing.T) { + jsonCases := []caze{ + { + Actual: fixedJSONArgs([]string{}), + Expected: []string{}, + }, + { + Actual: fixedJSONArgs([]string{ + "docker", + "version", + }), + Expected: []string{ + "docker", + "version", + }, + }, + { + Actual: fixedJSONArgs([]string{ + "docker", + "version", + "--format", + "json", + }), + Expected: []string{ + "docker", + "version", + "--format", + "{{json .}}", + }, + }, + { + Actual: fixedJSONArgs([]string{ + "docker", + "version", + "--format", + "jSoN", + }), + Expected: []string{ + "docker", + "version", + "--format", + "{{json .}}", + }, + }, + { + Actual: fixedJSONArgs([]string{ + "docker", + "version", + "--format", + "json", + "--kubeconfig", + "myKubeConfig", + }), + Expected: []string{ + "docker", + "version", + "--format", + "{{json .}}", + "--kubeconfig", + "myKubeConfig", + }, + }, + { + Actual: fixedJSONArgs([]string{ + "--format", + "json", + }), + Expected: []string{ + "--format", + "{{json .}}", + }, + }, + } + prettyCases := []caze{ + { + Actual: fixedPrettyArgs([]string{}), + Expected: []string{}, + }, + { + Actual: fixedPrettyArgs([]string{ + "docker", + "version", + }), + Expected: []string{ + "docker", + "version", + }, + }, + { + Actual: fixedPrettyArgs([]string{ + "docker", + "version", + "--format", + "pretty", + }), + Expected: []string{ + "docker", + "version", + }, + }, + { + Actual: fixedPrettyArgs([]string{ + "docker", + "version", + "--format", + "pRettY", + }), + Expected: []string{ + "docker", + "version", + }, + }, + { + Actual: fixedPrettyArgs([]string{ + "docker", + "version", + "--format", + "", + }), + Expected: []string{ + "docker", + "version", + }, + }, + { + Actual: fixedPrettyArgs([]string{ + "docker", + "version", + "--format", + "pretty", + "--kubeconfig", + "myKubeConfig", + }), + Expected: []string{ + "docker", + "version", + "--kubeconfig", + "myKubeConfig", + }, + }, + { + Actual: fixedPrettyArgs([]string{ + "--format", + "pretty", + }), + Expected: []string{}, + }, + } + + t.Run("json", func(t *testing.T) { + for _, c := range jsonCases { + assert.DeepEqual(t, c.Actual, c.Expected) + } + }) + + t.Run("pretty", func(t *testing.T) { + for _, c := range prettyCases { + assert.DeepEqual(t, c.Actual, c.Expected) + } + }) +} diff --git a/cli/cmd/volume/list.go b/cli/cmd/volume/list.go index 9ab3313ac..b279e9123 100644 --- a/cli/cmd/volume/list.go +++ b/cli/cmd/volume/list.go @@ -20,16 +20,20 @@ import ( "fmt" "io" "os" - "strings" - "text/tabwriter" "github.com/spf13/cobra" "github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/volumes" + "github.com/docker/compose-cli/formatter" ) +type listVolumeOpts struct { + format string +} + func listVolume() *cobra.Command { + var opts listVolumeOpts cmd := &cobra.Command{ Use: "ls", Short: "list available volumes in context.", @@ -43,24 +47,30 @@ func listVolume() *cobra.Command { if err != nil { return err } - printList(os.Stdout, vols) - return nil + view := viewFromVolumeList(vols) + return formatter.Print(view, opts.format, os.Stdout, func(w io.Writer) { + for _, vol := range view { + _, _ = fmt.Fprintf(w, "%s\t%s\n", vol.ID, vol.Description) + } + }, "ID", "DESCRIPTION") }, } + cmd.Flags().StringVar(&opts.format, "format", formatter.PRETTY, "Format the output. Values: [pretty | json]. (Default: pretty)") return cmd } -func printList(out io.Writer, volumes []volumes.Volume) { - printSection(out, func(w io.Writer) { - for _, vol := range volumes { - _, _ = fmt.Fprintf(w, "%s\t%s\n", vol.ID, vol.Description) - } - }, "ID", "DESCRIPTION") +type volumeView struct { + ID string + Description string } -func printSection(out io.Writer, printer func(io.Writer), headers ...string) { - w := tabwriter.NewWriter(out, 20, 1, 3, ' ', 0) - _, _ = fmt.Fprintln(w, strings.Join(headers, "\t")) - printer(w) - _ = w.Flush() +func viewFromVolumeList(volumeList []volumes.Volume) []volumeView { + retList := make([]volumeView, len(volumeList)) + for i, v := range volumeList { + retList[i] = volumeView{ + ID: v.ID, + Description: v.Description, + } + } + return retList } diff --git a/cli/cmd/volume/testdata/volumes-out.golden b/cli/cmd/volume/testdata/volumes-out.golden deleted file mode 100644 index 61c996793..000000000 --- a/cli/cmd/volume/testdata/volumes-out.golden +++ /dev/null @@ -1,2 +0,0 @@ -ID DESCRIPTION -volume/123 volume 123 diff --git a/cli/mobycli/exec.go b/cli/mobycli/exec.go index 665de45b2..5b89cd1c3 100644 --- a/cli/mobycli/exec.go +++ b/cli/mobycli/exec.go @@ -112,7 +112,10 @@ func IsDefaultContextCommand(dockerCommand string) bool { } // ExecSilent executes a command and do redirect output to stdOut, return output -func ExecSilent(ctx context.Context) ([]byte, error) { - cmd := exec.CommandContext(ctx, ComDockerCli, os.Args[1:]...) +func ExecSilent(ctx context.Context, args ...string) ([]byte, error) { + if len(args) == 0 { + args = os.Args[1:] + } + cmd := exec.CommandContext(ctx, ComDockerCli, args...) return cmd.CombinedOutput() } diff --git a/cli/cmd/secrets_test.go b/formatter/consts.go similarity index 61% rename from cli/cmd/secrets_test.go rename to formatter/consts.go index 9624c0c5d..1ca9ebcdd 100644 --- a/cli/cmd/secrets_test.go +++ b/formatter/consts.go @@ -14,26 +14,11 @@ limitations under the License. */ -package cmd +package formatter -import ( - "bytes" - "testing" - - "gotest.tools/v3/golden" - - "github.com/docker/compose-cli/api/secrets" +const ( + // JSON is the constant for Json formats on list commands + JSON = "json" + // PRETTY is the constant for default formats on list commands + PRETTY = "pretty" ) - -func TestPrintList(t *testing.T) { - secrets := []secrets.Secret{ - { - ID: "123", - Name: "secret123", - Description: "secret 1,2,3", - }, - } - out := &bytes.Buffer{} - printList(out, secrets) - golden.Assert(t, out.String(), "secrets-out.golden") -} diff --git a/formatter/formatter.go b/formatter/formatter.go new file mode 100644 index 000000000..52cd9d0d5 --- /dev/null +++ b/formatter/formatter.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 formatter + +import ( + "fmt" + "io" + "reflect" + "strings" + + "github.com/pkg/errors" + + "github.com/docker/compose-cli/errdefs" +) + +// Print prints formatted lists in different formats +func Print(toJSON interface{}, format string, outWriter io.Writer, writerFn func(w io.Writer), headers ...string) error { + switch strings.ToLower(format) { + case PRETTY, "": + return PrintPrettySection(outWriter, writerFn, headers...) + case JSON: + switch reflect.TypeOf(toJSON).Kind() { + case reflect.Slice: + s := reflect.ValueOf(toJSON) + for i := 0; i < s.Len(); i++ { + obj := s.Index(i).Interface() + jsonLine, err := ToJSON(obj, "", "") + if err != nil { + return err + } + _, _ = fmt.Fprint(outWriter, jsonLine) + } + default: + outJSON, err := ToStandardJSON(toJSON) + if err != nil { + return err + } + _, _ = fmt.Fprintln(outWriter, outJSON) + } + default: + return errors.Wrapf(errdefs.ErrParsingFailed, "format value %q could not be parsed", format) + } + return nil +} diff --git a/formatter/formatter_test.go b/formatter/formatter_test.go new file mode 100644 index 000000000..6657a0152 --- /dev/null +++ b/formatter/formatter_test.go @@ -0,0 +1,63 @@ +/* + 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 formatter + +import ( + "bytes" + "fmt" + "io" + "testing" + + "gotest.tools/assert" +) + +type testStruct struct { + Name string + Status string +} + +// Print prints formatted lists in different formats +func TestPrint(t *testing.T) { + testList := []testStruct{ + { + Name: "myName1", + Status: "myStatus1", + }, + { + Name: "myName2", + Status: "myStatus2", + }, + } + + b := &bytes.Buffer{} + assert.NilError(t, Print(testList, PRETTY, b, func(w io.Writer) { + for _, t := range testList { + _, _ = fmt.Fprintf(w, "%s\t%s\n", t.Name, t.Status) + } + }, "NAME", "STATUS")) + assert.Equal(t, b.String(), "NAME STATUS\nmyName1 myStatus1\nmyName2 myStatus2\n") + + b.Reset() + assert.NilError(t, Print(testList, JSON, b, func(w io.Writer) { + for _, t := range testList { + _, _ = fmt.Fprintf(w, "%s\t%s\n", t.Name, t.Status) + } + }, "NAME", "STATUS")) + assert.Equal(t, b.String(), `{"Name":"myName1","Status":"myStatus1"} +{"Name":"myName2","Status":"myStatus2"} +`) +} diff --git a/formatter/json.go b/formatter/json.go index bb9b6078b..afadb0c81 100644 --- a/formatter/json.go +++ b/formatter/json.go @@ -16,15 +16,24 @@ package formatter -import "encoding/json" +import ( + "bytes" + "encoding/json" +) const standardIndentation = " " // ToStandardJSON return a string with the JSON representation of the interface{} func ToStandardJSON(i interface{}) (string, error) { - b, err := json.MarshalIndent(i, "", standardIndentation) - if err != nil { - return "", err - } - return string(b), nil + return ToJSON(i, "", standardIndentation) +} + +// ToJSON return a string with the JSON representation of the interface{} +func ToJSON(i interface{}, prefix string, indentation string) (string, error) { + buffer := &bytes.Buffer{} + encoder := json.NewEncoder(buffer) + encoder.SetEscapeHTML(false) + encoder.SetIndent(prefix, indentation) + err := encoder.Encode(i) + return buffer.String(), err } diff --git a/cli/cmd/volume/list_test.go b/formatter/pretty.go similarity index 63% rename from cli/cmd/volume/list_test.go rename to formatter/pretty.go index f88716e90..bb85dedef 100644 --- a/cli/cmd/volume/list_test.go +++ b/formatter/pretty.go @@ -14,25 +14,19 @@ limitations under the License. */ -package volume +package formatter import ( - "bytes" - "testing" - - "gotest.tools/v3/golden" - - "github.com/docker/compose-cli/api/volumes" + "fmt" + "io" + "strings" + "text/tabwriter" ) -func TestPrintList(t *testing.T) { - secrets := []volumes.Volume{ - { - ID: "volume/123", - Description: "volume 123", - }, - } - out := &bytes.Buffer{} - printList(out, secrets) - golden.Assert(t, out.String(), "volumes-out.golden") +// PrintPrettySection prints a tabbed section on the writer parameter +func PrintPrettySection(out io.Writer, printer func(writer io.Writer), headers ...string) error { + w := tabwriter.NewWriter(out, 20, 1, 3, ' ', 0) + _, _ = fmt.Fprintln(w, strings.Join(headers, "\t")) + printer(w) + return w.Flush() } diff --git a/go.mod b/go.mod index f0cf60a46..aef8bdfa4 100644 --- a/go.mod +++ b/go.mod @@ -62,5 +62,6 @@ require ( google.golang.org/protobuf v1.25.0 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/ini.v1 v1.61.0 + gotest.tools v2.2.0+incompatible gotest.tools/v3 v3.0.2 ) diff --git a/go.sum b/go.sum index 378b6d0d0..1239c69e3 100644 --- a/go.sum +++ b/go.sum @@ -480,6 +480,7 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index f72566661..13268c2d1 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -75,6 +75,12 @@ func TestContextDefault(t *testing.T) { t.Run("ls", func(t *testing.T) { res := c.RunDockerCmd("context", "ls") golden.Assert(t, res.Stdout(), GoldenFile("ls-out-default")) + + res = c.RunDockerCmd("context", "ls", "--format", "pretty") + golden.Assert(t, res.Stdout(), GoldenFile("ls-out-default")) + + res = c.RunDockerCmd("context", "ls", "--format", "json") + golden.Assert(t, res.Stdout(), GoldenFile("ls-out-json")) }) t.Run("inspect", func(t *testing.T) { @@ -416,6 +422,26 @@ func TestVersion(t *testing.T) { res.Assert(t, icmd.Expected{Out: `"Client":`}) }) + t.Run("format cloud integration", func(t *testing.T) { + res := c.RunDockerCmd("version", "-f", "pretty") + res.Assert(t, icmd.Expected{Out: `Cloud integration:`}) + res = c.RunDockerCmd("version", "-f", "") + res.Assert(t, icmd.Expected{Out: `Cloud integration:`}) + + res = c.RunDockerCmd("version", "-f", "json") + res.Assert(t, icmd.Expected{Out: `"CloudIntegration":`}) + res = c.RunDockerCmd("version", "-f", "{{ json . }}") + res.Assert(t, icmd.Expected{Out: `"CloudIntegration":`}) + res = c.RunDockerCmd("version", "--format", "{{json .}}") + res.Assert(t, icmd.Expected{Out: `"CloudIntegration":`}) + res = c.RunDockerCmd("version", "--format", "{{json . }}") + res.Assert(t, icmd.Expected{Out: `"CloudIntegration":`}) + res = c.RunDockerCmd("version", "--format", "{{ json .}}") + res.Assert(t, icmd.Expected{Out: `"CloudIntegration":`}) + res = c.RunDockerCmd("version", "--format", "{{ json . }}") + res.Assert(t, icmd.Expected{Out: `"CloudIntegration":`}) + }) + t.Run("delegate version flag", func(t *testing.T) { c.RunDockerCmd("context", "create", "example", "test-example") c.RunDockerCmd("context", "use", "test-example") @@ -440,6 +466,12 @@ func TestMockBackend(t *testing.T) { t.Run("ps", func(t *testing.T) { res := c.RunDockerCmd("ps") golden.Assert(t, res.Stdout(), "ps-out-example.golden") + + res = c.RunDockerCmd("ps", "--format", "pretty") + golden.Assert(t, res.Stdout(), "ps-out-example.golden") + + res = c.RunDockerCmd("ps", "--format", "json") + golden.Assert(t, res.Stdout(), "ps-out-example-json.golden") }) t.Run("ps quiet", func(t *testing.T) { diff --git a/tests/e2e/testdata/ls-out-json.golden b/tests/e2e/testdata/ls-out-json.golden new file mode 100644 index 000000000..34d53cb0a --- /dev/null +++ b/tests/e2e/testdata/ls-out-json.golden @@ -0,0 +1 @@ +{"Current":true,"Description":"Current DOCKER_HOST based configuration","DockerEndpoint":"unix:///var/run/docker.sock","KubernetesEndpoint":"","Type":"moby","Name":"default","StackOrchestrator":"swarm"} diff --git a/tests/e2e/testdata/ps-out-example-json.golden b/tests/e2e/testdata/ps-out-example-json.golden new file mode 100644 index 000000000..2f7ddef9b --- /dev/null +++ b/tests/e2e/testdata/ps-out-example-json.golden @@ -0,0 +1,2 @@ +{"ID":"id","Image":"nginx","Status":"","Command":"","Ports":[]} +{"ID":"1234","Image":"alpine","Status":"","Command":"","Ports":[]} diff --git a/utils/formatter/container.go b/utils/formatter/container.go index dc5c3fdb2..5dcdf3421 100644 --- a/utils/formatter/container.go +++ b/utils/formatter/container.go @@ -33,8 +33,8 @@ type portGroup struct { // PortsToStrings returns a human readable published ports func PortsToStrings(ports []containers.Port, fqdn string) []string { groupMap := make(map[string]*portGroup) + result := []string{} var ( - result []string hostMappings []string groupMapKeys []string )