Make the behavior of prefix matching in Ingress consistent with Kubernetes doc
This commit is contained in:
parent
8c6ed23c5f
commit
4790e4910f
10 changed files with 262 additions and 2 deletions
|
|
@ -529,6 +529,29 @@ providers:
|
|||
--providers.kubernetesingress.nativeLBByDefault=true
|
||||
```
|
||||
|
||||
### `strictPrefixMatching`
|
||||
|
||||
_Optional, Default: false_
|
||||
|
||||
Make prefix matching strictly comply with the Kubernetes Ingress specification (path-element-wise matching instead of character-by-character string matching). For example, a PathPrefix of `/foo` will match `/foo`, `/foo/`, and `/foo/bar` but not `/foobar`.
|
||||
|
||||
```yaml tab="File (YAML)"
|
||||
providers:
|
||||
kubernetesIngress:
|
||||
strictPrefixMatching: true
|
||||
# ...
|
||||
```
|
||||
|
||||
```toml tab="File (TOML)"
|
||||
[providers.kubernetesIngress]
|
||||
strictPrefixMatching = true
|
||||
# ...
|
||||
```
|
||||
|
||||
```bash tab="CLI"
|
||||
--providers.kubernetesingress.strictPrefixMatching=true
|
||||
```
|
||||
|
||||
### Further
|
||||
|
||||
To learn more about the various aspects of the Ingress specification that Traefik supports,
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ which in turn creates the resulting routers, services, handlers, etc.
|
|||
| `providers.kubernetesIngress.allowExternalNameServices` | Allows the `Ingress` to reference ExternalName services. | false | No |
|
||||
| `providers.kubernetesIngress.nativeLBByDefault` | Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik for every `Ingress` by default.<br />It can br overridden in the [`ServerTransport`](../../../../routing/services/index.md#serverstransport). | false | No |
|
||||
| `providers.kubernetesIngress.disableClusterScopeResources` | Prevent from discovering cluster scope resources (`IngressClass` and `Nodes`).<br />By doing so, it alleviates the requirement of giving Traefik the rights to look up for cluster resources.<br />Furthermore, Traefik will not handle Ingresses with IngressClass references, therefore such Ingresses will be ignored (please note that annotations are not affected by this option).<br />This will also prevent from using the `NodePortLB` options on services. | false | No |
|
||||
| `providers.kubernetesIngress.strictPrefixMatching` | Make prefix matching strictly comply with the Kubernetes Ingress specification (path-element-wise matching instead of character-by-character string matching). For example, a PathPrefix of `/foo` will match `/foo`, `/foo/`, and `/foo/bar` but not `/foobar`. | false | No |
|
||||
|
||||
<!-- markdownlint-enable MD013 -->
|
||||
|
||||
|
|
|
|||
|
|
@ -1032,6 +1032,9 @@ Kubernetes namespaces.
|
|||
`--providers.kubernetesingress.nativelbbydefault`:
|
||||
Defines whether to use Native Kubernetes load-balancing mode by default. (Default: ```false```)
|
||||
|
||||
`--providers.kubernetesingress.strictprefixmatching`:
|
||||
Make prefix matching strictly comply with the Kubernetes Ingress specification (path-element-wise matching instead of character-by-character string matching). (Default: ```false```)
|
||||
|
||||
`--providers.kubernetesingress.throttleduration`:
|
||||
Ingress refresh throttle duration (Default: ```0```)
|
||||
|
||||
|
|
|
|||
|
|
@ -1032,6 +1032,9 @@ Kubernetes namespaces.
|
|||
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_NATIVELBBYDEFAULT`:
|
||||
Defines whether to use Native Kubernetes load-balancing mode by default. (Default: ```false```)
|
||||
|
||||
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_STRICTPREFIXMATCHING`:
|
||||
Make prefix matching strictly comply with the Kubernetes Ingress specification (path-element-wise matching instead of character-by-character string matching). (Default: ```false```)
|
||||
|
||||
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_THROTTLEDURATION`:
|
||||
Ingress refresh throttle duration (Default: ```0```)
|
||||
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@
|
|||
disableIngressClassLookup = true
|
||||
disableClusterScopeResources = true
|
||||
nativeLBByDefault = true
|
||||
strictPrefixMatching = true
|
||||
[providers.kubernetesIngress.ingressEndpoint]
|
||||
ip = "foobar"
|
||||
hostname = "foobar"
|
||||
|
|
|
|||
|
|
@ -157,6 +157,7 @@ providers:
|
|||
disableIngressClassLookup: true
|
||||
disableClusterScopeResources: true
|
||||
nativeLBByDefault: true
|
||||
strictPrefixMatching: true
|
||||
kubernetesCRD:
|
||||
endpoint: foobar
|
||||
token: foobar
|
||||
|
|
|
|||
|
|
@ -439,7 +439,7 @@ If the Kubernetes cluster version is 1.18+,
|
|||
the new `pathType` property can be leveraged to define the rules matchers:
|
||||
|
||||
- `Exact`: This path type forces the rule matcher to `Path`
|
||||
- `Prefix`: This path type forces the rule matcher to `PathPrefix`
|
||||
- `Prefix`: This path type forces the rule matcher to `PathPrefix`. Note that if you want the matching behavior to strictly comply with Kubernetes Ingress specification (request path is matched on an element-by-element basis), consider enabling [`strictPrefixMatching`](../../providers/kubernetes-ingress.md#strictprefixmatching) in the Ingress Provider configuration.
|
||||
|
||||
Please see [this documentation](https://kubernetes.io/docs/concepts/services-networking/ingress/#path-types) for more information.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
kind: Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: ""
|
||||
namespace: testing
|
||||
|
||||
spec:
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- path: /bar
|
||||
backend:
|
||||
service:
|
||||
name: service1
|
||||
port:
|
||||
number: 80
|
||||
pathType: Prefix
|
||||
|
||||
---
|
||||
kind: Service
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: service1
|
||||
namespace: testing
|
||||
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
clusterIP: 10.0.0.1
|
||||
|
||||
---
|
||||
kind: EndpointSlice
|
||||
apiVersion: discovery.k8s.io/v1
|
||||
metadata:
|
||||
name: service1-abc
|
||||
namespace: testing
|
||||
labels:
|
||||
kubernetes.io/service-name: service1
|
||||
|
||||
addressType: IPv4
|
||||
ports:
|
||||
- port: 8080
|
||||
name: ""
|
||||
endpoints:
|
||||
- addresses:
|
||||
- 10.10.0.1
|
||||
- 10.21.0.1
|
||||
conditions:
|
||||
ready: true
|
||||
|
|
@ -56,6 +56,7 @@ type Provider struct {
|
|||
DisableIngressClassLookup bool `description:"Disables the lookup of IngressClasses (Deprecated, please use DisableClusterScopeResources)." json:"disableIngressClassLookup,omitempty" toml:"disableIngressClassLookup,omitempty" yaml:"disableIngressClassLookup,omitempty" export:"true"`
|
||||
DisableClusterScopeResources bool `description:"Disables the lookup of cluster scope resources (incompatible with IngressClasses and NodePortLB enabled services)." json:"disableClusterScopeResources,omitempty" toml:"disableClusterScopeResources,omitempty" yaml:"disableClusterScopeResources,omitempty" export:"true"`
|
||||
NativeLBByDefault bool `description:"Defines whether to use Native Kubernetes load-balancing mode by default." json:"nativeLBByDefault,omitempty" toml:"nativeLBByDefault,omitempty" yaml:"nativeLBByDefault,omitempty" export:"true"`
|
||||
StrictPrefixMatching bool `description:"Make prefix matching strictly comply with the Kubernetes Ingress specification (path-element-wise matching instead of character-by-character string matching)." json:"strictPrefixMatching,omitempty" toml:"strictPrefixMatching,omitempty" yaml:"strictPrefixMatching,omitempty" export:"true"`
|
||||
|
||||
// The default rule syntax is initialized with the configuration defined by the user with the core.DefaultRuleSyntax option.
|
||||
DefaultRuleSyntax string `json:"-" toml:"-" yaml:"-" label:"-" file:"-"`
|
||||
|
|
@ -698,7 +699,7 @@ func (p *Provider) loadRouter(rule netv1.IngressRule, pa netv1.HTTPIngressPath,
|
|||
matcher = "Path"
|
||||
}
|
||||
|
||||
rules = append(rules, fmt.Sprintf("%s(`%s`)", matcher, pa.Path))
|
||||
rules = append(rules, buildRule(p.StrictPrefixMatching, matcher, pa.Path))
|
||||
}
|
||||
|
||||
rt.Rule = strings.Join(rules, " && ")
|
||||
|
|
@ -844,6 +845,41 @@ func makeRouterKeyWithHash(key, rule string) (string, error) {
|
|||
return dupKey, nil
|
||||
}
|
||||
|
||||
func buildRule(strictPrefixMatching bool, matcher string, path string) string {
|
||||
// When enabled, strictPrefixMatching ensures that prefix matching follows
|
||||
// the Kubernetes Ingress spec (path-element-wise instead of character-wise).
|
||||
if strictPrefixMatching && matcher == "PathPrefix" {
|
||||
// According to
|
||||
// https://kubernetes.io/docs/concepts/services-networking/ingress/#examples,
|
||||
// "/v12" should not match "/v1".
|
||||
//
|
||||
// Traefik's default PathPrefix matcher performs a character-wise prefix match,
|
||||
// unlike Kubernetes which matches path elements. To mimic Kubernetes behavior,
|
||||
// we will use Path and PathPrefix to replicate element-wise behavior.
|
||||
//
|
||||
// "PathPrefix" in Kubernetes Gateway API is semantically equivalent to the "Prefix" path type in the
|
||||
// Kubernetes Ingress API.
|
||||
return buildStrictPrefixMatchingRule(path)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s(`%s`)", matcher, path)
|
||||
}
|
||||
|
||||
// buildStrictPrefixMatchingRule is a helper function to build a path prefix rule that matches path prefix split by `/`.
|
||||
// For example, the paths `/abc`, `/abc/`, and `/abc/def` would all match the prefix `/abc`,
|
||||
// but the path `/abcd` would not. See TestStrictPrefixMatchingRule() for more examples.
|
||||
//
|
||||
// "PathPrefix" in Kubernetes Gateway API is semantically equivalent to the "Prefix" path type in the
|
||||
// Kubernetes Ingress API.
|
||||
func buildStrictPrefixMatchingRule(path string) string {
|
||||
if path == "/" {
|
||||
return "PathPrefix(`/`)"
|
||||
}
|
||||
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
return fmt.Sprintf("(Path(`%[1]s`) || PathPrefix(`%[1]s/`))", path)
|
||||
}
|
||||
|
||||
func throttleEvents(ctx context.Context, throttleDuration time.Duration, pool *safe.Pool, eventsChan <-chan interface{}) chan interface{} {
|
||||
if throttleDuration == 0 {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ package ingress
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
|
@ -14,6 +17,7 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
ptypes "github.com/traefik/paerser/types"
|
||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||
traefikhttp "github.com/traefik/traefik/v3/pkg/muxer/http"
|
||||
"github.com/traefik/traefik/v3/pkg/provider"
|
||||
"github.com/traefik/traefik/v3/pkg/provider/kubernetes/k8s"
|
||||
"github.com/traefik/traefik/v3/pkg/tls"
|
||||
|
|
@ -38,6 +42,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
|
|||
disableIngressClassLookup bool
|
||||
disableClusterScopeResources bool
|
||||
defaultRuleSyntax string
|
||||
strictPrefixMatching bool
|
||||
}{
|
||||
{
|
||||
desc: "Empty ingresses",
|
||||
|
|
@ -1621,6 +1626,40 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Ingress with strict prefix matching",
|
||||
expected: &dynamic.Configuration{
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"testing-bar": {
|
||||
Rule: "(Path(`/bar`) || PathPrefix(`/bar/`))",
|
||||
Service: "testing-service1-80",
|
||||
},
|
||||
},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"testing-service1-80": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Strategy: dynamic.BalancerStrategyWRR,
|
||||
PassHostHeader: pointer(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: ptypes.Duration(100 * time.Millisecond),
|
||||
},
|
||||
Servers: []dynamic.Server{
|
||||
{
|
||||
URL: "http://10.10.0.1:8080",
|
||||
},
|
||||
{
|
||||
URL: "http://10.21.0.1:8080",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
strictPrefixMatching: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
|
|
@ -1634,6 +1673,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
|
|||
DisableIngressClassLookup: test.disableIngressClassLookup,
|
||||
DisableClusterScopeResources: test.disableClusterScopeResources,
|
||||
DefaultRuleSyntax: test.defaultRuleSyntax,
|
||||
StrictPrefixMatching: test.strictPrefixMatching,
|
||||
}
|
||||
conf := p.loadConfigurationFromIngresses(context.Background(), clientMock)
|
||||
|
||||
|
|
@ -2256,3 +2296,106 @@ func readResources(t *testing.T, paths []string) []runtime.Object {
|
|||
|
||||
return k8sObjects
|
||||
}
|
||||
|
||||
func TestStrictPrefixMatchingRule(t *testing.T) {
|
||||
tests := []struct {
|
||||
path string
|
||||
requestPath string
|
||||
match bool
|
||||
}{ // The tests are taken from https://kubernetes.io/docs/concepts/services-networking/ingress/#examples
|
||||
{
|
||||
path: "/foo",
|
||||
requestPath: "/foo",
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
path: "/foo",
|
||||
requestPath: "/foo/",
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
path: "/foo/",
|
||||
requestPath: "/foo",
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
path: "/foo/",
|
||||
requestPath: "/foo/",
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
path: "/aaa/bb",
|
||||
requestPath: "/aaa/bbb",
|
||||
match: false,
|
||||
},
|
||||
{
|
||||
path: "/aaa/bbb",
|
||||
requestPath: "/aaa/bbb",
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
path: "/aaa/bbb/",
|
||||
requestPath: "/aaa/bbb",
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
path: "/aaa/bbb",
|
||||
requestPath: "/aaa/bbb/",
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
path: "/aaa/bbb",
|
||||
requestPath: "/aaa/bbb/ccc",
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
path: "/aaa/bbb",
|
||||
requestPath: "/aaa/bbbxyz",
|
||||
match: false,
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
requestPath: "/aaa/ccc",
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
path: "/aaa",
|
||||
requestPath: "/aaa/ccc",
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
path: "/...",
|
||||
requestPath: "/aaa",
|
||||
match: false,
|
||||
},
|
||||
{
|
||||
path: "/...",
|
||||
requestPath: "/.../",
|
||||
match: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(fmt.Sprintf("Prefix match case %s", tt.path), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
muxer, err := traefikhttp.NewMuxer()
|
||||
require.NoError(t, err)
|
||||
|
||||
rule := buildStrictPrefixMatchingRule(tt.path)
|
||||
err = muxer.AddRoute(rule, "", 0, handler)
|
||||
require.NoError(t, err)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, tt.requestPath, http.NoBody)
|
||||
muxer.ServeHTTP(w, req)
|
||||
|
||||
if tt.match {
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
} else {
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue