diff --git a/Makefile b/Makefile index 5ed9a0dfe194..5e828f2efa68 100644 --- a/Makefile +++ b/Makefile @@ -271,7 +271,7 @@ generate-go-conversions: ## Run all generate-go-conversions-* targets generate-go-conversions-core: $(CONVERSION_GEN) ## Generate conversions go code for core $(MAKE) clean-generated-conversions SRC_DIRS="./api/v1alpha3,./$(EXP_DIR)/api/v1alpha3,./$(EXP_DIR)/addons/api/v1alpha3" $(MAKE) clean-generated-conversions SRC_DIRS="./api/v1alpha4,./$(EXP_DIR)/api/v1alpha4,./$(EXP_DIR)/addons/api/v1alpha4" - $(MAKE) clean-generated-conversions SRC_DIRS="./internal/runtime/catalog/test/v1alpha1,./internal/runtime/catalog/test/v1alpha2" + $(MAKE) clean-generated-conversions SRC_DIRS="./internal/runtime/test/v1alpha1,./internal/runtime/test/v1alpha2" $(CONVERSION_GEN) \ --input-dirs=./api/v1alpha3 \ --input-dirs=./api/v1alpha4 \ @@ -288,8 +288,8 @@ generate-go-conversions-core: $(CONVERSION_GEN) ## Generate conversions go code --output-file-base=zz_generated.conversion $(CONVERSION_GEN_OUTPUT_BASE) \ --go-header-file=./hack/boilerplate/boilerplate.generatego.txt $(CONVERSION_GEN) \ - --input-dirs=./internal/runtime/catalog/test/v1alpha1 \ - --input-dirs=./internal/runtime/catalog/test/v1alpha2 \ + --input-dirs=./internal/runtime/test/v1alpha1 \ + --input-dirs=./internal/runtime/test/v1alpha2 \ --build-tag=ignore_autogenerated_runtime \ --output-file-base=zz_generated.conversion $(CONVERSION_GEN_OUTPUT_BASE) \ --go-header-file=./hack/boilerplate/boilerplate.generatego.txt diff --git a/api/v1alpha3/zz_generated.deepcopy.go b/api/v1alpha3/zz_generated.deepcopy.go index e1f023426332..9bf6f38819b8 100644 --- a/api/v1alpha3/zz_generated.deepcopy.go +++ b/api/v1alpha3/zz_generated.deepcopy.go @@ -24,7 +24,7 @@ package v1alpha3 import ( "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" + runtime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" "sigs.k8s.io/cluster-api/errors" ) diff --git a/api/v1alpha4/zz_generated.deepcopy.go b/api/v1alpha4/zz_generated.deepcopy.go index 125f9356bac1..467e8b4092fb 100644 --- a/api/v1alpha4/zz_generated.deepcopy.go +++ b/api/v1alpha4/zz_generated.deepcopy.go @@ -24,7 +24,7 @@ package v1alpha4 import ( "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" + runtime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" "sigs.k8s.io/cluster-api/errors" ) diff --git a/exp/runtime/hooks/api/v1alpha1/util.go b/exp/runtime/hooks/api/v1alpha1/util.go index 46416c8bb8cf..f1a7bb82a4c3 100644 --- a/exp/runtime/hooks/api/v1alpha1/util.go +++ b/exp/runtime/hooks/api/v1alpha1/util.go @@ -97,7 +97,7 @@ func getField(obj interface{}, field string) (interface{}, error) { } f := v.FieldByName(field) if !f.IsValid() { - return "", fmt.Errorf("unexpected type. filed %s does not exist", field) + return "", fmt.Errorf("unexpected type. field %s does not exist", field) } return f.Interface(), nil } diff --git a/internal/runtime/catalog/catalog.go b/internal/runtime/catalog/catalog.go index ad02918ec89a..1c121d471eae 100644 --- a/internal/runtime/catalog/catalog.go +++ b/internal/runtime/catalog/catalog.go @@ -337,6 +337,11 @@ func (gvh GroupVersionHook) GroupVersion() schema.GroupVersion { return schema.GroupVersion{Group: gvh.Group, Version: gvh.Version} } +// GroupHook returns the GroupHook of a GroupVersionHook. +func (gvh GroupVersionHook) GroupHook() GroupHook { + return GroupHook{Group: gvh.Group, Hook: gvh.Hook} +} + func (gvh GroupVersionHook) String() string { return strings.Join([]string{gvh.Group, "/", gvh.Version, ", Hook=", gvh.Hook}, "") } @@ -353,6 +358,10 @@ type GroupHook struct { Hook string } +func (gh GroupHook) String() string { + return fmt.Sprintf("Group=%s, Hook=%s", gh.Group, gh.Hook) +} + // GVHToPath calculates the path for a given GroupVersionHook. // This func is aligned with Kubernetes paths for cluster-wide resources, e.g.: // /apis/storage.k8s.io/v1/storageclasses/standard. diff --git a/internal/runtime/catalog/test/catalog_test.go b/internal/runtime/catalog/test/catalog_test.go index 88485d45959c..41b1ec10692b 100644 --- a/internal/runtime/catalog/test/catalog_test.go +++ b/internal/runtime/catalog/test/catalog_test.go @@ -29,8 +29,8 @@ import ( "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" + "sigs.k8s.io/cluster-api/internal/runtime/test/v1alpha1" + "sigs.k8s.io/cluster-api/internal/runtime/test/v1alpha2" ) var c = catalog.New() diff --git a/internal/runtime/catalog/test/v1alpha1/fake_types.go b/internal/runtime/catalog/test/v1alpha1/fake_types.go index b9843d43bcfe..bd6325c6134d 100644 --- a/internal/runtime/catalog/test/v1alpha1/fake_types.go +++ b/internal/runtime/catalog/test/v1alpha1/fake_types.go @@ -66,6 +66,9 @@ func (in *FakeRequest) DeepCopyObject() runtime.Object { type FakeResponse struct { metav1.TypeMeta `json:",inline"` + Status string `json:"status"` + Message string `json:"message"` + Second string First int } diff --git a/internal/runtime/catalog/test/v1alpha2/fake_types.go b/internal/runtime/catalog/test/v1alpha2/fake_types.go index 581868c3331c..74a16fd05b2a 100644 --- a/internal/runtime/catalog/test/v1alpha2/fake_types.go +++ b/internal/runtime/catalog/test/v1alpha2/fake_types.go @@ -60,6 +60,9 @@ func (in *FakeRequest) DeepCopyObject() runtime.Object { type FakeResponse struct { metav1.TypeMeta `json:",inline"` + Status string `json:"status"` + Message string `json:"message"` + Second string First int } diff --git a/internal/runtime/client/client.go b/internal/runtime/client/client.go index 42151411a2a6..e8219cc4baa5 100644 --- a/internal/runtime/client/client.go +++ b/internal/runtime/client/client.go @@ -41,6 +41,8 @@ import ( "sigs.k8s.io/cluster-api/internal/runtime/registry" ) +type errCallingExtensionHandler error + const defaultDiscoveryTimeout = 10 * time.Second // Options are creation options for a Client. @@ -77,11 +79,11 @@ type Client interface { // Unregister unregisters the ExtensionConfig. Unregister(extensionConfig *runtimev1.ExtensionConfig) error - // CallAll calls all the extension registered for the hook. - CallAll(ctx context.Context, hook catalog.Hook, request runtime.Object, response runtimehooksv1.AggregatableResponse) error + // CallAllExtensions calls all the ExtensionHandler registered for the hook. + CallAllExtensions(ctx context.Context, hook catalog.Hook, request runtime.Object, response runtimehooksv1.AggregatableResponse) error // Call calls only the extension with the given name. - Call(ctx context.Context, name string, request, response runtime.Object) error + CallExtension(ctx context.Context, hook catalog.Hook, name string, request, response runtime.Object) error } var _ Client = &client{} @@ -160,22 +162,26 @@ func (c *client) Unregister(extensionConfig *runtimev1.ExtensionConfig) error { return nil } -func (c *client) CallAll(ctx context.Context, hook catalog.Hook, request runtime.Object, response runtimehooksv1.AggregatableResponse) error { +// CallAllExtensions calls all the ExtensionHandlers registered for the hook. +// The ExtensionHandler are called sequentially. The function exits immediately after any of the ExtensionHandlers return an error. +// See `Call` for more details on when an ExtensionHandler returns an error. +// The aggregate result of the ExtensionHandlers is updated into the response object passed to the function. +func (c *client) CallAllExtensions(ctx context.Context, hook catalog.Hook, request runtime.Object, response runtimehooksv1.AggregatableResponse) error { gvh, err := c.catalog.GroupVersionHook(hook) if err != nil { return errors.Wrap(err, "failed to compute GroupVersionHook") } - registrations, err := c.registry.List(catalog.GroupHook{Group: gvh.Group, Hook: gvh.Hook}) + registrations, err := c.registry.List(gvh.GroupHook()) if err != nil { - return errors.Wrap(err, "failed to retrieve ExtensionHandlers information") + return errors.Wrapf(err, "failed to retrieve ExtensionHandlers for %s", gvh.GroupHook()) } responses := []*runtimehooksv1.ExtensionHandlerResponse{} for _, registration := range registrations { tmpResponse, err := c.catalog.NewResponse(gvh) if err != nil { - return errors.Wrap(err, "failed to create respons object") + return errors.Wrap(err, "failed to create response object") } - err = c.Call(ctx, registration.Name, request, tmpResponse) + err = c.CallExtension(ctx, hook, registration.Name, request, tmpResponse) // If at least once of the extension handlers failed lets short-circuit here and return early. if err != nil { return errors.Wrapf(err, "ExtensionHandler %s failed", registration.Name) @@ -191,10 +197,29 @@ func (c *client) CallAll(ctx context.Context, hook catalog.Hook, request runtime return nil } -func (c *client) Call(ctx context.Context, name string, request, response runtime.Object) error { +// Call make the calls to the extension with the given name. +// The response object passed will be updated with the response of the call. +// An error is returned if the extension is not compatible with the hook. +// If the ExtensionHandler returns a response with `Status` set to `Failure` the function returns an error +// and the response object is updated with the response received from the extension handler. +// +// FailurePolicy of the ExtensionHandler is used to handle errors that occur when performing the external call to the extension. +// - If FailurePolicy is set to Ignore, the error is ignored and the response object is updated to be the default success response. +// - If FailurePolicy is set to Fail, an error is returned and the response object may or may not be updated. +// Nb. FailurePolicy does not affect the following kinds of errors: +// - Internal errors. Examples: hooks is incompatible with ExtensionHandler, ExtensionHandler information is missing. +// - Error when ExtensionHandler returns a response with `Status` set to `Failure`. +func (c *client) CallExtension(ctx context.Context, hook catalog.Hook, name string, request, response runtime.Object) error { + gvh, err := c.catalog.GroupVersionHook(hook) + if err != nil { + return errors.Wrap(err, "failed to compute GroupVersionHook") + } registration, err := c.registry.Get(name) if err != nil { - return errors.Wrapf(err, "failed to retrieve registration information for %s", name) + return errors.Wrapf(err, "failed to retrieve ExtensionHandler with name %q", name) + } + if gvh.GroupHook() != registration.GroupVersionHook.GroupHook() { + return fmt.Errorf("ExtensionHandler %q does not support %s", name, gvh.GroupHook()) } var timeoutDuration time.Duration if registration.TimeoutSeconds != nil { @@ -212,7 +237,7 @@ func (c *client) Call(ctx context.Context, name string, request, response runtim // If the error is errCallingExtensionHandler then apply failure policy to calculate // the effective result of the operation. ignore := *registration.FailurePolicy == runtimev1.FailurePolicyIgnore - if _, ok := err.(*errCallingExtensionHandler); ok && ignore { + if _, ok := err.(errCallingExtensionHandler); ok && ignore { // Update the response to a default success response and return. // - Set status to success // - Set message to empty string @@ -340,25 +365,22 @@ func httpCall(ctx context.Context, request, response runtime.Object, opts *httpC } resp, err := client.Do(httpRequest) if err != nil { - return &errCallingExtensionHandler{ - extensionHandlerName: opts.name, - err: errors.Wrap(err, "failed to make the http call"), - } + return errCallingExtensionHandler( + errors.Wrapf(err, "failed to call ExtensionHandler: %q", opts.name), + ) } if resp.StatusCode != http.StatusOK { - return &errCallingExtensionHandler{ - extensionHandlerName: opts.name, - err: fmt.Errorf("non 200 response status received"), - } + return errCallingExtensionHandler( + fmt.Errorf("non 200 response code, %q, not accepted", resp.StatusCode), + ) } defer resp.Body.Close() if err := json.NewDecoder(resp.Body).Decode(responseLocal); err != nil { - return &errCallingExtensionHandler{ - extensionHandlerName: opts.name, - err: errors.Wrap(err, "failed to decode response"), - } + return errCallingExtensionHandler( + errors.Wrap(err, "failed to decode message"), + ) } if requireConversion { @@ -405,13 +427,3 @@ func urlForExtension(config runtimev1.ClientConfig, gvh catalog.GroupVersionHook u.Path = path.Join(u.Path, catalog.GVHToPath(gvh, name)) return u, nil } - -type errCallingExtensionHandler struct { - extensionHandlerName string - err error -} - -func (e *errCallingExtensionHandler) Error() string { - // FIXME: find a better error message. - return fmt.Sprintf("failed processing handler %s with error: %s", e.extensionHandlerName, e.err) -} diff --git a/internal/runtime/client/client_test.go b/internal/runtime/client/client_test.go index 497f1b18cf78..4b2a4fa21a0d 100644 --- a/internal/runtime/client/client_test.go +++ b/internal/runtime/client/client_test.go @@ -19,6 +19,8 @@ package client import ( "context" "encoding/json" + "fmt" + "net" "net/http" "net/http/httptest" "testing" @@ -29,9 +31,11 @@ import ( "k8s.io/utils/pointer" runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1" + runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1" "sigs.k8s.io/cluster-api/internal/runtime/catalog" - fakev1alpha1 "sigs.k8s.io/cluster-api/internal/runtime/catalog/test/v1alpha1" - fakev1alpha2 "sigs.k8s.io/cluster-api/internal/runtime/catalog/test/v1alpha2" + "sigs.k8s.io/cluster-api/internal/runtime/registry" + fakev1alpha1 "sigs.k8s.io/cluster-api/internal/runtime/test/v1alpha1" + fakev1alpha2 "sigs.k8s.io/cluster-api/internal/runtime/test/v1alpha2" ) func TestClient_httpCall(t *testing.T) { @@ -269,3 +273,279 @@ func TestURLForExtension(t *testing.T) { }) } } + +func TestClient_CallExtension(t *testing.T) { + g := NewWithT(t) + + testHostPort := "127.0.0.1:9090" + + fpFail := runtimev1.FailurePolicyFail + fpIgnore := runtimev1.FailurePolicyIgnore + + validExtensionHandlerWithFailPolicy := runtimev1.ExtensionConfig{ + Spec: runtimev1.ExtensionConfigSpec{ + ClientConfig: runtimev1.ClientConfig{ + URL: pointer.String(fmt.Sprintf("http://%s/", testHostPort)), + }, + }, + Status: runtimev1.ExtensionConfigStatus{ + Handlers: []runtimev1.ExtensionHandler{ + { + Name: "valid-extension", + RequestHook: runtimev1.GroupVersionHook{ + APIVersion: fakev1alpha1.GroupVersion.String(), + Hook: "FakeHook", + }, + TimeoutSeconds: pointer.Int32Ptr(1), + FailurePolicy: &fpFail, + }, + }, + }, + } + registryWithExtensionHandlerWithFailPolicy := registry.New() + err := registryWithExtensionHandlerWithFailPolicy.WarmUp(&runtimev1.ExtensionConfigList{ + Items: []runtimev1.ExtensionConfig{ + validExtensionHandlerWithFailPolicy, + }, + }) + g.Expect(err).NotTo(HaveOccurred()) + + validExtensionHandlerWithIgnorePolicy := runtimev1.ExtensionConfig{ + Spec: runtimev1.ExtensionConfigSpec{ + ClientConfig: runtimev1.ClientConfig{ + URL: pointer.String(fmt.Sprintf("http://%s/", testHostPort)), + }, + }, + Status: runtimev1.ExtensionConfigStatus{ + Handlers: []runtimev1.ExtensionHandler{ + { + Name: "valid-extension", + RequestHook: runtimev1.GroupVersionHook{ + APIVersion: fakev1alpha1.GroupVersion.String(), + Hook: "FakeHook", + }, + TimeoutSeconds: pointer.Int32Ptr(1), + FailurePolicy: &fpIgnore, + }, + }, + }, + } + + registryWithExtensionHandlerWithIgnorePolicy := registry.New() + err = registryWithExtensionHandlerWithIgnorePolicy.WarmUp(&runtimev1.ExtensionConfigList{ + Items: []runtimev1.ExtensionConfig{ + validExtensionHandlerWithIgnorePolicy, + }, + }) + g.Expect(err).NotTo(HaveOccurred()) + + successResponse := &fakev1alpha1.FakeResponse{ + TypeMeta: metav1.TypeMeta{ + Kind: "FakeResponse", + APIVersion: fakev1alpha1.GroupVersion.String(), + }, + Status: runtimehooksv1.ResponseStatusSuccess, + } + + failureResponse := &fakev1alpha1.FakeResponse{ + TypeMeta: metav1.TypeMeta{ + Kind: "FakeResponse", + APIVersion: fakev1alpha1.GroupVersion.String(), + }, + Status: runtimehooksv1.ResponseStatusFailure, + } + + type args struct { + hook catalog.Hook + name string + request runtime.Object + response runtime.Object + } + tests := []struct { + name string + startServer bool + registry registry.ExtensionRegistry + mockResponse runtime.Object + mockResponseStatusCode int + args args + wantErr bool + }{ + { + name: "should fail if ExtensionHandler information is not registered", + registry: registry.New(), + mockResponse: &fakev1alpha1.FakeResponse{}, + mockResponseStatusCode: http.StatusOK, + args: args{ + hook: fakev1alpha1.FakeHook, + name: "unregistered-extension", + request: &fakev1alpha1.FakeRequest{}, + response: &fakev1alpha1.FakeResponse{}, + }, + wantErr: true, + }, + { + name: "should when extension handler is not compatible with the hook", + registry: registryWithExtensionHandlerWithFailPolicy, + mockResponse: successResponse, + mockResponseStatusCode: http.StatusOK, + args: args{ + hook: fakev1alpha1.SecondFakeHook, + name: "valid-extension", + request: &fakev1alpha1.FakeRequest{ + TypeMeta: metav1.TypeMeta{ + Kind: "FakeRequest", + APIVersion: fakev1alpha1.GroupVersion.String(), + }, + }, + response: &fakev1alpha1.FakeResponse{ + TypeMeta: metav1.TypeMeta{ + Kind: "FakeResponse", + APIVersion: fakev1alpha1.GroupVersion.String(), + }, + }, + }, + startServer: false, + wantErr: true, + }, + { + name: "should succeed when calling ExtensionHandler with success response", + registry: registryWithExtensionHandlerWithFailPolicy, + mockResponse: successResponse, + mockResponseStatusCode: http.StatusOK, + args: args{ + hook: fakev1alpha1.FakeHook, + name: "valid-extension", + request: &fakev1alpha1.FakeRequest{ + TypeMeta: metav1.TypeMeta{ + Kind: "FakeRequest", + APIVersion: fakev1alpha1.GroupVersion.String(), + }, + }, + response: &fakev1alpha1.FakeResponse{ + TypeMeta: metav1.TypeMeta{ + Kind: "FakeResponse", + APIVersion: fakev1alpha1.GroupVersion.String(), + }, + }, + }, + startServer: true, + wantErr: false, + }, + { + name: "should fail when calling ExtensionHandler with failure response", + registry: registryWithExtensionHandlerWithFailPolicy, + mockResponse: failureResponse, + mockResponseStatusCode: http.StatusOK, + args: args{ + hook: fakev1alpha1.FakeHook, + name: "valid-extension", + request: &fakev1alpha1.FakeRequest{ + TypeMeta: metav1.TypeMeta{ + Kind: "FakeRequest", + APIVersion: fakev1alpha1.GroupVersion.String(), + }, + }, + response: &fakev1alpha1.FakeResponse{ + TypeMeta: metav1.TypeMeta{ + Kind: "FakeResponse", + APIVersion: fakev1alpha1.GroupVersion.String(), + }, + }, + }, + startServer: true, + wantErr: true, + }, + { + name: "should succeed with unreachable extension and Ignore failure policy", + registry: registryWithExtensionHandlerWithIgnorePolicy, + mockResponse: failureResponse, + mockResponseStatusCode: http.StatusOK, + args: args{ + hook: fakev1alpha1.FakeHook, + name: "valid-extension", + request: &fakev1alpha1.FakeRequest{ + TypeMeta: metav1.TypeMeta{ + Kind: "FakeRequest", + APIVersion: fakev1alpha1.GroupVersion.String(), + }, + }, + response: &fakev1alpha1.FakeResponse{ + TypeMeta: metav1.TypeMeta{ + Kind: "FakeResponse", + APIVersion: fakev1alpha1.GroupVersion.String(), + }, + }, + }, + startServer: false, + wantErr: false, + }, + { + name: "should fail with unreachable extension and Fail failure policy", + registry: registryWithExtensionHandlerWithFailPolicy, + mockResponse: failureResponse, + mockResponseStatusCode: http.StatusOK, + args: args{ + hook: fakev1alpha1.FakeHook, + name: "valid-extension", + request: &fakev1alpha1.FakeRequest{ + TypeMeta: metav1.TypeMeta{ + Kind: "FakeRequest", + APIVersion: fakev1alpha1.GroupVersion.String(), + }, + }, + response: &fakev1alpha1.FakeResponse{ + TypeMeta: metav1.TypeMeta{ + Kind: "FakeResponse", + APIVersion: fakev1alpha1.GroupVersion.String(), + }, + }, + }, + startServer: false, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + if tt.startServer { + l, err := net.Listen("tcp", testHostPort) + g.Expect(err).NotTo(HaveOccurred()) + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + respBody, err := json.Marshal(tt.mockResponse) + if err != nil { + panic(err) + } + w.WriteHeader(tt.mockResponseStatusCode) + _, _ = w.Write(respBody) + }) + srv := httptest.NewUnstartedServer(mux) + // NewUnstartedServer creates a listener. Close that listener and replace + // with the one we created. + g.Expect(srv.Listener.Close()).To(Succeed()) + srv.Listener = l + srv.Start() + defer srv.Close() + } + + ctlg := catalog.New() + _ = fakev1alpha1.AddToCatalog(ctlg) + _ = fakev1alpha2.AddToCatalog(ctlg) + + c := New(Options{ + Catalog: ctlg, + Registry: tt.registry, + }) + + ctx := context.Background() + err := c.CallExtension(ctx, tt.args.hook, tt.args.name, tt.args.request, tt.args.response) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).NotTo(HaveOccurred()) + } + }) + } +} diff --git a/internal/runtime/test/v1alpha1/conversion.go b/internal/runtime/test/v1alpha1/conversion.go new file mode 100644 index 000000000000..2e566c9c4b1b --- /dev/null +++ b/internal/runtime/test/v1alpha1/conversion.go @@ -0,0 +1,37 @@ +/* +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 + +import ( + conversion "k8s.io/apimachinery/pkg/conversion" + + clusterv1alpha4 "sigs.k8s.io/cluster-api/api/v1alpha4" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + v1alpha2 "sigs.k8s.io/cluster-api/internal/runtime/test/v1alpha2" +) + +func Convert_v1alpha1_FakeResponse_To_v1alpha2_FakeResponse(in *FakeResponse, out *v1alpha2.FakeResponse, s conversion.Scope) error { + return autoConvert_v1alpha1_FakeResponse_To_v1alpha2_FakeResponse(in, out, s) +} + +func Convert_v1alpha4_Cluster_To_v1beta1_Cluster(in *clusterv1alpha4.Cluster, out *clusterv1.Cluster, s conversion.Scope) error { + return clusterv1alpha4.Convert_v1alpha4_Cluster_To_v1beta1_Cluster(in, out, s) +} + +func Convert_v1beta1_Cluster_To_v1alpha4_Cluster(in *clusterv1.Cluster, out *clusterv1alpha4.Cluster, s conversion.Scope) error { + return clusterv1alpha4.Convert_v1beta1_Cluster_To_v1alpha4_Cluster(in, out, s) +} diff --git a/internal/runtime/test/v1alpha1/conversion_test.go b/internal/runtime/test/v1alpha1/conversion_test.go new file mode 100644 index 000000000000..3a3da04d9b01 --- /dev/null +++ b/internal/runtime/test/v1alpha1/conversion_test.go @@ -0,0 +1,59 @@ +/* +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 + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/cluster-api/internal/runtime/catalog" + "sigs.k8s.io/cluster-api/internal/runtime/test/v1alpha2" +) + +func TestConversion(t *testing.T) { + g := NewWithT(t) + + var c = catalog.New() + _ = AddToCatalog(c) + _ = v1alpha2.AddToCatalog(c) + + t.Run("down-convert FakeRequest v1alpha2 to v1alpha1", func(t *testing.T) { + request := &v1alpha2.FakeRequest{Cluster: clusterv1.Cluster{ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }}} + requestLocal := &FakeRequest{} + + g.Expect(c.Convert(request, requestLocal, context.Background())).To(Succeed()) + g.Expect(requestLocal.Cluster.GetName()).To(Equal(request.Cluster.Name)) + }) + + t.Run("up-convert FakeResponse v1alpha1 to v1alpha2", func(t *testing.T) { + responseLocal := &FakeResponse{ + First: 1, + Second: "foo", + } + response := &v1alpha2.FakeResponse{} + g.Expect(c.Convert(responseLocal, response, context.Background())).To(Succeed()) + + g.Expect(response.First).To(Equal(responseLocal.First)) + g.Expect(response.Second).To(Equal(responseLocal.Second)) + }) +} diff --git a/internal/runtime/test/v1alpha1/doc.go b/internal/runtime/test/v1alpha1/doc.go new file mode 100644 index 000000000000..b1809d0875cf --- /dev/null +++ b/internal/runtime/test/v1alpha1/doc.go @@ -0,0 +1,21 @@ +/* +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. +// +k8s:conversion-gen=sigs.k8s.io/cluster-api/internal/runtime/test/v1alpha2 +package v1alpha1 diff --git a/internal/runtime/test/v1alpha1/fake_types.go b/internal/runtime/test/v1alpha1/fake_types.go new file mode 100644 index 000000000000..190196ea142a --- /dev/null +++ b/internal/runtime/test/v1alpha1/fake_types.go @@ -0,0 +1,124 @@ +/* +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" + + clusterv1alpha4 "sigs.k8s.io/cluster-api/api/v1alpha4" + runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1" + "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 + + // localSchemeBuilder provide access to the SchemeBuilder used for managing rpc + // method's request and response types defined in this package. + // NOTE: this object is required to allow registration of automatically generated + // conversions func. + localSchemeBuilder = catalogBuilder +) + +func FakeHook(*FakeRequest, *FakeResponse) {} + +type FakeRequest struct { + metav1.TypeMeta `json:",inline"` + + Cluster clusterv1alpha4.Cluster + + Second string + First int +} + +func (in *FakeRequest) DeepCopyObject() runtime.Object { + panic("implement me!") +} + +type FakeResponse struct { + metav1.TypeMeta `json:",inline"` + + Status runtimehooksv1.ResponseStatus `json:"status"` + Message string `json:"message"` + + Second string + First int +} + +func (in *FakeResponse) DeepCopyObject() runtime.Object { + panic("implement me!") +} + +func SecondFakeHook(*SecondFakeRequest, *SecondFakeResponse) {} + +type SecondFakeRequest struct { + metav1.TypeMeta `json:",inline"` + + Cluster clusterv1alpha4.Cluster + + Second string + First int +} + +func (in *SecondFakeRequest) DeepCopyObject() runtime.Object { + panic("implement me!") +} + +type SecondFakeResponse struct { + metav1.TypeMeta `json:",inline"` + + Status runtimehooksv1.ResponseStatus `json:"status"` + Message string `json:"message"` + + Second string + First int +} + +func (in *SecondFakeResponse) 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, + }) + + catalogBuilder.RegisterHook(SecondFakeHook, &catalog.HookMeta{ + Tags: []string{"fake-tag"}, + Summary: "Second Fake summary", + Description: "Second Fake description", + Deprecated: true, + }) +} diff --git a/internal/runtime/test/v1alpha1/zz_generated.conversion.go b/internal/runtime/test/v1alpha1/zz_generated.conversion.go new file mode 100644 index 000000000000..e2207e27ea4e --- /dev/null +++ b/internal/runtime/test/v1alpha1/zz_generated.conversion.go @@ -0,0 +1,120 @@ +//go:build !ignore_autogenerated_runtime +// +build !ignore_autogenerated_runtime + +/* +Copyright 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. +*/ + +// Code generated by conversion-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + conversion "k8s.io/apimachinery/pkg/conversion" + runtime "k8s.io/apimachinery/pkg/runtime" + v1alpha4 "sigs.k8s.io/cluster-api/api/v1alpha4" + v1beta1 "sigs.k8s.io/cluster-api/api/v1beta1" + apiv1alpha1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1" + v1alpha2 "sigs.k8s.io/cluster-api/internal/runtime/test/v1alpha2" +) + +func init() { + localSchemeBuilder.Register(RegisterConversions) +} + +// RegisterConversions adds conversion functions to the given scheme. +// Public to allow building arbitrary schemes. +func RegisterConversions(s *runtime.Scheme) error { + if err := s.AddGeneratedConversionFunc((*FakeRequest)(nil), (*v1alpha2.FakeRequest)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_FakeRequest_To_v1alpha2_FakeRequest(a.(*FakeRequest), b.(*v1alpha2.FakeRequest), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*v1alpha2.FakeRequest)(nil), (*FakeRequest)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha2_FakeRequest_To_v1alpha1_FakeRequest(a.(*v1alpha2.FakeRequest), b.(*FakeRequest), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*v1alpha2.FakeResponse)(nil), (*FakeResponse)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha2_FakeResponse_To_v1alpha1_FakeResponse(a.(*v1alpha2.FakeResponse), b.(*FakeResponse), scope) + }); err != nil { + return err + } + if err := s.AddConversionFunc((*FakeResponse)(nil), (*v1alpha2.FakeResponse)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_FakeResponse_To_v1alpha2_FakeResponse(a.(*FakeResponse), b.(*v1alpha2.FakeResponse), scope) + }); err != nil { + return err + } + if err := s.AddConversionFunc((*v1alpha4.Cluster)(nil), (*v1beta1.Cluster)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha4_Cluster_To_v1beta1_Cluster(a.(*v1alpha4.Cluster), b.(*v1beta1.Cluster), scope) + }); err != nil { + return err + } + if err := s.AddConversionFunc((*v1beta1.Cluster)(nil), (*v1alpha4.Cluster)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_Cluster_To_v1alpha4_Cluster(a.(*v1beta1.Cluster), b.(*v1alpha4.Cluster), scope) + }); err != nil { + return err + } + return nil +} + +func autoConvert_v1alpha1_FakeRequest_To_v1alpha2_FakeRequest(in *FakeRequest, out *v1alpha2.FakeRequest, s conversion.Scope) error { + if err := Convert_v1alpha4_Cluster_To_v1beta1_Cluster(&in.Cluster, &out.Cluster, s); err != nil { + return err + } + out.Second = in.Second + out.First = in.First + return nil +} + +// Convert_v1alpha1_FakeRequest_To_v1alpha2_FakeRequest is an autogenerated conversion function. +func Convert_v1alpha1_FakeRequest_To_v1alpha2_FakeRequest(in *FakeRequest, out *v1alpha2.FakeRequest, s conversion.Scope) error { + return autoConvert_v1alpha1_FakeRequest_To_v1alpha2_FakeRequest(in, out, s) +} + +func autoConvert_v1alpha2_FakeRequest_To_v1alpha1_FakeRequest(in *v1alpha2.FakeRequest, out *FakeRequest, s conversion.Scope) error { + if err := Convert_v1beta1_Cluster_To_v1alpha4_Cluster(&in.Cluster, &out.Cluster, s); err != nil { + return err + } + out.Second = in.Second + out.First = in.First + return nil +} + +// Convert_v1alpha2_FakeRequest_To_v1alpha1_FakeRequest is an autogenerated conversion function. +func Convert_v1alpha2_FakeRequest_To_v1alpha1_FakeRequest(in *v1alpha2.FakeRequest, out *FakeRequest, s conversion.Scope) error { + return autoConvert_v1alpha2_FakeRequest_To_v1alpha1_FakeRequest(in, out, s) +} + +func autoConvert_v1alpha1_FakeResponse_To_v1alpha2_FakeResponse(in *FakeResponse, out *v1alpha2.FakeResponse, s conversion.Scope) error { + out.Status = apiv1alpha1.ResponseStatus(in.Status) + out.Message = in.Message + out.Second = in.Second + out.First = in.First + return nil +} + +func autoConvert_v1alpha2_FakeResponse_To_v1alpha1_FakeResponse(in *v1alpha2.FakeResponse, out *FakeResponse, s conversion.Scope) error { + out.Status = apiv1alpha1.ResponseStatus(in.Status) + out.Message = in.Message + out.Second = in.Second + out.First = in.First + return nil +} + +// Convert_v1alpha2_FakeResponse_To_v1alpha1_FakeResponse is an autogenerated conversion function. +func Convert_v1alpha2_FakeResponse_To_v1alpha1_FakeResponse(in *v1alpha2.FakeResponse, out *FakeResponse, s conversion.Scope) error { + return autoConvert_v1alpha2_FakeResponse_To_v1alpha1_FakeResponse(in, out, s) +} diff --git a/internal/runtime/test/v1alpha2/fake_types.go b/internal/runtime/test/v1alpha2/fake_types.go new file mode 100644 index 000000000000..98ea5e00a789 --- /dev/null +++ b/internal/runtime/test/v1alpha2/fake_types.go @@ -0,0 +1,83 @@ +/* +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" + + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1" + "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"` + + Cluster clusterv1.Cluster + + Second string + First int +} + +func (in *FakeRequest) DeepCopyObject() runtime.Object { + panic("implement me!") +} + +type FakeResponse struct { + metav1.TypeMeta `json:",inline"` + + Status runtimehooksv1.ResponseStatus `json:"status"` + Message string `json:"message"` + + 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, + }) + +}