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 }