Compare commits

...

22 commits

Author SHA1 Message Date
5fe4691625
Merge branch 'master' of github.com:traefik/traefik 2024-08-14 21:04:12 +05:30
Kevin Pollet
12a37346a4
Support ResponseHeaderModifier filter 2024-08-12 11:34:04 +02:00
Luke Rindels
78079377e8
Add 30 day certificatesDuration step 2024-08-08 10:22:05 +02:00
Wolfgang Ellsässer
75881359ab
Add encodings option to the compression middleware 2024-08-07 16:20:04 +02:00
kevinpollet
b611f967b7
Merge branch v3.1 into master 2024-08-06 16:38:39 +02:00
Kevin Pollet
4c4780f886
Prepare release v3.1.2 2024-08-06 15:34:03 +02:00
romain
926a8e88e9 Merge current v2.11 into v3.1 2024-08-06 14:54:50 +02:00
Romain
6b1adabeb5
Prepare release v2.11.8 2024-08-06 14:50:04 +02:00
Michel Loiseleur
4eedcabbb3
Use Standard channel by default with Gateway API 2024-08-06 11:36:04 +02:00
Romain
5bf4b536e2
Change logs output from stderr to stdout 2024-08-05 16:56:34 +02:00
Kevin Pollet
5380e48747
Include status addresses when comparing Gateway statuses 2024-08-05 12:22:04 +02:00
Daniel Jolly
ccc11a69f1
Fix yaml config example for HTTP provider headers 2024-08-05 11:26:04 +02:00
Daniel Jolly
0f57f108ae
Fix missing codeblock ending in HTTP discover documentation 2024-08-05 11:14:03 +02:00
Matthias Wirtz
c0b704e1b0
Fix grafana dashboard to work with scrape interval greater than 15s 2024-08-02 10:18:04 +02:00
Romain
a50345bf8d
Allow to disable Kubernetes cluster scope resources discovery
Co-authored-by: Kevin Pollet <pollet.kevin@gmail.com>
2024-08-01 15:50:04 +02:00
July
bd93e224de
Support HTTP BasicAuth for docker and swarm endpoint 2024-08-01 14:26:04 +02:00
Michael
ea019be133
Upgrade webui dependencies 2024-08-01 11:00:06 +02:00
Michael
02de683b94
Fix embedded youtube video 2024-08-01 09:30:04 +02:00
mmatur
930f84850b
Merge current v2.11 intov3.1 2024-07-31 17:14:45 +02:00
Romain
8970ae9199
Update to github.com/docker/docker v27.1.1 2024-07-31 16:20:04 +02:00
Landry Benguigui
de732ba53c
Add Access logs section to the migration guide
Co-authored-by: Simon Delicata <simon.delicata@free.fr>
2024-07-31 10:20:04 +02:00
Dylan Rodgers
0f7af2b4e7
Updated index.md to include video 2024-07-31 10:00:05 +02:00
70 changed files with 3962 additions and 2696 deletions

View file

@ -213,6 +213,7 @@ issues:
- 'Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked'
- "should have a package comment, unless it's in another file for this package"
- 'fmt.Sprintf can be replaced with string'
- 'SA1019: dockertypes.ContainerNode is deprecated'
exclude-rules:
- path: '(.+)_test.go'
linters:

View file

