mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-04 01:40:07 +00:00
Server side implementation of paging for etcd3
Add a feature gate in the apiserver to control whether paging can be used. Add controls to the storage factory that allow it to be disabled per resource. Use a JSON encoded continuation token that can be versioned. Create a 410 error if the continuation token is expired. Adds GetContinue() to ListMeta.
This commit is contained in:
parent
500b130ff0
commit
8952a0cb72
@ -420,6 +420,7 @@ func TestGCListWatcher(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
lw := listWatcher(client, podResource)
|
lw := listWatcher(client, podResource)
|
||||||
|
lw.DisablePaging = true
|
||||||
if _, err := lw.Watch(metav1.ListOptions{ResourceVersion: "1"}); err != nil {
|
if _, err := lw.Watch(metav1.ListOptions{ResourceVersion: "1"}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -178,6 +178,7 @@ var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureS
|
|||||||
genericfeatures.AdvancedAuditing: {Default: false, PreRelease: utilfeature.Alpha},
|
genericfeatures.AdvancedAuditing: {Default: false, PreRelease: utilfeature.Alpha},
|
||||||
genericfeatures.APIResponseCompression: {Default: false, PreRelease: utilfeature.Alpha},
|
genericfeatures.APIResponseCompression: {Default: false, PreRelease: utilfeature.Alpha},
|
||||||
genericfeatures.Initializers: {Default: false, PreRelease: utilfeature.Alpha},
|
genericfeatures.Initializers: {Default: false, PreRelease: utilfeature.Alpha},
|
||||||
|
genericfeatures.APIListChunking: {Default: false, PreRelease: utilfeature.Alpha},
|
||||||
|
|
||||||
// inherited features from apiextensions-apiserver, relisted here to get a conflict if it is changed
|
// inherited features from apiextensions-apiserver, relisted here to get a conflict if it is changed
|
||||||
// unintentionally on either side:
|
// unintentionally on either side:
|
||||||
|
@ -174,6 +174,17 @@ func NewGone(message string) *StatusError {
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewResourceExpired creates an error that indicates that the requested resource content has expired from
|
||||||
|
// the server (usually due to a resourceVersion that is too old).
|
||||||
|
func NewResourceExpired(message string) *StatusError {
|
||||||
|
return &StatusError{metav1.Status{
|
||||||
|
Status: metav1.StatusFailure,
|
||||||
|
Code: http.StatusGone,
|
||||||
|
Reason: metav1.StatusReasonExpired,
|
||||||
|
Message: message,
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
// NewInvalid returns an error indicating the item is invalid and cannot be processed.
|
// NewInvalid returns an error indicating the item is invalid and cannot be processed.
|
||||||
func NewInvalid(qualifiedKind schema.GroupKind, name string, errs field.ErrorList) *StatusError {
|
func NewInvalid(qualifiedKind schema.GroupKind, name string, errs field.ErrorList) *StatusError {
|
||||||
causes := make([]metav1.StatusCause, 0, len(errs))
|
causes := make([]metav1.StatusCause, 0, len(errs))
|
||||||
@ -394,12 +405,28 @@ func IsInvalid(err error) bool {
|
|||||||
return reasonForError(err) == metav1.StatusReasonInvalid
|
return reasonForError(err) == metav1.StatusReasonInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsGone is true if the error indicates the requested resource is no longer available.
|
||||||
|
func IsGone(err error) bool {
|
||||||
|
return reasonForError(err) == metav1.StatusReasonGone
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsResourceExpired is true if the error indicates the resource has expired and the current action is
|
||||||
|
// no longer possible.
|
||||||
|
func IsResourceExpired(err error) bool {
|
||||||
|
return reasonForError(err) == metav1.StatusReasonExpired
|
||||||
|
}
|
||||||
|
|
||||||
// IsMethodNotSupported determines if the err is an error which indicates the provided action could not
|
// IsMethodNotSupported determines if the err is an error which indicates the provided action could not
|
||||||
// be performed because it is not supported by the server.
|
// be performed because it is not supported by the server.
|
||||||
func IsMethodNotSupported(err error) bool {
|
func IsMethodNotSupported(err error) bool {
|
||||||
return reasonForError(err) == metav1.StatusReasonMethodNotAllowed
|
return reasonForError(err) == metav1.StatusReasonMethodNotAllowed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsServiceUnavailable is true if the error indicates the underlying service is no longer available.
|
||||||
|
func IsServiceUnavailable(err error) bool {
|
||||||
|
return reasonForError(err) == metav1.StatusReasonServiceUnavailable
|
||||||
|
}
|
||||||
|
|
||||||
// IsBadRequest determines if err is an error which indicates that the request is invalid.
|
// IsBadRequest determines if err is an error which indicates that the request is invalid.
|
||||||
func IsBadRequest(err error) bool {
|
func IsBadRequest(err error) bool {
|
||||||
return reasonForError(err) == metav1.StatusReasonBadRequest
|
return reasonForError(err) == metav1.StatusReasonBadRequest
|
||||||
|
@ -90,6 +90,8 @@ type ListInterface interface {
|
|||||||
SetResourceVersion(version string)
|
SetResourceVersion(version string)
|
||||||
GetSelfLink() string
|
GetSelfLink() string
|
||||||
SetSelfLink(selfLink string)
|
SetSelfLink(selfLink string)
|
||||||
|
GetContinue() string
|
||||||
|
SetContinue(c string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type exposes the type and APIVersion of versioned or internal API objects.
|
// Type exposes the type and APIVersion of versioned or internal API objects.
|
||||||
@ -105,6 +107,8 @@ func (meta *ListMeta) GetResourceVersion() string { return meta.ResourceV
|
|||||||
func (meta *ListMeta) SetResourceVersion(version string) { meta.ResourceVersion = version }
|
func (meta *ListMeta) SetResourceVersion(version string) { meta.ResourceVersion = version }
|
||||||
func (meta *ListMeta) GetSelfLink() string { return meta.SelfLink }
|
func (meta *ListMeta) GetSelfLink() string { return meta.SelfLink }
|
||||||
func (meta *ListMeta) SetSelfLink(selfLink string) { meta.SelfLink = selfLink }
|
func (meta *ListMeta) SetSelfLink(selfLink string) { meta.SelfLink = selfLink }
|
||||||
|
func (meta *ListMeta) GetContinue() string { return meta.Continue }
|
||||||
|
func (meta *ListMeta) SetContinue(c string) { meta.Continue = c }
|
||||||
|
|
||||||
func (obj *TypeMeta) GetObjectKind() schema.ObjectKind { return obj }
|
func (obj *TypeMeta) GetObjectKind() schema.ObjectKind { return obj }
|
||||||
|
|
||||||
|
@ -468,6 +468,14 @@ func (u *Unstructured) SetSelfLink(selfLink string) {
|
|||||||
u.setNestedField(selfLink, "metadata", "selfLink")
|
u.setNestedField(selfLink, "metadata", "selfLink")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *Unstructured) GetContinue() string {
|
||||||
|
return getNestedString(u.Object, "metadata", "continue")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Unstructured) SetContinue(c string) {
|
||||||
|
u.setNestedField(c, "metadata", "continue")
|
||||||
|
}
|
||||||
|
|
||||||
func (u *Unstructured) GetCreationTimestamp() metav1.Time {
|
func (u *Unstructured) GetCreationTimestamp() metav1.Time {
|
||||||
var timestamp metav1.Time
|
var timestamp metav1.Time
|
||||||
timestamp.UnmarshalQueryParameter(getNestedString(u.Object, "metadata", "creationTimestamp"))
|
timestamp.UnmarshalQueryParameter(getNestedString(u.Object, "metadata", "creationTimestamp"))
|
||||||
@ -652,6 +660,14 @@ func (u *UnstructuredList) SetSelfLink(selfLink string) {
|
|||||||
u.setNestedField(selfLink, "metadata", "selfLink")
|
u.setNestedField(selfLink, "metadata", "selfLink")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *UnstructuredList) GetContinue() string {
|
||||||
|
return getNestedString(u.Object, "metadata", "continue")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UnstructuredList) SetContinue(c string) {
|
||||||
|
u.setNestedField(c, "metadata", "continue")
|
||||||
|
}
|
||||||
|
|
||||||
func (u *UnstructuredList) SetGroupVersionKind(gvk schema.GroupVersionKind) {
|
func (u *UnstructuredList) SetGroupVersionKind(gvk schema.GroupVersionKind) {
|
||||||
u.SetAPIVersion(gvk.GroupVersion().String())
|
u.SetAPIVersion(gvk.GroupVersion().String())
|
||||||
u.SetKind(gvk.Kind)
|
u.SetKind(gvk.Kind)
|
||||||
|
@ -199,6 +199,7 @@ type InternalTypeMeta struct {
|
|||||||
CreationTimestamp metav1.Time `json:"creationTimestamp,omitempty"`
|
CreationTimestamp metav1.Time `json:"creationTimestamp,omitempty"`
|
||||||
SelfLink string `json:"selfLink,omitempty"`
|
SelfLink string `json:"selfLink,omitempty"`
|
||||||
ResourceVersion string `json:"resourceVersion,omitempty"`
|
ResourceVersion string `json:"resourceVersion,omitempty"`
|
||||||
|
Continue string `json:"next,omitempty"`
|
||||||
APIVersion string `json:"apiVersion,omitempty"`
|
APIVersion string `json:"apiVersion,omitempty"`
|
||||||
Labels map[string]string `json:"labels,omitempty"`
|
Labels map[string]string `json:"labels,omitempty"`
|
||||||
Annotations map[string]string `json:"annotations,omitempty"`
|
Annotations map[string]string `json:"annotations,omitempty"`
|
||||||
@ -210,6 +211,8 @@ func (m *InternalTypeMeta) GetResourceVersion() string { return m.ResourceVers
|
|||||||
func (m *InternalTypeMeta) SetResourceVersion(rv string) { m.ResourceVersion = rv }
|
func (m *InternalTypeMeta) SetResourceVersion(rv string) { m.ResourceVersion = rv }
|
||||||
func (m *InternalTypeMeta) GetSelfLink() string { return m.SelfLink }
|
func (m *InternalTypeMeta) GetSelfLink() string { return m.SelfLink }
|
||||||
func (m *InternalTypeMeta) SetSelfLink(link string) { m.SelfLink = link }
|
func (m *InternalTypeMeta) SetSelfLink(link string) { m.SelfLink = link }
|
||||||
|
func (m *InternalTypeMeta) GetContinue() string { return m.Continue }
|
||||||
|
func (m *InternalTypeMeta) SetContinue(c string) { m.Continue = c }
|
||||||
|
|
||||||
type MyAPIObject struct {
|
type MyAPIObject struct {
|
||||||
TypeMeta InternalTypeMeta `json:",inline"`
|
TypeMeta InternalTypeMeta `json:",inline"`
|
||||||
|
@ -54,6 +54,13 @@ const (
|
|||||||
// Allow asynchronous coordination of object creation.
|
// Allow asynchronous coordination of object creation.
|
||||||
// Auto-enabled by the Initializers admission plugin.
|
// Auto-enabled by the Initializers admission plugin.
|
||||||
Initializers utilfeature.Feature = "Initializers"
|
Initializers utilfeature.Feature = "Initializers"
|
||||||
|
|
||||||
|
// owner: @smarterclayton
|
||||||
|
// alpha: v1.8
|
||||||
|
//
|
||||||
|
// Allow API clients to retrieve resource lists in chunks rather than
|
||||||
|
// all at once.
|
||||||
|
APIListChunking utilfeature.Feature = "APIListChunking"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -68,4 +75,5 @@ var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureS
|
|||||||
AdvancedAuditing: {Default: false, PreRelease: utilfeature.Alpha},
|
AdvancedAuditing: {Default: false, PreRelease: utilfeature.Alpha},
|
||||||
APIResponseCompression: {Default: false, PreRelease: utilfeature.Alpha},
|
APIResponseCompression: {Default: false, PreRelease: utilfeature.Alpha},
|
||||||
Initializers: {Default: false, PreRelease: utilfeature.Alpha},
|
Initializers: {Default: false, PreRelease: utilfeature.Alpha},
|
||||||
|
APIListChunking: {Default: false, PreRelease: utilfeature.Alpha},
|
||||||
}
|
}
|
||||||
|
@ -293,6 +293,8 @@ func (e *Store) ListPredicate(ctx genericapirequest.Context, p storage.Selection
|
|||||||
options = &metainternalversion.ListOptions{ResourceVersion: ""}
|
options = &metainternalversion.ListOptions{ResourceVersion: ""}
|
||||||
}
|
}
|
||||||
p.IncludeUninitialized = options.IncludeUninitialized
|
p.IncludeUninitialized = options.IncludeUninitialized
|
||||||
|
p.Limit = options.Limit
|
||||||
|
p.Continue = options.Continue
|
||||||
list := e.NewListFunc()
|
list := e.NewListFunc()
|
||||||
qualifiedResource := e.qualifiedResourceFromContext(ctx)
|
qualifiedResource := e.qualifiedResourceFromContext(ctx)
|
||||||
if name, ok := p.MatchesSingle(); ok {
|
if name, ok := p.MatchesSingle(); ok {
|
||||||
|
@ -34,6 +34,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type EtcdOptions struct {
|
type EtcdOptions struct {
|
||||||
|
// The value of Paging on StorageConfig will be overriden by the
|
||||||
|
// calculated feature gate value.
|
||||||
StorageConfig storagebackend.Config
|
StorageConfig storagebackend.Config
|
||||||
EncryptionProviderConfigFilepath string
|
EncryptionProviderConfigFilepath string
|
||||||
|
|
||||||
|
@ -43,8 +43,10 @@ go_library(
|
|||||||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer/recognizer:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer/recognizer:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
|
||||||
|
"//vendor/k8s.io/apiserver/pkg/features:go_default_library",
|
||||||
"//vendor/k8s.io/apiserver/pkg/storage/storagebackend:go_default_library",
|
"//vendor/k8s.io/apiserver/pkg/storage/storagebackend:go_default_library",
|
||||||
"//vendor/k8s.io/apiserver/pkg/storage/value:go_default_library",
|
"//vendor/k8s.io/apiserver/pkg/storage/value:go_default_library",
|
||||||
|
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -27,8 +27,10 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
"k8s.io/apiserver/pkg/features"
|
||||||
"k8s.io/apiserver/pkg/storage/storagebackend"
|
"k8s.io/apiserver/pkg/storage/storagebackend"
|
||||||
"k8s.io/apiserver/pkg/storage/value"
|
"k8s.io/apiserver/pkg/storage/value"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Backend describes the storage servers, the information here should be enough
|
// Backend describes the storage servers, the information here should be enough
|
||||||
@ -112,6 +114,8 @@ type groupResourceOverrides struct {
|
|||||||
decoderDecoratorFn func([]runtime.Decoder) []runtime.Decoder
|
decoderDecoratorFn func([]runtime.Decoder) []runtime.Decoder
|
||||||
// transformer is optional and shall encrypt that resource at rest.
|
// transformer is optional and shall encrypt that resource at rest.
|
||||||
transformer value.Transformer
|
transformer value.Transformer
|
||||||
|
// disablePaging will prevent paging on the provided resource.
|
||||||
|
disablePaging bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply overrides the provided config and options if the override has a value in that position
|
// Apply overrides the provided config and options if the override has a value in that position
|
||||||
@ -138,6 +142,9 @@ func (o groupResourceOverrides) Apply(config *storagebackend.Config, options *St
|
|||||||
if o.transformer != nil {
|
if o.transformer != nil {
|
||||||
config.Transformer = o.transformer
|
config.Transformer = o.transformer
|
||||||
}
|
}
|
||||||
|
if o.disablePaging {
|
||||||
|
config.Paging = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ StorageFactory = &DefaultStorageFactory{}
|
var _ StorageFactory = &DefaultStorageFactory{}
|
||||||
@ -157,6 +164,7 @@ var specialDefaultResourcePrefixes = map[schema.GroupResource]string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewDefaultStorageFactory(config storagebackend.Config, defaultMediaType string, defaultSerializer runtime.StorageSerializer, resourceEncodingConfig ResourceEncodingConfig, resourceConfig APIResourceConfigSource) *DefaultStorageFactory {
|
func NewDefaultStorageFactory(config storagebackend.Config, defaultMediaType string, defaultSerializer runtime.StorageSerializer, resourceEncodingConfig ResourceEncodingConfig, resourceConfig APIResourceConfigSource) *DefaultStorageFactory {
|
||||||
|
config.Paging = utilfeature.DefaultFeatureGate.Enabled(features.APIListChunking)
|
||||||
if len(defaultMediaType) == 0 {
|
if len(defaultMediaType) == 0 {
|
||||||
defaultMediaType = runtime.ContentTypeJSON
|
defaultMediaType = runtime.ContentTypeJSON
|
||||||
}
|
}
|
||||||
@ -185,6 +193,14 @@ func (s *DefaultStorageFactory) SetEtcdPrefix(groupResource schema.GroupResource
|
|||||||
s.Overrides[groupResource] = overrides
|
s.Overrides[groupResource] = overrides
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetDisableAPIListChunking allows a specific resource to disable paging at the storage layer, to prevent
|
||||||
|
// exposure of key names in continuations. This may be overriden by feature gates.
|
||||||
|
func (s *DefaultStorageFactory) SetDisableAPIListChunking(groupResource schema.GroupResource) {
|
||||||
|
overrides := s.Overrides[groupResource]
|
||||||
|
overrides.disablePaging = true
|
||||||
|
s.Overrides[groupResource] = overrides
|
||||||
|
}
|
||||||
|
|
||||||
// SetResourceEtcdPrefix sets the prefix for a resource, but not the base-dir. You'll end up in `etcdPrefix/resourceEtcdPrefix`.
|
// SetResourceEtcdPrefix sets the prefix for a resource, but not the base-dir. You'll end up in `etcdPrefix/resourceEtcdPrefix`.
|
||||||
func (s *DefaultStorageFactory) SetResourceEtcdPrefix(groupResource schema.GroupResource, prefix string) {
|
func (s *DefaultStorageFactory) SetResourceEtcdPrefix(groupResource schema.GroupResource, prefix string) {
|
||||||
overrides := s.Overrides[groupResource]
|
overrides := s.Overrides[groupResource]
|
||||||
|
@ -64,6 +64,8 @@ go_library(
|
|||||||
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
|
||||||
|
"//vendor/k8s.io/apiserver/pkg/features:go_default_library",
|
||||||
|
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||||
"//vendor/k8s.io/apiserver/pkg/util/trace:go_default_library",
|
"//vendor/k8s.io/apiserver/pkg/util/trace:go_default_library",
|
||||||
"//vendor/k8s.io/client-go/tools/cache:go_default_library",
|
"//vendor/k8s.io/client-go/tools/cache:go_default_library",
|
||||||
],
|
],
|
||||||
|
@ -37,6 +37,8 @@ import (
|
|||||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
"k8s.io/apimachinery/pkg/watch"
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
|
"k8s.io/apiserver/pkg/features"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
utiltrace "k8s.io/apiserver/pkg/util/trace"
|
utiltrace "k8s.io/apiserver/pkg/util/trace"
|
||||||
"k8s.io/client-go/tools/cache"
|
"k8s.io/client-go/tools/cache"
|
||||||
)
|
)
|
||||||
@ -406,9 +408,11 @@ func (c *Cacher) Get(ctx context.Context, key string, resourceVersion string, ob
|
|||||||
|
|
||||||
// Implements storage.Interface.
|
// Implements storage.Interface.
|
||||||
func (c *Cacher) GetToList(ctx context.Context, key string, resourceVersion string, pred SelectionPredicate, listObj runtime.Object) error {
|
func (c *Cacher) GetToList(ctx context.Context, key string, resourceVersion string, pred SelectionPredicate, listObj runtime.Object) error {
|
||||||
if resourceVersion == "" {
|
pagingEnabled := utilfeature.DefaultFeatureGate.Enabled(features.APIListChunking)
|
||||||
|
if resourceVersion == "" || (pagingEnabled && (len(pred.Continue) > 0 || pred.Limit > 0)) {
|
||||||
// If resourceVersion is not specified, serve it from underlying
|
// If resourceVersion is not specified, serve it from underlying
|
||||||
// storage (for backward compatibility).
|
// storage (for backward compatibility). If a continuation or limit is
|
||||||
|
// requested, serve it from the underlying storage as well.
|
||||||
return c.storage.GetToList(ctx, key, resourceVersion, pred, listObj)
|
return c.storage.GetToList(ctx, key, resourceVersion, pred, listObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -459,7 +463,7 @@ func (c *Cacher) GetToList(ctx context.Context, key string, resourceVersion stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if c.versioner != nil {
|
if c.versioner != nil {
|
||||||
if err := c.versioner.UpdateList(listObj, readResourceVersion); err != nil {
|
if err := c.versioner.UpdateList(listObj, readResourceVersion, ""); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -468,9 +472,11 @@ func (c *Cacher) GetToList(ctx context.Context, key string, resourceVersion stri
|
|||||||
|
|
||||||
// Implements storage.Interface.
|
// Implements storage.Interface.
|
||||||
func (c *Cacher) List(ctx context.Context, key string, resourceVersion string, pred SelectionPredicate, listObj runtime.Object) error {
|
func (c *Cacher) List(ctx context.Context, key string, resourceVersion string, pred SelectionPredicate, listObj runtime.Object) error {
|
||||||
if resourceVersion == "" {
|
pagingEnabled := utilfeature.DefaultFeatureGate.Enabled(features.APIListChunking)
|
||||||
|
if resourceVersion == "" || (pagingEnabled && (len(pred.Continue) > 0 || pred.Limit > 0)) {
|
||||||
// If resourceVersion is not specified, serve it from underlying
|
// If resourceVersion is not specified, serve it from underlying
|
||||||
// storage (for backward compatibility).
|
// storage (for backward compatibility). If a continuation or limit is
|
||||||
|
// requested, serve it from the underlying storage as well.
|
||||||
return c.storage.List(ctx, key, resourceVersion, pred, listObj)
|
return c.storage.List(ctx, key, resourceVersion, pred, listObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -527,7 +533,7 @@ func (c *Cacher) List(ctx context.Context, key string, resourceVersion string, p
|
|||||||
}
|
}
|
||||||
trace.Step(fmt.Sprintf("Filtered %d items", listVal.Len()))
|
trace.Step(fmt.Sprintf("Filtered %d items", listVal.Len()))
|
||||||
if c.versioner != nil {
|
if c.versioner != nil {
|
||||||
if err := c.versioner.UpdateList(listObj, readResourceVersion); err != nil {
|
if err := c.versioner.UpdateList(listObj, readResourceVersion, ""); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ func (a APIObjectVersioner) UpdateObject(obj runtime.Object, resourceVersion uin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateList implements Versioner
|
// UpdateList implements Versioner
|
||||||
func (a APIObjectVersioner) UpdateList(obj runtime.Object, resourceVersion uint64) error {
|
func (a APIObjectVersioner) UpdateList(obj runtime.Object, resourceVersion uint64, nextKey string) error {
|
||||||
listAccessor, err := meta.ListAccessor(obj)
|
listAccessor, err := meta.ListAccessor(obj)
|
||||||
if err != nil || listAccessor == nil {
|
if err != nil || listAccessor == nil {
|
||||||
return err
|
return err
|
||||||
@ -53,6 +53,7 @@ func (a APIObjectVersioner) UpdateList(obj runtime.Object, resourceVersion uint6
|
|||||||
versionString = strconv.FormatUint(resourceVersion, 10)
|
versionString = strconv.FormatUint(resourceVersion, 10)
|
||||||
}
|
}
|
||||||
listAccessor.SetResourceVersion(versionString)
|
listAccessor.SetResourceVersion(versionString)
|
||||||
|
listAccessor.SetContinue(nextKey)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -365,7 +365,7 @@ func (h *etcdHelper) GetToList(ctx context.Context, key string, resourceVersion
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
trace.Step("Object decoded")
|
trace.Step("Object decoded")
|
||||||
if err := h.versioner.UpdateList(listObj, response.Index); err != nil {
|
if err := h.versioner.UpdateList(listObj, response.Index, ""); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -445,7 +445,7 @@ func (h *etcdHelper) List(ctx context.Context, key string, resourceVersion strin
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
trace.Step("Node list decoded")
|
trace.Step("Node list decoded")
|
||||||
if err := h.versioner.UpdateList(listObj, index); err != nil {
|
if err := h.versioner.UpdateList(listObj, index, ""); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -322,6 +322,7 @@ func NewUnsecuredEtcd3TestClientServer(t *testing.T, scheme *runtime.Scheme) (*E
|
|||||||
ServerList: server.V3Client.Endpoints(),
|
ServerList: server.V3Client.Endpoints(),
|
||||||
DeserializationCacheSize: etcdtest.DeserializationCacheSize,
|
DeserializationCacheSize: etcdtest.DeserializationCacheSize,
|
||||||
Copier: scheme,
|
Copier: scheme,
|
||||||
|
Paging: true,
|
||||||
}
|
}
|
||||||
return server, config
|
return server, config
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,7 @@ go_library(
|
|||||||
name = "go_default_library",
|
name = "go_default_library",
|
||||||
srcs = [
|
srcs = [
|
||||||
"compact.go",
|
"compact.go",
|
||||||
|
"errors.go",
|
||||||
"event.go",
|
"event.go",
|
||||||
"store.go",
|
"store.go",
|
||||||
"watcher.go",
|
"watcher.go",
|
||||||
@ -51,8 +52,8 @@ go_library(
|
|||||||
"//vendor/github.com/coreos/etcd/mvcc/mvccpb:go_default_library",
|
"//vendor/github.com/coreos/etcd/mvcc/mvccpb:go_default_library",
|
||||||
"//vendor/github.com/golang/glog:go_default_library",
|
"//vendor/github.com/golang/glog:go_default_library",
|
||||||
"//vendor/golang.org/x/net/context:go_default_library",
|
"//vendor/golang.org/x/net/context:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
|
||||||
"//vendor/k8s.io/apimachinery/pkg/conversion:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/conversion:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
|
||||||
|
42
staging/src/k8s.io/apiserver/pkg/storage/etcd3/errors.go
Normal file
42
staging/src/k8s.io/apiserver/pkg/storage/etcd3/errors.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package etcd3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
|
||||||
|
etcdrpc "github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func interpretWatchError(err error) error {
|
||||||
|
switch {
|
||||||
|
case err == etcdrpc.ErrCompacted:
|
||||||
|
return errors.NewResourceExpired("The resourceVersion for the provided watch is too old.")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func interpretListError(err error, paging bool) error {
|
||||||
|
switch {
|
||||||
|
case err == etcdrpc.ErrCompacted:
|
||||||
|
if paging {
|
||||||
|
return errors.NewResourceExpired("The provided from parameter is too old to display a consistent list result. You must start a new list without the from.")
|
||||||
|
}
|
||||||
|
return errors.NewResourceExpired("The resourceVersion for the provided list is too old.")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
@ -18,10 +18,13 @@ package etcd3
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -60,12 +63,13 @@ type store struct {
|
|||||||
client *clientv3.Client
|
client *clientv3.Client
|
||||||
// getOpts contains additional options that should be passed
|
// getOpts contains additional options that should be passed
|
||||||
// to all Get() calls.
|
// to all Get() calls.
|
||||||
getOps []clientv3.OpOption
|
getOps []clientv3.OpOption
|
||||||
codec runtime.Codec
|
codec runtime.Codec
|
||||||
versioner storage.Versioner
|
versioner storage.Versioner
|
||||||
transformer value.Transformer
|
transformer value.Transformer
|
||||||
pathPrefix string
|
pathPrefix string
|
||||||
watcher *watcher
|
watcher *watcher
|
||||||
|
pagingEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type elemForDecode struct {
|
type elemForDecode struct {
|
||||||
@ -82,23 +86,24 @@ type objState struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New returns an etcd3 implementation of storage.Interface.
|
// New returns an etcd3 implementation of storage.Interface.
|
||||||
func New(c *clientv3.Client, codec runtime.Codec, prefix string, transformer value.Transformer) storage.Interface {
|
func New(c *clientv3.Client, codec runtime.Codec, prefix string, transformer value.Transformer, pagingEnabled bool) storage.Interface {
|
||||||
return newStore(c, true, codec, prefix, transformer)
|
return newStore(c, true, pagingEnabled, codec, prefix, transformer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWithNoQuorumRead returns etcd3 implementation of storage.Interface
|
// NewWithNoQuorumRead returns etcd3 implementation of storage.Interface
|
||||||
// where Get operations don't require quorum read.
|
// where Get operations don't require quorum read.
|
||||||
func NewWithNoQuorumRead(c *clientv3.Client, codec runtime.Codec, prefix string, transformer value.Transformer) storage.Interface {
|
func NewWithNoQuorumRead(c *clientv3.Client, codec runtime.Codec, prefix string, transformer value.Transformer, pagingEnabled bool) storage.Interface {
|
||||||
return newStore(c, false, codec, prefix, transformer)
|
return newStore(c, false, pagingEnabled, codec, prefix, transformer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newStore(c *clientv3.Client, quorumRead bool, codec runtime.Codec, prefix string, transformer value.Transformer) *store {
|
func newStore(c *clientv3.Client, quorumRead, pagingEnabled bool, codec runtime.Codec, prefix string, transformer value.Transformer) *store {
|
||||||
versioner := etcd.APIObjectVersioner{}
|
versioner := etcd.APIObjectVersioner{}
|
||||||
result := &store{
|
result := &store{
|
||||||
client: c,
|
client: c,
|
||||||
codec: codec,
|
codec: codec,
|
||||||
versioner: versioner,
|
versioner: versioner,
|
||||||
transformer: transformer,
|
transformer: transformer,
|
||||||
|
pagingEnabled: pagingEnabled,
|
||||||
// for compatibility with etcd2 impl.
|
// for compatibility with etcd2 impl.
|
||||||
// no-op for default prefix of '/registry'.
|
// no-op for default prefix of '/registry'.
|
||||||
// keeps compatibility with etcd2 impl for custom prefixes that don't start with '/'
|
// keeps compatibility with etcd2 impl for custom prefixes that don't start with '/'
|
||||||
@ -386,7 +391,66 @@ func (s *store) GetToList(ctx context.Context, key string, resourceVersion strin
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// update version with cluster level revision
|
// update version with cluster level revision
|
||||||
return s.versioner.UpdateList(listObj, uint64(getResp.Header.Revision))
|
return s.versioner.UpdateList(listObj, uint64(getResp.Header.Revision), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// continueToken is a simple structured object for encoding the state of a continue token.
|
||||||
|
// TODO: if we change the version of the encoded from, we can't start encoding the new version
|
||||||
|
// until all other servers are upgraded (i.e. we need to support rolling schema)
|
||||||
|
// This is a public API struct and cannot change.
|
||||||
|
type continueToken struct {
|
||||||
|
APIVersion string `json:"v"`
|
||||||
|
ResourceVersion int64 `json:"rv"`
|
||||||
|
StartKey string `json:"start"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseFrom transforms an encoded predicate from into a versioned struct.
|
||||||
|
// TODO: return a typed error that instructs clients that they must relist
|
||||||
|
func decodeContinue(continueValue, keyPrefix string) (fromKey string, rv int64, err error) {
|
||||||
|
data, err := base64.RawURLEncoding.DecodeString(continueValue)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, fmt.Errorf("continue key is not valid: %v", err)
|
||||||
|
}
|
||||||
|
var c continueToken
|
||||||
|
if err := json.Unmarshal(data, &c); err != nil {
|
||||||
|
return "", 0, fmt.Errorf("continue key is not valid: %v", err)
|
||||||
|
}
|
||||||
|
switch c.APIVersion {
|
||||||
|
case "v1alpha1":
|
||||||
|
if c.ResourceVersion == 0 {
|
||||||
|
return "", 0, fmt.Errorf("continue key is not valid: incorrect encoded start resourceVersion (version v1alpha1)")
|
||||||
|
}
|
||||||
|
if len(c.StartKey) == 0 {
|
||||||
|
return "", 0, fmt.Errorf("continue key is not valid: encoded start key empty (version v1alpha1)")
|
||||||
|
}
|
||||||
|
// defend against path traversal attacks by clients - path.Clean will ensure that startKey cannot
|
||||||
|
// be at a higher level of the hierarchy, and so when we append the key prefix we will end up with
|
||||||
|
// continue start key that is fully qualified and cannot range over anything less specific than
|
||||||
|
// keyPrefix.
|
||||||
|
cleaned := path.Clean(c.StartKey)
|
||||||
|
if cleaned != c.StartKey || cleaned == "." || cleaned == "/" {
|
||||||
|
return "", 0, fmt.Errorf("continue key is not valid: %s", cleaned)
|
||||||
|
}
|
||||||
|
if len(cleaned) == 0 {
|
||||||
|
return "", 0, fmt.Errorf("continue key is not valid: encoded start key empty (version 0)")
|
||||||
|
}
|
||||||
|
return keyPrefix + cleaned, c.ResourceVersion, nil
|
||||||
|
default:
|
||||||
|
return "", 0, fmt.Errorf("continue key is not valid: server does not recognize this encoded version %q", c.APIVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeContinue returns a string representing the encoded continuation of the current query.
|
||||||
|
func encodeContinue(key, keyPrefix string, resourceVersion int64) (string, error) {
|
||||||
|
nextKey := strings.TrimPrefix(key, keyPrefix)
|
||||||
|
if nextKey == key {
|
||||||
|
return "", fmt.Errorf("unable to encode next field: the key and key prefix do not match")
|
||||||
|
}
|
||||||
|
out, err := json.Marshal(&continueToken{APIVersion: "v1alpha1", ResourceVersion: resourceVersion, StartKey: nextKey})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.RawURLEncoding.EncodeToString(out), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// List implements storage.Interface.List.
|
// List implements storage.Interface.List.
|
||||||
@ -402,16 +466,50 @@ func (s *store) List(ctx context.Context, key, resourceVersion string, pred stor
|
|||||||
if !strings.HasSuffix(key, "/") {
|
if !strings.HasSuffix(key, "/") {
|
||||||
key += "/"
|
key += "/"
|
||||||
}
|
}
|
||||||
getResp, err := s.client.KV.Get(ctx, key, clientv3.WithPrefix())
|
keyPrefix := key
|
||||||
|
|
||||||
|
// set the appropriate clientv3 options to filter the returned data set
|
||||||
|
options := make([]clientv3.OpOption, 0, 4)
|
||||||
|
if s.pagingEnabled && pred.Limit > 0 {
|
||||||
|
options = append(options, clientv3.WithLimit(pred.Limit))
|
||||||
|
}
|
||||||
|
var returnedRV int64
|
||||||
|
switch {
|
||||||
|
case s.pagingEnabled && len(pred.Continue) > 0:
|
||||||
|
continueKey, continueRV, err := decodeContinue(pred.Continue, keyPrefix)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
options = append(options, clientv3.WithRange(clientv3.GetPrefixRangeEnd(key)))
|
||||||
|
key = continueKey
|
||||||
|
|
||||||
|
options = append(options, clientv3.WithRev(continueRV))
|
||||||
|
returnedRV = continueRV
|
||||||
|
|
||||||
|
case len(resourceVersion) > 0:
|
||||||
|
fromRV, err := strconv.ParseInt(resourceVersion, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid resource version: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
options = append(options, clientv3.WithPrefix(), clientv3.WithRev(fromRV))
|
||||||
|
returnedRV = fromRV
|
||||||
|
|
||||||
|
default:
|
||||||
|
options = append(options, clientv3.WithPrefix())
|
||||||
|
}
|
||||||
|
|
||||||
|
getResp, err := s.client.KV.Get(ctx, key, options...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return interpretListError(err, len(pred.Continue) > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
elems := make([]*elemForDecode, 0, len(getResp.Kvs))
|
elems := make([]*elemForDecode, 0, len(getResp.Kvs))
|
||||||
for _, kv := range getResp.Kvs {
|
for _, kv := range getResp.Kvs {
|
||||||
data, _, err := s.transformer.TransformFromStorage(kv.Value, authenticatedDataString(kv.Key))
|
data, _, err := s.transformer.TransformFromStorage(kv.Value, authenticatedDataString(kv.Key))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utilruntime.HandleError(fmt.Errorf("unable to transform key %q: %v", key, err))
|
utilruntime.HandleError(fmt.Errorf("unable to transform key %q: %v", kv.Key, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -420,11 +518,31 @@ func (s *store) List(ctx context.Context, key, resourceVersion string, pred stor
|
|||||||
rev: uint64(kv.ModRevision),
|
rev: uint64(kv.ModRevision),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := decodeList(elems, storage.SimpleFilter(pred), listPtr, s.codec, s.versioner); err != nil {
|
if err := decodeList(elems, storage.SimpleFilter(pred), listPtr, s.codec, s.versioner); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// update version with cluster level revision
|
|
||||||
return s.versioner.UpdateList(listObj, uint64(getResp.Header.Revision))
|
// indicate to the client which resource version was returned
|
||||||
|
if returnedRV == 0 {
|
||||||
|
returnedRV = getResp.Header.Revision
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case !getResp.More:
|
||||||
|
// no continuation
|
||||||
|
return s.versioner.UpdateList(listObj, uint64(returnedRV), "")
|
||||||
|
case len(getResp.Kvs) == 0:
|
||||||
|
return fmt.Errorf("no results were found, but etcd indicated there were more values")
|
||||||
|
default:
|
||||||
|
// we want to start immediately after the last key
|
||||||
|
// TODO: this reveals info about certain keys
|
||||||
|
key := string(getResp.Kvs[len(getResp.Kvs)-1].Key)
|
||||||
|
next, err := encodeContinue(key+"\x00", keyPrefix, returnedRV)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.versioner.UpdateList(listObj, uint64(returnedRV), next)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch implements storage.Interface.Watch.
|
// Watch implements storage.Interface.Watch.
|
||||||
@ -548,8 +666,8 @@ func decode(codec runtime.Codec, versioner storage.Versioner, value []byte, objP
|
|||||||
|
|
||||||
// decodeList decodes a list of values into a list of objects, with resource version set to corresponding rev.
|
// decodeList decodes a list of values into a list of objects, with resource version set to corresponding rev.
|
||||||
// On success, ListPtr would be set to the list of objects.
|
// On success, ListPtr would be set to the list of objects.
|
||||||
func decodeList(elems []*elemForDecode, filter storage.FilterFunc, ListPtr interface{}, codec runtime.Codec, versioner storage.Versioner) error {
|
func decodeList(elems []*elemForDecode, filter storage.FilterFunc, listPtr interface{}, codec runtime.Codec, versioner storage.Versioner) error {
|
||||||
v, err := conversion.EnforcePtr(ListPtr)
|
v, err := conversion.EnforcePtr(listPtr)
|
||||||
if err != nil || v.Kind() != reflect.Slice {
|
if err != nil || v.Kind() != reflect.Slice {
|
||||||
panic("need ptr to slice")
|
panic("need ptr to slice")
|
||||||
}
|
}
|
||||||
|
@ -18,12 +18,17 @@ package etcd3
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/etcd/clientv3"
|
||||||
|
"github.com/coreos/etcd/integration"
|
||||||
|
"golang.org/x/net/context"
|
||||||
apitesting "k8s.io/apimachinery/pkg/api/testing"
|
apitesting "k8s.io/apimachinery/pkg/api/testing"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/fields"
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
@ -37,10 +42,6 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/storage"
|
"k8s.io/apiserver/pkg/storage"
|
||||||
storagetests "k8s.io/apiserver/pkg/storage/tests"
|
storagetests "k8s.io/apiserver/pkg/storage/tests"
|
||||||
"k8s.io/apiserver/pkg/storage/value"
|
"k8s.io/apiserver/pkg/storage/value"
|
||||||
|
|
||||||
"github.com/coreos/etcd/clientv3"
|
|
||||||
"github.com/coreos/etcd/integration"
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var scheme = runtime.NewScheme()
|
var scheme = runtime.NewScheme()
|
||||||
@ -587,7 +588,7 @@ func TestTransformationFailure(t *testing.T) {
|
|||||||
codec := apitesting.TestCodec(codecs, examplev1.SchemeGroupVersion)
|
codec := apitesting.TestCodec(codecs, examplev1.SchemeGroupVersion)
|
||||||
cluster := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1})
|
cluster := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1})
|
||||||
defer cluster.Terminate(t)
|
defer cluster.Terminate(t)
|
||||||
store := newStore(cluster.RandClient(), false, codec, "", prefixTransformer{prefix: []byte(defaultTestPrefix)})
|
store := newStore(cluster.RandClient(), false, false, codec, "", prefixTransformer{prefix: []byte(defaultTestPrefix)})
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
preset := []struct {
|
preset := []struct {
|
||||||
@ -667,7 +668,8 @@ func TestList(t *testing.T) {
|
|||||||
codec := apitesting.TestCodec(codecs, examplev1.SchemeGroupVersion)
|
codec := apitesting.TestCodec(codecs, examplev1.SchemeGroupVersion)
|
||||||
cluster := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1})
|
cluster := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1})
|
||||||
defer cluster.Terminate(t)
|
defer cluster.Terminate(t)
|
||||||
store := newStore(cluster.RandClient(), false, codec, "", prefixTransformer{prefix: []byte(defaultTestPrefix)})
|
store := newStore(cluster.RandClient(), false, true, codec, "", prefixTransformer{prefix: []byte(defaultTestPrefix)})
|
||||||
|
disablePagingStore := newStore(cluster.RandClient(), false, false, codec, "", prefixTransformer{prefix: []byte(defaultTestPrefix)})
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Setup storage with the following structure:
|
// Setup storage with the following structure:
|
||||||
@ -704,40 +706,106 @@ func TestList(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
list := &example.PodList{}
|
||||||
|
store.List(ctx, "/two-level", "0", storage.Everything, list)
|
||||||
|
continueRV, _ := strconv.Atoi(list.ResourceVersion)
|
||||||
|
secondContinuation, err := encodeContinue("/two-level/2", "/two-level/", int64(continueRV))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
prefix string
|
disablePaging bool
|
||||||
pred storage.SelectionPredicate
|
prefix string
|
||||||
expectedOut []*example.Pod
|
pred storage.SelectionPredicate
|
||||||
}{{ // test List on existing key
|
expectedOut []*example.Pod
|
||||||
prefix: "/one-level/",
|
expectContinue bool
|
||||||
pred: storage.Everything,
|
}{
|
||||||
expectedOut: []*example.Pod{preset[0].storedObj},
|
{ // test List on existing key
|
||||||
}, { // test List on non-existing key
|
prefix: "/one-level/",
|
||||||
prefix: "/non-existing/",
|
pred: storage.Everything,
|
||||||
pred: storage.Everything,
|
expectedOut: []*example.Pod{preset[0].storedObj},
|
||||||
expectedOut: nil,
|
|
||||||
}, { // test List with pod name matching
|
|
||||||
prefix: "/one-level/",
|
|
||||||
pred: storage.SelectionPredicate{
|
|
||||||
Label: labels.Everything(),
|
|
||||||
Field: fields.ParseSelectorOrDie("metadata.name!=" + preset[0].storedObj.Name),
|
|
||||||
GetAttrs: func(obj runtime.Object) (labels.Set, fields.Set, bool, error) {
|
|
||||||
pod := obj.(*example.Pod)
|
|
||||||
return nil, fields.Set{"metadata.name": pod.Name}, pod.Initializers != nil, nil
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
expectedOut: nil,
|
{ // test List on non-existing key
|
||||||
}, { // test List with multiple levels of directories and expect flattened result
|
prefix: "/non-existing/",
|
||||||
prefix: "/two-level/",
|
pred: storage.Everything,
|
||||||
pred: storage.Everything,
|
expectedOut: nil,
|
||||||
expectedOut: []*example.Pod{preset[1].storedObj, preset[2].storedObj},
|
},
|
||||||
}}
|
{ // test List with pod name matching
|
||||||
|
prefix: "/one-level/",
|
||||||
|
pred: storage.SelectionPredicate{
|
||||||
|
Label: labels.Everything(),
|
||||||
|
Field: fields.ParseSelectorOrDie("metadata.name!=" + preset[0].storedObj.Name),
|
||||||
|
GetAttrs: func(obj runtime.Object) (labels.Set, fields.Set, bool, error) {
|
||||||
|
pod := obj.(*example.Pod)
|
||||||
|
return nil, fields.Set{"metadata.name": pod.Name}, pod.Initializers != nil, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedOut: nil,
|
||||||
|
},
|
||||||
|
{ // test List with limit
|
||||||
|
prefix: "/two-level/",
|
||||||
|
pred: storage.SelectionPredicate{
|
||||||
|
Label: labels.Everything(),
|
||||||
|
Field: fields.Everything(),
|
||||||
|
Limit: 1,
|
||||||
|
GetAttrs: func(obj runtime.Object) (labels.Set, fields.Set, bool, error) {
|
||||||
|
pod := obj.(*example.Pod)
|
||||||
|
return nil, fields.Set{"metadata.name": pod.Name}, pod.Initializers != nil, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedOut: []*example.Pod{preset[1].storedObj},
|
||||||
|
expectContinue: true,
|
||||||
|
},
|
||||||
|
{ // test List with limit when paging disabled
|
||||||
|
disablePaging: true,
|
||||||
|
prefix: "/two-level/",
|
||||||
|
pred: storage.SelectionPredicate{
|
||||||
|
Label: labels.Everything(),
|
||||||
|
Field: fields.Everything(),
|
||||||
|
Limit: 1,
|
||||||
|
GetAttrs: func(obj runtime.Object) (labels.Set, fields.Set, bool, error) {
|
||||||
|
pod := obj.(*example.Pod)
|
||||||
|
return nil, fields.Set{"metadata.name": pod.Name}, pod.Initializers != nil, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedOut: []*example.Pod{preset[1].storedObj, preset[2].storedObj},
|
||||||
|
expectContinue: false,
|
||||||
|
},
|
||||||
|
{ // test List with pregenerated continue token
|
||||||
|
prefix: "/two-level/",
|
||||||
|
pred: storage.SelectionPredicate{
|
||||||
|
Label: labels.Everything(),
|
||||||
|
Field: fields.Everything(),
|
||||||
|
Limit: 1,
|
||||||
|
Continue: secondContinuation,
|
||||||
|
GetAttrs: func(obj runtime.Object) (labels.Set, fields.Set, bool, error) {
|
||||||
|
pod := obj.(*example.Pod)
|
||||||
|
return nil, fields.Set{"metadata.name": pod.Name}, pod.Initializers != nil, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedOut: []*example.Pod{preset[2].storedObj},
|
||||||
|
},
|
||||||
|
{ // test List with multiple levels of directories and expect flattened result
|
||||||
|
prefix: "/two-level/",
|
||||||
|
pred: storage.Everything,
|
||||||
|
expectedOut: []*example.Pod{preset[1].storedObj, preset[2].storedObj},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
for i, tt := range tests {
|
for i, tt := range tests {
|
||||||
out := &example.PodList{}
|
out := &example.PodList{}
|
||||||
err := store.List(ctx, tt.prefix, "0", tt.pred, out)
|
var err error
|
||||||
|
if tt.disablePaging {
|
||||||
|
err = disablePagingStore.List(ctx, tt.prefix, "0", tt.pred, out)
|
||||||
|
} else {
|
||||||
|
err = store.List(ctx, tt.prefix, "0", tt.pred, out)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("List failed: %v", err)
|
t.Fatalf("#%d: List failed: %v", i, err)
|
||||||
|
}
|
||||||
|
if (len(out.Continue) > 0) != tt.expectContinue {
|
||||||
|
t.Errorf("#%d: unexpected continue token: %v", i, out.Continue)
|
||||||
}
|
}
|
||||||
if len(tt.expectedOut) != len(out.Items) {
|
if len(tt.expectedOut) != len(out.Items) {
|
||||||
t.Errorf("#%d: length of list want=%d, get=%d", i, len(tt.expectedOut), len(out.Items))
|
t.Errorf("#%d: length of list want=%d, get=%d", i, len(tt.expectedOut), len(out.Items))
|
||||||
@ -750,12 +818,75 @@ func TestList(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// test continuations
|
||||||
|
out := &example.PodList{}
|
||||||
|
pred := func(limit int64, continueValue string) storage.SelectionPredicate {
|
||||||
|
return storage.SelectionPredicate{
|
||||||
|
Limit: limit,
|
||||||
|
Continue: continueValue,
|
||||||
|
Label: labels.Everything(),
|
||||||
|
Field: fields.Everything(),
|
||||||
|
GetAttrs: func(obj runtime.Object) (labels.Set, fields.Set, bool, error) {
|
||||||
|
pod := obj.(*example.Pod)
|
||||||
|
return nil, fields.Set{"metadata.name": pod.Name}, pod.Initializers != nil, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := store.List(ctx, "/", "0", pred(1, ""), out); err != nil {
|
||||||
|
t.Fatalf("Unable to get initial list: %v", err)
|
||||||
|
}
|
||||||
|
if len(out.Continue) == 0 {
|
||||||
|
t.Fatalf("No continuation token set")
|
||||||
|
}
|
||||||
|
if len(out.Items) != 1 || !reflect.DeepEqual(&out.Items[0], preset[0].storedObj) {
|
||||||
|
t.Fatalf("Unexpected first page: %#v", out.Items)
|
||||||
|
}
|
||||||
|
|
||||||
|
continueFromSecondItem := out.Continue
|
||||||
|
|
||||||
|
// no limit, should get two items
|
||||||
|
out = &example.PodList{}
|
||||||
|
if err := store.List(ctx, "/", "0", pred(0, continueFromSecondItem), out); err != nil {
|
||||||
|
t.Fatalf("Unable to get second page: %v", err)
|
||||||
|
}
|
||||||
|
if len(out.Continue) != 0 {
|
||||||
|
t.Fatalf("Unexpected continuation token set")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(out.Items, []example.Pod{*preset[1].storedObj, *preset[2].storedObj}) {
|
||||||
|
key, rv, err := decodeContinue(continueFromSecondItem, "/")
|
||||||
|
t.Logf("continue token was %d %s %v", rv, key, err)
|
||||||
|
t.Fatalf("Unexpected second page: %#v", out.Items)
|
||||||
|
}
|
||||||
|
|
||||||
|
// limit, should get two more pages
|
||||||
|
out = &example.PodList{}
|
||||||
|
if err := store.List(ctx, "/", "0", pred(1, continueFromSecondItem), out); err != nil {
|
||||||
|
t.Fatalf("Unable to get second page: %v", err)
|
||||||
|
}
|
||||||
|
if len(out.Continue) == 0 {
|
||||||
|
t.Fatalf("No continuation token set")
|
||||||
|
}
|
||||||
|
if len(out.Items) != 1 || !reflect.DeepEqual(&out.Items[0], preset[1].storedObj) {
|
||||||
|
t.Fatalf("Unexpected second page: %#v", out.Items)
|
||||||
|
}
|
||||||
|
continueFromThirdItem := out.Continue
|
||||||
|
out = &example.PodList{}
|
||||||
|
if err := store.List(ctx, "/", "0", pred(1, continueFromThirdItem), out); err != nil {
|
||||||
|
t.Fatalf("Unable to get second page: %v", err)
|
||||||
|
}
|
||||||
|
if len(out.Continue) != 0 {
|
||||||
|
t.Fatalf("Unexpected continuation token set")
|
||||||
|
}
|
||||||
|
if len(out.Items) != 1 || !reflect.DeepEqual(&out.Items[0], preset[2].storedObj) {
|
||||||
|
t.Fatalf("Unexpected third page: %#v", out.Items)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSetup(t *testing.T) (context.Context, *store, *integration.ClusterV3) {
|
func testSetup(t *testing.T) (context.Context, *store, *integration.ClusterV3) {
|
||||||
codec := apitesting.TestCodec(codecs, examplev1.SchemeGroupVersion)
|
codec := apitesting.TestCodec(codecs, examplev1.SchemeGroupVersion)
|
||||||
cluster := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1})
|
cluster := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1})
|
||||||
store := newStore(cluster.RandClient(), false, codec, "", prefixTransformer{prefix: []byte(defaultTestPrefix)})
|
store := newStore(cluster.RandClient(), false, true, codec, "", prefixTransformer{prefix: []byte(defaultTestPrefix)})
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
return ctx, store, cluster
|
return ctx, store, cluster
|
||||||
}
|
}
|
||||||
@ -787,9 +918,57 @@ func TestPrefix(t *testing.T) {
|
|||||||
"/registry": "/registry",
|
"/registry": "/registry",
|
||||||
}
|
}
|
||||||
for configuredPrefix, effectivePrefix := range testcases {
|
for configuredPrefix, effectivePrefix := range testcases {
|
||||||
store := newStore(cluster.RandClient(), false, codec, configuredPrefix, transformer)
|
store := newStore(cluster.RandClient(), false, true, codec, configuredPrefix, transformer)
|
||||||
if store.pathPrefix != effectivePrefix {
|
if store.pathPrefix != effectivePrefix {
|
||||||
t.Errorf("configured prefix of %s, expected effective prefix of %s, got %s", configuredPrefix, effectivePrefix, store.pathPrefix)
|
t.Errorf("configured prefix of %s, expected effective prefix of %s, got %s", configuredPrefix, effectivePrefix, store.pathPrefix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func encodeContinueOrDie(apiVersion string, resourceVersion int64, nextKey string) string {
|
||||||
|
out, err := json.Marshal(&continueToken{APIVersion: apiVersion, ResourceVersion: resourceVersion, StartKey: nextKey})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return base64.RawURLEncoding.EncodeToString(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_decodeContinue(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
continueValue string
|
||||||
|
keyPrefix string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantFromKey string
|
||||||
|
wantRv int64
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{name: "valid", args: args{continueValue: encodeContinueOrDie("v1alpha1", 1, "key"), keyPrefix: "/test/"}, wantRv: 1, wantFromKey: "/test/key"},
|
||||||
|
|
||||||
|
{name: "empty version", args: args{continueValue: encodeContinueOrDie("", 1, "key"), keyPrefix: "/test/"}, wantErr: true},
|
||||||
|
{name: "invalid version", args: args{continueValue: encodeContinueOrDie("v1", 1, "key"), keyPrefix: "/test/"}, wantErr: true},
|
||||||
|
|
||||||
|
{name: "path traversal - parent", args: args{continueValue: encodeContinueOrDie("v1alpha", 1, "../key"), keyPrefix: "/test/"}, wantErr: true},
|
||||||
|
{name: "path traversal - local", args: args{continueValue: encodeContinueOrDie("v1alpha", 1, "./key"), keyPrefix: "/test/"}, wantErr: true},
|
||||||
|
{name: "path traversal - double parent", args: args{continueValue: encodeContinueOrDie("v1alpha", 1, "./../key"), keyPrefix: "/test/"}, wantErr: true},
|
||||||
|
{name: "path traversal - after parent", args: args{continueValue: encodeContinueOrDie("v1alpha", 1, "key/../.."), keyPrefix: "/test/"}, wantErr: true},
|
||||||
|
{name: "path traversal - separator", args: args{continueValue: encodeContinueOrDie("v1alpha", 1, "/"), keyPrefix: "/test/"}, wantErr: true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
gotFromKey, gotRv, err := decodeContinue(tt.args.continueValue, tt.args.keyPrefix)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("decodeContinue() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if gotFromKey != tt.wantFromKey {
|
||||||
|
t.Errorf("decodeContinue() gotFromKey = %v, want %v", gotFromKey, tt.wantFromKey)
|
||||||
|
}
|
||||||
|
if gotRv != tt.wantRv {
|
||||||
|
t.Errorf("decodeContinue() gotRv = %v, want %v", gotRv, tt.wantRv)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -19,20 +19,18 @@ package etcd3
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
apierrs "k8s.io/apimachinery/pkg/api/errors"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/watch"
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
"k8s.io/apiserver/pkg/storage"
|
"k8s.io/apiserver/pkg/storage"
|
||||||
"k8s.io/apiserver/pkg/storage/value"
|
"k8s.io/apiserver/pkg/storage/value"
|
||||||
|
|
||||||
"github.com/coreos/etcd/clientv3"
|
"github.com/coreos/etcd/clientv3"
|
||||||
etcdrpc "github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
)
|
)
|
||||||
@ -139,7 +137,7 @@ func (wc *watchChan) run() {
|
|||||||
if err == context.Canceled {
|
if err == context.Canceled {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
errResult := parseError(err)
|
errResult := transformErrorToEvent(err)
|
||||||
if errResult != nil {
|
if errResult != nil {
|
||||||
// error result is guaranteed to be received by user before closing ResultChan.
|
// error result is guaranteed to be received by user before closing ResultChan.
|
||||||
select {
|
select {
|
||||||
@ -319,28 +317,15 @@ func (wc *watchChan) transform(e *event) (res *watch.Event) {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseError(err error) *watch.Event {
|
func transformErrorToEvent(err error) *watch.Event {
|
||||||
var status *metav1.Status
|
err = interpretWatchError(err)
|
||||||
switch {
|
if _, ok := err.(apierrs.APIStatus); !ok {
|
||||||
case err == etcdrpc.ErrCompacted:
|
err = apierrs.NewInternalError(err)
|
||||||
status = &metav1.Status{
|
|
||||||
Status: metav1.StatusFailure,
|
|
||||||
Message: err.Error(),
|
|
||||||
Code: http.StatusGone,
|
|
||||||
Reason: metav1.StatusReasonExpired,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
status = &metav1.Status{
|
|
||||||
Status: metav1.StatusFailure,
|
|
||||||
Message: err.Error(),
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
Reason: metav1.StatusReasonInternalError,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
status := err.(apierrs.APIStatus).Status()
|
||||||
return &watch.Event{
|
return &watch.Event{
|
||||||
Type: watch.Error,
|
Type: watch.Error,
|
||||||
Object: status,
|
Object: &status,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,13 +226,13 @@ func TestWatchError(t *testing.T) {
|
|||||||
codec := &testCodec{apitesting.TestCodec(codecs, examplev1.SchemeGroupVersion)}
|
codec := &testCodec{apitesting.TestCodec(codecs, examplev1.SchemeGroupVersion)}
|
||||||
cluster := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1})
|
cluster := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1})
|
||||||
defer cluster.Terminate(t)
|
defer cluster.Terminate(t)
|
||||||
invalidStore := newStore(cluster.RandClient(), false, codec, "", prefixTransformer{prefix: []byte("test!")})
|
invalidStore := newStore(cluster.RandClient(), false, true, codec, "", prefixTransformer{prefix: []byte("test!")})
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
w, err := invalidStore.Watch(ctx, "/abc", "0", storage.Everything)
|
w, err := invalidStore.Watch(ctx, "/abc", "0", storage.Everything)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Watch failed: %v", err)
|
t.Fatalf("Watch failed: %v", err)
|
||||||
}
|
}
|
||||||
validStore := newStore(cluster.RandClient(), false, codec, "", prefixTransformer{prefix: []byte("test!")})
|
validStore := newStore(cluster.RandClient(), false, true, codec, "", prefixTransformer{prefix: []byte("test!")})
|
||||||
validStore.GuaranteedUpdate(ctx, "/abc", &example.Pod{}, true, nil, storage.SimpleUpdate(
|
validStore.GuaranteedUpdate(ctx, "/abc", &example.Pod{}, true, nil, storage.SimpleUpdate(
|
||||||
func(runtime.Object) (runtime.Object, error) {
|
func(runtime.Object) (runtime.Object, error) {
|
||||||
return &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, nil
|
return &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, nil
|
||||||
|
@ -36,8 +36,9 @@ type Versioner interface {
|
|||||||
UpdateObject(obj runtime.Object, resourceVersion uint64) error
|
UpdateObject(obj runtime.Object, resourceVersion uint64) error
|
||||||
// UpdateList sets the resource version into an API list object. Returns an error if the object
|
// UpdateList sets the resource version into an API list object. Returns an error if the object
|
||||||
// cannot be updated correctly. May return nil if the requested object does not need metadata
|
// cannot be updated correctly. May return nil if the requested object does not need metadata
|
||||||
// from database.
|
// from database. continueValue is optional and indicates that more results are available if
|
||||||
UpdateList(obj runtime.Object, resourceVersion uint64) error
|
// the client passes that value to the server in a subsequent call.
|
||||||
|
UpdateList(obj runtime.Object, resourceVersion uint64, continueValue string) error
|
||||||
// PrepareObjectForStorage should set SelfLink and ResourceVersion to the empty value. Should
|
// PrepareObjectForStorage should set SelfLink and ResourceVersion to the empty value. Should
|
||||||
// return an error if the specified object cannot be updated.
|
// return an error if the specified object cannot be updated.
|
||||||
PrepareObjectForStorage(obj runtime.Object) error
|
PrepareObjectForStorage(obj runtime.Object) error
|
||||||
|
@ -76,6 +76,8 @@ type SelectionPredicate struct {
|
|||||||
IncludeUninitialized bool
|
IncludeUninitialized bool
|
||||||
GetAttrs AttrFunc
|
GetAttrs AttrFunc
|
||||||
IndexFields []string
|
IndexFields []string
|
||||||
|
Limit int64
|
||||||
|
Continue string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Matches returns true if the given object's labels and fields (as
|
// Matches returns true if the given object's labels and fields (as
|
||||||
@ -118,6 +120,9 @@ func (s *SelectionPredicate) MatchesObjectAttributes(l labels.Set, f fields.Set,
|
|||||||
// MatchesSingle will return (name, true) if and only if s.Field matches on the object's
|
// MatchesSingle will return (name, true) if and only if s.Field matches on the object's
|
||||||
// name.
|
// name.
|
||||||
func (s *SelectionPredicate) MatchesSingle() (string, bool) {
|
func (s *SelectionPredicate) MatchesSingle() (string, bool) {
|
||||||
|
if len(s.Continue) > 0 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
// TODO: should be namespace.name
|
// TODO: should be namespace.name
|
||||||
if name, ok := s.Field.RequiresExactMatch("metadata.name"); ok {
|
if name, ok := s.Field.RequiresExactMatch("metadata.name"); ok {
|
||||||
return name, true
|
return name, true
|
||||||
|
@ -41,6 +41,11 @@ type Config struct {
|
|||||||
CAFile string
|
CAFile string
|
||||||
// Quorum indicates that whether read operations should be quorum-level consistent.
|
// Quorum indicates that whether read operations should be quorum-level consistent.
|
||||||
Quorum bool
|
Quorum bool
|
||||||
|
// Paging indicates whether the server implementation should allow paging (if it is
|
||||||
|
// supported). This is generally configured by feature gating, or by a specific
|
||||||
|
// resource type not wishing to allow paging, and is not intended for end users to
|
||||||
|
// set.
|
||||||
|
Paging bool
|
||||||
// DeserializationCacheSize is the size of cache of deserialized objects.
|
// DeserializationCacheSize is the size of cache of deserialized objects.
|
||||||
// Currently this is only supported in etcd2.
|
// Currently this is only supported in etcd2.
|
||||||
// We will drop the cache once using protobuf.
|
// We will drop the cache once using protobuf.
|
||||||
|
@ -61,7 +61,7 @@ func newETCD3Storage(c storagebackend.Config) (storage.Interface, DestroyFunc, e
|
|||||||
transformer = value.IdentityTransformer
|
transformer = value.IdentityTransformer
|
||||||
}
|
}
|
||||||
if c.Quorum {
|
if c.Quorum {
|
||||||
return etcd3.New(client, c.Codec, c.Prefix, transformer), destroyFunc, nil
|
return etcd3.New(client, c.Codec, c.Prefix, transformer, c.Paging), destroyFunc, nil
|
||||||
}
|
}
|
||||||
return etcd3.NewWithNoQuorumRead(client, c.Codec, c.Prefix, transformer), destroyFunc, nil
|
return etcd3.NewWithNoQuorumRead(client, c.Codec, c.Prefix, transformer, c.Paging), destroyFunc, nil
|
||||||
}
|
}
|
||||||
|
@ -93,7 +93,7 @@ func AddObjectMetaFieldsSet(source fields.Set, objectMeta *metav1.ObjectMeta, ha
|
|||||||
|
|
||||||
func newEtcdTestStorage(t *testing.T, prefix string) (*etcdtesting.EtcdTestServer, storage.Interface) {
|
func newEtcdTestStorage(t *testing.T, prefix string) (*etcdtesting.EtcdTestServer, storage.Interface) {
|
||||||
server, _ := etcdtesting.NewUnsecuredEtcd3TestClientServer(t, scheme)
|
server, _ := etcdtesting.NewUnsecuredEtcd3TestClientServer(t, scheme)
|
||||||
storage := etcd3.New(server.V3Client, apitesting.TestCodec(codecs, examplev1.SchemeGroupVersion), prefix, value.IdentityTransformer)
|
storage := etcd3.New(server.V3Client, apitesting.TestCodec(codecs, examplev1.SchemeGroupVersion), prefix, value.IdentityTransformer, true)
|
||||||
return server, storage
|
return server, storage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,14 +30,20 @@ import (
|
|||||||
|
|
||||||
const defaultPageSize = 500
|
const defaultPageSize = 500
|
||||||
|
|
||||||
|
// ListPageFunc returns a list object for the given list options.
|
||||||
type ListPageFunc func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error)
|
type ListPageFunc func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error)
|
||||||
|
|
||||||
|
// SimplePageFunc adapts a context-less list function into one that accepts a context.
|
||||||
func SimplePageFunc(fn func(opts metav1.ListOptions) (runtime.Object, error)) ListPageFunc {
|
func SimplePageFunc(fn func(opts metav1.ListOptions) (runtime.Object, error)) ListPageFunc {
|
||||||
return func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) {
|
return func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) {
|
||||||
return fn(opts)
|
return fn(opts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListPager assists client code in breaking large list queries into multiple
|
||||||
|
// smaller chunks of PageSize or smaller. PageFn is expected to accept a
|
||||||
|
// metav1.ListOptions that supports paging and return a list. The pager does
|
||||||
|
// not alter the field or label selectors on the initial options list.
|
||||||
type ListPager struct {
|
type ListPager struct {
|
||||||
PageSize int64
|
PageSize int64
|
||||||
PageFn ListPageFunc
|
PageFn ListPageFunc
|
||||||
@ -45,6 +51,8 @@ type ListPager struct {
|
|||||||
FullListIfExpired bool
|
FullListIfExpired bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New creates a new pager from the provided pager function using the default
|
||||||
|
// options.
|
||||||
func New(fn ListPageFunc) *ListPager {
|
func New(fn ListPageFunc) *ListPager {
|
||||||
return &ListPager{
|
return &ListPager{
|
||||||
PageSize: defaultPageSize,
|
PageSize: defaultPageSize,
|
||||||
@ -53,6 +61,9 @@ func New(fn ListPageFunc) *ListPager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List returns a single list object, but attempts to retrieve smaller chunks from the
|
||||||
|
// server to reduce the impact on the server. If the chunk attempt fails, it will load
|
||||||
|
// the full list instead.
|
||||||
func (p *ListPager) List(ctx context.Context, options metav1.ListOptions) (runtime.Object, error) {
|
func (p *ListPager) List(ctx context.Context, options metav1.ListOptions) (runtime.Object, error) {
|
||||||
if options.Limit == 0 {
|
if options.Limit == 0 {
|
||||||
options.Limit = p.PageSize
|
options.Limit = p.PageSize
|
||||||
|
@ -238,6 +238,10 @@
|
|||||||
"ImportPath": "github.com/spf13/pflag",
|
"ImportPath": "github.com/spf13/pflag",
|
||||||
"Rev": "9ff6c6923cfffbcd502984b8e0c80539a94968b7"
|
"Rev": "9ff6c6923cfffbcd502984b8e0c80539a94968b7"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "golang.org/x/net/context",
|
||||||
|
"Rev": "1c05540f6879653db88113bc4a2b70aec4bd491f"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "golang.org/x/net/http2",
|
"ImportPath": "golang.org/x/net/http2",
|
||||||
"Rev": "1c05540f6879653db88113bc4a2b70aec4bd491f"
|
"Rev": "1c05540f6879653db88113bc4a2b70aec4bd491f"
|
||||||
|
Loading…
Reference in New Issue
Block a user