Skip to content

Commit e4c7b88

Browse files
committed
Fix PVC comparison to use decimal equality (CloudNativePG pattern)
The operator was incorrectly reporting "shrinking persistent volumes is not supported" errors due to inconsistent Quantity comparison and tests that validated internal representation instead of logical equality. Changes: 1. Adopt CloudNativePG's PVC comparison pattern - Use `.AsDec().Cmp()` for explicit decimal comparison - Switch statement structure for clarity (equal/shrink/expand) - Ensures "10Gi" correctly equals "10737418240" bytes 2. Fix tests to verify behavior, not implementation - Added `BeEquivalentToQuantity()` custom matcher - Compares Quantity values using decimal representation - Replaces `Equal()` which tested internal struct fields 3. Enhance error messages and logging - Show actual capacity values in error messages - Add debug logging at `-v=1` level for troubleshooting - Example: "shrinking not supported (existing: 10Gi, desired: 5Gi)" Root Cause: The original tests were checking if Quantity internal representations matched exactly (int64 vs decimal fields), rather than checking if the values were logically equivalent. This masked potential comparison bugs. Inspiration: CloudNativePG's `pkg/reconciler/persistentvolumeclaim/existing.go` demonstrates this pattern as a best practice in the Kubernetes operator community for handling PVC capacity comparisons. Files Modified: - internal/scaling/scaling.go - Use .AsDec().Cmp() comparison - internal/scaling/scaling_suite_test.go - Add BeEquivalentToQuantity matcher - internal/scaling/scaling_test.go - Use logical equality matcher - controllers/reconcile_persistence_test.go - Use ContainSubstring for errors Fixes: #2023
1 parent e20d5f4 commit e4c7b88

File tree

4 files changed

+56
-15
lines changed

4 files changed

+56
-15
lines changed

controllers/reconcile_persistence_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ var _ = Describe("Persistence", func() {
7373
}
7474
}
7575
return "ReconcileSuccess status: condition not present"
76-
}, 5).Should(Equal("ReconcileSuccess status: False, " +
76+
}, 5).Should(ContainSubstring("ReconcileSuccess status: False, " +
7777
"with reason: FailedReconcilePVC " +
7878
"and message: shrinking persistent volumes is not supported"))
7979
})

internal/scaling/scaling.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,24 @@ func (p PersistenceScaler) Scale(ctx context.Context, rmq rabbitmqv1beta1.Rabbit
4646
return errors.New(msg)
4747
}
4848

