mirror of
https://github.com/traefik/traefik
synced 2026-02-03 06:30:31 +00:00
Add routing configuration extension points
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package ext
|
||||
|
||||
// HTTP is a dynamic.HTTP extension.
|
||||
type HTTP struct{}
|
||||
|
||||
// Router is a dynamic.Router extension.
|
||||
type Router struct{}
|
||||
@@ -0,0 +1,3 @@
|
||||
module github.com/traefik/traefik/dynamic/ext
|
||||
|
||||
go 1.24.0
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user