change volume specs to int32 and define max iops and throughput values (#3139)

* change int64 volume specs to int32 like in used AWS lib
* upper iops and throughput limits
* minor refactoring in ebs.go
This commit is contained in:
Felix Kunde 2026-07-27 16:32:22 +02:00 committed by GitHub
parent 56a8f1d90b
commit d268c589c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 67 additions and 73 deletions

View File

@ -81,8 +81,8 @@ spec:
"cost_memory": 0.014375, "cost_memory": 0.014375,
"free_iops": 3000, "free_iops": 3000,
"free_throughput": 125, "free_throughput": 125,
"limit_iops": 16000, "limit_iops": 80000,
"limit_throughput": 1000, "limit_throughput": 2000,
"postgresql_versions": [ "postgresql_versions": [
"18", "18",
"17", "17",

View File

@ -3258,7 +3258,8 @@ spec:
description: Volume describes a single volume in the manifest. description: Volume describes a single volume in the manifest.
properties: properties:
iops: iops:
format: int64 format: int32
maximum: 80000
type: integer type: integer
isSubPathExpr: isSubPathExpr:
type: boolean type: boolean
@ -3319,7 +3320,8 @@ spec:
subPath: subPath:
type: string type: string
throughput: throughput:
format: int64 format: int32
maximum: 2000
type: integer type: integer
type: type:
type: string type: string

View File

@ -554,11 +554,11 @@ properties of the persistent storage that stores Postgres data.
* **iops** * **iops**
When running the operator on AWS the latest generation of EBS volumes (`gp3`) When running the operator on AWS the latest generation of EBS volumes (`gp3`)
allows for configuring the number of IOPS. Maximum is 16000. Optional. allows for configuring the number of IOPS. Maximum is 80000. Optional.
* **throughput** * **throughput**
When running the operator on AWS the latest generation of EBS volumes (`gp3`) When running the operator on AWS the latest generation of EBS volumes (`gp3`)
allows for configuring the throughput in MB/s. Maximum is 1000. Optional. allows for configuring the throughput in MB/s. Maximum is 2000. Optional.
* **selector** * **selector**
A label query over PVs to consider for binding. See the [Kubernetes A label query over PVs to consider for binding. See the [Kubernetes

View File

@ -3258,7 +3258,8 @@ spec:
description: Volume describes a single volume in the manifest. description: Volume describes a single volume in the manifest.
properties: properties:
iops: iops:
format: int64 format: int32
maximum: 80000
type: integer type: integer
isSubPathExpr: isSubPathExpr:
type: boolean type: boolean
@ -3319,7 +3320,8 @@ spec:
subPath: subPath:
type: string type: string
throughput: throughput:
format: int64 format: int32
maximum: 2000
type: integer type: integer
type: type:
type: string type: string

View File

@ -3258,7 +3258,8 @@ spec:
description: Volume describes a single volume in the manifest. description: Volume describes a single volume in the manifest.
properties: properties:
iops: iops:
format: int64 format: int32
maximum: 80000
type: integer type: integer
isSubPathExpr: isSubPathExpr:
type: boolean type: boolean
@ -3319,7 +3320,8 @@ spec:
subPath: subPath:
type: string type: string
throughput: throughput:
format: int64 format: int32
maximum: 2000
type: integer type: integer
type: type:
type: string type: string

View File

@ -167,8 +167,10 @@ type Volume struct {
StorageClass string `json:"storageClass,omitempty"` StorageClass string `json:"storageClass,omitempty"`
SubPath string `json:"subPath,omitempty"` SubPath string `json:"subPath,omitempty"`
IsSubPathExpr *bool `json:"isSubPathExpr,omitempty"` IsSubPathExpr *bool `json:"isSubPathExpr,omitempty"`
Iops *int64 `json:"iops,omitempty"` // +kubebuilder:validation:Maximum=80000
Throughput *int64 `json:"throughput,omitempty"` Iops *int32 `json:"iops,omitempty"`
// +kubebuilder:validation:Maximum=2000
Throughput *int32 `json:"throughput,omitempty"`
VolumeType string `json:"type,omitempty"` VolumeType string `json:"type,omitempty"`
} }

View File

@ -1686,12 +1686,12 @@ func (in *Volume) DeepCopyInto(out *Volume) {
} }
if in.Iops != nil { if in.Iops != nil {
in, out := &in.Iops, &out.Iops in, out := &in.Iops, &out.Iops
*out = new(int64) *out = new(int32)
**out = **in **out = **in
} }
if in.Throughput != nil { if in.Throughput != nil {
in, out := &in.Throughput, &out.Throughput in, out := &in.Throughput, &out.Throughput
*out = new(int64) *out = new(int32)
**out = **in **out = **in
} }
return return

View File

@ -81,24 +81,24 @@ func (c *Cluster) syncUnderlyingEBSVolume() error {
errors := make([]string, 0) errors := make([]string, 0)
for _, volume := range c.EBSVolumes { for _, volume := range c.EBSVolumes {
var modifyIops *int64 var modifyIops *int32
var modifyThroughput *int64 var modifyThroughput *int32
var modifySize *int64 var modifySize *int32
var modifyType *string var modifyType *string
if targetValue.Iops != nil && *targetValue.Iops >= int64(3000) { if targetValue.Iops != nil && *targetValue.Iops >= int32(3000) {
if volume.Iops != int64(*targetValue.Iops) { if volume.Iops != int32(*targetValue.Iops) {
modifyIops = targetValue.Iops modifyIops = targetValue.Iops
} }
} }
if targetValue.Throughput != nil && *targetValue.Throughput >= int64(125) { if targetValue.Throughput != nil && *targetValue.Throughput >= int32(125) {
if volume.Throughput != int64(*targetValue.Throughput) { if volume.Throughput != int32(*targetValue.Throughput) {
modifyThroughput = targetValue.Throughput modifyThroughput = targetValue.Throughput
} }
} }
if targetSize > int64(volume.Size) { if targetSize > volume.Size {
modifySize = &targetSize modifySize = &targetSize
} }
@ -437,6 +437,6 @@ func getPodNameFromPersistentVolume(pv *v1.PersistentVolume) *spec.NamespacedNam
return &spec.NamespacedName{Namespace: namespace, Name: name} return &spec.NamespacedName{Namespace: namespace, Name: name}
} }
func quantityToGigabyte(q resource.Quantity) int64 { func quantityToGigabyte(q resource.Quantity) int32 {
return q.ScaledValue(0) / (1 * constants.Gigabyte) return int32(q.ScaledValue(0) / (1 * constants.Gigabyte))
} }

View File

@ -13,7 +13,6 @@ import (
"github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/zalando/postgres-operator/mocks" "github.com/zalando/postgres-operator/mocks"
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
@ -25,9 +24,9 @@ import (
) )
type testVolume struct { type testVolume struct {
size int64 size int32
iops int64 iops int32
throughtput int64 throughtput int32
volType string volType string
} }
@ -121,7 +120,7 @@ func TestQuantityToGigabyte(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
quantityStr string quantityStr string
expected int64 expected int32
}{ }{
{ {
"test with 1Gi", "test with 1Gi",
@ -131,12 +130,12 @@ func TestQuantityToGigabyte(t *testing.T) {
{ {
"test with float", "test with float",
"1.5Gi", "1.5Gi",
int64(1), int32(1),
}, },
{ {
"test with 1000Mi", "test with 1000Mi",
"1000Mi", "1000Mi",
int64(0), int32(0),
}, },
} }
@ -247,8 +246,8 @@ func TestGp2Gp3Migration(t *testing.T) {
}, client, acidv1.Postgresql{Spec: acidv1.PostgresSpec{Volume: acidv1.Volume{VolumeType: "gp3"}}}, logger, eventRecorder) }, client, acidv1.Postgresql{Spec: acidv1.PostgresSpec{Volume: acidv1.Volume{VolumeType: "gp3"}}}, logger, eventRecorder)
cluster.Spec.Volume.Size = "150Gi" cluster.Spec.Volume.Size = "150Gi"
cluster.Spec.Volume.Iops = aws.Int64(6000) cluster.Spec.Volume.Iops = aws.Int32(6000)
cluster.Spec.Volume.Throughput = aws.Int64(275) cluster.Spec.Volume.Throughput = aws.Int32(275)
// set metadata, so that labels will get correct values // set metadata, so that labels will get correct values
cluster.Name = clusterName cluster.Name = clusterName
@ -280,8 +279,8 @@ func TestGp2Gp3Migration(t *testing.T) {
}, nil) }, nil)
// expect only gp2 volume to be modified // expect only gp2 volume to be modified
resizer.EXPECT().ModifyVolume(gomock.Eq("ebs-volume-1"), gomock.Eq(aws.String("gp3")), gomock.Nil(), gomock.Eq(aws.Int64(6000)), gomock.Eq(aws.Int64(275))).Return(nil) resizer.EXPECT().ModifyVolume(gomock.Eq("ebs-volume-1"), gomock.Eq(aws.String("gp3")), gomock.Nil(), gomock.Eq(aws.Int32(6000)), gomock.Eq(aws.Int32(275))).Return(nil)
resizer.EXPECT().ModifyVolume(gomock.Eq("ebs-volume-2"), gomock.Eq(aws.String("gp3")), gomock.Nil(), gomock.Eq(aws.Int64(6000)), gomock.Eq(aws.Int64(275))).Return(nil) resizer.EXPECT().ModifyVolume(gomock.Eq("ebs-volume-2"), gomock.Eq(aws.String("gp3")), gomock.Nil(), gomock.Eq(aws.Int32(6000)), gomock.Eq(aws.Int32(275))).Return(nil)
cluster.VolumeResizer = resizer cluster.VolumeResizer = resizer
cluster.syncVolumes() cluster.syncVolumes()
@ -343,8 +342,8 @@ func TestNoVolumeTypeChange(t *testing.T) {
}, nil) }, nil)
// expect only gp2 volume to be modified // expect only gp2 volume to be modified
resizer.EXPECT().ModifyVolume(gomock.Eq("ebs-volume-1"), gomock.Nil(), gomock.Eq(aws.Int64(177)), gomock.Nil(), gomock.Nil()).Return(nil) resizer.EXPECT().ModifyVolume(gomock.Eq("ebs-volume-1"), gomock.Nil(), gomock.Eq(aws.Int32(177)), gomock.Nil(), gomock.Nil()).Return(nil)
resizer.EXPECT().ModifyVolume(gomock.Eq("ebs-volume-2"), gomock.Nil(), gomock.Eq(aws.Int64(177)), gomock.Nil(), gomock.Nil()).Return(nil) resizer.EXPECT().ModifyVolume(gomock.Eq("ebs-volume-2"), gomock.Nil(), gomock.Eq(aws.Int32(177)), gomock.Nil(), gomock.Nil()).Return(nil)
cluster.VolumeResizer = resizer cluster.VolumeResizer = resizer
cluster.syncVolumes() cluster.syncVolumes()

