github.com/shashidharatd/test-infra@v0.0.0-20171006011030-71304e1ca560/jenkins/aws-janitor/main.go (about) 1 /* 2 Copyright 2016 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package main 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "flag" 23 "fmt" 24 "net/url" 25 "os" 26 "strings" 27 "time" 28 29 "github.com/aws/aws-sdk-go/aws" 30 "github.com/aws/aws-sdk-go/aws/awserr" 31 "github.com/aws/aws-sdk-go/aws/session" 32 "github.com/aws/aws-sdk-go/service/autoscaling" 33 "github.com/aws/aws-sdk-go/service/ec2" 34 "github.com/aws/aws-sdk-go/service/iam" 35 "github.com/aws/aws-sdk-go/service/s3" 36 "github.com/golang/glog" 37 ) 38 39 const defaultRegion = "us-east-1" 40 41 var maxTTL = flag.Duration("ttl", 24*time.Hour, "Maximum time before we attempt deletion of a resource. Set to 0s to nuke all non-default resources.") 42 var path = flag.String("path", "", "S3 path to store mark data in (required)") 43 44 type awsResourceType interface { 45 // MarkAndSweep queries the resource in a specific region, using 46 // the provided session (which has account-number acct), calling 47 // res.Mark(<resource>) on each resource and deleting 48 // appropriately. 49 MarkAndSweep(sess *session.Session, acct string, region string, res *awsResourceSet) error 50 } 51 52 // AWS resource types known to this script, in dependency order. 53 var awsResourceTypes = []awsResourceType{ 54 autoScalingGroups{}, 55 launchConfigurations{}, 56 instances{}, 57 // Addresses 58 // NetworkInterfaces 59 subnets{}, 60 securityGroups{}, 61 // NetworkACLs 62 // VPN Connections 63 internetGateways{}, 64 routeTables{}, 65 vpcs{}, 66 dhcpOptions{}, 67 volumes{}, 68 addresses{}, 69 } 70 71 type awsResource interface { 72 // ARN returns the AWS ARN for the resource 73 // (c.f. http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html). This 74 // is only used for uniqueness in the Mark set, but ARNs are 75 // intended to be globally unique across regions and accounts, so 76 // that works. 77 ARN() string 78 } 79 80 // awsResourceSet keeps track of the first time we saw a particular 81 // ARN, and the global TTL. See Mark() for more details. 82 type awsResourceSet struct { 83 firstSeen map[string]time.Time // ARN -> first time we saw 84 marked map[string]bool // ARN -> seen this run 85 swept []string // List of resources we attempted to sweep (to summarize) 86 ttl time.Duration 87 } 88 89 func LoadResourceSet(sess *session.Session, p *s3path, ttl time.Duration) (*awsResourceSet, error) { 90 s := &awsResourceSet{firstSeen: make(map[string]time.Time), marked: make(map[string]bool), ttl: ttl} 91 svc := s3.New(sess, &aws.Config{Region: aws.String(p.region)}) 92 resp, err := svc.GetObject(&s3.GetObjectInput{Bucket: aws.String(p.bucket), Key: aws.String(p.key)}) 93 if err != nil { 94 if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NoSuchKey" { 95 return s, nil 96 } 97 return nil, err 98 } 99 defer resp.Body.Close() 100 if err := json.NewDecoder(resp.Body).Decode(&s.firstSeen); err != nil { 101 return nil, err 102 } 103 return s, nil 104 } 105 106 func (s *awsResourceSet) Save(sess *session.Session, p *s3path) error { 107 b, err := json.MarshalIndent(s.firstSeen, "", " ") 108 if err != nil { 109 return err 110 } 111 svc := s3.New(sess, &aws.Config{Region: aws.String(p.region)}) 112 _, err = svc.PutObject(&s3.PutObjectInput{ 113 Bucket: aws.String(p.bucket), 114 Key: aws.String(p.key), 115 Body: bytes.NewReader(b), 116 CacheControl: aws.String("max-age=0"), 117 }) 118 return err 119 } 120 121 // Mark marks a particular resource as currently present, and advises 122 // on whether it should be deleted. If Mark(r) returns true, the TTL 123 // has expired for r and it should be deleted. 124 func (s *awsResourceSet) Mark(r awsResource) bool { 125 arn := r.ARN() 126 now := time.Now() 127 128 s.marked[arn] = true 129 if t, ok := s.firstSeen[arn]; ok { 130 since := now.Sub(t) 131 if since > s.ttl { 132 s.swept = append(s.swept, arn) 133 return true 134 } 135 glog.V(1).Infof("%s: seen for %v", r.ARN(), since) 136 return false 137 } 138 s.firstSeen[arn] = now 139 glog.V(1).Infof("%s: first seen", r.ARN()) 140 if s.ttl == 0 { 141 // If the TTL is 0, it should be deleted now. 142 s.swept = append(s.swept, arn) 143 return true 144 } 145 return false 146 } 147 148 // MarkComplete figures out which ARNs were in previous passes but not 149 // this one, and eliminates them. It should only be run after all 150 // resources have been marked. 151 func (s *awsResourceSet) MarkComplete() int { 152 var gone []string 153 for arn := range s.firstSeen { 154 if !s.marked[arn] { 155 gone = append(gone, arn) 156 } 157 } 158 for _, arn := range gone { 159 glog.V(1).Infof("%s: deleted since last run", arn) 160 delete(s.firstSeen, arn) 161 } 162 if len(s.swept) > 0 { 163 glog.Errorf("%d resources swept: %v", len(s.swept), s.swept) 164 } 165 return len(s.swept) 166 } 167 168 // Instances: https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#EC2.DescribeInstances 169 170 type instances struct{} 171 172 func (instances) MarkAndSweep(sess *session.Session, acct string, region string, set *awsResourceSet) error { 173 svc := ec2.New(sess, &aws.Config{Region: aws.String(region)}) 174 175 inp := &ec2.DescribeInstancesInput{ 176 Filters: []*ec2.Filter{ 177 { 178 Name: aws.String("instance-state-name"), 179 Values: []*string{aws.String("running"), aws.String("pending")}, 180 }, 181 }, 182 } 183 184 var toDelete []*string // Paged call, defer deletion until we have the whole list. 185 if err := svc.DescribeInstancesPages(inp, func(page *ec2.DescribeInstancesOutput, _ bool) bool { 186 for _, res := range page.Reservations { 187 for _, inst := range res.Instances { 188 i := &instance{ 189 Account: acct, 190 Region: region, 191 InstanceID: *inst.InstanceId, 192 } 193 if set.Mark(i) { 194 glog.Warningf("%s: deleting %T: %v", i.ARN(), inst, inst) 195 toDelete = append(toDelete, inst.InstanceId) 196 } 197 } 198 } 199 return true 200 }); err != nil { 201 return err 202 } 203 if len(toDelete) > 0 { 204 // TODO(zmerlynn): In theory this should be split up into 205 // blocks of 1000, but burn that bridge if it ever happens... 206 _, err := svc.TerminateInstances(&ec2.TerminateInstancesInput{InstanceIds: toDelete}) 207 if err != nil { 208 glog.Warningf("termination failed: %v (for %v)", err, toDelete) 209 } 210 } 211 return nil 212 } 213 214 type instance struct { 215 Account string 216 Region string 217 InstanceID string 218 } 219 220 func (i instance) ARN() string { 221 return fmt.Sprintf("arn:aws:ec2:%s:%s:instance/%s", i.Region, i.Account, i.InstanceID) 222 } 223 224 // AutoScalingGroups: https://docs.aws.amazon.com/sdk-for-go/api/service/autoscaling/#AutoScaling.DescribeAutoScalingGroups 225 226 type autoScalingGroups struct{} 227 228 func (autoScalingGroups) MarkAndSweep(sess *session.Session, acct string, region string, set *awsResourceSet) error { 229 svc := autoscaling.New(sess, &aws.Config{Region: aws.String(region)}) 230 231 var toDelete []*autoScalingGroup // Paged call, defer deletion until we have the whole list. 232 if err := svc.DescribeAutoScalingGroupsPages(nil, func(page *autoscaling.DescribeAutoScalingGroupsOutput, _ bool) bool { 233 for _, asg := range page.AutoScalingGroups { 234 a := &autoScalingGroup{ID: *asg.AutoScalingGroupARN, Name: *asg.AutoScalingGroupName} 235 if set.Mark(a) { 236 glog.Warningf("%s: deleting %T: %v", a.ARN(), asg, asg) 237 toDelete = append(toDelete, a) 238 } 239 } 240 return true 241 }); err != nil { 242 return err 243 } 244 for _, asg := range toDelete { 245 _, err := svc.DeleteAutoScalingGroup( 246 &autoscaling.DeleteAutoScalingGroupInput{ 247 AutoScalingGroupName: aws.String(asg.Name), 248 ForceDelete: aws.Bool(true), 249 }) 250 if err != nil { 251 glog.Warningf("%v: delete failed: %v", asg.ARN(), err) 252 } 253 } 254 // Block on ASGs finishing deletion. There are a lot of dependent 255 // resources, so this just makes the rest go more smoothly (and 256 // prevents a second pass). 257 for _, asg := range toDelete { 258 glog.Warningf("%v: waiting for delete", asg.ARN()) 259 err := svc.WaitUntilGroupNotExists( 260 &autoscaling.DescribeAutoScalingGroupsInput{ 261 AutoScalingGroupNames: []*string{aws.String(asg.Name)}, 262 }) 263 if err != nil { 264 glog.Warningf("%v: wait failed: %v", asg.ARN(), err) 265 } 266 } 267 return nil 268 } 269 270 type autoScalingGroup struct { 271 ID string 272 Name string 273 } 274 275 func (asg autoScalingGroup) ARN() string { 276 return asg.ID 277 } 278 279 // LaunchConfigurations: http://docs.aws.amazon.com/sdk-for-go/api/service/autoscaling/#AutoScaling.DescribeLaunchConfigurations 280 281 type launchConfigurations struct{} 282 283 func (launchConfigurations) MarkAndSweep(sess *session.Session, acct string, region string, set *awsResourceSet) error { 284 svc := autoscaling.New(sess, &aws.Config{Region: aws.String(region)}) 285 286 var toDelete []*launchConfiguration // Paged call, defer deletion until we have the whole list. 287 if err := svc.DescribeLaunchConfigurationsPages(nil, func(page *autoscaling.DescribeLaunchConfigurationsOutput, _ bool) bool { 288 for _, lc := range page.LaunchConfigurations { 289 l := &launchConfiguration{ID: *lc.LaunchConfigurationARN, Name: *lc.LaunchConfigurationName} 290 if set.Mark(l) { 291 glog.Warningf("%s: deleting %T: %v", l.ARN(), lc, lc) 292 toDelete = append(toDelete, l) 293 } 294 } 295 return true 296 }); err != nil { 297 return err 298 } 299 for _, lc := range toDelete { 300 _, err := svc.DeleteLaunchConfiguration( 301 &autoscaling.DeleteLaunchConfigurationInput{ 302 LaunchConfigurationName: aws.String(lc.Name), 303 }) 304 if err != nil { 305 glog.Warningf("%v: delete failed: %v", lc.ARN(), err) 306 } 307 } 308 return nil 309 } 310 311 type launchConfiguration struct { 312 ID string 313 Name string 314 } 315 316 func (lc launchConfiguration) ARN() string { 317 return lc.ID 318 } 319 320 // Subnets: https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#EC2.DescribeSubnets 321 322 type subnets struct{} 323 324 func (subnets) MarkAndSweep(sess *session.Session, acct string, region string, set *awsResourceSet) error { 325 svc := ec2.New(sess, &aws.Config{Region: aws.String(region)}) 326 327 resp, err := svc.DescribeSubnets(&ec2.DescribeSubnetsInput{ 328 Filters: []*ec2.Filter{ 329 { 330 Name: aws.String("defaultForAz"), 331 Values: []*string{aws.String("false")}, 332 }, 333 }, 334 }) 335 if err != nil { 336 return err 337 } 338 339 for _, sub := range resp.Subnets { 340 s := &subnet{Account: acct, Region: region, ID: *sub.SubnetId} 341 if set.Mark(s) { 342 glog.Warningf("%s: deleting %T: %v", s.ARN(), sub, sub) 343 _, err := svc.DeleteSubnet(&ec2.DeleteSubnetInput{SubnetId: sub.SubnetId}) 344 if err != nil { 345 glog.Warningf("%v: delete failed: %v", s.ARN(), err) 346 } 347 } 348 } 349 return nil 350 } 351 352 type subnet struct { 353 Account string 354 Region string 355 ID string 356 } 357 358 func (sub subnet) ARN() string { 359 return fmt.Sprintf("arn:aws:ec2:%s:%s:subnet/%s", sub.Region, sub.Account, sub.ID) 360 } 361 362 // SecurityGroups: https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#EC2.DescribeSecurityGroups 363 364 type securityGroups struct{} 365 366 type sgRef struct { 367 id string 368 perm *ec2.IpPermission 369 } 370 371 func addRefs(refs map[string][]*sgRef, id string, acct string, perms []*ec2.IpPermission) { 372 for _, perm := range perms { 373 for _, pair := range perm.UserIdGroupPairs { 374 // Ignore cross-account for now, and skip circular refs. 375 if *pair.UserId == acct && *pair.GroupId != id { 376 refs[*pair.GroupId] = append(refs[*pair.GroupId], &sgRef{id: id, perm: perm}) 377 } 378 } 379 } 380 } 381 382 func (securityGroups) MarkAndSweep(sess *session.Session, acct string, region string, set *awsResourceSet) error { 383 svc := ec2.New(sess, &aws.Config{Region: aws.String(region)}) 384 385 resp, err := svc.DescribeSecurityGroups(nil) 386 if err != nil { 387 return err 388 } 389 390 var toDelete []*securityGroup // Deferred to disentangle referencing security groups 391 ingress := make(map[string][]*sgRef) // sg.GroupId -> [sg.GroupIds with this ingress] 392 egress := make(map[string][]*sgRef) // sg.GroupId -> [sg.GroupIds with this egress] 393 for _, sg := range resp.SecurityGroups { 394 if *sg.GroupName == "default" { 395 // TODO(zmerlynn): Is there really no better way to detect this? 396 continue 397 } 398 s := &securityGroup{Account: acct, Region: region, ID: *sg.GroupId} 399 addRefs(ingress, *sg.GroupId, acct, sg.IpPermissions) 400 addRefs(egress, *sg.GroupId, acct, sg.IpPermissionsEgress) 401 if set.Mark(s) { 402 glog.Warningf("%s: deleting %T: %v", s.ARN(), sg, sg) 403 toDelete = append(toDelete, s) 404 } 405 } 406 for _, sg := range toDelete { 407 for _, ref := range ingress[sg.ID] { 408 glog.Infof("%v: revoking reference from %v", sg.ARN(), ref.id) 409 _, err := svc.RevokeSecurityGroupIngress(&ec2.RevokeSecurityGroupIngressInput{ 410 GroupId: aws.String(ref.id), 411 IpPermissions: []*ec2.IpPermission{ref.perm}, 412 }) 413 if err != nil { 414 glog.Warningf("%v: failed to revoke ingress reference from %v: %v", sg.ARN(), ref.id, err) 415 } 416 } 417 for _, ref := range egress[sg.ID] { 418 _, err := svc.RevokeSecurityGroupEgress(&ec2.RevokeSecurityGroupEgressInput{ 419 GroupId: aws.String(ref.id), 420 IpPermissions: []*ec2.IpPermission{ref.perm}, 421 }) 422 if err != nil { 423 glog.Warningf("%v: failed to revoke egress reference from %v: %v", sg.ARN(), ref.id, err) 424 } 425 } 426 _, err := svc.DeleteSecurityGroup(&ec2.DeleteSecurityGroupInput{GroupId: aws.String(sg.ID)}) 427 if err != nil { 428 glog.Warningf("%v: delete failed: %v", sg.ARN(), err) 429 } 430 } 431 return nil 432 } 433 434 type securityGroup struct { 435 Account string 436 Region string 437 ID string 438 } 439 440 func (sg securityGroup) ARN() string { 441 return fmt.Sprintf("arn:aws:ec2:%s:%s:security-group/%s", sg.Region, sg.Account, sg.ID) 442 } 443 444 // InternetGateways: https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#EC2.DescribeInternetGateways 445 446 type internetGateways struct{} 447 448 func (internetGateways) MarkAndSweep(sess *session.Session, acct string, region string, set *awsResourceSet) error { 449 svc := ec2.New(sess, &aws.Config{Region: aws.String(region)}) 450 451 resp, err := svc.DescribeInternetGateways(nil) 452 if err != nil { 453 return err 454 } 455 456 for _, ig := range resp.InternetGateways { 457 i := &internetGateway{Account: acct, Region: region, ID: *ig.InternetGatewayId} 458 if set.Mark(i) { 459 glog.Warningf("%s: deleting %T: %v", i.ARN(), ig, ig) 460 for _, att := range ig.Attachments { 461 _, err := svc.DetachInternetGateway(&ec2.DetachInternetGatewayInput{ 462 InternetGatewayId: ig.InternetGatewayId, 463 VpcId: att.VpcId, 464 }) 465 if err != nil { 466 glog.Warningf("%v: detach from %v failed: %v", i.ARN(), *att.VpcId, err) 467 } 468 } 469 _, err := svc.DeleteInternetGateway(&ec2.DeleteInternetGatewayInput{InternetGatewayId: ig.InternetGatewayId}) 470 if err != nil { 471 glog.Warningf("%v: delete failed: %v", i.ARN(), err) 472 } 473 } 474 } 475 return nil 476 } 477 478 type internetGateway struct { 479 Account string 480 Region string 481 ID string 482 } 483 484 func (ig internetGateway) ARN() string { 485 return fmt.Sprintf("arn:aws:ec2:%s:%s:internet-gateway/%s", ig.Region, ig.Account, ig.ID) 486 } 487 488 // RouteTables: https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#EC2.DescribeRouteTables 489 490 type routeTables struct{} 491 492 func (routeTables) MarkAndSweep(sess *session.Session, acct string, region string, set *awsResourceSet) error { 493 svc := ec2.New(sess, &aws.Config{Region: aws.String(region)}) 494 495 resp, err := svc.DescribeRouteTables(nil) 496 if err != nil { 497 return err 498 } 499 500 for _, rt := range resp.RouteTables { 501 // Filter out the RouteTables that have a main 502 // association. Given the documention for the main.association 503 // filter, you'd think we could filter on the Describe, but it 504 // doesn't actually work, see e.g. 505 // https://github.com/aws/aws-cli/issues/1810 506 main := false 507 for _, assoc := range rt.Associations { 508 main = main || *assoc.Main 509 } 510 if main { 511 continue 512 } 513 r := &routeTable{Account: acct, Region: region, ID: *rt.RouteTableId} 514 if set.Mark(r) { 515 for _, assoc := range rt.Associations { 516 glog.Infof("%v: disassociating from %v", r.ARN(), *assoc.SubnetId) 517 _, err := svc.DisassociateRouteTable(&ec2.DisassociateRouteTableInput{ 518 AssociationId: assoc.RouteTableAssociationId}) 519 if err != nil { 520 glog.Warningf("%v: disassociation from subnet %v failed: %v", r.ARN(), *assoc.SubnetId, err) 521 } 522 } 523 glog.Warningf("%s: deleting %T: %v", r.ARN(), rt, rt) 524 _, err := svc.DeleteRouteTable(&ec2.DeleteRouteTableInput{RouteTableId: rt.RouteTableId}) 525 if err != nil { 526 glog.Warningf("%v: delete failed: %v", r.ARN(), err) 527 } 528 } 529 } 530 return nil 531 } 532 533 type routeTable struct { 534 Account string 535 Region string 536 ID string 537 } 538 539 func (rt routeTable) ARN() string { 540 return fmt.Sprintf("arn:aws:ec2:%s:%s:route-table/%s", rt.Region, rt.Account, rt.ID) 541 } 542 543 // VPCs: https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#EC2.DescribeVpcs 544 545 type vpcs struct{} 546 547 func (vpcs) MarkAndSweep(sess *session.Session, acct string, region string, set *awsResourceSet) error { 548 svc := ec2.New(sess, &aws.Config{Region: aws.String(region)}) 549 550 resp, err := svc.DescribeVpcs(&ec2.DescribeVpcsInput{ 551 Filters: []*ec2.Filter{ 552 { 553 Name: aws.String("isDefault"), 554 Values: []*string{aws.String("false")}, 555 }, 556 }, 557 }) 558 if err != nil { 559 return err 560 } 561 562 for _, vp := range resp.Vpcs { 563 v := &vpc{Account: acct, Region: region, ID: *vp.VpcId} 564 if set.Mark(v) { 565 glog.Warningf("%s: deleting %T: %v", v.ARN(), vp, vp) 566 if vp.DhcpOptionsId != nil && *vp.DhcpOptionsId != "default" { 567 _, err := svc.AssociateDhcpOptions(&ec2.AssociateDhcpOptionsInput{ 568 VpcId: vp.VpcId, 569 DhcpOptionsId: aws.String("default"), 570 }) 571 if err != nil { 572 glog.Warning("%v: disassociating DHCP option set %v failed: %v", v.ARN(), vp.DhcpOptionsId, err) 573 } 574 } 575 _, err := svc.DeleteVpc(&ec2.DeleteVpcInput{VpcId: vp.VpcId}) 576 if err != nil { 577 glog.Warningf("%v: delete failed: %v", v.ARN(), err) 578 } 579 } 580 } 581 return nil 582 } 583 584 type vpc struct { 585 Account string 586 Region string 587 ID string 588 } 589 590 func (vp vpc) ARN() string { 591 return fmt.Sprintf("arn:aws:ec2:%s:%s:vpc/%s", vp.Region, vp.Account, vp.ID) 592 } 593 594 // DhcpOptions: https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#EC2.DescribeDhcpOptions 595 596 type dhcpOptions struct{} 597 598 func (dhcpOptions) MarkAndSweep(sess *session.Session, acct string, region string, set *awsResourceSet) error { 599 svc := ec2.New(sess, &aws.Config{Region: aws.String(region)}) 600 601 // This is a little gross, but I can't find an easier way to 602 // figure out the DhcpOptions associated with the default VPC. 603 defaultRefs := make(map[string]bool) 604 { 605 resp, err := svc.DescribeVpcs(&ec2.DescribeVpcsInput{ 606 Filters: []*ec2.Filter{ 607 { 608 Name: aws.String("isDefault"), 609 Values: []*string{aws.String("true")}, 610 }, 611 }, 612 }) 613 if err != nil { 614 return err 615 } 616 for _, vpc := range resp.Vpcs { 617 defaultRefs[*vpc.DhcpOptionsId] = true 618 } 619 } 620 621 resp, err := svc.DescribeDhcpOptions(nil) 622 if err != nil { 623 return err 624 } 625 626 var defaults []string 627 for _, dhcp := range resp.DhcpOptions { 628 if defaultRefs[*dhcp.DhcpOptionsId] { 629 continue 630 } 631 // Separately, skip any "default looking" DHCP Option Sets. See comment below. 632 if defaultLookingDHCPOptions(dhcp, region) { 633 defaults = append(defaults, *dhcp.DhcpOptionsId) 634 continue 635 } 636 dh := &dhcpOption{Account: acct, Region: region, ID: *dhcp.DhcpOptionsId} 637 if set.Mark(dh) { 638 glog.Warningf("%s: deleting %T: %v", dh.ARN(), dhcp, dhcp) 639 _, err := svc.DeleteDhcpOptions(&ec2.DeleteDhcpOptionsInput{DhcpOptionsId: dhcp.DhcpOptionsId}) 640 if err != nil { 641 glog.Warningf("%v: delete failed: %v", dh.ARN(), err) 642 } 643 } 644 } 645 if len(defaults) > 1 { 646 glog.Errorf("Found more than one default-looking DHCP option set: %v", defaults) 647 } 648 return nil 649 } 650 651 // defaultLookingDHCPOptions: This part is a little annoying. If 652 // you're running in a region with where there is no default-looking 653 // DHCP option set, when you create any VPC, AWS will create a 654 // default-looking DHCP option set for you. If you then re-associate 655 // or delete the VPC, the option set will hang around. However, if you 656 // have a default-looking DHCP option set (even with no default VPC) 657 // and create a VPC, AWS will associate the VPC with the DHCP option 658 // set of the default VPC. There's no signal as to whether the option 659 // set returned is the default or was created along with the 660 // VPC. Because of this, we just skip these during cleanup - there 661 // will only ever be one default set per region. 662 func defaultLookingDHCPOptions(dhcp *ec2.DhcpOptions, region string) bool { 663 if len(dhcp.Tags) != 0 { 664 return false 665 } 666 for _, conf := range dhcp.DhcpConfigurations { 667 if *conf.Key == "domain-name" { 668 var domain string 669 if region == "us-east-1" { 670 domain = "ec2.internal" 671 } else { 672 domain = region + ".compute.internal" 673 } 674 if len(conf.Values) != 1 || *conf.Values[0].Value != domain { 675 return false 676 } 677 } else if *conf.Key == "domain-name-servers" { 678 if len(conf.Values) != 1 || *conf.Values[0].Value != "AmazonProvidedDNS" { 679 return false 680 } 681 } else { 682 return false 683 } 684 } 685 return true 686 } 687 688 type dhcpOption struct { 689 Account string 690 Region string 691 ID string 692 } 693 694 func (dhcp dhcpOption) ARN() string { 695 return fmt.Sprintf("arn:aws:ec2:%s:%s:dhcp-option/%s", dhcp.Region, dhcp.Account, dhcp.ID) 696 } 697 698 // Volumes: https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#EC2.DescribeVolumes 699 700 type volumes struct{} 701 702 func (volumes) MarkAndSweep(sess *session.Session, acct string, region string, set *awsResourceSet) error { 703 svc := ec2.New(sess, &aws.Config{Region: aws.String(region)}) 704 705 var toDelete []*volume // Paged call, defer deletion until we have the whole list. 706 if err := svc.DescribeVolumesPages(nil, func(page *ec2.DescribeVolumesOutput, _ bool) bool { 707 for _, vol := range page.Volumes { 708 v := &volume{Account: acct, Region: region, ID: *vol.VolumeId} 709 if set.Mark(v) { 710 glog.Warningf("%s: deleting %T: %v", v.ARN(), vol, vol) 711 toDelete = append(toDelete, v) 712 } 713 } 714 return true 715 }); err != nil { 716 return err 717 } 718 for _, vol := range toDelete { 719 _, err := svc.DeleteVolume(&ec2.DeleteVolumeInput{VolumeId: aws.String(vol.ID)}) 720 if err != nil { 721 glog.Warningf("%v: delete failed: %v", vol.ARN(), err) 722 } 723 } 724 return nil 725 } 726 727 type volume struct { 728 Account string 729 Region string 730 ID string 731 } 732 733 func (vol volume) ARN() string { 734 return fmt.Sprintf("arn:aws:ec2:%s:%s:volume/%s", vol.Region, vol.Account, vol.ID) 735 } 736 737 // Elastic IPs: https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#EC2.DescribeAddresses 738 739 type addresses struct{} 740 741 func (addresses) MarkAndSweep(sess *session.Session, acct string, region string, set *awsResourceSet) error { 742 svc := ec2.New(sess, &aws.Config{Region: aws.String(region)}) 743 744 resp, err := svc.DescribeAddresses(nil) 745 if err != nil { 746 return err 747 } 748 749 for _, addr := range resp.Addresses { 750 a := &address{Account: acct, Region: region, ID: *addr.AllocationId} 751 if set.Mark(a) { 752 glog.Warningf("%s: deleting %T: %v", a.ARN(), addr, addr) 753 if addr.AssociationId != nil { 754 glog.Warningf("%s: disassociating %T from active instance", a.ARN(), addr) 755 _, err := svc.DisassociateAddress(&ec2.DisassociateAddressInput{AssociationId: addr.AssociationId}) 756 if err != nil { 757 glog.Warningf("%s: disassociating %T failed: %v", a.ARN(), addr, err) 758 } 759 } 760 _, err := svc.ReleaseAddress(&ec2.ReleaseAddressInput{AllocationId: addr.AllocationId}) 761 if err != nil { 762 glog.Warningf("%v: delete failed: %v", a.ARN(), err) 763 } 764 } 765 } 766 return nil 767 } 768 769 type address struct { 770 Account string 771 Region string 772 ID string 773 } 774 775 func (addr address) ARN() string { 776 // This ARN is a complete hallucination - there doesn't seem to be 777 // an ARN for elastic IPs. 778 return fmt.Sprintf("arn:aws:ec2:%s:%s:address/%s", addr.Region, addr.Account, addr.ID) 779 } 780 781 // ARNs (used for uniquifying within our previous mark file) 782 783 type arn struct { 784 partition string 785 service string 786 region string 787 account string 788 resourceType string 789 resource string 790 } 791 792 func parseARN(s string) (*arn, error) { 793 pieces := strings.Split(s, ":") 794 if len(pieces) != 6 || pieces[0] != "arn" || pieces[1] != "aws" { 795 return nil, fmt.Errorf("Invalid AWS ARN: %v", s) 796 } 797 var resourceType string 798 var resource string 799 res := strings.SplitN(pieces[5], "/", 2) 800 if len(res) == 1 { 801 resource = res[0] 802 } else { 803 resourceType = res[0] 804 resource = res[1] 805 } 806 return &arn{ 807 partition: pieces[1], 808 service: pieces[2], 809 region: pieces[3], 810 account: pieces[4], 811 resourceType: resourceType, 812 resource: resource, 813 }, nil 814 } 815 816 func getAccount(sess *session.Session, region string) (string, error) { 817 svc := iam.New(sess, &aws.Config{Region: aws.String(region)}) 818 resp, err := svc.GetUser(nil) 819 if err != nil { 820 return "", err 821 } 822 arn, err := parseARN(*resp.User.Arn) 823 if err != nil { 824 return "", err 825 } 826 return arn.account, nil 827 } 828 829 type s3path struct { 830 region string 831 bucket string 832 key string 833 } 834 835 func getS3Path(sess *session.Session, s string) (*s3path, error) { 836 url, err := url.Parse(s) 837 if err != nil { 838 return nil, err 839 } 840 if url.Scheme != "s3" { 841 return nil, fmt.Errorf("Scheme %q != 's3'", url.Scheme) 842 } 843 svc := s3.New(sess, &aws.Config{Region: aws.String(defaultRegion)}) 844 resp, err := svc.GetBucketLocation(&s3.GetBucketLocationInput{Bucket: aws.String(url.Host)}) 845 if err != nil { 846 return nil, err 847 } 848 region := "us-east-1" 849 if resp.LocationConstraint != nil { 850 region = *resp.LocationConstraint 851 } 852 return &s3path{region: region, bucket: url.Host, key: url.Path}, nil 853 } 854 855 func getRegions(sess *session.Session) ([]string, error) { 856 var regions []string 857 svc := ec2.New(sess, &aws.Config{Region: aws.String(defaultRegion)}) 858 resp, err := svc.DescribeRegions(nil) 859 if err != nil { 860 return nil, err 861 } 862 for _, region := range resp.Regions { 863 regions = append(regions, *region.RegionName) 864 } 865 return regions, nil 866 } 867 868 func main() { 869 flag.Lookup("logtostderr").Value.Set("true") 870 flag.Parse() 871 872 // Retry aggressively (with default back-off). If the account is 873 // in a really bad state, we may be contending with API rate 874 // limiting and fighting against the very resources we're trying 875 // to delete. 876 sess := session.Must(session.NewSessionWithOptions(session.Options{Config: aws.Config{MaxRetries: aws.Int(100)}})) 877 878 s3p, err := getS3Path(sess, *path) 879 if err != nil { 880 glog.Fatalf("--path %q isn't a valid S3 path: %v", *path, err) 881 } 882 acct, err := getAccount(sess, defaultRegion) 883 if err != nil { 884 glog.Fatalf("error getting current user: %v", err) 885 } 886 glog.V(1).Infof("account: %s", acct) 887 regions, err := getRegions(sess) 888 if err != nil { 889 glog.Fatalf("error getting available regions: %v", err) 890 } 891 glog.V(1).Infof("regions: %v", regions) 892 893 res, err := LoadResourceSet(sess, s3p, *maxTTL) 894 if err != nil { 895 glog.Fatalf("error loading %q: %v", *path, err) 896 } 897 for _, region := range regions { 898 for _, typ := range awsResourceTypes { 899 if err := typ.MarkAndSweep(sess, acct, region, res); err != nil { 900 glog.Errorf("error sweeping %T: %v", typ, err) 901 return 902 } 903 } 904 } 905 swept := res.MarkComplete() 906 if err := res.Save(sess, s3p); err != nil { 907 glog.Fatalf("error saving %q: %v", *path, err) 908 } 909 if swept > 0 { 910 os.Exit(1) 911 } 912 }