cloud/awscloud: add tests for ec2 operations

This commit is contained in:
Sanne Raymaekers 2024-08-01 17:39:04 +02:00
parent 810e9133e8
commit 16c9a7be88
4 changed files with 396 additions and 7 deletions

View file

@ -28,8 +28,10 @@ type AWS struct {
s3presign S3Presign
}
func newForTest(s3cli S3, upldr S3Manager, sign S3Presign) *AWS {
func newForTest(ec2cli EC2, ec2imds EC2Imds, s3cli S3, upldr S3Manager, sign S3Presign) *AWS {
return &AWS{
ec2: ec2cli,
ec2imds: ec2imds,
s3: s3cli,
s3uploader: upldr,
s3presign: sign,

View file

@ -5,13 +5,15 @@ import (
"path/filepath"
"testing"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/stretchr/testify/require"
"github.com/osbuild/osbuild-composer/internal/cloud/awscloud"
"github.com/osbuild/osbuild-composer/internal/common"
)
func TestS3MarkObjectAsPublic(t *testing.T) {
aws := awscloud.NewForTest(&s3mock{t, "bucket", "object-key"}, nil, nil)
aws := awscloud.NewForTest(nil, nil, &s3mock{t, "bucket", "object-key"}, nil, nil)
require.NotNil(t, aws)
require.NoError(t, aws.MarkS3ObjectAsPublic("bucket", "object-key"))
}
@ -20,16 +22,89 @@ func TestS3Upload(t *testing.T) {
tmpDir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "file"), []byte("imanimage"), 0600))
aws := awscloud.NewForTest(nil, &s3upldrmock{t, "imanimage", "bucket", "object-key"}, nil)
aws := awscloud.NewForTest(nil, nil, nil, &s3upldrmock{t, "imanimage", "bucket", "object-key"}, nil)
require.NotNil(t, aws)
_, err := aws.Upload(filepath.Join(tmpDir, "file"), "bucket", "object-key")
require.NoError(t, err)
}
func TestS3ObjectPresignedURL(t *testing.T) {
aws := awscloud.NewForTest(nil, nil, &s3signmock{t, "bucket", "object-key"})
aws := awscloud.NewForTest(nil, nil, nil, nil, &s3signmock{t, "bucket", "object-key"})
require.NotNil(t, aws)
url, err := aws.S3ObjectPresignedURL("bucket", "object-key")
require.NoError(t, err)
require.Equal(t, "https://url.real", url)
}
func TestEC2Register(t *testing.T) {
m := newEc2Mock(t)
aws := awscloud.NewForTest(m, nil, &s3mock{t, "bucket", "object-key"}, nil, nil)
require.NotNil(t, aws)
// Image without share
imageId, err := aws.Register("image-name", "bucket", "object-key", []string{}, "x86_64", common.ToPtr("uefi-preferred"))
require.NoError(t, err)
require.Equal(t, "image-id", *imageId)
// basic image import operations
require.Equal(t, 1, m.calledFn["ImportSnapshot"])
require.Equal(t, 1, m.calledFn["RegisterImage"])
// sharing operations
require.Equal(t, 0, m.calledFn["ModifyImageAttribute"])
require.Equal(t, 0, m.calledFn["ModifySnapshotAttribute"])
// Image with share
imageId, err = aws.Register("image-name", "bucket", "object-key", []string{"share-with-user"}, "x86_64", common.ToPtr("uefi-preferred"))
require.NoError(t, err)
require.Equal(t, "image-id", *imageId)
// basic image import operations
require.Equal(t, 2, m.calledFn["ImportSnapshot"])
require.Equal(t, 2, m.calledFn["RegisterImage"])
// sharing operations
require.Equal(t, 1, m.calledFn["ModifyImageAttribute"])
require.Equal(t, 1, m.calledFn["ModifySnapshotAttribute"])
// 2 snapshots, 2 images
require.Equal(t, 4, m.calledFn["CreateTags"])
}
func TestEC2CopyImage(t *testing.T) {
m := newEc2Mock(t)
aws := awscloud.NewForTest(m, nil, &s3mock{t, "bucket", "object-key"}, nil, nil)
imageId, err := aws.CopyImage("image-name", "image-id", "region")
require.NoError(t, err)
require.Equal(t, "image-id", imageId)
require.Equal(t, 1, m.calledFn["CopyImage"])
// 1 snapshot, 1 image
require.Equal(t, 2, m.calledFn["CreateTags"])
}
func TestEC2RemoveSnapshotAndDeregisterImage(t *testing.T) {
m := newEc2Mock(t)
aws := awscloud.NewForTest(m, nil, &s3mock{t, "bucket", "object-key"}, nil, nil)
require.NotNil(t, aws)
err := aws.RemoveSnapshotAndDeregisterImage(&ec2types.Image{
ImageId: &m.imageId,
State: ec2types.ImageStateAvailable,
BlockDeviceMappings: []ec2types.BlockDeviceMapping{
{
Ebs: &ec2types.EbsBlockDevice{
SnapshotId: &m.snapshotId,
},
},
},
})
require.NoError(t, err)
require.Equal(t, 1, m.calledFn["DeleteSnapshot"])
require.Equal(t, 1, m.calledFn["DeregisterImage"])
}
func TestEC2Regions(t *testing.T) {
m := newEc2Mock(t)
aws := awscloud.NewForTest(m, nil, &s3mock{t, "bucket", "object-key"}, nil, nil)
require.NotNil(t, aws)
regions, err := aws.Regions()
require.NoError(t, err)
require.NotEmpty(t, regions)
require.Equal(t, 1, m.calledFn["DescribeRegions"])
}

