feat(*): add --namespace flag to 'helm list'

Users can now specify a namespace filter for 'helm list'.  Only the
releases within the specified namespace will be shown.  For example,
'helm list --namespace foo' will only show releases for the 'foo'
namespace.  Also added a namespace field to the table view.

Closes #1563
pull/1922/head
Larry Rensing 8 years ago
parent 2c5e8d5bd4
commit 3a380923f4

@ -56,6 +56,7 @@ type releaseOptions struct {
version int32 version int32
chart *chart.Chart chart *chart.Chart
statusCode release.Status_Code statusCode release.Status_Code
namespace string
} }
func releaseMock(opts *releaseOptions) *release.Release { func releaseMock(opts *releaseOptions) *release.Release {
@ -71,6 +72,11 @@ func releaseMock(opts *releaseOptions) *release.Release {
version = opts.version version = opts.version
} }
namespace := opts.namespace
if namespace == "" {
namespace = "default"
}
ch := opts.chart ch := opts.chart
if opts.chart == nil { if opts.chart == nil {
ch = &chart.Chart{ ch = &chart.Chart{
@ -97,9 +103,10 @@ func releaseMock(opts *releaseOptions) *release.Release {
Status: &release.Status{Code: scode}, Status: &release.Status{Code: scode},
Description: "Release mock", Description: "Release mock",
}, },
Chart: ch, Chart: ch,
Config: &chart.Config{Raw: `name: "value"`}, Config: &chart.Config{Raw: `name: "value"`},
Version: version, Version: version,
Namespace: namespace,
Hooks: []*release.Hook{ Hooks: []*release.Hook{
{ {
Name: "pre-install-hook", Name: "pre-install-hook",

@ -70,6 +70,7 @@ type listCmd struct {
deleting bool deleting bool
deployed bool deployed bool
failed bool failed bool
namespace string
superseded bool superseded bool
client helm.Interface client helm.Interface
} }
@ -108,6 +109,8 @@ func newListCmd(client helm.Interface, out io.Writer) *cobra.Command {
f.BoolVar(&list.deleting, "deleting", false, "show releases that are currently being deleted") f.BoolVar(&list.deleting, "deleting", false, "show releases that are currently being deleted")
f.BoolVar(&list.deployed, "deployed", false, "show deployed releases. If no other is specified, this will be automatically enabled") f.BoolVar(&list.deployed, "deployed", false, "show deployed releases. If no other is specified, this will be automatically enabled")
f.BoolVar(&list.failed, "failed", false, "show failed releases") f.BoolVar(&list.failed, "failed", false, "show failed releases")
f.StringVar(&list.namespace, "namespace", "", "show releases within a specific namespace")
// TODO: Do we want this as a feature of 'helm list'? // TODO: Do we want this as a feature of 'helm list'?
//f.BoolVar(&list.superseded, "history", true, "show historical releases") //f.BoolVar(&list.superseded, "history", true, "show historical releases")
@ -134,6 +137,7 @@ func (l *listCmd) run() error {
helm.ReleaseListSort(int32(sortBy)), helm.ReleaseListSort(int32(sortBy)),
helm.ReleaseListOrder(int32(sortOrder)), helm.ReleaseListOrder(int32(sortOrder)),
helm.ReleaseListStatuses(stats), helm.ReleaseListStatuses(stats),
helm.ReleaseListNamespace(l.namespace),
) )
if err != nil { if err != nil {
@ -198,13 +202,14 @@ func (l *listCmd) statusCodes() []release.Status_Code {
func formatList(rels []*release.Release) string { func formatList(rels []*release.Release) string {
table := uitable.New() table := uitable.New()
table.MaxColWidth = 60 table.MaxColWidth = 60
table.AddRow("NAME", "REVISION", "UPDATED", "STATUS", "CHART") table.AddRow("NAME", "REVISION", "UPDATED", "STATUS", "CHART", "NAMESPACE")
for _, r := range rels { for _, r := range rels {
c := fmt.Sprintf("%s-%s", r.Chart.Metadata.Name, r.Chart.Metadata.Version) c := fmt.Sprintf("%s-%s", r.Chart.Metadata.Name, r.Chart.Metadata.Version)
t := timeconv.String(r.Info.LastDeployed) t := timeconv.String(r.Info.LastDeployed)
s := r.Info.Status.Code.String() s := r.Info.Status.Code.String()
v := r.Version v := r.Version
table.AddRow(r.Name, v, t, s, c) n := r.Namespace
table.AddRow(r.Name, v, t, s, c, n)
} }
return table.String() return table.String()
} }

@ -45,7 +45,7 @@ func TestListCmd(t *testing.T) {
resp: []*release.Release{ resp: []*release.Release{
releaseMock(&releaseOptions{name: "atlas"}), releaseMock(&releaseOptions{name: "atlas"}),
}, },
expected: "NAME \tREVISION\tUPDATED \tSTATUS \tCHART \natlas\t1 \t(.*)\tDEPLOYED\tfoo-0.1.0-beta.1\n", expected: "NAME \tREVISION\tUPDATED \tSTATUS \tCHART \tNAMESPACE\natlas\t1 \t(.*)\tDEPLOYED\tfoo-0.1.0-beta.1\tdefault \n",
}, },
{ {
name: "list, one deployed, one failed", name: "list, one deployed, one failed",
@ -87,6 +87,16 @@ func TestListCmd(t *testing.T) {
// See note on previous test. // See note on previous test.
expected: "thomas-guide\natlas-guide", expected: "thomas-guide\natlas-guide",
}, },
{
name: "namespace defined, multiple flags",
args: []string{"--all", "-q", "--namespace test123"},
resp: []*release.Release{
releaseMock(&releaseOptions{name: "thomas-guide", namespace: "test123"}),
releaseMock(&releaseOptions{name: "atlas-guide", namespace: "test321"}),
},
// See note on previous test.
expected: "thomas-guide",
},
} }
var buf bytes.Buffer var buf bytes.Buffer

@ -51,6 +51,7 @@ func TestListReleases_VerifyOptions(t *testing.T) {
rls.Status_DEPLOYED, rls.Status_DEPLOYED,
rls.Status_SUPERSEDED, rls.Status_SUPERSEDED,
} }
var namespace = "namespace"
// Expected ListReleasesRequest message // Expected ListReleasesRequest message
exp := &tpb.ListReleasesRequest{ exp := &tpb.ListReleasesRequest{
@ -60,6 +61,7 @@ func TestListReleases_VerifyOptions(t *testing.T) {
SortBy: tpb.ListSort_SortBy(sortBy), SortBy: tpb.ListSort_SortBy(sortBy),
SortOrder: tpb.ListSort_SortOrder(sortOrd), SortOrder: tpb.ListSort_SortOrder(sortOrd),
StatusCodes: codes, StatusCodes: codes,
Namespace: namespace,
} }
// Options used in ListReleases // Options used in ListReleases
@ -70,6 +72,7 @@ func TestListReleases_VerifyOptions(t *testing.T) {
ReleaseListOffset(offset), ReleaseListOffset(offset),
ReleaseListFilter(filter), ReleaseListFilter(filter),
ReleaseListStatuses(codes), ReleaseListStatuses(codes),
ReleaseListNamespace(namespace),
} }
// BeforeCall option to intercept helm client ListReleasesRequest // BeforeCall option to intercept helm client ListReleasesRequest

@ -134,6 +134,13 @@ func ReleaseListStatuses(statuses []release.Status_Code) ReleaseListOption {
} }
} }
// ReleaseListNamespace specifies the namespace to list releases from
func ReleaseListNamespace(namespace string) ReleaseListOption {
return func(opts *options) {
opts.listReq.Namespace = namespace
}
}
// InstallOption allows specifying various settings // InstallOption allows specifying various settings
// configurable by the helm client user for overriding // configurable by the helm client user for overriding
// the defaults used when running the `helm install` command. // the defaults used when running the `helm install` command.

@ -127,6 +127,8 @@ type ListReleasesRequest struct {
// SortOrder is the ordering directive used for sorting. // SortOrder is the ordering directive used for sorting.
SortOrder ListSort_SortOrder `protobuf:"varint,5,opt,name=sort_order,json=sortOrder,enum=hapi.services.tiller.ListSort_SortOrder" json:"sort_order,omitempty"` SortOrder ListSort_SortOrder `protobuf:"varint,5,opt,name=sort_order,json=sortOrder,enum=hapi.services.tiller.ListSort_SortOrder" json:"sort_order,omitempty"`
StatusCodes []hapi_release1.Status_Code `protobuf:"varint,6,rep,packed,name=status_codes,json=statusCodes,enum=hapi.release.Status_Code" json:"status_codes,omitempty"` StatusCodes []hapi_release1.Status_Code `protobuf:"varint,6,rep,packed,name=status_codes,json=statusCodes,enum=hapi.release.Status_Code" json:"status_codes,omitempty"`
// Namespace is the filter to select releases only from a specific namespace.
Namespace string `protobuf:"bytes,7,opt,name=namespace" json:"namespace,omitempty"`
} }
func (m *ListReleasesRequest) Reset() { *m = ListReleasesRequest{} } func (m *ListReleasesRequest) Reset() { *m = ListReleasesRequest{} }

@ -34,8 +34,9 @@ func TestConfigMapName(t *testing.T) {
func TestConfigMapGet(t *testing.T) { func TestConfigMapGet(t *testing.T) {
vers := int32(1) vers := int32(1)
name := "smug-pigeon" name := "smug-pigeon"
namespace := "default"
key := testKey(name, vers) key := testKey(name, vers)
rel := releaseStub(name, vers, rspb.Status_DEPLOYED) rel := releaseStub(name, vers, namespace, rspb.Status_DEPLOYED)
cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...)
@ -53,8 +54,9 @@ func TestConfigMapGet(t *testing.T) {
func TestUNcompressedConfigMapGet(t *testing.T) { func TestUNcompressedConfigMapGet(t *testing.T) {
vers := int32(1) vers := int32(1)
name := "smug-pigeon" name := "smug-pigeon"
namespace := "default"
key := testKey(name, vers) key := testKey(name, vers)
rel := releaseStub(name, vers, rspb.Status_DEPLOYED) rel := releaseStub(name, vers, namespace, rspb.Status_DEPLOYED)
// Create a test fixture which contains an uncompressed release // Create a test fixture which contains an uncompressed release
cfgmap, err := newConfigMapsObject(key, rel, nil) cfgmap, err := newConfigMapsObject(key, rel, nil)
@ -83,12 +85,12 @@ func TestUNcompressedConfigMapGet(t *testing.T) {
func TestConfigMapList(t *testing.T) { func TestConfigMapList(t *testing.T) {
cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{ cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{
releaseStub("key-1", 1, rspb.Status_DELETED), releaseStub("key-1", 1, "default", rspb.Status_DELETED),
releaseStub("key-2", 1, rspb.Status_DELETED), releaseStub("key-2", 1, "default", rspb.Status_DELETED),
releaseStub("key-3", 1, rspb.Status_DEPLOYED), releaseStub("key-3", 1, "default", rspb.Status_DEPLOYED),
releaseStub("key-4", 1, rspb.Status_DEPLOYED), releaseStub("key-4", 1, "default", rspb.Status_DEPLOYED),
releaseStub("key-5", 1, rspb.Status_SUPERSEDED), releaseStub("key-5", 1, "default", rspb.Status_SUPERSEDED),
releaseStub("key-6", 1, rspb.Status_SUPERSEDED), releaseStub("key-6", 1, "default", rspb.Status_SUPERSEDED),
}...) }...)
// list all deleted releases // list all deleted releases
@ -133,8 +135,9 @@ func TestConfigMapCreate(t *testing.T) {
vers := int32(1) vers := int32(1)
name := "smug-pigeon" name := "smug-pigeon"
namespace := "default"
key := testKey(name, vers) key := testKey(name, vers)
rel := releaseStub(name, vers, rspb.Status_DEPLOYED) rel := releaseStub(name, vers, namespace, rspb.Status_DEPLOYED)
// store the release in a configmap // store the release in a configmap
if err := cfgmaps.Create(key, rel); err != nil { if err := cfgmaps.Create(key, rel); err != nil {
@ -156,8 +159,9 @@ func TestConfigMapCreate(t *testing.T) {
func TestConfigMapUpdate(t *testing.T) { func TestConfigMapUpdate(t *testing.T) {
vers := int32(1) vers := int32(1)
name := "smug-pigeon" name := "smug-pigeon"
namespace := "default"
key := testKey(name, vers) key := testKey(name, vers)
rel := releaseStub(name, vers, rspb.Status_DEPLOYED) rel := releaseStub(name, vers, namespace, rspb.Status_DEPLOYED)
cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...)

@ -37,12 +37,12 @@ func TestMemoryCreate(t *testing.T) {
}{ }{
{ {
"create should success", "create should success",
releaseStub("rls-c", 1, rspb.Status_DEPLOYED), releaseStub("rls-c", 1, "default", rspb.Status_DEPLOYED),
false, false,
}, },
{ {
"create should fail (release already exists)", "create should fail (release already exists)",
releaseStub("rls-a", 1, rspb.Status_DEPLOYED), releaseStub("rls-a", 1, "default", rspb.Status_DEPLOYED),
true, true,
}, },
} }
@ -116,13 +116,13 @@ func TestMemoryUpdate(t *testing.T) {
{ {
"update release status", "update release status",
"rls-a.v4", "rls-a.v4",
releaseStub("rls-a", 4, rspb.Status_SUPERSEDED), releaseStub("rls-a", 4, "default", rspb.Status_SUPERSEDED),
false, false,
}, },
{ {
"update release does not exist", "update release does not exist",
"rls-z.v1", "rls-z.v1",
releaseStub("rls-z", 1, rspb.Status_DELETED), releaseStub("rls-z", 1, "default", rspb.Status_DELETED),
true, true,
}, },
} }

@ -27,11 +27,12 @@ import (
rspb "k8s.io/helm/pkg/proto/hapi/release" rspb "k8s.io/helm/pkg/proto/hapi/release"
) )
func releaseStub(name string, vers int32, code rspb.Status_Code) *rspb.Release { func releaseStub(name string, vers int32, namespace string, code rspb.Status_Code) *rspb.Release {
return &rspb.Release{ return &rspb.Release{
Name: name, Name: name,
Version: vers, Version: vers,
Info: &rspb.Info{Status: &rspb.Status{Code: code}}, Namespace: namespace,
Info: &rspb.Info{Status: &rspb.Status{Code: code}},
} }
} }
@ -42,15 +43,15 @@ func testKey(name string, vers int32) string {
func tsFixtureMemory(t *testing.T) *Memory { func tsFixtureMemory(t *testing.T) *Memory {
hs := []*rspb.Release{ hs := []*rspb.Release{
// rls-a // rls-a
releaseStub("rls-a", 4, rspb.Status_DEPLOYED), releaseStub("rls-a", 4, "default", rspb.Status_DEPLOYED),
releaseStub("rls-a", 1, rspb.Status_SUPERSEDED), releaseStub("rls-a", 1, "default", rspb.Status_SUPERSEDED),
releaseStub("rls-a", 3, rspb.Status_SUPERSEDED), releaseStub("rls-a", 3, "default", rspb.Status_SUPERSEDED),
releaseStub("rls-a", 2, rspb.Status_SUPERSEDED), releaseStub("rls-a", 2, "default", rspb.Status_SUPERSEDED),
// rls-b // rls-b
releaseStub("rls-b", 4, rspb.Status_DEPLOYED), releaseStub("rls-b", 4, "default", rspb.Status_DEPLOYED),
releaseStub("rls-b", 1, rspb.Status_SUPERSEDED), releaseStub("rls-b", 1, "default", rspb.Status_SUPERSEDED),
releaseStub("rls-b", 3, rspb.Status_SUPERSEDED), releaseStub("rls-b", 3, "default", rspb.Status_SUPERSEDED),
releaseStub("rls-b", 2, rspb.Status_SUPERSEDED), releaseStub("rls-b", 2, "default", rspb.Status_SUPERSEDED),
} }
mem := NewMemory() mem := NewMemory()

@ -24,8 +24,8 @@ import (
func TestRecordsAdd(t *testing.T) { func TestRecordsAdd(t *testing.T) {
rs := records([]*record{ rs := records([]*record{
newRecord("rls-a.v1", releaseStub("rls-a", 1, rspb.Status_SUPERSEDED)), newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.Status_SUPERSEDED)),
newRecord("rls-a.v2", releaseStub("rls-a", 2, rspb.Status_DEPLOYED)), newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.Status_DEPLOYED)),
}) })
var tests = []struct { var tests = []struct {
@ -38,13 +38,13 @@ func TestRecordsAdd(t *testing.T) {
"add valid key", "add valid key",
"rls-a.v3", "rls-a.v3",
false, false,
newRecord("rls-a.v3", releaseStub("rls-a", 3, rspb.Status_SUPERSEDED)), newRecord("rls-a.v3", releaseStub("rls-a", 3, "default", rspb.Status_SUPERSEDED)),
}, },
{ {
"add already existing key", "add already existing key",
"rls-a.v1", "rls-a.v1",
true, true,
newRecord("rls-a.v1", releaseStub("rls-a", 1, rspb.Status_DEPLOYED)), newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.Status_DEPLOYED)),
}, },
} }
@ -69,8 +69,8 @@ func TestRecordsRemove(t *testing.T) {
} }
rs := records([]*record{ rs := records([]*record{
newRecord("rls-a.v1", releaseStub("rls-a", 1, rspb.Status_SUPERSEDED)), newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.Status_SUPERSEDED)),
newRecord("rls-a.v2", releaseStub("rls-a", 2, rspb.Status_DEPLOYED)), newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.Status_DEPLOYED)),
}) })
for _, tt := range tests { for _, tt := range tests {

@ -112,6 +112,13 @@ func (s *ReleaseServer) ListReleases(req *services.ListReleasesRequest, stream s
return err return err
} }
if req.Namespace != "" {
rels, err = filterByNamespace(req.Namespace, rels)
if err != nil {
return err
}
}
if len(req.Filter) != 0 { if len(req.Filter) != 0 {
rels, err = filterReleases(req.Filter, rels) rels, err = filterReleases(req.Filter, rels)
if err != nil { if err != nil {
@ -178,6 +185,16 @@ func (s *ReleaseServer) ListReleases(req *services.ListReleasesRequest, stream s
return stream.Send(res) return stream.Send(res)
} }
func filterByNamespace(namespace string, rels []*release.Release) ([]*release.Release, error) {
matches := []*release.Release{}
for _, r := range rels {
if namespace == r.Namespace {
matches = append(matches, r)
}
}
return matches, nil
}
func filterReleases(filter string, rels []*release.Release) ([]*release.Release, error) { func filterReleases(filter string, rels []*release.Release) ([]*release.Release, error) {
preg, err := regexp.Compile(filter) preg, err := regexp.Compile(filter)
if err != nil { if err != nil {

@ -1411,6 +1411,48 @@ func TestListReleasesFilter(t *testing.T) {
} }
} }
func TestReleasesNamespace(t *testing.T) {
rs := rsFixture()
names := []string{
"axon",
"dendrite",
"neuron",
"ribosome",
}
namespaces := []string{
"default",
"test123",
"test123",
"cerebellum",
}
num := 4
for i := 0; i < num; i++ {
rel := releaseStub()
rel.Name = names[i]
rel.Namespace = namespaces[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,
Namespace: "test123",
}
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))
}
}
func mockEnvironment() *environment.Environment { func mockEnvironment() *environment.Environment {
e := environment.New() e := environment.New()
e.Releases = storage.Init(driver.NewMemory()) e.Releases = storage.Init(driver.NewMemory())

Loading…
Cancel
Save