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