Skip to content

Commit

Permalink
feat: support regex in path (#779)
Browse files Browse the repository at this point in the history
  • Loading branch information
lxm authored Dec 8, 2021
1 parent 1bbadf0 commit 4e84eb8
Show file tree
Hide file tree
Showing 9 changed files with 596 additions and 59 deletions.
40 changes: 40 additions & 0 deletions docs/en/latest/concepts/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
8 changes: 4 additions & 4 deletions pkg/kube/translation/annotations/cors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down
4 changes: 2 additions & 2 deletions pkg/kube/translation/annotations/iprestriction.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down
2 changes: 1 addition & 1 deletion pkg/kube/translation/annotations/redirect.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
)

const (
_httpToHttps = "k8s.apisix.apache.org/http-to-https"
_httpToHttps = AnnotationsPrefix + "http-to-https"
)

type redirect struct{}
Expand Down
6 changes: 3 additions & 3 deletions pkg/kube/translation/annotations/rewrite.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down
5 changes: 5 additions & 0 deletions pkg/kube/translation/annotations/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
169 changes: 120 additions & 49 deletions pkg/kube/translation/ingress.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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())
}
Expand All @@ -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{
Expand Down Expand Up @@ -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())
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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())
}
Expand Down
Loading

0 comments on commit 4e84eb8

Please sign in to comment.