View file

@ -6,8 +6,12 @@ import (
"testing"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/signer/v4"
"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/s3"
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/stretchr/testify/require"
@ -21,6 +25,8 @@ type s3mock struct {
}
func (m *s3mock) DeleteObject(ctx context.Context, input *s3.DeleteObjectInput, optfns ...func(*s3.Options)) (*s3.DeleteObjectOutput, error) {
require.Equal(m.t, m.bucket, *input.Bucket)
require.Equal(m.t, m.key, *input.Key)
return nil, nil
}
@ -69,3 +75,292 @@ func (m *s3signmock) PresignGetObject(ctx context.Context, input *s3.GetObjectIn
URL: "https://url.real",
}, nil
}
type ec2imdsmock struct {
t *testing.T
instanceID string
region string
}
func (m *ec2imdsmock) GetInstanceIdentityDocument(ctx context.Context, input *imds.GetInstanceIdentityDocumentInput, optfns ...func(*imds.Options)) (*imds.GetInstanceIdentityDocumentOutput, error) {
return &imds.GetInstanceIdentityDocumentOutput{
InstanceIdentityDocument: imds.InstanceIdentityDocument{
InstanceID: m.instanceID,
Region: m.region,
},
}, nil
}
type ec2mock struct {
t *testing.T
// Image function variables
imageId string
imageName string
snapshotId string
calledFn map[string]int
}
func newEc2Mock(t *testing.T) *ec2mock {
return &ec2mock{
t: t,
imageId: "image-id",
imageName: "image-name",
snapshotId: "snapshot-id",
calledFn: make(map[string]int),
}
}
func (m *ec2mock) DescribeRegions(ctx context.Context, input *ec2.DescribeRegionsInput, optfns ...func(*ec2.Options)) (*ec2.DescribeRegionsOutput, error) {
m.calledFn["DescribeRegions"] += 1
return &ec2.DescribeRegionsOutput{
Regions: []ec2types.Region{
{
RegionName: aws.String("region1"),
},
{
RegionName: aws.String("region2"),
},
},
}, nil
}
func (m *ec2mock) AuthorizeSecurityGroupIngress(ctx context.Context, input *ec2.AuthorizeSecurityGroupIngressInput, optfns ...func(*ec2.Options)) (*ec2.AuthorizeSecurityGroupIngressOutput, error) {
m.calledFn["AuthorizeSecurityGroupIngress"] += 1
return &ec2.AuthorizeSecurityGroupIngressOutput{
Return: aws.Bool(true),
}, nil
}
func (m *ec2mock) CreateSecurityGroup(ctx context.Context, input *ec2.CreateSecurityGroupInput, optfns ...func(*ec2.Options)) (*ec2.CreateSecurityGroupOutput, error) {
m.calledFn["CreateSecurityGroup"] += 1
return &ec2.CreateSecurityGroupOutput{
GroupId: aws.String("sg-id"),
}, nil
}
func (m *ec2mock) DeleteSecurityGroup(ctx context.Context, input *ec2.DeleteSecurityGroupInput, optfns ...func(*ec2.Options)) (*ec2.DeleteSecurityGroupOutput, error) {
m.calledFn["DeleteSecurityGroup"] += 1
return nil, nil
}
func (m *ec2mock) DescribeSecurityGroups(ctx context.Context, input *ec2.DescribeSecurityGroupsInput, optfns ...func(*ec2.Options)) (*ec2.DescribeSecurityGroupsOutput, error) {
m.calledFn["DescribeSecurityGroup"] += 1
return &ec2.DescribeSecurityGroupsOutput{
SecurityGroups: []ec2types.SecurityGroup{
{
GroupId: aws.String("sg-id"),
IpPermissions: []ec2types.IpPermission{
{},
},
IpPermissionsEgress: []ec2types.IpPermission{
{},
},
},
},
}, nil
}
func (m *ec2mock) CreateSubnet(ctx context.Context, input *ec2.CreateSubnetInput, optfns ...func(*ec2.Options)) (*ec2.CreateSubnetOutput, error) {
m.calledFn["CreateSubnet"] += 1
return nil, nil
}
func (m *ec2mock) DeleteSubnet(ctx context.Context, input *ec2.DeleteSubnetInput, optfns ...func(*ec2.Options)) (*ec2.DeleteSubnetOutput, error) {
m.calledFn["DeleteSubnet"] += 1
return nil, nil
}
func (m *ec2mock) DescribeSubnets(ctx context.Context, input *ec2.DescribeSubnetsInput, optfns ...func(*ec2.Options)) (*ec2.DescribeSubnetsOutput, error) {
m.calledFn["DescribeSubnets"] += 1
return &ec2.DescribeSubnetsOutput{
Subnets: []ec2types.Subnet{
{
SubnetId: aws.String("subnet-id"),
},
},
}, nil
}
func (m *ec2mock) CreateLaunchTemplate(ctx context.Context, input *ec2.CreateLaunchTemplateInput, optfns ...func(*ec2.Options)) (*ec2.CreateLaunchTemplateOutput, error) {
m.calledFn["CreateLaunchTemplate"] += 1
return &ec2.CreateLaunchTemplateOutput{
LaunchTemplate: &ec2types.LaunchTemplate{
LaunchTemplateId: aws.String("lt-id"),
},
}, nil
}
func (m *ec2mock) DeleteLaunchTemplate(ctx context.Context, input *ec2.DeleteLaunchTemplateInput, optfns ...func(*ec2.Options)) (*ec2.DeleteLaunchTemplateOutput, error) {
m.calledFn["DeleteLaunchTemplate"] += 1
return nil, nil
}
func (m *ec2mock) DescribeLaunchTemplates(ctx context.Context, input *ec2.DescribeLaunchTemplatesInput, optfns ...func(*ec2.Options)) (*ec2.DescribeLaunchTemplatesOutput, error) {
m.calledFn["DescribeLaunchTemplates"] += 1
return &ec2.DescribeLaunchTemplatesOutput{
LaunchTemplates: []ec2types.LaunchTemplate{
{
LaunchTemplateId: aws.String("lt-id"),
},
},
}, nil
}
func (m *ec2mock) DescribeInstances(ctx context.Context, input *ec2.DescribeInstancesInput, optfns ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) {
m.calledFn["DescribeInstances"] += 1
// For the waiters sometimes a running instance is required, sometimes a terminated one
state := ec2types.InstanceStateNameRunning
if m.calledFn["DescribeInstances"]%2 == 0 {
state = ec2types.InstanceStateNameTerminated
}
return &ec2.DescribeInstancesOutput{
Reservations: []ec2types.Reservation{
{
Instances: []ec2types.Instance{
{
InstanceId: aws.String("instance-id"),
VpcId: aws.String("vpc-id"),
ImageId: aws.String("image-id"),
SubnetId: aws.String("subnet-id"),
State: &ec2types.InstanceState{
Name: state,
},
},
},
},
},
}, nil
}
func (m *ec2mock) DescribeInstanceStatus(ctx context.Context, input *ec2.DescribeInstanceStatusInput, optfns ...func(*ec2.Options)) (*ec2.DescribeInstanceStatusOutput, error) {
m.calledFn["DescribeInstanceStatus"] += 1
// For the waiters sometimes a running instance is required, sometimes a terminated one
state := ec2types.InstanceStateNameRunning
if m.calledFn["DescribeInstanceStatus"]%2 == 0 {
state = ec2types.InstanceStateNameTerminated
}
return &ec2.DescribeInstanceStatusOutput{
InstanceStatuses: []ec2types.InstanceStatus{
{
InstanceId: aws.String("instance-id"),
InstanceState: &ec2types.InstanceState{
Name: state,
},
InstanceStatus: &ec2types.InstanceStatusSummary{
Status: ec2types.SummaryStatusOk,
},
},
},
}, nil
}
func (m *ec2mock) TerminateInstances(ctx context.Context, input *ec2.TerminateInstancesInput, optfns ...func(*ec2.Options)) (*ec2.TerminateInstancesOutput, error) {
m.calledFn["TerminateInstances"] += 1
return nil, nil
}
func (m *ec2mock) CreateFleet(ctx context.Context, input *ec2.CreateFleetInput, optfns ...func(*ec2.Options)) (*ec2.CreateFleetOutput, error) {
m.calledFn["CreateFleet"] += 1
return &ec2.CreateFleetOutput{
FleetId: aws.String("fleet-id"),
Instances: []ec2types.CreateFleetInstance{
{
InstanceIds: []string{
"instance-id",
},
},
},
}, nil
}
func (m *ec2mock) DeleteFleets(ctx context.Context, input *ec2.DeleteFleetsInput, optfns ...func(*ec2.Options)) (*ec2.DeleteFleetsOutput, error) {
m.calledFn["DeleteFleets"] += 1
return nil, nil
}
func (m *ec2mock) CopyImage(ctx context.Context, input *ec2.CopyImageInput, optfns ...func(*ec2.Options)) (*ec2.CopyImageOutput, error) {
m.calledFn["CopyImage"] += 1
return &ec2.CopyImageOutput{
ImageId: &m.imageId,
}, nil
}
func (m *ec2mock) RegisterImage(ctx context.Context, input *ec2.RegisterImageInput, optfns ...func(*ec2.Options)) (*ec2.RegisterImageOutput, error) {
m.calledFn["RegisterImage"] += 1
return &ec2.RegisterImageOutput{
ImageId: &m.imageId,
}, nil
}
func (m *ec2mock) DeregisterImage(ctx context.Context, input *ec2.DeregisterImageInput, optfns ...func(*ec2.Options)) (*ec2.DeregisterImageOutput, error) {
m.calledFn["DeregisterImage"] += 1
return nil, nil
}
func (m *ec2mock) DescribeImages(ctx context.Context, input *ec2.DescribeImagesInput, optfns ...func(*ec2.Options)) (*ec2.DescribeImagesOutput, error) {
m.calledFn["DescribeImages"] += 1
return &ec2.DescribeImagesOutput{
Images: []ec2types.Image{
{
ImageId: &m.imageId,
State: ec2types.ImageStateAvailable,
BlockDeviceMappings: []ec2types.BlockDeviceMapping{
{
Ebs: &ec2types.EbsBlockDevice{
SnapshotId: &m.snapshotId,
},
},
},
},
},
}, nil
}
func (m *ec2mock) ModifyImageAttribute(ctx context.Context, input *ec2.ModifyImageAttributeInput, optfns ...func(*ec2.Options)) (*ec2.ModifyImageAttributeOutput, error) {
m.calledFn["ModifyImageAttribute"] += 1
return nil, nil
}
func (m *ec2mock) DeleteSnapshot(ctx context.Context, input *ec2.DeleteSnapshotInput, optfns ...func(*ec2.Options)) (*ec2.DeleteSnapshotOutput, error) {
m.calledFn["DeleteSnapshot"] += 1
return nil, nil
}
func (m *ec2mock) DescribeImportSnapshotTasks(ctx context.Context, input *ec2.DescribeImportSnapshotTasksInput, optfns ...func(*ec2.Options)) (*ec2.DescribeImportSnapshotTasksOutput, error) {
m.calledFn["DescribeImportSnapshotTasks"] += 1
return &ec2.DescribeImportSnapshotTasksOutput{
ImportSnapshotTasks: []ec2types.ImportSnapshotTask{
{
ImportTaskId: aws.String("import-task-id"),
SnapshotTaskDetail: &ec2types.SnapshotTaskDetail{
SnapshotId: &m.snapshotId,
Status: aws.String("completed"),
},
},
},
}, nil
}
func (m *ec2mock) ImportSnapshot(ctx context.Context, input *ec2.ImportSnapshotInput, optfns ...func(*ec2.Options)) (*ec2.ImportSnapshotOutput, error) {
m.calledFn["ImportSnapshot"] += 1
return &ec2.ImportSnapshotOutput{
ImportTaskId: aws.String("import-task-id"),
}, nil
}
func (m *ec2mock) ModifySnapshotAttribute(ctx context.Context, input *ec2.ModifySnapshotAttributeInput, optfns ...func(*ec2.Options)) (*ec2.ModifySnapshotAttributeOutput, error) {
m.calledFn["ModifySnapshotAttribute"] += 1
return nil, nil
}
func (m *ec2mock) CreateTags(ctx context.Context, input *ec2.CreateTagsInput, optfns ...func(*ec2.Options)) (*ec2.CreateTagsOutput, error) {
m.calledFn["CreateTags"] += 1
return nil, nil
}

