feat: add optional boolean '--wait' flag to 'uninstall' command.

If set, 'uninstall' command will wait until all the resources are deleted before returning.
It will wait for as long as --timeout

closes #2378

Signed-off-by: Mike Ng <ming@redhat.com>
pull/9702/head
Mike Ng 4 years ago
parent a9c957d35b
commit 655bdcd2fd

@ -0,0 +1 @@
release "aeneas" uninstalled

@ -71,6 +71,7 @@ func newUninstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.BoolVar(&client.DryRun, "dry-run", false, "simulate a uninstall") f.BoolVar(&client.DryRun, "dry-run", false, "simulate a uninstall")
f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during uninstallation") f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during uninstallation")
f.BoolVar(&client.KeepHistory, "keep-history", false, "remove all associated resources and mark the release as deleted, but retain the release history") f.BoolVar(&client.KeepHistory, "keep-history", false, "remove all associated resources and mark the release as deleted, but retain the release history")
f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all the resources are deleted before returning. It will wait for as long as --timeout")
f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.StringVar(&client.Description, "description", "", "add a custom description") f.StringVar(&client.Description, "description", "", "add a custom description")

@ -57,6 +57,12 @@ func TestUninstall(t *testing.T) {
golden: "output/uninstall-keep-history.txt", golden: "output/uninstall-keep-history.txt",
rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})}, rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})},
}, },
{
name: "wait",
cmd: "uninstall aeneas --wait",
golden: "output/uninstall-wait.txt",
rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})},
},
{ {
name: "uninstall without release", name: "uninstall without release",
cmd: "uninstall", cmd: "uninstall",

@ -23,6 +23,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/kube"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/releaseutil" "helm.sh/helm/v3/pkg/releaseutil"
helmtime "helm.sh/helm/v3/pkg/time" helmtime "helm.sh/helm/v3/pkg/time"
@ -37,6 +38,7 @@ type Uninstall struct {
DisableHooks bool DisableHooks bool
DryRun bool DryRun bool
KeepHistory bool KeepHistory bool
Wait bool
Timeout time.Duration Timeout time.Duration
Description string Description string
} }
@ -110,13 +112,19 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
u.cfg.Log("uninstall: Failed to store updated release: %s", err) u.cfg.Log("uninstall: Failed to store updated release: %s", err)
} }
kept, errs := u.deleteRelease(rel) deletedResources, kept, errs := u.deleteRelease(rel)
if kept != "" { if kept != "" {
kept = "These resources were kept due to the resource policy:\n" + kept kept = "These resources were kept due to the resource policy:\n" + kept
} }
res.Info = kept res.Info = kept
if u.Wait {
if err := u.cfg.KubeClient.WaitForDelete(deletedResources, u.Timeout); err != nil {
errs = append(errs, err)
}
}
if !u.DisableHooks { if !u.DisableHooks {
if err := u.cfg.execHook(rel, release.HookPostDelete, u.Timeout); err != nil { if err := u.cfg.execHook(rel, release.HookPostDelete, u.Timeout); err != nil {
errs = append(errs, err) errs = append(errs, err)
@ -172,12 +180,12 @@ func joinErrors(errs []error) string {
return strings.Join(es, "; ") return strings.Join(es, "; ")
} }
// deleteRelease deletes the release and returns manifests that were kept in the deletion process // deleteRelease deletes the release and returns list of delete resources and manifests that were kept in the deletion process
func (u *Uninstall) deleteRelease(rel *release.Release) (string, []error) { func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, string, []error) {
var errs []error var errs []error
caps, err := u.cfg.getCapabilities() caps, err := u.cfg.getCapabilities()
if err != nil { if err != nil {
return rel.Manifest, []error{errors.Wrap(err, "could not get apiVersions from Kubernetes")} return nil, rel.Manifest, []error{errors.Wrap(err, "could not get apiVersions from Kubernetes")}
} }
manifests := releaseutil.SplitManifests(rel.Manifest) manifests := releaseutil.SplitManifests(rel.Manifest)
@ -187,7 +195,7 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (string, []error) {
// FIXME: One way to delete at this point would be to try a label-based // FIXME: One way to delete at this point would be to try a label-based
// deletion. The problem with this is that we could get a false positive // deletion. The problem with this is that we could get a false positive
// and delete something that was not legitimately part of this release. // and delete something that was not legitimately part of this release.
return rel.Manifest, []error{errors.Wrap(err, "corrupted release record. You must manually delete the resources")} return nil, rel.Manifest, []error{errors.Wrap(err, "corrupted release record. You must manually delete the resources")}
} }
filesToKeep, filesToDelete := filterManifestsToKeep(files) filesToKeep, filesToDelete := filterManifestsToKeep(files)
@ -203,10 +211,10 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (string, []error) {
resources, err := u.cfg.KubeClient.Build(strings.NewReader(builder.String()), false) resources, err := u.cfg.KubeClient.Build(strings.NewReader(builder.String()), false)
if err != nil { if err != nil {
return "", []error{errors.Wrap(err, "unable to build kubernetes objects for delete")} return nil, "", []error{errors.Wrap(err, "unable to build kubernetes objects for delete")}
} }
if len(resources) > 0 { if len(resources) > 0 {
_, errs = u.cfg.KubeClient.Delete(resources) _, errs = u.cfg.KubeClient.Delete(resources)
} }
return kept, errs return resources, kept, errs
} }

@ -17,9 +17,13 @@ limitations under the License.
package action package action
import ( import (
"fmt"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
kubefake "helm.sh/helm/v3/pkg/kube/fake"
"helm.sh/helm/v3/pkg/release"
) )
func uninstallAction(t *testing.T) *Uninstall { func uninstallAction(t *testing.T) *Uninstall {
@ -60,3 +64,34 @@ func TestUninstallRelease_deleteRelease(t *testing.T) {
` `
is.Contains(res.Info, expected) is.Contains(res.Info, expected)
} }
func TestUninstallRelease_Wait(t *testing.T) {
is := assert.New(t)
unAction := uninstallAction(t)
unAction.DisableHooks = true
unAction.DryRun = false
unAction.Wait = true
rel := releaseStub()
rel.Name = "come-fail-away"
rel.Manifest = `{
"apiVersion": "v1",
"kind": "Secret",
"metadata": {
"name": "secret"
},
"type": "Opaque",
"data": {
"password": "password"
}
}`
unAction.cfg.Releases.Create(rel)
failer := unAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitError = fmt.Errorf("U timed out")
unAction.cfg.KubeClient = failer
res, err := unAction.Run(rel.Name)
is.Error(err)
is.Contains(err.Error(), "U timed out")
is.Equal(res.Release.Info.Status, release.StatusUninstalled)
}

@ -162,6 +162,15 @@ func (c *Client) WaitWithJobs(resources ResourceList, timeout time.Duration) err
return w.waitForResources(resources) return w.waitForResources(resources)
} }
// Wait up to the given timeout for the specified resources to be deleted
func (c *Client) WaitForDelete(resources ResourceList, timeout time.Duration) error {
w := waiter{
log: c.Log,
timeout: timeout,
}
return w.waitForDeletedResources(resources)
}
func (c *Client) namespace() string { func (c *Client) namespace() string {
if c.Namespace != "" { if c.Namespace != "" {
return c.Namespace return c.Namespace

@ -63,7 +63,15 @@ func (f *FailingKubeClient) WaitWithJobs(resources kube.ResourceList, d time.Dur
if f.WaitError != nil { if f.WaitError != nil {
return f.WaitError return f.WaitError
} }
return f.PrintingKubeClient.Wait(resources, d) return f.PrintingKubeClient.WaitWithJobs(resources, d)
}
// WaitForDelete returns the configured error if set or prints
func (f *FailingKubeClient) WaitForDelete(resources kube.ResourceList, d time.Duration) error {
if f.WaitError != nil {
return f.WaitError
}
return f.PrintingKubeClient.WaitForDelete(resources, d)
} }
// Delete returns the configured error if set or prints // Delete returns the configured error if set or prints

@ -57,6 +57,11 @@ func (p *PrintingKubeClient) WaitWithJobs(resources kube.ResourceList, _ time.Du
return err return err
} }
func (p *PrintingKubeClient) WaitForDelete(resources kube.ResourceList, _ time.Duration) error {
_, err := io.Copy(p.Out, bufferize(resources))
return err
}
// Delete implements KubeClient delete. // Delete implements KubeClient delete.
// //
// It only prints out the content to be deleted. // It only prints out the content to be deleted.

@ -36,6 +36,8 @@ type Interface interface {
// WaitWithJobs wait up to the given timeout for the specified resources to be ready, including jobs. // WaitWithJobs wait up to the given timeout for the specified resources to be ready, including jobs.
WaitWithJobs(resources ResourceList, timeout time.Duration) error WaitWithJobs(resources ResourceList, timeout time.Duration) error
WaitForDelete(resources ResourceList, timeout time.Duration) error
// Delete destroys one or more resources. // Delete destroys one or more resources.
Delete(resources ResourceList) (*Result, []error) Delete(resources ResourceList) (*Result, []error)

@ -28,6 +28,7 @@ import (
batchv1 "k8s.io/api/batch/v1" batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1" extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
@ -60,6 +61,24 @@ func (w *waiter) waitForResources(created ResourceList) error {
}, ctx.Done()) }, ctx.Done())
} }
// waitForDeletedResources polls to check if all the resources are deleted or a timeout is reached
func (w *waiter) waitForDeletedResources(deleted ResourceList) error {
w.log("beginning wait for %d resources to be deleted with timeout of %v", len(deleted), w.timeout)
ctx, cancel := context.WithTimeout(context.Background(), w.timeout)
defer cancel()
return wait.PollImmediateUntil(2*time.Second, func() (bool, error) {
for _, v := range deleted {
err := v.Get()
if err == nil || !apierrors.IsNotFound(err) {
return false, err
}
}
return true, nil
}, ctx.Done())
}
// SelectorsForObject returns the pod label selector for a given object // SelectorsForObject returns the pod label selector for a given object
// //
// Modified version of https://github.com/kubernetes/kubernetes/blob/v1.14.1/pkg/kubectl/polymorphichelpers/helpers.go#L84 // Modified version of https://github.com/kubernetes/kubernetes/blob/v1.14.1/pkg/kubectl/polymorphichelpers/helpers.go#L84

Loading…
Cancel
Save