Skip to content

Commit

Permalink
Add option to support cascade deletion options
Browse files Browse the repository at this point in the history
Add --cascade=<background|foreground|orphan> option to helm uninstall
Current behaviour is hardcoded to background

Addresses issue: #10586

Signed-off-by: MichaelMorris <michael.morris@est.tech>
  • Loading branch information
MichaelMorrisEst committed Feb 2, 2023
1 parent 5bf273d commit 830d4a9
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 14 deletions.
12 changes: 12 additions & 0 deletions cmd/helm/uninstall.go
Expand Up @@ -51,6 +51,10 @@ func newUninstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return compListReleases(toComplete, args, cfg)
},
RunE: func(cmd *cobra.Command, args []string) error {
validationErr := validateCascadeFlag(client)
if validationErr != nil {
return validationErr
}
for i := 0; i < len(args); i++ {

res, err := client.Run(args[i])
Expand All @@ -72,8 +76,16 @@ func newUninstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
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.Wait, "wait", false, "if set, will wait until all the resources are deleted before returning. It will wait for as long as --timeout")
f.StringVar(&client.DeletionPropagation, "cascade", "background", "Must be \"background\", \"orphan\", or \"foreground\". Selects the deletion cascading strategy for the dependents. Defaults to background.")
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")

return cmd
}

func validateCascadeFlag(client *action.Uninstall) error {
if client.DeletionPropagation != "background" && client.DeletionPropagation != "foreground" && client.DeletionPropagation != "orphan" {
return fmt.Errorf("invalid cascade value (%s). Must be \"background\", \"foreground\", or \"orphan\"", client.DeletionPropagation)
}
return nil
}
32 changes: 26 additions & 6 deletions pkg/action/uninstall.go
Expand Up @@ -21,6 +21,7 @@ import (
"time"

"github.com/pkg/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/kube"
Expand All @@ -35,12 +36,13 @@ import (
type Uninstall struct {
cfg *Configuration

DisableHooks bool
DryRun bool
KeepHistory bool
Wait bool
Timeout time.Duration
Description string
DisableHooks bool
DryRun bool
KeepHistory bool
Wait bool
DeletionPropagation string
Timeout time.Duration
Description string
}

// NewUninstall creates a new Uninstall object with the given configuration.
Expand Down Expand Up @@ -220,7 +222,25 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, stri
return nil, "", []error{errors.Wrap(err, "unable to build kubernetes objects for delete")}
}
if len(resources) > 0 {
if kubeClient, ok := u.cfg.KubeClient.(kube.InterfaceDeletionPropagation); ok {
_, errs = kubeClient.DeleteWithPropagationPolicy(resources, parseCascadingFlag(u.cfg, u.DeletionPropagation))
return resources, kept, errs
}
_, errs = u.cfg.KubeClient.Delete(resources)
}
return resources, kept, errs
}

func parseCascadingFlag(cfg *Configuration, cascadingFlag string) v1.DeletionPropagation {
switch cascadingFlag {
case "orphan":
return v1.DeletePropagationOrphan
case "foreground":
return v1.DeletePropagationForeground
case "background":
return v1.DeletePropagationBackground
default:
cfg.Log("uninstall: given cascade value: %s, defaulting to delete propagation background", cascadingFlag)
return v1.DeletePropagationBackground
}
}
32 changes: 32 additions & 0 deletions pkg/action/uninstall_test.go
Expand Up @@ -95,3 +95,35 @@ func TestUninstallRelease_Wait(t *testing.T) {
is.Contains(err.Error(), "U timed out")
is.Equal(res.Release.Info.Status, release.StatusUninstalled)
}

