Merge branch v3.3 into master

This commit is contained in:
kevinpollet 2025-01-31 16:23:49 +01:00
commit 786d9f3272
No known key found for this signature in database
GPG key ID: 0C9A5DDD1B292453
63 changed files with 1660 additions and 4548 deletions

View file

@ -29,14 +29,10 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
assets = webui.FS
}
// allow iframes from traefik domains only
// Allow iframes from traefik domains only.
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-src
w.Header().Set("Content-Security-Policy", "frame-src 'self' https://traefik.io https://*.traefik.io;")
// The content type must be guessed by the file server.
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
w.Header().Del("Content-Type")
if r.RequestURI == "/" {
indexTemplate, err := template.ParseFS(assets, "index.html")
if err != nil {
@ -45,6 +41,8 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
apiPath := strings.TrimSuffix(h.BasePath, "/") + "/api/"
if err = indexTemplate.Execute(w, indexTemplateData{APIUrl: apiPath}); err != nil {
log.Error().Err(err).Msg("Unable to render index template")
@ -55,6 +53,10 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
// The content type must be guessed by the file server.
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
w.Header().Del("Content-Type")
http.FileServerFS(assets).ServeHTTP(w, r)
}
@ -84,13 +86,11 @@ func Append(router *mux.Router, basePath string, customAssets fs.FS) error {
router.Methods(http.MethodGet).
Path(dashboardPath).
HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// allow iframes from our domains only
// Allow iframes from our domains only.
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-src
w.Header().Set("Content-Security-Policy", "frame-src 'self' https://traefik.io https://*.traefik.io;")
// The content type must be guessed by the file server.
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
w.Header().Del("Content-Type")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
apiPath := strings.TrimSuffix(basePath, "/") + "/api/"
if err = indexTemplate.Execute(w, indexTemplateData{APIUrl: apiPath}); err != nil {
@ -103,7 +103,7 @@ func Append(router *mux.Router, basePath string, customAssets fs.FS) error {
router.Methods(http.MethodGet).
PathPrefix(dashboardPath).
HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// allow iframes from traefik domains only
// Allow iframes from traefik domains only.
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-src
w.Header().Set("Content-Security-Policy", "frame-src 'self' https://traefik.io https://*.traefik.io;")
@ -113,5 +113,6 @@ func Append(router *mux.Router, basePath string, customAssets fs.FS) error {
http.StripPrefix(dashboardPath, http.FileServerFS(assets)).ServeHTTP(w, r)
})
return nil
}

View file

@ -252,7 +252,7 @@ type ForwardAuth struct {
// AddAuthCookiesToResponse defines the list of cookies to copy from the authentication server response to the response.
AddAuthCookiesToResponse []string `json:"addAuthCookiesToResponse,omitempty" toml:"addAuthCookiesToResponse,omitempty" yaml:"addAuthCookiesToResponse,omitempty" export:"true"`
// HeaderField defines a header field to store the authenticated user.
// More info: https://doc.traefik.io/traefik/v3.0/middlewares/http/forwardauth/#headerfield
// More info: https://doc.traefik.io/traefik/v3.3/middlewares/http/forwardauth/#headerfield
HeaderField string `json:"headerField,omitempty" toml:"headerField,omitempty" yaml:"headerField,omitempty" export:"true"`
// ForwardBody defines whether to send the request body to the authentication server.
ForwardBody bool `json:"forwardBody,omitempty" toml:"forwardBody,omitempty" yaml:"forwardBody,omitempty" export:"true"`

View file

@ -60,8 +60,6 @@ func (ep *EntryPoint) SetDefaults() {
ep.HTTP.SetDefaults()
ep.HTTP2 = &HTTP2Config{}
ep.HTTP2.SetDefaults()
ep.Observability = &ObservabilityConfig{}
ep.Observability.SetDefaults()
}
// HTTPConfig is the HTTP configuration of an entry point.
@ -164,14 +162,15 @@ func (u *UDPConfig) SetDefaults() {
// ObservabilityConfig holds the observability configuration for an entry point.
type ObservabilityConfig struct {
AccessLogs bool `json:"accessLogs,omitempty" toml:"accessLogs,omitempty" yaml:"accessLogs,omitempty" export:"true"`
Tracing bool `json:"tracing,omitempty" toml:"tracing,omitempty" yaml:"tracing,omitempty" export:"true"`
Metrics bool `json:"metrics,omitempty" toml:"metrics,omitempty" yaml:"metrics,omitempty" export:"true"`
AccessLogs *bool `json:"accessLogs,omitempty" toml:"accessLogs,omitempty" yaml:"accessLogs,omitempty" export:"true"`
Tracing *bool `json:"tracing,omitempty" toml:"tracing,omitempty" yaml:"tracing,omitempty" export:"true"`
Metrics *bool `json:"metrics,omitempty" toml:"metrics,omitempty" yaml:"metrics,omitempty" export:"true"`
}
// SetDefaults sets the default values.
func (o *ObservabilityConfig) SetDefaults() {
o.AccessLogs = true
o.Tracing = true
o.Metrics = true
defaultValue := true
o.AccessLogs = &defaultValue
o.Tracing = &defaultValue
o.Metrics = &defaultValue
}

View file

@ -321,7 +321,7 @@ func (c *Configuration) SetEffectiveConfiguration() {
}
if resolver.ACME.DNSChallenge.DisablePropagationCheck {
log.Warn().Msgf("disablePropagationCheck is now deprecated, please use propagation.disableAllChecks instead.")
log.Warn().Msgf("disablePropagationCheck is now deprecated, please use propagation.disableChecks instead.")
if resolver.ACME.DNSChallenge.Propagation == nil {
resolver.ACME.DNSChallenge.Propagation = &acmeprovider.Propagation{}

View file

@ -77,11 +77,6 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
UDP: &UDPConfig{
Timeout: 3000000000,
},
Observability: &ObservabilityConfig{
AccessLogs: true,
Tracing: true,
Metrics: true,
},
}},
Providers: &Providers{},
},
@ -127,11 +122,6 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
UDP: &UDPConfig{
Timeout: 3000000000,
},
Observability: &ObservabilityConfig{
AccessLogs: true,
Tracing: true,
Metrics: true,
},
}},
Providers: &Providers{},
CertificatesResolvers: map[string]CertificateResolver{
@ -188,11 +178,6 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
UDP: &UDPConfig{
Timeout: 3000000000,
},
Observability: &ObservabilityConfig{
AccessLogs: true,
Tracing: true,
Metrics: true,
},
}},
Providers: &Providers{},
CertificatesResolvers: map[string]CertificateResolver{
@ -253,11 +238,6 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
UDP: &UDPConfig{
Timeout: 3000000000,
},
Observability: &ObservabilityConfig{
AccessLogs: true,
Tracing: true,
Metrics: true,
},
}},
Providers: &Providers{},
CertificatesResolvers: map[string]CertificateResolver{

View file

@ -24,6 +24,7 @@ import (
traefiktls "github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/types"
"go.opentelemetry.io/contrib/bridges/otellogrus"
"go.opentelemetry.io/otel/trace"
)
type key string
@ -209,6 +210,12 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http
},
}
if span := trace.SpanFromContext(req.Context()); span != nil {
spanContext := span.SpanContext()
logDataTable.Core[TraceID] = spanContext.TraceID().String()
logDataTable.Core[SpanID] = spanContext.SpanID().String()
}
reqWithDataTable := req.WithContext(context.WithValue(req.Context(), DataTableKey, logDataTable))
core[RequestCount] = nextRequestCount()