49-
// desired storage capacity is smaller than the current capacity; we can't proceed lest we lose data
50-
if err == nil && existingCapacity.Cmp(desiredCapacity) == 1 {
51-
msg := "shrinking persistent volumes is not supported"
52-
logger.Error(errors.New("unsupported operation"), msg)
53-
return errors.New(msg)
49+
// Compare storage capacities using decimal representation to handle different units correctly
50+
// This approach follows CloudNativePG's PVC comparison pattern (pkg/reconciler/persistentvolumeclaim/existing.go)
51+
// Using AsDec() ensures "10Gi" equals "10737418240" and "1Gi" equals "1024Mi"
52+
if err == nil {
53+
switch existingCapacity.AsDec().Cmp(desiredCapacity.AsDec()) {
54+
case 0:
55+
// Sizes are equal in StatefulSet template - but we still need to check individual PVCs
56+
logger.V(1).Info("StatefulSet PVC template capacity matches desired", "capacity", existingCapacity.String())
57+
case 1:
58+
// Current > desired (shrink) - not allowed
59+
msg := fmt.Sprintf("shrinking persistent volumes is not supported (existing: %s, desired: %s)",
60+
existingCapacity.String(), desiredCapacity.String())
61+
logger.Error(errors.New("unsupported operation"), msg)
62+
return errors.New(msg)
63+
default:
64+
// Current < desired (expand) - proceed with resize
65+
logger.Info("PVC expansion required", "existing", existingCapacity.String(), "desired", desiredCapacity.String())
66+
}
5467
}
5568

5669
existingPVCs, err := p.getClusterPVCs(ctx, rmq)

internal/scaling/scaling_suite_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,31 @@ func (matcher *updateActionMatcher) NegatedFailureMessage(actual any) string {
236236
return fmt.Sprintf("Expected '%s' on resource '%s' named '%s' in namespace '%s' not to match the observed action:\n%+v\n",
237237
matcher.expectedVerb, matcher.expectedResourceType, matcher.expectedResourceName, matcher.expectedNamespace, matcher.actualAction)
238238
}
239+
240+
// BeEquivalentToQuantity compares Quantity values logically using decimal representation
241+
// This avoids issues with different internal representations (int64 vs decimal)
242+
func BeEquivalentToQuantity(expected k8sresource.Quantity) types.GomegaMatcher {
243+
return &quantityEquivalenceMatcher{expected: expected}
244+
}
245+
246+
type quantityEquivalenceMatcher struct {
247+
expected k8sresource.Quantity
248+
}
249+
250+
func (m *quantityEquivalenceMatcher) Match(actual interface{}) (bool, error) {
251+
actualQty, ok := actual.(k8sresource.Quantity)
252+
if !ok {
253+
return false, fmt.Errorf("BeEquivalentToQuantity expects a resource.Quantity, got %T", actual)
254+
}
255+
// Compare using decimal representation for logical equality
256+
// This handles cases like "10Gi" vs "10737418240" correctly
257+
return actualQty.AsDec().Cmp(m.expected.AsDec()) == 0, nil
258+
}
259+
260+
func (m *quantityEquivalenceMatcher) FailureMessage(actual interface{}) string {
261+
return fmt.Sprintf("Expected quantity %v to equal %v", actual, m.expected)
262+
}
263+
264+
func (m *quantityEquivalenceMatcher) NegatedFailureMessage(actual interface{}) string {
265+
return fmt.Sprintf("Expected quantity %v not to equal %v", actual, m.expected)
266+
}

internal/scaling/scaling_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ var _ = Describe("Scaling", func() {
5656
"Spec": MatchFields(IgnoreExtras, Fields{
5757
"Resources": MatchFields(IgnoreExtras, Fields{
5858
"Requests": MatchAllKeys(Keys{
59-
corev1.ResourceStorage: Equal(fifteenG),
59+
corev1.ResourceStorage: BeEquivalentToQuantity(fifteenG),
6060
}),
6161
}),
6262
}),
@@ -93,7 +93,7 @@ var _ = Describe("Scaling", func() {
9393
"Spec": MatchFields(IgnoreExtras, Fields{
9494
"Resources": MatchFields(IgnoreExtras, Fields{
9595
"Requests": MatchAllKeys(Keys{
96-
corev1.ResourceStorage: Equal(fifteenG),
96+
corev1.ResourceStorage: BeEquivalentToQuantity(fifteenG),
9797
}),
9898
}),
9999
}),
@@ -107,7 +107,7 @@ var _ = Describe("Scaling", func() {
107107
initialAPIObjects = []runtime.Object{&existingSts, &existingPVC}
108108
})
109109
It("raises an error", func() {
110-
Expect(persistenceScaler.Scale(context.Background(), rmq, oneG)).To(MatchError("shrinking persistent volumes is not supported"))
110+
Expect(persistenceScaler.Scale(context.Background(), rmq, oneG)).To(MatchError(ContainSubstring("shrinking persistent volumes is not supported")))
111111
Expect(fakeClientset.Actions()).To(MatchAllElementsWithIndex(IndexIdentity, Elements{
112112
"0": beGetActionOnResource("statefulsets", "rabbit-server", namespace),
113113
}))
@@ -158,7 +158,7 @@ var _ = Describe("Scaling", func() {
158158
"Spec": MatchFields(IgnoreExtras, Fields{
159159
"Resources": MatchFields(IgnoreExtras, Fields{
160160
"Requests": MatchAllKeys(Keys{
161-
corev1.ResourceStorage: Equal(fifteenG),
161+
corev1.ResourceStorage: BeEquivalentToQuantity(fifteenG),
162162
}),
163163
}),
164164
}),
@@ -168,7 +168,7 @@ var _ = Describe("Scaling", func() {
168168
"Spec": MatchFields(IgnoreExtras, Fields{
169169
"Resources": MatchFields(IgnoreExtras, Fields{
170170
"Requests": MatchAllKeys(Keys{
171-
corev1.ResourceStorage: Equal(fifteenG),
171+
corev1.ResourceStorage: BeEquivalentToQuantity(fifteenG),
172172
}),
173173
}),
174174
}),
@@ -178,7 +178,7 @@ var _ = Describe("Scaling", func() {
178178
"Spec": MatchFields(IgnoreExtras, Fields{
179179
"Resources": MatchFields(IgnoreExtras, Fields{
180180
"Requests": MatchAllKeys(Keys{
181-
corev1.ResourceStorage: Equal(fifteenG),
181+
corev1.ResourceStorage: BeEquivalentToQuantity(fifteenG),
182182
}),
183183
}),
184184
}),
@@ -209,7 +209,7 @@ var _ = Describe("Scaling", func() {
209209
"Spec": MatchFields(IgnoreExtras, Fields{
210210
"Resources": MatchFields(IgnoreExtras, Fields{
211211
"Requests": MatchAllKeys(Keys{
212-
corev1.ResourceStorage: Equal(fifteenG),
212+
corev1.ResourceStorage: BeEquivalentToQuantity(fifteenG),
213213
}),
214214
}),
215215
}),
@@ -219,7 +219,7 @@ var _ = Describe("Scaling", func() {
219219
"Spec": MatchFields(IgnoreExtras, Fields{
220220
"Resources": MatchFields(IgnoreExtras, Fields{
221221
"Requests": MatchAllKeys(Keys{
222-
corev1.ResourceStorage: Equal(fifteenG),
222+
corev1.ResourceStorage: BeEquivalentToQuantity(fifteenG),
223223
}),
224224
}),
225225
}),
@@ -251,7 +251,7 @@ var _ = Describe("Scaling", func() {
251251
"Spec": MatchFields(IgnoreExtras, Fields{
252252
"Resources": MatchFields(IgnoreExtras, Fields{
253253
"Requests": MatchAllKeys(Keys{
254-
corev1.ResourceStorage: Equal(fifteenG),
254+
corev1.ResourceStorage: BeEquivalentToQuantity(fifteenG),
255255
}),
256256
}),
257257
}),

0 commit comments

Comments
 (0)