mirror of
https://github.com/traefik/traefik.git
synced 2026-06-17 19:09:29 +03:00
630 lines
20 KiB
Go
630 lines
20 KiB
Go
package ingressnginx
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"maps"
|
|
"math"
|
|
"net/http"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
|
"github.com/traefik/traefik/v3/pkg/provider"
|
|
"github.com/traefik/traefik/v3/pkg/tls"
|
|
"github.com/traefik/traefik/v3/pkg/types"
|
|
netv1 "k8s.io/api/networking/v1"
|
|
"k8s.io/utils/ptr"
|
|
)
|
|
|
|
// translate converts a model produced by Phase 1 into a Traefik dynamic.Configuration.
|
|
func (p *Provider) translate(ctx context.Context, mc *model) *dynamic.Configuration {
|
|
conf := &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),
|
|
},
|
|
TLS: &dynamic.TLSConfiguration{},
|
|
}
|
|
|
|
lb := &dynamic.ServersLoadBalancer{}
|
|
lb.SetDefaults()
|
|
conf.HTTP.Services[unavailableServiceName] = &dynamic.Service{LoadBalancer: lb}
|
|
|
|
for _, k := range slices.Sorted(maps.Keys(mc.Certs)) {
|
|
cp := mc.Certs[k]
|
|
conf.TLS.Certificates = append(conf.TLS.Certificates, &tls.CertAndStores{
|
|
Certificate: tls.Certificate{
|
|
CertFile: types.FileOrContent(cp.Cert),
|
|
KeyFile: types.FileOrContent(cp.Key),
|
|
},
|
|
})
|
|
}
|
|
|
|
if mc.DefaultBackend != nil {
|
|
obs := &dynamic.RouterObservabilityConfig{
|
|
Metadata: &dynamic.ObservabilityMetadata{
|
|
Ingress: &dynamic.KubernetesIngressMetadata{
|
|
Namespace: mc.DefaultBackend.Namespace,
|
|
ServiceName: mc.DefaultBackend.ServiceName,
|
|
},
|
|
},
|
|
}
|
|
|
|
var serversTransportName string
|
|
if loc := mc.DefaultBackendLocation; loc != nil {
|
|
obs.Metadata.Ingress.IngressName = loc.IngressName
|
|
obs.Metadata.Ingress.ServicePort = loc.ServicePort
|
|
if loc.ServersTransport != nil && loc.ServersTransportName != "" {
|
|
serversTransportName = loc.ServersTransportName
|
|
conf.HTTP.ServersTransports[loc.ServersTransportName] = loc.ServersTransport
|
|
}
|
|
}
|
|
|
|
conf.HTTP.Services[defaultBackendName] = buildService(mc.DefaultBackend, serversTransportName)
|
|
|
|
rt := &dynamic.Router{
|
|
EntryPoints: p.NonTLSEntryPoints,
|
|
Rule: `PathPrefix("/")`,
|
|
RuleSyntax: "default",
|
|
Priority: math.MinInt32,
|
|
Service: defaultBackendName,
|
|
Observability: obs,
|
|
}
|
|
rtTLS := &dynamic.Router{
|
|
EntryPoints: p.TLSEntryPoints,
|
|
Rule: `PathPrefix("/")`,
|
|
RuleSyntax: "default",
|
|
Priority: math.MinInt32,
|
|
Service: defaultBackendName,
|
|
TLS: &dynamic.RouterTLSConfig{},
|
|
Observability: obs,
|
|
}
|
|
|
|
// Apply the full middleware stack from the ingress location annotations to
|
|
// both catch-all routers, so options like enable-cors, custom-headers,
|
|
// rate-limits, redirects, etc. configured on a "spec.defaultBackend only"
|
|
// ingress reach its catch-all routers.
|
|
if loc := mc.DefaultBackendLocation; loc != nil {
|
|
p.applyMiddlewares(mc, loc, defaultBackendName, rt, conf)
|
|
p.applyMiddlewares(mc, loc, defaultBackendTLSName, rtTLS, conf)
|
|
}
|
|
|
|
conf.HTTP.Routers[defaultBackendName] = rt
|
|
conf.HTTP.Routers[defaultBackendTLSName] = rtTLS
|
|
}
|
|
|
|
for _, pt := range mc.PassthroughBackends {
|
|
backend, ok := mc.Backends[pt.BackendName]
|
|
if !ok {
|
|
log.Ctx(ctx).Error().Msgf("Passthrough backend %q not found in metamodel", pt.BackendName)
|
|
continue
|
|
}
|
|
|
|
lb := &dynamic.TCPServersLoadBalancer{}
|
|
for _, ep := range backend.Endpoints {
|
|
lb.Servers = append(lb.Servers, dynamic.TCPServer{Address: ep.Address})
|
|
}
|
|
|
|
conf.TCP.Services[pt.BackendName] = &dynamic.TCPService{LoadBalancer: lb}
|
|
conf.TCP.Routers[pt.RouterKey] = &dynamic.TCPRouter{
|
|
EntryPoints: p.TLSEntryPoints,
|
|
Rule: fmt.Sprintf("HostSNI(%q)", pt.Hostname),
|
|
RuleSyntax: "default",
|
|
Service: pt.BackendName,
|
|
TLS: &dynamic.RouterTCPTLSConfig{Passthrough: true},
|
|
}
|
|
}
|
|
|
|
for _, srv := range mc.Servers {
|
|
for _, loc := range srv.Locations {
|
|
if loc.ServersTransport != nil && loc.ServersTransportName != "" {
|
|
if _, exists := conf.HTTP.ServersTransports[loc.ServersTransportName]; !exists {
|
|
conf.HTTP.ServersTransports[loc.ServersTransportName] = loc.ServersTransport
|
|
}
|
|
}
|
|
|
|
if loc.TLSOption != nil && loc.TLSOptionName != "" {
|
|
if conf.TLS.Options == nil {
|
|
conf.TLS.Options = make(map[string]tls.Options)
|
|
}
|
|
if _, exists := conf.TLS.Options[loc.TLSOptionName]; !exists {
|
|
conf.TLS.Options[loc.TLSOptionName] = *loc.TLSOption
|
|
}
|
|
}
|
|
|
|
backend, ok := mc.Backends[loc.BackendName]
|
|
if !ok {
|
|
log.Ctx(ctx).Error().Msgf("Backend %q not found for location %s%s", loc.BackendName, srv.Hostname, loc.Path)
|
|
continue
|
|
}
|
|
|
|
primarySvcName := loc.BackendName
|
|
conf.HTTP.Services[primarySvcName] = buildServiceWithLocConfig(backend, loc.ServersTransportName, loc.Config)
|
|
|
|
obs := &dynamic.RouterObservabilityConfig{
|
|
Metadata: &dynamic.ObservabilityMetadata{
|
|
Ingress: &dynamic.KubernetesIngressMetadata{
|
|
Namespace: loc.Namespace,
|
|
IngressName: loc.IngressName,
|
|
ServiceName: loc.ServiceName,
|
|
ServicePort: loc.ServicePort,
|
|
},
|
|
},
|
|
}
|
|
|
|
routerSvcName := primarySvcName
|
|
canarySvcName := primarySvcName + "-canary"
|
|
if loc.Canary != nil {
|
|
canaryBackend, ok := mc.Backends[loc.Canary.BackendName]
|
|
if ok {
|
|
canaryWRRName := primarySvcName + "-wrr"
|
|
|
|
canarySvc := buildServiceWithLocConfig(canaryBackend, loc.ServersTransportName, loc.Config)
|
|
conf.HTTP.Services[canarySvcName] = canarySvc
|
|
|
|
conf.HTTP.Services[canaryWRRName] = &dynamic.Service{
|
|
Weighted: &dynamic.WeightedRoundRobin{
|
|
Sticky: buildSticky(loc.Config, "wrr"),
|
|
Services: []dynamic.WRRService{
|
|
{Name: primarySvcName, Weight: ptr.To(loc.Canary.WeightTotal - loc.Canary.Weight)},
|
|
{Name: canarySvcName, Weight: ptr.To(loc.Canary.Weight)},
|
|
},
|
|
},
|
|
}
|
|
routerSvcName = canaryWRRName
|
|
}
|
|
}
|
|
|
|
rule := buildRule(srv.Hostname, loc)
|
|
|
|
var routerKey string
|
|
if loc.IsIngressDefaultBackend {
|
|
routerKey = provider.Normalize(fmt.Sprintf("%s-%s-default-backend", loc.Namespace, loc.IngressName))
|
|
} else {
|
|
routerKey = provider.Normalize(fmt.Sprintf("%s-%s-rule-%d-path-%d", loc.Namespace, loc.IngressName, loc.RuleIndex, loc.LocationIndex))
|
|
}
|
|
|
|
rt := &dynamic.Router{
|
|
EntryPoints: p.NonTLSEntryPoints,
|
|
Rule: rule,
|
|
RuleSyntax: "default",
|
|
Service: routerSvcName,
|
|
Observability: obs,
|
|
}
|
|
|
|
rtTLS := &dynamic.Router{
|
|
EntryPoints: p.TLSEntryPoints,
|
|
Rule: rule,
|
|
RuleSyntax: "default",
|
|
Service: routerSvcName,
|
|
TLS: &dynamic.RouterTLSConfig{
|
|
Options: loc.TLSOptionName,
|
|
},
|
|
Observability: obs,
|
|
}
|
|
|
|
// TODO: in case we want to add the unavailable service only when it is used this should be done here.
|
|
if loc.Error {
|
|
rt.Service = unavailableServiceName
|
|
rtTLS.Service = unavailableServiceName
|
|
}
|
|
|
|
conf.HTTP.Routers[routerKey] = rt
|
|
conf.HTTP.Routers[routerKey+"-tls"] = rtTLS
|
|
|
|
if !loc.Error {
|
|
p.applyMiddlewares(mc, loc, routerKey, rt, conf)
|
|
p.applyMiddlewares(mc, loc, routerKey+"-tls", rtTLS, conf)
|
|
applyFromToWwwRedirect(loc, routerKey, rt, obs, conf)
|
|
applyFromToWwwRedirect(loc, routerKey+"-tls", rtTLS, obs, conf)
|
|
}
|
|
|
|
if loc.Canary != nil && loc.Canary.RequiresCanaryRouter() {
|
|
canaryKey := routerKey + "-canary"
|
|
canaryRouter := &dynamic.Router{
|
|
EntryPoints: rt.EntryPoints,
|
|
Rule: appendCanaryRule(rule, loc.Canary),
|
|
RuleSyntax: rt.RuleSyntax,
|
|
Service: canarySvcName,
|
|
Observability: obs,
|
|
}
|
|
conf.HTTP.Routers[canaryKey] = canaryRouter
|
|
p.applyMiddlewares(mc, loc, canaryKey, canaryRouter, conf)
|
|
|
|
canaryKeyTLS := canaryKey + "-tls"
|
|
canaryRouterTLS := &dynamic.Router{
|
|
EntryPoints: rtTLS.EntryPoints,
|
|
Rule: appendCanaryRule(rule, loc.Canary),
|
|
RuleSyntax: rtTLS.RuleSyntax,
|
|
Service: canarySvcName,
|
|
TLS: rtTLS.TLS,
|
|
Observability: obs,
|
|
}
|
|
conf.HTTP.Routers[canaryKeyTLS] = canaryRouterTLS
|
|
p.applyMiddlewares(mc, loc, canaryKeyTLS, canaryRouterTLS, conf)
|
|
}
|
|
|
|
if loc.Canary != nil && loc.Canary.RequiresNonCanaryRouter() {
|
|
nonCanaryKey := routerKey + "-non-canary"
|
|
nonCanaryRouter := &dynamic.Router{
|
|
EntryPoints: rt.EntryPoints,
|
|
Rule: appendNonCanaryRule(rule, loc.Canary),
|
|
RuleSyntax: rt.RuleSyntax,
|
|
Service: primarySvcName,
|
|
Observability: obs,
|
|
}
|
|
conf.HTTP.Routers[nonCanaryKey] = nonCanaryRouter
|
|
p.applyMiddlewares(mc, loc, nonCanaryKey, nonCanaryRouter, conf)
|
|
|
|
nonCanaryKeyTLS := nonCanaryKey + "-tls"
|
|
nonCanaryRouterTLS := &dynamic.Router{
|
|
EntryPoints: rtTLS.EntryPoints,
|
|
Rule: appendNonCanaryRule(rule, loc.Canary),
|
|
RuleSyntax: rtTLS.RuleSyntax,
|
|
Service: primarySvcName,
|
|
TLS: rtTLS.TLS,
|
|
Observability: obs,
|
|
}
|
|
conf.HTTP.Routers[nonCanaryKeyTLS] = nonCanaryRouterTLS
|
|
p.applyMiddlewares(mc, loc, nonCanaryKeyTLS, nonCanaryRouterTLS, conf)
|
|
}
|
|
}
|
|
}
|
|
|
|
return conf
|
|
}
|
|
|
|
func buildService(backend *backend, serversTransportName string) *dynamic.Service {
|
|
lb := &dynamic.ServersLoadBalancer{}
|
|
lb.SetDefaults()
|
|
|
|
if serversTransportName != "" {
|
|
lb.ServersTransport = serversTransportName
|
|
}
|
|
|
|
svc := &dynamic.Service{LoadBalancer: lb}
|
|
for _, ep := range backend.Endpoints {
|
|
svc.LoadBalancer.Servers = append(svc.LoadBalancer.Servers, dynamic.Server{
|
|
URL: fmt.Sprintf("http://%s", ep.Address),
|
|
Fenced: ep.Fenced,
|
|
})
|
|
}
|
|
|
|
return svc
|
|
}
|
|
|
|
func buildServiceWithLocConfig(backend *backend, serversTransportName string, locCfg IngressConfig) *dynamic.Service {
|
|
lb := &dynamic.ServersLoadBalancer{}
|
|
lb.SetDefaults()
|
|
|
|
if serversTransportName != "" && len(backend.Endpoints) > 0 {
|
|
lb.ServersTransport = serversTransportName
|
|
}
|
|
|
|
lb.Sticky = buildSticky(locCfg, "")
|
|
|
|
if upstreamHashBy := ptr.Deref(locCfg.UpstreamHashBy, ""); upstreamHashBy != "" {
|
|
lb.Strategy = dynamic.BalancerStrategyHRW
|
|
lb.NginxUpstreamHashBy = upstreamHashBy
|
|
}
|
|
|
|
scheme := parseBackendProtocol(ptr.Deref(locCfg.BackendProtocol, "HTTP"))
|
|
svc := &dynamic.Service{LoadBalancer: lb}
|
|
for _, ep := range backend.Endpoints {
|
|
svc.LoadBalancer.Servers = append(svc.LoadBalancer.Servers, dynamic.Server{
|
|
URL: fmt.Sprintf("%s://%s", scheme, ep.Address),
|
|
Fenced: ep.Fenced,
|
|
})
|
|
}
|
|
|
|
return svc
|
|
}
|
|
|
|
func appendCanaryRule(rule string, c *canaryConfig) string {
|
|
var rules []string
|
|
if c.Header != "" {
|
|
switch {
|
|
case c.HeaderValue == "" && c.HeaderPattern != "":
|
|
rules = append(rules, fmt.Sprintf("HeaderRegexp(%q, %q)", c.Header, c.HeaderPattern))
|
|
case c.HeaderValue != "":
|
|
rules = append(rules, fmt.Sprintf("Header(%q, %q)", c.Header, c.HeaderValue))
|
|
default:
|
|
rules = append(rules, fmt.Sprintf(`Header(%q, "always")`, c.Header))
|
|
}
|
|
}
|
|
|
|
if c.Cookie != "" {
|
|
cookieRule := fmt.Sprintf(`HeaderRegexp("Cookie", %q)`,
|
|
fmt.Sprintf(`(^|;\s*)%s=always(;|$)`, regexp.QuoteMeta(c.Cookie)))
|
|
if c.Header != "" && c.HeaderValue == "" && c.HeaderPattern == "" {
|
|
cookieRule = fmt.Sprintf("(%s && !%s)", cookieRule, fmt.Sprintf(`Header(%q, "never")`, c.Header))
|
|
}
|
|
rules = append(rules, cookieRule)
|
|
}
|
|
|
|
return fmt.Sprintf("(%s) && (%s)", rule, strings.Join(rules, " || "))
|
|
}
|
|
|
|
func appendNonCanaryRule(rule string, c *canaryConfig) string {
|
|
var rules []string
|
|
if c.Header != "" && c.HeaderValue == "" && c.HeaderPattern == "" {
|
|
rules = append(rules, fmt.Sprintf(`Header(%q, "never")`, c.Header))
|
|
}
|
|
if c.Cookie != "" {
|
|
rules = append(rules, fmt.Sprintf(`HeaderRegexp("Cookie", %q)`,
|
|
fmt.Sprintf(`(^|;\s*)%s=never(;|$)`, regexp.QuoteMeta(c.Cookie))))
|
|
}
|
|
return fmt.Sprintf("(%s) && (%s)", rule, strings.Join(rules, " || "))
|
|
}
|
|
|
|
// buildSticky returns a Sticky model if the affinity model is set to "cookie" and nil otherwise.
|
|
// It also appends the given nameSuffix to the cookie name if not empty.
|
|
func buildSticky(cfg IngressConfig, nameSuffix string) *dynamic.Sticky {
|
|
if ptr.Deref(cfg.Affinity, "") != "cookie" {
|
|
return nil
|
|
}
|
|
|
|
name := ptr.Deref(cfg.SessionCookieName, "INGRESSCOOKIE")
|
|
if nameSuffix != "" {
|
|
name += "-" + nameSuffix
|
|
}
|
|
|
|
return &dynamic.Sticky{
|
|
Cookie: &dynamic.Cookie{
|
|
Name: name,
|
|
Secure: ptr.Deref(cfg.SessionCookieSecure, false),
|
|
HTTPOnly: true,
|
|
SameSite: strings.ToLower(ptr.Deref(cfg.SessionCookieSameSite, "")),
|
|
MaxAge: ptr.Deref(cfg.SessionCookieMaxAge, 0),
|
|
Expires: ptr.Deref(cfg.SessionCookieExpires, 0),
|
|
Path: ptr.To(ptr.Deref(cfg.SessionCookiePath, "/")),
|
|
Domain: ptr.Deref(cfg.SessionCookieDomain, ""),
|
|
},
|
|
}
|
|
}
|
|
|
|
func (p *Provider) applyMiddlewares(mc *model, loc *location, routerKey string, rt *dynamic.Router, conf *dynamic.Configuration) {
|
|
if loc.SSLRedirectOnly && rt.TLS == nil {
|
|
name := routerKey + "-redirect-scheme"
|
|
conf.HTTP.Middlewares[name] = &dynamic.Middleware{
|
|
RedirectScheme: &dynamic.RedirectScheme{Scheme: "https", ForcePermanentRedirect: true},
|
|
}
|
|
rt.Middlewares = []string{name}
|
|
rt.Service = "noop@internal"
|
|
return
|
|
}
|
|
|
|
if loc.AccessLog != nil {
|
|
if rt.Observability == nil {
|
|
rt.Observability = &dynamic.RouterObservabilityConfig{}
|
|
}
|
|
rt.Observability.AccessLogs = ptr.To(*loc.AccessLog)
|
|
}
|
|
|
|
if loc.CustomHTTPErrors != nil {
|
|
e := loc.CustomHTTPErrors
|
|
errorSvcName := e.ErrorServiceName
|
|
if e.ErrorBackendName != "" {
|
|
errorSvcName = "default-backend-" + routerKey
|
|
if errBackend, ok := mc.Backends[e.ErrorBackendName]; ok {
|
|
conf.HTTP.Services[errorSvcName] = buildServiceWithLocConfig(errBackend, "", loc.Config)
|
|
}
|
|
}
|
|
headers := http.Header{
|
|
"X-Namespaces": {e.Namespace},
|
|
"X-Ingress-Name": {e.IngressName},
|
|
"X-Service-Name": {e.ServiceName},
|
|
"X-Service-Port": {e.ServicePort},
|
|
}
|
|
name := routerKey + "-custom-http-errors"
|
|
conf.HTTP.Middlewares[name] = &dynamic.Middleware{
|
|
Errors: &dynamic.ErrorPage{
|
|
Status: e.Status,
|
|
Service: errorSvcName,
|
|
NginxHeaders: &headers,
|
|
},
|
|
}
|
|
rt.Middlewares = append(rt.Middlewares, name)
|
|
}
|
|
|
|
if loc.AppRoot != nil {
|
|
name := routerKey + "-app-root"
|
|
conf.HTTP.Middlewares[name] = &dynamic.Middleware{
|
|
RedirectRegex: &dynamic.RedirectRegex{
|
|
Regex: `^(https?://[^/]+)/(\?.*)?$`,
|
|
Replacement: "$1" + *loc.AppRoot,
|
|
},
|
|
}
|
|
rt.Middlewares = append(rt.Middlewares, name)
|
|
}
|
|
|
|
if loc.Redirect != nil {
|
|
name := routerKey + "-redirect"
|
|
conf.HTTP.Middlewares[name] = &dynamic.Middleware{RedirectRegex: loc.Redirect}
|
|
rt.Middlewares = append(rt.Middlewares, name)
|
|
}
|
|
|
|
if loc.BasicAuth != nil {
|
|
name := routerKey + "-basic-auth"
|
|
conf.HTTP.Middlewares[name] = &dynamic.Middleware{BasicAuth: loc.BasicAuth}
|
|
rt.Middlewares = append(rt.Middlewares, name)
|
|
}
|
|
|
|
if loc.DigestAuth != nil {
|
|
name := routerKey + "-digest-auth"
|
|
conf.HTTP.Middlewares[name] = &dynamic.Middleware{DigestAuth: loc.DigestAuth}
|
|
rt.Middlewares = append(rt.Middlewares, name)
|
|
}
|
|
|
|
if loc.Buffering != nil {
|
|
name := routerKey + "-buffering"
|
|
conf.HTTP.Middlewares[name] = &dynamic.Middleware{Buffering: loc.Buffering}
|
|
rt.Middlewares = append(rt.Middlewares, name)
|
|
}
|
|
|
|
if loc.IPAllowList != nil {
|
|
name := routerKey + "-allowed-source-range"
|
|
conf.HTTP.Middlewares[name] = &dynamic.Middleware{IPAllowList: loc.IPAllowList}
|
|
rt.Middlewares = append(rt.Middlewares, name)
|
|
}
|
|
|
|
if loc.CORS != nil {
|
|
name := routerKey + "-cors"
|
|
conf.HTTP.Middlewares[name] = &dynamic.Middleware{Headers: loc.CORS}
|
|
rt.Middlewares = append(rt.Middlewares, name)
|
|
}
|
|
|
|
if loc.RewriteTarget != nil {
|
|
name := routerKey + "-rewrite-target"
|
|
conf.HTTP.Middlewares[name] = &dynamic.Middleware{RewriteTarget: loc.RewriteTarget}
|
|
rt.Middlewares = append(rt.Middlewares, name)
|
|
}
|
|
|
|
if loc.UpstreamVhost != nil {
|
|
name := routerKey + "-vhost"
|
|
conf.HTTP.Middlewares[name] = &dynamic.Middleware{UpstreamVHost: loc.UpstreamVhost}
|
|
rt.Middlewares = append(rt.Middlewares, name)
|
|
}
|
|
|
|
if loc.RateLimitRPM != nil {
|
|
name := routerKey + "-limit-rpm"
|
|
conf.HTTP.Middlewares[name] = &dynamic.Middleware{RateLimit: loc.RateLimitRPM}
|
|
rt.Middlewares = append(rt.Middlewares, name)
|
|
}
|
|
if loc.RateLimitRPS != nil {
|
|
name := routerKey + "-limit-rps"
|
|
conf.HTTP.Middlewares[name] = &dynamic.Middleware{RateLimit: loc.RateLimitRPS}
|
|
rt.Middlewares = append(rt.Middlewares, name)
|
|
}
|
|
|
|
if loc.LimitConnections != nil {
|
|
name := routerKey + "-limit-connections"
|
|
conf.HTTP.Middlewares[name] = &dynamic.Middleware{InFlightReq: loc.LimitConnections}
|
|
rt.Middlewares = append(rt.Middlewares, name)
|
|
}
|
|
|
|
if loc.AuthTLSPassCert != nil && rt.TLS != nil {
|
|
name := routerKey + "-pass-certificate-to-upstream"
|
|
conf.HTTP.Middlewares[name] = &dynamic.Middleware{AuthTLSPassCertificateToUpstream: loc.AuthTLSPassCert}
|
|
rt.Middlewares = append(rt.Middlewares, name)
|
|
}
|
|
|
|
if len(loc.ResolvedCustomHeaders) > 0 {
|
|
name := routerKey + "-custom-headers"
|
|
conf.HTTP.Middlewares[name] = &dynamic.Middleware{
|
|
Headers: &dynamic.Headers{CustomResponseHeaders: loc.ResolvedCustomHeaders},
|
|
}
|
|
rt.Middlewares = append(rt.Middlewares, name)
|
|
}
|
|
|
|
if loc.SnippetAuth != nil {
|
|
name := routerKey + "-snippet"
|
|
conf.HTTP.Middlewares[name] = &dynamic.Middleware{Snippet: loc.SnippetAuth}
|
|
rt.Middlewares = append(rt.Middlewares, name)
|
|
}
|
|
|
|
if loc.Retry != nil {
|
|
name := routerKey + "-retry"
|
|
conf.HTTP.Middlewares[name] = &dynamic.Middleware{Retry: loc.Retry}
|
|
rt.Middlewares = append(rt.Middlewares, name)
|
|
}
|
|
|
|
if p.applyMiddlewareFunc != nil {
|
|
if err := p.applyMiddlewareFunc(routerKey, rt, conf, loc.Config); err != nil {
|
|
log.Error().Err(err).Str("router", routerKey).Msg("Error in ApplyMiddlewareFunc")
|
|
}
|
|
} else if ptr.Deref(loc.Config.EnableModSecurity, false) ||
|
|
ptr.Deref(loc.Config.EnableOWASPCoreRules, false) ||
|
|
ptr.Deref(loc.Config.ModSecuritySnippet, "") != "" ||
|
|
ptr.Deref(loc.Config.ModSecurityTransactionID, "") != "" {
|
|
log.Error().Str("router", routerKey).Msg("mod-security annotations require ApplyMiddlewareFunc to be set")
|
|
}
|
|
}
|
|
|
|
func applyFromToWwwRedirect(loc *location, routerKey string, rt *dynamic.Router, obs *dynamic.RouterObservabilityConfig, conf *dynamic.Configuration) {
|
|
if loc.FromToWwwRedirect == nil {
|
|
return
|
|
}
|
|
|
|
f := loc.FromToWwwRedirect
|
|
mwName := routerKey + "-from-to-www-redirect"
|
|
conf.HTTP.Middlewares[mwName] = &dynamic.Middleware{
|
|
RedirectRegex: &dynamic.RedirectRegex{
|
|
Regex: `(https?)://[^/:]+(:[0-9]+)?/(.*)`,
|
|
Replacement: fmt.Sprintf("$1://%s$2/$3", f.TargetHostname),
|
|
StatusCode: ptr.To(http.StatusPermanentRedirect),
|
|
},
|
|
}
|
|
|
|
conf.HTTP.Routers[routerKey+"-from-to-www-redirect"] = &dynamic.Router{
|
|
EntryPoints: rt.EntryPoints,
|
|
Rule: f.ExtraRouterRule,
|
|
Priority: rt.Priority,
|
|
RuleSyntax: "default",
|
|
Middlewares: []string{mwName},
|
|
Service: rt.Service,
|
|
TLS: rt.TLS,
|
|
Observability: obs,
|
|
}
|
|
}
|
|
|
|
func buildRule(host string, loc *location) string {
|
|
var rules []string
|
|
|
|
if host != "" {
|
|
hosts := append([]string{host}, loc.Aliases...)
|
|
hostRules := make([]string, 0, len(hosts))
|
|
for _, h := range hosts {
|
|
hostRules = append(hostRules, fmt.Sprintf("Host(%q)", h))
|
|
}
|
|
if len(hostRules) > 1 {
|
|
rules = append(rules, "("+strings.Join(hostRules, " || ")+")")
|
|
} else {
|
|
rules = append(rules, hostRules[0])
|
|
}
|
|
}
|
|
|
|
if len(loc.Path) > 0 {
|
|
pathType := ptr.Deref(loc.PathType, netv1.PathTypePrefix)
|
|
if pathType == netv1.PathTypeImplementationSpecific {
|
|
pathType = netv1.PathTypePrefix
|
|
}
|
|
|
|
switch pathType {
|
|
case netv1.PathTypeExact:
|
|
rules = append(rules, fmt.Sprintf("Path(%q)", loc.Path))
|
|
case netv1.PathTypePrefix:
|
|
if loc.UseRegex {
|
|
rules = append(rules, fmt.Sprintf("PathRegexp(%q)", "(?i)^"+loc.Path))
|
|
} else {
|
|
rules = append(rules, buildPrefixRule(loc.Path))
|
|
}
|
|
}
|
|
}
|
|
|
|
return strings.Join(rules, " && ")
|
|
}
|
|
|
|
// buildPrefixRule is a helper function to build a path prefix rule that matches path prefix split by `/`.
|
|
// For example, the paths `/abc`, `/abc/`, and `/abc/def` would all match the prefix `/abc`,
|
|
// but the path `/abcd` would not. See TestStrictPrefixMatchingRule() for more examples.
|
|
//
|
|
// "PathPrefix" in Kubernetes Gateway API is semantically equivalent to the "Prefix" path type in the
|
|
// Kubernetes Ingress API.
|
|
func buildPrefixRule(path string) string {
|
|
if path == "/" {
|
|
return `PathPrefix("/")`
|
|
}
|
|
path = strings.TrimSuffix(path, "/")
|
|
return fmt.Sprintf("(Path(%q) || PathPrefix(%q))", path, path+"/")
|
|
}
|