Merge branch 'master' into trent--pooler-priority

This commit is contained in:
Trent Schmidt 2026-07-27 09:06:41 -07:00 committed by GitHub
commit 8d67543106
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 133 additions and 342 deletions

View File

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

View File

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

View File

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

View File

@ -350,16 +350,12 @@ configAwsOrGcp:
# AWS region used to store EBS volumes
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: ""
# AWS IAM role to supply in the iam.amazonaws.com/role annotation of Postgres pods
# kube_iam_role: ""
# Full ARN for IRSA (IAM Roles for Service Accounts) on EKS
# irsa_role_arn: ""

View File

@ -639,9 +639,11 @@ masters in single-node clusters and/or the last remaining running instance in a
cluster.
## PDB for critical operations
The `MinAvailable` parameter of this PDB is equal to the `numberOfInstances` set in the
cluster manifest, while label selector includes `critical-operation=true` condition. This
allows to protect all pods of a cluster, given they are labeled accordingly.
The `MaxUnavailable` parameter of this PDB is set to `0`, while label selector includes
`critical-operation=true` condition. This blocks voluntary disruptions for all pods of a
cluster that are labeled accordingly, without leaving an unsatisfiable budget behind when
no pods carry the label (which previously kept monitoring alerts like
`KubePdbNotEnoughHealthyPods` firing permanently).
For example, Operator labels all Spilo pods with `critical-operation=true` during the major
version upgrade run. You may want to protect cluster pods during other critical operations
by assigning the label to pods yourself or using other means of automation.
@ -651,7 +653,14 @@ The PDB is only relaxed in two scenarios:
* If a cluster is scaled down to `0` instances (e.g. for draining nodes)
* If the PDB is disabled in the configuration (`enable_pod_disruption_budget`)
The PDBs are still in place having `MinAvailable` set to `0`. Disabling PDBs
The PDBs are still in place but fully relaxed: the primary PDB with `MinAvailable`
set to `0` and the critical operations PDB with `MaxUnavailable` set to `100%`.
The two PDBs intentionally use different budget fields matching their purposes:
the primary PDB guarantees a minimum count of always-present pods
(`MinAvailable`), while the critical operations PDB freezes disruptions for
whatever pods currently carry the `critical-operation=true` label - a
usually-empty set, which `MaxUnavailable: 0` expresses without producing an
unsatisfiable budget while idle. Disabling PDBs
helps avoiding blocking Kubernetes upgrades in managed K8s environments at the
cost of prolonged DB downtime. See PR [#384](https://github.com/zalando/postgres-operator/pull/384)
for the use case.

View File

@ -554,11 +554,11 @@ properties of the persistent storage that stores Postgres data.
* **iops**
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**
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**
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
2. `pvc` : operator only changes PVC definition
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".
## Kubernetes resource requests
@ -821,16 +821,6 @@ yet officially supported.
Path to mount the above Secret in the filesystem of the container(s).
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
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.
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
volume specs to no more than once every 6 hours.
volume specs to no more than 4 times within 24 hours.
```yaml
spec:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -167,9 +167,11 @@ type Volume struct {
StorageClass string `json:"storageClass,omitempty"`
SubPath string `json:"subPath,omitempty"`
IsSubPathExpr *bool `json:"isSubPathExpr,omitempty"`
Iops *int64 `json:"iops,omitempty"`
Throughput *int64 `json:"throughput,omitempty"`
VolumeType string `json:"type,omitempty"`
// +kubebuilder:validation:Maximum=80000
Iops *int32 `json:"iops,omitempty"`
// +kubebuilder:validation:Maximum=2000
Throughput *int32 `json:"throughput,omitempty"`
VolumeType string `json:"type,omitempty"`
}
// AdditionalVolume specs additional optional volumes for statefulset

View File

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

View File

@ -176,7 +176,7 @@ func New(cfg Config, kubeClient k8sutil.KubernetesClient, pgSpec acidv1.Postgres
cluster.eventRecorder = eventRecorder
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}
}

View File

@ -2315,12 +2315,18 @@ func (c *Cluster) generatePrimaryPodDisruptionBudget() *policyv1.PodDisruptionBu
}
func (c *Cluster) generateCriticalOpPodDisruptionBudget() *policyv1.PodDisruptionBudget {
minAvailable := intstr.FromInt32(c.Spec.NumberOfInstances)
// MaxUnavailable: 0 blocks voluntary disruption of any pod carrying the
// critical-operation label, while keeping the budget satisfied when no
// pod matches (status.desiredHealthy stays 0 outside critical
// operations). The previous MinAvailable: N spec left desiredHealthy at
// N with zero matching pods during normal operation, permanently firing
// alerts like kube-prometheus-stack's KubePdbNotEnoughHealthyPods (#3020).
maxUnavailable := intstr.FromInt32(0)
pdbEnabled := c.OpConfig.EnablePodDisruptionBudget
// if PodDisruptionBudget is disabled or if there are no DB pods, set the budget to 0.
// if PodDisruptionBudget is disabled or if there are no DB pods, allow all disruptions.
if (pdbEnabled != nil && !(*pdbEnabled)) || c.Spec.NumberOfInstances <= 0 {
minAvailable = intstr.FromInt(0)
maxUnavailable = intstr.FromString("100%")
}
labels := c.labelsSet(false)
@ -2335,7 +2341,7 @@ func (c *Cluster) generateCriticalOpPodDisruptionBudget() *policyv1.PodDisruptio
OwnerReferences: c.ownerReferences(),
},
Spec: policyv1.PodDisruptionBudgetSpec{
MinAvailable: &minAvailable,
MaxUnavailable: &maxUnavailable,
Selector: &metav1.LabelSelector{
MatchLabels: labels,
},

View File

@ -2556,6 +2556,17 @@ func TestGeneratePodDisruptionBudget(t *testing.T) {
}
}
hasMaxUnavailable := func(expected string) func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error {
return func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error {
actual := podDisruptionBudget.Spec.MaxUnavailable.String()
if actual != expected {
return fmt.Errorf("PodDisruptionBudget MaxUnavailable is incorrect, got %s, expected %s",
actual, expected)
}
return nil
}
}
hasMinAvailable := func(expectedMinAvailable int) func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error {
return func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error {
actual := podDisruptionBudget.Spec.MinAvailable.IntVal
@ -2749,7 +2760,7 @@ func TestGeneratePodDisruptionBudget(t *testing.T) {
check: []func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error{
testPodDisruptionBudgetOwnerReference,
hasName("postgres-myapp-database-critical-op-pdb"),
hasMinAvailable(3),
hasMaxUnavailable("0"),
testLabelsAndSelectors(false),
},
},
@ -2766,7 +2777,7 @@ func TestGeneratePodDisruptionBudget(t *testing.T) {
check: []func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error{
testPodDisruptionBudgetOwnerReference,
hasName("postgres-myapp-database-critical-op-pdb"),
hasMinAvailable(0),
hasMaxUnavailable("100%"),
testLabelsAndSelectors(false),
},
},
@ -2783,7 +2794,7 @@ func TestGeneratePodDisruptionBudget(t *testing.T) {
check: []func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error{
testPodDisruptionBudgetOwnerReference,
hasName("postgres-myapp-database-critical-op-pdb"),
hasMinAvailable(0),
hasMaxUnavailable("100%"),
testLabelsAndSelectors(false),
},
},
@ -2800,7 +2811,7 @@ func TestGeneratePodDisruptionBudget(t *testing.T) {
check: []func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error{
testPodDisruptionBudgetOwnerReference,
hasName("postgres-myapp-database-critical-op-pdb"),
hasMinAvailable(3),
hasMaxUnavailable("0"),
testLabelsAndSelectors(false),
},
},

View File

@ -95,18 +95,10 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error {
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 {
return err
}
if c.OpConfig.EnableEBSGp3Migration && len(c.EBSVolumes) > 0 {
err = c.executeEBSMigration()
if nil != err {
return err
}
}
if err = c.syncPodServiceAccount(); err != nil {
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
c.logger.Info("could not find the cluster's logical backup job")
if err = c.createLogicalBackupJob(); err == nil {
c.logger.Infof("created missing logical backup job %s", jobName)
} else {

View File

@ -11,7 +11,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"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/util/constants"
"github.com/zalando/postgres-operator/pkg/util/filesystems"
@ -79,47 +78,35 @@ func (c *Cluster) syncUnderlyingEBSVolume() error {
}
targetSize := quantityToGigabyte(newSize)
awsGp3 := aws.String("gp3")
awsIo2 := aws.String("io2")
errors := make([]string, 0)
for _, volume := range c.EBSVolumes {
var modifyIops *int64
var modifyThroughput *int64
var modifySize *int64
var modifyIops *int32
var modifyThroughput *int32
var modifySize *int32
var modifyType *string
if targetValue.Iops != nil && *targetValue.Iops >= int64(3000) {
if volume.Iops != int64(*targetValue.Iops) {
if targetValue.Iops != nil && *targetValue.Iops >= int32(3000) {
if volume.Iops != int32(*targetValue.Iops) {
modifyIops = targetValue.Iops
}
}
if targetValue.Throughput != nil && *targetValue.Throughput >= int64(125) {
if volume.Throughput != int64(*targetValue.Throughput) {
if targetValue.Throughput != nil && *targetValue.Throughput >= int32(125) {
if volume.Throughput != int32(*targetValue.Throughput) {
modifyThroughput = targetValue.Throughput
}
}
if targetSize > int64(volume.Size) {
if targetSize > volume.Size {
modifySize = &targetSize
}
if modifyIops != nil || modifyThroughput != nil || modifySize != nil {
if modifyIops != nil || modifyThroughput != nil {
// 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 targetValue.VolumeType != "" && targetValue.VolumeType != volume.VolumeType {
modifyType = &targetValue.VolumeType
}
if modifyIops != nil || modifyThroughput != nil || modifySize != nil || modifyType != nil {
err = c.VolumeResizer.ModifyVolume(volume.VolumeID, modifyType, modifySize, modifyIops, modifyThroughput)
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))
@ -450,50 +437,6 @@ func getPodNameFromPersistentVolume(pv *v1.PersistentVolume) *spec.NamespacedNam
return &spec.NamespacedName{Namespace: namespace, Name: name}
}
func quantityToGigabyte(q resource.Quantity) int64 {
return 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
func quantityToGigabyte(q resource.Quantity) int32 {
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/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/zalando/postgres-operator/mocks"
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
@ -25,9 +24,9 @@ import (
)
type testVolume struct {
size int64
iops int64
throughtput int64
size int32
iops int32
throughtput int32
volType string
}
@ -121,7 +120,7 @@ func TestQuantityToGigabyte(t *testing.T) {
tests := []struct {
name string
quantityStr string
expected int64
expected int32
}{
{
"test with 1Gi",
@ -131,12 +130,12 @@ func TestQuantityToGigabyte(t *testing.T) {
{
"test with float",
"1.5Gi",
int64(1),
int32(1),
},
{
"test with 1000Mi",
"1000Mi",
int64(0),
int32(0),
},
}
@ -179,64 +178,6 @@ func CreatePVCs(namespace string, clusterName string, labels labels.Set, n int,
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) {
i := 0
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()
clusterName := "acid-test-cluster"
namespace := "default"
@ -300,77 +241,13 @@ func TestMigrateGp3Support(t *testing.T) {
ClusterLabels: map[string]string{"application": "spilo"},
ClusterNameLabel: "cluster-name",
},
StorageResizeMode: "mixed",
EnableEBSGp3Migration: false,
EnableEBSGp3MigrationMaxSize: 1000,
StorageResizeMode: "mixed",
},
}, 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.Iops = aws.Int64(6000)
cluster.Spec.Volume.Throughput = aws.Int64(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)
cluster.Spec.Volume.Iops = aws.Int32(6000)
cluster.Spec.Volume.Throughput = aws.Int32(275)
// set metadata, so that labels will get correct values
cluster.Name = clusterName
@ -402,14 +279,14 @@ func TestManualGp2Gp3Support(t *testing.T) {
}, nil)
// 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-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-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.Int32(6000)), gomock.Eq(aws.Int32(275))).Return(nil)
cluster.VolumeResizer = resizer
cluster.syncVolumes()
}
func TestDontTouchType(t *testing.T) {
func TestNoVolumeTypeChange(t *testing.T) {
client, _ := newFakeK8sPVCclient()
clusterName := "acid-test-cluster"
namespace := "default"
@ -422,9 +299,7 @@ func TestDontTouchType(t *testing.T) {
ClusterLabels: map[string]string{"application": "spilo"},
ClusterNameLabel: "cluster-name",
},
StorageResizeMode: "mixed",
EnableEBSGp3Migration: false,
EnableEBSGp3MigrationMaxSize: 1000,
StorageResizeMode: "mixed",
},
}, client, acidv1.Postgresql{}, logger, eventRecorder)
@ -467,8 +342,8 @@ func TestDontTouchType(t *testing.T) {
}, nil)
// 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-2"), 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.Int32(177)), gomock.Nil(), gomock.Nil()).Return(nil)
cluster.VolumeResizer = resizer
cluster.syncVolumes()

View File

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

View File

@ -201,8 +201,6 @@ type Config struct {
WALAZStorageAccount string `name:"wal_az_storage_account"`
AdditionalSecretMount string `name:"additional_secret_mount"`
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"`
EnableDBAccess bool `name:"enable_database_access" 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 {
switch v.VolumeType {
case "gp3":
p = append(p, VolumeProperties{VolumeID: *v.VolumeId, Size: int64(*v.Size), VolumeType: string(v.VolumeType), Iops: int64(*v.Iops), Throughput: int64(*v.Throughput)})
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)
vp := VolumeProperties{VolumeID: *v.VolumeId, Size: int32(*v.Size), VolumeType: string(v.VolumeType)}
if v.Iops != nil {
vp.Iops = int32(*v.Iops)
}
if v.Throughput != nil {
vp.Throughput = int32(*v.Throughput)
}
p = append(p, vp)
}
return p, nil
}
// 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 */
volumeOutput, err := r.connection.DescribeVolumes(context.TODO(), &ec2.DescribeVolumesInput{VolumeIds: []string{volumeID}})
if err != nil {
@ -113,12 +113,11 @@ func (r *EBSVolumeResizer) ResizeVolume(volumeID string, newSize int64) error {
if *vol.VolumeId != volumeID {
return fmt.Errorf("describe volume %q returned information about a non-matching volume %q", volumeID, *vol.VolumeId)
}
sizeInt32 := int32(newSize)
if *vol.Size == sizeInt32 {
if *vol.Size == newSize {
// nothing to do
return nil
}
input := ec2.ModifyVolumeInput{Size: &sizeInt32, VolumeId: &volumeID}
input := ec2.ModifyVolumeInput{Size: &newSize, VolumeId: &volumeID}
output, err := r.connection.ModifyVolume(context.TODO(), &input)
if err != nil {
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
func (r *EBSVolumeResizer) ModifyVolume(volumeID string, newType *string, newSize *int64, iops *int64, throughput *int64) error {
var sizeInt32 *int32
var iopsInt32 *int32
var throughputInt32 *int32
input := ec2.ModifyVolumeInput{
VolumeId: &volumeID,
}
func (r *EBSVolumeResizer) ModifyVolume(volumeID string, newType *string, newSize *int32, iops *int32, throughput *int32) error {
input := ec2.ModifyVolumeInput{VolumeId: &volumeID}
if newSize != nil {
s := int32(*newSize)
sizeInt32 = &s
input.Size = sizeInt32
input.Size = newSize
}
if iops != nil {
i := int32(*iops)
iopsInt32 = &i
input.Iops = iopsInt32
input.Iops = iops
}
if throughput != nil {
t := int32(*throughput)
throughputInt32 = &t
input.Throughput = throughputInt32
input.Throughput = throughput
}
if newType != nil {
input.VolumeType = types.VolumeType(*newType)

View File

@ -8,9 +8,9 @@ import v1 "k8s.io/api/core/v1"
type VolumeProperties struct {
VolumeID string
VolumeType string
Size int64
Iops int64
Throughput int64
Size int32
Iops int32
Throughput int32
}
// 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
GetProviderVolumeID(pv *v1.PersistentVolume) (string, error)
ExtractVolumeID(volumeID string) (string, error)
ResizeVolume(providerVolumeID string, newSize int64) error
ModifyVolume(providerVolumeID string, newType *string, newSize *int64, iops *int64, throughput *int64) error
ResizeVolume(providerVolumeID string, newSize int32) error
ModifyVolume(providerVolumeID string, newType *string, newSize *int32, iops *int32, throughput *int32) error
DisconnectFromProvider() error
DescribeVolumes(providerVolumesID []string) ([]VolumeProperties, error)
}

View File

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

View File

@ -86,9 +86,9 @@ COST_ELB = float(getenv('COST_ELB', 0.03)) # per hour
# maximum and limitation of IOPS and throughput
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))
LIMIT_THROUGHPUT = float(getenv('LIMIT_THROUGHPUT', 1000))
LIMIT_THROUGHPUT = float(getenv('LIMIT_THROUGHPUT', 2000))
# get the default value of core and memory
DEFAULT_MEMORY = getenv('DEFAULT_MEMORY', '300Mi')
DEFAULT_MEMORY_LIMIT = getenv('DEFAULT_MEMORY_LIMIT', '300Mi')

View File

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