formatter package heavy refactoring

- make it possible to extract the formatter implementation from the
  "common" code, that way, the formatter package stays small
- extract some formatter into their own packages

This is essentially moving the "formatter" implementation of each type
in their respective packages. The *main* reason to do that, is to be
able to depend on `cli/command/formatter` without depending of the
implementation detail of the formatter. As of now, depending on
`cli/command/formatter` means we depend on `docker/docker/api/types`,
`docker/licensing`, … — that should not be the case. `formatter`
should hold the common code (or helpers) to easily create formatter,
not all formatter implementations.

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
This commit is contained in:
Vincent Demeester 2018-10-23 17:05:44 +02:00
parent ea836abed5
commit 69fdd2a4ad
No known key found for this signature in database
GPG Key ID: 083CC6FD6EB699A3
75 changed files with 750 additions and 711 deletions

View File

@ -1,6 +1,9 @@
package formatter package checkpoint
import "github.com/docker/docker/api/types" import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types"
)
const ( const (
defaultCheckpointFormat = "table {{.Name}}" defaultCheckpointFormat = "table {{.Name}}"
@ -8,18 +11,18 @@ const (
checkpointNameHeader = "CHECKPOINT NAME" checkpointNameHeader = "CHECKPOINT NAME"
) )
// NewCheckpointFormat returns a format for use with a checkpoint Context // NewFormat returns a format for use with a checkpoint Context
func NewCheckpointFormat(source string) Format { func NewFormat(source string) formatter.Format {
switch source { switch source {
case TableFormatKey: case formatter.TableFormatKey:
return defaultCheckpointFormat return defaultCheckpointFormat
} }
return Format(source) return formatter.Format(source)
} }
// CheckpointWrite writes formatted checkpoints using the Context // FormatWrite writes formatted checkpoints using the Context
func CheckpointWrite(ctx Context, checkpoints []types.Checkpoint) error { func FormatWrite(ctx formatter.Context, checkpoints []types.Checkpoint) error {
render := func(format func(subContext subContext) error) error { render := func(format func(subContext formatter.SubContext) error) error {
for _, checkpoint := range checkpoints { for _, checkpoint := range checkpoints {
if err := format(&checkpointContext{c: checkpoint}); err != nil { if err := format(&checkpointContext{c: checkpoint}); err != nil {
return err return err
@ -31,20 +34,20 @@ func CheckpointWrite(ctx Context, checkpoints []types.Checkpoint) error {
} }
type checkpointContext struct { type checkpointContext struct {
HeaderContext formatter.HeaderContext
c types.Checkpoint c types.Checkpoint
} }
func newCheckpointContext() *checkpointContext { func newCheckpointContext() *checkpointContext {
cpCtx := checkpointContext{} cpCtx := checkpointContext{}
cpCtx.header = volumeHeaderContext{ cpCtx.Header = formatter.SubHeaderContext{
"Name": checkpointNameHeader, "Name": checkpointNameHeader,
} }
return &cpCtx return &cpCtx
} }
func (c *checkpointContext) MarshalJSON() ([]byte, error) { func (c *checkpointContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c) return formatter.MarshalJSON(c)
} }
func (c *checkpointContext) Name() string { func (c *checkpointContext) Name() string {

View File

@ -1,20 +1,21 @@
package formatter package checkpoint
import ( import (
"bytes" "bytes"
"testing" "testing"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"gotest.tools/assert" "gotest.tools/assert"
) )
func TestCheckpointContextFormatWrite(t *testing.T) { func TestCheckpointContextFormatWrite(t *testing.T) {
cases := []struct { cases := []struct {
context Context context formatter.Context
expected string expected string
}{ }{
{ {
Context{Format: NewCheckpointFormat(defaultCheckpointFormat)}, formatter.Context{Format: NewFormat(defaultCheckpointFormat)},
`CHECKPOINT NAME `CHECKPOINT NAME
checkpoint-1 checkpoint-1
checkpoint-2 checkpoint-2
@ -22,14 +23,14 @@ checkpoint-3
`, `,
}, },
{ {
Context{Format: NewCheckpointFormat("{{.Name}}")}, formatter.Context{Format: NewFormat("{{.Name}}")},
`checkpoint-1 `checkpoint-1
checkpoint-2 checkpoint-2
checkpoint-3 checkpoint-3
`, `,
}, },
{ {
Context{Format: NewCheckpointFormat("{{.Name}}:")}, formatter.Context{Format: NewFormat("{{.Name}}:")},
`checkpoint-1: `checkpoint-1:
checkpoint-2: checkpoint-2:
checkpoint-3: checkpoint-3:
@ -45,7 +46,7 @@ checkpoint-3:
for _, testcase := range cases { for _, testcase := range cases {
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
testcase.context.Output = out testcase.context.Output = out
err := CheckpointWrite(testcase.context, checkpoints) err := FormatWrite(testcase.context, checkpoints)
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, out.String(), testcase.expected) assert.Equal(t, out.String(), testcase.expected)
} }

View File

@ -48,7 +48,7 @@ func runList(dockerCli command.Cli, container string, opts listOptions) error {
cpCtx := formatter.Context{ cpCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCli.Out(),
Format: formatter.NewCheckpointFormat(formatter.TableFormatKey), Format: NewFormat(formatter.TableFormatKey),
} }
return formatter.CheckpointWrite(cpCtx, checkpoints) return FormatWrite(cpCtx, checkpoints)
} }

View File

@ -1,4 +1,4 @@
package formatter package config
import ( import (
"fmt" "fmt"
@ -6,17 +6,18 @@ import (
"time" "time"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/inspect" "github.com/docker/cli/cli/command/inspect"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
units "github.com/docker/go-units" units "github.com/docker/go-units"
) )
const ( const (
defaultConfigTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}" defaultConfigTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}"
configIDHeader = "ID" configIDHeader = "ID"
configCreatedHeader = "CREATED" configCreatedHeader = "CREATED"
configUpdatedHeader = "UPDATED" configUpdatedHeader = "UPDATED"
configInspectPrettyTemplate Format = `ID: {{.ID}} configInspectPrettyTemplate formatter.Format = `ID: {{.ID}}
Name: {{.Name}} Name: {{.Name}}
{{- if .Labels }} {{- if .Labels }}
Labels: Labels:
@ -29,23 +30,23 @@ Data:
{{.Data}}` {{.Data}}`
) )
// NewConfigFormat returns a Format for rendering using a config Context // NewFormat returns a Format for rendering using a config Context
func NewConfigFormat(source string, quiet bool) Format { func NewFormat(source string, quiet bool) formatter.Format {
switch source { switch source {
case PrettyFormatKey: case formatter.PrettyFormatKey:
return configInspectPrettyTemplate return configInspectPrettyTemplate
case TableFormatKey: case formatter.TableFormatKey:
if quiet { if quiet {
return defaultQuietFormat return formatter.DefaultQuietFormat
} }
return defaultConfigTableFormat return defaultConfigTableFormat
} }
return Format(source) return formatter.Format(source)
} }
// ConfigWrite writes the context // FormatWrite writes the context
func ConfigWrite(ctx Context, configs []swarm.Config) error { func FormatWrite(ctx formatter.Context, configs []swarm.Config) error {
render := func(format func(subContext subContext) error) error { render := func(format func(subContext formatter.SubContext) error) error {
for _, config := range configs { for _, config := range configs {
configCtx := &configContext{c: config} configCtx := &configContext{c: config}
if err := format(configCtx); err != nil { if err := format(configCtx); err != nil {
@ -60,23 +61,23 @@ func ConfigWrite(ctx Context, configs []swarm.Config) error {
func newConfigContext() *configContext { func newConfigContext() *configContext {
cCtx := &configContext{} cCtx := &configContext{}
cCtx.header = map[string]string{ cCtx.Header = formatter.SubHeaderContext{
"ID": configIDHeader, "ID": configIDHeader,
"Name": nameHeader, "Name": formatter.NameHeader,
"CreatedAt": configCreatedHeader, "CreatedAt": configCreatedHeader,
"UpdatedAt": configUpdatedHeader, "UpdatedAt": configUpdatedHeader,
"Labels": labelsHeader, "Labels": formatter.LabelsHeader,
} }
return cCtx return cCtx
} }
type configContext struct { type configContext struct {
HeaderContext formatter.HeaderContext
c swarm.Config c swarm.Config
} }
func (c *configContext) MarshalJSON() ([]byte, error) { func (c *configContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c) return formatter.MarshalJSON(c)
} }
func (c *configContext) ID() string { func (c *configContext) ID() string {
@ -114,12 +115,12 @@ func (c *configContext) Label(name string) string {
return c.c.Spec.Annotations.Labels[name] return c.c.Spec.Annotations.Labels[name]
} }
// ConfigInspectWrite renders the context for a list of configs // InspectFormatWrite renders the context for a list of configs
func ConfigInspectWrite(ctx Context, refs []string, getRef inspect.GetRefFunc) error { func InspectFormatWrite(ctx formatter.Context, refs []string, getRef inspect.GetRefFunc) error {
if ctx.Format != configInspectPrettyTemplate { if ctx.Format != configInspectPrettyTemplate {
return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef) return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef)
} }
render := func(format func(subContext subContext) error) error { render := func(format func(subContext formatter.SubContext) error) error {
for _, ref := range refs { for _, ref := range refs {
configI, _, err := getRef(ref) configI, _, err := getRef(ref)
if err != nil { if err != nil {
@ -140,7 +141,7 @@ func ConfigInspectWrite(ctx Context, refs []string, getRef inspect.GetRefFunc) e
type configInspectContext struct { type configInspectContext struct {
swarm.Config swarm.Config
subContext formatter.SubContext
} }
func (ctx *configInspectContext) ID() string { func (ctx *configInspectContext) ID() string {

View File

@ -1,10 +1,11 @@
package formatter package config
import ( import (
"bytes" "bytes"
"testing" "testing"
"time" "time"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"gotest.tools/assert" "gotest.tools/assert"
is "gotest.tools/assert/cmp" is "gotest.tools/assert/cmp"
@ -13,32 +14,32 @@ import (
func TestConfigContextFormatWrite(t *testing.T) { func TestConfigContextFormatWrite(t *testing.T) {
// Check default output format (verbose and non-verbose mode) for table headers // Check default output format (verbose and non-verbose mode) for table headers
cases := []struct { cases := []struct {
context Context context formatter.Context
expected string expected string
}{ }{
// Errors // Errors
{ {
Context{Format: "{{InvalidFunction}}"}, formatter.Context{Format: "{{InvalidFunction}}"},
`Template parsing error: template: :1: function "InvalidFunction" not defined `Template parsing error: template: :1: function "InvalidFunction" not defined
`, `,
}, },
{ {
Context{Format: "{{nil}}"}, formatter.Context{Format: "{{nil}}"},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`, `,
}, },
// Table format // Table format
{Context{Format: NewConfigFormat("table", false)}, {formatter.Context{Format: NewFormat("table", false)},
`ID NAME CREATED UPDATED `ID NAME CREATED UPDATED
1 passwords Less than a second ago Less than a second ago 1 passwords Less than a second ago Less than a second ago
2 id_rsa Less than a second ago Less than a second ago 2 id_rsa Less than a second ago Less than a second ago
`}, `},
{Context{Format: NewConfigFormat("table {{.Name}}", true)}, {formatter.Context{Format: NewFormat("table {{.Name}}", true)},
`NAME `NAME
passwords passwords
id_rsa id_rsa
`}, `},
{Context{Format: NewConfigFormat("{{.ID}}-{{.Name}}", false)}, {formatter.Context{Format: NewFormat("{{.ID}}-{{.Name}}", false)},
`1-passwords `1-passwords
2-id_rsa 2-id_rsa
`}, `},
@ -55,7 +56,7 @@ id_rsa
for _, testcase := range cases { for _, testcase := range cases {
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
testcase.context.Output = out testcase.context.Output = out
if err := ConfigWrite(testcase.context, configs); err != nil { if err := FormatWrite(testcase.context, configs); err != nil {
assert.ErrorContains(t, err, testcase.expected) assert.ErrorContains(t, err, testcase.expected)
} else { } else {
assert.Check(t, is.Equal(out.String(), testcase.expected)) assert.Check(t, is.Equal(out.String(), testcase.expected))

View File

@ -55,10 +55,10 @@ func runConfigInspect(dockerCli command.Cli, opts inspectOptions) error {
configCtx := formatter.Context{ configCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCli.Out(),
Format: formatter.NewConfigFormat(f, false), Format: NewFormat(f, false),
} }
if err := formatter.ConfigInspectWrite(configCtx, opts.names, getRef); err != nil { if err := InspectFormatWrite(configCtx, opts.names, getRef); err != nil {
return cli.StatusError{StatusCode: 1, Status: err.Error()} return cli.StatusError{StatusCode: 1, Status: err.Error()}
} }
return nil return nil

View File

@ -64,7 +64,7 @@ func runConfigList(dockerCli command.Cli, options listOptions) error {
configCtx := formatter.Context{ configCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCli.Out(),
Format: formatter.NewConfigFormat(format, options.quiet), Format: NewFormat(format, options.quiet),
} }
return formatter.ConfigWrite(configCtx, configs) return FormatWrite(configCtx, configs)
} }

View File

@ -41,7 +41,7 @@ func runDiff(dockerCli command.Cli, opts *diffOptions) error {
} }
diffCtx := formatter.Context{ diffCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCli.Out(),
Format: formatter.NewDiffFormat("{{.Type}} {{.Path}}"), Format: NewDiffFormat("{{.Type}} {{.Path}}"),
} }
return formatter.DiffWrite(diffCtx, changes) return DiffFormatWrite(diffCtx, changes)
} }

View File

@ -1,6 +1,7 @@
package formatter package container
import ( import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/archive"
) )
@ -13,18 +14,18 @@ const (
) )
// NewDiffFormat returns a format for use with a diff Context // NewDiffFormat returns a format for use with a diff Context
func NewDiffFormat(source string) Format { func NewDiffFormat(source string) formatter.Format {
switch source { switch source {
case TableFormatKey: case formatter.TableFormatKey:
return defaultDiffTableFormat return defaultDiffTableFormat
} }
return Format(source) return formatter.Format(source)
} }
// DiffWrite writes formatted diff using the Context // DiffFormatWrite writes formatted diff using the Context
func DiffWrite(ctx Context, changes []container.ContainerChangeResponseItem) error { func DiffFormatWrite(ctx formatter.Context, changes []container.ContainerChangeResponseItem) error {
render := func(format func(subContext subContext) error) error { render := func(format func(subContext formatter.SubContext) error) error {
for _, change := range changes { for _, change := range changes {
if err := format(&diffContext{c: change}); err != nil { if err := format(&diffContext{c: change}); err != nil {
return err return err
@ -36,13 +37,13 @@ func DiffWrite(ctx Context, changes []container.ContainerChangeResponseItem) err
} }
type diffContext struct { type diffContext struct {
HeaderContext formatter.HeaderContext
c container.ContainerChangeResponseItem c container.ContainerChangeResponseItem
} }
func newDiffContext() *diffContext { func newDiffContext() *diffContext {
diffCtx := diffContext{} diffCtx := diffContext{}
diffCtx.header = map[string]string{ diffCtx.Header = formatter.SubHeaderContext{
"Type": changeTypeHeader, "Type": changeTypeHeader,
"Path": pathHeader, "Path": pathHeader,
} }
@ -50,7 +51,7 @@ func newDiffContext() *diffContext {
} }
func (d *diffContext) MarshalJSON() ([]byte, error) { func (d *diffContext) MarshalJSON() ([]byte, error) {
return marshalJSON(d) return formatter.MarshalJSON(d)
} }
func (d *diffContext) Type() string { func (d *diffContext) Type() string {

View File

@ -1,9 +1,10 @@
package formatter package container
import ( import (
"bytes" "bytes"
"testing" "testing"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/archive"
"gotest.tools/assert" "gotest.tools/assert"
@ -13,11 +14,11 @@ import (
func TestDiffContextFormatWrite(t *testing.T) { func TestDiffContextFormatWrite(t *testing.T) {
// Check default output format (verbose and non-verbose mode) for table headers // Check default output format (verbose and non-verbose mode) for table headers
cases := []struct { cases := []struct {
context Context context formatter.Context
expected string expected string
}{ }{
{ {
Context{Format: NewDiffFormat("table")}, formatter.Context{Format: NewDiffFormat("table")},
`CHANGE TYPE PATH `CHANGE TYPE PATH
C /var/log/app.log C /var/log/app.log
A /usr/app/app.js A /usr/app/app.js
@ -25,7 +26,7 @@ D /usr/app/old_app.js
`, `,
}, },
{ {
Context{Format: NewDiffFormat("table {{.Path}}")}, formatter.Context{Format: NewDiffFormat("table {{.Path}}")},
`PATH `PATH
/var/log/app.log /var/log/app.log
/usr/app/app.js /usr/app/app.js
@ -33,7 +34,7 @@ D /usr/app/old_app.js
`, `,
}, },
{ {
Context{Format: NewDiffFormat("{{.Type}}: {{.Path}}")}, formatter.Context{Format: NewDiffFormat("{{.Type}}: {{.Path}}")},
`C: /var/log/app.log `C: /var/log/app.log
A: /usr/app/app.js A: /usr/app/app.js
D: /usr/app/old_app.js D: /usr/app/old_app.js
@ -50,7 +51,7 @@ D: /usr/app/old_app.js
for _, testcase := range cases { for _, testcase := range cases {
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
testcase.context.Output = out testcase.context.Output = out
err := DiffWrite(testcase.context, diffs) err := DiffFormatWrite(testcase.context, diffs)
if err != nil { if err != nil {
assert.Error(t, err, testcase.expected) assert.Error(t, err, testcase.expected)
} else { } else {

View File

@ -1,9 +1,10 @@
package formatter package container
import ( import (
"fmt" "fmt"
"sync" "sync"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/stringid"
units "github.com/docker/go-units" units "github.com/docker/go-units"
) )
@ -40,8 +41,8 @@ type StatsEntry struct {
IsInvalid bool IsInvalid bool
} }
// ContainerStats represents an entity to store containers statistics synchronously // Stats represents an entity to store containers statistics synchronously
type ContainerStats struct { type Stats struct {
mutex sync.Mutex mutex sync.Mutex
StatsEntry StatsEntry
err error err error
@ -49,7 +50,7 @@ type ContainerStats struct {
// GetError returns the container statistics error. // GetError returns the container statistics error.
// This is used to determine whether the statistics are valid or not // This is used to determine whether the statistics are valid or not
func (cs *ContainerStats) GetError() error { func (cs *Stats) GetError() error {
cs.mutex.Lock() cs.mutex.Lock()
defer cs.mutex.Unlock() defer cs.mutex.Unlock()
return cs.err return cs.err
@ -57,7 +58,7 @@ func (cs *ContainerStats) GetError() error {
// SetErrorAndReset zeroes all the container statistics and store the error. // SetErrorAndReset zeroes all the container statistics and store the error.
// It is used when receiving time out error during statistics collecting to reduce lock overhead // It is used when receiving time out error during statistics collecting to reduce lock overhead
func (cs *ContainerStats) SetErrorAndReset(err error) { func (cs *Stats) SetErrorAndReset(err error) {
cs.mutex.Lock() cs.mutex.Lock()
defer cs.mutex.Unlock() defer cs.mutex.Unlock()
cs.CPUPercentage = 0 cs.CPUPercentage = 0
@ -74,7 +75,7 @@ func (cs *ContainerStats) SetErrorAndReset(err error) {
} }
// SetError sets container statistics error // SetError sets container statistics error
func (cs *ContainerStats) SetError(err error) { func (cs *Stats) SetError(err error) {
cs.mutex.Lock() cs.mutex.Lock()
defer cs.mutex.Unlock() defer cs.mutex.Unlock()
cs.err = err cs.err = err
@ -84,7 +85,7 @@ func (cs *ContainerStats) SetError(err error) {
} }
// SetStatistics set the container statistics // SetStatistics set the container statistics
func (cs *ContainerStats) SetStatistics(s StatsEntry) { func (cs *Stats) SetStatistics(s StatsEntry) {
cs.mutex.Lock() cs.mutex.Lock()
defer cs.mutex.Unlock() defer cs.mutex.Unlock()
s.Container = cs.Container s.Container = cs.Container
@ -92,38 +93,38 @@ func (cs *ContainerStats) SetStatistics(s StatsEntry) {
} }
// GetStatistics returns container statistics with other meta data such as the container name // GetStatistics returns container statistics with other meta data such as the container name
func (cs *ContainerStats) GetStatistics() StatsEntry { func (cs *Stats) GetStatistics() StatsEntry {
cs.mutex.Lock() cs.mutex.Lock()
defer cs.mutex.Unlock() defer cs.mutex.Unlock()
return cs.StatsEntry return cs.StatsEntry
} }
// NewStatsFormat returns a format for rendering an CStatsContext // NewStatsFormat returns a format for rendering an CStatsContext
func NewStatsFormat(source, osType string) Format { func NewStatsFormat(source, osType string) formatter.Format {
if source == TableFormatKey { if source == formatter.TableFormatKey {
if osType == winOSType { if osType == winOSType {
return Format(winDefaultStatsTableFormat) return formatter.Format(winDefaultStatsTableFormat)
} }
return Format(defaultStatsTableFormat) return formatter.Format(defaultStatsTableFormat)
} }
return Format(source) return formatter.Format(source)
} }
// NewContainerStats returns a new ContainerStats entity and sets in it the given name // NewStats returns a new Stats entity and sets in it the given name
func NewContainerStats(container string) *ContainerStats { func NewStats(container string) *Stats {
return &ContainerStats{StatsEntry: StatsEntry{Container: container}} return &Stats{StatsEntry: StatsEntry{Container: container}}
} }
// ContainerStatsWrite renders the context for a list of containers statistics // statsFormatWrite renders the context for a list of containers statistics
func ContainerStatsWrite(ctx Context, containerStats []StatsEntry, osType string, trunc bool) error { func statsFormatWrite(ctx formatter.Context, Stats []StatsEntry, osType string, trunc bool) error {
render := func(format func(subContext subContext) error) error { render := func(format func(subContext formatter.SubContext) error) error {
for _, cstats := range containerStats { for _, cstats := range Stats {
containerStatsCtx := &containerStatsContext{ statsCtx := &statsContext{
s: cstats, s: cstats,
os: osType, os: osType,
trunc: trunc, trunc: trunc,
} }
if err := format(containerStatsCtx); err != nil { if err := format(statsCtx); err != nil {
return err return err
} }
} }
@ -133,11 +134,11 @@ func ContainerStatsWrite(ctx Context, containerStats []StatsEntry, osType string
if osType == winOSType { if osType == winOSType {
memUsage = winMemUseHeader memUsage = winMemUseHeader
} }
containerStatsCtx := containerStatsContext{} statsCtx := statsContext{}
containerStatsCtx.header = map[string]string{ statsCtx.Header = formatter.SubHeaderContext{
"Container": containerHeader, "Container": containerHeader,
"Name": nameHeader, "Name": formatter.NameHeader,
"ID": containerIDHeader, "ID": formatter.ContainerIDHeader,
"CPUPerc": cpuPercHeader, "CPUPerc": cpuPercHeader,
"MemUsage": memUsage, "MemUsage": memUsage,
"MemPerc": memPercHeader, "MemPerc": memPercHeader,
@ -145,47 +146,47 @@ func ContainerStatsWrite(ctx Context, containerStats []StatsEntry, osType string
"BlockIO": blockIOHeader, "BlockIO": blockIOHeader,
"PIDs": pidsHeader, "PIDs": pidsHeader,
} }
containerStatsCtx.os = osType statsCtx.os = osType
return ctx.Write(&containerStatsCtx, render) return ctx.Write(&statsCtx, render)
} }
type containerStatsContext struct { type statsContext struct {
HeaderContext formatter.HeaderContext
s StatsEntry s StatsEntry
os string os string
trunc bool trunc bool
} }
func (c *containerStatsContext) MarshalJSON() ([]byte, error) { func (c *statsContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c) return formatter.MarshalJSON(c)
} }
func (c *containerStatsContext) Container() string { func (c *statsContext) Container() string {
return c.s.Container return c.s.Container
} }
func (c *containerStatsContext) Name() string { func (c *statsContext) Name() string {
if len(c.s.Name) > 1 { if len(c.s.Name) > 1 {
return c.s.Name[1:] return c.s.Name[1:]
} }
return "--" return "--"
} }
func (c *containerStatsContext) ID() string { func (c *statsContext) ID() string {
if c.trunc { if c.trunc {
return stringid.TruncateID(c.s.ID) return stringid.TruncateID(c.s.ID)
} }
return c.s.ID return c.s.ID
} }
func (c *containerStatsContext) CPUPerc() string { func (c *statsContext) CPUPerc() string {
if c.s.IsInvalid { if c.s.IsInvalid {
return fmt.Sprintf("--") return fmt.Sprintf("--")
} }
return fmt.Sprintf("%.2f%%", c.s.CPUPercentage) return fmt.Sprintf("%.2f%%", c.s.CPUPercentage)
} }
func (c *containerStatsContext) MemUsage() string { func (c *statsContext) MemUsage() string {
if c.s.IsInvalid { if c.s.IsInvalid {
return fmt.Sprintf("-- / --") return fmt.Sprintf("-- / --")
} }
@ -195,28 +196,28 @@ func (c *containerStatsContext) MemUsage() string {
return fmt.Sprintf("%s / %s", units.BytesSize(c.s.Memory), units.BytesSize(c.s.MemoryLimit)) return fmt.Sprintf("%s / %s", units.BytesSize(c.s.Memory), units.BytesSize(c.s.MemoryLimit))
} }
func (c *containerStatsContext) MemPerc() string { func (c *statsContext) MemPerc() string {
if c.s.IsInvalid || c.os == winOSType { if c.s.IsInvalid || c.os == winOSType {
return fmt.Sprintf("--") return fmt.Sprintf("--")
} }
return fmt.Sprintf("%.2f%%", c.s.MemoryPercentage) return fmt.Sprintf("%.2f%%", c.s.MemoryPercentage)
} }
func (c *containerStatsContext) NetIO() string { func (c *statsContext) NetIO() string {
if c.s.IsInvalid { if c.s.IsInvalid {
return fmt.Sprintf("--") return fmt.Sprintf("--")
} }
return fmt.Sprintf("%s / %s", units.HumanSizeWithPrecision(c.s.NetworkRx, 3), units.HumanSizeWithPrecision(c.s.NetworkTx, 3)) return fmt.Sprintf("%s / %s", units.HumanSizeWithPrecision(c.s.NetworkRx, 3), units.HumanSizeWithPrecision(c.s.NetworkTx, 3))
} }
func (c *containerStatsContext) BlockIO() string { func (c *statsContext) BlockIO() string {
if c.s.IsInvalid { if c.s.IsInvalid {
return fmt.Sprintf("--") return fmt.Sprintf("--")
} }
return fmt.Sprintf("%s / %s", units.HumanSizeWithPrecision(c.s.BlockRead, 3), units.HumanSizeWithPrecision(c.s.BlockWrite, 3)) return fmt.Sprintf("%s / %s", units.HumanSizeWithPrecision(c.s.BlockRead, 3), units.HumanSizeWithPrecision(c.s.BlockWrite, 3))
} }
func (c *containerStatsContext) PIDs() string { func (c *statsContext) PIDs() string {
if c.s.IsInvalid || c.os == winOSType { if c.s.IsInvalid || c.os == winOSType {
return fmt.Sprintf("--") return fmt.Sprintf("--")
} }

View File

@ -1,9 +1,10 @@
package formatter package container
import ( import (
"bytes" "bytes"
"testing" "testing"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/stringid"
"gotest.tools/assert" "gotest.tools/assert"
is "gotest.tools/assert/cmp" is "gotest.tools/assert/cmp"
@ -12,7 +13,7 @@ import (
func TestContainerStatsContext(t *testing.T) { func TestContainerStatsContext(t *testing.T) {
containerID := stringid.GenerateRandomID() containerID := stringid.GenerateRandomID()
var ctx containerStatsContext var ctx statsContext
tt := []struct { tt := []struct {
stats StatsEntry stats StatsEntry
osType string osType string
@ -39,7 +40,7 @@ func TestContainerStatsContext(t *testing.T) {
} }
for _, te := range tt { for _, te := range tt {
ctx = containerStatsContext{s: te.stats, os: te.osType} ctx = statsContext{s: te.stats, os: te.osType}
if v := te.call(); v != te.expValue { if v := te.call(); v != te.expValue {
t.Fatalf("Expected %q, got %q", te.expValue, v) t.Fatalf("Expected %q, got %q", te.expValue, v)
} }
@ -48,34 +49,34 @@ func TestContainerStatsContext(t *testing.T) {
func TestContainerStatsContextWrite(t *testing.T) { func TestContainerStatsContextWrite(t *testing.T) {
tt := []struct { tt := []struct {
context Context context formatter.Context
expected string expected string
}{ }{
{ {
Context{Format: "{{InvalidFunction}}"}, formatter.Context{Format: "{{InvalidFunction}}"},
`Template parsing error: template: :1: function "InvalidFunction" not defined `Template parsing error: template: :1: function "InvalidFunction" not defined
`, `,
}, },
{ {
Context{Format: "{{nil}}"}, formatter.Context{Format: "{{nil}}"},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`, `,
}, },
{ {
Context{Format: "table {{.MemUsage}}"}, formatter.Context{Format: "table {{.MemUsage}}"},
`MEM USAGE / LIMIT `MEM USAGE / LIMIT
20B / 20B 20B / 20B
-- / -- -- / --
`, `,
}, },
{ {
Context{Format: "{{.Container}} {{.ID}} {{.Name}}"}, formatter.Context{Format: "{{.Container}} {{.ID}} {{.Name}}"},
`container1 abcdef foo `container1 abcdef foo
container2 -- container2 --
`, `,
}, },
{ {
Context{Format: "{{.Container}} {{.CPUPerc}}"}, formatter.Context{Format: "{{.Container}} {{.CPUPerc}}"},
`container1 20.00% `container1 20.00%
container2 -- container2 --
`, `,
@ -115,7 +116,7 @@ container2 --
} }
var out bytes.Buffer var out bytes.Buffer
te.context.Output = &out te.context.Output = &out
err := ContainerStatsWrite(te.context, stats, "linux", false) err := statsFormatWrite(te.context, stats, "linux", false)
if err != nil { if err != nil {
assert.Error(t, err, te.expected) assert.Error(t, err, te.expected)
} else { } else {
@ -126,24 +127,24 @@ container2 --
func TestContainerStatsContextWriteWindows(t *testing.T) { func TestContainerStatsContextWriteWindows(t *testing.T) {
tt := []struct { tt := []struct {
context Context context formatter.Context
expected string expected string
}{ }{
{ {
Context{Format: "table {{.MemUsage}}"}, formatter.Context{Format: "table {{.MemUsage}}"},
`PRIV WORKING SET `PRIV WORKING SET
20B 20B
-- / -- -- / --
`, `,
}, },
{ {
Context{Format: "{{.Container}} {{.CPUPerc}}"}, formatter.Context{Format: "{{.Container}} {{.CPUPerc}}"},
`container1 20.00% `container1 20.00%
container2 -- container2 --
`, `,
}, },
{ {
Context{Format: "{{.Container}} {{.MemPerc}} {{.PIDs}}"}, formatter.Context{Format: "{{.Container}} {{.MemPerc}} {{.PIDs}}"},
`container1 -- -- `container1 -- --
container2 -- -- container2 -- --
`, `,
@ -181,7 +182,7 @@ container2 -- --
} }
var out bytes.Buffer var out bytes.Buffer
te.context.Output = &out te.context.Output = &out
err := ContainerStatsWrite(te.context, stats, "windows", false) err := statsFormatWrite(te.context, stats, "windows", false)
if err != nil { if err != nil {
assert.Error(t, err, te.expected) assert.Error(t, err, te.expected)
} else { } else {
@ -194,25 +195,25 @@ func TestContainerStatsContextWriteWithNoStats(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
contexts := []struct { contexts := []struct {
context Context context formatter.Context
expected string expected string
}{ }{
{ {
Context{ formatter.Context{
Format: "{{.Container}}", Format: "{{.Container}}",
Output: &out, Output: &out,
}, },
"", "",
}, },
{ {
Context{ formatter.Context{
Format: "table {{.Container}}", Format: "table {{.Container}}",
Output: &out, Output: &out,
}, },
"CONTAINER\n", "CONTAINER\n",
}, },
{ {
Context{ formatter.Context{
Format: "table {{.Container}}\t{{.CPUPerc}}", Format: "table {{.Container}}\t{{.CPUPerc}}",
Output: &out, Output: &out,
}, },
@ -221,7 +222,7 @@ func TestContainerStatsContextWriteWithNoStats(t *testing.T) {
} }
for _, context := range contexts { for _, context := range contexts {
ContainerStatsWrite(context.context, []StatsEntry{}, "linux", false) statsFormatWrite(context.context, []StatsEntry{}, "linux", false)
assert.Check(t, is.Equal(context.expected, out.String())) assert.Check(t, is.Equal(context.expected, out.String()))
// Clean buffer // Clean buffer
out.Reset() out.Reset()
@ -232,25 +233,25 @@ func TestContainerStatsContextWriteWithNoStatsWindows(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
contexts := []struct { contexts := []struct {
context Context context formatter.Context
expected string expected string
}{ }{
{ {
Context{ formatter.Context{
Format: "{{.Container}}", Format: "{{.Container}}",
Output: &out, Output: &out,
}, },
"", "",
}, },
{ {
Context{ formatter.Context{
Format: "table {{.Container}}\t{{.MemUsage}}", Format: "table {{.Container}}\t{{.MemUsage}}",
Output: &out, Output: &out,
}, },
"CONTAINER PRIV WORKING SET\n", "CONTAINER PRIV WORKING SET\n",
}, },
{ {
Context{ formatter.Context{
Format: "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}", Format: "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}",
Output: &out, Output: &out,
}, },
@ -259,7 +260,7 @@ func TestContainerStatsContextWriteWithNoStatsWindows(t *testing.T) {
} }
for _, context := range contexts { for _, context := range contexts {
ContainerStatsWrite(context.context, []StatsEntry{}, "windows", false) statsFormatWrite(context.context, []StatsEntry{}, "windows", false)
assert.Check(t, is.Equal(context.expected, out.String())) assert.Check(t, is.Equal(context.expected, out.String()))
// Clean buffer // Clean buffer
out.Reset() out.Reset()
@ -270,12 +271,12 @@ func TestContainerStatsContextWriteTrunc(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
contexts := []struct { contexts := []struct {
context Context context formatter.Context
trunc bool trunc bool
expected string expected string
}{ }{
{ {
Context{ formatter.Context{
Format: "{{.ID}}", Format: "{{.ID}}",
Output: &out, Output: &out,
}, },
@ -283,7 +284,7 @@ func TestContainerStatsContextWriteTrunc(t *testing.T) {
"b95a83497c9161c9b444e3d70e1a9dfba0c1840d41720e146a95a08ebf938afc\n", "b95a83497c9161c9b444e3d70e1a9dfba0c1840d41720e146a95a08ebf938afc\n",
}, },
{ {
Context{ formatter.Context{
Format: "{{.ID}}", Format: "{{.ID}}",
Output: &out, Output: &out,
}, },
@ -293,7 +294,7 @@ func TestContainerStatsContextWriteTrunc(t *testing.T) {
} }
for _, context := range contexts { for _, context := range contexts {
ContainerStatsWrite(context.context, []StatsEntry{{ID: "b95a83497c9161c9b444e3d70e1a9dfba0c1840d41720e146a95a08ebf938afc"}}, "linux", context.trunc) statsFormatWrite(context.context, []StatsEntry{{ID: "b95a83497c9161c9b444e3d70e1a9dfba0c1840d41720e146a95a08ebf938afc"}}, "linux", context.trunc)
assert.Check(t, is.Equal(context.expected, out.String())) assert.Check(t, is.Equal(context.expected, out.String()))
// Clean buffer // Clean buffer
out.Reset() out.Reset()

View File

@ -108,7 +108,7 @@ func runStats(dockerCli command.Cli, opts *statsOptions) error {
closeChan <- err closeChan <- err
} }
for _, container := range cs { for _, container := range cs {
s := formatter.NewContainerStats(container.ID[:12]) s := NewStats(container.ID[:12])
if cStats.add(s) { if cStats.add(s) {
waitFirst.Add(1) waitFirst.Add(1)
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst) go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
@ -125,7 +125,7 @@ func runStats(dockerCli command.Cli, opts *statsOptions) error {
eh := command.InitEventHandler() eh := command.InitEventHandler()
eh.Handle("create", func(e events.Message) { eh.Handle("create", func(e events.Message) {
if opts.all { if opts.all {
s := formatter.NewContainerStats(e.ID[:12]) s := NewStats(e.ID[:12])
if cStats.add(s) { if cStats.add(s) {
waitFirst.Add(1) waitFirst.Add(1)
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst) go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
@ -134,7 +134,7 @@ func runStats(dockerCli command.Cli, opts *statsOptions) error {
}) })
eh.Handle("start", func(e events.Message) { eh.Handle("start", func(e events.Message) {
s := formatter.NewContainerStats(e.ID[:12]) s := NewStats(e.ID[:12])
if cStats.add(s) { if cStats.add(s) {
waitFirst.Add(1) waitFirst.Add(1)
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst) go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
@ -160,7 +160,7 @@ func runStats(dockerCli command.Cli, opts *statsOptions) error {
// Artificially send creation events for the containers we were asked to // Artificially send creation events for the containers we were asked to
// monitor (same code path than we use when monitoring all containers). // monitor (same code path than we use when monitoring all containers).
for _, name := range opts.containers { for _, name := range opts.containers {
s := formatter.NewContainerStats(name) s := NewStats(name)
if cStats.add(s) { if cStats.add(s) {
waitFirst.Add(1) waitFirst.Add(1)
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst) go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
@ -198,7 +198,7 @@ func runStats(dockerCli command.Cli, opts *statsOptions) error {
} }
statsCtx := formatter.Context{ statsCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCli.Out(),
Format: formatter.NewStatsFormat(format, daemonOSType), Format: NewStatsFormat(format, daemonOSType),
} }
cleanScreen := func() { cleanScreen := func() {
if !opts.noStream { if !opts.noStream {
@ -210,13 +210,13 @@ func runStats(dockerCli command.Cli, opts *statsOptions) error {
var err error var err error
for range time.Tick(500 * time.Millisecond) { for range time.Tick(500 * time.Millisecond) {
cleanScreen() cleanScreen()
ccstats := []formatter.StatsEntry{} ccstats := []StatsEntry{}
cStats.mu.Lock() cStats.mu.Lock()
for _, c := range cStats.cs { for _, c := range cStats.cs {
ccstats = append(ccstats, c.GetStatistics()) ccstats = append(ccstats, c.GetStatistics())
} }
cStats.mu.Unlock() cStats.mu.Unlock()
if err = formatter.ContainerStatsWrite(statsCtx, ccstats, daemonOSType, !opts.noTrunc); err != nil { if err = statsFormatWrite(statsCtx, ccstats, daemonOSType, !opts.noTrunc); err != nil {
break break
} }
if len(cStats.cs) == 0 && !showAll { if len(cStats.cs) == 0 && !showAll {

View File

@ -8,7 +8,6 @@ import (
"sync" "sync"
"time" "time"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -17,7 +16,7 @@ import (
type stats struct { type stats struct {
mu sync.Mutex mu sync.Mutex
cs []*formatter.ContainerStats cs []*Stats
} }
// daemonOSType is set once we have at least one stat for a container // daemonOSType is set once we have at least one stat for a container
@ -25,7 +24,7 @@ type stats struct {
// on the daemon platform. // on the daemon platform.
var daemonOSType string var daemonOSType string
func (s *stats) add(cs *formatter.ContainerStats) bool { func (s *stats) add(cs *Stats) bool {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
if _, exists := s.isKnownContainer(cs.Container); !exists { if _, exists := s.isKnownContainer(cs.Container); !exists {
@ -52,7 +51,7 @@ func (s *stats) isKnownContainer(cid string) (int, bool) {
return -1, false return -1, false
} }
func collect(ctx context.Context, s *formatter.ContainerStats, cli client.APIClient, streamStats bool, waitFirst *sync.WaitGroup) { func collect(ctx context.Context, s *Stats, cli client.APIClient, streamStats bool, waitFirst *sync.WaitGroup) {
logrus.Debugf("collecting stats for %s", s.Container) logrus.Debugf("collecting stats for %s", s.Container)
var ( var (
getFirst bool getFirst bool
@ -115,7 +114,7 @@ func collect(ctx context.Context, s *formatter.ContainerStats, cli client.APICli
mem = float64(v.MemoryStats.PrivateWorkingSet) mem = float64(v.MemoryStats.PrivateWorkingSet)
} }
netRx, netTx := calculateNetwork(v.Networks) netRx, netTx := calculateNetwork(v.Networks)
s.SetStatistics(formatter.StatsEntry{ s.SetStatistics(StatsEntry{
Name: v.Name, Name: v.Name,
ID: v.ID, ID: v.ID,
CPUPercentage: cpuPercent, CPUPercentage: cpuPercent,

View File

@ -156,10 +156,10 @@ func getLicenses(ctx context.Context, authConfig *types.AuthConfig, cli command.
updatesCtx := formatter.Context{ updatesCtx := formatter.Context{
Output: cli.Out(), Output: cli.Out(),
Format: formatter.NewSubscriptionsFormat(format, options.quiet), Format: NewSubscriptionsFormat(format, options.quiet),
Trunc: false, Trunc: false,
} }
if err := formatter.SubscriptionsWrite(updatesCtx, subs); err != nil { if err := SubscriptionsWrite(updatesCtx, subs); err != nil {
return nil, err return nil, err
} }
if options.displayOnly { if options.displayOnly {

View File

@ -99,10 +99,10 @@ func runCheck(dockerCli command.Cli, options checkOptions) error {
updatesCtx := formatter.Context{ updatesCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCli.Out(),
Format: formatter.NewUpdatesFormat(format, options.quiet), Format: NewUpdatesFormat(format, options.quiet),
Trunc: false, Trunc: false,
} }
return formatter.UpdatesWrite(updatesCtx, availUpdates) return UpdatesWrite(updatesCtx, availUpdates)
} }
func processVersions(currentVersion, verType string, func processVersions(currentVersion, verType string,

View File

@ -1,8 +1,9 @@
package formatter package engine
import ( import (
"time" "time"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/internal/licenseutils" "github.com/docker/cli/internal/licenseutils"
"github.com/docker/licensing/model" "github.com/docker/licensing/model"
) )
@ -27,25 +28,25 @@ const (
) )
// NewSubscriptionsFormat returns a Format for rendering using a license Context // NewSubscriptionsFormat returns a Format for rendering using a license Context
func NewSubscriptionsFormat(source string, quiet bool) Format { func NewSubscriptionsFormat(source string, quiet bool) formatter.Format {
switch source { switch source {
case TableFormatKey: case formatter.TableFormatKey:
if quiet { if quiet {
return defaultSubscriptionsQuietFormat return defaultSubscriptionsQuietFormat
} }
return defaultSubscriptionsTableFormat return defaultSubscriptionsTableFormat
case RawFormatKey: case formatter.RawFormatKey:
if quiet { if quiet {
return `license: {{.ID}}` return `license: {{.ID}}`
} }
return `license: {{.ID}}\nname: {{.Name}}\nowner: {{.Owner}}\ncomponents: {{.ComponentsString}}\n` return `license: {{.ID}}\nname: {{.Name}}\nowner: {{.Owner}}\ncomponents: {{.ComponentsString}}\n`
} }
return Format(source) return formatter.Format(source)
} }
// SubscriptionsWrite writes the context // SubscriptionsWrite writes the context
func SubscriptionsWrite(ctx Context, subs []licenseutils.LicenseDisplay) error { func SubscriptionsWrite(ctx formatter.Context, subs []licenseutils.LicenseDisplay) error {
render := func(format func(subContext subContext) error) error { render := func(format func(subContext formatter.SubContext) error) error {
for _, sub := range subs { for _, sub := range subs {
licenseCtx := &licenseContext{trunc: ctx.Trunc, l: sub} licenseCtx := &licenseContext{trunc: ctx.Trunc, l: sub}
if err := format(licenseCtx); err != nil { if err := format(licenseCtx); err != nil {
@ -55,7 +56,7 @@ func SubscriptionsWrite(ctx Context, subs []licenseutils.LicenseDisplay) error {
return nil return nil
} }
licenseCtx := licenseContext{} licenseCtx := licenseContext{}
licenseCtx.header = map[string]string{ licenseCtx.Header = map[string]string{
"Num": numHeader, "Num": numHeader,
"Owner": ownerHeader, "Owner": ownerHeader,
"Name": licenseNameHeader, "Name": licenseNameHeader,
@ -74,13 +75,13 @@ func SubscriptionsWrite(ctx Context, subs []licenseutils.LicenseDisplay) error {
} }
type licenseContext struct { type licenseContext struct {
HeaderContext formatter.HeaderContext
trunc bool trunc bool
l licenseutils.LicenseDisplay l licenseutils.LicenseDisplay
} }
func (c *licenseContext) MarshalJSON() ([]byte, error) { func (c *licenseContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c) return formatter.MarshalJSON(c)
} }
func (c *licenseContext) Num() int { func (c *licenseContext) Num() int {

View File

@ -1,4 +1,4 @@
package formatter package engine
import ( import (
"bytes" "bytes"
@ -7,6 +7,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/internal/licenseutils" "github.com/docker/cli/internal/licenseutils"
"github.com/docker/licensing/model" "github.com/docker/licensing/model"
"gotest.tools/assert" "gotest.tools/assert"
@ -15,43 +16,43 @@ import (
func TestSubscriptionContextWrite(t *testing.T) { func TestSubscriptionContextWrite(t *testing.T) {
cases := []struct { cases := []struct {
context Context context formatter.Context
expected string expected string
}{ }{
// Errors // Errors
{ {
Context{Format: "{{InvalidFunction}}"}, formatter.Context{Format: "{{InvalidFunction}}"},
`Template parsing error: template: :1: function "InvalidFunction" not defined `Template parsing error: template: :1: function "InvalidFunction" not defined
`, `,
}, },
{ {
Context{Format: "{{nil}}"}, formatter.Context{Format: "{{nil}}"},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`, `,
}, },
// Table format // Table format
{ {
Context{Format: NewSubscriptionsFormat("table", false)}, formatter.Context{Format: NewSubscriptionsFormat("table", false)},
`NUM OWNER PRODUCT ID EXPIRES PRICING COMPONENTS `NUM OWNER PRODUCT ID EXPIRES PRICING COMPONENTS
1 owner1 productid1 2020-01-01 10:00:00 +0000 UTC compstring 1 owner1 productid1 2020-01-01 10:00:00 +0000 UTC compstring
2 owner2 productid2 2020-01-01 10:00:00 +0000 UTC compstring 2 owner2 productid2 2020-01-01 10:00:00 +0000 UTC compstring
`, `,
}, },
{ {
Context{Format: NewSubscriptionsFormat("table", true)}, formatter.Context{Format: NewSubscriptionsFormat("table", true)},
`1:License Name: name1 Quantity: 10 nodes Expiration date: 2020-01-01 `1:License Name: name1 Quantity: 10 nodes Expiration date: 2020-01-01
2:License Name: name2 Quantity: 20 nodes Expiration date: 2020-01-01 2:License Name: name2 Quantity: 20 nodes Expiration date: 2020-01-01
`, `,
}, },
{ {
Context{Format: NewSubscriptionsFormat("table {{.Owner}}", false)}, formatter.Context{Format: NewSubscriptionsFormat("table {{.Owner}}", false)},
`OWNER `OWNER
owner1 owner1
owner2 owner2
`, `,
}, },
{ {
Context{Format: NewSubscriptionsFormat("table {{.Owner}}", true)}, formatter.Context{Format: NewSubscriptionsFormat("table {{.Owner}}", true)},
`OWNER `OWNER
owner1 owner1
owner2 owner2
@ -59,7 +60,7 @@ owner2
}, },
// Raw Format // Raw Format
{ {
Context{Format: NewSubscriptionsFormat("raw", false)}, formatter.Context{Format: NewSubscriptionsFormat("raw", false)},
`license: id1 `license: id1
name: name1 name: name1
owner: owner1 owner: owner1
@ -73,14 +74,14 @@ components: compstring
`, `,
}, },
{ {
Context{Format: NewSubscriptionsFormat("raw", true)}, formatter.Context{Format: NewSubscriptionsFormat("raw", true)},
`license: id1 `license: id1
license: id2 license: id2
`, `,
}, },
// Custom Format // Custom Format
{ {
Context{Format: NewSubscriptionsFormat("{{.Owner}}", false)}, formatter.Context{Format: NewSubscriptionsFormat("{{.Owner}}", false)},
`owner1 `owner1
owner2 owner2
`, `,
@ -223,7 +224,7 @@ func TestSubscriptionContextWriteJSON(t *testing.T) {
} }
out := &bytes.Buffer{} out := &bytes.Buffer{}
err := SubscriptionsWrite(Context{Format: "{{json .}}", Output: out}, subscriptions) err := SubscriptionsWrite(formatter.Context{Format: "{{json .}}", Output: out}, subscriptions)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -242,7 +243,7 @@ func TestSubscriptionContextWriteJSONField(t *testing.T) {
{Num: 2, Owner: "owner2"}, {Num: 2, Owner: "owner2"},
} }
out := &bytes.Buffer{} out := &bytes.Buffer{}
err := SubscriptionsWrite(Context{Format: "{{json .Owner}}", Output: out}, subscriptions) err := SubscriptionsWrite(formatter.Context{Format: "{{json .Owner}}", Output: out}, subscriptions)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -1,6 +1,7 @@
package formatter package engine
import ( import (
"github.com/docker/cli/cli/command/formatter"
clitypes "github.com/docker/cli/types" clitypes "github.com/docker/cli/types"
) )
@ -14,25 +15,25 @@ const (
) )
// NewUpdatesFormat returns a Format for rendering using a updates context // NewUpdatesFormat returns a Format for rendering using a updates context
func NewUpdatesFormat(source string, quiet bool) Format { func NewUpdatesFormat(source string, quiet bool) formatter.Format {
switch source { switch source {
case TableFormatKey: case formatter.TableFormatKey:
if quiet { if quiet {
return defaultUpdatesQuietFormat return defaultUpdatesQuietFormat
} }
return defaultUpdatesTableFormat return defaultUpdatesTableFormat
case RawFormatKey: case formatter.RawFormatKey:
if quiet { if quiet {
return `update_version: {{.Version}}` return `update_version: {{.Version}}`
} }
return `update_version: {{.Version}}\ntype: {{.Type}}\nnotes: {{.Notes}}\n` return `update_version: {{.Version}}\ntype: {{.Type}}\nnotes: {{.Notes}}\n`
} }
return Format(source) return formatter.Format(source)
} }
// UpdatesWrite writes the context // UpdatesWrite writes the context
func UpdatesWrite(ctx Context, availableUpdates []clitypes.Update) error { func UpdatesWrite(ctx formatter.Context, availableUpdates []clitypes.Update) error {
render := func(format func(subContext subContext) error) error { render := func(format func(subContext formatter.SubContext) error) error {
for _, update := range availableUpdates { for _, update := range availableUpdates {
updatesCtx := &updateContext{trunc: ctx.Trunc, u: update} updatesCtx := &updateContext{trunc: ctx.Trunc, u: update}
if err := format(updatesCtx); err != nil { if err := format(updatesCtx); err != nil {
@ -42,7 +43,7 @@ func UpdatesWrite(ctx Context, availableUpdates []clitypes.Update) error {
return nil return nil
} }
updatesCtx := updateContext{} updatesCtx := updateContext{}
updatesCtx.header = map[string]string{ updatesCtx.Header = map[string]string{
"Type": updatesTypeHeader, "Type": updatesTypeHeader,
"Version": versionHeader, "Version": versionHeader,
"Notes": notesHeader, "Notes": notesHeader,
@ -51,13 +52,13 @@ func UpdatesWrite(ctx Context, availableUpdates []clitypes.Update) error {
} }
type updateContext struct { type updateContext struct {
HeaderContext formatter.HeaderContext
trunc bool trunc bool
u clitypes.Update u clitypes.Update
} }
func (c *updateContext) MarshalJSON() ([]byte, error) { func (c *updateContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c) return formatter.MarshalJSON(c)
} }
func (c *updateContext) Type() string { func (c *updateContext) Type() string {

View File

@ -1,4 +1,4 @@
package formatter package engine
import ( import (
"bytes" "bytes"
@ -6,6 +6,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/docker/cli/cli/command/formatter"
clitypes "github.com/docker/cli/types" clitypes "github.com/docker/cli/types"
"gotest.tools/assert" "gotest.tools/assert"
is "gotest.tools/assert/cmp" is "gotest.tools/assert/cmp"
@ -13,43 +14,43 @@ import (
func TestUpdateContextWrite(t *testing.T) { func TestUpdateContextWrite(t *testing.T) {
cases := []struct { cases := []struct {
context Context context formatter.Context
expected string expected string
}{ }{
// Errors // Errors
{ {
Context{Format: "{{InvalidFunction}}"}, formatter.Context{Format: "{{InvalidFunction}}"},
`Template parsing error: template: :1: function "InvalidFunction" not defined `Template parsing error: template: :1: function "InvalidFunction" not defined
`, `,
}, },
{ {
Context{Format: "{{nil}}"}, formatter.Context{Format: "{{nil}}"},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`, `,
}, },
// Table format // Table format
{ {
Context{Format: NewUpdatesFormat("table", false)}, formatter.Context{Format: NewUpdatesFormat("table", false)},
`TYPE VERSION NOTES `TYPE VERSION NOTES
updateType1 version1 description 1 updateType1 version1 description 1
updateType2 version2 description 2 updateType2 version2 description 2
`, `,
}, },
{ {
Context{Format: NewUpdatesFormat("table", true)}, formatter.Context{Format: NewUpdatesFormat("table", true)},
`version1 `version1
version2 version2
`, `,
}, },
{ {
Context{Format: NewUpdatesFormat("table {{.Version}}", false)}, formatter.Context{Format: NewUpdatesFormat("table {{.Version}}", false)},
`VERSION `VERSION
version1 version1
version2 version2
`, `,
}, },
{ {
Context{Format: NewUpdatesFormat("table {{.Version}}", true)}, formatter.Context{Format: NewUpdatesFormat("table {{.Version}}", true)},
`VERSION `VERSION
version1 version1
version2 version2
@ -57,7 +58,7 @@ version2
}, },
// Raw Format // Raw Format
{ {
Context{Format: NewUpdatesFormat("raw", false)}, formatter.Context{Format: NewUpdatesFormat("raw", false)},
`update_version: version1 `update_version: version1
type: updateType1 type: updateType1
notes: description 1 notes: description 1
@ -69,14 +70,14 @@ notes: description 2
`, `,
}, },
{ {
Context{Format: NewUpdatesFormat("raw", true)}, formatter.Context{Format: NewUpdatesFormat("raw", true)},
`update_version: version1 `update_version: version1
update_version: version2 update_version: version2
`, `,
}, },
// Custom Format // Custom Format
{ {
Context{Format: NewUpdatesFormat("{{.Version}}", false)}, formatter.Context{Format: NewUpdatesFormat("{{.Version}}", false)},
`version1 `version1
version2 version2
`, `,
@ -110,7 +111,7 @@ func TestUpdateContextWriteJSON(t *testing.T) {
} }
out := &bytes.Buffer{} out := &bytes.Buffer{}
err := UpdatesWrite(Context{Format: "{{json .}}", Output: out}, updates) err := UpdatesWrite(formatter.Context{Format: "{{json .}}", Output: out}, updates)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -129,7 +130,7 @@ func TestUpdateContextWriteJSONField(t *testing.T) {
{Type: "updateType2", Version: "version2"}, {Type: "updateType2", Version: "version2"},
} }
out := &bytes.Buffer{} out := &bytes.Buffer{}
err := UpdatesWrite(Context{Format: "{{json .Type}}", Output: out}, updates) err := UpdatesWrite(formatter.Context{Format: "{{json .Type}}", Output: out}, updates)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -28,7 +28,7 @@ func NewBuildCacheFormat(source string, quiet bool) Format {
switch source { switch source {
case TableFormatKey: case TableFormatKey:
if quiet { if quiet {
return defaultQuietFormat return DefaultQuietFormat
} }
return Format(defaultBuildCacheTableFormat) return Format(defaultBuildCacheTableFormat)
case RawFormatKey: case RawFormatKey:
@ -72,7 +72,7 @@ func buildCacheSort(buildCache []*types.BuildCache) {
// BuildCacheWrite renders the context for a list of containers // BuildCacheWrite renders the context for a list of containers
func BuildCacheWrite(ctx Context, buildCaches []*types.BuildCache) error { func BuildCacheWrite(ctx Context, buildCaches []*types.BuildCache) error {
render := func(format func(subContext subContext) error) error { render := func(format func(subContext SubContext) error) error {
buildCacheSort(buildCaches) buildCacheSort(buildCaches)
for _, bc := range buildCaches { for _, bc := range buildCaches {
err := format(&buildCacheContext{trunc: ctx.Trunc, v: bc}) err := format(&buildCacheContext{trunc: ctx.Trunc, v: bc})
@ -85,8 +85,6 @@ func BuildCacheWrite(ctx Context, buildCaches []*types.BuildCache) error {
return ctx.Write(newBuildCacheContext(), render) return ctx.Write(newBuildCacheContext(), render)
} }
type buildCacheHeaderContext map[string]string
type buildCacheContext struct { type buildCacheContext struct {
HeaderContext HeaderContext
trunc bool trunc bool
@ -95,23 +93,23 @@ type buildCacheContext struct {
func newBuildCacheContext() *buildCacheContext { func newBuildCacheContext() *buildCacheContext {
buildCacheCtx := buildCacheContext{} buildCacheCtx := buildCacheContext{}
buildCacheCtx.header = buildCacheHeaderContext{ buildCacheCtx.Header = SubHeaderContext{
"ID": cacheIDHeader, "ID": cacheIDHeader,
"Parent": parentHeader, "Parent": parentHeader,
"CacheType": cacheTypeHeader, "CacheType": cacheTypeHeader,
"Size": sizeHeader, "Size": SizeHeader,
"CreatedSince": createdSinceHeader, "CreatedSince": CreatedSinceHeader,
"LastUsedSince": lastUsedSinceHeader, "LastUsedSince": lastUsedSinceHeader,
"UsageCount": usageCountHeader, "UsageCount": usageCountHeader,
"InUse": inUseHeader, "InUse": inUseHeader,
"Shared": sharedHeader, "Shared": sharedHeader,
"Description": descriptionHeader, "Description": DescriptionHeader,
} }
return &buildCacheCtx return &buildCacheCtx
} }
func (c *buildCacheContext) MarshalJSON() ([]byte, error) { func (c *buildCacheContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c) return MarshalJSON(c)
} }
func (c *buildCacheContext) ID() string { func (c *buildCacheContext) ID() string {

View File

@ -16,15 +16,12 @@ import (
const ( const (
defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}" defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
containerIDHeader = "CONTAINER ID" namesHeader = "NAMES"
namesHeader = "NAMES" commandHeader = "COMMAND"
commandHeader = "COMMAND" runningForHeader = "CREATED"
runningForHeader = "CREATED" mountsHeader = "MOUNTS"
statusHeader = "STATUS" localVolumes = "LOCAL VOLUMES"
portsHeader = "PORTS" networksHeader = "NETWORKS"
mountsHeader = "MOUNTS"
localVolumes = "LOCAL VOLUMES"
networksHeader = "NETWORKS"
) )
// NewContainerFormat returns a Format for rendering using a Context // NewContainerFormat returns a Format for rendering using a Context
@ -32,7 +29,7 @@ func NewContainerFormat(source string, quiet bool, size bool) Format {
switch source { switch source {
case TableFormatKey: case TableFormatKey:
if quiet { if quiet {
return defaultQuietFormat return DefaultQuietFormat
} }
format := defaultContainerTableFormat format := defaultContainerTableFormat
if size { if size {
@ -62,7 +59,7 @@ ports: {{- pad .Ports 1 0}}
// ContainerWrite renders the context for a list of containers // ContainerWrite renders the context for a list of containers
func ContainerWrite(ctx Context, containers []types.Container) error { func ContainerWrite(ctx Context, containers []types.Container) error {
render := func(format func(subContext subContext) error) error { render := func(format func(subContext SubContext) error) error {
for _, container := range containers { for _, container := range containers {
err := format(&containerContext{trunc: ctx.Trunc, c: container}) err := format(&containerContext{trunc: ctx.Trunc, c: container})
if err != nil { if err != nil {
@ -74,16 +71,6 @@ func ContainerWrite(ctx Context, containers []types.Container) error {
return ctx.Write(newContainerContext(), render) return ctx.Write(newContainerContext(), render)
} }
type containerHeaderContext map[string]string
func (c containerHeaderContext) Label(name string) string {
n := strings.Split(name, ".")
r := strings.NewReplacer("-", " ", "_", " ")
h := r.Replace(n[len(n)-1])
return h
}
type containerContext struct { type containerContext struct {
HeaderContext HeaderContext
trunc bool trunc bool
@ -92,17 +79,17 @@ type containerContext struct {
func newContainerContext() *containerContext { func newContainerContext() *containerContext {
containerCtx := containerContext{} containerCtx := containerContext{}
containerCtx.header = containerHeaderContext{ containerCtx.Header = SubHeaderContext{
"ID": containerIDHeader, "ID": ContainerIDHeader,
"Names": namesHeader, "Names": namesHeader,
"Image": imageHeader, "Image": ImageHeader,
"Command": commandHeader, "Command": commandHeader,
"CreatedAt": createdAtHeader, "CreatedAt": CreatedAtHeader,
"RunningFor": runningForHeader, "RunningFor": runningForHeader,
"Ports": portsHeader, "Ports": PortsHeader,
"Status": statusHeader, "Status": StatusHeader,
"Size": sizeHeader, "Size": SizeHeader,
"Labels": labelsHeader, "Labels": LabelsHeader,
"Mounts": mountsHeader, "Mounts": mountsHeader,
"LocalVolumes": localVolumes, "LocalVolumes": localVolumes,
"Networks": networksHeader, "Networks": networksHeader,
@ -111,7 +98,7 @@ func newContainerContext() *containerContext {
} }
func (c *containerContext) MarshalJSON() ([]byte, error) { func (c *containerContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c) return MarshalJSON(c)
} }
func (c *containerContext) ID() string { func (c *containerContext) ID() string {

View File

@ -1,28 +1,48 @@
package formatter package formatter
import "strings"
// Common header constants
const ( const (
imageHeader = "IMAGE" CreatedSinceHeader = "CREATED"
createdSinceHeader = "CREATED" CreatedAtHeader = "CREATED AT"
createdAtHeader = "CREATED AT" SizeHeader = "SIZE"
sizeHeader = "SIZE" LabelsHeader = "LABELS"
labelsHeader = "LABELS" NameHeader = "NAME"
nameHeader = "NAME" DescriptionHeader = "DESCRIPTION"
driverHeader = "DRIVER" DriverHeader = "DRIVER"
scopeHeader = "SCOPE" ScopeHeader = "SCOPE"
StatusHeader = "STATUS"
PortsHeader = "PORTS"
ImageHeader = "IMAGE"
ContainerIDHeader = "CONTAINER ID"
) )
type subContext interface { // SubContext defines what Context implementation should provide
type SubContext interface {
FullHeader() interface{} FullHeader() interface{}
} }
// SubHeaderContext is a map destined to formatter header (table format)
type SubHeaderContext map[string]string
// Label returns the header label for the specified string
func (c SubHeaderContext) Label(name string) string {
n := strings.Split(name, ".")
r := strings.NewReplacer("-", " ", "_", " ")
h := r.Replace(n[len(n)-1])
return h
}
// HeaderContext provides the subContext interface for managing headers // HeaderContext provides the subContext interface for managing headers
type HeaderContext struct { type HeaderContext struct {
header interface{} Header interface{}
} }
// FullHeader returns the header as an interface // FullHeader returns the header as an interface
func (c *HeaderContext) FullHeader() interface{} { func (c *HeaderContext) FullHeader() interface{} {
return c.header return c.Header
} }
func stripNamePrefix(ss []string) []string { func stripNamePrefix(ss []string) []string {

View File

@ -1,28 +1,12 @@
package formatter package formatter
import ( import (
"strings"
"testing" "testing"
"gotest.tools/assert" "github.com/docker/cli/internal/test"
is "gotest.tools/assert/cmp"
) )
// Deprecated: use internal/test.CompareMultipleValues instead
func compareMultipleValues(t *testing.T, value, expected string) { func compareMultipleValues(t *testing.T, value, expected string) {
// comma-separated values means probably a map input, which won't test.CompareMultipleValues(t, value, expected)
// be guaranteed to have the same order as our expected value
// We'll create maps and use reflect.DeepEquals to check instead:
entriesMap := make(map[string]string)
expMap := make(map[string]string)
entries := strings.Split(value, ",")
expectedEntries := strings.Split(expected, ",")
for _, entry := range entries {
keyval := strings.Split(entry, "=")
entriesMap[keyval[0]] = keyval[1]
}
for _, expected := range expectedEntries {
keyval := strings.Split(expected, "=")
expMap[keyval[0]] = keyval[1]
}
assert.Check(t, is.DeepEqual(expMap, entriesMap))
} }

View File

@ -122,11 +122,11 @@ func (ctx *DiskUsageContext) Write() (err error) {
} }
diskUsageContainersCtx := diskUsageContainersContext{containers: []*types.Container{}} diskUsageContainersCtx := diskUsageContainersContext{containers: []*types.Container{}}
diskUsageContainersCtx.header = map[string]string{ diskUsageContainersCtx.Header = SubHeaderContext{
"Type": typeHeader, "Type": typeHeader,
"TotalCount": totalHeader, "TotalCount": totalHeader,
"Active": activeHeader, "Active": activeHeader,
"Size": sizeHeader, "Size": SizeHeader,
"Reclaimable": reclaimableHeader, "Reclaimable": reclaimableHeader,
} }
ctx.postFormat(tmpl, &diskUsageContainersCtx) ctx.postFormat(tmpl, &diskUsageContainersCtx)
@ -263,7 +263,7 @@ type diskUsageImagesContext struct {
} }
func (c *diskUsageImagesContext) MarshalJSON() ([]byte, error) { func (c *diskUsageImagesContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c) return MarshalJSON(c)
} }
func (c *diskUsageImagesContext) Type() string { func (c *diskUsageImagesContext) Type() string {
@ -315,7 +315,7 @@ type diskUsageContainersContext struct {
} }
func (c *diskUsageContainersContext) MarshalJSON() ([]byte, error) { func (c *diskUsageContainersContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c) return MarshalJSON(c)
} }
func (c *diskUsageContainersContext) Type() string { func (c *diskUsageContainersContext) Type() string {
@ -377,7 +377,7 @@ type diskUsageVolumesContext struct {
} }
func (c *diskUsageVolumesContext) MarshalJSON() ([]byte, error) { func (c *diskUsageVolumesContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c) return MarshalJSON(c)
} }
func (c *diskUsageVolumesContext) Type() string { func (c *diskUsageVolumesContext) Type() string {
@ -439,7 +439,7 @@ type diskUsageBuilderContext struct {
} }
func (c *diskUsageBuilderContext) MarshalJSON() ([]byte, error) { func (c *diskUsageBuilderContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c) return MarshalJSON(c)
} }
func (c *diskUsageBuilderContext) Type() string { func (c *diskUsageBuilderContext) Type() string {

View File

@ -17,7 +17,7 @@ const (
RawFormatKey = "raw" RawFormatKey = "raw"
PrettyFormatKey = "pretty" PrettyFormatKey = "pretty"
defaultQuietFormat = "{{.ID}}" DefaultQuietFormat = "{{.ID}}"
) )
// Format is the format string rendered using the Context // Format is the format string rendered using the Context
@ -69,7 +69,7 @@ func (c *Context) parseFormat() (*template.Template, error) {
return tmpl, err return tmpl, err
} }
func (c *Context) postFormat(tmpl *template.Template, subContext subContext) { func (c *Context) postFormat(tmpl *template.Template, subContext SubContext) {
if c.Format.IsTable() { if c.Format.IsTable() {
t := tabwriter.NewWriter(c.Output, 20, 1, 3, ' ', 0) t := tabwriter.NewWriter(c.Output, 20, 1, 3, ' ', 0)
buffer := bytes.NewBufferString("") buffer := bytes.NewBufferString("")
@ -83,7 +83,7 @@ func (c *Context) postFormat(tmpl *template.Template, subContext subContext) {
} }
} }
func (c *Context) contextFormat(tmpl *template.Template, subContext subContext) error { func (c *Context) contextFormat(tmpl *template.Template, subContext SubContext) error {
if err := tmpl.Execute(c.buffer, subContext); err != nil { if err := tmpl.Execute(c.buffer, subContext); err != nil {
return errors.Errorf("Template parsing error: %v\n", err) return errors.Errorf("Template parsing error: %v\n", err)
} }
@ -95,10 +95,10 @@ func (c *Context) contextFormat(tmpl *template.Template, subContext subContext)
} }
// SubFormat is a function type accepted by Write() // SubFormat is a function type accepted by Write()
type SubFormat func(func(subContext) error) error type SubFormat func(func(SubContext) error) error
// Write the template to the buffer using this Context // Write the template to the buffer using this Context
func (c *Context) Write(sub subContext, f SubFormat) error { func (c *Context) Write(sub SubContext, f SubFormat) error {
c.buffer = bytes.NewBufferString("") c.buffer = bytes.NewBufferString("")
c.preFormat() c.preFormat()
@ -107,7 +107,7 @@ func (c *Context) Write(sub subContext, f SubFormat) error {
return err return err
} }
subFormat := func(subContext subContext) error { subFormat := func(subContext SubContext) error {
return c.contextFormat(tmpl, subContext) return c.contextFormat(tmpl, subContext)
} }
if err := f(subFormat); err != nil { if err := f(subFormat); err != nil {

View File

@ -36,7 +36,7 @@ func NewImageFormat(source string, quiet bool, digest bool) Format {
case TableFormatKey: case TableFormatKey:
switch { switch {
case quiet: case quiet:
return defaultQuietFormat return DefaultQuietFormat
case digest: case digest:
return defaultImageTableFormatWithDigest return defaultImageTableFormatWithDigest
default: default:
@ -73,7 +73,7 @@ virtual_size: {{.Size}}
// ImageWrite writes the formatter images using the ImageContext // ImageWrite writes the formatter images using the ImageContext
func ImageWrite(ctx ImageContext, images []types.ImageSummary) error { func ImageWrite(ctx ImageContext, images []types.ImageSummary) error {
render := func(format func(subContext subContext) error) error { render := func(format func(subContext SubContext) error) error {
return imageFormat(ctx, images, format) return imageFormat(ctx, images, format)
} }
return ctx.Write(newImageContext(), render) return ctx.Write(newImageContext(), render)
@ -84,7 +84,7 @@ func needDigest(ctx ImageContext) bool {
return ctx.Digest || ctx.Format.Contains("{{.Digest}}") return ctx.Digest || ctx.Format.Contains("{{.Digest}}")
} }
func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subContext subContext) error) error { func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subContext SubContext) error) error {
for _, image := range images { for _, image := range images {
formatted := []*imageContext{} formatted := []*imageContext{}
if isDangling(image) { if isDangling(image) {
@ -194,16 +194,16 @@ type imageContext struct {
func newImageContext() *imageContext { func newImageContext() *imageContext {
imageCtx := imageContext{} imageCtx := imageContext{}
imageCtx.header = map[string]string{ imageCtx.Header = SubHeaderContext{
"ID": imageIDHeader, "ID": imageIDHeader,
"Repository": repositoryHeader, "Repository": repositoryHeader,
"Tag": tagHeader, "Tag": tagHeader,
"Digest": digestHeader, "Digest": digestHeader,
"CreatedSince": createdSinceHeader, "CreatedSince": CreatedSinceHeader,
"CreatedAt": createdAtHeader, "CreatedAt": CreatedAtHeader,
"Size": sizeHeader, "Size": SizeHeader,
"Containers": containersHeader, "Containers": containersHeader,
"VirtualSize": sizeHeader, "VirtualSize": SizeHeader,
"SharedSize": sharedSizeHeader, "SharedSize": sharedSizeHeader,
"UniqueSize": uniqueSizeHeader, "UniqueSize": uniqueSizeHeader,
} }
@ -211,7 +211,7 @@ func newImageContext() *imageContext {
} }
func (c *imageContext) MarshalJSON() ([]byte, error) { func (c *imageContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c) return MarshalJSON(c)
} }
func (c *imageContext) ID() string { func (c *imageContext) ID() string {

View File

@ -8,7 +8,9 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
func marshalJSON(x interface{}) ([]byte, error) { // MarshalJSON marshals x into json
// It differs a bit from encoding/json MarshalJSON function for formatter
func MarshalJSON(x interface{}) ([]byte, error) {
m, err := marshalMap(x) m, err := marshalMap(x)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -37,7 +37,7 @@ func NewVolumeFormat(source string, quiet bool) Format {
// VolumeWrite writes formatted volumes using the Context // VolumeWrite writes formatted volumes using the Context
func VolumeWrite(ctx Context, volumes []*types.Volume) error { func VolumeWrite(ctx Context, volumes []*types.Volume) error {
render := func(format func(subContext subContext) error) error { render := func(format func(subContext SubContext) error) error {
for _, volume := range volumes { for _, volume := range volumes {
if err := format(&volumeContext{v: *volume}); err != nil { if err := format(&volumeContext{v: *volume}); err != nil {
return err return err
@ -48,16 +48,6 @@ func VolumeWrite(ctx Context, volumes []*types.Volume) error {
return ctx.Write(newVolumeContext(), render) return ctx.Write(newVolumeContext(), render)
} }
type volumeHeaderContext map[string]string
func (c volumeHeaderContext) Label(name string) string {
n := strings.Split(name, ".")
r := strings.NewReplacer("-", " ", "_", " ")
h := r.Replace(n[len(n)-1])
return h
}
type volumeContext struct { type volumeContext struct {
HeaderContext HeaderContext
v types.Volume v types.Volume
@ -65,20 +55,20 @@ type volumeContext struct {
func newVolumeContext() *volumeContext { func newVolumeContext() *volumeContext {
volumeCtx := volumeContext{} volumeCtx := volumeContext{}
volumeCtx.header = volumeHeaderContext{ volumeCtx.Header = SubHeaderContext{
"Name": volumeNameHeader, "Name": volumeNameHeader,
"Driver": driverHeader, "Driver": DriverHeader,
"Scope": scopeHeader, "Scope": ScopeHeader,
"Mountpoint": mountpointHeader, "Mountpoint": mountpointHeader,
"Labels": labelsHeader, "Labels": LabelsHeader,
"Links": linksHeader, "Links": linksHeader,
"Size": sizeHeader, "Size": SizeHeader,
} }
return &volumeCtx return &volumeCtx
} }
func (c *volumeContext) MarshalJSON() ([]byte, error) { func (c *volumeContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c) return MarshalJSON(c)
} }
func (c *volumeContext) Name() string { func (c *volumeContext) Name() string {

View File

@ -1,10 +1,11 @@
package formatter package image
import ( import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/image"
"github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/stringid"
units "github.com/docker/go-units" units "github.com/docker/go-units"
@ -20,12 +21,12 @@ const (
) )
// NewHistoryFormat returns a format for rendering an HistoryContext // NewHistoryFormat returns a format for rendering an HistoryContext
func NewHistoryFormat(source string, quiet bool, human bool) Format { func NewHistoryFormat(source string, quiet bool, human bool) formatter.Format {
switch source { switch source {
case TableFormatKey: case formatter.TableFormatKey:
switch { switch {
case quiet: case quiet:
return defaultQuietFormat return formatter.DefaultQuietFormat
case !human: case !human:
return nonHumanHistoryTableFormat return nonHumanHistoryTableFormat
default: default:
@ -33,12 +34,12 @@ func NewHistoryFormat(source string, quiet bool, human bool) Format {
} }
} }
return Format(source) return formatter.Format(source)
} }
// HistoryWrite writes the context // HistoryWrite writes the context
func HistoryWrite(ctx Context, human bool, histories []image.HistoryResponseItem) error { func HistoryWrite(ctx formatter.Context, human bool, histories []image.HistoryResponseItem) error {
render := func(format func(subContext subContext) error) error { render := func(format func(subContext formatter.SubContext) error) error {
for _, history := range histories { for _, history := range histories {
historyCtx := &historyContext{trunc: ctx.Trunc, h: history, human: human} historyCtx := &historyContext{trunc: ctx.Trunc, h: history, human: human}
if err := format(historyCtx); err != nil { if err := format(historyCtx); err != nil {
@ -48,26 +49,26 @@ func HistoryWrite(ctx Context, human bool, histories []image.HistoryResponseItem
return nil return nil
} }
historyCtx := &historyContext{} historyCtx := &historyContext{}
historyCtx.header = map[string]string{ historyCtx.Header = formatter.SubHeaderContext{
"ID": historyIDHeader, "ID": historyIDHeader,
"CreatedSince": createdSinceHeader, "CreatedSince": formatter.CreatedSinceHeader,
"CreatedAt": createdAtHeader, "CreatedAt": formatter.CreatedAtHeader,
"CreatedBy": createdByHeader, "CreatedBy": createdByHeader,
"Size": sizeHeader, "Size": formatter.SizeHeader,
"Comment": commentHeader, "Comment": commentHeader,
} }
return ctx.Write(historyCtx, render) return ctx.Write(historyCtx, render)
} }
type historyContext struct { type historyContext struct {
HeaderContext formatter.HeaderContext
trunc bool trunc bool
human bool human bool
h image.HistoryResponseItem h image.HistoryResponseItem
} }
func (c *historyContext) MarshalJSON() ([]byte, error) { func (c *historyContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c) return formatter.MarshalJSON(c)
} }
func (c *historyContext) ID() string { func (c *historyContext) ID() string {
@ -92,7 +93,7 @@ func (c *historyContext) CreatedSince() string {
func (c *historyContext) CreatedBy() string { func (c *historyContext) CreatedBy() string {
createdBy := strings.Replace(c.h.CreatedBy, "\t", " ", -1) createdBy := strings.Replace(c.h.CreatedBy, "\t", " ", -1)
if c.trunc { if c.trunc {
return Ellipsis(createdBy, 45) return formatter.Ellipsis(createdBy, 45)
} }
return createdBy return createdBy
} }

View File

@ -1,4 +1,4 @@
package formatter package image
import ( import (
"bytes" "bytes"
@ -7,6 +7,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/image"
"github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/stringid"
"gotest.tools/assert" "gotest.tools/assert"
@ -42,7 +44,7 @@ func TestHistoryContext_ID(t *testing.T) {
ctx = c.historyCtx ctx = c.historyCtx
v := c.call() v := c.call()
if strings.Contains(v, ",") { if strings.Contains(v, ",") {
compareMultipleValues(t, v, c.expValue) test.CompareMultipleValues(t, v, c.expValue)
} else if v != c.expValue { } else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v) t.Fatalf("Expected %s, was %s\n", c.expValue, v)
} }
@ -73,7 +75,7 @@ func TestHistoryContext_CreatedSince(t *testing.T) {
ctx = c.historyCtx ctx = c.historyCtx
v := c.call() v := c.call()
if strings.Contains(v, ",") { if strings.Contains(v, ",") {
compareMultipleValues(t, v, c.expValue) test.CompareMultipleValues(t, v, c.expValue)
} else if v != c.expValue { } else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v) t.Fatalf("Expected %s, was %s\n", c.expValue, v)
} }
@ -96,7 +98,7 @@ func TestHistoryContext_CreatedBy(t *testing.T) {
historyContext{ historyContext{
h: image.HistoryResponseItem{CreatedBy: withTabs}, h: image.HistoryResponseItem{CreatedBy: withTabs},
trunc: true, trunc: true,
}, Ellipsis(expected, 45), ctx.CreatedBy, }, formatter.Ellipsis(expected, 45), ctx.CreatedBy,
}, },
} }
@ -104,7 +106,7 @@ func TestHistoryContext_CreatedBy(t *testing.T) {
ctx = c.historyCtx ctx = c.historyCtx
v := c.call() v := c.call()
if strings.Contains(v, ",") { if strings.Contains(v, ",") {
compareMultipleValues(t, v, c.expValue) test.CompareMultipleValues(t, v, c.expValue)
} else if v != c.expValue { } else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v) t.Fatalf("Expected %s, was %s\n", c.expValue, v)
} }
@ -136,7 +138,7 @@ func TestHistoryContext_Size(t *testing.T) {
ctx = c.historyCtx ctx = c.historyCtx
v := c.call() v := c.call()
if strings.Contains(v, ",") { if strings.Contains(v, ",") {
compareMultipleValues(t, v, c.expValue) test.CompareMultipleValues(t, v, c.expValue)
} else if v != c.expValue { } else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v) t.Fatalf("Expected %s, was %s\n", c.expValue, v)
} }
@ -160,7 +162,7 @@ func TestHistoryContext_Comment(t *testing.T) {
ctx = c.historyCtx ctx = c.historyCtx
v := c.call() v := c.call()
if strings.Contains(v, ",") { if strings.Contains(v, ",") {
compareMultipleValues(t, v, c.expValue) test.CompareMultipleValues(t, v, c.expValue)
} else if v != c.expValue { } else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v) t.Fatalf("Expected %s, was %s\n", c.expValue, v)
} }
@ -198,17 +200,17 @@ imageID4 24 hours ago /bin/bash grep
` `
contexts := []struct { contexts := []struct {
context Context context formatter.Context
expected string expected string
}{ }{
{Context{ {formatter.Context{
Format: NewHistoryFormat("table", false, true), Format: NewHistoryFormat("table", false, true),
Trunc: true, Trunc: true,
Output: out, Output: out,
}, },
expectedTrunc, expectedTrunc,
}, },
{Context{ {formatter.Context{
Format: NewHistoryFormat("table", false, true), Format: NewHistoryFormat("table", false, true),
Trunc: false, Trunc: false,
Output: out, Output: out,

View File

@ -57,8 +57,8 @@ func runHistory(dockerCli command.Cli, opts historyOptions) error {
historyCtx := formatter.Context{ historyCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCli.Out(),
Format: formatter.NewHistoryFormat(format, opts.quiet, opts.human), Format: NewHistoryFormat(format, opts.quiet, opts.human),
Trunc: !opts.noTrunc, Trunc: !opts.noTrunc,
} }
return formatter.HistoryWrite(historyCtx, opts.human, history) return HistoryWrite(historyCtx, opts.human, history)
} }

View File

@ -1,9 +1,10 @@
package formatter package network
import ( import (
"fmt" "fmt"
"strings" "strings"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/stringid"
) )
@ -16,26 +17,26 @@ const (
internalHeader = "INTERNAL" internalHeader = "INTERNAL"
) )
// NewNetworkFormat returns a Format for rendering using a network Context // NewFormat returns a Format for rendering using a network Context
func NewNetworkFormat(source string, quiet bool) Format { func NewFormat(source string, quiet bool) formatter.Format {
switch source { switch source {
case TableFormatKey: case formatter.TableFormatKey:
if quiet { if quiet {
return defaultQuietFormat return formatter.DefaultQuietFormat
} }
return defaultNetworkTableFormat return defaultNetworkTableFormat
case RawFormatKey: case formatter.RawFormatKey:
if quiet { if quiet {
return `network_id: {{.ID}}` return `network_id: {{.ID}}`
} }
return `network_id: {{.ID}}\nname: {{.Name}}\ndriver: {{.Driver}}\nscope: {{.Scope}}\n` return `network_id: {{.ID}}\nname: {{.Name}}\ndriver: {{.Driver}}\nscope: {{.Scope}}\n`
} }
return Format(source) return formatter.Format(source)
} }
// NetworkWrite writes the context // FormatWrite writes the context
func NetworkWrite(ctx Context, networks []types.NetworkResource) error { func FormatWrite(ctx formatter.Context, networks []types.NetworkResource) error {
render := func(format func(subContext subContext) error) error { render := func(format func(subContext formatter.SubContext) error) error {
for _, network := range networks { for _, network := range networks {
networkCtx := &networkContext{trunc: ctx.Trunc, n: network} networkCtx := &networkContext{trunc: ctx.Trunc, n: network}
if err := format(networkCtx); err != nil { if err := format(networkCtx); err != nil {
@ -45,37 +46,27 @@ func NetworkWrite(ctx Context, networks []types.NetworkResource) error {
return nil return nil
} }
networkCtx := networkContext{} networkCtx := networkContext{}
networkCtx.header = networkHeaderContext{ networkCtx.Header = formatter.SubHeaderContext{
"ID": networkIDHeader, "ID": networkIDHeader,
"Name": nameHeader, "Name": formatter.NameHeader,
"Driver": driverHeader, "Driver": formatter.DriverHeader,
"Scope": scopeHeader, "Scope": formatter.ScopeHeader,
"IPv6": ipv6Header, "IPv6": ipv6Header,
"Internal": internalHeader, "Internal": internalHeader,
"Labels": labelsHeader, "Labels": formatter.LabelsHeader,
"CreatedAt": createdAtHeader, "CreatedAt": formatter.CreatedAtHeader,
} }
return ctx.Write(&networkCtx, render) return ctx.Write(&networkCtx, render)
} }
type networkHeaderContext map[string]string
func (c networkHeaderContext) Label(name string) string {
n := strings.Split(name, ".")
r := strings.NewReplacer("-", " ", "_", " ")
h := r.Replace(n[len(n)-1])
return h
}
type networkContext struct { type networkContext struct {
HeaderContext formatter.HeaderContext
trunc bool trunc bool
n types.NetworkResource n types.NetworkResource
} }
func (c *networkContext) MarshalJSON() ([]byte, error) { func (c *networkContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c) return formatter.MarshalJSON(c)
} }
func (c *networkContext) ID() string { func (c *networkContext) ID() string {

View File

@ -1,4 +1,4 @@
package formatter package network
import ( import (
"bytes" "bytes"
@ -8,6 +8,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/stringid"
"gotest.tools/assert" "gotest.tools/assert"
@ -61,7 +63,7 @@ func TestNetworkContext(t *testing.T) {
ctx = c.networkCtx ctx = c.networkCtx
v := c.call() v := c.call()
if strings.Contains(v, ",") { if strings.Contains(v, ",") {
compareMultipleValues(t, v, c.expValue) test.CompareMultipleValues(t, v, c.expValue)
} else if v != c.expValue { } else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v) t.Fatalf("Expected %s, was %s\n", c.expValue, v)
} }
@ -70,44 +72,44 @@ func TestNetworkContext(t *testing.T) {
func TestNetworkContextWrite(t *testing.T) { func TestNetworkContextWrite(t *testing.T) {
cases := []struct { cases := []struct {
context Context context formatter.Context
expected string expected string
}{ }{
// Errors // Errors
{ {
Context{Format: "{{InvalidFunction}}"}, formatter.Context{Format: "{{InvalidFunction}}"},
`Template parsing error: template: :1: function "InvalidFunction" not defined `Template parsing error: template: :1: function "InvalidFunction" not defined
`, `,
}, },
{ {
Context{Format: "{{nil}}"}, formatter.Context{Format: "{{nil}}"},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`, `,
}, },
// Table format // Table format
{ {
Context{Format: NewNetworkFormat("table", false)}, formatter.Context{Format: NewFormat("table", false)},
`NETWORK ID NAME DRIVER SCOPE `NETWORK ID NAME DRIVER SCOPE
networkID1 foobar_baz foo local networkID1 foobar_baz foo local
networkID2 foobar_bar bar local networkID2 foobar_bar bar local
`, `,
}, },
{ {
Context{Format: NewNetworkFormat("table", true)}, formatter.Context{Format: NewFormat("table", true)},
`networkID1 `networkID1
networkID2 networkID2
`, `,
}, },
{ {
Context{Format: NewNetworkFormat("table {{.Name}}", false)}, formatter.Context{Format: NewFormat("table {{.Name}}", false)},
`NAME `NAME
foobar_baz foobar_baz
foobar_bar foobar_bar
`, `,
}, },
{ {
Context{Format: NewNetworkFormat("table {{.Name}}", true)}, formatter.Context{Format: NewFormat("table {{.Name}}", true)},
`NAME `NAME
foobar_baz foobar_baz
foobar_bar foobar_bar
@ -115,7 +117,7 @@ foobar_bar
}, },
// Raw Format // Raw Format
{ {
Context{Format: NewNetworkFormat("raw", false)}, formatter.Context{Format: NewFormat("raw", false)},
`network_id: networkID1 `network_id: networkID1
name: foobar_baz name: foobar_baz
driver: foo driver: foo
@ -129,21 +131,21 @@ scope: local
`, `,
}, },
{ {
Context{Format: NewNetworkFormat("raw", true)}, formatter.Context{Format: NewFormat("raw", true)},
`network_id: networkID1 `network_id: networkID1
network_id: networkID2 network_id: networkID2
`, `,
}, },
// Custom Format // Custom Format
{ {
Context{Format: NewNetworkFormat("{{.Name}}", false)}, formatter.Context{Format: NewFormat("{{.Name}}", false)},
`foobar_baz `foobar_baz
foobar_bar foobar_bar
`, `,
}, },
// Custom Format with CreatedAt // Custom Format with CreatedAt
{ {
Context{Format: NewNetworkFormat("{{.Name}} {{.CreatedAt}}", false)}, formatter.Context{Format: NewFormat("{{.Name}} {{.CreatedAt}}", false)},
`foobar_baz 2016-01-01 00:00:00 +0000 UTC `foobar_baz 2016-01-01 00:00:00 +0000 UTC
foobar_bar 2017-01-01 00:00:00 +0000 UTC foobar_bar 2017-01-01 00:00:00 +0000 UTC
`, `,
@ -160,7 +162,7 @@ foobar_bar 2017-01-01 00:00:00 +0000 UTC
} }
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
testcase.context.Output = out testcase.context.Output = out
err := NetworkWrite(testcase.context, networks) err := FormatWrite(testcase.context, networks)
if err != nil { if err != nil {
assert.Error(t, err, testcase.expected) assert.Error(t, err, testcase.expected)
} else { } else {
@ -180,7 +182,7 @@ func TestNetworkContextWriteJSON(t *testing.T) {
} }
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
err := NetworkWrite(Context{Format: "{{json .}}", Output: out}, networks) err := FormatWrite(formatter.Context{Format: "{{json .}}", Output: out}, networks)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -199,7 +201,7 @@ func TestNetworkContextWriteJSONField(t *testing.T) {
{ID: "networkID2", Name: "foobar_bar"}, {ID: "networkID2", Name: "foobar_bar"},
} }
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
err := NetworkWrite(Context{Format: "{{json .ID}}", Output: out}, networks) err := FormatWrite(formatter.Context{Format: "{{json .ID}}", Output: out}, networks)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -65,8 +65,8 @@ func runList(dockerCli command.Cli, options listOptions) error {
networksCtx := formatter.Context{ networksCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCli.Out(),
Format: formatter.NewNetworkFormat(format, options.quiet), Format: NewFormat(format, options.quiet),
Trunc: !options.noTrunc, Trunc: !options.noTrunc,
} }
return formatter.NetworkWrite(networksCtx, networkResources) return FormatWrite(networksCtx, networkResources)
} }

View File

@ -1,4 +1,4 @@
package formatter package node
import ( import (
"encoding/base64" "encoding/base64"
@ -7,6 +7,7 @@ import (
"strings" "strings"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/inspect" "github.com/docker/cli/cli/command/inspect"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
@ -14,8 +15,8 @@ import (
) )
const ( const (
defaultNodeTableFormat = "table {{.ID}} {{if .Self}}*{{else}} {{ end }}\t{{.Hostname}}\t{{.Status}}\t{{.Availability}}\t{{.ManagerStatus}}\t{{.EngineVersion}}" defaultNodeTableFormat = "table {{.ID}} {{if .Self}}*{{else}} {{ end }}\t{{.Hostname}}\t{{.Status}}\t{{.Availability}}\t{{.ManagerStatus}}\t{{.EngineVersion}}"
nodeInspectPrettyTemplate Format = `ID: {{.ID}} nodeInspectPrettyTemplate formatter.Format = `ID: {{.ID}}
{{- if .Name }} {{- if .Name }}
Name: {{.Name}} Name: {{.Name}}
{{- end }} {{- end }}
@ -79,28 +80,28 @@ TLS Info:
tlsStatusHeader = "TLS STATUS" tlsStatusHeader = "TLS STATUS"
) )
// NewNodeFormat returns a Format for rendering using a node Context // NewFormat returns a Format for rendering using a node Context
func NewNodeFormat(source string, quiet bool) Format { func NewFormat(source string, quiet bool) formatter.Format {
switch source { switch source {
case PrettyFormatKey: case formatter.PrettyFormatKey:
return nodeInspectPrettyTemplate return nodeInspectPrettyTemplate
case TableFormatKey: case formatter.TableFormatKey:
if quiet { if quiet {
return defaultQuietFormat return formatter.DefaultQuietFormat
} }
return defaultNodeTableFormat return defaultNodeTableFormat
case RawFormatKey: case formatter.RawFormatKey:
if quiet { if quiet {
return `node_id: {{.ID}}` return `node_id: {{.ID}}`
} }
return `node_id: {{.ID}}\nhostname: {{.Hostname}}\nstatus: {{.Status}}\navailability: {{.Availability}}\nmanager_status: {{.ManagerStatus}}\n` return `node_id: {{.ID}}\nhostname: {{.Hostname}}\nstatus: {{.Status}}\navailability: {{.Availability}}\nmanager_status: {{.ManagerStatus}}\n`
} }
return Format(source) return formatter.Format(source)
} }
// NodeWrite writes the context // FormatWrite writes the context
func NodeWrite(ctx Context, nodes []swarm.Node, info types.Info) error { func FormatWrite(ctx formatter.Context, nodes []swarm.Node, info types.Info) error {
render := func(format func(subContext subContext) error) error { render := func(format func(subContext formatter.SubContext) error) error {
for _, node := range nodes { for _, node := range nodes {
nodeCtx := &nodeContext{n: node, info: info} nodeCtx := &nodeContext{n: node, info: info}
if err := format(nodeCtx); err != nil { if err := format(nodeCtx); err != nil {
@ -109,31 +110,28 @@ func NodeWrite(ctx Context, nodes []swarm.Node, info types.Info) error {
} }
return nil return nil
} }
header := nodeHeaderContext{ nodeCtx := nodeContext{}
nodeCtx.Header = formatter.SubHeaderContext{
"ID": nodeIDHeader, "ID": nodeIDHeader,
"Self": selfHeader, "Self": selfHeader,
"Hostname": hostnameHeader, "Hostname": hostnameHeader,
"Status": statusHeader, "Status": formatter.StatusHeader,
"Availability": availabilityHeader, "Availability": availabilityHeader,
"ManagerStatus": managerStatusHeader, "ManagerStatus": managerStatusHeader,
"EngineVersion": engineVersionHeader, "EngineVersion": engineVersionHeader,
"TLSStatus": tlsStatusHeader, "TLSStatus": tlsStatusHeader,
} }
nodeCtx := nodeContext{}
nodeCtx.header = header
return ctx.Write(&nodeCtx, render) return ctx.Write(&nodeCtx, render)
} }
type nodeHeaderContext map[string]string
type nodeContext struct { type nodeContext struct {
HeaderContext formatter.HeaderContext
n swarm.Node n swarm.Node
info types.Info info types.Info
} }
func (c *nodeContext) MarshalJSON() ([]byte, error) { func (c *nodeContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c) return formatter.MarshalJSON(c)
} }
func (c *nodeContext) ID() string { func (c *nodeContext) ID() string {
@ -182,12 +180,12 @@ func (c *nodeContext) EngineVersion() string {
return c.n.Description.Engine.EngineVersion return c.n.Description.Engine.EngineVersion
} }
// NodeInspectWrite renders the context for a list of nodes // InspectFormatWrite renders the context for a list of nodes
func NodeInspectWrite(ctx Context, refs []string, getRef inspect.GetRefFunc) error { func InspectFormatWrite(ctx formatter.Context, refs []string, getRef inspect.GetRefFunc) error {
if ctx.Format != nodeInspectPrettyTemplate { if ctx.Format != nodeInspectPrettyTemplate {
return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef) return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef)
} }
render := func(format func(subContext subContext) error) error { render := func(format func(subContext formatter.SubContext) error) error {
for _, ref := range refs { for _, ref := range refs {
nodeI, _, err := getRef(ref) nodeI, _, err := getRef(ref)
if err != nil { if err != nil {
@ -208,7 +206,7 @@ func NodeInspectWrite(ctx Context, refs []string, getRef inspect.GetRefFunc) err
type nodeInspectContext struct { type nodeInspectContext struct {
swarm.Node swarm.Node
subContext formatter.SubContext
} }
func (ctx *nodeInspectContext) ID() string { func (ctx *nodeInspectContext) ID() string {

View File

@ -1,4 +1,4 @@
package formatter package node
import ( import (
"bytes" "bytes"
@ -7,6 +7,8 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/stringid"
@ -44,7 +46,7 @@ func TestNodeContext(t *testing.T) {
ctx = c.nodeCtx ctx = c.nodeCtx
v := c.call() v := c.call()
if strings.Contains(v, ",") { if strings.Contains(v, ",") {
compareMultipleValues(t, v, c.expValue) test.CompareMultipleValues(t, v, c.expValue)
} else if v != c.expValue { } else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v) t.Fatalf("Expected %s, was %s\n", c.expValue, v)
} }
@ -53,27 +55,27 @@ func TestNodeContext(t *testing.T) {
func TestNodeContextWrite(t *testing.T) { func TestNodeContextWrite(t *testing.T) {
cases := []struct { cases := []struct {
context Context context formatter.Context
expected string expected string
clusterInfo swarm.ClusterInfo clusterInfo swarm.ClusterInfo
}{ }{
// Errors // Errors
{ {
context: Context{Format: "{{InvalidFunction}}"}, context: formatter.Context{Format: "{{InvalidFunction}}"},
expected: `Template parsing error: template: :1: function "InvalidFunction" not defined expected: `Template parsing error: template: :1: function "InvalidFunction" not defined
`, `,
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
}, },
{ {
context: Context{Format: "{{nil}}"}, context: formatter.Context{Format: "{{nil}}"},
expected: `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command expected: `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`, `,
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
}, },
// Table format // Table format
{ {
context: Context{Format: NewNodeFormat("table", false)}, context: formatter.Context{Format: NewFormat("table", false)},
expected: `ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION expected: `ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
nodeID1 foobar_baz Foo Drain Leader 18.03.0-ce nodeID1 foobar_baz Foo Drain Leader 18.03.0-ce
nodeID2 foobar_bar Bar Active Reachable 1.2.3 nodeID2 foobar_bar Bar Active Reachable 1.2.3
@ -81,7 +83,7 @@ nodeID3 foobar_boo Boo Active
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
}, },
{ {
context: Context{Format: NewNodeFormat("table", true)}, context: formatter.Context{Format: NewFormat("table", true)},
expected: `nodeID1 expected: `nodeID1
nodeID2 nodeID2
nodeID3 nodeID3
@ -89,7 +91,7 @@ nodeID3
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
}, },
{ {
context: Context{Format: NewNodeFormat("table {{.Hostname}}", false)}, context: formatter.Context{Format: NewFormat("table {{.Hostname}}", false)},
expected: `HOSTNAME expected: `HOSTNAME
foobar_baz foobar_baz
foobar_bar foobar_bar
@ -98,7 +100,7 @@ foobar_boo
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
}, },
{ {
context: Context{Format: NewNodeFormat("table {{.Hostname}}", true)}, context: formatter.Context{Format: NewFormat("table {{.Hostname}}", true)},
expected: `HOSTNAME expected: `HOSTNAME
foobar_baz foobar_baz
foobar_bar foobar_bar
@ -107,7 +109,7 @@ foobar_boo
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
}, },
{ {
context: Context{Format: NewNodeFormat("table {{.ID}}\t{{.Hostname}}\t{{.TLSStatus}}", false)}, context: formatter.Context{Format: NewFormat("table {{.ID}}\t{{.Hostname}}\t{{.TLSStatus}}", false)},
expected: `ID HOSTNAME TLS STATUS expected: `ID HOSTNAME TLS STATUS
nodeID1 foobar_baz Needs Rotation nodeID1 foobar_baz Needs Rotation
nodeID2 foobar_bar Ready nodeID2 foobar_bar Ready
@ -116,7 +118,7 @@ nodeID3 foobar_boo Unknown
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
}, },
{ // no cluster TLS status info, TLS status for all nodes is unknown { // no cluster TLS status info, TLS status for all nodes is unknown
context: Context{Format: NewNodeFormat("table {{.ID}}\t{{.Hostname}}\t{{.TLSStatus}}", false)}, context: formatter.Context{Format: NewFormat("table {{.ID}}\t{{.Hostname}}\t{{.TLSStatus}}", false)},
expected: `ID HOSTNAME TLS STATUS expected: `ID HOSTNAME TLS STATUS
nodeID1 foobar_baz Unknown nodeID1 foobar_baz Unknown
nodeID2 foobar_bar Unknown nodeID2 foobar_bar Unknown
@ -126,7 +128,7 @@ nodeID3 foobar_boo Unknown
}, },
// Raw Format // Raw Format
{ {
context: Context{Format: NewNodeFormat("raw", false)}, context: formatter.Context{Format: NewFormat("raw", false)},
expected: `node_id: nodeID1 expected: `node_id: nodeID1
hostname: foobar_baz hostname: foobar_baz
status: Foo status: Foo
@ -147,7 +149,7 @@ manager_status: ` + "\n\n", // to preserve whitespace
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
}, },
{ {
context: Context{Format: NewNodeFormat("raw", true)}, context: formatter.Context{Format: NewFormat("raw", true)},
expected: `node_id: nodeID1 expected: `node_id: nodeID1
node_id: nodeID2 node_id: nodeID2
node_id: nodeID3 node_id: nodeID3
@ -156,7 +158,7 @@ node_id: nodeID3
}, },
// Custom Format // Custom Format
{ {
context: Context{Format: NewNodeFormat("{{.Hostname}} {{.TLSStatus}}", false)}, context: formatter.Context{Format: NewFormat("{{.Hostname}} {{.TLSStatus}}", false)},
expected: `foobar_baz Needs Rotation expected: `foobar_baz Needs Rotation
foobar_bar Ready foobar_bar Ready
foobar_boo Unknown foobar_boo Unknown
@ -201,7 +203,7 @@ foobar_boo Unknown
} }
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
testcase.context.Output = out testcase.context.Output = out
err := NodeWrite(testcase.context, nodes, types.Info{Swarm: swarm.Info{Cluster: &testcase.clusterInfo}}) err := FormatWrite(testcase.context, nodes, types.Info{Swarm: swarm.Info{Cluster: &testcase.clusterInfo}})
if err != nil { if err != nil {
assert.Error(t, err, testcase.expected) assert.Error(t, err, testcase.expected)
} else { } else {
@ -247,7 +249,7 @@ func TestNodeContextWriteJSON(t *testing.T) {
{ID: "nodeID3", Description: swarm.NodeDescription{Hostname: "foobar_boo", Engine: swarm.EngineDescription{EngineVersion: "18.03.0-ce"}}}, {ID: "nodeID3", Description: swarm.NodeDescription{Hostname: "foobar_boo", Engine: swarm.EngineDescription{EngineVersion: "18.03.0-ce"}}},
} }
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
err := NodeWrite(Context{Format: "{{json .}}", Output: out}, nodes, testcase.info) err := FormatWrite(formatter.Context{Format: "{{json .}}", Output: out}, nodes, testcase.info)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -267,7 +269,7 @@ func TestNodeContextWriteJSONField(t *testing.T) {
{ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}}, {ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}},
} }
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
err := NodeWrite(Context{Format: "{{json .ID}}", Output: out}, nodes, types.Info{}) err := FormatWrite(formatter.Context{Format: "{{json .ID}}", Output: out}, nodes, types.Info{})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -311,11 +313,11 @@ func TestNodeInspectWriteContext(t *testing.T) {
}, },
} }
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
context := Context{ context := formatter.Context{
Format: NewNodeFormat("pretty", false), Format: NewFormat("pretty", false),
Output: out, Output: out,
} }
err := NodeInspectWrite(context, []string{"nodeID1"}, func(string) (interface{}, []byte, error) { err := InspectFormatWrite(context, []string{"nodeID1"}, func(string) (interface{}, []byte, error) {
return node, nil, nil return node, nil, nil
}) })
if err != nil { if err != nil {

View File

@ -62,10 +62,10 @@ func runInspect(dockerCli command.Cli, opts inspectOptions) error {
nodeCtx := formatter.Context{ nodeCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCli.Out(),
Format: formatter.NewNodeFormat(f, false), Format: NewFormat(f, false),
} }
if err := formatter.NodeInspectWrite(nodeCtx, opts.nodeIds, getRef); err != nil { if err := InspectFormatWrite(nodeCtx, opts.nodeIds, getRef); err != nil {
return cli.StatusError{StatusCode: 1, Status: err.Error()} return cli.StatusError{StatusCode: 1, Status: err.Error()}
} }
return nil return nil

View File

@ -69,10 +69,10 @@ func runList(dockerCli command.Cli, options listOptions) error {
nodesCtx := formatter.Context{ nodesCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCli.Out(),
Format: formatter.NewNodeFormat(format, options.quiet), Format: NewFormat(format, options.quiet),
} }
sort.Slice(nodes, func(i, j int) bool { sort.Slice(nodes, func(i, j int) bool {
return sortorder.NaturalLess(nodes[i].Description.Hostname, nodes[j].Description.Hostname) return sortorder.NaturalLess(nodes[i].Description.Hostname, nodes[j].Description.Hostname)
}) })
return formatter.NodeWrite(nodesCtx, nodes, info) return FormatWrite(nodesCtx, nodes, info)
} }

View File

@ -1,8 +1,9 @@
package formatter package plugin
import ( import (
"strings" "strings"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/stringid"
) )
@ -10,31 +11,30 @@ import (
const ( const (
defaultPluginTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Description}}\t{{.Enabled}}" defaultPluginTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Description}}\t{{.Enabled}}"
pluginIDHeader = "ID" enabledHeader = "ENABLED"
descriptionHeader = "DESCRIPTION" pluginIDHeader = "ID"
enabledHeader = "ENABLED"
) )
// NewPluginFormat returns a Format for rendering using a plugin Context // NewFormat returns a Format for rendering using a plugin Context
func NewPluginFormat(source string, quiet bool) Format { func NewFormat(source string, quiet bool) formatter.Format {
switch source { switch source {
case TableFormatKey: case formatter.TableFormatKey:
if quiet { if quiet {
return defaultQuietFormat return formatter.DefaultQuietFormat
} }
return defaultPluginTableFormat return defaultPluginTableFormat
case RawFormatKey: case formatter.RawFormatKey:
if quiet { if quiet {
return `plugin_id: {{.ID}}` return `plugin_id: {{.ID}}`
} }
return `plugin_id: {{.ID}}\nname: {{.Name}}\ndescription: {{.Description}}\nenabled: {{.Enabled}}\n` return `plugin_id: {{.ID}}\nname: {{.Name}}\ndescription: {{.Description}}\nenabled: {{.Enabled}}\n`
} }
return Format(source) return formatter.Format(source)
} }
// PluginWrite writes the context // FormatWrite writes the context
func PluginWrite(ctx Context, plugins []*types.Plugin) error { func FormatWrite(ctx formatter.Context, plugins []*types.Plugin) error {
render := func(format func(subContext subContext) error) error { render := func(format func(subContext formatter.SubContext) error) error {
for _, plugin := range plugins { for _, plugin := range plugins {
pluginCtx := &pluginContext{trunc: ctx.Trunc, p: *plugin} pluginCtx := &pluginContext{trunc: ctx.Trunc, p: *plugin}
if err := format(pluginCtx); err != nil { if err := format(pluginCtx); err != nil {
@ -44,24 +44,24 @@ func PluginWrite(ctx Context, plugins []*types.Plugin) error {
return nil return nil
} }
pluginCtx := pluginContext{} pluginCtx := pluginContext{}
pluginCtx.header = map[string]string{ pluginCtx.Header = formatter.SubHeaderContext{
"ID": pluginIDHeader, "ID": pluginIDHeader,
"Name": nameHeader, "Name": formatter.NameHeader,
"Description": descriptionHeader, "Description": formatter.DescriptionHeader,
"Enabled": enabledHeader, "Enabled": enabledHeader,
"PluginReference": imageHeader, "PluginReference": formatter.ImageHeader,
} }
return ctx.Write(&pluginCtx, render) return ctx.Write(&pluginCtx, render)
} }
type pluginContext struct { type pluginContext struct {
HeaderContext formatter.HeaderContext
trunc bool trunc bool
p types.Plugin p types.Plugin
} }
func (c *pluginContext) MarshalJSON() ([]byte, error) { func (c *pluginContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c) return formatter.MarshalJSON(c)
} }
func (c *pluginContext) ID() string { func (c *pluginContext) ID() string {
@ -79,7 +79,7 @@ func (c *pluginContext) Description() string {
desc := strings.Replace(c.p.Config.Description, "\n", "", -1) desc := strings.Replace(c.p.Config.Description, "\n", "", -1)
desc = strings.Replace(desc, "\r", "", -1) desc = strings.Replace(desc, "\r", "", -1)
if c.trunc { if c.trunc {
desc = Ellipsis(desc, 45) desc = formatter.Ellipsis(desc, 45)
} }
return desc return desc

View File

@ -1,4 +1,4 @@
package formatter package plugin
import ( import (
"bytes" "bytes"
@ -6,6 +6,8 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/stringid"
"gotest.tools/assert" "gotest.tools/assert"
@ -41,7 +43,7 @@ func TestPluginContext(t *testing.T) {
ctx = c.pluginCtx ctx = c.pluginCtx
v := c.call() v := c.call()
if strings.Contains(v, ",") { if strings.Contains(v, ",") {
compareMultipleValues(t, v, c.expValue) test.CompareMultipleValues(t, v, c.expValue)
} else if v != c.expValue { } else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v) t.Fatalf("Expected %s, was %s\n", c.expValue, v)
} }
@ -50,44 +52,44 @@ func TestPluginContext(t *testing.T) {
func TestPluginContextWrite(t *testing.T) { func TestPluginContextWrite(t *testing.T) {
cases := []struct { cases := []struct {
context Context context formatter.Context
expected string expected string
}{ }{
// Errors // Errors
{ {
Context{Format: "{{InvalidFunction}}"}, formatter.Context{Format: "{{InvalidFunction}}"},
`Template parsing error: template: :1: function "InvalidFunction" not defined `Template parsing error: template: :1: function "InvalidFunction" not defined
`, `,
}, },
{ {
Context{Format: "{{nil}}"}, formatter.Context{Format: "{{nil}}"},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`, `,
}, },
// Table format // Table format
{ {
Context{Format: NewPluginFormat("table", false)}, formatter.Context{Format: NewFormat("table", false)},
`ID NAME DESCRIPTION ENABLED `ID NAME DESCRIPTION ENABLED
pluginID1 foobar_baz description 1 true pluginID1 foobar_baz description 1 true
pluginID2 foobar_bar description 2 false pluginID2 foobar_bar description 2 false
`, `,
}, },
{ {
Context{Format: NewPluginFormat("table", true)}, formatter.Context{Format: NewFormat("table", true)},
`pluginID1 `pluginID1
pluginID2 pluginID2
`, `,
}, },
{ {
Context{Format: NewPluginFormat("table {{.Name}}", false)}, formatter.Context{Format: NewFormat("table {{.Name}}", false)},
`NAME `NAME
foobar_baz foobar_baz
foobar_bar foobar_bar
`, `,
}, },
{ {
Context{Format: NewPluginFormat("table {{.Name}}", true)}, formatter.Context{Format: NewFormat("table {{.Name}}", true)},
`NAME `NAME
foobar_baz foobar_baz
foobar_bar foobar_bar
@ -95,7 +97,7 @@ foobar_bar
}, },
// Raw Format // Raw Format
{ {
Context{Format: NewPluginFormat("raw", false)}, formatter.Context{Format: NewFormat("raw", false)},
`plugin_id: pluginID1 `plugin_id: pluginID1
name: foobar_baz name: foobar_baz
description: description 1 description: description 1
@ -109,14 +111,14 @@ enabled: false
`, `,
}, },
{ {
Context{Format: NewPluginFormat("raw", true)}, formatter.Context{Format: NewFormat("raw", true)},
`plugin_id: pluginID1 `plugin_id: pluginID1
plugin_id: pluginID2 plugin_id: pluginID2
`, `,
}, },
// Custom Format // Custom Format
{ {
Context{Format: NewPluginFormat("{{.Name}}", false)}, formatter.Context{Format: NewFormat("{{.Name}}", false)},
`foobar_baz `foobar_baz
foobar_bar foobar_bar
`, `,
@ -130,7 +132,7 @@ foobar_bar
} }
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
testcase.context.Output = out testcase.context.Output = out
err := PluginWrite(testcase.context, plugins) err := FormatWrite(testcase.context, plugins)
if err != nil { if err != nil {
assert.Error(t, err, testcase.expected) assert.Error(t, err, testcase.expected)
} else { } else {
@ -150,7 +152,7 @@ func TestPluginContextWriteJSON(t *testing.T) {
} }
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
err := PluginWrite(Context{Format: "{{json .}}", Output: out}, plugins) err := FormatWrite(formatter.Context{Format: "{{json .}}", Output: out}, plugins)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -169,7 +171,7 @@ func TestPluginContextWriteJSONField(t *testing.T) {
{ID: "pluginID2", Name: "foobar_bar"}, {ID: "pluginID2", Name: "foobar_bar"},
} }
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
err := PluginWrite(Context{Format: "{{json .ID}}", Output: out}, plugins) err := FormatWrite(formatter.Context{Format: "{{json .ID}}", Output: out}, plugins)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -63,8 +63,8 @@ func runList(dockerCli command.Cli, options listOptions) error {
pluginsCtx := formatter.Context{ pluginsCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCli.Out(),
Format: formatter.NewPluginFormat(format, options.quiet), Format: NewFormat(format, options.quiet),
Trunc: !options.noTrunc, Trunc: !options.noTrunc,
} }
return formatter.PluginWrite(pluginsCtx, plugins) return FormatWrite(pluginsCtx, plugins)
} }

View File

@ -1,9 +1,10 @@
package formatter package registry
import ( import (
"strconv" "strconv"
"strings" "strings"
"github.com/docker/cli/cli/command/formatter"
registry "github.com/docker/docker/api/types/registry" registry "github.com/docker/docker/api/types/registry"
) )
@ -16,19 +17,19 @@ const (
) )
// NewSearchFormat returns a Format for rendering using a network Context // NewSearchFormat returns a Format for rendering using a network Context
func NewSearchFormat(source string) Format { func NewSearchFormat(source string) formatter.Format {
switch source { switch source {
case "": case "":
return defaultSearchTableFormat return defaultSearchTableFormat
case TableFormatKey: case formatter.TableFormatKey:
return defaultSearchTableFormat return defaultSearchTableFormat
} }
return Format(source) return formatter.Format(source)
} }
// SearchWrite writes the context // SearchWrite writes the context
func SearchWrite(ctx Context, results []registry.SearchResult, auto bool, stars int) error { func SearchWrite(ctx formatter.Context, results []registry.SearchResult, auto bool, stars int) error {
render := func(format func(subContext subContext) error) error { render := func(format func(subContext formatter.SubContext) error) error {
for _, result := range results { for _, result := range results {
// --automated and -s, --stars are deprecated since Docker 1.12 // --automated and -s, --stars are deprecated since Docker 1.12
if (auto && !result.IsAutomated) || (stars > result.StarCount) { if (auto && !result.IsAutomated) || (stars > result.StarCount) {
@ -42,9 +43,9 @@ func SearchWrite(ctx Context, results []registry.SearchResult, auto bool, stars
return nil return nil
} }
searchCtx := searchContext{} searchCtx := searchContext{}
searchCtx.header = map[string]string{ searchCtx.Header = formatter.SubHeaderContext{
"Name": nameHeader, "Name": formatter.NameHeader,
"Description": descriptionHeader, "Description": formatter.DescriptionHeader,
"StarCount": starsHeader, "StarCount": starsHeader,
"IsOfficial": officialHeader, "IsOfficial": officialHeader,
"IsAutomated": automatedHeader, "IsAutomated": automatedHeader,
@ -53,7 +54,7 @@ func SearchWrite(ctx Context, results []registry.SearchResult, auto bool, stars
} }
type searchContext struct { type searchContext struct {
HeaderContext formatter.HeaderContext
trunc bool trunc bool
json bool json bool
s registry.SearchResult s registry.SearchResult
@ -61,7 +62,7 @@ type searchContext struct {
func (c *searchContext) MarshalJSON() ([]byte, error) { func (c *searchContext) MarshalJSON() ([]byte, error) {
c.json = true c.json = true
return marshalJSON(c) return formatter.MarshalJSON(c)
} }
func (c *searchContext) Name() string { func (c *searchContext) Name() string {
@ -72,7 +73,7 @@ func (c *searchContext) Description() string {
desc := strings.Replace(c.s.Description, "\n", " ", -1) desc := strings.Replace(c.s.Description, "\n", " ", -1)
desc = strings.Replace(desc, "\r", " ", -1) desc = strings.Replace(desc, "\r", " ", -1)
if c.trunc { if c.trunc {
desc = Ellipsis(desc, 45) desc = formatter.Ellipsis(desc, 45)
} }
return desc return desc
} }

View File

@ -1,4 +1,4 @@
package formatter package registry
import ( import (
"bytes" "bytes"
@ -6,6 +6,8 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/internal/test"
registrytypes "github.com/docker/docker/api/types/registry" registrytypes "github.com/docker/docker/api/types/registry"
"gotest.tools/assert" "gotest.tools/assert"
is "gotest.tools/assert/cmp" is "gotest.tools/assert/cmp"
@ -46,7 +48,7 @@ func TestSearchContext(t *testing.T) {
ctx = c.searchCtx ctx = c.searchCtx
v := c.call() v := c.call()
if strings.Contains(v, ",") { if strings.Contains(v, ",") {
compareMultipleValues(t, v, c.expValue) test.CompareMultipleValues(t, v, c.expValue)
} else if v != c.expValue { } else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v) t.Fatalf("Expected %s, was %s\n", c.expValue, v)
} }
@ -79,7 +81,7 @@ func TestSearchContextDescription(t *testing.T) {
{searchContext{ {searchContext{
s: registrytypes.SearchResult{Description: longDescription}, s: registrytypes.SearchResult{Description: longDescription},
trunc: true, trunc: true,
}, Ellipsis(longDescription, 45), ctx.Description}, }, formatter.Ellipsis(longDescription, 45), ctx.Description},
{searchContext{ {searchContext{
s: registrytypes.SearchResult{Description: descriptionWReturns}, s: registrytypes.SearchResult{Description: descriptionWReturns},
trunc: false, trunc: false,
@ -87,14 +89,14 @@ func TestSearchContextDescription(t *testing.T) {
{searchContext{ {searchContext{
s: registrytypes.SearchResult{Description: descriptionWReturns}, s: registrytypes.SearchResult{Description: descriptionWReturns},
trunc: true, trunc: true,
}, Ellipsis(longDescription, 45), ctx.Description}, }, formatter.Ellipsis(longDescription, 45), ctx.Description},
} }
for _, c := range cases { for _, c := range cases {
ctx = c.searchCtx ctx = c.searchCtx
v := c.call() v := c.call()
if strings.Contains(v, ",") { if strings.Contains(v, ",") {
compareMultipleValues(t, v, c.expValue) test.CompareMultipleValues(t, v, c.expValue)
} else if v != c.expValue { } else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v) t.Fatalf("Expected %s, was %s\n", c.expValue, v)
} }
@ -103,28 +105,28 @@ func TestSearchContextDescription(t *testing.T) {
func TestSearchContextWrite(t *testing.T) { func TestSearchContextWrite(t *testing.T) {
cases := []struct { cases := []struct {
context Context context formatter.Context
expected string expected string
}{ }{
// Errors // Errors
{ {
Context{Format: "{{InvalidFunction}}"}, formatter.Context{Format: "{{InvalidFunction}}"},
`Template parsing error: template: :1: function "InvalidFunction" not defined `Template parsing error: template: :1: function "InvalidFunction" not defined
`, `,
}, },
{ {
Context{Format: "{{nil}}"}, formatter.Context{Format: "{{nil}}"},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`, `,
}, },
// Table format // Table format
{ {
Context{Format: NewSearchFormat("table")}, formatter.Context{Format: NewSearchFormat("table")},
string(golden.Get(t, "search-context-write-table.golden")), string(golden.Get(t, "search-context-write-table.golden")),
}, },
{ {
Context{Format: NewSearchFormat("table {{.Name}}")}, formatter.Context{Format: NewSearchFormat("table {{.Name}}")},
`NAME `NAME
result1 result1
result2 result2
@ -132,14 +134,14 @@ result2
}, },
// Custom Format // Custom Format
{ {
Context{Format: NewSearchFormat("{{.Name}}")}, formatter.Context{Format: NewSearchFormat("{{.Name}}")},
`result1 `result1
result2 result2
`, `,
}, },
// Custom Format with CreatedAt // Custom Format with CreatedAt
{ {
Context{Format: NewSearchFormat("{{.Name}} {{.StarCount}}")}, formatter.Context{Format: NewSearchFormat("{{.Name}} {{.StarCount}}")},
`result1 5000 `result1 5000
result2 5 result2 5
`, `,
@ -164,19 +166,19 @@ result2 5
func TestSearchContextWriteAutomated(t *testing.T) { func TestSearchContextWriteAutomated(t *testing.T) {
cases := []struct { cases := []struct {
context Context context formatter.Context
expected string expected string
}{ }{
// Table format // Table format
{ {
Context{Format: NewSearchFormat("table")}, formatter.Context{Format: NewSearchFormat("table")},
`NAME DESCRIPTION STARS OFFICIAL AUTOMATED `NAME DESCRIPTION STARS OFFICIAL AUTOMATED
result2 Not official 5 [OK] result2 Not official 5 [OK]
`, `,
}, },
{ {
Context{Format: NewSearchFormat("table {{.Name}}")}, formatter.Context{Format: NewSearchFormat("table {{.Name}}")},
`NAME `NAME
result2 result2
`, `,
@ -201,17 +203,17 @@ result2
func TestSearchContextWriteStars(t *testing.T) { func TestSearchContextWriteStars(t *testing.T) {
cases := []struct { cases := []struct {
context Context context formatter.Context
expected string expected string
}{ }{
// Table format // Table format
{ {
Context{Format: NewSearchFormat("table")}, formatter.Context{Format: NewSearchFormat("table")},
string(golden.Get(t, "search-context-write-stars-table.golden")), string(golden.Get(t, "search-context-write-stars-table.golden")),
}, },
{ {
Context{Format: NewSearchFormat("table {{.Name}}")}, formatter.Context{Format: NewSearchFormat("table {{.Name}}")},
`NAME `NAME
result1 result1
`, `,
@ -245,7 +247,7 @@ func TestSearchContextWriteJSON(t *testing.T) {
} }
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
err := SearchWrite(Context{Format: "{{json .}}", Output: out}, results, false, 0) err := SearchWrite(formatter.Context{Format: "{{json .}}", Output: out}, results, false, 0)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -265,7 +267,7 @@ func TestSearchContextWriteJSONField(t *testing.T) {
{Name: "result2", Description: "Not official", StarCount: 5, IsOfficial: false, IsAutomated: true}, {Name: "result2", Description: "Not official", StarCount: 5, IsOfficial: false, IsAutomated: true},
} }
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
err := SearchWrite(Context{Format: "{{json .Name}}", Output: out}, results, false, 0) err := SearchWrite(formatter.Context{Format: "{{json .Name}}", Output: out}, results, false, 0)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -90,8 +90,8 @@ func runSearch(dockerCli command.Cli, options searchOptions) error {
}) })
searchCtx := formatter.Context{ searchCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCli.Out(),
Format: formatter.NewSearchFormat(options.format), Format: NewSearchFormat(options.format),
Trunc: !options.noTrunc, Trunc: !options.noTrunc,
} }
return formatter.SearchWrite(searchCtx, results, options.automated, int(options.stars)) return SearchWrite(searchCtx, results, options.automated, int(options.stars))
} }

View File

@ -1,4 +1,4 @@
package formatter package secret
import ( import (
"fmt" "fmt"
@ -6,17 +6,18 @@ import (
"time" "time"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/inspect" "github.com/docker/cli/cli/command/inspect"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
units "github.com/docker/go-units" units "github.com/docker/go-units"
) )
const ( const (
defaultSecretTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Driver}}\t{{.CreatedAt}}\t{{.UpdatedAt}}" defaultSecretTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Driver}}\t{{.CreatedAt}}\t{{.UpdatedAt}}"
secretIDHeader = "ID" secretIDHeader = "ID"
secretCreatedHeader = "CREATED" secretCreatedHeader = "CREATED"
secretUpdatedHeader = "UPDATED" secretUpdatedHeader = "UPDATED"
secretInspectPrettyTemplate Format = `ID: {{.ID}} secretInspectPrettyTemplate formatter.Format = `ID: {{.ID}}
Name: {{.Name}} Name: {{.Name}}
{{- if .Labels }} {{- if .Labels }}
Labels: Labels:
@ -28,23 +29,23 @@ Created at: {{.CreatedAt}}
Updated at: {{.UpdatedAt}}` Updated at: {{.UpdatedAt}}`
) )
// NewSecretFormat returns a Format for rendering using a secret Context // NewFormat returns a Format for rendering using a secret Context
func NewSecretFormat(source string, quiet bool) Format { func NewFormat(source string, quiet bool) formatter.Format {
switch source { switch source {
case PrettyFormatKey: case formatter.PrettyFormatKey:
return secretInspectPrettyTemplate return secretInspectPrettyTemplate
case TableFormatKey: case formatter.TableFormatKey:
if quiet { if quiet {
return defaultQuietFormat return formatter.DefaultQuietFormat
} }
return defaultSecretTableFormat return defaultSecretTableFormat
} }
return Format(source) return formatter.Format(source)
} }
// SecretWrite writes the context // FormatWrite writes the context
func SecretWrite(ctx Context, secrets []swarm.Secret) error { func FormatWrite(ctx formatter.Context, secrets []swarm.Secret) error {
render := func(format func(subContext subContext) error) error { render := func(format func(subContext formatter.SubContext) error) error {
for _, secret := range secrets { for _, secret := range secrets {
secretCtx := &secretContext{s: secret} secretCtx := &secretContext{s: secret}
if err := format(secretCtx); err != nil { if err := format(secretCtx); err != nil {
@ -59,24 +60,24 @@ func SecretWrite(ctx Context, secrets []swarm.Secret) error {
func newSecretContext() *secretContext { func newSecretContext() *secretContext {
sCtx := &secretContext{} sCtx := &secretContext{}
sCtx.header = map[string]string{ sCtx.Header = formatter.SubHeaderContext{
"ID": secretIDHeader, "ID": secretIDHeader,
"Name": nameHeader, "Name": formatter.NameHeader,
"Driver": driverHeader, "Driver": formatter.DriverHeader,
"CreatedAt": secretCreatedHeader, "CreatedAt": secretCreatedHeader,
"UpdatedAt": secretUpdatedHeader, "UpdatedAt": secretUpdatedHeader,
"Labels": labelsHeader, "Labels": formatter.LabelsHeader,
} }
return sCtx return sCtx
} }
type secretContext struct { type secretContext struct {
HeaderContext formatter.HeaderContext
s swarm.Secret s swarm.Secret
} }
func (c *secretContext) MarshalJSON() ([]byte, error) { func (c *secretContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c) return formatter.MarshalJSON(c)
} }
func (c *secretContext) ID() string { func (c *secretContext) ID() string {
@ -121,12 +122,12 @@ func (c *secretContext) Label(name string) string {
return c.s.Spec.Annotations.Labels[name] return c.s.Spec.Annotations.Labels[name]
} }
// SecretInspectWrite renders the context for a list of secrets // InspectFormatWrite renders the context for a list of secrets
func SecretInspectWrite(ctx Context, refs []string, getRef inspect.GetRefFunc) error { func InspectFormatWrite(ctx formatter.Context, refs []string, getRef inspect.GetRefFunc) error {
if ctx.Format != secretInspectPrettyTemplate { if ctx.Format != secretInspectPrettyTemplate {
return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef) return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef)
} }
render := func(format func(subContext subContext) error) error { render := func(format func(subContext formatter.SubContext) error) error {
for _, ref := range refs { for _, ref := range refs {
secretI, _, err := getRef(ref) secretI, _, err := getRef(ref)
if err != nil { if err != nil {
@ -147,7 +148,7 @@ func SecretInspectWrite(ctx Context, refs []string, getRef inspect.GetRefFunc) e
type secretInspectContext struct { type secretInspectContext struct {
swarm.Secret swarm.Secret
subContext formatter.SubContext
} }
func (ctx *secretInspectContext) ID() string { func (ctx *secretInspectContext) ID() string {

View File

@ -1,10 +1,11 @@
package formatter package secret
import ( import (
"bytes" "bytes"
"testing" "testing"
"time" "time"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"gotest.tools/assert" "gotest.tools/assert"
is "gotest.tools/assert/cmp" is "gotest.tools/assert/cmp"
@ -13,32 +14,32 @@ import (
func TestSecretContextFormatWrite(t *testing.T) { func TestSecretContextFormatWrite(t *testing.T) {
// Check default output format (verbose and non-verbose mode) for table headers // Check default output format (verbose and non-verbose mode) for table headers
cases := []struct { cases := []struct {
context Context context formatter.Context
expected string expected string
}{ }{
// Errors // Errors
{ {
Context{Format: "{{InvalidFunction}}"}, formatter.Context{Format: "{{InvalidFunction}}"},
`Template parsing error: template: :1: function "InvalidFunction" not defined `Template parsing error: template: :1: function "InvalidFunction" not defined
`, `,
}, },
{ {
Context{Format: "{{nil}}"}, formatter.Context{Format: "{{nil}}"},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`, `,
}, },
// Table format // Table format
{Context{Format: NewSecretFormat("table", false)}, {formatter.Context{Format: NewFormat("table", false)},
`ID NAME DRIVER CREATED UPDATED `ID NAME DRIVER CREATED UPDATED
1 passwords Less than a second ago Less than a second ago 1 passwords Less than a second ago Less than a second ago
2 id_rsa Less than a second ago Less than a second ago 2 id_rsa Less than a second ago Less than a second ago
`}, `},
{Context{Format: NewSecretFormat("table {{.Name}}", true)}, {formatter.Context{Format: NewFormat("table {{.Name}}", true)},
`NAME `NAME
passwords passwords
id_rsa id_rsa
`}, `},
{Context{Format: NewSecretFormat("{{.ID}}-{{.Name}}", false)}, {formatter.Context{Format: NewFormat("{{.ID}}-{{.Name}}", false)},
`1-passwords `1-passwords
2-id_rsa 2-id_rsa
`}, `},
@ -55,7 +56,7 @@ id_rsa
for _, testcase := range cases { for _, testcase := range cases {
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
testcase.context.Output = out testcase.context.Output = out
if err := SecretWrite(testcase.context, secrets); err != nil { if err := FormatWrite(testcase.context, secrets); err != nil {
assert.Error(t, err, testcase.expected) assert.Error(t, err, testcase.expected)
} else { } else {
assert.Check(t, is.Equal(testcase.expected, out.String())) assert.Check(t, is.Equal(testcase.expected, out.String()))

View File

@ -55,10 +55,10 @@ func runSecretInspect(dockerCli command.Cli, opts inspectOptions) error {
secretCtx := formatter.Context{ secretCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCli.Out(),
Format: formatter.NewSecretFormat(f, false), Format: NewFormat(f, false),
} }
if err := formatter.SecretInspectWrite(secretCtx, opts.names, getRef); err != nil { if err := InspectFormatWrite(secretCtx, opts.names, getRef); err != nil {
return cli.StatusError{StatusCode: 1, Status: err.Error()} return cli.StatusError{StatusCode: 1, Status: err.Error()}
} }
return nil return nil

View File

@ -63,7 +63,7 @@ func runSecretList(dockerCli command.Cli, options listOptions) error {
secretCtx := formatter.Context{ secretCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCli.Out(),
Format: formatter.NewSecretFormat(format, options.quiet), Format: NewFormat(format, options.quiet),
} }
return formatter.SecretWrite(secretCtx, secrets) return FormatWrite(secretCtx, secrets)
} }

View File

@ -1,4 +1,4 @@
package formatter package service
import ( import (
"fmt" "fmt"
@ -6,6 +6,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/inspect" "github.com/docker/cli/cli/command/inspect"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
@ -16,7 +17,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
const serviceInspectPrettyTemplate Format = ` const serviceInspectPrettyTemplate formatter.Format = `
ID: {{.ID}} ID: {{.ID}}
Name: {{.Name}} Name: {{.Name}}
{{- if .Labels }} {{- if .Labels }}
@ -144,13 +145,13 @@ Ports:
{{- end }} {{ end -}} {{- end }} {{ end -}}
` `
// NewServiceFormat returns a Format for rendering using a Context // NewFormat returns a Format for rendering using a Context
func NewServiceFormat(source string) Format { func NewFormat(source string) formatter.Format {
switch source { switch source {
case PrettyFormatKey: case formatter.PrettyFormatKey:
return serviceInspectPrettyTemplate return serviceInspectPrettyTemplate
default: default:
return Format(strings.TrimPrefix(source, RawFormatKey)) return formatter.Format(strings.TrimPrefix(source, formatter.RawFormatKey))
} }
} }
@ -166,12 +167,12 @@ func resolveNetworks(service swarm.Service, getNetwork inspect.GetRefFunc) map[s
return networkNames return networkNames
} }
// ServiceInspectWrite renders the context for a list of services // InspectFormatWrite renders the context for a list of services
func ServiceInspectWrite(ctx Context, refs []string, getRef, getNetwork inspect.GetRefFunc) error { func InspectFormatWrite(ctx formatter.Context, refs []string, getRef, getNetwork inspect.GetRefFunc) error {
if ctx.Format != serviceInspectPrettyTemplate { if ctx.Format != serviceInspectPrettyTemplate {
return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef) return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef)
} }
render := func(format func(subContext subContext) error) error { render := func(format func(subContext formatter.SubContext) error) error {
for _, ref := range refs { for _, ref := range refs {
serviceI, _, err := getRef(ref) serviceI, _, err := getRef(ref)
if err != nil { if err != nil {
@ -192,7 +193,7 @@ func ServiceInspectWrite(ctx Context, refs []string, getRef, getNetwork inspect.
type serviceInspectContext struct { type serviceInspectContext struct {
swarm.Service swarm.Service
subContext formatter.SubContext
// networkNames is a map from network IDs (as found in // networkNames is a map from network IDs (as found in
// Networks[x].Target) to network names. // Networks[x].Target) to network names.
@ -200,7 +201,7 @@ type serviceInspectContext struct {
} }
func (ctx *serviceInspectContext) MarshalJSON() ([]byte, error) { func (ctx *serviceInspectContext) MarshalJSON() ([]byte, error) {
return marshalJSON(ctx) return formatter.MarshalJSON(ctx)
} }
func (ctx *serviceInspectContext) ID() string { func (ctx *serviceInspectContext) ID() string {
@ -462,32 +463,32 @@ const (
replicasHeader = "REPLICAS" replicasHeader = "REPLICAS"
) )
// NewServiceListFormat returns a Format for rendering using a service Context // NewListFormat returns a Format for rendering using a service Context
func NewServiceListFormat(source string, quiet bool) Format { func NewListFormat(source string, quiet bool) formatter.Format {
switch source { switch source {
case TableFormatKey: case formatter.TableFormatKey:
if quiet { if quiet {
return defaultQuietFormat return formatter.DefaultQuietFormat
} }
return defaultServiceTableFormat return defaultServiceTableFormat
case RawFormatKey: case formatter.RawFormatKey:
if quiet { if quiet {
return `id: {{.ID}}` return `id: {{.ID}}`
} }
return `id: {{.ID}}\nname: {{.Name}}\nmode: {{.Mode}}\nreplicas: {{.Replicas}}\nimage: {{.Image}}\nports: {{.Ports}}\n` return `id: {{.ID}}\nname: {{.Name}}\nmode: {{.Mode}}\nreplicas: {{.Replicas}}\nimage: {{.Image}}\nports: {{.Ports}}\n`
} }
return Format(source) return formatter.Format(source)
} }
// ServiceListInfo stores the information about mode and replicas to be used by template // ListInfo stores the information about mode and replicas to be used by template
type ServiceListInfo struct { type ListInfo struct {
Mode string Mode string
Replicas string Replicas string
} }
// ServiceListWrite writes the context // ListFormatWrite writes the context
func ServiceListWrite(ctx Context, services []swarm.Service, info map[string]ServiceListInfo) error { func ListFormatWrite(ctx formatter.Context, services []swarm.Service, info map[string]ListInfo) error {
render := func(format func(subContext subContext) error) error { render := func(format func(subContext formatter.SubContext) error) error {
for _, service := range services { for _, service := range services {
serviceCtx := &serviceContext{service: service, mode: info[service.ID].Mode, replicas: info[service.ID].Replicas} serviceCtx := &serviceContext{service: service, mode: info[service.ID].Mode, replicas: info[service.ID].Replicas}
if err := format(serviceCtx); err != nil { if err := format(serviceCtx); err != nil {
@ -497,26 +498,26 @@ func ServiceListWrite(ctx Context, services []swarm.Service, info map[string]Ser
return nil return nil
} }
serviceCtx := serviceContext{} serviceCtx := serviceContext{}
serviceCtx.header = map[string]string{ serviceCtx.Header = formatter.SubHeaderContext{
"ID": serviceIDHeader, "ID": serviceIDHeader,
"Name": nameHeader, "Name": formatter.NameHeader,
"Mode": modeHeader, "Mode": modeHeader,
"Replicas": replicasHeader, "Replicas": replicasHeader,
"Image": imageHeader, "Image": formatter.ImageHeader,
"Ports": portsHeader, "Ports": formatter.PortsHeader,
} }
return ctx.Write(&serviceCtx, render) return ctx.Write(&serviceCtx, render)
} }
type serviceContext struct { type serviceContext struct {
HeaderContext formatter.HeaderContext
service swarm.Service service swarm.Service
mode string mode string
replicas string replicas string
} }
func (c *serviceContext) MarshalJSON() ([]byte, error) { func (c *serviceContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c) return formatter.MarshalJSON(c)
} }
func (c *serviceContext) ID() string { func (c *serviceContext) ID() string {

View File

@ -1,4 +1,4 @@
package formatter package service
import ( import (
"bytes" "bytes"
@ -7,6 +7,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"gotest.tools/assert" "gotest.tools/assert"
is "gotest.tools/assert/cmp" is "gotest.tools/assert/cmp"
@ -15,43 +16,43 @@ import (
func TestServiceContextWrite(t *testing.T) { func TestServiceContextWrite(t *testing.T) {
cases := []struct { cases := []struct {
context Context context formatter.Context
expected string expected string
}{ }{
// Errors // Errors
{ {
Context{Format: "{{InvalidFunction}}"}, formatter.Context{Format: "{{InvalidFunction}}"},
`Template parsing error: template: :1: function "InvalidFunction" not defined `Template parsing error: template: :1: function "InvalidFunction" not defined
`, `,
}, },
{ {
Context{Format: "{{nil}}"}, formatter.Context{Format: "{{nil}}"},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`, `,
}, },
// Table format // Table format
{ {
Context{Format: NewServiceListFormat("table", false)}, formatter.Context{Format: NewListFormat("table", false)},
`ID NAME MODE REPLICAS IMAGE PORTS `ID NAME MODE REPLICAS IMAGE PORTS
id_baz baz global 2/4 *:80->8080/tcp id_baz baz global 2/4 *:80->8080/tcp
id_bar bar replicated 2/4 *:80->8080/tcp id_bar bar replicated 2/4 *:80->8080/tcp
`, `,
}, },
{ {
Context{Format: NewServiceListFormat("table", true)}, formatter.Context{Format: NewListFormat("table", true)},
`id_baz `id_baz
id_bar id_bar
`, `,
}, },
{ {
Context{Format: NewServiceListFormat("table {{.Name}}", false)}, formatter.Context{Format: NewListFormat("table {{.Name}}", false)},
`NAME `NAME
baz baz
bar bar
`, `,
}, },
{ {
Context{Format: NewServiceListFormat("table {{.Name}}", true)}, formatter.Context{Format: NewListFormat("table {{.Name}}", true)},
`NAME `NAME
baz baz
bar bar
@ -59,18 +60,18 @@ bar
}, },
// Raw Format // Raw Format
{ {
Context{Format: NewServiceListFormat("raw", false)}, formatter.Context{Format: NewListFormat("raw", false)},
string(golden.Get(t, "service-context-write-raw.golden")), string(golden.Get(t, "service-context-write-raw.golden")),
}, },
{ {
Context{Format: NewServiceListFormat("raw", true)}, formatter.Context{Format: NewListFormat("raw", true)},
`id: id_baz `id: id_baz
id: id_bar id: id_bar
`, `,
}, },
// Custom Format // Custom Format
{ {
Context{Format: NewServiceListFormat("{{.Name}}", false)}, formatter.Context{Format: NewListFormat("{{.Name}}", false)},
`baz `baz
bar bar
`, `,
@ -112,7 +113,7 @@ bar
}, },
}, },
} }
info := map[string]ServiceListInfo{ info := map[string]ListInfo{
"id_baz": { "id_baz": {
Mode: "global", Mode: "global",
Replicas: "2/4", Replicas: "2/4",
@ -124,7 +125,7 @@ bar
} }
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
testcase.context.Output = out testcase.context.Output = out
err := ServiceListWrite(testcase.context, services, info) err := ListFormatWrite(testcase.context, services, info)
if err != nil { if err != nil {
assert.Error(t, err, testcase.expected) assert.Error(t, err, testcase.expected)
} else { } else {
@ -168,7 +169,7 @@ func TestServiceContextWriteJSON(t *testing.T) {
}, },
}, },
} }
info := map[string]ServiceListInfo{ info := map[string]ListInfo{
"id_baz": { "id_baz": {
Mode: "global", Mode: "global",
Replicas: "2/4", Replicas: "2/4",
@ -184,7 +185,7 @@ func TestServiceContextWriteJSON(t *testing.T) {
} }
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
err := ServiceListWrite(Context{Format: "{{json .}}", Output: out}, services, info) err := ListFormatWrite(formatter.Context{Format: "{{json .}}", Output: out}, services, info)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -201,7 +202,7 @@ func TestServiceContextWriteJSONField(t *testing.T) {
{ID: "id_baz", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "baz"}}}, {ID: "id_baz", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "baz"}}},
{ID: "id_bar", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "bar"}}}, {ID: "id_bar", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "bar"}}},
} }
info := map[string]ServiceListInfo{ info := map[string]ListInfo{
"id_baz": { "id_baz": {
Mode: "global", Mode: "global",
Replicas: "2/4", Replicas: "2/4",
@ -212,7 +213,7 @@ func TestServiceContextWriteJSONField(t *testing.T) {
}, },
} }
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
err := ServiceListWrite(Context{Format: "{{json .Name}}", Output: out}, services, info) err := ListFormatWrite(formatter.Context{Format: "{{json .Name}}", Output: out}, services, info)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -83,10 +83,10 @@ func runInspect(dockerCli command.Cli, opts inspectOptions) error {
serviceCtx := formatter.Context{ serviceCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCli.Out(),
Format: formatter.NewServiceFormat(f), Format: NewFormat(f),
} }
if err := formatter.ServiceInspectWrite(serviceCtx, opts.refs, getRef, getNetwork); err != nil { if err := InspectFormatWrite(serviceCtx, opts.refs, getRef, getNetwork); err != nil {
return cli.StatusError{StatusCode: 1, Status: err.Error()} return cli.StatusError{StatusCode: 1, Status: err.Error()}
} }
return nil return nil

View File

@ -104,7 +104,7 @@ func formatServiceInspect(t *testing.T, format formatter.Format, now time.Time)
Format: format, Format: format,
} }
err := formatter.ServiceInspectWrite(ctx, []string{"de179gar9d0o7ltdybungplod"}, err := InspectFormatWrite(ctx, []string{"de179gar9d0o7ltdybungplod"},
func(ref string) (interface{}, []byte, error) { func(ref string) (interface{}, []byte, error) {
return s, nil, nil return s, nil, nil
}, },
@ -122,7 +122,7 @@ func formatServiceInspect(t *testing.T, format formatter.Format, now time.Time)
} }
func TestPrettyPrintWithNoUpdateConfig(t *testing.T) { func TestPrettyPrintWithNoUpdateConfig(t *testing.T) {
s := formatServiceInspect(t, formatter.NewServiceFormat("pretty"), time.Now()) s := formatServiceInspect(t, NewFormat("pretty"), time.Now())
if strings.Contains(s, "UpdateStatus") { if strings.Contains(s, "UpdateStatus") {
t.Fatal("Pretty print failed before parsing UpdateStatus") t.Fatal("Pretty print failed before parsing UpdateStatus")
} }
@ -135,8 +135,8 @@ func TestJSONFormatWithNoUpdateConfig(t *testing.T) {
now := time.Now() now := time.Now()
// s1: [{"ID":..}] // s1: [{"ID":..}]
// s2: {"ID":..} // s2: {"ID":..}
s1 := formatServiceInspect(t, formatter.NewServiceFormat(""), now) s1 := formatServiceInspect(t, NewFormat(""), now)
s2 := formatServiceInspect(t, formatter.NewServiceFormat("{{json .}}"), now) s2 := formatServiceInspect(t, NewFormat("{{json .}}"), now)
var m1Wrap []map[string]interface{} var m1Wrap []map[string]interface{}
if err := json.Unmarshal([]byte(s1), &m1Wrap); err != nil { if err := json.Unmarshal([]byte(s1), &m1Wrap); err != nil {
t.Fatal(err) t.Fatal(err)
@ -153,7 +153,7 @@ func TestJSONFormatWithNoUpdateConfig(t *testing.T) {
} }
func TestPrettyPrintWithConfigsAndSecrets(t *testing.T) { func TestPrettyPrintWithConfigsAndSecrets(t *testing.T) {
s := formatServiceInspect(t, formatter.NewServiceFormat("pretty"), time.Now()) s := formatServiceInspect(t, NewFormat("pretty"), time.Now())
assert.Check(t, is.Contains(s, "Configs:"), "Pretty print missing configs") assert.Check(t, is.Contains(s, "Configs:"), "Pretty print missing configs")
assert.Check(t, is.Contains(s, "Secrets:"), "Pretty print missing secrets") assert.Check(t, is.Contains(s, "Secrets:"), "Pretty print missing secrets")

View File

@ -57,7 +57,7 @@ func runList(dockerCli command.Cli, options listOptions) error {
sort.Slice(services, func(i, j int) bool { sort.Slice(services, func(i, j int) bool {
return sortorder.NaturalLess(services[i].Spec.Name, services[j].Spec.Name) return sortorder.NaturalLess(services[i].Spec.Name, services[j].Spec.Name)
}) })
info := map[string]formatter.ServiceListInfo{} info := map[string]ListInfo{}
if len(services) > 0 && !options.quiet { if len(services) > 0 && !options.quiet {
// only non-empty services and not quiet, should we call TaskList and NodeList api // only non-empty services and not quiet, should we call TaskList and NodeList api
taskFilter := filters.NewArgs() taskFilter := filters.NewArgs()
@ -89,13 +89,13 @@ func runList(dockerCli command.Cli, options listOptions) error {
servicesCtx := formatter.Context{ servicesCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCli.Out(),
Format: formatter.NewServiceListFormat(format, options.quiet), Format: NewListFormat(format, options.quiet),
} }
return formatter.ServiceListWrite(servicesCtx, services, info) return ListFormatWrite(servicesCtx, services, info)
} }
// GetServicesStatus returns a map of mode and replicas // GetServicesStatus returns a map of mode and replicas
func GetServicesStatus(services []swarm.Service, nodes []swarm.Node, tasks []swarm.Task) map[string]formatter.ServiceListInfo { func GetServicesStatus(services []swarm.Service, nodes []swarm.Node, tasks []swarm.Task) map[string]ListInfo {
running := map[string]int{} running := map[string]int{}
tasksNoShutdown := map[string]int{} tasksNoShutdown := map[string]int{}
@ -116,16 +116,16 @@ func GetServicesStatus(services []swarm.Service, nodes []swarm.Node, tasks []swa
} }
} }
info := map[string]formatter.ServiceListInfo{} info := map[string]ListInfo{}
for _, service := range services { for _, service := range services {
info[service.ID] = formatter.ServiceListInfo{} info[service.ID] = ListInfo{}
if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil { if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil {
info[service.ID] = formatter.ServiceListInfo{ info[service.ID] = ListInfo{
Mode: "replicated", Mode: "replicated",
Replicas: fmt.Sprintf("%d/%d", running[service.ID], *service.Spec.Mode.Replicated.Replicas), Replicas: fmt.Sprintf("%d/%d", running[service.ID], *service.Spec.Mode.Replicated.Replicas),
} }
} else if service.Spec.Mode.Global != nil { } else if service.Spec.Mode.Global != nil {
info[service.ID] = formatter.ServiceListInfo{ info[service.ID] = ListInfo{
Mode: "global", Mode: "global",
Replicas: fmt.Sprintf("%d/%d", running[service.ID], tasksNoShutdown[service.ID]), Replicas: fmt.Sprintf("%d/%d", running[service.ID], tasksNoShutdown[service.ID]),
} }

View File

@ -2,6 +2,8 @@ package formatter
import ( import (
"strconv" "strconv"
"github.com/docker/cli/cli/command/formatter"
) )
const ( const (
@ -13,8 +15,17 @@ const (
stackServicesHeader = "SERVICES" stackServicesHeader = "SERVICES"
stackOrchestrastorHeader = "ORCHESTRATOR" stackOrchestrastorHeader = "ORCHESTRATOR"
stackNamespaceHeader = "NAMESPACE" stackNamespaceHeader = "NAMESPACE"
// TableFormatKey is an alias for formatter.TableFormatKey
TableFormatKey = formatter.TableFormatKey
) )
// Context is an alias for formatter.Context
type Context = formatter.Context
// Format is an alias for formatter.Format
type Format = formatter.Format
// Stack contains deployed stack information. // Stack contains deployed stack information.
type Stack struct { type Stack struct {
// Name is the name of the stack // Name is the name of the stack
@ -28,8 +39,8 @@ type Stack struct {
} }
// StackWrite writes formatted stacks using the Context // StackWrite writes formatted stacks using the Context
func StackWrite(ctx Context, stacks []*Stack) error { func StackWrite(ctx formatter.Context, stacks []*Stack) error {
render := func(format func(subContext subContext) error) error { render := func(format func(subContext formatter.SubContext) error) error {
for _, stack := range stacks { for _, stack := range stacks {
if err := format(&stackContext{s: stack}); err != nil { if err := format(&stackContext{s: stack}); err != nil {
return err return err
@ -41,14 +52,14 @@ func StackWrite(ctx Context, stacks []*Stack) error {
} }
type stackContext struct { type stackContext struct {
HeaderContext formatter.HeaderContext
s *Stack s *Stack
} }
func newStackContext() *stackContext { func newStackContext() *stackContext {
stackCtx := stackContext{} stackCtx := stackContext{}
stackCtx.header = map[string]string{ stackCtx.Header = formatter.SubHeaderContext{
"Name": nameHeader, "Name": formatter.NameHeader,
"Services": stackServicesHeader, "Services": stackServicesHeader,
"Orchestrator": stackOrchestrastorHeader, "Orchestrator": stackOrchestrastorHeader,
"Namespace": stackNamespaceHeader, "Namespace": stackNamespaceHeader,
@ -57,7 +68,7 @@ func newStackContext() *stackContext {
} }
func (s *stackContext) MarshalJSON() ([]byte, error) { func (s *stackContext) MarshalJSON() ([]byte, error) {
return marshalJSON(s) return formatter.MarshalJSON(s)
} }
func (s *stackContext) Name() string { func (s *stackContext) Name() string {

View File

@ -4,29 +4,30 @@ import (
"bytes" "bytes"
"testing" "testing"
"github.com/docker/cli/cli/command/formatter"
"gotest.tools/assert" "gotest.tools/assert"
is "gotest.tools/assert/cmp" is "gotest.tools/assert/cmp"
) )
func TestStackContextWrite(t *testing.T) { func TestStackContextWrite(t *testing.T) {
cases := []struct { cases := []struct {
context Context context formatter.Context
expected string expected string
}{ }{
// Errors // Errors
{ {
Context{Format: "{{InvalidFunction}}"}, formatter.Context{Format: "{{InvalidFunction}}"},
`Template parsing error: template: :1: function "InvalidFunction" not defined `Template parsing error: template: :1: function "InvalidFunction" not defined
`, `,
}, },
{ {
Context{Format: "{{nil}}"}, formatter.Context{Format: "{{nil}}"},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`, `,
}, },
// Table format // Table format
{ {
Context{Format: Format(SwarmStackTableFormat)}, formatter.Context{Format: formatter.Format(SwarmStackTableFormat)},
`NAME SERVICES ORCHESTRATOR `NAME SERVICES ORCHESTRATOR
baz 2 orchestrator1 baz 2 orchestrator1
bar 1 orchestrator2 bar 1 orchestrator2
@ -34,14 +35,14 @@ bar 1 orchestrator2
}, },
// Kubernetes table format adds Namespace column // Kubernetes table format adds Namespace column
{ {
Context{Format: Format(KubernetesStackTableFormat)}, formatter.Context{Format: formatter.Format(KubernetesStackTableFormat)},
`NAME SERVICES ORCHESTRATOR NAMESPACE `NAME SERVICES ORCHESTRATOR NAMESPACE
baz 2 orchestrator1 namespace1 baz 2 orchestrator1 namespace1
bar 1 orchestrator2 namespace2 bar 1 orchestrator2 namespace2
`, `,
}, },
{ {
Context{Format: Format("table {{.Name}}")}, formatter.Context{Format: formatter.Format("table {{.Name}}")},
`NAME `NAME
baz baz
bar bar
@ -49,7 +50,7 @@ bar
}, },
// Custom Format // Custom Format
{ {
Context{Format: Format("{{.Name}}")}, formatter.Context{Format: formatter.Format("{{.Name}}")},
`baz `baz
bar bar
`, `,

View File

@ -6,7 +6,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/service"
"github.com/docker/cli/kubernetes/labels" "github.com/docker/cli/kubernetes/labels"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
@ -154,16 +154,16 @@ const (
publishedOnRandomPortSuffix = "-random-ports" publishedOnRandomPortSuffix = "-random-ports"
) )
func convertToServices(replicas *appsv1beta2.ReplicaSetList, daemons *appsv1beta2.DaemonSetList, services *apiv1.ServiceList) ([]swarm.Service, map[string]formatter.ServiceListInfo, error) { func convertToServices(replicas *appsv1beta2.ReplicaSetList, daemons *appsv1beta2.DaemonSetList, services *apiv1.ServiceList) ([]swarm.Service, map[string]service.ListInfo, error) {
result := make([]swarm.Service, len(replicas.Items)) result := make([]swarm.Service, len(replicas.Items))
infos := make(map[string]formatter.ServiceListInfo, len(replicas.Items)+len(daemons.Items)) infos := make(map[string]service.ListInfo, len(replicas.Items)+len(daemons.Items))
for i, r := range replicas.Items { for i, r := range replicas.Items {
s, err := convertToService(r.Labels[labels.ForServiceName], services, r.Spec.Template.Spec.Containers) s, err := convertToService(r.Labels[labels.ForServiceName], services, r.Spec.Template.Spec.Containers)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
result[i] = *s result[i] = *s
infos[s.ID] = formatter.ServiceListInfo{ infos[s.ID] = service.ListInfo{
Mode: "replicated", Mode: "replicated",
Replicas: fmt.Sprintf("%d/%d", r.Status.AvailableReplicas, r.Status.Replicas), Replicas: fmt.Sprintf("%d/%d", r.Status.AvailableReplicas, r.Status.Replicas),
} }
@ -174,7 +174,7 @@ func convertToServices(replicas *appsv1beta2.ReplicaSetList, daemons *appsv1beta
return nil, nil, err return nil, nil, err
} }
result = append(result, *s) result = append(result, *s)
infos[s.ID] = formatter.ServiceListInfo{ infos[s.ID] = service.ListInfo{
Mode: "global", Mode: "global",
Replicas: fmt.Sprintf("%d/%d", d.Status.NumberReady, d.Status.DesiredNumberScheduled), Replicas: fmt.Sprintf("%d/%d", d.Status.NumberReady, d.Status.DesiredNumberScheduled),
} }

View File

@ -3,7 +3,7 @@ package kubernetes
import ( import (
"testing" "testing"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/service"
"github.com/docker/cli/kubernetes/labels" "github.com/docker/cli/kubernetes/labels"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"gotest.tools/assert" "gotest.tools/assert"
@ -28,7 +28,7 @@ func TestKubernetesServiceToSwarmServiceConversion(t *testing.T) {
replicas *appsv1beta2.ReplicaSetList replicas *appsv1beta2.ReplicaSetList
services *apiv1.ServiceList services *apiv1.ServiceList
expectedServices []swarm.Service expectedServices []swarm.Service
expectedListInfo map[string]formatter.ServiceListInfo expectedListInfo map[string]service.ListInfo
}{ }{
// Match replicas with headless stack services // Match replicas with headless stack services
{ {
@ -49,7 +49,7 @@ func TestKubernetesServiceToSwarmServiceConversion(t *testing.T) {
makeSwarmService("stack_service1", "uid1", nil), makeSwarmService("stack_service1", "uid1", nil),
makeSwarmService("stack_service2", "uid2", nil), makeSwarmService("stack_service2", "uid2", nil),
}, },
map[string]formatter.ServiceListInfo{ map[string]service.ListInfo{
"uid1": {Mode: "replicated", Replicas: "2/5"}, "uid1": {Mode: "replicated", Replicas: "2/5"},
"uid2": {Mode: "replicated", Replicas: "3/3"}, "uid2": {Mode: "replicated", Replicas: "3/3"},
}, },
@ -83,7 +83,7 @@ func TestKubernetesServiceToSwarmServiceConversion(t *testing.T) {
}, },
}), }),
}, },
map[string]formatter.ServiceListInfo{ map[string]service.ListInfo{
"uid1": {Mode: "replicated", Replicas: "1/1"}, "uid1": {Mode: "replicated", Replicas: "1/1"},
}, },
}, },
@ -117,7 +117,7 @@ func TestKubernetesServiceToSwarmServiceConversion(t *testing.T) {
}, },
}), }),
}, },
map[string]formatter.ServiceListInfo{ map[string]service.ListInfo{
"uid1": {Mode: "replicated", Replicas: "1/1"}, "uid1": {Mode: "replicated", Replicas: "1/1"},
}, },
}, },

View File

@ -8,7 +8,7 @@ import (
"net/url" "net/url"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/stack/formatter"
"github.com/docker/cli/cli/command/stack/options" "github.com/docker/cli/cli/command/stack/options"
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/pkg/errors" "github.com/pkg/errors"

View File

@ -5,7 +5,7 @@ import (
"sort" "sort"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/stack/formatter"
"github.com/docker/cli/cli/command/stack/options" "github.com/docker/cli/cli/command/stack/options"
"github.com/docker/cli/cli/command/task" "github.com/docker/cli/cli/command/task"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
@ -83,11 +83,11 @@ func printTasks(dockerCli command.Cli, options options.PS, namespace string, cli
tasksCtx := formatter.Context{ tasksCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCli.Out(),
Format: formatter.NewTaskFormat(format, options.Quiet), Format: task.NewTaskFormat(format, options.Quiet),
Trunc: !options.NoTrunc, Trunc: !options.NoTrunc,
} }
return formatter.TaskWrite(tasksCtx, tasks, names, nodes) return task.FormatWrite(tasksCtx, tasks, names, nodes)
} }
func resolveNode(name string, nodes *apiv1.NodeList, noResolve bool) (string, error) { func resolveNode(name string, nodes *apiv1.NodeList, noResolve bool) (string, error) {

View File

@ -4,7 +4,8 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/service"
"github.com/docker/cli/cli/command/stack/formatter"
"github.com/docker/cli/cli/command/stack/options" "github.com/docker/cli/cli/command/stack/options"
"github.com/docker/cli/kubernetes/labels" "github.com/docker/cli/kubernetes/labels"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
@ -115,7 +116,7 @@ func RunServices(dockerCli *KubeCli, opts options.Services) error {
services = filterServicesByName(services, filters.Get("name"), stackName) services = filterServicesByName(services, filters.Get("name"), stackName)
if opts.Quiet { if opts.Quiet {
info = map[string]formatter.ServiceListInfo{} info = map[string]service.ListInfo{}
} }
format := opts.Format format := opts.Format
@ -129,9 +130,9 @@ func RunServices(dockerCli *KubeCli, opts options.Services) error {
servicesCtx := formatter.Context{ servicesCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCli.Out(),
Format: formatter.NewServiceListFormat(format, opts.Quiet), Format: service.NewListFormat(format, opts.Quiet),
} }
return formatter.ServiceListWrite(servicesCtx, services, info) return service.ListFormatWrite(servicesCtx, services, info)
} }
func filterServicesByName(services []swarm.Service, names []string, stackName string) []swarm.Service { func filterServicesByName(services []swarm.Service, names []string, stackName string) []swarm.Service {

View File

@ -5,7 +5,7 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/stack/formatter"
"github.com/docker/cli/cli/command/stack/kubernetes" "github.com/docker/cli/cli/command/stack/kubernetes"
"github.com/docker/cli/cli/command/stack/options" "github.com/docker/cli/cli/command/stack/options"
"github.com/docker/cli/cli/command/stack/swarm" "github.com/docker/cli/cli/command/stack/swarm"

View File

@ -4,7 +4,7 @@ import (
"context" "context"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/stack/formatter"
"github.com/docker/cli/cli/compose/convert" "github.com/docker/cli/cli/compose/convert"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/pkg/errors" "github.com/pkg/errors"

View File

@ -5,8 +5,8 @@ import (
"fmt" "fmt"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/service" "github.com/docker/cli/cli/command/service"
"github.com/docker/cli/cli/command/stack/formatter"
"github.com/docker/cli/cli/command/stack/options" "github.com/docker/cli/cli/command/stack/options"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
@ -29,7 +29,7 @@ func RunServices(dockerCli command.Cli, opts options.Services) error {
return nil return nil
} }
info := map[string]formatter.ServiceListInfo{} info := map[string]service.ListInfo{}
if !opts.Quiet { if !opts.Quiet {
taskFilter := filters.NewArgs() taskFilter := filters.NewArgs()
for _, service := range services { for _, service := range services {
@ -60,7 +60,7 @@ func RunServices(dockerCli command.Cli, opts options.Services) error {
servicesCtx := formatter.Context{ servicesCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCli.Out(),
Format: formatter.NewServiceListFormat(format, opts.Quiet), Format: service.NewListFormat(format, opts.Quiet),
} }
return formatter.ServiceListWrite(servicesCtx, services, info) return service.ListFormatWrite(servicesCtx, services, info)
} }

View File

@ -1,4 +1,4 @@
package formatter package task
import ( import (
"fmt" "fmt"
@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/stringid"
@ -25,25 +26,25 @@ const (
) )
// NewTaskFormat returns a Format for rendering using a task Context // NewTaskFormat returns a Format for rendering using a task Context
func NewTaskFormat(source string, quiet bool) Format { func NewTaskFormat(source string, quiet bool) formatter.Format {
switch source { switch source {
case TableFormatKey: case formatter.TableFormatKey:
if quiet { if quiet {
return defaultQuietFormat return formatter.DefaultQuietFormat
} }
return defaultTaskTableFormat return defaultTaskTableFormat
case RawFormatKey: case formatter.RawFormatKey:
if quiet { if quiet {
return `id: {{.ID}}` return `id: {{.ID}}`
} }
return `id: {{.ID}}\nname: {{.Name}}\nimage: {{.Image}}\nnode: {{.Node}}\ndesired_state: {{.DesiredState}}\ncurrent_state: {{.CurrentState}}\nerror: {{.Error}}\nports: {{.Ports}}\n` return `id: {{.ID}}\nname: {{.Name}}\nimage: {{.Image}}\nnode: {{.Node}}\ndesired_state: {{.DesiredState}}\ncurrent_state: {{.CurrentState}}\nerror: {{.Error}}\nports: {{.Ports}}\n`
} }
return Format(source) return formatter.Format(source)
} }
// TaskWrite writes the context // FormatWrite writes the context
func TaskWrite(ctx Context, tasks []swarm.Task, names map[string]string, nodes map[string]string) error { func FormatWrite(ctx formatter.Context, tasks []swarm.Task, names map[string]string, nodes map[string]string) error {
render := func(format func(subContext subContext) error) error { render := func(format func(subContext formatter.SubContext) error) error {
for _, task := range tasks { for _, task := range tasks {
taskCtx := &taskContext{trunc: ctx.Trunc, task: task, name: names[task.ID], node: nodes[task.ID]} taskCtx := &taskContext{trunc: ctx.Trunc, task: task, name: names[task.ID], node: nodes[task.ID]}
if err := format(taskCtx); err != nil { if err := format(taskCtx); err != nil {
@ -53,23 +54,21 @@ func TaskWrite(ctx Context, tasks []swarm.Task, names map[string]string, nodes m
return nil return nil
} }
taskCtx := taskContext{} taskCtx := taskContext{}
taskCtx.header = taskHeaderContext{ taskCtx.Header = formatter.SubHeaderContext{
"ID": taskIDHeader, "ID": taskIDHeader,
"Name": nameHeader, "Name": formatter.NameHeader,
"Image": imageHeader, "Image": formatter.ImageHeader,
"Node": nodeHeader, "Node": nodeHeader,
"DesiredState": desiredStateHeader, "DesiredState": desiredStateHeader,
"CurrentState": currentStateHeader, "CurrentState": currentStateHeader,
"Error": errorHeader, "Error": errorHeader,
"Ports": portsHeader, "Ports": formatter.PortsHeader,
} }
return ctx.Write(&taskCtx, render) return ctx.Write(&taskCtx, render)
} }
type taskHeaderContext map[string]string
type taskContext struct { type taskContext struct {
HeaderContext formatter.HeaderContext
trunc bool trunc bool
task swarm.Task task swarm.Task
name string name string
@ -77,7 +76,7 @@ type taskContext struct {
} }
func (c *taskContext) MarshalJSON() ([]byte, error) { func (c *taskContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c) return formatter.MarshalJSON(c)
} }
func (c *taskContext) ID() string { func (c *taskContext) ID() string {

View File

@ -1,4 +1,4 @@
package formatter package task
import ( import (
"bytes" "bytes"
@ -6,6 +6,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"gotest.tools/assert" "gotest.tools/assert"
is "gotest.tools/assert/cmp" is "gotest.tools/assert/cmp"
@ -14,44 +15,44 @@ import (
func TestTaskContextWrite(t *testing.T) { func TestTaskContextWrite(t *testing.T) {
cases := []struct { cases := []struct {
context Context context formatter.Context
expected string expected string
}{ }{
{ {
Context{Format: "{{InvalidFunction}}"}, formatter.Context{Format: "{{InvalidFunction}}"},
`Template parsing error: template: :1: function "InvalidFunction" not defined `Template parsing error: template: :1: function "InvalidFunction" not defined
`, `,
}, },
{ {
Context{Format: "{{nil}}"}, formatter.Context{Format: "{{nil}}"},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`, `,
}, },
{ {
Context{Format: NewTaskFormat("table", true)}, formatter.Context{Format: NewTaskFormat("table", true)},
`taskID1 `taskID1
taskID2 taskID2
`, `,
}, },
{ {
Context{Format: NewTaskFormat("table {{.Name}}\t{{.Node}}\t{{.Ports}}", false)}, formatter.Context{Format: NewTaskFormat("table {{.Name}}\t{{.Node}}\t{{.Ports}}", false)},
string(golden.Get(t, "task-context-write-table-custom.golden")), string(golden.Get(t, "task-context-write-table-custom.golden")),
}, },
{ {
Context{Format: NewTaskFormat("table {{.Name}}", true)}, formatter.Context{Format: NewTaskFormat("table {{.Name}}", true)},
`NAME `NAME
foobar_baz foobar_baz
foobar_bar foobar_bar
`, `,
}, },
{ {
Context{Format: NewTaskFormat("raw", true)}, formatter.Context{Format: NewTaskFormat("raw", true)},
`id: taskID1 `id: taskID1
id: taskID2 id: taskID2
`, `,
}, },
{ {
Context{Format: NewTaskFormat("{{.Name}} {{.Node}}", false)}, formatter.Context{Format: NewTaskFormat("{{.Name}} {{.Node}}", false)},
`foobar_baz foo1 `foobar_baz foo1
foobar_bar foo2 foobar_bar foo2
`, `,
@ -73,7 +74,7 @@ foobar_bar foo2
} }
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
testcase.context.Output = out testcase.context.Output = out
err := TaskWrite(testcase.context, tasks, names, nodes) err := FormatWrite(testcase.context, tasks, names, nodes)
if err != nil { if err != nil {
assert.Error(t, err, testcase.expected) assert.Error(t, err, testcase.expected)
} else { } else {
@ -92,7 +93,7 @@ func TestTaskContextWriteJSONField(t *testing.T) {
"taskID2": "foobar_bar", "taskID2": "foobar_bar",
} }
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
err := TaskWrite(Context{Format: "{{json .ID}}", Output: out}, tasks, names, map[string]string{}) err := FormatWrite(formatter.Context{Format: "{{json .ID}}", Output: out}, tasks, names, map[string]string{})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -43,7 +43,7 @@ func Print(ctx context.Context, dockerCli command.Cli, tasks []swarm.Task, resol
tasksCtx := formatter.Context{ tasksCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCli.Out(),
Format: formatter.NewTaskFormat(format, quiet), Format: NewTaskFormat(format, quiet),
Trunc: trunc, Trunc: trunc,
} }
@ -80,7 +80,7 @@ func Print(ctx context.Context, dockerCli command.Cli, tasks []swarm.Task, resol
nodes[task.ID] = nodeValue nodes[task.ID] = nodeValue
} }
return formatter.TaskWrite(tasksCtx, tasks, names, nodes) return FormatWrite(tasksCtx, tasks, names, nodes)
} }
// DefaultFormat returns the default format from the config file, or table // DefaultFormat returns the default format from the config file, or table

View File

@ -1,9 +1,10 @@
package formatter package trust
import ( import (
"sort" "sort"
"strings" "strings"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/stringid"
) )
@ -36,18 +37,18 @@ type SignerInfo struct {
} }
// NewTrustTagFormat returns a Format for rendering using a trusted tag Context // NewTrustTagFormat returns a Format for rendering using a trusted tag Context
func NewTrustTagFormat() Format { func NewTrustTagFormat() formatter.Format {
return defaultTrustTagTableFormat return defaultTrustTagTableFormat
} }
// NewSignerInfoFormat returns a Format for rendering a signer role info Context // NewSignerInfoFormat returns a Format for rendering a signer role info Context
func NewSignerInfoFormat() Format { func NewSignerInfoFormat() formatter.Format {
return defaultSignerInfoTableFormat return defaultSignerInfoTableFormat
} }
// TrustTagWrite writes the context // TagWrite writes the context
func TrustTagWrite(ctx Context, signedTagInfoList []SignedTagInfo) error { func TagWrite(ctx formatter.Context, signedTagInfoList []SignedTagInfo) error {
render := func(format func(subContext subContext) error) error { render := func(format func(subContext formatter.SubContext) error) error {
for _, signedTag := range signedTagInfoList { for _, signedTag := range signedTagInfoList {
if err := format(&trustTagContext{s: signedTag}); err != nil { if err := format(&trustTagContext{s: signedTag}); err != nil {
return err return err
@ -56,7 +57,7 @@ func TrustTagWrite(ctx Context, signedTagInfoList []SignedTagInfo) error {
return nil return nil
} }
trustTagCtx := trustTagContext{} trustTagCtx := trustTagContext{}
trustTagCtx.header = trustTagHeaderContext{ trustTagCtx.Header = formatter.SubHeaderContext{
"SignedTag": signedTagNameHeader, "SignedTag": signedTagNameHeader,
"Digest": trustedDigestHeader, "Digest": trustedDigestHeader,
"Signers": signersHeader, "Signers": signersHeader,
@ -64,10 +65,8 @@ func TrustTagWrite(ctx Context, signedTagInfoList []SignedTagInfo) error {
return ctx.Write(&trustTagCtx, render) return ctx.Write(&trustTagCtx, render)
} }
type trustTagHeaderContext map[string]string
type trustTagContext struct { type trustTagContext struct {
HeaderContext formatter.HeaderContext
s SignedTagInfo s SignedTagInfo
} }
@ -88,8 +87,8 @@ func (c *trustTagContext) Signers() string {
} }
// SignerInfoWrite writes the context // SignerInfoWrite writes the context
func SignerInfoWrite(ctx Context, signerInfoList []SignerInfo) error { func SignerInfoWrite(ctx formatter.Context, signerInfoList []SignerInfo) error {
render := func(format func(subContext subContext) error) error { render := func(format func(subContext formatter.SubContext) error) error {
for _, signerInfo := range signerInfoList { for _, signerInfo := range signerInfoList {
if err := format(&signerInfoContext{ if err := format(&signerInfoContext{
trunc: ctx.Trunc, trunc: ctx.Trunc,
@ -101,17 +100,15 @@ func SignerInfoWrite(ctx Context, signerInfoList []SignerInfo) error {
return nil return nil
} }
signerInfoCtx := signerInfoContext{} signerInfoCtx := signerInfoContext{}
signerInfoCtx.header = signerInfoHeaderContext{ signerInfoCtx.Header = formatter.SubHeaderContext{
"Signer": signerNameHeader, "Signer": signerNameHeader,
"Keys": keysHeader, "Keys": keysHeader,
} }
return ctx.Write(&signerInfoCtx, render) return ctx.Write(&signerInfoCtx, render)
} }
type signerInfoHeaderContext map[string]string
type signerInfoContext struct { type signerInfoContext struct {
HeaderContext formatter.HeaderContext
trunc bool trunc bool
s SignerInfo s SignerInfo
} }

View File

@ -1,9 +1,10 @@
package formatter package trust
import ( import (
"bytes" "bytes"
"testing" "testing"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/stringid"
"gotest.tools/assert" "gotest.tools/assert"
is "gotest.tools/assert/cmp" is "gotest.tools/assert/cmp"
@ -86,19 +87,19 @@ func TestTrustTag(t *testing.T) {
func TestTrustTagContextWrite(t *testing.T) { func TestTrustTagContextWrite(t *testing.T) {
cases := []struct { cases := []struct {
context Context context formatter.Context
expected string expected string
}{ }{
// Errors // Errors
{ {
Context{ formatter.Context{
Format: "{{InvalidFunction}}", Format: "{{InvalidFunction}}",
}, },
`Template parsing error: template: :1: function "InvalidFunction" not defined `Template parsing error: template: :1: function "InvalidFunction" not defined
`, `,
}, },
{ {
Context{ formatter.Context{
Format: "{{nil}}", Format: "{{nil}}",
}, },
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
@ -106,7 +107,7 @@ func TestTrustTagContextWrite(t *testing.T) {
}, },
// Table Format // Table Format
{ {
Context{ formatter.Context{
Format: NewTrustTagFormat(), Format: NewTrustTagFormat(),
}, },
`SIGNED TAG DIGEST SIGNERS `SIGNED TAG DIGEST SIGNERS
@ -125,7 +126,7 @@ tag3 bbbbbbbb
} }
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
testcase.context.Output = out testcase.context.Output = out
err := TrustTagWrite(testcase.context, signedTags) err := TagWrite(testcase.context, signedTags)
if err != nil { if err != nil {
assert.Error(t, err, testcase.expected) assert.Error(t, err, testcase.expected)
} else { } else {
@ -134,15 +135,15 @@ tag3 bbbbbbbb
} }
} }
// With no trust data, the TrustTagWrite will print an empty table: // With no trust data, the TagWrite will print an empty table:
// it's up to the caller to decide whether or not to print this versus an error // it's up to the caller to decide whether or not to print this versus an error
func TestTrustTagContextEmptyWrite(t *testing.T) { func TestTrustTagContextEmptyWrite(t *testing.T) {
emptyCase := struct { emptyCase := struct {
context Context context formatter.Context
expected string expected string
}{ }{
Context{ formatter.Context{
Format: NewTrustTagFormat(), Format: NewTrustTagFormat(),
}, },
`SIGNED TAG DIGEST SIGNERS `SIGNED TAG DIGEST SIGNERS
@ -152,17 +153,17 @@ func TestTrustTagContextEmptyWrite(t *testing.T) {
emptySignedTags := []SignedTagInfo{} emptySignedTags := []SignedTagInfo{}
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
emptyCase.context.Output = out emptyCase.context.Output = out
err := TrustTagWrite(emptyCase.context, emptySignedTags) err := TagWrite(emptyCase.context, emptySignedTags)
assert.NilError(t, err) assert.NilError(t, err)
assert.Check(t, is.Equal(emptyCase.expected, out.String())) assert.Check(t, is.Equal(emptyCase.expected, out.String()))
} }
func TestSignerInfoContextEmptyWrite(t *testing.T) { func TestSignerInfoContextEmptyWrite(t *testing.T) {
emptyCase := struct { emptyCase := struct {
context Context context formatter.Context
expected string expected string
}{ }{
Context{ formatter.Context{
Format: NewSignerInfoFormat(), Format: NewSignerInfoFormat(),
}, },
`SIGNER KEYS `SIGNER KEYS
@ -178,19 +179,19 @@ func TestSignerInfoContextEmptyWrite(t *testing.T) {
func TestSignerInfoContextWrite(t *testing.T) { func TestSignerInfoContextWrite(t *testing.T) {
cases := []struct { cases := []struct {
context Context context formatter.Context
expected string expected string
}{ }{
// Errors // Errors
{ {
Context{ formatter.Context{
Format: "{{InvalidFunction}}", Format: "{{InvalidFunction}}",
}, },
`Template parsing error: template: :1: function "InvalidFunction" not defined `Template parsing error: template: :1: function "InvalidFunction" not defined
`, `,
}, },
{ {
Context{ formatter.Context{
Format: "{{nil}}", Format: "{{nil}}",
}, },
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
@ -198,7 +199,7 @@ func TestSignerInfoContextWrite(t *testing.T) {
}, },
// Table Format // Table Format
{ {
Context{ formatter.Context{
Format: NewSignerInfoFormat(), Format: NewSignerInfoFormat(),
Trunc: true, Trunc: true,
}, },
@ -210,7 +211,7 @@ eve foobarbazqux, key31, key32
}, },
// No truncation // No truncation
{ {
Context{ formatter.Context{
Format: NewSignerInfoFormat(), Format: NewSignerInfoFormat(),
}, },
`SIGNER KEYS `SIGNER KEYS

View File

@ -55,33 +55,33 @@ func printSortedAdminKeys(out io.Writer, adminRoles []client.RoleWithSignatures)
func printSignatures(out io.Writer, signatureRows []trustTagRow) error { func printSignatures(out io.Writer, signatureRows []trustTagRow) error {
trustTagCtx := formatter.Context{ trustTagCtx := formatter.Context{
Output: out, Output: out,
Format: formatter.NewTrustTagFormat(), Format: NewTrustTagFormat(),
} }
// convert the formatted type before printing // convert the formatted type before printing
formattedTags := []formatter.SignedTagInfo{} formattedTags := []SignedTagInfo{}
for _, sigRow := range signatureRows { for _, sigRow := range signatureRows {
formattedSigners := sigRow.Signers formattedSigners := sigRow.Signers
if len(formattedSigners) == 0 { if len(formattedSigners) == 0 {
formattedSigners = append(formattedSigners, fmt.Sprintf("(%s)", releasedRoleName)) formattedSigners = append(formattedSigners, fmt.Sprintf("(%s)", releasedRoleName))
} }
formattedTags = append(formattedTags, formatter.SignedTagInfo{ formattedTags = append(formattedTags, SignedTagInfo{
Name: sigRow.SignedTag, Name: sigRow.SignedTag,
Digest: sigRow.Digest, Digest: sigRow.Digest,
Signers: formattedSigners, Signers: formattedSigners,
}) })
} }
return formatter.TrustTagWrite(trustTagCtx, formattedTags) return TagWrite(trustTagCtx, formattedTags)
} }
func printSignerInfo(out io.Writer, roleToKeyIDs map[string][]string) error { func printSignerInfo(out io.Writer, roleToKeyIDs map[string][]string) error {
signerInfoCtx := formatter.Context{ signerInfoCtx := formatter.Context{
Output: out, Output: out,
Format: formatter.NewSignerInfoFormat(), Format: NewSignerInfoFormat(),
Trunc: true, Trunc: true,
} }
formattedSignerInfo := []formatter.SignerInfo{} formattedSignerInfo := []SignerInfo{}
for name, keyIDs := range roleToKeyIDs { for name, keyIDs := range roleToKeyIDs {
formattedSignerInfo = append(formattedSignerInfo, formatter.SignerInfo{ formattedSignerInfo = append(formattedSignerInfo, SignerInfo{
Name: name, Name: name,
Keys: keyIDs, Keys: keyIDs,
}) })
@ -89,5 +89,5 @@ func printSignerInfo(out io.Writer, roleToKeyIDs map[string][]string) error {
sort.Slice(formattedSignerInfo, func(i, j int) bool { sort.Slice(formattedSignerInfo, func(i, j int) bool {
return sortorder.NaturalLess(formattedSignerInfo[i].Name, formattedSignerInfo[j].Name) return sortorder.NaturalLess(formattedSignerInfo[i].Name, formattedSignerInfo[j].Name)
}) })
return formatter.SignerInfoWrite(signerInfoCtx, formattedSignerInfo) return SignerInfoWrite(signerInfoCtx, formattedSignerInfo)
} }

29
internal/test/strings.go Normal file
View File

@ -0,0 +1,29 @@
package test
import (
"strings"
"testing"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
)
// CompareMultipleValues compares comma-separated values, whatever the order is
func CompareMultipleValues(t *testing.T, value, expected string) {
// comma-separated values means probably a map input, which won't
// be guaranteed to have the same order as our expected value
// We'll create maps and use reflect.DeepEquals to check instead:
entriesMap := make(map[string]string)
expMap := make(map[string]string)
entries := strings.Split(value, ",")
expectedEntries := strings.Split(expected, ",")
for _, entry := range entries {
keyval := strings.Split(entry, "=")
entriesMap[keyval[0]] = keyval[1]
}
for _, expected := range expectedEntries {
keyval := strings.Split(expected, "=")
expMap[keyval[0]] = keyval[1]
}
assert.Check(t, is.DeepEqual(expMap, entriesMap))
}