sigs.k8s.io/cluster-api-provider-aws@v1.5.5/pkg/cloud/services/autoscaling/autoscalinggroup.go (about) 1 /* 2 Copyright 2018 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 asg 18 19 import ( 20 "fmt" 21 "strings" 22 23 "github.com/aws/aws-sdk-go/aws" 24 "github.com/aws/aws-sdk-go/service/autoscaling" 25 "github.com/aws/aws-sdk-go/service/ec2" 26 "github.com/pkg/errors" 27 "k8s.io/utils/pointer" 28 29 infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1" 30 expinfrav1 "sigs.k8s.io/cluster-api-provider-aws/exp/api/v1beta1" 31 "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/awserrors" 32 "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/converters" 33 "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/scope" 34 "sigs.k8s.io/cluster-api-provider-aws/pkg/record" 35 ) 36 37 // SDKToAutoScalingGroup converts an AWS EC2 SDK AutoScalingGroup to the CAPA AutoScalingGroup type. 38 func (s *Service) SDKToAutoScalingGroup(v *autoscaling.Group) (*expinfrav1.AutoScalingGroup, error) { 39 i := &expinfrav1.AutoScalingGroup{ 40 ID: aws.StringValue(v.AutoScalingGroupARN), 41 Name: aws.StringValue(v.AutoScalingGroupName), 42 // TODO(rudoi): this is just terrible 43 DesiredCapacity: aws.Int32(int32(aws.Int64Value(v.DesiredCapacity))), 44 MaxSize: int32(aws.Int64Value(v.MaxSize)), 45 MinSize: int32(aws.Int64Value(v.MinSize)), 46 CapacityRebalance: aws.BoolValue(v.CapacityRebalance), 47 //TODO: determine what additional values go here and what else should be in the struct 48 } 49 50 if v.MixedInstancesPolicy != nil { 51 i.MixedInstancesPolicy = &expinfrav1.MixedInstancesPolicy{ 52 InstancesDistribution: &expinfrav1.InstancesDistribution{ 53 OnDemandBaseCapacity: v.MixedInstancesPolicy.InstancesDistribution.OnDemandBaseCapacity, 54 OnDemandPercentageAboveBaseCapacity: v.MixedInstancesPolicy.InstancesDistribution.OnDemandPercentageAboveBaseCapacity, 55 }, 56 } 57 58 for _, override := range v.MixedInstancesPolicy.LaunchTemplate.Overrides { 59 i.MixedInstancesPolicy.Overrides = append(i.MixedInstancesPolicy.Overrides, expinfrav1.Overrides{InstanceType: aws.StringValue(override.InstanceType)}) 60 } 61 62 onDemandAllocationStrategy := aws.StringValue(v.MixedInstancesPolicy.InstancesDistribution.OnDemandAllocationStrategy) 63 if onDemandAllocationStrategy == string(expinfrav1.OnDemandAllocationStrategyPrioritized) { 64 i.MixedInstancesPolicy.InstancesDistribution.OnDemandAllocationStrategy = expinfrav1.OnDemandAllocationStrategyPrioritized 65 } 66 67 spotAllocationStrategy := aws.StringValue(v.MixedInstancesPolicy.InstancesDistribution.SpotAllocationStrategy) 68 if spotAllocationStrategy == string(expinfrav1.SpotAllocationStrategyLowestPrice) { 69 i.MixedInstancesPolicy.InstancesDistribution.SpotAllocationStrategy = expinfrav1.SpotAllocationStrategyLowestPrice 70 } else { 71 i.MixedInstancesPolicy.InstancesDistribution.SpotAllocationStrategy = expinfrav1.SpotAllocationStrategyCapacityOptimized 72 } 73 } 74 75 if v.Status != nil { 76 i.Status = expinfrav1.ASGStatus(*v.Status) 77 } 78 79 if len(v.Tags) > 0 { 80 i.Tags = converters.ASGTagsToMap(v.Tags) 81 } 82 83 if len(v.Instances) > 0 { 84 for _, autoscalingInstance := range v.Instances { 85 tmp := &infrav1.Instance{ 86 ID: aws.StringValue(autoscalingInstance.InstanceId), 87 State: infrav1.InstanceState(*autoscalingInstance.LifecycleState), 88 AvailabilityZone: *autoscalingInstance.AvailabilityZone, 89 } 90 i.Instances = append(i.Instances, *tmp) 91 } 92 } 93 94 return i, nil 95 } 96 97 // ASGIfExists returns the existing autoscaling group or nothing if it doesn't exist. 98 func (s *Service) ASGIfExists(name *string) (*expinfrav1.AutoScalingGroup, error) { 99 if name == nil { 100 s.scope.Info("Autoscaling Group does not have a name") 101 return nil, nil 102 } 103 104 s.scope.Info("Looking for asg by name", "name", *name) 105 106 input := &autoscaling.DescribeAutoScalingGroupsInput{ 107 AutoScalingGroupNames: []*string{name}, 108 } 109 110 out, err := s.ASGClient.DescribeAutoScalingGroups(input) 111 switch { 112 case awserrors.IsNotFound(err): 113 return nil, nil 114 case err != nil: 115 record.Eventf(s.scope.InfraCluster(), "FailedDescribeAutoScalingGroups", "failed to describe ASG %q: %v", *name, err) 116 return nil, errors.Wrapf(err, "failed to describe AutoScaling Group: %q", *name) 117 } 118 //TODO: double check if you're handling nil vals 119 return s.SDKToAutoScalingGroup(out.AutoScalingGroups[0]) 120 } 121 122 // GetASGByName returns the existing ASG or nothing if it doesn't exist. 123 func (s *Service) GetASGByName(scope *scope.MachinePoolScope) (*expinfrav1.AutoScalingGroup, error) { 124 s.scope.V(2).Info("Looking for existing AutoScalingGroup by name") 125 126 input := &autoscaling.DescribeAutoScalingGroupsInput{ 127 AutoScalingGroupNames: []*string{ 128 aws.String(scope.Name()), 129 }, 130 } 131 132 out, err := s.ASGClient.DescribeAutoScalingGroups(input) 133 switch { 134 case awserrors.IsNotFound(err): 135 return nil, nil 136 case err != nil: 137 record.Eventf(s.scope.InfraCluster(), "FailedDescribeInstances", "Failed to describe instances by tags: %v", err) 138 return nil, errors.Wrap(err, "failed to describe instances by tags") 139 case len(out.AutoScalingGroups) == 0: 140 record.Eventf(scope.AWSMachinePool, "FailedDescribeInstances", "No Auto Scaling Groups with %s found", scope.Name()) 141 return nil, nil 142 } 143 144 return s.SDKToAutoScalingGroup(out.AutoScalingGroups[0]) 145 } 146 147 // CreateASG runs an autoscaling group. 148 func (s *Service) CreateASG(scope *scope.MachinePoolScope) (*expinfrav1.AutoScalingGroup, error) { 149 subnets, err := s.SubnetIDs(scope) 150 if err != nil { 151 return nil, fmt.Errorf("getting subnets for ASG: %w", err) 152 } 153 154 input := &expinfrav1.AutoScalingGroup{ 155 Name: scope.Name(), 156 MaxSize: scope.AWSMachinePool.Spec.MaxSize, 157 MinSize: scope.AWSMachinePool.Spec.MinSize, 158 Subnets: subnets, 159 DefaultCoolDown: scope.AWSMachinePool.Spec.DefaultCoolDown, 160 CapacityRebalance: scope.AWSMachinePool.Spec.CapacityRebalance, 161 MixedInstancesPolicy: scope.AWSMachinePool.Spec.MixedInstancesPolicy, 162 } 163 164 if scope.MachinePool.Spec.Replicas != nil { 165 input.DesiredCapacity = scope.MachinePool.Spec.Replicas 166 } 167 168 if scope.AWSMachinePool.Status.LaunchTemplateID == "" { 169 return nil, errors.New("AWSMachinePool has no LaunchTemplateID for some reason") 170 } 171 172 // Make sure to use the MachinePoolScope here to get the merger of AWSCluster and AWSMachinePool tags 173 additionalTags := scope.AdditionalTags() 174 // Set the cloud provider tag 175 additionalTags[infrav1.ClusterAWSCloudProviderTagKey(s.scope.KubernetesClusterName())] = string(infrav1.ResourceLifecycleOwned) 176 177 input.Tags = infrav1.Build(infrav1.BuildParams{ 178 ClusterName: s.scope.KubernetesClusterName(), 179 Lifecycle: infrav1.ResourceLifecycleOwned, 180 Name: aws.String(scope.Name()), 181 Role: aws.String("node"), 182 Additional: additionalTags, 183 }) 184 185 s.scope.Info("Running instance") 186 if err := s.runPool(input, scope.AWSMachinePool.Status.LaunchTemplateID); err != nil { 187 // Only record the failure event if the error is not related to failed dependencies. 188 // This is to avoid spamming failure events since the machine will be requeued by the actuator. 189 // if !awserrors.IsFailedDependency(errors.Cause(err)) { 190 // record.Warnf(scope.AWSMachinePool, "FailedCreate", "Failed to create instance: %v", err) 191 // } 192 s.scope.Error(err, "unable to create AutoScalingGroup") 193 return nil, err 194 } 195 record.Eventf(scope.AWSMachinePool, "SuccessfulCreate", "Created new ASG: %s", scope.Name()) 196 197 return nil, nil 198 } 199 200 func (s *Service) runPool(i *expinfrav1.AutoScalingGroup, launchTemplateID string) error { 201 input := &autoscaling.CreateAutoScalingGroupInput{ 202 AutoScalingGroupName: aws.String(i.Name), 203 MaxSize: aws.Int64(int64(i.MaxSize)), 204 MinSize: aws.Int64(int64(i.MinSize)), 205 VPCZoneIdentifier: aws.String(strings.Join(i.Subnets, ", ")), 206 DefaultCooldown: aws.Int64(int64(i.DefaultCoolDown.Duration.Seconds())), 207 CapacityRebalance: aws.Bool(i.CapacityRebalance), 208 } 209 210 if i.DesiredCapacity != nil { 211 input.DesiredCapacity = aws.Int64(int64(aws.Int32Value(i.DesiredCapacity))) 212 } 213 214 if i.MixedInstancesPolicy != nil { 215 input.MixedInstancesPolicy = createSDKMixedInstancesPolicy(i.Name, i.MixedInstancesPolicy) 216 } else { 217 input.LaunchTemplate = &autoscaling.LaunchTemplateSpecification{ 218 LaunchTemplateId: aws.String(launchTemplateID), 219 Version: aws.String(expinfrav1.LaunchTemplateLatestVersion), 220 } 221 } 222 223 if i.Tags != nil { 224 input.Tags = BuildTagsFromMap(i.Name, i.Tags) 225 } 226 227 if _, err := s.ASGClient.CreateAutoScalingGroup(input); err != nil { 228 return errors.Wrap(err, "failed to create autoscaling group") 229 } 230 231 return nil 232 } 233 234 // DeleteASGAndWait will delete an ASG and wait until it is deleted. 235 func (s *Service) DeleteASGAndWait(name string) error { 236 if err := s.DeleteASG(name); err != nil { 237 return err 238 } 239 240 s.scope.V(2).Info("Waiting for ASG to be deleted", "name", name) 241 242 input := &autoscaling.DescribeAutoScalingGroupsInput{ 243 AutoScalingGroupNames: aws.StringSlice([]string{name}), 244 } 245 246 if err := s.ASGClient.WaitUntilGroupNotExists(input); err != nil { 247 return errors.Wrapf(err, "failed to wait for ASG %q deletion", name) 248 } 249 250 return nil 251 } 252 253 // DeleteASG will delete the ASG of a service. 254 func (s *Service) DeleteASG(name string) error { 255 s.scope.V(2).Info("Attempting to delete ASG", "name", name) 256 257 input := &autoscaling.DeleteAutoScalingGroupInput{ 258 AutoScalingGroupName: aws.String(name), 259 ForceDelete: aws.Bool(true), 260 } 261 262 if _, err := s.ASGClient.DeleteAutoScalingGroup(input); err != nil { 263 return errors.Wrapf(err, "failed to delete ASG %q", name) 264 } 265 266 s.scope.V(2).Info("Deleted ASG", "name", name) 267 return nil 268 } 269 270 // UpdateASG will update the ASG of a service. 271 func (s *Service) UpdateASG(scope *scope.MachinePoolScope) error { 272 subnetIDs, err := s.SubnetIDs(scope) 273 if err != nil { 274 return fmt.Errorf("getting subnets for ASG: %w", err) 275 } 276 277 input := &autoscaling.UpdateAutoScalingGroupInput{ 278 AutoScalingGroupName: aws.String(scope.Name()), //TODO: define dynamically - borrow logic from ec2 279 MaxSize: aws.Int64(int64(scope.AWSMachinePool.Spec.MaxSize)), 280 MinSize: aws.Int64(int64(scope.AWSMachinePool.Spec.MinSize)), 281 VPCZoneIdentifier: aws.String(strings.Join(subnetIDs, ", ")), 282 CapacityRebalance: aws.Bool(scope.AWSMachinePool.Spec.CapacityRebalance), 283 } 284 285 if scope.MachinePool.Spec.Replicas != nil { 286 input.DesiredCapacity = aws.Int64(int64(*scope.MachinePool.Spec.Replicas)) 287 } 288 289 if scope.AWSMachinePool.Spec.MixedInstancesPolicy != nil { 290 input.MixedInstancesPolicy = createSDKMixedInstancesPolicy(scope.Name(), scope.AWSMachinePool.Spec.MixedInstancesPolicy) 291 } else { 292 input.LaunchTemplate = &autoscaling.LaunchTemplateSpecification{ 293 LaunchTemplateId: aws.String(scope.AWSMachinePool.Status.LaunchTemplateID), 294 Version: aws.String(expinfrav1.LaunchTemplateLatestVersion), 295 } 296 } 297 298 if _, err := s.ASGClient.UpdateAutoScalingGroup(input); err != nil { 299 return errors.Wrapf(err, "failed to update ASG %q", scope.Name()) 300 } 301 302 return nil 303 } 304 305 // CanStartASGInstanceRefresh will start an ASG instance with refresh. 306 func (s *Service) CanStartASGInstanceRefresh(scope *scope.MachinePoolScope) (bool, error) { 307 describeInput := &autoscaling.DescribeInstanceRefreshesInput{AutoScalingGroupName: aws.String(scope.Name())} 308 refreshes, err := s.ASGClient.DescribeInstanceRefreshes(describeInput) 309 if err != nil { 310 return false, err 311 } 312 hasUnfinishedRefresh := false 313 if err == nil && len(refreshes.InstanceRefreshes) != 0 { 314 for i := range refreshes.InstanceRefreshes { 315 if *refreshes.InstanceRefreshes[i].Status == autoscaling.InstanceRefreshStatusInProgress || 316 *refreshes.InstanceRefreshes[i].Status == autoscaling.InstanceRefreshStatusPending || 317 *refreshes.InstanceRefreshes[i].Status == autoscaling.InstanceRefreshStatusCancelling { 318 hasUnfinishedRefresh = true 319 } 320 } 321 } 322 if hasUnfinishedRefresh { 323 return false, nil 324 } 325 return true, nil 326 } 327 328 // StartASGInstanceRefresh will start an ASG instance with refresh. 329 func (s *Service) StartASGInstanceRefresh(scope *scope.MachinePoolScope) error { 330 strategy := pointer.StringPtr(autoscaling.RefreshStrategyRolling) 331 var minHealthyPercentage, instanceWarmup *int64 332 if scope.AWSMachinePool.Spec.RefreshPreferences != nil { 333 if scope.AWSMachinePool.Spec.RefreshPreferences.Strategy != nil { 334 strategy = scope.AWSMachinePool.Spec.RefreshPreferences.Strategy 335 } 336 if scope.AWSMachinePool.Spec.RefreshPreferences.InstanceWarmup != nil { 337 instanceWarmup = scope.AWSMachinePool.Spec.RefreshPreferences.InstanceWarmup 338 } 339 if scope.AWSMachinePool.Spec.RefreshPreferences.MinHealthyPercentage != nil { 340 minHealthyPercentage = scope.AWSMachinePool.Spec.RefreshPreferences.MinHealthyPercentage 341 } 342 } 343 344 input := &autoscaling.StartInstanceRefreshInput{ 345 AutoScalingGroupName: aws.String(scope.Name()), 346 Strategy: strategy, 347 Preferences: &autoscaling.RefreshPreferences{ 348 InstanceWarmup: instanceWarmup, 349 MinHealthyPercentage: minHealthyPercentage, 350 }, 351 } 352 353 if _, err := s.ASGClient.StartInstanceRefresh(input); err != nil { 354 return errors.Wrapf(err, "failed to start ASG instance refresh %q", scope.Name()) 355 } 356 357 return nil 358 } 359 360 func createSDKMixedInstancesPolicy(name string, i *expinfrav1.MixedInstancesPolicy) *autoscaling.MixedInstancesPolicy { 361 mixedInstancesPolicy := &autoscaling.MixedInstancesPolicy{ 362 LaunchTemplate: &autoscaling.LaunchTemplate{ 363 LaunchTemplateSpecification: &autoscaling.LaunchTemplateSpecification{ 364 LaunchTemplateName: aws.String(name), 365 Version: aws.String(expinfrav1.LaunchTemplateLatestVersion), 366 }, 367 }, 368 } 369 370 if i.InstancesDistribution != nil { 371 mixedInstancesPolicy.InstancesDistribution = &autoscaling.InstancesDistribution{ 372 OnDemandAllocationStrategy: aws.String(string(i.InstancesDistribution.OnDemandAllocationStrategy)), 373 OnDemandBaseCapacity: i.InstancesDistribution.OnDemandBaseCapacity, 374 OnDemandPercentageAboveBaseCapacity: i.InstancesDistribution.OnDemandPercentageAboveBaseCapacity, 375 SpotAllocationStrategy: aws.String(string(i.InstancesDistribution.SpotAllocationStrategy)), 376 } 377 } 378 379 for _, override := range i.Overrides { 380 mixedInstancesPolicy.LaunchTemplate.Overrides = append(mixedInstancesPolicy.LaunchTemplate.Overrides, &autoscaling.LaunchTemplateOverrides{ 381 InstanceType: aws.String(override.InstanceType), 382 }) 383 } 384 385 return mixedInstancesPolicy 386 } 387 388 // BuildTagsFromMap takes a map of keys and values and returns them as autoscaling group tags. 389 func BuildTagsFromMap(asgName string, inTags map[string]string) []*autoscaling.Tag { 390 if inTags == nil { 391 return nil 392 } 393 tags := make([]*autoscaling.Tag, 0) 394 for k, v := range inTags { 395 tags = append(tags, &autoscaling.Tag{ 396 Key: aws.String(k), 397 Value: aws.String(v), 398 // We set the instance tags in the LaunchTemplate, disabling propagation to prevent the two 399 // resources from clobbering the tags set in the LaunchTemplate 400 PropagateAtLaunch: aws.Bool(false), 401 ResourceId: aws.String(asgName), 402 ResourceType: aws.String("auto-scaling-group"), 403 }) 404 } 405 406 return tags 407 } 408 409 // UpdateResourceTags updates the tags for an autoscaling group. 410 // This will be called if there is anything to create (update) or delete. 411 // We may not always have to perform each action, so we check what we're 412 // receiving to avoid calling AWS if we don't need to. 413 func (s *Service) UpdateResourceTags(resourceID *string, create, remove map[string]string) error { 414 s.scope.V(2).Info("Attempting to update tags on resource", "resource-id", *resourceID) 415 s.scope.Info("updating tags on resource", "resource-id", *resourceID, "create", create, "remove", remove) 416 417 // If we have anything to create or update 418 if len(create) > 0 { 419 s.scope.V(2).Info("Attempting to create tags on resource", "resource-id", *resourceID) 420 421 createOrUpdateTagsInput := &autoscaling.CreateOrUpdateTagsInput{} 422 423 createOrUpdateTagsInput.Tags = mapToTags(create, resourceID) 424 425 if _, err := s.ASGClient.CreateOrUpdateTags(createOrUpdateTagsInput); err != nil { 426 return errors.Wrapf(err, "failed to update tags on AutoScalingGroup %q", *resourceID) 427 } 428 } 429 430 // If we have anything to remove 431 if len(remove) > 0 { 432 s.scope.V(2).Info("Attempting to delete tags on resource", "resource-id", *resourceID) 433 434 // Convert our remove map into an array of *ec2.Tag 435 removeTagsInput := mapToTags(remove, resourceID) 436 437 // Create the DeleteTags input 438 input := &autoscaling.DeleteTagsInput{ 439 Tags: removeTagsInput, 440 } 441 442 // Delete tags in AWS. 443 if _, err := s.ASGClient.DeleteTags(input); err != nil { 444 return errors.Wrapf(err, "failed to delete tags on AutoScalingGroup %q: %v", *resourceID, remove) 445 } 446 } 447 448 return nil 449 } 450 451 func mapToTags(input map[string]string, resourceID *string) []*autoscaling.Tag { 452 tags := make([]*autoscaling.Tag, 0) 453 for k, v := range input { 454 tags = append(tags, &autoscaling.Tag{ 455 Key: aws.String(k), 456 PropagateAtLaunch: aws.Bool(false), 457 ResourceId: resourceID, 458 ResourceType: aws.String("auto-scaling-group"), 459 Value: aws.String(v), 460 }) 461 } 462 return tags 463 } 464 465 // SubnetIDs return subnet IDs of a AWSMachinePool based on given subnetIDs and filters. 466 func (s *Service) SubnetIDs(scope *scope.MachinePoolScope) ([]string, error) { 467 subnetIDs := make([]string, 0) 468 var inputFilters = make([]*ec2.Filter, 0) 469 470 for _, subnet := range scope.AWSMachinePool.Spec.Subnets { 471 switch { 472 case subnet.ID != nil: 473 subnetIDs = append(subnetIDs, aws.StringValue(subnet.ID)) 474 case subnet.Filters != nil: 475 for _, eachFilter := range subnet.Filters { 476 inputFilters = append(inputFilters, &ec2.Filter{ 477 Name: aws.String(eachFilter.Name), 478 Values: aws.StringSlice(eachFilter.Values), 479 }) 480 } 481 } 482 } 483 484 if len(inputFilters) > 0 { 485 out, err := s.EC2Client.DescribeSubnets(&ec2.DescribeSubnetsInput{ 486 Filters: inputFilters, 487 }) 488 if err != nil { 489 return nil, err 490 } 491 492 for _, subnet := range out.Subnets { 493 subnetIDs = append(subnetIDs, *subnet.SubnetId) 494 } 495 496 if len(subnetIDs) == 0 { 497 errMessage := fmt.Sprintf("failed to create ASG %q, no subnets available matching criteria %q", scope.Name(), inputFilters) 498 record.Warnf(scope.AWSMachinePool, "FailedCreate", errMessage) 499 return subnetIDs, awserrors.NewFailedDependency(errMessage) 500 } 501 } 502 503 return scope.SubnetIDs(subnetIDs) 504 }