blob: 3edd09dcdf92c8caa209778cc784faa784bf58d0 [file] [log] [blame]
Matthias Andreas Benkard832a54e2019-01-29 09:27:38 +01001/*
2Copyright 2015 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package endpoints
18
19import (
20 "fmt"
21 "net/http"
22 gpath "path"
23 "reflect"
24 "sort"
25 "strings"
26 "time"
27 "unicode"
28
29 restful "github.com/emicklei/go-restful"
30
31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32 "k8s.io/apimachinery/pkg/conversion"
33 "k8s.io/apimachinery/pkg/runtime"
34 "k8s.io/apimachinery/pkg/runtime/schema"
35 "k8s.io/apimachinery/pkg/types"
36 "k8s.io/apiserver/pkg/admission"
37 "k8s.io/apiserver/pkg/endpoints/handlers"
38 "k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
39 "k8s.io/apiserver/pkg/endpoints/metrics"
40 "k8s.io/apiserver/pkg/registry/rest"
41 genericfilters "k8s.io/apiserver/pkg/server/filters"
42 utilopenapi "k8s.io/apiserver/pkg/util/openapi"
43 openapibuilder "k8s.io/kube-openapi/pkg/builder"
44)
45
46const (
47 ROUTE_META_GVK = "x-kubernetes-group-version-kind"
48 ROUTE_META_ACTION = "x-kubernetes-action"
49)
50
51type APIInstaller struct {
52 group *APIGroupVersion
53 prefix string // Path prefix where API resources are to be registered.
54 minRequestTimeout time.Duration
55 enableAPIResponseCompression bool
56}
57
58// Struct capturing information about an action ("GET", "POST", "WATCH", "PROXY", etc).
59type action struct {
60 Verb string // Verb identifying the action ("GET", "POST", "WATCH", "PROXY", etc).
61 Path string // The path of the action
62 Params []*restful.Parameter // List of parameters associated with the action.
63 Namer handlers.ScopeNamer
64 AllNamespaces bool // true iff the action is namespaced but works on aggregate result for all namespaces
65}
66
67// An interface to see if one storage supports override its default verb for monitoring
68type StorageMetricsOverride interface {
69 // OverrideMetricsVerb gives a storage object an opportunity to override the verb reported to the metrics endpoint
70 OverrideMetricsVerb(oldVerb string) (newVerb string)
71}
72
73// An interface to see if an object supports swagger documentation as a method
74type documentable interface {
75 SwaggerDoc() map[string]string
76}
77
78// toDiscoveryKubeVerb maps an action.Verb to the logical kube verb, used for discovery
79var toDiscoveryKubeVerb = map[string]string{
80 "CONNECT": "", // do not list in discovery.
81 "DELETE": "delete",
82 "DELETECOLLECTION": "deletecollection",
83 "GET": "get",
84 "LIST": "list",
85 "PATCH": "patch",
86 "POST": "create",
87 "PROXY": "proxy",
88 "PUT": "update",
89 "WATCH": "watch",
90 "WATCHLIST": "watch",
91}
92
93// Install handlers for API resources.
94func (a *APIInstaller) Install() ([]metav1.APIResource, *restful.WebService, []error) {
95 var apiResources []metav1.APIResource
96 var errors []error
97 ws := a.newWebService()
98
99 // Register the paths in a deterministic (sorted) order to get a deterministic swagger spec.
100 paths := make([]string, len(a.group.Storage))
101 var i int = 0
102 for path := range a.group.Storage {
103 paths[i] = path
104 i++
105 }
106 sort.Strings(paths)
107 for _, path := range paths {
108 apiResource, err := a.registerResourceHandlers(path, a.group.Storage[path], ws)
109 if err != nil {
110 errors = append(errors, fmt.Errorf("error in registering resource: %s, %v", path, err))
111 }
112 if apiResource != nil {
113 apiResources = append(apiResources, *apiResource)
114 }
115 }
116 return apiResources, ws, errors
117}
118
119// newWebService creates a new restful webservice with the api installer's prefix and version.
120func (a *APIInstaller) newWebService() *restful.WebService {
121 ws := new(restful.WebService)
122 ws.Path(a.prefix)
123 // a.prefix contains "prefix/group/version"
124 ws.Doc("API at " + a.prefix)
125 // Backwards compatibility, we accepted objects with empty content-type at V1.
126 // If we stop using go-restful, we can default empty content-type to application/json on an
127 // endpoint by endpoint basis
128 ws.Consumes("*/*")
129 mediaTypes, streamMediaTypes := negotiation.MediaTypesForSerializer(a.group.Serializer)
130 ws.Produces(append(mediaTypes, streamMediaTypes...)...)
131 ws.ApiVersion(a.group.GroupVersion.String())
132
133 return ws
134}
135
136// getResourceKind returns the external group version kind registered for the given storage
137// object. If the storage object is a subresource and has an override supplied for it, it returns
138// the group version kind supplied in the override.
139func (a *APIInstaller) getResourceKind(path string, storage rest.Storage) (schema.GroupVersionKind, error) {
140 // Let the storage tell us exactly what GVK it has
141 if gvkProvider, ok := storage.(rest.GroupVersionKindProvider); ok {
142 return gvkProvider.GroupVersionKind(a.group.GroupVersion), nil
143 }
144
145 object := storage.New()
146 fqKinds, _, err := a.group.Typer.ObjectKinds(object)
147 if err != nil {
148 return schema.GroupVersionKind{}, err
149 }
150
151 // a given go type can have multiple potential fully qualified kinds. Find the one that corresponds with the group
152 // we're trying to register here
153 fqKindToRegister := schema.GroupVersionKind{}
154 for _, fqKind := range fqKinds {
155 if fqKind.Group == a.group.GroupVersion.Group {
156 fqKindToRegister = a.group.GroupVersion.WithKind(fqKind.Kind)
157 break
158 }
159 }
160 if fqKindToRegister.Empty() {
161 return schema.GroupVersionKind{}, fmt.Errorf("unable to locate fully qualified kind for %v: found %v when registering for %v", reflect.TypeOf(object), fqKinds, a.group.GroupVersion)
162 }
163
164 // group is guaranteed to match based on the check above
165 return fqKindToRegister, nil
166}
167
168func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storage, ws *restful.WebService) (*metav1.APIResource, error) {
169 admit := a.group.Admit
170
171 optionsExternalVersion := a.group.GroupVersion
172 if a.group.OptionsExternalVersion != nil {
173 optionsExternalVersion = *a.group.OptionsExternalVersion
174 }
175
176 resource, subresource, err := splitSubresource(path)
177 if err != nil {
178 return nil, err
179 }
180
181 fqKindToRegister, err := a.getResourceKind(path, storage)
182 if err != nil {
183 return nil, err
184 }
185
186 versionedPtr, err := a.group.Creater.New(fqKindToRegister)
187 if err != nil {
188 return nil, err
189 }
190 defaultVersionedObject := indirectArbitraryPointer(versionedPtr)
191 kind := fqKindToRegister.Kind
192 isSubresource := len(subresource) > 0
193
194 // If there is a subresource, namespace scoping is defined by the parent resource
195 namespaceScoped := true
196 if isSubresource {
197 parentStorage, ok := a.group.Storage[resource]
198 if !ok {
199 return nil, fmt.Errorf("missing parent storage: %q", resource)
200 }
201 scoper, ok := parentStorage.(rest.Scoper)
202 if !ok {
203 return nil, fmt.Errorf("%q must implement scoper", resource)
204 }
205 namespaceScoped = scoper.NamespaceScoped()
206
207 } else {
208 scoper, ok := storage.(rest.Scoper)
209 if !ok {
210 return nil, fmt.Errorf("%q must implement scoper", resource)
211 }
212 namespaceScoped = scoper.NamespaceScoped()
213 }
214
215 // what verbs are supported by the storage, used to know what verbs we support per path
216 creater, isCreater := storage.(rest.Creater)
217 namedCreater, isNamedCreater := storage.(rest.NamedCreater)
218 lister, isLister := storage.(rest.Lister)
219 getter, isGetter := storage.(rest.Getter)
220 getterWithOptions, isGetterWithOptions := storage.(rest.GetterWithOptions)
221 gracefulDeleter, isGracefulDeleter := storage.(rest.GracefulDeleter)
222 collectionDeleter, isCollectionDeleter := storage.(rest.CollectionDeleter)
223 updater, isUpdater := storage.(rest.Updater)
224 patcher, isPatcher := storage.(rest.Patcher)
225 watcher, isWatcher := storage.(rest.Watcher)
226 connecter, isConnecter := storage.(rest.Connecter)
227 storageMeta, isMetadata := storage.(rest.StorageMetadata)
228 if !isMetadata {
229 storageMeta = defaultStorageMetadata{}
230 }
231 exporter, isExporter := storage.(rest.Exporter)
232 if !isExporter {
233 exporter = nil
234 }
235
236 versionedExportOptions, err := a.group.Creater.New(optionsExternalVersion.WithKind("ExportOptions"))
237 if err != nil {
238 return nil, err
239 }
240
241 if isNamedCreater {
242 isCreater = true
243 }
244
245 var versionedList interface{}
246 if isLister {
247 list := lister.NewList()
248 listGVKs, _, err := a.group.Typer.ObjectKinds(list)
249 if err != nil {
250 return nil, err
251 }
252 versionedListPtr, err := a.group.Creater.New(a.group.GroupVersion.WithKind(listGVKs[0].Kind))
253 if err != nil {
254 return nil, err
255 }
256 versionedList = indirectArbitraryPointer(versionedListPtr)
257 }
258
259 versionedListOptions, err := a.group.Creater.New(optionsExternalVersion.WithKind("ListOptions"))
260 if err != nil {
261 return nil, err
262 }
263
264 var versionedDeleteOptions runtime.Object
265 var versionedDeleterObject interface{}
266 if isGracefulDeleter {
267 versionedDeleteOptions, err = a.group.Creater.New(optionsExternalVersion.WithKind("DeleteOptions"))
268 if err != nil {
269 return nil, err
270 }
271 versionedDeleterObject = indirectArbitraryPointer(versionedDeleteOptions)
272 }
273
274 versionedStatusPtr, err := a.group.Creater.New(optionsExternalVersion.WithKind("Status"))
275 if err != nil {
276 return nil, err
277 }
278 versionedStatus := indirectArbitraryPointer(versionedStatusPtr)
279 var (
280 getOptions runtime.Object
281 versionedGetOptions runtime.Object
282 getOptionsInternalKind schema.GroupVersionKind
283 getSubpath bool
284 )
285 if isGetterWithOptions {
286 getOptions, getSubpath, _ = getterWithOptions.NewGetOptions()
287 getOptionsInternalKinds, _, err := a.group.Typer.ObjectKinds(getOptions)
288 if err != nil {
289 return nil, err
290 }
291 getOptionsInternalKind = getOptionsInternalKinds[0]
292 versionedGetOptions, err = a.group.Creater.New(a.group.GroupVersion.WithKind(getOptionsInternalKind.Kind))
293 if err != nil {
294 versionedGetOptions, err = a.group.Creater.New(optionsExternalVersion.WithKind(getOptionsInternalKind.Kind))
295 if err != nil {
296 return nil, err
297 }
298 }
299 isGetter = true
300 }
301
302 var versionedWatchEvent interface{}
303 if isWatcher {
304 versionedWatchEventPtr, err := a.group.Creater.New(a.group.GroupVersion.WithKind("WatchEvent"))
305 if err != nil {
306 return nil, err
307 }
308 versionedWatchEvent = indirectArbitraryPointer(versionedWatchEventPtr)
309 }
310
311 var (
312 connectOptions runtime.Object
313 versionedConnectOptions runtime.Object
314 connectOptionsInternalKind schema.GroupVersionKind
315 connectSubpath bool
316 )
317 if isConnecter {
318 connectOptions, connectSubpath, _ = connecter.NewConnectOptions()
319 if connectOptions != nil {
320 connectOptionsInternalKinds, _, err := a.group.Typer.ObjectKinds(connectOptions)
321 if err != nil {
322 return nil, err
323 }
324
325 connectOptionsInternalKind = connectOptionsInternalKinds[0]
326 versionedConnectOptions, err = a.group.Creater.New(a.group.GroupVersion.WithKind(connectOptionsInternalKind.Kind))
327 if err != nil {
328 versionedConnectOptions, err = a.group.Creater.New(optionsExternalVersion.WithKind(connectOptionsInternalKind.Kind))
329 if err != nil {
330 return nil, err
331 }
332 }
333 }
334 }
335
336 allowWatchList := isWatcher && isLister // watching on lists is allowed only for kinds that support both watch and list.
337 nameParam := ws.PathParameter("name", "name of the "+kind).DataType("string")
338 pathParam := ws.PathParameter("path", "path to the resource").DataType("string")
339
340 params := []*restful.Parameter{}
341 actions := []action{}
342
343 var resourceKind string
344 kindProvider, ok := storage.(rest.KindProvider)
345 if ok {
346 resourceKind = kindProvider.Kind()
347 } else {
348 resourceKind = kind
349 }
350
351 tableProvider, _ := storage.(rest.TableConvertor)
352
353 var apiResource metav1.APIResource
354 // Get the list of actions for the given scope.
355 switch {
356 case !namespaceScoped:
357 // Handle non-namespace scoped resources like nodes.
358 resourcePath := resource
359 resourceParams := params
360 itemPath := resourcePath + "/{name}"
361 nameParams := append(params, nameParam)
362 proxyParams := append(nameParams, pathParam)
363 suffix := ""
364 if isSubresource {
365 suffix = "/" + subresource
366 itemPath = itemPath + suffix
367 resourcePath = itemPath
368 resourceParams = nameParams
369 }
370 apiResource.Name = path
371 apiResource.Namespaced = false
372 apiResource.Kind = resourceKind
373 namer := handlers.ContextBasedNaming{
374 SelfLinker: a.group.Linker,
375 ClusterScoped: true,
376 SelfLinkPathPrefix: gpath.Join(a.prefix, resource) + "/",
377 SelfLinkPathSuffix: suffix,
378 }
379
380 // Handler for standard REST verbs (GET, PUT, POST and DELETE).
381 // Add actions at the resource path: /api/apiVersion/resource
382 actions = appendIf(actions, action{"LIST", resourcePath, resourceParams, namer, false}, isLister)
383 actions = appendIf(actions, action{"POST", resourcePath, resourceParams, namer, false}, isCreater)
384 actions = appendIf(actions, action{"DELETECOLLECTION", resourcePath, resourceParams, namer, false}, isCollectionDeleter)
385 // DEPRECATED
386 actions = appendIf(actions, action{"WATCHLIST", "watch/" + resourcePath, resourceParams, namer, false}, allowWatchList)
387
388 // Add actions at the item path: /api/apiVersion/resource/{name}
389 actions = appendIf(actions, action{"GET", itemPath, nameParams, namer, false}, isGetter)
390 if getSubpath {
391 actions = appendIf(actions, action{"GET", itemPath + "/{path:*}", proxyParams, namer, false}, isGetter)
392 }
393 actions = appendIf(actions, action{"PUT", itemPath, nameParams, namer, false}, isUpdater)
394 actions = appendIf(actions, action{"PATCH", itemPath, nameParams, namer, false}, isPatcher)
395 actions = appendIf(actions, action{"DELETE", itemPath, nameParams, namer, false}, isGracefulDeleter)
396 actions = appendIf(actions, action{"WATCH", "watch/" + itemPath, nameParams, namer, false}, isWatcher)
397 actions = appendIf(actions, action{"CONNECT", itemPath, nameParams, namer, false}, isConnecter)
398 actions = appendIf(actions, action{"CONNECT", itemPath + "/{path:*}", proxyParams, namer, false}, isConnecter && connectSubpath)
399 break
400 default:
401 namespaceParamName := "namespaces"
402 // Handler for standard REST verbs (GET, PUT, POST and DELETE).
403 namespaceParam := ws.PathParameter("namespace", "object name and auth scope, such as for teams and projects").DataType("string")
404 namespacedPath := namespaceParamName + "/{" + "namespace" + "}/" + resource
405 namespaceParams := []*restful.Parameter{namespaceParam}
406
407 resourcePath := namespacedPath
408 resourceParams := namespaceParams
409 itemPath := namespacedPath + "/{name}"
410 nameParams := append(namespaceParams, nameParam)
411 proxyParams := append(nameParams, pathParam)
412 itemPathSuffix := ""
413 if isSubresource {
414 itemPathSuffix = "/" + subresource
415 itemPath = itemPath + itemPathSuffix
416 resourcePath = itemPath
417 resourceParams = nameParams
418 }
419 apiResource.Name = path
420 apiResource.Namespaced = true
421 apiResource.Kind = resourceKind
422 namer := handlers.ContextBasedNaming{
423 SelfLinker: a.group.Linker,
424 ClusterScoped: false,
425 SelfLinkPathPrefix: gpath.Join(a.prefix, namespaceParamName) + "/",
426 SelfLinkPathSuffix: itemPathSuffix,
427 }
428
429 actions = appendIf(actions, action{"LIST", resourcePath, resourceParams, namer, false}, isLister)
430 actions = appendIf(actions, action{"POST", resourcePath, resourceParams, namer, false}, isCreater)
431 actions = appendIf(actions, action{"DELETECOLLECTION", resourcePath, resourceParams, namer, false}, isCollectionDeleter)
432 // DEPRECATED
433 actions = appendIf(actions, action{"WATCHLIST", "watch/" + resourcePath, resourceParams, namer, false}, allowWatchList)
434
435 actions = appendIf(actions, action{"GET", itemPath, nameParams, namer, false}, isGetter)
436 if getSubpath {
437 actions = appendIf(actions, action{"GET", itemPath + "/{path:*}", proxyParams, namer, false}, isGetter)
438 }
439 actions = appendIf(actions, action{"PUT", itemPath, nameParams, namer, false}, isUpdater)
440 actions = appendIf(actions, action{"PATCH", itemPath, nameParams, namer, false}, isPatcher)
441 actions = appendIf(actions, action{"DELETE", itemPath, nameParams, namer, false}, isGracefulDeleter)
442 actions = appendIf(actions, action{"WATCH", "watch/" + itemPath, nameParams, namer, false}, isWatcher)
443 actions = appendIf(actions, action{"CONNECT", itemPath, nameParams, namer, false}, isConnecter)
444 actions = appendIf(actions, action{"CONNECT", itemPath + "/{path:*}", proxyParams, namer, false}, isConnecter && connectSubpath)
445
446 // list or post across namespace.
447 // For ex: LIST all pods in all namespaces by sending a LIST request at /api/apiVersion/pods.
448 // TODO: more strongly type whether a resource allows these actions on "all namespaces" (bulk delete)
449 if !isSubresource {
450 actions = appendIf(actions, action{"LIST", resource, params, namer, true}, isLister)
451 actions = appendIf(actions, action{"WATCHLIST", "watch/" + resource, params, namer, true}, allowWatchList)
452 }
453 break
454 }
455
456 // Create Routes for the actions.
457 // TODO: Add status documentation using Returns()
458 // Errors (see api/errors/errors.go as well as go-restful router):
459 // http.StatusNotFound, http.StatusMethodNotAllowed,
460 // http.StatusUnsupportedMediaType, http.StatusNotAcceptable,
461 // http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden,
462 // http.StatusRequestTimeout, http.StatusConflict, http.StatusPreconditionFailed,
463 // http.StatusUnprocessableEntity, http.StatusInternalServerError,
464 // http.StatusServiceUnavailable
465 // and api error codes
466 // Note that if we specify a versioned Status object here, we may need to
467 // create one for the tests, also
468 // Success:
469 // http.StatusOK, http.StatusCreated, http.StatusAccepted, http.StatusNoContent
470 //
471 // test/integration/auth_test.go is currently the most comprehensive status code test
472
473 mediaTypes, streamMediaTypes := negotiation.MediaTypesForSerializer(a.group.Serializer)
474 allMediaTypes := append(mediaTypes, streamMediaTypes...)
475 ws.Produces(allMediaTypes...)
476
477 kubeVerbs := map[string]struct{}{}
478 reqScope := handlers.RequestScope{
479 Serializer: a.group.Serializer,
480 ParameterCodec: a.group.ParameterCodec,
481 Creater: a.group.Creater,
482 Convertor: a.group.Convertor,
483 Defaulter: a.group.Defaulter,
484 Typer: a.group.Typer,
485 UnsafeConvertor: a.group.UnsafeConvertor,
486
487 // TODO: Check for the interface on storage
488 TableConvertor: tableProvider,
489
490 // 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.
491 Resource: a.group.GroupVersion.WithResource(resource),
492 Subresource: subresource,
493 Kind: fqKindToRegister,
494
495 MetaGroupVersion: metav1.SchemeGroupVersion,
496 }
497 if a.group.MetaGroupVersion != nil {
498 reqScope.MetaGroupVersion = *a.group.MetaGroupVersion
499 }
500 if a.group.OpenAPIConfig != nil {
501 openAPIDefinitions, err := openapibuilder.BuildOpenAPIDefinitionsForResource(defaultVersionedObject, a.group.OpenAPIConfig)
502 if err != nil {
503 return nil, fmt.Errorf("unable to build openapi definitions for %v: %v", fqKindToRegister, err)
504 }
505 reqScope.OpenAPISchema, err = utilopenapi.ToProtoSchema(openAPIDefinitions, fqKindToRegister)
506 if err != nil {
507 return nil, fmt.Errorf("unable to get openapi schema for %v: %v", fqKindToRegister, err)
508 }
509 }
510 for _, action := range actions {
511 producedObject := storageMeta.ProducesObject(action.Verb)
512 if producedObject == nil {
513 producedObject = defaultVersionedObject
514 }
515 reqScope.Namer = action.Namer
516
517 requestScope := "cluster"
518 var namespaced string
519 var operationSuffix string
520 if apiResource.Namespaced {
521 requestScope = "namespace"
522 namespaced = "Namespaced"
523 }
524 if strings.HasSuffix(action.Path, "/{path:*}") {
525 requestScope = "resource"
526 operationSuffix = operationSuffix + "WithPath"
527 }
528 if action.AllNamespaces {
529 requestScope = "cluster"
530 operationSuffix = operationSuffix + "ForAllNamespaces"
531 namespaced = ""
532 }
533
534 if kubeVerb, found := toDiscoveryKubeVerb[action.Verb]; found {
535 if len(kubeVerb) != 0 {
536 kubeVerbs[kubeVerb] = struct{}{}
537 }
538 } else {
539 return nil, fmt.Errorf("unknown action verb for discovery: %s", action.Verb)
540 }
541
542 routes := []*restful.RouteBuilder{}
543
544 // If there is a subresource, kind should be the parent's kind.
545 if isSubresource {
546 parentStorage, ok := a.group.Storage[resource]
547 if !ok {
548 return nil, fmt.Errorf("missing parent storage: %q", resource)
549 }
550
551 fqParentKind, err := a.getResourceKind(resource, parentStorage)
552 if err != nil {
553 return nil, err
554 }
555 kind = fqParentKind.Kind
556 }
557
558 verbOverrider, needOverride := storage.(StorageMetricsOverride)
559
560 switch action.Verb {
561 case "GET": // Get a resource.
562 var handler restful.RouteFunction
563 if isGetterWithOptions {
564 handler = restfulGetResourceWithOptions(getterWithOptions, reqScope, isSubresource)
565 } else {
566 handler = restfulGetResource(getter, exporter, reqScope)
567 }
568
569 if needOverride {
570 // need change the reported verb
571 handler = metrics.InstrumentRouteFunc(verbOverrider.OverrideMetricsVerb(action.Verb), resource, subresource, requestScope, handler)
572 } else {
573 handler = metrics.InstrumentRouteFunc(action.Verb, resource, subresource, requestScope, handler)
574 }
575
576 if a.enableAPIResponseCompression {
577 handler = genericfilters.RestfulWithCompression(handler)
578 }
579 doc := "read the specified " + kind
580 if isSubresource {
581 doc = "read " + subresource + " of the specified " + kind
582 }
583 route := ws.GET(action.Path).To(handler).
584 Doc(doc).
585 Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
586 Operation("read"+namespaced+kind+strings.Title(subresource)+operationSuffix).
587 Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...).
588 Returns(http.StatusOK, "OK", producedObject).
589 Writes(producedObject)
590 if isGetterWithOptions {
591 if err := addObjectParams(ws, route, versionedGetOptions); err != nil {
592 return nil, err
593 }
594 }
595 if isExporter {
596 if err := addObjectParams(ws, route, versionedExportOptions); err != nil {
597 return nil, err
598 }
599 }
600 addParams(route, action.Params)
601 routes = append(routes, route)
602 case "LIST": // List all resources of a kind.
603 doc := "list objects of kind " + kind
604 if isSubresource {
605 doc = "list " + subresource + " of objects of kind " + kind
606 }
607 handler := metrics.InstrumentRouteFunc(action.Verb, resource, subresource, requestScope, restfulListResource(lister, watcher, reqScope, false, a.minRequestTimeout))
608 if a.enableAPIResponseCompression {
609 handler = genericfilters.RestfulWithCompression(handler)
610 }
611 route := ws.GET(action.Path).To(handler).
612 Doc(doc).
613 Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
614 Operation("list"+namespaced+kind+strings.Title(subresource)+operationSuffix).
615 Produces(append(storageMeta.ProducesMIMETypes(action.Verb), allMediaTypes...)...).
616 Returns(http.StatusOK, "OK", versionedList).
617 Writes(versionedList)
618 if err := addObjectParams(ws, route, versionedListOptions); err != nil {
619 return nil, err
620 }
621 switch {
622 case isLister && isWatcher:
623 doc := "list or watch objects of kind " + kind
624 if isSubresource {
625 doc = "list or watch " + subresource + " of objects of kind " + kind
626 }
627 route.Doc(doc)
628 case isWatcher:
629 doc := "watch objects of kind " + kind
630 if isSubresource {
631 doc = "watch " + subresource + "of objects of kind " + kind
632 }
633 route.Doc(doc)
634 }
635 addParams(route, action.Params)
636 routes = append(routes, route)
637 case "PUT": // Update a resource.
638 doc := "replace the specified " + kind
639 if isSubresource {
640 doc = "replace " + subresource + " of the specified " + kind
641 }
642 handler := metrics.InstrumentRouteFunc(action.Verb, resource, subresource, requestScope, restfulUpdateResource(updater, reqScope, admit))
643 route := ws.PUT(action.Path).To(handler).
644 Doc(doc).
645 Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
646 Operation("replace"+namespaced+kind+strings.Title(subresource)+operationSuffix).
647 Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...).
648 Returns(http.StatusOK, "OK", producedObject).
649 // TODO: in some cases, the API may return a v1.Status instead of the versioned object
650 // but currently go-restful can't handle multiple different objects being returned.
651 Returns(http.StatusCreated, "Created", producedObject).
652 Reads(defaultVersionedObject).
653 Writes(producedObject)
654 addParams(route, action.Params)
655 routes = append(routes, route)
656 case "PATCH": // Partially update a resource
657 doc := "partially update the specified " + kind
658 if isSubresource {
659 doc = "partially update " + subresource + " of the specified " + kind
660 }
661 supportedTypes := []string{
662 string(types.JSONPatchType),
663 string(types.MergePatchType),
664 string(types.StrategicMergePatchType),
665 }
666 handler := metrics.InstrumentRouteFunc(action.Verb, resource, subresource, requestScope, restfulPatchResource(patcher, reqScope, admit, supportedTypes))
667 route := ws.PATCH(action.Path).To(handler).
668 Doc(doc).
669 Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
670 Consumes(string(types.JSONPatchType), string(types.MergePatchType), string(types.StrategicMergePatchType)).
671 Operation("patch"+namespaced+kind+strings.Title(subresource)+operationSuffix).
672 Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...).
673 Returns(http.StatusOK, "OK", producedObject).
674 Reads(metav1.Patch{}).
675 Writes(producedObject)
676 addParams(route, action.Params)
677 routes = append(routes, route)
678 case "POST": // Create a resource.
679 var handler restful.RouteFunction
680 if isNamedCreater {
681 handler = restfulCreateNamedResource(namedCreater, reqScope, admit)
682 } else {
683 handler = restfulCreateResource(creater, reqScope, admit)
684 }
685 handler = metrics.InstrumentRouteFunc(action.Verb, resource, subresource, requestScope, handler)
686 article := getArticleForNoun(kind, " ")
687 doc := "create" + article + kind
688 if isSubresource {
689 doc = "create " + subresource + " of" + article + kind
690 }
691 route := ws.POST(action.Path).To(handler).
692 Doc(doc).
693 Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
694 Operation("create"+namespaced+kind+strings.Title(subresource)+operationSuffix).
695 Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...).
696 Returns(http.StatusOK, "OK", producedObject).
697 // TODO: in some cases, the API may return a v1.Status instead of the versioned object
698 // but currently go-restful can't handle multiple different objects being returned.
699 Returns(http.StatusCreated, "Created", producedObject).
700 Returns(http.StatusAccepted, "Accepted", producedObject).
701 Reads(defaultVersionedObject).
702 Writes(producedObject)
703 addParams(route, action.Params)
704 routes = append(routes, route)
705 case "DELETE": // Delete a resource.
706 article := getArticleForNoun(kind, " ")
707 doc := "delete" + article + kind
708 if isSubresource {
709 doc = "delete " + subresource + " of" + article + kind
710 }
711 handler := metrics.InstrumentRouteFunc(action.Verb, resource, subresource, requestScope, restfulDeleteResource(gracefulDeleter, isGracefulDeleter, reqScope, admit))
712 route := ws.DELETE(action.Path).To(handler).
713 Doc(doc).
714 Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
715 Operation("delete"+namespaced+kind+strings.Title(subresource)+operationSuffix).
716 Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...).
717 Writes(versionedStatus).
718 Returns(http.StatusOK, "OK", versionedStatus)
719 if isGracefulDeleter {
720 route.Reads(versionedDeleterObject)
721 if err := addObjectParams(ws, route, versionedDeleteOptions); err != nil {
722 return nil, err
723 }
724 }
725 addParams(route, action.Params)
726 routes = append(routes, route)
727 case "DELETECOLLECTION":
728 doc := "delete collection of " + kind
729 if isSubresource {
730 doc = "delete collection of " + subresource + " of a " + kind
731 }
732 handler := metrics.InstrumentRouteFunc(action.Verb, resource, subresource, requestScope, restfulDeleteCollection(collectionDeleter, isCollectionDeleter, reqScope, admit))
733 route := ws.DELETE(action.Path).To(handler).
734 Doc(doc).
735 Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
736 Operation("deletecollection"+namespaced+kind+strings.Title(subresource)+operationSuffix).
737 Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...).
738 Writes(versionedStatus).
739 Returns(http.StatusOK, "OK", versionedStatus)
740 if err := addObjectParams(ws, route, versionedListOptions); err != nil {
741 return nil, err
742 }
743 addParams(route, action.Params)
744 routes = append(routes, route)
745 // TODO: deprecated
746 case "WATCH": // Watch a resource.
747 doc := "watch changes to an object of kind " + kind
748 if isSubresource {
749 doc = "watch changes to " + subresource + " of an object of kind " + kind
750 }
751 handler := metrics.InstrumentRouteFunc(action.Verb, resource, subresource, requestScope, restfulListResource(lister, watcher, reqScope, true, a.minRequestTimeout))
752 route := ws.GET(action.Path).To(handler).
753 Doc(doc).
754 Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
755 Operation("watch"+namespaced+kind+strings.Title(subresource)+operationSuffix).
756 Produces(allMediaTypes...).
757 Returns(http.StatusOK, "OK", versionedWatchEvent).
758 Writes(versionedWatchEvent)
759 if err := addObjectParams(ws, route, versionedListOptions); err != nil {
760 return nil, err
761 }
762 addParams(route, action.Params)
763 routes = append(routes, route)
764 // TODO: deprecated
765 case "WATCHLIST": // Watch all resources of a kind.
766 doc := "watch individual changes to a list of " + kind
767 if isSubresource {
768 doc = "watch individual changes to a list of " + subresource + " of " + kind
769 }
770 handler := metrics.InstrumentRouteFunc(action.Verb, resource, subresource, requestScope, restfulListResource(lister, watcher, reqScope, true, a.minRequestTimeout))
771 route := ws.GET(action.Path).To(handler).
772 Doc(doc).
773 Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
774 Operation("watch"+namespaced+kind+strings.Title(subresource)+"List"+operationSuffix).
775 Produces(allMediaTypes...).
776 Returns(http.StatusOK, "OK", versionedWatchEvent).
777 Writes(versionedWatchEvent)
778 if err := addObjectParams(ws, route, versionedListOptions); err != nil {
779 return nil, err
780 }
781 addParams(route, action.Params)
782 routes = append(routes, route)
783 case "CONNECT":
784 for _, method := range connecter.ConnectMethods() {
785 connectProducedObject := storageMeta.ProducesObject(method)
786 if connectProducedObject == nil {
787 connectProducedObject = "string"
788 }
789 doc := "connect " + method + " requests to " + kind
790 if isSubresource {
791 doc = "connect " + method + " requests to " + subresource + " of " + kind
792 }
793 handler := metrics.InstrumentRouteFunc(action.Verb, resource, subresource, requestScope, restfulConnectResource(connecter, reqScope, admit, path, isSubresource))
794 route := ws.Method(method).Path(action.Path).
795 To(handler).
796 Doc(doc).
797 Operation("connect" + strings.Title(strings.ToLower(method)) + namespaced + kind + strings.Title(subresource) + operationSuffix).
798 Produces("*/*").
799 Consumes("*/*").
800 Writes(connectProducedObject)
801 if versionedConnectOptions != nil {
802 if err := addObjectParams(ws, route, versionedConnectOptions); err != nil {
803 return nil, err
804 }
805 }
806 addParams(route, action.Params)
807 routes = append(routes, route)
808 }
809 default:
810 return nil, fmt.Errorf("unrecognized action verb: %s", action.Verb)
811 }
812 for _, route := range routes {
813 route.Metadata(ROUTE_META_GVK, metav1.GroupVersionKind{
814 Group: reqScope.Kind.Group,
815 Version: reqScope.Kind.Version,
816 Kind: reqScope.Kind.Kind,
817 })
818 route.Metadata(ROUTE_META_ACTION, strings.ToLower(action.Verb))
819 ws.Route(route)
820 }
821 // Note: update GetAuthorizerAttributes() when adding a custom handler.
822 }
823
824 apiResource.Verbs = make([]string, 0, len(kubeVerbs))
825 for kubeVerb := range kubeVerbs {
826 apiResource.Verbs = append(apiResource.Verbs, kubeVerb)
827 }
828 sort.Strings(apiResource.Verbs)
829
830 if shortNamesProvider, ok := storage.(rest.ShortNamesProvider); ok {
831 apiResource.ShortNames = shortNamesProvider.ShortNames()
832 }
833 if categoriesProvider, ok := storage.(rest.CategoriesProvider); ok {
834 apiResource.Categories = categoriesProvider.Categories()
835 }
836 if gvkProvider, ok := storage.(rest.GroupVersionKindProvider); ok {
837 gvk := gvkProvider.GroupVersionKind(a.group.GroupVersion)
838 apiResource.Group = gvk.Group
839 apiResource.Version = gvk.Version
840 apiResource.Kind = gvk.Kind
841 }
842
843 return &apiResource, nil
844}
845
846// indirectArbitraryPointer returns *ptrToObject for an arbitrary pointer
847func indirectArbitraryPointer(ptrToObject interface{}) interface{} {
848 return reflect.Indirect(reflect.ValueOf(ptrToObject)).Interface()
849}
850
851func appendIf(actions []action, a action, shouldAppend bool) []action {
852 if shouldAppend {
853 actions = append(actions, a)
854 }
855 return actions
856}
857
858// Wraps a http.Handler function inside a restful.RouteFunction
859func routeFunction(handler http.Handler) restful.RouteFunction {
860 return func(restReq *restful.Request, restResp *restful.Response) {
861 handler.ServeHTTP(restResp.ResponseWriter, restReq.Request)
862 }
863}
864
865func addParams(route *restful.RouteBuilder, params []*restful.Parameter) {
866 for _, param := range params {
867 route.Param(param)
868 }
869}
870
871// addObjectParams converts a runtime.Object into a set of go-restful Param() definitions on the route.
872// The object must be a pointer to a struct; only fields at the top level of the struct that are not
873// themselves interfaces or structs are used; only fields with a json tag that is non empty (the standard
874// Go JSON behavior for omitting a field) become query parameters. The name of the query parameter is
875// the JSON field name. If a description struct tag is set on the field, that description is used on the
876// query parameter. In essence, it converts a standard JSON top level object into a query param schema.
877func addObjectParams(ws *restful.WebService, route *restful.RouteBuilder, obj interface{}) error {
878 sv, err := conversion.EnforcePtr(obj)
879 if err != nil {
880 return err
881 }
882 st := sv.Type()
883 switch st.Kind() {
884 case reflect.Struct:
885 for i := 0; i < st.NumField(); i++ {
886 name := st.Field(i).Name
887 sf, ok := st.FieldByName(name)
888 if !ok {
889 continue
890 }
891 switch sf.Type.Kind() {
892 case reflect.Interface, reflect.Struct:
893 case reflect.Ptr:
894 // TODO: This is a hack to let metav1.Time through. This needs to be fixed in a more generic way eventually. bug #36191
895 if (sf.Type.Elem().Kind() == reflect.Interface || sf.Type.Elem().Kind() == reflect.Struct) && strings.TrimPrefix(sf.Type.String(), "*") != "metav1.Time" {
896 continue
897 }
898 fallthrough
899 default:
900 jsonTag := sf.Tag.Get("json")
901 if len(jsonTag) == 0 {
902 continue
903 }
904 jsonName := strings.SplitN(jsonTag, ",", 2)[0]
905 if len(jsonName) == 0 {
906 continue
907 }
908
909 var desc string
910 if docable, ok := obj.(documentable); ok {
911 desc = docable.SwaggerDoc()[jsonName]
912 }
913 route.Param(ws.QueryParameter(jsonName, desc).DataType(typeToJSON(sf.Type.String())))
914 }
915 }
916 }
917 return nil
918}
919
920// TODO: this is incomplete, expand as needed.
921// Convert the name of a golang type to the name of a JSON type
922func typeToJSON(typeName string) string {
923 switch typeName {
924 case "bool", "*bool":
925 return "boolean"
926 case "uint8", "*uint8", "int", "*int", "int32", "*int32", "int64", "*int64", "uint32", "*uint32", "uint64", "*uint64":
927 return "integer"
928 case "float64", "*float64", "float32", "*float32":
929 return "number"
930 case "metav1.Time", "*metav1.Time":
931 return "string"
932 case "byte", "*byte":
933 return "string"
934 case "v1.DeletionPropagation", "*v1.DeletionPropagation":
935 return "string"
936
937 // TODO: Fix these when go-restful supports a way to specify an array query param:
938 // https://github.com/emicklei/go-restful/issues/225
939 case "[]string", "[]*string":
940 return "string"
941 case "[]int32", "[]*int32":
942 return "integer"
943
944 default:
945 return typeName
946 }
947}
948
949// defaultStorageMetadata provides default answers to rest.StorageMetadata.
950type defaultStorageMetadata struct{}
951
952// defaultStorageMetadata implements rest.StorageMetadata
953var _ rest.StorageMetadata = defaultStorageMetadata{}
954
955func (defaultStorageMetadata) ProducesMIMETypes(verb string) []string {
956 return nil
957}
958
959func (defaultStorageMetadata) ProducesObject(verb string) interface{} {
960 return nil
961}
962
963// splitSubresource checks if the given storage path is the path of a subresource and returns
964// the resource and subresource components.
965func splitSubresource(path string) (string, string, error) {
966 var resource, subresource string
967 switch parts := strings.Split(path, "/"); len(parts) {
968 case 2:
969 resource, subresource = parts[0], parts[1]
970 case 1:
971 resource = parts[0]
972 default:
973 // TODO: support deeper paths
974 return "", "", fmt.Errorf("api_installer allows only one or two segment paths (resource or resource/subresource)")
975 }
976 return resource, subresource, nil
977}
978
979// getArticleForNoun returns the article needed for the given noun.
980func getArticleForNoun(noun string, padding string) string {
981 if noun[len(noun)-2:] != "ss" && noun[len(noun)-1:] == "s" {
982 // Plurals don't have an article.
983 // Don't catch words like class
984 return fmt.Sprintf("%v", padding)
985 }
986
987 article := "a"
988 if isVowel(rune(noun[0])) {
989 article = "an"
990 }
991
992 return fmt.Sprintf("%s%s%s", padding, article, padding)
993}
994
995// isVowel returns true if the rune is a vowel (case insensitive).
996func isVowel(c rune) bool {
997 vowels := []rune{'a', 'e', 'i', 'o', 'u'}
998 for _, value := range vowels {
999 if value == unicode.ToLower(c) {
1000 return true
1001 }
1002 }
1003 return false
1004}
1005
1006func restfulListResource(r rest.Lister, rw rest.Watcher, scope handlers.RequestScope, forceWatch bool, minRequestTimeout time.Duration) restful.RouteFunction {
1007 return func(req *restful.Request, res *restful.Response) {
1008 handlers.ListResource(r, rw, scope, forceWatch, minRequestTimeout)(res.ResponseWriter, req.Request)
1009 }
1010}
1011
1012func restfulCreateNamedResource(r rest.NamedCreater, scope handlers.RequestScope, admit admission.Interface) restful.RouteFunction {
1013 return func(req *restful.Request, res *restful.Response) {
1014 handlers.CreateNamedResource(r, scope, admit)(res.ResponseWriter, req.Request)
1015 }
1016}
1017
1018func restfulCreateResource(r rest.Creater, scope handlers.RequestScope, admit admission.Interface) restful.RouteFunction {
1019 return func(req *restful.Request, res *restful.Response) {
1020 handlers.CreateResource(r, scope, admit)(res.ResponseWriter, req.Request)
1021 }
1022}
1023
1024func restfulDeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope handlers.RequestScope, admit admission.Interface) restful.RouteFunction {
1025 return func(req *restful.Request, res *restful.Response) {
1026 handlers.DeleteResource(r, allowsOptions, scope, admit)(res.ResponseWriter, req.Request)
1027 }
1028}
1029
1030func restfulDeleteCollection(r rest.CollectionDeleter, checkBody bool, scope handlers.RequestScope, admit admission.Interface) restful.RouteFunction {
1031 return func(req *restful.Request, res *restful.Response) {
1032 handlers.DeleteCollection(r, checkBody, scope, admit)(res.ResponseWriter, req.Request)
1033 }
1034}
1035
1036func restfulUpdateResource(r rest.Updater, scope handlers.RequestScope, admit admission.Interface) restful.RouteFunction {
1037 return func(req *restful.Request, res *restful.Response) {
1038 handlers.UpdateResource(r, scope, admit)(res.ResponseWriter, req.Request)
1039 }
1040}
1041
1042func restfulPatchResource(r rest.Patcher, scope handlers.RequestScope, admit admission.Interface, supportedTypes []string) restful.RouteFunction {
1043 return func(req *restful.Request, res *restful.Response) {
1044 handlers.PatchResource(r, scope, admit, supportedTypes)(res.ResponseWriter, req.Request)
1045 }
1046}
1047
1048func restfulGetResource(r rest.Getter, e rest.Exporter, scope handlers.RequestScope) restful.RouteFunction {
1049 return func(req *restful.Request, res *restful.Response) {
1050 handlers.GetResource(r, e, scope)(res.ResponseWriter, req.Request)
1051 }
1052}
1053
1054func restfulGetResourceWithOptions(r rest.GetterWithOptions, scope handlers.RequestScope, isSubresource bool) restful.RouteFunction {
1055 return func(req *restful.Request, res *restful.Response) {
1056 handlers.GetResourceWithOptions(r, scope, isSubresource)(res.ResponseWriter, req.Request)
1057 }
1058}
1059
1060func restfulConnectResource(connecter rest.Connecter, scope handlers.RequestScope, admit admission.Interface, restPath string, isSubresource bool) restful.RouteFunction {
1061 return func(req *restful.Request, res *restful.Response) {
1062 handlers.ConnectResource(connecter, scope, admit, restPath, isSubresource)(res.ResponseWriter, req.Request)
1063 }
1064}