diff --git a/Makefile b/Makefile index e91372f9d47c..f63e81883e65 100644 --- a/Makefile +++ b/Makefile @@ -245,6 +245,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/... 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..1cbe4a57b67e --- /dev/null +++ b/exp/runtime/hooks/api/v1alpha1/common_types.go @@ -0,0 +1,27 @@ +/* +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 ResponseStatus = "Success" + + 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..010f7ef03ad0 --- /dev/null +++ b/exp/runtime/hooks/api/v1alpha1/discovery_types.go @@ -0,0 +1,86 @@ +/* +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" +) + +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 causes the admission to fail. + FailurePolicyFail FailurePolicy = "Fail" +) + +type Hook struct { + // APIVersion is the Version of the Hook + APIVersion string `json:"apiVersion"` + + // Name is the name of the hook + Name string `json:"name"` +} + +type RuntimeExtension struct { + // Name is the name of the RuntimeExtension + Name string `json:"name"` + + // Hook defines the specific runtime event for which this RuntimeExtension calls. + Hook Hook `json:"hook"` + + // TimeoutSeconds defines the timeout duration for client calls to the Hook + TimeoutSeconds *int32 `json:"timeoutSeconds,omitempty"` + + // FailurePolicy defines how failures in calls to the Hook should be handled by a client. + FailurePolicy *FailurePolicy `json:"failurePolicy,omitempty"` +} + +// DiscoveryHookRequest foo bar baz. +// +kubebuilder:object:root=true +type DiscoveryHookRequest struct { + metav1.TypeMeta `json:",inline"` +} + +// DiscoveryHookResponse foo bar baz. +// +kubebuilder:object:root=true +type DiscoveryHookResponse 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"` + + Extensions []RuntimeExtension `json:"extensions"` +} + +func Discovery(*DiscoveryHookRequest, *DiscoveryHookResponse) {} + +func init() { + catalogBuilder.RegisterHook(Discovery, &catalog.HookMeta{ + Tags: []string{"Discovery"}, + Summary: "Discovery endpoint", + Description: "Discovery endpoint discovers the supported hook of a runtime extension", + 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..974d6e816c00 --- /dev/null +++ b/exp/runtime/hooks/api/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 the v1alpha1 idl implementation for extension1. +// +k8s:conversion-gen=sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha3 +// +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..89b551ba6c85 --- /dev/null +++ b/exp/runtime/hooks/api/v1alpha1/groupversion_info.go @@ -0,0 +1,43 @@ +/* +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 rpc services 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 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 +) 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..ed455a27cb54 --- /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 *DiscoveryHookRequest) DeepCopyInto(out *DiscoveryHookRequest) { + *out = *in + out.TypeMeta = in.TypeMeta +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DiscoveryHookRequest. +func (in *DiscoveryHookRequest) DeepCopy() *DiscoveryHookRequest { + if in == nil { + return nil + } + out := new(DiscoveryHookRequest) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DiscoveryHookRequest) 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 *DiscoveryHookResponse) DeepCopyInto(out *DiscoveryHookResponse) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.Extensions != nil { + in, out := &in.Extensions, &out.Extensions + *out = make([]RuntimeExtension, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DiscoveryHookResponse. +func (in *DiscoveryHookResponse) DeepCopy() *DiscoveryHookResponse { + if in == nil { + return nil + } + out := new(DiscoveryHookResponse) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DiscoveryHookResponse) 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 *Hook) DeepCopyInto(out *Hook) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Hook. +func (in *Hook) DeepCopy() *Hook { + if in == nil { + return nil + } + out := new(Hook) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RuntimeExtension) DeepCopyInto(out *RuntimeExtension) { + *out = *in + out.Hook = in.Hook + 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 RuntimeExtension. +func (in *RuntimeExtension) DeepCopy() *RuntimeExtension { + if in == nil { + return nil + } + out := new(RuntimeExtension) + in.DeepCopyInto(out) + return out +} diff --git a/internal/runtime/catalog/catalog.go b/internal/runtime/catalog/catalog.go index f9ecc79d3a84..c463a58d31f8 100644 --- a/internal/runtime/catalog/catalog.go +++ b/internal/runtime/catalog/catalog.go @@ -334,3 +334,14 @@ func (gvh GroupVersionHook) String() string { var emptyGroupVersionHook = GroupVersionHook{} var emptyGroupVersionKind = schema.GroupVersionKind{} + +// 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/client.go/client.go b/internal/runtime/client.go/client.go new file mode 100644 index 000000000000..213183fa6785 --- /dev/null +++ b/internal/runtime/client.go/client.go @@ -0,0 +1,249 @@ +/* +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 ( + "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" +) + +const defaultDiscoveryTimeout = 10 * time.Second + +// Options are creation options for a Client. +type Options struct { + Catalog *catalog.Catalog +} + +func New(options Options) Client { + return &client{ + catalog: options.Catalog, + } +} + +type Client interface { + // Discover makes the discovery call on the extension and updates the runtime extensions + // information in the extension status. + // TODO: Need a final decision on if we also want to run register inside discover. + Discover(context.Context, *runtimev1.ExtensionConfig) (*runtimev1.ExtensionConfig, error) +} + +var _ Client = &client{} + +type client struct { + catalog *catalog.Catalog +} + +func (c *client) Discover(ctx context.Context, ext *runtimev1.ExtensionConfig) (*runtimev1.ExtensionConfig, error) { + gvh, err := c.catalog.GroupVersionHook(runtimehooksv1.Discovery) + if err != nil { + return nil, err + } + + request := &runtimehooksv1.DiscoveryHookRequest{} + response := &runtimehooksv1.DiscoveryHookResponse{} + + // Future work: The discovery runtime extension could be operating on a different hook version than + // the latest. We will have to loop through different versions of the discover hook here to actually + // finish discovery. + opts := &httpCallOptions{ + catalog: c.catalog, + config: ext.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 extension") + } + + modifiedExtension := &runtimev1.ExtensionConfig{} + ext.DeepCopyInto(modifiedExtension) + modifiedExtension.Status.Handlers = []runtimev1.ExtensionHandler{} + for _, extension := range response.Extensions { + modifiedExtension.Status.Handlers = append( + modifiedExtension.Status.Handlers, + runtimev1.ExtensionHandler{ + Name: extension.Name + "." + ext.Name, + RequestHook: runtimev1.GroupVersionHook{ + APIVersion: extension.Hook.APIVersion, + Hook: extension.Hook.Name, + }, + TimeoutSeconds: extension.TimeoutSeconds, + FailurePolicy: (*runtimev1.FailurePolicy)(extension.FailurePolicy), + }, + ) + } + + return modifiedExtension, nil +} + +type httpCallOptions struct { + catalog *catalog.Catalog + config runtimev1.ClientConfig + gvh catalog.GroupVersionHook + name string + timeout time.Duration + failurePolicy *runtimev1.FailurePolicy +} + +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 err + } + + requireConversion := opts.gvh.Version != request.GetObjectKind().GroupVersionKind().Version + + requestLocal := request + responseLocal := response + + if requireConversion { + var err error + requestLocal, err = opts.catalog.NewRequest(opts.gvh) + if err != nil { + return err + } + + if err := opts.catalog.Convert(request, requestLocal, ctx); err != nil { + return err + } + + responseLocal, err = opts.catalog.NewResponse(opts.gvh) + if err != nil { + return err + } + } + + if err := opts.catalog.ValidateRequest(opts.gvh, requestLocal); err != nil { + return errors.Wrapf(err, "request object is invalid for hook %v", opts.gvh) + } + if err := opts.catalog.ValidateResponse(opts.gvh, responseLocal); err != nil { + return errors.Wrapf(err, "response object is invalid for hook %v", opts.gvh) + } + + postBody, err := json.Marshal(requestLocal) + if err != nil { + return errors.Wrap(err, "failed to marshall request object") + } + + if opts.timeout != 0 { + values := url.Query() + values.Add("timeout", opts.timeout.String()) + url.RawQuery = values.Encode() + + ctx, _ = context.WithTimeout(ctx, opts.timeout) + } + + 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 err + } + + defer resp.Body.Close() + if err := json.NewDecoder(resp.Body).Decode(responseLocal); err != nil { + return errors.Wrap(err, "failed to decode response") + } + + if requireConversion { + if err := opts.catalog.Convert(responseLocal, response, ctx); err != nil { + return err + } + } + + return nil +} + +func urlForExtension(config runtimev1.ClientConfig, gvh catalog.GroupVersionHook, name string) (*url.URL, error) { + var u *url.URL + // TODO: Add additional validation here - webhook should make this safe, but for now it might be good to do url validation in here. + if config.Service != nil { + svc := config.Service + host := svc.Name + "." + svc.Namespace + ".svc" + if svc.Port != nil { + host = net.JoinHostPort(host, strconv.Itoa(int(*svc.Port))) + } + // TODO: decide if we want to enforce https + 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 config is invalid") + } + } + u.Path = path.Join(u.Path, catalog.GVHToPath(gvh, name)) + return u, nil +} diff --git a/internal/runtime/client.go/client_test.go b/internal/runtime/client.go/client_test.go new file mode 100644 index 000000000000..e1516a6c20d4 --- /dev/null +++ b/internal/runtime/client.go/client_test.go @@ -0,0 +1,169 @@ +/* +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" + + "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() + c.AddHook( + fakev1alpha1.GroupVersion, + fakev1alpha1.FakeHook, + &catalog.HookMeta{}, + ) + + // 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 to enable conversion + g.Expect(fakev1alpha1.AddToCatalog(c)).To(Succeed()) + c.AddHook( + fakev1alpha1.GroupVersion, + fakev1alpha1.FakeHook, + &catalog.HookMeta{}, + ) + + // 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) + } + + assert := g.Expect(httpCall(context.TODO(), tt.request, tt.response, tt.opts)) + if tt.wantErr { + assert.To(HaveOccurred()) + } else { + assert.To(Succeed()) + } + }) + } +} + +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) +}