diff --git a/data/data/install.openshift.io_installconfigs.yaml b/data/data/install.openshift.io_installconfigs.yaml index 1978d48905..db5ae9c7a4 100644 --- a/data/data/install.openshift.io_installconfigs.yaml +++ b/data/data/install.openshift.io_installconfigs.yaml @@ -249,6 +249,20 @@ spec: (GiB). minimum: 0 type: integer + throughput: + description: |- + Throughput to provision in MiB/s supported for the volume type. Not applicable to all types. + + This parameter is valid only for gp3 volumes. + Valid Range: Minimum value of 125. Maximum value of 2000. + + When omitted, this means no opinion, and the platform is left to + choose a reasonable default, which is subject to change over time. + The current default is 125. + format: int32 + maximum: 2000 + minimum: 125 + type: integer type: description: Type defines the type of the volume. type: string @@ -1795,6 +1809,20 @@ spec: gibibytes (GiB). minimum: 0 type: integer + throughput: + description: |- + Throughput to provision in MiB/s supported for the volume type. Not applicable to all types. + + This parameter is valid only for gp3 volumes. + Valid Range: Minimum value of 125. Maximum value of 2000. + + When omitted, this means no opinion, and the platform is left to + choose a reasonable default, which is subject to change over time. + The current default is 125. + format: int32 + maximum: 2000 + minimum: 125 + type: integer type: description: Type defines the type of the volume. type: string @@ -3281,6 +3309,20 @@ spec: (GiB). minimum: 0 type: integer + throughput: + description: |- + Throughput to provision in MiB/s supported for the volume type. Not applicable to all types. + + This parameter is valid only for gp3 volumes. + Valid Range: Minimum value of 125. Maximum value of 2000. + + When omitted, this means no opinion, and the platform is left to + choose a reasonable default, which is subject to change over time. + The current default is 125. + format: int32 + maximum: 2000 + minimum: 125 + type: integer type: description: Type defines the type of the volume. type: string @@ -4960,6 +5002,20 @@ spec: (GiB). minimum: 0 type: integer + throughput: + description: |- + Throughput to provision in MiB/s supported for the volume type. Not applicable to all types. + + This parameter is valid only for gp3 volumes. + Valid Range: Minimum value of 125. Maximum value of 2000. + + When omitted, this means no opinion, and the platform is left to + choose a reasonable default, which is subject to change over time. + The current default is 125. + format: int32 + maximum: 2000 + minimum: 125 + type: integer type: description: Type defines the type of the volume. type: string diff --git a/docs/user/aws/customization.md b/docs/user/aws/customization.md index 2bf3fd6603..cbb1d3ac23 100644 --- a/docs/user/aws/customization.md +++ b/docs/user/aws/customization.md @@ -18,6 +18,8 @@ Beyond the [platform-agnostic `install-config.yaml` properties](../customization * `rootVolume` (optional object): Defines the root volume for EC2 instances in the machine pool. * `iops` (optional integer): The amount of provisioned [IOPS][volume-iops]. This is only valid for `type` `io1`. + * `throughput` (optional integer): The amount of throughput in MiB/s [Throughput Performance][volume-throughput]. + This is only valid for `type` `gp3`. * `size` (optional integer): Size of the root volume in gibibytes (GiB). * `type` (optional string): The [type of volume][volume-type]. * `kmsKeyARN` (optional string): The [ARN of KMS key][kms-key] that should be used to encrypt the EBS volume. @@ -119,4 +121,5 @@ sshKey: ssh-ed25519 AAAA... [kms-key-default]: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetEbsDefaultKmsKeyId.html [kms-key]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html [volume-iops]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-io-characteristics.html +[volume-throughput]: https://docs.aws.amazon.com/ebs/latest/userguide/general-purpose.html#gp3-ebs-volume-type [volume-type]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html diff --git a/pkg/asset/machines/aws/awsmachines.go b/pkg/asset/machines/aws/awsmachines.go index 372a6a44a6..b36e72c8d1 100644 --- a/pkg/asset/machines/aws/awsmachines.go +++ b/pkg/asset/machines/aws/awsmachines.go @@ -118,6 +118,10 @@ func GenerateMachines(clusterID string, in *MachineInput) ([]*asset.RuntimeFile, } awsMachine.SetGroupVersionKind(capa.GroupVersion.WithKind("AWSMachine")) + if throughput := mpool.EC2RootVolume.Throughput; throughput != nil { + awsMachine.Spec.RootVolume.Throughput = ptr.To(int64(*throughput)) + } + if in.Role == "bootstrap" { awsMachine.Name = capiutils.GenerateBoostrapMachineName(clusterID) awsMachine.Labels["install.openshift.io/bootstrap"] = "" diff --git a/pkg/asset/machines/aws/awsmachines_test.go b/pkg/asset/machines/aws/awsmachines_test.go index c1766555ba..79b562e720 100644 --- a/pkg/asset/machines/aws/awsmachines_test.go +++ b/pkg/asset/machines/aws/awsmachines_test.go @@ -227,8 +227,10 @@ func TestGenerateMachines(t *testing.T) { fmt.Sprintf("%s-subnet-public-%s", stubClusterID, machineZone), }}}, }, - SSHKeyName: ptr.To(""), - RootVolume: &capa.Volume{Encrypted: ptr.To(true)}, + SSHKeyName: ptr.To(""), + RootVolume: &capa.Volume{ + Encrypted: ptr.To(true), + }, UncompressedUserData: ptr.To(true), Ignition: &capa.Ignition{ StorageType: capa.IgnitionStorageTypeOptionUnencryptedUserData, diff --git a/pkg/asset/machines/aws/machines.go b/pkg/asset/machines/aws/machines.go index b5519bea9c..a46fb98d9c 100644 --- a/pkg/asset/machines/aws/machines.go +++ b/pkg/asset/machines/aws/machines.go @@ -242,11 +242,12 @@ func provider(in *machineProviderInput) (*machineapi.AWSMachineProviderConfig, e BlockDevices: []machineapi.BlockDeviceMappingSpec{ { EBS: &machineapi.EBSBlockDeviceSpec{ - VolumeType: pointer.String(in.root.Type), - VolumeSize: pointer.Int64(int64(in.root.Size)), - Iops: pointer.Int64(int64(in.root.IOPS)), - Encrypted: pointer.Bool(true), - KMSKey: machineapi.AWSResourceReference{ARN: pointer.String(in.root.KMSKeyARN)}, + VolumeType: pointer.String(in.root.Type), + VolumeSize: pointer.Int64(int64(in.root.Size)), + Iops: pointer.Int64(int64(in.root.IOPS)), + ThroughputMib: in.root.Throughput, + Encrypted: pointer.Bool(true), + KMSKey: machineapi.AWSResourceReference{ARN: pointer.String(in.root.KMSKeyARN)}, }, }, }, diff --git a/pkg/types/aws/machinepool.go b/pkg/types/aws/machinepool.go index 3829c96c9b..7dc0ed540f 100644 --- a/pkg/types/aws/machinepool.go +++ b/pkg/types/aws/machinepool.go @@ -79,6 +79,9 @@ func (a *MachinePool) Set(required *MachinePool) { if required.EC2RootVolume.IOPS != 0 { a.EC2RootVolume.IOPS = required.EC2RootVolume.IOPS } + if required.EC2RootVolume.Throughput != nil { + a.EC2RootVolume.Throughput = required.EC2RootVolume.Throughput + } if required.EC2RootVolume.Size != 0 { a.EC2RootVolume.Size = required.EC2RootVolume.Size } @@ -119,6 +122,20 @@ type EC2RootVolume struct { // +optional IOPS int `json:"iops"` + // Throughput to provision in MiB/s supported for the volume type. Not applicable to all types. + // + // This parameter is valid only for gp3 volumes. + // Valid Range: Minimum value of 125. Maximum value of 2000. + // + // When omitted, this means no opinion, and the platform is left to + // choose a reasonable default, which is subject to change over time. + // The current default is 125. + // + // +kubebuilder:validation:Minimum:=125 + // +kubebuilder:validation:Maximum:=2000 + // +optional + Throughput *int32 `json:"throughput,omitempty"` + // Size defines the size of the volume in gibibytes (GiB). // // +kubebuilder:validation:Minimum=0 diff --git a/pkg/types/aws/validation/machinepool.go b/pkg/types/aws/validation/machinepool.go index f4f6a21cec..2579e83d62 100644 --- a/pkg/types/aws/validation/machinepool.go +++ b/pkg/types/aws/validation/machinepool.go @@ -51,6 +51,7 @@ func ValidateMachinePool(platform *aws.Platform, p *aws.MachinePool, fldPath *fi if p.EC2RootVolume.Type != "" { allErrs = append(allErrs, validateVolumeSize(p, fldPath)...) allErrs = append(allErrs, validateIOPS(p, fldPath)...) + allErrs = append(allErrs, validateThroughput(p, fldPath)...) } if p.EC2Metadata.Authentication != "" && !validMetadataAuthValues.Has(p.EC2Metadata.Authentication) { @@ -114,6 +115,28 @@ func validateIOPS(p *aws.MachinePool, fldPath *field.Path) field.ErrorList { return allErrs } +func validateThroughput(p *aws.MachinePool, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if p.EC2RootVolume.Throughput == nil { + return allErrs + } + + volumeType := strings.ToLower(p.EC2RootVolume.Type) + throughput := *p.EC2RootVolume.Throughput + + switch volumeType { + case "gp3": + if throughput < 125 || throughput > 2000 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("throughput"), throughput, "throughput must be between 125 MiB/s and 2000 MiB/s")) + } + default: + allErrs = append(allErrs, field.Invalid(fldPath.Child("throughput"), throughput, fmt.Sprintf("throughput not supported for type %s", volumeType))) + } + + return allErrs +} + // ValidateAMIID check the AMI ID is set for a machine pool. func ValidateAMIID(platform *aws.Platform, p *aws.MachinePool, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} diff --git a/pkg/types/aws/validation/machinepool_test.go b/pkg/types/aws/validation/machinepool_test.go index d8741d720e..23ede83190 100644 --- a/pkg/types/aws/validation/machinepool_test.go +++ b/pkg/types/aws/validation/machinepool_test.go @@ -129,6 +129,80 @@ func TestValidateMachinePool(t *testing.T) { }, expected: `^test-path\.authentication: Invalid value: \"foobarbaz\": must be either Required or Optional$`, }, + { + name: "valid root volume throughput, within allowed range", + pool: &aws.MachinePool{ + EC2RootVolume: aws.EC2RootVolume{ + Type: "gp3", + Size: 100, + Throughput: ptr.To(int32(1200)), + }, + }, + }, + { + name: "valid root volume throughput, nil or unspecified", + pool: &aws.MachinePool{ + EC2RootVolume: aws.EC2RootVolume{ + Type: "gp3", + Size: 100, + }, + }, + }, + { + name: "invalid root volume throughput, below minimum", + pool: &aws.MachinePool{ + EC2RootVolume: aws.EC2RootVolume{ + Type: "gp3", + Size: 100, + Throughput: ptr.To(int32(124)), + }, + }, + expected: `^test-path\.throughput: Invalid value: 124: throughput must be between 125 MiB/s and 2000 MiB/s$`, + }, + { + name: "invalid root volume throughput, above maximum", + pool: &aws.MachinePool{ + EC2RootVolume: aws.EC2RootVolume{ + Type: "gp3", + Size: 100, + Throughput: ptr.To(int32(2001)), + }, + }, + expected: `^test-path\.throughput: Invalid value: 2001: throughput must be between 125 MiB/s and 2000 MiB/s$`, + }, + { + name: "invalid root volume throughput, zero", + pool: &aws.MachinePool{ + EC2RootVolume: aws.EC2RootVolume{ + Type: "gp3", + Size: 100, + Throughput: ptr.To(int32(0)), + }, + }, + expected: `^test-path\.throughput: Invalid value: 0: throughput must be between 125 MiB/s and 2000 MiB/s$`, + }, + { + name: "invalid root volume throughput, negative", + pool: &aws.MachinePool{ + EC2RootVolume: aws.EC2RootVolume{ + Type: "gp3", + Size: 100, + Throughput: ptr.To(int32(-100)), + }, + }, + expected: `^test-path\.throughput: Invalid value: -100: throughput must be between 125 MiB/s and 2000 MiB/s$`, + }, + { + name: "invalid root volume throughput, unsupported volume type", + pool: &aws.MachinePool{ + EC2RootVolume: aws.EC2RootVolume{ + Type: "gp2", + Size: 100, + Throughput: ptr.To(int32(125)), + }, + }, + expected: `^test-path\.throughput: Invalid value: 125: throughput not supported for type gp2$`, + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { diff --git a/pkg/types/aws/zz_generated.deepcopy.go b/pkg/types/aws/zz_generated.deepcopy.go index fd19e02a2b..b0612b0e7a 100644 --- a/pkg/types/aws/zz_generated.deepcopy.go +++ b/pkg/types/aws/zz_generated.deepcopy.go @@ -45,6 +45,11 @@ func (in *EC2Metadata) DeepCopy() *EC2Metadata { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EC2RootVolume) DeepCopyInto(out *EC2RootVolume) { *out = *in + if in.Throughput != nil { + in, out := &in.Throughput, &out.Throughput + *out = new(int32) + **out = **in + } return } @@ -66,7 +71,7 @@ func (in *MachinePool) DeepCopyInto(out *MachinePool) { *out = make([]string, len(*in)) copy(*out, *in) } - out.EC2RootVolume = in.EC2RootVolume + in.EC2RootVolume.DeepCopyInto(&out.EC2RootVolume) out.EC2Metadata = in.EC2Metadata if in.AdditionalSecurityGroupIDs != nil { in, out := &in.AdditionalSecurityGroupIDs, &out.AdditionalSecurityGroupIDs