github.com/coreos/mantle@v0.13.0/platform/api/aws/images.go (about) 1 // Copyright 2016 CoreOS, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package aws 16 17 import ( 18 "errors" 19 "fmt" 20 "net/url" 21 "strings" 22 "sync" 23 "time" 24 25 "github.com/aws/aws-sdk-go/aws" 26 "github.com/aws/aws-sdk-go/aws/awserr" 27 "github.com/aws/aws-sdk-go/aws/endpoints" 28 "github.com/aws/aws-sdk-go/aws/request" 29 "github.com/aws/aws-sdk-go/service/ec2" 30 "github.com/aws/aws-sdk-go/service/iam" 31 ) 32 33 // The default size of Container Linux disks on AWS, in GiB. See discussion in 34 // https://github.com/coreos/mantle/pull/944 35 const ContainerLinuxDiskSizeGiB = 8 36 37 var ( 38 NoRegionPVSupport = errors.New("Region does not support PV") 39 ) 40 41 type EC2ImageType string 42 43 const ( 44 EC2ImageTypeHVM EC2ImageType = "hvm" 45 EC2ImageTypePV EC2ImageType = "paravirtual" 46 ) 47 48 type EC2ImageFormat string 49 50 const ( 51 EC2ImageFormatRaw EC2ImageFormat = ec2.DiskImageFormatRaw 52 EC2ImageFormatVmdk EC2ImageFormat = ec2.DiskImageFormatVmdk 53 ) 54 55 // TODO, these can be derived at runtime 56 // these are pv-grub-hd0_1.04-x86_64 57 var akis = map[string]string{ 58 "us-east-1": "aki-919dcaf8", 59 "us-west-1": "aki-880531cd", 60 "us-west-2": "aki-fc8f11cc", 61 "eu-west-1": "aki-52a34525", 62 "eu-central-1": "aki-184c7a05", 63 "ap-southeast-1": "aki-503e7402", 64 "ap-southeast-2": "aki-c362fff9", 65 "ap-northeast-1": "aki-176bf516", 66 "sa-east-1": "aki-5553f448", 67 68 "us-gov-west-1": "aki-1de98d3e", 69 "cn-north-1": "aki-9e8f1da7", 70 } 71 72 func RegionSupportsPV(region string) bool { 73 _, ok := akis[region] 74 return ok 75 } 76 77 func (e *EC2ImageFormat) Set(s string) error { 78 switch s { 79 case string(EC2ImageFormatVmdk): 80 *e = EC2ImageFormatVmdk 81 case string(EC2ImageFormatRaw): 82 *e = EC2ImageFormatRaw 83 default: 84 return fmt.Errorf("invalid ec2 image format: must be raw or vmdk") 85 } 86 return nil 87 } 88 89 func (e *EC2ImageFormat) String() string { 90 return string(*e) 91 } 92 93 func (e *EC2ImageFormat) Type() string { 94 return "ec2ImageFormat" 95 } 96 97 var vmImportRole = "vmimport" 98 99 type Snapshot struct { 100 SnapshotID string 101 } 102 103 // Look up a Snapshot by name. Return nil if not found. 104 func (a *API) FindSnapshot(imageName string) (*Snapshot, error) { 105 // Look for an existing snapshot with this image name. 106 snapshotRes, err := a.ec2.DescribeSnapshots(&ec2.DescribeSnapshotsInput{ 107 Filters: []*ec2.Filter{ 108 &ec2.Filter{ 109 Name: aws.String("status"), 110 Values: aws.StringSlice([]string{"completed"}), 111 }, 112 &ec2.Filter{ 113 Name: aws.String("tag:Name"), 114 Values: aws.StringSlice([]string{imageName}), 115 }, 116 }, 117 OwnerIds: aws.StringSlice([]string{"self"}), 118 }) 119 if err != nil { 120 return nil, fmt.Errorf("unable to describe snapshots: %v", err) 121 } 122 if len(snapshotRes.Snapshots) > 1 { 123 return nil, fmt.Errorf("found multiple matching snapshots") 124 } 125 if len(snapshotRes.Snapshots) == 1 { 126 snapshotID := *snapshotRes.Snapshots[0].SnapshotId 127 plog.Infof("found existing snapshot %v", snapshotID) 128 return &Snapshot{ 129 SnapshotID: snapshotID, 130 }, nil 131 } 132 133 // Look for an existing import task with this image name. We have 134 // to fetch all of them and walk the list ourselves. 135 var snapshotTaskID string 136 taskRes, err := a.ec2.DescribeImportSnapshotTasks(&ec2.DescribeImportSnapshotTasksInput{}) 137 if err != nil { 138 return nil, fmt.Errorf("unable to describe import tasks: %v", err) 139 } 140 for _, task := range taskRes.ImportSnapshotTasks { 141 if task.Description == nil || *task.Description != imageName { 142 continue 143 } 144 switch *task.SnapshotTaskDetail.Status { 145 case "cancelled", "cancelling", "deleted", "deleting": 146 continue 147 case "completed": 148 // Either we lost the race with a snapshot that just 149 // completed or this is an old import task for a 150 // snapshot that's been deleted. Check it. 151 _, err := a.ec2.DescribeSnapshots(&ec2.DescribeSnapshotsInput{ 152 SnapshotIds: []*string{task.SnapshotTaskDetail.SnapshotId}, 153 }) 154 if err != nil { 155 if awserr, ok := err.(awserr.Error); ok && awserr.Code() == "InvalidSnapshot.NotFound" { 156 continue 157 } else { 158 return nil, fmt.Errorf("couldn't describe snapshot from import task: %v", err) 159 } 160 } 161 } 162 if snapshotTaskID != "" { 163 return nil, fmt.Errorf("found multiple matching import tasks") 164 } 165 snapshotTaskID = *task.ImportTaskId 166 } 167 if snapshotTaskID == "" { 168 return nil, nil 169 } 170 171 plog.Infof("found existing snapshot import task %v", snapshotTaskID) 172 return a.finishSnapshotTask(snapshotTaskID, imageName) 173 } 174 175 // CreateSnapshot creates an AWS Snapshot 176 func (a *API) CreateSnapshot(imageName, sourceURL string, format EC2ImageFormat) (*Snapshot, error) { 177 if format == "" { 178 format = EC2ImageFormatVmdk 179 } 180 s3url, err := url.Parse(sourceURL) 181 if err != nil { 182 return nil, err 183 } 184 if s3url.Scheme != "s3" { 185 return nil, fmt.Errorf("source must have a 's3://' scheme, not: '%v://'", s3url.Scheme) 186 } 187 s3key := strings.TrimPrefix(s3url.Path, "/") 188 189 importRes, err := a.ec2.ImportSnapshot(&ec2.ImportSnapshotInput{ 190 RoleName: aws.String(vmImportRole), 191 Description: aws.String(imageName), 192 DiskContainer: &ec2.SnapshotDiskContainer{ 193 // TODO(euank): allow s3 source / local file -> s3 source 194 UserBucket: &ec2.UserBucket{ 195 S3Bucket: aws.String(s3url.Host), 196 S3Key: aws.String(s3key), 197 }, 198 Format: aws.String(string(format)), 199 }, 200 }) 201 if err != nil { 202 return nil, fmt.Errorf("unable to create import snapshot task: %v", err) 203 } 204 205 plog.Infof("created snapshot import task %v", *importRes.ImportTaskId) 206 return a.finishSnapshotTask(*importRes.ImportTaskId, imageName) 207 } 208 209 // Wait on a snapshot import task, post-process the snapshot (e.g. adding 210 // tags), and return a Snapshot. 211 func (a *API) finishSnapshotTask(snapshotTaskID, imageName string) (*Snapshot, error) { 212 snapshotDone := func(snapshotTaskID string) (bool, string, error) { 213 taskRes, err := a.ec2.DescribeImportSnapshotTasks(&ec2.DescribeImportSnapshotTasksInput{ 214 ImportTaskIds: []*string{aws.String(snapshotTaskID)}, 215 }) 216 if err != nil { 217 return false, "", err 218 } 219 220 details := taskRes.ImportSnapshotTasks[0].SnapshotTaskDetail 221 222 // I dream of AWS specifying this as an enum shape, not string 223 switch *details.Status { 224 case "completed": 225 return true, *details.SnapshotId, nil 226 case "pending", "active": 227 plog.Debugf("waiting for import task: %v (%v): %v", *details.Status, *details.Progress, *details.StatusMessage) 228 return false, "", nil 229 case "cancelled", "cancelling": 230 return false, "", fmt.Errorf("import task cancelled") 231 case "deleted", "deleting": 232 errMsg := "unknown error occured importing snapshot" 233 if details.StatusMessage != nil { 234 errMsg = *details.StatusMessage 235 } 236 return false, "", fmt.Errorf("could not import snapshot: %v", errMsg) 237 default: 238 return false, "", fmt.Errorf("unexpected status: %v", *details.Status) 239 } 240 } 241 242 // TODO(euank): write a waiter for import snapshot 243 var snapshotID string 244 for { 245 var done bool 246 var err error 247 done, snapshotID, err = snapshotDone(snapshotTaskID) 248 if err != nil { 249 return nil, err 250 } 251 if done { 252 break 253 } 254 time.Sleep(20 * time.Second) 255 } 256 257 // post-process 258 err := a.CreateTags([]string{snapshotID}, map[string]string{ 259 "Name": imageName, 260 }) 261 if err != nil { 262 return nil, fmt.Errorf("couldn't create tags: %v", err) 263 } 264 265 return &Snapshot{ 266 SnapshotID: snapshotID, 267 }, nil 268 } 269 270 func (a *API) CreateImportRole(bucket string) error { 271 iamc := iam.New(a.session) 272 _, err := iamc.GetRole(&iam.GetRoleInput{ 273 RoleName: &vmImportRole, 274 }) 275 if err != nil { 276 if awserr, ok := err.(awserr.Error); ok && awserr.Code() == "NoSuchEntity" { 277 // Role does not exist, let's try to create it 278 _, err := iamc.CreateRole(&iam.CreateRoleInput{ 279 RoleName: &vmImportRole, 280 AssumeRolePolicyDocument: aws.String(`{ 281 "Version": "2012-10-17", 282 "Statement": [{ 283 "Effect": "Allow", 284 "Condition": { 285 "StringEquals": { 286 "sts:ExternalId": "vmimport" 287 } 288 }, 289 "Action": "sts:AssumeRole", 290 "Principal": { 291 "Service": "vmie.amazonaws.com" 292 } 293 }] 294 }`), 295 }) 296 if err != nil { 297 return fmt.Errorf("coull not create vmimport role: %v", err) 298 } 299 } 300 } 301 302 // by convention, name our policies after the bucket so we can identify 303 // whether a regional bucket is covered by a policy without parsing the 304 // policy-doc json 305 policyName := bucket 306 _, err = iamc.GetRolePolicy(&iam.GetRolePolicyInput{ 307 RoleName: &vmImportRole, 308 PolicyName: &policyName, 309 }) 310 if err != nil { 311 if awserr, ok := err.(awserr.Error); ok && awserr.Code() == "NoSuchEntity" { 312 // Policy does not exist, let's try to create it 313 partition, ok := endpoints.PartitionForRegion(endpoints.DefaultPartitions(), a.opts.Region) 314 if !ok { 315 return fmt.Errorf("could not find partition for %v out of partitions %v", a.opts.Region, endpoints.DefaultPartitions()) 316 } 317 _, err := iamc.PutRolePolicy(&iam.PutRolePolicyInput{ 318 RoleName: &vmImportRole, 319 PolicyName: &policyName, 320 PolicyDocument: aws.String((`{ 321 "Version": "2012-10-17", 322 "Statement": [{ 323 "Effect": "Allow", 324 "Action": [ 325 "s3:ListBucket", 326 "s3:GetBucketLocation", 327 "s3:GetObject" 328 ], 329 "Resource": [ 330 "arn:` + partition.ID() + `:s3:::` + bucket + `", 331 "arn:` + partition.ID() + `:s3:::` + bucket + `/*" 332 ] 333 }, 334 { 335 "Effect": "Allow", 336 "Action": [ 337 "ec2:ModifySnapshotAttribute", 338 "ec2:CopySnapshot", 339 "ec2:RegisterImage", 340 "ec2:Describe*" 341 ], 342 "Resource": "*" 343 }] 344 }`)), 345 }) 346 if err != nil { 347 return fmt.Errorf("could not create role policy: %v", err) 348 } 349 } else { 350 return err 351 } 352 } 353 354 return nil 355 } 356 357 func (a *API) CreateHVMImage(snapshotID string, diskSizeGiB uint, name string, description string) (string, error) { 358 params := registerImageParams(snapshotID, diskSizeGiB, name, description, "xvd", EC2ImageTypeHVM) 359 params.EnaSupport = aws.Bool(true) 360 params.SriovNetSupport = aws.String("simple") 361 return a.createImage(params) 362 } 363 364 func (a *API) CreatePVImage(snapshotID string, diskSizeGiB uint, name string, description string) (string, error) { 365 if !RegionSupportsPV(a.opts.Region) { 366 return "", NoRegionPVSupport 367 } 368 params := registerImageParams(snapshotID, diskSizeGiB, name, description, "sd", EC2ImageTypePV) 369 params.KernelId = aws.String(akis[a.opts.Region]) 370 return a.createImage(params) 371 } 372 373 func (a *API) createImage(params *ec2.RegisterImageInput) (string, error) { 374 res, err := a.ec2.RegisterImage(params) 375 376 var imageID string 377 if err == nil { 378 imageID = *res.ImageId 379 } else if awserr, ok := err.(awserr.Error); ok && awserr.Code() == "InvalidAMIName.Duplicate" { 380 // The AMI already exists. Get its ID. Due to races, this 381 // may take several attempts. 382 for { 383 imageID, err = a.FindImage(*params.Name) 384 if err != nil { 385 return "", err 386 } 387 if imageID != "" { 388 plog.Infof("found existing image %v, reusing", imageID) 389 break 390 } 391 plog.Debugf("failed to locate image %q, retrying...", *params.Name) 392 time.Sleep(10 * time.Second) 393 } 394 } else { 395 return "", fmt.Errorf("error creating AMI: %v", err) 396 } 397 398 // We do this even in the already-exists path in case the previous 399 // run was interrupted. 400 err = a.CreateTags([]string{imageID}, map[string]string{ 401 "Name": *params.Name, 402 }) 403 if err != nil { 404 return "", fmt.Errorf("couldn't tag image name: %v", err) 405 } 406 407 return imageID, nil 408 } 409 410 func registerImageParams(snapshotID string, diskSizeGiB uint, name, description string, diskBaseName string, imageType EC2ImageType) *ec2.RegisterImageInput { 411 return &ec2.RegisterImageInput{ 412 Name: aws.String(name), 413 Description: aws.String(description), 414 Architecture: aws.String("x86_64"), 415 VirtualizationType: aws.String(string(imageType)), 416 RootDeviceName: aws.String(fmt.Sprintf("/dev/%sa", diskBaseName)), 417 BlockDeviceMappings: []*ec2.BlockDeviceMapping{ 418 &ec2.BlockDeviceMapping{ 419 DeviceName: aws.String(fmt.Sprintf("/dev/%sa", diskBaseName)), 420 Ebs: &ec2.EbsBlockDevice{ 421 SnapshotId: aws.String(snapshotID), 422 DeleteOnTermination: aws.Bool(true), 423 VolumeSize: aws.Int64(int64(diskSizeGiB)), 424 VolumeType: aws.String("gp2"), 425 }, 426 }, 427 &ec2.BlockDeviceMapping{ 428 DeviceName: aws.String(fmt.Sprintf("/dev/%sb", diskBaseName)), 429 VirtualName: aws.String("ephemeral0"), 430 }, 431 }, 432 } 433 } 434 435 func (a *API) GrantLaunchPermission(imageID string, userIDs []string) error { 436 arg := &ec2.ModifyImageAttributeInput{ 437 Attribute: aws.String("launchPermission"), 438 ImageId: aws.String(imageID), 439 LaunchPermission: &ec2.LaunchPermissionModifications{}, 440 } 441 for _, userID := range userIDs { 442 arg.LaunchPermission.Add = append(arg.LaunchPermission.Add, &ec2.LaunchPermission{ 443 UserId: aws.String(userID), 444 }) 445 } 446 _, err := a.ec2.ModifyImageAttribute(arg) 447 if err != nil { 448 return fmt.Errorf("couldn't grant launch permission: %v", err) 449 } 450 return nil 451 } 452 453 func (a *API) CopyImage(sourceImageID string, regions []string) (map[string]string, error) { 454 type result struct { 455 region string 456 imageID string 457 err error 458 } 459 460 image, err := a.describeImage(sourceImageID) 461 if err != nil { 462 return nil, err 463 } 464 465 if *image.VirtualizationType == ec2.VirtualizationTypeParavirtual { 466 for _, region := range regions { 467 if !RegionSupportsPV(region) { 468 return nil, NoRegionPVSupport 469 } 470 } 471 } 472 473 snapshotID, err := getImageSnapshotID(image) 474 if err != nil { 475 return nil, err 476 } 477 describeSnapshotRes, err := a.ec2.DescribeSnapshots(&ec2.DescribeSnapshotsInput{ 478 SnapshotIds: []*string{&snapshotID}, 479 }) 480 if err != nil { 481 return nil, fmt.Errorf("couldn't describe snapshot: %v", err) 482 } 483 snapshot := describeSnapshotRes.Snapshots[0] 484 485 describeAttributeRes, err := a.ec2.DescribeImageAttribute(&ec2.DescribeImageAttributeInput{ 486 Attribute: aws.String("launchPermission"), 487 ImageId: aws.String(sourceImageID), 488 }) 489 if err != nil { 490 return nil, fmt.Errorf("couldn't describe launch permissions: %v", err) 491 } 492 launchPermissions := describeAttributeRes.LaunchPermissions 493 494 var wg sync.WaitGroup 495 ch := make(chan result, len(regions)) 496 for _, region := range regions { 497 opts := *a.opts 498 opts.Region = region 499 aa, err := New(&opts) 500 if err != nil { 501 break 502 } 503 wg.Add(1) 504 go func() { 505 defer wg.Done() 506 res := result{region: aa.opts.Region} 507 res.imageID, res.err = aa.copyImageIn(a.opts.Region, sourceImageID, 508 *image.Name, *image.Description, 509 image.Tags, snapshot.Tags, 510 launchPermissions) 511 ch <- res 512 }() 513 } 514 wg.Wait() 515 close(ch) 516 517 amis := make(map[string]string) 518 for res := range ch { 519 if res.imageID != "" { 520 amis[res.region] = res.imageID 521 } 522 if err == nil { 523 err = res.err 524 } 525 } 526 return amis, err 527 } 528 529 func (a *API) copyImageIn(sourceRegion, sourceImageID, name, description string, imageTags, snapshotTags []*ec2.Tag, launchPermissions []*ec2.LaunchPermission) (string, error) { 530 imageID, err := a.FindImage(name) 531 if err != nil { 532 return "", err 533 } 534 535 if imageID == "" { 536 copyRes, err := a.ec2.CopyImage(&ec2.CopyImageInput{ 537 SourceRegion: aws.String(sourceRegion), 538 SourceImageId: aws.String(sourceImageID), 539 Name: aws.String(name), 540 Description: aws.String(description), 541 }) 542 if err != nil { 543 return "", fmt.Errorf("couldn't initiate image copy to %v: %v", a.opts.Region, err) 544 } 545 imageID = *copyRes.ImageId 546 } 547 548 // The 10-minute default timeout is not enough. Wait up to 30 minutes. 549 err = a.ec2.WaitUntilImageAvailableWithContext(aws.BackgroundContext(), &ec2.DescribeImagesInput{ 550 ImageIds: aws.StringSlice([]string{imageID}), 551 }, func(w *request.Waiter) { 552 w.MaxAttempts = 60 553 w.Delay = request.ConstantWaiterDelay(30 * time.Second) 554 }) 555 if err != nil { 556 return "", fmt.Errorf("couldn't copy image to %v: %v", a.opts.Region, err) 557 } 558 559 if len(imageTags) > 0 { 560 _, err = a.ec2.CreateTags(&ec2.CreateTagsInput{ 561 Resources: aws.StringSlice([]string{imageID}), 562 Tags: imageTags, 563 }) 564 if err != nil { 565 return "", fmt.Errorf("couldn't create image tags: %v", err) 566 } 567 } 568 569 if len(snapshotTags) > 0 { 570 image, err := a.describeImage(imageID) 571 if err != nil { 572 return "", err 573 } 574 _, err = a.ec2.CreateTags(&ec2.CreateTagsInput{ 575 Resources: []*string{image.BlockDeviceMappings[0].Ebs.SnapshotId}, 576 Tags: snapshotTags, 577 }) 578 if err != nil { 579 return "", fmt.Errorf("couldn't create snapshot tags: %v", err) 580 } 581 } 582 583 if len(launchPermissions) > 0 { 584 _, err = a.ec2.ModifyImageAttribute(&ec2.ModifyImageAttributeInput{ 585 Attribute: aws.String("launchPermission"), 586 ImageId: aws.String(imageID), 587 LaunchPermission: &ec2.LaunchPermissionModifications{ 588 Add: launchPermissions, 589 }, 590 }) 591 if err != nil { 592 return "", fmt.Errorf("couldn't grant launch permissions: %v", err) 593 } 594 } 595 596 // The AMI created by CopyImage doesn't immediately appear in 597 // DescribeImagesOutput, and CopyImage doesn't enforce the 598 // constraint that multiple images cannot have the same name. 599 // As a result we could have created a duplicate image after 600 // losing a race with a CopyImage task created by a previous run. 601 // Don't try to clean this up automatically for now, but at least 602 // detect it so plume pre-release doesn't leave any surprises for 603 // plume release. 604 _, err = a.FindImage(name) 605 if err != nil { 606 return "", fmt.Errorf("checking for duplicate images: %v", err) 607 } 608 609 return imageID, nil 610 } 611 612 // Find an image we own with the specified name. Return ID or "". 613 func (a *API) FindImage(name string) (string, error) { 614 describeRes, err := a.ec2.DescribeImages(&ec2.DescribeImagesInput{ 615 Filters: []*ec2.Filter{ 616 &ec2.Filter{ 617 Name: aws.String("name"), 618 Values: aws.StringSlice([]string{name}), 619 }, 620 }, 621 Owners: aws.StringSlice([]string{"self"}), 622 }) 623 if err != nil { 624 return "", fmt.Errorf("couldn't describe images: %v", err) 625 } 626 if len(describeRes.Images) > 1 { 627 return "", fmt.Errorf("found multiple images with name %v. DescribeImage output: %v", name, describeRes.Images) 628 } 629 if len(describeRes.Images) == 1 { 630 return *describeRes.Images[0].ImageId, nil 631 } 632 return "", nil 633 } 634 635 func (a *API) describeImage(imageID string) (*ec2.Image, error) { 636 describeRes, err := a.ec2.DescribeImages(&ec2.DescribeImagesInput{ 637 ImageIds: aws.StringSlice([]string{imageID}), 638 }) 639 if err != nil { 640 return nil, fmt.Errorf("couldn't describe image: %v", err) 641 } 642 return describeRes.Images[0], nil 643 } 644 645 // Grant everyone launch permission on the specified image and create-volume 646 // permission on its underlying snapshot. 647 func (a *API) PublishImage(imageID string) error { 648 // snapshot create-volume permission 649 image, err := a.describeImage(imageID) 650 if err != nil { 651 return err 652 } 653 snapshotID, err := getImageSnapshotID(image) 654 if err != nil { 655 return err 656 } 657 _, err = a.ec2.ModifySnapshotAttribute(&ec2.ModifySnapshotAttributeInput{ 658 Attribute: aws.String("createVolumePermission"), 659 SnapshotId: &snapshotID, 660 CreateVolumePermission: &ec2.CreateVolumePermissionModifications{ 661 Add: []*ec2.CreateVolumePermission{ 662 &ec2.CreateVolumePermission{ 663 Group: aws.String("all"), 664 }, 665 }, 666 }, 667 }) 668 if err != nil { 669 return fmt.Errorf("couldn't grant create volume permission on %v: %v", snapshotID, err) 670 } 671 672 // image launch permission 673 _, err = a.ec2.ModifyImageAttribute(&ec2.ModifyImageAttributeInput{ 674 Attribute: aws.String("launchPermission"), 675 ImageId: aws.String(imageID), 676 LaunchPermission: &ec2.LaunchPermissionModifications{ 677 Add: []*ec2.LaunchPermission{ 678 &ec2.LaunchPermission{ 679 Group: aws.String("all"), 680 }, 681 }, 682 }, 683 }) 684 if err != nil { 685 return fmt.Errorf("couldn't grant launch permission on %v: %v", imageID, err) 686 } 687 688 return nil 689 } 690 691 func getImageSnapshotID(image *ec2.Image) (string, error) { 692 // The EBS volume is usually listed before the ephemeral volume, but 693 // not always, e.g. ami-fddb0490 or ami-8cd40ce1 in cn-north-1 694 for _, mapping := range image.BlockDeviceMappings { 695 if mapping.Ebs != nil { 696 return *mapping.Ebs.SnapshotId, nil 697 } 698 } 699 // We observed a case where a returned `image` didn't have a block 700 // device mapping. Hopefully retrying this a couple times will work 701 // and it's just a sorta eventual consistency thing 702 return "", fmt.Errorf("no backing block device for %v", image.ImageId) 703 }