github.com/nathanielks/terraform@v0.6.1-0.20170509030759-13e1a62319dc/builtin/providers/aws/resource_aws_spot_fleet_request.go (about) 1 package aws 2 3 import ( 4 "bytes" 5 "fmt" 6 "log" 7 "strconv" 8 "time" 9 10 "github.com/aws/aws-sdk-go/aws" 11 "github.com/aws/aws-sdk-go/aws/awserr" 12 "github.com/aws/aws-sdk-go/service/ec2" 13 "github.com/hashicorp/terraform/helper/hashcode" 14 "github.com/hashicorp/terraform/helper/resource" 15 "github.com/hashicorp/terraform/helper/schema" 16 ) 17 18 func resourceAwsSpotFleetRequest() *schema.Resource { 19 return &schema.Resource{ 20 Create: resourceAwsSpotFleetRequestCreate, 21 Read: resourceAwsSpotFleetRequestRead, 22 Delete: resourceAwsSpotFleetRequestDelete, 23 Update: resourceAwsSpotFleetRequestUpdate, 24 25 SchemaVersion: 1, 26 MigrateState: resourceAwsSpotFleetRequestMigrateState, 27 28 Schema: map[string]*schema.Schema{ 29 "iam_fleet_role": { 30 Type: schema.TypeString, 31 Required: true, 32 ForceNew: true, 33 }, 34 "replace_unhealthy_instances": { 35 Type: schema.TypeBool, 36 Optional: true, 37 ForceNew: true, 38 Default: false, 39 }, 40 // http://docs.aws.amazon.com/sdk-for-go/api/service/ec2.html#type-SpotFleetLaunchSpecification 41 // http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_SpotFleetLaunchSpecification.html 42 "launch_specification": { 43 Type: schema.TypeSet, 44 Required: true, 45 ForceNew: true, 46 Elem: &schema.Resource{ 47 Schema: map[string]*schema.Schema{ 48 "vpc_security_group_ids": { 49 Type: schema.TypeSet, 50 Optional: true, 51 Computed: true, 52 Elem: &schema.Schema{Type: schema.TypeString}, 53 Set: schema.HashString, 54 }, 55 "associate_public_ip_address": { 56 Type: schema.TypeBool, 57 Optional: true, 58 Default: false, 59 }, 60 "ebs_block_device": { 61 Type: schema.TypeSet, 62 Optional: true, 63 Computed: true, 64 Elem: &schema.Resource{ 65 Schema: map[string]*schema.Schema{ 66 "delete_on_termination": { 67 Type: schema.TypeBool, 68 Optional: true, 69 Default: true, 70 ForceNew: true, 71 }, 72 "device_name": { 73 Type: schema.TypeString, 74 Required: true, 75 ForceNew: true, 76 }, 77 "encrypted": { 78 Type: schema.TypeBool, 79 Optional: true, 80 Computed: true, 81 ForceNew: true, 82 }, 83 "iops": { 84 Type: schema.TypeInt, 85 Optional: true, 86 Computed: true, 87 ForceNew: true, 88 }, 89 "snapshot_id": { 90 Type: schema.TypeString, 91 Optional: true, 92 Computed: true, 93 ForceNew: true, 94 }, 95 "volume_size": { 96 Type: schema.TypeInt, 97 Optional: true, 98 Computed: true, 99 ForceNew: true, 100 }, 101 "volume_type": { 102 Type: schema.TypeString, 103 Optional: true, 104 Computed: true, 105 ForceNew: true, 106 }, 107 }, 108 }, 109 Set: hashEbsBlockDevice, 110 }, 111 "ephemeral_block_device": { 112 Type: schema.TypeSet, 113 Optional: true, 114 Computed: true, 115 ForceNew: true, 116 Elem: &schema.Resource{ 117 Schema: map[string]*schema.Schema{ 118 "device_name": { 119 Type: schema.TypeString, 120 Required: true, 121 }, 122 "virtual_name": { 123 Type: schema.TypeString, 124 Required: true, 125 }, 126 }, 127 }, 128 Set: hashEphemeralBlockDevice, 129 }, 130 "root_block_device": { 131 // TODO: This is a set because we don't support singleton 132 // sub-resources today. We'll enforce that the set only ever has 133 // length zero or one below. When TF gains support for 134 // sub-resources this can be converted. 135 Type: schema.TypeSet, 136 Optional: true, 137 Computed: true, 138 Elem: &schema.Resource{ 139 // "You can only modify the volume size, volume type, and Delete on 140 // Termination flag on the block device mapping entry for the root 141 // device volume." - bit.ly/ec2bdmap 142 Schema: map[string]*schema.Schema{ 143 "delete_on_termination": { 144 Type: schema.TypeBool, 145 Optional: true, 146 Default: true, 147 ForceNew: true, 148 }, 149 "iops": { 150 Type: schema.TypeInt, 151 Optional: true, 152 Computed: true, 153 ForceNew: true, 154 }, 155 "volume_size": { 156 Type: schema.TypeInt, 157 Optional: true, 158 Computed: true, 159 ForceNew: true, 160 }, 161 "volume_type": { 162 Type: schema.TypeString, 163 Optional: true, 164 Computed: true, 165 ForceNew: true, 166 }, 167 }, 168 }, 169 Set: hashRootBlockDevice, 170 }, 171 "ebs_optimized": { 172 Type: schema.TypeBool, 173 Optional: true, 174 Default: false, 175 }, 176 "iam_instance_profile": { 177 Type: schema.TypeString, 178 ForceNew: true, 179 Optional: true, 180 }, 181 "ami": { 182 Type: schema.TypeString, 183 Required: true, 184 ForceNew: true, 185 }, 186 "instance_type": { 187 Type: schema.TypeString, 188 Required: true, 189 ForceNew: true, 190 }, 191 "key_name": { 192 Type: schema.TypeString, 193 Optional: true, 194 ForceNew: true, 195 Computed: true, 196 ValidateFunc: validateSpotFleetRequestKeyName, 197 }, 198 "monitoring": { 199 Type: schema.TypeBool, 200 Optional: true, 201 Default: false, 202 }, 203 "placement_group": { 204 Type: schema.TypeString, 205 Optional: true, 206 Computed: true, 207 ForceNew: true, 208 }, 209 "placement_tenancy": { 210 Type: schema.TypeString, 211 Optional: true, 212 ForceNew: true, 213 }, 214 "spot_price": { 215 Type: schema.TypeString, 216 Optional: true, 217 ForceNew: true, 218 }, 219 "user_data": { 220 Type: schema.TypeString, 221 Optional: true, 222 ForceNew: true, 223 StateFunc: func(v interface{}) string { 224 switch v.(type) { 225 case string: 226 return userDataHashSum(v.(string)) 227 default: 228 return "" 229 } 230 }, 231 }, 232 "weighted_capacity": { 233 Type: schema.TypeString, 234 Optional: true, 235 ForceNew: true, 236 }, 237 "subnet_id": { 238 Type: schema.TypeString, 239 Optional: true, 240 Computed: true, 241 ForceNew: true, 242 }, 243 "availability_zone": { 244 Type: schema.TypeString, 245 Optional: true, 246 Computed: true, 247 ForceNew: true, 248 }, 249 }, 250 }, 251 Set: hashLaunchSpecification, 252 }, 253 // Everything on a spot fleet is ForceNew except target_capacity 254 "target_capacity": { 255 Type: schema.TypeInt, 256 Required: true, 257 ForceNew: false, 258 }, 259 "allocation_strategy": { 260 Type: schema.TypeString, 261 Optional: true, 262 Default: "lowestPrice", 263 ForceNew: true, 264 }, 265 "excess_capacity_termination_policy": { 266 Type: schema.TypeString, 267 Optional: true, 268 Default: "Default", 269 ForceNew: false, 270 }, 271 "spot_price": { 272 Type: schema.TypeString, 273 Required: true, 274 ForceNew: true, 275 }, 276 "terminate_instances_with_expiration": { 277 Type: schema.TypeBool, 278 Optional: true, 279 ForceNew: true, 280 }, 281 "valid_from": { 282 Type: schema.TypeString, 283 Optional: true, 284 ForceNew: true, 285 }, 286 "valid_until": { 287 Type: schema.TypeString, 288 Optional: true, 289 ForceNew: true, 290 }, 291 "spot_request_state": { 292 Type: schema.TypeString, 293 Computed: true, 294 }, 295 "client_token": { 296 Type: schema.TypeString, 297 Computed: true, 298 }, 299 }, 300 } 301 } 302 303 func buildSpotFleetLaunchSpecification(d map[string]interface{}, meta interface{}) (*ec2.SpotFleetLaunchSpecification, error) { 304 conn := meta.(*AWSClient).ec2conn 305 306 opts := &ec2.SpotFleetLaunchSpecification{ 307 ImageId: aws.String(d["ami"].(string)), 308 InstanceType: aws.String(d["instance_type"].(string)), 309 SpotPrice: aws.String(d["spot_price"].(string)), 310 } 311 312 placement := new(ec2.SpotPlacement) 313 if v, ok := d["availability_zone"]; ok { 314 placement.AvailabilityZone = aws.String(v.(string)) 315 opts.Placement = placement 316 } 317 318 if v, ok := d["placement_tenancy"]; ok { 319 placement.Tenancy = aws.String(v.(string)) 320 opts.Placement = placement 321 } 322 323 if v, ok := d["ebs_optimized"]; ok { 324 opts.EbsOptimized = aws.Bool(v.(bool)) 325 } 326 327 if v, ok := d["monitoring"]; ok { 328 opts.Monitoring = &ec2.SpotFleetMonitoring{ 329 Enabled: aws.Bool(v.(bool)), 330 } 331 } 332 333 if v, ok := d["iam_instance_profile"]; ok { 334 opts.IamInstanceProfile = &ec2.IamInstanceProfileSpecification{ 335 Name: aws.String(v.(string)), 336 } 337 } 338 339 if v, ok := d["user_data"]; ok { 340 opts.UserData = aws.String(base64Encode([]byte(v.(string)))) 341 } 342 343 if v, ok := d["key_name"]; ok { 344 opts.KeyName = aws.String(v.(string)) 345 } 346 347 if v, ok := d["weighted_capacity"]; ok && v != "" { 348 wc, err := strconv.ParseFloat(v.(string), 64) 349 if err != nil { 350 return nil, err 351 } 352 opts.WeightedCapacity = aws.Float64(wc) 353 } 354 355 var securityGroupIds []*string 356 if v, ok := d["vpc_security_group_ids"]; ok { 357 if s := v.(*schema.Set); s.Len() > 0 { 358 for _, v := range s.List() { 359 securityGroupIds = append(securityGroupIds, aws.String(v.(string))) 360 } 361 } 362 } 363 364 subnetId, hasSubnetId := d["subnet_id"] 365 if hasSubnetId { 366 opts.SubnetId = aws.String(subnetId.(string)) 367 } 368 369 associatePublicIpAddress, hasPublicIpAddress := d["associate_public_ip_address"] 370 if hasPublicIpAddress && associatePublicIpAddress.(bool) == true && hasSubnetId { 371 372 // If we have a non-default VPC / Subnet specified, we can flag 373 // AssociatePublicIpAddress to get a Public IP assigned. By default these are not provided. 374 // You cannot specify both SubnetId and the NetworkInterface.0.* parameters though, otherwise 375 // you get: Network interfaces and an instance-level subnet ID may not be specified on the same request 376 // You also need to attach Security Groups to the NetworkInterface instead of the instance, 377 // to avoid: Network interfaces and an instance-level security groups may not be specified on 378 // the same request 379 ni := &ec2.InstanceNetworkInterfaceSpecification{ 380 AssociatePublicIpAddress: aws.Bool(true), 381 DeleteOnTermination: aws.Bool(true), 382 DeviceIndex: aws.Int64(int64(0)), 383 SubnetId: aws.String(subnetId.(string)), 384 Groups: securityGroupIds, 385 } 386 387 opts.NetworkInterfaces = []*ec2.InstanceNetworkInterfaceSpecification{ni} 388 opts.SubnetId = aws.String("") 389 } else { 390 for _, id := range securityGroupIds { 391 opts.SecurityGroups = append(opts.SecurityGroups, &ec2.GroupIdentifier{GroupId: id}) 392 } 393 } 394 395 blockDevices, err := readSpotFleetBlockDeviceMappingsFromConfig(d, conn) 396 if err != nil { 397 return nil, err 398 } 399 if len(blockDevices) > 0 { 400 opts.BlockDeviceMappings = blockDevices 401 } 402 403 return opts, nil 404 } 405 406 func validateSpotFleetRequestKeyName(v interface{}, k string) (ws []string, errors []error) { 407 value := v.(string) 408 409 if value == "" { 410 errors = append(errors, fmt.Errorf("Key name cannot be empty.")) 411 } 412 413 return 414 } 415 416 func readSpotFleetBlockDeviceMappingsFromConfig( 417 d map[string]interface{}, conn *ec2.EC2) ([]*ec2.BlockDeviceMapping, error) { 418 blockDevices := make([]*ec2.BlockDeviceMapping, 0) 419 420 if v, ok := d["ebs_block_device"]; ok { 421 vL := v.(*schema.Set).List() 422 for _, v := range vL { 423 bd := v.(map[string]interface{}) 424 ebs := &ec2.EbsBlockDevice{ 425 DeleteOnTermination: aws.Bool(bd["delete_on_termination"].(bool)), 426 } 427 428 if v, ok := bd["snapshot_id"].(string); ok && v != "" { 429 ebs.SnapshotId = aws.String(v) 430 } 431 432 if v, ok := bd["encrypted"].(bool); ok && v { 433 ebs.Encrypted = aws.Bool(v) 434 } 435 436 if v, ok := bd["volume_size"].(int); ok && v != 0 { 437 ebs.VolumeSize = aws.Int64(int64(v)) 438 } 439 440 if v, ok := bd["volume_type"].(string); ok && v != "" { 441 ebs.VolumeType = aws.String(v) 442 } 443 444 if v, ok := bd["iops"].(int); ok && v > 0 { 445 ebs.Iops = aws.Int64(int64(v)) 446 } 447 448 blockDevices = append(blockDevices, &ec2.BlockDeviceMapping{ 449 DeviceName: aws.String(bd["device_name"].(string)), 450 Ebs: ebs, 451 }) 452 } 453 } 454 455 if v, ok := d["ephemeral_block_device"]; ok { 456 vL := v.(*schema.Set).List() 457 for _, v := range vL { 458 bd := v.(map[string]interface{}) 459 blockDevices = append(blockDevices, &ec2.BlockDeviceMapping{ 460 DeviceName: aws.String(bd["device_name"].(string)), 461 VirtualName: aws.String(bd["virtual_name"].(string)), 462 }) 463 } 464 } 465 466 if v, ok := d["root_block_device"]; ok { 467 vL := v.(*schema.Set).List() 468 if len(vL) > 1 { 469 return nil, fmt.Errorf("Cannot specify more than one root_block_device.") 470 } 471 for _, v := range vL { 472 bd := v.(map[string]interface{}) 473 ebs := &ec2.EbsBlockDevice{ 474 DeleteOnTermination: aws.Bool(bd["delete_on_termination"].(bool)), 475 } 476 477 if v, ok := bd["volume_size"].(int); ok && v != 0 { 478 ebs.VolumeSize = aws.Int64(int64(v)) 479 } 480 481 if v, ok := bd["volume_type"].(string); ok && v != "" { 482 ebs.VolumeType = aws.String(v) 483 } 484 485 if v, ok := bd["iops"].(int); ok && v > 0 { 486 ebs.Iops = aws.Int64(int64(v)) 487 } 488 489 if dn, err := fetchRootDeviceName(d["ami"].(string), conn); err == nil { 490 if dn == nil { 491 return nil, fmt.Errorf( 492 "Expected 1 AMI for ID: %s, got none", 493 d["ami"].(string)) 494 } 495 496 blockDevices = append(blockDevices, &ec2.BlockDeviceMapping{ 497 DeviceName: dn, 498 Ebs: ebs, 499 }) 500 } else { 501 return nil, err 502 } 503 } 504 } 505 506 return blockDevices, nil 507 } 508 509 func buildAwsSpotFleetLaunchSpecifications( 510 d *schema.ResourceData, meta interface{}) ([]*ec2.SpotFleetLaunchSpecification, error) { 511 512 user_specs := d.Get("launch_specification").(*schema.Set).List() 513 specs := make([]*ec2.SpotFleetLaunchSpecification, len(user_specs)) 514 for i, user_spec := range user_specs { 515 user_spec_map := user_spec.(map[string]interface{}) 516 // panic: interface conversion: interface {} is map[string]interface {}, not *schema.ResourceData 517 opts, err := buildSpotFleetLaunchSpecification(user_spec_map, meta) 518 if err != nil { 519 return nil, err 520 } 521 specs[i] = opts 522 } 523 524 return specs, nil 525 } 526 527 func resourceAwsSpotFleetRequestCreate(d *schema.ResourceData, meta interface{}) error { 528 // http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_RequestSpotFleet.html 529 conn := meta.(*AWSClient).ec2conn 530 531 launch_specs, err := buildAwsSpotFleetLaunchSpecifications(d, meta) 532 if err != nil { 533 return err 534 } 535 536 // http://docs.aws.amazon.com/sdk-for-go/api/service/ec2.html#type-SpotFleetRequestConfigData 537 spotFleetConfig := &ec2.SpotFleetRequestConfigData{ 538 IamFleetRole: aws.String(d.Get("iam_fleet_role").(string)), 539 LaunchSpecifications: launch_specs, 540 SpotPrice: aws.String(d.Get("spot_price").(string)), 541 TargetCapacity: aws.Int64(int64(d.Get("target_capacity").(int))), 542 ClientToken: aws.String(resource.UniqueId()), 543 TerminateInstancesWithExpiration: aws.Bool(d.Get("terminate_instances_with_expiration").(bool)), 544 ReplaceUnhealthyInstances: aws.Bool(d.Get("replace_unhealthy_instances").(bool)), 545 } 546 547 if v, ok := d.GetOk("excess_capacity_termination_policy"); ok { 548 spotFleetConfig.ExcessCapacityTerminationPolicy = aws.String(v.(string)) 549 } 550 551 if v, ok := d.GetOk("allocation_strategy"); ok { 552 spotFleetConfig.AllocationStrategy = aws.String(v.(string)) 553 } else { 554 spotFleetConfig.AllocationStrategy = aws.String("lowestPrice") 555 } 556 557 if v, ok := d.GetOk("valid_from"); ok { 558 valid_from, err := time.Parse(awsAutoscalingScheduleTimeLayout, v.(string)) 559 if err != nil { 560 return err 561 } 562 spotFleetConfig.ValidFrom = &valid_from 563 } 564 565 if v, ok := d.GetOk("valid_until"); ok { 566 valid_until, err := time.Parse(awsAutoscalingScheduleTimeLayout, v.(string)) 567 if err != nil { 568 return err 569 } 570 spotFleetConfig.ValidUntil = &valid_until 571 } else { 572 valid_until := time.Now().Add(24 * time.Hour) 573 spotFleetConfig.ValidUntil = &valid_until 574 } 575 576 // http://docs.aws.amazon.com/sdk-for-go/api/service/ec2.html#type-RequestSpotFleetInput 577 spotFleetOpts := &ec2.RequestSpotFleetInput{ 578 SpotFleetRequestConfig: spotFleetConfig, 579 DryRun: aws.Bool(false), 580 } 581 582 log.Printf("[DEBUG] Requesting spot fleet with these opts: %+v", spotFleetOpts) 583 584 // Since IAM is eventually consistent, we retry creation as a newly created role may not 585 // take effect immediately, resulting in an InvalidSpotFleetRequestConfig error 586 var resp *ec2.RequestSpotFleetOutput 587 err = resource.Retry(1*time.Minute, func() *resource.RetryError { 588 var err error 589 resp, err = conn.RequestSpotFleet(spotFleetOpts) 590 591 if err != nil { 592 if awsErr, ok := err.(awserr.Error); ok { 593 // IAM is eventually consistent :/ 594 if awsErr.Code() == "InvalidSpotFleetRequestConfig" { 595 return resource.RetryableError( 596 fmt.Errorf("[WARN] Error creating Spot fleet request, retrying: %s", err)) 597 } 598 } 599 return resource.NonRetryableError(err) 600 } 601 return nil 602 }) 603 604 if err != nil { 605 return fmt.Errorf("Error requesting spot fleet: %s", err) 606 } 607 608 d.SetId(*resp.SpotFleetRequestId) 609 610 log.Printf("[INFO] Spot Fleet Request ID: %s", d.Id()) 611 log.Println("[INFO] Waiting for Spot Fleet Request to be active") 612 stateConf := &resource.StateChangeConf{ 613 Pending: []string{"submitted"}, 614 Target: []string{"active"}, 615 Refresh: resourceAwsSpotFleetRequestStateRefreshFunc(d, meta), 616 Timeout: 10 * time.Minute, 617 MinTimeout: 10 * time.Second, 618 Delay: 30 * time.Second, 619 } 620 621 _, err = stateConf.WaitForState() 622 if err != nil { 623 return err 624 } 625 626 return resourceAwsSpotFleetRequestRead(d, meta) 627 } 628 629 func resourceAwsSpotFleetRequestStateRefreshFunc(d *schema.ResourceData, meta interface{}) resource.StateRefreshFunc { 630 return func() (interface{}, string, error) { 631 conn := meta.(*AWSClient).ec2conn 632 req := &ec2.DescribeSpotFleetRequestsInput{ 633 SpotFleetRequestIds: []*string{aws.String(d.Id())}, 634 } 635 resp, err := conn.DescribeSpotFleetRequests(req) 636 637 if err != nil { 638 log.Printf("Error on retrieving Spot Fleet Request when waiting: %s", err) 639 return nil, "", nil 640 } 641 642 if resp == nil { 643 return nil, "", nil 644 } 645 646 if len(resp.SpotFleetRequestConfigs) == 0 { 647 return nil, "", nil 648 } 649 650 spotFleetRequest := resp.SpotFleetRequestConfigs[0] 651 652 return spotFleetRequest, *spotFleetRequest.SpotFleetRequestState, nil 653 } 654 } 655 656 func resourceAwsSpotFleetRequestRead(d *schema.ResourceData, meta interface{}) error { 657 // http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeSpotFleetRequests.html 658 conn := meta.(*AWSClient).ec2conn 659 660 req := &ec2.DescribeSpotFleetRequestsInput{ 661 SpotFleetRequestIds: []*string{aws.String(d.Id())}, 662 } 663 resp, err := conn.DescribeSpotFleetRequests(req) 664 665 if err != nil { 666 // If the spot request was not found, return nil so that we can show 667 // that it is gone. 668 ec2err, ok := err.(awserr.Error) 669 if ok && ec2err.Code() == "InvalidSpotFleetRequestId.NotFound" { 670 d.SetId("") 671 return nil 672 } 673 674 // Some other error, report it 675 return err 676 } 677 678 sfr := resp.SpotFleetRequestConfigs[0] 679 680 // if the request is cancelled, then it is gone 681 cancelledStates := map[string]bool{ 682 "cancelled": true, 683 "cancelled_running": true, 684 "cancelled_terminating": true, 685 } 686 if _, ok := cancelledStates[*sfr.SpotFleetRequestState]; ok { 687 d.SetId("") 688 return nil 689 } 690 691 d.SetId(*sfr.SpotFleetRequestId) 692 d.Set("spot_request_state", aws.StringValue(sfr.SpotFleetRequestState)) 693 694 config := sfr.SpotFleetRequestConfig 695 696 if config.AllocationStrategy != nil { 697 d.Set("allocation_strategy", aws.StringValue(config.AllocationStrategy)) 698 } 699 700 if config.ClientToken != nil { 701 d.Set("client_token", aws.StringValue(config.ClientToken)) 702 } 703 704 if config.ExcessCapacityTerminationPolicy != nil { 705 d.Set("excess_capacity_termination_policy", 706 aws.StringValue(config.ExcessCapacityTerminationPolicy)) 707 } 708 709 if config.IamFleetRole != nil { 710 d.Set("iam_fleet_role", aws.StringValue(config.IamFleetRole)) 711 } 712 713 if config.SpotPrice != nil { 714 d.Set("spot_price", aws.StringValue(config.SpotPrice)) 715 } 716 717 if config.TargetCapacity != nil { 718 d.Set("target_capacity", aws.Int64Value(config.TargetCapacity)) 719 } 720 721 if config.TerminateInstancesWithExpiration != nil { 722 d.Set("terminate_instances_with_expiration", 723 aws.BoolValue(config.TerminateInstancesWithExpiration)) 724 } 725 726 if config.ValidFrom != nil { 727 d.Set("valid_from", 728 aws.TimeValue(config.ValidFrom).Format(awsAutoscalingScheduleTimeLayout)) 729 } 730 731 if config.ValidUntil != nil { 732 d.Set("valid_until", 733 aws.TimeValue(config.ValidUntil).Format(awsAutoscalingScheduleTimeLayout)) 734 } 735 736 d.Set("replace_unhealthy_instances", config.ReplaceUnhealthyInstances) 737 d.Set("launch_specification", launchSpecsToSet(config.LaunchSpecifications, conn)) 738 739 return nil 740 } 741 742 func launchSpecsToSet(launchSpecs []*ec2.SpotFleetLaunchSpecification, conn *ec2.EC2) *schema.Set { 743 specSet := &schema.Set{F: hashLaunchSpecification} 744 for _, spec := range launchSpecs { 745 rootDeviceName, err := fetchRootDeviceName(aws.StringValue(spec.ImageId), conn) 746 if err != nil { 747 log.Panic(err) 748 } 749 750 specSet.Add(launchSpecToMap(spec, rootDeviceName)) 751 } 752 return specSet 753 } 754 755 func launchSpecToMap(l *ec2.SpotFleetLaunchSpecification, rootDevName *string) map[string]interface{} { 756 m := make(map[string]interface{}) 757 758 m["root_block_device"] = rootBlockDeviceToSet(l.BlockDeviceMappings, rootDevName) 759 m["ebs_block_device"] = ebsBlockDevicesToSet(l.BlockDeviceMappings, rootDevName) 760 m["ephemeral_block_device"] = ephemeralBlockDevicesToSet(l.BlockDeviceMappings) 761 762 if l.ImageId != nil { 763 m["ami"] = aws.StringValue(l.ImageId) 764 } 765 766 if l.InstanceType != nil { 767 m["instance_type"] = aws.StringValue(l.InstanceType) 768 } 769 770 if l.SpotPrice != nil { 771 m["spot_price"] = aws.StringValue(l.SpotPrice) 772 } 773 774 if l.EbsOptimized != nil { 775 m["ebs_optimized"] = aws.BoolValue(l.EbsOptimized) 776 } 777 778 if l.Monitoring != nil && l.Monitoring.Enabled != nil { 779 m["monitoring"] = aws.BoolValue(l.Monitoring.Enabled) 780 } 781 782 if l.IamInstanceProfile != nil && l.IamInstanceProfile.Name != nil { 783 m["iam_instance_profile"] = aws.StringValue(l.IamInstanceProfile.Name) 784 } 785 786 if l.UserData != nil { 787 m["user_data"] = userDataHashSum(aws.StringValue(l.UserData)) 788 } 789 790 if l.KeyName != nil { 791 m["key_name"] = aws.StringValue(l.KeyName) 792 } 793 794 if l.Placement != nil { 795 m["availability_zone"] = aws.StringValue(l.Placement.AvailabilityZone) 796 } 797 798 if l.SubnetId != nil { 799 m["subnet_id"] = aws.StringValue(l.SubnetId) 800 } 801 802 securityGroupIds := &schema.Set{F: schema.HashString} 803 if len(l.NetworkInterfaces) > 0 { 804 // This resource auto-creates one network interface when associate_public_ip_address is true 805 for _, group := range l.NetworkInterfaces[0].Groups { 806 securityGroupIds.Add(aws.StringValue(group)) 807 } 808 } else { 809 for _, group := range l.SecurityGroups { 810 securityGroupIds.Add(aws.StringValue(group.GroupId)) 811 } 812 } 813 m["vpc_security_group_ids"] = securityGroupIds 814 815 if l.WeightedCapacity != nil { 816 m["weighted_capacity"] = strconv.FormatFloat(*l.WeightedCapacity, 'f', 0, 64) 817 } 818 819 return m 820 } 821 822 func ebsBlockDevicesToSet(bdm []*ec2.BlockDeviceMapping, rootDevName *string) *schema.Set { 823 set := &schema.Set{F: hashEbsBlockDevice} 824 825 for _, val := range bdm { 826 if val.Ebs != nil { 827 m := make(map[string]interface{}) 828 829 ebs := val.Ebs 830 831 if val.DeviceName != nil { 832 if aws.StringValue(rootDevName) == aws.StringValue(val.DeviceName) { 833 continue 834 } 835 836 m["device_name"] = aws.StringValue(val.DeviceName) 837 } 838 839 if ebs.DeleteOnTermination != nil { 840 m["delete_on_termination"] = aws.BoolValue(ebs.DeleteOnTermination) 841 } 842 843 if ebs.SnapshotId != nil { 844 m["snapshot_id"] = aws.StringValue(ebs.SnapshotId) 845 } 846 847 if ebs.Encrypted != nil { 848 m["encrypted"] = aws.BoolValue(ebs.Encrypted) 849 } 850 851 if ebs.VolumeSize != nil { 852 m["volume_size"] = aws.Int64Value(ebs.VolumeSize) 853 } 854 855 if ebs.VolumeType != nil { 856 m["volume_type"] = aws.StringValue(ebs.VolumeType) 857 } 858 859 if ebs.Iops != nil { 860 m["iops"] = aws.Int64Value(ebs.Iops) 861 } 862 863 set.Add(m) 864 } 865 } 866 867 return set 868 } 869 870 func ephemeralBlockDevicesToSet(bdm []*ec2.BlockDeviceMapping) *schema.Set { 871 set := &schema.Set{F: hashEphemeralBlockDevice} 872 873 for _, val := range bdm { 874 if val.VirtualName != nil { 875 m := make(map[string]interface{}) 876 m["virtual_name"] = aws.StringValue(val.VirtualName) 877 878 if val.DeviceName != nil { 879 m["device_name"] = aws.StringValue(val.DeviceName) 880 } 881 882 set.Add(m) 883 } 884 } 885 886 return set 887 } 888 889 func rootBlockDeviceToSet( 890 bdm []*ec2.BlockDeviceMapping, 891 rootDevName *string, 892 ) *schema.Set { 893 set := &schema.Set{F: hashRootBlockDevice} 894 895 if rootDevName != nil { 896 for _, val := range bdm { 897 if aws.StringValue(val.DeviceName) == aws.StringValue(rootDevName) { 898 m := make(map[string]interface{}) 899 if val.Ebs.DeleteOnTermination != nil { 900 m["delete_on_termination"] = aws.BoolValue(val.Ebs.DeleteOnTermination) 901 } 902 903 if val.Ebs.VolumeSize != nil { 904 m["volume_size"] = aws.Int64Value(val.Ebs.VolumeSize) 905 } 906 907 if val.Ebs.VolumeType != nil { 908 m["volume_type"] = aws.StringValue(val.Ebs.VolumeType) 909 } 910 911 if val.Ebs.Iops != nil { 912 m["iops"] = aws.Int64Value(val.Ebs.Iops) 913 } 914 915 set.Add(m) 916 } 917 } 918 } 919 920 return set 921 } 922 923 func resourceAwsSpotFleetRequestUpdate(d *schema.ResourceData, meta interface{}) error { 924 // http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_ModifySpotFleetRequest.html 925 conn := meta.(*AWSClient).ec2conn 926 927 d.Partial(true) 928 929 req := &ec2.ModifySpotFleetRequestInput{ 930 SpotFleetRequestId: aws.String(d.Id()), 931 } 932 933 if val, ok := d.GetOk("target_capacity"); ok { 934 req.TargetCapacity = aws.Int64(int64(val.(int))) 935 } 936 937 if val, ok := d.GetOk("excess_capacity_termination_policy"); ok { 938 req.ExcessCapacityTerminationPolicy = aws.String(val.(string)) 939 } 940 941 resp, err := conn.ModifySpotFleetRequest(req) 942 if err == nil && aws.BoolValue(resp.Return) { 943 // TODO: rollback to old values? 944 } 945 946 return nil 947 } 948 949 func resourceAwsSpotFleetRequestDelete(d *schema.ResourceData, meta interface{}) error { 950 // http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CancelSpotFleetRequests.html 951 conn := meta.(*AWSClient).ec2conn 952 terminateInstances := d.Get("terminate_instances_with_expiration").(bool) 953 954 log.Printf("[INFO] Cancelling spot fleet request: %s", d.Id()) 955 resp, err := conn.CancelSpotFleetRequests(&ec2.CancelSpotFleetRequestsInput{ 956 SpotFleetRequestIds: []*string{aws.String(d.Id())}, 957 TerminateInstances: aws.Bool(terminateInstances), 958 }) 959 960 if err != nil { 961 return fmt.Errorf("Error cancelling spot request (%s): %s", d.Id(), err) 962 } 963 964 // check response successfulFleetRequestSet to make sure our request was canceled 965 var found bool 966 for _, s := range resp.SuccessfulFleetRequests { 967 if *s.SpotFleetRequestId == d.Id() { 968 found = true 969 } 970 } 971 972 if !found { 973 return fmt.Errorf("[ERR] Spot Fleet request (%s) was not found to be successfully canceled, dangling resources may exit", d.Id()) 974 } 975 976 // Only wait for instance termination if requested 977 if !terminateInstances { 978 return nil 979 } 980 981 return resource.Retry(5*time.Minute, func() *resource.RetryError { 982 resp, err := conn.DescribeSpotFleetInstances(&ec2.DescribeSpotFleetInstancesInput{ 983 SpotFleetRequestId: aws.String(d.Id()), 984 }) 985 if err != nil { 986 return resource.NonRetryableError(err) 987 } 988 989 if len(resp.ActiveInstances) == 0 { 990 log.Printf("[DEBUG] Active instance count is 0 for Spot Fleet Request (%s), removing", d.Id()) 991 return nil 992 } 993 994 log.Printf("[DEBUG] Active instance count in Spot Fleet Request (%s): %d", d.Id(), len(resp.ActiveInstances)) 995 996 return resource.RetryableError( 997 fmt.Errorf("fleet still has (%d) running instances", len(resp.ActiveInstances))) 998 }) 999 } 1000 1001 func hashEphemeralBlockDevice(v interface{}) int { 1002 var buf bytes.Buffer 1003 m := v.(map[string]interface{}) 1004 buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) 1005 buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string))) 1006 return hashcode.String(buf.String()) 1007 } 1008 1009 func hashRootBlockDevice(v interface{}) int { 1010 // there can be only one root device; no need to hash anything 1011 return 0 1012 } 1013 1014 func hashLaunchSpecification(v interface{}) int { 1015 var buf bytes.Buffer 1016 m := v.(map[string]interface{}) 1017 buf.WriteString(fmt.Sprintf("%s-", m["ami"].(string))) 1018 if m["availability_zone"] != "" { 1019 buf.WriteString(fmt.Sprintf("%s-", m["availability_zone"].(string))) 1020 } 1021 if m["subnet_id"] != "" { 1022 buf.WriteString(fmt.Sprintf("%s-", m["subnet_id"].(string))) 1023 } 1024 buf.WriteString(fmt.Sprintf("%s-", m["instance_type"].(string))) 1025 buf.WriteString(fmt.Sprintf("%s-", m["spot_price"].(string))) 1026 return hashcode.String(buf.String()) 1027 } 1028 1029 func hashEbsBlockDevice(v interface{}) int { 1030 var buf bytes.Buffer 1031 m := v.(map[string]interface{}) 1032 if name, ok := m["device_name"]; ok { 1033 buf.WriteString(fmt.Sprintf("%s-", name.(string))) 1034 } 1035 if id, ok := m["snapshot_id"]; ok { 1036 buf.WriteString(fmt.Sprintf("%s-", id.(string))) 1037 } 1038 return hashcode.String(buf.String()) 1039 }