github.com/cloud-foundations/dominator@v0.0.0-20221004181915-6e4fee580046/imagepublishers/amipublisher/libAws.go (about)

     1  package amipublisher
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"path"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/Cloud-Foundations/Dominator/lib/awsutil"
    11  	"github.com/Cloud-Foundations/Dominator/lib/log"
    12  	libtags "github.com/Cloud-Foundations/Dominator/lib/tags"
    13  	"github.com/aws/aws-sdk-go/aws"
    14  	"github.com/aws/aws-sdk-go/service/ec2"
    15  	"github.com/aws/aws-sdk-go/service/s3"
    16  )
    17  
    18  const (
    19  	creationTimeFormat = "2006-01-02T15:04:05.000Z"
    20  	rootDevName        = "/dev/sda1"
    21  )
    22  
    23  var (
    24  	amiNameReplacer = strings.NewReplacer(
    25  		"@", "@@",
    26  		"~", "@tilde@",
    27  		"!", "@bang@",
    28  		"#", "@hash@",
    29  		"$", "@dollar@",
    30  		"%", "@percent@",
    31  		"^", "@caret@",
    32  		"&", "@ampersand@",
    33  		"*", "@asterix@", // the gaul.
    34  		"=", "@equal@",
    35  		"+", "@plus@",
    36  		":", ".",
    37  	)
    38  )
    39  
    40  func attachVolume(awsService *ec2.EC2, instance *ec2.Instance, volumeId string,
    41  	logger log.Logger) error {
    42  	usedBlockDevices := make(map[string]struct{})
    43  	for _, device := range instance.BlockDeviceMappings {
    44  		usedBlockDevices[aws.StringValue(device.DeviceName)] = struct{}{}
    45  	}
    46  	blockDeviceName := getFreeVolumeName(usedBlockDevices)
    47  	if blockDeviceName == "" {
    48  		return errors.New("no space for new block device")
    49  	}
    50  	_, err := awsService.AttachVolume(&ec2.AttachVolumeInput{
    51  		Device:     aws.String(blockDeviceName),
    52  		InstanceId: instance.InstanceId,
    53  		VolumeId:   aws.String(volumeId),
    54  	})
    55  	if err != nil {
    56  		return fmt.Errorf("ec2.AttachVolume: %s", err)
    57  	}
    58  	blockDevMappings := make([]*ec2.InstanceBlockDeviceMappingSpecification, 1)
    59  	blockDevMappings[0] = &ec2.InstanceBlockDeviceMappingSpecification{
    60  		DeviceName: aws.String(blockDeviceName),
    61  		Ebs: &ec2.EbsInstanceBlockDeviceSpecification{
    62  			DeleteOnTermination: aws.Bool(true),
    63  			VolumeId:            aws.String(volumeId),
    64  		},
    65  	}
    66  	_, err = awsService.ModifyInstanceAttribute(
    67  		&ec2.ModifyInstanceAttributeInput{
    68  			BlockDeviceMappings: blockDevMappings,
    69  			InstanceId:          instance.InstanceId,
    70  		})
    71  	if err != nil {
    72  		return fmt.Errorf("ec2.ModifyInstanceAttribute: %s", err)
    73  	}
    74  	logger.Printf("requested attach(%s): %s on %s, waiting...\n",
    75  		aws.StringValue(instance.InstanceId), volumeId, blockDeviceName)
    76  	dvi := &ec2.DescribeVolumesInput{
    77  		VolumeIds: aws.StringSlice([]string{volumeId}),
    78  	}
    79  	if err := awsService.WaitUntilVolumeInUse(dvi); err != nil {
    80  		return fmt.Errorf("ec2.WaitUntilVolumeInUse: %s", err)
    81  	}
    82  	for ; true; time.Sleep(time.Second) {
    83  		desc, err := awsService.DescribeVolumes(dvi)
    84  		if err != nil {
    85  			return fmt.Errorf("ec2.DescribeVolumes: %s", err)
    86  		}
    87  		if len(desc.Volumes[0].Attachments) < 1 {
    88  			logger.Printf("attachments: %d\n",
    89  				len(desc.Volumes[0].Attachments))
    90  			continue
    91  		}
    92  		state := *desc.Volumes[0].Attachments[0].State
    93  		logger.Printf("state: \"%s\"\n", state)
    94  		if state == ec2.VolumeAttachmentStateAttached {
    95  			break
    96  		}
    97  	}
    98  	logger.Printf("attached: %s\n", volumeId)
    99  	return nil
   100  }
   101  
   102  // computeImageConsumption returns the size in GiB.
   103  func computeImageConsumption(image *ec2.Image) int64 {
   104  	var sizeGiB int64
   105  	for _, bdMapping := range image.BlockDeviceMappings {
   106  		// TODO(rgooch): Should use the snapshot size instead.
   107  		if ebs := bdMapping.Ebs; ebs != nil {
   108  			sizeGiB += aws.Int64Value(ebs.VolumeSize)
   109  		}
   110  	}
   111  	// TODO(rgooch): Should query the S3 folder instead.
   112  	if sizeGiB < 1 {
   113  		sizeGiB = 1
   114  	}
   115  	return sizeGiB
   116  }
   117  
   118  func createSnapshot(awsService *ec2.EC2, volumeId string, description string,
   119  	tags libtags.Tags, logger log.Logger) (
   120  	string, error) {
   121  	snapshot, err := awsService.CreateSnapshot(&ec2.CreateSnapshotInput{
   122  		VolumeId:    aws.String(volumeId),
   123  		Description: aws.String(description),
   124  	})
   125  	if err != nil {
   126  		return "", fmt.Errorf("ec2.CreateSnapshot: %s", err)
   127  	}
   128  	snapshotIds := make([]string, 1)
   129  	snapshotIds[0] = *snapshot.SnapshotId
   130  	logger.Printf("Created: %s\n", *snapshot.SnapshotId)
   131  	tags = tags.Copy()
   132  	tags["Name"] = description
   133  	if err := createTags(awsService, *snapshot.SnapshotId, tags); err != nil {
   134  		return "", err
   135  	}
   136  	logger.Printf("Tagged: %s, waiting...\n", *snapshot.SnapshotId)
   137  	err = awsService.WaitUntilSnapshotCompleted(&ec2.DescribeSnapshotsInput{
   138  		SnapshotIds: aws.StringSlice(snapshotIds),
   139  	})
   140  	if err != nil {
   141  		return "", fmt.Errorf("ec2.WaitUntilSnapshotCompleted: %s", err)
   142  	}
   143  	return *snapshot.SnapshotId, nil
   144  }
   145  
   146  func createTags(awsService *ec2.EC2, resourceId string,
   147  	tags map[string]string) error {
   148  	resourceIds := make([]string, 1)
   149  	resourceIds[0] = resourceId
   150  	awsTags := make([]*ec2.Tag, 0, len(tags))
   151  	for key, value := range tags {
   152  		awsTags = append(awsTags,
   153  			&ec2.Tag{Key: aws.String(key), Value: aws.String(value)})
   154  	}
   155  	_, err := awsService.CreateTags(&ec2.CreateTagsInput{
   156  		Resources: aws.StringSlice(resourceIds),
   157  		Tags:      awsTags,
   158  	})
   159  	if err != nil {
   160  		return fmt.Errorf("ec2.CreateTags: %s", err)
   161  	}
   162  	return nil
   163  }
   164  
   165  func createTagSpecification(resourceType string,
   166  	tags libtags.Tags) *ec2.TagSpecification {
   167  	if tags == nil {
   168  		return nil
   169  	}
   170  	awsTags := make([]*ec2.Tag, 0, len(tags))
   171  	for key, value := range tags {
   172  		awsTags = append(awsTags,
   173  			&ec2.Tag{Key: aws.String(key), Value: aws.String(value)})
   174  	}
   175  	return &ec2.TagSpecification{
   176  		ResourceType: aws.String(resourceType),
   177  		Tags:         awsTags,
   178  	}
   179  }
   180  
   181  func createVolume(awsService *ec2.EC2, availabilityZone *string, size uint64,
   182  	tags libtags.Tags, logger log.Logger) (string, error) {
   183  	tags = tags.Copy()
   184  	delete(tags, "ExpiresAt")
   185  	tags["Name"] = "image unpacker"
   186  	sizeInGiB := int64(size) >> 30
   187  	if sizeInGiB<<30 < int64(size) {
   188  		sizeInGiB++
   189  	}
   190  	volume, err := awsService.CreateVolume(&ec2.CreateVolumeInput{
   191  		AvailabilityZone: availabilityZone,
   192  		Encrypted:        aws.Bool(true),
   193  		Size:             aws.Int64(sizeInGiB),
   194  		TagSpecifications: []*ec2.TagSpecification{
   195  			createTagSpecification(ec2.ResourceTypeVolume, tags),
   196  		},
   197  		VolumeType: aws.String("gp2"),
   198  	})
   199  	if err != nil {
   200  		return "", fmt.Errorf("ec2.CreateVolume: %s", err)
   201  	}
   202  	volumeIds := make([]string, 1)
   203  	volumeIds[0] = *volume.VolumeId
   204  	logger.Printf("Created: %s, waiting...\n", *volume.VolumeId)
   205  	err = awsService.WaitUntilVolumeAvailable(&ec2.DescribeVolumesInput{
   206  		VolumeIds: aws.StringSlice(volumeIds),
   207  	})
   208  	if err != nil {
   209  		return "", fmt.Errorf("ec2.WaitUntilVolumeAvailable: %s", err)
   210  	}
   211  	return *volume.VolumeId, nil
   212  }
   213  
   214  func deleteImage(cs *awsutil.CredentialsStore, accountName, region string,
   215  	image *ec2.Image) error {
   216  	ec2Service := cs.GetEC2Service(accountName, region)
   217  	s3Service := s3.New(cs.GetSessionForAccount(accountName),
   218  		&aws.Config{Region: aws.String(region)})
   219  	imageId := aws.StringValue(image.ImageId)
   220  	if err := deregisterAmi(ec2Service, imageId); err != nil {
   221  		return errors.New(imageId + ": " + err.Error())
   222  	}
   223  	switch rootDevType := aws.StringValue(image.RootDeviceType); rootDevType {
   224  	case "ebs":
   225  		var firstError error
   226  		for _, bdMapping := range image.BlockDeviceMappings {
   227  			if bdMapping.Ebs == nil {
   228  				return nil
   229  			}
   230  			snapshotId := aws.StringValue(bdMapping.Ebs.SnapshotId)
   231  			if err := deleteSnapshot(ec2Service, snapshotId); err != nil {
   232  				if firstError == nil {
   233  					firstError = errors.New(snapshotId + ": " + err.Error())
   234  				}
   235  			}
   236  		}
   237  		if firstError != nil {
   238  			return firstError
   239  		}
   240  	case "instance-store":
   241  		splitPath := strings.Split(aws.StringValue(image.ImageLocation), "/")
   242  		bucket := splitPath[0]
   243  		folder := strings.Join(splitPath[1:len(splitPath)-1], "/")
   244  		return deleteS3Directory(s3Service, bucket, folder)
   245  	default:
   246  		return errors.New("unsupported root device type: " + rootDevType +
   247  			" for: " + imageId)
   248  	}
   249  	return nil
   250  }
   251  
   252  func deleteS3Directory(awsService *s3.S3, bucket, dir string) error {
   253  	out, err := awsService.ListObjects(&s3.ListObjectsInput{
   254  		Bucket: aws.String(bucket),
   255  		Prefix: aws.String(dir),
   256  	})
   257  	if err != nil {
   258  		fmt.Printf("error listing: %s: %s\n", dir, err)
   259  		return fmt.Errorf("s3.ListObjects: %s", err)
   260  	}
   261  	if len(out.Contents) < 1 {
   262  		return nil
   263  	}
   264  	objectIds := make([]*s3.ObjectIdentifier, 0, len(out.Contents))
   265  	for _, obj := range out.Contents {
   266  		objectIds = append(objectIds,
   267  			&s3.ObjectIdentifier{Key: obj.Key})
   268  	}
   269  	_, err = awsService.DeleteObjects(&s3.DeleteObjectsInput{
   270  		Bucket: aws.String(bucket),
   271  		Delete: &s3.Delete{Objects: objectIds},
   272  	})
   273  	if err != nil {
   274  		fmt.Printf("error deleting objects: %s\n", err)
   275  	}
   276  	return nil
   277  }
   278  
   279  func deleteSnapshot(awsService *ec2.EC2, snapshotId string) error {
   280  	var err error
   281  	for i := 0; i < 60; i++ {
   282  		_, err = awsService.DeleteSnapshot(&ec2.DeleteSnapshotInput{
   283  			SnapshotId: aws.String(snapshotId),
   284  		})
   285  		if err == nil {
   286  			return nil
   287  		}
   288  		if !strings.Contains(err.Error(), "in use by ami") &&
   289  			!strings.Contains(err.Error(), "RequestLimitExceeded") {
   290  			return err
   291  		}
   292  		time.Sleep(time.Second)
   293  	}
   294  	if strings.Contains(err.Error(), "in use by ami") {
   295  		return errors.New("timed out waiting for delete: " + snapshotId)
   296  	}
   297  	return err
   298  }
   299  
   300  func deleteTagsFromResources(awsService *ec2.EC2, tagKeys []string,
   301  	resourceId ...string) error {
   302  	if len(tagKeys) < 1 {
   303  		return nil
   304  	}
   305  	resourceIds := make([]string, 0)
   306  	for _, id := range resourceId {
   307  		if id != "" {
   308  			resourceIds = append(resourceIds, id)
   309  		}
   310  	}
   311  	if len(resourceIds) < 1 {
   312  		return nil
   313  	}
   314  	tags := make([]*ec2.Tag, 0, len(tagKeys))
   315  	for _, tagKey := range tagKeys {
   316  		tags = append(tags, &ec2.Tag{Key: aws.String(tagKey)})
   317  	}
   318  	_, err := awsService.DeleteTags(&ec2.DeleteTagsInput{
   319  		Resources: aws.StringSlice(resourceIds),
   320  		Tags:      tags,
   321  	})
   322  	if err != nil {
   323  		return fmt.Errorf("ec2.DeleteTags: %s", err)
   324  	}
   325  	return nil
   326  }
   327  
   328  func deleteVolume(awsService *ec2.EC2, volumeId string) error {
   329  	_, err := awsService.DeleteVolume(&ec2.DeleteVolumeInput{
   330  		VolumeId: aws.String(volumeId),
   331  	})
   332  	if err != nil {
   333  		return fmt.Errorf("ec2.DeleteVolume: %s", err)
   334  	}
   335  	return nil
   336  }
   337  
   338  func detachVolume(awsService *ec2.EC2, instanceId, volumeId string) error {
   339  	_, err := awsService.DetachVolume(&ec2.DetachVolumeInput{
   340  		InstanceId: aws.String(instanceId),
   341  		VolumeId:   aws.String(volumeId),
   342  	})
   343  	if err != nil {
   344  		return fmt.Errorf("ec2.DetachVolume: %s", err)
   345  	}
   346  	err = awsService.WaitUntilVolumeAvailable(&ec2.DescribeVolumesInput{
   347  		VolumeIds: aws.StringSlice([]string{volumeId}),
   348  	})
   349  	if err != nil {
   350  		return fmt.Errorf("ec2.WaitUntilVolumeAvailable: %s", err)
   351  	}
   352  	return nil
   353  }
   354  
   355  func deregisterAmi(awsService *ec2.EC2, amiId string) error {
   356  	var err error
   357  	for i := 0; i < 6; i++ {
   358  		_, err = awsService.DeregisterImage(&ec2.DeregisterImageInput{
   359  			ImageId: aws.String(amiId),
   360  		})
   361  		if err == nil {
   362  			break
   363  		}
   364  		if !strings.Contains(err.Error(), "RequestLimitExceeded") {
   365  			return err
   366  		}
   367  		time.Sleep(time.Second * 11)
   368  	}
   369  	if err != nil {
   370  		return errors.New("timed out waiting for delete: " + amiId)
   371  	}
   372  	imageIds := make([]*string, 1)
   373  	imageIds[0] = aws.String(amiId)
   374  	for i := 0; i < 60; i++ {
   375  		out, err := awsService.DescribeImages(&ec2.DescribeImagesInput{
   376  			ImageIds: imageIds,
   377  		})
   378  		if err != nil {
   379  			return fmt.Errorf("ec2.DescribeImages: %s", err)
   380  		}
   381  		if len(out.Images) < 1 {
   382  			return nil
   383  		}
   384  		time.Sleep(time.Second)
   385  	}
   386  	return errors.New("timed out waiting for deregister: " + amiId)
   387  }
   388  
   389  func describeInstances(awsService *ec2.EC2, input *ec2.DescribeInstancesInput) (
   390  	[]*ec2.Instance, error) {
   391  	if input == nil {
   392  		input = &ec2.DescribeInstancesInput{}
   393  	}
   394  	inp := *input
   395  	inp.NextToken = nil
   396  	instances := make([]*ec2.Instance, 0)
   397  	for {
   398  		out, err := awsService.DescribeInstances(&inp)
   399  		if err != nil {
   400  			return nil, fmt.Errorf("ec2.DescribeInstances: %s", err)
   401  		}
   402  		for _, reservation := range out.Reservations {
   403  			for _, instance := range reservation.Instances {
   404  				instances = append(instances, instance)
   405  			}
   406  		}
   407  		if out.NextToken == nil {
   408  			break
   409  		}
   410  		inp.NextToken = out.NextToken
   411  	}
   412  	return instances, nil
   413  }
   414  
   415  func findImage(awsService *ec2.EC2, tags libtags.Tags) (*ec2.Image, error) {
   416  	images, err := getImages(awsService, "", tags)
   417  	if err != nil {
   418  		return nil, err
   419  	}
   420  	return findLatestImage(images)
   421  }
   422  
   423  func findMarketplaceImage(awsService *ec2.EC2, productCode string) (
   424  	*ec2.Image, error) {
   425  	out, err := awsService.DescribeImages(
   426  		&ec2.DescribeImagesInput{
   427  			Filters: []*ec2.Filter{
   428  				{
   429  					Name:   aws.String("product-code"),
   430  					Values: aws.StringSlice([]string{productCode}),
   431  				},
   432  				{
   433  					Name:   aws.String("product-code.type"),
   434  					Values: aws.StringSlice([]string{"marketplace"}),
   435  				},
   436  			},
   437  		})
   438  	if err != nil {
   439  		return nil, fmt.Errorf("ec2.DescribeImages: %s", err)
   440  	}
   441  	return findLatestImage(out.Images)
   442  }
   443  
   444  func findLatestImage(images []*ec2.Image) (*ec2.Image, error) {
   445  	var youngestImage *ec2.Image
   446  	var youngestTime time.Time
   447  	for _, image := range images {
   448  		creationTime, err := time.Parse(creationTimeFormat,
   449  			aws.StringValue(image.CreationDate))
   450  		if err != nil {
   451  			return nil, err
   452  		}
   453  		if creationTime.After(youngestTime) {
   454  			youngestImage = image
   455  			youngestTime = creationTime
   456  		}
   457  	}
   458  	return youngestImage, nil
   459  }
   460  
   461  func getAccountId(awsService *ec2.EC2) (string, error) {
   462  	// TODO(rgooch): This relies on at least one instance existing. This doesn't
   463  	//               work in a fresh account. Figure out a better way.
   464  	out, err := awsService.DescribeInstances(&ec2.DescribeInstancesInput{
   465  		MaxResults: aws.Int64(5),
   466  	})
   467  	if err != nil {
   468  		return "", fmt.Errorf("ec2.DescribeInstances: %s", err)
   469  	}
   470  	if len(out.Reservations) < 1 {
   471  		return "", errors.New("no instances found")
   472  	}
   473  	return aws.StringValue(out.Reservations[0].OwnerId), nil
   474  }
   475  
   476  func getFreeVolumeName(usedBlockDevices map[string]struct{}) string {
   477  	// First try the "/dev/sdb" through "/dev/sdz" range.
   478  	for c := 'b'; c <= 'z'; c++ {
   479  		name := "/dev/sd" + string(c)
   480  		if _, ok := usedBlockDevices[name]; !ok {
   481  			return name
   482  		}
   483  	}
   484  	// Now try the "/dev/xvdba" through "/dev/xvdzz" range. Note that the entire
   485  	// range may not be supported.
   486  	var dev [2]byte
   487  	dev[0] = 'b'
   488  	dev[1] = 'a'
   489  	for {
   490  		name := "/dev/xvd" + string(dev[:])
   491  		if _, ok := usedBlockDevices[name]; !ok {
   492  			return name
   493  		}
   494  		dev[1]++
   495  		if dev[1] > 'z' {
   496  			dev[1] = 'a'
   497  			dev[0]++
   498  			if dev[0] > 'z' {
   499  				return ""
   500  			}
   501  		}
   502  	}
   503  }
   504  
   505  func getImages(awsService *ec2.EC2, accountId string, tags libtags.Tags) (
   506  	[]*ec2.Image, error) {
   507  	filters := awsutil.MakeFiltersFromTags(tags)
   508  	if accountId != "" {
   509  		filters = append(filters, &ec2.Filter{
   510  			Name:   aws.String("owner-id"),
   511  			Values: aws.StringSlice([]string{accountId}),
   512  		})
   513  	}
   514  	filters = append(filters, &ec2.Filter{
   515  		Name:   aws.String("is-public"),
   516  		Values: aws.StringSlice([]string{"false"}),
   517  	})
   518  	out, err := awsService.DescribeImages(
   519  		&ec2.DescribeImagesInput{Filters: filters})
   520  	if err != nil {
   521  		return nil, fmt.Errorf("ec2.DescribeImages: %s", err)
   522  	}
   523  	return out.Images, nil
   524  }
   525  
   526  func getInstances(awsService *ec2.EC2, nameTag string) (
   527  	[]*ec2.Instance, error) {
   528  	if nameTag == "" {
   529  		return nil, errors.New("no name given")
   530  	}
   531  	states := []string{
   532  		ec2.InstanceStateNamePending,
   533  		ec2.InstanceStateNameRunning,
   534  		ec2.InstanceStateNameStopping,
   535  		ec2.InstanceStateNameStopped,
   536  	}
   537  	instances, err := describeInstances(awsService, &ec2.DescribeInstancesInput{
   538  		Filters: []*ec2.Filter{
   539  			{
   540  				Name:   aws.String("tag:Name"),
   541  				Values: aws.StringSlice([]string{nameTag}),
   542  			},
   543  			{
   544  				Name:   aws.String("instance-state-name"),
   545  				Values: aws.StringSlice(states),
   546  			},
   547  		},
   548  	})
   549  	return instances, err
   550  }
   551  
   552  func getInstanceIds(instances []*ec2.Instance) []string {
   553  	instanceIds := make([]string, 0, len(instances))
   554  	for _, instance := range instances {
   555  		instanceIds = append(instanceIds, aws.StringValue(instance.InstanceId))
   556  	}
   557  	return instanceIds
   558  }
   559  
   560  func getRunningInstance(awsService *ec2.EC2, instances []*ec2.Instance,
   561  	logger log.Logger) (*ec2.Instance, error) {
   562  	for _, instance := range instances {
   563  		if aws.StringValue(instance.State.Name) ==
   564  			ec2.InstanceStateNameRunning {
   565  			return instance, nil
   566  		}
   567  	}
   568  	var stoppedInstance *ec2.Instance
   569  	isStopped := false
   570  	for _, instance := range instances {
   571  		if stoppedInstance != nil {
   572  			break
   573  		}
   574  		switch aws.StringValue(instance.State.Name) {
   575  		case ec2.InstanceStateNameStopped:
   576  			stoppedInstance = instance
   577  			isStopped = true
   578  		case ec2.InstanceStateNamePending:
   579  			stoppedInstance = instance
   580  		}
   581  	}
   582  	if stoppedInstance == nil {
   583  		return nil, nil
   584  	}
   585  	instanceIds := make([]*string, 1)
   586  	instanceIds[0] = stoppedInstance.InstanceId
   587  	if isStopped {
   588  		logger.Printf("starting instance: %s\n",
   589  			aws.StringValue(instanceIds[0]))
   590  		_, err := awsService.StartInstances(&ec2.StartInstancesInput{
   591  			InstanceIds: instanceIds,
   592  		})
   593  		if err != nil {
   594  			return nil, fmt.Errorf("ec2.StartInstances: %s", err)
   595  		}
   596  		stoppedInstance.LaunchTime = aws.Time(time.Now())
   597  	}
   598  	logger.Printf("waiting for pending instance: %s\n",
   599  		aws.StringValue(instanceIds[0]))
   600  	err := awsService.WaitUntilInstanceRunning(&ec2.DescribeInstancesInput{
   601  		InstanceIds: instanceIds,
   602  	})
   603  	if err != nil {
   604  		return nil, fmt.Errorf("ec2.WaitUntilInstanceRunning: %s", err)
   605  	}
   606  	return stoppedInstance, nil
   607  }
   608  
   609  func getSecurityGroup(awsService *ec2.EC2, vpcId string, tags libtags.Tags) (
   610  	*ec2.SecurityGroup, error) {
   611  	filters := awsutil.MakeFiltersFromTags(tags)
   612  	filters = append(filters, &ec2.Filter{
   613  		Name:   aws.String("vpc-id"),
   614  		Values: aws.StringSlice([]string{vpcId}),
   615  	})
   616  	out, err := awsService.DescribeSecurityGroups(
   617  		&ec2.DescribeSecurityGroupsInput{Filters: filters})
   618  	if err != nil {
   619  		return nil, fmt.Errorf("ec2.DescribeSecurityGroups: %s", err)
   620  	}
   621  	if len(out.SecurityGroups) < 1 {
   622  		return nil, errors.New("no security group found")
   623  	}
   624  	if len(out.SecurityGroups) > 1 {
   625  		return nil, errors.New("too many security groups found")
   626  	}
   627  	return out.SecurityGroups[0], nil
   628  }
   629  
   630  func getSubnet(awsService *ec2.EC2, vpcId string, tags libtags.Tags) (
   631  	*ec2.Subnet, error) {
   632  	filters := awsutil.MakeFiltersFromTags(tags)
   633  	filters = append(filters, &ec2.Filter{
   634  		Name:   aws.String("vpc-id"),
   635  		Values: aws.StringSlice([]string{vpcId}),
   636  	})
   637  	out, err := awsService.DescribeSubnets(
   638  		&ec2.DescribeSubnetsInput{Filters: filters})
   639  	if err != nil {
   640  		return nil, fmt.Errorf("ec2.DescribeSubnets: %s", err)
   641  	}
   642  	if len(out.Subnets) < 1 {
   643  		return nil, errors.New("no subnets found")
   644  	}
   645  	for _, subnet := range out.Subnets {
   646  		if aws.Int64Value(subnet.AvailableIpAddressCount) > 0 {
   647  			return subnet, nil
   648  		}
   649  	}
   650  	return nil, errors.New("no subnets with available IPs found")
   651  }
   652  
   653  func getVpc(awsService *ec2.EC2, tags libtags.Tags) (*ec2.Vpc, error) {
   654  	out, err := awsService.DescribeVpcs(
   655  		&ec2.DescribeVpcsInput{Filters: awsutil.MakeFiltersFromTags(tags)})
   656  	if err != nil {
   657  		return nil, fmt.Errorf("ec2.DescribeVpcs: %s", err)
   658  	}
   659  	if len(out.Vpcs) < 1 {
   660  		return nil, errors.New("no VPC found")
   661  	}
   662  	if len(out.Vpcs) > 1 {
   663  		return nil, errors.New("too many VPCs found")
   664  	}
   665  	return out.Vpcs[0], nil
   666  }
   667  
   668  func launchInstance(awsService *ec2.EC2, image *ec2.Image, rootVolumeSize uint,
   669  	tags libtags.Tags, vpcSearchTags, subnetSearchTags,
   670  	securityGroupSearchTags libtags.Tags, instanceType string,
   671  	sshKeyName string) (*ec2.Instance, error) {
   672  	vpc, err := getVpc(awsService, vpcSearchTags)
   673  	if err != nil {
   674  		return nil, err
   675  	}
   676  	subnet, err := getSubnet(awsService, aws.StringValue(vpc.VpcId),
   677  		subnetSearchTags)
   678  	if err != nil {
   679  		return nil, err
   680  	}
   681  	sg, err := getSecurityGroup(awsService, aws.StringValue(vpc.VpcId),
   682  		securityGroupSearchTags)
   683  	if err != nil {
   684  		return nil, err
   685  	}
   686  	var blockDeviceMappings []*ec2.BlockDeviceMapping
   687  	if rootVolumeSize != 0 {
   688  		blockDeviceMappings = []*ec2.BlockDeviceMapping{{
   689  			DeviceName: aws.String(rootDevName),
   690  			Ebs: &ec2.EbsBlockDevice{
   691  				DeleteOnTermination: aws.Bool(true),
   692  				VolumeSize:          aws.Int64(int64(rootVolumeSize)),
   693  			}},
   694  		}
   695  	}
   696  	reservation, err := awsService.RunInstances(&ec2.RunInstancesInput{
   697  		BlockDeviceMappings: blockDeviceMappings,
   698  		ImageId:             image.ImageId,
   699  		InstanceType:        aws.String(instanceType),
   700  		KeyName:             aws.String(sshKeyName),
   701  		MaxCount:            aws.Int64(1),
   702  		MinCount:            aws.Int64(1),
   703  		SecurityGroupIds:    []*string{sg.GroupId},
   704  		SubnetId:            subnet.SubnetId,
   705  		TagSpecifications: []*ec2.TagSpecification{
   706  			createTagSpecification(ec2.ResourceTypeInstance, tags),
   707  			createTagSpecification(ec2.ResourceTypeVolume, tags),
   708  		},
   709  	})
   710  	if err != nil {
   711  		return nil, fmt.Errorf("ec2.RunInstances: %s", err)
   712  	}
   713  	instance := reservation.Instances[0]
   714  	err = awsService.WaitUntilInstanceExists(&ec2.DescribeInstancesInput{
   715  		InstanceIds: []*string{instance.InstanceId},
   716  	})
   717  	if err != nil {
   718  		return nil, fmt.Errorf("ec2.WaitUntilInstanceExists: %s", err)
   719  	}
   720  	return instance, nil
   721  }
   722  
   723  func registerAmi(awsService *ec2.EC2, snapshotId string, s3Manifest string,
   724  	amiName string, imageName string, tags libtags.Tags, imageGiB uint64,
   725  	publishOptions *PublishOptions, logger log.Logger) (string, error) {
   726  	blkDevMaps := make([]*ec2.BlockDeviceMapping, 1)
   727  	var volumeSize *int64
   728  	if imageGiB > 0 {
   729  		volumeSize = aws.Int64(int64(imageGiB))
   730  	}
   731  	blkDevMaps[0] = &ec2.BlockDeviceMapping{
   732  		DeviceName: aws.String(rootDevName),
   733  		Ebs: &ec2.EbsBlockDevice{
   734  			DeleteOnTermination: aws.Bool(true),
   735  			SnapshotId:          aws.String(snapshotId),
   736  			VolumeSize:          volumeSize,
   737  			VolumeType:          aws.String("gp2"),
   738  		},
   739  	}
   740  	if amiName == "" {
   741  		amiName = imageName
   742  	}
   743  	if publishOptions == nil {
   744  		publishOptions = new(PublishOptions)
   745  	}
   746  	params := &ec2.RegisterImageInput{
   747  		Architecture:       aws.String("x86_64"),
   748  		Description:        aws.String(imageName),
   749  		EnaSupport:         aws.Bool(publishOptions.EnaSupport),
   750  		Name:               aws.String(amiNameReplacer.Replace(amiName)),
   751  		RootDeviceName:     aws.String(rootDevName),
   752  		SriovNetSupport:    aws.String("simple"),
   753  		VirtualizationType: aws.String("hvm"),
   754  	}
   755  	if snapshotId != "" {
   756  		params.BlockDeviceMappings = blkDevMaps
   757  	}
   758  	if s3Manifest != "" {
   759  		params.ImageLocation = aws.String(s3Manifest)
   760  	}
   761  	ami, err := awsService.RegisterImage(params)
   762  	if err != nil {
   763  		return "", fmt.Errorf("ec2.RegisterImage: %s", err)
   764  	}
   765  	logger.Printf("Created: %s\n", *ami.ImageId)
   766  	imageIds := []string{*ami.ImageId}
   767  	err = awsService.WaitUntilImageExists(&ec2.DescribeImagesInput{
   768  		ImageIds: aws.StringSlice(imageIds),
   769  	})
   770  	if err != nil {
   771  		return "", fmt.Errorf("ec2.WaitUntilImageExists: %s", err)
   772  	}
   773  	tags = tags.Copy()
   774  	tags["Name"] = path.Dir(imageName)
   775  	if err := createTags(awsService, *ami.ImageId, tags); err != nil {
   776  		return "", err
   777  	}
   778  	logger.Printf("Tagged: %s, waiting...\n", *ami.ImageId)
   779  	err = awsService.WaitUntilImageAvailable(&ec2.DescribeImagesInput{
   780  		ImageIds: aws.StringSlice(imageIds),
   781  	})
   782  	if err != nil {
   783  		return "", fmt.Errorf("ec2.WaitUntilImageAvailable: %s", err)
   784  	}
   785  	logger.Printf("AMI: %s available\n", *ami.ImageId)
   786  	return *ami.ImageId, nil
   787  }
   788  
   789  func libStartInstances(awsService *ec2.EC2, instanceIds ...string) error {
   790  	_, err := awsService.StartInstances(&ec2.StartInstancesInput{
   791  		InstanceIds: aws.StringSlice(instanceIds),
   792  	})
   793  	if err != nil {
   794  		return fmt.Errorf("ec2.StartInstances: %s", err)
   795  	}
   796  	err = awsService.WaitUntilInstanceRunning(&ec2.DescribeInstancesInput{
   797  		InstanceIds: aws.StringSlice(instanceIds),
   798  	})
   799  	if err != nil {
   800  		return fmt.Errorf("ec2.WaitUntilInstanceRunning: %s", err)
   801  	}
   802  	return nil
   803  }
   804  
   805  func stopInstances(awsService *ec2.EC2, instanceIds ...string) error {
   806  	_, err := awsService.StopInstances(&ec2.StopInstancesInput{
   807  		InstanceIds: aws.StringSlice(instanceIds),
   808  	})
   809  	if err != nil {
   810  		return fmt.Errorf("ec2.StopInstances: %s", err)
   811  	}
   812  	return nil
   813  }
   814  
   815  func libTerminateInstances(awsService *ec2.EC2, instanceIds ...string) error {
   816  	_, err := awsService.TerminateInstances(&ec2.TerminateInstancesInput{
   817  		InstanceIds: aws.StringSlice(instanceIds),
   818  	})
   819  	if err != nil {
   820  		return fmt.Errorf("ec2.TerminateInstances: %s", err)
   821  	}
   822  	return nil
   823  }