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
36 changes: 36 additions & 0 deletions pkg/asset/installconfig/aws/images.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package aws

import (
"context"
"fmt"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
)

// ImageInfo holds metadata for an AMI.
type ImageInfo struct {
BootMode string
}

// images retrieves image metadata for the specified AMI ID in the given region.
func images(ctx context.Context, session *session.Session, region string, amiID string) (ImageInfo, error) {
client := ec2.New(session, aws.NewConfig().WithRegion(region))

imageOutput, err := client.DescribeImagesWithContext(ctx, &ec2.DescribeImagesInput{
ImageIds: []*string{aws.String(amiID)},
})
if err != nil {
return ImageInfo{}, fmt.Errorf("fetching images: %w", err)
}

if len(imageOutput.Images) == 0 {
return ImageInfo{}, fmt.Errorf("AMI %s not found", amiID)
}

image := imageOutput.Images[0]
return ImageInfo{
BootMode: aws.StringValue(image.BootMode),
}, nil
}
2 changes: 2 additions & 0 deletions pkg/asset/installconfig/aws/instancetypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type InstanceType struct {
DefaultVCpus int64
MemInMiB int64
Arches []string
Features []string
}

// instanceTypes retrieves a list of instance types for the given region.
Expand All @@ -29,6 +30,7 @@ func instanceTypes(ctx context.Context, session *session.Session, region string)
DefaultVCpus: aws.Int64Value(info.VCpuInfo.DefaultVCpus),
MemInMiB: aws.Int64Value(info.MemoryInfo.SizeInMiB),
Arches: aws.StringValueSlice(info.ProcessorInfo.SupportedArchitectures),
Features: aws.StringValueSlice(info.ProcessorInfo.SupportedFeatures),
}
}
return !lastPage
Expand Down
37 changes: 37 additions & 0 deletions pkg/asset/installconfig/aws/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Metadata struct {
vpcSubnets SubnetGroups
vpc VPC
instanceTypes map[string]InstanceType
images map[string]ImageInfo

Region string `json:"region,omitempty"`
ProvidedSubnets []typesaws.Subnet `json:"subnets,omitempty"`
Expand Down Expand Up @@ -390,3 +391,39 @@ func (m *Metadata) InstanceTypes(ctx context.Context) (map[string]InstanceType,

return m.instanceTypes, nil
}

// Images retrieves image metadata for the specified AMI ID.
func (m *Metadata) Images(ctx context.Context, amiID string) (ImageInfo, error) {
m.mutex.Lock()
defer m.mutex.Unlock()

if amiID == "" {
return ImageInfo{}, fmt.Errorf("AMI ID cannot be empty")
}

// Check if AMI is already cached
if imageInfo, ok := m.images[amiID]; ok {
return imageInfo, nil
}

// Fetch uncached AMI
session, err := m.unlockedSession(ctx)
if err != nil {
return ImageInfo{}, err
}

imageInfo, err := images(ctx, session, m.Region, amiID)
if err != nil {
return ImageInfo{}, fmt.Errorf("error fetching AMI metadata: %w", err)
}

// Initialize map if needed
if m.images == nil {
m.images = map[string]ImageInfo{}
}

// Add newly fetched image to cache
m.images[amiID] = imageInfo

return imageInfo, nil
}
79 changes: 79 additions & 0 deletions pkg/asset/installconfig/aws/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net"
"net/http"
"net/url"
"slices"
"sort"

ec2v2 "github.com/aws/aws-sdk-go-v2/service/ec2"
Expand Down Expand Up @@ -453,6 +454,10 @@ func validateMachinePool(ctx context.Context, meta *Metadata, fldPath *field.Pat
}
}

if pool.CPUOptions != nil {
allErrs = append(allErrs, validateCPUOptions(ctx, meta, fldPath, pool)...)
}

if len(pool.AdditionalSecurityGroupIDs) > 0 {
allErrs = append(allErrs, validateSecurityGroupIDs(ctx, meta, fldPath.Child("additionalSecurityGroupIDs"), platform, pool)...)
}
Expand Down Expand Up @@ -964,3 +969,77 @@ func validateInstanceProfile(ctx context.Context, meta *Metadata, fldPath *field

return nil
}

func validateCPUOptions(ctx context.Context, meta *Metadata, fldPath *field.Path, pool *awstypes.MachinePool) field.ErrorList {
allErrs := field.ErrorList{}
cpuOpts := pool.CPUOptions

// Early return if no CPU options specified
if cpuOpts == nil {
return allErrs
}

// See sev-snp support requirements: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/sev-snp.html#snp-requirements
if cpuOpts.ConfidentialCompute != nil && *cpuOpts.ConfidentialCompute == awstypes.ConfidentialComputePolicySEVSNP {
// Validate AMI boot mode for SEV-SNP
allErrs = append(allErrs, validateAMIBootMode(ctx, meta, fldPath, pool)...)

// Validate instance type for SEV-SNP
allErrs = append(allErrs, validateInstanceTypeForSEVSNP(ctx, meta, fldPath, pool)...)
}

return allErrs
}

func validateInstanceTypeForSEVSNP(ctx context.Context, meta *Metadata, fldPath *field.Path, pool *awstypes.MachinePool) field.ErrorList {
allErrs := field.ErrorList{}

// Warn if using default instance type
if pool.InstanceType == "" {
logrus.Warnf("AMD SEV-SNP confidential computing is enabled for %s but no instance type is specified. The default instance type may not support amd-sev-snp", fldPath)
return allErrs
}

// Fetch instance types metadata
instanceTypes, err := meta.InstanceTypes(ctx)
if err != nil {
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, ec2.SupportedAdditionalProcessorFeatureAmdSevSnp) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), pool.InstanceType, "specified instance type in the specified region doesn't support amd-sev-snp"))
}

return allErrs
}

func validateAMIBootMode(ctx context.Context, meta *Metadata, fldPath *field.Path, pool *awstypes.MachinePool) field.ErrorList {
allErrs := field.ErrorList{}

amiID := pool.AMIID
if amiID == "" {
// Warn when using default AMI with SEV-SNP
logrus.Warnf("AMD SEV-SNP confidential computing is enabled for %s but no custom AMI is specified. The default RHCOS AMI may not have UEFI boot mode enabled", fldPath)
return allErrs
}

// Get image metadata
imageInfo, err := meta.Images(ctx, amiID)
if err != nil {
return append(allErrs, field.InternalError(fldPath.Child("amiID"), fmt.Errorf("unable to retrieve AMI metadata: %w", err)))
}

// Check if boot mode supports UEFI
if imageInfo.BootMode != ec2.BootModeValuesUefi && imageInfo.BootMode != ec2.BootModeValuesUefiPreferred {
allErrs = append(allErrs, field.Invalid(fldPath.Child("amiID"), amiID, fmt.Sprintf("AMI boot mode must be 'uefi' or 'uefi-preferred' when using AMD SEV-SNP confidential computing, got '%s'", imageInfo.BootMode)))
}

return allErrs
}
Loading