mirror of
https://github.com/traefik/traefik.git
synced 2026-06-19 07:36:07 +00:00
Add providers routing precedence configuration
Co-authored-by: Mathis Urien <contact.lbf38@gmail.com>
This commit is contained in:
@@ -448,6 +448,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
|
||||
| <a id="opt-providers-nomad-throttleduration" href="#opt-providers-nomad-throttleduration" title="#opt-providers-nomad-throttleduration">providers.nomad.throttleduration</a> | Watch throttle duration. | 0 |
|
||||
| <a id="opt-providers-nomad-watch" href="#opt-providers-nomad-watch" title="#opt-providers-nomad-watch">providers.nomad.watch</a> | Watch Nomad Service events. | false |
|
||||
| <a id="opt-providers-plugin-name" href="#opt-providers-plugin-name" title="#opt-providers-plugin-name">providers.plugin._name_</a> | Plugins configuration. | |
|
||||
| <a id="opt-providers-precedence" href="#opt-providers-precedence" title="#opt-providers-precedence">providers.precedence</a> | Defines the routing precedence between providers. | kubernetesgateway, kubernetescrd, kubernetes, kubernetesingressnginx, swarm, docker, file, redis, knative, consul, consulcatalog, nomad, etcd, ecs, http, zookeeper, rest |
|
||||
| <a id="opt-providers-providersthrottleduration" href="#opt-providers-providersthrottleduration" title="#opt-providers-providersthrottleduration">providers.providersthrottleduration</a> | Backends throttle duration: minimum duration between 2 events from providers before applying a new configuration. It avoids unnecessary reloads if multiples events are sent in a short amount of time. | 2 |
|
||||
| <a id="opt-providers-redis" href="#opt-providers-redis" title="#opt-providers-redis">providers.redis</a> | Enables Redis provider. | false |
|
||||
| <a id="opt-providers-redis-db" href="#opt-providers-redis-db" title="#opt-providers-redis-db">providers.redis.db</a> | Database to be selected after connecting to the server. | 0 |
|
||||
|
||||
@@ -142,6 +142,64 @@ you can do so in two different ways:
|
||||
- the generic configuration option `exposedByDefault`,
|
||||
- a finer granularity mechanism based on constraints.
|
||||
|
||||
## Providers Precedence
|
||||
|
||||
### `providers.precedence`
|
||||
|
||||
_Optional_
|
||||
|
||||
When two routers from **different providers** define the same rule with equal numeric [priority](../../routing-configuration/http/routing/rules-and-priority.md#priority-calculation),
|
||||
the `precedence` option determines which provider's route takes precedence.
|
||||
|
||||
The list is ordered from highest to lowest precedence: a provider listed first wins over providers listed later.
|
||||
|
||||
```yaml tab="File (YAML)"
|
||||
providers:
|
||||
precedence:
|
||||
- kubernetescrd
|
||||
- kubernetes
|
||||
- file
|
||||
```
|
||||
|
||||
```toml tab="File (TOML)"
|
||||
[providers]
|
||||
precedence = ["kubernetescrd", "kubernetes", "file"]
|
||||
```
|
||||
|
||||
```bash tab="CLI"
|
||||
--providers.precedence=kubernetescrd,kubernetes,file
|
||||
```
|
||||
|
||||
#### Default precedence
|
||||
|
||||
When `precedence` is not set, Traefik uses the following default order (highest precedence first):
|
||||
|
||||
| Position | Provider name |
|
||||
|----------|--------------------------|
|
||||
| <a id="opt-1" href="#opt-1" title="#opt-1">1</a> | `kubernetesgateway` |
|
||||
| <a id="opt-2" href="#opt-2" title="#opt-2">2</a> | `kubernetescrd` |
|
||||
| <a id="opt-3" href="#opt-3" title="#opt-3">3</a> | `kubernetes` |
|
||||
| <a id="opt-4" href="#opt-4" title="#opt-4">4</a> | `kubernetesingressnginx` |
|
||||
| <a id="opt-5" href="#opt-5" title="#opt-5">5</a> | `swarm` |
|
||||
| <a id="opt-6" href="#opt-6" title="#opt-6">6</a> | `docker` |
|
||||
| <a id="opt-7" href="#opt-7" title="#opt-7">7</a> | `file` |
|
||||
| <a id="opt-8" href="#opt-8" title="#opt-8">8</a> | `redis` |
|
||||
| <a id="opt-9" href="#opt-9" title="#opt-9">9</a> | `knative` |
|
||||
| <a id="opt-10" href="#opt-10" title="#opt-10">10</a> | `consul` |
|
||||
| <a id="opt-11" href="#opt-11" title="#opt-11">11</a> | `consulcatalog` |
|
||||
| <a id="opt-12" href="#opt-12" title="#opt-12">12</a> | `nomad` |
|
||||
| <a id="opt-13" href="#opt-13" title="#opt-13">13</a> | `etcd` |
|
||||
| <a id="opt-14" href="#opt-14" title="#opt-14">14</a> | `ecs` |
|
||||
| <a id="opt-15" href="#opt-15" title="#opt-15">15</a> | `http` |
|
||||
| <a id="opt-16" href="#opt-16" title="#opt-16">16</a> | `zookeeper` |
|
||||
| <a id="opt-17" href="#opt-17" title="#opt-17">17</a> | `rest` |
|
||||
|
||||
!!! note
|
||||
|
||||
- `precedence` only acts as a **tiebreaker**: it is applied only when two routes from different providers share the same numeric `priority` value. An explicit router priority always takes precedence.
|
||||
- A provider absent from `precedence` loses to any listed provider.
|
||||
- Provider names are case-insensitive.
|
||||
|
||||
### `exposedByDefault` and `traefik.enable`
|
||||
|
||||
List of providers that support these features:
|
||||
|
||||
@@ -241,6 +241,12 @@ Traefik reserves a range of priorities for its internal routers, the maximum use
|
||||
- `(MaxInt32 - 1000)` = `2147482647` for 32-bit platforms,
|
||||
- `(MaxInt64 - 1000)` = `9223372036854774807` for 64-bit platforms.
|
||||
|
||||
!!! info "Providers Precedence"
|
||||
|
||||
When two routes from **different providers** share the same numeric priority,
|
||||
Traefik uses the [`providers.precedence`](../../../install-configuration/providers/overview.md#providers-precedence) install configuration option to determine which route takes precedence.
|
||||
The provider listed first in `precedence` wins the tie.
|
||||
|
||||
### Example
|
||||
|
||||
```yaml tab="Structured (YAML)"
|
||||
|
||||
@@ -216,3 +216,9 @@ Traefik reserves a range of priorities for its internal routers, the maximum use
|
||||
|
||||
- `(MaxInt32 - 1000)` for 32-bit platforms,
|
||||
- `(MaxInt64 - 1000)` for 64-bit platforms.
|
||||
|
||||
!!! info "Providers Precedence"
|
||||
|
||||
When two routes from **different providers** share the same numeric priority,
|
||||
Traefik uses the [`providers.precedence`](../../../install-configuration/providers/overview.md#providers-precedence) install configuration option to determine which route takes precedence.
|
||||
The provider listed first in `precedence` wins the tie.
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
[global]
|
||||
checkNewVersion = false
|
||||
sendAnonymousUsage = false
|
||||
|
||||
[log]
|
||||
level = "DEBUG"
|
||||
noColor = true
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.web]
|
||||
address = ":8000"
|
||||
|
||||
[api]
|
||||
insecure = true
|
||||
|
||||
[providers]
|
||||
precedence = {{ .Precedence }}
|
||||
|
||||
[providers.file]
|
||||
filename = "{{ .SelfFilename }}"
|
||||
|
||||
[providers.docker]
|
||||
endpoint = "{{ .DockerHost }}"
|
||||
exposedByDefault = false
|
||||
|
||||
## dynamic configuration ##
|
||||
|
||||
[http.routers]
|
||||
[http.routers.file-router]
|
||||
rule = "PathPrefix(`/http`)"
|
||||
service = "file-service"
|
||||
entryPoints = ["web"]
|
||||
|
||||
[http.services]
|
||||
[http.services.file-service.loadBalancer]
|
||||
[[http.services.file-service.loadBalancer.servers]]
|
||||
url = "http://{{ .FileBackendAddress }}"
|
||||
|
||||
[tcp.routers]
|
||||
[tcp.routers.file-router]
|
||||
rule = "HostSNI(`*`)"
|
||||
service = "file-service"
|
||||
entryPoints = ["web"]
|
||||
|
||||
[tcp.services]
|
||||
[tcp.services.file-service.loadBalancer]
|
||||
[[tcp.services.file-service.loadBalancer.servers]]
|
||||
address = "{{ .FileBackendAddress }}"
|
||||
@@ -577,7 +577,7 @@ func (s *HealthCheckSuite) TestPropagateNoHealthCheck() {
|
||||
s.traefikCmd(withConfigFile(file))
|
||||
|
||||
// wait for traefik
|
||||
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 60*time.Second, try.BodyContains("Host(`noop.localhost`)"), try.BodyNotContains("Host(`root.localhost`)"))
|
||||
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 60*time.Second, try.BodyContains("Host(`noop.localhost`)"), try.BodyContains("cannot register wsp1 as updater for wsp-service1@file"))
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
rootReq, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000", nil)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
services:
|
||||
whoami:
|
||||
image: traefik/whoami
|
||||
labels:
|
||||
traefik.enable: "true"
|
||||
traefik.http.routers.docker-router.rule: "PathPrefix(`/http`)"
|
||||
traefik.http.routers.docker-router.entryPoints: web
|
||||
traefik.tcp.routers.docker-router.rule: "HostSNI(`*`)"
|
||||
traefik.tcp.routers.docker-router.entryPoints: web
|
||||
@@ -2656,3 +2656,88 @@ func (s *SimpleSuite) TestServiceMiddleware() {
|
||||
// The whoami service should have received the X-Custom-Header that was added by the service middleware
|
||||
assert.Contains(s.T(), string(body), "X-Custom-Header: service-middleware-test")
|
||||
}
|
||||
|
||||
// TestProviderPrecedenceFileWins verifies that, when two providers define
|
||||
// routes with the same rule and auto-computed priority, the provider listed
|
||||
// first in providers.precedence takes precedence (lower index = higher
|
||||
// provider priority).
|
||||
//
|
||||
// Setup:
|
||||
// - providers.file → file-router → fileBackend (body: "from-file")
|
||||
// - providers.docker → docker-router → whoami container
|
||||
// - precedence = ["file", "docker"] → file is index 0 → wins
|
||||
func (s *SimpleSuite) TestProviderPrecedenceFileWins() {
|
||||
s.createComposeProject("providers-precedence")
|
||||
s.composeUp("whoami")
|
||||
defer s.composeDown()
|
||||
|
||||
fileBackend := startTestServer("9042", http.StatusOK, "from-file")
|
||||
defer fileBackend.Close()
|
||||
|
||||
file := s.adaptFile("fixtures/providers-precedence.toml", struct {
|
||||
Precedence string
|
||||
FileBackendAddress string
|
||||
DockerHost string
|
||||
}{
|
||||
Precedence: `["file", "docker"]`,
|
||||
FileBackendAddress: "127.0.0.1:9042",
|
||||
DockerHost: s.getDockerHost(),
|
||||
})
|
||||
s.traefikCmd(withConfigFile(file))
|
||||
|
||||
// Wait for both providers to have loaded their routers.
|
||||
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second,
|
||||
try.StatusCodeIs(http.StatusOK),
|
||||
try.BodyContains("file-router@file"),
|
||||
try.BodyContains("docker-router@docker"))
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
// The file provider has higher priority → requests must reach the file backend.
|
||||
err = try.GetRequest("http://127.0.0.1:8000/http", 5*time.Second, try.BodyContains("from-file"))
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
// This request should be handled by the TCP route.
|
||||
err = try.GetRequest("http://127.0.0.1:8000/tcp", 5*time.Second, try.BodyContains("from-file"))
|
||||
require.NoError(s.T(), err)
|
||||
}
|
||||
|
||||
// TestProviderPrecedenceDockerWins mirrors TestProviderPrecedenceFileWins
|
||||
// but reverses the precedence so that the Docker provider wins instead.
|
||||
//
|
||||
// Setup:
|
||||
// - providers.file → file-router → fileBackend (body: "from-file")
|
||||
// - providers.docker → docker-router → whoami container
|
||||
// - precedence = ["docker", "file"] → docker is index 0 → wins
|
||||
func (s *SimpleSuite) TestProviderPrecedenceDockerWins() {
|
||||
s.createComposeProject("providers-precedence")
|
||||
s.composeUp("whoami")
|
||||
defer s.composeDown()
|
||||
|
||||
fileBackend := startTestServer("9042", http.StatusOK, "from-file")
|
||||
defer fileBackend.Close()
|
||||
|
||||
file := s.adaptFile("fixtures/providers-precedence.toml", struct {
|
||||
Precedence string
|
||||
FileBackendAddress string
|
||||
DockerHost string
|
||||
}{
|
||||
Precedence: `["docker", "file"]`,
|
||||
FileBackendAddress: "127.0.0.1:9042",
|
||||
DockerHost: s.getDockerHost(),
|
||||
})
|
||||
s.traefikCmd(withConfigFile(file))
|
||||
|
||||
// Wait for both providers to have loaded their routers.
|
||||
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second,
|
||||
try.BodyContains("file-router@file"),
|
||||
try.BodyContains("docker-router@docker"))
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
// The Docker provider has higher priority → requests must reach the whoami container.
|
||||
err = try.GetRequest("http://127.0.0.1:8000/http", 5*time.Second, try.BodyContains("Hostname:"))
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
// This request should be handled by the TCP route.
|
||||
err = try.GetRequest("http://127.0.0.1:8000/tcp", 5*time.Second, try.BodyContains("Hostname:"))
|
||||
require.NoError(s.T(), err)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package try
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -25,6 +26,8 @@ func BodyContains(values ...string) ResponseCondition {
|
||||
return fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
res.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
|
||||
for _, value := range values {
|
||||
if !strings.Contains(string(body), value) {
|
||||
return fmt.Errorf("could not find '%s' in body '%s'", value, string(body))
|
||||
@@ -43,6 +46,8 @@ func BodyNotContains(values ...string) ResponseCondition {
|
||||
return fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
res.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
|
||||
for _, value := range values {
|
||||
if strings.Contains(string(body), value) {
|
||||
return fmt.Errorf("find '%s' in body '%s'", value, string(body))
|
||||
@@ -61,6 +66,8 @@ func BodyContainsOr(values ...string) ResponseCondition {
|
||||
return fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
res.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
|
||||
for _, value := range values {
|
||||
if strings.Contains(string(body), value) {
|
||||
return nil
|
||||
@@ -79,6 +86,8 @@ func HasBody() ResponseCondition {
|
||||
return fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
res.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
|
||||
if len(body) == 0 {
|
||||
return errors.New("response doesn't have body content")
|
||||
}
|
||||
|
||||
@@ -232,6 +232,7 @@ func TestHandler_Overview(t *testing.T) {
|
||||
Global: &static.Global{},
|
||||
API: &static.API{},
|
||||
Providers: &static.Providers{
|
||||
Precedence: []string{"foo"},
|
||||
Docker: &docker.Provider{},
|
||||
Swarm: &docker.SwarmProvider{},
|
||||
File: &file.Provider{},
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -57,6 +58,27 @@ const (
|
||||
DefaultUDPTimeout = 3 * time.Second
|
||||
)
|
||||
|
||||
// providerNames is the ordered list of the Traefik provider names.
|
||||
var providerNames = []string{
|
||||
gateway.ProviderName,
|
||||
crd.ProviderName,
|
||||
ingress.ProviderName,
|
||||
ingressnginx.ProviderName,
|
||||
docker.SwarmName,
|
||||
docker.DockerName,
|
||||
file.ProviderName,
|
||||
redis.ProviderName,
|
||||
knative.ProviderName,
|
||||
consul.ProviderName,
|
||||
consulcatalog.ProviderName,
|
||||
nomad.ProviderName,
|
||||
etcd.ProviderName,
|
||||
ecs.ProviderName,
|
||||
http.ProviderName,
|
||||
zk.ProviderName,
|
||||
rest.ProviderName,
|
||||
}
|
||||
|
||||
// Allowed characters in URL following RFC 3986 (https://www.rfc-editor.org/rfc/rfc3986#section-2)
|
||||
var validBasePath = regexp.MustCompile(`^/[a-zA-Z0-9/_.:~-]*$`)
|
||||
|
||||
@@ -238,6 +260,7 @@ func (t *Tracing) SetDefaults() {
|
||||
// Providers contains providers configuration.
|
||||
type Providers struct {
|
||||
ProvidersThrottleDuration ptypes.Duration `description:"Backends throttle duration: minimum duration between 2 events from providers before applying a new configuration. It avoids unnecessary reloads if multiples events are sent in a short amount of time." json:"providersThrottleDuration,omitempty" toml:"providersThrottleDuration,omitempty" yaml:"providersThrottleDuration,omitempty" export:"true"`
|
||||
Precedence []string `description:"Defines the routing precedence between providers." json:"precedence,omitempty" toml:"precedence,omitempty" yaml:"precedence,omitempty" export:"true"`
|
||||
|
||||
Docker *docker.Provider `description:"Enables Docker provider." json:"docker,omitempty" toml:"docker,omitempty" yaml:"docker,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||
Swarm *docker.SwarmProvider `description:"Enables Docker Swarm provider." json:"swarm,omitempty" toml:"swarm,omitempty" yaml:"swarm,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||
@@ -260,6 +283,11 @@ type Providers struct {
|
||||
Plugin map[string]PluginConf `description:"Plugins configuration." json:"plugin,omitempty" toml:"plugin,omitempty" yaml:"plugin,omitempty"`
|
||||
}
|
||||
|
||||
// SetDefaults sets the default values.
|
||||
func (p *Providers) SetDefaults() {
|
||||
p.Precedence = providerNames
|
||||
}
|
||||
|
||||
// SetEffectiveConfiguration adds missing configuration parameters derived from existing ones.
|
||||
// It also takes care of maintaining backwards compatibility.
|
||||
func (c *Configuration) SetEffectiveConfiguration() {
|
||||
@@ -290,6 +318,10 @@ func (c *Configuration) SetEffectiveConfiguration() {
|
||||
c.Tracing.ResourceAttributes = c.Tracing.GlobalAttributes
|
||||
}
|
||||
|
||||
for i, providerName := range c.Providers.Precedence {
|
||||
c.Providers.Precedence[i] = strings.ToLower(providerName)
|
||||
}
|
||||
|
||||
if c.Providers.Docker != nil {
|
||||
if c.Providers.Docker.HTTPClientTimeout < 0 {
|
||||
c.Providers.Docker.HTTPClientTimeout = 0
|
||||
@@ -426,6 +458,14 @@ func (c *Configuration) ValidateConfiguration() error {
|
||||
}
|
||||
}
|
||||
|
||||
if c.Providers != nil {
|
||||
for _, providerName := range c.Providers.Precedence {
|
||||
if !slices.Contains(providerNames, providerName) {
|
||||
return fmt.Errorf("provider %q is not a valid provider name", providerName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.Providers != nil && c.Providers.KubernetesIngressNGINX != nil {
|
||||
if c.Providers.KubernetesIngressNGINX.WatchNamespace != "" && c.Providers.KubernetesIngressNGINX.WatchNamespaceSelector != "" {
|
||||
return errors.New("watchNamespace and watchNamespaceSelector options are mutually exclusive")
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/traefik/traefik/v3/pkg/provider/acme"
|
||||
)
|
||||
|
||||
@@ -50,7 +51,7 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
|
||||
{
|
||||
desc: "empty",
|
||||
conf: &Configuration{
|
||||
Providers: &Providers{},
|
||||
Providers: &Providers{Precedence: providerNames},
|
||||
},
|
||||
expected: &Configuration{
|
||||
EntryPoints: EntryPoints{"http": &EntryPoint{
|
||||
@@ -83,13 +84,13 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
|
||||
Timeout: 3000000000,
|
||||
},
|
||||
}},
|
||||
Providers: &Providers{},
|
||||
Providers: &Providers{Precedence: providerNames},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "ACME simple",
|
||||
conf: &Configuration{
|
||||
Providers: &Providers{},
|
||||
Providers: &Providers{Precedence: providerNames},
|
||||
CertificatesResolvers: map[string]CertificateResolver{
|
||||
"foo": {
|
||||
ACME: &acme.Configuration{
|
||||
@@ -131,7 +132,7 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
|
||||
Timeout: 3000000000,
|
||||
},
|
||||
}},
|
||||
Providers: &Providers{},
|
||||
Providers: &Providers{Precedence: providerNames},
|
||||
CertificatesResolvers: map[string]CertificateResolver{
|
||||
"foo": {
|
||||
ACME: &acme.Configuration{
|
||||
@@ -147,7 +148,7 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
|
||||
{
|
||||
desc: "ACME deprecation DelayBeforeCheck",
|
||||
conf: &Configuration{
|
||||
Providers: &Providers{},
|
||||
Providers: &Providers{Precedence: providerNames},
|
||||
CertificatesResolvers: map[string]CertificateResolver{
|
||||
"foo": {
|
||||
ACME: &acme.Configuration{
|
||||
@@ -190,7 +191,7 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
|
||||
Timeout: 3000000000,
|
||||
},
|
||||
}},
|
||||
Providers: &Providers{},
|
||||
Providers: &Providers{Precedence: providerNames},
|
||||
CertificatesResolvers: map[string]CertificateResolver{
|
||||
"foo": {
|
||||
ACME: &acme.Configuration{
|
||||
@@ -210,7 +211,7 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
|
||||
{
|
||||
desc: "ACME deprecation DisablePropagationCheck",
|
||||
conf: &Configuration{
|
||||
Providers: &Providers{},
|
||||
Providers: &Providers{Precedence: providerNames},
|
||||
CertificatesResolvers: map[string]CertificateResolver{
|
||||
"foo": {
|
||||
ACME: &acme.Configuration{
|
||||
@@ -253,7 +254,7 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
|
||||
Timeout: 3000000000,
|
||||
},
|
||||
}},
|
||||
Providers: &Providers{},
|
||||
Providers: &Providers{Precedence: providerNames},
|
||||
CertificatesResolvers: map[string]CertificateResolver{
|
||||
"foo": {
|
||||
ACME: &acme.Configuration{
|
||||
@@ -378,3 +379,55 @@ func TestValidateConfiguration_BasePath(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvidersPrecedence(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
cfg *Configuration
|
||||
expectedError bool
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
desc: "No precedence",
|
||||
cfg: &Configuration{
|
||||
Providers: &Providers{
|
||||
Precedence: providerNames,
|
||||
},
|
||||
},
|
||||
expected: providerNames,
|
||||
},
|
||||
{
|
||||
desc: "Precedence with non existing provider",
|
||||
cfg: &Configuration{
|
||||
Providers: &Providers{
|
||||
Precedence: []string{"unknown"},
|
||||
},
|
||||
},
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "Precedence with upper case provider",
|
||||
cfg: &Configuration{
|
||||
Providers: &Providers{
|
||||
Precedence: []string{"DOCKER"},
|
||||
},
|
||||
},
|
||||
expected: []string{"docker"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
test.cfg.SetEffectiveConfiguration()
|
||||
err := test.cfg.ValidateConfiguration()
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expected, test.cfg.Providers.Precedence)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,9 +72,9 @@ func TestClientIPMatcher(t *testing.T) {
|
||||
parser, err := NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := NewMuxer(parser)
|
||||
muxer := NewMuxer(parser, nil)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "", 0, handler)
|
||||
err = muxer.AddRoute(test.rule, "", 0, "", handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -147,9 +147,9 @@ func TestMethodMatcher(t *testing.T) {
|
||||
parser, err := NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := NewMuxer(parser)
|
||||
muxer := NewMuxer(parser, nil)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "", 0, handler)
|
||||
err = muxer.AddRoute(test.rule, "", 0, "", handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -276,9 +276,9 @@ func TestHostMatcher(t *testing.T) {
|
||||
parser, err := NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := NewMuxer(parser)
|
||||
muxer := NewMuxer(parser, nil)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "", 0, handler)
|
||||
err = muxer.AddRoute(test.rule, "", 0, "", handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -377,9 +377,9 @@ func TestHostRegexpMatcher(t *testing.T) {
|
||||
parser, err := NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := NewMuxer(parser)
|
||||
muxer := NewMuxer(parser, nil)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "", 0, handler)
|
||||
err = muxer.AddRoute(test.rule, "", 0, "", handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -452,9 +452,9 @@ func TestPathMatcher(t *testing.T) {
|
||||
parser, err := NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := NewMuxer(parser)
|
||||
muxer := NewMuxer(parser, nil)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "", 0, handler)
|
||||
err = muxer.AddRoute(test.rule, "", 0, "", handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -546,9 +546,9 @@ func TestPathRegexpMatcher(t *testing.T) {
|
||||
parser, err := NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := NewMuxer(parser)
|
||||
muxer := NewMuxer(parser, nil)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "", 0, handler)
|
||||
err = muxer.AddRoute(test.rule, "", 0, "", handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -619,9 +619,9 @@ func TestPathPrefixMatcher(t *testing.T) {
|
||||
parser, err := NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := NewMuxer(parser)
|
||||
muxer := NewMuxer(parser, nil)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "", 0, handler)
|
||||
err = muxer.AddRoute(test.rule, "", 0, "", handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -707,9 +707,9 @@ func TestHeaderMatcher(t *testing.T) {
|
||||
parser, err := NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := NewMuxer(parser)
|
||||
muxer := NewMuxer(parser, nil)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "", 0, handler)
|
||||
err = muxer.AddRoute(test.rule, "", 0, "", handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -816,9 +816,9 @@ func TestHeaderRegexpMatcher(t *testing.T) {
|
||||
parser, err := NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := NewMuxer(parser)
|
||||
muxer := NewMuxer(parser, nil)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "", 0, handler)
|
||||
err = muxer.AddRoute(test.rule, "", 0, "", handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -906,9 +906,9 @@ func TestQueryMatcher(t *testing.T) {
|
||||
parser, err := NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := NewMuxer(parser)
|
||||
muxer := NewMuxer(parser, nil)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "", 0, handler)
|
||||
err = muxer.AddRoute(test.rule, "", 0, "", handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -1021,9 +1021,9 @@ func TestQueryRegexpMatcher(t *testing.T) {
|
||||
parser, err := NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := NewMuxer(parser)
|
||||
muxer := NewMuxer(parser, nil)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "", 0, handler)
|
||||
err = muxer.AddRoute(test.rule, "", 0, "", handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
|
||||
@@ -76,9 +76,9 @@ func TestClientIPV2Matcher(t *testing.T) {
|
||||
parser, err := NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := NewMuxer(parser)
|
||||
muxer := NewMuxer(parser, nil)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "v2", 0, handler)
|
||||
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -154,9 +154,9 @@ func TestMethodV2Matcher(t *testing.T) {
|
||||
parser, err := NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := NewMuxer(parser)
|
||||
muxer := NewMuxer(parser, nil)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "v2", 0, handler)
|
||||
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -280,9 +280,9 @@ func TestHostV2Matcher(t *testing.T) {
|
||||
parser, err := NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := NewMuxer(parser)
|
||||
muxer := NewMuxer(parser, nil)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "v2", 0, handler)
|
||||
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -384,9 +384,9 @@ func TestHostRegexpV2Matcher(t *testing.T) {
|
||||
parser, err := NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := NewMuxer(parser)
|
||||
muxer := NewMuxer(parser, nil)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "v2", 0, handler)
|
||||
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -480,9 +480,9 @@ func TestPathV2Matcher(t *testing.T) {
|
||||
parser, err := NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := NewMuxer(parser)
|
||||
muxer := NewMuxer(parser, nil)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "v2", 0, handler)
|
||||
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -574,9 +574,9 @@ func TestPathPrefixV2Matcher(t *testing.T) {
|
||||
parser, err := NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := NewMuxer(parser)
|
||||
muxer := NewMuxer(parser, nil)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "v2", 0, handler)
|
||||
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -662,9 +662,9 @@ func TestHeadersMatcher(t *testing.T) {
|
||||
parser, err := NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := NewMuxer(parser)
|
||||
muxer := NewMuxer(parser, nil)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "v2", 0, handler)
|
||||
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -771,9 +771,9 @@ func TestHeaderRegexpV2Matcher(t *testing.T) {
|
||||
parser, err := NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := NewMuxer(parser)
|
||||
muxer := NewMuxer(parser, nil)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "v2", 0, handler)
|
||||
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -865,9 +865,9 @@ func TestHostRegexp(t *testing.T) {
|
||||
parser, err := NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := NewMuxer(parser)
|
||||
muxer := NewMuxer(parser, nil)
|
||||
|
||||
err = muxer.AddRoute(test.hostExp, "v2", 0, handler)
|
||||
err = muxer.AddRoute(test.hostExp, "v2", 0, "", handler)
|
||||
require.NoError(t, err)
|
||||
|
||||
results := make(map[string]int)
|
||||
@@ -1534,9 +1534,9 @@ func Test_addRoute(t *testing.T) {
|
||||
parser, err := NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := NewMuxer(parser)
|
||||
muxer := NewMuxer(parser, nil)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "v2", 0, handler)
|
||||
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
|
||||
+22
-10
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -24,15 +25,20 @@ type MatcherFunc func(*http.Request) bool
|
||||
type Muxer struct {
|
||||
routes routes
|
||||
|
||||
parser SyntaxParser
|
||||
defaultHandler http.Handler
|
||||
parser SyntaxParser
|
||||
defaultHandler http.Handler
|
||||
providersPrecedence []string
|
||||
}
|
||||
|
||||
// NewMuxer returns a new muxer instance.
|
||||
func NewMuxer(parser SyntaxParser) *Muxer {
|
||||
func NewMuxer(parser SyntaxParser, providersPrecedence []string) *Muxer {
|
||||
providersPrecedence = slices.Clone(providersPrecedence)
|
||||
slices.Reverse(providersPrecedence)
|
||||
|
||||
return &Muxer{
|
||||
parser: parser,
|
||||
defaultHandler: http.NotFoundHandler(),
|
||||
parser: parser,
|
||||
defaultHandler: http.NotFoundHandler(),
|
||||
providersPrecedence: providersPrecedence,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,16 +77,17 @@ func GetRulePriority(rule string) int {
|
||||
}
|
||||
|
||||
// AddRoute add a new route to the router.
|
||||
func (m *Muxer) AddRoute(rule string, syntax string, priority int, handler http.Handler) error {
|
||||
func (m *Muxer) AddRoute(rule string, syntax string, priority int, providerName string, handler http.Handler) error {
|
||||
matchers, err := m.parser.parse(syntax, rule)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while parsing rule %s: %w", rule, err)
|
||||
}
|
||||
|
||||
m.routes = append(m.routes, &route{
|
||||
handler: handler,
|
||||
matchers: matchers,
|
||||
priority: priority,
|
||||
handler: handler,
|
||||
matchers: matchers,
|
||||
priority: priority,
|
||||
providerPriority: slices.Index(m.providersPrecedence, providerName),
|
||||
})
|
||||
|
||||
sort.Sort(m.routes)
|
||||
@@ -206,7 +213,10 @@ func (r routes) Len() int { return len(r) }
|
||||
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 }
|
||||
func (r routes) Less(i, j int) bool {
|
||||
return r[i].priority > r[j].priority ||
|
||||
(r[i].priority == r[j].priority && r[i].providerPriority > r[j].providerPriority)
|
||||
}
|
||||
|
||||
// route holds the matchers to match HTTP route,
|
||||
// and the handler that will serve the request.
|
||||
@@ -218,6 +228,8 @@ type route struct {
|
||||
// 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
|
||||
// providerPriority is used to disambiguate between two (or more) rules that would all match for a given request and have the same priority.
|
||||
providerPriority int
|
||||
}
|
||||
|
||||
// matchersTree represents the matchers tree structure.
|
||||
|
||||
+124
-9
@@ -228,10 +228,10 @@ func TestMuxer(t *testing.T) {
|
||||
parser, err := NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := NewMuxer(parser)
|
||||
muxer := NewMuxer(parser, nil)
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
err = muxer.AddRoute(test.rule, "", 0, handler)
|
||||
err = muxer.AddRoute(test.rule, "", 0, "", handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -264,9 +264,10 @@ func TestMuxer(t *testing.T) {
|
||||
|
||||
func Test_addRoutePriority(t *testing.T) {
|
||||
type Case struct {
|
||||
xFrom string
|
||||
rule string
|
||||
priority int
|
||||
xFrom string
|
||||
rule string
|
||||
priority int
|
||||
providerName string
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
@@ -375,6 +376,120 @@ func Test_addRoutePriority(t *testing.T) {
|
||||
},
|
||||
expected: "header3",
|
||||
},
|
||||
{
|
||||
desc: "Same priority and rule, kubernetescrd wins over kubernetes",
|
||||
path: "/my",
|
||||
cases: []Case{
|
||||
{
|
||||
xFrom: "header1",
|
||||
rule: "PathPrefix(`/my`)",
|
||||
priority: 10,
|
||||
providerName: "kubernetes",
|
||||
},
|
||||
{
|
||||
xFrom: "header2",
|
||||
rule: "PathPrefix(`/my`)",
|
||||
priority: 10,
|
||||
providerName: "kubernetescrd",
|
||||
},
|
||||
},
|
||||
expected: "header2",
|
||||
},
|
||||
{
|
||||
desc: "Same priority and rule, kubernetesgateway wins over kubernetescrd",
|
||||
path: "/my",
|
||||
cases: []Case{
|
||||
{
|
||||
xFrom: "header1",
|
||||
rule: "PathPrefix(`/my`)",
|
||||
priority: 10,
|
||||
providerName: "kubernetescrd",
|
||||
},
|
||||
{
|
||||
xFrom: "header2",
|
||||
rule: "PathPrefix(`/my`)",
|
||||
priority: 10,
|
||||
providerName: "kubernetesgateway",
|
||||
},
|
||||
},
|
||||
expected: "header2",
|
||||
},
|
||||
{
|
||||
desc: "Same priority and rule, kubernetesgateway wins over kubernetes",
|
||||
path: "/my",
|
||||
cases: []Case{
|
||||
{
|
||||
xFrom: "header1",
|
||||
rule: "PathPrefix(`/my`)",
|
||||
priority: 10,
|
||||
providerName: "kubernetesgateway",
|
||||
},
|
||||
{
|
||||
xFrom: "header2",
|
||||
rule: "PathPrefix(`/my`)",
|
||||
priority: 10,
|
||||
providerName: "kubernetes",
|
||||
},
|
||||
},
|
||||
expected: "header1",
|
||||
},
|
||||
{
|
||||
desc: "Same priority and rule, kubernetescrd wins over kubernetesingressnginx",
|
||||
path: "/my",
|
||||
cases: []Case{
|
||||
{
|
||||
xFrom: "header1",
|
||||
rule: "PathPrefix(`/my`)",
|
||||
priority: 10,
|
||||
providerName: "kubernetesingressnginx",
|
||||
},
|
||||
{
|
||||
xFrom: "header2",
|
||||
rule: "PathPrefix(`/my`)",
|
||||
priority: 10,
|
||||
providerName: "kubernetescrd",
|
||||
},
|
||||
},
|
||||
expected: "header2",
|
||||
},
|
||||
{
|
||||
desc: "Same priority and rule, known provider wins over unknown provider",
|
||||
path: "/my",
|
||||
cases: []Case{
|
||||
{
|
||||
xFrom: "header1",
|
||||
rule: "PathPrefix(`/my`)",
|
||||
priority: 10,
|
||||
providerName: "unknownprovider",
|
||||
},
|
||||
{
|
||||
xFrom: "header2",
|
||||
rule: "PathPrefix(`/my`)",
|
||||
priority: 10,
|
||||
providerName: "kubernetes",
|
||||
},
|
||||
},
|
||||
expected: "header2",
|
||||
},
|
||||
{
|
||||
desc: "Higher numeric priority wins regardless of provider",
|
||||
path: "/my",
|
||||
cases: []Case{
|
||||
{
|
||||
xFrom: "header1",
|
||||
rule: "PathPrefix(`/my`)",
|
||||
priority: 20,
|
||||
providerName: "kubernetesingressnginx",
|
||||
},
|
||||
{
|
||||
xFrom: "header2",
|
||||
rule: "PathPrefix(`/my`)",
|
||||
priority: 10,
|
||||
providerName: "kubernetesgateway",
|
||||
},
|
||||
},
|
||||
expected: "header1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
@@ -383,7 +498,7 @@ func Test_addRoutePriority(t *testing.T) {
|
||||
parser, err := NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := NewMuxer(parser)
|
||||
muxer := NewMuxer(parser, []string{"kubernetesgateway", "kubernetescrd", "kubernetes", "kubernetesingressnginx"})
|
||||
|
||||
for _, route := range test.cases {
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -394,7 +509,7 @@ func Test_addRoutePriority(t *testing.T) {
|
||||
route.priority = GetRulePriority(route.rule)
|
||||
}
|
||||
|
||||
err := muxer.AddRoute(route.rule, "", route.priority, handler)
|
||||
err := muxer.AddRoute(route.rule, "", route.priority, route.providerName, handler)
|
||||
require.NoError(t, err, route.rule)
|
||||
}
|
||||
|
||||
@@ -517,9 +632,9 @@ func TestEmptyHost(t *testing.T) {
|
||||
parser, err := NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := NewMuxer(parser)
|
||||
muxer := NewMuxer(parser, nil)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "", 0, handler)
|
||||
err = muxer.AddRoute(test.rule, "", 0, "", handler)
|
||||
require.NoError(t, err)
|
||||
|
||||
// RequestDecorator is necessary for the host rule
|
||||
|
||||
@@ -34,10 +34,10 @@ func Test_HostSNICatchAll(t *testing.T) {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
muxer, err := NewMuxer()
|
||||
muxer, err := NewMuxer(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
|
||||
err = muxer.AddRoute(test.rule, "", 0, "", tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
|
||||
require.NoError(t, err)
|
||||
|
||||
handler, catchAll := muxer.Match(ConnData{
|
||||
@@ -146,10 +146,10 @@ func Test_HostSNI(t *testing.T) {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
muxer, err := NewMuxer()
|
||||
muxer, err := NewMuxer(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
|
||||
err = muxer.AddRoute(test.rule, "", 0, "", tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
|
||||
if test.buildErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -228,10 +228,10 @@ func Test_HostSNIRegexp(t *testing.T) {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
muxer, err := NewMuxer()
|
||||
muxer, err := NewMuxer(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
|
||||
err = muxer.AddRoute(test.rule, "", 0, "", tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
|
||||
if test.buildErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -299,10 +299,10 @@ func Test_ClientIP(t *testing.T) {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
muxer, err := NewMuxer()
|
||||
muxer, err := NewMuxer(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
|
||||
err = muxer.AddRoute(test.rule, "", 0, "", tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
|
||||
if test.buildErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -362,10 +362,10 @@ func Test_ALPN(t *testing.T) {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
muxer, err := NewMuxer()
|
||||
muxer, err := NewMuxer(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
|
||||
err = muxer.AddRoute(test.rule, "", 0, "", tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
|
||||
if test.buildErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
|
||||
@@ -470,10 +470,10 @@ func Test_addTCPRouteV2(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
router, err := NewMuxer()
|
||||
router, err := NewMuxer(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = router.AddRoute(test.rule, "v2", 0, handler)
|
||||
err = router.AddRoute(test.rule, "v2", 0, "", handler)
|
||||
if test.routeErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -606,10 +606,10 @@ func Test_HostSNICatchAllV2(t *testing.T) {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
muxer, err := NewMuxer()
|
||||
muxer, err := NewMuxer(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = muxer.AddRoute(test.rule, "v2", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
|
||||
err = muxer.AddRoute(test.rule, "v2", 0, "", tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
|
||||
require.NoError(t, err)
|
||||
|
||||
handler, catchAll := muxer.Match(ConnData{
|
||||
|
||||
+25
-12
@@ -3,6 +3,7 @@ package tcp
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -39,13 +40,15 @@ func NewConnData(serverName string, conn tcp.WriteCloser, alpnProtos []string) (
|
||||
|
||||
// Muxer defines a muxer that handles TCP routing with rules.
|
||||
type Muxer struct {
|
||||
routes routes
|
||||
parser predicate.Parser
|
||||
parserV2 predicate.Parser
|
||||
routes routes
|
||||
|
||||
parser predicate.Parser
|
||||
parserV2 predicate.Parser
|
||||
providersPrecedence []string
|
||||
}
|
||||
|
||||
// NewMuxer returns a TCP muxer.
|
||||
func NewMuxer() (*Muxer, error) {
|
||||
func NewMuxer(providersPrecedence []string) (*Muxer, error) {
|
||||
var matcherNames []string
|
||||
for matcherName := range tcpFuncs {
|
||||
matcherNames = append(matcherNames, matcherName)
|
||||
@@ -66,9 +69,13 @@ func NewMuxer() (*Muxer, error) {
|
||||
return nil, fmt.Errorf("error while creating v2 rules parser: %w", err)
|
||||
}
|
||||
|
||||
providersPrecedence = slices.Clone(providersPrecedence)
|
||||
slices.Reverse(providersPrecedence)
|
||||
|
||||
return &Muxer{
|
||||
parser: parser,
|
||||
parserV2: parserV2,
|
||||
parser: parser,
|
||||
parserV2: parserV2,
|
||||
providersPrecedence: providersPrecedence,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -118,7 +125,7 @@ func GetRulePriority(rule string) int {
|
||||
|
||||
// AddRoute adds a new route, associated to the given handler, at the given
|
||||
// priority, to the muxer.
|
||||
func (m *Muxer) AddRoute(rule string, syntax string, priority int, handler tcp.Handler) error {
|
||||
func (m *Muxer) AddRoute(rule string, syntax string, priority int, providerName string, handler tcp.Handler) error {
|
||||
var parse any
|
||||
var err error
|
||||
var matcherFuncs map[string]func(*matchersTree, ...string) error
|
||||
@@ -159,10 +166,11 @@ func (m *Muxer) AddRoute(rule string, syntax string, priority int, handler tcp.H
|
||||
}
|
||||
|
||||
newRoute := &route{
|
||||
handler: handler,
|
||||
matchers: matchers,
|
||||
catchAll: catchAll,
|
||||
priority: priority,
|
||||
handler: handler,
|
||||
matchers: matchers,
|
||||
catchAll: catchAll,
|
||||
priority: priority,
|
||||
providerPriority: slices.Index(m.providersPrecedence, providerName),
|
||||
}
|
||||
m.routes = append(m.routes, newRoute)
|
||||
|
||||
@@ -215,7 +223,10 @@ func (r routes) Len() int { return len(r) }
|
||||
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 }
|
||||
func (r routes) Less(i, j int) bool {
|
||||
return r[i].priority > r[j].priority ||
|
||||
(r[i].priority == r[j].priority && r[i].providerPriority > r[j].providerPriority)
|
||||
}
|
||||
|
||||
// route holds the matchers to match TCP route,
|
||||
// and the handler that will serve the connection.
|
||||
@@ -230,6 +241,8 @@ type route struct {
|
||||
// all match for a given request.
|
||||
// Computed from the matching rule length, if not user-set.
|
||||
priority int
|
||||
// providerPriority is used to disambiguate between two (or more) rules that would all match for a given request and have the same priority.
|
||||
providerPriority int
|
||||
}
|
||||
|
||||
// matchersTree represents the matchers tree structure.
|
||||
|
||||
+56
-32
@@ -272,10 +272,10 @@ func Test_addTCPRoute(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
router, err := NewMuxer()
|
||||
router, err := NewMuxer(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = router.AddRoute(test.rule, "", 0, handler)
|
||||
err = router.AddRoute(test.rule, "", 0, "", handler)
|
||||
if test.routeErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -388,47 +388,66 @@ func TestParseHostSNI(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_Priority(t *testing.T) {
|
||||
type rule struct {
|
||||
rule string
|
||||
priority int
|
||||
provider string
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
rules map[string]int
|
||||
serverName string
|
||||
expectedRule string
|
||||
desc string
|
||||
rules []rule
|
||||
serverName string
|
||||
expectedRule string
|
||||
expectedProvider string
|
||||
}{
|
||||
{
|
||||
desc: "One matching rule, calculated priority",
|
||||
rules: map[string]int{
|
||||
"HostSNI(`example.com`)": 0,
|
||||
"HostSNI(`example.org`)": 0,
|
||||
desc: "One matching rule, same priority",
|
||||
rules: []rule{
|
||||
{rule: "HostSNI(`example.com`)"},
|
||||
{rule: "HostSNI(`example.org`)"},
|
||||
},
|
||||
expectedRule: "HostSNI(`example.com`)",
|
||||
serverName: "example.com",
|
||||
expectedRule: "HostSNI(`example.com`)",
|
||||
},
|
||||
{
|
||||
desc: "One matching rule, custom priority",
|
||||
rules: map[string]int{
|
||||
"HostSNI(`example.org`)": 0,
|
||||
"HostSNI(`example.com`)": 10000,
|
||||
rules: []rule{
|
||||
{rule: "HostSNI(`example.com`)", priority: 10000},
|
||||
{rule: "HostSNI(`example.org`)"},
|
||||
},
|
||||
expectedRule: "HostSNI(`example.org`)",
|
||||
serverName: "example.org",
|
||||
expectedRule: "HostSNI(`example.org`)",
|
||||
},
|
||||
{
|
||||
desc: "Two matching rules, calculated priority",
|
||||
rules: map[string]int{
|
||||
"HostSNI(`example.org`)": 0,
|
||||
"HostSNI(`example.com`)": 0,
|
||||
desc: "Same rule and priority, kubernetescrd wins over kubernetes",
|
||||
rules: []rule{
|
||||
{rule: "HostSNI(`example.org`)", provider: "kubernetescrd"},
|
||||
{rule: "HostSNI(`example.org`)", provider: "kubernetes"},
|
||||
},
|
||||
expectedRule: "HostSNI(`example.org`)",
|
||||
serverName: "example.org",
|
||||
serverName: "example.org",
|
||||
expectedRule: "HostSNI(`example.org`)",
|
||||
expectedProvider: "kubernetescrd",
|
||||
},
|
||||
{
|
||||
desc: "Two matching rules, custom priority",
|
||||
rules: map[string]int{
|
||||
"HostSNI(`example.com`)": 10000,
|
||||
"HostSNI(`example.org`)": 0,
|
||||
desc: "Same rule and priority, known provider wins over unknown provider",
|
||||
rules: []rule{
|
||||
{rule: "HostSNI(`example.org`)", provider: "kubernetescrd"},
|
||||
{rule: "HostSNI(`example.org`)", provider: "foo"},
|
||||
},
|
||||
expectedRule: "HostSNI(`example.com`)",
|
||||
serverName: "example.com",
|
||||
serverName: "example.org",
|
||||
expectedRule: "HostSNI(`example.org`)",
|
||||
expectedProvider: "kubernetescrd",
|
||||
},
|
||||
{
|
||||
desc: "Same rule, higher numeric priority wins regardless of provider",
|
||||
rules: []rule{
|
||||
{rule: "HostSNI(`example.org`)", priority: 10, provider: "kubernetescrd"},
|
||||
{rule: "HostSNI(`example.org`)", priority: 20, provider: "kubernetes"},
|
||||
},
|
||||
serverName: "example.org",
|
||||
expectedRule: "HostSNI(`example.org`)",
|
||||
expectedProvider: "kubernetes",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -436,13 +455,17 @@ func Test_Priority(t *testing.T) {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
muxer, err := NewMuxer()
|
||||
muxer, err := NewMuxer([]string{"kubernetescrd", "kubernetes"})
|
||||
require.NoError(t, err)
|
||||
|
||||
matchedRule := ""
|
||||
for rule, priority := range test.rules {
|
||||
err := muxer.AddRoute(rule, "", priority, tcp.HandlerFunc(func(conn tcp.WriteCloser) {
|
||||
matchedRule = rule
|
||||
var (
|
||||
matchedRule string
|
||||
matchedProvider string
|
||||
)
|
||||
for _, r := range test.rules {
|
||||
err := muxer.AddRoute(r.rule, "", r.priority, r.provider, tcp.HandlerFunc(func(conn tcp.WriteCloser) {
|
||||
matchedRule = r.rule
|
||||
matchedProvider = r.provider
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -454,6 +477,7 @@ func Test_Priority(t *testing.T) {
|
||||
|
||||
handler.ServeTCP(nil)
|
||||
assert.Equal(t, test.expectedRule, matchedRule)
|
||||
assert.Equal(t, test.expectedProvider, matchedProvider)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ import (
|
||||
// defaultTemplateRule is the default template for the default rule.
|
||||
const defaultTemplateRule = "Host(`{{ normalize .Name }}`)"
|
||||
|
||||
// providerName is the Consul Catalog provider name.
|
||||
const providerName = "consulcatalog"
|
||||
// ProviderName is the Consul Catalog provider name.
|
||||
const ProviderName = "consulcatalog"
|
||||
|
||||
var _ provider.Provider = (*Provider)(nil)
|
||||
|
||||
@@ -58,7 +58,7 @@ func (p *ProviderBuilder) BuildProviders() []*Provider {
|
||||
if len(p.Namespaces) == 0 {
|
||||
return []*Provider{{
|
||||
Configuration: p.Configuration,
|
||||
name: providerName,
|
||||
name: ProviderName,
|
||||
}}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func (p *ProviderBuilder) BuildProviders() []*Provider {
|
||||
for _, namespace := range p.Namespaces {
|
||||
providers = append(providers, &Provider{
|
||||
Configuration: p.Configuration,
|
||||
name: providerName + "-" + namespace,
|
||||
name: ProviderName + "-" + namespace,
|
||||
namespace: namespace,
|
||||
})
|
||||
}
|
||||
@@ -145,7 +145,7 @@ func (p *Provider) Init() error {
|
||||
|
||||
// In case they didn't initialize Provider with BuildProviders.
|
||||
if p.name == "" {
|
||||
p.name = providerName
|
||||
p.name = ProviderName
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -21,7 +21,8 @@ import (
|
||||
"github.com/traefik/traefik/v3/pkg/safe"
|
||||
)
|
||||
|
||||
const dockerName = "docker"
|
||||
// DockerName is the docker provider name.
|
||||
const DockerName = "docker"
|
||||
|
||||
var _ provider.Provider = (*Provider)(nil)
|
||||
|
||||
@@ -53,7 +54,7 @@ func (p *Provider) Init() error {
|
||||
// Provide allows the docker provider to provide configurations to traefik using the given configuration channel.
|
||||
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
|
||||
pool.GoCtx(func(routineCtx context.Context) {
|
||||
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, dockerName).Logger()
|
||||
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, DockerName).Logger()
|
||||
ctxLog := logger.WithContext(routineCtx)
|
||||
|
||||
operation := func() error {
|
||||
@@ -61,7 +62,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
|
||||
ctx, cancel := context.WithCancel(ctxLog)
|
||||
defer cancel()
|
||||
|
||||
ctx = log.Ctx(ctx).With().Str(logs.ProviderName, dockerName).Logger().WithContext(ctx)
|
||||
ctx = log.Ctx(ctx).With().Str(logs.ProviderName, DockerName).Logger().WithContext(ctx)
|
||||
|
||||
dockerClient, err := p.createClient(ctxLog)
|
||||
if err != nil {
|
||||
@@ -88,7 +89,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
|
||||
|
||||
configuration := builder.build(ctxLog, dockerDataList)
|
||||
configurationChan <- dynamic.Message{
|
||||
ProviderName: dockerName,
|
||||
ProviderName: DockerName,
|
||||
Configuration: configuration,
|
||||
}
|
||||
|
||||
@@ -111,7 +112,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
|
||||
configuration := builder.build(ctx, containers)
|
||||
if configuration != nil {
|
||||
message := dynamic.Message{
|
||||
ProviderName: dockerName,
|
||||
ProviderName: DockerName,
|
||||
Configuration: configuration,
|
||||
}
|
||||
select {
|
||||
|
||||
@@ -22,7 +22,8 @@ import (
|
||||
"github.com/traefik/traefik/v3/pkg/safe"
|
||||
)
|
||||
|
||||
const swarmName = "swarm"
|
||||
// SwarmName is the swarm provider name.
|
||||
const SwarmName = "swarm"
|
||||
|
||||
var _ provider.Provider = (*SwarmProvider)(nil)
|
||||
|
||||
@@ -57,7 +58,7 @@ func (p *SwarmProvider) Init() error {
|
||||
// Provide allows the docker provider to provide configurations to traefik using the given configuration channel.
|
||||
func (p *SwarmProvider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
|
||||
pool.GoCtx(func(routineCtx context.Context) {
|
||||
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, swarmName).Logger()
|
||||
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, SwarmName).Logger()
|
||||
ctxLog := logger.WithContext(routineCtx)
|
||||
|
||||
operation := func() error {
|
||||
@@ -65,7 +66,7 @@ func (p *SwarmProvider) Provide(configurationChan chan<- dynamic.Message, pool *
|
||||
ctx, cancel := context.WithCancel(ctxLog)
|
||||
defer cancel()
|
||||
|
||||
ctx = log.Ctx(ctx).With().Str(logs.ProviderName, swarmName).Logger().WithContext(ctx)
|
||||
ctx = log.Ctx(ctx).With().Str(logs.ProviderName, SwarmName).Logger().WithContext(ctx)
|
||||
|
||||
dockerClient, err := p.createClient(ctx)
|
||||
if err != nil {
|
||||
@@ -92,7 +93,7 @@ func (p *SwarmProvider) Provide(configurationChan chan<- dynamic.Message, pool *
|
||||
|
||||
configuration := builder.build(ctxLog, dockerDataList)
|
||||
configurationChan <- dynamic.Message{
|
||||
ProviderName: swarmName,
|
||||
ProviderName: SwarmName,
|
||||
Configuration: configuration,
|
||||
}
|
||||
if p.Watch {
|
||||
@@ -102,7 +103,7 @@ func (p *SwarmProvider) Provide(configurationChan chan<- dynamic.Message, pool *
|
||||
ticker := time.NewTicker(time.Duration(p.RefreshSeconds))
|
||||
|
||||
pool.GoCtx(func(ctx context.Context) {
|
||||
logger := log.Ctx(ctx).With().Str(logs.ProviderName, swarmName).Logger()
|
||||
logger := log.Ctx(ctx).With().Str(logs.ProviderName, SwarmName).Logger()
|
||||
ctx = logger.WithContext(ctx)
|
||||
|
||||
defer close(errChan)
|
||||
@@ -119,7 +120,7 @@ func (p *SwarmProvider) Provide(configurationChan chan<- dynamic.Message, pool *
|
||||
configuration := builder.build(ctx, services)
|
||||
if configuration != nil {
|
||||
configurationChan <- dynamic.Message{
|
||||
ProviderName: swarmName,
|
||||
ProviderName: SwarmName,
|
||||
Configuration: configuration,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ import (
|
||||
"github.com/traefik/traefik/v3/pkg/safe"
|
||||
)
|
||||
|
||||
// ProviderName is the ECS provider name.
|
||||
const ProviderName = "ecs"
|
||||
|
||||
// Provider holds configurations of the provider.
|
||||
type Provider struct {
|
||||
Constraints string `description:"Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container." json:"constraints,omitempty" toml:"constraints,omitempty" yaml:"constraints,omitempty" export:"true"`
|
||||
@@ -107,7 +110,7 @@ func (p *Provider) Init() error {
|
||||
// Provide configuration to traefik from ECS.
|
||||
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
|
||||
pool.GoCtx(func(routineCtx context.Context) {
|
||||
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, "ecs").Logger()
|
||||
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, ProviderName).Logger()
|
||||
ctxLog := logger.WithContext(routineCtx)
|
||||
|
||||
operation := func() error {
|
||||
|
||||
@@ -26,7 +26,8 @@ import (
|
||||
"github.com/traefik/traefik/v3/pkg/types"
|
||||
)
|
||||
|
||||
const providerName = "file"
|
||||
// ProviderName is the file provider name.
|
||||
const ProviderName = "file"
|
||||
|
||||
var _ provider.Provider = (*Provider)(nil)
|
||||
|
||||
@@ -52,7 +53,7 @@ func (p *Provider) Init() error {
|
||||
// Provide allows the file provider to provide configurations to traefik
|
||||
// using the given configuration channel.
|
||||
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
|
||||
logger := log.With().Str(logs.ProviderName, providerName).Logger()
|
||||
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
|
||||
|
||||
if p.Watch {
|
||||
var watchItems []string
|
||||
@@ -174,7 +175,7 @@ func (p *Provider) addWatcher(pool *safe.Pool, items []string, configurationChan
|
||||
|
||||
// Process events
|
||||
pool.GoCtx(func(ctx context.Context) {
|
||||
logger := log.With().Str(logs.ProviderName, providerName).Logger()
|
||||
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
|
||||
defer watcher.Close()
|
||||
for {
|
||||
select {
|
||||
@@ -218,7 +219,7 @@ func (p *Provider) applyConfiguration(configurationChan chan<- dynamic.Message)
|
||||
// buildConfiguration loads configuration either from file or a directory
|
||||
// specified by 'Filename'/'Directory' and returns a 'Configuration' object.
|
||||
func (p *Provider) buildConfiguration() (*dynamic.Configuration, error) {
|
||||
ctx := log.With().Str(logs.ProviderName, providerName).Logger().WithContext(context.Background())
|
||||
ctx := log.With().Str(logs.ProviderName, ProviderName).Logger().WithContext(context.Background())
|
||||
|
||||
if len(p.Directory) > 0 {
|
||||
configurations, err := p.collectFileConfigs(ctx, p.Directory, "")
|
||||
|
||||
@@ -25,6 +25,9 @@ import (
|
||||
|
||||
var _ provider.Provider = (*Provider)(nil)
|
||||
|
||||
// ProviderName is the http provider name.
|
||||
const ProviderName = "http"
|
||||
|
||||
const defaultMaxResponseBodySize = -1
|
||||
|
||||
// Provider is a provider.Provider implementation that queries an HTTP(s) endpoint for a configuration.
|
||||
@@ -78,7 +81,7 @@ func (p *Provider) Init() error {
|
||||
// Provide allows the provider to provide configurations to traefik using the given configuration channel.
|
||||
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
|
||||
pool.GoCtx(func(routineCtx context.Context) {
|
||||
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, "http").Logger()
|
||||
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, ProviderName).Logger()
|
||||
ctxLog := logger.WithContext(routineCtx)
|
||||
|
||||
operation := func() error {
|
||||
|
||||
@@ -44,7 +44,8 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
providerName = "kubernetescrd"
|
||||
// ProviderName is the Kubernetes CRD provider name.
|
||||
ProviderName = "kubernetescrd"
|
||||
providerNamespaceSeparator = "@"
|
||||
)
|
||||
|
||||
@@ -76,7 +77,7 @@ func (p *Provider) Init() error {
|
||||
// Provide allows the k8s provider to provide configurations to traefik
|
||||
// using the given configuration channel.
|
||||
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
|
||||
logger := log.With().Str(logs.ProviderName, providerName).Logger()
|
||||
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
|
||||
ctxLog := logger.WithContext(context.Background())
|
||||
|
||||
k8sClient, err := p.newK8sClient(ctxLog)
|
||||
@@ -131,7 +132,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
|
||||
default:
|
||||
p.lastConfiguration.Set(confHash)
|
||||
configurationChan <- dynamic.Message{
|
||||
ProviderName: providerName,
|
||||
ProviderName: ProviderName,
|
||||
Configuration: conf,
|
||||
}
|
||||
}
|
||||
@@ -166,7 +167,7 @@ func (p *Provider) FillExtensionBuilderRegistry(registry gateway.ExtensionBuilde
|
||||
return "", nil, fmt.Errorf("namespace %q is not allowed", namespace)
|
||||
}
|
||||
|
||||
return makeID(namespace, name) + providerNamespaceSeparator + providerName, nil, nil
|
||||
return makeID(namespace, name) + providerNamespaceSeparator + ProviderName, nil, nil
|
||||
})
|
||||
|
||||
registry.RegisterBackendFuncs(traefikv1alpha1.GroupName, "TraefikService", func(name, namespace string) (string, *dynamic.Service, error) {
|
||||
@@ -174,7 +175,7 @@ func (p *Provider) FillExtensionBuilderRegistry(registry gateway.ExtensionBuilde
|
||||
return "", nil, fmt.Errorf("namespace %q is not allowed", namespace)
|
||||
}
|
||||
|
||||
return makeID(namespace, name) + providerNamespaceSeparator + providerName, nil, nil
|
||||
return makeID(namespace, name) + providerNamespaceSeparator + ProviderName, nil, nil
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ func makeMiddlewareKeys(ctx context.Context, namespace string, middlewares []tra
|
||||
for _, mi := range middlewares {
|
||||
name := mi.Name
|
||||
|
||||
if !allowCrossNamespace && strings.HasSuffix(mi.Name, providerNamespaceSeparator+providerName) {
|
||||
if !allowCrossNamespace && strings.HasSuffix(mi.Name, providerNamespaceSeparator+ProviderName) {
|
||||
// Since we are not able to know if another namespace is in the name (namespace-name@kubernetescrd),
|
||||
// if the provider namespace kubernetescrd is used,
|
||||
// we don't allow this format to avoid cross namespace references.
|
||||
@@ -521,7 +521,7 @@ func (c configBuilder) makeServersTransportKey(parentNamespace string, serversTr
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if !c.allowCrossNamespace && strings.HasSuffix(serversTransportName, providerNamespaceSeparator+providerName) {
|
||||
if !c.allowCrossNamespace && strings.HasSuffix(serversTransportName, providerNamespaceSeparator+ProviderName) {
|
||||
// Since we are not able to know if another namespace is in the name (namespace-name@kubernetescrd),
|
||||
// if the provider namespace kubernetescrd is used,
|
||||
// we don't allow this format to avoid cross namespace references.
|
||||
@@ -543,7 +543,7 @@ func (c configBuilder) loadServers(parentNamespace string, svc traefikv1alpha1.L
|
||||
}
|
||||
|
||||
// If the service uses explicitly the provider suffix
|
||||
sanitizedName := strings.TrimSuffix(svc.Name, providerNamespaceSeparator+providerName)
|
||||
sanitizedName := strings.TrimSuffix(svc.Name, providerNamespaceSeparator+ProviderName)
|
||||
service, exists, err := c.client.GetService(namespace, sanitizedName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -799,7 +799,7 @@ func fullServiceName(ctx context.Context, namespace string, service traefikv1alp
|
||||
}
|
||||
|
||||
name, pName := splitSvcNameProvider(service.Name)
|
||||
if pName == providerName {
|
||||
if pName == ProviderName {
|
||||
return provider.Normalize(fmt.Sprintf("%s-%s", namespace, name))
|
||||
}
|
||||
|
||||
|
||||
@@ -339,7 +339,7 @@ func (p *Provider) makeTCPServersTransportKey(parentNamespace string, serversTra
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if !p.AllowCrossNamespace && strings.HasSuffix(serversTransportName, providerNamespaceSeparator+providerName) {
|
||||
if !p.AllowCrossNamespace && strings.HasSuffix(serversTransportName, providerNamespaceSeparator+ProviderName) {
|
||||
// Since we are not able to know if another namespace is in the name (namespace-name@kubernetescrd),
|
||||
// if the provider namespace kubernetescrd is used,
|
||||
// we don't allow this format to avoid cross namespace references.
|
||||
|
||||
@@ -34,7 +34,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
providerName = "kubernetesgateway"
|
||||
// ProviderName is the Kubernetes Gateway API provider name.
|
||||
ProviderName = "kubernetesgateway"
|
||||
|
||||
controllerName = "traefik.io/gateway-controller"
|
||||
|
||||
@@ -166,7 +167,7 @@ func (p *Provider) SetRouterTransform(routerTransform k8s.RouterTransform) {
|
||||
|
||||
// Init the provider.
|
||||
func (p *Provider) Init() error {
|
||||
logger := log.With().Str(logs.ProviderName, providerName).Logger()
|
||||
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
|
||||
|
||||
var err error
|
||||
p.client, err = p.newK8sClient(logger.WithContext(context.Background()))
|
||||
@@ -179,7 +180,7 @@ func (p *Provider) Init() error {
|
||||
|
||||
// Provide allows the k8s provider to provide configurations to traefik using the given configuration channel.
|
||||
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
|
||||
logger := log.With().Str(logs.ProviderName, providerName).Logger()
|
||||
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
|
||||
ctxLog := logger.WithContext(context.Background())
|
||||
|
||||
pool.GoCtx(func(ctxPool context.Context) {
|
||||
@@ -221,7 +222,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
|
||||
default:
|
||||
p.lastConfiguration.Set(confHash)
|
||||
configurationChan <- dynamic.Message{
|
||||
ProviderName: providerName,
|
||||
ProviderName: ProviderName,
|
||||
Configuration: conf,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
providerName = "kubernetesingressnginx"
|
||||
// ProviderName is the Kubernetes Ingress NGINX provider name.
|
||||
ProviderName = "kubernetesingressnginx"
|
||||
|
||||
// NGINX default values.
|
||||
annotationIngressClass = "kubernetes.io/ingress.class"
|
||||
@@ -279,7 +280,7 @@ func (p *Provider) Init() error {
|
||||
|
||||
// Provide allows the k8s provider to provide configurations to traefik using the given configuration channel.
|
||||
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
|
||||
logger := log.With().Str(logs.ProviderName, providerName).Logger()
|
||||
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
|
||||
ctxLog := logger.WithContext(context.Background())
|
||||
|
||||
pool.GoCtx(func(ctxPool context.Context) {
|
||||
@@ -323,7 +324,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
|
||||
default:
|
||||
p.lastConfiguration.Set(confHash)
|
||||
configurationChan <- dynamic.Message{
|
||||
ProviderName: providerName,
|
||||
ProviderName: ProviderName,
|
||||
Configuration: conf,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,10 +74,13 @@ func (p *Provider) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProviderName is the Kubernetes Ingress provider name.
|
||||
const ProviderName = "kubernetes"
|
||||
|
||||
// Provide allows the k8s provider to provide configurations to traefik
|
||||
// using the given configuration channel.
|
||||
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
|
||||
logger := log.With().Str(logs.ProviderName, "kubernetes").Logger()
|
||||
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
|
||||
ctxLog := logger.WithContext(context.Background())
|
||||
|
||||
k8sClient, err := p.newK8sClient(ctxLog)
|
||||
@@ -130,7 +133,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
|
||||
default:
|
||||
p.lastConfiguration.Set(confHash)
|
||||
configurationChan <- dynamic.Message{
|
||||
ProviderName: "kubernetes",
|
||||
ProviderName: ProviderName,
|
||||
Configuration: conf,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2465,10 +2465,10 @@ func TestStrictPrefixMatchingRule(t *testing.T) {
|
||||
parser, err := traefikhttp.NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer := traefikhttp.NewMuxer(parser)
|
||||
muxer := traefikhttp.NewMuxer(parser, nil)
|
||||
|
||||
rule := buildStrictPrefixMatchingRule(tt.path)
|
||||
err = muxer.AddRoute(rule, "", 0, handler)
|
||||
err = muxer.AddRoute(rule, "", 0, "", handler)
|
||||
require.NoError(t, err)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
@@ -32,7 +32,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
providerName = "knative"
|
||||
// ProviderName is the Knative provider name.
|
||||
ProviderName = "knative"
|
||||
traefikIngressClassName = "traefik.ingress.networking.knative.dev"
|
||||
)
|
||||
|
||||
@@ -61,7 +62,7 @@ type Provider struct {
|
||||
|
||||
// Init the provider.
|
||||
func (p *Provider) Init() error {
|
||||
logger := log.With().Str(logs.ProviderName, providerName).Logger()
|
||||
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
|
||||
|
||||
// Initializes Kubernetes client.
|
||||
var err error
|
||||
@@ -75,7 +76,7 @@ func (p *Provider) Init() error {
|
||||
|
||||
// Provide allows the knative provider to provide configurations to traefik using the given configuration channel.
|
||||
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
|
||||
logger := log.With().Str(logs.ProviderName, providerName).Logger()
|
||||
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
|
||||
ctxLog := logger.WithContext(context.Background())
|
||||
|
||||
pool.GoCtx(func(ctxPool context.Context) {
|
||||
@@ -117,7 +118,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
|
||||
default:
|
||||
p.lastConfiguration.Set(confHash)
|
||||
configurationChan <- dynamic.Message{
|
||||
ProviderName: providerName,
|
||||
ProviderName: ProviderName,
|
||||
Configuration: conf,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
"github.com/traefik/traefik/v3/pkg/types"
|
||||
)
|
||||
|
||||
// providerName is the Consul provider name.
|
||||
const providerName = "consul"
|
||||
// ProviderName is the Consul provider name.
|
||||
const ProviderName = "consul"
|
||||
|
||||
var _ provider.Provider = (*Provider)(nil)
|
||||
|
||||
@@ -38,7 +38,7 @@ func (p *ProviderBuilder) BuildProviders() []*Provider {
|
||||
if len(p.Namespaces) == 0 {
|
||||
return []*Provider{{
|
||||
Provider: p.Provider,
|
||||
name: providerName,
|
||||
name: ProviderName,
|
||||
token: p.Token,
|
||||
tls: p.TLS,
|
||||
}}
|
||||
@@ -48,7 +48,7 @@ func (p *ProviderBuilder) BuildProviders() []*Provider {
|
||||
for _, namespace := range p.Namespaces {
|
||||
providers = append(providers, &Provider{
|
||||
Provider: p.Provider,
|
||||
name: providerName + "-" + namespace,
|
||||
name: ProviderName + "-" + namespace,
|
||||
namespace: namespace,
|
||||
token: p.Token,
|
||||
tls: p.TLS,
|
||||
@@ -78,7 +78,7 @@ func (p *Provider) Init() error {
|
||||
|
||||
// In case they didn't initialize with BuildProviders.
|
||||
if p.name == "" {
|
||||
p.name = providerName
|
||||
p.name = ProviderName
|
||||
}
|
||||
|
||||
config := &consul.Config{
|
||||
|
||||
@@ -11,6 +11,9 @@ import (
|
||||
"github.com/traefik/traefik/v3/pkg/types"
|
||||
)
|
||||
|
||||
// ProviderName is the Etcd provider name.
|
||||
const ProviderName = "etcd"
|
||||
|
||||
var _ provider.Provider = (*Provider)(nil)
|
||||
|
||||
// Provider holds configurations of the provider.
|
||||
@@ -44,5 +47,5 @@ func (p *Provider) Init() error {
|
||||
}
|
||||
}
|
||||
|
||||
return p.Provider.Init(etcdv3.StoreName, "etcd", config)
|
||||
return p.Provider.Init(etcdv3.StoreName, ProviderName, config)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ import (
|
||||
"github.com/traefik/traefik/v3/pkg/types"
|
||||
)
|
||||
|
||||
// ProviderName is the Redis provider name.
|
||||
const ProviderName = "redis"
|
||||
|
||||
var _ provider.Provider = (*Provider)(nil)
|
||||
|
||||
// Provider holds configurations of the provider.
|
||||
@@ -90,5 +93,5 @@ func (p *Provider) Init() error {
|
||||
}
|
||||
}
|
||||
|
||||
return p.Provider.Init(redis.StoreName, "redis", config)
|
||||
return p.Provider.Init(redis.StoreName, ProviderName, config)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ import (
|
||||
"github.com/traefik/traefik/v3/pkg/provider/kv"
|
||||
)
|
||||
|
||||
// ProviderName is the ZooKeeper provider name.
|
||||
const ProviderName = "zookeeper"
|
||||
|
||||
var _ provider.Provider = (*Provider)(nil)
|
||||
|
||||
// Provider holds configurations of the provider.
|
||||
@@ -32,5 +35,5 @@ func (p *Provider) Init() error {
|
||||
Password: p.Password,
|
||||
}
|
||||
|
||||
return p.Provider.Init(zookeeper.StoreName, "zookeeper", config)
|
||||
return p.Provider.Init(zookeeper.StoreName, ProviderName, config)
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// providerName is the name of this provider.
|
||||
providerName = "nomad"
|
||||
// ProviderName is the Nomad provider name.
|
||||
ProviderName = "nomad"
|
||||
|
||||
// defaultTemplateRule is the default template for the default rule.
|
||||
defaultTemplateRule = "Host(`{{ normalize .Name }}`)"
|
||||
@@ -68,7 +68,7 @@ func (p *ProviderBuilder) BuildProviders() []*Provider {
|
||||
if len(p.Namespaces) == 0 {
|
||||
return []*Provider{{
|
||||
Configuration: p.Configuration,
|
||||
name: providerName,
|
||||
name: ProviderName,
|
||||
}}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ func (p *ProviderBuilder) BuildProviders() []*Provider {
|
||||
for _, namespace := range p.Namespaces {
|
||||
providers = append(providers, &Provider{
|
||||
Configuration: p.Configuration,
|
||||
name: providerName + "-" + namespace,
|
||||
name: ProviderName + "-" + namespace,
|
||||
namespace: namespace,
|
||||
})
|
||||
}
|
||||
@@ -169,7 +169,7 @@ func (p *Provider) Init() error {
|
||||
|
||||
// In case they didn't initialize Provider with BuildProviders
|
||||
if p.name == "" {
|
||||
p.name = providerName
|
||||
p.name = ProviderName
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -13,6 +13,9 @@ import (
|
||||
"github.com/unrolled/render"
|
||||
)
|
||||
|
||||
// ProviderName is the REST provider name.
|
||||
const ProviderName = "rest"
|
||||
|
||||
var _ provider.Provider = (*Provider)(nil)
|
||||
|
||||
// Provider is a provider.Provider implementation that provides a Rest API.
|
||||
@@ -40,7 +43,7 @@ func (p *Provider) CreateRouter() *mux.Router {
|
||||
|
||||
func (p *Provider) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
vars := mux.Vars(req)
|
||||
if vars["provider"] != "rest" {
|
||||
if vars["provider"] != ProviderName {
|
||||
http.Error(rw, "Only 'rest' provider can be updated through the REST API", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -53,7 +56,7 @@ func (p *Provider) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
p.configurationChan <- dynamic.Message{ProviderName: "rest", Configuration: configuration}
|
||||
p.configurationChan <- dynamic.Message{ProviderName: ProviderName, Configuration: configuration}
|
||||
if err := templatesRenderer.JSON(rw, http.StatusOK, configuration); err != nil {
|
||||
log.Error().Err(err).Send()
|
||||
}
|
||||
|
||||
@@ -17,7 +17,12 @@ import (
|
||||
"github.com/traefik/traefik/v3/pkg/tls"
|
||||
)
|
||||
|
||||
const defaultInternalEntryPointName = "traefik"
|
||||
const (
|
||||
// ProviderName is the internal Traefik provider name.
|
||||
ProviderName = "internal"
|
||||
|
||||
defaultInternalEntryPointName = "traefik"
|
||||
)
|
||||
|
||||
var _ provider.Provider = (*Provider)(nil)
|
||||
|
||||
@@ -38,10 +43,10 @@ func (i *Provider) ThrottleDuration() time.Duration {
|
||||
|
||||
// Provide allows the provider to provide configurations to traefik using the given configuration channel.
|
||||
func (i *Provider) Provide(configurationChan chan<- dynamic.Message, _ *safe.Pool) error {
|
||||
ctx := log.With().Str(logs.ProviderName, "internal").Logger().WithContext(context.Background())
|
||||
ctx := log.With().Str(logs.ProviderName, ProviderName).Logger().WithContext(context.Background())
|
||||
|
||||
configurationChan <- dynamic.Message{
|
||||
ProviderName: "internal",
|
||||
ProviderName: ProviderName,
|
||||
Configuration: i.createConfiguration(ctx),
|
||||
}
|
||||
|
||||
|
||||
+29
-18
@@ -39,13 +39,14 @@ type serviceManager interface {
|
||||
|
||||
// Manager A route/router manager.
|
||||
type Manager struct {
|
||||
routerHandlers map[string]http.Handler
|
||||
serviceManager serviceManager
|
||||
observabilityMgr *middleware.ObservabilityMgr
|
||||
middlewaresBuilder middlewareChainBuilder
|
||||
conf *runtime.Configuration
|
||||
tlsManager *tls.Manager
|
||||
parser httpmuxer.SyntaxParser
|
||||
routerHandlers map[string]http.Handler
|
||||
serviceManager serviceManager
|
||||
observabilityMgr *middleware.ObservabilityMgr
|
||||
middlewaresBuilder middlewareChainBuilder
|
||||
conf *runtime.Configuration
|
||||
tlsManager *tls.Manager
|
||||
parser httpmuxer.SyntaxParser
|
||||
providersPrecedence []string
|
||||
}
|
||||
|
||||
// NewManager creates a new Manager.
|
||||
@@ -55,15 +56,17 @@ func NewManager(conf *runtime.Configuration,
|
||||
observabilityMgr *middleware.ObservabilityMgr,
|
||||
tlsManager *tls.Manager,
|
||||
parser httpmuxer.SyntaxParser,
|
||||
providersPrecedence []string,
|
||||
) *Manager {
|
||||
return &Manager{
|
||||
routerHandlers: make(map[string]http.Handler),
|
||||
serviceManager: serviceManager,
|
||||
observabilityMgr: observabilityMgr,
|
||||
middlewaresBuilder: middlewaresBuilder,
|
||||
conf: conf,
|
||||
tlsManager: tlsManager,
|
||||
parser: parser,
|
||||
routerHandlers: make(map[string]http.Handler),
|
||||
serviceManager: serviceManager,
|
||||
observabilityMgr: observabilityMgr,
|
||||
middlewaresBuilder: middlewaresBuilder,
|
||||
conf: conf,
|
||||
tlsManager: tlsManager,
|
||||
parser: parser,
|
||||
providersPrecedence: providersPrecedence,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,7 +228,7 @@ func (m *Manager) getHTTPRouters(ctx context.Context, entryPoints []string, tls
|
||||
}
|
||||
|
||||
func (m *Manager) buildEntryPointHandler(ctx context.Context, entryPointName string, configs map[string]*runtime.RouterInfo, config dynamic.RouterObservabilityConfig) (http.Handler, error) {
|
||||
muxer := httpmuxer.NewMuxer(m.parser)
|
||||
muxer := httpmuxer.NewMuxer(m.parser, m.providersPrecedence)
|
||||
|
||||
defaultHandler, err := m.observabilityMgr.BuildEPChain(ctx, entryPointName, false, config).Then(http.NotFoundHandler())
|
||||
if err != nil {
|
||||
@@ -274,7 +277,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, entryPointName str
|
||||
continue
|
||||
}
|
||||
|
||||
if err = muxer.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, handler); err != nil {
|
||||
if err = muxer.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, providerName(routerName), handler); err != nil {
|
||||
routerConfig.AddError(err, true)
|
||||
logger.Error().Err(err).Send()
|
||||
continue
|
||||
@@ -456,7 +459,7 @@ func (m *Manager) handleCycle(victimRouter string, path []string) {
|
||||
|
||||
// buildChildRoutersMuxer creates a muxer for child routers.
|
||||
func (m *Manager) buildChildRoutersMuxer(ctx context.Context, entryPointName string, childRefs []string) (http.Handler, error) {
|
||||
childMuxer := httpmuxer.NewMuxer(m.parser)
|
||||
childMuxer := httpmuxer.NewMuxer(m.parser, m.providersPrecedence)
|
||||
|
||||
// Set a default handler for the child muxer (404 Not Found).
|
||||
childMuxer.SetDefaultHandler(http.NotFoundHandler())
|
||||
@@ -490,7 +493,7 @@ func (m *Manager) buildChildRoutersMuxer(ctx context.Context, entryPointName str
|
||||
}
|
||||
|
||||
// Add the child router to the muxer.
|
||||
if err = childMuxer.AddRoute(childRouter.Rule, childRouter.RuleSyntax, childRouter.Priority, childHandler); err != nil {
|
||||
if err = childMuxer.AddRoute(childRouter.Rule, childRouter.RuleSyntax, childRouter.Priority, providerName(childName), childHandler); err != nil {
|
||||
childRouter.AddError(err, true)
|
||||
logger.Error().Err(err).Send()
|
||||
continue
|
||||
@@ -506,3 +509,11 @@ func (m *Manager) buildChildRoutersMuxer(ctx context.Context, entryPointName str
|
||||
|
||||
return childMuxer, nil
|
||||
}
|
||||
|
||||
func providerName(routerName string) string {
|
||||
parts := strings.Split(routerName, "@")
|
||||
if len(parts) == 2 {
|
||||
return parts[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@ func TestRouterManager_Get(t *testing.T) {
|
||||
parser, err := httpmuxer.NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser)
|
||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser, []string{})
|
||||
|
||||
handlers := routerManager.BuildHandlers(t.Context(), test.entryPoints, false)
|
||||
|
||||
@@ -720,7 +720,7 @@ func TestRuntimeConfiguration(t *testing.T) {
|
||||
parser, err := httpmuxer.NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser)
|
||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser, []string{})
|
||||
|
||||
_ = routerManager.BuildHandlers(t.Context(), entryPoints, false)
|
||||
_ = routerManager.BuildHandlers(t.Context(), entryPoints, true)
|
||||
@@ -801,7 +801,7 @@ func TestProviderOnMiddlewares(t *testing.T) {
|
||||
parser, err := httpmuxer.NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser)
|
||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser, []string{})
|
||||
|
||||
_ = routerManager.BuildHandlers(t.Context(), entryPoints, false)
|
||||
|
||||
@@ -856,7 +856,7 @@ func BenchmarkRouterServe(b *testing.B) {
|
||||
parser, err := httpmuxer.NewSyntaxParser()
|
||||
require.NoError(b, err)
|
||||
|
||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser)
|
||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser, []string{})
|
||||
|
||||
handlers := routerManager.BuildHandlers(b.Context(), entryPoints, false)
|
||||
|
||||
@@ -905,6 +905,155 @@ func BenchmarkService(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvidersPrecedence(t *testing.T) {
|
||||
// Each provider gets its own service with a fake URL whose host encodes the
|
||||
// provider label. labellingProxyBuilder writes the host back as the X-From
|
||||
// response header so the test can identify which backend was selected.
|
||||
//
|
||||
// Service names must be fully qualified ("svc@<provider>") because the
|
||||
// router manager qualifies every unqualified name with the provider embedded
|
||||
// in the router's own name ("router@<provider>").
|
||||
svcFor := func(provider string) *dynamic.Service {
|
||||
return &dynamic.Service{
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Strategy: dynamic.BalancerStrategyWRR,
|
||||
Servers: []dynamic.Server{{URL: "http://" + provider}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
providersPrecedence []string
|
||||
routersConfig map[string]*dynamic.Router
|
||||
serviceConfig map[string]*dynamic.Service
|
||||
expectedFrom string
|
||||
}{
|
||||
{
|
||||
desc: "kubernetescrd beats kubernetes when listed after it",
|
||||
providersPrecedence: []string{"kubernetescrd", "kubernetes"},
|
||||
routersConfig: map[string]*dynamic.Router{
|
||||
// Service names are bare; the manager qualifies them with the
|
||||
// provider extracted from the router key (@kubernetes / @kubernetescrd).
|
||||
"router@kubernetes": {
|
||||
EntryPoints: []string{"web"},
|
||||
Rule: "Host(`foo.bar`)",
|
||||
Service: "svc",
|
||||
},
|
||||
"router@kubernetescrd": {
|
||||
EntryPoints: []string{"web"},
|
||||
Rule: "Host(`foo.bar`)",
|
||||
Service: "svc",
|
||||
},
|
||||
},
|
||||
serviceConfig: map[string]*dynamic.Service{
|
||||
"svc@kubernetes": svcFor("kubernetes"),
|
||||
"svc@kubernetescrd": svcFor("kubernetescrd"),
|
||||
},
|
||||
expectedFrom: "kubernetescrd",
|
||||
},
|
||||
{
|
||||
desc: "kubernetes beats kubernetescrd when listed after it",
|
||||
providersPrecedence: []string{"kubernetes", "kubernetescrd"},
|
||||
routersConfig: map[string]*dynamic.Router{
|
||||
"router@kubernetes": {
|
||||
EntryPoints: []string{"web"},
|
||||
Rule: "Host(`foo.bar`)",
|
||||
Service: "svc",
|
||||
},
|
||||
"router@kubernetescrd": {
|
||||
EntryPoints: []string{"web"},
|
||||
Rule: "Host(`foo.bar`)",
|
||||
Service: "svc",
|
||||
},
|
||||
},
|
||||
serviceConfig: map[string]*dynamic.Service{
|
||||
"svc@kubernetes": svcFor("kubernetes"),
|
||||
"svc@kubernetescrd": svcFor("kubernetescrd"),
|
||||
},
|
||||
expectedFrom: "kubernetes",
|
||||
},
|
||||
{
|
||||
desc: "higher numeric priority wins regardless of providersPrecedence",
|
||||
providersPrecedence: []string{"kubernetescrd", "kubernetes"},
|
||||
routersConfig: map[string]*dynamic.Router{
|
||||
"router@kubernetes": {
|
||||
EntryPoints: []string{"web"},
|
||||
Rule: "Host(`foo.bar`)",
|
||||
Priority: 100,
|
||||
Service: "svc",
|
||||
},
|
||||
"router@kubernetescrd": {
|
||||
EntryPoints: []string{"web"},
|
||||
Rule: "Host(`foo.bar`)",
|
||||
Priority: 10,
|
||||
Service: "svc",
|
||||
},
|
||||
},
|
||||
serviceConfig: map[string]*dynamic.Service{
|
||||
"svc@kubernetes": svcFor("kubernetes"),
|
||||
"svc@kubernetescrd": svcFor("kubernetescrd"),
|
||||
},
|
||||
expectedFrom: "kubernetes",
|
||||
},
|
||||
{
|
||||
desc: "provider not in providersPrecedence loses to any listed provider",
|
||||
providersPrecedence: []string{"kubernetes"},
|
||||
routersConfig: map[string]*dynamic.Router{
|
||||
"router@file": {
|
||||
EntryPoints: []string{"web"},
|
||||
Rule: "Host(`foo.bar`)",
|
||||
Service: "svc",
|
||||
},
|
||||
"router@kubernetes": {
|
||||
EntryPoints: []string{"web"},
|
||||
Rule: "Host(`foo.bar`)",
|
||||
Service: "svc",
|
||||
},
|
||||
},
|
||||
serviceConfig: map[string]*dynamic.Service{
|
||||
"svc@file": svcFor("file"),
|
||||
"svc@kubernetes": svcFor("kubernetes"),
|
||||
},
|
||||
expectedFrom: "kubernetes",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rtConf := runtime.NewConfig(dynamic.Configuration{
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Services: test.serviceConfig,
|
||||
Routers: test.routersConfig,
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
},
|
||||
})
|
||||
|
||||
transportManager := service.NewTransportManager(nil)
|
||||
transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
||||
|
||||
serviceManager := service.NewManager(rtConf.Services, nil, nil, transportManager, labellingProxyBuilder{})
|
||||
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
|
||||
tlsManager := traefiktls.NewManager(nil)
|
||||
|
||||
parser, err := httpmuxer.NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser, test.providersPrecedence)
|
||||
handlers := routerManager.BuildHandlers(t.Context(), []string{"web"}, false)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil)
|
||||
requestdecorator.New(nil).ServeHTTP(w, req, handlers["web"].ServeHTTP)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, test.expectedFrom, w.Header().Get("X-From"), "wrong provider won the route")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_ComputeMultiLayerRouting(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
@@ -1449,7 +1598,7 @@ func TestManager_buildChildRoutersMuxer(t *testing.T) {
|
||||
parser, err := httpmuxer.NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser)
|
||||
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser, []string{})
|
||||
|
||||
// Compute multi-layer routing to populate ChildRefs
|
||||
manager.ParseRouterTree()
|
||||
@@ -1640,7 +1789,7 @@ func TestManager_buildHTTPHandler_WithChildRouters(t *testing.T) {
|
||||
parser, err := httpmuxer.NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser)
|
||||
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser, []string{})
|
||||
|
||||
// Run ParseRouterTree to validate configuration and populate ChildRefs/errors
|
||||
manager.ParseRouterTree()
|
||||
@@ -1787,7 +1936,7 @@ func TestManager_BuildHandlers_WithChildRouters(t *testing.T) {
|
||||
parser, err := httpmuxer.NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser)
|
||||
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser, []string{})
|
||||
|
||||
// Compute multi-layer routing to set up parent-child relationships
|
||||
manager.ParseRouterTree()
|
||||
@@ -1942,7 +2091,7 @@ func TestManager_BuildHandlers_Deny(t *testing.T) {
|
||||
parser, err := httpmuxer.NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser)
|
||||
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser, []string{})
|
||||
|
||||
// Compute multi-layer routing to set up parent-child relationships
|
||||
manager.ParseRouterTree()
|
||||
@@ -2014,3 +2163,17 @@ func (p proxyBuilderMock) Build(_ string, _ *url.URL, _, _ bool, _ time.Duration
|
||||
func (p proxyBuilderMock) Update(_ map[string]*dynamic.ServersTransport) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
// labellingProxyBuilder builds a handler that writes the target URL host as
|
||||
// the X-From response header, allowing tests to identify which backend was
|
||||
// selected by the router.
|
||||
type labellingProxyBuilder struct{}
|
||||
|
||||
func (l labellingProxyBuilder) Build(_ string, target *url.URL, _, _ bool, _ time.Duration) (http.Handler, error) {
|
||||
label := target.Host
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("X-From", label)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (l labellingProxyBuilder) Update(_ map[string]*dynamic.ServersTransport) {}
|
||||
|
||||
@@ -30,12 +30,13 @@ type middlewareBuilder interface {
|
||||
|
||||
// Manager is a route/router manager.
|
||||
type Manager struct {
|
||||
serviceManager *tcpservice.Manager
|
||||
middlewaresBuilder middlewareBuilder
|
||||
httpHandlers map[string]http.Handler
|
||||
httpsHandlers map[string]http.Handler
|
||||
tlsManager *traefiktls.Manager
|
||||
conf *runtime.Configuration
|
||||
serviceManager *tcpservice.Manager
|
||||
middlewaresBuilder middlewareBuilder
|
||||
httpHandlers map[string]http.Handler
|
||||
httpsHandlers map[string]http.Handler
|
||||
tlsManager *traefiktls.Manager
|
||||
conf *runtime.Configuration
|
||||
providersPrecedence []string
|
||||
}
|
||||
|
||||
// NewManager Creates a new Manager.
|
||||
@@ -45,14 +46,16 @@ func NewManager(conf *runtime.Configuration,
|
||||
httpHandlers map[string]http.Handler,
|
||||
httpsHandlers map[string]http.Handler,
|
||||
tlsManager *traefiktls.Manager,
|
||||
providersPrecedence []string,
|
||||
) *Manager {
|
||||
return &Manager{
|
||||
serviceManager: serviceManager,
|
||||
middlewaresBuilder: middlewaresBuilder,
|
||||
httpHandlers: httpHandlers,
|
||||
httpsHandlers: httpsHandlers,
|
||||
tlsManager: tlsManager,
|
||||
conf: conf,
|
||||
serviceManager: serviceManager,
|
||||
middlewaresBuilder: middlewaresBuilder,
|
||||
httpHandlers: httpHandlers,
|
||||
httpsHandlers: httpsHandlers,
|
||||
tlsManager: tlsManager,
|
||||
conf: conf,
|
||||
providersPrecedence: providersPrecedence,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +104,7 @@ type nameAndConfig struct {
|
||||
|
||||
func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*runtime.TCPRouterInfo, configsHTTP map[string]*runtime.RouterInfo, handlerHTTP, handlerHTTPS http.Handler) (*Router, error) {
|
||||
// Build a new Router.
|
||||
router, err := NewRouter()
|
||||
router, err := NewRouter(m.providersPrecedence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -324,7 +327,7 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim
|
||||
if routerConfig.TLS == nil {
|
||||
logger.Debug().Msgf("Adding route for %q", routerConfig.Rule)
|
||||
|
||||
if err := router.muxerTCP.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, handler); err != nil {
|
||||
if err := router.muxerTCP.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, providerName(routerName), handler); err != nil {
|
||||
routerConfig.AddError(err, true)
|
||||
logger.Error().Err(err).Send()
|
||||
}
|
||||
@@ -334,7 +337,7 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim
|
||||
if routerConfig.TLS.Passthrough {
|
||||
logger.Debug().Msgf("Adding Passthrough route for %q", routerConfig.Rule)
|
||||
|
||||
if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, handler); err != nil {
|
||||
if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, providerName(routerName), handler); err != nil {
|
||||
routerConfig.AddError(err, true)
|
||||
logger.Error().Err(err).Send()
|
||||
}
|
||||
@@ -368,7 +371,7 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim
|
||||
|
||||
logger.Debug().Msgf("Adding special TLS closing route for %q because broken TLS options %s", routerConfig.Rule, tlsOptionsName)
|
||||
|
||||
if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, &brokenTLSRouter{}); err != nil {
|
||||
if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, providerName(routerName), &brokenTLSRouter{}); err != nil {
|
||||
routerConfig.AddError(err, true)
|
||||
logger.Error().Err(err).Send()
|
||||
}
|
||||
@@ -402,7 +405,7 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim
|
||||
|
||||
logger.Debug().Msgf("Adding TLS route for %q", routerConfig.Rule)
|
||||
|
||||
if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, handler); err != nil {
|
||||
if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, providerName(routerName), handler); err != nil {
|
||||
routerConfig.AddError(err, true)
|
||||
logger.Error().Err(err).Send()
|
||||
continue
|
||||
@@ -430,3 +433,11 @@ func (m *Manager) buildTCPHandler(ctx context.Context, router *runtime.TCPRouter
|
||||
|
||||
return tcp.NewChain().Extend(*mHandler).Then(sHandler)
|
||||
}
|
||||
|
||||
func providerName(routerName string) string {
|
||||
parts := strings.Split(routerName, "@")
|
||||
if len(parts) == 2 {
|
||||
return parts[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -367,7 +367,7 @@ func TestRuntimeConfiguration(t *testing.T) {
|
||||
middlewaresBuilder := tcpmiddleware.NewBuilder(conf.TCPMiddlewares)
|
||||
|
||||
routerManager := NewManager(conf, serviceManager, middlewaresBuilder,
|
||||
nil, nil, tlsManager)
|
||||
nil, nil, tlsManager, nil)
|
||||
|
||||
_ = routerManager.BuildHandlers(t.Context(), entryPoints)
|
||||
|
||||
@@ -668,7 +668,7 @@ func TestDomainFronting(t *testing.T) {
|
||||
|
||||
middlewaresBuilder := tcpmiddleware.NewBuilder(conf.TCPMiddlewares)
|
||||
|
||||
routerManager := NewManager(conf, serviceManager, middlewaresBuilder, nil, httpsHandler, tlsManager)
|
||||
routerManager := NewManager(conf, serviceManager, middlewaresBuilder, nil, httpsHandler, tlsManager, nil)
|
||||
|
||||
routers := routerManager.BuildHandlers(t.Context(), entryPoints)
|
||||
|
||||
|
||||
@@ -51,18 +51,18 @@ type Router struct {
|
||||
}
|
||||
|
||||
// NewRouter returns a new TCP router.
|
||||
func NewRouter() (*Router, error) {
|
||||
muxTCP, err := tcpmuxer.NewMuxer()
|
||||
func NewRouter(providersPrecedence []string) (*Router, error) {
|
||||
muxTCP, err := tcpmuxer.NewMuxer(providersPrecedence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
muxTCPTLS, err := tcpmuxer.NewMuxer()
|
||||
muxTCPTLS, err := tcpmuxer.NewMuxer(providersPrecedence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
muxHTTPS, err := tcpmuxer.NewMuxer()
|
||||
muxHTTPS, err := tcpmuxer.NewMuxer(providersPrecedence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -230,8 +230,8 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) {
|
||||
}
|
||||
|
||||
// AddTCPRoute defines a handler for the given rule.
|
||||
func (r *Router) AddTCPRoute(rule string, priority int, target tcp.Handler) error {
|
||||
return r.muxerTCP.AddRoute(rule, "", priority, target)
|
||||
func (r *Router) AddTCPRoute(rule string, priority int, providerName string, target tcp.Handler) error {
|
||||
return r.muxerTCP.AddRoute(rule, "", priority, providerName, target)
|
||||
}
|
||||
|
||||
// AddHTTPTLSConfig defines a handler for a given sniHost and sets the matching tlsConfig.
|
||||
@@ -273,8 +273,10 @@ func (r *Router) SetHTTPSForwarder(handler tcp.Handler) {
|
||||
}
|
||||
}
|
||||
|
||||
rule := "HostSNI(`" + sniHost + "`)"
|
||||
if err := r.muxerHTTPS.AddRoute(rule, "", tcpmuxer.GetRulePriority(rule), tcpHandler); err != nil {
|
||||
rule := fmt.Sprintf(`HostSNI(%q)`, sniHost)
|
||||
// As the hostHTTPTLSConfig contains only one TLS config per SNI,
|
||||
// there is no conflict thus the provider name can be passed as empty as no tie-break is needed.
|
||||
if err := r.muxerHTTPS.AddRoute(rule, "", tcpmuxer.GetRulePriority(rule), "", tcpHandler); err != nil {
|
||||
log.Error().Err(err).Msg("Error while adding route for host")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,47 +27,6 @@ import (
|
||||
"github.com/traefik/traefik/v3/pkg/types"
|
||||
)
|
||||
|
||||
type applyRouter func(conf *runtime.Configuration)
|
||||
|
||||
type checkRouter func(addr string, timeout time.Duration) error
|
||||
|
||||
type httpForwarder struct {
|
||||
net.Listener
|
||||
|
||||
connChan chan net.Conn
|
||||
errChan chan error
|
||||
}
|
||||
|
||||
func newHTTPForwarder(ln net.Listener) *httpForwarder {
|
||||
return &httpForwarder{
|
||||
Listener: ln,
|
||||
connChan: make(chan net.Conn),
|
||||
errChan: make(chan error),
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the Listener.
|
||||
func (h *httpForwarder) Close() error {
|
||||
h.errChan <- http.ErrServerClosed
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeTCP uses the connection to serve it later in "Accept".
|
||||
func (h *httpForwarder) ServeTCP(conn tcp2.WriteCloser) {
|
||||
h.connChan <- conn
|
||||
}
|
||||
|
||||
// Accept retrieves a served connection in ServeTCP.
|
||||
func (h *httpForwarder) Accept() (net.Conn, error) {
|
||||
select {
|
||||
case conn := <-h.connChan:
|
||||
return conn, nil
|
||||
case err := <-h.errChan:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Test_Routing aims to settle the behavior between routers of different types on the same TCP entryPoint.
|
||||
// It has been introduced as a regression test following a fix on the v2.7 TCP Muxer.
|
||||
//
|
||||
@@ -202,7 +161,7 @@ func Test_Routing(t *testing.T) {
|
||||
middlewaresBuilder := tcpmiddleware.NewBuilder(conf.TCPMiddlewares)
|
||||
|
||||
manager := NewManager(conf, serviceManager, middlewaresBuilder,
|
||||
nil, nil, tlsManager)
|
||||
nil, nil, tlsManager, nil)
|
||||
|
||||
type checkCase struct {
|
||||
checkRouter
|
||||
@@ -701,7 +660,7 @@ func Test_Routing(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_Router_acmeTLSALPNHandlerTimeout(t *testing.T) {
|
||||
router, err := NewRouter()
|
||||
router, err := NewRouter(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
router.httpsTLSConfig = &tls.Config{}
|
||||
@@ -758,6 +717,272 @@ func Test_Router_acmeTLSALPNHandlerTimeout(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test_clientHelloInfo_oversizedRecordLength verifies that clientHelloInfo
|
||||
// does not block or allocate excessive memory when a client sends a TLS
|
||||
// record header with a maliciously large record length (up to 0xFFFF).
|
||||
//
|
||||
// Without the fix, clientHelloInfo allocates a ~65KB bufio.Reader and blocks
|
||||
// on Peek(65540), waiting for bytes that never arrive (until readTimeout).
|
||||
// With the fix, records exceeding the TLS maximum plaintext size (16384)
|
||||
// are rejected immediately.
|
||||
func Test_clientHelloInfo_oversizedRecordLength(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
recLen uint16
|
||||
}{
|
||||
{
|
||||
desc: "max uint16 record length (0xFFFF)",
|
||||
recLen: 0xFFFF,
|
||||
},
|
||||
{
|
||||
desc: "just above TLS maximum (18433)",
|
||||
recLen: 18433,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
serverConn, clientConn := net.Pipe()
|
||||
defer serverConn.Close()
|
||||
defer clientConn.Close()
|
||||
|
||||
type result struct {
|
||||
hello *clientHello
|
||||
err error
|
||||
}
|
||||
resultCh := make(chan result, 1)
|
||||
|
||||
go func() {
|
||||
pConn := &peekConn{reader: bufio.NewReader(serverConn)}
|
||||
hello, err := clientHelloInfo(pConn)
|
||||
resultCh <- result{hello, err}
|
||||
}()
|
||||
|
||||
// Send a TLS record header with an oversized record length.
|
||||
// Only the 5-byte header is sent; the client then stalls.
|
||||
hdr := []byte{
|
||||
0x16, // Content Type: Handshake
|
||||
0x03, 0x03, // Version: TLS 1.2
|
||||
byte(test.recLen >> 8), // Length high byte
|
||||
byte(test.recLen & 0xFF), // Length low byte
|
||||
}
|
||||
_, err := clientConn.Write(hdr)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Without the fix, clientHelloInfo blocks on Peek(recLen+5)
|
||||
// since only 5 bytes are available. The test would time out.
|
||||
// With the fix, it returns immediately.
|
||||
select {
|
||||
case r := <-resultCh:
|
||||
require.Error(t, r.err)
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("clientHelloInfo blocked on oversized TLS record length — recLen is not capped")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test_clientHelloInfo_tlsRecordFragmentation documents a known limitation:
|
||||
// clientHelloInfo only reads a single TLS record. When a ClientHello handshake
|
||||
// message is split across multiple TLS records (RFC 5246 §6.2.1), the SNI cannot
|
||||
// be extracted, leaving serverName empty and allowing SNI-based routing to be bypassed.
|
||||
func Test_clientHelloInfo_tlsRecordFragmentation(t *testing.T) {
|
||||
serverName := "foo.example.com"
|
||||
record := buildClientHelloRecord(t, serverName)
|
||||
|
||||
const hdrLen = 5
|
||||
payload := record[hdrLen:]
|
||||
|
||||
ver1, ver2 := record[1], record[2]
|
||||
|
||||
var recordsData bytes.Buffer
|
||||
for _, part := range [][]byte{payload[:len(serverName)/2], payload[len(serverName)/2:]} {
|
||||
recordsData.WriteByte(0x16)
|
||||
recordsData.WriteByte(ver1)
|
||||
recordsData.WriteByte(ver2)
|
||||
recordsData.WriteByte(byte(len(part) >> 8))
|
||||
recordsData.WriteByte(byte(len(part)))
|
||||
recordsData.Write(part)
|
||||
}
|
||||
|
||||
serverConn, clientConn := net.Pipe()
|
||||
t.Cleanup(func() {
|
||||
_ = serverConn.Close()
|
||||
_ = clientConn.Close()
|
||||
})
|
||||
|
||||
type result struct {
|
||||
hello *clientHello
|
||||
err error
|
||||
}
|
||||
resultCh := make(chan result, 1)
|
||||
|
||||
go func() {
|
||||
pConn := &peekConn{reader: bufio.NewReader(serverConn)}
|
||||
hello, err := clientHelloInfo(pConn)
|
||||
resultCh <- result{hello, err}
|
||||
}()
|
||||
|
||||
_, err := clientConn.Write(recordsData.Bytes())
|
||||
require.NoError(t, err)
|
||||
_ = clientConn.Close()
|
||||
|
||||
select {
|
||||
case r := <-resultCh:
|
||||
require.NoError(t, r.err)
|
||||
require.NotNil(t, r.hello)
|
||||
assert.True(t, r.hello.isTLS)
|
||||
assert.Equal(t, serverName, r.hello.serverName)
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("clientHelloInfo blocked")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostgresTLSTermination(t *testing.T) {
|
||||
certPEM, keyPEM, err := generate.KeyPair("test.localhost", time.Time{})
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
require.NoError(t, err)
|
||||
|
||||
tlsConf := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
|
||||
router, err := NewRouter(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Register a TCPTLS route (TLS termination, not passthrough) with a TLSHandler.
|
||||
// The TLSHandler wraps the actual handler, performing the TLS handshake.
|
||||
err = router.muxerTCPTLS.AddRoute("HostSNI(`test.localhost`)", "", 0, "", &tcp2.TLSHandler{
|
||||
Config: tlsConf,
|
||||
Next: tcp2.HandlerFunc(func(conn tcp2.WriteCloser) {
|
||||
_, _ = conn.Write([]byte("OK"))
|
||||
_ = conn.Close()
|
||||
}),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = ln.Close() })
|
||||
|
||||
go func() {
|
||||
conn, err := ln.Accept()
|
||||
require.NoError(t, err)
|
||||
|
||||
tcpConn := conn.(*net.TCPConn)
|
||||
router.ServeTCP(tcpConn)
|
||||
}()
|
||||
|
||||
clientConn, err := net.Dial("tcp", ln.Addr().String())
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = clientConn.Close() })
|
||||
|
||||
// Step 1: Client sends PostgresStartTLSMsg (SSLRequest).
|
||||
_, err = clientConn.Write(PostgresStartTLSMsg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Step 2: Client receives PostgresStartTLSReply ('S').
|
||||
reply := make([]byte, 1)
|
||||
_, err = io.ReadFull(clientConn, reply)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, PostgresStartTLSReply, reply)
|
||||
|
||||
// Step 3: Client performs TLS handshake.
|
||||
tlsClient := tls.Client(clientConn, &tls.Config{
|
||||
ServerName: "test.localhost",
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
require.NoError(t, tlsClient.Handshake())
|
||||
t.Cleanup(func() { _ = tlsClient.Close() })
|
||||
|
||||
// Step 4: Read the response from the handler through the TLS connection.
|
||||
buf := make([]byte, 256)
|
||||
n, err := tlsClient.Read(buf)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "OK", string(buf[:n]))
|
||||
}
|
||||
|
||||
func TestPostgresTLSPassthrough(t *testing.T) {
|
||||
certPEM, keyPEM, err := generate.KeyPair("test.localhost", time.Time{})
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
require.NoError(t, err)
|
||||
|
||||
tlsConf := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
|
||||
router, err := NewRouter(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Register a TCPTLS route (TLS passthrough) with a tcp.Handler.
|
||||
err = router.muxerTCPTLS.AddRoute("HostSNI(`test.localhost`)", "", 0, "", tcp2.HandlerFunc(func(conn tcp2.WriteCloser) {
|
||||
// First we should receive the PostgresStartTLSMsg.
|
||||
buf := make([]byte, len(PostgresStartTLSMsg))
|
||||
_, err := conn.Read(buf)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, PostgresStartTLSMsg, buf)
|
||||
|
||||
// Next we should answer with the PostgresStartTLSReply.
|
||||
_, err = conn.Write(PostgresStartTLSReply)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then we should do the TLS handshake.
|
||||
tlsConn := tls.Server(conn, tlsConf)
|
||||
require.NoError(t, tlsConn.Handshake())
|
||||
|
||||
// Finally we write the response through the TLS connection.
|
||||
_, err = tlsConn.Write([]byte("OK"))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = ln.Close() })
|
||||
|
||||
go func() {
|
||||
conn, err := ln.Accept()
|
||||
require.NoError(t, err)
|
||||
|
||||
tcpConn := conn.(*net.TCPConn)
|
||||
router.ServeTCP(tcpConn)
|
||||
}()
|
||||
|
||||
clientConn, err := net.Dial("tcp", ln.Addr().String())
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = clientConn.Close() })
|
||||
|
||||
// Step 1: Client sends PostgresStartTLSMsg (SSLRequest).
|
||||
_, err = clientConn.Write(PostgresStartTLSMsg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Step 2: Client receives PostgresStartTLSReply ('S').
|
||||
reply := make([]byte, 1)
|
||||
_, err = io.ReadFull(clientConn, reply)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, PostgresStartTLSReply, reply)
|
||||
|
||||
// Step 3: Client performs TLS handshake.
|
||||
tlsClient := tls.Client(clientConn, &tls.Config{
|
||||
ServerName: "test.localhost",
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
require.NoError(t, tlsClient.Handshake())
|
||||
t.Cleanup(func() { _ = tlsClient.Close() })
|
||||
|
||||
// Step 4: Read the response from the handler through the TLS connection.
|
||||
buf := make([]byte, 256)
|
||||
n, err := tlsClient.Read(buf)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "OK", string(buf[:n]))
|
||||
}
|
||||
|
||||
// routerTCPCatchAll configures a TCP CatchAll No TLS - HostSNI(`*`) router.
|
||||
func routerTCPCatchAll(conf *runtime.Configuration) {
|
||||
conf.TCPRouters["tcp-catchall"] = &runtime.TCPRouterInfo{
|
||||
@@ -1084,129 +1309,6 @@ func checkHTTPSTLS12(addr string, timeout time.Duration) error {
|
||||
return checkHTTPS(addr, timeout, tls.VersionTLS12)
|
||||
}
|
||||
|
||||
// Test_clientHelloInfo_oversizedRecordLength verifies that clientHelloInfo
|
||||
// does not block or allocate excessive memory when a client sends a TLS
|
||||
// record header with a maliciously large record length (up to 0xFFFF).
|
||||
//
|
||||
// Without the fix, clientHelloInfo allocates a ~65KB bufio.Reader and blocks
|
||||
// on Peek(65540), waiting for bytes that never arrive (until readTimeout).
|
||||
// With the fix, records exceeding the TLS maximum plaintext size (16384)
|
||||
// are rejected immediately.
|
||||
func Test_clientHelloInfo_oversizedRecordLength(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
recLen uint16
|
||||
}{
|
||||
{
|
||||
desc: "max uint16 record length (0xFFFF)",
|
||||
recLen: 0xFFFF,
|
||||
},
|
||||
{
|
||||
desc: "just above TLS maximum (18433)",
|
||||
recLen: 18433,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
serverConn, clientConn := net.Pipe()
|
||||
defer serverConn.Close()
|
||||
defer clientConn.Close()
|
||||
|
||||
type result struct {
|
||||
hello *clientHello
|
||||
err error
|
||||
}
|
||||
resultCh := make(chan result, 1)
|
||||
|
||||
go func() {
|
||||
pConn := &peekConn{reader: bufio.NewReader(serverConn)}
|
||||
hello, err := clientHelloInfo(pConn)
|
||||
resultCh <- result{hello, err}
|
||||
}()
|
||||
|
||||
// Send a TLS record header with an oversized record length.
|
||||
// Only the 5-byte header is sent; the client then stalls.
|
||||
hdr := []byte{
|
||||
0x16, // Content Type: Handshake
|
||||
0x03, 0x03, // Version: TLS 1.2
|
||||
byte(test.recLen >> 8), // Length high byte
|
||||
byte(test.recLen & 0xFF), // Length low byte
|
||||
}
|
||||
_, err := clientConn.Write(hdr)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Without the fix, clientHelloInfo blocks on Peek(recLen+5)
|
||||
// since only 5 bytes are available. The test would time out.
|
||||
// With the fix, it returns immediately.
|
||||
select {
|
||||
case r := <-resultCh:
|
||||
require.Error(t, r.err)
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("clientHelloInfo blocked on oversized TLS record length — recLen is not capped")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test_clientHelloInfo_tlsRecordFragmentation documents a known limitation:
|
||||
// clientHelloInfo only reads a single TLS record. When a ClientHello handshake
|
||||
// message is split across multiple TLS records (RFC 5246 §6.2.1), the SNI cannot
|
||||
// be extracted, leaving serverName empty and allowing SNI-based routing to be bypassed.
|
||||
func Test_clientHelloInfo_tlsRecordFragmentation(t *testing.T) {
|
||||
serverName := "foo.example.com"
|
||||
record := buildClientHelloRecord(t, serverName)
|
||||
|
||||
const hdrLen = 5
|
||||
payload := record[hdrLen:]
|
||||
|
||||
ver1, ver2 := record[1], record[2]
|
||||
|
||||
var recordsData bytes.Buffer
|
||||
for _, part := range [][]byte{payload[:len(serverName)/2], payload[len(serverName)/2:]} {
|
||||
recordsData.WriteByte(0x16)
|
||||
recordsData.WriteByte(ver1)
|
||||
recordsData.WriteByte(ver2)
|
||||
recordsData.WriteByte(byte(len(part) >> 8))
|
||||
recordsData.WriteByte(byte(len(part)))
|
||||
recordsData.Write(part)
|
||||
}
|
||||
|
||||
serverConn, clientConn := net.Pipe()
|
||||
t.Cleanup(func() {
|
||||
_ = serverConn.Close()
|
||||
_ = clientConn.Close()
|
||||
})
|
||||
|
||||
type result struct {
|
||||
hello *clientHello
|
||||
err error
|
||||
}
|
||||
resultCh := make(chan result, 1)
|
||||
|
||||
go func() {
|
||||
pConn := &peekConn{reader: bufio.NewReader(serverConn)}
|
||||
hello, err := clientHelloInfo(pConn)
|
||||
resultCh <- result{hello, err}
|
||||
}()
|
||||
|
||||
_, err := clientConn.Write(recordsData.Bytes())
|
||||
require.NoError(t, err)
|
||||
_ = clientConn.Close()
|
||||
|
||||
select {
|
||||
case r := <-resultCh:
|
||||
require.NoError(t, r.err)
|
||||
require.NotNil(t, r.hello)
|
||||
assert.True(t, r.hello.isTLS)
|
||||
assert.Equal(t, serverName, r.hello.serverName)
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("clientHelloInfo blocked")
|
||||
}
|
||||
}
|
||||
|
||||
// buildClientHelloRecord captures a real TLS ClientHello record from Go's TLS stack
|
||||
// for the given serverName.
|
||||
// It returns the raw record bytes and the byte offset of the SNI value within those bytes.
|
||||
@@ -1237,145 +1339,43 @@ func buildClientHelloRecord(t *testing.T, serverName string) []byte {
|
||||
return record
|
||||
}
|
||||
|
||||
func TestPostgresTLSTermination(t *testing.T) {
|
||||
certPEM, keyPEM, err := generate.KeyPair("test.localhost", time.Time{})
|
||||
require.NoError(t, err)
|
||||
type applyRouter func(conf *runtime.Configuration)
|
||||
|
||||
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
require.NoError(t, err)
|
||||
type checkRouter func(addr string, timeout time.Duration) error
|
||||
|
||||
tlsConf := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
type httpForwarder struct {
|
||||
net.Listener
|
||||
|
||||
router, err := NewRouter()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Register a TCPTLS route (TLS termination, not passthrough) with a TLSHandler.
|
||||
// The TLSHandler wraps the actual handler, performing the TLS handshake.
|
||||
err = router.muxerTCPTLS.AddRoute("HostSNI(`test.localhost`)", "", 0, &tcp2.TLSHandler{
|
||||
Config: tlsConf,
|
||||
Next: tcp2.HandlerFunc(func(conn tcp2.WriteCloser) {
|
||||
_, _ = conn.Write([]byte("OK"))
|
||||
_ = conn.Close()
|
||||
}),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = ln.Close() })
|
||||
|
||||
go func() {
|
||||
conn, err := ln.Accept()
|
||||
require.NoError(t, err)
|
||||
|
||||
tcpConn := conn.(*net.TCPConn)
|
||||
router.ServeTCP(tcpConn)
|
||||
}()
|
||||
|
||||
clientConn, err := net.Dial("tcp", ln.Addr().String())
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = clientConn.Close() })
|
||||
|
||||
// Step 1: Client sends PostgresStartTLSMsg (SSLRequest).
|
||||
_, err = clientConn.Write(PostgresStartTLSMsg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Step 2: Client receives PostgresStartTLSReply ('S').
|
||||
reply := make([]byte, 1)
|
||||
_, err = io.ReadFull(clientConn, reply)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, PostgresStartTLSReply, reply)
|
||||
|
||||
// Step 3: Client performs TLS handshake.
|
||||
tlsClient := tls.Client(clientConn, &tls.Config{
|
||||
ServerName: "test.localhost",
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
require.NoError(t, tlsClient.Handshake())
|
||||
t.Cleanup(func() { _ = tlsClient.Close() })
|
||||
|
||||
// Step 4: Read the response from the handler through the TLS connection.
|
||||
buf := make([]byte, 256)
|
||||
n, err := tlsClient.Read(buf)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "OK", string(buf[:n]))
|
||||
connChan chan net.Conn
|
||||
errChan chan error
|
||||
}
|
||||
|
||||
func TestPostgresTLSPassthrough(t *testing.T) {
|
||||
certPEM, keyPEM, err := generate.KeyPair("test.localhost", time.Time{})
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
require.NoError(t, err)
|
||||
|
||||
tlsConf := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
func newHTTPForwarder(ln net.Listener) *httpForwarder {
|
||||
return &httpForwarder{
|
||||
Listener: ln,
|
||||
connChan: make(chan net.Conn),
|
||||
errChan: make(chan error),
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the Listener.
|
||||
func (h *httpForwarder) Close() error {
|
||||
h.errChan <- http.ErrServerClosed
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeTCP uses the connection to serve it later in "Accept".
|
||||
func (h *httpForwarder) ServeTCP(conn tcp2.WriteCloser) {
|
||||
h.connChan <- conn
|
||||
}
|
||||
|
||||
// Accept retrieves a served connection in ServeTCP.
|
||||
func (h *httpForwarder) Accept() (net.Conn, error) {
|
||||
select {
|
||||
case conn := <-h.connChan:
|
||||
return conn, nil
|
||||
case err := <-h.errChan:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
router, err := NewRouter()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Register a TCPTLS route (TLS passthrough) with a tcp.Handler.
|
||||
err = router.muxerTCPTLS.AddRoute("HostSNI(`test.localhost`)", "", 0, tcp2.HandlerFunc(func(conn tcp2.WriteCloser) {
|
||||
// First we should receive the PostgresStartTLSMsg.
|
||||
buf := make([]byte, len(PostgresStartTLSMsg))
|
||||
_, err := conn.Read(buf)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, PostgresStartTLSMsg, buf)
|
||||
|
||||
// Next we should answer with the PostgresStartTLSReply.
|
||||
_, err = conn.Write(PostgresStartTLSReply)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then we should do the TLS handshake.
|
||||
tlsConn := tls.Server(conn, tlsConf)
|
||||
require.NoError(t, tlsConn.Handshake())
|
||||
|
||||
// Finally we write the response through the TLS connection.
|
||||
_, err = tlsConn.Write([]byte("OK"))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = ln.Close() })
|
||||
|
||||
go func() {
|
||||
conn, err := ln.Accept()
|
||||
require.NoError(t, err)
|
||||
|
||||
tcpConn := conn.(*net.TCPConn)
|
||||
router.ServeTCP(tcpConn)
|
||||
}()
|
||||
|
||||
clientConn, err := net.Dial("tcp", ln.Addr().String())
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = clientConn.Close() })
|
||||
|
||||
// Step 1: Client sends PostgresStartTLSMsg (SSLRequest).
|
||||
_, err = clientConn.Write(PostgresStartTLSMsg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Step 2: Client receives PostgresStartTLSReply ('S').
|
||||
reply := make([]byte, 1)
|
||||
_, err = io.ReadFull(clientConn, reply)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, PostgresStartTLSReply, reply)
|
||||
|
||||
// Step 3: Client performs TLS handshake.
|
||||
tlsClient := tls.Client(clientConn, &tls.Config{
|
||||
ServerName: "test.localhost",
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
require.NoError(t, tlsClient.Handshake())
|
||||
t.Cleanup(func() { _ = tlsClient.Close() })
|
||||
|
||||
// Step 4: Read the response from the handler through the TLS connection.
|
||||
buf := make([]byte, 256)
|
||||
n, err := tlsClient.Read(buf)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "OK", string(buf[:n]))
|
||||
}
|
||||
|
||||
+19
-12
@@ -39,7 +39,8 @@ type RouterFactory struct {
|
||||
|
||||
cancelPrevState func()
|
||||
|
||||
parser httpmuxer.SyntaxParser
|
||||
parser httpmuxer.SyntaxParser
|
||||
providersPrecedence []string
|
||||
}
|
||||
|
||||
// NewRouterFactory creates a new RouterFactory.
|
||||
@@ -77,16 +78,22 @@ func NewRouterFactory(staticConfiguration static.Configuration, managerFactory *
|
||||
return nil, fmt.Errorf("creating parser: %w", err)
|
||||
}
|
||||
|
||||
var providersPrecedence []string
|
||||
if staticConfiguration.Providers != nil {
|
||||
providersPrecedence = staticConfiguration.Providers.Precedence
|
||||
}
|
||||
|
||||
return &RouterFactory{
|
||||
entryPointsTCP: entryPointsTCP,
|
||||
entryPointsUDP: entryPointsUDP,
|
||||
managerFactory: managerFactory,
|
||||
observabilityMgr: observabilityMgr,
|
||||
tlsManager: tlsManager,
|
||||
pluginBuilder: pluginBuilder,
|
||||
dialerManager: dialerManager,
|
||||
allowACMEByPass: allowACMEByPass,
|
||||
parser: parser,
|
||||
entryPointsTCP: entryPointsTCP,
|
||||
entryPointsUDP: entryPointsUDP,
|
||||
managerFactory: managerFactory,
|
||||
observabilityMgr: observabilityMgr,
|
||||
tlsManager: tlsManager,
|
||||
pluginBuilder: pluginBuilder,
|
||||
dialerManager: dialerManager,
|
||||
allowACMEByPass: allowACMEByPass,
|
||||
parser: parser,
|
||||
providersPrecedence: providersPrecedence,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -106,7 +113,7 @@ func (f *RouterFactory) CreateRouters(rtConf *runtime.Configuration) (map[string
|
||||
|
||||
serviceManager.SetMiddlewareChainBuilder(middlewaresBuilder)
|
||||
|
||||
routerManager := router.NewManager(rtConf, serviceManager, middlewaresBuilder, f.observabilityMgr, f.tlsManager, f.parser)
|
||||
routerManager := router.NewManager(rtConf, serviceManager, middlewaresBuilder, f.observabilityMgr, f.tlsManager, f.parser, f.providersPrecedence)
|
||||
|
||||
routerManager.ParseRouterTree()
|
||||
|
||||
@@ -120,7 +127,7 @@ func (f *RouterFactory) CreateRouters(rtConf *runtime.Configuration) (map[string
|
||||
|
||||
middlewaresTCPBuilder := tcpmiddleware.NewBuilder(rtConf.TCPMiddlewares)
|
||||
|
||||
rtTCPManager := tcprouter.NewManager(rtConf, svcTCPManager, middlewaresTCPBuilder, handlersNonTLS, handlersTLS, f.tlsManager)
|
||||
rtTCPManager := tcprouter.NewManager(rtConf, svcTCPManager, middlewaresTCPBuilder, handlersNonTLS, handlersTLS, f.tlsManager, f.providersPrecedence)
|
||||
routersTCP := rtTCPManager.BuildHandlers(ctx, f.entryPointsTCP)
|
||||
|
||||
for ep, r := range routersTCP {
|
||||
|
||||
@@ -187,7 +187,7 @@ func NewTCPEntryPoint(ctx context.Context, name string, config *static.EntryPoin
|
||||
return nil, fmt.Errorf("building listener: %w", err)
|
||||
}
|
||||
|
||||
rt, err := tcprouter.NewRouter()
|
||||
rt, err := tcprouter.NewRouter(nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating TCP router: %w", err)
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ func TestHTTP3AdvertisedPort(t *testing.T) {
|
||||
}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
router, err := tcprouter.NewRouter()
|
||||
router, err := tcprouter.NewRouter(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
router.AddHTTPTLSConfig("*", &tls.Config{
|
||||
@@ -159,7 +159,7 @@ func TestHTTP30RTT(t *testing.T) {
|
||||
}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
router, err := tcprouter.NewRouter()
|
||||
router, err := tcprouter.NewRouter(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
router.AddHTTPTLSConfig("example.com", &tls.Config{
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
)
|
||||
|
||||
func TestShutdownHijacked(t *testing.T) {
|
||||
router, err := tcprouter.NewRouter()
|
||||
router, err := tcprouter.NewRouter(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
@@ -40,7 +40,7 @@ func TestShutdownHijacked(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestShutdownHTTP(t *testing.T) {
|
||||
router, err := tcprouter.NewRouter()
|
||||
router, err := tcprouter.NewRouter(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
@@ -52,10 +52,10 @@ func TestShutdownHTTP(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestShutdownTCP(t *testing.T) {
|
||||
router, err := tcprouter.NewRouter()
|
||||
router, err := tcprouter.NewRouter(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = router.AddTCPRoute("HostSNI(`*`)", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {
|
||||
err = router.AddTCPRoute("HostSNI(`*`)", 0, "", tcp.HandlerFunc(func(conn tcp.WriteCloser) {
|
||||
_, err := http.ReadRequest(bufio.NewReader(conn))
|
||||
if err != nil {
|
||||
return
|
||||
@@ -177,7 +177,7 @@ func TestReadTimeoutWithoutFirstByte(t *testing.T) {
|
||||
}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
router, err := tcprouter.NewRouter()
|
||||
router, err := tcprouter.NewRouter(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
@@ -216,7 +216,7 @@ func TestReadTimeoutWithFirstByte(t *testing.T) {
|
||||
}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
router, err := tcprouter.NewRouter()
|
||||
router, err := tcprouter.NewRouter(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
@@ -258,7 +258,7 @@ func TestKeepAliveMaxRequests(t *testing.T) {
|
||||
}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
router, err := tcprouter.NewRouter()
|
||||
router, err := tcprouter.NewRouter(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
@@ -306,7 +306,7 @@ func TestKeepAliveMaxTime(t *testing.T) {
|
||||
}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
router, err := tcprouter.NewRouter()
|
||||
router, err := tcprouter.NewRouter(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
@@ -350,7 +350,7 @@ func TestKeepAliveH2c(t *testing.T) {
|
||||
}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
router, err := tcprouter.NewRouter()
|
||||
router, err := tcprouter.NewRouter(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user