@@ -24,6 +24,7 @@ import (
2424 "errors"
2525 "fmt"
2626 "reflect"
27+ "strings"
2728 "testing"
2829 "time"
2930
@@ -44,6 +45,53 @@ import (
4445
4546var tlog = ctrl .Log .WithName ("controller_testing" )
4647
48+ type patchRecordingWriter struct {
49+ patchCalls int
50+ updateCalls int
51+ patchData []byte
52+ }
53+
54+ func (w * patchRecordingWriter ) Create (context.Context , client.Object , ... client.CreateOption ) error {
55+ return nil
56+ }
57+
58+ func (w * patchRecordingWriter ) Delete (context.Context , client.Object , ... client.DeleteOption ) error {
59+ return nil
60+ }
61+
62+ func (w * patchRecordingWriter ) Update (context.Context , client.Object , ... client.UpdateOption ) error {
63+ w .updateCalls ++
64+ return nil
65+ }
66+
67+ func (w * patchRecordingWriter ) Patch (_ context.Context , obj client.Object , patch client.Patch , _ ... client.PatchOption ) error {
68+ var err error
69+ w .patchCalls ++
70+ w .patchData , err = patch .Data (obj )
71+ return err
72+ }
73+
74+ func (w * patchRecordingWriter ) DeleteAllOf (context.Context , client.Object , ... client.DeleteAllOfOption ) error {
75+ return nil
76+ }
77+
78+ type concurrentFinalizerWriter struct {
79+ patchRecordingWriter
80+ currentResourceVersion string
81+ }
82+
83+ func (w * concurrentFinalizerWriter ) Patch (ctx context.Context , obj client.Object , patch client.Patch , opts ... client.PatchOption ) error {
84+ if err := w .patchRecordingWriter .Patch (ctx , obj , patch , opts ... ); err != nil {
85+ return err
86+ }
87+ staleResourceVersion := fmt .Sprintf (`"resourceVersion":"%s"` , obj .GetResourceVersion ())
88+ if strings .Contains (string (w .patchData ), staleResourceVersion ) && obj .GetResourceVersion () != w .currentResourceVersion {
89+ return apierrors .NewConflict (schema.GroupResource {Resource : "configmaps" }, obj .GetName (),
90+ fmt .Errorf ("object resourceVersion changed to %s" , w .currentResourceVersion ))
91+ }
92+ return nil
93+ }
94+
4795func TestRequeueWithError (t * testing.T ) {
4896 _ , err := CheckedRequeueWithError (errors .New ("test error" ), tlog , "test" )
4997 if err == nil {
@@ -116,6 +164,97 @@ func TestResultToP(t *testing.T) {
116164 }
117165}
118166
167+ func TestHandleCRDeletionPatchesFinalizerMetadata (t * testing.T ) {
168+ const finalizer = "finalizer/protection"
169+ reqCtx := RequestCtx {Ctx : context .Background (), Log : tlog }
170+
171+ t .Run ("add finalizer" , func (t * testing.T ) {
172+ writer := & patchRecordingWriter {}
173+ obj := & corev1.ConfigMap {ObjectMeta : metav1.ObjectMeta {Name : "cm" , ResourceVersion : "1" }}
174+
175+ res , err := HandleCRDeletion (reqCtx , writer , obj , finalizer , nil )
176+ if err != nil {
177+ t .Fatalf ("HandleCRDeletion returned error: %v" , err )
178+ }
179+ if res != nil {
180+ t .Fatalf ("expected no reconcile result, got %v" , res )
181+ }
182+ if writer .patchCalls != 1 || writer .updateCalls != 0 {
183+ t .Fatalf ("expected one patch and no update, got patch=%d update=%d" , writer .patchCalls , writer .updateCalls )
184+ }
185+ assertMetadataOnlyPatch (t , writer .patchData )
186+ assertOptimisticLockPatch (t , writer .patchData , obj .GetResourceVersion ())
187+ if ! controllerutil .ContainsFinalizer (obj , finalizer ) {
188+ t .Fatalf ("expected finalizer %q to be added" , finalizer )
189+ }
190+ })
191+
192+ t .Run ("remove finalizer" , func (t * testing.T ) {
193+ writer := & patchRecordingWriter {}
194+ now := metav1 .Now ()
195+ obj := & corev1.ConfigMap {ObjectMeta : metav1.ObjectMeta {
196+ Name : "cm" ,
197+ ResourceVersion : "2" ,
198+ Finalizers : []string {finalizer },
199+ DeletionTimestamp : & now ,
200+ }}
201+
202+ res , err := HandleCRDeletion (reqCtx , writer , obj , finalizer , nil )
203+ if err != nil {
204+ t .Fatalf ("HandleCRDeletion returned error: %v" , err )
205+ }
206+ if res == nil {
207+ t .Fatalf ("expected reconciled result" )
208+ }
209+ if writer .patchCalls != 1 || writer .updateCalls != 0 {
210+ t .Fatalf ("expected one patch and no update, got patch=%d update=%d" , writer .patchCalls , writer .updateCalls )
211+ }
212+ assertMetadataOnlyPatch (t , writer .patchData )
213+ assertOptimisticLockPatch (t , writer .patchData , obj .GetResourceVersion ())
214+ if controllerutil .ContainsFinalizer (obj , finalizer ) {
215+ t .Fatalf ("expected finalizer %q to be removed" , finalizer )
216+ }
217+ })
218+
219+ t .Run ("returns conflict on concurrent finalizer update" , func (t * testing.T ) {
220+ writer := & concurrentFinalizerWriter {currentResourceVersion : "2" }
221+ obj := & corev1.ConfigMap {ObjectMeta : metav1.ObjectMeta {
222+ Name : "cm" ,
223+ ResourceVersion : "1" ,
224+ Finalizers : []string {"other/finalizer" },
225+ }}
226+
227+ res , err := HandleCRDeletion (reqCtx , writer , obj , finalizer , nil )
228+ if ! apierrors .IsConflict (err ) {
229+ t .Fatalf ("expected conflict from stale finalizer patch, got res=%v err=%v patch=%s" , res , err , writer .patchData )
230+ }
231+ if writer .patchCalls != 1 || writer .updateCalls != 0 {
232+ t .Fatalf ("expected one patch and no update, got patch=%d update=%d" , writer .patchCalls , writer .updateCalls )
233+ }
234+ assertMetadataOnlyPatch (t , writer .patchData )
235+ assertOptimisticLockPatch (t , writer .patchData , "1" )
236+ })
237+ }
238+
239+ func assertMetadataOnlyPatch (t * testing.T , data []byte ) {
240+ t .Helper ()
241+ patchData := string (data )
242+ if ! strings .Contains (patchData , `"metadata"` ) {
243+ t .Fatalf ("expected metadata patch, got %s" , patchData )
244+ }
245+ if strings .Contains (patchData , `"spec"` ) {
246+ t .Fatalf ("metadata patch must not include spec, got %s" , patchData )
247+ }
248+ }
249+
250+ func assertOptimisticLockPatch (t * testing.T , data []byte , resourceVersion string ) {
251+ t .Helper ()
252+ expected := fmt .Sprintf (`"resourceVersion":"%s"` , resourceVersion )
253+ if ! strings .Contains (string (data ), expected ) {
254+ t .Fatalf ("expected optimistic-locking resourceVersion %s in patch, got %s" , resourceVersion , data )
255+ }
256+ }
257+
119258var _ = Describe ("Cluster Controller" , func () {
120259
121260 const finalizer = "finalizer/protection"
0 commit comments