Skip to content

Commit

Permalink
feat(list): Optional output as JSON and YAML
Browse files Browse the repository at this point in the history
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
  • Loading branch information
seaneagan committed Apr 18, 2018
1 parent 1ee1706 commit 49c3d50
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 29 deletions.
138 changes: 113 additions & 25 deletions cmd/helm/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ limitations under the License.
package main

import (
"encoding/json"
"fmt"
"io"
"strings"

"github.com/ghodss/yaml"
"github.com/gosuri/uitable"
"github.com/spf13/cobra"

Expand Down Expand Up @@ -75,6 +77,22 @@ type listCmd struct {
pending bool
client helm.Interface
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 {
Expand Down Expand Up @@ -114,6 +132,7 @@ func newListCmd(client helm.Interface, out io.Writer) *cobra.Command {
f.BoolVar(&list.pending, "pending", false, "show pending releases")
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.StringVar(&list.output, "output", "", "output the specified format (json or yaml)")

// TODO: Do we want this as a feature of 'helm list'?
//f.BoolVar(&list.superseded, "history", true, "show historical releases")
Expand Down Expand Up @@ -148,23 +167,17 @@ func (l *listCmd) run() error {
return prettyError(err)
}

if len(res.GetReleases()) == 0 {
return nil
}
rels := filterList(res.Releases)

if res.Next != "" && !l.short {
fmt.Fprintf(l.out, "\tnext: %s\n", res.Next)
}
result := getListResult(rels, res.Next)

rels := filterList(res.Releases)
output, err := formatResult(l.output, l.short, result, l.colWidth)

if l.short {
for _, r := range rels {
fmt.Fprintln(l.out, r.Name)
}
return nil
if err != nil {
return prettyError(err)
}
fmt.Fprintln(l.out, formatList(rels, l.colWidth))

fmt.Fprintln(l.out, output)
return nil
}

Expand Down Expand Up @@ -233,23 +246,98 @@ func (l *listCmd) statusCodes() []release.Status_Code {
return status
}

func formatList(rels []*release.Release, colWidth uint) string {
table := uitable.New()

table.MaxColWidth = colWidth
table.AddRow("NAME", "REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "NAMESPACE")
func getListResult(rels []*release.Release, next string) listResult {
listReleases := []listRelease{}
for _, r := range rels {
md := r.GetChart().GetMetadata()
c := fmt.Sprintf("%s-%s", md.GetName(), md.GetVersion())
t := "-"
if tspb := r.GetInfo().GetLastDeployed(); tspb != nil {
t = timeconv.String(tspb)
}
s := r.GetInfo().GetStatus().GetCode().String()
v := r.GetVersion()
a := md.GetAppVersion()
n := r.GetNamespace()
table.AddRow(r.GetName(), v, t, s, c, a, n)

lr := listRelease{
Name: r.GetName(),
Revision: r.GetVersion(),
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,
}
return table.String()
}

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 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")
}
80 changes: 79 additions & 1 deletion cmd/helm/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,18 @@ package main

import (
"io"
"regexp"
"testing"

"github.com/spf13/cobra"

"io/ioutil"
"os"

"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/proto/hapi/release"
"os"
)

func TestListCmd(t *testing.T) {
Expand All @@ -46,6 +48,11 @@ func TestListCmd(t *testing.T) {
ch, _ := chartutil.Load(chartPath)

tests := []releaseCase{
{
name: "empty",
rels: []*release.Release{},
expected: "",
},
{
name: "with a release",
rels: []*release.Release{
Expand All @@ -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",
},
{
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",
flags: []string{"-q"},
Expand Down
3 changes: 2 additions & 1 deletion docs/helm/helm_list.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ helm list [flags] [FILTER]
-m, --max int maximum number of releases to fetch (default 256)
--namespace string show releases within a specific namespace
-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
-r, --reverse reverse the sort order
-q, --short output short (quiet) listing format
Expand All @@ -73,4 +74,4 @@ helm list [flags] [FILTER]
### SEE ALSO
* [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
23 changes: 21 additions & 2 deletions pkg/helm/fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,28 @@ var _ Interface = (*FakeClient)(nil)

// ListReleases lists the current releases
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{
Count: int64(len(c.Rels)),
Releases: c.Rels,
Count: count,
Releases: rels,
}
if next != "" {
resp.Next = next
}
return resp, nil
}
Expand Down

0 comments on commit 49c3d50

Please sign in to comment.