github.com/cloud-foundations/dominator@v0.0.0-20221004181915-6e4fee580046/imagepublishers/amipublisher/unusedImages.go (about) 1 package amipublisher 2 3 import ( 4 "time" 5 6 "github.com/Cloud-Foundations/Dominator/lib/awsutil" 7 "github.com/Cloud-Foundations/Dominator/lib/concurrent" 8 "github.com/Cloud-Foundations/Dominator/lib/format" 9 "github.com/Cloud-Foundations/Dominator/lib/log" 10 libtags "github.com/Cloud-Foundations/Dominator/lib/tags" 11 "github.com/aws/aws-sdk-go/aws" 12 "github.com/aws/aws-sdk-go/service/ec2" 13 ) 14 15 type imageUsage struct { 16 allInstances []*ec2.Instance 17 allUsingInstances []*ec2.Instance 18 images map[string]*ec2.Image // Key: AMI ID. 19 used usedImages 20 oldInstances []*ec2.Instance 21 } 22 23 type targetImageUsage struct { 24 accountName string 25 region string 26 err error 27 imageUsage 28 } 29 30 type usedImages map[string]struct{} // Key: AMI ID. 31 32 func deleteUnusedImages(targets awsutil.TargetList, skipList awsutil.TargetList, 33 searchTags, excludeSearchTags libtags.Tags, minImageAge time.Duration, 34 logger log.DebugLogger) (UnusedImagesResult, error) { 35 logger.Debugln(0, "loading credentials") 36 cs, err := awsutil.LoadCredentials() 37 if err != nil { 38 return UnusedImagesResult{}, err 39 } 40 rawResults, err := listUnusedImagesCS(targets, skipList, searchTags, 41 excludeSearchTags, minImageAge, cs, false, logger) 42 if err != nil { 43 return UnusedImagesResult{}, err 44 } 45 concurrentState := concurrent.NewState(0) 46 for _, result := range rawResults { 47 for amiId, image := range result.images { 48 accountName := result.accountName 49 region := result.region 50 image := image 51 err := concurrentState.GoRun(func() error { 52 return deleteImage(cs, accountName, region, image) 53 }) 54 if err != nil { 55 return UnusedImagesResult{}, err 56 } 57 logger.Printf("%s: %s: deleted: %s\n", 58 result.accountName, result.region, amiId) 59 } 60 } 61 if err := concurrentState.Reap(); err != nil { 62 return UnusedImagesResult{}, err 63 } 64 return generateResults(rawResults, logger), nil 65 } 66 67 func generateResults(rawResults []targetImageUsage, 68 logger log.DebugLogger) UnusedImagesResult { 69 logger.Debugln(0, "generating results") 70 results := UnusedImagesResult{} 71 for _, result := range rawResults { 72 for amiId, image := range result.images { 73 results.UnusedImages = append(results.UnusedImages, Image{ 74 Target: awsutil.Target{ 75 AccountName: result.accountName, 76 Region: result.region, 77 }, 78 AmiId: amiId, 79 AmiName: aws.StringValue(image.Name), 80 CreationDate: aws.StringValue(image.CreationDate), 81 Description: aws.StringValue(image.Description), 82 Size: uint(computeImageConsumption(image)), 83 Tags: awsutil.CreateTagsFromList(image.Tags), 84 }) 85 } 86 for _, instance := range result.oldInstances { 87 results.OldInstances = append(results.OldInstances, Instance{ 88 Target: awsutil.Target{ 89 AccountName: result.accountName, 90 Region: result.region, 91 }, 92 AmiId: aws.StringValue(instance.ImageId), 93 InstanceId: aws.StringValue(instance.InstanceId), 94 LaunchTime: instance.LaunchTime.Format( 95 format.TimeFormatSeconds), 96 Tags: awsutil.CreateTagsFromList(instance.Tags), 97 }) 98 } 99 } 100 return results 101 } 102 103 func listUnusedImages(targets awsutil.TargetList, skipList awsutil.TargetList, 104 searchTags, excludeSearchTags libtags.Tags, minImageAge time.Duration, 105 logger log.DebugLogger) (UnusedImagesResult, error) { 106 logger.Debugln(0, "loading credentials") 107 cs, err := awsutil.LoadCredentials() 108 if err != nil { 109 return UnusedImagesResult{}, err 110 } 111 rawResults, err := listUnusedImagesCS(targets, skipList, searchTags, 112 excludeSearchTags, minImageAge, cs, false, logger) 113 if err != nil { 114 return UnusedImagesResult{}, err 115 } 116 return generateResults(rawResults, logger), nil 117 } 118 119 func listUnusedImagesCS(targets awsutil.TargetList, skipList awsutil.TargetList, 120 searchTags, excludeSearchTags libtags.Tags, minImageAge time.Duration, 121 cs *awsutil.CredentialsStore, ignoreInstances bool, 122 logger log.DebugLogger) ( 123 []targetImageUsage, error) { 124 resultsChannel := make(chan targetImageUsage, 1) 125 logger.Debugln(0, "collecting raw data") 126 numTargets, err := cs.ForEachEC2Target(targets, skipList, 127 func(awsService *ec2.EC2, account, region string, logger log.Logger) { 128 usage, err := listTargetUnusedImages(awsService, searchTags, 129 excludeSearchTags, cs.AccountNameToId(account), minImageAge, 130 ignoreInstances, logger) 131 if err != nil { 132 logger.Println(err) 133 } 134 resultsChannel <- targetImageUsage{ 135 accountName: account, 136 region: region, 137 err: err, 138 imageUsage: usage, 139 } 140 }, 141 false, logger) 142 if err != nil { 143 return nil, err 144 } 145 // Collect results. 146 logger.Debugln(0, "waiting for raw data") 147 var firstError error 148 rawResults := make([]targetImageUsage, 0, numTargets) 149 for i := 0; i < numTargets; i++ { 150 result := <-resultsChannel 151 if result.err != nil { 152 if firstError == nil { 153 firstError = result.err 154 } 155 } else { 156 rawResults = append(rawResults, result) 157 } 158 } 159 if firstError != nil { 160 return nil, firstError 161 } 162 // Aggregate used map across accounts. 163 logger.Debugln(0, "aggregating usage across accounts") 164 imagesUsedPerRegion := make(map[string]usedImages) // Key: region. 165 totalImages := 0 166 var totalGiBytes int64 167 for _, result := range rawResults { 168 usedMap := imagesUsedPerRegion[result.region] 169 if usedMap == nil { 170 usedMap = make(usedImages) 171 imagesUsedPerRegion[result.region] = usedMap 172 } 173 for amiId := range result.used { 174 usedMap[amiId] = struct{}{} 175 } 176 for _, image := range result.images { 177 totalGiBytes += computeImageConsumption(image) 178 } 179 totalImages += len(result.images) 180 } 181 logger.Printf("total images found: %d consuming %s\n", 182 totalImages, format.FormatBytes(uint64(totalGiBytes)<<30)) 183 if !ignoreInstances { 184 // Delete used images from images table. 185 logger.Debugln(0, "ignoring used images") 186 for _, result := range rawResults { 187 usedMap := imagesUsedPerRegion[result.region] 188 for amiId := range result.images { 189 if _, ok := usedMap[amiId]; ok { 190 delete(result.images, amiId) 191 } 192 } 193 } 194 // Compute space consumed by unused AMIs. 195 numUnusedImages := 0 196 var unusedGiBytes int64 197 for _, result := range rawResults { 198 numUnusedImages += len(result.images) 199 for _, image := range result.images { 200 unusedGiBytes += computeImageConsumption(image) 201 } 202 } 203 logger.Printf("number of unused images: %d consuming: %s\n", 204 numUnusedImages, format.FormatBytes(uint64(unusedGiBytes)<<30)) 205 } 206 return rawResults, nil 207 } 208 209 func listTargetUnusedImages(awsService *ec2.EC2, searchTags libtags.Tags, 210 excludeSearchTags libtags.Tags, accountId string, 211 minImageAge time.Duration, ignoreInstances bool, logger log.Logger) ( 212 imageUsage, error) { 213 results := imageUsage{ 214 images: make(map[string]*ec2.Image), 215 used: make(usedImages), 216 } 217 visibleImages := make(map[string]struct{}) 218 if images, err := getImages(awsService, "", searchTags); err != nil { 219 return imageUsage{}, err 220 } else { 221 for _, image := range images { 222 creationTime, err := time.Parse(creationTimeFormat, 223 aws.StringValue(image.CreationDate)) 224 if err != nil { 225 return imageUsage{}, err 226 } 227 if time.Since(creationTime) < minImageAge { 228 continue 229 } 230 amiId := aws.StringValue(image.ImageId) 231 visibleImages[amiId] = struct{}{} 232 if aws.StringValue(image.OwnerId) == accountId { 233 results.images[amiId] = image 234 } 235 } 236 } 237 if len(excludeSearchTags) > 0 { 238 images, err := getImages(awsService, accountId, excludeSearchTags) 239 if err != nil { 240 return imageUsage{}, err 241 } else { 242 for _, image := range images { 243 amiId := aws.StringValue(image.ImageId) 244 delete(visibleImages, amiId) 245 delete(results.images, amiId) 246 } 247 } 248 } 249 if ignoreInstances { 250 return results, nil 251 } 252 instances, err := describeInstances(awsService, nil) 253 if err != nil { 254 return imageUsage{}, err 255 } 256 for _, instance := range instances { 257 amiId := aws.StringValue(instance.ImageId) 258 results.used[amiId] = struct{}{} 259 if _, ok := visibleImages[amiId]; ok { 260 results.oldInstances = append(results.oldInstances, instance) 261 } 262 } 263 return results, nil 264 }