View File

@ -89,12 +89,12 @@ func (r *EBSVolumeResizer) DescribeVolumes(volumeIds []string) ([]VolumeProperti
} }
for _, v := range volumeOutput.Volumes { for _, v := range volumeOutput.Volumes {
vp := VolumeProperties{VolumeID: *v.VolumeId, Size: int64(*v.Size), VolumeType: string(v.VolumeType)} vp := VolumeProperties{VolumeID: *v.VolumeId, Size: int32(*v.Size), VolumeType: string(v.VolumeType)}
if v.Iops != nil { if v.Iops != nil {
vp.Iops = int64(*v.Iops) vp.Iops = int32(*v.Iops)
} }
if v.Throughput != nil { if v.Throughput != nil {
vp.Throughput = int64(*v.Throughput) vp.Throughput = int32(*v.Throughput)
} }
p = append(p, vp) p = append(p, vp)
} }
@ -103,7 +103,7 @@ func (r *EBSVolumeResizer) DescribeVolumes(volumeIds []string) ([]VolumeProperti
} }
// ResizeVolume actually calls AWS API to resize the EBS volume if necessary. // ResizeVolume actually calls AWS API to resize the EBS volume if necessary.
func (r *EBSVolumeResizer) ResizeVolume(volumeID string, newSize int64) error { func (r *EBSVolumeResizer) ResizeVolume(volumeID string, newSize int32) error {
/* first check if the volume is already of a requested size */ /* first check if the volume is already of a requested size */
volumeOutput, err := r.connection.DescribeVolumes(context.TODO(), &ec2.DescribeVolumesInput{VolumeIds: []string{volumeID}}) volumeOutput, err := r.connection.DescribeVolumes(context.TODO(), &ec2.DescribeVolumesInput{VolumeIds: []string{volumeID}})
if err != nil { if err != nil {
@ -113,12 +113,11 @@ func (r *EBSVolumeResizer) ResizeVolume(volumeID string, newSize int64) error {
if *vol.VolumeId != volumeID { if *vol.VolumeId != volumeID {
return fmt.Errorf("describe volume %q returned information about a non-matching volume %q", volumeID, *vol.VolumeId) return fmt.Errorf("describe volume %q returned information about a non-matching volume %q", volumeID, *vol.VolumeId)
} }
sizeInt32 := int32(newSize) if *vol.Size == newSize {
if *vol.Size == sizeInt32 {
// nothing to do // nothing to do
return nil return nil
} }
input := ec2.ModifyVolumeInput{Size: &sizeInt32, VolumeId: &volumeID} input := ec2.ModifyVolumeInput{Size: &newSize, VolumeId: &volumeID}
output, err := r.connection.ModifyVolume(context.TODO(), &input) output, err := r.connection.ModifyVolume(context.TODO(), &input)
if err != nil { if err != nil {
return fmt.Errorf("could not modify persistent volume: %v", err) return fmt.Errorf("could not modify persistent volume: %v", err)
@ -154,28 +153,16 @@ func (r *EBSVolumeResizer) ResizeVolume(volumeID string, newSize int64) error {
} }
// ModifyVolume Modify EBS volume // ModifyVolume Modify EBS volume
func (r *EBSVolumeResizer) ModifyVolume(volumeID string, newType *string, newSize *int64, iops *int64, throughput *int64) error { func (r *EBSVolumeResizer) ModifyVolume(volumeID string, newType *string, newSize *int32, iops *int32, throughput *int32) error {
var sizeInt32 *int32 input := ec2.ModifyVolumeInput{VolumeId: &volumeID}
var iopsInt32 *int32
var throughputInt32 *int32
input := ec2.ModifyVolumeInput{
VolumeId: &volumeID,
}
if newSize != nil { if newSize != nil {
s := int32(*newSize) input.Size = newSize
sizeInt32 = &s
input.Size = sizeInt32
} }
if iops != nil { if iops != nil {
i := int32(*iops) input.Iops = iops
iopsInt32 = &i
input.Iops = iopsInt32
} }
if throughput != nil { if throughput != nil {
t := int32(*throughput) input.Throughput = throughput
throughputInt32 = &t
input.Throughput = throughputInt32
} }
if newType != nil { if newType != nil {
input.VolumeType = types.VolumeType(*newType) input.VolumeType = types.VolumeType(*newType)

View File

@ -8,9 +8,9 @@ import v1 "k8s.io/api/core/v1"
type VolumeProperties struct { type VolumeProperties struct {
VolumeID string VolumeID string
VolumeType string VolumeType string
Size int64 Size int32
Iops int64 Iops int32
Throughput int64 Throughput int32
} }
// VolumeResizer defines the set of methods used to implememnt provider-specific resizing of persistent volumes. // VolumeResizer defines the set of methods used to implememnt provider-specific resizing of persistent volumes.
@ -20,8 +20,8 @@ type VolumeResizer interface {
VolumeBelongsToProvider(pv *v1.PersistentVolume) bool VolumeBelongsToProvider(pv *v1.PersistentVolume) bool
GetProviderVolumeID(pv *v1.PersistentVolume) (string, error) GetProviderVolumeID(pv *v1.PersistentVolume) (string, error)
ExtractVolumeID(volumeID string) (string, error) ExtractVolumeID(volumeID string) (string, error)
ResizeVolume(providerVolumeID string, newSize int64) error ResizeVolume(providerVolumeID string, newSize int32) error
ModifyVolume(providerVolumeID string, newType *string, newSize *int64, iops *int64, throughput *int64) error ModifyVolume(providerVolumeID string, newType *string, newSize *int32, iops *int32, throughput *int32) error
DisconnectFromProvider() error DisconnectFromProvider() error
DescribeVolumes(providerVolumesID []string) ([]VolumeProperties, error) DescribeVolumes(providerVolumesID []string) ([]VolumeProperties, error)
} }

View File

@ -70,8 +70,8 @@ spec:
"cost_memory": 0.014375, "cost_memory": 0.014375,
"free_iops": 3000, "free_iops": 3000,
"free_throughput": 125, "free_throughput": 125,
"limit_iops": 16000, "limit_iops": 80000,
"limit_throughput": 1000, "limit_throughput": 2000,
"postgresql_versions": [ "postgresql_versions": [
"18", "18",
"17", "17",

View File

@ -86,9 +86,9 @@ COST_ELB = float(getenv('COST_ELB', 0.03)) # per hour
# maximum and limitation of IOPS and throughput # maximum and limitation of IOPS and throughput
FREE_IOPS = float(getenv('FREE_IOPS', 3000)) FREE_IOPS = float(getenv('FREE_IOPS', 3000))
LIMIT_IOPS = float(getenv('LIMIT_IOPS', 16000)) LIMIT_IOPS = float(getenv('LIMIT_IOPS', 80000))
FREE_THROUGHPUT = float(getenv('FREE_THROUGHPUT', 125)) FREE_THROUGHPUT = float(getenv('FREE_THROUGHPUT', 125))
LIMIT_THROUGHPUT = float(getenv('LIMIT_THROUGHPUT', 1000)) LIMIT_THROUGHPUT = float(getenv('LIMIT_THROUGHPUT', 2000))
# get the default value of core and memory # get the default value of core and memory
DEFAULT_MEMORY = getenv('DEFAULT_MEMORY', '300Mi') DEFAULT_MEMORY = getenv('DEFAULT_MEMORY', '300Mi')
DEFAULT_MEMORY_LIMIT = getenv('DEFAULT_MEMORY_LIMIT', '300Mi') DEFAULT_MEMORY_LIMIT = getenv('DEFAULT_MEMORY_LIMIT', '300Mi')

View File

@ -28,8 +28,8 @@ default_operator_ui_config='{
"cost_memory": 0.014375, "cost_memory": 0.014375,
"free_iops": 3000, "free_iops": 3000,
"free_throughput": 125, "free_throughput": 125,
"limit_iops": 16000, "limit_iops": 80000,
"limit_throughput": 1000, "limit_throughput": 2000,
"postgresql_versions": [ "postgresql_versions": [
"18", "18",
"17", "17",