Skip to content

Commit 3b0b34e

Browse files
authored
Update AMI cleaner script to properly handle macOS images (aws#1370)
1 parent a1f9e81 commit 3b0b34e

File tree

1 file changed

+115
-19
lines changed

1 file changed

+115
-19
lines changed

tool/clean/clean_ami/clean_ami.go

Lines changed: 115 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ package main
88

99
import (
1010
"context"
11+
"errors"
1112
"fmt"
1213
"log"
14+
"sort"
15+
"strings"
1316
"time"
1417

1518
"github.com/aws/aws-sdk-go-v2/aws"
@@ -22,18 +25,113 @@ import (
2225
)
2326

2427
func main() {
25-
err := cleanAMI()
28+
err := cleanAMIs()
2629
if err != nil {
2730
log.Fatalf("errors cleaning %v", err)
2831
}
2932
}
3033

31-
func cleanAMI() error {
34+
// takes a list of AMIs and sorts them by creation date (youngest to oldest)
35+
func sortAMIsByCreationDate(amiList []types.Image, errList *[]error) []types.Image {
36+
sort.Slice(amiList, func(i, j int) bool {
37+
if amiList[i].CreationDate != nil && amiList[j].CreationDate != nil {
38+
iCreationDate, iErr := smithyTime.ParseDateTime(*amiList[i].CreationDate)
39+
jCreationDate, jErr := smithyTime.ParseDateTime(*amiList[j].CreationDate)
40+
41+
if err := errors.Join(iErr, jErr); err != nil && errList != nil {
42+
*errList = append(*errList, err)
43+
return false
44+
}
45+
46+
return iCreationDate.After(jCreationDate)
47+
} else {
48+
return false
49+
}
50+
})
51+
52+
return amiList
53+
}
54+
55+
// given a slice of AMIs, deregisters them one by one
56+
func deregisterAMIs(ctx context.Context, ec2client *ec2.Client, images []types.Image, errList *[]error) {
57+
for _, image := range images {
58+
if image.Name != nil && image.ImageId != nil && image.CreationDate != nil {
59+
log.Printf("Try to delete ami %v tags %v image id %v image creation date raw %v", *image.Name, image.Tags, *image.ImageId, *image.CreationDate)
60+
deregisterImageInput := &ec2.DeregisterImageInput{ImageId: image.ImageId}
61+
_, err := ec2client.DeregisterImage(ctx, deregisterImageInput)
62+
63+
if err != nil && errList != nil {
64+
log.Printf("Error while deregistering ami %v", *image.Name)
65+
*errList = append(*errList, err)
66+
}
67+
}
68+
}
69+
}
70+
71+
// given a map of macos version/architecture to a list of corresponding AMIs, deregister AMIs that are no longer needed
72+
func cleanMacAMIs(ctx context.Context, ec2client *ec2.Client, macosImageAmiMap map[string][]types.Image, expirationDate time.Time, errList *[]error) {
73+
for name, amiList := range macosImageAmiMap {
74+
// don't delete an ami if it's the only one for that version/architecture
75+
if len(amiList) == 1 {
76+
continue
77+
}
78+
79+
// Sort AMIs by creation date (youngest to oldest)
80+
amiList = sortAMIsByCreationDate(amiList, errList)
81+
82+
// find the youngest AMI in the list
83+
youngestCreationDate, err := smithyTime.ParseDateTime(aws.ToString(amiList[0].CreationDate))
84+
85+
if err != nil && errList != nil {
86+
*errList = append(*errList, err)
87+
continue
88+
}
89+
90+
if expirationDate.After(youngestCreationDate) {
91+
// If the youngest AMI is over 60 days old, we keep one (the youngest) and can delete the rest
92+
log.Printf("Youngest AMI for %s is over 60 days old. Deleting all but the youngest.", name)
93+
deregisterAMIs(ctx, ec2client, amiList[1:], errList)
94+
} else {
95+
// If the youngest AMI is under 60 days old, keep incrementing until we find AMIs older than 60 days and delete them
96+
for index, ami := range amiList {
97+
creationDate, err := smithyTime.ParseDateTime(aws.ToString(ami.CreationDate))
98+
if err != nil && errList != nil {
99+
*errList = append(*errList, err)
100+
continue
101+
}
102+
if expirationDate.After(creationDate) {
103+
// once you find the first AMI that's over 60 days old, delete the ones that follow
104+
deregisterAMIs(ctx, ec2client, amiList[index:], errList)
105+
break
106+
}
107+
}
108+
}
109+
}
110+
}
111+
112+
// given a single non macos image, determine its age and deregister if needed
113+
func cleanNonMacAMIs(ctx context.Context, ec2client *ec2.Client, image types.Image, expirationDate time.Time, errList *[]error) {
114+
creationDate, err := smithyTime.ParseDateTime(aws.ToString(image.CreationDate))
115+
if err != nil && errList != nil {
116+
*errList = append(*errList, err)
117+
return
118+
}
119+
120+
if expirationDate.After(creationDate) {
121+
deregisterAMIs(ctx, ec2client, []types.Image{image}, errList)
122+
}
123+
}
124+
125+
func cleanAMIs() error {
32126
log.Print("Begin to clean EC2 AMI")
33127

128+
// sets expiration date to 60 days in the past
34129
expirationDate := time.Now().UTC().Add(clean.KeepDurationSixtyDay)
35-
cxt := context.Background()
36-
defaultConfig, err := config.LoadDefaultConfig(cxt)
130+
log.Printf("Expiration date set as %v", expirationDate)
131+
132+
// load default config
133+
ctx := context.Background()
134+
defaultConfig, err := config.LoadDefaultConfig(ctx)
37135
if err != nil {
38136
return err
39137
}
@@ -46,30 +144,28 @@ func cleanAMI() error {
46144

47145
//get instances to delete
48146
describeImagesInput := ec2.DescribeImagesInput{Filters: []types.Filter{nameFilter}}
49-
describeImagesOutput, err := ec2client.DescribeImages(cxt, &describeImagesInput)
147+
describeImagesOutput, err := ec2client.DescribeImages(ctx, &describeImagesInput)
50148
if err != nil {
51149
return err
52150
}
53151

54152
var errList []error
153+
// stores a list of AMIs per each macos version/architecture
154+
macosImageAmiMap := make(map[string][]types.Image)
155+
55156
for _, image := range describeImagesOutput.Images {
56-
creationDate, err := smithyTime.ParseDateTime(*image.CreationDate)
57-
if err != nil {
58-
errList = append(errList, err)
59-
continue
60-
}
61-
log.Printf("image name %v image id %v experation date %v creation date parsed %v image creation date raw %v",
62-
*image.Name, *image.ImageId, creationDate, expirationDate, *image.CreationDate)
63-
if expirationDate.After(creationDate) {
64-
log.Printf("Try to delete ami %s tags %v launch-date %s", *image.Name, image.Tags, *image.CreationDate)
65-
deregisterImageInput := ec2.DeregisterImageInput{ImageId: image.ImageId}
66-
_, err := ec2client.DeregisterImage(cxt, &deregisterImageInput)
67-
if err != nil {
68-
errList = append(errList, err)
69-
}
157+
if image.Name != nil && strings.HasPrefix(*image.Name, "cloudwatch-agent-integration-test-mac") {
158+
// mac image - add it to the map and do nothing else for now
159+
macosImageAmiMap[*image.Name] = append(macosImageAmiMap[*image.Name], image)
160+
} else {
161+
// non mac image - clean it if it's older than 60 days
162+
cleanNonMacAMIs(ctx, ec2client, image, expirationDate, &errList)
70163
}
71164
}
72165

166+
// handle the mac AMIs
167+
cleanMacAMIs(ctx, ec2client, macosImageAmiMap, expirationDate, &errList)
168+
73169
if len(errList) != 0 {
74170
return fmt.Errorf("%v", errList)
75171
}

0 commit comments

Comments
 (0)