Skip to content

Commit

Permalink
implementation-specific options
Browse files Browse the repository at this point in the history
A new options struct has been introduced. Such a struct can be
customized by providers to implement provider-specific API features.

Signed-off-by: Mattia Lavacca <[email protected]>
  • Loading branch information
mlavacca committed Nov 11, 2023
1 parent 14f252f commit ed3aee3
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 58 deletions.
10 changes: 10 additions & 0 deletions pkg/i2gw/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ type ResourceConverter interface {
ToGatewayAPI(resources InputResources) (GatewayResources, field.ErrorList)
}

// HTTPPathMatchOption is an option to customize the ingress implementationSpecific
// match type conversion.
type HTTPPathMatchOption func(*gatewayv1beta1.HTTPPathMatch)

// ImplementationSpecificOptions contains all the pointers to implementation-specific
// customization functions.
type ImplementationSpecificOptions struct {
HTTPPathmatch HTTPPathMatchOption
}

// InputResources contains all Ingress objects, and Provider specific
// custom resources.
type InputResources struct {
Expand Down
32 changes: 13 additions & 19 deletions pkg/i2gw/providers/common/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,9 @@ import (
gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
)

// ToHTTPRouteMatchOption is an option to give the providers the possibility to customize
// HTTPRouteMatch support.
type ToHTTPRouteMatchOption func(routePath networkingv1.HTTPIngressPath, path *field.Path) (*gatewayv1beta1.HTTPRouteMatch, *field.Error)

// ToGateway converts the received ingresses to i2gw.GatewayResources,
// without taking into consideration any provider specific logic.
func ToGateway(ingresses []networkingv1.Ingress, toHTTPRouteMatchCustom ToHTTPRouteMatchOption) (i2gw.GatewayResources, field.ErrorList) {
func ToGateway(ingresses []networkingv1.Ingress, options i2gw.ImplementationSpecificOptions) (i2gw.GatewayResources, field.ErrorList) {
aggregator := ingressAggregator{ruleGroups: map[ruleGroupKey]*ingressRuleGroup{}}

var errs field.ErrorList
Expand All @@ -47,7 +43,7 @@ func ToGateway(ingresses []networkingv1.Ingress, toHTTPRouteMatchCustom ToHTTPRo
return i2gw.GatewayResources{}, errs
}

routes, gateways, errs := aggregator.toHTTPRoutesAndGateways(toHTTPRouteMatchCustom)
routes, gateways, errs := aggregator.toHTTPRoutesAndGateways(options)
if len(errs) > 0 {
return i2gw.GatewayResources{}, errs
}
Expand Down Expand Up @@ -161,7 +157,7 @@ func (a *ingressAggregator) addIngressRule(namespace, name, ingressClass string,
rg.rules = append(rg.rules, ingressRule{rule: rule})
}

func (a *ingressAggregator) toHTTPRoutesAndGateways(toHTTPRouteMatchCustom ToHTTPRouteMatchOption) ([]gatewayv1beta1.HTTPRoute, []gatewayv1beta1.Gateway, field.ErrorList) {
func (a *ingressAggregator) toHTTPRoutesAndGateways(options i2gw.ImplementationSpecificOptions) ([]gatewayv1beta1.HTTPRoute, []gatewayv1beta1.Gateway, field.ErrorList) {
var httpRoutes []gatewayv1beta1.HTTPRoute
var errors field.ErrorList
listenersByNamespacedGateway := map[string][]gatewayv1beta1.Listener{}
Expand All @@ -182,7 +178,7 @@ func (a *ingressAggregator) toHTTPRoutesAndGateways(toHTTPRouteMatchCustom ToHTT
}
gwKey := fmt.Sprintf("%s/%s", rg.namespace, rg.ingressClass)
listenersByNamespacedGateway[gwKey] = append(listenersByNamespacedGateway[gwKey], listener)
httpRoute, errs := rg.toHTTPRoute(toHTTPRouteMatchCustom)
httpRoute, errs := rg.toHTTPRoute(options)
httpRoutes = append(httpRoutes, httpRoute)
errors = append(errors, errs...)
}
Expand Down Expand Up @@ -273,7 +269,7 @@ func (a *ingressAggregator) toHTTPRoutesAndGateways(toHTTPRouteMatchCustom ToHTT
return httpRoutes, gateways, errors
}

func (rg *ingressRuleGroup) toHTTPRoute(toHTTPRouteMatchCustom ToHTTPRouteMatchOption) (gatewayv1beta1.HTTPRoute, field.ErrorList) {
func (rg *ingressRuleGroup) toHTTPRoute(options i2gw.ImplementationSpecificOptions) (gatewayv1beta1.HTTPRoute, field.ErrorList) {
pathsByMatchGroup := map[pathMatchKey][]ingressPath{}
var errors field.ErrorList

Expand Down Expand Up @@ -311,11 +307,7 @@ func (rg *ingressRuleGroup) toHTTPRoute(toHTTPRouteMatchCustom ToHTTPRouteMatchO
fieldPath := field.NewPath("spec", "rules").Index(path.ruleIdx).Child(path.ruleType).Child("paths").Index(path.pathIdx)
var match *gatewayv1beta1.HTTPRouteMatch
var err *field.Error
if toHTTPRouteMatchCustom != nil {
match, err = toHTTPRouteMatchCustom(path.path, fieldPath)
} else {
match, err = toHTTPRouteMatch(path.path, fieldPath)
}
match, err = toHTTPRouteMatch(path.path, fieldPath, options.HTTPPathmatch)
if err != nil {
errors = append(errors, err)
continue
Expand Down Expand Up @@ -358,20 +350,22 @@ func getPathMatchKey(ip ingressPath) pathMatchKey {
return pathMatchKey(fmt.Sprintf("%s/%s", pathType, ip.path.Path))
}

func toHTTPRouteMatch(routePath networkingv1.HTTPIngressPath, path *field.Path) (*gatewayv1beta1.HTTPRouteMatch, *field.Error) {
func toHTTPRouteMatch(routePath networkingv1.HTTPIngressPath, path *field.Path, implementationSpecificPath i2gw.HTTPPathMatchOption) (*gatewayv1beta1.HTTPRouteMatch, *field.Error) {
pmPrefix := gatewayv1beta1.PathMatchPathPrefix
pmExact := gatewayv1beta1.PathMatchExact

match := &gatewayv1beta1.HTTPRouteMatch{Path: &gatewayv1beta1.HTTPPathMatch{Value: &routePath.Path}}
//exhaustive:ignore -explicit-exhaustive-switch
// networkingv1.PathTypeImplementationSpecific is not supported here, hence it goes into default case.
switch *routePath.PathType {
case networkingv1.PathTypePrefix:
match.Path.Type = &pmPrefix
case networkingv1.PathTypeExact:
match.Path.Type = &pmExact
default:
return nil, field.Invalid(path.Child("pathType"), routePath.PathType, fmt.Sprintf("unsupported path match type: %s", *routePath.PathType))
case networkingv1.PathTypeImplementationSpecific:
if implementationSpecificPath != nil {
implementationSpecificPath(match.Path)
} else {
return nil, field.Invalid(path.Child("pathType"), routePath.PathType, fmt.Sprintf("unsupported path match type: %s", *routePath.PathType))
}
}

return match, nil
Expand Down
2 changes: 1 addition & 1 deletion pkg/i2gw/providers/common/converter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ func Test_ingresses2GatewaysAndHttpRoutes(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {

gatewayResources, errs := ToGateway(tc.ingresses, nil)
gatewayResources, errs := ToGateway(tc.ingresses, i2gw.ImplementationSpecificOptions{})

if len(gatewayResources.HTTPRoutes) != len(tc.expectedGatewayResources.HTTPRoutes) {
t.Errorf("Expected %d HTTPRoutes, got %d: %+v",
Expand Down
2 changes: 1 addition & 1 deletion pkg/i2gw/providers/ingressnginx/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (c *converter) ToGatewayAPI(resources i2gw.InputResources) (i2gw.GatewayRes

// Convert plain ingress resources to gateway resources, ignoring all
// provider-specific features.
gatewayResources, errs := common.ToGateway(resources.Ingresses, nil)
gatewayResources, errs := common.ToGateway(resources.Ingresses, i2gw.ImplementationSpecificOptions{})
if len(errs) > 0 {
return i2gw.GatewayResources{}, errs
}
Expand Down
44 changes: 44 additions & 0 deletions pkg/i2gw/providers/ingressnginx/converter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/utils/pointer"
gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
)

func Test_ToGateway(t *testing.T) {
iPrefix := networkingv1.PathTypePrefix
//iExact := networkingv1.PathTypeExact
gPathPrefix := gatewayv1beta1.PathMatchPathPrefix
isPathType := networkingv1.PathTypeImplementationSpecific
//gExact := gatewayv1beta1.PathMatchExact

testCases := []struct {
Expand Down Expand Up @@ -164,6 +166,48 @@ func Test_ToGateway(t *testing.T) {
},
expectedErrors: field.ErrorList{},
},
{
name: "ImplementationSpecific HTTPRouteMatching",
ingresses: []networkingv1.Ingress{
{
ObjectMeta: metav1.ObjectMeta{
Name: "implementation-specific-regex",
Namespace: "default",
},
Spec: networkingv1.IngressSpec{
IngressClassName: ptrTo("ingress-kong"),
Rules: []networkingv1.IngressRule{{
Host: "test.mydomain.com",
IngressRuleValue: networkingv1.IngressRuleValue{
HTTP: &networkingv1.HTTPIngressRuleValue{
Paths: []networkingv1.HTTPIngressPath{{
Path: "/~/echo/**/test",
PathType: &isPathType,
Backend: networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
Name: "test",
Port: networkingv1.ServiceBackendPort{
Number: 80,
},
},
},
}},
},
},
}},
},
},
},
expectedGatewayResources: i2gw.GatewayResources{},
expectedErrors: field.ErrorList{
{
Type: field.ErrorTypeInvalid,
Field: "spec.rules[0].http.paths[0].pathType",
BadValue: pointer.String("ImplementationSpecific"),
Detail: "unsupported path match type: ImplementationSpecific",
},
},
},
}

for _, tc := range testCases {
Expand Down
12 changes: 11 additions & 1 deletion pkg/i2gw/providers/kong/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Ingress Kong Provider

The project supports translating kong specific annotations.
## Annotations supported

The project supports translating Kong-specific annotations.

Current supported annotations:

Expand All @@ -17,3 +19,11 @@ Current supported annotations:
by separating values with commas. Example: `konghq.com/plugins: "plugin1,plugin2"`.

If you are reliant on any annotations not listed above, please open an issue.

## Implementation-specific features

The following implementation-specific features are supported:

- The ingress `ImplementationSpecific` match type is properly converted to
- `RegularExpression` HTTPRoute match type when the path has the prefix `/~`.
- `PathPrefix` HTTPRoute match type when there is no prefix `/~`.
35 changes: 3 additions & 32 deletions pkg/i2gw/providers/kong/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,9 @@ limitations under the License.
package kong

import (
"fmt"
"strings"

"github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw"
"github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/common"
networkingv1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
)

// converter implements the ToGatewayAPI function of i2gw.ResourceConverter interface.
Expand Down Expand Up @@ -52,7 +47,9 @@ func (c *converter) ToGatewayAPI(resources i2gw.InputResources) (i2gw.GatewayRes

// Convert plain ingress resources to gateway resources, ignoring all
// provider-specific features.
gatewayResources, errs := common.ToGateway(resources.Ingresses, toHTTPRouteMatchOption)
gatewayResources, errs := common.ToGateway(resources.Ingresses, i2gw.ImplementationSpecificOptions{
HTTPPathmatch: httpPathMatch,
})
if len(errs) > 0 {
return i2gw.GatewayResources{}, errs
}
Expand All @@ -66,29 +63,3 @@ func (c *converter) ToGatewayAPI(resources i2gw.InputResources) (i2gw.GatewayRes

return gatewayResources, errs
}

func toHTTPRouteMatchOption(routePath networkingv1.HTTPIngressPath, path *field.Path) (*gatewayv1beta1.HTTPRouteMatch, *field.Error) {
pmPrefix := gatewayv1beta1.PathMatchPathPrefix
pmExact := gatewayv1beta1.PathMatchExact
pmRegex := gatewayv1beta1.PathMatchRegularExpression

match := &gatewayv1beta1.HTTPRouteMatch{Path: &gatewayv1beta1.HTTPPathMatch{Value: &routePath.Path}}
switch *routePath.PathType {
case networkingv1.PathTypePrefix:
match.Path.Type = &pmPrefix
case networkingv1.PathTypeExact:
match.Path.Type = &pmExact
case networkingv1.PathTypeImplementationSpecific:
if strings.HasPrefix(routePath.Path, "/~") {
match.Path.Type = &pmRegex
match.Path.Value = common.PtrTo(strings.TrimPrefix(*match.Path.Value, "/~"))
} else {
match.Path.Type = &pmPrefix
}

default:
return nil, field.Invalid(path.Child("pathType"), routePath.PathType, fmt.Sprintf("unsupported path match type: %s", *routePath.PathType))
}

return match, nil
}
4 changes: 2 additions & 2 deletions pkg/i2gw/providers/kong/converter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,8 +372,8 @@ func Test_ToGateway(t *testing.T) {
},
},
HTTPRoutes: map[types.NamespacedName]gatewayv1beta1.HTTPRoute{
{Namespace: "default", Name: "test-mydomain-com"}: {
ObjectMeta: metav1.ObjectMeta{Name: "test-mydomain-com", Namespace: "default"},
{Namespace: "default", Name: "implementation-specific-regex-test-mydomain-com"}: {
ObjectMeta: metav1.ObjectMeta{Name: "implementation-specific-regex-test-mydomain-com", Namespace: "default"},
Spec: gatewayv1beta1.HTTPRouteSpec{
CommonRouteSpec: gatewayv1beta1.CommonRouteSpec{
ParentRefs: []gatewayv1beta1.ParentReference{{
Expand Down
4 changes: 3 additions & 1 deletion pkg/i2gw/providers/kong/header_matching_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,9 @@ func TestHeaderMatchingFeature(t *testing.T) {

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
gatewayResources, errs := common.ToGateway(tc.inputResources.Ingresses, toHTTPRouteMatchOption)
gatewayResources, errs := common.ToGateway(tc.inputResources.Ingresses, i2gw.ImplementationSpecificOptions{
HTTPPathmatch: httpPathMatch,
})
if len(errs) != 0 {
t.Errorf("Expected no errors, got %d: %+v", len(errs), errs)
}
Expand Down
35 changes: 35 additions & 0 deletions pkg/i2gw/providers/kong/implementation_specific.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
Copyright 2023 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 kong

import (
"strings"

"github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/common"
gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
)

func httpPathMatch(path *gatewayv1beta1.HTTPPathMatch) {
pmPrefix := gatewayv1beta1.PathMatchPathPrefix
pmRegex := gatewayv1beta1.PathMatchRegularExpression
if strings.HasPrefix(*path.Value, "/~") {
path.Type = &pmRegex
path.Value = common.PtrTo(strings.TrimPrefix(*path.Value, "/~"))
} else {
path.Type = &pmPrefix
}
}
4 changes: 3 additions & 1 deletion pkg/i2gw/providers/kong/method_matching_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,9 @@ func TestMethodMatchingFeature(t *testing.T) {

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
gatewayResources, errs := common.ToGateway(tc.inputResources.Ingresses, toHTTPRouteMatchOption)
gatewayResources, errs := common.ToGateway(tc.inputResources.Ingresses, i2gw.ImplementationSpecificOptions{
HTTPPathmatch: httpPathMatch,
})
if len(errs) != 0 {
t.Errorf("Expected no errors, got %d: %+v", len(errs), errs)
}
Expand Down

0 comments on commit ed3aee3

Please sign in to comment.