knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/reconciler/testing/context.go (about) 1 /* 2 Copyright 2019 The Knative Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package testing 18 19 import ( 20 "context" 21 "os" 22 "sync/atomic" 23 "testing" 24 "time" 25 26 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 27 "k8s.io/apimachinery/pkg/runtime" 28 "k8s.io/apimachinery/pkg/runtime/schema" 29 "k8s.io/apimachinery/pkg/runtime/serializer" 30 "k8s.io/apimachinery/pkg/util/wait" 31 "k8s.io/apimachinery/pkg/watch" 32 "k8s.io/client-go/rest" 33 clientgotesting "k8s.io/client-go/testing" 34 "k8s.io/client-go/tools/record" 35 36 "knative.dev/pkg/controller" 37 "knative.dev/pkg/injection" 38 logtesting "knative.dev/pkg/logging/testing" 39 ) 40 41 func init() { 42 // Disable WatchListClient feature in tests to work around K8s 1.35 issue where 43 // kubernetes.Interface doesn't expose IsWatchListSemanticsUnSupported(), preventing 44 // fake clients from being detected. This causes tests to timeout waiting for bookmark 45 // events that fake clients don't send. 46 // See: https://github.com/kubernetes/enhancements/blob/master/keps/sig-api-machinery/3157-watch-list/README.md 47 os.Setenv("KUBE_FEATURE_WatchListClient", "false") 48 } 49 50 // SetupFakeContext sets up the the Context and the fake informers for the tests. 51 // The optional fs() can be used to edit ctx before the SetupInformer steps 52 func SetupFakeContext(t testing.TB, fs ...func(context.Context) context.Context) (context.Context, []controller.Informer) { 53 c, _, is := SetupFakeContextWithCancel(t, fs...) 54 return c, is 55 } 56 57 // SetupFakeContextWithCancel sets up the the Context and the fake informers for the tests 58 // The provided context can be canceled using provided callback. 59 // The optional fs() can be used to edit ctx before the SetupInformer steps 60 func SetupFakeContextWithCancel(t testing.TB, fs ...func(context.Context) context.Context) (context.Context, context.CancelFunc, []controller.Informer) { 61 ctx, c := context.WithCancel(logtesting.TestContextWithLogger(t)) 62 ctx = controller.WithEventRecorder(ctx, record.NewFakeRecorder(1000)) 63 for _, f := range fs { 64 ctx = f(ctx) //nolint:fatcontext 65 } 66 ctx = injection.WithConfig(ctx, &rest.Config{}) 67 68 ctx, is := injection.Fake.SetupInformers(ctx, injection.GetConfig(ctx)) 69 return ctx, c, is 70 } 71 72 // fakeClient is an interface capturing the two functions we need from fake clients. 73 type fakeClient interface { 74 PrependWatchReactor(resource string, reaction clientgotesting.WatchReactionFunc) 75 PrependReactor(verb, resource string, reaction clientgotesting.ReactionFunc) 76 } 77 78 // withTracker is an interface capturing only the Tracker function. The dynamic client 79 // currently does not have that, so we need to special-case it. 80 type withTracker interface { 81 Tracker() clientgotesting.ObjectTracker 82 } 83 84 // RunAndSyncInformers runs the given informers, then makes sure their caches are all 85 // synced and in addition makes sure that all the Watch calls have been properly setup. 86 // See https://github.com/kubernetes/kubernetes/issues/95372 for background on the Watch 87 // calls tragedy. 88 func RunAndSyncInformers(ctx context.Context, informers ...controller.Informer) (func(), error) { 89 var watchesPending atomic.Int32 90 91 for _, client := range injection.Fake.FetchAllClients(ctx) { 92 c := client.(fakeClient) 93 94 var tracker clientgotesting.ObjectTracker 95 if withTracker, ok := c.(withTracker); ok { 96 tracker = withTracker.Tracker() 97 } else { 98 // Required setup for the dynamic client as it doesn't define a Tracker() function. 99 // TODO(markusthoemmes): Drop this if https://github.com/kubernetes/kubernetes/pull/100085 lands. 100 scheme := runtime.NewScheme() 101 scheme.AddKnownTypeWithName(schema.GroupVersionKind{Group: "fake-dynamic-client-group", Version: "v1", Kind: "List"}, &unstructured.UnstructuredList{}) 102 codecs := serializer.NewCodecFactory(scheme) 103 tracker = clientgotesting.NewObjectTracker(scheme, codecs.UniversalDecoder()) 104 } 105 106 c.PrependReactor("list", "*", func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) { 107 // Every list (before actual informer usage) is going to be followed by a Watch call. 108 watchesPending.Add(1) 109 return false, nil, nil 110 }) 111 112 c.PrependWatchReactor("*", func(action clientgotesting.Action) (handled bool, ret watch.Interface, err error) { 113 // The actual Watch call. This is a reimplementation of the default Watch 114 // calls in fakes to guarantee we have actually **done** the work. 115 gvr := action.GetResource() 116 ns := action.GetNamespace() 117 watch, err := tracker.Watch(gvr, ns) 118 if err != nil { 119 return false, nil, err 120 } 121 122 watchesPending.Add(-1) 123 124 return true, watch, nil 125 }) 126 } 127 128 wf, err := controller.RunInformers(ctx.Done(), informers...) 129 if err != nil { 130 return wf, err 131 } 132 133 err = wait.PollUntilContextTimeout(ctx, time.Microsecond, wait.ForeverTestTimeout, true, func(ctx context.Context) (bool, error) { 134 if watchesPending.Load() == 0 { 135 return true, nil 136 } 137 return false, nil 138 }) 139 return wf, err 140 }