From 2b3293af225a0471cdf187777c63b4d5b3863456 Mon Sep 17 00:00:00 2001 From: Martin Schuppert Date: Mon, 3 Jul 2023 17:40:47 +0200 Subject: [PATCH] Allow customize route via route.Override Adds an OverridSpec to the Route which allows to customize metadata.Annotations, metadata.Labels and spec of a route. The override values get merged into the object definition created by the operator. This allows e.g. to add custom labels, configure the route via annotations as in [1], or set TLS parameters. [1] https://docs.openshift.com/container-platform/4.13/networking/routes/route-configuration.html#nw-route-specific-annotations_route-configuration Jira: OSP-21715 Jira: OSP-26299 --- modules/common/endpoint/endpoint.go | 8 +- modules/common/go.mod | 2 +- modules/common/route/route.go | 46 ++- modules/common/route/types.go | 145 +++++++++ modules/common/route/zz_generated.deepcopy.go | 175 ++++++++++ modules/common/test/functional/route_test.go | 305 ++++++++++++++++++ modules/common/test/functional/suite_test.go | 8 + modules/test/go.mod | 10 +- 8 files changed, 690 insertions(+), 9 deletions(-) create mode 100644 modules/common/route/zz_generated.deepcopy.go create mode 100644 modules/common/test/functional/route_test.go diff --git a/modules/common/endpoint/endpoint.go b/modules/common/endpoint/endpoint.go index 6f1d0394..91c74da1 100644 --- a/modules/common/endpoint/endpoint.go +++ b/modules/common/endpoint/endpoint.go @@ -52,6 +52,8 @@ type Data struct { Path string // details for metallb service generation MetalLB *MetalLBData + // possible overrides for Route + RouteOverride *route.OverrideSpec } // MetalLBData - information specific to creating the MetalLB service @@ -174,7 +176,7 @@ func ExposeEndpoints( if endpointType == EndpointPublic { // Create the route // TODO TLS - route := route.NewRoute( + route, err := route.NewRoute( route.GenericRoute(&route.GenericRouteDetails{ Name: endpointName, Namespace: h.GetBeforeObject().GetNamespace(), @@ -184,7 +186,11 @@ func ExposeEndpoints( }), exportLabels, timeout, + data.RouteOverride, ) + if err != nil { + return endpointMap, ctrl.Result{}, err + } ctrlResult, err = route.CreateOrPatch(ctx, h) if err != nil { diff --git a/modules/common/go.mod b/modules/common/go.mod index a84ef8a2..acdec6d7 100644 --- a/modules/common/go.mod +++ b/modules/common/go.mod @@ -69,7 +69,7 @@ require ( k8s.io/component-base v0.26.6 // indirect; indirect // indirect k8s.io/klog/v2 v2.80.1 // indirect k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a // indirect; indirect // indirect - k8s.io/utils v0.0.0-20230711102312-30195339c3c7 // indirect; indirect // indirect + k8s.io/utils v0.0.0-20230711102312-30195339c3c7 // indirect // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect; indirect // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.3.0 // indirect diff --git a/modules/common/route/route.go b/modules/common/route/route.go index 016e2e54..c6ba5d12 100644 --- a/modules/common/route/route.go +++ b/modules/common/route/route.go @@ -18,6 +18,7 @@ package route import ( "context" + "encoding/json" "fmt" "time" @@ -29,6 +30,7 @@ import ( k8s_errors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/apimachinery/pkg/util/strategicpatch" ctrl "sigs.k8s.io/controller-runtime" ) @@ -37,11 +39,49 @@ func NewRoute( route *routev1.Route, labels map[string]string, timeout time.Duration, -) *Route { - return &Route{ + override *OverrideSpec, +) (*Route, error) { + r := &Route{ route: route, timeout: timeout, } + + // patch route with possible overrides of Labels, Annotations and Spec + if override != nil { + if override.EmbeddedLabelsAnnotations != nil { + if override.Labels != nil { + r.route.Labels = util.MergeStringMaps(override.Labels, r.route.Labels) + } + if override.Annotations != nil { + r.route.Annotations = util.MergeStringMaps(override.Annotations, r.route.Annotations) + } + } + if override.Spec != nil { + originalSpec, err := json.Marshal(r.route.Spec) + if err != nil { + return r, fmt.Errorf("error marshalling Route Spec: %w", err) + } + + patch, err := json.Marshal(override.Spec) + if err != nil { + return r, fmt.Errorf("error marshalling Route Spec override: %w", err) + } + + patchedJSON, err := strategicpatch.StrategicMergePatch(originalSpec, patch, routev1.RouteSpec{}) + if err != nil { + return r, fmt.Errorf("error patching Route Spec: %w", err) + } + + patchedSpec := routev1.RouteSpec{} + err = json.Unmarshal(patchedJSON, &patchedSpec) + if err != nil { + return r, fmt.Errorf("error unmarshalling patched Route Spec: %w", err) + } + r.route.Spec = patchedSpec + } + } + + return r, nil } // GetHostname - returns the hostname of the created route @@ -89,7 +129,7 @@ func (r *Route) CreateOrPatch( } op, err := controllerutil.CreateOrPatch(ctx, h.GetClient(), route, func() error { - route.Labels = util.MergeStringMaps(route.Labels, r.route.Labels) + route.Labels = r.route.Labels route.Annotations = r.route.Annotations route.Spec = r.route.Spec if len(route.Spec.Host) == 0 && len(route.Status.Ingress) > 0 { diff --git a/modules/common/route/types.go b/modules/common/route/types.go index 7a2e9171..5f0b4568 100644 --- a/modules/common/route/types.go +++ b/modules/common/route/types.go @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +// +kubebuilder:object:generate:=true + package route import ( @@ -38,3 +40,146 @@ type GenericRouteDetails struct { TargetPortName string FQDN string } + +// OverrideSpec configuration for the Route created to serve traffic to the cluster. +type OverrideSpec struct { + // +optional + *EmbeddedLabelsAnnotations `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Spec defines the behavior of a Route. + // https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + // + // The spec will be merged using StrategicMergePatch + // - Provided parameters will override the ones from the original spec. + // - Required parameters of sub structs have to be named. + // - For parameters which are list of struct it depends on the patchStrategy defined on the list + // https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/#notes-on-the-strategic-merge-patch + // If `patchStrategy:"merge"` is set, src and dst list gets merged, otherwise they get replaced. + // +optional + Spec *Spec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` +} + +// EmbeddedLabelsAnnotations is an embedded subset of the fields included in k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta. +// Only labels and annotations are included. +// New labels/annotations get merged with the ones created by the operator. If a privided +// annotation/label is the same as one created by the service operator, the ones provided +// via this override will replace the one from the operator. +type EmbeddedLabelsAnnotations struct { + // Map of string keys and values that can be used to organize and categorize + // (scope and select) objects. May match selectors of replication controllers + // and services. + // More info: http://kubernetes.io/docs/user-guide/labels + // +optional + Labels map[string]string `json:"labels,omitempty" protobuf:"bytes,11,rep,name=labels"` + + // Annotations is an unstructured key value map stored with a resource that may be + // set by external tools to store and retrieve arbitrary metadata. They are not + // queryable and should be preserved when modifying objects. + // More info: http://kubernetes.io/docs/user-guide/annotations + // +optional + Annotations map[string]string `json:"annotations,omitempty" protobuf:"bytes,12,rep,name=annotations"` +} + +// Spec describes the hostname or path the route exposes, any security information, +// and one to four backends (services) the route points to. Requests are distributed +// among the backends depending on the weights assigned to each backend. When using +// roundrobin scheduling the portion of requests that go to each backend is the backend +// weight divided by the sum of all of the backend weights. When the backend has more than +// one endpoint the requests that end up on the backend are roundrobin distributed among +// the endpoints. Weights are between 0 and 256 with default 100. Weight 0 causes no requests +// to the backend. If all weights are zero the route will be considered to have no backends +// and return a standard 503 response. +// +// The `tls` field is optional and allows specific certificates or behavior for the +// route. Routers typically configure a default certificate on a wildcard domain to +// terminate routes without explicit certificates, but custom hostnames usually must +// choose passthrough (send traffic directly to the backend via the TLS Server-Name- +// Indication field) or provide a certificate. +// +// Copy of RouteSpec in https://github.com/openshift/api/blob/master/route/v1/types.go, +// parameters set to be optional, have omitempty, and no default. +type Spec struct { + // host is an alias/DNS that points to the service. Optional. + // If not specified a route name will typically be automatically + // chosen. + // Must follow DNS952 subdomain conventions. + // + // +optional + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=`^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$` + Host string `json:"host,omitempty" protobuf:"bytes,1,opt,name=host"` + // subdomain is a DNS subdomain that is requested within the ingress controller's + // domain (as a subdomain). If host is set this field is ignored. An ingress + // controller may choose to ignore this suggested name, in which case the controller + // will report the assigned name in the status.ingress array or refuse to admit the + // route. If this value is set and the server does not support this field host will + // be populated automatically. Otherwise host is left empty. The field may have + // multiple parts separated by a dot, but not all ingress controllers may honor + // the request. This field may not be changed after creation except by a user with + // the update routes/custom-host permission. + // + // Example: subdomain `frontend` automatically receives the router subdomain + // `apps.mycluster.com` to have a full hostname `frontend.apps.mycluster.com`. + // + // +optional + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=`^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$` + Subdomain string `json:"subdomain,omitempty" protobuf:"bytes,8,opt,name=subdomain"` + + // path that the router watches for, to route traffic for to the service. Optional + // + // +optional + // +kubebuilder:validation:Pattern=`^/` + Path string `json:"path,omitempty" protobuf:"bytes,2,opt,name=path"` + + // to is an object the route should use as the primary backend. Only the Service kind + // is allowed, and it will be defaulted to Service. If the weight field (0-256 default 100) + // is set to zero, no traffic will be sent to this backend. + To TargetReference `json:"to,omitempty" protobuf:"bytes,3,opt,name=to"` + + // alternateBackends allows up to 3 additional backends to be assigned to the route. + // Only the Service kind is allowed, and it will be defaulted to Service. + // Use the weight field in RouteTargetReference object to specify relative preference. + // + // +kubebuilder:validation:MaxItems=3 + AlternateBackends []TargetReference `json:"alternateBackends,omitempty" protobuf:"bytes,4,rep,name=alternateBackends"` + + // If specified, the port to be used by the router. Most routers will use all + // endpoints exposed by the service by default - set this value to instruct routers + // which port to use. + // +optional + Port *routev1.RoutePort `json:"port,omitempty" protobuf:"bytes,5,opt,name=port"` + + // The tls field provides the ability to configure certificates and termination for the route. + TLS *routev1.TLSConfig `json:"tls,omitempty" protobuf:"bytes,6,opt,name=tls"` + + // Wildcard policy if any for the route. + // Currently only 'Subdomain' or 'None' is allowed. + // + // +kubebuilder:validation:Enum=None;Subdomain;"" + WildcardPolicy routev1.WildcardPolicyType `json:"wildcardPolicy,omitempty" protobuf:"bytes,7,opt,name=wildcardPolicy"` +} + +// TargetReference specifies the target that resolve into endpoints. Only the 'Service' +// kind is allowed. Use 'weight' field to emphasize one over others. +// Copy of RouteTargetReference in https://github.com/openshift/api/blob/master/route/v1/types.go, +// parameters set to be optional, have omitempty, and no default. +type TargetReference struct { + // The kind of target that the route is referring to. Currently, only 'Service' is allowed + // + // +optional + // +kubebuilder:validation:Enum=Service;"" + Kind string `json:"kind,omitempty" protobuf:"bytes,1,opt,name=kind"` + + // name of the service/target that is being referred to. e.g. name of the service + // + // +optional + Name string `json:"name,omitempty" protobuf:"bytes,2,opt,name=name"` + + // weight as an integer between 0 and 256, default 100, that specifies the target's relative weight + // against other target reference objects. 0 suppresses requests to this backend. + // + // +optional + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=256 + Weight *int32 `json:"weight,omitempty" protobuf:"varint,3,opt,name=weight"` +} diff --git a/modules/common/route/zz_generated.deepcopy.go b/modules/common/route/zz_generated.deepcopy.go new file mode 100644 index 00000000..b60d3ce1 --- /dev/null +++ b/modules/common/route/zz_generated.deepcopy.go @@ -0,0 +1,175 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* + + +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 route + +import ( + "github.com/openshift/api/route/v1" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EmbeddedLabelsAnnotations) DeepCopyInto(out *EmbeddedLabelsAnnotations) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EmbeddedLabelsAnnotations. +func (in *EmbeddedLabelsAnnotations) DeepCopy() *EmbeddedLabelsAnnotations { + if in == nil { + return nil + } + out := new(EmbeddedLabelsAnnotations) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GenericRouteDetails) DeepCopyInto(out *GenericRouteDetails) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericRouteDetails. +func (in *GenericRouteDetails) DeepCopy() *GenericRouteDetails { + if in == nil { + return nil + } + out := new(GenericRouteDetails) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OverrideSpec) DeepCopyInto(out *OverrideSpec) { + *out = *in + if in.EmbeddedLabelsAnnotations != nil { + in, out := &in.EmbeddedLabelsAnnotations, &out.EmbeddedLabelsAnnotations + *out = new(EmbeddedLabelsAnnotations) + (*in).DeepCopyInto(*out) + } + if in.Spec != nil { + in, out := &in.Spec, &out.Spec + *out = new(Spec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OverrideSpec. +func (in *OverrideSpec) DeepCopy() *OverrideSpec { + if in == nil { + return nil + } + out := new(OverrideSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Route) DeepCopyInto(out *Route) { + *out = *in + if in.route != nil { + in, out := &in.route, &out.route + *out = new(v1.Route) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Route. +func (in *Route) DeepCopy() *Route { + if in == nil { + return nil + } + out := new(Route) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Spec) DeepCopyInto(out *Spec) { + *out = *in + in.To.DeepCopyInto(&out.To) + if in.AlternateBackends != nil { + in, out := &in.AlternateBackends, &out.AlternateBackends + *out = make([]TargetReference, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Port != nil { + in, out := &in.Port, &out.Port + *out = new(v1.RoutePort) + **out = **in + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(v1.TLSConfig) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Spec. +func (in *Spec) DeepCopy() *Spec { + if in == nil { + return nil + } + out := new(Spec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TargetReference) DeepCopyInto(out *TargetReference) { + *out = *in + if in.Weight != nil { + in, out := &in.Weight, &out.Weight + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetReference. +func (in *TargetReference) DeepCopy() *TargetReference { + if in == nil { + return nil + } + out := new(TargetReference) + in.DeepCopyInto(out) + return out +} diff --git a/modules/common/test/functional/route_test.go b/modules/common/test/functional/route_test.go new file mode 100644 index 00000000..b0efd9fb --- /dev/null +++ b/modules/common/test/functional/route_test.go @@ -0,0 +1,305 @@ +/* +Copyright 2023 Red Hat + +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 functional + +import ( + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/openstack-k8s-operators/lib-common/modules/common/route" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/pointer" + + routev1 "github.com/openshift/api/route/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func getExampleRoute(namespace string) *routev1.Route { + return &routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-route", + Namespace: namespace, + Labels: map[string]string{ + "label": "a", + "replace": "a", + }, + Annotations: map[string]string{ + "anno": "a", + "replace": "a", + }, + }, + Spec: routev1.RouteSpec{ + Host: "some.host.svc", + Port: &routev1.RoutePort{ + TargetPort: intstr.FromInt(80), + }, + To: routev1.RouteTargetReference{ + Kind: "Service", + Name: "my-service", + Weight: pointer.Int32(100), + }, + }, + } +} + +var _ = Describe("route package", func() { + var namespace string + + BeforeEach(func() { + // NOTE(gibi): We need to create a unique namespace for each test run + // as namespaces cannot be deleted in a locally running envtest. See + // https://book.kubebuilder.io/reference/envtest.html#namespace-usage-limitation + namespace = uuid.New().String() + th.CreateNamespace(namespace) + // We still request the delete of the Namespace to properly cleanup if + // we run the test in an existing cluster. + DeferCleanup(th.DeleteNamespace, namespace) + + }) + + It("creates route with defaults", func() { + r, err := route.NewRoute( + getExampleRoute(namespace), + map[string]string{}, + timeout, + &route.OverrideSpec{}, + ) + Expect(err).ShouldNot(HaveOccurred()) + + _, err = r.CreateOrPatch(ctx, h) + Expect(err).ShouldNot(HaveOccurred()) + rv1 := th.AssertRouteExists(types.NamespacedName{Namespace: namespace, Name: "test-route"}) + Expect(rv1.Annotations["anno"]).To(Equal("a")) + Expect(rv1.Annotations["replace"]).To(Equal("a")) + Expect(rv1.Labels["label"]).To(Equal("a")) + Expect(rv1.Labels["replace"]).To(Equal("a")) + Expect(rv1.Spec.Host).To(Equal("some.host.svc")) + Expect(rv1.Spec.Port.TargetPort.IntVal).To(Equal(int32(80))) + Expect(rv1.Spec.To.Name).To(Equal("my-service")) + Expect(*rv1.Spec.To.Weight).To(Equal(int32(100))) + + }) + + It("merges labels to the route", func() { + r, err := route.NewRoute( + getExampleRoute(namespace), + map[string]string{}, + timeout, + &route.OverrideSpec{ + EmbeddedLabelsAnnotations: &route.EmbeddedLabelsAnnotations{ + Labels: map[string]string{ + "foo": "b", + "replace": "b", + }, + }, + }, + ) + Expect(err).ShouldNot(HaveOccurred()) + + _, err = r.CreateOrPatch(ctx, h) + Expect(err).ShouldNot(HaveOccurred()) + rv1 := th.AssertRouteExists(types.NamespacedName{Namespace: namespace, Name: "test-route"}) + // non overridden label exists + Expect(rv1.Labels["label"]).To(Equal("a")) + // adds new label + Expect(rv1.Labels["foo"]).To(Equal("b")) + // override replaces existing label + Expect(rv1.Labels["replace"]).To(Equal("b")) + }) + + It("merges annotations to the route", func() { + r, err := route.NewRoute( + getExampleRoute(namespace), + map[string]string{}, + timeout, + &route.OverrideSpec{ + EmbeddedLabelsAnnotations: &route.EmbeddedLabelsAnnotations{ + Annotations: map[string]string{ + "foo": "b", + "replace": "b", + }, + }, + }, + ) + Expect(err).ShouldNot(HaveOccurred()) + + _, err = r.CreateOrPatch(ctx, h) + Expect(err).ShouldNot(HaveOccurred()) + rv1 := th.AssertRouteExists(types.NamespacedName{Namespace: namespace, Name: "test-route"}) + // non overridden annotation exists + Expect(rv1.Annotations["anno"]).To(Equal("a")) + // adds new annotation + Expect(rv1.Annotations["foo"]).To(Equal("b")) + // override replaces existing annotation + Expect(rv1.Annotations["replace"]).To(Equal("b")) + }) + + It("overrides spec.host if specified", func() { + r, err := route.NewRoute( + getExampleRoute(namespace), + map[string]string{}, + timeout, + &route.OverrideSpec{ + Spec: &route.Spec{ + Host: "custom.host.domain", + }, + }, + ) + Expect(err).ShouldNot(HaveOccurred()) + + _, err = r.CreateOrPatch(ctx, h) + Expect(err).ShouldNot(HaveOccurred()) + rv1 := th.AssertRouteExists(types.NamespacedName{Namespace: namespace, Name: "test-route"}) + Expect(rv1.Spec.Host).To(Equal("custom.host.domain")) + }) + + It("overrides spec.subdomain if specified", func() { + r, err := route.NewRoute( + getExampleRoute(namespace), + map[string]string{}, + timeout, + &route.OverrideSpec{ + Spec: &route.Spec{ + Subdomain: "subdomain", + }, + }, + ) + Expect(err).ShouldNot(HaveOccurred()) + + _, err = r.CreateOrPatch(ctx, h) + Expect(err).ShouldNot(HaveOccurred()) + rv1 := th.AssertRouteExists(types.NamespacedName{Namespace: namespace, Name: "test-route"}) + Expect(rv1.Spec.Subdomain).To(Equal("subdomain")) + }) + + It("overrides spec.path if specified", func() { + r, err := route.NewRoute( + getExampleRoute(namespace), + map[string]string{}, + timeout, + &route.OverrideSpec{ + Spec: &route.Spec{ + Path: "/some/path", + }, + }, + ) + Expect(err).ShouldNot(HaveOccurred()) + + _, err = r.CreateOrPatch(ctx, h) + Expect(err).ShouldNot(HaveOccurred()) + rv1 := th.AssertRouteExists(types.NamespacedName{Namespace: namespace, Name: "test-route"}) + Expect(rv1.Spec.Path).To(Equal("/some/path")) + }) + + It("overrides spec.to if specified", func() { + r, err := route.NewRoute( + getExampleRoute(namespace), + map[string]string{}, + timeout, + &route.OverrideSpec{ + Spec: &route.Spec{ + To: route.TargetReference{ + Name: "my-custom-service", + Weight: pointer.Int32(10), + }, + }, + }, + ) + Expect(err).ShouldNot(HaveOccurred()) + + _, err = r.CreateOrPatch(ctx, h) + Expect(err).ShouldNot(HaveOccurred()) + rv1 := th.AssertRouteExists(types.NamespacedName{Namespace: namespace, Name: "test-route"}) + Expect(rv1.Spec.To.Kind).To(Equal("Service")) + Expect(rv1.Spec.To.Name).To(Equal("my-custom-service")) + Expect(*rv1.Spec.To.Weight).To(Equal(int32(10))) + }) + + It("overrides spec.alternateBackends if specified", func() { + r, err := route.NewRoute( + getExampleRoute(namespace), + map[string]string{}, + timeout, + &route.OverrideSpec{ + Spec: &route.Spec{ + AlternateBackends: []route.TargetReference{ + { + Kind: "Service", + Name: "my-alternate-service", + Weight: pointer.Int32(200), + }, + }, + }, + }, + ) + Expect(err).ShouldNot(HaveOccurred()) + + _, err = r.CreateOrPatch(ctx, h) + Expect(err).ShouldNot(HaveOccurred()) + rv1 := th.AssertRouteExists(types.NamespacedName{Namespace: namespace, Name: "test-route"}) + Expect(rv1.Spec.AlternateBackends[0].Name).To(Equal("my-alternate-service")) + Expect(*rv1.Spec.AlternateBackends[0].Weight).To(Equal(int32(200))) + }) + + It("overrides spec.port if specified", func() { + r, err := route.NewRoute( + getExampleRoute(namespace), + map[string]string{}, + timeout, + &route.OverrideSpec{ + Spec: &route.Spec{ + Port: &routev1.RoutePort{ + TargetPort: intstr.FromInt(8080), + }, + }, + }, + ) + Expect(err).ShouldNot(HaveOccurred()) + + _, err = r.CreateOrPatch(ctx, h) + Expect(err).ShouldNot(HaveOccurred()) + rv1 := th.AssertRouteExists(types.NamespacedName{Namespace: namespace, Name: "test-route"}) + Expect(rv1.Spec.Port.TargetPort.IntVal).To(Equal(int32(8080))) + }) + + It("overrides spec.tls if specified", func() { + r, err := route.NewRoute( + getExampleRoute(namespace), + map[string]string{}, + timeout, + &route.OverrideSpec{ + Spec: &route.Spec{ + TLS: &routev1.TLSConfig{ + Termination: routev1.TLSTerminationEdge, + Certificate: "cert", + Key: "key", + CACertificate: "cacert", + }, + }, + }, + ) + Expect(err).ShouldNot(HaveOccurred()) + + _, err = r.CreateOrPatch(ctx, h) + Expect(err).ShouldNot(HaveOccurred()) + rv1 := th.AssertRouteExists(types.NamespacedName{Namespace: namespace, Name: "test-route"}) + Expect(rv1.Spec.TLS.Termination).To(Equal(routev1.TLSTerminationEdge)) + Expect(rv1.Spec.TLS.Certificate).To(Equal("cert")) + Expect(rv1.Spec.TLS.Key).To(Equal("key")) + Expect(rv1.Spec.TLS.CACertificate).To(Equal("cacert")) + }) +}) diff --git a/modules/common/test/functional/suite_test.go b/modules/common/test/functional/suite_test.go index 81ab5ccc..6c9f5b1b 100644 --- a/modules/common/test/functional/suite_test.go +++ b/modules/common/test/functional/suite_test.go @@ -18,6 +18,7 @@ package functional import ( "context" + "path/filepath" "testing" "time" @@ -36,6 +37,7 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" + routev1 "github.com/openshift/api/route/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" @@ -82,9 +84,13 @@ var _ = BeforeSuite(func() { By("bootstrapping test environment") testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "..", "test", "openshift_crds", "route", "v1"), + }, ErrorIfCRDPathMissing: true, } var err error + // cfg is defined in this file globally. cfg, err = testEnv.Start() Expect(err).NotTo(HaveOccurred()) @@ -94,6 +100,8 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) err = corev1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = routev1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) //+kubebuilder:scaffold:scheme logger = ctrl.Log.WithName("---Test---") diff --git a/modules/test/go.mod b/modules/test/go.mod index 34a439b9..5d6b2c87 100644 --- a/modules/test/go.mod +++ b/modules/test/go.mod @@ -9,7 +9,7 @@ require ( github.com/onsi/gomega v1.27.8 github.com/openstack-k8s-operators/infra-operator/apis v0.0.0-20230720153501-076b82bb4427 github.com/openstack-k8s-operators/keystone-operator/api v0.0.0-20230612072624-8ebcfc19377a - github.com/openstack-k8s-operators/lib-common/modules/common v0.0.0-20230619102827-49e72f626a11 + github.com/openstack-k8s-operators/lib-common/modules/common v0.0.0-20230707063813-c894bf75835d github.com/openstack-k8s-operators/mariadb-operator/api v0.0.0-20230717141726-1bd909777952 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 golang.org/x/mod v0.10.0 @@ -18,7 +18,11 @@ require ( sigs.k8s.io/controller-runtime v0.14.6 ) -require github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.4.0 // indirect +require ( + github.com/imdario/mergo v0.3.16 // indirect + github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.4.0 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect +) require ( github.com/beorn7/perks v1.0.1 // indirect @@ -36,11 +40,9 @@ require ( github.com/google/gnostic v0.6.9 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/imdario/mergo v0.3.16 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect