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