Merge branch 'master' into fix-critical-op-pdb-idle-alerts

This commit is contained in:
Felix Kunde 2026-07-27 16:32:39 +02:00 committed by GitHub
commit 3d1c115ef0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 95 additions and 330 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

@ -66,11 +66,6 @@ spec:
aws_region: aws_region:
default: eu-central-1 default: eu-central-1
type: string type: string
enable_ebs_gp3_migration:
type: boolean
enable_ebs_gp3_migration_max_size:
format: int64
type: integer
gcp_credentials: gcp_credentials:
type: string type: string
irsa_role_arn: irsa_role_arn:

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

@ -350,16 +350,12 @@ configAwsOrGcp:
# AWS region used to store EBS volumes # AWS region used to store EBS volumes
aws_region: eu-central-1 aws_region: eu-central-1
# enable automatic migration on AWS from gp2 to gp3 volumes
enable_ebs_gp3_migration: false
# defines maximum volume size in GB until which auto migration happens
# enable_ebs_gp3_migration_max_size: 1000
# GCP credentials that will be used by the operator / pods # GCP credentials that will be used by the operator / pods
# gcp_credentials: "" # gcp_credentials: ""
# AWS IAM role to supply in the iam.amazonaws.com/role annotation of Postgres pods # AWS IAM role to supply in the iam.amazonaws.com/role annotation of Postgres pods
# kube_iam_role: "" # kube_iam_role: ""
# Full ARN for IRSA (IAM Roles for Service Accounts) on EKS # Full ARN for IRSA (IAM Roles for Service Accounts) on EKS
# irsa_role_arn: "" # irsa_role_arn: ""

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

@ -582,7 +582,7 @@ configuration they are grouped under the `kubernetes` key.
1. `ebs` : operator resizes EBS volumes directly and executes `resizefs` within a pod 1. `ebs` : operator resizes EBS volumes directly and executes `resizefs` within a pod
2. `pvc` : operator only changes PVC definition 2. `pvc` : operator only changes PVC definition
3. `off` : disables resize of the volumes. 3. `off` : disables resize of the volumes.
4. `mixed` : operator uses AWS API to adjust size, throughput, and IOPS, and calls pvc change for file system resize 4. `mixed` : operator uses AWS API to adjust size, type, throughput, and IOPS, and calls pvc change for file system resize
Default is "pvc". Default is "pvc".
## Kubernetes resource requests ## Kubernetes resource requests
@ -821,16 +821,6 @@ yet officially supported.
Path to mount the above Secret in the filesystem of the container(s). Path to mount the above Secret in the filesystem of the container(s).
The default is empty. The default is empty.
* **enable_ebs_gp3_migration**
enable automatic migration on AWS from gp2 to gp3 volumes, that are smaller
than the configured max size (see below). This ignores that EBS gp3 is by
default only 125 MB/sec vs 250 MB/sec for gp2 >= 333GB.
The default is `false`.
* **enable_ebs_gp3_migration_max_size**
defines the maximum volume size in GB until which auto migration happens.
Default is 1000 (1TB) which matches 3000 IOPS.
## Logical backup ## Logical backup
These parameters configure a K8s cron job managed by the operator to produce These parameters configure a K8s cron job managed by the operator to produce

View File

