diff --git a/pkg/types/aws/validation/machinepool.go b/pkg/types/aws/validation/machinepool.go index e869d3e1f2..4ba81505ba 100644 --- a/pkg/types/aws/validation/machinepool.go +++ b/pkg/types/aws/validation/machinepool.go @@ -17,7 +17,7 @@ var ( types.ArchitectureARM64: true, } - // validArchitectureValues lists the supported arches for AWS + // validArchitectureValues lists the supported arches for AWS. validArchitectureValues = func() []string { v := make([]string, 0, len(validArchitectures)) for m := range validArchitectures { @@ -39,6 +39,14 @@ var ( // We set a user limit of 10 and reserve 6 for use by OpenShift. const maxUserSecurityGroupsCount = 10 +// maxThroughputIopsRatio is the maximum allowed ratio of throughput (MiBps) to IOPS for gp3 volumes. +// AWS constraint: throughput (MiBps) / iops <= 0.25 (maximum 0.25 MiBps per iops). +const maxThroughputIopsRatio = 0.25 + +// gp3DefaultIOPS is the default IOPS value for gp3 volumes when not explicitly set. +// According to AWS documentation, gp3 volumes have a baseline of 3,000 IOPS. +const gp3DefaultIOPS int32 = 3000 + // ValidateMachinePool checks that the specified machine pool is valid. func ValidateMachinePool(platform *aws.Platform, p *aws.MachinePool, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} @@ -129,6 +137,31 @@ func validateThroughput(p *aws.MachinePool, fldPath *field.Path) field.ErrorList 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")) + return allErrs + } + // AWS constraint: throughput (MiBps) / iops <= 0.25 (maximum 0.25 MiBps per iops) + // When iops is 0 or omitted, AWS defaults to 3000 iops + // Validate that the throughput/iops ratio does not exceed the maximum allowed ratio + iops := int32(p.EC2RootVolume.IOPS) + if iops == 0 { + // Use AWS default of 3000 iops when iops is not set + iops = gp3DefaultIOPS + } + // Use integer comparison to avoid floating point precision issues + // throughput / iops <= 0.25 is equivalent to throughput * 4 <= iops + if throughput*4 > iops { + // Calculate minimum required iops: iops >= throughput / 0.25 + // Round up to nearest 100 for safety + calculatedIOPS := ((throughput * 4) + 99) / 100 * 100 + // According to AWS documentation, gp3 volumes have a baseline of 3,000 IOPS + minIOPS := max(calculatedIOPS, gp3DefaultIOPS) + // Calculate ratio for error message display + ratio := float32(throughput) / float32(iops) + allErrs = append(allErrs, field.Invalid( + fldPath.Child("throughput"), + throughput, + fmt.Sprintf("throughput (MiBps) to iops ratio of %.6f is too high; maximum is %.6f MiBps per iops. When iops is not set, AWS defaults to %d iops. Please set iops to at least %d to satisfy the constraint", ratio, maxThroughputIopsRatio, gp3DefaultIOPS, minIOPS), + )) } default: allErrs = append(allErrs, field.Invalid(fldPath.Child("throughput"), throughput, fmt.Sprintf("throughput not supported for type %s", volumeType))) diff --git a/pkg/types/aws/validation/machinepool_test.go b/pkg/types/aws/validation/machinepool_test.go index cb54059a80..ec0a9bbb27 100644 --- a/pkg/types/aws/validation/machinepool_test.go +++ b/pkg/types/aws/validation/machinepool_test.go @@ -74,7 +74,7 @@ func TestValidateMachinePool(t *testing.T) { IOPS: 10000, }, }, - expected: fmt.Sprintf("test-path.iops: Invalid value: 10000: iops not supported for type gp2"), + expected: "test-path.iops: Invalid value: 10000: iops not supported for type gp2", }, { name: "invalid zone", @@ -96,7 +96,7 @@ func TestValidateMachinePool(t *testing.T) { Size: 128, }, }, - expected: fmt.Sprintf("test-path.type: Invalid value: \"bad-volume-type\": failed to find volume type bad-volume-type"), + expected: "test-path.type: Invalid value: \"bad-volume-type\": failed to find volume type bad-volume-type", }, { name: "invalid volume size using negative", @@ -107,7 +107,7 @@ func TestValidateMachinePool(t *testing.T) { IOPS: 10000, }, }, - expected: fmt.Sprintf("test-path.size: Invalid value: -1: volume size value must be a positive number"), + expected: "test-path.size: Invalid value: -1: volume size value must be a positive number", }, { name: "invalid metadata auth option", @@ -119,15 +119,49 @@ 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", + name: "valid root volume throughput with sufficient iops", pool: &aws.MachinePool{ EC2RootVolume: aws.EC2RootVolume{ Type: "gp3", Size: 100, Throughput: ptr.To(int32(1200)), + IOPS: 4800, // 1200 / 4800 = 0.25, which is the maximum allowed ratio }, }, }, + { + name: "valid root volume throughput with default iops (within ratio)", + pool: &aws.MachinePool{ + EC2RootVolume: aws.EC2RootVolume{ + Type: "gp3", + Size: 100, + Throughput: ptr.To(int32(750)), // 750 / 3000 = 0.25, which is the maximum allowed ratio + }, + }, + }, + { + name: "invalid root volume throughput, exceeds ratio with default iops", + pool: &aws.MachinePool{ + EC2RootVolume: aws.EC2RootVolume{ + Type: "gp3", + Size: 100, + Throughput: ptr.To(int32(1200)), // 1200 / 3000 = 0.4 > 0.25 + }, + }, + expected: `^test-path\.throughput: Invalid value: 1200: throughput \(MiBps\) to iops ratio of 0\.400000 is too high; maximum is 0\.250000 MiBps per iops\. When iops is not set, AWS defaults to 3000 iops\. Please set iops to at least 4800 to satisfy the constraint$`, + }, + { + name: "invalid root volume throughput, exceeds ratio with explicit iops", + pool: &aws.MachinePool{ + EC2RootVolume: aws.EC2RootVolume{ + Type: "gp3", + Size: 100, + Throughput: ptr.To(int32(1000)), + IOPS: 3000, // 1000 / 3000 = 0.333 > 0.25 + }, + }, + expected: `^test-path\.throughput: Invalid value: 1000: throughput \(MiBps\) to iops ratio of 0\.333333 is too high; maximum is 0\.250000 MiBps per iops\. When iops is not set, AWS defaults to 3000 iops\. Please set iops to at least 4000 to satisfy the constraint$`, + }, { name: "valid root volume throughput, nil or unspecified", pool: &aws.MachinePool{