Merge branch 'v3.0' of github.com:traefik/traefik

This commit is contained in:
baalajimaestro 2022-12-24 19:10:55 +05:30
commit 7f25d6de67
Signed by: baalajimaestro
GPG key ID: F93C394FE9BBAFD5
23 changed files with 901 additions and 226 deletions

View file

@ -344,6 +344,35 @@ providers:
--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`
#### `hostname`

View file

@ -735,6 +735,9 @@ Allow ExternalName services. (Default: ```false```)
`--providers.kubernetesingress.certauthfilepath`:
Kubernetes certificate authority file path (not needed for in-cluster client).
`--providers.kubernetesingress.disableingressclasslookup`:
Disables the lookup of IngressClasses. (Default: ```false```)
`--providers.kubernetesingress.endpoint`:
Kubernetes server endpoint (required for external cluster client).

View file

@ -735,6 +735,9 @@ Allow ExternalName services. (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_CERTAUTHFILEPATH`:
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`:
Kubernetes server endpoint (required for external cluster client).

View file

@ -108,6 +108,7 @@
throttleDuration = "42s"
allowEmptyServices = true
allowExternalNameServices = true
disableIngressClassLookup = true
[providers.kubernetesIngress.ingressEndpoint]
ip = "foobar"
hostname = "foobar"

View file

@ -117,6 +117,7 @@ providers:
throttleDuration: 42s
allowEmptyServices: true
allowExternalNameServices: true
disableIngressClassLookup: true
ingressEndpoint:
ip: foobar
hostname: foobar

1
go.mod
View file

@ -385,7 +385,6 @@ require (
replace (
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/gorilla/mux => github.com/containous/mux v0.0.0-20220627093034-b2dd784e613f
github.com/mailgun/minheap => github.com/containous/minheap v0.0.0-20190809180810-6e71eb837595
)

7
go.sum
View file

@ -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/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/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/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
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/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/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 v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=

View 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

View file

@ -128,6 +128,17 @@ func (s *K8sSuite) TestIngressclass(c *check.C) {
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) {
err := try.GetRequest("http://127.0.0.1:"+apiPort+"/api/entrypoints", 20*time.Second, try.BodyContains(`"name":"web"`))
c.Assert(err, checker.IsNil)

View 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"
}
}
}

View file

@ -7,14 +7,13 @@ import (
"strings"
"unicode/utf8"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v2/pkg/ip"
"github.com/traefik/traefik/v2/pkg/middlewares/requestdecorator"
"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),
"Method": expectNParameters(method, 1),
"Host": expectNParameters(host, 1),
@ -28,17 +27,17 @@ var httpFuncs = map[string]func(*mux.Route, ...string) error{
"QueryRegexp": expectNParameters(queryRegexp, 1, 2),
}
func expectNParameters(fn func(*mux.Route, ...string) error, n ...int) func(*mux.Route, ...string) error {
return func(route *mux.Route, s ...string) error {
func expectNParameters(fn func(*matchersTree, ...string) error, n ...int) func(*matchersTree, ...string) error {
return func(tree *matchersTree, s ...string) error {
if !slices.Contains(n, len(s)) {
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)
if err != nil {
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{}
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
tree.matcher = func(req *http.Request) bool {
ok, err := checker.Contains(strategy.GetIP(req))
if err != nil {
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 nil
}
func method(route *mux.Route, methods ...string) error {
return route.Methods(methods...).GetError()
func method(tree *matchersTree, methods ...string) error {
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]
if !IsASCII(host) {
@ -72,7 +77,7 @@ func host(route *mux.Route, hosts ...string) error {
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())
if len(reqHost) == 0 {
return false
@ -104,12 +109,12 @@ func host(route *mux.Route, hosts ...string) error {
}
return false
})
}
return nil
}
func hostRegexp(route *mux.Route, hosts ...string) error {
func hostRegexp(tree *matchersTree, hosts ...string) error {
host := hosts[0]
if !IsASCII(host) {
@ -121,29 +126,29 @@ func hostRegexp(route *mux.Route, hosts ...string) error {
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())) ||
re.MatchString(requestdecorator.GetCNAMEFlatten(req.Context()))
})
}
return nil
}
func path(route *mux.Route, paths ...string) error {
func path(tree *matchersTree, paths ...string) error {
path := paths[0]
if !strings.HasPrefix(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 nil
}
func pathRegexp(route *mux.Route, paths ...string) error {
func pathRegexp(tree *matchersTree, paths ...string) error {
path := paths[0]
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)
}
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
tree.matcher = func(req *http.Request) bool {
return re.MatchString(req.URL.Path)
})
}
return nil
}
func pathPrefix(route *mux.Route, paths ...string) error {
func pathPrefix(tree *matchersTree, paths ...string) error {
path := paths[0]
if !strings.HasPrefix(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 nil
}
func header(route *mux.Route, headers ...string) error {
return route.Headers(headers...).GetError()
func header(tree *matchersTree, headers ...string) error {
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 {
return route.HeadersRegexp(headers...).GetError()
func headerRegexp(tree *matchersTree, headers ...string) error {
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]
var value string
@ -188,21 +222,21 @@ func query(route *mux.Route, queries ...string) error {
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]
if !ok {
return false
}
return slices.Contains(values, value)
})
}
return nil
}
func queryRegexp(route *mux.Route, queries ...string) error {
func queryRegexp(tree *matchersTree, queries ...string) error {
if len(queries) == 1 {
return query(route, queries...)
return query(tree, queries...)
}
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)
}
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
tree.matcher = func(req *http.Request) bool {
values, ok := req.URL.Query()[key]
if !ok {
return false
@ -223,7 +257,7 @@ func queryRegexp(route *mux.Route, queries ...string) error {
})
return idx >= 0
})
}
return nil
}

View file

@ -3,6 +3,7 @@ package http
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@ -121,16 +122,18 @@ func TestMethodMatcher(t *testing.T) {
desc: "valid Method matcher",
rule: "Method(`GET`)",
expected: map[string]int{
http.MethodGet: http.StatusOK,
http.MethodPost: http.StatusMethodNotAllowed,
http.MethodGet: http.StatusOK,
http.MethodPost: http.StatusNotFound,
strings.ToLower(http.MethodGet): http.StatusNotFound,
},
},
{
desc: "valid Method matcher (lower case)",
rule: "Method(`get`)",
expected: map[string]int{
http.MethodGet: http.StatusOK,
http.MethodPost: http.StatusMethodNotAllowed,
http.MethodGet: http.StatusOK,
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:8080": http.StatusOK,
"https://example.com/path": http.StatusOK,
"https://EXAMPLE.COM/path": http.StatusOK,
"https://example.org": 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,
},
},
{
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 {
@ -747,6 +762,18 @@ func TestHeaderRegexpMatcher(t *testing.T) {
{"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",
rule: "HeaderRegexp(`X-Forwarded-Proto`, `http{secure:s?}`)",

View file

@ -3,15 +3,16 @@ package http
import (
"fmt"
"net/http"
"sort"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v2/pkg/rules"
"github.com/vulcand/predicate"
)
// Muxer handles routing with rules.
type Muxer struct {
*mux.Router
routes routes
parser predicate.Parser
}
@ -24,18 +25,30 @@ func NewMuxer() (*Muxer, error) {
parser, err := rules.NewParser(matchers)
if err != nil {
return nil, err
return nil, fmt.Errorf("error while creating parser: %w", err)
}
return &Muxer{
Router: mux.NewRouter().SkipClean(true),
parser: parser,
}, 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.
func (r *Muxer) AddRoute(rule string, priority int, handler http.Handler) error {
parse, err := r.parser.Parse(rule)
func (m *Muxer) AddRoute(rule string, priority int, handler http.Handler) error {
parse, err := m.parser.Parse(rule)
if err != nil {
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)
}
var matchers matchersTree
err = matchers.addRule(buildTree())
if err != nil {
return fmt.Errorf("error while adding rule %s: %w", rule, err)
}
if priority == 0 {
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())
if err != nil {
route.BuildOnly()
return err
}
sort.Sort(m.routes)
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.
func ParseDomains(rule string) ([]string, error) {
var matchers []string
@ -149,12 +88,12 @@ func ParseDomains(rule string) ([]string, error) {
parser, err := rules.NewParser(matchers)
if err != nil {
return nil, err
return nil, fmt.Errorf("error while creating parser: %w", err)
}
parse, err := parser.Parse(rule)
if err != nil {
return nil, err
return nil, fmt.Errorf("error while parsing rule %s: %w", rule, err)
}
buildTree, ok := parse.(rules.TreeBuilder)
@ -164,3 +103,97 @@ func ParseDomains(rule string) ([]string, error) {
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
}

View file

@ -380,8 +380,6 @@ func Test_addRoutePriority(t *testing.T) {
require.NoError(t, err, route.rule)
}
muxer.SortRoutes()
w := httptest.NewRecorder()
req := testhelpers.MustNewRequest(http.MethodGet, test.path, http.NoBody)

View file

@ -13,32 +13,6 @@ import (
"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.
type ConnData struct {
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.
type Muxer struct {
routes []*route
routes routes
parser predicate.Parser
}
@ -114,9 +88,9 @@ func (m *Muxer) AddRoute(rule string, priority int, handler tcp.Handler) error {
ruleTree := buildTree()
var matchers matchersTree
err = addRule(&matchers, ruleTree)
err = matchers.addRule(ruleTree)
if err != nil {
return err
return fmt.Errorf("error while adding rule %s: %w", rule, err)
}
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)
sort.Sort(routes(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)
}
}
}
sort.Sort(m.routes)
return nil
}
@ -188,6 +128,32 @@ func (m *Muxer) HasRoutes() bool {
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.
type routes []*route
@ -215,14 +181,12 @@ type route struct {
priority int
}
// matcher is a matcher func used to match connection properties.
type matcher func(meta ConnData) bool
// matchersTree represents the matchers tree structure.
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.
// 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 string
// Mutually exclusive with matcher.
@ -252,3 +216,37 @@ func (m *matchersTree) match(meta ConnData) bool {
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
}

View file

@ -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

View file

@ -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

View file

@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"net"
"net/http"
"os"
"regexp"
"sort"
@ -756,6 +757,26 @@ func gatewayHTTPRouteToHTTPConf(ctx context.Context, ep string, listener v1alpha
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 {
continue
}
@ -1663,6 +1684,85 @@ func loadTCPServices(client Client, namespace string, backendRefs []v1alpha2.Bac
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 {
protocol := "http"
if portSpec.Port == 443 || strings.HasPrefix(portSpec.Name, "https") {

View file

@ -1555,6 +1555,141 @@ func TestLoadHTTPRoutes(t *testing.T) {
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 {

View file

@ -50,15 +50,16 @@ type Client interface {
}
type clientWrapper struct {
clientset kubernetes.Interface
factoriesKube map[string]informers.SharedInformerFactory
factoriesSecret map[string]informers.SharedInformerFactory
factoriesIngress map[string]informers.SharedInformerFactory
clusterFactory informers.SharedInformerFactory
ingressLabelSelector string
isNamespaceAll bool
watchedNamespaces []string
serverVersion *version.Version
clientset kubernetes.Interface
factoriesKube map[string]informers.SharedInformerFactory
factoriesSecret map[string]informers.SharedInformerFactory
factoriesIngress map[string]informers.SharedInformerFactory
clusterFactory informers.SharedInformerFactory
ingressLabelSelector string
isNamespaceAll bool
disableIngressClassInformer bool
watchedNamespaces []string
serverVersion *version.Version
}
// 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)
if supportsNetworkingV1Ingress(serverVersion) {

View file

@ -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"`
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"`
DisableIngressClassLookup bool `description:"Disables the lookup of IngressClasses." json:"disableIngressClassLookup,omitempty" toml:"disableIngressClassLookup,omitempty" yaml:"disableIngressClassLookup,omitempty" export:"true"`
lastConfiguration safe.Safe
}
@ -91,6 +92,7 @@ func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) {
}
cl.ingressLabelSelector = p.LabelSelector
cl.disableIngressClassInformer = p.DisableIngressClassLookup
return cl, nil
}
@ -195,7 +197,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
var ingressClasses []*networkingv1.IngressClass
if supportsIngressClass(serverVersion) {
if !p.DisableIngressClassLookup && supportsIngressClass(serverVersion) {
ics, err := client.GetIngressClasses()
if err != nil {
log.Ctx(ctx).Warn().Err(err).Msg("Failed to list ingress classes")

View file

@ -26,11 +26,12 @@ func Bool(v bool) *bool { return &v }
func TestLoadConfigurationFromIngresses(t *testing.T) {
testCases := []struct {
desc string
ingressClass string
serverVersion string
expected *dynamic.Configuration
allowEmptyServices bool
desc string
ingressClass string
serverVersion string
expected *dynamic.Configuration
allowEmptyServices bool
disableIngressClassLookup bool
}{
{
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",
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",
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",
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",
serverVersion: "v1.19",
@ -1783,7 +1882,11 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
}
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)
assert.Equal(t, test.expected, conf)

View file

@ -134,8 +134,6 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
}
}
muxer.SortRoutes()
chain := alice.New()
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return recovery.New(ctx, next)