Commit c87a902b authored by Brian's avatar Brian Committed by GitHub

Merge pull request #1274 from fibonacci1729/feat/history

feat(helm/cmd): support for retrieving release history
parents ea66d66d e9dd302a
......@@ -75,6 +75,9 @@ service ReleaseService {
rpc RollbackRelease(RollbackReleaseRequest) returns (RollbackReleaseResponse) {
}
// ReleaseHistory retrieves a releasse's history.
rpc GetHistory(GetHistoryRequest) returns (GetHistoryResponse) {
}
}
// ListReleasesRequest requests a list of releases.
......@@ -262,3 +265,16 @@ message GetVersionRequest {
message GetVersionResponse {
hapi.version.Version Version = 1;
}
// GetHistoryRequest requests a release's history.
message GetHistoryRequest {
// The name of the release.
string name = 1;
// The maximum number of releases to include.
int32 max = 2;
}
// GetHistoryResponse is received in response to a GetHistory rpc.
message GetHistoryResponse {
repeated hapi.release.Release releases = 1;
}
......@@ -93,6 +93,7 @@ func newRootCmd(out io.Writer) *cobra.Command {
newFetchCmd(out),
newGetCmd(nil, out),
newHomeCmd(out),
newHistoryCmd(nil, out),
newInitCmd(out),
newInspectCmd(nil, out),
newInstallCmd(nil, out),
......
......@@ -175,6 +175,10 @@ func (c *fakeReleaseClient) ReleaseContent(rlsName string, opts ...helm.ContentO
return resp, c.err
}
func (c *fakeReleaseClient) ReleaseHistory(rlsName string, opts ...helm.HistoryOption) (*rls.GetHistoryResponse, error) {
return &rls.GetHistoryResponse{Releases: c.rels}, c.err
}
func (c *fakeReleaseClient) Option(opt ...helm.Option) helm.Interface {
return c
}
......
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"io"
"github.com/gosuri/uitable"
"github.com/spf13/cobra"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/proto/hapi/release"
"k8s.io/helm/pkg/timeconv"
)
var historyHelp = `
History prints historical revisions for a given release.
A default maximum of 256 revisions will be returned. Setting '--max'
configures the maximum length of the revision list returned.
The historical release set is printed as a formatted table, e.g:
$ helm history angry-bird --max=4
REVISION UPDATED STATUS CHART
4 Mon Oct 3 10:15:13 2016 DEPLOYED alpine-0.1.0
3 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0
2 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0
1 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0
`
type historyCmd struct {
max int32
rls string
out io.Writer
helmc helm.Interface
}
func newHistoryCmd(c helm.Interface, w io.Writer) *cobra.Command {
his := &historyCmd{out: w, helmc: c}
cmd := &cobra.Command{
Use: "history [flags] RELEASE_NAME",
Long: historyHelp,
Short: "fetch release history",
Aliases: []string{"hist"},
PersistentPreRunE: setupConnection,
RunE: func(cmd *cobra.Command, args []string) error {
switch {
case len(args) == 0:
return errReleaseRequired
case his.helmc == nil:
his.helmc = helm.NewClient(helm.Host(tillerHost))
}
his.rls = args[0]
return his.run()
},
}
cmd.Flags().Int32Var(&his.max, "max", 256, "maximum number of revision to include in history")
return cmd
}
func (cmd *historyCmd) run() error {
opts := []helm.HistoryOption{
helm.WithMaxHistory(cmd.max),
}
r, err := cmd.helmc.ReleaseHistory(cmd.rls, opts...)
if err != nil {
return prettyError(err)
}
if len(r.Releases) == 0 {
return nil
}
fmt.Fprintln(cmd.out, formatHistory(r.Releases))
return nil
}
func formatHistory(rls []*release.Release) string {
tbl := uitable.New()
tbl.MaxColWidth = 30
tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART")
for _, r := range rls {
c := fmt.Sprintf("%s-%s", r.Chart.Metadata.Name, r.Chart.Metadata.Version)
t := timeconv.String(r.Info.LastDeployed)
s := r.Info.Status.Code.String()
v := r.Version
tbl.AddRow(v, t, s, c)
}
return tbl.String()
}
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"bytes"
"regexp"
"testing"
rpb "k8s.io/helm/pkg/proto/hapi/release"
)
func TestHistoryCmd(t *testing.T) {
mk := func(name string, vers int32, code rpb.Status_Code) *rpb.Release {
return releaseMock(&releaseOptions{
name: name,
version: vers,
statusCode: code,
})
}
tests := []struct {
cmds string
desc string
args []string
resp []*rpb.Release
xout string
}{
{
cmds: "helm history RELEASE_NAME",
desc: "get history for release",
args: []string{"angry-bird"},
resp: []*rpb.Release{
mk("angry-bird", 4, rpb.Status_DEPLOYED),
mk("angry-bird", 3, rpb.Status_SUPERSEDED),
mk("angry-bird", 2, rpb.Status_SUPERSEDED),
mk("angry-bird", 1, rpb.Status_SUPERSEDED),
},
xout: "REVISION\tUPDATED \tSTATUS \tCHART \n4 \t(.*)\tDEPLOYED \tfoo-0.1.0-beta.1\n3 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\n2 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\n1 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\n",
},
{
cmds: "helm history --max=MAX RELEASE_NAME",
desc: "get history with max limit set",
args: []string{"--max=2", "angry-bird"},
resp: []*rpb.Release{
mk("angry-bird", 4, rpb.Status_DEPLOYED),
mk("angry-bird", 3, rpb.Status_SUPERSEDED),
},
xout: "REVISION\tUPDATED \tSTATUS \tCHART \n4 \t(.*)\tDEPLOYED \tfoo-0.1.0-beta.1\n3 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\n",
},
}
var buf bytes.Buffer
for _, tt := range tests {
frc := &fakeReleaseClient{rels: tt.resp}
cmd := newHistoryCmd(frc, &buf)
cmd.ParseFlags(tt.args)
if err := cmd.RunE(cmd, tt.args); err != nil {
t.Fatalf("%q\n\t%s: unexpected error: %v", tt.cmds, tt.desc, err)
}
re := regexp.MustCompile(tt.xout)
if !re.Match(buf.Bytes()) {
t.Fatalf("%q\n\t%s:\nexpected\n\t%q\nactual\n\t%q", tt.cmds, tt.desc, tt.xout, buf.String())
}
buf.Reset()
}
}
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"sort"
"golang.org/x/net/context"
rpb "k8s.io/helm/pkg/proto/hapi/release"
tpb "k8s.io/helm/pkg/proto/hapi/services"
)
func (s *releaseServer) GetHistory(ctx context.Context, req *tpb.GetHistoryRequest) (*tpb.GetHistoryResponse, error) {
if !checkClientVersion(ctx) {
return nil, errIncompatibleVersion
}
h, err := s.env.Releases.History(req.Name)
if err != nil {
return nil, err
}
sort.Sort(sort.Reverse(byRev(h)))
var resp tpb.GetHistoryResponse
for i := 0; i < min(len(h), int(req.Max)); i++ {
resp.Releases = append(resp.Releases, h[i])
}
return &resp, nil
}
type byRev []*rpb.Release
func (s byRev) Len() int { return len(s) }
func (s byRev) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s byRev) Less(i, j int) bool { return s[i].Version < s[j].Version }
func min(x, y int) int {
if x < y {
return x
}
return y
}
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"reflect"
"testing"
"k8s.io/helm/pkg/helm"
rpb "k8s.io/helm/pkg/proto/hapi/release"
tpb "k8s.io/helm/pkg/proto/hapi/services"
)
func TestGetHistory_WithRevisions(t *testing.T) {
mk := func(name string, vers int32, code rpb.Status_Code) *rpb.Release {
return &rpb.Release{
Name: name,
Version: vers,
Info: &rpb.Info{Status: &rpb.Status{Code: code}},
}
}
// GetReleaseHistoryTests
tests := []struct {
desc string
req *tpb.GetHistoryRequest
res *tpb.GetHistoryResponse
}{
{
desc: "get release with history and default limit (max=256)",
req: &tpb.GetHistoryRequest{Name: "angry-bird", Max: 256},
res: &tpb.GetHistoryResponse{Releases: []*rpb.Release{
mk("angry-bird", 4, rpb.Status_DEPLOYED),
mk("angry-bird", 3, rpb.Status_SUPERSEDED),
mk("angry-bird", 2, rpb.Status_SUPERSEDED),
mk("angry-bird", 1, rpb.Status_SUPERSEDED),
}},
},
{
desc: "get release with history using result limit (max=2)",
req: &tpb.GetHistoryRequest{Name: "angry-bird", Max: 2},
res: &tpb.GetHistoryResponse{Releases: []*rpb.Release{
mk("angry-bird", 4, rpb.Status_DEPLOYED),
mk("angry-bird", 3, rpb.Status_SUPERSEDED),
}},
},
}
// test release history for release 'angry-bird'
hist := []*rpb.Release{
mk("angry-bird", 4, rpb.Status_DEPLOYED),
mk("angry-bird", 3, rpb.Status_SUPERSEDED),
mk("angry-bird", 2, rpb.Status_SUPERSEDED),
mk("angry-bird", 1, rpb.Status_SUPERSEDED),
}
srv := rsFixture()
for _, rls := range hist {
if err := srv.env.Releases.Create(rls); err != nil {
t.Fatalf("Failed to create release: %s", err)
}
}
// run tests
for _, tt := range tests {
res, err := srv.GetHistory(helm.NewContext(), tt.req)
if err != nil {
t.Fatalf("%s:\nFailed to get History of %q: %s", tt.desc, tt.req.Name, err)
}
if !reflect.DeepEqual(res, tt.res) {
t.Fatalf("%s:\nExpected:\n\t%+v\nActual\n\t%+v", tt.desc, tt.res, res)
}
}
}
func TestGetHistory_WithNoRevisions(t *testing.T) {
tests := []struct {
desc string
req *tpb.GetHistoryRequest
}{
{
desc: "get release with no history",
req: &tpb.GetHistoryRequest{Name: "sad-panda", Max: 256},
},
}
// create release 'sad-panda' with no revision history
rls := namedReleaseStub("sad-panda", rpb.Status_DEPLOYED)
srv := rsFixture()
srv.env.Releases.Create(rls)
for _, tt := range tests {
res, err := srv.GetHistory(helm.NewContext(), tt.req)
if err != nil {
t.Fatalf("%s:\nFailed to get History of %q: %s", tt.desc, tt.req.Name, err)
}
if len(res.Releases) > 1 {
t.Fatalf("%s:\nExpected zero items, got %d", tt.desc, len(res.Releases))
}
}
}
......@@ -225,6 +225,24 @@ func (h *Client) ReleaseContent(rlsName string, opts ...ContentOption) (*rls.Get
return h.content(ctx, req)
}
// ReleaseHistory returns a release's revision history.
func (h *Client) ReleaseHistory(rlsName string, opts ...HistoryOption) (*rls.GetHistoryResponse, error) {
for _, opt := range opts {
opt(&h.opts)
}
req := &h.opts.histReq
req.Name = rlsName
ctx := NewContext()
if h.opts.before != nil {
if err := h.opts.before(ctx, req); err != nil {
return nil, err
}
}
return h.history(ctx, req)
}
// Executes tiller.ListReleases RPC.
func (h *Client) list(ctx context.Context, req *rls.ListReleasesRequest) (*rls.ListReleasesResponse, error) {
c, err := grpc.Dial(h.opts.host, grpc.WithInsecure())
......@@ -325,3 +343,15 @@ func (h *Client) version(ctx context.Context, req *rls.GetVersionRequest) (*rls.
rlc := rls.NewReleaseServiceClient(c)
return rlc.GetVersion(ctx, req)
}
// Executes tiller.GetHistory RPC.
func (h *Client) history(ctx context.Context, req *rls.GetHistoryRequest) (*rls.GetHistoryResponse, error) {
c, err := grpc.Dial(h.opts.host, grpc.WithInsecure())
if err != nil {
return nil, err
}
defer c.Close()
rlc := rls.NewReleaseServiceClient(c)
return rlc.GetHistory(ctx, req)
}
......@@ -29,5 +29,6 @@ type Interface interface {
UpdateRelease(rlsName, chStr string, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error)
RollbackRelease(rlsName string, opts ...RollbackOption) (*rls.RollbackReleaseResponse, error)
ReleaseContent(rlsName string, opts ...ContentOption) (*rls.GetReleaseContentResponse, error)
ReleaseHistory(rlsName string, opts ...HistoryOption) (*rls.GetHistoryResponse, error)
GetVersion(opts ...VersionOption) (*rls.GetVersionResponse, error)
}
......@@ -60,6 +60,8 @@ type options struct {
rollbackReq rls.RollbackReleaseRequest
// before intercepts client calls before sending
before func(context.Context, proto.Message) error
// release history options are applied directly to the get release history request
histReq rls.GetHistoryRequest
}
// Host specifies the host address of the Tiller release server, (default = ":44134").
......@@ -272,6 +274,18 @@ type UpdateOption func(*options)
// running the `helm rollback` command.
type RollbackOption func(*options)
// HistoryOption allows configuring optional request data for
// issuing a GetHistory rpc.
type HistoryOption func(*options)
// WithMaxHistory sets the max number of releases to return
// in a release history query.
func WithMaxHistory(max int32) HistoryOption {
return func(opts *options) {
opts.histReq.Max = max
}
}
// NewContext creates a versioned context.
func NewContext() context.Context {
md := metadata.Pairs("x-helm-api-client", version.Version)
......
This diff is collapsed.
......@@ -126,6 +126,18 @@ func (s *Storage) Deployed(name string) (*rspb.Release, error) {
}
}
// History returns the revision history for the release with the provided name, or
// returns ErrReleaseNotFound if no such release name exists.
func (s *Storage) History(name string) ([]*rspb.Release, error) {
log.Printf("Getting release history for '%s'\n", name)
l, err := s.Driver.Query(map[string]string{"NAME": name, "OWNER": "TILLER"})
if err != nil {
return nil, err
}
return l, nil
}
// makeKey concatenates a release name and version into
// a string with format ```<release_name>#v<version>```.
// This key is used to uniquely identify storage objects.
......
......@@ -186,6 +186,37 @@ func TestStorageDeployed(t *testing.T) {
}
}
func TestStorageHistory(t *testing.T) {
storage := Init(driver.NewMemory())
const name = "angry-bird"
// setup storage with test releases
setup := func() {
// release records
rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.Status_SUPERSEDED}.ToRelease()
rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.Status_SUPERSEDED}.ToRelease()
rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.Status_SUPERSEDED}.ToRelease()
rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.Status_DEPLOYED}.ToRelease()
// create the release records in the storage
assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)")
assertErrNil(t.Fatal, storage.Create(rls1), "Storing release 'angry-bird' (v2)")
assertErrNil(t.Fatal, storage.Create(rls2), "Storing release 'angry-bird' (v3)")
assertErrNil(t.Fatal, storage.Create(rls3), "Storing release 'angry-bird' (v4)")
}
setup()
h, err := storage.History(name)
if err != nil {
t.Fatalf("Failed to query for release history (%q): %s\n", name, err)
}
if len(h) != 4 {
t.Fatalf("Release history (%q) is empty\n", name)
}
}
type ReleaseTestData struct {
Name string
Version int32
......
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