diff --git a/cmd/helm/install.go b/cmd/helm/install.go index ec16146921db4c327cae7d509b0f322a00f3d8b5..3f7d701f1d966140d7b156b0c11d4cfdeeb01711 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 0000000000000000000000000000000000000000..19e9f6c9e46a897716c01fd091e211d068c06b26 --- /dev/null +++ b/cmd/helm/list.go @@ -0,0 +1,111 @@ +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. 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]", + 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.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) +} + +func listCmd(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + fmt.Println("TODO: Implement filter.") + } + + res, err := helm.ListReleases(listMax, listOffset) + if err != nil { + return err + } + + rels := res.Releases + if res.Count+res.Offset < res.Total { + fmt.Println("Not all values were fetched.") + } + + if listByDate { + sort.Sort(byDate(rels)) + } else { + sort.Sort(byName(rels)) + } + + // 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 { + table := uitable.New() + table.MaxColWidth = 30 + table.AddRow("NAME", "UPDATED", "CHART") + for _, r := range rels { + 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 68f37daf481d4fb20dccea0336cd3de26f23ff03..2333b3c2b57cfcc7214af7c3cca62e632b53d3fc 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,8 +34,43 @@ 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 { - return errNotImplemented + 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: total, + Releases: rels, + } + stream.Send(res) + return nil } func (s *releaseServer) GetReleaseStatus(c ctx.Context, req *services.GetReleaseStatusRequest) (*services.GetReleaseStatusResponse, error) { diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index 53d51c505b8e57a9cf9815a805c255a5938959c5..f76e3d91ecc0e9ed5cb6ffe20b8897f37433b228 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/docs/examples/alpine/Chart.yaml b/docs/examples/alpine/Chart.yaml index 0e86771b38ad35a147dcdb09e0d92082f3b5507a..cab858d0a2ecd178ea3a190f08dcb128378e9a6e 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 8a5913bb24954d1a67a14da4660eeeec9e1610bd..4b366e85f0a7ba28e2e2d99c41dbe382431d82b4 100644 --- a/pkg/helm/helm.go +++ b/pkg/helm/helm.go @@ -13,8 +13,23 @@ 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{ + Limit: int64(limit), + Offset: int64(offset), + } + 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.