Compare commits

...

31 commits

Author SHA1 Message Date
5482418b84
Merge branch 'master' of github.com:traefik/traefik
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-05-30 08:20:10 +05:30
Kevin Pollet
e9bd2b45ac
Fix route attachments to gateways
Co-authored-by: Romain <rtribotte@users.noreply.github.com>
2024-05-28 14:30:04 +02:00
Dimitris Mavrommatis
6e61fe0de1
Support RegularExpression for path matching 2024-05-23 20:08:03 +02:00
Kevin Pollet
0e215f9b61
Support invalid HTTPRoute status
Co-authored-by: Romain <rtribotte@users.noreply.github.com>
2024-05-22 17:20:04 +02:00
kevinpollet
7fdb1ff8af
Merge branch v3.0 into master 2024-05-22 16:01:03 +02:00
Kevin Pollet
736f37cb58
Prepare release v3.0.1 2024-05-22 15:08:04 +02:00
kevinpollet
cff71ee496
Merge branch v2.11 into v3.0 2024-05-21 16:24:57 +02:00
Kevin Pollet
f02b223639
Prepare release v2.11.3 2024-05-21 16:16:05 +02:00
Dmitry Romashov
d4d23dce72
Fix UI unit tests 2024-05-21 15:26:04 +02:00
Romain
5e4dc783c7
Allow empty configuration for OpenTelemetry metrics and tracing 2024-05-21 10:42:04 +02:00
David
440cb11250
Add support for IP White list 2024-05-21 09:24:08 +02:00
Fontany--Legall Brandon
42920595ad
Display of Content Security Policy values getting out of screen 2024-05-17 16:18:04 +02:00
Nicolas Mengin
e68e647fd9
Fix OTel documentation 2024-05-16 09:52:06 +02:00
Michael
8b558646fc
fix: remove providers not more support in documentation 2024-05-15 16:26:04 +02:00
Michael
f8e45a0b29
fix: doc consistency forwardauth 2024-05-15 15:52:04 +02:00
de609cd667
Merge branch 'master' of github.com:traefik/traefik 2024-05-15 18:55:21 +05:30
HalloTschuess
d65de8fe6c
Fix rule syntax version for all internal routers 2024-05-15 10:46:04 +02:00
BreadInvasion
5f2c00b438
Fixed typo in PathRegexp explanation 2024-05-15 10:20:04 +02:00
Landry Benguigui
c2c1c3e09e
Fix the rule syntax mechanism for TCP 2024-05-14 09:42:04 +02:00
Michael
d8a778b5cd
Fix log.compress value 2024-05-13 15:44:03 +02:00
Michel Loiseleur
d8cf90dade
Improve mirroring example on Kubernetes 2024-05-13 15:42:04 +02:00
Marc Mognol
6a06560318
Change log level from Warning to Info when ExternalName services is enabled 2024-05-13 09:06:03 +02:00
Ludovic Fernandez
a4aad5ce5c
fix: router documentation example 2024-05-13 08:54:03 +02:00
Romain
15973f5503
Remove deadlines when handling PostgreSQL connections 2024-05-06 15:46:04 +02:00
Yewolf
a4150409c8
Add link to the new http3 config in migration 2024-05-06 14:50:04 +02:00
Romain
aee515b930
Regenerate v3.0.0 changelog 2024-05-02 18:42:03 +02:00
Kevin Pollet
b0d19bd466
Bump tscert dependency to 28a91b69a046
Co-authored-by: Romain <rtribotte@users.noreply.github.com>
2024-04-30 02:20:04 +02:00
Romain
d99d2f95e6
Prepare release v3.0.0 2024-04-29 16:06:04 +02:00
Prajith
8d2a2ff08f
Native Kubernetes service load-balancing at the provider level 2024-04-29 12:20:04 +02:00
Jesper Noordsij
73e5dbbfe5
Update Kubernetes version for v3 Helm chart 2024-04-29 10:44:03 +02:00
Marvin Stenger
ee3e7cbbec
chore: patch migration/v2.md 2024-04-25 14:54:04 +02:00
94 changed files with 4660 additions and 3285 deletions

View file

