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  }