package common import ( "context" "fmt" "sync" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2/ec2iface" "github.com/hashicorp/packer/packer-plugin-sdk/multistep" packersdk "github.com/hashicorp/packer/packer-plugin-sdk/packer" "github.com/hashicorp/packer/packer-plugin-sdk/template/config" ) type StepAMIRegionCopy struct { AccessConfig *AccessConfig Regions []string AMIKmsKeyId string RegionKeyIds map[string]string EncryptBootVolume config.Trilean // nil means preserve Name string OriginalRegion string toDelete string getRegionConn func(*AccessConfig, string) (ec2iface.EC2API, error) AMISkipBuildRegion bool } func (s *StepAMIRegionCopy) DeduplicateRegions(intermediary bool) { // Deduplicates regions by looping over the list of regions and storing // the regions as keys in a map. This saves users from accidentally copying // regions twice if they've added a region to a map twice. RegionMap := map[string]bool{} RegionSlice := []string{} // Original build region may or may not be present in the Regions list, so // let's make absolutely sure it's in our map. RegionMap[s.OriginalRegion] = true for _, r := range s.Regions { RegionMap[r] = true } if !intermediary || s.AMISkipBuildRegion { // We don't want to copy back into the original region if we aren't // using an intermediary image, so remove the original region from our // map. // We also don't want to copy back into the original region if the // intermediary image is because we're skipping the build region. delete(RegionMap, s.OriginalRegion) } // Now print all those keys into the region slice again for k := range RegionMap { RegionSlice = append(RegionSlice, k) } s.Regions = RegionSlice } func (s *StepAMIRegionCopy) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { ui := state.Get("ui").(packersdk.Ui) amis := state.Get("amis").(map[string]string) snapshots := state.Get("snapshots").(map[string][]string) intermediary, _ := state.Get("intermediary_image").(bool) s.DeduplicateRegions(intermediary) ami := amis[s.OriginalRegion] // Make a note to delete the intermediary AMI if necessary. if intermediary { s.toDelete = ami } if s.EncryptBootVolume.True() { // encrypt_boot is true, so we have to copy the temporary // AMI with required encryption setting. // temp image was created by stepCreateAMI. if s.RegionKeyIds == nil { s.RegionKeyIds = make(map[string]string) } // Make sure the kms_key_id for the original region is in the map, as // long as the AMIKmsKeyId isn't being defaulted. if s.AMIKmsKeyId != "" { if _, ok := s.RegionKeyIds[s.OriginalRegion]; !ok { s.RegionKeyIds[s.OriginalRegion] = s.AMIKmsKeyId } } else { if regionKey, ok := s.RegionKeyIds[s.OriginalRegion]; ok { s.AMIKmsKeyId = regionKey } } } if len(s.Regions) == 0 { return multistep.ActionContinue } ui.Say(fmt.Sprintf("Copying/Encrypting AMI (%s) to other regions...", ami)) var lock sync.Mutex var wg sync.WaitGroup errs := new(packersdk.MultiError) wg.Add(len(s.Regions)) for _, region := range s.Regions { var regKeyID string ui.Message(fmt.Sprintf("Copying to: %s", region)) if s.EncryptBootVolume.True() { // Encrypt is true, explicitly regKeyID = s.RegionKeyIds[region] } else { // Encrypt is nil or false; Make sure region key is empty regKeyID = "" } go func(region string) { defer wg.Done() id, snapshotIds, err := s.amiRegionCopy(ctx, state, s.AccessConfig, s.Name, ami, region, s.OriginalRegion, regKeyID, s.EncryptBootVolume.ToBoolPointer()) lock.Lock() defer lock.Unlock() amis[region] = id snapshots[region] = snapshotIds if err != nil { errs = packersdk.MultiErrorAppend(errs, err) } }(region) } // TODO(mitchellh): Wait but also allow for cancels to go through... ui.Message("Waiting for all copies to complete...") wg.Wait() // If there were errors, show them if len(errs.Errors) > 0 { state.Put("error", errs) ui.Error(errs.Error()) return multistep.ActionHalt } state.Put("amis", amis) return multistep.ActionContinue } func (s *StepAMIRegionCopy) Cleanup(state multistep.StateBag) { ec2conn := state.Get("ec2").(*ec2.EC2) ui := state.Get("ui").(packersdk.Ui) if len(s.toDelete) == 0 { return } // Delete the unencrypted amis and snapshots ui.Say("Deregistering the AMI and deleting unencrypted temporary " + "AMIs and snapshots") err := DestroyAMIs([]*string{&s.toDelete}, ec2conn) if err != nil { state.Put("error", err) ui.Error(err.Error()) return } } func getRegionConn(config *AccessConfig, target string) (ec2iface.EC2API, error) { // Connect to the region where the AMI will be copied to session, err := config.Session() if err != nil { return nil, fmt.Errorf("Error getting region connection for copy: %s", err) } regionconn := ec2.New(session.Copy(&aws.Config{ Region: aws.String(target), })) return regionconn, nil } // amiRegionCopy does a copy for the given AMI to the target region and // returns the resulting ID and snapshot IDs, or error. func (s *StepAMIRegionCopy) amiRegionCopy(ctx context.Context, state multistep.StateBag, config *AccessConfig, name, imageId, target, source, keyId string, encrypt *bool) (string, []string, error) { snapshotIds := []string{} if s.getRegionConn == nil { s.getRegionConn = getRegionConn } regionconn, err := s.getRegionConn(config, target) if err != nil { return "", snapshotIds, err } resp, err := regionconn.CopyImage(&ec2.CopyImageInput{ SourceRegion: &source, SourceImageId: &imageId, Name: &name, Encrypted: encrypt, KmsKeyId: aws.String(keyId), }) if err != nil { return "", snapshotIds, fmt.Errorf("Error Copying AMI (%s) to region (%s): %s", imageId, target, err) } // Wait for the image to become ready if err := s.AccessConfig.PollingConfig.WaitUntilAMIAvailable(ctx, regionconn, *resp.ImageId); err != nil { return "", snapshotIds, fmt.Errorf("Error waiting for AMI (%s) in region (%s): %s", *resp.ImageId, target, err) } // Getting snapshot IDs out of the copied AMI describeImageResp, err := regionconn.DescribeImages(&ec2.DescribeImagesInput{ImageIds: []*string{resp.ImageId}}) if err != nil { return "", snapshotIds, fmt.Errorf("Error describing copied AMI (%s) in region (%s): %s", imageId, target, err) } for _, blockDeviceMapping := range describeImageResp.Images[0].BlockDeviceMappings { if blockDeviceMapping.Ebs != nil && blockDeviceMapping.Ebs.SnapshotId != nil { snapshotIds = append(snapshotIds, *blockDeviceMapping.Ebs.SnapshotId) } } return *resp.ImageId, snapshotIds, nil }