mirror of
https://github.com/traefik/traefik.git
synced 2026-10-28 08:36:10 +08:00
Merge branch v3.7 into master
This commit is contained in:
90 files changed
+2438
-1334
No files matched your search
@@ -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
@@ -1 +1 @@
|
||||
1.26
|
||||
1.27
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}"
|
||||
@@ -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"),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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/"},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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:]
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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()},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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"},
|
||||
},
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
Reference in new issue
Block a user