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