Do not require a port for ExternalName services

This commit is contained in:
Kevin Pollet 2026-04-24 11:16:05 +02:00 committed by GitHub
parent 2e80ad282a
commit 28e655452c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 187 additions and 28 deletions

View File

@ -0,0 +1,41 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-with-external-name
namespace: default
spec:
ingressClassName: nginx
rules:
- host: external.localhost
http:
paths:
- path: /external-with-matching-port
pathType: Exact
backend:
service:
name: external
port:
number: 80
- path: /external-with-matching-named-port
pathType: Exact
backend:
service:
name: external
port:
name: http
- path: /external-with-non-matching-port
pathType: Exact
backend:
service:
name: external
port:
number: 3000
- path: /external-with-non-matching-named-port
pathType: Exact
backend:
service:
name: external
port:
name: foo

View File

@ -78,3 +78,18 @@ endpoints:
- 10.10.0.6
conditions:
ready: true
---
kind: Service
apiVersion: v1
metadata:
name: external
namespace: default
spec:
type: ExternalName
externalName: external.com
ports:
- port: 80
targetPort: 8080
name: http

View File

@ -27,6 +27,7 @@ import (
"github.com/traefik/traefik/v3/pkg/types"
corev1 "k8s.io/api/core/v1"
netv1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/utils/ptr"
)
@ -375,12 +376,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
continue
}
port := backend.Service.Port.Name
if len(backend.Service.Port.Name) == 0 {
port = strconv.Itoa(int(backend.Service.Port.Number))
}
serviceName := provider.Normalize(ingress.Namespace + "-" + backend.Service.Name + "-" + port)
serviceName := provider.Normalize(ingress.Namespace + "-" + backend.Service.Name + "-" + portString(backend.Service.Port))
conf.TCP.Services[serviceName] = service
routerKey := strings.TrimPrefix(provider.Normalize(ingress.Namespace+"-"+ingress.Name+"-"+rule.Host), "-")
@ -449,13 +445,8 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
continue
}
portString := pa.Backend.Service.Port.Name
if len(pa.Backend.Service.Port.Name) == 0 {
portString = strconv.Itoa(int(pa.Backend.Service.Port.Number))
}
// TODO: if no service, do not add middlewares and 503.
serviceName := provider.Normalize(ingress.Namespace + "-" + ingress.Name + "-" + pa.Backend.Service.Name + "-" + portString)
serviceName := provider.Normalize(ingress.Namespace + "-" + ingress.Name + "-" + pa.Backend.Service.Name + "-" + portString(pa.Backend.Service.Port))
service, err := p.buildService(ingress.Namespace, pa.Backend, ingressConfig)
if err != nil {
@ -589,6 +580,24 @@ func (p *Provider) buildPassthroughService(namespace string, backend netv1.Ingre
return &dynamic.TCPService{LoadBalancer: lb}, nil
}
func getServicePort(service *corev1.Service, backend netv1.IngressBackend) (corev1.ServicePort, bool) {
for _, p := range service.Spec.Ports {
// A port with number 0 or an empty name is not allowed, this case is there for the default backend service.
if (backend.Service.Port.Number == 0 && backend.Service.Port.Name == "") ||
(backend.Service.Port.Number == p.Port || (backend.Service.Port.Name == p.Name && len(p.Name) > 0)) {
return p, true
}
}
// If the port is not found and the service is of type ExternalName, we return the port defined in the backend.
// If this is a named port, the port value will be 0 to be consistent with ingress-nginx.
if service.Spec.Type == corev1.ServiceTypeExternalName {
return corev1.ServicePort{TargetPort: intstr.Parse(portString(backend.Service.Port))}, true
}
return corev1.ServicePort{}, false
}
func (p *Provider) getBackendAddresses(namespace string, backend netv1.IngressBackend, cfg ingressConfig) ([]backendAddress, error) {
service, err := p.k8sClient.GetService(namespace, backend.Service.Name)
if err != nil {
@ -599,30 +608,18 @@ func (p *Provider) getBackendAddresses(namespace string, backend netv1.IngressBa
return nil, errors.New("externalName services not allowed")
}
var portName string
var portSpec corev1.ServicePort
var match bool
for _, p := range service.Spec.Ports {
// A port with number 0 or an empty name is not allowed, this case is there for the default backend service.
if (backend.Service.Port.Number == 0 && backend.Service.Port.Name == "") ||
(backend.Service.Port.Number == p.Port || (backend.Service.Port.Name == p.Name && len(p.Name) > 0)) {
portName = p.Name
portSpec = p
match = true
break
}
}
servicePort, match := getServicePort(service, backend)
if !match {
return nil, errors.New("service port not found")
}
if service.Spec.Type == corev1.ServiceTypeExternalName {
return []backendAddress{{Address: net.JoinHostPort(service.Spec.ExternalName, strconv.Itoa(int(portSpec.Port)))}}, nil
return []backendAddress{{Address: net.JoinHostPort(service.Spec.ExternalName, strconv.Itoa(servicePort.TargetPort.IntValue()))}}, nil
}
// When service upstream is set to true we return the service ClusterIP as the backend address.
if ptr.Deref(cfg.ServiceUpstream, false) {
return []backendAddress{{Address: net.JoinHostPort(service.Spec.ClusterIP, strconv.Itoa(int(portSpec.Port)))}}, nil
return []backendAddress{{Address: net.JoinHostPort(service.Spec.ClusterIP, strconv.Itoa(int(servicePort.Port)))}}, nil
}
endpointSlices, err := p.k8sClient.GetEndpointSlicesForService(namespace, backend.Service.Name)
@ -635,7 +632,7 @@ func (p *Provider) getBackendAddresses(namespace string, backend netv1.IngressBa
for _, endpointSlice := range endpointSlices {
var port int32
for _, p := range endpointSlice.Ports {
if portName == *p.Name {
if servicePort.Name == *p.Name {
port = *p.Port
break
}
@ -1140,3 +1137,10 @@ func throttleEvents(ctx context.Context, throttleDuration time.Duration, pool *s
return eventsChanBuffered
}
func portString(port netv1.ServiceBackendPort) string {
if port.Name == "" {
return strconv.Itoa(int(port.Number))
}
return port.Name
}

View File

@ -703,6 +703,105 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "External name service",
paths: []string{
"services.yml",
"ingressclasses.yml",
"ingresses/ingress-with-external-name.yml",
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-ingress-with-external-name-rule-0-path-0": {
Rule: `Host("external.localhost") && Path("/external-with-matching-port")`,
RuleSyntax: "default",
Service: "default-ingress-with-external-name-external-80",
},
"default-ingress-with-external-name-rule-0-path-1": {
Rule: `Host("external.localhost") && Path("/external-with-matching-named-port")`,
RuleSyntax: "default",
Service: "default-ingress-with-external-name-external-http",
},
"default-ingress-with-external-name-rule-0-path-2": {
Rule: `Host("external.localhost") && Path("/external-with-non-matching-port")`,
RuleSyntax: "default",
Service: "default-ingress-with-external-name-external-3000",
},
"default-ingress-with-external-name-rule-0-path-3": {
Rule: `Host("external.localhost") && Path("/external-with-non-matching-named-port")`,
RuleSyntax: "default",
Service: "default-ingress-with-external-name-external-foo",
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"default-ingress-with-external-name-external-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://external.com:8080",
},
},
Strategy: "wrr",
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
"default-ingress-with-external-name-external-3000": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://external.com:3000",
},
},
Strategy: "wrr",
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
"default-ingress-with-external-name-external-http": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://external.com:8080",
},
},
Strategy: "wrr",
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
"default-ingress-with-external-name-external-foo": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://external.com:0",
},
},
Strategy: "wrr",
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
}
for _, test := range testCases {