mirror of
https://github.com/traefik/traefik.git
synced 2026-05-04 20:06:21 +02:00
Handle duplicate server-alias on ingress-nginx provider
This commit is contained in:
parent
a6141798f2
commit
42e69bcd67
@ -0,0 +1,43 @@
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: first-ingress
|
||||
namespace: default
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/server-alias: "shared.localhost"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: first.localhost
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: whoami
|
||||
port:
|
||||
number: 80
|
||||
path: /
|
||||
pathType: Prefix
|
||||
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: second-ingress
|
||||
namespace: default
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/server-alias: "shared.localhost"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: second.localhost
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: whoami
|
||||
port:
|
||||
number: 80
|
||||
path: /
|
||||
pathType: Prefix
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"os"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -479,11 +480,36 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
|
||||
canaryIngresses []ingress
|
||||
)
|
||||
|
||||
// Sort the ingresses by creation timestamps, to help to decide when two ingresses have the same server-alias value.
|
||||
listedIngresses := p.k8sClient.ListIngresses()
|
||||
sort.SliceStable(listedIngresses, func(a, b int) bool {
|
||||
ta, tb := listedIngresses[a].CreationTimestamp, listedIngresses[b].CreationTimestamp
|
||||
|
||||
// When the timestamp is exactly the same, fallback to descending namespace/name lexicographic order.
|
||||
// The same fallback and debug log with ingress-nginx.
|
||||
// Ref: https://github.com/kubernetes/ingress-nginx/blob/main/internal/ingress/controller/store/store.go#L1102-L1115.
|
||||
if ta.Equal(&tb) {
|
||||
ia := listedIngresses[a].Namespace + "/" + listedIngresses[a].Name
|
||||
ib := listedIngresses[b].Namespace + "/" + listedIngresses[b].Name
|
||||
log.Ctx(ctx).Debug().
|
||||
Str("ingress_a", ia).
|
||||
Str("ingress_b", ib).
|
||||
Msg("Ingresses have identical CreationTimestamp, falling back to descending namespace/name lexicographic order")
|
||||
return ia > ib
|
||||
}
|
||||
|
||||
return ta.Before(&tb)
|
||||
})
|
||||
|
||||
hosts := make(map[string]bool)
|
||||
hostsWithUseRegex := make(map[string]bool)
|
||||
serverSnippets := make(map[string]string)
|
||||
ingressPaths := make(map[string]ingressPath) // indexed by namespace+host+path+pathType.
|
||||
for _, ing := range p.k8sClient.ListIngresses() {
|
||||
// Build a map of claimed server-aliases: the first ingress (by creation time) to
|
||||
// declare an alias owns it; any later ingress that repeats the same alias is denied.
|
||||
claimedAliases := make(map[string]string)
|
||||
|
||||
for _, ing := range listedIngresses {
|
||||
if !p.shouldProcessIngress(ing, ingressClasses) {
|
||||
continue
|
||||
}
|
||||
@ -544,6 +570,13 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
for _, alias := range ptr.Deref(i.IngressConfig.ServerAlias, nil) {
|
||||
serverAlias := strings.ToLower(alias)
|
||||
if _, exist := claimedAliases[serverAlias]; !exist {
|
||||
claimedAliases[serverAlias] = i.Namespace + "/" + i.Name
|
||||
}
|
||||
}
|
||||
|
||||
ingresses = append(ingresses, i)
|
||||
}
|
||||
|
||||
@ -836,7 +869,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
|
||||
|
||||
rt := &dynamic.Router{
|
||||
EntryPoints: p.NonTLSEntryPoints,
|
||||
Rule: buildRule(ctxIngress, rule.Host, pa, ingress.IngressConfig, hosts, hostsWithUseRegex),
|
||||
Rule: buildRule(ctxIngress, ingress, rule.Host, pa, hosts, hostsWithUseRegex, claimedAliases),
|
||||
// "default" stands for the default rule syntax in Traefik v3, i.e. the v3 syntax.
|
||||
RuleSyntax: "default",
|
||||
Service: serviceName,
|
||||
@ -2267,20 +2300,27 @@ func basicAuthUsers(secret *corev1.Secret, authSecretType string) (dynamic.Users
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func buildRule(ctx context.Context, host string, pa netv1.HTTPIngressPath, config IngressConfig, allHosts map[string]bool, hostsWithUseRegex map[string]bool) string {
|
||||
func buildRule(ctx context.Context, ingress ingress, host string, pa netv1.HTTPIngressPath, allHosts map[string]bool, hostsWithUseRegex map[string]bool, claimedAliases map[string]string) string {
|
||||
var rules []string
|
||||
if host != "" {
|
||||
hosts := []string{host}
|
||||
if config.ServerAlias != nil {
|
||||
for _, alias := range *config.ServerAlias {
|
||||
if _, ok := allHosts[strings.ToLower(alias)]; ok {
|
||||
log.Ctx(ctx).Debug().
|
||||
Str("alias", alias).
|
||||
Msg("Skipping server-alias because it is already defined as a host in another Ingress")
|
||||
continue
|
||||
}
|
||||
hosts = append(hosts, alias)
|
||||
for _, alias := range ptr.Deref(ingress.IngressConfig.ServerAlias, nil) {
|
||||
serverAlias := strings.ToLower(alias)
|
||||
if _, ok := allHosts[serverAlias]; ok {
|
||||
log.Ctx(ctx).Debug().
|
||||
Str("alias", alias).
|
||||
Msg("Skipping server-alias because it is already defined as a host in another Ingress")
|
||||
continue
|
||||
}
|
||||
ingressKey := ingress.Namespace + "/" + ingress.Name
|
||||
if owner, ok := claimedAliases[serverAlias]; ok && owner != ingressKey {
|
||||
log.Ctx(ctx).Debug().
|
||||
Str("alias", alias).
|
||||
Str("ingress", ingressKey).
|
||||
Msgf("Skipping server-alias because it is already claimed by %s Ingress", owner)
|
||||
continue
|
||||
}
|
||||
hosts = append(hosts, alias)
|
||||
}
|
||||
|
||||
var hostRules []string
|
||||
|
||||
@ -10824,6 +10824,174 @@ func TestLoadIngresses(t *testing.T) {
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Server Alias with Alias-Alias Conflict",
|
||||
paths: []string{
|
||||
"services.yml",
|
||||
"ingressclasses.yml",
|
||||
"ingresses/ingress-with-server-alias-alias-conflict.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-first-ingress-rule-0-path-0": {
|
||||
EntryPoints: []string{"http"},
|
||||
Rule: `Host("first.localhost") && PathPrefix("/")`,
|
||||
RuleSyntax: "default",
|
||||
Middlewares: []string{"default-first-ingress-rule-0-path-0-retry"},
|
||||
Service: "default-first-ingress-whoami-80",
|
||||
Observability: &dynamic.RouterObservabilityConfig{
|
||||
Metadata: &dynamic.ObservabilityMetadata{
|
||||
Ingress: &dynamic.KubernetesIngressMetadata{
|
||||
Namespace: "default",
|
||||
IngressName: "first-ingress",
|
||||
ServiceName: "whoami",
|
||||
ServicePort: "80",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"default-second-ingress-rule-0-path-0": {
|
||||
EntryPoints: []string{"http"},
|
||||
Rule: `(Host("second.localhost") || Host("shared.localhost")) && PathPrefix("/")`,
|
||||
RuleSyntax: "default",
|
||||
Middlewares: []string{"default-second-ingress-rule-0-path-0-retry"},
|
||||
Service: "default-second-ingress-whoami-80",
|
||||
Observability: &dynamic.RouterObservabilityConfig{
|
||||
Metadata: &dynamic.ObservabilityMetadata{
|
||||
Ingress: &dynamic.KubernetesIngressMetadata{
|
||||
Namespace: "default",
|
||||
IngressName: "second-ingress",
|
||||
ServiceName: "whoami",
|
||||
ServicePort: "80",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"default-first-ingress-rule-0-path-0-tls": {
|
||||
EntryPoints: []string{"https"},
|
||||
Rule: `Host("first.localhost") && PathPrefix("/")`,
|
||||
RuleSyntax: "default",
|
||||
Middlewares: []string{"default-first-ingress-rule-0-path-0-tls-retry"},
|
||||
Service: "default-first-ingress-whoami-80",
|
||||
Observability: &dynamic.RouterObservabilityConfig{
|
||||
Metadata: &dynamic.ObservabilityMetadata{
|
||||
Ingress: &dynamic.KubernetesIngressMetadata{
|
||||
Namespace: "default",
|
||||
IngressName: "first-ingress",
|
||||
ServiceName: "whoami",
|
||||
ServicePort: "80",
|
||||
},
|
||||
},
|
||||
},
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
},
|
||||
"default-second-ingress-rule-0-path-0-tls": {
|
||||
EntryPoints: []string{"https"},
|
||||
Rule: `(Host("second.localhost") || Host("shared.localhost")) && PathPrefix("/")`,
|
||||
RuleSyntax: "default",
|
||||
Middlewares: []string{"default-second-ingress-rule-0-path-0-tls-retry"},
|
||||
Service: "default-second-ingress-whoami-80",
|
||||
Observability: &dynamic.RouterObservabilityConfig{
|
||||
Metadata: &dynamic.ObservabilityMetadata{
|
||||
Ingress: &dynamic.KubernetesIngressMetadata{
|
||||
Namespace: "default",
|
||||
IngressName: "second-ingress",
|
||||
ServiceName: "whoami",
|
||||
ServicePort: "80",
|
||||
},
|
||||
},
|
||||
},
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{
|
||||
"default-first-ingress-rule-0-path-0-retry": {
|
||||
Retry: &dynamic.Retry{
|
||||
Attempts: 3,
|
||||
},
|
||||
},
|
||||
"default-first-ingress-rule-0-path-0-tls-retry": {
|
||||
Retry: &dynamic.Retry{
|
||||
Attempts: 3,
|
||||
},
|
||||
},
|
||||
"default-second-ingress-rule-0-path-0-retry": {
|
||||
Retry: &dynamic.Retry{
|
||||
Attempts: 3,
|
||||
},
|
||||
},
|
||||
"default-second-ingress-rule-0-path-0-tls-retry": {
|
||||
Retry: &dynamic.Retry{
|
||||
Attempts: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"unavailable-service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Strategy: "wrr",
|
||||
PassHostHeader: ptr.To(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: dynamic.DefaultFlushInterval,
|
||||
},
|
||||
},
|
||||
},
|
||||
"default-first-ingress-whoami-80": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{
|
||||
{URL: "http://10.10.0.1:80"},
|
||||
{URL: "http://10.10.0.2:80"},
|
||||
},
|
||||
Strategy: "wrr",
|
||||
PassHostHeader: ptr.To(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: dynamic.DefaultFlushInterval,
|
||||
},
|
||||
ServersTransport: "default-first-ingress",
|
||||
},
|
||||
},
|
||||
"default-second-ingress-whoami-80": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{
|
||||
{URL: "http://10.10.0.1:80"},
|
||||
{URL: "http://10.10.0.2:80"},
|
||||
},
|
||||
Strategy: "wrr",
|
||||
PassHostHeader: ptr.To(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: dynamic.DefaultFlushInterval,
|
||||
},
|
||||
ServersTransport: "default-second-ingress",
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{
|
||||
"default-first-ingress": {
|
||||
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
|
||||
DialTimeout: ptypes.Duration(60 * time.Second),
|
||||
ReadTimeout: ptypes.Duration(60 * time.Second),
|
||||
WriteTimeout: ptypes.Duration(60 * time.Second),
|
||||
IdleConnTimeout: ptypes.Duration(60 * time.Second),
|
||||
},
|
||||
},
|
||||
"default-second-ingress": {
|
||||
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
|
||||
DialTimeout: ptypes.Duration(60 * time.Second),
|
||||
ReadTimeout: ptypes.Duration(60 * time.Second),
|
||||
WriteTimeout: ptypes.Duration(60 * time.Second),
|
||||
IdleConnTimeout: ptypes.Duration(60 * time.Second),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Proxy HTTP version 1.1",
|
||||
paths: []string{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user