diff --git a/apis/database/v1beta1/rdsinstance_types.go b/apis/database/v1beta1/rdsinstance_types.go index 059d8c124e..8827100070 100644 --- a/apis/database/v1beta1/rdsinstance_types.go +++ b/apis/database/v1beta1/rdsinstance_types.go @@ -491,13 +491,18 @@ type RDSInstanceParameters struct { // IOPS is the amount of Provisioned IOPS (input/output operations per second) to be // initially allocated for the DB instance. For information about valid IOPS - // values, see see Amazon RDS Provisioned IOPS Storage to Improve Performance + // values, see Amazon RDS Provisioned IOPS Storage to Improve Performance // (http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html#USER_PIOPS) // in the Amazon RDS User Guide. // Constraints: Must be a multiple between 1 and 50 of the storage amount for // the DB instance. Must also be an integer multiple of 1000. For example, if // the size of your DB instance is 500 GiB, then your IOPS value can be 2000, // 3000, 4000, or 5000. + // + // For valid IOPS values on DB instances with storage type "gp3", + // see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html#gp3-storage. + // + // Note: controller considers 0 and null as equivalent // +optional IOPS *int `json:"iops,omitempty"` @@ -713,6 +718,16 @@ type RDSInstanceParameters struct { // +optional StorageEncrypted *bool `json:"storageEncrypted,omitempty"` + // The storage throughput value for the DB instance. + // + // This setting applies only to the gp3 storage type. + // + // This setting doesn't apply to Amazon Aurora or RDS Custom DB instances. + // + // Note: controller considers 0 and null as equivalent + // +optional + StorageThroughput *int `json:"storageThroughput,omitempty"` + // StorageType specifies the storage type to be associated with the DB instance. // Valid values: standard | gp2 | io1 // If you specify io1, you must also include a value for the IOPS parameter. @@ -1089,6 +1104,10 @@ type PendingModifiedValues struct { // class of the DB instance. ProcessorFeatures []ProcessorFeature `json:"processorFeatures,omitempty"` + // StorageThroughput indicates the new storage throughput value for the DB instance + // that will be applied or is currently being applied. + StorageThroughput int `json:"storageThroughput,omitempty"` + // StorageType specifies the storage type to be associated with the DB instance. StorageType string `json:"storageType,omitempty"` } diff --git a/apis/database/v1beta1/zz_generated.deepcopy.go b/apis/database/v1beta1/zz_generated.deepcopy.go index 6c21a81a17..8d94ef307f 100644 --- a/apis/database/v1beta1/zz_generated.deepcopy.go +++ b/apis/database/v1beta1/zz_generated.deepcopy.go @@ -773,6 +773,11 @@ func (in *RDSInstanceParameters) DeepCopyInto(out *RDSInstanceParameters) { *out = new(bool) **out = **in } + if in.StorageThroughput != nil { + in, out := &in.StorageThroughput, &out.StorageThroughput + *out = new(int) + **out = **in + } if in.StorageType != nil { in, out := &in.StorageType, &out.StorageType *out = new(string) diff --git a/package/crds/database.aws.crossplane.io_rdsinstances.yaml b/package/crds/database.aws.crossplane.io_rdsinstances.yaml index 8196566ceb..0400349722 100644 --- a/package/crds/database.aws.crossplane.io_rdsinstances.yaml +++ b/package/crds/database.aws.crossplane.io_rdsinstances.yaml @@ -492,15 +492,17 @@ spec: deleting a Read Replica.' type: string iops: - description: 'IOPS is the amount of Provisioned IOPS (input/output + description: "IOPS is the amount of Provisioned IOPS (input/output operations per second) to be initially allocated for the DB - instance. For information about valid IOPS values, see see Amazon + instance. For information about valid IOPS values, see Amazon RDS Provisioned IOPS Storage to Improve Performance (http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html#USER_PIOPS) in the Amazon RDS User Guide. Constraints: Must be a multiple between 1 and 50 of the storage amount for the DB instance. Must also be an integer multiple of 1000. For example, if the size of your DB instance is 500 GiB, then your IOPS value can - be 2000, 3000, 4000, or 5000.' + be 2000, 3000, 4000, or 5000. \n For valid IOPS values on DB + instances with storage type \"gp3\", see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html#gp3-storage. + \n Note: controller considers 0 and null as equivalent" type: integer kmsKeyId: description: KMSKeyID for an encrypted DB instance. The KMS key @@ -1062,6 +1064,12 @@ spec: DB instances is managed by the DB cluster. For more information, see CreateDBCluster. Default: false' type: boolean + storageThroughput: + description: "The storage throughput value for the DB instance. + \n This setting applies only to the gp3 storage type. \n This + setting doesn't apply to Amazon Aurora or RDS Custom DB instances. + \n Note: controller considers 0 and null as equivalent" + type: integer storageType: description: 'StorageType specifies the storage type to be associated with the DB instance. Valid values: standard | gp2 | io1 If @@ -1664,6 +1672,11 @@ spec: - value type: object type: array + storageThroughput: + description: StorageThroughput indicates the new storage throughput + value for the DB instance that will be applied or is currently + being applied. + type: integer storageType: description: StorageType specifies the storage type to be associated with the DB instance. diff --git a/pkg/clients/database/rds.go b/pkg/clients/database/rds.go index c618b54d9d..4c98f5280b 100644 --- a/pkg/clients/database/rds.go +++ b/pkg/clients/database/rds.go @@ -100,7 +100,6 @@ func GenerateCreateRDSInstanceInput(name, password string, p *v1beta1.RDSInstanc EnablePerformanceInsights: p.EnablePerformanceInsights, Engine: aws.String(p.Engine), EngineVersion: p.EngineVersion, - Iops: pointer.ToIntAsInt32Ptr(p.IOPS), KmsKeyId: p.KMSKeyID, LicenseModel: p.LicenseModel, MasterUserPassword: pointer.ToOrNilIfZeroValue(password), @@ -140,6 +139,13 @@ func GenerateCreateRDSInstanceInput(name, password string, p *v1beta1.RDSInstanc } } } + // for storageType gp3 below engine specific allocatedStorage threshold, do not send iops and storageThroughput + // to avoid errors like "You can't specify IOPS or storage throughput for engine postgres and a storage size less than 400." + // This allows users to set iops/storageThroughput to the default values themselves. + if !IsStorageTypeGP3BelowAllocatedStorageThreshold(p) { + c.Iops = pointer.ToIntAsInt32Ptr(p.IOPS) + c.StorageThroughput = pointer.ToIntAsInt32Ptr(p.StorageThroughput) + } return c } @@ -185,6 +191,7 @@ func GenerateRestoreRDSInstanceFromS3Input(name, password string, p *v1beta1.RDS SourceEngine: p.RestoreFrom.S3.SourceEngine, SourceEngineVersion: p.RestoreFrom.S3.SourceEngineVersion, StorageEncrypted: p.StorageEncrypted, + StorageThroughput: pointer.ToIntAsInt32Ptr(p.StorageThroughput), StorageType: p.StorageType, VpcSecurityGroupIds: p.VPCSecurityGroupIDs, } @@ -234,6 +241,7 @@ func GenerateRestoreRDSInstanceFromSnapshotInput(name string, p *v1beta1.RDSInst OptionGroupName: p.OptionGroupName, Port: pointer.ToIntAsInt32Ptr(p.Port), PubliclyAccessible: p.PubliclyAccessible, + StorageThroughput: pointer.ToIntAsInt32Ptr(p.StorageThroughput), StorageType: p.StorageType, VpcSecurityGroupIds: p.VPCSecurityGroupIDs, } @@ -287,6 +295,7 @@ func GenerateRestoreRDSInstanceToPointInTimeInput(name string, p *v1beta1.RDSIns OptionGroupName: p.OptionGroupName, Port: pointer.ToIntAsInt32Ptr(p.Port), PubliclyAccessible: p.PubliclyAccessible, + StorageThroughput: pointer.ToIntAsInt32Ptr(p.StorageThroughput), StorageType: p.StorageType, VpcSecurityGroupIds: p.VPCSecurityGroupIDs, @@ -321,7 +330,7 @@ func GenerateRestoreRDSInstanceToPointInTimeInput(name string, p *v1beta1.RDSIns // CreatePatch creates a *v1beta1.RDSInstanceParameters that has only the changed // values between the target *v1beta1.RDSInstanceParameters and the current // *rds.DBInstance -func CreatePatch(in *rdstypes.DBInstance, spec *v1beta1.RDSInstanceParameters) (*v1beta1.RDSInstanceParameters, error) { +func CreatePatch(in *rdstypes.DBInstance, spec *v1beta1.RDSInstanceParameters) (*v1beta1.RDSInstanceParameters, error) { //nolint:gocyclo target := spec.DeepCopy() currentParams := &v1beta1.RDSInstanceParameters{} LateInitialize(currentParams, in) @@ -361,6 +370,14 @@ func CreatePatch(in *rdstypes.DBInstance, spec *v1beta1.RDSInstanceParameters) ( } } + // Depending on whether the instance was created as gp2 or modified from another type (e.g. gp3) to gp2, + // AWS provides different responses for IOPS/StorageThroughput (either 0 or nil). + // Therefore, we consider both 0 and nil to be equivalent. + if aws.ToInt(target.IOPS) == aws.ToInt(currentParams.IOPS) { + currentParams.IOPS = target.IOPS + currentParams.StorageThroughput = target.StorageThroughput + } + jsonPatch, err := jsonpatch.CreateJSONPatch(currentParams, target) if err != nil { return nil, err @@ -412,6 +429,7 @@ func GenerateModifyDBInstanceInput(name string, p *v1beta1.RDSInstanceParameters PreferredMaintenanceWindow: p.PreferredMaintenanceWindow, PromotionTier: pointer.ToIntAsInt32Ptr(p.PromotionTier), PubliclyAccessible: p.PubliclyAccessible, + StorageThroughput: pointer.ToIntAsInt32Ptr(p.StorageThroughput), StorageType: p.StorageType, UseDefaultProcessorFeatures: p.UseDefaultProcessorFeatures, VpcSecurityGroupIds: p.VPCSecurityGroupIDs, @@ -539,6 +557,7 @@ func GenerateObservation(db rdstypes.DBInstance) v1beta1.RDSInstanceObservation LicenseModel: aws.ToString(db.PendingModifiedValues.LicenseModel), MultiAZ: aws.ToBool(db.PendingModifiedValues.MultiAZ), Port: int(aws.ToInt32(db.PendingModifiedValues.Port)), + StorageThroughput: int(aws.ToInt32(db.PendingModifiedValues.StorageThroughput)), StorageType: aws.ToString(db.PendingModifiedValues.StorageType), } if db.PendingModifiedValues.PendingCloudwatchLogsExports != nil { @@ -616,6 +635,7 @@ func LateInitialize(in *v1beta1.RDSInstanceParameters, db *rdstypes.DBInstance) in.PromotionTier = pointer.LateInitializeIntFrom32Ptr(in.PromotionTier, db.PromotionTier) in.PubliclyAccessible = pointer.LateInitialize(in.PubliclyAccessible, ptr.To(db.PubliclyAccessible)) in.StorageEncrypted = pointer.LateInitialize(in.StorageEncrypted, ptr.To(db.StorageEncrypted)) + in.StorageThroughput = pointer.LateInitializeIntFrom32Ptr(in.StorageThroughput, db.StorageThroughput) in.StorageType = pointer.LateInitialize(in.StorageType, db.StorageType) in.Timezone = pointer.LateInitialize(in.Timezone, db.Timezone) @@ -928,3 +948,20 @@ func DiffTags(spec []v1beta1.Tag, current []rdstypes.Tag) (addTags []rdstypes.Ta return addTags, removeTags } + +// IsStorageTypeGP3BelowAllocatedStorageThreshold returns true if storageType is gp3 and allocatedStorage is below engine specific threshold +// See also https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html#gp3-storage. +func IsStorageTypeGP3BelowAllocatedStorageThreshold(p *v1beta1.RDSInstanceParameters) bool { + if pointer.StringValue(p.StorageType) != "gp3" { + return false + } + + switch allocatedStorage, engine := aws.ToInt(p.AllocatedStorage), p.Engine; engine { + case "mariadb", "mysql", "postgres": + return allocatedStorage < 400 + case "oracle-ee", "oracle-ee-cdb", "oracle-se2", "oracle-se2-cdb": + return allocatedStorage < 200 + } + + return false +} diff --git a/pkg/clients/database/rds_test.go b/pkg/clients/database/rds_test.go index 4265958bdd..750e0cd043 100644 --- a/pkg/clients/database/rds_test.go +++ b/pkg/clients/database/rds_test.go @@ -796,6 +796,7 @@ func TestGenerateObservation(t *testing.T) { LicenseModel: &name, MultiAZ: &multiAZ, Port: &port32, + StorageThroughput: &storage32, StorageType: &storageType, } pendingCloudwatch := rdstypes.PendingCloudwatchLogsExports{ @@ -891,6 +892,7 @@ func TestGenerateObservation(t *testing.T) { LicenseModel: name, MultiAZ: multiAZ, Port: port, + StorageThroughput: storage, StorageType: storageType, PendingCloudwatchLogsExports: v1beta1.PendingCloudwatchLogsExports{ LogTypesToDisable: nil, @@ -1077,6 +1079,7 @@ func TestLateInitialize(t *testing.T) { PromotionTier: &tier32, PubliclyAccessible: trueFlag, StorageEncrypted: trueFlag, + StorageThroughput: &storage32, StorageType: &storageType, Timezone: &zone, DBSecurityGroups: []rdstypes.DBSecurityGroupMembership{{DBSecurityGroupName: &name, Status: &status}}, @@ -1123,6 +1126,7 @@ func TestLateInitialize(t *testing.T) { PromotionTier: &tier, PubliclyAccessible: &trueFlag, StorageEncrypted: &trueFlag, + StorageThroughput: &storage, StorageType: &storageType, Timezone: &zone, DBSecurityGroups: []string{name}, @@ -1344,6 +1348,7 @@ func TestGenerateModifyDBInstanceInput(t *testing.T) { PromotionTier: &tier, PubliclyAccessible: &trueFlag, StorageEncrypted: &trueFlag, + StorageThroughput: &storage, StorageType: &storageType, Timezone: &zone, DBSecurityGroups: dbSecurityGroups, @@ -1395,6 +1400,7 @@ func TestGenerateModifyDBInstanceInput(t *testing.T) { PreferredMaintenanceWindow: &window, PromotionTier: &tier32, PubliclyAccessible: &trueFlag, + StorageThroughput: &storage32, StorageType: &storageType, UseDefaultProcessorFeatures: &trueFlag, VpcSecurityGroupIds: vpcIds, @@ -1473,6 +1479,7 @@ func TestGenerateCreateRDSInstanceInput(t *testing.T) { PromotionTier: &tier, PubliclyAccessible: &trueFlag, StorageEncrypted: &trueFlag, + StorageThroughput: &storage, StorageType: &storageType, Timezone: &zone, DBSecurityGroups: dbSecurityGroups, @@ -1524,6 +1531,7 @@ func TestGenerateCreateRDSInstanceInput(t *testing.T) { PromotionTier: &tier32, PubliclyAccessible: &trueFlag, StorageEncrypted: &trueFlag, + StorageThroughput: &storage32, StorageType: &storageType, Timezone: &zone, VpcSecurityGroupIds: vpcIds, diff --git a/pkg/controller/database/rdsinstance/rdsinstance.go b/pkg/controller/database/rdsinstance/rdsinstance.go index 4dc7c7c2ff..d8c6313a78 100644 --- a/pkg/controller/database/rdsinstance/rdsinstance.go +++ b/pkg/controller/database/rdsinstance/rdsinstance.go @@ -289,6 +289,13 @@ func (e *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext if cr.Spec.ForProvider.MasterUsername != nil { conn[xpv1.ResourceCredentialsSecretUserKey] = []byte(aws.ToString(cr.Spec.ForProvider.MasterUsername)) } + // for storageType gp3 below engine specific allocatedStorage threshold, do not send iops and storageThroughput + // to avoid errors like "You can't specify IOPS or storage throughput for engine postgres and a storage size less than 400." + // This allows users to set iops/storageThroughput to the default values themselves. + if rds.IsStorageTypeGP3BelowAllocatedStorageThreshold(&cr.Spec.ForProvider) { + modify.Iops = nil + modify.StorageThroughput = nil + } if _, err = e.client.ModifyDBInstance(ctx, modify); err != nil { return managed.ExternalUpdate{}, errorutils.Wrap(err, errModifyFailed) diff --git a/pkg/controller/rds/dbinstance/setup.go b/pkg/controller/rds/dbinstance/setup.go index 228172612f..b4909fa57a 100644 --- a/pkg/controller/rds/dbinstance/setup.go +++ b/pkg/controller/rds/dbinstance/setup.go @@ -202,6 +202,14 @@ func (e *custom) preCreate(ctx context.Context, cr *svcapitypes.DBInstance, obj return errors.Wrap(err, dbinstance.ErrCachePassword) } + // for storageType gp3 below engine specific allocatedStorage threshold, do not send iops and storageThroughput + // to avoid errors like "You can't specify IOPS or storage throughput for engine postgres and a storage size less than 400." + // This allows users to set iops/storageThroughput to the default values themselves. + if isStorageTypeGP3BelowAllocatedStorageThreshold(cr) { + obj.Iops = nil + obj.StorageThroughput = nil + } + return nil } @@ -253,6 +261,14 @@ func (e *custom) preUpdate(ctx context.Context, cr *svcapitypes.DBInstance, obj obj.VpcSecurityGroupIds = nil } + // for storageType gp3 below engine specific allocatedStorage threshold, do not send iops and storageThroughput + // to avoid errors like "You can't specify IOPS or storage throughput for engine postgres and a storage size less than 400." + // This allows users to set iops/storageThroughput to the default values themselves. + if isStorageTypeGP3BelowAllocatedStorageThreshold(cr) { + obj.Iops = nil + obj.StorageThroughput = nil + } + return nil } @@ -715,3 +731,20 @@ func handleKmsKey(inKey *string, dbKey *string) *string { } return dbKey } + +// isStorageTypeGP3BelowAllocatedStorageThreshold returns true if storageType is gp3 and allocatedStorage is below engine specific threshold +// See also https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html#gp3-storage. +func isStorageTypeGP3BelowAllocatedStorageThreshold(cr *svcapitypes.DBInstance) bool { + if pointer.StringValue(cr.Spec.ForProvider.StorageType) != "gp3" { + return false + } + + switch allocatedStorage, engine := pointer.Int64Value(cr.Spec.ForProvider.AllocatedStorage), pointer.StringValue(cr.Spec.ForProvider.Engine); engine { + case "mariadb", "mysql", "postgres": + return allocatedStorage < 400 + case "oracle-ee", "oracle-ee-cdb", "oracle-se2", "oracle-se2-cdb": + return allocatedStorage < 200 + } + + return false +}