github.com/cilium/cilium@v1.16.2/test/controlplane/suite/testcase.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package suite
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"strings"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/cilium/hive/cell"
    14  	"github.com/cilium/hive/hivetest"
    15  	"github.com/cilium/statedb"
    16  	"github.com/spf13/cobra"
    17  	"github.com/spf13/viper"
    18  	corev1 "k8s.io/api/core/v1"
    19  	discov1 "k8s.io/api/discovery/v1"
    20  	discov1beta1 "k8s.io/api/discovery/v1beta1"
    21  	"k8s.io/apimachinery/pkg/api/meta"
    22  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    23  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    24  	"k8s.io/apimachinery/pkg/fields"
    25  	k8sRuntime "k8s.io/apimachinery/pkg/runtime"
    26  	"k8s.io/apimachinery/pkg/runtime/schema"
    27  	versionapi "k8s.io/apimachinery/pkg/version"
    28  	"k8s.io/apimachinery/pkg/watch"
    29  	fakediscovery "k8s.io/client-go/discovery/fake"
    30  	k8sTesting "k8s.io/client-go/testing"
    31  
    32  	agentCmd "github.com/cilium/cilium/daemon/cmd"
    33  	operatorCmd "github.com/cilium/cilium/operator/cmd"
    34  	operatorOption "github.com/cilium/cilium/operator/option"
    35  	fakeTypes "github.com/cilium/cilium/pkg/datapath/fake/types"
    36  	datapathTables "github.com/cilium/cilium/pkg/datapath/tables"
    37  	"github.com/cilium/cilium/pkg/k8s/apis"
    38  	cilium_v2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2"
    39  	k8sClient "github.com/cilium/cilium/pkg/k8s/client"
    40  	slim_corev1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/api/core/v1"
    41  	"github.com/cilium/cilium/pkg/k8s/version"
    42  	"github.com/cilium/cilium/pkg/lock"
    43  	"github.com/cilium/cilium/pkg/node/types"
    44  	agentOption "github.com/cilium/cilium/pkg/option"
    45  )
    46  
    47  type trackerAndDecoder struct {
    48  	tracker k8sTesting.ObjectTracker
    49  	decoder k8sRuntime.Decoder
    50  }
    51  
    52  type ControlPlaneTest struct {
    53  	t                 *testing.T
    54  	tempDir           string
    55  	validationTimeout time.Duration
    56  
    57  	nodeName            string
    58  	clients             *k8sClient.FakeClientset
    59  	trackers            []trackerAndDecoder
    60  	agentHandle         *agentHandle
    61  	operatorHandle      *operatorHandle
    62  	Datapath            *fakeTypes.FakeDatapath
    63  	establishedWatchers *lock.Map[string, struct{}]
    64  }
    65  
    66  func (cpt *ControlPlaneTest) AgentDB() (*statedb.DB, statedb.Table[datapathTables.NodeAddress]) {
    67  	return cpt.agentHandle.db, cpt.agentHandle.nodeAddrs
    68  }
    69  
    70  func NewControlPlaneTest(t *testing.T, nodeName string, k8sVersion string) *ControlPlaneTest {
    71  	clients, _ := k8sClient.NewFakeClientset()
    72  	var w lock.Map[string, struct{}]
    73  	clients.KubernetesFakeClientset = augmentTracker(clients.KubernetesFakeClientset, t, &w)
    74  	clients.SlimFakeClientset = augmentTracker(clients.SlimFakeClientset, t, &w)
    75  	clients.CiliumFakeClientset = augmentTracker(clients.CiliumFakeClientset, t, &w)
    76  	clients.APIExtFakeClientset = augmentTracker(clients.APIExtFakeClientset, t, &w)
    77  	fd := clients.KubernetesFakeClientset.Discovery().(*fakediscovery.FakeDiscovery)
    78  	fd.FakedServerVersion = toVersionInfo(k8sVersion)
    79  
    80  	resources, ok := apiResources[k8sVersion]
    81  	if !ok {
    82  		panic(fmt.Sprintf("k8s version %s not found in apiResources", k8sVersion))
    83  	}
    84  	clients.KubernetesFakeClientset.Resources = resources
    85  	clients.SlimFakeClientset.Resources = resources
    86  	clients.CiliumFakeClientset.Resources = resources
    87  	clients.APIExtFakeClientset.Resources = resources
    88  
    89  	trackers := []trackerAndDecoder{
    90  		{clients.KubernetesFakeClientset.Tracker(), coreDecoder},
    91  		{clients.SlimFakeClientset.Tracker(), slimDecoder},
    92  		{clients.CiliumFakeClientset.Tracker(), ciliumDecoder},
    93  	}
    94  
    95  	return &ControlPlaneTest{
    96  		t:                   t,
    97  		nodeName:            nodeName,
    98  		clients:             clients,
    99  		trackers:            trackers,
   100  		establishedWatchers: &w,
   101  	}
   102  }
   103  
   104  // SetupEnvironment sets the fake k8s clients, creates the fake datapath and
   105  // creates the test directories.
   106  func (cpt *ControlPlaneTest) SetupEnvironment() *ControlPlaneTest {
   107  	types.SetName(cpt.nodeName)
   108  
   109  	// Configure k8s and perform capability detection with the fake client.
   110  	version.Update(cpt.clients, true)
   111  
   112  	cpt.tempDir = setupTestDirectories()
   113  
   114  	return cpt
   115  }
   116  
   117  // ClearEnvironment removes all the test directories.
   118  func (cpt *ControlPlaneTest) ClearEnvironment() {
   119  	os.RemoveAll(cpt.tempDir)
   120  }
   121  
   122  func (cpt *ControlPlaneTest) StartAgent(modConfig func(*agentOption.DaemonConfig), extraCells ...cell.Cell) *ControlPlaneTest {
   123  	if cpt.agentHandle != nil {
   124  		cpt.t.Fatal("StartAgent() already called")
   125  	}
   126  
   127  	cpt.agentHandle = &agentHandle{
   128  		t: cpt.t,
   129  	}
   130  
   131  	cpt.agentHandle.setupCiliumAgentHive(cpt.clients, cell.Group(extraCells...))
   132  
   133  	mockCmd := &cobra.Command{}
   134  	cpt.agentHandle.hive.RegisterFlags(mockCmd.Flags())
   135  	agentCmd.InitGlobalFlags(mockCmd, cpt.agentHandle.hive.Viper())
   136  
   137  	cpt.agentHandle.populateCiliumAgentOptions(cpt.tempDir, modConfig)
   138  
   139  	cpt.agentHandle.log = hivetest.Logger(cpt.t)
   140  	daemon, err := cpt.agentHandle.startCiliumAgent()
   141  	if err != nil {
   142  		cpt.t.Fatalf("Failed to start cilium agent: %s", err)
   143  	}
   144  	cpt.agentHandle.d = daemon
   145  	cpt.Datapath = cpt.agentHandle.dp
   146  
   147  	return cpt
   148  }
   149  
   150  func (cpt *ControlPlaneTest) StopAgent() *ControlPlaneTest {
   151  	cpt.agentHandle.tearDown()
   152  	cpt.agentHandle = nil
   153  	cpt.Datapath = nil
   154  
   155  	return cpt
   156  }
   157  
   158  func (cpt *ControlPlaneTest) StartOperator(
   159  	modConfig func(*operatorOption.OperatorConfig),
   160  	modCellConfig func(vp *viper.Viper),
   161  ) *ControlPlaneTest {
   162  	if cpt.operatorHandle != nil {
   163  		cpt.t.Fatal("StartOperator() already called")
   164  	}
   165  
   166  	h := setupCiliumOperatorHive(cpt.clients)
   167  
   168  	mockCmd := &cobra.Command{}
   169  	h.RegisterFlags(mockCmd.Flags())
   170  	operatorCmd.InitGlobalFlags(mockCmd, h.Viper())
   171  
   172  	populateCiliumOperatorOptions(h.Viper(), modConfig, modCellConfig)
   173  
   174  	h.Viper().Set(apis.SkipCRDCreation, true)
   175  
   176  	// Disable support for operator HA. This should be cleaned up
   177  	// by injecting the capabilities, or by supporting the leader
   178  	// election machinery in the controlplane tests.
   179  	version.DisableLeasesResourceLock()
   180  
   181  	log := hivetest.Logger(cpt.t)
   182  	err := startCiliumOperator(h, log)
   183  	if err != nil {
   184  		cpt.t.Fatalf("Failed to start operator: %s", err)
   185  	}
   186  
   187  	cpt.operatorHandle = &operatorHandle{
   188  		t:    cpt.t,
   189  		hive: h,
   190  		log:  log,
   191  	}
   192  
   193  	return cpt
   194  }
   195  
   196  func (cpt *ControlPlaneTest) StopOperator() *ControlPlaneTest {
   197  	cpt.operatorHandle.tearDown()
   198  	cpt.operatorHandle = nil
   199  
   200  	return cpt
   201  }
   202  
   203  func (cpt *ControlPlaneTest) UpdateObjects(objs ...k8sRuntime.Object) *ControlPlaneTest {
   204  	t := cpt.t
   205  	for _, obj := range objs {
   206  		gvr, ns, name := gvrAndName(obj)
   207  
   208  		// Convert to unstructured form for JSON marshalling.
   209  		// TODO: simpler way?
   210  		uobj, ok := obj.(*unstructured.Unstructured)
   211  		if !ok {
   212  			fields, err := k8sRuntime.DefaultUnstructuredConverter.ToUnstructured(obj)
   213  			if err != nil {
   214  				t.Fatalf("Failed to convert %T to unstructured: %s", obj, err)
   215  			}
   216  			uobj = &unstructured.Unstructured{Object: fields}
   217  		}
   218  
   219  		// Marshal the object to JSON in order to allow decoding it in different ways,
   220  		// e.g. as v1.Node and as slim_corev1.Node. This avoids having to write both
   221  		// the core and slim versions of the object in the test case.
   222  		jsonBytes, err := uobj.MarshalJSON()
   223  		if err != nil {
   224  			t.Fatalf("Failed to marshal %T to JSON: %s", obj, err)
   225  		}
   226  
   227  		accepted := false
   228  		var errors []error
   229  		for _, td := range cpt.trackers {
   230  			if obj, _, err := td.decoder.Decode(jsonBytes, nil, nil); err == nil {
   231  				accepted = true
   232  
   233  				if _, err := td.tracker.Get(gvr, ns, name); err == nil {
   234  					if err := td.tracker.Update(gvr, obj, ns); err != nil {
   235  						t.Fatalf("Failed to update object %T: %s", obj, err)
   236  					}
   237  				} else {
   238  					if err := td.tracker.Add(obj); err != nil {
   239  						t.Fatalf("Failed to add object %T: %s", obj, err)
   240  					}
   241  				}
   242  			} else {
   243  				errors = append(errors, err)
   244  			}
   245  		}
   246  		if !accepted {
   247  			t.Fatalf("None of the decoders accepted %s: %v", gvr, errors)
   248  		}
   249  	}
   250  	return cpt
   251  }
   252  
   253  // Get retrieves a k8s object given its group-version-resource, namespace and name.
   254  // All the mocked control plane trackers will be queried in the search:
   255  // - core
   256  // - slim
   257  // - cilium
   258  // The first match will be returned.
   259  // If the object cannot be found, a non nil error is returned.
   260  func (cpt *ControlPlaneTest) Get(gvr schema.GroupVersionResource, ns, name string) (k8sRuntime.Object, error) {
   261  	var (
   262  		obj k8sRuntime.Object
   263  		err error
   264  	)
   265  	for _, td := range cpt.trackers {
   266  		if obj, err = td.tracker.Get(gvr, ns, name); err == nil {
   267  			return obj, nil
   268  		}
   269  	}
   270  	return nil, err
   271  }
   272  
   273  // EnsureWatchers delays progress of the test until watchers for resources have been established on
   274  // the clientset.
   275  func (cpt *ControlPlaneTest) EnsureWatchers(resources ...string) *ControlPlaneTest {
   276  	cpt.retry(func() error {
   277  		for _, resource := range resources {
   278  			if _, ok := cpt.establishedWatchers.Load(resource); !ok {
   279  				return fmt.Errorf("no watcher for %s yet", resource)
   280  			}
   281  		}
   282  		return nil
   283  	})
   284  
   285  	return cpt
   286  }
   287  
   288  func (cpt *ControlPlaneTest) UpdateObjectsFromFile(filename string) *ControlPlaneTest {
   289  	bs, err := os.ReadFile(filename)
   290  	if err != nil {
   291  		cpt.t.Fatalf("Failed to read %s: %s", filename, err)
   292  	}
   293  	objs, err := unmarshalList(bs)
   294  	if err != nil {
   295  		cpt.t.Fatalf("Failed to unmarshal objects from %s: %s", filename, err)
   296  	}
   297  	return cpt.UpdateObjects(objs...)
   298  }
   299  
   300  func (cpt *ControlPlaneTest) DeleteObjects(objs ...k8sRuntime.Object) *ControlPlaneTest {
   301  	for _, obj := range objs {
   302  		gvr, ns, name := gvrAndName(obj)
   303  
   304  		deleted := false
   305  		for _, td := range cpt.trackers {
   306  			if err := td.tracker.Delete(gvr, ns, name); err == nil {
   307  				deleted = true
   308  			}
   309  		}
   310  		if !deleted {
   311  			cpt.t.Fatalf("Failed to delete object %s/%s as it was not found", ns, name)
   312  		}
   313  	}
   314  	return cpt
   315  }
   316  
   317  func (cpt *ControlPlaneTest) WithValidationTimeout(d time.Duration) *ControlPlaneTest {
   318  	cpt.validationTimeout = d
   319  	return cpt
   320  }
   321  
   322  func (cpt *ControlPlaneTest) Eventually(check func() error) *ControlPlaneTest {
   323  	if err := cpt.retry(check); err != nil {
   324  		cpt.t.Fatal(err)
   325  	}
   326  	return cpt
   327  }
   328  
   329  func (cpt *ControlPlaneTest) Execute(task func() error) *ControlPlaneTest {
   330  	if err := task(); err != nil {
   331  		cpt.t.Fatal(err)
   332  	}
   333  	return cpt
   334  }
   335  
   336  func (cpt *ControlPlaneTest) retry(act func() error) error {
   337  	wait := 50 * time.Millisecond
   338  	end := time.Now().Add(cpt.validationTimeout)
   339  
   340  	// With validationTimeout set to 0, act will be retried without enforcing any timeout.
   341  	// This is useful to reduce controlplane tests flakyness in CI environment.
   342  	// Use WithValidationTimeout to set a custom timeout for local development.
   343  	for cpt.validationTimeout == 0 || time.Now().Add(wait).Before(end) {
   344  		time.Sleep(wait)
   345  
   346  		err := act()
   347  		if err == nil {
   348  			return nil
   349  		}
   350  		cpt.t.Logf("validation failed: %s", err)
   351  
   352  		wait *= 2
   353  		if wait > time.Second {
   354  			wait = time.Second
   355  		}
   356  		cpt.t.Logf("going to retry after %s...", wait)
   357  	}
   358  
   359  	time.Sleep(time.Until(end))
   360  	return act()
   361  }
   362  
   363  func toVersionInfo(rawVersion string) *versionapi.Info {
   364  	parts := strings.Split(rawVersion, ".")
   365  	return &versionapi.Info{Major: parts[0], Minor: parts[1]}
   366  }
   367  
   368  func gvrAndName(obj k8sRuntime.Object) (gvr schema.GroupVersionResource, ns string, name string) {
   369  	gvk := obj.GetObjectKind().GroupVersionKind()
   370  	gvr, _ = meta.UnsafeGuessKindToResource(gvk)
   371  	objMeta, err := meta.Accessor(obj)
   372  	if err != nil {
   373  		panic(err)
   374  	}
   375  	ns = objMeta.GetNamespace()
   376  	name = objMeta.GetName()
   377  	return
   378  }
   379  
   380  var (
   381  	corev1APIResources = &metav1.APIResourceList{
   382  		GroupVersion: corev1.SchemeGroupVersion.String(),
   383  		APIResources: []metav1.APIResource{
   384  			{Name: "nodes", Kind: "Node"},
   385  			{Name: "pods", Namespaced: true, Kind: "Pod"},
   386  			{Name: "services", Namespaced: true, Kind: "Service"},
   387  			{Name: "endpoints", Namespaced: true, Kind: "Endpoint"},
   388  		},
   389  	}
   390  
   391  	ciliumv2APIResources = &metav1.APIResourceList{
   392  		TypeMeta:     metav1.TypeMeta{},
   393  		GroupVersion: cilium_v2.SchemeGroupVersion.String(),
   394  		APIResources: []metav1.APIResource{
   395  			{Name: cilium_v2.CNPluralName, Kind: cilium_v2.CNKindDefinition},
   396  			{Name: cilium_v2.CEPPluralName, Namespaced: true, Kind: cilium_v2.CEPKindDefinition},
   397  			{Name: cilium_v2.CIDPluralName, Namespaced: true, Kind: cilium_v2.CIDKindDefinition},
   398  			{Name: cilium_v2.CEGPPluralName, Namespaced: true, Kind: cilium_v2.CEGPKindDefinition},
   399  			{Name: cilium_v2.CNPPluralName, Namespaced: true, Kind: cilium_v2.CNPKindDefinition},
   400  			{Name: cilium_v2.CCNPPluralName, Namespaced: true, Kind: cilium_v2.CCNPKindDefinition},
   401  			{Name: cilium_v2.CLRPPluralName, Namespaced: true, Kind: cilium_v2.CLRPKindDefinition},
   402  			{Name: cilium_v2.CEWPluralName, Namespaced: true, Kind: cilium_v2.CEWKindDefinition},
   403  			{Name: cilium_v2.CCECPluralName, Namespaced: true, Kind: cilium_v2.CCECKindDefinition},
   404  			{Name: cilium_v2.CECPluralName, Namespaced: true, Kind: cilium_v2.CECKindDefinition},
   405  		},
   406  	}
   407  
   408  	discoveryV1APIResources = &metav1.APIResourceList{
   409  		TypeMeta:     metav1.TypeMeta{},
   410  		GroupVersion: discov1.SchemeGroupVersion.String(),
   411  		APIResources: []metav1.APIResource{
   412  			{Name: "endpointslices", Namespaced: true, Kind: "EndpointSlice"},
   413  		},
   414  	}
   415  
   416  	discoveryV1beta1APIResources = &metav1.APIResourceList{
   417  		GroupVersion: discov1beta1.SchemeGroupVersion.String(),
   418  		APIResources: []metav1.APIResource{
   419  			{Name: "endpointslices", Namespaced: true, Kind: "EndpointSlice"},
   420  		},
   421  	}
   422  
   423  	// apiResources is the list of API resources for the k8s version that we're mocking.
   424  	// This is mostly relevant for the feature detection at pkg/k8s/version/version.go.
   425  	// The lists here are currently not exhaustive and expanded on need-by-need basis.
   426  	apiResources = map[string][]*metav1.APIResourceList{
   427  		"1.24": {
   428  			corev1APIResources,
   429  			discoveryV1APIResources,
   430  			discoveryV1beta1APIResources,
   431  			ciliumv2APIResources,
   432  		},
   433  		"1.25": {
   434  			corev1APIResources,
   435  			discoveryV1APIResources,
   436  			ciliumv2APIResources,
   437  		},
   438  		"1.26": {
   439  			corev1APIResources,
   440  			discoveryV1APIResources,
   441  			ciliumv2APIResources,
   442  		},
   443  	}
   444  )
   445  
   446  func matchFieldSelector(obj k8sRuntime.Object, selector fields.Selector) bool {
   447  	if selector == nil {
   448  		return true
   449  	}
   450  
   451  	fs := fields.Set{}
   452  	acc, err := meta.Accessor(obj)
   453  	if err != nil {
   454  		panic(err)
   455  	}
   456  	fs["metadata.name"] = acc.GetName()
   457  	fs["metadata.namespace"] = acc.GetNamespace()
   458  
   459  	// Special handling for specific objects. Only add things here that k8s api-server
   460  	// handles, see for example ToSelectableFields() in pkg/registry/core/pod/strategy.go
   461  	// of kubernetes. We don't want to end up with tests passing with fake client and
   462  	// failing against the real API server.
   463  	if pod, ok := obj.(*corev1.Pod); ok {
   464  		fs["spec.nodeName"] = pod.Spec.NodeName
   465  	}
   466  	if pod, ok := obj.(*slim_corev1.Pod); ok {
   467  		fs["spec.nodeName"] = pod.Spec.NodeName
   468  	}
   469  
   470  	if !selector.Matches(fs) {
   471  		// Check if we failed because we were trying to match a field that doesn't exist.
   472  		// If so, we'll panic so that an exception can be added.
   473  		for _, req := range selector.Requirements() {
   474  			if _, ok := fs[req.Field]; !ok {
   475  				panic(fmt.Sprintf(
   476  					"Unknown field selector %q!\nPlease add handling for it to matchFieldSelector() in test/controlplane/suite/testcase.go",
   477  					req.Field))
   478  			}
   479  		}
   480  		return false
   481  	}
   482  	return true
   483  }
   484  
   485  type fakeWithTracker interface {
   486  	PrependReactor(verb string, resource string, reaction k8sTesting.ReactionFunc)
   487  	PrependWatchReactor(resource string, reaction k8sTesting.WatchReactionFunc)
   488  	Tracker() k8sTesting.ObjectTracker
   489  }
   490  
   491  type filteringWatcher struct {
   492  	parent       watch.Interface
   493  	events       chan watch.Event
   494  	restrictions k8sTesting.WatchRestrictions
   495  }
   496  
   497  var _ watch.Interface = &filteringWatcher{}
   498  
   499  func (fw *filteringWatcher) Stop() {
   500  	fw.parent.Stop()
   501  }
   502  
   503  func (fw *filteringWatcher) ResultChan() <-chan watch.Event {
   504  	if fw.events != nil {
   505  		return fw.events
   506  	}
   507  
   508  	fw.events = make(chan watch.Event)
   509  	selector := fw.restrictions.Fields
   510  	go func() {
   511  		for event := range fw.parent.ResultChan() {
   512  			if matchFieldSelector(event.Object, selector) {
   513  				fw.events <- event
   514  			}
   515  		}
   516  		close(fw.events)
   517  	}()
   518  	return fw.events
   519  }
   520  
   521  func filterList(obj k8sRuntime.Object, restrictions k8sTesting.ListRestrictions) {
   522  	selector := restrictions.Fields
   523  	if selector == nil || selector.Empty() {
   524  		return
   525  	}
   526  
   527  	switch obj := obj.(type) {
   528  	case *corev1.NodeList:
   529  		items := make([]corev1.Node, 0, len(obj.Items))
   530  		for i := range obj.Items {
   531  			if matchFieldSelector(&obj.Items[i], selector) {
   532  				items = append(items, obj.Items[i])
   533  			}
   534  		}
   535  		obj.Items = items
   536  	case *slim_corev1.NodeList:
   537  		items := make([]slim_corev1.Node, 0, len(obj.Items))
   538  		for i := range obj.Items {
   539  			if matchFieldSelector(&obj.Items[i], selector) {
   540  				items = append(items, obj.Items[i])
   541  			}
   542  		}
   543  		obj.Items = items
   544  	case *slim_corev1.EndpointsList:
   545  		items := make([]slim_corev1.Endpoints, 0, len(obj.Items))
   546  		for i := range obj.Items {
   547  			if matchFieldSelector(&obj.Items[i], selector) {
   548  				items = append(items, obj.Items[i])
   549  			}
   550  		}
   551  		obj.Items = items
   552  	case *slim_corev1.PodList:
   553  		items := make([]slim_corev1.Pod, 0, len(obj.Items))
   554  		for i := range obj.Items {
   555  			if matchFieldSelector(&obj.Items[i], selector) {
   556  				items = append(items, obj.Items[i])
   557  			}
   558  		}
   559  		obj.Items = items
   560  	case *cilium_v2.CiliumNodeList:
   561  		items := make([]cilium_v2.CiliumNode, 0, len(obj.Items))
   562  		for i := range obj.Items {
   563  			if matchFieldSelector(&obj.Items[i], selector) {
   564  				items = append(items, obj.Items[i])
   565  			}
   566  		}
   567  		obj.Items = items
   568  	default:
   569  		panic(
   570  			fmt.Sprintf("Unhandled type %T for field selector filtering!\nPlease add handling for it to filterList()", obj),
   571  		)
   572  	}
   573  }
   574  
   575  // augmentTracker augments the fake clientset to support filtering with a field selector
   576  // in List and Watch actions, as well as recording which watchers have been established.
   577  // The reason we need to do this is the following: The k8s object tracker's implementation
   578  // of Watch is not equivalent to Watch on a real api-server, as it does not respect the
   579  // ResourceVersion from whence to start the watch. As a consequence, when informers (or
   580  // reflectors) call ListAndWatch, they miss events which occur between the end of List and
   581  // the establishment of Watch.
   582  //
   583  // To decrease the likelihood of this race occurring in the control plane tests, we
   584  // install a mechanism to wait for watchers of specific resources: see also
   585  // EnsureWatchers. This isn't a complete fix - if multiple watchers for the same resource
   586  // are established, this may give false positives.
   587  func augmentTracker[T fakeWithTracker](f T, t *testing.T, watchers *lock.Map[string, struct{}]) T {
   588  	o := f.Tracker()
   589  	objectReaction := k8sTesting.ObjectReaction(o)
   590  
   591  	// Prepend our own reactors that adds field selector filtering to
   592  	// the results.
   593  	f.PrependReactor("*", "*", func(action k8sTesting.Action) (handled bool, ret k8sRuntime.Object, err error) {
   594  		handled, ret, err = objectReaction(action)
   595  
   596  		switch action := action.(type) {
   597  		case k8sTesting.ListActionImpl:
   598  			filterList(ret, action.GetListRestrictions())
   599  		}
   600  		return
   601  
   602  	})
   603  
   604  	f.PrependWatchReactor(
   605  		"*",
   606  		func(action k8sTesting.Action) (handled bool, ret watch.Interface, err error) {
   607  			w := action.(k8sTesting.WatchAction)
   608  			gvr := w.GetResource()
   609  			ns := w.GetNamespace()
   610  			watch, err := o.Watch(gvr, ns)
   611  			if err != nil {
   612  				return false, nil, err
   613  			}
   614  			if _, ok := watchers.Load(gvr.Resource); ok {
   615  				t.Logf("Multiple watches for resource %q intercepted. This highlights a potential cause for flakes", gvr.Resource)
   616  			}
   617  			watchers.Store(gvr.Resource, struct{}{})
   618  
   619  			fw := &filteringWatcher{
   620  				parent:       watch,
   621  				restrictions: w.GetWatchRestrictions(),
   622  			}
   623  			return true, fw, nil
   624  
   625  		})
   626  
   627  	return f
   628  }