kubernetes/kubernetes · #142556

Use ListOptions for request arguments but panic on unsupported combinations

serathius · merged Sep 30, 20268 files · 450 + / 149 −
staging/src/k8s.io/apiserver/pkg/storage/testing/correctness/correctness.go152 + / 2 −
@@ -23,6 +23,7 @@ import (  	"github.com/stretchr/testify/require" +	apierrors "k8s.io/apimachinery/pkg/api/errors" 	"k8s.io/apimachinery/pkg/api/meta" 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 	"k8s.io/apimachinery/pkg/runtime"@@ -59,6 +60,7 @@ func correctnessTestSteps() []testStep { 	pod4Key := mustGetKey(pod4) 	pod5Key := mustGetKey(pod5) 	wrongRV := "99"+	_, invalidRVErr := storage.APIObjectVersioner{}.ParseResourceVersion("abc") 	pod2RV := "3" 	pod3RV8 := "8" 	pod3RV9 := "9"@@ -1111,6 +1113,146 @@ func correctnessTestSteps() []testStep { 				{Object: nil, Err: nil}, 			}, 		},+		{+			Name: "49. Create pod1 with ResourceVersion set returns ErrResourceVersionSetOnCreate",+			Request: Request{+				Op:     OpCreate,+				Key:    pod1Key,+				Create: CreateRequest{Object: withRV(pod1, "5")},+			},+			CorrectResponse: Response{Err: storage.ErrResourceVersionSetOnCreate},+			InvalidResponses: []Response{+				{Object: withRV(pod1, "19")},+				{Object: nil, Err: nil},+			},+		},+		{+			Name: "50. Create with empty key returns empty key error",+			Request: Request{+				Op:     OpCreate,+				Key:    "",+				Create: CreateRequest{Object: pod1},+			},+			CorrectResponse: Response{Err: fmt.Errorf("empty key: %q", "")},+			InvalidResponses: []Response{+				{Object: withRV(pod1, "19")},+				{Object: nil, Err: nil},+			},+		},+		{+			Name: "51. Get with key escaping the prefix returns invalid key error",+			Request: Request{+				Op:  OpGet,+				Key: "/pods/../secrets/s1",+			},+			CorrectResponse: Response{Err: fmt.Errorf("invalid key: %q", "/pods/../secrets/s1")},+			InvalidResponses: []Response{+				{Object: nil, Err: storage.NewKeyNotFoundError("/pods/../secrets/s1", 0)},+				{Object: nil, Err: nil},+			},+		},+		{+			Name: "52. Update with key containing . returns invalid key error",+			Request: Request{+				Op:  OpUpdate,+				Key: "/pods/./ns1/pod4",+				Update: UpdateRequest{+					UpdateFunc: storage.SimpleUpdate(func(obj runtime.Object) (runtime.Object, error) { return obj, nil }),+				},+			},+			CorrectResponse: Response{Err: fmt.Errorf("invalid key: %q", "/pods/./ns1/pod4")},+			InvalidResponses: []Response{+				{Object: nil, Err: storage.NewKeyNotFoundError("/pods/./ns1/pod4", 18)},+				{Object: nil, Err: nil},+			},+		},+		{+			Name: "53. Delete with key / returns empty key error",+			Request: Request{+				Op:  OpDelete,+				Key: "/",+			},+			CorrectResponse: Response{Err: fmt.Errorf("empty key: %q", "/")},+			InvalidResponses: []Response{+				{Object: nil, Err: storage.NewKeyNotFoundError("/", 18)},+				{Object: nil, Err: nil},+			},+		},+		{+			Name: "54. List with key ending with .. returns invalid key error",+			Request: Request{+				Op:   OpList,+				Key:  "/pods/..",+				List: listRecursive,+			},+			CorrectResponse: Response{Err: fmt.Errorf("invalid key: %q", "/pods/..")},+			InvalidResponses: []Response{+				{Object: newTestPodList("18", withLabel(pod4, "17", "version", "v2"))},+				{Object: nil, Err: nil},+			},+		},+		{+			Name: "55. List with unparsable ResourceVersion returns bad request",+			Request: Request{+				Op:  OpList,+				Key: "/pods/",+				List: ListRequest{Options: storage.ListOptions{+					ResourceVersion: "abc",+					Recursive:       true,+					Predicate:       storage.Everything,+				}},+			},+			CorrectResponse: Response{Err: apierrors.NewBadRequest(fmt.Sprintf("invalid resource version: %v", invalidRVErr))},+			InvalidResponses: []Response{+				{Object: newTestPodList("18", withLabel(pod4, "17", "version", "v2"))},+				{Object: nil, Err: nil},+			},+		},+		{+			Name: "56. List with unknown ResourceVersionMatch returns error",+			Request: Request{+				Op:  OpList,+				Key: "/pods/",+				List: ListRequest{Options: storage.ListOptions{+					ResourceVersion:      "15",+					ResourceVersionMatch: "Newest",+					Recursive:            true,+					Predicate:            storage.Everything,+				}},+			},+			CorrectResponse: Response{Err: fmt.Errorf("unknown ResourceVersionMatch value: %v", "Newest")},+			InvalidResponses: []Response{+				{Object: newTestPodList("18", withLabel(pod4, "17", "version", "v2"))},+				{Object: nil, Err: nil},+			},+		},+	}+}++type watchTestCase struct {+	Name        string+	Request     WatchRequest+	ExpectError error+}++func watchTestCasesInvalid() []watchTestCase {+	_, invalidRVErr := storage.APIObjectVersioner{}.ParseResourceVersion("abc")+	return []watchTestCase{+		{+			Name:        "Watch with empty key returns empty key error",+			Request:     WatchRequest{Key: "", Options: storage.ListOptions{ResourceVersion: "1", Predicate: storage.Everything, Recursive: true}},+			ExpectError: fmt.Errorf("empty key: %q", ""),+		},+		{+			Name:        "Watch with key escaping the prefix returns invalid key error",+			Request:     WatchRequest{Key: "/pods/../secrets", Options: storage.ListOptions{ResourceVersion: "1", Predicate: storage.Everything, Recursive: true}},+			ExpectError: fmt.Errorf("invalid key: %q", "/pods/../secrets"),+		},+		{+			Name:        "Watch with unparsable ResourceVersion returns invalid error",+			Request:     WatchRequest{Key: "/pods/", Options: storage.ListOptions{ResourceVersion: "abc", Predicate: storage.Everything, Recursive: true}},+			ExpectError: invalidRVErr,+		}, 	} } @@ -1121,8 +1263,8 @@ func RunTestCorrectness(ctx context.Context, t *testing.T, store storage.Interfa 	initialState := NewEmptyModel(storagePrefix, func() runtime.Object { return &example.Pod{} }, func() runtime.Object { return &example.PodList{} }, versioner) 	model := initialState.Clone() -	watchRequest := WatchRequest{ResourceVersion: "1"}-	watcher, err := store.Watch(ctx, "/pods/", storage.ListOptions{ResourceVersion: watchRequest.ResourceVersion, Predicate: storage.Everything, Recursive: true})+	watchRequest := WatchRequest{Key: "/pods/", Options: storage.ListOptions{ResourceVersion: "1", Predicate: storage.Everything, Recursive: true}}+	watcher, err := store.Watch(ctx, watchRequest.Key, watchRequest.Options) 	require.NoError(t, err) 	defer watcher.Stop() @@ -1171,6 +1313,14 @@ func RunTestCorrectness(ctx context.Context, t *testing.T, store storage.Interfa 	gotEvents := collectEventsTillRV(t, watcher, versioner, model.ResourceVersion) 	validator := NewWatchValidator(versioner, replay, keyFunc) 	require.NoError(t, validator.ValidateWatch(watchRequest, WatchResponse{Events: gotEvents}))++	for _, step := range watchTestCasesInvalid() {+		w, err := store.Watch(ctx, step.Request.Key, step.Request.Options)+		if err == nil {+			w.Stop()+		}+		require.NoError(t, validator.ValidateWatch(step.Request, WatchResponse{Err: err}), "step %s", step.Name)+	} }  func collectEventsTillRV(t *testing.T, watcher watch.Interface, versioner storage.Versioner, targetRV uint64) []watch.Event {
staging/src/k8s.io/apiserver/pkg/storage/testing/correctness/model.go65 + / 10 −
@@ -117,6 +117,30 @@ func (s *Model) Step(input Request, output Response) (ok bool, next *Model, chan 	case OpGet: 		expected = s.get(input.Key, input.Get.Options) 	case OpList:+		if err := checkKey(input.Key, input.List.Options.Recursive); err != nil {+			expected = Response{Err: err}+			break+		}+		if _, _, err := storage.ValidateListOptions("", s.Versioner, input.List.Options); err != nil {+			expected = Response{Err: err}+			break+		}+		if input.List.Options.Predicate.Label == nil || input.List.Options.Predicate.Field == nil {+			// etcd3 and the cacher call methods on both selectors.+			panic("nil label or field selector is not supported, use storage.Everything to match everything")+		}+		if input.List.Options.Predicate.Limit != 0 || input.List.Options.Predicate.Continue != "" {+			panic("pagination (limit, continue) is not supported")+		}+		if !input.List.Options.Predicate.Empty() && input.List.Options.Predicate.GetAttrs == nil {+			panic("selectors without GetAttrs are not supported")+		}+		if input.List.Options.RecordTimestamps {+			panic("recordTimestamps is not supported, it wraps objects in storage-internal types")+		}+		if !input.List.Options.Predicate.Empty() {+			panic("label and field selectors are not supported, the model doesn't filter lists")+		} 		// Model only validates consistent reads, for stale reads we just validate RV and contents are validated later during replay. 		if input.List.Options.ResourceVersion != "" { 			return s.validateListRV(input.List.Options, output), s, nil@@ -125,6 +149,8 @@ func (s *Model) Step(input Request, output Response) (ok bool, next *Model, chan 	case OpUpdate: 		next = s.Clone() 		expected, change = next.update(context.Background(), input.Key, input.Update.IgnoreNotFound, input.Update.Preconditions, input.Update.UpdateFunc, input.Update.CachedExistingObject)+	default:+		panic(fmt.Sprintf("unknown operation %q", input.Op)) 	} 	if !reflect.DeepEqual(expected, output) { 		return false, s, nil@@ -179,6 +205,12 @@ func (s *Model) validateListRV(opts storage.ListOptions, output Response) bool { }  func (s *Model) update(ctx context.Context, key string, ignoreNotFound bool, preconditions *storage.Preconditions, tryUpdate storage.UpdateFunc, cachedExistingObject runtime.Object) (Response, *Change) {+	if err := checkKey(key, false); err != nil {+		return Response{Err: err}, nil+	}+	if tryUpdate == nil {+		panic("tryUpdate must be set")+	} 	stored, exists := s.Items[key] 	var currentObj runtime.Object 	var currentRV uint64@@ -204,10 +236,6 @@ func (s *Model) update(ctx context.Context, key string, ignoreNotFound bool, pre 		return Response{Object: nil, Err: err}, nil 	} -	if tryUpdate == nil {-		return Response{Object: nil, Err: fmt.Errorf("tryUpdate function must not be nil")}, nil-	}- 	updated, _, err := tryUpdate(currentObj, storage.ResponseMeta{ResourceVersion: currentRV}) 	if err != nil { 		return Response{Object: nil, Err: err}, nil@@ -237,11 +265,16 @@ func (s *Model) update(ctx context.Context, key string, ignoreNotFound bool, pre 	} 	accessor.SetResourceVersion(strconv.FormatUint(s.ResourceVersion, 10)) 	s.Items[key] = copied-	// With ignoreNotFound on a missing key, stored is nil and this is a create.-	return Response{Object: copied, Err: nil}, &Change{ResourceVersion: s.ResourceVersion, Object: copied, PrevObject: stored}+	return Response{Object: copied, Err: nil}, &Change{Key: key, ResourceVersion: s.ResourceVersion, Object: copied, PrevObject: stored} }  func (s *Model) create(key string, obj runtime.Object) (Response, *Change) {+	if err := checkKey(key, false); err != nil {+		return Response{Err: err}, nil+	}+	if rv, err := s.Versioner.ObjectResourceVersion(obj); err == nil && rv != 0 {+		return Response{Err: storage.ErrResourceVersionSetOnCreate}, nil+	} 	if _, exists := s.Items[key]; exists { 		return Response{Object: nil, Err: storage.NewKeyExistsError(s.Prefix+key, 0)}, nil 	}@@ -253,10 +286,16 @@ func (s *Model) create(key string, obj runtime.Object) (Response, *Change) { 	} 	accessor.SetResourceVersion(strconv.FormatUint(s.ResourceVersion, 10)) 	s.Items[key] = copied-	return Response{Object: copied, Err: nil}, &Change{ResourceVersion: s.ResourceVersion, Object: copied}+	return Response{Object: copied, Err: nil}, &Change{Key: key, ResourceVersion: s.ResourceVersion, Object: copied} }  func (s *Model) get(key string, opts storage.GetOptions) Response {+	if err := checkKey(key, false); err != nil {+		return Response{Err: err}+	}+	if opts.ResourceVersion != "" {+		panic("get with resourceVersion is not supported, the model only serves the latest state")+	} 	stored, exists := s.Items[key] 	if !exists { 		if opts.IgnoreNotFound {@@ -268,10 +307,9 @@ func (s *Model) get(key string, opts storage.GetOptions) Response { }  func (s *Model) list(key string, opts storage.ListOptions) Response {-	prefix := strings.TrimSuffix(key, "/") + "/" 	var items []runtime.Object 	for _, k := range slices.Sorted(maps.Keys(s.Items)) {-		if (opts.Recursive && strings.HasPrefix(k, prefix)) || (!opts.Recursive && k == key) {+		if keyInScope(key, opts.Recursive, k) { 			items = append(items, s.Items[k].DeepCopyObject()) 		} 	}@@ -285,7 +323,24 @@ func (s *Model) list(key string, opts storage.ListOptions) Response { 	return Response{Object: list, Err: nil} } +// keyInScope reports whether k is selected by a list or watch on key.+func keyInScope(key string, recursive bool, k string) bool {+	if !recursive {+		return k == key+	}+	// Like storage.PrepareKey, "/a" must not select "/ab".+	return strings.HasPrefix(k, strings.TrimSuffix(key, "/")+"/")+}++func checkKey(key string, recursive bool) error {+	_, err := storage.PrepareKey("", key, recursive)+	return err+}+ func (s *Model) delete(ctx context.Context, key string, preconditions *storage.Preconditions, validateDeletion storage.ValidateObjectFunc) (Response, *Change) {+	if err := checkKey(key, false); err != nil {+		return Response{Err: err}, nil+	} 	stored, exists := s.Items[key] 	if !exists { 		return Response{Object: nil, Err: storage.NewKeyNotFoundError(s.Prefix+key, int64(s.ResourceVersion))}, nil@@ -306,7 +361,7 @@ func (s *Model) delete(ctx context.Context, key string, preconditions *storage.P 	} 	accessor.SetResourceVersion(strconv.FormatUint(s.ResourceVersion, 10)) 	delete(s.Items, key)-	return Response{Object: deletedObj, Err: nil}, &Change{ResourceVersion: s.ResourceVersion, PrevObject: stored}+	return Response{Object: deletedObj, Err: nil}, &Change{Key: key, ResourceVersion: s.ResourceVersion, PrevObject: stored} }  func (s *Model) Describe() string {
staging/src/k8s.io/apiserver/pkg/storage/testing/correctness/model_test.go13 + / 0 −
@@ -65,3 +65,16 @@ func TestCorrectness(t *testing.T) { 	} 	require.Equal(t, expectEvents, gotEvents) }++// TestOptionsInvalid checks the model expects the same error storage returns+// for watches it rejects. Invalid requests are covered by correctnessTestSteps.+func TestOptionsInvalid(t *testing.T) {+	versioner := storage.APIObjectVersioner{}+	validator := NewWatchValidator(versioner, &Replay{versioner: versioner}, getKey)+	for _, step := range watchTestCasesInvalid() {+		t.Run(step.Name, func(t *testing.T) {+			require.NoError(t, validator.ValidateWatch(step.Request, WatchResponse{Err: step.ExpectError}))+			require.Error(t, validator.ValidateWatch(step.Request, WatchResponse{}))+		})+	}+}
staging/src/k8s.io/apiserver/pkg/storage/testing/correctness/replay.go4 + / 1 −
@@ -94,7 +94,10 @@ func (r *Replay) Events(request WatchRequest, rvRange *ResourceVersionRange) ([] 		if change.ResourceVersion < rvRange.Min || change.ResourceVersion >= rvRange.Max { 			continue 		}-		watchEvent, err := change.toWatchEvent(r.versioner, request.Predicate)+		if !keyInScope(request.Key, request.Options.Recursive, change.Key) {+			continue+		}+		watchEvent, err := change.toWatchEvent(r.versioner, request.Options.Predicate) 		if err != nil { 			return nil, err 		}
staging/src/k8s.io/apiserver/pkg/storage/testing/correctness/types.go4 + / 4 −
@@ -161,16 +161,16 @@ type Response struct { // create and Object is nil for a delete. Like etcd3 and the cacher, deciding // what a watcher with a predicate receives requires both objects. type Change struct {+	Key             string 	ResourceVersion uint64 	Object          runtime.Object 	PrevObject      runtime.Object } -// WatchRequest contains parameters for a watch stream.+// WatchRequest contains parameters for a watch stream, exactly as passed to storage.Interface.Watch. type WatchRequest struct {-	ResourceVersion string-	// Predicate filters events. The zero value matches everything.-	Predicate storage.SelectionPredicate+	Key     string+	Options storage.ListOptions }  // WatchResponse contains the events and any terminal error received from a watch stream.
staging/src/k8s.io/apiserver/pkg/storage/testing/correctness/watch.go37 + / 2 −
@@ -44,6 +44,12 @@ func NewWatchValidator(versioner storage.Versioner, replay *Replay, keyFunc func }  func (v WatchValidator) ValidateWatch(request WatchRequest, response WatchResponse) error {+	if err := v.checkWatch(request); err != nil {+		if !reflect.DeepEqual(err, response.Err) {+			return fmt.Errorf("watch %+v: expected error %v, got %v", request, err, response.Err)+		}+		return nil+	} 	if response.Err != nil { 		return fmt.Errorf("watch %+v: unexpected error: %w", request, response.Err) 	}@@ -59,6 +65,35 @@ func (v WatchValidator) ValidateWatch(request WatchRequest, response WatchRespon 	return nil } +// checkWatch returns the error storage returns for an invalid watch and panics+// on watches the model can't reproduce.+func (v WatchValidator) checkWatch(request WatchRequest) error {+	opts := request.Options+	if err := checkKey(request.Key, opts.Recursive); err != nil {+		return err+	}+	if _, err := v.versioner.ParseResourceVersion(opts.ResourceVersion); err != nil {+		return err+	}+	if opts.Predicate.Label == nil || opts.Predicate.Field == nil {+		// etcd3 and the cacher call methods on both selectors.+		panic("nil label or field selector is not supported, use storage.Everything to match everything")+	}+	if opts.Predicate.Limit != 0 || opts.Predicate.Continue != "" {+		panic("pagination (limit, continue) is not supported")+	}+	if !opts.Predicate.Empty() && opts.Predicate.GetAttrs == nil {+		panic("selectors without GetAttrs are not supported")+	}+	if opts.RecordTimestamps {+		panic("recordTimestamps is not supported, it wraps objects in storage-internal types")+	}+	if opts.SendInitialEvents != nil && *opts.SendInitialEvents {+		panic("initial events are not supported, set sendInitialEvents=false for resourceVersion \"\" or \"0\"")+	}+	return nil+}+ func validateMaybeLastEventError(events []watch.Event) error { 	for i, ev := range events { 		if ev.Type != watch.Error {@@ -212,8 +247,8 @@ func watchRevisionRange(versioner storage.Versioner, request WatchRequest, event 			maxRV = max(maxRV, rv+1) 		} 	}-	if request.ResourceVersion != "0" && request.ResourceVersion != "" {-		requestedRV, err := versioner.ParseResourceVersion(request.ResourceVersion)+	if request.Options.ResourceVersion != "0" && request.Options.ResourceVersion != "" {+		requestedRV, err := versioner.ParseResourceVersion(request.Options.ResourceVersion) 		if err != nil { 			return nil, err 		}
staging/src/k8s.io/apiserver/pkg/storage/testing/correctness/watch_test.go155 + / 112 −
@@ -45,14 +45,16 @@ func TestValidateWatch(t *testing.T) { 		pod1RV7       = watch.Event{Type: watch.Modified, Object: dropLabel(pod1, "7", "color")} 		deletePod1RV8 = watch.Event{Type: watch.Deleted, Object: withRV(pod1, "8")} 	)+	pod1Key := mustGetKey(pod1)+	pod2Key := mustGetKey(pod2) 	history := []Change{-		{ResourceVersion: 2, Object: addPod1RV2.Object},-		{ResourceVersion: 3, Object: addPod2RV3.Object},-		{ResourceVersion: 4, Object: bluePod1RV4.Object, PrevObject: addPod1RV2.Object},-		{ResourceVersion: 5, PrevObject: addPod2RV3.Object},-		{ResourceVersion: 6, Object: redPod1RV6.Object, PrevObject: bluePod1RV4.Object},-		{ResourceVersion: 7, Object: pod1RV7.Object, PrevObject: redPod1RV6.Object},-		{ResourceVersion: 8, PrevObject: pod1RV7.Object},+		{Key: pod1Key, ResourceVersion: 2, Object: addPod1RV2.Object},+		{Key: pod2Key, ResourceVersion: 3, Object: addPod2RV3.Object},+		{Key: pod1Key, ResourceVersion: 4, Object: bluePod1RV4.Object, PrevObject: addPod1RV2.Object},+		{Key: pod2Key, ResourceVersion: 5, PrevObject: addPod2RV3.Object},+		{Key: pod1Key, ResourceVersion: 6, Object: redPod1RV6.Object, PrevObject: bluePod1RV4.Object},+		{Key: pod1Key, ResourceVersion: 7, Object: pod1RV7.Object, PrevObject: redPod1RV6.Object},+		{Key: pod1Key, ResourceVersion: 8, PrevObject: pod1RV7.Object}, 	} 	// Events for object transitions between selectors. 	var (@@ -81,6 +83,16 @@ func TestValidateWatch(t *testing.T) { 		Field:    fields.OneTermEqualSelector("metadata.name", "pod2"), 		GetAttrs: storage.DefaultNamespaceScopedAttr, 	}+	// watchRequest mirrors the watches the integration traffic opens.+	watchRequest := func(rv string, pred storage.SelectionPredicate) WatchRequest {+		opts := storage.ListOptions{ResourceVersion: rv, Predicate: pred, Recursive: true}+		if rv == "" || rv == "0" {+			// Otherwise storage starts with initial events, which aren't supported.+			opts.SendInitialEvents = new(false)+			opts.ResourceVersionMatch = metav1.ResourceVersionMatchNotOlderThan+		}+		return WatchRequest{Key: "/pods/", Options: opts}+	}  	tests := []struct { 		name        string@@ -91,82 +103,82 @@ func TestValidateWatch(t *testing.T) { 		{ 			name: "whole history", 			requests: []WatchRequest{-				{ResourceVersion: ""},-				{ResourceVersion: "0"},-				{ResourceVersion: "1"},+				watchRequest("", storage.Everything),+				watchRequest("0", storage.Everything),+				watchRequest("1", storage.Everything), 			}, 			events: []watch.Event{addPod1RV2, addPod2RV3, bluePod1RV4, deletePod2RV5, redPod1RV6, pod1RV7, deletePod1RV8}, 		}, 		{ 			name: "partial history from beginning", 			requests: []WatchRequest{-				{ResourceVersion: ""},-				{ResourceVersion: "0"},+				watchRequest("", storage.Everything),+				watchRequest("0", storage.Everything), 			}, 			events: []watch.Event{addPod2RV3, bluePod1RV4, deletePod2RV5, redPod1RV6, pod1RV7, deletePod1RV8}, 		}, 		{ 			name: "partial history from the end", 			requests: []WatchRequest{-				{ResourceVersion: ""},-				{ResourceVersion: "0"},-				{ResourceVersion: "1"},+				watchRequest("", storage.Everything),+				watchRequest("0", storage.Everything),+				watchRequest("1", storage.Everything), 			}, 			events: []watch.Event{addPod1RV2, addPod2RV3, bluePod1RV4}, 		}, 		{ 			name: "watch opened on exact resource version", 			requests: []WatchRequest{-				{ResourceVersion: "3"},+				watchRequest("3", storage.Everything), 			}, 			events: []watch.Event{bluePod1RV4, deletePod2RV5}, 		}, 		{ 			name: "missing event in the middle", 			requests: []WatchRequest{-				{ResourceVersion: ""},-				{ResourceVersion: "0"},-				{ResourceVersion: "1"},+				watchRequest("", storage.Everything),+				watchRequest("0", storage.Everything),+				watchRequest("1", storage.Everything), 			}, 			events:      []watch.Event{addPod2RV3 /*bluePod1RV4,*/, deletePod2RV5}, 			expectError: true, 		}, 		{ 			name: "duplicate event", 			requests: []WatchRequest{-				{ResourceVersion: ""},-				{ResourceVersion: "0"},-				{ResourceVersion: "1"},+				watchRequest("", storage.Everything),+				watchRequest("0", storage.Everything),+				watchRequest("1", storage.Everything), 			}, 			events:      []watch.Event{addPod1RV2, addPod2RV3, addPod2RV3, bluePod1RV4}, 			expectError: true, 		}, 		{ 			name: "events out of order of different type", 			requests: []WatchRequest{-				{ResourceVersion: ""},-				{ResourceVersion: "0"},-				{ResourceVersion: "1"},+				watchRequest("", storage.Everything),+				watchRequest("0", storage.Everything),+				watchRequest("1", storage.Everything), 			}, 			events:      []watch.Event{addPod1RV2, addPod2RV3, deletePod2RV5, bluePod1RV4}, 			expectError: true, 		}, 		{ 			name: "events out of order of the same type", 			requests: []WatchRequest{-				{ResourceVersion: ""},-				{ResourceVersion: "0"},-				{ResourceVersion: "1"},+				watchRequest("", storage.Everything),+				watchRequest("0", storage.Everything),+				watchRequest("1", storage.Everything), 			}, 			events:      []watch.Event{addPod2RV3, addPod1RV2, bluePod1RV4, deletePod2RV5}, 			expectError: true, 		}, 		{ 			name: "event the history never produced", 			requests: []WatchRequest{-				{ResourceVersion: ""},-				{ResourceVersion: "0"},-				{ResourceVersion: "1"},+				watchRequest("", storage.Everything),+				watchRequest("0", storage.Everything),+				watchRequest("1", storage.Everything), 			}, 			events: []watch.Event{ 				addPod1RV2,@@ -178,9 +190,9 @@ func TestValidateWatch(t *testing.T) { 		{ 			name: "wrong event type", 			requests: []WatchRequest{-				{ResourceVersion: ""},-				{ResourceVersion: "0"},-				{ResourceVersion: "1"},+				watchRequest("", storage.Everything),+				watchRequest("0", storage.Everything),+				watchRequest("1", storage.Everything), 			}, 			events: []watch.Event{ 				addPod1RV2,@@ -192,7 +204,7 @@ func TestValidateWatch(t *testing.T) { 		{ 			name: "wrong object in event", 			requests: []WatchRequest{-				{ResourceVersion: "1"},+				watchRequest("1", storage.Everything), 			}, 			events: []watch.Event{ 				addPod1RV2,@@ -204,25 +216,25 @@ func TestValidateWatch(t *testing.T) { 		{ 			name: "event at the requested resource version", 			requests: []WatchRequest{-				{ResourceVersion: "3"},+				watchRequest("3", storage.Everything), 			}, 			events:      []watch.Event{addPod2RV3, bluePod1RV4, deletePod2RV5}, 			expectError: true, 		}, 		{ 			name: "bookmark after every event events", 			requests: []WatchRequest{-				{ResourceVersion: ""},-				{ResourceVersion: "0"},-				{ResourceVersion: "1"},+				watchRequest("", storage.Everything),+				watchRequest("0", storage.Everything),+				watchRequest("1", storage.Everything), 			}, 			events: []watch.Event{newBookmark("1"), addPod1RV2, newBookmark("2"), addPod2RV3, newBookmark("3"), bluePod1RV4, newBookmark("4"), deletePod2RV5, newBookmark("5")}, 		}, 		{ 			name: "bookmark after the event", 			requests: []WatchRequest{-				{ResourceVersion: ""},-				{ResourceVersion: "0"},+				watchRequest("", storage.Everything),+				watchRequest("0", storage.Everything), 			}, 			events: []watch.Event{ 				addPod2RV3,@@ -232,8 +244,8 @@ func TestValidateWatch(t *testing.T) { 		{ 			name: "bookmark ahead of the event", 			requests: []WatchRequest{-				{ResourceVersion: ""},-				{ResourceVersion: "0"},+				watchRequest("", storage.Everything),+				watchRequest("0", storage.Everything), 			}, 			events: []watch.Event{ 				newBookmark("3"),@@ -244,9 +256,9 @@ func TestValidateWatch(t *testing.T) { 		{ 			name: "bookmark on fresh watch", 			requests: []WatchRequest{-				{ResourceVersion: ""},-				{ResourceVersion: "0"},-				{ResourceVersion: "2"},+				watchRequest("", storage.Everything),+				watchRequest("0", storage.Everything),+				watchRequest("2", storage.Everything), 			}, 			events: []watch.Event{ 				newBookmark("3"),@@ -255,9 +267,9 @@ func TestValidateWatch(t *testing.T) { 		{ 			name: "bookmark on fresh watch", 			requests: []WatchRequest{-				{ResourceVersion: ""},-				{ResourceVersion: "0"},-				{ResourceVersion: "2"},+				watchRequest("", storage.Everything),+				watchRequest("0", storage.Everything),+				watchRequest("2", storage.Everything), 			}, 			events: []watch.Event{ 				addPod2RV3,@@ -268,106 +280,106 @@ func TestValidateWatch(t *testing.T) { 		{ 			name: "label selector color=blue", 			requests: []WatchRequest{-				{ResourceVersion: "", Predicate: isBlue},-				{ResourceVersion: "0", Predicate: isBlue},-				{ResourceVersion: "1", Predicate: isBlue},-				{ResourceVersion: "2", Predicate: isBlue},-				{ResourceVersion: "3", Predicate: isBlue},+				watchRequest("", isBlue),+				watchRequest("0", isBlue),+				watchRequest("1", isBlue),+				watchRequest("2", isBlue),+				watchRequest("3", isBlue), 			}, 			events: []watch.Event{addBluePod1RV4, deleteBluePod1RV6}, 		}, 		{ 			name: "label selector opened on exact resource version", 			requests: []WatchRequest{-				{ResourceVersion: "4", Predicate: isBlue},-				{ResourceVersion: "5", Predicate: isBlue},+				watchRequest("4", isBlue),+				watchRequest("5", isBlue), 			}, 			events: []watch.Event{deleteBluePod1RV6}, 		}, 		{ 			name: "label selector color=red", 			requests: []WatchRequest{-				{ResourceVersion: "", Predicate: isRed},-				{ResourceVersion: "0", Predicate: isRed},-				{ResourceVersion: "1", Predicate: isRed},-				{ResourceVersion: "2", Predicate: isRed},-				{ResourceVersion: "3", Predicate: isRed},-				{ResourceVersion: "4", Predicate: isRed},-				{ResourceVersion: "5", Predicate: isRed},+				watchRequest("", isRed),+				watchRequest("0", isRed),+				watchRequest("1", isRed),+				watchRequest("2", isRed),+				watchRequest("3", isRed),+				watchRequest("4", isRed),+				watchRequest("5", isRed), 			}, 			events: []watch.Event{addRedPod1RV6, deleteRedPod1RV7}, 		}, 		{ 			name: "field selector name=pod1", 			requests: []WatchRequest{-				{ResourceVersion: "", Predicate: isPod1},-				{ResourceVersion: "0", Predicate: isPod1},-				{ResourceVersion: "1", Predicate: isPod1},+				watchRequest("", isPod1),+				watchRequest("0", isPod1),+				watchRequest("1", isPod1), 			}, 			events: []watch.Event{addPod1RV2, bluePod1RV4, redPod1RV6, pod1RV7, deletePod1RV8}, 		}, 		{ 			name: "field selector name=pod2", 			requests: []WatchRequest{-				{ResourceVersion: "", Predicate: isPod2},-				{ResourceVersion: "0", Predicate: isPod2},-				{ResourceVersion: "1", Predicate: isPod2},-				{ResourceVersion: "2", Predicate: isPod2},+				watchRequest("", isPod2),+				watchRequest("0", isPod2),+				watchRequest("1", isPod2),+				watchRequest("2", isPod2), 			}, 			events: []watch.Event{addPod2RV3, deletePod2RV5}, 		}, 		{ 			name: "event not matching the selector", 			requests: []WatchRequest{-				{ResourceVersion: "", Predicate: isBlue},-				{ResourceVersion: "0", Predicate: isBlue},-				{ResourceVersion: "1", Predicate: isBlue},+				watchRequest("", isBlue),+				watchRequest("0", isBlue),+				watchRequest("1", isBlue), 			}, 			events:      []watch.Event{addPod1RV2, addBluePod1RV4, deleteBluePod1RV6}, 			expectError: true, 		}, 		{ 			name: "missing move into the selector", 			requests: []WatchRequest{-				{ResourceVersion: "1", Predicate: isBlue},-				{ResourceVersion: "2", Predicate: isBlue},-				{ResourceVersion: "3", Predicate: isBlue},+				watchRequest("1", isBlue),+				watchRequest("2", isBlue),+				watchRequest("3", isBlue), 			}, 			events:      []watch.Event{ /*addBluePod1RV4,*/ deleteBluePod1RV6}, 			expectError: true, 		}, 		{ 			name: "move into the selector as modified instead of add", 			requests: []WatchRequest{-				{ResourceVersion: "", Predicate: isBlue},-				{ResourceVersion: "0", Predicate: isBlue},-				{ResourceVersion: "1", Predicate: isBlue},-				{ResourceVersion: "2", Predicate: isBlue},-				{ResourceVersion: "3", Predicate: isBlue},+				watchRequest("", isBlue),+				watchRequest("0", isBlue),+				watchRequest("1", isBlue),+				watchRequest("2", isBlue),+				watchRequest("3", isBlue), 			}, 			events:      []watch.Event{bluePod1RV4, deleteBluePod1RV6}, 			expectError: true, 		}, 		{ 			name: "move out of the selector as modified instead of delete", 			requests: []WatchRequest{-				{ResourceVersion: "", Predicate: isBlue},-				{ResourceVersion: "0", Predicate: isBlue},-				{ResourceVersion: "1", Predicate: isBlue},-				{ResourceVersion: "2", Predicate: isBlue},-				{ResourceVersion: "3", Predicate: isBlue},+				watchRequest("", isBlue),+				watchRequest("0", isBlue),+				watchRequest("1", isBlue),+				watchRequest("2", isBlue),+				watchRequest("3", isBlue), 			}, 			events:      []watch.Event{addBluePod1RV4, redPod1RV6}, 			expectError: true, 		}, 		{ 			name: "move out of the selector with update object instead of previous object", 			requests: []WatchRequest{-				{ResourceVersion: "", Predicate: isBlue},-				{ResourceVersion: "0", Predicate: isBlue},-				{ResourceVersion: "1", Predicate: isBlue},-				{ResourceVersion: "2", Predicate: isBlue},-				{ResourceVersion: "3", Predicate: isBlue},+				watchRequest("", isBlue),+				watchRequest("0", isBlue),+				watchRequest("1", isBlue),+				watchRequest("2", isBlue),+				watchRequest("3", isBlue), 			}, 			events: []watch.Event{ 				addBluePod1RV4,@@ -378,11 +390,11 @@ func TestValidateWatch(t *testing.T) { 		{ 			name: "move out of the selector with update object instead of previous object", 			requests: []WatchRequest{-				{ResourceVersion: "", Predicate: isBlue},-				{ResourceVersion: "0", Predicate: isBlue},-				{ResourceVersion: "1", Predicate: isBlue},-				{ResourceVersion: "2", Predicate: isBlue},-				{ResourceVersion: "3", Predicate: isBlue},+				watchRequest("", isBlue),+				watchRequest("0", isBlue),+				watchRequest("1", isBlue),+				watchRequest("2", isBlue),+				watchRequest("3", isBlue), 			}, 			events: []watch.Event{ 				addBluePod1RV4,@@ -393,52 +405,83 @@ func TestValidateWatch(t *testing.T) { 		{ 			name: "watch ending with error event", 			requests: []WatchRequest{-				{ResourceVersion: ""},-				{ResourceVersion: "0"},-				{ResourceVersion: "1"},+				watchRequest("", storage.Everything),+				watchRequest("0", storage.Everything),+				watchRequest("1", storage.Everything), 			}, 			events: []watch.Event{addPod1RV2, addPod2RV3, bluePod1RV4, newErrorEvent()}, 		}, 		{ 			name: "watch with only error event", 			requests: []WatchRequest{-				{ResourceVersion: ""},-				{ResourceVersion: "0"},-				{ResourceVersion: "1"},-				{ResourceVersion: "3"},+				watchRequest("", storage.Everything),+				watchRequest("0", storage.Everything),+				watchRequest("1", storage.Everything),+				watchRequest("3", storage.Everything), 			}, 			events: []watch.Event{newErrorEvent()}, 		}, 		{ 			name: "event after error event", 			requests: []WatchRequest{-				{ResourceVersion: ""},-				{ResourceVersion: "0"},-				{ResourceVersion: "1"},+				watchRequest("", storage.Everything),+				watchRequest("0", storage.Everything),+				watchRequest("1", storage.Everything), 			}, 			events:      []watch.Event{addPod1RV2, newErrorEvent(), addPod2RV3}, 			expectError: true, 		}, 		{ 			name: "missing event before error event", 			requests: []WatchRequest{-				{ResourceVersion: ""},-				{ResourceVersion: "0"},-				{ResourceVersion: "1"},+				watchRequest("", storage.Everything),+				watchRequest("0", storage.Everything),+				watchRequest("1", storage.Everything), 			}, 			events:      []watch.Event{addPod1RV2 /*addPod2RV3,*/, bluePod1RV4, newErrorEvent()}, 			expectError: true, 		}, 		{ 			name: "error event with non-status object", 			requests: []WatchRequest{-				{ResourceVersion: ""},-				{ResourceVersion: "0"},-				{ResourceVersion: "1"},+				watchRequest("", storage.Everything),+				watchRequest("0", storage.Everything),+				watchRequest("1", storage.Everything), 			}, 			events:      []watch.Event{addPod1RV2, {Type: watch.Error, Object: withRV(pod2, "3")}}, 			expectError: true, 		},+		{+			name: "watch on a single key",+			requests: []WatchRequest{+				{Key: pod1Key, Options: storage.ListOptions{ResourceVersion: "1", Predicate: storage.Everything}},+			},+			events: []watch.Event{addPod1RV2, bluePod1RV4, redPod1RV6, pod1RV7, deletePod1RV8},+		},+		{+			name: "watch on a single key with event for another key",+			requests: []WatchRequest{+				{Key: pod1Key, Options: storage.ListOptions{ResourceVersion: "1", Predicate: storage.Everything}},+			},+			events:      []watch.Event{addPod1RV2, addPod2RV3, bluePod1RV4},+			expectError: true,+		},+		{+			name: "watch on a namespace",+			requests: []WatchRequest{+				{Key: "/pods/ns1", Options: storage.ListOptions{ResourceVersion: "1", Predicate: storage.Everything, Recursive: true}},+				{Key: "/pods/ns1/", Options: storage.ListOptions{ResourceVersion: "1", Predicate: storage.Everything, Recursive: true}},+			},+			events: []watch.Event{addPod1RV2, addPod2RV3, bluePod1RV4, deletePod2RV5, redPod1RV6, pod1RV7, deletePod1RV8},+		},+		{+			name: "watch on another namespace with event from outside of it",+			requests: []WatchRequest{+				{Key: "/pods/ns", Options: storage.ListOptions{ResourceVersion: "1", Predicate: storage.Everything, Recursive: true}},+			},+			events:      []watch.Event{addPod1RV2},+			expectError: true,+		}, 	}  	versioner := storage.APIObjectVersioner{}
test/integration/apiserver/storage/traffic.go20 + / 18 −
@@ -480,41 +480,43 @@ func runTraffic(ctx context.Context, store storage.Interface, request *correctne }  func randomWatchRequest(ctx context.Context, store storage.Interface, distribution []ChoiceWeight[WatchRequestType]) correctness.WatchRequest {-	var offset int64+	var rv string 	switch selected := PickRandom(distribution); selected { 	case RVEmpty:-		return correctness.WatchRequest{ResourceVersion: ""}+		rv = "" 	case RVZero:-		return correctness.WatchRequest{ResourceVersion: "0"}+		rv = "0" 	case RVOne:-		return correctness.WatchRequest{ResourceVersion: "1"}+		rv = "1" 	case RVCurrent:-		offset = 0+		rv = relativeRV(ctx, store, 0) 	case RVPast:-		offset = -int64(1 + rand.Intn(10))+		rv = relativeRV(ctx, store, -int64(1+rand.Intn(10))) 	case RVFuture:-		offset = int64(1 + rand.Intn(10))+		rv = relativeRV(ctx, store, int64(1+rand.Intn(10))) 	default: 		panic(fmt.Sprintf("%v: unknown watch request type", selected)) 	}+	opts := storage.ListOptions{ResourceVersion: rv, Predicate: storage.Everything, Recursive: true}+	if rv == "" || rv == "0" {+		// Otherwise storage starts with synthetic ADDED events for existing+		// objects. API validation requires the match with sendInitialEvents.+		opts.SendInitialEvents = new(false)+		opts.ResourceVersionMatch = metav1.ResourceVersionMatchNotOlderThan+	}+	return correctness.WatchRequest{Key: "/pods/", Options: opts}+}++func relativeRV(ctx context.Context, store storage.Interface, offset int64) string { 	currentRV, err := store.GetCurrentResourceVersion(ctx) 	if err != nil { 		panic(err) 	}-	rv := max(int64(currentRV)+offset, 1)-	return correctness.WatchRequest{ResourceVersion: strconv.FormatInt(rv, 10)}+	return strconv.FormatInt(max(int64(currentRV)+offset, 1), 10) }  func runWatch(ctx context.Context, store storage.Interface, req correctness.WatchRequest, cfg WatchConfig) correctness.WatchResponse {-	// Without SendInitialEvents=false, ResourceVersion="0" or "" causes storage-	// to emit synthetic ADDED events for existing objects at their current RV.-	sendInitialEvents := false-	w, err := store.Watch(ctx, "/pods/", storage.ListOptions{-		ResourceVersion:   req.ResourceVersion,-		Predicate:         storage.Everything,-		Recursive:         true,-		SendInitialEvents: &sendInitialEvents,-	})+	w, err := store.Watch(ctx, req.Key, req.Options) 	if err != nil { 		if _, ok := errors.AsType[*storage.StorageError](err); ok { 			return correctness.WatchResponse{Err: err}