@ -1129,7 +1129,7 @@ When using AWS with gp3 volumes you should set the mode to `mixed` because it
will also adjust the IOPS and throughput that can be defined in the manifest. will also adjust the IOPS and throughput that can be defined in the manifest.
Check the [AWS docs](https://aws.amazon.com/ebs/general-purpose/) to learn Check the [AWS docs](https://aws.amazon.com/ebs/general-purpose/) to learn
about default and maximum values. Keep in mind that AWS rate-limits updating about default and maximum values. Keep in mind that AWS rate-limits updating
volume specs to no more than once every 6 hours. volume specs to no more than 4 times within 24 hours.
```yaml ```yaml
spec: spec:

View File

@ -60,8 +60,8 @@ spec:
volume: volume:
size: 1Gi size: 1Gi
# storageClass: my-sc # storageClass: my-sc
# iops: 1000 # for EBS gp3 # iops: 1000
# throughput: 250 # in MB/s for EBS gp3 # throughput: 250 # in MB/s
# selector: # selector:
# matchExpressions: # matchExpressions:
# - { key: flavour, operator: In, values: [ "banana", "chocolate" ] } # - { key: flavour, operator: In, values: [ "banana", "chocolate" ] }

View File

@ -41,8 +41,6 @@ data:
enable_cross_namespace_secret: "false" enable_cross_namespace_secret: "false"
enable_finalizers: "false" enable_finalizers: "false"
enable_database_access: "true" enable_database_access: "true"
enable_ebs_gp3_migration: "false"
enable_ebs_gp3_migration_max_size: "1000"
enable_init_containers: "true" enable_init_containers: "true"
enable_lazy_spilo_upgrade: "false" enable_lazy_spilo_upgrade: "false"
enable_maintenance_windows: "true" enable_maintenance_windows: "true"

View File

@ -66,11 +66,6 @@ spec:
aws_region: aws_region:
default: eu-central-1 default: eu-central-1
type: string type: string
enable_ebs_gp3_migration:
type: boolean
enable_ebs_gp3_migration_max_size:
format: int64
type: integer
gcp_credentials: gcp_credentials:
type: string type: string
irsa_role_arn: irsa_role_arn:

View File

@ -167,8 +167,6 @@ configuration:
# additional_secret_mount: "some-secret-name" # additional_secret_mount: "some-secret-name"
# additional_secret_mount_path: "/some/dir" # additional_secret_mount_path: "/some/dir"
aws_region: eu-central-1 aws_region: eu-central-1
enable_ebs_gp3_migration: false
# enable_ebs_gp3_migration_max_size: 1000
# gcp_credentials: "" # gcp_credentials: ""
# kube_iam_role: "" # kube_iam_role: ""
# irsa_role_arn: "" # irsa_role_arn: ""

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

@ -246,17 +246,15 @@ type LoadBalancerConfiguration struct {
type AWSGCPConfiguration struct { type AWSGCPConfiguration struct {
WALES3Bucket string `json:"wal_s3_bucket,omitempty"` WALES3Bucket string `json:"wal_s3_bucket,omitempty"`
// +kubebuilder:default=eu-central-1 // +kubebuilder:default=eu-central-1
AWSRegion string `json:"aws_region,omitempty"` AWSRegion string `json:"aws_region,omitempty"`
WALGSBucket string `json:"wal_gs_bucket,omitempty"` WALGSBucket string `json:"wal_gs_bucket,omitempty"`
GCPCredentials string `json:"gcp_credentials,omitempty"` GCPCredentials string `json:"gcp_credentials,omitempty"`
WALAZStorageAccount string `json:"wal_az_storage_account,omitempty"` WALAZStorageAccount string `json:"wal_az_storage_account,omitempty"`
LogS3Bucket string `json:"log_s3_bucket,omitempty"` LogS3Bucket string `json:"log_s3_bucket,omitempty"`
KubeIAMRole string `json:"kube_iam_role,omitempty"` KubeIAMRole string `json:"kube_iam_role,omitempty"`
IRSARoleARN string `json:"irsa_role_arn,omitempty"` IRSARoleARN string `json:"irsa_role_arn,omitempty"`
AdditionalSecretMount string `json:"additional_secret_mount,omitempty"` AdditionalSecretMount string `json:"additional_secret_mount,omitempty"`
AdditionalSecretMountPath string `json:"additional_secret_mount_path,omitempty"` AdditionalSecretMountPath string `json:"additional_secret_mount_path,omitempty"`
EnableEBSGp3Migration bool `json:"enable_ebs_gp3_migration,omitempty"`
EnableEBSGp3MigrationMaxSize int64 `json:"enable_ebs_gp3_migration_max_size,omitempty"`
} }
// OperatorDebugConfiguration defines options for the debug mode // OperatorDebugConfiguration defines options for the debug mode

View File

@ -66,11 +66,6 @@ spec:
aws_region: aws_region:
default: eu-central-1 default: eu-central-1
type: string type: string
enable_ebs_gp3_migration:
type: boolean
enable_ebs_gp3_migration_max_size:
format: int64
type: integer
gcp_credentials: gcp_credentials:
type: string type: string
irsa_role_arn: irsa_role_arn:

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,9 +167,11 @@ 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"`
VolumeType string `json:"type,omitempty"` // +kubebuilder:validation:Maximum=2000
Throughput *int32 `json:"throughput,omitempty"`
VolumeType string `json:"type,omitempty"`
} }
// AdditionalVolume specs additional optional volumes for statefulset // AdditionalVolume specs additional optional volumes for statefulset

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

