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 }