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  }