sigs.k8s.io/cluster-api-provider-aws@v1.5.5/cmd/clusterawsadm/ami/copy.go (about)

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package ami
    18  
    19  import (
    20  	"fmt"
    21  	"strconv"
    22  	"time"
    23  
    24  	"github.com/aws/aws-sdk-go/aws"
    25  	"github.com/aws/aws-sdk-go/aws/session"
    26  	"github.com/aws/aws-sdk-go/service/ec2"
    27  	"github.com/go-logr/logr"
    28  	"github.com/pkg/errors"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  
    31  	amiv1 "sigs.k8s.io/cluster-api-provider-aws/cmd/clusterawsadm/api/ami/v1beta1"
    32  	ec2service "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/ec2"
    33  	"sigs.k8s.io/cluster-api/util"
    34  )
    35  
    36  // CopyInput defines input that can be copied to create an AWSAMI.
    37  type CopyInput struct {
    38  	SourceRegion      string
    39  	DestinationRegion string
    40  	OwnerID           string
    41  	OperatingSystem   string
    42  	KubernetesVersion string
    43  	KmsKeyID          string
    44  	DryRun            bool
    45  	Encrypted         bool
    46  	Log               logr.Logger
    47  }
    48  
    49  // Copy will create an AWSAMI from a CopyInput.
    50  func Copy(input CopyInput) (*amiv1.AWSAMI, error) {
    51  	sourceSession, err := session.NewSessionWithOptions(session.Options{
    52  		SharedConfigState: session.SharedConfigEnable,
    53  		Config:            aws.Config{Region: aws.String(input.SourceRegion)},
    54  	})
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  	ec2Client := ec2.New(sourceSession)
    59  
    60  	image, err := ec2service.DefaultAMILookup(ec2Client, input.OwnerID, input.OperatingSystem, input.KubernetesVersion, "")
    61  	if err != nil {
    62  		return nil, err
    63  	}
    64  
    65  	var newImageID, newImageName string
    66  
    67  	destSession, err := session.NewSessionWithOptions(session.Options{
    68  		SharedConfigState: session.SharedConfigEnable,
    69  		Config:            aws.Config{Region: aws.String(input.DestinationRegion)},
    70  	})
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  
    75  	if input.Encrypted {
    76  		newImageName, newImageID, err = copyWithSnapshot(copyWithSnapshotInput{
    77  			sourceRegion:      input.SourceRegion,
    78  			image:             image,
    79  			destinationRegion: input.DestinationRegion,
    80  			encrypted:         input.Encrypted,
    81  			kmsKeyID:          input.KmsKeyID,
    82  			sess:              destSession,
    83  			log:               input.Log,
    84  		})
    85  	} else {
    86  		newImageName, newImageID, err = copyWithoutSnapshot(copyWithoutSnapshotInput{
    87  			sourceRegion: input.SourceRegion,
    88  			image:        image,
    89  			dryRun:       input.DryRun,
    90  			sess:         destSession,
    91  			log:          input.Log,
    92  		})
    93  	}
    94  
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	ami := amiv1.AWSAMI{
   100  		ObjectMeta: metav1.ObjectMeta{
   101  			Name:              newImageName,
   102  			CreationTimestamp: metav1.NewTime(time.Now()),
   103  		},
   104  		TypeMeta: metav1.TypeMeta{
   105  			Kind:       amiv1.AWSAMIKind,
   106  			APIVersion: amiv1.SchemeGroupVersion.String(),
   107  		},
   108  		Spec: amiv1.AWSAMISpec{
   109  			OS:                input.OperatingSystem,
   110  			Region:            input.DestinationRegion,
   111  			ImageID:           newImageID,
   112  			KubernetesVersion: input.KubernetesVersion,
   113  		},
   114  	}
   115  
   116  	if err == nil {
   117  		input.Log.Info("Completed!")
   118  	}
   119  
   120  	return &ami, err
   121  }
   122  
   123  type copyWithoutSnapshotInput struct {
   124  	sourceRegion string
   125  	dryRun       bool
   126  	log          logr.Logger
   127  	sess         *session.Session
   128  	image        *ec2.Image
   129  }
   130  
   131  func copyWithoutSnapshot(input copyWithoutSnapshotInput) (string, string, error) {
   132  	imgName := aws.StringValue(input.image.Name)
   133  	ec2Client := ec2.New(input.sess)
   134  	in2 := &ec2.CopyImageInput{
   135  		Description:   input.image.Description,
   136  		DryRun:        aws.Bool(input.dryRun),
   137  		Name:          input.image.Name,
   138  		SourceImageId: input.image.ImageId,
   139  		SourceRegion:  aws.String(input.sourceRegion),
   140  	}
   141  	log := input.log.WithValues("imageName", imgName)
   142  	log.Info("Copying the retrieved image", "imageID", aws.StringValue(input.image.ImageId), "ownerID", aws.StringValue(input.image.OwnerId))
   143  	out, err := ec2Client.CopyImage(in2)
   144  	if err != nil {
   145  		fmt.Printf("version %q\n", out)
   146  		return imgName, "", err
   147  	}
   148  
   149  	return imgName, aws.StringValue(out.ImageId), nil
   150  }
   151  
   152  type copyWithSnapshotInput struct {
   153  	sourceRegion      string
   154  	destinationRegion string
   155  	kmsKeyID          string
   156  	dryRun            bool
   157  	encrypted         bool
   158  	log               logr.Logger
   159  	image             *ec2.Image
   160  	sess              *session.Session
   161  }
   162  
   163  func copyWithSnapshot(input copyWithSnapshotInput) (string, string, error) {
   164  	ec2Client := ec2.New(input.sess)
   165  	imgName := *input.image.Name + util.RandomString(3) + strconv.Itoa(int(time.Now().Unix()))
   166  	log := input.log.WithValues("imageName", imgName)
   167  
   168  	if len(input.image.BlockDeviceMappings) == 0 || input.image.BlockDeviceMappings[0].Ebs == nil {
   169  		return imgName, "", errors.New("image does not have EBS attached")
   170  	}
   171  
   172  	kmsKeyIDPtr := aws.String(input.kmsKeyID)
   173  	if input.kmsKeyID == "" {
   174  		kmsKeyIDPtr = nil
   175  	}
   176  
   177  	copyInput := &ec2.CopySnapshotInput{
   178  		Description:       input.image.Description,
   179  		DestinationRegion: aws.String(input.destinationRegion),
   180  		DryRun:            aws.Bool(input.dryRun),
   181  		Encrypted:         aws.Bool(input.encrypted),
   182  		SourceRegion:      aws.String(input.sourceRegion),
   183  		KmsKeyId:          kmsKeyIDPtr,
   184  		SourceSnapshotId:  input.image.BlockDeviceMappings[0].Ebs.SnapshotId,
   185  	}
   186  
   187  	// Generate a presigned url from the CopySnapshotInput
   188  	req, _ := ec2Client.CopySnapshotRequest(copyInput)
   189  	str, err := req.Presign(15 * time.Minute)
   190  	if err != nil {
   191  		return imgName, "", errors.Wrap(err, "Failed to generate presigned url")
   192  	}
   193  	copyInput.PresignedUrl = aws.String(str)
   194  
   195  	out, err := ec2Client.CopySnapshot(copyInput)
   196  	if err != nil {
   197  		return imgName, "", errors.Wrap(err, "Failed copying snapshot")
   198  	}
   199  	log.Info("Copying snapshot, this may take a couple of minutes...",
   200  		"sourceSnapshot", aws.StringValue(input.image.BlockDeviceMappings[0].Ebs.SnapshotId),
   201  		"destinationSnapshot", aws.StringValue(out.SnapshotId),
   202  	)
   203  
   204  	err = ec2Client.WaitUntilSnapshotCompleted(&ec2.DescribeSnapshotsInput{
   205  		DryRun:      aws.Bool(input.dryRun),
   206  		SnapshotIds: []*string{out.SnapshotId},
   207  	})
   208  	if err != nil {
   209  		return imgName, "", errors.Wrap(err, fmt.Sprintf("Failed waiting for encrypted snapshot copy completion: %q\n", aws.StringValue(out.SnapshotId)))
   210  	}
   211  
   212  	ebsMapping := &ec2.BlockDeviceMapping{
   213  		DeviceName: input.image.BlockDeviceMappings[0].DeviceName,
   214  		Ebs: &ec2.EbsBlockDevice{
   215  			SnapshotId: out.SnapshotId,
   216  		},
   217  	}
   218  
   219  	log.Info("Registering AMI")
   220  
   221  	registerOut, err := ec2Client.RegisterImage(&ec2.RegisterImageInput{
   222  		Architecture:        input.image.Architecture,
   223  		BlockDeviceMappings: []*ec2.BlockDeviceMapping{ebsMapping},
   224  		Description:         input.image.Description,
   225  		DryRun:              aws.Bool(input.dryRun),
   226  		EnaSupport:          input.image.EnaSupport,
   227  		KernelId:            input.image.KernelId,
   228  		Name:                aws.String(imgName),
   229  		RamdiskId:           input.image.RamdiskId,
   230  		RootDeviceName:      input.image.RootDeviceName,
   231  		SriovNetSupport:     input.image.SriovNetSupport,
   232  		VirtualizationType:  input.image.VirtualizationType,
   233  	})
   234  
   235  	if err != nil {
   236  		return imgName, "", err
   237  	}
   238  
   239  	return imgName, aws.StringValue(registerOut.ImageId), err
   240  }