2019-01-12 04:58:27 +00:00
/ *
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"
"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"
)
const (
ROUTE_META_GVK = "x-kubernetes-group-version-kind"
ROUTE_META_ACTION = "x-kubernetes-action"
)
type APIInstaller struct {
2019-04-07 17:07:55 +00:00
group * APIGroupVersion
prefix string // Path prefix where API resources are to be registered.
minRequestTimeout time . Duration
2019-01-12 04:58:27 +00:00
}
// 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 GetResourceKind ( groupVersion schema . GroupVersion , storage rest . Storage , typer runtime . ObjectTyper ) ( schema . GroupVersionKind , error ) {
// Let the storage tell us exactly what GVK it has
if gvkProvider , ok := storage . ( rest . GroupVersionKindProvider ) ; ok {
return gvkProvider . GroupVersionKind ( groupVersion ) , nil
}
object := storage . New ( )
fqKinds , _ , err := 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 == groupVersion . Group {
fqKindToRegister = 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 , 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
}
2019-04-07 17:07:55 +00:00
group , version := a . group . GroupVersion . Group , a . group . GroupVersion . Version
2019-01-12 04:58:27 +00:00
fqKindToRegister , err := GetResourceKind ( a . group . GroupVersion , storage , a . group . Typer )
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
}
versionedCreateOptions , err := a . group . Creater . New ( optionsExternalVersion . WithKind ( "CreateOptions" ) )
if err != nil {
return nil , err
}
2019-04-07 17:07:55 +00:00
versionedPatchOptions , err := a . group . Creater . New ( optionsExternalVersion . WithKind ( "PatchOptions" ) )
if err != nil {
return nil , err
}
2019-01-12 04:58:27 +00:00
versionedUpdateOptions , err := a . group . Creater . New ( optionsExternalVersion . WithKind ( "UpdateOptions" ) )
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
2019-04-07 17:07:55 +00:00
2019-01-12 04:58:27 +00:00
// 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 in 1.11
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 )
// DEPRECATED in 1.11
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 )
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 in 1.11
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 )
// DEPRECATED in 1.11
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 )
// DEPRECATED in 1.11
actions = appendIf ( actions , action { "WATCHLIST" , "watch/" + resource , params , namer , true } , allowWatchList )
}
}
// 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 ,
Authorizer : a . group . Authorizer ,
// 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 ,
HubGroupVersion : schema . GroupVersion { Group : fqKindToRegister . Group , Version : runtime . APIVersionInternal } ,
MetaGroupVersion : metav1 . SchemeGroupVersion ,
2019-03-04 01:22:32 +00:00
MaxRequestBodyBytes : a . group . MaxRequestBodyBytes ,
2019-01-12 04:58:27 +00:00
}
if a . group . MetaGroupVersion != nil {
reqScope . MetaGroupVersion = * a . group . MetaGroupVersion
}
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 := GetResourceKind ( a . group . GroupVersion , parentStorage , a . group . Typer )
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
2019-04-07 17:07:55 +00:00
handler = metrics . InstrumentRouteFunc ( verbOverrider . OverrideMetricsVerb ( action . Verb ) , group , version , resource , subresource , requestScope , metrics . APIServerComponent , handler )
2019-01-12 04:58:27 +00:00
} else {
2019-04-07 17:07:55 +00:00
handler = metrics . InstrumentRouteFunc ( action . Verb , group , version , resource , subresource , requestScope , metrics . APIServerComponent , handler )
2019-01-12 04:58:27 +00:00
}
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 {
2019-04-07 17:07:55 +00:00
if err := AddObjectParams ( ws , route , versionedGetOptions ) ; err != nil {
2019-01-12 04:58:27 +00:00
return nil , err
}
}
if isExporter {
2019-04-07 17:07:55 +00:00
if err := AddObjectParams ( ws , route , versionedExportOptions ) ; err != nil {
2019-01-12 04:58:27 +00:00
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
}
2019-04-07 17:07:55 +00:00
handler := metrics . InstrumentRouteFunc ( action . Verb , group , version , resource , subresource , requestScope , metrics . APIServerComponent , restfulListResource ( lister , watcher , reqScope , false , a . minRequestTimeout ) )
2019-01-12 04:58:27 +00:00
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 )
2019-04-07 17:07:55 +00:00
if err := AddObjectParams ( ws , route , versionedListOptions ) ; err != nil {
2019-01-12 04:58:27 +00:00
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
}
2019-04-07 17:07:55 +00:00
handler := metrics . InstrumentRouteFunc ( action . Verb , group , version , resource , subresource , requestScope , metrics . APIServerComponent , restfulUpdateResource ( updater , reqScope , admit ) )
2019-01-12 04:58:27 +00:00
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 )
2019-04-07 17:07:55 +00:00
if err := AddObjectParams ( ws , route , versionedUpdateOptions ) ; err != nil {
2019-01-12 04:58:27 +00:00
return nil , err
}
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 ) ,
}
2019-04-07 17:07:55 +00:00
handler := metrics . InstrumentRouteFunc ( action . Verb , group , version , resource , subresource , requestScope , metrics . APIServerComponent , restfulPatchResource ( patcher , reqScope , admit , supportedTypes ) )
2019-01-12 04:58:27 +00:00
route := ws . PATCH ( action . Path ) . To ( handler ) .
Doc ( doc ) .
Param ( ws . QueryParameter ( "pretty" , "If 'true', then the output is pretty printed." ) ) .
2019-04-07 17:07:55 +00:00
Consumes ( supportedTypes ... ) .
2019-01-12 04:58:27 +00:00
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 )
2019-04-07 17:07:55 +00:00
if err := AddObjectParams ( ws , route , versionedPatchOptions ) ; err != nil {
2019-01-12 04:58:27 +00:00
return nil , err
}
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 )
}
2019-04-07 17:07:55 +00:00
handler = metrics . InstrumentRouteFunc ( action . Verb , group , version , resource , subresource , requestScope , metrics . APIServerComponent , handler )
article := GetArticleForNoun ( kind , " " )
2019-01-12 04:58:27 +00:00
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 )
2019-04-07 17:07:55 +00:00
if err := AddObjectParams ( ws , route , versionedCreateOptions ) ; err != nil {
2019-01-12 04:58:27 +00:00
return nil , err
}
addParams ( route , action . Params )
routes = append ( routes , route )
case "DELETE" : // Delete a resource.
2019-04-07 17:07:55 +00:00
article := GetArticleForNoun ( kind , " " )
2019-01-12 04:58:27 +00:00
doc := "delete" + article + kind
if isSubresource {
doc = "delete " + subresource + " of" + article + kind
}
2019-04-07 17:07:55 +00:00
handler := metrics . InstrumentRouteFunc ( action . Verb , group , version , resource , subresource , requestScope , metrics . APIServerComponent , restfulDeleteResource ( gracefulDeleter , isGracefulDeleter , reqScope , admit ) )
2019-01-12 04:58:27 +00:00
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 ) .
Returns ( http . StatusAccepted , "Accepted" , versionedStatus )
if isGracefulDeleter {
route . Reads ( versionedDeleterObject )
route . ParameterNamed ( "body" ) . Required ( false )
2019-04-07 17:07:55 +00:00
if err := AddObjectParams ( ws , route , versionedDeleteOptions ) ; err != nil {
2019-01-12 04:58:27 +00:00
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
}
2019-04-07 17:07:55 +00:00
handler := metrics . InstrumentRouteFunc ( action . Verb , group , version , resource , subresource , requestScope , metrics . APIServerComponent , restfulDeleteCollection ( collectionDeleter , isCollectionDeleter , reqScope , admit ) )
2019-01-12 04:58:27 +00:00
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 )
2019-04-07 17:07:55 +00:00
if err := AddObjectParams ( ws , route , versionedListOptions ) ; err != nil {
2019-01-12 04:58:27 +00:00
return nil , err
}
addParams ( route , action . Params )
routes = append ( routes , route )
// deprecated in 1.11
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
}
doc += ". deprecated: use the 'watch' parameter with a list operation instead, filtered to a single item with the 'fieldSelector' parameter."
2019-04-07 17:07:55 +00:00
handler := metrics . InstrumentRouteFunc ( action . Verb , group , version , resource , subresource , requestScope , metrics . APIServerComponent , restfulListResource ( lister , watcher , reqScope , true , a . minRequestTimeout ) )
2019-01-12 04:58:27 +00:00
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 )
2019-04-07 17:07:55 +00:00
if err := AddObjectParams ( ws , route , versionedListOptions ) ; err != nil {
2019-01-12 04:58:27 +00:00
return nil , err
}
addParams ( route , action . Params )
routes = append ( routes , route )
// deprecated in 1.11
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
}
doc += ". deprecated: use the 'watch' parameter with a list operation instead."
2019-04-07 17:07:55 +00:00
handler := metrics . InstrumentRouteFunc ( action . Verb , group , version , resource , subresource , requestScope , metrics . APIServerComponent , restfulListResource ( lister , watcher , reqScope , true , a . minRequestTimeout ) )
2019-01-12 04:58:27 +00:00
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 )
2019-04-07 17:07:55 +00:00
if err := AddObjectParams ( ws , route , versionedListOptions ) ; err != nil {
2019-01-12 04:58:27 +00:00
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
}
2019-04-07 17:07:55 +00:00
handler := metrics . InstrumentRouteFunc ( action . Verb , group , version , resource , subresource , requestScope , metrics . APIServerComponent , restfulConnectResource ( connecter , reqScope , admit , path , isSubresource ) )
2019-01-12 04:58:27 +00:00
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 {
2019-04-07 17:07:55 +00:00
if err := AddObjectParams ( ws , route , versionedConnectOptions ) ; err != nil {
2019-01-12 04:58:27 +00:00
return nil , err
}
}
addParams ( route , action . Params )
routes = append ( routes , route )
// transform ConnectMethods to kube verbs
if kubeVerb , found := toDiscoveryKubeVerb [ method ] ; found {
if len ( kubeVerb ) != 0 {
kubeVerbs [ kubeVerb ] = struct { } { }
}
}
}
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
}
func addParams ( route * restful . RouteBuilder , params [ ] * restful . Parameter ) {
for _ , param := range params {
route . Param ( param )
}
}
2019-04-07 17:07:55 +00:00
// AddObjectParams converts a runtime.Object into a set of go-restful Param() definitions on the route.
2019-01-12 04:58:27 +00:00
// 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.
2019-04-07 17:07:55 +00:00
func AddObjectParams ( ws * restful . WebService , route * restful . RouteBuilder , obj interface { } ) error {
2019-01-12 04:58:27 +00:00
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
}
2019-04-07 17:07:55 +00:00
// GetArticleForNoun returns the article needed for the given noun.
func GetArticleForNoun ( noun string , padding string ) string {
2019-01-12 04:58:27 +00:00
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 )
}
}