diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 2f394968f..5ef2dc19b 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -190,6 +190,7 @@ func newRootCmd(args []string) *cobra.Command { newRollbackCmd(nil, out), newStatusCmd(nil, out), newUpgradeCmd(nil, out), + newPruneCmd(nil, out), newReleaseTestCmd(nil, out), newResetCmd(nil, out), diff --git a/cmd/helm/prune.go b/cmd/helm/prune.go new file mode 100644 index 000000000..354075b3b --- /dev/null +++ b/cmd/helm/prune.go @@ -0,0 +1,86 @@ +/* +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 ( + "fmt" + "io" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" +) + +const pruneDesc = ` +This command purges all deleted releases, cleaning them from removing them permanently so they can't be rolled back. +` + +type pruneCmd struct { + disableHooks bool + timeout int64 + + out io.Writer + client helm.Interface +} + +func newPruneCmd(c helm.Interface, out io.Writer) *cobra.Command { + prune := &pruneCmd{ + out: out, + client: c, + } + + cmd := &cobra.Command{ + Use: "prune", + Short: "Purges deleted releases", + Long: pruneDesc, + PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, + RunE: func(cmd *cobra.Command, args []string) error { + prune.client = ensureHelmClient(prune.client) + + if err := prune.run(); err != nil { + return err + } + + fmt.Fprintf(out, "Releases pruned\n") + return nil + }, + } + + f := cmd.Flags() + settings.AddFlagsTLS(f) + f.BoolVar(&prune.disableHooks, "no-hooks", false, "Prevent hooks from running during deletion") + f.Int64Var(&prune.timeout, "timeout", 300, "Time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") + + // set defaults from environment + settings.InitTLS(f) + + return cmd +} + +func (d *pruneCmd) run() error { + opts := []helm.DeleteOption{ + helm.DeleteDisableHooks(d.disableHooks), + helm.DeleteTimeout(d.timeout), + } + _, errs := d.client.PruneReleases(opts...) + for _, err := range errs { + if err != nil { + return prettyError(err) + } + } + return nil +} diff --git a/pkg/helm/client.go b/pkg/helm/client.go index fa867c2d3..0cfb913de 100644 --- a/pkg/helm/client.go +++ b/pkg/helm/client.go @@ -29,6 +29,7 @@ import ( healthpb "google.golang.org/grpc/health/grpc_health_v1" "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/proto/hapi/chart" + "k8s.io/helm/pkg/proto/hapi/release" rls "k8s.io/helm/pkg/proto/hapi/services" ) @@ -148,6 +149,25 @@ func (h *Client) DeleteRelease(rlsName string, opts ...DeleteOption) (*rls.Unins return h.delete(ctx, req) } +func (h *Client) PruneReleases(opts ...DeleteOption) ([]*rls.UninstallReleaseResponse, []error) { + res, err := h.ListReleases(ReleaseListStatuses([]release.Status_Code{release.Status_DELETED})) + if err != nil { + return nil, []error{err} + } + if res != nil && res.Releases != nil { + resps := make([]*rls.UninstallReleaseResponse, len(res.Releases)) + errs := make([]error, len(res.Releases)) + for _, rel := range res.Releases { + res, err := h.DeleteRelease(rel.Name, append(opts, DeletePurge(true))...) + resps = append(resps, res) + errs = append(errs, err) + } + return resps, errs + } else { + return nil, []error{fmt.Errorf("No deleted releases to prune")} + } +} + // UpdateRelease loads a chart from chstr and updates a release to a new/different chart. func (h *Client) UpdateRelease(rlsName string, chstr string, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) { // load the chart to update diff --git a/pkg/helm/fake.go b/pkg/helm/fake.go index c8ce91f44..889e551c1 100644 --- a/pkg/helm/fake.go +++ b/pkg/helm/fake.go @@ -19,6 +19,7 @@ package helm // import "k8s.io/helm/pkg/helm" import ( "bytes" "errors" + "fmt" "math/rand" "strings" "sync" @@ -141,6 +142,29 @@ func (c *FakeClient) DeleteRelease(rlsName string, opts ...DeleteOption) (*rls.U return nil, storageerrors.ErrReleaseNotFound(rlsName) } +func (c *FakeClient) PruneReleases(opts ...DeleteOption) ([]*rls.UninstallReleaseResponse, []error) { + relsToDelete := make([]*release.Release, 0) + for _, rel := range c.Rels { + if rel.Info.GetStatus().Code == release.Status_DELETED { + relsToDelete = append(relsToDelete, rel) + } + } + res := &rls.ListReleasesResponse{Releases: relsToDelete} + + if res != nil && res.Releases != nil { + resps := make([]*rls.UninstallReleaseResponse, len(res.Releases)) + errs := make([]error, len(res.Releases)) + for _, rel := range res.Releases { + res, err := c.DeleteRelease(rel.Name, append(opts, DeletePurge(true))...) + resps = append(resps, res) + errs = append(errs, err) + } + return resps, errs + } else { + return nil, []error{fmt.Errorf("No deleted releases to prune")} + } +} + // GetVersion returns a fake version func (c *FakeClient) GetVersion(opts ...VersionOption) (*rls.GetVersionResponse, error) { return &rls.GetVersionResponse{ diff --git a/pkg/helm/interface.go b/pkg/helm/interface.go index d09b6cf8f..c7e4e19d1 100644 --- a/pkg/helm/interface.go +++ b/pkg/helm/interface.go @@ -27,6 +27,7 @@ type Interface interface { InstallRelease(chStr, namespace string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) InstallReleaseFromChart(chart *chart.Chart, namespace string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) DeleteRelease(rlsName string, opts ...DeleteOption) (*rls.UninstallReleaseResponse, error) + PruneReleases(opts ...DeleteOption) ([]*rls.UninstallReleaseResponse, []error) ReleaseStatus(rlsName string, opts ...StatusOption) (*rls.GetReleaseStatusResponse, error) UpdateRelease(rlsName, chStr string, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) UpdateReleaseFromChart(rlsName string, chart *chart.Chart, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error)