Merge branch v3.7 into master

This commit is contained in:
Neffi42 committed 2026-10-22 09:53:38 +02:00
commit ee3b69d184
90 files changed
+2438 -1334

No files matched your search

+1 -1
View File
@@ -6,7 +6,7 @@ on:
- '*'
env:
GOLANGCI_LINT_VERSION: v2.13.2
GOLANGCI_LINT_VERSION: v2.14.0
MISSPELL_VERSION: v0.7.0
permissions:
+1 -1
View File
@@ -1 +1 @@
1.26
1.27
+2
View File
@@ -77,6 +77,8 @@ Prefer modern standard-library packages (`slices`, `maps`, `cmp`, ...) over hand
- **Logging.** The project uses `github.com/rs/zerolog` exclusively — do not import `log`, `slog`, or `logrus`. Inside a middleware, get a logger via `middlewares.GetLogger(ctx, name, typeName)` (see [`pkg/middlewares/middleware.go`](./pkg/middlewares/middleware.go)) where `typeName` is a package-level `const` like `const typeNameForward = "ForwardAuth"`. Elsewhere, extract the logger from the context with `log.Ctx(ctx)` and attach it to a new context with `.WithContext(ctx)`.
- **Context propagation.** `context.Context` is always the first argument, named `ctx`. Avoid `context.Background()` in request paths; propagate from the caller. Define custom context keys as unexported struct types (`type myKey struct{}`) to prevent collisions.
- **ResponseWriter wrappers implement `Unwrap`.** Any type wrapping an `http.ResponseWriter` must expose it with `Unwrap() http.ResponseWriter`. `http.ResponseController` walks the chain through `Unwrap` to reach the connection (read/write deadlines, full duplex, ...); a wrapper without it breaks the chain for every handler below it, and the failure is silent (`http.ErrNotSupported`). Do not add `Flush`/`Hijack` methods that only forward to the wrapped writer, do not implement the deprecated `http.CloseNotifier` (use `req.Context().Done()`), and do not assert `http.Flusher`/`http.Hijacker` on a ResponseWriter: call `http.NewResponseController(rw).Flush()`/`.Hijack()`, and return its error as is (callers check it with `errors.Is(err, http.ErrNotSupported)`). A wrapper that must act on flush or hijack implements `FlushError() error` or `Hijack()` and calls the wrapped writer through `http.ResponseController`. A wrapper that holds back the status code (retry, failover, error pages) must not flush before writing it: flushing commits an implicit `200`.
- **Third-party handlers are used directly, plugins are wrapped.** A library must flush and hijack through `http.ResponseController` and give the next handler a writer with `Unwrap`; if it does not, fix it upstream. Yaegi/WASM plugins, whose code is unknown, may assert `http.Flusher`/`http.Hijacker` on the writer they receive and may give the next handler a writer without `Unwrap`, which is their right (e.g. a buffering writer must not be flushed past). The plugin builders wrap them with `wrapExternal` (or `wrapExternalWith` when the build function also returns a value), see [`pkg/plugins/external.go`](./pkg/plugins/external.go): deadlines, full duplex and hijacking fall back to the writer given to the plugin when the plugin's writer does not support them; flushing never falls back.
## Testing conventions
+13 -15
View File
@@ -18,21 +18,19 @@ type TraefikCmdConfiguration struct {
// NewTraefikConfiguration creates a TraefikCmdConfiguration with default values.
func NewTraefikConfiguration() *TraefikCmdConfiguration {
return &TraefikCmdConfiguration{
Configuration: static.Configuration{
Global: &static.Global{
CheckNewVersion: true,
},
EntryPoints: make(static.EntryPoints),
Providers: &static.Providers{
ProvidersThrottleDuration: ptypes.Duration(2 * time.Second),
},
ServersTransport: &static.ServersTransport{
MaxIdleConnsPerHost: 200,
},
TCPServersTransport: &static.TCPServersTransport{
DialTimeout: ptypes.Duration(30 * time.Second),
DialKeepAlive: ptypes.Duration(15 * time.Second),
},
Global: &static.Global{
CheckNewVersion: true,
},
EntryPoints: make(static.EntryPoints),
Providers: &static.Providers{
ProvidersThrottleDuration: ptypes.Duration(2 * time.Second),
},
ServersTransport: &static.ServersTransport{
MaxIdleConnsPerHost: 200,
},
TCPServersTransport: &static.TCPServersTransport{
DialTimeout: ptypes.Duration(30 * time.Second),
DialKeepAlive: ptypes.Duration(15 * time.Second),
},
ConfigFile: "",
}
@@ -7,8 +7,12 @@ description: "Security is a key part of Traefik Proxy. Read the technical docume
## Security Advisories
We strongly advise you to join our mailing list to be aware of the latest announcements from our security team.
You can subscribe by sending an email to security+subscribe@traefik.io or on [the online viewer](https://groups.google.com/a/traefik.io/forum/#!forum/security).
We publish our security announcements as [GitHub security advisories](https://github.com/traefik/traefik/security/advisories).
Check this page to stay aware of the latest announcements from our security team.
Each advisory is published together with the releases that fix the vulnerability.
To be notified when they are out, watch the [Traefik repository](https://github.com/traefik/traefik) on GitHub,
and select **Custom** > **Releases**.
## CVE
+43
View File
@@ -140,6 +140,49 @@ kubectl apply -f https://raw.githubusercontent.com/traefik/traefik/v3.8/docs/con
---
## v3.7.17
Starting with `v3.7.17`, Traefik is built with Go 1.27.
### GODEBUG TLS Settings
Go 1.27 removed the `tlsrsakex`, `tls10server` and `tls3des` `GODEBUG` settings.
Setting them in the `GODEBUG` environment variable has no effect anymore.
For the incoming connections, the RSA key exchange and 3DES cipher suites, as well as TLS 1.0 and 1.1,
can still be enabled with the `cipherSuites` and `minVersion` fields of the [TLS options](../reference/routing-configuration/http/tls/tls-options.md),
referenced in the TLS configuration of a [router](../reference/routing-configuration/http/routing/router.md) or of an [entrypoint](../reference/install-configuration/entrypoints.md#opt-http-tls-options).
For the connections to the backends, they can still be enabled with the `cipherSuites` and `minVersion` fields of the [ServersTransport](../reference/routing-configuration/http/load-balancing/serverstransport.md#opt-cipherSuites).
Please check out the [go documentation](https://go.dev/doc/go1.27#cryptotlspkgcryptotls) for more details.
### Maximum Number of Request Header Values
Go 1.27 limits the number of header values a request can carry to 500.
A request exceeding this limit is rejected with a `431 Request Header Fields Too Large` response,
for HTTP/1 and HTTP/2.
This limit can be changed with the new [`maxHeaderValueCount`](../reference/install-configuration/entrypoints.md#opt-http-maxHeaderValueCount) entry point option.
As the rejection happens before the routing, the rejected requests are not reported in the access logs.
Please check out the [go documentation](https://go.dev/doc/go1.27#nethttppkgnethttp) for more details.
---
## v3.7.16
### Kubernetes CRD and Ingress-NGINX Providers: Secret references in middlewares
Until `v3.7.16`, the routing configuration of a middleware held the values that the provider read from Kubernetes Secrets.
Starting with `v3.7.16`, it holds a reference to the Secret key instead, in the form `urn:k8s:secret:<secret name>:<key>`.
Traefik reads the value when it builds the middleware, so the behavior of the middleware does not change.
The API, the dashboard and the support dump show the references.
---
## v3.7.15
### CONNECT Requests
@@ -103,6 +103,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| <a id="opt-entrypoints-name-http-encodedcharacters-allowencodedslash" href="#opt-entrypoints-name-http-encodedcharacters-allowencodedslash" title="#opt-entrypoints-name-http-encodedcharacters-allowencodedslash">entrypoints._name_.http.encodedcharacters.allowencodedslash</a> | Defines whether requests with encoded slash characters in the path are allowed. | true |
| <a id="opt-entrypoints-name-http-encodequerysemicolons" href="#opt-entrypoints-name-http-encodequerysemicolons" title="#opt-entrypoints-name-http-encodequerysemicolons">entrypoints._name_.http.encodequerysemicolons</a> | Defines whether request query semicolons should be URLEncoded. | false |
| <a id="opt-entrypoints-name-http-maxheaderbytes" href="#opt-entrypoints-name-http-maxheaderbytes" title="#opt-entrypoints-name-http-maxheaderbytes">entrypoints._name_.http.maxheaderbytes</a> | Maximum size of request headers in bytes. | 1048576 |
| <a id="opt-entrypoints-name-http-maxheadervaluecount" href="#opt-entrypoints-name-http-maxheadervaluecount" title="#opt-entrypoints-name-http-maxheadervaluecount">entrypoints._name_.http.maxheadervaluecount</a> | Maximum number of request header values. | 500 |
| <a id="opt-entrypoints-name-http-middlewares" href="#opt-entrypoints-name-http-middlewares" title="#opt-entrypoints-name-http-middlewares">entrypoints._name_.http.middlewares</a> | Default middlewares for the routers linked to the entry point. | |
| <a id="opt-entrypoints-name-http-redirections-entrypoint-permanent" href="#opt-entrypoints-name-http-redirections-entrypoint-permanent" title="#opt-entrypoints-name-http-redirections-entrypoint-permanent">entrypoints._name_.http.redirections.entrypoint.permanent</a> | Applies a permanent redirection. | true |
| <a id="opt-entrypoints-name-http-redirections-entrypoint-priority" href="#opt-entrypoints-name-http-redirections-entrypoint-priority" title="#opt-entrypoints-name-http-redirections-entrypoint-priority">entrypoints._name_.http.redirections.entrypoint.priority</a> | Priority of the generated router. | 9223372036854775806 |
@@ -109,6 +109,7 @@ additionalArguments:
| <a id="opt-http-aliasHeadersStrategy" href="#opt-http-aliasHeadersStrategy" title="#opt-http-aliasHeadersStrategy">`http.aliasHeadersStrategy`</a> | Defines the strategy to handle the requests carrying a header whose name aliases another header name (keep, delete, and reject).<br /> More information [here](#aliasheadersstrategy). | keep | No |
| <a id="opt-http-sanitizePath" href="#opt-http-sanitizePath" title="#opt-http-sanitizePath">`http.sanitizePath`</a> | Defines whether to enable the request path sanitization.<br /> More information [here](#sanitizepath). | true | No |
| <a id="opt-http-maxHeaderBytes" href="#opt-http-maxHeaderBytes" title="#opt-http-maxHeaderBytes">`http.maxHeaderBytes`</a> | Set the maximum size of request headers in bytes. | 1048576 | No |
| <a id="opt-http-maxHeaderValueCount" href="#opt-http-maxHeaderValueCount" title="#opt-http-maxHeaderValueCount">`http.maxHeaderValueCount`</a> | Set the maximum number of request header values Traefik reads for each request. A request exceeding this limit is rejected with a `431 Request Header Fields Too Large` status. <br /> Comma-separated values in a single header line count as one value, while values sent on multiple header lines count once per line. <br /> This option applies to HTTP/1 and HTTP/2 only; it has no effect on HTTP/3 requests. | 500 | No |
| <a id="opt-http-middlewares" href="#opt-http-middlewares" title="#opt-http-middlewares">`http.middlewares`</a> | Set the list of middlewares that are prepended by default to the list of middlewares of each router associated to the named entry point. <br />More information [here](#httpmiddlewares). | - | No |
| <a id="opt-http-tls" href="#opt-http-tls" title="#opt-http-tls">`http.tls`</a> | Enable TLS on every router attached to the `entryPoint`. <br /> If no certificate are set, a default self-signed certificate is generated by Traefik. <br /> We recommend to not use self signed certificates in production. | - | No |
| <a id="opt-http-tls-options" href="#opt-http-tls-options" title="#opt-http-tls-options">`http.tls.options`</a> | Apply TLS options on every router attached to the `entryPoint`. <br /> These options apply only while the router does not define its own `tls` section. <br /> Defining a router `tls` section (even empty, or with any field such as `certResolver`) replaces the entrypoint TLS configuration for that router (no merge). <br /> More information in the [dedicated section](../../reference/routing-configuration/http/tls/tls-options.md) and [HTTP TLS overview](../../reference/routing-configuration/http/tls/overview.md). | - | No |
@@ -61,6 +61,7 @@
encodeQuerySemicolons = true
sanitizePath = true
maxHeaderBytes = 42
maxHeaderValueCount = 42
aliasHeadersStrategy = "foobar"
underscoreHeadersStrategy = "foobar"
[entryPoints.EntryPoint0.http.redirections]
@@ -99,6 +99,7 @@ entryPoints:
encodeQuerySemicolons: true
sanitizePath: true
maxHeaderBytes: 42
maxHeaderValueCount: 42
aliasHeadersStrategy: foobar
underscoreHeadersStrategy: foobar
http2:
+7 -6
View File
@@ -1,6 +1,6 @@
module github.com/traefik/traefik/v3
go 1.26.0
go 1.27.0
require (
github.com/BurntSushi/toml v1.6.0
@@ -39,7 +39,8 @@ require (
github.com/huandu/xstrings v1.5.0
github.com/influxdata/influxdb-client-go/v2 v2.7.0
github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab // No tag on the repo.
github.com/klauspost/compress v1.19.2
// TODO: pin a released version once https://github.com/klauspost/compress/pull/1272 is part of a release.
github.com/klauspost/compress v1.20.4-0.20261021124103-35c6f98a5cb9
github.com/kvtools/consul v1.0.2
github.com/kvtools/etcdv3 v1.0.3
github.com/kvtools/redis v1.2.1
@@ -72,7 +73,7 @@ require (
github.com/testcontainers/testcontainers-go/modules/k3s v0.42.0
github.com/tetratelabs/wazero v1.8.0
github.com/tidwall/gjson v1.17.0
github.com/traefik/grpc-web v0.17.0
github.com/traefik/grpc-web v0.18.0
github.com/traefik/paerser v0.2.2
github.com/traefik/traefik/dynamic/ext v0.0.0-00010101000000-000000000000
github.com/traefik/yaegi v0.16.1
@@ -80,7 +81,7 @@ require (
github.com/unrolled/render v1.0.2
github.com/unrolled/secure v1.0.9
github.com/valyala/fasthttp v1.73.0
github.com/vulcand/oxy/v2 v2.2.0
github.com/vulcand/oxy/v2 v2.3.0
github.com/vulcand/predicate v1.3.0
github.com/yuin/gopher-lua v1.1.1
go.opentelemetry.io/collector/pdata v1.41.0
@@ -140,7 +141,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.10.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0 // indirect
github.com/HdrHistogram/hdrhistogram-go v1.2.0 // indirect
github.com/HdrHistogram/hdrhistogram-go v1.4.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
@@ -248,7 +249,7 @@ require (
github.com/googleapis/gax-go/v2 v2.24.0 // indirect
github.com/gophercloud/gophercloud v1.14.1 // indirect
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect
github.com/gravitational/trace v1.5.1 // indirect
github.com/gravitational/trace v1.5.4 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect
github.com/hashicorp/cronexpr v1.1.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
+10 -10
View File
@@ -75,8 +75,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/HdrHistogram/hdrhistogram-go v1.2.0 h1:XMJkDWuz6bM9Fzy7zORuVFKH7ZJY41G2q8KWhVGkNiY=
github.com/HdrHistogram/hdrhistogram-go v1.2.0/go.mod h1:CiIeGiHSd06zjX+FypuEJ5EQ07KKtxZ+8J6hszwVQig=
github.com/HdrHistogram/hdrhistogram-go v1.4.0 h1:JdU0squ73hT0G45GVKzZ8KXS8ImiFuPHAA36aWaCfMM=
github.com/HdrHistogram/hdrhistogram-go v1.4.0/go.mod h1:CiIeGiHSd06zjX+FypuEJ5EQ07KKtxZ+8J6hszwVQig=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
@@ -622,8 +622,8 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/gravitational/trace v1.5.1 h1:CdSymAjkE1VOef+lsC5x29jX9WbgI0fBtnRqeT4Fh+c=
github.com/gravitational/trace v1.5.1/go.mod h1:sJKfJHIQ7IkG8kvYpFPEr6mj3WDEdZ0YAc7xAD8w7lw=
github.com/gravitational/trace v1.5.4 h1:nMmF7alwvNiQJhC817Hhmgm95bzKOXbnxvkickTkRDY=
github.com/gravitational/trace v1.5.4/go.mod h1:/uCbC3ukVU8Pdrh8+3vNLyyE1aGheBWpGGrMnwIK80E=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
@@ -776,8 +776,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.20.4-0.20261021124103-35c6f98a5cb9 h1:tltN12g/YvB18Pxz7JftcnN/yHpXAidEnQv4TKdCBJc=
github.com/klauspost/compress v1.20.4-0.20261021124103-35c6f98a5cb9/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
@@ -1257,8 +1257,8 @@ github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/traefik/grpc-web v0.17.0 h1:ZMPoEeH/8SqO0vl0NOs03aW6ky56NKKVsucLp9xMi+g=
github.com/traefik/grpc-web v0.17.0/go.mod h1:2jNLG2xOchSXr9/tUp8ZdqSvpeACPW3j7qGZrMwSfnw=
github.com/traefik/grpc-web v0.18.0 h1:rPRPcpPSUPatFfT6lgOigqF4jz3zCUfrFCSxuRG5QF0=
github.com/traefik/grpc-web v0.18.0/go.mod h1:2jNLG2xOchSXr9/tUp8ZdqSvpeACPW3j7qGZrMwSfnw=
github.com/traefik/paerser v0.2.2 h1:cpzW/ZrQrBh3mdwD/jnp6aXASiUFKOVr6ldP+keJTcQ=
github.com/traefik/paerser v0.2.2/go.mod h1:7BBDd4FANoVgaTZG+yh26jI6CA2nds7D/4VTEdIsh24=
github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E=
@@ -1297,8 +1297,8 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/volcengine/volc-sdk-golang v1.0.256 h1:UDSYn6iXipw8u/NuvN0XLzDiCxAi9zZT182poqZ3qq0=
github.com/volcengine/volc-sdk-golang v1.0.256/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM=
github.com/vulcand/oxy/v2 v2.2.0 h1:+H3on8BpcG9v712w2EO1LAQv+scHiPn1nSg3F6szxz0=
github.com/vulcand/oxy/v2 v2.2.0/go.mod h1:MSvNdC6DkalmCKy9t0jFra5t92H2a2hRhLl9MuH1K1M=
github.com/vulcand/oxy/v2 v2.3.0 h1:hi/zBnfuYILg7Q5HYBFbj0spAQ8t+1n7hg0elj54nus=
github.com/vulcand/oxy/v2 v2.3.0/go.mod h1:iIM9ASk0fRs926AtnLaKVWB+Y6qjHw1ZFLZ7WudM2gA=
github.com/vulcand/predicate v1.3.0 h1:jtNe4PHbLJ649dR7Gl+MSAzUhLGtLspAkWlSjoOiXg8=
github.com/vulcand/predicate v1.3.0/go.mod h1:opzv9MetRuMNnuoPeTSWtwzjcXsxQC00/fuWzkPTn4s=
github.com/vultr/govultr/v3 v3.33.0 h1:SD8y4tmoSXbpX137JT5k7jLxJ5j0WcnRtDbq/+P26IA=
@@ -0,0 +1,25 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[entryPoints]
[entryPoints.web]
address = ":8000"
[entryPoints.web.http]
maxHeaderValueCount = 10
[providers.file]
filename = "{{ .SelfFilename }}"
## dynamic configuration ##
[http.routers]
[http.routers.test-router]
entryPoints = ["web"]
service = "test-service"
rule = "Host(`127.0.0.1`)"
[http.services]
[http.services.test-service]
[[http.services.test-service.loadBalancer.servers]]
url = "{{ .TestServer }}"
+2 -4
View File
@@ -1468,10 +1468,8 @@ func (s *HTTPSSuite) modifyCertificateConfFileContent(certFileName, confFileName
TLS: &dynamic.TLSConfiguration{
Certificates: []*traefiktls.CertAndStores{
{
Certificate: traefiktls.Certificate{
CertFile: types.FileOrContent("fixtures/https/" + certFileName + ".cert"),
KeyFile: types.FileOrContent("fixtures/https/" + certFileName + ".key"),
},
CertFile: types.FileOrContent("fixtures/https/" + certFileName + ".cert"),
KeyFile: types.FileOrContent("fixtures/https/" + certFileName + ".key"),
},
},
},
+45
View File
@@ -14,6 +14,7 @@ import (
"net/http/httptest"
"os"
"regexp"
"strconv"
"strings"
"sync/atomic"
"syscall"
@@ -2438,6 +2439,50 @@ func (s *SimpleSuite) TestAllowACMEByPassRedirect() {
assert.Equal(s.T(), http.StatusMovedPermanently, resp.StatusCode)
}
func (s *SimpleSuite) TestMaxHeaderValueCount() {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()
file := s.adaptFile("fixtures/simple_max_header_value_count.toml", struct {
TestServer string
}{ts.URL})
s.traefikCmd(withConfigFile(file))
testCases := []struct {
name string
headerCount int
expectedStatus int
}{
{
name: "5 headers",
headerCount: 5,
expectedStatus: http.StatusOK,
},
{
name: "20 headers",
headerCount: 20,
expectedStatus: http.StatusRequestHeaderFieldsTooLarge,
},
}
for _, test := range testCases {
s.Run(test.name, func() {
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000", nil)
require.NoError(s.T(), err)
for i := range test.headerCount {
req.Header.Add("X-Header", strconv.Itoa(i))
}
err = try.Request(req, 2*time.Second, try.StatusCodeIs(test.expectedStatus))
require.NoError(s.T(), err)
})
}
}
func (s *SimpleSuite) TestUnderscoreHeadersStrategy() {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if _, ok := r.Header["X_auth_user"]; ok {
+10 -20
View File
@@ -537,10 +537,8 @@ func TestHandler_Certificates(t *testing.T) {
if test.setup.loadCerts {
dynamicConfigs := []*tlspkg.CertAndStores{{
Certificate: tlspkg.Certificate{
CertFile: localhostCert,
KeyFile: localhostKey,
},
CertFile: localhostCert,
KeyFile: localhostKey,
}}
tlsManager.UpdateConfigs(t.Context(), nil, nil, dynamicConfigs)
@@ -549,28 +547,20 @@ func TestHandler_Certificates(t *testing.T) {
if test.setup.loadMultipleCerts {
dynamicConfigs := []*tlspkg.CertAndStores{
{
Certificate: tlspkg.Certificate{
CertFile: localhostCert,
KeyFile: localhostKey,
},
CertFile: localhostCert,
KeyFile: localhostKey,
},
{
Certificate: tlspkg.Certificate{
CertFile: warnCert,
KeyFile: warnKey,
},
CertFile: warnCert,
KeyFile: warnKey,
},
{
Certificate: tlspkg.Certificate{
CertFile: expiredCert,
KeyFile: expiredKey,
},
CertFile: expiredCert,
KeyFile: expiredKey,
},
{
Certificate: tlspkg.Certificate{
CertFile: acmeCert,
KeyFile: acmeKey,
},
CertFile: acmeCert,
KeyFile: acmeKey,
},
}
+5
View File
@@ -67,6 +67,11 @@ type Middleware struct {
RewriteTarget *RewriteTarget `json:"rewriteTarget,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
UpstreamVHost *UpstreamVHost `json:"upstreamVHost,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
AppRoot *AppRoot `json:"appRoot,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
// URNRefs holds the values a provider read for the URN references of the middleware, keyed by the JSON Pointer of the field holding the reference.
// The values are only set when the middleware is built (see ResolveURNRefs), so the dynamic configuration only exposes the references.
// Only providers set it, and it must stay part of the configuration hash, so that a change of a referenced value reloads the middleware.
URNRefs map[string]string `json:"-" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"false" loggable:"false"`
}
// +k8s:deepcopy-gen=true
+197
View File
@@ -0,0 +1,197 @@
package dynamic
import (
"cmp"
"errors"
"fmt"
"maps"
"reflect"
"slices"
"strconv"
"strings"
)
// RFC 6901 escapes "~" as "~0" and "/" as "~1".
// A replacer works in a single pass, so it never reads its own output: "~01" decodes into "~1" and not into "/".
var (
jsonPointerEscaper = strings.NewReplacer("~", "~0", "/", "~1")
jsonPointerUnescaper = strings.NewReplacer("~1", "/", "~0", "~")
)
// JSONPointer returns the JSON Pointer (RFC 6901) made of the given reference tokens.
func JSONPointer(tokens ...string) string {
var b strings.Builder
for _, token := range tokens {
b.WriteByte('/')
b.WriteString(jsonPointerEscaper.Replace(token))
}
return b.String()
}
// ResolveURNRefs returns a copy of the middleware where each URN reference is replaced by its value.
// It returns the middleware itself when it has no URN reference.
func (m *Middleware) ResolveURNRefs() (*Middleware, error) {
if len(m.URNRefs) == 0 {
return m, nil
}
resolved := m.DeepCopy()
for _, pointer := range slices.Sorted(maps.Keys(m.URNRefs)) {
if _, err := resolved.swapURNRef(pointer, m.URNRefs[pointer]); err != nil {
return nil, fmt.Errorf("resolving URN reference %q: %w", pointer, err)
}
}
return resolved, nil
}
// RedactURNRefs returns err where each quoted value of a URN reference is replaced by its quoted URN.
// The decoding of a plugin configuration puts the invalid values in its errors, and the API and the log show these errors.
// Only the quoted forms are replaced, because replacing a value that matches a word of the message would reveal it.
func (m *Middleware) RedactURNRefs(err error) error {
if err == nil || len(m.URNRefs) == 0 {
return err
}
// Each field holds the URN of its value, and swapURNRef returns the string that it replaces.
// It changes a copy, because m is shared with the running configuration.
cp := m.DeepCopy()
urns := make(map[string]string, len(m.URNRefs))
for pointer, value := range m.URNRefs {
// A reference that fails to resolve gives its value to no builder, and an empty old string matches everywhere.
if urn, swapErr := cp.swapURNRef(pointer, value); swapErr == nil && value != "" {
urns[value] = urn
}
}
// A value can contain another value, so the longest values are replaced first.
values := slices.SortedFunc(maps.Keys(urns), func(a, b string) int { return cmp.Compare(len(b), len(a)) })
var oldnew []string
for _, value := range values {
// The decoding errors put the values between double quotes (strconv, time) or single quotes (mapstructure).
oldnew = append(oldnew,
strconv.Quote(value), strconv.Quote(urns[value]),
"'"+value+"'", "'"+urns[value]+"'",
)
}
msg := err.Error()
redacted := strings.NewReplacer(oldnew...).Replace(msg)
if redacted == msg {
return err
}
// The wrapped errors still contain the values, so the redacted error must not wrap them.
return errors.New(redacted)
}
// swapURNRef sets the value at the JSON Pointer, and returns the URN that it replaced.
func (m *Middleware) swapURNRef(pointer, value string) (urn string, err error) {
path, err := parseJSONPointer(pointer)
if err != nil {
return "", err
}
return swapStringAt(reflect.ValueOf(m).Elem(), path, value)
}
// swapStringAt follows the path from v, sets the string at its end to value, and returns the old string.
// v must be settable, so the caller gives a copy of the configuration.
func swapStringAt(v reflect.Value, path []string, value string) (old string, err error) {
switch v.Kind() {
case reflect.String:
if len(path) > 0 {
return "", fmt.Errorf("path continues after a string value: %q", path)
}
old = v.String()
v.SetString(value)
return old, nil
case reflect.Pointer:
if v.IsNil() {
return "", errors.New("nil value on the path")
}
return swapStringAt(v.Elem(), path, value)
case reflect.Interface:
if v.IsNil() {
return "", errors.New("nil value on the path")
}
// Go cannot change the value held by an interface in place, so swapStringAt changes a copy and stores it back.
elem := reflect.New(v.Elem().Type()).Elem()
elem.Set(v.Elem())
old, err = swapStringAt(elem, path, value)
v.Set(elem)
return old, err
}
// Pointers and interfaces lead to the next value without a token, so the path can only end on a string.
if len(path) == 0 {
return "", fmt.Errorf("value of type %s is not a string", v.Type())
}
switch v.Kind() {
case reflect.Struct:
// The pointers use the JSON names of the fields, so a field without such a name never matches.
for i := range v.NumField() {
field := v.Type().Field(i)
name, _, _ := strings.Cut(field.Tag.Get("json"), ",")
if field.IsExported() && name != "" && name != "-" && name == path[0] {
return swapStringAt(v.Field(i), path[1:], value)
}
}
return "", fmt.Errorf("unknown field %q", path[0])
case reflect.Map:
if v.Type().Key().Kind() != reflect.String {
return "", fmt.Errorf("unsupported map key of type %s on the path", v.Type().Key())
}
key := reflect.ValueOf(path[0]).Convert(v.Type().Key())
if !v.MapIndex(key).IsValid() {
return "", fmt.Errorf("unknown key %q", path[0])
}
// Go cannot change a map value in place, so swapStringAt changes a copy and stores it back.
elem := reflect.New(v.Type().Elem()).Elem()
elem.Set(v.MapIndex(key))
old, err = swapStringAt(elem, path[1:], value)
v.SetMapIndex(key, elem)
return old, err
case reflect.Slice:
index, err := strconv.Atoi(path[0])
// RFC 6901 only allows "0" or digits without a leading zero, so the index must be written as Itoa writes it.
if err != nil || strconv.Itoa(index) != path[0] || index < 0 || index >= v.Len() {
return "", fmt.Errorf("invalid array index %q", path[0])
}
return swapStringAt(v.Index(index), path[1:], value)
default:
return "", fmt.Errorf("unsupported value of type %s on the path", v.Type())
}
}
func parseJSONPointer(pointer string) ([]string, error) {
rest, ok := strings.CutPrefix(pointer, "/")
if !ok {
return nil, errors.New("JSON Pointer must start with a slash")
}
path := strings.Split(rest, "/")
for i, token := range path {
path[i] = jsonPointerUnescaper.Replace(token)
}
return path, nil
}
+395
View File
@@ -0,0 +1,395 @@
package dynamic
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"testing"
"github.com/mitchellh/hashstructure"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/types"
)
func TestJSONPointer(t *testing.T) {
t.Parallel()
testCases := []struct {
desc string
tokens []string
expected string
}{
{
desc: "no token is the whole document",
expected: "",
},
{
desc: "tokens are joined with slashes",
tokens: []string{"plugin", "my-plugin", "0"},
expected: "/plugin/my-plugin/0",
},
{
desc: "slashes and tildes in tokens are escaped",
tokens: []string{"a/b", "m~n", "~1"},
expected: "/a~1b/m~0n/~01",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
assert.Equal(t, test.expected, JSONPointer(test.tokens...))
})
}
}
func TestMiddleware_ResolveURNRefs(t *testing.T) {
t.Parallel()
testCases := []struct {
desc string
middleware *Middleware
expected *Middleware
expectedErr string
}{
{
desc: "nested plugin values are set",
middleware: &Middleware{
Plugin: map[string]PluginConf{
"my-plugin": {
"token": "urn:k8s:secret:creds:token",
"nested": map[string]any{
"a/b": "urn:k8s:secret:creds:a",
"list": []any{"keep", "urn:k8s:secret:creds:b"},
},
"count": float64(1),
},
},
URNRefs: map[string]string{
"/plugin/my-plugin/token": "token-value",
"/plugin/my-plugin/nested/a~1b": "a-value",
"/plugin/my-plugin/nested/list/1": "b-value",
},
},
expected: &Middleware{
Plugin: map[string]PluginConf{
"my-plugin": {
"token": "token-value",
"nested": map[string]any{
"a/b": "a-value",
"list": []any{"keep", "b-value"},
},
"count": float64(1),
},
},
URNRefs: map[string]string{
"/plugin/my-plugin/token": "token-value",
"/plugin/my-plugin/nested/a~1b": "a-value",
"/plugin/my-plugin/nested/list/1": "b-value",
},
},
},
{
desc: "escaped tilde followed by 1 is decoded once",
middleware: &Middleware{
Plugin: map[string]PluginConf{"my-plugin": {"~1": "urn:k8s:secret:creds:token"}},
URNRefs: map[string]string{"/plugin/my-plugin/~01": "token-value"},
},
expected: &Middleware{
Plugin: map[string]PluginConf{"my-plugin": {"~1": "token-value"}},
URNRefs: map[string]string{"/plugin/my-plugin/~01": "token-value"},
},
},
{
desc: "users, TLS key and Redis credentials are set",
middleware: &Middleware{
// The provider splits one Secret key into one user per line, so the users share the URN of the key.
BasicAuth: &BasicAuth{Users: Users{"urn:k8s:secret:auth:users", "urn:k8s:secret:auth:users"}, Realm: "realm"},
ForwardAuth: &ForwardAuth{Address: "http://auth", TLS: &ClientTLS{CA: "ca", Key: "urn:k8s:secret:tls:tls.key"}},
RateLimit: &RateLimit{Redis: &Redis{
Password: "urn:k8s:secret:redis:password",
TLS: &types.ClientTLS{Key: "urn:k8s:secret:redis-tls:tls.key"},
}},
URNRefs: map[string]string{
"/basicAuth/users/0": "user1:hash1",
"/basicAuth/users/1": "user2:hash2",
"/forwardAuth/tls/key": "key",
"/rateLimit/redis/password": "password",
"/rateLimit/redis/tls/key": "redis-key",
},
},
expected: &Middleware{
BasicAuth: &BasicAuth{Users: Users{"user1:hash1", "user2:hash2"}, Realm: "realm"},
ForwardAuth: &ForwardAuth{Address: "http://auth", TLS: &ClientTLS{CA: "ca", Key: "key"}},
RateLimit: &RateLimit{Redis: &Redis{
Password: "password",
TLS: &types.ClientTLS{Key: "redis-key"},
}},
URNRefs: map[string]string{
"/basicAuth/users/0": "user1:hash1",
"/basicAuth/users/1": "user2:hash2",
"/forwardAuth/tls/key": "key",
"/rateLimit/redis/password": "password",
"/rateLimit/redis/tls/key": "redis-key",
},
},
},
{
desc: "pointer without leading slash",
middleware: &Middleware{
BasicAuth: &BasicAuth{Users: Users{"urn"}},
URNRefs: map[string]string{"basicAuth/users/0": "value"},
},
expectedErr: `resolving URN reference "basicAuth/users/0": JSON Pointer must start with a slash`,
},
{
desc: "unknown field",
middleware: &Middleware{
BasicAuth: &BasicAuth{Users: Users{"urn"}},
URNRefs: map[string]string{"/basicAuth/unknown": "value"},
},
expectedErr: `resolving URN reference "/basicAuth/unknown": unknown field "unknown"`,
},
{
desc: "nil value on the path",
middleware: &Middleware{
ForwardAuth: &ForwardAuth{Address: "http://auth"},
URNRefs: map[string]string{"/forwardAuth/tls/key": "value"},
},
expectedErr: `resolving URN reference "/forwardAuth/tls/key": nil value on the path`,
},
{
desc: "unknown map key",
middleware: &Middleware{
Plugin: map[string]PluginConf{"my-plugin": {"token": "urn"}},
URNRefs: map[string]string{"/plugin/my-plugin/other": "value"},
},
expectedErr: `resolving URN reference "/plugin/my-plugin/other": unknown key "other"`,
},
{
desc: "array index with a leading zero",
middleware: &Middleware{
BasicAuth: &BasicAuth{Users: Users{"urn"}},
URNRefs: map[string]string{"/basicAuth/users/00": "value"},
},
expectedErr: `resolving URN reference "/basicAuth/users/00": invalid array index "00"`,
},
{
desc: "array index out of range",
middleware: &Middleware{
BasicAuth: &BasicAuth{Users: Users{"urn"}},
URNRefs: map[string]string{"/basicAuth/users/1": "value"},
},
expectedErr: `resolving URN reference "/basicAuth/users/1": invalid array index "1"`,
},
{
desc: "value that is not a string",
middleware: &Middleware{
Plugin: map[string]PluginConf{"my-plugin": {"count": float64(1)}},
URNRefs: map[string]string{"/plugin/my-plugin/count": "value"},
},
expectedErr: `resolving URN reference "/plugin/my-plugin/count": value of type float64 is not a string`,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
original := test.middleware.DeepCopy()
resolved, err := test.middleware.ResolveURNRefs()
assert.Equal(t, original, test.middleware)
if test.expectedErr != "" {
require.EqualError(t, err, test.expectedErr)
return
}
require.NoError(t, err)
assert.Equal(t, test.expected, resolved)
})
}
}
func TestMiddleware_ResolveURNRefs_withoutRefs(t *testing.T) {
t.Parallel()
middleware := &Middleware{BasicAuth: &BasicAuth{Users: Users{"user:hash"}}}
resolved, err := middleware.ResolveURNRefs()
require.NoError(t, err)
assert.Same(t, middleware, resolved)
}
func Test_swapStringAt(t *testing.T) {
t.Parallel()
type item struct {
Name string `json:"name"`
NoTag string
}
testCases := []struct {
desc string
value any
pointer string
expected any
expectedOld string
expectedErr string
}{
{
desc: "struct held by an interface is set",
value: map[string]any{"item": item{Name: "urn"}},
pointer: "/item/name",
expected: map[string]any{"item": item{Name: "value"}},
expectedOld: "urn",
},
{
desc: "array index with a plus sign",
value: &[]string{"urn", "urn"},
pointer: "/+1",
expectedErr: `invalid array index "+1"`,
},
{
desc: "empty token does not match a field without JSON name",
value: &item{},
pointer: "/",
expectedErr: `unknown field ""`,
},
{
desc: "map key that is not a string",
value: map[int]string{1: "urn"},
pointer: "/1",
expectedErr: "unsupported map key of type int on the path",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
tokens, err := parseJSONPointer(test.pointer)
require.NoError(t, err)
old, err := swapStringAt(reflect.ValueOf(test.value), tokens, "value")
if test.expectedErr != "" {
require.EqualError(t, err, test.expectedErr)
return
}
require.NoError(t, err)
assert.Equal(t, test.expectedOld, old)
assert.Equal(t, test.expected, test.value)
})
}
}
func TestMiddleware_RedactURNRefs(t *testing.T) {
t.Parallel()
testCases := []struct {
desc string
pluginConf PluginConf
urnRefs map[string]string
err error
expected string
}{
{
desc: "double quoted value is replaced with its double quoted URN",
pluginConf: PluginConf{"token": "urn:k8s:secret:creds:token"},
urnRefs: map[string]string{"/plugin/my-plugin/token": "s3cr3t"},
err: errors.New(`strconv.ParseInt: parsing "s3cr3t": invalid syntax`),
expected: `strconv.ParseInt: parsing "urn:k8s:secret:creds:token": invalid syntax`,
},
{
desc: "double quoted value with escaped characters is replaced with its double quoted URN",
pluginConf: PluginConf{"token": "urn:k8s:secret:creds:token"},
urnRefs: map[string]string{"/plugin/my-plugin/token": `s3"cr\3t`},
err: fmt.Errorf("parsing %q: invalid syntax", `s3"cr\3t`),
expected: `parsing "urn:k8s:secret:creds:token": invalid syntax`,
},
{
desc: "single quoted value is replaced with its single quoted URN",
pluginConf: PluginConf{"token": "urn:k8s:secret:creds:token"},
urnRefs: map[string]string{"/plugin/my-plugin/token": "s3cr3t"},
err: errors.New("'token' expected type 'int', got unconvertible type 'string', value: 's3cr3t'"),
expected: "'token' expected type 'int', got unconvertible type 'string', value: 'urn:k8s:secret:creds:token'",
},
{
desc: "value that is not quoted is not replaced",
pluginConf: PluginConf{"password": "urn:k8s:secret:redis:password"},
urnRefs: map[string]string{"/plugin/my-plugin/password": "redis"},
err: errors.New("dial tcp: lookup redis: no such host"),
expected: "dial tcp: lookup redis: no such host",
},
{
desc: "value that contains another value is replaced first",
pluginConf: PluginConf{"a": "urn:k8s:secret:creds:a", "b": "urn:k8s:secret:creds:b"},
urnRefs: map[string]string{"/plugin/my-plugin/a": "ab", "/plugin/my-plugin/b": "ab'c"},
err: errors.New("value: 'ab'c'"),
expected: "value: 'urn:k8s:secret:creds:b'",
},
{
desc: "empty value is ignored",
urnRefs: map[string]string{"/a": ""},
err: errors.New("error"),
expected: "error",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
middleware := &Middleware{URNRefs: test.urnRefs}
if test.pluginConf != nil {
middleware.Plugin = map[string]PluginConf{"my-plugin": test.pluginConf}
}
assert.EqualError(t, middleware.RedactURNRefs(test.err), test.expected)
})
}
}
func TestMiddleware_RedactURNRefs_withoutValue(t *testing.T) {
t.Parallel()
middleware := &Middleware{URNRefs: map[string]string{"/basicAuth/users/0": "user:hash"}}
err := fmt.Errorf("building: %w", errors.ErrUnsupported)
assert.Same(t, err, middleware.RedactURNRefs(err))
}
func TestMiddleware_URNRefs_json(t *testing.T) {
t.Parallel()
middleware := &Middleware{
BasicAuth: &BasicAuth{Users: Users{"urn:k8s:secret:auth:users"}},
URNRefs: map[string]string{"/basicAuth/users/0": "user:hash"},
}
data, err := json.Marshal(middleware)
require.NoError(t, err)
assert.JSONEq(t, `{"basicAuth":{"users":["urn:k8s:secret:auth:users"]}}`, string(data))
}
func TestMiddleware_URNRefs_hash(t *testing.T) {
t.Parallel()
hash := func(value string) uint64 {
h, err := hashstructure.Hash(&Middleware{URNRefs: map[string]string{"/basicAuth/users/0": value}}, nil)
require.NoError(t, err)
return h
}
// The providers skip a configuration with an unchanged hash, so a new Secret value must change the hash.
assert.NotEqual(t, hash("user:hash1"), hash("user:hash2"))
}
@@ -1226,6 +1226,13 @@ func (in *Middleware) DeepCopyInto(out *Middleware) {
*out = new(AppRoot)
**out = **in
}
if in.URNRefs != nil {
in, out := &in.URNRefs, &out.URNRefs
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
return
}
+2
View File
@@ -102,6 +102,7 @@ type HTTPConfig struct {
EncodeQuerySemicolons bool `description:"Defines whether request query semicolons should be URLEncoded." json:"encodeQuerySemicolons,omitempty" toml:"encodeQuerySemicolons,omitempty" yaml:"encodeQuerySemicolons,omitempty" export:"true"`
SanitizePath *bool `description:"Defines whether to enable request path sanitization (removal of /./, /../ and multiple slash sequences)." json:"sanitizePath,omitempty" toml:"sanitizePath,omitempty" yaml:"sanitizePath,omitempty" export:"true"`
MaxHeaderBytes int `description:"Maximum size of request headers in bytes." json:"maxHeaderBytes,omitempty" toml:"maxHeaderBytes,omitempty" yaml:"maxHeaderBytes,omitempty" export:"true"`
MaxHeaderValueCount int `description:"Maximum number of request header values." json:"maxHeaderValueCount,omitempty" toml:"maxHeaderValueCount,omitempty" yaml:"maxHeaderValueCount,omitempty" export:"true"`
AliasHeadersStrategy string `description:"Defines the strategy to handle the requests carrying a header whose name aliases another header name (keep, delete, and reject)." json:"aliasHeadersStrategy,omitempty" toml:"aliasHeadersStrategy,omitempty" yaml:"aliasHeadersStrategy,omitempty" export:"true"`
// UnderscoreHeadersStrategy is the strategy to handle request headers with underscores in their names.
@@ -114,6 +115,7 @@ type HTTPConfig struct {
func (h *HTTPConfig) SetDefaults() {
h.SanitizePath = new(true)
h.MaxHeaderBytes = http.DefaultMaxHeaderBytes
h.MaxHeaderValueCount = http.DefaultMaxHeaderValueCount
}
// EncodedCharacters configures which encoded characters are allowed in the request path.
+15 -10
View File
@@ -222,8 +222,9 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
ProxyProtocol: nil,
ForwardedHeaders: &ForwardedHeaders{},
HTTP: HTTPConfig{
SanitizePath: new(true),
MaxHeaderBytes: 1048576,
SanitizePath: new(true),
MaxHeaderBytes: 1048576,
MaxHeaderValueCount: 500,
},
HTTP2: &HTTP2Config{
MaxConcurrentStreams: 250,
@@ -270,8 +271,9 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
ProxyProtocol: nil,
ForwardedHeaders: &ForwardedHeaders{},
HTTP: HTTPConfig{
SanitizePath: new(true),
MaxHeaderBytes: 1048576,
SanitizePath: new(true),
MaxHeaderBytes: 1048576,
MaxHeaderValueCount: 500,
},
HTTP2: &HTTP2Config{
MaxConcurrentStreams: 250,
@@ -329,8 +331,9 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
ProxyProtocol: nil,
ForwardedHeaders: &ForwardedHeaders{},
HTTP: HTTPConfig{
SanitizePath: new(true),
MaxHeaderBytes: 1048576,
SanitizePath: new(true),
MaxHeaderBytes: 1048576,
MaxHeaderValueCount: 500,
},
HTTP2: &HTTP2Config{
MaxConcurrentStreams: 250,
@@ -392,8 +395,9 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
ProxyProtocol: nil,
ForwardedHeaders: &ForwardedHeaders{},
HTTP: HTTPConfig{
SanitizePath: new(true),
MaxHeaderBytes: 1048576,
SanitizePath: new(true),
MaxHeaderBytes: 1048576,
MaxHeaderValueCount: 500,
},
HTTP2: &HTTP2Config{
MaxConcurrentStreams: 250,
@@ -503,8 +507,9 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
},
ForwardedHeaders: &ForwardedHeaders{},
HTTP: HTTPConfig{
SanitizePath: new(true),
MaxHeaderBytes: 1048576,
SanitizePath: new(true),
MaxHeaderBytes: 1048576,
MaxHeaderValueCount: 500,
},
HTTP2: &HTTP2Config{
MaxConcurrentStreams: 250,
+3 -13
View File
@@ -1,7 +1,6 @@
package healthcheck
import (
"bufio"
"context"
"errors"
"fmt"
@@ -483,16 +482,7 @@ func (c *codeCatcher) Write(bytes []byte) (int, error) {
return c.ResponseWriter.Write(bytes)
}
func (c *codeCatcher) Flush() {
if flusher, ok := c.ResponseWriter.(http.Flusher); ok {
flusher.Flush()
}
}
func (c *codeCatcher) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if h, ok := c.ResponseWriter.(http.Hijacker); ok {
return h.Hijack()
}
return nil, nil, fmt.Errorf("not a hijacker: %T", c.ResponseWriter)
// Unwrap exposes the underlying ResponseWriter for http.ResponseController.
func (c *codeCatcher) Unwrap() http.ResponseWriter {
return c.ResponseWriter
}
+5 -8
View File
@@ -1603,13 +1603,6 @@ func doLoggingWithAbortedStream(t *testing.T, config *otypes.AccessLog) {
}
func streamBackend(rw http.ResponseWriter, r *http.Request) {
// Get the Flusher to flush the response to the client
flusher, ok := rw.(http.Flusher)
if !ok {
http.Error(rw, "Streaming unsupported!", http.StatusInternalServerError)
return
}
// Set the headers for streaming
rw.Header().Set("Content-Type", "text/plain")
rw.Header().Set("Transfer-Encoding", "chunked")
@@ -1627,7 +1620,11 @@ func streamBackend(rw http.ResponseWriter, r *http.Request) {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
flusher.Flush()
rc := http.NewResponseController(rw)
if err := rc.Flush(); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
}
}
}
+9 -2
View File
@@ -1,6 +1,7 @@
package auth
import (
"fmt"
"os"
"strings"
)
@@ -19,11 +20,17 @@ func getUsers(fileName string, appendUsers []string, parser UserParser) (map[str
return nil, err
}
// loadUsers puts the users of the file before the users of the list.
fileUsers := len(users) - len(appendUsers)
userMap := make(map[string]string)
for _, user := range users {
for i, user := range users {
userName, userHash, err := parser(user)
if err != nil {
return nil, err
if i < fileUsers {
return nil, fmt.Errorf("parsing user %d of %s: %w", i+1, fileName, err)
}
return nil, fmt.Errorf("parsing user at index %d: %w", i-fileUsers, err)
}
userMap[userName] = userHash
}
+3 -1
View File
@@ -2,6 +2,7 @@ package auth
import (
"context"
"errors"
"fmt"
"maps"
"net/http"
@@ -152,7 +153,8 @@ func (b *basicAuth) secretBasic(user, _ string) string {
func basicUserParser(user string) (string, string, error) {
split := strings.Split(user, ":")
if len(split) != 2 {
return "", "", fmt.Errorf("error parsing BasicUser: %v", user)
// The user is not part of the error, as it may come from a secret.
return "", "", errors.New("expected user:hash format")
}
return split[0], split[1], nil
}
+40
View File
@@ -6,6 +6,7 @@ import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync"
"sync/atomic"
"testing"
@@ -26,6 +27,45 @@ func TestNewBasicEmpty(t *testing.T) {
require.Error(t, err)
}
func TestNewBasicInvalidUser(t *testing.T) {
validUsersFile := filepath.Join(t.TempDir(), "valid-users")
require.NoError(t, os.WriteFile(validUsersFile, []byte("test:hash\ntest2:hash\n"), 0o600))
usersFile := filepath.Join(t.TempDir(), "users")
require.NoError(t, os.WriteFile(usersFile, []byte("# comment\n\ntest:hash\nnot-a-user\n"), 0o600))
testCases := []struct {
desc string
auth dynamic.BasicAuth
expectedErr string
}{
{
desc: "invalid user in the list",
auth: dynamic.BasicAuth{Users: []string{"test:hash", "not-a-user"}},
expectedErr: "parsing user at index 1: expected user:hash format",
},
{
desc: "index in the list does not count the users of the file",
auth: dynamic.BasicAuth{UsersFile: validUsersFile, Users: []string{"test3:hash", "not-a-user"}},
expectedErr: "parsing user at index 1: expected user:hash format",
},
{
desc: "invalid user in the file",
auth: dynamic.BasicAuth{UsersFile: usersFile},
expectedErr: fmt.Sprintf("parsing user 2 of %s: expected user:hash format", usersFile),
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
_, err := NewBasic(t.Context(), nil, test.auth, "authName")
assert.EqualError(t, err, test.expectedErr)
})
}
}
func TestNewBasicNotFoundSecretIsSet(t *testing.T) {
auth := dynamic.BasicAuth{
Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/"},
+3 -2
View File
@@ -2,7 +2,7 @@ package auth
import (
"context"
"fmt"
"errors"
"net/http"
"net/url"
"strings"
@@ -122,7 +122,8 @@ func (d *digestAuth) secretDigest(user, realm string) string {
func digestUserParser(user string) (string, string, error) {
split := strings.Split(user, ":")
if len(split) != 3 {
return "", "", fmt.Errorf("error parsing DigestUser: %v", user)
// The user is not part of the error, as it may come from a secret.
return "", "", errors.New("expected user:realm:hash format")
}
return split[0] + ":" + split[1], split[2], nil
}
+2 -2
View File
@@ -20,10 +20,10 @@ func TestDigestAuthError(t *testing.T) {
})
auth := dynamic.DigestAuth{
Users: []string{"test"},
Users: []string{"test:traefik:a2688e031edb4be6a3797f3882655c05", "test"},
}
_, err := NewDigest(t.Context(), next, auth, "authName")
assert.Error(t, err)
assert.EqualError(t, err, "parsing user at index 1: expected user:realm:hash format")
}
func TestDigestAuthFail(t *testing.T) {
+2 -6
View File
@@ -598,12 +598,12 @@ func TestForwardAuthFailResponseHeaders(t *testing.T) {
func TestForwardAuthClientClosedRequest(t *testing.T) {
requestStarted := make(chan struct{})
requestCancelled := make(chan struct{})
responseComplete := make(chan struct{})
authTs := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
close(requestStarted)
<-requestCancelled
// Wait for the client to give up on the request, so the auth server never answers before the cancellation is observed.
<-r.Context().Done()
}))
t.Cleanup(authTs.Close)
@@ -629,11 +629,7 @@ func TestForwardAuthClientClosedRequest(t *testing.T) {
<-requestStarted
// Cancel request and wait for cancellation to propagate
cancel()
<-ctx.Done()
close(requestCancelled)
<-responseComplete
@@ -7,6 +7,7 @@ import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -68,3 +69,42 @@ func TestBuffering(t *testing.T) {
})
}
}
// deadlineRecorder records the write deadline set through http.ResponseController.
type deadlineRecorder struct {
*httptest.ResponseRecorder
writeDeadline time.Time
}
func (r *deadlineRecorder) SetWriteDeadline(deadline time.Time) error {
r.writeDeadline = deadline
return nil
}
func TestBuffering_responseController(t *testing.T) {
deadline := time.Now().Add(time.Minute)
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rc := http.NewResponseController(rw)
assert.NoError(t, rc.SetWriteDeadline(deadline))
rw.WriteHeader(http.StatusServiceUnavailable)
assert.ErrorIs(t, rc.Flush(), http.ErrNotSupported)
_, err := rw.Write([]byte("unavailable"))
assert.NoError(t, err)
})
buffMiddleware, err := New(t.Context(), next, dynamic.Buffering{}, "foo")
require.NoError(t, err)
recorder := &deadlineRecorder{ResponseRecorder: httptest.NewRecorder()}
buffMiddleware.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "http://localhost", nil))
assert.Equal(t, deadline, recorder.writeDeadline)
assert.False(t, recorder.Flushed)
assert.Equal(t, http.StatusServiceUnavailable, recorder.Code)
assert.Equal(t, "unavailable", recorder.Body.String())
}
+3 -19
View File
@@ -24,15 +24,10 @@
package capture
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"github.com/traefik/traefik/v3/pkg/middlewares"
)
type key string
@@ -144,8 +139,6 @@ func (r *readCounter) Close() error {
return r.source.Close()
}
var _ middlewares.Stateful = &captureResponseWriter{}
// captureResponseWriter is a wrapper of type http.ResponseWriter
// that tracks response status and size.
type captureResponseWriter struct {
@@ -182,16 +175,7 @@ func (crw *captureResponseWriter) WriteHeader(s int) {
crw.status = s
}
func (crw *captureResponseWriter) Flush() {
if f, ok := crw.rw.(http.Flusher); ok {
f.Flush()
}
}
func (crw *captureResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if h, ok := crw.rw.(http.Hijacker); ok {
return h.Hijack()
}
return nil, nil, fmt.Errorf("not a hijacker: %T", crw.rw)
// Unwrap exposes the underlying ResponseWriter for http.ResponseController.
func (crw *captureResponseWriter) Unwrap() http.ResponseWriter {
return crw.rw
}
+1 -1
View File
@@ -501,7 +501,7 @@ func TestShouldWriteHeaderWhenFlush(t *testing.T) {
rw.Header().Add(contentEncodingHeader, gzipName)
rw.Header().Add(varyHeader, acceptEncodingHeader)
rw.WriteHeader(http.StatusUnauthorized)
rw.(http.Flusher).Flush()
assert.NoError(t, http.NewResponseController(rw).Flush())
_, err := rw.Write([]byte("short"))
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
+25 -22
View File
@@ -307,25 +307,21 @@ func (r *responseWriter) Write(p []byte) (int, error) {
return r.compressionWriter.Write(p)
}
// Flush flushes data to the appropriate underlying writer(s), although it does
// FlushError flushes data to the appropriate underlying writer(s), although it does
// not guarantee that all buffered data will be sent.
// If not enough bytes have been written to determine whether to enable compression,
// no flushing will take place.
func (r *responseWriter) Flush() {
func (r *responseWriter) FlushError() (err error) {
if !r.seenData {
// we should not flush if there never was any data, because flushing the bw
// (just like closing) would send some extra end of compressionStarted stream bytes.
return
return nil
}
// It was already established by Write that compression is disabled, we only
// have to flush the uncompressed writer.
if r.compressionDisabled {
if rw, ok := r.rw.(http.Flusher); ok {
rw.Flush()
}
return
return http.NewResponseController(r.rw).Flush()
}
// Here, nothing was ever written either to rw or to bw (since we're still
@@ -349,36 +345,43 @@ func (r *responseWriter) Flush() {
// because we also ignore the error returned by Write anyway
_ = r.compressionWriter.Flush()
if rw, ok := r.rw.(http.Flusher); ok {
rw.Flush()
}
err = http.NewResponseController(r.rw).Flush()
}()
// We empty whatever is left of the buffer that Write never took care of.
n, err := r.compressionWriter.Write(r.buf)
if err != nil {
return
n, werr := r.compressionWriter.Write(r.buf)
if werr != nil {
return nil
}
// And just like in Write we also handle "short writes".
if n < len(r.buf) {
r.buf = r.buf[n:]
return
return nil
}
r.buf = r.buf[:0]
return nil
}
func (r *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hijacker, ok := r.rw.(http.Hijacker); ok {
// We only make use of r.hijacked in close (and not in Write/WriteHeader)
// because we want to let the stdlib catch the error on writes, as
// they already do a good job of logging it.
r.hijacked = true
return hijacker.Hijack()
conn, brw, err := http.NewResponseController(r.rw).Hijack()
if errors.Is(err, http.ErrNotSupported) {
return nil, nil, err
}
return nil, nil, fmt.Errorf("%T is not a http.Hijacker", r.rw)
// We only make use of r.hijacked in close (and not in Write/WriteHeader)
// because we want to let the stdlib catch the error on writes, as
// they already do a good job of logging it.
r.hijacked = true
return conn, brw, err
}
// Unwrap exposes the underlying ResponseWriter for http.ResponseController.
func (r *responseWriter) Unwrap() http.ResponseWriter {
return r.rw
}
// close closes the underlying writers if/when appropriate.
@@ -268,7 +268,7 @@ func Test_FlushBeforeWrite(t *testing.T) {
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
rw.(http.Flusher).Flush()
assert.NoError(t, http.NewResponseController(rw).Flush())
_, err := rw.Write(bigTestBody)
require.NoError(t, err)
@@ -336,7 +336,7 @@ func Test_FlushAfterWrite(t *testing.T) {
_, err := rw.Write(bigTestBody[0:1])
require.NoError(t, err)
rw.(http.Flusher).Flush()
assert.NoError(t, http.NewResponseController(rw).Flush())
for _, b := range bigTestBody[1:] {
_, err := rw.Write([]byte{b})
require.NoError(t, err)
@@ -398,7 +398,7 @@ func Test_FlushAfterWriteNil(t *testing.T) {
_, err := rw.Write(nil)
require.NoError(t, err)
rw.(http.Flusher).Flush()
assert.NoError(t, http.NewResponseController(rw).Flush())
})
srv := httptest.NewServer(mustNewCompressionHandler(t, test.cfg, test.algo, next))
@@ -456,7 +456,7 @@ func Test_FlushAfterAllWrites(t *testing.T) {
_, err := rw.Write(bigTestBody[i : i+1])
require.NoError(t, err)
}
rw.(http.Flusher).Flush()
assert.NoError(t, http.NewResponseController(rw).Flush())
})
srv := httptest.NewServer(mustNewCompressionHandler(t, test.cfg, test.algo, next))
@@ -523,7 +523,7 @@ func Test_FlushForceCompress(t *testing.T) {
_, err := rw.Write(smallTestBody)
require.NoError(t, err)
rw.(http.Flusher).Flush()
assert.NoError(t, http.NewResponseController(rw).Flush())
})
srv := httptest.NewServer(mustNewCompressionHandler(t, test.cfg, test.algo, next))
@@ -867,7 +867,7 @@ func Test_FlushExcludedContentTypes(t *testing.T) {
require.NoError(t, err)
// Flush between each write
rw.(http.Flusher).Flush()
assert.NoError(t, http.NewResponseController(rw).Flush())
tb = tb[toWrite:]
}
})
@@ -988,7 +988,7 @@ func Test_FlushIncludedContentTypes(t *testing.T) {
require.NoError(t, err)
// Flush between each write
rw.(http.Flusher).Flush()
assert.NoError(t, http.NewResponseController(rw).Flush())
tb = tb[toWrite:]
}
})
+17 -36
View File
@@ -1,11 +1,9 @@
package customerrors
import (
"bufio"
"context"
"fmt"
"maps"
"net"
"net/http"
"net/url"
"strconv"
@@ -19,12 +17,6 @@ import (
"k8s.io/utils/ptr"
)
// Compile time validation that the response recorder implements http interfaces correctly.
var (
_ middlewares.Stateful = &codeModifier{}
_ middlewares.Stateful = &codeCatcher{}
)
const (
typeName = "CustomError"
schemeHTTP = "http"
@@ -293,16 +285,8 @@ func (cc *codeCatcher) WriteHeader(code int) {
cc.headersSent = true
}
// Hijack hijacks the connection.
func (cc *codeCatcher) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hj, ok := cc.responseWriter.(http.Hijacker); ok {
return hj.Hijack()
}
return nil, nil, fmt.Errorf("%T is not a http.Hijacker", cc.responseWriter)
}
// Flush sends any buffered data to the client.
func (cc *codeCatcher) Flush() {
// FlushError sends any buffered data to the client.
func (cc *codeCatcher) FlushError() error {
// If WriteHeader was already called from the caller, this is a NOOP.
// Otherwise, cc.code is actually a 200 here.
cc.WriteHeader(cc.code)
@@ -313,12 +297,15 @@ func (cc *codeCatcher) Flush() {
// (e.g., To prevent superfluous WriteHeader on request with a
// `Transfer-Encoding: chunked` header).
if cc.caughtFilteredCode {
return
return nil
}
if flusher, ok := cc.responseWriter.(http.Flusher); ok {
flusher.Flush()
}
return http.NewResponseController(cc.responseWriter).Flush()
}
// Unwrap exposes the underlying ResponseWriter for http.ResponseController.
func (cc *codeCatcher) Unwrap() http.ResponseWriter {
return cc.responseWriter
}
func (cc *codeCatcher) getCode() int {
@@ -398,20 +385,14 @@ func (r *codeModifier) WriteHeader(code int) {
r.headerSent = true
}
// Hijack hijacks the connection.
func (r *codeModifier) Hijack() (net.Conn, *bufio.ReadWriter, error) {
hijacker, ok := r.responseWriter.(http.Hijacker)
if !ok {
return nil, nil, fmt.Errorf("%T is not a http.Hijacker", r.responseWriter)
}
return hijacker.Hijack()
}
// Flush sends any buffered data to the client.
func (r *codeModifier) Flush() {
// FlushError sends any buffered data to the client.
func (r *codeModifier) FlushError() error {
r.WriteHeader(r.code)
if flusher, ok := r.responseWriter.(http.Flusher); ok {
flusher.Flush()
}
return http.NewResponseController(r.responseWriter).Flush()
}
// Unwrap exposes the underlying ResponseWriter for http.ResponseController.
func (r *codeModifier) Unwrap() http.ResponseWriter {
return r.responseWriter
}
@@ -1,7 +1,6 @@
package snippet
import (
"bufio"
"context"
"errors"
"fmt"
@@ -555,21 +554,6 @@ func (w *snippetResponseWriter) Unwrap() http.ResponseWriter {
return w.ResponseWriter
}
// Flush implements http.Flusher.
func (w *snippetResponseWriter) Flush() {
if f, ok := w.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}
// Hijack implements http.Hijacker.
func (w *snippetResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if h, ok := w.ResponseWriter.(http.Hijacker); ok {
return h.Hijack()
}
return nil, nil, fmt.Errorf("not a hijacker: %T", w.ResponseWriter)
}
// WriteResponse writes the final response based on the action context.
// For redirect status codes (301, 302, 303, 307, 308) with a URL, it performs an HTTP redirect.
// For other status codes, it writes the status code and optional body text.
+3 -12
View File
@@ -1,8 +1,6 @@
package observability
import (
"bufio"
"net"
"net/http"
)
@@ -28,14 +26,7 @@ func (s *statusCodeRecorder) Status() int {
return s.status
}
// Hijack hijacks the connection.
func (s *statusCodeRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return s.ResponseWriter.(http.Hijacker).Hijack()
}
// Flush sends any buffered data to the client.
func (s *statusCodeRecorder) Flush() {
if flusher, ok := s.ResponseWriter.(http.Flusher); ok {
flusher.Flush()
}
// Unwrap exposes the underlying ResponseWriter for http.ResponseController.
func (s *statusCodeRecorder) Unwrap() http.ResponseWriter {
return s.ResponseWriter
}
+5 -40
View File
@@ -1,10 +1,7 @@
package recovery
import (
"bufio"
"context"
"fmt"
"net"
"net/http"
"runtime"
@@ -30,13 +27,13 @@ func New(ctx context.Context, next http.Handler) (http.Handler, error) {
}
func (re *recovery) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
recoveryRW := newRecoveryResponseWriter(rw)
recoveryRW := &responseWriterWrapper{rw: rw}
defer recoverFunc(recoveryRW, req)
re.next.ServeHTTP(recoveryRW, req)
}
func recoverFunc(rw recoveryResponseWriter, req *http.Request) {
func recoverFunc(rw *responseWriterWrapper, req *http.Request) {
if err := recover(); err != nil {
defer rw.finalizeResponse()
@@ -61,21 +58,6 @@ func shouldLogPanic(panicValue any) bool {
return panicValue != nil && panicValue != http.ErrAbortHandler
}
type recoveryResponseWriter interface {
http.ResponseWriter
finalizeResponse()
}
func newRecoveryResponseWriter(rw http.ResponseWriter) recoveryResponseWriter {
wrapper := &responseWriterWrapper{rw: rw}
if _, ok := rw.(http.CloseNotifier); !ok {
return wrapper
}
return &responseWriterWrapperWithCloseNotify{wrapper}
}
type responseWriterWrapper struct {
rw http.ResponseWriter
headersSent bool
@@ -105,18 +87,9 @@ func (r *responseWriterWrapper) WriteHeader(code int) {
r.rw.WriteHeader(code)
}
func (r *responseWriterWrapper) Flush() {
if f, ok := r.rw.(http.Flusher); ok {
f.Flush()
}
}
func (r *responseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if h, ok := r.rw.(http.Hijacker); ok {
return h.Hijack()
}
return nil, nil, fmt.Errorf("not a hijacker: %T", r.rw)
// Unwrap exposes the underlying ResponseWriter for http.ResponseController.
func (r *responseWriterWrapper) Unwrap() http.ResponseWriter {
return r.rw
}
func (r *responseWriterWrapper) finalizeResponse() {
@@ -130,11 +103,3 @@ func (r *responseWriterWrapper) finalizeResponse() {
// we can safely return a fresh new error response.
http.Error(r.rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
type responseWriterWrapperWithCloseNotify struct {
*responseWriterWrapper
}
func (r *responseWriterWrapperWithCloseNotify) CloseNotify() <-chan bool {
return r.rw.(http.CloseNotifier).CloseNotify()
}
+3 -17
View File
@@ -1,9 +1,6 @@
package middlewares
import (
"bufio"
"fmt"
"net"
"net/http"
"github.com/rs/zerolog/log"
@@ -88,18 +85,7 @@ func (r *ResponseModifier) Write(b []byte) (int, error) {
return r.rw.Write(b)
}
// Hijack hijacks the connection.
func (r *ResponseModifier) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if h, ok := r.rw.(http.Hijacker); ok {
return h.Hijack()
}
return nil, nil, fmt.Errorf("not a hijacker: %T", r.rw)
}
// Flush sends any buffered data to the client.
func (r *ResponseModifier) Flush() {
if flusher, ok := r.rw.(http.Flusher); ok {
flusher.Flush()
}
// Unwrap exposes the underlying ResponseWriter for http.ResponseController.
func (r *ResponseModifier) Unwrap() http.ResponseWriter {
return r.rw
}
+22 -11
View File
@@ -26,9 +26,6 @@ import (
"go.opentelemetry.io/otel/trace"
)
// Compile time validation that the response writer implements http interfaces correctly.
var _ middlewares.Stateful = &responseWriter{}
const typeName = "Retry"
// Listener is used to inform about retry attempts.
@@ -368,23 +365,37 @@ func (r *responseWriter) WriteHeader(code int) {
r.written = true
}
// FlushError sends any buffered data to the client.
func (r *responseWriter) FlushError() error {
if !r.written {
r.WriteHeader(http.StatusOK)
}
// The response is discarded for a retry,
// and flushing it would commit an implicit 200 to the client.
if r.shouldNotWrite {
return nil
}
return http.NewResponseController(r.responseWriter).Flush()
}
func (r *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
hijacker, ok := r.responseWriter.(http.Hijacker)
if !ok {
return nil, nil, fmt.Errorf("%T is not a http.Hijacker", r.responseWriter)
conn, brw, err := http.NewResponseController(r.responseWriter).Hijack()
if errors.Is(err, http.ErrNotSupported) {
return nil, nil, err
}
// When the connection is Hijacked when using Websockets, we don't want to retry anymore,
// so we set the hijacked flag to true.
r.hijacked = true
return hijacker.Hijack()
return conn, brw, err
}
func (r *responseWriter) Flush() {
if flusher, ok := r.responseWriter.(http.Flusher); ok {
flusher.Flush()
}
// Unwrap exposes the underlying ResponseWriter for http.ResponseController.
func (r *responseWriter) Unwrap() http.ResponseWriter {
return r.responseWriter
}
type peekedBody struct {
+16
View File
@@ -481,6 +481,7 @@ func TestRetryHTTPStatusCodes(t *testing.T) {
requestBody string
responseDelay time.Duration
amountOfTCPFailures int
flush bool
wantRetryAttempts int
wantResponseStatus int
}{
@@ -508,6 +509,17 @@ func TestRetryHTTPStatusCodes(t *testing.T) {
wantRetryAttempts: 3,
wantResponseStatus: http.StatusOK,
},
{
desc: "retry on 503 status code with flush",
config: dynamic.Retry{
Attempts: 3,
Status: []string{"503"},
},
responseStatusCodes: []int{http.StatusServiceUnavailable, http.StatusNotFound},
flush: true,
wantRetryAttempts: 1,
wantResponseStatus: http.StatusNotFound,
},
{
desc: "retry on multiple specific status codes",
config: dynamic.Retry{
@@ -690,6 +702,10 @@ func TestRetryHTTPStatusCodes(t *testing.T) {
callCount++
rw.WriteHeader(statusCode)
if test.flush {
require.NoError(t, http.NewResponseController(rw).Flush())
}
})
retryListener := &countingRetryListener{}
-11
View File
@@ -1,11 +0,0 @@
package middlewares
import "net/http"
// Stateful interface groups all http interfaces that must be
// implemented by a stateful middleware (ie: recorders).
type Stateful interface {
http.ResponseWriter
http.Hijacker
http.Flusher
}
+9 -15
View File
@@ -9,12 +9,8 @@ import (
"net"
"net/http"
"time"
"github.com/traefik/traefik/v3/pkg/middlewares"
)
var _ middlewares.Stateful = &responseCapturer{}
// responseCapturer captures the response for the record.
// While buffering, nothing reaches the client until serve, so that the record is sent first.
// The response is buffered until its record is complete, then sent, and the rest of the response is streamed.
@@ -127,29 +123,22 @@ func (r *responseCapturer) Write(p []byte) (int, error) {
return r.rw.Write(p)
}
func (r *responseCapturer) Flush() {
func (r *responseCapturer) FlushError() error {
if r.buffering {
return
return nil
}
if f, ok := r.rw.(http.Flusher); ok {
f.Flush()
}
return http.NewResponseController(r.rw).Flush()
}
func (r *responseCapturer) Hijack() (net.Conn, *bufio.ReadWriter, error) {
h, ok := r.rw.(http.Hijacker)
if !ok {
return nil, nil, fmt.Errorf("not a hijacker: %T", r.rw)
}
// A hijacked connection is out of our control, so the response is served first,
// as it should not be buffered anymore.
if err := r.serve(); err != nil {
return nil, nil, err
}
conn, brw, err := h.Hijack()
conn, brw, err := http.NewResponseController(r.rw).Hijack()
// The status is then written on the hijacked connection, which the reverse proxy only does for a 101 Switching Protocols.
if err == nil && r.status == 0 {
@@ -159,6 +148,11 @@ func (r *responseCapturer) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return conn, brw, err
}
// Unwrap exposes the underlying ResponseWriter for http.ResponseController.
func (r *responseCapturer) Unwrap() http.ResponseWriter {
return r.rw
}
// capture keeps at most dest.maxRecordBodySize bytes of the body for the record.
func (r *responseCapturer) capture(p []byte) {
if !r.dest.recordBody {
+3 -13
View File
@@ -30,7 +30,7 @@ func TestResponseCapturer_streamed(t *testing.T) {
assert.Equal(t, "yes", recorder.Header().Get("X-Backend"))
assert.False(t, capturer.buffering)
capturer.Flush()
require.NoError(t, capturer.FlushError())
assert.True(t, recorder.Flushed)
require.NoError(t, capturer.serve())
@@ -59,7 +59,7 @@ func TestResponseCapturer_withheld(t *testing.T) {
assert.True(t, capturer.buffering)
// A withheld response cannot be flushed.
capturer.Flush()
require.NoError(t, capturer.FlushError())
assert.False(t, recorder.Flushed)
rec := capturer.record(time.Second)
@@ -250,17 +250,7 @@ func TestResponseCapturer_hijackServesWithheldResponse(t *testing.T) {
_, err := capturer.Write([]byte("pong"))
require.NoError(t, err)
// httptest.ResponseRecorder is not a http.Hijacker, but the withheld response is served first.
_, _, err = capturer.Hijack()
require.Error(t, err)
assert.Empty(t, recorder.Body.String())
// The response is served as soon as the underlying writer can be hijacked.
capturer = newResponseCapturer(&hijackableRecorder{ResponseRecorder: recorder}, &destination{recordBody: true, maxRecordBodySize: -1}, true, nil)
_, err = capturer.Write([]byte("pong"))
require.NoError(t, err)
// The response is served before the hijack is attempted.
_, _, err = capturer.Hijack()
require.Error(t, err)
-4
View File
@@ -5,12 +5,8 @@ import (
"fmt"
"net"
"net/http"
"github.com/traefik/traefik/v3/pkg/middlewares"
)
var _ middlewares.Stateful = &statusRecorder{}
// statusRecorder is the http.ResponseWriter given to the tap services, discarding their response body.
type statusRecorder struct {
status int
+4 -4
View File
@@ -39,7 +39,7 @@ func TestNew(t *testing.T) {
},
{
desc: "unknown service",
config: dynamic.Tap{Response: &dynamic.TapResponse{TapRequest: dynamic.TapRequest{Service: "unknown"}}},
config: dynamic.Tap{Response: &dynamic.TapResponse{Service: "unknown"}},
expectedErr: "building response destination: building tap service handler: service not found",
},
{
@@ -64,20 +64,20 @@ func TestNew(t *testing.T) {
{
desc: "response body recorded without maxRecordBodySize",
config: dynamic.Tap{
Response: &dynamic.TapResponse{TapRequest: dynamic.TapRequest{Service: "responses", RecordBody: true}},
Response: &dynamic.TapResponse{Service: "responses", RecordBody: true},
},
expectedErr: "response maxRecordBodySize must be defined when recordBody is set",
},
{
desc: "response body not recorded without maxRecordBodySize",
config: dynamic.Tap{
Response: &dynamic.TapResponse{TapRequest: dynamic.TapRequest{Service: "responses", RejectOnRecordError: true}},
Response: &dynamic.TapResponse{Service: "responses", RejectOnRecordError: true},
},
},
{
desc: "response body recorded with no limit on purpose",
config: dynamic.Tap{
Response: &dynamic.TapResponse{TapRequest: dynamic.TapRequest{Service: "responses", RecordBody: true, MaxRecordBodySize: new(int64(-1))}},
Response: &dynamic.TapResponse{Service: "responses", RecordBody: true, MaxRecordBodySize: new(int64(-1))},
},
},
{
+145
View File
@@ -0,0 +1,145 @@
package plugins
import (
"bufio"
"context"
"errors"
"net"
"net/http"
"time"
)
type entryWriterKey struct{}
// wrapExternal builds, with the given build function, a plugin handler (Yaegi, Wasm), whose code we do not control, around next.
// The external handler receives a ResponseWriter implementing http.Flusher and http.Hijacker,
// as such code asserts them directly instead of using http.ResponseController.
// The ResponseWriter it gives to next may not implement Unwrap:
// it is wrapped to reach the deadlines, full duplex and hijacking of the ResponseWriter given to the external handler,
// when the external ResponseWriter does not support them.
// Flush is left to the external ResponseWriter: a buffering writer must not be flushed past.
func wrapExternal(next http.Handler, build func(next http.Handler) (http.Handler, error)) (http.Handler, error) {
handler, _, err := wrapExternalWith(next, func(next http.Handler) (http.Handler, struct{}, error) {
external, err := build(next)
return external, struct{}{}, err
})
return handler, err
}
// wrapExternalWith is wrapExternal for a build function which also returns a value, such as a context modifier, returned as is.
func wrapExternalWith[T any](next http.Handler, build func(next http.Handler) (http.Handler, T, error)) (http.Handler, T, error) {
external, value, err := build(exitHandler(next))
if err != nil {
var zero T
return nil, zero, err
}
return entryHandler(external), value, nil
}
func entryHandler(external http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
entry := newEntryWriter(rw)
external.ServeHTTP(entry, req.WithContext(context.WithValue(req.Context(), entryWriterKey{}, entry)))
})
}
func exitHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
entry, ok := req.Context().Value(entryWriterKey{}).(http.ResponseWriter)
if !ok || rw == entry {
next.ServeHTTP(rw, req)
return
}
next.ServeHTTP(&exitWriter{ResponseWriter: rw, entry: entry}, req)
})
}
// entryWriter exposes http.Flusher and http.Hijacker backed by http.ResponseController.
type entryWriter struct {
http.ResponseWriter
rc *http.ResponseController
}
// newEntryWriter wraps the given ResponseWriter in an entryWriter, unless it already is one.
func newEntryWriter(rw http.ResponseWriter) http.ResponseWriter {
if ew, ok := rw.(*entryWriter); ok {
return ew
}
return &entryWriter{ResponseWriter: rw, rc: http.NewResponseController(rw)}
}
// Flush sends any buffered data to the client.
func (w *entryWriter) Flush() {
_ = w.FlushError()
}
// FlushError sends any buffered data to the client, and is preferred over Flush by http.ResponseController.
func (w *entryWriter) FlushError() error {
return w.rc.Flush()
}
// Hijack hijacks the connection.
func (w *entryWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return w.rc.Hijack()
}
// Unwrap exposes the underlying ResponseWriter for http.ResponseController.
func (w *entryWriter) Unwrap() http.ResponseWriter {
return w.ResponseWriter
}
// exitWriter wraps the ResponseWriter created by an external handler.
type exitWriter struct {
http.ResponseWriter
// entry is the ResponseWriter given to the external handler.
entry http.ResponseWriter
}
// SetReadDeadline sets the read deadline through the external ResponseWriter, or the entry ResponseWriter if it does not support it.
func (w *exitWriter) SetReadDeadline(deadline time.Time) error {
if err := http.NewResponseController(w.ResponseWriter).SetReadDeadline(deadline); !errors.Is(err, http.ErrNotSupported) {
return err
}
return http.NewResponseController(w.entry).SetReadDeadline(deadline)
}
// SetWriteDeadline sets the write deadline through the external ResponseWriter, or the entry ResponseWriter if it does not support it.
func (w *exitWriter) SetWriteDeadline(deadline time.Time) error {
if err := http.NewResponseController(w.ResponseWriter).SetWriteDeadline(deadline); !errors.Is(err, http.ErrNotSupported) {
return err
}
return http.NewResponseController(w.entry).SetWriteDeadline(deadline)
}
// EnableFullDuplex enables full duplex through the external ResponseWriter, or the entry ResponseWriter if it does not support it.
func (w *exitWriter) EnableFullDuplex() error {
if err := http.NewResponseController(w.ResponseWriter).EnableFullDuplex(); !errors.Is(err, http.ErrNotSupported) {
return err
}
return http.NewResponseController(w.entry).EnableFullDuplex()
}
// Hijack hijacks the connection through the external ResponseWriter, or the entry ResponseWriter if it does not support it.
// Unlike flushing past it, hijacking past the external ResponseWriter cannot corrupt the response:
// the writes it does afterward fail with http.ErrHijacked.
func (w *exitWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if conn, brw, err := http.NewResponseController(w.ResponseWriter).Hijack(); !errors.Is(err, http.ErrNotSupported) {
return conn, brw, err
}
return http.NewResponseController(w.entry).Hijack()
}
// Unwrap exposes the external ResponseWriter, so that http.ResponseController uses its own Flush.
func (w *exitWriter) Unwrap() http.ResponseWriter {
return w.ResponseWriter
}
+196
View File
@@ -0,0 +1,196 @@
package plugins
import (
"bufio"
"net"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// unwrapOnlyWriter only exposes the wrapped writer through Unwrap.
type unwrapOnlyWriter struct {
http.ResponseWriter
}
func (w *unwrapOnlyWriter) Unwrap() http.ResponseWriter {
return w.ResponseWriter
}
// controllerWriter records the deadlines, full duplex and hijack calls reaching it.
type controllerWriter struct {
http.ResponseWriter
readDeadline time.Time
writeDeadline time.Time
fullDuplex bool
hijacked bool
}
func (w *controllerWriter) SetReadDeadline(deadline time.Time) error {
w.readDeadline = deadline
return nil
}
func (w *controllerWriter) SetWriteDeadline(deadline time.Time) error {
w.writeDeadline = deadline
return nil
}
func (w *controllerWriter) EnableFullDuplex() error {
w.fullDuplex = true
return nil
}
func (w *controllerWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
w.hijacked = true
return nil, nil, nil
}
// bareWriter hides every optional interface of the wrapped writer.
type bareWriter struct {
http.ResponseWriter
}
// flushHijackWriter handles Flush and Hijack itself.
type flushHijackWriter struct {
http.ResponseWriter
}
func (w *flushHijackWriter) Flush() {}
func (w *flushHijackWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return nil, nil, nil
}
func TestEntryWriter_Flush(t *testing.T) {
recorder := httptest.NewRecorder()
rw := newEntryWriter(&unwrapOnlyWriter{ResponseWriter: recorder})
flusher, ok := rw.(http.Flusher)
require.True(t, ok)
flusher.Flush()
assert.True(t, recorder.Flushed)
}
func TestEntryWriter_Hijack(t *testing.T) {
rw := newEntryWriter(&unwrapOnlyWriter{ResponseWriter: httptest.NewRecorder()})
hijacker, ok := rw.(http.Hijacker)
require.True(t, ok)
_, _, err := hijacker.Hijack()
assert.ErrorIs(t, err, http.ErrNotSupported)
}
func TestNewEntryWriter_wrapsOnce(t *testing.T) {
rw := newEntryWriter(httptest.NewRecorder())
assert.Same(t, rw, newEntryWriter(rw))
}
func TestWrapExternal(t *testing.T) {
testCases := []struct {
desc string
externalWriter func(rw http.ResponseWriter) http.ResponseWriter
expectExternalCalls bool
expectExternalHijack bool
expectFlushErr error
}{
{
desc: "external writer implements nothing",
externalWriter: func(rw http.ResponseWriter) http.ResponseWriter {
return &bareWriter{ResponseWriter: rw}
},
expectFlushErr: http.ErrNotSupported,
},
{
desc: "external writer implements Flush and Hijack",
externalWriter: func(rw http.ResponseWriter) http.ResponseWriter {
return &flushHijackWriter{ResponseWriter: rw}
},
expectExternalHijack: true,
},
{
desc: "external writer implements deadlines, full duplex and Hijack",
externalWriter: func(rw http.ResponseWriter) http.ResponseWriter {
return &controllerWriter{ResponseWriter: rw}
},
expectExternalCalls: true,
expectExternalHijack: true,
expectFlushErr: http.ErrNotSupported,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
deadline := time.Now().Add(time.Minute)
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rc := http.NewResponseController(rw)
assert.NoError(t, rc.SetReadDeadline(deadline))
assert.NoError(t, rc.SetWriteDeadline(deadline))
assert.NoError(t, rc.EnableFullDuplex())
assert.ErrorIs(t, rc.Flush(), test.expectFlushErr)
_, _, err := rc.Hijack()
assert.NoError(t, err)
})
var externalWriter http.ResponseWriter
handler, err := wrapExternal(next, func(next http.Handler) (http.Handler, error) {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
externalWriter = test.externalWriter(rw)
next.ServeHTTP(externalWriter, req)
}), nil
})
require.NoError(t, err)
recorder := httptest.NewRecorder()
root := &controllerWriter{ResponseWriter: recorder}
handler.ServeHTTP(root, httptest.NewRequest(http.MethodGet, "/", nil))
assert.False(t, recorder.Flushed)
assert.Equal(t, !test.expectExternalHijack, root.hijacked)
called := root
if test.expectExternalCalls {
called = externalWriter.(*controllerWriter)
assert.Zero(t, root.readDeadline)
assert.Zero(t, root.writeDeadline)
assert.False(t, root.fullDuplex)
}
assert.Equal(t, deadline, called.readDeadline)
assert.Equal(t, deadline, called.writeDeadline)
assert.True(t, called.fullDuplex)
})
}
}
func TestWrapExternal_entryWriterPassedThrough(t *testing.T) {
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
_, ok := rw.(*entryWriter)
assert.True(t, ok)
})
handler, err := wrapExternal(next, func(next http.Handler) (http.Handler, error) {
return next, nil
})
require.NoError(t, err)
handler.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil))
}
+3 -1
View File
@@ -51,7 +51,9 @@ func (b wasmMiddlewareBuilder) newMiddleware(config map[string]any, middlewareNa
}
func (b wasmMiddlewareBuilder) newHandler(ctx context.Context, next http.Handler, cfg reflect.Value, middlewareName string) (http.Handler, error) {
h, applyCtx, err := b.buildMiddleware(ctx, next, cfg, middlewareName)
h, applyCtx, err := wrapExternalWith(next, func(next http.Handler) (http.Handler, func(ctx context.Context) context.Context, error) {
return b.buildMiddleware(ctx, next, cfg, middlewareName)
})
if err != nil {
return nil, fmt.Errorf("building Wasm middleware: %w", err)
}
+6
View File
@@ -60,6 +60,12 @@ func (b yaegiMiddlewareBuilder) newMiddleware(config map[string]any, middlewareN
}
func (b yaegiMiddlewareBuilder) newHandler(ctx context.Context, next http.Handler, cfg reflect.Value, middlewareName string) (http.Handler, error) {
return wrapExternal(next, func(next http.Handler) (http.Handler, error) {
return b.newPluginHandler(ctx, next, cfg, middlewareName)
})
}
func (b yaegiMiddlewareBuilder) newPluginHandler(ctx context.Context, next http.Handler, cfg reflect.Value, middlewareName string) (http.Handler, error) {
args := []reflect.Value{reflect.ValueOf(ctx), reflect.ValueOf(next), cfg, reflect.ValueOf(middlewareName)}
results := b.fnNew.Call(args)
+3 -5
View File
@@ -153,11 +153,9 @@ func createMessage(certs map[string]*Certificate) dynamic.Message {
for _, cert := range certs {
certConf := &traefiktls.CertAndStores{
Certificate: traefiktls.Certificate{
CertFile: types.FileOrContent(cert.Certificate),
KeyFile: types.FileOrContent(cert.Key),
},
Stores: []string{tlsalpn01.ACMETLS1Protocol},
CertFile: types.FileOrContent(cert.Certificate),
KeyFile: types.FileOrContent(cert.Key),
Stores: []string{tlsalpn01.ACMETLS1Protocol},
}
conf.Configuration.TLS.Certificates = append(conf.Configuration.TLS.Certificates, certConf)
}
+3 -5
View File
@@ -899,11 +899,9 @@ func (p *Provider) buildMessage() dynamic.Message {
for _, cert := range p.certificates {
certConf := &traefiktls.CertAndStores{
Certificate: traefiktls.Certificate{
CertFile: types.FileOrContent(cert.Certificate.Certificate),
KeyFile: types.FileOrContent(cert.Key),
},
Stores: []string{cert.Store},
CertFile: types.FileOrContent(cert.Certificate.Certificate),
KeyFile: types.FileOrContent(cert.Key),
Stores: []string{cert.Store},
}
conf.Configuration.TLS.Certificates = append(conf.Configuration.TLS.Certificates, certConf)
}
+5 -15
View File
@@ -49,9 +49,7 @@ func TestGetUncheckedCertificates(t *testing.T) {
domains: []string{"*.traefik.wtf"},
acmeCertificates: []*CertAndStore{
{
Certificate: Certificate{
Domain: types.Domain{Main: "*.traefik.wtf"},
},
Domain: types.Domain{Main: "*.traefik.wtf"},
},
},
expectedDomains: nil,
@@ -72,9 +70,7 @@ func TestGetUncheckedCertificates(t *testing.T) {
domains: []string{"traefik.wtf", "foo.traefik.wtf"},
acmeCertificates: []*CertAndStore{
{
Certificate: Certificate{
Domain: types.Domain{Main: "traefik.wtf"},
},
Domain: types.Domain{Main: "traefik.wtf"},
},
},
expectedDomains: []string{"foo.traefik.wtf"},
@@ -90,9 +86,7 @@ func TestGetUncheckedCertificates(t *testing.T) {
domains: []string{"traefik.wtf"},
acmeCertificates: []*CertAndStore{
{
Certificate: Certificate{
Domain: types.Domain{Main: "traefik.wtf"},
},
Domain: types.Domain{Main: "traefik.wtf"},
},
},
expectedDomains: nil,
@@ -108,9 +102,7 @@ func TestGetUncheckedCertificates(t *testing.T) {
domains: []string{"who.traefik.wtf", "foo.traefik.wtf"},
acmeCertificates: []*CertAndStore{
{
Certificate: Certificate{
Domain: types.Domain{Main: "*.traefik.wtf"},
},
Domain: types.Domain{Main: "*.traefik.wtf"},
},
},
expectedDomains: nil,
@@ -120,9 +112,7 @@ func TestGetUncheckedCertificates(t *testing.T) {
domains: []string{"traefik.wtf", "foo.traefik.wtf"},
acmeCertificates: []*CertAndStore{
{
Certificate: Certificate{
Domain: types.Domain{Main: "*.traefik.wtf"},
},
Domain: types.Domain{Main: "*.traefik.wtf"},
},
},
expectedDomains: []string{"traefik.wtf"},
+7 -11
View File
@@ -406,10 +406,8 @@ func TestDynConfBuilder_DefaultRule(t *testing.T) {
t.Parallel()
p := Provider{
Shared: Shared{
ExposedByDefault: true,
DefaultRule: test.defaultRule,
},
ExposedByDefault: true,
DefaultRule: test.defaultRule,
}
require.NoError(t, p.Init())
@@ -3910,14 +3908,12 @@ func TestDynConfBuilder_build(t *testing.T) {
t.Parallel()
p := Provider{
Shared: Shared{
AllowEmptyServices: test.allowEmptyServices,
ExposedByDefault: true,
UseBindPortIP: test.useBindPortIP,
DefaultRule: "Host(`{{ normalize .Name }}.traefik.wtf`)",
},
AllowEmptyServices: test.allowEmptyServices,
ExposedByDefault: true,
UseBindPortIP: test.useBindPortIP,
DefaultRule: "Host(`{{ normalize .Name }}.traefik.wtf`)",
Constraints: test.constraints,
}
p.Constraints = test.constraints
require.NoError(t, p.Init())
+30 -34
View File
@@ -152,23 +152,21 @@ func TestSwarmProvider_listServices(t *testing.T) {
dockerVersion: "1.30",
networks: []networktypes.Summary{
{
Network: networktypes.Network{
Name: "network_name",
ID: "yk6l57rfwizjzxxzftn4amaot",
Created: time.Now(),
Scope: "swarm",
Driver: "overlay",
EnableIPv6: false,
Internal: true,
Ingress: false,
ConfigOnly: false,
Options: map[string]string{
"com.docker.networktypes.driver.overlay.vxlanid_list": "4098",
"com.docker.networktypes.enable_ipv6": "false",
},
Labels: map[string]string{
"com.docker.stack.namespace": "test",
},
Name: "network_name",
ID: "yk6l57rfwizjzxxzftn4amaot",
Created: time.Now(),
Scope: "swarm",
Driver: "overlay",
EnableIPv6: false,
Internal: true,
Ingress: false,
ConfigOnly: false,
Options: map[string]string{
"com.docker.networktypes.driver.overlay.vxlanid_list": "4098",
"com.docker.networktypes.enable_ipv6": "false",
},
Labels: map[string]string{
"com.docker.stack.namespace": "test",
},
},
},
@@ -209,23 +207,21 @@ func TestSwarmProvider_listServices(t *testing.T) {
dockerVersion: "1.30",
networks: []networktypes.Summary{
{
Network: networktypes.Network{
Name: "network_name",
ID: "yk6l57rfwizjzxxzftn4amaot",
Created: time.Now(),
Scope: "swarm",
Driver: "overlay",
EnableIPv6: false,
Internal: true,
Ingress: false,
ConfigOnly: false,
Options: map[string]string{
"com.docker.networktypes.driver.overlay.vxlanid_list": "4098",
"com.docker.networktypes.enable_ipv6": "false",
},
Labels: map[string]string{
"com.docker.stack.namespace": "test",
},
Name: "network_name",
ID: "yk6l57rfwizjzxxzftn4amaot",
Created: time.Now(),
Scope: "swarm",
Driver: "overlay",
EnableIPv6: false,
Internal: true,
Ingress: false,
ConfigOnly: false,
Options: map[string]string{
"com.docker.networktypes.driver.overlay.vxlanid_list": "4098",
"com.docker.networktypes.enable_ipv6": "false",
},
Labels: map[string]string{
"com.docker.stack.namespace": "test",
},
},
},
+6 -11
View File
@@ -8,24 +8,19 @@ import (
"github.com/stretchr/testify/require"
traefikcrdfake "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/generated/clientset/versioned/fake"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kubefake "k8s.io/client-go/kubernetes/fake"
)
func TestClientIgnoresHelmOwnedSecrets(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "secret",
},
Namespace: "default",
Name: "secret",
}
helmSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "helm-secret",
Labels: map[string]string{
"owner": "helm",
},
Namespace: "default",
Name: "helm-secret",
Labels: map[string]string{
"owner": "helm",
},
}
+62 -49
View File
@@ -270,19 +270,21 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
logger := log.Ctx(ctx).With().Str(logs.MiddlewareName, id).Logger()
ctxMid := logger.WithContext(ctx)
basicAuth, err := createBasicAuthMiddleware(client, middleware.Namespace, middleware.Spec.BasicAuth)
refs := k8s.URNRefs{}
basicAuth, err := createBasicAuthMiddleware(client, middleware.Namespace, middleware.Spec.BasicAuth, refs)
if err != nil {
logger.Error().Err(err).Msg("Error while reading basic auth middleware")
continue
}
digestAuth, err := createDigestAuthMiddleware(client, middleware.Namespace, middleware.Spec.DigestAuth)
digestAuth, err := createDigestAuthMiddleware(client, middleware.Namespace, middleware.Spec.DigestAuth, refs)
if err != nil {
logger.Error().Err(err).Msg("Error while reading digest auth middleware")
continue
}
forwardAuth, err := createForwardAuthMiddleware(client, middleware.Namespace, middleware.Spec.ForwardAuth)
forwardAuth, err := createForwardAuthMiddleware(client, middleware.Namespace, middleware.Spec.ForwardAuth, refs)
if err != nil {
logger.Error().Err(err).Msg("Error while reading forward auth middleware")
continue
@@ -297,13 +299,13 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
addToConfig(log.Ctx(ctxMid), "service", errorPage.Service, conf.HTTP.Services, errorPageService)
}
plugin, err := createPluginMiddleware(client, middleware.Namespace, middleware.Spec.Plugin)
plugin, err := createPluginMiddleware(client, middleware.Namespace, middleware.Spec.Plugin, refs)
if err != nil {
logger.Error().Err(err).Msg("Error while reading plugins middleware")
continue
}
rateLimit, err := createRateLimitMiddleware(client, middleware.Namespace, middleware.Spec.RateLimit)
rateLimit, err := createRateLimitMiddleware(client, middleware.Namespace, middleware.Spec.RateLimit, refs)
if err != nil {
logger.Error().Err(err).Msg("Error while reading rateLimit middleware")
continue
@@ -364,6 +366,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
GrpcWeb: middleware.Spec.GrpcWeb,
Tap: tap,
Plugin: plugin,
URNRefs: refs.OrNil(),
})
}
@@ -872,7 +875,7 @@ func getNativeServiceAddress(service corev1.Service, svcPort corev1.ServicePort)
return net.JoinHostPort(service.Spec.ClusterIP, strconv.Itoa(int(svcPort.Port))), nil
}
func createPluginMiddleware(k8sClient Client, ns string, plugins map[string]apiextensionv1.JSON) (map[string]dynamic.PluginConf, error) {
func createPluginMiddleware(k8sClient Client, ns string, plugins map[string]apiextensionv1.JSON, refs k8s.URNRefs) (map[string]dynamic.PluginConf, error) {
if plugins == nil {
return nil, nil
}
@@ -887,9 +890,9 @@ func createPluginMiddleware(k8sClient Client, ns string, plugins map[string]apie
return nil, err
}
for _, pc := range pcMap {
for key := range pc {
if pc[key], err = loadSecretKeys(k8sClient, ns, pc[key]); err != nil {
for name, pc := range pcMap {
for key, value := range pc {
if err = loadSecretKeys(k8sClient, ns, dynamic.JSONPointer("plugin", name, key), value, refs); err != nil {
return nil, err
}
}
@@ -898,32 +901,38 @@ func createPluginMiddleware(k8sClient Client, ns string, plugins map[string]apie
return pcMap, nil
}
func loadSecretKeys(k8sClient Client, ns string, i any) (any, error) {
var err error
switch iv := i.(type) {
// loadSecretKeys records in refs the value of every Secret URN found in value, under the JSON Pointer of the URN.
// The URNs stay in the configuration: the middleware builder replaces them with their values.
func loadSecretKeys(k8sClient Client, ns, pointer string, value any, refs k8s.URNRefs) error {
switch v := value.(type) {
case string:
if !strings.HasPrefix(iv, "urn:k8s:secret:") {
return iv, nil
if !strings.HasPrefix(v, k8s.SecretURNPrefix) {
return nil
}
return getSecretValue(k8sClient, ns, iv)
secretValue, err := getSecretValue(k8sClient, ns, v)
if err != nil {
return err
}
refs[pointer] = secretValue
case []any:
for i := range iv {
if iv[i], err = loadSecretKeys(k8sClient, ns, iv[i]); err != nil {
return nil, err
for i, item := range v {
if err := loadSecretKeys(k8sClient, ns, pointer+"/"+strconv.Itoa(i), item, refs); err != nil {
return err
}
}
case map[string]any:
for k := range iv {
if iv[k], err = loadSecretKeys(k8sClient, ns, iv[k]); err != nil {
return nil, err
for key, item := range v {
if err := loadSecretKeys(k8sClient, ns, pointer+dynamic.JSONPointer(key), item, refs); err != nil {
return err
}
}
}
return i, nil
return nil
}
func getSecretValue(c Client, ns, urn string) (string, error) {
@@ -1015,7 +1024,7 @@ func createCompressMiddleware(compress *traefikv1alpha1.Compress) *dynamic.Compr
return c
}
func createRateLimitMiddleware(client Client, namespace string, rateLimit *traefikv1alpha1.RateLimit) (*dynamic.RateLimit, error) {
func createRateLimitMiddleware(client Client, namespace string, rateLimit *traefikv1alpha1.RateLimit, refs k8s.URNRefs) (*dynamic.RateLimit, error) {
if rateLimit == nil {
return nil, nil
}
@@ -1073,8 +1082,8 @@ func createRateLimitMiddleware(client Client, namespace string, rateLimit *traef
if err != nil {
return nil, fmt.Errorf("failed to load auth secret: %w", err)
}
rl.Redis.TLS.Cert = authSecretCert
rl.Redis.TLS.Key = authSecretKey
rl.Redis.TLS.Cert = refs.Add("/rateLimit/redis/tls/cert", rateLimit.Redis.TLS.CertSecret, "tls.crt", authSecretCert)
rl.Redis.TLS.Key = refs.Add("/rateLimit/redis/tls/key", rateLimit.Redis.TLS.CertSecret, "tls.key", authSecretKey)
}
}
@@ -1100,11 +1109,12 @@ func createRateLimitMiddleware(client Client, namespace string, rateLimit *traef
}
if rateLimit.Redis.Secret != "" {
var err error
rl.Redis.Username, rl.Redis.Password, err = loadRedisCredentials(namespace, rateLimit.Redis.Secret, client)
username, password, err := loadRedisCredentials(namespace, rateLimit.Redis.Secret, client)
if err != nil {
return nil, err
}
rl.Redis.Username = refs.Add("/rateLimit/redis/username", rateLimit.Redis.Secret, "username", username)
rl.Redis.Password = refs.Add("/rateLimit/redis/password", rateLimit.Redis.Secret, "password", password)
}
}
@@ -1163,7 +1173,7 @@ func createRetryMiddleware(retry *traefikv1alpha1.Retry) (*dynamic.Retry, error)
return r, nil
}
func createForwardAuthMiddleware(k8sClient Client, namespace string, auth *traefikv1alpha1.ForwardAuth) (*dynamic.ForwardAuth, error) {
func createForwardAuthMiddleware(k8sClient Client, namespace string, auth *traefikv1alpha1.ForwardAuth, refs k8s.URNRefs) (*dynamic.ForwardAuth, error) {
if auth == nil {
return nil, nil
}
@@ -1212,8 +1222,8 @@ func createForwardAuthMiddleware(k8sClient Client, namespace string, auth *traef
if err != nil {
return nil, fmt.Errorf("failed to load auth secret: %w", err)
}
forwardAuth.TLS.Cert = authSecretCert
forwardAuth.TLS.Key = authSecretKey
forwardAuth.TLS.Cert = refs.Add("/forwardAuth/tls/cert", auth.TLS.CertSecret, "tls.crt", authSecretCert)
forwardAuth.TLS.Key = refs.Add("/forwardAuth/tls/key", auth.TLS.CertSecret, "tls.key", authSecretKey)
}
forwardAuth.TLS.CAOptional = auth.TLS.CAOptional
@@ -1291,7 +1301,7 @@ func loadAuthTLSSecret(namespace, secretName string, k8sClient Client) (string,
return getCertificateBlocks(secret, namespace, secretName)
}
func createBasicAuthMiddleware(client Client, namespace string, basicAuth *traefikv1alpha1.BasicAuth) (*dynamic.BasicAuth, error) {
func createBasicAuthMiddleware(client Client, namespace string, basicAuth *traefikv1alpha1.BasicAuth, refs k8s.URNRefs) (*dynamic.BasicAuth, error) {
if basicAuth == nil {
return nil, nil
}
@@ -1318,27 +1328,28 @@ func createBasicAuthMiddleware(client Client, namespace string, basicAuth *traef
}
return &dynamic.BasicAuth{
Users: credentials,
// The user is computed from both keys: the password key is the one that must not be exposed.
Users: refs.AddUsers("/basicAuth/users", basicAuth.Secret, "password", credentials),
Realm: basicAuth.Realm,
RemoveHeader: basicAuth.RemoveHeader,
HeaderField: basicAuth.HeaderField,
}, nil
}
credentials, err := loadAuthCredentials(secret)
key, credentials, err := loadAuthCredentials(secret)
if err != nil {
return nil, fmt.Errorf("failed to load basic auth credentials: %w", err)
}
return &dynamic.BasicAuth{
Users: credentials,
Users: refs.AddUsers("/basicAuth/users", basicAuth.Secret, key, credentials),
Realm: basicAuth.Realm,
RemoveHeader: basicAuth.RemoveHeader,
HeaderField: basicAuth.HeaderField,
}, nil
}
func createDigestAuthMiddleware(client Client, namespace string, digestAuth *traefikv1alpha1.DigestAuth) (*dynamic.DigestAuth, error) {
func createDigestAuthMiddleware(client Client, namespace string, digestAuth *traefikv1alpha1.DigestAuth, refs k8s.URNRefs) (*dynamic.DigestAuth, error) {
if digestAuth == nil {
return nil, nil
}
@@ -1358,13 +1369,13 @@ func createDigestAuthMiddleware(client Client, namespace string, digestAuth *tra
return nil, fmt.Errorf("data for secret '%s/%s' must not be nil", namespace, digestAuth.Secret)
}
credentials, err := loadAuthCredentials(secret)
key, credentials, err := loadAuthCredentials(secret)
if err != nil {
return nil, fmt.Errorf("failed to load digest auth credentials: %w", err)
}
return &dynamic.DigestAuth{
Users: credentials,
Users: refs.AddUsers("/digestAuth/users", digestAuth.Secret, key, credentials),
Realm: digestAuth.Realm,
RemoveHeader: digestAuth.RemoveHeader,
HeaderField: digestAuth.HeaderField,
@@ -1399,14 +1410,18 @@ func loadBasicAuthCredentials(secret *corev1.Secret) ([]string, error) {
return []string{fmt.Sprintf("%s:{SHA}%s", username, passwordSum)}, nil
}
func loadAuthCredentials(secret *corev1.Secret) ([]string, error) {
// loadAuthCredentials returns the key of the single entry of the secret, and the credentials it holds, one per line.
func loadAuthCredentials(secret *corev1.Secret) (string, []string, error) {
if len(secret.Data) != 1 {
return nil, fmt.Errorf("found %d elements for secret '%s/%s', must be single element exactly", len(secret.Data), secret.Namespace, secret.Name)
return "", nil, fmt.Errorf("found %d elements for secret '%s/%s', must be single element exactly", len(secret.Data), secret.Namespace, secret.Name)
}
var firstSecret []byte
for _, v := range secret.Data {
firstSecret = v
var (
key string
firstSecret []byte
)
for k, v := range secret.Data {
key, firstSecret = k, v
break
}
@@ -1418,13 +1433,13 @@ func loadAuthCredentials(secret *corev1.Secret) ([]string, error) {
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error reading secret for %s/%s: %w", secret.Namespace, secret.Name, err)
return "", nil, fmt.Errorf("error reading secret for %s/%s: %w", secret.Namespace, secret.Name, err)
}
if len(credentials) == 0 {
return nil, fmt.Errorf("secret '%s/%s' does not contain any credentials", secret.Namespace, secret.Name)
return "", nil, fmt.Errorf("secret '%s/%s' does not contain any credentials", secret.Namespace, secret.Name)
}
return credentials, nil
return key, credentials, nil
}
func (p *Provider) buildTLSOptions(ctx context.Context, client Client) map[string]tls.Options {
@@ -1654,10 +1669,8 @@ func getTLS(k8sClient Client, secretName, namespace string) (*tls.CertAndStores,
}
return &tls.CertAndStores{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(cert),
KeyFile: types.FileOrContent(key),
},
CertFile: types.FileOrContent(cert),
KeyFile: types.FileOrContent(key),
}, nil
}
+70 -37
View File
@@ -788,10 +788,8 @@ func TestLoadIngressRouteTCPs(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
KeyFile: types.FileOrContent("-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----"),
},
CertFile: types.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
KeyFile: types.FileOrContent("-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----"),
},
},
},
@@ -2600,12 +2598,12 @@ func TestLoadIngressRoutes(t *testing.T) {
},
Redis: &dynamic.Redis{
Endpoints: []string{"127.0.0.1:6379"},
Username: "user",
Password: "password",
Username: "urn:k8s:secret:redissecret:username",
Password: "urn:k8s:secret:redissecret:password",
TLS: &types.ClientTLS{
CA: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----",
Cert: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----",
Key: "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----",
Cert: "urn:k8s:secret:tlssecret:tls.crt",
Key: "urn:k8s:secret:tlssecret:tls.key",
},
DB: 0,
PoolSize: 42,
@@ -2615,6 +2613,12 @@ func TestLoadIngressRoutes(t *testing.T) {
DialTimeout: new(ptypes.Duration(42 * time.Second)),
},
},
URNRefs: map[string]string{
"/rateLimit/redis/username": "user",
"/rateLimit/redis/password": "password",
"/rateLimit/redis/tls/cert": "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----",
"/rateLimit/redis/tls/key": "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----",
},
},
},
Services: map[string]*dynamic.Service{
@@ -5825,10 +5829,8 @@ func TestLoadIngressRoutes(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
KeyFile: types.FileOrContent("-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----"),
},
CertFile: types.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
KeyFile: types.FileOrContent("-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----"),
},
},
},
@@ -7381,12 +7383,20 @@ func TestLoadIngressRoutes(t *testing.T) {
Middlewares: map[string]*dynamic.Middleware{
"default-basicauth": {
BasicAuth: &dynamic.BasicAuth{
Users: dynamic.Users{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
Users: dynamic.Users{"urn:k8s:secret:authsecret:users", "urn:k8s:secret:authsecret:users"},
},
URNRefs: map[string]string{
"/basicAuth/users/0": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"/basicAuth/users/1": "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
},
},
"default-digestauth": {
DigestAuth: &dynamic.DigestAuth{
Users: dynamic.Users{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
Users: dynamic.Users{"urn:k8s:secret:authsecret:users", "urn:k8s:secret:authsecret:users"},
},
URNRefs: map[string]string{
"/digestAuth/users/0": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"/digestAuth/users/1": "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
},
},
"default-forwardauth": {
@@ -7396,10 +7406,14 @@ func TestLoadIngressRoutes(t *testing.T) {
HeaderField: "X-Header-Field",
TLS: &dynamic.ClientTLS{
CA: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----",
Cert: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----",
Key: "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----",
Cert: "urn:k8s:secret:tlssecret:tls.crt",
Key: "urn:k8s:secret:tlssecret:tls.key",
},
},
URNRefs: map[string]string{
"/forwardAuth/tls/cert": "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----",
"/forwardAuth/tls/key": "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----",
},
},
},
Services: map[string]*dynamic.Service{},
@@ -7429,9 +7443,12 @@ func TestLoadIngressRoutes(t *testing.T) {
Plugin: map[string]dynamic.PluginConf{
"test-secret": map[string]any{
"user": "admin",
"secret": "this_is_the_secret",
"secret": "urn:k8s:secret:name:key",
},
},
URNRefs: map[string]string{
"/plugin/test-secret/secret": "this_is_the_secret",
},
},
},
Services: map[string]*dynamic.Service{},
@@ -7464,12 +7481,15 @@ func TestLoadIngressRoutes(t *testing.T) {
"secret_1": map[string]any{
"secret_2": map[string]any{
"user": "admin",
"secret": "this_is_the_very_deep_secret",
"secret": "urn:k8s:secret:name:key",
},
},
},
},
},
URNRefs: map[string]string{
"/plugin/test-secret/secret_0/secret_1/secret_2/secret": "this_is_the_very_deep_secret",
},
},
},
Services: map[string]*dynamic.Service{},
@@ -7498,9 +7518,13 @@ func TestLoadIngressRoutes(t *testing.T) {
"default-test-secret": {
Plugin: map[string]dynamic.PluginConf{
"test-secret": map[string]any{
"secret": []any{"secret_data1", "secret_data2"},
"secret": []any{"urn:k8s:secret:name:key1", "urn:k8s:secret:name:key2"},
},
},
URNRefs: map[string]string{
"/plugin/test-secret/secret/0": "secret_data1",
"/plugin/test-secret/secret/1": "secret_data2",
},
},
},
Services: map[string]*dynamic.Service{},
@@ -7532,15 +7556,19 @@ func TestLoadIngressRoutes(t *testing.T) {
"users": []any{
map[string]any{
"name": "admin",
"secret": "admin_password",
"secret": "urn:k8s:secret:name:key1",
},
map[string]any{
"name": "user",
"secret": "user_password",
"secret": "urn:k8s:secret:name:key2",
},
},
},
},
URNRefs: map[string]string{
"/plugin/test-secret/users/0/secret": "admin_password",
"/plugin/test-secret/users/1/secret": "user_password",
},
},
},
Services: map[string]*dynamic.Service{},
@@ -7601,11 +7629,9 @@ func TestLoadIngressRoutes(t *testing.T) {
Timeout: ptypes.Duration(5 * time.Second),
},
Response: &dynamic.TapResponse{
TapRequest: dynamic.TapRequest{
Service: "default-tap-tap-response-service",
Path: "/",
Timeout: ptypes.Duration(30 * time.Second),
},
Service: "default-tap-tap-response-service",
Path: "/",
Timeout: ptypes.Duration(30 * time.Second),
RequestHeaders: []string{"X-Request-Id"},
},
},
@@ -8080,11 +8106,9 @@ func TestLoadIngressRoutes(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
KeyFile: types.FileOrContent("-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----"),
},
Stores: []string{"default"},
CertFile: types.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
KeyFile: types.FileOrContent("-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----"),
Stores: []string{"default"},
},
},
Stores: map[string]tls.Store{
@@ -10326,6 +10350,11 @@ func TestLoadIngressRoutes(t *testing.T) {
conf := p.loadConfigurationFromCRD(t.Context(), client)
assert.Equal(t, test.expected, conf)
for name, middleware := range conf.HTTP.Middlewares {
_, err := middleware.ResolveURNRefs()
assert.NoError(t, err, name)
}
})
}
}
@@ -14723,11 +14752,13 @@ func TestCreateBasicAuthCredentials(t *testing.T) {
}
// Testing for username/password components in basic-auth secret
basicAuth, secretErr := createBasicAuthMiddleware(client, "default", &traefikv1alpha1.BasicAuth{Secret: "basic-auth-secret"})
refs := k8s.URNRefs{}
basicAuth, secretErr := createBasicAuthMiddleware(client, "default", &traefikv1alpha1.BasicAuth{Secret: "basic-auth-secret"}, refs)
require.NoError(t, secretErr)
require.Len(t, basicAuth.Users, 1)
assert.Equal(t, dynamic.Users{"urn:k8s:secret:basic-auth-secret:password"}, basicAuth.Users)
require.Len(t, refs, 1)
components := strings.Split(basicAuth.Users[0], ":")
components := strings.Split(refs["/basicAuth/users/0"], ":")
require.Len(t, components, 2)
username := components[0]
@@ -14738,11 +14769,13 @@ func TestCreateBasicAuthCredentials(t *testing.T) {
assert.True(t, auth.CheckSecret("password", hashedPassword))
// Testing for username/password components in htpasswd secret
basicAuth, secretErr = createBasicAuthMiddleware(client, "default", &traefikv1alpha1.BasicAuth{Secret: "auth-secret"})
refs = k8s.URNRefs{}
basicAuth, secretErr = createBasicAuthMiddleware(client, "default", &traefikv1alpha1.BasicAuth{Secret: "auth-secret"}, refs)
require.NoError(t, secretErr)
require.Len(t, basicAuth.Users, 2)
assert.Equal(t, dynamic.Users{"urn:k8s:secret:auth-secret:users", "urn:k8s:secret:auth-secret:users"}, basicAuth.Users)
require.Len(t, refs, 2)
components = strings.Split(basicAuth.Users[1], ":")
components = strings.Split(refs["/basicAuth/users/1"], ":")
require.Len(t, components, 2)
username = components[0]
+4 -12
View File
@@ -546,9 +546,7 @@ func (c *clientWrapper) UpdateHTTPRouteStatus(ctx context.Context, route ktypes.
currentRoute = currentRoute.DeepCopy()
currentRoute.Status = gatev1.HTTPRouteStatus{
RouteStatus: gatev1.RouteStatus{
Parents: parentStatuses,
},
Parents: parentStatuses,
}
if _, err = c.csGateway.GatewayV1().HTTPRoutes(route.Namespace).UpdateStatus(ctx, currentRoute, metav1.UpdateOptions{}); err != nil {
@@ -588,9 +586,7 @@ func (c *clientWrapper) UpdateGRPCRouteStatus(ctx context.Context, route ktypes.
currentRoute = currentRoute.DeepCopy()
currentRoute.Status = gatev1.GRPCRouteStatus{
RouteStatus: gatev1.RouteStatus{
Parents: parentStatuses,
},
Parents: parentStatuses,
}
if _, err = c.csGateway.GatewayV1().GRPCRoutes(route.Namespace).UpdateStatus(ctx, currentRoute, metav1.UpdateOptions{}); err != nil {
@@ -630,9 +626,7 @@ func (c *clientWrapper) UpdateTCPRouteStatus(ctx context.Context, route ktypes.N
currentRoute = currentRoute.DeepCopy()
currentRoute.Status = gatev1.TCPRouteStatus{
RouteStatus: gatev1.RouteStatus{
Parents: parentStatuses,
},
Parents: parentStatuses,
}
if _, err = c.csGateway.GatewayV1().TCPRoutes(route.Namespace).UpdateStatus(ctx, currentRoute, metav1.UpdateOptions{}); err != nil {
@@ -672,9 +666,7 @@ func (c *clientWrapper) UpdateTLSRouteStatus(ctx context.Context, route ktypes.N
currentRoute = currentRoute.DeepCopy()
currentRoute.Status = gatev1.TLSRouteStatus{
RouteStatus: gatev1.RouteStatus{
Parents: parentStatuses,
},
Parents: parentStatuses,
}
if _, err = c.csGateway.GatewayV1().TLSRoutes(route.Namespace).UpdateStatus(ctx, currentRoute, metav1.UpdateOptions{}); err != nil {
@@ -1239,10 +1239,8 @@ func (p *Provider) getTLSCert(secretName gatev1.ObjectName, namespace string) (*
}
certAndStore := &tls.CertAndStores{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(cert),
KeyFile: types.FileOrContent(key),
},
CertFile: types.FileOrContent(cert),
KeyFile: types.FileOrContent(key),
}
if _, err := certAndStore.GetCertificate(); err != nil {
return nil, fmt.Errorf("validating certificate: %w", err)
+115 -195
View File
@@ -705,10 +705,8 @@ func TestLoadHTTPRoutes(t *testing.T) {
Options: map[string]tls.Options{},
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
},
@@ -1363,10 +1361,8 @@ func TestLoadHTTPRoutes(t *testing.T) {
},
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
},
@@ -1444,16 +1440,12 @@ func TestLoadHTTPRoutes(t *testing.T) {
},
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
},
@@ -1997,10 +1989,8 @@ func TestLoadHTTPRoutes(t *testing.T) {
},
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
},
@@ -2121,10 +2111,8 @@ func TestLoadHTTPRoutes(t *testing.T) {
},
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
},
@@ -3897,10 +3885,8 @@ func TestLoadHTTPRoutes(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
Options: map[string]tls.Options{
@@ -6991,10 +6977,8 @@ func TestLoadTLSRoutes(t *testing.T) {
Options: map[string]tls.Options{},
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
},
@@ -7306,10 +7290,8 @@ func TestLoadTLSRoutes(t *testing.T) {
Options: map[string]tls.Options{},
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
},
@@ -8236,10 +8218,8 @@ func TestLoadTLSRoutes(t *testing.T) {
Options: map[string]tls.Options{},
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
},
@@ -8325,10 +8305,8 @@ func TestLoadTLSRoutes(t *testing.T) {
Options: map[string]tls.Options{},
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
},
@@ -8409,10 +8387,8 @@ func TestLoadTLSRoutes(t *testing.T) {
Options: map[string]tls.Options{},
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
},
@@ -9027,10 +9003,8 @@ func TestLoadMixedRoutes(t *testing.T) {
},
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
},
@@ -9251,10 +9225,8 @@ func TestLoadMixedRoutes(t *testing.T) {
},
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
},
@@ -9445,10 +9417,8 @@ func TestLoadMixedRoutes(t *testing.T) {
},
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
},
@@ -9639,10 +9609,8 @@ func TestLoadMixedRoutes(t *testing.T) {
},
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
},
@@ -9791,10 +9759,8 @@ func TestLoadMixedRoutes(t *testing.T) {
},
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
},
@@ -10092,16 +10058,12 @@ func TestLoadMixedRoutes(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
Options: map[string]tls.Options{
@@ -10308,10 +10270,8 @@ func TestLoadMixedRoutes(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
Options: map[string]tls.Options{
@@ -10560,10 +10520,8 @@ func TestLoadRoutesWithReferenceGrants(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
Options: map[string]tls.Options{},
@@ -10780,10 +10738,8 @@ func TestLoadRoutesWithReferenceGrants(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
Options: map[string]tls.Options{
@@ -10891,7 +10847,7 @@ func Test_matchingGatewayListener(t *testing.T) {
{
desc: "Unsupported group",
gateways: []gatewayWithListeners{{
gateway: &gatev1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "gateway", Namespace: "default"}},
gateway: &gatev1.Gateway{Name: "gateway", Namespace: "default"},
listeners: []gatewayListener{{SectionName: "foo"}},
}},
parentRefs: []gatev1.ParentReference{{
@@ -10901,7 +10857,7 @@ func Test_matchingGatewayListener(t *testing.T) {
{
desc: "Unsupported kind",
gateways: []gatewayWithListeners{{
gateway: &gatev1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "gateway", Namespace: "default"}},
gateway: &gatev1.Gateway{Name: "gateway", Namespace: "default"},
listeners: []gatewayListener{{SectionName: "foo"}},
}},
parentRefs: []gatev1.ParentReference{{
@@ -10912,7 +10868,7 @@ func Test_matchingGatewayListener(t *testing.T) {
{
desc: "Namespace does not match the listener",
gateways: []gatewayWithListeners{{
gateway: &gatev1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "gateway", Namespace: "default"}},
gateway: &gatev1.Gateway{Name: "gateway", Namespace: "default"},
listeners: []gatewayListener{{SectionName: "foo"}},
}},
parentRefs: []gatev1.ParentReference{{
@@ -10924,7 +10880,7 @@ func Test_matchingGatewayListener(t *testing.T) {
{
desc: "Route namespace defaulting does not match the listener",
gateways: []gatewayWithListeners{{
gateway: &gatev1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "gateway", Namespace: "default"}},
gateway: &gatev1.Gateway{Name: "gateway", Namespace: "default"},
listeners: []gatewayListener{{SectionName: "foo"}},
}},
routeNamespace: "foo",
@@ -10936,7 +10892,7 @@ func Test_matchingGatewayListener(t *testing.T) {
{
desc: "Name does not match the listener",
gateways: []gatewayWithListeners{{
gateway: &gatev1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "gateway", Namespace: "default"}},
gateway: &gatev1.Gateway{Name: "gateway", Namespace: "default"},
listeners: []gatewayListener{{}},
}},
parentRefs: []gatev1.ParentReference{{
@@ -10949,7 +10905,7 @@ func Test_matchingGatewayListener(t *testing.T) {
{
desc: "Match",
gateways: []gatewayWithListeners{{
gateway: &gatev1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "gateway", Namespace: "default"}},
gateway: &gatev1.Gateway{Name: "gateway", Namespace: "default"},
listeners: []gatewayListener{{}},
}},
parentRefs: []gatev1.ParentReference{{
@@ -10973,7 +10929,7 @@ func Test_matchingGatewayListener(t *testing.T) {
{
desc: "Match with route namespace defaulting",
gateways: []gatewayWithListeners{{
gateway: &gatev1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "gateway", Namespace: "default"}},
gateway: &gatev1.Gateway{Name: "gateway", Namespace: "default"},
listeners: []gatewayListener{{}},
}},
routeNamespace: "default",
@@ -10999,7 +10955,7 @@ func Test_matchingGatewayListener(t *testing.T) {
// this controller.
desc: "Only parentRefs targeting our Gateways are returned",
gateways: []gatewayWithListeners{{
gateway: &gatev1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "gateway", Namespace: "default"}},
gateway: &gatev1.Gateway{Name: "gateway", Namespace: "default"},
listeners: []gatewayListener{{}},
}},
parentRefs: []gatev1.ParentReference{
@@ -11031,7 +10987,7 @@ func Test_matchingGatewayListener(t *testing.T) {
{
desc: "ParentRef is associated to all the listeners of its Gateway",
gateways: []gatewayWithListeners{{
gateway: &gatev1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "gateway", Namespace: "default"}},
gateway: &gatev1.Gateway{Name: "gateway", Namespace: "default"},
listeners: []gatewayListener{
{SectionName: "web"},
{SectionName: "websecure"},
@@ -11064,11 +11020,11 @@ func Test_matchingGatewayListener(t *testing.T) {
desc: "ParentRef is only associated to the referenced Gateway",
gateways: []gatewayWithListeners{
{
gateway: &gatev1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "gateway-a", Namespace: "default"}},
gateway: &gatev1.Gateway{Name: "gateway-a", Namespace: "default"},
listeners: []gatewayListener{{SectionName: "web"}},
},
{
gateway: &gatev1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "gateway-b", Namespace: "default"}},
gateway: &gatev1.Gateway{Name: "gateway-b", Namespace: "default"},
listeners: []gatewayListener{{SectionName: "web"}},
},
},
@@ -11096,7 +11052,7 @@ func Test_matchingGatewayListener(t *testing.T) {
// ResolvedRefs even though the route attaches to none of them.
desc: "ParentRef with a non-matching port still returns the Gateway listeners",
gateways: []gatewayWithListeners{{
gateway: &gatev1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "gateway", Namespace: "default"}},
gateway: &gatev1.Gateway{Name: "gateway", Namespace: "default"},
listeners: []gatewayListener{{SectionName: "web", Port: 80}},
}},
parentRefs: []gatev1.ParentReference{{
@@ -11456,7 +11412,7 @@ func Test_attachedRoutes_conflicts(t *testing.T) {
// Traefik instance (our controllerName but a parentRef not targeting a managed
// Gateway), while replacing our stale statuses with the freshly computed ones.
func Test_mergeRouteParentStatuses(t *testing.T) {
gateways := []gatewayWithListeners{{gateway: &gatev1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "my-gateway", Namespace: "default"}}}}
gateways := []gatewayWithListeners{{gateway: &gatev1.Gateway{Name: "my-gateway", Namespace: "default"}}}
otherController := gatev1.RouteParentStatus{
ControllerName: "example.com/other-controller",
@@ -11542,10 +11498,8 @@ func Test_managesGateway(t *testing.T) {
{
desc: "no scoping manages everything",
gw: &gatev1.Gateway{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "foo",
},
Namespace: "default",
Name: "foo",
},
want: true,
},
@@ -11553,10 +11507,8 @@ func Test_managesGateway(t *testing.T) {
desc: "matching ref",
gateways: []NamespacedName{{Namespace: "default", Name: "foo"}},
gw: &gatev1.Gateway{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "foo",
},
Namespace: "default",
Name: "foo",
},
want: true,
},
@@ -11564,10 +11516,8 @@ func Test_managesGateway(t *testing.T) {
desc: "different name",
gateways: []NamespacedName{{Namespace: "default", Name: "foo"}},
gw: &gatev1.Gateway{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "bar",
},
Namespace: "default",
Name: "bar",
},
want: false,
},
@@ -11575,10 +11525,8 @@ func Test_managesGateway(t *testing.T) {
desc: "different namespace",
gateways: []NamespacedName{{Namespace: "default", Name: "foo"}},
gw: &gatev1.Gateway{
ObjectMeta: metav1.ObjectMeta{
Namespace: "other",
Name: "foo",
},
Namespace: "other",
Name: "foo",
},
want: false,
},
@@ -11589,10 +11537,8 @@ func Test_managesGateway(t *testing.T) {
{Namespace: "other", Name: "bar"},
},
gw: &gatev1.Gateway{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "foo",
},
Namespace: "default",
Name: "foo",
},
want: true,
},
@@ -11603,10 +11549,8 @@ func Test_managesGateway(t *testing.T) {
{Namespace: "other", Name: "bar"},
},
gw: &gatev1.Gateway{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "baz",
},
Namespace: "default",
Name: "baz",
},
want: false,
},
@@ -12918,10 +12862,8 @@ func readResources(t *testing.T, paths []string) ([]runtime.Object, []runtime.Ob
func Test_listenerSetRefsGateway(t *testing.T) {
gw := &gatev1.Gateway{
ObjectMeta: metav1.ObjectMeta{
Name: "my-gw",
Namespace: "default",
},
Name: "my-gw",
Namespace: "default",
}
testCases := []struct {
@@ -12932,10 +12874,8 @@ func Test_listenerSetRefsGateway(t *testing.T) {
{
desc: "Matching reference",
ls: &gatev1.ListenerSet{
ObjectMeta: metav1.ObjectMeta{
Name: "my-ls",
Namespace: "default",
},
Name: "my-ls",
Namespace: "default",
Spec: gatev1.ListenerSetSpec{
ParentRef: gatev1.ParentGatewayReference{
Name: "my-gw",
@@ -12947,10 +12887,8 @@ func Test_listenerSetRefsGateway(t *testing.T) {
{
desc: "Wrong name",
ls: &gatev1.ListenerSet{
ObjectMeta: metav1.ObjectMeta{
Name: "my-ls",
Namespace: "default",
},
Name: "my-ls",
Namespace: "default",
Spec: gatev1.ListenerSetSpec{
ParentRef: gatev1.ParentGatewayReference{
Name: "other-gw",
@@ -12962,10 +12900,8 @@ func Test_listenerSetRefsGateway(t *testing.T) {
{
desc: "Wrong namespace",
ls: &gatev1.ListenerSet{
ObjectMeta: metav1.ObjectMeta{
Name: "my-ls",
Namespace: "default",
},
Name: "my-ls",
Namespace: "default",
Spec: gatev1.ListenerSetSpec{
ParentRef: gatev1.ParentGatewayReference{
Name: "my-gw",
@@ -12978,10 +12914,8 @@ func Test_listenerSetRefsGateway(t *testing.T) {
{
desc: "Cross-namespace matching",
ls: &gatev1.ListenerSet{
ObjectMeta: metav1.ObjectMeta{
Name: "my-ls",
Namespace: "other",
},
Name: "my-ls",
Namespace: "other",
Spec: gatev1.ListenerSetSpec{
ParentRef: gatev1.ParentGatewayReference{
Name: "my-gw",
@@ -12994,10 +12928,8 @@ func Test_listenerSetRefsGateway(t *testing.T) {
{
desc: "Wrong group",
ls: &gatev1.ListenerSet{
ObjectMeta: metav1.ObjectMeta{
Name: "my-ls",
Namespace: "default",
},
Name: "my-ls",
Namespace: "default",
Spec: gatev1.ListenerSetSpec{
ParentRef: gatev1.ParentGatewayReference{
Group: new(gatev1.Group("wrong.group")),
@@ -13010,10 +12942,8 @@ func Test_listenerSetRefsGateway(t *testing.T) {
{
desc: "Wrong kind",
ls: &gatev1.ListenerSet{
ObjectMeta: metav1.ObjectMeta{
Name: "my-ls",
Namespace: "default",
},
Name: "my-ls",
Namespace: "default",
Spec: gatev1.ListenerSetSpec{
ParentRef: gatev1.ParentGatewayReference{
Kind: new(gatev1.Kind("NotAGateway")),
@@ -13045,18 +12975,18 @@ func Test_isListenerSetAllowed(t *testing.T) {
{
desc: "Nil AllowedListeners",
gw: &gatev1.Gateway{
ObjectMeta: metav1.ObjectMeta{Name: "gw", Namespace: "default"},
Spec: gatev1.GatewaySpec{},
Name: "gw", Namespace: "default",
Spec: gatev1.GatewaySpec{},
},
ls: &gatev1.ListenerSet{
ObjectMeta: metav1.ObjectMeta{Name: "ls", Namespace: "default"},
Name: "ls", Namespace: "default",
},
expected: false,
},
{
desc: "Nil From",
gw: &gatev1.Gateway{
ObjectMeta: metav1.ObjectMeta{Name: "gw", Namespace: "default"},
Name: "gw", Namespace: "default",
Spec: gatev1.GatewaySpec{
AllowedListeners: &gatev1.AllowedListeners{
Namespaces: &gatev1.ListenerNamespaces{},
@@ -13064,14 +12994,14 @@ func Test_isListenerSetAllowed(t *testing.T) {
},
},
ls: &gatev1.ListenerSet{
ObjectMeta: metav1.ObjectMeta{Name: "ls", Namespace: "default"},
Name: "ls", Namespace: "default",
},
expected: false,
},
{
desc: "FromNone",
gw: &gatev1.Gateway{
ObjectMeta: metav1.ObjectMeta{Name: "gw", Namespace: "default"},
Name: "gw", Namespace: "default",
Spec: gatev1.GatewaySpec{
AllowedListeners: &gatev1.AllowedListeners{
Namespaces: &gatev1.ListenerNamespaces{
@@ -13081,14 +13011,14 @@ func Test_isListenerSetAllowed(t *testing.T) {
},
},
ls: &gatev1.ListenerSet{
ObjectMeta: metav1.ObjectMeta{Name: "ls", Namespace: "default"},
Name: "ls", Namespace: "default",
},
expected: false,
},
{
desc: "FromSame - same namespace",
gw: &gatev1.Gateway{
ObjectMeta: metav1.ObjectMeta{Name: "gw", Namespace: "default"},
Name: "gw", Namespace: "default",
Spec: gatev1.GatewaySpec{
AllowedListeners: &gatev1.AllowedListeners{
Namespaces: &gatev1.ListenerNamespaces{
@@ -13098,14 +13028,14 @@ func Test_isListenerSetAllowed(t *testing.T) {
},
},
ls: &gatev1.ListenerSet{
ObjectMeta: metav1.ObjectMeta{Name: "ls", Namespace: "default"},
Name: "ls", Namespace: "default",
},
expected: true,
},
{
desc: "FromSame - different namespace",
gw: &gatev1.Gateway{
ObjectMeta: metav1.ObjectMeta{Name: "gw", Namespace: "default"},
Name: "gw", Namespace: "default",
Spec: gatev1.GatewaySpec{
AllowedListeners: &gatev1.AllowedListeners{
Namespaces: &gatev1.ListenerNamespaces{
@@ -13115,14 +13045,14 @@ func Test_isListenerSetAllowed(t *testing.T) {
},
},
ls: &gatev1.ListenerSet{
ObjectMeta: metav1.ObjectMeta{Name: "ls", Namespace: "other"},
Name: "ls", Namespace: "other",
},
expected: false,
},
{
desc: "FromAll",
gw: &gatev1.Gateway{
ObjectMeta: metav1.ObjectMeta{Name: "gw", Namespace: "default"},
Name: "gw", Namespace: "default",
Spec: gatev1.GatewaySpec{
AllowedListeners: &gatev1.AllowedListeners{
Namespaces: &gatev1.ListenerNamespaces{
@@ -13132,14 +13062,14 @@ func Test_isListenerSetAllowed(t *testing.T) {
},
},
ls: &gatev1.ListenerSet{
ObjectMeta: metav1.ObjectMeta{Name: "ls", Namespace: "any-namespace"},
Name: "ls", Namespace: "any-namespace",
},
expected: true,
},
{
desc: "FromSelector - matching namespace labels",
gw: &gatev1.Gateway{
ObjectMeta: metav1.ObjectMeta{Name: "gw", Namespace: "default"},
Name: "gw", Namespace: "default",
Spec: gatev1.GatewaySpec{
AllowedListeners: &gatev1.AllowedListeners{
Namespaces: &gatev1.ListenerNamespaces{
@@ -13152,17 +13082,13 @@ func Test_isListenerSetAllowed(t *testing.T) {
},
},
ls: &gatev1.ListenerSet{
ObjectMeta: metav1.ObjectMeta{
Name: "ls",
Namespace: "default",
},
Name: "ls",
Namespace: "default",
},
namespaces: []*corev1.Namespace{
{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Labels: map[string]string{"env": "prod"},
},
Name: "default",
Labels: map[string]string{"env": "prod"},
},
},
expected: true,
@@ -13170,7 +13096,7 @@ func Test_isListenerSetAllowed(t *testing.T) {
{
desc: "FromSelector - non-matching namespace labels",
gw: &gatev1.Gateway{
ObjectMeta: metav1.ObjectMeta{Name: "gw", Namespace: "default"},
Name: "gw", Namespace: "default",
Spec: gatev1.GatewaySpec{
AllowedListeners: &gatev1.AllowedListeners{
Namespaces: &gatev1.ListenerNamespaces{
@@ -13183,17 +13109,13 @@ func Test_isListenerSetAllowed(t *testing.T) {
},
},
ls: &gatev1.ListenerSet{
ObjectMeta: metav1.ObjectMeta{
Name: "ls",
Namespace: "default",
},
Name: "ls",
Namespace: "default",
},
namespaces: []*corev1.Namespace{
{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Labels: map[string]string{"env": "staging"},
},
Name: "default",
Labels: map[string]string{"env": "staging"},
},
},
expected: false,
@@ -13250,7 +13172,7 @@ func Test_makeListenerSetStatus(t *testing.T) {
listeners: []gatewayListener{
{
SectionName: "http",
ListenerSet: &gatev1.ListenerSet{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "my-ls"}},
ListenerSet: &gatev1.ListenerSet{Namespace: "default", Name: "my-ls"},
Status: &gatev1.ListenerStatus{
Name: "http",
SupportedKinds: []gatev1.RouteGroupKind{{Kind: "HTTPRoute", Group: new(gatev1.Group(gatev1.GroupName))}},
@@ -13275,7 +13197,7 @@ func Test_makeListenerSetStatus(t *testing.T) {
listeners: []gatewayListener{
{
SectionName: "https",
ListenerSet: &gatev1.ListenerSet{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "my-ls"}},
ListenerSet: &gatev1.ListenerSet{Namespace: "default", Name: "my-ls"},
Status: &gatev1.ListenerStatus{
Name: "https",
Conditions: []metav1.Condition{
@@ -13300,7 +13222,7 @@ func Test_makeListenerSetStatus(t *testing.T) {
listeners: []gatewayListener{
{
SectionName: "http",
ListenerSet: &gatev1.ListenerSet{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "my-ls"}},
ListenerSet: &gatev1.ListenerSet{Namespace: "default", Name: "my-ls"},
Status: &gatev1.ListenerStatus{
Name: "http",
Conditions: []metav1.Condition{
@@ -13325,7 +13247,7 @@ func Test_makeListenerSetStatus(t *testing.T) {
listeners: []gatewayListener{
{
SectionName: "http",
ListenerSet: &gatev1.ListenerSet{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "my-ls"}},
ListenerSet: &gatev1.ListenerSet{Namespace: "default", Name: "my-ls"},
Status: &gatev1.ListenerStatus{
Name: "http",
Conditions: []metav1.Condition{},
@@ -13333,7 +13255,7 @@ func Test_makeListenerSetStatus(t *testing.T) {
},
{
SectionName: "invalid",
ListenerSet: &gatev1.ListenerSet{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "my-ls"}},
ListenerSet: &gatev1.ListenerSet{Namespace: "default", Name: "my-ls"},
Status: &gatev1.ListenerStatus{
Name: "invalid",
Conditions: []metav1.Condition{
@@ -13407,7 +13329,7 @@ func Test_makeListenerSetStatus(t *testing.T) {
// and the status of the ListenerSets the Gateway does not allow.
func Test_loadListenerSetListeners(t *testing.T) {
gateway := &gatev1.Gateway{
ObjectMeta: metav1.ObjectMeta{Name: "my-gateway", Namespace: "default"},
Name: "my-gateway", Namespace: "default",
Spec: gatev1.GatewaySpec{
AllowedListeners: &gatev1.AllowedListeners{
Namespaces: &gatev1.ListenerNamespaces{From: new(gatev1.NamespacesFromAll)},
@@ -13417,11 +13339,9 @@ func Test_loadListenerSetListeners(t *testing.T) {
listenerSet := func(name string, created time.Time, listeners ...gatev1.ListenerEntry) *gatev1.ListenerSet {
return &gatev1.ListenerSet{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "default",
CreationTimestamp: metav1.NewTime(created),
},
Name: name,
Namespace: "default",
CreationTimestamp: metav1.NewTime(created),
Spec: gatev1.ListenerSetSpec{
ParentRef: gatev1.ParentGatewayReference{Name: "my-gateway"},
Listeners: listeners,
+43 -18
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"maps"
"net"
"regexp"
"slices"
@@ -16,6 +17,7 @@ import (
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/provider"
"github.com/traefik/traefik/v3/pkg/provider/kubernetes/k8s"
"github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/types"
corev1 "k8s.io/api/core/v1"
@@ -437,7 +439,7 @@ func (p *Provider) build(ctx context.Context, ingressClasses []*netv1.IngressCla
// Pre-resolve basic auth.
if ing.config.AuthType != nil {
basic, digest, err := p.resolveBasicAuth(ing.Namespace, ing.config)
basic, digest, authURNRefs, err := p.resolveBasicAuth(ing.Namespace, ing.config)
if err != nil {
logger.Error().
Err(err).
@@ -449,6 +451,7 @@ func (p *Provider) build(ctx context.Context, ingressClasses []*netv1.IngressCla
}
loc.BasicAuth = basic
loc.DigestAuth = digest
loc.AuthURNRefs = authURNRefs
}
// Pre-resolve custom headers ConfigMap.
@@ -969,12 +972,13 @@ func (p *Provider) buildClientAuthTLSOption(ingressNamespace string, cfg Ingress
// then skips the ingress, as ingress-nginx does.
func (p *Provider) applyDefaultBackendAuth(namespace string, cfg IngressConfig, loc *location) error {
if cfg.AuthType != nil {
basic, digest, err := p.resolveBasicAuth(namespace, cfg)
basic, digest, authURNRefs, err := p.resolveBasicAuth(namespace, cfg)
if err != nil {
return err
}
loc.BasicAuth = basic
loc.DigestAuth = digest
loc.AuthURNRefs = authURNRefs
}
if cfg.CustomHeaders != nil {
@@ -1028,24 +1032,26 @@ func (p *Provider) resolveDefaultBackendCustomErrors(mc *model, namespace string
}
}
func (p *Provider) resolveBasicAuth(namespace string, cfg IngressConfig) (*dynamic.BasicAuth, *dynamic.DigestAuth, error) {
// resolveBasicAuth returns the basic or digest auth configuration, and the URN references of its users.
// The users hold the URNs of their Secret keys, and the returned URN references hold their values.
func (p *Provider) resolveBasicAuth(namespace string, cfg IngressConfig) (*dynamic.BasicAuth, *dynamic.DigestAuth, map[string]string, error) {
if cfg.AuthType == nil {
return nil, nil, nil
return nil, nil, nil, nil
}
authType := *cfg.AuthType
if authType != "basic" && authType != "digest" {
return nil, nil, fmt.Errorf("invalid auth-type %q", authType)
return nil, nil, nil, fmt.Errorf("invalid auth-type %q", authType)
}
authSecret := ptr.Deref(cfg.AuthSecret, "")
if authSecret == "" {
return nil, nil, errors.New("auth-secret must not be empty")
return nil, nil, nil, errors.New("auth-secret must not be empty")
}
parts := strings.Split(authSecret, "/")
if len(parts) > 2 {
return nil, nil, fmt.Errorf("invalid auth secret format %q", authSecret)
return nil, nil, nil, fmt.Errorf("invalid auth secret format %q", authSecret)
}
secretName := parts[0]
@@ -1056,29 +1062,41 @@ func (p *Provider) resolveBasicAuth(namespace string, cfg IngressConfig) (*dynam
}
if !p.AllowCrossNamespaceResources && secretNamespace != namespace {
return nil, nil, fmt.Errorf("cross-namespace auth secret not allowed: %s/%s", secretNamespace, secretName)
return nil, nil, nil, fmt.Errorf("cross-namespace auth secret not allowed: %s/%s", secretNamespace, secretName)
}
secret, err := p.k8sClient.GetSecret(secretNamespace, secretName)
if err != nil {
return nil, nil, fmt.Errorf("getting secret %s: %w", authSecret, err)
return nil, nil, nil, fmt.Errorf("getting secret %s: %w", authSecret, err)
}
authSecretType := ptr.Deref(cfg.AuthSecretType, "auth-file")
if authSecretType != "auth-file" && authSecretType != "auth-map" {
return nil, nil, fmt.Errorf("invalid auth-secret-type %q", authSecretType)
return nil, nil, nil, fmt.Errorf("invalid auth-secret-type %q", authSecretType)
}
users, err := basicAuthUsers(secret, authSecretType)
if err != nil {
return nil, nil, fmt.Errorf("getting users from secret: %w", err)
return nil, nil, nil, fmt.Errorf("getting users from secret: %w", err)
}
middlewareType := "basicAuth"
if authType == "digest" {
middlewareType = "digestAuth"
}
// With an auth-map Secret, each user comes from its own key, so each user has its own URN and URNRefs.AddUsers, which takes one key, does not fit.
refs := k8s.URNRefs{}
userURNs := make(dynamic.Users, len(users))
for i, user := range users {
userURNs[i] = refs.Add(dynamic.JSONPointer(middlewareType, "users", strconv.Itoa(i)), authSecret, user.key, user.value)
}
realm := ptr.Deref(cfg.AuthRealm, "")
if authType == "digest" {
return nil, &dynamic.DigestAuth{Users: dynamic.Users(users), Realm: realm}, nil
return nil, &dynamic.DigestAuth{Users: userURNs, Realm: realm}, refs.OrNil(), nil
}
return &dynamic.BasicAuth{Users: dynamic.Users(users), Realm: realm}, nil, nil
return &dynamic.BasicAuth{Users: userURNs, Realm: realm}, nil, refs.OrNil(), nil
}
func (p *Provider) resolveCustomHeaders(namespace, customHeadersRef string) (map[string]string, error) {
@@ -1230,14 +1248,21 @@ func clientAuthTypeFromString(verifyClient *string) string {
}
}
func basicAuthUsers(secret *corev1.Secret, authSecretType string) ([]string, error) {
var users []string
// secretUser is a user read from a Secret, with the key of the Secret holding it.
type secretUser struct {
key string
value string
}
func basicAuthUsers(secret *corev1.Secret, authSecretType string) ([]secretUser, error) {
var users []secretUser
if authSecretType == "auth-map" {
if len(secret.Data) == 0 {
return nil, fmt.Errorf("secret %s/%s contains no user credentials", secret.Namespace, secret.Name)
}
for user, pass := range secret.Data {
users = append(users, user+":"+string(pass))
// Sorted, so that an unchanged Secret gives the same configuration and does not trigger a reload.
for _, user := range slices.Sorted(maps.Keys(secret.Data)) {
users = append(users, secretUser{key: user, value: user + ":" + string(secret.Data[user])})
}
return users, nil
}
@@ -1250,7 +1275,7 @@ func basicAuthUsers(secret *corev1.Secret, authSecretType string) ([]string, err
for rawLine := range strings.SplitSeq(string(authFileContent), "\n") {
line := strings.TrimSpace(rawLine)
if line != "" && !strings.HasPrefix(line, "#") {
users = append(users, line)
users = append(users, secretUser{key: "auth", value: line})
}
}
@@ -0,0 +1,79 @@
package ingressnginx
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
corev1 "k8s.io/api/core/v1"
kubefake "k8s.io/client-go/kubernetes/fake"
)
func Test_basicAuthUsers(t *testing.T) {
t.Parallel()
testCases := []struct {
desc string
authSecretType string
data map[string][]byte
expected []secretUser
}{
{
desc: "auth-file users are read from the auth key, without comments and blank lines",
authSecretType: "auth-file",
data: map[string][]byte{"auth": []byte("# comment\nuser1:hash1\n\n user2:hash2 \n")},
expected: []secretUser{
{key: "auth", value: "user1:hash1"},
{key: "auth", value: "user2:hash2"},
},
},
{
desc: "auth-map users are sorted by key",
authSecretType: "auth-map",
data: map[string][]byte{"user2": []byte("hash2"), "user1": []byte("hash1"), "user3": []byte("hash3")},
expected: []secretUser{
{key: "user1", value: "user1:hash1"},
{key: "user2", value: "user2:hash2"},
{key: "user3", value: "user3:hash3"},
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
users, err := basicAuthUsers(&corev1.Secret{Data: test.data}, test.authSecretType)
require.NoError(t, err)
assert.Equal(t, test.expected, users)
})
}
}
func TestProvider_resolveBasicAuth_authMap(t *testing.T) {
t.Parallel()
kubeClient := kubefake.NewClientset(&corev1.Secret{
Name: "basic-auth", Namespace: "default",
Data: map[string][]byte{"user2": []byte("hash2"), "user1": []byte("hash1")},
})
client := newClient(kubeClient)
_, err := client.WatchAll(t.Context(), "", "")
require.NoError(t, err)
p := Provider{k8sClient: client}
basic, digest, urnRefs, err := p.resolveBasicAuth("default", IngressConfig{
AuthType: new("basic"),
AuthSecret: new("basic-auth"),
AuthSecretType: new("auth-map"),
})
require.NoError(t, err)
assert.Equal(t, &dynamic.BasicAuth{Users: dynamic.Users{"urn:k8s:secret:basic-auth:user1", "urn:k8s:secret:basic-auth:user2"}}, basic)
assert.Nil(t, digest)
assert.Equal(t, map[string]string{"/basicAuth/users/0": "user1:hash1", "/basicAuth/users/1": "user2:hash2"}, urnRefs)
}
@@ -94,18 +94,14 @@ func TestIsLoadBalancerIngressEquals(t *testing.T) {
func TestClientIgnoresHelmOwnedSecrets(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "secret",
},
Namespace: "default",
Name: "secret",
}
helmSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "helm-secret",
Labels: map[string]string{
"owner": "helm",
},
Namespace: "default",
Name: "helm-secret",
Labels: map[string]string{
"owner": "helm",
},
}
@@ -146,13 +142,11 @@ func TestClientIgnoresHelmOwnedSecrets(t *testing.T) {
func TestClientIgnoresEmptyEndpointSliceUpdates(t *testing.T) {
emptyEndpointSlice := &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
Name: "empty-endpointslice",
Namespace: "test",
ResourceVersion: "1244",
Annotations: map[string]string{
"test-annotation": "_",
},
Name: "empty-endpointslice",
Namespace: "test",
ResourceVersion: "1244",
Annotations: map[string]string{
"test-annotation": "_",
},
}
@@ -161,12 +155,10 @@ func TestClientIgnoresEmptyEndpointSliceUpdates(t *testing.T) {
samplePortProtocol := corev1.ProtocolTCP
sampleAddressReady := true
filledEndpointSlice := &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
Name: "filled-endpointslice",
Namespace: "test",
ResourceVersion: "1234",
},
AddressType: discoveryv1.AddressTypeIPv4,
Name: "filled-endpointslice",
Namespace: "test",
ResourceVersion: "1234",
AddressType: discoveryv1.AddressTypeIPv4,
Endpoints: []discoveryv1.Endpoint{{
Addresses: []string{"10.13.37.1"},
Conditions: discoveryv1.EndpointConditions{
@@ -1314,10 +1314,8 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
},
},
@@ -1572,18 +1570,24 @@ func TestLoadIngresses(t *testing.T) {
"default-ingress-with-basicauth-rule-0-path-0-basic-auth": {
BasicAuth: &dynamic.BasicAuth{
Users: dynamic.Users{
"user:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=",
"urn:k8s:secret:default/basic-auth:auth",
},
Realm: "Authentication Required",
},
URNRefs: map[string]string{
"/basicAuth/users/0": "user:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=",
},
},
"default-ingress-with-basicauth-rule-0-path-0-tls-basic-auth": {
BasicAuth: &dynamic.BasicAuth{
Users: dynamic.Users{
"user:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=",
"urn:k8s:secret:default/basic-auth:auth",
},
Realm: "Authentication Required",
},
URNRefs: map[string]string{
"/basicAuth/users/0": "user:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=",
},
},
"default-ingress-with-basicauth-whoami-80-remove-authorization-header": {
Headers: &dynamic.Headers{
@@ -1708,18 +1712,24 @@ func TestLoadIngresses(t *testing.T) {
"default-ingress-with-digestauth-rule-0-path-0-digest-auth": {
DigestAuth: &dynamic.DigestAuth{
Users: dynamic.Users{
"user:Authentication Required:0a132d8fa4138c56c1c2b50e9c9d1a1f",
"urn:k8s:secret:default/digest-auth:auth",
},
Realm: "Authentication Required",
},
URNRefs: map[string]string{
"/digestAuth/users/0": "user:Authentication Required:0a132d8fa4138c56c1c2b50e9c9d1a1f",
},
},
"default-ingress-with-digestauth-rule-0-path-0-tls-digest-auth": {
DigestAuth: &dynamic.DigestAuth{
Users: dynamic.Users{
"user:Authentication Required:0a132d8fa4138c56c1c2b50e9c9d1a1f",
"urn:k8s:secret:default/digest-auth:auth",
},
Realm: "Authentication Required",
},
URNRefs: map[string]string{
"/digestAuth/users/0": "user:Authentication Required:0a132d8fa4138c56c1c2b50e9c9d1a1f",
},
},
"default-ingress-with-digestauth-whoami-80-remove-authorization-header": {
Headers: &dynamic.Headers{
@@ -2872,10 +2882,8 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
},
},
@@ -3332,10 +3340,13 @@ func TestLoadIngresses(t *testing.T) {
"default-ingress-with-ssl-passthrough-and-auth-rule-0-path-0-basic-auth": {
BasicAuth: &dynamic.BasicAuth{
Users: dynamic.Users{
"user:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=",
"urn:k8s:secret:default/basic-auth:auth",
},
Realm: "Authentication Required",
},
URNRefs: map[string]string{
"/basicAuth/users/0": "user:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=",
},
},
"default-ingress-with-ssl-passthrough-and-auth-rule-0-path-0-allowed-source-range": {
IPAllowList: &dynamic.IPAllowList{
@@ -3438,10 +3449,8 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
},
},
@@ -3723,10 +3732,8 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
},
},
@@ -8519,18 +8526,24 @@ func TestLoadIngresses(t *testing.T) {
"default-backend-basic-auth": {
BasicAuth: &dynamic.BasicAuth{
Users: dynamic.Users{
"user:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=",
"urn:k8s:secret:default/basic-auth:auth",
},
Realm: "Authentication Required",
},
URNRefs: map[string]string{
"/basicAuth/users/0": "user:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=",
},
},
"default-backend-tls-basic-auth": {
BasicAuth: &dynamic.BasicAuth{
Users: dynamic.Users{
"user:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=",
"urn:k8s:secret:default/basic-auth:auth",
},
Realm: "Authentication Required",
},
URNRefs: map[string]string{
"/basicAuth/users/0": "user:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=",
},
},
"default-backend-retry": {
Retry: &dynamic.Retry{
@@ -8946,27 +8959,39 @@ func TestLoadIngresses(t *testing.T) {
Middlewares: map[string]*dynamic.Middleware{
"default-ingress-with-default-backend-per-host-basic-auth-default-backend-basic-auth": {
BasicAuth: &dynamic.BasicAuth{
Users: dynamic.Users{"user:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g="},
Users: dynamic.Users{"urn:k8s:secret:default/basic-auth:auth"},
Realm: "Authentication Required",
},
URNRefs: map[string]string{
"/basicAuth/users/0": "user:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=",
},
},
"default-ingress-with-default-backend-per-host-basic-auth-default-backend-tls-basic-auth": {
BasicAuth: &dynamic.BasicAuth{
Users: dynamic.Users{"user:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g="},
Users: dynamic.Users{"urn:k8s:secret:default/basic-auth:auth"},
Realm: "Authentication Required",
},
URNRefs: map[string]string{
"/basicAuth/users/0": "user:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=",
},
},
"default-ingress-with-default-backend-per-host-basic-auth-rule-0-path-0-basic-auth": {
BasicAuth: &dynamic.BasicAuth{
Users: dynamic.Users{"user:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g="},
Users: dynamic.Users{"urn:k8s:secret:default/basic-auth:auth"},
Realm: "Authentication Required",
},
URNRefs: map[string]string{
"/basicAuth/users/0": "user:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=",
},
},
"default-ingress-with-default-backend-per-host-basic-auth-rule-0-path-0-tls-basic-auth": {
BasicAuth: &dynamic.BasicAuth{
Users: dynamic.Users{"user:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g="},
Users: dynamic.Users{"urn:k8s:secret:default/basic-auth:auth"},
Realm: "Authentication Required",
},
URNRefs: map[string]string{
"/basicAuth/users/0": "user:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=",
},
},
"default-ingress-with-default-backend-per-host-basic-auth-default-backend-remove-authorization-header": {
Headers: &dynamic.Headers{
@@ -9888,10 +9913,8 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
},
},
@@ -12916,10 +12939,8 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
},
Options: map[string]tls.Options{
@@ -13690,10 +13711,8 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
},
Options: map[string]tls.Options{
@@ -13940,10 +13959,8 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
},
Options: map[string]tls.Options{
@@ -14263,10 +14280,8 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
},
Options: map[string]tls.Options{
@@ -17267,10 +17282,8 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
},
Options: map[string]tls.Options{
@@ -17433,10 +17446,8 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
},
Options: map[string]tls.Options{
@@ -21654,10 +21665,8 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
},
},
@@ -23311,10 +23320,8 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
},
},
@@ -23845,16 +23852,12 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
{
Certificate: tls.Certificate{
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
},
},
@@ -23976,10 +23979,8 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
},
},
@@ -24122,10 +24123,8 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
},
Options: map[string]tls.Options{
@@ -24944,10 +24943,8 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
},
},
@@ -25207,6 +25204,11 @@ func TestLoadIngresses(t *testing.T) {
conf := p.loadConfiguration(t.Context())
assert.Equal(t, test.expected, conf)
for name, middleware := range conf.HTTP.Middlewares {
_, err := middleware.ResolveURNRefs()
assert.NoError(t, err, name)
}
})
}
}
@@ -130,6 +130,9 @@ type location struct {
// DigestAuth holds the resolved digest-auth middleware (nil unless auth-type=digest).
DigestAuth *dynamic.DigestAuth
// AuthURNRefs holds the values of the users of BasicAuth or DigestAuth, which only hold their URNs.
AuthURNRefs map[string]string
// ResolvedCustomHeaders holds the pre-fetched custom response headers.
// Populated in Phase 1 from the custom-headers ConfigMap.
ResolvedCustomHeaders map[string]string
@@ -43,10 +43,8 @@ func (p *Provider) translate(ctx context.Context, mc *model) *dynamic.Configurat
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),
},
CertFile: types.FileOrContent(cp.Cert),
KeyFile: types.FileOrContent(cp.Key),
})
}
@@ -562,13 +560,13 @@ func (p *Provider) buildMiddlewareChain(mc *model, loc *location, routerKey stri
if loc.BasicAuth != nil {
name := routerKey + "-basic-auth"
conf.HTTP.Middlewares[name] = &dynamic.Middleware{BasicAuth: loc.BasicAuth}
conf.HTTP.Middlewares[name] = &dynamic.Middleware{BasicAuth: loc.BasicAuth, URNRefs: loc.AuthURNRefs}
rt.Middlewares = append(rt.Middlewares, name)
}
if loc.DigestAuth != nil {
name := routerKey + "-digest-auth"
conf.HTTP.Middlewares[name] = &dynamic.Middleware{DigestAuth: loc.DigestAuth}
conf.HTTP.Middlewares[name] = &dynamic.Middleware{DigestAuth: loc.DigestAuth, URNRefs: loc.AuthURNRefs}
rt.Middlewares = append(rt.Middlewares, name)
}
+15 -23
View File
@@ -134,18 +134,14 @@ func TestIsLoadBalancerIngressEquals(t *testing.T) {
func TestClientIgnoresHelmOwnedSecrets(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "secret",
},
Namespace: "default",
Name: "secret",
}
helmSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "helm-secret",
Labels: map[string]string{
"owner": "helm",
},
Namespace: "default",
Name: "helm-secret",
Labels: map[string]string{
"owner": "helm",
},
}
@@ -190,13 +186,11 @@ func TestClientIgnoresHelmOwnedSecrets(t *testing.T) {
func TestClientIgnoresEmptyEndpointSliceUpdates(t *testing.T) {
emptyEndpointSlice := &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
Name: "empty-endpointslice",
Namespace: "test",
ResourceVersion: "1244",
Annotations: map[string]string{
"test-annotation": "_",
},
Name: "empty-endpointslice",
Namespace: "test",
ResourceVersion: "1244",
Annotations: map[string]string{
"test-annotation": "_",
},
}
@@ -205,12 +199,10 @@ func TestClientIgnoresEmptyEndpointSliceUpdates(t *testing.T) {
samplePortProtocol := corev1.ProtocolTCP
sampleAddressReady := true
filledEndpointSlice := &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
Name: "filled-endpointslice",
Namespace: "test",
ResourceVersion: "1234",
},
AddressType: discoveryv1.AddressTypeIPv4,
Name: "filled-endpointslice",
Namespace: "test",
ResourceVersion: "1234",
AddressType: discoveryv1.AddressTypeIPv4,
Endpoints: []discoveryv1.Endpoint{{
Addresses: []string{"10.13.37.1"},
Conditions: discoveryv1.EndpointConditions{
@@ -863,10 +863,8 @@ func getCertificates(ctx context.Context, ingress *netv1.Ingress, k8sClient Clie
}
tlsConfigs[configKey] = &tls.CertAndStores{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(cert),
KeyFile: types.FileOrContent(key),
},
CertFile: types.FileOrContent(cert),
KeyFile: types.FileOrContent(key),
}
}
}
@@ -1389,10 +1389,8 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
KeyFile: types.FileOrContent("-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----"),
},
CertFile: types.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
KeyFile: types.FileOrContent("-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----"),
},
},
},
@@ -3221,10 +3219,8 @@ func TestGetCertificates(t *testing.T) {
client: clientMock{
secrets: []*corev1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "testing",
},
Name: "test-secret",
Namespace: "testing",
Data: map[string][]byte{
"tls.key": []byte("tls-key"),
},
@@ -3239,10 +3235,8 @@ func TestGetCertificates(t *testing.T) {
client: clientMock{
secrets: []*corev1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "testing",
},
Name: "test-secret",
Namespace: "testing",
Data: map[string][]byte{
"tls.crt": []byte("tls-crt"),
},
@@ -3257,11 +3251,9 @@ func TestGetCertificates(t *testing.T) {
client: clientMock{
secrets: []*corev1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "testing",
},
Data: map[string][]byte{},
Name: "test-secret",
Namespace: "testing",
Data: map[string][]byte{},
},
},
},
@@ -3284,20 +3276,16 @@ func TestGetCertificates(t *testing.T) {
client: clientMock{
secrets: []*corev1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret2",
Namespace: "testing",
},
Name: "test-secret2",
Namespace: "testing",
Data: map[string][]byte{
"tls.crt": []byte("tls-crt"),
"tls.key": []byte("tls-key"),
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "testing",
},
Name: "test-secret",
Namespace: "testing",
Data: map[string][]byte{
"tls.crt": []byte("tls-crt"),
"tls.key": []byte("tls-key"),
@@ -25,34 +25,26 @@ func Test_EndpointSlicesByServiceName(t *testing.T) {
svcName: "app",
endpointSlices: []*discoveryv1.EndpointSlice{
{
ObjectMeta: metav1.ObjectMeta{
Name: "slice-a",
Namespace: "default",
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
Name: "slice-a",
Namespace: "default",
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "slice-b",
Namespace: "default",
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
Name: "slice-b",
Namespace: "default",
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
},
expected: []*discoveryv1.EndpointSlice{
{
ObjectMeta: metav1.ObjectMeta{
Name: "slice-a",
Namespace: "default",
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
Name: "slice-a",
Namespace: "default",
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "slice-b",
Namespace: "default",
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
Name: "slice-b",
Namespace: "default",
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
},
},
@@ -62,11 +54,9 @@ func Test_EndpointSlicesByServiceName(t *testing.T) {
svcName: "unknown",
endpointSlices: []*discoveryv1.EndpointSlice{
{
ObjectMeta: metav1.ObjectMeta{
Name: "slice-a",
Namespace: "default",
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
Name: "slice-a",
Namespace: "default",
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
},
expected: []*discoveryv1.EndpointSlice{},
@@ -77,11 +67,9 @@ func Test_EndpointSlicesByServiceName(t *testing.T) {
svcName: "app",
endpointSlices: []*discoveryv1.EndpointSlice{
{
ObjectMeta: metav1.ObjectMeta{
Name: "slice-a",
Namespace: "other",
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
Name: "slice-a",
Namespace: "other",
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
},
expected: []*discoveryv1.EndpointSlice{},
@@ -92,11 +80,9 @@ func Test_EndpointSlicesByServiceName(t *testing.T) {
svcName: "app",
endpointSlices: []*discoveryv1.EndpointSlice{
{
ObjectMeta: metav1.ObjectMeta{
Name: "slice-a",
Namespace: "default",
Labels: map[string]string{discoveryv1.LabelServiceName: "other"},
},
Name: "slice-a",
Namespace: "default",
Labels: map[string]string{discoveryv1.LabelServiceName: "other"},
},
},
expected: []*discoveryv1.EndpointSlice{},
@@ -107,34 +93,26 @@ func Test_EndpointSlicesByServiceName(t *testing.T) {
svcName: "app",
endpointSlices: []*discoveryv1.EndpointSlice{
{
ObjectMeta: metav1.ObjectMeta{
Name: "slice-b",
Namespace: "default",
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
Name: "slice-b",
Namespace: "default",
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "slice-a",
Namespace: "default",
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
Name: "slice-a",
Namespace: "default",
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
},
expected: []*discoveryv1.EndpointSlice{
{
ObjectMeta: metav1.ObjectMeta{
Name: "slice-a",
Namespace: "default",
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
Name: "slice-a",
Namespace: "default",
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "slice-b",
Namespace: "default",
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
Name: "slice-b",
Namespace: "default",
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
},
},
@@ -144,38 +122,30 @@ func Test_EndpointSlicesByServiceName(t *testing.T) {
svcName: "app",
endpointSlices: []*discoveryv1.EndpointSlice{
{
ObjectMeta: metav1.ObjectMeta{
Name: "slice-a",
Namespace: "default",
CreationTimestamp: metav1.NewTime(time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC)),
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
Name: "slice-a",
Namespace: "default",
CreationTimestamp: metav1.NewTime(time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC)),
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "slice-b",
Namespace: "default",
CreationTimestamp: metav1.NewTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)),
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
Name: "slice-b",
Namespace: "default",
CreationTimestamp: metav1.NewTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)),
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
},
expected: []*discoveryv1.EndpointSlice{
{
ObjectMeta: metav1.ObjectMeta{
Name: "slice-b",
Namespace: "default",
CreationTimestamp: metav1.NewTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)),
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
Name: "slice-b",
Namespace: "default",
CreationTimestamp: metav1.NewTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)),
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "slice-a",
Namespace: "default",
CreationTimestamp: metav1.NewTime(time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC)),
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
Name: "slice-a",
Namespace: "default",
CreationTimestamp: metav1.NewTime(time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC)),
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
},
},
@@ -185,38 +155,30 @@ func Test_EndpointSlicesByServiceName(t *testing.T) {
svcName: "app",
endpointSlices: []*discoveryv1.EndpointSlice{
{
ObjectMeta: metav1.ObjectMeta{
Name: "slice-a",
Namespace: "default",
CreationTimestamp: metav1.NewTime(time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC)),
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
Name: "slice-a",
Namespace: "default",
CreationTimestamp: metav1.NewTime(time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC)),
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "slice-z",
Namespace: "default",
CreationTimestamp: metav1.NewTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)),
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
Name: "slice-z",
Namespace: "default",
CreationTimestamp: metav1.NewTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)),
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
},
expected: []*discoveryv1.EndpointSlice{
{
ObjectMeta: metav1.ObjectMeta{
Name: "slice-z",
Namespace: "default",
CreationTimestamp: metav1.NewTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)),
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
Name: "slice-z",
Namespace: "default",
CreationTimestamp: metav1.NewTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)),
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "slice-a",
Namespace: "default",
CreationTimestamp: metav1.NewTime(time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC)),
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
Name: "slice-a",
Namespace: "default",
CreationTimestamp: metav1.NewTime(time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC)),
Labels: map[string]string{discoveryv1.LabelServiceName: "app"},
},
},
},
+52 -125
View File
@@ -6,7 +6,6 @@ import (
"github.com/stretchr/testify/assert"
discoveryv1 "k8s.io/api/discovery/v1"
netv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func Test_detectChanges(t *testing.T) {
@@ -38,141 +37,105 @@ func Test_detectChanges(t *testing.T) {
{
name: "With same version",
oldObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "1",
},
ResourceVersion: "1",
},
newObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "1",
},
ResourceVersion: "1",
},
},
{
name: "With different version",
oldObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "1",
},
ResourceVersion: "1",
},
newObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "2",
},
ResourceVersion: "2",
},
},
{
name: "Ingress With same version",
oldObj: &netv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "1",
},
ResourceVersion: "1",
},
newObj: &netv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "1",
},
ResourceVersion: "1",
},
},
{
name: "Ingress With different version",
oldObj: &netv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "1",
},
ResourceVersion: "1",
},
newObj: &netv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "2",
},
ResourceVersion: "2",
},
want: true,
},
{
name: "With same annotations",
oldObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "1",
Annotations: map[string]string{
"test-annotation": "_",
},
ResourceVersion: "1",
Annotations: map[string]string{
"test-annotation": "_",
},
},
newObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "2",
Annotations: map[string]string{
"test-annotation": "_",
},
ResourceVersion: "2",
Annotations: map[string]string{
"test-annotation": "_",
},
},
},
{
name: "With different annotations",
oldObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "1",
Annotations: map[string]string{
"test-annotation": "V",
},
ResourceVersion: "1",
Annotations: map[string]string{
"test-annotation": "V",
},
},
newObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "2",
Annotations: map[string]string{
"test-annotation": "X",
},
ResourceVersion: "2",
Annotations: map[string]string{
"test-annotation": "X",
},
},
},
{
name: "With same endpoints and ports",
oldObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "1",
},
Endpoints: []discoveryv1.Endpoint{},
Ports: []discoveryv1.EndpointPort{},
ResourceVersion: "1",
Endpoints: []discoveryv1.Endpoint{},
Ports: []discoveryv1.EndpointPort{},
},
newObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "1",
},
Endpoints: []discoveryv1.Endpoint{},
Ports: []discoveryv1.EndpointPort{},
ResourceVersion: "1",
Endpoints: []discoveryv1.Endpoint{},
Ports: []discoveryv1.EndpointPort{},
},
},
{
name: "With different len of endpoints",
oldObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "1",
},
Endpoints: []discoveryv1.Endpoint{},
ResourceVersion: "1",
Endpoints: []discoveryv1.Endpoint{},
},
newObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "2",
},
Endpoints: []discoveryv1.Endpoint{{}},
ResourceVersion: "2",
Endpoints: []discoveryv1.Endpoint{{}},
},
want: true,
},
{
name: "With different endpoints",
oldObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "1",
},
ResourceVersion: "1",
Endpoints: []discoveryv1.Endpoint{{
Addresses: []string{"10.10.10.10"},
}},
},
newObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "2",
},
ResourceVersion: "2",
Endpoints: []discoveryv1.Endpoint{{
Addresses: []string{"10.10.10.11"},
}},
@@ -182,18 +145,14 @@ func Test_detectChanges(t *testing.T) {
{
name: "With different endpoint ready condition",
oldObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "1",
},
ResourceVersion: "1",
Endpoints: []discoveryv1.Endpoint{{
Addresses: []string{"10.10.10.10"},
Conditions: discoveryv1.EndpointConditions{Ready: new(true)},
}},
},
newObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "2",
},
ResourceVersion: "2",
Endpoints: []discoveryv1.Endpoint{{
Addresses: []string{"10.10.10.10"},
Conditions: discoveryv1.EndpointConditions{Ready: new(false)},
@@ -204,18 +163,14 @@ func Test_detectChanges(t *testing.T) {
{
name: "With different endpoint serving condition",
oldObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "1",
},
ResourceVersion: "1",
Endpoints: []discoveryv1.Endpoint{{
Addresses: []string{"10.10.10.10"},
Conditions: discoveryv1.EndpointConditions{Serving: new(true)},
}},
},
newObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "2",
},
ResourceVersion: "2",
Endpoints: []discoveryv1.Endpoint{{
Addresses: []string{"10.10.10.10"},
Conditions: discoveryv1.EndpointConditions{Serving: new(false)},
@@ -226,18 +181,14 @@ func Test_detectChanges(t *testing.T) {
{
name: "With different endpoint terminating condition",
oldObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "1",
},
ResourceVersion: "1",
Endpoints: []discoveryv1.Endpoint{{
Addresses: []string{"10.10.10.10"},
Conditions: discoveryv1.EndpointConditions{Terminating: new(false)},
}},
},
newObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "2",
},
ResourceVersion: "2",
Endpoints: []discoveryv1.Endpoint{{
Addresses: []string{"10.10.10.10"},
Conditions: discoveryv1.EndpointConditions{Terminating: new(true)},
@@ -248,17 +199,13 @@ func Test_detectChanges(t *testing.T) {
{
name: "With endpoint serving condition nil then explicitly true",
oldObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "1",
},
ResourceVersion: "1",
Endpoints: []discoveryv1.Endpoint{{
Addresses: []string{"10.10.10.10"},
}},
},
newObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "2",
},
ResourceVersion: "2",
Endpoints: []discoveryv1.Endpoint{{
Addresses: []string{"10.10.10.10"},
Conditions: discoveryv1.EndpointConditions{Serving: new(true)},
@@ -268,17 +215,13 @@ func Test_detectChanges(t *testing.T) {
{
name: "With endpoint serving condition nil then explicitly false",
oldObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "1",
},
ResourceVersion: "1",
Endpoints: []discoveryv1.Endpoint{{
Addresses: []string{"10.10.10.10"},
}},
},
newObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "2",
},
ResourceVersion: "2",
Endpoints: []discoveryv1.Endpoint{{
Addresses: []string{"10.10.10.10"},
Conditions: discoveryv1.EndpointConditions{Serving: new(false)},
@@ -289,17 +232,13 @@ func Test_detectChanges(t *testing.T) {
{
name: "With endpoint terminating condition nil then explicitly false",
oldObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "1",
},
ResourceVersion: "1",
Endpoints: []discoveryv1.Endpoint{{
Addresses: []string{"10.10.10.10"},
}},
},
newObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "2",
},
ResourceVersion: "2",
Endpoints: []discoveryv1.Endpoint{{
Addresses: []string{"10.10.10.10"},
Conditions: discoveryv1.EndpointConditions{Terminating: new(false)},
@@ -309,17 +248,13 @@ func Test_detectChanges(t *testing.T) {
{
name: "With endpoint terminating condition nil then explicitly true",
oldObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "1",
},
ResourceVersion: "1",
Endpoints: []discoveryv1.Endpoint{{
Addresses: []string{"10.10.10.10"},
}},
},
newObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "2",
},
ResourceVersion: "2",
Endpoints: []discoveryv1.Endpoint{{
Addresses: []string{"10.10.10.10"},
Conditions: discoveryv1.EndpointConditions{Terminating: new(true)},
@@ -330,33 +265,25 @@ func Test_detectChanges(t *testing.T) {
{
name: "With different len of ports",
oldObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "1",
},
Ports: []discoveryv1.EndpointPort{},
ResourceVersion: "1",
Ports: []discoveryv1.EndpointPort{},
},
newObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "2",
},
Ports: []discoveryv1.EndpointPort{{}},
ResourceVersion: "2",
Ports: []discoveryv1.EndpointPort{{}},
},
want: true,
},
{
name: "With different ports",
oldObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "1",
},
ResourceVersion: "1",
Ports: []discoveryv1.EndpointPort{{
Port: new(int32(80)),
}},
},
newObj: &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: "2",
},
ResourceVersion: "2",
Ports: []discoveryv1.EndpointPort{{
Port: new(int32(8080)),
}},
@@ -12,15 +12,13 @@ import (
func TestStripManagedFields(t *testing.T) {
service := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "whoami",
ManagedFields: []metav1.ManagedFieldsEntry{
{
Manager: "kubectl",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "v1",
Time: &metav1.Time{Time: time.Now()},
},
Name: "whoami",
ManagedFields: []metav1.ManagedFieldsEntry{
{
Manager: "kubectl",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "v1",
Time: &metav1.Time{Time: time.Now()},
},
},
}
+44
View File
@@ -0,0 +1,44 @@
package k8s
import (
"strconv"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
)
// SecretURNPrefix is the prefix of the URN referencing the value of a key of a Secret.
const SecretURNPrefix = "urn:k8s:secret:"
// SecretURN returns the URN referencing the value of the given key of the given Secret.
func SecretURN(secretName, key string) string {
return SecretURNPrefix + secretName + ":" + key
}
// URNRefs collects the values read from Secrets for a middleware, keyed by the JSON Pointer of the field referencing them.
type URNRefs map[string]string
// Add records the value under the JSON Pointer, and returns the URN of the Secret key to set in the field.
func (r URNRefs) Add(pointer, secretName, key, value string) string {
r[pointer] = value
return SecretURN(secretName, key)
}
// AddUsers records each user under the JSON Pointer of its entry in the list at pointer, and returns one URN per user.
func (r URNRefs) AddUsers(pointer, secretName, key string, users []string) dynamic.Users {
urns := make(dynamic.Users, len(users))
for i, user := range users {
urns[i] = r.Add(pointer+"/"+strconv.Itoa(i), secretName, key, user)
}
return urns
}
// OrNil returns nil when no value was recorded, so middlewares without Secret keep the configuration they had.
func (r URNRefs) OrNil() map[string]string {
if len(r) == 0 {
return nil
}
return r
}
+4 -8
View File
@@ -864,20 +864,16 @@ func Test_buildConfiguration(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent("foobar"),
KeyFile: types.FileOrContent("foobar"),
},
CertFile: types.FileOrContent("foobar"),
KeyFile: types.FileOrContent("foobar"),
Stores: []string{
"foobar",
"foobar",
},
},
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent("foobar"),
KeyFile: types.FileOrContent("foobar"),
},
CertFile: types.FileOrContent("foobar"),
KeyFile: types.FileOrContent("foobar"),
Stores: []string{
"foobar",
"foobar",
+33 -39
View File
@@ -81,20 +81,16 @@ func TestMergeCollectionSet_BasicMapMerge(t *testing.T) {
func TestMergeCollectionSet_EmbeddedStruct(t *testing.T) {
dst := &testCollectionSetWithEmbedded{
TestEmbedded: TestEmbedded{
EmbeddedItems: map[string]*testResource{
"embedded1": {Value: "dst-embedded"},
},
EmbeddedItems: map[string]*testResource{
"embedded1": {Value: "dst-embedded"},
},
Items: map[string]*testResource{
"item1": {Value: "dst-item"},
},
}
src := &testCollectionSetWithEmbedded{
TestEmbedded: TestEmbedded{
EmbeddedItems: map[string]*testResource{
"embedded2": {Value: "src-embedded"},
},
EmbeddedItems: map[string]*testResource{
"embedded2": {Value: "src-embedded"},
},
Items: map[string]*testResource{
"item2": {Value: "src-item"},
@@ -106,11 +102,9 @@ func TestMergeCollectionSet_EmbeddedStruct(t *testing.T) {
assert.Empty(t, tracker.toDelete)
assert.Equal(t, &testCollectionSetWithEmbedded{
TestEmbedded: TestEmbedded{
EmbeddedItems: map[string]*testResource{
"embedded1": {Value: "dst-embedded"},
"embedded2": {Value: "src-embedded"},
},
EmbeddedItems: map[string]*testResource{
"embedded1": {Value: "dst-embedded"},
"embedded2": {Value: "src-embedded"},
},
Items: map[string]*testResource{
"item1": {Value: "dst-item"},
@@ -348,8 +342,8 @@ func TestMerge(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{CertFile: "cert1.pem", KeyFile: "key1.pem"},
Stores: []string{"store1"},
CertFile: "cert1.pem", KeyFile: "key1.pem",
Stores: []string{"store1"},
},
},
},
@@ -358,8 +352,8 @@ func TestMerge(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{CertFile: "cert2.pem", KeyFile: "key2.pem"},
Stores: []string{"store2"},
CertFile: "cert2.pem", KeyFile: "key2.pem",
Stores: []string{"store2"},
},
},
},
@@ -369,12 +363,12 @@ func TestMerge(t *testing.T) {
expected: buildExpectedConfiguration(func(c *dynamic.Configuration) {
c.TLS.Certificates = []*tls.CertAndStores{
{
Certificate: tls.Certificate{CertFile: "cert1.pem", KeyFile: "key1.pem"},
Stores: []string{"store1"},
CertFile: "cert1.pem", KeyFile: "key1.pem",
Stores: []string{"store1"},
},
{
Certificate: tls.Certificate{CertFile: "cert2.pem", KeyFile: "key2.pem"},
Stores: []string{"store2"},
CertFile: "cert2.pem", KeyFile: "key2.pem",
Stores: []string{"store2"},
},
}
}),
@@ -386,8 +380,8 @@ func TestMerge(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"},
Stores: []string{"store1"},
CertFile: "cert.pem", KeyFile: "key.pem",
Stores: []string{"store1"},
},
},
},
@@ -396,8 +390,8 @@ func TestMerge(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"},
Stores: []string{"store2"},
CertFile: "cert.pem", KeyFile: "key.pem",
Stores: []string{"store2"},
},
},
},
@@ -407,8 +401,8 @@ func TestMerge(t *testing.T) {
expected: buildExpectedConfiguration(func(c *dynamic.Configuration) {
c.TLS.Certificates = []*tls.CertAndStores{
{
Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"},
Stores: []string{"store1", "store2"},
CertFile: "cert.pem", KeyFile: "key.pem",
Stores: []string{"store1", "store2"},
},
}
}),
@@ -420,8 +414,8 @@ func TestMerge(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"},
Stores: []string{"store1", "store2"},
CertFile: "cert.pem", KeyFile: "key.pem",
Stores: []string{"store1", "store2"},
},
},
},
@@ -430,8 +424,8 @@ func TestMerge(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"},
Stores: []string{"store2", "store3"},
CertFile: "cert.pem", KeyFile: "key.pem",
Stores: []string{"store2", "store3"},
},
},
},
@@ -441,8 +435,8 @@ func TestMerge(t *testing.T) {
expected: buildExpectedConfiguration(func(c *dynamic.Configuration) {
c.TLS.Certificates = []*tls.CertAndStores{
{
Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"},
Stores: []string{"store1", "store2", "store3"},
CertFile: "cert.pem", KeyFile: "key.pem",
Stores: []string{"store1", "store2", "store3"},
},
}
}),
@@ -454,8 +448,8 @@ func TestMerge(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"},
Stores: []string{"store1"},
CertFile: "cert.pem", KeyFile: "key.pem",
Stores: []string{"store1"},
},
},
},
@@ -464,8 +458,8 @@ func TestMerge(t *testing.T) {
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"},
Stores: []string{"store2"},
CertFile: "cert.pem", KeyFile: "key.pem",
Stores: []string{"store2"},
},
},
},
@@ -475,8 +469,8 @@ func TestMerge(t *testing.T) {
expected: buildExpectedConfiguration(func(c *dynamic.Configuration) {
c.TLS.Certificates = []*tls.CertAndStores{
{
Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"},
Stores: []string{"store1"},
CertFile: "cert.pem", KeyFile: "key.pem",
Stores: []string{"store1"},
},
}
}),
+2 -4
View File
@@ -80,10 +80,8 @@ func Test_globalConfig(t *testing.T) {
for _, test := range cases {
t.Run(test.Name, func(t *testing.T) {
p := Provider{
Configuration: Configuration{
ExposedByDefault: test.ExposedByDefault,
Prefix: test.Prefix,
},
ExposedByDefault: test.ExposedByDefault,
Prefix: test.Prefix,
}
result := p.getExtraConf(test.Tags)
require.Equal(t, test.exp, result)
+4 -4
View File
@@ -175,12 +175,12 @@ func TestProvider_sendDynamicConfig(t *testing.T) {
},
want: []*traefiktls.CertAndStores{
{
Certificate: traefiktls.Certificate{CertFile: "bar.crt", KeyFile: "bar.key"},
Stores: []string{traefiktls.DefaultTLSStoreName},
CertFile: "bar.crt", KeyFile: "bar.key",
Stores: []string{traefiktls.DefaultTLSStoreName},
},
{
Certificate: traefiktls.Certificate{CertFile: "foo.crt", KeyFile: "foo.key"},
Stores: []string{traefiktls.DefaultTLSStoreName},
CertFile: "foo.crt", KeyFile: "foo.key",
Stores: []string{traefiktls.DefaultTLSStoreName},
},
},
},
+11 -6
View File
@@ -4,8 +4,8 @@ import (
"bufio"
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httptrace"
@@ -70,16 +70,21 @@ func (w *writeDetector) Write(p []byte) (int, error) {
}
type writeFlusher struct {
io.Writer
rw http.ResponseWriter
}
func (w *writeFlusher) Write(b []byte) (int, error) {
n, err := w.Writer.Write(b)
if f, ok := w.Writer.(http.Flusher); ok {
f.Flush()
n, err := w.rw.Write(b)
if err != nil {
return n, err
}
return n, err
// ErrNotSupported is ignored to keep flushing a no-op on writers that cannot flush.
if err := http.NewResponseController(w.rw).Flush(); err != nil && !errors.Is(err, http.ErrNotSupported) {
return n, err
}
return n, nil
}
type timeoutError struct {
+6 -7
View File
@@ -2,6 +2,7 @@ package fast
import (
"context"
"errors"
"fmt"
"io"
"net"
@@ -41,12 +42,6 @@ func upgradeResponseHandler(ctx context.Context, reqUpType string) upgradeHandle
return
}
hj, ok := rw.(http.Hijacker)
if !ok {
httputil.ErrorHandlerWithContext(ctx, rw, fmt.Errorf("can't switch protocols using non-Hijacker ResponseWriter type %T", rw))
backConn.Close()
return
}
backConnCloseCh := make(chan bool)
go func() {
// Ensure that the cancellation of a request closes the backend.
@@ -59,7 +54,11 @@ func upgradeResponseHandler(ctx context.Context, reqUpType string) upgradeHandle
}()
defer close(backConnCloseCh)
conn, brw, err := hj.Hijack()
conn, brw, err := http.NewResponseController(rw).Hijack()
if errors.Is(err, http.ErrNotSupported) {
httputil.ErrorHandlerWithContext(ctx, rw, fmt.Errorf("can't switch protocols using non-Hijacker ResponseWriter type %T: %w", rw, err))
return
}
if err != nil {
httputil.ErrorHandlerWithContext(ctx, rw, fmt.Errorf("hijack failed on protocol switch: %w", err))
return
+69 -90
View File
@@ -24,7 +24,6 @@ import (
"github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd"
"github.com/traefik/traefik/v3/pkg/provider/kubernetes/gateway"
"github.com/traefik/traefik/v3/pkg/provider/kubernetes/ingress"
"github.com/traefik/traefik/v3/pkg/provider/kv"
"github.com/traefik/traefik/v3/pkg/provider/kv/consul"
"github.com/traefik/traefik/v3/pkg/provider/kv/etcd"
"github.com/traefik/traefik/v3/pkg/provider/kv/redis"
@@ -458,11 +457,9 @@ func init() {
},
Certificates: []*traefiktls.CertAndStores{
{
Certificate: traefiktls.Certificate{
CertFile: "cert.pem",
KeyFile: "key.pem",
},
Stores: []string{"foo"},
CertFile: "cert.pem",
KeyFile: "key.pem",
Stores: []string{"foo"},
},
},
Stores: map[string]traefiktls.Store{
@@ -606,46 +603,38 @@ func TestDo_staticConfiguration(t *testing.T) {
}
config.Providers.Docker = &docker.Provider{
Shared: docker.Shared{
ExposedByDefault: true,
Constraints: `Label("foo", "bar")`,
AllowEmptyServices: true,
Network: "MyNetwork",
UseBindPortIP: true,
Watch: true,
DefaultRule: "PathPrefix(`/`)",
},
ClientConfig: docker.ClientConfig{
Endpoint: "MyEndPoint", TLS: &types.ClientTLS{
CA: "myCa",
Cert: "mycert.pem",
Key: "mycert.key",
InsecureSkipVerify: true,
},
HTTPClientTimeout: 42,
ExposedByDefault: true,
Constraints: `Label("foo", "bar")`,
AllowEmptyServices: true,
Network: "MyNetwork",
UseBindPortIP: true,
Watch: true,
DefaultRule: "PathPrefix(`/`)",
Endpoint: "MyEndPoint", TLS: &types.ClientTLS{
CA: "myCa",
Cert: "mycert.pem",
Key: "mycert.key",
InsecureSkipVerify: true,
},
HTTPClientTimeout: 42,
}
config.Providers.Swarm = &docker.SwarmProvider{
Shared: docker.Shared{
ExposedByDefault: true,
Constraints: `Label("foo", "bar")`,
AllowEmptyServices: true,
Network: "MyNetwork",
UseBindPortIP: true,
Watch: true,
DefaultRule: "PathPrefix(`/`)",
ExposedByDefault: true,
Constraints: `Label("foo", "bar")`,
AllowEmptyServices: true,
Network: "MyNetwork",
UseBindPortIP: true,
Watch: true,
DefaultRule: "PathPrefix(`/`)",
Endpoint: "MyEndPoint", TLS: &types.ClientTLS{
CA: "myCa",
Cert: "mycert.pem",
Key: "mycert.key",
InsecureSkipVerify: true,
},
ClientConfig: docker.ClientConfig{
Endpoint: "MyEndPoint", TLS: &types.ClientTLS{
CA: "myCa",
Cert: "mycert.pem",
Key: "mycert.key",
InsecureSkipVerify: true,
},
HTTPClientTimeout: 42,
},
RefreshSeconds: 42,
HTTPClientTimeout: 42,
RefreshSeconds: 42,
}
config.Providers.KubernetesIngress = &ingress.Provider{
@@ -687,34 +676,32 @@ func TestDo_staticConfiguration(t *testing.T) {
}
config.Providers.ConsulCatalog = &consulcatalog.ProviderBuilder{
Configuration: consulcatalog.Configuration{
Constraints: `Label("foo", "bar")`,
Endpoint: &consulcatalog.EndpointConfig{
Address: "MyAddress",
Scheme: "MyScheme",
DataCenter: "MyDatacenter",
Token: "MyToken",
TLS: &types.ClientTLS{
CA: "myCa",
Cert: "mycert.pem",
Key: "mycert.key",
InsecureSkipVerify: true,
},
HTTPAuth: &consulcatalog.EndpointHTTPAuthConfig{
Username: "MyUsername",
Password: "MyPassword",
},
EndpointWaitTime: 42,
Constraints: `Label("foo", "bar")`,
Endpoint: &consulcatalog.EndpointConfig{
Address: "MyAddress",
Scheme: "MyScheme",
DataCenter: "MyDatacenter",
Token: "MyToken",
TLS: &types.ClientTLS{
CA: "myCa",
Cert: "mycert.pem",
Key: "mycert.key",
InsecureSkipVerify: true,
},
Prefix: "MyPrefix",
RefreshInterval: 42,
RequireConsistent: true,
Stale: true,
Cache: true,
ExposedByDefault: true,
DefaultRule: "PathPrefix(`/`)",
HTTPAuth: &consulcatalog.EndpointHTTPAuthConfig{
Username: "MyUsername",
Password: "MyPassword",
},
EndpointWaitTime: 42,
},
Namespaces: []string{"ns1", "ns2"},
Prefix: "MyPrefix",
RefreshInterval: 42,
RequireConsistent: true,
Stale: true,
Cache: true,
ExposedByDefault: true,
DefaultRule: "PathPrefix(`/`)",
Namespaces: []string{"ns1", "ns2"},
}
config.Providers.Ecs = &ecs.Provider{
@@ -731,11 +718,9 @@ func TestDo_staticConfiguration(t *testing.T) {
}
config.Providers.Consul = &consul.ProviderBuilder{
Provider: kv.Provider{
RootKey: "RootKey",
Endpoints: nil,
},
Token: "secret",
RootKey: "RootKey",
Endpoints: nil,
Token: "secret",
TLS: &types.ClientTLS{
CA: "myCa",
Cert: "mycert.pem",
@@ -746,12 +731,10 @@ func TestDo_staticConfiguration(t *testing.T) {
}
config.Providers.Etcd = &etcd.Provider{
Provider: kv.Provider{
RootKey: "RootKey",
Endpoints: nil,
},
Username: "username",
Password: "password",
RootKey: "RootKey",
Endpoints: nil,
Username: "username",
Password: "password",
TLS: &types.ClientTLS{
CA: "myCa",
Cert: "mycert.pem",
@@ -761,21 +744,17 @@ func TestDo_staticConfiguration(t *testing.T) {
}
config.Providers.ZooKeeper = &zk.Provider{
Provider: kv.Provider{
RootKey: "RootKey",
Endpoints: nil,
},
Username: "username",
Password: "password",
RootKey: "RootKey",
Endpoints: nil,
Username: "username",
Password: "password",
}
config.Providers.Redis = &redis.Provider{
Provider: kv.Provider{
RootKey: "RootKey",
Endpoints: nil,
},
Username: "username",
Password: "password",
RootKey: "RootKey",
Endpoints: nil,
Username: "username",
Password: "password",
TLS: &types.ClientTLS{
CA: "myCa",
Cert: "mycert.pem",
+6 -4
View File
@@ -150,15 +150,17 @@ func Test_mergeConfiguration_tlsCertificates(t *testing.T) {
"tlsalpn.acme": &dynamic.Configuration{
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{{
Certificate: tls.Certificate{CertFile: "foo", KeyFile: "bar"},
Stores: []string{tlsalpn01.ACMETLS1Protocol},
CertFile: "foo",
KeyFile: "bar",
Stores: []string{tlsalpn01.ACMETLS1Protocol},
}},
},
},
},
expected: []*tls.CertAndStores{{
Certificate: tls.Certificate{CertFile: "foo", KeyFile: "bar"},
Stores: []string{tlsalpn01.ACMETLS1Protocol},
CertFile: "foo",
KeyFile: "bar",
Stores: []string{tlsalpn01.ACMETLS1Protocol},
}},
},
}
+15 -4
View File
@@ -100,11 +100,16 @@ func (b *Builder) BuildMiddlewareChain(ctx context.Context, middlewares []string
// it is the responsibility of the caller to make sure that b.configs[middlewareName].Middleware exists.
func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (alice.Constructor, error) {
config := b.configs[middlewareName]
if config == nil || config.Middleware == nil {
info := b.configs[middlewareName]
if info == nil || info.Middleware == nil {
return nil, fmt.Errorf("invalid middleware %q configuration", middlewareName)
}
config, err := info.Middleware.ResolveURNRefs()
if err != nil {
return nil, fmt.Errorf("resolving middleware %q configuration: %w", middlewareName, err)
}
var middleware alice.Constructor
badConf := errors.New("cannot create middleware: multi-types middleware not supported, consider declaring two different pieces of middleware instead")
@@ -426,13 +431,19 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (
return nil, fmt.Errorf("plugin: %w", err)
}
// Unlike the other middlewares, the decoding of a plugin configuration puts the invalid values in its errors.
plug, err := b.pluginBuilder.Build(pluginType, rawPluginConfig, middlewareName)
if err != nil {
return nil, fmt.Errorf("plugin: %w", err)
return nil, info.RedactURNRefs(fmt.Errorf("plugin: %w", err))
}
middleware = func(next http.Handler) (http.Handler, error) {
return newTraceablePlugin(ctx, middlewareName, plug, next)
handler, err := newTraceablePlugin(ctx, middlewareName, plug, next)
if err != nil {
return nil, info.RedactURNRefs(err)
}
return handler, nil
}
}
+140
View File
@@ -1,6 +1,7 @@
package middleware
import (
"context"
"errors"
"net/http"
"net/http/httptest"
@@ -10,6 +11,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/config/runtime"
"github.com/traefik/traefik/v3/pkg/plugins"
"github.com/traefik/traefik/v3/pkg/server/provider"
)
@@ -380,3 +382,141 @@ func TestBuilder_buildConstructor(t *testing.T) {
})
}
}
type fakePluginsBuilder struct {
config map[string]any
buildErr error
constructorErr error
}
func (b *fakePluginsBuilder) Build(_ string, config map[string]any, _ string) (plugins.Constructor, error) {
b.config = config
if b.buildErr != nil {
return nil, b.buildErr
}
return func(_ context.Context, next http.Handler) (http.Handler, error) {
if b.constructorErr != nil {
return nil, b.constructorErr
}
return next, nil
}, nil
}
func TestBuilder_BuildMiddlewareChain_pluginURNRefs(t *testing.T) {
rtConf := runtime.NewConfig(dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{
"plugin@file": {
Plugin: map[string]dynamic.PluginConf{"my-plugin": {"token": "urn:k8s:secret:creds:token"}},
URNRefs: map[string]string{"/plugin/my-plugin/token": "secret"},
},
},
},
})
pluginsBuilder := &fakePluginsBuilder{}
builder := NewBuilder(rtConf.Middlewares, nil, pluginsBuilder)
_, err := builder.BuildMiddlewareChain(t.Context(), []string{"plugin@file"}).Then(http.NotFoundHandler())
require.NoError(t, err)
assert.Equal(t, map[string]any{"token": "secret"}, pluginsBuilder.config)
assert.Equal(t, dynamic.PluginConf{"token": "urn:k8s:secret:creds:token"}, rtConf.Middlewares["plugin@file"].Plugin["my-plugin"])
}
func TestBuilder_BuildMiddlewareChain_pluginErrorURNRefs(t *testing.T) {
t.Parallel()
testCases := []struct {
desc string
pluginsBuilder *fakePluginsBuilder
expectedErr string
}{
{
desc: "build error",
pluginsBuilder: &fakePluginsBuilder{buildErr: errors.New(`strconv.ParseInt: parsing "s3cr3t": invalid syntax`)},
expectedErr: `plugin: strconv.ParseInt: parsing "urn:k8s:secret:creds:token": invalid syntax`,
},
{
desc: "constructor error",
pluginsBuilder: &fakePluginsBuilder{constructorErr: errors.New(`strconv.ParseInt: parsing "s3cr3t": invalid syntax`)},
expectedErr: `strconv.ParseInt: parsing "urn:k8s:secret:creds:token": invalid syntax`,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
rtConf := runtime.NewConfig(dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{
"plugin@file": {
Plugin: map[string]dynamic.PluginConf{"my-plugin": {"token": "urn:k8s:secret:creds:token"}},
URNRefs: map[string]string{"/plugin/my-plugin/token": "s3cr3t"},
},
},
},
})
builder := NewBuilder(rtConf.Middlewares, nil, test.pluginsBuilder)
_, err := builder.BuildMiddlewareChain(t.Context(), []string{"plugin@file"}).Then(http.NotFoundHandler())
require.EqualError(t, err, test.expectedErr)
assert.Equal(t, []string{test.expectedErr}, rtConf.Middlewares["plugin@file"].Err)
})
}
}
func TestBuilder_BuildMiddlewareChain_basicAuthURNRefs(t *testing.T) {
rtConf := runtime.NewConfig(dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{
"auth@file": {
BasicAuth: &dynamic.BasicAuth{Users: dynamic.Users{"urn:k8s:secret:auth:users"}},
URNRefs: map[string]string{"/basicAuth/users/0": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/"},
},
},
},
})
builder := NewBuilder(rtConf.Middlewares, nil, nil)
next := http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
rw.WriteHeader(http.StatusOK)
})
handler, err := builder.BuildMiddlewareChain(t.Context(), []string{"auth@file"}).Then(next)
require.NoError(t, err)
req := httptest.NewRequest(http.MethodGet, "http://localhost", nil)
req.SetBasicAuth("test", "test")
rw := httptest.NewRecorder()
handler.ServeHTTP(rw, req)
assert.Equal(t, http.StatusOK, rw.Code)
assert.Equal(t, dynamic.Users{"urn:k8s:secret:auth:users"}, rtConf.Middlewares["auth@file"].BasicAuth.Users)
}
func TestBuilder_BuildMiddlewareChain_invalidURNRefs(t *testing.T) {
rtConf := runtime.NewConfig(dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{
"auth@file": {
BasicAuth: &dynamic.BasicAuth{Users: dynamic.Users{"urn:k8s:secret:auth:users"}},
URNRefs: map[string]string{"/basicAuth/users/1": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/"},
},
},
},
})
builder := NewBuilder(rtConf.Middlewares, nil, nil)
_, err := builder.BuildMiddlewareChain(t.Context(), []string{"auth@file"}).Then(http.NotFoundHandler())
require.Error(t, err)
assert.Equal(t, runtime.StatusDisabled, rtConf.Middlewares["auth@file"].Status)
}
+2 -2
View File
@@ -152,8 +152,8 @@ func Test_Routing(t *testing.T) {
},
},
[]*traefiktls.CertAndStores{{
Certificate: traefiktls.Certificate{CertFile: types.FileOrContent(certPEM), KeyFile: types.FileOrContent(keyPEM)},
Stores: []string{tlsalpn01.ACMETLS1Protocol},
CertFile: types.FileOrContent(certPEM), KeyFile: types.FileOrContent(keyPEM),
Stores: []string{tlsalpn01.ACMETLS1Protocol},
}})
middlewaresBuilder := tcpmiddleware.NewBuilder(conf.TCPMiddlewares)
+8 -7
View File
@@ -774,13 +774,14 @@ func newHTTPServer(ctx context.Context, ln net.Listener, configuration *static.E
}
serverHTTP := &http.Server{
Protocols: &protocols,
Handler: handler,
ErrorLog: stdlog.New(logs.NoLevel(log.Logger, zerolog.DebugLevel), "", 0),
ReadTimeout: time.Duration(configuration.Transport.RespondingTimeouts.ReadTimeout),
WriteTimeout: time.Duration(configuration.Transport.RespondingTimeouts.WriteTimeout),
IdleTimeout: time.Duration(configuration.Transport.RespondingTimeouts.IdleTimeout),
MaxHeaderBytes: configuration.HTTP.MaxHeaderBytes,
Protocols: &protocols,
Handler: handler,
ErrorLog: stdlog.New(logs.NoLevel(log.Logger, zerolog.DebugLevel), "", 0),
ReadTimeout: time.Duration(configuration.Transport.RespondingTimeouts.ReadTimeout),
WriteTimeout: time.Duration(configuration.Transport.RespondingTimeouts.WriteTimeout),
IdleTimeout: time.Duration(configuration.Transport.RespondingTimeouts.IdleTimeout),
MaxHeaderBytes: configuration.HTTP.MaxHeaderBytes,
MaxHeaderValueCount: configuration.HTTP.MaxHeaderValueCount,
HTTP2: &http.HTTP2Config{
MaxConcurrentStreams: int(configuration.HTTP2.MaxConcurrentStreams),
MaxDecoderHeaderTableSize: int(configuration.HTTP2.MaxDecoderHeaderTableSize),
@@ -177,6 +177,7 @@ func TestFailoverStatusCode(t *testing.T) {
desc string
statusCode []string
handlerStatusCode int
flush bool
expectedHandler string
expectedStatusCode int
expectedResponseBody string
@@ -189,6 +190,15 @@ func TestFailoverStatusCode(t *testing.T) {
expectedStatusCode: 200,
expectedResponseBody: "fallback response",
},
{
desc: "main handler returns 503 and flushes, failover triggered",
statusCode: []string{"503"},
handlerStatusCode: 503,
flush: true,
expectedHandler: "fallback",
expectedStatusCode: 200,
expectedResponseBody: "fallback response",
},
{
desc: "main handler returns 200, failover not triggered",
statusCode: []string{"503"},
@@ -261,6 +271,10 @@ func TestFailoverStatusCode(t *testing.T) {
rw.WriteHeader(test.handlerStatusCode)
_, err := rw.Write([]byte("main response"))
require.NoError(t, err)
if test.flush {
require.NoError(t, http.NewResponseController(rw).Flush())
}
}))
failover.SetFallbackHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
@@ -273,7 +287,7 @@ func TestFailoverStatusCode(t *testing.T) {
recorder := httptest.NewRecorder()
failover.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/", nil))
assert.Equal(t, test.expectedHandler, recorder.Header().Get("server"))
assert.Equal(t, test.expectedHandler, recorder.Result().Header.Get("server"))
assert.Equal(t, test.expectedStatusCode, recorder.Code)
assert.Equal(t, test.expectedResponseBody, recorder.Body.String())
})
@@ -1,9 +1,6 @@
package failover
import (
"bufio"
"fmt"
"net"
"net/http"
"github.com/traefik/traefik/v3/pkg/types"
@@ -62,16 +59,22 @@ func (r *responseWriter) WriteHeader(statusCode int) {
}
}
func (r *responseWriter) Flush() {
if flusher, ok := r.ResponseWriter.(http.Flusher); ok {
flusher.Flush()
}
}
func (r *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if h, ok := r.ResponseWriter.(http.Hijacker); ok {
return h.Hijack()
// FlushError sends any buffered data to the client.
func (r *responseWriter) FlushError() error {
if !r.written {
r.WriteHeader(http.StatusOK)
}
return nil, nil, fmt.Errorf("not a hijacker: %T", r.ResponseWriter)
// The response is discarded for the fallback,
// and flushing it would commit an implicit 200 to the client.
if r.needFallback {
return nil
}
return http.NewResponseController(r.ResponseWriter).Flush()
}
// Unwrap exposes the underlying ResponseWriter for http.ResponseController.
func (r *responseWriter) Unwrap() http.ResponseWriter {
return r.ResponseWriter
}
+8 -16
View File
@@ -78,10 +78,8 @@ ZboOWVe3icTy64BT3OQhmg==
func TestTLSInStore(t *testing.T) {
dynamicConfigs := []*CertAndStores{{
Certificate: Certificate{
CertFile: localhostCert,
KeyFile: localhostKey,
},
CertFile: localhostCert,
KeyFile: localhostKey,
}}
tlsManager := NewManager(nil)
@@ -95,10 +93,8 @@ func TestTLSInStore(t *testing.T) {
func TestTLSInvalidStore(t *testing.T) {
dynamicConfigs := []*CertAndStores{{
Certificate: Certificate{
CertFile: localhostCert,
KeyFile: localhostKey,
},
CertFile: localhostCert,
KeyFile: localhostKey,
}}
tlsManager := NewManager(nil)
@@ -120,10 +116,8 @@ func TestTLSInvalidStore(t *testing.T) {
func TestManager_Get(t *testing.T) {
dynamicConfigs := []*CertAndStores{{
Certificate: Certificate{
CertFile: localhostCert,
KeyFile: localhostKey,
},
CertFile: localhostCert,
KeyFile: localhostKey,
}}
tlsConfigs := map[string]Options{
@@ -402,10 +396,8 @@ func TestManager_UpdateConfigs_OCSPConfig(t *testing.T) {
tlsManager.UpdateConfigs(testContext, nil, nil, []*CertAndStores{
{
Certificate: Certificate{
CertFile: certWithOCSPServer,
KeyFile: certKey,
},
CertFile: certWithOCSPServer,
KeyFile: certKey,
},
})