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 }