From 17cb7c89412e483aeb2d8f0ce7fecce1232728e4 Mon Sep 17 00:00:00 2001 From: Antonin Stefanutti Date: Thu, 15 Nov 2018 11:45:58 +0100 Subject: [PATCH] Support List with options for custom metrics label selector --- pkg/apiserver/endpoints/handlers/get.go | 213 +++++++++++++++++++ pkg/apiserver/endpoints/handlers/response.go | 196 +++++++++++++++++ pkg/apiserver/endpoints/handlers/rest.go | 73 +++++++ pkg/apiserver/endpoints/handlers/watch.go | 111 ++++++++++ pkg/apiserver/installer/cmhandlers.go | 2 +- pkg/apiserver/installer/installer.go | 6 +- pkg/apiserver/registry/rest/rest.go | 45 ++++ pkg/registry/custom_metrics/reststorage.go | 4 +- 8 files changed, 646 insertions(+), 4 deletions(-) create mode 100644 pkg/apiserver/endpoints/handlers/get.go create mode 100644 pkg/apiserver/endpoints/handlers/response.go create mode 100644 pkg/apiserver/endpoints/handlers/rest.go create mode 100644 pkg/apiserver/endpoints/handlers/watch.go create mode 100644 pkg/apiserver/registry/rest/rest.go diff --git a/pkg/apiserver/endpoints/handlers/get.go b/pkg/apiserver/endpoints/handlers/get.go new file mode 100644 index 000000000..e3b5cb6dc --- /dev/null +++ b/pkg/apiserver/endpoints/handlers/get.go @@ -0,0 +1,213 @@ +/* +Copyright 2017 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 handlers + +import ( + "context" + "fmt" + "math/rand" + "net/http" + "net/url" + "strings" + "time" + + "github.com/golang/glog" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/endpoints/handlers" + "k8s.io/apiserver/pkg/endpoints/metrics" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/rest" + utiltrace "k8s.io/utils/trace" + + cm_rest "github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/apiserver/registry/rest" +) + +func ListResourceWithOptions(r cm_rest.ListerWithOptions, rw rest.Watcher, scope handlers.RequestScope, forceWatch bool, minRequestTimeout time.Duration) http.HandlerFunc { + list := func(w http.ResponseWriter, req *http.Request, ctx context.Context, opts *metainternalversion.ListOptions) (runtime.Object, error) { + extraOpts, subpath, subpathKey := r.NewListOptions() + if err := getRequestOptions(req, scope, extraOpts, subpath, subpathKey, false); err != nil { + err = errors.NewBadRequest(err.Error()) + return nil, err + } + return r.List(ctx, opts, extraOpts) + } + return listResource(list, rw, scope, forceWatch, minRequestTimeout) +} + +// getRequestOptions parses out options and can include path information. The path information shouldn't include the subresource. +func getRequestOptions(req *http.Request, scope handlers.RequestScope, into runtime.Object, subpath bool, subpathKey string, isSubresource bool) error { + if into == nil { + return nil + } + + query := req.URL.Query() + if subpath { + newQuery := make(url.Values) + for k, v := range query { + newQuery[k] = v + } + + ctx := req.Context() + requestInfo, _ := request.RequestInfoFrom(ctx) + startingIndex := 2 + if isSubresource { + startingIndex = 3 + } + + p := strings.Join(requestInfo.Parts[startingIndex:], "/") + + // ensure non-empty subpaths correctly reflect a leading slash + if len(p) > 0 && !strings.HasPrefix(p, "/") { + p = "/" + p + } + + // ensure subpaths correctly reflect the presence of a trailing slash on the original request + if strings.HasSuffix(requestInfo.Path, "/") && !strings.HasSuffix(p, "/") { + p += "/" + } + + newQuery[subpathKey] = []string{p} + query = newQuery + } + return scope.ParameterCodec.DecodeParameters(query, scope.Kind.GroupVersion(), into) +} + +func listResource(list func(http.ResponseWriter, *http.Request, context.Context, *metainternalversion.ListOptions) (runtime.Object, error), rw rest.Watcher, scope handlers.RequestScope, forceWatch bool, minRequestTimeout time.Duration) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + // For performance tracking purposes. + trace := utiltrace.New("List " + req.URL.Path) + + namespace, err := scope.Namer.Namespace(req) + if err != nil { + writeError(&scope, err, w, req) + return + } + + // Watches for single objects are routed to this function. + // Treat a name parameter the same as a field selector entry. + hasName := true + _, name, err := scope.Namer.Name(req) + if err != nil { + hasName = false + } + + ctx := req.Context() + ctx = request.WithNamespace(ctx, namespace) + + opts := metainternalversion.ListOptions{} + if err := metainternalversion.ParameterCodec.DecodeParameters(req.URL.Query(), scope.MetaGroupVersion, &opts); err != nil { + err = errors.NewBadRequest(err.Error()) + writeError(&scope, err, w, req) + return + } + + // transform fields + // TODO: DecodeParametersInto should do this. + if opts.FieldSelector != nil { + fn := func(label, value string) (newLabel, newValue string, err error) { + return scope.Convertor.ConvertFieldLabel(scope.Kind, label, value) + } + if opts.FieldSelector, err = opts.FieldSelector.Transform(fn); err != nil { + // TODO: allow bad request to set field causes based on query parameters + err = errors.NewBadRequest(err.Error()) + writeError(&scope, err, w, req) + return + } + } + + if hasName { + // metadata.name is the canonical internal name. + // SelectionPredicate will notice that this is a request for + // a single object and optimize the storage query accordingly. + nameSelector := fields.OneTermEqualSelector("metadata.name", name) + + // Note that fieldSelector setting explicitly the "metadata.name" + // will result in reaching this branch (as the value of that field + // is propagated to requestInfo as the name parameter. + // That said, the allowed field selectors in this branch are: + // nil, fields.Everything and field selector matching metadata.name + // for our name. + if opts.FieldSelector != nil && !opts.FieldSelector.Empty() { + selectedName, ok := opts.FieldSelector.RequiresExactMatch("metadata.name") + if !ok || name != selectedName { + writeError(&scope, errors.NewBadRequest("fieldSelector metadata.name doesn't match requested name"), w, req) + return + } + } else { + opts.FieldSelector = nameSelector + } + } + + if opts.Watch || forceWatch { + if rw == nil { + writeError(&scope, errors.NewMethodNotSupported(scope.Resource.GroupResource(), "watch"), w, req) + return + } + // TODO: Currently we explicitly ignore ?timeout= and use only ?timeoutSeconds=. + timeout := time.Duration(0) + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + if timeout == 0 && minRequestTimeout > 0 { + timeout = time.Duration(float64(minRequestTimeout) * (rand.Float64() + 1.0)) + } + glog.V(3).Infof("Starting watch for %s, rv=%s labels=%s fields=%s timeout=%s", req.URL.Path, opts.ResourceVersion, opts.LabelSelector, opts.FieldSelector, timeout) + + watcher, err := rw.Watch(ctx, &opts) + if err != nil { + writeError(&scope, err, w, req) + return + } + requestInfo, _ := request.RequestInfoFrom(ctx) + metrics.RecordLongRunning(req, requestInfo, func() { + serveWatch(watcher, scope, req, w, timeout) + }) + return + } + + // Log only long List requests (ignore Watch). + defer trace.LogIfLong(500 * time.Millisecond) + trace.Step("About to List from storage") + result, err := list(w, req, ctx, &opts) + if err != nil { + writeError(&scope, err, w, req) + return + } + trace.Step("Listing from storage done") + numberOfItems, err := setListSelfLink(result, ctx, req, scope.Namer) + if err != nil { + writeError(&scope, err, w, req) + return + } + trace.Step("Self-linking done") + // Ensure empty lists return a non-nil items slice + if numberOfItems == 0 && meta.IsListType(result) { + if err := meta.SetList(result, []runtime.Object{}); err != nil { + writeError(&scope, err, w, req) + return + } + } + + transformResponseObject(ctx, scope, req, w, http.StatusOK, result) + trace.Step(fmt.Sprintf("Writing http response done (%d items)", numberOfItems)) + } +} diff --git a/pkg/apiserver/endpoints/handlers/response.go b/pkg/apiserver/endpoints/handlers/response.go new file mode 100644 index 000000000..3ced4afda --- /dev/null +++ b/pkg/apiserver/endpoints/handlers/response.go @@ -0,0 +1,196 @@ +/* +Copyright 2017 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 handlers + +import ( + "context" + "fmt" + "net/http" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/endpoints/handlers" + "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" + "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" +) + +// transformResponseObject takes an object loaded from storage and performs any necessary transformations. +// Will write the complete response object. +func transformResponseObject(ctx context.Context, scope handlers.RequestScope, req *http.Request, w http.ResponseWriter, statusCode int, result runtime.Object) { + // TODO: fetch the media type much earlier in request processing and pass it into this method. + mediaType, _, err := negotiation.NegotiateOutputMediaType(req, scope.Serializer, &scope) + if err != nil { + status := responsewriters.ErrorToAPIStatus(err) + responsewriters.WriteRawJSON(int(status.Code), status, w) + return + } + + // If conversion was allowed by the scope, perform it before writing the response + if target := mediaType.Convert; target != nil { + switch { + + case target.Kind == "PartialObjectMetadata" && target.GroupVersion() == metav1beta1.SchemeGroupVersion: + if meta.IsListType(result) { + // TODO: this should be calculated earlier + err = newNotAcceptableError(fmt.Sprintf("you requested PartialObjectMetadata, but the requested object is a list (%T)", result)) + writeError(&scope, err, w, req) + return + } + m, err := meta.Accessor(result) + if err != nil { + writeError(&scope, err, w, req) + return + } + partial := meta.AsPartialObjectMetadata(m) + partial.GetObjectKind().SetGroupVersionKind(metav1beta1.SchemeGroupVersion.WithKind("PartialObjectMetadata")) + + // renegotiate under the internal version + _, info, err := negotiation.NegotiateOutputMediaType(req, metainternalversion.Codecs, &scope) + if err != nil { + writeError(&scope, err, w, req) + return + } + encoder := metainternalversion.Codecs.EncoderForVersion(info.Serializer, metav1beta1.SchemeGroupVersion) + responsewriters.SerializeObject(info.MediaType, encoder, w, req, statusCode, partial) + return + + case target.Kind == "PartialObjectMetadataList" && target.GroupVersion() == metav1beta1.SchemeGroupVersion: + if !meta.IsListType(result) { + // TODO: this should be calculated earlier + err = newNotAcceptableError(fmt.Sprintf("you requested PartialObjectMetadataList, but the requested object is not a list (%T)", result)) + writeError(&scope, err, w, req) + return + } + list := &metav1beta1.PartialObjectMetadataList{} + err := meta.EachListItem(result, func(obj runtime.Object) error { + m, err := meta.Accessor(obj) + if err != nil { + return err + } + partial := meta.AsPartialObjectMetadata(m) + partial.GetObjectKind().SetGroupVersionKind(metav1beta1.SchemeGroupVersion.WithKind("PartialObjectMetadata")) + list.Items = append(list.Items, partial) + return nil + }) + if err != nil { + writeError(&scope, err, w, req) + return + } + + // renegotiate under the internal version + _, info, err := negotiation.NegotiateOutputMediaType(req, metainternalversion.Codecs, &scope) + if err != nil { + writeError(&scope, err, w, req) + return + } + encoder := metainternalversion.Codecs.EncoderForVersion(info.Serializer, metav1beta1.SchemeGroupVersion) + responsewriters.SerializeObject(info.MediaType, encoder, w, req, statusCode, list) + return + + case target.Kind == "Table" && target.GroupVersion() == metav1beta1.SchemeGroupVersion: + // TODO: relax the version abstraction + // TODO: skip if this is a status response (delete without body)? + + opts := &metav1beta1.TableOptions{} + if err := metav1beta1.ParameterCodec.DecodeParameters(req.URL.Query(), metav1beta1.SchemeGroupVersion, opts); err != nil { + writeError(&scope, err, w, req) + return + } + + table, err := scope.TableConvertor.ConvertToTable(ctx, result, opts) + if err != nil { + writeError(&scope, err, w, req) + return + } + + for i := range table.Rows { + item := &table.Rows[i] + switch opts.IncludeObject { + case metav1beta1.IncludeObject: + item.Object.Object, err = scope.Convertor.ConvertToVersion(item.Object.Object, scope.Kind.GroupVersion()) + if err != nil { + writeError(&scope, err, w, req) + return + } + // TODO: rely on defaulting for the value here? + case metav1beta1.IncludeMetadata, "": + m, err := meta.Accessor(item.Object.Object) + if err != nil { + writeError(&scope, err, w, req) + return + } + // TODO: turn this into an internal type and do conversion in order to get object kind automatically set? + partial := meta.AsPartialObjectMetadata(m) + partial.GetObjectKind().SetGroupVersionKind(metav1beta1.SchemeGroupVersion.WithKind("PartialObjectMetadata")) + item.Object.Object = partial + case metav1beta1.IncludeNone: + item.Object.Object = nil + default: + // TODO: move this to validation on the table options? + err = errors.NewBadRequest(fmt.Sprintf("unrecognized includeObject value: %q", opts.IncludeObject)) + writeError(&scope, err, w, req) + } + } + + // renegotiate under the internal version + _, info, err := negotiation.NegotiateOutputMediaType(req, metainternalversion.Codecs, &scope) + if err != nil { + writeError(&scope, err, w, req) + return + } + encoder := metainternalversion.Codecs.EncoderForVersion(info.Serializer, metav1beta1.SchemeGroupVersion) + responsewriters.SerializeObject(info.MediaType, encoder, w, req, statusCode, table) + return + + default: + // this block should only be hit if scope AllowsConversion is incorrect + accepted, _ := negotiation.MediaTypesForSerializer(metainternalversion.Codecs) + err := negotiation.NewNotAcceptableError(accepted) + status := responsewriters.ErrorToAPIStatus(err) + responsewriters.WriteRawJSON(int(status.Code), status, w) + return + } + } + + responsewriters.WriteObject(statusCode, scope.Kind.GroupVersion(), scope.Serializer, result, w, req) +} + +// errNotAcceptable indicates Accept negotiation has failed +type errNotAcceptable struct { + message string +} + +func newNotAcceptableError(message string) error { + return errNotAcceptable{message} +} + +func (e errNotAcceptable) Error() string { + return e.message +} + +func (e errNotAcceptable) Status() metav1.Status { + return metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusNotAcceptable, + Reason: metav1.StatusReason("NotAcceptable"), + Message: e.Error(), + } +} diff --git a/pkg/apiserver/endpoints/handlers/rest.go b/pkg/apiserver/endpoints/handlers/rest.go new file mode 100644 index 000000000..17610dee0 --- /dev/null +++ b/pkg/apiserver/endpoints/handlers/rest.go @@ -0,0 +1,73 @@ +/* +Copyright 2017 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 handlers + +import ( + "context" + "fmt" + "net/http" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/endpoints/handlers" + "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/klog" +) + +func writeError(scope *handlers.RequestScope, err error, w http.ResponseWriter, req *http.Request) { + responsewriters.ErrorNegotiated(err, scope.Serializer, scope.Kind.GroupVersion(), w, req) +} + +// setSelfLink sets the self link of an object (or the child items in a list) to the base URL of the request +// plus the path and query generated by the provided linkFunc +func setSelfLink(obj runtime.Object, requestInfo *request.RequestInfo, namer handlers.ScopeNamer) error { + // TODO: SelfLink generation should return a full URL? + uri, err := namer.GenerateLink(requestInfo, obj) + if err != nil { + return nil + } + + return namer.SetSelfLink(obj, uri) +} + +// setListSelfLink sets the self link of a list to the base URL, then sets the self links +// on all child objects returned. Returns the number of items in the list. +func setListSelfLink(obj runtime.Object, ctx context.Context, req *http.Request, namer handlers.ScopeNamer) (int, error) { + if !meta.IsListType(obj) { + return 0, nil + } + + uri, err := namer.GenerateListLink(req) + if err != nil { + return 0, err + } + if err := namer.SetSelfLink(obj, uri); err != nil { + klog.V(4).Infof("Unable to set self link on object: %v", err) + } + requestInfo, ok := request.RequestInfoFrom(ctx) + if !ok { + return 0, fmt.Errorf("missing requestInfo") + } + + count := 0 + err = meta.EachListItem(obj, func(obj runtime.Object) error { + count++ + return setSelfLink(obj, requestInfo, namer) + }) + return count, err +} diff --git a/pkg/apiserver/endpoints/handlers/watch.go b/pkg/apiserver/endpoints/handlers/watch.go new file mode 100644 index 000000000..88b960ade --- /dev/null +++ b/pkg/apiserver/endpoints/handlers/watch.go @@ -0,0 +1,111 @@ +/* +Copyright 2014 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 handlers + +import ( + "fmt" + "net/http" + "reflect" + "time" + + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/apiserver/pkg/endpoints/handlers" + "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" + "k8s.io/apiserver/pkg/endpoints/request" +) + +// nothing will ever be sent down this channel +var neverExitWatch <-chan time.Time = make(chan time.Time) + +// timeoutFactory abstracts watch timeout logic for testing +type TimeoutFactory interface { + TimeoutCh() (<-chan time.Time, func() bool) +} + +// realTimeoutFactory implements timeoutFactory +type realTimeoutFactory struct { + timeout time.Duration +} + +// TimeoutCh returns a channel which will receive something when the watch times out, +// and a cleanup function to call when this happens. +func (w *realTimeoutFactory) TimeoutCh() (<-chan time.Time, func() bool) { + if w.timeout == 0 { + return neverExitWatch, func() bool { return false } + } + t := time.NewTimer(w.timeout) + return t.C, t.Stop +} + +// serveWatch handles serving requests to the server +// TODO: the functionality in this method and in WatchServer.Serve is not cleanly decoupled. +func serveWatch(watcher watch.Interface, scope handlers.RequestScope, req *http.Request, w http.ResponseWriter, timeout time.Duration) { + // negotiate for the stream serializer + serializer, err := negotiation.NegotiateOutputStreamSerializer(req, scope.Serializer) + if err != nil { + writeError(&scope, err, w, req) + return + } + framer := serializer.StreamSerializer.Framer + streamSerializer := serializer.StreamSerializer.Serializer + embedded := serializer.Serializer + if framer == nil { + writeError(&scope, fmt.Errorf("no framer defined for %q available for embedded encoding", serializer.MediaType), w, req) + return + } + encoder := scope.Serializer.EncoderForVersion(streamSerializer, scope.Kind.GroupVersion()) + + useTextFraming := serializer.EncodesAsText + + // find the embedded serializer matching the media type + embeddedEncoder := scope.Serializer.EncoderForVersion(embedded, scope.Kind.GroupVersion()) + + // TODO: next step, get back mediaTypeOptions from negotiate and return the exact value here + mediaType := serializer.MediaType + if mediaType != runtime.ContentTypeJSON { + mediaType += ";stream=watch" + } + + ctx := req.Context() + requestInfo, ok := request.RequestInfoFrom(ctx) + if !ok { + writeError(&scope, fmt.Errorf("missing requestInfo"), w, req) + return + } + + server := &handlers.WatchServer{ + Watching: watcher, + Scope: scope, + + UseTextFraming: useTextFraming, + MediaType: mediaType, + Framer: framer, + Encoder: encoder, + EmbeddedEncoder: embeddedEncoder, + Fixup: func(obj runtime.Object) { + if err := setSelfLink(obj, requestInfo, scope.Namer); err != nil { + utilruntime.HandleError(fmt.Errorf("failed to set link for object %v: %v", reflect.TypeOf(obj), err)) + } + }, + + TimeoutFactory: &realTimeoutFactory{timeout}, + } + + server.ServeHTTP(w, req) +} diff --git a/pkg/apiserver/installer/cmhandlers.go b/pkg/apiserver/installer/cmhandlers.go index 85edb1bc6..2c073b4ee 100644 --- a/pkg/apiserver/installer/cmhandlers.go +++ b/pkg/apiserver/installer/cmhandlers.go @@ -24,9 +24,9 @@ import ( "k8s.io/apiserver/pkg/endpoints/handlers" "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" "k8s.io/apiserver/pkg/endpoints/metrics" - "k8s.io/apiserver/pkg/registry/rest" "github.com/emicklei/go-restful" + "github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/apiserver/registry/rest" ) type CMHandlers struct{} diff --git a/pkg/apiserver/installer/installer.go b/pkg/apiserver/installer/installer.go index dbe164df1..d9fd11c4e 100644 --- a/pkg/apiserver/installer/installer.go +++ b/pkg/apiserver/installer/installer.go @@ -35,6 +35,8 @@ import ( "k8s.io/apiserver/pkg/registry/rest" "github.com/emicklei/go-restful" + cm_handlers "github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/apiserver/endpoints/handlers" + cm_rest "github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/apiserver/registry/rest" ) // NB: the contents of this file should mostly be a subset of the functionality @@ -287,8 +289,8 @@ func restfulListResource(r rest.Lister, rw rest.Watcher, scope handlers.RequestS } } -func restfulListResourceWithOptions(r rest.ListerWithOptions, rw rest.Watcher, scope handlers.RequestScope, forceWatch bool, minRequestTimeout time.Duration) restful.RouteFunction { +func restfulListResourceWithOptions(r cm_rest.ListerWithOptions, rw rest.Watcher, scope handlers.RequestScope, forceWatch bool, minRequestTimeout time.Duration) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { - handlers.ListResourceWithOptions(r, rw, scope, forceWatch, minRequestTimeout)(res.ResponseWriter, req.Request) + cm_handlers.ListResourceWithOptions(r, rw, scope, forceWatch, minRequestTimeout)(res.ResponseWriter, req.Request) } } diff --git a/pkg/apiserver/registry/rest/rest.go b/pkg/apiserver/registry/rest/rest.go new file mode 100644 index 000000000..2fc01efb3 --- /dev/null +++ b/pkg/apiserver/registry/rest/rest.go @@ -0,0 +1,45 @@ +/* +Copyright 2017 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 rest + +import ( + "context" + + metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" + "k8s.io/apimachinery/pkg/runtime" +) + +// ListerWithOptions is an object that can retrieve resources that match the provided field +// and label criteria and takes additional options on the list request. +type ListerWithOptions interface { + // NewList returns an empty object that can be used with the List call. + // This object must be a pointer type for use with Codec.DecodeInto([]byte, runtime.Object) + NewList() runtime.Object + + // List selects resources in the storage which match to the selector. 'options' can be nil. + // The extraOptions object passed to it is of the same type returned by the NewListOptions + // method. + List(ctx context.Context, options *metainternalversion.ListOptions, extraOptions runtime.Object) (runtime.Object, error) + + // NewListOptions returns an empty options object that will be used to pass extra options + // to the List method. It may return a bool and a string, if true, the + // value of the request path below the list will be included as the named + // string in the serialization of the runtime object. E.g., returning "path" + // will convert the trailing request scheme value to "path" in the map[string][]string + // passed to the converter. + NewListOptions() (runtime.Object, bool, string) +} diff --git a/pkg/registry/custom_metrics/reststorage.go b/pkg/registry/custom_metrics/reststorage.go index 61fb0563d..7a528a213 100644 --- a/pkg/registry/custom_metrics/reststorage.go +++ b/pkg/registry/custom_metrics/reststorage.go @@ -20,7 +20,9 @@ import ( "context" "fmt" + cm_rest "github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/apiserver/registry/rest" "github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider" + metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" @@ -37,7 +39,7 @@ type REST struct { } var _ rest.Storage = &REST{} -var _ rest.ListerWithOptions = &REST{} +var _ cm_rest.ListerWithOptions = &REST{} func NewREST(cmProvider provider.CustomMetricsProvider) *REST { return &REST{