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  }