github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/pkg/action/uninstall.go (about)

     1  /*
     2  Copyright The Helm Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package action
    18  
    19  import (
    20  	"strings"
    21  	"time"
    22  
    23  	"github.com/pkg/errors"
    24  
    25  	"github.com/stefanmcshane/helm/pkg/chartutil"
    26  	"github.com/stefanmcshane/helm/pkg/kube"
    27  	"github.com/stefanmcshane/helm/pkg/release"
    28  	"github.com/stefanmcshane/helm/pkg/releaseutil"
    29  	helmtime "github.com/stefanmcshane/helm/pkg/time"
    30  )
    31  
    32  // Uninstall is the action for uninstalling releases.
    33  //
    34  // It provides the implementation of 'helm uninstall'.
    35  type Uninstall struct {
    36  	cfg *Configuration
    37  
    38  	DisableHooks bool
    39  	DryRun       bool
    40  	KeepHistory  bool
    41  	Wait         bool
    42  	Timeout      time.Duration
    43  	Description  string
    44  }
    45  
    46  // NewUninstall creates a new Uninstall object with the given configuration.
    47  func NewUninstall(cfg *Configuration) *Uninstall {
    48  	return &Uninstall{
    49  		cfg: cfg,
    50  	}
    51  }
    52  
    53  // Run uninstalls the given release.
    54  func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error) {
    55  	if err := u.cfg.KubeClient.IsReachable(); err != nil {
    56  		return nil, err
    57  	}
    58  
    59  	if u.DryRun {
    60  		// In the dry run case, just see if the release exists
    61  		r, err := u.cfg.releaseContent(name, 0)
    62  		if err != nil {
    63  			return &release.UninstallReleaseResponse{}, err
    64  		}
    65  		return &release.UninstallReleaseResponse{Release: r}, nil
    66  	}
    67  
    68  	if err := chartutil.ValidateReleaseName(name); err != nil {
    69  		return nil, errors.Errorf("uninstall: Release name is invalid: %s", name)
    70  	}
    71  
    72  	rels, err := u.cfg.Releases.History(name)
    73  	if err != nil {
    74  		return nil, errors.Wrapf(err, "uninstall: Release not loaded: %s", name)
    75  	}
    76  	if len(rels) < 1 {
    77  		return nil, errMissingRelease
    78  	}
    79  
    80  	releaseutil.SortByRevision(rels)
    81  	rel := rels[len(rels)-1]
    82  
    83  	// TODO: Are there any cases where we want to force a delete even if it's
    84  	// already marked deleted?
    85  	if rel.Info.Status == release.StatusUninstalled {
    86  		if !u.KeepHistory {
    87  			if err := u.purgeReleases(rels...); err != nil {
    88  				return nil, errors.Wrap(err, "uninstall: Failed to purge the release")
    89  			}
    90  			return &release.UninstallReleaseResponse{Release: rel}, nil
    91  		}
    92  		return nil, errors.Errorf("the release named %q is already deleted", name)
    93  	}
    94  
    95  	u.cfg.Log("uninstall: Deleting %s", name)
    96  	rel.Info.Status = release.StatusUninstalling
    97  	rel.Info.Deleted = helmtime.Now()
    98  	rel.Info.Description = "Deletion in progress (or silently failed)"
    99  	res := &release.UninstallReleaseResponse{Release: rel}
   100  
   101  	if !u.DisableHooks {
   102  		if err := u.cfg.execHook(rel, release.HookPreDelete, u.Timeout); err != nil {
   103  			return res, err
   104  		}
   105  	} else {
   106  		u.cfg.Log("delete hooks disabled for %s", name)
   107  	}
   108  
   109  	// From here on out, the release is currently considered to be in StatusUninstalling
   110  	// state.
   111  	if err := u.cfg.Releases.Update(rel); err != nil {
   112  		u.cfg.Log("uninstall: Failed to store updated release: %s", err)
   113  	}
   114  
   115  	deletedResources, kept, errs := u.deleteRelease(rel)
   116  	if errs != nil {
   117  		u.cfg.Log("uninstall: Failed to delete release: %s", errs)
   118  		return nil, errors.Errorf("failed to delete release: %s", name)
   119  	}
   120  
   121  	if kept != "" {
   122  		kept = "These resources were kept due to the resource policy:\n" + kept
   123  	}
   124  	res.Info = kept
   125  
   126  	if u.Wait {
   127  		if kubeClient, ok := u.cfg.KubeClient.(kube.InterfaceExt); ok {
   128  			if err := kubeClient.WaitForDelete(deletedResources, u.Timeout); err != nil {
   129  				errs = append(errs, err)
   130  			}
   131  		}
   132  	}
   133  
   134  	if !u.DisableHooks {
   135  		if err := u.cfg.execHook(rel, release.HookPostDelete, u.Timeout); err != nil {
   136  			errs = append(errs, err)
   137  		}
   138  	}
   139  
   140  	rel.Info.Status = release.StatusUninstalled
   141  	if len(u.Description) > 0 {
   142  		rel.Info.Description = u.Description
   143  	} else {
   144  		rel.Info.Description = "Uninstallation complete"
   145  	}
   146  
   147  	if !u.KeepHistory {
   148  		u.cfg.Log("purge requested for %s", name)
   149  		err := u.purgeReleases(rels...)
   150  		if err != nil {
   151  			errs = append(errs, errors.Wrap(err, "uninstall: Failed to purge the release"))
   152  		}
   153  
   154  		// Return the errors that occurred while deleting the release, if any
   155  		if len(errs) > 0 {
   156  			return res, errors.Errorf("uninstallation completed with %d error(s): %s", len(errs), joinErrors(errs))
   157  		}
   158  
   159  		return res, nil
   160  	}
   161  
   162  	if err := u.cfg.Releases.Update(rel); err != nil {
   163  		u.cfg.Log("uninstall: Failed to store updated release: %s", err)
   164  	}
   165  
   166  	if len(errs) > 0 {
   167  		return res, errors.Errorf("uninstallation completed with %d error(s): %s", len(errs), joinErrors(errs))
   168  	}
   169  	return res, nil
   170  }
   171  
   172  func (u *Uninstall) purgeReleases(rels ...*release.Release) error {
   173  	for _, rel := range rels {
   174  		if _, err := u.cfg.Releases.Delete(rel.Name, rel.Version); err != nil {
   175  			return err
   176  		}
   177  	}
   178  	return nil
   179  }
   180  
   181  func joinErrors(errs []error) string {
   182  	es := make([]string, 0, len(errs))
   183  	for _, e := range errs {
   184  		es = append(es, e.Error())
   185  	}
   186  	return strings.Join(es, "; ")
   187  }
   188  
   189  // deleteRelease deletes the release and returns list of delete resources and manifests that were kept in the deletion process
   190  func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, string, []error) {
   191  	var errs []error
   192  	caps, err := u.cfg.getCapabilities()
   193  	if err != nil {
   194  		return nil, rel.Manifest, []error{errors.Wrap(err, "could not get apiVersions from Kubernetes")}
   195  	}
   196  
   197  	manifests := releaseutil.SplitManifests(rel.Manifest)
   198  	_, files, err := releaseutil.SortManifests(manifests, caps.APIVersions, releaseutil.UninstallOrder)
   199  	if err != nil {
   200  		// We could instead just delete everything in no particular order.
   201  		// FIXME: One way to delete at this point would be to try a label-based
   202  		// deletion. The problem with this is that we could get a false positive
   203  		// and delete something that was not legitimately part of this release.
   204  		return nil, rel.Manifest, []error{errors.Wrap(err, "corrupted release record. You must manually delete the resources")}
   205  	}
   206  
   207  	filesToKeep, filesToDelete := filterManifestsToKeep(files)
   208  	var kept string
   209  	for _, f := range filesToKeep {
   210  		kept += "[" + f.Head.Kind + "] " + f.Head.Metadata.Name + "\n"
   211  	}
   212  
   213  	var builder strings.Builder
   214  	for _, file := range filesToDelete {
   215  		builder.WriteString("\n---\n" + file.Content)
   216  	}
   217  
   218  	resources, err := u.cfg.KubeClient.Build(strings.NewReader(builder.String()), false)
   219  	if err != nil {
   220  		return nil, "", []error{errors.Wrap(err, "unable to build kubernetes objects for delete")}
   221  	}
   222  	if len(resources) > 0 {
   223  		_, errs = u.cfg.KubeClient.Delete(resources)
   224  	}
   225  	return resources, kept, errs
   226  }