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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

View File

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

1
go.mod
View File

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

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.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=

View File

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

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
func PortsToStrings(ports []containers.Port, fqdn string) []string {
groupMap := make(map[string]*portGroup)
result := []string{}
var (
result []string
hostMappings []string
groupMapKeys []string
)