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