Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions pkg/asset/installconfig/aws/awserrors.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
const (
AccessDeniedException = "AccessDeniedException"
NoSuchResourceException = "NoSuchResourceException"
InvalidInstanceType = "InvalidInstanceType"
)

// IsUnauthorized checks if the error is due to lacking permissions.
Expand All @@ -29,6 +30,19 @@ func IsUnauthorized(err error) bool {
return false
}

// IsInvalidInstanceType returns true if the error is an AWS InvalidInstanceType error,
// indicating the requested instance type does not exist in the region.
func IsInvalidInstanceType(err error) bool {
if err == nil {
return false
}
var apiErr smithy.APIError
if errors.As(err, &apiErr) {
return apiErr.ErrorCode() == InvalidInstanceType
}
return false
}

// IsHTTPForbidden returns true if and only if the error is an HTTP
// 403 error from the AWS API.
func IsHTTPForbidden(err error) bool {
Expand Down
62 changes: 32 additions & 30 deletions pkg/asset/installconfig/aws/instancetypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
)

// Networking describes the network settings for an instance type.
Expand All @@ -24,41 +25,42 @@ type InstanceType struct {
Features []string
}

// instanceTypes retrieves a list of instance types for the given region.
func instanceTypes(ctx context.Context, client *ec2.Client) (map[string]InstanceType, error) {
types := map[string]InstanceType{}

paginator := ec2.NewDescribeInstanceTypesPaginator(client, &ec2.DescribeInstanceTypesInput{})
for paginator.HasMorePages() {
page, err := paginator.NextPage(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list instance types: %w", err)
}

for _, sdkTypeInfo := range page.InstanceTypes {
typeInfo := InstanceType{
DefaultVCpus: int64(aws.ToInt32(sdkTypeInfo.VCpuInfo.DefaultVCpus)),
MemInMiB: aws.ToInt64(sdkTypeInfo.MemoryInfo.SizeInMiB),
Hypervisor: string(sdkTypeInfo.Hypervisor),
}
// getInstanceType returns metadata for the named instance type. If the type does
// not exist in the configured region.
func getInstanceType(ctx context.Context, client *ec2.Client, instanceType string) (InstanceType, error) {
out, err := client.DescribeInstanceTypes(ctx, &ec2.DescribeInstanceTypesInput{
InstanceTypes: []ec2types.InstanceType{ec2types.InstanceType(instanceType)},
})
if err != nil {
return InstanceType{}, fmt.Errorf("failed to get instance type %s details: %w", instanceType, err)
}

for _, arch := range sdkTypeInfo.ProcessorInfo.SupportedArchitectures {
typeInfo.Arches = append(typeInfo.Arches, string(arch))
}
// A nonexistent type is reported as an InvalidInstanceType error above, so an
// empty result here is an unexpected API response rather than a missing type.
if len(out.InstanceTypes) == 0 {
return InstanceType{}, fmt.Errorf("unexpected empty response describing instance type %s", instanceType)
}

if netInfo := sdkTypeInfo.NetworkInfo; netInfo != nil {
typeInfo.Networking = Networking{
IPv6Supported: aws.ToBool(netInfo.Ipv6Supported),
}
}
sdkTypeInfo := out.InstanceTypes[0]
typeInfo := InstanceType{
DefaultVCpus: int64(aws.ToInt32(sdkTypeInfo.VCpuInfo.DefaultVCpus)),
MemInMiB: aws.ToInt64(sdkTypeInfo.MemoryInfo.SizeInMiB),
Hypervisor: string(sdkTypeInfo.Hypervisor),
}

for _, features := range sdkTypeInfo.ProcessorInfo.SupportedFeatures {
typeInfo.Features = append(typeInfo.Features, string(features))
}
for _, arch := range sdkTypeInfo.ProcessorInfo.SupportedArchitectures {
typeInfo.Arches = append(typeInfo.Arches, string(arch))
}

types[string(sdkTypeInfo.InstanceType)] = typeInfo
if netInfo := sdkTypeInfo.NetworkInfo; netInfo != nil {
typeInfo.Networking = Networking{
IPv6Supported: aws.ToBool(netInfo.Ipv6Supported),
}
}

return types, nil
for _, features := range sdkTypeInfo.ProcessorInfo.SupportedFeatures {
typeInfo.Features = append(typeInfo.Features, string(features))
}

return typeInfo, nil
}
31 changes: 19 additions & 12 deletions pkg/asset/installconfig/aws/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,24 +352,31 @@ func (m *Metadata) populateVPC(ctx context.Context) error {
return err
}

// InstanceTypes retrieves instance type metadata indexed by InstanceType for the configured region.
func (m *Metadata) InstanceTypes(ctx context.Context) (map[string]InstanceType, error) {
// InstanceType returns metadata for the named instance type.
func (m *Metadata) InstanceType(ctx context.Context, instanceType string) (InstanceType, error) {
m.mutex.Lock()
defer m.mutex.Unlock()

if len(m.instanceTypes) == 0 {
client, err := m.EC2Client(ctx)
if err != nil {
return nil, err
}
if t, ok := m.instanceTypes[instanceType]; ok {
return t, nil
}

m.instanceTypes, err = instanceTypes(ctx, client)
if err != nil {
return nil, fmt.Errorf("error listing instance types: %w", err)
}
client, err := m.EC2Client(ctx)
if err != nil {
return InstanceType{}, err
}

t, err := getInstanceType(ctx, client, instanceType)
if err != nil {
return InstanceType{}, err
}

if m.instanceTypes == nil {
m.instanceTypes = map[string]InstanceType{}
}
m.instanceTypes[instanceType] = t

return m.instanceTypes, nil
return t, nil
}

// Images retrieves image metadata for the specified AMI ID.
Expand Down
29 changes: 13 additions & 16 deletions pkg/asset/installconfig/aws/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -456,11 +456,14 @@ func validateMachinePool(ctx context.Context, meta *Metadata, fldPath *field.Pat
}
}
if pool.InstanceType != "" {
instanceTypes, err := meta.InstanceTypes(ctx)
if err != nil {
typeMeta, err := meta.InstanceType(ctx, pool.InstanceType)
switch {
case IsInvalidInstanceType(err):
errMsg := fmt.Sprintf("instance type %s not found", pool.InstanceType)
allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), pool.InstanceType, errMsg))
case err != nil:
return append(allErrs, field.InternalError(fldPath, err))
}
if typeMeta, ok := instanceTypes[pool.InstanceType]; ok {
default:
if typeMeta.DefaultVCpus < req.minimumVCpus {
errMsg := fmt.Sprintf("instance type does not meet minimum resource requirements of %d vCPUs", req.minimumVCpus)
allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), pool.InstanceType, errMsg))
Expand Down Expand Up @@ -489,9 +492,6 @@ func validateMachinePool(ctx context.Context, meta *Metadata, fldPath *field.Pat
allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), pool.InstanceType, errMsg))
}
}
} else {
errMsg := fmt.Sprintf("instance type %s not found", pool.InstanceType)
allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), pool.InstanceType, errMsg))
}
}