@ -1,3 +1,33 @@
## [v3.1.2](https://github.com/traefik/traefik/tree/v3.1.2) (2024-08-06)
[All Commits](https://github.com/traefik/traefik/compare/v3.1.1...v3.1.2)
**Bug fixes:**
- **[k8s,k8s/gatewayapi]** Include status addresses when comparing Gateway statuses ([#10972](https://github.com/traefik/traefik/pull/10972) by [kevinpollet](https://github.com/kevinpollet))
- **[k8s/ingress,k8s/crd,k8s]** Allow to disable Kubernetes cluster scope resources discovery ([#10946](https://github.com/traefik/traefik/pull/10946) by [rtribotte](https://github.com/rtribotte))
- **[logs]** Change logs output from stderr to stdout ([#10973](https://github.com/traefik/traefik/pull/10973) by [rtribotte](https://github.com/rtribotte))
- Fix grafana dashboard to work with scrape interval greater than 15s ([#10954](https://github.com/traefik/traefik/pull/10954) by [swiffer](https://github.com/swiffer))
**Documentation:**
- **[accesslogs]** Add Access logs section to the migration guide ([#10947](https://github.com/traefik/traefik/pull/10947) by [lbenguigui](https://github.com/lbenguigui))
- **[http]** Fix missing codeblock ending in HTTP discover documentation ([#10967](https://github.com/traefik/traefik/pull/10967) by [djcode](https://github.com/djcode))
- **[http]** Fix yaml config example for HTTP provider headers ([#10966](https://github.com/traefik/traefik/pull/10966) by [djcode](https://github.com/djcode))
- **[k8s,k8s/gatewayapi]** Use Standard channel by default with Gateway API ([#10974](https://github.com/traefik/traefik/pull/10974) by [mloiseleur](https://github.com/mloiseleur))
**Misc:**
- Merge branch v2.11 into v3.1 ([#10978](https://github.com/traefik/traefik/pull/10978) by [rtribotte](https://github.com/rtribotte))
- Merge v2.11 into v3.1 ([#10956](https://github.com/traefik/traefik/pull/10956) by [mmatur](https://github.com/mmatur))
## [v2.11.8](https://github.com/traefik/traefik/tree/v2.11.8) (2024-08-06)
[All Commits](https://github.com/traefik/traefik/compare/v2.11.7...v2.11.8)
**Bug fixes:**
- **[docker]** Update to github.com/docker/docker v27.1.1 ([#10955](https://github.com/traefik/traefik/pull/10955) by [rtribotte](https://github.com/rtribotte))
- **[webui]** Upgrade webui dependencies ([#10961](https://github.com/traefik/traefik/pull/10961) by [mmatur](https://github.com/mmatur))
**Documentation:**
- Fix embedded youtube video ([#10958](https://github.com/traefik/traefik/pull/10958) by [mmatur](https://github.com/mmatur))
- Updated index.md to include video ([#10944](https://github.com/traefik/traefik/pull/10944) by [tomatokoolaid](https://github.com/tomatokoolaid))
## [v3.1.1](https://github.com/traefik/traefik/tree/v3.1.1) (2024-07-30)
[All Commits](https://github.com/traefik/traefik/compare/v3.1.0...v3.1.1)

View file

@ -46,7 +46,7 @@ func setupLogger(staticConfiguration *static.Configuration) {
}
func getLogWriter(staticConfiguration *static.Configuration) io.Writer {
var w io.Writer = os.Stderr
var w io.Writer = os.Stdout
if staticConfiguration.Log != nil && len(staticConfiguration.Log.FilePath) > 0 {
_, _ = os.OpenFile(staticConfiguration.Log.FilePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o666)

View file

@ -1028,7 +1028,7 @@
"refId": "A"
}
],
"title": "5xx over [$interval]",
"title": "5xx over $interval",
"type": "timeseries"
},
{
@ -1128,7 +1128,7 @@
"refId": "A"
}
],
"title": "Other codes over [$interval]",
"title": "Other codes over $interval",
"type": "timeseries"
},
{

View file

@ -242,7 +242,7 @@
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "sum(rate(traefik_entrypoint_requests_total{entrypoint=~\"$entrypoint\"}[1m])) by (entrypoint)",
"expr": "sum(rate(traefik_entrypoint_requests_total{entrypoint=~\"$entrypoint\"}[$interval])) by (entrypoint)",
"legendFormat": "{{entrypoint}}",
"range": true,
"refId": "A"
@ -340,7 +340,7 @@
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "(sum(rate(traefik_entrypoint_request_duration_seconds_bucket{le=\"0.3\",code=\"200\",entrypoint=~\"$entrypoint\"}[5m])) by (method) + \n sum(rate(traefik_entrypoint_request_duration_seconds_bucket{le=\"1.2\",code=\"200\",entrypoint=~\"$entrypoint\"}[5m])) by (method)) / 2 / \n sum(rate(traefik_entrypoint_request_duration_seconds_count{code=\"200\",entrypoint=~\"$entrypoint\"}[5m])) by (method)\n",
"expr": "(sum(rate(traefik_entrypoint_request_duration_seconds_bucket{le=\"0.3\",code=\"200\",entrypoint=~\"$entrypoint\"}[$interval])) by (method) + \n sum(rate(traefik_entrypoint_request_duration_seconds_bucket{le=\"1.2\",code=\"200\",entrypoint=~\"$entrypoint\"}[$interval])) by (method)) / 2 / \n sum(rate(traefik_entrypoint_request_duration_seconds_count{code=\"200\",entrypoint=~\"$entrypoint\"}[$interval])) by (method)\n",
"legendFormat": "{{method}}",
"range": true,
"refId": "A"
@ -408,7 +408,7 @@
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "sum(rate(traefik_service_requests_total{service=~\"$service.*\",protocol=\"http\"}[1m])) by (method, code)",
"expr": "sum(rate(traefik_service_requests_total{service=~\"$service.*\",protocol=\"http\"}[$interval])) by (method, code)",
"legendFormat": "{{method}}[{{code}}]",
"range": true,
"refId": "A"
@ -606,7 +606,7 @@
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "topk(15,\n label_replace(\n sum by (service,code) \n (rate(traefik_service_requests_total{service=~\"$service.*\",protocol=\"http\"}[5m])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)",
"expr": "topk(15,\n label_replace(\n sum by (service,code) \n (rate(traefik_service_requests_total{service=~\"$service.*\",protocol=\"http\"}[$interval])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)",
"legendFormat": "[{{code}}] on {{service}}",
"range": true,
"refId": "A"
@ -710,7 +710,7 @@
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "label_replace(\n 1 - (sum by (service)\n (rate(traefik_service_request_duration_seconds_bucket{le=\"1.2\",service=~\"$service.*\"}[5m])) / sum by (service) \n (rate(traefik_service_request_duration_seconds_count{service=~\"$service.*\"}[5m]))\n ) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\"\n)",
"expr": "label_replace(\n 1 - (sum by (service)\n (rate(traefik_service_request_duration_seconds_bucket{le=\"1.2\",service=~\"$service.*\"}[$interval])) / sum by (service) \n (rate(traefik_service_request_duration_seconds_count{service=~\"$service.*\"}[$interval]))\n ) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\"\n)",
"legendFormat": "{{service}}",
"range": true,
"refId": "A"
@ -804,7 +804,7 @@
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "label_replace(\n 1 - (sum by (service)\n (rate(traefik_service_request_duration_seconds_bucket{le=\"0.3\",service=~\"$service.*\"}[5m])) / sum by (service) \n (rate(traefik_service_request_duration_seconds_count{service=~\"$service.*\"}[5m]))\n ) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\"\n)",
"expr": "label_replace(\n 1 - (sum by (service)\n (rate(traefik_service_request_duration_seconds_bucket{le=\"0.3\",service=~\"$service.*\"}[$interval])) / sum by (service) \n (rate(traefik_service_request_duration_seconds_count{service=~\"$service.*\"}[$interval]))\n ) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\"\n)",
"legendFormat": "{{service}}",
"range": true,
"refId": "A"
@ -916,13 +916,13 @@
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "topk(15,\n label_replace(\n sum by (service,method,code) \n (rate(traefik_service_requests_total{service=~\"$service.*\",code=~\"2..\",protocol=\"http\"}[5m])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)",
"expr": "topk(15,\n label_replace(\n sum by (service,method,code) \n (rate(traefik_service_requests_total{service=~\"$service.*\",code=~\"2..\",protocol=\"http\"}[$interval])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)",
"legendFormat": "{{method}}[{{code}}] on {{service}}",
"range": true,
"refId": "A"
}
],
"title": "2xx over 5 min",
"title": "2xx over $interval",
"type": "timeseries"
},
{
@ -1015,13 +1015,13 @@
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "topk(15,\n label_replace(\n sum by (service,method,code) \n (rate(traefik_service_requests_total{service=~\"$service.*\",code=~\"5..\",protocol=\"http\"}[5m])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)",
"expr": "topk(15,\n label_replace(\n sum by (service,method,code) \n (rate(traefik_service_requests_total{service=~\"$service.*\",code=~\"5..\",protocol=\"http\"}[$interval])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)",
"legendFormat": "{{method}}[{{code}}] on {{service}}",
"range": true,
"refId": "A"
}
],
"title": "5xx over 5 min",
"title": "5xx over $interval",
"type": "timeseries"
},
{
@ -1114,13 +1114,13 @@
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "topk(15,\n label_replace(\n sum by (service,method,code) \n (rate(traefik_service_requests_total{service=~\"$service.*\",code!~\"2..|5..\",protocol=\"http\"}[5m])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)",
"expr": "topk(15,\n label_replace(\n sum by (service,method,code) \n (rate(traefik_service_requests_total{service=~\"$service.*\",code!~\"2..|5..\",protocol=\"http\"}[$interval])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)",
"legendFormat": "{{method}}[{{code}}] on {{service}}",
"range": true,
"refId": "A"
}
],
"title": "Other codes over 5 min",
"title": "Other codes over $interval",
"type": "timeseries"
},
{
@ -1213,7 +1213,7 @@
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "topk(15,\n label_replace(\n sum by (service,method) \n (rate(traefik_service_requests_bytes_total{service=~\"$service.*\",protocol=\"http\"}[1m])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)",
"expr": "topk(15,\n label_replace(\n sum by (service,method) \n (rate(traefik_service_requests_bytes_total{service=~\"$service.*\",protocol=\"http\"}[$interval])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)",
"legendFormat": "{{method}} on {{service}}",
"range": true,
"refId": "A"
@ -1312,7 +1312,7 @@
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "topk(15,\n label_replace(\n sum by (service,method) \n (rate(traefik_service_responses_bytes_total{service=~\"$service.*\",protocol=\"http\"}[1m])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)",
"expr": "topk(15,\n label_replace(\n sum by (service,method) \n (rate(traefik_service_responses_bytes_total{service=~\"$service.*\",protocol=\"http\"}[$interval])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)",
"legendFormat": "{{method}} on {{service}}",
"range": true,
"refId": "A"
@ -1448,6 +1448,69 @@
"skipUrlSync": false,
"type": "datasource"
},
{
"auto": true,
"auto_count": 30,
"auto_min": "1m",
"current": {
"selected": false,
"text": "auto",
"value": "$__auto_interval_interval"
},
"hide": 0,
"name": "interval",
"options": [
{
"selected": true,
"text": "auto",
"value": "$__auto_interval_interval"
},
{
"selected": false,
"text": "1m",
"value": "1m"
},
{
"selected": false,
"text": "5m",
"value": "5m"
},
{
"selected": false,
"text": "10m",
"value": "10m"
},
{
"selected": false,
"text": "30m",
"value": "30m"
},
{
"selected": false,
"text": "1h",
"value": "1h"
},
{
"selected": false,
"text": "2h",
"value": "2h"
},
{
"selected": false,
"text": "4h",
"value": "4h"
},
{
"selected": false,
"text": "8h",
"value": "8h"
}
],
"query": "1m,5m,10m,30m,1h,2h,4h,8h",
"refresh": 2,
"skipUrlSync": false,
"type": "interval"
},
{
"current": {},
"datasource": {

View file

@ -617,6 +617,7 @@ It defaults to `2160` (90 days) to follow Let's Encrypt certificates' duration.
|----------------------|-------------------|-------------------------|
| >= 1 year | 4 months | 1 week |
| >= 90 days | 30 days | 1 day |
| >= 30 days | 10 days | 12 hours |
| >= 7 days | 1 day | 1 hour |
| >= 24 hours | 6 hours | 10 min |
| < 24 hours | 20 min | 1 min |

View file

@ -7,16 +7,22 @@ description: "Traefik Proxy, an open source Edge Router, auto-discovers configur
![Architecture](assets/img/traefik-architecture.png)
Traefik is an [open-source](https://github.com/traefik/traefik) *Edge Router* that makes publishing your services a fun and easy experience.
It receives requests on behalf of your system and finds out which components are responsible for handling them.
Traefik is an [open-source](https://github.com/traefik/traefik) *Application Proxy* that makes publishing your services a fun and easy experience.
It receives requests on behalf of your system and identifies which components are responsible for handling them, and routes them securely.
What sets Traefik apart, besides its many features, is that it automatically discovers the right configuration for your services.
The magic happens when Traefik inspects your infrastructure, where it finds relevant information and discovers which service serves which request.
Traefik is natively compliant with every major cluster technology, such as Kubernetes, Docker, Docker Swarm, AWS, and [the list goes on](providers/overview.md); and can handle many at the same time. (It even works for legacy software running on bare metal.)
Traefik is natively compliant with every major cluster technology, such as Kubernetes, Docker Swarm, AWS, and [the list goes on](providers/overview.md); and can handle many at the same time. (It even works for legacy software running on bare metal.)
With Traefik, there is no need to maintain and synchronize a separate configuration file: everything happens automatically, in real time (no restarts, no connection interruptions).
With Traefik, you spend time developing and deploying new features to your system, not on configuring and maintaining its working state.
With Traefik, you spend time developing and deploying new features to your system, not on configuring and maintaining its working state.
And if your needs change, you can add API gateway and API management capabilities seamlessly to your existing Traefik deployments. It takes less than a minute, theres no rip-and-replace, and all your configurations are preserved. See how it works in this video:
<div style="text-align: center;">
<iframe src="https://www.youtube.com/embed/zriUO5YPgFg?modestbranding=1&rel=0&controls=1" width="560" height="315" title="Upgrade Traefik Proxy to API Gateway and API Management in Seconds // Traefik Labs" frameborder="0" allowfullscreen></iframe>
</div>
Developing Traefik, our main goal is to make it effortless to use, and we're sure you'll enjoy it.
@ -26,4 +32,4 @@ Developing Traefik, our main goal is to make it effortless to use, and we're sur
Join our user friendly and active [Community Forum](https://community.traefik.io "Link to Traefik Community Forum") to discuss, learn, and connect with the Traefik community.
Using Traefik OSS in Production? Add enterprise-grade API Gateway and API Management capabilities to your existing deployments seamlessly. No rip and replace. No learning curve. Learn more from [this short video](https://info.traefik.io/traefik-upgrade-walkthrough)
Using Traefik OSS in Production? Consider our enterprise-grade [API Gateway](https://traefik.io/traefik-hub-api-gateway/), [API Management](https://traefik.io/traefik-hub/), and [Commercial Support](https://info.traefik.io/request-commercial-support) solutions.

View file

@ -255,3 +255,48 @@ http:
[http.middlewares.test-compress.compress]
defaultEncoding = "gzip"
```
### `encodings`
_Optional, Default="zstd, br, gzip"_
`encodings` specifies the list of supported compression encodings.
At least one encoding value must be specified, and valid entries are `zstd` (Zstandard), `br` (Brotli), and `gzip` (Gzip).
The order of the list also sets the priority, the top entry has the highest priority.
```yaml tab="Docker & Swarm"
labels:
- "traefik.http.middlewares.test-compress.compress.encodings=zstd,br"
```
```yaml tab="Kubernetes"
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: test-compress
spec:
compress:
encodings:
- zstd
- br
```
```yaml tab="Consul Catalog"
- "traefik.http.middlewares.test-compress.compress.encodings=zstd,br"
```
```yaml tab="File (YAML)"
http:
middlewares:
test-compress:
compress:
encodings:
- zstd
- br
```
```toml tab="File (TOML)"
[http.middlewares]
[http.middlewares.test-compress.compress]
encodings = ["zstd","br"]
```

View file

@ -591,6 +591,11 @@ Please take a look at the observability documentation for more information:
- [Metrics](../observability/metrics/overview.md#addinternals)
- [Tracing](../observability/tracing/overview.md#addinternals)
#### Access logs
In v3, the `ServiceURL` field is not an object anymore but a string representation.
An update may be required if you index access logs.
## Dynamic Configuration Changes
### Router Rule Matchers

View file

@ -455,7 +455,7 @@ To enable HTTP/3 on an EntryPoint, please check out the [HTTP/3 configuration](.
In `v2.6`, the [Kubernetes Gateway API provider](../providers/kubernetes-gateway.md) now only supports the version [v1alpha2](https://gateway-api.sigs.k8s.io/v1alpha2/guides/) of the specification and
[route namespaces](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.RouteNamespaces) selectors, which requires Traefik to fetch and watch the cluster namespaces.
Therefore, the [RBAC](../reference/dynamic-configuration/kubernetes-gateway.md#rbac) and [CRD](../reference/dynamic-configuration/kubernetes-gateway.md#definitions) definitions must be updated.
Therefore, the RBAC and CRD definitions must be updated.
## v2.6.0 to v2.6.1

View file

@ -10,9 +10,11 @@ description: "Learn the steps needed to migrate to new Traefik Proxy v3 versions
### Kubernetes Provider RBACs
Starting with v3.1, the Kubernetes Providers now use the [EndpointSlices API](https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/) (Kubernetes >=v1.21) to discover service endpoint addresses.
It also brings NodePort load-balancing which requires Nodes resources lookup.
Therefore, in the corresponding RBACs (see [KubernetesIngress](../routing/providers/kubernetes-ingress.md#configuration-example), [KubernetesCRD](../reference/dynamic-configuration/kubernetes-crd.md#rbac), and [KubernetesGateway](../reference/dynamic-configuration/kubernetes-gateway.md#rbac) provider RBACs),
the `endpoints` right has to be removed and the following `endpointslices` right has to be added.
Therefore, in the corresponding RBACs (see [KubernetesIngress](../routing/providers/kubernetes-ingress.md#configuration-example), [KubernetesCRD](../reference/dynamic-configuration/kubernetes-crd.md#rbac), and [KubernetesGateway](../reference/dynamic-configuration/kubernetes-gateway-rbac.yml) provider RBACs):
- the `endpoints` right has to be removed and the following `endpointslices` right has to be added:
```yaml
...
@ -26,6 +28,21 @@ the `endpoints` right has to be removed and the following `endpointslices` right
...
```
- the `nodes` right has to be added:
```yaml
...
- apiGroups:
- ""
resources:
- nodes
verbs:
- get
- list
- watch
...
```
#### Gateway API: KubernetesGateway Provider
In v3.1, the KubernetesGateway Provider is no longer an experimental feature.
@ -51,3 +68,10 @@ It can be enabled without the associated `experimental.kubernetesgateway` option
The `kubernetesgateway` option should be removed from the experimental section of the static configuration.
To configure `kubernetesgateway`, please check out the [KubernetesGateway Provider documentation](../providers/kubernetes-gateway.md).
## v3.1.0 to v3.1.1
### IngressClass Lookup
The Kubernetes Ingress provider option `disableIngressClassLookup` has been deprecated in v3.1.1, and will be removed in the next major version.
Please use the `disableClusterScopeResources` option instead to avoid cluster scope resources discovery (IngressClass, Nodes).

View file

@ -134,6 +134,7 @@ You can specify which Docker API Endpoint to use with the directive [`endpoint`]
- Accounting at container level, by exposing the socket on a another container than Traefik's.
- Accounting at kernel level, by enforcing kernel calls with mechanisms like [SELinux](https://en.wikipedia.org/wiki/Security-Enhanced_Linux), to only allows an identified set of actions for Traefik's process (or the "socket exposer" process).
- SSH public key authentication (SSH is supported with Docker > 18.09)
- Authentication using HTTP Basic authentication through an HTTP proxy that exposes the Docker daemon socket.
??? info "More Resources and Examples"
@ -216,6 +217,50 @@ See the [Docker API Access](#docker-api-access) section for more information.
# ...
```
??? example "Using HTTP"
Using Docker Engine API you can connect Traefik to remote daemon using HTTP.
```yaml tab="File (YAML)"
providers:
docker:
endpoint: "http://127.0.0.1:2375"
# ...
```
```toml tab="File (TOML)"
[providers.docker]
endpoint = "http://127.0.0.1:2375"
# ...
```
```bash tab="CLI"
--providers.docker.endpoint=http://127.0.0.1:2375
# ...
```
??? example "Using TCP"
Using Docker Engine API you can connect Traefik to remote daemon using TCP.
```yaml tab="File (YAML)"
providers:
docker:
endpoint: "tcp://127.0.0.1:2375"
# ...
```
```toml tab="File (TOML)"
[providers.docker]
endpoint = "tcp://127.0.0.1:2375"
# ...
```
```bash tab="CLI"
--providers.docker.endpoint=tcp://127.0.0.1:2375
# ...
```
```yaml tab="File (YAML)"
providers:
docker:
@ -231,6 +276,56 @@ providers:
--providers.docker.endpoint=unix:///var/run/docker.sock
```
### `username`
_Optional, Default=""_
Defines the username for Basic HTTP authentication.
This should be used when the Docker daemon socket is exposed through an HTTP proxy that requires Basic HTTP authentication.
```yaml tab="File (YAML)"
providers:
docker:
username: foo
# ...
```
```toml tab="File (TOML)"
[providers.docker]
username = "foo"
# ...
```
```bash tab="CLI"
--providers.docker.username="foo"
# ...
```
### `password`
_Optional, Default=""_
Defines the password for Basic HTTP authentication.
This should be used when the Docker daemon socket is exposed through an HTTP proxy that requires Basic HTTP authentication.
```yaml tab="File (YAML)"
providers:
docker:
password: foo
# ...
```
```toml tab="File (TOML)"
[providers.docker]
password = "foo"
# ...
```
```bash tab="CLI"
--providers.docker.password="foo"
# ...
```
### `useBindPortIP`
_Optional, Default=false_

View file

@ -84,8 +84,9 @@ Defines custom headers to be sent to the endpoint.
```yaml tab="File (YAML)"
providers:
headers:
name: value
http:
headers:
name: value
```
```toml tab="File (TOML)"
@ -95,6 +96,7 @@ providers:
```bash tab="CLI"
--providers.http.headers.name=value
```
### `tls`

View file

@ -8,7 +8,7 @@ description: "Learn how to use the Kubernetes Gateway API as a provider for conf
The Kubernetes Gateway provider is a Traefik implementation of the [Gateway API](https://gateway-api.sigs.k8s.io/)
specification from the Kubernetes Special Interest Groups (SIGs).
This provider supports version [v1.1.0](https://github.com/kubernetes-sigs/gateway-api/releases/tag/v1.1.0) of the Gateway API specification.
This provider supports Standard version [v1.1.0](https://github.com/kubernetes-sigs/gateway-api/releases/tag/v1.1.0) of the Gateway API specification.
It fully supports all HTTP core and some extended features, as well as the `TCPRoute` and `TLSRoute` resources from the [Experimental channel](https://gateway-api.sigs.k8s.io/concepts/versioning/?h=#release-channels).
@ -26,10 +26,10 @@ For more details, check out the conformance [report](https://github.com/kubernet
1. Install/update the Kubernetes Gateway API CRDs.
```bash
# Install Gateway API CRDs from the Experimental channel.
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.1.0/experimental-install.yaml
# Install Gateway API CRDs from the Standard channel.
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.1.0/standard-install.yaml
```
2. Install/update the Traefik [RBAC](../reference/dynamic-configuration/kubernetes-gateway.md#rbac).
```bash
@ -38,7 +38,7 @@ For more details, check out the conformance [report](https://github.com/kubernet
```
3. Deploy Traefik and enable the `kubernetesGateway` provider in the static configuration as detailed below:
```yaml tab="File (YAML)"
providers:
kubernetesGateway: {}
@ -269,6 +269,15 @@ providers:
--providers.kubernetesgateway.experimentalchannel=true
```
!!! info "Experimental Channel"
When enabling experimental channel resources support, the experimental CRDs (Custom Resource Definitions) needs to be deployed too.
```bash
# Install Gateway API CRDs from the Experimental channel.
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.1.0/experimental-install.yaml
```
### `labelselector`
_Optional, Default: ""_

View file

@ -287,6 +287,11 @@ providers:
_Optional, Default: false_
??? warning "Deprecated"
The Kubernetes Ingress provider option `disableIngressClassLookup` has been deprecated in v3.1, and will be removed in the next major version.
Please use the `disableClusterScopeResources` option instead.
If the parameter is set to `true`,
Traefik will not discover IngressClasses in the cluster.
By doing so, it alleviates the requirement of giving Traefik the rights to look IngressClasses up.
@ -312,6 +317,33 @@ providers:
--providers.kubernetesingress.disableingressclasslookup=true
```
### `disableClusterScopeResources`
_Optional, Default: false_
When this parameter is set to `true`,
Traefik will not discover cluster scope resources (`IngressClass` and `Nodes`).
By doing so, it alleviates the requirement of giving Traefik the rights to look up for cluster resources.
Furthermore, Traefik will not handle Ingresses with IngressClass references, therefore such Ingresses will be ignored (please note that annotations are not affected by this option).
This will also prevent from using the `NodePortLB` options on services.
```yaml tab="File (YAML)"
providers:
kubernetesIngress:
disableClusterScopeResources: true
# ...
```
```toml tab="File (TOML)"
[providers.kubernetesIngress]
disableClusterScopeResources = true
# ...
```
```bash tab="CLI"
--providers.kubernetesingress.disableClusterScopeResources=true
```
### `ingressEndpoint`
#### `hostname`

View file

@ -151,6 +151,7 @@ You can specify which Docker API Endpoint to use with the directive [`endpoint`]
It allows scheduling of Traefik on worker nodes, with only the "socket exposer" container on the manager nodes.
- Accounting at kernel level, by enforcing kernel calls with mechanisms like [SELinux](https://en.wikipedia.org/wiki/Security-Enhanced_Linux), to only allows an identified set of actions for Traefik's process (or the "socket exposer" process).
- SSH public key authentication (SSH is supported with Docker > 18.09)
- Authentication using HTTP Basic authentication through an HTTP proxy that exposes the Docker daemon socket.
??? info "More Resources and Examples"
@ -262,6 +263,50 @@ See the [Docker Swarm API Access](#docker-api-access) section for more informati
# ...
```
??? example "Using HTTP"
Using Docker Engine API you can connect Traefik to remote daemon using HTTP.
```yaml tab="File (YAML)"
providers:
swarm:
endpoint: "http://127.0.0.1:2375"
# ...
```
```toml tab="File (TOML)"
[providers.swarm]
swarm = "http://127.0.0.1:2375"
# ...
```
```bash tab="CLI"
--providers.swarm.endpoint=http://127.0.0.1:2375
# ...
```
??? example "Using TCP"
Using Docker Engine API you can connect Traefik to remote daemon using TCP.
```yaml tab="File (YAML)"
providers:
swarm:
endpoint: "tcp://127.0.0.1:2375"
# ...
```
```toml tab="File (TOML)"
[providers.swarm]
swarm = "tcp://127.0.0.1:2375"
# ...
```
```bash tab="CLI"
--providers.swarm.endpoint=tcp://127.0.0.1:2375
# ...
```
```yaml tab="File (YAML)"
providers:
swarm:
@ -277,6 +322,56 @@ providers:
--providers.swarm.endpoint=unix:///var/run/docker.sock
```
### `username`
_Optional, Default=""_
Defines the username for Basic HTTP authentication.
This should be used when the Docker daemon socket is exposed through an HTTP proxy that requires Basic HTTP authentication.
```yaml tab="File (YAML)"
providers:
swarm:
username: foo
# ...
```
```toml tab="File (TOML)"
[providers.swarm]
username = "foo"
# ...
```
```bash tab="CLI"
--providers.swarm.username="foo"
# ...
```
### `password`
_Optional, Default=""_
Defines the password for Basic HTTP authentication.
This should be used when the Docker daemon socket is exposed through an HTTP proxy that requires Basic HTTP authentication.
```yaml tab="File (YAML)"
providers:
swarm:
password: foo
# ...
```
```toml tab="File (TOML)"
[providers.swarm]
password = "foo"
# ...
```
```bash tab="CLI"
--providers.swarm.password="foo"
# ...
```
### `useBindPortIP`
_Optional, Default=false_

View file

@ -19,6 +19,7 @@
- "traefik.http.middlewares.middleware05.circuitbreaker.responsecode=42"
- "traefik.http.middlewares.middleware06.compress=true"
- "traefik.http.middlewares.middleware06.compress.defaultencoding=foobar"
- "traefik.http.middlewares.middleware06.compress.encodings=foobar, foobar"
- "traefik.http.middlewares.middleware06.compress.excludedcontenttypes=foobar, foobar"
- "traefik.http.middlewares.middleware06.compress.includedcontenttypes=foobar, foobar"
- "traefik.http.middlewares.middleware06.compress.minresponsebodybytes=42"

View file

@ -143,6 +143,7 @@
excludedContentTypes = ["foobar", "foobar"]
includedContentTypes = ["foobar", "foobar"]
minResponseBodyBytes = 42
encodings = ["foobar", "foobar"]
defaultEncoding = "foobar"
[http.middlewares.Middleware07]
[http.middlewares.Middleware07.contentType]

View file

@ -152,6 +152,9 @@ http:
- foobar
- foobar
minResponseBodyBytes: 42
encodings:
- foobar
- foobar
defaultEncoding: foobar
Middleware07:
contentType:

View file

@ -904,7 +904,7 @@ spec:
compress:
description: |-
Compress holds the compress middleware configuration.
This middleware compresses responses before sending them to the client, using gzip compression.
This middleware compresses responses before sending them to the client, using gzip, brotli, or zstd compression.
More info: https://doc.traefik.io/traefik/v3.1/middlewares/http/compress/
properties:
defaultEncoding:
@ -912,6 +912,12 @@ spec:
the `Accept-Encoding` header is not in the request or contains
a wildcard (`*`).
type: string
encodings:
description: Encodings defines the list of supported compression
algorithms.
items:
type: string
type: array
excludedContentTypes:
description: |-
ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests and responses before compressing.

View file

@ -22,6 +22,8 @@ THIS FILE MUST NOT BE EDITED BY HAND
| `traefik/http/middlewares/Middleware05/circuitBreaker/recoveryDuration` | `42s` |
| `traefik/http/middlewares/Middleware05/circuitBreaker/responseCode` | `42` |
| `traefik/http/middlewares/Middleware06/compress/defaultEncoding` | `foobar` |
| `traefik/http/middlewares/Middleware06/compress/encodings/0` | `foobar` |
| `traefik/http/middlewares/Middleware06/compress/encodings/1` | `foobar` |
| `traefik/http/middlewares/Middleware06/compress/excludedContentTypes/0` | `foobar` |
| `traefik/http/middlewares/Middleware06/compress/excludedContentTypes/1` | `foobar` |
| `traefik/http/middlewares/Middleware06/compress/includedContentTypes/0` | `foobar` |

View file

@ -180,7 +180,7 @@ spec:
compress:
description: |-
Compress holds the compress middleware configuration.
This middleware compresses responses before sending them to the client, using gzip compression.
This middleware compresses responses before sending them to the client, using gzip, brotli, or zstd compression.
More info: https://doc.traefik.io/traefik/v3.1/middlewares/http/compress/
properties:
defaultEncoding:
@ -188,6 +188,12 @@ spec:
the `Accept-Encoding` header is not in the request or contains
a wildcard (`*`).
type: string
encodings:
description: Encodings defines the list of supported compression
algorithms.
items:
type: string
type: array
excludedContentTypes:
description: |-
ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests and responses before compressing.

View file

@ -591,6 +591,9 @@ Client timeout for HTTP connections. (Default: ```0```)
`--providers.docker.network`:
Default Docker network used.
`--providers.docker.password`:
Password for Basic HTTP authentication.
`--providers.docker.tls.ca`:
TLS CA
@ -606,6 +609,9 @@ TLS key
`--providers.docker.usebindportip`:
Use the ip address from the bound port, rather than from the inner network. (Default: ```false```)
`--providers.docker.username`:
Username for Basic HTTP authentication.
`--providers.docker.watch`:
Watch Docker events. (Default: ```true```)
@ -726,6 +732,9 @@ Allow ExternalName services. (Default: ```false```)
`--providers.kubernetescrd.certauthfilepath`:
Kubernetes certificate authority file path (not needed for in-cluster client).
`--providers.kubernetescrd.disableclusterscoperesources`:
Disables the lookup of cluster scope resources (incompatible with IngressClasses and NodePortLB enabled services). (Default: ```false```)
`--providers.kubernetescrd.endpoint`:
Kubernetes server endpoint (required for external cluster client).
@ -798,8 +807,11 @@ Allow ExternalName services. (Default: ```false```)
`--providers.kubernetesingress.certauthfilepath`:
Kubernetes certificate authority file path (not needed for in-cluster client).
`--providers.kubernetesingress.disableclusterscoperesources`:
Disables the lookup of cluster scope resources (incompatible with IngressClasses and NodePortLB enabled services). (Default: ```false```)
`--providers.kubernetesingress.disableingressclasslookup`:
Disables the lookup of IngressClasses. (Default: ```false```)
Disables the lookup of IngressClasses (Deprecated, please use DisableClusterScopeResources). (Default: ```false```)
`--providers.kubernetesingress.endpoint`:
Kubernetes server endpoint (required for external cluster client).
@ -969,6 +981,9 @@ Client timeout for HTTP connections. (Default: ```0```)
`--providers.swarm.network`:
Default Docker network used.
`--providers.swarm.password`:
Password for Basic HTTP authentication.
`--providers.swarm.refreshseconds`:
Polling interval for swarm mode. (Default: ```15```)
@ -987,6 +1002,9 @@ TLS key
`--providers.swarm.usebindportip`:
Use the ip address from the bound port, rather than from the inner network. (Default: ```false```)
`--providers.swarm.username`:
Username for Basic HTTP authentication.
`--providers.swarm.watch`:
Watch Docker events. (Default: ```true```)

View file

@ -591,6 +591,9 @@ Client timeout for HTTP connections. (Default: ```0```)
`TRAEFIK_PROVIDERS_DOCKER_NETWORK`:
Default Docker network used.
`TRAEFIK_PROVIDERS_DOCKER_PASSWORD`:
Password for Basic HTTP authentication.
`TRAEFIK_PROVIDERS_DOCKER_TLS_CA`:
TLS CA
@ -606,6 +609,9 @@ TLS key
`TRAEFIK_PROVIDERS_DOCKER_USEBINDPORTIP`:
Use the ip address from the bound port, rather than from the inner network. (Default: ```false```)
`TRAEFIK_PROVIDERS_DOCKER_USERNAME`:
Username for Basic HTTP authentication.
`TRAEFIK_PROVIDERS_DOCKER_WATCH`:
Watch Docker events. (Default: ```true```)
@ -726,6 +732,9 @@ Allow ExternalName services. (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESCRD_CERTAUTHFILEPATH`:
Kubernetes certificate authority file path (not needed for in-cluster client).
`TRAEFIK_PROVIDERS_KUBERNETESCRD_DISABLECLUSTERSCOPERESOURCES`:
Disables the lookup of cluster scope resources (incompatible with IngressClasses and NodePortLB enabled services). (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESCRD_ENDPOINT`:
Kubernetes server endpoint (required for external cluster client).
@ -798,8 +807,11 @@ Allow ExternalName services. (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_CERTAUTHFILEPATH`:
Kubernetes certificate authority file path (not needed for in-cluster client).
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_DISABLECLUSTERSCOPERESOURCES`:
Disables the lookup of cluster scope resources (incompatible with IngressClasses and NodePortLB enabled services). (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_DISABLEINGRESSCLASSLOOKUP`:
Disables the lookup of IngressClasses. (Default: ```false```)
Disables the lookup of IngressClasses (Deprecated, please use DisableClusterScopeResources). (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_ENDPOINT`:
Kubernetes server endpoint (required for external cluster client).
@ -969,6 +981,9 @@ Client timeout for HTTP connections. (Default: ```0```)
`TRAEFIK_PROVIDERS_SWARM_NETWORK`:
Default Docker network used.
`TRAEFIK_PROVIDERS_SWARM_PASSWORD`:
Password for Basic HTTP authentication.
`TRAEFIK_PROVIDERS_SWARM_REFRESHSECONDS`:
Polling interval for swarm mode. (Default: ```15```)
@ -987,6 +1002,9 @@ TLS key
`TRAEFIK_PROVIDERS_SWARM_USEBINDPORTIP`:
Use the ip address from the bound port, rather than from the inner network. (Default: ```false```)
`TRAEFIK_PROVIDERS_SWARM_USERNAME`:
Username for Basic HTTP authentication.
`TRAEFIK_PROVIDERS_SWARM_WATCH`:
Watch Docker events. (Default: ```true```)

View file

@ -85,6 +85,8 @@
useBindPortIP = true
watch = true
defaultRule = "foobar"
username = "foobar"
password = "foobar"
endpoint = "foobar"
httpClientTimeout = "42s"
[providers.docker.tls]
@ -100,6 +102,8 @@
useBindPortIP = true
watch = true
defaultRule = "foobar"
username = "foobar"
password = "foobar"
endpoint = "foobar"
httpClientTimeout = "42s"
refreshSeconds = "42s"
@ -124,6 +128,7 @@
allowEmptyServices = true
allowExternalNameServices = true
disableIngressClassLookup = true
disableClusterScopeResources = true
nativeLBByDefault = true
[providers.kubernetesIngress.ingressEndpoint]
ip = "foobar"
@ -141,6 +146,7 @@
throttleDuration = "42s"
allowEmptyServices = true
nativeLBByDefault = true
disableClusterScopeResources = true
[providers.kubernetesGateway]
endpoint = "foobar"
token = "foobar"

View file

@ -96,6 +96,8 @@ providers:
useBindPortIP: true
watch: true
defaultRule: foobar
username: foobar
password: foobar
endpoint: foobar
tls:
ca: foobar
@ -111,6 +113,8 @@ providers:
useBindPortIP: true
watch: true
defaultRule: foobar
username: foobar
password: foobar
endpoint: foobar
tls:
ca: foobar
@ -141,6 +145,7 @@ providers:
allowEmptyServices: true
allowExternalNameServices: true
disableIngressClassLookup: true
disableClusterScopeResources: true
nativeLBByDefault: true
kubernetesCRD:
endpoint: foobar
@ -156,6 +161,7 @@ providers:
throttleDuration: 42s
allowEmptyServices: true
nativeLBByDefault: true
disableClusterScopeResources: true
kubernetesGateway:
endpoint: foobar
token: foobar

20
go.mod
View file

@ -12,8 +12,8 @@ require (
github.com/containous/alice v0.0.0-20181107144136-d83ebdd94cbd // No tag on the repo.
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e // No tag on the repo.
github.com/coreos/go-systemd/v22 v22.5.0
github.com/docker/cli v24.0.9+incompatible
github.com/docker/docker v27.0.3+incompatible
github.com/docker/cli v27.1.1+incompatible
github.com/docker/docker v27.1.1+incompatible
github.com/docker/go-connections v0.5.0
github.com/fatih/structs v1.1.0
github.com/fsnotify/fsnotify v1.7.0
@ -22,7 +22,7 @@ require (
github.com/go-kit/log v0.2.1
github.com/golang/protobuf v1.5.4
github.com/google/go-github/v28 v28.1.1
github.com/gorilla/mux v1.8.0
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.1
github.com/hashicorp/consul/api v1.26.1
github.com/hashicorp/go-hclog v1.6.3
@ -34,7 +34,7 @@ require (
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/juliens/wasm-goexport v0.0.6
github.com/klauspost/compress v1.17.4
github.com/klauspost/compress v1.17.9
github.com/kvtools/consul v1.0.2
github.com/kvtools/etcdv3 v1.0.2
github.com/kvtools/redis v1.1.0
@ -129,7 +129,7 @@ require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Microsoft/hcsshim v0.11.5 // indirect
github.com/Microsoft/hcsshim v0.11.7 // indirect
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
github.com/VividCortex/gohistogram v1.0.0 // indirect
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect
@ -157,9 +157,9 @@ require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/civo/civogo v0.3.11 // indirect
github.com/cloudflare/cloudflare-go v0.97.0 // indirect
github.com/containerd/containerd v1.7.18 // indirect
github.com/containerd/errdefs v0.1.0 // indirect
github.com/containerd/containerd v1.7.20 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/cpu/goacmedns v0.1.1 // indirect
github.com/cpuguy83/dockercfg v0.3.1 // indirect
@ -194,7 +194,7 @@ require (
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
github.com/go-zookeeper/zk v1.0.3 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/gofrs/flock v0.8.1 // indirect
github.com/gofrs/flock v0.12.0 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
@ -255,7 +255,7 @@ require (
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/user v0.1.0 // indirect
github.com/moby/sys/user v0.2.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
@ -287,7 +287,7 @@ require (
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/pquerna/otp v1.4.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/redis/go-redis/v9 v9.2.1 // indirect
github.com/rs/cors v1.7.0 // indirect

35
go.sum
View file

@ -82,8 +82,8 @@ github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Microsoft/hcsshim v0.11.5 h1:haEcLNpj9Ka1gd3B3tAEs9CpE0c+1IhoL59w/exYU38=
github.com/Microsoft/hcsshim v0.11.5/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU=
github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ=
github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 h1:xPMsUicZ3iosVPSIP7bW5EcGUzjiiMl1OYTe14y/R24=
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks=
@ -192,12 +192,12 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao=
github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4=
github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM=
github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0=
github.com/containerd/containerd v1.7.20 h1:Sl6jQYk3TRavaU83h66QMbI2Nqg9Jm6qzwX57Vsn1SQ=
github.com/containerd/containerd v1.7.20/go.mod h1:52GsS5CwquuqPuLncsXwG0t2CiUce+KsNHJZQJvAgR0=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/containous/alice v0.0.0-20181107144136-d83ebdd94cbd h1:0n+lFLh5zU0l6KSk3KpnDwfbPGAR44aRLgTbCnhRBHU=
github.com/containous/alice v0.0.0-20181107144136-d83ebdd94cbd/go.mod h1:BbQgeDS5i0tNvypwEoF1oNjOJw8knRAE1DnVvjDstcQ=
github.com/containous/go-http-auth v0.4.1-0.20200324110947-a37a7636d23e h1:D+uTEzDZc1Fhmd0Pq06c+O9+KkAyExw0eVmu/NOqaHU=
@ -247,10 +247,10 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dnsimple/dnsimple-go v1.7.0 h1:JKu9xJtZ3SqOC+BuYgAWeab7+EEx0sz422vu8j611ZY=
github.com/dnsimple/dnsimple-go v1.7.0/go.mod h1:EKpuihlWizqYafSnQHGCd/gyvy3HkEQJ7ODB4KdV8T8=
github.com/docker/cli v24.0.9+incompatible h1:OxbimnP/z+qVjDLpq9wbeFU3Nc30XhSe+LkwYQisD50=
github.com/docker/cli v24.0.9+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v27.0.3+incompatible h1:aBGI9TeQ4MPlhquTQKq9XbK79rKFVwXNUAYz9aXyEBE=
github.com/docker/docker v27.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE=
github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@ -377,8 +377,9 @@ github.com/goccy/go-json v0.7.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGF
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/flock v0.12.0 h1:xHW8t8GPAiGtqz7KxiSqfOEXwpOaqhpYZrTE2MQBgXY=
github.com/gofrs/flock v0.12.0/go.mod h1:FirDy1Ing0mI2+kB6wk+vyyAH+e6xiE+EYA0jnzV9jc=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@ -599,8 +600,8 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
@ -742,8 +743,8 @@ github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
github.com/moby/sys/user v0.2.0 h1:OnpapJsRp25vkhw8TFG6OLJODNh/3rEwRWtJ3kakwRM=
github.com/moby/sys/user v0.2.0/go.mod h1:RYstrcWOJpVh+6qzUqp2bU3eaRpdiQeKGlKitaH0PM8=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -872,8 +873,8 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=

View file

@ -904,7 +904,7 @@ spec:
compress:
description: |-
Compress holds the compress middleware configuration.
This middleware compresses responses before sending them to the client, using gzip compression.
This middleware compresses responses before sending them to the client, using gzip, brotli, or zstd compression.
More info: https://doc.traefik.io/traefik/v3.1/middlewares/http/compress/
properties:
defaultEncoding:
@ -912,6 +912,12 @@ spec:
the `Accept-Encoding` header is not in the request or contains
a wildcard (`*`).
type: string
encodings:
description: Encodings defines the list of supported compression
algorithms.
items:
type: string
type: array
excludedContentTypes:
description: |-
ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests and responses before compressing.

View file

@ -86,8 +86,8 @@ func (s *K8sConformanceSuite) SetupSuite() {
s.T().Fatal("Traefik image is not present")
}
s.k3sContainer, err = k3s.RunContainer(ctx,
testcontainers.WithImage(k3sImage),
s.k3sContainer, err = k3s.Run(ctx,
k3sImage,
k3s.WithManifest("./fixtures/k8s-conformance/00-experimental-v1.1.0.yml"),
k3s.WithManifest("./fixtures/k8s-conformance/01-rbac.yml"),
k3s.WithManifest("./fixtures/k8s-conformance/02-traefik.yml"),
@ -206,6 +206,7 @@ func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() {
features.SupportHTTPRouteHostRewrite,
features.SupportHTTPRoutePathRewrite,
features.SupportHTTPRoutePathRedirect,
features.SupportHTTPRouteResponseHeaderModification,
),
})
require.NoError(s.T(), err)

View file

@ -204,16 +204,17 @@ func (c *configuration) deprecationNotice(logger zerolog.Logger) bool {
}
type providers struct {
Docker *docker `json:"docker,omitempty" toml:"docker,omitempty" yaml:"docker,omitempty" label:"allowEmpty" file:"allowEmpty"`
Swarm *swarm `json:"swarm,omitempty" toml:"swarm,omitempty" yaml:"swarm,omitempty" label:"allowEmpty" file:"allowEmpty"`
Consul *consul `json:"consul,omitempty" toml:"consul,omitempty" yaml:"consul,omitempty" label:"allowEmpty" file:"allowEmpty"`
ConsulCatalog *consulCatalog `json:"consulCatalog,omitempty" toml:"consulCatalog,omitempty" yaml:"consulCatalog,omitempty" label:"allowEmpty" file:"allowEmpty"`
Nomad *nomad `json:"nomad,omitempty" toml:"nomad,omitempty" yaml:"nomad,omitempty" label:"allowEmpty" file:"allowEmpty"`
Marathon map[string]any `json:"marathon,omitempty" toml:"marathon,omitempty" yaml:"marathon,omitempty" label:"allowEmpty" file:"allowEmpty"`
Rancher map[string]any `json:"rancher,omitempty" toml:"rancher,omitempty" yaml:"rancher,omitempty" label:"allowEmpty" file:"allowEmpty"`
ETCD *etcd `json:"etcd,omitempty" toml:"etcd,omitempty" yaml:"etcd,omitempty" label:"allowEmpty" file:"allowEmpty"`
Redis *redis `json:"redis,omitempty" toml:"redis,omitempty" yaml:"redis,omitempty" label:"allowEmpty" file:"allowEmpty"`
HTTP *http `json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" label:"allowEmpty" file:"allowEmpty"`
Docker *docker `json:"docker,omitempty" toml:"docker,omitempty" yaml:"docker,omitempty" label:"allowEmpty" file:"allowEmpty"`
Swarm *swarm `json:"swarm,omitempty" toml:"swarm,omitempty" yaml:"swarm,omitempty" label:"allowEmpty" file:"allowEmpty"`
Consul *consul `json:"consul,omitempty" toml:"consul,omitempty" yaml:"consul,omitempty" label:"allowEmpty" file:"allowEmpty"`
ConsulCatalog *consulCatalog `json:"consulCatalog,omitempty" toml:"consulCatalog,omitempty" yaml:"consulCatalog,omitempty" label:"allowEmpty" file:"allowEmpty"`
Nomad *nomad `json:"nomad,omitempty" toml:"nomad,omitempty" yaml:"nomad,omitempty" label:"allowEmpty" file:"allowEmpty"`
Marathon map[string]any `json:"marathon,omitempty" toml:"marathon,omitempty" yaml:"marathon,omitempty" label:"allowEmpty" file:"allowEmpty"`
Rancher map[string]any `json:"rancher,omitempty" toml:"rancher,omitempty" yaml:"rancher,omitempty" label:"allowEmpty" file:"allowEmpty"`
ETCD *etcd `json:"etcd,omitempty" toml:"etcd,omitempty" yaml:"etcd,omitempty" label:"allowEmpty" file:"allowEmpty"`
Redis *redis `json:"redis,omitempty" toml:"redis,omitempty" yaml:"redis,omitempty" label:"allowEmpty" file:"allowEmpty"`
HTTP *http `json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" label:"allowEmpty" file:"allowEmpty"`
KubernetesIngress *ingress `json:"kubernetesIngress,omitempty" toml:"kubernetesIngress,omitempty" yaml:"kubernetesIngress,omitempty" file:"allowEmpty"`
}
func (p *providers) deprecationNotice(logger zerolog.Logger) bool {
@ -243,6 +244,7 @@ func (p *providers) deprecationNotice(logger zerolog.Logger) bool {
etcdIncompatible := p.ETCD.deprecationNotice(logger)
redisIncompatible := p.Redis.deprecationNotice(logger)
httpIncompatible := p.HTTP.deprecationNotice(logger)
p.KubernetesIngress.deprecationNotice(logger)
return incompatible ||
dockerIncompatible ||
consulIncompatible ||
@ -457,6 +459,22 @@ func (h *http) deprecationNotice(logger zerolog.Logger) bool {
return incompatible
}
type ingress struct {
DisableIngressClassLookup *bool `json:"disableIngressClassLookup,omitempty" toml:"disableIngressClassLookup,omitempty" yaml:"disableIngressClassLookup,omitempty"`
}
func (i *ingress) deprecationNotice(logger zerolog.Logger) {
if i == nil {
return
}
if i.DisableIngressClassLookup != nil {
logger.Error().Msg("Kubernetes Ingress provider `disableIngressClassLookup` option has been deprecated in v3.1, and will be removed in the next major version." +
"Please use the `disableClusterScopeResources` option instead." +
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.1/migration/v3/#ingressclasslookup")
}
}
type experimental struct {
HTTP3 *bool `json:"http3,omitempty" toml:"http3,omitempty" yaml:"http3,omitempty"`
KubernetesGateway *bool `json:"kubernetesGateway,omitempty" toml:"kubernetesGateway,omitempty" yaml:"kubernetesGateway,omitempty"`

View file

@ -40,10 +40,11 @@ type Middleware struct {
Plugin map[string]PluginConf `json:"plugin,omitempty" toml:"plugin,omitempty" yaml:"plugin,omitempty" export:"true"`
// Gateway API HTTPRoute filters middlewares.
RequestHeaderModifier *RequestHeaderModifier `json:"requestHeaderModifier,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
RequestRedirect *RequestRedirect `json:"requestRedirect,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
URLRewrite *URLRewrite `json:"URLRewrite,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
// Gateway API filter middlewares.
RequestHeaderModifier *HeaderModifier `json:"requestHeaderModifier,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
ResponseHeaderModifier *HeaderModifier `json:"responseHeaderModifier,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
RequestRedirect *RequestRedirect `json:"requestRedirect,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
URLRewrite *URLRewrite `json:"URLRewrite,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
}
// +k8s:deepcopy-gen=true
@ -165,8 +166,7 @@ func (c *CircuitBreaker) SetDefaults() {
// +k8s:deepcopy-gen=true
// Compress holds the compress middleware configuration.
// This middleware compresses responses before sending them to the client, using gzip compression.
// More info: https://doc.traefik.io/traefik/v3.1/middlewares/http/compress/
// This middleware compresses responses before sending them to the client, using gzip, brotli, or zstd compression.
type Compress struct {
// ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests and responses before compressing.
// `application/grpc` is always excluded.
@ -176,10 +176,16 @@ type Compress struct {
// MinResponseBodyBytes defines the minimum amount of bytes a response body must have to be compressed.
// Default: 1024.
MinResponseBodyBytes int `json:"minResponseBodyBytes,omitempty" toml:"minResponseBodyBytes,omitempty" yaml:"minResponseBodyBytes,omitempty" export:"true"`
// Encodings defines the list of supported compression algorithms.
Encodings []string `json:"encodings,omitempty" toml:"encodings,omitempty" yaml:"encodings,omitempty" export:"true"`
// DefaultEncoding specifies the default encoding if the `Accept-Encoding` header is not in the request or contains a wildcard (`*`).
DefaultEncoding string `json:"defaultEncoding,omitempty" toml:"defaultEncoding,omitempty" yaml:"defaultEncoding,omitempty" export:"true"`
}
func (c *Compress) SetDefaults() {
c.Encodings = []string{"zstd", "br", "gzip"}
}
// +k8s:deepcopy-gen=true
// DigestAuth holds the digest auth middleware configuration.
@ -689,8 +695,8 @@ type Users []string
// +k8s:deepcopy-gen=true
// RequestHeaderModifier holds the request header modifier configuration.
type RequestHeaderModifier struct {
// HeaderModifier holds the request/response header modifier configuration.
type HeaderModifier struct {
Set map[string]string `json:"set,omitempty"`
Add map[string]string `json:"add,omitempty"`
Remove []string `json:"remove,omitempty"`

View file

@ -158,6 +158,11 @@ func (in *Compress) DeepCopyInto(out *Compress) {
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Encodings != nil {
in, out := &in.Encodings, &out.Encodings
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
@ -501,6 +506,41 @@ func (in *HTTPConfiguration) DeepCopy() *HTTPConfiguration {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HeaderModifier) DeepCopyInto(out *HeaderModifier) {
*out = *in
if in.Set != nil {
in, out := &in.Set, &out.Set
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Add != nil {
in, out := &in.Add, &out.Add
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Remove != nil {
in, out := &in.Remove, &out.Remove
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HeaderModifier.
func (in *HeaderModifier) DeepCopy() *HeaderModifier {
if in == nil {
return nil
}
out := new(HeaderModifier)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Headers) DeepCopyInto(out *Headers) {
*out = *in
@ -861,7 +901,12 @@ func (in *Middleware) DeepCopyInto(out *Middleware) {
}
if in.RequestHeaderModifier != nil {
in, out := &in.RequestHeaderModifier, &out.RequestHeaderModifier
*out = new(RequestHeaderModifier)
*out = new(HeaderModifier)
(*in).DeepCopyInto(*out)
}
if in.ResponseHeaderModifier != nil {
in, out := &in.ResponseHeaderModifier, &out.ResponseHeaderModifier
*out = new(HeaderModifier)
(*in).DeepCopyInto(*out)
}
if in.RequestRedirect != nil {
@ -1082,41 +1127,6 @@ func (in *ReplacePathRegex) DeepCopy() *ReplacePathRegex {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RequestHeaderModifier) DeepCopyInto(out *RequestHeaderModifier) {
*out = *in
if in.Set != nil {
in, out := &in.Set, &out.Set
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Add != nil {
in, out := &in.Add, &out.Add
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Remove != nil {
in, out := &in.Remove, &out.Remove
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RequestHeaderModifier.
func (in *RequestHeaderModifier) DeepCopy() *RequestHeaderModifier {
if in == nil {
return nil
}
out := new(RequestHeaderModifier)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RequestRedirect) DeepCopyInto(out *RequestRedirect) {
*out = *in

View file

@ -137,6 +137,7 @@ func TestDecodeConfiguration(t *testing.T) {
"traefik.http.middlewares.Middleware17.stripprefix.prefixes": "foobar, fiibar",
"traefik.http.middlewares.Middleware17.stripprefix.forceslash": "true",
"traefik.http.middlewares.Middleware18.stripprefixregex.regex": "foobar, fiibar",
"traefik.http.middlewares.Middleware19.compress.encodings": "foobar, fiibar",
"traefik.http.middlewares.Middleware19.compress.minresponsebodybytes": "42",
"traefik.http.middlewares.Middleware20.plugin.tomato.aaa": "foo1",
"traefik.http.middlewares.Middleware20.plugin.tomato.bbb": "foo2",
@ -493,6 +494,10 @@ func TestDecodeConfiguration(t *testing.T) {
"Middleware19": {
Compress: &dynamic.Compress{
MinResponseBodyBytes: 42,
Encodings: []string{
"foobar",
"fiibar",
},
},
},
"Middleware2": {
@ -1009,6 +1014,10 @@ func TestEncodeConfiguration(t *testing.T) {
"Middleware19": {
Compress: &dynamic.Compress{
MinResponseBodyBytes: 42,
Encodings: []string{
"foobar",
"fiibar",
},
},
},
"Middleware2": {
@ -1377,6 +1386,7 @@ func TestEncodeConfiguration(t *testing.T) {
"traefik.HTTP.Middlewares.Middleware17.StripPrefix.Prefixes": "foobar, fiibar",
"traefik.HTTP.Middlewares.Middleware17.StripPrefix.ForceSlash": "true",
"traefik.HTTP.Middlewares.Middleware18.StripPrefixRegex.Regex": "foobar, fiibar",
"traefik.HTTP.Middlewares.Middleware19.Compress.Encodings": "foobar, fiibar",
"traefik.HTTP.Middlewares.Middleware19.Compress.MinResponseBodyBytes": "42",
"traefik.HTTP.Middlewares.Middleware20.Plugin.tomato.aaa": "foo1",
"traefik.HTTP.Middlewares.Middleware20.Plugin.tomato.bbb": "foo2",

View file

@ -22,13 +22,18 @@ type Encoding struct {
Weight *float64
}
func getCompressionType(acceptEncoding []string, defaultType string) string {
if defaultType == "" {
// Keeps the pre-existing default inside Traefik.
defaultType = brotliName
func getCompressionEncoding(acceptEncoding []string, defaultEncoding string, supportedEncodings []string) string {
if defaultEncoding == "" {
if slices.Contains(supportedEncodings, brotliName) {
// Keeps the pre-existing default inside Traefik if brotli is a supported encoding.
defaultEncoding = brotliName
} else if len(supportedEncodings) > 0 {
// Otherwise use the first supported encoding.
defaultEncoding = supportedEncodings[0]
}
}
encodings, hasWeight := parseAcceptEncoding(acceptEncoding)
encodings, hasWeight := parseAcceptEncoding(acceptEncoding, supportedEncodings)
if hasWeight {
if len(encodings) == 0 {
@ -46,26 +51,26 @@ func getCompressionType(acceptEncoding []string, defaultType string) string {
}
if encoding.Type == wildcardName {
return defaultType
return defaultEncoding
}
return encoding.Type
}
for _, dt := range []string{zstdName, brotliName, gzipName} {
for _, dt := range supportedEncodings {
if slices.ContainsFunc(encodings, func(e Encoding) bool { return e.Type == dt }) {
return dt
}
}
if slices.ContainsFunc(encodings, func(e Encoding) bool { return e.Type == wildcardName }) {
return defaultType
return defaultEncoding
}
return identityName
}
func parseAcceptEncoding(acceptEncoding []string) ([]Encoding, bool) {
func parseAcceptEncoding(acceptEncoding, supportedEncodings []string) ([]Encoding, bool) {
var encodings []Encoding
var hasWeight bool
@ -76,10 +81,9 @@ func parseAcceptEncoding(acceptEncoding []string) ([]Encoding, bool) {
continue
}
switch parsed[0] {
case zstdName, brotliName, gzipName, identityName, wildcardName:
// supported encoding
default:
if !slices.Contains(supportedEncodings, parsed[0]) &&
parsed[0] != identityName &&
parsed[0] != wildcardName {
continue
}

View file

@ -6,73 +6,86 @@ import (
"github.com/stretchr/testify/assert"
)
func Test_getCompressionType(t *testing.T) {
func Test_getCompressionEncoding(t *testing.T) {
testCases := []struct {
desc string
values []string
defaultType string
expected string
desc string
acceptEncoding []string
defaultEncoding string
supportedEncodings []string
expected string
}{
{
desc: "br > gzip (no weight)",
values: []string{"gzip, br"},
expected: brotliName,
desc: "br > gzip (no weight)",
acceptEncoding: []string{"gzip, br"},
expected: brotliName,
},
{
desc: "zstd > br > gzip (no weight)",
values: []string{"zstd, gzip, br"},
expected: zstdName,
desc: "zstd > br > gzip (no weight)",
acceptEncoding: []string{"zstd, gzip, br"},
expected: zstdName,
},
{
desc: "known compression type (no weight)",
values: []string{"compress, gzip"},
expected: gzipName,
desc: "known compression encoding (no weight)",
acceptEncoding: []string{"compress, gzip"},
expected: gzipName,
},
{
desc: "unknown compression type (no weight), no encoding",
values: []string{"compress, rar"},
expected: identityName,
desc: "unknown compression encoding (no weight), no encoding",
acceptEncoding: []string{"compress, rar"},
expected: identityName,
},
{
desc: "wildcard return the default compression type",
values: []string{"*"},
expected: brotliName,
desc: "wildcard return the default compression encoding",
acceptEncoding: []string{"*"},
expected: brotliName,
},
{
desc: "wildcard return the custom default compression type",
values: []string{"*"},
defaultType: "foo",
expected: "foo",
desc: "wildcard return the custom default compression encoding",
acceptEncoding: []string{"*"},
defaultEncoding: "foo",
expected: "foo",
},
{
desc: "follows weight",
values: []string{"br;q=0.8, gzip;q=1.0, *;q=0.1"},
expected: gzipName,
desc: "follows weight",
acceptEncoding: []string{"br;q=0.8, gzip;q=1.0, *;q=0.1"},
expected: gzipName,
},
{
desc: "ignore unknown compression type",
values: []string{"compress;q=1.0, gzip;q=0.5"},
expected: gzipName,
desc: "ignore unknown compression encoding",
acceptEncoding: []string{"compress;q=1.0, gzip;q=0.5"},
expected: gzipName,
},
{
desc: "fallback on non-zero compression type",
values: []string{"compress;q=1.0, gzip, identity;q=0"},
expected: gzipName,
desc: "fallback on non-zero compression encoding",
acceptEncoding: []string{"compress;q=1.0, gzip, identity;q=0"},
expected: gzipName,
},
{
desc: "not acceptable (identity)",
values: []string{"compress;q=1.0, identity;q=0"},
expected: notAcceptable,
desc: "not acceptable (identity)",
acceptEncoding: []string{"compress;q=1.0, identity;q=0"},
expected: notAcceptable,
},
{
desc: "not acceptable (wildcard)",
values: []string{"compress;q=1.0, *;q=0"},
expected: notAcceptable,
desc: "not acceptable (wildcard)",
acceptEncoding: []string{"compress;q=1.0, *;q=0"},
expected: notAcceptable,
},
{
desc: "non-zero is higher than 0",
values: []string{"gzip, *;q=0"},
expected: gzipName,
desc: "non-zero is higher than 0",
acceptEncoding: []string{"gzip, *;q=0"},
expected: gzipName,
},
{
desc: "zstd forbidden, brotli first",
acceptEncoding: []string{"zstd, gzip, br"},
supportedEncodings: []string{brotliName, gzipName},
expected: brotliName,
},
{
desc: "follows weight, ignores forbidden encoding",
acceptEncoding: []string{"br;q=0.8, gzip;q=1.0, *;q=0.1"},
supportedEncodings: []string{zstdName, brotliName},
expected: brotliName,
},
}
@ -80,19 +93,24 @@ func Test_getCompressionType(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
encodingType := getCompressionType(test.values, test.defaultType)
if test.supportedEncodings == nil {
test.supportedEncodings = defaultSupportedEncodings
}
assert.Equal(t, test.expected, encodingType)
encoding := getCompressionEncoding(test.acceptEncoding, test.defaultEncoding, test.supportedEncodings)
assert.Equal(t, test.expected, encoding)
})
}
}
func Test_parseAcceptEncoding(t *testing.T) {
testCases := []struct {
desc string
values []string
expected []Encoding
assertWeight assert.BoolAssertionFunc
desc string
values []string
supportedEncodings []string
expected []Encoding
assertWeight assert.BoolAssertionFunc
}{
{
desc: "weight",
@ -105,6 +123,17 @@ func Test_parseAcceptEncoding(t *testing.T) {
},
assertWeight: assert.True,
},
{
desc: "weight with supported encodings",
values: []string{"br;q=1.0, zstd;q=0.9, gzip;q=0.8, *;q=0.1"},
supportedEncodings: []string{brotliName, gzipName},
expected: []Encoding{
{Type: brotliName, Weight: ptr[float64](1)},
{Type: gzipName, Weight: ptr(0.8)},
{Type: wildcardName, Weight: ptr(0.1)},
},
assertWeight: assert.True,
},
{
desc: "mixed",
values: []string{"zstd,gzip, br;q=1.0, *;q=0"},
@ -116,6 +145,16 @@ func Test_parseAcceptEncoding(t *testing.T) {
},
assertWeight: assert.True,
},
{
desc: "mixed with supported encodings",
values: []string{"zstd,gzip, br;q=1.0, *;q=0"},
supportedEncodings: []string{zstdName},
expected: []Encoding{
{Type: zstdName},
{Type: wildcardName, Weight: ptr[float64](0)},
},
assertWeight: assert.True,
},
{
desc: "no weight",
values: []string{"zstd, gzip, br, *"},
@ -127,6 +166,16 @@ func Test_parseAcceptEncoding(t *testing.T) {
},
assertWeight: assert.False,
},
{
desc: "no weight with supported encodings",
values: []string{"zstd, gzip, br, *"},
supportedEncodings: []string{"gzip"},
expected: []Encoding{
{Type: gzipName},
{Type: wildcardName},
},
assertWeight: assert.False,
},
{
desc: "weight and identity",
values: []string{"gzip;q=1.0, identity; q=0.5, *;q=0"},
@ -137,13 +186,27 @@ func Test_parseAcceptEncoding(t *testing.T) {
},
assertWeight: assert.True,
},
{
desc: "weight and identity",
values: []string{"gzip;q=1.0, identity; q=0.5, *;q=0"},
supportedEncodings: []string{"br"},
expected: []Encoding{
{Type: identityName, Weight: ptr(0.5)},
{Type: wildcardName, Weight: ptr[float64](0)},
},
assertWeight: assert.True,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
aes, hasWeight := parseAcceptEncoding(test.values)
if test.supportedEncodings == nil {
test.supportedEncodings = defaultSupportedEncodings
}
aes, hasWeight := parseAcceptEncoding(test.values, test.supportedEncodings)
assert.Equal(t, test.expected, aes)
test.assertWeight(t, hasWeight)

View file

@ -16,9 +16,11 @@ import (
const typeName = "Compress"
// DefaultMinSize is the default minimum size (in bytes) required to enable compression.
// defaultMinSize is the default minimum size (in bytes) required to enable compression.
// See https://github.com/klauspost/compress/blob/9559b037e79ad673c71f6ef7c732c00949014cd2/gzhttp/compress.go#L47.
const DefaultMinSize = 1024
const defaultMinSize = 1024
var defaultSupportedEncodings = []string{zstdName, brotliName, gzipName}
// Compress is a middleware that allows to compress the response.
type compress struct {
@ -27,6 +29,7 @@ type compress struct {
excludes []string
includes []string
minSize int
encodings []string
defaultEncoding string
brotliHandler http.Handler
@ -62,17 +65,30 @@ func New(ctx context.Context, next http.Handler, conf dynamic.Compress, name str
includes = append(includes, mediaType)
}
minSize := DefaultMinSize
minSize := defaultMinSize
if conf.MinResponseBodyBytes > 0 {
minSize = conf.MinResponseBodyBytes
}
if len(conf.Encodings) == 0 {
return nil, errors.New("at least one encoding must be specified")
}
for _, encoding := range conf.Encodings {
if !slices.Contains(defaultSupportedEncodings, encoding) {
return nil, fmt.Errorf("unsupported encoding: %s", encoding)
}
}
if conf.DefaultEncoding != "" && !slices.Contains(conf.Encodings, conf.DefaultEncoding) {
return nil, fmt.Errorf("unsupported default encoding: %s", conf.DefaultEncoding)
}
c := &compress{
next: next,
name: name,
excludes: excludes,
includes: includes,
minSize: minSize,
encodings: conf.Encodings,
defaultEncoding: conf.DefaultEncoding,
}
@ -131,7 +147,7 @@ func (c *compress) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
return
}
c.chooseHandler(getCompressionType(acceptEncoding, c.defaultEncoding), rw, req)
c.chooseHandler(getCompressionEncoding(acceptEncoding, c.defaultEncoding, c.encodings), rw, req)
}
func (c *compress) chooseHandler(typ string, rw http.ResponseWriter, req *http.Request) {

View file

@ -102,7 +102,11 @@ func TestNegotiation(t *testing.T) {
next := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
_, _ = rw.Write(generateBytes(10))
})
handler, err := New(context.Background(), next, dynamic.Compress{MinResponseBodyBytes: 1}, "testing")
cfg := dynamic.Compress{
MinResponseBodyBytes: 1,
Encodings: defaultSupportedEncodings,
}
handler, err := New(context.Background(), next, cfg, "testing")
require.NoError(t, err)
rw := httptest.NewRecorder()
@ -123,7 +127,7 @@ func TestShouldCompressWhenNoContentEncodingHeader(t *testing.T) {
_, err := rw.Write(baseBody)
assert.NoError(t, err)
})
handler, err := New(context.Background(), next, dynamic.Compress{}, "testing")
handler, err := New(context.Background(), next, dynamic.Compress{Encodings: defaultSupportedEncodings}, "testing")
require.NoError(t, err)
rw := httptest.NewRecorder()
@ -153,7 +157,7 @@ func TestShouldNotCompressWhenContentEncodingHeader(t *testing.T) {
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
})
handler, err := New(context.Background(), next, dynamic.Compress{}, "testing")
handler, err := New(context.Background(), next, dynamic.Compress{Encodings: defaultSupportedEncodings}, "testing")
require.NoError(t, err)
rw := httptest.NewRecorder()
@ -175,7 +179,7 @@ func TestShouldNotCompressWhenNoAcceptEncodingHeader(t *testing.T) {
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
})
handler, err := New(context.Background(), next, dynamic.Compress{}, "testing")
handler, err := New(context.Background(), next, dynamic.Compress{Encodings: defaultSupportedEncodings}, "testing")
require.NoError(t, err)
rw := httptest.NewRecorder()
@ -202,7 +206,7 @@ func TestShouldNotCompressWhenIdentityAcceptEncodingHeader(t *testing.T) {
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
})
handler, err := New(context.Background(), next, dynamic.Compress{}, "testing")
handler, err := New(context.Background(), next, dynamic.Compress{Encodings: defaultSupportedEncodings}, "testing")
require.NoError(t, err)
rw := httptest.NewRecorder()
@ -229,7 +233,7 @@ func TestShouldNotCompressWhenEmptyAcceptEncodingHeader(t *testing.T) {
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
})
handler, err := New(context.Background(), next, dynamic.Compress{}, "testing")
handler, err := New(context.Background(), next, dynamic.Compress{Encodings: defaultSupportedEncodings}, "testing")
require.NoError(t, err)
rw := httptest.NewRecorder()
@ -251,7 +255,7 @@ func TestShouldNotCompressHeadRequest(t *testing.T) {
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
})
handler, err := New(context.Background(), next, dynamic.Compress{}, "testing")
handler, err := New(context.Background(), next, dynamic.Compress{Encodings: defaultSupportedEncodings}, "testing")
require.NoError(t, err)
rw := httptest.NewRecorder()
@ -274,6 +278,7 @@ func TestShouldNotCompressWhenSpecificContentType(t *testing.T) {
{
desc: "Exclude Request Content-Type",
conf: dynamic.Compress{
Encodings: defaultSupportedEncodings,
ExcludedContentTypes: []string{"text/event-stream"},
},
reqContentType: "text/event-stream",
@ -281,6 +286,7 @@ func TestShouldNotCompressWhenSpecificContentType(t *testing.T) {
{
desc: "Exclude Response Content-Type",
conf: dynamic.Compress{
Encodings: defaultSupportedEncodings,
ExcludedContentTypes: []string{"text/event-stream"},
},
respContentType: "text/event-stream",
@ -288,6 +294,7 @@ func TestShouldNotCompressWhenSpecificContentType(t *testing.T) {
{
desc: "Include Response Content-Type",
conf: dynamic.Compress{
Encodings: defaultSupportedEncodings,
IncludedContentTypes: []string{"text/plain"},
},
respContentType: "text/html",
@ -295,6 +302,7 @@ func TestShouldNotCompressWhenSpecificContentType(t *testing.T) {
{
desc: "Ignoring application/grpc with exclude option",
conf: dynamic.Compress{
Encodings: defaultSupportedEncodings,
ExcludedContentTypes: []string{"application/json"},
},
reqContentType: "application/grpc",
@ -302,13 +310,16 @@ func TestShouldNotCompressWhenSpecificContentType(t *testing.T) {
{
desc: "Ignoring application/grpc with include option",
conf: dynamic.Compress{
Encodings: defaultSupportedEncodings,
IncludedContentTypes: []string{"application/json"},
},
reqContentType: "application/grpc",
},
{
desc: "Ignoring application/grpc with no option",
conf: dynamic.Compress{},
desc: "Ignoring application/grpc with no option",
conf: dynamic.Compress{
Encodings: defaultSupportedEncodings,
},
reqContentType: "application/grpc",
},
}
@ -358,6 +369,7 @@ func TestShouldCompressWhenSpecificContentType(t *testing.T) {
{
desc: "Include Response Content-Type",
conf: dynamic.Compress{
Encodings: defaultSupportedEncodings,
IncludedContentTypes: []string{"text/html"},
},
respContentType: "text/html",
@ -429,7 +441,7 @@ func TestIntegrationShouldNotCompress(t *testing.T) {
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
compress, err := New(context.Background(), test.handler, dynamic.Compress{}, "testing")
compress, err := New(context.Background(), test.handler, dynamic.Compress{Encodings: defaultSupportedEncodings}, "testing")
require.NoError(t, err)
ts := httptest.NewServer(compress)
@ -464,7 +476,7 @@ func TestShouldWriteHeaderWhenFlush(t *testing.T) {
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
})
handler, err := New(context.Background(), next, dynamic.Compress{}, "testing")
handler, err := New(context.Background(), next, dynamic.Compress{Encodings: defaultSupportedEncodings}, "testing")
require.NoError(t, err)
ts := httptest.NewServer(handler)
@ -515,7 +527,7 @@ func TestIntegrationShouldCompress(t *testing.T) {
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
compress, err := New(context.Background(), test.handler, dynamic.Compress{}, "testing")
compress, err := New(context.Background(), test.handler, dynamic.Compress{Encodings: defaultSupportedEncodings}, "testing")
require.NoError(t, err)
ts := httptest.NewServer(compress)
@ -571,8 +583,11 @@ func TestMinResponseBodyBytes(t *testing.T) {
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
})
handler, err := New(context.Background(), next, dynamic.Compress{MinResponseBodyBytes: test.minResponseBodyBytes}, "testing")
cfg := dynamic.Compress{
MinResponseBodyBytes: test.minResponseBodyBytes,
Encodings: defaultSupportedEncodings,
}
handler, err := New(context.Background(), next, cfg, "testing")
require.NoError(t, err)
rw := httptest.NewRecorder()
@ -607,8 +622,11 @@ func Test1xxResponses(t *testing.T) {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
compress, err := New(context.Background(), next, dynamic.Compress{MinResponseBodyBytes: 1024}, "testing")
cfg := dynamic.Compress{
MinResponseBodyBytes: 1024,
Encodings: defaultSupportedEncodings,
}
compress, err := New(context.Background(), next, cfg, "testing")
require.NoError(t, err)
server := httptest.NewServer(compress)

View file

@ -9,7 +9,7 @@ import (
"go.opentelemetry.io/otel/trace"
)
const typeName = "RequestHeaderModifier"
const requestHeaderModifierTypeName = "RequestHeaderModifier"
// requestHeaderModifier is a middleware used to modify the headers of an HTTP request.
type requestHeaderModifier struct {
@ -22,8 +22,8 @@ type requestHeaderModifier struct {
}
// NewRequestHeaderModifier creates a new request header modifier middleware.
func NewRequestHeaderModifier(ctx context.Context, next http.Handler, config dynamic.RequestHeaderModifier, name string) http.Handler {
logger := middlewares.GetLogger(ctx, name, typeName)
func NewRequestHeaderModifier(ctx context.Context, next http.Handler, config dynamic.HeaderModifier, name string) http.Handler {
logger := middlewares.GetLogger(ctx, name, requestHeaderModifierTypeName)
logger.Debug().Msg("Creating middleware")
return &requestHeaderModifier{
@ -36,7 +36,7 @@ func NewRequestHeaderModifier(ctx context.Context, next http.Handler, config dyn
}
func (r *requestHeaderModifier) GetTracingInformation() (string, string, trace.SpanKind) {
return r.name, typeName, trace.SpanKindUnspecified
return r.name, requestHeaderModifierTypeName, trace.SpanKindUnspecified
}
func (r *requestHeaderModifier) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -14,25 +14,25 @@ import (
func TestRequestHeaderModifier(t *testing.T) {
testCases := []struct {
desc string
config dynamic.RequestHeaderModifier
config dynamic.HeaderModifier
requestHeaders http.Header
expectedHeaders http.Header
}{
{
desc: "no config",
config: dynamic.RequestHeaderModifier{},
config: dynamic.HeaderModifier{},
expectedHeaders: map[string][]string{},
},
{
desc: "set header",
config: dynamic.RequestHeaderModifier{
config: dynamic.HeaderModifier{
Set: map[string]string{"Foo": "Bar"},
},
expectedHeaders: map[string][]string{"Foo": {"Bar"}},
},
{
desc: "set header with existing headers",
config: dynamic.RequestHeaderModifier{
config: dynamic.HeaderModifier{
Set: map[string]string{"Foo": "Bar"},
},
requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foo"}},
@ -40,7 +40,7 @@ func TestRequestHeaderModifier(t *testing.T) {
},
{
desc: "set multiple headers with existing headers",
config: dynamic.RequestHeaderModifier{
config: dynamic.HeaderModifier{
Set: map[string]string{"Foo": "Bar", "Bar": "Foo"},
},
requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foobar"}},
@ -48,14 +48,14 @@ func TestRequestHeaderModifier(t *testing.T) {
},
{
desc: "add header",
config: dynamic.RequestHeaderModifier{
config: dynamic.HeaderModifier{
Add: map[string]string{"Foo": "Bar"},
},
expectedHeaders: map[string][]string{"Foo": {"Bar"}},
},
{
desc: "add header with existing headers",
config: dynamic.RequestHeaderModifier{
config: dynamic.HeaderModifier{
Add: map[string]string{"Foo": "Bar"},
},
requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foo"}},
@ -63,7 +63,7 @@ func TestRequestHeaderModifier(t *testing.T) {
},
{
desc: "add multiple headers with existing headers",
config: dynamic.RequestHeaderModifier{
config: dynamic.HeaderModifier{
Add: map[string]string{"Foo": "Bar", "Bar": "Foo"},
},
requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foobar"}},
@ -71,14 +71,14 @@ func TestRequestHeaderModifier(t *testing.T) {
},
{
desc: "remove header",
config: dynamic.RequestHeaderModifier{
config: dynamic.HeaderModifier{
Remove: []string{"Foo"},
},
expectedHeaders: map[string][]string{},
},
{
desc: "remove header with existing headers",
config: dynamic.RequestHeaderModifier{
config: dynamic.HeaderModifier{
Remove: []string{"Foo"},
},
requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foo"}},
@ -86,7 +86,7 @@ func TestRequestHeaderModifier(t *testing.T) {
},
{
desc: "remove multiple headers with existing headers",
config: dynamic.RequestHeaderModifier{
config: dynamic.HeaderModifier{
Remove: []string{"Foo", "Bar"},
},
requestHeaders: map[string][]string{"Foo": {"Bar"}, "Bar": {"Foo"}, "Baz": {"Bar"}},
@ -106,11 +106,11 @@ func TestRequestHeaderModifier(t *testing.T) {
handler := NewRequestHeaderModifier(context.Background(), next, test.config, "foo-request-header-modifier")
req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil)
if test.requestHeaders != nil {
req.Header = test.requestHeaders
for h, v := range test.requestHeaders {
req.Header[h] = v
}
resp := httptest.NewRecorder()
handler.ServeHTTP(resp, req)
assert.Equal(t, test.expectedHeaders, gotHeaders)

View file

@ -0,0 +1,60 @@
package headermodifier
import (
"context"
"net/http"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"go.opentelemetry.io/otel/trace"
)
const responseHeaderModifierTypeName = "ResponseHeaderModifier"
// requestHeaderModifier is a middleware used to modify the headers of an HTTP response.
type responseHeaderModifier struct {
next http.Handler
name string
set map[string]string
add map[string]string
remove []string
}
// NewResponseHeaderModifier creates a new response header modifier middleware.
func NewResponseHeaderModifier(ctx context.Context, next http.Handler, config dynamic.HeaderModifier, name string) http.Handler {
logger := middlewares.GetLogger(ctx, name, responseHeaderModifierTypeName)
logger.Debug().Msg("Creating middleware")
return &responseHeaderModifier{
next: next,
name: name,
set: config.Set,
add: config.Add,
remove: config.Remove,
}
}
func (r *responseHeaderModifier) GetTracingInformation() (string, string, trace.SpanKind) {
return r.name, responseHeaderModifierTypeName, trace.SpanKindUnspecified
}
func (r *responseHeaderModifier) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
r.next.ServeHTTP(middlewares.NewResponseModifier(rw, req, r.modifyResponseHeaders), req)
}
func (r *responseHeaderModifier) modifyResponseHeaders(res *http.Response) error {
for headerName, headerValue := range r.set {
res.Header.Set(headerName, headerValue)
}
for headerName, headerValue := range r.add {
res.Header.Add(headerName, headerValue)
}
for _, headerName := range r.remove {
res.Header.Del(headerName)
}
return nil
}

View file

@ -0,0 +1,121 @@
package headermodifier
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/testhelpers"
)
func TestResponseHeaderModifier(t *testing.T) {
testCases := []struct {
desc string
config dynamic.HeaderModifier
responseHeaders http.Header
expectedHeaders http.Header
}{
{
desc: "no config",
config: dynamic.HeaderModifier{},
expectedHeaders: map[string][]string{},
},
{
desc: "set header",
config: dynamic.HeaderModifier{
Set: map[string]string{"Foo": "Bar"},
},
expectedHeaders: map[string][]string{"Foo": {"Bar"}},
},
{
desc: "set header with existing headers",
config: dynamic.HeaderModifier{
Set: map[string]string{"Foo": "Bar"},
},
responseHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foo"}},
expectedHeaders: map[string][]string{"Foo": {"Bar"}, "Bar": {"Foo"}},
},
{
desc: "set multiple headers with existing headers",
config: dynamic.HeaderModifier{
Set: map[string]string{"Foo": "Bar", "Bar": "Foo"},
},
responseHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foobar"}},
expectedHeaders: map[string][]string{"Foo": {"Bar"}, "Bar": {"Foo"}},
},
{
desc: "add header",
config: dynamic.HeaderModifier{
Add: map[string]string{"Foo": "Bar"},
},
expectedHeaders: map[string][]string{"Foo": {"Bar"}},
},
{
desc: "add header with existing headers",
config: dynamic.HeaderModifier{
Add: map[string]string{"Foo": "Bar"},
},
responseHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foo"}},
expectedHeaders: map[string][]string{"Foo": {"Baz", "Bar"}, "Bar": {"Foo"}},
},
{
desc: "add multiple headers with existing headers",
config: dynamic.HeaderModifier{
Add: map[string]string{"Foo": "Bar", "Bar": "Foo"},
},
responseHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foobar"}},
expectedHeaders: map[string][]string{"Foo": {"Baz", "Bar"}, "Bar": {"Foobar", "Foo"}},
},
{
desc: "remove header",
config: dynamic.HeaderModifier{
Remove: []string{"Foo"},
},
expectedHeaders: map[string][]string{},
},
{
desc: "remove header with existing headers",
config: dynamic.HeaderModifier{
Remove: []string{"Foo"},
},
responseHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foo"}},
expectedHeaders: map[string][]string{"Bar": {"Foo"}},
},
{
desc: "remove multiple headers with existing headers",
config: dynamic.HeaderModifier{
Remove: []string{"Foo", "Bar"},
},
responseHeaders: map[string][]string{"Foo": {"Bar"}, "Bar": {"Foo"}, "Baz": {"Bar"}},
expectedHeaders: map[string][]string{"Baz": {"Bar"}},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var nextCallCount int
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
nextCallCount++
rw.WriteHeader(http.StatusOK)
})
handler := NewResponseHeaderModifier(context.Background(), next, test.config, "foo-response-header-modifier")
req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil)
resp := httptest.NewRecorder()
for k, v := range test.responseHeaders {
resp.Header()[k] = v
}
handler.ServeHTTP(resp, req)
assert.Equal(t, 1, nextCallCount)
assert.Equal(t, test.expectedHeaders, resp.Header())
})
}
}

View file

@ -13,9 +13,7 @@ import (
"go.opentelemetry.io/otel/trace"
)
const (
typeName = "RequestRedirect"
)
const typeName = "RequestRedirect"
type redirect struct {
name string

View file

@ -696,6 +696,8 @@ func getCertificateRenewDurations(certificatesDuration int) (time.Duration, time
return 4 * 30 * 24 * time.Hour, 7 * 24 * time.Hour // 4 month, 1 week
case certificatesDuration >= 3*30*24: // >= 90 days
return 30 * 24 * time.Hour, 24 * time.Hour // 30 days, 1 day
case certificatesDuration >= 30*24: // >= 30 days
return 10 * 24 * time.Hour, 12 * time.Hour // 10 days, 12 hours
case certificatesDuration >= 7*24: // >= 7 days
return 24 * time.Hour, time.Hour // 1 days, 1 hour
case certificatesDuration >= 24: // >= 1 days

View file

@ -613,6 +613,12 @@ func Test_getCertificateRenewDurations(t *testing.T) {
expectRenewPeriod: time.Hour * 24 * 30,
expectRenewInterval: time.Hour * 24,
},
{
desc: "30 Days certificates: 10 days renew period, 12 hour renew interval",
certificatesDurations: 24 * 30,
expectRenewPeriod: time.Hour * 24 * 10,
expectRenewInterval: time.Hour * 12,
},
{
desc: "7 Days certificates: 1 days renew period, 1 hour renew interval",
certificatesDurations: 24 * 7,

View file

@ -1,22 +1,22 @@
package docker
import (
docker "github.com/docker/docker/api/types"
dockertypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/go-connections/nat"
)
func containerJSON(ops ...func(*docker.ContainerJSON)) docker.ContainerJSON {
c := &docker.ContainerJSON{
ContainerJSONBase: &docker.ContainerJSONBase{
func containerJSON(ops ...func(*dockertypes.ContainerJSON)) dockertypes.ContainerJSON {
c := &dockertypes.ContainerJSON{
ContainerJSONBase: &dockertypes.ContainerJSONBase{
Name: "fake",
HostConfig: &container.HostConfig{},
},
Config: &container.Config{},
NetworkSettings: &docker.NetworkSettings{
NetworkSettingsBase: docker.NetworkSettingsBase{},
NetworkSettings: &dockertypes.NetworkSettings{
NetworkSettingsBase: dockertypes.NetworkSettingsBase{},
},
}
@ -27,34 +27,34 @@ func containerJSON(ops ...func(*docker.ContainerJSON)) docker.ContainerJSON {
return *c
}
func name(name string) func(*docker.ContainerJSON) {
return func(c *docker.ContainerJSON) {
func name(name string) func(*dockertypes.ContainerJSON) {
return func(c *dockertypes.ContainerJSON) {
c.ContainerJSONBase.Name = name
}
}
func networkMode(mode string) func(*docker.ContainerJSON) {
return func(c *docker.ContainerJSON) {
func networkMode(mode string) func(*dockertypes.ContainerJSON) {
return func(c *dockertypes.ContainerJSON) {
c.ContainerJSONBase.HostConfig.NetworkMode = container.NetworkMode(mode)
}
}
func nodeIP(ip string) func(*docker.ContainerJSON) {
return func(c *docker.ContainerJSON) {
c.ContainerJSONBase.Node = &docker.ContainerNode{
func nodeIP(ip string) func(*dockertypes.ContainerJSON) {
return func(c *dockertypes.ContainerJSON) {
c.ContainerJSONBase.Node = &dockertypes.ContainerNode{
IPAddress: ip,
}
}
}
func ports(portMap nat.PortMap) func(*docker.ContainerJSON) {
return func(c *docker.ContainerJSON) {
func ports(portMap nat.PortMap) func(*dockertypes.ContainerJSON) {
return func(c *dockertypes.ContainerJSON) {
c.NetworkSettings.NetworkSettingsBase.Ports = portMap
}
}
func withNetwork(name string, ops ...func(*network.EndpointSettings)) func(*docker.ContainerJSON) {
return func(c *docker.ContainerJSON) {
func withNetwork(name string, ops ...func(*network.EndpointSettings)) func(*dockertypes.ContainerJSON) {
return func(c *dockertypes.ContainerJSON) {
if c.NetworkSettings.Networks == nil {
c.NetworkSettings.Networks = map[string]*network.EndpointSettings{}
}

View file

@ -7,6 +7,7 @@ import (
"time"
docker "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/go-connections/nat"
"github.com/stretchr/testify/assert"
@ -3972,12 +3973,12 @@ func TestDynConfBuilder_getIPAddress_swarm(t *testing.T) {
testCases := []struct {
service swarm.Service
expected string
networks map[string]*docker.NetworkResource
networks map[string]*network.Summary
}{
{
service: swarmService(withEndpointSpec(modeDNSSR)),
expected: "",
networks: map[string]*docker.NetworkResource{},
networks: map[string]*network.Summary{},
},
{
service: swarmService(
@ -3985,7 +3986,7 @@ func TestDynConfBuilder_getIPAddress_swarm(t *testing.T) {
withEndpoint(virtualIP("1", "10.11.12.13/24")),
),
expected: "10.11.12.13",
networks: map[string]*docker.NetworkResource{
networks: map[string]*network.Summary{
"1": {
Name: "foo",
},
@ -4003,7 +4004,7 @@ func TestDynConfBuilder_getIPAddress_swarm(t *testing.T) {
),
),
expected: "10.11.12.99",
networks: map[string]*docker.NetworkResource{
networks: map[string]*network.Summary{
"1": {
Name: "foonet",
},

View file

@ -6,7 +6,7 @@ import (
"testing"
"time"
dockertypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/swarm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -18,7 +18,7 @@ func TestListTasks(t *testing.T) {
tasks []swarm.Task
isGlobalSVC bool
expectedTasks []string
networks map[string]*dockertypes.NetworkResource
networks map[string]*network.Summary
}{
{
service: swarmService(serviceName("container")),
@ -53,7 +53,7 @@ func TestListTasks(t *testing.T) {
"container.1",
"container.4",
},
networks: map[string]*dockertypes.NetworkResource{
networks: map[string]*network.Summary{
"1": {
Name: "foo",
},
@ -91,7 +91,7 @@ func TestSwarmProvider_listServices(t *testing.T) {
services []swarm.Service
tasks []swarm.Task
dockerVersion string
networks []dockertypes.NetworkResource
networks []network.Summary
expectedServices []string
}{
{
@ -117,7 +117,7 @@ func TestSwarmProvider_listServices(t *testing.T) {
withEndpointSpec(modeDNSSR)),
},
dockerVersion: "1.30",
networks: []dockertypes.NetworkResource{},
networks: []network.Summary{},
expectedServices: []string{},
},
{
@ -143,7 +143,7 @@ func TestSwarmProvider_listServices(t *testing.T) {
withEndpointSpec(modeDNSSR)),
},
dockerVersion: "1.30",
networks: []dockertypes.NetworkResource{
networks: []network.Summary{
{
Name: "network_name",
ID: "yk6l57rfwizjzxxzftn4amaot",
@ -198,7 +198,7 @@ func TestSwarmProvider_listServices(t *testing.T) {
),
},
dockerVersion: "1.30",
networks: []dockertypes.NetworkResource{
networks: []network.Summary{
{
Name: "network_name",
ID: "yk6l57rfwizjzxxzftn4amaot",
@ -255,7 +255,7 @@ func TestSwarmProvider_parseService_task(t *testing.T) {
tasks []swarm.Task
isGlobalSVC bool
expected map[string]dockerData
networks map[string]*dockertypes.NetworkResource
networks map[string]*network.Summary
}{
{
service: swarmService(serviceName("container")),
@ -276,7 +276,7 @@ func TestSwarmProvider_parseService_task(t *testing.T) {
Name: "container.3",
},
},
networks: map[string]*dockertypes.NetworkResource{
networks: map[string]*network.Summary{
"1": {
Name: "foo",
},
@ -301,7 +301,7 @@ func TestSwarmProvider_parseService_task(t *testing.T) {
Name: "container.id3",
},
},
networks: map[string]*dockertypes.NetworkResource{
networks: map[string]*network.Summary{
"1": {
Name: "foo",
},
@ -339,7 +339,7 @@ func TestSwarmProvider_parseService_task(t *testing.T) {
},
},
},
networks: map[string]*dockertypes.NetworkResource{
networks: map[string]*network.Summary{
"1": {
Name: "vlan",
},

View file

@ -2,6 +2,7 @@ package docker
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"text/template"
@ -101,6 +102,8 @@ func parseContainer(container dockertypes.ContainerJSON) dockerData {
type ClientConfig struct {
apiVersion string
Username string `description:"Username for Basic HTTP authentication." json:"username,omitempty" toml:"username,omitempty" yaml:"username,omitempty"`
Password string `description:"Password for Basic HTTP authentication." json:"password,omitempty" toml:"password,omitempty" yaml:"password,omitempty"`
Endpoint string `description:"Docker server endpoint. Can be a TCP or a Unix socket endpoint." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
TLS *types.ClientTLS `description:"Enable Docker TLS support." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"`
HTTPClientTimeout ptypes.Duration `description:"Client timeout for HTTP connections." json:"httpClientTimeout,omitempty" toml:"httpClientTimeout,omitempty" yaml:"httpClientTimeout,omitempty" export:"true"`
@ -115,6 +118,9 @@ func createClient(ctx context.Context, cfg ClientConfig) (*client.Client, error)
httpHeaders := map[string]string{
"User-Agent": "Traefik " + version.Version,
}
if cfg.Username != "" && cfg.Password != "" {
httpHeaders["Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(cfg.Username+":"+cfg.Password))
}
opts = append(opts,
client.WithHTTPHeaders(httpHeaders),

View file

@ -57,10 +57,12 @@ type clientWrapper struct {
csCrd traefikclientset.Interface
csKube kclientset.Interface
factoryClusterScope kinformers.SharedInformerFactory
factoriesCrd map[string]traefikinformers.SharedInformerFactory
factoriesKube map[string]kinformers.SharedInformerFactory
factoriesSecret map[string]kinformers.SharedInformerFactory
clusterScopeFactory kinformers.SharedInformerFactory
disableClusterScopeInformer bool
factoriesCrd map[string]traefikinformers.SharedInformerFactory
factoriesKube map[string]kinformers.SharedInformerFactory
factoriesSecret map[string]kinformers.SharedInformerFactory
labelSelector string
@ -237,18 +239,11 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
c.factoriesSecret[ns] = factorySecret
}
c.factoryClusterScope = kinformers.NewSharedInformerFactory(c.csKube, resyncPeriod)
_, err := c.factoryClusterScope.Core().V1().Nodes().Informer().AddEventHandler(eventHandler)
if err != nil {
return nil, err
}
for _, ns := range namespaces {
c.factoriesCrd[ns].Start(stopCh)
c.factoriesKube[ns].Start(stopCh)
c.factoriesSecret[ns].Start(stopCh)
}
c.factoryClusterScope.Start(stopCh)
for _, ns := range namespaces {
for t, ok := range c.factoriesCrd[ns].WaitForCacheSync(stopCh) {
@ -270,9 +265,19 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
}
}
for t, ok := range c.factoryClusterScope.WaitForCacheSync(stopCh) {
if !ok {
return nil, fmt.Errorf("timed out waiting for controller caches to sync %s", t.String())
if !c.disableClusterScopeInformer {
c.clusterScopeFactory = kinformers.NewSharedInformerFactory(c.csKube, resyncPeriod)
_, err := c.clusterScopeFactory.Core().V1().Nodes().Informer().AddEventHandler(eventHandler)
if err != nil {
return nil, err
}
c.clusterScopeFactory.Start(stopCh)
for t, ok := range c.clusterScopeFactory.WaitForCacheSync(stopCh) {
if !ok {
return nil, fmt.Errorf("timed out waiting for controller caches to sync %s", t.String())
}
}
}
@ -474,7 +479,7 @@ func (c *clientWrapper) GetSecret(namespace, name string) (*corev1.Secret, bool,
}
func (c *clientWrapper) GetNodes() ([]*corev1.Node, bool, error) {
nodes, err := c.factoryClusterScope.Core().V1().Nodes().Lister().List(labels.Everything())
nodes, err := c.clusterScopeFactory.Core().V1().Nodes().Lister().List(labels.Everything())
exist, err := translateNotFoundError(err)
return nodes, exist, err
}

View file

@ -50,17 +50,18 @@ const (
// Provider holds configurations of the provider.
type Provider struct {
Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
Token types.FileOrContent `description:"Kubernetes bearer token (not needed for in-cluster client). It accepts either a token value or a file path to the token." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty" loggable:"false"`
CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"`
Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"`
AllowCrossNamespace bool `description:"Allow cross namespace resource reference." json:"allowCrossNamespace,omitempty" toml:"allowCrossNamespace,omitempty" yaml:"allowCrossNamespace,omitempty" export:"true"`
AllowExternalNameServices bool `description:"Allow ExternalName services." json:"allowExternalNameServices,omitempty" toml:"allowExternalNameServices,omitempty" yaml:"allowExternalNameServices,omitempty" export:"true"`
LabelSelector string `description:"Kubernetes label selector to use." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"`
IngressClass string `description:"Value of kubernetes.io/ingress.class annotation to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,omitempty" export:"true"`
ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
AllowEmptyServices bool `description:"Allow the creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"`
NativeLBByDefault bool `description:"Defines whether to use Native Kubernetes load-balancing mode by default." json:"nativeLBByDefault,omitempty" toml:"nativeLBByDefault,omitempty" yaml:"nativeLBByDefault,omitempty" export:"true"`
Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
Token types.FileOrContent `description:"Kubernetes bearer token (not needed for in-cluster client). It accepts either a token value or a file path to the token." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty" loggable:"false"`
CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"`
Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"`
AllowCrossNamespace bool `description:"Allow cross namespace resource reference." json:"allowCrossNamespace,omitempty" toml:"allowCrossNamespace,omitempty" yaml:"allowCrossNamespace,omitempty" export:"true"`
AllowExternalNameServices bool `description:"Allow ExternalName services." json:"allowExternalNameServices,omitempty" toml:"allowExternalNameServices,omitempty" yaml:"allowExternalNameServices,omitempty" export:"true"`
LabelSelector string `description:"Kubernetes label selector to use." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"`
IngressClass string `description:"Value of kubernetes.io/ingress.class annotation to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,omitempty" export:"true"`
ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
AllowEmptyServices bool `description:"Allow the creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"`
NativeLBByDefault bool `description:"Defines whether to use Native Kubernetes load-balancing mode by default." json:"nativeLBByDefault,omitempty" toml:"nativeLBByDefault,omitempty" yaml:"nativeLBByDefault,omitempty" export:"true"`
DisableClusterScopeResources bool `description:"Disables the lookup of cluster scope resources (incompatible with IngressClasses and NodePortLB enabled services)." json:"disableClusterScopeResources,omitempty" toml:"disableClusterScopeResources,omitempty" yaml:"disableClusterScopeResources,omitempty" export:"true"`
lastConfiguration safe.Safe
@ -112,6 +113,7 @@ func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) {
}
client.labelSelector = p.LabelSelector
client.disableClusterScopeInformer = p.DisableClusterScopeResources
return client, nil
}
@ -302,7 +304,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
InFlightReq: middleware.Spec.InFlightReq,
Buffering: middleware.Spec.Buffering,
CircuitBreaker: circuitBreaker,
Compress: middleware.Spec.Compress,
Compress: createCompressMiddleware(middleware.Spec.Compress),
PassTLSClientCert: middleware.Spec.PassTLSClientCert,
Retry: retry,
ContentType: middleware.Spec.ContentType,
@ -656,14 +658,49 @@ func createCircuitBreakerMiddleware(circuitBreaker *traefikv1alpha1.CircuitBreak
return cb, nil
}
func createCompressMiddleware(compress *traefikv1alpha1.Compress) *dynamic.Compress {
if compress == nil {
return nil
}
c := &dynamic.Compress{}
c.SetDefaults()
if compress.ExcludedContentTypes != nil {
c.ExcludedContentTypes = compress.ExcludedContentTypes
}
if compress.IncludedContentTypes != nil {
c.IncludedContentTypes = compress.IncludedContentTypes
}
if compress.MinResponseBodyBytes != nil {
c.MinResponseBodyBytes = *compress.MinResponseBodyBytes
}
if compress.Encodings != nil {
c.Encodings = compress.Encodings
}
if compress.DefaultEncoding != nil {
c.DefaultEncoding = *compress.DefaultEncoding
}
return c
}
func createRateLimitMiddleware(rateLimit *traefikv1alpha1.RateLimit) (*dynamic.RateLimit, error) {
if rateLimit == nil {
return nil, nil
}
rl := &dynamic.RateLimit{Average: rateLimit.Average}
rl := &dynamic.RateLimit{}
rl.SetDefaults()
if rateLimit.Average != nil {
rl.Average = *rateLimit.Average
}
if rateLimit.Burst != nil {
rl.Burst = *rateLimit.Burst
}

View file

@ -51,11 +51,12 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
}
cb := configBuilder{
client: client,
allowCrossNamespace: p.AllowCrossNamespace,
allowExternalNameServices: p.AllowExternalNameServices,
allowEmptyServices: p.AllowEmptyServices,
NativeLBByDefault: p.NativeLBByDefault,
client: client,
allowCrossNamespace: p.AllowCrossNamespace,
allowExternalNameServices: p.AllowExternalNameServices,
allowEmptyServices: p.AllowEmptyServices,
nativeLBByDefault: p.NativeLBByDefault,
disableClusterScopeResources: p.DisableClusterScopeResources,
}
for _, route := range ingressRoute.Spec.Routes {
@ -199,11 +200,12 @@ func (p *Provider) makeMiddlewareKeys(ctx context.Context, ingRouteNamespace str
}
type configBuilder struct {
client Client
allowCrossNamespace bool
allowExternalNameServices bool
allowEmptyServices bool
NativeLBByDefault bool
client Client
allowCrossNamespace bool
allowExternalNameServices bool
allowEmptyServices bool
nativeLBByDefault bool
disableClusterScopeResources bool
}
// buildTraefikService creates the configuration for the traefik service defined in tService,
@ -428,7 +430,7 @@ func (c configBuilder) loadServers(parentNamespace string, svc traefikv1alpha1.L
return []dynamic.Server{{URL: fmt.Sprintf("%s://%s", protocol, hostPort)}}, nil
}
nativeLB := c.NativeLBByDefault
nativeLB := c.nativeLBByDefault
if svc.NativeLB != nil {
nativeLB = *svc.NativeLB
}
@ -448,6 +450,10 @@ func (c configBuilder) loadServers(parentNamespace string, svc traefikv1alpha1.L
var servers []dynamic.Server
if service.Spec.Type == corev1.ServiceTypeNodePort && svc.NodePortLB {
if c.disableClusterScopeResources {
return nil, errors.New("nodes lookup is disabled")
}
nodes, nodesExists, nodesErr := c.client.GetNodes()
if nodesErr != nil {
return nil, nodesErr

View file

@ -239,6 +239,10 @@ func (p *Provider) loadTCPServers(client Client, namespace string, svc traefikv1
var servers []dynamic.TCPServer
if service.Spec.Type == corev1.ServiceTypeNodePort && svc.NodePortLB {
if p.DisableClusterScopeResources {
return nil, errors.New("nodes lookup is disabled")
}
nodes, nodesExists, nodesErr := client.GetNodes()
if nodesErr != nil {
return nil, nodesErr

View file

@ -7446,10 +7446,10 @@ func TestNativeLB(t *testing.T) {
func TestNodePortLB(t *testing.T) {
testCases := []struct {
desc string
ingressClass string
paths []string
expected *dynamic.Configuration
desc string
paths []string
disableClusterScope bool
expected *dynamic.Configuration
}{
{
desc: "Empty",
@ -7594,6 +7594,90 @@ func TestNodePortLB(t *testing.T) {
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "HTTP with node port LB, cluster scope resources disabled",
paths: []string{"services.yml", "with_node_port_lb.yml"},
disableClusterScope: true,
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
ServersTransports: map[string]*dynamic.TCPServersTransport{},
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
ServersTransports: map[string]*dynamic.ServersTransport{},
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "TCP with native Service LB, cluster scope resources disabled",
paths: []string{"tcp/services.yml", "tcp/with_node_port_service_lb.yml"},
disableClusterScope: true,
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
ServersTransports: map[string]*dynamic.TCPServersTransport{},
Routers: map[string]*dynamic.TCPRouter{
"default-test.route-fdd3e9338e47a45efefc": {
EntryPoints: []string{"foo"},
Service: "default-test.route-fdd3e9338e47a45efefc",
Rule: "HostSNI(`foo.com`)",
},
},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
ServersTransports: map[string]*dynamic.ServersTransport{},
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "UDP with native Service LB, cluster scope resources disabled",
paths: []string{"udp/services.yml", "udp/with_node_port_service_lb.yml"},
disableClusterScope: true,
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{
"default-test.route-0": {
EntryPoints: []string{"foo"},
Service: "default-test.route-0",
},
},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
ServersTransports: map[string]*dynamic.TCPServersTransport{},
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
ServersTransports: map[string]*dynamic.ServersTransport{},
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
}
for _, test := range testCases {
@ -7617,7 +7701,9 @@ func TestNodePortLB(t *testing.T) {
<-eventCh
}
p := Provider{}
p := Provider{
DisableClusterScopeResources: test.disableClusterScope,
}
conf := p.loadConfigurationFromCRD(context.Background(), client)
assert.Equal(t, test.expected, conf)

View file

@ -123,6 +123,10 @@ func (p *Provider) loadUDPServers(client Client, namespace string, svc traefikv1
var servers []dynamic.UDPServer
if service.Spec.Type == corev1.ServiceTypeNodePort && svc.NodePortLB {
if p.DisableClusterScopeResources {
return nil, errors.New("nodes lookup is disabled")
}
nodes, nodesExists, nodesErr := client.GetNodes()
if nodesErr != nil {
return nil, nodesErr

View file

@ -46,7 +46,7 @@ type MiddlewareSpec struct {
InFlightReq *dynamic.InFlightReq `json:"inFlightReq,omitempty"`
Buffering *dynamic.Buffering `json:"buffering,omitempty"`
CircuitBreaker *CircuitBreaker `json:"circuitBreaker,omitempty"`
Compress *dynamic.Compress `json:"compress,omitempty"`
Compress *Compress `json:"compress,omitempty"`
PassTLSClientCert *dynamic.PassTLSClientCert `json:"passTLSClientCert,omitempty"`
Retry *Retry `json:"retry,omitempty"`
ContentType *dynamic.ContentType `json:"contentType,omitempty"`
@ -188,7 +188,7 @@ type RateLimit struct {
// It defaults to 0, which means no rate limiting.
// The rate is actually defined by dividing Average by Period. So for a rate below 1req/s,
// one needs to define a Period larger than a second.
Average int64 `json:"average,omitempty"`
Average *int64 `json:"average,omitempty"`
// Period, in combination with Average, defines the actual maximum rate, such as:
// r = Average / Period. It defaults to a second.
Period *intstr.IntOrString `json:"period,omitempty"`
@ -203,6 +203,26 @@ type RateLimit struct {
// +k8s:deepcopy-gen=true
// Compress holds the compress middleware configuration.
// This middleware compresses responses before sending them to the client, using gzip, brotli, or zstd compression.
// More info: https://doc.traefik.io/traefik/v3.1/middlewares/http/compress/
type Compress struct {
// ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests and responses before compressing.
// `application/grpc` is always excluded.
ExcludedContentTypes []string `json:"excludedContentTypes,omitempty"`
// IncludedContentTypes defines the list of content types to compare the Content-Type header of the responses before compressing.
IncludedContentTypes []string `json:"includedContentTypes,omitempty"`
// MinResponseBodyBytes defines the minimum amount of bytes a response body must have to be compressed.
// Default: 1024.
MinResponseBodyBytes *int `json:"minResponseBodyBytes,omitempty"`
// Encodings defines the list of supported compression algorithms.
Encodings []string `json:"encodings,omitempty"`
// DefaultEncoding specifies the default encoding if the `Accept-Encoding` header is not in the request or contains a wildcard (`*`).
DefaultEncoding *string `json:"defaultEncoding,omitempty"`
}
// +k8s:deepcopy-gen=true
// Retry holds the retry middleware configuration.
// This middleware reissues requests a given number of times to a backend server if that server does not reply.
// As soon as the server answers, the middleware stops retrying, regardless of the response status.

View file

@ -164,6 +164,47 @@ func (in *ClientTLS) DeepCopy() *ClientTLS {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Compress) DeepCopyInto(out *Compress) {
*out = *in
if in.ExcludedContentTypes != nil {
in, out := &in.ExcludedContentTypes, &out.ExcludedContentTypes
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.IncludedContentTypes != nil {
in, out := &in.IncludedContentTypes, &out.IncludedContentTypes
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.MinResponseBodyBytes != nil {
in, out := &in.MinResponseBodyBytes, &out.MinResponseBodyBytes
*out = new(int)
**out = **in
}
if in.Encodings != nil {
in, out := &in.Encodings, &out.Encodings
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.DefaultEncoding != nil {
in, out := &in.DefaultEncoding, &out.DefaultEncoding
*out = new(string)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Compress.
func (in *Compress) DeepCopy() *Compress {
if in == nil {
return nil
}
out := new(Compress)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DigestAuth) DeepCopyInto(out *DigestAuth) {
*out = *in
@ -776,7 +817,7 @@ func (in *MiddlewareSpec) DeepCopyInto(out *MiddlewareSpec) {
}
if in.Compress != nil {
in, out := &in.Compress, &out.Compress
*out = new(dynamic.Compress)
*out = new(Compress)
(*in).DeepCopyInto(*out)
}
if in.PassTLSClientCert != nil {
@ -975,6 +1016,11 @@ func (in *ObjectReference) DeepCopy() *ObjectReference {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RateLimit) DeepCopyInto(out *RateLimit) {
*out = *in
if in.Average != nil {
in, out := &in.Average, &out.Average
*out = new(int64)
**out = **in
}
if in.Period != nil {
in, out := &in.Period, &out.Period
*out = new(intstr.IntOrString)

View file

@ -684,32 +684,9 @@ func translateNotFoundError(err error) (bool, error) {
}
func gatewayStatusEqual(statusA, statusB gatev1.GatewayStatus) bool {
if len(statusA.Listeners) != len(statusB.Listeners) {
return false
}
if !conditionsEqual(statusA.Conditions, statusB.Conditions) {
return false
}
listenerMatches := 0
for _, newListener := range statusB.Listeners {
for _, oldListener := range statusA.Listeners {
if newListener.Name == oldListener.Name {
if !conditionsEqual(newListener.Conditions, oldListener.Conditions) {
return false
}
if newListener.AttachedRoutes != oldListener.AttachedRoutes {
return false
}
listenerMatches++
}
}
}
return listenerMatches == len(statusA.Listeners)
return reflect.DeepEqual(statusA.Addresses, statusB.Addresses) &&
listenersStatusEqual(statusA.Listeners, statusB.Listeners) &&
conditionsEqual(statusA.Conditions, statusB.Conditions)
}
func routeParentStatusesEqual(routeParentStatusesA, routeParentStatusesB []gatev1alpha2.RouteParentStatus) bool {
@ -737,15 +714,17 @@ func routeParentStatusesEqual(routeParentStatusesA, routeParentStatusesB []gatev
}
func routeParentStatusEqual(sA, sB gatev1alpha2.RouteParentStatus) bool {
if !reflect.DeepEqual(sA.ParentRef, sB.ParentRef) {
return false
}
return sA.ControllerName == sB.ControllerName &&
reflect.DeepEqual(sA.ParentRef, sB.ParentRef) &&
conditionsEqual(sA.Conditions, sB.Conditions)
}
if sA.ControllerName != sB.ControllerName {
return false
}
return conditionsEqual(sA.Conditions, sB.Conditions)
func listenersStatusEqual(listenerA, listenerB []gatev1.ListenerStatus) bool {
return slices.EqualFunc(listenerA, listenerB, func(lA gatev1.ListenerStatus, lB gatev1.ListenerStatus) bool {
return lA.Name == lB.Name &&
lA.AttachedRoutes == lB.AttachedRoutes &&
conditionsEqual(lA.Conditions, lB.Conditions)
})
}
func conditionsEqual(conditionsA, conditionsB []metav1.Condition) bool {

View file

@ -0,0 +1,62 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: http
protocol: HTTP
port: 80
allowedRoutes:
kinds:
- kind: HTTPRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: http-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- "example.org"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami
port: 80
weight: 1
kind: Service
group: ""
filters:
- type: ResponseHeaderModifier
responseHeaderModifier:
set:
- name: X-Foo
value: Bar
add:
- name: X-Bar
value: Foo
remove:
- X-Baz

View file

@ -316,6 +316,9 @@ func (p *Provider) loadMiddlewares(conf *dynamic.Configuration, namespace, route
case gatev1.HTTPRouteFilterRequestHeaderModifier:
middlewares[name] = createRequestHeaderModifier(filter.RequestHeaderModifier)
case gatev1.HTTPRouteFilterResponseHeaderModifier:
middlewares[name] = createResponseHeaderModifier(filter.ResponseHeaderModifier)
case gatev1.HTTPRouteFilterExtensionRef:
name, middleware, err := p.loadHTTPRouteFilterExtensionRef(namespace, filter.ExtensionRef)
if err != nil {
@ -599,7 +602,29 @@ func createRequestHeaderModifier(filter *gatev1.HTTPHeaderFilter) *dynamic.Middl
}
return &dynamic.Middleware{
RequestHeaderModifier: &dynamic.RequestHeaderModifier{
RequestHeaderModifier: &dynamic.HeaderModifier{
Set: sets,
Add: adds,
Remove: filter.Remove,
},
}
}
// createResponseHeaderModifier does not enforce/check the configuration,
// as the spec indicates that either the webhook or CEL (since v1.0 GA Release) should enforce that.
func createResponseHeaderModifier(filter *gatev1.HTTPHeaderFilter) *dynamic.Middleware {
sets := map[string]string{}
for _, header := range filter.Set {
sets[string(header.Name)] = header.Value
}
adds := map[string]string{}
for _, header := range filter.Add {
adds[string(header.Name)] = header.Value
}
return &dynamic.Middleware{
ResponseHeaderModifier: &dynamic.HeaderModifier{
Set: sets,
Add: adds,
Remove: filter.Remove,

View file

@ -1722,7 +1722,77 @@ func TestLoadHTTPRoutes(t *testing.T) {
},
Middlewares: map[string]*dynamic.Middleware{
"default-http-app-1-my-gateway-web-0-364ce6ec04c3d49b19c4-requestheadermodifier-0": {
RequestHeaderModifier: &dynamic.RequestHeaderModifier{
RequestHeaderModifier: &dynamic.HeaderModifier{
Set: map[string]string{"X-Foo": "Bar"},
Add: map[string]string{"X-Bar": "Foo"},
Remove: []string{"X-Baz"},
},
},
},
Services: map[string]*dynamic.Service{
"default-http-app-1-my-gateway-web-0-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "default-whoami-80",
Weight: ptr.To(1),
},
},
},
},
"default-whoami-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Simple HTTPRoute, response header modifier",
paths: []string{"services.yml", "httproute/filter_response_header_modifier.yml"},
entryPoints: map[string]Entrypoint{"web": {
Address: ":80",
}},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-http-app-1-my-gateway-web-0-364ce6ec04c3d49b19c4": {
EntryPoints: []string{"web"},
Service: "default-http-app-1-my-gateway-web-0-wrr",
Rule: "Host(`example.org`) && PathPrefix(`/`)",
Priority: 13,
RuleSyntax: "v3",
Middlewares: []string{"default-http-app-1-my-gateway-web-0-364ce6ec04c3d49b19c4-responseheadermodifier-0"},
},
},
Middlewares: map[string]*dynamic.Middleware{
"default-http-app-1-my-gateway-web-0-364ce6ec04c3d49b19c4-responseheadermodifier-0": {
ResponseHeaderModifier: &dynamic.HeaderModifier{
Set: map[string]string{"X-Foo": "Bar"},
Add: map[string]string{"X-Bar": "Foo"},
Remove: []string{"X-Baz"},

View file

@ -49,14 +49,14 @@ type Client interface {
type clientWrapper struct {
clientset kclientset.Interface
factoryClusterScope kinformers.SharedInformerFactory
clusterScopeFactory kinformers.SharedInformerFactory
factoriesKube map[string]kinformers.SharedInformerFactory
factoriesSecret map[string]kinformers.SharedInformerFactory
factoriesIngress map[string]kinformers.SharedInformerFactory
clusterFactory kinformers.SharedInformerFactory
ingressLabelSelector string
isNamespaceAll bool
disableIngressClassInformer bool
disableIngressClassInformer bool // Deprecated.
disableClusterScopeInformer bool
watchedNamespaces []string
serverVersion *version.Version
}
@ -201,58 +201,52 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
c.factoriesSecret[ns] = factorySecret
}
c.factoryClusterScope = kinformers.NewSharedInformerFactory(c.clientset, resyncPeriod)
_, err = c.factoryClusterScope.Core().V1().Nodes().Informer().AddEventHandler(eventHandler)
if err != nil {
return nil, err
}
for _, ns := range namespaces {
c.factoriesIngress[ns].Start(stopCh)
c.factoriesKube[ns].Start(stopCh)
c.factoriesSecret[ns].Start(stopCh)
}
c.factoryClusterScope.Start(stopCh)
for _, ns := range namespaces {
for typ, ok := range c.factoriesIngress[ns].WaitForCacheSync(stopCh) {
for t, ok := range c.factoriesIngress[ns].WaitForCacheSync(stopCh) {
if !ok {
return nil, fmt.Errorf("timed out waiting for controller caches to sync %s in namespace %q", typ, ns)
return nil, fmt.Errorf("timed out waiting for controller caches to sync %s in namespace %q", t.String(), ns)
}
}
for typ, ok := range c.factoriesKube[ns].WaitForCacheSync(stopCh) {
for t, ok := range c.factoriesKube[ns].WaitForCacheSync(stopCh) {
if !ok {
return nil, fmt.Errorf("timed out waiting for controller caches to sync %s in namespace %q", typ, ns)
return nil, fmt.Errorf("timed out waiting for controller caches to sync %s in namespace %q", t.String(), ns)
}
}
for typ, ok := range c.factoriesSecret[ns].WaitForCacheSync(stopCh) {
for t, ok := range c.factoriesSecret[ns].WaitForCacheSync(stopCh) {
if !ok {
return nil, fmt.Errorf("timed out waiting for controller caches to sync %s in namespace %q", typ, ns)
return nil, fmt.Errorf("timed out waiting for controller caches to sync %s in namespace %q", t.String(), ns)
}
}
}
for t, ok := range c.factoryClusterScope.WaitForCacheSync(stopCh) {
if !ok {
return nil, fmt.Errorf("timed out waiting for controller caches to sync %s", t.String())
}
}
if !c.disableIngressClassInformer || !c.disableClusterScopeInformer {
c.clusterScopeFactory = kinformers.NewSharedInformerFactory(c.clientset, resyncPeriod)
if !c.disableIngressClassInformer {
c.clusterFactory = kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod)
_, err = c.clusterFactory.Networking().V1().IngressClasses().Informer().AddEventHandler(eventHandler)
_, err = c.clusterScopeFactory.Networking().V1().IngressClasses().Informer().AddEventHandler(eventHandler)
if err != nil {
return nil, err
}
c.clusterFactory.Start(stopCh)
if !c.disableClusterScopeInformer {
_, err = c.clusterScopeFactory.Core().V1().Nodes().Informer().AddEventHandler(eventHandler)
if err != nil {
return nil, err
}
}
for typ, ok := range c.clusterFactory.WaitForCacheSync(stopCh) {
c.clusterScopeFactory.Start(stopCh)
for t, ok := range c.clusterScopeFactory.WaitForCacheSync(stopCh) {
if !ok {
return nil, fmt.Errorf("timed out waiting for controller caches to sync %s", typ)
return nil, fmt.Errorf("timed out waiting for controller caches to sync %s", t.String())
}
}
}
@ -370,18 +364,18 @@ func (c *clientWrapper) GetSecret(namespace, name string) (*corev1.Secret, bool,
}
func (c *clientWrapper) GetNodes() ([]*corev1.Node, bool, error) {
nodes, err := c.factoryClusterScope.Core().V1().Nodes().Lister().List(labels.Everything())
nodes, err := c.clusterScopeFactory.Core().V1().Nodes().Lister().List(labels.Everything())
exist, err := translateNotFoundError(err)
return nodes, exist, err
}
func (c *clientWrapper) GetIngressClasses() ([]*netv1.IngressClass, error) {
if c.clusterFactory == nil {
if c.clusterScopeFactory == nil {
return nil, errors.New("cluster factory not loaded")
}
var ics []*netv1.IngressClass
ingressClasses, err := c.clusterFactory.Networking().V1().IngressClasses().Lister().List(labels.Everything())
ingressClasses, err := c.clusterScopeFactory.Networking().V1().IngressClasses().Lister().List(labels.Everything())
if err != nil {
return nil, err
}

View file

@ -0,0 +1,45 @@
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: ""
namespace: testing
spec:
rules:
- host: traefik.tchouk
http:
paths:
- path: /bar
backend:
service:
name: service1
port:
number: 8080
pathType: Prefix
---
kind: Service
apiVersion: v1
metadata:
name: service1
namespace: testing
annotations:
traefik.ingress.kubernetes.io/service.nodeportlb: "true"
spec:
ports:
- port: 8080
nodePort: 32456
clusterIP: 10.0.0.1
type: NodePort
externalName: traefik.wtf
---
kind: Node
apiVersion: v1
metadata:
name: traefik-node
status:
addresses:
- type: InternalIP
address: 172.16.4.4

View file

@ -51,8 +51,10 @@ type Provider struct {
ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
AllowEmptyServices bool `description:"Allow creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"`
AllowExternalNameServices bool `description:"Allow ExternalName services." json:"allowExternalNameServices,omitempty" toml:"allowExternalNameServices,omitempty" yaml:"allowExternalNameServices,omitempty" export:"true"`
DisableIngressClassLookup bool `description:"Disables the lookup of IngressClasses." json:"disableIngressClassLookup,omitempty" toml:"disableIngressClassLookup,omitempty" yaml:"disableIngressClassLookup,omitempty" export:"true"`
NativeLBByDefault bool `description:"Defines whether to use Native Kubernetes load-balancing mode by default." json:"nativeLBByDefault,omitempty" toml:"nativeLBByDefault,omitempty" yaml:"nativeLBByDefault,omitempty" export:"true"`
// Deprecated: please use DisableClusterScopeResources.
DisableIngressClassLookup bool `description:"Disables the lookup of IngressClasses (Deprecated, please use DisableClusterScopeResources)." json:"disableIngressClassLookup,omitempty" toml:"disableIngressClassLookup,omitempty" yaml:"disableIngressClassLookup,omitempty" export:"true"`
DisableClusterScopeResources bool `description:"Disables the lookup of cluster scope resources (incompatible with IngressClasses and NodePortLB enabled services)." json:"disableClusterScopeResources,omitempty" toml:"disableClusterScopeResources,omitempty" yaml:"disableClusterScopeResources,omitempty" export:"true"`
NativeLBByDefault bool `description:"Defines whether to use Native Kubernetes load-balancing mode by default." json:"nativeLBByDefault,omitempty" toml:"nativeLBByDefault,omitempty" yaml:"nativeLBByDefault,omitempty" export:"true"`
lastConfiguration safe.Safe
@ -114,7 +116,8 @@ func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) {
}
cl.ingressLabelSelector = p.LabelSelector
cl.disableIngressClassInformer = p.DisableIngressClassLookup
cl.disableIngressClassInformer = p.DisableIngressClassLookup || p.DisableClusterScopeResources
cl.disableClusterScopeInformer = p.DisableClusterScopeResources
return cl, nil
}
@ -212,7 +215,6 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
},
TCP: &dynamic.TCPConfiguration{},
}
var ingressClasses []*netv1.IngressClass
@ -591,6 +593,10 @@ func (p *Provider) loadService(client Client, namespace string, backend netv1.In
}
if svcConfig.Service.NodePortLB && service.Spec.Type == corev1.ServiceTypeNodePort {
if p.DisableClusterScopeResources {
return nil, errors.New("nodes lookup is disabled")
}
nodes, nodesExists, nodesErr := client.GetNodes()
if nodesErr != nil {
return nil, nodesErr

View file

@ -35,7 +35,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Empty ingresses",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
@ -46,7 +45,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress one rule host only",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
@ -57,7 +55,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with a basic rule on one path",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -90,7 +87,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with annotations",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -148,7 +144,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with two different rules with one path",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -185,7 +180,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with conflicting routers on host",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -222,7 +216,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with conflicting routers on path",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -259,7 +252,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress one rule with two paths",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -296,7 +288,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress one rule with one path and one host",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -329,7 +320,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with one host without path",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -359,7 +349,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress one rule with one host and two paths",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -396,7 +385,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress Two rules with one host and one path",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -433,7 +421,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with two services",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -487,7 +474,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
desc: "Ingress with one service without endpoints subset",
allowEmptyServices: true,
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -512,7 +498,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with one service without endpoint",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{},
@ -523,7 +508,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Single Service Ingress (without any rules)",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -558,7 +542,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with port value in backend and no pod replica",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -591,7 +574,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with port name in backend and no pod replica",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -624,7 +606,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with port name in backend and 2 pod replica",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -657,7 +638,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with two paths using same service and different port name",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -710,7 +690,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with a named port matching subset of service pods",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -743,7 +722,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "2 ingresses in different namespace with same service name",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -796,7 +774,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with unknown service port name",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{},
@ -807,7 +784,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with unknown service port",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{},
@ -818,7 +794,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with port invalid for one service",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -848,7 +823,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "TLS support",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -889,7 +863,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with a basic rule on one path with https (port == 443)",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -922,7 +895,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with a basic rule on one path with https (portname == https)",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -955,7 +927,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with a basic rule on one path with https (portname starts with https)",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
@ -989,7 +960,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Double Single Service Ingress",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -1024,7 +994,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with default traefik ingressClass",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -1054,7 +1023,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress without provider traefik ingressClass and unknown annotation",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{},
@ -1066,7 +1034,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
desc: "Ingress with non matching provider traefik ingressClass and annotation",
ingressClass: "tchouk",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{},
@ -1078,7 +1045,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
desc: "Ingress with ingressClass without annotation",
ingressClass: "tchouk",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{},
@ -1090,7 +1056,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
desc: "Ingress with ingressClass without annotation",
ingressClass: "toto",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{},
@ -1101,7 +1066,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with wildcard host",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -1133,7 +1097,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with multiple ingressClasses",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -1168,7 +1131,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
desc: "Ingress with ingressClasses filter",
ingressClass: "traefik-lb2",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -1198,7 +1160,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with prefix pathType",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -1228,7 +1189,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with empty pathType",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -1258,7 +1218,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with exact pathType",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -1288,7 +1247,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with implementationSpecific pathType",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -1318,7 +1276,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with ingress annotation",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -1351,7 +1308,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
desc: "Ingress with ingress annotation",
disableIngressClassLookup: true,
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -1381,7 +1337,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with ingressClass",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -1414,7 +1369,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
desc: "Ingress with ingressClass",
disableIngressClassLookup: true,
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{},
@ -1425,7 +1379,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with named port",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -1455,7 +1408,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with missing ingressClass",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{},
@ -1466,7 +1418,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
{
desc: "Ingress with defaultbackend",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -1524,7 +1475,6 @@ func TestLoadConfigurationFromIngressesWithExternalNameServices(t *testing.T) {
{
desc: "Ingress with service with externalName",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{},
@ -1536,7 +1486,6 @@ func TestLoadConfigurationFromIngressesWithExternalNameServices(t *testing.T) {
desc: "Ingress with service with externalName enabled",
allowExternalNameServices: true,
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -1566,7 +1515,6 @@ func TestLoadConfigurationFromIngressesWithExternalNameServices(t *testing.T) {
{
desc: "Ingress with IPv6 endpoints",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -1597,7 +1545,6 @@ func TestLoadConfigurationFromIngressesWithExternalNameServices(t *testing.T) {
desc: "Ingress with IPv6 endpoints externalname enabled",
allowExternalNameServices: true,
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -1650,7 +1597,6 @@ func TestLoadConfigurationFromIngressesWithNativeLB(t *testing.T) {
{
desc: "Ingress with native service lb",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -1693,14 +1639,13 @@ func TestLoadConfigurationFromIngressesWithNativeLB(t *testing.T) {
func TestLoadConfigurationFromIngressesWithNodePortLB(t *testing.T) {
testCases := []struct {
desc string
ingressClass string
expected *dynamic.Configuration
desc string
clusterScopeDisabled bool
expected *dynamic.Configuration
}{
{
desc: "Ingress with node port lb",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -1725,6 +1670,17 @@ func TestLoadConfigurationFromIngressesWithNodePortLB(t *testing.T) {
},
},
},
{
desc: "Ingress with node port lb cluster scope disabled",
clusterScopeDisabled: true,
expected: &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{},
Services: map[string]*dynamic.Service{},
},
},
},
}
for _, test := range testCases {
@ -1733,7 +1689,7 @@ func TestLoadConfigurationFromIngressesWithNodePortLB(t *testing.T) {
clientMock := newClientMock(generateTestFilename(test.desc))
p := Provider{IngressClass: test.ingressClass}
p := Provider{DisableClusterScopeResources: test.clusterScopeDisabled}
conf := p.loadConfigurationFromIngresses(context.Background(), clientMock)
assert.Equal(t, test.expected, conf)
@ -1927,7 +1883,6 @@ func TestLoadConfigurationFromIngressesWithNativeLBByDefault(t *testing.T) {
{
desc: "Ingress with native service lb",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
@ -1955,7 +1910,6 @@ func TestLoadConfigurationFromIngressesWithNativeLBByDefault(t *testing.T) {
{
desc: "Ingress with native lb by default",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{

View file

@ -207,6 +207,7 @@ func Test_buildConfiguration(t *testing.T) {
"traefik/http/middlewares/Middleware02/buffering/retryExpression": "foobar",
"traefik/http/middlewares/Middleware02/buffering/maxRequestBodyBytes": "42",
"traefik/http/middlewares/Middleware02/buffering/memRequestBodyBytes": "42",
"traefik/http/middlewares/Middleware05/compress/encodings": "foobar, foobar",
"traefik/http/middlewares/Middleware05/compress/minResponseBodyBytes": "42",
"traefik/http/middlewares/Middleware18/retry/attempts": "42",
"traefik/http/middlewares/Middleware19/stripPrefix/prefixes/0": "foobar",
@ -412,6 +413,10 @@ func Test_buildConfiguration(t *testing.T) {
"Middleware05": {
Compress: &dynamic.Compress{
MinResponseBodyBytes: 42,
Encodings: []string{
"foobar",
"foobar",
},
},
},
"Middleware08": {

View file

@ -397,6 +397,15 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (
}
}
if config.ResponseHeaderModifier != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return headermodifier.NewResponseHeaderModifier(ctx, next, *config.ResponseHeaderModifier, middlewareName), nil
}
}
if config.RequestRedirect != nil {
if middleware != nil {
return nil, badConf

View file

@ -4,11 +4,11 @@ RepositoryName = "traefik"
OutputType = "file"
FileName = "traefik_changelog.md"
# example new bugfix v3.1.1
# example new bugfix v3.1.2
CurrentRef = "v3.1"
PreviousRef = "v3.1.0"
PreviousRef = "v3.1.1"
BaseBranch = "v3.1"
FutureCurrentRefName = "v3.1.1"
FutureCurrentRefName = "v3.1.2"
ThresholdPreviousRef = 10
ThresholdCurrentRef = 10

View file

@ -7,7 +7,7 @@
"private": true,
"scripts": {
"transfer": "node dev/scripts/transfer.js",
"lint": "eslint --ext .js,.vue src",
"lint": "eslint src/**/*.{js,vue}",
"dev": "APP_ENV=development quasar dev",
"build-quasar": "quasar build",
"build-staging": "NODE_ENV=production APP_ENV=development yarn build-quasar",
@ -18,8 +18,8 @@
"test:unit:ci": "vitest run"
},
"dependencies": {
"@quasar/extras": "^1.16.9",
"axios": "^1.6.7",
"@quasar/extras": "^1.16.12",
"axios": "^1.7.2",
"bowser": "^2.11.0",
"chart.js": "^4.4.1",
"core-js": "^3.35.1",
@ -27,7 +27,7 @@
"iframe-resizer": "^4.3.9",
"lodash.isequal": "4.5.0",
"moment": "^2.30.1",
"quasar": "^2.14.3",
"quasar": "^2.16.6",
"query-string": "^8.1.0",
"vh-check": "^2.0.5",
"vue": "^3.0.0",
@ -39,8 +39,8 @@
"devDependencies": {
"@babel/core": "^7.23.9",
"@babel/eslint-parser": "^7.23.10",
"@quasar/app-vite": "^1.4.3",
"@quasar/babel-preset-app": "^2.0.2",
"@quasar/app-vite": "^2.0.0-beta.15",
"@quasar/babel-preset-app": "^2.0.3",
"@quasar/quasar-app-extension-testing-unit-vitest": "^1.0.0",
"@vue/test-utils": "^2.4.4",
"autoprefixer": "^10.4.2",
@ -51,7 +51,7 @@
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-vue": "^9.0.0",
"postcss": "^8.4.14",
"vitest": "^1.3.1"
"vitest": "^1.6.0"
},
"engines": {
"node": "^20 || ^18 || ^16",

File diff suppressed because it is too large Load diff