1
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
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
74
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
88 type Options struct {
89
90 V2 bool
91
92
93 StripValueValidation bool
94
95
96 StripNullable bool
97
98
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
119 if opts.AllowNonStructural || len(structuralschema.ValidateStructural(nil, ss)) == 0 {
120 schema = ss
121
122
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
137
138
139
140 b := newBuilder(crd, version, schema, opts)
141
142
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
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
214 var _ = util.OpenAPICanonicalTypeNamer(&CRDCanonicalTypeNamer{})
215
216
217
218 type CRDCanonicalTypeNamer struct {
219 group string
220 version string
221 kind string
222 }
223
224
225 func (c *CRDCanonicalTypeNamer) OpenAPICanonicalTypeName() string {
226 return fmt.Sprintf("%s/%s.%s", c.group, c.version, c.kind)
227 }
228
229
230
231
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
247
248
249
250
251
252
253
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
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
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
303
304
305
306
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
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
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
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
378
379 func (b *builder) buildKubeNative(crd *apiextensionsv1.CustomResourceDefinition, schema *structuralschema.Structural, opts Options, crdPreserveUnknownFields bool) (ret *spec.Schema) {
380
381
382
383
384 if schema == nil || (opts.V2 && (schema.XPreserveUnknownFields || crdPreserveUnknownFields)) {
385 ret = &spec.Schema{
386 SchemaProps: spec.SchemaProps{Type: []string{"object"}},
387 }
388
389
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
436
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
459
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
489
490
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
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
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
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