Add routing configuration extension points

This commit is contained in:
Harold Ozouf
2026-01-29 17:38:06 +01:00
committed by GitHub
parent 8425e09806
commit a4a91344ed
16 changed files with 1049 additions and 556 deletions
+1
View File
@@ -82,6 +82,7 @@ linters:
toolchain-pattern: go1\.\d+\.\d+$
tool-forbidden: true
go-version-pattern: ^1\.\d+(\.0)?$
replace-local: true
replace-allow-list:
- github.com/abbot/go-http-auth
- github.com/gorilla/mux
+5 -1
View File
@@ -37,6 +37,7 @@ require (
github.com/hashicorp/go-version v1.8.0
github.com/hashicorp/nomad/api v0.0.0-20231213195942-64e3dca9274b // No tag on the repo.
github.com/http-wasm/http-wasm-host-go v0.7.0
github.com/huandu/xstrings v1.5.0
github.com/influxdata/influxdb-client-go/v2 v2.7.0
github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab // No tag on the repo.
github.com/klauspost/compress v1.18.0
@@ -71,6 +72,7 @@ require (
github.com/tidwall/gjson v1.17.0
github.com/traefik/grpc-web v0.16.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
github.com/unrolled/render v1.0.2
github.com/unrolled/secure v1.0.9
@@ -253,7 +255,6 @@ require (
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/hcl v1.0.1-vault-5 // indirect
github.com/hashicorp/serf v0.10.1 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.182 // indirect
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
github.com/imdario/mergo v0.3.16 // indirect
@@ -411,6 +412,9 @@ require (
sigs.k8s.io/randfill v1.0.0 // indirect
)
// Dynamic config extension.
replace github.com/traefik/traefik/dynamic/ext => ./pkg/config/dynamic/ext
// Containous forks
replace (
github.com/abbot/go-http-auth => github.com/containous/go-http-auth v0.4.1-0.20200324110947-a37a7636d23e
+7
View File
@@ -0,0 +1,7 @@
package ext
// HTTP is a dynamic.HTTP extension.
type HTTP struct{}
// Router is a dynamic.Router extension.
type Router struct{}
+3
View File
@@ -0,0 +1,3 @@
module github.com/traefik/traefik/dynamic/ext
go 1.24.0
+48 -12
View File
@@ -5,6 +5,7 @@ import (
"time"
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/dynamic/ext"
otypes "github.com/traefik/traefik/v3/pkg/observability/types"
traefiktls "github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/types"
@@ -33,6 +34,8 @@ const (
// HTTPConfiguration contains all the HTTP configuration parameters.
type HTTPConfiguration struct {
ext.HTTP `yaml:",inline"`
Routers map[string]*Router `json:"routers,omitempty" toml:"routers,omitempty" yaml:"routers,omitempty" export:"true"`
Services map[string]*Service `json:"services,omitempty" toml:"services,omitempty" yaml:"services,omitempty" export:"true"`
Middlewares map[string]*Middleware `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
@@ -63,10 +66,22 @@ type Service struct {
Failover *Failover `json:"failover,omitempty" toml:"failover,omitempty" yaml:"failover,omitempty" label:"-" export:"true"`
}
// Merge merges another Service into this one.
// Returns true if the merge succeeds, false if configurations conflict.
func (s *Service) Merge(other *Service) bool {
if s.LoadBalancer == nil || other.LoadBalancer == nil {
return reflect.DeepEqual(s, other)
}
return s.LoadBalancer.Merge(other.LoadBalancer)
}
// +k8s:deepcopy-gen=true
// Router holds the router configuration.
type Router struct {
ext.Router `yaml:",inline"`
EntryPoints []string `json:"entryPoints,omitempty" toml:"entryPoints,omitempty" yaml:"entryPoints,omitempty" export:"true"`
Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
Service string `json:"service,omitempty" toml:"service,omitempty" yaml:"service,omitempty" export:"true"`
@@ -343,8 +358,39 @@ type ServersLoadBalancer struct {
ServersTransport string `json:"serversTransport,omitempty" toml:"serversTransport,omitempty" yaml:"serversTransport,omitempty" export:"true"`
}
// Mergeable tells if the given service is mergeable.
func (l *ServersLoadBalancer) Mergeable(loadBalancer *ServersLoadBalancer) bool {
// Merge merges the other load balancer into this one.
// Returns true if merge succeeded, false if configurations conflict.
func (l *ServersLoadBalancer) Merge(other *ServersLoadBalancer) bool {
if !l.mergeable(other) {
return false
}
// Deduplicate and append servers.
uniq := make(map[string]struct{}, len(l.Servers))
for _, server := range l.Servers {
uniq[server.URL] = struct{}{}
}
for _, server := range other.Servers {
if _, ok := uniq[server.URL]; !ok {
l.Servers = append(l.Servers, server)
}
}
return true
}
// SetDefaults Default values for a ServersLoadBalancer.
func (l *ServersLoadBalancer) SetDefaults() {
defaultPassHostHeader := DefaultPassHostHeader
l.PassHostHeader = &defaultPassHostHeader
l.Strategy = BalancerStrategyWRR
l.ResponseForwarding = &ResponseForwarding{}
l.ResponseForwarding.SetDefaults()
}
// mergeable tells if the given service is mergeable.
func (l *ServersLoadBalancer) mergeable(loadBalancer *ServersLoadBalancer) bool {
savedServers := l.Servers
defer func() {
l.Servers = savedServers
@@ -360,16 +406,6 @@ func (l *ServersLoadBalancer) Mergeable(loadBalancer *ServersLoadBalancer) bool
return reflect.DeepEqual(l, loadBalancer)
}
// SetDefaults Default values for a ServersLoadBalancer.
func (l *ServersLoadBalancer) SetDefaults() {
defaultPassHostHeader := DefaultPassHostHeader
l.PassHostHeader = &defaultPassHostHeader
l.Strategy = BalancerStrategyWRR
l.ResponseForwarding = &ResponseForwarding{}
l.ResponseForwarding.SetDefaults()
}
// +k8s:deepcopy-gen=true
// ResponseForwarding holds the response forwarding configuration.
+33 -2
View File
@@ -35,6 +35,16 @@ type TCPService struct {
Weighted *TCPWeightedRoundRobin `json:"weighted,omitempty" toml:"weighted,omitempty" yaml:"weighted,omitempty" label:"-" export:"true"`
}
// Merge merges another TCPService into this one.
// Returns true if the merge succeeds, false if configurations conflict.
func (s *TCPService) Merge(other *TCPService) bool {
if s.LoadBalancer == nil || other.LoadBalancer == nil {
return reflect.DeepEqual(s, other)
}
return s.LoadBalancer.Merge(other.LoadBalancer)
}
// +k8s:deepcopy-gen=true
// TCPWeightedRoundRobin is a weighted round robin tcp load-balancer of services.
@@ -102,8 +112,29 @@ type TCPServersLoadBalancer struct {
HealthCheck *TCPServerHealthCheck `json:"healthCheck,omitempty" toml:"healthCheck,omitempty" yaml:"healthCheck,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"`
}
// Mergeable tells if the given service is mergeable.
func (l *TCPServersLoadBalancer) Mergeable(loadBalancer *TCPServersLoadBalancer) bool {
// Merge merges the other load balancer into this one.
// Returns true if the merge succeeds, false if configurations conflict.
func (l *TCPServersLoadBalancer) Merge(other *TCPServersLoadBalancer) bool {
if !l.mergeable(other) {
return false
}
// Deduplicate and append servers.
uniq := make(map[string]struct{}, len(l.Servers))
for _, server := range l.Servers {
uniq[server.Address] = struct{}{}
}
for _, server := range other.Servers {
if _, ok := uniq[server.Address]; !ok {
l.Servers = append(l.Servers, server)
}
}
return true
}
// mergeable tells if the given service is mergeable.
func (l *TCPServersLoadBalancer) mergeable(loadBalancer *TCPServersLoadBalancer) bool {
savedServers := l.Servers
defer func() {
l.Servers = savedServers
+33 -2
View File
@@ -20,6 +20,16 @@ type UDPService struct {
Weighted *UDPWeightedRoundRobin `json:"weighted,omitempty" toml:"weighted,omitempty" yaml:"weighted,omitempty" label:"-" export:"true"`
}
// Merge merges another UDPService into this one.
// Returns true if the merge succeeds, false if configurations conflict.
func (s *UDPService) Merge(other *UDPService) bool {
if s.LoadBalancer == nil || other.LoadBalancer == nil {
return reflect.DeepEqual(s, other)
}
return s.LoadBalancer.Merge(other.LoadBalancer)
}
// +k8s:deepcopy-gen=true
// UDPWeightedRoundRobin is a weighted round robin UDP load-balancer of services.
@@ -56,8 +66,29 @@ type UDPServersLoadBalancer struct {
Servers []UDPServer `json:"servers,omitempty" toml:"servers,omitempty" yaml:"servers,omitempty" label-slice-as-struct:"server" export:"true"`
}
// Mergeable reports whether the given load-balancer can be merged with the receiver.
func (l *UDPServersLoadBalancer) Mergeable(loadBalancer *UDPServersLoadBalancer) bool {
// Merge merges the other load balancer into this one.
// Returns true if merge succeeded, false if configurations conflict.
func (l *UDPServersLoadBalancer) Merge(other *UDPServersLoadBalancer) bool {
if !l.mergeable(other) {
return false
}
// Deduplicate and append servers.
uniq := make(map[string]struct{}, len(l.Servers))
for _, server := range l.Servers {
uniq[server.Address] = struct{}{}
}
for _, server := range other.Servers {
if _, ok := uniq[server.Address]; !ok {
l.Servers = append(l.Servers, server)
}
}
return true
}
// mergeable reports whether the given load-balancer can be merged with the receiver.
func (l *UDPServersLoadBalancer) mergeable(loadBalancer *UDPServersLoadBalancer) bool {
savedServers := l.Servers
defer func() {
l.Servers = savedServers
@@ -489,6 +489,7 @@ func (in *HRWService) DeepCopy() *HRWService {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HTTPConfiguration) DeepCopyInto(out *HTTPConfiguration) {
*out = *in
out.HTTP = in.HTTP
if in.Routers != nil {
in, out := &in.Routers, &out.Routers
*out = make(map[string]*Router, len(*in))
@@ -1390,6 +1391,7 @@ func (in *Retry) DeepCopy() *Retry {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Router) DeepCopyInto(out *Router) {
*out = *in
out.Router = in.Router
if in.EntryPoints != nil {
in, out := &in.EntryPoints, &out.EntryPoints
*out = make([]string, len(*in))
-381
View File
@@ -4,7 +4,6 @@ import (
"bytes"
"context"
"maps"
"reflect"
"slices"
"strings"
"text/template"
@@ -14,388 +13,8 @@ import (
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/observability/logs"
"github.com/traefik/traefik/v3/pkg/tls"
)
// Merge merges multiple configurations.
func Merge(ctx context.Context, configurations map[string]*dynamic.Configuration) *dynamic.Configuration {
logger := log.Ctx(ctx)
configuration := &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),
},
TCP: &dynamic.TCPConfiguration{
Routers: make(map[string]*dynamic.TCPRouter),
Services: make(map[string]*dynamic.TCPService),
Middlewares: make(map[string]*dynamic.TCPMiddleware),
ServersTransports: make(map[string]*dynamic.TCPServersTransport),
},
UDP: &dynamic.UDPConfiguration{
Routers: make(map[string]*dynamic.UDPRouter),
Services: make(map[string]*dynamic.UDPService),
},
TLS: &dynamic.TLSConfiguration{
Stores: make(map[string]tls.Store),
},
}
servicesToDelete := map[string]struct{}{}
services := map[string][]string{}
routersToDelete := map[string]struct{}{}
routers := map[string][]string{}
servicesTCPToDelete := map[string]struct{}{}
servicesTCP := map[string][]string{}
routersTCPToDelete := map[string]struct{}{}
routersTCP := map[string][]string{}
servicesUDPToDelete := map[string]struct{}{}
servicesUDP := map[string][]string{}
routersUDPToDelete := map[string]struct{}{}
routersUDP := map[string][]string{}
middlewaresToDelete := map[string]struct{}{}
middlewares := map[string][]string{}
middlewaresTCPToDelete := map[string]struct{}{}
middlewaresTCP := map[string][]string{}
transportsToDelete := map[string]struct{}{}
transports := map[string][]string{}
transportsTCPToDelete := map[string]struct{}{}
transportsTCP := map[string][]string{}
storesToDelete := map[string]struct{}{}
stores := map[string][]string{}
var sortedKeys []string
for key := range configurations {
sortedKeys = append(sortedKeys, key)
}
slices.Sort(sortedKeys)
for _, root := range sortedKeys {
conf := configurations[root]
for serviceName, service := range conf.HTTP.Services {
services[serviceName] = append(services[serviceName], root)
if !AddService(configuration.HTTP, serviceName, service) {
servicesToDelete[serviceName] = struct{}{}
}
}
for routerName, router := range conf.HTTP.Routers {
routers[routerName] = append(routers[routerName], root)
if !AddRouter(configuration.HTTP, routerName, router) {
routersToDelete[routerName] = struct{}{}
}
}
for transportName, transport := range conf.HTTP.ServersTransports {
transports[transportName] = append(transports[transportName], root)
if !AddTransport(configuration.HTTP, transportName, transport) {
transportsToDelete[transportName] = struct{}{}
}
}
for serviceName, service := range conf.TCP.Services {
servicesTCP[serviceName] = append(servicesTCP[serviceName], root)
if !AddServiceTCP(configuration.TCP, serviceName, service) {
servicesTCPToDelete[serviceName] = struct{}{}
}
}
for routerName, router := range conf.TCP.Routers {
routersTCP[routerName] = append(routersTCP[routerName], root)
if !AddRouterTCP(configuration.TCP, routerName, router) {
routersTCPToDelete[routerName] = struct{}{}
}
}
for transportName, transport := range conf.TCP.ServersTransports {
transportsTCP[transportName] = append(transportsTCP[transportName], root)
if !AddTransportTCP(configuration.TCP, transportName, transport) {
transportsTCPToDelete[transportName] = struct{}{}
}
}
for serviceName, service := range conf.UDP.Services {
servicesUDP[serviceName] = append(servicesUDP[serviceName], root)
if !AddServiceUDP(configuration.UDP, serviceName, service) {
servicesUDPToDelete[serviceName] = struct{}{}
}
}
for routerName, router := range conf.UDP.Routers {
routersUDP[routerName] = append(routersUDP[routerName], root)
if !AddRouterUDP(configuration.UDP, routerName, router) {
routersUDPToDelete[routerName] = struct{}{}
}
}
for middlewareName, middleware := range conf.HTTP.Middlewares {
middlewares[middlewareName] = append(middlewares[middlewareName], root)
if !AddMiddleware(configuration.HTTP, middlewareName, middleware) {
middlewaresToDelete[middlewareName] = struct{}{}
}
}
for middlewareName, middleware := range conf.TCP.Middlewares {
middlewaresTCP[middlewareName] = append(middlewaresTCP[middlewareName], root)
if !AddMiddlewareTCP(configuration.TCP, middlewareName, middleware) {
middlewaresTCPToDelete[middlewareName] = struct{}{}
}
}
for storeName, store := range conf.TLS.Stores {
stores[storeName] = append(stores[storeName], root)
if !AddStore(configuration.TLS, storeName, store) {
storesToDelete[storeName] = struct{}{}
}
}
}
for serviceName := range servicesToDelete {
logger.Error().Str(logs.ServiceName, serviceName).
Interface("configuration", services[serviceName]).
Msg("Service defined multiple times with different configurations")
delete(configuration.HTTP.Services, serviceName)
}
for routerName := range routersToDelete {
logger.Error().Str(logs.RouterName, routerName).
Interface("configuration", routers[routerName]).
Msg("Router defined multiple times with different configurations")
delete(configuration.HTTP.Routers, routerName)
}
for transportName := range transportsToDelete {
logger.Error().Str(logs.ServersTransportName, transportName).
Interface("configuration", transports[transportName]).
Msg("ServersTransport defined multiple times with different configurations")
delete(configuration.HTTP.ServersTransports, transportName)
}
for serviceName := range servicesTCPToDelete {
logger.Error().Str(logs.ServiceName, serviceName).
Interface("configuration", servicesTCP[serviceName]).
Msg("Service TCP defined multiple times with different configurations")
delete(configuration.TCP.Services, serviceName)
}
for routerName := range routersTCPToDelete {
logger.Error().Str(logs.RouterName, routerName).
Interface("configuration", routersTCP[routerName]).
Msg("Router TCP defined multiple times with different configurations")
delete(configuration.TCP.Routers, routerName)
}
for transportName := range transportsTCPToDelete {
logger.Error().Str(logs.ServersTransportName, transportName).
Interface("configuration", transportsTCP[transportName]).
Msg("ServersTransport TCP defined multiple times with different configurations")
delete(configuration.TCP.ServersTransports, transportName)
}
for serviceName := range servicesUDPToDelete {
logger.Error().Str(logs.ServiceName, serviceName).
Interface("configuration", servicesUDP[serviceName]).
Msg("UDP service defined multiple times with different configurations")
delete(configuration.UDP.Services, serviceName)
}
for routerName := range routersUDPToDelete {
logger.Error().Str(logs.RouterName, routerName).
Interface("configuration", routersUDP[routerName]).
Msg("UDP router defined multiple times with different configurations")
delete(configuration.UDP.Routers, routerName)
}
for middlewareName := range middlewaresToDelete {
logger.Error().Str(logs.MiddlewareName, middlewareName).
Interface("configuration", middlewares[middlewareName]).
Msg("Middleware defined multiple times with different configurations")
delete(configuration.HTTP.Middlewares, middlewareName)
}
for middlewareName := range middlewaresTCPToDelete {
logger.Error().Str(logs.MiddlewareName, middlewareName).
Interface("configuration", middlewaresTCP[middlewareName]).
Msg("TCP Middleware defined multiple times with different configurations")
delete(configuration.TCP.Middlewares, middlewareName)
}
for storeName := range storesToDelete {
logger.Error().Str("storeName", storeName).
Msgf("TLS store defined multiple times with different configurations in %v", stores[storeName])
delete(configuration.TLS.Stores, storeName)
}
return configuration
}
// AddServiceTCP adds a service to a configuration.
func AddServiceTCP(configuration *dynamic.TCPConfiguration, serviceName string, service *dynamic.TCPService) bool {
if _, ok := configuration.Services[serviceName]; !ok {
configuration.Services[serviceName] = service
return true
}
if !configuration.Services[serviceName].LoadBalancer.Mergeable(service.LoadBalancer) {
return false
}
uniq := map[string]struct{}{}
for _, server := range configuration.Services[serviceName].LoadBalancer.Servers {
uniq[server.Address] = struct{}{}
}
for _, server := range service.LoadBalancer.Servers {
if _, ok := uniq[server.Address]; !ok {
configuration.Services[serviceName].LoadBalancer.Servers = append(configuration.Services[serviceName].LoadBalancer.Servers, server)
}
}
return true
}
// AddRouterTCP adds a router to a configuration.
func AddRouterTCP(configuration *dynamic.TCPConfiguration, routerName string, router *dynamic.TCPRouter) bool {
if _, ok := configuration.Routers[routerName]; !ok {
configuration.Routers[routerName] = router
return true
}
return reflect.DeepEqual(configuration.Routers[routerName], router)
}
// AddMiddlewareTCP adds a middleware to a configuration.
func AddMiddlewareTCP(configuration *dynamic.TCPConfiguration, middlewareName string, middleware *dynamic.TCPMiddleware) bool {
if _, ok := configuration.Middlewares[middlewareName]; !ok {
configuration.Middlewares[middlewareName] = middleware
return true
}
return reflect.DeepEqual(configuration.Middlewares[middlewareName], middleware)
}
// AddTransportTCP adds a servers transport to a configuration.
func AddTransportTCP(configuration *dynamic.TCPConfiguration, transportName string, transport *dynamic.TCPServersTransport) bool {
if _, ok := configuration.ServersTransports[transportName]; !ok {
configuration.ServersTransports[transportName] = transport
return true
}
return reflect.DeepEqual(configuration.ServersTransports[transportName], transport)
}
// AddServiceUDP adds a service to a configuration.
func AddServiceUDP(configuration *dynamic.UDPConfiguration, serviceName string, service *dynamic.UDPService) bool {
if _, ok := configuration.Services[serviceName]; !ok {
configuration.Services[serviceName] = service
return true
}
if !configuration.Services[serviceName].LoadBalancer.Mergeable(service.LoadBalancer) {
return false
}
uniq := map[string]struct{}{}
for _, server := range configuration.Services[serviceName].LoadBalancer.Servers {
uniq[server.Address] = struct{}{}
}
for _, server := range service.LoadBalancer.Servers {
if _, ok := uniq[server.Address]; !ok {
configuration.Services[serviceName].LoadBalancer.Servers = append(configuration.Services[serviceName].LoadBalancer.Servers, server)
}
}
return true
}
// AddRouterUDP adds a router to a configuration.
func AddRouterUDP(configuration *dynamic.UDPConfiguration, routerName string, router *dynamic.UDPRouter) bool {
if _, ok := configuration.Routers[routerName]; !ok {
configuration.Routers[routerName] = router
return true
}
return reflect.DeepEqual(configuration.Routers[routerName], router)
}
// AddService adds a service to a configuration.
func AddService(configuration *dynamic.HTTPConfiguration, serviceName string, service *dynamic.Service) bool {
if _, ok := configuration.Services[serviceName]; !ok {
configuration.Services[serviceName] = service
return true
}
if !configuration.Services[serviceName].LoadBalancer.Mergeable(service.LoadBalancer) {
return false
}
uniq := map[string]struct{}{}
for _, server := range configuration.Services[serviceName].LoadBalancer.Servers {
uniq[server.URL] = struct{}{}
}
for _, server := range service.LoadBalancer.Servers {
if _, ok := uniq[server.URL]; !ok {
configuration.Services[serviceName].LoadBalancer.Servers = append(configuration.Services[serviceName].LoadBalancer.Servers, server)
}
}
return true
}
// AddRouter adds a router to a configuration.
func AddRouter(configuration *dynamic.HTTPConfiguration, routerName string, router *dynamic.Router) bool {
if _, ok := configuration.Routers[routerName]; !ok {
configuration.Routers[routerName] = router
return true
}
return reflect.DeepEqual(configuration.Routers[routerName], router)
}
// AddTransport adds a servers transport to a configuration.
func AddTransport(configuration *dynamic.HTTPConfiguration, transportName string, transport *dynamic.ServersTransport) bool {
if _, ok := configuration.ServersTransports[transportName]; !ok {
configuration.ServersTransports[transportName] = transport
return true
}
return reflect.DeepEqual(configuration.ServersTransports[transportName], transport)
}
// AddMiddleware adds a middleware to a configuration.
func AddMiddleware(configuration *dynamic.HTTPConfiguration, middlewareName string, middleware *dynamic.Middleware) bool {
if _, ok := configuration.Middlewares[middlewareName]; !ok {
configuration.Middlewares[middlewareName] = middleware
return true
}
return reflect.DeepEqual(configuration.Middlewares[middlewareName], middleware)
}
// AddStore adds a middleware to a configurations.
func AddStore(configuration *dynamic.TLSConfiguration, storeName string, store tls.Store) bool {
if _, ok := configuration.Stores[storeName]; !ok {
configuration.Stores[storeName] = store
return true
}
return reflect.DeepEqual(configuration.Stores[storeName], store)
}
// MakeDefaultRuleTemplate creates the default rule template.
func MakeDefaultRuleTemplate(defaultRule string, funcMap template.FuncMap) (*template.Template, error) {
defaultFuncMap := sprig.TxtFuncMap()
+1 -1
View File
@@ -106,7 +106,7 @@ func (p *Provider) buildConfiguration(ctx context.Context, items []itemData, cer
configurations[svcName] = confFromLabel
}
return provider.Merge(ctx, configurations)
return provider.Merge(ctx, provider.NameSortedConfigurations(configurations), provider.ResourceStrategyMerge)
}
func (p *Provider) keepContainer(ctx context.Context, item itemData) bool {
+1 -1
View File
@@ -101,7 +101,7 @@ func (p *DynConfBuilder) build(ctx context.Context, containersInspected []docker
configurations[containerName] = confFromLabel
}
return provider.Merge(ctx, configurations)
return provider.Merge(ctx, provider.NameSortedConfigurations(configurations), provider.ResourceStrategyMerge)
}
func (p *DynConfBuilder) buildTCPServiceConfiguration(ctx context.Context, container dockerData, configuration *dynamic.TCPConfiguration) error {
+1 -1
View File
@@ -86,7 +86,7 @@ func (p *Provider) buildConfiguration(ctx context.Context, instances []ecsInstan
configurations[instanceName] = confFromLabel
}
return provider.Merge(ctx, configurations)
return provider.Merge(ctx, provider.NameSortedConfigurations(configurations), provider.ResourceStrategyMerge)
}
func (p *Provider) buildTCPServiceConfiguration(instance ecsInstance, configuration *dynamic.TCPConfiguration) error {
+26 -154
View File
@@ -221,7 +221,12 @@ func (p *Provider) buildConfiguration() (*dynamic.Configuration, error) {
ctx := log.With().Str(logs.ProviderName, providerName).Logger().WithContext(context.Background())
if len(p.Directory) > 0 {
return p.loadFileConfigFromDirectory(ctx, p.Directory, nil)
configurations, err := p.collectFileConfigs(ctx, p.Directory, "")
if err != nil {
return nil, fmt.Errorf("collecting file configs: %w", err)
}
return provider.Merge(ctx, configurations, provider.ResourceStrategySkipDuplicates), nil
}
if len(p.Filename) > 0 {
@@ -376,47 +381,28 @@ func (p *Provider) loadFileConfig(ctx context.Context, filename string, parseTem
return configuration, nil
}
func (p *Provider) loadFileConfigFromDirectory(ctx context.Context, directory string, configuration *dynamic.Configuration) (*dynamic.Configuration, error) {
// collectFileConfigs recursively collects configurations from files in the given directory.
func (p *Provider) collectFileConfigs(ctx context.Context, directory, prefix string) ([]provider.NamedConfiguration, error) {
var configurations []provider.NamedConfiguration
fileList, err := os.ReadDir(directory)
if err != nil {
return configuration, fmt.Errorf("unable to read directory %s: %w", directory, err)
return nil, fmt.Errorf("reading directory %s: %w", directory, err)
}
if configuration == nil {
configuration = &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),
},
TCP: &dynamic.TCPConfiguration{
Routers: make(map[string]*dynamic.TCPRouter),
Services: make(map[string]*dynamic.TCPService),
Middlewares: make(map[string]*dynamic.TCPMiddleware),
ServersTransports: make(map[string]*dynamic.TCPServersTransport),
},
TLS: &dynamic.TLSConfiguration{
Stores: make(map[string]tls.Store),
Options: make(map[string]tls.Options),
},
UDP: &dynamic.UDPConfiguration{
Routers: make(map[string]*dynamic.UDPRouter),
Services: make(map[string]*dynamic.UDPService),
},
}
}
configTLSMaps := make(map[*tls.CertAndStores]struct{})
for _, item := range fileList {
logger := log.Ctx(ctx).With().Str("filename", item.Name()).Logger()
itemPath := filepath.Join(directory, item.Name())
filename := item.Name()
if prefix != "" {
filename = filepath.Join(prefix, item.Name())
}
if item.IsDir() {
configuration, err = p.loadFileConfigFromDirectory(logger.WithContext(ctx), filepath.Join(directory, item.Name()), configuration)
sub, err := p.collectFileConfigs(ctx, itemPath, filename)
if err != nil {
return configuration, fmt.Errorf("unable to load content configuration from subdirectory %s: %w", item, err)
return nil, fmt.Errorf("loading content configuration from subdirectory %s: %w", item, err)
}
configurations = append(configurations, sub...)
continue
}
@@ -427,132 +413,18 @@ func (p *Provider) loadFileConfigFromDirectory(ctx context.Context, directory st
continue
}
var c *dynamic.Configuration
c, err = p.loadFileConfig(logger.WithContext(ctx), filepath.Join(directory, item.Name()), true)
c, err := p.loadFileConfig(ctx, itemPath, true)
if err != nil {
return configuration, fmt.Errorf("%s: %w", filepath.Join(directory, item.Name()), err)
return nil, fmt.Errorf("%s: %w", itemPath, err)
}
for name, conf := range c.HTTP.Routers {
if _, exists := configuration.HTTP.Routers[name]; exists {
logger.Warn().Str(logs.RouterName, name).Msg("HTTP router already configured, skipping")
} else {
configuration.HTTP.Routers[name] = conf
}
}
for name, conf := range c.HTTP.Middlewares {
if _, exists := configuration.HTTP.Middlewares[name]; exists {
logger.Warn().Str(logs.MiddlewareName, name).Msg("HTTP middleware already configured, skipping")
} else {
configuration.HTTP.Middlewares[name] = conf
}
}
for name, conf := range c.HTTP.Services {
if _, exists := configuration.HTTP.Services[name]; exists {
logger.Warn().Str(logs.ServiceName, name).Msg("HTTP service already configured, skipping")
} else {
configuration.HTTP.Services[name] = conf
}
}
for name, conf := range c.HTTP.ServersTransports {
if _, exists := configuration.HTTP.ServersTransports[name]; exists {
logger.Warn().Str(logs.ServersTransportName, name).Msg("HTTP servers transport already configured, skipping")
} else {
configuration.HTTP.ServersTransports[name] = conf
}
}
for name, conf := range c.TCP.Routers {
if _, exists := configuration.TCP.Routers[name]; exists {
logger.Warn().Str(logs.RouterName, name).Msg("TCP router already configured, skipping")
} else {
configuration.TCP.Routers[name] = conf
}
}
for name, conf := range c.TCP.Middlewares {
if _, exists := configuration.TCP.Middlewares[name]; exists {
logger.Warn().Str(logs.MiddlewareName, name).Msg("TCP middleware already configured, skipping")
} else {
configuration.TCP.Middlewares[name] = conf
}
}
for name, conf := range c.TCP.Services {
if _, exists := configuration.TCP.Services[name]; exists {
logger.Warn().Str(logs.ServiceName, name).Msg("TCP service already configured, skipping")
} else {
configuration.TCP.Services[name] = conf
}
}
for name, conf := range c.TCP.ServersTransports {
if _, exists := configuration.TCP.ServersTransports[name]; exists {
logger.Warn().Str(logs.ServersTransportName, name).Msg("TCP servers transport already configured, skipping")
} else {
configuration.TCP.ServersTransports[name] = conf
}
}
for name, conf := range c.UDP.Routers {
if _, exists := configuration.UDP.Routers[name]; exists {
logger.Warn().Str(logs.RouterName, name).Msg("UDP router already configured, skipping")
} else {
configuration.UDP.Routers[name] = conf
}
}
for name, conf := range c.UDP.Services {
if _, exists := configuration.UDP.Services[name]; exists {
logger.Warn().Str(logs.ServiceName, name).Msg("UDP service already configured, skipping")
} else {
configuration.UDP.Services[name] = conf
}
}
for _, conf := range c.TLS.Certificates {
if _, exists := configTLSMaps[conf]; exists {
logger.Warn().Msgf("TLS configuration %v already configured, skipping", conf)
} else {
configTLSMaps[conf] = struct{}{}
}
}
for name, conf := range c.TLS.Options {
if _, exists := configuration.TLS.Options[name]; exists {
logger.Warn().Msgf("TLS options %v already configured, skipping", name)
} else {
if configuration.TLS.Options == nil {
configuration.TLS.Options = map[string]tls.Options{}
}
configuration.TLS.Options[name] = conf
}
}
for name, conf := range c.TLS.Stores {
if _, exists := configuration.TLS.Stores[name]; exists {
logger.Warn().Msgf("TLS store %v already configured, skipping", name)
} else {
if configuration.TLS.Stores == nil {
configuration.TLS.Stores = map[string]tls.Store{}
}
configuration.TLS.Stores[name] = conf
}
}
configurations = append(configurations, provider.NamedConfiguration{
Name: filename,
Configuration: c,
})
}
if len(configTLSMaps) > 0 && configuration.TLS == nil {
configuration.TLS = &dynamic.TLSConfiguration{}
}
for conf := range configTLSMaps {
configuration.TLS.Certificates = append(configuration.TLS.Certificates, conf)
}
return configuration, nil
return configurations, nil
}
func (p *Provider) decodeConfiguration(filePath, content string) (*dynamic.Configuration, error) {
+330
View File
@@ -0,0 +1,330 @@
package provider
import (
"context"
"maps"
"reflect"
"slices"
"strings"
"github.com/huandu/xstrings"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/observability/logs"
"github.com/traefik/traefik/v3/pkg/tls"
)
type resourceMeta struct {
logField string
displayName string
}
var resourceLogFields = map[reflect.Type]resourceMeta{
reflect.TypeFor[dynamic.Router](): {logs.RouterName, "HTTP router"},
reflect.TypeFor[dynamic.Service](): {logs.ServiceName, "HTTP service"},
reflect.TypeFor[dynamic.Middleware](): {logs.MiddlewareName, "HTTP middleware"},
reflect.TypeFor[dynamic.ServersTransport](): {logs.ServersTransportName, "HTTP servers transport"},
reflect.TypeFor[dynamic.TCPRouter](): {logs.RouterName, "TCP router"},
reflect.TypeFor[dynamic.TCPService](): {logs.ServiceName, "TCP service"},
reflect.TypeFor[dynamic.TCPMiddleware](): {logs.MiddlewareName, "TCP middleware"},
reflect.TypeFor[dynamic.TCPServersTransport](): {logs.ServersTransportName, "TCP servers transport"},
reflect.TypeFor[dynamic.UDPRouter](): {logs.RouterName, "UDP router"},
reflect.TypeFor[dynamic.UDPService](): {logs.ServiceName, "UDP service"},
}
// ResourceStrategy defines how the merge should handle resources.
type ResourceStrategy int
const (
// ResourceStrategyMerge tries to call the Merge method on the resource.
ResourceStrategyMerge ResourceStrategy = iota
// ResourceStrategySkipDuplicates skips duplicate resources.
ResourceStrategySkipDuplicates
)
// NamedConfiguration is a configuration with its name.
type NamedConfiguration struct {
Name string
Configuration *dynamic.Configuration
}
// NameSortedConfigurations returns the configurations sorted by name.
func NameSortedConfigurations(configurations map[string]*dynamic.Configuration) []NamedConfiguration {
origins := slices.Sorted(maps.Keys(configurations))
sorted := make([]NamedConfiguration, 0, len(origins))
for _, origin := range origins {
sorted = append(sorted, NamedConfiguration{Name: origin, Configuration: configurations[origin]})
}
return sorted
}
// Merge merges multiple configurations.
func Merge(ctx context.Context, configurations []NamedConfiguration, strategy ResourceStrategy) *dynamic.Configuration {
merged := &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),
},
TCP: &dynamic.TCPConfiguration{
Routers: make(map[string]*dynamic.TCPRouter),
Services: make(map[string]*dynamic.TCPService),
Middlewares: make(map[string]*dynamic.TCPMiddleware),
ServersTransports: make(map[string]*dynamic.TCPServersTransport),
},
UDP: &dynamic.UDPConfiguration{
Routers: make(map[string]*dynamic.UDPRouter),
Services: make(map[string]*dynamic.UDPService),
},
TLS: &dynamic.TLSConfiguration{
Stores: make(map[string]tls.Store),
},
}
tracker := newMergeTracker()
for _, c := range configurations {
if c.Configuration.HTTP != nil {
mergeResourceMaps(ctx, reflect.ValueOf(merged.HTTP).Elem(), reflect.ValueOf(c.Configuration.HTTP).Elem(), c.Name, tracker, strategy)
}
if c.Configuration.TCP != nil {
mergeResourceMaps(ctx, reflect.ValueOf(merged.TCP).Elem(), reflect.ValueOf(c.Configuration.TCP).Elem(), c.Name, tracker, strategy)
}
if c.Configuration.UDP != nil {
mergeResourceMaps(ctx, reflect.ValueOf(merged.UDP).Elem(), reflect.ValueOf(c.Configuration.UDP).Elem(), c.Name, tracker, strategy)
}
if c.Configuration.TLS != nil {
mergeResourceMaps(ctx, reflect.ValueOf(merged.TLS).Elem(), reflect.ValueOf(c.Configuration.TLS).Elem(), c.Name, tracker, strategy)
merged.TLS.Certificates = mergeCertificates(ctx, merged.TLS.Certificates, c.Configuration.TLS.Certificates, c.Name, strategy)
}
}
deleteConflicts(ctx, tracker)
return merged
}
// mergeResourceMaps merges all the resource maps defined in the provided struct.
// Conflicts are recorded in the given merge tracker.
func mergeResourceMaps(ctx context.Context, dst, src reflect.Value, origin string, tracker *mergeTracker, strategy ResourceStrategy) {
dstType := dst.Type()
for i := range dstType.NumField() {
field := dstType.Field(i)
if !field.IsExported() {
continue
}
dstField := dst.Field(i)
srcField := src.Field(i)
// Merge the resource maps of embedded structs.
if field.Anonymous {
mergeResourceMaps(ctx, dstField, srcField, origin, tracker, strategy)
continue
}
if dstField.Kind() == reflect.Map {
mergeResourceMap(ctx, dstField, srcField, origin, tracker, strategy)
}
}
}
// mergeResourceMap merges a resource map src into dst.
// New keys from src are added to dst.
// Duplicate keys are merged if the resource type implements a Merge method, otherwise
// the values must be identical. Conflicts are recorded in the given merge tracker.
func mergeResourceMap(ctx context.Context, dst, src reflect.Value, origin string, tracker *mergeTracker, strategy ResourceStrategy) {
if src.IsNil() {
return
}
if dst.IsNil() {
dst.Set(reflect.MakeMap(dst.Type()))
}
for _, resourceKey := range src.MapKeys() {
resourceKeyStr := resourceKey.String()
tracker.recordOrigin(dst, resourceKeyStr, origin)
srcValue := src.MapIndex(resourceKey)
dstValue := dst.MapIndex(resourceKey)
// Key doesn't exist in dst, add it.
if !dstValue.IsValid() {
dst.SetMapIndex(resourceKey, srcValue)
continue
}
// Key exists, need to merge or detect conflict.
switch strategy {
case ResourceStrategyMerge:
if !tryMerge(dstValue, srcValue) {
tracker.markForDeletion(dst, resourceKeyStr, dst.Type().Elem())
}
case ResourceStrategySkipDuplicates:
logSkippedDuplicate(ctx, dst.Type().Elem(), resourceKeyStr, origin)
}
}
}
// tryMerge attempts to merge two resources.
// Returns true if the merge succeeds, false if values conflict.
func tryMerge(dst, src reflect.Value) bool {
if dst.Kind() != reflect.Ptr {
return reflect.DeepEqual(dst.Interface(), src.Interface())
}
if dst.IsNil() || src.IsNil() {
return reflect.DeepEqual(dst.Interface(), src.Interface())
}
// Check if the struct has the method `func (* T) Merge(other T) bool`.
// We use reflection to detect this method because Go's type system doesn't allow type assertions
// on generic interfaces (Mergeable[T]) practically.
mergeMethod := dst.MethodByName("Merge")
if mergeMethod.IsValid() {
methodType := mergeMethod.Type()
if methodType.NumIn() == 1 && methodType.NumOut() == 1 && methodType.Out(0).Kind() == reflect.Bool {
// Make sure the parameter type matches the type holding the method.
if methodType.In(0).AssignableTo(src.Type()) {
results := mergeMethod.Call([]reflect.Value{src})
return results[0].Bool()
}
}
}
// When Merge is not implemented, merge is not allowed; the values must be the same.
return reflect.DeepEqual(dst.Elem().Interface(), src.Elem().Interface())
}
// deleteConflicts removes conflicting items and logs errors.
func deleteConflicts(ctx context.Context, tracker *mergeTracker) {
logger := log.Ctx(ctx)
for ck, info := range tracker.toDelete {
resourceNameField, resourceTypeWords := resourceLogMeta(info.resourceType)
logger.Error().
Str(resourceNameField, ck.resourceKey).
Interface("configuration", tracker.origins[ck]).
Msgf("%s defined multiple times with different configurations", resourceTypeWords)
info.resourceMap.SetMapIndex(reflect.ValueOf(ck.resourceKey), reflect.Value{})
}
}
// mergeCertificates merges multiple certificates.
func mergeCertificates(ctx context.Context, certificates []*tls.CertAndStores, newCertificates []*tls.CertAndStores, origin string, strategy ResourceStrategy) []*tls.CertAndStores {
for _, certificate := range newCertificates {
var found bool
for _, existingCertificate := range certificates {
if existingCertificate.Certificate == certificate.Certificate {
found = true
switch strategy {
case ResourceStrategyMerge:
existingCertificate.Stores = mergeStores(existingCertificate.Stores, certificate.Stores)
case ResourceStrategySkipDuplicates:
log.Ctx(ctx).Warn().
Str("origin", origin).
Msgf("TLS certificate %v already configured, skipping", certificate.Certificate)
}
break
}
}
if !found {
certificates = append(certificates, certificate)
}
}
return certificates
}
// mergeStores merges two store slices, deduplicating entries while. Order is preserved.
func mergeStores(existing, other []string) []string {
seen := make(map[string]struct{}, len(existing))
for _, s := range existing {
seen[s] = struct{}{}
}
for _, s := range other {
if _, ok := seen[s]; !ok {
existing = append(existing, s)
seen[s] = struct{}{}
}
}
return existing
}
// logSkippedDuplicate logs a warning when a duplicate resource is skipped.
func logSkippedDuplicate(ctx context.Context, resourceType reflect.Type, resourceKey, origin string) {
resourceNameField, resourceTypeWords := resourceLogMeta(resourceType)
log.Ctx(ctx).Warn().
Str("origin", origin).
Str(resourceNameField, resourceKey).
Msgf("%s already configured, skipping", resourceTypeWords)
}
// resourceLogMeta returns the log field name and human-readable type description for the given resource element type.
func resourceLogMeta(resourceType reflect.Type) (resourceNameField, resourceTypeWords string) {
if resourceType.Kind() == reflect.Ptr {
resourceType = resourceType.Elem()
}
meta, ok := resourceLogFields[resourceType]
if ok {
return meta.logField, meta.displayName
}
resourceTypeName := resourceType.Name()
resourceNameField = xstrings.ToCamelCase(resourceTypeName) + "Name"
resourceTypeWords = strings.ReplaceAll(xstrings.ToKebabCase(resourceTypeName), "-", " ")
return resourceNameField, resourceTypeWords
}
// mergeTracker tracks item origins and items marked for deletion during merge.
type mergeTracker struct {
toDelete map[conflictKey]conflictInfo
origins map[conflictKey][]string
}
// conflictKey uniquely identifies an entry in a map.
type conflictKey struct {
mapPtr uintptr
resourceKey string
}
// conflictInfo stores information about a merge conflict.
type conflictInfo struct {
resourceMap reflect.Value // The map to delete from.
resourceType reflect.Type
}
func newMergeTracker() *mergeTracker {
return &mergeTracker{
toDelete: make(map[conflictKey]conflictInfo),
origins: make(map[conflictKey][]string),
}
}
func (t *mergeTracker) recordOrigin(resourceMap reflect.Value, resourceKey, origin string) {
ck := conflictKey{mapPtr: resourceMap.Pointer(), resourceKey: resourceKey}
t.origins[ck] = append(t.origins[ck], origin)
}
func (t *mergeTracker) markForDeletion(resourceMap reflect.Value, resourceKey string, resourceType reflect.Type) {
ck := conflictKey{mapPtr: resourceMap.Pointer(), resourceKey: resourceKey}
t.toDelete[ck] = conflictInfo{
resourceMap: resourceMap,
resourceType: resourceType,
}
}
+557
View File
@@ -0,0 +1,557 @@
package provider
import (
"context"
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/tls"
)
// testResource is a simple type without a Merge method.
type testResource struct {
Value string
}
// testMergeableResource implements the Merge method.
type testMergeableResource struct {
Config string
Servers []string
}
// Merge merges another testMergeableResource into this one.
// Returns true if the merge succeeds, false if configurations conflict.
func (r *testMergeableResource) Merge(other *testMergeableResource) bool {
if r.Config != other.Config {
return false
}
r.Servers = append(r.Servers, other.Servers...)
return true
}
// testCollectionSet is a container with a map field.
type testCollectionSet struct {
Resources map[string]*testResource
}
// testMergeableCollectionSet is a container with a mergeable map field.
type testMergeableCollectionSet struct {
Resources map[string]*testMergeableResource
}
// TestEmbedded is an embedded struct for testing anonymous field handling.
// Must be exported for reflection to process it.
type TestEmbedded struct {
EmbeddedItems map[string]*testResource
}
// testCollectionSetWithEmbedded has both embedded and direct map fields.
type testCollectionSetWithEmbedded struct {
TestEmbedded
Items map[string]*testResource
}
func TestMergeCollectionSet_BasicMapMerge(t *testing.T) {
dst := &testCollectionSet{
Resources: map[string]*testResource{
"existing": {Value: "dst"},
},
}
src := &testCollectionSet{
Resources: map[string]*testResource{
"new": {Value: "src"},
},
}
tracker := newMergeTracker()
mergeResourceMaps(context.Background(), reflect.ValueOf(dst).Elem(), reflect.ValueOf(src).Elem(), "provider", tracker, ResourceStrategyMerge)
assert.Empty(t, tracker.toDelete)
assert.Equal(t, &testCollectionSet{
Resources: map[string]*testResource{
"existing": {Value: "dst"},
"new": {Value: "src"},
},
}, dst)
}
func TestMergeCollectionSet_EmbeddedStruct(t *testing.T) {
dst := &testCollectionSetWithEmbedded{
TestEmbedded: TestEmbedded{
EmbeddedItems: map[string]*testResource{
"embedded1": {Value: "dst-embedded"},
},
},
Items: map[string]*testResource{
"item1": {Value: "dst-item"},
},
}
src := &testCollectionSetWithEmbedded{
TestEmbedded: TestEmbedded{
EmbeddedItems: map[string]*testResource{
"embedded2": {Value: "src-embedded"},
},
},
Items: map[string]*testResource{
"item2": {Value: "src-item"},
},
}
tracker := newMergeTracker()
mergeResourceMaps(context.Background(), reflect.ValueOf(dst).Elem(), reflect.ValueOf(src).Elem(), "provider", tracker, ResourceStrategyMerge)
assert.Empty(t, tracker.toDelete)
assert.Equal(t, &testCollectionSetWithEmbedded{
TestEmbedded: TestEmbedded{
EmbeddedItems: map[string]*testResource{
"embedded1": {Value: "dst-embedded"},
"embedded2": {Value: "src-embedded"},
},
},
Items: map[string]*testResource{
"item1": {Value: "dst-item"},
"item2": {Value: "src-item"},
},
}, dst)
}
func TestMergeCollectionSet_MergeableInterface(t *testing.T) {
dst := &testMergeableCollectionSet{
Resources: map[string]*testMergeableResource{
"svc1": {Config: "same", Servers: []string{"server1"}},
},
}
src := &testMergeableCollectionSet{
Resources: map[string]*testMergeableResource{
"svc1": {Config: "same", Servers: []string{"server2"}},
},
}
tracker := newMergeTracker()
mergeResourceMaps(context.Background(), reflect.ValueOf(dst).Elem(), reflect.ValueOf(src).Elem(), "provider", tracker, ResourceStrategyMerge)
assert.Empty(t, tracker.toDelete)
assert.Equal(t, &testMergeableCollectionSet{
Resources: map[string]*testMergeableResource{
"svc1": {Config: "same", Servers: []string{"server1", "server2"}},
},
}, dst)
}
func TestMergeCollectionSet_MergeableConflict(t *testing.T) {
dst := &testMergeableCollectionSet{
Resources: map[string]*testMergeableResource{
"svc1": {Config: "config-A", Servers: []string{"server1"}},
},
}
src := &testMergeableCollectionSet{
Resources: map[string]*testMergeableResource{
"svc1": {Config: "config-B", Servers: []string{"server2"}},
},
}
tracker := newMergeTracker()
mergeResourceMaps(context.Background(), reflect.ValueOf(dst).Elem(), reflect.ValueOf(src).Elem(), "provider1", tracker, ResourceStrategyMerge)
// Merge() returns false due to config mismatch -> marked for deletion.
assert.Len(t, tracker.toDelete, 1)
assertMarkedForDeletion(t, tracker.toDelete, "svc1")
}
func TestMergeCollectionSet_DeepEqualFallback(t *testing.T) {
dst := &testCollectionSet{
Resources: map[string]*testResource{
"res1": {Value: "same"},
},
}
src := &testCollectionSet{
Resources: map[string]*testResource{
"res1": {Value: "same"},
},
}
tracker := newMergeTracker()
mergeResourceMaps(context.Background(), reflect.ValueOf(dst).Elem(), reflect.ValueOf(src).Elem(), "provider1", tracker, ResourceStrategyMerge)
// Same values -> no conflict.
assert.Empty(t, tracker.toDelete)
assert.Equal(t, &testCollectionSet{
Resources: map[string]*testResource{
"res1": {Value: "same"},
},
}, dst)
}
func TestMergeCollectionSet_DeepEqualConflict(t *testing.T) {
dst := &testCollectionSet{
Resources: map[string]*testResource{
"res1": {Value: "value-A"},
},
}
src := &testCollectionSet{
Resources: map[string]*testResource{
"res1": {Value: "value-B"},
},
}
tracker := newMergeTracker()
mergeResourceMaps(context.Background(), reflect.ValueOf(dst).Elem(), reflect.ValueOf(src).Elem(), "provider1", tracker, ResourceStrategyMerge)
// Different values, no Merge method -> conflict.
assert.Len(t, tracker.toDelete, 1)
assertMarkedForDeletion(t, tracker.toDelete, "res1")
}
func TestMerge(t *testing.T) {
testCases := []struct {
desc string
configurations map[string]*dynamic.Configuration
strategy ResourceStrategy
expected *dynamic.Configuration
}{
{
desc: "HTTP routers: multiple providers different routers",
configurations: map[string]*dynamic.Configuration{
"provider1": {
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"router1": {Rule: "Host(`example1.com`)"},
},
},
},
"provider2": {
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"router2": {Rule: "Host(`example2.com`)"},
},
},
},
},
strategy: ResourceStrategyMerge,
expected: buildExpectedConfiguration(func(c *dynamic.Configuration) {
c.HTTP.Routers["router1"] = &dynamic.Router{Rule: "Host(`example1.com`)"}
c.HTTP.Routers["router2"] = &dynamic.Router{Rule: "Host(`example2.com`)"}
}),
},
{
desc: "HTTP routers: conflict multiple providers same router different config",
configurations: map[string]*dynamic.Configuration{
"provider1": {
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"router1": {Rule: "Host(`example1.com`)"},
},
},
},
"provider2": {
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"router1": {Rule: "Host(`example2.com`)"},
},
},
},
},
strategy: ResourceStrategyMerge,
expected: buildExpectedConfiguration(nil),
},
{
desc: "HTTP services: multiple providers same service servers merged",
configurations: map[string]*dynamic.Configuration{
"provider1": {
HTTP: &dynamic.HTTPConfiguration{
Services: map[string]*dynamic.Service{
"service1": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{URL: "http://server1:80"},
},
},
},
},
},
},
"provider2": {
HTTP: &dynamic.HTTPConfiguration{
Services: map[string]*dynamic.Service{
"service1": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{URL: "http://server2:80"},
},
},
},
},
},
},
},
strategy: ResourceStrategyMerge,
expected: buildExpectedConfiguration(func(c *dynamic.Configuration) {
c.HTTP.Services["service1"] = &dynamic.Service{
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{URL: "http://server1:80"},
{URL: "http://server2:80"},
},
},
}
}),
},
{
desc: "HTTP services: multiple providers same service duplicate servers deduplicated",
configurations: map[string]*dynamic.Configuration{
"provider1": {
HTTP: &dynamic.HTTPConfiguration{
Services: map[string]*dynamic.Service{
"service1": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{URL: "http://server1:80"},
},
},
},
},
},
},
"provider2": {
HTTP: &dynamic.HTTPConfiguration{
Services: map[string]*dynamic.Service{
"service1": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{URL: "http://server1:80"},
},
},
},
},
},
},
},
strategy: ResourceStrategyMerge,
expected: buildExpectedConfiguration(func(c *dynamic.Configuration) {
c.HTTP.Services["service1"] = &dynamic.Service{
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{URL: "http://server1:80"},
},
},
}
}),
},
{
desc: "TLS certificates: different certificates both kept",
configurations: map[string]*dynamic.Configuration{
"provider1": {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{CertFile: "cert1.pem", KeyFile: "key1.pem"},
Stores: []string{"store1"},
},
},
},
},
"provider2": {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{CertFile: "cert2.pem", KeyFile: "key2.pem"},
Stores: []string{"store2"},
},
},
},
},
},
strategy: ResourceStrategyMerge,
expected: buildExpectedConfiguration(func(c *dynamic.Configuration) {
c.TLS.Certificates = []*tls.CertAndStores{
{
Certificate: tls.Certificate{CertFile: "cert1.pem", KeyFile: "key1.pem"},
Stores: []string{"store1"},
},
{
Certificate: tls.Certificate{CertFile: "cert2.pem", KeyFile: "key2.pem"},
Stores: []string{"store2"},
},
}
}),
},
{
desc: "TLS certificates: same certificate stores merged with ResourceStrategyMerge",
configurations: map[string]*dynamic.Configuration{
"provider1": {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"},
Stores: []string{"store1"},
},
},
},
},
"provider2": {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"},
Stores: []string{"store2"},
},
},
},
},
},
strategy: ResourceStrategyMerge,
expected: buildExpectedConfiguration(func(c *dynamic.Configuration) {
c.TLS.Certificates = []*tls.CertAndStores{
{
Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"},
Stores: []string{"store1", "store2"},
},
}
}),
},
{
desc: "TLS certificates: same certificate overlapping stores deduplicated",
configurations: map[string]*dynamic.Configuration{
"provider1": {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"},
Stores: []string{"store1", "store2"},
},
},
},
},
"provider2": {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"},
Stores: []string{"store2", "store3"},
},
},
},
},
},
strategy: ResourceStrategyMerge,
expected: buildExpectedConfiguration(func(c *dynamic.Configuration) {
c.TLS.Certificates = []*tls.CertAndStores{
{
Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"},
Stores: []string{"store1", "store2", "store3"},
},
}
}),
},
{
desc: "TLS certificates: same certificate stores not merged with ResourceStrategySkipDuplicates",
configurations: map[string]*dynamic.Configuration{
"provider1": {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"},
Stores: []string{"store1"},
},
},
},
},
"provider2": {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"},
Stores: []string{"store2"},
},
},
},
},
},
strategy: ResourceStrategySkipDuplicates,
expected: buildExpectedConfiguration(func(c *dynamic.Configuration) {
c.TLS.Certificates = []*tls.CertAndStores{
{
Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"},
Stores: []string{"store1"},
},
}
}),
},
{
desc: "nil configuration from one provider",
configurations: map[string]*dynamic.Configuration{
"provider1": {
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"router1": {Rule: "Host(`example.com`)"},
},
},
},
"provider2": {
// No HTTP configuration
},
},
strategy: ResourceStrategyMerge,
expected: buildExpectedConfiguration(func(c *dynamic.Configuration) {
c.HTTP.Routers["router1"] = &dynamic.Router{Rule: "Host(`example.com`)"}
}),
},
{
desc: "empty configurations",
configurations: map[string]*dynamic.Configuration{},
strategy: ResourceStrategyMerge,
expected: buildExpectedConfiguration(nil),
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
result := Merge(context.Background(), NameSortedConfigurations(test.configurations), test.strategy)
assert.Equal(t, test.expected, result)
})
}
}
func buildExpectedConfiguration(modifier func(*dynamic.Configuration)) *dynamic.Configuration {
c := &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),
},
TCP: &dynamic.TCPConfiguration{
Routers: make(map[string]*dynamic.TCPRouter),
Services: make(map[string]*dynamic.TCPService),
Middlewares: make(map[string]*dynamic.TCPMiddleware),
ServersTransports: make(map[string]*dynamic.TCPServersTransport),
},
UDP: &dynamic.UDPConfiguration{
Routers: make(map[string]*dynamic.UDPRouter),
Services: make(map[string]*dynamic.UDPService),
},
TLS: &dynamic.TLSConfiguration{
Stores: make(map[string]tls.Store),
},
}
if modifier != nil {
modifier(c)
}
return c
}
// assertMarkedForDeletion checks that toDelete contains an entry with the given key.
func assertMarkedForDeletion(t *testing.T, toDelete map[conflictKey]conflictInfo, key string) {
t.Helper()
for ck := range toDelete {
if ck.resourceKey == key {
return
}
}
t.Errorf("toDelete does not contain key %q", key)
}
+1 -1
View File
@@ -84,7 +84,7 @@ func (p *Provider) buildConfig(ctx context.Context, items []item) *dynamic.Confi
configurations[svcName] = config
}
return provider.Merge(ctx, configurations)
return provider.Merge(ctx, provider.NameSortedConfigurations(configurations), provider.ResourceStrategyMerge)
}
func (p *Provider) buildTCPConfig(i item, configuration *dynamic.TCPConfiguration) error {