@ -176,7 +176,7 @@ func New(cfg Config, kubeClient k8sutil.KubernetesClient, pgSpec acidv1.Postgres
cluster.eventRecorder = eventRecorder cluster.eventRecorder = eventRecorder
cluster.EBSVolumes = make(map[string]volumes.VolumeProperties) cluster.EBSVolumes = make(map[string]volumes.VolumeProperties)
if cfg.OpConfig.StorageResizeMode != "pvc" || cfg.OpConfig.EnableEBSGp3Migration { if cfg.OpConfig.StorageResizeMode != "pvc" {
cluster.VolumeResizer = &volumes.EBSVolumeResizer{AWSRegion: cfg.OpConfig.AWSRegion} cluster.VolumeResizer = &volumes.EBSVolumeResizer{AWSRegion: cfg.OpConfig.AWSRegion}
} }

View File

@ -95,18 +95,10 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error {
c.logger.Errorf("could not sync Patroni resources: %v", err) c.logger.Errorf("could not sync Patroni resources: %v", err)
} }
// sync volume may already transition volumes to gp3, if iops/throughput or type is specified
if err = c.syncVolumes(); err != nil { if err = c.syncVolumes(); err != nil {
return err return err
} }
if c.OpConfig.EnableEBSGp3Migration && len(c.EBSVolumes) > 0 {
err = c.executeEBSMigration()
if nil != err {
return err
}
}
if err = c.syncPodServiceAccount(); err != nil { if err = c.syncPodServiceAccount(); err != nil {
c.logger.Errorf("could not sync pod service account: %v", err) c.logger.Errorf("could not sync pod service account: %v", err)
} }
@ -1811,7 +1803,6 @@ func (c *Cluster) syncLogicalBackupJob() error {
// no existing logical backup job, create new one // no existing logical backup job, create new one
c.logger.Info("could not find the cluster's logical backup job") c.logger.Info("could not find the cluster's logical backup job")
if err = c.createLogicalBackupJob(); err == nil { if err = c.createLogicalBackupJob(); err == nil {
c.logger.Infof("created missing logical backup job %s", jobName) c.logger.Infof("created missing logical backup job %s", jobName)
} else { } else {

View File

@ -11,7 +11,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/zalando/postgres-operator/pkg/spec" "github.com/zalando/postgres-operator/pkg/spec"
"github.com/zalando/postgres-operator/pkg/util/constants" "github.com/zalando/postgres-operator/pkg/util/constants"
"github.com/zalando/postgres-operator/pkg/util/filesystems" "github.com/zalando/postgres-operator/pkg/util/filesystems"
@ -79,47 +78,35 @@ func (c *Cluster) syncUnderlyingEBSVolume() error {
} }
targetSize := quantityToGigabyte(newSize) targetSize := quantityToGigabyte(newSize)
awsGp3 := aws.String("gp3")
awsIo2 := aws.String("io2")
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
} }
if modifyIops != nil || modifyThroughput != nil || modifySize != nil { if targetValue.VolumeType != "" && targetValue.VolumeType != volume.VolumeType {
if modifyIops != nil || modifyThroughput != nil { modifyType = &targetValue.VolumeType
// we default to gp3 if iops and throughput are configured }
modifyType = awsGp3
if targetValue.VolumeType == "io2" {
modifyType = awsIo2
}
} else if targetValue.VolumeType == "gp3" && volume.VolumeType != "gp3" {
modifyType = awsGp3
} else {
// do not touch type
modifyType = nil
}
if modifyIops != nil || modifyThroughput != nil || modifySize != nil || modifyType != nil {
err = c.VolumeResizer.ModifyVolume(volume.VolumeID, modifyType, modifySize, modifyIops, modifyThroughput) err = c.VolumeResizer.ModifyVolume(volume.VolumeID, modifyType, modifySize, modifyIops, modifyThroughput)
if err != nil { if err != nil {
errors = append(errors, fmt.Sprintf("modify failed: %v, showing current EBS values: volume-id=%s size=%d iops=%d throughput=%d", err, volume.VolumeID, volume.Size, volume.Iops, volume.Throughput)) errors = append(errors, fmt.Sprintf("modify failed: %v, showing current EBS values: volume-id=%s size=%d iops=%d throughput=%d", err, volume.VolumeID, volume.Size, volume.Iops, volume.Throughput))
@ -450,50 +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))
}
func (c *Cluster) executeEBSMigration() error {
pvs, err := c.listPersistentVolumes()
if err != nil {
return fmt.Errorf("could not list persistent volumes: %v", err)
}
if len(pvs) == 0 {
c.logger.Warningf("no persistent volumes found - skipping EBS migration")
return nil
}
c.logger.Debugf("found %d volumes, size of known volumes %d", len(pvs), len(c.EBSVolumes))
if len(pvs) == len(c.EBSVolumes) {
hasGp2 := false
for _, v := range c.EBSVolumes {
if v.VolumeType == "gp2" {
hasGp2 = true
}
}
if !hasGp2 {
c.logger.Debugf("no EBS gp2 volumes left to migrate")
return nil
}
}
var i3000 int64 = 3000
var i125 int64 = 125
for _, volume := range c.EBSVolumes {
if volume.VolumeType == "gp2" && volume.Size < c.OpConfig.EnableEBSGp3MigrationMaxSize {
c.logger.Infof("modifying EBS volume %s to type gp3 migration (%d)", volume.VolumeID, volume.Size)
err = c.VolumeResizer.ModifyVolume(volume.VolumeID, aws.String("gp3"), &volume.Size, &i3000, &i125)
if nil != err {
c.logger.Warningf("modifying volume %s failed: %v", volume.VolumeID, err)
}
} else {
c.logger.Debugf("skipping EBS volume %s to type gp3 migration (%d)", volume.VolumeID, volume.Size)
}
c.EBSVolumes[volume.VolumeID] = volume
}
return nil
} }

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),
}, },
} }
@ -179,64 +178,6 @@ func CreatePVCs(namespace string, clusterName string, labels labels.Set, n int,
return pvcList return pvcList
} }
func TestMigrateEBS(t *testing.T) {
client, _ := newFakeK8sPVCclient()
clusterName := "acid-test-cluster"
namespace := "default"
// new cluster with pvc storage resize mode and configured labels
var cluster = New(
Config{
OpConfig: config.Config{
Resources: config.Resources{
ClusterLabels: map[string]string{"application": "spilo"},
ClusterNameLabel: "cluster-name",
},
StorageResizeMode: "pvc",
EnableEBSGp3Migration: true,
EnableEBSGp3MigrationMaxSize: 1000,
},
}, client, acidv1.Postgresql{}, logger, eventRecorder)
cluster.Spec.Volume.Size = "1Gi"
// set metadata, so that labels will get correct values
cluster.Name = clusterName
cluster.Namespace = namespace
filterLabels := cluster.labelsSet(false)
testVolumes := []testVolume{testVol, testVol}
initTestVolumesAndPods(cluster.KubeClient, namespace, clusterName, filterLabels, testVolumes)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
resizer := mocks.NewMockVolumeResizer(ctrl)
resizer.EXPECT().ExtractVolumeID(gomock.Eq("aws://eu-central-1b/ebs-volume-1")).Return("ebs-volume-1", nil)
resizer.EXPECT().ExtractVolumeID(gomock.Eq("aws://eu-central-1b/ebs-volume-2")).Return("ebs-volume-2", nil)
resizer.EXPECT().GetProviderVolumeID(gomock.Any()).
DoAndReturn(func(pv *v1.PersistentVolume) (string, error) {
return resizer.ExtractVolumeID(pv.Spec.AWSElasticBlockStore.VolumeID)
}).
Times(2)
resizer.EXPECT().DescribeVolumes(gomock.Eq([]string{"ebs-volume-1", "ebs-volume-2"})).Return(
[]volumes.VolumeProperties{
{VolumeID: "ebs-volume-1", VolumeType: "gp2", Size: 100},
{VolumeID: "ebs-volume-2", VolumeType: "gp3", Size: 100}}, nil)
// expect only gp2 volume to be modified
resizer.EXPECT().ModifyVolume(gomock.Eq("ebs-volume-1"), gomock.Eq(aws.String("gp3")), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
cluster.VolumeResizer = resizer
err := cluster.populateVolumeMetaData()
assert.NoError(t, err)
err = cluster.executeEBSMigration()
assert.NoError(t, err)
}
func initTestVolumesAndPods(client k8sutil.KubernetesClient, namespace, clustername string, labels labels.Set, volumes []testVolume) { func initTestVolumesAndPods(client k8sutil.KubernetesClient, namespace, clustername string, labels labels.Set, volumes []testVolume) {
i := 0 i := 0
for _, v := range volumes { for _, v := range volumes {
@ -287,7 +228,7 @@ func initTestVolumesAndPods(client k8sutil.KubernetesClient, namespace, clustern
} }
} }
func TestMigrateGp3Support(t *testing.T) { func TestGp2Gp3Migration(t *testing.T) {
client, _ := newFakeK8sPVCclient() client, _ := newFakeK8sPVCclient()
clusterName := "acid-test-cluster" clusterName := "acid-test-cluster"
namespace := "default" namespace := "default"
@ -300,77 +241,13 @@ func TestMigrateGp3Support(t *testing.T) {
ClusterLabels: map[string]string{"application": "spilo"}, ClusterLabels: map[string]string{"application": "spilo"},
ClusterNameLabel: "cluster-name", ClusterNameLabel: "cluster-name",
}, },
StorageResizeMode: "mixed", StorageResizeMode: "mixed",
EnableEBSGp3Migration: false,
EnableEBSGp3MigrationMaxSize: 1000,
}, },
}, client, acidv1.Postgresql{}, 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
cluster.Name = clusterName
cluster.Namespace = namespace
filterLabels := cluster.labelsSet(false)
testVolumes := []testVolume{testVol, testVol, testVol}
initTestVolumesAndPods(cluster.KubeClient, namespace, clusterName, filterLabels, testVolumes)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
resizer := mocks.NewMockVolumeResizer(ctrl)
resizer.EXPECT().ExtractVolumeID(gomock.Eq("aws://eu-central-1b/ebs-volume-1")).Return("ebs-volume-1", nil)
resizer.EXPECT().ExtractVolumeID(gomock.Eq("aws://eu-central-1b/ebs-volume-2")).Return("ebs-volume-2", nil)
resizer.EXPECT().ExtractVolumeID(gomock.Eq("aws://eu-central-1b/ebs-volume-3")).Return("ebs-volume-3", nil)
resizer.EXPECT().GetProviderVolumeID(gomock.Any()).
DoAndReturn(func(pv *v1.PersistentVolume) (string, error) {
return resizer.ExtractVolumeID(pv.Spec.AWSElasticBlockStore.VolumeID)
}).
Times(3)
resizer.EXPECT().DescribeVolumes(gomock.Eq([]string{"ebs-volume-1", "ebs-volume-2", "ebs-volume-3"})).Return(
[]volumes.VolumeProperties{
{VolumeID: "ebs-volume-1", VolumeType: "gp3", Size: 100, Iops: 3000},
{VolumeID: "ebs-volume-2", VolumeType: "gp3", Size: 105, Iops: 4000},
{VolumeID: "ebs-volume-3", VolumeType: "gp3", Size: 151, Iops: 6000, Throughput: 275}}, nil)
// expect only gp2 volume to be modified
resizer.EXPECT().ModifyVolume(gomock.Eq("ebs-volume-1"), gomock.Eq(aws.String("gp3")), gomock.Eq(aws.Int64(150)), 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.Eq(aws.Int64(150)), gomock.Eq(aws.Int64(6000)), gomock.Eq(aws.Int64(275))).Return(nil)
// resizer.EXPECT().ModifyVolume(gomock.Eq("ebs-volume-3"), gomock.Eq(aws.String("gp3")), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
cluster.VolumeResizer = resizer
cluster.syncVolumes()
}
func TestManualGp2Gp3Support(t *testing.T) {
client, _ := newFakeK8sPVCclient()
clusterName := "acid-test-cluster"
namespace := "default"
// new cluster with pvc storage resize mode and configured labels
var cluster = New(
Config{
OpConfig: config.Config{
Resources: config.Resources{
ClusterLabels: map[string]string{"application": "spilo"},
ClusterNameLabel: "cluster-name",
},
StorageResizeMode: "mixed",
EnableEBSGp3Migration: false,
EnableEBSGp3MigrationMaxSize: 1000,
},
}, client, acidv1.Postgresql{}, logger, eventRecorder)
cluster.Spec.Volume.Size = "150Gi"
cluster.Spec.Volume.Iops = aws.Int64(6000)
cluster.Spec.Volume.Throughput = aws.Int64(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
@ -402,14 +279,14 @@ func TestManualGp2Gp3Support(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()
} }
func TestDontTouchType(t *testing.T) { func TestNoVolumeTypeChange(t *testing.T) {
client, _ := newFakeK8sPVCclient() client, _ := newFakeK8sPVCclient()
clusterName := "acid-test-cluster" clusterName := "acid-test-cluster"
namespace := "default" namespace := "default"
@ -422,9 +299,7 @@ func TestDontTouchType(t *testing.T) {
ClusterLabels: map[string]string{"application": "spilo"}, ClusterLabels: map[string]string{"application": "spilo"},
ClusterNameLabel: "cluster-name", ClusterNameLabel: "cluster-name",
}, },
StorageResizeMode: "mixed", StorageResizeMode: "mixed",
EnableEBSGp3Migration: false,
EnableEBSGp3MigrationMaxSize: 1000,
}, },
}, client, acidv1.Postgresql{}, logger, eventRecorder) }, client, acidv1.Postgresql{}, logger, eventRecorder)
@ -467,8 +342,8 @@ func TestDontTouchType(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

@ -181,8 +181,6 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.WALAZStorageAccount = fromCRD.AWSGCP.WALAZStorageAccount result.WALAZStorageAccount = fromCRD.AWSGCP.WALAZStorageAccount
result.AdditionalSecretMount = fromCRD.AWSGCP.AdditionalSecretMount result.AdditionalSecretMount = fromCRD.AWSGCP.AdditionalSecretMount
result.AdditionalSecretMountPath = fromCRD.AWSGCP.AdditionalSecretMountPath result.AdditionalSecretMountPath = fromCRD.AWSGCP.AdditionalSecretMountPath
result.EnableEBSGp3Migration = fromCRD.AWSGCP.EnableEBSGp3Migration
result.EnableEBSGp3MigrationMaxSize = util.CoalesceInt64(fromCRD.AWSGCP.EnableEBSGp3MigrationMaxSize, 1000)
// logical backup config // logical backup config
result.LogicalBackupSchedule = util.Coalesce(fromCRD.LogicalBackup.Schedule, "30 00 * * *") result.LogicalBackupSchedule = util.Coalesce(fromCRD.LogicalBackup.Schedule, "30 00 * * *")

View File

@ -200,8 +200,6 @@ type Config struct {
WALAZStorageAccount string `name:"wal_az_storage_account"` WALAZStorageAccount string `name:"wal_az_storage_account"`
AdditionalSecretMount string `name:"additional_secret_mount"` AdditionalSecretMount string `name:"additional_secret_mount"`
AdditionalSecretMountPath string `name:"additional_secret_mount_path"` AdditionalSecretMountPath string `name:"additional_secret_mount_path"`
EnableEBSGp3Migration bool `name:"enable_ebs_gp3_migration" default:"false"`
EnableEBSGp3MigrationMaxSize int64 `name:"enable_ebs_gp3_migration_max_size" default:"1000"`
DebugLogging bool `name:"debug_logging" default:"true"` DebugLogging bool `name:"debug_logging" default:"true"`
EnableDBAccess bool `name:"enable_database_access" default:"true"` EnableDBAccess bool `name:"enable_database_access" default:"true"`
EnableTeamsAPI bool `name:"enable_teams_api" default:"true"` EnableTeamsAPI bool `name:"enable_teams_api" default:"true"`

View File

@ -89,21 +89,21 @@ func (r *EBSVolumeResizer) DescribeVolumes(volumeIds []string) ([]VolumeProperti
} }
for _, v := range volumeOutput.Volumes { for _, v := range volumeOutput.Volumes {
switch v.VolumeType { vp := VolumeProperties{VolumeID: *v.VolumeId, Size: int32(*v.Size), VolumeType: string(v.VolumeType)}
case "gp3": if v.Iops != nil {
p = append(p, VolumeProperties{VolumeID: *v.VolumeId, Size: int64(*v.Size), VolumeType: string(v.VolumeType), Iops: int64(*v.Iops), Throughput: int64(*v.Throughput)}) vp.Iops = int32(*v.Iops)
case "gp2":
p = append(p, VolumeProperties{VolumeID: *v.VolumeId, Size: int64(*v.Size), VolumeType: string(v.VolumeType)})
default:
return nil, fmt.Errorf("discovered unexpected volume type %s %s", *v.VolumeId, v.VolumeType)
} }
if v.Throughput != nil {
vp.Throughput = int32(*v.Throughput)
}
p = append(p, vp)
} }
return p, nil return p, nil
} }
// 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",