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 }