github.com/openshift/installer@v1.4.17/pkg/destroy/aws/shared.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/credentials/stscreds"
    13  	"github.com/aws/aws-sdk-go/aws/session"
    14  	"github.com/aws/aws-sdk-go/service/iam"
    15  	"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi"
    16  	"github.com/aws/aws-sdk-go/service/route53"
    17  	"github.com/pkg/errors"
    18  	"github.com/sirupsen/logrus"
    19  	"k8s.io/apimachinery/pkg/util/wait"
    20  )
    21  
    22  func (o *ClusterUninstaller) removeSharedTags(
    23  	ctx context.Context,
    24  	session *session.Session,
    25  	tagClients []*resourcegroupstaggingapi.ResourceGroupsTaggingAPI,
    26  	tracker *ErrorTracker,
    27  ) error {
    28  	for _, key := range o.clusterOwnedKeys() {
    29  		if err := o.removeSharedTag(ctx, session, tagClients, key, tracker); err != nil {
    30  			return err
    31  		}
    32  	}
    33  	return nil
    34  }
    35  
    36  func (o *ClusterUninstaller) clusterOwnedKeys() []string {
    37  	var keys []string
    38  	for _, filter := range o.Filters {
    39  		for key, value := range filter {
    40  			if !strings.HasPrefix(key, "kubernetes.io/cluster/") {
    41  				continue
    42  			}
    43  			if value != "owned" {
    44  				o.Logger.Warnf("Ignoring non-owned cluster key %s: %s for shared-tag removal", key, value)
    45  			}
    46  			keys = append(keys, key)
    47  		}
    48  	}
    49  	return keys
    50  }
    51  
    52  func (o *ClusterUninstaller) removeSharedTag(ctx context.Context, session *session.Session, tagClients []*resourcegroupstaggingapi.ResourceGroupsTaggingAPI, key string, tracker *ErrorTracker) error {
    53  	const sharedValue = "shared"
    54  
    55  	request := &resourcegroupstaggingapi.UntagResourcesInput{
    56  		TagKeys: []*string{aws.String(key)},
    57  	}
    58  
    59  	removed := map[string]struct{}{}
    60  	tagClients = append([]*resourcegroupstaggingapi.ResourceGroupsTaggingAPI(nil), tagClients...)
    61  	for len(tagClients) > 0 {
    62  		nextTagClients := tagClients[:0]
    63  		for _, tagClient := range tagClients {
    64  			o.Logger.Debugf("Search for and remove tags in %s matching %s: shared", *tagClient.Config.Region, key)
    65  			var arns []string
    66  			err := tagClient.GetResourcesPagesWithContext(
    67  				ctx,
    68  				&resourcegroupstaggingapi.GetResourcesInput{TagFilters: []*resourcegroupstaggingapi.TagFilter{{
    69  					Key:    aws.String(key),
    70  					Values: []*string{aws.String(sharedValue)},
    71  				}}},
    72  				func(results *resourcegroupstaggingapi.GetResourcesOutput, lastPage bool) bool {
    73  					for _, resource := range results.ResourceTagMappingList {
    74  						arnString := aws.StringValue(resource.ResourceARN)
    75  						logger := o.Logger.WithField("arn", arnString)
    76  						parsedARN, err := arn.Parse(arnString)
    77  						if err != nil {
    78  							logger.WithError(err).Debug("could not parse ARN")
    79  							continue
    80  						}
    81  						if _, ok := removed[arnString]; !ok {
    82  							if err := o.cleanSharedARN(ctx, session, parsedARN, logger); err != nil {
    83  								tracker.suppressWarning(arnString, err, logger)
    84  								if err := ctx.Err(); err != nil {
    85  									return false
    86  								}
    87  								continue
    88  							}
    89  							arns = append(arns, arnString)
    90  						}
    91  					}
    92  
    93  					return !lastPage
    94  				},
    95  			)
    96  			if err != nil {
    97  				err2 := errors.Wrap(err, "get tagged resources")
    98  				o.Logger.Info(err2)
    99  				if aerr, ok := err.(awserr.Error); ok {
   100  					switch aerr.Code() {
   101  					case resourcegroupstaggingapi.ErrorCodeInvalidParameterException:
   102  						continue
   103  					}
   104  				}
   105  				nextTagClients = append(nextTagClients, tagClient)
   106  				continue
   107  			}
   108  			if len(arns) == 0 {
   109  				o.Logger.Debugf("No matches in %s for %s: shared, removing client", *tagClient.Config.Region, key)
   110  				continue
   111  			}
   112  			// appending the tag client here but it needs to be removed if there is a InvalidParameterException when trying to
   113  			// untag below since that only leads to an infinite loop error.
   114  			nextTagClients = append(nextTagClients, tagClient)
   115  
   116  			for i := 0; i < len(arns); i += 20 {
   117  				request.ResourceARNList = make([]*string, 0, 20)
   118  				for j := 0; i+j < len(arns) && j < 20; j++ {
   119  					request.ResourceARNList = append(request.ResourceARNList, aws.String(arns[i+j]))
   120  				}
   121  				result, err := tagClient.UntagResourcesWithContext(ctx, request)
   122  				if err != nil {
   123  					var awsErr awserr.Error
   124  					ok := errors.As(err, &awsErr)
   125  					if ok && awsErr.Code() == resourcegroupstaggingapi.ErrorCodeInvalidParameterException {
   126  						nextTagClients = nextTagClients[:len(nextTagClients)-1]
   127  					}
   128  					err = errors.Wrap(err, "untag shared resources")
   129  					o.Logger.Info(err)
   130  					continue
   131  				}
   132  				for _, arn := range request.ResourceARNList {
   133  					if info, failed := result.FailedResourcesMap[*arn]; failed {
   134  						o.Logger.WithField("arn", *arn).Infof("Failed to remove tag %s: shared; error=%s", key, *info.ErrorMessage)
   135  						continue
   136  					}
   137  					o.Logger.WithField("arn", *arn).Infof("Removed tag %s: shared", key)
   138  					removed[*arn] = exists
   139  				}
   140  			}
   141  		}
   142  		tagClients = nextTagClients
   143  	}
   144  
   145  	iamClient := iam.New(session)
   146  	iamRoleSearch := &IamRoleSearch{
   147  		Client:  iamClient,
   148  		Filters: []Filter{{key: sharedValue}},
   149  		Logger:  o.Logger,
   150  	}
   151  	o.Logger.Debugf("Search for and remove shared tags for IAM roles matching %s: shared", key)
   152  	if err := wait.PollImmediateUntil(
   153  		time.Second*10,
   154  		func() (bool, error) {
   155  			_, sharedRoles, err := iamRoleSearch.find(ctx)
   156  			if err != nil {
   157  				o.Logger.Infof("Could not search for shared IAM roles: %v", err)
   158  				return false, nil
   159  			}
   160  			done := true
   161  			for _, role := range sharedRoles {
   162  				o.Logger.Debugf("Removing the shared tag from the %q IAM role", role)
   163  				input := &iam.UntagRoleInput{
   164  					RoleName: &role,
   165  					TagKeys:  []*string{&key},
   166  				}
   167  				if _, err := iamClient.UntagRoleWithContext(ctx, input); err != nil {
   168  					done = false
   169  					o.Logger.Infof("Could not remove the shared tag from the %q IAM role: %v", role, err)
   170  				}
   171  			}
   172  			return done, nil
   173  		},
   174  		ctx.Done(),
   175  	); err != nil {
   176  		return errors.Wrap(err, "problem removing shared tags from IAM roles")
   177  	}
   178  
   179  	return nil
   180  }
   181  
   182  func (o *ClusterUninstaller) cleanSharedARN(ctx context.Context, session *session.Session, arn arn.ARN, logger logrus.FieldLogger) error {
   183  	switch service := arn.Service; service {
   184  	case "route53":
   185  		return o.cleanSharedRoute53(ctx, session, arn, logger)
   186  	default:
   187  		logger.Debugf("Nothing to clean for shared %s resource", service)
   188  		return nil
   189  	}
   190  }
   191  
   192  func (o *ClusterUninstaller) cleanSharedRoute53(ctx context.Context, session *session.Session, arn arn.ARN, logger logrus.FieldLogger) error {
   193  	resourceType, id, err := splitSlash("resource", arn.Resource)
   194  	if err != nil {
   195  		return err
   196  	}
   197  	logger = logger.WithField("id", id)
   198  
   199  	switch resourceType {
   200  	case "hostedzone":
   201  		return o.cleanSharedHostedZone(ctx, session, id, logger)
   202  	default:
   203  		logger.Debugf("Nothing to clean for shared %s resource", resourceType)
   204  		return nil
   205  	}
   206  }
   207  
   208  func (o *ClusterUninstaller) cleanSharedHostedZone(ctx context.Context, session *session.Session, id string, logger logrus.FieldLogger) error {
   209  	// The private hosted zone (phz) may belong to a different account,
   210  	// in which case we need a separate client.
   211  	publicZoneClient := route53.New(session)
   212  	privateZoneClient := route53.New(session)
   213  	if o.HostedZoneRole != "" {
   214  		creds := stscreds.NewCredentials(session, o.HostedZoneRole)
   215  		privateZoneClient = route53.New(session, &aws.Config{Credentials: creds})
   216  		logger.Infof("Assuming role %s to destroy records in private hosted zone", o.HostedZoneRole)
   217  	}
   218  
   219  	if o.ClusterDomain == "" {
   220  		logger.Debug("No cluster domain specified in metadata; cannot clean the shared hosted zone")
   221  		return nil
   222  	}
   223  	dottedClusterDomain := o.ClusterDomain + "."
   224  
   225  	publicZoneID, err := findAncestorPublicRoute53(ctx, publicZoneClient, dottedClusterDomain, logger)
   226  	if err != nil {
   227  		return err
   228  	}
   229  
   230  	var lastError error
   231  	err = privateZoneClient.ListResourceRecordSetsPagesWithContext(
   232  		ctx,
   233  		&route53.ListResourceRecordSetsInput{HostedZoneId: aws.String(id)},
   234  		func(results *route53.ListResourceRecordSetsOutput, lastPage bool) bool {
   235  			for _, recordSet := range results.ResourceRecordSets {
   236  				// skip record sets that are not part of the cluster
   237  				name := aws.StringValue(recordSet.Name)
   238  				if !strings.HasSuffix(name, dottedClusterDomain) {
   239  					continue
   240  				}
   241  				if len(name) == len(dottedClusterDomain) {
   242  					continue
   243  				}
   244  				recordsetFields := logrus.Fields{"recordset": fmt.Sprintf("%s (%s)", aws.StringValue(recordSet.Name), aws.StringValue(recordSet.Type))}
   245  				// delete any matching record sets in the public hosted zone
   246  				if publicZoneID != "" {
   247  					publicZoneLogger := logger.WithField("id", publicZoneID)
   248  					if err := deleteMatchingRecordSetInPublicZone(ctx, publicZoneClient, publicZoneID, recordSet, publicZoneLogger); err != nil {
   249  						if lastError != nil {
   250  							publicZoneLogger.Debug(lastError)
   251  						}
   252  						lastError = errors.Wrapf(err, "deleting record set matching %#v from public zone %s", recordSet, publicZoneID)
   253  						// do not delete the record set in the private zone if the delete failed in the public zone;
   254  						// otherwise the record set in the public zone will get leaked
   255  						continue
   256  					}
   257  					publicZoneLogger.WithFields(recordsetFields).Debug("Deleted from public zone")
   258  				}
   259  				// delete the record set
   260  				if err := deleteRoute53RecordSet(ctx, privateZoneClient, id, recordSet, logger); err != nil {
   261  					if lastError != nil {
   262  						logger.Debug(lastError)
   263  					}
   264  					lastError = errors.Wrapf(err, "deleting record set %#v from zone %s", recordSet, id)
   265  				}
   266  				logger.WithFields(recordsetFields).Debug("Deleted from public zone")
   267  			}
   268  			return !lastPage
   269  		},
   270  	)
   271  
   272  	if lastError != nil {
   273  		return lastError
   274  	}
   275  	if err != nil {
   276  		return err
   277  	}
   278  
   279  	logger.Info("Cleaned record sets from hosted zone")
   280  	return nil
   281  }
   282  
   283  func deleteMatchingRecordSetInPublicZone(ctx context.Context, client *route53.Route53, zoneID string, recordSet *route53.ResourceRecordSet, logger logrus.FieldLogger) error {
   284  	in := &route53.ListResourceRecordSetsInput{
   285  		HostedZoneId:    aws.String(zoneID),
   286  		MaxItems:        aws.String("1"),
   287  		StartRecordName: recordSet.Name,
   288  		StartRecordType: recordSet.Type,
   289  	}
   290  	out, err := client.ListResourceRecordSetsWithContext(ctx, in)
   291  	if err != nil {
   292  		return err
   293  	}
   294  	if len(out.ResourceRecordSets) == 0 {
   295  		return nil
   296  	}
   297  	matchingRecordSet := out.ResourceRecordSets[0]
   298  	if aws.StringValue(matchingRecordSet.Name) != aws.StringValue(recordSet.Name) ||
   299  		aws.StringValue(matchingRecordSet.Type) != aws.StringValue(recordSet.Type) {
   300  		return nil
   301  	}
   302  	return deleteRoute53RecordSet(ctx, client, zoneID, matchingRecordSet, logger)
   303  }