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