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