Expand Down Expand Up @@ -1173,19 +1173,16 @@ func validateInstanceTypeForSEVSNP(ctx context.Context, meta *Metadata, fldPath
return allErrs
}

// Fetch instance types metadata
instanceTypes, err := meta.InstanceTypes(ctx)
// Fetch instance type metadata
typeMeta, err := meta.InstanceType(ctx, pool.InstanceType)
if err != nil {
// The instance type is not found; already caught in validateMachinePool.
if IsInvalidInstanceType(err) {
return allErrs
}
return append(allErrs, field.InternalError(fldPath, err))
}

// Validate the specified instance type supports SEV-SNP
// If the instance type is not found, it's already caught in validateMachinePool
typeMeta, ok := instanceTypes[pool.InstanceType]
if !ok {
return allErrs
}

if !slices.Contains(typeMeta.Features, string(ec2types.SupportedAdditionalProcessorFeatureAmdSevSnp)) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), pool.InstanceType, "specified instance type in the specified region doesn't support amd-sev-snp"))
}
Expand Down
34 changes: 34 additions & 0 deletions pkg/asset/installconfig/aws/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import (
"strings"
"testing"

awssdk "github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials"
"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/route53"
route53types "github.com/aws/aws-sdk-go-v2/service/route53/types"
Expand Down Expand Up @@ -1803,8 +1806,38 @@ func TestValidate(t *testing.T) {
})
}

// Provide a mock EC2 client to avoid a live API call when looking up an instance type.
// The EC2 query API POSTs to the root path, so the request URL carries a trailing slash.
// httpmock matches responders by exact URL, so the endpoint includes the slash to match what the SDK sends.
//
// Instance types referenced by the tests are pre-seeded in Metadata.instanceTypes, so only
// unknown types reach the API. DescribeInstanceTypes returns an InvalidInstanceType error for a
// type that does not exist, which getInstanceType translates into an *InstanceTypeNotFoundError.
const mockEC2Endpoint = "https://ec2.mock.local/"
httpmock.RegisterResponder(http.MethodPost, mockEC2Endpoint, func(r *http.Request) (*http.Response, error) {
const invalidInstanceTypeResp = `<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Errors>
<Error>
<Code>InvalidInstanceType</Code>
<Message>The following supplied instance types do not exist</Message>
</Error>
</Errors>
<RequestID>req-mock</RequestID>
</Response>`
return httpmock.NewStringResponse(http.StatusBadRequest, invalidInstanceTypeResp), nil
})

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ec2Mock := ec2.New(ec2.Options{
Region: test.installConfig.Platform.AWS.Region,
Credentials: credentials.NewStaticCredentialsProvider("id", "secret", "token"),
BaseEndpoint: awssdk.String(mockEC2Endpoint),
// An HTTP client must be defined so that the SDK doesn't build
// its own transport, which escapes the mock.
HTTPClient: &http.Client{},
})
meta := &Metadata{
availabilityZones: test.availZones,
availableRegions: test.availRegions,
Expand All @@ -1821,6 +1854,7 @@ func TestValidate(t *testing.T) {
Hosts: test.hosts,
Region: test.installConfig.Platform.AWS.Region,
ProvidedSubnets: test.installConfig.Platform.AWS.VPC.Subnets,
ec2Client: ec2Mock,
}

if test.subnetsInVPC != nil {
Expand Down