From 2016109616d50057fb5cccce76cac105dc6ffc05 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Wed, 19 Dec 2018 10:01:58 -0700 Subject: [PATCH] feat: replace client/server internals with action package While we removed Tiller, we left the internal client/server architecture mostly intact. This replaces that architecture with the `pkg/action` package. This implements the action package for list, but nothing else. Signed-off-by: Matt Butcher --- cmd/helm/helm.go | 33 ++- cmd/helm/helm_test.go | 58 +++++- cmd/helm/list.go | 191 +++++++----------- cmd/helm/list_test.go | 115 ----------- cmd/helm/root.go | 7 +- cmd/helm/testdata/output/list-with-failed.txt | 2 +- .../list-with-mulitple-flags-deleting.txt | 2 +- .../list-with-mulitple-flags-namespaced.txt | 2 +- .../list-with-mulitple-flags-pending.txt | 2 +- .../output/list-with-mulitple-flags.txt | 2 +- .../output/list-with-mulitple-flags2.txt | 2 +- .../output/list-with-old-releases.txt | 2 +- .../testdata/output/list-with-pending.txt | 4 +- pkg/action/action.go | 16 ++ pkg/action/doc.go | 16 ++ pkg/action/list.go | 157 +++++++++----- pkg/action/list_test.go | 50 +++++ pkg/helm/client.go | 13 -- pkg/helm/fake.go | 5 - pkg/helm/helm_test.go | 56 ----- pkg/helm/interface.go | 1 - pkg/helm/option.go | 51 ----- pkg/tiller/release_list.go | 83 -------- pkg/tiller/release_list_test.go | 191 ------------------ 24 files changed, 354 insertions(+), 707 deletions(-) delete mode 100644 cmd/helm/list_test.go create mode 100644 pkg/action/list_test.go delete mode 100644 pkg/tiller/release_list.go delete mode 100644 pkg/tiller/release_list_test.go diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 02e5bd994..46568a9cb 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -26,9 +26,11 @@ import ( "k8s.io/cli-runtime/pkg/genericclioptions" _ "k8s.io/client-go/plugin/pkg/client/auth" + "k8s.io/helm/pkg/action" "k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/helm/environment" "k8s.io/helm/pkg/kube" + "k8s.io/helm/pkg/storage" "k8s.io/helm/pkg/storage/driver" ) @@ -50,7 +52,7 @@ func logf(format string, v ...interface{}) { } func main() { - cmd := newRootCmd(nil, os.Stdout, os.Args[1:]) + cmd := newRootCmd(nil, newActionConfig(false), os.Stdout, os.Args[1:]) if err := cmd.Execute(); err != nil { logf("%+v", err) os.Exit(1) @@ -102,13 +104,28 @@ func newActionConfig(allNamespaces bool) *action.Configuration { if !allNamespaces { namespace = getNamespace() } - // TODO add other backends - d := driver.NewSecrets(clientset.CoreV1().Secrets(namespace)) - d.Log = logf - - c := &action.Configuration{ - KubeClient: helm.KubeClient(kc), - Storage: storage + + var store *storage.Storage + switch os.Getenv("HELM_DRIVER") { + case "secret", "secrets", "": + d := driver.NewSecrets(clientset.CoreV1().Secrets(namespace)) + d.Log = logf + store = storage.Init(d) + case "configmap", "configmaps": + d := driver.NewConfigMaps(clientset.CoreV1().ConfigMaps(namespace)) + d.Log = logf + store = storage.Init(d) + case "memory": + d := driver.NewMemory() + store = storage.Init(d) + default: + // Not sure what to do here. + panic("Unknown driver in HELM_DRIVER: " + os.Getenv("HELM_DRIVER")) + } + + return &action.Configuration{ + KubeClient: kc, + Releases: store, } } diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index 60276e6a2..de77a5550 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -27,10 +27,13 @@ import ( "github.com/spf13/cobra" "k8s.io/helm/internal/test" + "k8s.io/helm/pkg/action" "k8s.io/helm/pkg/hapi/release" "k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/helm/helmpath" "k8s.io/helm/pkg/repo" + "k8s.io/helm/pkg/storage" + "k8s.io/helm/pkg/storage/driver" ) // base temp directory @@ -82,6 +85,51 @@ func runTestCmd(t *testing.T, tests []cmdTestCase) { } } +func runTestActionCmd(t *testing.T, tests []cmdTestCase) { + t.Helper() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer resetEnv()() + + store := storageFixture() + for _, rel := range tt.rels { + store.Create(rel) + } + _, out, err := executeActionCommandC(store, tt.cmd) + if (err != nil) != tt.wantError { + t.Errorf("expected error, got '%v'", err) + } + if tt.golden != "" { + test.AssertGoldenString(t, out, tt.golden) + } + }) + } +} + +func storageFixture() *storage.Storage { + return storage.Init(driver.NewMemory()) +} + +func executeActionCommandC(store *storage.Storage, cmd string) (*cobra.Command, string, error) { + args, err := shellwords.Parse(cmd) + if err != nil { + return nil, "", err + } + buf := new(bytes.Buffer) + + actionConfig := &action.Configuration{ + Releases: store, + } + + root := newRootCmd(nil, actionConfig, buf, args) + root.SetOutput(buf) + root.SetArgs(args) + + c, err := root.ExecuteC() + + return c, buf.String(), err +} + // cmdTestCase describes a test case that works with releases. type cmdTestCase struct { name string @@ -93,18 +141,26 @@ type cmdTestCase struct { testRunStatus map[string]release.TestRunStatus } +// deprecated: Switch to executeActionCommandC func executeCommand(c helm.Interface, cmd string) (string, error) { _, output, err := executeCommandC(c, cmd) return output, err } +// deprecated: Switch to executeActionCommandC func executeCommandC(client helm.Interface, cmd string) (*cobra.Command, string, error) { args, err := shellwords.Parse(cmd) if err != nil { return nil, "", err } buf := new(bytes.Buffer) - root := newRootCmd(client, buf, args) + + actionConfig := &action.Configuration{ + Releases: storage.Init(driver.NewMemory()), + //KubeClient: client. + } + + root := newRootCmd(client, actionConfig, buf, args) root.SetOutput(buf) root.SetArgs(args) diff --git a/cmd/helm/list.go b/cmd/helm/list.go index 72fbd8c98..ac137f282 100644 --- a/cmd/helm/list.go +++ b/cmd/helm/list.go @@ -26,10 +26,7 @@ import ( "k8s.io/helm/cmd/helm/require" "k8s.io/helm/pkg/action" - "k8s.io/helm/pkg/hapi" "k8s.io/helm/pkg/hapi/release" - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/storage" ) var listHelp = ` @@ -61,28 +58,28 @@ flag with the '--offset' flag allows you to page through results. type listOptions struct { // flags - all bool // --all - allNamespaces bool // --all-namespaces - byDate bool // --date - colWidth uint // --col-width - uninstalled bool // --uninstalled - uninstalling bool // --uninstalling - deployed bool // --deployed - failed bool // --failed - limit int // --max - offset string // --offset - pending bool // --pending - short bool // --short - sortDesc bool // --reverse - superseded bool // --superseded + all bool // --all + allNamespaces bool // --all-namespaces + byDate bool // --date + colWidth uint // --col-width + uninstalled bool // --uninstalled + uninstalling bool // --uninstalling + deployed bool // --deployed + failed bool // --failed + limit int // --max + offset int // --offset + pending bool // --pending + short bool // --short + sortDesc bool // --reverse + superseded bool // --superseded filter string - client helm.Interface + //client helm.Interface } -func newListCmd(client helm.Interface, out io.Writer) *cobra.Command { - o := &listOptions{client: client} +func newListCmd(actionConfig *action.Configuration, out io.Writer) *cobra.Command { + o := &listOptions{} cmd := &cobra.Command{ Use: "list [FILTER]", @@ -94,14 +91,40 @@ func newListCmd(client helm.Interface, out io.Writer) *cobra.Command { if len(args) > 0 { o.filter = strings.Join(args, " ") } - o.client = ensureHelmClient(o.client, o.allNamespaces) - - lister := action.NewList(action.Configuration{ - Releases: storage.Init(driver.ConfigMap), - KubeClient: newClient(o.all), - }) - //return o.run(out) - _, err := lister.Run() + + if o.allNamespaces { + actionConfig = newActionConfig(true) + } + + lister := action.NewList(actionConfig) + lister.All = o.limit == -1 + lister.AllNamespaces = o.allNamespaces + lister.Limit = o.limit + lister.Offset = o.offset + lister.Filter = o.filter + + // Set StateMask + lister.StateMask = o.setStateMask() + + // Set sorter + lister.Sort = action.ByNameAsc + if o.sortDesc { + lister.Sort = action.ByNameDesc + } + if o.byDate { + lister.Sort = action.ByDate + } + + results, err := lister.Run() + + if o.short { + for _, res := range results { + fmt.Fprintln(out, res.Name) + } + return err + } + + fmt.Fprintln(out, formatList(results, 90)) return err }, } @@ -111,7 +134,7 @@ func newListCmd(client helm.Interface, out io.Writer) *cobra.Command { f.BoolVarP(&o.byDate, "date", "d", false, "sort by release date") f.BoolVarP(&o.sortDesc, "reverse", "r", false, "reverse the sort order") f.IntVarP(&o.limit, "max", "m", 256, "maximum number of releases to fetch") - f.StringVarP(&o.offset, "offset", "o", "", "next release name in the list, used to offset from start value") + f.IntVarP(&o.offset, "offset", "o", 0, "next release name in the list, used to offset from start value") f.BoolVarP(&o.all, "all", "a", false, "show all releases, not just the ones marked deployed") f.BoolVar(&o.uninstalled, "uninstalled", false, "show uninstalled releases") f.BoolVar(&o.superseded, "superseded", false, "show superseded releases") @@ -125,111 +148,35 @@ func newListCmd(client helm.Interface, out io.Writer) *cobra.Command { return cmd } -func (o *listOptions) run(out io.Writer) error { - sortBy := hapi.SortByName - if o.byDate { - sortBy = hapi.SortByLastReleased - } - - sortOrder := hapi.SortAsc - if o.sortDesc { - sortOrder = hapi.SortDesc - } - - stats := o.statusCodes() - - res, err := o.client.ListReleases( - helm.ReleaseListLimit(o.limit), - helm.ReleaseListOffset(o.offset), - helm.ReleaseListFilter(o.filter), - helm.ReleaseListSort(sortBy), - helm.ReleaseListOrder(sortOrder), - helm.ReleaseListStatuses(stats), - ) - - if err != nil { - return err - } - - if len(res) == 0 { - return nil - } - - rels := filterList(res) - - if o.short { - for _, r := range rels { - fmt.Fprintln(out, r.Name) - } - return nil - } - fmt.Fprintln(out, formatList(rels, o.colWidth)) - return nil -} - -// filterList returns a list scrubbed of old releases. -func filterList(rels []*release.Release) []*release.Release { - idx := map[string]int{} - - for _, r := range rels { - name, version := r.Name, r.Version - if max, ok := idx[name]; ok { - // check if we have a greater version already - if max > version { - continue - } - } - idx[name] = version - } - - uniq := make([]*release.Release, 0, len(idx)) - for _, r := range rels { - if idx[r.Name] == r.Version { - uniq = append(uniq, r) - } - } - return uniq -} - -// statusCodes gets the list of status codes that are to be included in the results. -func (o *listOptions) statusCodes() []release.ReleaseStatus { +// setStateMask calculates the state mask based on parameters. +func (o *listOptions) setStateMask() action.ListStates { if o.all { - return []release.ReleaseStatus{ - release.StatusUnknown, - release.StatusDeployed, - release.StatusUninstalled, - release.StatusUninstalling, - release.StatusFailed, - release.StatusPendingInstall, - release.StatusPendingUpgrade, - release.StatusPendingRollback, - } + return action.ListAll } - status := []release.ReleaseStatus{} + + state := action.ListStates(0) if o.deployed { - status = append(status, release.StatusDeployed) + state |= action.ListDeployed } if o.uninstalled { - status = append(status, release.StatusUninstalled) + state |= action.ListUninstalled } if o.uninstalling { - status = append(status, release.StatusUninstalling) - } - if o.failed { - status = append(status, release.StatusFailed) - } - if o.superseded { - status = append(status, release.StatusSuperseded) + state |= action.ListUninstalling } if o.pending { - status = append(status, release.StatusPendingInstall, release.StatusPendingUpgrade, release.StatusPendingRollback) + state |= action.ListPendingInstall | action.ListPendingRollback | action.ListPendingUpgrade + } + if o.failed { + state |= action.ListFailed } - // Default case. - if len(status) == 0 { - status = append(status, release.StatusDeployed, release.StatusFailed) + // Apply a default + if state == 0 { + return action.ListDeployed | action.ListFailed } - return status + + return state } func formatList(rels []*release.Release, colWidth uint) string { diff --git a/cmd/helm/list_test.go b/cmd/helm/list_test.go deleted file mode 100644 index 3561e6b1c..000000000 --- a/cmd/helm/list_test.go +++ /dev/null @@ -1,115 +0,0 @@ -/* -Copyright The Helm Authors. - -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 ( - "testing" - - "k8s.io/helm/pkg/hapi/release" - "k8s.io/helm/pkg/helm" -) - -func TestListCmd(t *testing.T) { - tests := []cmdTestCase{{ - name: "with a release", - cmd: "list", - rels: []*release.Release{ - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide"}), - }, - golden: "output/list-with-release.txt", - }, { - name: "list", - cmd: "list", - rels: []*release.Release{ - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas"}), - }, - golden: "output/list.txt", - }, { - name: "list, one deployed, one failed", - cmd: "list -q", - rels: []*release.Release{ - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", Status: release.StatusFailed}), - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", Status: release.StatusDeployed}), - }, - golden: "output/list-with-failed.txt", - }, { - name: "with a release, multiple flags", - cmd: "list --uninstalled --deployed --failed -q", - rels: []*release.Release{ - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", Status: release.StatusUninstalled}), - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", Status: release.StatusDeployed}), - }, - // Note: We're really only testing that the flags parsed correctly. Which results are returned - // depends on the backend. And until pkg/helm is done, we can't mock this. - golden: "output/list-with-mulitple-flags.txt", - }, { - name: "with a release, multiple flags", - cmd: "list --all -q", - rels: []*release.Release{ - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", Status: release.StatusUninstalled}), - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", Status: release.StatusDeployed}), - }, - // See note on previous test. - golden: "output/list-with-mulitple-flags2.txt", - }, { - name: "with a release, multiple flags, deleting", - cmd: "list --all -q", - rels: []*release.Release{ - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", Status: release.StatusUninstalling}), - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", Status: release.StatusDeployed}), - }, - // See note on previous test. - golden: "output/list-with-mulitple-flags-deleting.txt", - }, { - name: "namespace defined, multiple flags", - cmd: "list --all -q --namespace test123", - rels: []*release.Release{ - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", Namespace: "test123"}), - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", Namespace: "test321"}), - }, - // See note on previous test. - golden: "output/list-with-mulitple-flags-namespaced.txt", - }, { - name: "with a pending release, multiple flags", - cmd: "list --all -q", - rels: []*release.Release{ - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", Status: release.StatusPendingInstall}), - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", Status: release.StatusDeployed}), - }, - golden: "output/list-with-mulitple-flags-pending.txt", - }, { - name: "with a pending release, pending flag", - cmd: "list --pending -q", - rels: []*release.Release{ - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", Status: release.StatusPendingInstall}), - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "wild-idea", Status: release.StatusPendingUpgrade}), - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "crazy-maps", Status: release.StatusPendingRollback}), - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", Status: release.StatusDeployed}), - }, - golden: "output/list-with-pending.txt", - }, { - name: "with old releases", - cmd: "list", - rels: []*release.Release{ - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide"}), - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", Status: release.StatusFailed}), - }, - golden: "output/list-with-old-releases.txt", - }} - - runTestCmd(t, tests) -} diff --git a/cmd/helm/root.go b/cmd/helm/root.go index 1aef4b405..b64662435 100644 --- a/cmd/helm/root.go +++ b/cmd/helm/root.go @@ -22,6 +22,7 @@ import ( "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/require" + "k8s.io/helm/pkg/action" "k8s.io/helm/pkg/helm" ) @@ -42,11 +43,13 @@ Common actions from this point include: Environment: $HELM_HOME set an alternative location for Helm files. By default, these are stored in ~/.helm + $HELM_DRIVER set the backend storage driver. Values are: configmap, secret, memory $HELM_NO_PLUGINS disable plugins. Set HELM_NO_PLUGINS=1 to disable plugins. $KUBECONFIG set an alternative Kubernetes configuration file (default "~/.kube/config") ` -func newRootCmd(c helm.Interface, out io.Writer, args []string) *cobra.Command { +// TODO: 'c helm.Interface' is deprecated in favor of actionConfig +func newRootCmd(c helm.Interface, actionConfig *action.Configuration, out io.Writer, args []string) *cobra.Command { cmd := &cobra.Command{ Use: "helm", Short: "The Helm package manager for Kubernetes.", @@ -74,7 +77,7 @@ func newRootCmd(c helm.Interface, out io.Writer, args []string) *cobra.Command { newGetCmd(c, out), newHistoryCmd(c, out), newInstallCmd(c, out), - newListCmd(c, out), + newListCmd(actionConfig, out), newReleaseTestCmd(c, out), newRollbackCmd(c, out), newStatusCmd(c, out), diff --git a/cmd/helm/testdata/output/list-with-failed.txt b/cmd/helm/testdata/output/list-with-failed.txt index 289dcac23..6cacd15a0 100644 --- a/cmd/helm/testdata/output/list-with-failed.txt +++ b/cmd/helm/testdata/output/list-with-failed.txt @@ -1,2 +1,2 @@ -thomas-guide atlas-guide +thomas-guide diff --git a/cmd/helm/testdata/output/list-with-mulitple-flags-deleting.txt b/cmd/helm/testdata/output/list-with-mulitple-flags-deleting.txt index 289dcac23..6cacd15a0 100644 --- a/cmd/helm/testdata/output/list-with-mulitple-flags-deleting.txt +++ b/cmd/helm/testdata/output/list-with-mulitple-flags-deleting.txt @@ -1,2 +1,2 @@ -thomas-guide atlas-guide +thomas-guide diff --git a/cmd/helm/testdata/output/list-with-mulitple-flags-namespaced.txt b/cmd/helm/testdata/output/list-with-mulitple-flags-namespaced.txt index 289dcac23..6cacd15a0 100644 --- a/cmd/helm/testdata/output/list-with-mulitple-flags-namespaced.txt +++ b/cmd/helm/testdata/output/list-with-mulitple-flags-namespaced.txt @@ -1,2 +1,2 @@ -thomas-guide atlas-guide +thomas-guide diff --git a/cmd/helm/testdata/output/list-with-mulitple-flags-pending.txt b/cmd/helm/testdata/output/list-with-mulitple-flags-pending.txt index 289dcac23..6cacd15a0 100644 --- a/cmd/helm/testdata/output/list-with-mulitple-flags-pending.txt +++ b/cmd/helm/testdata/output/list-with-mulitple-flags-pending.txt @@ -1,2 +1,2 @@ -thomas-guide atlas-guide +thomas-guide diff --git a/cmd/helm/testdata/output/list-with-mulitple-flags.txt b/cmd/helm/testdata/output/list-with-mulitple-flags.txt index 289dcac23..6cacd15a0 100644 --- a/cmd/helm/testdata/output/list-with-mulitple-flags.txt +++ b/cmd/helm/testdata/output/list-with-mulitple-flags.txt @@ -1,2 +1,2 @@ -thomas-guide atlas-guide +thomas-guide diff --git a/cmd/helm/testdata/output/list-with-mulitple-flags2.txt b/cmd/helm/testdata/output/list-with-mulitple-flags2.txt index 289dcac23..6cacd15a0 100644 --- a/cmd/helm/testdata/output/list-with-mulitple-flags2.txt +++ b/cmd/helm/testdata/output/list-with-mulitple-flags2.txt @@ -1,2 +1,2 @@ -thomas-guide atlas-guide +thomas-guide diff --git a/cmd/helm/testdata/output/list-with-old-releases.txt b/cmd/helm/testdata/output/list-with-old-releases.txt index 76a90e3b2..0a0d11d3f 100644 --- a/cmd/helm/testdata/output/list-with-old-releases.txt +++ b/cmd/helm/testdata/output/list-with-old-releases.txt @@ -1,3 +1,3 @@ NAME REVISION UPDATED STATUS CHART NAMESPACE thomas-guide 1 1977-09-02 22:04:05 +0000 UTC deployed foo-0.1.0-beta.1 default -thomas-guide 1 1977-09-02 22:04:05 +0000 UTC failed foo-0.1.0-beta.1 default +thomas-guide 2 1977-09-02 22:04:05 +0000 UTC failed foo-0.1.0-beta.1 default diff --git a/cmd/helm/testdata/output/list-with-pending.txt b/cmd/helm/testdata/output/list-with-pending.txt index 9a5953f48..c90e0e93d 100644 --- a/cmd/helm/testdata/output/list-with-pending.txt +++ b/cmd/helm/testdata/output/list-with-pending.txt @@ -1,4 +1,4 @@ +atlas-guide +crazy-maps thomas-guide wild-idea -crazy-maps -atlas-guide diff --git a/pkg/action/action.go b/pkg/action/action.go index 11f3ef86c..3ba941182 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -1,3 +1,19 @@ +/* +Copyright The Helm Authors. + +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 action import ( diff --git a/pkg/action/doc.go b/pkg/action/doc.go index 7c7b9da23..3c91bd618 100644 --- a/pkg/action/doc.go +++ b/pkg/action/doc.go @@ -1,3 +1,19 @@ +/* +Copyright The Helm Authors. + +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 action contains the logic for each action that Helm can perform. // // This is a library for calling top-level Helm actions like 'install', diff --git a/pkg/action/list.go b/pkg/action/list.go index 11f3e5306..51cf15cf7 100644 --- a/pkg/action/list.go +++ b/pkg/action/list.go @@ -1,14 +1,32 @@ +/* +Copyright The Helm Authors. + +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 action import ( - "errors" "regexp" - "sort" "k8s.io/helm/pkg/hapi/release" "k8s.io/helm/pkg/releaseutil" ) +// ListStates represents zero or more status codes that a list item may have set +// +// Because this is used as a bitmask filter, more than one one bit can be flipped +// in the ListStates. type ListStates uint const ( @@ -32,18 +50,48 @@ const ( ListUnknown ) -type ByDate struct{} -type ByNameAsc struct{} -type ByNameDesc struct{} - -// ReleaseSorter is a sorter for releases -type ReleaseSorter interface { - SetList([]*release.Release) - Len() int - Less(i, j int) bool - Swap(i, j int) +// FromName takes a state name and returns a ListStates representation. +// +// Currently, there are only names for individual flipped bits, so the returned +// ListStates will only match one of the constants. However, it is possible that +// this behavior could change in the future. +func (s ListStates) FromName(str string) ListStates { + switch str { + case "deployed": + return ListDeployed + case "uninstalled": + return ListUninstalled + case "superseded": + return ListSuperseded + case "failed": + return ListFailed + case "uninstalling": + return ListUninstalling + case "pending-install": + return ListPendingInstall + case "pending-upgrade": + return ListPendingUpgrade + case "pending-rollback": + return ListPendingRollback + } + return ListUnknown } +// ListAll is a convenience for enabling all list filters +const ListAll = ListDeployed | ListUninstalled | ListUninstalling | ListPendingInstall | ListPendingRollback | ListPendingUpgrade | ListSuperseded | ListFailed + +// Sorter is a top-level sort +type Sorter uint + +const ( + // ByDate sorts by date + ByDate Sorter = iota + // ByNameAsc sorts by ascending lexicographic order + ByNameAsc + // ByNameDesc sorts by descending lexicographic order + ByNameDesc +) + // List is the action for listing releases. // // It provides, for example, the implementation of 'helm list'. @@ -55,7 +103,7 @@ type List struct { // Sort indicates the sort to use // // see pkg/releaseutil for several useful sorters - Sort ReleaseSorter + Sort Sorter // StateMask accepts a bitmask of states for items to show. // The default is ListDeployed StateMask ListStates @@ -73,56 +121,65 @@ type List struct { func NewList(cfg *Configuration) *List { return &List{ StateMask: ListDeployed | ListFailed, + cfg: cfg, } } +// Run executes the list command, returning a set of matches. func (a *List) Run() ([]*release.Release, error) { - return []*release.Release{}, errors.New("not implemented") -} - -func (a *List) listReleases() ([]*release.Release, error) { - rels, err := a.cfg.Releases.ListReleases() - if err != nil { - return rels, err - } - - // TODO: add the rest of the filters here - // Run filter - rels, err = a.filterReleases(rels) - if err != nil { - return rels, err + offset := 0 + limit := 0 + var filter *regexp.Regexp + if a.Filter != "" { + var err error + filter, err = regexp.Compile(a.Filter) + if err != nil { + return nil, err + } } - // Run sort - rels = a.sort(rels) + results, err := a.cfg.Releases.List(func(rel *release.Release) bool { + // If we haven't reached offset, skip because there + // is nothing to add. + if offset < a.Offset { + offset++ + return false + } + // If over limit, return. This is rather inefficient + if limit >= a.Limit { + return false + } - return rels, nil -} + // Skip anything that the mask doesn't cover + currentStatus := a.StateMask.FromName(rel.Info.Status.String()) + if a.StateMask¤tStatus == 0 { + return false + } -func (a *List) filterReleases(rels []*release.Release) ([]*release.Release, error) { - if a.Filter == "" { - return rels, nil - } - preg, err := regexp.Compile(a.Filter) - if err != nil { - return rels, err - } - matches := []*release.Release{} - for _, r := range rels { - if preg.MatchString(r.Name) { - matches = append(matches, r) + // Skip anything that doesn't match the filter. + if filter != nil && !filter.MatchString(rel.Name) { + return false } + + limit++ + return true + }) + if results != nil { + a.sort(results) } - return matches, nil + + return results, err } -func (a *List) sort(rels []*release.Release) []*release.Release { - if a.Sort == nil { +// sort is an in-place sort where order is based on the value of a.Sort +func (a *List) sort(rels []*release.Release) { + switch a.Sort { + case ByDate: + releaseutil.SortByDate(rels) + case ByNameDesc: + releaseutil.Reverse(rels, releaseutil.SortByName) + default: releaseutil.SortByName(rels) - return rels } - a.Sort.SetList(rels) - sort.Sort(a.Sort) - return rels } diff --git a/pkg/action/list_test.go b/pkg/action/list_test.go new file mode 100644 index 000000000..6e0aff2d3 --- /dev/null +++ b/pkg/action/list_test.go @@ -0,0 +1,50 @@ +/* +Copyright The Helm Authors. + +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 action + +import "testing" + +func TestListStates(t *testing.T) { + for input, expect := range map[string]ListStates{ + "deployed": ListDeployed, + "uninstalled": ListUninstalled, + "uninstalling": ListUninstalling, + "superseded": ListSuperseded, + "failed": ListFailed, + "pending-install": ListPendingInstall, + "pending-rollback": ListPendingRollback, + "pending-upgrade": ListPendingUpgrade, + "unknown": ListUnknown, + "totally made up key": ListUnknown, + } { + if expect != expect.FromName(input) { + t.Errorf("Expected %d for %s", expect, input) + } + // This is a cheap way to verify that ListAll actually allows everything but Unknown + if got := expect.FromName(input); got != ListUnknown && got&ListAll == 0 { + t.Errorf("Expected %s to match the ListAll filter", input) + } + } + + filter := ListDeployed | ListPendingRollback + if status := filter.FromName("deployed"); filter&status == 0 { + t.Errorf("Expected %d to match mask %d", status, filter) + } + if status := filter.FromName("failed"); filter&status != 0 { + t.Errorf("Expected %d to fail to match mask %d", status, filter) + } +} diff --git a/pkg/helm/client.go b/pkg/helm/client.go index aa4e1fab0..4ed3da2d0 100644 --- a/pkg/helm/client.go +++ b/pkg/helm/client.go @@ -52,19 +52,6 @@ func (c *Client) Option(opts ...Option) *Client { return c } -// ListReleases lists the current releases. -func (c *Client) ListReleases(opts ...ReleaseListOption) ([]*release.Release, error) { - reqOpts := c.opts - for _, opt := range opts { - opt(&reqOpts) - } - req := &reqOpts.listReq - if err := reqOpts.runBefore(req); err != nil { - return nil, err - } - return c.tiller.ListReleases(req) -} - // InstallRelease loads a chart from chstr, installs it, and returns the release response. func (c *Client) InstallRelease(chstr, ns string, opts ...InstallOption) (*release.Release, error) { // load the chart to install diff --git a/pkg/helm/fake.go b/pkg/helm/fake.go index 80b850a6b..7bc9eae60 100644 --- a/pkg/helm/fake.go +++ b/pkg/helm/fake.go @@ -46,11 +46,6 @@ func (c *FakeClient) Option(opts ...Option) Interface { var _ Interface = &FakeClient{} var _ Interface = (*FakeClient)(nil) -// ListReleases lists the current releases -func (c *FakeClient) ListReleases(opts ...ReleaseListOption) ([]*release.Release, error) { - return c.Rels, nil -} - // InstallRelease creates a new release and returns the release func (c *FakeClient) InstallRelease(chStr, ns string, opts ...InstallOption) (*release.Release, error) { chart := &chart.Chart{} diff --git a/pkg/helm/helm_test.go b/pkg/helm/helm_test.go index 50cd821ad..7bbf9701a 100644 --- a/pkg/helm/helm_test.go +++ b/pkg/helm/helm_test.go @@ -26,7 +26,6 @@ import ( cpb "k8s.io/helm/pkg/chart" "k8s.io/helm/pkg/chart/loader" "k8s.io/helm/pkg/hapi" - rls "k8s.io/helm/pkg/hapi/release" ) // Path to example charts relative to pkg/helm. @@ -36,61 +35,6 @@ const chartsDir = "../../docs/examples/" var errSkip = errors.New("test: skip") // Verify each ReleaseListOption is applied to a ListReleasesRequest correctly. -func TestListReleases_VerifyOptions(t *testing.T) { - // Options testdata - var limit = 2 - var offset = "offset" - var filter = "filter" - var sortBy = hapi.SortByLastReleased - var sortOrd = hapi.SortAsc - var codes = []rls.ReleaseStatus{ - rls.StatusFailed, - rls.StatusUninstalled, - rls.StatusDeployed, - rls.StatusSuperseded, - } - - // Expected ListReleasesRequest message - exp := &hapi.ListReleasesRequest{ - Limit: int64(limit), - Offset: offset, - Filter: filter, - SortBy: sortBy, - SortOrder: sortOrd, - StatusCodes: codes, - } - - // Options used in ListReleases - ops := []ReleaseListOption{ - ReleaseListSort(sortBy), - ReleaseListOrder(sortOrd), - ReleaseListLimit(limit), - ReleaseListOffset(offset), - ReleaseListFilter(filter), - ReleaseListStatuses(codes), - } - - // BeforeCall option to intercept Helm client ListReleasesRequest - b4c := BeforeCall(func(msg interface{}) error { - switch act := msg.(type) { - case *hapi.ListReleasesRequest: - t.Logf("ListReleasesRequest: %#+v\n", act) - assert(t, exp, act) - default: - t.Fatalf("expected message of type ListReleasesRequest, got %T\n", act) - } - return errSkip - }) - - client := NewClient(b4c) - - if _, err := client.ListReleases(ops...); err != errSkip { - t.Fatalf("did not expect error but got (%v)\n``", err) - } - - // ensure options for call are not saved to client - assert(t, "", client.opts.listReq.Filter) -} // Verify each InstallOption is applied to an InstallReleaseRequest correctly. func TestInstallRelease_VerifyOptions(t *testing.T) { diff --git a/pkg/helm/interface.go b/pkg/helm/interface.go index a66eefb23..96db762b0 100644 --- a/pkg/helm/interface.go +++ b/pkg/helm/interface.go @@ -24,7 +24,6 @@ import ( // Interface for helm client for mocking in tests type Interface interface { - ListReleases(opts ...ReleaseListOption) ([]*release.Release, error) InstallRelease(chStr, namespace string, opts ...InstallOption) (*release.Release, error) InstallReleaseFromChart(chart *chart.Chart, namespace string, opts ...InstallOption) (*release.Release, error) UninstallRelease(rlsName string, opts ...UninstallOption) (*hapi.UninstallReleaseResponse, error) diff --git a/pkg/helm/option.go b/pkg/helm/option.go index 83eaba92f..8c8dbbd25 100644 --- a/pkg/helm/option.go +++ b/pkg/helm/option.go @@ -20,7 +20,6 @@ import ( "k8s.io/client-go/discovery" "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/hapi/release" "k8s.io/helm/pkg/storage/driver" "k8s.io/helm/pkg/tiller/environment" ) @@ -87,56 +86,6 @@ func BeforeCall(fn func(interface{}) error) Option { } } -// ReleaseListOption allows specifying various settings -// configurable by the helm client user for overriding -// the defaults used when running the `helm list` command. -type ReleaseListOption func(*options) - -// ReleaseListOffset specifies the offset into a list of releases. -func ReleaseListOffset(offset string) ReleaseListOption { - return func(opts *options) { - opts.listReq.Offset = offset - } -} - -// ReleaseListFilter specifies a filter to apply a list of releases. -func ReleaseListFilter(filter string) ReleaseListOption { - return func(opts *options) { - opts.listReq.Filter = filter - } -} - -// ReleaseListLimit set an upper bound on the number of releases returned. -func ReleaseListLimit(limit int) ReleaseListOption { - return func(opts *options) { - opts.listReq.Limit = int64(limit) - } -} - -// ReleaseListOrder specifies how to order a list of releases. -func ReleaseListOrder(order hapi.SortOrder) ReleaseListOption { - return func(opts *options) { - opts.listReq.SortOrder = order - } -} - -// ReleaseListSort specifies how to sort a release list. -func ReleaseListSort(sort hapi.SortBy) ReleaseListOption { - return func(opts *options) { - opts.listReq.SortBy = sort - } -} - -// ReleaseListStatuses specifies which status codes should be returned. -func ReleaseListStatuses(statuses []release.ReleaseStatus) ReleaseListOption { - return func(opts *options) { - if len(statuses) == 0 { - statuses = []release.ReleaseStatus{release.StatusDeployed} - } - opts.listReq.StatusCodes = statuses - } -} - // InstallOption allows specifying various settings // configurable by the helm client user for overriding // the defaults used when running the `helm install` command. diff --git a/pkg/tiller/release_list.go b/pkg/tiller/release_list.go deleted file mode 100644 index 222369f31..000000000 --- a/pkg/tiller/release_list.go +++ /dev/null @@ -1,83 +0,0 @@ -/* -Copyright The Helm Authors. - -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 tiller - -import ( - "regexp" - - "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/hapi/release" - relutil "k8s.io/helm/pkg/releaseutil" -) - -// ListReleases lists the releases found by the server. -func (s *ReleaseServer) ListReleases(req *hapi.ListReleasesRequest) ([]*release.Release, error) { - if len(req.StatusCodes) == 0 { - req.StatusCodes = []release.ReleaseStatus{release.StatusDeployed} - } - - rels, err := s.Releases.ListFilterAll(func(r *release.Release) bool { - for _, sc := range req.StatusCodes { - if sc == r.Info.Status { - return true - } - } - return false - }) - if err != nil { - return nil, err - } - - if len(req.Filter) != 0 { - rels, err = filterReleases(req.Filter, rels) - if err != nil { - return nil, err - } - } - - switch req.SortBy { - case hapi.SortByName: - relutil.SortByName(rels) - case hapi.SortByLastReleased: - relutil.SortByDate(rels) - } - - if req.SortOrder == hapi.SortDesc { - ll := len(rels) - rr := make([]*release.Release, ll) - for i, item := range rels { - rr[ll-i-1] = item - } - rels = rr - } - - return rels, 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 -} diff --git a/pkg/tiller/release_list_test.go b/pkg/tiller/release_list_test.go deleted file mode 100644 index abce1f569..000000000 --- a/pkg/tiller/release_list_test.go +++ /dev/null @@ -1,191 +0,0 @@ -/* -Copyright The Helm Authors. - -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 tiller - -import ( - "fmt" - "testing" - - "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/hapi/release" -) - -func TestListReleases(t *testing.T) { - rs := rsFixture(t) - num := 7 - for i := 0; i < num; i++ { - rel := releaseStub() - rel.Name = fmt.Sprintf("rel-%d", i) - if err := rs.Releases.Create(rel); err != nil { - t.Fatalf("Could not store mock release: %s", err) - } - } - - rels, err := rs.ListReleases(&hapi.ListReleasesRequest{}) - if err != nil { - t.Fatalf("Failed listing: %s", err) - } - - if len(rels) != num { - t.Errorf("Expected %d releases, got %d", num, len(rels)) - } -} - -func TestListReleasesByStatus(t *testing.T) { - rs := rsFixture(t) - stubs := []*release.Release{ - namedReleaseStub("kamal", release.StatusDeployed), - namedReleaseStub("astrolabe", release.StatusUninstalled), - namedReleaseStub("octant", release.StatusFailed), - namedReleaseStub("sextant", release.StatusUnknown), - } - for _, stub := range stubs { - if err := rs.Releases.Create(stub); err != nil { - t.Fatalf("Could not create stub: %s", err) - } - } - - tests := []struct { - statusCodes []release.ReleaseStatus - names []string - }{ - { - names: []string{"kamal"}, - statusCodes: []release.ReleaseStatus{release.StatusDeployed}, - }, - { - names: []string{"astrolabe"}, - statusCodes: []release.ReleaseStatus{release.StatusUninstalled}, - }, - { - names: []string{"kamal", "octant"}, - statusCodes: []release.ReleaseStatus{release.StatusDeployed, release.StatusFailed}, - }, - { - names: []string{"kamal", "astrolabe", "octant", "sextant"}, - statusCodes: []release.ReleaseStatus{ - release.StatusDeployed, - release.StatusUninstalled, - release.StatusFailed, - release.StatusUnknown, - }, - }, - } - - for i, tt := range tests { - rels, err := rs.ListReleases(&hapi.ListReleasesRequest{StatusCodes: tt.statusCodes, Offset: "", Limit: 64}) - if err != nil { - t.Fatalf("Failed listing %d: %s", i, err) - } - - if len(tt.names) != len(rels) { - t.Fatalf("Expected %d releases, got %d", len(tt.names), len(rels)) - } - - for _, name := range tt.names { - found := false - for _, rel := range rels { - if rel.Name == name { - found = true - } - } - if !found { - t.Errorf("%d: Did not find name %q", i, name) - } - } - } -} - -func TestListReleasesSort(t *testing.T) { - rs := rsFixture(t) - - // Put them in by reverse order so that the mock doesn't "accidentally" - // sort. - num := 7 - for i := num; i > 0; i-- { - rel := releaseStub() - rel.Name = fmt.Sprintf("rel-%d", i) - if err := rs.Releases.Create(rel); err != nil { - t.Fatalf("Could not store mock release: %s", err) - } - } - - limit := 6 - req := &hapi.ListReleasesRequest{ - Offset: "", - Limit: int64(limit), - SortBy: hapi.SortByName, - } - rels, err := rs.ListReleases(req) - if err != nil { - t.Fatalf("Failed listing: %s", err) - } - - // if len(rels) != limit { - // t.Errorf("Expected %d releases, got %d", limit, len(rels)) - // } - - for i := 0; i < limit; i++ { - n := fmt.Sprintf("rel-%d", i+1) - if rels[i].Name != n { - t.Errorf("Expected %q, got %q", n, rels[i].Name) - } - } -} - -func TestListReleasesFilter(t *testing.T) { - rs := rsFixture(t) - names := []string{ - "axon", - "dendrite", - "neuron", - "neuroglia", - "synapse", - "nucleus", - "organelles", - } - num := 7 - for i := 0; i < num; i++ { - rel := releaseStub() - rel.Name = names[i] - if err := rs.Releases.Create(rel); err != nil { - t.Fatalf("Could not store mock release: %s", err) - } - } - - req := &hapi.ListReleasesRequest{ - Offset: "", - Limit: 64, - Filter: "neuro[a-z]+", - SortBy: hapi.SortByName, - } - rels, err := rs.ListReleases(req) - if err != nil { - t.Fatalf("Failed listing: %s", err) - } - - if len(rels) != 2 { - t.Errorf("Expected 2 releases, got %d", len(rels)) - } - - if rels[0].Name != "neuroglia" { - t.Errorf("Unexpected sort order: %v.", rels) - } - if rels[1].Name != "neuron" { - t.Errorf("Unexpected sort order: %v.", rels) - } -}