github.com/jmbataller/terraform@v0.6.8-0.20151125192640-b7a12e3a580c/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["spot_price"] = &schema.Schema{
    35  				Type:     schema.TypeString,
    36  				Required: true,
    37  				ForceNew: true,
    38  			}
    39  			s["spot_type"] = &schema.Schema{
    40  				Type:     schema.TypeString,
    41  				Optional: true,
    42  				Default:  "persistent",
    43  			}
    44  			s["wait_for_fulfillment"] = &schema.Schema{
    45  				Type:     schema.TypeBool,
    46  				Optional: true,
    47  				Default:  false,
    48  			}
    49  			s["spot_bid_status"] = &schema.Schema{
    50  				Type:     schema.TypeString,
    51  				Computed: true,
    52  			}
    53  			s["spot_request_state"] = &schema.Schema{
    54  				Type:     schema.TypeString,
    55  				Computed: true,
    56  			}
    57  			s["spot_instance_id"] = &schema.Schema{
    58  				Type:     schema.TypeString,
    59  				Computed: true,
    60  			}
    61  
    62  			return s
    63  		}(),
    64  	}
    65  }
    66  
    67  func resourceAwsSpotInstanceRequestCreate(d *schema.ResourceData, meta interface{}) error {
    68  	conn := meta.(*AWSClient).ec2conn
    69  
    70  	instanceOpts, err := buildAwsInstanceOpts(d, meta)
    71  	if err != nil {
    72  		return err
    73  	}
    74  
    75  	spotOpts := &ec2.RequestSpotInstancesInput{
    76  		SpotPrice: aws.String(d.Get("spot_price").(string)),
    77  		Type:      aws.String(d.Get("spot_type").(string)),
    78  
    79  		// Though the AWS API supports creating spot instance requests for multiple
    80  		// instances, for TF purposes we fix this to one instance per request.
    81  		// Users can get equivalent behavior out of TF's "count" meta-parameter.
    82  		InstanceCount: aws.Int64(1),
    83  
    84  		LaunchSpecification: &ec2.RequestSpotLaunchSpecification{
    85  			BlockDeviceMappings: instanceOpts.BlockDeviceMappings,
    86  			EbsOptimized:        instanceOpts.EBSOptimized,
    87  			Monitoring:          instanceOpts.Monitoring,
    88  			IamInstanceProfile:  instanceOpts.IAMInstanceProfile,
    89  			ImageId:             instanceOpts.ImageID,
    90  			InstanceType:        instanceOpts.InstanceType,
    91  			KeyName:             instanceOpts.KeyName,
    92  			Placement:           instanceOpts.SpotPlacement,
    93  			SecurityGroupIds:    instanceOpts.SecurityGroupIDs,
    94  			SecurityGroups:      instanceOpts.SecurityGroups,
    95  			SubnetId:            instanceOpts.SubnetID,
    96  			UserData:            instanceOpts.UserData64,
    97  		},
    98  	}
    99  
   100  	// If the instance is configured with a Network Interface (a subnet, has
   101  	// public IP, etc), then the instanceOpts.SecurityGroupIds and SubnetId will
   102  	// be nil
   103  	if len(instanceOpts.NetworkInterfaces) > 0 {
   104  		spotOpts.LaunchSpecification.SecurityGroupIds = instanceOpts.NetworkInterfaces[0].Groups
   105  		spotOpts.LaunchSpecification.SubnetId = instanceOpts.NetworkInterfaces[0].SubnetId
   106  	}
   107  
   108  	// Make the spot instance request
   109  	log.Printf("[DEBUG] Requesting spot bid opts: %s", spotOpts)
   110  	resp, err := conn.RequestSpotInstances(spotOpts)
   111  	if err != nil {
   112  		return fmt.Errorf("Error requesting spot instances: %s", err)
   113  	}
   114  	if len(resp.SpotInstanceRequests) != 1 {
   115  		return fmt.Errorf(
   116  			"Expected response with length 1, got: %s", resp)
   117  	}
   118  
   119  	sir := *resp.SpotInstanceRequests[0]
   120  	d.SetId(*sir.SpotInstanceRequestId)
   121  
   122  	if d.Get("wait_for_fulfillment").(bool) {
   123  		spotStateConf := &resource.StateChangeConf{
   124  			// http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-bid-status.html
   125  			Pending:    []string{"start", "pending-evaluation", "pending-fulfillment"},
   126  			Target:     "fulfilled",
   127  			Refresh:    SpotInstanceStateRefreshFunc(conn, sir),
   128  			Timeout:    10 * time.Minute,
   129  			Delay:      10 * time.Second,
   130  			MinTimeout: 3 * time.Second,
   131  		}
   132  
   133  		log.Printf("[DEBUG] waiting for spot bid to resolve... this may take several minutes.")
   134  		_, err = spotStateConf.WaitForState()
   135  
   136  		if err != nil {
   137  			return fmt.Errorf("Error while waiting for spot request (%s) to resolve: %s", sir, err)
   138  		}
   139  	}
   140  
   141  	return resourceAwsSpotInstanceRequestUpdate(d, meta)
   142  }
   143  
   144  // Update spot state, etc
   145  func resourceAwsSpotInstanceRequestRead(d *schema.ResourceData, meta interface{}) error {
   146  	conn := meta.(*AWSClient).ec2conn
   147  
   148  	req := &ec2.DescribeSpotInstanceRequestsInput{
   149  		SpotInstanceRequestIds: []*string{aws.String(d.Id())},
   150  	}
   151  	resp, err := conn.DescribeSpotInstanceRequests(req)
   152  
   153  	if err != nil {
   154  		// If the spot request was not found, return nil so that we can show
   155  		// that it is gone.
   156  		if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidSpotInstanceRequestID.NotFound" {
   157  			d.SetId("")
   158  			return nil
   159  		}
   160  
   161  		// Some other error, report it
   162  		return err
   163  	}
   164  
   165  	// If nothing was found, then return no state
   166  	if len(resp.SpotInstanceRequests) == 0 {
   167  		d.SetId("")
   168  		return nil
   169  	}
   170  
   171  	request := resp.SpotInstanceRequests[0]
   172  
   173  	// if the request is cancelled, then it is gone
   174  	if *request.State == "cancelled" {
   175  		d.SetId("")
   176  		return nil
   177  	}
   178  
   179  	d.Set("spot_bid_status", *request.Status.Code)
   180  	// Instance ID is not set if the request is still pending
   181  	if request.InstanceId != nil {
   182  		d.Set("spot_instance_id", *request.InstanceId)
   183  		// Read the instance data, setting up connection information
   184  		if err := readInstance(d, meta); err != nil {
   185  			return fmt.Errorf("[ERR] Error reading Spot Instance Data: %s", err)
   186  		}
   187  	}
   188  	d.Set("spot_request_state", *request.State)
   189  	d.Set("tags", tagsToMap(request.Tags))
   190  
   191  	return nil
   192  }
   193  
   194  func readInstance(d *schema.ResourceData, meta interface{}) error {
   195  	conn := meta.(*AWSClient).ec2conn
   196  
   197  	resp, err := conn.DescribeInstances(&ec2.DescribeInstancesInput{
   198  		InstanceIds: []*string{aws.String(d.Get("spot_instance_id").(string))},
   199  	})
   200  	if err != nil {
   201  		// If the instance was not found, return nil so that we can show
   202  		// that the instance is gone.
   203  		if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidInstanceID.NotFound" {
   204  			return fmt.Errorf("no instance found")
   205  		}
   206  
   207  		// Some other error, report it
   208  		return err
   209  	}
   210  
   211  	// If nothing was found, then return no state
   212  	if len(resp.Reservations) == 0 {
   213  		return fmt.Errorf("no instances found")
   214  	}
   215  
   216  	instance := resp.Reservations[0].Instances[0]
   217  
   218  	// Set these fields for connection information
   219  	if instance != nil {
   220  		d.Set("public_dns", instance.PublicDnsName)
   221  		d.Set("public_ip", instance.PublicIpAddress)
   222  		d.Set("private_dns", instance.PrivateDnsName)
   223  		d.Set("private_ip", instance.PrivateIpAddress)
   224  
   225  		// set connection information
   226  		if instance.PublicIpAddress != nil {
   227  			d.SetConnInfo(map[string]string{
   228  				"type": "ssh",
   229  				"host": *instance.PublicIpAddress,
   230  			})
   231  		} else if instance.PrivateIpAddress != nil {
   232  			d.SetConnInfo(map[string]string{
   233  				"type": "ssh",
   234  				"host": *instance.PrivateIpAddress,
   235  			})
   236  		}
   237  	}
   238  
   239  	return nil
   240  }
   241  
   242  func resourceAwsSpotInstanceRequestUpdate(d *schema.ResourceData, meta interface{}) error {
   243  	conn := meta.(*AWSClient).ec2conn
   244  
   245  	d.Partial(true)
   246  	if err := setTags(conn, d); err != nil {
   247  		return err
   248  	} else {
   249  		d.SetPartial("tags")
   250  	}
   251  
   252  	d.Partial(false)
   253  
   254  	return resourceAwsSpotInstanceRequestRead(d, meta)
   255  }
   256  
   257  func resourceAwsSpotInstanceRequestDelete(d *schema.ResourceData, meta interface{}) error {
   258  	conn := meta.(*AWSClient).ec2conn
   259  
   260  	log.Printf("[INFO] Cancelling spot request: %s", d.Id())
   261  	_, err := conn.CancelSpotInstanceRequests(&ec2.CancelSpotInstanceRequestsInput{
   262  		SpotInstanceRequestIds: []*string{aws.String(d.Id())},
   263  	})
   264  
   265  	if err != nil {
   266  		return fmt.Errorf("Error cancelling spot request (%s): %s", d.Id(), err)
   267  	}
   268  
   269  	if instanceId := d.Get("spot_instance_id").(string); instanceId != "" {
   270  		log.Printf("[INFO] Terminating instance: %s", instanceId)
   271  		if err := awsTerminateInstance(conn, instanceId); err != nil {
   272  			return fmt.Errorf("Error terminating spot instance: %s", err)
   273  		}
   274  	}
   275  
   276  	return nil
   277  }
   278  
   279  // SpotInstanceStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
   280  // an EC2 spot instance request
   281  func SpotInstanceStateRefreshFunc(
   282  	conn *ec2.EC2, sir ec2.SpotInstanceRequest) resource.StateRefreshFunc {
   283  
   284  	return func() (interface{}, string, error) {
   285  		resp, err := conn.DescribeSpotInstanceRequests(&ec2.DescribeSpotInstanceRequestsInput{
   286  			SpotInstanceRequestIds: []*string{sir.SpotInstanceRequestId},
   287  		})
   288  
   289  		if err != nil {
   290  			if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidSpotInstanceRequestID.NotFound" {
   291  				// Set this to nil as if we didn't find anything.
   292  				resp = nil
   293  			} else {
   294  				log.Printf("Error on StateRefresh: %s", err)
   295  				return nil, "", err
   296  			}
   297  		}
   298  
   299  		if resp == nil || len(resp.SpotInstanceRequests) == 0 {
   300  			// Sometimes AWS just has consistency issues and doesn't see
   301  			// our request yet. Return an empty state.
   302  			return nil, "", nil
   303  		}
   304  
   305  		req := resp.SpotInstanceRequests[0]
   306  		return req, *req.Status.Code, nil
   307  	}
   308  }