Problem
Some fields must never move backwards once applied: a storage size must not shrink, a running version must not silently downgrade. An ocf mutation cannot express this because mutations are pure functions of desired state with no view of the live object, so the clamp has to be hoisted into the controller before the components are built. Every consumer then also needs the escape hatch: an annotation that sanctions the otherwise refused change once and is deleted after it is honored.
konsole-is/camunda-operator carries four copies: internal/controller/camundacluster/storage.go (recordIgnoredShrink), internal/controller/elasticsearchcluster/controller.go (keepAppliedStorageSize), internal/controller/databaseserver/controller.go (keepAppliedStorageSize, keepAppliedWALSize, shared keepAppliedSize), and internal/controller/databaseserver/version.go (keepRunningVersion, the same shape over a version instead of a quantity). Three of the four redeclare the identical private event constants StorageShrinkIgnored and Resize. The override side exists too: AllowVersionDowngradeAnnotation is read, acted on, and then deleted in internal/controller/camundacluster/downgrade.go, and the stamp-what-was-applied annotations (RequestedStorageSizeAnnotation, BrokerVersionAnnotation) implement the same idea by hand.
Approach
Two composable pieces on the mutation side:
mutation.KeepApplied(path string, cmp func(desired, applied any) bool): at apply time, compare the desired value at path with the live applied value and keep the applied one when the comparator refuses the move, emitting a framework-owned event (reason and action declared once) naming the ignored change.
mutation.OneShotOverrideAnnotation(key string): when the annotation is present, permit the otherwise refused change for this pass and remove the annotation in the same reconcile so it cannot sanction a second change.
This needs the mutation pipeline to see the live object at clamp time, which is the actual framework change; the two helpers are thin once that exists.
Verification
- The three storage-size copies in konsole-is/camunda-operator port to
KeepApplied and the triplicated event constants collapse into the framework vocabulary.
- The version downgrade flow ports to
KeepApplied plus OneShotOverrideAnnotation with the same observable behavior: refused without the annotation, honored once with it, annotation gone afterwards.
- A test covers: shrink refused with event, growth passes, override honors exactly one change, override annotation removed even when the change equals the applied value.
Context
Found while auditing konsole-is/camunda-operator for repeated hand-rolled patterns. Smallest change on the audit list and the most literal duplication, so a good first candidate if only one gets picked up.
Problem
Some fields must never move backwards once applied: a storage size must not shrink, a running version must not silently downgrade. An ocf mutation cannot express this because mutations are pure functions of desired state with no view of the live object, so the clamp has to be hoisted into the controller before the components are built. Every consumer then also needs the escape hatch: an annotation that sanctions the otherwise refused change once and is deleted after it is honored.
konsole-is/camunda-operator carries four copies:
internal/controller/camundacluster/storage.go(recordIgnoredShrink),internal/controller/elasticsearchcluster/controller.go(keepAppliedStorageSize),internal/controller/databaseserver/controller.go(keepAppliedStorageSize,keepAppliedWALSize, sharedkeepAppliedSize), andinternal/controller/databaseserver/version.go(keepRunningVersion, the same shape over a version instead of a quantity). Three of the four redeclare the identical private event constantsStorageShrinkIgnoredandResize. The override side exists too:AllowVersionDowngradeAnnotationis read, acted on, and then deleted ininternal/controller/camundacluster/downgrade.go, and the stamp-what-was-applied annotations (RequestedStorageSizeAnnotation,BrokerVersionAnnotation) implement the same idea by hand.Approach
Two composable pieces on the mutation side:
mutation.KeepApplied(path string, cmp func(desired, applied any) bool): at apply time, compare the desired value atpathwith the live applied value and keep the applied one when the comparator refuses the move, emitting a framework-owned event (reason and action declared once) naming the ignored change.mutation.OneShotOverrideAnnotation(key string): when the annotation is present, permit the otherwise refused change for this pass and remove the annotation in the same reconcile so it cannot sanction a second change.This needs the mutation pipeline to see the live object at clamp time, which is the actual framework change; the two helpers are thin once that exists.
Verification
KeepAppliedand the triplicated event constants collapse into the framework vocabulary.KeepAppliedplusOneShotOverrideAnnotationwith the same observable behavior: refused without the annotation, honored once with it, annotation gone afterwards.Context
Found while auditing konsole-is/camunda-operator for repeated hand-rolled patterns. Smallest change on the audit list and the most literal duplication, so a good first candidate if only one gets picked up.