Handle duplicate server-alias on ingress-nginx provider

This commit is contained in:
Gina A. 2026-04-22 15:42:05 +07:00 committed by GitHub
parent a6141798f2
commit 42e69bcd67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 263 additions and 12 deletions

View File

@ -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

View File

@ -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

View File

@ -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{