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  }