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