diff --git a/pkg/asset/machines/aws/awsmachines.go b/pkg/asset/machines/aws/awsmachines.go index bb33f8a6fc..372a6a44a6 100644 --- a/pkg/asset/machines/aws/awsmachines.go +++ b/pkg/asset/machines/aws/awsmachines.go @@ -139,6 +139,16 @@ func GenerateMachines(clusterID string, in *MachineInput) ([]*asset.RuntimeFile, ) } + if mpool.CPUOptions != nil { + cpuOptions := capa.CPUOptions{} + + if mpool.CPUOptions.ConfidentialCompute != nil { + cpuOptions.ConfidentialCompute = capa.AWSConfidentialComputePolicy(*mpool.CPUOptions.ConfidentialCompute) + } + + awsMachine.Spec.CPUOptions = cpuOptions + } + result = append(result, &asset.RuntimeFile{ File: asset.File{Filename: fmt.Sprintf("10_inframachine_%s.yaml", awsMachine.Name)}, Object: awsMachine, diff --git a/pkg/asset/machines/aws/machines.go b/pkg/asset/machines/aws/machines.go index b0b21c531a..b5519bea9c 100644 --- a/pkg/asset/machines/aws/machines.go +++ b/pkg/asset/machines/aws/machines.go @@ -11,6 +11,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/utils/pointer" + "k8s.io/utils/ptr" v1 "github.com/openshift/api/config/v1" machinev1 "github.com/openshift/api/machine/v1" @@ -35,6 +36,7 @@ type machineProviderInput struct { userTags map[string]string publicSubnet bool securityGroupIDs []string + cpuOptions *awstypes.CPUOptions } // Machines returns a list of machines for a machinepool. @@ -77,6 +79,7 @@ func Machines(clusterID string, region string, subnets aws.SubnetsByZone, pool * userTags: userTags, publicSubnet: publicSubnet, securityGroupIDs: pool.Platform.AWS.AdditionalSecurityGroupIDs, + cpuOptions: mpool.CPUOptions, }) if err != nil { return nil, nil, errors.Wrap(err, "failed to create provider") @@ -291,6 +294,16 @@ func provider(in *machineProviderInput) (*machineapi.AWSMachineProviderConfig, e config.MetadataServiceOptions.Authentication = machineapi.MetadataServiceAuthentication(in.imds.Authentication) } + if in.cpuOptions != nil { + cpuOptions := machineapi.CPUOptions{} + + if in.cpuOptions.ConfidentialCompute != nil { + cpuOptions.ConfidentialCompute = ptr.To(machineapi.AWSConfidentialComputePolicy(*in.cpuOptions.ConfidentialCompute)) + } + + config.CPUOptions = &cpuOptions + } + return config, nil } diff --git a/pkg/asset/machines/aws/machinesets.go b/pkg/asset/machines/aws/machinesets.go index 0d51fcfeeb..35cc1f38e6 100644 --- a/pkg/asset/machines/aws/machinesets.go +++ b/pkg/asset/machines/aws/machinesets.go @@ -102,6 +102,7 @@ func MachineSets(in *MachineSetInput) ([]*machineapi.MachineSet, error) { userTags: in.InstallConfigPlatformAWS.UserTags, publicSubnet: publicSubnet, securityGroupIDs: in.Pool.Platform.AWS.AdditionalSecurityGroupIDs, + cpuOptions: mpool.CPUOptions, }) if err != nil { return nil, errors.Wrap(err, "failed to create provider") diff --git a/pkg/types/aws/machinepool.go b/pkg/types/aws/machinepool.go index 60ee8d977f..3829c96c9b 100644 --- a/pkg/types/aws/machinepool.go +++ b/pkg/types/aws/machinepool.go @@ -48,6 +48,14 @@ type MachinePool struct { // +kubebuilder:validation:MaxItems=10 // +optional AdditionalSecurityGroupIDs []string `json:"additionalSecurityGroupIDs,omitempty"` + + // CPUOptions defines CPU-related settings for the instance, including the confidential computing policy. + // When omitted, this means no opinion and the AWS platform is left to choose a reasonable default. + // More info: + // https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CpuOptionsRequest.html, + // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/cpu-options-supported-instances-values.html + // +optional + CPUOptions *CPUOptions `json:"cpuOptions,omitempty,omitzero"` } // Set sets the values from `required` to `a`. @@ -96,6 +104,10 @@ func (a *MachinePool) Set(required *MachinePool) { if len(required.AdditionalSecurityGroupIDs) > 0 { a.AdditionalSecurityGroupIDs = required.AdditionalSecurityGroupIDs } + + if required.CPUOptions != nil { + a.CPUOptions = required.CPUOptions + } } // EC2RootVolume defines the storage for an ec2 instance. @@ -135,3 +147,34 @@ type EC2Metadata struct { // +optional Authentication string `json:"authentication,omitempty"` } + +// ConfidentialComputePolicy represents the confidential compute configuration for the instance. +// +kubebuilder:validation:Enum=Disabled;AMDEncryptedVirtualizationNestedPaging +type ConfidentialComputePolicy string + +const ( + // ConfidentialComputePolicyDisabled disables confidential computing for the instance. + ConfidentialComputePolicyDisabled ConfidentialComputePolicy = "Disabled" + // ConfidentialComputePolicySEVSNP enables AMD SEV-SNP as the confidential computing technology for the instance. + ConfidentialComputePolicySEVSNP ConfidentialComputePolicy = "AMDEncryptedVirtualizationNestedPaging" +) + +// CPUOptions defines CPU-related settings for the instance, including the confidential computing policy. +// If provided, it must not be empty — at least one field must be set. +// +kubebuilder:validation:MinProperties=1 +type CPUOptions struct { + // ConfidentialCompute specifies whether confidential computing should be enabled for the instance, + // and, if so, which confidential computing technology to use. + // Valid values are: Disabled, AMDEncryptedVirtualizationNestedPaging and omitted. + // When set to Disabled, confidential computing will be disabled for the instance. + // When set to AMDEncryptedVirtualizationNestedPaging, AMD SEV-SNP will be used as the confidential computing technology for the instance. + // In this case, ensure the following conditions are met: + // 1) The selected instance type supports AMD SEV-SNP. + // 2) The selected AWS region supports AMD SEV-SNP. + // 3) The selected AMI supports AMD SEV-SNP. + // More details can be checked at https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/sev-snp.html + // When omitted, this means no opinion and the AWS platform is left to choose a reasonable default, + // which is subject to change without notice. The current default is Disabled. + // +optional + ConfidentialCompute *ConfidentialComputePolicy `json:"confidentialCompute,omitempty"` +} diff --git a/pkg/types/aws/validation/machinepool.go b/pkg/types/aws/validation/machinepool.go index b4db532fe5..f4f6a21cec 100644 --- a/pkg/types/aws/validation/machinepool.go +++ b/pkg/types/aws/validation/machinepool.go @@ -27,6 +27,11 @@ var ( }() validMetadataAuthValues = sets.NewString("Required", "Optional") + + validConfidentialComputePolicy = []aws.ConfidentialComputePolicy{ + aws.ConfidentialComputePolicyDisabled, + aws.ConfidentialComputePolicySEVSNP, + } ) // AWS has a limit of 16 security groups. See: @@ -53,6 +58,7 @@ func ValidateMachinePool(platform *aws.Platform, p *aws.MachinePool, fldPath *fi } allErrs = append(allErrs, validateSecurityGroups(platform, p, fldPath)...) + allErrs = append(allErrs, ValidateCPUOptions(p, fldPath)...) return allErrs } @@ -133,3 +139,41 @@ func ValidateMachinePoolArchitecture(pool *types.MachinePool, fldPath *field.Pat } return allErrs } + +// ValidateCPUOptions checks that valid CPU options are set for a machine pool. +func ValidateCPUOptions(p *aws.MachinePool, fldPath *field.Path) field.ErrorList { + if p.CPUOptions == nil { + return nil + } + + allErrs := field.ErrorList{} + + if *p.CPUOptions == (aws.CPUOptions{}) { + allErrs = append( + allErrs, + field.Invalid( + fldPath.Child("cpuOptions"), + "{}", + "At least one field must be set if cpuOptions is provided", + ), + ) + } + + if p.CPUOptions.ConfidentialCompute != nil { + switch *p.CPUOptions.ConfidentialCompute { + case aws.ConfidentialComputePolicyDisabled, aws.ConfidentialComputePolicySEVSNP: + // Valid values + default: + allErrs = append( + allErrs, + field.NotSupported( + fldPath.Child("confidentialCompute"), + p.CPUOptions.ConfidentialCompute, + validConfidentialComputePolicy, + ), + ) + } + } + + return allErrs +} diff --git a/pkg/types/aws/validation/machinepool_test.go b/pkg/types/aws/validation/machinepool_test.go index ae8794dfaa..d8741d720e 100644 --- a/pkg/types/aws/validation/machinepool_test.go +++ b/pkg/types/aws/validation/machinepool_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/ptr" "github.com/openshift/installer/pkg/types/aws" ) @@ -250,3 +251,57 @@ func Test_validateAMIID(t *testing.T) { }) } } + +func Test_validateCPUOptions(t *testing.T) { + cases := []struct { + name string + pool *aws.MachinePool + err string + }{{ + name: "confidential compute policy set to AMD SEV-SNP", + pool: &aws.MachinePool{ + CPUOptions: &aws.CPUOptions{ + ConfidentialCompute: ptr.To(aws.ConfidentialComputePolicySEVSNP), + }, + }, + }, { + name: "confidential compute disabled", + pool: &aws.MachinePool{ + CPUOptions: &aws.CPUOptions{ + ConfidentialCompute: ptr.To(aws.ConfidentialComputePolicyDisabled), + }, + }, + }, { + name: "empty confidential compute policy", + pool: &aws.MachinePool{ + CPUOptions: &aws.CPUOptions{ + ConfidentialCompute: ptr.To(aws.ConfidentialComputePolicy("")), + }, + }, + err: `^test-path.confidentialCompute: Unsupported value: "": supported values: "Disabled", "AMDEncryptedVirtualizationNestedPaging"$`, + }, { + name: "invalid confidential compute policy", + pool: &aws.MachinePool{ + CPUOptions: &aws.CPUOptions{ + ConfidentialCompute: ptr.To(aws.ConfidentialComputePolicy("invalid")), + }, + }, + err: `^test-path.confidentialCompute: Unsupported value: "invalid": supported values: "Disabled", "AMDEncryptedVirtualizationNestedPaging"$`, + }, { + name: "empty cpu options", + pool: &aws.MachinePool{ + CPUOptions: &aws.CPUOptions{}, + }, + err: `^test-path.cpuOptions: Invalid value: "{}": At least one field must be set if cpuOptions is provided$`, + }} + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + err := ValidateCPUOptions(tc.pool, field.NewPath("test-path")).ToAggregate() + if tc.err == "" { + assert.NoError(t, err) + } else { + assert.Regexp(t, tc.err, err) + } + }) + } +} diff --git a/pkg/types/aws/zz_generated.deepcopy.go b/pkg/types/aws/zz_generated.deepcopy.go index 57b5e8e08e..fd19e02a2b 100644 --- a/pkg/types/aws/zz_generated.deepcopy.go +++ b/pkg/types/aws/zz_generated.deepcopy.go @@ -5,6 +5,27 @@ package aws +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CPUOptions) DeepCopyInto(out *CPUOptions) { + *out = *in + if in.ConfidentialCompute != nil { + in, out := &in.ConfidentialCompute, &out.ConfidentialCompute + *out = new(ConfidentialComputePolicy) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CPUOptions. +func (in *CPUOptions) DeepCopy() *CPUOptions { + if in == nil { + return nil + } + out := new(CPUOptions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EC2Metadata) DeepCopyInto(out *EC2Metadata) { *out = *in @@ -52,6 +73,11 @@ func (in *MachinePool) DeepCopyInto(out *MachinePool) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.CPUOptions != nil { + in, out := &in.CPUOptions, &out.CPUOptions + *out = new(CPUOptions) + (*in).DeepCopyInto(*out) + } return }