Skip to content

Commit

Permalink
Merge pull request #89 from mlavacca/kong-implementation-specific-path
Browse files Browse the repository at this point in the history
feat: kong implementationSpecific pathType
  • Loading branch information
k8s-ci-robot authored Nov 16, 2023
2 parents 3f40382 + 94e1d82 commit 0ac84b9
Show file tree
Hide file tree
Showing 13 changed files with 337 additions and 40 deletions.
9 changes: 8 additions & 1 deletion PROVIDER.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ func (r *resourceReader) ReadResourcesFromFiles(ctx context.Context, customResou
```
3. Create a struct named `converter` which implements the `ResourceConverter` interface in a file named `converter.go`.
The implemented `ToGatewayAPI` function should simply call every registered `featureParser` function, one by one.
Take a look at `ingressnginx/converter.go` for example.
Take a look at `ingressnginx/converter.go` for an example.
The `ImplementationSpecificOptions` struct contains the handlers to customize native ingress implementation-specific fields.
Take a look at `kong/converter.go` for an example.

```go
package examplegateway

Expand All @@ -74,6 +77,7 @@ type converter struct {
conf *i2gw.ProviderConf

featureParsers []i2gw.FeatureParser
implementationSpecificOptions i2gw.ProviderImplementationSpecificOptions
}

// newConverter returns an ingress-nginx converter instance.
Expand All @@ -83,6 +87,9 @@ func newConverter(conf *i2gw.ProviderConf) *converter {
featureParsers: []i2gw.FeatureParser{
// The list of feature parsers comes here.
},
implementationSpecificOptions: i2gw.ProviderImplementationSpecificOptions{
// The list of the implementationSpecific ingress fields options comes here.
},
}
}
```
Expand Down
11 changes: 11 additions & 0 deletions pkg/i2gw/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,17 @@ type ResourceConverter interface {
ToGatewayAPI(resources InputResources) (GatewayResources, field.ErrorList)
}

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

// ProviderImplementationSpecificOptions contains customized implementation-specific fields and functions.
// These will be used by the common package to customize the provider-specific behavior for all the
// implementation-specific fields of the ingress API.
type ProviderImplementationSpecificOptions struct {
ToImplementationSpecificHTTPPathTypeMatch ImplementationSpecificHTTPPathTypeMatchConverter
}

// InputResources contains all Ingress objects, and Provider specific
// custom resources.
type InputResources struct {
Expand Down
34 changes: 22 additions & 12 deletions pkg/i2gw/providers/common/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import (

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

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

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

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

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

Expand Down Expand Up @@ -305,7 +305,7 @@ func (rg *ingressRuleGroup) toHTTPRoute() (gatewayv1beta1.HTTPRoute, field.Error
for _, paths := range pathsByMatchGroup {
path := paths[0]
fieldPath := field.NewPath("spec", "rules").Index(path.ruleIdx).Child(path.ruleType).Child("paths").Index(path.pathIdx)
match, err := toHTTPRouteMatch(path, fieldPath)
match, err := toHTTPRouteMatch(path.path, fieldPath, options.ToImplementationSpecificHTTPPathTypeMatch)
if err != nil {
errors = append(errors, err)
continue
Expand Down Expand Up @@ -348,20 +348,30 @@ func getPathMatchKey(ip ingressPath) pathMatchKey {
return pathMatchKey(fmt.Sprintf("%s/%s", pathType, ip.path.Path))
}

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

match := &gatewayv1beta1.HTTPRouteMatch{Path: &gatewayv1beta1.HTTPPathMatch{Value: &ip.path.Path}}
//exhaustive:ignore -explicit-exhaustive-switch
// networkingv1.PathTypeImplementationSpecific is not supported here, hence it goes into default case.
switch *ip.path.PathType {
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
// In case the path type is ImplementationSpecific, the path value and type
// will be set by the provider-specific customization function. If such function
// is not given by the provider, an error is returned.
case networkingv1.PathTypeImplementationSpecific:
if toImplementationSpecificPathMatch != nil {
toImplementationSpecificPathMatch(match.Path)
} else {
return nil, field.Invalid(path.Child("pathType"), routePath.PathType, "implementationSpecific path type is not supported in generic translation, and your provider does not provide custom support to translate it")
}
default:
return nil, field.Invalid(path.Child("pathType"), ip.path.PathType, fmt.Sprintf("unsupported path match type: %s", *ip.path.PathType))
// default should never hit, as all the possible cases are already checked
// via proper switch cases.
return nil, field.Invalid(path.Child("pathType"), match.Path.Type, fmt.Sprintf("unsupported path match type: %s", *match.Path.Type))

}

return match, nil
Expand Down
34 changes: 15 additions & 19 deletions pkg/i2gw/providers/common/converter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func Test_ingresses2GatewaysAndHttpRoutes(t *testing.T) {
Name: "example-com-http",
Port: 80,
Protocol: gatewayv1beta1.HTTPProtocolType,
Hostname: ptrTo(gatewayv1beta1.Hostname("example.com")),
Hostname: PtrTo(gatewayv1beta1.Hostname("example.com")),
}},
},
},
Expand All @@ -104,14 +104,14 @@ func Test_ingresses2GatewaysAndHttpRoutes(t *testing.T) {
Matches: []gatewayv1beta1.HTTPRouteMatch{{
Path: &gatewayv1beta1.HTTPPathMatch{
Type: &gPathPrefix,
Value: ptrTo("/foo"),
Value: PtrTo("/foo"),
},
}},
BackendRefs: []gatewayv1beta1.HTTPBackendRef{{
BackendRef: gatewayv1beta1.BackendRef{
BackendObjectReference: gatewayv1beta1.BackendObjectReference{
Name: "example",
Port: ptrTo(gatewayv1beta1.PortNumber(3000)),
Port: PtrTo(gatewayv1beta1.PortNumber(3000)),
},
},
}},
Expand Down Expand Up @@ -162,12 +162,12 @@ func Test_ingresses2GatewaysAndHttpRoutes(t *testing.T) {
Name: "example-com-http",
Port: 80,
Protocol: gatewayv1beta1.HTTPProtocolType,
Hostname: ptrTo(gatewayv1beta1.Hostname("example.com")),
Hostname: PtrTo(gatewayv1beta1.Hostname("example.com")),
}, {
Name: "example-com-https",
Port: 443,
Protocol: gatewayv1beta1.HTTPSProtocolType,
Hostname: ptrTo(gatewayv1beta1.Hostname("example.com")),
Hostname: PtrTo(gatewayv1beta1.Hostname("example.com")),
TLS: &gatewayv1beta1.GatewayTLSConfig{
CertificateRefs: []gatewayv1beta1.SecretObjectReference{{
Name: "example-cert",
Expand All @@ -191,14 +191,14 @@ func Test_ingresses2GatewaysAndHttpRoutes(t *testing.T) {
Matches: []gatewayv1beta1.HTTPRouteMatch{{
Path: &gatewayv1beta1.HTTPPathMatch{
Type: &gPathPrefix,
Value: ptrTo("/foo"),
Value: PtrTo("/foo"),
},
}},
BackendRefs: []gatewayv1beta1.HTTPBackendRef{{
BackendRef: gatewayv1beta1.BackendRef{
BackendObjectReference: gatewayv1beta1.BackendObjectReference{
Name: "example",
Port: ptrTo(gatewayv1beta1.PortNumber(3000)),
Port: PtrTo(gatewayv1beta1.PortNumber(3000)),
},
},
}},
Expand All @@ -214,7 +214,7 @@ func Test_ingresses2GatewaysAndHttpRoutes(t *testing.T) {
ingresses: []networkingv1.Ingress{{
ObjectMeta: metav1.ObjectMeta{Name: "net", Namespace: "different"},
Spec: networkingv1.IngressSpec{
IngressClassName: ptrTo("example-proxy"),
IngressClassName: PtrTo("example-proxy"),
Rules: []networkingv1.IngressRule{{
Host: "example.net",
IngressRuleValue: networkingv1.IngressRuleValue{
Expand All @@ -226,7 +226,7 @@ func Test_ingresses2GatewaysAndHttpRoutes(t *testing.T) {
Resource: &corev1.TypedLocalObjectReference{
Name: "custom",
Kind: "StorageBucket",
APIGroup: ptrTo("vendor.example.com"),
APIGroup: PtrTo("vendor.example.com"),
},
},
}},
Expand All @@ -253,7 +253,7 @@ func Test_ingresses2GatewaysAndHttpRoutes(t *testing.T) {
Name: "example-net-http",
Port: 80,
Protocol: gatewayv1beta1.HTTPProtocolType,
Hostname: ptrTo(gatewayv1beta1.Hostname("example.net")),
Hostname: PtrTo(gatewayv1beta1.Hostname("example.net")),
}},
},
},
Expand All @@ -272,15 +272,15 @@ func Test_ingresses2GatewaysAndHttpRoutes(t *testing.T) {
Matches: []gatewayv1beta1.HTTPRouteMatch{{
Path: &gatewayv1beta1.HTTPPathMatch{
Type: &gExact,
Value: ptrTo("/bar"),
Value: PtrTo("/bar"),
},
}},
BackendRefs: []gatewayv1beta1.HTTPBackendRef{{
BackendRef: gatewayv1beta1.BackendRef{
BackendObjectReference: gatewayv1beta1.BackendObjectReference{
Name: "custom",
Group: ptrTo(gatewayv1beta1.Group("vendor.example.com")),
Kind: ptrTo(gatewayv1beta1.Kind("StorageBucket")),
Group: PtrTo(gatewayv1beta1.Group("vendor.example.com")),
Kind: PtrTo(gatewayv1beta1.Kind("StorageBucket")),
},
},
}},
Expand All @@ -300,7 +300,7 @@ func Test_ingresses2GatewaysAndHttpRoutes(t *testing.T) {
BackendRef: gatewayv1beta1.BackendRef{
BackendObjectReference: gatewayv1beta1.BackendObjectReference{
Name: "default",
Port: ptrTo(gatewayv1beta1.PortNumber(8080)),
Port: PtrTo(gatewayv1beta1.PortNumber(8080)),
},
}},
}},
Expand All @@ -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)
gatewayResources, errs := ToGateway(tc.ingresses, i2gw.ProviderImplementationSpecificOptions{})

if len(gatewayResources.HTTPRoutes) != len(tc.expectedGatewayResources.HTTPRoutes) {
t.Errorf("Expected %d HTTPRoutes, got %d: %+v",
Expand Down Expand Up @@ -358,7 +358,3 @@ func Test_ingresses2GatewaysAndHttpRoutes(t *testing.T) {
})
}
}

func ptrTo[T any](a T) *T {
return &a
}
4 changes: 4 additions & 0 deletions pkg/i2gw/providers/common/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,7 @@ func ToBackendRef(ib networkingv1.IngressBackend, path *field.Path) (*gatewayv1b
},
}, nil
}

func PtrTo[T any](a T) *T {
return &a
}
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)
gatewayResources, errs := common.ToGateway(resources.Ingresses, i2gw.ProviderImplementationSpecificOptions{})
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-nginx"),
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: "implementationSpecific path type is not supported in generic translation, and your provider does not provide custom support to translate it",
},
},
},
}

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

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

The project supports translating Kong-specific annotations.

Current supported annotations:

Expand All @@ -9,11 +11,19 @@ Current supported annotations:
be specified by separating values with commas. Example: `konghq.com/methods: "POST,GET"`.
- `konghq.com/headers.*`: If specified, the values of this annotation are used to
perform header matching on the associated ingress rules. The header name is specified
in the annotation key after `.`, and the annotations value can contain multiple
in the annotation key after `.`, and the annotation value can contain multiple
header values separated by commas. All the header values for a specific header
name are intended to be ORed. Example: `konghq.com/headers.x-routing: "alpha,bravo"`.
- `konghq.com/plugins`: If specified, the values of this annotation are used to
configure plugins on the associated ingress rules. Multiple plugins can be specified
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.
Loading

0 comments on commit 0ac84b9

Please sign in to comment.