sigs.k8s.io/cluster-api@v1.7.1/exp/runtime/catalog/openapi.go (about)

     1  /*
     2  Copyright 2022 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 catalog
    18  
    19  import (
    20  	"fmt"
    21  	"net/http"
    22  	"reflect"
    23  	"strings"
    24  
    25  	"github.com/pkg/errors"
    26  	"golang.org/x/text/cases"
    27  	"golang.org/x/text/language"
    28  	"k8s.io/apimachinery/pkg/runtime/schema"
    29  	"k8s.io/kube-openapi/pkg/common"
    30  	"k8s.io/kube-openapi/pkg/spec3"
    31  	"k8s.io/kube-openapi/pkg/validation/spec"
    32  )
    33  
    34  // OpenAPI generates and returns the OpenAPI spec.
    35  func (c *Catalog) OpenAPI(version string) (*spec3.OpenAPI, error) {
    36  	openAPI := &spec3.OpenAPI{
    37  		Version: "3.0.0",
    38  		Info: &spec.Info{
    39  			InfoProps: spec.InfoProps{
    40  				Description: "This document defines the Open API specification of the services that Cluster API runtime is going " +
    41  					"to call while managing the Cluster's lifecycle.\n" +
    42  					"\n" +
    43  					"Services described in this specification are also referred to as Runtime Hooks, given that they allow " +
    44  					"external components to hook-in the cluster's lifecycle. The corresponding components implementing handlers " +
    45  					"for Runtime Hooks calls are referred to as Runtime Extensions.\n" +
    46  					"\n" +
    47  					"More information is available in the [Cluster API book](https://cluster-api.sigs.k8s.io/).",
    48  				Title: "Cluster API - Runtime SDK",
    49  				License: &spec.License{
    50  					Name: "Apache 2.0",
    51  					URL:  "http://www.apache.org/licenses/LICENSE-2.0.html",
    52  				},
    53  				Version: version,
    54  			},
    55  		},
    56  		Paths: &spec3.Paths{
    57  			Paths: map[string]*spec3.Path{},
    58  		},
    59  		Components: &spec3.Components{
    60  			Schemas: map[string]*spec.Schema{},
    61  		},
    62  	}
    63  
    64  	for gvh, hookDescriptor := range c.gvhToHookDescriptor {
    65  		err := addHookAndTypesToOpenAPI(openAPI, c, gvh, hookDescriptor)
    66  		if err != nil {
    67  			return nil, err
    68  		}
    69  	}
    70  
    71  	return openAPI, nil
    72  }
    73  
    74  func addHookAndTypesToOpenAPI(openAPI *spec3.OpenAPI, c *Catalog, gvh GroupVersionHook, hookDescriptor hookDescriptor) error {
    75  	// Create the operation.
    76  	operation := &spec3.Operation{
    77  		OperationProps: spec3.OperationProps{
    78  			Tags:        hookDescriptor.metadata.Tags,
    79  			Summary:     hookDescriptor.metadata.Summary,
    80  			Description: hookDescriptor.metadata.Description,
    81  			OperationId: operationID(gvh),
    82  			Responses: &spec3.Responses{
    83  				ResponsesProps: spec3.ResponsesProps{
    84  					StatusCodeResponses: make(map[int]*spec3.Response),
    85  				},
    86  			},
    87  			Deprecated: hookDescriptor.metadata.Deprecated,
    88  		},
    89  	}
    90  	path := GVHToPath(gvh, "")
    91  
    92  	// Add name parameter to operation path if necessary.
    93  	// This e.g. reflects the real path which a Runtime Extensions will handle.
    94  	if !hookDescriptor.metadata.Singleton {
    95  		path = GVHToPath(gvh, "{name}")
    96  		operation.Parameters = append(operation.Parameters, &spec3.Parameter{
    97  			ParameterProps: spec3.ParameterProps{
    98  				Name:        "name",
    99  				In:          "path",
   100  				Description: "The handler name. Handlers within a single external component implementing Runtime Extensions must have different names",
   101  				Required:    true,
   102  				Schema: &spec.Schema{
   103  					SchemaProps: spec.SchemaProps{
   104  						Type: []string{"string"},
   105  					},
   106  				},
   107  			},
   108  		})
   109  	}
   110  
   111  	// Add request type to operation.
   112  	requestGVK, err := c.Request(gvh)
   113  	if err != nil {
   114  		return err
   115  	}
   116  	requestType, ok := c.scheme.AllKnownTypes()[requestGVK]
   117  	if !ok {
   118  		return errors.Errorf("type for request GVK %q is unknown", requestGVK)
   119  	}
   120  	requestTypeName := typeName(requestType, requestGVK)
   121  	operation.RequestBody = &spec3.RequestBody{
   122  		RequestBodyProps: spec3.RequestBodyProps{
   123  			Content: createContent(requestTypeName),
   124  		},
   125  	}
   126  	if err := addTypeToOpenAPI(openAPI, c, requestTypeName); err != nil {
   127  		return err
   128  	}
   129  
   130  	// Add response type to operation.
   131  	responseGVK, err := c.Response(gvh)
   132  	if err != nil {
   133  		return err
   134  	}
   135  	responseType := c.scheme.AllKnownTypes()[responseGVK]
   136  	if !ok {
   137  		return errors.Errorf("type for response GVK %q is unknown", responseGVK)
   138  	}
   139  	responseTypeName := typeName(responseType, responseGVK)
   140  	operation.Responses.StatusCodeResponses[http.StatusOK] = &spec3.Response{
   141  		ResponseProps: spec3.ResponseProps{
   142  			Description: "Status code 200 indicates that the request has been processed successfully. Runtime Extension authors must use fields in the response like e.g. status and message to return processing outcomes.",
   143  			Content:     createContent(responseTypeName),
   144  		},
   145  	}
   146  	if err := addTypeToOpenAPI(openAPI, c, responseTypeName); err != nil {
   147  		return err
   148  	}
   149  
   150  	// Add operation to openAPI.
   151  	openAPI.Paths.Paths[path] = &spec3.Path{
   152  		PathProps: spec3.PathProps{
   153  			Post: operation,
   154  		},
   155  	}
   156  	return nil
   157  }
   158  
   159  func createContent(typeName string) map[string]*spec3.MediaType {
   160  	return map[string]*spec3.MediaType{
   161  		"application/json": {
   162  			MediaTypeProps: spec3.MediaTypeProps{
   163  				Schema: &spec.Schema{
   164  					SchemaProps: spec.SchemaProps{
   165  						Ref: componentRef(typeName),
   166  					},
   167  				},
   168  			},
   169  		},
   170  	}
   171  }
   172  
   173  func addTypeToOpenAPI(openAPI *spec3.OpenAPI, c *Catalog, typeName string) error {
   174  	componentName := componentName(typeName)
   175  
   176  	// Check if schema already has been added.
   177  	if _, ok := openAPI.Components.Schemas[componentName]; ok {
   178  		return nil
   179  	}
   180  
   181  	// Loop through all OpenAPIDefinitions, so we don't have to lookup typeName => package
   182  	// (which we couldn't do for external packages like clusterv1 because we cannot map typeName
   183  	// to a package without hard-coding the mapping).
   184  	var openAPIDefinition *common.OpenAPIDefinition
   185  	for _, openAPIDefinitionsGetter := range c.openAPIDefinitions {
   186  		openAPIDefinitions := openAPIDefinitionsGetter(componentRef)
   187  
   188  		if def, ok := openAPIDefinitions[typeName]; ok {
   189  			openAPIDefinition = &def
   190  			break
   191  		}
   192  	}
   193  
   194  	if openAPIDefinition == nil {
   195  		return errors.Errorf("failed to get definition for %v. If you added a new type, you may need to add +k8s:openapi-gen=true to the package or type and run openapi-gen again", typeName)
   196  	}
   197  
   198  	// Add schema for component to components.
   199  	openAPI.Components.Schemas[componentName] = &spec.Schema{
   200  		VendorExtensible:   openAPIDefinition.Schema.VendorExtensible,
   201  		SchemaProps:        openAPIDefinition.Schema.SchemaProps,
   202  		SwaggerSchemaProps: openAPIDefinition.Schema.SwaggerSchemaProps,
   203  	}
   204  
   205  	// Add schema for dependencies to components recursively.
   206  	for _, d := range openAPIDefinition.Dependencies {
   207  		if err := addTypeToOpenAPI(openAPI, c, d); err != nil {
   208  			return err
   209  		}
   210  	}
   211  
   212  	return nil
   213  }
   214  
   215  // typeName calculates a type name. This matches the format used in the generated
   216  // GetOpenAPIDefinitions funcs, e.g. "k8s.io/api/core/v1.ObjectReference".
   217  func typeName(t reflect.Type, gvk schema.GroupVersionKind) string {
   218  	return fmt.Sprintf("%s.%s", t.PkgPath(), gvk.Kind)
   219  }
   220  
   221  // componentRef calculates a componentRef which is used in the OpenAPI specification
   222  // to reference components in the components section.
   223  func componentRef(typeName string) spec.Ref {
   224  	return spec.MustCreateRef(fmt.Sprintf("#/components/schemas/%s", componentName(typeName)))
   225  }
   226  
   227  // componentName calculates the componentName for the OpenAPI specification based on a typeName.
   228  // For example: "k8s.io/api/core/v1.ObjectReference" => "k8s.io.api.core.v1.ObjectReference".
   229  // Note: This is necessary because we cannot use additional Slashes in the componentRef.
   230  func componentName(typeName string) string {
   231  	return strings.ReplaceAll(typeName, "/", ".")
   232  }
   233  
   234  // operationID calculates an operationID similar to Kubernetes OpenAPI.
   235  // Kubernetes examples:
   236  // * readRbacAuthorizationV1NamespacedRole
   237  // * listExtensionsV1beta1IngressForAllNamespaces
   238  // In our case:
   239  // * hooksRuntimeClusterV1alpha1Discovery.
   240  func operationID(gvh GroupVersionHook) string {
   241  	shortAPIGroup := strings.TrimSuffix(gvh.Group, ".x-k8s.io")
   242  
   243  	split := strings.Split(shortAPIGroup, ".")
   244  	title := cases.Title(language.Und)
   245  
   246  	res := split[0]
   247  	for i := 1; i < len(split); i++ {
   248  		res += title.String(split[i])
   249  	}
   250  	res += title.String(gvh.Version) + title.String(gvh.Hook)
   251  
   252  	return res
   253  }