...

Source file src/k8s.io/apiextensions-apiserver/pkg/controller/openapi/builder/builder.go

Documentation: k8s.io/apiextensions-apiserver/pkg/controller/openapi/builder

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package builder
    18  
    19  import (
    20  	"fmt"
    21  	"net/http"
    22  	"strings"
    23  	"sync"
    24  
    25  	"github.com/emicklei/go-restful/v3"
    26  
    27  	v1 "k8s.io/api/autoscaling/v1"
    28  	apiextensionshelpers "k8s.io/apiextensions-apiserver/pkg/apihelpers"
    29  	apiextensionsinternal "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
    30  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    31  	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation"
    32  	structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
    33  	openapiv2 "k8s.io/apiextensions-apiserver/pkg/controller/openapi/v2"
    34  	generatedopenapi "k8s.io/apiextensions-apiserver/pkg/generated/openapi"
    35  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    36  	metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
    37  	"k8s.io/apimachinery/pkg/runtime"
    38  	"k8s.io/apimachinery/pkg/types"
    39  	"k8s.io/apimachinery/pkg/util/sets"
    40  	"k8s.io/apiserver/pkg/endpoints"
    41  	"k8s.io/apiserver/pkg/endpoints/openapi"
    42  	utilopenapi "k8s.io/apiserver/pkg/util/openapi"
    43  	"k8s.io/client-go/kubernetes/scheme"
    44  	openapibuilder "k8s.io/kube-openapi/pkg/builder"
    45  	"k8s.io/kube-openapi/pkg/builder3"
    46  	"k8s.io/kube-openapi/pkg/common"
    47  	"k8s.io/kube-openapi/pkg/common/restfuladapter"
    48  	"k8s.io/kube-openapi/pkg/spec3"
    49  	"k8s.io/kube-openapi/pkg/util"
    50  	"k8s.io/kube-openapi/pkg/validation/spec"
    51  )
    52  
    53  const (
    54  	// Reference and Go types for built-in metadata
    55  	objectMetaSchemaRef = "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"
    56  	listMetaSchemaRef   = "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta"
    57  
    58  	typeMetaType   = "k8s.io/apimachinery/pkg/apis/meta/v1.TypeMeta"
    59  	objectMetaType = "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"
    60  
    61  	definitionPrefix   = "#/definitions/"
    62  	v3DefinitionPrefix = "#/components/schemas/"
    63  )
    64  
    65  var (
    66  	swaggerPartialObjectMetadataDescriptions     = metav1beta1.PartialObjectMetadata{}.SwaggerDoc()
    67  	swaggerPartialObjectMetadataListDescriptions = metav1beta1.PartialObjectMetadataList{}.SwaggerDoc()
    68  
    69  	nameToken      = "{name}"
    70  	namespaceToken = "{namespace}"
    71  )
    72  
    73  // The path for definitions in OpenAPI v2 and v3 are different. Translate the path if necessary
    74  // The provided schemaRef uses a v2 prefix and is converted to v3 if the v2 bool is false
    75  func refForOpenAPIVersion(schemaRef string, v2 bool) string {
    76  	if v2 {
    77  		return schemaRef
    78  	}
    79  	return strings.Replace(schemaRef, definitionPrefix, v3DefinitionPrefix, 1)
    80  }
    81  
    82  var definitions map[string]common.OpenAPIDefinition
    83  var definitionsV3 map[string]common.OpenAPIDefinition
    84  var buildDefinitions sync.Once
    85  var namer *openapi.DefinitionNamer
    86  
    87  // Options contains builder options.
    88  type Options struct {
    89  	// Convert to OpenAPI v2.
    90  	V2 bool
    91  
    92  	// Strip value validation.
    93  	StripValueValidation bool
    94  
    95  	// Strip nullable.
    96  	StripNullable bool
    97  
    98  	// AllowNonStructural indicates swagger should be built for a schema that fits into the structural type but does not meet all structural invariants
    99  	AllowNonStructural bool
   100  
   101  	IncludeSelectableFields bool
   102  }
   103  
   104  func generateBuilder(crd *apiextensionsv1.CustomResourceDefinition, version string, opts Options) (*builder, error) {
   105  	var schema *structuralschema.Structural
   106  	s, err := apiextensionshelpers.GetSchemaForVersion(crd, version)
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  
   111  	if s != nil && s.OpenAPIV3Schema != nil {
   112  		internalCRDSchema := &apiextensionsinternal.CustomResourceValidation{}
   113  		if err := apiextensionsv1.Convert_v1_CustomResourceValidation_To_apiextensions_CustomResourceValidation(s, internalCRDSchema, nil); err != nil {
   114  			return nil, fmt.Errorf("failed converting CRD validation to internal version: %v", err)
   115  		}
   116  		if !validation.SchemaHasInvalidTypes(internalCRDSchema.OpenAPIV3Schema) {
   117  			if ss, err := structuralschema.NewStructural(internalCRDSchema.OpenAPIV3Schema); err == nil {
   118  				// skip non-structural schemas unless explicitly asked to produce swagger from them
   119  				if opts.AllowNonStructural || len(structuralschema.ValidateStructural(nil, ss)) == 0 {
   120  					schema = ss
   121  
   122  					// This adds ValueValidation fields (anyOf, allOf) which may be stripped below if opts.StripValueValidation is true
   123  					schema = schema.Unfold()
   124  
   125  					if opts.StripValueValidation {
   126  						schema = schema.StripValueValidations()
   127  					}
   128  					if opts.StripNullable {
   129  						schema = schema.StripNullable()
   130  					}
   131  				}
   132  			}
   133  		}
   134  	}
   135  
   136  	// TODO(roycaihw): remove the WebService templating below. The following logic
   137  	// comes from function registerResourceHandlers() in k8s.io/apiserver.
   138  	// Alternatives are either (ideally) refactoring registerResourceHandlers() to
   139  	// reuse the code, or faking an APIInstaller for CR to feed to registerResourceHandlers().
   140  	b := newBuilder(crd, version, schema, opts)
   141  
   142  	// Sample response types for building web service
   143  	sample := &CRDCanonicalTypeNamer{
   144  		group:   b.group,
   145  		version: b.version,
   146  		kind:    b.kind,
   147  	}
   148  	sampleList := &CRDCanonicalTypeNamer{
   149  		group:   b.group,
   150  		version: b.version,
   151  		kind:    b.listKind,
   152  	}
   153  	status := &metav1.Status{}
   154  	patch := &metav1.Patch{}
   155  	scale := &v1.Scale{}
   156  
   157  	routes := make([]*restful.RouteBuilder, 0)
   158  	root := fmt.Sprintf("/apis/%s/%s/%s", b.group, b.version, b.plural)
   159  
   160  	if b.namespaced {
   161  		routes = append(routes, b.buildRoute(root, "", "GET", "list", "list", sampleList).Operation("list"+b.kind+"ForAllNamespaces"))
   162  		root = fmt.Sprintf("/apis/%s/%s/namespaces/{namespace}/%s", b.group, b.version, b.plural)
   163  	}
   164  	routes = append(routes, b.buildRoute(root, "", "GET", "list", "list", sampleList))
   165  	routes = append(routes, b.buildRoute(root, "", "POST", "post", "create", sample).Reads(sample))
   166  	routes = append(routes, b.buildRoute(root, "", "DELETE", "deletecollection", "deletecollection", status))
   167  
   168  	routes = append(routes, b.buildRoute(root, "/{name}", "GET", "get", "read", sample))
   169  	routes = append(routes, b.buildRoute(root, "/{name}", "PUT", "put", "replace", sample).Reads(sample))
   170  	routes = append(routes, b.buildRoute(root, "/{name}", "DELETE", "delete", "delete", status))
   171  	routes = append(routes, b.buildRoute(root, "/{name}", "PATCH", "patch", "patch", sample).Reads(patch))
   172  
   173  	subresources, err := apiextensionshelpers.GetSubresourcesForVersion(crd, version)
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  	if subresources != nil && subresources.Status != nil {
   178  		routes = append(routes, b.buildRoute(root, "/{name}/status", "GET", "get", "read", sample))
   179  		routes = append(routes, b.buildRoute(root, "/{name}/status", "PUT", "put", "replace", sample).Reads(sample))
   180  		routes = append(routes, b.buildRoute(root, "/{name}/status", "PATCH", "patch", "patch", sample).Reads(patch))
   181  	}
   182  	if subresources != nil && subresources.Scale != nil {
   183  		routes = append(routes, b.buildRoute(root, "/{name}/scale", "GET", "get", "read", scale))
   184  		routes = append(routes, b.buildRoute(root, "/{name}/scale", "PUT", "put", "replace", scale).Reads(scale))
   185  		routes = append(routes, b.buildRoute(root, "/{name}/scale", "PATCH", "patch", "patch", scale).Reads(patch))
   186  	}
   187  
   188  	for _, route := range routes {
   189  		b.ws.Route(route)
   190  	}
   191  	return b, nil
   192  }
   193  
   194  func BuildOpenAPIV3(crd *apiextensionsv1.CustomResourceDefinition, version string, opts Options) (*spec3.OpenAPI, error) {
   195  	b, err := generateBuilder(crd, version, opts)
   196  	if err != nil {
   197  		return nil, err
   198  	}
   199  
   200  	return builder3.BuildOpenAPISpecFromRoutes(restfuladapter.AdaptWebServices([]*restful.WebService{b.ws}), b.getOpenAPIV3Config())
   201  }
   202  
   203  // BuildOpenAPIV2 builds OpenAPI v2 for the given crd in the given version
   204  func BuildOpenAPIV2(crd *apiextensionsv1.CustomResourceDefinition, version string, opts Options) (*spec.Swagger, error) {
   205  	b, err := generateBuilder(crd, version, opts)
   206  	if err != nil {
   207  		return nil, err
   208  	}
   209  
   210  	return openapibuilder.BuildOpenAPISpecFromRoutes(restfuladapter.AdaptWebServices([]*restful.WebService{b.ws}), b.getOpenAPIConfig())
   211  }
   212  
   213  // Implements CanonicalTypeNamer
   214  var _ = util.OpenAPICanonicalTypeNamer(&CRDCanonicalTypeNamer{})
   215  
   216  // CRDCanonicalTypeNamer implements CanonicalTypeNamer interface for CRDs to
   217  // seed kube-openapi canonical type name without Go types
   218  type CRDCanonicalTypeNamer struct {
   219  	group   string
   220  	version string
   221  	kind    string
   222  }
   223  
   224  // OpenAPICanonicalTypeName returns canonical type name for given CRD
   225  func (c *CRDCanonicalTypeNamer) OpenAPICanonicalTypeName() string {
   226  	return fmt.Sprintf("%s/%s.%s", c.group, c.version, c.kind)
   227  }
   228  
   229  // builder contains validation schema and basic naming information for a CRD in
   230  // one version. The builder works to build a WebService that kube-openapi can
   231  // consume.
   232  type builder struct {
   233  	schema     *spec.Schema
   234  	listSchema *spec.Schema
   235  	ws         *restful.WebService
   236  
   237  	group    string
   238  	version  string
   239  	kind     string
   240  	listKind string
   241  	plural   string
   242  
   243  	namespaced bool
   244  }
   245  
   246  // subresource is a handy method to get subresource name. Valid inputs are:
   247  //
   248  //	input                     output
   249  //	""                        ""
   250  //	"/"                       ""
   251  //	"/{name}"                 ""
   252  //	"/{name}/scale"           "scale"
   253  //	"/{name}/scale/foo"       invalid input
   254  func subresource(path string) string {
   255  	parts := strings.Split(path, "/")
   256  	if len(parts) <= 2 {
   257  		return ""
   258  	}
   259  	if len(parts) == 3 {
   260  		return parts[2]
   261  	}
   262  	// panic to alert on programming error
   263  	panic("failed to parse subresource; invalid path")
   264  }
   265  
   266  func (b *builder) descriptionFor(path, operationVerb string) string {
   267  	var article string
   268  	switch operationVerb {
   269  	case "list":
   270  		article = " objects of kind "
   271  	case "read", "replace":
   272  		article = " the specified "
   273  	case "patch":
   274  		article = " the specified "
   275  	case "create", "delete":
   276  		article = endpoints.GetArticleForNoun(b.kind, " ")
   277  	default:
   278  		article = ""
   279  	}
   280  
   281  	var description string
   282  	sub := subresource(path)
   283  	if len(sub) > 0 {
   284  		sub = " " + sub + " of"
   285  	}
   286  	switch operationVerb {
   287  	case "patch":
   288  		description = "partially update" + sub + article + b.kind
   289  	case "deletecollection":
   290  		// to match the text for built-in APIs
   291  		if len(sub) > 0 {
   292  			sub = sub + " a"
   293  		}
   294  		description = "delete collection of" + sub + " " + b.kind
   295  	default:
   296  		description = operationVerb + sub + article + b.kind
   297  	}
   298  
   299  	return description
   300  }
   301  
   302  // buildRoute returns a RouteBuilder for WebService to consume and builds path in swagger
   303  //
   304  //	action can be one of: GET, PUT, PATCH, POST, DELETE;
   305  //	verb can be one of: list, read, replace, patch, create, delete, deletecollection;
   306  //	sample is the sample Go type for response type.
   307  func (b *builder) buildRoute(root, path, httpMethod, actionVerb, operationVerb string, sample interface{}) *restful.RouteBuilder {
   308  	var namespaced string
   309  	if b.namespaced {
   310  		namespaced = "Namespaced"
   311  	}
   312  	route := b.ws.Method(httpMethod).
   313  		Path(root+path).
   314  		To(func(req *restful.Request, res *restful.Response) {}).
   315  		Doc(b.descriptionFor(path, operationVerb)).
   316  		Param(b.ws.QueryParameter("pretty", "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).")).
   317  		Operation(operationVerb+namespaced+b.kind+strings.Title(subresource(path))).
   318  		Metadata(endpoints.RouteMetaGVK, metav1.GroupVersionKind{
   319  			Group:   b.group,
   320  			Version: b.version,
   321  			Kind:    b.kind,
   322  		}).
   323  		Metadata(endpoints.RouteMetaAction, actionVerb).
   324  		Produces("application/json", "application/yaml").
   325  		Returns(http.StatusOK, "OK", sample).
   326  		Writes(sample)
   327  	if strings.Contains(root, namespaceToken) || strings.Contains(path, namespaceToken) {
   328  		route.Param(b.ws.PathParameter("namespace", "object name and auth scope, such as for teams and projects").DataType("string"))
   329  	}
   330  	if strings.Contains(root, nameToken) || strings.Contains(path, nameToken) {
   331  		route.Param(b.ws.PathParameter("name", "name of the "+b.kind).DataType("string"))
   332  	}
   333  
   334  	// Build consume media types
   335  	if httpMethod == "PATCH" {
   336  		supportedTypes := []string{
   337  			string(types.JSONPatchType),
   338  			string(types.MergePatchType),
   339  			string(types.ApplyPatchType),
   340  		}
   341  		route.Consumes(supportedTypes...)
   342  	} else {
   343  		route.Consumes(runtime.ContentTypeJSON, runtime.ContentTypeYAML)
   344  	}
   345  
   346  	// Build option parameters
   347  	switch actionVerb {
   348  	case "get":
   349  		endpoints.AddObjectParams(b.ws, route, &metav1.GetOptions{})
   350  	case "list", "deletecollection":
   351  		endpoints.AddObjectParams(b.ws, route, &metav1.ListOptions{})
   352  	case "put":
   353  		endpoints.AddObjectParams(b.ws, route, &metav1.UpdateOptions{})
   354  	case "patch":
   355  		endpoints.AddObjectParams(b.ws, route, &metav1.PatchOptions{})
   356  	case "post":
   357  		endpoints.AddObjectParams(b.ws, route, &metav1.CreateOptions{})
   358  	case "delete":
   359  		endpoints.AddObjectParams(b.ws, route, &metav1.DeleteOptions{})
   360  		route.Reads(&metav1.DeleteOptions{}).ParameterNamed("body").Required(false)
   361  	}
   362  
   363  	// Build responses
   364  	switch actionVerb {
   365  	case "post":
   366  		route.Returns(http.StatusAccepted, "Accepted", sample)
   367  		route.Returns(http.StatusCreated, "Created", sample)
   368  	case "delete":
   369  		route.Returns(http.StatusAccepted, "Accepted", sample)
   370  	case "put":
   371  		route.Returns(http.StatusCreated, "Created", sample)
   372  	}
   373  
   374  	return route
   375  }
   376  
   377  // buildKubeNative builds input schema with Kubernetes' native object meta, type meta and
   378  // extensions
   379  func (b *builder) buildKubeNative(crd *apiextensionsv1.CustomResourceDefinition, schema *structuralschema.Structural, opts Options, crdPreserveUnknownFields bool) (ret *spec.Schema) {
   380  	// only add properties if we have a schema. Otherwise, kubectl would (wrongly) assume additionalProperties=false
   381  	// and forbid anything outside of apiVersion, kind and metadata. We have to fix kubectl to stop doing this, e.g. by
   382  	// adding additionalProperties=true support to explicitly allow additional fields.
   383  	// TODO: fix kubectl to understand additionalProperties=true
   384  	if schema == nil || (opts.V2 && (schema.XPreserveUnknownFields || crdPreserveUnknownFields)) {
   385  		ret = &spec.Schema{
   386  			SchemaProps: spec.SchemaProps{Type: []string{"object"}},
   387  		}
   388  		// no, we cannot add more properties here, not even TypeMeta/ObjectMeta because kubectl will complain about
   389  		// unknown fields for anything else.
   390  	} else {
   391  		if opts.V2 {
   392  			schema = openapiv2.ToStructuralOpenAPIV2(schema)
   393  		}
   394  
   395  		ret = schema.ToKubeOpenAPI()
   396  		ret.SetProperty("metadata", *spec.RefSchema(refForOpenAPIVersion(objectMetaSchemaRef, opts.V2)).WithDescription(swaggerPartialObjectMetadataDescriptions["metadata"]))
   397  		addTypeMetaProperties(ret, opts.V2)
   398  		addEmbeddedProperties(ret, opts)
   399  	}
   400  	ret.AddExtension(endpoints.RouteMetaGVK, []interface{}{
   401  		map[string]interface{}{
   402  			"group":   b.group,
   403  			"version": b.version,
   404  			"kind":    b.kind,
   405  		},
   406  	})
   407  
   408  	if opts.IncludeSelectableFields {
   409  		if selectableFields := buildSelectableFields(crd, b.version); selectableFields != nil {
   410  			ret.AddExtension(endpoints.RouteMetaSelectableFields, selectableFields)
   411  		}
   412  	}
   413  
   414  	return ret
   415  }
   416  
   417  func addEmbeddedProperties(s *spec.Schema, opts Options) {
   418  	if s == nil {
   419  		return
   420  	}
   421  
   422  	for k := range s.Properties {
   423  		v := s.Properties[k]
   424  		addEmbeddedProperties(&v, opts)
   425  		s.Properties[k] = v
   426  	}
   427  	if s.Items != nil {
   428  		addEmbeddedProperties(s.Items.Schema, opts)
   429  	}
   430  	if s.AdditionalProperties != nil {
   431  		addEmbeddedProperties(s.AdditionalProperties.Schema, opts)
   432  	}
   433  
   434  	if isTrue, ok := s.VendorExtensible.Extensions.GetBool("x-kubernetes-preserve-unknown-fields"); ok && isTrue && opts.V2 {
   435  		// don't add metadata properties if we're publishing to openapi v2 and are allowing unknown fields.
   436  		// adding these metadata properties makes kubectl refuse to validate unknown fields.
   437  		return
   438  	}
   439  	if isTrue, ok := s.VendorExtensible.Extensions.GetBool("x-kubernetes-embedded-resource"); ok && isTrue {
   440  		s.SetProperty("apiVersion", withDescription(getDefinition(typeMetaType, opts.V2).SchemaProps.Properties["apiVersion"],
   441  			"apiVersion defines the versioned schema of this representation of an object. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
   442  		))
   443  		s.SetProperty("kind", withDescription(getDefinition(typeMetaType, opts.V2).SchemaProps.Properties["kind"],
   444  			"kind is a string value representing the type of this object. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
   445  		))
   446  		s.SetProperty("metadata", *spec.RefSchema(refForOpenAPIVersion(objectMetaSchemaRef, opts.V2)).WithDescription(swaggerPartialObjectMetadataDescriptions["metadata"]))
   447  
   448  		req := sets.NewString(s.Required...)
   449  		if !req.Has("kind") {
   450  			s.Required = append(s.Required, "kind")
   451  		}
   452  		if !req.Has("apiVersion") {
   453  			s.Required = append(s.Required, "apiVersion")
   454  		}
   455  	}
   456  }
   457  
   458  // getDefinition gets definition for given Kubernetes type. This function is extracted from
   459  // kube-openapi builder logic
   460  func getDefinition(name string, v2 bool) spec.Schema {
   461  	buildDefinitions.Do(generateBuildDefinitionsFunc)
   462  
   463  	if v2 {
   464  		return definitions[name].Schema
   465  	}
   466  	return definitionsV3[name].Schema
   467  }
   468  
   469  func withDescription(s spec.Schema, desc string) spec.Schema {
   470  	return *s.WithDescription(desc)
   471  }
   472  
   473  func generateBuildDefinitionsFunc() {
   474  	namer = openapi.NewDefinitionNamer(scheme.Scheme)
   475  	definitionsV3 = utilopenapi.GetOpenAPIDefinitionsWithoutDisabledFeatures(generatedopenapi.GetOpenAPIDefinitions)(func(name string) spec.Ref {
   476  		defName, _ := namer.GetDefinitionName(name)
   477  		prefix := v3DefinitionPrefix
   478  		return spec.MustCreateRef(prefix + common.EscapeJsonPointer(defName))
   479  	})
   480  
   481  	definitions = utilopenapi.GetOpenAPIDefinitionsWithoutDisabledFeatures(generatedopenapi.GetOpenAPIDefinitions)(func(name string) spec.Ref {
   482  		defName, _ := namer.GetDefinitionName(name)
   483  		prefix := definitionPrefix
   484  		return spec.MustCreateRef(prefix + common.EscapeJsonPointer(defName))
   485  	})
   486  }
   487  
   488  // addTypeMetaProperties adds Kubernetes-specific type meta properties to input schema:
   489  //
   490  //	apiVersion and kind
   491  func addTypeMetaProperties(s *spec.Schema, v2 bool) {
   492  	s.SetProperty("apiVersion", getDefinition(typeMetaType, v2).SchemaProps.Properties["apiVersion"])
   493  	s.SetProperty("kind", getDefinition(typeMetaType, v2).SchemaProps.Properties["kind"])
   494  }
   495  
   496  // buildListSchema builds the list kind schema for the CRD
   497  func (b *builder) buildListSchema(crd *apiextensionsv1.CustomResourceDefinition, opts Options) *spec.Schema {
   498  	name := definitionPrefix + util.ToRESTFriendlyName(fmt.Sprintf("%s/%s/%s", b.group, b.version, b.kind))
   499  	doc := fmt.Sprintf("List of %s. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md", b.plural)
   500  	s := new(spec.Schema).
   501  		Typed("object", "").
   502  		WithDescription(fmt.Sprintf("%s is a list of %s", b.listKind, b.kind)).
   503  		WithRequired("items").
   504  		SetProperty("items", *spec.ArrayProperty(spec.RefSchema(refForOpenAPIVersion(name, opts.V2))).WithDescription(doc)).
   505  		SetProperty("metadata", *spec.RefSchema(refForOpenAPIVersion(listMetaSchemaRef, opts.V2)).WithDescription(swaggerPartialObjectMetadataListDescriptions["metadata"]))
   506  
   507  	addTypeMetaProperties(s, opts.V2)
   508  	s.AddExtension(endpoints.RouteMetaGVK, []map[string]string{
   509  		{
   510  			"group":   b.group,
   511  			"version": b.version,
   512  			"kind":    b.listKind,
   513  		},
   514  	})
   515  	if opts.IncludeSelectableFields {
   516  		if selectableFields := buildSelectableFields(crd, b.version); selectableFields != nil {
   517  			s.AddExtension(endpoints.RouteMetaSelectableFields, selectableFields)
   518  		}
   519  	}
   520  	return s
   521  }
   522  
   523  // getOpenAPIConfig builds config which wires up generated definitions for kube-openapi to consume
   524  func (b *builder) getOpenAPIConfig() *common.Config {
   525  	return &common.Config{
   526  		ProtocolList: []string{"https"},
   527  		Info: &spec.Info{
   528  			InfoProps: spec.InfoProps{
   529  				Title:   "Kubernetes CRD Swagger",
   530  				Version: "v0.1.0",
   531  			},
   532  		},
   533  		CommonResponses: map[int]spec.Response{
   534  			401: {
   535  				ResponseProps: spec.ResponseProps{
   536  					Description: "Unauthorized",
   537  				},
   538  			},
   539  		},
   540  		GetOperationIDAndTags: openapi.GetOperationIDAndTags,
   541  		GetDefinitionName: func(name string) (string, spec.Extensions) {
   542  			buildDefinitions.Do(generateBuildDefinitionsFunc)
   543  			return namer.GetDefinitionName(name)
   544  		},
   545  		GetDefinitions: func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
   546  			def := utilopenapi.GetOpenAPIDefinitionsWithoutDisabledFeatures(generatedopenapi.GetOpenAPIDefinitions)(ref)
   547  			def[fmt.Sprintf("%s/%s.%s", b.group, b.version, b.kind)] = common.OpenAPIDefinition{
   548  				Schema:       *b.schema,
   549  				Dependencies: []string{objectMetaType},
   550  			}
   551  			def[fmt.Sprintf("%s/%s.%s", b.group, b.version, b.listKind)] = common.OpenAPIDefinition{
   552  				Schema: *b.listSchema,
   553  			}
   554  			return def
   555  		},
   556  	}
   557  }
   558  
   559  func (b *builder) getOpenAPIV3Config() *common.OpenAPIV3Config {
   560  	return &common.OpenAPIV3Config{
   561  		Info: &spec.Info{
   562  			InfoProps: spec.InfoProps{
   563  				Title:   "Kubernetes CRD Swagger",
   564  				Version: "v0.1.0",
   565  			},
   566  		},
   567  		CommonResponses: map[int]*spec3.Response{
   568  			401: {
   569  				ResponseProps: spec3.ResponseProps{
   570  					Description: "Unauthorized",
   571  				},
   572  			},
   573  		},
   574  		GetOperationIDAndTags: openapi.GetOperationIDAndTags,
   575  		GetDefinitionName: func(name string) (string, spec.Extensions) {
   576  			buildDefinitions.Do(generateBuildDefinitionsFunc)
   577  			return namer.GetDefinitionName(name)
   578  		},
   579  		GetDefinitions: func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
   580  			def := utilopenapi.GetOpenAPIDefinitionsWithoutDisabledFeatures(generatedopenapi.GetOpenAPIDefinitions)(ref)
   581  			def[fmt.Sprintf("%s/%s.%s", b.group, b.version, b.kind)] = common.OpenAPIDefinition{
   582  				Schema:       *b.schema,
   583  				Dependencies: []string{objectMetaType},
   584  			}
   585  			def[fmt.Sprintf("%s/%s.%s", b.group, b.version, b.listKind)] = common.OpenAPIDefinition{
   586  				Schema: *b.listSchema,
   587  			}
   588  			return def
   589  		},
   590  	}
   591  }
   592  
   593  func newBuilder(crd *apiextensionsv1.CustomResourceDefinition, version string, schema *structuralschema.Structural, opts Options) *builder {
   594  	b := &builder{
   595  		schema: &spec.Schema{
   596  			SchemaProps: spec.SchemaProps{Type: []string{"object"}},
   597  		},
   598  		listSchema: &spec.Schema{},
   599  		ws:         &restful.WebService{},
   600  
   601  		group:    crd.Spec.Group,
   602  		version:  version,
   603  		kind:     crd.Spec.Names.Kind,
   604  		listKind: crd.Spec.Names.ListKind,
   605  		plural:   crd.Spec.Names.Plural,
   606  	}
   607  	if crd.Spec.Scope == apiextensionsv1.NamespaceScoped {
   608  		b.namespaced = true
   609  	}
   610  
   611  	// Pre-build schema with Kubernetes native properties
   612  	b.schema = b.buildKubeNative(crd, schema, opts, crd.Spec.PreserveUnknownFields)
   613  	b.listSchema = b.buildListSchema(crd, opts)
   614  
   615  	return b
   616  }
   617  
   618  func buildSelectableFields(crd *apiextensionsv1.CustomResourceDefinition, version string) any {
   619  	var specVersion *apiextensionsv1.CustomResourceDefinitionVersion
   620  	for _, v := range crd.Spec.Versions {
   621  		if v.Name == version {
   622  			specVersion = &v
   623  			break
   624  		}
   625  	}
   626  	if specVersion == nil && len(specVersion.SelectableFields) == 0 {
   627  		return nil
   628  	}
   629  	selectableFields := make([]any, len(specVersion.SelectableFields))
   630  	for i, sf := range specVersion.SelectableFields {
   631  		props := map[string]any{
   632  			"fieldPath": strings.TrimPrefix(sf.JSONPath, "."),
   633  		}
   634  		selectableFields[i] = props
   635  	}
   636  	return selectableFields
   637  }
   638  

View as plain text