// 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) (unversioned.GroupVersionKind, error) { if fqKindToRegister, ok := a.group.SubresourceGroupVersionKind[path]; ok { return fqKindToRegister, nil } object := storage.New() fqKinds, err := a.group.Typer.ObjectKinds(object) if err != nil { return unversioned.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 := unversioned.GroupVersionKind{} for _, fqKind := range fqKinds { if fqKind.Group == a.group.GroupVersion.Group { fqKindToRegister = a.group.GroupVersion.WithKind(fqKind.Kind) break } // TODO This keeps it doing what it was doing before, but it doesn't feel right. if fqKind.Group == extensions.GroupName && fqKind.Kind == "ThirdPartyResourceData" { fqKindToRegister = a.group.GroupVersion.WithKind(fqKind.Kind) } } if fqKindToRegister.IsEmpty() { return unversioned.GroupVersionKind{}, fmt.Errorf("unable to locate fully qualified kind for %v: found %v when registering for %v", reflect.TypeOf(object), fqKinds, a.group.GroupVersion) } return fqKindToRegister, nil }
func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storage, ws *restful.WebService, proxyHandler http.Handler) (*unversioned.APIResource, error) { admit := a.group.Admit context := a.group.Context optionsExternalVersion := a.group.GroupVersion if a.group.OptionsExternalVersion != nil { optionsExternalVersion = *a.group.OptionsExternalVersion } 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 nil, fmt.Errorf("api_installer allows only one or two segment paths (resource or resource/subresource)") } hasSubresource := len(subresource) > 0 object := storage.New() fqKinds, err := a.group.Typer.ObjectKinds(object) if err != nil { return nil, 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 := unversioned.GroupVersionKind{} for _, fqKind := range fqKinds { if fqKind.Group == a.group.GroupVersion.Group { fqKindToRegister = fqKind break } // TODO This keeps it doing what it was doing before, but it doesn't feel right. if fqKind.Group == extensions.GroupName && fqKind.Kind == "ThirdPartyResourceData" { fqKindToRegister = fqKind fqKindToRegister.Group = a.group.GroupVersion.Group fqKindToRegister.Version = a.group.GroupVersion.Version } } if fqKindToRegister.IsEmpty() { return nil, fmt.Errorf("unable to locate fully qualified kind for %v: found %v when registering for %v", reflect.TypeOf(object), fqKinds, a.group.GroupVersion) } kind := fqKindToRegister.Kind versionedPtr, err := a.group.Creater.New(a.group.GroupVersion.WithKind(kind)) if err != nil { return nil, err } versionedObject := indirectArbitraryPointer(versionedPtr) mapping, err := a.group.Mapper.RESTMapping(fqKindToRegister.GroupKind(), a.group.GroupVersion.Version) if err != nil { return nil, err } // subresources must have parent resources, and follow the namespacing rules of their parent if hasSubresource { parentStorage, ok := a.group.Storage[resource] if !ok { return nil, fmt.Errorf("subresources can only be declared when the parent is also registered: %s needs %s", path, resource) } parentObject := parentStorage.New() parentFQKinds, err := a.group.Typer.ObjectKinds(parentObject) if err != nil { return nil, 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 parentFQKindToRegister := unversioned.GroupVersionKind{} for _, fqKind := range parentFQKinds { if fqKind.Group == a.group.GroupVersion.Group { parentFQKindToRegister = fqKind break } } if parentFQKindToRegister.IsEmpty() { return nil, fmt.Errorf("unable to locate fully qualified kind for %v: found %v when registering for %v", reflect.TypeOf(object), fqKinds, a.group.GroupVersion) } parentMapping, err := a.group.Mapper.RESTMapping(parentFQKindToRegister.GroupKind(), a.group.GroupVersion.Version) if err != nil { return nil, err } mapping.Scope = parentMapping.Scope } // 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() listGVK, err := a.group.Typer.ObjectKind(list) versionedListPtr, err := a.group.Creater.New(a.group.GroupVersion.WithKind(listGVK.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 versionedDeleterObject interface{} switch { case isGracefulDeleter: objectPtr, err := a.group.Creater.New(optionsExternalVersion.WithKind("DeleteOptions")) if err != nil { return nil, err } versionedDeleterObject = indirectArbitraryPointer(objectPtr) 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 unversioned.GroupVersionKind getSubpath bool ) if isGetterWithOptions { getOptions, getSubpath, _ = getterWithOptions.NewGetOptions() getOptionsInternalKind, err = a.group.Typer.ObjectKind(getOptions) if err != nil { return nil, err } versionedGetOptions, err = a.group.Creater.New(optionsExternalVersion.WithKind(getOptionsInternalKind.Kind)) if err != nil { return nil, err } isGetter = true } var ( connectOptions runtime.Object versionedConnectOptions runtime.Object connectOptionsInternalKind unversioned.GroupVersionKind connectSubpath bool ) if isConnecter { connectOptions, connectSubpath, _ = connecter.NewConnectOptions() if connectOptions != nil { connectOptionsInternalKind, err = a.group.Typer.ObjectKind(connectOptions) if err != nil { return nil, err } versionedConnectOptions, err = a.group.Creater.New(optionsExternalVersion.WithKind(connectOptionsInternalKind.Kind)) } } var ctxFn ContextFunc ctxFn = func(req *restful.Request) api.Context { if context == nil { return api.NewContext() } if ctx, ok := context.Get(req.Request); ok { return ctx } return api.NewContext() } 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 apiResource unversioned.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) if hasSubresource { itemPath = itemPath + "/" + subresource resourcePath = itemPath resourceParams = nameParams } apiResource.Name = path apiResource.Namespaced = false namer := rootScopeNaming{scope, a.group.Linker, gpath.Join(a.prefix, itemPath)} // 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}, isLister) actions = appendIf(actions, action{"POST", resourcePath, resourceParams, namer}, isCreater) actions = appendIf(actions, action{"DELETECOLLECTION", resourcePath, resourceParams, namer}, isCollectionDeleter) // DEPRECATED actions = appendIf(actions, action{"WATCHLIST", "watch/" + resourcePath, resourceParams, namer}, allowWatchList) // Add actions at the item path: /api/apiVersion/resource/{name} actions = appendIf(actions, action{"GET", itemPath, nameParams, namer}, isGetter) if getSubpath { actions = appendIf(actions, action{"GET", itemPath + "/{path:*}", proxyParams, namer}, isGetter) } actions = appendIf(actions, action{"PUT", itemPath, nameParams, namer}, isUpdater) actions = appendIf(actions, action{"PATCH", itemPath, nameParams, namer}, isPatcher) actions = appendIf(actions, action{"DELETE", itemPath, nameParams, namer}, isDeleter) actions = appendIf(actions, action{"WATCH", "watch/" + itemPath, nameParams, namer}, isWatcher) actions = appendIf(actions, action{"PROXY", "proxy/" + itemPath + "/{path:*}", proxyParams, namer}, isRedirector) actions = appendIf(actions, action{"PROXY", "proxy/" + itemPath, nameParams, namer}, isRedirector) actions = appendIf(actions, action{"CONNECT", itemPath, nameParams, namer}, isConnecter) actions = appendIf(actions, action{"CONNECT", itemPath + "/{path:*}", proxyParams, namer}, 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 itemPath := namespacedPath + "/{name}" nameParams := append(namespaceParams, nameParam) proxyParams := append(nameParams, pathParam) if hasSubresource { itemPath = itemPath + "/" + subresource resourcePath = itemPath resourceParams = nameParams } apiResource.Name = path apiResource.Namespaced = true namer := scopeNaming{scope, a.group.Linker, gpath.Join(a.prefix, itemPath), false} actions = appendIf(actions, action{"LIST", resourcePath, resourceParams, namer}, isLister) actions = appendIf(actions, action{"POST", resourcePath, resourceParams, namer}, isCreater) actions = appendIf(actions, action{"DELETECOLLECTION", resourcePath, resourceParams, namer}, isCollectionDeleter) // DEPRECATED actions = appendIf(actions, action{"WATCHLIST", "watch/" + resourcePath, resourceParams, namer}, allowWatchList) actions = appendIf(actions, action{"GET", itemPath, nameParams, namer}, isGetter) if getSubpath { actions = appendIf(actions, action{"GET", itemPath + "/{path:*}", proxyParams, namer}, isGetter) } actions = appendIf(actions, action{"PUT", itemPath, nameParams, namer}, isUpdater) actions = appendIf(actions, action{"PATCH", itemPath, nameParams, namer}, isPatcher) actions = appendIf(actions, action{"DELETE", itemPath, nameParams, namer}, isDeleter) actions = appendIf(actions, action{"WATCH", "watch/" + itemPath, nameParams, namer}, isWatcher) actions = appendIf(actions, action{"PROXY", "proxy/" + itemPath + "/{path:*}", proxyParams, namer}, isRedirector) actions = appendIf(actions, action{"PROXY", "proxy/" + itemPath, nameParams, namer}, isRedirector) actions = appendIf(actions, action{"CONNECT", itemPath, nameParams, namer}, isConnecter) actions = appendIf(actions, action{"CONNECT", itemPath + "/{path:*}", proxyParams, namer}, 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, gpath.Join(a.prefix, itemPath), true} actions = appendIf(actions, action{"LIST", resource, params, namer}, isLister) actions = appendIf(actions, action{"WATCHLIST", "watch/" + resource, params, namer}, 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 reqScope := RequestScope{ ContextFunc: ctxFn, Serializer: a.group.Serializer, ParameterCodec: a.group.ParameterCodec, Creater: a.group.Creater, Convertor: a.group.Convertor, Resource: a.group.GroupVersion.WithResource(resource), Subresource: subresource, Kind: a.group.GroupVersion.WithKind(kind), } for _, action := range actions { reqScope.Namer = action.Namer m := monitorFilter(action.Verb, resource) namespaced := "" if strings.Contains(action.Path, scope.ArgumentName()) { namespaced = "Namespaced" } switch action.Verb { case "GET": // Get a resource. var handler restful.RouteFunction if isGetterWithOptions { handler = GetResourceWithOptions(getterWithOptions, reqScope) } else { handler = GetResource(getter, exporter, reqScope) } doc := "read the specified " + kind if hasSubresource { doc = "read " + subresource + " of the specified " + kind } route := ws.GET(action.Path).To(handler). Filter(m). Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Operation("read"+namespaced+kind+strings.Title(subresource)). Produces(append(storageMeta.ProducesMIMETypes(action.Verb), a.group.Serializer.SupportedMediaTypes()...)...). 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 } route := ws.GET(action.Path).To(ListResource(lister, watcher, reqScope, false, a.minRequestTimeout)). Filter(m). Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Operation("list"+namespaced+kind+strings.Title(subresource)). Produces(append(storageMeta.ProducesMIMETypes(action.Verb), a.group.Serializer.SupportedMediaTypes()...)...). 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 } route := ws.PUT(action.Path).To(UpdateResource(updater, reqScope, a.group.Typer, admit)). Filter(m). Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Operation("replace"+namespaced+kind+strings.Title(subresource)). Produces(append(storageMeta.ProducesMIMETypes(action.Verb), a.group.Serializer.SupportedMediaTypes()...)...). 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 } route := ws.PATCH(action.Path).To(PatchResource(patcher, reqScope, a.group.Typer, admit, mapping.ObjectConvertor)). Filter(m). Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Consumes(string(api.JSONPatchType), string(api.MergePatchType), string(api.StrategicMergePatchType)). Operation("patch"+namespaced+kind+strings.Title(subresource)). Produces(append(storageMeta.ProducesMIMETypes(action.Verb), a.group.Serializer.SupportedMediaTypes()...)...). Returns(http.StatusOK, "OK", versionedObject). Reads(unversioned.Patch{}). Writes(versionedObject) addParams(route, action.Params) ws.Route(route) case "POST": // Create a resource. var handler restful.RouteFunction if isNamedCreater { handler = CreateNamedResource(namedCreater, reqScope, a.group.Typer, admit) } else { handler = CreateResource(creater, reqScope, a.group.Typer, admit) } doc := "create a " + kind if hasSubresource { doc = "create " + subresource + " of a " + kind } route := ws.POST(action.Path).To(handler). Filter(m). Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Operation("create"+namespaced+kind+strings.Title(subresource)). Produces(append(storageMeta.ProducesMIMETypes(action.Verb), a.group.Serializer.SupportedMediaTypes()...)...). Returns(http.StatusOK, "OK", versionedObject). Reads(versionedObject). Writes(versionedObject) addParams(route, action.Params) ws.Route(route) case "DELETE": // Delete a resource. doc := "delete a " + kind if hasSubresource { doc = "delete " + subresource + " of a " + kind } route := ws.DELETE(action.Path).To(DeleteResource(gracefulDeleter, isGracefulDeleter, reqScope, admit)). Filter(m). Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Operation("delete"+namespaced+kind+strings.Title(subresource)). Produces(append(storageMeta.ProducesMIMETypes(action.Verb), a.group.Serializer.SupportedMediaTypes()...)...). Writes(versionedStatus). Returns(http.StatusOK, "OK", versionedStatus) if isGracefulDeleter { route.Reads(versionedDeleterObject) } addParams(route, action.Params) ws.Route(route) case "DELETECOLLECTION": doc := "delete collection of " + kind if hasSubresource { doc = "delete collection of " + subresource + " of a " + kind } route := ws.DELETE(action.Path).To(DeleteCollection(collectionDeleter, isCollectionDeleter, reqScope, admit)). Filter(m). Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Operation("deletecollection"+namespaced+kind+strings.Title(subresource)). Produces(append(storageMeta.ProducesMIMETypes(action.Verb), a.group.Serializer.SupportedMediaTypes()...)...). 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 } route := ws.GET(action.Path).To(ListResource(lister, watcher, reqScope, true, a.minRequestTimeout)). Filter(m). Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Operation("watch"+namespaced+kind+strings.Title(subresource)). Produces("application/json"). Returns(http.StatusOK, "OK", watchjson.WatchEvent{}). Writes(watchjson.WatchEvent{}) 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 } route := ws.GET(action.Path).To(ListResource(lister, watcher, reqScope, true, a.minRequestTimeout)). Filter(m). Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Operation("watch"+namespaced+kind+strings.Title(subresource)+"List"). Produces("application/json"). Returns(http.StatusOK, "OK", watchjson.WatchEvent{}). Writes(watchjson.WatchEvent{}) if err := addObjectParams(ws, route, versionedListOptions); err != nil { return nil, err } addParams(route, action.Params) ws.Route(route) 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) addProxyRoute(ws, "PUT", a.prefix, action.Path, proxyHandler, namespaced, kind, resource, subresource, hasSubresource, action.Params) addProxyRoute(ws, "POST", a.prefix, action.Path, proxyHandler, namespaced, kind, resource, subresource, hasSubresource, action.Params) addProxyRoute(ws, "DELETE", a.prefix, action.Path, proxyHandler, namespaced, kind, resource, subresource, hasSubresource, action.Params) addProxyRoute(ws, "HEAD", a.prefix, action.Path, proxyHandler, namespaced, kind, resource, subresource, hasSubresource, action.Params) addProxyRoute(ws, "OPTIONS", a.prefix, action.Path, proxyHandler, namespaced, kind, resource, subresource, hasSubresource, action.Params) case "CONNECT": for _, method := range connecter.ConnectMethods() { doc := "connect " + method + " requests to " + kind if hasSubresource { doc = "connect " + method + " requests to " + subresource + " of " + kind } route := ws.Method(method).Path(action.Path). To(ConnectResource(connecter, reqScope, admit, path)). Filter(m). Doc(doc). Operation("connect" + strings.Title(strings.ToLower(method)) + namespaced + kind + strings.Title(subresource)). 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 GetAttribs() when adding a custom handler. } return &apiResource, nil }