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 }