feat(list): Optional output as JSON and YAML

The choice of interface `--output (json|yaml)` is to match that of the
status command, except that -o is not available as it is already used
by --offset.

WIP #1534
pull/3923/head
Sean Eagan 7 years ago
parent 1ee1706870
commit 49c3d50e4e

@ -17,10 +17,12 @@ limitations under the License.
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"io" "io"
"strings" "strings"
"github.com/ghodss/yaml"
"github.com/gosuri/uitable" "github.com/gosuri/uitable"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -75,6 +77,22 @@ type listCmd struct {
pending bool pending bool
client helm.Interface client helm.Interface
colWidth uint colWidth uint
output string
}
type listResult struct {
Next string
Releases []listRelease
}
type listRelease struct {
Name string
Revision int32
Updated string
Status string
Chart string
AppVersion string
Namespace string
} }
func newListCmd(client helm.Interface, out io.Writer) *cobra.Command { func newListCmd(client helm.Interface, out io.Writer) *cobra.Command {
@ -114,6 +132,7 @@ func newListCmd(client helm.Interface, out io.Writer) *cobra.Command {
f.BoolVar(&list.pending, "pending", false, "show pending releases") f.BoolVar(&list.pending, "pending", false, "show pending releases")
f.StringVar(&list.namespace, "namespace", "", "show releases within a specific namespace") f.StringVar(&list.namespace, "namespace", "", "show releases within a specific namespace")
f.UintVar(&list.colWidth, "col-width", 60, "specifies the max column width of output") f.UintVar(&list.colWidth, "col-width", 60, "specifies the max column width of output")
f.StringVar(&list.output, "output", "", "output the specified format (json or yaml)")
// TODO: Do we want this as a feature of 'helm list'? // TODO: Do we want this as a feature of 'helm list'?
//f.BoolVar(&list.superseded, "history", true, "show historical releases") //f.BoolVar(&list.superseded, "history", true, "show historical releases")
@ -148,23 +167,17 @@ func (l *listCmd) run() error {
return prettyError(err) return prettyError(err)
} }
if len(res.GetReleases()) == 0 { rels := filterList(res.Releases)
return nil
}
if res.Next != "" && !l.short { result := getListResult(rels, res.Next)
fmt.Fprintf(l.out, "\tnext: %s\n", res.Next)
}
rels := filterList(res.Releases) output, err := formatResult(l.output, l.short, result, l.colWidth)
if l.short { if err != nil {
for _, r := range rels { return prettyError(err)
fmt.Fprintln(l.out, r.Name)
}
return nil
} }
fmt.Fprintln(l.out, formatList(rels, l.colWidth))
fmt.Fprintln(l.out, output)
return nil return nil
} }
@ -233,23 +246,98 @@ func (l *listCmd) statusCodes() []release.Status_Code {
return status return status
} }
func formatList(rels []*release.Release, colWidth uint) string { func getListResult(rels []*release.Release, next string) listResult {
table := uitable.New() listReleases := []listRelease{}
table.MaxColWidth = colWidth
table.AddRow("NAME", "REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "NAMESPACE")
for _, r := range rels { for _, r := range rels {
md := r.GetChart().GetMetadata() md := r.GetChart().GetMetadata()
c := fmt.Sprintf("%s-%s", md.GetName(), md.GetVersion())
t := "-" t := "-"
if tspb := r.GetInfo().GetLastDeployed(); tspb != nil { if tspb := r.GetInfo().GetLastDeployed(); tspb != nil {
t = timeconv.String(tspb) t = timeconv.String(tspb)
} }
s := r.GetInfo().GetStatus().GetCode().String()
v := r.GetVersion() lr := listRelease{
a := md.GetAppVersion() Name: r.GetName(),
n := r.GetNamespace() Revision: r.GetVersion(),
table.AddRow(r.GetName(), v, t, s, c, a, n) Updated: t,
Status: r.GetInfo().GetStatus().GetCode().String(),
Chart: fmt.Sprintf("%s-%s", md.GetName(), md.GetVersion()),
AppVersion: md.GetAppVersion(),
Namespace: r.GetNamespace(),
}
listReleases = append(listReleases, lr)
}
return listResult{
Releases: listReleases,
Next: next,
}
}
func shortenListResult(result listResult) []string {
names := []string{}
for _, r := range result.Releases {
names = append(names, r.Name)
}
return names
}
func formatResult(format string, short bool, result listResult, colWidth uint) (string, error) {
var output string
var err error
var shortResult []string
var finalResult interface{}
if short {
shortResult = shortenListResult(result)
finalResult = shortResult
} else {
finalResult = result
}
switch format {
case "":
if short {
output = formatTextShort(shortResult)
} else {
output = formatText(result, colWidth)
}
case "json":
o, e := json.Marshal(finalResult)
if e != nil {
err = fmt.Errorf("Failed to Marshal JSON output: %s", e)
} else {
output = string(o)
}
case "yaml":
o, e := yaml.Marshal(finalResult)
if e != nil {
err = fmt.Errorf("Failed to Marshal YAML output: %s", e)
} else {
output = string(o)
}
default:
err = fmt.Errorf("Unknown output format \"%s\"", format)
} }
return table.String() return output, err
}
func formatText(result listResult, colWidth uint) string {
nextOutput := ""
if result.Next != "" {
nextOutput = fmt.Sprintf("\tnext: %s\n", result.Next)
}
table := uitable.New()
table.MaxColWidth = colWidth
table.AddRow("NAME", "REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "NAMESPACE")
for _, lr := range result.Releases {
table.AddRow(lr.Name, lr.Revision, lr.Updated, lr.Status, lr.Chart, lr.AppVersion, lr.Namespace)
}
return fmt.Sprintf("%s%s", nextOutput, table.String())
}
func formatTextShort(shortResult []string) string {
return strings.Join(shortResult, "\n")
} }

@ -18,16 +18,18 @@ package main
import ( import (
"io" "io"
"regexp"
"testing" "testing"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"io/ioutil" "io/ioutil"
"os"
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/proto/hapi/release" "k8s.io/helm/pkg/proto/hapi/release"
"os"
) )
func TestListCmd(t *testing.T) { func TestListCmd(t *testing.T) {
@ -46,6 +48,11 @@ func TestListCmd(t *testing.T) {
ch, _ := chartutil.Load(chartPath) ch, _ := chartutil.Load(chartPath)
tests := []releaseCase{ tests := []releaseCase{
{
name: "empty",
rels: []*release.Release{},
expected: "",
},
{ {
name: "with a release", name: "with a release",
rels: []*release.Release{ rels: []*release.Release{
@ -67,6 +74,77 @@ func TestListCmd(t *testing.T) {
}, },
expected: "NAME \tREVISION\tUPDATED \tSTATUS \tCHART \tAPP VERSION\tNAMESPACE\natlas\t1 \t(.*)\tDEPLOYED\tfoo-0.1.0-beta.1\t2.X.A \tdefault \n", expected: "NAME \tREVISION\tUPDATED \tSTATUS \tCHART \tAPP VERSION\tNAMESPACE\natlas\t1 \t(.*)\tDEPLOYED\tfoo-0.1.0-beta.1\t2.X.A \tdefault \n",
}, },
{
name: "with json output",
flags: []string{"--max", "1", "--output", "json"},
rels: []*release.Release{
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide"}),
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide"}),
},
expected: regexp.QuoteMeta(`{"Next":"atlas-guide","Releases":[{"Name":"thomas-guide","Revision":1,"Updated":"`) + `([^"]*)` + regexp.QuoteMeta(`","Status":"DEPLOYED","Chart":"foo-0.1.0-beta.1","AppVersion":"","Namespace":"default"}]}
`),
},
{
name: "with yaml output",
flags: []string{"--max", "1", "--output", "yaml"},
rels: []*release.Release{
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide"}),
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide"}),
},
expected: regexp.QuoteMeta(`Next: atlas-guide
Releases:
- AppVersion: ""
Chart: foo-0.1.0-beta.1
Name: thomas-guide
Namespace: default
Revision: 1
Status: DEPLOYED
Updated: `) + `(.*)` + `
`,
},
{
name: "with short json output",
flags: []string{"-q", "--output", "json"},
rels: []*release.Release{
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas"}),
},
expected: regexp.QuoteMeta(`["atlas"]
`),
},
{
name: "with short yaml output",
flags: []string{"-q", "--output", "yaml"},
rels: []*release.Release{
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas"}),
},
expected: regexp.QuoteMeta(`- atlas
`),
},
{
name: "with json output without next",
flags: []string{"--output", "json"},
rels: []*release.Release{},
expected: regexp.QuoteMeta(`{"Next":"","Releases":[]}
`),
},
{
name: "with yaml output without next",
flags: []string{"--output", "yaml"},
rels: []*release.Release{},
expected: regexp.QuoteMeta(`Next: ""
Releases: []
`),
},
{
name: "with unknown output format",
flags: []string{"--output", "_unknown_"},
rels: []*release.Release{},
err: true,
expected: regexp.QuoteMeta(``),
},
{ {
name: "list, one deployed, one failed", name: "list, one deployed, one failed",
flags: []string{"-q"}, flags: []string{"-q"},

@ -49,6 +49,7 @@ helm list [flags] [FILTER]
-m, --max int maximum number of releases to fetch (default 256) -m, --max int maximum number of releases to fetch (default 256)
--namespace string show releases within a specific namespace --namespace string show releases within a specific namespace
-o, --offset string next release name in the list, used to offset from start value -o, --offset string next release name in the list, used to offset from start value
--output string output the specified format (json or yaml)
--pending show pending releases --pending show pending releases
-r, --reverse reverse the sort order -r, --reverse reverse the sort order
-q, --short output short (quiet) listing format -q, --short output short (quiet) listing format
@ -73,4 +74,4 @@ helm list [flags] [FILTER]
### SEE ALSO ### SEE ALSO
* [helm](helm.md) - The Helm package manager for Kubernetes. * [helm](helm.md) - The Helm package manager for Kubernetes.
###### Auto generated by spf13/cobra on 8-Mar-2018 ###### Auto generated by spf13/cobra on 17-Apr-2018

@ -49,9 +49,28 @@ var _ Interface = (*FakeClient)(nil)
// ListReleases lists the current releases // ListReleases lists the current releases
func (c *FakeClient) ListReleases(opts ...ReleaseListOption) (*rls.ListReleasesResponse, error) { func (c *FakeClient) ListReleases(opts ...ReleaseListOption) (*rls.ListReleasesResponse, error) {
reqOpts := c.Opts
for _, opt := range opts {
opt(&reqOpts)
}
req := &reqOpts.listReq
rels := c.Rels
count := int64(len(c.Rels))
var next string
limit := req.GetLimit()
// TODO: Handle all other options.
if limit != 0 && limit < count {
rels = rels[:limit]
count = limit
next = c.Rels[limit].GetName()
}
resp := &rls.ListReleasesResponse{ resp := &rls.ListReleasesResponse{
Count: int64(len(c.Rels)), Count: count,
Releases: c.Rels, Releases: rels,
}
if next != "" {
resp.Next = next
} }
return resp, nil return resp, nil
} }

Loading…
Cancel
Save