diff --git a/spinifex/gateway/ec2/volume/CreateVolume.go b/spinifex/gateway/ec2/volume/CreateVolume.go index 2bf862195..5bab46d02 100644 --- a/spinifex/gateway/ec2/volume/CreateVolume.go +++ b/spinifex/gateway/ec2/volume/CreateVolume.go @@ -23,7 +23,7 @@ func ValidateCreateVolumeInput(input *ec2.CreateVolumeInput) error { } if input.VolumeType != nil && *input.VolumeType != "" && *input.VolumeType != "gp3" { - return errors.New(awserrors.ErrorInvalidParameterValue) + return errors.New(awserrors.ErrorUnknownVolumeType) } return nil diff --git a/spinifex/gateway/ec2/volume/CreateVolume_test.go b/spinifex/gateway/ec2/volume/CreateVolume_test.go index 2b3a96b18..1abe953e6 100644 --- a/spinifex/gateway/ec2/volume/CreateVolume_test.go +++ b/spinifex/gateway/ec2/volume/CreateVolume_test.go @@ -105,7 +105,7 @@ func TestValidateCreateVolumeInput(t *testing.T) { VolumeType: aws.String("io1"), }, wantErr: true, - errMsg: awserrors.ErrorInvalidParameterValue, + errMsg: awserrors.ErrorUnknownVolumeType, }, { name: "InvalidVolumeType_GP2", @@ -115,7 +115,7 @@ func TestValidateCreateVolumeInput(t *testing.T) { VolumeType: aws.String("gp2"), }, wantErr: true, - errMsg: awserrors.ErrorInvalidParameterValue, + errMsg: awserrors.ErrorUnknownVolumeType, }, { name: "ValidInput_EmptyVolumeType", diff --git a/spinifex/handlers/ec2/volume/service_impl.go b/spinifex/handlers/ec2/volume/service_impl.go index 3c11fdfdc..e5e5da02a 100644 --- a/spinifex/handlers/ec2/volume/service_impl.go +++ b/spinifex/handlers/ec2/volume/service_impl.go @@ -28,7 +28,13 @@ import ( "github.com/nats-io/nats.go" ) -const defaultGP3IOPS = 3000 +const ( + // gp3 IOPS envelope (AWS): 3000 baseline on any size, up to 500 IOPS/GiB, + // capped at 16000. + defaultGP3IOPS = 3000 + maxGP3IOPS = 16000 + gp3IOPSPerGiB = 500 +) // Ensure VolumeServiceImpl implements VolumeService var _ VolumeService = (*VolumeServiceImpl)(nil) @@ -92,7 +98,7 @@ func (s *VolumeServiceImpl) CreateVolume(input *ec2.CreateVolumeInput, accountID // Validate volume type: only gp3 supported (or empty defaults to gp3) if input.VolumeType != nil && *input.VolumeType != "" && *input.VolumeType != "gp3" { - return nil, errors.New(awserrors.ErrorInvalidParameterValue) + return nil, errors.New(awserrors.ErrorUnknownVolumeType) } volumeType := "gp3" @@ -141,7 +147,17 @@ func (s *VolumeServiceImpl) CreateVolume(input *ec2.CreateVolumeInput, accountID now := time.Now() volumeID := utils.GenerateResourceID("vol") + // Honor caller-supplied Iops for gp3, else the 3000 baseline. The ceiling is + // min(16000, 500*size) but never below the free baseline, so small volumes + // still get 3000. iops := defaultGP3IOPS + if input.Iops != nil { + iops = int(*input.Iops) + } + maxIOPS := min(max(int(size)*gp3IOPSPerGiB, defaultGP3IOPS), maxGP3IOPS) + if iops < defaultGP3IOPS || iops > maxIOPS { + return nil, errors.New(awserrors.ErrorInvalidParameterValue) + } slog.Info("CreateVolume", "volumeId", volumeID, "size", size, "type", volumeType, "az", *input.AvailabilityZone, "snapshotId", snapshotID) diff --git a/spinifex/handlers/ec2/volume/service_impl_test.go b/spinifex/handlers/ec2/volume/service_impl_test.go index a1cffcb8a..5d2d27a22 100644 --- a/spinifex/handlers/ec2/volume/service_impl_test.go +++ b/spinifex/handlers/ec2/volume/service_impl_test.go @@ -92,7 +92,7 @@ func TestCreateVolume_Validation(t *testing.T) { AvailabilityZone: aws.String("ap-southeast-2a"), VolumeType: aws.String("io1"), }, - wantErr: awserrors.ErrorInvalidParameterValue, + wantErr: awserrors.ErrorUnknownVolumeType, }, { name: "UnsupportedVolumeType_GP2", @@ -102,7 +102,7 @@ func TestCreateVolume_Validation(t *testing.T) { AvailabilityZone: aws.String("ap-southeast-2a"), VolumeType: aws.String("gp2"), }, - wantErr: awserrors.ErrorInvalidParameterValue, + wantErr: awserrors.ErrorUnknownVolumeType, }, { name: "UnsupportedVolumeType_ST1", @@ -112,6 +112,36 @@ func TestCreateVolume_Validation(t *testing.T) { AvailabilityZone: aws.String("ap-southeast-2a"), VolumeType: aws.String("st1"), }, + wantErr: awserrors.ErrorUnknownVolumeType, + }, + { + name: "Iops_BelowBaseline", + az: "ap-southeast-2a", + input: &ec2.CreateVolumeInput{ + Size: aws.Int64(80), + AvailabilityZone: aws.String("ap-southeast-2a"), + Iops: aws.Int64(2999), + }, + wantErr: awserrors.ErrorInvalidParameterValue, + }, + { + name: "Iops_AboveCeiling", + az: "ap-southeast-2a", + input: &ec2.CreateVolumeInput{ + Size: aws.Int64(80), + AvailabilityZone: aws.String("ap-southeast-2a"), + Iops: aws.Int64(16001), + }, + wantErr: awserrors.ErrorInvalidParameterValue, + }, + { + name: "Iops_AboveRatioForSmallVolume", + az: "ap-southeast-2a", + input: &ec2.CreateVolumeInput{ + Size: aws.Int64(10), + AvailabilityZone: aws.String("ap-southeast-2a"), + Iops: aws.Int64(6000), + }, wantErr: awserrors.ErrorInvalidParameterValue, }, { @@ -180,6 +210,14 @@ func TestCreateVolume_PassesValidation(t *testing.T) { AvailabilityZone: aws.String("ap-southeast-2a"), }, }, + { + name: "ExplicitIopsInRange", + input: &ec2.CreateVolumeInput{ + Size: aws.Int64(80), + AvailabilityZone: aws.String("ap-southeast-2a"), + Iops: aws.Int64(8000), + }, + }, } for _, tt := range tests {