sigs.k8s.io/cluster-api@v1.7.1/exp/runtime/catalog/catalog.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 "reflect" 22 goruntime "runtime" 23 "strings" 24 25 "github.com/pkg/errors" 26 "k8s.io/apimachinery/pkg/runtime" 27 "k8s.io/apimachinery/pkg/runtime/schema" 28 "k8s.io/apimachinery/pkg/util/naming" 29 "k8s.io/kube-openapi/pkg/common" 30 ) 31 32 // Catalog contains all information about RuntimeHooks defined in Cluster API, 33 // including mappings between RuntimeHook functions and the corresponding GroupVersionHooks, 34 // metadata and OpenAPIDefinitions. 35 type Catalog struct { 36 // scheme is used to access to api-machinery utilities to implement conversions of 37 // request and response types. 38 scheme *runtime.Scheme 39 40 // gvhToType maps a GroupVersionHook to the corresponding RuntimeHook function. 41 gvhToType map[GroupVersionHook]reflect.Type 42 43 // typeToGVH maps a RuntimeHook function to the corresponding GroupVersionHook. 44 typeToGVH map[reflect.Type]GroupVersionHook 45 46 // gvhToHookDescriptor maps a GroupVersionHook to the corresponding hook descriptor. 47 gvhToHookDescriptor map[GroupVersionHook]hookDescriptor 48 49 // openAPIDefinitions is a list of OpenAPIDefinitionsGetter, which return OpenAPI definitions 50 // for all request and response types of a GroupVersion. 51 openAPIDefinitions []OpenAPIDefinitionsGetter 52 53 // catalogName is the name of this catalog. It is set based on the stack of the New caller. 54 // This is useful for error reporting to indicate the origin of the Catalog. 55 catalogName string 56 } 57 58 // Hook is a marker interface for a RuntimeHook function. 59 // RuntimeHook functions should be defined as a: func(*RequestType, *ResponseType). 60 // The name of the func should be the name of the hook. 61 type Hook interface{} 62 63 // hookDescriptor is a data structure which holds 64 // all information about a Hook. 65 type hookDescriptor struct { 66 metadata *HookMeta 67 68 // request gvk for the Hook. 69 request schema.GroupVersionKind 70 // response gvk for the Hook. 71 response schema.GroupVersionKind 72 } 73 74 // HookMeta holds metadata for a Hook, which is used to generate 75 // the OpenAPI definition for a Hook. 76 type HookMeta struct { 77 // Summary of the hook. 78 Summary string 79 80 // Description of the hook. 81 Description string 82 83 // Tags of the hook. 84 Tags []string 85 86 // Deprecated signals if the hook is deprecated. 87 Deprecated bool 88 89 // Singleton signals if the hook can only be implemented once on a 90 // Runtime Extension, e.g. like the Discovery hook. 91 Singleton bool 92 } 93 94 // OpenAPIDefinitionsGetter defines a func which returns OpenAPI definitions for all 95 // request and response types of a GroupVersion. 96 // NOTE: The OpenAPIDefinitionsGetter funcs in the API packages are generated by openapi-gen. 97 type OpenAPIDefinitionsGetter func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition 98 99 // New creates a new Catalog. 100 func New() *Catalog { 101 return &Catalog{ 102 scheme: runtime.NewScheme(), 103 gvhToType: map[GroupVersionHook]reflect.Type{}, 104 typeToGVH: map[reflect.Type]GroupVersionHook{}, 105 gvhToHookDescriptor: map[GroupVersionHook]hookDescriptor{}, 106 openAPIDefinitions: []OpenAPIDefinitionsGetter{}, 107 // Note: We have to ignore the current file so that GetNameFromCallsite retrieves the name of the caller of New (the parent). 108 catalogName: naming.GetNameFromCallsite("sigs.k8s.io/cluster-api/exp/runtime/catalog/catalog.go"), 109 } 110 } 111 112 // AddHook adds a RuntimeHook function and its request and response types with the gv GroupVersion. 113 // The passed in hookFunc must have the following type: func(*RequestType,*ResponseType) 114 // The name of the func becomes the "Hook" in GroupVersionHook. 115 // GroupVersion must not have empty fields. 116 func (c *Catalog) AddHook(gv schema.GroupVersion, hookFunc Hook, hookMeta *HookMeta) { 117 // Validate gv.Group and gv.Version are not empty. 118 if gv.Group == "" { 119 panic("Group must not be empty") 120 } 121 if gv.Version == "" { 122 panic("Version must not be empty") 123 } 124 125 // Validate that hookFunc is a func. 126 t := reflect.TypeOf(hookFunc) 127 if t.Kind() != reflect.Func { 128 panic("Hook must be a func") 129 } 130 if t.NumIn() != 2 { 131 panic("Hook must have two input parameter: *RequestType, *ResponseType") 132 } 133 if t.NumOut() != 0 { 134 panic("Hook must have no output parameter") 135 } 136 137 // Create request and response objects based on the input types. 138 request, ok := reflect.New(t.In(0).Elem()).Interface().(runtime.Object) 139 if !ok { 140 panic("Hook request (first parameter) must be a runtime.Object") 141 } 142 response, ok := reflect.New(t.In(1).Elem()).Interface().(runtime.Object) 143 if !ok { 144 panic("Hook response (second parameter) must be a runtime.Object") 145 } 146 147 // Calculate the hook name based on the func name. 148 hookName := HookName(hookFunc) 149 150 gvh := GroupVersionHook{ 151 Group: gv.Group, 152 Version: gv.Version, 153 Hook: hookName, 154 } 155 156 // Validate that the GVH is not already registered with another type. 157 if oldT, found := c.gvhToType[gvh]; found && oldT != t { 158 panic(fmt.Sprintf("Double registration of different type for %v: old=%v.%v, new=%v.%v in catalog %q", gvh, oldT.PkgPath(), oldT.Name(), t.PkgPath(), t.Name(), c.catalogName)) 159 } 160 161 // Add GVH <=> RuntimeHook function mappings. 162 c.gvhToType[gvh] = t 163 c.typeToGVH[t] = gvh 164 165 // Add Request and Response types to scheme. 166 c.scheme.AddKnownTypes(gv, request) 167 c.scheme.AddKnownTypes(gv, response) 168 169 // Create a hook descriptor and store it in the GVH => Descriptor map. 170 requestGVK, err := c.GroupVersionKind(request) 171 if err != nil { 172 panic(fmt.Sprintf("Failed to get GVK for request %T: %v", request, err)) 173 } 174 responseGVK, err := c.GroupVersionKind(response) 175 if err != nil { 176 panic(fmt.Sprintf("Failed to get GVK for response %T: %v", request, err)) 177 } 178 if hookMeta == nil { 179 panic("hookMeta cannot be nil") 180 } 181 c.gvhToHookDescriptor[gvh] = hookDescriptor{ 182 metadata: hookMeta, 183 request: requestGVK, 184 response: responseGVK, 185 } 186 } 187 188 // AddOpenAPIDefinitions adds an OpenAPIDefinitionsGetter. 189 func (c *Catalog) AddOpenAPIDefinitions(getter OpenAPIDefinitionsGetter) { 190 c.openAPIDefinitions = append(c.openAPIDefinitions, getter) 191 } 192 193 // Convert will attempt to convert in into out. Both must be pointers. 194 // Returns an error if the conversion isn't possible. 195 func (c *Catalog) Convert(in, out interface{}, context interface{}) error { 196 return c.scheme.Convert(in, out, context) 197 } 198 199 // GroupVersionHook returns the GVH of the hookFunc or an error if hook is not a function 200 // or not registered. 201 func (c *Catalog) GroupVersionHook(hookFunc Hook) (GroupVersionHook, error) { 202 // Validate that hookFunc is a func. 203 t := reflect.TypeOf(hookFunc) 204 if t.Kind() != reflect.Func { 205 return emptyGroupVersionHook, errors.Errorf("hook %s is not a func", HookName(hookFunc)) 206 } 207 208 gvh, ok := c.typeToGVH[t] 209 if !ok { 210 return emptyGroupVersionHook, errors.Errorf("hook %s is not registered in catalog %q", HookName(hookFunc), c.catalogName) 211 } 212 return gvh, nil 213 } 214 215 // GroupVersionKind returns the GVK of the object or an error if the object is not a pointer 216 // or not registered. 217 func (c *Catalog) GroupVersionKind(obj runtime.Object) (schema.GroupVersionKind, error) { 218 gvks, _, err := c.scheme.ObjectKinds(obj) 219 if err != nil { 220 return emptyGroupVersionKind, errors.Errorf("failed to get GVK for object: %v", err) 221 } 222 223 if len(gvks) > 1 { 224 return emptyGroupVersionKind, errors.Errorf("failed to get GVK for object: multiple GVKs: %s", gvks) 225 } 226 return gvks[0], nil 227 } 228 229 // Request returns the GroupVersionKind of the request of a GroupVersionHook. 230 func (c *Catalog) Request(hook GroupVersionHook) (schema.GroupVersionKind, error) { 231 descriptor, ok := c.gvhToHookDescriptor[hook] 232 if !ok { 233 return emptyGroupVersionKind, errors.Errorf("failed to get request GVK for hook %s: hook is not registered in catalog %q", hook, c.catalogName) 234 } 235 236 return descriptor.request, nil 237 } 238 239 // Response returns the GroupVersionKind of the response of a GroupVersionHook. 240 func (c *Catalog) Response(hook GroupVersionHook) (schema.GroupVersionKind, error) { 241 descriptor, ok := c.gvhToHookDescriptor[hook] 242 if !ok { 243 return emptyGroupVersionKind, errors.Errorf("failed to get response GVK for hook %s: hook is not registered in catalog %q", hook, c.catalogName) 244 } 245 246 return descriptor.response, nil 247 } 248 249 // NewRequest returns a request object for a GroupVersionHook. 250 func (c *Catalog) NewRequest(hook GroupVersionHook) (runtime.Object, error) { 251 descriptor, ok := c.gvhToHookDescriptor[hook] 252 if !ok { 253 return nil, errors.Errorf("failed to create request object for hook %s: hook is not registered in catalog %q", hook, c.catalogName) 254 } 255 obj, err := c.scheme.New(descriptor.request) 256 if err != nil { 257 return nil, errors.Wrapf(err, "failed to create request object for hook %s", hook) 258 } 259 return obj, nil 260 } 261 262 // NewResponse returns a response object for a GroupVersionHook. 263 func (c *Catalog) NewResponse(hook GroupVersionHook) (runtime.Object, error) { 264 descriptor, ok := c.gvhToHookDescriptor[hook] 265 if !ok { 266 return nil, errors.Errorf("failed to create response object for hook %s: hook is not registered in catalog %q", hook, c.catalogName) 267 } 268 obj, err := c.scheme.New(descriptor.response) 269 if err != nil { 270 return nil, errors.Wrapf(err, "failed to create response object for hook %s", hook) 271 } 272 return obj, nil 273 } 274 275 // ValidateRequest validates a request object. Specifically it validates that 276 // the GVK of an object matches the GVK of the request of the Hook. 277 func (c *Catalog) ValidateRequest(hook GroupVersionHook, obj runtime.Object) error { 278 // Get GVK of obj. 279 objGVK, err := c.GroupVersionKind(obj) 280 if err != nil { 281 return errors.Wrapf(err, "failed to validate request for hook %s", hook) 282 } 283 284 // Get request GVK from hook. 285 hookGVK, err := c.Request(hook) 286 if err != nil { 287 return errors.Wrapf(err, "failed to validate request for hook %s", hook) 288 } 289 290 if objGVK != hookGVK { 291 return errors.Errorf("request object of hook %s has invalid GVK %q, expected %q", hook, objGVK, hookGVK) 292 } 293 return nil 294 } 295 296 // ValidateResponse validates a response object. Specifically it validates that 297 // the GVK of an object matches the GVK of the response of the Hook. 298 func (c *Catalog) ValidateResponse(hook GroupVersionHook, obj runtime.Object) error { 299 // Get GVK of obj. 300 objGVK, err := c.GroupVersionKind(obj) 301 if err != nil { 302 return errors.Wrapf(err, "failed to validate response for hook %s", hook) 303 } 304 305 // Get response GVK from hook. 306 hookGVK, err := c.Response(hook) 307 if err != nil { 308 return errors.Wrapf(err, "failed to validate response for hook %s", hook) 309 } 310 311 if objGVK != hookGVK { 312 return errors.Errorf("response object of hook %s has invalid GVK %q, expected %q", hook, objGVK, hookGVK) 313 } 314 return nil 315 } 316 317 // IsHookRegistered returns true if the GroupVersionHook is registered with the catalog. 318 func (c *Catalog) IsHookRegistered(gvh GroupVersionHook) bool { 319 _, found := c.gvhToType[gvh] 320 return found 321 } 322 323 // GroupVersionHook unambiguously identifies a Hook. 324 type GroupVersionHook struct { 325 Group string 326 Version string 327 Hook string 328 } 329 330 // Empty returns true if group, version and hook are empty. 331 func (gvh GroupVersionHook) Empty() bool { 332 return gvh.Group == "" && gvh.Version == "" && gvh.Hook == "" 333 } 334 335 // GroupVersion returns the GroupVersion of a GroupVersionHook. 336 func (gvh GroupVersionHook) GroupVersion() schema.GroupVersion { 337 return schema.GroupVersion{Group: gvh.Group, Version: gvh.Version} 338 } 339 340 // GroupHook returns the GroupHook of a GroupVersionHook. 341 func (gvh GroupVersionHook) GroupHook() GroupHook { 342 return GroupHook{Group: gvh.Group, Hook: gvh.Hook} 343 } 344 345 // String returns a string representation of a GroupVersionHook. 346 func (gvh GroupVersionHook) String() string { 347 return strings.Join([]string{gvh.Group, "/", gvh.Version, ", Hook=", gvh.Hook}, "") 348 } 349 350 // HookName returns the name of the runtime hook. 351 // Note: The name of the hook is the name of the hookFunc. 352 func HookName(hookFunc Hook) string { 353 hookFuncName := goruntime.FuncForPC(reflect.ValueOf(hookFunc).Pointer()).Name() 354 hookName := hookFuncName[strings.LastIndex(hookFuncName, ".")+1:] 355 return hookName 356 } 357 358 var emptyGroupVersionHook = GroupVersionHook{} 359 360 var emptyGroupVersionKind = schema.GroupVersionKind{} 361 362 // GroupHook represents Group and Hook of a GroupVersionHook. 363 // This can be used instead of GroupVersionHook when 364 // Version should not be used. 365 type GroupHook struct { 366 Group string 367 Hook string 368 } 369 370 // String returns a string representation of a GroupHook. 371 func (gh GroupHook) String() string { 372 if gh.Group == "" { 373 return gh.Hook 374 } 375 return gh.Hook + "." + gh.Group 376 } 377 378 // GVHToPath calculates the path for a given GroupVersionHook. 379 // This func is aligned with Kubernetes paths for cluster-wide resources, e.g.: 380 // /apis/storage.k8s.io/v1/storageclasses/standard. 381 // Note: name is only appended if set, e.g. the Discovery hook does not have a name. 382 func GVHToPath(gvh GroupVersionHook, name string) string { 383 if name == "" { 384 return fmt.Sprintf("/%s/%s/%s", gvh.Group, gvh.Version, strings.ToLower(gvh.Hook)) 385 } 386 return fmt.Sprintf("/%s/%s/%s/%s", gvh.Group, gvh.Version, strings.ToLower(gvh.Hook), strings.ToLower(name)) 387 }