github.com/openshift/installer@v1.4.17/pkg/destroy/aws/aws.go (about)

     1  package aws
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/aws/aws-sdk-go/aws"
    10  	"github.com/aws/aws-sdk-go/aws/arn"
    11  	"github.com/aws/aws-sdk-go/aws/awserr"
    12  	"github.com/aws/aws-sdk-go/aws/endpoints"
    13  	"github.com/aws/aws-sdk-go/aws/request"
    14  	"github.com/aws/aws-sdk-go/aws/session"
    15  	"github.com/aws/aws-sdk-go/service/efs"
    16  	"github.com/aws/aws-sdk-go/service/iam"
    17  	"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi"
    18  	"github.com/aws/aws-sdk-go/service/route53"
    19  	"github.com/aws/aws-sdk-go/service/s3"
    20  	"github.com/aws/aws-sdk-go/service/s3/s3manager"
    21  	"github.com/pkg/errors"
    22  	"github.com/sirupsen/logrus"
    23  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    24  	"k8s.io/apimachinery/pkg/util/sets"
    25  	"k8s.io/apimachinery/pkg/util/wait"
    26  
    27  	awssession "github.com/openshift/installer/pkg/asset/installconfig/aws"
    28  	"github.com/openshift/installer/pkg/destroy/providers"
    29  	"github.com/openshift/installer/pkg/types"
    30  	"github.com/openshift/installer/pkg/version"
    31  )
    32  
    33  var exists = struct{}{}
    34  
    35  // Filter holds the key/value pairs for the tags we will be matching against.
    36  //
    37  // A resource matches the filter if all of the key/value pairs are in its tags.
    38  type Filter map[string]string
    39  
    40  // ClusterUninstaller holds the various options for the cluster we want to delete
    41  type ClusterUninstaller struct {
    42  	// Filters is a slice of filters for matching resources.  A
    43  	// resources matches the whole slice if it matches any of the
    44  	// entries.  For example:
    45  	//
    46  	//   filter := []map[string]string{
    47  	//     {
    48  	//       "a": "b",
    49  	//       "c": "d:,
    50  	//     },
    51  	//     {
    52  	//       "d": "e",
    53  	//     },
    54  	//   }
    55  	//
    56  	// will match resources with (a:b and c:d) or d:e.
    57  	Filters        []Filter // filter(s) we will be searching for
    58  	Logger         logrus.FieldLogger
    59  	Region         string
    60  	ClusterID      string
    61  	ClusterDomain  string
    62  	HostedZoneRole string
    63  
    64  	// Session is the AWS session to be used for deletion.  If nil, a
    65  	// new session will be created based on the usual credential
    66  	// configuration (AWS_PROFILE, AWS_ACCESS_KEY_ID, etc.).
    67  	Session *session.Session
    68  }
    69  
    70  // New returns an AWS destroyer from ClusterMetadata.
    71  func New(logger logrus.FieldLogger, metadata *types.ClusterMetadata) (providers.Destroyer, error) {
    72  	filters := make([]Filter, 0, len(metadata.ClusterPlatformMetadata.AWS.Identifier))
    73  	for _, filter := range metadata.ClusterPlatformMetadata.AWS.Identifier {
    74  		filters = append(filters, filter)
    75  	}
    76  	region := metadata.ClusterPlatformMetadata.AWS.Region
    77  	session, err := awssession.GetSessionWithOptions(
    78  		awssession.WithRegion(region),
    79  		awssession.WithServiceEndpoints(region, metadata.ClusterPlatformMetadata.AWS.ServiceEndpoints),
    80  	)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  
    85  	return &ClusterUninstaller{
    86  		Filters:        filters,
    87  		Region:         region,
    88  		Logger:         logger,
    89  		ClusterID:      metadata.InfraID,
    90  		ClusterDomain:  metadata.AWS.ClusterDomain,
    91  		Session:        session,
    92  		HostedZoneRole: metadata.AWS.HostedZoneRole,
    93  	}, nil
    94  }
    95  
    96  func (o *ClusterUninstaller) validate() error {
    97  	if len(o.Filters) == 0 {
    98  		return errors.Errorf("you must specify at least one tag filter")
    99  	}
   100  	return nil
   101  }
   102  
   103  // Run is the entrypoint to start the uninstall process
   104  func (o *ClusterUninstaller) Run() (*types.ClusterQuota, error) {
   105  	_, err := o.RunWithContext(context.Background())
   106  	return nil, err
   107  }
   108  
   109  // RunWithContext runs the uninstall process with a context.
   110  // The first return is the list of ARNs for resources that could not be destroyed.
   111  func (o *ClusterUninstaller) RunWithContext(ctx context.Context) ([]string, error) {
   112  	err := o.validate()
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  
   117  	awsSession := o.Session
   118  	if awsSession == nil {
   119  		// Relying on appropriate AWS ENV vars (eg AWS_PROFILE, AWS_ACCESS_KEY_ID, etc)
   120  		awsSession, err = session.NewSession(aws.NewConfig().WithRegion(o.Region))
   121  		if err != nil {
   122  			return nil, err
   123  		}
   124  	}
   125  	awsSession.Handlers.Build.PushBackNamed(request.NamedHandler{
   126  		Name: "openshiftInstaller.OpenshiftInstallerUserAgentHandler",
   127  		Fn:   request.MakeAddToUserAgentHandler("OpenShift/4.x Destroyer", version.Raw),
   128  	})
   129  
   130  	tagClients := []*resourcegroupstaggingapi.ResourceGroupsTaggingAPI{
   131  		resourcegroupstaggingapi.New(awsSession),
   132  	}
   133  
   134  	if o.HostedZoneRole != "" {
   135  		cfg := awssession.GetR53ClientCfg(awsSession, o.HostedZoneRole)
   136  		// This client is specifically for finding route53 zones,
   137  		// so it needs to use the global us-east-1 region.
   138  		cfg.Region = aws.String(endpoints.UsEast1RegionID)
   139  		tagClients = append(tagClients, resourcegroupstaggingapi.New(awsSession, cfg))
   140  	}
   141  
   142  	switch o.Region {
   143  	case endpoints.CnNorth1RegionID, endpoints.CnNorthwest1RegionID:
   144  		break
   145  	case endpoints.UsIsoEast1RegionID, endpoints.UsIsoWest1RegionID, endpoints.UsIsobEast1RegionID:
   146  		break
   147  	case endpoints.UsGovEast1RegionID, endpoints.UsGovWest1RegionID:
   148  		if o.Region != endpoints.UsGovWest1RegionID {
   149  			tagClients = append(tagClients,
   150  				resourcegroupstaggingapi.New(awsSession, aws.NewConfig().WithRegion(endpoints.UsGovWest1RegionID)))
   151  		}
   152  	default:
   153  		if o.Region != endpoints.UsEast1RegionID {
   154  			tagClients = append(tagClients,
   155  				resourcegroupstaggingapi.New(awsSession, aws.NewConfig().WithRegion(endpoints.UsEast1RegionID)))
   156  		}
   157  	}
   158  
   159  	iamClient := iam.New(awsSession)
   160  	iamRoleSearch := &IamRoleSearch{
   161  		Client:  iamClient,
   162  		Filters: o.Filters,
   163  		Logger:  o.Logger,
   164  	}
   165  	iamUserSearch := &IamUserSearch{
   166  		client:  iamClient,
   167  		filters: o.Filters,
   168  		logger:  o.Logger,
   169  	}
   170  
   171  	// Get the initial resources to delete, so that they can be returned if the context is canceled while terminating
   172  	// instances.
   173  	deleted := sets.New[string]()
   174  	resourcesToDelete, tagClientsWithResources, err := o.findResourcesToDelete(ctx, tagClients, iamClient, iamRoleSearch, iamUserSearch, deleted)
   175  	if err != nil {
   176  		o.Logger.WithError(err).Info("error while finding resources to delete")
   177  		if err := ctx.Err(); err != nil {
   178  			return resourcesToDelete.UnsortedList(), err
   179  		}
   180  	}
   181  
   182  	tracker := new(ErrorTracker)
   183  
   184  	// Terminate EC2 instances. The instances need to be terminated first so that we can ensure that there is nothing
   185  	// running on the cluster creating new resources while we are attempting to delete resources, which could leak
   186  	// the new resources.
   187  	err = DeleteEC2Instances(ctx, o.Logger, awsSession, o.Filters, resourcesToDelete, deleted, tracker)
   188  	if err != nil {
   189  		return resourcesToDelete.UnsortedList(), err
   190  	}
   191  
   192  	// Delete the rest of the resources.
   193  	err = wait.PollImmediateUntil(
   194  		time.Second*10,
   195  		func() (done bool, err error) {
   196  			newlyDeleted, loopError := DeleteResources(ctx, o.Logger, awsSession, resourcesToDelete.UnsortedList(), tracker)
   197  			// Delete from the resources-to-delete set so that the current state of the resources to delete can be
   198  			// returned if the context is completed.
   199  			resourcesToDelete = resourcesToDelete.Difference(newlyDeleted)
   200  			deleted = deleted.Union(newlyDeleted)
   201  			if loopError != nil {
   202  				if err := ctx.Err(); err != nil {
   203  					return false, err
   204  				}
   205  			}
   206  			// Store resources to delete in a temporary variable so that, in case the context is cancelled, the current
   207  			// resources to delete are not lost.
   208  			nextResourcesToDelete, nextTagClients, err := o.findResourcesToDelete(ctx, tagClientsWithResources, iamClient, iamRoleSearch, iamUserSearch, deleted)
   209  			if err != nil {
   210  				o.Logger.WithError(err).Info("error while finding resources to delete")
   211  				if err := ctx.Err(); err != nil {
   212  					return false, err
   213  				}
   214  				loopError = errors.Wrap(err, "error while finding resources to delete")
   215  			}
   216  			resourcesToDelete = nextResourcesToDelete
   217  			tagClientsWithResources = nextTagClients
   218  			return len(resourcesToDelete) == 0 && loopError == nil, nil
   219  		},
   220  		ctx.Done(),
   221  	)
   222  	if err != nil {
   223  		return resourcesToDelete.UnsortedList(), err
   224  	}
   225  
   226  	err = o.removeSharedTags(ctx, awsSession, tagClients, tracker)
   227  	if err != nil {
   228  		return nil, err
   229  	}
   230  
   231  	return nil, nil
   232  }
   233  
   234  // findUntaggableResources returns the resources for the cluster that cannot be tagged. Any resource that contains
   235  // a shared tag will be ignored.
   236  //
   237  //	deleted - the resources that have already been deleted. Any resources specified in this set will be ignored.
   238  func (o *ClusterUninstaller) findUntaggableResources(ctx context.Context, iamClient *iam.IAM, deleted sets.Set[string]) (sets.Set[string], error) {
   239  	resources := sets.New[string]()
   240  	o.Logger.Debug("search for IAM instance profiles")
   241  	for _, profileType := range []string{"master", "worker", "bootstrap"} {
   242  		profile := fmt.Sprintf("%s-%s-profile", o.ClusterID, profileType)
   243  		response, err := iamClient.GetInstanceProfileWithContext(ctx, &iam.GetInstanceProfileInput{InstanceProfileName: &profile})
   244  		if err != nil {
   245  			var awsErr awserr.Error
   246  			if errors.As(err, &awsErr) && awsErr.Code() == iam.ErrCodeNoSuchEntityException {
   247  				continue
   248  			}
   249  			return resources, fmt.Errorf("failed to get IAM instance profile: %w", err)
   250  		}
   251  		arnString := *response.InstanceProfile.Arn
   252  		if !deleted.Has(arnString) {
   253  			resources.Insert(arnString)
   254  		}
   255  	}
   256  	return resources, nil
   257  }
   258  
   259  // findResourcesToDelete returns the resources that should be deleted.
   260  //
   261  // tagClients - clients of the tagging API to use to search for resources.
   262  // deleted - the resources that have already been deleted. Any resources specified in this set will be ignored.
   263  func (o *ClusterUninstaller) findResourcesToDelete(
   264  	ctx context.Context,
   265  	tagClients []*resourcegroupstaggingapi.ResourceGroupsTaggingAPI,
   266  	iamClient *iam.IAM,
   267  	iamRoleSearch *IamRoleSearch,
   268  	iamUserSearch *IamUserSearch,
   269  	deleted sets.Set[string],
   270  ) (sets.Set[string], []*resourcegroupstaggingapi.ResourceGroupsTaggingAPI, error) {
   271  	var errs []error
   272  	resources, tagClients, err := FindTaggedResourcesToDelete(ctx, o.Logger, tagClients, o.Filters, iamRoleSearch, iamUserSearch, deleted)
   273  	if err != nil {
   274  		errs = append(errs, err)
   275  	}
   276  
   277  	// Find untaggable resources
   278  	untaggableResources, err := o.findUntaggableResources(ctx, iamClient, deleted)
   279  	if err != nil {
   280  		errs = append(errs, err)
   281  	}
   282  	resources = resources.Union(untaggableResources)
   283  
   284  	return resources, tagClients, utilerrors.NewAggregate(errs)
   285  }
   286  
   287  // FindTaggedResourcesToDelete returns the tagged resources that should be deleted.
   288  //
   289  //	tagClients - clients of the tagging API to use to search for resources.
   290  //	deleted - the resources that have already been deleted. Any resources specified in this set will be ignored.
   291  func FindTaggedResourcesToDelete(
   292  	ctx context.Context,
   293  	logger logrus.FieldLogger,
   294  	tagClients []*resourcegroupstaggingapi.ResourceGroupsTaggingAPI,
   295  	filters []Filter,
   296  	iamRoleSearch *IamRoleSearch,
   297  	iamUserSearch *IamUserSearch,
   298  	deleted sets.Set[string],
   299  ) (sets.Set[string], []*resourcegroupstaggingapi.ResourceGroupsTaggingAPI, error) {
   300  	resources := sets.New[string]()
   301  	var tagClientsWithResources []*resourcegroupstaggingapi.ResourceGroupsTaggingAPI
   302  	var errs []error
   303  
   304  	// Find resources by tag
   305  	for _, tagClient := range tagClients {
   306  		resourcesInTagClient, err := findResourcesByTag(ctx, logger, tagClient, filters, deleted)
   307  		if err != nil {
   308  			errs = append(errs, err)
   309  		}
   310  		resources = resources.Union(resourcesInTagClient)
   311  		// If there are still resources to be deleted for the tag client or if there was an error getting the resources
   312  		// for the tag client, then retain the tag client for future queries.
   313  		if len(resourcesInTagClient) > 0 || err != nil {
   314  			tagClientsWithResources = append(tagClientsWithResources, tagClient)
   315  		} else {
   316  			logger.Debugf("no deletions from %s, removing client", *tagClient.Config.Region)
   317  		}
   318  	}
   319  
   320  	// Find IAM roles
   321  	if iamRoleSearch != nil {
   322  		iamRoleResources, err := findIAMRoles(ctx, iamRoleSearch, deleted, logger)
   323  		if err != nil {
   324  			errs = append(errs, err)
   325  		}
   326  		resources = resources.Union(iamRoleResources)
   327  	}
   328  
   329  	// Find IAM users
   330  	if iamUserSearch != nil {
   331  		iamUserResources, err := findIAMUsers(ctx, iamUserSearch, deleted, logger)
   332  		if err != nil {
   333  			errs = append(errs, err)
   334  		}
   335  		resources = resources.Union(iamUserResources)
   336  	}
   337  
   338  	return resources, tagClientsWithResources, utilerrors.NewAggregate(errs)
   339  }
   340  
   341  // findResourcesByTag returns the resources with tags that satisfy the filters.
   342  //
   343  //	tagClients - clients of the tagging API to use to search for resources.
   344  //	deleted - the resources that have already been deleted. Any resources specified in this set will be ignored.
   345  func findResourcesByTag(
   346  	ctx context.Context,
   347  	logger logrus.FieldLogger,
   348  	tagClient *resourcegroupstaggingapi.ResourceGroupsTaggingAPI,
   349  	filters []Filter,
   350  	deleted sets.Set[string],
   351  ) (sets.Set[string], error) {
   352  	resources := sets.New[string]()
   353  	for _, filter := range filters {
   354  		logger.Debugf("search for matching resources by tag in %s matching %#+v", *tagClient.Config.Region, filter)
   355  		tagFilters := make([]*resourcegroupstaggingapi.TagFilter, 0, len(filter))
   356  		for key, value := range filter {
   357  			tagFilters = append(tagFilters, &resourcegroupstaggingapi.TagFilter{
   358  				Key:    aws.String(key),
   359  				Values: []*string{aws.String(value)},
   360  			})
   361  		}
   362  		err := tagClient.GetResourcesPagesWithContext(
   363  			ctx,
   364  			&resourcegroupstaggingapi.GetResourcesInput{TagFilters: tagFilters},
   365  			func(results *resourcegroupstaggingapi.GetResourcesOutput, lastPage bool) bool {
   366  				for _, resource := range results.ResourceTagMappingList {
   367  					arnString := *resource.ResourceARN
   368  					if !deleted.Has(arnString) {
   369  						resources.Insert(arnString)
   370  					}
   371  				}
   372  				return !lastPage
   373  			},
   374  		)
   375  		if err != nil {
   376  			err = errors.Wrap(err, "get tagged resources")
   377  			logger.Info(err)
   378  			return resources, err
   379  		}
   380  	}
   381  	return resources, nil
   382  }
   383  
   384  // DeleteResources deletes the specified resources.
   385  //
   386  //	resources - the resources to be deleted.
   387  //
   388  // The first return is the ARNs of the resources that were successfully deleted
   389  func DeleteResources(ctx context.Context, logger logrus.FieldLogger, awsSession *session.Session, resources []string, tracker *ErrorTracker) (sets.Set[string], error) {
   390  	deleted := sets.New[string]()
   391  	for _, arnString := range resources {
   392  		l := logger.WithField("arn", arnString)
   393  		parsedARN, err := arn.Parse(arnString)
   394  		if err != nil {
   395  			l.WithError(err).Debug("could not parse ARN")
   396  			continue
   397  		}
   398  		if err := deleteARN(ctx, awsSession, parsedARN, logger); err != nil {
   399  			tracker.suppressWarning(arnString, err, l)
   400  			if err := ctx.Err(); err != nil {
   401  				return deleted, err
   402  			}
   403  			continue
   404  		}
   405  		deleted.Insert(arnString)
   406  	}
   407  	return deleted, nil
   408  }
   409  
   410  func splitSlash(name string, input string) (base string, suffix string, err error) {
   411  	segments := strings.SplitN(input, "/", 2)
   412  	if len(segments) != 2 {
   413  		return "", "", errors.Errorf("%s %q does not contain the expected slash", name, input)
   414  	}
   415  	return segments[0], segments[1], nil
   416  }
   417  
   418  func tagMatch(filters []Filter, tags map[string]string) bool {
   419  	for _, filter := range filters {
   420  		match := true
   421  		for filterKey, filterValue := range filter {
   422  			tagValue, ok := tags[filterKey]
   423  			if !ok {
   424  				match = false
   425  				break
   426  			}
   427  			if tagValue != filterValue {
   428  				match = false
   429  				break
   430  			}
   431  		}
   432  		if match {
   433  			return true
   434  		}
   435  	}
   436  	return len(filters) == 0
   437  }
   438  
   439  // getPublicHostedZone will find the ID of the non-Terraform-managed public route53 zone given the
   440  // Terraform-managed zone's privateID.
   441  func getPublicHostedZone(ctx context.Context, client *route53.Route53, privateID string, logger logrus.FieldLogger) (string, error) {
   442  	response, err := client.GetHostedZoneWithContext(ctx, &route53.GetHostedZoneInput{
   443  		Id: aws.String(privateID),
   444  	})
   445  	if err != nil {
   446  		return "", err
   447  	}
   448  
   449  	privateName := *response.HostedZone.Name
   450  
   451  	if response.HostedZone.Config != nil && response.HostedZone.Config.PrivateZone != nil {
   452  		if !*response.HostedZone.Config.PrivateZone {
   453  			return "", errors.Errorf("getPublicHostedZone requires a private ID, but was passed the public %s", privateID)
   454  		}
   455  	} else {
   456  		logger.WithField("hosted zone", privateName).Warn("could not determine whether hosted zone is private")
   457  	}
   458  
   459  	return findAncestorPublicRoute53(ctx, client, privateName, logger)
   460  }
   461  
   462  // findAncestorPublicRoute53 finds a public route53 zone with the closest ancestor or match to dnsName.
   463  // It returns "", when no public route53 zone could be found.
   464  func findAncestorPublicRoute53(ctx context.Context, client *route53.Route53, dnsName string, logger logrus.FieldLogger) (string, error) {
   465  	for len(dnsName) > 0 {
   466  		sZone, err := findPublicRoute53(ctx, client, dnsName, logger)
   467  		if err != nil {
   468  			return "", err
   469  		}
   470  		if sZone != "" {
   471  			return sZone, nil
   472  		}
   473  
   474  		idx := strings.Index(dnsName, ".")
   475  		if idx == -1 {
   476  			break
   477  		}
   478  		dnsName = dnsName[idx+1:]
   479  	}
   480  	return "", nil
   481  }
   482  
   483  // findPublicRoute53 finds a public route53 zone matching the dnsName.
   484  // It returns "", when no public route53 zone could be found.
   485  func findPublicRoute53(ctx context.Context, client *route53.Route53, dnsName string, logger logrus.FieldLogger) (string, error) {
   486  	request := &route53.ListHostedZonesByNameInput{
   487  		DNSName: aws.String(dnsName),
   488  	}
   489  	for i := 0; true; i++ {
   490  		logger.Debugf("listing AWS hosted zones %q (page %d)", dnsName, i)
   491  		list, err := client.ListHostedZonesByNameWithContext(ctx, request)
   492  		if err != nil {
   493  			return "", err
   494  		}
   495  
   496  		for _, zone := range list.HostedZones {
   497  			if *zone.Name != dnsName {
   498  				// No name after this can match dnsName
   499  				return "", nil
   500  			}
   501  			if zone.Config == nil || zone.Config.PrivateZone == nil {
   502  				logger.WithField("hosted zone", *zone.Name).Warn("could not determine whether hosted zone is private")
   503  				continue
   504  			}
   505  			if !*zone.Config.PrivateZone {
   506  				return *zone.Id, nil
   507  			}
   508  		}
   509  
   510  		if *list.IsTruncated && *list.NextDNSName == *request.DNSName {
   511  			request.HostedZoneId = list.NextHostedZoneId
   512  			continue
   513  		}
   514  
   515  		break
   516  	}
   517  	return "", nil
   518  }
   519  
   520  func deleteARN(ctx context.Context, session *session.Session, arn arn.ARN, logger logrus.FieldLogger) error {
   521  	switch arn.Service {
   522  	case "ec2":
   523  		return deleteEC2(ctx, session, arn, logger)
   524  	case "elasticloadbalancing":
   525  		return deleteElasticLoadBalancing(ctx, session, arn, logger)
   526  	case "iam":
   527  		return deleteIAM(ctx, session, arn, logger)
   528  	case "route53":
   529  		return deleteRoute53(ctx, session, arn, logger)
   530  	case "s3":
   531  		return deleteS3(ctx, session, arn, logger)
   532  	case "elasticfilesystem":
   533  		return deleteElasticFileSystem(ctx, session, arn, logger)
   534  	default:
   535  		return errors.Errorf("unrecognized ARN service %s (%s)", arn.Service, arn)
   536  	}
   537  }
   538  
   539  func deleteRoute53(ctx context.Context, session *session.Session, arn arn.ARN, logger logrus.FieldLogger) error {
   540  	resourceType, id, err := splitSlash("resource", arn.Resource)
   541  	if err != nil {
   542  		return err
   543  	}
   544  	logger = logger.WithField("id", id)
   545  
   546  	if resourceType != "hostedzone" {
   547  		return errors.Errorf("unrecognized Route 53 resource type %s", resourceType)
   548  	}
   549  
   550  	client := route53.New(session)
   551  
   552  	publicZoneID, err := getPublicHostedZone(ctx, client, id, logger)
   553  	if err != nil {
   554  		// In some cases AWS may return the zone in the list of tagged resources despite the fact
   555  		// it no longer exists.
   556  		if err.(awserr.Error).Code() == route53.ErrCodeNoSuchHostedZone {
   557  			return nil
   558  		}
   559  		return err
   560  	}
   561  
   562  	recordSetKey := func(recordSet *route53.ResourceRecordSet) string {
   563  		return fmt.Sprintf("%s %s", *recordSet.Type, *recordSet.Name)
   564  	}
   565  
   566  	publicEntries := map[string]*route53.ResourceRecordSet{}
   567  	if len(publicZoneID) != 0 {
   568  		err = client.ListResourceRecordSetsPagesWithContext(
   569  			ctx,
   570  			&route53.ListResourceRecordSetsInput{HostedZoneId: aws.String(publicZoneID)},
   571  			func(results *route53.ListResourceRecordSetsOutput, lastPage bool) bool {
   572  				for _, recordSet := range results.ResourceRecordSets {
   573  					key := recordSetKey(recordSet)
   574  					publicEntries[key] = recordSet
   575  				}
   576  
   577  				return !lastPage
   578  			},
   579  		)
   580  		if err != nil {
   581  			return err
   582  		}
   583  	} else {
   584  		logger.Debug("shared public zone not found")
   585  	}
   586  
   587  	var lastError error
   588  	err = client.ListResourceRecordSetsPagesWithContext(
   589  		ctx,
   590  		&route53.ListResourceRecordSetsInput{HostedZoneId: aws.String(id)},
   591  		func(results *route53.ListResourceRecordSetsOutput, lastPage bool) bool {
   592  			for _, recordSet := range results.ResourceRecordSets {
   593  				if *recordSet.Type == "SOA" || *recordSet.Type == "NS" {
   594  					// can't delete SOA and NS types
   595  					continue
   596  				}
   597  				key := recordSetKey(recordSet)
   598  				if publicEntry, ok := publicEntries[key]; ok {
   599  					err := deleteRoute53RecordSet(ctx, client, publicZoneID, publicEntry, logger.WithField("public zone", publicZoneID))
   600  					if err != nil {
   601  						if lastError != nil {
   602  							logger.Debug(lastError)
   603  						}
   604  						lastError = errors.Wrapf(err, "deleting record set %#v from public zone %s", publicEntry, publicZoneID)
   605  					}
   606  					// do not delete the record set in the private zone if the delete failed in the public zone;
   607  					// otherwise the record set in the public zone will get leaked
   608  					continue
   609  				}
   610  
   611  				err = deleteRoute53RecordSet(ctx, client, id, recordSet, logger)
   612  				if err != nil {
   613  					if lastError != nil {
   614  						logger.Debug(lastError)
   615  					}
   616  					lastError = errors.Wrapf(err, "deleting record set %#+v from zone %s", recordSet, id)
   617  				}
   618  			}
   619  
   620  			return !lastPage
   621  		},
   622  	)
   623  
   624  	if lastError != nil {
   625  		return lastError
   626  	}
   627  	if err != nil {
   628  		return err
   629  	}
   630  
   631  	_, err = client.DeleteHostedZoneWithContext(ctx, &route53.DeleteHostedZoneInput{
   632  		Id: aws.String(id),
   633  	})
   634  	if err != nil {
   635  		if err.(awserr.Error).Code() == "NoSuchHostedZone" {
   636  			return nil
   637  		}
   638  		return err
   639  	}
   640  
   641  	logger.Info("Deleted")
   642  	return nil
   643  }
   644  
   645  func deleteRoute53RecordSet(ctx context.Context, client *route53.Route53, zoneID string, recordSet *route53.ResourceRecordSet, logger logrus.FieldLogger) error {
   646  	logger = logger.WithField("record set", fmt.Sprintf("%s %s", *recordSet.Type, *recordSet.Name))
   647  	_, err := client.ChangeResourceRecordSetsWithContext(ctx, &route53.ChangeResourceRecordSetsInput{
   648  		HostedZoneId: aws.String(zoneID),
   649  		ChangeBatch: &route53.ChangeBatch{
   650  			Changes: []*route53.Change{
   651  				{
   652  					Action:            aws.String("DELETE"),
   653  					ResourceRecordSet: recordSet,
   654  				},
   655  			},
   656  		},
   657  	})
   658  	if err != nil {
   659  		return err
   660  	}
   661  
   662  	logger.Info("Deleted")
   663  	return nil
   664  }
   665  
   666  func deleteS3(ctx context.Context, session *session.Session, arn arn.ARN, logger logrus.FieldLogger) error {
   667  	client := s3.New(session)
   668  
   669  	iter := s3manager.NewDeleteListIterator(client, &s3.ListObjectsInput{
   670  		Bucket: aws.String(arn.Resource),
   671  	})
   672  	err := s3manager.NewBatchDeleteWithClient(client).Delete(ctx, iter)
   673  	if err != nil && !isBucketNotFound(err) {
   674  		return err
   675  	}
   676  	logger.Debug("Emptied")
   677  
   678  	var lastError error
   679  	err = client.ListObjectVersionsPagesWithContext(ctx, &s3.ListObjectVersionsInput{
   680  		Bucket:  aws.String(arn.Resource),
   681  		MaxKeys: aws.Int64(1000),
   682  	}, func(page *s3.ListObjectVersionsOutput, lastPage bool) bool {
   683  		var deleteObjects []*s3.ObjectIdentifier
   684  		for _, deleteMarker := range page.DeleteMarkers {
   685  			deleteObjects = append(deleteObjects, &s3.ObjectIdentifier{
   686  				Key:       aws.String(*deleteMarker.Key),
   687  				VersionId: aws.String(*deleteMarker.VersionId),
   688  			})
   689  		}
   690  		for _, version := range page.Versions {
   691  			deleteObjects = append(deleteObjects, &s3.ObjectIdentifier{
   692  				Key:       aws.String(*version.Key),
   693  				VersionId: aws.String(*version.VersionId),
   694  			})
   695  		}
   696  		if len(deleteObjects) > 0 {
   697  			_, err := client.DeleteObjectsWithContext(ctx, &s3.DeleteObjectsInput{
   698  				Bucket: aws.String(arn.Resource),
   699  				Delete: &s3.Delete{
   700  					Objects: deleteObjects,
   701  				},
   702  			})
   703  			if err != nil {
   704  				lastError = errors.Wrapf(err, "delete object failed %v", err)
   705  			}
   706  		}
   707  		return !lastPage
   708  	})
   709  	if lastError != nil {
   710  		return lastError
   711  	}
   712  	if err != nil && !isBucketNotFound(err) {
   713  		return err
   714  	}
   715  	logger.Debug("Versions Deleted")
   716  
   717  	_, err = client.DeleteBucketWithContext(ctx, &s3.DeleteBucketInput{
   718  		Bucket: aws.String(arn.Resource),
   719  	})
   720  	if err != nil && !isBucketNotFound(err) {
   721  		return err
   722  	}
   723  
   724  	logger.Info("Deleted")
   725  	return nil
   726  }
   727  
   728  func isBucketNotFound(err interface{}) bool {
   729  	switch s3Err := err.(type) {
   730  	case awserr.Error:
   731  		if s3Err.Code() == "NoSuchBucket" {
   732  			return true
   733  		}
   734  		origErr := s3Err.OrigErr()
   735  		if origErr != nil {
   736  			return isBucketNotFound(origErr)
   737  		}
   738  	case s3manager.Error:
   739  		if s3Err.OrigErr != nil {
   740  			return isBucketNotFound(s3Err.OrigErr)
   741  		}
   742  	case s3manager.Errors:
   743  		if len(s3Err) == 1 {
   744  			return isBucketNotFound(s3Err[0])
   745  		}
   746  	}
   747  	return false
   748  }
   749  
   750  func deleteElasticFileSystem(ctx context.Context, session *session.Session, arn arn.ARN, logger logrus.FieldLogger) error {
   751  	client := efs.New(session)
   752  
   753  	resourceType, id, err := splitSlash("resource", arn.Resource)
   754  	if err != nil {
   755  		return err
   756  	}
   757  
   758  	switch resourceType {
   759  	case "file-system":
   760  		return deleteFileSystem(ctx, client, id, logger)
   761  	case "access-point":
   762  		return deleteAccessPoint(ctx, client, id, logger)
   763  	default:
   764  		return errors.Errorf("unrecognized elastic file system resource type %s", resourceType)
   765  	}
   766  }
   767  
   768  func deleteFileSystem(ctx context.Context, client *efs.EFS, fsid string, logger logrus.FieldLogger) error {
   769  	logger = logger.WithField("Elastic FileSystem ID", fsid)
   770  
   771  	// Delete all MountTargets + AccessPoints under given FS ID
   772  	mountTargetIDs, err := getMountTargets(ctx, client, fsid)
   773  	if err != nil {
   774  		return err
   775  	}
   776  	for _, mt := range mountTargetIDs {
   777  		err := deleteMountTarget(ctx, client, mt, logger)
   778  		if err != nil {
   779  			return err
   780  		}
   781  	}
   782  	accessPointIDs, err := getAccessPoints(ctx, client, fsid)
   783  	if err != nil {
   784  		return err
   785  	}
   786  	for _, ap := range accessPointIDs {
   787  		err := deleteAccessPoint(ctx, client, ap, logger)
   788  		if err != nil {
   789  			return err
   790  		}
   791  	}
   792  
   793  	_, err = client.DeleteFileSystemWithContext(ctx, &efs.DeleteFileSystemInput{FileSystemId: aws.String(fsid)})
   794  	if err != nil {
   795  		if err.(awserr.Error).Code() == efs.ErrCodeFileSystemNotFound {
   796  			return nil
   797  		}
   798  		return err
   799  	}
   800  
   801  	logger.Info("Deleted")
   802  	return nil
   803  }
   804  
   805  func getAccessPoints(ctx context.Context, client *efs.EFS, apID string) ([]string, error) {
   806  	var accessPointIDs []string
   807  	err := client.DescribeAccessPointsPagesWithContext(
   808  		ctx,
   809  		&efs.DescribeAccessPointsInput{FileSystemId: aws.String(apID)},
   810  		func(page *efs.DescribeAccessPointsOutput, lastPage bool) bool {
   811  			for _, ap := range page.AccessPoints {
   812  				apName := ap.AccessPointId
   813  				if apName == nil {
   814  					continue
   815  				}
   816  				accessPointIDs = append(accessPointIDs, *apName)
   817  			}
   818  			return !lastPage
   819  
   820  		},
   821  	)
   822  	if err != nil {
   823  		return nil, err
   824  	}
   825  	return accessPointIDs, nil
   826  }
   827  
   828  func getMountTargets(ctx context.Context, client *efs.EFS, fsid string) ([]string, error) {
   829  	var mountTargetIDs []string
   830  	// There is no DescribeMountTargetsPagesWithContext.
   831  	// Number of Mount Targets should be equal to nr. of subnets that can access the volume, i.e. relatively small.
   832  	rsp, err := client.DescribeMountTargetsWithContext(
   833  		ctx,
   834  		&efs.DescribeMountTargetsInput{FileSystemId: aws.String(fsid)},
   835  	)
   836  	if err != nil {
   837  		return nil, err
   838  	}
   839  
   840  	for _, mt := range rsp.MountTargets {
   841  		mtName := mt.MountTargetId
   842  		if mtName == nil {
   843  			continue
   844  		}
   845  		mountTargetIDs = append(mountTargetIDs, *mtName)
   846  	}
   847  
   848  	return mountTargetIDs, nil
   849  }
   850  
   851  func deleteAccessPoint(ctx context.Context, client *efs.EFS, id string, logger logrus.FieldLogger) error {
   852  	logger = logger.WithField("AccessPoint ID", id)
   853  	_, err := client.DeleteAccessPointWithContext(ctx, &efs.DeleteAccessPointInput{AccessPointId: aws.String(id)})
   854  	if err != nil {
   855  		if err.(awserr.Error).Code() == efs.ErrCodeAccessPointNotFound {
   856  			return nil
   857  		}
   858  
   859  		return err
   860  	}
   861  
   862  	logger.Info("Deleted")
   863  	return nil
   864  }
   865  
   866  func deleteMountTarget(ctx context.Context, client *efs.EFS, id string, logger logrus.FieldLogger) error {
   867  	logger = logger.WithField("Mount Target ID", id)
   868  	_, err := client.DeleteMountTargetWithContext(ctx, &efs.DeleteMountTargetInput{MountTargetId: aws.String(id)})
   869  	if err != nil {
   870  		if err.(awserr.Error).Code() == efs.ErrCodeMountTargetNotFound {
   871  			return nil
   872  		}
   873  		return err
   874  	}
   875  
   876  	logger.Info("Deleted")
   877  	return nil
   878  }