github.com/danp/terraform@v0.9.5-0.20170426144147-39d740081351/builtin/providers/aws/resource_aws_spot_fleet_request.go (about)

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