Skip to content

Commit 8d323cc

Browse files
committed
Refactor Volume Auto Grow to support additional volume types
This commit refactors the existing pgData volume auto grow code to better support upcoming feature enhancements relating to auto grow capability for the pgBackRest repository volume and pg_wal volume. Issue: PGO-2606
1 parent c829c06 commit 8d323cc

File tree

9 files changed

+841
-597
lines changed

9 files changed

+841
-597
lines changed
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// Copyright 2021 - 2025 Crunchy Data Solutions, Inc.
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package postgrescluster
6+
7+
import (
8+
"context"
9+
10+
corev1 "k8s.io/api/core/v1"
11+
"k8s.io/apimachinery/pkg/api/resource"
12+
13+
"github.com/crunchydata/postgres-operator/internal/feature"
14+
"github.com/crunchydata/postgres-operator/internal/logging"
15+
"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
16+
)
17+
18+
// storeDesiredRequest saves the appropriate request value to the PostgresCluster
19+
// status. If the value has grown, create an Event.
20+
func (r *Reconciler) storeDesiredRequest(
21+
ctx context.Context, cluster *v1beta1.PostgresCluster,
22+
volumeType, instanceSetName, desiredRequest, desiredRequestBackup string,
23+
) string {
24+
var current resource.Quantity
25+
var previous resource.Quantity
26+
var err error
27+
log := logging.FromContext(ctx)
28+
29+
// Parse the desired request from the cluster's status.
30+
if desiredRequest != "" {
31+
current, err = resource.ParseQuantity(desiredRequest)
32+
if err != nil {
33+
log.Error(err, "Unable to parse "+volumeType+" volume request from status ("+
34+
desiredRequest+") for "+cluster.Name+"/"+instanceSetName)
35+
// If there was an error parsing the value, treat as unset (equivalent to zero).
36+
desiredRequest = ""
37+
current, _ = resource.ParseQuantity("")
38+
39+
}
40+
}
41+
42+
// Parse the desired request from the status backup.
43+
if desiredRequestBackup != "" {
44+
previous, err = resource.ParseQuantity(desiredRequestBackup)
45+
if err != nil {
46+
log.Error(err, "Unable to parse "+volumeType+" volume request from status backup ("+
47+
desiredRequestBackup+") for "+cluster.Name+"/"+instanceSetName)
48+
// If there was an error parsing the value, treat as unset (equivalent to zero).
49+
desiredRequestBackup = ""
50+
previous, _ = resource.ParseQuantity("")
51+
52+
}
53+
}
54+
55+
// determine if the appropriate volume limit is set
56+
limitSet := limitIsSet(cluster, volumeType, instanceSetName)
57+
58+
if limitSet && current.Value() > previous.Value() {
59+
r.Recorder.Eventf(cluster, corev1.EventTypeNormal, "VolumeAutoGrow",
60+
"%s volume expansion to %v requested for %s/%s.",
61+
volumeType, current.String(), cluster.Name, instanceSetName)
62+
}
63+
64+
// If the desired size was not observed, update with previously stored value.
65+
// This can happen in scenarios where the annotation on the Pod is missing
66+
// such as when the cluster is shutdown or a Pod is in the middle of a restart.
67+
if desiredRequest == "" {
68+
desiredRequest = desiredRequestBackup
69+
}
70+
71+
return desiredRequest
72+
}
73+
74+
// limitIsSet determines if the limit is set for a given volume type and returns
75+
// a corresponding boolean value
76+
func limitIsSet(cluster *v1beta1.PostgresCluster, volumeType, instanceSetName string) bool {
77+
78+
var limitSet bool
79+
80+
switch volumeType {
81+
82+
// Cycle through the instance sets to ensure the correct limit is identified.
83+
case "pgData":
84+
for _, specInstance := range cluster.Spec.InstanceSets {
85+
if specInstance.Name == instanceSetName {
86+
limitSet = !specInstance.DataVolumeClaimSpec.Resources.Limits.Storage().IsZero()
87+
}
88+
}
89+
}
90+
// TODO: Add cases for pgWAL and repo volumes
91+
92+
return limitSet
93+
94+
}
95+
96+
// setVolumeSize compares the potential sizes from the instance spec, status
97+
// and limit and sets the appropriate current value.
98+
func (r *Reconciler) setVolumeSize(ctx context.Context, cluster *v1beta1.PostgresCluster,
99+
pvc *corev1.PersistentVolumeClaim, volumeType, instanceSpecName string) {
100+
101+
log := logging.FromContext(ctx)
102+
103+
// Store the limit for this instance set. This value will not change below.
104+
volumeLimitFromSpec := pvc.Spec.Resources.Limits.Storage()
105+
106+
// This value will capture our desired update.
107+
volumeRequestSize := pvc.Spec.Resources.Requests.Storage()
108+
109+
// A limit of 0 is ignorned, so the volume request is used.
110+
if volumeLimitFromSpec.IsZero() {
111+
return
112+
}
113+
114+
// If the request value is greater than the set limit, use the limit and issue
115+
// a warning event.
116+
if volumeRequestSize.Value() > volumeLimitFromSpec.Value() {
117+
r.Recorder.Eventf(cluster, corev1.EventTypeWarning, "VolumeRequestOverLimit",
118+
"%s volume request (%v) for %s/%s is greater than set limit (%v). Limit value will be used.",
119+
volumeType, volumeRequestSize, cluster.Name, instanceSpecName, volumeLimitFromSpec)
120+
121+
pvc.Spec.Resources.Requests = corev1.ResourceList{
122+
corev1.ResourceStorage: *resource.NewQuantity(volumeLimitFromSpec.Value(), resource.BinarySI),
123+
}
124+
// Otherwise, if the feature gate is not enabled, do not autogrow.
125+
} else if feature.Enabled(ctx, feature.AutoGrowVolumes) {
126+
127+
// determine the appropriate volume request based on what's set in the status
128+
if dpv, err := getDesiredVolumeSize(
129+
cluster, volumeType, instanceSpecName, volumeRequestSize,
130+
); err != nil {
131+
log.Error(err, "For "+cluster.Name+"/"+instanceSpecName+
132+
": Unable to parse "+volumeType+" volume request: "+dpv)
133+
}
134+
135+
// If the volume request size is greater than or equal to the limit and the
136+
// limit is not zero, update the request size to the limit value.
137+
// If the user manually requests a lower limit that is smaller than the current
138+
// or requested volume size, it will be ignored in favor of the limit value.
139+
if volumeRequestSize.Value() >= volumeLimitFromSpec.Value() {
140+
141+
r.Recorder.Eventf(cluster, corev1.EventTypeNormal, "VolumeLimitReached",
142+
"%s volume(s) for %s/%s are at size limit (%v).", volumeType,
143+
cluster.Name, instanceSpecName, volumeLimitFromSpec)
144+
145+
// If the volume size request is greater than the limit, issue an
146+
// additional event warning.
147+
if volumeRequestSize.Value() > volumeLimitFromSpec.Value() {
148+
r.Recorder.Eventf(cluster, corev1.EventTypeWarning, "DesiredVolumeAboveLimit",
149+
"The desired size (%v) for the %s/%s %s volume(s) is greater than the size limit (%v).",
150+
volumeRequestSize, cluster.Name, instanceSpecName, volumeType, volumeLimitFromSpec)
151+
}
152+
153+
volumeRequestSize = volumeLimitFromSpec
154+
}
155+
pvc.Spec.Resources.Requests = corev1.ResourceList{
156+
corev1.ResourceStorage: *resource.NewQuantity(volumeRequestSize.Value(), resource.BinarySI),
157+
}
158+
}
159+
}
160+
161+
// getDesiredVolumeSize compares the volume request size to the suggested autogrow
162+
// size stored in the status and updates the value when the status value is larger.
163+
func getDesiredVolumeSize(cluster *v1beta1.PostgresCluster,
164+
volumeType, instanceSpecName string,
165+
volumeRequestSize *resource.Quantity) (string, error) {
166+
167+
switch volumeType {
168+
case "pgData":
169+
for i := range cluster.Status.InstanceSets {
170+
if instanceSpecName == cluster.Status.InstanceSets[i].Name {
171+
for _, dpv := range cluster.Status.InstanceSets[i].DesiredPGDataVolume {
172+
if dpv != "" {
173+
desiredRequest, err := resource.ParseQuantity(dpv)
174+
if err == nil {
175+
if desiredRequest.Value() > volumeRequestSize.Value() {
176+
*volumeRequestSize = desiredRequest
177+
}
178+
} else {
179+
return dpv, err
180+
}
181+
}
182+
}
183+
}
184+
}
185+
// TODO: Add cases for pgWAL and repo volumes (requires relevant status sections)
186+
}
187+
return "", nil
188+
}

0 commit comments

Comments
 (0)