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