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 }