Files
2026-01-29 17:38:06 +01:00

331 lines
11 KiB
Go

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,
}
}