github.com/IBM-Cloud/terraform@v0.6.4-0.20170726051544-8872b87621df/builtin/providers/aws/resource_aws_spot_instance_request.go (about)

     1  package aws
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"time"
     7  
     8  	"github.com/aws/aws-sdk-go/aws"
     9  	"github.com/aws/aws-sdk-go/aws/awserr"
    10  	"github.com/aws/aws-sdk-go/service/ec2"
    11  	"github.com/hashicorp/terraform/helper/resource"
    12  	"github.com/hashicorp/terraform/helper/schema"
    13  )
    14  
    15  func resourceAwsSpotInstanceRequest() *schema.Resource {
    16  	return &schema.Resource{
    17  		Create: resourceAwsSpotInstanceRequestCreate,
    18  		Read:   resourceAwsSpotInstanceRequestRead,
    19  		Delete: resourceAwsSpotInstanceRequestDelete,
    20  		Update: resourceAwsSpotInstanceRequestUpdate,
    21  
    22  		Schema: func() map[string]*schema.Schema {
    23  			// The Spot Instance Request Schema is based on the AWS Instance schema.
    24  			s := resourceAwsInstance().Schema
    25  
    26  			// Everything on a spot instance is ForceNew except tags
    27  			for k, v := range s {
    28  				if k == "tags" {
    29  					continue
    30  				}
    31  				v.ForceNew = true
    32  			}
    33  
    34  			s["volume_tags"] = &schema.Schema{
    35  				Type:     schema.TypeMap,
    36  				Optional: true,
    37  			}
    38  
    39  			s["spot_price"] = &schema.Schema{
    40  				Type:     schema.TypeString,
    41  				Required: true,
    42  				ForceNew: true,
    43  			}
    44  			s["spot_type"] = &schema.Schema{
    45  				Type:     schema.TypeString,
    46  				Optional: true,
    47  				Default:  "persistent",
    48  			}
    49  			s["wait_for_fulfillment"] = &schema.Schema{
    50  				Type:     schema.TypeBool,
    51  				Optional: true,
    52  				Default:  false,
    53  			}
    54  			s["spot_bid_status"] = &schema.Schema{
    55  				Type:     schema.TypeString,
    56  				Computed: true,
    57  			}
    58  			s["spot_request_state"] = &schema.Schema{
    59  				Type:     schema.TypeString,
    60  				Computed: true,
    61  			}
    62  			s["spot_instance_id"] = &schema.Schema{
    63  				Type:     schema.TypeString,
    64  				Computed: true,
    65  			}
    66  			s["block_duration_minutes"] = &schema.Schema{
    67  				Type:     schema.TypeInt,
    68  				Optional: true,
    69  				ForceNew: true,
    70  			}
    71  
    72  			return s
    73  		}(),
    74  	}
    75  }
    76  
    77  func resourceAwsSpotInstanceRequestCreate(d *schema.ResourceData, meta interface{}) error {
    78  	conn := meta.(*AWSClient).ec2conn
    79  
    80  	instanceOpts, err := buildAwsInstanceOpts(d, meta)
    81  	if err != nil {
    82  		return err
    83  	}
    84  
    85  	spotOpts := &ec2.RequestSpotInstancesInput{
    86  		SpotPrice: aws.String(d.Get("spot_price").(string)),
    87  		Type:      aws.String(d.Get("spot_type").(string)),
    88  
    89  		// Though the AWS API supports creating spot instance requests for multiple
    90  		// instances, for TF purposes we fix this to one instance per request.
    91  		// Users can get equivalent behavior out of TF's "count" meta-parameter.
    92  		InstanceCount: aws.Int64(1),
    93  
    94  		LaunchSpecification: &ec2.RequestSpotLaunchSpecification{
    95  			BlockDeviceMappings: instanceOpts.BlockDeviceMappings,
    96  			EbsOptimized:        instanceOpts.EBSOptimized,
    97  			Monitoring:          instanceOpts.Monitoring,
    98  			IamInstanceProfile:  instanceOpts.IAMInstanceProfile,
    99  			ImageId:             instanceOpts.ImageID,
   100  			InstanceType:        instanceOpts.InstanceType,
   101  			KeyName:             instanceOpts.KeyName,
   102  			Placement:           instanceOpts.SpotPlacement,
   103  			SecurityGroupIds:    instanceOpts.SecurityGroupIDs,
   104  			SecurityGroups:      instanceOpts.SecurityGroups,
   105  			SubnetId:            instanceOpts.SubnetID,
   106  			UserData:            instanceOpts.UserData64,
   107  		},
   108  	}
   109  
   110  	if v, ok := d.GetOk("block_duration_minutes"); ok {
   111  		spotOpts.BlockDurationMinutes = aws.Int64(int64(v.(int)))
   112  	}
   113  
   114  	// If the instance is configured with a Network Interface (a subnet, has
   115  	// public IP, etc), then the instanceOpts.SecurityGroupIds and SubnetId will
   116  	// be nil
   117  	if len(instanceOpts.NetworkInterfaces) > 0 {
   118  		spotOpts.LaunchSpecification.SecurityGroupIds = instanceOpts.NetworkInterfaces[0].Groups
   119  		spotOpts.LaunchSpecification.SubnetId = instanceOpts.NetworkInterfaces[0].SubnetId
   120  	}
   121  
   122  	// Make the spot instance request
   123  	log.Printf("[DEBUG] Requesting spot bid opts: %s", spotOpts)
   124  
   125  	var resp *ec2.RequestSpotInstancesOutput
   126  	err = resource.Retry(15*time.Second, func() *resource.RetryError {
   127  		var err error
   128  		resp, err = conn.RequestSpotInstances(spotOpts)
   129  		// IAM instance profiles can take ~10 seconds to propagate in AWS:
   130  		// http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#launch-instance-with-role-console
   131  		if isAWSErr(err, "InvalidParameterValue", "Invalid IAM Instance Profile") {
   132  			log.Printf("[DEBUG] Invalid IAM Instance Profile referenced, retrying...")
   133  			return resource.RetryableError(err)
   134  		}
   135  		// IAM roles can also take time to propagate in AWS:
   136  		if isAWSErr(err, "InvalidParameterValue", " has no associated IAM Roles") {
   137  			log.Printf("[DEBUG] IAM Instance Profile appears to have no IAM roles, retrying...")
   138  			return resource.RetryableError(err)
   139  		}
   140  		return resource.NonRetryableError(err)
   141  	})
   142  
   143  	if err != nil {
   144  		return fmt.Errorf("Error requesting spot instances: %s", err)
   145  	}
   146  	if len(resp.SpotInstanceRequests) != 1 {
   147  		return fmt.Errorf(
   148  			"Expected response with length 1, got: %s", resp)
   149  	}
   150  
   151  	sir := *resp.SpotInstanceRequests[0]
   152  	d.SetId(*sir.SpotInstanceRequestId)
   153  
   154  	if d.Get("wait_for_fulfillment").(bool) {
   155  		spotStateConf := &resource.StateChangeConf{
   156  			// http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-bid-status.html
   157  			Pending:    []string{"start", "pending-evaluation", "pending-fulfillment"},
   158  			Target:     []string{"fulfilled"},
   159  			Refresh:    SpotInstanceStateRefreshFunc(conn, sir),
   160  			Timeout:    10 * time.Minute,
   161  			Delay:      10 * time.Second,
   162  			MinTimeout: 3 * time.Second,
   163  		}
   164  
   165  		log.Printf("[DEBUG] waiting for spot bid to resolve... this may take several minutes.")
   166  		_, err = spotStateConf.WaitForState()
   167  
   168  		if err != nil {
   169  			return fmt.Errorf("Error while waiting for spot request (%s) to resolve: %s", sir, err)
   170  		}
   171  	}
   172  
   173  	return resourceAwsSpotInstanceRequestUpdate(d, meta)
   174  }
   175  
   176  // Update spot state, etc
   177  func resourceAwsSpotInstanceRequestRead(d *schema.ResourceData, meta interface{}) error {
   178  	conn := meta.(*AWSClient).ec2conn
   179  
   180  	req := &ec2.DescribeSpotInstanceRequestsInput{
   181  		SpotInstanceRequestIds: []*string{aws.String(d.Id())},
   182  	}
   183  	resp, err := conn.DescribeSpotInstanceRequests(req)
   184  
   185  	if err != nil {
   186  		// If the spot request was not found, return nil so that we can show
   187  		// that it is gone.
   188  		if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidSpotInstanceRequestID.NotFound" {
   189  			d.SetId("")
   190  			return nil
   191  		}
   192  
   193  		// Some other error, report it
   194  		return err
   195  	}
   196  
   197  	// If nothing was found, then return no state
   198  	if len(resp.SpotInstanceRequests) == 0 {
   199  		d.SetId("")
   200  		return nil
   201  	}
   202  
   203  	request := resp.SpotInstanceRequests[0]
   204  
   205  	// if the request is cancelled, then it is gone
   206  	if *request.State == "cancelled" {
   207  		d.SetId("")
   208  		return nil
   209  	}
   210  
   211  	d.Set("spot_bid_status", *request.Status.Code)
   212  	// Instance ID is not set if the request is still pending
   213  	if request.InstanceId != nil {
   214  		d.Set("spot_instance_id", *request.InstanceId)
   215  		// Read the instance data, setting up connection information
   216  		if err := readInstance(d, meta); err != nil {
   217  			return fmt.Errorf("[ERR] Error reading Spot Instance Data: %s", err)
   218  		}
   219  	}
   220  
   221  	d.Set("spot_request_state", request.State)
   222  	d.Set("block_duration_minutes", request.BlockDurationMinutes)
   223  	d.Set("tags", tagsToMap(request.Tags))
   224  
   225  	return nil
   226  }
   227  
   228  func readInstance(d *schema.ResourceData, meta interface{}) error {
   229  	conn := meta.(*AWSClient).ec2conn
   230  
   231  	resp, err := conn.DescribeInstances(&ec2.DescribeInstancesInput{
   232  		InstanceIds: []*string{aws.String(d.Get("spot_instance_id").(string))},
   233  	})
   234  	if err != nil {
   235  		// If the instance was not found, return nil so that we can show
   236  		// that the instance is gone.
   237  		if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidInstanceID.NotFound" {
   238  			return fmt.Errorf("no instance found")
   239  		}
   240  
   241  		// Some other error, report it
   242  		return err
   243  	}
   244  
   245  	// If nothing was found, then return no state
   246  	if len(resp.Reservations) == 0 {
   247  		return fmt.Errorf("no instances found")
   248  	}
   249  
   250  	instance := resp.Reservations[0].Instances[0]
   251  
   252  	// Set these fields for connection information
   253  	if instance != nil {
   254  		d.Set("public_dns", instance.PublicDnsName)
   255  		d.Set("public_ip", instance.PublicIpAddress)
   256  		d.Set("private_dns", instance.PrivateDnsName)
   257  		d.Set("private_ip", instance.PrivateIpAddress)
   258  
   259  		// set connection information
   260  		if instance.PublicIpAddress != nil {
   261  			d.SetConnInfo(map[string]string{
   262  				"type": "ssh",
   263  				"host": *instance.PublicIpAddress,
   264  			})
   265  		} else if instance.PrivateIpAddress != nil {
   266  			d.SetConnInfo(map[string]string{
   267  				"type": "ssh",
   268  				"host": *instance.PrivateIpAddress,
   269  			})
   270  		}
   271  		if err := readBlockDevices(d, instance, conn); err != nil {
   272  			return err
   273  		}
   274  
   275  		var ipv6Addresses []string
   276  		if len(instance.NetworkInterfaces) > 0 {
   277  			for _, ni := range instance.NetworkInterfaces {
   278  				if *ni.Attachment.DeviceIndex == 0 {
   279  					d.Set("subnet_id", ni.SubnetId)
   280  					d.Set("network_interface_id", ni.NetworkInterfaceId)
   281  					d.Set("associate_public_ip_address", ni.Association != nil)
   282  					d.Set("ipv6_address_count", len(ni.Ipv6Addresses))
   283  
   284  					for _, address := range ni.Ipv6Addresses {
   285  						ipv6Addresses = append(ipv6Addresses, *address.Ipv6Address)
   286  					}
   287  				}
   288  			}
   289  		} else {
   290  			d.Set("subnet_id", instance.SubnetId)
   291  			d.Set("network_interface_id", "")
   292  		}
   293  
   294  		if err := d.Set("ipv6_addresses", ipv6Addresses); err != nil {
   295  			log.Printf("[WARN] Error setting ipv6_addresses for AWS Spot Instance (%s): %s", d.Id(), err)
   296  		}
   297  	}
   298  
   299  	return nil
   300  }
   301  
   302  func resourceAwsSpotInstanceRequestUpdate(d *schema.ResourceData, meta interface{}) error {
   303  	conn := meta.(*AWSClient).ec2conn
   304  
   305  	d.Partial(true)
   306  	if err := setTags(conn, d); err != nil {
   307  		return err
   308  	} else {
   309  		d.SetPartial("tags")
   310  	}
   311  
   312  	d.Partial(false)
   313  
   314  	return resourceAwsSpotInstanceRequestRead(d, meta)
   315  }
   316  
   317  func resourceAwsSpotInstanceRequestDelete(d *schema.ResourceData, meta interface{}) error {
   318  	conn := meta.(*AWSClient).ec2conn
   319  
   320  	log.Printf("[INFO] Cancelling spot request: %s", d.Id())
   321  	_, err := conn.CancelSpotInstanceRequests(&ec2.CancelSpotInstanceRequestsInput{
   322  		SpotInstanceRequestIds: []*string{aws.String(d.Id())},
   323  	})
   324  
   325  	if err != nil {
   326  		return fmt.Errorf("Error cancelling spot request (%s): %s", d.Id(), err)
   327  	}
   328  
   329  	if instanceId := d.Get("spot_instance_id").(string); instanceId != "" {
   330  		log.Printf("[INFO] Terminating instance: %s", instanceId)
   331  		if err := awsTerminateInstance(conn, instanceId); err != nil {
   332  			return fmt.Errorf("Error terminating spot instance: %s", err)
   333  		}
   334  	}
   335  
   336  	return nil
   337  }
   338  
   339  // SpotInstanceStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
   340  // an EC2 spot instance request
   341  func SpotInstanceStateRefreshFunc(
   342  	conn *ec2.EC2, sir ec2.SpotInstanceRequest) resource.StateRefreshFunc {
   343  
   344  	return func() (interface{}, string, error) {
   345  		resp, err := conn.DescribeSpotInstanceRequests(&ec2.DescribeSpotInstanceRequestsInput{
   346  			SpotInstanceRequestIds: []*string{sir.SpotInstanceRequestId},
   347  		})
   348  
   349  		if err != nil {
   350  			if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidSpotInstanceRequestID.NotFound" {
   351  				// Set this to nil as if we didn't find anything.
   352  				resp = nil
   353  			} else {
   354  				log.Printf("Error on StateRefresh: %s", err)
   355  				return nil, "", err
   356  			}
   357  		}
   358  
   359  		if resp == nil || len(resp.SpotInstanceRequests) == 0 {
   360  			// Sometimes AWS just has consistency issues and doesn't see
   361  			// our request yet. Return an empty state.
   362  			return nil, "", nil
   363  		}
   364  
   365  		req := resp.SpotInstanceRequests[0]
   366  		return req, *req.Status.Code, nil
   367  	}
   368  }