github.com/recobe182/terraform@v0.8.5-0.20170117231232-49ab22a935b7/builtin/providers/aws/resource_aws_ami.go (about) 1 package aws 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "log" 8 "strings" 9 "time" 10 11 "github.com/aws/aws-sdk-go/aws" 12 "github.com/aws/aws-sdk-go/aws/awserr" 13 "github.com/aws/aws-sdk-go/service/ec2" 14 15 "github.com/hashicorp/terraform/helper/hashcode" 16 "github.com/hashicorp/terraform/helper/schema" 17 ) 18 19 func resourceAwsAmi() *schema.Resource { 20 // Our schema is shared also with aws_ami_copy and aws_ami_from_instance 21 resourceSchema := resourceAwsAmiCommonSchema(false) 22 23 return &schema.Resource{ 24 Create: resourceAwsAmiCreate, 25 26 Schema: resourceSchema, 27 28 // The Read, Update and Delete operations are shared with aws_ami_copy 29 // and aws_ami_from_instance, since they differ only in how the image 30 // is created. 31 Read: resourceAwsAmiRead, 32 Update: resourceAwsAmiUpdate, 33 Delete: resourceAwsAmiDelete, 34 } 35 } 36 37 func resourceAwsAmiCreate(d *schema.ResourceData, meta interface{}) error { 38 client := meta.(*AWSClient).ec2conn 39 40 req := &ec2.RegisterImageInput{ 41 Name: aws.String(d.Get("name").(string)), 42 Description: aws.String(d.Get("description").(string)), 43 Architecture: aws.String(d.Get("architecture").(string)), 44 ImageLocation: aws.String(d.Get("image_location").(string)), 45 RootDeviceName: aws.String(d.Get("root_device_name").(string)), 46 SriovNetSupport: aws.String(d.Get("sriov_net_support").(string)), 47 VirtualizationType: aws.String(d.Get("virtualization_type").(string)), 48 } 49 50 if kernelId := d.Get("kernel_id").(string); kernelId != "" { 51 req.KernelId = aws.String(kernelId) 52 } 53 if ramdiskId := d.Get("ramdisk_id").(string); ramdiskId != "" { 54 req.RamdiskId = aws.String(ramdiskId) 55 } 56 57 ebsBlockDevsSet := d.Get("ebs_block_device").(*schema.Set) 58 ephemeralBlockDevsSet := d.Get("ephemeral_block_device").(*schema.Set) 59 for _, ebsBlockDevI := range ebsBlockDevsSet.List() { 60 ebsBlockDev := ebsBlockDevI.(map[string]interface{}) 61 blockDev := &ec2.BlockDeviceMapping{ 62 DeviceName: aws.String(ebsBlockDev["device_name"].(string)), 63 Ebs: &ec2.EbsBlockDevice{ 64 DeleteOnTermination: aws.Bool(ebsBlockDev["delete_on_termination"].(bool)), 65 VolumeSize: aws.Int64(int64(ebsBlockDev["volume_size"].(int))), 66 VolumeType: aws.String(ebsBlockDev["volume_type"].(string)), 67 }, 68 } 69 if iops := ebsBlockDev["iops"].(int); iops != 0 { 70 blockDev.Ebs.Iops = aws.Int64(int64(iops)) 71 } 72 encrypted := ebsBlockDev["encrypted"].(bool) 73 if snapshotId := ebsBlockDev["snapshot_id"].(string); snapshotId != "" { 74 blockDev.Ebs.SnapshotId = aws.String(snapshotId) 75 if encrypted { 76 return errors.New("can't set both 'snapshot_id' and 'encrypted'") 77 } 78 } else if encrypted { 79 blockDev.Ebs.Encrypted = aws.Bool(true) 80 } 81 req.BlockDeviceMappings = append(req.BlockDeviceMappings, blockDev) 82 } 83 for _, ephemeralBlockDevI := range ephemeralBlockDevsSet.List() { 84 ephemeralBlockDev := ephemeralBlockDevI.(map[string]interface{}) 85 blockDev := &ec2.BlockDeviceMapping{ 86 DeviceName: aws.String(ephemeralBlockDev["device_name"].(string)), 87 VirtualName: aws.String(ephemeralBlockDev["virtual_name"].(string)), 88 } 89 req.BlockDeviceMappings = append(req.BlockDeviceMappings, blockDev) 90 } 91 92 res, err := client.RegisterImage(req) 93 if err != nil { 94 return err 95 } 96 97 id := *res.ImageId 98 d.SetId(id) 99 d.Partial(true) // make sure we record the id even if the rest of this gets interrupted 100 d.Set("id", id) 101 d.Set("manage_ebs_block_devices", false) 102 d.SetPartial("id") 103 d.SetPartial("manage_ebs_block_devices") 104 d.Partial(false) 105 106 _, err = resourceAwsAmiWaitForAvailable(id, client) 107 if err != nil { 108 return err 109 } 110 111 return resourceAwsAmiUpdate(d, meta) 112 } 113 114 func resourceAwsAmiRead(d *schema.ResourceData, meta interface{}) error { 115 client := meta.(*AWSClient).ec2conn 116 id := d.Id() 117 118 req := &ec2.DescribeImagesInput{ 119 ImageIds: []*string{aws.String(id)}, 120 } 121 122 res, err := client.DescribeImages(req) 123 if err != nil { 124 if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidAMIID.NotFound" { 125 log.Printf("[DEBUG] %s no longer exists, so we'll drop it from the state", id) 126 d.SetId("") 127 return nil 128 } 129 130 return err 131 } 132 133 if len(res.Images) != 1 { 134 d.SetId("") 135 return nil 136 } 137 138 image := res.Images[0] 139 state := *image.State 140 141 if state == "pending" { 142 // This could happen if a user manually adds an image we didn't create 143 // to the state. We'll wait for the image to become available 144 // before we continue. We should never take this branch in normal 145 // circumstances since we would've waited for availability during 146 // the "Create" step. 147 image, err = resourceAwsAmiWaitForAvailable(id, client) 148 if err != nil { 149 return err 150 } 151 state = *image.State 152 } 153 154 if state == "deregistered" { 155 d.SetId("") 156 return nil 157 } 158 159 if state != "available" { 160 return fmt.Errorf("AMI has become %s", state) 161 } 162 163 d.Set("name", image.Name) 164 d.Set("description", image.Description) 165 d.Set("image_location", image.ImageLocation) 166 d.Set("architecture", image.Architecture) 167 d.Set("kernel_id", image.KernelId) 168 d.Set("ramdisk_id", image.RamdiskId) 169 d.Set("root_device_name", image.RootDeviceName) 170 d.Set("sriov_net_support", image.SriovNetSupport) 171 d.Set("virtualization_type", image.VirtualizationType) 172 173 var ebsBlockDevs []map[string]interface{} 174 var ephemeralBlockDevs []map[string]interface{} 175 176 for _, blockDev := range image.BlockDeviceMappings { 177 if blockDev.Ebs != nil { 178 ebsBlockDev := map[string]interface{}{ 179 "device_name": *blockDev.DeviceName, 180 "delete_on_termination": *blockDev.Ebs.DeleteOnTermination, 181 "encrypted": *blockDev.Ebs.Encrypted, 182 "iops": 0, 183 "volume_size": int(*blockDev.Ebs.VolumeSize), 184 "volume_type": *blockDev.Ebs.VolumeType, 185 } 186 if blockDev.Ebs.Iops != nil { 187 ebsBlockDev["iops"] = int(*blockDev.Ebs.Iops) 188 } 189 // The snapshot ID might not be set. 190 if blockDev.Ebs.SnapshotId != nil { 191 ebsBlockDev["snapshot_id"] = *blockDev.Ebs.SnapshotId 192 } 193 ebsBlockDevs = append(ebsBlockDevs, ebsBlockDev) 194 } else { 195 ephemeralBlockDevs = append(ephemeralBlockDevs, map[string]interface{}{ 196 "device_name": *blockDev.DeviceName, 197 "virtual_name": *blockDev.VirtualName, 198 }) 199 } 200 } 201 202 d.Set("ebs_block_device", ebsBlockDevs) 203 d.Set("ephemeral_block_device", ephemeralBlockDevs) 204 205 d.Set("tags", tagsToMap(image.Tags)) 206 207 return nil 208 } 209 210 func resourceAwsAmiUpdate(d *schema.ResourceData, meta interface{}) error { 211 client := meta.(*AWSClient).ec2conn 212 213 d.Partial(true) 214 215 if err := setTags(client, d); err != nil { 216 return err 217 } else { 218 d.SetPartial("tags") 219 } 220 221 if d.Get("description").(string) != "" { 222 _, err := client.ModifyImageAttribute(&ec2.ModifyImageAttributeInput{ 223 ImageId: aws.String(d.Id()), 224 Description: &ec2.AttributeValue{ 225 Value: aws.String(d.Get("description").(string)), 226 }, 227 }) 228 if err != nil { 229 return err 230 } 231 d.SetPartial("description") 232 } 233 234 d.Partial(false) 235 236 return resourceAwsAmiRead(d, meta) 237 } 238 239 func resourceAwsAmiDelete(d *schema.ResourceData, meta interface{}) error { 240 client := meta.(*AWSClient).ec2conn 241 242 req := &ec2.DeregisterImageInput{ 243 ImageId: aws.String(d.Id()), 244 } 245 246 _, err := client.DeregisterImage(req) 247 if err != nil { 248 return err 249 } 250 251 // If we're managing the EBS snapshots then we need to delete those too. 252 if d.Get("manage_ebs_snapshots").(bool) { 253 errs := map[string]error{} 254 ebsBlockDevsSet := d.Get("ebs_block_device").(*schema.Set) 255 req := &ec2.DeleteSnapshotInput{} 256 for _, ebsBlockDevI := range ebsBlockDevsSet.List() { 257 ebsBlockDev := ebsBlockDevI.(map[string]interface{}) 258 snapshotId := ebsBlockDev["snapshot_id"].(string) 259 if snapshotId != "" { 260 req.SnapshotId = aws.String(snapshotId) 261 _, err := client.DeleteSnapshot(req) 262 if err != nil { 263 errs[snapshotId] = err 264 } 265 } 266 } 267 268 if len(errs) > 0 { 269 errParts := []string{"Errors while deleting associated EBS snapshots:"} 270 for snapshotId, err := range errs { 271 errParts = append(errParts, fmt.Sprintf("%s: %s", snapshotId, err)) 272 } 273 errParts = append(errParts, "These are no longer managed by Terraform and must be deleted manually.") 274 return errors.New(strings.Join(errParts, "\n")) 275 } 276 } 277 278 d.SetId("") 279 280 return nil 281 } 282 283 func resourceAwsAmiWaitForAvailable(id string, client *ec2.EC2) (*ec2.Image, error) { 284 log.Printf("Waiting for AMI %s to become available...", id) 285 286 req := &ec2.DescribeImagesInput{ 287 ImageIds: []*string{aws.String(id)}, 288 } 289 pollsWhereNotFound := 0 290 for { 291 res, err := client.DescribeImages(req) 292 if err != nil { 293 // When using RegisterImage (for aws_ami) the AMI sometimes isn't available at all 294 // right after the API responds, so we need to tolerate a couple Not Found errors 295 // before an available AMI shows up. 296 if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidAMIID.NotFound" { 297 pollsWhereNotFound++ 298 // We arbitrarily stop polling after getting a "not found" error five times, 299 // assuming that the AMI has been deleted by something other than Terraform. 300 if pollsWhereNotFound > 5 { 301 return nil, fmt.Errorf("gave up waiting for AMI to be created: %s", err) 302 } 303 time.Sleep(4 * time.Second) 304 continue 305 } 306 return nil, fmt.Errorf("error reading AMI: %s", err) 307 } 308 309 if len(res.Images) != 1 { 310 return nil, fmt.Errorf("new AMI vanished while pending") 311 } 312 313 state := *res.Images[0].State 314 315 if state == "pending" { 316 // Give it a few seconds before we poll again. 317 time.Sleep(4 * time.Second) 318 continue 319 } 320 321 if state == "available" { 322 // We're done! 323 return res.Images[0], nil 324 } 325 326 // If we're not pending or available then we're in one of the invalid/error 327 // states, so stop polling and bail out. 328 stateReason := *res.Images[0].StateReason 329 return nil, fmt.Errorf("new AMI became %s while pending: %s", state, stateReason) 330 } 331 } 332 333 func resourceAwsAmiCommonSchema(computed bool) map[string]*schema.Schema { 334 // The "computed" parameter controls whether we're making 335 // a schema for an AMI that's been implicitly registered (aws_ami_copy, aws_ami_from_instance) 336 // or whether we're making a schema for an explicit registration (aws_ami). 337 // When set, almost every attribute is marked as "computed". 338 // When not set, only the "id" attribute is computed. 339 // "name" and "description" are never computed, since they must always 340 // be provided by the user. 341 342 var virtualizationTypeDefault interface{} 343 var deleteEbsOnTerminationDefault interface{} 344 var sriovNetSupportDefault interface{} 345 var architectureDefault interface{} 346 var volumeTypeDefault interface{} 347 if !computed { 348 virtualizationTypeDefault = "paravirtual" 349 deleteEbsOnTerminationDefault = true 350 sriovNetSupportDefault = "simple" 351 architectureDefault = "x86_64" 352 volumeTypeDefault = "standard" 353 } 354 355 return map[string]*schema.Schema{ 356 "id": &schema.Schema{ 357 Type: schema.TypeString, 358 Computed: true, 359 }, 360 "image_location": &schema.Schema{ 361 Type: schema.TypeString, 362 Optional: !computed, 363 Computed: true, 364 ForceNew: !computed, 365 }, 366 "architecture": &schema.Schema{ 367 Type: schema.TypeString, 368 Optional: !computed, 369 Computed: computed, 370 ForceNew: !computed, 371 Default: architectureDefault, 372 }, 373 "description": &schema.Schema{ 374 Type: schema.TypeString, 375 Optional: true, 376 }, 377 "kernel_id": &schema.Schema{ 378 Type: schema.TypeString, 379 Optional: !computed, 380 Computed: computed, 381 ForceNew: !computed, 382 }, 383 "name": &schema.Schema{ 384 Type: schema.TypeString, 385 Required: true, 386 ForceNew: true, 387 }, 388 "ramdisk_id": &schema.Schema{ 389 Type: schema.TypeString, 390 Optional: !computed, 391 Computed: computed, 392 ForceNew: !computed, 393 }, 394 "root_device_name": &schema.Schema{ 395 Type: schema.TypeString, 396 Optional: !computed, 397 Computed: computed, 398 ForceNew: !computed, 399 }, 400 "sriov_net_support": &schema.Schema{ 401 Type: schema.TypeString, 402 Optional: !computed, 403 Computed: computed, 404 ForceNew: !computed, 405 Default: sriovNetSupportDefault, 406 }, 407 "virtualization_type": &schema.Schema{ 408 Type: schema.TypeString, 409 Optional: !computed, 410 Computed: computed, 411 ForceNew: !computed, 412 Default: virtualizationTypeDefault, 413 }, 414 415 // The following block device attributes intentionally mimick the 416 // corresponding attributes on aws_instance, since they have the 417 // same meaning. 418 // However, we don't use root_block_device here because the constraint 419 // on which root device attributes can be overridden for an instance to 420 // not apply when registering an AMI. 421 422 "ebs_block_device": &schema.Schema{ 423 Type: schema.TypeSet, 424 Optional: true, 425 Computed: true, 426 Elem: &schema.Resource{ 427 Schema: map[string]*schema.Schema{ 428 "delete_on_termination": &schema.Schema{ 429 Type: schema.TypeBool, 430 Optional: !computed, 431 Default: deleteEbsOnTerminationDefault, 432 ForceNew: !computed, 433 Computed: computed, 434 }, 435 436 "device_name": &schema.Schema{ 437 Type: schema.TypeString, 438 Required: !computed, 439 ForceNew: !computed, 440 Computed: computed, 441 }, 442 443 "encrypted": &schema.Schema{ 444 Type: schema.TypeBool, 445 Optional: !computed, 446 Computed: computed, 447 ForceNew: !computed, 448 }, 449 450 "iops": &schema.Schema{ 451 Type: schema.TypeInt, 452 Optional: !computed, 453 Computed: computed, 454 ForceNew: !computed, 455 }, 456 457 "snapshot_id": &schema.Schema{ 458 Type: schema.TypeString, 459 Optional: !computed, 460 Computed: computed, 461 ForceNew: !computed, 462 }, 463 464 "volume_size": &schema.Schema{ 465 Type: schema.TypeInt, 466 Optional: !computed, 467 Computed: true, 468 ForceNew: !computed, 469 }, 470 471 "volume_type": &schema.Schema{ 472 Type: schema.TypeString, 473 Optional: !computed, 474 Computed: computed, 475 ForceNew: !computed, 476 Default: volumeTypeDefault, 477 }, 478 }, 479 }, 480 Set: func(v interface{}) int { 481 var buf bytes.Buffer 482 m := v.(map[string]interface{}) 483 buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) 484 buf.WriteString(fmt.Sprintf("%s-", m["snapshot_id"].(string))) 485 return hashcode.String(buf.String()) 486 }, 487 }, 488 489 "ephemeral_block_device": &schema.Schema{ 490 Type: schema.TypeSet, 491 Optional: true, 492 Computed: true, 493 ForceNew: true, 494 Elem: &schema.Resource{ 495 Schema: map[string]*schema.Schema{ 496 "device_name": &schema.Schema{ 497 Type: schema.TypeString, 498 Required: !computed, 499 Computed: computed, 500 }, 501 502 "virtual_name": &schema.Schema{ 503 Type: schema.TypeString, 504 Required: !computed, 505 Computed: computed, 506 }, 507 }, 508 }, 509 Set: func(v interface{}) int { 510 var buf bytes.Buffer 511 m := v.(map[string]interface{}) 512 buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) 513 buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string))) 514 return hashcode.String(buf.String()) 515 }, 516 }, 517 518 "tags": tagsSchema(), 519 520 // Not a public attribute; used to let the aws_ami_copy and aws_ami_from_instance 521 // resources record that they implicitly created new EBS snapshots that we should 522 // now manage. Not set by aws_ami, since the snapshots used there are presumed to 523 // be independently managed. 524 "manage_ebs_snapshots": &schema.Schema{ 525 Type: schema.TypeBool, 526 Computed: true, 527 ForceNew: true, 528 }, 529 } 530 }