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  }