github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/pkg/packager/remove.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // SPDX-FileCopyrightText: 2021-Present The Jackal Authors
     3  
     4  // Package packager contains functions for interacting with, managing and deploying Jackal packages.
     5  package packager
     6  
     7  import (
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"runtime"
    12  
    13  	"slices"
    14  
    15  	"github.com/Racer159/jackal/src/config"
    16  	"github.com/Racer159/jackal/src/internal/packager/helm"
    17  	"github.com/Racer159/jackal/src/pkg/cluster"
    18  	"github.com/Racer159/jackal/src/pkg/message"
    19  	"github.com/Racer159/jackal/src/pkg/packager/actions"
    20  	"github.com/Racer159/jackal/src/pkg/packager/filters"
    21  	"github.com/Racer159/jackal/src/pkg/packager/sources"
    22  	"github.com/Racer159/jackal/src/types"
    23  	"github.com/defenseunicorns/pkg/helpers"
    24  	"helm.sh/helm/v3/pkg/storage/driver"
    25  	corev1 "k8s.io/api/core/v1"
    26  )
    27  
    28  // Remove removes a package that was already deployed onto a cluster, uninstalling all installed helm charts.
    29  func (p *Packager) Remove() (err error) {
    30  	_, isClusterSource := p.source.(*sources.ClusterSource)
    31  	if isClusterSource {
    32  		p.cluster = p.source.(*sources.ClusterSource).Cluster
    33  	}
    34  	spinner := message.NewProgressSpinner("Removing Jackal package %s", p.cfg.PkgOpts.PackageSource)
    35  	defer spinner.Stop()
    36  
    37  	var packageName string
    38  
    39  	// we do not want to allow removal of signed packages without a signature if there are remove actions
    40  	// as this is arbitrary code execution from an untrusted source
    41  	p.cfg.Pkg, p.warnings, err = p.source.LoadPackageMetadata(p.layout, false, false)
    42  	if err != nil {
    43  		return err
    44  	}
    45  	packageName = p.cfg.Pkg.Metadata.Name
    46  
    47  	// Build a list of components to remove and determine if we need a cluster connection
    48  	componentsToRemove := []string{}
    49  	packageRequiresCluster := false
    50  
    51  	// If components were provided; just remove the things we were asked to remove
    52  	filter := filters.Combine(
    53  		filters.ByLocalOS(runtime.GOOS),
    54  		filters.BySelectState(p.cfg.PkgOpts.OptionalComponents),
    55  	)
    56  	included, err := filter.Apply(p.cfg.Pkg)
    57  	if err != nil {
    58  		return err
    59  	}
    60  
    61  	for _, component := range included {
    62  		componentsToRemove = append(componentsToRemove, component.Name)
    63  
    64  		if component.RequiresCluster() {
    65  			packageRequiresCluster = true
    66  		}
    67  	}
    68  
    69  	// Get or build the secret for the deployed package
    70  	deployedPackage := &types.DeployedPackage{}
    71  
    72  	if packageRequiresCluster {
    73  		err = p.connectToCluster(cluster.DefaultTimeout)
    74  		if err != nil {
    75  			return err
    76  		}
    77  		deployedPackage, err = p.cluster.GetDeployedPackage(packageName)
    78  		if err != nil {
    79  			return fmt.Errorf("unable to load the secret for the package we are attempting to remove: %s", err.Error())
    80  		}
    81  	} else {
    82  		// If we do not need the cluster, create a deployed components object based on the info we have
    83  		deployedPackage.Name = packageName
    84  		deployedPackage.Data = p.cfg.Pkg
    85  		for _, r := range componentsToRemove {
    86  			deployedPackage.DeployedComponents = append(deployedPackage.DeployedComponents, types.DeployedComponent{Name: r})
    87  		}
    88  	}
    89  
    90  	for _, dc := range helpers.Reverse(deployedPackage.DeployedComponents) {
    91  		// Only remove the component if it was requested or if we are removing the whole package
    92  		if !slices.Contains(componentsToRemove, dc.Name) {
    93  			continue
    94  		}
    95  
    96  		if deployedPackage, err = p.removeComponent(deployedPackage, dc, spinner); err != nil {
    97  			return fmt.Errorf("unable to remove the component '%s': %w", dc.Name, err)
    98  		}
    99  	}
   100  
   101  	return nil
   102  }
   103  
   104  func (p *Packager) updatePackageSecret(deployedPackage types.DeployedPackage) {
   105  	// Only attempt to update the package secret if we are actually connected to a cluster
   106  	if p.cluster != nil {
   107  		secretName := config.JackalPackagePrefix + deployedPackage.Name
   108  
   109  		// Save the new secret with the removed components removed from the secret
   110  		newPackageSecret := p.cluster.GenerateSecret(cluster.JackalNamespaceName, secretName, corev1.SecretTypeOpaque)
   111  		newPackageSecret.Labels[cluster.JackalPackageInfoLabel] = deployedPackage.Name
   112  
   113  		newPackageSecretData, _ := json.Marshal(deployedPackage)
   114  		newPackageSecret.Data["data"] = newPackageSecretData
   115  
   116  		_, err := p.cluster.CreateOrUpdateSecret(newPackageSecret)
   117  
   118  		// We warn and ignore errors because we may have removed the cluster that this package was inside of
   119  		if err != nil {
   120  			message.Warnf("Unable to update the '%s' package secret: '%s' (this may be normal if the cluster was removed)", secretName, err.Error())
   121  		}
   122  	}
   123  }
   124  
   125  func (p *Packager) removeComponent(deployedPackage *types.DeployedPackage, deployedComponent types.DeployedComponent, spinner *message.Spinner) (*types.DeployedPackage, error) {
   126  	components := deployedPackage.Data.Components
   127  
   128  	c := helpers.Find(components, func(t types.JackalComponent) bool {
   129  		return t.Name == deployedComponent.Name
   130  	})
   131  
   132  	onRemove := c.Actions.OnRemove
   133  	onFailure := func() {
   134  		if err := actions.Run(p.cfg, onRemove.Defaults, onRemove.OnFailure, nil); err != nil {
   135  			message.Debugf("Unable to run the failure action: %s", err)
   136  		}
   137  	}
   138  
   139  	if err := actions.Run(p.cfg, onRemove.Defaults, onRemove.Before, nil); err != nil {
   140  		onFailure()
   141  		return nil, fmt.Errorf("unable to run the before action for component (%s): %w", c.Name, err)
   142  	}
   143  
   144  	for _, chart := range helpers.Reverse(deployedComponent.InstalledCharts) {
   145  		spinner.Updatef("Uninstalling chart '%s' from the '%s' component", chart.ChartName, deployedComponent.Name)
   146  
   147  		helmCfg := helm.NewClusterOnly(p.cfg, p.cluster)
   148  		if err := helmCfg.RemoveChart(chart.Namespace, chart.ChartName, spinner); err != nil {
   149  			if !errors.Is(err, driver.ErrReleaseNotFound) {
   150  				onFailure()
   151  				return deployedPackage, fmt.Errorf("unable to uninstall the helm chart %s in the namespace %s: %w",
   152  					chart.ChartName, chart.Namespace, err)
   153  			}
   154  			message.Warnf("Helm release for helm chart '%s' in the namespace '%s' was not found.  Was it already removed?",
   155  				chart.ChartName, chart.Namespace)
   156  		}
   157  
   158  		// Remove the uninstalled chart from the list of installed charts
   159  		// NOTE: We are saving the secret as we remove charts in case a failure happens later on in the process of removing the component.
   160  		//       If we don't save the secrets as we remove charts, we will run into issues if we try to remove the component again as we will
   161  		//       be trying to remove charts that have already been removed.
   162  		deployedComponent.InstalledCharts = helpers.RemoveMatches(deployedComponent.InstalledCharts, func(t types.InstalledChart) bool {
   163  			return t.ChartName == chart.ChartName
   164  		})
   165  		p.updatePackageSecret(*deployedPackage)
   166  	}
   167  
   168  	if err := actions.Run(p.cfg, onRemove.Defaults, onRemove.After, nil); err != nil {
   169  		onFailure()
   170  		return deployedPackage, fmt.Errorf("unable to run the after action: %w", err)
   171  	}
   172  
   173  	if err := actions.Run(p.cfg, onRemove.Defaults, onRemove.OnSuccess, nil); err != nil {
   174  		onFailure()
   175  		return deployedPackage, fmt.Errorf("unable to run the success action: %w", err)
   176  	}
   177  
   178  	// Remove the component we just removed from the array
   179  	deployedPackage.DeployedComponents = helpers.RemoveMatches(deployedPackage.DeployedComponents, func(t types.DeployedComponent) bool {
   180  		return t.Name == c.Name
   181  	})
   182  
   183  	if len(deployedPackage.DeployedComponents) == 0 && p.cluster != nil {
   184  		secretName := config.JackalPackagePrefix + deployedPackage.Name
   185  
   186  		// All the installed components were deleted, therefore this package is no longer actually deployed
   187  		packageSecret, err := p.cluster.GetSecret(cluster.JackalNamespaceName, secretName)
   188  
   189  		// We warn and ignore errors because we may have removed the cluster that this package was inside of
   190  		if err != nil {
   191  			message.Warnf("Unable to delete the '%s' package secret: '%s' (this may be normal if the cluster was removed)", secretName, err.Error())
   192  		} else {
   193  			err = p.cluster.DeleteSecret(packageSecret)
   194  			if err != nil {
   195  				message.Warnf("Unable to delete the '%s' package secret: '%s' (this may be normal if the cluster was removed)", secretName, err.Error())
   196  			}
   197  		}
   198  	} else {
   199  		p.updatePackageSecret(*deployedPackage)
   200  	}
   201  
   202  	return deployedPackage, nil
   203  }