diff --git a/go.mod b/go.mod index 82fdac0b8b84..6acf98699b24 100644 --- a/go.mod +++ b/go.mod @@ -153,9 +153,11 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect k8s.io/cli-runtime v0.23.0 // indirect - k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect + k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect sigs.k8s.io/kustomize/api v0.10.1 // indirect sigs.k8s.io/kustomize/kyaml v0.13.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect ) + +require github.com/emicklei/go-restful v2.15.0+incompatible // indirect diff --git a/go.sum b/go.sum index 90bbe0b6ba75..20c0b3beaf93 100644 --- a/go.sum +++ b/go.sum @@ -188,6 +188,8 @@ github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7fo github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.15.0+incompatible h1:8KpYO/Xl/ZudZs5RNOEhWMBY4hmzlZhhRd9cu+jrZP4= +github.com/emicklei/go-restful v2.15.0+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= diff --git a/hack/tools/go.sum b/hack/tools/go.sum index 751a2b3d73d4..5790c22b7edc 100644 --- a/hack/tools/go.sum +++ b/hack/tools/go.sum @@ -347,6 +347,7 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.15.0+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= diff --git a/internal/runtime/catalog/builder.go b/internal/runtime/catalog/builder.go new file mode 100644 index 000000000000..95c7f03a6585 --- /dev/null +++ b/internal/runtime/catalog/builder.go @@ -0,0 +1,85 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package catalog + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// Builder builds a Catalog to allow mappings between Go types and the corresponding +// GroupVersionHooks, metadata and OpenAPIDefinitions. +type Builder struct { + // GroupVersion is the GroupVersion used when registering + // Hooks and OpenAPIDefinitions. + GroupVersion schema.GroupVersion + + // catalogBuilder are the functions which are used + // to register Hooks and OpenAPIDefinitionsGetter into a catalog. + catalogBuilder []func(*Catalog) + + // SchemeBuilder are the functions which are used + // to add the types of Hook requests and responses to + // the scheme of a catalog. + schemeBuilder runtime.SchemeBuilder +} + +// RegisterHook registers a Hook and its request and response types. +// The passed in hookFunc must have the following type: func(*RequestType,*ResponseType) +// The name of the func becomes the "hook" field of the GroupVersionHook. +func (bld *Builder) RegisterHook(hookFunc Hook, hookMeta *HookMeta) *Builder { + bld.catalogBuilder = append(bld.catalogBuilder, func(catalog *Catalog) { + catalog.AddHook(bld.GroupVersion, hookFunc, hookMeta) + }) + return bld +} + +// RegisterOpenAPIDefinitions registers a func returning the OpenAPIDefinitions for +// request and response types of all Runtime Hooks of a given group version. +// NOTE: The OpenAPIDefinitionsGetter funcs in the API packages are generated by +// openapi-gen. +func (bld *Builder) RegisterOpenAPIDefinitions(getter OpenAPIDefinitionsGetter) *Builder { + bld.catalogBuilder = append(bld.catalogBuilder, func(c *Catalog) { + c.AddOpenAPIDefinitions(bld.GroupVersion, getter) + }) + return bld +} + +// AddToCatalog adds all registered Hooks their request and response types and the +// OpenAPIDefinitions to the catalog. +func (bld *Builder) AddToCatalog(catalog *Catalog) error { + for _, addTo := range bld.catalogBuilder { + addTo(catalog) + } + return bld.schemeBuilder.AddToScheme(catalog.scheme) +} + +// Register adds a scheme setup function to the schemeBuilder. +// Note: This function is used by generated conversion code. +// If we would just expose the func from the embedded schemeBuilder +// directly it would not work because it is nil at that time and +// appending to a nil schemeBuilder doesn't propagate to our builder. +func (bld *Builder) Register(f func(*runtime.Scheme) error) { + bld.schemeBuilder.Register(f) +} + +// Build returns a new Catalog containing all registered Hooks their request and response types +// and the OpenAPIDefinitions. +func (bld *Builder) Build() (*Catalog, error) { + s := New() + return s, bld.AddToCatalog(s) +} diff --git a/internal/runtime/catalog/catalog.go b/internal/runtime/catalog/catalog.go new file mode 100644 index 000000000000..f9ecc79d3a84 --- /dev/null +++ b/internal/runtime/catalog/catalog.go @@ -0,0 +1,336 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package catalog + +import ( + "fmt" + "reflect" + goruntime "runtime" + "strings" + + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/naming" + "k8s.io/kube-openapi/pkg/common" +) + +// Catalog contains all information about RuntimeHooks defined in Cluster API, +// including mappings between RuntimeHook functions and the corresponding GroupVersionHooks, +// metadata and OpenAPIDefinitions. +type Catalog struct { + // scheme is used to access to api-machinery utilities to implement conversions of + // request and response types. + scheme *runtime.Scheme + + // gvhToType maps a GroupVersionHook to the corresponding RuntimeHook function. + gvhToType map[GroupVersionHook]reflect.Type + + // gvhToType maps a RuntimeHook to the corresponding GroupVersionHook. + typeToGVH map[reflect.Type]GroupVersionHook + + // gvhToHookDescriptor maps a GroupVersionHook to the corresponding hook descriptor. + gvhToHookDescriptor map[GroupVersionHook]hookDescriptor + + // gvToOpenAPIDefinitions maps a GroupVersion to a func + // which returns OpenAPI definitions for all request and response types with that + // GroupVersion. + gvToOpenAPIDefinitions map[schema.GroupVersion]OpenAPIDefinitionsGetter + + // catalogName is the name of this catalog. It is set based on the stack of the New caller. + // This is useful for error reporting to indicate the origin of the Catalog. + catalogName string +} + +// Hook is a marker interface for a hook. +// Hooks should be defined as a: func(*RequestType, *ResponseType). +type Hook interface{} + +// hookDescriptor is a data structure which holds +// all information about a Hook. +type hookDescriptor struct { + metadata *HookMeta + + // request gvk for the Hook. + request schema.GroupVersionKind + // response gvk for the Hook. + response schema.GroupVersionKind +} + +// HookMeta holds metadata for a Hook, which is used to generate +// the OpenAPI definition for a Hook. +type HookMeta struct { + // Summary of the hook. + Summary string + + // Description of the hook. + Description string + + // Tags of the hook. + Tags []string + + // Deprecated signals if the hook is deprecated. + Deprecated bool + + // Singleton signals if the hook can only be implemented once on a + // Runtime Extension, e.g. like the DiscoveryHook. + Singleton bool +} + +// OpenAPIDefinitionsGetter defines a func which returns OpenAPI definitions for all +// request and response types of a GroupVersion. +// NOTE: The OpenAPIDefinitionsGetter funcs in the API packages are generated by openapi-gen. +type OpenAPIDefinitionsGetter func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition + +// New creates a new Catalog. +func New() *Catalog { + return &Catalog{ + scheme: runtime.NewScheme(), + gvhToType: map[GroupVersionHook]reflect.Type{}, + typeToGVH: map[reflect.Type]GroupVersionHook{}, + gvhToHookDescriptor: map[GroupVersionHook]hookDescriptor{}, + gvToOpenAPIDefinitions: map[schema.GroupVersion]OpenAPIDefinitionsGetter{}, + // Note: We have to ignore the current file so that GetNameFromCallsite retrieves the name of the caller (the parent). + catalogName: naming.GetNameFromCallsite("sigs.k8s.io/cluster-api/internal/runtime/catalog/catalog.go"), + } +} + +// AddHook adds a HookFunc and its request and response types with the gv GroupVersion. +// The passed in hookFunc must have the following type: func(*RequestType,*ResponseType) +// The name of the func becomes the "hook" in GroupVersionHook. +// GroupVersion must not have empty fields. +func (c *Catalog) AddHook(gv schema.GroupVersion, hookFunc Hook, hookMeta *HookMeta) { + // Validate gv.Group and gv.Version are not empty. + if gv.Group == "" { + panic("group must not be empty") + } + if gv.Version == "" { + panic("version must not be empty") + } + + // Validate that hookFunc is a func. + t := reflect.TypeOf(hookFunc) + if t.Kind() != reflect.Func { + panic("hook must be a func") + } + if t.NumIn() != 2 { + panic("hook must have two input parameter: *RequestType, *ResponseType") + } + if t.NumOut() != 0 { + panic("hook must have no output parameter") + } + + // Create request and response objects based on the input types. + request, ok := reflect.New(t.In(0).Elem()).Interface().(runtime.Object) + if !ok { + panic("hook request (first parameter) must be a runtime.Object") + } + response, ok := reflect.New(t.In(1).Elem()).Interface().(runtime.Object) + if !ok { + panic("hook response (second parameter) must be a runtime.Object") + } + + // Calculate the hook name based on the func name. + hookFuncName := goruntime.FuncForPC(reflect.ValueOf(hookFunc).Pointer()).Name() + hookName := hookFuncName[strings.LastIndex(hookFuncName, ".")+1:] + + gvh := GroupVersionHook{ + Group: gv.Group, + Version: gv.Version, + Hook: hookName, + } + + // Validate that the GVH is not already registered with another type. + if oldT, found := c.gvhToType[gvh]; found && oldT != t { + 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)) + } + + // Add GVH <=> RuntimeHook mappings. + c.gvhToType[gvh] = t + c.typeToGVH[t] = gvh + + // Add Request and Response types to scheme. + c.scheme.AddKnownTypes(gv, request) + c.scheme.AddKnownTypes(gv, response) + + // Create a hook descriptor and store it in the GVH => Descriptor map. + requestGVK, err := c.GroupVersionKind(request) + if err != nil { + panic(fmt.Sprintf("failed to get GVK for request %T: %v", request, err)) + } + responseGVK, err := c.GroupVersionKind(response) + if err != nil { + panic(fmt.Sprintf("failed to get GVK for response %T: %v", request, err)) + } + if hookMeta == nil { + panic("hookMeta cannot be nil") + } + c.gvhToHookDescriptor[gvh] = hookDescriptor{ + metadata: hookMeta, + request: requestGVK, + response: responseGVK, + } +} + +// AddOpenAPIDefinitions adds an OpenAPIDefinitionsGetter with the gv GroupVersion. +func (c *Catalog) AddOpenAPIDefinitions(gv schema.GroupVersion, getter OpenAPIDefinitionsGetter) { + if _, ok := c.gvToOpenAPIDefinitions[gv]; ok { + panic(fmt.Sprintf("double registration of OpenAPI definitions for GroupVersion %q", gv)) + } + c.gvToOpenAPIDefinitions[gv] = getter +} + +// Convert will attempt to convert in into out. Both must be pointers. +// Returns an error if the conversion isn't possible. +func (c *Catalog) Convert(in, out interface{}, context interface{}) error { + return c.scheme.Convert(in, out, context) +} + +// GroupVersionHook returns the GVH of the Hook or an error if hook is not a function +// or not registered. +func (c *Catalog) GroupVersionHook(hookFunc Hook) (GroupVersionHook, error) { + // Validate that hookFunc is a func. + t := reflect.TypeOf(hookFunc) + if t.Kind() != reflect.Func { + return emptyGroupVersionHook, errors.Errorf("hook %T is not a func", hookFunc) + } + + gvh, ok := c.typeToGVH[t] + if !ok { + return emptyGroupVersionHook, errors.Errorf("hook %T is not registered in catalog %q", hookFunc, c.catalogName) + } + return gvh, nil +} + +// GroupVersionKind returns the GVK of the object or an error if the object is not a pointer +// or not registered. +func (c *Catalog) GroupVersionKind(obj runtime.Object) (schema.GroupVersionKind, error) { + gvks, _, err := c.scheme.ObjectKinds(obj) + if err != nil { + return emptyGroupVersionKind, errors.Errorf("failed to get GVK for object: %v", err) + } + + if len(gvks) > 1 { + return emptyGroupVersionKind, errors.Errorf("failed to get GVK for object: multiple GVKs: %s", gvks) + } + return gvks[0], nil +} + +// Request returns the GroupVersionKind of the request of a GroupVersionHook. +func (c *Catalog) Request(hook GroupVersionHook) (schema.GroupVersionKind, error) { + descriptor, ok := c.gvhToHookDescriptor[hook] + if !ok { + return emptyGroupVersionKind, errors.Errorf("hook %T is not registered in catalog %q", hook, c.catalogName) + } + + return descriptor.request, nil +} + +// Response returns the GroupVersionKind of the response of a GroupVersionHook. +func (c *Catalog) Response(hook GroupVersionHook) (schema.GroupVersionKind, error) { + descriptor, ok := c.gvhToHookDescriptor[hook] + if !ok { + return emptyGroupVersionKind, errors.Errorf("hook %T is not registered in catalog %q", hook, c.catalogName) + } + + return descriptor.response, nil +} + +// NewRequest returns a request object for a GroupVersionHook. +func (c *Catalog) NewRequest(hook GroupVersionHook) (runtime.Object, error) { + descriptor, ok := c.gvhToHookDescriptor[hook] + if !ok { + return nil, errors.Errorf("hook %T is not registered in catalog %q", hook, c.catalogName) + } + return c.scheme.New(descriptor.request) +} + +// NewResponse returns a response object for a GroupVersionHook. +func (c *Catalog) NewResponse(hook GroupVersionHook) (runtime.Object, error) { + descriptor, ok := c.gvhToHookDescriptor[hook] + if !ok { + return nil, errors.Errorf("hook %T is not registered in catalog %q", hook, c.catalogName) + } + return c.scheme.New(descriptor.response) +} + +// ValidateRequest validates a request object. Specifically it validates that +// the GVK of an object matches the GVK of the request of the Hook. +func (c *Catalog) ValidateRequest(hook GroupVersionHook, obj runtime.Object) error { + // Get GVK of obj. + objGVK, err := c.GroupVersionKind(obj) + if err != nil { + return err + } + + // Get request GVK from hook. + hookGVK, err := c.Request(hook) + if err != nil { + return err + } + + if objGVK != hookGVK { + return errors.Errorf("request object has invalid GVK %q, expected %q", objGVK, hookGVK) + } + return nil +} + +// ValidateResponse validates a response object. Specifically it validates that +// the GVK of an object matches the GVK of the response of the Hook. +func (c *Catalog) ValidateResponse(hook GroupVersionHook, obj runtime.Object) error { + // Get GVK of obj. + objGVK, err := c.GroupVersionKind(obj) + if err != nil { + return err + } + + // Get response GVK from hook. + hookGVK, err := c.Response(hook) + if err != nil { + return err + } + + if objGVK != hookGVK { + return errors.Errorf("response object has invalid GVK %q, expected %q", objGVK, hookGVK) + } + return nil +} + +// GroupVersionHook unambiguously identifies a Hook. +type GroupVersionHook struct { + Group string + Version string + Hook string +} + +// Empty returns true if group, version, and hook are empty. +func (gvh GroupVersionHook) Empty() bool { + return gvh.Group == "" && gvh.Version == "" && gvh.Hook == "" +} + +// GroupVersion returns the GroupVersion of a GroupVersionHook. +func (gvh GroupVersionHook) GroupVersion() schema.GroupVersion { + return schema.GroupVersion{Group: gvh.Group, Version: gvh.Version} +} + +func (gvh GroupVersionHook) String() string { + return strings.Join([]string{gvh.Group, "/", gvh.Version, ", Hook=", gvh.Hook}, "") +} + +var emptyGroupVersionHook = GroupVersionHook{} + +var emptyGroupVersionKind = schema.GroupVersionKind{} diff --git a/internal/runtime/catalog/doc.go b/internal/runtime/catalog/doc.go new file mode 100644 index 000000000000..6b75a579ca5b --- /dev/null +++ b/internal/runtime/catalog/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package catalog provides the Catalog and corresponding builders. +package catalog diff --git a/internal/runtime/catalog/test/catalog_test.go b/internal/runtime/catalog/test/catalog_test.go new file mode 100644 index 000000000000..88485d45959c --- /dev/null +++ b/internal/runtime/catalog/test/catalog_test.go @@ -0,0 +1,282 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package test contains catalog tests +// Note: They have to be outside the catalog package to be realistic. Otherwise using +// test types with different versions would result in a cyclic dependency and thus +// wouldn't be possible. +package test + +import ( + "testing" + + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + + "sigs.k8s.io/cluster-api/internal/runtime/catalog" + "sigs.k8s.io/cluster-api/internal/runtime/catalog/test/v1alpha1" + "sigs.k8s.io/cluster-api/internal/runtime/catalog/test/v1alpha2" +) + +var c = catalog.New() + +func init() { + _ = v1alpha1.AddToCatalog(c) + _ = v1alpha2.AddToCatalog(c) +} + +func TestCatalog(t *testing.T) { + g := NewWithT(t) + + verify := func(hook catalog.Hook, expectedGV schema.GroupVersion) { + // Test GroupVersionHook + hookGVH, err := c.GroupVersionHook(hook) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(hookGVH.GroupVersion()).To(Equal(expectedGV)) + g.Expect(hookGVH.Hook).To(Equal("FakeHook")) + + // Test Request + requestGVK, err := c.Request(hookGVH) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(requestGVK.GroupVersion()).To(Equal(expectedGV)) + g.Expect(requestGVK.Kind).To(Equal("FakeRequest")) + + // Test Response + responseGVK, err := c.Response(hookGVH) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(responseGVK.GroupVersion()).To(Equal(expectedGV)) + g.Expect(responseGVK.Kind).To(Equal("FakeResponse")) + + // Test NewRequest + request, err := c.NewRequest(hookGVH) + g.Expect(err).ToNot(HaveOccurred()) + + // Test NewResponse + response, err := c.NewResponse(hookGVH) + g.Expect(err).ToNot(HaveOccurred()) + + // Test ValidateRequest/ValidateResponse + g.Expect(c.ValidateRequest(hookGVH, request)).To(Succeed()) + g.Expect(c.ValidateResponse(hookGVH, response)).To(Succeed()) + } + + verify(v1alpha1.FakeHook, v1alpha1.GroupVersion) + verify(v1alpha2.FakeHook, v1alpha2.GroupVersion) +} + +func TestValidateRequest(t *testing.T) { + v1alpha1Hook, err := c.GroupVersionHook(v1alpha1.FakeHook) + if err != nil { + panic("failed to get GVH of hook") + } + v1alpha1HookRequest, err := c.NewRequest(v1alpha1Hook) + if err != nil { + panic("failed to create request for hook") + } + + v1alpha2Hook, err := c.GroupVersionHook(v1alpha2.FakeHook) + if err != nil { + panic("failed to get GVH of hook") + } + + tests := []struct { + name string + hook catalog.GroupVersionHook + request runtime.Object + wantError bool + }{ + { + name: "should succeed when hook and request match", + hook: v1alpha1Hook, + request: v1alpha1HookRequest, + wantError: false, + }, + { + name: "should error when hook and request do not match", + hook: v1alpha2Hook, + request: v1alpha1HookRequest, + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + err := c.ValidateRequest(tt.hook, tt.request) + if tt.wantError { + g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).NotTo(HaveOccurred()) + } + }) + } +} + +func TestValidateResponse(t *testing.T) { + v1alpha1Hook, err := c.GroupVersionHook(v1alpha1.FakeHook) + if err != nil { + panic("failed to get GVH of hook") + } + v1alpha1HookResponse, err := c.NewResponse(v1alpha1Hook) + if err != nil { + panic("failed to create request for hook") + } + + v1alpha2Hook, err := c.GroupVersionHook(v1alpha2.FakeHook) + if err != nil { + panic("failed to get GVH of hook") + } + + tests := []struct { + name string + hook catalog.GroupVersionHook + response runtime.Object + wantError bool + }{ + { + name: "should succeed when hook and response match", + hook: v1alpha1Hook, + response: v1alpha1HookResponse, + wantError: false, + }, + { + name: "should error when hook and response do not match", + hook: v1alpha2Hook, + response: v1alpha1HookResponse, + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + err := c.ValidateResponse(tt.hook, tt.response) + if tt.wantError { + g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).NotTo(HaveOccurred()) + } + }) + } +} + +type GoodRequest struct { + metav1.TypeMeta `json:",inline"` + + First string `json:"first"` +} + +func (in *GoodRequest) DeepCopyObject() runtime.Object { + panic("implement me!") +} + +// BadRequest does not implement runtime.Object interface (missing DeepCopyObject function). +type BadRequest struct { + metav1.TypeMeta `json:",inline"` + + First string `json:"first"` +} + +type GoodResponse struct { + metav1.TypeMeta `json:",inline"` + + First string `json:"first"` +} + +func (out *GoodResponse) DeepCopyObject() runtime.Object { + panic("implement me!") +} + +// BadResponse does not implement runtime.Object interface (missing DeepCopyObject function). +type BadResponse struct { + metav1.TypeMeta `json:",inline"` + + First string `json:"first"` +} + +func GoodHook(*GoodRequest, *GoodResponse) {} + +func HookWithReturn(*GoodRequest) *GoodResponse { return nil } + +func HookWithNoInputs() {} + +func HookWithThreeInputs(*GoodRequest, *GoodRequest, *GoodResponse) {} + +func HookWithBadRequestAndResponse(*BadRequest, *BadResponse) {} + +func TestAddHook(t *testing.T) { + c := catalog.New() + + tests := []struct { + name string + hook catalog.Hook + hookMeta *catalog.HookMeta + wantPanic bool + }{ + { + name: "should pass for valid hook", + hook: GoodHook, + hookMeta: &catalog.HookMeta{}, + wantPanic: false, + }, + { + name: "should fail for hook with a return value", + hook: HookWithReturn, + hookMeta: &catalog.HookMeta{}, + wantPanic: true, + }, + { + name: "should fail for a hook with no inputs", + hook: HookWithNoInputs, + hookMeta: &catalog.HookMeta{}, + wantPanic: true, + }, + { + name: "should fail for a hook with more than two arguments", + hook: HookWithThreeInputs, + hookMeta: &catalog.HookMeta{}, + wantPanic: true, + }, + { + name: "should fail for hook with bad request and response arguments", + hook: HookWithBadRequestAndResponse, + hookMeta: &catalog.HookMeta{}, + wantPanic: true, + }, + { + name: "should fail if the hookMeta is nil", + hook: GoodHook, + hookMeta: nil, + wantPanic: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + testFunc := func() { + c.AddHook(v1alpha1.GroupVersion, tt.hook, tt.hookMeta) + } + if tt.wantPanic { + g.Expect(testFunc).Should(Panic()) + } else { + g.Expect(testFunc).ShouldNot(Panic()) + } + }) + } +} diff --git a/internal/runtime/catalog/test/v1alpha1/fake_types.go b/internal/runtime/catalog/test/v1alpha1/fake_types.go new file mode 100644 index 000000000000..e794f0a569fa --- /dev/null +++ b/internal/runtime/catalog/test/v1alpha1/fake_types.go @@ -0,0 +1,75 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1alpha1 contains types for catalog tests +// Note: they have to be in a separate package because otherwise it wouldn't +// be possible to register different versions of the same hook. +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + + "sigs.k8s.io/cluster-api/internal/runtime/catalog" +) + +var ( + // GroupVersion is group version identifying rpc services defined in this package + // and their request and response types. + GroupVersion = schema.GroupVersion{Group: "test.runtime.cluster.x-k8s.io", Version: "v1alpha1"} + + // catalogBuilder is used to add rpc services and their request and response types + // to a Catalog. + catalogBuilder = catalog.Builder{GroupVersion: GroupVersion} + + // AddToCatalog adds rpc services defined in this package and their request and + // response types to a catalog. + AddToCatalog = catalogBuilder.AddToCatalog +) + +func FakeHook(*FakeRequest, *FakeResponse) {} + +type FakeRequest struct { + metav1.TypeMeta `json:",inline"` + + Second string + First int +} + +func (in *FakeRequest) DeepCopyObject() runtime.Object { + panic("implement me!") +} + +type FakeResponse struct { + metav1.TypeMeta `json:",inline"` + + Second string + First int +} + +func (in *FakeResponse) DeepCopyObject() runtime.Object { + panic("implement me!") +} + +func init() { + catalogBuilder.RegisterHook(FakeHook, &catalog.HookMeta{ + Tags: []string{"fake-tag"}, + Summary: "Fake summary", + Description: "Fake description", + Deprecated: true, + }) +} diff --git a/internal/runtime/catalog/test/v1alpha2/fake_types.go b/internal/runtime/catalog/test/v1alpha2/fake_types.go new file mode 100644 index 000000000000..2f9b27414b1c --- /dev/null +++ b/internal/runtime/catalog/test/v1alpha2/fake_types.go @@ -0,0 +1,75 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1alpha2 contains types for catalog tests +// Note: they have to be in a separate package because otherwise it wouldn't +// be possible to register different versions of the same hook. +package v1alpha2 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + + "sigs.k8s.io/cluster-api/internal/runtime/catalog" +) + +var ( + // GroupVersion is group version identifying rpc services defined in this package + // and their request and response types. + GroupVersion = schema.GroupVersion{Group: "test.runtime.cluster.x-k8s.io", Version: "v1alpha2"} + + // catalogBuilder is used to add rpc services and their request and response types + // to a Catalog. + catalogBuilder = catalog.Builder{GroupVersion: GroupVersion} + + // AddToCatalog adds rpc services defined in this package and their request and + // response types to a catalog. + AddToCatalog = catalogBuilder.AddToCatalog +) + +func FakeHook(*FakeRequest, *FakeResponse) {} + +type FakeRequest struct { + metav1.TypeMeta `json:",inline"` + + Second string + First int +} + +func (in *FakeRequest) DeepCopyObject() runtime.Object { + panic("implement me!") +} + +type FakeResponse struct { + metav1.TypeMeta `json:",inline"` + + Second string + First int +} + +func (in *FakeResponse) DeepCopyObject() runtime.Object { + panic("implement me!") +} + +func init() { + catalogBuilder.RegisterHook(FakeHook, &catalog.HookMeta{ + Tags: []string{"fake-tag"}, + Summary: "Fake summary", + Description: "Fake description", + Deprecated: true, + }) +} diff --git a/test/go.sum b/test/go.sum index cded5feb1bc2..d49729d5c4ea 100644 --- a/test/go.sum +++ b/test/go.sum @@ -346,6 +346,7 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.15.0+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=