github.com/xyziemba/terraform@v0.7.1-0.20160816223025-3ea544774db1/builtin/providers/aws/resource_aws_spot_fleet_request.go (about)

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