From 68e4bb78b2c8eae3f277f4f09c20c42422682467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Ma=C5=82ek?= Date: Fri, 27 Sep 2024 15:06:10 +0200 Subject: [PATCH] feat(konnect): allow sericeless KongRoutes (#101) --- api/configuration/v1alpha1/kongroute_types.go | 14 +- .../v1alpha1/zz_generated.deepcopy.go | 5 + .../configuration.konghq.com_kongroutes.yaml | 70 +++++++- docs/api-reference.md | 4 +- .../kongroute/testcases/common.go | 1 + .../kongroute/testcases/controlplaneref.go | 152 ++++++++++++++++++ 6 files changed, 238 insertions(+), 8 deletions(-) create mode 100644 test/crdsvalidation/kongroute/testcases/controlplaneref.go diff --git a/api/configuration/v1alpha1/kongroute_types.go b/api/configuration/v1alpha1/kongroute_types.go index e980ce46..cae9c309 100644 --- a/api/configuration/v1alpha1/kongroute_types.go +++ b/api/configuration/v1alpha1/kongroute_types.go @@ -36,8 +36,11 @@ import ( // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Programmed",description="The Resource is Programmed on Konnect",type=string,JSONPath=`.status.conditions[?(@.type=='Programmed')].status` // +kubebuilder:validation:XValidation:rule="!has(oldSelf.spec.serviceRef) || has(self.spec.serviceRef)", message="serviceRef is required once set" -// +kubebuilder:validation:XValidation:rule="(!self.status.conditions.exists(c, c.type == 'Programmed' && c.status == 'True')) ? true : oldSelf.spec.serviceRef == self.spec.serviceRef", message="spec.serviceRef is immutable when an entity is already Programmed" // +kubebuilder:validation:XValidation:rule="has(self.spec.protocols) && self.spec.protocols.exists(p, p == 'http') ? (has(self.spec.hosts) || has(self.spec.methods) || has(self.spec.paths) || has(self.spec.paths) || has(self.spec.paths) || has(self.spec.headers) ) : true", message="If protocols has 'http', at least one of 'hosts', 'methods', 'paths' or 'headers' must be set" +// +kubebuilder:validation:XValidation:rule="has(self.spec.controlPlaneRef) && !has(self.spec.serviceRef) || !has(self.spec.controlPlaneRef) && has(self.spec.serviceRef)", message="Only one of controlPlaneRef or serviceRef can be set" +// +kubebuilder:validation:XValidation:rule="!has(self.spec.controlPlaneRef) ? true : !has(self.spec.controlPlaneRef.konnectNamespacedRef) ? true : !has(self.spec.controlPlaneRef.konnectNamespacedRef.__namespace__)", message="spec.controlPlaneRef cannot specify namespace for namespaced resource" +// +kubebuilder:validation:XValidation:rule="!has(self.spec.serviceRef) ? true : (!self.status.conditions.exists(c, c.type == 'Programmed' && c.status == 'True')) ? true : oldSelf.spec.serviceRef == self.spec.serviceRef", message="spec.serviceRef is immutable when an entity is already Programmed" +// +kubebuilder:validation:XValidation:rule="!has(self.spec.controlPlaneRef) ? true :(!self.status.conditions.exists(c, c.type == 'Programmed' && c.status == 'True')) ? true : oldSelf.spec.controlPlaneRef == self.spec.controlPlaneRef", message="spec.controlPlaneRef is immutable when an entity is already Programmed" type KongRoute struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` @@ -50,7 +53,14 @@ type KongRoute struct { // KongRouteSpec defines specification of a Kong Route. type KongRouteSpec struct { - // ServiceRef is a reference to a Service this Route is associated with. + // ControlPlaneRef is a reference to a ControlPlane this KongRoute is associated with. + // Route can either specify a ControlPlaneRef and be 'serviceless' route or + // specify a ServiceRef and be associated with a Service. + // +optional + ControlPlaneRef *ControlPlaneRef `json:"controlPlaneRef,omitempty"` + // ServiceRef is a reference to a Service this KongRoute is associated with. + // Route can either specify a ControlPlaneRef and be 'serviceless' route or + // specify a ServiceRef and be associated with a Service. // +optional ServiceRef *ServiceRef `json:"serviceRef,omitempty"` diff --git a/api/configuration/v1alpha1/zz_generated.deepcopy.go b/api/configuration/v1alpha1/zz_generated.deepcopy.go index c6e5cf8e..d0d73290 100644 --- a/api/configuration/v1alpha1/zz_generated.deepcopy.go +++ b/api/configuration/v1alpha1/zz_generated.deepcopy.go @@ -1627,6 +1627,11 @@ func (in *KongRouteList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KongRouteSpec) DeepCopyInto(out *KongRouteSpec) { *out = *in + if in.ControlPlaneRef != nil { + in, out := &in.ControlPlaneRef, &out.ControlPlaneRef + *out = new(ControlPlaneRef) + (*in).DeepCopyInto(*out) + } if in.ServiceRef != nil { in, out := &in.ServiceRef, &out.ServiceRef *out = new(ServiceRef) diff --git a/config/crd/bases/configuration.konghq.com_kongroutes.yaml b/config/crd/bases/configuration.konghq.com_kongroutes.yaml index 5197a608..7fa28160 100644 --- a/config/crd/bases/configuration.konghq.com_kongroutes.yaml +++ b/config/crd/bases/configuration.konghq.com_kongroutes.yaml @@ -44,6 +44,53 @@ spec: spec: description: KongRouteSpec defines specification of a Kong Route. properties: + controlPlaneRef: + description: |- + ControlPlaneRef is a reference to a ControlPlane this KongRoute is associated with. + Route can either specify a ControlPlaneRef and be 'serviceless' route or + specify a ServiceRef and be associated with a Service. + properties: + konnectID: + description: |- + KonnectID is the schema for the KonnectID type. + This field is required when the Type is konnectID. + type: string + konnectNamespacedRef: + description: |- + KonnectNamespacedRef is a reference to a Konnect Control Plane entity inside the cluster. + It contains the name of the Konnect Control Plane. + This field is required when the Type is konnectNamespacedRef. + properties: + name: + description: Name is the name of the Konnect Control Plane. + type: string + namespace: + description: |- + Namespace is the namespace where the Konnect Control Plane is in. + Currently only cluster scoped resources (KongVault) are allowed to set `konnectNamespacedRef.namespace`. + type: string + required: + - name + type: object + type: + description: |- + Type can be one of: + - konnectID + - konnectNamespacedRef + enum: + - konnectID + - konnectNamespacedRef + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: when type is konnectNamespacedRef, konnectNamespacedRef + must be set + rule: 'self.type == ''konnectNamespacedRef'' ? has(self.konnectNamespacedRef) + : true' + - message: when type is konnectID, konnectID must be set + rule: 'self.type == ''konnectID'' ? has(self.konnectID) : true' destinations: description: A list of IP destinations of incoming connections that match this Route when using stream routing. Each entry is an object @@ -137,8 +184,10 @@ spec: data with chunked transfer encoding. type: boolean serviceRef: - description: ServiceRef is a reference to a Service this Route is - associated with. + description: |- + ServiceRef is a reference to a Service this KongRoute is associated with. + Route can either specify a ControlPlaneRef and be 'serviceless' route or + specify a ServiceRef and be associated with a Service. properties: namespacedRef: description: NamespacedRef is a reference to a KongService. @@ -295,15 +344,26 @@ spec: x-kubernetes-validations: - message: serviceRef is required once set rule: '!has(oldSelf.spec.serviceRef) || has(self.spec.serviceRef)' - - message: spec.serviceRef is immutable when an entity is already Programmed - rule: '(!self.status.conditions.exists(c, c.type == ''Programmed'' && c.status - == ''True'')) ? true : oldSelf.spec.serviceRef == self.spec.serviceRef' - message: If protocols has 'http', at least one of 'hosts', 'methods', 'paths' or 'headers' must be set rule: 'has(self.spec.protocols) && self.spec.protocols.exists(p, p == ''http'') ? (has(self.spec.hosts) || has(self.spec.methods) || has(self.spec.paths) || has(self.spec.paths) || has(self.spec.paths) || has(self.spec.headers) ) : true' + - message: Only one of controlPlaneRef or serviceRef can be set + rule: has(self.spec.controlPlaneRef) && !has(self.spec.serviceRef) || !has(self.spec.controlPlaneRef) + && has(self.spec.serviceRef) + - message: spec.controlPlaneRef cannot specify namespace for namespaced resource + rule: '!has(self.spec.controlPlaneRef) ? true : !has(self.spec.controlPlaneRef.konnectNamespacedRef) + ? true : !has(self.spec.controlPlaneRef.konnectNamespacedRef.__namespace__)' + - message: spec.serviceRef is immutable when an entity is already Programmed + rule: '!has(self.spec.serviceRef) ? true : (!self.status.conditions.exists(c, + c.type == ''Programmed'' && c.status == ''True'')) ? true : oldSelf.spec.serviceRef + == self.spec.serviceRef' + - message: spec.controlPlaneRef is immutable when an entity is already Programmed + rule: '!has(self.spec.controlPlaneRef) ? true :(!self.status.conditions.exists(c, + c.type == ''Programmed'' && c.status == ''True'')) ? true : oldSelf.spec.controlPlaneRef + == self.spec.controlPlaneRef' served: true storage: true subresources: diff --git a/docs/api-reference.md b/docs/api-reference.md index f3ddd023..aaef1247 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -646,6 +646,7 @@ _Appears in:_ - [KongKeySetSpec](#kongkeysetspec) - [KongKeySpec](#kongkeyspec) - [KongPluginBindingSpec](#kongpluginbindingspec) +- [KongRouteSpec](#kongroutespec) - [KongServiceSpec](#kongservicespec) - [KongUpstreamSpec](#kongupstreamspec) - [KongVaultSpec](#kongvaultspec) @@ -1154,7 +1155,8 @@ KongRouteSpec defines specification of a Kong Route. | Field | Description | | --- | --- | -| `serviceRef` _[ServiceRef](#serviceref)_ | ServiceRef is a reference to a Service this Route is associated with. | +| `controlPlaneRef` _[ControlPlaneRef](#controlplaneref)_ | ControlPlaneRef is a reference to a ControlPlane this KongRoute is associated with. Route can either specify a ControlPlaneRef and be 'serviceless' route or specify a ServiceRef and be associated with a Service. | +| `serviceRef` _[ServiceRef](#serviceref)_ | ServiceRef is a reference to a Service this KongRoute is associated with. Route can either specify a ControlPlaneRef and be 'serviceless' route or specify a ServiceRef and be associated with a Service. | | `destinations` _Destinations array_ | A list of IP destinations of incoming connections that match this Route when using stream routing. Each entry is an object with fields "ip" (optionally in CIDR range notation) and/or "port". | | `headers` _object (keys:string, values:string)_ | One or more lists of values indexed by header name that will cause this Route to match if present in the request. The `Host` header cannot be used with this attribute: hosts should be specified using the `hosts` attribute. When `headers` contains only one value and that value starts with the special prefix `~*`, the value is interpreted as a regular expression. | | `hosts` _string array_ | A list of domain names that match this Route. Note that the hosts value is case sensitive. | diff --git a/test/crdsvalidation/kongroute/testcases/common.go b/test/crdsvalidation/kongroute/testcases/common.go index 20f3b4a8..42a3b95a 100644 --- a/test/crdsvalidation/kongroute/testcases/common.go +++ b/test/crdsvalidation/kongroute/testcases/common.go @@ -28,6 +28,7 @@ var TestCases = []testCasesGroup{} func init() { TestCases = append(TestCases, + cpRef, serviceRef, protocols, ) diff --git a/test/crdsvalidation/kongroute/testcases/controlplaneref.go b/test/crdsvalidation/kongroute/testcases/controlplaneref.go new file mode 100644 index 00000000..7f14466b --- /dev/null +++ b/test/crdsvalidation/kongroute/testcases/controlplaneref.go @@ -0,0 +1,152 @@ +package testcases + +import ( + "github.com/samber/lo" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" +) + +var cpRef = testCasesGroup{ + Name: "cp ref validation", + TestCases: []testCase{ + { + Name: "cannot specify with service ref", + KongRoute: configurationv1alpha1.KongRoute{ + ObjectMeta: commonObjectMeta, + Spec: configurationv1alpha1.KongRouteSpec{ + ControlPlaneRef: &configurationv1alpha1.ControlPlaneRef{ + Type: configurationv1alpha1.ControlPlaneRefKonnectNamespacedRef, + KonnectNamespacedRef: &configurationv1alpha1.KonnectNamespacedRef{ + Name: "test-konnect-control-plane", + }, + }, + ServiceRef: &configurationv1alpha1.ServiceRef{ + Type: configurationv1alpha1.ServiceRefNamespacedRef, + NamespacedRef: &configurationv1alpha1.NamespacedServiceRef{ + Name: "test-konnect-service", + }, + }, + KongRouteAPISpec: configurationv1alpha1.KongRouteAPISpec{}, + }, + }, + ExpectedErrorMessage: lo.ToPtr("Only one of controlPlaneRef or serviceRef can be set"), + }, + { + Name: "konnectNamespacedRef reference is valid", + KongRoute: configurationv1alpha1.KongRoute{ + ObjectMeta: commonObjectMeta, + Spec: configurationv1alpha1.KongRouteSpec{ + ControlPlaneRef: &configurationv1alpha1.ControlPlaneRef{ + Type: configurationv1alpha1.ControlPlaneRefKonnectNamespacedRef, + KonnectNamespacedRef: &configurationv1alpha1.KonnectNamespacedRef{ + Name: "test-konnect-control-plane", + }, + }, + KongRouteAPISpec: configurationv1alpha1.KongRouteAPISpec{}, + }, + }, + }, + { + Name: "not providing konnectNamespacedRef when type is konnectNamespacedRef yields an error", + KongRoute: configurationv1alpha1.KongRoute{ + ObjectMeta: commonObjectMeta, + Spec: configurationv1alpha1.KongRouteSpec{ + ControlPlaneRef: &configurationv1alpha1.ControlPlaneRef{ + Type: configurationv1alpha1.ControlPlaneRefKonnectNamespacedRef, + }, + KongRouteAPISpec: configurationv1alpha1.KongRouteAPISpec{}, + }, + }, + ExpectedErrorMessage: lo.ToPtr("when type is konnectNamespacedRef, konnectNamespacedRef must be set"), + }, + { + Name: "not providing konnectID when type is konnectID yields an error", + KongRoute: configurationv1alpha1.KongRoute{ + ObjectMeta: commonObjectMeta, + Spec: configurationv1alpha1.KongRouteSpec{ + ControlPlaneRef: &configurationv1alpha1.ControlPlaneRef{ + Type: configurationv1alpha1.ControlPlaneRefKonnectID, + }, + KongRouteAPISpec: configurationv1alpha1.KongRouteAPISpec{}, + }, + }, + ExpectedErrorMessage: lo.ToPtr("when type is konnectID, konnectID must be set"), + }, + { + Name: "providing namespace in konnectNamespacedRef yields an error", + KongRoute: configurationv1alpha1.KongRoute{ + ObjectMeta: commonObjectMeta, + Spec: configurationv1alpha1.KongRouteSpec{ + ControlPlaneRef: &configurationv1alpha1.ControlPlaneRef{ + Type: configurationv1alpha1.ControlPlaneRefKonnectNamespacedRef, + KonnectNamespacedRef: &configurationv1alpha1.KonnectNamespacedRef{ + Name: "test-konnect-control-plane", + Namespace: "another-namespace", + }, + }, + KongRouteAPISpec: configurationv1alpha1.KongRouteAPISpec{}, + }, + }, + ExpectedErrorMessage: lo.ToPtr("spec.controlPlaneRef cannot specify namespace for namespaced resource"), + }, + { + Name: "konnectNamespacedRef reference name cannot be changed when an entity is Programmed", + KongRoute: configurationv1alpha1.KongRoute{ + ObjectMeta: commonObjectMeta, + Spec: configurationv1alpha1.KongRouteSpec{ + ControlPlaneRef: &configurationv1alpha1.ControlPlaneRef{ + Type: configurationv1alpha1.ControlPlaneRefKonnectNamespacedRef, + KonnectNamespacedRef: &configurationv1alpha1.KonnectNamespacedRef{ + Name: "test-konnect-control-plane", + }, + }, + KongRouteAPISpec: configurationv1alpha1.KongRouteAPISpec{}, + }, + }, + KongRouteStatus: &configurationv1alpha1.KongRouteStatus{ + Conditions: []metav1.Condition{ + { + Type: "Programmed", + Status: metav1.ConditionTrue, + Reason: "Programmed", + LastTransitionTime: metav1.Now(), + }, + }, + }, + Update: func(ks *configurationv1alpha1.KongRoute) { + ks.Spec.ControlPlaneRef.KonnectNamespacedRef.Name = "new-konnect-control-plane" + }, + ExpectedUpdateErrorMessage: lo.ToPtr("spec.controlPlaneRef is immutable when an entity is already Programmed"), + }, + { + Name: "konnectNamespacedRef reference type cannot be changed when an entity is Programmed", + KongRoute: configurationv1alpha1.KongRoute{ + ObjectMeta: commonObjectMeta, + Spec: configurationv1alpha1.KongRouteSpec{ + ControlPlaneRef: &configurationv1alpha1.ControlPlaneRef{ + Type: configurationv1alpha1.ControlPlaneRefKonnectNamespacedRef, + KonnectNamespacedRef: &configurationv1alpha1.KonnectNamespacedRef{ + Name: "test-konnect-control-plane", + }, + }, + KongRouteAPISpec: configurationv1alpha1.KongRouteAPISpec{}, + }, + }, + KongRouteStatus: &configurationv1alpha1.KongRouteStatus{ + Conditions: []metav1.Condition{ + { + Type: "Programmed", + Status: metav1.ConditionTrue, + Reason: "Programmed", + LastTransitionTime: metav1.Now(), + }, + }, + }, + Update: func(ks *configurationv1alpha1.KongRoute) { + ks.Spec.ControlPlaneRef.Type = configurationv1alpha1.ControlPlaneRefKonnectID + }, + ExpectedUpdateErrorMessage: lo.ToPtr("spec.controlPlaneRef is immutable when an entity is already Programmed"), + }, + }, +}