mirror of https://github.com/docker/cli.git
Merge pull request #26519 from AkihiroSuda/fix-cli-command-formatter-json-support
Fix broken JSON support in cli/command/formatter
This commit is contained in:
commit
3ab6e03ba3
|
@ -79,6 +79,10 @@ type containerContext struct {
|
||||||
c types.Container
|
c types.Container
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *containerContext) MarshalJSON() ([]byte, error) {
|
||||||
|
return marshalJSON(c)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *containerContext) ID() string {
|
func (c *containerContext) ID() string {
|
||||||
c.AddHeader(containerIDHeader)
|
c.AddHeader(containerIDHeader)
|
||||||
if c.trunc {
|
if c.trunc {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package formatter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -323,3 +324,49 @@ func TestContainerContextWriteWithNoContainers(t *testing.T) {
|
||||||
out.Reset()
|
out.Reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContainerContextWriteJSON(t *testing.T) {
|
||||||
|
unix := time.Now().Add(-65 * time.Second).Unix()
|
||||||
|
containers := []types.Container{
|
||||||
|
{ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unix},
|
||||||
|
{ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unix},
|
||||||
|
}
|
||||||
|
expectedCreated := time.Unix(unix, 0).String()
|
||||||
|
expectedJSONs := []map[string]interface{}{
|
||||||
|
{"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID1", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_baz", "Ports": "", "RunningFor": "About a minute", "Size": "0 B", "Status": ""},
|
||||||
|
{"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID2", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_bar", "Ports": "", "RunningFor": "About a minute", "Size": "0 B", "Status": ""},
|
||||||
|
}
|
||||||
|
out := bytes.NewBufferString("")
|
||||||
|
err := ContainerWrite(Context{Format: "{{json .}}", Output: out}, containers)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||||
|
t.Logf("Output: line %d: %s", i, line)
|
||||||
|
var m map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(line), &m); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.DeepEqual(t, m, expectedJSONs[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContainerContextWriteJSONField(t *testing.T) {
|
||||||
|
containers := []types.Container{
|
||||||
|
{ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu"},
|
||||||
|
{ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu"},
|
||||||
|
}
|
||||||
|
out := bytes.NewBufferString("")
|
||||||
|
err := ContainerWrite(Context{Format: "{{json .ID}}", Output: out}, containers)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||||
|
t.Logf("Output: line %d: %s", i, line)
|
||||||
|
var s string
|
||||||
|
if err := json.Unmarshal([]byte(line), &s); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, s, containers[i].ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -53,6 +53,10 @@ type networkContext struct {
|
||||||
n types.NetworkResource
|
n types.NetworkResource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *networkContext) MarshalJSON() ([]byte, error) {
|
||||||
|
return marshalJSON(c)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *networkContext) ID() string {
|
func (c *networkContext) ID() string {
|
||||||
c.AddHeader(networkIDHeader)
|
c.AddHeader(networkIDHeader)
|
||||||
if c.trunc {
|
if c.trunc {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package formatter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -160,3 +161,48 @@ foobar_bar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNetworkContextWriteJSON(t *testing.T) {
|
||||||
|
networks := []types.NetworkResource{
|
||||||
|
{ID: "networkID1", Name: "foobar_baz"},
|
||||||
|
{ID: "networkID2", Name: "foobar_bar"},
|
||||||
|
}
|
||||||
|
expectedJSONs := []map[string]interface{}{
|
||||||
|
{"Driver": "", "ID": "networkID1", "IPv6": "false", "Internal": "false", "Labels": "", "Name": "foobar_baz", "Scope": ""},
|
||||||
|
{"Driver": "", "ID": "networkID2", "IPv6": "false", "Internal": "false", "Labels": "", "Name": "foobar_bar", "Scope": ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
out := bytes.NewBufferString("")
|
||||||
|
err := NetworkWrite(Context{Format: "{{json .}}", Output: out}, networks)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||||
|
t.Logf("Output: line %d: %s", i, line)
|
||||||
|
var m map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(line), &m); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.DeepEqual(t, m, expectedJSONs[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNetworkContextWriteJSONField(t *testing.T) {
|
||||||
|
networks := []types.NetworkResource{
|
||||||
|
{ID: "networkID1", Name: "foobar_baz"},
|
||||||
|
{ID: "networkID2", Name: "foobar_bar"},
|
||||||
|
}
|
||||||
|
out := bytes.NewBufferString("")
|
||||||
|
err := NetworkWrite(Context{Format: "{{json .ID}}", Output: out}, networks)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||||
|
t.Logf("Output: line %d: %s", i, line)
|
||||||
|
var s string
|
||||||
|
if err := json.Unmarshal([]byte(line), &s); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, s, networks[i].ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
package formatter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
func marshalJSON(x interface{}) ([]byte, error) {
|
||||||
|
m, err := marshalMap(x)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return json.Marshal(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// marshalMap marshals x to map[string]interface{}
|
||||||
|
func marshalMap(x interface{}) (map[string]interface{}, error) {
|
||||||
|
val := reflect.ValueOf(x)
|
||||||
|
if val.Kind() != reflect.Ptr {
|
||||||
|
return nil, fmt.Errorf("expected a pointer to a struct, got %v", val.Kind())
|
||||||
|
}
|
||||||
|
if val.IsNil() {
|
||||||
|
return nil, fmt.Errorf("expxected a pointer to a struct, got nil pointer")
|
||||||
|
}
|
||||||
|
valElem := val.Elem()
|
||||||
|
if valElem.Kind() != reflect.Struct {
|
||||||
|
return nil, fmt.Errorf("expected a pointer to a struct, got a pointer to %v", valElem.Kind())
|
||||||
|
}
|
||||||
|
typ := val.Type()
|
||||||
|
m := make(map[string]interface{})
|
||||||
|
for i := 0; i < val.NumMethod(); i++ {
|
||||||
|
k, v, err := marshalForMethod(typ.Method(i), val.Method(i))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if k != "" {
|
||||||
|
m[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var unmarshallableNames = map[string]struct{}{"FullHeader": {}}
|
||||||
|
|
||||||
|
// marshalForMethod returns the map key and the map value for marshalling the method.
|
||||||
|
// It returns ("", nil, nil) for valid but non-marshallable parameter. (e.g. "unexportedFunc()")
|
||||||
|
func marshalForMethod(typ reflect.Method, val reflect.Value) (string, interface{}, error) {
|
||||||
|
if val.Kind() != reflect.Func {
|
||||||
|
return "", nil, fmt.Errorf("expected func, got %v", val.Kind())
|
||||||
|
}
|
||||||
|
name, numIn, numOut := typ.Name, val.Type().NumIn(), val.Type().NumOut()
|
||||||
|
_, blackListed := unmarshallableNames[name]
|
||||||
|
// FIXME: In text/template, (numOut == 2) is marshallable,
|
||||||
|
// if the type of the second param is error.
|
||||||
|
marshallable := unicode.IsUpper(rune(name[0])) && !blackListed &&
|
||||||
|
numIn == 0 && numOut == 1
|
||||||
|
if !marshallable {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
result := val.Call(make([]reflect.Value, numIn))
|
||||||
|
intf := result[0].Interface()
|
||||||
|
return name, intf, nil
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
package formatter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dummy struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummy) Func1() string {
|
||||||
|
return "Func1"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummy) func2() string {
|
||||||
|
return "func2(should not be marshalled)"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummy) Func3() (string, int) {
|
||||||
|
return "Func3(should not be marshalled)", -42
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummy) Func4() int {
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
|
||||||
|
type dummyType string
|
||||||
|
|
||||||
|
func (d *dummy) Func5() dummyType {
|
||||||
|
return dummyType("Func5")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummy) FullHeader() string {
|
||||||
|
return "FullHeader(should not be marshalled)"
|
||||||
|
}
|
||||||
|
|
||||||
|
var dummyExpected = map[string]interface{}{
|
||||||
|
"Func1": "Func1",
|
||||||
|
"Func4": 4,
|
||||||
|
"Func5": dummyType("Func5"),
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalMap(t *testing.T) {
|
||||||
|
d := dummy{}
|
||||||
|
m, err := marshalMap(&d)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(dummyExpected, m) {
|
||||||
|
t.Fatalf("expected %+v, got %+v",
|
||||||
|
dummyExpected, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalMapBad(t *testing.T) {
|
||||||
|
if _, err := marshalMap(nil); err == nil {
|
||||||
|
t.Fatal("expected an error (argument is nil)")
|
||||||
|
}
|
||||||
|
if _, err := marshalMap(dummy{}); err == nil {
|
||||||
|
t.Fatal("expected an error (argument is non-pointer)")
|
||||||
|
}
|
||||||
|
x := 42
|
||||||
|
if _, err := marshalMap(&x); err == nil {
|
||||||
|
t.Fatal("expected an error (argument is a pointer to non-struct)")
|
||||||
|
}
|
||||||
|
}
|
|
@ -139,6 +139,10 @@ type serviceInspectContext struct {
|
||||||
subContext
|
subContext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctx *serviceInspectContext) MarshalJSON() ([]byte, error) {
|
||||||
|
return marshalJSON(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
func (ctx *serviceInspectContext) ID() string {
|
func (ctx *serviceInspectContext) ID() string {
|
||||||
return ctx.Service.ID
|
return ctx.Service.ID
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,10 @@ type volumeContext struct {
|
||||||
v types.Volume
|
v types.Volume
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *volumeContext) MarshalJSON() ([]byte, error) {
|
||||||
|
return marshalJSON(c)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *volumeContext) Name() string {
|
func (c *volumeContext) Name() string {
|
||||||
c.AddHeader(volumeNameHeader)
|
c.AddHeader(volumeNameHeader)
|
||||||
return c.v.Name
|
return c.v.Name
|
||||||
|
|
|
@ -2,6 +2,7 @@ package formatter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -142,3 +143,47 @@ foobar_bar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVolumeContextWriteJSON(t *testing.T) {
|
||||||
|
volumes := []*types.Volume{
|
||||||
|
{Driver: "foo", Name: "foobar_baz"},
|
||||||
|
{Driver: "bar", Name: "foobar_bar"},
|
||||||
|
}
|
||||||
|
expectedJSONs := []map[string]interface{}{
|
||||||
|
{"Driver": "foo", "Labels": "", "Links": "N/A", "Mountpoint": "", "Name": "foobar_baz", "Scope": "", "Size": "N/A"},
|
||||||
|
{"Driver": "bar", "Labels": "", "Links": "N/A", "Mountpoint": "", "Name": "foobar_bar", "Scope": "", "Size": "N/A"},
|
||||||
|
}
|
||||||
|
out := bytes.NewBufferString("")
|
||||||
|
err := VolumeWrite(Context{Format: "{{json .}}", Output: out}, volumes)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||||
|
t.Logf("Output: line %d: %s", i, line)
|
||||||
|
var m map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(line), &m); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.DeepEqual(t, m, expectedJSONs[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVolumeContextWriteJSONField(t *testing.T) {
|
||||||
|
volumes := []*types.Volume{
|
||||||
|
{Driver: "foo", Name: "foobar_baz"},
|
||||||
|
{Driver: "bar", Name: "foobar_bar"},
|
||||||
|
}
|
||||||
|
out := bytes.NewBufferString("")
|
||||||
|
err := VolumeWrite(Context{Format: "{{json .Name}}", Output: out}, volumes)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||||
|
t.Logf("Output: line %d: %s", i, line)
|
||||||
|
var s string
|
||||||
|
if err := json.Unmarshal([]byte(line), &s); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, s, volumes[i].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,15 +2,17 @@ package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
"github.com/docker/docker/cli/command/formatter"
|
"github.com/docker/docker/cli/command/formatter"
|
||||||
|
"github.com/docker/docker/pkg/testutil/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPrettyPrintWithNoUpdateConfig(t *testing.T) {
|
func formatServiceInspect(t *testing.T, format formatter.Format, now time.Time) string {
|
||||||
b := new(bytes.Buffer)
|
b := new(bytes.Buffer)
|
||||||
|
|
||||||
endpointSpec := &swarm.EndpointSpec{
|
endpointSpec := &swarm.EndpointSpec{
|
||||||
|
@ -29,8 +31,8 @@ func TestPrettyPrintWithNoUpdateConfig(t *testing.T) {
|
||||||
ID: "de179gar9d0o7ltdybungplod",
|
ID: "de179gar9d0o7ltdybungplod",
|
||||||
Meta: swarm.Meta{
|
Meta: swarm.Meta{
|
||||||
Version: swarm.Version{Index: 315},
|
Version: swarm.Version{Index: 315},
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: now,
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: now,
|
||||||
},
|
},
|
||||||
Spec: swarm.ServiceSpec{
|
Spec: swarm.ServiceSpec{
|
||||||
Annotations: swarm.Annotations{
|
Annotations: swarm.Annotations{
|
||||||
|
@ -73,14 +75,14 @@ func TestPrettyPrintWithNoUpdateConfig(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
UpdateStatus: swarm.UpdateStatus{
|
UpdateStatus: swarm.UpdateStatus{
|
||||||
StartedAt: time.Now(),
|
StartedAt: now,
|
||||||
CompletedAt: time.Now(),
|
CompletedAt: now,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := formatter.Context{
|
ctx := formatter.Context{
|
||||||
Output: b,
|
Output: b,
|
||||||
Format: formatter.NewServiceFormat("pretty"),
|
Format: format,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := formatter.ServiceInspectWrite(ctx, []string{"de179gar9d0o7ltdybungplod"}, func(ref string) (interface{}, []byte, error) {
|
err := formatter.ServiceInspectWrite(ctx, []string{"de179gar9d0o7ltdybungplod"}, func(ref string) (interface{}, []byte, error) {
|
||||||
|
@ -89,8 +91,39 @@ func TestPrettyPrintWithNoUpdateConfig(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
if strings.Contains(b.String(), "UpdateStatus") {
|
func TestPrettyPrintWithNoUpdateConfig(t *testing.T) {
|
||||||
|
s := formatServiceInspect(t, formatter.NewServiceFormat("pretty"), time.Now())
|
||||||
|
if strings.Contains(s, "UpdateStatus") {
|
||||||
t.Fatal("Pretty print failed before parsing UpdateStatus")
|
t.Fatal("Pretty print failed before parsing UpdateStatus")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestJSONFormatWithNoUpdateConfig(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
// s1: [{"ID":..}]
|
||||||
|
// s2: {"ID":..}
|
||||||
|
s1 := formatServiceInspect(t, formatter.NewServiceFormat(""), now)
|
||||||
|
t.Log("// s1")
|
||||||
|
t.Logf("%s", s1)
|
||||||
|
s2 := formatServiceInspect(t, formatter.NewServiceFormat("{{json .}}"), now)
|
||||||
|
t.Log("// s2")
|
||||||
|
t.Logf("%s", s2)
|
||||||
|
var m1Wrap []map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(s1), &m1Wrap); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(m1Wrap) != 1 {
|
||||||
|
t.Fatalf("strange s1=%s", s1)
|
||||||
|
}
|
||||||
|
m1 := m1Wrap[0]
|
||||||
|
t.Logf("m1=%+v", m1)
|
||||||
|
var m2 map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(s2), &m2); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Logf("m2=%+v", m2)
|
||||||
|
assert.DeepEqual(t, m2, m1)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue