From cff9b2d6ae926532ab76941eb15f29730aebc274 Mon Sep 17 00:00:00 2001 From: Yuvaraj Kakaraparthi Date: Wed, 27 Apr 2022 19:30:44 -0700 Subject: [PATCH] Runtime SDK client and Discovery Co-authored-by: chrischdi Co-authored-by: sbueringer --- .golangci.yml | 8 +- Makefile | 8 + .../hooks/api/v1alpha1/common_types.go | 29 ++ .../hooks/api/v1alpha1/discovery_types.go | 92 ++++++ exp/runtime/hooks/api/v1alpha1/doc.go | 20 ++ .../hooks/api/v1alpha1/groupversion_info.go | 37 +++ .../api/v1alpha1/zz_generated.deepcopy.go | 122 +++++++ internal/runtime/catalog/catalog.go | 25 +- .../catalog/test/v1alpha1/conversion.go | 37 +++ .../catalog/test/v1alpha1/conversion_test.go | 59 ++++ internal/runtime/catalog/test/v1alpha1/doc.go | 21 ++ .../catalog/test/v1alpha1/fake_types.go | 11 +- .../test/v1alpha1/zz_generated.conversion.go | 115 +++++++ .../catalog/test/v1alpha2/fake_types.go | 5 +- internal/runtime/client/client.go | 304 ++++++++++++++++++ internal/runtime/client/client_test.go | 271 ++++++++++++++++ 16 files changed, 1157 insertions(+), 7 deletions(-) create mode 100644 exp/runtime/hooks/api/v1alpha1/common_types.go create mode 100644 exp/runtime/hooks/api/v1alpha1/discovery_types.go create mode 100644 exp/runtime/hooks/api/v1alpha1/doc.go create mode 100644 exp/runtime/hooks/api/v1alpha1/groupversion_info.go create mode 100644 exp/runtime/hooks/api/v1alpha1/zz_generated.deepcopy.go create mode 100644 internal/runtime/catalog/test/v1alpha1/conversion.go create mode 100644 internal/runtime/catalog/test/v1alpha1/conversion_test.go create mode 100644 internal/runtime/catalog/test/v1alpha1/doc.go create mode 100644 internal/runtime/catalog/test/v1alpha1/zz_generated.conversion.go create mode 100644 internal/runtime/client/client.go create mode 100644 internal/runtime/client/client_test.go diff --git a/.golangci.yml b/.golangci.yml index fa41d79de378..0c7c3c5bffe0 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -111,6 +111,8 @@ linters-settings: # CAPI exp runtime - pkg: sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1 alias: runtimev1 + - pkg: sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1 + alias: runtimehooksv1 # CAPD - pkg: sigs.k8s.io/cluster-api/test/infrastructure/docker/api/v1alpha3 alias: infrav1alpha3 @@ -217,11 +219,11 @@ issues: - linters: - revive text: exported (method|function|type|const) (.+) should have comment or be unexported - path: .*(api|types)\/.*\/conversion.*\.go$ + path: .*(api|types|test)\/.*\/conversion.*\.go$ - linters: - revive text: "var-naming: don't use underscores in Go names;" - path: .*(api|types)\/.*\/conversion.*\.go$ + path: .*(api|types|test)\/.*\/conversion.*\.go$ - linters: - revive text: "receiver-naming: receiver name" @@ -229,7 +231,7 @@ issues: - linters: - stylecheck text: "ST1003: should not use underscores in Go names;" - path: .*(api|types)\/.*\/conversion.*\.go$ + path: .*(api|types|test)\/.*\/conversion.*\.go$ - linters: - stylecheck text: "ST1016: methods on the same type should have the same receiver name" diff --git a/Makefile b/Makefile index 264a7fae72a5..5ed9a0dfe194 100644 --- a/Makefile +++ b/Makefile @@ -246,6 +246,7 @@ generate-go-deepcopy-core: $(CONTROLLER_GEN) ## Generate deepcopy go code for co paths=./$(EXP_DIR)/api/... \ paths=./$(EXP_DIR)/addons/api/... \ paths=./$(EXP_DIR)/runtime/api/... \ + paths=./$(EXP_DIR)/runtime/hooks/api/... \ paths=./cmd/clusterctl/... \ paths=./internal/test/builder/... @@ -270,6 +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" $(CONVERSION_GEN) \ --input-dirs=./api/v1alpha3 \ --input-dirs=./api/v1alpha4 \ @@ -285,6 +287,12 @@ generate-go-conversions-core: $(CONVERSION_GEN) ## Generate conversions go code --extra-peer-dirs=sigs.k8s.io/cluster-api/api/v1alpha4 \ --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 \ + --build-tag=ignore_autogenerated_runtime \ + --output-file-base=zz_generated.conversion $(CONVERSION_GEN_OUTPUT_BASE) \ + --go-header-file=./hack/boilerplate/boilerplate.generatego.txt .PHONY: generate-go-conversions-kubeadm-bootstrap generate-go-conversions-kubeadm-bootstrap: $(CONVERSION_GEN) ## Generate conversions go code for kubeadm bootstrap diff --git a/exp/runtime/hooks/api/v1alpha1/common_types.go b/exp/runtime/hooks/api/v1alpha1/common_types.go new file mode 100644 index 000000000000..94638a78fb2d --- /dev/null +++ b/exp/runtime/hooks/api/v1alpha1/common_types.go @@ -0,0 +1,29 @@ +/* +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 + +// ResponseStatus represents the status of the hook response. +// +enum +type ResponseStatus string + +const ( + // ResponseStatusSuccess represents the success response. + ResponseStatusSuccess ResponseStatus = "Success" + + // ResponseStatusFailure represents a failure response. + ResponseStatusFailure ResponseStatus = "Failure" +) diff --git a/exp/runtime/hooks/api/v1alpha1/discovery_types.go b/exp/runtime/hooks/api/v1alpha1/discovery_types.go new file mode 100644 index 000000000000..43f8a5641ff8 --- /dev/null +++ b/exp/runtime/hooks/api/v1alpha1/discovery_types.go @@ -0,0 +1,92 @@ +/* +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 ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "sigs.k8s.io/cluster-api/internal/runtime/catalog" +) + +// DiscoveryRequest represents the object of a discovery request. +// +kubebuilder:object:root=true +type DiscoveryRequest struct { + metav1.TypeMeta `json:",inline"` +} + +// DiscoveryResponse represents the object received as a discovery response. +// +kubebuilder:object:root=true +type DiscoveryResponse struct { + metav1.TypeMeta `json:",inline"` + + // Status of the call. One of "Success" or "Failure". + Status ResponseStatus `json:"status"` + + // A human-readable description of the status of the call. + Message string `json:"message"` + + // Handlers defines the current ExtensionHandlers supported by an Extension. + Handlers []ExtensionHandler `json:"handlers"` +} + +// ExtensionHandler represents the discovery information of the extension which includes +// the hook it supports. +type ExtensionHandler struct { + // Name is the name of the ExtensionHandler. + Name string `json:"name"` + + // RequestHook defines the versioned runtime hook which this ExtensionHandler serves. + RequestHook GroupVersionHook `json:"requestHook"` + + // TimeoutSeconds defines the timeout duration for client calls to the ExtensionHandler. + TimeoutSeconds *int32 `json:"timeoutSeconds,omitempty"` + + // FailurePolicy defines how failures in calls to the ExtensionHandler should be handled by a client. + FailurePolicy *FailurePolicy `json:"failurePolicy,omitempty"` +} + +// GroupVersionHook defines the runtime hook when the ExtensionHandler is called. +type GroupVersionHook struct { + // APIVersion is the Version of the Hook + APIVersion string `json:"apiVersion"` + + // Hook is the name of the hook + Hook string `json:"hook"` +} + +// FailurePolicy is the type of a extension failure policy. +type FailurePolicy string + +const ( + // FailurePolicyIgnore means that an error calling the extension is ignored. + FailurePolicyIgnore FailurePolicy = "Ignore" + + // FailurePolicyFail means that an error calling the extension is not ignored. + FailurePolicyFail FailurePolicy = "Fail" +) + +// Discovery represents the discovery hook. +func Discovery(*DiscoveryRequest, *DiscoveryResponse) {} + +func init() { + catalogBuilder.RegisterHook(Discovery, &catalog.HookMeta{ + Tags: []string{"Discovery"}, + Summary: "Discovery endpoint", + Description: "Discovery endpoint discovers the supported hooks of a RuntimeExtension", + Singleton: true, + }) +} diff --git a/exp/runtime/hooks/api/v1alpha1/doc.go b/exp/runtime/hooks/api/v1alpha1/doc.go new file mode 100644 index 000000000000..c3fbb8fa2343 --- /dev/null +++ b/exp/runtime/hooks/api/v1alpha1/doc.go @@ -0,0 +1,20 @@ +/* +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 the v1alpha1 RuntimeHooks. +// +kubebuilder:object:generate=true +// +k8s:openapi-gen=true +package v1alpha1 diff --git a/exp/runtime/hooks/api/v1alpha1/groupversion_info.go b/exp/runtime/hooks/api/v1alpha1/groupversion_info.go new file mode 100644 index 000000000000..47c84d56717d --- /dev/null +++ b/exp/runtime/hooks/api/v1alpha1/groupversion_info.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 ( + "k8s.io/apimachinery/pkg/runtime/schema" + + "sigs.k8s.io/cluster-api/internal/runtime/catalog" +) + +var ( + // GroupVersion is group version identifying RuntimeHooks defined in this package + // and their request and response types. + GroupVersion = schema.GroupVersion{Group: "hooks.runtime.cluster.x-k8s.io", Version: "v1alpha1"} + + // catalogBuilder is used to add RuntimeHooks and their request and response types + // to a Catalog. + catalogBuilder = &catalog.Builder{GroupVersion: GroupVersion} + + // AddToCatalog adds RuntimeHooks defined in this package and their request and + // response types to a catalog. + AddToCatalog = catalogBuilder.AddToCatalog +) diff --git a/exp/runtime/hooks/api/v1alpha1/zz_generated.deepcopy.go b/exp/runtime/hooks/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 000000000000..8d820b3694a3 --- /dev/null +++ b/exp/runtime/hooks/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,122 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +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 controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DiscoveryRequest) DeepCopyInto(out *DiscoveryRequest) { + *out = *in + out.TypeMeta = in.TypeMeta +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DiscoveryRequest. +func (in *DiscoveryRequest) DeepCopy() *DiscoveryRequest { + if in == nil { + return nil + } + out := new(DiscoveryRequest) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DiscoveryRequest) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DiscoveryResponse) DeepCopyInto(out *DiscoveryResponse) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.Handlers != nil { + in, out := &in.Handlers, &out.Handlers + *out = make([]ExtensionHandler, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DiscoveryResponse. +func (in *DiscoveryResponse) DeepCopy() *DiscoveryResponse { + if in == nil { + return nil + } + out := new(DiscoveryResponse) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DiscoveryResponse) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExtensionHandler) DeepCopyInto(out *ExtensionHandler) { + *out = *in + out.RequestHook = in.RequestHook + if in.TimeoutSeconds != nil { + in, out := &in.TimeoutSeconds, &out.TimeoutSeconds + *out = new(int32) + **out = **in + } + if in.FailurePolicy != nil { + in, out := &in.FailurePolicy, &out.FailurePolicy + *out = new(FailurePolicy) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionHandler. +func (in *ExtensionHandler) DeepCopy() *ExtensionHandler { + if in == nil { + return nil + } + out := new(ExtensionHandler) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GroupVersionHook) DeepCopyInto(out *GroupVersionHook) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupVersionHook. +func (in *GroupVersionHook) DeepCopy() *GroupVersionHook { + if in == nil { + return nil + } + out := new(GroupVersionHook) + in.DeepCopyInto(out) + return out +} diff --git a/internal/runtime/catalog/catalog.go b/internal/runtime/catalog/catalog.go index 45df37397b86..ad02918ec89a 100644 --- a/internal/runtime/catalog/catalog.go +++ b/internal/runtime/catalog/catalog.go @@ -256,7 +256,12 @@ func (c *Catalog) NewRequest(hook GroupVersionHook) (runtime.Object, error) { if !ok { return nil, errors.Errorf("hook %T is not registered in catalog %q", hook, c.catalogName) } - return c.scheme.New(descriptor.request) + obj, err := c.scheme.New(descriptor.request) + if err != nil { + return nil, errors.Wrap(err, "failed to create request object") + } + obj.GetObjectKind().SetGroupVersionKind(descriptor.request) + return obj, nil } // NewResponse returns a response object for a GroupVersionHook. @@ -265,7 +270,12 @@ func (c *Catalog) NewResponse(hook GroupVersionHook) (runtime.Object, error) { if !ok { return nil, errors.Errorf("hook %T is not registered in catalog %q", hook, c.catalogName) } - return c.scheme.New(descriptor.response) + obj, err := c.scheme.New(descriptor.response) + if err != nil { + return nil, errors.Wrap(err, "failed to create response object") + } + obj.GetObjectKind().SetGroupVersionKind(descriptor.response) + return obj, nil } // ValidateRequest validates a request object. Specifically it validates that @@ -342,3 +352,14 @@ type GroupHook struct { Group string Hook string } + +// 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. +// Note: name is only appended if set, e.g. the Discovery Hook does not have a name. +func GVHToPath(gvh GroupVersionHook, name string) string { + if name == "" { + return fmt.Sprintf("/%s/%s/%s", gvh.Group, gvh.Version, strings.ToLower(gvh.Hook)) + } + return fmt.Sprintf("/%s/%s/%s/%s", gvh.Group, gvh.Version, strings.ToLower(gvh.Hook), strings.ToLower(name)) +} diff --git a/internal/runtime/catalog/test/v1alpha1/conversion.go b/internal/runtime/catalog/test/v1alpha1/conversion.go new file mode 100644 index 000000000000..b27bd6f8bbb4 --- /dev/null +++ b/internal/runtime/catalog/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/catalog/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/catalog/test/v1alpha1/conversion_test.go b/internal/runtime/catalog/test/v1alpha1/conversion_test.go new file mode 100644 index 000000000000..8af5976cb341 --- /dev/null +++ b/internal/runtime/catalog/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/catalog/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/catalog/test/v1alpha1/doc.go b/internal/runtime/catalog/test/v1alpha1/doc.go new file mode 100644 index 000000000000..e692ed6e393f --- /dev/null +++ b/internal/runtime/catalog/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/catalog/test/v1alpha2 +package v1alpha1 diff --git a/internal/runtime/catalog/test/v1alpha1/fake_types.go b/internal/runtime/catalog/test/v1alpha1/fake_types.go index e794f0a569fa..b9843d43bcfe 100644 --- a/internal/runtime/catalog/test/v1alpha1/fake_types.go +++ b/internal/runtime/catalog/test/v1alpha1/fake_types.go @@ -24,6 +24,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + clusterv1alpha4 "sigs.k8s.io/cluster-api/api/v1alpha4" "sigs.k8s.io/cluster-api/internal/runtime/catalog" ) @@ -34,11 +35,17 @@ var ( // catalogBuilder is used to add rpc services and their request and response types // to a Catalog. - catalogBuilder = catalog.Builder{GroupVersion: GroupVersion} + 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) {} @@ -46,6 +53,8 @@ func FakeHook(*FakeRequest, *FakeResponse) {} type FakeRequest struct { metav1.TypeMeta `json:",inline"` + Cluster clusterv1alpha4.Cluster + Second string First int } diff --git a/internal/runtime/catalog/test/v1alpha1/zz_generated.conversion.go b/internal/runtime/catalog/test/v1alpha1/zz_generated.conversion.go new file mode 100644 index 000000000000..d37e97cfd2a2 --- /dev/null +++ b/internal/runtime/catalog/test/v1alpha1/zz_generated.conversion.go @@ -0,0 +1,115 @@ +//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" + v1alpha2 "sigs.k8s.io/cluster-api/internal/runtime/catalog/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.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.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/catalog/test/v1alpha2/fake_types.go b/internal/runtime/catalog/test/v1alpha2/fake_types.go index 2f9b27414b1c..581868c3331c 100644 --- a/internal/runtime/catalog/test/v1alpha2/fake_types.go +++ b/internal/runtime/catalog/test/v1alpha2/fake_types.go @@ -24,6 +24,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" "sigs.k8s.io/cluster-api/internal/runtime/catalog" ) @@ -34,7 +35,7 @@ var ( // catalogBuilder is used to add rpc services and their request and response types // to a Catalog. - catalogBuilder = catalog.Builder{GroupVersion: GroupVersion} + catalogBuilder = &catalog.Builder{GroupVersion: GroupVersion} // AddToCatalog adds rpc services defined in this package and their request and // response types to a catalog. @@ -46,6 +47,8 @@ func FakeHook(*FakeRequest, *FakeResponse) {} type FakeRequest struct { metav1.TypeMeta `json:",inline"` + Cluster clusterv1.Cluster + Second string First int } diff --git a/internal/runtime/client/client.go b/internal/runtime/client/client.go new file mode 100644 index 000000000000..02966ad0ba49 --- /dev/null +++ b/internal/runtime/client/client.go @@ -0,0 +1,304 @@ +/* +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 client provides the Runtime SDK client. +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net" + "net/http" + "net/url" + "path" + "strconv" + "time" + + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/runtime" + utilnet "k8s.io/apimachinery/pkg/util/net" + "k8s.io/client-go/transport" + + 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" + "sigs.k8s.io/cluster-api/internal/runtime/registry" +) + +const defaultDiscoveryTimeout = 10 * time.Second + +// Options are creation options for a Client. +type Options struct { + Catalog *catalog.Catalog + Registry registry.ExtensionRegistry +} + +// New returns a new Client. +func New(options Options) Client { + return &client{ + catalog: options.Catalog, + registry: options.Registry, + } +} + +// Client is the runtime client to interact with hooks and extensions. +type Client interface { + // WarmUp can be used to initialize a "cold" RuntimeClient with all + // known runtimev1.ExtensionConfigs at a given time. + // After WarmUp completes the RuntimeClient is considered ready. + WarmUp(extensionConfigList *runtimev1.ExtensionConfigList) error + + // IsReady return true after the RuntimeClient finishes warmup. + IsReady() bool + + // Discover makes the discovery call on the extension and returns an updated ExtensionConfig + // with extension handlers information in the extension status. + Discover(context.Context, *runtimev1.ExtensionConfig) (*runtimev1.ExtensionConfig, error) + + // Register registers the ExtensionConfig. + Register(extensionConfig *runtimev1.ExtensionConfig) error + + // Unregister unregisters the ExtensionConfig. + Unregister(extensionConfig *runtimev1.ExtensionConfig) error +} + +var _ Client = &client{} + +type client struct { + catalog *catalog.Catalog + registry registry.ExtensionRegistry +} + +func (c *client) WarmUp(extensionConfigList *runtimev1.ExtensionConfigList) error { + if err := c.registry.WarmUp(extensionConfigList); err != nil { + return errors.Wrap(err, "failed to warm up") + } + return nil +} + +func (c *client) IsReady() bool { + return c.registry.IsReady() +} + +func (c *client) Discover(ctx context.Context, extensionConfig *runtimev1.ExtensionConfig) (*runtimev1.ExtensionConfig, error) { + gvh, err := c.catalog.GroupVersionHook(runtimehooksv1.Discovery) + if err != nil { + return nil, errors.Wrap(err, "failed to compute GVH of hook") + } + + request := &runtimehooksv1.DiscoveryRequest{} + response := &runtimehooksv1.DiscoveryResponse{} + opts := &httpCallOptions{ + catalog: c.catalog, + config: extensionConfig.Spec.ClientConfig, + gvh: gvh, + timeout: defaultDiscoveryTimeout, + } + if err := httpCall(ctx, request, response, opts); err != nil { + return nil, errors.Wrap(err, "failed to call the Discovery endpoint") + } + // Check to see if the response is a failure and handle the failure accordingly. + if response.Status == runtimehooksv1.ResponseStatusFailure { + return nil, fmt.Errorf("discovery failed with %v", response.Message) + } + + modifiedExtensionConfig := extensionConfig.DeepCopy() + // Reset the handlers that were previously registered with the ExtensionConfig. + modifiedExtensionConfig.Status.Handlers = []runtimev1.ExtensionHandler{} + + for _, handler := range response.Handlers { + modifiedExtensionConfig.Status.Handlers = append( + modifiedExtensionConfig.Status.Handlers, + runtimev1.ExtensionHandler{ + Name: handler.Name + "." + extensionConfig.Name, // Uniquely identifies a handler of an Extension. + RequestHook: runtimev1.GroupVersionHook{ + APIVersion: handler.RequestHook.APIVersion, + Hook: handler.RequestHook.Hook, + }, + TimeoutSeconds: handler.TimeoutSeconds, + FailurePolicy: (*runtimev1.FailurePolicy)(handler.FailurePolicy), + }, + ) + } + + return modifiedExtensionConfig, nil +} + +func (c *client) Register(extensionConfig *runtimev1.ExtensionConfig) error { + if err := c.registry.Add(extensionConfig); err != nil { + return errors.Wrap(err, "failed to register ExtensionConfig") + } + return nil +} + +func (c *client) Unregister(extensionConfig *runtimev1.ExtensionConfig) error { + if err := c.registry.Remove(extensionConfig); err != nil { + return errors.Wrap(err, "failed to unregister ExtensionConfig") + } + return nil +} + +type httpCallOptions struct { + catalog *catalog.Catalog + config runtimev1.ClientConfig + gvh catalog.GroupVersionHook + name string + timeout time.Duration +} + +func httpCall(ctx context.Context, request, response runtime.Object, opts *httpCallOptions) error { + if opts == nil || request == nil || response == nil { + return fmt.Errorf("opts, request and response cannot be nil") + } + if opts.catalog == nil { + return fmt.Errorf("opts.Catalog cannot be nil") + } + + url, err := urlForExtension(opts.config, opts.gvh, opts.name) + if err != nil { + return errors.Wrapf(err, "failed to compute URL of the extension handler %q", opts.name) + } + + requireConversion := opts.gvh.Version != request.GetObjectKind().GroupVersionKind().Version + + requestLocal := request + responseLocal := response + + if requireConversion { + // The request and response objects need to be converted to match the version supported by + // the ExtensionHandler. + var err error + + // Create a new hook request object that is compatible with the version of ExtensionHandler. + requestLocal, err = opts.catalog.NewRequest(opts.gvh) + if err != nil { + return errors.Wrapf(err, "failed to create new request for hook %s", opts.gvh) + } + + if err := opts.catalog.Convert(request, requestLocal, ctx); err != nil { + return errors.Wrapf(err, "failed to convert request from %T to %T", request, requestLocal) + } + + // Create a new hook response object that is compatible with the version of the ExtensionHandler. + responseLocal, err = opts.catalog.NewResponse(opts.gvh) + if err != nil { + return errors.Wrapf(err, "failed to create new response for hook %s", opts.gvh) + } + } + + // Make sure the request is compatible with the version of the hook expected by the ExtensionHandler. + if err := opts.catalog.ValidateRequest(opts.gvh, requestLocal); err != nil { + return errors.Wrapf(err, "request object is invalid for hook %s", opts.gvh) + } + // Make sure the response is compatible with the version of the hook expected by the ExtensionHandler. + if err := opts.catalog.ValidateResponse(opts.gvh, responseLocal); err != nil { + return errors.Wrapf(err, "response object is invalid for hook %s", opts.gvh) + } + + postBody, err := json.Marshal(requestLocal) + if err != nil { + return errors.Wrap(err, "failed to marshall request object") + } + + if opts.timeout != 0 { + // Make the call timebound if timeout is non-zero value. + values := url.Query() + values.Add("timeout", opts.timeout.String()) + url.RawQuery = values.Encode() + + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, opts.timeout) + defer cancel() + } + + httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, url.String(), bytes.NewBuffer(postBody)) + if err != nil { + return errors.Wrap(err, "failed to create http request") + } + + // use client-go's transport.TLSConfigureFor to ensure good defaults for tls + client := http.DefaultClient + if opts.config.CABundle != nil { + tlsConfig, err := transport.TLSConfigFor(&transport.Config{ + TLS: transport.TLSConfig{ + CAData: opts.config.CABundle, + ServerName: url.Hostname(), + }, + }) + if err != nil { + return errors.Wrap(err, "failed to create tls config") + } + // this also adds http2 + client.Transport = utilnet.SetTransportDefaults(&http.Transport{ + TLSClientConfig: tlsConfig, + }) + } + resp, err := client.Do(httpRequest) + if err != nil { + return errors.Wrap(err, "failed to perform the http call") + } + + defer resp.Body.Close() + if err := json.NewDecoder(resp.Body).Decode(responseLocal); err != nil { + return errors.Wrap(err, "failed to decode response") + } + + if requireConversion { + // Convert the received response to the original version of the response object. + if err := opts.catalog.Convert(responseLocal, response, ctx); err != nil { + return errors.Wrapf(err, "failed to convert response from %T to %T", requestLocal, response) + } + } + + return nil +} + +func urlForExtension(config runtimev1.ClientConfig, gvh catalog.GroupVersionHook, name string) (*url.URL, error) { + var u *url.URL + if config.Service != nil { + // The Extension's ClientConfig points ot a service. Construct the URL to the service. + svc := config.Service + host := svc.Name + "." + svc.Namespace + ".svc" + if svc.Port != nil { + host = net.JoinHostPort(host, strconv.Itoa(int(*svc.Port))) + } + scheme := "http" + if len(config.CABundle) > 0 { + scheme = "https" + } + u = &url.URL{ + Scheme: scheme, + Host: host, + } + if svc.Path != nil { + u.Path = *svc.Path + } + } else { + if config.URL == nil { + return nil, errors.New("at least one of Service and URL should be defined in config") + } + var err error + u, err = url.Parse(*config.URL) + if err != nil { + return nil, errors.Wrap(err, "URL in ClientConfig is invalid") + } + } + // Add the subpatch to the ExtensionHandler for the given hook. + u.Path = path.Join(u.Path, catalog.GVHToPath(gvh, name)) + return u, nil +} diff --git a/internal/runtime/client/client_test.go b/internal/runtime/client/client_test.go new file mode 100644 index 000000000000..497f1b18cf78 --- /dev/null +++ b/internal/runtime/client/client_test.go @@ -0,0 +1,271 @@ +/* +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 client + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/pointer" + + runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/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" +) + +func TestClient_httpCall(t *testing.T) { + g := NewWithT(t) + + tableTests := []struct { + name string + request runtime.Object + response runtime.Object + opts *httpCallOptions + wantErr bool + }{ + { + name: "should error if request, response and options are nil", + request: nil, + response: nil, + opts: nil, + wantErr: true, + }, + { + name: "should error if catalog is not set", + request: &fakev1alpha1.FakeRequest{}, + response: &fakev1alpha1.FakeResponse{}, + opts: &httpCallOptions{ + catalog: nil, + }, + wantErr: true, + }, + { + name: "should error if hooks is not registered with catalog", + request: &fakev1alpha1.FakeRequest{}, + response: &fakev1alpha1.FakeResponse{}, + opts: &httpCallOptions{ + catalog: catalog.New(), + }, + wantErr: true, + }, + { + name: "should succeed for valid request and response objects", + request: &fakev1alpha1.FakeRequest{ + TypeMeta: metav1.TypeMeta{ + Kind: "FakeRequest", + APIVersion: fakev1alpha1.GroupVersion.Identifier(), + }, + }, + response: &fakev1alpha1.FakeResponse{}, + opts: func() *httpCallOptions { + c := catalog.New() + g.Expect(fakev1alpha1.AddToCatalog(c)).To(Succeed()) + + // get same gvh for hook by using the FakeHook and catalog + gvh, err := c.GroupVersionHook(fakev1alpha1.FakeHook) + g.Expect(err).To(Succeed()) + + return &httpCallOptions{ + catalog: c, + gvh: gvh, + } + }(), + wantErr: false, + }, + { + name: "should success if response and response are valid objects - with conversion", + request: &fakev1alpha2.FakeRequest{ + TypeMeta: metav1.TypeMeta{ + Kind: "FakeRequest", + APIVersion: fakev1alpha2.GroupVersion.Identifier(), + }, + }, + response: &fakev1alpha2.FakeResponse{}, + opts: func() *httpCallOptions { + c := catalog.New() + // register fakev1alpha1 and fakev1alpha2 to enable conversion + g.Expect(fakev1alpha1.AddToCatalog(c)).To(Succeed()) + g.Expect(fakev1alpha2.AddToCatalog(c)).To(Succeed()) + + // get same gvh for hook by using the FakeHook and catalog + gvh, err := c.GroupVersionHook(fakev1alpha1.FakeHook) + g.Expect(err).To(Succeed()) + + return &httpCallOptions{ + catalog: c, + gvh: gvh, + } + }(), + wantErr: false, + }, + } + for _, tt := range tableTests { + t.Run(tt.name, func(t *testing.T) { + // a http server is only required if we have a valid catalog, otherwise httpCall will not reach out to the server + if tt.opts != nil && tt.opts.catalog != nil { + // create http server with fakeHookHandler + mux := http.NewServeMux() + mux.HandleFunc("/", fakeHookHandler) + srv := httptest.NewServer(mux) + defer srv.Close() + + // set url to srv for in tt.opts + tt.opts.config.URL = pointer.String(srv.URL) + } + + err := httpCall(context.TODO(), tt.request, tt.response, tt.opts) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).NotTo(HaveOccurred()) + } + }) + } +} + +func fakeHookHandler(w http.ResponseWriter, r *http.Request) { + response := &fakev1alpha1.FakeResponse{ + TypeMeta: metav1.TypeMeta{ + Kind: "FakeHookResponse", + APIVersion: fakev1alpha1.GroupVersion.Identifier(), + }, + Second: "", + First: 1, + } + respBody, err := json.Marshal(response) + if err != nil { + panic(err) + } + w.WriteHeader(http.StatusOK) + _, _ = w.Write(respBody) +} + +func TestURLForExtension(t *testing.T) { + type args struct { + config runtimev1.ClientConfig + gvh catalog.GroupVersionHook + extensionHandlerName string + } + + type want struct { + scheme string + host string + path string + } + + gvh := catalog.GroupVersionHook{ + Group: "test.runtime.cluster.x-k8s.io", + Version: "v1alpha1", + Hook: "testhook.test-extension", + } + + tests := []struct { + name string + args args + want want + wantErr bool + }{ + { + name: "ClientConfig using service should have correct URL values", + args: args{ + config: runtimev1.ClientConfig{ + Service: &runtimev1.ServiceReference{ + Namespace: "test1", + Name: "extension-service", + Port: pointer.Int32(8443), + }, + }, + gvh: gvh, + extensionHandlerName: "test-handler", + }, + want: want{ + scheme: "http", + host: "extension-service.test1.svc:8443", + path: catalog.GVHToPath(gvh, "test-handler"), + }, + wantErr: false, + }, + { + name: "ClientConfig using service and CAbundle should have correct URL values", + args: args{ + config: runtimev1.ClientConfig{ + Service: &runtimev1.ServiceReference{ + Namespace: "test1", + Name: "extension-service", + Port: pointer.Int32(8443), + }, + CABundle: []byte("some-ca-data"), + }, + gvh: gvh, + extensionHandlerName: "test-handler", + }, + want: want{ + scheme: "https", + host: "extension-service.test1.svc:8443", + path: catalog.GVHToPath(gvh, "test-handler"), + }, + wantErr: false, + }, + { + name: "ClientConfig using URL should have correct URL values", + args: args{ + config: runtimev1.ClientConfig{ + URL: pointer.String("https://extension-host.com"), + }, + gvh: gvh, + extensionHandlerName: "test-handler", + }, + want: want{ + scheme: "https", + host: "extension-host.com", + path: catalog.GVHToPath(gvh, "test-handler"), + }, + wantErr: false, + }, + { + name: "should error if both Service and URL are missing", + args: args{ + config: runtimev1.ClientConfig{}, + gvh: gvh, + extensionHandlerName: "test-handler", + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + u, err := urlForExtension(tt.args.config, tt.args.gvh, tt.args.extensionHandlerName) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(u.Scheme).To(Equal(tt.want.scheme)) + g.Expect(u.Host).To(Equal(tt.want.host)) + g.Expect(u.Path).To(Equal(tt.want.path)) + } + }) + } +}