client-go testing: fix List+Watch support

5644850607 added support for List+Watch to a fake client-go instance.
However, that support was not quite working yet as seen when analyzing a test
flake:

- List returned early when there were no objects, without adding the
  ResourceVersion. The ResourceVersion should have been "0" instead.
- When encountering "" as ResourceVersion, Watch didn't deliver
  any objects. That was meant to preserve compatibility with clients
  which don't expect objects from a Watch, but the right semantic of
  "" is "Start at most recent", which includes delivering existing
  objects.

Tests which meddle with the List implementation via a reactor (like
clustertrustbundlepublisher) have to be aware that Watch now may
return objects when given an empty ResourceVersion.
This commit is contained in:
Patrick Ohly
2026-01-09 16:54:10 +01:00
parent 6f93518c7f
commit 6bfa727bee
3 changed files with 30 additions and 14 deletions

View File

@@ -313,6 +313,10 @@ func fakeKubeClientSetWithCTBList(t *testing.T, signerName string, ctbs ...runti
}
}
// Ensure that Watch doesn't return any objects either by bumping up the resource version
// beyond what the fake client uses.
retList.ResourceVersion = "1000"
return true, retList, nil
})

View File

@@ -369,7 +369,7 @@ func TestListWithNoFixturesAndTypedScheme(t *testing.T) {
expectedList := &unstructured.UnstructuredList{}
expectedList.SetGroupVersionKind(listGVK)
expectedList.SetResourceVersion("") // by product of the fake setting resource version
expectedList.SetResourceVersion("0") // No objects created so far.
expectedList.SetContinue("")
if diff := cmp.Diff(expectedList, list); diff != "" {

View File

@@ -322,6 +322,7 @@ type tracker struct {
// but this is not how fake client-go has traditionally worked and starting to do
// that now might break tests.
type versionedObject struct {
// resourceVersion is always > 0 for a stored object.
resourceVersion int64
runtime.Object
}
@@ -368,6 +369,10 @@ func (t *tracker) List(gvr schema.GroupVersionResource, gvk schema.GroupVersionK
t.lock.RLock()
defer t.lock.RUnlock()
if listMeta, err := meta.ListAccessor(list); err == nil {
listMeta.SetResourceVersion(fmt.Sprintf("%d", t.resourceVersions[gvr]))
}
objs, ok := t.objects[gvr]
if !ok {
return list, nil
@@ -384,9 +389,6 @@ func (t *tracker) List(gvr schema.GroupVersionResource, gvk schema.GroupVersionK
if err := meta.SetList(list, matchingObjs); err != nil {
return nil, err
}
if listMeta, err := meta.ListAccessor(list); err == nil {
listMeta.SetResourceVersion(fmt.Sprintf("%d", t.resourceVersions[gvr]))
}
return list.DeepCopyObject(), nil
}
@@ -395,16 +397,26 @@ func (t *tracker) Watch(gvr schema.GroupVersionResource, ns string, opts ...meta
if err != nil {
return nil, err
}
// By default, emulate the traditional behavior of the tracker and don't deliver
// *any* existing objects. This ensures compatibility which users
// which don't pass options and then also don't expect the additional objects.
resourceVersion := int64(-1)
if len(opts) > 0 && opts[0].ResourceVersion != "" {
rv, err := strconv.ParseInt(opts[0].ResourceVersion, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid ResourceVersion %q in ListOptions, must be int64: %w", opts[0].ResourceVersion, err)
// *any* existing objects unless list options are provided.
addExisting := false
addFromRV := int64(0)
if len(opts) > 0 {
// Providing options, as the generated client-go fake does, enables support
// for existing objects depending on the resource version.
//
// The default if ResourceVersion is empty is "start at most recent",
// which includes delivering all existing objects. addFromRV == 0
// matches all objects below because all stored objects have addFromRV > 0.
addExisting = true
if opts[0].ResourceVersion != "" {
rv, err := strconv.ParseInt(opts[0].ResourceVersion, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid ResourceVersion %q in ListOptions, must be int64: %w", opts[0].ResourceVersion, err)
}
addFromRV = rv
}
resourceVersion = rv
}
t.lock.Lock()
@@ -419,14 +431,14 @@ func (t *tracker) Watch(gvr schema.GroupVersionResource, ns string, opts ...meta
// Deliver all objects that match the list options, for example
// between the initial List and the following Watch.
if resourceVersion != -1 {
if addExisting {
objs := t.objects[gvr]
matchingObjs, err := filterByNamespace(objs, ns)
if err != nil {
return nil, err
}
for _, obj := range matchingObjs {
if resourceVersion == 0 || resourceVersion < obj.resourceVersion {
if addFromRV < obj.resourceVersion {
fakewatcher.Add(obj.Object)
}
}