diff --git a/CHANGELOG.md b/CHANGELOG.md index db6bb7180..a442e44c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [v2.11.45](https://github.com/traefik/traefik/tree/v2.11.45) (2026-05-05) +[All Commits](https://github.com/traefik/traefik/compare/v2.11.44...v2.11.45) + +**Bug fixes:** +- **[k8s/crd]** Remove cross-provider sanitization for Kubernetes service loading ([#13087](https://github.com/traefik/traefik/pull/13087) @rtribotte) +- **[docker, ecs]** Migrate to github.com/moby/moby modules ([#13053](https://github.com/traefik/traefik/pull/13053) @mmatur) + ## [v3.6.15](https://github.com/traefik/traefik/tree/v3.6.15) (2026-04-29) [All Commits](https://github.com/traefik/traefik/compare/v3.6.14...v3.6.15) diff --git a/pkg/provider/kubernetes/crd/fixtures/with_error_page_traefik_service.yml b/pkg/provider/kubernetes/crd/fixtures/with_error_page_traefik_service.yml new file mode 100644 index 000000000..4e3f18a8e --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_error_page_traefik_service.yml @@ -0,0 +1,29 @@ +apiVersion: traefik.io/v1alpha1 +kind: TraefikService +metadata: + name: errorpage-wrr + namespace: default + +spec: + weighted: + services: + - name: whoami + port: 80 + weight: 1 + +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: errorpage + namespace: default + +spec: + errors: + status: + - "404" + - "500" + query: query + service: + name: errorpage-wrr + kind: TraefikService diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index d820c412e..edd75981b 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -266,16 +266,20 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) continue } - errorPage, errorPageService, err := p.createErrorPageMiddleware(client, middleware.Namespace, middleware.Spec.Errors) + errorPageName, errorPage, errorPageService, err := p.createErrorPageMiddleware(ctxMid, client, middleware.Namespace, middleware.Spec.Errors) if err != nil { logger.Error().Err(err).Msg("Error while reading error page middleware") continue } - if errorPage != nil && errorPageService != nil { - serviceName := id + "-errorpage-service" - errorPage.Service = serviceName - conf.HTTP.Services[serviceName] = errorPageService + if errorPage != nil { + if errorPageService != nil { + serviceName := id + "-errorpage-service" + errorPage.Service = serviceName + conf.HTTP.Services[serviceName] = errorPageService + } else { + errorPage.Service = errorPageName + } } plugin, err := createPluginMiddleware(client, middleware.Namespace, middleware.Spec.Plugin) @@ -604,15 +608,9 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) return conf } -func (p *Provider) createErrorPageMiddleware(client Client, namespace string, errorPage *traefikv1alpha1.ErrorPage) (*dynamic.ErrorPage, *dynamic.Service, error) { +func (p *Provider) createErrorPageMiddleware(ctx context.Context, client Client, namespace string, errorPage *traefikv1alpha1.ErrorPage) (string, *dynamic.ErrorPage, *dynamic.Service, error) { if errorPage == nil { - return nil, nil, nil - } - - errorPageMiddleware := &dynamic.ErrorPage{ - Status: errorPage.Status, - StatusRewrites: errorPage.StatusRewrites, - Query: errorPage.Query, + return "", nil, nil, nil } cb := configBuilder{ @@ -622,12 +620,16 @@ func (p *Provider) createErrorPageMiddleware(client Client, namespace string, er allowEmptyServices: p.AllowEmptyServices, } - balancerServerHTTP, err := cb.buildServersLB(namespace, errorPage.Service.LoadBalancerSpec) + balancerName, balancerServerHTTP, err := cb.nameAndService(ctx, namespace, errorPage.Service.LoadBalancerSpec) if err != nil { - return nil, nil, err + return "", nil, nil, err } - return errorPageMiddleware, balancerServerHTTP, nil + return balancerName, &dynamic.ErrorPage{ + Status: errorPage.Status, + StatusRewrites: errorPage.StatusRewrites, + Query: errorPage.Query, + }, balancerServerHTTP, nil } // getServicePort always returns a valid port, an error otherwise. diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go index 653fb981b..8062cc363 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_http.go +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -375,7 +375,7 @@ func (c configBuilder) buildMirroring(ctx context.Context, tService *traefikv1al } // buildServersLB creates the configuration for the load-balancer of servers defined by svc. -func (c configBuilder) buildServersLB(namespace string, svc traefikv1alpha1.LoadBalancerSpec) (*dynamic.Service, error) { +func (c configBuilder) buildServersLB(svc traefikv1alpha1.LoadBalancerSpec) (*dynamic.Service, error) { lb := &dynamic.ServersLoadBalancer{} lb.SetDefaults() @@ -389,7 +389,7 @@ func (c configBuilder) buildServersLB(namespace string, svc traefikv1alpha1.Load // Here we are just logging a warning as the default value is already applied. case "RoundRobin": log.Warn(). - Str("namespace", namespace). + Str("namespace", svc.Namespace). Str("service", svc.Name). Msgf("RoundRobin strategy value is deprecated, please use %s value instead", dynamic.BalancerStrategyWRR) @@ -398,7 +398,7 @@ func (c configBuilder) buildServersLB(namespace string, svc traefikv1alpha1.Load } } - servers, err := c.loadServers(namespace, svc) + servers, err := c.loadServers(svc) if err != nil { return nil, err } @@ -493,7 +493,7 @@ func (c configBuilder) buildServersLB(namespace string, svc traefikv1alpha1.Load } } - lb.ServersTransport, err = c.makeServersTransportKey(namespace, svc.ServersTransport) + lb.ServersTransport, err = c.makeServersTransportKey(svc.Namespace, svc.ServersTransport) if err != nil { return nil, err } @@ -520,21 +520,13 @@ func (c configBuilder) makeServersTransportKey(parentNamespace string, serversTr return provider.Normalize(makeID(parentNamespace, serversTransportName)), nil } -func (c configBuilder) loadServers(parentNamespace string, svc traefikv1alpha1.LoadBalancerSpec) ([]dynamic.Server, error) { - namespace := namespaceOrFallback(svc, parentNamespace) - - if !isNamespaceAllowed(c.allowCrossNamespace, parentNamespace, namespace) { - return nil, fmt.Errorf("load balancer service %s/%s is not in the parent resource namespace %s", svc.Namespace, svc.Name, parentNamespace) - } - - // If the service uses explicitly the provider suffix - sanitizedName := strings.TrimSuffix(svc.Name, providerNamespaceSeparator+providerName) - service, exists, err := c.client.GetService(namespace, sanitizedName) +func (c configBuilder) loadServers(svc traefikv1alpha1.LoadBalancerSpec) ([]dynamic.Server, error) { + service, exists, err := c.client.GetService(svc.Namespace, svc.Name) if err != nil { return nil, err } if !exists { - return nil, fmt.Errorf("kubernetes service not found: %s/%s", namespace, sanitizedName) + return nil, fmt.Errorf("kubernetes service not found: %s/%s", svc.Namespace, svc.Name) } svcPort, err := getServicePort(service, svc.Port) @@ -543,12 +535,12 @@ func (c configBuilder) loadServers(parentNamespace string, svc traefikv1alpha1.L } if service.Spec.Type != corev1.ServiceTypeExternalName && svc.HealthCheck != nil { - return nil, fmt.Errorf("healthCheck allowed only for ExternalName services: %s/%s", namespace, sanitizedName) + return nil, fmt.Errorf("healthCheck allowed only for ExternalName services: %s/%s", svc.Namespace, svc.Name) } if service.Spec.Type == corev1.ServiceTypeExternalName { if !c.allowExternalNameServices { - return nil, fmt.Errorf("externalName services not allowed: %s/%s", namespace, sanitizedName) + return nil, fmt.Errorf("externalName services not allowed: %s/%s", svc.Namespace, svc.Name) } protocol, err := parseServiceProtocol(svc.Scheme, svcPort.Name, svcPort.Port) @@ -590,7 +582,7 @@ func (c configBuilder) loadServers(parentNamespace string, svc traefikv1alpha1.L return nil, nodesErr } if !nodesExists || len(nodes) == 0 { - return nil, fmt.Errorf("nodes not found for NodePort service %s/%s", namespace, sanitizedName) + return nil, fmt.Errorf("nodes not found for NodePort service %s/%s", svc.Namespace, svc.Name) } protocol, err := parseServiceProtocol(svc.Scheme, svcPort.Name, svcPort.Port) @@ -611,13 +603,13 @@ func (c configBuilder) loadServers(parentNamespace string, svc traefikv1alpha1.L } if len(servers) == 0 { - return nil, fmt.Errorf("no servers were generated for service %s in namespace", sanitizedName) + return nil, fmt.Errorf("no servers were generated for service %s in namespace", svc.Name) } return servers, nil } - endpointSlices, err := c.client.GetEndpointSlicesForService(namespace, sanitizedName) + endpointSlices, err := c.client.GetEndpointSlicesForService(svc.Namespace, svc.Name) if err != nil { return nil, fmt.Errorf("getting endpointslices: %w", err) } @@ -663,7 +655,7 @@ func (c configBuilder) loadServers(parentNamespace string, svc traefikv1alpha1.L } if len(servers) == 0 && !c.allowEmptyServices { - return nil, fmt.Errorf("no servers found for %s/%s", namespace, sanitizedName) + return nil, fmt.Errorf("no servers found for %s/%s", svc.Namespace, svc.Name) } return servers, nil @@ -676,25 +668,26 @@ func (c configBuilder) loadServers(parentNamespace string, svc traefikv1alpha1.L func (c configBuilder) nameAndService(ctx context.Context, parentNamespace string, service traefikv1alpha1.LoadBalancerSpec) (string, *dynamic.Service, error) { svcCtx := log.Ctx(ctx).With().Str(logs.ServiceName, service.Name).Logger().WithContext(ctx) - namespace := namespaceOrFallback(service, parentNamespace) + service = *service.DeepCopy() + service.Namespace = namespaceOrFallback(service, parentNamespace) - if !isNamespaceAllowed(c.allowCrossNamespace, parentNamespace, namespace) { + if !isNamespaceAllowed(c.allowCrossNamespace, parentNamespace, service.Namespace) { return "", nil, fmt.Errorf("service %s/%s not in the parent resource namespace %s", service.Namespace, service.Name, parentNamespace) } switch service.Kind { case "", "Service": - serversLB, err := c.buildServersLB(namespace, service) + serversLB, err := c.buildServersLB(service) if err != nil { return "", nil, err } - fullName := fullServiceName(svcCtx, namespace, service, service.Port) + fullName := fullServiceName(svcCtx, service, service.Port) return fullName, serversLB, nil case "TraefikService": - return fullServiceName(svcCtx, namespace, service, intstr.FromInt(0)), nil, nil + return fullServiceName(svcCtx, service, intstr.FromInt(0)), nil, nil default: return "", nil, fmt.Errorf("unsupported service kind %s", service.Kind) @@ -742,18 +735,18 @@ func splitSvcNameProvider(name string) (string, string) { return svc, pvd } -func fullServiceName(ctx context.Context, namespace string, service traefikv1alpha1.LoadBalancerSpec, port intstr.IntOrString) string { +func fullServiceName(ctx context.Context, service traefikv1alpha1.LoadBalancerSpec, port intstr.IntOrString) string { if (port.Type == intstr.Int && port.IntVal != 0) || (port.Type == intstr.String && port.StrVal != "") { - return provider.Normalize(fmt.Sprintf("%s-%s-%s", namespace, service.Name, &port)) + return provider.Normalize(fmt.Sprintf("%s-%s-%s", service.Namespace, service.Name, &port)) } if !strings.Contains(service.Name, providerNamespaceSeparator) { - return provider.Normalize(fmt.Sprintf("%s-%s", namespace, service.Name)) + return provider.Normalize(fmt.Sprintf("%s-%s", service.Namespace, service.Name)) } name, pName := splitSvcNameProvider(service.Name) if pName == providerName { - return provider.Normalize(fmt.Sprintf("%s-%s", namespace, name)) + return provider.Normalize(fmt.Sprintf("%s-%s", service.Namespace, name)) } if service.Namespace != "" { diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index 0a2ca954d..640c75383 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -4394,6 +4394,65 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, }, + { + desc: "Error page middleware referencing a TraefikService", + paths: []string{"services.yml", "with_error_page_traefik_service.yml"}, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TLS: &dynamic.TLSConfiguration{}, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{ + "default-errorpage": { + Errors: &dynamic.ErrorPage{ + Status: []string{"404", "500"}, + Service: "default-errorpage-wrr", + Query: "query", + }, + }, + }, + Services: map[string]*dynamic.Service{ + "default-errorpage-wrr": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-whoami-80", + Weight: func(i int) *int { return &i }(1), + }, + }, + }, + }, + "default-whoami-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Strategy: dynamic.BalancerStrategyWRR, + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + PassHostHeader: pointer(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, { desc: "Simple Ingress Route, with options", paths: []string{"services.yml", "with_options.yml"},