Add json output format to several commands

- docker context ls
- docker ps
- docker compose ls
- docker compose ps
- docker secret ls
- docker volume ls
- docker version

Signed-off-by: Ulysses Souza <ulyssessouza@gmail.com>
This commit is contained in:
Ulysses Souza 2020-09-28 17:08:27 +02:00
parent abd6af6386
commit 8961805412
23 changed files with 665 additions and 118 deletions

View File

@ -33,6 +33,7 @@ type composeOptions struct {
WorkingDir string WorkingDir string
ConfigPaths []string ConfigPaths []string
Environment []string Environment []string
Format string
} }
func (o *composeOptions) toProjectName() (string, error) { func (o *composeOptions) toProjectName() (string, error) {

View File

@ -21,10 +21,16 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"strings"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/client"
"github.com/docker/compose-cli/api/compose"
"github.com/docker/compose-cli/errdefs"
"github.com/docker/compose-cli/formatter"
) )
func listCommand() *cobra.Command { func listCommand() *cobra.Command {
@ -35,10 +41,15 @@ func listCommand() *cobra.Command {
return runList(cmd.Context(), opts) return runList(cmd.Context(), opts)
}, },
} }
lsCmd.Flags().StringVarP(&opts.Name, "project-name", "p", "", "Project name") addComposeCommonFlags(lsCmd.Flags(), &opts)
return lsCmd 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 { func runList(ctx context.Context, opts composeOptions) error {
c, err := client.New(ctx) c, err := client.New(ctx)
if err != nil { if err != nil {
@ -49,10 +60,26 @@ func runList(ctx context.Context, opts composeOptions) error {
return err return err
} }
err = printSection(os.Stdout, func(w io.Writer) { return printListFormatted(opts.Format, os.Stdout, stackList)
}
func printListFormatted(format string, out io.Writer, stackList []compose.Stack) error {
var err error
switch strings.ToLower(format) {
case formatter.PRETTY, "":
err = formatter.PrintPrettySection(out, func(w io.Writer) {
for _, stack := range stackList { for _, stack := range stackList {
fmt.Fprintf(w, "%s\t%s\n", stack.Name, stack.Status) fmt.Fprintf(w, "%s\t%s\n", stack.Name, stack.Status)
} }
}, "NAME", "STATUS") }, "NAME", "STATUS")
case formatter.JSON:
outJSON, err := formatter.ToStandardJSON(stackList)
if err != nil {
return err
}
_, _ = fmt.Fprint(out, outJSON)
default:
err = errors.Wrapf(errdefs.ErrParsingFailed, "format value %q could not be parsed", format)
}
return err return err
} }

View File

@ -0,0 +1,45 @@
/*
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 (
"bytes"
"testing"
"gotest.tools/assert"
"gotest.tools/golden"
"github.com/docker/compose-cli/api/compose"
"github.com/docker/compose-cli/formatter"
)
func TestPrintComposeList(t *testing.T) {
secretList := []compose.Stack{
{
ID: "123",
Name: "myName123",
Status: "Running",
},
}
out := &bytes.Buffer{}
assert.NilError(t, printListFormatted(formatter.PRETTY, out, secretList))
golden.Assert(t, out.String(), "compose-list-out.golden")
out.Reset()
assert.NilError(t, printListFormatted(formatter.JSON, out, secretList))
golden.Assert(t, out.String(), "compose-list-out-json.golden")
}

View File

@ -22,11 +22,14 @@ import (
"io" "io"
"os" "os"
"strings" "strings"
"text/tabwriter"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/client"
"github.com/docker/compose-cli/api/compose"
"github.com/docker/compose-cli/errdefs"
"github.com/docker/compose-cli/formatter"
) )
func psCommand() *cobra.Command { func psCommand() *cobra.Command {
@ -37,10 +40,9 @@ func psCommand() *cobra.Command {
return runPs(cmd.Context(), opts) 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().StringVar(&opts.WorkingDir, "workdir", "", "Work dir")
psCmd.Flags().StringArrayVarP(&opts.ConfigPaths, "file", "f", []string{}, "Compose configuration files") psCmd.Flags().StringArrayVarP(&opts.ConfigPaths, "file", "f", []string{}, "Compose configuration files")
addComposeCommonFlags(psCmd.Flags(), &opts)
return psCmd return psCmd
} }
@ -59,17 +61,26 @@ func runPs(ctx context.Context, opts composeOptions) error {
return err return err
} }
err = printSection(os.Stdout, func(w io.Writer) { return printPsFormatted(opts.Format, os.Stdout, serviceList)
}
func printPsFormatted(format string, out io.Writer, serviceList []compose.ServiceStatus) error {
var err error
switch strings.ToLower(format) {
case formatter.PRETTY, "":
err = formatter.PrintPrettySection(out, func(w io.Writer) {
for _, service := range serviceList { 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, ", ")) 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") }, "ID", "NAME", "REPLICAS", "PORTS")
case formatter.JSON:
outJSON, err := formatter.ToStandardJSON(serviceList)
if err != nil {
return err return err
} }
_, _ = fmt.Fprint(out, outJSON)
func printSection(out io.Writer, printer func(io.Writer), headers ...string) error { default:
w := tabwriter.NewWriter(out, 20, 1, 3, ' ', 0) err = errors.Wrapf(errdefs.ErrParsingFailed, "format value %q could not be parsed", format)
fmt.Fprintln(w, strings.Join(headers, "\t")) }
printer(w) return err
return w.Flush()
} }

View File

@ -0,0 +1,7 @@
[
{
"ID": "123",
"Name": "myName123",
"Status": "Running"
}
]

View File

@ -0,0 +1,2 @@
NAME STATUS
myName123 Running

View File

@ -17,18 +17,19 @@
package context package context
import ( import (
"errors"
"fmt" "fmt"
"io"
"os" "os"
"sort" "sort"
"strings" "strings"
"text/tabwriter"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/compose-cli/cli/mobycli" "github.com/docker/compose-cli/cli/mobycli"
apicontext "github.com/docker/compose-cli/context" apicontext "github.com/docker/compose-cli/context"
"github.com/docker/compose-cli/context/store" "github.com/docker/compose-cli/context/store"
"github.com/docker/compose-cli/errdefs"
"github.com/docker/compose-cli/formatter" "github.com/docker/compose-cli/formatter"
) )
@ -58,7 +59,8 @@ func listCommand() *cobra.Command {
} }
cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false, "Only show context names") 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().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 | go template]. (Default: pretty)")
_ = cmd.Flags().MarkHidden("json")
return cmd return cmd
} }
@ -68,7 +70,7 @@ func runList(cmd *cobra.Command, opts lsOpts) error {
if err != nil { if err != nil {
return err return err
} }
if opts.format != "" { if opts.format != "" && opts.format != formatter.JSON && opts.format != formatter.PRETTY {
mobycli.Exec(cmd.Root()) mobycli.Exec(cmd.Root())
return nil return nil
} }
@ -93,26 +95,23 @@ func runList(cmd *cobra.Command, opts lsOpts) error {
} }
if opts.json { if opts.json {
j, err := formatter.ToStandardJSON(contexts) opts.format = formatter.JSON
if err != nil {
return err
}
fmt.Println(j)
return nil
} }
w := tabwriter.NewWriter(os.Stdout, 20, 1, 3, ' ', 0) return printContextLsFormatted(opts.format, currentContext, os.Stdout, contexts)
fmt.Fprintln(w, "NAME\tTYPE\tDESCRIPTION\tDOCKER ENDPOINT\tKUBERNETES ENDPOINT\tORCHESTRATOR") }
format := "%s\t%s\t%s\t%s\t%s\t%s\n"
func printContextLsFormatted(format string, currContext string, out io.Writer, contexts []*store.DockerContext) error {
var err error
switch strings.ToLower(format) {
case formatter.PRETTY, "":
err = formatter.PrintPrettySection(out, func(w io.Writer) {
for _, c := range contexts { for _, c := range contexts {
contextName := c.Name contextName := c.Name
if c.Name == currentContext { if c.Name == currContext {
contextName += " *" contextName += " *"
} }
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
fmt.Fprintf(w,
format,
contextName, contextName,
c.Type(), c.Type(),
c.Metadata.Description, c.Metadata.Description,
@ -120,8 +119,17 @@ func runList(cmd *cobra.Command, opts lsOpts) error {
getEndpoint("kubernetes", c.Endpoints), getEndpoint("kubernetes", c.Endpoints),
c.Metadata.StackOrchestrator) c.Metadata.StackOrchestrator)
} }
}, "NAME", "TYPE", "DESCRIPTION", "DOCKER ENDPOINT", "KUBERNETES ENDPOINT", "ORCHESTRATOR")
return w.Flush() case formatter.JSON:
out, err := formatter.ToStandardJSON(contexts)
if err != nil {
return err
}
fmt.Println(out)
default:
err = errors.Wrapf(errdefs.ErrParsingFailed, "format value %q could not be parsed", format)
}
return err
} }
func getEndpoint(name string, meta map[string]interface{}) string { func getEndpoint(name string, meta map[string]interface{}) string {

View File

@ -19,15 +19,16 @@ package cmd
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"os" "os"
"strings" "strings"
"text/tabwriter"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/client"
"github.com/docker/compose-cli/api/containers" "github.com/docker/compose-cli/api/containers"
"github.com/docker/compose-cli/errdefs"
formatter2 "github.com/docker/compose-cli/formatter" formatter2 "github.com/docker/compose-cli/formatter"
"github.com/docker/compose-cli/utils/formatter" "github.com/docker/compose-cli/utils/formatter"
) )
@ -36,13 +37,7 @@ type psOpts struct {
all bool all bool
quiet bool quiet bool
json bool json bool
} format string
func (o psOpts) validate() error {
if o.quiet && o.json {
return errors.New(`cannot combine "quiet" and "json" options`)
}
return nil
} }
// PsCommand lists containers // PsCommand lists containers
@ -59,50 +54,69 @@ func PsCommand() *cobra.Command {
cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") 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().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().BoolVar(&opts.json, "json", false, "Format output as JSON")
cmd.Flags().StringVar(&opts.format, "format", "", "Format the output. Values: [pretty | json | go template]. (Default: pretty)")
_ = cmd.Flags().MarkHidden("json")
return cmd 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 { func runPs(ctx context.Context, opts psOpts) error {
err := opts.validate() err := opts.validate()
if err != nil { if err != nil {
return err return err
} }
c, err := client.New(ctx) c, err := client.New(ctx)
if err != nil { if err != nil {
return errors.Wrap(err, "cannot connect to backend") 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 { if err != nil {
return errors.Wrap(err, "fetch containers") return errors.Wrap(err, "fetch containerList")
} }
if opts.quiet { if opts.quiet {
for _, c := range containers { for _, c := range containerList {
fmt.Println(c.ID) fmt.Println(c.ID)
} }
return nil return nil
} }
if opts.json { if opts.json {
j, err := formatter2.ToStandardJSON(containers) opts.format = formatter2.JSON
}
return printPsFormatted(opts.format, os.Stdout, containerList)
}
func printPsFormatted(format string, out io.Writer, containers []containers.Container) error {
var err error
switch strings.ToLower(format) {
case formatter2.PRETTY, "":
err = formatter2.PrintPrettySection(out, func(w io.Writer) {
for _, c := range containers {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", c.ID, c.Image, c.Command, c.Status,
strings.Join(formatter.PortsToStrings(c.Ports, fqdn(c)), ", "))
}
}, "CONTAINER ID", "IMAGE", "COMMAND", "STATUS", "PORTS")
case formatter2.JSON:
out, err := formatter2.ToStandardJSON(containers)
if err != nil { if err != nil {
return err return err
} }
fmt.Println(j) fmt.Println(out)
return nil
}
w := tabwriter.NewWriter(os.Stdout, 20, 1, 3, ' ', 0) default:
fmt.Fprintf(w, "CONTAINER ID\tIMAGE\tCOMMAND\tSTATUS\tPORTS\n") err = errors.Wrapf(errdefs.ErrParsingFailed, "format value %q could not be parsed", format)
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 err
return w.Flush()
} }
func fqdn(container containers.Container) string { func fqdn(container containers.Container) string {

View File

@ -21,12 +21,14 @@ import (
"io" "io"
"os" "os"
"strings" "strings"
"text/tabwriter"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/client"
"github.com/docker/compose-cli/api/secrets" "github.com/docker/compose-cli/api/secrets"
"github.com/docker/compose-cli/errdefs"
"github.com/docker/compose-cli/formatter"
) )
type createSecretOptions struct { type createSecretOptions struct {
@ -105,7 +107,12 @@ func inspectSecret() *cobra.Command {
return cmd return cmd
} }
type listSecretsOpts struct {
format string
}
func listSecrets() *cobra.Command { func listSecrets() *cobra.Command {
var opts listSecretsOpts
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "list", Use: "list",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
@ -119,10 +126,10 @@ func listSecrets() *cobra.Command {
if err != nil { if err != nil {
return err return err
} }
printList(os.Stdout, list) return printSecretList(opts.format, os.Stdout, list)
return nil
}, },
} }
cmd.Flags().StringVar(&opts.format, "format", "", "Format the output. Values: [pretty | json]. (Default: pretty)")
return cmd return cmd
} }
@ -149,17 +156,23 @@ func deleteSecret() *cobra.Command {
return cmd return cmd
} }
func printList(out io.Writer, secrets []secrets.Secret) { func printSecretList(format string, out io.Writer, secrets []secrets.Secret) error {
printSection(out, func(w io.Writer) { var err error
switch strings.ToLower(format) {
case formatter.PRETTY, "":
err = formatter.PrintPrettySection(out, func(w io.Writer) {
for _, secret := range secrets { for _, secret := range secrets {
fmt.Fprintf(w, "%s\t%s\t%s\n", secret.ID, secret.Name, secret.Description) // nolint:errcheck fmt.Fprintf(w, "%s\t%s\t%s\n", secret.ID, secret.Name, secret.Description) // nolint:errcheck
} }
}, "ID", "NAME", "DESCRIPTION") }, "ID", "NAME", "DESCRIPTION")
case formatter.JSON:
outJSON, err := formatter.ToStandardJSON(secrets)
if err != nil {
return err
} }
_, _ = fmt.Fprint(out, outJSON)
func printSection(out io.Writer, printer func(io.Writer), headers ...string) { default:
w := tabwriter.NewWriter(out, 20, 1, 3, ' ', 0) err = errors.Wrapf(errdefs.ErrParsingFailed, "format value %q could not be parsed", format)
fmt.Fprintln(w, strings.Join(headers, "\t")) // nolint:errcheck }
printer(w) return err
w.Flush() // nolint:errcheck
} }

View File

@ -20,13 +20,15 @@ import (
"bytes" "bytes"
"testing" "testing"
"gotest.tools/assert"
"gotest.tools/v3/golden" "gotest.tools/v3/golden"
"github.com/docker/compose-cli/api/secrets" "github.com/docker/compose-cli/api/secrets"
"github.com/docker/compose-cli/formatter"
) )
func TestPrintList(t *testing.T) { func TestPrintList(t *testing.T) {
secrets := []secrets.Secret{ secretList := []secrets.Secret{
{ {
ID: "123", ID: "123",
Name: "secret123", Name: "secret123",
@ -34,6 +36,10 @@ func TestPrintList(t *testing.T) {
}, },
} }
out := &bytes.Buffer{} out := &bytes.Buffer{}
printList(out, secrets) assert.NilError(t, printSecretList(formatter.PRETTY, out, secretList))
golden.Assert(t, out.String(), "secrets-out.golden") golden.Assert(t, out.String(), "secrets-out.golden")
out.Reset()
assert.NilError(t, printSecretList(formatter.JSON, out, secretList))
golden.Assert(t, out.String(), "secrets-out-json.golden")
} }

View File

@ -0,0 +1,8 @@
[
{
"ID": "123",
"Name": "secret123",
"Labels": null,
"Description": "secret 1,2,3"
}
]

View File

@ -18,43 +18,94 @@ package cmd
import ( import (
"fmt" "fmt"
"os"
"strings" "strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/compose-cli/cli/cmd/mobyflags" "github.com/docker/compose-cli/cli/cmd/mobyflags"
"github.com/docker/compose-cli/cli/mobycli" "github.com/docker/compose-cli/cli/mobycli"
"github.com/docker/compose-cli/formatter"
) )
const formatOpt = "format"
// VersionCommand command to display version // VersionCommand command to display version
func VersionCommand(version string) *cobra.Command { func VersionCommand(version string) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "version", Use: "version",
Short: "Show the Docker version information", Short: "Show the Docker version information",
Args: cobra.MaximumNArgs(0), Args: cobra.MaximumNArgs(0),
RunE: func(cmd *cobra.Command, _ []string) error { Run: func(cmd *cobra.Command, _ []string) {
return runVersion(cmd, version) runVersion(cmd, version)
}, },
} }
// define flags for backward compatibility with com.docker.cli // define flags for backward compatibility with com.docker.cli
flags := cmd.Flags() flags := cmd.Flags()
flags.StringP("format", "f", "", "Format the output using the given Go template") flags.StringP(formatOpt, "f", "", "Format the output using the given Go template")
// flags.String(&opts.format, "format", "", "Format the output. Values: [pretty | json | go template]. (Default: pretty)")
flags.String("kubeconfig", "", "Kubernetes config file") flags.String("kubeconfig", "", "Kubernetes config file")
mobyflags.AddMobyFlagsForRetrocompatibility(flags) mobyflags.AddMobyFlagsForRetrocompatibility(flags)
return cmd return cmd
} }
func runVersion(cmd *cobra.Command, version string) error { func runVersion(cmd *cobra.Command, version string) {
var versionString string
format := strings.TrimSpace(cmd.Flag(formatOpt).Value.String())
displayedVersion := strings.TrimPrefix(version, "v") 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 .}}", "{{json . }}", "{{ 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)
}
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 // 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 // Still, technically the [] byte versionResult could be nil, just let the original command display what it has to display
if versionResult == nil { if versionResult == nil {
mobycli.Exec(cmd.Root()) mobycli.Exec(cmd.Root())
return nil return ""
} }
var s string = string(versionResult) return string(versionResult)
fmt.Print(strings.Replace(s, "\n Version:", "\n Cloud integration "+displayedVersion+"\n Version:", 1)) }
return nil
func fixedPrettyArgs(oArgs []string) []string {
var args []string
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 {
var args []string
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"
} }

190
cli/cmd/version_test.go Normal file
View File

@ -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: nil,
},
{
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: nil,
},
{
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: nil,
},
}
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)
}
})
}

View File

@ -21,15 +21,22 @@ import (
"io" "io"
"os" "os"
"strings" "strings"
"text/tabwriter"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/client"
"github.com/docker/compose-cli/api/volumes" "github.com/docker/compose-cli/api/volumes"
"github.com/docker/compose-cli/errdefs"
"github.com/docker/compose-cli/formatter"
) )
type listVolumeOpts struct {
format string
}
func listVolume() *cobra.Command { func listVolume() *cobra.Command {
var opts listVolumeOpts
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "ls", Use: "ls",
Short: "list available volumes in context.", Short: "list available volumes in context.",
@ -43,24 +50,30 @@ func listVolume() *cobra.Command {
if err != nil { if err != nil {
return err return err
} }
printList(os.Stdout, vols) return printList(opts.format, os.Stdout, vols)
return nil
}, },
} }
cmd.Flags().StringVar(&opts.format, "format", formatter.PRETTY, "Format the output. Values: [pretty | json]. (Default: pretty)")
return cmd return cmd
} }
func printList(out io.Writer, volumes []volumes.Volume) { func printList(format string, out io.Writer, volumes []volumes.Volume) error {
printSection(out, func(w io.Writer) { var err error
switch strings.ToLower(format) {
case formatter.PRETTY, "":
_ = formatter.PrintPrettySection(out, func(w io.Writer) {
for _, vol := range volumes { for _, vol := range volumes {
_, _ = fmt.Fprintf(w, "%s\t%s\n", vol.ID, vol.Description) _, _ = fmt.Fprintf(w, "%s\t%s\n", vol.ID, vol.Description)
} }
}, "ID", "DESCRIPTION") }, "ID", "DESCRIPTION")
case formatter.JSON:
outJSON, err := formatter.ToStandardJSON(volumes)
if err != nil {
return err
} }
_, _ = fmt.Fprint(out, outJSON)
func printSection(out io.Writer, printer func(io.Writer), headers ...string) { default:
w := tabwriter.NewWriter(out, 20, 1, 3, ' ', 0) err = errors.Wrapf(errdefs.ErrParsingFailed, "format value %q could not be parsed", format)
_, _ = fmt.Fprintln(w, strings.Join(headers, "\t")) }
printer(w) return err
_ = w.Flush()
} }

View File

@ -20,9 +20,11 @@ import (
"bytes" "bytes"
"testing" "testing"
"gotest.tools/assert"
"gotest.tools/v3/golden" "gotest.tools/v3/golden"
"github.com/docker/compose-cli/api/volumes" "github.com/docker/compose-cli/api/volumes"
"github.com/docker/compose-cli/formatter"
) )
func TestPrintList(t *testing.T) { func TestPrintList(t *testing.T) {
@ -33,6 +35,11 @@ func TestPrintList(t *testing.T) {
}, },
} }
out := &bytes.Buffer{} out := &bytes.Buffer{}
printList(out, secrets) assert.NilError(t, printList(formatter.PRETTY, out, secrets))
golden.Assert(t, out.String(), "volumes-out.golden") golden.Assert(t, out.String(), "volumes-out.golden")
out.Reset()
assert.NilError(t, printList(formatter.JSON, out, secrets))
golden.Assert(t, out.String(), "volumes-out-json.golden")
} }

View File

@ -0,0 +1,6 @@
[
{
"ID": "volume/123",
"Description": "volume 123"
}
]

View File

@ -112,7 +112,10 @@ func IsDefaultContextCommand(dockerCommand string) bool {
} }
// ExecSilent executes a command and do redirect output to stdOut, return output // ExecSilent executes a command and do redirect output to stdOut, return output
func ExecSilent(ctx context.Context) ([]byte, error) { func ExecSilent(ctx context.Context, args ...string) ([]byte, error) {
cmd := exec.CommandContext(ctx, ComDockerCli, os.Args[1:]...) if len(args) == 0 {
args = os.Args[1:]
}
cmd := exec.CommandContext(ctx, ComDockerCli, args...)
return cmd.CombinedOutput() return cmd.CombinedOutput()
} }

24
formatter/consts.go Normal file
View File

@ -0,0 +1,24 @@
/*
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
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"
)

32
formatter/pretty.go Normal file
View File

@ -0,0 +1,32 @@
/*
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"
"strings"
"text/tabwriter"
)
// 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()
}

1
go.mod
View File

@ -62,5 +62,6 @@ require (
google.golang.org/protobuf v1.25.0 google.golang.org/protobuf v1.25.0
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/ini.v1 v1.61.0 gopkg.in/ini.v1 v1.61.0
gotest.tools v2.2.0+incompatible
gotest.tools/v3 v3.0.2 gotest.tools/v3 v3.0.2
) )

View File

@ -75,6 +75,12 @@ func TestContextDefault(t *testing.T) {
t.Run("ls", func(t *testing.T) { t.Run("ls", func(t *testing.T) {
res := c.RunDockerCmd("context", "ls") res := c.RunDockerCmd("context", "ls")
golden.Assert(t, res.Stdout(), GoldenFile("ls-out-default")) 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) { t.Run("inspect", func(t *testing.T) {
@ -407,6 +413,26 @@ func TestVersion(t *testing.T) {
res.Assert(t, icmd.Expected{Out: `"Client":`}) 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) { t.Run("delegate version flag", func(t *testing.T) {
c.RunDockerCmd("context", "create", "example", "test-example") c.RunDockerCmd("context", "create", "example", "test-example")
c.RunDockerCmd("context", "use", "test-example") c.RunDockerCmd("context", "use", "test-example")
@ -431,6 +457,12 @@ func TestMockBackend(t *testing.T) {
t.Run("ps", func(t *testing.T) { t.Run("ps", func(t *testing.T) {
res := c.RunDockerCmd("ps") res := c.RunDockerCmd("ps")
golden.Assert(t, res.Stdout(), "ps-out-example.golden") 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) { t.Run("ps quiet", func(t *testing.T) {

16
tests/e2e/testdata/ls-out-json.golden vendored Normal file
View File

@ -0,0 +1,16 @@
[
{
"Name": "default",
"Metadata": {
"Description": "Current DOCKER_HOST based configuration",
"StackOrchestrator": "swarm",
"Type": "moby"
},
"Endpoints": {
"docker": {
"Host": "unix:///var/run/docker.sock"
},
"kubernetes": {}
}
}
]

View File

@ -0,0 +1,30 @@
[
{
"ID": "id",
"Status": "",
"Image": "nginx",
"Command": "",
"CPUTime": 0,
"CPULimit": 0,
"MemoryUsage": 0,
"MemoryLimit": 0,
"PidsCurrent": 0,
"PidsLimit": 0,
"Platform": "",
"RestartPolicyCondition": ""
},
{
"ID": "1234",
"Status": "",
"Image": "alpine",
"Command": "",
"CPUTime": 0,
"CPULimit": 0,
"MemoryUsage": 0,
"MemoryLimit": 0,
"PidsCurrent": 0,
"PidsLimit": 0,
"Platform": "",
"RestartPolicyCondition": ""
}
]