mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-22 11:21:47 +00:00
Add SendInitialEvents parameter to ListOption
This commit is contained in:
parent
b99fe0d5b9
commit
2ae115e5a2
@ -66,6 +66,31 @@ type ListOptions struct {
|
||||
// it does not recognize and will return a 410 error if the token can no longer be used because
|
||||
// it has expired.
|
||||
Continue string
|
||||
|
||||
// `sendInitialEvents=true` may be set together with `watch=true`.
|
||||
// In that case, the watch stream will begin with synthetic events to
|
||||
// produce the current state of objects in the collection. Once all such
|
||||
// events have been sent, a synthetic "Bookmark" event will be sent.
|
||||
// The bookmark will report the ResourceVersion (RV) corresponding to the
|
||||
// set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation.
|
||||
// Afterwards, the watch stream will proceed as usual, sending watch events
|
||||
// corresponding to changes (subsequent to the RV) to objects watched.
|
||||
//
|
||||
// When `sendInitialEvents` option is set, we require `resourceVersionMatch`
|
||||
// option to also be set. The semantic of the watch request is as following:
|
||||
// - `resourceVersionMatch` = NotOlderThan
|
||||
// is interpreted as "data at least as new as the provided `resourceVersion`"
|
||||
// and the bookmark event is send when the state is synced
|
||||
// to a `resourceVersion` at least as fresh as the one provided by the ListOptions.
|
||||
// If `resourceVersion` is unset, this is interpreted as "consistent read" and the
|
||||
// bookmark event is send when the state is synced at least to the moment
|
||||
// when request started being processed.
|
||||
// - `resourceVersionMatch` set to any other value or unset
|
||||
// Invalid error is returned.
|
||||
//
|
||||
// Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward
|
||||
// compatibility reasons) and to false otherwise.
|
||||
SendInitialEvents *bool
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
@ -17,18 +17,20 @@ limitations under the License.
|
||||
package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// ValidateListOptions returns all validation errors found while validating the ListOptions.
|
||||
func ValidateListOptions(options *internalversion.ListOptions) field.ErrorList {
|
||||
func ValidateListOptions(options *internalversion.ListOptions, isWatchListFeatureEnabled bool) field.ErrorList {
|
||||
if options.Watch {
|
||||
return validateWatchOptions(options, isWatchListFeatureEnabled)
|
||||
}
|
||||
allErrs := field.ErrorList{}
|
||||
if match := options.ResourceVersionMatch; len(match) > 0 {
|
||||
if options.Watch {
|
||||
allErrs = append(allErrs, field.Forbidden(field.NewPath("resourceVersionMatch"), "resourceVersionMatch is forbidden for watch"))
|
||||
}
|
||||
if len(options.ResourceVersion) == 0 {
|
||||
allErrs = append(allErrs, field.Forbidden(field.NewPath("resourceVersionMatch"), "resourceVersionMatch is forbidden unless resourceVersion is provided"))
|
||||
}
|
||||
@ -42,5 +44,33 @@ func ValidateListOptions(options *internalversion.ListOptions) field.ErrorList {
|
||||
allErrs = append(allErrs, field.Forbidden(field.NewPath("resourceVersionMatch"), "resourceVersionMatch \"exact\" is forbidden for resourceVersion \"0\""))
|
||||
}
|
||||
}
|
||||
if options.SendInitialEvents != nil {
|
||||
allErrs = append(allErrs, field.Forbidden(field.NewPath("sendInitialEvents"), "sendInitialEvents is forbidden for list"))
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func validateWatchOptions(options *internalversion.ListOptions, isWatchListFeatureEnabled bool) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
match := options.ResourceVersionMatch
|
||||
if options.SendInitialEvents != nil {
|
||||
if match != metav1.ResourceVersionMatchNotOlderThan {
|
||||
allErrs = append(allErrs, field.Forbidden(field.NewPath("resourceVersionMatch"), fmt.Sprintf("sendInitialEvents requires setting resourceVersionMatch to %s", metav1.ResourceVersionMatchNotOlderThan)))
|
||||
}
|
||||
if !isWatchListFeatureEnabled {
|
||||
allErrs = append(allErrs, field.Forbidden(field.NewPath("sendInitialEvents"), "sendInitialEvents is forbidden for watch unless the WatchList feature gate is enabled"))
|
||||
}
|
||||
}
|
||||
if len(match) > 0 {
|
||||
if options.SendInitialEvents == nil {
|
||||
allErrs = append(allErrs, field.Forbidden(field.NewPath("resourceVersionMatch"), "resourceVersionMatch is forbidden for watch unless sendInitialEvents is provided"))
|
||||
}
|
||||
if match != metav1.ResourceVersionMatchNotOlderThan {
|
||||
allErrs = append(allErrs, field.NotSupported(field.NewPath("resourceVersionMatch"), match, []string{string(metav1.ResourceVersionMatchNotOlderThan)}))
|
||||
}
|
||||
if len(options.Continue) > 0 {
|
||||
allErrs = append(allErrs, field.Forbidden(field.NewPath("resourceVersionMatch"), "resourceVersionMatch is forbidden when continue is provided"))
|
||||
}
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
@ -23,10 +23,15 @@ import (
|
||||
)
|
||||
|
||||
func TestValidateListOptions(t *testing.T) {
|
||||
boolPtrFn := func(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
opts internalversion.ListOptions
|
||||
expectError string
|
||||
watchListFeatureEnabled bool
|
||||
expectErrors []string
|
||||
}{
|
||||
{
|
||||
name: "valid-default",
|
||||
@ -45,7 +50,7 @@ func TestValidateListOptions(t *testing.T) {
|
||||
ResourceVersion: "0",
|
||||
ResourceVersionMatch: metav1.ResourceVersionMatchExact,
|
||||
},
|
||||
expectError: "resourceVersionMatch: Forbidden: resourceVersionMatch \"exact\" is forbidden for resourceVersion \"0\"",
|
||||
expectErrors: []string{"resourceVersionMatch: Forbidden: resourceVersionMatch \"exact\" is forbidden for resourceVersion \"0\""},
|
||||
},
|
||||
{
|
||||
name: "valid-resourceversionmatch-notolderthan",
|
||||
@ -60,18 +65,144 @@ func TestValidateListOptions(t *testing.T) {
|
||||
ResourceVersion: "0",
|
||||
ResourceVersionMatch: "foo",
|
||||
},
|
||||
expectError: "resourceVersionMatch: Unsupported value: \"foo\": supported values: \"Exact\", \"NotOlderThan\", \"\"",
|
||||
expectErrors: []string{"resourceVersionMatch: Unsupported value: \"foo\": supported values: \"Exact\", \"NotOlderThan\", \"\""},
|
||||
},
|
||||
{
|
||||
name: "list-sendInitialEvents-forbidden",
|
||||
opts: internalversion.ListOptions{
|
||||
SendInitialEvents: boolPtrFn(true),
|
||||
},
|
||||
expectErrors: []string{"sendInitialEvents: Forbidden: sendInitialEvents is forbidden for list"},
|
||||
},
|
||||
{
|
||||
name: "valid-watch-default",
|
||||
opts: internalversion.ListOptions{
|
||||
Watch: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid-watch-sendInitialEvents-on",
|
||||
opts: internalversion.ListOptions{
|
||||
Watch: true,
|
||||
SendInitialEvents: boolPtrFn(true),
|
||||
ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan,
|
||||
AllowWatchBookmarks: true,
|
||||
},
|
||||
watchListFeatureEnabled: true,
|
||||
},
|
||||
{
|
||||
name: "valid-watch-sendInitialEvents-off",
|
||||
opts: internalversion.ListOptions{
|
||||
Watch: true,
|
||||
SendInitialEvents: boolPtrFn(false),
|
||||
ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan,
|
||||
AllowWatchBookmarks: true,
|
||||
},
|
||||
watchListFeatureEnabled: true,
|
||||
},
|
||||
{
|
||||
name: "watch-resourceversionmatch-without-sendInitialEvents-forbidden",
|
||||
opts: internalversion.ListOptions{
|
||||
Watch: true,
|
||||
ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan,
|
||||
},
|
||||
expectErrors: []string{"resourceVersionMatch: Forbidden: resourceVersionMatch is forbidden for watch unless sendInitialEvents is provided"},
|
||||
},
|
||||
{
|
||||
name: "watch-sendInitialEvents-without-resourceversionmatch-forbidden",
|
||||
opts: internalversion.ListOptions{
|
||||
Watch: true,
|
||||
SendInitialEvents: boolPtrFn(true),
|
||||
},
|
||||
expectErrors: []string{"resourceVersionMatch: Forbidden: sendInitialEvents requires setting resourceVersionMatch to NotOlderThan", "sendInitialEvents: Forbidden: sendInitialEvents is forbidden for watch unless the WatchList feature gate is enabled"},
|
||||
},
|
||||
{
|
||||
name: "watch-sendInitialEvents-with-exact-resourceversionmatch-forbidden",
|
||||
opts: internalversion.ListOptions{
|
||||
Watch: true,
|
||||
SendInitialEvents: boolPtrFn(true),
|
||||
ResourceVersionMatch: metav1.ResourceVersionMatchExact,
|
||||
AllowWatchBookmarks: true,
|
||||
},
|
||||
watchListFeatureEnabled: true,
|
||||
expectErrors: []string{"resourceVersionMatch: Forbidden: sendInitialEvents requires setting resourceVersionMatch to NotOlderThan", "resourceVersionMatch: Unsupported value: \"Exact\": supported values: \"NotOlderThan\""},
|
||||
},
|
||||
{
|
||||
name: "watch-sendInitialEvents-on-with-empty-resourceversionmatch-forbidden",
|
||||
opts: internalversion.ListOptions{
|
||||
Watch: true,
|
||||
SendInitialEvents: boolPtrFn(true),
|
||||
ResourceVersionMatch: "",
|
||||
},
|
||||
expectErrors: []string{"resourceVersionMatch: Forbidden: sendInitialEvents requires setting resourceVersionMatch to NotOlderThan", "sendInitialEvents: Forbidden: sendInitialEvents is forbidden for watch unless the WatchList feature gate is enabled"},
|
||||
},
|
||||
{
|
||||
name: "watch-sendInitialEvents-off-with-empty-resourceversionmatch-forbidden",
|
||||
opts: internalversion.ListOptions{
|
||||
Watch: true,
|
||||
SendInitialEvents: boolPtrFn(false),
|
||||
ResourceVersionMatch: "",
|
||||
},
|
||||
expectErrors: []string{"resourceVersionMatch: Forbidden: sendInitialEvents requires setting resourceVersionMatch to NotOlderThan", "sendInitialEvents: Forbidden: sendInitialEvents is forbidden for watch unless the WatchList feature gate is enabled"},
|
||||
},
|
||||
{
|
||||
name: "watch-sendInitialEvents-with-incorrect-resourceversionmatch-forbidden",
|
||||
opts: internalversion.ListOptions{
|
||||
Watch: true,
|
||||
SendInitialEvents: boolPtrFn(true),
|
||||
ResourceVersionMatch: "incorrect",
|
||||
AllowWatchBookmarks: true,
|
||||
},
|
||||
watchListFeatureEnabled: true,
|
||||
expectErrors: []string{"resourceVersionMatch: Forbidden: sendInitialEvents requires setting resourceVersionMatch to NotOlderThan", "resourceVersionMatch: Unsupported value: \"incorrect\": supported values: \"NotOlderThan\""},
|
||||
},
|
||||
{
|
||||
// note that validating allowWatchBookmarks would break backward compatibility
|
||||
// because it was possible to request initial events via resourceVersion=0 before this change
|
||||
name: "watch-sendInitialEvents-no-allowWatchBookmark",
|
||||
opts: internalversion.ListOptions{
|
||||
Watch: true,
|
||||
SendInitialEvents: boolPtrFn(true),
|
||||
ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan,
|
||||
},
|
||||
watchListFeatureEnabled: true,
|
||||
},
|
||||
{
|
||||
name: "watch-sendInitialEvents-no-watchlist-fg-disabled",
|
||||
opts: internalversion.ListOptions{
|
||||
Watch: true,
|
||||
SendInitialEvents: boolPtrFn(true),
|
||||
ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan,
|
||||
AllowWatchBookmarks: true,
|
||||
},
|
||||
expectErrors: []string{"sendInitialEvents: Forbidden: sendInitialEvents is forbidden for watch unless the WatchList feature gate is enabled"},
|
||||
},
|
||||
{
|
||||
name: "watch-sendInitialEvents-no-watchlist-fg-disabled",
|
||||
opts: internalversion.ListOptions{
|
||||
Watch: true,
|
||||
SendInitialEvents: boolPtrFn(true),
|
||||
ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan,
|
||||
AllowWatchBookmarks: true,
|
||||
Continue: "123",
|
||||
},
|
||||
watchListFeatureEnabled: true,
|
||||
expectErrors: []string{"resourceVersionMatch: Forbidden: resourceVersionMatch is forbidden when continue is provided"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
errs := ValidateListOptions(&tc.opts)
|
||||
if tc.expectError != "" {
|
||||
if len(errs) != 1 {
|
||||
t.Errorf("expected an error but got %d errors", len(errs))
|
||||
} else if errs[0].Error() != tc.expectError {
|
||||
t.Errorf("expected error '%s' but got '%s'", tc.expectError, errs[0].Error())
|
||||
errs := ValidateListOptions(&tc.opts, tc.watchListFeatureEnabled)
|
||||
if len(tc.expectErrors) > 0 {
|
||||
if len(errs) != len(tc.expectErrors) {
|
||||
t.Errorf("expected %d errors but got %d errors", len(tc.expectErrors), len(errs))
|
||||
return
|
||||
}
|
||||
for i, expectedErr := range tc.expectErrors {
|
||||
if expectedErr != errs[i].Error() {
|
||||
t.Errorf("expected error '%s' but got '%s'", expectedErr, errs[i].Error())
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -400,6 +400,32 @@ type ListOptions struct {
|
||||
// This field is not supported when watch is true. Clients may start a watch from the last
|
||||
// resourceVersion value returned by the server and not miss any modifications.
|
||||
Continue string `json:"continue,omitempty" protobuf:"bytes,8,opt,name=continue"`
|
||||
|
||||
// `sendInitialEvents=true` may be set together with `watch=true`.
|
||||
// In that case, the watch stream will begin with synthetic events to
|
||||
// produce the current state of objects in the collection. Once all such
|
||||
// events have been sent, a synthetic "Bookmark" event will be sent.
|
||||
// The bookmark will report the ResourceVersion (RV) corresponding to the
|
||||
// set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation.
|
||||
// Afterwards, the watch stream will proceed as usual, sending watch events
|
||||
// corresponding to changes (subsequent to the RV) to objects watched.
|
||||
//
|
||||
// When `sendInitialEvents` option is set, we require `resourceVersionMatch`
|
||||
// option to also be set. The semantic of the watch request is as following:
|
||||
// - `resourceVersionMatch` = NotOlderThan
|
||||
// is interpreted as "data at least as new as the provided `resourceVersion`"
|
||||
// and the bookmark event is send when the state is synced
|
||||
// to a `resourceVersion` at least as fresh as the one provided by the ListOptions.
|
||||
// If `resourceVersion` is unset, this is interpreted as "consistent read" and the
|
||||
// bookmark event is send when the state is synced at least to the moment
|
||||
// when request started being processed.
|
||||
// - `resourceVersionMatch` set to any other value or unset
|
||||
// Invalid error is returned.
|
||||
//
|
||||
// Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward
|
||||
// compatibility reasons) and to false otherwise.
|
||||
// +optional
|
||||
SendInitialEvents *bool `json:"sendInitialEvents,omitempty" protobuf:"varint,11,opt,name=sendInitialEvents"`
|
||||
}
|
||||
|
||||
// resourceVersionMatch specifies how the resourceVersion parameter is applied. resourceVersionMatch
|
||||
|
Loading…
Reference in New Issue
Block a user