View file

@ -28,6 +28,7 @@ import (
"github.com/traefik/traefik/v3/pkg/types"
"go.opentelemetry.io/collector/pdata/plog/plogotlp"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace/noop"
)
const delta float64 = 1e-10
@ -409,6 +410,8 @@ func TestLoggerJSON(t *testing.T) {
"time": assertNotEmpty(),
"StartLocal": assertNotEmpty(),
"StartUTC": assertNotEmpty(),
TraceID: assertNotEmpty(),
SpanID: assertNotEmpty(),
},
},
{
@ -452,6 +455,8 @@ func TestLoggerJSON(t *testing.T) {
"time": assertNotEmpty(),
StartLocal: assertNotEmpty(),
StartUTC: assertNotEmpty(),
TraceID: assertNotEmpty(),
SpanID: assertNotEmpty(),
},
},
{
@ -627,6 +632,8 @@ func TestLogger_AbortedRequest(t *testing.T) {
"downstream_Content-Type": assertString("text/plain"),
"downstream_Transfer-Encoding": assertString("chunked"),
"downstream_Cache-Control": assertString("no-cache"),
TraceID: assertNotEmpty(),
SpanID: assertNotEmpty(),
}
config := &types.AccessLog{
@ -945,6 +952,10 @@ func doLoggingTLSOpt(t *testing.T, config *types.AccessLog, enableTLS bool) {
}
}
tracer := noop.Tracer{}
spanCtx, _ := tracer.Start(req.Context(), "test")
req = req.WithContext(spanCtx)
chain := alice.New()
chain = chain.Append(capture.Wrap)
chain = chain.Append(WrapHandler(logger))

View file

@ -8,7 +8,6 @@ import (
"github.com/containous/alice"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/accesslog"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
@ -63,12 +62,6 @@ func (e *entryPointTracing) ServeHTTP(rw http.ResponseWriter, req *http.Request)
e.tracer.CaptureServerRequest(span, req)
if logData := accesslog.GetLogData(req); logData != nil {
spanContext := span.SpanContext()
logData.Core[accesslog.TraceID] = spanContext.TraceID().String()
logData.Core[accesslog.SpanID] = spanContext.SpanID().String()
}
recorder := newStatusCodeRecorder(rw, http.StatusOK)
e.next.ServeHTTP(recorder, req)

View file

@ -7,7 +7,6 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/traefik/traefik/v3/pkg/middlewares/accesslog"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/attribute"
)
@ -79,27 +78,3 @@ func TestEntryPointMiddleware_tracing(t *testing.T) {
})
}
}
func TestEntryPointMiddleware_tracingInfoIntoLog(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "http://www.test.com/", http.NoBody)
req = req.WithContext(
context.WithValue(
req.Context(),
accesslog.DataTableKey,
&accesslog.LogData{Core: accesslog.CoreLogData{}},
),
)
next := http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {})
tracer := &mockTracer{}
handler := newEntryPoint(context.Background(), tracing.NewTracer(tracer, []string{}, []string{}, []string{}), "test", next)
handler.ServeHTTP(httptest.NewRecorder(), req)
expectedSpanCtx := tracer.spans[0].SpanContext()
logData := accesslog.GetLogData(req)
assert.Equal(t, expectedSpanCtx.TraceID().String(), logData.Core[accesslog.TraceID])
assert.Equal(t, expectedSpanCtx.SpanID().String(), logData.Core[accesslog.SpanID])
}

