From 6950fe42dd71e2bc6073b05849c07b42d80a53dd Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Mon, 2 May 2016 20:09:44 -0600 Subject: [PATCH 1/2] feat(helm, tiller): add release listings This adds client and server support for 'helm list'. --- cmd/helm/install.go | 11 +++--- cmd/helm/list.go | 68 +++++++++++++++++++++++++++++++++ cmd/tiller/release_server.go | 16 +++++++- docs/examples/alpine/Chart.yaml | 2 +- pkg/helm/helm.go | 16 +++++++- 5 files changed, 104 insertions(+), 9 deletions(-) create mode 100644 cmd/helm/list.go diff --git a/cmd/helm/install.go b/cmd/helm/install.go index ec1614692..3f7d701f1 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -54,12 +54,13 @@ func runInstall(cmd *cobra.Command, args []string) error { // Might be friendly to wrap our proto model with pretty-printers. // func printRelease(rel *release.Release) { + if rel == nil { + return + } + fmt.Printf("release.name: %s\n", rel.Name) if verbose { - if rel != nil { - fmt.Printf("release.name: %s\n", rel.Name) - fmt.Printf("release.info: %s\n", rel.GetInfo()) - fmt.Printf("release.chart: %s\n", rel.GetChart()) - } + fmt.Printf("release.info: %s\n", rel.GetInfo()) + fmt.Printf("release.chart: %s\n", rel.GetChart()) } } diff --git a/cmd/helm/list.go b/cmd/helm/list.go new file mode 100644 index 000000000..ed09c3431 --- /dev/null +++ b/cmd/helm/list.go @@ -0,0 +1,68 @@ +package main + +import ( + "fmt" + + "github.com/kubernetes/helm/pkg/helm" + "github.com/kubernetes/helm/pkg/proto/hapi/release" + "github.com/spf13/cobra" +) + +var listHelp = ` +This command lists all of the currently deployed releases. + +By default, items are sorted alphabetically. +` + +var listCommand = &cobra.Command{ + Use: "list [flags] [FILTER]", + Short: "List releases", + Long: listHelp, + RunE: listCmd, +} + +var listLong bool +var listMax int + +func init() { + listCommand.LocalFlags().BoolVar(&listLong, "l", false, "output long listing format") + listCommand.LocalFlags().IntVar(&listMax, "m", 256, "maximum number of releases to fetch") + RootCommand.AddCommand(listCommand) +} + +func listCmd(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + fmt.Println("TODO: Implement filter.") + } + + res, err := helm.ListReleases(listMax, 0) + if err != nil { + return err + } + + rels := res.Releases + if res.Count+res.Offset < res.Total { + fmt.Println("Not all values were fetched.") + } + + // TODO: Add sort here. + + // Purty output, ya'll + if listLong { + return formatList(rels) + } + for _, r := range rels { + fmt.Println(r.Name) + } + + return nil +} + +func formatList(rels []*release.Release) error { + // TODO: Pretty it up + for _, r := range rels { + fmt.Println(r.Name) + } + + return nil +} diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 68f37daf4..6c85b07b9 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -34,7 +34,21 @@ var ( ) func (s *releaseServer) ListReleases(req *services.ListReleasesRequest, stream services.ReleaseService_ListReleasesServer) error { - return errNotImplemented + rels, err := s.env.Releases.List() + if err != nil { + return err + } + + l := int64(len(rels)) + + res := &services.ListReleasesResponse{ + Offset: 0, + Count: l, + Total: l, + Releases: rels, + } + stream.Send(res) + return nil } func (s *releaseServer) GetReleaseStatus(c ctx.Context, req *services.GetReleaseStatusRequest) (*services.GetReleaseStatusResponse, error) { diff --git a/docs/examples/alpine/Chart.yaml b/docs/examples/alpine/Chart.yaml index 0e86771b3..cab858d0a 100644 --- a/docs/examples/alpine/Chart.yaml +++ b/docs/examples/alpine/Chart.yaml @@ -1,4 +1,4 @@ name: alpine description: Deploy a basic Alpine Linux pod version: 0.1.0 -home: "https://github.com/kubernetes/helm" +home: https://github.com/kubernetes/helm diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go index 8a5913bb2..48a805c94 100644 --- a/pkg/helm/helm.go +++ b/pkg/helm/helm.go @@ -13,8 +13,20 @@ var Config = &config{ } // ListReleases lists the current releases. -func ListReleases(limit, offset int) (<-chan *services.ListReleasesResponse, error) { - return nil, ErrNotImplemented +func ListReleases(limit, offset int) (*services.ListReleasesResponse, error) { + c := Config.client() + if err := c.dial(); err != nil { + return nil, err + } + defer c.Close() + + req := &services.ListReleasesRequest{} + cli, err := c.impl.ListReleases(context.TODO(), req, c.cfg.CallOpts()...) + if err != nil { + return nil, err + } + + return cli.Recv() } // GetReleaseStatus returns the given release's status. From e869c36cd29f7c7d80fba1ebf17d9b1c84926045 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 3 May 2016 16:27:09 -0600 Subject: [PATCH 2/2] feat(helm, tiller): implement list --- cmd/helm/list.go | 65 +++++++++++++++++++++++++------ cmd/tiller/release_server.go | 24 +++++++++++- cmd/tiller/release_server_test.go | 38 ++++++++++++++++++ pkg/helm/helm.go | 5 ++- 4 files changed, 119 insertions(+), 13 deletions(-) diff --git a/cmd/helm/list.go b/cmd/helm/list.go index ed09c3431..19e9f6c9e 100644 --- a/cmd/helm/list.go +++ b/cmd/helm/list.go @@ -2,31 +2,42 @@ package main import ( "fmt" + "sort" + "time" + "github.com/gosuri/uitable" "github.com/kubernetes/helm/pkg/helm" "github.com/kubernetes/helm/pkg/proto/hapi/release" + "github.com/kubernetes/helm/pkg/timeconv" "github.com/spf13/cobra" ) var listHelp = ` This command lists all of the currently deployed releases. -By default, items are sorted alphabetically. +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. ` var listCommand = &cobra.Command{ - Use: "list [flags] [FILTER]", - Short: "List releases", - Long: listHelp, - RunE: listCmd, + Use: "list [flags]", + Short: "List releases", + Long: listHelp, + RunE: listCmd, + Aliases: []string{"ls"}, } var listLong bool var listMax int +var listOffset int +var listByDate bool func init() { - listCommand.LocalFlags().BoolVar(&listLong, "l", false, "output long listing format") - listCommand.LocalFlags().IntVar(&listMax, "m", 256, "maximum number of releases to fetch") + 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)") RootCommand.AddCommand(listCommand) } @@ -35,7 +46,7 @@ func listCmd(cmd *cobra.Command, args []string) error { fmt.Println("TODO: Implement filter.") } - res, err := helm.ListReleases(listMax, 0) + res, err := helm.ListReleases(listMax, listOffset) if err != nil { return err } @@ -45,7 +56,11 @@ func listCmd(cmd *cobra.Command, args []string) error { fmt.Println("Not all values were fetched.") } - // TODO: Add sort here. + if listByDate { + sort.Sort(byDate(rels)) + } else { + sort.Sort(byName(rels)) + } // Purty output, ya'll if listLong { @@ -59,10 +74,38 @@ func listCmd(cmd *cobra.Command, args []string) error { } func formatList(rels []*release.Release) error { - // TODO: Pretty it up + table := uitable.New() + table.MaxColWidth = 30 + table.AddRow("NAME", "UPDATED", "CHART") for _, r := range rels { - fmt.Println(r.Name) + c := fmt.Sprintf("%s-%s", r.Chart.Metadata.Name, r.Chart.Metadata.Version) + t := timeconv.Format(r.Info.LastDeployed, time.ANSIC) + table.AddRow(r.Name, t, c) } + fmt.Println(table) 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 +} diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 6c85b07b9..2333b3c2b 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -3,6 +3,7 @@ package main import ( "bytes" "errors" + "fmt" "log" "github.com/kubernetes/helm/cmd/tiller/environment" @@ -33,18 +34,39 @@ var ( errMissingRelease = errors.New("no release provided") ) +// ListDefaultLimit is the default limit for number of items returned in a list. +var ListDefaultLimit int64 = 512 + func (s *releaseServer) ListReleases(req *services.ListReleasesRequest, stream services.ReleaseService_ListReleasesServer) error { rels, err := s.env.Releases.List() if err != nil { return err } + total := int64(len(rels)) + 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) + } + rels = rels[req.Offset:] + l = int64(len(rels)) + } + + if req.Limit == 0 { + req.Limit = ListDefaultLimit + } + + if l > req.Limit { + rels = rels[0:req.Limit] + l = int64(len(rels)) + } res := &services.ListReleasesResponse{ Offset: 0, Count: l, - Total: l, + Total: total, Releases: rels, } stream.Send(res) diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index 53d51c505..f76e3d91e 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "strings" "testing" @@ -12,6 +13,7 @@ import ( "github.com/kubernetes/helm/pkg/storage" "github.com/kubernetes/helm/pkg/timeconv" "golang.org/x/net/context" + "google.golang.org/grpc/metadata" ) func rsFixture() *releaseServer { @@ -193,8 +195,44 @@ func TestGetReleaseStatus(t *testing.T) { } } +func TestListReleases(t *testing.T) { + rs := rsFixture() + num := 7 + for i := 0; i < num; 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) + } + } + + mrs := &mockListServer{} + if err := rs.ListReleases(&services.ListReleasesRequest{Offset: 0, Limit: 64}, mrs); err != nil { + t.Fatalf("Failed listing: %s", err) + } + + if len(mrs.val.Releases) != num { + t.Errorf("Expected %d releases, got %d", num, len(mrs.val.Releases)) + } +} + func mockEnvironment() *environment.Environment { e := environment.New() e.Releases = storage.NewMemory() return e } + +type mockListServer struct { + val *services.ListReleasesResponse +} + +func (l *mockListServer) Send(res *services.ListReleasesResponse) error { + l.val = res + return nil +} + +func (l *mockListServer) Context() context.Context { return context.TODO() } +func (l *mockListServer) SendMsg(v interface{}) error { return nil } +func (l *mockListServer) RecvMsg(v interface{}) error { return nil } +func (l *mockListServer) SendHeader(m metadata.MD) error { return nil } +func (l *mockListServer) SetTrailer(m metadata.MD) {} diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go index 48a805c94..4b366e85f 100644 --- a/pkg/helm/helm.go +++ b/pkg/helm/helm.go @@ -20,7 +20,10 @@ func ListReleases(limit, offset int) (*services.ListReleasesResponse, error) { } defer c.Close() - req := &services.ListReleasesRequest{} + req := &services.ListReleasesRequest{ + Limit: int64(limit), + Offset: int64(offset), + } cli, err := c.impl.ListReleases(context.TODO(), req, c.cfg.CallOpts()...) if err != nil { return nil, err