mirror of
https://github.com/traefik/traefik
synced 2026-02-03 11:10:33 +00:00
Merge current v3.6 into master
This commit is contained in:
@@ -79,7 +79,13 @@ func Append(router *mux.Router, basePath string, customAssets fs.FS) error {
|
||||
router.Methods(http.MethodGet).
|
||||
Path(basePath).
|
||||
HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
prefix := strings.TrimSuffix(req.Header.Get("X-Forwarded-Prefix"), "/")
|
||||
xfPrefix := req.Header.Get("X-Forwarded-Prefix")
|
||||
if strings.Contains(xfPrefix, "//") {
|
||||
log.Error().Msgf("X-Forwarded-Prefix contains an invalid value: %s, defaulting to empty prefix", xfPrefix)
|
||||
xfPrefix = ""
|
||||
}
|
||||
|
||||
prefix := strings.TrimSuffix(xfPrefix, "/")
|
||||
http.Redirect(resp, req, prefix+dashboardPath, http.StatusFound)
|
||||
})
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ import (
|
||||
"testing/fstest"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_ContentSecurityPolicy(t *testing.T) {
|
||||
@@ -60,6 +62,52 @@ func Test_ContentSecurityPolicy(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_XForwardedPrefix(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
prefix string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "location in X-Forwarded-Prefix",
|
||||
prefix: "//foobar/test",
|
||||
expected: "/dashboard/",
|
||||
},
|
||||
{
|
||||
desc: "scheme in X-Forwarded-Prefix",
|
||||
prefix: "http://foobar",
|
||||
expected: "/dashboard/",
|
||||
},
|
||||
{
|
||||
desc: "path in X-Forwarded-Prefix",
|
||||
prefix: "foobar",
|
||||
expected: "/foobar/dashboard/",
|
||||
},
|
||||
}
|
||||
|
||||
router := mux.NewRouter()
|
||||
err := Append(router, "/", fstest.MapFS{"index.html": &fstest.MapFile{
|
||||
Mode: 0o755,
|
||||
ModTime: time.Now(),
|
||||
}})
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
|
||||
req.Header.Set("X-Forwarded-Prefix", test.prefix)
|
||||
rw := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rw, req)
|
||||
|
||||
assert.Equal(t, http.StatusFound, rw.Code)
|
||||
assert.Equal(t, test.expected, rw.Result().Header.Get("Location"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type errorFS struct{}
|
||||
|
||||
func (e errorFS) Open(name string) (fs.File, error) {
|
||||
|
||||
@@ -317,6 +317,18 @@ func (c *Configuration) SetEffectiveConfiguration() {
|
||||
c.Providers.KubernetesGateway.EntryPoints = entryPoints
|
||||
}
|
||||
|
||||
// Configure Ingress NGINX provider.
|
||||
if c.Providers.KubernetesIngressNGINX != nil {
|
||||
var nonTLSEntryPoints []string
|
||||
for epName, entryPoint := range c.EntryPoints {
|
||||
if entryPoint.HTTP.TLS == nil {
|
||||
nonTLSEntryPoints = append(nonTLSEntryPoints, epName)
|
||||
}
|
||||
}
|
||||
|
||||
c.Providers.KubernetesIngressNGINX.NonTLSEntryPoints = nonTLSEntryPoints
|
||||
}
|
||||
|
||||
// Defines the default rule syntax for the Kubernetes Ingress Provider.
|
||||
// This allows the provider to adapt the matcher syntax to the desired rule syntax version.
|
||||
if c.Core != nil && c.Providers.KubernetesIngress != nil {
|
||||
|
||||
@@ -921,11 +921,11 @@ func (p *Provider) renewCertificates(ctx context.Context, renewPeriod time.Durat
|
||||
for _, cert := range certificates {
|
||||
client, err := p.getClient()
|
||||
if err != nil {
|
||||
logger.Info().Err(err).Msgf("Error renewing certificate from LE : %+v", cert.Domain)
|
||||
logger.Info().Err(err).Msgf("Error renewing ACME certificate: %+v", cert.Domain)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Info().Msgf("Renewing certificate from LE : %+v", cert.Domain)
|
||||
logger.Info().Msgf("Renewing ACME certificate: %+v", cert.Domain)
|
||||
|
||||
res := certificate.Resource{
|
||||
Domain: cert.Domain.Main,
|
||||
@@ -935,12 +935,14 @@ func (p *Provider) renewCertificates(ctx context.Context, renewPeriod time.Durat
|
||||
|
||||
opts := &certificate.RenewOptions{
|
||||
Bundle: true,
|
||||
EmailAddresses: p.EmailAddresses,
|
||||
Profile: p.Profile,
|
||||
PreferredChain: p.PreferredChain,
|
||||
}
|
||||
|
||||
renewedCert, err := client.Certificate.RenewWithOptions(res, opts)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msgf("Error renewing certificate from LE: %v", cert.Domain)
|
||||
logger.Error().Err(err).Msgf("Error renewing ACME certificate: %v", cert.Domain)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: ingress-with-no-annotation
|
||||
namespace: default
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: whoami.localhost
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: whoami
|
||||
port:
|
||||
number: 80
|
||||
path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- hosts:
|
||||
- whoami.localhost
|
||||
secretName: whoami-tls
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: ingress-with-use-regex
|
||||
namespace: default
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/use-regex: "true"
|
||||
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: use-regex.localhost
|
||||
http:
|
||||
paths:
|
||||
- path: /test(.*)
|
||||
pathType: ImplementationSpecific
|
||||
backend:
|
||||
service:
|
||||
name: whoami
|
||||
port:
|
||||
number: 80
|
||||
@@ -80,6 +80,9 @@ type Provider struct {
|
||||
DefaultBackendService string `description:"Service used to serve HTTP requests not matching any known server name (catch-all). Takes the form 'namespace/name'." json:"defaultBackendService,omitempty" toml:"defaultBackendService,omitempty" yaml:"defaultBackendService,omitempty" export:"true"`
|
||||
DisableSvcExternalName bool `description:"Disable support for Services of type ExternalName." json:"disableSvcExternalName,omitempty" toml:"disableSvcExternalName,omitempty" yaml:"disableSvcExternalName,omitempty" export:"true"`
|
||||
|
||||
// NonTLSEntryPoints contains the names of entrypoints that are configured without TLS.
|
||||
NonTLSEntryPoints []string `json:"-" toml:"-" yaml:"-" label:"-" file:"-"`
|
||||
|
||||
defaultBackendServiceNamespace string
|
||||
defaultBackendServiceName string
|
||||
|
||||
@@ -800,7 +803,7 @@ func (p *Provider) applyMiddlewares(namespace, routerKey string, ingressConfig i
|
||||
|
||||
// Apply SSL redirect is mandatory to be applied after all other middlewares.
|
||||
// TODO: check how to remove this, and create the HTTP router elsewhere.
|
||||
applySSLRedirectConfiguration(routerKey, ingressConfig, hasTLS, rt, conf)
|
||||
p.applySSLRedirectConfiguration(routerKey, ingressConfig, hasTLS, rt, conf)
|
||||
|
||||
applyUpstreamVhost(routerKey, ingressConfig, rt, conf)
|
||||
|
||||
@@ -1007,7 +1010,7 @@ func applyWhitelistSourceRangeConfiguration(routerName string, ingressConfig ing
|
||||
rt.Middlewares = append(rt.Middlewares, whitelistSourceRangeMiddlewareName)
|
||||
}
|
||||
|
||||
func applySSLRedirectConfiguration(routerName string, ingressConfig ingressConfig, hasTLS bool, rt *dynamic.Router, conf *dynamic.Configuration) {
|
||||
func (p *Provider) applySSLRedirectConfiguration(routerName string, ingressConfig ingressConfig, hasTLS bool, rt *dynamic.Router, conf *dynamic.Configuration) {
|
||||
var forceSSLRedirect bool
|
||||
if ingressConfig.ForceSSLRedirect != nil {
|
||||
forceSSLRedirect = *ingressConfig.ForceSSLRedirect
|
||||
@@ -1019,7 +1022,9 @@ func applySSLRedirectConfiguration(routerName string, ingressConfig ingressConfi
|
||||
// An Ingress with TLS configuration creates only a Traefik router with a TLS configuration,
|
||||
// so no Non-TLS router exists to handle HTTP traffic, and we should create it.
|
||||
httpRouter := &dynamic.Router{
|
||||
Rule: rt.Rule,
|
||||
// Only attach to entryPoint which do not activate TLS.
|
||||
EntryPoints: p.NonTLSEntryPoints,
|
||||
Rule: rt.Rule,
|
||||
// "default" stands for the default rule syntax in Traefik v3, i.e. the v3 syntax.
|
||||
RuleSyntax: "default",
|
||||
Middlewares: rt.Middlewares,
|
||||
@@ -1133,7 +1138,7 @@ func buildRule(host string, pa netv1.HTTPIngressPath, config ingressConfig) stri
|
||||
rules = append(rules, fmt.Sprintf("Path(`%s`)", pa.Path))
|
||||
case netv1.PathTypePrefix:
|
||||
if ptr.Deref(config.UseRegex, false) {
|
||||
rules = append(rules, fmt.Sprintf("PathRegexp(`^%s`)", regexp.QuoteMeta(pa.Path)))
|
||||
rules = append(rules, fmt.Sprintf("PathRegexp(`^%s`)", pa.Path))
|
||||
} else {
|
||||
rules = append(rules, buildPrefixRule(pa.Path))
|
||||
}
|
||||
|
||||
@@ -98,6 +98,76 @@ func TestLoadIngresses(t *testing.T) {
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "No annotation",
|
||||
paths: []string{
|
||||
"ingresses/00-ingress-with-no-annotation.yml",
|
||||
"ingressclasses.yml",
|
||||
"services.yml",
|
||||
"secrets.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-no-annotation-rule-0-path-0": {
|
||||
Rule: "Host(`whoami.localhost`) && PathPrefix(`/`)",
|
||||
RuleSyntax: "default",
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
Service: "default-ingress-with-no-annotation-whoami-80",
|
||||
},
|
||||
"default-ingress-with-no-annotation-rule-0-path-0-http": {
|
||||
EntryPoints: []string{"web"},
|
||||
Rule: "Host(`whoami.localhost`) && PathPrefix(`/`)",
|
||||
RuleSyntax: "default",
|
||||
Middlewares: []string{"default-ingress-with-no-annotation-rule-0-path-0-redirect-scheme"},
|
||||
Service: "noop@internal",
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{
|
||||
"default-ingress-with-no-annotation-rule-0-path-0-redirect-scheme": {
|
||||
RedirectScheme: &dynamic.RedirectScheme{
|
||||
Scheme: "https",
|
||||
ForcePermanentRedirect: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"default-ingress-with-no-annotation-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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{
|
||||
Certificates: []*tls.CertAndStores{
|
||||
{
|
||||
Certificate: tls.Certificate{
|
||||
CertFile: "-----BEGIN CERTIFICATE-----",
|
||||
KeyFile: "-----BEGIN CERTIFICATE-----",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Basic Auth",
|
||||
paths: []string{
|
||||
@@ -228,15 +298,17 @@ func TestLoadIngresses(t *testing.T) {
|
||||
Service: "default-ingress-with-ssl-redirect-whoami-80",
|
||||
},
|
||||
"default-ingress-with-ssl-redirect-rule-0-path-0-http": {
|
||||
EntryPoints: []string{"web"},
|
||||
Rule: "Host(`sslredirect.localhost`) && Path(`/`)",
|
||||
RuleSyntax: "default",
|
||||
Middlewares: []string{"default-ingress-with-ssl-redirect-rule-0-path-0-redirect-scheme"},
|
||||
Service: "noop@internal",
|
||||
},
|
||||
"default-ingress-without-ssl-redirect-rule-0-path-0-http": {
|
||||
Rule: "Host(`withoutsslredirect.localhost`) && Path(`/`)",
|
||||
RuleSyntax: "default",
|
||||
Service: "default-ingress-without-ssl-redirect-whoami-80",
|
||||
EntryPoints: []string{"web"},
|
||||
Rule: "Host(`withoutsslredirect.localhost`) && Path(`/`)",
|
||||
RuleSyntax: "default",
|
||||
Service: "default-ingress-without-ssl-redirect-whoami-80",
|
||||
},
|
||||
"default-ingress-without-ssl-redirect-rule-0-path-0": {
|
||||
Rule: "Host(`withoutsslredirect.localhost`) && Path(`/`)",
|
||||
@@ -637,6 +709,51 @@ func TestLoadIngresses(t *testing.T) {
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Use Regex",
|
||||
paths: []string{
|
||||
"services.yml",
|
||||
"ingressclasses.yml",
|
||||
"ingresses/10-ingress-with-use-regex.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-use-regex-rule-0-path-0": {
|
||||
Rule: "Host(`use-regex.localhost`) && PathRegexp(`^/test(.*)`)",
|
||||
RuleSyntax: "default",
|
||||
Service: "default-ingress-with-use-regex-whoami-80",
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"default-ingress-with-use-regex-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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Default Backend",
|
||||
defaultBackendServiceName: "whoami",
|
||||
@@ -914,6 +1031,7 @@ func TestLoadIngresses(t *testing.T) {
|
||||
k8sClient: client,
|
||||
defaultBackendServiceName: test.defaultBackendServiceName,
|
||||
defaultBackendServiceNamespace: test.defaultBackendServiceNamespace,
|
||||
NonTLSEntryPoints: []string{"web"},
|
||||
}
|
||||
p.SetDefaults()
|
||||
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
kind: Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: defaultbackend
|
||||
namespace: testing
|
||||
|
||||
spec:
|
||||
defaultBackend:
|
||||
resource:
|
||||
apiGroup: example.com
|
||||
kind: SomeBackend
|
||||
name: foo
|
||||
@@ -0,0 +1,8 @@
|
||||
kind: Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: defaultbackend
|
||||
namespace: testing
|
||||
|
||||
spec:
|
||||
defaultBackend: {}
|
||||
@@ -269,6 +269,17 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
|
||||
continue
|
||||
}
|
||||
|
||||
if ingress.Spec.DefaultBackend.Resource != nil {
|
||||
// https://kubernetes.io/docs/concepts/services-networking/ingress/#resource-backend
|
||||
logger.Error().Msg("Resource is not supported for default backend")
|
||||
continue
|
||||
}
|
||||
|
||||
if ingress.Spec.DefaultBackend.Service == nil {
|
||||
logger.Error().Msg("Default backend is missing service definition")
|
||||
continue
|
||||
}
|
||||
|
||||
service, err := p.loadService(client, ingress.Namespace, *ingress.Spec.DefaultBackend)
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
|
||||
@@ -550,6 +550,26 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Ingress with defaultbackend with resource",
|
||||
expected: &dynamic.Configuration{
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
Routers: map[string]*dynamic.Router{},
|
||||
Services: map[string]*dynamic.Service{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Ingress with empty defaultbackend",
|
||||
expected: &dynamic.Configuration{
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
Routers: map[string]*dynamic.Router{},
|
||||
Services: map[string]*dynamic.Service{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Ingress with one service without endpoint",
|
||||
expected: &dynamic.Configuration{
|
||||
|
||||
@@ -3,6 +3,7 @@ package tcp
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
@@ -222,7 +223,17 @@ func (r *Router) acmeTLSALPNHandler() tcp.Handler {
|
||||
}
|
||||
|
||||
return tcp.HandlerFunc(func(conn tcp.WriteCloser) {
|
||||
_ = tls.Server(conn, r.httpsTLSConfig).Handshake()
|
||||
tlsConn := tls.Server(conn, r.httpsTLSConfig)
|
||||
defer tlsConn.Close()
|
||||
|
||||
// This avoids stale connections when validating the ACME challenge,
|
||||
// as we expect a validation request to complete in a short period of time.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
log.Debug().Err(err).Msg("Error during ACME-TLS/1 handshake")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -697,6 +697,64 @@ func Test_Routing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Router_acmeTLSALPNHandlerTimeout(t *testing.T) {
|
||||
router, err := NewRouter()
|
||||
require.NoError(t, err)
|
||||
|
||||
router.httpsTLSConfig = &tls.Config{}
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
acceptCh := make(chan struct{}, 1)
|
||||
go func() {
|
||||
close(acceptCh)
|
||||
|
||||
conn, err := listener.Accept()
|
||||
require.NoError(t, err)
|
||||
|
||||
defer listener.Close()
|
||||
|
||||
router.acmeTLSALPNHandler().
|
||||
ServeTCP(conn.(*net.TCPConn))
|
||||
}()
|
||||
|
||||
<-acceptCh
|
||||
|
||||
conn, err := net.DialTimeout("tcp", listener.Addr().String(), 2*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
// This is a minimal truncated Client Hello message
|
||||
// to simulate a hanging connection during TLS handshake.
|
||||
clientHello := []byte{
|
||||
// TLS Record Header
|
||||
0x16, // Content Type: Handshake
|
||||
0x03, 0x01, // Version: TLS 1.0 (for compatibility)
|
||||
0x00, 0x50, // Length: 80 bytes
|
||||
}
|
||||
|
||||
_, err = conn.Write(clientHello)
|
||||
require.NoError(t, err)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
// This will return an EOF as the acmeTLSALPNHandler will close the connection
|
||||
// after a timeout during the TLS handshake.
|
||||
b := make([]byte, 256)
|
||||
_, err = conn.Read(b)
|
||||
|
||||
errCh <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
assert.ErrorIs(t, err, io.EOF)
|
||||
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("Error: Timeout waiting for acmeTLSALPNHandler to close the connection")
|
||||
}
|
||||
}
|
||||
|
||||
// routerTCPCatchAll configures a TCP CatchAll No TLS - HostSNI(`*`) router.
|
||||
func routerTCPCatchAll(conf *runtime.Configuration) {
|
||||
conf.TCPRouters["tcp-catchall"] = &runtime.TCPRouterInfo{
|
||||
|
||||
@@ -12,11 +12,16 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// newTestRand creates a deterministic random source for reproducible tests.
|
||||
func newTestRand() *rand.Rand {
|
||||
return rand.New(rand.NewSource(12345))
|
||||
}
|
||||
|
||||
// genIPAddress generate randomly an IP address as a string.
|
||||
func genIPAddress() string {
|
||||
func genIPAddress(rng *rand.Rand) string {
|
||||
buf := make([]byte, 4)
|
||||
|
||||
ip := rand.Uint32()
|
||||
ip := rng.Uint32()
|
||||
|
||||
binary.LittleEndian.PutUint32(buf, ip)
|
||||
ipStr := net.IP(buf)
|
||||
@@ -37,6 +42,7 @@ func initStatusArray(size int, value int) []int {
|
||||
// The tests validate repartition using a margin of 10% of the number of requests
|
||||
|
||||
func TestBalancer(t *testing.T) {
|
||||
rng := newTestRand()
|
||||
balancer := New(false)
|
||||
|
||||
balancer.Add("first", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
@@ -52,7 +58,7 @@ func TestBalancer(t *testing.T) {
|
||||
recorder := &responseRecorder{ResponseRecorder: httptest.NewRecorder(), save: map[string]int{}}
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
for range 100 {
|
||||
req.RemoteAddr = genIPAddress()
|
||||
req.RemoteAddr = genIPAddress(rng)
|
||||
balancer.ServeHTTP(recorder, req)
|
||||
}
|
||||
assert.InDelta(t, 80, recorder.save["first"], 10)
|
||||
@@ -132,6 +138,7 @@ func TestBalancerOneServerDown(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBalancerDownThenUp(t *testing.T) {
|
||||
rng := newTestRand()
|
||||
balancer := New(false)
|
||||
|
||||
balancer.Add("first", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
@@ -155,7 +162,7 @@ func TestBalancerDownThenUp(t *testing.T) {
|
||||
recorder = &responseRecorder{ResponseRecorder: httptest.NewRecorder(), save: map[string]int{}}
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
for range 100 {
|
||||
req.RemoteAddr = genIPAddress()
|
||||
req.RemoteAddr = genIPAddress(rng)
|
||||
balancer.ServeHTTP(recorder, req)
|
||||
}
|
||||
assert.InDelta(t, 50, recorder.save["first"], 10)
|
||||
@@ -163,6 +170,7 @@ func TestBalancerDownThenUp(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBalancerPropagate(t *testing.T) {
|
||||
rng := newTestRand()
|
||||
balancer1 := New(true)
|
||||
|
||||
balancer1.Add("first", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
@@ -188,8 +196,6 @@ func TestBalancerPropagate(t *testing.T) {
|
||||
topBalancer.Add("balancer1", balancer1, Int(1), false)
|
||||
_ = balancer1.RegisterStatusUpdater(func(up bool) {
|
||||
topBalancer.SetStatus(context.WithValue(t.Context(), serviceName, "top"), "balancer1", up)
|
||||
// TODO(mpl): if test gets flaky, add channel or something here to signal that
|
||||
// propagation is done, and wait on it before sending request.
|
||||
})
|
||||
topBalancer.Add("balancer2", balancer2, Int(1), false)
|
||||
_ = balancer2.RegisterStatusUpdater(func(up bool) {
|
||||
@@ -199,7 +205,7 @@ func TestBalancerPropagate(t *testing.T) {
|
||||
recorder := &responseRecorder{ResponseRecorder: httptest.NewRecorder(), save: map[string]int{}}
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
for range 100 {
|
||||
req.RemoteAddr = genIPAddress()
|
||||
req.RemoteAddr = genIPAddress(rng)
|
||||
topBalancer.ServeHTTP(recorder, req)
|
||||
}
|
||||
assert.InDelta(t, 25, recorder.save["first"], 10)
|
||||
@@ -214,7 +220,7 @@ func TestBalancerPropagate(t *testing.T) {
|
||||
recorder = &responseRecorder{ResponseRecorder: httptest.NewRecorder(), save: map[string]int{}}
|
||||
req = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
for range 100 {
|
||||
req.RemoteAddr = genIPAddress()
|
||||
req.RemoteAddr = genIPAddress(rng)
|
||||
topBalancer.ServeHTTP(recorder, req)
|
||||
}
|
||||
assert.InDelta(t, 25, recorder.save["first"], 10)
|
||||
@@ -230,7 +236,7 @@ func TestBalancerPropagate(t *testing.T) {
|
||||
recorder = &responseRecorder{ResponseRecorder: httptest.NewRecorder(), save: map[string]int{}}
|
||||
req = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
for range 100 {
|
||||
req.RemoteAddr = genIPAddress()
|
||||
req.RemoteAddr = genIPAddress(rng)
|
||||
topBalancer.ServeHTTP(recorder, req)
|
||||
}
|
||||
assert.InDelta(t, 50, recorder.save["first"], 10)
|
||||
@@ -254,6 +260,7 @@ func TestBalancerAllServersZeroWeight(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSticky(t *testing.T) {
|
||||
rng := newTestRand()
|
||||
balancer := New(false)
|
||||
|
||||
balancer.Add("first", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
@@ -269,7 +276,7 @@ func TestSticky(t *testing.T) {
|
||||
recorder := &responseRecorder{ResponseRecorder: httptest.NewRecorder(), save: map[string]int{}}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.RemoteAddr = genIPAddress()
|
||||
req.RemoteAddr = genIPAddress(rng)
|
||||
for range 10 {
|
||||
for _, cookie := range recorder.Result().Cookies() {
|
||||
req.AddCookie(cookie)
|
||||
|
||||
@@ -972,23 +972,22 @@ func TestTrafficShiftsWhenPerformanceDegrades(t *testing.T) {
|
||||
assert.InDelta(t, 25, recorder.save["server2"], 10) // 25 ± 10 requests
|
||||
|
||||
// Phase 2: server1 degrades (simulating GC pause, CPU spike, or network latency).
|
||||
server1Delay.Store(15) // Now 15ms (3x slower)
|
||||
server1Delay.Store(50) // Now 50ms (10x slower) - dramatic degradation for reliable detection
|
||||
|
||||
// Make more requests to shift the moving average.
|
||||
// Ring buffer has 100 samples, need significant new samples to shift average.
|
||||
// server1's average will climb from ~5ms toward 15ms.
|
||||
// server1's average will climb from ~5ms toward 50ms.
|
||||
recorder2 := &responseRecorder{ResponseRecorder: httptest.NewRecorder(), save: map[string]int{}}
|
||||
for range 60 {
|
||||
balancer.ServeHTTP(recorder2, httptest.NewRequest(http.MethodGet, "/", nil))
|
||||
}
|
||||
|
||||
// server2 should get significantly more traffic (>75%)
|
||||
// Score for server1: (~10-15ms × 1) / 1 = 10-15 (as average climbs)
|
||||
// Score for server2: (5ms × 1) / 1 = 5
|
||||
// server2 should get significantly more traffic
|
||||
// With 10x performance difference, server2 should dominate.
|
||||
total2 := recorder2.save["server1"] + recorder2.save["server2"]
|
||||
assert.Equal(t, 60, total2)
|
||||
assert.Greater(t, recorder2.save["server2"], 45) // At least 75% (45/60)
|
||||
assert.Less(t, recorder2.save["server1"], 15) // At most 25% (15/60)
|
||||
assert.Greater(t, recorder2.save["server2"], 35) // At least ~60% (35/60)
|
||||
assert.Less(t, recorder2.save["server1"], 25) // At most ~40% (25/60)
|
||||
}
|
||||
|
||||
// TestMultipleServersWithSameScore tests WRR tie-breaking when multiple servers have identical scores.
|
||||
|
||||
Reference in New Issue
Block a user