Merge branch v3.7 into master

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
romainandClaude Opus 5.5 committed 2026-09-23 15:08:35 +02:00
commit 34ea85d945
18 files changed
+991 -1330

No files matched your search

+1
View File
@@ -93,6 +93,7 @@ linters:
- github.com/jaguilar/vt100
- github.com/cucumber/godog
- github.com/vulcand/oxy/v2
- software.sslmate.com/src/go-pkcs12
govet:
enable-all: true
disable:
+12
View File
@@ -108,6 +108,18 @@ Dashboards, alerts and API queries relying on the previous names must be updated
The routers and middlewares generated on the HTTP entry points for an Ingress carrying the `nginx.ingress.kubernetes.io/ssl-passthrough` annotation are renamed as well.
They are now named after the Ingress rule and path they come from, `<namespace>-<ingress>-rule-<rule index>-path-<path index>` instead of `<namespace>-<ingress>-<host>-http`.
### Kubernetes Gateway API Provider: routes that match a request equally well
Until `v3.7.14`, two routes on the same listener could give routers with the very same rule.
This occurs, for example, with a route that has no host name and a route that has the host name of the listener.
One of the two routes served the requests, but you could not know which one.
The route could also change at each reload of the configuration.
Starting with `v3.7.14`, only the route that has precedence serves the requests.
The specification gives precedence to the older route, then to the first route in alphabetical order by `{namespace}/{name}`.
Traefik does not create the routers of the other routes.
Traefik writes a warning in the log, and gives the name of the router that serves the rule.
---
## v3.7.13
+8 -7
View File
@@ -23,7 +23,7 @@ require (
github.com/docker/go-connections v0.8.1
github.com/fatih/structs v1.1.0
github.com/fsnotify/fsnotify v1.10.1
github.com/go-acme/lego/v5 v5.5.1
github.com/go-acme/lego/v5 v5.5.2
github.com/go-kit/kit v0.13.0
github.com/go-kit/log v0.2.1
github.com/golang/protobuf v1.5.4
@@ -71,7 +71,7 @@ require (
github.com/testcontainers/testcontainers-go/modules/k3s v0.42.0
github.com/tetratelabs/wazero v1.8.0
github.com/tidwall/gjson v1.17.0
github.com/traefik/grpc-web v0.16.0
github.com/traefik/grpc-web v0.17.0
github.com/traefik/paerser v0.2.2
github.com/traefik/traefik/dynamic/ext v0.0.0-00010101000000-000000000000
github.com/traefik/yaegi v0.16.1
@@ -175,10 +175,10 @@ require (
github.com/bodgit/gssapi v0.0.4 // indirect
github.com/bodgit/tsig v1.3.1 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
github.com/bytedance/sonic v1.15.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/coder/websocket v1.8.15 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v1.0.0-rc.1 // indirect
@@ -199,7 +199,6 @@ require (
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect
github.com/go-acme/alidns-20150109/v5 v5.6.1 // indirect
github.com/go-acme/esa-20240910/v3 v3.13.0 // indirect
github.com/go-acme/jdcloud-sdk-go v1.64.0 // indirect
@@ -279,7 +278,7 @@ require (
github.com/labbsr0x/goh v1.0.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/linode/linodego v1.69.1 // indirect
github.com/liquidweb/liquidweb-cli v0.7.0 // indirect
github.com/liquidweb/liquidweb-cli v0.6.10 // indirect
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
github.com/magiconair/properties v1.8.10 // indirect
@@ -391,7 +390,6 @@ require (
go.uber.org/zap v1.28.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/arch v0.4.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/oauth2 v0.37.0 // indirect
golang.org/x/term v0.46.0 // indirect
@@ -408,7 +406,6 @@ require (
k8s.io/klog/v2 v2.140.0 // indirect
k8s.io/kube-openapi v0.0.0-20260501160325-927ab1f70cd6 // indirect
k8s.io/streaming v0.36.3 // indirect
nhooyr.io/websocket v1.8.7 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
software.sslmate.com/src/go-pkcs12 v0.7.3 // indirect
@@ -423,6 +420,10 @@ replace (
github.com/gorilla/mux => github.com/containous/mux v0.0.0-20250523120546-41b6ec3aed59
)
// software.sslmate.com serves the repository as static files, and the git shipped with macOS links an old libcurl that mishandles the resulting 404s over HTTP/2, so GOPROXY=direct fetches fail for macOS users.
// The SSLMate GitHub mirror carries the same commit and the same go.mod bytes.
replace software.sslmate.com/src/go-pkcs12 => github.com/SSLMate/go-pkcs12 v0.7.3
// ambiguous import: found package github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http in multiple modules
// tencentcloud uses monorepo with multimodule but the go.mod files are incomplete.
exclude github.com/tencentcloud/tencentcloud-sdk-go v3.0.83+incompatible
+10 -1102
View File
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,68 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners:
- name: http
protocol: HTTP
port: 80
hostname: foo.com
allowedRoutes:
kinds:
- kind: GRPCRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
# This route has no host name. The listener gives it the host name foo.com.
# Thus this route gives the same rule as the route below.
# This route is also the older of the two routes. The creation time makes the decision.
---
kind: GRPCRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: grpc-app-catch-all
namespace: default
creationTimestamp: "2024-01-01T00:00:01Z"
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- name: whoami
port: 80
weight: 1
---
kind: GRPCRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: grpc-app-hostname
namespace: default
creationTimestamp: "2024-01-01T00:00:02Z"
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- foo.com
rules:
- backendRefs:
- name: whoami
port: 80
weight: 1
@@ -0,0 +1,80 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners:
- name: http
protocol: HTTP
port: 80
hostname: foo.com
allowedRoutes:
kinds:
- kind: HTTPRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
# This route has no host name. The listener gives it the host name foo.com.
# Thus this route gives the same rule as the route below.
# This route is also the older of the two routes. The creation time makes the decision.
---
kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: http-app-catch-all
namespace: default
creationTimestamp: "2024-01-01T00:00:01Z"
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami
port: 80
weight: 1
kind: Service
group: ""
---
kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: http-app-hostname
namespace: default
creationTimestamp: "2024-01-01T00:00:02Z"
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- "foo.com"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami
port: 80
weight: 1
kind: Service
group: ""
@@ -0,0 +1,88 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-tcp-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners:
- name: tcp
protocol: TCP
port: 9000
allowedRoutes:
namespaces:
from: Same
kinds:
- kind: TCPRoute
group: gateway.networking.k8s.io
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-tls-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners:
- name: tls
protocol: TLS
port: 9000
tls:
mode: Passthrough
allowedRoutes:
namespaces:
from: Same
kinds:
- kind: TLSRoute
group: gateway.networking.k8s.io
# A TLSRoute without host names and a TCPRoute both give the rule HostSNI("*").
# The TLSRoute router is evaluated by the TLS muxer, and the TCPRoute router by the non-TLS muxer.
# Thus the two routers do not compete, and Traefik creates both.
---
kind: TLSRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: tls-app
namespace: default
spec:
parentRefs:
- name: my-tls-gateway
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- name: whoamitcp
port: 9000
weight: 1
kind: Service
group: ""
---
kind: TCPRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: tcp-app
namespace: default
spec:
parentRefs:
- name: my-tcp-gateway
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- name: whoamitcp
port: 9000
weight: 1
kind: Service
group: ""
@@ -0,0 +1,68 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-tcp-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners:
- name: tcp
protocol: TCP
port: 9000
allowedRoutes:
namespaces:
from: Same
kinds:
- kind: TCPRoute
group: gateway.networking.k8s.io
# A TCPRoute has no routing criteria. Thus the two routes below give the rule HostSNI("*").
# The route below is the older of the two routes. The creation time makes the decision.
---
kind: TCPRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: tcp-app-first
namespace: default
creationTimestamp: "2024-01-01T00:00:01Z"
spec:
parentRefs:
- name: my-tcp-gateway
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- name: whoamitcp
port: 9000
weight: 1
kind: Service
group: ""
---
kind: TCPRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: tcp-app-second
namespace: default
creationTimestamp: "2024-01-01T00:00:02Z"
spec:
parentRefs:
- name: my-tcp-gateway
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- name: whoamitcp
port: 9000
weight: 1
kind: Service
group: ""
@@ -0,0 +1,74 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-tls-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners:
- name: tls
protocol: TLS
port: 9000
hostname: foo.com
tls:
mode: Passthrough
allowedRoutes:
namespaces:
from: Same
kinds:
- kind: TLSRoute
group: gateway.networking.k8s.io
# This route has no host name. The listener gives it the host name foo.com.
# Thus this route gives the same rule as the route below.
# This route is also the older of the two routes. The creation time makes the decision.
---
kind: TLSRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: tls-app-catch-all
namespace: default
creationTimestamp: "2024-01-01T00:00:01Z"
spec:
parentRefs:
- name: my-tls-gateway
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- name: whoamitcp
port: 9000
weight: 1
kind: Service
group: ""
---
kind: TLSRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: tls-app-hostname
namespace: default
creationTimestamp: "2024-01-01T00:00:02Z"
spec:
parentRefs:
- name: my-tls-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- foo.com
rules:
- backendRefs:
- name: whoamitcp
port: 9000
weight: 1
kind: Service
group: ""
+36 -19
View File
@@ -20,7 +20,7 @@ import (
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
)
func (p *Provider) loadGRPCRoute(ctx context.Context, gateways []gatewayWithListeners, route *gatev1.GRPCRoute, conf *dynamic.Configuration, attachedRoutes attachedRoutes, statusReport *statusReport) {
func (p *Provider) loadGRPCRoute(ctx context.Context, gateways []gatewayWithListeners, route *gatev1.GRPCRoute, conf *dynamic.Configuration, attachedRoutes attachedRoutes, servedRules servedRules, statusReport *statusReport) {
logger := log.Ctx(ctx).With().
Str("grpc_route", route.Name).
Str("namespace", route.Namespace).
@@ -81,13 +81,21 @@ func (p *Provider) loadGRPCRoute(ctx context.Context, gateways []gatewayWithList
// The ResolvedRefs condition must be reported for every parentRef,
// even when the route does not attach to the listener.
routeConf, condition := p.loadGRPCRouteConfiguration(logger.WithContext(ctx), match.GatewayName, match.GatewayNamespace, listener, route, hostnames, statusReport)
routerConfs, condition := p.loadGRPCRouteConfiguration(logger.WithContext(ctx), match.GatewayName, match.GatewayNamespace, listener, route, hostnames, statusReport)
if resolvedRefCondition == nil || resolvedRefCondition.Status == metav1.ConditionTrue {
resolvedRefCondition = new(condition)
}
if accepted && listener.Attached {
mergeHTTPConfiguration(routeConf, conf)
for _, rc := range routerConfs {
router := rc.Conf.HTTP.Routers[rc.Name]
if servedBy, alreadyServed := servedRules.register(rc.Name, router.ParentRefs, router.Rule); alreadyServed {
logger.Warn().Msgf("Traefik does not create router %q, because router %q serves the rule %q under the same parent routers", rc.Name, servedBy, router.Rule)
continue
}
mergeHTTPConfiguration(rc.Conf, conf)
}
// Only consider the route attached if the listener is in an "attached" state.
acceptedCondition.Reason = string(gatev1.RouteReasonAccepted)
@@ -108,15 +116,14 @@ func (p *Provider) loadGRPCRoute(ctx context.Context, gateways []gatewayWithList
}
}
func (p *Provider) loadGRPCRouteConfiguration(ctx context.Context, gatewayName, gatewayNamespace string, listener gatewayListener, route *gatev1.GRPCRoute, hostnames []gatev1.Hostname, statusReport *statusReport) (*dynamic.Configuration, metav1.Condition) {
conf := &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: make(map[string]*dynamic.Router),
Middlewares: make(map[string]*dynamic.Middleware),
Services: make(map[string]*dynamic.Service),
ServersTransports: make(map[string]*dynamic.ServersTransport),
},
}
// grpcRouteRouter is a router of a GRPCRoute, with the configuration of its resources.
type grpcRouteRouter struct {
Name string
Conf *dynamic.Configuration
}
func (p *Provider) loadGRPCRouteConfiguration(ctx context.Context, gatewayName, gatewayNamespace string, listener gatewayListener, route *gatev1.GRPCRoute, hostnames []gatev1.Hostname, statusReport *statusReport) ([]grpcRouteRouter, metav1.Condition) {
var routers []grpcRouteRouter
condition := metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
@@ -134,7 +141,6 @@ func (p *Provider) loadGRPCRouteConfiguration(ctx context.Context, gatewayName,
for _, match := range matches {
rule, priority := buildGRPCMatchRule(hostnames, match)
router := dynamic.Router{
// "default" stands for the default rule syntax in Traefik v3, i.e. the v3 syntax.
RuleSyntax: "default",
@@ -143,15 +149,25 @@ func (p *Provider) loadGRPCRouteConfiguration(ctx context.Context, gatewayName,
ParentRefs: listener.RouterNames,
}
var err error
routerName := makeRouterName(strings.ToLower(kindGRPCRoute), rule, route.Namespace, route.Name, gatewayNamespace, gatewayName, listener.EPName, ri)
router.Middlewares, err = p.loadGRPCMiddlewares(conf, route.Namespace, routerName, routeRule.Filters)
routerConf := &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: make(map[string]*dynamic.Router),
Middlewares: make(map[string]*dynamic.Middleware),
Services: make(map[string]*dynamic.Service),
ServersTransports: make(map[string]*dynamic.ServersTransport),
},
}
var err error
router.Middlewares, err = p.loadGRPCMiddlewares(routerConf, route.Namespace, routerName, routeRule.Filters)
switch {
case err != nil:
log.Ctx(ctx).Error().Err(err).Msg("Unable to load GRPC route filters")
errWrrName := routerName + "-err-wrr"
conf.HTTP.Services[errWrrName] = &dynamic.Service{
routerConf.HTTP.Services[errWrrName] = &dynamic.Service{
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
@@ -169,17 +185,18 @@ func (p *Provider) loadGRPCRouteConfiguration(ctx context.Context, gatewayName,
default:
var serviceCondition *metav1.Condition
router.Service, serviceCondition = p.loadGRPCService(gatewayName, listener, conf, routerName, routeRule, route, statusReport)
router.Service, serviceCondition = p.loadGRPCService(gatewayName, listener, routerConf, routerName, routeRule, route, statusReport)
if serviceCondition != nil {
condition = *serviceCondition
}
}
conf.HTTP.Routers[routerName] = &router
routerConf.HTTP.Routers[routerName] = &router
routers = append(routers, grpcRouteRouter{Name: routerName, Conf: routerConf})
}
}
return conf, condition
return routers, condition
}
func (p *Provider) loadGRPCService(gatewayName string, listener gatewayListener, conf *dynamic.Configuration, routerName string, routeRule gatev1.GRPCRouteRule, route *gatev1.GRPCRoute, statusReport *statusReport) (string, *metav1.Condition) {
+36 -18
View File
@@ -24,7 +24,7 @@ import (
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
)
func (p *Provider) loadHTTPRoute(ctx context.Context, gateways []gatewayWithListeners, route *gatev1.HTTPRoute, conf *dynamic.Configuration, attachedRoutes attachedRoutes, statusReport *statusReport) {
func (p *Provider) loadHTTPRoute(ctx context.Context, gateways []gatewayWithListeners, route *gatev1.HTTPRoute, conf *dynamic.Configuration, attachedRoutes attachedRoutes, servedRules servedRules, statusReport *statusReport) {
logger := log.Ctx(ctx).With().
Str("http_route", route.Name).
Str("namespace", route.Namespace).
@@ -85,13 +85,21 @@ func (p *Provider) loadHTTPRoute(ctx context.Context, gateways []gatewayWithList
// The ResolvedRefs condition must be reported for every parentRef,
// even when the route does not attach to the listener.
routeConf, condition := p.loadHTTPRouteConfiguration(logger.WithContext(ctx), match.GatewayName, match.GatewayNamespace, listener, route, hostnames, statusReport)
routerConfs, condition := p.loadHTTPRouteConfiguration(logger.WithContext(ctx), match.GatewayName, match.GatewayNamespace, listener, route, hostnames, statusReport)
if resolvedRefCondition == nil || resolvedRefCondition.Status == metav1.ConditionTrue {
resolvedRefCondition = new(condition)
}
if accepted && listener.Attached {
mergeHTTPConfiguration(routeConf, conf)
for _, rc := range routerConfs {
router := rc.Conf.HTTP.Routers[rc.Name]
if servedBy, alreadyServed := servedRules.register(rc.Name, router.ParentRefs, router.Rule); alreadyServed {
logger.Warn().Msgf("Traefik does not create router %q, because router %q serves the rule %q under the same parent routers", rc.Name, servedBy, router.Rule)
continue
}
mergeHTTPConfiguration(rc.Conf, conf)
}
// Only consider the route attached if the listener is in an "attached" state.
acceptedCondition.Reason = string(gatev1.RouteReasonAccepted)
@@ -112,15 +120,14 @@ func (p *Provider) loadHTTPRoute(ctx context.Context, gateways []gatewayWithList
}
}
func (p *Provider) loadHTTPRouteConfiguration(ctx context.Context, gatewayName, gatewayNamespace string, listener gatewayListener, route *gatev1.HTTPRoute, hostnames []gatev1.Hostname, statusReport *statusReport) (*dynamic.Configuration, metav1.Condition) {
conf := &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: make(map[string]*dynamic.Router),
Middlewares: make(map[string]*dynamic.Middleware),
Services: make(map[string]*dynamic.Service),
ServersTransports: make(map[string]*dynamic.ServersTransport),
},
}
// httpRouteRouter is a router of an HTTPRoute, with the configuration of its resources.
type httpRouteRouter struct {
Name string
Conf *dynamic.Configuration
}
func (p *Provider) loadHTTPRouteConfiguration(ctx context.Context, gatewayName, gatewayNamespace string, listener gatewayListener, route *gatev1.HTTPRoute, hostnames []gatev1.Hostname, statusReport *statusReport) ([]httpRouteRouter, metav1.Condition) {
var routers []httpRouteRouter
condition := metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
@@ -141,16 +148,26 @@ func (p *Provider) loadHTTPRouteConfiguration(ctx context.Context, gatewayName,
ParentRefs: listener.RouterNames,
}
var err error
routerName := makeRouterName(strings.ToLower(kindHTTPRoute), rule, route.Namespace, route.Name, gatewayNamespace, gatewayName, listener.EPName, ri)
routerConf := &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: make(map[string]*dynamic.Router),
Middlewares: make(map[string]*dynamic.Middleware),
Services: make(map[string]*dynamic.Service),
ServersTransports: make(map[string]*dynamic.ServersTransport),
},
}
var err error
// TODO loadMiddlewares errors could change the condition.
router.Middlewares, err = p.loadMiddlewares(conf, route.Namespace, routerName, routeRule.Filters, match.Path)
router.Middlewares, err = p.loadMiddlewares(routerConf, route.Namespace, routerName, routeRule.Filters, match.Path)
switch {
case err != nil:
log.Ctx(ctx).Error().Err(err).Msg("Unable to load HTTPRoute filters")
errWrrName := routerName + "-err-wrr"
conf.HTTP.Services[errWrrName] = &dynamic.Service{
routerConf.HTTP.Services[errWrrName] = &dynamic.Service{
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
@@ -179,7 +196,7 @@ func (p *Provider) loadHTTPRouteConfiguration(ctx context.Context, gatewayName,
default:
var serviceCondition *metav1.Condition
router.Service, serviceCondition = p.loadWRRService(ctx, gatewayName, listener, conf, routerName, routeRule, route, match.Path, statusReport)
router.Service, serviceCondition = p.loadWRRService(ctx, gatewayName, listener, routerConf, routerName, routeRule, route, match.Path, statusReport)
if serviceCondition != nil {
condition = *serviceCondition
}
@@ -187,11 +204,12 @@ func (p *Provider) loadHTTPRouteConfiguration(ctx context.Context, gatewayName,
p.applyRouterTransform(ctx, &router, route)
conf.HTTP.Routers[routerName] = &router
routerConf.HTTP.Routers[routerName] = &router
routers = append(routers, httpRouteRouter{Name: routerName, Conf: routerConf})
}
}
return conf, condition
return routers, condition
}
func (p *Provider) loadWRRService(ctx context.Context, gatewayName string, listener gatewayListener, conf *dynamic.Configuration, routerName string, routeRule gatev1.HTTPRouteRule, route *gatev1.HTTPRoute, pathMatch *gatev1.HTTPPathMatch, statusReport *statusReport) (string, *metav1.Condition) {
+39 -7
View File
@@ -506,19 +506,16 @@ func (p *Provider) loadHTTPAndGRPCRoutes(ctx context.Context, gateways []gateway
routes = append(routes, route)
}
slices.SortStableFunc(routes, func(a, b metav1.Object) int {
return cmp.Or(a.GetCreationTimestamp().Time.Compare(b.GetCreationTimestamp().Time),
strings.Compare(a.GetNamespace(), b.GetNamespace()),
strings.Compare(a.GetName(), b.GetName()))
})
slices.SortStableFunc(routes, compareRoutes)
attached := make(attachedRoutes)
served := make(servedRules)
for _, route := range routes {
switch route := route.(type) {
case *gatev1.HTTPRoute:
p.loadHTTPRoute(ctx, gateways, route, conf, attached, statusReport)
p.loadHTTPRoute(ctx, gateways, route, conf, attached, served, statusReport)
case *gatev1.GRPCRoute:
p.loadGRPCRoute(ctx, gateways, route, conf, attached, statusReport)
p.loadGRPCRoute(ctx, gateways, route, conf, attached, served, statusReport)
}
}
}
@@ -1379,6 +1376,41 @@ func allowRoute(listener gatewayListener, routeNamespace, routeKind string) bool
})
}
// compareRoutes orders the routes as the specification asks for the ones matching
// a request equally well to be discriminated.
func compareRoutes(a, b metav1.Object) int {
return cmp.Or(a.GetCreationTimestamp().Time.Compare(b.GetCreationTimestamp().Time),
strings.Compare(a.GetNamespace(), b.GetNamespace()),
strings.Compare(a.GetName(), b.GetName()))
}
// servedRuleKey identifies a rule within the scope its router competes in.
// A TCP router competes in the muxer of its entry points,
// and an HTTP router in the muxer of its listener parent routers, as it has no entry points of its own.
type servedRuleKey struct {
Scope string
Rule string
}
// servedRules keeps the router that serves each rule.
// Two routers that compete and have the same rule match the same requests.
// Thus only the first router that a muxer evaluates can serve a request.
type servedRules map[servedRuleKey]string
// register keeps the given router as the router that serves its rule within the given scope.
// If a different router serves that rule already, register gives its name, and true.
func (sr servedRules) register(name string, scope []string, rule string) (string, bool) {
key := servedRuleKey{Scope: strings.Join(scope, ","), Rule: rule}
if servedBy, served := sr[key]; served {
return servedBy, true
}
sr[key] = name
return "", false
}
// listenerRef identifies a listener of a Gateway.
type listenerRef struct {
Name string
+355 -128
View File
@@ -3705,6 +3705,74 @@ func TestLoadHTTPRoutes(t *testing.T) {
},
},
},
{
desc: "HTTPRoutes with identical rules",
paths: []string{"services.yml", "httproute/with_identical_rules.yml"},
entryPoints: map[string]Entrypoint{"web": {
Address: ":80",
}},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"listener-web-http-foo-com-5f20d578e2e3eccac466": {
EntryPoints: []string{"web"},
Rule: `Host("foo.com")`,
},
"httproute-default-http-app-catch-all-gw-default-my-gateway-ep-web-0-402d8b0319543c8e637b": {
Service: "httproute-default-http-app-catch-all-gw-default-my-gateway-ep-web-0-402d8b0319543c8e637b-wrr",
Rule: `Host("foo.com") && PathPrefix("/")`,
Priority: 9,
RuleSyntax: "default",
ParentRefs: []string{"listener-web-http-foo-com-5f20d578e2e3eccac466"},
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"httproute-default-http-app-catch-all-gw-default-my-gateway-ep-web-0-402d8b0319543c8e637b-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "httproute-default-http-app-catch-all-gw-default-my-gateway-ep-web-0-402d8b0319543c8e637b-svc-default-whoami-0",
Weight: new(1),
},
},
},
},
"httproute-default-http-app-catch-all-gw-default-my-gateway-ep-web-0-402d8b0319543c8e637b-svc-default-whoami-0": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: dynamic.BalancerStrategyWRR,
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: new(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{
Options: map[string]tls.Options{},
},
},
},
}
for _, test := range testCases {
@@ -4963,6 +5031,74 @@ func TestLoadGRPCRoutes(t *testing.T) {
},
},
},
{
desc: "GRPCRoutes with identical rules",
paths: []string{"services.yml", "grpcroute/with_identical_rules.yml"},
entryPoints: map[string]Entrypoint{"web": {
Address: ":80",
}},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"listener-web-http-foo-com-5f20d578e2e3eccac466": {
EntryPoints: []string{"web"},
Rule: `Host("foo.com")`,
},
"grpcroute-default-grpc-app-catch-all-gw-default-my-gateway-ep-web-0-24ad420c9f1560b62d23": {
Service: "grpcroute-default-grpc-app-catch-all-gw-default-my-gateway-ep-web-0-24ad420c9f1560b62d23-wrr",
Rule: `Host("foo.com") && PathPrefix("/")`,
Priority: 22,
RuleSyntax: "default",
ParentRefs: []string{"listener-web-http-foo-com-5f20d578e2e3eccac466"},
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"grpcroute-default-grpc-app-catch-all-gw-default-my-gateway-ep-web-0-24ad420c9f1560b62d23-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "grpcroute-default-grpc-app-catch-all-gw-default-my-gateway-ep-web-0-24ad420c9f1560b62d23-svc-default-whoami-0",
Weight: new(1),
},
},
},
},
"grpcroute-default-grpc-app-catch-all-gw-default-my-gateway-ep-web-0-24ad420c9f1560b62d23-svc-default-whoami-0": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: dynamic.BalancerStrategyWRR,
Servers: []dynamic.Server{
{
URL: "h2c://10.10.0.1:80",
},
{
URL: "h2c://10.10.0.2:80",
},
},
PassHostHeader: new(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{
Options: map[string]tls.Options{},
},
},
},
}
for _, test := range testCases {
@@ -5707,12 +5843,6 @@ func TestLoadTCPRoutes(t *testing.T) {
Rule: `HostSNI("*")`,
RuleSyntax: "default",
},
"tcproute-default-tcp-app-gw-default-my-tcp-gateway-ep-tcp-1-1-2303f3a24dbdd3670244": {
EntryPoints: []string{"tcp-1"},
Service: "tcproute-default-tcp-app-gw-default-my-tcp-gateway-ep-tcp-1-1-2303f3a24dbdd3670244-wrr",
Rule: `HostSNI("*")`,
RuleSyntax: "default",
},
},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{
@@ -5726,16 +5856,6 @@ func TestLoadTCPRoutes(t *testing.T) {
},
},
},
"tcproute-default-tcp-app-gw-default-my-tcp-gateway-ep-tcp-1-1-2303f3a24dbdd3670244-wrr": {
Weighted: &dynamic.TCPWeightedRoundRobin{
Services: []dynamic.TCPWRRService{
{
Name: "tcproute-default-tcp-app-gw-default-my-tcp-gateway-ep-tcp-1-1-2303f3a24dbdd3670244-svc-default-whoamitcp-0",
Weight: new(1),
},
},
},
},
"tcproute-default-tcp-app-gw-default-my-tcp-gateway-ep-tcp-1-0-1767e543195002a45b0b-svc-default-whoamitcp-0": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
@@ -5748,18 +5868,6 @@ func TestLoadTCPRoutes(t *testing.T) {
},
},
},
"tcproute-default-tcp-app-gw-default-my-tcp-gateway-ep-tcp-1-1-2303f3a24dbdd3670244-svc-default-whoamitcp-0": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{
Address: "10.10.0.9:10000",
},
{
Address: "10.10.0.10:10000",
},
},
},
},
},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
@@ -6364,6 +6472,64 @@ func TestLoadTCPRoutes(t *testing.T) {
},
},
},
{
desc: "TCPRoutes with identical rules",
paths: []string{"services.yml", "tcproute/with_identical_rules.yml"},
entryPoints: map[string]Entrypoint{
"tcp": {Address: ":9000"},
},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{
"tcproute-default-tcp-app-first-gw-default-my-tcp-gateway-ep-tcp-0-79a3b41b9e16bdaa2841": {
EntryPoints: []string{"tcp"},
Service: "tcproute-default-tcp-app-first-gw-default-my-tcp-gateway-ep-tcp-0-79a3b41b9e16bdaa2841-wrr",
Rule: `HostSNI("*")`,
RuleSyntax: "default",
},
},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{
"tcproute-default-tcp-app-first-gw-default-my-tcp-gateway-ep-tcp-0-79a3b41b9e16bdaa2841-wrr": {
Weighted: &dynamic.TCPWeightedRoundRobin{
Services: []dynamic.TCPWRRService{
{
Name: "tcproute-default-tcp-app-first-gw-default-my-tcp-gateway-ep-tcp-0-79a3b41b9e16bdaa2841-svc-default-whoamitcp-0",
Weight: new(1),
},
},
},
},
"tcproute-default-tcp-app-first-gw-default-my-tcp-gateway-ep-tcp-0-79a3b41b9e16bdaa2841-svc-default-whoamitcp-0": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{
Address: "10.10.0.9:9000",
},
{
Address: "10.10.0.10:9000",
},
},
},
},
},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{
Options: map[string]tls.Options{},
},
},
},
}
for _, test := range testCases {
@@ -7489,16 +7655,6 @@ func TestLoadTLSRoutes(t *testing.T) {
Passthrough: true,
},
},
"tlsroute-default-tls-app-gw-default-my-gateway-ep-tcp-1-1-c675f072c5cdb7649f23": {
EntryPoints: []string{"tcp-1"},
Service: "tlsroute-default-tls-app-gw-default-my-gateway-ep-tcp-1-1-c675f072c5cdb7649f23-wrr",
Priority: 0,
Rule: `HostSNI("*")`,
RuleSyntax: "default",
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: true,
},
},
},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{
@@ -7515,16 +7671,6 @@ func TestLoadTLSRoutes(t *testing.T) {
},
},
},
"tlsroute-default-tls-app-gw-default-my-gateway-ep-tcp-1-1-c675f072c5cdb7649f23-wrr": {
Weighted: &dynamic.TCPWeightedRoundRobin{
Services: []dynamic.TCPWRRService{
{
Name: "tlsroute-default-tls-app-gw-default-my-gateway-ep-tcp-1-1-c675f072c5cdb7649f23-svc-default-whoamitcp-0",
Weight: new(1),
},
},
},
},
"tlsroute-default-tls-app-gw-default-my-gateway-ep-tcp-1-0-50b1500bd2ad9fd2008a-svc-default-whoamitcp-0": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
@@ -7537,18 +7683,6 @@ func TestLoadTLSRoutes(t *testing.T) {
},
},
},
"tlsroute-default-tls-app-gw-default-my-gateway-ep-tcp-1-1-c675f072c5cdb7649f23-svc-default-whoamitcp-0": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{
Address: "10.10.0.9:10000",
},
{
Address: "10.10.0.10:10000",
},
},
},
},
},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
@@ -8087,6 +8221,77 @@ func TestLoadTLSRoutes(t *testing.T) {
},
},
},
{
desc: "TLSRoutes with identical rules",
paths: []string{"services.yml", "tlsroute/with_identical_rules.yml"},
entryPoints: map[string]Entrypoint{
"tcp": {Address: ":9000"},
},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{
"tlsroute-default-tls-app-catch-all-gw-default-my-tls-gateway-ep-tcp-0-86149371d80f428d4c2d": {
EntryPoints: []string{"tcp"},
Service: "tlsroute-default-tls-app-catch-all-gw-default-my-tls-gateway-ep-tcp-0-86149371d80f428d4c2d-wrr",
Rule: `HostSNI("foo.com")`,
Priority: 7,
RuleSyntax: "default",
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: true,
},
},
"deny-unknown-host": {
Rule: "HostSNI(`*`) && !ALPN(`h2`) && !ALPN(`http/1.1`)",
Priority: 1,
Service: "deny-unknown-host",
TLS: &dynamic.RouterTCPTLSConfig{},
},
},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{
"tlsroute-default-tls-app-catch-all-gw-default-my-tls-gateway-ep-tcp-0-86149371d80f428d4c2d-wrr": {
Weighted: &dynamic.TCPWeightedRoundRobin{
Services: []dynamic.TCPWRRService{
{
Name: "tlsroute-default-tls-app-catch-all-gw-default-my-tls-gateway-ep-tcp-0-86149371d80f428d4c2d-svc-default-whoamitcp-0",
Weight: new(1),
},
},
},
},
"tlsroute-default-tls-app-catch-all-gw-default-my-tls-gateway-ep-tcp-0-86149371d80f428d4c2d-svc-default-whoamitcp-0": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{
Address: "10.10.0.9:9000",
},
{
Address: "10.10.0.10:9000",
},
},
},
},
"deny-unknown-host": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{},
},
},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{
Options: map[string]tls.Options{},
},
},
},
}
for _, test := range testCases {
@@ -8765,20 +8970,6 @@ func TestLoadMixedRoutes(t *testing.T) {
Options: "listener-websecure-https-439feda8e3f1dbdca529",
},
},
"httproute-default-http-app-default-gw-default-my-gateway-ep-web-0-3b55179f1d48675f1432": {
Service: "httproute-default-http-app-default-gw-default-my-gateway-ep-web-0-3b55179f1d48675f1432-wrr",
Rule: `PathPrefix("/")`,
Priority: 2,
RuleSyntax: "default",
ParentRefs: []string{"listener-web-http-5b3226d4ebd42b7200f1"},
},
"httproute-default-http-app-default-gw-default-my-gateway-ep-websecure-0-4a26538fbaaf60dc52c9": {
Service: "httproute-default-http-app-default-gw-default-my-gateway-ep-websecure-0-4a26538fbaaf60dc52c9-wrr",
Rule: `PathPrefix("/")`,
Priority: 2,
RuleSyntax: "default",
ParentRefs: []string{"listener-websecure-https-439feda8e3f1dbdca529"},
},
"httproute-bar-http-app-bar-gw-default-my-gateway-ep-web-0-cf99cd1374e6056a6755": {
Service: "httproute-bar-http-app-bar-gw-default-my-gateway-ep-web-0-cf99cd1374e6056a6755-wrr",
Rule: `PathPrefix("/")`,
@@ -8796,60 +8987,6 @@ func TestLoadMixedRoutes(t *testing.T) {
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"httproute-default-http-app-default-gw-default-my-gateway-ep-web-0-3b55179f1d48675f1432-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "httproute-default-http-app-default-gw-default-my-gateway-ep-web-0-3b55179f1d48675f1432-svc-default-whoami-0",
Weight: new(1),
},
},
},
},
"httproute-default-http-app-default-gw-default-my-gateway-ep-websecure-0-4a26538fbaaf60dc52c9-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "httproute-default-http-app-default-gw-default-my-gateway-ep-websecure-0-4a26538fbaaf60dc52c9-svc-default-whoami-0",
Weight: new(1),
},
},
},
},
"httproute-default-http-app-default-gw-default-my-gateway-ep-web-0-3b55179f1d48675f1432-svc-default-whoami-0": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: dynamic.BalancerStrategyWRR,
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: new(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
"httproute-default-http-app-default-gw-default-my-gateway-ep-websecure-0-4a26538fbaaf60dc52c9-svc-default-whoami-0": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: dynamic.BalancerStrategyWRR,
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: new(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
"httproute-bar-http-app-bar-gw-default-my-gateway-ep-web-0-cf99cd1374e6056a6755-svc-bar-whoami-bar-0": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: dynamic.BalancerStrategyWRR,
@@ -9396,6 +9533,96 @@ func TestLoadMixedRoutes(t *testing.T) {
},
},
},
{
desc: "TLSRoute and TCPRoute with the same rule on the same entry point",
paths: []string{"services.yml", "mixed/with_tlsroute_and_tcproute_on_same_entrypoint.yml"},
entryPoints: map[string]Entrypoint{
"tcp": {Address: ":9000"},
},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{
"deny-unknown-host": {
Rule: "HostSNI(`*`) && !ALPN(`h2`) && !ALPN(`http/1.1`)",
Priority: 1,
Service: "deny-unknown-host",
TLS: &dynamic.RouterTCPTLSConfig{},
},
"tlsroute-default-tls-app-gw-default-my-tls-gateway-ep-tcp-0-b5a69536e2d72608adeb": {
EntryPoints: []string{"tcp"},
Service: "tlsroute-default-tls-app-gw-default-my-tls-gateway-ep-tcp-0-b5a69536e2d72608adeb-wrr",
Rule: `HostSNI("*")`,
RuleSyntax: "default",
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: true,
},
},
"tcproute-default-tcp-app-gw-default-my-tcp-gateway-ep-tcp-0-ada211f39fad8759e161": {
EntryPoints: []string{"tcp"},
Service: "tcproute-default-tcp-app-gw-default-my-tcp-gateway-ep-tcp-0-ada211f39fad8759e161-wrr",
Rule: `HostSNI("*")`,
RuleSyntax: "default",
},
},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{
"deny-unknown-host": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{},
},
"tlsroute-default-tls-app-gw-default-my-tls-gateway-ep-tcp-0-b5a69536e2d72608adeb-wrr": {
Weighted: &dynamic.TCPWeightedRoundRobin{
Services: []dynamic.TCPWRRService{
{
Name: "tlsroute-default-tls-app-gw-default-my-tls-gateway-ep-tcp-0-b5a69536e2d72608adeb-svc-default-whoamitcp-0",
Weight: new(1),
},
},
},
},
"tlsroute-default-tls-app-gw-default-my-tls-gateway-ep-tcp-0-b5a69536e2d72608adeb-svc-default-whoamitcp-0": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{Address: "10.10.0.9:9000"},
{Address: "10.10.0.10:9000"},
},
},
},
"tcproute-default-tcp-app-gw-default-my-tcp-gateway-ep-tcp-0-ada211f39fad8759e161-wrr": {
Weighted: &dynamic.TCPWeightedRoundRobin{
Services: []dynamic.TCPWRRService{
{
Name: "tcproute-default-tcp-app-gw-default-my-tcp-gateway-ep-tcp-0-ada211f39fad8759e161-svc-default-whoamitcp-0",
Weight: new(1),
},
},
},
},
"tcproute-default-tcp-app-gw-default-my-tcp-gateway-ep-tcp-0-ada211f39fad8759e161-svc-default-whoamitcp-0": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{Address: "10.10.0.9:9000"},
{Address: "10.10.0.10:9000"},
},
},
},
},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{
Options: map[string]tls.Options{},
},
},
},
}
for _, test := range testCases {
+40 -25
View File
@@ -1,7 +1,6 @@
package gateway
import (
"cmp"
"context"
"fmt"
"maps"
@@ -31,18 +30,16 @@ func (p *Provider) loadTCPRoutes(ctx context.Context, gateways []gatewayWithList
// TCPRoutes attaching to the same listener all match every connection,
// and the specification gives precedence to the oldest one.
// https://gateway-api.sigs.k8s.io/guides/api-design/#conflicts
slices.SortStableFunc(routes, func(a, b *gatev1.TCPRoute) int {
if cmpTime := a.CreationTimestamp.Time.Compare(b.CreationTimestamp.Time); cmpTime != 0 {
return cmpTime
}
slices.SortStableFunc(routes, func(a, b *gatev1.TCPRoute) int { return compareRoutes(a, b) })
return cmp.Or(
cmp.Compare(a.Namespace, b.Namespace),
cmp.Compare(a.Name, b.Name),
)
})
served := make(servedRules)
for _, route := range routes {
logger := log.Ctx(ctx).With().
Str("tcp_route", route.Name).
Str("namespace", route.Namespace).
Logger()
routeParentRefs := matchingGatewayListenersForParentRef(gateways, route.Namespace, route.Spec.ParentRefs)
if len(routeParentRefs) == 0 {
continue
@@ -75,7 +72,7 @@ func (p *Provider) loadTCPRoutes(ctx context.Context, gateways []gatewayWithList
// The ResolvedRefs condition must be reported for every parentRef,
// even when the route does not attach to the listener.
routeConf, condition := p.loadTCPRoute(match.GatewayName, match.GatewayNamespace, listener, route)
routerConfs, condition := p.loadTCPRoute(match.GatewayName, match.GatewayNamespace, listener, route)
if resolvedRefCondition == nil || resolvedRefCondition.Status == metav1.ConditionTrue {
resolvedRefCondition = new(condition)
}
@@ -86,7 +83,15 @@ func (p *Provider) loadTCPRoutes(ctx context.Context, gateways []gatewayWithList
// A TCP listener supports no route kind other than TCPRoute, so the
// first route to attach to it is the one to bind.
if listener.Status.AttachedRoutes == 1 {
mergeTCPConfiguration(routeConf, conf)
for _, rc := range routerConfs {
router := rc.Conf.TCP.Routers[rc.Name]
if servedBy, alreadyServed := served.register(rc.Name, router.EntryPoints, router.Rule); alreadyServed {
logger.Warn().Msgf("Traefik does not create router %q, because router %q serves the rule %q on the same entry points", rc.Name, servedBy, router.Rule)
continue
}
mergeTCPConfiguration(rc.Conf, conf)
}
}
// Only consider the route attached if the listener is in an "attached" state.
@@ -109,15 +114,14 @@ func (p *Provider) loadTCPRoutes(ctx context.Context, gateways []gatewayWithList
}
}
func (p *Provider) loadTCPRoute(gatewayName, gatewayNamespace string, listener gatewayListener, route *gatev1.TCPRoute) (*dynamic.Configuration, metav1.Condition) {
conf := &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: make(map[string]*dynamic.TCPRouter),
Middlewares: make(map[string]*dynamic.TCPMiddleware),
Services: make(map[string]*dynamic.TCPService),
ServersTransports: make(map[string]*dynamic.TCPServersTransport),
},
}
// tcpRouteRouter is a router of a TCPRoute, with the configuration of its resources.
type tcpRouteRouter struct {
Name string
Conf *dynamic.Configuration
}
func (p *Provider) loadTCPRoute(gatewayName, gatewayNamespace string, listener gatewayListener, route *gatev1.TCPRoute) ([]tcpRouteRouter, metav1.Condition) {
var routers []tcpRouteRouter
condition := metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
@@ -149,6 +153,15 @@ func (p *Provider) loadTCPRoute(gatewayName, gatewayNamespace string, listener g
// Routing criteria should be introduced at some point.
routerName := makeRouterName(strings.ToLower(kindTCPRoute), "", route.Namespace, route.Name, gatewayNamespace, gatewayName, listener.EPName, ri)
routerConf := &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: make(map[string]*dynamic.TCPRouter),
Middlewares: make(map[string]*dynamic.TCPMiddleware),
Services: make(map[string]*dynamic.TCPService),
ServersTransports: make(map[string]*dynamic.TCPServersTransport),
},
}
if len(rule.BackendRefs) == 1 && isInternalService(rule.BackendRefs[0]) {
if !isCrossProviderNamespaceAllowed(p.CrossProviderNamespaces, route.Namespace) {
condition = metav1.Condition{
@@ -164,20 +177,22 @@ func (p *Provider) loadTCPRoute(gatewayName, gatewayNamespace string, listener g
}
router.Service = string(rule.BackendRefs[0].Name)
conf.TCP.Routers[routerName] = &router
routerConf.TCP.Routers[routerName] = &router
routers = append(routers, tcpRouteRouter{Name: routerName, Conf: routerConf})
continue
}
var serviceCondition *metav1.Condition
router.Service, serviceCondition = p.loadTCPWRRService(conf, routerName, rule.BackendRefs, route)
router.Service, serviceCondition = p.loadTCPWRRService(routerConf, routerName, rule.BackendRefs, route)
if serviceCondition != nil {
condition = *serviceCondition
}
conf.TCP.Routers[routerName] = &router
routerConf.TCP.Routers[routerName] = &router
routers = append(routers, tcpRouteRouter{Name: routerName, Conf: routerConf})
}
return conf, condition
return routers, condition
}
// loadTCPWRRService is generating a WRR service, even when there is only one target.
+43 -15
View File
@@ -29,7 +29,17 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, gateways []gatewayWithList
return
}
// The provider loads the routes in the order that the specification gives,
// to make a decision between the routes that match a connection equally well.
slices.SortStableFunc(routes, func(a, b *gatev1.TLSRoute) int { return compareRoutes(a, b) })
served := make(servedRules)
for _, route := range routes {
logger := log.Ctx(ctx).With().
Str("tls_route", route.Name).
Str("namespace", route.Namespace).Logger()
routeParentRefs := matchingGatewayListenersForParentRef(gateways, route.Namespace, route.Spec.ParentRefs)
if len(routeParentRefs) == 0 {
continue
@@ -70,13 +80,21 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, gateways []gatewayWithList
// The ResolvedRefs condition must be reported for every parentRef,
// even when the route does not attach to the listener.
routeConf, condition := p.loadTLSRoute(match.GatewayName, match.GatewayNamespace, listener, route, hostnames, statusReport)
routerConfs, condition := p.loadTLSRoute(match.GatewayName, match.GatewayNamespace, listener, route, hostnames, statusReport)
if resolvedRefCondition == nil || resolvedRefCondition.Status == metav1.ConditionTrue {
resolvedRefCondition = new(condition)
}
if accepted && listener.Attached {
mergeTCPConfiguration(routeConf, conf)
for _, rc := range routerConfs {
router := rc.Conf.TCP.Routers[rc.Name]
if servedBy, alreadyServed := served.register(rc.Name, router.EntryPoints, router.Rule); alreadyServed {
logger.Warn().Msgf("Traefik does not create router %q, because router %q serves the rule %q on the same entry points", rc.Name, servedBy, router.Rule)
continue
}
mergeTCPConfiguration(rc.Conf, conf)
}
// Only consider the route attached if the listener is in an "attached" state.
acceptedCondition.Reason = string(gatev1.RouteReasonAccepted)
@@ -112,15 +130,14 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, gateways []gatewayWithList
}
}
func (p *Provider) loadTLSRoute(gatewayName, gatewayNamespace string, listener gatewayListener, route *gatev1.TLSRoute, hostnames []gatev1.Hostname, statusReport *statusReport) (*dynamic.Configuration, metav1.Condition) {
conf := &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: make(map[string]*dynamic.TCPRouter),
Middlewares: make(map[string]*dynamic.TCPMiddleware),
Services: make(map[string]*dynamic.TCPService),
ServersTransports: make(map[string]*dynamic.TCPServersTransport),
},
}
// tlsRouteRouter is a router of a TLSRoute, with the configuration of its resources.
type tlsRouteRouter struct {
Name string
Conf *dynamic.Configuration
}
func (p *Provider) loadTLSRoute(gatewayName, gatewayNamespace string, listener gatewayListener, route *gatev1.TLSRoute, hostnames []gatev1.Hostname, statusReport *statusReport) ([]tlsRouteRouter, metav1.Condition) {
var routers []tlsRouteRouter
condition := metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
@@ -151,6 +168,15 @@ func (p *Provider) loadTLSRoute(gatewayName, gatewayNamespace string, listener g
// Routing criteria should be introduced at some point.
routerName := makeRouterName(strings.ToLower(kindTLSRoute), "", route.Namespace, route.Name, gatewayNamespace, gatewayName, listener.EPName, ri)
routerConf := &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: make(map[string]*dynamic.TCPRouter),
Middlewares: make(map[string]*dynamic.TCPMiddleware),
Services: make(map[string]*dynamic.TCPService),
ServersTransports: make(map[string]*dynamic.TCPServersTransport),
},
}
if len(routeRule.BackendRefs) == 1 && isInternalService(routeRule.BackendRefs[0]) {
if !isCrossProviderNamespaceAllowed(p.CrossProviderNamespaces, route.Namespace) {
condition = metav1.Condition{
@@ -166,20 +192,22 @@ func (p *Provider) loadTLSRoute(gatewayName, gatewayNamespace string, listener g
}
router.Service = string(routeRule.BackendRefs[0].Name)
conf.TCP.Routers[routerName] = &router
routerConf.TCP.Routers[routerName] = &router
routers = append(routers, tlsRouteRouter{Name: routerName, Conf: routerConf})
continue
}
var serviceCondition *metav1.Condition
router.Service, serviceCondition = p.loadTLSWRRService(gatewayName, listener, conf, routerName, routeRule.BackendRefs, route, statusReport)
router.Service, serviceCondition = p.loadTLSWRRService(gatewayName, listener, routerConf, routerName, routeRule.BackendRefs, route, statusReport)
if serviceCondition != nil {
condition = *serviceCondition
}
conf.TCP.Routers[routerName] = &router
routerConf.TCP.Routers[routerName] = &router
routers = append(routers, tlsRouteRouter{Name: routerName, Conf: routerConf})
}
return conf, condition
return routers, condition
}
// loadTLSWRRService is generating a WRR service, even when there is only one target.
+4 -4
View File
@@ -875,7 +875,7 @@ func isAliasingHeaderName(name string) bool {
return false
}
// removeHeadersWithUnderscores removes any request header and trailer whose name contains an underscore character.
// removeHeadersWithUnderscores removes any request header whose name contains an underscore character.
func removeHeadersWithUnderscores(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
for key := range req.Header {
@@ -888,7 +888,7 @@ func removeHeadersWithUnderscores(h http.Handler) http.Handler {
})
}
// rejectHeadersWithUnderscores rejects with a 400 Bad Request any request carrying a header or trailer whose name contains an underscore character.
// rejectHeadersWithUnderscores rejects with a 400 Bad Request any request carrying a header whose name contains an underscore character.
func rejectHeadersWithUnderscores(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
for key := range req.Header {
@@ -902,7 +902,7 @@ func rejectHeadersWithUnderscores(h http.Handler) http.Handler {
})
}
// removeAliasingHeaders removes any request header and trailer whose name contains a character
// removeAliasingHeaders removes any request header whose name contains a character
// which is neither a letter, a digit, nor a dash, as such a name aliases another header name.
func removeAliasingHeaders(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
@@ -916,7 +916,7 @@ func removeAliasingHeaders(h http.Handler) http.Handler {
})
}
// rejectAliasingHeaders rejects with a 400 Bad Request any request carrying a header or trailer whose name
// rejectAliasingHeaders rejects with a 400 Bad Request any request carrying a header whose name
// contains a character which is neither a letter, a digit, nor a dash, as such a name aliases another header name.
func rejectAliasingHeaders(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+5 -5
View File
@@ -214,15 +214,15 @@ func (b *Balancer) nextServer() (*namedHandler, error) {
if len(healthy) == 1 {
return healthy[0], nil
}
// In order to not get the same backend twice, we make the second call to s.rand.Intn one fewer
// In order to not get the same backend twice, we make the second call to b.rand.Intn one fewer
// than the length of the slice. We then have to shift over the second index if it is equal or
// greater than the first index, wrapping round if needed.
// greater than the first index.
b.randMu.Lock()
n1, n2 := b.rand.Intn(len(healthy)), b.rand.Intn(len(healthy))
n1, n2 := b.rand.Intn(len(healthy)), b.rand.Intn(len(healthy)-1)
b.randMu.Unlock()
if n2 == n1 {
n2 = (n2 + 1) % len(healthy)
if n2 >= n1 {
n2++
}
h1, h2 := healthy[n1], healthy[n2]
@@ -1,6 +1,7 @@
package p2c
import (
"math/rand"
"net/http"
"net/http/httptest"
"strconv"
@@ -64,6 +65,29 @@ func TestP2C(t *testing.T) {
}
}
// TestP2CSecondChoiceDistribution makes sure that the second candidate is drawn uniformly among
// the other servers, and does not favor the one following the first candidate.
func TestP2CSecondChoiceDistribution(t *testing.T) {
balancer := New(nil, false)
balancer.rand = rand.New(rand.NewSource(12345))
for _, h := range testHandlers(1, 0, 0) {
balancer.handlers = append(balancer.handlers, h)
balancer.status[h.name] = struct{}{}
}
selected := map[string]int{}
for range 3000 {
got, err := balancer.nextServer()
require.NoError(t, err)
selected[got.name]++
}
assert.InDelta(t, 1500, selected["1"], 100)
assert.InDelta(t, 1500, selected["2"], 100)
}
func TestSticky(t *testing.T) {
balancer := New(&dynamic.Sticky{
Cookie: &dynamic.Cookie{