From a84761246cc5c35665c7f602f4736c69fc62d852 Mon Sep 17 00:00:00 2001 From: Conor Gallagher Date: Fri, 6 Nov 2026 11:55:03 +0000 Subject: [PATCH] fix: match StatefulSet volumeClaimTemplates storage quantities in SSA matcher The API server stores PersistentVolumeClaim template storage requests in canonical form, so a desired storage of 1024Mi is stored as 1Gi and the dependent StatefulSet was updated on every reconciliation. Normalise the volumeClaimTemplates resource requests with the shared quantity check used for pod template resources. Signed-off-by: Conor Gallagher --- ...BasedGenericKubernetesResourceMatcher.java | 13 +++++ ...dGenericKubernetesResourceMatcherTest.java | 3 +- ...lates-desired-with-equivalent-storage.yaml | 50 +++++++++++++++++++ ...lSetDesiredSanitizerDependentResource.java | 10 ++++ 4 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/sample-sts-volumeclaimtemplates-desired-with-equivalent-storage.yaml diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java index d31e4f6e7e..1945609079 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java @@ -231,6 +231,7 @@ protected void sanitizeState(R actual, R desired, Map actualMap) } } + @SuppressWarnings("unchecked") private static void sanitizeVolumeClaimTemplates( Map actualMap, StatefulSetSpec actualSpec, StatefulSetSpec desiredSpec) { int claims = desiredSpec.getVolumeClaimTemplates().size(); @@ -251,6 +252,18 @@ private static void sanitizeVolumeClaimTemplates( .map(Map.class::cast) .ifPresent(m -> m.remove("status")); } + var desiredResources = claim.getSpec().getResources(); + var actualResources = actualSpec.getVolumeClaimTemplates().get(i).getSpec().getResources(); + if (desiredResources != null && actualResources != null) { + Optional.ofNullable( + GenericKubernetesResource.get( + actualMap, "spec", "volumeClaimTemplates", i, "spec", "resources", "requests")) + .map(Map.class::cast) + .ifPresent( + m -> + QuantitySanitizer.sanitizeQuantities( + m, actualResources.getRequests(), desiredResources.getRequests())); + } } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcherTest.java index fda7c4fad3..eb7e54f492 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcherTest.java @@ -253,7 +253,8 @@ void withFinalizer() { strings = { "sample-sts-volumeclaimtemplates-desired.yaml", "sample-sts-volumeclaimtemplates-desired-with-status.yaml", - "sample-sts-volumeclaimtemplates-desired-with-volumemode.yaml" + "sample-sts-volumeclaimtemplates-desired-with-volumemode.yaml", + "sample-sts-volumeclaimtemplates-desired-with-equivalent-storage.yaml" }) void testSanitizeState_statefulSetWithVolumeClaims(String desiredResourceFileName) { var desiredStatefulSet = loadResource(desiredResourceFileName, StatefulSet.class); diff --git a/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/sample-sts-volumeclaimtemplates-desired-with-equivalent-storage.yaml b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/sample-sts-volumeclaimtemplates-desired-with-equivalent-storage.yaml new file mode 100644 index 0000000000..f8daf9d100 --- /dev/null +++ b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/sample-sts-volumeclaimtemplates-desired-with-equivalent-storage.yaml @@ -0,0 +1,50 @@ +# +# Copyright Java Operator SDK Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# desired StatefulSet with a VolumeClaimTemplate with a numerically equal, non-canonical storage quantity +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: "test" +spec: + replicas: 1 + selector: + matchLabels: + app: test-app + serviceName: "nginx-service" + template: + metadata: + labels: + app: test-app + spec: + containers: + - name: nginx + image: nginx:1.17.0 + ports: + - containerPort: 80 + volumeMounts: + - name: persistent-storage + mountPath: /usr/share/nginx/html + volumeClaimTemplates: + - metadata: + name: persistent-storage + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1024Mi + storageClassName: standard diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerDependentResource.java index e4bcaac460..2c40bedc25 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerDependentResource.java @@ -16,6 +16,7 @@ package io.javaoperatorsdk.operator.dependent.statefulsetdesiredsanitizer; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.Quantity; import io.fabric8.kubernetes.api.model.apps.StatefulSet; import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.reconciler.Context; @@ -39,6 +40,15 @@ protected StatefulSet desired( .withName(primary.getMetadata().getName()) .withNamespace(primary.getMetadata().getNamespace()) .build()); + // the API server stores 1024Mi as 1Gi + template + .getSpec() + .getVolumeClaimTemplates() + .get(0) + .getSpec() + .getResources() + .getRequests() + .put("storage", new Quantity("1024Mi")); return template; }