git subrepo clone (merge) https://github.com/kubernetes-incubator/metrics-server.git metrics-server
subrepo:
subdir: "metrics-server"
merged: "92d8412"
upstream:
origin: "https://github.com/kubernetes-incubator/metrics-server.git"
branch: "master"
commit: "92d8412"
git-subrepo:
version: "0.4.0"
origin: "???"
commit: "???"
diff --git a/metrics-server/vendor/k8s.io/apiserver/pkg/endpoints/installer.go b/metrics-server/vendor/k8s.io/apiserver/pkg/endpoints/installer.go
new file mode 100644
index 0000000..3edd09d
--- /dev/null
+++ b/metrics-server/vendor/k8s.io/apiserver/pkg/endpoints/installer.go
@@ -0,0 +1,1064 @@
+/*
+Copyright 2015 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 endpoints
+
+import (
+ "fmt"
+ "net/http"
+ gpath "path"
+ "reflect"
+ "sort"
+ "strings"
+ "time"
+ "unicode"
+
+ restful "github.com/emicklei/go-restful"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/conversion"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/apimachinery/pkg/types"
+ "k8s.io/apiserver/pkg/admission"
+ "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"
+ genericfilters "k8s.io/apiserver/pkg/server/filters"
+ utilopenapi "k8s.io/apiserver/pkg/util/openapi"
+ openapibuilder "k8s.io/kube-openapi/pkg/builder"
+)
+
+const (
+ ROUTE_META_GVK = "x-kubernetes-group-version-kind"
+ ROUTE_META_ACTION = "x-kubernetes-action"
+)
+
+type APIInstaller struct {
+ group *APIGroupVersion
+ prefix string // Path prefix where API resources are to be registered.
+ minRequestTimeout time.Duration
+ enableAPIResponseCompression bool
+}
+
+// Struct capturing information about an action ("GET", "POST", "WATCH", "PROXY", etc).
+type action struct {
+ Verb string // Verb identifying the action ("GET", "POST", "WATCH", "PROXY", etc).
+ Path string // The path of the action
+ Params []*restful.Parameter // List of parameters associated with the action.
+ Namer handlers.ScopeNamer
+ AllNamespaces bool // true iff the action is namespaced but works on aggregate result for all namespaces
+}
+
+// An interface to see if one storage supports override its default verb for monitoring
+type StorageMetricsOverride interface {
+ // OverrideMetricsVerb gives a storage object an opportunity to override the verb reported to the metrics endpoint
+ OverrideMetricsVerb(oldVerb string) (newVerb string)
+}
+
+// An interface to see if an object supports swagger documentation as a method
+type documentable interface {
+ SwaggerDoc() map[string]string
+}
+
+// toDiscoveryKubeVerb maps an action.Verb to the logical kube verb, used for discovery
+var toDiscoveryKubeVerb = map[string]string{
+ "CONNECT": "", // do not list in discovery.
+ "DELETE": "delete",
+ "DELETECOLLECTION": "deletecollection",
+ "GET": "get",
+ "LIST": "list",
+ "PATCH": "patch",
+ "POST": "create",
+ "PROXY": "proxy",
+ "PUT": "update",
+ "WATCH": "watch",
+ "WATCHLIST": "watch",
+}
+
+// Install handlers for API resources.
+func (a *APIInstaller) Install() ([]metav1.APIResource, *restful.WebService, []error) {
+ var apiResources []metav1.APIResource
+ var errors []error
+ ws := a.newWebService()
+
+ // Register the paths in a deterministic (sorted) order to get a deterministic swagger spec.
+ paths := make([]string, len(a.group.Storage))
+ var i int = 0
+ for path := range a.group.Storage {
+ paths[i] = path
+ i++
+ }
+ sort.Strings(paths)
+ for _, path := range paths {
+ apiResource, err := a.registerResourceHandlers(path, a.group.Storage[path], ws)
+ if err != nil {
+ errors = append(errors, fmt.Errorf("error in registering resource: %s, %v", path, err))
+ }
+ if apiResource != nil {
+ apiResources = append(apiResources, *apiResource)
+ }
+ }
+ return apiResources, ws, errors
+}
+
+// newWebService creates a new restful webservice with the api installer's prefix and version.
+func (a *APIInstaller) newWebService() *restful.WebService {
+ ws := new(restful.WebService)
+ ws.Path(a.prefix)
+ // a.prefix contains "prefix/group/version"
+ ws.Doc("API at " + a.prefix)
+ // Backwards compatibility, we accepted objects with empty content-type at V1.
+ // If we stop using go-restful, we can default empty content-type to application/json on an
+ // endpoint by endpoint basis
+ ws.Consumes("*/*")
+ mediaTypes, streamMediaTypes := negotiation.MediaTypesForSerializer(a.group.Serializer)
+ ws.Produces(append(mediaTypes, streamMediaTypes...)...)
+ ws.ApiVersion(a.group.GroupVersion.String())
+
+ return ws
+}
+
+// getResourceKind returns the external group version kind registered for the given storage
+// object. If the storage object is a subresource and has an override supplied for it, it returns
+// the group version kind supplied in the override.
+func (a *APIInstaller) getResourceKind(path string, storage rest.Storage) (schema.GroupVersionKind, error) {
+ // Let the storage tell us exactly what GVK it has
+ if gvkProvider, ok := storage.(rest.GroupVersionKindProvider); ok {
+ return gvkProvider.GroupVersionKind(a.group.GroupVersion), nil
+ }
+
+ object := storage.New()
+ fqKinds, _, err := a.group.Typer.ObjectKinds(object)
+ if err != nil {
+ return schema.GroupVersionKind{}, err
+ }
+
+ // a given go type can have multiple potential fully qualified kinds. Find the one that corresponds with the group
+ // we're trying to register here
+ fqKindToRegister := schema.GroupVersionKind{}
+ for _, fqKind := range fqKinds {
+ if fqKind.Group == a.group.GroupVersion.Group {
+ fqKindToRegister = a.group.GroupVersion.WithKind(fqKind.Kind)
+ break
+ }
+ }
+ if fqKindToRegister.Empty() {
+ return schema.GroupVersionKind{}, fmt.Errorf("unable to locate fully qualified kind for %v: found %v when registering for %v", reflect.TypeOf(object), fqKinds, a.group.GroupVersion)
+ }
+
+ // group is guaranteed to match based on the check above
+ return fqKindToRegister, nil
+}
+
+func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storage, ws *restful.WebService) (*metav1.APIResource, error) {
+ admit := a.group.Admit
+
+ optionsExternalVersion := a.group.GroupVersion
+ if a.group.OptionsExternalVersion != nil {
+ optionsExternalVersion = *a.group.OptionsExternalVersion
+ }
+
+ resource, subresource, err := splitSubresource(path)
+ if err != nil {
+ return nil, err
+ }
+
+ fqKindToRegister, err := a.getResourceKind(path, storage)
+ if err != nil {
+ return nil, err
+ }
+
+ versionedPtr, err := a.group.Creater.New(fqKindToRegister)
+ if err != nil {
+ return nil, err
+ }
+ defaultVersionedObject := indirectArbitraryPointer(versionedPtr)
+ kind := fqKindToRegister.Kind
+ isSubresource := len(subresource) > 0
+
+ // If there is a subresource, namespace scoping is defined by the parent resource
+ namespaceScoped := true
+ if isSubresource {
+ parentStorage, ok := a.group.Storage[resource]
+ if !ok {
+ return nil, fmt.Errorf("missing parent storage: %q", resource)
+ }
+ scoper, ok := parentStorage.(rest.Scoper)
+ if !ok {
+ return nil, fmt.Errorf("%q must implement scoper", resource)
+ }
+ namespaceScoped = scoper.NamespaceScoped()
+
+ } else {
+ scoper, ok := storage.(rest.Scoper)
+ if !ok {
+ return nil, fmt.Errorf("%q must implement scoper", resource)
+ }
+ namespaceScoped = scoper.NamespaceScoped()
+ }
+
+ // what verbs are supported by the storage, used to know what verbs we support per path
+ creater, isCreater := storage.(rest.Creater)
+ namedCreater, isNamedCreater := storage.(rest.NamedCreater)
+ lister, isLister := storage.(rest.Lister)
+ getter, isGetter := storage.(rest.Getter)
+ getterWithOptions, isGetterWithOptions := storage.(rest.GetterWithOptions)
+ gracefulDeleter, isGracefulDeleter := storage.(rest.GracefulDeleter)
+ collectionDeleter, isCollectionDeleter := storage.(rest.CollectionDeleter)
+ updater, isUpdater := storage.(rest.Updater)
+ patcher, isPatcher := storage.(rest.Patcher)
+ watcher, isWatcher := storage.(rest.Watcher)
+ connecter, isConnecter := storage.(rest.Connecter)
+ storageMeta, isMetadata := storage.(rest.StorageMetadata)
+ if !isMetadata {
+ storageMeta = defaultStorageMetadata{}
+ }
+ exporter, isExporter := storage.(rest.Exporter)
+ if !isExporter {
+ exporter = nil
+ }
+
+ versionedExportOptions, err := a.group.Creater.New(optionsExternalVersion.WithKind("ExportOptions"))
+ if err != nil {
+ return nil, err
+ }
+
+ if isNamedCreater {
+ isCreater = true
+ }
+
+ var versionedList interface{}
+ if isLister {
+ list := lister.NewList()
+ listGVKs, _, err := a.group.Typer.ObjectKinds(list)
+ if err != nil {
+ return nil, err
+ }
+ versionedListPtr, err := a.group.Creater.New(a.group.GroupVersion.WithKind(listGVKs[0].Kind))
+ if err != nil {
+ return nil, err
+ }
+ versionedList = indirectArbitraryPointer(versionedListPtr)
+ }
+
+ versionedListOptions, err := a.group.Creater.New(optionsExternalVersion.WithKind("ListOptions"))
+ if err != nil {
+ return nil, err
+ }
+
+ var versionedDeleteOptions runtime.Object
+ var versionedDeleterObject interface{}
+ if isGracefulDeleter {
+ versionedDeleteOptions, err = a.group.Creater.New(optionsExternalVersion.WithKind("DeleteOptions"))
+ if err != nil {
+ return nil, err
+ }
+ versionedDeleterObject = indirectArbitraryPointer(versionedDeleteOptions)
+ }
+
+ versionedStatusPtr, err := a.group.Creater.New(optionsExternalVersion.WithKind("Status"))
+ if err != nil {
+ return nil, err
+ }
+ versionedStatus := indirectArbitraryPointer(versionedStatusPtr)
+ var (
+ getOptions runtime.Object
+ versionedGetOptions runtime.Object
+ getOptionsInternalKind schema.GroupVersionKind
+ getSubpath bool
+ )
+ if isGetterWithOptions {
+ getOptions, getSubpath, _ = getterWithOptions.NewGetOptions()
+ getOptionsInternalKinds, _, err := a.group.Typer.ObjectKinds(getOptions)
+ if err != nil {
+ return nil, err
+ }
+ getOptionsInternalKind = getOptionsInternalKinds[0]
+ versionedGetOptions, err = a.group.Creater.New(a.group.GroupVersion.WithKind(getOptionsInternalKind.Kind))
+ if err != nil {
+ versionedGetOptions, err = a.group.Creater.New(optionsExternalVersion.WithKind(getOptionsInternalKind.Kind))
+ if err != nil {
+ return nil, err
+ }
+ }
+ isGetter = true
+ }
+
+ var versionedWatchEvent interface{}
+ if isWatcher {
+ versionedWatchEventPtr, err := a.group.Creater.New(a.group.GroupVersion.WithKind("WatchEvent"))
+ if err != nil {
+ return nil, err
+ }
+ versionedWatchEvent = indirectArbitraryPointer(versionedWatchEventPtr)
+ }
+
+ var (
+ connectOptions runtime.Object
+ versionedConnectOptions runtime.Object
+ connectOptionsInternalKind schema.GroupVersionKind
+ connectSubpath bool
+ )
+ if isConnecter {
+ connectOptions, connectSubpath, _ = connecter.NewConnectOptions()
+ if connectOptions != nil {
+ connectOptionsInternalKinds, _, err := a.group.Typer.ObjectKinds(connectOptions)
+ if err != nil {
+ return nil, err
+ }
+
+ connectOptionsInternalKind = connectOptionsInternalKinds[0]
+ versionedConnectOptions, err = a.group.Creater.New(a.group.GroupVersion.WithKind(connectOptionsInternalKind.Kind))
+ if err != nil {
+ versionedConnectOptions, err = a.group.Creater.New(optionsExternalVersion.WithKind(connectOptionsInternalKind.Kind))
+ if err != nil {
+ return nil, err
+ }
+ }
+ }
+ }
+
+ allowWatchList := isWatcher && isLister // watching on lists is allowed only for kinds that support both watch and list.
+ nameParam := ws.PathParameter("name", "name of the "+kind).DataType("string")
+ pathParam := ws.PathParameter("path", "path to the resource").DataType("string")
+
+ params := []*restful.Parameter{}
+ actions := []action{}
+
+ var resourceKind string
+ kindProvider, ok := storage.(rest.KindProvider)
+ if ok {
+ resourceKind = kindProvider.Kind()
+ } else {
+ resourceKind = kind
+ }
+
+ tableProvider, _ := storage.(rest.TableConvertor)
+
+ var apiResource metav1.APIResource
+ // Get the list of actions for the given scope.
+ switch {
+ case !namespaceScoped:
+ // Handle non-namespace scoped resources like nodes.
+ resourcePath := resource
+ resourceParams := params
+ itemPath := resourcePath + "/{name}"
+ nameParams := append(params, nameParam)
+ proxyParams := append(nameParams, pathParam)
+ suffix := ""
+ if isSubresource {
+ suffix = "/" + subresource
+ itemPath = itemPath + suffix
+ resourcePath = itemPath
+ resourceParams = nameParams
+ }
+ apiResource.Name = path
+ apiResource.Namespaced = false
+ apiResource.Kind = resourceKind
+ namer := handlers.ContextBasedNaming{
+ SelfLinker: a.group.Linker,
+ ClusterScoped: true,
+ SelfLinkPathPrefix: gpath.Join(a.prefix, resource) + "/",
+ SelfLinkPathSuffix: suffix,
+ }
+
+ // Handler for standard REST verbs (GET, PUT, POST and DELETE).
+ // Add actions at the resource path: /api/apiVersion/resource
+ actions = appendIf(actions, action{"LIST", resourcePath, resourceParams, namer, false}, isLister)
+ actions = appendIf(actions, action{"POST", resourcePath, resourceParams, namer, false}, isCreater)
+ actions = appendIf(actions, action{"DELETECOLLECTION", resourcePath, resourceParams, namer, false}, isCollectionDeleter)
+ // DEPRECATED
+ actions = appendIf(actions, action{"WATCHLIST", "watch/" + resourcePath, resourceParams, namer, false}, allowWatchList)
+
+ // Add actions at the item path: /api/apiVersion/resource/{name}
+ actions = appendIf(actions, action{"GET", itemPath, nameParams, namer, false}, isGetter)
+ if getSubpath {
+ actions = appendIf(actions, action{"GET", itemPath + "/{path:*}", proxyParams, namer, false}, isGetter)
+ }
+ actions = appendIf(actions, action{"PUT", itemPath, nameParams, namer, false}, isUpdater)
+ actions = appendIf(actions, action{"PATCH", itemPath, nameParams, namer, false}, isPatcher)
+ actions = appendIf(actions, action{"DELETE", itemPath, nameParams, namer, false}, isGracefulDeleter)
+ actions = appendIf(actions, action{"WATCH", "watch/" + itemPath, nameParams, namer, false}, isWatcher)
+ actions = appendIf(actions, action{"CONNECT", itemPath, nameParams, namer, false}, isConnecter)
+ actions = appendIf(actions, action{"CONNECT", itemPath + "/{path:*}", proxyParams, namer, false}, isConnecter && connectSubpath)
+ break
+ default:
+ namespaceParamName := "namespaces"
+ // Handler for standard REST verbs (GET, PUT, POST and DELETE).
+ namespaceParam := ws.PathParameter("namespace", "object name and auth scope, such as for teams and projects").DataType("string")
+ namespacedPath := namespaceParamName + "/{" + "namespace" + "}/" + resource
+ namespaceParams := []*restful.Parameter{namespaceParam}
+
+ resourcePath := namespacedPath
+ resourceParams := namespaceParams
+ itemPath := namespacedPath + "/{name}"
+ nameParams := append(namespaceParams, nameParam)
+ proxyParams := append(nameParams, pathParam)
+ itemPathSuffix := ""
+ if isSubresource {
+ itemPathSuffix = "/" + subresource
+ itemPath = itemPath + itemPathSuffix
+ resourcePath = itemPath
+ resourceParams = nameParams
+ }
+ apiResource.Name = path
+ apiResource.Namespaced = true
+ apiResource.Kind = resourceKind
+ namer := handlers.ContextBasedNaming{
+ SelfLinker: a.group.Linker,
+ ClusterScoped: false,
+ SelfLinkPathPrefix: gpath.Join(a.prefix, namespaceParamName) + "/",
+ SelfLinkPathSuffix: itemPathSuffix,
+ }
+
+ actions = appendIf(actions, action{"LIST", resourcePath, resourceParams, namer, false}, isLister)
+ actions = appendIf(actions, action{"POST", resourcePath, resourceParams, namer, false}, isCreater)
+ actions = appendIf(actions, action{"DELETECOLLECTION", resourcePath, resourceParams, namer, false}, isCollectionDeleter)
+ // DEPRECATED
+ actions = appendIf(actions, action{"WATCHLIST", "watch/" + resourcePath, resourceParams, namer, false}, allowWatchList)
+
+ actions = appendIf(actions, action{"GET", itemPath, nameParams, namer, false}, isGetter)
+ if getSubpath {
+ actions = appendIf(actions, action{"GET", itemPath + "/{path:*}", proxyParams, namer, false}, isGetter)
+ }
+ actions = appendIf(actions, action{"PUT", itemPath, nameParams, namer, false}, isUpdater)
+ actions = appendIf(actions, action{"PATCH", itemPath, nameParams, namer, false}, isPatcher)
+ actions = appendIf(actions, action{"DELETE", itemPath, nameParams, namer, false}, isGracefulDeleter)
+ actions = appendIf(actions, action{"WATCH", "watch/" + itemPath, nameParams, namer, false}, isWatcher)
+ actions = appendIf(actions, action{"CONNECT", itemPath, nameParams, namer, false}, isConnecter)
+ actions = appendIf(actions, action{"CONNECT", itemPath + "/{path:*}", proxyParams, namer, false}, isConnecter && connectSubpath)
+
+ // list or post across namespace.
+ // For ex: LIST all pods in all namespaces by sending a LIST request at /api/apiVersion/pods.
+ // TODO: more strongly type whether a resource allows these actions on "all namespaces" (bulk delete)
+ if !isSubresource {
+ actions = appendIf(actions, action{"LIST", resource, params, namer, true}, isLister)
+ actions = appendIf(actions, action{"WATCHLIST", "watch/" + resource, params, namer, true}, allowWatchList)
+ }
+ break
+ }
+
+ // Create Routes for the actions.
+ // TODO: Add status documentation using Returns()
+ // Errors (see api/errors/errors.go as well as go-restful router):
+ // http.StatusNotFound, http.StatusMethodNotAllowed,
+ // http.StatusUnsupportedMediaType, http.StatusNotAcceptable,
+ // http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden,
+ // http.StatusRequestTimeout, http.StatusConflict, http.StatusPreconditionFailed,
+ // http.StatusUnprocessableEntity, http.StatusInternalServerError,
+ // http.StatusServiceUnavailable
+ // and api error codes
+ // Note that if we specify a versioned Status object here, we may need to
+ // create one for the tests, also
+ // Success:
+ // http.StatusOK, http.StatusCreated, http.StatusAccepted, http.StatusNoContent
+ //
+ // test/integration/auth_test.go is currently the most comprehensive status code test
+
+ mediaTypes, streamMediaTypes := negotiation.MediaTypesForSerializer(a.group.Serializer)
+ allMediaTypes := append(mediaTypes, streamMediaTypes...)
+ ws.Produces(allMediaTypes...)
+
+ kubeVerbs := map[string]struct{}{}
+ reqScope := handlers.RequestScope{
+ Serializer: a.group.Serializer,
+ ParameterCodec: a.group.ParameterCodec,
+ Creater: a.group.Creater,
+ Convertor: a.group.Convertor,
+ Defaulter: a.group.Defaulter,
+ Typer: a.group.Typer,
+ UnsafeConvertor: a.group.UnsafeConvertor,
+
+ // TODO: Check for the interface on storage
+ TableConvertor: tableProvider,
+
+ // TODO: This seems wrong for cross-group subresources. It makes an assumption that a subresource and its parent are in the same group version. Revisit this.
+ Resource: a.group.GroupVersion.WithResource(resource),
+ Subresource: subresource,
+ Kind: fqKindToRegister,
+
+ MetaGroupVersion: metav1.SchemeGroupVersion,
+ }
+ if a.group.MetaGroupVersion != nil {
+ reqScope.MetaGroupVersion = *a.group.MetaGroupVersion
+ }
+ if a.group.OpenAPIConfig != nil {
+ openAPIDefinitions, err := openapibuilder.BuildOpenAPIDefinitionsForResource(defaultVersionedObject, a.group.OpenAPIConfig)
+ if err != nil {
+ return nil, fmt.Errorf("unable to build openapi definitions for %v: %v", fqKindToRegister, err)
+ }
+ reqScope.OpenAPISchema, err = utilopenapi.ToProtoSchema(openAPIDefinitions, fqKindToRegister)
+ if err != nil {
+ return nil, fmt.Errorf("unable to get openapi schema for %v: %v", fqKindToRegister, err)
+ }
+ }
+ for _, action := range actions {
+ producedObject := storageMeta.ProducesObject(action.Verb)
+ if producedObject == nil {
+ producedObject = defaultVersionedObject
+ }
+ reqScope.Namer = action.Namer
+
+ requestScope := "cluster"
+ var namespaced string
+ var operationSuffix string
+ if apiResource.Namespaced {
+ requestScope = "namespace"
+ namespaced = "Namespaced"
+ }
+ if strings.HasSuffix(action.Path, "/{path:*}") {
+ requestScope = "resource"
+ operationSuffix = operationSuffix + "WithPath"
+ }
+ if action.AllNamespaces {
+ requestScope = "cluster"
+ operationSuffix = operationSuffix + "ForAllNamespaces"
+ namespaced = ""
+ }
+
+ if kubeVerb, found := toDiscoveryKubeVerb[action.Verb]; found {
+ if len(kubeVerb) != 0 {
+ kubeVerbs[kubeVerb] = struct{}{}
+ }
+ } else {
+ return nil, fmt.Errorf("unknown action verb for discovery: %s", action.Verb)
+ }
+
+ routes := []*restful.RouteBuilder{}
+
+ // If there is a subresource, kind should be the parent's kind.
+ if isSubresource {
+ parentStorage, ok := a.group.Storage[resource]
+ if !ok {
+ return nil, fmt.Errorf("missing parent storage: %q", resource)
+ }
+
+ fqParentKind, err := a.getResourceKind(resource, parentStorage)
+ if err != nil {
+ return nil, err
+ }
+ kind = fqParentKind.Kind
+ }
+
+ verbOverrider, needOverride := storage.(StorageMetricsOverride)
+
+ switch action.Verb {
+ case "GET": // Get a resource.
+ var handler restful.RouteFunction
+ if isGetterWithOptions {
+ handler = restfulGetResourceWithOptions(getterWithOptions, reqScope, isSubresource)
+ } else {
+ handler = restfulGetResource(getter, exporter, reqScope)
+ }
+
+ if needOverride {
+ // need change the reported verb
+ handler = metrics.InstrumentRouteFunc(verbOverrider.OverrideMetricsVerb(action.Verb), resource, subresource, requestScope, handler)
+ } else {
+ handler = metrics.InstrumentRouteFunc(action.Verb, resource, subresource, requestScope, handler)
+ }
+
+ if a.enableAPIResponseCompression {
+ handler = genericfilters.RestfulWithCompression(handler)
+ }
+ doc := "read the specified " + kind
+ if isSubresource {
+ doc = "read " + subresource + " of the specified " + kind
+ }
+ route := ws.GET(action.Path).To(handler).
+ Doc(doc).
+ Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
+ Operation("read"+namespaced+kind+strings.Title(subresource)+operationSuffix).
+ Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...).
+ Returns(http.StatusOK, "OK", producedObject).
+ Writes(producedObject)
+ if isGetterWithOptions {
+ if err := addObjectParams(ws, route, versionedGetOptions); err != nil {
+ return nil, err
+ }
+ }
+ if isExporter {
+ if err := addObjectParams(ws, route, versionedExportOptions); err != nil {
+ return nil, err
+ }
+ }
+ addParams(route, action.Params)
+ routes = append(routes, route)
+ case "LIST": // List all resources of a kind.
+ doc := "list objects of kind " + kind
+ if isSubresource {
+ doc = "list " + subresource + " of objects of kind " + kind
+ }
+ handler := metrics.InstrumentRouteFunc(action.Verb, resource, subresource, requestScope, restfulListResource(lister, watcher, reqScope, false, a.minRequestTimeout))
+ if a.enableAPIResponseCompression {
+ handler = genericfilters.RestfulWithCompression(handler)
+ }
+ route := ws.GET(action.Path).To(handler).
+ Doc(doc).
+ Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
+ Operation("list"+namespaced+kind+strings.Title(subresource)+operationSuffix).
+ Produces(append(storageMeta.ProducesMIMETypes(action.Verb), allMediaTypes...)...).
+ Returns(http.StatusOK, "OK", versionedList).
+ Writes(versionedList)
+ if err := addObjectParams(ws, route, versionedListOptions); err != nil {
+ return nil, err
+ }
+ switch {
+ case isLister && isWatcher:
+ doc := "list or watch objects of kind " + kind
+ if isSubresource {
+ doc = "list or watch " + subresource + " of objects of kind " + kind
+ }
+ route.Doc(doc)
+ case isWatcher:
+ doc := "watch objects of kind " + kind
+ if isSubresource {
+ doc = "watch " + subresource + "of objects of kind " + kind
+ }
+ route.Doc(doc)
+ }
+ addParams(route, action.Params)
+ routes = append(routes, route)
+ case "PUT": // Update a resource.
+ doc := "replace the specified " + kind
+ if isSubresource {
+ doc = "replace " + subresource + " of the specified " + kind
+ }
+ handler := metrics.InstrumentRouteFunc(action.Verb, resource, subresource, requestScope, restfulUpdateResource(updater, reqScope, admit))
+ route := ws.PUT(action.Path).To(handler).
+ Doc(doc).
+ Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
+ Operation("replace"+namespaced+kind+strings.Title(subresource)+operationSuffix).
+ Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...).
+ Returns(http.StatusOK, "OK", producedObject).
+ // TODO: in some cases, the API may return a v1.Status instead of the versioned object
+ // but currently go-restful can't handle multiple different objects being returned.
+ Returns(http.StatusCreated, "Created", producedObject).
+ Reads(defaultVersionedObject).
+ Writes(producedObject)
+ addParams(route, action.Params)
+ routes = append(routes, route)
+ case "PATCH": // Partially update a resource
+ doc := "partially update the specified " + kind
+ if isSubresource {
+ doc = "partially update " + subresource + " of the specified " + kind
+ }
+ supportedTypes := []string{
+ string(types.JSONPatchType),
+ string(types.MergePatchType),
+ string(types.StrategicMergePatchType),
+ }
+ handler := metrics.InstrumentRouteFunc(action.Verb, resource, subresource, requestScope, restfulPatchResource(patcher, reqScope, admit, supportedTypes))
+ route := ws.PATCH(action.Path).To(handler).
+ Doc(doc).
+ Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
+ Consumes(string(types.JSONPatchType), string(types.MergePatchType), string(types.StrategicMergePatchType)).
+ Operation("patch"+namespaced+kind+strings.Title(subresource)+operationSuffix).
+ Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...).
+ Returns(http.StatusOK, "OK", producedObject).
+ Reads(metav1.Patch{}).
+ Writes(producedObject)
+ addParams(route, action.Params)
+ routes = append(routes, route)
+ case "POST": // Create a resource.
+ var handler restful.RouteFunction
+ if isNamedCreater {
+ handler = restfulCreateNamedResource(namedCreater, reqScope, admit)
+ } else {
+ handler = restfulCreateResource(creater, reqScope, admit)
+ }
+ handler = metrics.InstrumentRouteFunc(action.Verb, resource, subresource, requestScope, handler)
+ article := getArticleForNoun(kind, " ")
+ doc := "create" + article + kind
+ if isSubresource {
+ doc = "create " + subresource + " of" + article + kind
+ }
+ route := ws.POST(action.Path).To(handler).
+ Doc(doc).
+ Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
+ Operation("create"+namespaced+kind+strings.Title(subresource)+operationSuffix).
+ Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...).
+ Returns(http.StatusOK, "OK", producedObject).
+ // TODO: in some cases, the API may return a v1.Status instead of the versioned object
+ // but currently go-restful can't handle multiple different objects being returned.
+ Returns(http.StatusCreated, "Created", producedObject).
+ Returns(http.StatusAccepted, "Accepted", producedObject).
+ Reads(defaultVersionedObject).
+ Writes(producedObject)
+ addParams(route, action.Params)
+ routes = append(routes, route)
+ case "DELETE": // Delete a resource.
+ article := getArticleForNoun(kind, " ")
+ doc := "delete" + article + kind
+ if isSubresource {
+ doc = "delete " + subresource + " of" + article + kind
+ }
+ handler := metrics.InstrumentRouteFunc(action.Verb, resource, subresource, requestScope, restfulDeleteResource(gracefulDeleter, isGracefulDeleter, reqScope, admit))
+ route := ws.DELETE(action.Path).To(handler).
+ Doc(doc).
+ Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
+ Operation("delete"+namespaced+kind+strings.Title(subresource)+operationSuffix).
+ Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...).
+ Writes(versionedStatus).
+ Returns(http.StatusOK, "OK", versionedStatus)
+ if isGracefulDeleter {
+ route.Reads(versionedDeleterObject)
+ if err := addObjectParams(ws, route, versionedDeleteOptions); err != nil {
+ return nil, err
+ }
+ }
+ addParams(route, action.Params)
+ routes = append(routes, route)
+ case "DELETECOLLECTION":
+ doc := "delete collection of " + kind
+ if isSubresource {
+ doc = "delete collection of " + subresource + " of a " + kind
+ }
+ handler := metrics.InstrumentRouteFunc(action.Verb, resource, subresource, requestScope, restfulDeleteCollection(collectionDeleter, isCollectionDeleter, reqScope, admit))
+ route := ws.DELETE(action.Path).To(handler).
+ Doc(doc).
+ Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
+ Operation("deletecollection"+namespaced+kind+strings.Title(subresource)+operationSuffix).
+ Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...).
+ Writes(versionedStatus).
+ Returns(http.StatusOK, "OK", versionedStatus)
+ if err := addObjectParams(ws, route, versionedListOptions); err != nil {
+ return nil, err
+ }
+ addParams(route, action.Params)
+ routes = append(routes, route)
+ // TODO: deprecated
+ case "WATCH": // Watch a resource.
+ doc := "watch changes to an object of kind " + kind
+ if isSubresource {
+ doc = "watch changes to " + subresource + " of an object of kind " + kind
+ }
+ handler := metrics.InstrumentRouteFunc(action.Verb, resource, subresource, requestScope, restfulListResource(lister, watcher, reqScope, true, a.minRequestTimeout))
+ route := ws.GET(action.Path).To(handler).
+ Doc(doc).
+ Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
+ Operation("watch"+namespaced+kind+strings.Title(subresource)+operationSuffix).
+ Produces(allMediaTypes...).
+ Returns(http.StatusOK, "OK", versionedWatchEvent).
+ Writes(versionedWatchEvent)
+ if err := addObjectParams(ws, route, versionedListOptions); err != nil {
+ return nil, err
+ }
+ addParams(route, action.Params)
+ routes = append(routes, route)
+ // TODO: deprecated
+ case "WATCHLIST": // Watch all resources of a kind.
+ doc := "watch individual changes to a list of " + kind
+ if isSubresource {
+ doc = "watch individual changes to a list of " + subresource + " of " + kind
+ }
+ handler := metrics.InstrumentRouteFunc(action.Verb, resource, subresource, requestScope, restfulListResource(lister, watcher, reqScope, true, a.minRequestTimeout))
+ route := ws.GET(action.Path).To(handler).
+ Doc(doc).
+ Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
+ Operation("watch"+namespaced+kind+strings.Title(subresource)+"List"+operationSuffix).
+ Produces(allMediaTypes...).
+ Returns(http.StatusOK, "OK", versionedWatchEvent).
+ Writes(versionedWatchEvent)
+ if err := addObjectParams(ws, route, versionedListOptions); err != nil {
+ return nil, err
+ }
+ addParams(route, action.Params)
+ routes = append(routes, route)
+ case "CONNECT":
+ for _, method := range connecter.ConnectMethods() {
+ connectProducedObject := storageMeta.ProducesObject(method)
+ if connectProducedObject == nil {
+ connectProducedObject = "string"
+ }
+ doc := "connect " + method + " requests to " + kind
+ if isSubresource {
+ doc = "connect " + method + " requests to " + subresource + " of " + kind
+ }
+ handler := metrics.InstrumentRouteFunc(action.Verb, resource, subresource, requestScope, restfulConnectResource(connecter, reqScope, admit, path, isSubresource))
+ route := ws.Method(method).Path(action.Path).
+ To(handler).
+ Doc(doc).
+ Operation("connect" + strings.Title(strings.ToLower(method)) + namespaced + kind + strings.Title(subresource) + operationSuffix).
+ Produces("*/*").
+ Consumes("*/*").
+ Writes(connectProducedObject)
+ if versionedConnectOptions != nil {
+ if err := addObjectParams(ws, route, versionedConnectOptions); err != nil {
+ return nil, err
+ }
+ }
+ addParams(route, action.Params)
+ routes = append(routes, route)
+ }
+ default:
+ return nil, fmt.Errorf("unrecognized action verb: %s", action.Verb)
+ }
+ for _, route := range routes {
+ route.Metadata(ROUTE_META_GVK, metav1.GroupVersionKind{
+ Group: reqScope.Kind.Group,
+ Version: reqScope.Kind.Version,
+ Kind: reqScope.Kind.Kind,
+ })
+ route.Metadata(ROUTE_META_ACTION, strings.ToLower(action.Verb))
+ ws.Route(route)
+ }
+ // Note: update GetAuthorizerAttributes() when adding a custom handler.
+ }
+
+ apiResource.Verbs = make([]string, 0, len(kubeVerbs))
+ for kubeVerb := range kubeVerbs {
+ apiResource.Verbs = append(apiResource.Verbs, kubeVerb)
+ }
+ sort.Strings(apiResource.Verbs)
+
+ if shortNamesProvider, ok := storage.(rest.ShortNamesProvider); ok {
+ apiResource.ShortNames = shortNamesProvider.ShortNames()
+ }
+ if categoriesProvider, ok := storage.(rest.CategoriesProvider); ok {
+ apiResource.Categories = categoriesProvider.Categories()
+ }
+ if gvkProvider, ok := storage.(rest.GroupVersionKindProvider); ok {
+ gvk := gvkProvider.GroupVersionKind(a.group.GroupVersion)
+ apiResource.Group = gvk.Group
+ apiResource.Version = gvk.Version
+ apiResource.Kind = gvk.Kind
+ }
+
+ return &apiResource, nil
+}
+
+// indirectArbitraryPointer returns *ptrToObject for an arbitrary pointer
+func indirectArbitraryPointer(ptrToObject interface{}) interface{} {
+ return reflect.Indirect(reflect.ValueOf(ptrToObject)).Interface()
+}
+
+func appendIf(actions []action, a action, shouldAppend bool) []action {
+ if shouldAppend {
+ actions = append(actions, a)
+ }
+ return actions
+}
+
+// Wraps a http.Handler function inside a restful.RouteFunction
+func routeFunction(handler http.Handler) restful.RouteFunction {
+ return func(restReq *restful.Request, restResp *restful.Response) {
+ handler.ServeHTTP(restResp.ResponseWriter, restReq.Request)
+ }
+}
+
+func addParams(route *restful.RouteBuilder, params []*restful.Parameter) {
+ for _, param := range params {
+ route.Param(param)
+ }
+}
+
+// addObjectParams converts a runtime.Object into a set of go-restful Param() definitions on the route.
+// The object must be a pointer to a struct; only fields at the top level of the struct that are not
+// themselves interfaces or structs are used; only fields with a json tag that is non empty (the standard
+// Go JSON behavior for omitting a field) become query parameters. The name of the query parameter is
+// the JSON field name. If a description struct tag is set on the field, that description is used on the
+// query parameter. In essence, it converts a standard JSON top level object into a query param schema.
+func addObjectParams(ws *restful.WebService, route *restful.RouteBuilder, obj interface{}) error {
+ sv, err := conversion.EnforcePtr(obj)
+ if err != nil {
+ return err
+ }
+ st := sv.Type()
+ switch st.Kind() {
+ case reflect.Struct:
+ for i := 0; i < st.NumField(); i++ {
+ name := st.Field(i).Name
+ sf, ok := st.FieldByName(name)
+ if !ok {
+ continue
+ }
+ switch sf.Type.Kind() {
+ case reflect.Interface, reflect.Struct:
+ case reflect.Ptr:
+ // TODO: This is a hack to let metav1.Time through. This needs to be fixed in a more generic way eventually. bug #36191
+ if (sf.Type.Elem().Kind() == reflect.Interface || sf.Type.Elem().Kind() == reflect.Struct) && strings.TrimPrefix(sf.Type.String(), "*") != "metav1.Time" {
+ continue
+ }
+ fallthrough
+ default:
+ jsonTag := sf.Tag.Get("json")
+ if len(jsonTag) == 0 {
+ continue
+ }
+ jsonName := strings.SplitN(jsonTag, ",", 2)[0]
+ if len(jsonName) == 0 {
+ continue
+ }
+
+ var desc string
+ if docable, ok := obj.(documentable); ok {
+ desc = docable.SwaggerDoc()[jsonName]
+ }
+ route.Param(ws.QueryParameter(jsonName, desc).DataType(typeToJSON(sf.Type.String())))
+ }
+ }
+ }
+ return nil
+}
+
+// TODO: this is incomplete, expand as needed.
+// Convert the name of a golang type to the name of a JSON type
+func typeToJSON(typeName string) string {
+ switch typeName {
+ case "bool", "*bool":
+ return "boolean"
+ case "uint8", "*uint8", "int", "*int", "int32", "*int32", "int64", "*int64", "uint32", "*uint32", "uint64", "*uint64":
+ return "integer"
+ case "float64", "*float64", "float32", "*float32":
+ return "number"
+ case "metav1.Time", "*metav1.Time":
+ return "string"
+ case "byte", "*byte":
+ return "string"
+ case "v1.DeletionPropagation", "*v1.DeletionPropagation":
+ return "string"
+
+ // TODO: Fix these when go-restful supports a way to specify an array query param:
+ // https://github.com/emicklei/go-restful/issues/225
+ case "[]string", "[]*string":
+ return "string"
+ case "[]int32", "[]*int32":
+ return "integer"
+
+ default:
+ return typeName
+ }
+}
+
+// defaultStorageMetadata provides default answers to rest.StorageMetadata.
+type defaultStorageMetadata struct{}
+
+// defaultStorageMetadata implements rest.StorageMetadata
+var _ rest.StorageMetadata = defaultStorageMetadata{}
+
+func (defaultStorageMetadata) ProducesMIMETypes(verb string) []string {
+ return nil
+}
+
+func (defaultStorageMetadata) ProducesObject(verb string) interface{} {
+ return nil
+}
+
+// splitSubresource checks if the given storage path is the path of a subresource and returns
+// the resource and subresource components.
+func splitSubresource(path string) (string, string, error) {
+ var resource, subresource string
+ switch parts := strings.Split(path, "/"); len(parts) {
+ case 2:
+ resource, subresource = parts[0], parts[1]
+ case 1:
+ resource = parts[0]
+ default:
+ // TODO: support deeper paths
+ return "", "", fmt.Errorf("api_installer allows only one or two segment paths (resource or resource/subresource)")
+ }
+ return resource, subresource, nil
+}
+
+// getArticleForNoun returns the article needed for the given noun.
+func getArticleForNoun(noun string, padding string) string {
+ if noun[len(noun)-2:] != "ss" && noun[len(noun)-1:] == "s" {
+ // Plurals don't have an article.
+ // Don't catch words like class
+ return fmt.Sprintf("%v", padding)
+ }
+
+ article := "a"
+ if isVowel(rune(noun[0])) {
+ article = "an"
+ }
+
+ return fmt.Sprintf("%s%s%s", padding, article, padding)
+}
+
+// isVowel returns true if the rune is a vowel (case insensitive).
+func isVowel(c rune) bool {
+ vowels := []rune{'a', 'e', 'i', 'o', 'u'}
+ for _, value := range vowels {
+ if value == unicode.ToLower(c) {
+ return true
+ }
+ }
+ return false
+}
+
+func restfulListResource(r rest.Lister, rw rest.Watcher, scope handlers.RequestScope, forceWatch bool, minRequestTimeout time.Duration) restful.RouteFunction {
+ return func(req *restful.Request, res *restful.Response) {
+ handlers.ListResource(r, rw, scope, forceWatch, minRequestTimeout)(res.ResponseWriter, req.Request)
+ }
+}
+
+func restfulCreateNamedResource(r rest.NamedCreater, scope handlers.RequestScope, admit admission.Interface) restful.RouteFunction {
+ return func(req *restful.Request, res *restful.Response) {
+ handlers.CreateNamedResource(r, scope, admit)(res.ResponseWriter, req.Request)
+ }
+}
+
+func restfulCreateResource(r rest.Creater, scope handlers.RequestScope, admit admission.Interface) restful.RouteFunction {
+ return func(req *restful.Request, res *restful.Response) {
+ handlers.CreateResource(r, scope, admit)(res.ResponseWriter, req.Request)
+ }
+}
+
+func restfulDeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope handlers.RequestScope, admit admission.Interface) restful.RouteFunction {
+ return func(req *restful.Request, res *restful.Response) {
+ handlers.DeleteResource(r, allowsOptions, scope, admit)(res.ResponseWriter, req.Request)
+ }
+}
+
+func restfulDeleteCollection(r rest.CollectionDeleter, checkBody bool, scope handlers.RequestScope, admit admission.Interface) restful.RouteFunction {
+ return func(req *restful.Request, res *restful.Response) {
+ handlers.DeleteCollection(r, checkBody, scope, admit)(res.ResponseWriter, req.Request)
+ }
+}
+
+func restfulUpdateResource(r rest.Updater, scope handlers.RequestScope, admit admission.Interface) restful.RouteFunction {
+ return func(req *restful.Request, res *restful.Response) {
+ handlers.UpdateResource(r, scope, admit)(res.ResponseWriter, req.Request)
+ }
+}
+
+func restfulPatchResource(r rest.Patcher, scope handlers.RequestScope, admit admission.Interface, supportedTypes []string) restful.RouteFunction {
+ return func(req *restful.Request, res *restful.Response) {
+ handlers.PatchResource(r, scope, admit, supportedTypes)(res.ResponseWriter, req.Request)
+ }
+}
+
+func restfulGetResource(r rest.Getter, e rest.Exporter, scope handlers.RequestScope) restful.RouteFunction {
+ return func(req *restful.Request, res *restful.Response) {
+ handlers.GetResource(r, e, scope)(res.ResponseWriter, req.Request)
+ }
+}
+
+func restfulGetResourceWithOptions(r rest.GetterWithOptions, scope handlers.RequestScope, isSubresource bool) restful.RouteFunction {
+ return func(req *restful.Request, res *restful.Response) {
+ handlers.GetResourceWithOptions(r, scope, isSubresource)(res.ResponseWriter, req.Request)
+ }
+}
+
+func restfulConnectResource(connecter rest.Connecter, scope handlers.RequestScope, admit admission.Interface, restPath string, isSubresource bool) restful.RouteFunction {
+ return func(req *restful.Request, res *restful.Response) {
+ handlers.ConnectResource(connecter, scope, admit, restPath, isSubresource)(res.ResponseWriter, req.Request)
+ }
+}