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 }