Merge pull request #689 from ulyssessouza/json-out

Add json output format to several commands
This commit is contained in:
Guillaume Tardif 2020-10-05 10:30:30 +02:00 committed by GitHub
commit 97576db803
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 665 additions and 174 deletions

View File

@ -53,7 +53,7 @@ func (cs *aciContainerService) List(ctx context.Context, all bool) ([]containers
if err != nil { if err != nil {
return nil, err return nil, err
} }
var res []containers.Container res := []containers.Container{}
for _, group := range containerGroups { for _, group := range containerGroups {
if group.Containers == nil || len(*group.Containers) == 0 { if group.Containers == nil || len(*group.Containers) == 0 {
return nil, fmt.Errorf("no containers found in ACI container group %s", *group.Name) return nil, fmt.Errorf("no containers found in ACI container group %s", *group.Name)

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

@ -23,8 +23,11 @@ import (
"os" "os"
"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/formatter"
) )
func listCommand() *cobra.Command { func listCommand() *cobra.Command {
@ -35,10 +38,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 +57,26 @@ func runList(ctx context.Context, opts composeOptions) error {
return err return err
} }
err = printSection(os.Stdout, func(w io.Writer) { view := viewFromStackList(stackList)
for _, stack := range stackList { return formatter.Print(view, opts.Format, os.Stdout, func(w io.Writer) {
fmt.Fprintf(w, "%s\t%s\n", stack.Name, stack.Status) for _, stack := range view {
_, _ = fmt.Fprintf(w, "%s\t%s\n", stack.Name, stack.Status)
} }
}, "NAME", "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
} }

View File

@ -22,11 +22,12 @@ import (
"io" "io"
"os" "os"
"strings" "strings"
"text/tabwriter"
"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/formatter"
) )
func psCommand() *cobra.Command { func psCommand() *cobra.Command {
@ -37,10 +38,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 +59,34 @@ func runPs(ctx context.Context, opts composeOptions) error {
return err return err
} }
err = printSection(os.Stdout, func(w io.Writer) { view := viewFromServiceStatusList(serviceList)
for _, service := range serviceList { return formatter.Print(view, opts.Format, os.Stdout,
fmt.Fprintf(w, "%s\t%s\t%d/%d\t%s\n", service.ID, service.Name, service.Replicas, service.Desired, strings.Join(service.Ports, ", ")) 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") },
return err "ID", "NAME", "REPLICAS", "PORTS")
} }
func printSection(out io.Writer, printer func(io.Writer), headers ...string) error { type serviceStatusView struct {
w := tabwriter.NewWriter(out, 20, 1, 3, ' ', 0) ID string
fmt.Fprintln(w, strings.Join(headers, "\t")) Name string
printer(w) Replicas int
return w.Flush() 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
} }

View File

@ -17,13 +17,13 @@
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"
@ -58,7 +58,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]. (Default: pretty)")
_ = cmd.Flags().MarkHidden("json")
return cmd return cmd
} }
@ -68,7 +69,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,35 +94,27 @@ 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) view := viewFromContextList(contexts, currentContext)
fmt.Fprintln(w, "NAME\tTYPE\tDESCRIPTION\tDOCKER ENDPOINT\tKUBERNETES ENDPOINT\tORCHESTRATOR") return formatter.Print(view, opts.format, os.Stdout,
format := "%s\t%s\t%s\t%s\t%s\t%s\n" func(w io.Writer) {
for _, c := range view {
for _, c := range contexts {
contextName := c.Name contextName := c.Name
if c.Name == currentContext { if c.Current {
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.Description,
getEndpoint("docker", c.Endpoints), c.DockerEndpoint,
getEndpoint("kubernetes", c.Endpoints), c.KubernetesEndpoint,
c.Metadata.StackOrchestrator) c.StackOrchestrator)
} }
},
return w.Flush() "NAME", "TYPE", "DESCRIPTION", "DOCKER ENDPOINT", "KUBERNETES ENDPOINT", "ORCHESTRATOR")
} }
func getEndpoint(name string, meta map[string]interface{}) string { func getEndpoint(name string, meta map[string]interface{}) string {
@ -141,3 +134,29 @@ func getEndpoint(name string, meta map[string]interface{}) string {
return result 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
}

View File

@ -56,7 +56,7 @@ func runInspect(ctx context.Context, id string) error {
if err != nil { if err != nil {
return err return err
} }
fmt.Println(j) fmt.Print(j)
return nil return nil
} }

View File

@ -19,9 +19,9 @@ 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"
@ -36,13 +36,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 +53,52 @@ 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]. (Default: pretty)")
_ = cmd.Flags().MarkHidden("json") // Legacy. This is used by VSCode Docker extension
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 containers")
} }
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
if err != nil {
return err
}
fmt.Println(j)
return nil
} }
w := tabwriter.NewWriter(os.Stdout, 20, 1, 3, ' ', 0) view := viewFromContainerList(containerList)
fmt.Fprintf(w, "CONTAINER ID\tIMAGE\tCOMMAND\tSTATUS\tPORTS\n") return formatter2.Print(view, opts.format, os.Stdout, func(w io.Writer) {
format := "%s\t%s\t%s\t%s\t%s\n" for _, c := range view {
for _, container := range containers { _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", c.ID, c.Image, c.Command, c.Status,
fmt.Fprintf(w, format, container.ID, container.Image, container.Command, container.Status, strings.Join(formatter.PortsToStrings(container.Ports, fqdn(container)), ", ")) strings.Join(c.Ports, ", "))
} }
}, "CONTAINER ID", "IMAGE", "COMMAND", "STATUS", "PORTS")
return w.Flush()
} }
func fqdn(container containers.Container) string { func fqdn(container containers.Container) string {
@ -112,3 +108,25 @@ func fqdn(container containers.Container) string {
} }
return fqdn 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
}

