diff --git a/pkg/i2gw/providers/ingressnginx/README.md b/pkg/i2gw/providers/ingressnginx/README.md index 478d29b0..2b301ff8 100644 --- a/pkg/i2gw/providers/ingressnginx/README.md +++ b/pkg/i2gw/providers/ingressnginx/README.md @@ -6,6 +6,10 @@ The project supports translating ingress-nginx specific annotations. To specify the name of the Ingress class to select, use `--ingress-nginx-ingress-class=ingress-nginx` (default to 'nginx'). +**ImplementationSpecific Path Type** + +The ingress-nginx provider supports the `ImplementationSpecific` path type. When this path type is used, paths are converted to `PathMatchRegularExpression` in the Gateway API HTTPRoute, as ingress-nginx treats ImplementationSpecific paths as regular expressions. + Current supported annotations: - `nginx.ingress.kubernetes.io/canary`: If set to true will enable weighting backends. diff --git a/pkg/i2gw/providers/ingressnginx/converter.go b/pkg/i2gw/providers/ingressnginx/converter.go index 911a4b73..cfa90d9b 100644 --- a/pkg/i2gw/providers/ingressnginx/converter.go +++ b/pkg/i2gw/providers/ingressnginx/converter.go @@ -25,7 +25,8 @@ import ( // resourcesToIRConverter implements the ToIR function of i2gw.ResourcesToIRConverter interface. type resourcesToIRConverter struct { - featureParsers []i2gw.FeatureParser + featureParsers []i2gw.FeatureParser + implementationSpecificOptions i2gw.ProviderImplementationSpecificOptions } // newResourcesToIRConverter returns an ingress-nginx resourcesToIRConverter instance. @@ -34,6 +35,9 @@ func newResourcesToIRConverter() *resourcesToIRConverter { featureParsers: []i2gw.FeatureParser{ canaryFeature, }, + implementationSpecificOptions: i2gw.ProviderImplementationSpecificOptions{ + ToImplementationSpecificHTTPPathTypeMatch: implementationSpecificHTTPPathTypeMatch, + }, } } @@ -44,7 +48,7 @@ func (c *resourcesToIRConverter) convert(storage *storage) (intermediate.IR, fie // Convert plain ingress resources to gateway resources, ignoring all // provider-specific features. - ir, errs := common.ToIR(ingressList, storage.ServicePorts, i2gw.ProviderImplementationSpecificOptions{}) + ir, errs := common.ToIR(ingressList, storage.ServicePorts, c.implementationSpecificOptions) if len(errs) > 0 { return intermediate.IR{}, errs } diff --git a/pkg/i2gw/providers/ingressnginx/converter_test.go b/pkg/i2gw/providers/ingressnginx/converter_test.go index 2b8e273d..10ad7fd5 100644 --- a/pkg/i2gw/providers/ingressnginx/converter_test.go +++ b/pkg/i2gw/providers/ingressnginx/converter_test.go @@ -30,7 +30,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/validation/field" - "k8s.io/utils/ptr" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" ) @@ -337,15 +336,56 @@ func Test_ToIR(t *testing.T) { }, }, }, - expectedIR: intermediate.IR{}, - expectedErrors: field.ErrorList{ - { - Type: field.ErrorTypeInvalid, - Field: "spec.rules[0].http.paths[0].pathType", - BadValue: ptr.To("ImplementationSpecific"), - Detail: "implementationSpecific path type is not supported in generic translation, and your provider does not provide custom support to translate it", + expectedIR: intermediate.IR{ + Gateways: map[types.NamespacedName]intermediate.GatewayContext{ + {Namespace: "default", Name: "ingress-nginx"}: { + Gateway: gatewayv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{Name: "ingress-nginx", Namespace: "default"}, + Spec: gatewayv1.GatewaySpec{ + GatewayClassName: "ingress-nginx", + Listeners: []gatewayv1.Listener{{ + Name: "test-mydomain-com-http", + Port: 80, + Protocol: gatewayv1.HTTPProtocolType, + Hostname: ptrTo(gatewayv1.Hostname("test.mydomain.com")), + }}, + }, + }, + }, + }, + HTTPRoutes: map[types.NamespacedName]intermediate.HTTPRouteContext{ + {Namespace: "default", Name: "implementation-specific-regex-test-mydomain-com"}: { + HTTPRoute: gatewayv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{Name: "implementation-specific-regex-test-mydomain-com", Namespace: "default"}, + Spec: gatewayv1.HTTPRouteSpec{ + CommonRouteSpec: gatewayv1.CommonRouteSpec{ + ParentRefs: []gatewayv1.ParentReference{{ + Name: "ingress-nginx", + }}, + }, + Hostnames: []gatewayv1.Hostname{"test.mydomain.com"}, + Rules: []gatewayv1.HTTPRouteRule{{ + Matches: []gatewayv1.HTTPRouteMatch{{ + Path: &gatewayv1.HTTPPathMatch{ + Type: ptrTo(gatewayv1.PathMatchRegularExpression), + Value: ptrTo("/~/echo/**/test"), + }, + }}, + BackendRefs: []gatewayv1.HTTPBackendRef{{ + BackendRef: gatewayv1.BackendRef{ + BackendObjectReference: gatewayv1.BackendObjectReference{ + Name: "test", + Port: ptrTo(gatewayv1.PortNumber(80)), + }, + }, + }}, + }}, + }, + }, + }, }, }, + expectedErrors: field.ErrorList{}, }, { name: "multiple rules with TLS", diff --git a/pkg/i2gw/providers/ingressnginx/implementation_specific.go b/pkg/i2gw/providers/ingressnginx/implementation_specific.go new file mode 100644 index 00000000..2617890b --- /dev/null +++ b/pkg/i2gw/providers/ingressnginx/implementation_specific.go @@ -0,0 +1,29 @@ +/* +Copyright 2025 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 ingressnginx + +import ( + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +// implementationSpecificHTTPPathTypeMatch handles the ImplementationSpecific path type +// for ingress-nginx. When ImplementationSpecific is used, ingress-nginx treats paths +// as regular expressions, so we convert them to PathMatchRegularExpression. +func implementationSpecificHTTPPathTypeMatch(path *gatewayv1.HTTPPathMatch) { + pmRegex := gatewayv1.PathMatchRegularExpression + path.Type = &pmRegex +} diff --git a/pkg/i2gw/providers/ingressnginx/implementation_specific_test.go b/pkg/i2gw/providers/ingressnginx/implementation_specific_test.go new file mode 100644 index 00000000..d1cd66a7 --- /dev/null +++ b/pkg/i2gw/providers/ingressnginx/implementation_specific_test.go @@ -0,0 +1,67 @@ +/* +Copyright 2025 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 ingressnginx + +import ( + "testing" + + "github.com/stretchr/testify/assert" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +func Test_implementationSpecificHTTPPathTypeMatch(t *testing.T) { + testCases := []struct { + name string + inputPath string + expectedType gatewayv1.PathMatchType + expectedValue string + }{ + { + name: "regex path with wildcard", + inputPath: "/.*/execution/.*", + expectedType: gatewayv1.PathMatchRegularExpression, + expectedValue: "/.*/execution/.*", + }, + { + name: "regex path with specific pattern", + inputPath: "/api/v3/amp/login.*", + expectedType: gatewayv1.PathMatchRegularExpression, + expectedValue: "/api/v3/amp/login.*", + }, + { + name: "simple path", + inputPath: "/page/track.*", + expectedType: gatewayv1.PathMatchRegularExpression, + expectedValue: "/page/track.*", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + path := &gatewayv1.HTTPPathMatch{ + Value: &tc.inputPath, + } + + implementationSpecificHTTPPathTypeMatch(path) + + assert.NotNil(t, path.Type) + assert.Equal(t, tc.expectedType, *path.Type) + assert.NotNil(t, path.Value) + assert.Equal(t, tc.expectedValue, *path.Value) + }) + } +}