@ -1,3 +1,215 @@
## [v3.0.1](https://github.com/traefik/traefik/tree/v3.0.1) (2024-05-22)
[All Commits](https://github.com/traefik/traefik/compare/v3.0.0...v3.0.1)
**Bug fixes:**
- **[k8s/ingress]** Fix rule syntax version for all internal routers ([#10689](https://github.com/traefik/traefik/pull/10689) by [HalloTschuess](https://github.com/HalloTschuess))
- **[metrics,tracing]** Allow empty configuration for OpenTelemetry metrics and tracing ([#10729](https://github.com/traefik/traefik/pull/10729) by [rtribotte](https://github.com/rtribotte))
- **[provider,tls]** Bump tscert dependency to 28a91b69a046 ([#10668](https://github.com/traefik/traefik/pull/10668) by [kevinpollet](https://github.com/kevinpollet))
- **[rules,tcp]** Fix the rule syntax mechanism for TCP ([#10680](https://github.com/traefik/traefik/pull/10680) by [lbenguigui](https://github.com/lbenguigui))
- **[tls,server]** Remove deadlines when handling PostgreSQL connections ([#10675](https://github.com/traefik/traefik/pull/10675) by [rtribotte](https://github.com/rtribotte))
- **[webui]** Add support for IP White list ([#10740](https://github.com/traefik/traefik/pull/10740) by [davidbaptista](https://github.com/davidbaptista))
**Documentation:**
- **[http3]** Add link to the new http3 config in migration ([#10673](https://github.com/traefik/traefik/pull/10673) by [yyewolf](https://github.com/yyewolf))
- **[logs]** Fix log.compress value ([#10716](https://github.com/traefik/traefik/pull/10716) by [mmatur](https://github.com/mmatur))
- **[metrics]** Fix OTel documentation ([#10723](https://github.com/traefik/traefik/pull/10723) by [nmengin](https://github.com/nmengin))
- **[middleware]** Fix doc consistency forwardauth ([#10724](https://github.com/traefik/traefik/pull/10724) by [mmatur](https://github.com/mmatur))
- **[middleware]** Remove providers not supported in documentation ([#10725](https://github.com/traefik/traefik/pull/10725) by [mmatur](https://github.com/mmatur))
- **[rules]** Fix typo in PathRegexp explanation ([#10719](https://github.com/traefik/traefik/pull/10719) by [BreadInvasion](https://github.com/BreadInvasion))
- **[rules]** Fix router documentation example ([#10704](https://github.com/traefik/traefik/pull/10704) by [ldez](https://github.com/ldez))
## [v2.11.3](https://github.com/traefik/traefik/tree/v2.11.3) (2024-05-17)
[All Commits](https://github.com/traefik/traefik/compare/v2.11.2...v2.11.3)
**Bug fixes:**
- **[server]** Remove deadlines for non-TLS connections ([#10615](https://github.com/traefik/traefik/pull/10615) by [rtribotte](https://github.com/rtribotte))
- **[webui]** Display of Content Security Policy values getting out of screen ([#10710](https://github.com/traefik/traefik/pull/10710) by [brandonfl](https://github.com/brandonfl))
- **[webui]** Fix provider icon size ([#10621](https://github.com/traefik/traefik/pull/10621) by [framebassman](https://github.com/framebassman))
**Documentation:**
- **[k8s/crd]** Fix migration/v2.md ([#10658](https://github.com/traefik/traefik/pull/10658) by [stemar94](https://github.com/stemar94))
- **[k8s/gatewayapi]** Fix HTTPRoute use of backendRefs ([#10630](https://github.com/traefik/traefik/pull/10630) by [sakaru](https://github.com/sakaru))
- **[k8s/gatewayapi]** Fix HTTPRoute path type ([#10629](https://github.com/traefik/traefik/pull/10629) by [sakaru](https://github.com/sakaru))
- **[k8s]** Improve mirroring example on Kubernetes ([#10701](https://github.com/traefik/traefik/pull/10701) by [mloiseleur](https://github.com/mloiseleur))
- Consistent entryPoints capitalization in CLI flag usage ([#10650](https://github.com/traefik/traefik/pull/10650) by [jnoordsij](https://github.com/jnoordsij))
- Fix unfinished migration sentence for v2.11.2 ([#10633](https://github.com/traefik/traefik/pull/10633) by [kevinpollet](https://github.com/kevinpollet))
## [v3.0.0](https://github.com/traefik/traefik/tree/v3.0.0) (2024-04-29)
[All Commits](https://github.com/traefik/traefik/compare/v3.0.0-beta1...v3.0.0)
**Enhancements:**
- **[consul]** ConsulCatalog StrictChecks ([#10388](https://github.com/traefik/traefik/pull/10388) by [djenriquez](https://github.com/djenriquez))
- **[docker,docker/swarm]** Split Docker provider ([#9652](https://github.com/traefik/traefik/pull/9652) by [ldez](https://github.com/ldez))
- **[docker,service]** Adds weight on ServersLoadBalancer ([#10372](https://github.com/traefik/traefik/pull/10372) by [juliens](https://github.com/juliens))
- **[ecs]** Add option to keep only healthy ECS tasks ([#8027](https://github.com/traefik/traefik/pull/8027) by [Michampt](https://github.com/Michampt))
- **[file]** Reload provider file configuration on SIGHUP ([#9993](https://github.com/traefik/traefik/pull/9993) by [sokoide](https://github.com/sokoide))
- **[healthcheck]** Support gRPC healthcheck ([#8583](https://github.com/traefik/traefik/pull/8583) by [jjacque](https://github.com/jjacque))
- **[healthcheck]** Add a status option to the service health check ([#9463](https://github.com/traefik/traefik/pull/9463) by [guoard](https://github.com/guoard))
- **[http]** Support custom headers when fetching configuration through HTTP ([#9421](https://github.com/traefik/traefik/pull/9421) by [kevinpollet](https://github.com/kevinpollet))
- **[http3]** Moves HTTP/3 outside the experimental section ([#9570](https://github.com/traefik/traefik/pull/9570) by [sdelicata](https://github.com/sdelicata))
- **[k8s,hub]** Remove deprecated code ([#9804](https://github.com/traefik/traefik/pull/9804) by [ldez](https://github.com/ldez))
- **[k8s,k8s/gatewayapi]** Support for cross-namespace references / GatewayAPI ReferenceGrants ([#10346](https://github.com/traefik/traefik/pull/10346) by [pascal-hofmann](https://github.com/pascal-hofmann))
- **[k8s,k8s/gatewayapi]** Support HostSNIRegexp in GatewayAPI TLS routes ([#9486](https://github.com/traefik/traefik/pull/9486) by [ddtmachado](https://github.com/ddtmachado))
- **[k8s,k8s/gatewayapi]** Upgrade gateway api to v1.0.0 ([#10205](https://github.com/traefik/traefik/pull/10205) by [mmatur](https://github.com/mmatur))
- **[k8s/crd,k8s]** Support file path as input param for Kubernetes token value ([#10232](https://github.com/traefik/traefik/pull/10232) by [sssash18](https://github.com/sssash18))
- **[k8s/gatewayapi]** Add option to set Gateway status address ([#10582](https://github.com/traefik/traefik/pull/10582) by [kevinpollet](https://github.com/kevinpollet))
- **[k8s/gatewayapi]** Toggle support for experimental channel ([#10435](https://github.com/traefik/traefik/pull/10435) by [SantoDE](https://github.com/SantoDE))
- **[k8s/gatewayapi]** Add option to set Gateway status address ([#10582](https://github.com/traefik/traefik/pull/10582) by [kevinpollet](https://github.com/kevinpollet))
- **[k8s/gatewayapi]** Add support for HTTPRequestRedirectFilter in k8s Gateway API ([#9408](https://github.com/traefik/traefik/pull/9408) by [romantomjak](https://github.com/romantomjak))
- **[k8s/gatewayapi]** Handle middlewares in filters extension reference ([#10511](https://github.com/traefik/traefik/pull/10511) by [youkoulayley](https://github.com/youkoulayley))
- **[k8s/ingress,k8s/crd,k8s,k8s/gatewayapi]** Use runtime.Object in routerTransform ([#10523](https://github.com/traefik/traefik/pull/10523) by [juliens](https://github.com/juliens))
- **[k8s/ingress,k8s]** Add option to the Ingress provider to disable IngressClass lookup ([#9281](https://github.com/traefik/traefik/pull/9281) by [jandillenkofer](https://github.com/jandillenkofer))
- **[k8s/ingress,k8s]** Remove support of the networking.k8s.io/v1beta1 APIVersion ([#9949](https://github.com/traefik/traefik/pull/9949) by [rtribotte](https://github.com/rtribotte))
- **[logs]** Introduce static config hints ([#10351](https://github.com/traefik/traefik/pull/10351) by [rtribotte](https://github.com/rtribotte))
- **[logs,performance]** New logger for the Traefik logs ([#9515](https://github.com/traefik/traefik/pull/9515) by [ldez](https://github.com/ldez))
- **[logs,plugins]** Retry on plugin API calls ([#9530](https://github.com/traefik/traefik/pull/9530) by [ldez](https://github.com/ldez))
- **[logs,provider]** Improve provider logs ([#9562](https://github.com/traefik/traefik/pull/9562) by [ldez](https://github.com/ldez))
- **[logs]** Improve test logger assertions ([#9533](https://github.com/traefik/traefik/pull/9533) by [ldez](https://github.com/ldez))
- **[marathon]** Remove Marathon provider ([#9614](https://github.com/traefik/traefik/pull/9614) by [rtribotte](https://github.com/rtribotte))
- **[metrics,tracing,accesslogs]** Remove observability for internal resources ([#9633](https://github.com/traefik/traefik/pull/9633) by [rtribotte](https://github.com/rtribotte))
- **[metrics,tracing]** Upgrade opentelemetry dependencies ([#10472](https://github.com/traefik/traefik/pull/10472) by [mmatur](https://github.com/mmatur))
- **[metrics]** Add support for sending DogStatsD metrics over Unix Socket ([#10199](https://github.com/traefik/traefik/pull/10199) by [liamvdv](https://github.com/liamvdv))
- **[metrics]** Remove InfluxDB v1 metrics middleware ([#9612](https://github.com/traefik/traefik/pull/9612) by [tomMoulard](https://github.com/tomMoulard))
- **[metrics]** Upgrade OpenTelemetry dependencies ([#10181](https://github.com/traefik/traefik/pull/10181) by [mmatur](https://github.com/mmatur))
- **[metrics]** Support gRPC and gRPC-Web protocol in metrics ([#9483](https://github.com/traefik/traefik/pull/9483) by [longit644](https://github.com/longit644))
- **[middleware,accesslogs]** Log TLS client subject ([#9285](https://github.com/traefik/traefik/pull/9285) by [xmessi](https://github.com/xmessi))
- **[middleware,metrics,tracing,otel]** Add OpenTelemetry tracing and metrics support ([#8999](https://github.com/traefik/traefik/pull/8999) by [tomMoulard](https://github.com/tomMoulard))
- **[middleware]** Disable Content-Type auto-detection by default ([#9546](https://github.com/traefik/traefik/pull/9546) by [sdelicata](https://github.com/sdelicata))
- **[middleware]** Add gRPC-Web middleware ([#9451](https://github.com/traefik/traefik/pull/9451) by [juliens](https://github.com/juliens))
- **[middleware]** Add support for Brotli ([#9387](https://github.com/traefik/traefik/pull/9387) by [glinton](https://github.com/glinton))
- **[middleware]** Renaming IPWhiteList to IPAllowList ([#9457](https://github.com/traefik/traefik/pull/9457) by [wxmbugu](https://github.com/wxmbugu))
- **[middleware,authentication,tracing]** Add captured headers options for tracing ([#10457](https://github.com/traefik/traefik/pull/10457) by [rtribotte](https://github.com/rtribotte))
- **[middleware,authentication]** Add forwardAuth.addAuthCookiesToResponse ([#8924](https://github.com/traefik/traefik/pull/8924) by [tgunsch](https://github.com/tgunsch))
- **[middleware,metrics]** Semconv OTLP stable HTTP metrics ([#10421](https://github.com/traefik/traefik/pull/10421) by [mmatur](https://github.com/mmatur))
- **[middleware]** Feat re introduce IpWhitelist middleware as deprecated ([#10341](https://github.com/traefik/traefik/pull/10341) by [mmatur](https://github.com/mmatur))
- **[middleware]** Disable br compression when no Accept-Encoding header is present ([#10178](https://github.com/traefik/traefik/pull/10178) by [robin-moser](https://github.com/robin-moser))
- **[middleware]** Implements the includedContentTypes option for the compress middleware ([#10207](https://github.com/traefik/traefik/pull/10207) by [rjsocha](https://github.com/rjsocha))
- **[middleware]** Add `rejectStatusCode` option to `IPAllowList` middleware ([#10130](https://github.com/traefik/traefik/pull/10130) by [jfly](https://github.com/jfly))
- **[middleware]** Merge v2.11 into v3.0 ([#10426](https://github.com/traefik/traefik/pull/10426) by [mmatur](https://github.com/mmatur))
- **[middleware]** Add ResponseCode to CircuitBreaker ([#10147](https://github.com/traefik/traefik/pull/10147) by [fahhem](https://github.com/fahhem))
- **[nomad]** Allow empty services ([#10375](https://github.com/traefik/traefik/pull/10375) by [chrispruitt](https://github.com/chrispruitt))
- **[nomad]** Support multiple namespaces in the Nomad Provider ([#9332](https://github.com/traefik/traefik/pull/9332) by [0teh](https://github.com/0teh))
- **[plugins]** Add http-wasm plugin support to Traefik ([#10189](https://github.com/traefik/traefik/pull/10189) by [zetaab](https://github.com/zetaab))
- **[plugins]** Upgrade http-wasm host to v0.6.0 to support clients using v0.4.0 ([#10475](https://github.com/traefik/traefik/pull/10475) by [jcchavezs](https://github.com/jcchavezs))
- **[rancher]** Remove Rancher v1 provider ([#9613](https://github.com/traefik/traefik/pull/9613) by [tomMoulard](https://github.com/tomMoulard))
- **[rules]** Bring back v2 rule matchers ([#10339](https://github.com/traefik/traefik/pull/10339) by [rtribotte](https://github.com/rtribotte))
- **[rules]** Remove containous/mux from HTTP muxer ([#9558](https://github.com/traefik/traefik/pull/9558) by [tomMoulard](https://github.com/tomMoulard))
- **[rules]** Update routing syntax ([#9531](https://github.com/traefik/traefik/pull/9531) by [skwair](https://github.com/skwair))
- **[server]** Add SO_REUSEPORT support for EntryPoints ([#9834](https://github.com/traefik/traefik/pull/9834) by [aofei](https://github.com/aofei))
- **[server]** Rework servers load-balancer to use the WRR ([#9431](https://github.com/traefik/traefik/pull/9431) by [juliens](https://github.com/juliens))
- **[server]** Allow default entrypoints definition ([#9100](https://github.com/traefik/traefik/pull/9100) by [applejag](https://github.com/applejag))
- **[sticky-session]** Support setting sticky cookie max age ([#10176](https://github.com/traefik/traefik/pull/10176) by [Patrick0308](https://github.com/Patrick0308))
- **[tls,tcp,service]** Add TCP Servers Transports support ([#9465](https://github.com/traefik/traefik/pull/9465) by [sdelicata](https://github.com/sdelicata))
- **[tls,service]** Support SPIFFE mTLS between Traefik and Backend servers ([#9394](https://github.com/traefik/traefik/pull/9394) by [jlevesy](https://github.com/jlevesy))
- **[tls]** Add Tailscale certificate resolver ([#9237](https://github.com/traefik/traefik/pull/9237) by [kevinpollet](https://github.com/kevinpollet))
- **[tls]** Support SNI routing with Postgres STARTTLS connections ([#9377](https://github.com/traefik/traefik/pull/9377) by [rtribotte](https://github.com/rtribotte))
- **[tracing,otel]** Migrate to opentelemetry ([#10223](https://github.com/traefik/traefik/pull/10223) by [zetaab](https://github.com/zetaab))
- **[tracing]** Support OTEL_PROPAGATORS to configure tracing propagation ([#10465](https://github.com/traefik/traefik/pull/10465) by [youkoulayley](https://github.com/youkoulayley))
- **[webui,middleware,k8s/gatewayapi]** Support RequestHeaderModifier filter ([#10521](https://github.com/traefik/traefik/pull/10521) by [rtribotte](https://github.com/rtribotte))
- **[webui]** Added router priority to webui&#39;s list and detail page ([#9004](https://github.com/traefik/traefik/pull/9004) by [bendre90](https://github.com/bendre90))
- Reintroduce dropped v2 dynamic config ([#10355](https://github.com/traefik/traefik/pull/10355) by [rtribotte](https://github.com/rtribotte))
- Remove deprecated options ([#9527](https://github.com/traefik/traefik/pull/9527) by [sdelicata](https://github.com/sdelicata))
**Bug fixes:**
- **[consul,tls]** Enable TLS for Consul Connect TCP services ([#10140](https://github.com/traefik/traefik/pull/10140) by [rtribotte](https://github.com/rtribotte))
- **[docker]** Fix struct names in comment ([#10503](https://github.com/traefik/traefik/pull/10503) by [hishope](https://github.com/hishope))
- **[k8s/crd,k8s]** Adds the missing circuit-breaker response code for CRD ([#10625](https://github.com/traefik/traefik/pull/10625) by [ldez](https://github.com/ldez))
- **[k8s/crd,k8s]** Delete warning in Kubernetes CRD provider about the supported version ([#10414](https://github.com/traefik/traefik/pull/10414) by [nmengin](https://github.com/nmengin))
- **[logs]** Avoid cumulative send anonymous usage log ([#10579](https://github.com/traefik/traefik/pull/10579) by [mmatur](https://github.com/mmatur))
- **[logs]** Change traefik cmd error log to error level ([#9569](https://github.com/traefik/traefik/pull/9569) by [tomMoulard](https://github.com/tomMoulard))
- **[logs]** Fix log level ([#9545](https://github.com/traefik/traefik/pull/9545) by [ldez](https://github.com/ldez))
- **[metrics]** Fix OpenTelemetry metrics ([#9962](https://github.com/traefik/traefik/pull/9962) by [rtribotte](https://github.com/rtribotte))
- **[metrics]** Fix OpenTelemetry service name ([#9619](https://github.com/traefik/traefik/pull/9619) by [tomMoulard](https://github.com/tomMoulard))
- **[metrics]** Fix open connections metric ([#9656](https://github.com/traefik/traefik/pull/9656) by [mpl](https://github.com/mpl))
- **[metrics]** Remove config reload failure metrics ([#9660](https://github.com/traefik/traefik/pull/9660) by [rtribotte](https://github.com/rtribotte))
- **[metrics]** Fix OpenTelemetry unit tests ([#10380](https://github.com/traefik/traefik/pull/10380) by [mmatur](https://github.com/mmatur))
- **[metrics]** Fix ServerUp metric ([#9534](https://github.com/traefik/traefik/pull/9534) by [kevinpollet](https://github.com/kevinpollet))
- **[middleware,authentication,metrics,tracing]** Align OpenTelemetry tracing and metrics configurations ([#10404](https://github.com/traefik/traefik/pull/10404) by [rtribotte](https://github.com/rtribotte))
- **[middleware]** Fix brotli response status code when compression is disabled ([#10396](https://github.com/traefik/traefik/pull/10396) by [rtribotte](https://github.com/rtribotte))
- **[middleware]** Allow short healthcheck interval with long timeout ([#9832](https://github.com/traefik/traefik/pull/9832) by [kevinmcconnell](https://github.com/kevinmcconnell))
- **[middleware]** Fix GrpcWeb middleware to clear ContentLength after translating to normal gRPC message ([#9782](https://github.com/traefik/traefik/pull/9782) by [CleverUnderDog](https://github.com/CleverUnderDog))
- **[provider,tls]** Bump tscert dependency to 28a91b69a046 ([#10668](https://github.com/traefik/traefik/pull/10668) by [kevinpollet](https://github.com/kevinpollet))
- **[rules]** Rework Host and HostRegexp matchers ([#9559](https://github.com/traefik/traefik/pull/9559) by [tomMoulard](https://github.com/tomMoulard))
- **[rules]** Support regexp in path/pathprefix in matcher v2 ([#10546](https://github.com/traefik/traefik/pull/10546) by [youkoulayley](https://github.com/youkoulayley))
- **[sticky-session,server]** Set sameSite field for wrr load balancer sticky cookie ([#10066](https://github.com/traefik/traefik/pull/10066) by [sunyakun](https://github.com/sunyakun))
- **[tcp]** Don&#39;t log EOF or timeout errors while peeking first bytes in Postgres StartTLS hook ([#9663](https://github.com/traefik/traefik/pull/9663) by [rtribotte](https://github.com/rtribotte))
- **[tls,server]** Compute priority for https forwarder TLS routes ([#10288](https://github.com/traefik/traefik/pull/10288) by [rtribotte](https://github.com/rtribotte))
- **[tls,service]** Enforce default servers transport SPIFFE config ([#9444](https://github.com/traefik/traefik/pull/9444) by [jlevesy](https://github.com/jlevesy))
- **[webui]** Detect dashboard assets content types ([#9622](https://github.com/traefik/traefik/pull/9622) by [tomMoulard](https://github.com/tomMoulard))
- **[webui]** Add missing Docker Swarm logo ([#10529](https://github.com/traefik/traefik/pull/10529) by [ldez](https://github.com/ldez))
- **[webui]** fix: detect dashboard content types ([#9594](https://github.com/traefik/traefik/pull/9594) by [ldez](https://github.com/ldez))
- Fix a regression on flags using spaces between key and value ([#10445](https://github.com/traefik/traefik/pull/10445) by [ldez](https://github.com/ldez))
**Documentation:**
- **[docker/swarm]** Remove documentation of old swarm options ([#10001](https://github.com/traefik/traefik/pull/10001) by [ldez](https://github.com/ldez))
- **[docker/swarm]** Fix minor typo in swarm example ([#10071](https://github.com/traefik/traefik/pull/10071) by [kaznovac](https://github.com/kaznovac))
- **[k8s,k8s/gatewayapi]** Add ReferenceGrants to Gateway API Traefik controller RBAC ([#10462](https://github.com/traefik/traefik/pull/10462) by [rtribotte](https://github.com/rtribotte))
- **[k8s]** Update Kubernetes version for v3 Helm chart ([#10637](https://github.com/traefik/traefik/pull/10637) by [jnoordsij](https://github.com/jnoordsij))
- **[k8s]** Improve Kubernetes support documentation ([#9974](https://github.com/traefik/traefik/pull/9974) by [rtribotte](https://github.com/rtribotte))
- **[k8s]** Fix invalid version in docs about Gateway API on Traefik v3 ([#10474](https://github.com/traefik/traefik/pull/10474) by [mloiseleur](https://github.com/mloiseleur))
- **[rules]** Improve ruleSyntax option documentation ([#10441](https://github.com/traefik/traefik/pull/10441) by [rtribotte](https://github.com/rtribotte))
- Prepare release v3.0.0 ([#10666](https://github.com/traefik/traefik/pull/10666) by [rtribotte](https://github.com/rtribotte))
- Prepare release v3.0.0-rc2 ([#10514](https://github.com/traefik/traefik/pull/10514) by [rtribotte](https://github.com/rtribotte))
- Fix typo in migration docs ([#10478](https://github.com/traefik/traefik/pull/10478) by [Eisberge](https://github.com/Eisberge))
- Prepare release v3.0.0 rc3 ([#10520](https://github.com/traefik/traefik/pull/10520) by [rtribotte](https://github.com/rtribotte))
- Fix typo in dialer_test.go ([#10552](https://github.com/traefik/traefik/pull/10552) by [eltociear](https://github.com/eltociear))
- Fix typo and improve explanation on internal resources ([#10563](https://github.com/traefik/traefik/pull/10563) by [mloiseleur](https://github.com/mloiseleur))
- Prepare release v3.0.0-rc1 ([#10429](https://github.com/traefik/traefik/pull/10429) by [mmatur](https://github.com/mmatur))
- Update version comment in quick-start.md ([#10383](https://github.com/traefik/traefik/pull/10383) by [matthieuwerner](https://github.com/matthieuwerner))
- Improve migration guide ([#10319](https://github.com/traefik/traefik/pull/10319) by [rtribotte](https://github.com/rtribotte))
- Prepare release v3.0.0 beta5 ([#10273](https://github.com/traefik/traefik/pull/10273) by [rtribotte](https://github.com/rtribotte))
- Prepare release v3.0.0-beta4 ([#10165](https://github.com/traefik/traefik/pull/10165) by [mmatur](https://github.com/mmatur))
- Prepare release v3.0.0-rc4 ([#10588](https://github.com/traefik/traefik/pull/10588) by [kevinpollet](https://github.com/kevinpollet))
- Fix bad anchor on documentation ([#10041](https://github.com/traefik/traefik/pull/10041) by [mmatur](https://github.com/mmatur))
- Prepare release v3.0.0-rc5 ([#10605](https://github.com/traefik/traefik/pull/10605) by [ldez](https://github.com/ldez))
- Fix migration guide heading ([#9989](https://github.com/traefik/traefik/pull/9989) by [ldez](https://github.com/ldez))
- Prepare release v3.0.0-beta3 ([#9978](https://github.com/traefik/traefik/pull/9978) by [ldez](https://github.com/ldez))
- Fix some typos in comments ([#10626](https://github.com/traefik/traefik/pull/10626) by [hidewrong](https://github.com/hidewrong))
- Adjust quick start ([#9790](https://github.com/traefik/traefik/pull/9790) by [svx](https://github.com/svx))
- Mention PathPrefix matcher changes in V3 Migration Guide ([#9727](https://github.com/traefik/traefik/pull/9727) by [aofei](https://github.com/aofei))
- Fix yaml indentation in the HTTP3 example ([#9724](https://github.com/traefik/traefik/pull/9724) by [benwaffle](https://github.com/benwaffle))
- Add OpenTelemetry in observability overview ([#9654](https://github.com/traefik/traefik/pull/9654) by [tomMoulard](https://github.com/tomMoulard))
- Prepare release v3.0.0-beta2 ([#9587](https://github.com/traefik/traefik/pull/9587) by [tomMoulard](https://github.com/tomMoulard))
- Prepare release v3.0.0-beta1 ([#9577](https://github.com/traefik/traefik/pull/9577) by [rtribotte](https://github.com/rtribotte))
**Misc:**
- Merge current v2.11 into v3.0 ([#10651](https://github.com/traefik/traefik/pull/10651) by [ldez](https://github.com/ldez))
- Merge current v2.11 into v3.0 ([#10632](https://github.com/traefik/traefik/pull/10632) by [kevinpollet](https://github.com/kevinpollet))
- Merge current v2.11 into v3.0 ([#10604](https://github.com/traefik/traefik/pull/10604) by [ldez](https://github.com/ldez))
- Merge branch v2.11 into v3.0 ([#10587](https://github.com/traefik/traefik/pull/10587) by [kevinpollet](https://github.com/kevinpollet))
- Merge current v2.11 into v3.0 ([#10566](https://github.com/traefik/traefik/pull/10566) by [mmatur](https://github.com/mmatur))
- Merge current v2.11 into v3.0 ([#10564](https://github.com/traefik/traefik/pull/10564) by [ldez](https://github.com/ldez))
- Merge branch v2.11 into v3.0 ([#10519](https://github.com/traefik/traefik/pull/10519) by [rtribotte](https://github.com/rtribotte))
- Merge v2.11 into v3.0 ([#10513](https://github.com/traefik/traefik/pull/10513) by [mmatur](https://github.com/mmatur))
- Merge v2.11 into v3.0 ([#10417](https://github.com/traefik/traefik/pull/10417) by [mmatur](https://github.com/mmatur))
- Merge current v2.11 into v3.0 ([#10382](https://github.com/traefik/traefik/pull/10382) by [mmatur](https://github.com/mmatur))
- Merge back v2.11 into v3.0 ([#10377](https://github.com/traefik/traefik/pull/10377) by [mmatur](https://github.com/mmatur))
- Merge back v2.11 into v3.0 ([#10353](https://github.com/traefik/traefik/pull/10353) by [youkoulayley](https://github.com/youkoulayley))
- Merge current v2.11 into v3.0 ([#10328](https://github.com/traefik/traefik/pull/10328) by [mmatur](https://github.com/mmatur))
- Merge current v2.10 into v3.0 ([#10272](https://github.com/traefik/traefik/pull/10272) by [rtribotte](https://github.com/rtribotte))
- Merge current v2.10 into v3.0 ([#10164](https://github.com/traefik/traefik/pull/10164) by [mmatur](https://github.com/mmatur))
- Merge current v2.10 into v3.0 ([#10038](https://github.com/traefik/traefik/pull/10038) by [mmatur](https://github.com/mmatur))
- Merge branch v2.10 into v3.0 ([#9977](https://github.com/traefik/traefik/pull/9977) by [ldez](https://github.com/ldez))
- Merge branch v2.10 into v3.0 ([#9931](https://github.com/traefik/traefik/pull/9931) by [ldez](https://github.com/ldez))
- Merge branch v2.10 into v3.0 ([#9896](https://github.com/traefik/traefik/pull/9896) by [ldez](https://github.com/ldez))
- Merge branch v2.10 into v3.0 ([#9867](https://github.com/traefik/traefik/pull/9867) by [ldez](https://github.com/ldez))
- Merge branch v2.10 into v3.0 ([#9850](https://github.com/traefik/traefik/pull/9850) by [ldez](https://github.com/ldez))
- Merge branch v2.10 into v3.0 ([#9845](https://github.com/traefik/traefik/pull/9845) by [ldez](https://github.com/ldez))
- Merge branch v2.10 into v3.0 ([#9803](https://github.com/traefik/traefik/pull/9803) by [ldez](https://github.com/ldez))
- Merge branch v2.10 into v3.0 ([#9793](https://github.com/traefik/traefik/pull/9793) by [ldez](https://github.com/ldez))
- Merge branch v2.9 into v3.0 ([#9722](https://github.com/traefik/traefik/pull/9722) by [rtribotte](https://github.com/rtribotte))
- Merge branch v2.9 into v3.0 ([#9650](https://github.com/traefik/traefik/pull/9650) by [tomMoulard](https://github.com/tomMoulard))
- Merge branch v2.9 into v3.0 ([#9632](https://github.com/traefik/traefik/pull/9632) by [kevinpollet](https://github.com/kevinpollet))
- Merge current v2.9 into master ([#9576](https://github.com/traefik/traefik/pull/9576) by [rtribotte](https://github.com/rtribotte))
- Merge branch v2.9 into master ([#9554](https://github.com/traefik/traefik/pull/9554) by [ldez](https://github.com/ldez))
- Merge branch v2.9 into master ([#9536](https://github.com/traefik/traefik/pull/9536) by [ldez](https://github.com/ldez))
- Merge branch v2.9 into master ([#9532](https://github.com/traefik/traefik/pull/9532) by [ldez](https://github.com/ldez))
- Merge branch v2.9 into master ([#9482](https://github.com/traefik/traefik/pull/9482) by [kevinpollet](https://github.com/kevinpollet))
- Merge branch v2.9 into master ([#9464](https://github.com/traefik/traefik/pull/9464) by [ldez](https://github.com/ldez))
- Merge branch v2.9 into master ([#9449](https://github.com/traefik/traefik/pull/9449) by [kevinpollet](https://github.com/kevinpollet))
- Merge branch v2.9 into master ([#9419](https://github.com/traefik/traefik/pull/9419) by [kevinpollet](https://github.com/kevinpollet))
- Merge branch v2.9 into master ([#9351](https://github.com/traefik/traefik/pull/9351) by [rtribotte](https://github.com/rtribotte))
## [v3.0.0-rc5](https://github.com/traefik/traefik/tree/v3.0.0-rc4) (2024-04-11) ## [v3.0.0-rc5](https://github.com/traefik/traefik/tree/v3.0.0-rc4) (2024-04-11)
[All Commits](https://github.com/traefik/traefik/compare/v3.0.0-rc4...v3.0.0-rc5) [All Commits](https://github.com/traefik/traefik/compare/v3.0.0-rc4...v3.0.0-rc5)

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View file

@ -44,7 +44,7 @@ Traefik can be installed in Kubernetes using the Helm chart from <https://github
Ensure that the following requirements are met: Ensure that the following requirements are met:
* Kubernetes 1.16+ * Kubernetes 1.22+
* Helm version 3.9+ is [installed](https://helm.sh/docs/intro/install/) * Helm version 3.9+ is [installed](https://helm.sh/docs/intro/install/)
Add Traefik Labs chart repository to Helm: Add Traefik Labs chart repository to Helm:

View file

@ -300,7 +300,7 @@ labels:
``` ```
```yaml tab="Kubernetes" ```yaml tab="Kubernetes"
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.io/v1alpha1
kind: Middleware kind: Middleware
metadata: metadata:
name: test-auth name: test-auth
@ -316,13 +316,6 @@ spec:
- "traefik.http.middlewares.test-auth.forwardauth.addAuthCookiesToResponse=Session-Cookie,State-Cookie" - "traefik.http.middlewares.test-auth.forwardauth.addAuthCookiesToResponse=Session-Cookie,State-Cookie"
``` ```
```toml tab="File (TOML)"
[http.middlewares]
[http.middlewares.test-auth.forwardAuth]
address = "https://example.com/auth"
addAuthCookiesToResponse = ["Session-Cookie", "State-Cookie"]
```
```yaml tab="File (YAML)" ```yaml tab="File (YAML)"
http: http:
middlewares: middlewares:
@ -334,6 +327,13 @@ http:
- "State-Cookie" - "State-Cookie"
``` ```
```toml tab="File (TOML)"
[http.middlewares]
[http.middlewares.test-auth.forwardAuth]
address = "https://example.com/auth"
addAuthCookiesToResponse = ["Session-Cookie", "State-Cookie"]
```
### `tls` ### `tls`
_Optional_ _Optional_

View file

@ -35,18 +35,6 @@ spec:
- "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcerange=127.0.0.1/32, 192.168.1.7" - "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcerange=127.0.0.1/32, 192.168.1.7"
``` ```
```json tab="Marathon"
"labels": {
"traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcerange": "127.0.0.1/32,192.168.1.7"
}
```
```yaml tab="Rancher"
# Accepts request from defined IP
labels:
- "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcerange=127.0.0.1/32, 192.168.1.7"
```
```yaml tab="File (YAML)" ```yaml tab="File (YAML)"
# Accepts request from defined IP # Accepts request from defined IP
http: http:
@ -125,20 +113,6 @@ spec:
- "traefik.http.middlewares.test-ipallowlist.ipallowlist.ipstrategy.depth=2" - "traefik.http.middlewares.test-ipallowlist.ipallowlist.ipstrategy.depth=2"
``` ```
```json tab="Marathon"
"labels": {
"traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcerange": "127.0.0.1/32, 192.168.1.7",
"traefik.http.middlewares.test-ipallowlist.ipallowlist.ipstrategy.depth": "2"
}
```
```yaml tab="Rancher"
# Whitelisting Based on `X-Forwarded-For` with `depth=2`
labels:
- "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcerange=127.0.0.1/32, 192.168.1.7"
- "traefik.http.middlewares.test-ipallowlist.ipallowlist.ipstrategy.depth=2"
```
```yaml tab="File (YAML)" ```yaml tab="File (YAML)"
# Allowlisting Based on `X-Forwarded-For` with `depth=2` # Allowlisting Based on `X-Forwarded-For` with `depth=2`
http: http:
@ -207,20 +181,6 @@ spec:
- "traefik.http.middlewares.test-ipallowlist.ipallowlist.ipstrategy.excludedips=127.0.0.1/32, 192.168.1.7" - "traefik.http.middlewares.test-ipallowlist.ipallowlist.ipstrategy.excludedips=127.0.0.1/32, 192.168.1.7"
``` ```
```json tab="Marathon"
"labels": {
"traefik.http.middlewares.test-ipallowlist.ipallowlist.sourceRange=127.0.0.1/32, 192.168.1.0/24"
"traefik.http.middlewares.test-ipallowlist.ipallowlist.ipstrategy.excludedips": "127.0.0.1/32, 192.168.1.7"
}
```
```yaml tab="Rancher"
# Exclude from `X-Forwarded-For`
labels:
- "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourceRange=127.0.0.1/32, 192.168.1.0/24"
- "traefik.http.middlewares.test-ipallowlist.ipallowlist.ipstrategy.excludedips=127.0.0.1/32, 192.168.1.7"
```
```yaml tab="File (YAML)" ```yaml tab="File (YAML)"
# Exclude from `X-Forwarded-For` # Exclude from `X-Forwarded-For`
http: http:

View file

@ -147,6 +147,7 @@ It is now unsupported and would prevent Traefik to start.
##### Remediation ##### Remediation
The `http3` option should be removed from the static configuration experimental section. The `http3` option should be removed from the static configuration experimental section.
To configure `http3`, please checkout the [entrypoint configuration documentation](https://doc.traefik.io/traefik/v3.0/routing/entrypoints/#http3_1).
### Consul provider ### Consul provider

View file

@ -513,7 +513,7 @@ In `v2.10`, the Kubernetes CRDs API Group `traefik.containo.us` is deprecated, a
As the Kubernetes CRD provider still works with both API Versions (`traefik.io/v1alpha1` and `traefik.containo.us/v1alpha1`), As the Kubernetes CRD provider still works with both API Versions (`traefik.io/v1alpha1` and `traefik.containo.us/v1alpha1`),
it means that for the same kind, namespace and name, the provider will only keep the `traefik.io/v1alpha1` resource. it means that for the same kind, namespace and name, the provider will only keep the `traefik.io/v1alpha1` resource.
In addition, the Kubernetes CRDs API Version `traefik.io/v1alpha1` will not be supported in Traefik v3 itself. In addition, the Kubernetes CRDs API Version `traefik.containo.us/v1alpha1` will not be supported in Traefik v3 itself.
Please note that it is a requirement to update the CRDs and the RBAC in the cluster before upgrading Traefik. Please note that it is a requirement to update the CRDs and the RBAC in the cluster before upgrading Traefik.
To do so, please apply the required [CRDs](https://raw.githubusercontent.com/traefik/traefik/v2.10/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml) and [RBAC](https://raw.githubusercontent.com/traefik/traefik/v2.10/docs/content/reference/dynamic-configuration/kubernetes-crd-rbac.yml) manifests for v2.10: To do so, please apply the required [CRDs](https://raw.githubusercontent.com/traefik/traefik/v2.10/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml) and [RBAC](https://raw.githubusercontent.com/traefik/traefik/v2.10/docs/content/reference/dynamic-configuration/kubernetes-crd-rbac.yml) manifests for v2.10:

View file

@ -169,14 +169,14 @@ The default is not to perform compression.
```yaml tab="File (YAML)" ```yaml tab="File (YAML)"
log: log:
compress: 3 compress: true
``` ```
```toml tab="File (TOML)" ```toml tab="File (TOML)"
[log] [log]
compress = 3 compress = true
``` ```
```bash tab="CLI" ```bash tab="CLI"
--log.compress=3 --log.compress=true
``` ```

View file

@ -5,7 +5,7 @@ description: "Traefik Proxy supports these metrics backend systems: Datadog, Inf
# Metrics # Metrics
Traefik supports these metrics backends: Traefik provides metrics in the [OpenTelemetry](./opentelemetry.md) format as well as the following vendor specific backends:
- [Datadog](./datadog.md) - [Datadog](./datadog.md)
- [InfluxDB2](./influxdb2.md) - [InfluxDB2](./influxdb2.md)
@ -46,6 +46,13 @@ addInternals = true
| Open connections | Gauge | `entrypoint`, `protocol` | The current count of open connections, by entrypoint and protocol. | | Open connections | Gauge | `entrypoint`, `protocol` | The current count of open connections, by entrypoint and protocol. |
| TLS certificates not after | Gauge | | The expiration date of certificates. | | TLS certificates not after | Gauge | | The expiration date of certificates. |
```opentelemetry tab="OpenTelemetry"
traefik_config_reloads_total
traefik_config_last_reload_success
traefik_open_connections
traefik_tls_certs_not_after
```
```prom tab="Prometheus" ```prom tab="Prometheus"
traefik_config_reloads_total traefik_config_reloads_total
traefik_config_last_reload_success traefik_config_last_reload_success
@ -75,13 +82,6 @@ traefik.tls.certs.notAfterTimestamp
{prefix}.tls.certs.notAfterTimestamp {prefix}.tls.certs.notAfterTimestamp
``` ```
```opentelemetry tab="OpenTelemetry"
traefik_config_reloads_total
traefik_config_last_reload_success
traefik_open_connections
traefik_tls_certs_not_after
```
### Labels ### Labels
Here is a comprehensive list of labels that are provided by the global metrics: Here is a comprehensive list of labels that are provided by the global metrics:
@ -91,201 +91,9 @@ Here is a comprehensive list of labels that are provided by the global metrics:
| `entrypoint` | Entrypoint that handled the connection | "example_entrypoint" | | `entrypoint` | Entrypoint that handled the connection | "example_entrypoint" |
| `protocol` | Connection protocol | "TCP" | | `protocol` | Connection protocol | "TCP" |
## HTTP Metrics ## OpenTelemetry Semantic Conventions
### EntryPoint Metrics Traefik Proxy follows [official OpenTelemetry semantic conventions v1.23.1](https://github.com/open-telemetry/semantic-conventions/blob/v1.23.1/docs/http/http-metrics.md).
| Metric | Type | [Labels](#labels) | Description |
|-----------------------|-----------|--------------------------------------------|---------------------------------------------------------------------|
| Requests total | Count | `code`, `method`, `protocol`, `entrypoint` | The total count of HTTP requests received by an entrypoint. |
| Requests TLS total | Count | `tls_version`, `tls_cipher`, `entrypoint` | The total count of HTTPS requests received by an entrypoint. |
| Request duration | Histogram | `code`, `method`, `protocol`, `entrypoint` | Request processing duration histogram on an entrypoint. |
| Requests bytes total | Count | `code`, `method`, `protocol`, `entrypoint` | The total size of HTTP requests in bytes handled by an entrypoint. |
| Responses bytes total | Count | `code`, `method`, `protocol`, `entrypoint` | The total size of HTTP responses in bytes handled by an entrypoint. |
```prom tab="Prometheus"
traefik_entrypoint_requests_total
traefik_entrypoint_requests_tls_total
traefik_entrypoint_request_duration_seconds
traefik_entrypoint_requests_bytes_total
traefik_entrypoint_responses_bytes_total
```
```dd tab="Datadog"
entrypoint.request.total
entrypoint.request.tls.total
entrypoint.request.duration
entrypoint.requests.bytes.total
entrypoint.responses.bytes.total
```
```influxdb tab="InfluxDB2"
traefik.entrypoint.requests.total
traefik.entrypoint.requests.tls.total
traefik.entrypoint.request.duration
traefik.entrypoint.requests.bytes.total
traefik.entrypoint.responses.bytes.total
```
```statsd tab="StatsD"
# Default prefix: "traefik"
{prefix}.entrypoint.request.total
{prefix}.entrypoint.request.tls.total
{prefix}.entrypoint.request.duration
{prefix}.entrypoint.requests.bytes.total
{prefix}.entrypoint.responses.bytes.total
```
```opentelemetry tab="OpenTelemetry"
traefik_entrypoint_requests_total
traefik_entrypoint_requests_tls_total
traefik_entrypoint_request_duration_seconds
traefik_entrypoint_requests_bytes_total
traefik_entrypoint_responses_bytes_total
```
### Router Metrics
| Metric | Type | [Labels](#labels) | Description |
|-----------------------|-----------|---------------------------------------------------|----------------------------------------------------------------|
| Requests total | Count | `code`, `method`, `protocol`, `router`, `service` | The total count of HTTP requests handled by a router. |
| Requests TLS total | Count | `tls_version`, `tls_cipher`, `router`, `service` | The total count of HTTPS requests handled by a router. |
| Request duration | Histogram | `code`, `method`, `protocol`, `router`, `service` | Request processing duration histogram on a router. |
| Requests bytes total | Count | `code`, `method`, `protocol`, `router`, `service` | The total size of HTTP requests in bytes handled by a router. |
| Responses bytes total | Count | `code`, `method`, `protocol`, `router`, `service` | The total size of HTTP responses in bytes handled by a router. |
```prom tab="Prometheus"
traefik_router_requests_total
traefik_router_requests_tls_total
traefik_router_request_duration_seconds
traefik_router_requests_bytes_total
traefik_router_responses_bytes_total
```
```dd tab="Datadog"
router.request.total
router.request.tls.total
router.request.duration
router.requests.bytes.total
router.responses.bytes.total
```
```influxdb tab="InfluxDB2"
traefik.router.requests.total
traefik.router.requests.tls.total
traefik.router.request.duration
traefik.router.requests.bytes.total
traefik.router.responses.bytes.total
```
```statsd tab="StatsD"
# Default prefix: "traefik"
{prefix}.router.request.total
{prefix}.router.request.tls.total
{prefix}.router.request.duration
{prefix}.router.requests.bytes.total
{prefix}.router.responses.bytes.total
```
```opentelemetry tab="OpenTelemetry"
traefik_router_requests_total
traefik_router_requests_tls_total
traefik_router_request_duration_seconds
traefik_router_requests_bytes_total
traefik_router_responses_bytes_total
```
### Service Metrics
| Metric | Type | Labels | Description |
|-----------------------|-----------|-----------------------------------------|-------------------------------------------------------------|
| Requests total | Count | `code`, `method`, `protocol`, `service` | The total count of HTTP requests processed on a service. |
| Requests TLS total | Count | `tls_version`, `tls_cipher`, `service` | The total count of HTTPS requests processed on a service. |
| Request duration | Histogram | `code`, `method`, `protocol`, `service` | Request processing duration histogram on a service. |
| Retries total | Count | `service` | The count of requests retries on a service. |
| Server UP | Gauge | `service`, `url` | Current service's server status, 0 for a down or 1 for up. |
| Requests bytes total | Count | `code`, `method`, `protocol`, `service` | The total size of requests in bytes received by a service. |
| Responses bytes total | Count | `code`, `method`, `protocol`, `service` | The total size of responses in bytes returned by a service. |
```prom tab="Prometheus"
traefik_service_requests_total
traefik_service_requests_tls_total
traefik_service_request_duration_seconds
traefik_service_retries_total
traefik_service_server_up
traefik_service_requests_bytes_total
traefik_service_responses_bytes_total
```
```dd tab="Datadog"
service.request.total
router.service.tls.total
service.request.duration
service.retries.total
service.server.up
service.requests.bytes.total
service.responses.bytes.total
```
```influxdb tab="InfluxDB2"
traefik.service.requests.total
traefik.service.requests.tls.total
traefik.service.request.duration
traefik.service.retries.total
traefik.service.server.up
traefik.service.requests.bytes.total
traefik.service.responses.bytes.total
```
```statsd tab="StatsD"
# Default prefix: "traefik"
{prefix}.service.request.total
{prefix}.service.request.tls.total
{prefix}.service.request.duration
{prefix}.service.retries.total
{prefix}.service.server.up
{prefix}.service.requests.bytes.total
{prefix}.service.responses.bytes.total
```
```opentelemetry tab="OpenTelemetry"
traefik_service_requests_total
traefik_service_requests_tls_total
traefik_service_request_duration_seconds
traefik_service_retries_total
traefik_service_server_up
traefik_service_requests_bytes_total
traefik_service_responses_bytes_total
```
### Labels
Here is a comprehensive list of labels that are provided by the metrics:
| Label | Description | example |
|---------------|---------------------------------------|----------------------------|
| `cn` | Certificate Common Name | "example.com" |
| `code` | Request code | "200" |
| `entrypoint` | Entrypoint that handled the request | "example_entrypoint" |
| `method` | Request Method | "GET" |
| `protocol` | Request protocol | "http" |
| `router` | Router that handled the request | "example_router" |
| `sans` | Certificate Subject Alternative NameS | "example.com" |
| `serial` | Certificate Serial Number | "123..." |
| `service` | Service that handled the request | "example_service@provider" |
| `tls_cipher` | TLS cipher used for the request | "TLS_FALLBACK_SCSV" |
| `tls_version` | TLS version used for the request | "1.0" |
| `url` | Service server url | "http://example.com" |
!!! info "`method` label value"
If the HTTP method verb on a request is not one defined in the set of common methods for [`HTTP/1.1`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods)
or the [`PRI`](https://datatracker.ietf.org/doc/html/rfc7540#section-11.6) verb (for `HTTP/2`),
then the value for the method label becomes `EXTENSION_METHOD`.
## Semantic Conventions for HTTP Metrics
Traefik Proxy follows [official OTLP semantic conventions v1.23.1](https://github.com/open-telemetry/semantic-conventions/blob/v1.23.1/docs/http/http-metrics.md).
### HTTP Server ### HTTP Server
@ -328,3 +136,197 @@ Here is a comprehensive list of labels that are provided by the metrics:
| `server.address` | Name of the local HTTP server that received the request | "example.com" | | `server.address` | Name of the local HTTP server that received the request | "example.com" |
| `server.port` | Port of the local HTTP server that received the request | "80" | | `server.port` | Port of the local HTTP server that received the request | "80" |
| `url.scheme` | The URI scheme component identifying the used protocol | "http" | | `url.scheme` | The URI scheme component identifying the used protocol | "http" |
## HTTP Metrics
On top of the official OpenTelemetry semantic conventions, Traefik provides its own metrics to monitor the incoming traffic.
### EntryPoint Metrics
| Metric | Type | [Labels](#labels) | Description |
|-----------------------|-----------|--------------------------------------------|---------------------------------------------------------------------|
| Requests total | Count | `code`, `method`, `protocol`, `entrypoint` | The total count of HTTP requests received by an entrypoint. |
| Requests TLS total | Count | `tls_version`, `tls_cipher`, `entrypoint` | The total count of HTTPS requests received by an entrypoint. |
| Request duration | Histogram | `code`, `method`, `protocol`, `entrypoint` | Request processing duration histogram on an entrypoint. |
| Requests bytes total | Count | `code`, `method`, `protocol`, `entrypoint` | The total size of HTTP requests in bytes handled by an entrypoint. |
| Responses bytes total | Count | `code`, `method`, `protocol`, `entrypoint` | The total size of HTTP responses in bytes handled by an entrypoint. |
```opentelemetry tab="OpenTelemetry"
traefik_entrypoint_requests_total
traefik_entrypoint_requests_tls_total
traefik_entrypoint_request_duration_seconds
traefik_entrypoint_requests_bytes_total
traefik_entrypoint_responses_bytes_total
```
```prom tab="Prometheus"
traefik_entrypoint_requests_total
traefik_entrypoint_requests_tls_total
traefik_entrypoint_request_duration_seconds
traefik_entrypoint_requests_bytes_total
traefik_entrypoint_responses_bytes_total
```
```dd tab="Datadog"
entrypoint.request.total
entrypoint.request.tls.total
entrypoint.request.duration
entrypoint.requests.bytes.total
entrypoint.responses.bytes.total
```
```influxdb tab="InfluxDB2"
traefik.entrypoint.requests.total
traefik.entrypoint.requests.tls.total
traefik.entrypoint.request.duration
traefik.entrypoint.requests.bytes.total
traefik.entrypoint.responses.bytes.total
```
```statsd tab="StatsD"
# Default prefix: "traefik"
{prefix}.entrypoint.request.total
{prefix}.entrypoint.request.tls.total
{prefix}.entrypoint.request.duration
{prefix}.entrypoint.requests.bytes.total
{prefix}.entrypoint.responses.bytes.total
```
### Router Metrics
| Metric | Type | [Labels](#labels) | Description |
|-----------------------|-----------|---------------------------------------------------|----------------------------------------------------------------|
| Requests total | Count | `code`, `method`, `protocol`, `router`, `service` | The total count of HTTP requests handled by a router. |
| Requests TLS total | Count | `tls_version`, `tls_cipher`, `router`, `service` | The total count of HTTPS requests handled by a router. |
| Request duration | Histogram | `code`, `method`, `protocol`, `router`, `service` | Request processing duration histogram on a router. |
| Requests bytes total | Count | `code`, `method`, `protocol`, `router`, `service` | The total size of HTTP requests in bytes handled by a router. |
| Responses bytes total | Count | `code`, `method`, `protocol`, `router`, `service` | The total size of HTTP responses in bytes handled by a router. |
```opentelemetry tab="OpenTelemetry"
traefik_router_requests_total
traefik_router_requests_tls_total
traefik_router_request_duration_seconds
traefik_router_requests_bytes_total
traefik_router_responses_bytes_total
```
```prom tab="Prometheus"
traefik_router_requests_total
traefik_router_requests_tls_total
traefik_router_request_duration_seconds
traefik_router_requests_bytes_total
traefik_router_responses_bytes_total
```
```dd tab="Datadog"
router.request.total
router.request.tls.total
router.request.duration
router.requests.bytes.total
router.responses.bytes.total
```
```influxdb tab="InfluxDB2"
traefik.router.requests.total
traefik.router.requests.tls.total
traefik.router.request.duration
traefik.router.requests.bytes.total
traefik.router.responses.bytes.total
```
```statsd tab="StatsD"
# Default prefix: "traefik"
{prefix}.router.request.total
{prefix}.router.request.tls.total
{prefix}.router.request.duration
{prefix}.router.requests.bytes.total
{prefix}.router.responses.bytes.total
```
### Service Metrics
| Metric | Type | Labels | Description |
|-----------------------|-----------|-----------------------------------------|-------------------------------------------------------------|
| Requests total | Count | `code`, `method`, `protocol`, `service` | The total count of HTTP requests processed on a service. |
| Requests TLS total | Count | `tls_version`, `tls_cipher`, `service` | The total count of HTTPS requests processed on a service. |
| Request duration | Histogram | `code`, `method`, `protocol`, `service` | Request processing duration histogram on a service. |
| Retries total | Count | `service` | The count of requests retries on a service. |
| Server UP | Gauge | `service`, `url` | Current service's server status, 0 for a down or 1 for up. |
| Requests bytes total | Count | `code`, `method`, `protocol`, `service` | The total size of requests in bytes received by a service. |
| Responses bytes total | Count | `code`, `method`, `protocol`, `service` | The total size of responses in bytes returned by a service. |
```opentelemetry tab="OpenTelemetry"
traefik_service_requests_total
traefik_service_requests_tls_total
traefik_service_request_duration_seconds
traefik_service_retries_total
traefik_service_server_up
traefik_service_requests_bytes_total
traefik_service_responses_bytes_total
```
```prom tab="Prometheus"
traefik_service_requests_total
traefik_service_requests_tls_total
traefik_service_request_duration_seconds
traefik_service_retries_total
traefik_service_server_up
traefik_service_requests_bytes_total
traefik_service_responses_bytes_total
```
```dd tab="Datadog"
service.request.total
router.service.tls.total
service.request.duration
service.retries.total
service.server.up
service.requests.bytes.total
service.responses.bytes.total
```
```influxdb tab="InfluxDB2"
traefik.service.requests.total
traefik.service.requests.tls.total
traefik.service.request.duration
traefik.service.retries.total
traefik.service.server.up
traefik.service.requests.bytes.total
traefik.service.responses.bytes.total
```
```statsd tab="StatsD"
# Default prefix: "traefik"
{prefix}.service.request.total
{prefix}.service.request.tls.total
{prefix}.service.request.duration
{prefix}.service.retries.total
{prefix}.service.server.up
{prefix}.service.requests.bytes.total
{prefix}.service.responses.bytes.total
```
### Labels
Here is a comprehensive list of labels that are provided by the metrics:
| Label | Description | example |
|---------------|---------------------------------------|----------------------------|
| `cn` | Certificate Common Name | "example.com" |
| `code` | Request code | "200" |
| `entrypoint` | Entrypoint that handled the request | "example_entrypoint" |
| `method` | Request Method | "GET" |
| `protocol` | Request protocol | "http" |
| `router` | Router that handled the request | "example_router" |
| `sans` | Certificate Subject Alternative NameS | "example.com" |
| `serial` | Certificate Serial Number | "123..." |
| `service` | Service that handled the request | "example_service@provider" |
| `tls_cipher` | TLS cipher used for the request | "TLS_FALLBACK_SCSV" |
| `tls_version` | TLS version used for the request | "1.0" |
| `url` | Service server url | "http://example.com" |
!!! info "`method` label value"
If the HTTP method verb on a request is not one defined in the set of common methods for [`HTTP/1.1`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods)
or the [`PRI`](https://datatracker.ietf.org/doc/html/rfc7540#section-11.6) verb (for `HTTP/2`),
then the value for the method label becomes `EXTENSION_METHOD`.

View file

@ -29,7 +29,7 @@ Read the [Access Logs documentation](./access-logs.md) to learn how to configure
Traefik offers a metrics feature that provides valuable insights about the performance and usage. Traefik offers a metrics feature that provides valuable insights about the performance and usage.
These metrics include the number of requests received, the requests duration, and more. These metrics include the number of requests received, the requests duration, and more.
Traefik supports these metrics systems: Prometheus, Datadog, InfluxDB 2.X, and StatsD. On top of supporting metrics in the OpenTelemetry format, Traefik supports the following vendor specific metrics systems: Prometheus, Datadog, InfluxDB 2.X, and StatsD.
Read the [Metrics documentation](./metrics/overview.md) to learn how to configure it. Read the [Metrics documentation](./metrics/overview.md) to learn how to configure it.
@ -37,6 +37,6 @@ Read the [Metrics documentation](./metrics/overview.md) to learn how to configur
The Traefik tracing system allows developers to gain deep visibility into the flow of requests through their infrastructure. The Traefik tracing system allows developers to gain deep visibility into the flow of requests through their infrastructure.
Traefik supports these tracing with OpenTelemetry. Traefik provides tracing information in the OpenTelemery format.
Read the [Tracing documentation](./tracing/overview.md) to learn how to configure it. Read the [Tracing documentation](./tracing/overview.md) to learn how to configure it.

View file

@ -337,6 +337,30 @@ providers:
--providers.kubernetescrd.allowexternalnameservices=true --providers.kubernetescrd.allowexternalnameservices=true
``` ```
### `nativeLBByDefault`
_Optional, Default: false_
Defines whether to use Native Kubernetes load-balancing mode by default.
For more information, please check out the IngressRoute `nativeLB` option [documentation](../routing/providers/kubernetes-crd.md#load-balancing).
```yaml tab="File (YAML)"
providers:
kubernetesCRD:
nativeLBByDefault: true
# ...
```
```toml tab="File (TOML)"
[providers.kubernetesCRD]
nativeLBByDefault = true
# ...
```
```bash tab="CLI"
--providers.kubernetescrd.nativeLBByDefault=true
```
## Full Example ## Full Example
For additional information, refer to the [full example](../user-guides/crd-acme/index.md) with Let's Encrypt. For additional information, refer to the [full example](../user-guides/crd-acme/index.md) with Let's Encrypt.

View file

@ -467,6 +467,30 @@ providers:
--providers.kubernetesingress.allowexternalnameservices=true --providers.kubernetesingress.allowexternalnameservices=true
``` ```
### `nativeLBByDefault`
_Optional, Default: false_
Defines whether to use Native Kubernetes load-balancing mode by default.
For more information, please check out the `traefik.ingress.kubernetes.io/service.nativelb` [service annotation documentation](../routing/providers/kubernetes-ingress.md#on-service).
```yaml tab="File (YAML)"
providers:
kubernetesIngress:
nativeLBByDefault: true
# ...
```
```toml tab="File (TOML)"
[providers.kubernetesIngress]
nativeLBByDefault = true
# ...
```
```bash tab="CLI"
--providers.kubernetesingress.nativeLBByDefault=true
```
### Further ### Further
To learn more about the various aspects of the Ingress specification that Traefik supports, To learn more about the various aspects of the Ingress specification that Traefik supports,

View file

@ -339,6 +339,9 @@ Enable metrics on services. (Default: ```true```)
`--metrics.otlp.explicitboundaries`: `--metrics.otlp.explicitboundaries`:
Boundaries for latency metrics. (Default: ```0.005000, 0.010000, 0.025000, 0.050000, 0.075000, 0.100000, 0.250000, 0.500000, 0.750000, 1.000000, 2.500000, 5.000000, 7.500000, 10.000000```) Boundaries for latency metrics. (Default: ```0.005000, 0.010000, 0.025000, 0.050000, 0.075000, 0.100000, 0.250000, 0.500000, 0.750000, 1.000000, 2.500000, 5.000000, 7.500000, 10.000000```)
`--metrics.otlp.grpc`:
gRPC configuration for the OpenTelemetry collector. (Default: ```false```)
`--metrics.otlp.grpc.endpoint`: `--metrics.otlp.grpc.endpoint`:
Sets the gRPC endpoint (host:port) of the collector. (Default: ```localhost:4317```) Sets the gRPC endpoint (host:port) of the collector. (Default: ```localhost:4317```)
@ -360,6 +363,9 @@ TLS insecure skip verify (Default: ```false```)
`--metrics.otlp.grpc.tls.key`: `--metrics.otlp.grpc.tls.key`:
TLS key TLS key
`--metrics.otlp.http`:
HTTP configuration for the OpenTelemetry collector. (Default: ```false```)
`--metrics.otlp.http.endpoint`: `--metrics.otlp.http.endpoint`:
Sets the HTTP endpoint (scheme://host:port/path) of the collector. (Default: ```https://localhost:4318```) Sets the HTTP endpoint (scheme://host:port/path) of the collector. (Default: ```https://localhost:4318```)
@ -714,6 +720,9 @@ Kubernetes label selector to use.
`--providers.kubernetescrd.namespaces`: `--providers.kubernetescrd.namespaces`:
Kubernetes namespaces. Kubernetes namespaces.
`--providers.kubernetescrd.nativelbbydefault`:
Defines whether to use Native Kubernetes load-balancing mode by default. (Default: ```false```)
`--providers.kubernetescrd.throttleduration`: `--providers.kubernetescrd.throttleduration`:
Ingress refresh throttle duration (Default: ```0```) Ingress refresh throttle duration (Default: ```0```)
@ -795,6 +804,9 @@ Kubernetes Ingress label selector to use.
`--providers.kubernetesingress.namespaces`: `--providers.kubernetesingress.namespaces`:
Kubernetes namespaces. Kubernetes namespaces.
`--providers.kubernetesingress.nativelbbydefault`:
Defines whether to use Native Kubernetes load-balancing mode by default. (Default: ```false```)
`--providers.kubernetesingress.throttleduration`: `--providers.kubernetesingress.throttleduration`:
Ingress refresh throttle duration (Default: ```0```) Ingress refresh throttle duration (Default: ```0```)
@ -1050,6 +1062,9 @@ Defines additional attributes (key:value) on all spans.
`--tracing.otlp`: `--tracing.otlp`:
Settings for OpenTelemetry. (Default: ```false```) Settings for OpenTelemetry. (Default: ```false```)
`--tracing.otlp.grpc`:
gRPC configuration for the OpenTelemetry collector. (Default: ```false```)
`--tracing.otlp.grpc.endpoint`: `--tracing.otlp.grpc.endpoint`:
Sets the gRPC endpoint (host:port) of the collector. (Default: ```localhost:4317```) Sets the gRPC endpoint (host:port) of the collector. (Default: ```localhost:4317```)
@ -1071,6 +1086,9 @@ TLS insecure skip verify (Default: ```false```)
`--tracing.otlp.grpc.tls.key`: `--tracing.otlp.grpc.tls.key`:
TLS key TLS key
`--tracing.otlp.http`:
HTTP configuration for the OpenTelemetry collector. (Default: ```false```)
`--tracing.otlp.http.endpoint`: `--tracing.otlp.http.endpoint`:
Sets the HTTP endpoint (scheme://host:port/path) of the collector. (Default: ```https://localhost:4318```) Sets the HTTP endpoint (scheme://host:port/path) of the collector. (Default: ```https://localhost:4318```)

View file

@ -339,6 +339,9 @@ Enable metrics on services. (Default: ```true```)
`TRAEFIK_METRICS_OTLP_EXPLICITBOUNDARIES`: `TRAEFIK_METRICS_OTLP_EXPLICITBOUNDARIES`:
Boundaries for latency metrics. (Default: ```0.005000, 0.010000, 0.025000, 0.050000, 0.075000, 0.100000, 0.250000, 0.500000, 0.750000, 1.000000, 2.500000, 5.000000, 7.500000, 10.000000```) Boundaries for latency metrics. (Default: ```0.005000, 0.010000, 0.025000, 0.050000, 0.075000, 0.100000, 0.250000, 0.500000, 0.750000, 1.000000, 2.500000, 5.000000, 7.500000, 10.000000```)
`TRAEFIK_METRICS_OTLP_GRPC`:
gRPC configuration for the OpenTelemetry collector. (Default: ```false```)
`TRAEFIK_METRICS_OTLP_GRPC_ENDPOINT`: `TRAEFIK_METRICS_OTLP_GRPC_ENDPOINT`:
Sets the gRPC endpoint (host:port) of the collector. (Default: ```localhost:4317```) Sets the gRPC endpoint (host:port) of the collector. (Default: ```localhost:4317```)
@ -360,6 +363,9 @@ TLS insecure skip verify (Default: ```false```)
`TRAEFIK_METRICS_OTLP_GRPC_TLS_KEY`: `TRAEFIK_METRICS_OTLP_GRPC_TLS_KEY`:
TLS key TLS key
`TRAEFIK_METRICS_OTLP_HTTP`:
HTTP configuration for the OpenTelemetry collector. (Default: ```false```)
`TRAEFIK_METRICS_OTLP_HTTP_ENDPOINT`: `TRAEFIK_METRICS_OTLP_HTTP_ENDPOINT`:
Sets the HTTP endpoint (scheme://host:port/path) of the collector. (Default: ```https://localhost:4318```) Sets the HTTP endpoint (scheme://host:port/path) of the collector. (Default: ```https://localhost:4318```)
@ -714,6 +720,9 @@ Kubernetes label selector to use.
`TRAEFIK_PROVIDERS_KUBERNETESCRD_NAMESPACES`: `TRAEFIK_PROVIDERS_KUBERNETESCRD_NAMESPACES`:
Kubernetes namespaces. Kubernetes namespaces.
`TRAEFIK_PROVIDERS_KUBERNETESCRD_NATIVELBBYDEFAULT`:
Defines whether to use Native Kubernetes load-balancing mode by default. (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESCRD_THROTTLEDURATION`: `TRAEFIK_PROVIDERS_KUBERNETESCRD_THROTTLEDURATION`:
Ingress refresh throttle duration (Default: ```0```) Ingress refresh throttle duration (Default: ```0```)
@ -795,6 +804,9 @@ Kubernetes Ingress label selector to use.
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_NAMESPACES`: `TRAEFIK_PROVIDERS_KUBERNETESINGRESS_NAMESPACES`:
Kubernetes namespaces. Kubernetes namespaces.
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_NATIVELBBYDEFAULT`:
Defines whether to use Native Kubernetes load-balancing mode by default. (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_THROTTLEDURATION`: `TRAEFIK_PROVIDERS_KUBERNETESINGRESS_THROTTLEDURATION`:
Ingress refresh throttle duration (Default: ```0```) Ingress refresh throttle duration (Default: ```0```)
@ -1050,6 +1062,9 @@ Defines additional attributes (key:value) on all spans.
`TRAEFIK_TRACING_OTLP`: `TRAEFIK_TRACING_OTLP`:
Settings for OpenTelemetry. (Default: ```false```) Settings for OpenTelemetry. (Default: ```false```)
`TRAEFIK_TRACING_OTLP_GRPC`:
gRPC configuration for the OpenTelemetry collector. (Default: ```false```)
`TRAEFIK_TRACING_OTLP_GRPC_ENDPOINT`: `TRAEFIK_TRACING_OTLP_GRPC_ENDPOINT`:
Sets the gRPC endpoint (host:port) of the collector. (Default: ```localhost:4317```) Sets the gRPC endpoint (host:port) of the collector. (Default: ```localhost:4317```)
@ -1071,6 +1086,9 @@ TLS insecure skip verify (Default: ```false```)
`TRAEFIK_TRACING_OTLP_GRPC_TLS_KEY`: `TRAEFIK_TRACING_OTLP_GRPC_TLS_KEY`:
TLS key TLS key
`TRAEFIK_TRACING_OTLP_HTTP`:
HTTP configuration for the OpenTelemetry collector. (Default: ```false```)
`TRAEFIK_TRACING_OTLP_HTTP_ENDPOINT`: `TRAEFIK_TRACING_OTLP_HTTP_ENDPOINT`:
Sets the HTTP endpoint (scheme://host:port/path) of the collector. (Default: ```https://localhost:4318```) Sets the HTTP endpoint (scheme://host:port/path) of the collector. (Default: ```https://localhost:4318```)

View file

@ -124,6 +124,7 @@
allowEmptyServices = true allowEmptyServices = true
allowExternalNameServices = true allowExternalNameServices = true
disableIngressClassLookup = true disableIngressClassLookup = true
nativeLBByDefault = true
[providers.kubernetesIngress.ingressEndpoint] [providers.kubernetesIngress.ingressEndpoint]
ip = "foobar" ip = "foobar"
hostname = "foobar" hostname = "foobar"
@ -139,6 +140,7 @@
ingressClass = "foobar" ingressClass = "foobar"
throttleDuration = "42s" throttleDuration = "42s"
allowEmptyServices = true allowEmptyServices = true
nativeLBByDefault = true
[providers.kubernetesGateway] [providers.kubernetesGateway]
endpoint = "foobar" endpoint = "foobar"
token = "foobar" token = "foobar"

View file

@ -141,6 +141,7 @@ providers:
allowEmptyServices: true allowEmptyServices: true
allowExternalNameServices: true allowExternalNameServices: true
disableIngressClassLookup: true disableIngressClassLookup: true
nativeLBByDefault: true
kubernetesCRD: kubernetesCRD:
endpoint: foobar endpoint: foobar
token: foobar token: foobar
@ -154,6 +155,7 @@ providers:
ingressClass: foobar ingressClass: foobar
throttleDuration: 42s throttleDuration: 42s
allowEmptyServices: true allowEmptyServices: true
nativeLBByDefault: true
kubernetesGateway: kubernetesGateway:
endpoint: foobar endpoint: foobar
token: foobar token: foobar

View file

@ -901,15 +901,15 @@ More information in the dedicated [mirroring](../services/index.md#mirroring-ser
spec: spec:
mirroring: mirroring:
name: svc1 name: svc1 # svc1 receives 100% of the traffic
port: 80 port: 80
mirrors: mirrors:
- name: svc2 - name: svc2 # svc2 receives a copy of 20% of this traffic
port: 80 port: 80
percent: 20 percent: 20
- name: svc3 - name: svc3 # svc3 receives a copy of 15% of this traffic
kind: TraefikService kind: TraefikService
percent: 20 percent: 15
``` ```
```yaml tab="Mirroring Traefik Service" ```yaml tab="Mirroring Traefik Service"
@ -922,15 +922,15 @@ More information in the dedicated [mirroring](../services/index.md#mirroring-ser
spec: spec:
mirroring: mirroring:
name: wrr1 name: wrr1 # wrr1 receives 100% of the traffic
kind: TraefikService kind: TraefikService
mirrors: mirrors:
- name: svc2 - name: svc2 # svc2 receives a copy of 20% of this traffic
port: 80 port: 80
percent: 20 percent: 20
- name: svc3 - name: svc3 # svc3 receives a copy of 10% of this traffic
kind: TraefikService kind: TraefikService
percent: 20 percent: 10
``` ```
```yaml tab="K8s Service" ```yaml tab="K8s Service"

View file

@ -273,7 +273,7 @@ Kubernetes cluster before creating `HTTPRoute` objects.
| [6] | `rules` | A list of HTTP matchers, filters and actions. | | [6] | `rules` | A list of HTTP matchers, filters and actions. |
| [7] | `matches` | Conditions used for matching the rule against incoming HTTP requests. Each match is independent, i.e. this rule will be matched if **any** one of the matches is satisfied. | | [7] | `matches` | Conditions used for matching the rule against incoming HTTP requests. Each match is independent, i.e. this rule will be matched if **any** one of the matches is satisfied. |
| [8] | `path` | An HTTP request path matcher. If this field is not specified, a default prefix match on the "/" path is provided. | | [8] | `path` | An HTTP request path matcher. If this field is not specified, a default prefix match on the "/" path is provided. |
| [9] | `type` | Type of match against the path Value (supported types: `Exact`, `PathPrefix`). | | [9] | `type` | Type of match against the path Value (supported types: `Exact`, `PathPrefix`, and `RegularExpression`). |
| [10] | `value` | The value of the HTTP path to match against. | | [10] | `value` | The value of the HTTP path to match against. |
| [11] | `headers` | Conditions to select a HTTP route by matching HTTP request headers. | | [11] | `headers` | Conditions to select a HTTP route by matching HTTP request headers. |
| [12] | `name` | Name of the HTTP header to be matched. | | [12] | `name` | Name of the HTTP header to be matched. |

View file

@ -368,7 +368,7 @@ Path are always starting with a `/`, except for `PathRegexp`.
[case-insensitively](https://en.wikipedia.org/wiki/Case_sensitivity): [case-insensitively](https://en.wikipedia.org/wiki/Case_sensitivity):
```yaml ```yaml
HostRegexp(`(?i)^/products`) PathRegexp(`(?i)^/products`)
``` ```
#### Query and QueryRegexp #### Query and QueryRegexp
@ -827,7 +827,7 @@ http:
``` ```
!!! info "Multiple Hosts in a Rule" !!! info "Multiple Hosts in a Rule"
The rule ```Host(`test1.example.com`,`test2.example.com`)``` will request a certificate with the main domain `test1.example.com` and SAN `test2.example.com`. The rule ```Host(`test1.example.com`) || Host(`test2.example.com`)``` will request a certificate with the main domain `test1.example.com` and SAN `test2.example.com`.
#### `domains` #### `domains`

2
go.mod
View file

@ -55,7 +55,7 @@ require (
github.com/spiffe/go-spiffe/v2 v2.1.1 github.com/spiffe/go-spiffe/v2 v2.1.1
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/stvp/go-udp-testing v0.0.0-20191102171040-06b61409b154 github.com/stvp/go-udp-testing v0.0.0-20191102171040-06b61409b154
github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2 github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046
github.com/testcontainers/testcontainers-go v0.30.0 github.com/testcontainers/testcontainers-go v0.30.0
github.com/testcontainers/testcontainers-go/modules/k3s v0.30.0 github.com/testcontainers/testcontainers-go/modules/k3s v0.30.0
github.com/tetratelabs/wazero v1.5.0 github.com/tetratelabs/wazero v1.5.0

6
go.sum
View file

@ -80,6 +80,7 @@ github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYr
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8=
@ -1072,8 +1073,8 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
github.com/stvp/go-udp-testing v0.0.0-20191102171040-06b61409b154 h1:XGopsea1Dw7ecQ8JscCNQXDGYAKDiWjDeXnpN/+BY9g= github.com/stvp/go-udp-testing v0.0.0-20191102171040-06b61409b154 h1:XGopsea1Dw7ecQ8JscCNQXDGYAKDiWjDeXnpN/+BY9g=
github.com/stvp/go-udp-testing v0.0.0-20191102171040-06b61409b154/go.mod h1:7jxmlfBCDBXRzr0eAQJ48XC1hBu1np4CS5+cHEYfwpc= github.com/stvp/go-udp-testing v0.0.0-20191102171040-06b61409b154/go.mod h1:7jxmlfBCDBXRzr0eAQJ48XC1hBu1np4CS5+cHEYfwpc=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2 h1:xwMw7LFhV9dbvot9A7NLClP9udqbjrQlIwWMH8e7uiQ= github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046 h1:8rUlviSVOEe7TMk7W0gIPrW8MqEzYfZHpsNWSf8s2vg=
github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2/go.mod h1:hL4gB6APAasMR2NNi/JHzqKkxW3EPQlFgLEq9PMi2t0= github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 h1:mmz27tVi2r70JYnm5y0Zk8w0Qzsx+vfUw3oqSyrEfP8= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 h1:mmz27tVi2r70JYnm5y0Zk8w0Qzsx+vfUw3oqSyrEfP8=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 h1:g9SWTaTy/rEuhMErC2jWq9Qt5ci+jBYSvXnJsLq4adg= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 h1:g9SWTaTy/rEuhMErC2jWq9Qt5ci+jBYSvXnJsLq4adg=
@ -1410,7 +1411,6 @@ golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View file

@ -199,21 +199,10 @@ func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() {
RunTest: *k8sConformanceRunTest, RunTest: *k8sConformanceRunTest,
// Until the feature are all supported, following tests are skipped. // Until the feature are all supported, following tests are skipped.
SkipTests: []string{ SkipTests: []string{
tests.GatewayClassObservedGenerationBump.ShortName,
tests.GatewayWithAttachedRoutes.ShortName,
tests.GatewayModifyListeners.ShortName,
tests.GatewayInvalidTLSConfiguration.ShortName,
tests.HTTPRouteHostnameIntersection.ShortName,
tests.HTTPRouteListenerHostnameMatching.ShortName, tests.HTTPRouteListenerHostnameMatching.ShortName,
tests.HTTPRouteInvalidNonExistentBackendRef.ShortName,
tests.HTTPRouteInvalidReferenceGrant.ShortName,
tests.HTTPRouteInvalidCrossNamespaceParentRef.ShortName, tests.HTTPRouteInvalidCrossNamespaceParentRef.ShortName,
tests.HTTPRouteInvalidParentRefNotMatchingSectionName.ShortName,
tests.HTTPRouteInvalidCrossNamespaceBackendRef.ShortName,
tests.HTTPRouteMatchingAcrossRoutes.ShortName, tests.HTTPRouteMatchingAcrossRoutes.ShortName,
tests.HTTPRoutePartiallyInvalidViaInvalidReferenceGrant.ShortName, tests.HTTPRoutePartiallyInvalidViaInvalidReferenceGrant.ShortName,
tests.HTTPRouteRedirectHostAndStatus.ShortName,
tests.HTTPRouteInvalidBackendRefUnknownKind.ShortName,
tests.HTTPRoutePathMatchOrder.ShortName, tests.HTTPRoutePathMatchOrder.ShortName,
tests.HTTPRouteHeaderMatching.ShortName, tests.HTTPRouteHeaderMatching.ShortName,
tests.HTTPRouteReferenceGrant.ShortName, tests.HTTPRouteReferenceGrant.ShortName,

View file

@ -53,6 +53,7 @@
], ],
"service": "api@internal", "service": "api@internal",
"rule": "PathPrefix(`/api`)", "rule": "PathPrefix(`/api`)",
"ruleSyntax": "v3",
"priority": 9223372036854775806, "priority": 9223372036854775806,
"status": "enabled", "status": "enabled",
"using": [ "using": [
@ -69,6 +70,7 @@
], ],
"service": "dashboard@internal", "service": "dashboard@internal",
"rule": "PathPrefix(`/`)", "rule": "PathPrefix(`/`)",
"ruleSyntax": "v3",
"priority": 9223372036854775805, "priority": 9223372036854775805,
"status": "enabled", "status": "enabled",
"using": [ "using": [

View file

@ -53,6 +53,7 @@
], ],
"service": "api@internal", "service": "api@internal",
"rule": "PathPrefix(`/api`)", "rule": "PathPrefix(`/api`)",
"ruleSyntax": "v3",
"priority": 9223372036854775806, "priority": 9223372036854775806,
"status": "enabled", "status": "enabled",
"using": [ "using": [
@ -69,6 +70,7 @@
], ],
"service": "dashboard@internal", "service": "dashboard@internal",
"rule": "PathPrefix(`/`)", "rule": "PathPrefix(`/`)",
"ruleSyntax": "v3",
"priority": 9223372036854775805, "priority": 9223372036854775805,
"status": "enabled", "status": "enabled",
"using": [ "using": [

View file

@ -6,6 +6,7 @@
], ],
"service": "api@internal", "service": "api@internal",
"rule": "PathPrefix(`/api`)", "rule": "PathPrefix(`/api`)",
"ruleSyntax": "v3",
"priority": 9223372036854775806, "priority": 9223372036854775806,
"status": "enabled", "status": "enabled",
"using": [ "using": [
@ -22,6 +23,7 @@
], ],
"service": "dashboard@internal", "service": "dashboard@internal",
"rule": "PathPrefix(`/`)", "rule": "PathPrefix(`/`)",
"ruleSyntax": "v3",
"priority": 9223372036854775805, "priority": 9223372036854775805,
"status": "enabled", "status": "enabled",
"using": [ "using": [

View file

@ -6,6 +6,7 @@
], ],
"service": "api@internal", "service": "api@internal",
"rule": "PathPrefix(`/api`)", "rule": "PathPrefix(`/api`)",
"ruleSyntax": "v3",
"priority": 9223372036854775806, "priority": 9223372036854775806,
"status": "enabled", "status": "enabled",
"using": [ "using": [
@ -22,6 +23,7 @@
], ],
"service": "dashboard@internal", "service": "dashboard@internal",
"rule": "PathPrefix(`/`)", "rule": "PathPrefix(`/`)",
"ruleSyntax": "v3",
"priority": 9223372036854775805, "priority": 9223372036854775805,
"status": "enabled", "status": "enabled",
"using": [ "using": [

View file

@ -6,6 +6,7 @@
], ],
"service": "api@internal", "service": "api@internal",
"rule": "PathPrefix(`/api`)", "rule": "PathPrefix(`/api`)",
"ruleSyntax": "v3",
"priority": 9223372036854775806, "priority": 9223372036854775806,
"status": "enabled", "status": "enabled",
"using": [ "using": [
@ -22,6 +23,7 @@
], ],
"service": "dashboard@internal", "service": "dashboard@internal",
"rule": "PathPrefix(`/`)", "rule": "PathPrefix(`/`)",
"ruleSyntax": "v3",
"priority": 9223372036854775805, "priority": 9223372036854775805,
"status": "enabled", "status": "enabled",
"using": [ "using": [

View file

@ -6,6 +6,7 @@
], ],
"service": "api@internal", "service": "api@internal",
"rule": "PathPrefix(`/api`)", "rule": "PathPrefix(`/api`)",
"ruleSyntax": "v3",
"priority": 9223372036854775806, "priority": 9223372036854775806,
"status": "enabled", "status": "enabled",
"using": [ "using": [
@ -22,6 +23,7 @@
], ],
"service": "dashboard@internal", "service": "dashboard@internal",
"rule": "PathPrefix(`/`)", "rule": "PathPrefix(`/`)",
"ruleSyntax": "v3",
"priority": 9223372036854775805, "priority": 9223372036854775805,
"status": "enabled", "status": "enabled",
"using": [ "using": [

View file

@ -6,6 +6,7 @@
], ],
"service": "api@internal", "service": "api@internal",
"rule": "PathPrefix(`/api`)", "rule": "PathPrefix(`/api`)",
"ruleSyntax": "v3",
"priority": 9223372036854775806, "priority": 9223372036854775806,
"status": "enabled", "status": "enabled",
"using": [ "using": [
@ -22,6 +23,7 @@
], ],
"service": "dashboard@internal", "service": "dashboard@internal",
"rule": "PathPrefix(`/`)", "rule": "PathPrefix(`/`)",
"ruleSyntax": "v3",
"priority": 9223372036854775805, "priority": 9223372036854775805,
"status": "enabled", "status": "enabled",
"using": [ "using": [

View file

@ -53,6 +53,7 @@
], ],
"service": "api@internal", "service": "api@internal",
"rule": "PathPrefix(`/api`)", "rule": "PathPrefix(`/api`)",
"ruleSyntax": "v3",
"priority": 9223372036854775806, "priority": 9223372036854775806,
"status": "enabled", "status": "enabled",
"using": [ "using": [
@ -69,6 +70,7 @@
], ],
"service": "dashboard@internal", "service": "dashboard@internal",
"rule": "PathPrefix(`/`)", "rule": "PathPrefix(`/`)",
"ruleSyntax": "v3",
"priority": 9223372036854775805, "priority": 9223372036854775805,
"status": "enabled", "status": "enabled",
"using": [ "using": [

View file

@ -53,6 +53,7 @@
], ],
"service": "api@internal", "service": "api@internal",
"rule": "PathPrefix(`/api`)", "rule": "PathPrefix(`/api`)",
"ruleSyntax": "v3",
"priority": 9223372036854775806, "priority": 9223372036854775806,
"status": "enabled", "status": "enabled",
"using": [ "using": [
@ -69,6 +70,7 @@
], ],
"service": "dashboard@internal", "service": "dashboard@internal",
"rule": "PathPrefix(`/`)", "rule": "PathPrefix(`/`)",
"ruleSyntax": "v3",
"priority": 9223372036854775805, "priority": 9223372036854775805,
"status": "enabled", "status": "enabled",
"using": [ "using": [

View file

@ -128,6 +128,10 @@ type WeightedRoundRobin struct {
type WRRService struct { type WRRService struct {
Name string `json:"name,omitempty" toml:"name,omitempty" yaml:"name,omitempty" export:"true"` Name string `json:"name,omitempty" toml:"name,omitempty" yaml:"name,omitempty" export:"true"`
Weight *int `json:"weight,omitempty" toml:"weight,omitempty" yaml:"weight,omitempty" export:"true"` Weight *int `json:"weight,omitempty" toml:"weight,omitempty" yaml:"weight,omitempty" export:"true"`
// Status defines an HTTP status code that should be returned when calling the service.
// This is required by the Gateway API implementation which expects specific HTTP status to be returned.
Status *int `json:"-" toml:"-" yaml:"-" label:"-" file:"-"`
} }
// SetDefaults Default values for a WRRService. // SetDefaults Default values for a WRRService.

View file

@ -2189,6 +2189,11 @@ func (in *WRRService) DeepCopyInto(out *WRRService) {
*out = new(int) *out = new(int)
**out = **in **out = **in
} }
if in.Status != nil {
in, out := &in.Status, &out.Status
*out = new(int)
**out = **in
}
return return
} }

View file

@ -0,0 +1,15 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: global-native-lb
namespace: default
spec:
entryPoints:
- foo
routes:
- match: HostSNI(`foo.com`)
services:
- name: native-svc-tcp
port: 8000

View file

@ -0,0 +1,14 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRouteUDP
metadata:
name: global-native-lb
namespace: default
spec:
entryPoints:
- foo
routes:
- services:
- name: native-svc-udp
port: 8000

View file

@ -0,0 +1,16 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: global-native-lb
namespace: default
spec:
entryPoints:
- foo
routes:
- match: Host(`foo.com`)
kind: Rule
services:
- name: native-svc
port: 80

View file

@ -60,6 +60,7 @@ type Provider struct {
IngressClass string `description:"Value of kubernetes.io/ingress.class annotation to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,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"` 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"` 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"`
lastConfiguration safe.Safe lastConfiguration safe.Safe
@ -135,7 +136,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
} }
if p.AllowExternalNameServices { if p.AllowExternalNameServices {
logger.Warn().Msg("ExternalName service loading is enabled, please ensure that this is expected (see AllowExternalNameServices option)") logger.Info().Msg("ExternalName service loading is enabled, please ensure that this is expected (see AllowExternalNameServices option)")
} }
pool.GoCtx(func(ctxPool context.Context) { pool.GoCtx(func(ctxPool context.Context) {

View file

@ -55,6 +55,7 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
allowCrossNamespace: p.AllowCrossNamespace, allowCrossNamespace: p.AllowCrossNamespace,
allowExternalNameServices: p.AllowExternalNameServices, allowExternalNameServices: p.AllowExternalNameServices,
allowEmptyServices: p.AllowEmptyServices, allowEmptyServices: p.AllowEmptyServices,
NativeLBByDefault: p.NativeLBByDefault,
} }
for _, route := range ingressRoute.Spec.Routes { for _, route := range ingressRoute.Spec.Routes {
@ -202,6 +203,7 @@ type configBuilder struct {
allowCrossNamespace bool allowCrossNamespace bool
allowExternalNameServices bool allowExternalNameServices bool
allowEmptyServices bool allowEmptyServices bool
NativeLBByDefault bool
} }
// buildTraefikService creates the configuration for the traefik service defined in tService, // buildTraefikService creates the configuration for the traefik service defined in tService,
@ -377,20 +379,6 @@ func (c configBuilder) loadServers(parentNamespace string, svc traefikv1alpha1.L
return nil, err return nil, err
} }
if svc.NativeLB {
address, err := getNativeServiceAddress(*service, *svcPort)
if err != nil {
return nil, fmt.Errorf("getting native Kubernetes Service address: %w", err)
}
protocol, err := parseServiceProtocol(svc.Scheme, svcPort.Name, svcPort.Port)
if err != nil {
return nil, err
}
return []dynamic.Server{{URL: fmt.Sprintf("%s://%s", protocol, address)}}, nil
}
var servers []dynamic.Server var servers []dynamic.Server
if service.Spec.Type == corev1.ServiceTypeExternalName { if service.Spec.Type == corev1.ServiceTypeExternalName {
if !c.allowExternalNameServices { if !c.allowExternalNameServices {
@ -409,6 +397,24 @@ func (c configBuilder) loadServers(parentNamespace string, svc traefikv1alpha1.L
}), nil }), nil
} }
nativeLB := c.NativeLBByDefault
if svc.NativeLB != nil {
nativeLB = *svc.NativeLB
}
if nativeLB {
address, err := getNativeServiceAddress(*service, *svcPort)
if err != nil {
return nil, fmt.Errorf("getting native Kubernetes Service address: %w", err)
}
protocol, err := parseServiceProtocol(svc.Scheme, svcPort.Name, svcPort.Port)
if err != nil {
return nil, err
}
return []dynamic.Server{{URL: fmt.Sprintf("%s://%s", protocol, address)}}, nil
}
if service.Spec.Type == corev1.ServiceTypeNodePort && svc.NodePortLB { if service.Spec.Type == corev1.ServiceTypeNodePort && svc.NodePortLB {
nodes, nodesExists, nodesErr := c.client.GetNodes() nodes, nodesExists, nodesErr := c.client.GetNodes()
if nodesErr != nil { if nodesErr != nil {

View file

@ -237,15 +237,6 @@ func (p *Provider) loadTCPServers(client Client, namespace string, svc traefikv1
return nil, err return nil, err
} }
if svc.NativeLB {
address, err := getNativeServiceAddress(*service, *svcPort)
if err != nil {
return nil, fmt.Errorf("getting native Kubernetes Service address: %w", err)
}
return []dynamic.TCPServer{{Address: address}}, nil
}
var servers []dynamic.TCPServer var servers []dynamic.TCPServer
if service.Spec.Type == corev1.ServiceTypeNodePort && svc.NodePortLB { if service.Spec.Type == corev1.ServiceTypeNodePort && svc.NodePortLB {
@ -280,6 +271,19 @@ func (p *Provider) loadTCPServers(client Client, namespace string, svc traefikv1
Address: net.JoinHostPort(service.Spec.ExternalName, strconv.Itoa(int(svcPort.Port))), Address: net.JoinHostPort(service.Spec.ExternalName, strconv.Itoa(int(svcPort.Port))),
}) })
} else { } else {
nativeLB := p.NativeLBByDefault
if svc.NativeLB != nil {
nativeLB = *svc.NativeLB
}
if nativeLB {
address, err := getNativeServiceAddress(*service, *svcPort)
if err != nil {
return nil, fmt.Errorf("getting native Kubernetes Service address: %w", err)
}
return []dynamic.TCPServer{{Address: address}}, nil
}
endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, svc.Name) endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, svc.Name)
if endpointsErr != nil { if endpointsErr != nil {
return nil, endpointsErr return nil, endpointsErr

View file

@ -7361,3 +7361,339 @@ func (p *extensionBuilderRegistryMock) RegisterBackendFuncs(group, kind string,
p.groupKindBackendFuncs[group][kind] = builderFunc p.groupKindBackendFuncs[group][kind] = builderFunc
} }
func TestGlobalNativeLB(t *testing.T) {
testCases := []struct {
desc string
ingressClass string
paths []string
NativeLBByDefault bool
expected *dynamic.Configuration
}{
{
desc: "Empty",
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: "HTTP with global native Service LB",
paths: []string{"services.yml", "with_global_native_service_lb.yml"},
NativeLBByDefault: 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{
"default-global-native-lb-6f97418635c7e18853da": {
EntryPoints: []string{"foo"},
Service: "default-global-native-lb-6f97418635c7e18853da",
Rule: "Host(`foo.com`)",
Priority: 0,
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"default-global-native-lb-6f97418635c7e18853da": {
LoadBalancer: &dynamic.ServersLoadBalancer{
ResponseForwarding: &dynamic.ResponseForwarding{FlushInterval: dynamic.DefaultFlushInterval},
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
},
PassHostHeader: Bool(true),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "HTTP with native Service LB in ingressroute",
paths: []string{"services.yml", "with_native_service_lb.yml"},
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{
"default-test-route-6f97418635c7e18853da": {
EntryPoints: []string{"foo"},
Service: "default-test-route-6f97418635c7e18853da",
Rule: "Host(`foo.com`)",
Priority: 0,
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"default-test-route-6f97418635c7e18853da": {
LoadBalancer: &dynamic.ServersLoadBalancer{
ResponseForwarding: &dynamic.ResponseForwarding{FlushInterval: dynamic.DefaultFlushInterval},
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
},
PassHostHeader: Bool(true),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "TCP with global native Service LB",
paths: []string{"tcp/services.yml", "tcp/with_global_native_service_lb.yml"},
NativeLBByDefault: true,
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
HTTP: &dynamic.HTTPConfiguration{
ServersTransports: map[string]*dynamic.ServersTransport{},
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
},
TCP: &dynamic.TCPConfiguration{
ServersTransports: map[string]*dynamic.TCPServersTransport{},
Routers: map[string]*dynamic.TCPRouter{
"default-global-native-lb-fdd3e9338e47a45efefc": {
EntryPoints: []string{"foo"},
Service: "default-global-native-lb-fdd3e9338e47a45efefc",
Rule: "HostSNI(`foo.com`)",
},
},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{
"default-global-native-lb-fdd3e9338e47a45efefc": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{
Address: "10.10.0.1:8000",
Port: "",
},
},
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "TCP with native Service LB in ingressroute",
paths: []string{"tcp/services.yml", "tcp/with_native_service_lb.yml"},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
HTTP: &dynamic.HTTPConfiguration{
ServersTransports: map[string]*dynamic.ServersTransport{},
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
},
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{
"default-test.route-fdd3e9338e47a45efefc": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{
Address: "10.10.0.1:8000",
Port: "",
},
},
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "UDP with native Service LB in ingressroute",
paths: []string{"udp/services.yml", "udp/with_native_service_lb.yml"},
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{
"default-test.route-0": {
LoadBalancer: &dynamic.UDPServersLoadBalancer{
Servers: []dynamic.UDPServer{
{
Address: "10.10.0.1:8000",
Port: "",
},
},
},
},
},
},
HTTP: &dynamic.HTTPConfiguration{
ServersTransports: map[string]*dynamic.ServersTransport{},
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
},
TCP: &dynamic.TCPConfiguration{
ServersTransports: map[string]*dynamic.TCPServersTransport{},
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "UDP with global native Service LB",
paths: []string{"udp/services.yml", "udp/with_global_native_service_lb.yml"},
NativeLBByDefault: true,
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{
"default-global-native-lb-0": {
EntryPoints: []string{"foo"},
Service: "default-global-native-lb-0",
},
},
Services: map[string]*dynamic.UDPService{
"default-global-native-lb-0": {
LoadBalancer: &dynamic.UDPServersLoadBalancer{
Servers: []dynamic.UDPServer{
{
Address: "10.10.0.1:8000",
Port: "",
},
},
},
},
},
},
HTTP: &dynamic.HTTPConfiguration{
ServersTransports: map[string]*dynamic.ServersTransport{},
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
},
TCP: &dynamic.TCPConfiguration{
ServersTransports: map[string]*dynamic.TCPServersTransport{},
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var k8sObjects []runtime.Object
var crdObjects []runtime.Object
for _, path := range test.paths {
yamlContent, err := os.ReadFile(filepath.FromSlash("./fixtures/" + path))
if err != nil {
panic(err)
}
objects := k8s.MustParseYaml(yamlContent)
for _, obj := range objects {
switch o := obj.(type) {
case *corev1.Service, *corev1.Endpoints, *corev1.Secret:
k8sObjects = append(k8sObjects, o)
case *traefikv1alpha1.IngressRoute:
crdObjects = append(crdObjects, o)
case *traefikv1alpha1.IngressRouteTCP:
crdObjects = append(crdObjects, o)
case *traefikv1alpha1.IngressRouteUDP:
crdObjects = append(crdObjects, o)
case *traefikv1alpha1.Middleware:
crdObjects = append(crdObjects, o)
case *traefikv1alpha1.TraefikService:
crdObjects = append(crdObjects, o)
case *traefikv1alpha1.TLSOption:
crdObjects = append(crdObjects, o)
case *traefikv1alpha1.TLSStore:
crdObjects = append(crdObjects, o)
default:
}
}
}
kubeClient := kubefake.NewSimpleClientset(k8sObjects...)
crdClient := traefikcrdfake.NewSimpleClientset(crdObjects...)
client := newClientImpl(kubeClient, crdClient)
stopCh := make(chan struct{})
eventCh, err := client.WatchAll([]string{"default", "cross-ns"}, stopCh)
require.NoError(t, err)
if k8sObjects != nil || crdObjects != nil {
// just wait for the first event
<-eventCh
}
p := Provider{NativeLBByDefault: test.NativeLBByDefault}
conf := p.loadConfigurationFromCRD(context.Background(), client)
assert.Equal(t, test.expected, conf)
})
}
}

View file

@ -121,15 +121,6 @@ func (p *Provider) loadUDPServers(client Client, namespace string, svc traefikv1
return nil, err return nil, err
} }
if svc.NativeLB {
address, err := getNativeServiceAddress(*service, *svcPort)
if err != nil {
return nil, fmt.Errorf("getting native Kubernetes Service address: %w", err)
}
return []dynamic.UDPServer{{Address: address}}, nil
}
var servers []dynamic.UDPServer var servers []dynamic.UDPServer
if service.Spec.Type == corev1.ServiceTypeNodePort && svc.NodePortLB { if service.Spec.Type == corev1.ServiceTypeNodePort && svc.NodePortLB {
@ -164,6 +155,19 @@ func (p *Provider) loadUDPServers(client Client, namespace string, svc traefikv1
Address: net.JoinHostPort(service.Spec.ExternalName, strconv.Itoa(int(svcPort.Port))), Address: net.JoinHostPort(service.Spec.ExternalName, strconv.Itoa(int(svcPort.Port))),
}) })
} else { } else {
nativeLB := p.NativeLBByDefault
if svc.NativeLB != nil {
nativeLB = *svc.NativeLB
}
if nativeLB {
address, err := getNativeServiceAddress(*service, *svcPort)
if err != nil {
return nil, fmt.Errorf("getting native Kubernetes Service address: %w", err)
}
return []dynamic.UDPServer{{Address: address}}, nil
}
endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, svc.Name) endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, svc.Name)
if endpointsErr != nil { if endpointsErr != nil {
return nil, endpointsErr return nil, endpointsErr

View file

@ -125,7 +125,7 @@ type LoadBalancerSpec struct {
// whether the LB's children are directly the pods IPs or if the only child is the Kubernetes Service clusterIP. // whether the LB's children are directly the pods IPs or if the only child is the Kubernetes Service clusterIP.
// The Kubernetes Service itself does load-balance to the pods. // The Kubernetes Service itself does load-balance to the pods.
// By default, NativeLB is false. // By default, NativeLB is false.
NativeLB bool `json:"nativeLB,omitempty"` NativeLB *bool `json:"nativeLB,omitempty"`
// NodePortLB controls, when creating the load-balancer, // NodePortLB controls, when creating the load-balancer,
// whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is NodePort. // whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is NodePort.
// It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes. // It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes.

View file

@ -92,7 +92,7 @@ type ServiceTCP struct {
// whether the LB's children are directly the pods IPs or if the only child is the Kubernetes Service clusterIP. // whether the LB's children are directly the pods IPs or if the only child is the Kubernetes Service clusterIP.
// The Kubernetes Service itself does load-balance to the pods. // The Kubernetes Service itself does load-balance to the pods.
// By default, NativeLB is false. // By default, NativeLB is false.
NativeLB bool `json:"nativeLB,omitempty"` NativeLB *bool `json:"nativeLB,omitempty"`
// NodePortLB controls, when creating the load-balancer, // NodePortLB controls, when creating the load-balancer,
// whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is NodePort. // whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is NodePort.
// It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes. // It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes.

View file

@ -37,7 +37,7 @@ type ServiceUDP struct {
// whether the LB's children are directly the pods IPs or if the only child is the Kubernetes Service clusterIP. // whether the LB's children are directly the pods IPs or if the only child is the Kubernetes Service clusterIP.
// The Kubernetes Service itself does load-balance to the pods. // The Kubernetes Service itself does load-balance to the pods.
// By default, NativeLB is false. // By default, NativeLB is false.
NativeLB bool `json:"nativeLB,omitempty"` NativeLB *bool `json:"nativeLB,omitempty"`
// NodePortLB controls, when creating the load-balancer, // NodePortLB controls, when creating the load-balancer,
// whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is NodePort. // whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is NodePort.
// It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes. // It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes.

View file

@ -577,6 +577,11 @@ func (in *LoadBalancerSpec) DeepCopyInto(out *LoadBalancerSpec) {
*out = new(int) *out = new(int)
**out = **in **out = **in
} }
if in.NativeLB != nil {
in, out := &in.NativeLB, &out.NativeLB
*out = new(bool)
**out = **in
}
return return
} }
@ -1333,6 +1338,11 @@ func (in *ServiceTCP) DeepCopyInto(out *ServiceTCP) {
*out = new(dynamic.ProxyProtocol) *out = new(dynamic.ProxyProtocol)
**out = **in **out = **in
} }
if in.NativeLB != nil {
in, out := &in.NativeLB, &out.NativeLB
*out = new(bool)
**out = **in
}
return return
} }
@ -1355,6 +1365,11 @@ func (in *ServiceUDP) DeepCopyInto(out *ServiceUDP) {
*out = new(int) *out = new(int)
**out = **in **out = **in
} }
if in.NativeLB != nil {
in, out := &in.NativeLB, &out.NativeLB
*out = new(bool)
**out = **in
}
return return
} }

View file

@ -32,11 +32,11 @@ type resourceEventHandler struct {
ev chan<- interface{} ev chan<- interface{}
} }
func (reh *resourceEventHandler) OnAdd(obj interface{}, isInInitialList bool) { func (reh *resourceEventHandler) OnAdd(obj interface{}, _ bool) {
eventHandlerFunc(reh.ev, obj) eventHandlerFunc(reh.ev, obj)
} }
func (reh *resourceEventHandler) OnUpdate(oldObj, newObj interface{}) { func (reh *resourceEventHandler) OnUpdate(_, newObj interface{}) {
eventHandlerFunc(reh.ev, newObj) eventHandlerFunc(reh.ev, newObj)
} }
@ -49,19 +49,21 @@ func (reh *resourceEventHandler) OnDelete(obj interface{}) {
// The stores can then be accessed via the Get* functions. // The stores can then be accessed via the Get* functions.
type Client interface { type Client interface {
WatchAll(namespaces []string, stopCh <-chan struct{}) (<-chan interface{}, error) WatchAll(namespaces []string, stopCh <-chan struct{}) (<-chan interface{}, error)
GetGatewayClasses() ([]*gatev1.GatewayClass, error)
UpdateGatewayStatus(gateway *gatev1.Gateway, gatewayStatus gatev1.GatewayStatus) error UpdateGatewayStatus(gateway *gatev1.Gateway, gatewayStatus gatev1.GatewayStatus) error
UpdateGatewayClassStatus(gatewayClass *gatev1.GatewayClass, condition metav1.Condition) error UpdateGatewayClassStatus(gatewayClass *gatev1.GatewayClass, condition metav1.Condition) error
UpdateHTTPRouteStatus(ctx context.Context, gateway *gatev1.Gateway, nsName ktypes.NamespacedName, status gatev1.HTTPRouteStatus) error UpdateHTTPRouteStatus(ctx context.Context, route ktypes.NamespacedName, status gatev1.HTTPRouteStatus) error
GetGateways() []*gatev1.Gateway UpdateTCPRouteStatus(ctx context.Context, route ktypes.NamespacedName, status gatev1alpha2.TCPRouteStatus) error
GetHTTPRoutes(namespaces []string) ([]*gatev1.HTTPRoute, error) UpdateTLSRouteStatus(ctx context.Context, route ktypes.NamespacedName, status gatev1alpha2.TLSRouteStatus) error
GetTCPRoutes(namespaces []string) ([]*gatev1alpha2.TCPRoute, error) ListGatewayClasses() ([]*gatev1.GatewayClass, error)
GetTLSRoutes(namespaces []string) ([]*gatev1alpha2.TLSRoute, error) ListGateways() []*gatev1.Gateway
GetReferenceGrants(namespace string) ([]*gatev1beta1.ReferenceGrant, error) ListHTTPRoutes() ([]*gatev1.HTTPRoute, error)
ListTCPRoutes() ([]*gatev1alpha2.TCPRoute, error)
ListTLSRoutes() ([]*gatev1alpha2.TLSRoute, error)
ListNamespaces(selector labels.Selector) ([]string, error)
ListReferenceGrants(namespace string) ([]*gatev1beta1.ReferenceGrant, error)
GetService(namespace, name string) (*corev1.Service, bool, error) GetService(namespace, name string) (*corev1.Service, bool, error)
GetSecret(namespace, name string) (*corev1.Secret, bool, error) GetSecret(namespace, name string) (*corev1.Secret, bool, error)
GetEndpoints(namespace, name string) (*corev1.Endpoints, bool, error) GetEndpoints(namespace, name string) (*corev1.Endpoints, bool, error)
GetNamespaces(selector labels.Selector) ([]string, error)
} }
type clientWrapper struct { type clientWrapper struct {
@ -280,7 +282,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
return eventCh, nil return eventCh, nil
} }
func (c *clientWrapper) GetNamespaces(selector labels.Selector) ([]string, error) { func (c *clientWrapper) ListNamespaces(selector labels.Selector) ([]string, error) {
ns, err := c.factoryNamespace.Core().V1().Namespaces().Lister().List(selector) ns, err := c.factoryNamespace.Core().V1().Namespaces().Lister().List(selector)
if err != nil { if err != nil {
return nil, err return nil, err
@ -297,22 +299,12 @@ func (c *clientWrapper) GetNamespaces(selector labels.Selector) ([]string, error
return namespaces, nil return namespaces, nil
} }
func (c *clientWrapper) GetHTTPRoutes(namespaces []string) ([]*gatev1.HTTPRoute, error) { func (c *clientWrapper) ListHTTPRoutes() ([]*gatev1.HTTPRoute, error) {
var httpRoutes []*gatev1.HTTPRoute var httpRoutes []*gatev1.HTTPRoute
for _, namespace := range namespaces { for _, namespace := range c.watchedNamespaces {
if !c.isWatchedNamespace(namespace) {
log.Warn().Msgf("Failed to get HTTPRoutes: %q is not within watched namespaces", namespace)
continue
}
routes, err := c.factoriesGateway[c.lookupNamespace(namespace)].Gateway().V1().HTTPRoutes().Lister().HTTPRoutes(namespace).List(labels.Everything()) routes, err := c.factoriesGateway[c.lookupNamespace(namespace)].Gateway().V1().HTTPRoutes().Lister().HTTPRoutes(namespace).List(labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("listing HTTP routes in namespace %s", namespace)
}
if len(routes) == 0 {
log.Debug().Msgf("No HTTPRoutes found in namespace %q", namespace)
continue
} }
httpRoutes = append(httpRoutes, routes...) httpRoutes = append(httpRoutes, routes...)
@ -321,53 +313,35 @@ func (c *clientWrapper) GetHTTPRoutes(namespaces []string) ([]*gatev1.HTTPRoute,
return httpRoutes, nil return httpRoutes, nil
} }
func (c *clientWrapper) GetTCPRoutes(namespaces []string) ([]*gatev1alpha2.TCPRoute, error) { func (c *clientWrapper) ListTCPRoutes() ([]*gatev1alpha2.TCPRoute, error) {
var tcpRoutes []*gatev1alpha2.TCPRoute var tcpRoutes []*gatev1alpha2.TCPRoute
for _, namespace := range namespaces { for _, namespace := range c.watchedNamespaces {
if !c.isWatchedNamespace(namespace) {
log.Warn().Msgf("Failed to get TCPRoutes: %q is not within watched namespaces", namespace)
continue
}
routes, err := c.factoriesGateway[c.lookupNamespace(namespace)].Gateway().V1alpha2().TCPRoutes().Lister().TCPRoutes(namespace).List(labels.Everything()) routes, err := c.factoriesGateway[c.lookupNamespace(namespace)].Gateway().V1alpha2().TCPRoutes().Lister().TCPRoutes(namespace).List(labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("listing TCP routes in namespace %s", namespace)
}
if len(routes) == 0 {
log.Debug().Msgf("No TCPRoutes found in namespace %q", namespace)
continue
} }
tcpRoutes = append(tcpRoutes, routes...) tcpRoutes = append(tcpRoutes, routes...)
} }
return tcpRoutes, nil return tcpRoutes, nil
} }
func (c *clientWrapper) GetTLSRoutes(namespaces []string) ([]*gatev1alpha2.TLSRoute, error) { func (c *clientWrapper) ListTLSRoutes() ([]*gatev1alpha2.TLSRoute, error) {
var tlsRoutes []*gatev1alpha2.TLSRoute var tlsRoutes []*gatev1alpha2.TLSRoute
for _, namespace := range namespaces { for _, namespace := range c.watchedNamespaces {
if !c.isWatchedNamespace(namespace) {
log.Warn().Msgf("Failed to get TLSRoutes: %q is not within watched namespaces", namespace)
continue
}
routes, err := c.factoriesGateway[c.lookupNamespace(namespace)].Gateway().V1alpha2().TLSRoutes().Lister().TLSRoutes(namespace).List(labels.Everything()) routes, err := c.factoriesGateway[c.lookupNamespace(namespace)].Gateway().V1alpha2().TLSRoutes().Lister().TLSRoutes(namespace).List(labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("listing TLS routes in namespace %s", namespace)
}
if len(routes) == 0 {
log.Debug().Msgf("No TLSRoutes found in namespace %q", namespace)
continue
} }
tlsRoutes = append(tlsRoutes, routes...) tlsRoutes = append(tlsRoutes, routes...)
} }
return tlsRoutes, nil return tlsRoutes, nil
} }
func (c *clientWrapper) GetReferenceGrants(namespace string) ([]*gatev1beta1.ReferenceGrant, error) { func (c *clientWrapper) ListReferenceGrants(namespace string) ([]*gatev1beta1.ReferenceGrant, error) {
if !c.isWatchedNamespace(namespace) { if !c.isWatchedNamespace(namespace) {
log.Warn().Msgf("Failed to get ReferenceGrants: %q is not within watched namespaces", namespace) log.Warn().Msgf("Failed to get ReferenceGrants: %q is not within watched namespaces", namespace)
@ -382,7 +356,7 @@ func (c *clientWrapper) GetReferenceGrants(namespace string) ([]*gatev1beta1.Ref
return referenceGrants, nil return referenceGrants, nil
} }
func (c *clientWrapper) GetGateways() []*gatev1.Gateway { func (c *clientWrapper) ListGateways() []*gatev1.Gateway {
var result []*gatev1.Gateway var result []*gatev1.Gateway
for ns, factory := range c.factoriesGateway { for ns, factory := range c.factoriesGateway {
@ -397,7 +371,7 @@ func (c *clientWrapper) GetGateways() []*gatev1.Gateway {
return result return result
} }
func (c *clientWrapper) GetGatewayClasses() ([]*gatev1.GatewayClass, error) { func (c *clientWrapper) ListGatewayClasses() ([]*gatev1.GatewayClass, error) {
return c.factoryGatewayClass.Gateway().V1().GatewayClasses().Lister().List(labels.Everything()) return c.factoryGatewayClass.Gateway().V1().GatewayClasses().Lister().List(labels.Everything())
} }
@ -437,7 +411,7 @@ func (c *clientWrapper) UpdateGatewayStatus(gateway *gatev1.Gateway, gatewayStat
return fmt.Errorf("cannot update Gateway status %s/%s: namespace is not within watched namespaces", gateway.Namespace, gateway.Name) return fmt.Errorf("cannot update Gateway status %s/%s: namespace is not within watched namespaces", gateway.Namespace, gateway.Name)
} }
if statusEquals(gateway.Status, gatewayStatus) { if gatewayStatusEquals(gateway.Status, gatewayStatus) {
return nil return nil
} }
@ -455,89 +429,106 @@ func (c *clientWrapper) UpdateGatewayStatus(gateway *gatev1.Gateway, gatewayStat
return nil return nil
} }
func (c *clientWrapper) UpdateHTTPRouteStatus(ctx context.Context, gateway *gatev1.Gateway, nsName ktypes.NamespacedName, status gatev1.HTTPRouteStatus) error { func (c *clientWrapper) UpdateHTTPRouteStatus(ctx context.Context, route ktypes.NamespacedName, status gatev1.HTTPRouteStatus) error {
if !c.isWatchedNamespace(nsName.Namespace) { if !c.isWatchedNamespace(route.Namespace) {
return fmt.Errorf("updating HTTPRoute status %s/%s: namespace is not within watched namespaces", nsName.Namespace, nsName.Name) return fmt.Errorf("updating HTTPRoute status %s/%s: namespace is not within watched namespaces", route.Namespace, route.Name)
} }
route, err := c.factoriesGateway[c.lookupNamespace(nsName.Namespace)].Gateway().V1().HTTPRoutes().Lister().HTTPRoutes(nsName.Namespace).Get(nsName.Name) currentRoute, err := c.factoriesGateway[c.lookupNamespace(route.Namespace)].Gateway().V1().HTTPRoutes().Lister().HTTPRoutes(route.Namespace).Get(route.Name)
if err != nil { if err != nil {
return fmt.Errorf("getting HTTPRoute %s/%s: %w", nsName.Namespace, nsName.Name, err) return fmt.Errorf("getting HTTPRoute %s/%s: %w", route.Namespace, route.Name, err)
} }
var statuses []gatev1.RouteParentStatus // TODO: keep statuses for gateways managed by other Traefik instances.
for _, status := range route.Status.Parents { var parentStatuses []gatev1.RouteParentStatus
if status.ControllerName != controllerName { for _, currentParentStatus := range currentRoute.Status.Parents {
statuses = append(statuses, status) if currentParentStatus.ControllerName != controllerName {
continue parentStatuses = append(parentStatuses, currentParentStatus)
}
if status.ParentRef.Namespace != nil && string(*status.ParentRef.Namespace) != gateway.Namespace {
statuses = append(statuses, status)
continue
}
if string(status.ParentRef.Name) != gateway.Name {
statuses = append(statuses, status)
continue continue
} }
} }
statuses = append(statuses, status.Parents...)
route = route.DeepCopy() parentStatuses = append(parentStatuses, status.Parents...)
route.Status = gatev1.HTTPRouteStatus{
currentRoute = currentRoute.DeepCopy()
currentRoute.Status = gatev1.HTTPRouteStatus{
RouteStatus: gatev1.RouteStatus{ RouteStatus: gatev1.RouteStatus{
Parents: statuses, Parents: parentStatuses,
}, },
} }
if _, err := c.csGateway.GatewayV1().HTTPRoutes(nsName.Namespace).UpdateStatus(ctx, route, metav1.UpdateOptions{}); err != nil { if _, err := c.csGateway.GatewayV1().HTTPRoutes(route.Namespace).UpdateStatus(ctx, currentRoute, metav1.UpdateOptions{}); err != nil {
return fmt.Errorf("updating HTTPRoute %s/%s status: %w", nsName.Namespace, nsName.Name, err) return fmt.Errorf("updating HTTPRoute %s/%s status: %w", route.Namespace, route.Name, err)
} }
return nil return nil
} }
func statusEquals(oldStatus, newStatus gatev1.GatewayStatus) bool { func (c *clientWrapper) UpdateTCPRouteStatus(ctx context.Context, route ktypes.NamespacedName, status gatev1alpha2.TCPRouteStatus) error {
if len(oldStatus.Listeners) != len(newStatus.Listeners) { if !c.isWatchedNamespace(route.Namespace) {
return false return fmt.Errorf("updating TCPRoute status %s/%s: namespace is not within watched namespaces", route.Namespace, route.Name)
} }
if !conditionsEquals(oldStatus.Conditions, newStatus.Conditions) { currentRoute, err := c.factoriesGateway[c.lookupNamespace(route.Namespace)].Gateway().V1alpha2().TCPRoutes().Lister().TCPRoutes(route.Namespace).Get(route.Name)
return false if err != nil {
return fmt.Errorf("getting TCPRoute %s/%s: %w", route.Namespace, route.Name, err)
} }
listenerMatches := 0 // TODO: keep statuses for gateways managed by other Traefik instances.
for _, newListener := range newStatus.Listeners { var parentStatuses []gatev1alpha2.RouteParentStatus
for _, oldListener := range oldStatus.Listeners { for _, currentParentStatus := range currentRoute.Status.Parents {
if newListener.Name == oldListener.Name { if currentParentStatus.ControllerName != controllerName {
if !conditionsEquals(newListener.Conditions, oldListener.Conditions) { parentStatuses = append(parentStatuses, currentParentStatus)
return false continue
}
listenerMatches++
}
} }
} }
return listenerMatches == len(oldStatus.Listeners) parentStatuses = append(parentStatuses, status.Parents...)
currentRoute = currentRoute.DeepCopy()
currentRoute.Status = gatev1alpha2.TCPRouteStatus{
RouteStatus: gatev1.RouteStatus{
Parents: parentStatuses,
},
}
if _, err := c.csGateway.GatewayV1alpha2().TCPRoutes(route.Namespace).UpdateStatus(ctx, currentRoute, metav1.UpdateOptions{}); err != nil {
return fmt.Errorf("updating TCPRoute %s/%s status: %w", route.Namespace, route.Name, err)
}
return nil
} }
func conditionsEquals(conditionsA, conditionsB []metav1.Condition) bool { func (c *clientWrapper) UpdateTLSRouteStatus(ctx context.Context, route ktypes.NamespacedName, status gatev1alpha2.TLSRouteStatus) error {
if len(conditionsA) != len(conditionsB) { if !c.isWatchedNamespace(route.Namespace) {
return false return fmt.Errorf("updating TLSRoute status %s/%s: namespace is not within watched namespaces", route.Namespace, route.Name)
} }
conditionMatches := 0 currentRoute, err := c.factoriesGateway[c.lookupNamespace(route.Namespace)].Gateway().V1alpha2().TLSRoutes().Lister().TLSRoutes(route.Namespace).Get(route.Name)
for _, conditionA := range conditionsA { if err != nil {
for _, conditionB := range conditionsB { return fmt.Errorf("getting TLSRoute %s/%s: %w", route.Namespace, route.Name, err)
if conditionA.Type == conditionB.Type {
if conditionA.Reason != conditionB.Reason || conditionA.Status != conditionB.Status || conditionA.Message != conditionB.Message || conditionA.ObservedGeneration != conditionB.ObservedGeneration {
return false
}
conditionMatches++
} }
// TODO: keep statuses for gateways managed by other Traefik instances.
var parentStatuses []gatev1alpha2.RouteParentStatus
for _, currentParentStatus := range currentRoute.Status.Parents {
if currentParentStatus.ControllerName != controllerName {
parentStatuses = append(parentStatuses, currentParentStatus)
continue
} }
} }
return conditionMatches == len(conditionsA) parentStatuses = append(parentStatuses, status.Parents...)
currentRoute = currentRoute.DeepCopy()
currentRoute.Status = gatev1alpha2.TLSRouteStatus{
RouteStatus: gatev1.RouteStatus{
Parents: parentStatuses,
},
}
if _, err := c.csGateway.GatewayV1alpha2().TLSRoutes(route.Namespace).UpdateStatus(ctx, currentRoute, metav1.UpdateOptions{}); err != nil {
return fmt.Errorf("updating TLSRoute %s/%s status: %w", route.Namespace, route.Name, err)
}
return nil
} }
// GetService returns the named service from the given namespace. // GetService returns the named service from the given namespace.
@ -582,11 +573,21 @@ func (c *clientWrapper) GetSecret(namespace, name string) (*corev1.Secret, bool,
// The distinction is necessary because we index all informers on the special // The distinction is necessary because we index all informers on the special
// identifier iff all-namespaces are requested but receive specific namespace // identifier iff all-namespaces are requested but receive specific namespace
// identifiers from the Kubernetes API, so we have to bridge this gap. // identifiers from the Kubernetes API, so we have to bridge this gap.
func (c *clientWrapper) lookupNamespace(ns string) string { func (c *clientWrapper) lookupNamespace(namespace string) string {
if c.isNamespaceAll { if c.isNamespaceAll {
return metav1.NamespaceAll return metav1.NamespaceAll
} }
return ns return namespace
}
// isWatchedNamespace checks to ensure that the namespace is being watched before we request
// it to ensure we don't panic by requesting an out-of-watch object.
func (c *clientWrapper) isWatchedNamespace(namespace string) bool {
if c.isNamespaceAll {
return true
}
return slices.Contains(c.watchedNamespaces, namespace)
} }
// eventHandlerFunc will pass the obj on to the events channel or drop it. // eventHandlerFunc will pass the obj on to the events channel or drop it.
@ -608,12 +609,51 @@ func translateNotFoundError(err error) (bool, error) {
return err == nil, err return err == nil, err
} }
// isWatchedNamespace checks to ensure that the namespace is being watched before we request func gatewayStatusEquals(statusA, statusB gatev1.GatewayStatus) bool {
// it to ensure we don't panic by requesting an out-of-watch object. if len(statusA.Listeners) != len(statusB.Listeners) {
func (c *clientWrapper) isWatchedNamespace(ns string) bool { return false
if c.isNamespaceAll {
return true
} }
return slices.Contains(c.watchedNamespaces, ns) if !conditionsEquals(statusA.Conditions, statusB.Conditions) {
return false
}
listenerMatches := 0
for _, newListener := range statusB.Listeners {
for _, oldListener := range statusA.Listeners {
if newListener.Name == oldListener.Name {
if !conditionsEquals(newListener.Conditions, oldListener.Conditions) {
return false
}
if newListener.AttachedRoutes != oldListener.AttachedRoutes {
return false
}
listenerMatches++
}
}
}
return listenerMatches == len(statusA.Listeners)
}
func conditionsEquals(conditionsA, conditionsB []metav1.Condition) bool {
if len(conditionsA) != len(conditionsB) {
return false
}
conditionMatches := 0
for _, conditionA := range conditionsA {
for _, conditionB := range conditionsB {
if conditionA.Type == conditionB.Type {
if conditionA.Reason != conditionB.Reason || conditionA.Status != conditionB.Status || conditionA.Message != conditionB.Message || conditionA.ObservedGeneration != conditionB.ObservedGeneration {
return false
}
conditionMatches++
}
}
}
return conditionMatches == len(conditionsA)
} }

View file

@ -8,7 +8,7 @@ import (
gatev1 "sigs.k8s.io/gateway-api/apis/v1" gatev1 "sigs.k8s.io/gateway-api/apis/v1"
) )
func TestStatusEquals(t *testing.T) { func Test_gatewayStatusEquals(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
statusA gatev1.GatewayStatus statusA gatev1.GatewayStatus
@ -230,13 +230,45 @@ func TestStatusEquals(t *testing.T) {
}, },
expected: false, expected: false,
}, },
{
desc: "Gateway listeners with same conditions but different number of attached routes",
statusA: gatev1.GatewayStatus{
Listeners: []gatev1.ListenerStatus{
{
Name: "foo",
AttachedRoutes: 1,
Conditions: []metav1.Condition{
{
Type: "foobar",
Reason: "foobar",
},
},
},
},
},
statusB: gatev1.GatewayStatus{
Listeners: []gatev1.ListenerStatus{
{
Name: "foo",
AttachedRoutes: 2,
Conditions: []metav1.Condition{
{
Type: "foobar",
Reason: "foobar",
},
},
},
},
},
expected: false,
},
} }
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
result := statusEquals(test.statusA, test.statusB) result := gatewayStatusEquals(test.statusA, test.statusB)
assert.Equal(t, test.expected, result) assert.Equal(t, test.expected, result)
}) })

View file

@ -1,46 +0,0 @@
---
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:
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:
- "foo.com"
rules:
- matches:
- path:
type: PathPrefix
value: /bar
backendRefs:
- name: whoami
port: 80
weight: 1

View file

@ -68,3 +68,14 @@ spec:
weight: 1 weight: 1
kind: Service kind: Service
group: "" group: ""
- matches:
- path:
type: RegularExpression
value: "^/buzz/[0-9]+$"
backendRefs:
- name: whoami
port: 80
weight: 1
kind: Service
group: ""

View file

@ -1,49 +0,0 @@
---
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: tls
protocol: TLS
port: 9001
tls:
mode: Passthrough
allowedRoutes:
kinds:
- kind: TLSRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: TLSRoute
apiVersion: gateway.networking.k8s.io/v1alpha2
metadata:
name: tls-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- "*.foo.*.bar"
rules:
- backendRefs:
- name: whoamitcp
port: 9000
weight: 1
kind: Service
group: ""

View file

@ -0,0 +1,575 @@
package gateway
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"regexp"
"strconv"
"strings"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/provider"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ktypes "k8s.io/apimachinery/pkg/types"
"k8s.io/utils/ptr"
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
)
func (p *Provider) loadHTTPRoutes(ctx context.Context, client Client, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
routes, err := client.ListHTTPRoutes()
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("Unable to list HTTPRoutes")
return
}
for _, route := range routes {
logger := log.Ctx(ctx).With().
Str("http_route", route.Name).
Str("namespace", route.Namespace).
Logger()
var parentStatuses []gatev1.RouteParentStatus
for _, parentRef := range route.Spec.ParentRefs {
parentStatus := &gatev1.RouteParentStatus{
ParentRef: parentRef,
ControllerName: controllerName,
Conditions: []metav1.Condition{
{
Type: string(gatev1.RouteConditionAccepted),
Status: metav1.ConditionTrue,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonAccepted),
},
},
}
var attachedListeners bool
notAcceptedReason := gatev1.RouteReasonNoMatchingParent
for _, listener := range gatewayListeners {
if !matchListener(listener, route.Namespace, parentRef) {
continue
}
if !allowRoute(listener, route.Namespace, kindHTTPRoute) {
notAcceptedReason = gatev1.RouteReasonNotAllowedByListeners
continue
}
hostnames, ok := findMatchingHostnames(listener.Hostname, route.Spec.Hostnames)
if !ok {
notAcceptedReason = gatev1.RouteReasonNoMatchingListenerHostname
continue
}
listener.Status.AttachedRoutes++
// TODO should we build the conf if the listener is not attached
// only consider the route attached if the listener is in an "attached" state.
if listener.Attached {
attachedListeners = true
}
resolveConditions := p.loadHTTPRoute(logger.WithContext(ctx), client, listener, route, hostnames, conf)
// TODO: handle more accurately route conditions (in case of multiple listener matching).
for _, condition := range resolveConditions {
parentStatus.Conditions = appendCondition(parentStatus.Conditions, condition)
}
}
if !attachedListeners {
parentStatus.Conditions = []metav1.Condition{
{
Type: string(gatev1.RouteConditionAccepted),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(notAcceptedReason),
},
{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonRefNotPermitted),
},
}
}
parentStatuses = append(parentStatuses, *parentStatus)
}
status := gatev1.HTTPRouteStatus{
RouteStatus: gatev1.RouteStatus{
Parents: parentStatuses,
},
}
if err := client.UpdateHTTPRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, status); err != nil {
logger.Error().
Err(err).
Msg("Unable to update HTTPRoute status")
}
}
}
func (p *Provider) loadHTTPRoute(ctx context.Context, client Client, listener gatewayListener, route *gatev1.HTTPRoute, hostnames []gatev1.Hostname, conf *dynamic.Configuration) []metav1.Condition {
routeConditions := []metav1.Condition{
{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionTrue,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteConditionResolvedRefs),
},
}
hostRule := hostRule(hostnames)
for _, routeRule := range route.Spec.Rules {
router := dynamic.Router{
RuleSyntax: "v3",
Rule: routerRule(routeRule, hostRule),
EntryPoints: []string{listener.EPName},
}
if listener.Protocol == gatev1.HTTPSProtocolType {
router.TLS = &dynamic.RouterTLSConfig{}
}
// Adding the gateway desc and the entryPoint desc prevents overlapping of routers build from the same routes.
routerName := route.Name + "-" + listener.GWName + "-" + listener.EPName
routerKey := makeRouterKey(router.Rule, makeID(route.Namespace, routerName))
var wrr dynamic.WeightedRoundRobin
wrrName := provider.Normalize(routerKey + "-wrr")
middlewares, err := p.loadMiddlewares(listener.Protocol, route.Namespace, routerKey, routeRule.Filters)
if err != nil {
log.Ctx(ctx).Error().
Err(err).
Msg("Unable to load HTTPRoute filters")
wrr.Services = append(wrr.Services, dynamic.WRRService{
Name: "invalid-httproute-filter",
Status: ptr.To(500),
Weight: ptr.To(1),
})
conf.HTTP.Services[wrrName] = &dynamic.Service{Weighted: &wrr}
router.Service = wrrName
} else {
for name, middleware := range middlewares {
// If the middleware config is nil in the return of the loadMiddlewares function,
// it means that we just need a reference to that middleware.
if middleware != nil {
conf.HTTP.Middlewares[name] = middleware
}
router.Middlewares = append(router.Middlewares, name)
}
// Traefik internal service can be used only if there is only one BackendRef service reference.
if len(routeRule.BackendRefs) == 1 && isInternalService(routeRule.BackendRefs[0].BackendRef) {
router.Service = string(routeRule.BackendRefs[0].Name)
} else {
for _, backendRef := range routeRule.BackendRefs {
name, svc, errCondition := p.loadHTTPService(client, route, backendRef)
weight := ptr.To(int(ptr.Deref(backendRef.Weight, 1)))
if errCondition != nil {
routeConditions = appendCondition(routeConditions, *errCondition)
wrr.Services = append(wrr.Services, dynamic.WRRService{
Name: name,
Status: ptr.To(500),
Weight: weight,
})
continue
}
if svc != nil {
conf.HTTP.Services[name] = svc
}
wrr.Services = append(wrr.Services, dynamic.WRRService{
Name: name,
Weight: weight,
})
}
conf.HTTP.Services[wrrName] = &dynamic.Service{Weighted: &wrr}
router.Service = wrrName
}
}
rt := &router
p.applyRouterTransform(ctx, rt, route)
routerKey = provider.Normalize(routerKey)
conf.HTTP.Routers[routerKey] = rt
}
return routeConditions
}
// loadHTTPService returns a dynamic.Service config corresponding to the given gatev1.HTTPBackendRef.
// Note that the returned dynamic.Service config can be nil (for cross-provider, internal services, and backendFunc).
func (p *Provider) loadHTTPService(client Client, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef) (string, *dynamic.Service, *metav1.Condition) {
group := groupCore
if backendRef.Group != nil && *backendRef.Group != "" {
group = string(*backendRef.Group)
}
kind := ptr.Deref(backendRef.Kind, "Service")
namespace := ptr.Deref(backendRef.Namespace, gatev1.Namespace(route.Namespace))
namespaceStr := string(namespace)
serviceName := provider.Normalize(makeID(namespaceStr, string(backendRef.Name)))
// TODO support cross namespace through ReferenceGrant.
if namespaceStr != route.Namespace {
return serviceName, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s namespace not allowed", group, kind, namespace, backendRef.Name),
}
}
if group != groupCore || kind != "Service" {
name, service, err := p.loadHTTPBackendRef(namespaceStr, backendRef)
if err != nil {
return serviceName, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonInvalidKind),
Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s: %s", group, kind, namespace, backendRef.Name, err),
}
}
return name, service, nil
}
port := ptr.Deref(backendRef.Port, gatev1.PortNumber(0))
if port == 0 {
return serviceName, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonUnsupportedProtocol),
Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s port is required", group, kind, namespace, backendRef.Name),
}
}
portStr := strconv.FormatInt(int64(port), 10)
serviceName = provider.Normalize(serviceName + "-" + portStr)
lb, err := loadHTTPServers(client, namespaceStr, backendRef)
if err != nil {
return serviceName, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonBackendNotFound),
Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s: %s", group, kind, namespace, backendRef.Name, err),
}
}
return serviceName, &dynamic.Service{LoadBalancer: lb}, nil
}
func (p *Provider) loadHTTPBackendRef(namespace string, backendRef gatev1.HTTPBackendRef) (string, *dynamic.Service, error) {
// Support for cross-provider references (e.g: api@internal).
// This provides the same behavior as for IngressRoutes.
if *backendRef.Kind == "TraefikService" && strings.Contains(string(backendRef.Name), "@") {
return string(backendRef.Name), nil, nil
}
backendFunc, ok := p.groupKindBackendFuncs[string(*backendRef.Group)][string(*backendRef.Kind)]
if !ok {
return "", nil, fmt.Errorf("unsupported HTTPBackendRef %s/%s/%s", *backendRef.Group, *backendRef.Kind, backendRef.Name)
}
if backendFunc == nil {
return "", nil, fmt.Errorf("undefined backendFunc for HTTPBackendRef %s/%s/%s", *backendRef.Group, *backendRef.Kind, backendRef.Name)
}
return backendFunc(string(backendRef.Name), namespace)
}
func (p *Provider) loadMiddlewares(listenerProtocol gatev1.ProtocolType, namespace, prefix string, filters []gatev1.HTTPRouteFilter) (map[string]*dynamic.Middleware, error) {
middlewares := make(map[string]*dynamic.Middleware)
for i, filter := range filters {
switch filter.Type {
case gatev1.HTTPRouteFilterRequestRedirect:
middlewareName := provider.Normalize(fmt.Sprintf("%s-%s-%d", prefix, strings.ToLower(string(filter.Type)), i))
middlewares[middlewareName] = createRedirectRegexMiddleware(listenerProtocol, filter.RequestRedirect)
case gatev1.HTTPRouteFilterRequestHeaderModifier:
middlewareName := provider.Normalize(fmt.Sprintf("%s-%s-%d", prefix, strings.ToLower(string(filter.Type)), i))
middlewares[middlewareName] = createRequestHeaderModifier(filter.RequestHeaderModifier)
case gatev1.HTTPRouteFilterExtensionRef:
name, middleware, err := p.loadHTTPRouteFilterExtensionRef(namespace, filter.ExtensionRef)
if err != nil {
return nil, fmt.Errorf("loading ExtensionRef filter %s: %w", filter.Type, err)
}
middlewares[name] = middleware
default:
// As per the spec: https://gateway-api.sigs.k8s.io/api-types/httproute/#filters-optional
// In all cases where incompatible or unsupported filters are
// specified, implementations MUST add a warning condition to
// status.
return nil, fmt.Errorf("unsupported filter %s", filter.Type)
}
}
return middlewares, nil
}
func (p *Provider) loadHTTPRouteFilterExtensionRef(namespace string, extensionRef *gatev1.LocalObjectReference) (string, *dynamic.Middleware, error) {
if extensionRef == nil {
return "", nil, errors.New("filter extension ref undefined")
}
filterFunc, ok := p.groupKindFilterFuncs[string(extensionRef.Group)][string(extensionRef.Kind)]
if !ok {
return "", nil, fmt.Errorf("unsupported filter extension ref %s/%s/%s", extensionRef.Group, extensionRef.Kind, extensionRef.Name)
}
if filterFunc == nil {
return "", nil, fmt.Errorf("undefined filterFunc for filter extension ref %s/%s/%s", extensionRef.Group, extensionRef.Kind, extensionRef.Name)
}
return filterFunc(string(extensionRef.Name), namespace)
}
// TODO support cross namespace through ReferencePolicy.
func loadHTTPServers(client Client, namespace string, backendRef gatev1.HTTPBackendRef) (*dynamic.ServersLoadBalancer, error) {
service, exists, err := client.GetService(namespace, string(backendRef.Name))
if err != nil {
return nil, fmt.Errorf("getting service: %w", err)
}
if !exists {
return nil, errors.New("service not found")
}
var portSpec corev1.ServicePort
var match bool
for _, p := range service.Spec.Ports {
if backendRef.Port == nil || p.Port == int32(*backendRef.Port) {
portSpec = p
match = true
break
}
}
if !match {
return nil, errors.New("service port not found")
}
endpoints, endpointsExists, err := client.GetEndpoints(namespace, string(backendRef.Name))
if err != nil {
return nil, fmt.Errorf("getting endpoints: %w", err)
}
if !endpointsExists {
return nil, errors.New("endpoints not found")
}
if len(endpoints.Subsets) == 0 {
return nil, errors.New("subset not found")
}
lb := &dynamic.ServersLoadBalancer{}
lb.SetDefaults()
var port int32
var portStr string
for _, subset := range endpoints.Subsets {
for _, p := range subset.Ports {
if portSpec.Name == p.Name {
port = p.Port
break
}
}
if port == 0 {
return nil, errors.New("cannot define a port")
}
protocol := getProtocol(portSpec)
portStr = strconv.FormatInt(int64(port), 10)
for _, addr := range subset.Addresses {
lb.Servers = append(lb.Servers, dynamic.Server{
URL: fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(addr.IP, portStr)),
})
}
}
return lb, nil
}
func hostRule(hostnames []gatev1.Hostname) string {
var rules []string
for _, hostname := range hostnames {
host := string(hostname)
wildcard := strings.Count(host, "*")
if wildcard == 0 {
rules = append(rules, fmt.Sprintf("Host(`%s`)", host))
continue
}
host = strings.Replace(regexp.QuoteMeta(host), `\*\.`, `[a-z0-9-\.]+\.`, 1)
rules = append(rules, fmt.Sprintf("HostRegexp(`^%s$`)", host))
}
switch len(rules) {
case 0:
return ""
case 1:
return rules[0]
default:
return fmt.Sprintf("(%s)", strings.Join(rules, " || "))
}
}
func routerRule(routeRule gatev1.HTTPRouteRule, hostRule string) string {
var rule string
var matchesRules []string
for _, match := range routeRule.Matches {
path := ptr.Deref(match.Path, gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchPathPrefix),
Value: ptr.To("/"),
})
pathType := ptr.Deref(path.Type, gatev1.PathMatchPathPrefix)
pathValue := ptr.Deref(path.Value, "/")
var matchRules []string
switch pathType {
case gatev1.PathMatchExact:
matchRules = append(matchRules, fmt.Sprintf("Path(`%s`)", pathValue))
case gatev1.PathMatchPathPrefix:
matchRules = append(matchRules, buildPathMatchPathPrefixRule(pathValue))
case gatev1.PathMatchRegularExpression:
matchRules = append(matchRules, fmt.Sprintf("PathRegexp(`%s`)", pathValue))
}
matchRules = append(matchRules, headerRules(match.Headers)...)
matchesRules = append(matchesRules, strings.Join(matchRules, " && "))
}
// If no matches are specified, the default is a prefix
// path match on "/", which has the effect of matching every
// HTTP request.
if len(routeRule.Matches) == 0 {
matchesRules = append(matchesRules, "PathPrefix(`/`)")
}
if hostRule != "" {
if len(matchesRules) == 0 {
return hostRule
}
rule += hostRule + " && "
}
if len(matchesRules) == 1 {
return rule + matchesRules[0]
}
if len(rule) == 0 {
return strings.Join(matchesRules, " || ")
}
return rule + "(" + strings.Join(matchesRules, " || ") + ")"
}
func headerRules(headers []gatev1.HTTPHeaderMatch) []string {
var headerRules []string
for _, header := range headers {
typ := ptr.Deref(header.Type, gatev1.HeaderMatchExact)
switch typ {
case gatev1.HeaderMatchExact:
headerRules = append(headerRules, fmt.Sprintf("Header(`%s`,`%s`)", header.Name, header.Value))
case gatev1.HeaderMatchRegularExpression:
headerRules = append(headerRules, fmt.Sprintf("HeaderRegexp(`%s`,`%s`)", header.Name, header.Value))
}
}
return headerRules
}
func buildPathMatchPathPrefixRule(path string) string {
if path == "/" {
return "PathPrefix(`/`)"
}
path = strings.TrimSuffix(path, "/")
return fmt.Sprintf("(Path(`%[1]s`) || PathPrefix(`%[1]s/`))", path)
}
// createRequestHeaderModifier 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 createRequestHeaderModifier(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{
RequestHeaderModifier: &dynamic.RequestHeaderModifier{
Set: sets,
Add: adds,
Remove: filter.Remove,
},
}
}
func createRedirectRegexMiddleware(listenerProtocol gatev1.ProtocolType, filter *gatev1.HTTPRequestRedirectFilter) *dynamic.Middleware {
// The spec allows for an empty string in which case we should use the
// scheme of the request which in this case is the listener scheme.
filterScheme := ptr.Deref(filter.Scheme, strings.ToLower(string(listenerProtocol)))
statusCode := ptr.Deref(filter.StatusCode, http.StatusFound)
port := "${port}"
if filter.Port != nil {
port = fmt.Sprintf(":%d", *filter.Port)
}
hostname := "${hostname}"
if filter.Hostname != nil && *filter.Hostname != "" {
hostname = string(*filter.Hostname)
}
return &dynamic.Middleware{
RedirectRegex: &dynamic.RedirectRegex{
Regex: `^[a-z]+:\/\/(?P<userInfo>.+@)?(?P<hostname>\[[\w:\.]+\]|[\w\._-]+)(?P<port>:\d+)?\/(?P<path>.*)`,
Replacement: fmt.Sprintf("%s://${userinfo}%s%s/${path}", filterScheme, hostname, port),
Permanent: statusCode == http.StatusMovedPermanently,
},
}
}
func getProtocol(portSpec corev1.ServicePort) string {
protocol := "http"
if portSpec.Port == 443 || strings.HasPrefix(portSpec.Name, "https") {
protocol = "https"
}
return protocol
}

View file

@ -0,0 +1,281 @@
package gateway
import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/utils/ptr"
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
)
func Test_hostRule(t *testing.T) {
testCases := []struct {
desc string
hostnames []gatev1.Hostname
expectedRule string
expectErr bool
}{
{
desc: "Empty rule and matches",
expectedRule: "",
},
{
desc: "One Host",
hostnames: []gatev1.Hostname{
"Foo",
},
expectedRule: "Host(`Foo`)",
},
{
desc: "Multiple Hosts",
hostnames: []gatev1.Hostname{
"Foo",
"Bar",
"Bir",
},
expectedRule: "(Host(`Foo`) || Host(`Bar`) || Host(`Bir`))",
},
{
desc: "Several Host and wildcard",
hostnames: []gatev1.Hostname{
"*.bar.foo",
"bar.foo",
"foo.foo",
},
expectedRule: "(HostRegexp(`^[a-z0-9-\\.]+\\.bar\\.foo$`) || Host(`bar.foo`) || Host(`foo.foo`))",
},
{
desc: "Host with wildcard",
hostnames: []gatev1.Hostname{
"*.bar.foo",
},
expectedRule: "HostRegexp(`^[a-z0-9-\\.]+\\.bar\\.foo$`)",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
rule := hostRule(test.hostnames)
assert.Equal(t, test.expectedRule, rule)
})
}
}
func Test_routerRule(t *testing.T) {
testCases := []struct {
desc string
routeRule gatev1.HTTPRouteRule
hostRule string
expectedRule string
expectedError bool
}{
{
desc: "Empty rule and matches",
expectedRule: "PathPrefix(`/`)",
},
{
desc: "One Host rule without matches",
hostRule: "Host(`foo.com`)",
expectedRule: "Host(`foo.com`) && PathPrefix(`/`)",
},
{
desc: "One HTTPRouteMatch with nil HTTPHeaderMatch",
routeRule: gatev1.HTTPRouteRule{
Matches: []gatev1.HTTPRouteMatch{
{
Path: ptr.To(gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchPathPrefix),
Value: ptr.To("/"),
}),
Headers: nil,
},
},
},
expectedRule: "PathPrefix(`/`)",
},
{
desc: "One HTTPRouteMatch with nil HTTPHeaderMatch Type",
routeRule: gatev1.HTTPRouteRule{
Matches: []gatev1.HTTPRouteMatch{
{
Path: ptr.To(gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchPathPrefix),
Value: ptr.To("/"),
}),
Headers: []gatev1.HTTPHeaderMatch{
{Name: "foo", Value: "bar"},
},
},
},
},
expectedRule: "PathPrefix(`/`) && Header(`foo`,`bar`)",
},
{
desc: "One HTTPRouteMatch with nil HTTPPathMatch",
routeRule: gatev1.HTTPRouteRule{
Matches: []gatev1.HTTPRouteMatch{
{Path: nil},
},
},
expectedRule: "PathPrefix(`/`)",
},
{
desc: "One HTTPRouteMatch with nil HTTPPathMatch Type",
routeRule: gatev1.HTTPRouteRule{
Matches: []gatev1.HTTPRouteMatch{
{
Path: &gatev1.HTTPPathMatch{
Type: nil,
Value: ptr.To("/foo/"),
},
},
},
},
expectedRule: "(Path(`/foo`) || PathPrefix(`/foo/`))",
},
{
desc: "One HTTPRouteMatch with nil HTTPPathMatch Values",
routeRule: gatev1.HTTPRouteRule{
Matches: []gatev1.HTTPRouteMatch{
{
Path: &gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchExact),
Value: nil,
},
},
},
},
expectedRule: "Path(`/`)",
},
{
desc: "One Path in matches",
routeRule: gatev1.HTTPRouteRule{
Matches: []gatev1.HTTPRouteMatch{
{
Path: &gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchExact),
Value: ptr.To("/foo/"),
},
},
},
},
expectedRule: "Path(`/foo/`)",
},
{
desc: "One Path in matches and another empty",
routeRule: gatev1.HTTPRouteRule{
Matches: []gatev1.HTTPRouteMatch{
{
Path: &gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchExact),
Value: ptr.To("/foo/"),
},
},
{},
},
},
expectedRule: "Path(`/foo/`) || PathPrefix(`/`)",
},
{
desc: "Path OR Header rules",
routeRule: gatev1.HTTPRouteRule{
Matches: []gatev1.HTTPRouteMatch{
{
Path: &gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchExact),
Value: ptr.To("/foo/"),
},
},
{
Headers: []gatev1.HTTPHeaderMatch{
{
Type: ptr.To(gatev1.HeaderMatchExact),
Name: "my-header",
Value: "foo",
},
},
},
},
},
expectedRule: "Path(`/foo/`) || PathPrefix(`/`) && Header(`my-header`,`foo`)",
},
{
desc: "Path && Header rules",
routeRule: gatev1.HTTPRouteRule{
Matches: []gatev1.HTTPRouteMatch{
{
Path: &gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchExact),
Value: ptr.To("/foo/"),
},
Headers: []gatev1.HTTPHeaderMatch{
{
Type: ptr.To(gatev1.HeaderMatchExact),
Name: "my-header",
Value: "foo",
},
},
},
},
},
expectedRule: "Path(`/foo/`) && Header(`my-header`,`foo`)",
},
{
desc: "Host && Path && Header rules",
hostRule: "Host(`foo.com`)",
routeRule: gatev1.HTTPRouteRule{
Matches: []gatev1.HTTPRouteMatch{
{
Path: &gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchExact),
Value: ptr.To("/foo/"),
},
Headers: []gatev1.HTTPHeaderMatch{
{
Type: ptr.To(gatev1.HeaderMatchExact),
Name: "my-header",
Value: "foo",
},
},
},
},
},
expectedRule: "Host(`foo.com`) && Path(`/foo/`) && Header(`my-header`,`foo`)",
},
{
desc: "Host && (Path || Header) rules",
hostRule: "Host(`foo.com`)",
routeRule: gatev1.HTTPRouteRule{
Matches: []gatev1.HTTPRouteMatch{
{
Path: &gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchExact),
Value: ptr.To("/foo/"),
},
},
{
Headers: []gatev1.HTTPHeaderMatch{
{
Type: ptr.To(gatev1.HeaderMatchExact),
Name: "my-header",
Value: "foo",
},
},
},
},
},
expectedRule: "Host(`foo.com`) && (Path(`/foo/`) || PathPrefix(`/`) && Header(`my-header`,`foo`))",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
rule := routerRule(test.routeRule, test.hostRule)
assert.Equal(t, test.expectedRule, rule)
})
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,296 @@
package gateway
import (
"context"
"errors"
"fmt"
"net"
"strconv"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/provider"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ktypes "k8s.io/apimachinery/pkg/types"
"k8s.io/utils/ptr"
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
gatev1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
)
func (p *Provider) loadTCPRoutes(ctx context.Context, client Client, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
logger := log.Ctx(ctx)
routes, err := client.ListTCPRoutes()
if err != nil {
logger.Error().Err(err).Msgf("Get TCPRoutes: %s", err)
}
for _, route := range routes {
logger := log.Ctx(ctx).With().Str("tcproute", route.Name).Str("namespace", route.Namespace).Logger()
var parentStatuses []gatev1alpha2.RouteParentStatus
for _, parentRef := range route.Spec.ParentRefs {
parentStatus := &gatev1alpha2.RouteParentStatus{
ParentRef: parentRef,
ControllerName: controllerName,
Conditions: []metav1.Condition{
{
Type: string(gatev1.RouteConditionAccepted),
Status: metav1.ConditionTrue,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonAccepted),
},
},
}
var attachedListeners bool
for _, listener := range gatewayListeners {
if !matchListener(listener, route.Namespace, parentRef) {
continue
}
if !allowRoute(listener, route.Namespace, kindTCPRoute) {
continue
}
listener.Status.AttachedRoutes++
attachedListeners = true
resolveConditions := p.loadTCPRoute(client, listener, route, conf)
// TODO: handle more accurately route conditions (in case of multiple listener matching).
for _, condition := range resolveConditions {
parentStatus.Conditions = appendCondition(parentStatus.Conditions, condition)
}
}
if !attachedListeners {
parentStatus.Conditions = []metav1.Condition{
{
Type: string(gatev1.RouteConditionAccepted),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonNoMatchingParent),
},
}
}
parentStatuses = append(parentStatuses, *parentStatus)
}
routeStatus := gatev1alpha2.TCPRouteStatus{
RouteStatus: gatev1alpha2.RouteStatus{
Parents: parentStatuses,
},
}
if err := client.UpdateTCPRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, routeStatus); err != nil {
logger.Error().
Err(err).
Msg("Unable to update TCPRoute status")
}
}
}
func (p *Provider) loadTCPRoute(client Client, listener gatewayListener, route *gatev1alpha2.TCPRoute, conf *dynamic.Configuration) []metav1.Condition {
routeConditions := []metav1.Condition{
{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionTrue,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteConditionResolvedRefs),
},
}
router := dynamic.TCPRouter{
Rule: "HostSNI(`*`)",
EntryPoints: []string{listener.EPName},
RuleSyntax: "v3",
}
if listener.Protocol == gatev1.TLSProtocolType && listener.TLS != nil {
// TODO support let's encrypt
router.TLS = &dynamic.RouterTCPTLSConfig{
Passthrough: listener.TLS.Mode != nil && *listener.TLS.Mode == gatev1.TLSModePassthrough,
}
}
// Adding the gateway desc and the entryPoint desc prevents overlapping of routers build from the same routes.
routerName := route.Name + "-" + listener.GWName + "-" + listener.EPName
routerKey := provider.Normalize(makeRouterKey("", makeID(route.Namespace, routerName)))
var ruleServiceNames []string
for i, rule := range route.Spec.Rules {
if rule.BackendRefs == nil {
// Should not happen due to validation.
// https://github.com/kubernetes-sigs/gateway-api/blob/v0.4.0/apis/v1alpha2/tcproute_types.go#L76
continue
}
wrrService, subServices, err := loadTCPServices(client, route.Namespace, rule.BackendRefs)
if err != nil {
routeConditions = appendCondition(routeConditions, metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonBackendNotFound),
Message: fmt.Sprintf("Cannot load TCPRoute service %s/%s: %v", route.Namespace, route.Name, err),
})
return routeConditions
}
for svcName, svc := range subServices {
conf.TCP.Services[svcName] = svc
}
serviceName := fmt.Sprintf("%s-wrr-%d", routerKey, i)
conf.TCP.Services[serviceName] = wrrService
ruleServiceNames = append(ruleServiceNames, serviceName)
}
if len(ruleServiceNames) == 1 {
router.Service = ruleServiceNames[0]
conf.TCP.Routers[routerKey] = &router
return routeConditions
}
routeServiceKey := routerKey + "-wrr"
routeService := &dynamic.TCPService{Weighted: &dynamic.TCPWeightedRoundRobin{}}
for _, name := range ruleServiceNames {
service := dynamic.TCPWRRService{Name: name}
service.SetDefaults()
routeService.Weighted.Services = append(routeService.Weighted.Services, service)
}
conf.TCP.Services[routeServiceKey] = routeService
router.Service = routeServiceKey
conf.TCP.Routers[routerKey] = &router
return routeConditions
}
// loadTCPServices is generating a WRR service, even when there is only one target.
func loadTCPServices(client Client, namespace string, backendRefs []gatev1.BackendRef) (*dynamic.TCPService, map[string]*dynamic.TCPService, error) {
services := map[string]*dynamic.TCPService{}
wrrSvc := &dynamic.TCPService{
Weighted: &dynamic.TCPWeightedRoundRobin{
Services: []dynamic.TCPWRRService{},
},
}
for _, backendRef := range backendRefs {
if backendRef.Group == nil || backendRef.Kind == nil {
// Should not happen as this is validated by kubernetes
continue
}
if isInternalService(backendRef) {
return nil, nil, fmt.Errorf("traefik internal service %s is not allowed in a WRR loadbalancer", backendRef.Name)
}
weight := int(ptr.Deref(backendRef.Weight, 1))
if isTraefikService(backendRef) {
wrrSvc.Weighted.Services = append(wrrSvc.Weighted.Services, dynamic.TCPWRRService{Name: string(backendRef.Name), Weight: &weight})
continue
}
if *backendRef.Group != "" && *backendRef.Group != groupCore && *backendRef.Kind != "Service" {
return nil, nil, fmt.Errorf("unsupported BackendRef %s/%s/%s", *backendRef.Group, *backendRef.Kind, backendRef.Name)
}
svc := dynamic.TCPService{
LoadBalancer: &dynamic.TCPServersLoadBalancer{},
}
service, exists, err := client.GetService(namespace, string(backendRef.Name))
if err != nil {
return nil, nil, err
}
if !exists {
return nil, nil, errors.New("service not found")
}
if len(service.Spec.Ports) > 1 && backendRef.Port == nil {
// If the port is unspecified and the backend is a Service
// object consisting of multiple port definitions, the route
// must be dropped from the Gateway. The controller should
// raise the "ResolvedRefs" condition on the Gateway with the
// "DroppedRoutes" reason. The gateway status for this route
// should be updated with a condition that describes the error
// more specifically.
log.Error().Msg("A multiple ports Kubernetes Service cannot be used if unspecified backendRef.Port")
continue
}
var portSpec corev1.ServicePort
var match bool
for _, p := range service.Spec.Ports {
if backendRef.Port == nil || p.Port == int32(*backendRef.Port) {
portSpec = p
match = true
break
}
}
if !match {
return nil, nil, errors.New("service port not found")
}
endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, string(backendRef.Name))
if endpointsErr != nil {
return nil, nil, endpointsErr
}
if !endpointsExists {
return nil, nil, errors.New("endpoints not found")
}
if len(endpoints.Subsets) == 0 {
return nil, nil, errors.New("subset not found")
}
var port int32
var portStr string
for _, subset := range endpoints.Subsets {
for _, p := range subset.Ports {
if portSpec.Name == p.Name {
port = p.Port
break
}
}
if port == 0 {
return nil, nil, errors.New("cannot define a port")
}
portStr = strconv.FormatInt(int64(port), 10)
for _, addr := range subset.Addresses {
svc.LoadBalancer.Servers = append(svc.LoadBalancer.Servers, dynamic.TCPServer{
Address: net.JoinHostPort(addr.IP, portStr),
})
}
}
serviceName := provider.Normalize(makeID(service.Namespace, service.Name) + "-" + portStr)
services[serviceName] = &svc
wrrSvc.Weighted.Services = append(wrrSvc.Weighted.Services, dynamic.TCPWRRService{Name: serviceName, Weight: &weight})
}
if len(wrrSvc.Weighted.Services) == 0 {
return nil, nil, errors.New("no service has been created")
}
return wrrSvc, services, nil
}

View file

@ -0,0 +1,210 @@
package gateway
import (
"context"
"fmt"
"regexp"
"strings"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/provider"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ktypes "k8s.io/apimachinery/pkg/types"
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
gatev1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
)
func (p *Provider) loadTLSRoutes(ctx context.Context, client Client, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
logger := log.Ctx(ctx)
routes, err := client.ListTLSRoutes()
if err != nil {
logger.Error().Err(err).Msgf("Get TLSRoutes: %s", err)
}
for _, route := range routes {
logger := log.Ctx(ctx).With().Str("tlsroute", route.Name).Str("namespace", route.Namespace).Logger()
var parentStatuses []gatev1alpha2.RouteParentStatus
for _, parentRef := range route.Spec.ParentRefs {
parentStatus := &gatev1alpha2.RouteParentStatus{
ParentRef: parentRef,
ControllerName: controllerName,
Conditions: []metav1.Condition{
{
Type: string(gatev1.RouteConditionAccepted),
Status: metav1.ConditionTrue,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonAccepted),
},
},
}
var attachedListeners bool
for _, listener := range gatewayListeners {
if !matchListener(listener, route.Namespace, parentRef) {
continue
}
if !allowRoute(listener, route.Namespace, kindTLSRoute) {
continue
}
hostnames, ok := findMatchingHostnames(listener.Hostname, route.Spec.Hostnames)
if !ok {
continue
}
listener.Status.AttachedRoutes++
attachedListeners = true
resolveConditions := p.loadTLSRoute(client, listener, route, hostnames, conf)
// TODO: handle more accurately route conditions (in case of multiple listener matching).
for _, condition := range resolveConditions {
parentStatus.Conditions = appendCondition(parentStatus.Conditions, condition)
}
}
if !attachedListeners {
parentStatus.Conditions = []metav1.Condition{
{
Type: string(gatev1.RouteConditionAccepted),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonNoMatchingParent),
},
}
}
parentStatuses = append(parentStatuses, *parentStatus)
}
routeStatus := gatev1alpha2.TLSRouteStatus{
RouteStatus: gatev1alpha2.RouteStatus{
Parents: parentStatuses,
},
}
if err := client.UpdateTLSRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, routeStatus); err != nil {
logger.Error().
Err(err).
Msg("Unable to update TLSRoute status")
}
}
}
func (p *Provider) loadTLSRoute(client Client, listener gatewayListener, route *gatev1alpha2.TLSRoute, hostnames []gatev1.Hostname, conf *dynamic.Configuration) []metav1.Condition {
routeConditions := []metav1.Condition{
{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionTrue,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteConditionResolvedRefs),
},
}
router := dynamic.TCPRouter{
RuleSyntax: "v3",
Rule: hostSNIRule(hostnames),
EntryPoints: []string{listener.EPName},
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: listener.TLS.Mode != nil && *listener.TLS.Mode == gatev1.TLSModePassthrough,
},
}
// Adding the gateway desc and the entryPoint desc prevents overlapping of routers build from the same routes.
routerName := route.Name + "-" + listener.GWName + "-" + listener.EPName
routerKey := provider.Normalize(makeRouterKey(router.Rule, makeID(route.Namespace, routerName)))
var ruleServiceNames []string
for i, routeRule := range route.Spec.Rules {
if len(routeRule.BackendRefs) == 0 {
// Should not happen due to validation.
// https://github.com/kubernetes-sigs/gateway-api/blob/v0.4.0/apis/v1alpha2/tlsroute_types.go#L120
continue
}
wrrService, subServices, err := loadTCPServices(client, route.Namespace, routeRule.BackendRefs)
if err != nil {
// update "ResolvedRefs" status true with "InvalidBackendRefs" reason
routeConditions = appendCondition(routeConditions, metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonBackendNotFound),
Message: fmt.Sprintf("Cannot load TLSRoute service %s/%s: %v", route.Namespace, route.Name, err),
})
continue
}
for svcName, svc := range subServices {
conf.TCP.Services[svcName] = svc
}
serviceName := fmt.Sprintf("%s-wrr-%d", routerKey, i)
conf.TCP.Services[serviceName] = wrrService
ruleServiceNames = append(ruleServiceNames, serviceName)
}
if len(ruleServiceNames) == 1 {
router.Service = ruleServiceNames[0]
conf.TCP.Routers[routerKey] = &router
return routeConditions
}
routeServiceKey := routerKey + "-wrr"
routeService := &dynamic.TCPService{Weighted: &dynamic.TCPWeightedRoundRobin{}}
for _, name := range ruleServiceNames {
service := dynamic.TCPWRRService{Name: name}
service.SetDefaults()
routeService.Weighted.Services = append(routeService.Weighted.Services, service)
}
conf.TCP.Services[routeServiceKey] = routeService
router.Service = routeServiceKey
conf.TCP.Routers[routerKey] = &router
return routeConditions
}
func hostSNIRule(hostnames []gatev1.Hostname) string {
rules := make([]string, 0, len(hostnames))
uniqHostnames := map[gatev1.Hostname]struct{}{}
for _, hostname := range hostnames {
if len(hostname) == 0 {
continue
}
if _, exists := uniqHostnames[hostname]; exists {
continue
}
host := string(hostname)
uniqHostnames[hostname] = struct{}{}
wildcard := strings.Count(host, "*")
if wildcard == 0 {
rules = append(rules, fmt.Sprintf("HostSNI(`%s`)", host))
continue
}
host = strings.Replace(regexp.QuoteMeta(host), `\*\.`, `[a-z0-9-\.]+\.`, 1)
rules = append(rules, fmt.Sprintf("HostSNIRegexp(`^%s$`)", host))
}
if len(hostnames) == 0 || len(rules) == 0 {
return "HostSNI(`*`)"
}
return strings.Join(rules, " || ")
}

View file

@ -0,0 +1,66 @@
package gateway
import (
"testing"
"github.com/stretchr/testify/assert"
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
)
func Test_hostSNIRule(t *testing.T) {
testCases := []struct {
desc string
hostnames []gatev1.Hostname
expectedRule string
expectError bool
}{
{
desc: "Empty",
expectedRule: "HostSNI(`*`)",
},
{
desc: "Empty hostname",
hostnames: []gatev1.Hostname{""},
expectedRule: "HostSNI(`*`)",
},
{
desc: "Supported wildcard",
hostnames: []gatev1.Hostname{"*.foo"},
expectedRule: "HostSNIRegexp(`^[a-z0-9-\\.]+\\.foo$`)",
},
{
desc: "Some empty hostnames",
hostnames: []gatev1.Hostname{"foo", "", "bar"},
expectedRule: "HostSNI(`foo`) || HostSNI(`bar`)",
},
{
desc: "Valid hostname",
hostnames: []gatev1.Hostname{"foo"},
expectedRule: "HostSNI(`foo`)",
},
{
desc: "Multiple valid hostnames",
hostnames: []gatev1.Hostname{"foo", "bar"},
expectedRule: "HostSNI(`foo`) || HostSNI(`bar`)",
},
{
desc: "Multiple valid hostnames with wildcard",
hostnames: []gatev1.Hostname{"bar.foo", "foo.foo", "*.foo"},
expectedRule: "HostSNI(`bar.foo`) || HostSNI(`foo.foo`) || HostSNIRegexp(`^[a-z0-9-\\.]+\\.foo$`)",
},
{
desc: "Multiple overlapping hostnames",
hostnames: []gatev1.Hostname{"foo", "bar", "foo", "baz"},
expectedRule: "HostSNI(`foo`) || HostSNI(`bar`) || HostSNI(`baz`)",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
rule := hostSNIRule(test.hostnames)
assert.Equal(t, test.expectedRule, rule)
})
}
}

View file

@ -45,7 +45,7 @@ type ServiceIng struct {
ServersTransport string `json:"serversTransport,omitempty"` ServersTransport string `json:"serversTransport,omitempty"`
PassHostHeader *bool `json:"passHostHeader"` PassHostHeader *bool `json:"passHostHeader"`
Sticky *dynamic.Sticky `json:"sticky,omitempty" label:"allowEmpty"` Sticky *dynamic.Sticky `json:"sticky,omitempty" label:"allowEmpty"`
NativeLB bool `json:"nativeLB,omitempty"` NativeLB *bool `json:"nativeLB,omitempty"`
NodePortLB bool `json:"nodePortLB,omitempty"` NodePortLB bool `json:"nodePortLB,omitempty"`
} }

View file

@ -125,7 +125,7 @@ func Test_parseServiceConfig(t *testing.T) {
ServersScheme: "protocol", ServersScheme: "protocol",
ServersTransport: "foobar@file", ServersTransport: "foobar@file",
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
NativeLB: true, NativeLB: Bool(true),
}, },
}, },
}, },

View file

@ -0,0 +1,30 @@
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: global-native-lb
namespace: default
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: default
spec:
ports:
- port: 8080
clusterIP: 10.0.0.1
type: ClusterIP
externalName: traefik.wtf

View file

@ -52,6 +52,7 @@ type Provider struct {
AllowEmptyServices bool `description:"Allow creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,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"` 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"` 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"`
lastConfiguration safe.Safe lastConfiguration safe.Safe
@ -134,7 +135,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
} }
if p.AllowExternalNameServices { if p.AllowExternalNameServices {
logger.Warn().Msg("ExternalName service loading is enabled, please ensure that this is expected (see AllowExternalNameServices option)") logger.Info().Msg("ExternalName service loading is enabled, please ensure that this is expected (see AllowExternalNameServices option)")
} }
pool.GoCtx(func(ctxPool context.Context) { pool.GoCtx(func(ctxPool context.Context) {
@ -277,6 +278,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
rt := &dynamic.Router{ rt := &dynamic.Router{
Rule: "PathPrefix(`/`)", Rule: "PathPrefix(`/`)",
RuleSyntax: "v3",
Priority: math.MinInt32, Priority: math.MinInt32,
Service: "default-backend", Service: "default-backend",
} }
@ -571,6 +573,8 @@ func (p *Provider) loadService(client Client, namespace string, backend netv1.In
return nil, err return nil, err
} }
nativeLB := p.NativeLBByDefault
if svcConfig != nil && svcConfig.Service != nil { if svcConfig != nil && svcConfig.Service != nil {
svc.LoadBalancer.Sticky = svcConfig.Service.Sticky svc.LoadBalancer.Sticky = svcConfig.Service.Sticky
@ -582,19 +586,8 @@ func (p *Provider) loadService(client Client, namespace string, backend netv1.In
svc.LoadBalancer.ServersTransport = svcConfig.Service.ServersTransport svc.LoadBalancer.ServersTransport = svcConfig.Service.ServersTransport
} }
if svcConfig.Service.NativeLB { if svcConfig.Service.NativeLB != nil {
address, err := getNativeServiceAddress(*service, portSpec) nativeLB = *svcConfig.Service.NativeLB
if err != nil {
return nil, fmt.Errorf("getting native Kubernetes Service address: %w", err)
}
protocol := getProtocol(portSpec, portSpec.Name, svcConfig)
svc.LoadBalancer.Servers = []dynamic.Server{
{URL: fmt.Sprintf("%s://%s", protocol, address)},
}
return svc, nil
} }
if svcConfig.Service.NodePortLB && service.Spec.Type == corev1.ServiceTypeNodePort { if svcConfig.Service.NodePortLB && service.Spec.Type == corev1.ServiceTypeNodePort {
@ -644,6 +637,20 @@ func (p *Provider) loadService(client Client, namespace string, backend netv1.In
return svc, nil return svc, nil
} }
if nativeLB {
address, err := getNativeServiceAddress(*service, portSpec)
if err != nil {
return nil, fmt.Errorf("getting native Kubernetes Service address: %w", err)
}
protocol := getProtocol(portSpec, portSpec.Name, svcConfig)
svc.LoadBalancer.Servers = []dynamic.Server{
{URL: fmt.Sprintf("%s://%s", protocol, address)},
}
return svc, nil
}
endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, backend.Service.Name) endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, backend.Service.Name)
if endpointsErr != nil { if endpointsErr != nil {
return nil, endpointsErr return nil, endpointsErr

View file

@ -529,6 +529,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
Routers: map[string]*dynamic.Router{ Routers: map[string]*dynamic.Router{
"default-router": { "default-router": {
Rule: "PathPrefix(`/`)", Rule: "PathPrefix(`/`)",
RuleSyntax: "v3",
Service: "default-backend", Service: "default-backend",
Priority: math.MinInt32, Priority: math.MinInt32,
}, },
@ -994,6 +995,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
Routers: map[string]*dynamic.Router{ Routers: map[string]*dynamic.Router{
"default-router": { "default-router": {
Rule: "PathPrefix(`/`)", Rule: "PathPrefix(`/`)",
RuleSyntax: "v3",
Service: "default-backend", Service: "default-backend",
Priority: math.MinInt32, Priority: math.MinInt32,
}, },
@ -1470,6 +1472,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
Routers: map[string]*dynamic.Router{ Routers: map[string]*dynamic.Router{
"default-router": { "default-router": {
Rule: "PathPrefix(`/`)", Rule: "PathPrefix(`/`)",
RuleSyntax: "v3",
Priority: math.MinInt32, Priority: math.MinInt32,
Service: "default-backend", Service: "default-backend",
}, },
@ -1914,3 +1917,84 @@ func TestGetCertificates(t *testing.T) {
}) })
} }
} }
func TestLoadConfigurationFromIngressesWithNativeLBByDefault(t *testing.T) {
testCases := []struct {
desc string
ingressClass string
expected *dynamic.Configuration
}{
{
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{
"testing-traefik-tchouk-bar": {
Rule: "Host(`traefik.tchouk`) && PathPrefix(`/bar`)",
Service: "testing-service1-8080",
},
},
Services: map[string]*dynamic.Service{
"testing-service1-8080": {
LoadBalancer: &dynamic.ServersLoadBalancer{
ResponseForwarding: &dynamic.ResponseForwarding{FlushInterval: dynamic.DefaultFlushInterval},
PassHostHeader: Bool(true),
Servers: []dynamic.Server{
{
URL: "http://10.0.0.1:8080",
},
},
},
},
},
},
},
},
{
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{
"default-global-native-lb-traefik-tchouk-bar": {
Rule: "Host(`traefik.tchouk`) && PathPrefix(`/bar`)",
Service: "default-service1-8080",
},
},
Services: map[string]*dynamic.Service{
"default-service1-8080": {
LoadBalancer: &dynamic.ServersLoadBalancer{
ResponseForwarding: &dynamic.ResponseForwarding{FlushInterval: dynamic.DefaultFlushInterval},
PassHostHeader: Bool(true),
Servers: []dynamic.Server{
{
URL: "http://10.0.0.1:8080",
},
},
},
},
},
},
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
clientMock := newClientMock(generateTestFilename(test.desc))
p := Provider{
IngressClass: test.ingressClass,
NativeLBByDefault: true,
}
conf := p.loadConfigurationFromIngresses(context.Background(), clientMock)
assert.Equal(t, test.expected, conf)
})
}
}

View file

@ -7,6 +7,7 @@
], ],
"service": "api@internal", "service": "api@internal",
"rule": "PathPrefix(`/api`)", "rule": "PathPrefix(`/api`)",
"ruleSyntax": "v3",
"priority": 9223372036854775806 "priority": 9223372036854775806
}, },
"dashboard": { "dashboard": {
@ -19,6 +20,7 @@
], ],
"service": "dashboard@internal", "service": "dashboard@internal",
"rule": "PathPrefix(`/`)", "rule": "PathPrefix(`/`)",
"ruleSyntax": "v3",
"priority": 9223372036854775805 "priority": 9223372036854775805
} }
}, },

View file

@ -7,6 +7,7 @@
], ],
"service": "api@internal", "service": "api@internal",
"rule": "PathPrefix(`/api`)", "rule": "PathPrefix(`/api`)",
"ruleSyntax": "v3",
"priority": 9223372036854775806 "priority": 9223372036854775806
} }
}, },

View file

@ -7,6 +7,7 @@
], ],
"service": "api@internal", "service": "api@internal",
"rule": "PathPrefix(`/api`)", "rule": "PathPrefix(`/api`)",
"ruleSyntax": "v3",
"priority": 9223372036854775806 "priority": 9223372036854775806
}, },
"dashboard": { "dashboard": {
@ -19,6 +20,7 @@
], ],
"service": "dashboard@internal", "service": "dashboard@internal",
"rule": "PathPrefix(`/`)", "rule": "PathPrefix(`/`)",
"ruleSyntax": "v3",
"priority": 9223372036854775805 "priority": 9223372036854775805
}, },
"debug": { "debug": {
@ -27,6 +29,7 @@
], ],
"service": "api@internal", "service": "api@internal",
"rule": "PathPrefix(`/debug`)", "rule": "PathPrefix(`/debug`)",
"ruleSyntax": "v3",
"priority": 9223372036854775806 "priority": 9223372036854775806
}, },
"ping": { "ping": {
@ -35,6 +38,7 @@
], ],
"service": "ping@internal", "service": "ping@internal",
"rule": "PathPrefix(`/ping`)", "rule": "PathPrefix(`/ping`)",
"ruleSyntax": "v3",
"priority": 9223372036854775807 "priority": 9223372036854775807
}, },
"prometheus": { "prometheus": {
@ -43,6 +47,7 @@
], ],
"service": "prometheus@internal", "service": "prometheus@internal",
"rule": "PathPrefix(`/metrics`)", "rule": "PathPrefix(`/metrics`)",
"ruleSyntax": "v3",
"priority": 9223372036854775807 "priority": 9223372036854775807
}, },
"rest": { "rest": {
@ -51,6 +56,7 @@
], ],
"service": "rest@internal", "service": "rest@internal",
"rule": "PathPrefix(`/api/providers`)", "rule": "PathPrefix(`/api/providers`)",
"ruleSyntax": "v3",
"priority": 9223372036854775807 "priority": 9223372036854775807
} }
}, },

View file

@ -7,6 +7,7 @@
], ],
"service": "ping@internal", "service": "ping@internal",
"rule": "PathPrefix(`/ping`)", "rule": "PathPrefix(`/ping`)",
"ruleSyntax": "v3",
"priority": 9223372036854775807 "priority": 9223372036854775807
} }
}, },

View file

@ -7,6 +7,7 @@
], ],
"service": "prometheus@internal", "service": "prometheus@internal",
"rule": "PathPrefix(`/metrics`)", "rule": "PathPrefix(`/metrics`)",
"ruleSyntax": "v3",
"priority": 9223372036854775807 "priority": 9223372036854775807
} }
}, },

View file

@ -9,7 +9,8 @@
"redirect-web-to-websecure" "redirect-web-to-websecure"
], ],
"service": "noop@internal", "service": "noop@internal",
"rule": "HostRegexp(`^.+$`)" "rule": "HostRegexp(`^.+$`)",
"ruleSyntax": "v3"
} }
}, },
"services": { "services": {

View file

@ -9,7 +9,8 @@
"redirect-web-to-443" "redirect-web-to-443"
], ],
"service": "noop@internal", "service": "noop@internal",
"rule": "HostRegexp(`^.+$`)" "rule": "HostRegexp(`^.+$`)",
"ruleSyntax": "v3"
} }
}, },
"services": { "services": {

View file

@ -9,7 +9,8 @@
"redirect-web-to-websecure" "redirect-web-to-websecure"
], ],
"service": "noop@internal", "service": "noop@internal",
"rule": "HostRegexp(`^.+$`)" "rule": "HostRegexp(`^.+$`)",
"ruleSyntax": "v3"
} }
}, },
"services": { "services": {

View file

@ -7,6 +7,7 @@
], ],
"service": "rest@internal", "service": "rest@internal",
"rule": "PathPrefix(`/api/providers`)", "rule": "PathPrefix(`/api/providers`)",
"ruleSyntax": "v3",
"priority": 9223372036854775807 "priority": 9223372036854775807
} }
}, },

View file

@ -106,6 +106,7 @@ func (i *Provider) acme(cfg *dynamic.Configuration) {
if len(eps) > 0 { if len(eps) > 0 {
rt := &dynamic.Router{ rt := &dynamic.Router{
Rule: "PathPrefix(`/.well-known/acme-challenge/`)", Rule: "PathPrefix(`/.well-known/acme-challenge/`)",
RuleSyntax: "v3",
EntryPoints: eps, EntryPoints: eps,
Service: "acme-http@internal", Service: "acme-http@internal",
Priority: math.MaxInt, Priority: math.MaxInt,
@ -141,6 +142,7 @@ func (i *Provider) redirection(ctx context.Context, cfg *dynamic.Configuration)
rt := &dynamic.Router{ rt := &dynamic.Router{
Rule: "HostRegexp(`^.+$`)", Rule: "HostRegexp(`^.+$`)",
RuleSyntax: "v3",
EntryPoints: []string{name}, EntryPoints: []string{name},
Middlewares: []string{mdName}, Middlewares: []string{mdName},
Service: "noop@internal", Service: "noop@internal",
@ -241,6 +243,7 @@ func (i *Provider) apiConfiguration(cfg *dynamic.Configuration) {
Service: "api@internal", Service: "api@internal",
Priority: math.MaxInt - 1, Priority: math.MaxInt - 1,
Rule: "PathPrefix(`/api`)", Rule: "PathPrefix(`/api`)",
RuleSyntax: "v3",
} }
if i.staticCfg.API.Dashboard { if i.staticCfg.API.Dashboard {
@ -249,6 +252,7 @@ func (i *Provider) apiConfiguration(cfg *dynamic.Configuration) {
Service: "dashboard@internal", Service: "dashboard@internal",
Priority: math.MaxInt - 2, Priority: math.MaxInt - 2,
Rule: "PathPrefix(`/`)", Rule: "PathPrefix(`/`)",
RuleSyntax: "v3",
Middlewares: []string{"dashboard_redirect@internal", "dashboard_stripprefix@internal"}, Middlewares: []string{"dashboard_redirect@internal", "dashboard_stripprefix@internal"},
} }
@ -270,6 +274,7 @@ func (i *Provider) apiConfiguration(cfg *dynamic.Configuration) {
Service: "api@internal", Service: "api@internal",
Priority: math.MaxInt - 1, Priority: math.MaxInt - 1,
Rule: "PathPrefix(`/debug`)", Rule: "PathPrefix(`/debug`)",
RuleSyntax: "v3",
} }
} }
} }
@ -292,6 +297,7 @@ func (i *Provider) pingConfiguration(cfg *dynamic.Configuration) {
Service: "ping@internal", Service: "ping@internal",
Priority: math.MaxInt, Priority: math.MaxInt,
Rule: "PathPrefix(`/ping`)", Rule: "PathPrefix(`/ping`)",
RuleSyntax: "v3",
} }
} }
@ -309,6 +315,7 @@ func (i *Provider) restConfiguration(cfg *dynamic.Configuration) {
Service: "rest@internal", Service: "rest@internal",
Priority: math.MaxInt, Priority: math.MaxInt,
Rule: "PathPrefix(`/api/providers`)", Rule: "PathPrefix(`/api/providers`)",
RuleSyntax: "v3",
} }
} }
@ -326,6 +333,7 @@ func (i *Provider) prometheusConfiguration(cfg *dynamic.Configuration) {
Service: "prometheus@internal", Service: "prometheus@internal",
Priority: math.MaxInt, Priority: math.MaxInt,
Rule: "PathPrefix(`/metrics`)", Rule: "PathPrefix(`/metrics`)",
RuleSyntax: "v3",
} }
} }

View file

@ -84,6 +84,9 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
for serviceName, service := range configuration.TCP.Services { for serviceName, service := range configuration.TCP.Services {
conf.TCP.Services[provider.MakeQualifiedName(pvd, serviceName)] = service conf.TCP.Services[provider.MakeQualifiedName(pvd, serviceName)] = service
} }
for modelName, model := range configuration.TCP.Models {
conf.TCP.Models[provider.MakeQualifiedName(pvd, modelName)] = model
}
for serversTransportName, serversTransport := range configuration.TCP.ServersTransports { for serversTransportName, serversTransport := range configuration.TCP.ServersTransports {
conf.TCP.ServersTransports[provider.MakeQualifiedName(pvd, serversTransportName)] = serversTransport conf.TCP.ServersTransports[provider.MakeQualifiedName(pvd, serversTransportName)] = serversTransport
} }
@ -146,10 +149,7 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
} }
func applyModel(cfg dynamic.Configuration) dynamic.Configuration { func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
if cfg.HTTP == nil || len(cfg.HTTP.Models) == 0 { if cfg.HTTP != nil && len(cfg.HTTP.Models) > 0 {
return cfg
}
rts := make(map[string]*dynamic.Router) rts := make(map[string]*dynamic.Router)
for name, rt := range cfg.HTTP.Routers { for name, rt := range cfg.HTTP.Routers {
@ -192,6 +192,7 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
} }
cfg.HTTP.Routers = rts cfg.HTTP.Routers = rts
}
if cfg.TCP == nil || len(cfg.TCP.Models) == 0 { if cfg.TCP == nil || len(cfg.TCP.Models) == 0 {
return cfg return cfg
@ -199,7 +200,7 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
tcpRouters := make(map[string]*dynamic.TCPRouter) tcpRouters := make(map[string]*dynamic.TCPRouter)
for _, rt := range cfg.TCP.Routers { for name, rt := range cfg.TCP.Routers {
router := rt.DeepCopy() router := rt.DeepCopy()
if router.RuleSyntax == "" { if router.RuleSyntax == "" {
@ -208,6 +209,8 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
break break
} }
} }
tcpRouters[name] = router
} }
cfg.TCP.Routers = tcpRouters cfg.TCP.Routers = tcpRouters

View file

@ -656,6 +656,50 @@ func Test_applyModel(t *testing.T) {
}, },
}, },
}, },
{
desc: "with TCP model, two entry points",
input: dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{
"test": {
EntryPoints: []string{"websecure", "web"},
},
"test2": {
EntryPoints: []string{"web"},
RuleSyntax: "barfoo",
},
},
Middlewares: make(map[string]*dynamic.TCPMiddleware),
Services: make(map[string]*dynamic.TCPService),
Models: map[string]*dynamic.TCPModel{
"websecure@internal": {
DefaultRuleSyntax: "foobar",
},
},
},
},
expected: dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{
"test": {
EntryPoints: []string{"websecure", "web"},
RuleSyntax: "foobar",
},
"test2": {
EntryPoints: []string{"web"},
RuleSyntax: "barfoo",
},
},
Middlewares: make(map[string]*dynamic.TCPMiddleware),
Services: make(map[string]*dynamic.TCPService),
Models: map[string]*dynamic.TCPModel{
"websecure@internal": {
DefaultRuleSyntax: "foobar",
},
},
},
},
},
} }
for _, test := range testCases { for _, test := range testCases {

View file

@ -123,6 +123,11 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) {
} }
if postgres { if postgres {
// Remove read/write deadline and delegate this to underlying TCP server.
if err := conn.SetDeadline(time.Time{}); err != nil {
log.Error().Err(err).Msg("Error while setting deadline")
}
r.servePostgres(r.GetConn(conn, getPeeked(br))) r.servePostgres(r.GetConn(conn, getPeeked(br)))
return return
} }

View file

@ -222,6 +222,14 @@ func (m *Manager) getWRRServiceHandler(ctx context.Context, serviceName string,
balancer := wrr.New(config.Sticky, config.HealthCheck != nil) balancer := wrr.New(config.Sticky, config.HealthCheck != nil)
for _, service := range shuffle(config.Services, m.rand) { for _, service := range shuffle(config.Services, m.rand) {
if service.Status != nil {
serviceHandler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(*service.Status)
})
balancer.Add(service.Name, serviceHandler, service.Weight)
continue
}
serviceHandler, err := m.BuildHTTP(ctx, service.Name) serviceHandler, err := m.BuildHTTP(ctx, service.Name)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -26,8 +26,8 @@ import (
// Config provides configuration settings for the open-telemetry tracer. // Config provides configuration settings for the open-telemetry tracer.
type Config struct { type Config struct {
GRPC *types.OtelGRPC `description:"gRPC configuration for the OpenTelemetry collector." json:"grpc,omitempty" toml:"grpc,omitempty" yaml:"grpc,omitempty" export:"true"` GRPC *types.OtelGRPC `description:"gRPC configuration for the OpenTelemetry collector." json:"grpc,omitempty" toml:"grpc,omitempty" yaml:"grpc,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
HTTP *types.OtelHTTP `description:"HTTP configuration for the OpenTelemetry collector." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" export:"true"` HTTP *types.OtelHTTP `description:"HTTP configuration for the OpenTelemetry collector." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
} }
// SetDefaults sets the default values. // SetDefaults sets the default values.

View file

@ -108,8 +108,8 @@ func (i *InfluxDB2) SetDefaults() {
// OTLP contains specific configuration used by the OpenTelemetry Metrics exporter. // OTLP contains specific configuration used by the OpenTelemetry Metrics exporter.
type OTLP struct { type OTLP struct {
GRPC *OtelGRPC `description:"gRPC configuration for the OpenTelemetry collector." json:"grpc,omitempty" toml:"grpc,omitempty" yaml:"grpc,omitempty" export:"true"` GRPC *OtelGRPC `description:"gRPC configuration for the OpenTelemetry collector." json:"grpc,omitempty" toml:"grpc,omitempty" yaml:"grpc,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
HTTP *OtelHTTP `description:"HTTP configuration for the OpenTelemetry collector." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" export:"true"` HTTP *OtelHTTP `description:"HTTP configuration for the OpenTelemetry collector." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
AddEntryPointsLabels bool `description:"Enable metrics on entry points." json:"addEntryPointsLabels,omitempty" toml:"addEntryPointsLabels,omitempty" yaml:"addEntryPointsLabels,omitempty" export:"true"` AddEntryPointsLabels bool `description:"Enable metrics on entry points." json:"addEntryPointsLabels,omitempty" toml:"addEntryPointsLabels,omitempty" yaml:"addEntryPointsLabels,omitempty" export:"true"`
AddRoutersLabels bool `description:"Enable metrics on routers." json:"addRoutersLabels,omitempty" toml:"addRoutersLabels,omitempty" yaml:"addRoutersLabels,omitempty" export:"true"` AddRoutersLabels bool `description:"Enable metrics on routers." json:"addRoutersLabels,omitempty" toml:"addRoutersLabels,omitempty" yaml:"addRoutersLabels,omitempty" export:"true"`

View file

@ -4,11 +4,11 @@ RepositoryName = "traefik"
OutputType = "file" OutputType = "file"
FileName = "traefik_changelog.md" FileName = "traefik_changelog.md"
# example new bugfix v2.11.2 # example new bugfix v3.0.1
CurrentRef = "v2.11" CurrentRef = "v3.0"
PreviousRef = "v2.11.1" PreviousRef = "v3.0.0"
BaseBranch = "v2.11" BaseBranch = "v3.0"
FutureCurrentRefName = "v2.11.2" FutureCurrentRefName = "v3.0.1"
ThresholdPreviousRef = 10 ThresholdPreviousRef = 10
ThresholdCurrentRef = 10 ThresholdCurrentRef = 10

View file

@ -4,11 +4,11 @@ RepositoryName = "traefik"
OutputType = "file" OutputType = "file"
FileName = "traefik_changelog.md" FileName = "traefik_changelog.md"
# example final release of v2.11.0 # example final release of v3.0.0
CurrentRef = "v2.11" CurrentRef = "v3.0"
PreviousRef = "v2.11.0-rc1" PreviousRef = "v3.0.0-beta1"
BaseBranch = "v2.11" BaseBranch = "v3.0"
FutureCurrentRefName = "v2.11.0" FutureCurrentRefName = "v3.0.0"
ThresholdPreviousRef = 10 ThresholdPreviousRef = 10
ThresholdCurrentRef = 10 ThresholdCurrentRef = 10

View file

@ -4,11 +4,11 @@ RepositoryName = "traefik"
OutputType = "file" OutputType = "file"
FileName = "traefik_changelog.md" FileName = "traefik_changelog.md"
# example final release of v2.11.0 # example final release of v3.0.0
CurrentRef = "v2.11.0-rc1" CurrentRef = "v3.0.0-beta1"
PreviousRef = "v2.10.0-rc1" PreviousRef = "v2.9.0-rc1"
BaseBranch = "master" BaseBranch = "master"
FutureCurrentRefName = "v2.11.0-rc1" FutureCurrentRefName = "v3.0.0-beta1"
ThresholdPreviousRef = 10 ThresholdPreviousRef = 10
ThresholdCurrentRef = 10 ThresholdCurrentRef = 10

View file

@ -9,7 +9,6 @@ module.exports = {
env: { env: {
node: true, node: true,
browser: true, browser: true,
mocha: true,
'vue/setup-compiler-macros': true 'vue/setup-compiler-macros': true
}, },
@ -18,14 +17,12 @@ module.exports = {
// consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
'plugin:vue/vue3-essential', 'plugin:vue/vue3-essential',
'plugin:vue/vue3-recommended', 'plugin:vue/vue3-recommended',
'plugin:mocha/recommended',
'standard' 'standard'
], ],
// required to lint *.vue files // required to lint *.vue files
plugins: [ plugins: [
'vue', 'vue',
'mocha'
], ],
globals: { globals: {

View file

@ -8,12 +8,14 @@
"scripts": { "scripts": {
"transfer": "node dev/scripts/transfer.js", "transfer": "node dev/scripts/transfer.js",
"lint": "eslint --ext .js,.vue src", "lint": "eslint --ext .js,.vue src",
"test-unit": "mocha-webpack --mode=production './src/**/*.spec.js'",
"dev": "export APP_ENV='development' && quasar dev", "dev": "export APP_ENV='development' && quasar dev",
"build-quasar": "quasar build", "build-quasar": "quasar build",
"build-staging": "export NODE_ENV='production' && export APP_ENV='development' && yarn build-quasar", "build-staging": "export NODE_ENV='production' && export APP_ENV='development' && yarn build-quasar",
"build": "export NODE_ENV='production' && export APP_ENV='production' && yarn build-quasar && yarn transfer spa", "build": "export NODE_ENV='production' && export APP_ENV='production' && yarn build-quasar && yarn transfer spa",
"build:nc": "yarn build" "build:nc": "yarn build",
"test": "echo \"See package.json => scripts for available tests.\" && exit 0",
"test:unit": "vitest",
"test:unit:ci": "vitest run"
}, },
"dependencies": { "dependencies": {
"@quasar/extras": "^1.16.9", "@quasar/extras": "^1.16.9",
@ -39,18 +41,17 @@
"@babel/eslint-parser": "^7.23.10", "@babel/eslint-parser": "^7.23.10",
"@quasar/app-vite": "^1.4.3", "@quasar/app-vite": "^1.4.3",
"@quasar/babel-preset-app": "^2.0.2", "@quasar/babel-preset-app": "^2.0.2",
"@quasar/quasar-app-extension-testing-unit-vitest": "^1.0.0",
"@vue/test-utils": "^2.4.4", "@vue/test-utils": "^2.4.4",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",
"chai": "5.0.3",
"eslint": "^8.11.0", "eslint": "^8.11.0",
"eslint-config-standard": "^17.0.0", "eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.19.1", "eslint-plugin-import": "^2.19.1",
"eslint-plugin-mocha": "^10.2.0",
"eslint-plugin-n": "^16.6.2", "eslint-plugin-n": "^16.6.2",
"eslint-plugin-promise": "^6.0.0", "eslint-plugin-promise": "^6.0.0",
"eslint-plugin-vue": "^9.0.0", "eslint-plugin-vue": "^9.0.0",
"mocha": "^10.2.0", "postcss": "^8.4.14",
"postcss": "^8.4.14" "vitest": "^1.3.1"
}, },
"engines": { "engines": {
"node": "^20 || ^18 || ^16", "node": "^20 || ^18 || ^16",

View file

@ -122,8 +122,8 @@ module.exports = configure(function (ctx) {
build: { build: {
// Needed to have relative assets in the index.html // Needed to have relative assets in the index.html
// https://github.com/quasarframework/quasar/issues/8513#issuecomment-1127654470 // https://github.com/quasarframework/quasar/issues/8513#issuecomment-1127654470
extendViteConf(viteConf, {isServer, isClient}) { extendViteConf (viteConf, { isServer, isClient }) {
viteConf.base = ""; viteConf.base = ''
}, },
viteVuePluginOptions: { viteVuePluginOptions: {
template: { template: {
@ -173,7 +173,7 @@ module.exports = configure(function (ctx) {
animations: [], animations: [],
ssr: { ssr: {
pwa: false, pwa: false
}, },
pwa: { pwa: {
@ -201,29 +201,29 @@ module.exports = configure(function (ctx) {
theme_color: '#027be3', theme_color: '#027be3',
icons: [ icons: [
{ {
'src': 'icons/icon-128x128.png', src: 'icons/icon-128x128.png',
'sizes': '128x128', sizes: '128x128',
'type': 'image/png' type: 'image/png'
}, },
{ {
'src': 'icons/icon-192x192.png', src: 'icons/icon-192x192.png',
'sizes': '192x192', sizes: '192x192',
'type': 'image/png' type: 'image/png'
}, },
{ {
'src': 'icons/icon-256x256.png', src: 'icons/icon-256x256.png',
'sizes': '256x256', sizes: '256x256',
'type': 'image/png' type: 'image/png'
}, },
{ {
'src': 'icons/icon-384x384.png', src: 'icons/icon-384x384.png',
'sizes': '384x384', sizes: '384x384',
'type': 'image/png' type: 'image/png'
}, },
{ {
'src': 'icons/icon-512x512.png', src: 'icons/icon-512x512.png',
'sizes': '512x512', sizes: '512x512',
'type': 'image/png' type: 'image/png'
} }
] ]
} }

View file

@ -0,0 +1,5 @@
{
"@quasar/testing-unit-vitest": {
"options": []
}
}

View file

@ -20,7 +20,7 @@ make clean-webui generate-webui # Generate static contents in `webui/static/` fo
## How to build (only for frontend developer) ## How to build (only for frontend developer)
- prerequisite: [Node 12.11+](https://nodejs.org) [Yarn](https://yarnpkg.com/) - prerequisite: [Node 20.11+](https://nodejs.org) [Yarn 1.22.19](https://yarnpkg.com/)
- Go to the `webui/` directory - Go to the `webui/` directory
@ -57,7 +57,7 @@ make clean-webui generate-webui # Generate static contents in `webui/static/` fo
- [Node](https://nodejs.org) - [Node](https://nodejs.org)
- [Yarn](https://yarnpkg.com/) - [Yarn](https://yarnpkg.com/)
- [Webpack](https://github.com/webpack/webpack) - [Quasar](https://quasar.dev/)
- [Vue](https://vuejs.org/) - [Vue](https://vuejs.org/)
- [Bulma](https://bulma.io) - [Bulma](https://bulma.io)
- [D3](https://d3js.org) - [D3](https://d3js.org)

View file

@ -809,12 +809,11 @@
<div class="text-subtitle2"> <div class="text-subtitle2">
Content Security Policy Content Security Policy
</div> </div>
<q-chip <q-card class="app-chip app-chip-green app-card-as-chip">
dense <q-card-section>
class="app-chip app-chip-green"
>
{{ exData(middleware).contentSecurityPolicy }} {{ exData(middleware).contentSecurityPolicy }}
</q-chip> </q-card-section>
</q-card>
</div> </div>
</div> </div>
</q-card-section> </q-card-section>
@ -945,8 +944,8 @@
</div> </div>
</q-card-section> </q-card-section>
<!-- EXTRA FIELDS FROM MIDDLEWARES - [ipAllowList] - sourceRange --> <!-- EXTRA FIELDS FROM MIDDLEWARES - [ipWhiteList] - sourceRange -->
<q-card-section v-if="middleware.ipAllowList"> <q-card-section v-if="middleware.ipWhiteList">
<div class="row items-start no-wrap"> <div class="row items-start no-wrap">
<div class="col"> <div class="col">
<div class="text-subtitle2"> <div class="text-subtitle2">
@ -963,8 +962,8 @@
</div> </div>
</div> </div>
</q-card-section> </q-card-section>
<!-- EXTRA FIELDS FROM MIDDLEWARES - [ipAllowList] - ipStrategy --> <!-- EXTRA FIELDS FROM MIDDLEWARES - [ipWhiteList] - ipStrategy -->
<q-card-section v-if="middleware.ipAllowList"> <q-card-section v-if="middleware.ipWhiteList">
<div class="row items-start"> <div class="row items-start">
<div class="col-12"> <div class="col-12">
<div class="text-subtitle2"> <div class="text-subtitle2">

View file

@ -121,6 +121,14 @@ body {
border-radius: 8px; border-radius: 8px;
} }
.app-card-as-chip {
box-shadow: none;
.q-card__section {
padding: 5px !important;
}
}
// Chips // Chips
.app-chip { .app-chip {
border-radius: 8px; border-radius: 8px;

View file

@ -1,4 +1,4 @@
import { expect } from 'chai' import { describe, expect, it } from 'vitest'
import store from './index.js' import store from './index.js'
const { const {

View file

@ -1,4 +1,4 @@
import { expect } from 'chai' import { describe, expect, it } from 'vitest'
import store from './index.js' import store from './index.js'
const { const {

View file

@ -1,4 +1,4 @@
import { expect } from 'chai' import { describe, expect, it } from 'vitest'
import store from './index.js' import store from './index.js'
const { const {

View file

@ -0,0 +1 @@
// This file will be run before each test file

24
webui/vitest.config.mjs Normal file
View file

@ -0,0 +1,24 @@
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';
import { quasar, transformAssetUrls } from '@quasar/vite-plugin';
import jsconfigPaths from 'vite-jsconfig-paths';
// https://vitejs.dev/config/
export default defineConfig({
test: {
environment: 'happy-dom',
setupFiles: 'test/vitest/setup-file.js',
include: [
// Matches vitest tests in any subfolder of 'src' or into 'test/vitest/__tests__'
// Matches all files with extension 'js', 'jsx', 'ts' and 'tsx'
'src/**/*.vitest.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
],
},
plugins: [
vue({
template: { transformAssetUrls },
}),
quasar(),
jsconfigPaths(),
],
});

File diff suppressed because it is too large Load diff