Commit b243a86f authored by Matt Butcher's avatar Matt Butcher

Merge pull request #669 from technosophos/feat/better-list

feat(helm,tiller): improve list output, support filters
parents 14516a21 a8642c81
......@@ -53,26 +53,61 @@ service ReleaseService {
}
// ListReleasesRequest requests a list of releases.
//
// Releases can be retrieved in chunks by setting limit and offset.
//
// Releases can be sorted according to a few pre-determined sort stategies.
message ListReleasesRequest {
// The maximum number of releases to be returned
// Limit is the maximum number of releases to be returned.
int64 limit = 1;
// The zero-based offset at which the returned release list begins
int64 offset = 2;
// Offset is the last release name that was seen. The next listing
// operation will start with the name after this one.
// Example: If list one returns albert, bernie, carl, and sets 'next: dennis'.
// dennis is the offset. Supplying 'dennis' for the next request should
// cause the next batch to return a set of results starting with 'dennis'.
string offset = 2;
// SortBy is the sort field that the ListReleases server should sort data before returning.
ListSort.SortBy sort_by = 3;
// Filter is a regular expression used to filter which releases should be listed.
//
// Anything that matches the regexp will be included in the results.
string filter = 4;
ListSort.SortOrder sort_order = 5;
}
// ListSort defines sorting fields on a release list.
message ListSort{
// SortBy defines sort operations.
enum SortBy {
UNKNOWN = 0;
NAME = 1;
LAST_RELEASED = 2;
}
// SortOrder defines sort orders to augment sorting operations.
enum SortOrder {
ASC = 0;
DESC = 1;
}
}
// ListReleasesResponse is a list of releases.
message ListReleasesResponse {
// The expected total number of releases to be returned
// Count is the expected total number of releases to be returned.
int64 count = 1;
// The zero-based offset at which the list is positioned
int64 offset = 2;
// Next is the name of the next release. If this is other than an empty
// string, it means there are more results.
string next = 2;
// The total number of queryable releases
// Total is the total number of queryable releases.
int64 total = 3;
// The resulting releases
// Releases is the list of found release objects.
repeated hapi.release.Release releases = 4;
}
......
......@@ -2,11 +2,12 @@ package main
import (
"fmt"
"sort"
"strings"
"github.com/gosuri/uitable"
"github.com/kubernetes/helm/pkg/helm"
"github.com/kubernetes/helm/pkg/proto/hapi/release"
"github.com/kubernetes/helm/pkg/proto/hapi/services"
"github.com/kubernetes/helm/pkg/timeconv"
"github.com/spf13/cobra"
)
......@@ -14,54 +15,79 @@ import (
var listHelp = `
This command lists all of the currently deployed releases.
By default, items are sorted alphabetically. Sorting is done client-side, so if
the number of releases is less than the setting in '--max', some values will
be omitted, and in no particular lexicographic order.
By default, items are sorted alphabetically. Use the '-d' flag to sort by
release date.
If an argument is provided, it will be treated as a filter. Filters are
regular expressions (Perl compatible) that are applied to the list of releases.
Only items that match the filter will be returned.
$ helm list -l 'ara[a-z]+'
NAME UPDATED CHART
maudlin-arachnid Mon May 9 16:07:08 2016 alpine-0.1.0
If no results are found, 'helm list' will exit 0, but with no output (or in
the case of '-l', only headers).
By default, up to 256 items may be returned. To limit this, use the '--max' flag.
Setting '--max' to 0 will not return all results. Rather, it will return the
server's default, which may be much higher than 256. Pairing the '--max'
flag with the '--offset' flag allows you to page through results.
`
var listCommand = &cobra.Command{
Use: "list [flags]",
Use: "list [flags] [FILTER]",
Short: "List releases",
Long: listHelp,
RunE: listCmd,
Aliases: []string{"ls"},
}
var listLong bool
var listMax int
var listOffset int
var listByDate bool
var (
listLong bool
listMax int
listOffset string
listByDate bool
listSortDesc bool
)
func init() {
listCommand.Flags().BoolVarP(&listLong, "long", "l", false, "output long listing format")
listCommand.Flags().BoolVarP(&listByDate, "date", "d", false, "sort by release date")
listCommand.Flags().IntVarP(&listMax, "max", "m", 256, "maximum number of releases to fetch")
listCommand.Flags().IntVarP(&listOffset, "offset", "o", 0, "offset from start value (zero-indexed)")
f := listCommand.Flags()
f.BoolVarP(&listLong, "long", "l", false, "output long listing format")
f.BoolVarP(&listByDate, "date", "d", false, "sort by release date")
f.BoolVarP(&listSortDesc, "reverse", "r", false, "reverse the sort order")
f.IntVarP(&listMax, "max", "m", 256, "maximum number of releases to fetch")
f.StringVarP(&listOffset, "offset", "o", "", "the next release name in the list, used to offset from start value")
RootCommand.AddCommand(listCommand)
}
func listCmd(cmd *cobra.Command, args []string) error {
var filter string
if len(args) > 0 {
fmt.Println("TODO: Implement filter.")
filter = strings.Join(args, " ")
}
res, err := helm.ListReleases(listMax, listOffset)
if err != nil {
return prettyError(err)
sortBy := services.ListSort_NAME
if listByDate {
sortBy = services.ListSort_LAST_RELEASED
}
rels := res.Releases
if res.Count+res.Offset < res.Total {
fmt.Println("Not all values were fetched.")
sortOrder := services.ListSort_ASC
if listSortDesc {
sortOrder = services.ListSort_DESC
}
if listByDate {
sort.Sort(byDate(rels))
} else {
sort.Sort(byName(rels))
res, err := helm.ListReleases(listMax, listOffset, sortBy, sortOrder, filter)
if err != nil {
return prettyError(err)
}
// Purty output, ya'll
if res.Next != "" {
fmt.Printf("\tnext: %s", res.Next)
}
rels := res.Releases
if listLong {
return formatList(rels)
}
......@@ -85,26 +111,3 @@ func formatList(rels []*release.Release) error {
return nil
}
// byName implements the sort.Interface for []*release.Release.
type byName []*release.Release
func (r byName) Len() int {
return len(r)
}
func (r byName) Swap(p, q int) {
r[p], r[q] = r[q], r[p]
}
func (r byName) Less(i, j int) bool {
return r[i].Name < r[j].Name
}
type byDate []*release.Release
func (r byDate) Len() int { return len(r) }
func (r byDate) Swap(p, q int) {
r[p], r[q] = r[q], r[p]
}
func (r byDate) Less(p, q int) bool {
return r[p].Info.LastDeployed.Seconds < r[q].Info.LastDeployed.Seconds
}
......@@ -5,6 +5,8 @@ import (
"errors"
"fmt"
"log"
"regexp"
"sort"
"github.com/kubernetes/helm/cmd/tiller/environment"
"github.com/kubernetes/helm/pkg/proto/hapi/release"
......@@ -47,14 +49,49 @@ func (s *releaseServer) ListReleases(req *services.ListReleasesRequest, stream s
return err
}
if len(req.Filter) != 0 {
rels, err = filterReleases(req.Filter, rels)
if err != nil {
return err
}
}
total := int64(len(rels))
switch req.SortBy {
case services.ListSort_NAME:
sort.Sort(byName(rels))
case services.ListSort_LAST_RELEASED:
sort.Sort(byDate(rels))
}
if req.SortOrder == services.ListSort_DESC {
ll := len(rels)
rr := make([]*release.Release, ll)
for i, item := range rels {
rr[ll-i-1] = item
}
rels = rr
}
l := int64(len(rels))
if req.Offset > 0 {
if req.Offset >= l {
return fmt.Errorf("offset %d is outside of range %d", req.Offset, l)
if req.Offset != "" {
i := -1
for ii, cur := range rels {
if cur.Name == req.Offset {
i = ii
}
}
rels = rels[req.Offset:]
if i == -1 {
return fmt.Errorf("offset %q not found", req.Offset)
}
if len(rels) < i {
return fmt.Errorf("no items after %q", req.Offset)
}
rels = rels[i:]
l = int64(len(rels))
}
......@@ -62,13 +99,15 @@ func (s *releaseServer) ListReleases(req *services.ListReleasesRequest, stream s
req.Limit = ListDefaultLimit
}
next := ""
if l > req.Limit {
next = rels[req.Limit].Name
rels = rels[0:req.Limit]
l = int64(len(rels))
}
res := &services.ListReleasesResponse{
Offset: 0,
Next: next,
Count: l,
Total: total,
Releases: rels,
......@@ -77,6 +116,20 @@ func (s *releaseServer) ListReleases(req *services.ListReleasesRequest, stream s
return nil
}
func filterReleases(filter string, rels []*release.Release) ([]*release.Release, error) {
preg, err := regexp.Compile(filter)
if err != nil {
return rels, err
}
matches := []*release.Release{}
for _, r := range rels {
if preg.MatchString(r.Name) {
matches = append(matches, r)
}
}
return matches, nil
}
func (s *releaseServer) GetReleaseStatus(c ctx.Context, req *services.GetReleaseStatusRequest) (*services.GetReleaseStatusResponse, error) {
if req.Name == "" {
return nil, errMissingRelease
......@@ -224,3 +277,26 @@ func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR
res := services.UninstallReleaseResponse{Release: rel}
return &res, nil
}
// byName implements the sort.Interface for []*release.Release.
type byName []*release.Release
func (r byName) Len() int {
return len(r)
}
func (r byName) Swap(p, q int) {
r[p], r[q] = r[q], r[p]
}
func (r byName) Less(i, j int) bool {
return r[i].Name < r[j].Name
}
type byDate []*release.Release
func (r byDate) Len() int { return len(r) }
func (r byDate) Swap(p, q int) {
r[p], r[q] = r[q], r[p]
}
func (r byDate) Less(p, q int) bool {
return r[p].Info.LastDeployed.Seconds < r[q].Info.LastDeployed.Seconds
}
......@@ -208,7 +208,7 @@ func TestListReleases(t *testing.T) {
}
mrs := &mockListServer{}
if err := rs.ListReleases(&services.ListReleasesRequest{Offset: 0, Limit: 64}, mrs); err != nil {
if err := rs.ListReleases(&services.ListReleasesRequest{Offset: "", Limit: 64}, mrs); err != nil {
t.Fatalf("Failed listing: %s", err)
}
......@@ -217,6 +217,86 @@ func TestListReleases(t *testing.T) {
}
}
func TestListReleasesSort(t *testing.T) {
rs := rsFixture()
// Put them in by reverse order so that the mock doesn't "accidentally"
// sort.
num := 7
for i := num; i > 0; i-- {
rel := releaseMock()
rel.Name = fmt.Sprintf("rel-%d", i)
if err := rs.env.Releases.Create(rel); err != nil {
t.Fatalf("Could not store mock release: %s", err)
}
}
limit := 6
mrs := &mockListServer{}
req := &services.ListReleasesRequest{
Offset: "",
Limit: int64(limit),
SortBy: services.ListSort_NAME,
}
if err := rs.ListReleases(req, mrs); err != nil {
t.Fatalf("Failed listing: %s", err)
}
if len(mrs.val.Releases) != limit {
t.Errorf("Expected %d releases, got %d", limit, len(mrs.val.Releases))
}
for i := 0; i < limit; i++ {
n := fmt.Sprintf("rel-%d", i+1)
if mrs.val.Releases[i].Name != n {
t.Errorf("Expected %q, got %q", n, mrs.val.Releases[i].Name)
}
}
}
func TestListReleasesFilter(t *testing.T) {
rs := rsFixture()
names := []string{
"axon",
"dendrite",
"neuron",
"neuroglia",
"synapse",
"nucleus",
"organelles",
}
num := 7
for i := 0; i < num; i++ {
rel := releaseMock()
rel.Name = names[i]
if err := rs.env.Releases.Create(rel); err != nil {
t.Fatalf("Could not store mock release: %s", err)
}
}
mrs := &mockListServer{}
req := &services.ListReleasesRequest{
Offset: "",
Limit: 64,
Filter: "neuro[a-z]+",
SortBy: services.ListSort_NAME,
}
if err := rs.ListReleases(req, mrs); err != nil {
t.Fatalf("Failed listing: %s", err)
}
if len(mrs.val.Releases) != 2 {
t.Errorf("Expected 2 releases, got %d", len(mrs.val.Releases))
}
if mrs.val.Releases[0].Name != "neuroglia" {
t.Errorf("Unexpected sort order: %v.", mrs.val.Releases)
}
if mrs.val.Releases[1].Name != "neuron" {
t.Errorf("Unexpected sort order: %v.", mrs.val.Releases)
}
}
func mockEnvironment() *environment.Environment {
e := environment.New()
e.Releases = storage.NewMemory()
......
......@@ -13,7 +13,7 @@ var Config = &config{
}
// ListReleases lists the current releases.
func ListReleases(limit, offset int) (*services.ListReleasesResponse, error) {
func ListReleases(limit int, offset string, sort services.ListSort_SortBy, order services.ListSort_SortOrder, filter string) (*services.ListReleasesResponse, error) {
c := Config.client()
if err := c.dial(); err != nil {
return nil, err
......@@ -21,8 +21,11 @@ func ListReleases(limit, offset int) (*services.ListReleasesResponse, error) {
defer c.Close()
req := &services.ListReleasesRequest{
Limit: int64(limit),
Offset: int64(offset),
Limit: int64(limit),
Offset: offset,
SortBy: sort,
SortOrder: order,
Filter: filter,
}
cli, err := c.impl.ListReleases(context.TODO(), req, c.cfg.CallOpts()...)
if err != nil {
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment