internal/boot: adapt to aws sdk v2

This commit is contained in:
Sanne Raymaekers 2024-08-02 14:47:05 +02:00
parent b10bbc0fb0
commit fa3b203178
5 changed files with 123 additions and 99 deletions

View file

@ -328,19 +328,16 @@ func testBootUsingAWS(t *testing.T, imagePath string) {
imageName, err := test.GenerateCIArtifactName("osbuild-image-tests-image-") imageName, err := test.GenerateCIArtifactName("osbuild-image-tests-image-")
require.NoError(t, err) require.NoError(t, err)
e, err := boot.NewEC2(creds)
require.NoError(t, err)
// the following line should be done by osbuild-composer at some point // the following line should be done by osbuild-composer at some point
err = boot.UploadImageToAWS(creds, imagePath, imageName) err = boot.UploadImageToAWS(creds, imagePath, imageName)
require.NoErrorf(t, err, "upload to amazon failed, resources could have been leaked") require.NoErrorf(t, err, "upload to amazon failed, resources could have been leaked")
imageDesc, err := boot.DescribeEC2Image(e, imageName) imageDesc, err := boot.DescribeEC2Image(creds, imageName)
require.NoErrorf(t, err, "cannot describe the ec2 image") require.NoErrorf(t, err, "cannot describe the ec2 image")
// delete the image after the test is over // delete the image after the test is over
defer func() { defer func() {
err = boot.DeleteEC2Image(e, imageDesc) err = boot.DeleteEC2Image(creds, imageDesc)
require.NoErrorf(t, err, "cannot delete the ec2 image, resources could have been leaked") require.NoErrorf(t, err, "cannot delete the ec2 image, resources could have been leaked")
}() }()
@ -359,7 +356,7 @@ func testBootUsingAWS(t *testing.T, imagePath string) {
// boot the uploaded image and try to connect to it // boot the uploaded image and try to connect to it
err = boot.WithSSHKeyPair(func(privateKey, publicKey string) error { err = boot.WithSSHKeyPair(func(privateKey, publicKey string) error {
return boot.WithBootedImageInEC2(e, securityGroupName, imageDesc, publicKey, instanceType, func(address string) error { return boot.WithBootedImageInEC2(creds, securityGroupName, imageDesc, publicKey, instanceType, func(address string) error {
testSSH(t, address, privateKey, nil) testSSH(t, address, privateKey, nil)
return nil return nil
}) })

View file

@ -3,15 +3,17 @@
package boot package boot
import ( import (
"context"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"os" "os"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go/service/ec2" ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/osbuild/images/pkg/arch" "github.com/osbuild/images/pkg/arch"
"github.com/osbuild/osbuild-composer/internal/cloud/awscloud" "github.com/osbuild/osbuild-composer/internal/cloud/awscloud"
@ -92,57 +94,40 @@ func wrapErrorf(innerError error, format string, a ...interface{}) error {
func UploadImageToAWS(c *awsCredentials, imagePath string, imageName string) error { func UploadImageToAWS(c *awsCredentials, imagePath string, imageName string) error {
uploader, err := awscloud.New(c.Region, c.AccessKeyId, c.SecretAccessKey, c.sessionToken) uploader, err := awscloud.New(c.Region, c.AccessKeyId, c.SecretAccessKey, c.sessionToken)
if err != nil { if err != nil {
return fmt.Errorf("cannot create aws uploader: %v", err) return fmt.Errorf("cannot create aws uploader: %w", err)
} }
_, err = uploader.Upload(imagePath, c.Bucket, imageName) _, err = uploader.Upload(imagePath, c.Bucket, imageName)
if err != nil { if err != nil {
return fmt.Errorf("cannot upload the image: %v", err) return fmt.Errorf("cannot upload the image: %w", err)
} }
_, err = uploader.Register(imageName, c.Bucket, imageName, nil, arch.Current().String(), nil) _, err = uploader.Register(imageName, c.Bucket, imageName, nil, arch.Current().String(), nil)
if err != nil { if err != nil {
return fmt.Errorf("cannot register the image: %v", err) return fmt.Errorf("cannot register the image: %w", err)
} }
return nil return nil
} }
// NewEC2 creates EC2 struct from given credentials
func NewEC2(c *awsCredentials) (*ec2.EC2, error) {
creds := credentials.NewStaticCredentials(c.AccessKeyId, c.SecretAccessKey, "")
sess, err := session.NewSession(&aws.Config{
Credentials: creds,
Region: aws.String(c.Region),
})
if err != nil {
return nil, fmt.Errorf("cannot create aws session: %v", err)
}
return ec2.New(sess), nil
}
type imageDescription struct { type imageDescription struct {
Id *string Id *string
SnapshotId *string SnapshotId *string
// this doesn't support multiple snapshots per one image, // this doesn't support multiple snapshots per one image,
// because this feature is not supported in composer // because this feature is not supported in composer
Img *ec2types.Image
} }
// DescribeEC2Image searches for EC2 image by its name and returns // DescribeEC2Image searches for EC2 image by its name and returns
// its id and snapshot id // its id and snapshot id
func DescribeEC2Image(e *ec2.EC2, imageName string) (*imageDescription, error) { func DescribeEC2Image(c *awsCredentials, imageName string) (*imageDescription, error) {
imageDescriptions, err := e.DescribeImages(&ec2.DescribeImagesInput{ awscl, err := awscloud.New(c.Region, c.AccessKeyId, c.SecretAccessKey, c.sessionToken)
Filters: []*ec2.Filter{
{
Name: aws.String("name"),
Values: []*string{
aws.String(imageName),
},
},
},
})
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot describe the image: %v", err) return nil, fmt.Errorf("cannot create aws client: %w", err)
}
imageDescriptions, err := awscl.DescribeImagesByName(imageName)
if err != nil {
return nil, fmt.Errorf("cannot describe images: %w", err)
} }
imageId := imageDescriptions.Images[0].ImageId imageId := imageDescriptions.Images[0].ImageId
snapshotId := imageDescriptions.Images[0].BlockDeviceMappings[0].Ebs.SnapshotId snapshotId := imageDescriptions.Images[0].BlockDeviceMappings[0].Ebs.SnapshotId
@ -150,37 +135,27 @@ func DescribeEC2Image(e *ec2.EC2, imageName string) (*imageDescription, error) {
return &imageDescription{ return &imageDescription{
Id: imageId, Id: imageId,
SnapshotId: snapshotId, SnapshotId: snapshotId,
Img: &imageDescriptions.Images[0],
}, nil }, nil
} }
// DeleteEC2Image deletes the specified image and its associated snapshot // DeleteEC2Image deletes the specified image and its associated snapshot
func DeleteEC2Image(e *ec2.EC2, imageDesc *imageDescription) error { func DeleteEC2Image(c *awsCredentials, imageDesc *imageDescription) error {
var retErr error awscl, err := awscloud.New(c.Region, c.AccessKeyId, c.SecretAccessKey, c.sessionToken)
// firstly, deregister the image
_, err := e.DeregisterImage(&ec2.DeregisterImageInput{
ImageId: imageDesc.Id,
})
if err != nil { if err != nil {
retErr = wrapErrorf(retErr, "cannot deregister the image: %v", err) return fmt.Errorf("cannot create aws client: %w", err)
} }
return awscl.RemoveSnapshotAndDeregisterImage(imageDesc.Img)
// now it's possible to delete the snapshot
_, err = e.DeleteSnapshot(&ec2.DeleteSnapshotInput{
SnapshotId: imageDesc.SnapshotId,
})
if err != nil {
retErr = wrapErrorf(retErr, "cannot delete the snapshot: %v", err)
}
return retErr
} }
// WithBootedImageInEC2 runs the function f in the context of booted // WithBootedImageInEC2 runs the function f in the context of booted
// image in AWS EC2 // image in AWS EC2
func WithBootedImageInEC2(e *ec2.EC2, securityGroupName string, imageDesc *imageDescription, publicKey string, instanceType string, f func(address string) error) (retErr error) { func WithBootedImageInEC2(c *awsCredentials, securityGroupName string, imageDesc *imageDescription, publicKey string, instanceType string, f func(address string) error) (retErr error) {
awscl, err := awscloud.New(c.Region, c.AccessKeyId, c.SecretAccessKey, c.sessionToken)
if err != nil {
return fmt.Errorf("cannot create aws client: %w", err)
}
// generate user data with given public key // generate user data with given public key
userData, err := CreateUserData(publicKey) userData, err := CreateUserData(publicKey)
if err != nil { if err != nil {
@ -190,71 +165,92 @@ func WithBootedImageInEC2(e *ec2.EC2, securityGroupName string, imageDesc *image
// Security group must be now generated, because by default // Security group must be now generated, because by default
// all traffic to EC2 instance is filtered. // all traffic to EC2 instance is filtered.
// Firstly create a security group // Firstly create a security group
securityGroup, err := e.CreateSecurityGroup(&ec2.CreateSecurityGroupInput{ securityGroup, err := awscl.EC2ForTestsOnly().CreateSecurityGroup(
GroupName: aws.String(securityGroupName), context.Background(),
Description: aws.String("image-tests-security-group"), &ec2.CreateSecurityGroupInput{
}) GroupName: aws.String(securityGroupName),
Description: aws.String("image-tests-security-group"),
},
)
if err != nil { if err != nil {
return fmt.Errorf("cannot create a new security group: %v", err) return fmt.Errorf("cannot create a new security group: %w", err)
} }
defer func() { defer func() {
_, err = e.DeleteSecurityGroup(&ec2.DeleteSecurityGroupInput{ _, err = awscl.EC2ForTestsOnly().DeleteSecurityGroup(
GroupId: securityGroup.GroupId, context.Background(),
}) &ec2.DeleteSecurityGroupInput{
GroupId: securityGroup.GroupId,
},
)
if err != nil { if err != nil {
retErr = wrapErrorf(retErr, "cannot delete the security group: %v", err) retErr = wrapErrorf(retErr, "cannot delete the security group: %w", err)
} }
}() }()
// Authorize incoming SSH connections. // Authorize incoming SSH connections.
_, err = e.AuthorizeSecurityGroupIngress(&ec2.AuthorizeSecurityGroupIngressInput{ _, err = awscl.EC2ForTestsOnly().AuthorizeSecurityGroupIngress(
CidrIp: aws.String("0.0.0.0/0"), context.Background(),
GroupId: securityGroup.GroupId, &ec2.AuthorizeSecurityGroupIngressInput{
FromPort: aws.Int64(22), CidrIp: aws.String("0.0.0.0/0"),
ToPort: aws.Int64(22), GroupId: securityGroup.GroupId,
IpProtocol: aws.String("tcp"), FromPort: aws.Int32(22),
}) ToPort: aws.Int32(22),
IpProtocol: aws.String("tcp"),
},
)
if err != nil { if err != nil {
return fmt.Errorf("canot add a rule to the security group: %v", err) return fmt.Errorf("canot add a rule to the security group: %w", err)
} }
// Finally, run the instance from the given image and with the created security group // Finally, run the instance from the given image and with the created security group
res, err := e.RunInstances(&ec2.RunInstancesInput{ res, err := awscl.EC2ForTestsOnly().RunInstances(
MaxCount: aws.Int64(1), context.Background(),
MinCount: aws.Int64(1), &ec2.RunInstancesInput{
ImageId: imageDesc.Id, MaxCount: aws.Int32(1),
InstanceType: aws.String(instanceType), MinCount: aws.Int32(1),
SecurityGroupIds: []*string{securityGroup.GroupId}, ImageId: imageDesc.Id,
UserData: aws.String(encodeBase64(userData)), InstanceType: ec2types.InstanceType(instanceType),
}) SecurityGroupIds: []string{*securityGroup.GroupId},
UserData: aws.String(encodeBase64(userData)),
},
)
if err != nil { if err != nil {
return fmt.Errorf("cannot create a new instance: %v", err) return fmt.Errorf("cannot create a new instance: %w", err)
} }
describeInstanceInput := &ec2.DescribeInstancesInput{ describeInstanceInput := &ec2.DescribeInstancesInput{
InstanceIds: []*string{ InstanceIds: []string{
res.Instances[0].InstanceId, *res.Instances[0].InstanceId,
}, },
} }
defer func() { defer func() {
// We need to terminate the instance now and wait until the termination is done. // We need to terminate the instance now and wait until the termination is done.
// Otherwise, it wouldn't be possible to delete the image. // Otherwise, it wouldn't be possible to delete the image.
_, err = e.TerminateInstances(&ec2.TerminateInstancesInput{ _, err = awscl.EC2ForTestsOnly().TerminateInstances(
InstanceIds: []*string{ context.Background(),
res.Instances[0].InstanceId, &ec2.TerminateInstancesInput{
InstanceIds: []string{
*res.Instances[0].InstanceId,
},
}, },
}) )
if err != nil { if err != nil {
retErr = wrapErrorf(retErr, "cannot terminate the instance: %v", err) retErr = wrapErrorf(retErr, "cannot terminate the instance: %w", err)
return return
} }
err = e.WaitUntilInstanceTerminated(describeInstanceInput) instTermWaiter := ec2.NewInstanceTerminatedWaiter(awscl.EC2ForTestsOnly())
err = instTermWaiter.Wait(
context.Background(),
describeInstanceInput,
time.Hour,
)
if err != nil { if err != nil {
retErr = wrapErrorf(retErr, "waiting for the instance termination failed: %v", err) retErr = wrapErrorf(retErr, "cannot terminate the instance: %w", err)
return
} }
}() }()
@ -262,15 +258,20 @@ func WithBootedImageInEC2(e *ec2.EC2, securityGroupName string, imageDesc *image
// is in the state "EXISTS". However, in this state the instance is not // is in the state "EXISTS". However, in this state the instance is not
// much usable, therefore wait until "RUNNING" state, in which the instance // much usable, therefore wait until "RUNNING" state, in which the instance
// actually can do something useful for us. // actually can do something useful for us.
err = e.WaitUntilInstanceRunning(describeInstanceInput) instWaiter := ec2.NewInstanceRunningWaiter(awscl.EC2ForTestsOnly())
err = instWaiter.Wait(
context.Background(),
describeInstanceInput,
time.Hour,
)
if err != nil { if err != nil {
return fmt.Errorf("waiting for the instance to be running failed: %v", err) return fmt.Errorf("waiting for the instance to be running failed: %w", err)
} }
// By describing the instance, we can get the ip address. // By describing the instance, we can get the ip address.
out, err := e.DescribeInstances(describeInstanceInput) out, err := awscl.EC2ForTestsOnly().DescribeInstances(context.Background(), describeInstanceInput)
if err != nil { if err != nil {
return fmt.Errorf("cannot describe the instance: %v", err) return fmt.Errorf("cannot describe the instance: %w", err)
} }
return f(*out.Reservations[0].Instances[0].PublicIpAddress) return f(*out.Reservations[0].Instances[0].PublicIpAddress)

View file

@ -180,6 +180,11 @@ func NewForEndpointFromFile(filename, endpoint, region, caBundle string, skipSSL
return newAwsFromCredsWithEndpoint(config.WithSharedCredentialsFiles([]string{filename, "default"}), region, endpoint, caBundle, skipSSLVerification) return newAwsFromCredsWithEndpoint(config.WithSharedCredentialsFiles([]string{filename, "default"}), region, endpoint, caBundle, skipSSLVerification)
} }
// This is used by the internal/boot test, which access the ec2 apis directly
func (a *AWS) EC2ForTestsOnly() EC2 {
return a.ec2
}
func (a *AWS) Upload(filename, bucket, key string) (*manager.UploadOutput, error) { func (a *AWS) Upload(filename, bucket, key string) (*manager.UploadOutput, error) {
file, err := os.Open(filename) file, err := os.Open(filename)
if err != nil { if err != nil {
@ -627,3 +632,19 @@ func (a *AWS) Regions() ([]string, error) {
} }
return result, nil return result, nil
} }
func (a *AWS) DescribeImagesByName(name string) (*ec2.DescribeImagesOutput, error) {
return a.ec2.DescribeImages(
context.Background(),
&ec2.DescribeImagesInput{
Filters: []ec2types.Filter{
{
Name: aws.String("name"),
Values: []string{
name,
},
},
},
},
)
}

View file

@ -32,6 +32,7 @@ type EC2 interface {
// Instances // Instances
DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error)
DescribeInstanceStatus(context.Context, *ec2.DescribeInstanceStatusInput, ...func(*ec2.Options)) (*ec2.DescribeInstanceStatusOutput, error) DescribeInstanceStatus(context.Context, *ec2.DescribeInstanceStatusInput, ...func(*ec2.Options)) (*ec2.DescribeInstanceStatusOutput, error)
RunInstances(context.Context, *ec2.RunInstancesInput, ...func(*ec2.Options)) (*ec2.RunInstancesOutput, error)
TerminateInstances(context.Context, *ec2.TerminateInstancesInput, ...func(*ec2.Options)) (*ec2.TerminateInstancesOutput, error) TerminateInstances(context.Context, *ec2.TerminateInstancesInput, ...func(*ec2.Options)) (*ec2.TerminateInstancesOutput, error)
// Fleets // Fleets

View file

@ -261,6 +261,10 @@ func (m *ec2mock) DescribeInstanceStatus(ctx context.Context, input *ec2.Describ
}, nil }, nil
} }
func (m *ec2mock) RunInstances(ctx context.Context, input *ec2.RunInstancesInput, optfns ...func(*ec2.Options)) (*ec2.RunInstancesOutput, error) {
return nil, nil
}
func (m *ec2mock) TerminateInstances(ctx context.Context, input *ec2.TerminateInstancesInput, optfns ...func(*ec2.Options)) (*ec2.TerminateInstancesOutput, error) { func (m *ec2mock) TerminateInstances(ctx context.Context, input *ec2.TerminateInstancesInput, optfns ...func(*ec2.Options)) (*ec2.TerminateInstancesOutput, error) {
m.calledFn["TerminateInstances"] += 1 m.calledFn["TerminateInstances"] += 1
return nil, nil return nil, nil