func addProxyRoute(ws *restful.WebService, method string, prefix string, path string, proxyHandler http.Handler, namespaced, kind, resource, subresource string, hasSubresource bool, params []*restful.Parameter, operationSuffix string) { doc := "proxy " + method + " requests to " + kind if hasSubresource { doc = "proxy " + method + " requests to " + subresource + " of " + kind } handler := metrics.InstrumentRouteFunc("PROXY", resource, routeFunction(proxyHandler)) proxyRoute := ws.Method(method).Path(path).To(handler). Doc(doc). Operation("proxy" + strings.Title(method) + namespaced + kind + strings.Title(subresource) + operationSuffix). Produces("*/*"). Consumes("*/*"). Writes("string") addParams(proxyRoute, params) ws.Route(proxyRoute) }
func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storage, ws *restful.WebService, proxyHandler http.Handler) (*metav1.APIResource, error) { admit := a.group.Admit context := a.group.Context optionsExternalVersion := a.group.GroupVersion if a.group.OptionsExternalVersion != nil { optionsExternalVersion = *a.group.OptionsExternalVersion } resource, subresource, err := splitSubresource(path) if err != nil { return nil, err } mapping, err := a.restMapping(resource) 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 hasSubresource := len(subresource) > 0 // 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) deleter, isDeleter := storage.(rest.Deleter) 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) _, isRedirector := storage.(rest.Redirector) 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{} switch { case isGracefulDeleter: versionedDeleteOptions, err = a.group.Creater.New(optionsExternalVersion.WithKind("DeleteOptions")) if err != nil { return nil, err } versionedDeleterObject = indirectArbitraryPointer(versionedDeleteOptions) isDeleter = true case isDeleter: gracefulDeleter = rest.GracefulDeleteAdapter{Deleter: deleter} } 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(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(optionsExternalVersion.WithKind(connectOptionsInternalKind.Kind)) if err != nil { return nil, err } } } var ctxFn handlers.ContextFunc ctxFn = func(req *restful.Request) request.Context { if context == nil { return request.WithUserAgent(request.NewContext(), req.HeaderParameter("User-Agent")) } if ctx, ok := context.Get(req.Request); ok { return request.WithUserAgent(ctx, req.HeaderParameter("User-Agent")) } return request.WithUserAgent(request.NewContext(), req.HeaderParameter("User-Agent")) } allowWatchList := isWatcher && isLister // watching on lists is allowed only for kinds that support both watch and list. scope := mapping.Scope 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 } var apiResource metav1.APIResource // Get the list of actions for the given scope. switch scope.Name() { case meta.RESTScopeNameRoot: // Handle non-namespace scoped resources like nodes. resourcePath := resource resourceParams := params itemPath := resourcePath + "/{name}" nameParams := append(params, nameParam) proxyParams := append(nameParams, pathParam) suffix := "" if hasSubresource { suffix = "/" + subresource itemPath = itemPath + suffix resourcePath = itemPath resourceParams = nameParams } apiResource.Name = path apiResource.Namespaced = false apiResource.Kind = resourceKind namer := rootScopeNaming{scope, a.group.Linker, gpath.Join(a.prefix, resourcePath, "/"), 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}, isDeleter) actions = appendIf(actions, action{"WATCH", "watch/" + itemPath, nameParams, namer, false}, isWatcher) // We add "proxy" subresource to remove the need for the generic top level prefix proxy. // The generic top level prefix proxy is deprecated in v1.2, and will be removed in 1.3, or 1.4 at the latest. // TODO: DEPRECATED in v1.2. actions = appendIf(actions, action{"PROXY", "proxy/" + itemPath + "/{path:*}", proxyParams, namer, false}, isRedirector) // TODO: DEPRECATED in v1.2. actions = appendIf(actions, action{"PROXY", "proxy/" + itemPath, nameParams, namer, false}, isRedirector) actions = appendIf(actions, action{"CONNECT", itemPath, nameParams, namer, false}, isConnecter) actions = appendIf(actions, action{"CONNECT", itemPath + "/{path:*}", proxyParams, namer, false}, isConnecter && connectSubpath) break case meta.RESTScopeNameNamespace: // Handler for standard REST verbs (GET, PUT, POST and DELETE). namespaceParam := ws.PathParameter(scope.ArgumentName(), scope.ParamDescription()).DataType("string") namespacedPath := scope.ParamName() + "/{" + scope.ArgumentName() + "}/" + resource namespaceParams := []*restful.Parameter{namespaceParam} resourcePath := namespacedPath resourceParams := namespaceParams itemPathPrefix := gpath.Join(a.prefix, scope.ParamName()) + "/" itemPath := namespacedPath + "/{name}" itemPathMiddle := "/" + resource + "/" nameParams := append(namespaceParams, nameParam) proxyParams := append(nameParams, pathParam) itemPathSuffix := "" if hasSubresource { itemPathSuffix = "/" + subresource itemPath = itemPath + itemPathSuffix resourcePath = itemPath resourceParams = nameParams } apiResource.Name = path apiResource.Namespaced = true apiResource.Kind = resourceKind itemPathFn := func(name, namespace string) bytes.Buffer { var buf bytes.Buffer buf.WriteString(itemPathPrefix) buf.WriteString(url.QueryEscape(namespace)) buf.WriteString(itemPathMiddle) buf.WriteString(url.QueryEscape(name)) buf.WriteString(itemPathSuffix) return buf } namer := scopeNaming{scope, a.group.Linker, itemPathFn, false} 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}, isDeleter) actions = appendIf(actions, action{"WATCH", "watch/" + itemPath, nameParams, namer, false}, isWatcher) // We add "proxy" subresource to remove the need for the generic top level prefix proxy. // The generic top level prefix proxy is deprecated in v1.2, and will be removed in 1.3, or 1.4 at the latest. // TODO: DEPRECATED in v1.2. actions = appendIf(actions, action{"PROXY", "proxy/" + itemPath + "/{path:*}", proxyParams, namer, false}, isRedirector) // TODO: DEPRECATED in v1.2. actions = appendIf(actions, action{"PROXY", "proxy/" + itemPath, nameParams, namer, false}, isRedirector) 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 !hasSubresource { namer = scopeNaming{scope, a.group.Linker, itemPathFn, true} actions = appendIf(actions, action{"LIST", resource, params, namer, true}, isLister) actions = appendIf(actions, action{"WATCHLIST", "watch/" + resource, params, namer, true}, allowWatchList) } break default: return nil, fmt.Errorf("unsupported restscope: %s", scope.Name()) } // 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, // 422 (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{ ContextFunc: ctxFn, Serializer: a.group.Serializer, ParameterCodec: a.group.ParameterCodec, Creater: a.group.Creater, Convertor: a.group.Convertor, Copier: a.group.Copier, // 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, } for _, action := range actions { versionedObject := storageMeta.ProducesObject(action.Verb) if versionedObject == nil { versionedObject = defaultVersionedObject } reqScope.Namer = action.Namer namespaced := "" if apiResource.Namespaced { namespaced = "Namespaced" } operationSuffix := "" if strings.HasSuffix(action.Path, "/{path:*}") { operationSuffix = operationSuffix + "WithPath" } if action.AllNamespaces { 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) } switch action.Verb { case "GET": // Get a resource. var handler restful.RouteFunction if isGetterWithOptions { handler = handlers.GetResourceWithOptions(getterWithOptions, reqScope) } else { handler = handlers.GetResource(getter, exporter, reqScope) } handler = metrics.InstrumentRouteFunc(action.Verb, resource, handler) doc := "read the specified " + kind if hasSubresource { 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", versionedObject). Writes(versionedObject) 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) ws.Route(route) case "LIST": // List all resources of a kind. doc := "list objects of kind " + kind if hasSubresource { doc = "list " + subresource + " of objects of kind " + kind } handler := metrics.InstrumentRouteFunc(action.Verb, resource, handlers.ListResource(lister, watcher, reqScope, false, a.minRequestTimeout)) 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 hasSubresource { doc = "list or watch " + subresource + " of objects of kind " + kind } route.Doc(doc) case isWatcher: doc := "watch objects of kind " + kind if hasSubresource { doc = "watch " + subresource + "of objects of kind " + kind } route.Doc(doc) } addParams(route, action.Params) ws.Route(route) case "PUT": // Update a resource. doc := "replace the specified " + kind if hasSubresource { doc = "replace " + subresource + " of the specified " + kind } handler := metrics.InstrumentRouteFunc(action.Verb, resource, handlers.UpdateResource(updater, reqScope, a.group.Typer, 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", versionedObject). Reads(versionedObject). Writes(versionedObject) addParams(route, action.Params) ws.Route(route) case "PATCH": // Partially update a resource doc := "partially update the specified " + kind if hasSubresource { doc = "partially update " + subresource + " of the specified " + kind } handler := metrics.InstrumentRouteFunc(action.Verb, resource, handlers.PatchResource(patcher, reqScope, admit, mapping.ObjectConvertor)) 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", versionedObject). Reads(metav1.Patch{}). Writes(versionedObject) addParams(route, action.Params) ws.Route(route) case "POST": // Create a resource. var handler restful.RouteFunction if isNamedCreater { handler = handlers.CreateNamedResource(namedCreater, reqScope, a.group.Typer, admit) } else { handler = handlers.CreateResource(creater, reqScope, a.group.Typer, admit) } handler = metrics.InstrumentRouteFunc(action.Verb, resource, handler) article := getArticleForNoun(kind, " ") doc := "create" + article + kind if hasSubresource { 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", versionedObject). Reads(versionedObject). Writes(versionedObject) addParams(route, action.Params) ws.Route(route) case "DELETE": // Delete a resource. article := getArticleForNoun(kind, " ") doc := "delete" + article + kind if hasSubresource { doc = "delete " + subresource + " of" + article + kind } handler := metrics.InstrumentRouteFunc(action.Verb, resource, handlers.DeleteResource(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) ws.Route(route) case "DELETECOLLECTION": doc := "delete collection of " + kind if hasSubresource { doc = "delete collection of " + subresource + " of a " + kind } handler := metrics.InstrumentRouteFunc(action.Verb, resource, handlers.DeleteCollection(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) ws.Route(route) // TODO: deprecated case "WATCH": // Watch a resource. doc := "watch changes to an object of kind " + kind if hasSubresource { doc = "watch changes to " + subresource + " of an object of kind " + kind } handler := metrics.InstrumentRouteFunc(action.Verb, resource, handlers.ListResource(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) ws.Route(route) // TODO: deprecated case "WATCHLIST": // Watch all resources of a kind. doc := "watch individual changes to a list of " + kind if hasSubresource { doc = "watch individual changes to a list of " + subresource + " of " + kind } handler := metrics.InstrumentRouteFunc(action.Verb, resource, handlers.ListResource(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) ws.Route(route) // We add "proxy" subresource to remove the need for the generic top level prefix proxy. // The generic top level prefix proxy is deprecated in v1.2, and will be removed in 1.3, or 1.4 at the latest. // TODO: DEPRECATED in v1.2. case "PROXY": // Proxy requests to a resource. // Accept all methods as per http://issue.k8s.io/3996 addProxyRoute(ws, "GET", a.prefix, action.Path, proxyHandler, namespaced, kind, resource, subresource, hasSubresource, action.Params, operationSuffix) addProxyRoute(ws, "PUT", a.prefix, action.Path, proxyHandler, namespaced, kind, resource, subresource, hasSubresource, action.Params, operationSuffix) addProxyRoute(ws, "POST", a.prefix, action.Path, proxyHandler, namespaced, kind, resource, subresource, hasSubresource, action.Params, operationSuffix) addProxyRoute(ws, "DELETE", a.prefix, action.Path, proxyHandler, namespaced, kind, resource, subresource, hasSubresource, action.Params, operationSuffix) addProxyRoute(ws, "HEAD", a.prefix, action.Path, proxyHandler, namespaced, kind, resource, subresource, hasSubresource, action.Params, operationSuffix) addProxyRoute(ws, "OPTIONS", a.prefix, action.Path, proxyHandler, namespaced, kind, resource, subresource, hasSubresource, action.Params, operationSuffix) case "CONNECT": for _, method := range connecter.ConnectMethods() { doc := "connect " + method + " requests to " + kind if hasSubresource { doc = "connect " + method + " requests to " + subresource + " of " + kind } handler := metrics.InstrumentRouteFunc(action.Verb, resource, handlers.ConnectResource(connecter, reqScope, admit, path)) 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("string") if versionedConnectOptions != nil { if err := addObjectParams(ws, route, versionedConnectOptions); err != nil { return nil, err } } addParams(route, action.Params) ws.Route(route) } default: return nil, fmt.Errorf("unrecognized action verb: %s", action.Verb) } // 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) return &apiResource, nil }