github.com/econnell/terraform@v0.5.4-0.20150722160631-78eb236786a4/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/aws/awsutil" 11 "github.com/aws/aws-sdk-go/service/ec2" 12 "github.com/hashicorp/terraform/helper/resource" 13 "github.com/hashicorp/terraform/helper/schema" 14 ) 15 16 func resourceAwsSpotInstanceRequest() *schema.Resource { 17 return &schema.Resource{ 18 Create: resourceAwsSpotInstanceRequestCreate, 19 Read: resourceAwsSpotInstanceRequestRead, 20 Delete: resourceAwsSpotInstanceRequestDelete, 21 Update: resourceAwsSpotInstanceRequestUpdate, 22 23 Schema: func() map[string]*schema.Schema { 24 // The Spot Instance Request Schema is based on the AWS Instance schema. 25 s := resourceAwsInstance().Schema 26 27 // Everything on a spot instance is ForceNew except tags 28 for k, v := range s { 29 if k == "tags" { 30 continue 31 } 32 v.ForceNew = true 33 } 34 35 s["spot_price"] = &schema.Schema{ 36 Type: schema.TypeString, 37 Required: true, 38 ForceNew: true, 39 } 40 s["wait_for_fulfillment"] = &schema.Schema{ 41 Type: schema.TypeBool, 42 Optional: true, 43 Default: false, 44 } 45 s["spot_bid_status"] = &schema.Schema{ 46 Type: schema.TypeString, 47 Computed: true, 48 } 49 s["spot_request_state"] = &schema.Schema{ 50 Type: schema.TypeString, 51 Computed: true, 52 } 53 s["spot_instance_id"] = &schema.Schema{ 54 Type: schema.TypeString, 55 Computed: true, 56 } 57 58 return s 59 }(), 60 } 61 } 62 63 func resourceAwsSpotInstanceRequestCreate(d *schema.ResourceData, meta interface{}) error { 64 conn := meta.(*AWSClient).ec2conn 65 66 instanceOpts, err := buildAwsInstanceOpts(d, meta) 67 if err != nil { 68 return err 69 } 70 71 spotOpts := &ec2.RequestSpotInstancesInput{ 72 SpotPrice: aws.String(d.Get("spot_price").(string)), 73 74 // We always set the type to "persistent", since the imperative-like 75 // behavior of "one-time" does not map well to TF's declarative domain. 76 Type: aws.String("persistent"), 77 78 // Though the AWS API supports creating spot instance requests for multiple 79 // instances, for TF purposes we fix this to one instance per request. 80 // Users can get equivalent behavior out of TF's "count" meta-parameter. 81 InstanceCount: aws.Long(1), 82 83 LaunchSpecification: &ec2.RequestSpotLaunchSpecification{ 84 BlockDeviceMappings: instanceOpts.BlockDeviceMappings, 85 EBSOptimized: instanceOpts.EBSOptimized, 86 Monitoring: instanceOpts.Monitoring, 87 IAMInstanceProfile: instanceOpts.IAMInstanceProfile, 88 ImageID: instanceOpts.ImageID, 89 InstanceType: instanceOpts.InstanceType, 90 Placement: instanceOpts.SpotPlacement, 91 SecurityGroupIDs: instanceOpts.SecurityGroupIDs, 92 SecurityGroups: instanceOpts.SecurityGroups, 93 UserData: instanceOpts.UserData64, 94 }, 95 } 96 97 // Make the spot instance request 98 log.Printf("[DEBUG] Requesting spot bid opts: %s", awsutil.StringValue(spotOpts)) 99 resp, err := conn.RequestSpotInstances(spotOpts) 100 if err != nil { 101 return fmt.Errorf("Error requesting spot instances: %s", err) 102 } 103 if len(resp.SpotInstanceRequests) != 1 { 104 return fmt.Errorf( 105 "Expected response with length 1, got: %s", awsutil.StringValue(resp)) 106 } 107 108 sir := *resp.SpotInstanceRequests[0] 109 d.SetId(*sir.SpotInstanceRequestID) 110 111 if d.Get("wait_for_fulfillment").(bool) { 112 spotStateConf := &resource.StateChangeConf{ 113 // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-bid-status.html 114 Pending: []string{"start", "pending-evaluation", "pending-fulfillment"}, 115 Target: "fulfilled", 116 Refresh: SpotInstanceStateRefreshFunc(conn, sir), 117 Timeout: 10 * time.Minute, 118 Delay: 10 * time.Second, 119 MinTimeout: 3 * time.Second, 120 } 121 122 log.Printf("[DEBUG] waiting for spot bid to resolve... this may take several minutes.") 123 _, err = spotStateConf.WaitForState() 124 125 if err != nil { 126 return fmt.Errorf("Error while waiting for spot request (%s) to resolve: %s", awsutil.StringValue(sir), err) 127 } 128 } 129 130 return resourceAwsSpotInstanceRequestUpdate(d, meta) 131 } 132 133 // Update spot state, etc 134 func resourceAwsSpotInstanceRequestRead(d *schema.ResourceData, meta interface{}) error { 135 conn := meta.(*AWSClient).ec2conn 136 137 req := &ec2.DescribeSpotInstanceRequestsInput{ 138 SpotInstanceRequestIDs: []*string{aws.String(d.Id())}, 139 } 140 resp, err := conn.DescribeSpotInstanceRequests(req) 141 142 if err != nil { 143 // If the spot request was not found, return nil so that we can show 144 // that it is gone. 145 if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidSpotInstanceRequestID.NotFound" { 146 d.SetId("") 147 return nil 148 } 149 150 // Some other error, report it 151 return err 152 } 153 154 // If nothing was found, then return no state 155 if len(resp.SpotInstanceRequests) == 0 { 156 d.SetId("") 157 return nil 158 } 159 160 request := resp.SpotInstanceRequests[0] 161 162 // if the request is cancelled, then it is gone 163 if *request.State == "cancelled" { 164 d.SetId("") 165 return nil 166 } 167 168 d.Set("spot_bid_status", *request.Status.Code) 169 // Instance ID is not set if the request is still pending 170 if request.InstanceID != nil { 171 d.Set("spot_instance_id", *request.InstanceID) 172 } 173 d.Set("spot_request_state", *request.State) 174 d.Set("tags", tagsToMap(request.Tags)) 175 176 return nil 177 } 178 179 func resourceAwsSpotInstanceRequestUpdate(d *schema.ResourceData, meta interface{}) error { 180 conn := meta.(*AWSClient).ec2conn 181 182 d.Partial(true) 183 if err := setTags(conn, d); err != nil { 184 return err 185 } else { 186 d.SetPartial("tags") 187 } 188 189 d.Partial(false) 190 191 return resourceAwsSpotInstanceRequestRead(d, meta) 192 } 193 194 func resourceAwsSpotInstanceRequestDelete(d *schema.ResourceData, meta interface{}) error { 195 conn := meta.(*AWSClient).ec2conn 196 197 log.Printf("[INFO] Cancelling spot request: %s", d.Id()) 198 _, err := conn.CancelSpotInstanceRequests(&ec2.CancelSpotInstanceRequestsInput{ 199 SpotInstanceRequestIDs: []*string{aws.String(d.Id())}, 200 }) 201 202 if err != nil { 203 return fmt.Errorf("Error cancelling spot request (%s): %s", d.Id(), err) 204 } 205 206 if instanceId := d.Get("spot_instance_id").(string); instanceId != "" { 207 log.Printf("[INFO] Terminating instance: %s", instanceId) 208 if err := awsTerminateInstance(conn, instanceId); err != nil { 209 return fmt.Errorf("Error terminating spot instance: %s", err) 210 } 211 } 212 213 return nil 214 } 215 216 // SpotInstanceStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch 217 // an EC2 spot instance request 218 func SpotInstanceStateRefreshFunc( 219 conn *ec2.EC2, sir ec2.SpotInstanceRequest) resource.StateRefreshFunc { 220 221 return func() (interface{}, string, error) { 222 resp, err := conn.DescribeSpotInstanceRequests(&ec2.DescribeSpotInstanceRequestsInput{ 223 SpotInstanceRequestIDs: []*string{sir.SpotInstanceRequestID}, 224 }) 225 226 if err != nil { 227 if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidSpotInstanceRequestID.NotFound" { 228 // Set this to nil as if we didn't find anything. 229 resp = nil 230 } else { 231 log.Printf("Error on StateRefresh: %s", err) 232 return nil, "", err 233 } 234 } 235 236 if resp == nil || len(resp.SpotInstanceRequests) == 0 { 237 // Sometimes AWS just has consistency issues and doesn't see 238 // our request yet. Return an empty state. 239 return nil, "", nil 240 } 241 242 req := resp.SpotInstanceRequests[0] 243 return req, *req.Status.Code, nil 244 } 245 }