github.com/recobe182/terraform@v0.8.5-0.20170117231232-49ab22a935b7/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 base64Encode([]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 DeleteOnTermination: aws.Bool(true), 379 DeviceIndex: aws.Int64(int64(0)), 380 SubnetId: aws.String(subnetId.(string)), 381 Groups: groupIds, 382 } 383 384 opts.NetworkInterfaces = []*ec2.InstanceNetworkInterfaceSpecification{ni} 385 opts.SubnetId = aws.String("") 386 } 387 388 blockDevices, err := readSpotFleetBlockDeviceMappingsFromConfig(d, conn) 389 if err != nil { 390 return nil, err 391 } 392 if len(blockDevices) > 0 { 393 opts.BlockDeviceMappings = blockDevices 394 } 395 396 return opts, nil 397 } 398 399 func validateSpotFleetRequestKeyName(v interface{}, k string) (ws []string, errors []error) { 400 value := v.(string) 401 402 if value == "" { 403 errors = append(errors, fmt.Errorf("Key name cannot be empty.")) 404 } 405 406 return 407 } 408 409 func readSpotFleetBlockDeviceMappingsFromConfig( 410 d map[string]interface{}, conn *ec2.EC2) ([]*ec2.BlockDeviceMapping, error) { 411 blockDevices := make([]*ec2.BlockDeviceMapping, 0) 412 413 if v, ok := d["ebs_block_device"]; ok { 414 vL := v.(*schema.Set).List() 415 for _, v := range vL { 416 bd := v.(map[string]interface{}) 417 ebs := &ec2.EbsBlockDevice{ 418 DeleteOnTermination: aws.Bool(bd["delete_on_termination"].(bool)), 419 } 420 421 if v, ok := bd["snapshot_id"].(string); ok && v != "" { 422 ebs.SnapshotId = aws.String(v) 423 } 424 425 if v, ok := bd["encrypted"].(bool); ok && v { 426 ebs.Encrypted = aws.Bool(v) 427 } 428 429 if v, ok := bd["volume_size"].(int); ok && v != 0 { 430 ebs.VolumeSize = aws.Int64(int64(v)) 431 } 432 433 if v, ok := bd["volume_type"].(string); ok && v != "" { 434 ebs.VolumeType = aws.String(v) 435 } 436 437 if v, ok := bd["iops"].(int); ok && v > 0 { 438 ebs.Iops = aws.Int64(int64(v)) 439 } 440 441 blockDevices = append(blockDevices, &ec2.BlockDeviceMapping{ 442 DeviceName: aws.String(bd["device_name"].(string)), 443 Ebs: ebs, 444 }) 445 } 446 } 447 448 if v, ok := d["ephemeral_block_device"]; ok { 449 vL := v.(*schema.Set).List() 450 for _, v := range vL { 451 bd := v.(map[string]interface{}) 452 blockDevices = append(blockDevices, &ec2.BlockDeviceMapping{ 453 DeviceName: aws.String(bd["device_name"].(string)), 454 VirtualName: aws.String(bd["virtual_name"].(string)), 455 }) 456 } 457 } 458 459 if v, ok := d["root_block_device"]; ok { 460 vL := v.(*schema.Set).List() 461 if len(vL) > 1 { 462 return nil, fmt.Errorf("Cannot specify more than one root_block_device.") 463 } 464 for _, v := range vL { 465 bd := v.(map[string]interface{}) 466 ebs := &ec2.EbsBlockDevice{ 467 DeleteOnTermination: aws.Bool(bd["delete_on_termination"].(bool)), 468 } 469 470 if v, ok := bd["volume_size"].(int); ok && v != 0 { 471 ebs.VolumeSize = aws.Int64(int64(v)) 472 } 473 474 if v, ok := bd["volume_type"].(string); ok && v != "" { 475 ebs.VolumeType = aws.String(v) 476 } 477 478 if v, ok := bd["iops"].(int); ok && v > 0 { 479 ebs.Iops = aws.Int64(int64(v)) 480 } 481 482 if dn, err := fetchRootDeviceName(d["ami"].(string), conn); err == nil { 483 if dn == nil { 484 return nil, fmt.Errorf( 485 "Expected 1 AMI for ID: %s, got none", 486 d["ami"].(string)) 487 } 488 489 blockDevices = append(blockDevices, &ec2.BlockDeviceMapping{ 490 DeviceName: dn, 491 Ebs: ebs, 492 }) 493 } else { 494 return nil, err 495 } 496 } 497 } 498 499 return blockDevices, nil 500 } 501 502 func buildAwsSpotFleetLaunchSpecifications( 503 d *schema.ResourceData, meta interface{}) ([]*ec2.SpotFleetLaunchSpecification, error) { 504 505 user_specs := d.Get("launch_specification").(*schema.Set).List() 506 specs := make([]*ec2.SpotFleetLaunchSpecification, len(user_specs)) 507 for i, user_spec := range user_specs { 508 user_spec_map := user_spec.(map[string]interface{}) 509 // panic: interface conversion: interface {} is map[string]interface {}, not *schema.ResourceData 510 opts, err := buildSpotFleetLaunchSpecification(user_spec_map, meta) 511 if err != nil { 512 return nil, err 513 } 514 specs[i] = opts 515 } 516 517 return specs, nil 518 } 519 520 func resourceAwsSpotFleetRequestCreate(d *schema.ResourceData, meta interface{}) error { 521 // http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_RequestSpotFleet.html 522 conn := meta.(*AWSClient).ec2conn 523 524 launch_specs, err := buildAwsSpotFleetLaunchSpecifications(d, meta) 525 if err != nil { 526 return err 527 } 528 529 // http://docs.aws.amazon.com/sdk-for-go/api/service/ec2.html#type-SpotFleetRequestConfigData 530 spotFleetConfig := &ec2.SpotFleetRequestConfigData{ 531 IamFleetRole: aws.String(d.Get("iam_fleet_role").(string)), 532 LaunchSpecifications: launch_specs, 533 SpotPrice: aws.String(d.Get("spot_price").(string)), 534 TargetCapacity: aws.Int64(int64(d.Get("target_capacity").(int))), 535 ClientToken: aws.String(resource.UniqueId()), 536 TerminateInstancesWithExpiration: aws.Bool(d.Get("terminate_instances_with_expiration").(bool)), 537 } 538 539 if v, ok := d.GetOk("excess_capacity_termination_policy"); ok { 540 spotFleetConfig.ExcessCapacityTerminationPolicy = aws.String(v.(string)) 541 } 542 543 if v, ok := d.GetOk("allocation_strategy"); ok { 544 spotFleetConfig.AllocationStrategy = aws.String(v.(string)) 545 } else { 546 spotFleetConfig.AllocationStrategy = aws.String("lowestPrice") 547 } 548 549 if v, ok := d.GetOk("valid_from"); ok { 550 valid_from, err := time.Parse(awsAutoscalingScheduleTimeLayout, v.(string)) 551 if err != nil { 552 return err 553 } 554 spotFleetConfig.ValidFrom = &valid_from 555 } 556 557 if v, ok := d.GetOk("valid_until"); ok { 558 valid_until, err := time.Parse(awsAutoscalingScheduleTimeLayout, v.(string)) 559 if err != nil { 560 return err 561 } 562 spotFleetConfig.ValidUntil = &valid_until 563 } else { 564 valid_until := time.Now().Add(24 * time.Hour) 565 spotFleetConfig.ValidUntil = &valid_until 566 } 567 568 // http://docs.aws.amazon.com/sdk-for-go/api/service/ec2.html#type-RequestSpotFleetInput 569 spotFleetOpts := &ec2.RequestSpotFleetInput{ 570 SpotFleetRequestConfig: spotFleetConfig, 571 DryRun: aws.Bool(false), 572 } 573 574 log.Printf("[DEBUG] Requesting spot fleet with these opts: %+v", spotFleetOpts) 575 576 // Since IAM is eventually consistent, we retry creation as a newly created role may not 577 // take effect immediately, resulting in an InvalidSpotFleetRequestConfig error 578 var resp *ec2.RequestSpotFleetOutput 579 err = resource.Retry(1*time.Minute, func() *resource.RetryError { 580 var err error 581 resp, err = conn.RequestSpotFleet(spotFleetOpts) 582 583 if err != nil { 584 if awsErr, ok := err.(awserr.Error); ok { 585 // IAM is eventually consistent :/ 586 if awsErr.Code() == "InvalidSpotFleetRequestConfig" { 587 return resource.RetryableError( 588 fmt.Errorf("[WARN] Error creating Spot fleet request, retrying: %s", err)) 589 } 590 } 591 return resource.NonRetryableError(err) 592 } 593 return nil 594 }) 595 596 if err != nil { 597 return fmt.Errorf("Error requesting spot fleet: %s", err) 598 } 599 600 d.SetId(*resp.SpotFleetRequestId) 601 602 log.Printf("[INFO] Spot Fleet Request ID: %s", d.Id()) 603 log.Println("[INFO] Waiting for Spot Fleet Request to be active") 604 stateConf := &resource.StateChangeConf{ 605 Pending: []string{"submitted"}, 606 Target: []string{"active"}, 607 Refresh: resourceAwsSpotFleetRequestStateRefreshFunc(d, meta), 608 Timeout: 10 * time.Minute, 609 MinTimeout: 10 * time.Second, 610 Delay: 30 * time.Second, 611 } 612 613 _, err = stateConf.WaitForState() 614 if err != nil { 615 return err 616 } 617 618 return resourceAwsSpotFleetRequestRead(d, meta) 619 } 620 621 func resourceAwsSpotFleetRequestStateRefreshFunc(d *schema.ResourceData, meta interface{}) resource.StateRefreshFunc { 622 return func() (interface{}, string, error) { 623 conn := meta.(*AWSClient).ec2conn 624 req := &ec2.DescribeSpotFleetRequestsInput{ 625 SpotFleetRequestIds: []*string{aws.String(d.Id())}, 626 } 627 resp, err := conn.DescribeSpotFleetRequests(req) 628 629 if err != nil { 630 log.Printf("Error on retrieving Spot Fleet Request when waiting: %s", err) 631 return nil, "", nil 632 } 633 634 if resp == nil { 635 return nil, "", nil 636 } 637 638 if len(resp.SpotFleetRequestConfigs) == 0 { 639 return nil, "", nil 640 } 641 642 spotFleetRequest := resp.SpotFleetRequestConfigs[0] 643 644 return spotFleetRequest, *spotFleetRequest.SpotFleetRequestState, nil 645 } 646 } 647 648 func resourceAwsSpotFleetRequestRead(d *schema.ResourceData, meta interface{}) error { 649 // http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeSpotFleetRequests.html 650 conn := meta.(*AWSClient).ec2conn 651 652 req := &ec2.DescribeSpotFleetRequestsInput{ 653 SpotFleetRequestIds: []*string{aws.String(d.Id())}, 654 } 655 resp, err := conn.DescribeSpotFleetRequests(req) 656 657 if err != nil { 658 // If the spot request was not found, return nil so that we can show 659 // that it is gone. 660 ec2err, ok := err.(awserr.Error) 661 if ok && ec2err.Code() == "InvalidSpotFleetRequestId.NotFound" { 662 d.SetId("") 663 return nil 664 } 665 666 // Some other error, report it 667 return err 668 } 669 670 sfr := resp.SpotFleetRequestConfigs[0] 671 672 // if the request is cancelled, then it is gone 673 cancelledStates := map[string]bool{ 674 "cancelled": true, 675 "cancelled_running": true, 676 "cancelled_terminating": true, 677 } 678 if _, ok := cancelledStates[*sfr.SpotFleetRequestState]; ok { 679 d.SetId("") 680 return nil 681 } 682 683 d.SetId(*sfr.SpotFleetRequestId) 684 d.Set("spot_request_state", aws.StringValue(sfr.SpotFleetRequestState)) 685 686 config := sfr.SpotFleetRequestConfig 687 688 if config.AllocationStrategy != nil { 689 d.Set("allocation_strategy", aws.StringValue(config.AllocationStrategy)) 690 } 691 692 if config.ClientToken != nil { 693 d.Set("client_token", aws.StringValue(config.ClientToken)) 694 } 695 696 if config.ExcessCapacityTerminationPolicy != nil { 697 d.Set("excess_capacity_termination_policy", 698 aws.StringValue(config.ExcessCapacityTerminationPolicy)) 699 } 700 701 if config.IamFleetRole != nil { 702 d.Set("iam_fleet_role", aws.StringValue(config.IamFleetRole)) 703 } 704 705 if config.SpotPrice != nil { 706 d.Set("spot_price", aws.StringValue(config.SpotPrice)) 707 } 708 709 if config.TargetCapacity != nil { 710 d.Set("target_capacity", aws.Int64Value(config.TargetCapacity)) 711 } 712 713 if config.TerminateInstancesWithExpiration != nil { 714 d.Set("terminate_instances_with_expiration", 715 aws.BoolValue(config.TerminateInstancesWithExpiration)) 716 } 717 718 if config.ValidFrom != nil { 719 d.Set("valid_from", 720 aws.TimeValue(config.ValidFrom).Format(awsAutoscalingScheduleTimeLayout)) 721 } 722 723 if config.ValidUntil != nil { 724 d.Set("valid_until", 725 aws.TimeValue(config.ValidUntil).Format(awsAutoscalingScheduleTimeLayout)) 726 } 727 728 d.Set("launch_specification", launchSpecsToSet(config.LaunchSpecifications, conn)) 729 730 return nil 731 } 732 733 func launchSpecsToSet(ls []*ec2.SpotFleetLaunchSpecification, conn *ec2.EC2) *schema.Set { 734 specs := &schema.Set{F: hashLaunchSpecification} 735 for _, val := range ls { 736 dn, err := fetchRootDeviceName(aws.StringValue(val.ImageId), conn) 737 if err != nil { 738 log.Panic(err) 739 } else { 740 ls := launchSpecToMap(val, dn) 741 specs.Add(ls) 742 } 743 } 744 return specs 745 } 746 747 func launchSpecToMap( 748 l *ec2.SpotFleetLaunchSpecification, 749 rootDevName *string, 750 ) map[string]interface{} { 751 m := make(map[string]interface{}) 752 753 m["root_block_device"] = rootBlockDeviceToSet(l.BlockDeviceMappings, rootDevName) 754 m["ebs_block_device"] = ebsBlockDevicesToSet(l.BlockDeviceMappings, rootDevName) 755 m["ephemeral_block_device"] = ephemeralBlockDevicesToSet(l.BlockDeviceMappings) 756 757 if l.ImageId != nil { 758 m["ami"] = aws.StringValue(l.ImageId) 759 } 760 761 if l.InstanceType != nil { 762 m["instance_type"] = aws.StringValue(l.InstanceType) 763 } 764 765 if l.SpotPrice != nil { 766 m["spot_price"] = aws.StringValue(l.SpotPrice) 767 } 768 769 if l.EbsOptimized != nil { 770 m["ebs_optimized"] = aws.BoolValue(l.EbsOptimized) 771 } 772 773 if l.Monitoring != nil && l.Monitoring.Enabled != nil { 774 m["monitoring"] = aws.BoolValue(l.Monitoring.Enabled) 775 } 776 777 if l.IamInstanceProfile != nil && l.IamInstanceProfile.Name != nil { 778 m["iam_instance_profile"] = aws.StringValue(l.IamInstanceProfile.Name) 779 } 780 781 if l.UserData != nil { 782 ud_dec, err := base64.StdEncoding.DecodeString(aws.StringValue(l.UserData)) 783 if err == nil { 784 m["user_data"] = string(ud_dec) 785 } 786 } 787 788 if l.KeyName != nil { 789 m["key_name"] = aws.StringValue(l.KeyName) 790 } 791 792 if l.Placement != nil { 793 m["availability_zone"] = aws.StringValue(l.Placement.AvailabilityZone) 794 } 795 796 if l.SubnetId != nil { 797 m["subnet_id"] = aws.StringValue(l.SubnetId) 798 } 799 800 if l.WeightedCapacity != nil { 801 m["weighted_capacity"] = strconv.FormatFloat(*l.WeightedCapacity, 'f', 0, 64) 802 } 803 804 // m["security_groups"] = securityGroupsToSet(l.SecutiryGroups) 805 return m 806 } 807 808 func ebsBlockDevicesToSet(bdm []*ec2.BlockDeviceMapping, rootDevName *string) *schema.Set { 809 set := &schema.Set{F: hashEbsBlockDevice} 810 811 for _, val := range bdm { 812 if val.Ebs != nil { 813 m := make(map[string]interface{}) 814 815 ebs := val.Ebs 816 817 if val.DeviceName != nil { 818 if aws.StringValue(rootDevName) == aws.StringValue(val.DeviceName) { 819 continue 820 } 821 822 m["device_name"] = aws.StringValue(val.DeviceName) 823 } 824 825 if ebs.DeleteOnTermination != nil { 826 m["delete_on_termination"] = aws.BoolValue(ebs.DeleteOnTermination) 827 } 828 829 if ebs.SnapshotId != nil { 830 m["snapshot_id"] = aws.StringValue(ebs.SnapshotId) 831 } 832 833 if ebs.Encrypted != nil { 834 m["encrypted"] = aws.BoolValue(ebs.Encrypted) 835 } 836 837 if ebs.VolumeSize != nil { 838 m["volume_size"] = aws.Int64Value(ebs.VolumeSize) 839 } 840 841 if ebs.VolumeType != nil { 842 m["volume_type"] = aws.StringValue(ebs.VolumeType) 843 } 844 845 if ebs.Iops != nil { 846 m["iops"] = aws.Int64Value(ebs.Iops) 847 } 848 849 set.Add(m) 850 } 851 } 852 853 return set 854 } 855 856 func ephemeralBlockDevicesToSet(bdm []*ec2.BlockDeviceMapping) *schema.Set { 857 set := &schema.Set{F: hashEphemeralBlockDevice} 858 859 for _, val := range bdm { 860 if val.VirtualName != nil { 861 m := make(map[string]interface{}) 862 m["virtual_name"] = aws.StringValue(val.VirtualName) 863 864 if val.DeviceName != nil { 865 m["device_name"] = aws.StringValue(val.DeviceName) 866 } 867 868 set.Add(m) 869 } 870 } 871 872 return set 873 } 874 875 func rootBlockDeviceToSet( 876 bdm []*ec2.BlockDeviceMapping, 877 rootDevName *string, 878 ) *schema.Set { 879 set := &schema.Set{F: hashRootBlockDevice} 880 881 if rootDevName != nil { 882 for _, val := range bdm { 883 if aws.StringValue(val.DeviceName) == aws.StringValue(rootDevName) { 884 m := make(map[string]interface{}) 885 if val.Ebs.DeleteOnTermination != nil { 886 m["delete_on_termination"] = aws.BoolValue(val.Ebs.DeleteOnTermination) 887 } 888 889 if val.Ebs.VolumeSize != nil { 890 m["volume_size"] = aws.Int64Value(val.Ebs.VolumeSize) 891 } 892 893 if val.Ebs.VolumeType != nil { 894 m["volume_type"] = aws.StringValue(val.Ebs.VolumeType) 895 } 896 897 if val.Ebs.Iops != nil { 898 m["iops"] = aws.Int64Value(val.Ebs.Iops) 899 } 900 901 set.Add(m) 902 } 903 } 904 } 905 906 return set 907 } 908 909 func resourceAwsSpotFleetRequestUpdate(d *schema.ResourceData, meta interface{}) error { 910 // http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_ModifySpotFleetRequest.html 911 conn := meta.(*AWSClient).ec2conn 912 913 d.Partial(true) 914 915 req := &ec2.ModifySpotFleetRequestInput{ 916 SpotFleetRequestId: aws.String(d.Id()), 917 } 918 919 if val, ok := d.GetOk("target_capacity"); ok { 920 req.TargetCapacity = aws.Int64(int64(val.(int))) 921 } 922 923 if val, ok := d.GetOk("excess_capacity_termination_policy"); ok { 924 req.ExcessCapacityTerminationPolicy = aws.String(val.(string)) 925 } 926 927 resp, err := conn.ModifySpotFleetRequest(req) 928 if err == nil && aws.BoolValue(resp.Return) { 929 // TODO: rollback to old values? 930 } 931 932 return nil 933 } 934 935 func resourceAwsSpotFleetRequestDelete(d *schema.ResourceData, meta interface{}) error { 936 // http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CancelSpotFleetRequests.html 937 conn := meta.(*AWSClient).ec2conn 938 939 log.Printf("[INFO] Cancelling spot fleet request: %s", d.Id()) 940 resp, err := conn.CancelSpotFleetRequests(&ec2.CancelSpotFleetRequestsInput{ 941 SpotFleetRequestIds: []*string{aws.String(d.Id())}, 942 TerminateInstances: aws.Bool(d.Get("terminate_instances_with_expiration").(bool)), 943 }) 944 945 if err != nil { 946 return fmt.Errorf("Error cancelling spot request (%s): %s", d.Id(), err) 947 } 948 949 // check response successfulFleetRequestSet to make sure our request was canceled 950 var found bool 951 for _, s := range resp.SuccessfulFleetRequests { 952 if *s.SpotFleetRequestId == d.Id() { 953 found = true 954 } 955 } 956 957 if !found { 958 return fmt.Errorf("[ERR] Spot Fleet request (%s) was not found to be successfully canceled, dangling resources may exit", d.Id()) 959 } 960 961 return resource.Retry(5*time.Minute, func() *resource.RetryError { 962 resp, err := conn.DescribeSpotFleetInstances(&ec2.DescribeSpotFleetInstancesInput{ 963 SpotFleetRequestId: aws.String(d.Id()), 964 }) 965 if err != nil { 966 return resource.NonRetryableError(err) 967 } 968 969 if len(resp.ActiveInstances) == 0 { 970 log.Printf("[DEBUG] Active instance count is 0 for Spot Fleet Request (%s), removing", d.Id()) 971 return nil 972 } 973 974 log.Printf("[DEBUG] Active instance count in Spot Fleet Request (%s): %d", d.Id(), len(resp.ActiveInstances)) 975 976 return resource.RetryableError( 977 fmt.Errorf("fleet still has (%d) running instances", len(resp.ActiveInstances))) 978 }) 979 } 980 981 func hashEphemeralBlockDevice(v interface{}) int { 982 var buf bytes.Buffer 983 m := v.(map[string]interface{}) 984 buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) 985 buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string))) 986 return hashcode.String(buf.String()) 987 } 988 989 func hashRootBlockDevice(v interface{}) int { 990 // there can be only one root device; no need to hash anything 991 return 0 992 } 993 994 func hashLaunchSpecification(v interface{}) int { 995 var buf bytes.Buffer 996 m := v.(map[string]interface{}) 997 buf.WriteString(fmt.Sprintf("%s-", m["ami"].(string))) 998 if m["availability_zone"] != "" { 999 buf.WriteString(fmt.Sprintf("%s-", m["availability_zone"].(string))) 1000 } 1001 if m["subnet_id"] != "" { 1002 buf.WriteString(fmt.Sprintf("%s-", m["subnet_id"].(string))) 1003 } 1004 buf.WriteString(fmt.Sprintf("%s-", m["instance_type"].(string))) 1005 buf.WriteString(fmt.Sprintf("%s-", m["spot_price"].(string))) 1006 buf.WriteString(fmt.Sprintf("%s-", m["user_data"].(string))) 1007 return hashcode.String(buf.String()) 1008 } 1009 1010 func hashEbsBlockDevice(v interface{}) int { 1011 var buf bytes.Buffer 1012 m := v.(map[string]interface{}) 1013 if name, ok := m["device_name"]; ok { 1014 buf.WriteString(fmt.Sprintf("%s-", name.(string))) 1015 } 1016 if id, ok := m["snapshot_id"]; ok { 1017 buf.WriteString(fmt.Sprintf("%s-", id.(string))) 1018 } 1019 return hashcode.String(buf.String()) 1020 }