diff --git a/docs/en/latest/concepts/annotations.md b/docs/en/latest/concepts/annotations.md index 73855bee03..02163545fc 100644 --- a/docs/en/latest/concepts/annotations.md +++ b/docs/en/latest/concepts/annotations.md @@ -131,3 +131,43 @@ spec: port: number: 80 ``` + +Path regular expression +--------- + +You can use the follow annotations to enable path regular expression + +* `k8s.apisix.apache.org/use-regex` + +If this annotations set to `true` and the `PathType` set to `ImplementationSpecific`, the path will be match as regular expression. + +For example, the follwing Ingress. Request path with `/api/*/action1` will use `service1` and `/api/*/action2` will be use `service2` + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: apisix + k8s.apisix.apache.org/use-regex: "true" + name: ingress-v1 +spec: + rules: + - host: httpbin.org + http: + paths: + - path: /api/.*/action1 + pathType: ImplementationSpecific + backend: + service: + name: service1 + port: + number: 80 + - path: /api/.*/action2 + pathType: ImplementationSpecific + backend: + service: + name: service2 + port: + number: 80 +``` diff --git a/pkg/kube/translation/annotations/cors.go b/pkg/kube/translation/annotations/cors.go index c2b862857b..ee88e6c40d 100644 --- a/pkg/kube/translation/annotations/cors.go +++ b/pkg/kube/translation/annotations/cors.go @@ -19,10 +19,10 @@ import ( ) const ( - _enableCors = "k8s.apisix.apache.org/enable-cors" - _corsAllowOrigin = "k8s.apisix.apache.org/cors-allow-origin" - _corsAllowHeaders = "k8s.apisix.apache.org/cors-allow-headers" - _corsAllowMethods = "k8s.apisix.apache.org/cors-allow-methods" + _enableCors = AnnotationsPrefix + "enable-cors" + _corsAllowOrigin = AnnotationsPrefix + "cors-allow-origin" + _corsAllowHeaders = AnnotationsPrefix + "cors-allow-headers" + _corsAllowMethods = AnnotationsPrefix + "cors-allow-methods" ) type cors struct{} diff --git a/pkg/kube/translation/annotations/iprestriction.go b/pkg/kube/translation/annotations/iprestriction.go index 735b9583fb..1d17957b68 100644 --- a/pkg/kube/translation/annotations/iprestriction.go +++ b/pkg/kube/translation/annotations/iprestriction.go @@ -19,8 +19,8 @@ import ( ) const ( - _allowlistSourceRange = "k8s.apisix.apache.org/allowlist-source-range" - _blocklistSourceRange = "k8s.apisix.apache.org/blocklist-source-range" + _allowlistSourceRange = AnnotationsPrefix + "allowlist-source-range" + _blocklistSourceRange = AnnotationsPrefix + "blocklist-source-range" ) type ipRestriction struct{} diff --git a/pkg/kube/translation/annotations/redirect.go b/pkg/kube/translation/annotations/redirect.go index e10d5f8bbe..c162d84a90 100644 --- a/pkg/kube/translation/annotations/redirect.go +++ b/pkg/kube/translation/annotations/redirect.go @@ -19,7 +19,7 @@ import ( ) const ( - _httpToHttps = "k8s.apisix.apache.org/http-to-https" + _httpToHttps = AnnotationsPrefix + "http-to-https" ) type redirect struct{} diff --git a/pkg/kube/translation/annotations/rewrite.go b/pkg/kube/translation/annotations/rewrite.go index 72e23858e2..35a0f9477e 100644 --- a/pkg/kube/translation/annotations/rewrite.go +++ b/pkg/kube/translation/annotations/rewrite.go @@ -21,9 +21,9 @@ import ( ) const ( - _rewriteTarget = "k8s.apisix.apache.org/rewrite-target" - _rewriteTargetRegex = "k8s.apisix.apache.org/rewrite-target-regex" - _rewriteTargetRegexTemplate = "k8s.apisix.apache.org/rewrite-target-regex-template" + _rewriteTarget = AnnotationsPrefix + "rewrite-target" + _rewriteTargetRegex = AnnotationsPrefix + "rewrite-target-regex" + _rewriteTargetRegexTemplate = AnnotationsPrefix + "rewrite-target-regex-template" ) type rewrite struct{} diff --git a/pkg/kube/translation/annotations/types.go b/pkg/kube/translation/annotations/types.go index 37467598e2..4f8f6421f9 100644 --- a/pkg/kube/translation/annotations/types.go +++ b/pkg/kube/translation/annotations/types.go @@ -18,6 +18,11 @@ import ( "strings" ) +const ( + // AnnotationsPrefix is the apisix annotation prefix + AnnotationsPrefix = "k8s.apisix.apache.org/" +) + // Extractor encapsulates some auxiliary methods to extract annotations. type Extractor interface { // GetStringAnnotation returns the string value of the target annotation. diff --git a/pkg/kube/translation/ingress.go b/pkg/kube/translation/ingress.go index 41dcc94eea..d7c577978b 100644 --- a/pkg/kube/translation/ingress.go +++ b/pkg/kube/translation/ingress.go @@ -28,16 +28,23 @@ import ( "github.com/apache/apisix-ingress-controller/pkg/id" kubev2beta3 "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v2beta3" + apisixconst "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/const" + "github.com/apache/apisix-ingress-controller/pkg/kube/translation/annotations" "github.com/apache/apisix-ingress-controller/pkg/log" apisixv1 "github.com/apache/apisix-ingress-controller/pkg/types/apisix/v1" ) +const ( + _regexPriority = 100 +) + func (t *translator) translateIngressV1(ing *networkingv1.Ingress) (*TranslateContext, error) { ctx := &TranslateContext{ upstreamMap: make(map[string]struct{}), } plugins := t.translateAnnotations(ing.Annotations) - + annoExtractor := annotations.NewExtractor(ing.Annotations) + useRegex := annoExtractor.GetBoolAnnotation(annotations.AnnotationsPrefix + "use-regex") // add https for _, tls := range ing.Spec.TLS { apisixTls := kubev2beta3.ApisixTls{ @@ -86,29 +93,49 @@ func (t *translator) translateIngressV1(ing *networkingv1.Ingress) (*TranslateCo ctx.addUpstream(ups) } uris := []string{pathRule.Path} - if pathRule.PathType != nil && *pathRule.PathType == networkingv1.PathTypePrefix { - // As per the specification of Ingress path matching rule: - // if the last element of the path is a substring of the - // last element in request path, it is not a match, e.g. /foo/bar - // matches /foo/bar/baz, but does not match /foo/barbaz. - // While in APISIX, /foo/bar matches both /foo/bar/baz and - // /foo/barbaz. - // In order to be conformant with Ingress specification, here - // we create two paths here, the first is the path itself - // (exact match), the other is path + "/*" (prefix match). - prefix := pathRule.Path - if strings.HasSuffix(prefix, "/") { - prefix += "*" - } else { - prefix += "/*" + var nginxVars []kubev2beta3.ApisixRouteHTTPMatchExpr + if pathRule.PathType != nil { + if *pathRule.PathType == networkingv1.PathTypePrefix { + // As per the specification of Ingress path matching rule: + // if the last element of the path is a substring of the + // last element in request path, it is not a match, e.g. /foo/bar + // matches /foo/bar/baz, but does not match /foo/barbaz. + // While in APISIX, /foo/bar matches both /foo/bar/baz and + // /foo/barbaz. + // In order to be conformant with Ingress specification, here + // we create two paths here, the first is the path itself + // (exact match), the other is path + "/*" (prefix match). + prefix := pathRule.Path + if strings.HasSuffix(prefix, "/") { + prefix += "*" + } else { + prefix += "/*" + } + uris = append(uris, prefix) + } else if *pathRule.PathType == networkingv1.PathTypeImplementationSpecific && useRegex { + nginxVars = append(nginxVars, kubev2beta3.ApisixRouteHTTPMatchExpr{ + Subject: kubev2beta3.ApisixRouteHTTPMatchExprSubject{ + Scope: apisixconst.ScopePath, + }, + Op: apisixconst.OpRegexMatch, + Value: &pathRule.Path, + }) + uris = []string{"/*"} } - uris = append(uris, prefix) } route := apisixv1.NewDefaultRoute() route.Name = composeIngressRouteName(rule.Host, pathRule.Path) route.ID = id.GenID(route.Name) route.Host = rule.Host route.Uris = uris + if len(nginxVars) > 0 { + routeVars, err := t.translateRouteMatchExprs(nginxVars) + if err != nil { + return nil, err + } + route.Vars = routeVars + route.Priority = _regexPriority + } if len(plugins) > 0 { route.Plugins = *(plugins.DeepCopy()) } @@ -126,6 +153,8 @@ func (t *translator) translateIngressV1beta1(ing *networkingv1beta1.Ingress) (*T upstreamMap: make(map[string]struct{}), } plugins := t.translateAnnotations(ing.Annotations) + annoExtractor := annotations.NewExtractor(ing.Annotations) + useRegex := annoExtractor.GetBoolAnnotation(annotations.AnnotationsPrefix + "use-regex") // add https for _, tls := range ing.Spec.TLS { apisixTls := kubev2beta3.ApisixTls{ @@ -174,29 +203,49 @@ func (t *translator) translateIngressV1beta1(ing *networkingv1beta1.Ingress) (*T ctx.addUpstream(ups) } uris := []string{pathRule.Path} - if pathRule.PathType != nil && *pathRule.PathType == networkingv1beta1.PathTypePrefix { - // As per the specification of Ingress path matching rule: - // if the last element of the path is a substring of the - // last element in request path, it is not a match, e.g. /foo/bar - // matches /foo/bar/baz, but does not match /foo/barbaz. - // While in APISIX, /foo/bar matches both /foo/bar/baz and - // /foo/barbaz. - // In order to be conformant with Ingress specification, here - // we create two paths here, the first is the path itself - // (exact match), the other is path + "/*" (prefix match). - prefix := pathRule.Path - if strings.HasSuffix(prefix, "/") { - prefix += "*" - } else { - prefix += "/*" + var nginxVars []kubev2beta3.ApisixRouteHTTPMatchExpr + if pathRule.PathType != nil { + if *pathRule.PathType == networkingv1beta1.PathTypePrefix { + // As per the specification of Ingress path matching rule: + // if the last element of the path is a substring of the + // last element in request path, it is not a match, e.g. /foo/bar + // matches /foo/bar/baz, but does not match /foo/barbaz. + // While in APISIX, /foo/bar matches both /foo/bar/baz and + // /foo/barbaz. + // In order to be conformant with Ingress specification, here + // we create two paths here, the first is the path itself + // (exact match), the other is path + "/*" (prefix match). + prefix := pathRule.Path + if strings.HasSuffix(prefix, "/") { + prefix += "*" + } else { + prefix += "/*" + } + uris = append(uris, prefix) + } else if *pathRule.PathType == networkingv1beta1.PathTypeImplementationSpecific && useRegex { + nginxVars = append(nginxVars, kubev2beta3.ApisixRouteHTTPMatchExpr{ + Subject: kubev2beta3.ApisixRouteHTTPMatchExprSubject{ + Scope: apisixconst.ScopePath, + }, + Op: apisixconst.OpRegexMatch, + Value: &pathRule.Path, + }) + uris = []string{"/*"} } - uris = append(uris, prefix) } route := apisixv1.NewDefaultRoute() route.Name = composeIngressRouteName(rule.Host, pathRule.Path) route.ID = id.GenID(route.Name) route.Host = rule.Host route.Uris = uris + if len(nginxVars) > 0 { + routeVars, err := t.translateRouteMatchExprs(nginxVars) + if err != nil { + return nil, err + } + route.Vars = routeVars + route.Priority = _regexPriority + } if len(plugins) > 0 { route.Plugins = *(plugins.DeepCopy()) } @@ -245,6 +294,8 @@ func (t *translator) translateIngressExtensionsV1beta1(ing *extensionsv1beta1.In upstreamMap: make(map[string]struct{}), } plugins := t.translateAnnotations(ing.Annotations) + annoExtractor := annotations.NewExtractor(ing.Annotations) + useRegex := annoExtractor.GetBoolAnnotation(annotations.AnnotationsPrefix + "use-regex") for _, rule := range ing.Spec.Rules { for _, pathRule := range rule.HTTP.Paths { @@ -265,29 +316,49 @@ func (t *translator) translateIngressExtensionsV1beta1(ing *extensionsv1beta1.In ctx.addUpstream(ups) } uris := []string{pathRule.Path} - if pathRule.PathType != nil && *pathRule.PathType == extensionsv1beta1.PathTypePrefix { - // As per the specification of Ingress path matching rule: - // if the last element of the path is a substring of the - // last element in request path, it is not a match, e.g. /foo/bar - // matches /foo/bar/baz, but does not match /foo/barbaz. - // While in APISIX, /foo/bar matches both /foo/bar/baz and - // /foo/barbaz. - // In order to be conformant with Ingress specification, here - // we create two paths here, the first is the path itself - // (exact match), the other is path + "/*" (prefix match). - prefix := pathRule.Path - if strings.HasSuffix(prefix, "/") { - prefix += "*" - } else { - prefix += "/*" + var nginxVars []kubev2beta3.ApisixRouteHTTPMatchExpr + if pathRule.PathType != nil { + if *pathRule.PathType == extensionsv1beta1.PathTypePrefix { + // As per the specification of Ingress path matching rule: + // if the last element of the path is a substring of the + // last element in request path, it is not a match, e.g. /foo/bar + // matches /foo/bar/baz, but does not match /foo/barbaz. + // While in APISIX, /foo/bar matches both /foo/bar/baz and + // /foo/barbaz. + // In order to be conformant with Ingress specification, here + // we create two paths here, the first is the path itself + // (exact match), the other is path + "/*" (prefix match). + prefix := pathRule.Path + if strings.HasSuffix(prefix, "/") { + prefix += "*" + } else { + prefix += "/*" + } + uris = append(uris, prefix) + } else if *pathRule.PathType == extensionsv1beta1.PathTypeImplementationSpecific && useRegex { + nginxVars = append(nginxVars, kubev2beta3.ApisixRouteHTTPMatchExpr{ + Subject: kubev2beta3.ApisixRouteHTTPMatchExprSubject{ + Scope: apisixconst.ScopePath, + }, + Op: apisixconst.OpRegexMatch, + Value: &pathRule.Path, + }) + uris = []string{"/*"} } - uris = append(uris, prefix) } route := apisixv1.NewDefaultRoute() route.Name = composeIngressRouteName(rule.Host, pathRule.Path) route.ID = id.GenID(route.Name) route.Host = rule.Host route.Uris = uris + if len(nginxVars) > 0 { + routeVars, err := t.translateRouteMatchExprs(nginxVars) + if err != nil { + return nil, err + } + route.Vars = routeVars + route.Priority = _regexPriority + } if len(plugins) > 0 { route.Plugins = *(plugins.DeepCopy()) } diff --git a/pkg/kube/translation/ingress_test.go b/pkg/kube/translation/ingress_test.go index 9254ae1bfe..98b953e2c4 100644 --- a/pkg/kube/translation/ingress_test.go +++ b/pkg/kube/translation/ingress_test.go @@ -30,8 +30,11 @@ import ( "k8s.io/client-go/tools/cache" "github.com/apache/apisix-ingress-controller/pkg/kube" + configv2beta3 "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v2beta3" fakeapisix "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/client/clientset/versioned/fake" apisixinformers "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/client/informers/externalversions" + apisixconst "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/const" + v1 "github.com/apache/apisix-ingress-controller/pkg/types/apisix/v1" ) var ( @@ -198,6 +201,102 @@ func TestTranslateIngressV1BackendWithInvalidService(t *testing.T) { }, err) } +func TestTranslateIngressV1WithRegex(t *testing.T) { + prefix := networkingv1.PathTypeImplementationSpecific + regexPath := "/foo/*/bar" + ing := &networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Annotations: map[string]string{ + "k8s.apisix.apache.org/use-regex": "true", + }, + }, + Spec: networkingv1.IngressSpec{ + Rules: []networkingv1.IngressRule{ + { + Host: "apisix.apache.org", + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{ + Paths: []networkingv1.HTTPIngressPath{ + { + Path: regexPath, + PathType: &prefix, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "test-service", + Port: networkingv1.ServiceBackendPort{ + Name: "port1", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + client := fake.NewSimpleClientset() + informersFactory := informers.NewSharedInformerFactory(client, 0) + svcInformer := informersFactory.Core().V1().Services().Informer() + svcLister := informersFactory.Core().V1().Services().Lister() + epLister, epInformer := kube.NewEndpointListerAndInformer(informersFactory, false) + apisixClient := fakeapisix.NewSimpleClientset() + apisixInformersFactory := apisixinformers.NewSharedInformerFactory(apisixClient, 0) + processCh := make(chan struct{}) + svcInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + processCh <- struct{}{} + }, + }) + epInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + processCh <- struct{}{} + }, + }) + + stopCh := make(chan struct{}) + defer close(stopCh) + go svcInformer.Run(stopCh) + go epInformer.Run(stopCh) + cache.WaitForCacheSync(stopCh, svcInformer.HasSynced) + + _, err := client.CoreV1().Services("default").Create(context.Background(), _testSvc, metav1.CreateOptions{}) + assert.Nil(t, err) + _, err = client.CoreV1().Endpoints("default").Create(context.Background(), _testEp, metav1.CreateOptions{}) + assert.Nil(t, err) + + tr := &translator{ + TranslatorOptions: &TranslatorOptions{ + ServiceLister: svcLister, + EndpointLister: epLister, + ApisixUpstreamLister: apisixInformersFactory.Apisix().V2beta3().ApisixUpstreams().Lister(), + }, + } + + <-processCh + <-processCh + ctx, err := tr.translateIngressV1(ing) + assert.Nil(t, err) + assert.Len(t, ctx.Routes, 1) + assert.Len(t, ctx.Upstreams, 1) + routeVars, err := tr.translateRouteMatchExprs([]configv2beta3.ApisixRouteHTTPMatchExpr{{ + Subject: configv2beta3.ApisixRouteHTTPMatchExprSubject{ + Scope: apisixconst.ScopePath, + }, + Op: apisixconst.OpRegexMatch, + Value: ®exPath, + }}) + assert.Nil(t, err) + + var expectedVars v1.Vars = routeVars + + assert.Equal(t, []string{"/*"}, ctx.Routes[0].Uris) + assert.Equal(t, expectedVars, ctx.Routes[0].Vars) +} + func TestTranslateIngressV1(t *testing.T) { prefix := networkingv1.PathTypePrefix // no backend. @@ -420,6 +519,101 @@ func TestTranslateIngressV1beta1BackendWithInvalidService(t *testing.T) { }, err) } +func TestTranslateIngressV1beta1WithRegex(t *testing.T) { + prefix := networkingv1beta1.PathTypeImplementationSpecific + // no backend. + regexPath := "/foo/*/bar" + ing := &networkingv1beta1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Annotations: map[string]string{ + "k8s.apisix.apache.org/use-regex": "true", + }, + }, + Spec: networkingv1beta1.IngressSpec{ + Rules: []networkingv1beta1.IngressRule{ + { + Host: "apisix.apache.org", + IngressRuleValue: networkingv1beta1.IngressRuleValue{ + HTTP: &networkingv1beta1.HTTPIngressRuleValue{ + Paths: []networkingv1beta1.HTTPIngressPath{ + { + Path: regexPath, + PathType: &prefix, + Backend: networkingv1beta1.IngressBackend{ + ServiceName: "test-service", + ServicePort: intstr.IntOrString{ + Type: intstr.String, + StrVal: "port1", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + client := fake.NewSimpleClientset() + informersFactory := informers.NewSharedInformerFactory(client, 0) + svcInformer := informersFactory.Core().V1().Services().Informer() + svcLister := informersFactory.Core().V1().Services().Lister() + epLister, epInformer := kube.NewEndpointListerAndInformer(informersFactory, false) + apisixClient := fakeapisix.NewSimpleClientset() + apisixInformersFactory := apisixinformers.NewSharedInformerFactory(apisixClient, 0) + processCh := make(chan struct{}) + svcInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + processCh <- struct{}{} + }, + }) + epInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + processCh <- struct{}{} + }, + }) + + stopCh := make(chan struct{}) + defer close(stopCh) + go svcInformer.Run(stopCh) + go epInformer.Run(stopCh) + cache.WaitForCacheSync(stopCh, svcInformer.HasSynced) + + _, err := client.CoreV1().Services("default").Create(context.Background(), _testSvc, metav1.CreateOptions{}) + assert.Nil(t, err) + _, err = client.CoreV1().Endpoints("default").Create(context.Background(), _testEp, metav1.CreateOptions{}) + assert.Nil(t, err) + + tr := &translator{ + TranslatorOptions: &TranslatorOptions{ + ServiceLister: svcLister, + EndpointLister: epLister, + ApisixUpstreamLister: apisixInformersFactory.Apisix().V2beta3().ApisixUpstreams().Lister(), + }, + } + + <-processCh + <-processCh + ctx, err := tr.translateIngressV1beta1(ing) + assert.Nil(t, err) + assert.Len(t, ctx.Routes, 1) + assert.Len(t, ctx.Upstreams, 1) + + routeVars, err := tr.translateRouteMatchExprs([]configv2beta3.ApisixRouteHTTPMatchExpr{{ + Subject: configv2beta3.ApisixRouteHTTPMatchExprSubject{ + Scope: apisixconst.ScopePath, + }, + Op: apisixconst.OpRegexMatch, + Value: ®exPath, + }}) + assert.Nil(t, err) + var expectedVars v1.Vars = routeVars + assert.Equal(t, []string{"/*"}, ctx.Routes[0].Uris) + assert.Equal(t, expectedVars, ctx.Routes[0].Vars) +} + func TestTranslateIngressV1beta1(t *testing.T) { prefix := networkingv1beta1.PathTypePrefix // no backend. @@ -716,3 +910,99 @@ func TestTranslateIngressExtensionsV1beta1BackendWithInvalidService(t *testing.T reason: "port not found", }, err) } + +func TestTranslateIngressExtensionsV1beta1WithRegex(t *testing.T) { + prefix := extensionsv1beta1.PathTypeImplementationSpecific + regexPath := "/foo/*/bar" + ing := &extensionsv1beta1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Annotations: map[string]string{ + "k8s.apisix.apache.org/use-regex": "true", + }, + }, + Spec: extensionsv1beta1.IngressSpec{ + Rules: []extensionsv1beta1.IngressRule{ + { + Host: "apisix.apache.org", + IngressRuleValue: extensionsv1beta1.IngressRuleValue{ + HTTP: &extensionsv1beta1.HTTPIngressRuleValue{ + Paths: []extensionsv1beta1.HTTPIngressPath{ + { + Path: regexPath, + PathType: &prefix, + Backend: extensionsv1beta1.IngressBackend{ + ServiceName: "test-service", + ServicePort: intstr.IntOrString{ + Type: intstr.String, + StrVal: "port1", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + client := fake.NewSimpleClientset() + informersFactory := informers.NewSharedInformerFactory(client, 0) + svcInformer := informersFactory.Core().V1().Services().Informer() + svcLister := informersFactory.Core().V1().Services().Lister() + epLister, epInformer := kube.NewEndpointListerAndInformer(informersFactory, false) + apisixClient := fakeapisix.NewSimpleClientset() + apisixInformersFactory := apisixinformers.NewSharedInformerFactory(apisixClient, 0) + processCh := make(chan struct{}) + svcInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + processCh <- struct{}{} + }, + }) + epInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + processCh <- struct{}{} + }, + }) + + stopCh := make(chan struct{}) + defer close(stopCh) + go svcInformer.Run(stopCh) + go epInformer.Run(stopCh) + cache.WaitForCacheSync(stopCh, svcInformer.HasSynced) + + _, err := client.CoreV1().Services("default").Create(context.Background(), _testSvc, metav1.CreateOptions{}) + assert.Nil(t, err) + _, err = client.CoreV1().Endpoints("default").Create(context.Background(), _testEp, metav1.CreateOptions{}) + assert.Nil(t, err) + + tr := &translator{ + TranslatorOptions: &TranslatorOptions{ + ServiceLister: svcLister, + EndpointLister: epLister, + ApisixUpstreamLister: apisixInformersFactory.Apisix().V2beta3().ApisixUpstreams().Lister(), + }, + } + + <-processCh + <-processCh + ctx, err := tr.translateIngressExtensionsV1beta1(ing) + assert.Nil(t, err) + assert.Len(t, ctx.Routes, 1) + assert.Len(t, ctx.Upstreams, 1) + routeVars, err := tr.translateRouteMatchExprs([]configv2beta3.ApisixRouteHTTPMatchExpr{{ + Subject: configv2beta3.ApisixRouteHTTPMatchExprSubject{ + Scope: apisixconst.ScopePath, + }, + Op: apisixconst.OpRegexMatch, + Value: ®exPath, + }}) + assert.Nil(t, err) + + var expectedVars v1.Vars = routeVars + + assert.Equal(t, []string{"/*"}, ctx.Routes[0].Uris) + assert.Equal(t, expectedVars, ctx.Routes[0].Vars) + +} diff --git a/test/e2e/ingress/ingress.go b/test/e2e/ingress/ingress.go index 890573005d..92dd07f88d 100644 --- a/test/e2e/ingress/ingress.go +++ b/test/e2e/ingress/ingress.go @@ -385,6 +385,40 @@ spec: // Mismatched host _ = s.NewAPISIXClient().GET("/status/200").WithHeader("Host", "a.httpbin.org").Expect().Status(http.StatusNotFound) }) + + ginkgo.It("path regex match", func() { + backendSvc, backendPort := s.DefaultHTTPBackend() + ing := fmt.Sprintf(` +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: apisix + k8s.apisix.apache.org/use-regex: 'true' + name: ingress-v1 +spec: + rules: + - host: httpbin.org + http: + paths: + - path: /anything/.*/ok + pathType: ImplementationSpecific + backend: + service: + name: %s + port: + number: %d +`, backendSvc, backendPort[0]) + err := s.CreateResourceFromString(ing) + assert.Nil(ginkgo.GinkgoT(), err, "creating ingress") + time.Sleep(5 * time.Second) + + _ = s.NewAPISIXClient().GET("/anything/aaa/ok").WithHeader("Host", "httpbin.org").Expect().Status(http.StatusOK) + _ = s.NewAPISIXClient().GET("/anything/aaa/notok").WithHeader("Host", "httpbin.org").Expect().Status(http.StatusNotFound).Body().Contains("404 Route Not Found") + _ = s.NewAPISIXClient().GET("/statusaaa").WithHeader("Host", "httpbin.org").Expect().Status(http.StatusNotFound).Body().Contains("404 Route Not Found") + // Mismatched host + _ = s.NewAPISIXClient().GET("/anything/aaa/ok").WithHeader("Host", "a.httpbin.org").Expect().Status(http.StatusNotFound) + }) }) var _ = ginkgo.Describe("support ingress.networking/v1beta1", func() { @@ -451,6 +485,38 @@ spec: // Mismatched host _ = s.NewAPISIXClient().GET("/status/200").WithHeader("Host", "a.httpbin.org").Expect().Status(http.StatusNotFound) }) + + ginkgo.It("path regex match", func() { + backendSvc, backendPort := s.DefaultHTTPBackend() + ing := fmt.Sprintf(` +apiVersion: networking.k8s.io/v1beta1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: apisix + k8s.apisix.apache.org/use-regex: 'true' + name: ingress-v1beta1 +spec: + rules: + - host: httpbin.org + http: + paths: + - path: /anything/.*/ok + pathType: ImplementationSpecific + backend: + serviceName: %s + servicePort: %d +`, backendSvc, backendPort[0]) + err := s.CreateResourceFromString(ing) + assert.Nil(ginkgo.GinkgoT(), err, "creating ingress") + time.Sleep(5 * time.Second) + + _ = s.NewAPISIXClient().GET("/anything/aaa/ok").WithHeader("Host", "httpbin.org").Expect().Status(http.StatusOK) + _ = s.NewAPISIXClient().GET("/anything/aaa/notok").WithHeader("Host", "httpbin.org").Expect().Status(http.StatusNotFound).Body().Contains("404 Route Not Found") + _ = s.NewAPISIXClient().GET("/statusaaa").WithHeader("Host", "httpbin.org").Expect().Status(http.StatusNotFound).Body().Contains("404 Route Not Found") + // Mismatched host + _ = s.NewAPISIXClient().GET("/anything/aaa/ok").WithHeader("Host", "a.httpbin.org").Expect().Status(http.StatusNotFound) + }) }) var _ = ginkgo.Describe("support ingress.extensions/v1beta1", func() { @@ -517,6 +583,38 @@ spec: // Mismatched host _ = s.NewAPISIXClient().GET("/status/200").WithHeader("Host", "a.httpbin.org").Expect().Status(http.StatusNotFound) }) + + ginkgo.It("path regex match", func() { + backendSvc, backendPort := s.DefaultHTTPBackend() + ing := fmt.Sprintf(` +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: apisix + k8s.apisix.apache.org/use-regex: 'true' + name: ingress-v1beta1 +spec: + rules: + - host: httpbin.org + http: + paths: + - path: /anything/.*/ok + pathType: ImplementationSpecific + backend: + serviceName: %s + servicePort: %d +`, backendSvc, backendPort[0]) + err := s.CreateResourceFromString(ing) + assert.Nil(ginkgo.GinkgoT(), err, "creating ingress") + time.Sleep(5 * time.Second) + + _ = s.NewAPISIXClient().GET("/anything/aaa/ok").WithHeader("Host", "httpbin.org").Expect().Status(http.StatusOK) + _ = s.NewAPISIXClient().GET("/anything/aaa/notok").WithHeader("Host", "httpbin.org").Expect().Status(http.StatusNotFound).Body().Contains("404 Route Not Found") + _ = s.NewAPISIXClient().GET("/statusaaa").WithHeader("Host", "httpbin.org").Expect().Status(http.StatusNotFound).Body().Contains("404 Route Not Found") + // Mismatched host + _ = s.NewAPISIXClient().GET("/anything/aaa/ok").WithHeader("Host", "a.httpbin.org").Expect().Status(http.StatusNotFound) + }) }) var _ = ginkgo.Describe("support ingress.networking/v1 with headless service backend", func() { @@ -622,4 +720,37 @@ spec: // Mismatched host _ = s.NewAPISIXClient().GET("/status/200").WithHeader("Host", "a.httpbin.org").Expect().Status(http.StatusNotFound) }) + + ginkgo.It("path regex match", func() { + ing := fmt.Sprintf(` +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: apisix + k8s.apisix.apache.org/use-regex: 'true' + name: ingress-v1 +spec: + rules: + - host: httpbin.org + http: + paths: + - path: /anything/.*/ok + pathType: ImplementationSpecific + backend: + service: + name: %s + port: + number: %d +`, backendSvc, backendPort[0]) + err := s.CreateResourceFromString(ing) + assert.Nil(ginkgo.GinkgoT(), err, "creating ingress") + time.Sleep(5 * time.Second) + + _ = s.NewAPISIXClient().GET("/anything/aaa/ok").WithHeader("Host", "httpbin.org").Expect().Status(http.StatusOK) + _ = s.NewAPISIXClient().GET("/anything/aaa/notok").WithHeader("Host", "httpbin.org").Expect().Status(http.StatusNotFound).Body().Contains("404 Route Not Found") + _ = s.NewAPISIXClient().GET("/statusaaa").WithHeader("Host", "httpbin.org").Expect().Status(http.StatusNotFound).Body().Contains("404 Route Not Found") + // Mismatched host + _ = s.NewAPISIXClient().GET("/anything/aaa/ok").WithHeader("Host", "a.httpbin.org").Expect().Status(http.StatusNotFound) + }) })