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  }