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