func TestUninstallRelease_Cascade(t *testing.T) {
is := assert.New(t)

unAction := uninstallAction(t)
unAction.DisableHooks = true
unAction.DryRun = false
unAction.Wait = false
unAction.DeletionPropagation = "foreground"

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.DeleteWithPropagationError = fmt.Errorf("Uninstall with cascade failed")
failer.BuildDummy = true
unAction.cfg.KubeClient = failer
_, err := unAction.Run(rel.Name)
is.Error(err)
is.Contains(err.Error(), "failed to delete release: come-fail-away")
}
27 changes: 19 additions & 8 deletions pkg/kube/client.go
Expand Up @@ -456,7 +456,7 @@ func (c *Client) Update(original, target ResourceList, force bool) (*Result, err
c.Log("Skipping delete of %q due to annotation [%s=%s]", info.Name, ResourcePolicyAnno, KeepPolicy)
continue
}
if err := deleteResource(info); err != nil {
if err := deleteResource(info, metav1.DeletePropagationBackground); err != nil {
c.Log("Failed to delete %q, err: %s", info.ObjectName(), err)
continue
}
Expand All @@ -465,17 +465,29 @@ func (c *Client) Update(original, target ResourceList, force bool) (*Result, err
return res, nil
}

// Delete deletes Kubernetes resources specified in the resources list. It will
// attempt to delete all resources even if one or more fail and collect any
// errors. All successfully deleted items will be returned in the `Deleted`
// ResourceList that is part of the result.
// Delete deletes Kubernetes resources specified in the resources list with
// background cascade deletion. It will attempt to delete all resources even
// if one or more fail and collect any errors. All successfully deleted items
// will be returned in the `Deleted` ResourceList that is part of the result.
func (c *Client) Delete(resources ResourceList) (*Result, []error) {
return delete(c, resources, metav1.DeletePropagationBackground)
}

// Delete deletes Kubernetes resources specified in the resources list with
// given deletion propagation policy. It will attempt to delete all resources even
// if one or more fail and collect any errors. All successfully deleted items
// will be returned in the `Deleted` ResourceList that is part of the result.
func (c *Client) DeleteWithPropagationPolicy(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error) {
return delete(c, resources, policy)
}

func delete(c *Client, resources ResourceList, propagation metav1.DeletionPropagation) (*Result, []error) {
var errs []error
res := &Result{}
mtx := sync.Mutex{}
err := perform(resources, func(info *resource.Info) error {
c.Log("Starting delete for %q %s", info.Name, info.Mapping.GroupVersionKind.Kind)
err := deleteResource(info)
err := deleteResource(info, propagation)
if err == nil || apierrors.IsNotFound(err) {
if err != nil {
c.Log("Ignoring delete failure for %q %s: %v", info.Name, info.Mapping.GroupVersionKind, err)
Expand Down Expand Up @@ -593,8 +605,7 @@ func createResource(info *resource.Info) error {
return info.Refresh(obj, true)
}

func deleteResource(info *resource.Info) error {
policy := metav1.DeletePropagationBackground
func deleteResource(info *resource.Info, policy metav1.DeletionPropagation) error {
opts := &metav1.DeleteOptions{PropagationPolicy: &policy}
_, err := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager()).DeleteWithOptions(info.Namespace, info.Name, opts)
return err
Expand Down
24 changes: 24 additions & 0 deletions pkg/kube/fake/fake.go
Expand Up @@ -22,6 +22,7 @@ import (
"time"

v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/resource"

Expand All @@ -37,10 +38,12 @@ type FailingKubeClient struct {
GetError error
WaitError error
DeleteError error
DeleteWithPropagationError error
WatchUntilReadyError error
UpdateError error
BuildError error
BuildTableError error
BuildDummy bool
BuildUnstructuredError error
WaitAndGetCompletedPodPhaseError error
WaitDuration time.Duration
Expand Down Expand Up @@ -116,6 +119,9 @@ func (f *FailingKubeClient) Build(r io.Reader, _ bool) (kube.ResourceList, error
if f.BuildError != nil {
return []*resource.Info{}, f.BuildError
}
if f.BuildDummy {
return createDummyResourceList(), nil
}
return f.PrintingKubeClient.Build(r, false)
}

Expand All @@ -134,3 +140,21 @@ func (f *FailingKubeClient) WaitAndGetCompletedPodPhase(s string, d time.Duratio
}
return f.PrintingKubeClient.WaitAndGetCompletedPodPhase(s, d)
}

// DeleteWithPropagationPolicy returns the configured error if set or prints
func (f *FailingKubeClient) DeleteWithPropagationPolicy(resources kube.ResourceList, policy metav1.DeletionPropagation) (*kube.Result, []error) {
if f.DeleteWithPropagationError != nil {
return nil, []error{f.DeleteWithPropagationError}
}
return f.PrintingKubeClient.DeleteWithPropagationPolicy(resources, policy)
}

func createDummyResourceList() kube.ResourceList {
var resInfo resource.Info
resInfo.Name = "dummyName"
resInfo.Namespace = "dummyNamespace"
var resourceList kube.ResourceList
resourceList.Append(&resInfo)
return resourceList

}
12 changes: 12 additions & 0 deletions pkg/kube/fake/printer.go
Expand Up @@ -22,6 +22,7 @@ import (
"time"

v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/resource"

Expand Down Expand Up @@ -115,6 +116,17 @@ func (p *PrintingKubeClient) WaitAndGetCompletedPodPhase(_ string, _ time.Durati
return v1.PodSucceeded, nil
}

// DeleteWithPropagationPolicy implements KubeClient delete.
//
// It only prints out the content to be deleted.
func (p *PrintingKubeClient) DeleteWithPropagationPolicy(resources kube.ResourceList, policy metav1.DeletionPropagation) (*kube.Result, []error) {
_, err := io.Copy(p.Out, bufferize(resources))
if err != nil {
return nil, []error{err}
}
return &kube.Result{Deleted: resources}, nil
}

func bufferize(resources kube.ResourceList) io.Reader {
var builder strings.Builder
for _, info := range resources {
Expand Down
10 changes: 10 additions & 0 deletions pkg/kube/interface.go
Expand Up @@ -21,6 +21,7 @@ import (
"time"

v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)

Expand Down Expand Up @@ -79,6 +80,14 @@ type InterfaceExt interface {
WaitForDelete(resources ResourceList, timeout time.Duration) error
}

// InterfaceDeletionPropagation is introduced to avoid breaking backwards compatibility for Interface implementers.
//
// TODO Helm 4: Remove InterfaceDeletionPropagation and integrate its method(s) into the Interface.
type InterfaceDeletionPropagation interface {
// Delete destroys one or more resources. The deletion propagation is handled as per the given deletion propagation value.
DeleteWithPropagationPolicy(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error)
}

// InterfaceResources is introduced to avoid breaking backwards compatibility for Interface implementers.
//
// TODO Helm 4: Remove InterfaceResources and integrate its method(s) into the Interface.
Expand All @@ -103,4 +112,5 @@ type InterfaceResources interface {

var _ Interface = (*Client)(nil)
var _ InterfaceExt = (*Client)(nil)
var _ InterfaceDeletionPropagation = (*Client)(nil)
var _ InterfaceResources = (*Client)(nil)

0 comments on commit 830d4a9

Please sign in to comment.