@@ -3,6 +3,7 @@ package switchedcontroller
33import (
44 "context"
55 "fmt"
6+ "sync"
67 "testing"
78 "time"
89
@@ -394,6 +395,27 @@ type applyStatusCall struct {
394395 applyConfiguration * applyoperatorv1.OperatorStatusApplyConfiguration
395396}
396397
398+ // synchronizedOperatorClient serializes access to a non-thread-safe OperatorClient
399+ // (e.g. fakeOperatorClient) so it can be used safely from multiple goroutines.
400+ type synchronizedOperatorClient struct {
401+ v1helpers.OperatorClient
402+ mu sync.Mutex
403+ statusApplied chan struct {}
404+ }
405+
406+ func (c * synchronizedOperatorClient ) ApplyOperatorStatus (ctx context.Context , fieldManager string , applyConfiguration * applyoperatorv1.OperatorStatusApplyConfiguration ) error {
407+ c .mu .Lock ()
408+ err := c .OperatorClient .ApplyOperatorStatus (ctx , fieldManager , applyConfiguration )
409+ c .mu .Unlock ()
410+ if c .statusApplied != nil {
411+ select {
412+ case c .statusApplied <- struct {}{}:
413+ default :
414+ }
415+ }
416+ return err
417+ }
418+
397419type recordingOperatorClient struct {
398420 v1helpers.OperatorClient
399421 applyStatusCalls []applyStatusCall
@@ -405,11 +427,13 @@ func (r *recordingOperatorClient) ApplyOperatorStatus(ctx context.Context, field
405427}
406428
407429func TestSwitchedControllerClearsDelegateConditionsOnSwitchOff (t * testing.T ) {
408- delegateSynced := make (chan struct {})
409-
410430 recorder := events .NewInMemoryRecorder ("switchedcontroller_test" , clocktesting .NewFakePassiveClock (time .Now ()))
411431 fakeClient := v1helpers .NewFakeOperatorClient (& operatorv1.OperatorSpec {ManagementState : operatorv1 .Managed }, & operatorv1.OperatorStatus {}, nil )
412- operatorClient := & recordingOperatorClient {OperatorClient : fakeClient }
432+ syncClient := & synchronizedOperatorClient {
433+ OperatorClient : fakeClient ,
434+ statusApplied : make (chan struct {}, 1 ),
435+ }
436+ operatorClient := & recordingOperatorClient {OperatorClient : syncClient }
413437
414438 delegateFn := func (_ context.Context ) * factory.Factory {
415439 var enqueueSyncHook factory.PostStartHook = func (ctx context.Context , syncContext factory.SyncContext ) error {
@@ -418,13 +442,9 @@ func TestSwitchedControllerClearsDelegateConditionsOnSwitchOff(t *testing.T) {
418442 }
419443 return factory .New ().
420444 WithSync (func (ctx context.Context , controllerContext factory.SyncContext ) error {
421- select {
422- case delegateSynced <- struct {}{}:
423- default :
424- }
425445 return fmt .Errorf ("route.route.openshift.io \" oauth-openshift\" not found" )
426446 }).
427- WithSyncDegradedOnError (fakeClient ).
447+ WithSyncDegradedOnError (syncClient ).
428448 WithPostStartHooks (enqueueSyncHook )
429449 }
430450
@@ -449,31 +469,16 @@ func TestSwitchedControllerClearsDelegateConditionsOnSwitchOff(t *testing.T) {
449469 t .Fatalf ("unexpected error when syncing: %v" , err )
450470 }
451471
452- // Wait for the delegate to sync at least once and set a degraded condition
472+ // Wait for the delegate's reportDegraded to call ApplyOperatorStatus.
473+ // This signal fires after reportDegraded completes (not just after sync returns),
474+ // which avoids a data race on the non-thread-safe fakeOperatorClient.
453475 syncTimeoutCtx , syncTimeoutCancel := context .WithTimeout (t .Context (), 500 * time .Millisecond )
454476 defer syncTimeoutCancel ()
455477
456478 select {
457479 case <- syncTimeoutCtx .Done ():
458- t .Fatal ("timed out waiting for delegate controller to sync" )
459- case <- delegateSynced :
460- }
461-
462- // Verify the delegate set a degraded condition
463- _ , status , _ , err := operatorClient .GetOperatorState ()
464- if err != nil {
465- t .Fatalf ("unexpected error getting operator state: %v" , err )
466- }
467-
468- foundDegraded := false
469- for _ , cond := range status .Conditions {
470- if cond .Type == "test-controllerDegraded" && cond .Status == operatorv1 .ConditionTrue {
471- foundDegraded = true
472- break
473- }
474- }
475- if ! foundDegraded {
476- t .Fatal ("expected test-controllerDegraded condition to be True after delegate sync error" )
480+ t .Fatal ("timed out waiting for delegate controller to report degraded status" )
481+ case <- syncClient .statusApplied :
477482 }
478483
479484 // Switch off and sync the wrapper — this should clear delegate conditions
0 commit comments