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