Skip to content

Commit 69ca907

Browse files
Merge pull request #2044 from jsafrane/csidriver-apply-cleared
STOR-2627: Re-apply CSIDriver after disabled fields were cleared
2 parents 7b84eb9 + 58ff0df commit 69ca907

File tree

2 files changed

+96
-11
lines changed

2 files changed

+96
-11
lines changed

pkg/operator/resource/resourceapply/storage.go

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,10 @@ func storageClassNeedsRecreate(oldSC, newSC *storagev1.StorageClass) bool {
135135
return false
136136
}
137137

138-
// ApplyCSIDriver merges objectmeta, does not worry about anything else
138+
// ApplyCSIDriver merges objectmeta and tries to update spec if any of the required fields were cleared by the API server.
139+
// It assumes they were cleared due to a feature gate not enabled in the API server and it will be enabled soon.
140+
// When used by StaticResourceController, it will retry periodically and eventually save the spec with the field.
139141
func ApplyCSIDriver(ctx context.Context, client storageclientv1.CSIDriversGetter, recorder events.Recorder, requiredOriginal *storagev1.CSIDriver) (*storagev1.CSIDriver, bool, error) {
140-
141142
required := requiredOriginal.DeepCopy()
142143
if required.Annotations == nil {
143144
required.Annotations = map[string]string{}
@@ -173,31 +174,57 @@ func ApplyCSIDriver(ctx context.Context, client storageclientv1.CSIDriversGetter
173174
}
174175
}
175176

176-
metadataModified := false
177+
needsUpdate := false
178+
// Most CSIDriver fields are immutable. Any change to them should trigger Delete() + Create() calls.
179+
needsRecreate := false
180+
177181
existingCopy := existing.DeepCopy()
178-
resourcemerge.EnsureObjectMeta(&metadataModified, &existingCopy.ObjectMeta, required.ObjectMeta)
182+
// Metadata change should need just Update() call.
183+
resourcemerge.EnsureObjectMeta(&needsUpdate, &existingCopy.ObjectMeta, required.ObjectMeta)
179184

180185
requiredSpecHash := required.Annotations[specHashAnnotation]
181186
existingSpecHash := existing.Annotations[specHashAnnotation]
182-
sameSpec := requiredSpecHash == existingSpecHash
183-
if sameSpec && !metadataModified {
187+
// Assume whole re-create is needed on any spec change.
188+
// We don't keep a track of which field is mutable.
189+
needsRecreate = requiredSpecHash != existingSpecHash
190+
191+
// TODO: remove when CSIDriver spec.nodeAllocatableUpdatePeriodSeconds is enabled by default
192+
// (https://github.com/kubernetes/enhancements/tree/master/keps/sig-storage/4876-mutable-csinode-allocatable)
193+
if !needsRecreate && !alphaFieldsSaved(existingCopy, required) {
194+
// The required spec is the same as in previous succesful call, however,
195+
// the API server must have cleared some alpha/beta fields in it.
196+
// Try to save the object again. In case the fields are cleared again,
197+
// the caller (typically StaticResourceController) must retry periodically.
198+
klog.V(4).Infof("Detected CSIDriver %q field cleared by the API server, updating", required.Name)
199+
200+
// Assumption: the alpha fields are **mutable**, so only Update() is needed.
201+
// Update() with the same spec as before + the field cleared by the API server
202+
// won't generate any informer events. StaticResourceController will retry with
203+
// periodic retry (1 minute.)
204+
// We cannot use needsRecreate=true, as it will generate informer events and
205+
// StaticResourceController will retry immediately, leading to a busy loop.
206+
needsUpdate = true
207+
existingCopy.Spec = required.Spec
208+
}
209+
210+
if !needsUpdate && !needsRecreate {
184211
return existing, false, nil
185212
}
186213

187214
if klog.V(2).Enabled() {
188215
klog.Infof("CSIDriver %q changes: %v", required.Name, JSONPatchNoError(existing, existingCopy))
189216
}
190217

191-
if sameSpec {
192-
// Update metadata by a simple Update call
218+
if !needsRecreate {
219+
// only needsUpdate is true, update the object by a simple Update call
193220
actual, err := client.CSIDrivers().Update(ctx, existingCopy, metav1.UpdateOptions{})
194221
resourcehelper.ReportUpdateEvent(recorder, required, err)
195222
return actual, true, err
196223
}
197224

225+
// needsRecreate is true, needsUpdate does not matter. Delete and re-create the object.
198226
existingCopy.Spec = required.Spec
199227
existingCopy.ObjectMeta.ResourceVersion = ""
200-
// Spec is read-only after creation. Delete and re-create the object
201228
err = client.CSIDrivers().Delete(ctx, existingCopy.Name, metav1.DeleteOptions{})
202229
resourcehelper.ReportDeleteEvent(recorder, existingCopy, err, "Deleting CSIDriver to re-create it with updated parameters")
203230
if err != nil && !apierrors.IsNotFound(err) {
@@ -214,10 +241,17 @@ func ApplyCSIDriver(ctx context.Context, client storageclientv1.CSIDriversGetter
214241
} else if err != nil {
215242
err = fmt.Errorf("failed to re-create CSIDriver %s: %s", existingCopy.Name, err)
216243
}
217-
resourcehelper.ReportCreateEvent(recorder, existingCopy, err)
244+
resourcehelper.ReportCreateEvent(recorder, actual, err)
218245
return actual, true, err
219246
}
220247

248+
// alphaFieldsSaved checks that all required fields in the CSIDriver required spec are present and equal in the actual spec.
249+
func alphaFieldsSaved(actual, required *storagev1.CSIDriver) bool {
250+
// DeepDerivative checks that all fields in "required" are present and equal in "actual"
251+
// Fields not present in "required" are ignored.
252+
return equality.Semantic.DeepDerivative(required.Spec, actual.Spec)
253+
}
254+
221255
func validateRequiredCSIDriverLabels(required *storagev1.CSIDriver) error {
222256
supportsEphemeralVolumes := false
223257
for _, mode := range required.Spec.VolumeLifecycleModes {

pkg/operator/resource/resourceapply/storage_test.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,9 @@ func TestApplyCSIDriver(t *testing.T) {
434434
name string
435435
existing []*storagev1.CSIDriver
436436
input *storagev1.CSIDriver
437+
// Compute hash of input.Spec and use it as the existing.annotations[specHashAnnotation]
438+
// Useful for simulating the API server clearing some alpha/beta fields in existing.Spec.
439+
existingHashFromInput bool
437440

438441
expectedModified bool
439442
expectedError error
@@ -757,13 +760,61 @@ func TestApplyCSIDriver(t *testing.T) {
757760
}
758761
},
759762
},
763+
{
764+
name: "alphaFieldsSaved: same spec hash but missing alpha field triggers update",
765+
existing: []*storagev1.CSIDriver{
766+
{
767+
ObjectMeta: metav1.ObjectMeta{
768+
Name: "foo",
769+
},
770+
Spec: storagev1.CSIDriverSpec{
771+
AttachRequired: ptr.To(true),
772+
PodInfoOnMount: ptr.To(true),
773+
// NodeAllocatableUpdatePeriodSeconds is missing (simulating API server cleared it)
774+
},
775+
},
776+
},
777+
input: &storagev1.CSIDriver{
778+
ObjectMeta: metav1.ObjectMeta{
779+
Name: "foo",
780+
},
781+
Spec: storagev1.CSIDriverSpec{
782+
AttachRequired: ptr.To(true),
783+
PodInfoOnMount: ptr.To(true),
784+
NodeAllocatableUpdatePeriodSeconds: ptr.To[int64](30),
785+
},
786+
},
787+
existingHashFromInput: true,
788+
expectedModified: true,
789+
verifyActions: func(actions []clienttesting.Action, t *testing.T) {
790+
if len(actions) != 2 {
791+
t.Fatal(spew.Sdump(actions))
792+
}
793+
if !actions[0].Matches("get", "csidrivers") || actions[0].(clienttesting.GetAction).GetName() != "foo" {
794+
t.Error(spew.Sdump(actions))
795+
}
796+
if !actions[1].Matches("update", "csidrivers") {
797+
t.Error(spew.Sdump(actions))
798+
}
799+
// Verify that the update includes the alpha field
800+
actual := actions[1].(clienttesting.UpdateAction).GetObject().(*storagev1.CSIDriver)
801+
if actual.Spec.NodeAllocatableUpdatePeriodSeconds == nil || *actual.Spec.NodeAllocatableUpdatePeriodSeconds != 30 {
802+
t.Errorf("expected NodeAllocatableUpdatePeriodSeconds to be 30, got %v", actual.Spec.NodeAllocatableUpdatePeriodSeconds)
803+
}
804+
},
805+
},
760806
}
761807
for _, test := range tests {
762808
t.Run(test.name, func(t *testing.T) {
763809
objs := make([]runtime.Object, len(test.existing))
764810
for i, csiDriver := range test.existing {
765811
// Add spec hash annotation
766-
SetSpecHashAnnotation(&csiDriver.ObjectMeta, csiDriver.Spec)
812+
if test.existingHashFromInput {
813+
SetSpecHashAnnotation(&csiDriver.ObjectMeta, test.input.Spec)
814+
} else {
815+
// Add spec hash annotation based on existing spec
816+
SetSpecHashAnnotation(&csiDriver.ObjectMeta, csiDriver.Spec)
817+
}
767818
// Convert *CSIDriver to *Object
768819
objs[i] = csiDriver
769820
}

0 commit comments

Comments
 (0)