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 }