Merge branch 'v3.0' of github.com:traefik/traefik
This commit is contained in:
commit
7f25d6de67
23 changed files with 901 additions and 226 deletions
|
@ -344,6 +344,35 @@ providers:
|
||||||
--providers.kubernetesingress.ingressclass=traefik-internal
|
--providers.kubernetesingress.ingressclass=traefik-internal
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `disableIngressClassLookup`
|
||||||
|
|
||||||
|
_Optional, Default: false_
|
||||||
|
|
||||||
|
If the parameter is set to `true`,
|
||||||
|
Traefik will not discover IngressClasses in the cluster.
|
||||||
|
By doing so, it alleviates the requirement of giving Traefik the rights to look IngressClasses up.
|
||||||
|
Furthermore, when this option is set to `true`,
|
||||||
|
Traefik is not able to handle Ingresses with IngressClass references,
|
||||||
|
therefore such Ingresses will be ignored.
|
||||||
|
Please note that annotations are not affected by this option.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
kubernetesIngress:
|
||||||
|
disableIngressClassLookup: true
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.kubernetesIngress]
|
||||||
|
disableIngressClassLookup = true
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.kubernetesingress.disableingressclasslookup=true
|
||||||
|
```
|
||||||
|
|
||||||
### `ingressEndpoint`
|
### `ingressEndpoint`
|
||||||
|
|
||||||
#### `hostname`
|
#### `hostname`
|
||||||
|
|
|
@ -735,6 +735,9 @@ Allow ExternalName services. (Default: ```false```)
|
||||||
`--providers.kubernetesingress.certauthfilepath`:
|
`--providers.kubernetesingress.certauthfilepath`:
|
||||||
Kubernetes certificate authority file path (not needed for in-cluster client).
|
Kubernetes certificate authority file path (not needed for in-cluster client).
|
||||||
|
|
||||||
|
`--providers.kubernetesingress.disableingressclasslookup`:
|
||||||
|
Disables the lookup of IngressClasses. (Default: ```false```)
|
||||||
|
|
||||||
`--providers.kubernetesingress.endpoint`:
|
`--providers.kubernetesingress.endpoint`:
|
||||||
Kubernetes server endpoint (required for external cluster client).
|
Kubernetes server endpoint (required for external cluster client).
|
||||||
|
|
||||||
|
|
|
@ -735,6 +735,9 @@ Allow ExternalName services. (Default: ```false```)
|
||||||
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_CERTAUTHFILEPATH`:
|
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_CERTAUTHFILEPATH`:
|
||||||
Kubernetes certificate authority file path (not needed for in-cluster client).
|
Kubernetes certificate authority file path (not needed for in-cluster client).
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_DISABLEINGRESSCLASSLOOKUP`:
|
||||||
|
Disables the lookup of IngressClasses. (Default: ```false```)
|
||||||
|
|
||||||
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_ENDPOINT`:
|
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_ENDPOINT`:
|
||||||
Kubernetes server endpoint (required for external cluster client).
|
Kubernetes server endpoint (required for external cluster client).
|
||||||
|
|
||||||
|
|
|
@ -108,6 +108,7 @@
|
||||||
throttleDuration = "42s"
|
throttleDuration = "42s"
|
||||||
allowEmptyServices = true
|
allowEmptyServices = true
|
||||||
allowExternalNameServices = true
|
allowExternalNameServices = true
|
||||||
|
disableIngressClassLookup = true
|
||||||
[providers.kubernetesIngress.ingressEndpoint]
|
[providers.kubernetesIngress.ingressEndpoint]
|
||||||
ip = "foobar"
|
ip = "foobar"
|
||||||
hostname = "foobar"
|
hostname = "foobar"
|
||||||
|
|
|
@ -117,6 +117,7 @@ providers:
|
||||||
throttleDuration: 42s
|
throttleDuration: 42s
|
||||||
allowEmptyServices: true
|
allowEmptyServices: true
|
||||||
allowExternalNameServices: true
|
allowExternalNameServices: true
|
||||||
|
disableIngressClassLookup: true
|
||||||
ingressEndpoint:
|
ingressEndpoint:
|
||||||
ip: foobar
|
ip: foobar
|
||||||
hostname: foobar
|
hostname: foobar
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -385,7 +385,6 @@ require (
|
||||||
replace (
|
replace (
|
||||||
github.com/abbot/go-http-auth => github.com/containous/go-http-auth v0.4.1-0.20200324110947-a37a7636d23e
|
github.com/abbot/go-http-auth => github.com/containous/go-http-auth v0.4.1-0.20200324110947-a37a7636d23e
|
||||||
github.com/go-check/check => github.com/containous/check v0.0.0-20170915194414-ca0bf163426a
|
github.com/go-check/check => github.com/containous/check v0.0.0-20170915194414-ca0bf163426a
|
||||||
github.com/gorilla/mux => github.com/containous/mux v0.0.0-20220627093034-b2dd784e613f
|
|
||||||
github.com/mailgun/minheap => github.com/containous/minheap v0.0.0-20190809180810-6e71eb837595
|
github.com/mailgun/minheap => github.com/containous/minheap v0.0.0-20190809180810-6e71eb837595
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
7
go.sum
7
go.sum
|
@ -457,8 +457,6 @@ github.com/containous/go-http-auth v0.4.1-0.20200324110947-a37a7636d23e h1:D+uTE
|
||||||
github.com/containous/go-http-auth v0.4.1-0.20200324110947-a37a7636d23e/go.mod h1:s8kLgBQolDbsJOPVIGCEEv9zGAKUUf/685Gi0Qqg8z8=
|
github.com/containous/go-http-auth v0.4.1-0.20200324110947-a37a7636d23e/go.mod h1:s8kLgBQolDbsJOPVIGCEEv9zGAKUUf/685Gi0Qqg8z8=
|
||||||
github.com/containous/minheap v0.0.0-20190809180810-6e71eb837595 h1:aPspFRO6b94To3gl4yTDOEtpjFwXI7V2W+z0JcNljQ4=
|
github.com/containous/minheap v0.0.0-20190809180810-6e71eb837595 h1:aPspFRO6b94To3gl4yTDOEtpjFwXI7V2W+z0JcNljQ4=
|
||||||
github.com/containous/minheap v0.0.0-20190809180810-6e71eb837595/go.mod h1:+lHFbEasIiQVGzhVDVw/cn0ZaOzde2OwNncp1NhXV4c=
|
github.com/containous/minheap v0.0.0-20190809180810-6e71eb837595/go.mod h1:+lHFbEasIiQVGzhVDVw/cn0ZaOzde2OwNncp1NhXV4c=
|
||||||
github.com/containous/mux v0.0.0-20220627093034-b2dd784e613f h1:1uEtynq2C0ljy3630jt7EAxg8jZY2gy6YHdGwdqEpWw=
|
|
||||||
github.com/containous/mux v0.0.0-20220627093034-b2dd784e613f/go.mod h1:z8WW7n06n8/1xF9Jl9WmuDeZuHAhfL+bwarNjsciwwg=
|
|
||||||
github.com/coredns/coredns v1.1.2/go.mod h1:zASH/MVDgR6XZTbxvOnsZfffS+31vg6Ackf/wo1+AM0=
|
github.com/coredns/coredns v1.1.2/go.mod h1:zASH/MVDgR6XZTbxvOnsZfffS+31vg6Ackf/wo1+AM0=
|
||||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
|
@ -936,6 +934,11 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR
|
||||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
||||||
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
||||||
|
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
|
github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
|
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
|
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||||
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
|
18
integration/fixtures/k8s_ingressclass_disabled.toml
Normal file
18
integration/fixtures/k8s_ingressclass_disabled.toml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
[global]
|
||||||
|
checkNewVersion = false
|
||||||
|
sendAnonymousUsage = false
|
||||||
|
|
||||||
|
[log]
|
||||||
|
level = "DEBUG"
|
||||||
|
noColor = true
|
||||||
|
|
||||||
|
[api]
|
||||||
|
insecure = true
|
||||||
|
|
||||||
|
[entryPoints]
|
||||||
|
[entryPoints.web]
|
||||||
|
address = ":8000"
|
||||||
|
|
||||||
|
[providers.kubernetesIngress]
|
||||||
|
ingressClass = "traefik-keep"
|
||||||
|
disableIngressClassLookup = true
|
|
@ -128,6 +128,17 @@ func (s *K8sSuite) TestIngressclass(c *check.C) {
|
||||||
testConfiguration(c, "testdata/rawdata-ingressclass.json", "8080")
|
testConfiguration(c, "testdata/rawdata-ingressclass.json", "8080")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *K8sSuite) TestDisableIngressclassLookup(c *check.C) {
|
||||||
|
cmd, display := s.traefikCmd(withConfigFile("fixtures/k8s_ingressclass_disabled.toml"))
|
||||||
|
defer display(c)
|
||||||
|
|
||||||
|
err := cmd.Start()
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
defer s.killCmd(cmd)
|
||||||
|
|
||||||
|
testConfiguration(c, "testdata/rawdata-ingressclass-disabled.json", "8080")
|
||||||
|
}
|
||||||
|
|
||||||
func testConfiguration(c *check.C, path, apiPort string) {
|
func testConfiguration(c *check.C, path, apiPort string) {
|
||||||
err := try.GetRequest("http://127.0.0.1:"+apiPort+"/api/entrypoints", 20*time.Second, try.BodyContains(`"name":"web"`))
|
err := try.GetRequest("http://127.0.0.1:"+apiPort+"/api/entrypoints", 20*time.Second, try.BodyContains(`"name":"web"`))
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
74
integration/testdata/rawdata-ingressclass-disabled.json
vendored
Normal file
74
integration/testdata/rawdata-ingressclass-disabled.json
vendored
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
{
|
||||||
|
"routers": {
|
||||||
|
"api@internal": {
|
||||||
|
"entryPoints": [
|
||||||
|
"traefik"
|
||||||
|
],
|
||||||
|
"service": "api@internal",
|
||||||
|
"rule": "PathPrefix(`/api`)",
|
||||||
|
"priority": 2147483646,
|
||||||
|
"status": "enabled",
|
||||||
|
"using": [
|
||||||
|
"traefik"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dashboard@internal": {
|
||||||
|
"entryPoints": [
|
||||||
|
"traefik"
|
||||||
|
],
|
||||||
|
"middlewares": [
|
||||||
|
"dashboard_redirect@internal",
|
||||||
|
"dashboard_stripprefix@internal"
|
||||||
|
],
|
||||||
|
"service": "dashboard@internal",
|
||||||
|
"rule": "PathPrefix(`/`)",
|
||||||
|
"priority": 2147483645,
|
||||||
|
"status": "enabled",
|
||||||
|
"using": [
|
||||||
|
"traefik"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"middlewares": {
|
||||||
|
"dashboard_redirect@internal": {
|
||||||
|
"redirectRegex": {
|
||||||
|
"regex": "^(http:\\/\\/(\\[[\\w:.]+\\]|[\\w\\._-]+)(:\\d+)?)\\/$",
|
||||||
|
"replacement": "${1}/dashboard/",
|
||||||
|
"permanent": true
|
||||||
|
},
|
||||||
|
"status": "enabled",
|
||||||
|
"usedBy": [
|
||||||
|
"dashboard@internal"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dashboard_stripprefix@internal": {
|
||||||
|
"stripPrefix": {
|
||||||
|
"prefixes": [
|
||||||
|
"/dashboard/",
|
||||||
|
"/dashboard"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status": "enabled",
|
||||||
|
"usedBy": [
|
||||||
|
"dashboard@internal"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"api@internal": {
|
||||||
|
"status": "enabled",
|
||||||
|
"usedBy": [
|
||||||
|
"api@internal"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dashboard@internal": {
|
||||||
|
"status": "enabled",
|
||||||
|
"usedBy": [
|
||||||
|
"dashboard@internal"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noop@internal": {
|
||||||
|
"status": "enabled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,14 +7,13 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/traefik/traefik/v2/pkg/ip"
|
"github.com/traefik/traefik/v2/pkg/ip"
|
||||||
"github.com/traefik/traefik/v2/pkg/middlewares/requestdecorator"
|
"github.com/traefik/traefik/v2/pkg/middlewares/requestdecorator"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
var httpFuncs = map[string]func(*mux.Route, ...string) error{
|
var httpFuncs = map[string]func(*matchersTree, ...string) error{
|
||||||
"ClientIP": expectNParameters(clientIP, 1),
|
"ClientIP": expectNParameters(clientIP, 1),
|
||||||
"Method": expectNParameters(method, 1),
|
"Method": expectNParameters(method, 1),
|
||||||
"Host": expectNParameters(host, 1),
|
"Host": expectNParameters(host, 1),
|
||||||
|
@ -28,17 +27,17 @@ var httpFuncs = map[string]func(*mux.Route, ...string) error{
|
||||||
"QueryRegexp": expectNParameters(queryRegexp, 1, 2),
|
"QueryRegexp": expectNParameters(queryRegexp, 1, 2),
|
||||||
}
|
}
|
||||||
|
|
||||||
func expectNParameters(fn func(*mux.Route, ...string) error, n ...int) func(*mux.Route, ...string) error {
|
func expectNParameters(fn func(*matchersTree, ...string) error, n ...int) func(*matchersTree, ...string) error {
|
||||||
return func(route *mux.Route, s ...string) error {
|
return func(tree *matchersTree, s ...string) error {
|
||||||
if !slices.Contains(n, len(s)) {
|
if !slices.Contains(n, len(s)) {
|
||||||
return fmt.Errorf("unexpected number of parameters; got %d, expected one of %v", len(s), n)
|
return fmt.Errorf("unexpected number of parameters; got %d, expected one of %v", len(s), n)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fn(route, s...)
|
return fn(tree, s...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func clientIP(route *mux.Route, clientIP ...string) error {
|
func clientIP(tree *matchersTree, clientIP ...string) error {
|
||||||
checker, err := ip.NewChecker(clientIP)
|
checker, err := ip.NewChecker(clientIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("initializing IP checker for ClientIP matcher: %w", err)
|
return fmt.Errorf("initializing IP checker for ClientIP matcher: %w", err)
|
||||||
|
@ -46,7 +45,7 @@ func clientIP(route *mux.Route, clientIP ...string) error {
|
||||||
|
|
||||||
strategy := ip.RemoteAddrStrategy{}
|
strategy := ip.RemoteAddrStrategy{}
|
||||||
|
|
||||||
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
tree.matcher = func(req *http.Request) bool {
|
||||||
ok, err := checker.Contains(strategy.GetIP(req))
|
ok, err := checker.Contains(strategy.GetIP(req))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Ctx(req.Context()).Warn().Err(err).Msg("ClientIP matcher: could not match remote address")
|
log.Ctx(req.Context()).Warn().Err(err).Msg("ClientIP matcher: could not match remote address")
|
||||||
|
@ -54,16 +53,22 @@ func clientIP(route *mux.Route, clientIP ...string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
return ok
|
return ok
|
||||||
})
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func method(route *mux.Route, methods ...string) error {
|
func method(tree *matchersTree, methods ...string) error {
|
||||||
return route.Methods(methods...).GetError()
|
method := strings.ToUpper(methods[0])
|
||||||
|
|
||||||
|
tree.matcher = func(req *http.Request) bool {
|
||||||
|
return method == req.Method
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func host(route *mux.Route, hosts ...string) error {
|
func host(tree *matchersTree, hosts ...string) error {
|
||||||
host := hosts[0]
|
host := hosts[0]
|
||||||
|
|
||||||
if !IsASCII(host) {
|
if !IsASCII(host) {
|
||||||
|
@ -72,7 +77,7 @@ func host(route *mux.Route, hosts ...string) error {
|
||||||
|
|
||||||
host = strings.ToLower(host)
|
host = strings.ToLower(host)
|
||||||
|
|
||||||
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
tree.matcher = func(req *http.Request) bool {
|
||||||
reqHost := requestdecorator.GetCanonizedHost(req.Context())
|
reqHost := requestdecorator.GetCanonizedHost(req.Context())
|
||||||
if len(reqHost) == 0 {
|
if len(reqHost) == 0 {
|
||||||
return false
|
return false
|
||||||
|
@ -104,12 +109,12 @@ func host(route *mux.Route, hosts ...string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
})
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func hostRegexp(route *mux.Route, hosts ...string) error {
|
func hostRegexp(tree *matchersTree, hosts ...string) error {
|
||||||
host := hosts[0]
|
host := hosts[0]
|
||||||
|
|
||||||
if !IsASCII(host) {
|
if !IsASCII(host) {
|
||||||
|
@ -121,29 +126,29 @@ func hostRegexp(route *mux.Route, hosts ...string) error {
|
||||||
return fmt.Errorf("compiling HostRegexp matcher: %w", err)
|
return fmt.Errorf("compiling HostRegexp matcher: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
tree.matcher = func(req *http.Request) bool {
|
||||||
return re.MatchString(requestdecorator.GetCanonizedHost(req.Context())) ||
|
return re.MatchString(requestdecorator.GetCanonizedHost(req.Context())) ||
|
||||||
re.MatchString(requestdecorator.GetCNAMEFlatten(req.Context()))
|
re.MatchString(requestdecorator.GetCNAMEFlatten(req.Context()))
|
||||||
})
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func path(route *mux.Route, paths ...string) error {
|
func path(tree *matchersTree, paths ...string) error {
|
||||||
path := paths[0]
|
path := paths[0]
|
||||||
|
|
||||||
if !strings.HasPrefix(path, "/") {
|
if !strings.HasPrefix(path, "/") {
|
||||||
return fmt.Errorf("path %q does not start with a '/'", path)
|
return fmt.Errorf("path %q does not start with a '/'", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
tree.matcher = func(req *http.Request) bool {
|
||||||
return req.URL.Path == path
|
return req.URL.Path == path
|
||||||
})
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func pathRegexp(route *mux.Route, paths ...string) error {
|
func pathRegexp(tree *matchersTree, paths ...string) error {
|
||||||
path := paths[0]
|
path := paths[0]
|
||||||
|
|
||||||
re, err := regexp.Compile(path)
|
re, err := regexp.Compile(path)
|
||||||
|
@ -151,36 +156,65 @@ func pathRegexp(route *mux.Route, paths ...string) error {
|
||||||
return fmt.Errorf("compiling PathPrefix matcher: %w", err)
|
return fmt.Errorf("compiling PathPrefix matcher: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
tree.matcher = func(req *http.Request) bool {
|
||||||
return re.MatchString(req.URL.Path)
|
return re.MatchString(req.URL.Path)
|
||||||
})
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func pathPrefix(route *mux.Route, paths ...string) error {
|
func pathPrefix(tree *matchersTree, paths ...string) error {
|
||||||
path := paths[0]
|
path := paths[0]
|
||||||
|
|
||||||
if !strings.HasPrefix(path, "/") {
|
if !strings.HasPrefix(path, "/") {
|
||||||
return fmt.Errorf("path %q does not start with a '/'", path)
|
return fmt.Errorf("path %q does not start with a '/'", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
tree.matcher = func(req *http.Request) bool {
|
||||||
return strings.HasPrefix(req.URL.Path, path)
|
return strings.HasPrefix(req.URL.Path, path)
|
||||||
})
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func header(route *mux.Route, headers ...string) error {
|
func header(tree *matchersTree, headers ...string) error {
|
||||||
return route.Headers(headers...).GetError()
|
key, value := http.CanonicalHeaderKey(headers[0]), headers[1]
|
||||||
|
|
||||||
|
tree.matcher = func(req *http.Request) bool {
|
||||||
|
for _, headerValue := range req.Header[key] {
|
||||||
|
if headerValue == value {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func headerRegexp(route *mux.Route, headers ...string) error {
|
func headerRegexp(tree *matchersTree, headers ...string) error {
|
||||||
return route.HeadersRegexp(headers...).GetError()
|
key, value := http.CanonicalHeaderKey(headers[0]), headers[1]
|
||||||
|
|
||||||
|
re, err := regexp.Compile(value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("compiling HeaderRegexp matcher: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tree.matcher = func(req *http.Request) bool {
|
||||||
|
for _, headerValue := range req.Header[key] {
|
||||||
|
if re.MatchString(headerValue) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func query(route *mux.Route, queries ...string) error {
|
func query(tree *matchersTree, queries ...string) error {
|
||||||
key := queries[0]
|
key := queries[0]
|
||||||
|
|
||||||
var value string
|
var value string
|
||||||
|
@ -188,21 +222,21 @@ func query(route *mux.Route, queries ...string) error {
|
||||||
value = queries[1]
|
value = queries[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
tree.matcher = func(req *http.Request) bool {
|
||||||
values, ok := req.URL.Query()[key]
|
values, ok := req.URL.Query()[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return slices.Contains(values, value)
|
return slices.Contains(values, value)
|
||||||
})
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func queryRegexp(route *mux.Route, queries ...string) error {
|
func queryRegexp(tree *matchersTree, queries ...string) error {
|
||||||
if len(queries) == 1 {
|
if len(queries) == 1 {
|
||||||
return query(route, queries...)
|
return query(tree, queries...)
|
||||||
}
|
}
|
||||||
|
|
||||||
key, value := queries[0], queries[1]
|
key, value := queries[0], queries[1]
|
||||||
|
@ -212,7 +246,7 @@ func queryRegexp(route *mux.Route, queries ...string) error {
|
||||||
return fmt.Errorf("compiling QueryRegexp matcher: %w", err)
|
return fmt.Errorf("compiling QueryRegexp matcher: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
tree.matcher = func(req *http.Request) bool {
|
||||||
values, ok := req.URL.Query()[key]
|
values, ok := req.URL.Query()[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
|
@ -223,7 +257,7 @@ func queryRegexp(route *mux.Route, queries ...string) error {
|
||||||
})
|
})
|
||||||
|
|
||||||
return idx >= 0
|
return idx >= 0
|
||||||
})
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package http
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -121,16 +122,18 @@ func TestMethodMatcher(t *testing.T) {
|
||||||
desc: "valid Method matcher",
|
desc: "valid Method matcher",
|
||||||
rule: "Method(`GET`)",
|
rule: "Method(`GET`)",
|
||||||
expected: map[string]int{
|
expected: map[string]int{
|
||||||
http.MethodGet: http.StatusOK,
|
http.MethodGet: http.StatusOK,
|
||||||
http.MethodPost: http.StatusMethodNotAllowed,
|
http.MethodPost: http.StatusNotFound,
|
||||||
|
strings.ToLower(http.MethodGet): http.StatusNotFound,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "valid Method matcher (lower case)",
|
desc: "valid Method matcher (lower case)",
|
||||||
rule: "Method(`get`)",
|
rule: "Method(`get`)",
|
||||||
expected: map[string]int{
|
expected: map[string]int{
|
||||||
http.MethodGet: http.StatusOK,
|
http.MethodGet: http.StatusOK,
|
||||||
http.MethodPost: http.StatusMethodNotAllowed,
|
http.MethodPost: http.StatusNotFound,
|
||||||
|
strings.ToLower(http.MethodGet): http.StatusNotFound,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -200,6 +203,7 @@ func TestHostMatcher(t *testing.T) {
|
||||||
"https://example.com": http.StatusOK,
|
"https://example.com": http.StatusOK,
|
||||||
"https://example.com:8080": http.StatusOK,
|
"https://example.com:8080": http.StatusOK,
|
||||||
"https://example.com/path": http.StatusOK,
|
"https://example.com/path": http.StatusOK,
|
||||||
|
"https://EXAMPLE.COM/path": http.StatusOK,
|
||||||
"https://example.org": http.StatusNotFound,
|
"https://example.org": http.StatusNotFound,
|
||||||
"https://example.org/path": http.StatusNotFound,
|
"https://example.org/path": http.StatusNotFound,
|
||||||
},
|
},
|
||||||
|
@ -665,6 +669,17 @@ func TestHeaderMatcher(t *testing.T) {
|
||||||
{"X-Forwarded-Host": []string{"example.com"}}: http.StatusNotFound,
|
{"X-Forwarded-Host": []string{"example.com"}}: http.StatusNotFound,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "valid Header matcher (non-canonical form)",
|
||||||
|
rule: "Header(`x-forwarded-proto`, `https`)",
|
||||||
|
expected: map[*http.Header]int{
|
||||||
|
{"X-Forwarded-Proto": []string{"https"}}: http.StatusOK,
|
||||||
|
{"x-forwarded-proto": []string{"https"}}: http.StatusNotFound,
|
||||||
|
{"X-Forwarded-Proto": []string{"http", "https"}}: http.StatusOK,
|
||||||
|
{"X-Forwarded-Proto": []string{"https", "http"}}: http.StatusOK,
|
||||||
|
{"X-Forwarded-Host": []string{"example.com"}}: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range testCases {
|
for _, test := range testCases {
|
||||||
|
@ -747,6 +762,18 @@ func TestHeaderRegexpMatcher(t *testing.T) {
|
||||||
{"X-Forwarded-Host": []string{"example.com"}}: http.StatusNotFound,
|
{"X-Forwarded-Host": []string{"example.com"}}: http.StatusNotFound,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "valid HeaderRegexp matcher (non-canonical form)",
|
||||||
|
rule: "HeaderRegexp(`x-forwarded-proto`, `^https?$`)",
|
||||||
|
expected: map[*http.Header]int{
|
||||||
|
{"X-Forwarded-Proto": []string{"http"}}: http.StatusOK,
|
||||||
|
{"x-forwarded-proto": []string{"http"}}: http.StatusNotFound,
|
||||||
|
{"X-Forwarded-Proto": []string{"https"}}: http.StatusOK,
|
||||||
|
{"X-Forwarded-Proto": []string{"HTTPS"}}: http.StatusNotFound,
|
||||||
|
{"X-Forwarded-Proto": []string{"ws", "https"}}: http.StatusOK,
|
||||||
|
{"X-Forwarded-Host": []string{"example.com"}}: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
desc: "valid HeaderRegexp matcher with Traefik v2 syntax",
|
desc: "valid HeaderRegexp matcher with Traefik v2 syntax",
|
||||||
rule: "HeaderRegexp(`X-Forwarded-Proto`, `http{secure:s?}`)",
|
rule: "HeaderRegexp(`X-Forwarded-Proto`, `http{secure:s?}`)",
|
||||||
|
|
|
@ -3,15 +3,16 @@ package http
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/traefik/traefik/v2/pkg/rules"
|
"github.com/traefik/traefik/v2/pkg/rules"
|
||||||
"github.com/vulcand/predicate"
|
"github.com/vulcand/predicate"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Muxer handles routing with rules.
|
// Muxer handles routing with rules.
|
||||||
type Muxer struct {
|
type Muxer struct {
|
||||||
*mux.Router
|
routes routes
|
||||||
parser predicate.Parser
|
parser predicate.Parser
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,18 +25,30 @@ func NewMuxer() (*Muxer, error) {
|
||||||
|
|
||||||
parser, err := rules.NewParser(matchers)
|
parser, err := rules.NewParser(matchers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("error while creating parser: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Muxer{
|
return &Muxer{
|
||||||
Router: mux.NewRouter().SkipClean(true),
|
|
||||||
parser: parser,
|
parser: parser,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServeHTTP forwards the connection to the matching HTTP handler.
|
||||||
|
// Serves 404 if no handler is found.
|
||||||
|
func (m *Muxer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
for _, route := range m.routes {
|
||||||
|
if route.matchers.match(req) {
|
||||||
|
route.handler.ServeHTTP(rw, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http.NotFoundHandler().ServeHTTP(rw, req)
|
||||||
|
}
|
||||||
|
|
||||||
// AddRoute add a new route to the router.
|
// AddRoute add a new route to the router.
|
||||||
func (r *Muxer) AddRoute(rule string, priority int, handler http.Handler) error {
|
func (m *Muxer) AddRoute(rule string, priority int, handler http.Handler) error {
|
||||||
parse, err := r.parser.Parse(rule)
|
parse, err := m.parser.Parse(rule)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error while parsing rule %s: %w", rule, err)
|
return fmt.Errorf("error while parsing rule %s: %w", rule, err)
|
||||||
}
|
}
|
||||||
|
@ -45,101 +58,27 @@ func (r *Muxer) AddRoute(rule string, priority int, handler http.Handler) error
|
||||||
return fmt.Errorf("error while parsing rule %s", rule)
|
return fmt.Errorf("error while parsing rule %s", rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var matchers matchersTree
|
||||||
|
err = matchers.addRule(buildTree())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while adding rule %s: %w", rule, err)
|
||||||
|
}
|
||||||
|
|
||||||
if priority == 0 {
|
if priority == 0 {
|
||||||
priority = len(rule)
|
priority = len(rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
route := r.NewRoute().Handler(handler).Priority(priority)
|
m.routes = append(m.routes, &route{
|
||||||
|
handler: handler,
|
||||||
|
matchers: matchers,
|
||||||
|
priority: priority,
|
||||||
|
})
|
||||||
|
|
||||||
err = addRuleOnRoute(route, buildTree())
|
sort.Sort(m.routes)
|
||||||
if err != nil {
|
|
||||||
route.BuildOnly()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func addRuleOnRouter(router *mux.Router, rule *rules.Tree) error {
|
|
||||||
switch rule.Matcher {
|
|
||||||
case "and":
|
|
||||||
route := router.NewRoute()
|
|
||||||
err := addRuleOnRoute(route, rule.RuleLeft)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return addRuleOnRoute(route, rule.RuleRight)
|
|
||||||
case "or":
|
|
||||||
err := addRuleOnRouter(router, rule.RuleLeft)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return addRuleOnRouter(router, rule.RuleRight)
|
|
||||||
default:
|
|
||||||
err := rules.CheckRule(rule)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if rule.Not {
|
|
||||||
return not(httpFuncs[rule.Matcher])(router.NewRoute(), rule.Value...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return httpFuncs[rule.Matcher](router.NewRoute(), rule.Value...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func addRuleOnRoute(route *mux.Route, rule *rules.Tree) error {
|
|
||||||
switch rule.Matcher {
|
|
||||||
case "and":
|
|
||||||
err := addRuleOnRoute(route, rule.RuleLeft)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return addRuleOnRoute(route, rule.RuleRight)
|
|
||||||
case "or":
|
|
||||||
subRouter := route.Subrouter()
|
|
||||||
|
|
||||||
err := addRuleOnRouter(subRouter, rule.RuleLeft)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return addRuleOnRouter(subRouter, rule.RuleRight)
|
|
||||||
default:
|
|
||||||
err := rules.CheckRule(rule)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if rule.Not {
|
|
||||||
return not(httpFuncs[rule.Matcher])(route, rule.Value...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return httpFuncs[rule.Matcher](route, rule.Value...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func not(m func(*mux.Route, ...string) error) func(*mux.Route, ...string) error {
|
|
||||||
return func(r *mux.Route, v ...string) error {
|
|
||||||
router := mux.NewRouter()
|
|
||||||
|
|
||||||
err := m(router.NewRoute(), v...)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
r.MatcherFunc(func(req *http.Request, ma *mux.RouteMatch) bool {
|
|
||||||
return !router.Match(req, ma)
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseDomains extract domains from rule.
|
// ParseDomains extract domains from rule.
|
||||||
func ParseDomains(rule string) ([]string, error) {
|
func ParseDomains(rule string) ([]string, error) {
|
||||||
var matchers []string
|
var matchers []string
|
||||||
|
@ -149,12 +88,12 @@ func ParseDomains(rule string) ([]string, error) {
|
||||||
|
|
||||||
parser, err := rules.NewParser(matchers)
|
parser, err := rules.NewParser(matchers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("error while creating parser: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
parse, err := parser.Parse(rule)
|
parse, err := parser.Parse(rule)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("error while parsing rule %s: %w", rule, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTree, ok := parse.(rules.TreeBuilder)
|
buildTree, ok := parse.(rules.TreeBuilder)
|
||||||
|
@ -164,3 +103,97 @@ func ParseDomains(rule string) ([]string, error) {
|
||||||
|
|
||||||
return buildTree().ParseMatchers([]string{"Host"}), nil
|
return buildTree().ParseMatchers([]string{"Host"}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// routes implements sort.Interface.
|
||||||
|
type routes []*route
|
||||||
|
|
||||||
|
// Len implements sort.Interface.
|
||||||
|
func (r routes) Len() int { return len(r) }
|
||||||
|
|
||||||
|
// Swap implements sort.Interface.
|
||||||
|
func (r routes) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
||||||
|
|
||||||
|
// Less implements sort.Interface.
|
||||||
|
func (r routes) Less(i, j int) bool { return r[i].priority > r[j].priority }
|
||||||
|
|
||||||
|
// route holds the matchers to match HTTP route,
|
||||||
|
// and the handler that will serve the request.
|
||||||
|
type route struct {
|
||||||
|
// matchers tree structure reflecting the rule.
|
||||||
|
matchers matchersTree
|
||||||
|
// handler responsible for handling the route.
|
||||||
|
handler http.Handler
|
||||||
|
// priority is used to disambiguate between two (or more) rules that would all match for a given request.
|
||||||
|
// Computed from the matching rule length, if not user-set.
|
||||||
|
priority int
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchersTree represents the matchers tree structure.
|
||||||
|
type matchersTree struct {
|
||||||
|
// matcher is a matcher func used to match HTTP request properties.
|
||||||
|
// If matcher is not nil, it means that this matcherTree is a leaf of the tree.
|
||||||
|
// It is therefore mutually exclusive with left and right.
|
||||||
|
matcher func(*http.Request) bool
|
||||||
|
// operator to combine the evaluation of left and right leaves.
|
||||||
|
operator string
|
||||||
|
// Mutually exclusive with matcher.
|
||||||
|
left *matchersTree
|
||||||
|
right *matchersTree
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *matchersTree) match(req *http.Request) bool {
|
||||||
|
if m == nil {
|
||||||
|
// This should never happen as it should have been detected during parsing.
|
||||||
|
log.Warn().Msg("Rule matcher is nil")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.matcher != nil {
|
||||||
|
return m.matcher(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch m.operator {
|
||||||
|
case "or":
|
||||||
|
return m.left.match(req) || m.right.match(req)
|
||||||
|
case "and":
|
||||||
|
return m.left.match(req) && m.right.match(req)
|
||||||
|
default:
|
||||||
|
// This should never happen as it should have been detected during parsing.
|
||||||
|
log.Warn().Str("operator", m.operator).Msg("Invalid rule operator")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *matchersTree) addRule(rule *rules.Tree) error {
|
||||||
|
switch rule.Matcher {
|
||||||
|
case "and", "or":
|
||||||
|
m.operator = rule.Matcher
|
||||||
|
m.left = &matchersTree{}
|
||||||
|
err := m.left.addRule(rule.RuleLeft)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while adding rule %s: %w", rule.Matcher, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.right = &matchersTree{}
|
||||||
|
return m.right.addRule(rule.RuleRight)
|
||||||
|
default:
|
||||||
|
err := rules.CheckRule(rule)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while checking rule %s: %w", rule.Matcher, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = httpFuncs[rule.Matcher](m, rule.Value...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while adding rule %s: %w", rule.Matcher, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule.Not {
|
||||||
|
matcherFunc := m.matcher
|
||||||
|
m.matcher = func(req *http.Request) bool {
|
||||||
|
return !matcherFunc(req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -380,8 +380,6 @@ func Test_addRoutePriority(t *testing.T) {
|
||||||
require.NoError(t, err, route.rule)
|
require.NoError(t, err, route.rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
muxer.SortRoutes()
|
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
req := testhelpers.MustNewRequest(http.MethodGet, test.path, http.NoBody)
|
req := testhelpers.MustNewRequest(http.MethodGet, test.path, http.NoBody)
|
||||||
|
|
||||||
|
|
|
@ -13,32 +13,6 @@ import (
|
||||||
"github.com/vulcand/predicate"
|
"github.com/vulcand/predicate"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseHostSNI extracts the HostSNIs declared in a rule.
|
|
||||||
// This is a first naive implementation used in TCP routing.
|
|
||||||
func ParseHostSNI(rule string) ([]string, error) {
|
|
||||||
var matchers []string
|
|
||||||
for matcher := range tcpFuncs {
|
|
||||||
matchers = append(matchers, matcher)
|
|
||||||
}
|
|
||||||
|
|
||||||
parser, err := rules.NewParser(matchers)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
parse, err := parser.Parse(rule)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTree, ok := parse.(rules.TreeBuilder)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("error while parsing rule %s", rule)
|
|
||||||
}
|
|
||||||
|
|
||||||
return buildTree().ParseMatchers([]string{"HostSNI"}), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConnData contains TCP connection metadata.
|
// ConnData contains TCP connection metadata.
|
||||||
type ConnData struct {
|
type ConnData struct {
|
||||||
serverName string
|
serverName string
|
||||||
|
@ -67,7 +41,7 @@ func NewConnData(serverName string, conn tcp.WriteCloser, alpnProtos []string) (
|
||||||
|
|
||||||
// Muxer defines a muxer that handles TCP routing with rules.
|
// Muxer defines a muxer that handles TCP routing with rules.
|
||||||
type Muxer struct {
|
type Muxer struct {
|
||||||
routes []*route
|
routes routes
|
||||||
parser predicate.Parser
|
parser predicate.Parser
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,9 +88,9 @@ func (m *Muxer) AddRoute(rule string, priority int, handler tcp.Handler) error {
|
||||||
ruleTree := buildTree()
|
ruleTree := buildTree()
|
||||||
|
|
||||||
var matchers matchersTree
|
var matchers matchersTree
|
||||||
err = addRule(&matchers, ruleTree)
|
err = matchers.addRule(ruleTree)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("error while adding rule %s: %w", rule, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var catchAll bool
|
var catchAll bool
|
||||||
|
@ -144,41 +118,7 @@ func (m *Muxer) AddRoute(rule string, priority int, handler tcp.Handler) error {
|
||||||
}
|
}
|
||||||
m.routes = append(m.routes, newRoute)
|
m.routes = append(m.routes, newRoute)
|
||||||
|
|
||||||
sort.Sort(routes(m.routes))
|
sort.Sort(m.routes)
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func addRule(tree *matchersTree, rule *rules.Tree) error {
|
|
||||||
switch rule.Matcher {
|
|
||||||
case "and", "or":
|
|
||||||
tree.operator = rule.Matcher
|
|
||||||
tree.left = &matchersTree{}
|
|
||||||
err := addRule(tree.left, rule.RuleLeft)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tree.right = &matchersTree{}
|
|
||||||
return addRule(tree.right, rule.RuleRight)
|
|
||||||
default:
|
|
||||||
err := rules.CheckRule(rule)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tcpFuncs[rule.Matcher](tree, rule.Value...)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if rule.Not {
|
|
||||||
matcherFunc := tree.matcher
|
|
||||||
tree.matcher = func(meta ConnData) bool {
|
|
||||||
return !matcherFunc(meta)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -188,6 +128,32 @@ func (m *Muxer) HasRoutes() bool {
|
||||||
return len(m.routes) > 0
|
return len(m.routes) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseHostSNI extracts the HostSNIs declared in a rule.
|
||||||
|
// This is a first naive implementation used in TCP routing.
|
||||||
|
func ParseHostSNI(rule string) ([]string, error) {
|
||||||
|
var matchers []string
|
||||||
|
for matcher := range tcpFuncs {
|
||||||
|
matchers = append(matchers, matcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
parser, err := rules.NewParser(matchers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
parse, err := parser.Parse(rule)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTree, ok := parse.(rules.TreeBuilder)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("error while parsing rule %s", rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildTree().ParseMatchers([]string{"HostSNI"}), nil
|
||||||
|
}
|
||||||
|
|
||||||
// routes implements sort.Interface.
|
// routes implements sort.Interface.
|
||||||
type routes []*route
|
type routes []*route
|
||||||
|
|
||||||
|
@ -215,14 +181,12 @@ type route struct {
|
||||||
priority int
|
priority int
|
||||||
}
|
}
|
||||||
|
|
||||||
// matcher is a matcher func used to match connection properties.
|
|
||||||
type matcher func(meta ConnData) bool
|
|
||||||
|
|
||||||
// matchersTree represents the matchers tree structure.
|
// matchersTree represents the matchers tree structure.
|
||||||
type matchersTree struct {
|
type matchersTree struct {
|
||||||
|
// matcher is a matcher func used to match connection properties.
|
||||||
// If matcher is not nil, it means that this matcherTree is a leaf of the tree.
|
// If matcher is not nil, it means that this matcherTree is a leaf of the tree.
|
||||||
// It is therefore mutually exclusive with left and right.
|
// It is therefore mutually exclusive with left and right.
|
||||||
matcher matcher
|
matcher func(ConnData) bool
|
||||||
// operator to combine the evaluation of left and right leaves.
|
// operator to combine the evaluation of left and right leaves.
|
||||||
operator string
|
operator string
|
||||||
// Mutually exclusive with matcher.
|
// Mutually exclusive with matcher.
|
||||||
|
@ -252,3 +216,37 @@ func (m *matchersTree) match(meta ConnData) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *matchersTree) addRule(rule *rules.Tree) error {
|
||||||
|
switch rule.Matcher {
|
||||||
|
case "and", "or":
|
||||||
|
m.operator = rule.Matcher
|
||||||
|
m.left = &matchersTree{}
|
||||||
|
err := m.left.addRule(rule.RuleLeft)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.right = &matchersTree{}
|
||||||
|
return m.right.addRule(rule.RuleRight)
|
||||||
|
default:
|
||||||
|
err := rules.CheckRule(rule)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tcpFuncs[rule.Matcher](m, rule.Value...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule.Not {
|
||||||
|
matcherFunc := m.matcher
|
||||||
|
m.matcher = func(meta ConnData) bool {
|
||||||
|
return !matcherFunc(meta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
---
|
||||||
|
kind: GatewayClass
|
||||||
|
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||||
|
metadata:
|
||||||
|
name: my-gateway-class
|
||||||
|
spec:
|
||||||
|
controllerName: traefik.io/gateway-controller
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: Gateway
|
||||||
|
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||||
|
metadata:
|
||||||
|
name: my-gateway
|
||||||
|
namespace: default
|
||||||
|
spec:
|
||||||
|
gatewayClassName: my-gateway-class
|
||||||
|
listeners: # Use GatewayClass defaults for listener definition.
|
||||||
|
- name: http
|
||||||
|
protocol: HTTP
|
||||||
|
port: 80
|
||||||
|
allowedRoutes:
|
||||||
|
kinds:
|
||||||
|
- kind: HTTPRoute
|
||||||
|
group: gateway.networking.k8s.io
|
||||||
|
namespaces:
|
||||||
|
from: Same
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: HTTPRoute
|
||||||
|
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||||
|
metadata:
|
||||||
|
name: http-app-1
|
||||||
|
namespace: default
|
||||||
|
spec:
|
||||||
|
parentRefs:
|
||||||
|
- name: my-gateway
|
||||||
|
kind: Gateway
|
||||||
|
group: gateway.networking.k8s.io
|
||||||
|
hostnames:
|
||||||
|
- "example.org"
|
||||||
|
rules:
|
||||||
|
- backendRefs:
|
||||||
|
- name: whoami
|
||||||
|
port: 80
|
||||||
|
weight: 1
|
||||||
|
kind: Service
|
||||||
|
group: ""
|
||||||
|
filters:
|
||||||
|
- type: RequestRedirect
|
||||||
|
requestRedirect:
|
||||||
|
scheme: https
|
||||||
|
statusCode: 301
|
|
@ -0,0 +1,52 @@
|
||||||
|
---
|
||||||
|
kind: GatewayClass
|
||||||
|
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||||
|
metadata:
|
||||||
|
name: my-gateway-class
|
||||||
|
spec:
|
||||||
|
controllerName: traefik.io/gateway-controller
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: Gateway
|
||||||
|
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||||
|
metadata:
|
||||||
|
name: my-gateway
|
||||||
|
namespace: default
|
||||||
|
spec:
|
||||||
|
gatewayClassName: my-gateway-class
|
||||||
|
listeners: # Use GatewayClass defaults for listener definition.
|
||||||
|
- name: http
|
||||||
|
protocol: HTTP
|
||||||
|
port: 80
|
||||||
|
allowedRoutes:
|
||||||
|
kinds:
|
||||||
|
- kind: HTTPRoute
|
||||||
|
group: gateway.networking.k8s.io
|
||||||
|
namespaces:
|
||||||
|
from: Same
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: HTTPRoute
|
||||||
|
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||||
|
metadata:
|
||||||
|
name: http-app-1
|
||||||
|
namespace: default
|
||||||
|
spec:
|
||||||
|
parentRefs:
|
||||||
|
- name: my-gateway
|
||||||
|
kind: Gateway
|
||||||
|
group: gateway.networking.k8s.io
|
||||||
|
hostnames:
|
||||||
|
- "example.org"
|
||||||
|
rules:
|
||||||
|
- backendRefs:
|
||||||
|
- name: whoami
|
||||||
|
port: 80
|
||||||
|
weight: 1
|
||||||
|
kind: Service
|
||||||
|
group: ""
|
||||||
|
filters:
|
||||||
|
- type: RequestRedirect
|
||||||
|
requestRedirect:
|
||||||
|
hostname: example.com
|
||||||
|
port: 443
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -756,6 +757,26 @@ func gatewayHTTPRouteToHTTPConf(ctx context.Context, ep string, listener v1alpha
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
middlewares, err := loadMiddlewares(listener, routerKey, routeRule.Filters)
|
||||||
|
if err != nil {
|
||||||
|
// update "ResolvedRefs" status true with "InvalidFilters" reason
|
||||||
|
conditions = append(conditions, metav1.Condition{
|
||||||
|
Type: string(v1alpha2.ListenerConditionResolvedRefs),
|
||||||
|
Status: metav1.ConditionFalse,
|
||||||
|
LastTransitionTime: metav1.Now(),
|
||||||
|
Reason: "InvalidFilters", // TODO check the spec if a proper reason is introduced at some point
|
||||||
|
Message: fmt.Sprintf("Cannot load HTTPRoute filter %s/%s: %v", route.Namespace, route.Name, err),
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO update the RouteStatus condition / deduplicate conditions on listener
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for middlewareName, middleware := range middlewares {
|
||||||
|
conf.HTTP.Middlewares[middlewareName] = middleware
|
||||||
|
router.Middlewares = append(router.Middlewares, middlewareName)
|
||||||
|
}
|
||||||
|
|
||||||
if len(routeRule.BackendRefs) == 0 {
|
if len(routeRule.BackendRefs) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -1663,6 +1684,85 @@ func loadTCPServices(client Client, namespace string, backendRefs []v1alpha2.Bac
|
||||||
return wrrSvc, services, nil
|
return wrrSvc, services, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadMiddlewares(listener v1alpha2.Listener, prefix string, filters []v1alpha2.HTTPRouteFilter) (map[string]*dynamic.Middleware, error) {
|
||||||
|
middlewares := make(map[string]*dynamic.Middleware)
|
||||||
|
|
||||||
|
// The spec allows for an empty string in which case we should use the
|
||||||
|
// scheme of the request which in this case is the listener scheme.
|
||||||
|
var listenerScheme string
|
||||||
|
switch listener.Protocol {
|
||||||
|
case v1alpha2.HTTPProtocolType:
|
||||||
|
listenerScheme = "http"
|
||||||
|
case v1alpha2.HTTPSProtocolType:
|
||||||
|
listenerScheme = "https"
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid listener protocol %s", listener.Protocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, filter := range filters {
|
||||||
|
var middleware *dynamic.Middleware
|
||||||
|
switch filter.Type {
|
||||||
|
case v1alpha2.HTTPRouteFilterRequestRedirect:
|
||||||
|
var err error
|
||||||
|
middleware, err = createRedirectRegexMiddleware(listenerScheme, filter.RequestRedirect)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating RedirectRegex middleware: %w", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// As per the spec:
|
||||||
|
// https://gateway-api.sigs.k8s.io/api-types/httproute/#filters-optional
|
||||||
|
// In all cases where incompatible or unsupported filters are
|
||||||
|
// specified, implementations MUST add a warning condition to
|
||||||
|
// status.
|
||||||
|
return nil, fmt.Errorf("unsupported filter %s", filter.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
middlewareName := provider.Normalize(fmt.Sprintf("%s-%s-%d", prefix, strings.ToLower(string(filter.Type)), i))
|
||||||
|
middlewares[middlewareName] = middleware
|
||||||
|
}
|
||||||
|
|
||||||
|
return middlewares, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createRedirectRegexMiddleware(scheme string, filter *v1alpha2.HTTPRequestRedirectFilter) (*dynamic.Middleware, error) {
|
||||||
|
// Use the HTTPRequestRedirectFilter scheme if defined.
|
||||||
|
filterScheme := scheme
|
||||||
|
if filter.Scheme != nil {
|
||||||
|
filterScheme = *filter.Scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
if filterScheme != "http" && filterScheme != "https" {
|
||||||
|
return nil, fmt.Errorf("invalid scheme %s", filterScheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
statusCode := http.StatusFound
|
||||||
|
if filter.StatusCode != nil {
|
||||||
|
statusCode = *filter.StatusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if statusCode != http.StatusMovedPermanently && statusCode != http.StatusFound {
|
||||||
|
return nil, fmt.Errorf("invalid status code %d", statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
port := "${port}"
|
||||||
|
if filter.Port != nil {
|
||||||
|
port = fmt.Sprintf(":%d", *filter.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
hostname := "${hostname}"
|
||||||
|
if filter.Hostname != nil && *filter.Hostname != "" {
|
||||||
|
hostname = string(*filter.Hostname)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &dynamic.Middleware{
|
||||||
|
RedirectRegex: &dynamic.RedirectRegex{
|
||||||
|
Regex: `^[a-z]+:\/\/(?P<userInfo>.+@)?(?P<hostname>\[[\w:\.]+\]|[\w\._-]+)(?P<port>:\d+)?\/(?P<path>.*)`,
|
||||||
|
Replacement: fmt.Sprintf("%s://${userinfo}%s%s/${path}", filterScheme, hostname, port),
|
||||||
|
Permanent: statusCode == http.StatusMovedPermanently,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func getProtocol(portSpec corev1.ServicePort) string {
|
func getProtocol(portSpec corev1.ServicePort) string {
|
||||||
protocol := "http"
|
protocol := "http"
|
||||||
if portSpec.Port == 443 || strings.HasPrefix(portSpec.Name, "https") {
|
if portSpec.Port == 443 || strings.HasPrefix(portSpec.Name, "https") {
|
||||||
|
|
|
@ -1555,6 +1555,141 @@ func TestLoadHTTPRoutes(t *testing.T) {
|
||||||
TLS: &dynamic.TLSConfiguration{},
|
TLS: &dynamic.TLSConfiguration{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "Simple HTTPRoute, redirect HTTP to HTTPS",
|
||||||
|
paths: []string{"services.yml", "httproute/filter_http_to_https.yml"},
|
||||||
|
entryPoints: map[string]Entrypoint{"web": {
|
||||||
|
Address: ":80",
|
||||||
|
}},
|
||||||
|
expected: &dynamic.Configuration{
|
||||||
|
UDP: &dynamic.UDPConfiguration{
|
||||||
|
Routers: map[string]*dynamic.UDPRouter{},
|
||||||
|
Services: map[string]*dynamic.UDPService{},
|
||||||
|
},
|
||||||
|
TCP: &dynamic.TCPConfiguration{
|
||||||
|
Routers: map[string]*dynamic.TCPRouter{},
|
||||||
|
Middlewares: map[string]*dynamic.TCPMiddleware{},
|
||||||
|
Services: map[string]*dynamic.TCPService{},
|
||||||
|
ServersTransports: map[string]*dynamic.TCPServersTransport{},
|
||||||
|
},
|
||||||
|
HTTP: &dynamic.HTTPConfiguration{
|
||||||
|
Routers: map[string]*dynamic.Router{
|
||||||
|
"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4": {
|
||||||
|
EntryPoints: []string{"web"},
|
||||||
|
Service: "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-wrr",
|
||||||
|
Rule: "Host(`example.org`) && PathPrefix(`/`)",
|
||||||
|
Middlewares: []string{"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-requestredirect-0"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Middlewares: map[string]*dynamic.Middleware{
|
||||||
|
"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-requestredirect-0": {
|
||||||
|
RedirectRegex: &dynamic.RedirectRegex{
|
||||||
|
Regex: "^[a-z]+:\\/\\/(?P<userInfo>.+@)?(?P<hostname>\\[[\\w:\\.]+\\]|[\\w\\._-]+)(?P<port>:\\d+)?\\/(?P<path>.*)",
|
||||||
|
Replacement: "https://${userinfo}${hostname}${port}/${path}",
|
||||||
|
Permanent: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Services: map[string]*dynamic.Service{
|
||||||
|
"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-wrr": {
|
||||||
|
Weighted: &dynamic.WeightedRoundRobin{
|
||||||
|
Services: []dynamic.WRRService{
|
||||||
|
{
|
||||||
|
Name: "default-whoami-80",
|
||||||
|
Weight: func(i int) *int { return &i }(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"default-whoami-80": {
|
||||||
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
|
Servers: []dynamic.Server{
|
||||||
|
{
|
||||||
|
URL: "http://10.10.0.1:80",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "http://10.10.0.2:80",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PassHostHeader: pointer.Bool(true),
|
||||||
|
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||||
|
FlushInterval: ptypes.Duration(100 * time.Millisecond),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ServersTransports: map[string]*dynamic.ServersTransport{},
|
||||||
|
},
|
||||||
|
TLS: &dynamic.TLSConfiguration{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Simple HTTPRoute, redirect HTTP to HTTPS with hostname",
|
||||||
|
paths: []string{"services.yml", "httproute/filter_http_to_https_with_hostname_and_port.yml"},
|
||||||
|
entryPoints: map[string]Entrypoint{"web": {
|
||||||
|
Address: ":80",
|
||||||
|
}},
|
||||||
|
expected: &dynamic.Configuration{
|
||||||
|
UDP: &dynamic.UDPConfiguration{
|
||||||
|
Routers: map[string]*dynamic.UDPRouter{},
|
||||||
|
Services: map[string]*dynamic.UDPService{},
|
||||||
|
},
|
||||||
|
TCP: &dynamic.TCPConfiguration{
|
||||||
|
Routers: map[string]*dynamic.TCPRouter{},
|
||||||
|
Middlewares: map[string]*dynamic.TCPMiddleware{},
|
||||||
|
Services: map[string]*dynamic.TCPService{},
|
||||||
|
ServersTransports: map[string]*dynamic.TCPServersTransport{},
|
||||||
|
},
|
||||||
|
HTTP: &dynamic.HTTPConfiguration{
|
||||||
|
Routers: map[string]*dynamic.Router{
|
||||||
|
"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4": {
|
||||||
|
EntryPoints: []string{"web"},
|
||||||
|
Service: "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-wrr",
|
||||||
|
Rule: "Host(`example.org`) && PathPrefix(`/`)",
|
||||||
|
Middlewares: []string{"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-requestredirect-0"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Middlewares: map[string]*dynamic.Middleware{
|
||||||
|
"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-requestredirect-0": {
|
||||||
|
RedirectRegex: &dynamic.RedirectRegex{
|
||||||
|
Regex: "^[a-z]+:\\/\\/(?P<userInfo>.+@)?(?P<hostname>\\[[\\w:\\.]+\\]|[\\w\\._-]+)(?P<port>:\\d+)?\\/(?P<path>.*)",
|
||||||
|
Replacement: "http://${userinfo}example.com:443/${path}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Services: map[string]*dynamic.Service{
|
||||||
|
"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-wrr": {
|
||||||
|
Weighted: &dynamic.WeightedRoundRobin{
|
||||||
|
Services: []dynamic.WRRService{
|
||||||
|
{
|
||||||
|
Name: "default-whoami-80",
|
||||||
|
Weight: func(i int) *int { return &i }(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"default-whoami-80": {
|
||||||
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
|
Servers: []dynamic.Server{
|
||||||
|
{
|
||||||
|
URL: "http://10.10.0.1:80",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "http://10.10.0.2:80",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PassHostHeader: pointer.Bool(true),
|
||||||
|
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||||
|
FlushInterval: ptypes.Duration(100 * time.Millisecond),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ServersTransports: map[string]*dynamic.ServersTransport{},
|
||||||
|
},
|
||||||
|
TLS: &dynamic.TLSConfiguration{},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range testCases {
|
for _, test := range testCases {
|
||||||
|
|
|
@ -50,15 +50,16 @@ type Client interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type clientWrapper struct {
|
type clientWrapper struct {
|
||||||
clientset kubernetes.Interface
|
clientset kubernetes.Interface
|
||||||
factoriesKube map[string]informers.SharedInformerFactory
|
factoriesKube map[string]informers.SharedInformerFactory
|
||||||
factoriesSecret map[string]informers.SharedInformerFactory
|
factoriesSecret map[string]informers.SharedInformerFactory
|
||||||
factoriesIngress map[string]informers.SharedInformerFactory
|
factoriesIngress map[string]informers.SharedInformerFactory
|
||||||
clusterFactory informers.SharedInformerFactory
|
clusterFactory informers.SharedInformerFactory
|
||||||
ingressLabelSelector string
|
ingressLabelSelector string
|
||||||
isNamespaceAll bool
|
isNamespaceAll bool
|
||||||
watchedNamespaces []string
|
disableIngressClassInformer bool
|
||||||
serverVersion *version.Version
|
watchedNamespaces []string
|
||||||
|
serverVersion *version.Version
|
||||||
}
|
}
|
||||||
|
|
||||||
// newInClusterClient returns a new Provider client that is expected to run
|
// newInClusterClient returns a new Provider client that is expected to run
|
||||||
|
@ -214,7 +215,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if supportsIngressClass(serverVersion) {
|
if !c.disableIngressClassInformer && supportsIngressClass(serverVersion) {
|
||||||
c.clusterFactory = informers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod)
|
c.clusterFactory = informers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod)
|
||||||
|
|
||||||
if supportsNetworkingV1Ingress(serverVersion) {
|
if supportsNetworkingV1Ingress(serverVersion) {
|
||||||
|
|
|
@ -48,6 +48,7 @@ type Provider struct {
|
||||||
ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
|
ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
|
||||||
AllowEmptyServices bool `description:"Allow creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"`
|
AllowEmptyServices bool `description:"Allow creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"`
|
||||||
AllowExternalNameServices bool `description:"Allow ExternalName services." json:"allowExternalNameServices,omitempty" toml:"allowExternalNameServices,omitempty" yaml:"allowExternalNameServices,omitempty" export:"true"`
|
AllowExternalNameServices bool `description:"Allow ExternalName services." json:"allowExternalNameServices,omitempty" toml:"allowExternalNameServices,omitempty" yaml:"allowExternalNameServices,omitempty" export:"true"`
|
||||||
|
DisableIngressClassLookup bool `description:"Disables the lookup of IngressClasses." json:"disableIngressClassLookup,omitempty" toml:"disableIngressClassLookup,omitempty" yaml:"disableIngressClassLookup,omitempty" export:"true"`
|
||||||
lastConfiguration safe.Safe
|
lastConfiguration safe.Safe
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,6 +92,7 @@ func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
cl.ingressLabelSelector = p.LabelSelector
|
cl.ingressLabelSelector = p.LabelSelector
|
||||||
|
cl.disableIngressClassInformer = p.DisableIngressClassLookup
|
||||||
return cl, nil
|
return cl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,7 +197,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
|
||||||
|
|
||||||
var ingressClasses []*networkingv1.IngressClass
|
var ingressClasses []*networkingv1.IngressClass
|
||||||
|
|
||||||
if supportsIngressClass(serverVersion) {
|
if !p.DisableIngressClassLookup && supportsIngressClass(serverVersion) {
|
||||||
ics, err := client.GetIngressClasses()
|
ics, err := client.GetIngressClasses()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Ctx(ctx).Warn().Err(err).Msg("Failed to list ingress classes")
|
log.Ctx(ctx).Warn().Err(err).Msg("Failed to list ingress classes")
|
||||||
|
|
|
@ -26,11 +26,12 @@ func Bool(v bool) *bool { return &v }
|
||||||
|
|
||||||
func TestLoadConfigurationFromIngresses(t *testing.T) {
|
func TestLoadConfigurationFromIngresses(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
ingressClass string
|
ingressClass string
|
||||||
serverVersion string
|
serverVersion string
|
||||||
expected *dynamic.Configuration
|
expected *dynamic.Configuration
|
||||||
allowEmptyServices bool
|
allowEmptyServices bool
|
||||||
|
disableIngressClassLookup bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
desc: "Empty ingresses",
|
desc: "Empty ingresses",
|
||||||
|
@ -1392,6 +1393,40 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Duplicate test case with the same fixture as the one above, but with the disableIngressClassLookup option to true.
|
||||||
|
// Showing that disabling the ingressClass discovery still allow the discovery of ingresses with ingress annotation.
|
||||||
|
desc: "v18 Ingress with ingress annotation",
|
||||||
|
serverVersion: "v1.18",
|
||||||
|
disableIngressClassLookup: true,
|
||||||
|
expected: &dynamic.Configuration{
|
||||||
|
TCP: &dynamic.TCPConfiguration{},
|
||||||
|
HTTP: &dynamic.HTTPConfiguration{
|
||||||
|
Middlewares: map[string]*dynamic.Middleware{},
|
||||||
|
Routers: map[string]*dynamic.Router{
|
||||||
|
"testing-bar": {
|
||||||
|
Rule: "PathPrefix(`/bar`)",
|
||||||
|
Service: "testing-service1-80",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Services: map[string]*dynamic.Service{
|
||||||
|
"testing-service1-80": {
|
||||||
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
|
PassHostHeader: Bool(true),
|
||||||
|
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||||
|
FlushInterval: ptypes.Duration(100 * time.Millisecond),
|
||||||
|
},
|
||||||
|
Servers: []dynamic.Server{
|
||||||
|
{
|
||||||
|
URL: "http://10.10.0.1:8080",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
desc: "v18 Ingress with ingressClasses filter",
|
desc: "v18 Ingress with ingressClasses filter",
|
||||||
serverVersion: "v1.18",
|
serverVersion: "v1.18",
|
||||||
|
@ -1424,6 +1459,22 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Duplicate test case with the same fixture as the one above, but with the disableIngressClassLookup option to true.
|
||||||
|
// Showing that disabling the ingressClass discovery avoid discovering Ingresses with an IngressClass.
|
||||||
|
desc: "v18 Ingress with ingressClasses filter",
|
||||||
|
serverVersion: "v1.18",
|
||||||
|
ingressClass: "traefik-lb2",
|
||||||
|
disableIngressClassLookup: true,
|
||||||
|
expected: &dynamic.Configuration{
|
||||||
|
TCP: &dynamic.TCPConfiguration{},
|
||||||
|
HTTP: &dynamic.HTTPConfiguration{
|
||||||
|
Middlewares: map[string]*dynamic.Middleware{},
|
||||||
|
Routers: map[string]*dynamic.Router{},
|
||||||
|
Services: map[string]*dynamic.Service{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
desc: "v19 Ingress with prefix pathType",
|
desc: "v19 Ingress with prefix pathType",
|
||||||
serverVersion: "v1.19",
|
serverVersion: "v1.19",
|
||||||
|
@ -1610,6 +1661,39 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Duplicate test case with the same fixture as the one above, but with the disableIngressClassLookup option to true.
|
||||||
|
// Showing that disabling the ingressClass discovery still allow the discovery of ingresses with ingress annotation.
|
||||||
|
desc: "v19 Ingress with ingress annotation",
|
||||||
|
serverVersion: "v1.19",
|
||||||
|
expected: &dynamic.Configuration{
|
||||||
|
TCP: &dynamic.TCPConfiguration{},
|
||||||
|
HTTP: &dynamic.HTTPConfiguration{
|
||||||
|
Middlewares: map[string]*dynamic.Middleware{},
|
||||||
|
Routers: map[string]*dynamic.Router{
|
||||||
|
"testing-bar": {
|
||||||
|
Rule: "PathPrefix(`/bar`)",
|
||||||
|
Service: "testing-service1-80",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Services: map[string]*dynamic.Service{
|
||||||
|
"testing-service1-80": {
|
||||||
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
|
PassHostHeader: Bool(true),
|
||||||
|
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||||
|
FlushInterval: ptypes.Duration(100 * time.Millisecond),
|
||||||
|
},
|
||||||
|
Servers: []dynamic.Server{
|
||||||
|
{
|
||||||
|
URL: "http://10.10.0.1:8080",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
desc: "v19 Ingress with ingressClass",
|
desc: "v19 Ingress with ingressClass",
|
||||||
serverVersion: "v1.19",
|
serverVersion: "v1.19",
|
||||||
|
@ -1641,6 +1725,21 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Duplicate test case with the same fixture as the one above, but with the disableIngressClassLookup option to true.
|
||||||
|
// Showing that disabling the ingressClass discovery avoid discovering Ingresses with an IngressClass.
|
||||||
|
desc: "v19 Ingress with ingressClass",
|
||||||
|
disableIngressClassLookup: true,
|
||||||
|
serverVersion: "v1.19",
|
||||||
|
expected: &dynamic.Configuration{
|
||||||
|
TCP: &dynamic.TCPConfiguration{},
|
||||||
|
HTTP: &dynamic.HTTPConfiguration{
|
||||||
|
Middlewares: map[string]*dynamic.Middleware{},
|
||||||
|
Routers: map[string]*dynamic.Router{},
|
||||||
|
Services: map[string]*dynamic.Service{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
desc: "v19 Ingress with ingressClassv1",
|
desc: "v19 Ingress with ingressClassv1",
|
||||||
serverVersion: "v1.19",
|
serverVersion: "v1.19",
|
||||||
|
@ -1783,7 +1882,11 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
clientMock := newClientMock(serverVersion, paths...)
|
clientMock := newClientMock(serverVersion, paths...)
|
||||||
p := Provider{IngressClass: test.ingressClass, AllowEmptyServices: test.allowEmptyServices}
|
p := Provider{
|
||||||
|
IngressClass: test.ingressClass,
|
||||||
|
AllowEmptyServices: test.allowEmptyServices,
|
||||||
|
DisableIngressClassLookup: test.disableIngressClassLookup,
|
||||||
|
}
|
||||||
conf := p.loadConfigurationFromIngresses(context.Background(), clientMock)
|
conf := p.loadConfigurationFromIngresses(context.Background(), clientMock)
|
||||||
|
|
||||||
assert.Equal(t, test.expected, conf)
|
assert.Equal(t, test.expected, conf)
|
||||||
|
|
|
@ -134,8 +134,6 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
muxer.SortRoutes()
|
|
||||||
|
|
||||||
chain := alice.New()
|
chain := alice.New()
|
||||||
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
|
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
|
||||||
return recovery.New(ctx, next)
|
return recovery.New(ctx, next)
|
||||||
|
|
Loading…
Reference in a new issue