View file

@ -1,11 +1,15 @@
package awscloud
package awscloud_test
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/osbuild/osbuild-composer/internal/cloud/awscloud"
)
func TestSecureInstanceUserData(t *testing.T) {
func TestSIUserData(t *testing.T) {
type testCase struct {
CloudWatchGroup string
Hostname string
@ -58,10 +62,23 @@ write_files:
for idx, tc := range testCases {
t.Run(fmt.Sprintf("Test case %d", idx), func(t *testing.T) {
userData := SecureInstanceUserData(tc.CloudWatchGroup, tc.Hostname)
userData := awscloud.SecureInstanceUserData(tc.CloudWatchGroup, tc.Hostname)
if userData != tc.ExpectedUserData {
t.Errorf("Expected: %s, got: %s", tc.ExpectedUserData, userData)
}
})
}
}
func TestSIRunSecureInstance(t *testing.T) {
m := newEc2Mock(t)
aws := awscloud.NewForTest(m, &ec2imdsmock{t, "instance-id", "region1"}, nil, nil, nil)
require.NotNil(t, aws)
si, err := aws.RunSecureInstance("iam-profile", "key-name", "cw-group", "hostname")
require.NoError(t, err)
require.NotNil(t, si)
require.Equal(t, 1, m.calledFn["CreateFleet"])
require.Equal(t, 1, m.calledFn["CreateSecurityGroup"])
require.Equal(t, 1, m.calledFn["CreateLaunchTemplate"])
}