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