github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/cmd/gc/gc_helm.go (about)

     1  package gc
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"os"
     9  	"regexp"
    10  	"sort"
    11  	"strconv"
    12  
    13  	"github.com/olli-ai/jx/v2/pkg/cmd/helper"
    14  
    15  	"github.com/ghodss/yaml"
    16  
    17  	"github.com/spf13/cobra"
    18  	v1 "k8s.io/api/core/v1"
    19  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    20  
    21  	"github.com/jenkins-x/jx-logging/pkg/log"
    22  	"github.com/olli-ai/jx/v2/pkg/cmd/opts"
    23  	"github.com/olli-ai/jx/v2/pkg/cmd/templates"
    24  )
    25  
    26  // GetOptions is the start of the data required to perform the operation.  As new fields are added, add them here instead of
    27  // referencing the cmd.Flags()
    28  type GCHelmOptions struct {
    29  	*opts.CommonOptions
    30  
    31  	RevisionHistoryLimit int
    32  	OutDir               string
    33  	DryRun               bool
    34  	NoBackup             bool
    35  }
    36  
    37  var (
    38  	GCHelmLong = templates.LongDesc(`
    39  		Garbage collect Helm ConfigMaps.  To facilitate rollbacks, Helm leaves a history of chart versions in place in Kubernetes and these should be pruned at intervals to avoid consuming excessive system resources.
    40  
    41  `)
    42  
    43  	GCHelmExample = templates.Examples(`
    44  		jx garbage collect helm
    45  		jx gc helm
    46  `)
    47  )
    48  
    49  // NewCmdGCHelm  a command object for the "garbage collect" command
    50  func NewCmdGCHelm(commonOpts *opts.CommonOptions) *cobra.Command {
    51  	options := &GCHelmOptions{
    52  		CommonOptions: commonOpts,
    53  	}
    54  
    55  	cmd := &cobra.Command{
    56  		Use:     "helm",
    57  		Short:   "garbage collection for Helm ConfigMaps",
    58  		Long:    GCHelmLong,
    59  		Example: GCHelmExample,
    60  		Run: func(cmd *cobra.Command, args []string) {
    61  			options.Cmd = cmd
    62  			options.Args = args
    63  			err := options.Run()
    64  			helper.CheckErr(err)
    65  		},
    66  	}
    67  	cmd.Flags().IntVarP(&options.RevisionHistoryLimit, "revision-history-limit", "", 10, "Minimum number of versions per release to keep")
    68  	cmd.Flags().StringVarP(&options.OutDir, opts.OptionOutputDir, "o", "configmaps", "Relative directory to output backup to. Defaults to ./configmaps")
    69  	cmd.Flags().BoolVarP(&options.DryRun, "dry-run", "", false, "Does not perform the delete operation on Kubernetes")
    70  	cmd.Flags().BoolVarP(&options.NoBackup, "no-backup", "", false, "Does not perform the backup operation to store files locally")
    71  	return cmd
    72  }
    73  
    74  func (o *GCHelmOptions) Run() error {
    75  	kubeClient, err := o.KubeClient()
    76  	if err != nil {
    77  		return err
    78  	}
    79  
    80  	kubeNamespace := "kube-system"
    81  
    82  	cms, err := kubeClient.CoreV1().ConfigMaps(kubeNamespace).List(metav1.ListOptions{LabelSelector: "OWNER=TILLER"})
    83  	if err != nil {
    84  		return err
    85  	}
    86  	if len(cms.Items) == 0 {
    87  		// no configmaps found so lets return gracefully
    88  		log.Logger().Debug("no config maps found")
    89  		return nil
    90  	}
    91  
    92  	releases := ExtractReleases(cms)
    93  	log.Logger().Debug(fmt.Sprintf("Found %d releases.", len(releases)))
    94  	log.Logger().Debug(fmt.Sprintf("Releases: %v", releases))
    95  
    96  	for _, release := range releases {
    97  		log.Logger().Debug(fmt.Sprintf("Checking %s. ", release))
    98  		versions := ExtractVersions(cms, release)
    99  		log.Logger().Debug(fmt.Sprintf("Found %d.", len(versions)))
   100  		log.Logger().Debug(fmt.Sprintf("%v", versions))
   101  		to_delete := VersionsToDelete(versions, o.RevisionHistoryLimit)
   102  		if len(to_delete) > 0 {
   103  			if o.DryRun {
   104  				log.Logger().Info("Would delete:")
   105  				log.Logger().Infof("%v", to_delete)
   106  			} else {
   107  				// Backup and delete
   108  				if o.NoBackup == false {
   109  					// First make sure that destination path exists
   110  					err3 := os.MkdirAll(o.OutDir, 0755)
   111  					if err3 != nil {
   112  						// Failed to create path
   113  						return err3
   114  					}
   115  				}
   116  				for _, version := range to_delete {
   117  					cm, err1 := ExtractConfigMap(cms, version)
   118  					if err1 == nil {
   119  						if o.NoBackup == false {
   120  							// Create backup for ConfigMap about to be deleted
   121  							filename := o.OutDir + "/" + version + ".yaml"
   122  							log.Logger().Info(fmt.Sprintf("Backing up %v. ", filename))
   123  							y, err2 := yaml.Marshal(cm)
   124  							if err2 != nil {
   125  								// Failed to Marshall to YAML
   126  								return err2
   127  							}
   128  							// Add apiVersion and Kind
   129  							var b bytes.Buffer
   130  							b.WriteString("apiVersion: v1\nkind: ConfigMap\n")
   131  							b.Write(y)
   132  							err4 := ioutil.WriteFile(filename, b.Bytes(), 0600)
   133  							if err4 == nil {
   134  								log.Logger().Info("Success. ")
   135  							} else {
   136  								// Failed to write backup so abort
   137  								return err4
   138  							}
   139  						}
   140  						// Now delete
   141  						var opts *metav1.DeleteOptions
   142  						err5 := kubeClient.CoreV1().ConfigMaps(kubeNamespace).Delete(version, opts)
   143  						if err5 == nil {
   144  							log.Logger().Info(fmt.Sprintf("ConfigMap %v deleted.", version))
   145  						} else {
   146  							// Failed to delete
   147  							return err5
   148  						}
   149  					} else {
   150  						// Failed to find a ConfigMap that we know was in memory. Unlikely to occur.
   151  						log.Logger().Warn(fmt.Sprintf("Failed to find ConfigMap %s. ", version))
   152  					}
   153  				}
   154  			}
   155  		} else {
   156  			log.Logger().Debug("Nothing to do.")
   157  		}
   158  	}
   159  	return nil
   160  }
   161  
   162  // ExtractReleases Extract a set of releases from a list of ConfigMaps
   163  func ExtractReleases(cms *v1.ConfigMapList) []string {
   164  	found := make(map[string]bool)
   165  	for _, cm := range cms.Items {
   166  		if cmname, ok := cm.Labels["NAME"]; ok {
   167  			// Collect unique names
   168  			if _, seen := found[cmname]; !seen {
   169  				found[cmname] = true
   170  			}
   171  		}
   172  	}
   173  
   174  	// Return a set of unique Helm releases
   175  	releases := []string{}
   176  	for key := range found {
   177  		releases = append(releases, key)
   178  	}
   179  	return releases
   180  }
   181  
   182  // ExtractVersions Extract a set of versions of a named release from a list of ConfigMaps
   183  func ExtractVersions(cms *v1.ConfigMapList, release string) []string {
   184  	found := []string{}
   185  	for _, cm := range cms.Items {
   186  		if release == cm.Labels["NAME"] {
   187  			found = append(found, cm.Name)
   188  		}
   189  	}
   190  	return found
   191  }
   192  
   193  // VersionsToDelete returns a slice of strings
   194  func VersionsToDelete(versions []string, desired int) []string {
   195  	if desired >= len(versions) {
   196  		// nothing to delete
   197  		return []string{}
   198  	}
   199  	sort.Sort(ByVersion(versions))
   200  	return versions[:len(versions)-desired]
   201  }
   202  
   203  // ExtractConfigMap extracts a configmap
   204  func ExtractConfigMap(cms *v1.ConfigMapList, version string) (v1.ConfigMap, error) {
   205  	for _, cm := range cms.Items {
   206  		if version == cm.Name {
   207  			return cm, nil
   208  		}
   209  	}
   210  	return v1.ConfigMap{}, errors.New("Not found")
   211  }
   212  
   213  // Components for sorting versions by numeric version number where version name ends in .vddd where ddd is an arbitrary sequence of digits
   214  type ByVersion []string
   215  
   216  func (a ByVersion) Len() int      { return len(a) }
   217  func (a ByVersion) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
   218  func (a ByVersion) Less(i, j int) bool {
   219  	r, _ := regexp.Compile(`.v\d*$`)
   220  	loc := r.FindStringIndex(a[i])
   221  	if loc == nil {
   222  		return false
   223  	}
   224  	trim := loc[0] + 2 // start of numeric
   225  	version_number_i, err_i := strconv.Atoi(a[i][trim:])
   226  	version_number_j, err_j := strconv.Atoi(a[j][trim:])
   227  	if (err_i == nil) && (err_j == nil) {
   228  		return version_number_i < version_number_j
   229  	}
   230  
   231  	return false
   232  }