View file

@ -151,7 +151,7 @@ func (p *passTLSClientCert) ServeHTTP(rw http.ResponseWriter, req *http.Request)
if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 {
req.Header.Set(xForwardedTLSClientCert, getCertificates(ctx, req.TLS.PeerCertificates))
} else {
logger.Warn().Msg("Tried to extract a certificate on a request without mutual TLS")
logger.Debug().Msg("Tried to extract a certificate on a request without mutual TLS")
}
}
@ -160,7 +160,7 @@ func (p *passTLSClientCert) ServeHTTP(rw http.ResponseWriter, req *http.Request)
headerContent := p.getCertInfo(ctx, req.TLS.PeerCertificates)
req.Header.Set(xForwardedTLSClientCertInfo, url.QueryEscape(headerContent))
} else {
logger.Warn().Msg("Tried to extract a certificate on a request without mutual TLS")
logger.Debug().Msg("Tried to extract a certificate on a request without mutual TLS")
}
}

View file

@ -1,6 +1,7 @@
package acme
import (
"context"
"encoding/json"
"io"
"os"
@ -23,9 +24,9 @@ type LocalStore struct {
}
// NewLocalStore initializes a new LocalStore with a file name.
func NewLocalStore(filename string) *LocalStore {
func NewLocalStore(filename string, routinesPool *safe.Pool) *LocalStore {
store := &LocalStore{filename: filename, saveDataChan: make(chan map[string]*StoredData)}
store.listenSaveAction()
store.listenSaveAction(routinesPool)
return store
}
@ -100,18 +101,31 @@ func (s *LocalStore) get(resolverName string) (*StoredData, error) {
}
// listenSaveAction listens to a chan to store ACME data in json format into `LocalStore.filename`.
func (s *LocalStore) listenSaveAction() {
safe.Go(func() {
func (s *LocalStore) listenSaveAction(routinesPool *safe.Pool) {
routinesPool.GoCtx(func(ctx context.Context) {
logger := log.With().Str(logs.ProviderName, "acme").Logger()
for object := range s.saveDataChan {
data, err := json.MarshalIndent(object, "", " ")
if err != nil {
logger.Error().Err(err).Send()
}
for {
select {
case <-ctx.Done():
return
err = os.WriteFile(s.filename, data, 0o600)
if err != nil {
logger.Error().Err(err).Send()
case object := <-s.saveDataChan:
select {
case <-ctx.Done():
// Stop handling events because Traefik is shutting down.
return
default:
}
data, err := json.MarshalIndent(object, "", " ")
if err != nil {
logger.Error().Err(err).Send()
}
err = os.WriteFile(s.filename, data, 0o600)
if err != nil {
logger.Error().Err(err).Send()
}
}
}
})

View file

@ -1,6 +1,7 @@
package acme
import (
"context"
"fmt"
"os"
"path/filepath"
@ -9,6 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/safe"
)
func TestLocalStore_GetAccount(t *testing.T) {
@ -45,7 +47,7 @@ func TestLocalStore_GetAccount(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
s := NewLocalStore(test.filename)
s := NewLocalStore(test.filename, safe.NewPool(context.Background()))
account, err := s.GetAccount("test")
require.NoError(t, err)
@ -58,7 +60,7 @@ func TestLocalStore_GetAccount(t *testing.T) {
func TestLocalStore_SaveAccount(t *testing.T) {
acmeFile := filepath.Join(t.TempDir(), "acme.json")
s := NewLocalStore(acmeFile)
s := NewLocalStore(acmeFile, safe.NewPool(context.Background()))
email := "some@email.com"

View file

@ -91,7 +91,7 @@ type DNSChallenge struct {
// Deprecated: please use Propagation.DelayBeforeChecks instead.
DelayBeforeCheck ptypes.Duration `description:"(Deprecated) Assume DNS propagates after a delay in seconds rather than finding and querying nameservers." json:"delayBeforeCheck,omitempty" toml:"delayBeforeCheck,omitempty" yaml:"delayBeforeCheck,omitempty" export:"true"`
// Deprecated: please use Propagation.DisableAllChecks instead.
// Deprecated: please use Propagation.DisableChecks instead.
DisablePropagationCheck bool `description:"(Deprecated) Disable the DNS propagation checks before notifying ACME that the DNS challenge is ready. [not recommended]" json:"disablePropagationCheck,omitempty" toml:"disablePropagationCheck,omitempty" yaml:"disablePropagationCheck,omitempty" export:"true"`
}

View file

@ -60,6 +60,7 @@ metadata:
spec:
forwardAuth:
address: test.com
headerField: X-Header-Field
tls:
certSecret: tlssecret
caSecret: casecret

View file

@ -789,6 +789,7 @@ func createForwardAuthMiddleware(k8sClient Client, namespace string, auth *traef
AuthResponseHeadersRegex: auth.AuthResponseHeadersRegex,
AuthRequestHeaders: auth.AuthRequestHeaders,
AddAuthCookiesToResponse: auth.AddAuthCookiesToResponse,
HeaderField: auth.HeaderField,
ForwardBody: auth.ForwardBody,
PreserveLocationHeader: auth.PreserveLocationHeader,
PreserveRequestMethod: auth.PreserveRequestMethod,

View file

@ -3961,6 +3961,7 @@ func TestLoadIngressRoutes(t *testing.T) {
ForwardAuth: &dynamic.ForwardAuth{
Address: "test.com",
MaxBodySize: pointer(int64(-1)),
HeaderField: "X-Header-Field",
TLS: &dynamic.ClientTLS{
CA: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----",
Cert: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----",

View file

@ -161,6 +161,9 @@ type ForwardAuth struct {
TLS *ClientTLS `json:"tls,omitempty"`
// AddAuthCookiesToResponse defines the list of cookies to copy from the authentication server response to the response.
AddAuthCookiesToResponse []string `json:"addAuthCookiesToResponse,omitempty"`
// HeaderField defines a header field to store the authenticated user.
// More info: https://doc.traefik.io/traefik/v3.3/middlewares/http/forwardauth/#headerfield
HeaderField string `json:"headerField,omitempty"`
// ForwardBody defines whether to send the request body to the authentication server.
ForwardBody bool `json:"forwardBody,omitempty"`
// MaxBodySize defines the maximum body size in bytes allowed to be forwarded to the authentication server.

View file

@ -229,33 +229,32 @@ func (i *Provider) entryPointModels(cfg *dynamic.Configuration) {
}
}
if len(ep.HTTP.Middlewares) == 0 && ep.HTTP.TLS == nil && defaultRuleSyntax == "" {
if len(ep.HTTP.Middlewares) == 0 && ep.HTTP.TLS == nil && defaultRuleSyntax == "" && ep.Observability == nil {
continue
}
m := &dynamic.Model{
Middlewares: ep.HTTP.Middlewares,
httpModel := &dynamic.Model{
DefaultRuleSyntax: defaultRuleSyntax,
Middlewares: ep.HTTP.Middlewares,
}
if ep.Observability != nil {
m.Observability = dynamic.RouterObservabilityConfig{
AccessLogs: &ep.Observability.AccessLogs,
Tracing: &ep.Observability.Tracing,
Metrics: &ep.Observability.Metrics,
httpModel.Observability = dynamic.RouterObservabilityConfig{
AccessLogs: ep.Observability.AccessLogs,
Tracing: ep.Observability.Tracing,
Metrics: ep.Observability.Metrics,
}
}
if ep.HTTP.TLS != nil {
m.TLS = &dynamic.RouterTLSConfig{
httpModel.TLS = &dynamic.RouterTLSConfig{
Options: ep.HTTP.TLS.Options,
CertResolver: ep.HTTP.TLS.CertResolver,
Domains: ep.HTTP.TLS.Domains,
}
}
m.DefaultRuleSyntax = defaultRuleSyntax
cfg.HTTP.Models[name] = m
cfg.HTTP.Models[name] = httpModel
}
}

View file

@ -18,6 +18,8 @@ import (
var updateExpected = flag.Bool("update_expected", false, "Update expected files in fixtures")
func pointer[T any](v T) *T { return &v }
func Test_createConfiguration(t *testing.T) {
testCases := []struct {
desc string
@ -185,9 +187,9 @@ func Test_createConfiguration(t *testing.T) {
},
},
Observability: &static.ObservabilityConfig{
AccessLogs: false,
Tracing: false,
Metrics: false,
AccessLogs: pointer(false),
Tracing: pointer(false),
Metrics: pointer(false),
},
},
},

View file

@ -20,8 +20,9 @@ import (
// rwWithUpgrade contains a ResponseWriter and an upgradeHandler,
// used to upgrade the connection (e.g. Websockets).
type rwWithUpgrade struct {
RW http.ResponseWriter
Upgrade upgradeHandler
ReqMethod string
RW http.ResponseWriter
Upgrade upgradeHandler
}
// conn is an enriched net.Conn.
@ -178,7 +179,6 @@ func (c *conn) handleResponse(r rwWithUpgrade) error {
}
res.Reset()
res.Header.Reset()
res.Header.SetNoDefaultContentType(true)
continue
@ -211,7 +211,13 @@ func (c *conn) handleResponse(r rwWithUpgrade) error {
r.RW.WriteHeader(res.StatusCode())
if res.Header.ContentLength() == 0 {
if noResponseBodyExpected(r.ReqMethod) {
return nil
}
hasContentLength := len(res.Header.Peek("Content-Length")) > 0
if hasContentLength && res.Header.ContentLength() == 0 {
return nil
}
@ -221,6 +227,20 @@ func (c *conn) handleResponse(r rwWithUpgrade) error {
return nil
}
if !hasContentLength {
b := c.bufferPool.Get()
if b == nil {
b = make([]byte, bufferSize)
}
defer c.bufferPool.Put(b)
if _, err := io.CopyBuffer(r.RW, c.br, b); err != nil {
return err
}
return nil
}
// Chunked response, Content-Length is set to -1 by FastProxy when "Transfer-Encoding: chunked" header is received.
if res.Header.ContentLength() == -1 {
cbr := httputil.NewChunkedReader(c.br)
@ -444,8 +464,8 @@ func (c *connPool) askForNewConn(errCh chan<- error) {
c.releaseConn(newConn)
}
// isBodyAllowedForStatus reports whether a given response status code
// permits a body. See RFC 7230, section 3.3.
// isBodyAllowedForStatus reports whether a given response status code permits a body.
// See RFC 7230, section 3.3.
// From https://github.com/golang/go/blame/master/src/net/http/transfer.go#L459
func isBodyAllowedForStatus(status int) bool {
switch {
@ -458,3 +478,9 @@ func isBodyAllowedForStatus(status int) bool {
}
return true
}
// noResponseBodyExpected reports whether a given request method permits a body.
// From https://github.com/golang/go/blame/master/src/net/http/transfer.go#L250
func noResponseBodyExpected(requestMethod string) bool {
return requestMethod == "HEAD"
}

View file

@ -284,8 +284,9 @@ func (p *ReverseProxy) roundTrip(rw http.ResponseWriter, req *http.Request, outR
// Sending the responseWriter unlocks the connection readLoop, to handle the response.
co.RWCh <- rwWithUpgrade{
RW: rw,
Upgrade: upgradeResponseHandler(req.Context(), reqUpType),
ReqMethod: req.Method,
RW: rw,
Upgrade: upgradeResponseHandler(req.Context(), reqUpType),
}
if err := <-co.ErrCh; err != nil {

View file

@ -278,6 +278,74 @@ func TestPreservePath(t *testing.T) {
assert.Equal(t, http.StatusOK, res.Code)
}
func TestHeadRequest(t *testing.T) {
var callCount int
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
callCount++
assert.Equal(t, http.MethodHead, req.Method)
rw.Header().Set("Content-Length", "42")
}))
t.Cleanup(server.Close)
builder := NewProxyBuilder(&transportManagerMock{}, static.FastProxyConfig{})
serverURL, err := url.JoinPath(server.URL)
require.NoError(t, err)
proxyHandler, err := builder.Build("", testhelpers.MustParseURL(serverURL), true, true)
require.NoError(t, err)
req := httptest.NewRequest(http.MethodHead, "/", http.NoBody)
res := httptest.NewRecorder()
proxyHandler.ServeHTTP(res, req)
assert.Equal(t, 1, callCount)
assert.Equal(t, http.StatusOK, res.Code)
}
func TestNoContentLengthResponse(t *testing.T) {
backendListener, err := net.Listen("tcp", ":0")
require.NoError(t, err)
t.Cleanup(func() {
_ = backendListener.Close()
})
go func() {
t.Helper()
conn, err := backendListener.Accept()
require.NoError(t, err)
_, err = conn.Write([]byte("HTTP/1.1 200 OK\r\n\r\nfoo"))
require.NoError(t, err)
// CloseWrite the connection to signal the end of the response.
if v, ok := conn.(interface{ CloseWrite() error }); ok {
err = v.CloseWrite()
require.NoError(t, err)
}
}()
builder := NewProxyBuilder(&transportManagerMock{}, static.FastProxyConfig{})
serverURL := "http://" + backendListener.Addr().String()
proxyHandler, err := builder.Build("", testhelpers.MustParseURL(serverURL), true, true)
require.NoError(t, err)
req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
res := httptest.NewRecorder()
proxyHandler.ServeHTTP(res, req)
assert.Equal(t, http.StatusOK, res.Code)
assert.Equal(t, "foo", res.Body.String())
}
func newCertificate(t *testing.T, domain string) *tls.Certificate {
t.Helper()

View file

@ -191,14 +191,14 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
cp.Observability.AccessLogs = m.Observability.AccessLogs
}
if cp.Observability.Tracing == nil {
cp.Observability.Tracing = m.Observability.Tracing
}
if cp.Observability.Metrics == nil {
cp.Observability.Metrics = m.Observability.Metrics
}
if cp.Observability.Tracing == nil {
cp.Observability.Tracing = m.Observability.Tracing
}
rtName := name
if len(eps) > 1 {
rtName = epName + "-" + name
@ -215,6 +215,9 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
cfg.HTTP.Routers = rts
}
// Apply default observability model to HTTP routers.
applyDefaultObservabilityModel(cfg)
if cfg.TCP == nil || len(cfg.TCP.Models) == 0 {
return cfg
}
@ -238,3 +241,38 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
return cfg
}
// applyDefaultObservabilityModel applies the default observability model to the configuration.
// This function is used to ensure that the observability configuration is set for all routers,
// and make sure it is serialized and available in the API.
// We could have introduced a "default" model, but it would have been more complex to manage for now.
// This could be generalized in the future.
func applyDefaultObservabilityModel(cfg dynamic.Configuration) {
if cfg.HTTP != nil {
for _, router := range cfg.HTTP.Routers {
if router.Observability == nil {
router.Observability = &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
}
continue
}
if router.Observability.AccessLogs == nil {
router.Observability.AccessLogs = pointer(true)
}
if router.Observability.Tracing == nil {
router.Observability.Tracing = pointer(true)
}
if router.Observability.Metrics == nil {
router.Observability.Metrics = pointer(true)
}
}
}
}
func pointer[T any](v T) *T { return &v }

View file

@ -9,8 +9,6 @@ import (
"github.com/traefik/traefik/v3/pkg/tls"
)
func pointer[T any](v T) *T { return &v }
func Test_mergeConfiguration(t *testing.T) {
testCases := []struct {
desc string
@ -508,6 +506,33 @@ func Test_applyModel(t *testing.T) {
},
},
},
{
desc: "without model, one router",
input: dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{"test": {}},
Middlewares: make(map[string]*dynamic.Middleware),
Services: make(map[string]*dynamic.Service),
Models: make(map[string]*dynamic.Model),
},
},
expected: dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"test": {
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
},
},
},
Middlewares: make(map[string]*dynamic.Middleware),
Services: make(map[string]*dynamic.Service),
Models: make(map[string]*dynamic.Model),
},
},
},
{
desc: "with model, not used",
input: dynamic.Configuration{
@ -560,10 +585,14 @@ func Test_applyModel(t *testing.T) {
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"test": {
EntryPoints: []string{"websecure"},
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{},
Observability: &dynamic.RouterObservabilityConfig{},
EntryPoints: []string{"websecure"},
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{},
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
},
},
},
Middlewares: make(map[string]*dynamic.Middleware),
@ -659,9 +688,9 @@ func Test_applyModel(t *testing.T) {
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{CertResolver: "router"},
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: nil,
Tracing: nil,
Metrics: nil,
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
},
},
},
@ -700,12 +729,21 @@ func Test_applyModel(t *testing.T) {
Routers: map[string]*dynamic.Router{
"test": {
EntryPoints: []string{"web"},
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
},
},
"websecure-test": {
EntryPoints: []string{"websecure"},
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{},
Observability: &dynamic.RouterObservabilityConfig{},
EntryPoints: []string{"websecure"},
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{},
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
},
},
},
Middlewares: make(map[string]*dynamic.Middleware),

View file

@ -84,7 +84,8 @@ func TestNewConfigurationWatcher(t *testing.T) {
th.WithRouters(
th.WithRouter("test@mock",
th.WithEntryPoints("e"),
th.WithServiceName("scv"))),
th.WithServiceName("scv"),
th.WithObservability())),
th.WithMiddlewares(),
th.WithLoadBalancerServices(),
),
@ -175,7 +176,7 @@ func TestIgnoreTransientConfiguration(t *testing.T) {
expectedConfig := dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"))),
th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"), th.WithObservability())),
th.WithLoadBalancerServices(th.WithService("bar@mock")),
th.WithMiddlewares(),
),
@ -200,7 +201,7 @@ func TestIgnoreTransientConfiguration(t *testing.T) {
expectedConfig3 := dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"))),
th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"), th.WithObservability())),
th.WithLoadBalancerServices(th.WithService("bar-config3@mock")),
th.WithMiddlewares(),
),
@ -447,7 +448,7 @@ func TestListenProvidersDoesNotSkipFlappingConfiguration(t *testing.T) {
expected := dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"))),
th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"), th.WithObservability())),
th.WithLoadBalancerServices(th.WithService("bar@mock")),
th.WithMiddlewares(),
),
@ -538,7 +539,7 @@ func TestListenProvidersIgnoreSameConfig(t *testing.T) {
expected := dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"))),
th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"), th.WithObservability())),
th.WithLoadBalancerServices(th.WithService("bar@mock")),
th.WithMiddlewares(),
),
@ -674,7 +675,7 @@ func TestListenProvidersIgnoreIntermediateConfigs(t *testing.T) {
expected := dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("final@mock", th.WithEntryPoints("ep"))),
th.WithRouters(th.WithRouter("final@mock", th.WithEntryPoints("ep"), th.WithObservability())),
th.WithLoadBalancerServices(th.WithService("final@mock")),
th.WithMiddlewares(),
),
@ -738,8 +739,8 @@ func TestListenProvidersPublishesConfigForEachProvider(t *testing.T) {
expected := dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(
th.WithRouter("foo@mock", th.WithEntryPoints("ep")),
th.WithRouter("foo@mock2", th.WithEntryPoints("ep")),
th.WithRouter("foo@mock", th.WithEntryPoints("ep"), th.WithObservability()),
th.WithRouter("foo@mock2", th.WithEntryPoints("ep"), th.WithObservability()),
),
th.WithLoadBalancerServices(
th.WithService("bar@mock"),

View file

@ -110,7 +110,7 @@ func (o *ObservabilityMgr) ShouldAddAccessLogs(serviceName string, observability
return false
}
return observabilityConfig == nil || observabilityConfig.AccessLogs != nil && *observabilityConfig.AccessLogs
return observabilityConfig == nil || observabilityConfig.AccessLogs == nil || *observabilityConfig.AccessLogs
}
// ShouldAddMetrics returns whether the metrics should be enabled for the given resource and the observability config.
@ -127,7 +127,7 @@ func (o *ObservabilityMgr) ShouldAddMetrics(serviceName string, observabilityCon
return false
}
return observabilityConfig == nil || observabilityConfig.Metrics != nil && *observabilityConfig.Metrics
return observabilityConfig == nil || observabilityConfig.Metrics == nil || *observabilityConfig.Metrics
}
// ShouldAddTracing returns whether the tracing should be enabled for the given serviceName and the observability config.
@ -144,7 +144,7 @@ func (o *ObservabilityMgr) ShouldAddTracing(serviceName string, observabilityCon
return false
}
return observabilityConfig == nil || observabilityConfig.Tracing != nil && *observabilityConfig.Tracing
return observabilityConfig == nil || observabilityConfig.Tracing == nil || *observabilityConfig.Tracing
}
// MetricsRegistry is an accessor to the metrics registry.

View file

@ -53,6 +53,17 @@ func WithServiceName(serviceName string) func(*dynamic.Router) {
}
}
// WithObservability is a helper to create a configuration.
func WithObservability() func(*dynamic.Router) {
return func(r *dynamic.Router) {
r.Observability = &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
}
}
}
// WithLoadBalancerServices is a helper to create a configuration.
func WithLoadBalancerServices(opts ...func(service *dynamic.ServersLoadBalancer) string) func(*dynamic.HTTPConfiguration) {
return func(c *dynamic.HTTPConfiguration) {
@ -149,3 +160,5 @@ func WithSticky(cookieName string) func(*dynamic.ServersLoadBalancer) {
}
}
}
func pointer[T any](v T) *T { return &v }