k3s/vendor/k8s.io/apiextensions-apiserver/pkg/controller/openapi/builder.go
Darren Shepherd 8031137b79 Update vendor
2019-08-30 23:08:05 -07:00

472 lines
16 KiB
Go

/*
Copyright 2019 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 openapi
import (
"fmt"
"net/http"
"strings"
"sync"
restful "github.com/emicklei/go-restful"
"github.com/go-openapi/spec"
v1 "k8s.io/api/autoscaling/v1"
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/endpoints"
"k8s.io/apiserver/pkg/endpoints/openapi"
openapibuilder "k8s.io/kube-openapi/pkg/builder"
"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/util"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
generatedopenapi "k8s.io/apiextensions-apiserver/pkg/generated/openapi"
)
const (
// Reference and Go types for built-in metadata
objectMetaSchemaRef = "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"
listMetaType = "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"
typeMetaType = "k8s.io/apimachinery/pkg/apis/meta/v1.TypeMeta"
definitionPrefix = "#/definitions/"
)
var (
swaggerPartialObjectMetadataDescriptions = metav1beta1.PartialObjectMetadata{}.SwaggerDoc()
)
var definitions map[string]common.OpenAPIDefinition
var buildDefinitions sync.Once
var namer *openapi.DefinitionNamer
// BuildSwagger builds swagger for the given crd in the given version
func BuildSwagger(crd *apiextensions.CustomResourceDefinition, version string) (*spec.Swagger, error) {
var schema *structuralschema.Structural
s, err := apiextensions.GetSchemaForVersion(crd, version)
if err != nil {
return nil, err
}
if s != nil && s.OpenAPIV3Schema != nil {
ss, err := structuralschema.NewStructural(s.OpenAPIV3Schema)
if err == nil && len(structuralschema.ValidateStructural(ss, nil)) == 0 {
// skip non-structural schemas
schema = ss.Unfold()
}
}
// TODO(roycaihw): remove the WebService templating below. The following logic
// comes from function registerResourceHandlers() in k8s.io/apiserver.
// Alternatives are either (ideally) refactoring registerResourceHandlers() to
// reuse the code, or faking an APIInstaller for CR to feed to registerResourceHandlers().
b := newBuilder(crd, version, schema, true)
// Sample response types for building web service
sample := &CRDCanonicalTypeNamer{
group: b.group,
version: b.version,
kind: b.kind,
}
sampleList := &CRDCanonicalTypeNamer{
group: b.group,
version: b.version,
kind: b.listKind,
}
status := &metav1.Status{}
patch := &metav1.Patch{}
scale := &v1.Scale{}
routes := make([]*restful.RouteBuilder, 0)
root := fmt.Sprintf("/apis/%s/%s/%s", b.group, b.version, b.plural)
if b.namespaced {
routes = append(routes, b.buildRoute(root, "", "GET", "list", sampleList).
Operation("list"+b.kind+"ForAllNamespaces"))
root = fmt.Sprintf("/apis/%s/%s/namespaces/{namespace}/%s", b.group, b.version, b.plural)
}
routes = append(routes, b.buildRoute(root, "", "GET", "list", sampleList))
routes = append(routes, b.buildRoute(root, "", "POST", "create", sample).Reads(sample))
routes = append(routes, b.buildRoute(root, "", "DELETE", "deletecollection", status))
routes = append(routes, b.buildRoute(root, "/{name}", "GET", "read", sample))
routes = append(routes, b.buildRoute(root, "/{name}", "PUT", "replace", sample).Reads(sample))
routes = append(routes, b.buildRoute(root, "/{name}", "DELETE", "delete", status))
routes = append(routes, b.buildRoute(root, "/{name}", "PATCH", "patch", sample).Reads(patch))
subresources, err := apiextensions.GetSubresourcesForVersion(crd, version)
if err != nil {
return nil, err
}
if subresources != nil && subresources.Status != nil {
routes = append(routes, b.buildRoute(root, "/{name}/status", "GET", "read", sample))
routes = append(routes, b.buildRoute(root, "/{name}/status", "PUT", "replace", sample).Reads(sample))
routes = append(routes, b.buildRoute(root, "/{name}/status", "PATCH", "patch", sample).Reads(patch))
}
if subresources != nil && subresources.Scale != nil {
routes = append(routes, b.buildRoute(root, "/{name}/scale", "GET", "read", scale))
routes = append(routes, b.buildRoute(root, "/{name}/scale", "PUT", "replace", scale).Reads(scale))
routes = append(routes, b.buildRoute(root, "/{name}/scale", "PATCH", "patch", scale).Reads(patch))
}
for _, route := range routes {
b.ws.Route(route)
}
openAPISpec, err := openapibuilder.BuildOpenAPISpec([]*restful.WebService{b.ws}, b.getOpenAPIConfig())
if err != nil {
return nil, err
}
return openAPISpec, nil
}
// Implements CanonicalTypeNamer
var _ = util.OpenAPICanonicalTypeNamer(&CRDCanonicalTypeNamer{})
// CRDCanonicalTypeNamer implements CanonicalTypeNamer interface for CRDs to
// seed kube-openapi canonical type name without Go types
type CRDCanonicalTypeNamer struct {
group string
version string
kind string
}
// OpenAPICanonicalTypeName returns canonical type name for given CRD
func (c *CRDCanonicalTypeNamer) OpenAPICanonicalTypeName() string {
return fmt.Sprintf("%s/%s.%s", c.group, c.version, c.kind)
}
// builder contains validation schema and basic naming information for a CRD in
// one version. The builder works to build a WebService that kube-openapi can
// consume.
type builder struct {
schema *spec.Schema
listSchema *spec.Schema
ws *restful.WebService
group string
version string
kind string
listKind string
plural string
namespaced bool
}
// subresource is a handy method to get subresource name. Valid inputs are:
// input output
// "" ""
// "/" ""
// "/{name}" ""
// "/{name}/scale" "scale"
// "/{name}/scale/foo" invalid input
func subresource(path string) string {
parts := strings.Split(path, "/")
if len(parts) <= 2 {
return ""
}
if len(parts) == 3 {
return parts[2]
}
// panic to alert on programming error
panic("failed to parse subresource; invalid path")
}
func (b *builder) descriptionFor(path, verb string) string {
var article string
switch verb {
case "list":
article = " objects of kind "
case "read", "replace":
article = " the specified "
case "patch":
article = " the specified "
case "create", "delete":
article = endpoints.GetArticleForNoun(b.kind, " ")
default:
article = ""
}
var description string
sub := subresource(path)
if len(sub) > 0 {
sub = " " + sub + " of"
}
switch verb {
case "patch":
description = "partially update" + sub + article + b.kind
case "deletecollection":
// to match the text for built-in APIs
if len(sub) > 0 {
sub = sub + " a"
}
description = "delete collection of" + sub + " " + b.kind
default:
description = verb + sub + article + b.kind
}
return description
}
// buildRoute returns a RouteBuilder for WebService to consume and builds path in swagger
// action can be one of: GET, PUT, PATCH, POST, DELETE;
// verb can be one of: list, read, replace, patch, create, delete, deletecollection;
// sample is the sample Go type for response type.
func (b *builder) buildRoute(root, path, action, verb string, sample interface{}) *restful.RouteBuilder {
var namespaced string
if b.namespaced {
namespaced = "Namespaced"
}
route := b.ws.Method(action).
Path(root+path).
To(func(req *restful.Request, res *restful.Response) {}).
Doc(b.descriptionFor(path, verb)).
Param(b.ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
Operation(verb+namespaced+b.kind+strings.Title(subresource(path))).
Metadata(endpoints.ROUTE_META_GVK, metav1.GroupVersionKind{
Group: b.group,
Version: b.version,
Kind: b.kind,
}).
Metadata(endpoints.ROUTE_META_ACTION, strings.ToLower(action)).
Produces("application/json", "application/yaml").
Returns(http.StatusOK, "OK", sample).
Writes(sample)
// Build consume media types
if action == "PATCH" {
route.Consumes("application/json-patch+json",
"application/merge-patch+json",
"application/strategic-merge-patch+json")
} else {
route.Consumes("*/*")
}
// Build option parameters
switch verb {
case "get":
// TODO: CRD support for export is still under consideration
endpoints.AddObjectParams(b.ws, route, &metav1.GetOptions{})
case "list", "deletecollection":
endpoints.AddObjectParams(b.ws, route, &metav1.ListOptions{})
case "replace", "patch":
// TODO: PatchOption added in feature branch but not in master yet
endpoints.AddObjectParams(b.ws, route, &metav1.UpdateOptions{})
case "create":
endpoints.AddObjectParams(b.ws, route, &metav1.CreateOptions{})
case "delete":
endpoints.AddObjectParams(b.ws, route, &metav1.DeleteOptions{})
route.Reads(&metav1.DeleteOptions{}).ParameterNamed("body").Required(false)
}
// Build responses
switch verb {
case "create":
route.Returns(http.StatusAccepted, "Accepted", sample)
route.Returns(http.StatusCreated, "Created", sample)
case "delete":
route.Returns(http.StatusAccepted, "Accepted", sample)
case "replace":
route.Returns(http.StatusCreated, "Created", sample)
}
return route
}
// buildKubeNative builds input schema with Kubernetes' native object meta, type meta and
// extensions
func (b *builder) buildKubeNative(schema *structuralschema.Structural, v2 bool) (ret *spec.Schema) {
// only add properties if we have a schema. Otherwise, kubectl would (wrongly) assume additionalProperties=false
// and forbid anything outside of apiVersion, kind and metadata. We have to fix kubectl to stop doing this, e.g. by
// adding additionalProperties=true support to explicitly allow additional fields.
// TODO: fix kubectl to understand additionalProperties=true
if schema == nil || (v2 && schema.XPreserveUnknownFields) {
ret = &spec.Schema{
SchemaProps: spec.SchemaProps{Type: []string{"object"}},
}
// no, we cannot add more properties here, not even TypeMeta/ObjectMeta because kubectl will complain about
// unknown fields for anything else.
} else {
if v2 {
schema = ToStructuralOpenAPIV2(schema)
}
ret = schema.ToGoOpenAPI()
ret.SetProperty("metadata", *spec.RefSchema(objectMetaSchemaRef).
WithDescription(swaggerPartialObjectMetadataDescriptions["metadata"]))
addTypeMetaProperties(ret)
addEmbeddedProperties(ret, v2)
}
ret.AddExtension(endpoints.ROUTE_META_GVK, []interface{}{
map[string]interface{}{
"group": b.group,
"version": b.version,
"kind": b.kind,
},
})
return ret
}
func addEmbeddedProperties(s *spec.Schema, v2 bool) {
if s == nil {
return
}
for k := range s.Properties {
v := s.Properties[k]
addEmbeddedProperties(&v, v2)
s.Properties[k] = v
}
if s.Items != nil {
addEmbeddedProperties(s.Items.Schema, v2)
}
if s.AdditionalProperties != nil {
addEmbeddedProperties(s.AdditionalProperties.Schema, v2)
}
if isTrue, ok := s.VendorExtensible.Extensions.GetBool("x-kubernetes-preserve-unknown-fields"); ok && isTrue && v2 {
// don't add metadata properties if we're publishing to openapi v2 and are allowing unknown fields.
// adding these metadata properties makes kubectl refuse to validate unknown fields.
return
}
if isTrue, ok := s.VendorExtensible.Extensions.GetBool("x-kubernetes-embedded-resource"); ok && isTrue {
s.SetProperty("apiVersion", withDescription(getDefinition(typeMetaType).SchemaProps.Properties["apiVersion"],
"apiVersion defines the versioned schema of this representation of an object. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources",
))
s.SetProperty("kind", withDescription(getDefinition(typeMetaType).SchemaProps.Properties["kind"],
"kind is a string value representing the type of this object. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds",
))
s.SetProperty("metadata", *spec.RefSchema(objectMetaSchemaRef).WithDescription(swaggerPartialObjectMetadataDescriptions["metadata"]))
req := sets.NewString(s.Required...)
if !req.Has("kind") {
s.Required = append(s.Required, "kind")
}
if !req.Has("apiVersion") {
s.Required = append(s.Required, "apiVersion")
}
}
}
// getDefinition gets definition for given Kubernetes type. This function is extracted from
// kube-openapi builder logic
func getDefinition(name string) spec.Schema {
buildDefinitions.Do(buildDefinitionsFunc)
return definitions[name].Schema
}
func withDescription(s spec.Schema, desc string) spec.Schema {
return *s.WithDescription(desc)
}
func buildDefinitionsFunc() {
namer = openapi.NewDefinitionNamer(runtime.NewScheme())
definitions = generatedopenapi.GetOpenAPIDefinitions(func(name string) spec.Ref {
defName, _ := namer.GetDefinitionName(name)
return spec.MustCreateRef(definitionPrefix + common.EscapeJsonPointer(defName))
})
}
// addTypeMetaProperties adds Kubernetes-specific type meta properties to input schema:
// apiVersion and kind
func addTypeMetaProperties(s *spec.Schema) {
s.SetProperty("apiVersion", getDefinition(typeMetaType).SchemaProps.Properties["apiVersion"])
s.SetProperty("kind", getDefinition(typeMetaType).SchemaProps.Properties["kind"])
}
// buildListSchema builds the list kind schema for the CRD
func (b *builder) buildListSchema() *spec.Schema {
name := definitionPrefix + util.ToRESTFriendlyName(fmt.Sprintf("%s/%s/%s", b.group, b.version, b.kind))
doc := fmt.Sprintf("List of %s. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md", b.plural)
s := new(spec.Schema).WithDescription(fmt.Sprintf("%s is a list of %s", b.listKind, b.kind)).
WithRequired("items").
SetProperty("items", *spec.ArrayProperty(spec.RefSchema(name)).WithDescription(doc)).
SetProperty("metadata", getDefinition(listMetaType))
addTypeMetaProperties(s)
s.AddExtension(endpoints.ROUTE_META_GVK, []map[string]string{
{
"group": b.group,
"version": b.version,
"kind": b.listKind,
},
})
return s
}
// getOpenAPIConfig builds config which wires up generated definitions for kube-openapi to consume
func (b *builder) getOpenAPIConfig() *common.Config {
return &common.Config{
ProtocolList: []string{"https"},
Info: &spec.Info{
InfoProps: spec.InfoProps{
Title: "Kubernetes CRD Swagger",
Version: "v0.1.0",
},
},
CommonResponses: map[int]spec.Response{
401: {
ResponseProps: spec.ResponseProps{
Description: "Unauthorized",
},
},
},
GetOperationIDAndTags: openapi.GetOperationIDAndTags,
GetDefinitionName: func(name string) (string, spec.Extensions) {
buildDefinitions.Do(buildDefinitionsFunc)
return namer.GetDefinitionName(name)
},
GetDefinitions: func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
def := generatedopenapi.GetOpenAPIDefinitions(ref)
def[fmt.Sprintf("%s/%s.%s", b.group, b.version, b.kind)] = common.OpenAPIDefinition{
Schema: *b.schema,
}
def[fmt.Sprintf("%s/%s.%s", b.group, b.version, b.listKind)] = common.OpenAPIDefinition{
Schema: *b.listSchema,
}
return def
},
}
}
func newBuilder(crd *apiextensions.CustomResourceDefinition, version string, schema *structuralschema.Structural, v2 bool) *builder {
b := &builder{
schema: &spec.Schema{
SchemaProps: spec.SchemaProps{Type: []string{"object"}},
},
listSchema: &spec.Schema{},
ws: &restful.WebService{},
group: crd.Spec.Group,
version: version,
kind: crd.Spec.Names.Kind,
listKind: crd.Spec.Names.ListKind,
plural: crd.Spec.Names.Plural,
}
if crd.Spec.Scope == apiextensions.NamespaceScoped {
b.namespaced = true
}
// Pre-build schema with Kubernetes native properties
b.schema = b.buildKubeNative(schema, v2)
b.listSchema = b.buildListSchema()
return b
}