Unverified Commit 8ccd01da authored by bryanl's avatar bryanl
Browse files

Add JSON format to table printer



This PR adds JSON output to the existing table printer and updates all
actions which use the table printer.

JSON tables are printed out like:

```json
{
  "kind": "moduleList",
  "data": [
    {"module": "/"},
    {"module": "a"},
    {"module": "a.b"}
  ]
}
```

Where:

* `kind` is the kind of data in the table
* `data` contains the table rows matched to the header

Fixes #693
Signed-off-by: default avatarbryanl <bryanliles@gmail.com>
parent 88a9cbd9
......@@ -27,7 +27,7 @@ ks component list
```
-h, --help help for list
--module string Component module
-o, --output string Output format. Valid options: wide
-o, --output string Output format. Valid options: table|json
```
### Options inherited from parent commands
......
......@@ -26,7 +26,7 @@ ks env list [flags]
```
-h, --help help for list
-o, --output string Output format. One of: json|wide
-o, --output string Output format. Valid options: table|json
```
### Options inherited from parent commands
......
......@@ -13,8 +13,9 @@ ks module list [flags]
### Options
```
--env string Environment to list modules for
-h, --help help for list
--env string Environment to list modules for
-h, --help help for list
-o, --output string Output format. Valid options: table|json
```
### Options inherited from parent commands
......
......@@ -40,6 +40,7 @@ ks param diff dev prod --component=guestbook
```
--component string Specify the component to diff against
-h, --help help for diff
-o, --output string Output format. Valid options: table|json
```
### Options inherited from parent commands
......
......@@ -45,6 +45,7 @@ ks param list guestbook --env=dev
--env string Specify environment to list parameters for
-h, --help help for list
--module string Specify module to list parameters for
-o, --output string Output format. Valid options: table|json
```
### Options inherited from parent commands
......
......@@ -29,8 +29,9 @@ ks pkg list [flags]
### Options
```
-h, --help help for list
--installed Only list installed packages
-h, --help help for list
--installed Only list installed packages
-o, --output string Output format. Valid options: table|json
```
### Options inherited from parent commands
......
......@@ -30,7 +30,8 @@ ks prototype list [flags]
### Options
```
-h, --help help for list
-h, --help help for list
-o, --output string Output format. Valid options: table|json
```
### Options inherited from parent commands
......
......@@ -31,7 +31,8 @@ ks prototype search service
### Options
```
-h, --help help for search
-h, --help help for search
-o, --output string Output format. Valid options: table|json
```
### Options inherited from parent commands
......
......@@ -26,7 +26,8 @@ ks registry list [flags]
### Options
```
-h, --help help for list
-h, --help help for list
-o, --output string Output format. Valid options: table|json
```
### Options inherited from parent commands
......
......@@ -16,7 +16,6 @@
package actions
import (
"encoding/json"
"io"
"os"
"sort"
......@@ -68,47 +67,20 @@ func NewComponentList(m map[string]interface{}) (*ComponentList, error) {
// Run runs the ComponentList action.
func (cl *ComponentList) Run() error {
ns, err := cl.cm.Module(cl.app, cl.module)
module, err := cl.cm.Module(cl.app, cl.module)
if err != nil {
return err
}
components, err := ns.Components()
components, err := module.Components()
if err != nil {
return err
}
switch cl.output {
default:
return errors.Errorf("invalid output option %q", cl.output)
case "":
cl.listComponents(components)
case "wide":
return cl.listComponentsWide(components)
case "json":
return cl.listComponentsJSON(components)
}
return nil
}
func (cl *ComponentList) listComponents(components []component.Component) {
var list []string
for _, c := range components {
list = append(list, c.Name(true))
}
sort.Strings(list)
table := table.New(cl.out)
table.SetHeader([]string{"component"})
for _, item := range list {
table.Append([]string{item})
}
table.Render()
return cl.listComponents(components)
}
func (cl *ComponentList) listComponentsWide(components []component.Component) error {
func (cl *ComponentList) listComponents(components []component.Component) error {
var rows [][]string
for _, c := range components {
summary, err := c.Summarize()
......@@ -131,24 +103,14 @@ func (cl *ComponentList) listComponentsWide(components []component.Component) er
return rows[i][0] < rows[j][0]
})
table := table.New(cl.out)
table.SetHeader([]string{"component", "type", "apiversion", "kind", "name"})
table.AppendBulk(rows)
table.Render()
return nil
}
func (cl *ComponentList) listComponentsJSON(components []component.Component) error {
var summaries []component.Summary
for _, c := range components {
s, err := c.Summarize()
if err != nil {
return errors.Wrapf(err, "get summary for %s", c.Name(true))
}
summaries = append(summaries, s)
t := table.New("componentList", cl.out)
f, err := table.DetectFormat(cl.output)
if err != nil {
return errors.Wrap(err, "detecting output format")
}
return json.NewEncoder(cl.out).Encode(summaries)
t.SetFormat(f)
t.SetHeader([]string{"component", "type", "apiversion", "kind", "name"})
t.AppendBulk(rows)
return t.Render()
}
......@@ -17,57 +17,20 @@ package actions
import (
"bytes"
"path/filepath"
"testing"
amocks "github.com/ksonnet/ksonnet/pkg/app/mocks"
"github.com/ksonnet/ksonnet/pkg/component"
cmocks "github.com/ksonnet/ksonnet/pkg/component/mocks"
"github.com/pkg/errors"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestComponentList(t *testing.T) {
withApp(t, func(appMock *amocks.App) {
module := ""
output := ""
c := &cmocks.Component{}
c.On("Name", true).Return("c1")
cs := []component.Component{c}
ns := &cmocks.Module{}
ns.On("Components").Return(cs, nil)
cm := &cmocks.Manager{}
cm.On("Module", mock.Anything, "").Return(ns, nil)
in := map[string]interface{}{
OptionApp: appMock,
OptionModule: module,
OptionOutput: output,
}
a, err := NewComponentList(in)
require.NoError(t, err)
a.cm = cm
var buf bytes.Buffer
a.out = &buf
err = a.Run()
require.NoError(t, err)
assertOutput(t, "component/list/output.txt", buf.String())
})
}
func TestComponentList_json(t *testing.T) {
withApp(t, func(appMock *amocks.App) {
module := ""
output := "json"
func TestComponentList_wide(t *testing.T) {
validComponentManager := func() component.Manager {
summary1 := component.Summary{ComponentName: "ingress"}
c1 := &cmocks.Component{}
c1.On("Summarize").Return(summary1, nil)
......@@ -78,73 +41,126 @@ func TestComponentList_json(t *testing.T) {
cs := []component.Component{c1, c2}
ns := &cmocks.Module{}
ns.On("Components").Return(cs, nil)
module := &cmocks.Module{}
module.On("Components").Return(cs, nil)
cm := &cmocks.Manager{}
cm.On("Module", mock.Anything, "").Return(ns, nil)
in := map[string]interface{}{
OptionApp: appMock,
OptionModule: module,
OptionOutput: output,
}
cm.On("Module", mock.Anything, "").Return(module, nil)
a, err := NewComponentList(in)
require.NoError(t, err)
return cm
}
a.cm = cm
componentManagerCannotLoadModule := func() component.Manager {
cm := &cmocks.Manager{}
cm.On("Module", mock.Anything, "").Return(nil, errors.New("can't load module"))
var buf bytes.Buffer
a.out = &buf
return cm
}
err = a.Run()
require.NoError(t, err)
cannotLoadComponents := func() component.Manager {
module := &cmocks.Module{}
module.On("Components").Return(nil, errors.New("can't load components"))
assertOutput(t, "component/list/json.txt", buf.String())
})
}
cm := &cmocks.Manager{}
cm.On("Module", mock.Anything, "").Return(module, nil)
func TestComponentList_wide(t *testing.T) {
withApp(t, func(appMock *amocks.App) {
module := ""
output := "wide"
return cm
}
summary1 := component.Summary{ComponentName: "ingress"}
cannotLoadSummary := func() component.Manager {
c1 := &cmocks.Component{}
c1.On("Summarize").Return(summary1, nil)
summary2 := component.Summary{ComponentName: "deployment"}
c2 := &cmocks.Component{}
c2.On("Summarize").Return(summary2, nil)
c1.On("Summarize").Return(component.Summary{}, errors.New("can't load summary"))
cs := []component.Component{c1, c2}
cs := []component.Component{c1}
ns := &cmocks.Module{}
ns.On("Components").Return(cs, nil)
module := &cmocks.Module{}
module.On("Components").Return(cs, nil)
cm := &cmocks.Manager{}
cm.On("Module", mock.Anything, "").Return(ns, nil)
in := map[string]interface{}{
OptionApp: appMock,
OptionModule: module,
OptionOutput: output,
}
a, err := NewComponentList(in)
require.NoError(t, err)
a.cm = cm
var buf bytes.Buffer
a.out = &buf
err = a.Run()
require.NoError(t, err)
cm.On("Module", mock.Anything, "").Return(module, nil)
return cm
}
cases := []struct {
name string
componentManager component.Manager
output string
expectedFile string
isErr bool
}{
{
name: "with json format",
componentManager: validComponentManager(),
output: "json",
expectedFile: filepath.ToSlash("component/list/output.json"),
},
{
name: "with table format",
componentManager: validComponentManager(),
output: "table",
expectedFile: filepath.ToSlash("component/list/table.txt"),
},
{
name: "with unspecified format",
componentManager: validComponentManager(),
expectedFile: filepath.ToSlash("component/list/table.txt"),
},
{
name: "with invalid format",
componentManager: validComponentManager(),
output: "invalid",
isErr: true,
},
{
name: "can't load module",
componentManager: componentManagerCannotLoadModule(),
isErr: true,
},
{
name: "can't load components",
componentManager: cannotLoadComponents(),
isErr: true,
},
{
name: "can't load summary",
componentManager: cannotLoadSummary(),
isErr: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
withApp(t, func(appMock *amocks.App) {
moduleName := ""
in := map[string]interface{}{
OptionApp: appMock,
OptionModule: moduleName,
OptionOutput: tc.output,
}
a, err := NewComponentList(in)
require.NoError(t, err)
a.cm = tc.componentManager
var buf bytes.Buffer
a.out = &buf
err = a.Run()
if tc.isErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assertOutput(t, tc.expectedFile, buf.String())
})
})
}
assertOutput(t, "component/list/wide.txt", buf.String())
})
}
func TestComponentList_requires_app(t *testing.T) {
......
......@@ -16,7 +16,6 @@
package actions
import (
"encoding/json"
"io"
"os"
"sort"
......@@ -36,9 +35,10 @@ func RunEnvList(m map[string]interface{}) error {
return nl.Run()
}
// EnvList lists available namespaces
// EnvList lists available namespaces. To initialize EnvList,
// use the `NewEnvList` constructor.
type EnvList struct {
app app.App
envListFn func() (app.EnvironmentConfigs, error)
outputType string
out io.Writer
}
......@@ -47,19 +47,17 @@ type EnvList struct {
func NewEnvList(m map[string]interface{}) (*EnvList, error) {
ol := newOptionLoader(m)
el := &EnvList{
app: ol.LoadApp(),
outputType: ol.LoadOptionalString(OptionOutput),
out: os.Stdout,
}
a := ol.LoadApp()
outputType := ol.LoadOptionalString(OptionOutput)
if ol.err != nil {
return nil, ol.err
}
if el.outputType == "" {
el.outputType = OutputWide
el := &EnvList{
outputType: outputType,
envListFn: a.Environments,
out: os.Stdout,
}
return el, nil
......@@ -67,33 +65,19 @@ func NewEnvList(m map[string]interface{}) (*EnvList, error) {
// Run runs the env list action.
func (el *EnvList) Run() error {
switch el.outputType {
default:
return errors.Errorf("unknown output format %q", el.outputType)
case OutputWide:
return el.outputWide()
case OutputJSON:
return el.outputJSON()
}
}
func (el *EnvList) outputJSON() error {
environments, err := el.app.Environments()
environments, err := el.envListFn()
if err != nil {
return err
}
return json.NewEncoder(el.out).Encode(environments)
}
t := table.New("envList", el.out)
t.SetHeader([]string{"name", "override", "kubernetes-version", "namespace", "server"})
func (el *EnvList) outputWide() error {
environments, err := el.app.Environments()
f, err := table.DetectFormat(el.outputType)
if err != nil {
return err
return errors.Wrap(err, "detecting output format")
}
table := table.New(el.out)
table.SetHeader([]string{"name", "override", "kubernetes-version", "namespace", "server"})
t.SetFormat(f)
var rows [][]string
......@@ -116,8 +100,8 @@ func (el *EnvList) outputWide() error {
return rows[i][0] < rows[j][0]
})
table.AppendBulk(rows)
t.AppendBulk(rows)
return t.Render()
table.Render()
return nil
}
......@@ -23,54 +23,82 @@ import (
"github.com/ksonnet/ksonnet/pkg/app"
amocks "github.com/ksonnet/ksonnet/pkg/app/mocks"
"github.com/ksonnet/ksonnet/pkg/util/test"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
)
func TestEnvList(t *testing.T) {
withApp(t, func(appMock *amocks.App) {
env := &app.EnvironmentConfig{
setupValidApp := func(appMock *amocks.App) {
defaultEnv := &app.EnvironmentConfig{
KubernetesVersion: "v1.7.0",
Destination: &app.EnvironmentDestinationSpec{
Namespace: "default",
Server: "http://example.com",
},
}
prodEnv := &app.EnvironmentConfig{
KubernetesVersion: "v1.7.0",
Destination: &app.EnvironmentDestinationSpec{
Namespace: "prod",
Server: "http://example.com",
},
}
envs := app.EnvironmentConfigs{
"default": env,
"default": defaultEnv,
"prod": prodEnv,
}
appMock.On("Environments").Return(envs, nil)
}
cases := []struct {
name string
outputType string
expectedFile string
isErr bool
}{
{
name: "no format specified",
expectedFile: filepath.Join("env", "list", "output.txt"),
},
{
name: "wide output",
outputType: OutputWide,
expectedFile: filepath.Join("env", "list", "output.txt"),
},
{
name: "json output",
outputType: OutputJSON,
expectedFile: filepath.Join("env", "list", "output.json"),
},
{
name: "invalid output format",
outputType: "invalid",
isErr: true,
},
}
envListFail := func(appMock *amocks.App) {
appMock.On("Environments").Return(nil, errors.New("failed"))
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cases := []struct {
name string
initApp func(*amocks.App)
outputType string
expectedFile string
isErr bool
}{
{
name: "table output",
initApp: setupValidApp,
outputType: "table",
expectedFile: filepath.Join("env", "list", "output.txt"),