View File

@ -20,13 +20,12 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"strings"
"text/tabwriter"
"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/formatter"
) )
type createSecretOptions struct { type createSecretOptions struct {
@ -105,7 +104,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"},
@ -115,17 +119,40 @@ func listSecrets() *cobra.Command {
if err != nil { if err != nil {
return err return err
} }
list, err := c.SecretsService().ListSecrets(cmd.Context()) secretsList, err := c.SecretsService().ListSecrets(cmd.Context())
if err != nil { if err != nil {
return err return err
} }
printList(os.Stdout, list) view := viewFromSecretList(secretsList)
return nil 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 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 { type deleteSecretOptions struct {
recover bool recover bool
} }
@ -148,18 +175,3 @@ func deleteSecret() *cobra.Command {
cmd.Flags().BoolVar(&opts.recover, "recover", false, "Enable recovery.") cmd.Flags().BoolVar(&opts.recover, "recover", false, "Enable recovery.")
return cmd 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
}

View File

@ -18,43 +18,96 @@ 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. Values: [pretty | json]. (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.ToLower(strings.ReplaceAll(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.}}": // 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 // 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 {
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"
} }

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: []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)
}
})
}

View File

@ -20,16 +20,20 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"strings"
"text/tabwriter"
"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/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 +47,30 @@ func listVolume() *cobra.Command {
if err != nil { if err != nil {
return err return err
} }
printList(os.Stdout, vols) view := viewFromVolumeList(vols)
return nil return formatter.Print(view, opts.format, os.Stdout, func(w io.Writer) {
}, for _, vol := range view {
}
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) _, _ = fmt.Fprintf(w, "%s\t%s\n", vol.ID, vol.Description)
} }
}, "ID", "DESCRIPTION") }, "ID", "DESCRIPTION")
},
}
cmd.Flags().StringVar(&opts.format, "format", formatter.PRETTY, "Format the output. Values: [pretty | json]. (Default: pretty)")
return cmd
} }
func printSection(out io.Writer, printer func(io.Writer), headers ...string) { type volumeView struct {
w := tabwriter.NewWriter(out, 20, 1, 3, ' ', 0) ID string
_, _ = fmt.Fprintln(w, strings.Join(headers, "\t")) Description string
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
} }

View File

@ -1,2 +0,0 @@
ID DESCRIPTION
volume/123 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()
} }

View File

@ -14,26 +14,11 @@
limitations under the License. limitations under the License.
*/ */
package cmd package formatter
import ( const (
"bytes" // JSON is the constant for Json formats on list commands
"testing" JSON = "json"
// PRETTY is the constant for default formats on list commands
"gotest.tools/v3/golden" PRETTY = "pretty"
"github.com/docker/compose-cli/api/secrets"
) )
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")
}

58
formatter/formatter.go Normal file
View File

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

View File

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

View File

@ -16,15 +16,24 @@
package formatter package formatter
import "encoding/json" import (
"bytes"
"encoding/json"
)
const standardIndentation = " " const standardIndentation = " "
// ToStandardJSON return a string with the JSON representation of the interface{} // ToStandardJSON return a string with the JSON representation of the interface{}
func ToStandardJSON(i interface{}) (string, error) { func ToStandardJSON(i interface{}) (string, error) {
b, err := json.MarshalIndent(i, "", standardIndentation) return ToJSON(i, "", standardIndentation)
if err != nil {
return "", err
} }
return string(b), nil
// 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
} }

View File

@ -14,25 +14,19 @@
limitations under the License. limitations under the License.
*/ */
package volume package formatter
import ( import (
"bytes" "fmt"
"testing" "io"
"strings"
"gotest.tools/v3/golden" "text/tabwriter"
"github.com/docker/compose-cli/api/volumes"
) )
func TestPrintList(t *testing.T) { // PrintPrettySection prints a tabbed section on the writer parameter
secrets := []volumes.Volume{ func PrintPrettySection(out io.Writer, printer func(writer io.Writer), headers ...string) error {
{ w := tabwriter.NewWriter(out, 20, 1, 3, ' ', 0)
ID: "volume/123", _, _ = fmt.Fprintln(w, strings.Join(headers, "\t"))
Description: "volume 123", printer(w)
}, return w.Flush()
}
out := &bytes.Buffer{}
printList(out, secrets)
golden.Assert(t, out.String(), "volumes-out.golden")
} }

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

1
go.sum
View File

@ -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.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/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/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= 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-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=

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) {
@ -416,6 +422,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")
@ -440,6 +466,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) {

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

@ -0,0 +1 @@
{"Current":true,"Description":"Current DOCKER_HOST based configuration","DockerEndpoint":"unix:///var/run/docker.sock","KubernetesEndpoint":"","Type":"moby","Name":"default","StackOrchestrator":"swarm"}

View File

@ -0,0 +1,2 @@
{"ID":"id","Image":"nginx","Status":"","Command":"","Ports":[]}
{"ID":"1234","Image":"alpine","Status":"","Command":"","Ports":[]}

View File

@ -33,8 +33,8 @@ type portGroup struct {
// PortsToStrings returns a human readable published ports // PortsToStrings returns a human readable published ports
func PortsToStrings(ports []containers.Port, fqdn string) []string { func PortsToStrings(ports []containers.Port, fqdn string) []string {
groupMap := make(map[string]*portGroup) groupMap := make(map[string]*portGroup)
result := []string{}
var ( var (
result []string
hostMappings []string hostMappings []string
groupMapKeys []string groupMapKeys []string
) )