mirror of
https://github.com/containous/traefik.git
synced 2025-12-22 08:23:52 +03:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4f0c3051c | ||
|
|
1e0e03edc7 | ||
|
|
0a3239463b | ||
|
|
653b105cb7 | ||
|
|
928f7ed8ce | ||
|
|
950e957b03 | ||
|
|
f0957c8df4 | ||
|
|
4e441f8b18 | ||
|
|
cd562a0451 | ||
|
|
c63be08b07 | ||
|
|
8a621274b8 | ||
|
|
e931a71660 | ||
|
|
61ad0f13e8 | ||
|
|
63a6172ec4 | ||
|
|
206427c4ea | ||
|
|
4d7d627319 | ||
|
|
c3d428a16e | ||
|
|
d6b127ba91 | ||
|
|
7314f7ddc9 | ||
|
|
4b50f27d6e | ||
|
|
ef03ed5875 | ||
|
|
14a1aedf57 | ||
|
|
e5a3a23c02 | ||
|
|
d76a4e36ee | ||
|
|
0b6438b7c0 |
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ linux-amd64, linux-386, linux-arm, linux-arm64, linux-ppc64le, linux-s390x, linux-riscv64, darwin, windows-amd64, windows-arm64, windows-386, freebsd, openbsd ]
|
os: [ linux-amd64, linux-386, linux-arm, linux-arm64, linux-ppc64le, linux-s390x, linux-riscv64, darwin-amd64, darwin-arm64, windows-amd64, windows-arm64, windows-386, freebsd-amd64, freebsd-386, openbsd-amd64, openbsd-386, openbsd-riscv64 ]
|
||||||
needs:
|
needs:
|
||||||
- build-webui
|
- build-webui
|
||||||
|
|
||||||
|
|||||||
@@ -54,10 +54,12 @@ changelog:
|
|||||||
archives:
|
archives:
|
||||||
- id: traefik
|
- id: traefik
|
||||||
name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
|
name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
|
||||||
format: tar.gz
|
formats:
|
||||||
|
- tar.gz
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
format: zip
|
formats:
|
||||||
|
- zip
|
||||||
files:
|
files:
|
||||||
- LICENSE.md
|
- LICENSE.md
|
||||||
- CHANGELOG.md
|
- CHANGELOG.md
|
||||||
|
|||||||
69
CHANGELOG.md
69
CHANGELOG.md
@@ -1,3 +1,72 @@
|
|||||||
|
## [v3.6.5](https://github.com/traefik/traefik/tree/v3.6.5) (2025-12-16)
|
||||||
|
[All Commits](https://github.com/traefik/traefik/compare/v3.6.4...v3.6.5)
|
||||||
|
|
||||||
|
**Bug fixes:**
|
||||||
|
- **[k8s/ingress-nginx]** Fix NGINX sslredirect annotation support ([#12387](https://github.com/traefik/traefik/pull/12387) by [rtribotte](https://github.com/rtribotte))
|
||||||
|
- **[server]** Print access logs for rejected requests and warn about new behavior ([#12424](https://github.com/traefik/traefik/pull/12424) by [kevinpollet](https://github.com/kevinpollet))
|
||||||
|
|
||||||
|
**Documentation:**
|
||||||
|
- **[k8s/ingress-nginx]** Add auth-signin to unsupported nginx annotations list ([#12370](https://github.com/traefik/traefik/pull/12370) by [fibsifan](https://github.com/fibsifan))
|
||||||
|
- Add a Breaking change note to the changelog ([#12398](https://github.com/traefik/traefik/pull/12398) by [nmengin](https://github.com/nmengin))
|
||||||
|
- Fix encodedCharacters entryPoint option documentation ([#12385](https://github.com/traefik/traefik/pull/12385) by [rtribotte](https://github.com/rtribotte))
|
||||||
|
|
||||||
|
## [v3.6.4](https://github.com/traefik/traefik/tree/v3.6.4) (2025-12-05)
|
||||||
|
[All Commits](https://github.com/traefik/traefik/compare/v3.6.2...v3.6.4)
|
||||||
|
|
||||||
|
**CVE's fixed:**
|
||||||
|
- [CVE-2025-66490](https://nvd.nist.gov/vuln/detail/CVE-2025-66490) (Advisory [GHSA-gm3x-23wp-hc2c](https://github.com/traefik/traefik/security/advisories/GHSA-gm3x-23wp-hc2c)): **Breaking Change** please read the [migration guide](https://doc.traefik.io/traefik/v3.6/migrate/v3/#v364).
|
||||||
|
- [CVE-2025-66491](https://nvd.nist.gov/vuln/detail/CVE-2025-66491) (Advisory [GHSA-7vww-mvcr-x6vj](https://github.com/traefik/traefik/security/advisories/GHSA-7vww-mvcr-x6vj))
|
||||||
|
|
||||||
|
**Important:** Please read the [migration guide](https://doc.traefik.io/traefik/v3.6/migrate/v3/#v364).
|
||||||
|
|
||||||
|
**Bug fixes:**
|
||||||
|
- **[server]** Reject suspicious encoded characters ([#12360](https://github.com/traefik/traefik/pull/12360) by [rtribotte](https://github.com/rtribotte))
|
||||||
|
- **[plugins]** Validate plugin module name ([#12291](https://github.com/traefik/traefik/pull/12291) by [kevinpollet](https://github.com/kevinpollet))
|
||||||
|
- **[http3]** Bump github.com/quic-go/quic-go to v0.57.1 ([#12319](https://github.com/traefik/traefik/pull/12319) by [GreyXor](https://github.com/GreyXor))
|
||||||
|
- **[http3]** Bump github.com/quic-go/quic-go to v0.57.0 ([#12308](https://github.com/traefik/traefik/pull/12308) by [GreyXor](https://github.com/GreyXor))
|
||||||
|
- **[server]** Bump golang.org/x/crypto to v0.45.0 ([#12296](https://github.com/traefik/traefik/pull/12296) by [kevinpollet](https://github.com/kevinpollet))
|
||||||
|
- **[acme]** Bump github.com/go-acme/lego/v4 to v4.29.0 ([#12333](https://github.com/traefik/traefik/pull/12333) by [ldez](https://github.com/ldez))
|
||||||
|
- **[k8s/ingress-nginx]** Fix SSL redirect to match NGINX behavior ([#12361](https://github.com/traefik/traefik/pull/12361) by [mmatur](https://github.com/mmatur))
|
||||||
|
- **[k8s/ingress-nginx]** Fix the service name for ingress-nginx provider ([#12352](https://github.com/traefik/traefik/pull/12352) by [mmatur](https://github.com/mmatur))
|
||||||
|
- **[k8s/ingress-nginx]** Fix nginx.ingress.kubernetes.io/proxy-ssl-verify annotation support ([#12351](https://github.com/traefik/traefik/pull/12351) by [rtribotte](https://github.com/rtribotte))
|
||||||
|
- **[middleware,authentication]** Change ForwardAuth error log level from DEBUG to ERROR ([#12324](https://github.com/traefik/traefik/pull/12324) by [murataslan1](https://github.com/murataslan1))
|
||||||
|
|
||||||
|
**Documentation:**
|
||||||
|
- **[api]** Fix typo in API dashboard configuration instructions ([#12335](https://github.com/traefik/traefik/pull/12335) by [NAICOLAS](https://github.com/NAICOLAS))
|
||||||
|
- **[docker]** Add documentation for loadbalancer.server.url in Docker and Swarm providers ([#12289](https://github.com/traefik/traefik/pull/12289) by [webash](https://github.com/webash))
|
||||||
|
- **[k8s/gatewayapi]** Fix links of Helm chart values reference to providers.kubernetesGateway.enabled ([#12315](https://github.com/traefik/traefik/pull/12315) by [shouhei](https://github.com/shouhei))
|
||||||
|
- **[k8s/ingress-nginx]** Fix default value of ingress-nginx provider in documentation ([#12328](https://github.com/traefik/traefik/pull/12328) by [mloiseleur](https://github.com/mloiseleur))
|
||||||
|
- **[k8s/ingress-nginx]** NGINX Ingress Controller to Traefik Migration Guide ([#12318](https://github.com/traefik/traefik/pull/12318) by [sheddy-traefik](https://github.com/sheddy-traefik))
|
||||||
|
- **[k8s/ingress-nginx]** Improve the configuration options display of the Kubernetes ingress-nginx provider ([#12297](https://github.com/traefik/traefik/pull/12297) by [mloiseleur](https://github.com/mloiseleur))
|
||||||
|
- **[k8s/ingress-nginx]** Improve ingress-nginx provider documentation ([#12288](https://github.com/traefik/traefik/pull/12288) by [sheddy-traefik](https://github.com/sheddy-traefik))
|
||||||
|
- **[service]** Fix loadbalancer doc for highest random weight ([#12283](https://github.com/traefik/traefik/pull/12283) by [ozon2](https://github.com/ozon2))
|
||||||
|
- Correctly Format the HTTP Service Documentation ([#12311](https://github.com/traefik/traefik/pull/12311) by [sheddy-traefik](https://github.com/sheddy-traefik))
|
||||||
|
- Add documentation about checkNewVersion ([#12298](https://github.com/traefik/traefik/pull/12298) by [darkweaver87](https://github.com/darkweaver87))
|
||||||
|
|
||||||
|
**Misc:**
|
||||||
|
- Merge branch v2.11 into v3.6 ([#12364](https://github.com/traefik/traefik/pull/12364) by [kevinpollet](https://github.com/kevinpollet))
|
||||||
|
- Merge branch v2.11 into v3.6 ([#12341](https://github.com/traefik/traefik/pull/12341) by [mmatur](https://github.com/mmatur))
|
||||||
|
- Merge branch v2.11 into v3.6 ([#12368](https://github.com/traefik/traefik/pull/12368) by [mmatur](https://github.com/mmatur))
|
||||||
|
|
||||||
|
## [v3.6.3](https://github.com/traefik/traefik/tree/v3.6.3) (2025-12-04)
|
||||||
|
[All Commits](https://github.com/traefik/traefik/compare/v3.6.2...v3.6.3)
|
||||||
|
|
||||||
|
Release canceled.
|
||||||
|
|
||||||
|
## [v2.11.32](https://github.com/traefik/traefik/tree/v2.11.32) (2025-12-04)
|
||||||
|
[All Commits](https://github.com/traefik/traefik/compare/v2.11.31...v2.11.32)
|
||||||
|
|
||||||
|
**Bug fixes:**
|
||||||
|
- **[server]** Reject suspicious encoded characters ([#12360](https://github.com/traefik/traefik/pull/12360) by [rtribotte](https://github.com/rtribotte))
|
||||||
|
- **[plugins]** Validate plugin module name ([#12291](https://github.com/traefik/traefik/pull/12291) by [kevinpollet](https://github.com/kevinpollet))
|
||||||
|
- **[http3]** Bump github.com/quic-go/quic-go to v0.57.1 ([#12319](https://github.com/traefik/traefik/pull/12319) by [GreyXor](https://github.com/GreyXor))
|
||||||
|
- **[http3]** Bump github.com/quic-go/quic-go to v0.57.0 ([#12308](https://github.com/traefik/traefik/pull/12308) by [GreyXor](https://github.com/GreyXor))
|
||||||
|
- **[server]** Bump golang.org/x/crypto to v0.45.0 ([#12296](https://github.com/traefik/traefik/pull/12296) by [kevinpollet](https://github.com/kevinpollet))
|
||||||
|
|
||||||
|
**Documentation:**
|
||||||
|
- Update SECURITY.md to streamline information ([#12310](https://github.com/traefik/traefik/pull/12310) by [emilevauge](https://github.com/emilevauge))
|
||||||
|
- Update SECURITY.md ([#12304](https://github.com/traefik/traefik/pull/12304) by [cwayne18](https://github.com/cwayne18))
|
||||||
|
|
||||||
## [v3.6.2](https://github.com/traefik/traefik/tree/v3.6.2) (2025-11-18)
|
## [v3.6.2](https://github.com/traefik/traefik/tree/v3.6.2) (2025-11-18)
|
||||||
[All Commits](https://github.com/traefik/traefik/compare/v3.6.1...v3.6.2)
|
[All Commits](https://github.com/traefik/traefik/compare/v3.6.1...v3.6.2)
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,11 @@ func runCmd(staticConfiguration *static.Configuration) error {
|
|||||||
return fmt.Errorf("setting up logger: %w", err)
|
return fmt.Errorf("setting up logger: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Display warning to advertise for new behavior of rejecting encoded characters in the request path.
|
||||||
|
// Deprecated: this has to be removed in the next minor/major version.
|
||||||
|
log.Warn().Msg("Starting with v3.6.3, Traefik now rejects some encoded characters in the request path by default. " +
|
||||||
|
"Refer to the documentation for more details: https://doc.traefik.io/traefik/migrate/v3/#encoded-characters-in-request-path")
|
||||||
|
|
||||||
http.DefaultTransport.(*http.Transport).Proxy = http.ProxyFromEnvironment
|
http.DefaultTransport.(*http.Transport).Proxy = http.ProxyFromEnvironment
|
||||||
|
|
||||||
staticConfiguration.SetEffectiveConfiguration()
|
staticConfiguration.SetEffectiveConfiguration()
|
||||||
|
|||||||
@@ -6,25 +6,14 @@ Below is a non-exhaustive list of versions and their maintenance status:
|
|||||||
|
|
||||||
| Version | Release Date | Active Support | Security Support |
|
| Version | Release Date | Active Support | Security Support |
|
||||||
|---------|--------------|--------------------|-------------------|
|
|---------|--------------|--------------------|-------------------|
|
||||||
| 3.5 | Jul 23, 2025 | Yes | Yes |
|
| 3.6 | Nov 07, 2025 | Yes | Yes |
|
||||||
|
| 3.5 | Jul 23, 2025 | Ended Nov 07, 2025 | No |
|
||||||
| 3.4 | May 05, 2025 | Ended Jul 23, 2025 | No |
|
| 3.4 | May 05, 2025 | Ended Jul 23, 2025 | No |
|
||||||
| 3.3 | Jan 06, 2025 | Ended May 05, 2025 | No |
|
| 3.3 | Jan 06, 2025 | Ended May 05, 2025 | No |
|
||||||
| 3.2 | Oct 28, 2024 | Ended Jan 06, 2025 | No |
|
| 3.2 | Oct 28, 2024 | Ended Jan 06, 2025 | No |
|
||||||
| 3.1 | Jul 15, 2024 | Ended Oct 28, 2024 | No |
|
| 3.1 | Jul 15, 2024 | Ended Oct 28, 2024 | No |
|
||||||
| 3.0 | Apr 29, 2024 | Ended Jul 15, 2024 | No |
|
| 3.0 | Apr 29, 2024 | Ended Jul 15, 2024 | No |
|
||||||
| 2.11 | Feb 12, 2024 | Ended Apr 29, 2025 | Ends Feb 01, 2026 |
|
| 2.11 | Feb 12, 2024 | Ended Apr 29, 2025 | Ends Feb 01, 2026 |
|
||||||
| 2.10 | Apr 24, 2023 | Ended Feb 12, 2024 | No |
|
|
||||||
| 2.9 | Oct 03, 2022 | Ended Apr 24, 2023 | No |
|
|
||||||
| 2.8 | Jun 29, 2022 | Ended Oct 03, 2022 | No |
|
|
||||||
| 2.7 | May 24, 2022 | Ended Jun 29, 2022 | No |
|
|
||||||
| 2.6 | Jan 24, 2022 | Ended May 24, 2022 | No |
|
|
||||||
| 2.5 | Aug 17, 2021 | Ended Jan 24, 2022 | No |
|
|
||||||
| 2.4 | Jan 19, 2021 | Ended Aug 17, 2021 | No |
|
|
||||||
| 2.3 | Sep 23, 2020 | Ended Jan 19, 2021 | No |
|
|
||||||
| 2.2 | Mar 25, 2020 | Ended Sep 23, 2020 | No |
|
|
||||||
| 2.1 | Dec 11, 2019 | Ended Mar 25, 2020 | No |
|
|
||||||
| 2.0 | Sep 16, 2019 | Ended Dec 11, 2019 | No |
|
|
||||||
| 1.7 | Sep 24, 2018 | Ended Dec 31, 2021 | No |
|
|
||||||
|
|
||||||
??? example "Active Support / Security Support"
|
??? example "Active Support / Security Support"
|
||||||
|
|
||||||
|
|||||||
678
docs/content/migrate/nginx-to-traefik.md
Normal file
678
docs/content/migrate/nginx-to-traefik.md
Normal file
@@ -0,0 +1,678 @@
|
|||||||
|
---
|
||||||
|
title: "Migrate from Ingress NGINX Controller to Traefik"
|
||||||
|
description: "Step-by-step guide to migrate from Kubernetes Ingress NGINX Controller to Traefik with zero downtime and annotation compatibility."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Migrate from Ingress NGINX Controller to Traefik
|
||||||
|
|
||||||
|
How to migrate from Ingress NGINX Controller to Traefik with zero downtime.
|
||||||
|
{: .subtitle }
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
!!! danger "Ingress NGINX Controller Retirement"
|
||||||
|
|
||||||
|
The Kubernetes Ingress NGINX Controller project has announced its retirement in **March 2026**. After this date:
|
||||||
|
|
||||||
|
- No new releases or updates
|
||||||
|
- No security patches
|
||||||
|
- No bug fixes
|
||||||
|
|
||||||
|
For more information, see the [official Kubernetes blog announcement](https://kubernetes.io/blog/2025/11/11/ingress-nginx-retirement).
|
||||||
|
|
||||||
|
## What You Will Achieve
|
||||||
|
|
||||||
|
By completing this migration, your existing Ingress resources will work with Traefik without any modifications. The Traefik Kubernetes Ingress NGINX Provider automatically translates NGINX annotations into Traefik configuration:
|
||||||
|
|
||||||
|
```yaml tab="Your Existing Ingress (No Changes Needed)"
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: myapp
|
||||||
|
annotations:
|
||||||
|
# These NGINX annotations are automatically translated by Traefik
|
||||||
|
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||||
|
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||||
|
nginx.ingress.kubernetes.io/enable-cors: "true"
|
||||||
|
nginx.ingress.kubernetes.io/cors-allow-origin: "https://example.com"
|
||||||
|
nginx.ingress.kubernetes.io/affinity: "cookie"
|
||||||
|
nginx.ingress.kubernetes.io/session-cookie-name: "route"
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx # ← Traefik will watch this class
|
||||||
|
rules:
|
||||||
|
- host: myapp.example.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: whoami
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: whoami
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: whoami
|
||||||
|
image: traefik/whoami
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: whoami
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 80
|
||||||
|
targetPort: 80
|
||||||
|
```
|
||||||
|
|
||||||
|
For a complete list of supported annotations and behavioral differences, see the [Ingress NGINX Routing Configuration](../reference/routing-configuration/kubernetes/ingress-nginx.md) documentation.
|
||||||
|
|
||||||
|
!!! info "Traefik Version Requirement"
|
||||||
|
|
||||||
|
The Kubernetes Ingress NGINX provider requires **Traefik v3.6.2 or later**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before starting the migration, ensure you have:
|
||||||
|
|
||||||
|
- **Existing Ingress NGINX Controller** running in your Kubernetes cluster
|
||||||
|
- **Kubernetes cluster access** with `kubectl` configured
|
||||||
|
- **Cluster support for running multiple LoadBalancer services** on ports 80/443 simultaneously
|
||||||
|
- **Helm**
|
||||||
|
- **Cluster admin permissions** to create RBAC resources
|
||||||
|
- **Backup of critical configurations** (Ingress resources, ConfigMaps, Secrets)
|
||||||
|
|
||||||
|
!!! tip "Backup Recommendations"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Export all Ingress resources
|
||||||
|
kubectl get ingress --all-namespaces -o yaml > ingress-backup.yaml
|
||||||
|
|
||||||
|
# Export NGINX ConfigMaps
|
||||||
|
kubectl get configmap --all-namespaces -l app.kubernetes.io/name=ingress-nginx -o yaml > nginx-configmaps.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Strategy Overview
|
||||||
|
|
||||||
|
This migration achieves **zero downtime** by running Traefik alongside NGINX. Both controllers serve the same Ingress resources simultaneously, allowing you to progressively shift traffic before removing NGINX.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Current: DNS → LoadBalancer → NGINX → Your Services
|
||||||
|
|
||||||
|
Migration: DNS → LoadBalancer → NGINX → Your Services
|
||||||
|
→ LoadBalancer → Traefik → Your Services
|
||||||
|
|
||||||
|
Final: DNS → LoadBalancer → Traefik → Your Services
|
||||||
|
```
|
||||||
|
|
||||||
|
**Migration Flow:**
|
||||||
|
|
||||||
|
1. Install Traefik alongside NGINX (both serving traffic in parallel)
|
||||||
|
2. Add Traefik LoadBalancer to DNS (if you choose DNS option; cf. step 3)
|
||||||
|
3. Progressively shift traffic from NGINX to Traefik
|
||||||
|
4. Remove NGINX from DNS, preserve the IngressClass, and uninstall
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: Install Traefik Alongside NGINX
|
||||||
|
|
||||||
|
??? info "Install Ingress NGINX Controller"
|
||||||
|
|
||||||
|
If you have not installed Ingress NGINX Controller yet, you can set up a fresh Ingress NGINX Controller installation following the instructions below:
|
||||||
|
|
||||||
|
### Install Ingress NGINX Controller
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm upgrade --install ingress-nginx ingress-nginx \
|
||||||
|
--repo https://kubernetes.github.io/ingress-nginx \
|
||||||
|
--namespace ingress-nginx --create-namespace
|
||||||
|
```
|
||||||
|
Install Traefik with the Kubernetes Ingress NGINX provider enabled. Both controllers will serve the same Ingress resources simultaneously.
|
||||||
|
|
||||||
|
### Add Traefik Helm Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm repo add traefik https://traefik.github.io/charts
|
||||||
|
helm repo update
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install Traefik
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm upgrade --install traefik traefik/traefik \
|
||||||
|
--namespace traefik --create-namespace \
|
||||||
|
--set providers.kubernetesIngressNginx.enabled=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Or using a [values file](https://github.com/traefik/traefik-helm-chart/blob/master/traefik/VALUES.md) for more configuration:
|
||||||
|
|
||||||
|
```yaml tab="traefik-values.yaml"
|
||||||
|
...
|
||||||
|
providers:
|
||||||
|
kubernetesIngressNginx:
|
||||||
|
enabled: true
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm upgrade --install traefik traefik/traefik \
|
||||||
|
--namespace traefik --create-namespace \
|
||||||
|
--values traefik-values.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify Both Controllers Are Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check NGINX pods
|
||||||
|
kubectl get pods -n ingress-nginx
|
||||||
|
|
||||||
|
# Check Traefik pods
|
||||||
|
kubectl get pods -n traefik
|
||||||
|
|
||||||
|
# Check both services have LoadBalancer IPs
|
||||||
|
kubectl get svc -n ingress-nginx ingress-nginx-controller
|
||||||
|
kubectl get svc -n traefik traefik
|
||||||
|
```
|
||||||
|
|
||||||
|
At this point, both NGINX and Traefik are running and can serve the same Ingress resources. Traffic is still flowing only through NGINX since DNS points to the NGINX LoadBalancer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: Verify Traefik Is Handling Traffic
|
||||||
|
|
||||||
|
Before adding Traefik to DNS, verify it correctly serves your Ingress resources.
|
||||||
|
|
||||||
|
### Test via Traefik's LoadBalancer IP
|
||||||
|
|
||||||
|
Get Traefik's LoadBalancer IP and use `--resolve` to test without changing DNS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get LoadBalancer IPs
|
||||||
|
NGINX_IP=$(kubectl get svc -n ingress-nginx ingress-nginx-controller -o go-template='{{ $ing := index .status.loadBalancer.ingress 0 }}{{ if $ing.ip }}{{ $ing.ip }}{{ else }}{{ $ing.hostname }}{{ end }}')
|
||||||
|
TRAEFIK_IP=$(kubectl get svc -n traefik traefik -o go-template='{{ $ing := index .status.loadBalancer.ingress 0 }}{{ if $ing.ip }}{{ $ing.ip }}{{ else }}{{ $ing.hostname }}{{ end }}')
|
||||||
|
echo -e "Nginx IP: $NGINX_IP\nTraefik IP: $TRAEFIK_IP"
|
||||||
|
|
||||||
|
# Test HTTP for both
|
||||||
|
FQDN=myapp.example.com
|
||||||
|
# Observe HTTPS redirections:
|
||||||
|
curl --connect-to "${FQDN}:80:${NGINX_IP}:80" "http://${FQDN}" -D -
|
||||||
|
curl --connect-to "${FQDN}:80:${TRAEFIK_IP}:80" "http://${FQDN}" -D - # note X-Forwarded-Server which should be traefik
|
||||||
|
|
||||||
|
# Test HTTPS
|
||||||
|
curl --connect-to "${FQDN}:443:${NGINX_IP}:443" "https://${FQDN}"
|
||||||
|
curl --connect-to "${FQDN}:443:${TRAEFIK_IP}:443" "https://${FQDN}"
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning "TLS Certificates During Migration"
|
||||||
|
|
||||||
|
Both NGINX and Traefik must serve valid TLS certificates for HTTPS tests to succeed. Since Traefik is not publicly exposed during this verification phase, **Let's Encrypt HTTP challenge will not work**.
|
||||||
|
|
||||||
|
Your options for TLS certificates during migration:
|
||||||
|
|
||||||
|
- **Existing certificates via `tls.secretName`** - If you use cert-manager or another external tool, your existing TLS secrets referenced in `spec.tls` will work with both controllers
|
||||||
|
- **Let's Encrypt DNS challenge** - Configure Traefik's [ACME DNS challenge](../reference/install-configuration/tls/certificate-resolvers/acme.md#dnschallenge) to obtain certificates without public exposure
|
||||||
|
|
||||||
|
Avoid using `curl -k` (skip certificate verification) as this masks TLS configuration issues that could cause problems after migration.
|
||||||
|
|
||||||
|
### Verify Ingress Discovery
|
||||||
|
|
||||||
|
Check Traefik logs to confirm it discovered your Ingress resources:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl logs -n traefik deployment/traefik | grep -i "ingress"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: Shift Traffic to Traefik
|
||||||
|
|
||||||
|
With both controllers running and verified, progressively shift traffic from NGINX to Traefik.
|
||||||
|
|
||||||
|
### Option A: DNS-Based Migration
|
||||||
|
|
||||||
|
Add the Traefik LoadBalancer IP to your DNS records alongside NGINX. This allows both controllers to receive traffic.
|
||||||
|
|
||||||
|
**Get LoadBalancer addresses:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# NGINX LoadBalancer
|
||||||
|
echo $(kubectl get svc -n ingress-nginx ingress-nginx-controller -o go-template='{{ $ing := index .status.loadBalancer.ingress 0 }}{{ if $ing.ip }}{{ $ing.ip }}{{ else }}{{ $ing.hostname }}{{ end }}')
|
||||||
|
|
||||||
|
# Traefik LoadBalancer
|
||||||
|
echo $(kubectl get svc -n traefik traefik -o go-template='{{ $ing := index .status.loadBalancer.ingress 0 }}{{ if $ing.ip }}{{ $ing.ip }}{{ else }}{{ $ing.hostname }}{{ end }}')
|
||||||
|
```
|
||||||
|
|
||||||
|
**Progressive DNS migration:**
|
||||||
|
|
||||||
|
1. **Add Traefik to DNS** - Add the Traefik LoadBalancer IP to your DNS records (both IPs now receive traffic via round-robin)
|
||||||
|
2. **Monitor** - Observe traffic patterns on both controllers
|
||||||
|
3. **Remove NGINX from DNS** - Once confident, remove the NGINX LoadBalancer IP from DNS
|
||||||
|
4. **Wait for DNS propagation** - Allow time for DNS caches to expire
|
||||||
|
5. **Uninstall NGINX** - Proceed to [Step 4](#step-4-uninstall-ingress-nginx-controller)
|
||||||
|
|
||||||
|
!!! warning "DNS TTL May Not Be Respected"
|
||||||
|
|
||||||
|
Some ISPs ignore DNS TTL values to reduce traffic costs, caching records longer than specified. After removing NGINX from DNS, keep NGINX running for at least 24-48 hours before uninstalling to avoid dropping traffic from users whose ISPs have stale DNS caches.
|
||||||
|
|
||||||
|
??? info "ExternalDNS Users"
|
||||||
|
|
||||||
|
If you use [ExternalDNS](https://github.com/kubernetes-sigs/external-dns) to automatically manage DNS records based on Ingress status, both NGINX and Traefik will compete to update the Ingress status with their LoadBalancer IPs when `publishService` is enabled. Traefik typically wins because it updates faster, which can cause unexpected traffic shifts.
|
||||||
|
|
||||||
|
**Recommended approach for ExternalDNS:**
|
||||||
|
|
||||||
|
1. **[Install Traefik](#step-1-install-traefik-alongside-nginx) with `publishService` disabled**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# traefik-values.yaml
|
||||||
|
providers:
|
||||||
|
kubernetesIngressNginx:
|
||||||
|
enabled: true
|
||||||
|
publishService:
|
||||||
|
enabled: false # Disable to prevent status updates
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Test Traefik** using [port-forward](#step-2-verify-traefik-is-handling-traffic) or a separate test hostname
|
||||||
|
|
||||||
|
3. **Switch DNS via NGINX** - Configure NGINX to publish Traefik's service address:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# nginx-values.yaml
|
||||||
|
controller:
|
||||||
|
publishService:
|
||||||
|
pathOverride: "traefik/traefik" # Points to Traefik's service
|
||||||
|
```
|
||||||
|
|
||||||
|
This makes NGINX update the Ingress status with Traefik's LoadBalancer IP, causing ExternalDNS to point traffic to Traefik.
|
||||||
|
|
||||||
|
4. **Verify traffic flows through Traefik** - At this point, you can still rollback by removing the `pathOverride`
|
||||||
|
|
||||||
|
5. **[Enable `publishService` on Traefik](#step-1-install-traefik-alongside-nginx)** and [uninstall NGINX](#step-5-uninstall-nginx-ingress-controller)
|
||||||
|
|
||||||
|
### Option B: External Load Balancer with Weighted Traffic
|
||||||
|
|
||||||
|
For more control over traffic distribution, use an external load balancer (like Traefik, Cloudflare, AWS ALB, or a dedicated load balancer) in front of both Kubernetes LoadBalancers.
|
||||||
|
|
||||||
|
!!! note "Infrastructure Prerequisite"
|
||||||
|
|
||||||
|
This option assumes you already have an external load balancer in your infrastructure, or are willing to set one up **before** starting the migration. Adding an external load balancer is a significant infrastructure change that should be planned and tested separately from the ingress controller migration.
|
||||||
|
|
||||||
|
**Setup:**
|
||||||
|
|
||||||
|
1. Create an external load balancer pointing to the NGINX Kubernetes LoadBalancer
|
||||||
|
2. Update DNS to point to the external load balancer
|
||||||
|
3. Add the Traefik Kubernetes LoadBalancer to the external load balancer with a low weight (e.g., 10%)
|
||||||
|
4. Gradually increase Traefik's weight while decreasing NGINX's weight
|
||||||
|
5. Once NGINX receives no traffic, uninstall it
|
||||||
|
|
||||||
|
**Example weight progression:**
|
||||||
|
|
||||||
|
| Phase | NGINX Weight | Traefik Weight | Duration |
|
||||||
|
|-------|-------------|----------------|----------|
|
||||||
|
| Initial | 100% | 0% | - |
|
||||||
|
| Start | 90% | 10% | 1 hour |
|
||||||
|
| Increase | 50% | 50% | 2 hour |
|
||||||
|
| Near-complete | 10% | 90% | 4 hour |
|
||||||
|
| Final | 0% | 100% | - |
|
||||||
|
|
||||||
|
!!! tip "External Load Balancer Options"
|
||||||
|
|
||||||
|
- **Cloudflare Load Balancing** - Traffic steering with health checks
|
||||||
|
- **AWS Global Accelerator** - Weighted routing across endpoints
|
||||||
|
- **Google Cloud Load Balancing** - Traffic splitting
|
||||||
|
- **Traefik / HAProxy / NGINX (external)** - Self-hosted option with weighted backends
|
||||||
|
- ...
|
||||||
|
|
||||||
|
### LoadBalancer IP Retention
|
||||||
|
|
||||||
|
If you want Traefik to eventually use the same LoadBalancer IP as NGINX (to simplify DNS management), you can transfer the IP after the migration. Since Traefik is already running with its own LoadBalancer, this can be done with zero downtime.
|
||||||
|
|
||||||
|
**Zero-downtime IP transfer process:**
|
||||||
|
|
||||||
|
1. Traefik is already running with its own LoadBalancer IP (from Step 1)
|
||||||
|
2. Add Traefik's LoadBalancer IP to DNS (traffic now goes to both NGINX and Traefik)
|
||||||
|
3. Remove NGINX's IP from DNS and wait for propagation
|
||||||
|
4. Delete NGINX's LoadBalancer service to release the IP
|
||||||
|
5. Upgrade Traefik to claim the released IP
|
||||||
|
6. (Optional) Remove Traefik's old IP from DNS once the new IP is active
|
||||||
|
|
||||||
|
This way, traffic is always flowing to Traefik during the IP transfer.
|
||||||
|
|
||||||
|
**Get your current NGINX LoadBalancer IP:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get svc -n ingress-nginx ingress-nginx-controller -o go-template='{{ $ing := index .status.loadBalancer.ingress 0 }}{{ if $ing.ip }}{{ $ing.ip }}{{ else }}{{ $ing.hostname }}{{ end }}'
|
||||||
|
```
|
||||||
|
|
||||||
|
??? note "AWS (Network Load Balancer with Elastic IPs)"
|
||||||
|
|
||||||
|
AWS does not support static IPs for Classic Load Balancers. Use Network Load Balancers (NLB) with Elastic IPs instead. This requires the [AWS Load Balancer Controller](https://kubernetes-sigs.github.io/aws-load-balancer-controller/) to be installed in your cluster.
|
||||||
|
|
||||||
|
**Pre-allocate Elastic IPs (one per availability zone):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aws ec2 allocate-address --domain vpc --region <your-region>
|
||||||
|
# Note the AllocationId (eipalloc-xxx) for each EIP
|
||||||
|
```
|
||||||
|
|
||||||
|
**Update `traefik-values.yaml`:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
service:
|
||||||
|
type: LoadBalancer
|
||||||
|
loadBalancerClass: service.k8s.aws/nlb # Requires AWS Load Balancer Controller
|
||||||
|
annotations:
|
||||||
|
service.beta.kubernetes.io/aws-load-balancer-type: "external"
|
||||||
|
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip"
|
||||||
|
service.beta.kubernetes.io/aws-load-balancer-eip-allocations: "eipalloc-xxx,eipalloc-yyy"
|
||||||
|
```
|
||||||
|
|
||||||
|
For more details, see the [AWS Load Balancer Controller annotations documentation](https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/guide/service/annotations/).
|
||||||
|
|
||||||
|
??? note "Azure"
|
||||||
|
|
||||||
|
Azure supports static public IPs for Load Balancers.
|
||||||
|
|
||||||
|
**Identify existing public IP:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
az network public-ip list --resource-group <your-resource-group> \
|
||||||
|
--query "[?ipAddress=='<your-ip>'].name" -o tsv
|
||||||
|
```
|
||||||
|
|
||||||
|
**Update `traefik-values.yaml`:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
service:
|
||||||
|
type: LoadBalancer
|
||||||
|
annotations:
|
||||||
|
# Only needed if the public IP is in a different resource group than the AKS cluster
|
||||||
|
service.beta.kubernetes.io/azure-load-balancer-resource-group: "<public-ip-resource-group>"
|
||||||
|
spec:
|
||||||
|
loadBalancerIP: "<your-existing-ip>"
|
||||||
|
```
|
||||||
|
|
||||||
|
For more details, see the [Azure AKS static IP documentation](https://learn.microsoft.com/en-us/azure/aks/static-ip).
|
||||||
|
|
||||||
|
??? note "GCP"
|
||||||
|
|
||||||
|
GCP supports static IPs through reserved regional IP addresses.
|
||||||
|
|
||||||
|
**Reserve or identify existing IP:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List existing static IPs
|
||||||
|
gcloud compute addresses list
|
||||||
|
|
||||||
|
# Or reserve a new regional static IP (must be in the same region as your GKE cluster)
|
||||||
|
gcloud compute addresses create traefik-ip --region <your-cluster-region>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Update `traefik-values.yaml`:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
service:
|
||||||
|
type: LoadBalancer
|
||||||
|
spec:
|
||||||
|
loadBalancerIP: "<your-static-ip>"
|
||||||
|
```
|
||||||
|
|
||||||
|
For more details, see the [GKE LoadBalancer Service parameters documentation](https://cloud.google.com/kubernetes-engine/docs/concepts/service-load-balancer-parameters).
|
||||||
|
|
||||||
|
??? note "Other Cloud Providers"
|
||||||
|
|
||||||
|
- **DigitalOcean:** Supports `loadBalancerIP` with floating IPs
|
||||||
|
- **Linode:** Supports `loadBalancerIP` specification
|
||||||
|
- **Bare Metal (MetalLB):** Use IP address pools
|
||||||
|
|
||||||
|
**Transfer the IP:**
|
||||||
|
|
||||||
|
Once DNS is pointing to Traefik and your values are configured with the target IP:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ensure Traefik is already receiving traffic via its current LoadBalancer
|
||||||
|
kubectl get svc -n traefik traefik
|
||||||
|
|
||||||
|
# Delete NGINX LoadBalancer service to release the IP
|
||||||
|
kubectl delete svc -n ingress-nginx ingress-nginx-controller
|
||||||
|
|
||||||
|
# Upgrade Traefik to claim the released IP
|
||||||
|
helm upgrade traefik traefik/traefik \
|
||||||
|
--namespace traefik \
|
||||||
|
--values traefik-values.yaml
|
||||||
|
|
||||||
|
# Verify Traefik now has the old NGINX IP
|
||||||
|
kubectl get svc -n traefik traefik
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! tip "Zero Downtime During Helm Upgrade"
|
||||||
|
|
||||||
|
The Helm upgrade only restarts the Traefik pod, not the LoadBalancer service. Traefik uses a `RollingUpdate` deployment strategy by default, so the new pod starts before the old one terminates. For additional safety, configure high availability:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# In traefik-values.yaml
|
||||||
|
deployment:
|
||||||
|
replicas: 2
|
||||||
|
|
||||||
|
# Spread pods across nodes to survive node failures
|
||||||
|
affinity:
|
||||||
|
podAntiAffinity:
|
||||||
|
requiredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
- labelSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: traefik
|
||||||
|
app.kubernetes.io/instance: traefik
|
||||||
|
topologyKey: kubernetes.io/hostname
|
||||||
|
|
||||||
|
# Ensure at least one pod is always available during disruptions
|
||||||
|
podDisruptionBudget:
|
||||||
|
enabled: true
|
||||||
|
minAvailable: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
With multiple replicas spread across nodes and a PodDisruptionBudget, at least one pod is always running during upgrades and node maintenance.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4: Uninstall Ingress NGINX Controller
|
||||||
|
|
||||||
|
Once NGINX is no longer receiving traffic, remove it from your cluster. Before uninstalling, you must ensure the `nginx` IngressClass is preserved. Traefik needs it to continue discovering your Ingresses.
|
||||||
|
|
||||||
|
### Preserve the IngressClass
|
||||||
|
|
||||||
|
??? note "If NGINX Was Installed via Helm"
|
||||||
|
|
||||||
|
Add the `helm.sh/resource-policy: keep` annotation to tell Helm to preserve the IngressClass:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add the required annotation
|
||||||
|
helm upgrade ingress-nginx ingress-nginx \
|
||||||
|
--repo https://kubernetes.github.io/ingress-nginx \
|
||||||
|
--namespace ingress-nginx \
|
||||||
|
--reuse-values \
|
||||||
|
--set-json 'controller.ingressClassResource.annotations={"helm.sh/resource-policy": "keep"}'
|
||||||
|
# Check that the annotation is really here
|
||||||
|
kubectl describe ingressclass nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
The `--reuse-values` flag is critical - it preserves all your existing NGINX configuration. Without it, Helm would reset everything to defaults, potentially breaking your setup.
|
||||||
|
|
||||||
|
!!! info "kubectl annotate/patch/edit does not work"
|
||||||
|
|
||||||
|
Adding the annotation via `kubectl annotate`, `kubectl patch`, or `kubectl edit` will not preserve the IngressClass. Helm stores its release state internally and checks annotations from its internal manifest, not the live cluster state. Only `helm upgrade` updates Helm's internal state.
|
||||||
|
|
||||||
|
??? note "If NGINX Was Installed via GitOps (ArgoCD, Flux)"
|
||||||
|
|
||||||
|
Ensure the `nginx` IngressClass is defined as a standalone resource in your Git repository, separate from the NGINX Helm release:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ingressclass.yaml
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: IngressClass
|
||||||
|
metadata:
|
||||||
|
name: nginx
|
||||||
|
spec:
|
||||||
|
controller: k8s.io/ingress-nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
??? note "If NGINX Was Installed Manually"
|
||||||
|
|
||||||
|
Create the IngressClass as a standalone resource:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f - <<EOF
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: IngressClass
|
||||||
|
metadata:
|
||||||
|
name: nginx
|
||||||
|
spec:
|
||||||
|
controller: k8s.io/ingress-nginx
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete NGINX Admission Webhook
|
||||||
|
|
||||||
|
You should delete the admission webhook to avoid issues with Ingress modifications after NGINX is removed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl delete validatingwebhookconfiguration ingress-nginx-admission
|
||||||
|
kubectl delete mutatingwebhookconfiguration ingress-nginx-admission --ignore-not-found
|
||||||
|
```
|
||||||
|
|
||||||
|
### Uninstall NGINX
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm uninstall ingress-nginx -n ingress-nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
If you added the `helm.sh/resource-policy: keep` annotation, you should see:
|
||||||
|
|
||||||
|
```text
|
||||||
|
These resources were kept due to the resource policy:
|
||||||
|
[IngressClass] nginx
|
||||||
|
|
||||||
|
release "ingress-nginx" uninstalled
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify IngressClass Exists
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get ingressclass nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
In case, the ingressClass is somehow deleted, you can recreate it using the commands in [Preserve the IngressClass](#preserve-the-ingressclass).
|
||||||
|
|
||||||
|
### Clean Up NGINX Namespace
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl delete namespace ingress-nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! success "Migration Complete"
|
||||||
|
|
||||||
|
Congratulations! You have successfully migrated from Ingress NGINX Controller to Traefik with zero downtime. Your existing Ingresses with `ingressClassName: nginx` continue to work, now served by Traefik.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
There is a dashboard available in Traefik that can help to understand what's going on.
|
||||||
|
Refer to the [dedicated documentation](../reference/install-configuration/api-dashboard.md#configuration-example) to enable it.
|
||||||
|
|
||||||
|
??? note "Ingresses Not Discovered by Traefik"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify IngressClass exists
|
||||||
|
kubectl get ingressclass nginx
|
||||||
|
|
||||||
|
# Check Traefik provider configuration
|
||||||
|
kubectl logs -n traefik deployment/traefik | grep -i "nginx\|ingress"
|
||||||
|
|
||||||
|
# Verify Ingress has correct ingressClassName
|
||||||
|
kubectl get ingress <name> -o yaml | grep ingressClassName
|
||||||
|
```
|
||||||
|
|
||||||
|
??? note "Annotation Not Working as Expected"
|
||||||
|
|
||||||
|
Some NGINX annotations have behavioral differences in Traefik. Check the [limitations documentation](../reference/routing-configuration/kubernetes/ingress-nginx.md#limitations) for details.
|
||||||
|
|
||||||
|
??? note "TLS Certificates Not Working"
|
||||||
|
|
||||||
|
Existing TLS configurations continue to work with Traefik:
|
||||||
|
|
||||||
|
- Keep `spec.tls` entries exactly as-is; Traefik terminates TLS using the referenced secrets
|
||||||
|
- TLS secrets must stay in the same namespace as the Ingress
|
||||||
|
- NGINX `ssl-redirect` / `force-ssl-redirect` annotations are honored
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify TLS secret exists in the same namespace as Ingress
|
||||||
|
kubectl get secrets -n <namespace>
|
||||||
|
|
||||||
|
# Check secret format
|
||||||
|
kubectl get secret <tls-secret-name> -n <namespace> -o yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
??? note "LoadBalancer IP Not Assigned"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check service status
|
||||||
|
kubectl describe svc -n traefik traefik
|
||||||
|
|
||||||
|
# Check for events
|
||||||
|
kubectl get events -n traefik --sort-by='.lastTimestamp'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
**Learn More About Traefik:**
|
||||||
|
|
||||||
|
- [Kubernetes Ingress NGINX Install Configuration](../reference/install-configuration/providers/kubernetes/kubernetes-ingress-nginx.md) - Detailed provider configuration
|
||||||
|
- [Kubernetes Ingress NGINX Routing Configuration](../reference/routing-configuration/kubernetes/ingress-nginx.md) - Routing rules and annotation support
|
||||||
|
- [HTTP Middlewares](../reference/routing-configuration/http/middlewares/overview.md) - Extend functionality beyond NGINX annotations
|
||||||
|
- [TLS Configuration](../reference/routing-configuration/http/tls/overview.md) - Advanced TLS and certificate management
|
||||||
|
|
||||||
|
**Enhance Your Setup:**
|
||||||
|
|
||||||
|
- Enable [metrics](../reference/install-configuration/observability/metrics.md) and [tracing](../reference/install-configuration/observability/tracing.md)
|
||||||
|
- Configure [access logs](../reference/install-configuration/observability/logs-and-accesslogs.md) for observability
|
||||||
|
- Explore [Traefik Middlewares](../reference/routing-configuration/http/middlewares/overview.md) for advanced traffic management
|
||||||
|
- Migrate from Nginx-based config to Traefik [IngressRoute](../reference/routing-configuration/kubernetes/crd/http/ingressroute.md) or [Kubernetes Gateway API](../reference/routing-configuration/kubernetes/gateway-api.md)
|
||||||
|
- Consider [Traefik Hub](https://traefik.io/traefik-hub/) for enterprise features like AI & API Gateway, API Management, and advanced security
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feedback and Support
|
||||||
|
|
||||||
|
If you encounter issues during migration or have suggestions for improving this guide:
|
||||||
|
|
||||||
|
- **Report Issues:** [GitHub Issues](https://github.com/traefik/traefik/issues)
|
||||||
|
- **Community Support:** [Traefik Community Forum](https://community.traefik.io/)
|
||||||
|
- **Enterprise Support:** [Traefik Labs Commercial Support](https://traefik.io/pricing/)
|
||||||
|
|
||||||
|
We welcome contributions to improve this migration guide. See our [contribution guidelines](../contributing/submitting-pull-requests.md) to get started.
|
||||||
@@ -554,3 +554,25 @@ The KubernetesIngressNGINX Provider is no longer experimental in v3.6.2 and can
|
|||||||
|
|
||||||
1. Remove the `kubernetesIngressNGINX` option from the experimental section
|
1. Remove the `kubernetesIngressNGINX` option from the experimental section
|
||||||
2. Configure the provider using the [kubernetesIngressNGINX Provider documentation](../reference/install-configuration/providers/kubernetes/kubernetes-ingress-nginx.md)
|
2. Configure the provider using the [kubernetesIngressNGINX Provider documentation](../reference/install-configuration/providers/kubernetes/kubernetes-ingress-nginx.md)
|
||||||
|
|
||||||
|
## v3.6.4
|
||||||
|
|
||||||
|
### Encoded Characters in Request Path
|
||||||
|
|
||||||
|
Starting with `v3.6.3`, for security reasons, Traefik now rejects requests with a path containing a specific set of encoded characters by default.
|
||||||
|
|
||||||
|
When such a request is received, Traefik responds with a `400 Bad Request` status code.
|
||||||
|
|
||||||
|
Here is the list of the encoded characters that are rejected by default, along with the corresponding configuration option to allow them:
|
||||||
|
|
||||||
|
| Encoded Character | Character | Config option to allow the encoded character |
|
||||||
|
|-------------------|-------------------------|--------------------------------------------------------------------------------------|
|
||||||
|
| `%2f` or `%2F` | `/` (slash) | `entryPoints.<name>`<br/>`.http.encodedCharacters`<br/>`.allowEncodedSlash` |
|
||||||
|
| `%5c` or `%5C` | `\` (backslash) | `entryPoints.<name>.`<br/>`.http.encodedCharacters`<br/>`.allowEncodedBackSlash` |
|
||||||
|
| `%00` | `NULL` (null character) | `entryPoints.<name>.`<br/>`.http.encodedCharacters`<br/>`.allowEncodedNullCharacter` |
|
||||||
|
| `%3b` or `%3B` | `;` (semicolon) | `entryPoints.<name>.`<br/>`.http.encodedCharacters`<br/>`.allowEncodedSemicolon` |
|
||||||
|
| `%25` | `%` (percent) | `entryPoints.<name>.`<br/>`.http.encodedCharacters`<br/>`.allowEncodedPercent` |
|
||||||
|
| `%3f` or `%3F` | `?` (question mark) | `entryPoints.<name>.`<br/>`.http.encodedCharacters`<br/>`.allowEncodedQuestionMark` |
|
||||||
|
| `%23` | `#` (hash) | `entryPoints.<name>.`<br/>`.http.encodedCharacters`<br/>`.allowEncodedHash` |
|
||||||
|
|
||||||
|
Please check out the entrypoint [encodedCharacters option](../reference/install-configuration/entrypoints.md#opt-http-encodedCharacters) documentation for more details.
|
||||||
|
|||||||
@@ -2041,8 +2041,9 @@ spec:
|
|||||||
More info: https://doc.traefik.io/traefik/v3.6/middlewares/http/redirectscheme/
|
More info: https://doc.traefik.io/traefik/v3.6/middlewares/http/redirectscheme/
|
||||||
properties:
|
properties:
|
||||||
permanent:
|
permanent:
|
||||||
description: Permanent defines whether the redirection is permanent
|
description: |-
|
||||||
(308).
|
Permanent defines whether the redirection is permanent.
|
||||||
|
For HTTP GET requests a 301 is returned, otherwise a 308 is returned.
|
||||||
type: boolean
|
type: boolean
|
||||||
port:
|
port:
|
||||||
description: Port defines the port of the new URL.
|
description: Port defines the port of the new URL.
|
||||||
|
|||||||
@@ -1211,8 +1211,9 @@ spec:
|
|||||||
More info: https://doc.traefik.io/traefik/v3.6/middlewares/http/redirectscheme/
|
More info: https://doc.traefik.io/traefik/v3.6/middlewares/http/redirectscheme/
|
||||||
properties:
|
properties:
|
||||||
permanent:
|
permanent:
|
||||||
description: Permanent defines whether the redirection is permanent
|
description: |-
|
||||||
(308).
|
Permanent defines whether the redirection is permanent.
|
||||||
|
For HTTP GET requests a 301 is returned, otherwise a 308 is returned.
|
||||||
type: boolean
|
type: boolean
|
||||||
port:
|
port:
|
||||||
description: Port defines the port of the new URL.
|
description: Port defines the port of the new URL.
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ http:
|
|||||||
The API and the dashboard can be configured:
|
The API and the dashboard can be configured:
|
||||||
|
|
||||||
- In the Helm Chart: You can find the options to customize the Traefik installation
|
- In the Helm Chart: You can find the options to customize the Traefik installation
|
||||||
enabing the dashboard [here](https://github.com/traefik/traefik-helm-chart/blob/master/traefik/values.yaml#L155).
|
enabling the dashboard [here](https://github.com/traefik/traefik-helm-chart/blob/master/traefik/values.yaml#L155).
|
||||||
- In the Traefik Static Configuration as described below.
|
- In the Traefik Static Configuration as described below.
|
||||||
|
|
||||||
| Field | Description | Default | Required |
|
| Field | Description | Default | Required |
|
||||||
|
|||||||
@@ -83,9 +83,16 @@ THIS FILE MUST NOT BE EDITED BY HAND
|
|||||||
| <a id="opt-entrypoints-name-asdefault" href="#opt-entrypoints-name-asdefault" title="#opt-entrypoints-name-asdefault">entrypoints._name_.asdefault</a> | Adds this EntryPoint to the list of default EntryPoints to be used on routers that don't have any Entrypoint defined. | false |
|
| <a id="opt-entrypoints-name-asdefault" href="#opt-entrypoints-name-asdefault" title="#opt-entrypoints-name-asdefault">entrypoints._name_.asdefault</a> | Adds this EntryPoint to the list of default EntryPoints to be used on routers that don't have any Entrypoint defined. | false |
|
||||||
| <a id="opt-entrypoints-name-forwardedheaders-connection" href="#opt-entrypoints-name-forwardedheaders-connection" title="#opt-entrypoints-name-forwardedheaders-connection">entrypoints._name_.forwardedheaders.connection</a> | List of Connection headers that are allowed to pass through the middleware chain before being removed. | |
|
| <a id="opt-entrypoints-name-forwardedheaders-connection" href="#opt-entrypoints-name-forwardedheaders-connection" title="#opt-entrypoints-name-forwardedheaders-connection">entrypoints._name_.forwardedheaders.connection</a> | List of Connection headers that are allowed to pass through the middleware chain before being removed. | |
|
||||||
| <a id="opt-entrypoints-name-forwardedheaders-insecure" href="#opt-entrypoints-name-forwardedheaders-insecure" title="#opt-entrypoints-name-forwardedheaders-insecure">entrypoints._name_.forwardedheaders.insecure</a> | Trust all forwarded headers. | false |
|
| <a id="opt-entrypoints-name-forwardedheaders-insecure" href="#opt-entrypoints-name-forwardedheaders-insecure" title="#opt-entrypoints-name-forwardedheaders-insecure">entrypoints._name_.forwardedheaders.insecure</a> | Trust all forwarded headers. | false |
|
||||||
| <a id="opt-entrypoints-name-forwardedheaders-notappendxforwardedfor" href="#opt-entrypoints-name-forwardedheaders-notappendxforwardedfor" title="#opt-entrypoints-name-forwardedheaders-notappendxforwardedfor">entrypoints._name_.forwardedheaders.notappendxforwardedfor</a> | Disable appending RemoteAddr to X-Forwarded-For header. Defaults to false (appending is enabled). | false |
|
|
||||||
| <a id="opt-entrypoints-name-forwardedheaders-trustedips" href="#opt-entrypoints-name-forwardedheaders-trustedips" title="#opt-entrypoints-name-forwardedheaders-trustedips">entrypoints._name_.forwardedheaders.trustedips</a> | Trust only forwarded headers from selected IPs. | |
|
| <a id="opt-entrypoints-name-forwardedheaders-trustedips" href="#opt-entrypoints-name-forwardedheaders-trustedips" title="#opt-entrypoints-name-forwardedheaders-trustedips">entrypoints._name_.forwardedheaders.trustedips</a> | Trust only forwarded headers from selected IPs. | |
|
||||||
| <a id="opt-entrypoints-name-http" href="#opt-entrypoints-name-http" title="#opt-entrypoints-name-http">entrypoints._name_.http</a> | HTTP configuration. | |
|
| <a id="opt-entrypoints-name-http" href="#opt-entrypoints-name-http" title="#opt-entrypoints-name-http">entrypoints._name_.http</a> | HTTP configuration. | |
|
||||||
|
| <a id="opt-entrypoints-name-http-encodedcharacters" href="#opt-entrypoints-name-http-encodedcharacters" title="#opt-entrypoints-name-http-encodedcharacters">entrypoints._name_.http.encodedcharacters</a> | Defines which encoded characters are allowed in the request path. | |
|
||||||
|
| <a id="opt-entrypoints-name-http-encodedcharacters-allowencodedbackslash" href="#opt-entrypoints-name-http-encodedcharacters-allowencodedbackslash" title="#opt-entrypoints-name-http-encodedcharacters-allowencodedbackslash">entrypoints._name_.http.encodedcharacters.allowencodedbackslash</a> | Defines whether requests with encoded back slash characters in the path are allowed. | false |
|
||||||
|
| <a id="opt-entrypoints-name-http-encodedcharacters-allowencodedhash" href="#opt-entrypoints-name-http-encodedcharacters-allowencodedhash" title="#opt-entrypoints-name-http-encodedcharacters-allowencodedhash">entrypoints._name_.http.encodedcharacters.allowencodedhash</a> | Defines whether requests with encoded hash characters in the path are allowed. | false |
|
||||||
|
| <a id="opt-entrypoints-name-http-encodedcharacters-allowencodednullcharacter" href="#opt-entrypoints-name-http-encodedcharacters-allowencodednullcharacter" title="#opt-entrypoints-name-http-encodedcharacters-allowencodednullcharacter">entrypoints._name_.http.encodedcharacters.allowencodednullcharacter</a> | Defines whether requests with encoded null characters in the path are allowed. | false |
|
||||||
|
| <a id="opt-entrypoints-name-http-encodedcharacters-allowencodedpercent" href="#opt-entrypoints-name-http-encodedcharacters-allowencodedpercent" title="#opt-entrypoints-name-http-encodedcharacters-allowencodedpercent">entrypoints._name_.http.encodedcharacters.allowencodedpercent</a> | Defines whether requests with encoded percent characters in the path are allowed. | false |
|
||||||
|
| <a id="opt-entrypoints-name-http-encodedcharacters-allowencodedquestionmark" href="#opt-entrypoints-name-http-encodedcharacters-allowencodedquestionmark" title="#opt-entrypoints-name-http-encodedcharacters-allowencodedquestionmark">entrypoints._name_.http.encodedcharacters.allowencodedquestionmark</a> | Defines whether requests with encoded question mark characters in the path are allowed. | false |
|
||||||
|
| <a id="opt-entrypoints-name-http-encodedcharacters-allowencodedsemicolon" href="#opt-entrypoints-name-http-encodedcharacters-allowencodedsemicolon" title="#opt-entrypoints-name-http-encodedcharacters-allowencodedsemicolon">entrypoints._name_.http.encodedcharacters.allowencodedsemicolon</a> | Defines whether requests with encoded semicolon characters in the path are allowed. | false |
|
||||||
|
| <a id="opt-entrypoints-name-http-encodedcharacters-allowencodedslash" href="#opt-entrypoints-name-http-encodedcharacters-allowencodedslash" title="#opt-entrypoints-name-http-encodedcharacters-allowencodedslash">entrypoints._name_.http.encodedcharacters.allowencodedslash</a> | Defines whether requests with encoded slash characters in the path are allowed. | false |
|
||||||
| <a id="opt-entrypoints-name-http-encodequerysemicolons" href="#opt-entrypoints-name-http-encodequerysemicolons" title="#opt-entrypoints-name-http-encodequerysemicolons">entrypoints._name_.http.encodequerysemicolons</a> | Defines whether request query semicolons should be URLEncoded. | false |
|
| <a id="opt-entrypoints-name-http-encodequerysemicolons" href="#opt-entrypoints-name-http-encodequerysemicolons" title="#opt-entrypoints-name-http-encodequerysemicolons">entrypoints._name_.http.encodequerysemicolons</a> | Defines whether request query semicolons should be URLEncoded. | false |
|
||||||
| <a id="opt-entrypoints-name-http-maxheaderbytes" href="#opt-entrypoints-name-http-maxheaderbytes" title="#opt-entrypoints-name-http-maxheaderbytes">entrypoints._name_.http.maxheaderbytes</a> | Maximum size of request headers in bytes. | 1048576 |
|
| <a id="opt-entrypoints-name-http-maxheaderbytes" href="#opt-entrypoints-name-http-maxheaderbytes" title="#opt-entrypoints-name-http-maxheaderbytes">entrypoints._name_.http.maxheaderbytes</a> | Maximum size of request headers in bytes. | 1048576 |
|
||||||
| <a id="opt-entrypoints-name-http-middlewares" href="#opt-entrypoints-name-http-middlewares" title="#opt-entrypoints-name-http-middlewares">entrypoints._name_.http.middlewares</a> | Default middlewares for the routers linked to the entry point. | |
|
| <a id="opt-entrypoints-name-http-middlewares" href="#opt-entrypoints-name-http-middlewares" title="#opt-entrypoints-name-http-middlewares">entrypoints._name_.http.middlewares</a> | Default middlewares for the routers linked to the entry point. | |
|
||||||
@@ -142,7 +149,6 @@ THIS FILE MUST NOT BE EDITED BY HAND
|
|||||||
| <a id="opt-experimental-plugins-name-settings-useunsafe" href="#opt-experimental-plugins-name-settings-useunsafe" title="#opt-experimental-plugins-name-settings-useunsafe">experimental.plugins._name_.settings.useunsafe</a> | Allow the plugin to use unsafe and syscall packages. | false |
|
| <a id="opt-experimental-plugins-name-settings-useunsafe" href="#opt-experimental-plugins-name-settings-useunsafe" title="#opt-experimental-plugins-name-settings-useunsafe">experimental.plugins._name_.settings.useunsafe</a> | Allow the plugin to use unsafe and syscall packages. | false |
|
||||||
| <a id="opt-experimental-plugins-name-version" href="#opt-experimental-plugins-name-version" title="#opt-experimental-plugins-name-version">experimental.plugins._name_.version</a> | plugin's version. | |
|
| <a id="opt-experimental-plugins-name-version" href="#opt-experimental-plugins-name-version" title="#opt-experimental-plugins-name-version">experimental.plugins._name_.version</a> | plugin's version. | |
|
||||||
| <a id="opt-global-checknewversion" href="#opt-global-checknewversion" title="#opt-global-checknewversion">global.checknewversion</a> | Periodically check if a new version has been released. | true |
|
| <a id="opt-global-checknewversion" href="#opt-global-checknewversion" title="#opt-global-checknewversion">global.checknewversion</a> | Periodically check if a new version has been released. | true |
|
||||||
| <a id="opt-global-notappendxforwardedfor" href="#opt-global-notappendxforwardedfor" title="#opt-global-notappendxforwardedfor">global.notappendxforwardedfor</a> | Disable appending RemoteAddr to X-Forwarded-For header. Defaults to false (appending is enabled). | false |
|
|
||||||
| <a id="opt-global-sendanonymoususage" href="#opt-global-sendanonymoususage" title="#opt-global-sendanonymoususage">global.sendanonymoususage</a> | Periodically send anonymous usage statistics. If the option is not specified, it will be disabled by default. | false |
|
| <a id="opt-global-sendanonymoususage" href="#opt-global-sendanonymoususage" title="#opt-global-sendanonymoususage">global.sendanonymoususage</a> | Periodically send anonymous usage statistics. If the option is not specified, it will be disabled by default. | false |
|
||||||
| <a id="opt-hostresolver" href="#opt-hostresolver" title="#opt-hostresolver">hostresolver</a> | Enable CNAME Flattening. | false |
|
| <a id="opt-hostresolver" href="#opt-hostresolver" title="#opt-hostresolver">hostresolver</a> | Enable CNAME Flattening. | false |
|
||||||
| <a id="opt-hostresolver-cnameflattening" href="#opt-hostresolver-cnameflattening" title="#opt-hostresolver-cnameflattening">hostresolver.cnameflattening</a> | A flag to enable/disable CNAME flattening | false |
|
| <a id="opt-hostresolver-cnameflattening" href="#opt-hostresolver-cnameflattening" title="#opt-hostresolver-cnameflattening">hostresolver.cnameflattening</a> | A flag to enable/disable CNAME flattening | false |
|
||||||
|
|||||||
@@ -90,11 +90,18 @@ additionalArguments:
|
|||||||
| <a id="opt-asDefault" href="#opt-asDefault" title="#opt-asDefault">`asDefault`</a> | Mark the `entryPoint` to be in the list of default `entryPoints`.<br /> `entryPoints`in this list are used (by default) on HTTP and TCP routers that do not define their own `entryPoints` option.<br /> More information [here](#asdefault). | false | No |
|
| <a id="opt-asDefault" href="#opt-asDefault" title="#opt-asDefault">`asDefault`</a> | Mark the `entryPoint` to be in the list of default `entryPoints`.<br /> `entryPoints`in this list are used (by default) on HTTP and TCP routers that do not define their own `entryPoints` option.<br /> More information [here](#asdefault). | false | No |
|
||||||
| <a id="opt-forwardedHeaders-trustedIPs" href="#opt-forwardedHeaders-trustedIPs" title="#opt-forwardedHeaders-trustedIPs">`forwardedHeaders.trustedIPs`</a> | Set the IPs or CIDR from where Traefik trusts the forwarded headers information (`X-Forwarded-*`). | - | No |
|
| <a id="opt-forwardedHeaders-trustedIPs" href="#opt-forwardedHeaders-trustedIPs" title="#opt-forwardedHeaders-trustedIPs">`forwardedHeaders.trustedIPs`</a> | Set the IPs or CIDR from where Traefik trusts the forwarded headers information (`X-Forwarded-*`). | - | No |
|
||||||
| <a id="opt-forwardedHeaders-insecure" href="#opt-forwardedHeaders-insecure" title="#opt-forwardedHeaders-insecure">`forwardedHeaders.insecure`</a> | Set the insecure mode to always trust the forwarded headers information (`X-Forwarded-*`).<br />We recommend to use this option only for tests purposes, not in production. | false | No |
|
| <a id="opt-forwardedHeaders-insecure" href="#opt-forwardedHeaders-insecure" title="#opt-forwardedHeaders-insecure">`forwardedHeaders.insecure`</a> | Set the insecure mode to always trust the forwarded headers information (`X-Forwarded-*`).<br />We recommend to use this option only for tests purposes, not in production. | false | No |
|
||||||
| <a id="opt-forwardedHeaders-notAppendXForwardedFor" href="#opt-forwardedHeaders-notAppendXForwardedFor" title="#opt-forwardedHeaders-notAppendXForwardedFor">`forwardedHeaders.`<br />`notAppendXForwardedFor`</a> | When set to `true`, Traefik will not append the client's `RemoteAddr` to the `X-Forwarded-For` header. The existing header is preserved as-is. If no `X-Forwarded-For` header exists, none will be added. | false | No |
|
|
||||||
| <a id="opt-http-redirections-entryPoint-to" href="#opt-http-redirections-entryPoint-to" title="#opt-http-redirections-entryPoint-to">`http.redirections.`<br />`entryPoint.to`</a> | The target element to enable (permanent) redirecting of all incoming requests on an entry point to another one. <br /> The target element can be an entry point name (ex: `websecure`), or a port (`:443`). | - | Yes |
|
| <a id="opt-http-redirections-entryPoint-to" href="#opt-http-redirections-entryPoint-to" title="#opt-http-redirections-entryPoint-to">`http.redirections.`<br />`entryPoint.to`</a> | The target element to enable (permanent) redirecting of all incoming requests on an entry point to another one. <br /> The target element can be an entry point name (ex: `websecure`), or a port (`:443`). | - | Yes |
|
||||||
| <a id="opt-http-redirections-entryPoint-scheme" href="#opt-http-redirections-entryPoint-scheme" title="#opt-http-redirections-entryPoint-scheme">`http.redirections.`<br />`entryPoint.scheme`</a> | The target scheme to use for (permanent) redirection of all incoming requests. | https | No |
|
| <a id="opt-http-redirections-entryPoint-scheme" href="#opt-http-redirections-entryPoint-scheme" title="#opt-http-redirections-entryPoint-scheme">`http.redirections.`<br />`entryPoint.scheme`</a> | The target scheme to use for (permanent) redirection of all incoming requests. | https | No |
|
||||||
| <a id="opt-http-redirections-entryPoint-permanent" href="#opt-http-redirections-entryPoint-permanent" title="#opt-http-redirections-entryPoint-permanent">`http.redirections.`<br />`entryPoint.permanent`</a> | Enable permanent redirecting of all incoming requests on an entry point to another one changing the scheme. <br /> The target element, it can be an entry point name (ex: `websecure`), or a port (`:443`). | false | No |
|
| <a id="opt-http-redirections-entryPoint-permanent" href="#opt-http-redirections-entryPoint-permanent" title="#opt-http-redirections-entryPoint-permanent">`http.redirections.`<br />`entryPoint.permanent`</a> | Enable permanent redirecting of all incoming requests on an entry point to another one changing the scheme. <br /> The target element, it can be an entry point name (ex: `websecure`), or a port (`:443`). | false | No |
|
||||||
| <a id="opt-http-redirections-entryPoint-priority" href="#opt-http-redirections-entryPoint-priority" title="#opt-http-redirections-entryPoint-priority">`http.redirections.`<br />`entryPoint.priority`</a> | Default priority applied to the routers attached to the `entryPoint`. | MaxInt32-1 (2147483646) | No |
|
| <a id="opt-http-redirections-entryPoint-priority" href="#opt-http-redirections-entryPoint-priority" title="#opt-http-redirections-entryPoint-priority">`http.redirections.`<br />`entryPoint.priority`</a> | Default priority applied to the routers attached to the `entryPoint`. | MaxInt32-1 (2147483646) | No |
|
||||||
|
| <a id="opt-http-encodedCharacters" href="#opt-http-encodedCharacters" title="#opt-http-encodedCharacters">`http.encodedCharacters`</a> | Defines which encoded characters are allowed in the request path. More information [here](#encoded-characters). | false | No |
|
||||||
|
| <a id="opt-http-encodedCharacters-allowEncodedSlash" href="#opt-http-encodedCharacters-allowEncodedSlash" title="#opt-http-encodedCharacters-allowEncodedSlash">`http.encodedCharacters.`<br />`allowEncodedSlash`</a> | Defines whether requests with encoded slash characters in the path are allowed. | false | No |
|
||||||
|
| <a id="opt-http-encodedCharacters-allowEncodedBackSlash" href="#opt-http-encodedCharacters-allowEncodedBackSlash" title="#opt-http-encodedCharacters-allowEncodedBackSlash">`http.encodedCharacters.`<br />`allowEncodedBackSlash`</a> | Defines whether requests with encoded back slash characters in the path are allowed. | false | No |
|
||||||
|
| <a id="opt-http-encodedCharacters-allowEncodedNullCharacter" href="#opt-http-encodedCharacters-allowEncodedNullCharacter" title="#opt-http-encodedCharacters-allowEncodedNullCharacter">`http.encodedCharacters.`<br />`allowEncodedNullCharacter`</a> | Defines whether requests with encoded null characters in the path are allowed. | false | No |
|
||||||
|
| <a id="opt-http-encodedCharacters-allowEncodedSemicolon" href="#opt-http-encodedCharacters-allowEncodedSemicolon" title="#opt-http-encodedCharacters-allowEncodedSemicolon">`http.encodedCharacters.`<br />`allowEncodedSemicolon`</a> | Defines whether requests with encoded semicolon characters in the path are allowed. | false | No |
|
||||||
|
| <a id="opt-http-encodedCharacters-allowEncodedPercent" href="#opt-http-encodedCharacters-allowEncodedPercent" title="#opt-http-encodedCharacters-allowEncodedPercent">`http.encodedCharacters.`<br />`allowEncodedPercent`</a> | Defines whether requests with encoded percent characters in the path are allowed. | false | No |
|
||||||
|
| <a id="opt-http-encodedCharacters-allowEncodedQuestionMark" href="#opt-http-encodedCharacters-allowEncodedQuestionMark" title="#opt-http-encodedCharacters-allowEncodedQuestionMark">`http.encodedCharacters.`<br />`allowEncodedQuestionMark`</a> | Defines whether requests with encoded question mark characters in the path are allowed. | false | No |
|
||||||
|
| <a id="opt-http-encodedCharacters-allowEncodedHash" href="#opt-http-encodedCharacters-allowEncodedHash" title="#opt-http-encodedCharacters-allowEncodedHash">`http.encodedCharacters.`<br />`allowEncodedHash`</a> | Defines whether requests with encoded hash characters in the path are allowed. | false | No |
|
||||||
| <a id="opt-http-encodeQuerySemicolons" href="#opt-http-encodeQuerySemicolons" title="#opt-http-encodeQuerySemicolons">`http.encodeQuerySemicolons`</a> | Enable query semicolons encoding. <br /> Use this option to avoid non-encoded semicolons to be interpreted as query parameter separators by Traefik. <br /> When using this option, the non-encoded semicolons characters in query will be transmitted encoded to the backend.<br /> More information [here](#encodequerysemicolons). | false | No |
|
| <a id="opt-http-encodeQuerySemicolons" href="#opt-http-encodeQuerySemicolons" title="#opt-http-encodeQuerySemicolons">`http.encodeQuerySemicolons`</a> | Enable query semicolons encoding. <br /> Use this option to avoid non-encoded semicolons to be interpreted as query parameter separators by Traefik. <br /> When using this option, the non-encoded semicolons characters in query will be transmitted encoded to the backend.<br /> More information [here](#encodequerysemicolons). | false | No |
|
||||||
| <a id="opt-http-sanitizePath" href="#opt-http-sanitizePath" title="#opt-http-sanitizePath">`http.sanitizePath`</a> | Defines whether to enable the request path sanitization.<br /> More information [here](#sanitizepath). | false | No |
|
| <a id="opt-http-sanitizePath" href="#opt-http-sanitizePath" title="#opt-http-sanitizePath">`http.sanitizePath`</a> | Defines whether to enable the request path sanitization.<br /> More information [here](#sanitizepath). | false | No |
|
||||||
| <a id="opt-http-middlewares" href="#opt-http-middlewares" title="#opt-http-middlewares">`http.middlewares`</a> | Set the list of middlewares that are prepended by default to the list of middlewares of each router associated to the named entry point. <br />More information [here](#httpmiddlewares). | - | No |
|
| <a id="opt-http-middlewares" href="#opt-http-middlewares" title="#opt-http-middlewares">`http.middlewares`</a> | Set the list of middlewares that are prepended by default to the list of middlewares of each router associated to the named entry point. <br />More information [here](#httpmiddlewares). | - | No |
|
||||||
@@ -209,6 +216,27 @@ it can lead to unsafe routing when the `sanitizePath` option is set to `false`.
|
|||||||
| <a id="opt-false-6" href="#opt-false-6" title="#opt-false-6">false</a> | /./foo/../bar// | /./foo/../bar// |
|
| <a id="opt-false-6" href="#opt-false-6" title="#opt-false-6">false</a> | /./foo/../bar// | /./foo/../bar// |
|
||||||
| <a id="opt-true-6" href="#opt-true-6" title="#opt-true-6">true</a> | /./foo/../bar// | /bar/ |
|
| <a id="opt-true-6" href="#opt-true-6" title="#opt-true-6">true</a> | /./foo/../bar// | /bar/ |
|
||||||
|
|
||||||
|
### Encoded Characters
|
||||||
|
|
||||||
|
You can configure Traefik to control the handling of encoded characters in request paths for security purposes.
|
||||||
|
By default, Traefik rejects requests containing certain encoded characters that could be used in path traversal or other security attacks.
|
||||||
|
|
||||||
|
!!! warning "Security Considerations"
|
||||||
|
|
||||||
|
Allowing certain encoded characters may expose your application to security vulnerabilities.
|
||||||
|
|
||||||
|
Here is the list of the encoded characters that are rejected by default:
|
||||||
|
|
||||||
|
| Encoded Character | Character |
|
||||||
|
|-------------------|-------------------------|
|
||||||
|
| <a id="opt-2f-or-2F" href="#opt-2f-or-2F" title="#opt-2f-or-2F">`%2f` or `%2F`</a> | `/` (slash) |
|
||||||
|
| <a id="opt-5c-or-5C" href="#opt-5c-or-5C" title="#opt-5c-or-5C">`%5c` or `%5C`</a> | `\` (backslash) |
|
||||||
|
| <a id="opt-00" href="#opt-00" title="#opt-00">`%00`</a> | `NULL` (null character) |
|
||||||
|
| <a id="opt-3b-or-3B" href="#opt-3b-or-3B" title="#opt-3b-or-3B">`%3b` or `%3B`</a> | `;` (semicolon) |
|
||||||
|
| <a id="opt-25" href="#opt-25" title="#opt-25">`%25`</a> | `%` (percent) |
|
||||||
|
| <a id="opt-3f-or-3F" href="#opt-3f-or-3F" title="#opt-3f-or-3F">`%3f` or `%3F`</a> | `?` (question mark) |
|
||||||
|
| <a id="opt-23" href="#opt-23" title="#opt-23">`%23`</a> | `#` (hash) |
|
||||||
|
|
||||||
### HTTP3
|
### HTTP3
|
||||||
|
|
||||||
As HTTP/3 actually uses UDP, when Traefik is configured with a TCP `entryPoint`
|
As HTTP/3 actually uses UDP, when Traefik is configured with a TCP `entryPoint`
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ It also supports many of the [ingress-nginx](https://kubernetes.github.io/ingres
|
|||||||
The Kubernetes NGINX Ingress Controller project has announced its retirement in **March 2026** and will no longer receive updates or security patches.
|
The Kubernetes NGINX Ingress Controller project has announced its retirement in **March 2026** and will no longer receive updates or security patches.
|
||||||
Traefik provides a migration path by supporting NGINX annotations, allowing you to transition your workloads without rewriting all your Ingress configurations.
|
Traefik provides a migration path by supporting NGINX annotations, allowing you to transition your workloads without rewriting all your Ingress configurations.
|
||||||
|
|
||||||
|
**→ See the [NGINX to Traefik Migration Guide](../../../../migrate/nginx-to-traefik.md) for step-by-step instructions.**
|
||||||
|
|
||||||
For more information about the NGINX Ingress Controller retirement, see the [official Kubernetes blog announcement](https://kubernetes.io/blog/2025/11/11/ingress-nginx-retirement).
|
For more information about the NGINX Ingress Controller retirement, see the [official Kubernetes blog announcement](https://kubernetes.io/blog/2025/11/11/ingress-nginx-retirement).
|
||||||
|
|
||||||
## Ingress Discovery
|
## Ingress Discovery
|
||||||
@@ -32,11 +34,6 @@ You can enable the Kubernetes Ingress NGINX provider as detailed below:
|
|||||||
```yaml tab="File (YAML)"
|
```yaml tab="File (YAML)"
|
||||||
providers:
|
providers:
|
||||||
kubernetesIngressNGINX:
|
kubernetesIngressNGINX:
|
||||||
endpoint: "https://kubernetes.default.svc"
|
|
||||||
token: "mytoken"
|
|
||||||
certAuthFilePath: "/path/to/ca.crt"
|
|
||||||
throttleDuration: "2s"
|
|
||||||
|
|
||||||
# Namespace discovery
|
# Namespace discovery
|
||||||
watchNamespace: "default"
|
watchNamespace: "default"
|
||||||
# OR use namespace selector (mutually exclusive with watchNamespace)
|
# OR use namespace selector (mutually exclusive with watchNamespace)
|
||||||
@@ -47,25 +44,10 @@ providers:
|
|||||||
controllerClass: "k8s.io/ingress-nginx"
|
controllerClass: "k8s.io/ingress-nginx"
|
||||||
watchIngressWithoutClass: false
|
watchIngressWithoutClass: false
|
||||||
ingressClassByName: false
|
ingressClassByName: false
|
||||||
|
|
||||||
# Status updates
|
|
||||||
publishService: "kube-system/traefik"
|
|
||||||
publishStatusAddress: "203.0.113.42"
|
|
||||||
|
|
||||||
# Default backend
|
|
||||||
defaultBackendService: "default/default-backend"
|
|
||||||
|
|
||||||
# Security
|
|
||||||
disableSvcExternalName: false
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```toml tab="File (TOML)"
|
```toml tab="File (TOML)"
|
||||||
[providers.kubernetesIngressNGINX]
|
[providers.kubernetesIngressNGINX]
|
||||||
endpoint = "https://kubernetes.default.svc"
|
|
||||||
token = "mytoken"
|
|
||||||
certAuthFilePath = "/path/to/ca.crt"
|
|
||||||
throttleDuration = "2s"
|
|
||||||
|
|
||||||
# Namespace discovery
|
# Namespace discovery
|
||||||
watchNamespace = "default"
|
watchNamespace = "default"
|
||||||
# OR use namespace selector (mutually exclusive with watchNamespace)
|
# OR use namespace selector (mutually exclusive with watchNamespace)
|
||||||
@@ -76,33 +58,15 @@ providers:
|
|||||||
controllerClass = "k8s.io/ingress-nginx"
|
controllerClass = "k8s.io/ingress-nginx"
|
||||||
watchIngressWithoutClass = false
|
watchIngressWithoutClass = false
|
||||||
ingressClassByName = false
|
ingressClassByName = false
|
||||||
|
|
||||||
# Status updates
|
|
||||||
publishService = "kube-system/traefik"
|
|
||||||
publishStatusAddress = "203.0.113.42"
|
|
||||||
|
|
||||||
# Default backend
|
|
||||||
defaultBackendService = "default/default-backend"
|
|
||||||
|
|
||||||
# Security
|
|
||||||
disableSvcExternalName = false
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash tab="CLI"
|
```bash tab="CLI"
|
||||||
--providers.kubernetesingressnginx=true
|
--providers.kubernetesingressnginx=true
|
||||||
--providers.kubernetesingressnginx.endpoint=https://kubernetes.default.svc
|
|
||||||
--providers.kubernetesingressnginx.token=mytoken
|
|
||||||
--providers.kubernetesingressnginx.certauthfilepath=/path/to/ca.crt
|
|
||||||
--providers.kubernetesingressnginx.throttleduration=2s
|
|
||||||
--providers.kubernetesingressnginx.watchnamespace=default
|
--providers.kubernetesingressnginx.watchnamespace=default
|
||||||
--providers.kubernetesingressnginx.ingressclass=nginx
|
--providers.kubernetesingressnginx.ingressclass=nginx
|
||||||
--providers.kubernetesingressnginx.controllerclass=k8s.io/ingress-nginx
|
--providers.kubernetesingressnginx.controllerclass=k8s.io/ingress-nginx
|
||||||
--providers.kubernetesingressnginx.watchingresswithoutclass=false
|
--providers.kubernetesingressnginx.watchingresswithoutclass=false
|
||||||
--providers.kubernetesingressnginx.ingressclassbyname=false
|
--providers.kubernetesingressnginx.ingressclassbyname=false
|
||||||
--providers.kubernetesingressnginx.publishservice=kube-system/traefik
|
|
||||||
--providers.kubernetesingressnginx.publishstatusaddress=203.0.113.42
|
|
||||||
--providers.kubernetesingressnginx.defaultbackendservice=default/default-backend
|
|
||||||
--providers.kubernetesingressnginx.disablesvcexternalname=false
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```yaml tab="Helm Chart Values"
|
```yaml tab="Helm Chart Values"
|
||||||
@@ -111,18 +75,6 @@ providers:
|
|||||||
# -- Enable Kubernetes Ingress NGINX provider
|
# -- Enable Kubernetes Ingress NGINX provider
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
# -- Kubernetes server endpoint (required for external cluster client)
|
|
||||||
endpoint: "https://kubernetes.default.svc"
|
|
||||||
|
|
||||||
# -- Kubernetes bearer token (not needed for in-cluster client)
|
|
||||||
token: "mytoken"
|
|
||||||
|
|
||||||
# -- Kubernetes certificate authority file path (not needed for in-cluster client)
|
|
||||||
certAuthFilePath: "/path/to/ca.crt"
|
|
||||||
|
|
||||||
# -- Ingress refresh throttle duration
|
|
||||||
throttleDuration: "2s"
|
|
||||||
|
|
||||||
# Namespace discovery
|
# Namespace discovery
|
||||||
# -- Namespace the controller watches for updates to Kubernetes objects
|
# -- Namespace the controller watches for updates to Kubernetes objects
|
||||||
# When using rbac.namespaced, it will watch helm release namespace and namespaces listed in this array
|
# When using rbac.namespaced, it will watch helm release namespace and namespaces listed in this array
|
||||||
@@ -140,22 +92,6 @@ providers:
|
|||||||
watchIngressWithoutClass: false
|
watchIngressWithoutClass: false
|
||||||
# -- Define if Ingress Controller should watch for Ingress Class by Name together with Controller Class
|
# -- Define if Ingress Controller should watch for Ingress Class by Name together with Controller Class
|
||||||
ingressClassByName: false
|
ingressClassByName: false
|
||||||
|
|
||||||
# Status updates
|
|
||||||
# -- Service fronting the Ingress controller
|
|
||||||
publishService:
|
|
||||||
enabled: true
|
|
||||||
pathOverride: "kube-system/traefik"
|
|
||||||
# -- Customized address (or addresses, separated by comma) to set as the load-balancer status of Ingress objects
|
|
||||||
publishStatusAddress: "203.0.113.42"
|
|
||||||
|
|
||||||
# Default backend
|
|
||||||
# -- Service used to serve HTTP requests not matching any known server name (catch-all). Takes the form 'namespace/name'
|
|
||||||
defaultBackendService: "default/default-backend"
|
|
||||||
|
|
||||||
# Security
|
|
||||||
# -- Disable support for Services of type ExternalName
|
|
||||||
disableSvcExternalName: false
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This provider watches for incoming Ingress events and automatically translates NGINX annotations into Traefik's dynamic configuration, creating the corresponding routers, services, middlewares, and other components needed to route traffic to your cluster services.
|
This provider watches for incoming Ingress events and automatically translates NGINX annotations into Traefik's dynamic configuration, creating the corresponding routers, services, middlewares, and other components needed to route traffic to your cluster services.
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ On subsequent requests, to keep the session alive with the same server, the clie
|
|||||||
|
|
||||||
??? example "Adding Stickiness -- Using the [File Provider](../../../install-configuration/providers/others/file.md)"
|
??? example "Adding Stickiness -- Using the [File Provider](../../../install-configuration/providers/others/file.md)"
|
||||||
|
|
||||||
```yaml tab="YAML"
|
```yaml tab="Structured (YAML)"
|
||||||
## Dynamic configuration
|
## Dynamic configuration
|
||||||
http:
|
http:
|
||||||
services:
|
services:
|
||||||
@@ -199,7 +199,7 @@ On subsequent requests, to keep the session alive with the same server, the clie
|
|||||||
cookie: {}
|
cookie: {}
|
||||||
```
|
```
|
||||||
|
|
||||||
```toml tab="TOML"
|
```toml tab="Structured (TOML)"
|
||||||
## Dynamic configuration
|
## Dynamic configuration
|
||||||
[http.services]
|
[http.services]
|
||||||
[http.services.my-service]
|
[http.services.my-service]
|
||||||
@@ -208,7 +208,7 @@ On subsequent requests, to keep the session alive with the same server, the clie
|
|||||||
|
|
||||||
??? example "Adding Stickiness with custom Options -- Using the [File Provider](../../../install-configuration/providers/others/file.md)"
|
??? example "Adding Stickiness with custom Options -- Using the [File Provider](../../../install-configuration/providers/others/file.md)"
|
||||||
|
|
||||||
```yaml tab="YAML"
|
```yaml tab="Structured (YAML)"
|
||||||
## Dynamic configuration
|
## Dynamic configuration
|
||||||
http:
|
http:
|
||||||
services:
|
services:
|
||||||
@@ -222,7 +222,7 @@ On subsequent requests, to keep the session alive with the same server, the clie
|
|||||||
httpOnly: true
|
httpOnly: true
|
||||||
```
|
```
|
||||||
|
|
||||||
```toml tab="TOML"
|
```toml tab="Structured (TOML)"
|
||||||
## Dynamic configuration
|
## Dynamic configuration
|
||||||
[http.services]
|
[http.services]
|
||||||
[http.services.my-service]
|
[http.services.my-service]
|
||||||
@@ -236,7 +236,7 @@ On subsequent requests, to keep the session alive with the same server, the clie
|
|||||||
|
|
||||||
??? example "Setting Stickiness on all the required levels -- Using the [File Provider](../../../install-configuration/providers/others/file.md)"
|
??? example "Setting Stickiness on all the required levels -- Using the [File Provider](../../../install-configuration/providers/others/file.md)"
|
||||||
|
|
||||||
```yaml tab="YAML"
|
```yaml tab="Structured (YAML)"
|
||||||
## Dynamic configuration
|
## Dynamic configuration
|
||||||
http:
|
http:
|
||||||
services:
|
services:
|
||||||
@@ -270,7 +270,7 @@ On subsequent requests, to keep the session alive with the same server, the clie
|
|||||||
- url: http://127.0.0.1:8084
|
- url: http://127.0.0.1:8084
|
||||||
```
|
```
|
||||||
|
|
||||||
```toml tab="TOML"
|
```toml tab="Structured (TOML)"
|
||||||
## Dynamic configuration
|
## Dynamic configuration
|
||||||
[http.services]
|
[http.services]
|
||||||
[http.services.wrr1]
|
[http.services.wrr1]
|
||||||
@@ -304,7 +304,7 @@ On subsequent requests, to keep the session alive with the same server, the clie
|
|||||||
|
|
||||||
To keep a session open with the same server, the client would then need to specify the two levels within the cookie for each request, e.g. with curl:
|
To keep a session open with the same server, the client would then need to specify the two levels within the cookie for each request, e.g. with curl:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
curl -b "lvl1=whoami1; lvl2=http://127.0.0.1:8081" http://localhost:8000
|
curl -b "lvl1=whoami1; lvl2=http://127.0.0.1:8081" http://localhost:8000
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -453,13 +453,14 @@ http:
|
|||||||
[[http.services.appv2.loadBalancer.servers]]
|
[[http.services.appv2.loadBalancer.servers]]
|
||||||
url = "http://private-ip-server-2/"
|
url = "http://private-ip-server-2/"
|
||||||
```
|
```
|
||||||
|
|
||||||
## P2C
|
## P2C
|
||||||
|
|
||||||
Power of two choices algorithm is a load balancing strategy that selects two servers at random and chooses the one with the least number of active requests.
|
Power of two choices algorithm is a load balancing strategy that selects two servers at random and chooses the one with the least number of active requests.
|
||||||
|
|
||||||
??? example "P2C Load Balancing -- Using the [File Provider](../../../install-configuration/providers/others/file.md)"
|
??? example "P2C Load Balancing -- Using the [File Provider](../../../install-configuration/providers/others/file.md)"
|
||||||
|
|
||||||
```yaml tab="YAML"
|
```yaml tab="Structured (YAML)"
|
||||||
## Dynamic configuration
|
## Dynamic configuration
|
||||||
http:
|
http:
|
||||||
services:
|
services:
|
||||||
@@ -472,7 +473,7 @@ Power of two choices algorithm is a load balancing strategy that selects two ser
|
|||||||
- url: "http://private-ip-server-3/"
|
- url: "http://private-ip-server-3/"
|
||||||
```
|
```
|
||||||
|
|
||||||
```toml tab="TOML"
|
```toml tab="Structured (TOML) "
|
||||||
## Dynamic configuration
|
## Dynamic configuration
|
||||||
[http.services]
|
[http.services]
|
||||||
[http.services.my-service.loadBalancer]
|
[http.services.my-service.loadBalancer]
|
||||||
@@ -501,7 +502,7 @@ Weighted Round Robin (WRR) with Earliest Deadline First (EDF) scheduling is used
|
|||||||
|
|
||||||
??? example "Basic Least-Time Load Balancing -- Using the [File Provider](../../../install-configuration/providers/others/file.md)"
|
??? example "Basic Least-Time Load Balancing -- Using the [File Provider](../../../install-configuration/providers/others/file.md)"
|
||||||
|
|
||||||
```yaml tab="YAML"
|
```yaml tab="Structured (YAML)"
|
||||||
## Dynamic configuration
|
## Dynamic configuration
|
||||||
http:
|
http:
|
||||||
services:
|
services:
|
||||||
@@ -514,7 +515,7 @@ Weighted Round Robin (WRR) with Earliest Deadline First (EDF) scheduling is used
|
|||||||
- url: "http://private-ip-server-3/"
|
- url: "http://private-ip-server-3/"
|
||||||
```
|
```
|
||||||
|
|
||||||
```toml tab="TOML"
|
```toml tab="Structured (TOML)"
|
||||||
## Dynamic configuration
|
## Dynamic configuration
|
||||||
[http.services]
|
[http.services]
|
||||||
[http.services.my-service.loadBalancer]
|
[http.services.my-service.loadBalancer]
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ Enable seamless migration from NGINX Ingress Controller to Traefik with NGINX an
|
|||||||
The Kubernetes NGINX Ingress Controller project has announced its retirement in **March 2026** and will no longer receive updates or security patches.
|
The Kubernetes NGINX Ingress Controller project has announced its retirement in **March 2026** and will no longer receive updates or security patches.
|
||||||
Traefik provides a migration path by supporting NGINX annotations, allowing you to transition your workloads without rewriting all your Ingress configurations.
|
Traefik provides a migration path by supporting NGINX annotations, allowing you to transition your workloads without rewriting all your Ingress configurations.
|
||||||
|
|
||||||
|
**→ See the [NGINX to Traefik Migration Guide](../../../migrate/nginx-to-traefik.md) for step-by-step instructions.**
|
||||||
|
|
||||||
For more information about the NGINX Ingress Controller retirement, see the [official Kubernetes blog announcement](https://kubernetes.io/blog/2025/11/11/ingress-nginx-retirement).
|
For more information about the NGINX Ingress Controller retirement, see the [official Kubernetes blog announcement](https://kubernetes.io/blog/2025/11/11/ingress-nginx-retirement).
|
||||||
|
|
||||||
## Ingress Discovery
|
## Ingress Discovery
|
||||||
@@ -292,7 +294,6 @@ The following annotations are organized by category for easier navigation.
|
|||||||
| <a id="opt-nginx-ingress-kubernetes-ioload-balance" href="#opt-nginx-ingress-kubernetes-ioload-balance" title="#opt-nginx-ingress-kubernetes-ioload-balance">`nginx.ingress.kubernetes.io/load-balance`</a> | Only round_robin supported; ewma and IP hash not supported. |
|
| <a id="opt-nginx-ingress-kubernetes-ioload-balance" href="#opt-nginx-ingress-kubernetes-ioload-balance" title="#opt-nginx-ingress-kubernetes-ioload-balance">`nginx.ingress.kubernetes.io/load-balance`</a> | Only round_robin supported; ewma and IP hash not supported. |
|
||||||
| <a id="opt-nginx-ingress-kubernetes-iobackend-protocol" href="#opt-nginx-ingress-kubernetes-iobackend-protocol" title="#opt-nginx-ingress-kubernetes-iobackend-protocol">`nginx.ingress.kubernetes.io/backend-protocol`</a> | FCGI and AUTO_HTTP not supported. |
|
| <a id="opt-nginx-ingress-kubernetes-iobackend-protocol" href="#opt-nginx-ingress-kubernetes-iobackend-protocol" title="#opt-nginx-ingress-kubernetes-iobackend-protocol">`nginx.ingress.kubernetes.io/backend-protocol`</a> | FCGI and AUTO_HTTP not supported. |
|
||||||
| <a id="opt-nginx-ingress-kubernetes-ioservice-upstream" href="#opt-nginx-ingress-kubernetes-ioservice-upstream" title="#opt-nginx-ingress-kubernetes-ioservice-upstream">`nginx.ingress.kubernetes.io/service-upstream`</a> | |
|
| <a id="opt-nginx-ingress-kubernetes-ioservice-upstream" href="#opt-nginx-ingress-kubernetes-ioservice-upstream" title="#opt-nginx-ingress-kubernetes-ioservice-upstream">`nginx.ingress.kubernetes.io/service-upstream`</a> | |
|
||||||
| <a id="opt-nginx-ingress-kubernetes-ioupstream-vhost" href="#opt-nginx-ingress-kubernetes-ioupstream-vhost" title="#opt-nginx-ingress-kubernetes-ioupstream-vhost">`nginx.ingress.kubernetes.io/upstream-vhost`</a> | |
|
|
||||||
|
|
||||||
### CORS
|
### CORS
|
||||||
|
|
||||||
@@ -339,6 +340,7 @@ The following annotations are organized by category for easier navigation.
|
|||||||
|-----------------------------------------------------------------------------|------------------------------------------------------|
|
|-----------------------------------------------------------------------------|------------------------------------------------------|
|
||||||
| <a id="opt-nginx-ingress-kubernetes-ioapp-root" href="#opt-nginx-ingress-kubernetes-ioapp-root" title="#opt-nginx-ingress-kubernetes-ioapp-root">`nginx.ingress.kubernetes.io/app-root`</a> | |
|
| <a id="opt-nginx-ingress-kubernetes-ioapp-root" href="#opt-nginx-ingress-kubernetes-ioapp-root" title="#opt-nginx-ingress-kubernetes-ioapp-root">`nginx.ingress.kubernetes.io/app-root`</a> | |
|
||||||
| <a id="opt-nginx-ingress-kubernetes-ioaffinity-canary-behavior" href="#opt-nginx-ingress-kubernetes-ioaffinity-canary-behavior" title="#opt-nginx-ingress-kubernetes-ioaffinity-canary-behavior">`nginx.ingress.kubernetes.io/affinity-canary-behavior`</a> | |
|
| <a id="opt-nginx-ingress-kubernetes-ioaffinity-canary-behavior" href="#opt-nginx-ingress-kubernetes-ioaffinity-canary-behavior" title="#opt-nginx-ingress-kubernetes-ioaffinity-canary-behavior">`nginx.ingress.kubernetes.io/affinity-canary-behavior`</a> | |
|
||||||
|
| <a id="opt-nginx-ingress-kubernetes-ioauth-signin" href="#opt-nginx-ingress-kubernetes-ioauth-signin" title="#opt-nginx-ingress-kubernetes-ioauth-signin">`nginx.ingress.kubernetes.io/auth-signin`</a> | |
|
||||||
| <a id="opt-nginx-ingress-kubernetes-ioauth-tls-secret" href="#opt-nginx-ingress-kubernetes-ioauth-tls-secret" title="#opt-nginx-ingress-kubernetes-ioauth-tls-secret">`nginx.ingress.kubernetes.io/auth-tls-secret`</a> | |
|
| <a id="opt-nginx-ingress-kubernetes-ioauth-tls-secret" href="#opt-nginx-ingress-kubernetes-ioauth-tls-secret" title="#opt-nginx-ingress-kubernetes-ioauth-tls-secret">`nginx.ingress.kubernetes.io/auth-tls-secret`</a> | |
|
||||||
| <a id="opt-nginx-ingress-kubernetes-ioauth-tls-verify-depth" href="#opt-nginx-ingress-kubernetes-ioauth-tls-verify-depth" title="#opt-nginx-ingress-kubernetes-ioauth-tls-verify-depth">`nginx.ingress.kubernetes.io/auth-tls-verify-depth`</a> | |
|
| <a id="opt-nginx-ingress-kubernetes-ioauth-tls-verify-depth" href="#opt-nginx-ingress-kubernetes-ioauth-tls-verify-depth" title="#opt-nginx-ingress-kubernetes-ioauth-tls-verify-depth">`nginx.ingress.kubernetes.io/auth-tls-verify-depth`</a> | |
|
||||||
| <a id="opt-nginx-ingress-kubernetes-ioauth-tls-verify-client" href="#opt-nginx-ingress-kubernetes-ioauth-tls-verify-client" title="#opt-nginx-ingress-kubernetes-ioauth-tls-verify-client">`nginx.ingress.kubernetes.io/auth-tls-verify-client`</a> | |
|
| <a id="opt-nginx-ingress-kubernetes-ioauth-tls-verify-client" href="#opt-nginx-ingress-kubernetes-ioauth-tls-verify-client" title="#opt-nginx-ingress-kubernetes-ioauth-tls-verify-client">`nginx.ingress.kubernetes.io/auth-tls-verify-client`</a> | |
|
||||||
@@ -421,6 +423,7 @@ The following annotations are organized by category for easier navigation.
|
|||||||
| <a id="opt-nginx-ingress-kubernetes-iomirror-host" href="#opt-nginx-ingress-kubernetes-iomirror-host" title="#opt-nginx-ingress-kubernetes-iomirror-host">`nginx.ingress.kubernetes.io/mirror-host`</a> | |
|
| <a id="opt-nginx-ingress-kubernetes-iomirror-host" href="#opt-nginx-ingress-kubernetes-iomirror-host" title="#opt-nginx-ingress-kubernetes-iomirror-host">`nginx.ingress.kubernetes.io/mirror-host`</a> | |
|
||||||
| <a id="opt-nginx-ingress-kubernetes-iox-forwarded-prefix" href="#opt-nginx-ingress-kubernetes-iox-forwarded-prefix" title="#opt-nginx-ingress-kubernetes-iox-forwarded-prefix">`nginx.ingress.kubernetes.io/x-forwarded-prefix`</a> | |
|
| <a id="opt-nginx-ingress-kubernetes-iox-forwarded-prefix" href="#opt-nginx-ingress-kubernetes-iox-forwarded-prefix" title="#opt-nginx-ingress-kubernetes-iox-forwarded-prefix">`nginx.ingress.kubernetes.io/x-forwarded-prefix`</a> | |
|
||||||
| <a id="opt-nginx-ingress-kubernetes-ioupstream-hash-by" href="#opt-nginx-ingress-kubernetes-ioupstream-hash-by" title="#opt-nginx-ingress-kubernetes-ioupstream-hash-by">`nginx.ingress.kubernetes.io/upstream-hash-by`</a> | |
|
| <a id="opt-nginx-ingress-kubernetes-ioupstream-hash-by" href="#opt-nginx-ingress-kubernetes-ioupstream-hash-by" title="#opt-nginx-ingress-kubernetes-ioupstream-hash-by">`nginx.ingress.kubernetes.io/upstream-hash-by`</a> | |
|
||||||
|
| <a id="opt-nginx-ingress-kubernetes-ioupstream-vhost" href="#opt-nginx-ingress-kubernetes-ioupstream-vhost" title="#opt-nginx-ingress-kubernetes-ioupstream-vhost">`nginx.ingress.kubernetes.io/upstream-vhost`</a> | |
|
||||||
| <a id="opt-nginx-ingress-kubernetes-iodenylist-source-range" href="#opt-nginx-ingress-kubernetes-iodenylist-source-range" title="#opt-nginx-ingress-kubernetes-iodenylist-source-range">`nginx.ingress.kubernetes.io/denylist-source-range`</a> | |
|
| <a id="opt-nginx-ingress-kubernetes-iodenylist-source-range" href="#opt-nginx-ingress-kubernetes-iodenylist-source-range" title="#opt-nginx-ingress-kubernetes-iodenylist-source-range">`nginx.ingress.kubernetes.io/denylist-source-range`</a> | |
|
||||||
| <a id="opt-nginx-ingress-kubernetes-iowhitelist-source-range" href="#opt-nginx-ingress-kubernetes-iowhitelist-source-range" title="#opt-nginx-ingress-kubernetes-iowhitelist-source-range">`nginx.ingress.kubernetes.io/whitelist-source-range`</a> | |
|
| <a id="opt-nginx-ingress-kubernetes-iowhitelist-source-range" href="#opt-nginx-ingress-kubernetes-iowhitelist-source-range" title="#opt-nginx-ingress-kubernetes-iowhitelist-source-range">`nginx.ingress.kubernetes.io/whitelist-source-range`</a> | |
|
||||||
| <a id="opt-nginx-ingress-kubernetes-ioproxy-buffering" href="#opt-nginx-ingress-kubernetes-ioproxy-buffering" title="#opt-nginx-ingress-kubernetes-ioproxy-buffering">`nginx.ingress.kubernetes.io/proxy-buffering`</a> | |
|
| <a id="opt-nginx-ingress-kubernetes-ioproxy-buffering" href="#opt-nginx-ingress-kubernetes-ioproxy-buffering" title="#opt-nginx-ingress-kubernetes-ioproxy-buffering">`nginx.ingress.kubernetes.io/proxy-buffering`</a> | |
|
||||||
|
|||||||
@@ -237,6 +237,30 @@ Trust only forwarded headers from selected IPs.
|
|||||||
`--entrypoints.<name>.http`:
|
`--entrypoints.<name>.http`:
|
||||||
HTTP configuration.
|
HTTP configuration.
|
||||||
|
|
||||||
|
`--entrypoints.<name>.http.encodedcharacters`:
|
||||||
|
Defines which encoded characters are allowed in the request path.
|
||||||
|
|
||||||
|
`--entrypoints.<name>.http.encodedcharacters.allowencodedbackslash`:
|
||||||
|
Defines whether requests with encoded back slash characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
|
`--entrypoints.<name>.http.encodedcharacters.allowencodedhash`:
|
||||||
|
Defines whether requests with encoded hash characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
|
`--entrypoints.<name>.http.encodedcharacters.allowencodednullcharacter`:
|
||||||
|
Defines whether requests with encoded null characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
|
`--entrypoints.<name>.http.encodedcharacters.allowencodedpercent`:
|
||||||
|
Defines whether requests with encoded percent characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
|
`--entrypoints.<name>.http.encodedcharacters.allowencodedquestionmark`:
|
||||||
|
Defines whether requests with encoded question mark characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
|
`--entrypoints.<name>.http.encodedcharacters.allowencodedsemicolon`:
|
||||||
|
Defines whether requests with encoded semicolon characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
|
`--entrypoints.<name>.http.encodedcharacters.allowencodedslash`:
|
||||||
|
Defines whether requests with encoded slash characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
`--entrypoints.<name>.http.encodequerysemicolons`:
|
`--entrypoints.<name>.http.encodequerysemicolons`:
|
||||||
Defines whether request query semicolons should be URLEncoded. (Default: ```false```)
|
Defines whether request query semicolons should be URLEncoded. (Default: ```false```)
|
||||||
|
|
||||||
|
|||||||
@@ -246,6 +246,30 @@ HTTP/3 configuration. (Default: ```false```)
|
|||||||
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP3_ADVERTISEDPORT`:
|
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP3_ADVERTISEDPORT`:
|
||||||
UDP port to advertise, on which HTTP/3 is available. (Default: ```0```)
|
UDP port to advertise, on which HTTP/3 is available. (Default: ```0```)
|
||||||
|
|
||||||
|
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_ENCODEDCHARACTERS`:
|
||||||
|
Defines which encoded characters are allowed in the request path.
|
||||||
|
|
||||||
|
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDBACKSLASH`:
|
||||||
|
Defines whether requests with encoded back slash characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
|
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDHASH`:
|
||||||
|
Defines whether requests with encoded hash characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
|
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDNULLCHARACTER`:
|
||||||
|
Defines whether requests with encoded null characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
|
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDPERCENT`:
|
||||||
|
Defines whether requests with encoded percent characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
|
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDQUESTIONMARK`:
|
||||||
|
Defines whether requests with encoded question mark characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
|
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDSEMICOLON`:
|
||||||
|
Defines whether requests with encoded semicolon characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
|
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDSLASH`:
|
||||||
|
Defines whether requests with encoded slash characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_ENCODEQUERYSEMICOLONS`:
|
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_ENCODEQUERYSEMICOLONS`:
|
||||||
Defines whether request query semicolons should be URLEncoded. (Default: ```false```)
|
Defines whether request query semicolons should be URLEncoded. (Default: ```false```)
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,14 @@
|
|||||||
[[entryPoints.EntryPoint0.http.tls.domains]]
|
[[entryPoints.EntryPoint0.http.tls.domains]]
|
||||||
main = "foobar"
|
main = "foobar"
|
||||||
sans = ["foobar", "foobar"]
|
sans = ["foobar", "foobar"]
|
||||||
|
[entryPoints.EntryPoint0.http.encodedCharacters]
|
||||||
|
allowEncodedSlash = true
|
||||||
|
allowEncodedBackSlash = true
|
||||||
|
allowEncodedNullCharacter = true
|
||||||
|
allowEncodedSemicolon = true
|
||||||
|
allowEncodedPercent = true
|
||||||
|
allowEncodedQuestionMark = true
|
||||||
|
allowEncodedHash = true
|
||||||
[entryPoints.EntryPoint0.http2]
|
[entryPoints.EntryPoint0.http2]
|
||||||
maxConcurrentStreams = 42
|
maxConcurrentStreams = 42
|
||||||
[entryPoints.EntryPoint0.http3]
|
[entryPoints.EntryPoint0.http3]
|
||||||
|
|||||||
@@ -83,6 +83,14 @@ entryPoints:
|
|||||||
sans:
|
sans:
|
||||||
- foobar
|
- foobar
|
||||||
- foobar
|
- foobar
|
||||||
|
encodedCharacters:
|
||||||
|
allowEncodedSlash: true
|
||||||
|
allowEncodedBackSlash: true
|
||||||
|
allowEncodedNullCharacter: true
|
||||||
|
allowEncodedSemicolon: true
|
||||||
|
allowEncodedPercent: true
|
||||||
|
allowEncodedQuestionMark: true
|
||||||
|
allowEncodedHash: true
|
||||||
encodeQuerySemicolons: true
|
encodeQuerySemicolons: true
|
||||||
sanitizePath: true
|
sanitizePath: true
|
||||||
maxHeaderBytes: 42
|
maxHeaderBytes: 42
|
||||||
|
|||||||
@@ -129,6 +129,15 @@ They can be defined by using a file (YAML or TOML) or CLI arguments.
|
|||||||
trustedIPs:
|
trustedIPs:
|
||||||
- "127.0.0.1"
|
- "127.0.0.1"
|
||||||
- "192.168.0.1"
|
- "192.168.0.1"
|
||||||
|
http:
|
||||||
|
encodedCharacters:
|
||||||
|
allowEncodedSlash: true
|
||||||
|
allowEncodedBackSlash: true
|
||||||
|
allowEncodedNullCharacter: true
|
||||||
|
allowEncodedSemicolon: true
|
||||||
|
allowEncodedPercent: true
|
||||||
|
allowEncodedQuestionMark: true
|
||||||
|
allowEncodedHash: true
|
||||||
```
|
```
|
||||||
|
|
||||||
```toml tab="File (TOML)"
|
```toml tab="File (TOML)"
|
||||||
@@ -156,6 +165,14 @@ They can be defined by using a file (YAML or TOML) or CLI arguments.
|
|||||||
[entryPoints.name.forwardedHeaders]
|
[entryPoints.name.forwardedHeaders]
|
||||||
insecure = true
|
insecure = true
|
||||||
trustedIPs = ["127.0.0.1", "192.168.0.1"]
|
trustedIPs = ["127.0.0.1", "192.168.0.1"]
|
||||||
|
[entryPoints.name.http.encodedCharacters]
|
||||||
|
allowEncodedSlash = true
|
||||||
|
allowEncodedBackSlash = true
|
||||||
|
allowEncodedNullCharacter = true
|
||||||
|
allowEncodedSemicolon = true
|
||||||
|
allowEncodedPercent = true
|
||||||
|
allowEncodedQuestionMark = true
|
||||||
|
allowEncodedHash = true
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash tab="CLI"
|
```bash tab="CLI"
|
||||||
@@ -174,6 +191,13 @@ They can be defined by using a file (YAML or TOML) or CLI arguments.
|
|||||||
--entryPoints.name.proxyProtocol.trustedIPs=127.0.0.1,192.168.0.1
|
--entryPoints.name.proxyProtocol.trustedIPs=127.0.0.1,192.168.0.1
|
||||||
--entryPoints.name.forwardedHeaders.insecure=true
|
--entryPoints.name.forwardedHeaders.insecure=true
|
||||||
--entryPoints.name.forwardedHeaders.trustedIPs=127.0.0.1,192.168.0.1
|
--entryPoints.name.forwardedHeaders.trustedIPs=127.0.0.1,192.168.0.1
|
||||||
|
--entryPoints.name.http.encodedCharacters.allowEncodedSlash=true
|
||||||
|
--entryPoints.name.http.encodedCharacters.allowEncodedBackSlash=true
|
||||||
|
--entryPoints.name.http.encodedCharacters.allowEncodedNullCharacter=true
|
||||||
|
--entryPoints.name.http.encodedCharacters.allowEncodedSemicolon=true
|
||||||
|
--entryPoints.name.http.encodedCharacters.allowEncodedPercent=true
|
||||||
|
--entryPoints.name.http.encodedCharacters.allowEncodedQuestionMark=true
|
||||||
|
--entryPoints.name.http.encodedCharacters.allowEncodedHash=true
|
||||||
```
|
```
|
||||||
|
|
||||||
### Address
|
### Address
|
||||||
@@ -614,6 +638,239 @@ You can configure Traefik to trust the forwarded headers information (`X-Forward
|
|||||||
--entryPoints.web.forwardedHeaders.connection=foobar
|
--entryPoints.web.forwardedHeaders.connection=foobar
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Encoded Characters
|
||||||
|
|
||||||
|
You can configure Traefik to control the handling of encoded characters in request paths for security purposes.
|
||||||
|
By default, Traefik rejects requests containing certain encoded characters that could be used in path traversal or other security attacks.
|
||||||
|
|
||||||
|
!!! warning "Security Considerations"
|
||||||
|
|
||||||
|
Allowing certain encoded characters may expose your application to security vulnerabilities.
|
||||||
|
|
||||||
|
??? info "`encodedCharacters.allowEncodedSlash`"
|
||||||
|
|
||||||
|
_Optional, Default=false_
|
||||||
|
|
||||||
|
Controls whether requests with encoded slash characters (`%2F` or `%2f`) in the path are allowed.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
## Static configuration
|
||||||
|
entryPoints:
|
||||||
|
web:
|
||||||
|
address: ":80"
|
||||||
|
http:
|
||||||
|
encodedCharacters:
|
||||||
|
allowEncodedSlash: true
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
## Static configuration
|
||||||
|
[entryPoints]
|
||||||
|
[entryPoints.web]
|
||||||
|
address = ":80"
|
||||||
|
|
||||||
|
[entryPoints.web.http.encodedCharacters]
|
||||||
|
allowEncodedSlash = true
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
## Static configuration
|
||||||
|
--entryPoints.web.address=:80
|
||||||
|
--entryPoints.web.http.encodedCharacters.allowEncodedSlash=true
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`encodedCharacters.allowEncodedBackSlash`"
|
||||||
|
|
||||||
|
_Optional, Default=false_
|
||||||
|
|
||||||
|
Controls whether requests with encoded back slash characters (`%5C` or `%5c`) in the path are allowed.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
## Static configuration
|
||||||
|
entryPoints:
|
||||||
|
web:
|
||||||
|
address: ":80"
|
||||||
|
http:
|
||||||
|
encodedCharacters:
|
||||||
|
allowEncodedBackSlash: true
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
## Static configuration
|
||||||
|
[entryPoints]
|
||||||
|
[entryPoints.web]
|
||||||
|
address = ":80"
|
||||||
|
|
||||||
|
[entryPoints.web.http.encodedCharacters]
|
||||||
|
allowEncodedBackSlash = true
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
## Static configuration
|
||||||
|
--entryPoints.web.address=:80
|
||||||
|
--entryPoints.web.http.encodedCharacters.allowEncodedBackSlash=true
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`encodedCharacters.allowEncodedNullCharacter`"
|
||||||
|
|
||||||
|
_Optional, Default=false_
|
||||||
|
|
||||||
|
Controls whether requests with encoded null characters (`%00`) in the path are allowed.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
## Static configuration
|
||||||
|
entryPoints:
|
||||||
|
web:
|
||||||
|
address: ":80"
|
||||||
|
http:
|
||||||
|
encodedCharacters:
|
||||||
|
allowEncodedNullCharacter: true
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
## Static configuration
|
||||||
|
[entryPoints]
|
||||||
|
[entryPoints.web]
|
||||||
|
address = ":80"
|
||||||
|
|
||||||
|
[entryPoints.web.http.encodedCharacters]
|
||||||
|
allowEncodedNullCharacter = true
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
## Static configuration
|
||||||
|
--entryPoints.web.address=:80
|
||||||
|
--entryPoints.web.http.encodedCharacters.allowEncodedNullCharacter=true
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`encodedCharacters.allowEncodedSemicolon`"
|
||||||
|
|
||||||
|
_Optional, Default=false_
|
||||||
|
|
||||||
|
Controls whether requests with encoded semicolon characters (`%3B` or `%3b`) in the path are allowed.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
## Static configuration
|
||||||
|
entryPoints:
|
||||||
|
web:
|
||||||
|
address: ":80"
|
||||||
|
http:
|
||||||
|
encodedCharacters:
|
||||||
|
allowEncodedSemicolon: true
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
## Static configuration
|
||||||
|
[entryPoints]
|
||||||
|
[entryPoints.web]
|
||||||
|
address = ":80"
|
||||||
|
|
||||||
|
[entryPoints.web.http.encodedCharacters]
|
||||||
|
allowEncodedSemicolon = true
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
## Static configuration
|
||||||
|
--entryPoints.web.address=:80
|
||||||
|
--entryPoints.web.http.encodedCharacters.allowEncodedSemicolon=true
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`encodedCharacters.allowEncodedPercent`"
|
||||||
|
|
||||||
|
_Optional, Default=false_
|
||||||
|
|
||||||
|
Controls whether requests with encoded percent characters (`%25`) in the path are allowed.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
## Static configuration
|
||||||
|
entryPoints:
|
||||||
|
web:
|
||||||
|
address: ":80"
|
||||||
|
http:
|
||||||
|
encodedCharacters:
|
||||||
|
allowEncodedPercent: true
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
## Static configuration
|
||||||
|
[entryPoints]
|
||||||
|
[entryPoints.web]
|
||||||
|
address = ":80"
|
||||||
|
|
||||||
|
[entryPoints.web.http.encodedCharacters]
|
||||||
|
allowEncodedPercent = true
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
## Static configuration
|
||||||
|
--entryPoints.web.address=:80
|
||||||
|
--entryPoints.web.http.encodedCharacters.allowEncodedPercent=true
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`encodedCharacters.allowEncodedQuestionMark`"
|
||||||
|
|
||||||
|
_Optional, Default=false_
|
||||||
|
|
||||||
|
Controls whether requests with encoded question mark characters (`%3F` or `%3f`) in the path are allowed.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
## Static configuration
|
||||||
|
entryPoints:
|
||||||
|
web:
|
||||||
|
address: ":80"
|
||||||
|
http:
|
||||||
|
encodedCharacters:
|
||||||
|
allowEncodedQuestionMark: true
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
## Static configuration
|
||||||
|
[entryPoints]
|
||||||
|
[entryPoints.web]
|
||||||
|
address = ":80"
|
||||||
|
|
||||||
|
[entryPoints.web.http.encodedCharacters]
|
||||||
|
allowEncodedQuestionMark = true
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
## Static configuration
|
||||||
|
--entryPoints.web.address=:80
|
||||||
|
--entryPoints.web.http.encodedCharacters.allowEncodedQuestionMark=true
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`encodedCharacters.allowEncodedHash`"
|
||||||
|
|
||||||
|
_Optional, Default=false_
|
||||||
|
|
||||||
|
Controls whether requests with encoded hash characters (`%23`) in the path are allowed.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
## Static configuration
|
||||||
|
entryPoints:
|
||||||
|
web:
|
||||||
|
address: ":80"
|
||||||
|
http:
|
||||||
|
encodedCharacters:
|
||||||
|
allowEncodedHash: true
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
## Static configuration
|
||||||
|
[entryPoints]
|
||||||
|
[entryPoints.web]
|
||||||
|
address = ":80"
|
||||||
|
|
||||||
|
[entryPoints.web.http.encodedCharacters]
|
||||||
|
allowEncodedHash = true
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
## Static configuration
|
||||||
|
--entryPoints.web.address=:80
|
||||||
|
--entryPoints.web.http.encodedCharacters.allowEncodedHash=true
|
||||||
|
```
|
||||||
|
|
||||||
### Transport
|
### Transport
|
||||||
|
|
||||||
#### `respondingTimeouts`
|
#### `respondingTimeouts`
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ title: "Content-Length"
|
|||||||
description: "Enforce strict Content‑Length validation in Traefik by streaming or full buffering to prevent truncated or over‑long requests and responses. Read the technical documentation."
|
description: "Enforce strict Content‑Length validation in Traefik by streaming or full buffering to prevent truncated or over‑long requests and responses. Read the technical documentation."
|
||||||
---
|
---
|
||||||
|
|
||||||
Traefik acts as a streaming proxy. By default, it checks each chunk of data against the `Content-Length` header as it passes it on to the backend or client. This live check blocks truncated or over‑long streams without holding the entire message.
|
Traefik acts as a streaming proxy. By default, it checks each chunk of data against the `Content-Length` header as it passes it on to the backend or client.
|
||||||
|
This live check blocks truncated or over‑long streams without holding the entire message.
|
||||||
|
|
||||||
If you need Traefik to read and verify the full body before any data moves on, add the [buffering middleware](../middlewares/http/buffering.md):
|
If you need Traefik to read and verify the full body before any data moves on, add the [buffering middleware](../middlewares/http/buffering.md):
|
||||||
|
|
||||||
@@ -21,4 +22,6 @@ With buffering enabled, Traefik will:
|
|||||||
- Reject the message if the counts do not match.
|
- Reject the message if the counts do not match.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
Buffering adds overhead. Every request and response is held in full before forwarding, which can increase memory use and latency. Use it when strict content validation is critical to your security posture.
|
|
||||||
|
Buffering adds overhead. Every request and response is held in full before forwarding, which can increase memory use and latency.
|
||||||
|
Use it when strict content validation is critical to your security posture.
|
||||||
|
|||||||
130
docs/content/security/request-path.md
Normal file
130
docs/content/security/request-path.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
---
|
||||||
|
title: "Request Path"
|
||||||
|
description: "Learn how Traefik processes and secures request paths through sanitization and encoded character filtering to protect against path traversal and injection attacks."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Request Path
|
||||||
|
|
||||||
|
Protecting Against Path-Based Attacks Through Sanitization and Filtering
|
||||||
|
{: .subtitle }
|
||||||
|
|
||||||
|
Traefik implements multiple layers of security when processing incoming request paths.
|
||||||
|
This includes path sanitization to normalize potentially dangerous sequences and encoded character filtering to prevent attack vectors that use URL encoding.
|
||||||
|
Understanding how Traefik handles request paths is crucial for maintaining a secure routing infrastructure.
|
||||||
|
|
||||||
|
## How Traefik Processes Request Paths
|
||||||
|
|
||||||
|
When Traefik receives an HTTP request, it processes the request path through several security-focused stages:
|
||||||
|
|
||||||
|
### 1. Encoded Character Filtering
|
||||||
|
|
||||||
|
Traefik inspects the path for potentially dangerous encoded characters and rejects requests containing them unless explicitly allowed.
|
||||||
|
|
||||||
|
Here is the list of the encoded characters that are rejected by default:
|
||||||
|
|
||||||
|
| Encoded Character | Character |
|
||||||
|
|-------------------|-------------------------|
|
||||||
|
| `%2f` or `%2F` | `/` (slash) |
|
||||||
|
| `%5c` or `%5C` | `\` (backslash) |
|
||||||
|
| `%00` | `NULL` (null character) |
|
||||||
|
| `%3b` or `%3B` | `;` (semicolon) |
|
||||||
|
| `%25` | `%` (percent) |
|
||||||
|
| `%3f` or `%3F` | `?` (question mark) |
|
||||||
|
| `%23` | `#` (hash) |
|
||||||
|
|
||||||
|
### 2. Path Normalization
|
||||||
|
|
||||||
|
Traefik normalizes the request path by decoding the unreserved percent-encoded characters,
|
||||||
|
as they are equivalent to their non-encoded form (according to [rfc3986#section-2.3](https://datatracker.ietf.org/doc/html/rfc3986#section-2.3)),
|
||||||
|
and capitalizing the percent-encoded characters (according to [rfc3986#section-6.2.2.1](https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.2.1)).
|
||||||
|
|
||||||
|
### 3. Path Sanitization
|
||||||
|
|
||||||
|
Traefik sanitizes request paths to prevent common attack vectors,
|
||||||
|
by removing the `..`, `.` and duplicate slash segments from the URL (according to [rfc3986#section-6.2.2.3](https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.2.3)).
|
||||||
|
|
||||||
|
## Path Security Configuration
|
||||||
|
|
||||||
|
Traefik provides two main mechanisms for path security that work together to protect your applications.
|
||||||
|
|
||||||
|
### Path Sanitization
|
||||||
|
|
||||||
|
Path sanitization is enabled by default and helps prevent directory traversal attacks by normalizing request paths.
|
||||||
|
Configure it in the [EntryPoints](../routing/entrypoints.md#sanitizepath) HTTP section:
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
entryPoints:
|
||||||
|
websecure:
|
||||||
|
address: ":443"
|
||||||
|
http:
|
||||||
|
sanitizePath: true # Default: true (recommended)
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[entryPoints.websecure]
|
||||||
|
address = ":443"
|
||||||
|
|
||||||
|
[entryPoints.websecure.http]
|
||||||
|
sanitizePath = true
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--entryPoints.websecure.address=:443
|
||||||
|
--entryPoints.websecure.http.sanitizePath=true
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sanitization behavior:**
|
||||||
|
|
||||||
|
- `./foo/bar` → `/foo/bar` (removes relative current directory)
|
||||||
|
- `/foo/../bar` → `/bar` (resolves parent directory traversal)
|
||||||
|
- `/foo/bar//` → `/foo/bar/` (removes duplicate slashes)
|
||||||
|
- `/./foo/../bar//` → `/bar/` (combines all normalizations)
|
||||||
|
|
||||||
|
### Encoded Character Filtering
|
||||||
|
|
||||||
|
Encoded character filtering provides an additional security layer by rejecting potentially dangerous URL-encoded characters.
|
||||||
|
Configure it in the [EntryPoints](../routing/entrypoints.md#encoded-characters) HTTP section.
|
||||||
|
|
||||||
|
This filtering occurs before path sanitization and catches attack attempts that use encoding to bypass other security controls.
|
||||||
|
|
||||||
|
All encoded character filtering is enabled by default (`false` means encoded characters are rejected), providing maximum security:
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
entryPoints:
|
||||||
|
websecure:
|
||||||
|
address: ":443"
|
||||||
|
http:
|
||||||
|
encodedCharacters:
|
||||||
|
allowEncodedSlash: false # %2F - Default: false (RECOMMENDED)
|
||||||
|
allowEncodedBackSlash: false # %5C - Default: false (RECOMMENDED)
|
||||||
|
allowEncodedNullCharacter: false # %00 - Default: false (RECOMMENDED)
|
||||||
|
allowEncodedSemicolon: false # %3B - Default: false (RECOMMENDED)
|
||||||
|
allowEncodedPercent: false # %25 - Default: false (RECOMMENDED)
|
||||||
|
allowEncodedQuestionMark: false # %3F - Default: false (RECOMMENDED)
|
||||||
|
allowEncodedHash: false # %23 - Default: false (RECOMMENDED)
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[entryPoints.websecure]
|
||||||
|
address = ":443"
|
||||||
|
|
||||||
|
[entryPoints.websecure.http.encodedCharacters]
|
||||||
|
allowEncodedSlash = false
|
||||||
|
allowEncodedBackSlash = false
|
||||||
|
allowEncodedNullCharacter = false
|
||||||
|
allowEncodedSemicolon = false
|
||||||
|
allowEncodedPercent = false
|
||||||
|
allowEncodedQuestionMark = false
|
||||||
|
allowEncodedHash = false
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--entryPoints.websecure.address=:443
|
||||||
|
--entryPoints.websecure.http.encodedCharacters.allowEncodedSlash=false
|
||||||
|
--entryPoints.websecure.http.encodedCharacters.allowEncodedBackSlash=false
|
||||||
|
--entryPoints.websecure.http.encodedCharacters.allowEncodedNullCharacter=false
|
||||||
|
--entryPoints.websecure.http.encodedCharacters.allowEncodedSemicolon=false
|
||||||
|
--entryPoints.websecure.http.encodedCharacters.allowEncodedPercent=false
|
||||||
|
--entryPoints.websecure.http.encodedCharacters.allowEncodedQuestionMark=false
|
||||||
|
--entryPoints.websecure.http.encodedCharacters.allowEncodedHash=false
|
||||||
|
```
|
||||||
@@ -212,6 +212,7 @@ nav:
|
|||||||
- 'Extend': 'extend/extend-traefik.md'
|
- 'Extend': 'extend/extend-traefik.md'
|
||||||
- '<span class="nav-link-with-icon">Govern <img src="https://doc.traefik.io/traefik-hub/img/ps-traefik-hub-logo-light.svg" class="menu-icon" alt="Traefik Hub API Gateway"></span>': 'govern/index.md'
|
- '<span class="nav-link-with-icon">Govern <img src="https://doc.traefik.io/traefik-hub/img/ps-traefik-hub-logo-light.svg" class="menu-icon" alt="Traefik Hub API Gateway"></span>': 'govern/index.md'
|
||||||
- 'Migrate':
|
- 'Migrate':
|
||||||
|
- 'NGINX Ingress to Traefik': 'migrate/nginx-to-traefik.md'
|
||||||
- 'Traefik v3 minor migrations': 'migrate/v3.md'
|
- 'Traefik v3 minor migrations': 'migrate/v3.md'
|
||||||
- 'Traefik v2 to v3':
|
- 'Traefik v2 to v3':
|
||||||
- 'Migration guide': 'migrate/v2-to-v3.md'
|
- 'Migration guide': 'migrate/v2-to-v3.md'
|
||||||
@@ -357,6 +358,7 @@ nav:
|
|||||||
- 'KV' : 'reference/routing-configuration/other-providers/kv.md'
|
- 'KV' : 'reference/routing-configuration/other-providers/kv.md'
|
||||||
- 'File' : 'reference/routing-configuration/other-providers/file.md'
|
- 'File' : 'reference/routing-configuration/other-providers/file.md'
|
||||||
- 'Security':
|
- 'Security':
|
||||||
|
- 'Request Path': 'security/request-path.md'
|
||||||
- 'Content-Length': 'security/content-length.md'
|
- 'Content-Length': 'security/content-length.md'
|
||||||
- 'TLS in Multi-Tenant Kubernetes': 'security/tls-certs-in-multi-tenant-kubernetes.md'
|
- 'TLS in Multi-Tenant Kubernetes': 'security/tls-certs-in-multi-tenant-kubernetes.md'
|
||||||
- 'Deprecation Notices':
|
- 'Deprecation Notices':
|
||||||
|
|||||||
84
go.mod
84
go.mod
@@ -8,13 +8,13 @@ require (
|
|||||||
github.com/abbot/go-http-auth v0.0.0-00010101000000-000000000000 // No tag on the repo.
|
github.com/abbot/go-http-auth v0.0.0-00010101000000-000000000000 // No tag on the repo.
|
||||||
github.com/andybalholm/brotli v1.1.1
|
github.com/andybalholm/brotli v1.1.1
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||||
github.com/aws/aws-sdk-go-v2 v1.39.4
|
github.com/aws/aws-sdk-go-v2 v1.40.0
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.31.15
|
github.com/aws/aws-sdk-go-v2/config v1.32.2
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.19
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.2
|
||||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.203.1
|
github.com/aws/aws-sdk-go-v2/service/ec2 v1.203.1
|
||||||
github.com/aws/aws-sdk-go-v2/service/ecs v1.53.15
|
github.com/aws/aws-sdk-go-v2/service/ecs v1.53.15
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.56.13
|
github.com/aws/aws-sdk-go-v2/service/ssm v1.56.13
|
||||||
github.com/aws/smithy-go v1.23.1
|
github.com/aws/smithy-go v1.23.2
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0
|
github.com/cenkalti/backoff/v4 v4.3.0
|
||||||
github.com/containous/alice v0.0.0-20181107144136-d83ebdd94cbd // No tag on the repo.
|
github.com/containous/alice v0.0.0-20181107144136-d83ebdd94cbd // No tag on the repo.
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0
|
github.com/coreos/go-systemd/v22 v22.5.0
|
||||||
@@ -23,7 +23,7 @@ require (
|
|||||||
github.com/docker/go-connections v0.5.0
|
github.com/docker/go-connections v0.5.0
|
||||||
github.com/fatih/structs v1.1.0
|
github.com/fatih/structs v1.1.0
|
||||||
github.com/fsnotify/fsnotify v1.9.0
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
github.com/go-acme/lego/v4 v4.28.0
|
github.com/go-acme/lego/v4 v4.29.0
|
||||||
github.com/go-kit/kit v0.13.0
|
github.com/go-kit/kit v0.13.0
|
||||||
github.com/go-kit/log v0.2.1
|
github.com/go-kit/log v0.2.1
|
||||||
github.com/golang/protobuf v1.5.4
|
github.com/golang/protobuf v1.5.4
|
||||||
@@ -55,7 +55,7 @@ require (
|
|||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // No tag on the repo.
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // No tag on the repo.
|
||||||
github.com/prometheus/client_golang v1.23.0
|
github.com/prometheus/client_golang v1.23.0
|
||||||
github.com/prometheus/client_model v0.6.2
|
github.com/prometheus/client_model v0.6.2
|
||||||
github.com/quic-go/quic-go v0.57.0
|
github.com/quic-go/quic-go v0.57.1
|
||||||
github.com/redis/go-redis/v9 v9.8.0
|
github.com/redis/go-redis/v9 v9.8.0
|
||||||
github.com/rs/zerolog v1.33.0
|
github.com/rs/zerolog v1.33.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
@@ -126,8 +126,8 @@ require (
|
|||||||
dario.cat/mergo v1.0.1 // indirect
|
dario.cat/mergo v1.0.1 // indirect
|
||||||
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
|
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
|
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
|
||||||
@@ -142,7 +142,7 @@ require (
|
|||||||
github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect
|
github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect
|
||||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
|
||||||
github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect
|
github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
github.com/Masterminds/semver/v3 v3.3.1 // indirect
|
github.com/Masterminds/semver/v3 v3.3.1 // indirect
|
||||||
@@ -157,19 +157,20 @@ require (
|
|||||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
|
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
|
||||||
github.com/aliyun/credentials-go v1.4.7 // indirect
|
github.com/aliyun/credentials-go v1.4.7 // indirect
|
||||||
github.com/armon/go-metrics v0.4.1 // indirect
|
github.com/armon/go-metrics v0.4.1 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.11 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.11 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.11 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.11 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.2 // indirect
|
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.8 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.59.1 // indirect
|
github.com/aws/aws-sdk-go-v2/service/route53 v1.61.0 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.8 // indirect
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.3 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.9 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 // indirect
|
||||||
github.com/aziontech/azionapi-go-sdk v0.143.0 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 // indirect
|
||||||
github.com/baidubce/bce-sdk-go v0.9.250 // indirect
|
github.com/aziontech/azionapi-go-sdk v0.144.0 // indirect
|
||||||
|
github.com/baidubce/bce-sdk-go v0.9.252 // indirect
|
||||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/blendle/zapdriver v1.3.1 // indirect
|
github.com/blendle/zapdriver v1.3.1 // indirect
|
||||||
@@ -195,15 +196,16 @@ require (
|
|||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||||
github.com/exoscale/egoscale/v3 v3.1.27 // indirect
|
github.com/exoscale/egoscale/v3 v3.1.31 // indirect
|
||||||
github.com/fatih/color v1.18.0 // indirect
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
github.com/ghodss/yaml v1.0.0 // indirect
|
github.com/ghodss/yaml v1.0.0 // indirect
|
||||||
github.com/gin-gonic/gin v1.9.1 // indirect
|
github.com/gin-gonic/gin v1.9.1 // indirect
|
||||||
github.com/go-acme/alidns-20150109/v4 v4.6.1 // indirect
|
github.com/go-acme/alidns-20150109/v4 v4.7.0 // indirect
|
||||||
github.com/go-acme/tencentclouddnspod v1.1.10 // indirect
|
github.com/go-acme/esa-20240910/v2 v2.40.1 // indirect
|
||||||
|
github.com/go-acme/tencentclouddnspod v1.1.25 // indirect
|
||||||
github.com/go-acme/tencentedgdeone v1.1.48 // indirect
|
github.com/go-acme/tencentedgdeone v1.1.48 // indirect
|
||||||
github.com/go-errors/errors v1.0.1 // indirect
|
github.com/go-errors/errors v1.0.1 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||||
@@ -233,7 +235,7 @@ require (
|
|||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/google/s2a-go v0.1.9 // indirect
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||||
github.com/gophercloud/gophercloud v1.14.1 // indirect
|
github.com/gophercloud/gophercloud v1.14.1 // indirect
|
||||||
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect
|
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect
|
||||||
@@ -249,7 +251,7 @@ require (
|
|||||||
github.com/hashicorp/hcl v1.0.1-vault-5 // indirect
|
github.com/hashicorp/hcl v1.0.1-vault-5 // indirect
|
||||||
github.com/hashicorp/serf v0.10.1 // indirect
|
github.com/hashicorp/serf v0.10.1 // indirect
|
||||||
github.com/huandu/xstrings v1.5.0 // indirect
|
github.com/huandu/xstrings v1.5.0 // indirect
|
||||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.173 // indirect
|
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.178 // indirect
|
||||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
||||||
github.com/imdario/mergo v0.3.16 // indirect
|
github.com/imdario/mergo v0.3.16 // indirect
|
||||||
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
|
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
|
||||||
@@ -264,7 +266,7 @@ require (
|
|||||||
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
|
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
|
||||||
github.com/labbsr0x/goh v1.0.1 // indirect
|
github.com/labbsr0x/goh v1.0.1 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/linode/linodego v1.60.0 // indirect
|
github.com/linode/linodego v1.61.0 // indirect
|
||||||
github.com/liquidweb/liquidweb-cli v0.6.9 // indirect
|
github.com/liquidweb/liquidweb-cli v0.6.9 // indirect
|
||||||
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
|
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||||
@@ -300,12 +302,12 @@ require (
|
|||||||
github.com/nrdcg/dnspod-go v0.4.0 // indirect
|
github.com/nrdcg/dnspod-go v0.4.0 // indirect
|
||||||
github.com/nrdcg/freemyip v0.3.0 // indirect
|
github.com/nrdcg/freemyip v0.3.0 // indirect
|
||||||
github.com/nrdcg/goacmedns v0.2.0 // indirect
|
github.com/nrdcg/goacmedns v0.2.0 // indirect
|
||||||
github.com/nrdcg/goinwx v0.11.0 // indirect
|
github.com/nrdcg/goinwx v0.12.0 // indirect
|
||||||
github.com/nrdcg/mailinabox v0.3.0 // indirect
|
github.com/nrdcg/mailinabox v0.3.0 // indirect
|
||||||
github.com/nrdcg/namesilo v0.5.0 // indirect
|
github.com/nrdcg/namesilo v0.5.0 // indirect
|
||||||
github.com/nrdcg/nodion v0.1.0 // indirect
|
github.com/nrdcg/nodion v0.1.0 // indirect
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.103.0 // indirect
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.0 // indirect
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.103.0 // indirect
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.0 // indirect
|
||||||
github.com/nrdcg/porkbun v0.4.0 // indirect
|
github.com/nrdcg/porkbun v0.4.0 // indirect
|
||||||
github.com/nrdcg/vegadns v0.3.0 // indirect
|
github.com/nrdcg/vegadns v0.3.0 // indirect
|
||||||
github.com/nzdjb/go-metaname v1.0.0 // indirect
|
github.com/nzdjb/go-metaname v1.0.0 // indirect
|
||||||
@@ -328,7 +330,7 @@ require (
|
|||||||
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 // indirect
|
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 // indirect
|
||||||
github.com/sacloud/api-client-go v0.3.3 // indirect
|
github.com/sacloud/api-client-go v0.3.3 // indirect
|
||||||
github.com/sacloud/go-http v0.1.9 // indirect
|
github.com/sacloud/go-http v0.1.9 // indirect
|
||||||
github.com/sacloud/iaas-api-go v1.20.0 // indirect
|
github.com/sacloud/iaas-api-go v1.22.0 // indirect
|
||||||
github.com/sacloud/packages-go v0.0.11 // indirect
|
github.com/sacloud/packages-go v0.0.11 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||||
@@ -349,7 +351,7 @@ require (
|
|||||||
github.com/spf13/viper v1.18.2 // indirect
|
github.com/spf13/viper v1.18.2 // indirect
|
||||||
github.com/stretchr/objx v0.5.2 // indirect
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.48 // indirect
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.3 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||||
@@ -359,12 +361,12 @@ require (
|
|||||||
github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419 // indirect
|
github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
||||||
github.com/volcengine/volc-sdk-golang v1.0.224 // indirect
|
github.com/volcengine/volc-sdk-golang v1.0.229 // indirect
|
||||||
github.com/vultr/govultr/v3 v3.24.0 // indirect
|
github.com/vultr/govultr/v3 v3.25.0 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
github.com/yandex-cloud/go-genproto v0.34.0 // indirect
|
github.com/yandex-cloud/go-genproto v0.38.0 // indirect
|
||||||
github.com/yandex-cloud/go-sdk/services/dns v0.0.16 // indirect
|
github.com/yandex-cloud/go-sdk/services/dns v0.0.20 // indirect
|
||||||
github.com/yandex-cloud/go-sdk/v2 v2.24.0 // indirect
|
github.com/yandex-cloud/go-sdk/v2 v2.28.0 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
github.com/zeebo/errs v1.4.0 // indirect
|
github.com/zeebo/errs v1.4.0 // indirect
|
||||||
@@ -389,17 +391,17 @@ require (
|
|||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/arch v0.4.0 // indirect
|
golang.org/x/arch v0.4.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||||
golang.org/x/oauth2 v0.32.0 // indirect
|
golang.org/x/oauth2 v0.33.0 // indirect
|
||||||
golang.org/x/term v0.37.0 // indirect
|
golang.org/x/term v0.37.0 // indirect
|
||||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||||
google.golang.org/api v0.254.0 // indirect
|
google.golang.org/api v0.256.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/ns1/ns1-go.v2 v2.15.1 // indirect
|
gopkg.in/ns1/ns1-go.v2 v2.15.2 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
k8s.io/klog/v2 v2.130.1 // indirect
|
k8s.io/klog/v2 v2.130.1 // indirect
|
||||||
k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3 // indirect
|
k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3 // indirect
|
||||||
|
|||||||
172
go.sum
172
go.sum
@@ -50,10 +50,10 @@ github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYs
|
|||||||
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo=
|
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo=
|
||||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
|
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
|
||||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 h1:KpMC6LFL7mqpExyMC9jVOYRiVhLmamjeZfRsUpB7l4s=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
|
||||||
@@ -95,8 +95,8 @@ github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUM
|
|||||||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
||||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
||||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI=
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
@@ -145,7 +145,6 @@ github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC
|
|||||||
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
|
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
|
||||||
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
|
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
|
||||||
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
|
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
|
||||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.12/go.mod h1:f2wDpbM7hK9SvLIH09zSKVU1TsyemUNOqErMscMMl7c=
|
|
||||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 h1:Q00FU3H94Ts0ZIHDmY+fYGgB7dV9D/YX6FGsgorQPgw=
|
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 h1:Q00FU3H94Ts0ZIHDmY+fYGgB7dV9D/YX6FGsgorQPgw=
|
||||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=
|
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=
|
||||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
|
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
|
||||||
@@ -168,7 +167,6 @@ github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/Ke
|
|||||||
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||||
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||||
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
|
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
|
||||||
github.com/alibabacloud-go/tea v1.3.12/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
|
|
||||||
github.com/alibabacloud-go/tea v1.3.13 h1:WhGy6LIXaMbBM6VBYcsDCz6K/TPsT1Ri2hPmmZffZ94=
|
github.com/alibabacloud-go/tea v1.3.13 h1:WhGy6LIXaMbBM6VBYcsDCz6K/TPsT1Ri2hPmmZffZ94=
|
||||||
github.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
|
github.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
|
||||||
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
|
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
|
||||||
@@ -199,18 +197,18 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:W
|
|||||||
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
|
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
|
||||||
github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
|
github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.39.4 h1:qTsQKcdQPHnfGYBBs+Btl8QwxJeoWcOcPcixK90mRhg=
|
github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.39.4/go.mod h1:yWSxrnioGUZ4WVv9TgMrNUeLV3PFESn/v+6T/Su8gnM=
|
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.31.15 h1:gE3M4xuNXfC/9bG4hyowGm/35uQTi7bUKeYs5e/6uvU=
|
github.com/aws/aws-sdk-go-v2/config v1.32.2 h1:4liUsdEpUUPZs5WVapsJLx5NPmQhQdez7nYFcovrytk=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.31.15/go.mod h1:HvnvGJoE2I95KAIW8kkWVPJ4XhdrlvwJpV6pEzFQa8o=
|
github.com/aws/aws-sdk-go-v2/config v1.32.2/go.mod h1:l0hs06IFz1eCT+jTacU/qZtC33nvcnLADAPL/XyrkZI=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.19 h1:Jc1zzwkSY1QbkEcLujwqRTXOdvW8ppND3jRBb/VhBQc=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.2 h1:qZry8VUyTK4VIo5aEdUcBjPZHL2v4FyQ3QEOaWcFLu4=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.19/go.mod h1:DIfQ9fAk5H0pGtnqfqkbSIzky82qYnGvh06ASQXXg6A=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.2/go.mod h1:YUqm5a1/kBnoK+/NY5WEiMocZihKSo15/tJdmdXnM5g=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.11 h1:X7X4YKb+c0rkI6d4uJ5tEMxXgCZ+jZ/D6mvkno8c8Uw=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.11/go.mod h1:EqM6vPZQsZHYvC4Cai35UDg/f5NCEU+vp0WfbVqVcZc=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.11 h1:7AANQZkF3ihM8fbdftpjhken0TP9sBzFbV/Ze/Y4HXA=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.11/go.mod h1:NTF4QCGkm6fzVwncpkFQqoquQyOolcyXfbpC98urj+c=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.11 h1:ShdtWUZT37LCAA4Mw2kJAJtzaszfSHFb5n25sdcv4YE=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.11/go.mod h1:7bUb2sSr2MZ3M/N+VyETLTQtInemHXb/Fl3s8CLzm0Y=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||||
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o=
|
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o=
|
||||||
@@ -218,29 +216,31 @@ github.com/aws/aws-sdk-go-v2/service/ec2 v1.203.1 h1:ZgY9zeVAe+54Qa7o1GXKRNTez79
|
|||||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.203.1/go.mod h1:0naMk66LtdeTmE+1CWQTKwtzOQ2t8mavOhMhR0Pv1m0=
|
github.com/aws/aws-sdk-go-v2/service/ec2 v1.203.1/go.mod h1:0naMk66LtdeTmE+1CWQTKwtzOQ2t8mavOhMhR0Pv1m0=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ecs v1.53.15 h1:uH0DMwDjLGgjjYMk3M1MXHggk37trTiJIvwyJNP17Ig=
|
github.com/aws/aws-sdk-go-v2/service/ecs v1.53.15 h1:uH0DMwDjLGgjjYMk3M1MXHggk37trTiJIvwyJNP17Ig=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ecs v1.53.15/go.mod h1:49tE5yYdlAHqZIO8u5+u9Xy9k8IaV0v5cstZrjnX5+c=
|
github.com/aws/aws-sdk-go-v2/service/ecs v1.53.15/go.mod h1:49tE5yYdlAHqZIO8u5+u9Xy9k8IaV0v5cstZrjnX5+c=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2 h1:xtuxji5CS0JknaXoACOunXOYOQzgfTvGAc9s2QdCJA4=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2/go.mod h1:zxwi0DIR0rcRcgdbl7E2MSOvxDyyXGBlScvBkARFaLQ=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.11 h1:GpMf3z2KJa4RnJ0ew3Hac+hRFYLZ9DDjfgXjuW+pB54=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE46kyYqyhs0XEBDFFSREtdnr8HQuLPQPLCrY=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.11/go.mod h1:6MZP3ZI4QQsgUCFTwMZA2V0sEriNQ8k2hmoHF3qjimQ=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E=
|
||||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.2 h1:pr1dQ9vamhAf2mYOgiRRC/w9Ht4POFhy6+xXw7hOqwY=
|
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.8 h1:jhwva7OKpYXrTQmCG4L7lF2FvB2irs1oRyGAwmQ4lmA=
|
||||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.2/go.mod h1:A4Ch93K7Wam4Qe0Wl0XbPgcgoL5KIJtFIe7wHw6OPWE=
|
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.8/go.mod h1:x+omzRoqYYFX+H8/va+Gt2Yg4xGaHZMRowr77Y/UGIA=
|
||||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.59.1 h1:KuoA/cmy/yK8n9v/d6WH36cZwGxFOrn0TmZ4lNN3MKQ=
|
github.com/aws/aws-sdk-go-v2/service/route53 v1.61.0 h1:W3+0Cbc9awFBr9Yt7nFUkvB4N4e7vVIGtKD1qDttXn4=
|
||||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.59.1/go.mod h1:BymbICXBfXQHO6i+yTBhocA9a6DM0uMDQqYelqa9wzs=
|
github.com/aws/aws-sdk-go-v2/service/route53 v1.61.0/go.mod h1:Wa3q5R2uwIfIL3HZH+vG1/P9y7CjjfzTgcz5IWXlsZs=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 h1:MxMBdKTYBjPQChlJhi4qlEueqB1p1KcbTEa7tD5aqPs=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.56.13 h1:JfPeW7F6Y+VqBg6p+8zQv4wlgceguYu5ZT0USEGZ89g=
|
github.com/aws/aws-sdk-go-v2/service/ssm v1.56.13 h1:JfPeW7F6Y+VqBg6p+8zQv4wlgceguYu5ZT0USEGZ89g=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.56.13/go.mod h1:EonGQFn66wZkJJrrKXrryrxoS3V30rcHvaWvc6oGHCI=
|
github.com/aws/aws-sdk-go-v2/service/ssm v1.56.13/go.mod h1:EonGQFn66wZkJJrrKXrryrxoS3V30rcHvaWvc6oGHCI=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.8 h1:M5nimZmugcZUO9wG7iVtROxPhiqyZX6ejS1lxlDPbTU=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 h1:ksUT5KtgpZd3SAiFJNJ0AFEJVva3gjBmN7eXUZjzUwQ=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.8/go.mod h1:mbef/pgKhtKRwrigPPs7SSSKZgytzP8PQ6P6JAAdqyM=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.3 h1:S5GuJZpYxE0lKeMHKn+BRTz6PTFpgThyJ+5mYfux7BM=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 h1:GtsxyiF3Nd3JahRBJbxLCCdYW9ltGQYrFWg8XdkGDd8=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.3/go.mod h1:X4OF+BTd7HIb3L+tc4UlWHVrpgwZZIVENU15pRDVTI0=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.9 h1:Ekml5vGg6sHSZLZJQJagefnVe6PmqC2oiRkBq4F7fU0=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 h1:a5UTtD4mHBU3t0o6aHQZFJTNKVfxFWfPX7J0Lr7G+uY=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.9/go.mod h1:/e15V+o1zFHWdH3u7lpI3rVBcxszktIKuHKCY2/py+k=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso=
|
||||||
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
||||||
github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M=
|
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
|
||||||
github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||||
github.com/aziontech/azionapi-go-sdk v0.143.0 h1:4eEBlYT10prgeCVTNR9FIc7f59Crbl2zrH1a4D1BUqU=
|
github.com/aziontech/azionapi-go-sdk v0.144.0 h1:T+/w18o+FCiZsk3Z0ACBVVe7c/5EGLG15S3P8JfuPfo=
|
||||||
github.com/aziontech/azionapi-go-sdk v0.143.0/go.mod h1:cA5DY/VP4X5Eu11LpQNzNn83ziKjja7QVMIl4J45feA=
|
github.com/aziontech/azionapi-go-sdk v0.144.0/go.mod h1:OKxP/R0iVXnJJakYwMhh2BGAXnud8Ruy55Ak9ANuWoU=
|
||||||
github.com/baidubce/bce-sdk-go v0.9.250 h1:fnvV5clsNCAP6pCauj0eNaUnoLVmjQGnco7rcMqp984=
|
github.com/baidubce/bce-sdk-go v0.9.252 h1:TONS/utgfEkDjvHllVZFBrTsjsNhk51rhWuj3ppcL4s=
|
||||||
github.com/baidubce/bce-sdk-go v0.9.250/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
|
github.com/baidubce/bce-sdk-go v0.9.252/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
|
||||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||||
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
@@ -382,8 +382,8 @@ github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lSh
|
|||||||
github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||||
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
|
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
|
||||||
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
|
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
|
||||||
github.com/exoscale/egoscale/v3 v3.1.27 h1:vKdWZG8QFDc7rY7lCfcuudO+ovyp5psYjFwKVqmkhCE=
|
github.com/exoscale/egoscale/v3 v3.1.31 h1:/dySEUSAxU+hlAS/eLxAoY8ZYmtOtaoL1P+lDwH7ojY=
|
||||||
github.com/exoscale/egoscale/v3 v3.1.27/go.mod h1:0iY8OxgHJCS5TKqDNhwOW95JBKCnBZl3YGU4Yt+NqkU=
|
github.com/exoscale/egoscale/v3 v3.1.31/go.mod h1:0iY8OxgHJCS5TKqDNhwOW95JBKCnBZl3YGU4Yt+NqkU=
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||||
@@ -421,12 +421,14 @@ github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwv
|
|||||||
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
|
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
|
||||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||||
github.com/go-acme/alidns-20150109/v4 v4.6.1 h1:Dch3aWRcw4U62+jKPjPQN3iW3TPvgIywATbvHzojXeo=
|
github.com/go-acme/alidns-20150109/v4 v4.7.0 h1:PqJ/wR0JTpL4v0Owu1uM7bPQ1Yww0eQLAuuSdLjjQaQ=
|
||||||
github.com/go-acme/alidns-20150109/v4 v4.6.1/go.mod h1:RBcqBA5IvUWtlpjx6dC6EkPVyBNLQ+mR18XoaP38BFY=
|
github.com/go-acme/alidns-20150109/v4 v4.7.0/go.mod h1:btQvB6xZoN6ykKB74cPhiR+uvhrEE2AFVXm6RDmCHm0=
|
||||||
github.com/go-acme/lego/v4 v4.28.0 h1:URKsCcybo7SjqqZckeBcDN9Vl29/bKS///75tcNkMHQ=
|
github.com/go-acme/esa-20240910/v2 v2.40.1 h1:pog3UlF5d+3LPoo1L8u8PqB189recIXX8T7pGoEz18A=
|
||||||
github.com/go-acme/lego/v4 v4.28.0/go.mod h1:bzjilr03IgbaOwlH396hq5W56Bi0/uoRwW/JM8hP7m4=
|
github.com/go-acme/esa-20240910/v2 v2.40.1/go.mod h1:ZYdN9EN9ikn26SNapxCVjZ65pHT/1qm4fzuJ7QGVX6g=
|
||||||
github.com/go-acme/tencentclouddnspod v1.1.10 h1:ERVJ4mc3cT4Nb3+n6H/c1AwZnChGBqLoymE0NVYscKI=
|
github.com/go-acme/lego/v4 v4.29.0 h1:vKMEtvoKb0gOO9rWO9zMBwE4CgI5A5CWDsK4QEeBqzo=
|
||||||
github.com/go-acme/tencentclouddnspod v1.1.10/go.mod h1:Bo/0YQJ/99FM+44HmCQkByuptX1tJsJ9V14MGV/2Qco=
|
github.com/go-acme/lego/v4 v4.29.0/go.mod h1:rnYyDj1NdDd9y1dHkVuUS97j7bfe9I61+oY9odKaHM8=
|
||||||
|
github.com/go-acme/tencentclouddnspod v1.1.25 h1:7H3ZKshkaHzCXfRpAHVB5nvxeDDl2XLeNZfrNHiZj/s=
|
||||||
|
github.com/go-acme/tencentclouddnspod v1.1.25/go.mod h1:XXfzp0AYV7UAUsHKT6R0KAUJFhqAUXmWGF07Elpa5cE=
|
||||||
github.com/go-acme/tencentedgdeone v1.1.48 h1:WLyLBsRVhSLFmtbEFXk0naLODSQn7X6J0Fc/qR8xVUk=
|
github.com/go-acme/tencentedgdeone v1.1.48 h1:WLyLBsRVhSLFmtbEFXk0naLODSQn7X6J0Fc/qR8xVUk=
|
||||||
github.com/go-acme/tencentedgdeone v1.1.48/go.mod h1:mu6tA+bPhlSd+CKUfzRikE0mfxmTlBI6dVTn9LY9dRI=
|
github.com/go-acme/tencentedgdeone v1.1.48/go.mod h1:mu6tA+bPhlSd+CKUfzRikE0mfxmTlBI6dVTn9LY9dRI=
|
||||||
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
|
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
|
||||||
@@ -616,8 +618,8 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
|||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||||
@@ -721,8 +723,8 @@ github.com/http-wasm/http-wasm-host-go v0.7.0/go.mod h1:adXKcLmL7yuavH/e0kBAp7b3
|
|||||||
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.173 h1:Y4ixGadyrK9xHw6Z+cyiiME3SBXepEcUoiT+B8C5FoQ=
|
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.178 h1:eNVkjzdPMgM2qih9aECiFUI8S9zgpOwXxeBPAwQqtvU=
|
||||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.173/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI=
|
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.178/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI=
|
||||||
github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=
|
github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
@@ -837,8 +839,8 @@ github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++
|
|||||||
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
|
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
|
||||||
github.com/lestrrat-go/jwx v1.2.7/go.mod h1:bw24IXWbavc0R2RsOtpXL7RtMyP589yZ1+L7kd09ZGA=
|
github.com/lestrrat-go/jwx v1.2.7/go.mod h1:bw24IXWbavc0R2RsOtpXL7RtMyP589yZ1+L7kd09ZGA=
|
||||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||||
github.com/linode/linodego v1.60.0 h1:SgsebJFRCi+lSmYy+C40wmKZeJllGGm+W12Qw4+yVdI=
|
github.com/linode/linodego v1.61.0 h1:9g20NWl+/SbhDFj6X5EOZXtM2hBm1Mx8I9h8+F3l1LM=
|
||||||
github.com/linode/linodego v1.60.0/go.mod h1:1+Bt0oTz5rBnDOJbGhccxn7LYVytXTIIfAy7QYmijDs=
|
github.com/linode/linodego v1.61.0/go.mod h1:64o30geLNwR0NeYh5HM/WrVCBXcSqkKnRK3x9xoRuJI=
|
||||||
github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
|
github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
|
||||||
github.com/liquidweb/liquidweb-cli v0.6.9 h1:acbIvdRauiwbxIsOCEMXGwF75aSJDbDiyAWPjVnwoYM=
|
github.com/liquidweb/liquidweb-cli v0.6.9 h1:acbIvdRauiwbxIsOCEMXGwF75aSJDbDiyAWPjVnwoYM=
|
||||||
github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ=
|
github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ=
|
||||||
@@ -986,18 +988,18 @@ github.com/nrdcg/freemyip v0.3.0 h1:0D2rXgvLwe2RRaVIjyUcQ4S26+cIS2iFwnhzDsEuuwc=
|
|||||||
github.com/nrdcg/freemyip v0.3.0/go.mod h1:c1PscDvA0ukBF0dwelU/IwOakNKnVxetpAQ863RMJoM=
|
github.com/nrdcg/freemyip v0.3.0/go.mod h1:c1PscDvA0ukBF0dwelU/IwOakNKnVxetpAQ863RMJoM=
|
||||||
github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0=
|
github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0=
|
||||||
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
|
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
|
||||||
github.com/nrdcg/goinwx v0.11.0 h1:GER0SE3POub7rxARt3Y3jRy1OON1hwF1LRxHz5xsFBw=
|
github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
|
||||||
github.com/nrdcg/goinwx v0.11.0/go.mod h1:0BXSC0FxVtU4aTjX0Zw3x0DK32tjugLzeNIAGtwXvPQ=
|
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
|
||||||
github.com/nrdcg/mailinabox v0.3.0 h1:PHkC1elKXKAjEvdx2HHFMgcEGZFqudAl7aU3L2JDhM4=
|
github.com/nrdcg/mailinabox v0.3.0 h1:PHkC1elKXKAjEvdx2HHFMgcEGZFqudAl7aU3L2JDhM4=
|
||||||
github.com/nrdcg/mailinabox v0.3.0/go.mod h1:1eFIGcM4lI+AfFOUpbs548SFGz1ZWoMOGbECBmkghw4=
|
github.com/nrdcg/mailinabox v0.3.0/go.mod h1:1eFIGcM4lI+AfFOUpbs548SFGz1ZWoMOGbECBmkghw4=
|
||||||
github.com/nrdcg/namesilo v0.5.0 h1:6QNxT/XxE+f5B+7QlfWorthNzOzcGlBLRQxqi6YeBrE=
|
github.com/nrdcg/namesilo v0.5.0 h1:6QNxT/XxE+f5B+7QlfWorthNzOzcGlBLRQxqi6YeBrE=
|
||||||
github.com/nrdcg/namesilo v0.5.0/go.mod h1:4UkwlwQfDt74kSGmhLaDylnBrD94IfflnpoEaj6T2qw=
|
github.com/nrdcg/namesilo v0.5.0/go.mod h1:4UkwlwQfDt74kSGmhLaDylnBrD94IfflnpoEaj6T2qw=
|
||||||
github.com/nrdcg/nodion v0.1.0 h1:zLKaqTn2X0aDuBHHfyA1zFgeZfiCpmu/O9DM73okavw=
|
github.com/nrdcg/nodion v0.1.0 h1:zLKaqTn2X0aDuBHHfyA1zFgeZfiCpmu/O9DM73okavw=
|
||||||
github.com/nrdcg/nodion v0.1.0/go.mod h1:inbuh3neCtIWlMPZHtEpe43TmRXxHV6+hk97iCZicms=
|
github.com/nrdcg/nodion v0.1.0/go.mod h1:inbuh3neCtIWlMPZHtEpe43TmRXxHV6+hk97iCZicms=
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.103.0 h1:GPwwX9GFIBjV4u1M3Cr8eKCP6drW01IsfQSDIz6SUk8=
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.0 h1:bppmFqrJ87U4gWilemAW9oa4Qepf2JBTK/mPgaZLP2A=
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.103.0/go.mod h1:SfDIKzNQ5AGNMMOA3LGqSPnn63F6Gc4E4bsKArqymvg=
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.0/go.mod h1:SfDIKzNQ5AGNMMOA3LGqSPnn63F6Gc4E4bsKArqymvg=
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.103.0 h1:MjHla6lf1jpjGXORLpzMeo/tSmx0ejmjMjdjTByaDGY=
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.0 h1:IHPZs4Mo/lxyo+gYB+baheb2kGmHtNGQk2DKPDHqPjA=
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.103.0/go.mod h1:o1/kMADX0SlB4hJjWtcs3M6VIUOGR78yhPyiBv6oBkk=
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.0/go.mod h1:yELd0uJLiIyv9sGIh5ZRCHEB1B2QFNURWkQIMqb3ZwE=
|
||||||
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
|
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
|
||||||
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
|
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
|
||||||
github.com/nrdcg/vegadns v0.3.0 h1:11FQMw7xVIRUWO9o5+Z/5YZhmPWlm4oxUUH3F6EVqQU=
|
github.com/nrdcg/vegadns v0.3.0 h1:11FQMw7xVIRUWO9o5+Z/5YZhmPWlm4oxUUH3F6EVqQU=
|
||||||
@@ -1114,8 +1116,8 @@ github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9
|
|||||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE=
|
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
|
||||||
github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
|
github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
|
||||||
@@ -1142,8 +1144,8 @@ github.com/sacloud/api-client-go v0.3.3 h1:ZpSAyGpITA8UFO3Hq4qMHZLGuNI1FgxAxo4sq
|
|||||||
github.com/sacloud/api-client-go v0.3.3/go.mod h1:0p3ukcWYXRCc2AUWTl1aA+3sXLvurvvDqhRaLZRLBwo=
|
github.com/sacloud/api-client-go v0.3.3/go.mod h1:0p3ukcWYXRCc2AUWTl1aA+3sXLvurvvDqhRaLZRLBwo=
|
||||||
github.com/sacloud/go-http v0.1.9 h1:Xa5PY8/pb7XWhwG9nAeXSrYXPbtfBWqawgzxD5co3VE=
|
github.com/sacloud/go-http v0.1.9 h1:Xa5PY8/pb7XWhwG9nAeXSrYXPbtfBWqawgzxD5co3VE=
|
||||||
github.com/sacloud/go-http v0.1.9/go.mod h1:DpDG+MSyxYaBwPJ7l3aKLMzwYdTVtC5Bo63HActcgoE=
|
github.com/sacloud/go-http v0.1.9/go.mod h1:DpDG+MSyxYaBwPJ7l3aKLMzwYdTVtC5Bo63HActcgoE=
|
||||||
github.com/sacloud/iaas-api-go v1.20.0 h1:L4TfAzoFSwxrD3QXX8UxJa2o+GZrP9b863K+voTy3tQ=
|
github.com/sacloud/iaas-api-go v1.22.0 h1:nvLQNuxcfxILvoxA6WcnTjU9A8yv8dPI1OSYHAPxBJk=
|
||||||
github.com/sacloud/iaas-api-go v1.20.0/go.mod h1:XV995RM1I7k5AHb7UZrCVyDF/8bZXDxa+uk1EXoj/Zs=
|
github.com/sacloud/iaas-api-go v1.22.0/go.mod h1:PLcolyFlby/0ExZTOdUf9xzhkEMBuVzORadXDNN21no=
|
||||||
github.com/sacloud/packages-go v0.0.11 h1:hrRWLmfPM9w7GBs6xb5/ue6pEMl8t1UuDKyR/KfteHo=
|
github.com/sacloud/packages-go v0.0.11 h1:hrRWLmfPM9w7GBs6xb5/ue6pEMl8t1UuDKyR/KfteHo=
|
||||||
github.com/sacloud/packages-go v0.0.11/go.mod h1:XNF5MCTWcHo9NiqWnYctVbASSSZR3ZOmmQORIzcurJ8=
|
github.com/sacloud/packages-go v0.0.11/go.mod h1:XNF5MCTWcHo9NiqWnYctVbASSSZR3ZOmmQORIzcurJ8=
|
||||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||||
@@ -1254,9 +1256,10 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
|
|||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046 h1:8rUlviSVOEe7TMk7W0gIPrW8MqEzYfZHpsNWSf8s2vg=
|
github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046 h1:8rUlviSVOEe7TMk7W0gIPrW8MqEzYfZHpsNWSf8s2vg=
|
||||||
github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
|
github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.10/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.25/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.48 h1:aoRUrz2ag27jQWcOKHgeE+toSti6/xPqHKMLruOtJuM=
|
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.48/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.48/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.3 h1:r05ohLc0LVEpiEQeOJ5QwCiKk6XM9kjTca6+UAbNR/8=
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.3/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||||
github.com/testcontainers/testcontainers-go v0.32.0 h1:ug1aK08L3gCHdhknlTTwWjPHPS+/alvLJU/DRxTD/ME=
|
github.com/testcontainers/testcontainers-go v0.32.0 h1:ug1aK08L3gCHdhknlTTwWjPHPS+/alvLJU/DRxTD/ME=
|
||||||
github.com/testcontainers/testcontainers-go v0.32.0/go.mod h1:CRHrzHLQhlXUsa5gXjTOfqIEJcrK5+xMDmBr/WMI88E=
|
github.com/testcontainers/testcontainers-go v0.32.0/go.mod h1:CRHrzHLQhlXUsa5gXjTOfqIEJcrK5+xMDmBr/WMI88E=
|
||||||
github.com/testcontainers/testcontainers-go/modules/k3s v0.32.0 h1:Z3DTMveNUqeGJZ+CXZhpvI7OF1BS71Ywi3SwoXLZ4Lc=
|
github.com/testcontainers/testcontainers-go/modules/k3s v0.32.0 h1:Z3DTMveNUqeGJZ+CXZhpvI7OF1BS71Ywi3SwoXLZ4Lc=
|
||||||
@@ -1315,14 +1318,14 @@ github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPU
|
|||||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ=
|
github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ=
|
||||||
github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q=
|
github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q=
|
||||||
github.com/volcengine/volc-sdk-golang v1.0.224 h1:k9Vtg64tQAgFTOGWzhyL0b0axuTuExXbLNVlslWlBZI=
|
github.com/volcengine/volc-sdk-golang v1.0.229 h1:gOkDltTS6Fta8OyfYrbeY9bqCHHyiJuGYNJpR5MR+Fo=
|
||||||
github.com/volcengine/volc-sdk-golang v1.0.224/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM=
|
github.com/volcengine/volc-sdk-golang v1.0.229/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM=
|
||||||
github.com/vulcand/oxy/v2 v2.0.3 h1:CPWVPfW4hVZXzwwiQzpFidbnJKpahjPHezM+7TkZRNw=
|
github.com/vulcand/oxy/v2 v2.0.3 h1:CPWVPfW4hVZXzwwiQzpFidbnJKpahjPHezM+7TkZRNw=
|
||||||
github.com/vulcand/oxy/v2 v2.0.3/go.mod h1:k3t+xjyqmXVh88FdFDbYmUKMEvNpaejvBW14es6H70A=
|
github.com/vulcand/oxy/v2 v2.0.3/go.mod h1:k3t+xjyqmXVh88FdFDbYmUKMEvNpaejvBW14es6H70A=
|
||||||
github.com/vulcand/predicate v1.2.0 h1:uFsW1gcnnR7R+QTID+FVcs0sSYlIGntoGOTb3rQJt50=
|
github.com/vulcand/predicate v1.2.0 h1:uFsW1gcnnR7R+QTID+FVcs0sSYlIGntoGOTb3rQJt50=
|
||||||
github.com/vulcand/predicate v1.2.0/go.mod h1:VipoNYXny6c8N381zGUWkjuuNHiRbeAZhE7Qm9c+2GA=
|
github.com/vulcand/predicate v1.2.0/go.mod h1:VipoNYXny6c8N381zGUWkjuuNHiRbeAZhE7Qm9c+2GA=
|
||||||
github.com/vultr/govultr/v3 v3.24.0 h1:fTTTj0VBve+Miy+wGhlb90M2NMDfpGFi6Frlj3HVy6M=
|
github.com/vultr/govultr/v3 v3.25.0 h1:rS8/Vdy8HlHArwmD4MtLY+hbbpYAbcnZueZrE6b0oUg=
|
||||||
github.com/vultr/govultr/v3 v3.24.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
github.com/vultr/govultr/v3 v3.25.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
||||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
@@ -1333,12 +1336,12 @@ github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gi
|
|||||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yandex-cloud/go-genproto v0.34.0 h1:qhTJpPxOTKQbV44rIqoZSdzxDtZW27fkFjAcipEy8Zs=
|
github.com/yandex-cloud/go-genproto v0.38.0 h1:uB3btG7mLOnu53ehYtRARCk04+80sBpxDrSkP3qC6G8=
|
||||||
github.com/yandex-cloud/go-genproto v0.34.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo=
|
github.com/yandex-cloud/go-genproto v0.38.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo=
|
||||||
github.com/yandex-cloud/go-sdk/services/dns v0.0.16 h1:0UYrBlQjTO2ct5xcSx6rqkQB95wRBPMVwxfqLQD1sUE=
|
github.com/yandex-cloud/go-sdk/services/dns v0.0.20 h1:xHBRa+IIYpTgMbTbmZf7aEKIqrJMcZGIF8ea4XIyLX0=
|
||||||
github.com/yandex-cloud/go-sdk/services/dns v0.0.16/go.mod h1:HlS3aIAdYEmJu2Ska/nzpcuv9LLVSMMXKGhzyLQwf5s=
|
github.com/yandex-cloud/go-sdk/services/dns v0.0.20/go.mod h1:8nYQULLJbbe51qdBY7Ay5v8wtDgdH7nHCMZs4XkwzLg=
|
||||||
github.com/yandex-cloud/go-sdk/v2 v2.24.0 h1:G53N/RB5g/jw2xNN0egspnwd2ByHA1OVH6wbTx/tIlo=
|
github.com/yandex-cloud/go-sdk/v2 v2.28.0 h1:KDOrN75xokZBYbgjq6Pjvo+hEpu32xFhErtomLBML5s=
|
||||||
github.com/yandex-cloud/go-sdk/v2 v2.24.0/go.mod h1:ZRdpyOig8c/W3bNhwvkeXWWPeDScd9nmXv4AJzKvOsk=
|
github.com/yandex-cloud/go-sdk/v2 v2.28.0/go.mod h1:6vmAhqoCVYSJEb5OuhHUqIdxDy2b9uUXp1e5sqMhTmo=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||||
@@ -1624,10 +1627,9 @@ golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4Iltr
|
|||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -1882,8 +1884,8 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
|
|||||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||||
google.golang.org/api v0.254.0 h1:jl3XrGj7lRjnlUvZAbAdhINTLbsg5dbjmR90+pTQvt4=
|
google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
|
||||||
google.golang.org/api v0.254.0/go.mod h1:5BkSURm3D9kAqjGvBNgf0EcbX6Rnrf6UArKkwBzAyqQ=
|
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
@@ -1926,8 +1928,8 @@ google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuO
|
|||||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
|
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
@@ -1985,8 +1987,8 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
|||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
gopkg.in/ns1/ns1-go.v2 v2.15.1 h1:8rri2TzAPYcVbBGXn48+dz1Xg30PzHfZ4k8A9JOS0Z0=
|
gopkg.in/ns1/ns1-go.v2 v2.15.2 h1:aBVyKeEH3rBFWwX72xPPjEuRL4+Lp5P9GlAcrzu0Y5M=
|
||||||
gopkg.in/ns1/ns1-go.v2 v2.15.1/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc=
|
gopkg.in/ns1/ns1-go.v2 v2.15.2/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc=
|
||||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
|||||||
@@ -2041,8 +2041,9 @@ spec:
|
|||||||
More info: https://doc.traefik.io/traefik/v3.6/middlewares/http/redirectscheme/
|
More info: https://doc.traefik.io/traefik/v3.6/middlewares/http/redirectscheme/
|
||||||
properties:
|
properties:
|
||||||
permanent:
|
permanent:
|
||||||
description: Permanent defines whether the redirection is permanent
|
description: |-
|
||||||
(308).
|
Permanent defines whether the redirection is permanent.
|
||||||
|
For HTTP GET requests a 301 is returned, otherwise a 308 is returned.
|
||||||
type: boolean
|
type: boolean
|
||||||
port:
|
port:
|
||||||
description: Port defines the port of the new URL.
|
description: Port defines the port of the new URL.
|
||||||
|
|||||||
20
integration/fixtures/simple_deny.toml
Normal file
20
integration/fixtures/simple_deny.toml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
[global]
|
||||||
|
checkNewVersion = false
|
||||||
|
sendAnonymousUsage = false
|
||||||
|
|
||||||
|
[entryPoints]
|
||||||
|
[entryPoints.web]
|
||||||
|
address = ":8000"
|
||||||
|
|
||||||
|
[api]
|
||||||
|
insecure = true
|
||||||
|
|
||||||
|
[providers.file]
|
||||||
|
filename = "{{ .SelfFilename }}"
|
||||||
|
|
||||||
|
## dynamic configuration ##
|
||||||
|
|
||||||
|
[http.routers]
|
||||||
|
[http.routers.router]
|
||||||
|
service = "noop@internal"
|
||||||
|
rule = "Host(`deny.localhost`)"
|
||||||
@@ -9,6 +9,8 @@
|
|||||||
[entryPoints]
|
[entryPoints]
|
||||||
[entryPoints.web]
|
[entryPoints.web]
|
||||||
address = ":8000"
|
address = ":8000"
|
||||||
|
[entryPoints.web.http.encodedCharacters]
|
||||||
|
allowEncodedSlash = true
|
||||||
|
|
||||||
[api]
|
[api]
|
||||||
insecure = true
|
insecure = true
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
[entryPoints]
|
|
||||||
[entryPoints.web]
|
|
||||||
address = ":8000"
|
|
||||||
[entryPoints.web.forwardedHeaders]
|
|
||||||
insecure = true
|
|
||||||
notAppendXForwardedFor = true
|
|
||||||
|
|
||||||
[api]
|
|
||||||
insecure = true
|
|
||||||
|
|
||||||
[providers.file]
|
|
||||||
filename = "{{ .DynamicConfPath }}"
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
[entryPoints]
|
|
||||||
[entryPoints.web]
|
|
||||||
address = ":8000"
|
|
||||||
[entryPoints.web.forwardedHeaders]
|
|
||||||
insecure = true
|
|
||||||
|
|
||||||
[api]
|
|
||||||
insecure = true
|
|
||||||
|
|
||||||
[providers.file]
|
|
||||||
filename = "{{ .DynamicConfPath }}"
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
[entryPoints]
|
|
||||||
[entryPoints.web]
|
|
||||||
address = ":8000"
|
|
||||||
[entryPoints.web.forwardedHeaders]
|
|
||||||
insecure = true
|
|
||||||
notAppendXForwardedFor = true
|
|
||||||
|
|
||||||
[api]
|
|
||||||
insecure = true
|
|
||||||
|
|
||||||
[experimental]
|
|
||||||
[experimental.fastProxy]
|
|
||||||
debug = true
|
|
||||||
|
|
||||||
[providers.file]
|
|
||||||
filename = "{{ .DynamicConfPath }}"
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
[entryPoints]
|
|
||||||
[entryPoints.web]
|
|
||||||
address = ":8000"
|
|
||||||
[entryPoints.web.forwardedHeaders]
|
|
||||||
insecure = true
|
|
||||||
|
|
||||||
[api]
|
|
||||||
insecure = true
|
|
||||||
|
|
||||||
[experimental]
|
|
||||||
[experimental.fastProxy]
|
|
||||||
debug = true
|
|
||||||
|
|
||||||
[providers.file]
|
|
||||||
filename = "{{ .DynamicConfPath }}"
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
[http.routers]
|
|
||||||
[http.routers.router1]
|
|
||||||
entryPoints = ["web"]
|
|
||||||
rule = "PathPrefix(`/`)"
|
|
||||||
service = "service1"
|
|
||||||
|
|
||||||
[http.services]
|
|
||||||
[http.services.service1.loadBalancer]
|
|
||||||
[[http.services.service1.loadBalancer.servers]]
|
|
||||||
url = "{{ .Server }}"
|
|
||||||
@@ -94,197 +94,6 @@ func (s *SimpleSuite) TestSimpleFastProxy() {
|
|||||||
assert.GreaterOrEqual(s.T(), 1, callCount)
|
assert.GreaterOrEqual(s.T(), 1, callCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SimpleSuite) TestXForwardedForDisabled() {
|
|
||||||
srv1 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
// Echo back the X-Forwarded-For header
|
|
||||||
xff := req.Header.Get("X-Forwarded-For")
|
|
||||||
_, _ = rw.Write([]byte(xff))
|
|
||||||
}))
|
|
||||||
defer srv1.Close()
|
|
||||||
|
|
||||||
dynamicConf := s.adaptFile("resources/compose/x_forwarded_for.toml", struct {
|
|
||||||
Server string
|
|
||||||
}{
|
|
||||||
Server: srv1.URL,
|
|
||||||
})
|
|
||||||
|
|
||||||
staticConf := s.adaptFile("fixtures/x_forwarded_for.toml", struct {
|
|
||||||
DynamicConfPath string
|
|
||||||
}{
|
|
||||||
DynamicConfPath: dynamicConf,
|
|
||||||
})
|
|
||||||
|
|
||||||
s.traefikCmd(withConfigFile(staticConf))
|
|
||||||
|
|
||||||
// Wait for Traefik to start
|
|
||||||
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second, try.BodyContains("service1"))
|
|
||||||
require.NoError(s.T(), err)
|
|
||||||
|
|
||||||
// Test with appendXForwardedFor = false
|
|
||||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
|
|
||||||
require.NoError(s.T(), err)
|
|
||||||
|
|
||||||
// Set an existing X-Forwarded-For header
|
|
||||||
req.Header.Set("X-Forwarded-For", "1.2.3.4")
|
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
|
||||||
require.NoError(s.T(), err)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
require.NoError(s.T(), err)
|
|
||||||
|
|
||||||
// The backend should receive the original X-Forwarded-For header unchanged
|
|
||||||
// (Traefik should NOT append RemoteAddr when appendXForwardedFor = false)
|
|
||||||
assert.Equal(s.T(), "1.2.3.4", string(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SimpleSuite) TestXForwardedForEnabled() {
|
|
||||||
srv1 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
// Echo back the X-Forwarded-For header
|
|
||||||
xff := req.Header.Get("X-Forwarded-For")
|
|
||||||
_, _ = rw.Write([]byte(xff))
|
|
||||||
}))
|
|
||||||
defer srv1.Close()
|
|
||||||
|
|
||||||
dynamicConf := s.adaptFile("resources/compose/x_forwarded_for.toml", struct {
|
|
||||||
Server string
|
|
||||||
}{
|
|
||||||
Server: srv1.URL,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Use a config with appendXForwardedFor = true
|
|
||||||
staticConf := s.adaptFile("fixtures/x_forwarded_for_enabled.toml", struct {
|
|
||||||
DynamicConfPath string
|
|
||||||
}{
|
|
||||||
DynamicConfPath: dynamicConf,
|
|
||||||
})
|
|
||||||
|
|
||||||
s.traefikCmd(withConfigFile(staticConf))
|
|
||||||
|
|
||||||
// Wait for Traefik to start
|
|
||||||
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second, try.BodyContains("service1"))
|
|
||||||
require.NoError(s.T(), err)
|
|
||||||
|
|
||||||
// Test with default appendXForwardedFor = true
|
|
||||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
|
|
||||||
require.NoError(s.T(), err)
|
|
||||||
|
|
||||||
// Set an existing X-Forwarded-For header
|
|
||||||
req.Header.Set("X-Forwarded-For", "1.2.3.4")
|
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
|
||||||
require.NoError(s.T(), err)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
require.NoError(s.T(), err)
|
|
||||||
|
|
||||||
// The backend should receive the X-Forwarded-For header with RemoteAddr appended
|
|
||||||
// (should be "1.2.3.4, 127.0.0.1" since the request comes from localhost)
|
|
||||||
assert.Contains(s.T(), string(body), "1.2.3.4,")
|
|
||||||
assert.Contains(s.T(), string(body), "127.0.0.1")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SimpleSuite) TestXForwardedForDisabledFastProxy() {
|
|
||||||
srv1 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
// Verify FastProxy is being used
|
|
||||||
assert.Contains(s.T(), req.Header, "X-Traefik-Fast-Proxy")
|
|
||||||
|
|
||||||
// Echo back the X-Forwarded-For header
|
|
||||||
xff := req.Header.Get("X-Forwarded-For")
|
|
||||||
_, _ = rw.Write([]byte(xff))
|
|
||||||
}))
|
|
||||||
defer srv1.Close()
|
|
||||||
|
|
||||||
dynamicConf := s.adaptFile("resources/compose/x_forwarded_for.toml", struct {
|
|
||||||
Server string
|
|
||||||
}{
|
|
||||||
Server: srv1.URL,
|
|
||||||
})
|
|
||||||
|
|
||||||
staticConf := s.adaptFile("fixtures/x_forwarded_for_fastproxy.toml", struct {
|
|
||||||
DynamicConfPath string
|
|
||||||
}{
|
|
||||||
DynamicConfPath: dynamicConf,
|
|
||||||
})
|
|
||||||
|
|
||||||
s.traefikCmd(withConfigFile(staticConf))
|
|
||||||
|
|
||||||
// Wait for Traefik to start
|
|
||||||
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second, try.BodyContains("service1"))
|
|
||||||
require.NoError(s.T(), err)
|
|
||||||
|
|
||||||
// Test with appendXForwardedFor = false
|
|
||||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
|
|
||||||
require.NoError(s.T(), err)
|
|
||||||
|
|
||||||
// Set an existing X-Forwarded-For header
|
|
||||||
req.Header.Set("X-Forwarded-For", "1.2.3.4")
|
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
|
||||||
require.NoError(s.T(), err)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
require.NoError(s.T(), err)
|
|
||||||
|
|
||||||
// The backend should receive the original X-Forwarded-For header unchanged
|
|
||||||
// (FastProxy should NOT append RemoteAddr when notAppendXForwardedFor = true)
|
|
||||||
assert.Equal(s.T(), "1.2.3.4", string(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SimpleSuite) TestXForwardedForEnabledFastProxy() {
|
|
||||||
srv1 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
// Verify FastProxy is being used
|
|
||||||
assert.Contains(s.T(), req.Header, "X-Traefik-Fast-Proxy")
|
|
||||||
|
|
||||||
// Echo back the X-Forwarded-For header
|
|
||||||
xff := req.Header.Get("X-Forwarded-For")
|
|
||||||
_, _ = rw.Write([]byte(xff))
|
|
||||||
}))
|
|
||||||
defer srv1.Close()
|
|
||||||
|
|
||||||
dynamicConf := s.adaptFile("resources/compose/x_forwarded_for.toml", struct {
|
|
||||||
Server string
|
|
||||||
}{
|
|
||||||
Server: srv1.URL,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Use a config with appendXForwardedFor = false (default)
|
|
||||||
staticConf := s.adaptFile("fixtures/x_forwarded_for_fastproxy_enabled.toml", struct {
|
|
||||||
DynamicConfPath string
|
|
||||||
}{
|
|
||||||
DynamicConfPath: dynamicConf,
|
|
||||||
})
|
|
||||||
|
|
||||||
s.traefikCmd(withConfigFile(staticConf))
|
|
||||||
|
|
||||||
// Wait for Traefik to start
|
|
||||||
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second, try.BodyContains("service1"))
|
|
||||||
require.NoError(s.T(), err)
|
|
||||||
|
|
||||||
// Test with default appendXForwardedFor = true
|
|
||||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
|
|
||||||
require.NoError(s.T(), err)
|
|
||||||
|
|
||||||
// Set an existing X-Forwarded-For header
|
|
||||||
req.Header.Set("X-Forwarded-For", "1.2.3.4")
|
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
|
||||||
require.NoError(s.T(), err)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
require.NoError(s.T(), err)
|
|
||||||
|
|
||||||
// The backend should receive the X-Forwarded-For header with RemoteAddr appended
|
|
||||||
// (FastProxy should append RemoteAddr when notAppendXForwardedFor = false)
|
|
||||||
// (should be "1.2.3.4, 127.0.0.1" since the request comes from localhost)
|
|
||||||
assert.Contains(s.T(), string(body), "1.2.3.4,")
|
|
||||||
assert.Contains(s.T(), string(body), "127.0.0.1")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SimpleSuite) TestWithWebConfig() {
|
func (s *SimpleSuite) TestWithWebConfig() {
|
||||||
s.cmdTraefik(withConfigFile("fixtures/simple_web.toml"))
|
s.cmdTraefik(withConfigFile("fixtures/simple_web.toml"))
|
||||||
|
|
||||||
@@ -1886,16 +1695,16 @@ func (s *SimpleSuite) TestDenyFragment() {
|
|||||||
s.composeUp()
|
s.composeUp()
|
||||||
defer s.composeDown()
|
defer s.composeDown()
|
||||||
|
|
||||||
s.traefikCmd(withConfigFile("fixtures/simple_default.toml"))
|
file := s.adaptFile("fixtures/simple_deny.toml", struct{}{})
|
||||||
|
_, _ = s.cmdTraefik(withConfigFile(file))
|
||||||
|
|
||||||
// Expected a 404 as we did not configure anything
|
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`deny.localhost`)"))
|
||||||
err := try.GetRequest("http://127.0.0.1:8000/", 1*time.Second, try.StatusCodeIs(http.StatusNotFound))
|
|
||||||
require.NoError(s.T(), err)
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
conn, err := net.Dial("tcp", "127.0.0.1:8000")
|
conn, err := net.Dial("tcp", "127.0.0.1:8000")
|
||||||
require.NoError(s.T(), err)
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
_, err = conn.Write([]byte("GET /#/?bar=toto;boo=titi HTTP/1.1\nHost: other.localhost\n\n"))
|
_, err = conn.Write([]byte("GET /#/?bar=toto;boo=titi HTTP/1.1\nHost: deny.localhost\n\n"))
|
||||||
require.NoError(s.T(), err)
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
|
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func TestHandler_SupportDump(t *testing.T) {
|
|||||||
assert.Contains(t, string(files["version.json"]), `"version":"dev"`)
|
assert.Contains(t, string(files["version.json"]), `"version":"dev"`)
|
||||||
|
|
||||||
// Verify static config contains entry points
|
// Verify static config contains entry points
|
||||||
assert.Contains(t, string(files["static-config.json"]), `"entryPoints":{"web":{"address":"xxxx","http":{}}}`)
|
assert.Contains(t, string(files["static-config.json"]), `"entryPoints":{"web":{"address":"xxxx","http":{"encodedCharacters":{}}}`)
|
||||||
|
|
||||||
// Verify runtime config contains services
|
// Verify runtime config contains services
|
||||||
assert.Contains(t, string(files["runtime-config.json"]), `"services":`)
|
assert.Contains(t, string(files["runtime-config.json"]), `"services":`)
|
||||||
|
|||||||
4
pkg/api/testdata/entrypoint-bar.json
vendored
4
pkg/api/testdata/entrypoint-bar.json
vendored
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"address": ":81",
|
"address": ":81",
|
||||||
"http": {},
|
"http": {
|
||||||
|
"encodedCharacters": {}
|
||||||
|
},
|
||||||
"name": "bar"
|
"name": "bar"
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"address": ":81",
|
"address": ":81",
|
||||||
"http": {},
|
"http": {
|
||||||
|
"encodedCharacters": {}
|
||||||
|
},
|
||||||
"name": "foo / bar"
|
"name": "foo / bar"
|
||||||
}
|
}
|
||||||
|
|||||||
20
pkg/api/testdata/entrypoints-many-lastpage.json
vendored
20
pkg/api/testdata/entrypoints-many-lastpage.json
vendored
@@ -1,27 +1,37 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"address": ":14",
|
"address": ":14",
|
||||||
"http": {},
|
"http": {
|
||||||
|
"encodedCharacters": {}
|
||||||
|
},
|
||||||
"name": "ep14"
|
"name": "ep14"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": ":15",
|
"address": ":15",
|
||||||
"http": {},
|
"http": {
|
||||||
|
"encodedCharacters": {}
|
||||||
|
},
|
||||||
"name": "ep15"
|
"name": "ep15"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": ":16",
|
"address": ":16",
|
||||||
"http": {},
|
"http": {
|
||||||
|
"encodedCharacters": {}
|
||||||
|
},
|
||||||
"name": "ep16"
|
"name": "ep16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": ":17",
|
"address": ":17",
|
||||||
"http": {},
|
"http": {
|
||||||
|
"encodedCharacters": {}
|
||||||
|
},
|
||||||
"name": "ep17"
|
"name": "ep17"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": ":18",
|
"address": ":18",
|
||||||
"http": {},
|
"http": {
|
||||||
|
"encodedCharacters": {}
|
||||||
|
},
|
||||||
"name": "ep18"
|
"name": "ep18"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
4
pkg/api/testdata/entrypoints-page2.json
vendored
4
pkg/api/testdata/entrypoints-page2.json
vendored
@@ -1,7 +1,9 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"address": ":82",
|
"address": ":82",
|
||||||
"http": {},
|
"http": {
|
||||||
|
"encodedCharacters": {}
|
||||||
|
},
|
||||||
"name": "web2"
|
"name": "web2"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
8
pkg/api/testdata/entrypoints.json
vendored
8
pkg/api/testdata/entrypoints.json
vendored
@@ -8,7 +8,9 @@
|
|||||||
"192.168.1.4"
|
"192.168.1.4"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"http": {},
|
"http": {
|
||||||
|
"encodedCharacters": {}
|
||||||
|
},
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"proxyProtocol": {
|
"proxyProtocol": {
|
||||||
"insecure": true,
|
"insecure": true,
|
||||||
@@ -38,7 +40,9 @@
|
|||||||
"192.168.1.40"
|
"192.168.1.40"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"http": {},
|
"http": {
|
||||||
|
"encodedCharacters": {}
|
||||||
|
},
|
||||||
"name": "websecure",
|
"name": "websecure",
|
||||||
"proxyProtocol": {
|
"proxyProtocol": {
|
||||||
"insecure": true,
|
"insecure": true,
|
||||||
|
|||||||
@@ -655,8 +655,13 @@ type RedirectScheme struct {
|
|||||||
Scheme string `json:"scheme,omitempty" toml:"scheme,omitempty" yaml:"scheme,omitempty" export:"true"`
|
Scheme string `json:"scheme,omitempty" toml:"scheme,omitempty" yaml:"scheme,omitempty" export:"true"`
|
||||||
// Port defines the port of the new URL.
|
// Port defines the port of the new URL.
|
||||||
Port string `json:"port,omitempty" toml:"port,omitempty" yaml:"port,omitempty" export:"true"`
|
Port string `json:"port,omitempty" toml:"port,omitempty" yaml:"port,omitempty" export:"true"`
|
||||||
// Permanent defines whether the redirection is permanent (308).
|
// Permanent defines whether the redirection is permanent.
|
||||||
|
// For HTTP GET requests a 301 is returned, otherwise a 308 is returned.
|
||||||
Permanent bool `json:"permanent,omitempty" toml:"permanent,omitempty" yaml:"permanent,omitempty" export:"true"`
|
Permanent bool `json:"permanent,omitempty" toml:"permanent,omitempty" yaml:"permanent,omitempty" export:"true"`
|
||||||
|
// ForcePermanentRedirect is an internal field (not exposed in configuration).
|
||||||
|
// When set to true, this forces the use of permanent redirects 308, regardless of the request method.
|
||||||
|
// Used by the provider ingress-ngin.
|
||||||
|
ForcePermanentRedirect bool `json:"-" toml:"-" yaml:"-" label:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// +k8s:deepcopy-gen=true
|
// +k8s:deepcopy-gen=true
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ type HTTPConfig struct {
|
|||||||
Redirections *Redirections `description:"Set of redirection" json:"redirections,omitempty" toml:"redirections,omitempty" yaml:"redirections,omitempty" export:"true"`
|
Redirections *Redirections `description:"Set of redirection" json:"redirections,omitempty" toml:"redirections,omitempty" yaml:"redirections,omitempty" export:"true"`
|
||||||
Middlewares []string `description:"Default middlewares for the routers linked to the entry point." json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
|
Middlewares []string `description:"Default middlewares for the routers linked to the entry point." json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
|
||||||
TLS *TLSConfig `description:"Default TLS configuration for the routers linked to the entry point." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
TLS *TLSConfig `description:"Default TLS configuration for the routers linked to the entry point." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||||
EncodeQuerySemicolons bool `description:"Defines whether request query semicolons should be URLEncoded." json:"encodeQuerySemicolons,omitempty" toml:"encodeQuerySemicolons,omitempty" yaml:"encodeQuerySemicolons,omitempty"`
|
EncodedCharacters EncodedCharacters `description:"Defines which encoded characters are allowed in the request path." json:"encodedCharacters,omitempty" toml:"encodedCharacters,omitempty" yaml:"encodedCharacters,omitempty" export:"true"`
|
||||||
|
EncodeQuerySemicolons bool `description:"Defines whether request query semicolons should be URLEncoded." json:"encodeQuerySemicolons,omitempty" toml:"encodeQuerySemicolons,omitempty" yaml:"encodeQuerySemicolons,omitempty" export:"true"`
|
||||||
SanitizePath *bool `description:"Defines whether to enable request path sanitization (removal of /./, /../ and multiple slash sequences)." json:"sanitizePath,omitempty" toml:"sanitizePath,omitempty" yaml:"sanitizePath,omitempty" export:"true"`
|
SanitizePath *bool `description:"Defines whether to enable request path sanitization (removal of /./, /../ and multiple slash sequences)." json:"sanitizePath,omitempty" toml:"sanitizePath,omitempty" yaml:"sanitizePath,omitempty" export:"true"`
|
||||||
MaxHeaderBytes int `description:"Maximum size of request headers in bytes." json:"maxHeaderBytes,omitempty" toml:"maxHeaderBytes,omitempty" yaml:"maxHeaderBytes,omitempty" export:"true"`
|
MaxHeaderBytes int `description:"Maximum size of request headers in bytes." json:"maxHeaderBytes,omitempty" toml:"maxHeaderBytes,omitempty" yaml:"maxHeaderBytes,omitempty" export:"true"`
|
||||||
}
|
}
|
||||||
@@ -80,6 +81,50 @@ func (c *HTTPConfig) SetDefaults() {
|
|||||||
c.MaxHeaderBytes = http.DefaultMaxHeaderBytes
|
c.MaxHeaderBytes = http.DefaultMaxHeaderBytes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EncodedCharacters configures which encoded characters are allowed in the request path.
|
||||||
|
type EncodedCharacters struct {
|
||||||
|
AllowEncodedSlash bool `description:"Defines whether requests with encoded slash characters in the path are allowed." json:"allowEncodedSlash,omitempty" toml:"allowEncodedSlash,omitempty" yaml:"allowEncodedSlash,omitempty" export:"true"`
|
||||||
|
AllowEncodedBackSlash bool `description:"Defines whether requests with encoded back slash characters in the path are allowed." json:"allowEncodedBackSlash,omitempty" toml:"allowEncodedBackSlash,omitempty" yaml:"allowEncodedBackSlash,omitempty" export:"true"`
|
||||||
|
AllowEncodedNullCharacter bool `description:"Defines whether requests with encoded null characters in the path are allowed." json:"allowEncodedNullCharacter,omitempty" toml:"allowEncodedNullCharacter,omitempty" yaml:"allowEncodedNullCharacter,omitempty" export:"true"`
|
||||||
|
AllowEncodedSemicolon bool `description:"Defines whether requests with encoded semicolon characters in the path are allowed." json:"allowEncodedSemicolon,omitempty" toml:"allowEncodedSemicolon,omitempty" yaml:"allowEncodedSemicolon,omitempty" export:"true"`
|
||||||
|
AllowEncodedPercent bool `description:"Defines whether requests with encoded percent characters in the path are allowed." json:"allowEncodedPercent,omitempty" toml:"allowEncodedPercent,omitempty" yaml:"allowEncodedPercent,omitempty" export:"true"`
|
||||||
|
AllowEncodedQuestionMark bool `description:"Defines whether requests with encoded question mark characters in the path are allowed." json:"allowEncodedQuestionMark,omitempty" toml:"allowEncodedQuestionMark,omitempty" yaml:"allowEncodedQuestionMark,omitempty" export:"true"`
|
||||||
|
AllowEncodedHash bool `description:"Defines whether requests with encoded hash characters in the path are allowed." json:"allowEncodedHash,omitempty" toml:"allowEncodedHash,omitempty" yaml:"allowEncodedHash,omitempty" export:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map returns a map of unallowed encoded characters.
|
||||||
|
func (h *EncodedCharacters) Map() map[string]struct{} {
|
||||||
|
characters := make(map[string]struct{})
|
||||||
|
|
||||||
|
if !h.AllowEncodedSlash {
|
||||||
|
characters["%2F"] = struct{}{}
|
||||||
|
characters["%2f"] = struct{}{}
|
||||||
|
}
|
||||||
|
if !h.AllowEncodedBackSlash {
|
||||||
|
characters["%5C"] = struct{}{}
|
||||||
|
characters["%5c"] = struct{}{}
|
||||||
|
}
|
||||||
|
if !h.AllowEncodedNullCharacter {
|
||||||
|
characters["%00"] = struct{}{}
|
||||||
|
}
|
||||||
|
if !h.AllowEncodedSemicolon {
|
||||||
|
characters["%3B"] = struct{}{}
|
||||||
|
characters["%3b"] = struct{}{}
|
||||||
|
}
|
||||||
|
if !h.AllowEncodedPercent {
|
||||||
|
characters["%25"] = struct{}{}
|
||||||
|
}
|
||||||
|
if !h.AllowEncodedQuestionMark {
|
||||||
|
characters["%3F"] = struct{}{}
|
||||||
|
characters["%3f"] = struct{}{}
|
||||||
|
}
|
||||||
|
if !h.AllowEncodedHash {
|
||||||
|
characters["%23"] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return characters
|
||||||
|
}
|
||||||
|
|
||||||
// HTTP2Config is the HTTP2 configuration of an entry point.
|
// HTTP2Config is the HTTP2 configuration of an entry point.
|
||||||
type HTTP2Config struct {
|
type HTTP2Config struct {
|
||||||
MaxConcurrentStreams int32 `description:"Specifies the number of concurrent streams per connection that each client is allowed to initiate." json:"maxConcurrentStreams,omitempty" toml:"maxConcurrentStreams,omitempty" yaml:"maxConcurrentStreams,omitempty" export:"true"`
|
MaxConcurrentStreams int32 `description:"Specifies the number of concurrent streams per connection that each client is allowed to initiate." json:"maxConcurrentStreams,omitempty" toml:"maxConcurrentStreams,omitempty" yaml:"maxConcurrentStreams,omitempty" export:"true"`
|
||||||
@@ -131,7 +176,6 @@ type ForwardedHeaders struct {
|
|||||||
Insecure bool `description:"Trust all forwarded headers." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"`
|
Insecure bool `description:"Trust all forwarded headers." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"`
|
||||||
TrustedIPs []string `description:"Trust only forwarded headers from selected IPs." json:"trustedIPs,omitempty" toml:"trustedIPs,omitempty" yaml:"trustedIPs,omitempty"`
|
TrustedIPs []string `description:"Trust only forwarded headers from selected IPs." json:"trustedIPs,omitempty" toml:"trustedIPs,omitempty" yaml:"trustedIPs,omitempty"`
|
||||||
Connection []string `description:"List of Connection headers that are allowed to pass through the middleware chain before being removed." json:"connection,omitempty" toml:"connection,omitempty" yaml:"connection,omitempty"`
|
Connection []string `description:"List of Connection headers that are allowed to pass through the middleware chain before being removed." json:"connection,omitempty" toml:"connection,omitempty" yaml:"connection,omitempty"`
|
||||||
NotAppendXForwardedFor bool `description:"Disable appending RemoteAddr to X-Forwarded-For header. Defaults to false (appending is enabled)." json:"notAppendXForwardedFor,omitempty" toml:"notAppendXForwardedFor,omitempty" yaml:"notAppendXForwardedFor,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProxyProtocol contains Proxy-Protocol configuration.
|
// ProxyProtocol contains Proxy-Protocol configuration.
|
||||||
|
|||||||
@@ -65,3 +65,161 @@ func TestEntryPointProtocol(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEncodedCharactersMap(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config EncodedCharacters
|
||||||
|
expected map[string]struct{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Handles empty configuration",
|
||||||
|
expected: map[string]struct{}{
|
||||||
|
"%2F": {},
|
||||||
|
"%2f": {},
|
||||||
|
"%5C": {},
|
||||||
|
"%5c": {},
|
||||||
|
"%00": {},
|
||||||
|
"%3B": {},
|
||||||
|
"%3b": {},
|
||||||
|
"%25": {},
|
||||||
|
"%3F": {},
|
||||||
|
"%3f": {},
|
||||||
|
"%23": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Exclude encoded slash when allowed",
|
||||||
|
config: EncodedCharacters{
|
||||||
|
AllowEncodedSlash: true,
|
||||||
|
},
|
||||||
|
expected: map[string]struct{}{
|
||||||
|
"%5C": {},
|
||||||
|
"%5c": {},
|
||||||
|
"%00": {},
|
||||||
|
"%3B": {},
|
||||||
|
"%3b": {},
|
||||||
|
"%25": {},
|
||||||
|
"%3F": {},
|
||||||
|
"%3f": {},
|
||||||
|
"%23": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "Exclude encoded backslash when allowed",
|
||||||
|
config: EncodedCharacters{
|
||||||
|
AllowEncodedBackSlash: true,
|
||||||
|
},
|
||||||
|
expected: map[string]struct{}{
|
||||||
|
"%2F": {},
|
||||||
|
"%2f": {},
|
||||||
|
"%00": {},
|
||||||
|
"%3B": {},
|
||||||
|
"%3b": {},
|
||||||
|
"%25": {},
|
||||||
|
"%3F": {},
|
||||||
|
"%3f": {},
|
||||||
|
"%23": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "Exclude encoded null character when allowed",
|
||||||
|
config: EncodedCharacters{
|
||||||
|
AllowEncodedNullCharacter: true,
|
||||||
|
},
|
||||||
|
expected: map[string]struct{}{
|
||||||
|
"%2F": {},
|
||||||
|
"%2f": {},
|
||||||
|
"%5C": {},
|
||||||
|
"%5c": {},
|
||||||
|
"%3B": {},
|
||||||
|
"%3b": {},
|
||||||
|
"%25": {},
|
||||||
|
"%3F": {},
|
||||||
|
"%3f": {},
|
||||||
|
"%23": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Exclude encoded semicolon when allowed",
|
||||||
|
config: EncodedCharacters{
|
||||||
|
AllowEncodedSemicolon: true,
|
||||||
|
},
|
||||||
|
expected: map[string]struct{}{
|
||||||
|
"%2F": {},
|
||||||
|
"%2f": {},
|
||||||
|
"%5C": {},
|
||||||
|
"%5c": {},
|
||||||
|
"%00": {},
|
||||||
|
"%25": {},
|
||||||
|
"%3F": {},
|
||||||
|
"%3f": {},
|
||||||
|
"%23": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Exclude encoded percent when allowed",
|
||||||
|
config: EncodedCharacters{
|
||||||
|
AllowEncodedPercent: true,
|
||||||
|
},
|
||||||
|
expected: map[string]struct{}{
|
||||||
|
"%2F": {},
|
||||||
|
"%2f": {},
|
||||||
|
"%5C": {},
|
||||||
|
"%5c": {},
|
||||||
|
"%00": {},
|
||||||
|
"%3B": {},
|
||||||
|
"%3b": {},
|
||||||
|
"%3F": {},
|
||||||
|
"%3f": {},
|
||||||
|
"%23": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Exclude encoded question mark when allowed",
|
||||||
|
config: EncodedCharacters{
|
||||||
|
AllowEncodedQuestionMark: true,
|
||||||
|
},
|
||||||
|
expected: map[string]struct{}{
|
||||||
|
"%2F": {},
|
||||||
|
"%2f": {},
|
||||||
|
"%5C": {},
|
||||||
|
"%5c": {},
|
||||||
|
"%00": {},
|
||||||
|
"%3B": {},
|
||||||
|
"%3b": {},
|
||||||
|
"%25": {},
|
||||||
|
"%23": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Exclude encoded hash when allowed",
|
||||||
|
config: EncodedCharacters{
|
||||||
|
AllowEncodedHash: true,
|
||||||
|
},
|
||||||
|
expected: map[string]struct{}{
|
||||||
|
"%2F": {},
|
||||||
|
"%2f": {},
|
||||||
|
"%5C": {},
|
||||||
|
"%5c": {},
|
||||||
|
"%00": {},
|
||||||
|
"%3B": {},
|
||||||
|
"%3b": {},
|
||||||
|
"%25": {},
|
||||||
|
"%3F": {},
|
||||||
|
"%3f": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
result := test.config.Map()
|
||||||
|
require.Equal(t, test.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -114,7 +114,6 @@ type CertificateResolver struct {
|
|||||||
type Global struct {
|
type Global struct {
|
||||||
CheckNewVersion bool `description:"Periodically check if a new version has been released." json:"checkNewVersion,omitempty" toml:"checkNewVersion,omitempty" yaml:"checkNewVersion,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
CheckNewVersion bool `description:"Periodically check if a new version has been released." json:"checkNewVersion,omitempty" toml:"checkNewVersion,omitempty" yaml:"checkNewVersion,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||||
SendAnonymousUsage bool `description:"Periodically send anonymous usage statistics. If the option is not specified, it will be disabled by default." json:"sendAnonymousUsage,omitempty" toml:"sendAnonymousUsage,omitempty" yaml:"sendAnonymousUsage,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
SendAnonymousUsage bool `description:"Periodically send anonymous usage statistics. If the option is not specified, it will be disabled by default." json:"sendAnonymousUsage,omitempty" toml:"sendAnonymousUsage,omitempty" yaml:"sendAnonymousUsage,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||||
NotAppendXForwardedFor bool `description:"Disable appending RemoteAddr to X-Forwarded-For header. Defaults to false (appending is enabled)." json:"notAppendXForwardedFor,omitempty" toml:"notAppendXForwardedFor,omitempty" yaml:"notAppendXForwardedFor,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServersTransport options to configure communication between Traefik and the servers.
|
// ServersTransport options to configure communication between Traefik and the servers.
|
||||||
|
|||||||
@@ -481,7 +481,7 @@ func TestServiceTCPHealthChecker_Launch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait for all health checks to complete deterministically
|
// Wait for all health checks to complete deterministically
|
||||||
for range test.server.StatusSequence {
|
for i := range test.server.StatusSequence {
|
||||||
test.server.Next()
|
test.server.Next()
|
||||||
|
|
||||||
initialUpserted := lb.numUpsertedServers
|
initialUpserted := lb.numUpsertedServers
|
||||||
@@ -490,6 +490,11 @@ func TestServiceTCPHealthChecker_Launch(t *testing.T) {
|
|||||||
for time.Now().Before(deadline) {
|
for time.Now().Before(deadline) {
|
||||||
time.Sleep(5 * time.Millisecond)
|
time.Sleep(5 * time.Millisecond)
|
||||||
if lb.numUpsertedServers > initialUpserted || lb.numRemovedServers > initialRemoved {
|
if lb.numUpsertedServers > initialUpserted || lb.numRemovedServers > initialRemoved {
|
||||||
|
// Stop the health checker immediately after the last expected sequence completes
|
||||||
|
// to prevent extra health checks from firing and modifying the counters.
|
||||||
|
if i == len(test.server.StatusSequence)-1 {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
|
|
||||||
forwardResponse, forwardErr := fa.client.Do(forwardReq)
|
forwardResponse, forwardErr := fa.client.Do(forwardReq)
|
||||||
if forwardErr != nil {
|
if forwardErr != nil {
|
||||||
logger.Debug().Err(forwardErr).Msgf("Error calling %s", fa.address)
|
logger.Error().Err(forwardErr).Msgf("Error calling %s", fa.address)
|
||||||
observability.SetStatusErrorf(req.Context(), "Error calling %s. Cause: %s", fa.address, forwardErr)
|
observability.SetStatusErrorf(req.Context(), "Error calling %s. Cause: %s", fa.address, forwardErr)
|
||||||
|
|
||||||
statusCode := http.StatusInternalServerError
|
statusCode := http.StatusInternalServerError
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/traefik/traefik/v3/pkg/ip"
|
"github.com/traefik/traefik/v3/pkg/ip"
|
||||||
"github.com/traefik/traefik/v3/pkg/proxy/httputil"
|
|
||||||
"golang.org/x/net/http/httpguts"
|
"golang.org/x/net/http/httpguts"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,14 +50,13 @@ type XForwarded struct {
|
|||||||
insecure bool
|
insecure bool
|
||||||
trustedIPs []string
|
trustedIPs []string
|
||||||
connectionHeaders []string
|
connectionHeaders []string
|
||||||
notAppendXForwardedFor bool
|
|
||||||
ipChecker *ip.Checker
|
ipChecker *ip.Checker
|
||||||
next http.Handler
|
next http.Handler
|
||||||
hostname string
|
hostname string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewXForwarded creates a new XForwarded.
|
// NewXForwarded creates a new XForwarded.
|
||||||
func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []string, notAppendXForwardedFor bool, next http.Handler) (*XForwarded, error) {
|
func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []string, next http.Handler) (*XForwarded, error) {
|
||||||
var ipChecker *ip.Checker
|
var ipChecker *ip.Checker
|
||||||
if len(trustedIPs) > 0 {
|
if len(trustedIPs) > 0 {
|
||||||
var err error
|
var err error
|
||||||
@@ -77,7 +75,6 @@ func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []strin
|
|||||||
insecure: insecure,
|
insecure: insecure,
|
||||||
trustedIPs: trustedIPs,
|
trustedIPs: trustedIPs,
|
||||||
connectionHeaders: connectionHeaders,
|
connectionHeaders: connectionHeaders,
|
||||||
notAppendXForwardedFor: notAppendXForwardedFor,
|
|
||||||
ipChecker: ipChecker,
|
ipChecker: ipChecker,
|
||||||
next: next,
|
next: next,
|
||||||
hostname: hostname,
|
hostname: hostname,
|
||||||
@@ -201,10 +198,6 @@ func (x *XForwarded) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
x.removeConnectionHeaders(r)
|
x.removeConnectionHeaders(r)
|
||||||
|
|
||||||
if x.notAppendXForwardedFor {
|
|
||||||
r = r.WithContext(httputil.SetNotAppendXFF(r.Context()))
|
|
||||||
}
|
|
||||||
|
|
||||||
x.next.ServeHTTP(w, r)
|
x.next.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -516,7 +516,7 @@ func TestServeHTTP(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := NewXForwarded(test.insecure, test.trustedIps, test.connectionHeaders, false,
|
m, err := NewXForwarded(test.insecure, test.trustedIps, test.connectionHeaders,
|
||||||
http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}))
|
http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -655,7 +655,7 @@ func TestConnection(t *testing.T) {
|
|||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
forwarded, err := NewXForwarded(true, nil, test.connectionHeaders, false, nil)
|
forwarded, err := NewXForwarded(true, nil, test.connectionHeaders, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "https://localhost", nil)
|
req := httptest.NewRequest(http.MethodGet, "https://localhost", nil)
|
||||||
|
|||||||
@@ -22,13 +22,14 @@ type redirect struct {
|
|||||||
regex *regexp.Regexp
|
regex *regexp.Regexp
|
||||||
replacement string
|
replacement string
|
||||||
permanent bool
|
permanent bool
|
||||||
|
forcePermanentRedirect bool
|
||||||
errHandler utils.ErrorHandler
|
errHandler utils.ErrorHandler
|
||||||
name string
|
name string
|
||||||
rawURL func(*http.Request) string
|
rawURL func(*http.Request) string
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a Redirect middleware.
|
// New creates a Redirect middleware.
|
||||||
func newRedirect(next http.Handler, regex, replacement string, permanent bool, rawURL func(*http.Request) string, name string) (http.Handler, error) {
|
func newRedirect(next http.Handler, regex, replacement string, permanent bool, forcePermanentRedirect bool, rawURL func(*http.Request) string, name string) (http.Handler, error) {
|
||||||
re, err := regexp.Compile(regex)
|
re, err := regexp.Compile(regex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -38,6 +39,7 @@ func newRedirect(next http.Handler, regex, replacement string, permanent bool, r
|
|||||||
regex: re,
|
regex: re,
|
||||||
replacement: replacement,
|
replacement: replacement,
|
||||||
permanent: permanent,
|
permanent: permanent,
|
||||||
|
forcePermanentRedirect: forcePermanentRedirect,
|
||||||
errHandler: utils.DefaultHandler,
|
errHandler: utils.DefaultHandler,
|
||||||
next: next,
|
next: next,
|
||||||
name: name,
|
name: name,
|
||||||
@@ -69,7 +71,7 @@ func (r *redirect) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if newURL != oldURL {
|
if newURL != oldURL {
|
||||||
handler := &moveHandler{location: parsedURL, permanent: r.permanent}
|
handler := &moveHandler{location: parsedURL, permanent: r.permanent, forcePermanentRedirect: r.forcePermanentRedirect}
|
||||||
handler.ServeHTTP(rw, req)
|
handler.ServeHTTP(rw, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -84,6 +86,7 @@ func (r *redirect) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
type moveHandler struct {
|
type moveHandler struct {
|
||||||
location *url.URL
|
location *url.URL
|
||||||
permanent bool
|
permanent bool
|
||||||
|
forcePermanentRedirect bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *moveHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
func (m *moveHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
@@ -100,6 +103,11 @@ func (m *moveHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
status = http.StatusPermanentRedirect
|
status = http.StatusPermanentRedirect
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m.forcePermanentRedirect {
|
||||||
|
status = http.StatusPermanentRedirect
|
||||||
|
}
|
||||||
|
|
||||||
rw.WriteHeader(status)
|
rw.WriteHeader(status)
|
||||||
_, err := rw.Write([]byte(http.StatusText(status)))
|
_, err := rw.Write([]byte(http.StatusText(status)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func NewRedirectRegex(ctx context.Context, next http.Handler, conf dynamic.Redir
|
|||||||
logger.Debug().Msg("Creating middleware")
|
logger.Debug().Msg("Creating middleware")
|
||||||
logger.Debug().Msgf("Setting up redirection from %s to %s", conf.Regex, conf.Replacement)
|
logger.Debug().Msgf("Setting up redirection from %s to %s", conf.Regex, conf.Replacement)
|
||||||
|
|
||||||
return newRedirect(next, conf.Regex, conf.Replacement, conf.Permanent, rawURL, name)
|
return newRedirect(next, conf.Regex, conf.Replacement, conf.Permanent, false, rawURL, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func rawURL(req *http.Request) string {
|
func rawURL(req *http.Request) string {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func NewRedirectScheme(ctx context.Context, next http.Handler, conf dynamic.Redi
|
|||||||
|
|
||||||
rs := &redirectScheme{name: name}
|
rs := &redirectScheme{name: name}
|
||||||
|
|
||||||
handler, err := newRedirect(next, uriPattern, conf.Scheme+"://${2}"+port+"${4}", conf.Permanent, rs.clientRequestURL, name)
|
handler, err := newRedirect(next, uriPattern, conf.Scheme+"://${2}"+port+"${4}", conf.Permanent, conf.ForcePermanentRedirect, rs.clientRequestURL, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,6 +165,27 @@ func TestRedirectSchemeHandler(t *testing.T) {
|
|||||||
expectedURL: "https://foo:8443",
|
expectedURL: "https://foo:8443",
|
||||||
expectedStatus: http.StatusMovedPermanently,
|
expectedStatus: http.StatusMovedPermanently,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "HTTP to HTTPS with explicit 308 status code",
|
||||||
|
config: dynamic.RedirectScheme{
|
||||||
|
Scheme: "https",
|
||||||
|
ForcePermanentRedirect: true,
|
||||||
|
},
|
||||||
|
url: "http://foo",
|
||||||
|
expectedURL: "https://foo",
|
||||||
|
expectedStatus: http.StatusPermanentRedirect,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "HTTP to HTTPS with explicit 308 status code for GET request",
|
||||||
|
method: http.MethodGet,
|
||||||
|
config: dynamic.RedirectScheme{
|
||||||
|
Scheme: "https",
|
||||||
|
ForcePermanentRedirect: true,
|
||||||
|
},
|
||||||
|
url: "http://foo",
|
||||||
|
expectedURL: "https://foo",
|
||||||
|
expectedStatus: http.StatusPermanentRedirect,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
desc: "to HTTP 80",
|
desc: "to HTTP 80",
|
||||||
config: dynamic.RedirectScheme{
|
config: dynamic.RedirectScheme{
|
||||||
|
|||||||
@@ -49,8 +49,6 @@ type ingressConfig struct {
|
|||||||
CORSAllowMethods *[]string `annotation:"nginx.ingress.kubernetes.io/cors-allow-methods"`
|
CORSAllowMethods *[]string `annotation:"nginx.ingress.kubernetes.io/cors-allow-methods"`
|
||||||
CORSAllowOrigin *[]string `annotation:"nginx.ingress.kubernetes.io/cors-allow-origin"`
|
CORSAllowOrigin *[]string `annotation:"nginx.ingress.kubernetes.io/cors-allow-origin"`
|
||||||
CORSMaxAge *int `annotation:"nginx.ingress.kubernetes.io/cors-max-age"`
|
CORSMaxAge *int `annotation:"nginx.ingress.kubernetes.io/cors-max-age"`
|
||||||
|
|
||||||
UpstreamVhost *string `annotation:"nginx.ingress.kubernetes.io/upstream-vhost"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseIngressConfig parses the annotations from an Ingress object into an ingressConfig struct.
|
// parseIngressConfig parses the annotations from an Ingress object into an ingressConfig struct.
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: Ingress
|
|
||||||
metadata:
|
|
||||||
name: ingress-with-upstream-vhost
|
|
||||||
namespace: default
|
|
||||||
annotations:
|
|
||||||
nginx.ingress.kubernetes.io/upstream-vhost: "upstream-host-header-value"
|
|
||||||
|
|
||||||
spec:
|
|
||||||
ingressClassName: nginx
|
|
||||||
rules:
|
|
||||||
- host: upstream-vhost.localhost
|
|
||||||
http:
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Exact
|
|
||||||
backend:
|
|
||||||
service:
|
|
||||||
name: whoami
|
|
||||||
port:
|
|
||||||
number: 80
|
|
||||||
@@ -453,7 +453,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: if no service, do not add middlewares and 503.
|
// TODO: if no service, do not add middlewares and 503.
|
||||||
serviceName := provider.Normalize(ingress.Namespace + "-" + pa.Backend.Service.Name + "-" + portString)
|
serviceName := provider.Normalize(ingress.Namespace + "-" + ingress.Name + "-" + pa.Backend.Service.Name + "-" + portString)
|
||||||
|
|
||||||
service, err := p.buildService(ingress.Namespace, pa.Backend, ingressConfig)
|
service, err := p.buildService(ingress.Namespace, pa.Backend, ingressConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -509,7 +509,7 @@ func (p *Provider) buildServersTransport(namespace, name string, cfg ingressConf
|
|||||||
Name: provider.Normalize(namespace + "-" + name),
|
Name: provider.Normalize(namespace + "-" + name),
|
||||||
ServersTransport: &dynamic.ServersTransport{
|
ServersTransport: &dynamic.ServersTransport{
|
||||||
ServerName: ptr.Deref(cfg.ProxySSLName, ptr.Deref(cfg.ProxySSLServerName, "")),
|
ServerName: ptr.Deref(cfg.ProxySSLName, ptr.Deref(cfg.ProxySSLServerName, "")),
|
||||||
InsecureSkipVerify: strings.ToLower(ptr.Deref(cfg.ProxySSLVerify, "off")) == "on",
|
InsecureSkipVerify: strings.ToLower(ptr.Deref(cfg.ProxySSLVerify, "off")) == "off",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -800,8 +800,6 @@ func (p *Provider) applyMiddlewares(namespace, routerKey string, ingressConfig i
|
|||||||
// TODO: check how to remove this, and create the HTTP router elsewhere.
|
// TODO: check how to remove this, and create the HTTP router elsewhere.
|
||||||
applySSLRedirectConfiguration(routerKey, ingressConfig, hasTLS, rt, conf)
|
applySSLRedirectConfiguration(routerKey, ingressConfig, hasTLS, rt, conf)
|
||||||
|
|
||||||
applyUpstreamVhost(routerKey, ingressConfig, rt, conf)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -936,21 +934,6 @@ func applyCORSConfiguration(routerName string, ingressConfig ingressConfig, rt *
|
|||||||
rt.Middlewares = append(rt.Middlewares, corsMiddlewareName)
|
rt.Middlewares = append(rt.Middlewares, corsMiddlewareName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyUpstreamVhost(routerName string, ingressConfig ingressConfig, rt *dynamic.Router, conf *dynamic.Configuration) {
|
|
||||||
if ingressConfig.UpstreamVhost == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
vHostMiddlewareName := routerName + "-vhost"
|
|
||||||
conf.HTTP.Middlewares[vHostMiddlewareName] = &dynamic.Middleware{
|
|
||||||
Headers: &dynamic.Headers{
|
|
||||||
CustomRequestHeaders: map[string]string{"Host": *ingressConfig.UpstreamVhost},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
rt.Middlewares = append(rt.Middlewares, vHostMiddlewareName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func applySSLRedirectConfiguration(routerName string, ingressConfig ingressConfig, hasTLS bool, rt *dynamic.Router, conf *dynamic.Configuration) {
|
func applySSLRedirectConfiguration(routerName string, ingressConfig ingressConfig, hasTLS bool, rt *dynamic.Router, conf *dynamic.Configuration) {
|
||||||
var forceSSLRedirect bool
|
var forceSSLRedirect bool
|
||||||
if ingressConfig.ForceSSLRedirect != nil {
|
if ingressConfig.ForceSSLRedirect != nil {
|
||||||
@@ -959,8 +942,9 @@ func applySSLRedirectConfiguration(routerName string, ingressConfig ingressConfi
|
|||||||
|
|
||||||
sslRedirect := ptr.Deref(ingressConfig.SSLRedirect, hasTLS)
|
sslRedirect := ptr.Deref(ingressConfig.SSLRedirect, hasTLS)
|
||||||
|
|
||||||
if !forceSSLRedirect && !sslRedirect {
|
|
||||||
if hasTLS {
|
if hasTLS {
|
||||||
|
// An Ingress with TLS configuration creates only a Traefik router with a TLS configuration,
|
||||||
|
// so no Non-TLS router exists to handle HTTP traffic, and we should create it.
|
||||||
httpRouter := &dynamic.Router{
|
httpRouter := &dynamic.Router{
|
||||||
Rule: rt.Rule,
|
Rule: rt.Rule,
|
||||||
// "default" stands for the default rule syntax in Traefik v3, i.e. the v3 syntax.
|
// "default" stands for the default rule syntax in Traefik v3, i.e. the v3 syntax.
|
||||||
@@ -968,30 +952,40 @@ func applySSLRedirectConfiguration(routerName string, ingressConfig ingressConfi
|
|||||||
Middlewares: rt.Middlewares,
|
Middlewares: rt.Middlewares,
|
||||||
Service: rt.Service,
|
Service: rt.Service,
|
||||||
}
|
}
|
||||||
|
|
||||||
conf.HTTP.Routers[routerName+"-http"] = httpRouter
|
conf.HTTP.Routers[routerName+"-http"] = httpRouter
|
||||||
|
|
||||||
|
// If either forceSSLRedirect or sslRedirect are enabled,
|
||||||
|
// the HTTP router needs to redirect to HTTPS.
|
||||||
|
if forceSSLRedirect || sslRedirect {
|
||||||
|
redirectMiddlewareName := routerName + "-redirect-scheme"
|
||||||
|
conf.HTTP.Middlewares[redirectMiddlewareName] = &dynamic.Middleware{
|
||||||
|
RedirectScheme: &dynamic.RedirectScheme{
|
||||||
|
Scheme: "https",
|
||||||
|
ForcePermanentRedirect: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
httpRouter.Middlewares = []string{redirectMiddlewareName}
|
||||||
|
httpRouter.Service = "noop@internal"
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectRouter := &dynamic.Router{
|
// An Ingress with no TLS configuration and forceSSLRedirect annotation should always redirect on HTTPS,
|
||||||
Rule: rt.Rule,
|
// even if no route exists for HTTPS.
|
||||||
// "default" stands for the default rule syntax in Traefik v3, i.e. the v3 syntax.
|
if forceSSLRedirect {
|
||||||
RuleSyntax: "default",
|
|
||||||
Service: "noop@internal",
|
|
||||||
}
|
|
||||||
|
|
||||||
redirectMiddlewareName := routerName + "-redirect-scheme"
|
redirectMiddlewareName := routerName + "-redirect-scheme"
|
||||||
conf.HTTP.Middlewares[redirectMiddlewareName] = &dynamic.Middleware{
|
conf.HTTP.Middlewares[redirectMiddlewareName] = &dynamic.Middleware{
|
||||||
RedirectScheme: &dynamic.RedirectScheme{
|
RedirectScheme: &dynamic.RedirectScheme{
|
||||||
Scheme: "https",
|
Scheme: "https",
|
||||||
Permanent: true,
|
ForcePermanentRedirect: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
redirectRouter.Middlewares = append(redirectRouter.Middlewares, redirectMiddlewareName)
|
rt.Middlewares = append([]string{redirectMiddlewareName}, rt.Middlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
conf.HTTP.Routers[routerName+"-redirect"] = redirectRouter
|
// An Ingress that is not forcing sslRedirect and has no TLS configuration does not redirect,
|
||||||
|
// even if sslRedirect is enabled.
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyForwardAuthConfiguration(routerName string, ingressConfig ingressConfig, rt *dynamic.Router, conf *dynamic.Configuration) error {
|
func applyForwardAuthConfiguration(routerName string, ingressConfig ingressConfig, rt *dynamic.Router, conf *dynamic.Configuration) error {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ func TestLoadIngresses(t *testing.T) {
|
|||||||
Rule: "Host(`whoami.localhost`) && Path(`/basicauth`)",
|
Rule: "Host(`whoami.localhost`) && Path(`/basicauth`)",
|
||||||
RuleSyntax: "default",
|
RuleSyntax: "default",
|
||||||
Middlewares: []string{"default-ingress-with-basicauth-rule-0-path-0-basic-auth"},
|
Middlewares: []string{"default-ingress-with-basicauth-rule-0-path-0-basic-auth"},
|
||||||
Service: "default-whoami-80",
|
Service: "default-ingress-with-basicauth-whoami-80",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Middlewares: map[string]*dynamic.Middleware{
|
Middlewares: map[string]*dynamic.Middleware{
|
||||||
@@ -78,7 +78,7 @@ func TestLoadIngresses(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Services: map[string]*dynamic.Service{
|
Services: map[string]*dynamic.Service{
|
||||||
"default-whoami-80": {
|
"default-ingress-with-basicauth-whoami-80": {
|
||||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
Servers: []dynamic.Server{
|
Servers: []dynamic.Server{
|
||||||
{
|
{
|
||||||
@@ -119,7 +119,7 @@ func TestLoadIngresses(t *testing.T) {
|
|||||||
Rule: "Host(`whoami.localhost`) && Path(`/forwardauth`)",
|
Rule: "Host(`whoami.localhost`) && Path(`/forwardauth`)",
|
||||||
RuleSyntax: "default",
|
RuleSyntax: "default",
|
||||||
Middlewares: []string{"default-ingress-with-forwardauth-rule-0-path-0-forward-auth"},
|
Middlewares: []string{"default-ingress-with-forwardauth-rule-0-path-0-forward-auth"},
|
||||||
Service: "default-whoami-80",
|
Service: "default-ingress-with-forwardauth-whoami-80",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Middlewares: map[string]*dynamic.Middleware{
|
Middlewares: map[string]*dynamic.Middleware{
|
||||||
@@ -131,7 +131,7 @@ func TestLoadIngresses(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Services: map[string]*dynamic.Service{
|
Services: map[string]*dynamic.Service{
|
||||||
"default-whoami-80": {
|
"default-ingress-with-forwardauth-whoami-80": {
|
||||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
Servers: []dynamic.Server{
|
Servers: []dynamic.Server{
|
||||||
{
|
{
|
||||||
@@ -173,9 +173,9 @@ func TestLoadIngresses(t *testing.T) {
|
|||||||
Rule: "Host(`sslredirect.localhost`) && Path(`/`)",
|
Rule: "Host(`sslredirect.localhost`) && Path(`/`)",
|
||||||
RuleSyntax: "default",
|
RuleSyntax: "default",
|
||||||
TLS: &dynamic.RouterTLSConfig{},
|
TLS: &dynamic.RouterTLSConfig{},
|
||||||
Service: "default-whoami-80",
|
Service: "default-ingress-with-ssl-redirect-whoami-80",
|
||||||
},
|
},
|
||||||
"default-ingress-with-ssl-redirect-rule-0-path-0-redirect": {
|
"default-ingress-with-ssl-redirect-rule-0-path-0-http": {
|
||||||
Rule: "Host(`sslredirect.localhost`) && Path(`/`)",
|
Rule: "Host(`sslredirect.localhost`) && Path(`/`)",
|
||||||
RuleSyntax: "default",
|
RuleSyntax: "default",
|
||||||
Middlewares: []string{"default-ingress-with-ssl-redirect-rule-0-path-0-redirect-scheme"},
|
Middlewares: []string{"default-ingress-with-ssl-redirect-rule-0-path-0-redirect-scheme"},
|
||||||
@@ -184,42 +184,71 @@ func TestLoadIngresses(t *testing.T) {
|
|||||||
"default-ingress-without-ssl-redirect-rule-0-path-0-http": {
|
"default-ingress-without-ssl-redirect-rule-0-path-0-http": {
|
||||||
Rule: "Host(`withoutsslredirect.localhost`) && Path(`/`)",
|
Rule: "Host(`withoutsslredirect.localhost`) && Path(`/`)",
|
||||||
RuleSyntax: "default",
|
RuleSyntax: "default",
|
||||||
Service: "default-whoami-80",
|
Service: "default-ingress-without-ssl-redirect-whoami-80",
|
||||||
},
|
},
|
||||||
"default-ingress-without-ssl-redirect-rule-0-path-0": {
|
"default-ingress-without-ssl-redirect-rule-0-path-0": {
|
||||||
Rule: "Host(`withoutsslredirect.localhost`) && Path(`/`)",
|
Rule: "Host(`withoutsslredirect.localhost`) && Path(`/`)",
|
||||||
RuleSyntax: "default",
|
RuleSyntax: "default",
|
||||||
TLS: &dynamic.RouterTLSConfig{},
|
TLS: &dynamic.RouterTLSConfig{},
|
||||||
Service: "default-whoami-80",
|
Service: "default-ingress-without-ssl-redirect-whoami-80",
|
||||||
},
|
},
|
||||||
"default-ingress-with-force-ssl-redirect-rule-0-path-0": {
|
"default-ingress-with-force-ssl-redirect-rule-0-path-0": {
|
||||||
Rule: "Host(`forcesslredirect.localhost`) && Path(`/`)",
|
|
||||||
RuleSyntax: "default",
|
|
||||||
Service: "default-whoami-80",
|
|
||||||
},
|
|
||||||
"default-ingress-with-force-ssl-redirect-rule-0-path-0-redirect": {
|
|
||||||
Rule: "Host(`forcesslredirect.localhost`) && Path(`/`)",
|
Rule: "Host(`forcesslredirect.localhost`) && Path(`/`)",
|
||||||
RuleSyntax: "default",
|
RuleSyntax: "default",
|
||||||
Middlewares: []string{"default-ingress-with-force-ssl-redirect-rule-0-path-0-redirect-scheme"},
|
Middlewares: []string{"default-ingress-with-force-ssl-redirect-rule-0-path-0-redirect-scheme"},
|
||||||
Service: "noop@internal",
|
Service: "default-ingress-with-force-ssl-redirect-whoami-80",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Middlewares: map[string]*dynamic.Middleware{
|
Middlewares: map[string]*dynamic.Middleware{
|
||||||
"default-ingress-with-ssl-redirect-rule-0-path-0-redirect-scheme": {
|
"default-ingress-with-ssl-redirect-rule-0-path-0-redirect-scheme": {
|
||||||
RedirectScheme: &dynamic.RedirectScheme{
|
RedirectScheme: &dynamic.RedirectScheme{
|
||||||
Scheme: "https",
|
Scheme: "https",
|
||||||
Permanent: true,
|
ForcePermanentRedirect: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"default-ingress-with-force-ssl-redirect-rule-0-path-0-redirect-scheme": {
|
"default-ingress-with-force-ssl-redirect-rule-0-path-0-redirect-scheme": {
|
||||||
RedirectScheme: &dynamic.RedirectScheme{
|
RedirectScheme: &dynamic.RedirectScheme{
|
||||||
Scheme: "https",
|
Scheme: "https",
|
||||||
Permanent: true,
|
ForcePermanentRedirect: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Services: map[string]*dynamic.Service{
|
Services: map[string]*dynamic.Service{
|
||||||
"default-whoami-80": {
|
"default-ingress-with-ssl-redirect-whoami-80": {
|
||||||
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
|
Servers: []dynamic.Server{
|
||||||
|
{
|
||||||
|
URL: "http://10.10.0.1:80",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "http://10.10.0.2:80",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Strategy: "wrr",
|
||||||
|
PassHostHeader: ptr.To(true),
|
||||||
|
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||||
|
FlushInterval: dynamic.DefaultFlushInterval,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"default-ingress-without-ssl-redirect-whoami-80": {
|
||||||
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
|
Servers: []dynamic.Server{
|
||||||
|
{
|
||||||
|
URL: "http://10.10.0.1:80",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "http://10.10.0.2:80",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Strategy: "wrr",
|
||||||
|
PassHostHeader: ptr.To(true),
|
||||||
|
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||||
|
FlushInterval: dynamic.DefaultFlushInterval,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"default-ingress-with-force-ssl-redirect-whoami-80": {
|
||||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
Servers: []dynamic.Server{
|
Servers: []dynamic.Server{
|
||||||
{
|
{
|
||||||
@@ -313,12 +342,12 @@ func TestLoadIngresses(t *testing.T) {
|
|||||||
"default-ingress-with-sticky-rule-0-path-0": {
|
"default-ingress-with-sticky-rule-0-path-0": {
|
||||||
Rule: "Host(`sticky.localhost`) && Path(`/`)",
|
Rule: "Host(`sticky.localhost`) && Path(`/`)",
|
||||||
RuleSyntax: "default",
|
RuleSyntax: "default",
|
||||||
Service: "default-whoami-80",
|
Service: "default-ingress-with-sticky-whoami-80",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Middlewares: map[string]*dynamic.Middleware{},
|
Middlewares: map[string]*dynamic.Middleware{},
|
||||||
Services: map[string]*dynamic.Service{
|
Services: map[string]*dynamic.Service{
|
||||||
"default-whoami-80": {
|
"default-ingress-with-sticky-whoami-80": {
|
||||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
Servers: []dynamic.Server{
|
Servers: []dynamic.Server{
|
||||||
{
|
{
|
||||||
@@ -370,12 +399,12 @@ func TestLoadIngresses(t *testing.T) {
|
|||||||
"default-ingress-with-proxy-ssl-rule-0-path-0": {
|
"default-ingress-with-proxy-ssl-rule-0-path-0": {
|
||||||
Rule: "Host(`proxy-ssl.localhost`) && Path(`/`)",
|
Rule: "Host(`proxy-ssl.localhost`) && Path(`/`)",
|
||||||
RuleSyntax: "default",
|
RuleSyntax: "default",
|
||||||
Service: "default-whoami-tls-443",
|
Service: "default-ingress-with-proxy-ssl-whoami-tls-443",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Middlewares: map[string]*dynamic.Middleware{},
|
Middlewares: map[string]*dynamic.Middleware{},
|
||||||
Services: map[string]*dynamic.Service{
|
Services: map[string]*dynamic.Service{
|
||||||
"default-whoami-tls-443": {
|
"default-ingress-with-proxy-ssl-whoami-tls-443": {
|
||||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
Servers: []dynamic.Server{
|
Servers: []dynamic.Server{
|
||||||
{
|
{
|
||||||
@@ -397,7 +426,7 @@ func TestLoadIngresses(t *testing.T) {
|
|||||||
ServersTransports: map[string]*dynamic.ServersTransport{
|
ServersTransports: map[string]*dynamic.ServersTransport{
|
||||||
"default-ingress-with-proxy-ssl": {
|
"default-ingress-with-proxy-ssl": {
|
||||||
ServerName: "whoami.localhost",
|
ServerName: "whoami.localhost",
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: false,
|
||||||
RootCAs: []types.FileOrContent{"-----BEGIN CERTIFICATE-----"},
|
RootCAs: []types.FileOrContent{"-----BEGIN CERTIFICATE-----"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -423,7 +452,7 @@ func TestLoadIngresses(t *testing.T) {
|
|||||||
Rule: "Host(`cors.localhost`) && Path(`/`)",
|
Rule: "Host(`cors.localhost`) && Path(`/`)",
|
||||||
RuleSyntax: "default",
|
RuleSyntax: "default",
|
||||||
Middlewares: []string{"default-ingress-with-cors-rule-0-path-0-cors"},
|
Middlewares: []string{"default-ingress-with-cors-rule-0-path-0-cors"},
|
||||||
Service: "default-whoami-80",
|
Service: "default-ingress-with-cors-whoami-80",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Middlewares: map[string]*dynamic.Middleware{
|
Middlewares: map[string]*dynamic.Middleware{
|
||||||
@@ -439,7 +468,7 @@ func TestLoadIngresses(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Services: map[string]*dynamic.Service{
|
Services: map[string]*dynamic.Service{
|
||||||
"default-whoami-80": {
|
"default-ingress-with-cors-whoami-80": {
|
||||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
Servers: []dynamic.Server{
|
Servers: []dynamic.Server{
|
||||||
{
|
{
|
||||||
@@ -479,12 +508,12 @@ func TestLoadIngresses(t *testing.T) {
|
|||||||
"default-ingress-with-service-upstream-rule-0-path-0": {
|
"default-ingress-with-service-upstream-rule-0-path-0": {
|
||||||
Rule: "Host(`service-upstream.localhost`) && Path(`/`)",
|
Rule: "Host(`service-upstream.localhost`) && Path(`/`)",
|
||||||
RuleSyntax: "default",
|
RuleSyntax: "default",
|
||||||
Service: "default-whoami-80",
|
Service: "default-ingress-with-service-upstream-whoami-80",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Middlewares: map[string]*dynamic.Middleware{},
|
Middlewares: map[string]*dynamic.Middleware{},
|
||||||
Services: map[string]*dynamic.Service{
|
Services: map[string]*dynamic.Service{
|
||||||
"default-whoami-80": {
|
"default-ingress-with-service-upstream-whoami-80": {
|
||||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
Servers: []dynamic.Server{
|
Servers: []dynamic.Server{
|
||||||
{
|
{
|
||||||
@@ -504,58 +533,6 @@ func TestLoadIngresses(t *testing.T) {
|
|||||||
TLS: &dynamic.TLSConfiguration{},
|
TLS: &dynamic.TLSConfiguration{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
desc: "Upstream vhost",
|
|
||||||
paths: []string{
|
|
||||||
"services.yml",
|
|
||||||
"ingressclasses.yml",
|
|
||||||
"ingresses/10-ingress-with-upstream-vhost.yml",
|
|
||||||
},
|
|
||||||
expected: &dynamic.Configuration{
|
|
||||||
TCP: &dynamic.TCPConfiguration{
|
|
||||||
Routers: map[string]*dynamic.TCPRouter{},
|
|
||||||
Services: map[string]*dynamic.TCPService{},
|
|
||||||
},
|
|
||||||
HTTP: &dynamic.HTTPConfiguration{
|
|
||||||
Routers: map[string]*dynamic.Router{
|
|
||||||
"default-ingress-with-upstream-vhost-rule-0-path-0": {
|
|
||||||
Rule: "Host(`upstream-vhost.localhost`) && Path(`/`)",
|
|
||||||
RuleSyntax: "default",
|
|
||||||
Middlewares: []string{"default-ingress-with-upstream-vhost-rule-0-path-0-vhost"},
|
|
||||||
Service: "default-whoami-80",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Middlewares: map[string]*dynamic.Middleware{
|
|
||||||
"default-ingress-with-upstream-vhost-rule-0-path-0-vhost": {
|
|
||||||
Headers: &dynamic.Headers{
|
|
||||||
CustomRequestHeaders: map[string]string{"Host": "upstream-host-header-value"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Services: map[string]*dynamic.Service{
|
|
||||||
"default-whoami-80": {
|
|
||||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
|
||||||
Servers: []dynamic.Server{
|
|
||||||
{
|
|
||||||
URL: "http://10.10.0.1:80",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URL: "http://10.10.0.2:80",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Strategy: "wrr",
|
|
||||||
PassHostHeader: ptr.To(true),
|
|
||||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
|
||||||
FlushInterval: dynamic.DefaultFlushInterval,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ServersTransports: map[string]*dynamic.ServersTransport{},
|
|
||||||
},
|
|
||||||
TLS: &dynamic.TLSConfiguration{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
desc: "Default Backend",
|
desc: "Default Backend",
|
||||||
defaultBackendServiceName: "whoami",
|
defaultBackendServiceName: "whoami",
|
||||||
|
|||||||
@@ -212,7 +212,6 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
|
|
||||||
outReq.Header.SetMethod(req.Method)
|
outReq.Header.SetMethod(req.Method)
|
||||||
|
|
||||||
if !proxyhttputil.ShouldNotAppendXFF(req.Context()) {
|
|
||||||
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
|
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
|
||||||
// If we aren't the first proxy retain prior
|
// If we aren't the first proxy retain prior
|
||||||
// X-Forwarded-For information as a comma+space
|
// X-Forwarded-For information as a comma+space
|
||||||
@@ -227,7 +226,6 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
outReq.Header.Set("X-Forwarded-For", clientIP)
|
outReq.Header.Set("X-Forwarded-For", clientIP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.roundTrip(rw, req, outReq, reqUpType); err != nil {
|
if err := p.roundTrip(rw, req, outReq, reqUpType); err != nil {
|
||||||
proxyhttputil.ErrorHandler(rw, req, err)
|
proxyhttputil.ErrorHandler(rw, req, err)
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||||
"github.com/traefik/traefik/v3/pkg/config/static"
|
"github.com/traefik/traefik/v3/pkg/config/static"
|
||||||
proxyhttputil "github.com/traefik/traefik/v3/pkg/proxy/httputil"
|
|
||||||
"github.com/traefik/traefik/v3/pkg/testhelpers"
|
"github.com/traefik/traefik/v3/pkg/testhelpers"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -407,90 +406,6 @@ func TestTransferEncodingChunked(t *testing.T) {
|
|||||||
assert.Equal(t, "chunk 0\nchunk 1\nchunk 2\n", string(body))
|
assert.Equal(t, "chunk 0\nchunk 1\nchunk 2\n", string(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestXForwardedFor(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
desc string
|
|
||||||
notAppendXFF bool
|
|
||||||
incomingXFF string
|
|
||||||
expectedXFF string
|
|
||||||
expectedXFFNotPresent bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
desc: "appends RemoteAddr when notAppendXFF is false",
|
|
||||||
notAppendXFF: false,
|
|
||||||
incomingXFF: "",
|
|
||||||
expectedXFF: "192.0.2.1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "appends RemoteAddr to existing XFF when notAppendXFF is false",
|
|
||||||
notAppendXFF: false,
|
|
||||||
incomingXFF: "203.0.113.1",
|
|
||||||
expectedXFF: "203.0.113.1, 192.0.2.1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "does not append RemoteAddr when notAppendXFF is true and no incoming XFF",
|
|
||||||
notAppendXFF: true,
|
|
||||||
incomingXFF: "",
|
|
||||||
expectedXFFNotPresent: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "preserves existing XFF when notAppendXFF is true",
|
|
||||||
notAppendXFF: true,
|
|
||||||
incomingXFF: "203.0.113.1",
|
|
||||||
expectedXFF: "203.0.113.1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "preserves multiple XFF values when notAppendXFF is true",
|
|
||||||
notAppendXFF: true,
|
|
||||||
incomingXFF: "203.0.113.1, 198.51.100.1",
|
|
||||||
expectedXFF: "203.0.113.1, 198.51.100.1",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range testCases {
|
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
|
||||||
var receivedXFF string
|
|
||||||
var xffPresent bool
|
|
||||||
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
receivedXFF = req.Header.Get("X-Forwarded-For")
|
|
||||||
xffPresent = req.Header.Get("X-Forwarded-For") != "" || len(req.Header["X-Forwarded-For"]) > 0
|
|
||||||
rw.WriteHeader(http.StatusOK)
|
|
||||||
}))
|
|
||||||
t.Cleanup(server.Close)
|
|
||||||
|
|
||||||
builder := NewProxyBuilder(&transportManagerMock{}, static.FastProxyConfig{})
|
|
||||||
|
|
||||||
proxyHandler, err := builder.Build("", testhelpers.MustParseURL(server.URL), true, false)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := t.Context()
|
|
||||||
if test.notAppendXFF {
|
|
||||||
ctx = proxyhttputil.SetNotAppendXFF(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
|
|
||||||
req = req.WithContext(ctx)
|
|
||||||
req.RemoteAddr = "192.0.2.1:12345"
|
|
||||||
|
|
||||||
if test.incomingXFF != "" {
|
|
||||||
req.Header.Set("X-Forwarded-For", test.incomingXFF)
|
|
||||||
}
|
|
||||||
|
|
||||||
res := httptest.NewRecorder()
|
|
||||||
proxyHandler.ServeHTTP(res, req)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, res.Code)
|
|
||||||
|
|
||||||
if test.expectedXFFNotPresent {
|
|
||||||
assert.False(t, xffPresent, "X-Forwarded-For header should not be present")
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, test.expectedXFF, receivedXFF)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type transportManagerMock struct {
|
type transportManagerMock struct {
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,41 +19,17 @@ import (
|
|||||||
"golang.org/x/net/http/httpguts"
|
"golang.org/x/net/http/httpguts"
|
||||||
)
|
)
|
||||||
|
|
||||||
type key string
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// StatusClientClosedRequest non-standard HTTP status code for client disconnection.
|
// StatusClientClosedRequest non-standard HTTP status code for client disconnection.
|
||||||
StatusClientClosedRequest = 499
|
StatusClientClosedRequest = 499
|
||||||
|
|
||||||
// StatusClientClosedRequestText non-standard HTTP status for client disconnection.
|
// StatusClientClosedRequestText non-standard HTTP status for client disconnection.
|
||||||
StatusClientClosedRequestText = "Client Closed Request"
|
StatusClientClosedRequestText = "Client Closed Request"
|
||||||
|
|
||||||
notAppendXFFKey key = "NotAppendXFF"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetNotAppendXFF indicates xff should not be appended.
|
|
||||||
func SetNotAppendXFF(ctx context.Context) context.Context {
|
|
||||||
return context.WithValue(ctx, notAppendXFFKey, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShouldNotAppendXFF returns whether X-Forwarded-For should not be appended.
|
|
||||||
func ShouldNotAppendXFF(ctx context.Context) bool {
|
|
||||||
val := ctx.Value(notAppendXFFKey)
|
|
||||||
if val == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
notAppendXFF, ok := val.(bool)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return notAppendXFF
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildSingleHostProxy(target *url.URL, passHostHeader bool, preservePath bool, flushInterval time.Duration, roundTripper http.RoundTripper, bufferPool httputil.BufferPool) http.Handler {
|
func buildSingleHostProxy(target *url.URL, passHostHeader bool, preservePath bool, flushInterval time.Duration, roundTripper http.RoundTripper, bufferPool httputil.BufferPool) http.Handler {
|
||||||
return &httputil.ReverseProxy{
|
return &httputil.ReverseProxy{
|
||||||
Rewrite: rewriteRequestBuilder(target, passHostHeader, preservePath),
|
Director: directorBuilder(target, passHostHeader, preservePath),
|
||||||
Transport: roundTripper,
|
Transport: roundTripper,
|
||||||
FlushInterval: flushInterval,
|
FlushInterval: flushInterval,
|
||||||
BufferPool: bufferPool,
|
BufferPool: bufferPool,
|
||||||
@@ -62,82 +38,45 @@ func buildSingleHostProxy(target *url.URL, passHostHeader bool, preservePath boo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func rewriteRequestBuilder(target *url.URL, passHostHeader bool, preservePath bool) func(*httputil.ProxyRequest) {
|
func directorBuilder(target *url.URL, passHostHeader bool, preservePath bool) func(req *http.Request) {
|
||||||
return func(pr *httputil.ProxyRequest) {
|
return func(outReq *http.Request) {
|
||||||
copyForwardedHeader(pr.Out.Header, pr.In.Header)
|
outReq.URL.Scheme = target.Scheme
|
||||||
if !ShouldNotAppendXFF(pr.In.Context()) {
|
outReq.URL.Host = target.Host
|
||||||
if clientIP, _, err := net.SplitHostPort(pr.In.RemoteAddr); err == nil {
|
|
||||||
// If we aren't the first proxy retain prior
|
|
||||||
// X-Forwarded-For information as a comma+space
|
|
||||||
// separated list and fold multiple headers into one.
|
|
||||||
prior, ok := pr.Out.Header["X-Forwarded-For"]
|
|
||||||
omit := ok && prior == nil // Issue 38079: nil now means don't populate the header
|
|
||||||
if len(prior) > 0 {
|
|
||||||
clientIP = strings.Join(prior, ", ") + ", " + clientIP
|
|
||||||
}
|
|
||||||
if !omit {
|
|
||||||
pr.Out.Header.Set("X-Forwarded-For", clientIP)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pr.Out.URL.Scheme = target.Scheme
|
u := outReq.URL
|
||||||
pr.Out.URL.Host = target.Host
|
if outReq.RequestURI != "" {
|
||||||
|
parsedURL, err := url.ParseRequestURI(outReq.RequestURI)
|
||||||
u := pr.Out.URL
|
|
||||||
if pr.Out.RequestURI != "" {
|
|
||||||
parsedURL, err := url.ParseRequestURI(pr.Out.RequestURI)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
u = parsedURL
|
u = parsedURL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pr.Out.URL.Path = u.Path
|
outReq.URL.Path = u.Path
|
||||||
pr.Out.URL.RawPath = u.RawPath
|
outReq.URL.RawPath = u.RawPath
|
||||||
|
|
||||||
if preservePath {
|
if preservePath {
|
||||||
pr.Out.URL.Path, pr.Out.URL.RawPath = JoinURLPath(target, u)
|
outReq.URL.Path, outReq.URL.RawPath = JoinURLPath(target, u)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a plugin/middleware adds semicolons in query params, they should be urlEncoded.
|
// If a plugin/middleware adds semicolons in query params, they should be urlEncoded.
|
||||||
pr.Out.URL.RawQuery = strings.ReplaceAll(u.RawQuery, ";", "&")
|
outReq.URL.RawQuery = strings.ReplaceAll(u.RawQuery, ";", "&")
|
||||||
pr.Out.RequestURI = "" // Outgoing request should not have RequestURI
|
outReq.RequestURI = "" // Outgoing request should not have RequestURI
|
||||||
|
|
||||||
pr.Out.Proto = "HTTP/1.1"
|
outReq.Proto = "HTTP/1.1"
|
||||||
pr.Out.ProtoMajor = 1
|
outReq.ProtoMajor = 1
|
||||||
pr.Out.ProtoMinor = 1
|
outReq.ProtoMinor = 1
|
||||||
|
|
||||||
// Do not pass client Host header unless option PassHostHeader is set.
|
// Do not pass client Host header unless option PassHostHeader is set.
|
||||||
if !passHostHeader {
|
if !passHostHeader {
|
||||||
pr.Out.Host = pr.Out.URL.Host
|
outReq.Host = outReq.URL.Host
|
||||||
}
|
}
|
||||||
|
|
||||||
if isWebSocketUpgrade(pr.Out) {
|
if isWebSocketUpgrade(outReq) {
|
||||||
cleanWebSocketHeaders(pr.Out)
|
cleanWebSocketHeaders(outReq)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// copyForwardedHeader copies header that are removed by the reverseProxy when a rewriteRequest is used.
|
|
||||||
func copyForwardedHeader(dst, src http.Header) {
|
|
||||||
prior, ok := src["X-Forwarded-For"]
|
|
||||||
if ok {
|
|
||||||
dst["X-Forwarded-For"] = prior
|
|
||||||
}
|
|
||||||
prior, ok = src["Forwarded"]
|
|
||||||
if ok {
|
|
||||||
dst["Forwarded"] = prior
|
|
||||||
}
|
|
||||||
prior, ok = src["X-Forwarded-Host"]
|
|
||||||
if ok {
|
|
||||||
dst["X-Forwarded-Host"] = prior
|
|
||||||
}
|
|
||||||
prior, ok = src["X-Forwarded-Proto"]
|
|
||||||
if ok {
|
|
||||||
dst["X-Forwarded-Proto"] = prior
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanWebSocketHeaders Even if the websocket RFC says that headers should be case-insensitive,
|
// cleanWebSocketHeaders Even if the websocket RFC says that headers should be case-insensitive,
|
||||||
// some servers need Sec-WebSocket-Key, Sec-WebSocket-Extensions, Sec-WebSocket-Accept,
|
// some servers need Sec-WebSocket-Key, Sec-WebSocket-Extensions, Sec-WebSocket-Accept,
|
||||||
// Sec-WebSocket-Protocol and Sec-WebSocket-Version to be case-sensitive.
|
// Sec-WebSocket-Protocol and Sec-WebSocket-Version to be case-sensitive.
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/http/httputil"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -14,7 +13,7 @@ import (
|
|||||||
"github.com/traefik/traefik/v3/pkg/testhelpers"
|
"github.com/traefik/traefik/v3/pkg/testhelpers"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_rewriteRequestBuilder(t *testing.T) {
|
func Test_directorBuilder(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
target *url.URL
|
target *url.URL
|
||||||
@@ -26,7 +25,6 @@ func Test_rewriteRequestBuilder(t *testing.T) {
|
|||||||
expectedPath string
|
expectedPath string
|
||||||
expectedRawPath string
|
expectedRawPath string
|
||||||
expectedQuery string
|
expectedQuery string
|
||||||
notAppendXFF bool
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Basic proxy",
|
name: "Basic proxy",
|
||||||
@@ -39,18 +37,6 @@ func Test_rewriteRequestBuilder(t *testing.T) {
|
|||||||
expectedPath: "/test",
|
expectedPath: "/test",
|
||||||
expectedQuery: "param=value",
|
expectedQuery: "param=value",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "Basic proxy - notAppendXFF",
|
|
||||||
target: testhelpers.MustParseURL("http://example.com"),
|
|
||||||
passHostHeader: false,
|
|
||||||
preservePath: false,
|
|
||||||
incomingURL: "http://localhost/test?param=value",
|
|
||||||
expectedScheme: "http",
|
|
||||||
expectedHost: "example.com",
|
|
||||||
expectedPath: "/test",
|
|
||||||
expectedQuery: "param=value",
|
|
||||||
notAppendXFF: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "HTTPS target",
|
name: "HTTPS target",
|
||||||
target: testhelpers.MustParseURL("https://secure.example.com"),
|
target: testhelpers.MustParseURL("https://secure.example.com"),
|
||||||
@@ -99,41 +85,21 @@ func Test_rewriteRequestBuilder(t *testing.T) {
|
|||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
rewriteRequest := rewriteRequestBuilder(test.target, test.passHostHeader, test.preservePath)
|
director := directorBuilder(test.target, test.passHostHeader, test.preservePath)
|
||||||
|
|
||||||
ctx := t.Context()
|
req := httptest.NewRequest(http.MethodGet, test.incomingURL, http.NoBody)
|
||||||
if test.notAppendXFF {
|
director(req)
|
||||||
ctx = SetNotAppendXFF(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
reqIn := httptest.NewRequest(http.MethodGet, test.incomingURL, http.NoBody)
|
assert.Equal(t, test.expectedScheme, req.URL.Scheme)
|
||||||
reqIn = reqIn.WithContext(ctx)
|
assert.Equal(t, test.expectedHost, req.Host)
|
||||||
reqIn.Header.Add("X-Forwarded-For", "1.2.3.4")
|
assert.Equal(t, test.expectedPath, req.URL.Path)
|
||||||
reqIn.RemoteAddr = "127.0.0.1:1234"
|
assert.Equal(t, test.expectedRawPath, req.URL.RawPath)
|
||||||
|
assert.Equal(t, test.expectedQuery, req.URL.RawQuery)
|
||||||
reqOut := httptest.NewRequest(http.MethodGet, test.incomingURL, http.NoBody)
|
assert.Empty(t, req.RequestURI)
|
||||||
pr := &httputil.ProxyRequest{
|
assert.Equal(t, "HTTP/1.1", req.Proto)
|
||||||
In: reqIn,
|
assert.Equal(t, 1, req.ProtoMajor)
|
||||||
Out: reqOut,
|
assert.Equal(t, 1, req.ProtoMinor)
|
||||||
}
|
assert.False(t, !test.passHostHeader && req.Host != req.URL.Host)
|
||||||
rewriteRequest(pr)
|
|
||||||
|
|
||||||
if test.notAppendXFF {
|
|
||||||
assert.Equal(t, "1.2.3.4", reqOut.Header.Get("X-Forwarded-For"))
|
|
||||||
} else {
|
|
||||||
// When not disabled, X-Forwarded-For should have RemoteAddr appended
|
|
||||||
assert.Equal(t, "1.2.3.4, 127.0.0.1", reqOut.Header.Get("X-Forwarded-For"))
|
|
||||||
}
|
|
||||||
assert.Equal(t, test.expectedScheme, reqOut.URL.Scheme)
|
|
||||||
assert.Equal(t, test.expectedHost, reqOut.Host)
|
|
||||||
assert.Equal(t, test.expectedPath, reqOut.URL.Path)
|
|
||||||
assert.Equal(t, test.expectedRawPath, reqOut.URL.RawPath)
|
|
||||||
assert.Equal(t, test.expectedQuery, reqOut.URL.RawQuery)
|
|
||||||
assert.Empty(t, reqOut.RequestURI)
|
|
||||||
assert.Equal(t, "HTTP/1.1", reqOut.Proto)
|
|
||||||
assert.Equal(t, 1, reqOut.ProtoMajor)
|
|
||||||
assert.Equal(t, 1, reqOut.ProtoMinor)
|
|
||||||
assert.False(t, !test.passHostHeader && reqOut.Host != reqOut.URL.Host)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,8 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"encodedCharacters": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
62
pkg/server/router/deny.go
Normal file
62
pkg/server/router/deny.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// denyFragment rejects the request if the URL path contains a fragment (hash character).
|
||||||
|
// When go receives an HTTP request, it assumes the absence of fragment URL.
|
||||||
|
// However, it is still possible to send a fragment in the request.
|
||||||
|
// In this case, Traefik will encode the '#' character, altering the request's intended meaning.
|
||||||
|
// To avoid this behavior, the following function rejects requests that include a fragment in the URL.
|
||||||
|
func denyFragment(h http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if strings.Contains(req.URL.RawPath, "#") {
|
||||||
|
log.Debug().Msgf("Rejecting request because it contains a fragment in the URL path: %s", req.URL.RawPath)
|
||||||
|
rw.WriteHeader(http.StatusBadRequest)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.ServeHTTP(rw, req)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// denyEncodedPathCharacters reject the request if the escaped path contains encoded characters in the given list.
|
||||||
|
func denyEncodedPathCharacters(encodedCharacters map[string]struct{}, h http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if len(encodedCharacters) == 0 {
|
||||||
|
h.ServeHTTP(rw, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
escapedPath := req.URL.EscapedPath()
|
||||||
|
|
||||||
|
for i := 0; i < len(escapedPath); i++ {
|
||||||
|
if escapedPath[i] != '%' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should never happen as the standard library will reject requests containing invalid percent-encodings.
|
||||||
|
// This discards URLs with a percent character at the end.
|
||||||
|
if i+2 >= len(escapedPath) {
|
||||||
|
rw.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// This rejects a request with a path containing the given encoded characters.
|
||||||
|
if _, exists := encodedCharacters[escapedPath[i:i+3]]; exists {
|
||||||
|
log.Debug().Msgf("Rejecting request because it contains encoded character %s in the URL path: %s", escapedPath[i:i+3], escapedPath)
|
||||||
|
rw.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 2
|
||||||
|
}
|
||||||
|
|
||||||
|
h.ServeHTTP(rw, req)
|
||||||
|
})
|
||||||
|
}
|
||||||
98
pkg/server/router/deny_test.go
Normal file
98
pkg/server/router/deny_test.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_denyFragment(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
url string
|
||||||
|
wantStatus int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Rejects fragment character",
|
||||||
|
url: "http://example.com/#",
|
||||||
|
wantStatus: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Allows without fragment",
|
||||||
|
url: "http://example.com/",
|
||||||
|
wantStatus: http.StatusOK,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
handler := denyFragment(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, test.url, nil)
|
||||||
|
res := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(res, req)
|
||||||
|
|
||||||
|
assert.Equal(t, test.wantStatus, res.Code)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_denyEncodedPathCharacters(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
encoded map[string]struct{}
|
||||||
|
url string
|
||||||
|
wantStatus int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Rejects disallowed characters",
|
||||||
|
encoded: map[string]struct{}{
|
||||||
|
"%0A": {},
|
||||||
|
"%0D": {},
|
||||||
|
},
|
||||||
|
url: "http://example.com/foo%0Abar",
|
||||||
|
wantStatus: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Allows valid paths",
|
||||||
|
encoded: map[string]struct{}{
|
||||||
|
"%0A": {},
|
||||||
|
"%0D": {},
|
||||||
|
},
|
||||||
|
url: "http://example.com/foo%20bar",
|
||||||
|
wantStatus: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Handles empty path",
|
||||||
|
encoded: map[string]struct{}{
|
||||||
|
"%0A": {},
|
||||||
|
},
|
||||||
|
url: "http://example.com/",
|
||||||
|
wantStatus: http.StatusOK,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
handler := denyEncodedPathCharacters(test.encoded, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, test.url, nil)
|
||||||
|
res := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(res, req)
|
||||||
|
|
||||||
|
assert.Equal(t, test.wantStatus, res.Code)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,10 +46,18 @@ type Manager struct {
|
|||||||
conf *runtime.Configuration
|
conf *runtime.Configuration
|
||||||
tlsManager *tls.Manager
|
tlsManager *tls.Manager
|
||||||
parser httpmuxer.SyntaxParser
|
parser httpmuxer.SyntaxParser
|
||||||
|
deniedEncodedPathCharacters map[string]map[string]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewManager creates a new Manager.
|
// NewManager creates a new Manager.
|
||||||
func NewManager(conf *runtime.Configuration, serviceManager serviceManager, middlewaresBuilder middlewareBuilder, observabilityMgr *middleware.ObservabilityMgr, tlsManager *tls.Manager, parser httpmuxer.SyntaxParser) *Manager {
|
func NewManager(conf *runtime.Configuration,
|
||||||
|
serviceManager serviceManager,
|
||||||
|
middlewaresBuilder middlewareBuilder,
|
||||||
|
observabilityMgr *middleware.ObservabilityMgr,
|
||||||
|
tlsManager *tls.Manager,
|
||||||
|
parser httpmuxer.SyntaxParser,
|
||||||
|
deniedEncodedPathCharacters map[string]map[string]struct{},
|
||||||
|
) *Manager {
|
||||||
return &Manager{
|
return &Manager{
|
||||||
routerHandlers: make(map[string]http.Handler),
|
routerHandlers: make(map[string]http.Handler),
|
||||||
serviceManager: serviceManager,
|
serviceManager: serviceManager,
|
||||||
@@ -58,6 +66,7 @@ func NewManager(conf *runtime.Configuration, serviceManager serviceManager, midd
|
|||||||
conf: conf,
|
conf: conf,
|
||||||
tlsManager: tlsManager,
|
tlsManager: tlsManager,
|
||||||
parser: parser,
|
parser: parser,
|
||||||
|
deniedEncodedPathCharacters: deniedEncodedPathCharacters,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +166,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, entryPointName str
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
handler, err := m.buildRouterHandler(ctxRouter, routerName, routerConfig)
|
handler, err := m.buildRouterHandler(ctxRouter, entryPointName, routerName, routerConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
routerConfig.AddError(err, true)
|
routerConfig.AddError(err, true)
|
||||||
logger.Error().Err(err).Send()
|
logger.Error().Err(err).Send()
|
||||||
@@ -191,7 +200,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, entryPointName str
|
|||||||
return chain.Then(muxer)
|
return chain.Then(muxer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) buildRouterHandler(ctx context.Context, routerName string, routerConfig *runtime.RouterInfo) (http.Handler, error) {
|
func (m *Manager) buildRouterHandler(ctx context.Context, entryPointName, routerName string, routerConfig *runtime.RouterInfo) (http.Handler, error) {
|
||||||
if handler, ok := m.routerHandlers[routerName]; ok {
|
if handler, ok := m.routerHandlers[routerName]; ok {
|
||||||
return handler, nil
|
return handler, nil
|
||||||
}
|
}
|
||||||
@@ -207,16 +216,16 @@ func (m *Manager) buildRouterHandler(ctx context.Context, routerName string, rou
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handler, err := m.buildHTTPHandler(ctx, routerConfig, routerName)
|
handler, err := m.buildHTTPHandler(ctx, routerConfig, entryPointName, routerName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
m.routerHandlers[routerName] = handler
|
m.routerHandlers[routerName] = handler
|
||||||
return m.routerHandlers[routerName], nil
|
return handler, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) buildHTTPHandler(ctx context.Context, router *runtime.RouterInfo, routerName string) (http.Handler, error) {
|
func (m *Manager) buildHTTPHandler(ctx context.Context, router *runtime.RouterInfo, entryPointName, routerName string) (http.Handler, error) {
|
||||||
var qualifiedNames []string
|
var qualifiedNames []string
|
||||||
for _, name := range router.Middlewares {
|
for _, name := range router.Middlewares {
|
||||||
qualifiedNames = append(qualifiedNames, provider.GetQualifiedName(ctx, name))
|
qualifiedNames = append(qualifiedNames, provider.GetQualifiedName(ctx, name))
|
||||||
@@ -239,7 +248,7 @@ func (m *Manager) buildHTTPHandler(ctx context.Context, router *runtime.RouterIn
|
|||||||
switch {
|
switch {
|
||||||
case len(router.ChildRefs) > 0:
|
case len(router.ChildRefs) > 0:
|
||||||
// This router routes to child routers - create a muxer for them
|
// This router routes to child routers - create a muxer for them
|
||||||
nextHandler, err = m.buildChildRoutersMuxer(ctx, router.ChildRefs)
|
nextHandler, err = m.buildChildRoutersMuxer(ctx, entryPointName, router.ChildRefs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("building child routers muxer: %w", err)
|
return nil, fmt.Errorf("building child routers muxer: %w", err)
|
||||||
}
|
}
|
||||||
@@ -266,6 +275,17 @@ func (m *Manager) buildHTTPHandler(ctx context.Context, router *runtime.RouterIn
|
|||||||
return accesslog.NewConcatFieldHandler(next, accesslog.RouterName, routerName), nil
|
return accesslog.NewConcatFieldHandler(next, accesslog.RouterName, routerName), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Here we are adding deny handlers for encoded path characters and fragment.
|
||||||
|
// Deny handler are only added for root routers, child routers are protected by their parent router deny handlers.
|
||||||
|
if len(router.ParentRefs) == 0 {
|
||||||
|
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
|
||||||
|
return denyFragment(next), nil
|
||||||
|
})
|
||||||
|
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
|
||||||
|
return denyEncodedPathCharacters(m.deniedEncodedPathCharacters[entryPointName], next), nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
mHandler := m.middlewaresBuilder.BuildChain(ctx, router.Middlewares)
|
mHandler := m.middlewaresBuilder.BuildChain(ctx, router.Middlewares)
|
||||||
|
|
||||||
return chain.Extend(*mHandler).Then(nextHandler)
|
return chain.Extend(*mHandler).Then(nextHandler)
|
||||||
@@ -441,7 +461,7 @@ func (m *Manager) handleCycle(victimRouter string, path []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildChildRoutersMuxer creates a muxer for child routers.
|
// buildChildRoutersMuxer creates a muxer for child routers.
|
||||||
func (m *Manager) buildChildRoutersMuxer(ctx context.Context, childRefs []string) (http.Handler, error) {
|
func (m *Manager) buildChildRoutersMuxer(ctx context.Context, entryPointName string, childRefs []string) (http.Handler, error) {
|
||||||
childMuxer := httpmuxer.NewMuxer(m.parser)
|
childMuxer := httpmuxer.NewMuxer(m.parser)
|
||||||
|
|
||||||
// Set a default handler for the child muxer (404 Not Found).
|
// Set a default handler for the child muxer (404 Not Found).
|
||||||
@@ -468,7 +488,7 @@ func (m *Manager) buildChildRoutersMuxer(ctx context.Context, childRefs []string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build the child router handler.
|
// Build the child router handler.
|
||||||
childHandler, err := m.buildRouterHandler(ctxChild, childName, childRouter)
|
childHandler, err := m.buildRouterHandler(ctxChild, entryPointName, childName, childRouter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
childRouter.AddError(err, true)
|
childRouter.AddError(err, true)
|
||||||
logger.Error().Err(err).Send()
|
logger.Error().Err(err).Send()
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
ptypes "github.com/traefik/paerser/types"
|
ptypes "github.com/traefik/paerser/types"
|
||||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||||
"github.com/traefik/traefik/v3/pkg/config/runtime"
|
"github.com/traefik/traefik/v3/pkg/config/runtime"
|
||||||
|
"github.com/traefik/traefik/v3/pkg/config/static"
|
||||||
"github.com/traefik/traefik/v3/pkg/middlewares/requestdecorator"
|
"github.com/traefik/traefik/v3/pkg/middlewares/requestdecorator"
|
||||||
httpmuxer "github.com/traefik/traefik/v3/pkg/muxer/http"
|
httpmuxer "github.com/traefik/traefik/v3/pkg/muxer/http"
|
||||||
"github.com/traefik/traefik/v3/pkg/server/middleware"
|
"github.com/traefik/traefik/v3/pkg/server/middleware"
|
||||||
@@ -332,7 +333,7 @@ func TestRouterManager_Get(t *testing.T) {
|
|||||||
parser, err := httpmuxer.NewSyntaxParser()
|
parser, err := httpmuxer.NewSyntaxParser()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser)
|
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser, nil)
|
||||||
|
|
||||||
handlers := routerManager.BuildHandlers(t.Context(), test.entryPoints, false)
|
handlers := routerManager.BuildHandlers(t.Context(), test.entryPoints, false)
|
||||||
|
|
||||||
@@ -720,7 +721,7 @@ func TestRuntimeConfiguration(t *testing.T) {
|
|||||||
parser, err := httpmuxer.NewSyntaxParser()
|
parser, err := httpmuxer.NewSyntaxParser()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser)
|
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser, nil)
|
||||||
|
|
||||||
_ = routerManager.BuildHandlers(t.Context(), entryPoints, false)
|
_ = routerManager.BuildHandlers(t.Context(), entryPoints, false)
|
||||||
_ = routerManager.BuildHandlers(t.Context(), entryPoints, true)
|
_ = routerManager.BuildHandlers(t.Context(), entryPoints, true)
|
||||||
@@ -801,7 +802,7 @@ func TestProviderOnMiddlewares(t *testing.T) {
|
|||||||
parser, err := httpmuxer.NewSyntaxParser()
|
parser, err := httpmuxer.NewSyntaxParser()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser)
|
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser, nil)
|
||||||
|
|
||||||
_ = routerManager.BuildHandlers(t.Context(), entryPoints, false)
|
_ = routerManager.BuildHandlers(t.Context(), entryPoints, false)
|
||||||
|
|
||||||
@@ -811,30 +812,6 @@ func TestProviderOnMiddlewares(t *testing.T) {
|
|||||||
assert.Equal(t, []string{"m1@docker", "m2@docker", "m1@file"}, rtConf.Middlewares["chain@docker"].Chain.Middlewares)
|
assert.Equal(t, []string{"m1@docker", "m2@docker", "m1@file"}, rtConf.Middlewares["chain@docker"].Chain.Middlewares)
|
||||||
}
|
}
|
||||||
|
|
||||||
type staticTransportManager struct {
|
|
||||||
res *http.Response
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s staticTransportManager) GetRoundTripper(_ string) (http.RoundTripper, error) {
|
|
||||||
return &staticTransport{res: s.res}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s staticTransportManager) GetTLSConfig(_ string) (*tls.Config, error) {
|
|
||||||
panic("implement me")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s staticTransportManager) Get(_ string) (*dynamic.ServersTransport, error) {
|
|
||||||
panic("implement me")
|
|
||||||
}
|
|
||||||
|
|
||||||
type staticTransport struct {
|
|
||||||
res *http.Response
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *staticTransport) RoundTrip(_ *http.Request) (*http.Response, error) {
|
|
||||||
return t.res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkRouterServe(b *testing.B) {
|
func BenchmarkRouterServe(b *testing.B) {
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||||
|
|
||||||
@@ -880,7 +857,7 @@ func BenchmarkRouterServe(b *testing.B) {
|
|||||||
parser, err := httpmuxer.NewSyntaxParser()
|
parser, err := httpmuxer.NewSyntaxParser()
|
||||||
require.NoError(b, err)
|
require.NoError(b, err)
|
||||||
|
|
||||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser)
|
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser, nil)
|
||||||
|
|
||||||
handlers := routerManager.BuildHandlers(b.Context(), entryPoints, false)
|
handlers := routerManager.BuildHandlers(b.Context(), entryPoints, false)
|
||||||
|
|
||||||
@@ -1473,14 +1450,14 @@ func TestManager_buildChildRoutersMuxer(t *testing.T) {
|
|||||||
parser, err := httpmuxer.NewSyntaxParser()
|
parser, err := httpmuxer.NewSyntaxParser()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser)
|
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser, nil)
|
||||||
|
|
||||||
// Compute multi-layer routing to populate ChildRefs
|
// Compute multi-layer routing to populate ChildRefs
|
||||||
manager.ParseRouterTree()
|
manager.ParseRouterTree()
|
||||||
|
|
||||||
// Build the child routers muxer
|
// Build the child routers muxer
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
muxer, err := manager.buildChildRoutersMuxer(ctx, test.childRefs)
|
muxer, err := manager.buildChildRoutersMuxer(ctx, "test", test.childRefs)
|
||||||
|
|
||||||
if test.expectedError != "" {
|
if test.expectedError != "" {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -1664,14 +1641,14 @@ func TestManager_buildHTTPHandler_WithChildRouters(t *testing.T) {
|
|||||||
parser, err := httpmuxer.NewSyntaxParser()
|
parser, err := httpmuxer.NewSyntaxParser()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser)
|
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser, nil)
|
||||||
|
|
||||||
// Run ParseRouterTree to validate configuration and populate ChildRefs/errors
|
// Run ParseRouterTree to validate configuration and populate ChildRefs/errors
|
||||||
manager.ParseRouterTree()
|
manager.ParseRouterTree()
|
||||||
|
|
||||||
// Build the HTTP handler
|
// Build the HTTP handler
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
handler, err := manager.buildHTTPHandler(ctx, test.router, "test-router")
|
handler, err := manager.buildHTTPHandler(ctx, test.router, "test", "test-router")
|
||||||
|
|
||||||
if test.expectedError != "" {
|
if test.expectedError != "" {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
@@ -1699,8 +1676,6 @@ func TestManager_BuildHandlers_WithChildRouters(t *testing.T) {
|
|||||||
desc string
|
desc string
|
||||||
routers map[string]*dynamic.Router
|
routers map[string]*dynamic.Router
|
||||||
services map[string]*dynamic.Service
|
services map[string]*dynamic.Service
|
||||||
entryPoints []string
|
|
||||||
expectedEntryPoint string
|
|
||||||
expectedRequests []struct {
|
expectedRequests []struct {
|
||||||
path string
|
path string
|
||||||
statusCode int
|
statusCode int
|
||||||
@@ -1736,8 +1711,6 @@ func TestManager_BuildHandlers_WithChildRouters(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
entryPoints: []string{"web"},
|
|
||||||
expectedEntryPoint: "web",
|
|
||||||
expectedRequests: []struct {
|
expectedRequests: []struct {
|
||||||
path string
|
path string
|
||||||
statusCode int
|
statusCode int
|
||||||
@@ -1779,8 +1752,6 @@ func TestManager_BuildHandlers_WithChildRouters(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
entryPoints: []string{"web"},
|
|
||||||
expectedEntryPoint: "web",
|
|
||||||
expectedRequests: []struct {
|
expectedRequests: []struct {
|
||||||
path string
|
path string
|
||||||
statusCode int
|
statusCode int
|
||||||
@@ -1817,17 +1788,16 @@ func TestManager_BuildHandlers_WithChildRouters(t *testing.T) {
|
|||||||
parser, err := httpmuxer.NewSyntaxParser()
|
parser, err := httpmuxer.NewSyntaxParser()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser)
|
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser, nil)
|
||||||
|
|
||||||
// Compute multi-layer routing to set up parent-child relationships
|
// Compute multi-layer routing to set up parent-child relationships
|
||||||
manager.ParseRouterTree()
|
manager.ParseRouterTree()
|
||||||
|
|
||||||
// Build handlers
|
// Build handlers
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
handlers := manager.BuildHandlers(ctx, test.entryPoints, false)
|
handlers := manager.BuildHandlers(ctx, []string{"web"}, false)
|
||||||
|
|
||||||
require.Contains(t, handlers, test.expectedEntryPoint)
|
handler := handlers["web"]
|
||||||
handler := handlers[test.expectedEntryPoint]
|
|
||||||
require.NotNil(t, handler)
|
require.NotNil(t, handler)
|
||||||
|
|
||||||
// Test that the handler routes requests correctly
|
// Test that the handler routes requests correctly
|
||||||
@@ -1843,8 +1813,225 @@ func TestManager_BuildHandlers_WithChildRouters(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestManager_BuildHandlers_Deny(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
routers map[string]*dynamic.Router
|
||||||
|
services map[string]*dynamic.Service
|
||||||
|
requestPath string
|
||||||
|
encodedCharacters static.EncodedCharacters
|
||||||
|
expectedStatusCode int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "parent router without child routers request with encoded slash",
|
||||||
|
requestPath: "/foo%2F",
|
||||||
|
routers: map[string]*dynamic.Router{
|
||||||
|
"parent": {
|
||||||
|
EntryPoints: []string{"web"},
|
||||||
|
Rule: "PathPrefix(`/`)",
|
||||||
|
Service: "service",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
services: map[string]*dynamic.Service{
|
||||||
|
"service": {
|
||||||
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
|
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "parent router with child routers request with encoded slash",
|
||||||
|
requestPath: "/foo%2F",
|
||||||
|
routers: map[string]*dynamic.Router{
|
||||||
|
"parent": {
|
||||||
|
EntryPoints: []string{"web"},
|
||||||
|
Rule: "PathPrefix(`/`)",
|
||||||
|
},
|
||||||
|
"child1": {
|
||||||
|
Rule: "PathPrefix(`/`)",
|
||||||
|
Service: "child1-service",
|
||||||
|
ParentRefs: []string{"parent"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
services: map[string]*dynamic.Service{
|
||||||
|
"child1-service": {
|
||||||
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
|
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "parent router without child router allowing encoded slash",
|
||||||
|
requestPath: "/foo%2F",
|
||||||
|
routers: map[string]*dynamic.Router{
|
||||||
|
"parent": {
|
||||||
|
EntryPoints: []string{"web"},
|
||||||
|
Rule: "PathPrefix(`/`)",
|
||||||
|
Service: "service",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
services: map[string]*dynamic.Service{
|
||||||
|
"service": {
|
||||||
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
|
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
encodedCharacters: static.EncodedCharacters{
|
||||||
|
AllowEncodedSlash: true,
|
||||||
|
},
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "parent router with child routers allowing encoded slash",
|
||||||
|
requestPath: "/foo%2F",
|
||||||
|
routers: map[string]*dynamic.Router{
|
||||||
|
"parent": {
|
||||||
|
EntryPoints: []string{"web"},
|
||||||
|
Rule: "PathPrefix(`/`)",
|
||||||
|
},
|
||||||
|
"child1": {
|
||||||
|
Rule: "PathPrefix(`/`)",
|
||||||
|
Service: "child1-service",
|
||||||
|
ParentRefs: []string{"parent"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
services: map[string]*dynamic.Service{
|
||||||
|
"child1-service": {
|
||||||
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
|
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
encodedCharacters: static.EncodedCharacters{
|
||||||
|
AllowEncodedSlash: true,
|
||||||
|
},
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "parent router without child routers request with fragment",
|
||||||
|
requestPath: "/foo#",
|
||||||
|
routers: map[string]*dynamic.Router{
|
||||||
|
"parent": {
|
||||||
|
EntryPoints: []string{"web"},
|
||||||
|
Rule: "PathPrefix(`/`)",
|
||||||
|
Service: "service",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
services: map[string]*dynamic.Service{
|
||||||
|
"service": {
|
||||||
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
|
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "parent router with child routers request with fragment",
|
||||||
|
requestPath: "/foo#",
|
||||||
|
routers: map[string]*dynamic.Router{
|
||||||
|
"parent": {
|
||||||
|
EntryPoints: []string{"web"},
|
||||||
|
Rule: "PathPrefix(`/`)",
|
||||||
|
},
|
||||||
|
"child1": {
|
||||||
|
Rule: "Path(`/v1`)",
|
||||||
|
Service: "child1-service",
|
||||||
|
ParentRefs: []string{"parent"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
services: map[string]*dynamic.Service{
|
||||||
|
"child1-service": {
|
||||||
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
|
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
// Create runtime routers
|
||||||
|
runtimeRouters := make(map[string]*runtime.RouterInfo)
|
||||||
|
for name, router := range test.routers {
|
||||||
|
runtimeRouters[name] = &runtime.RouterInfo{
|
||||||
|
Router: router,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create runtime services
|
||||||
|
runtimeServices := make(map[string]*runtime.ServiceInfo)
|
||||||
|
for name, service := range test.services {
|
||||||
|
runtimeServices[name] = &runtime.ServiceInfo{
|
||||||
|
Service: service,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conf := &runtime.Configuration{
|
||||||
|
Routers: runtimeRouters,
|
||||||
|
Services: runtimeServices,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up the manager with mocks
|
||||||
|
serviceManager := &mockServiceManager{}
|
||||||
|
middlewareBuilder := &mockMiddlewareBuilder{}
|
||||||
|
|
||||||
|
parser, err := httpmuxer.NewSyntaxParser()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
deniedEncodedPathCharacters := map[string]map[string]struct{}{"web": test.encodedCharacters.Map()}
|
||||||
|
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser, deniedEncodedPathCharacters)
|
||||||
|
|
||||||
|
// Compute multi-layer routing to set up parent-child relationships
|
||||||
|
manager.ParseRouterTree()
|
||||||
|
|
||||||
|
// Build handlers
|
||||||
|
ctx := t.Context()
|
||||||
|
handlers := manager.BuildHandlers(ctx, []string{"web"}, false)
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
request := httptest.NewRequest(http.MethodGet, test.requestPath, http.NoBody)
|
||||||
|
|
||||||
|
handlers["web"].ServeHTTP(recorder, request)
|
||||||
|
|
||||||
|
assert.Equal(t, test.expectedStatusCode, recorder.Code)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Mock implementations for testing
|
// Mock implementations for testing
|
||||||
|
|
||||||
|
type staticTransportManager struct {
|
||||||
|
res *http.Response
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s staticTransportManager) GetRoundTripper(_ string) (http.RoundTripper, error) {
|
||||||
|
return &staticTransport{res: s.res}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s staticTransportManager) GetTLSConfig(_ string) (*tls.Config, error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s staticTransportManager) Get(_ string) (*dynamic.ServersTransport, error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
type staticTransport struct {
|
||||||
|
res *http.Response
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *staticTransport) RoundTrip(_ *http.Request) (*http.Response, error) {
|
||||||
|
return t.res, nil
|
||||||
|
}
|
||||||
|
|
||||||
type mockServiceManager struct{}
|
type mockServiceManager struct{}
|
||||||
|
|
||||||
func (m *mockServiceManager) BuildHTTP(_ context.Context, _ string) (http.Handler, error) {
|
func (m *mockServiceManager) BuildHTTP(_ context.Context, _ string) (http.Handler, error) {
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ import (
|
|||||||
type RouterFactory struct {
|
type RouterFactory struct {
|
||||||
entryPointsTCP []string
|
entryPointsTCP []string
|
||||||
entryPointsUDP []string
|
entryPointsUDP []string
|
||||||
|
|
||||||
allowACMEByPass map[string]bool
|
allowACMEByPass map[string]bool
|
||||||
|
deniedEncodedPathCharacters map[string]map[string]struct{}
|
||||||
|
|
||||||
managerFactory *service.ManagerFactory
|
managerFactory *service.ManagerFactory
|
||||||
|
|
||||||
@@ -71,6 +73,11 @@ func NewRouterFactory(staticConfiguration static.Configuration, managerFactory *
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deniedEncodedPathCharacters := map[string]map[string]struct{}{}
|
||||||
|
for name, ep := range staticConfiguration.EntryPoints {
|
||||||
|
deniedEncodedPathCharacters[name] = ep.HTTP.EncodedCharacters.Map()
|
||||||
|
}
|
||||||
|
|
||||||
parser, err := httpmuxer.NewSyntaxParser()
|
parser, err := httpmuxer.NewSyntaxParser()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("creating parser: %w", err)
|
return nil, fmt.Errorf("creating parser: %w", err)
|
||||||
@@ -85,6 +92,7 @@ func NewRouterFactory(staticConfiguration static.Configuration, managerFactory *
|
|||||||
pluginBuilder: pluginBuilder,
|
pluginBuilder: pluginBuilder,
|
||||||
dialerManager: dialerManager,
|
dialerManager: dialerManager,
|
||||||
allowACMEByPass: allowACMEByPass,
|
allowACMEByPass: allowACMEByPass,
|
||||||
|
deniedEncodedPathCharacters: deniedEncodedPathCharacters,
|
||||||
parser: parser,
|
parser: parser,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -103,7 +111,7 @@ func (f *RouterFactory) CreateRouters(rtConf *runtime.Configuration) (map[string
|
|||||||
|
|
||||||
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, f.pluginBuilder)
|
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, f.pluginBuilder)
|
||||||
|
|
||||||
routerManager := router.NewManager(rtConf, serviceManager, middlewaresBuilder, f.observabilityMgr, f.tlsManager, f.parser)
|
routerManager := router.NewManager(rtConf, serviceManager, middlewaresBuilder, f.observabilityMgr, f.tlsManager, f.parser, f.deniedEncodedPathCharacters)
|
||||||
|
|
||||||
routerManager.ParseRouterTree()
|
routerManager.ParseRouterTree()
|
||||||
|
|
||||||
|
|||||||
@@ -650,7 +650,6 @@ func newHTTPServer(ctx context.Context, ln net.Listener, configuration *static.E
|
|||||||
configuration.ForwardedHeaders.Insecure,
|
configuration.ForwardedHeaders.Insecure,
|
||||||
configuration.ForwardedHeaders.TrustedIPs,
|
configuration.ForwardedHeaders.TrustedIPs,
|
||||||
configuration.ForwardedHeaders.Connection,
|
configuration.ForwardedHeaders.Connection,
|
||||||
configuration.ForwardedHeaders.NotAppendXForwardedFor,
|
|
||||||
next)
|
next)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -684,8 +683,6 @@ func newHTTPServer(ctx context.Context, ln net.Listener, configuration *static.E
|
|||||||
|
|
||||||
handler = normalizePath(handler)
|
handler = normalizePath(handler)
|
||||||
|
|
||||||
handler = denyFragment(handler)
|
|
||||||
|
|
||||||
serverHTTP := &http.Server{
|
serverHTTP := &http.Server{
|
||||||
Protocols: &protocols,
|
Protocols: &protocols,
|
||||||
Handler: handler,
|
Handler: handler,
|
||||||
@@ -788,23 +785,6 @@ func encodeQuerySemicolons(h http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// When go receives an HTTP request, it assumes the absence of fragment URL.
|
|
||||||
// However, it is still possible to send a fragment in the request.
|
|
||||||
// In this case, Traefik will encode the '#' character, altering the request's intended meaning.
|
|
||||||
// To avoid this behavior, the following function rejects requests that include a fragment in the URL.
|
|
||||||
func denyFragment(h http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
if strings.Contains(req.URL.RawPath, "#") {
|
|
||||||
log.Debug().Msgf("Rejecting request because it contains a fragment in the URL path: %s", req.URL.RawPath)
|
|
||||||
rw.WriteHeader(http.StatusBadRequest)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.ServeHTTP(rw, req)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// sanitizePath removes the "..", "." and duplicate slash segments from the URL according to https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.2.3.
|
// sanitizePath removes the "..", "." and duplicate slash segments from the URL according to https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.2.3.
|
||||||
// It cleans the request URL Path and RawPath, and updates the request URI.
|
// It cleans the request URL Path and RawPath, and updates the request URI.
|
||||||
func sanitizePath(h http.Handler) http.Handler {
|
func sanitizePath(h http.Handler) http.Handler {
|
||||||
|
|||||||
@@ -525,6 +525,10 @@ func TestPathOperations(t *testing.T) {
|
|||||||
configuration := &static.EntryPoint{}
|
configuration := &static.EntryPoint{}
|
||||||
configuration.SetDefaults()
|
configuration.SetDefaults()
|
||||||
|
|
||||||
|
// We need to allow some of the suspicious encoded characters to test the path operations in case they are authorized.
|
||||||
|
configuration.HTTP.EncodedCharacters.AllowEncodedSlash = true
|
||||||
|
configuration.HTTP.EncodedCharacters.AllowEncodedPercent = true
|
||||||
|
|
||||||
// Create the HTTP server using newHTTPServer.
|
// Create the HTTP server using newHTTPServer.
|
||||||
server, err := newHTTPServer(t.Context(), ln, configuration, false, requestdecorator.New(nil))
|
server, err := newHTTPServer(t.Context(), ln, configuration, false, requestdecorator.New(nil))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ RepositoryName = "traefik"
|
|||||||
OutputType = "file"
|
OutputType = "file"
|
||||||
FileName = "traefik_changelog.md"
|
FileName = "traefik_changelog.md"
|
||||||
|
|
||||||
# example new bugfix v3.6.2
|
# example new bugfix v3.6.5
|
||||||
CurrentRef = "v3.6"
|
CurrentRef = "v3.6"
|
||||||
PreviousRef = "v3.6.1"
|
PreviousRef = "v3.6.4"
|
||||||
BaseBranch = "v3.6"
|
BaseBranch = "v3.6"
|
||||||
FutureCurrentRefName = "v3.6.2"
|
FutureCurrentRefName = "v3.6.5"
|
||||||
|
|
||||||
ThresholdPreviousRef = 10
|
ThresholdPreviousRef = 10000
|
||||||
ThresholdCurrentRef = 10
|
ThresholdCurrentRef = 10000
|
||||||
|
|
||||||
Debug = true
|
Debug = true
|
||||||
DisplayLabel = true
|
DisplayLabel = true
|
||||||
|
|||||||
@@ -101,5 +101,5 @@
|
|||||||
"public"
|
"public"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.12.0"
|
"packageManager": "yarn@4.9.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { AriaTr, VariantProps, styled } from '@traefiklabs/faency'
|
import { AriaTr, VariantProps, styled } from '@traefiklabs/faency'
|
||||||
import { ComponentProps, forwardRef, ReactNode } from 'react'
|
import { ComponentProps, forwardRef, ReactNode } from 'react'
|
||||||
|
import { useHref } from 'react-router-dom'
|
||||||
import { useHrefWithReturnTo } from 'hooks/use-href-with-return-to'
|
|
||||||
|
|
||||||
const UnstyledLink = styled('a', {
|
const UnstyledLink = styled('a', {
|
||||||
color: 'inherit',
|
color: 'inherit',
|
||||||
@@ -19,7 +18,7 @@ type ClickableRowProps = ComponentProps<typeof AriaTr> &
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default forwardRef<HTMLTableRowElement | null, ClickableRowProps>(({ children, css, to, ...props }, ref) => {
|
export default forwardRef<HTMLTableRowElement | null, ClickableRowProps>(({ children, css, to, ...props }, ref) => {
|
||||||
const href = useHrefWithReturnTo(to)
|
const href = useHref(to)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AriaTr asChild interactive ref={ref} css={css} {...props}>
|
<AriaTr asChild interactive ref={ref} css={css} {...props}>
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { CSS, Text } from '@traefiklabs/faency'
|
|
||||||
import { useContext } from 'react'
|
|
||||||
|
|
||||||
import CopyButton from 'components/buttons/CopyButton'
|
|
||||||
import { ToastContext } from 'contexts/toasts'
|
|
||||||
|
|
||||||
type CopyableTextProps = {
|
|
||||||
notifyText?: string
|
|
||||||
text: string
|
|
||||||
css?: CSS
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CopyableText({ notifyText, text, css }: CopyableTextProps) {
|
|
||||||
const { addToast } = useContext(ToastContext)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Text
|
|
||||||
css={{
|
|
||||||
flex: '1 1 auto',
|
|
||||||
minWidth: 0,
|
|
||||||
overflow: 'hidden',
|
|
||||||
overflowWrap: 'anywhere',
|
|
||||||
verticalAlign: 'middle',
|
|
||||||
fontSize: 'inherit',
|
|
||||||
...css,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
<CopyButton
|
|
||||||
text={text}
|
|
||||||
onClick={() => {
|
|
||||||
if (notifyText) addToast({ message: notifyText, severity: 'success' })
|
|
||||||
}}
|
|
||||||
css={{ display: 'inline-block', height: 20, verticalAlign: 'middle', ml: '$1' }}
|
|
||||||
iconOnly
|
|
||||||
/>
|
|
||||||
</Text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Card, styled } from '@traefiklabs/faency'
|
|
||||||
|
|
||||||
const ScrollableCard = styled(Card, {
|
|
||||||
width: '100%',
|
|
||||||
maxHeight: 300,
|
|
||||||
overflowY: 'auto',
|
|
||||||
overflowX: 'hidden',
|
|
||||||
scrollbarWidth: 'thin',
|
|
||||||
scrollbarColor: '$colors-primary $colors-01dp',
|
|
||||||
scrollbarGutter: 'stable',
|
|
||||||
})
|
|
||||||
|
|
||||||
export default ScrollableCard
|
|
||||||
@@ -3,7 +3,7 @@ import { AnimatePresence, motion } from 'framer-motion'
|
|||||||
import { ReactNode, useEffect } from 'react'
|
import { ReactNode, useEffect } from 'react'
|
||||||
import { FiX } from 'react-icons/fi'
|
import { FiX } from 'react-icons/fi'
|
||||||
|
|
||||||
import { colorByStatus, iconByStatus } from 'components/resources/Status'
|
import { colorByStatus, iconByStatus, StatusType } from 'components/resources/Status'
|
||||||
|
|
||||||
const CloseButton = styled(Button, {
|
const CloseButton = styled(Button, {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -39,7 +39,7 @@ const toastVariants = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ToastState = {
|
export type ToastState = {
|
||||||
severity: Resource.Status
|
severity: StatusType
|
||||||
message?: string
|
message?: string
|
||||||
isVisible?: boolean
|
isVisible?: boolean
|
||||||
key?: string
|
key?: string
|
||||||
@@ -88,7 +88,7 @@ export const Toast = ({ message, dismiss, severity = 'error', icon, isVisible =
|
|||||||
exit="hidden"
|
exit="hidden"
|
||||||
variants={toastVariants}
|
variants={toastVariants}
|
||||||
>
|
>
|
||||||
<Box css={{ width: '$4', height: '$4', color: 'white' }}>{icon ? icon : propsBySeverity[severity].icon}</Box>
|
<Box css={{ width: '$4', height: '$4' }}>{icon ? icon : propsBySeverity[severity].icon}</Box>
|
||||||
<Text css={{ color: 'white', fontWeight: 600, lineHeight: '$4' }}>{message}</Text>
|
<Text css={{ color: 'white', fontWeight: 600, lineHeight: '$4' }}>{message}</Text>
|
||||||
{!timeout && (
|
{!timeout && (
|
||||||
<CloseButton ghost onClick={dismiss} css={{ px: '$2' }}>
|
<CloseButton ghost onClick={dismiss} css={{ px: '$2' }}>
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
import { Flex, Button, CSS, AccessibleIcon } from '@traefiklabs/faency'
|
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { FiCheck, FiCopy } from 'react-icons/fi'
|
|
||||||
|
|
||||||
type CopyButtonProps = {
|
|
||||||
text: string
|
|
||||||
disabled?: boolean
|
|
||||||
css?: CSS
|
|
||||||
onClick?: () => void
|
|
||||||
iconOnly?: boolean
|
|
||||||
title?: string
|
|
||||||
color?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const CopyButton = ({
|
|
||||||
text,
|
|
||||||
disabled,
|
|
||||||
css,
|
|
||||||
onClick,
|
|
||||||
iconOnly = false,
|
|
||||||
title = 'Copy',
|
|
||||||
color = 'var(--colors-textSubtle)',
|
|
||||||
}: CopyButtonProps) => {
|
|
||||||
const [showConfirmation, setShowConfirmation] = useState(false)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
ghost
|
|
||||||
size="small"
|
|
||||||
css={{
|
|
||||||
color: '$hiContrast',
|
|
||||||
px: iconOnly ? '$1' : undefined,
|
|
||||||
...css,
|
|
||||||
}}
|
|
||||||
title={title}
|
|
||||||
onClick={async (e: React.MouseEvent): Promise<void> => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
await navigator.clipboard.writeText(text)
|
|
||||||
if (onClick) onClick()
|
|
||||||
setShowConfirmation(true)
|
|
||||||
setTimeout(() => setShowConfirmation(false), 1500)
|
|
||||||
}}
|
|
||||||
disabled={disabled}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<Flex align="center" gap={2} css={{ userSelect: 'none' }}>
|
|
||||||
<AccessibleIcon label="copy">
|
|
||||||
{showConfirmation ? <FiCheck color={color} size={14} /> : <FiCopy color={color} size={14} />}
|
|
||||||
</AccessibleIcon>
|
|
||||||
{!iconOnly ? (showConfirmation ? 'Copied!' : title) : null}
|
|
||||||
</Flex>
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CopyButton
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Box } from '@traefiklabs/faency'
|
|
||||||
import { HTMLAttributes, useMemo } from 'react'
|
import { HTMLAttributes, useMemo } from 'react'
|
||||||
|
|
||||||
import Consul from 'components/icons/providers/Consul'
|
import Consul from 'components/icons/providers/Consul'
|
||||||
@@ -15,14 +14,13 @@ import Nomad from 'components/icons/providers/Nomad'
|
|||||||
import Plugin from 'components/icons/providers/Plugin'
|
import Plugin from 'components/icons/providers/Plugin'
|
||||||
import Redis from 'components/icons/providers/Redis'
|
import Redis from 'components/icons/providers/Redis'
|
||||||
import Zookeeper from 'components/icons/providers/Zookeeper'
|
import Zookeeper from 'components/icons/providers/Zookeeper'
|
||||||
import Tooltip from 'components/Tooltip'
|
|
||||||
|
|
||||||
export type ProviderIconProps = HTMLAttributes<SVGElement> & {
|
export type ProviderIconProps = HTMLAttributes<SVGElement> & {
|
||||||
height?: number | string
|
height?: number | string
|
||||||
width?: number | string
|
width?: number | string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProviderIcon({ name, size = 20 }: { name: string; size?: number }) {
|
export default function ProviderIcon({ name, size = 32 }: { name: string; size?: number }) {
|
||||||
const Icon = useMemo(() => {
|
const Icon = useMemo(() => {
|
||||||
if (!name || typeof name !== 'string') return Internal
|
if (!name || typeof name !== 'string') return Internal
|
||||||
|
|
||||||
@@ -78,13 +76,3 @@ export default function ProviderIcon({ name, size = 20 }: { name: string; size?:
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProviderIconWithTooltip = ({ provider, size = 20 }) => {
|
|
||||||
return (
|
|
||||||
<Tooltip label={provider}>
|
|
||||||
<Box css={{ width: size, height: size }}>
|
|
||||||
<ProviderIcon name={provider} size={size} />
|
|
||||||
</Box>
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
import { useMemo } from 'react'
|
|
||||||
|
|
||||||
import ProviderIcon from 'components/icons/providers'
|
|
||||||
import { ProviderName } from 'components/resources/DetailItemComponents'
|
|
||||||
import DetailsCard from 'components/resources/DetailsCard'
|
|
||||||
import { ResourceStatus } from 'components/resources/ResourceStatus'
|
|
||||||
import { parseMiddlewareType } from 'libs/parsers'
|
|
||||||
|
|
||||||
type MiddlewareDefinitionProps = {
|
|
||||||
data: Middleware.Details
|
|
||||||
testId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const MiddlewareDefinition = ({ data, testId }: MiddlewareDefinitionProps) => {
|
|
||||||
const providerName = useMemo(() => {
|
|
||||||
return data.provider
|
|
||||||
}, [data.provider])
|
|
||||||
|
|
||||||
const detailsItems = useMemo(
|
|
||||||
() =>
|
|
||||||
[
|
|
||||||
data.status && { key: 'Status', val: <ResourceStatus status={data.status} withLabel /> },
|
|
||||||
(data.type || data.plugin) && { key: 'Type', val: parseMiddlewareType(data) },
|
|
||||||
data.provider && {
|
|
||||||
key: 'Provider',
|
|
||||||
val: (
|
|
||||||
<>
|
|
||||||
<ProviderIcon name={data.provider} />
|
|
||||||
<ProviderName css={{ ml: '$2' }}>{providerName}</ProviderName>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
].filter(Boolean) as { key: string; val: string | React.ReactElement }[],
|
|
||||||
[data, providerName],
|
|
||||||
)
|
|
||||||
|
|
||||||
return <DetailsCard items={detailsItems} testId={testId} />
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MiddlewareDefinition
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import { Card, Flex, H1, Skeleton, Text } from '@traefiklabs/faency'
|
|
||||||
import { useMemo } from 'react'
|
|
||||||
import { Helmet } from 'react-helmet-async'
|
|
||||||
|
|
||||||
import MiddlewareDefinition from './MiddlewareDefinition'
|
|
||||||
import { RenderUnknownProp } from './RenderUnknownProp'
|
|
||||||
|
|
||||||
import { DetailsCardSkeleton } from 'components/resources/DetailsCard'
|
|
||||||
import ResourceErrors, { ResourceErrorsSkeleton } from 'components/resources/ResourceErrors'
|
|
||||||
import { UsedByRoutersSection, UsedByRoutersSkeleton } from 'components/resources/UsedByRoutersSection'
|
|
||||||
import { NotFound } from 'pages/NotFound'
|
|
||||||
|
|
||||||
type MiddlewareDetailProps = {
|
|
||||||
data?: Resource.DetailsData
|
|
||||||
error?: Error | null
|
|
||||||
name: string
|
|
||||||
protocol: 'http' | 'tcp'
|
|
||||||
}
|
|
||||||
|
|
||||||
const filterMiddlewareProps = (middleware: Middleware.Details): string[] => {
|
|
||||||
const filteredProps = [] as string[]
|
|
||||||
const propsToRemove = ['name', 'plugin', 'status', 'type', 'provider', 'error', 'usedBy', 'routers']
|
|
||||||
|
|
||||||
Object.keys(middleware).map((propName) => {
|
|
||||||
if (!propsToRemove.includes(propName)) {
|
|
||||||
filteredProps.push(propName)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return filteredProps
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MiddlewareDetail = ({ data, error, name, protocol }: MiddlewareDetailProps) => {
|
|
||||||
const filteredProps = useMemo(() => {
|
|
||||||
if (data) {
|
|
||||||
return filterMiddlewareProps(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
return []
|
|
||||||
}, [data])
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Helmet>
|
|
||||||
<title>{name} - Traefik Proxy</title>
|
|
||||||
</Helmet>
|
|
||||||
<Text data-testid="error-text">
|
|
||||||
Sorry, we could not fetch detail information for this Middleware right now. Please, try again later.
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Helmet>
|
|
||||||
<title>{name} - Traefik Proxy</title>
|
|
||||||
</Helmet>
|
|
||||||
<Skeleton css={{ height: '$7', width: '320px', mb: '$7' }} data-testid="skeleton" />
|
|
||||||
<Flex direction="column" gap={6}>
|
|
||||||
<DetailsCardSkeleton />
|
|
||||||
<ResourceErrorsSkeleton />
|
|
||||||
<UsedByRoutersSkeleton />
|
|
||||||
</Flex>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.name) {
|
|
||||||
return <NotFound />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Helmet>
|
|
||||||
<title>{data.name} - Traefik Proxy</title>
|
|
||||||
</Helmet>
|
|
||||||
<H1 css={{ mb: '$7' }}>{data.name}</H1>
|
|
||||||
<Flex direction="column" gap={6}>
|
|
||||||
<MiddlewareDefinition data={data} testId="middleware-card" />
|
|
||||||
{!!data.error && <ResourceErrors errors={data.error} />}
|
|
||||||
{(!!data.plugin || !!filteredProps.length) && (
|
|
||||||
<Card>
|
|
||||||
{data.plugin &&
|
|
||||||
Object.keys(data.plugin).map((pluginName) => (
|
|
||||||
<RenderUnknownProp key={pluginName} name={pluginName} prop={data.plugin?.[pluginName]} />
|
|
||||||
))}
|
|
||||||
{filteredProps?.map((propName) => (
|
|
||||||
<RenderUnknownProp key={propName} name={propName} prop={data[propName]} removeTitlePrefix={data.type} />
|
|
||||||
))}
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<UsedByRoutersSection data-testid="routers-table" data={data} protocol={protocol} />
|
|
||||||
</Flex>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
53
webui/src/components/resources/AdditionalFeatures.spec.tsx
Normal file
53
webui/src/components/resources/AdditionalFeatures.spec.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import AdditionalFeatures from './AdditionalFeatures'
|
||||||
|
|
||||||
|
import { MiddlewareProps } from 'hooks/use-resource-detail'
|
||||||
|
import { renderWithProviders } from 'utils/test'
|
||||||
|
|
||||||
|
describe('<AdditionalFeatures />', () => {
|
||||||
|
it('should render the middleware info', () => {
|
||||||
|
renderWithProviders(<AdditionalFeatures uid="test-key" />)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render the middleware info with number', () => {
|
||||||
|
const middlewares: MiddlewareProps[] = [
|
||||||
|
{
|
||||||
|
retry: {
|
||||||
|
attempts: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const { container } = renderWithProviders(<AdditionalFeatures uid="test-key" middlewares={middlewares} />)
|
||||||
|
|
||||||
|
expect(container.innerHTML).toContain('Retry: Attempts=2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render the middleware info with string', () => {
|
||||||
|
const middlewares: MiddlewareProps[] = [
|
||||||
|
{
|
||||||
|
circuitBreaker: {
|
||||||
|
expression: 'expression',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const { container } = renderWithProviders(<AdditionalFeatures uid="test-key" middlewares={middlewares} />)
|
||||||
|
|
||||||
|
expect(container.innerHTML).toContain('CircuitBreaker: Expression="expression"')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render the middleware info with string', () => {
|
||||||
|
const middlewares: MiddlewareProps[] = [
|
||||||
|
{
|
||||||
|
rateLimit: {
|
||||||
|
burst: 100,
|
||||||
|
average: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const { container } = renderWithProviders(<AdditionalFeatures uid="test-key" middlewares={middlewares} />)
|
||||||
|
|
||||||
|
expect(container.innerHTML).toContain('RateLimit: Burst=100, Average=100')
|
||||||
|
})
|
||||||
|
})
|
||||||
73
webui/src/components/resources/AdditionalFeatures.tsx
Normal file
73
webui/src/components/resources/AdditionalFeatures.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { Badge, Box, Text } from '@traefiklabs/faency'
|
||||||
|
|
||||||
|
import Tooltip from 'components/Tooltip'
|
||||||
|
import { MiddlewareProps, ValuesMapType } from 'hooks/use-resource-detail'
|
||||||
|
|
||||||
|
function capitalize(word: string): string {
|
||||||
|
return word.charAt(0).toUpperCase() + word.slice(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function quote(value: string | number): string | number {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return `"${value}"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
function quoteArray(values: (string | number)[]): (string | number)[] {
|
||||||
|
return values.map(quote)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderFeatureValues = (valuesMap: ValuesMapType): string => {
|
||||||
|
return Object.entries(valuesMap)
|
||||||
|
.map(([name, value]) => {
|
||||||
|
const capitalizedName = capitalize(name)
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return [capitalizedName, `"${value}"`].join('=')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof Array) {
|
||||||
|
return [capitalizedName, quoteArray(value).join(', ')].join('=')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
return [capitalizedName, `{${renderFeatureValues(value)}}`].join('=')
|
||||||
|
}
|
||||||
|
|
||||||
|
return [capitalizedName, value].join('=')
|
||||||
|
})
|
||||||
|
.join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
|
const FeatureMiddleware = ({ middleware }: { middleware: MiddlewareProps }) => {
|
||||||
|
const [name, value] = Object.entries(middleware)[0]
|
||||||
|
const content = `${capitalize(name)}: ${renderFeatureValues(value)}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip label={content} action="copy">
|
||||||
|
<Badge variant="blue" css={{ mr: '$2', mt: '$2' }}>
|
||||||
|
{content}
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdditionalFeaturesProps = {
|
||||||
|
middlewares?: MiddlewareProps[]
|
||||||
|
uid: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const AdditionalFeatures = ({ middlewares, uid }: AdditionalFeaturesProps) => {
|
||||||
|
return middlewares?.length ? (
|
||||||
|
<Box css={{ mt: '-$2' }}>
|
||||||
|
{middlewares.map((m, idx) => (
|
||||||
|
<FeatureMiddleware key={`${uid}-${idx}`} middleware={m} />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Text css={{ fontStyle: 'italic', color: 'hsl(0, 0%, 56%)' }}>No additional features</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdditionalFeatures
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import { Badge, CSS, Flex, styled, Text } from '@traefiklabs/faency'
|
|
||||||
import { ReactNode } from 'react'
|
|
||||||
import { BsToggleOff, BsToggleOn } from 'react-icons/bs'
|
|
||||||
|
|
||||||
import { colorByStatus } from './Status'
|
|
||||||
|
|
||||||
import CopyButton from 'components/buttons/CopyButton'
|
|
||||||
|
|
||||||
export const ItemTitle = styled(Text, {
|
|
||||||
marginBottom: '$3',
|
|
||||||
color: 'hsl(0, 0%, 56%)',
|
|
||||||
fontSize: '12px',
|
|
||||||
fontWeight: 600,
|
|
||||||
textAlign: 'left',
|
|
||||||
textTransform: 'capitalize',
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
})
|
|
||||||
|
|
||||||
const ItemBlockContainer = styled(Flex, {
|
|
||||||
maxWidth: '100%',
|
|
||||||
flexWrap: 'wrap !important',
|
|
||||||
rowGap: '$2',
|
|
||||||
|
|
||||||
// This forces the Tooltips to respect max-width, since we can't define
|
|
||||||
// it directly on the component, and the Chips are automatically covered.
|
|
||||||
span: {
|
|
||||||
maxWidth: '100%',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const FlexLimited = styled(Flex, {
|
|
||||||
maxWidth: '100%',
|
|
||||||
margin: '0 -8px -8px 0',
|
|
||||||
span: {
|
|
||||||
maxWidth: '100%',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
type ChipsType = {
|
|
||||||
items: string[]
|
|
||||||
variant?: 'gray' | 'red' | 'blue' | 'green' | 'neon' | 'orange' | 'purple'
|
|
||||||
alignment?: 'center' | 'left'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Chips = ({ items, variant, alignment = 'left' }: ChipsType) => (
|
|
||||||
<FlexLimited wrap="wrap">
|
|
||||||
{items.map((item, index) => (
|
|
||||||
<Badge key={index} variant={variant} css={{ textAlign: alignment, mr: '$2', mb: '$2' }}>
|
|
||||||
<Flex gap={1} align="center">
|
|
||||||
{item}
|
|
||||||
<CopyButton text={item} iconOnly />
|
|
||||||
</Flex>
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</FlexLimited>
|
|
||||||
)
|
|
||||||
|
|
||||||
type ItemBlockType = {
|
|
||||||
title: string
|
|
||||||
children?: ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ItemBlock = ({ title, children }: ItemBlockType) => (
|
|
||||||
<Flex css={{ flexDirection: 'column', '&:not(:last-child)': { mb: '$5' } }}>
|
|
||||||
<ItemTitle>{title}</ItemTitle>
|
|
||||||
<ItemBlockContainer css={{ alignItems: 'center' }}>{children}</ItemBlockContainer>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const BooleanState = ({ enabled, css }: { enabled: boolean; css?: CSS }) => (
|
|
||||||
<Flex align="center" gap={2} css={{ color: '$textDefault', ...css }}>
|
|
||||||
{enabled ? (
|
|
||||||
<BsToggleOn color={colorByStatus.enabled} size={24} data-testid={`enabled-true`} />
|
|
||||||
) : (
|
|
||||||
<BsToggleOff color="inherit" size={24} data-testid={`enabled-false`} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Text css={{ color: enabled ? colorByStatus.enabled : 'inherit', fontWeight: 600, fontSize: 'inherit' }}>
|
|
||||||
{enabled ? 'True' : 'False'}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const ProviderName = styled(Text, {
|
|
||||||
textTransform: 'capitalize',
|
|
||||||
overflowWrap: 'break-word',
|
|
||||||
fontSize: 'inherit !important',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const EmptyPlaceholder = styled(Text, {
|
|
||||||
color: 'hsl(0, 0%, 76%)',
|
|
||||||
fontSize: '20px',
|
|
||||||
fontWeight: '700',
|
|
||||||
lineHeight: '1.2',
|
|
||||||
})
|
|
||||||
352
webui/src/components/resources/DetailSections.tsx
Normal file
352
webui/src/components/resources/DetailSections.tsx
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
import { Badge, Box, Card, Flex, H2, styled, Text } from '@traefiklabs/faency'
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
import { FiArrowRight, FiToggleLeft, FiToggleRight } from 'react-icons/fi'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { StatusWrapper } from './ResourceStatus'
|
||||||
|
import { colorByStatus } from './Status'
|
||||||
|
|
||||||
|
import Tooltip from 'components/Tooltip'
|
||||||
|
|
||||||
|
const CustomHeading = styled(H2, {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
})
|
||||||
|
|
||||||
|
type SectionHeaderType = {
|
||||||
|
icon?: ReactNode
|
||||||
|
title?: string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SectionHeader = ({ icon, title }: SectionHeaderType) => {
|
||||||
|
if (!title) {
|
||||||
|
return (
|
||||||
|
<CustomHeading css={{ mb: '$6' }}>
|
||||||
|
<Box css={{ width: 5, height: 4, bg: 'hsl(220, 6%, 90%)', borderRadius: 1 }} />
|
||||||
|
<Box css={{ width: '50%', maxWidth: '300px', height: 4, bg: 'hsl(220, 6%, 90%)', borderRadius: 1, ml: '$2' }} />
|
||||||
|
</CustomHeading>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CustomHeading css={{ mb: '$5' }}>
|
||||||
|
{icon ? icon : null}
|
||||||
|
<Text size={6} css={{ ml: '$2' }}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
</CustomHeading>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ItemTitle = styled(Text, {
|
||||||
|
marginBottom: '$3',
|
||||||
|
color: 'hsl(0, 0%, 56%)',
|
||||||
|
letterSpacing: '3px',
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: 600,
|
||||||
|
textAlign: 'left',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
})
|
||||||
|
|
||||||
|
const SpacedCard = styled(Card, {
|
||||||
|
'& + &': {
|
||||||
|
marginTop: '16px',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const CardDescription = styled(Text, {
|
||||||
|
textAlign: 'left',
|
||||||
|
fontWeight: '700',
|
||||||
|
fontSize: '16px',
|
||||||
|
lineHeight: '16px',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
})
|
||||||
|
|
||||||
|
const CardListColumnWrapper = styled(Flex, {
|
||||||
|
display: 'flex',
|
||||||
|
})
|
||||||
|
|
||||||
|
const CardListColumn = styled(Flex, {
|
||||||
|
minWidth: 160,
|
||||||
|
maxWidth: '66%',
|
||||||
|
maxHeight: '416px',
|
||||||
|
overflowY: 'auto',
|
||||||
|
p: '$1',
|
||||||
|
})
|
||||||
|
|
||||||
|
const ItemBlockContainer = styled(Flex, {
|
||||||
|
maxWidth: '100%',
|
||||||
|
flexWrap: 'wrap !important',
|
||||||
|
rowGap: '$2',
|
||||||
|
|
||||||
|
// This forces the Tooltips to respect max-width, since we can't define
|
||||||
|
// it directly on the component, and the Chips are automatically covered.
|
||||||
|
span: {
|
||||||
|
maxWidth: '100%',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const FlexLink = styled('a', {
|
||||||
|
display: 'flex',
|
||||||
|
flexFlow: 'column',
|
||||||
|
textDecoration: 'none',
|
||||||
|
})
|
||||||
|
|
||||||
|
type CardType = {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
focus?: boolean
|
||||||
|
link?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SectionType = SectionHeaderType & {
|
||||||
|
cards?: CardType[] | undefined
|
||||||
|
isLast?: boolean
|
||||||
|
bigDescription?: boolean
|
||||||
|
}
|
||||||
|
const CardSkeleton = ({ bigDescription }: { bigDescription?: boolean }) => {
|
||||||
|
return (
|
||||||
|
<SpacedCard css={{ p: '$3' }}>
|
||||||
|
<ItemTitle>
|
||||||
|
<Box css={{ height: '12px', bg: '$slate5', borderRadius: 1, mb: '$3', mr: '60%' }} />
|
||||||
|
</ItemTitle>
|
||||||
|
<CardDescription>
|
||||||
|
<Box
|
||||||
|
css={{
|
||||||
|
height: bigDescription ? '22px' : '14px',
|
||||||
|
mr: '20%',
|
||||||
|
bg: '$slate5',
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CardDescription>
|
||||||
|
</SpacedCard>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CardListSection = ({ icon, title, cards, isLast, bigDescription }: SectionType) => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex css={{ flexDirection: 'column', flexGrow: 1 }}>
|
||||||
|
<SectionHeader icon={icon} title={title} />
|
||||||
|
<CardListColumnWrapper>
|
||||||
|
<CardListColumn>
|
||||||
|
<Flex css={{ flexDirection: 'column', flexGrow: 1, marginRight: '$3' }}>
|
||||||
|
{!cards && <CardSkeleton bigDescription={bigDescription} />}
|
||||||
|
{cards
|
||||||
|
?.filter((c) => !!c.description)
|
||||||
|
.map((card) => (
|
||||||
|
<SpacedCard key={card.description} css={{ border: card.focus ? `2px solid $primary` : '', p: '$3' }}>
|
||||||
|
<FlexLink
|
||||||
|
data-testid={card.link}
|
||||||
|
onClick={(): false | void => !!card.link && navigate(card.link)}
|
||||||
|
css={{ cursor: card.link ? 'pointer' : 'inherit' }}
|
||||||
|
>
|
||||||
|
<ItemTitle>{card.title}</ItemTitle>
|
||||||
|
<CardDescription>{card.description}</CardDescription>
|
||||||
|
</FlexLink>
|
||||||
|
</SpacedCard>
|
||||||
|
))}
|
||||||
|
<Box css={{ height: '16px' }}> </Box>
|
||||||
|
</Flex>
|
||||||
|
</CardListColumn>
|
||||||
|
{!isLast && (
|
||||||
|
<Flex css={{ mt: '$5', mx: 'auto' }}>
|
||||||
|
<FiArrowRight color="hsl(0, 0%, 76%)" size={24} />
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</CardListColumnWrapper>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const FlexCard = styled(Card, {
|
||||||
|
display: 'flex',
|
||||||
|
flexFlow: 'column',
|
||||||
|
flexGrow: '1',
|
||||||
|
overflowY: 'auto',
|
||||||
|
height: '600px',
|
||||||
|
})
|
||||||
|
|
||||||
|
const NarrowFlexCard = styled(FlexCard, {
|
||||||
|
height: '400px',
|
||||||
|
})
|
||||||
|
|
||||||
|
const ItemTitleSkeleton = styled(Box, {
|
||||||
|
height: '16px',
|
||||||
|
backgroundColor: '$slate5',
|
||||||
|
borderRadius: '3px',
|
||||||
|
})
|
||||||
|
|
||||||
|
const ItemDescriptionSkeleton = styled(Box, {
|
||||||
|
height: '16px',
|
||||||
|
backgroundColor: '$slate5',
|
||||||
|
borderRadius: '3px',
|
||||||
|
})
|
||||||
|
|
||||||
|
type DetailSectionSkeletonType = {
|
||||||
|
narrow?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DetailSectionSkeleton = ({ narrow }: DetailSectionSkeletonType) => {
|
||||||
|
const Card = narrow ? NarrowFlexCard : FlexCard
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex css={{ flexDirection: 'column' }}>
|
||||||
|
<SectionHeader />
|
||||||
|
<Card css={{ p: '$5' }}>
|
||||||
|
<LayoutTwoCols css={{ mb: '$2' }}>
|
||||||
|
<ItemTitleSkeleton css={{ width: '40%' }} />
|
||||||
|
<ItemTitleSkeleton css={{ width: '40%' }} />
|
||||||
|
</LayoutTwoCols>
|
||||||
|
<LayoutTwoCols css={{ mb: '$5' }}>
|
||||||
|
<ItemDescriptionSkeleton css={{ width: '90%' }} />
|
||||||
|
<ItemDescriptionSkeleton css={{ width: '90%' }} />
|
||||||
|
</LayoutTwoCols>
|
||||||
|
<Flex css={{ mb: '$2' }}>
|
||||||
|
<ItemTitleSkeleton css={{ width: '30%' }} />
|
||||||
|
</Flex>
|
||||||
|
<Flex css={{ mb: '$5' }}>
|
||||||
|
<ItemDescriptionSkeleton css={{ width: '50%' }} />
|
||||||
|
</Flex>
|
||||||
|
<Flex css={{ mb: '$2' }}>
|
||||||
|
<ItemTitleSkeleton css={{ width: '30%' }} />
|
||||||
|
</Flex>
|
||||||
|
<Flex css={{ mb: '$5' }}>
|
||||||
|
<ItemDescriptionSkeleton css={{ width: '70%' }} />
|
||||||
|
</Flex>
|
||||||
|
<Flex css={{ mb: '$2' }}>
|
||||||
|
<ItemTitleSkeleton css={{ width: '30%' }} />
|
||||||
|
</Flex>
|
||||||
|
<Flex css={{ mb: '$5' }}>
|
||||||
|
<ItemDescriptionSkeleton css={{ width: '50%' }} />
|
||||||
|
</Flex>
|
||||||
|
<LayoutTwoCols css={{ mb: '$2' }}>
|
||||||
|
<ItemTitleSkeleton css={{ width: '40%' }} />
|
||||||
|
<ItemTitleSkeleton css={{ width: '40%' }} />
|
||||||
|
</LayoutTwoCols>
|
||||||
|
<LayoutTwoCols css={{ mb: '$5' }}>
|
||||||
|
<ItemDescriptionSkeleton css={{ width: '90%' }} />
|
||||||
|
<ItemDescriptionSkeleton css={{ width: '90%' }} />
|
||||||
|
</LayoutTwoCols>
|
||||||
|
</Card>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DetailSectionType = SectionHeaderType & {
|
||||||
|
children?: ReactNode
|
||||||
|
noPadding?: boolean
|
||||||
|
narrow?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DetailSection = ({ icon, title, children, narrow, noPadding }: DetailSectionType) => {
|
||||||
|
const Card = narrow ? NarrowFlexCard : FlexCard
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex css={{ flexDirection: 'column' }}>
|
||||||
|
<SectionHeader icon={icon} title={title} />
|
||||||
|
<Card css={{ padding: noPadding ? 0 : '$5' }}>{children}</Card>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const FlexLimited = styled(Flex, {
|
||||||
|
maxWidth: '100%',
|
||||||
|
margin: '0 -8px -8px 0',
|
||||||
|
span: {
|
||||||
|
maxWidth: '100%',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
type ChipsType = {
|
||||||
|
items: string[]
|
||||||
|
variant?: 'gray' | 'red' | 'blue' | 'green' | 'neon' | 'orange' | 'purple'
|
||||||
|
alignment?: 'center' | 'left'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Chips = ({ items, variant, alignment = 'left' }: ChipsType) => (
|
||||||
|
<FlexLimited wrap="wrap">
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<Tooltip key={index} label={item} action="copy">
|
||||||
|
<Badge variant={variant} css={{ textAlign: alignment, mr: '$2', mb: '$2' }}>
|
||||||
|
{item}
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</FlexLimited>
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChipPropsListType = {
|
||||||
|
data: {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
variant?: 'gray' | 'red' | 'blue' | 'green' | 'neon' | 'orange' | 'purple'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChipPropsList = ({ data, variant }: ChipPropsListType) => (
|
||||||
|
<Flex css={{ flexWrap: 'wrap' }}>
|
||||||
|
{Object.entries(data).map((entry: [string, string]) => (
|
||||||
|
<Badge key={entry[0]} variant={variant} css={{ textAlign: 'left', mr: '$2', mb: '$2' }}>
|
||||||
|
{entry[1]}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
|
||||||
|
type ItemBlockType = {
|
||||||
|
title: string
|
||||||
|
children?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ItemBlock = ({ title, children }: ItemBlockType) => (
|
||||||
|
<Flex css={{ flexDirection: 'column', mb: '$5' }}>
|
||||||
|
<ItemTitle>{title}</ItemTitle>
|
||||||
|
<ItemBlockContainer css={{ alignItems: 'center' }}>{children}</ItemBlockContainer>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
|
||||||
|
const LayoutCols = styled(Box, {
|
||||||
|
display: 'grid',
|
||||||
|
gridGap: '16px',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const LayoutTwoCols = styled(LayoutCols, {
|
||||||
|
gridTemplateColumns: 'repeat(2, minmax(50%, 1fr))',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const LayoutThreeCols = styled(LayoutCols, {
|
||||||
|
gridTemplateColumns: 'repeat(3, minmax(30%, 1fr))',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const BooleanState = ({ enabled }: { enabled: boolean }) => (
|
||||||
|
<Flex align="center" gap={2}>
|
||||||
|
<StatusWrapper
|
||||||
|
css={{
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: enabled ? colorByStatus.enabled : colorByStatus.disabled,
|
||||||
|
}}
|
||||||
|
data-testid={`enabled-${enabled}`}
|
||||||
|
>
|
||||||
|
{enabled ? <FiToggleRight color="#fff" size={20} /> : <FiToggleLeft color="#fff" size={20} />}
|
||||||
|
</StatusWrapper>
|
||||||
|
<Text css={{ color: enabled ? colorByStatus.enabled : colorByStatus.disabled, fontWeight: 600 }}>
|
||||||
|
{enabled ? 'True' : 'False'}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const ProviderName = styled(Text, {
|
||||||
|
textTransform: 'capitalize',
|
||||||
|
overflowWrap: 'break-word',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const EmptyPlaceholder = styled(Text, {
|
||||||
|
color: 'hsl(0, 0%, 76%)',
|
||||||
|
fontSize: '20px',
|
||||||
|
fontWeight: '700',
|
||||||
|
lineHeight: '1.2',
|
||||||
|
})
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
import { Card, CSS, Flex, Grid, H2, Skeleton, styled, Text } from '@traefiklabs/faency'
|
|
||||||
import { Fragment, ReactNode, useMemo } from 'react'
|
|
||||||
|
|
||||||
import ScrollableCard from 'components/ScrollableCard'
|
|
||||||
import breakpoints from 'utils/breakpoints'
|
|
||||||
|
|
||||||
const StyledText = styled(Text, {
|
|
||||||
fontSize: 'inherit !important',
|
|
||||||
lineHeight: '24px',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const ValText = styled(StyledText, {
|
|
||||||
overflowWrap: 'break-word',
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const SectionTitle = ({ icon, title }: { icon?: ReactNode; title: string }) => {
|
|
||||||
return (
|
|
||||||
<Flex gap={2} align="center" css={{ color: '$headingDefault' }}>
|
|
||||||
{icon && icon}
|
|
||||||
<H2 css={{ fontSize: '$5' }}>{title}</H2>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type DetailsCardProps = {
|
|
||||||
css?: CSS
|
|
||||||
keyColumns?: number
|
|
||||||
items: { key: string; val: string | React.ReactElement; stackVertical?: boolean; forceNewRow?: boolean }[]
|
|
||||||
minKeyWidth?: string
|
|
||||||
maxKeyWidth?: string
|
|
||||||
testidPrefix?: string
|
|
||||||
testId?: string
|
|
||||||
title?: string
|
|
||||||
icon?: ReactNode
|
|
||||||
scrollable?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DetailsCard({
|
|
||||||
css = {},
|
|
||||||
keyColumns = 2,
|
|
||||||
items,
|
|
||||||
minKeyWidth,
|
|
||||||
maxKeyWidth,
|
|
||||||
testidPrefix = 'definition',
|
|
||||||
testId,
|
|
||||||
title,
|
|
||||||
icon,
|
|
||||||
scrollable = false,
|
|
||||||
}: DetailsCardProps) {
|
|
||||||
const ParentComponent = useMemo(() => {
|
|
||||||
if (scrollable) {
|
|
||||||
return ScrollableCard
|
|
||||||
}
|
|
||||||
return Card
|
|
||||||
}, [scrollable])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex as="section" direction="column" gap={2} css={{ ...css }} data-testid={testId || `${testidPrefix}-section`}>
|
|
||||||
{title ? <SectionTitle icon={icon} title={title} /> : null}
|
|
||||||
<ParentComponent css={{ flex: 1 }}>
|
|
||||||
<Grid
|
|
||||||
css={{
|
|
||||||
gap: '$2 $3',
|
|
||||||
gridTemplateColumns: maxKeyWidth
|
|
||||||
? `repeat(${keyColumns}, minmax(auto, ${maxKeyWidth}) 1fr)`
|
|
||||||
: `repeat(${keyColumns}, auto 1fr)`,
|
|
||||||
[`@media (max-width:${breakpoints.laptop}px)`]: {
|
|
||||||
gridTemplateColumns: maxKeyWidth ? `minmax(auto, ${maxKeyWidth}) 1fr` : 'auto 1fr',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{items.map((item, index) => {
|
|
||||||
// Handle forceNewRow props
|
|
||||||
const cellsBeforeThis = items.slice(0, index).reduce((count, prevItem) => {
|
|
||||||
if (prevItem.stackVertical) return count + keyColumns
|
|
||||||
return count + 1
|
|
||||||
}, 0)
|
|
||||||
|
|
||||||
const needsEmptyCell = item.forceNewRow && cellsBeforeThis % keyColumns !== 0
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment key={index}>
|
|
||||||
{needsEmptyCell && (
|
|
||||||
<>
|
|
||||||
<div />
|
|
||||||
<div />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{item.stackVertical ? (
|
|
||||||
<Flex direction="column" gap={2} css={{ gridColumn: 'span 2' }}>
|
|
||||||
<StyledText
|
|
||||||
css={{
|
|
||||||
fontWeight: 600,
|
|
||||||
minWidth: minKeyWidth,
|
|
||||||
maxWidth: maxKeyWidth,
|
|
||||||
overflowWrap: 'break-word',
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.key}
|
|
||||||
</StyledText>
|
|
||||||
{typeof item.val === 'string' ? (
|
|
||||||
<ValText>{item.val}</ValText>
|
|
||||||
) : (
|
|
||||||
<Flex
|
|
||||||
css={{
|
|
||||||
'> *': {
|
|
||||||
height: 'fit-content',
|
|
||||||
},
|
|
||||||
height: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.val}
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Grid>
|
|
||||||
{index < keyColumns
|
|
||||||
? items
|
|
||||||
.filter((hiddenItem) => hiddenItem.key != item.key)
|
|
||||||
.map((hiddenItem, jndex) => (
|
|
||||||
<StyledText
|
|
||||||
key={`hidden-${index}-${jndex}`}
|
|
||||||
aria-hidden="true"
|
|
||||||
css={{
|
|
||||||
gridArea: '1 / 1',
|
|
||||||
fontWeight: 600,
|
|
||||||
visibility: 'hidden',
|
|
||||||
maxWidth: maxKeyWidth,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{hiddenItem.key}
|
|
||||||
</StyledText>
|
|
||||||
))
|
|
||||||
: null}
|
|
||||||
<StyledText
|
|
||||||
css={{
|
|
||||||
gridArea: '1 / 1',
|
|
||||||
fontWeight: 600,
|
|
||||||
minWidth: minKeyWidth,
|
|
||||||
maxWidth: maxKeyWidth,
|
|
||||||
overflowWrap: 'break-word',
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.key}
|
|
||||||
</StyledText>
|
|
||||||
</Grid>
|
|
||||||
{typeof item.val === 'string' ? (
|
|
||||||
<ValText css={{ flex: 1 }}>{item.val}</ValText>
|
|
||||||
) : (
|
|
||||||
<Flex
|
|
||||||
align="center"
|
|
||||||
css={{
|
|
||||||
alignSelf: 'start',
|
|
||||||
'> *': {
|
|
||||||
height: 'fit-content',
|
|
||||||
},
|
|
||||||
height: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.val}
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Grid>
|
|
||||||
</ParentComponent>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DetailsCardSkeleton({
|
|
||||||
keyColumns = 2,
|
|
||||||
rows = 3,
|
|
||||||
testidPrefix = 'definition',
|
|
||||||
title,
|
|
||||||
icon,
|
|
||||||
}: { rows?: number } & Omit<DetailsCardProps, 'items'>) {
|
|
||||||
return (
|
|
||||||
<Flex as="section" direction="column" gap={2} data-testid={`${testidPrefix}-section-skeleton`}>
|
|
||||||
{title ? <SectionTitle icon={icon} title={title} /> : <Skeleton css={{ height: '$5', width: '150px' }} />}
|
|
||||||
<Card css={{ flex: 1 }}>
|
|
||||||
<Grid
|
|
||||||
css={{
|
|
||||||
gap: '$2 $3',
|
|
||||||
gridTemplateColumns: `repeat(${keyColumns}, auto 1fr)`,
|
|
||||||
[`@media (max-width:${breakpoints.laptop}px)`]: { gridTemplateColumns: 'auto 1fr' },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{[...Array(rows * keyColumns)].map((_, idx) => (
|
|
||||||
<Fragment key={idx}>
|
|
||||||
<Skeleton css={{ height: '$5', width: '96px' }} />
|
|
||||||
<Skeleton css={{ height: '$5', width: '192px' }} />
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
</Card>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
import { AriaTable, AriaTbody, AriaTd, AriaTr, Flex, Text } from '@traefiklabs/faency'
|
import { AriaTable, AriaTbody, AriaTd, AriaTr, Flex, Text } from '@traefiklabs/faency'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
import Status from './Status'
|
import Status, { StatusType } from './Status'
|
||||||
|
|
||||||
import CopyableText from 'components/CopyableText'
|
import Tooltip from 'components/Tooltip'
|
||||||
|
|
||||||
type GenericTableProps = {
|
type GenericTableProps = {
|
||||||
items: (number | string)[]
|
items: (number | string)[]
|
||||||
status?: Resource.Status
|
status?: StatusType
|
||||||
copyable?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GenericTable({ items, status, copyable = false }: GenericTableProps) {
|
export default function GenericTable({ items, status }: GenericTableProps) {
|
||||||
const border = useMemo(() => `1px solid $${status === 'error' ? 'textRed' : 'tableRowBorder'}`, [status])
|
const border = useMemo(() => `1px solid $${status === 'error' ? 'textRed' : 'tableRowBorder'}`, [status])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -20,31 +19,23 @@ export default function GenericTable({ items, status, copyable = false }: Generi
|
|||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<AriaTr key={index}>
|
<AriaTr key={index}>
|
||||||
<AriaTd css={{ p: '$2' }}>
|
<AriaTd css={{ p: '$2' }}>
|
||||||
|
<Tooltip label={item.toString()} action="copy">
|
||||||
<Flex align="start" gap={2} css={{ width: 'fit-content' }}>
|
<Flex align="start" gap={2} css={{ width: 'fit-content' }}>
|
||||||
{status ? (
|
{status ? (
|
||||||
<Status status="error" css={{ p: '4px', marginRight: 0 }} size={14} />
|
<Status status="error" css={{ p: '4px', marginRight: 0 }} size={16} />
|
||||||
) : (
|
) : (
|
||||||
<Text css={{ fontFamily: 'monospace', mt: '1px', userSelect: 'none' }} variant="subtle">
|
<Text css={{ fontFamily: 'monospace', mt: '1px', userSelect: 'none' }} variant="subtle">
|
||||||
{index}
|
{index}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{copyable ? (
|
|
||||||
<CopyableText
|
|
||||||
text={String(item)}
|
|
||||||
css={{
|
|
||||||
fontFamily: status === 'error' ? 'monospace' : undefined,
|
|
||||||
color: status === 'error' ? '$textRed' : 'initial',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Text
|
<Text
|
||||||
css={{ fontFamily: status === 'error' ? 'monospace' : undefined }}
|
css={{ fontFamily: status === 'error' ? 'monospace' : undefined }}
|
||||||
variant={status === 'error' ? 'red' : undefined}
|
variant={status === 'error' ? 'red' : undefined}
|
||||||
>
|
>
|
||||||
{item}
|
{item}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</Tooltip>
|
||||||
</AriaTd>
|
</AriaTd>
|
||||||
</AriaTr>
|
</AriaTr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
113
webui/src/components/resources/MiddlewarePanel.tsx
Normal file
113
webui/src/components/resources/MiddlewarePanel.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { Box, Flex, H3, styled, Text } from '@traefiklabs/faency'
|
||||||
|
import { FiLayers } from 'react-icons/fi'
|
||||||
|
|
||||||
|
import { DetailSection, EmptyPlaceholder, ItemBlock, LayoutTwoCols, ProviderName } from './DetailSections'
|
||||||
|
import GenericTable from './GenericTable'
|
||||||
|
import { RenderUnknownProp } from './RenderUnknownProp'
|
||||||
|
import { ResourceStatus } from './ResourceStatus'
|
||||||
|
|
||||||
|
import { EmptyIcon } from 'components/icons/EmptyIcon'
|
||||||
|
import ProviderIcon from 'components/icons/providers'
|
||||||
|
import { Middleware, RouterDetailType } from 'hooks/use-resource-detail'
|
||||||
|
import { parseMiddlewareType } from 'libs/parsers'
|
||||||
|
|
||||||
|
const Separator = styled('hr', {
|
||||||
|
border: 'none',
|
||||||
|
background: '$tableRowBorder',
|
||||||
|
margin: '0 0 24px',
|
||||||
|
height: '1px',
|
||||||
|
minHeight: '1px',
|
||||||
|
})
|
||||||
|
|
||||||
|
const filterMiddlewareProps = (middleware: Middleware): string[] => {
|
||||||
|
const filteredProps = [] as string[]
|
||||||
|
const propsToRemove = ['name', 'plugin', 'status', 'type', 'provider', 'error', 'usedBy', 'routers']
|
||||||
|
|
||||||
|
Object.keys(middleware).map((propName) => {
|
||||||
|
if (!propsToRemove.includes(propName)) {
|
||||||
|
filteredProps.push(propName)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return filteredProps
|
||||||
|
}
|
||||||
|
|
||||||
|
type RenderMiddlewareProps = {
|
||||||
|
middleware: Middleware
|
||||||
|
withHeader?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RenderMiddleware = ({ middleware, withHeader }: RenderMiddlewareProps) => (
|
||||||
|
<Flex key={middleware.name} css={{ flexDirection: 'column' }}>
|
||||||
|
{withHeader && <H3 css={{ mb: '$7', overflowWrap: 'break-word' }}>{middleware.name}</H3>}
|
||||||
|
<LayoutTwoCols>
|
||||||
|
{(middleware.type || middleware.plugin) && (
|
||||||
|
<ItemBlock title="Type">
|
||||||
|
<Text css={{ lineHeight: '32px', overflowWrap: 'break-word' }}>{parseMiddlewareType(middleware)}</Text>
|
||||||
|
</ItemBlock>
|
||||||
|
)}
|
||||||
|
{middleware.provider && (
|
||||||
|
<ItemBlock title="Provider">
|
||||||
|
<ProviderIcon name={middleware.provider} />
|
||||||
|
<ProviderName css={{ ml: '$2' }}>{middleware.provider}</ProviderName>
|
||||||
|
</ItemBlock>
|
||||||
|
)}
|
||||||
|
</LayoutTwoCols>
|
||||||
|
{middleware.status && (
|
||||||
|
<ItemBlock title="Status">
|
||||||
|
<ResourceStatus status={middleware.status} withLabel />
|
||||||
|
</ItemBlock>
|
||||||
|
)}
|
||||||
|
{middleware.error && (
|
||||||
|
<ItemBlock title="Errors">
|
||||||
|
<GenericTable items={middleware.error} status="error" />
|
||||||
|
</ItemBlock>
|
||||||
|
)}
|
||||||
|
{middleware.plugin &&
|
||||||
|
Object.keys(middleware.plugin).map((pluginName) => (
|
||||||
|
<RenderUnknownProp key={pluginName} name={pluginName} prop={middleware.plugin?.[pluginName]} />
|
||||||
|
))}
|
||||||
|
{filterMiddlewareProps(middleware).map((propName) => (
|
||||||
|
<RenderUnknownProp
|
||||||
|
key={propName}
|
||||||
|
name={propName}
|
||||||
|
prop={middleware[propName]}
|
||||||
|
removeTitlePrefix={middleware.type}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
|
||||||
|
const MiddlewarePanel = ({ data }: { data: RouterDetailType }) => (
|
||||||
|
<DetailSection icon={<FiLayers size={20} />} title="Middlewares">
|
||||||
|
{data.middlewares ? (
|
||||||
|
data.middlewares.map((middleware, index) => (
|
||||||
|
<Box key={middleware.name}>
|
||||||
|
<RenderMiddleware middleware={middleware} withHeader />
|
||||||
|
{data.middlewares && index < data.middlewares.length - 1 && <Separator />}
|
||||||
|
</Box>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Flex direction="column" align="center" justify="center" css={{ flexGrow: 1, textAlign: 'center' }}>
|
||||||
|
<Box
|
||||||
|
css={{
|
||||||
|
width: 88,
|
||||||
|
svg: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EmptyIcon />
|
||||||
|
</Box>
|
||||||
|
<EmptyPlaceholder css={{ mt: '$3' }}>
|
||||||
|
There are no
|
||||||
|
<br />
|
||||||
|
Middlewares configured
|
||||||
|
</EmptyPlaceholder>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</DetailSection>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default MiddlewarePanel
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { Text } from '@traefiklabs/faency'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
import CopyableText from 'components/CopyableText'
|
import { BooleanState, ItemBlock } from './DetailSections'
|
||||||
import { BooleanState, ItemBlock } from 'components/resources/DetailItemComponents'
|
import GenericTable from './GenericTable'
|
||||||
import GenericTable from 'components/resources/GenericTable'
|
import IpStrategyTable, { IpStrategy } from './IpStrategyTable'
|
||||||
import IpStrategyTable, { IpStrategy } from 'components/resources/IpStrategyTable'
|
|
||||||
|
import Tooltip from 'components/Tooltip'
|
||||||
|
|
||||||
type RenderUnknownPropProps = {
|
type RenderUnknownPropProps = {
|
||||||
name: string
|
name: string
|
||||||
@@ -20,19 +22,23 @@ export const RenderUnknownProp = ({ name, prop, removeTitlePrefix }: RenderUnkno
|
|||||||
try {
|
try {
|
||||||
if (typeof prop !== 'undefined') {
|
if (typeof prop !== 'undefined') {
|
||||||
if (typeof prop === 'boolean') {
|
if (typeof prop === 'boolean') {
|
||||||
return wrap(<BooleanState css={{ fontSize: '$3' }} enabled={prop} />)
|
return wrap(<BooleanState enabled={prop} />)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof prop === 'string' && ['true', 'false'].includes((prop as string).toLowerCase())) {
|
if (typeof prop === 'string' && ['true', 'false'].includes((prop as string).toLowerCase())) {
|
||||||
return wrap(<BooleanState css={{ fontSize: '$3' }} enabled={prop === 'true'} />)
|
return wrap(<BooleanState enabled={prop === 'true'} />)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['string', 'number'].includes(typeof prop)) {
|
if (['string', 'number'].includes(typeof prop)) {
|
||||||
return wrap(<CopyableText text={prop as string} css={{ fontSize: '$3' }} />)
|
return wrap(
|
||||||
|
<Tooltip label={prop as string} action="copy">
|
||||||
|
<Text css={{ overflowWrap: 'break-word' }}>{prop as string}</Text>
|
||||||
|
</Tooltip>,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (JSON.stringify(prop) === '{}') {
|
if (JSON.stringify(prop) === '{}') {
|
||||||
return wrap(<BooleanState enabled css={{ fontSize: '$3' }} />)
|
return wrap(<BooleanState enabled />)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prop instanceof Array) {
|
if (prop instanceof Array) {
|
||||||
@@ -69,7 +75,7 @@ export const RenderUnknownProp = ({ name, prop, removeTitlePrefix }: RenderUnkno
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Unable to render plugin property:', { name, prop }, { error })
|
console.log('Unable to render plugin property:', { name, prop }, { error })
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { Card, Flex, Skeleton } from '@traefiklabs/faency'
|
|
||||||
import { FiAlertTriangle } from 'react-icons/fi'
|
|
||||||
|
|
||||||
import { SectionTitle } from './DetailsCard'
|
|
||||||
import GenericTable from './GenericTable'
|
|
||||||
|
|
||||||
const ResourceErrors = ({ errors }: { errors: string[] }) => {
|
|
||||||
return (
|
|
||||||
<Flex direction="column" gap={2}>
|
|
||||||
<SectionTitle title="Errors" icon={<FiAlertTriangle color="hsl(347, 100%, 60.0%)" size={20} />} />
|
|
||||||
<Card>
|
|
||||||
<GenericTable items={errors} status="error" copyable />
|
|
||||||
</Card>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ResourceErrorsSkeleton = () => {
|
|
||||||
return (
|
|
||||||
<Flex direction="column" gap={2}>
|
|
||||||
<Skeleton css={{ width: 200 }} />
|
|
||||||
<Card css={{ width: '100%', height: 150, gap: '$3', display: 'flex', flexDirection: 'column' }}>
|
|
||||||
{[...Array(4)].map((_, idx) => (
|
|
||||||
<Skeleton key={`1-${idx}`} />
|
|
||||||
))}
|
|
||||||
</Card>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ResourceErrors
|
|
||||||
@@ -1,26 +1,25 @@
|
|||||||
import { Box, Flex, styled, Text } from '@traefiklabs/faency'
|
import { Flex, styled, Text } from '@traefiklabs/faency'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
import { colorByStatus, iconByStatus } from 'components/resources/Status'
|
import { colorByStatus, iconByStatus, StatusType } from 'components/resources/Status'
|
||||||
|
|
||||||
export const StatusWrapper = styled(Flex, {
|
export const StatusWrapper = styled(Flex, {
|
||||||
height: '24px',
|
height: '32px',
|
||||||
width: '24px',
|
width: '32px',
|
||||||
padding: 0,
|
padding: 0,
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
})
|
})
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
status: Resource.Status
|
status: StatusType
|
||||||
label?: string
|
label?: string
|
||||||
withLabel?: boolean
|
withLabel?: boolean
|
||||||
size?: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Value = { color: string; icon: ReactNode; label: string }
|
type Value = { color: string; icon: ReactNode; label: string }
|
||||||
|
|
||||||
export const ResourceStatus = ({ status, withLabel = false, size = 20 }: Props) => {
|
export const ResourceStatus = ({ status, withLabel = false }: Props) => {
|
||||||
const valuesByStatus: { [key in Resource.Status]: Value } = {
|
const valuesByStatus: { [key in StatusType]: Value } = {
|
||||||
info: {
|
info: {
|
||||||
color: colorByStatus.info,
|
color: colorByStatus.info,
|
||||||
icon: iconByStatus.info,
|
icon: iconByStatus.info,
|
||||||
@@ -51,11 +50,6 @@ export const ResourceStatus = ({ status, withLabel = false, size = 20 }: Props)
|
|||||||
icon: iconByStatus.disabled,
|
icon: iconByStatus.disabled,
|
||||||
label: 'Error',
|
label: 'Error',
|
||||||
},
|
},
|
||||||
loading: {
|
|
||||||
color: colorByStatus.loading,
|
|
||||||
icon: iconByStatus.loading,
|
|
||||||
label: 'Loading...',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const values = valuesByStatus[status]
|
const values = valuesByStatus[status]
|
||||||
@@ -65,12 +59,12 @@ export const ResourceStatus = ({ status, withLabel = false, size = 20 }: Props)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex align="center" css={{ width: size, height: size }} data-testid={status}>
|
<Flex css={{ alignItems: 'center' }} data-testid={status}>
|
||||||
<Box css={{ color: values.color, width: size, height: size }}>{values.icon}</Box>
|
<StatusWrapper css={{ alignItems: 'center', justifyContent: 'center', backgroundColor: values.color }}>
|
||||||
|
{values.icon}
|
||||||
|
</StatusWrapper>
|
||||||
{withLabel && values.label && (
|
{withLabel && values.label && (
|
||||||
<Text css={{ ml: '$2', color: values.color, fontWeight: 600, fontSize: 'inherit !important' }}>
|
<Text css={{ ml: '$2', color: values.color, fontWeight: 600 }}>{values.label}</Text>
|
||||||
{values.label}
|
|
||||||
</Text>
|
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
|
|||||||
76
webui/src/components/resources/RouterPanel.tsx
Normal file
76
webui/src/components/resources/RouterPanel.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { Badge, Text } from '@traefiklabs/faency'
|
||||||
|
import { FiInfo } from 'react-icons/fi'
|
||||||
|
|
||||||
|
import { DetailSection, ItemBlock, LayoutTwoCols, ProviderName } from './DetailSections'
|
||||||
|
import GenericTable from './GenericTable'
|
||||||
|
import { ResourceStatus } from './ResourceStatus'
|
||||||
|
|
||||||
|
import ProviderIcon from 'components/icons/providers'
|
||||||
|
import Tooltip from 'components/Tooltip'
|
||||||
|
import { ResourceDetailDataType } from 'hooks/use-resource-detail'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: ResourceDetailDataType
|
||||||
|
}
|
||||||
|
|
||||||
|
const RouterPanel = ({ data }: Props) => (
|
||||||
|
<DetailSection icon={<FiInfo size={20} />} title="Router Details">
|
||||||
|
<LayoutTwoCols>
|
||||||
|
{data.status && (
|
||||||
|
<ItemBlock title="Status">
|
||||||
|
<ResourceStatus status={data.status} withLabel />
|
||||||
|
</ItemBlock>
|
||||||
|
)}
|
||||||
|
{data.provider && (
|
||||||
|
<ItemBlock title="Provider">
|
||||||
|
<ProviderIcon name={data.provider} />
|
||||||
|
<ProviderName css={{ ml: '$2' }}>{data.provider}</ProviderName>
|
||||||
|
</ItemBlock>
|
||||||
|
)}
|
||||||
|
{data.priority && (
|
||||||
|
<ItemBlock title="Priority">
|
||||||
|
<Tooltip label={data.priority.toString()} action="copy">
|
||||||
|
<Text css={{ overflowWrap: 'break-word' }}>{data.priority.toString()}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
</ItemBlock>
|
||||||
|
)}
|
||||||
|
</LayoutTwoCols>
|
||||||
|
{data.rule ? (
|
||||||
|
<ItemBlock title="Rule">
|
||||||
|
<Tooltip label={data.rule} action="copy">
|
||||||
|
<Text css={{ overflowWrap: 'break-word' }}>{data.rule}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
</ItemBlock>
|
||||||
|
) : null}
|
||||||
|
{data.name && (
|
||||||
|
<ItemBlock title="Name">
|
||||||
|
<Tooltip label={data.name} action="copy">
|
||||||
|
<Text css={{ overflowWrap: 'break-word' }}>{data.name}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
</ItemBlock>
|
||||||
|
)}
|
||||||
|
{!!data.using && data.using && data.using.length > 0 && (
|
||||||
|
<ItemBlock title="Entrypoints">
|
||||||
|
{data.using.map((ep) => (
|
||||||
|
<Tooltip key={ep} label={ep} action="copy">
|
||||||
|
<Badge css={{ mr: '$2' }}>{ep}</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</ItemBlock>
|
||||||
|
)}
|
||||||
|
{data.service && (
|
||||||
|
<ItemBlock title="Service">
|
||||||
|
<Tooltip label={data.service} action="copy">
|
||||||
|
<Text css={{ overflowWrap: 'break-word' }}>{data.service}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
</ItemBlock>
|
||||||
|
)}
|
||||||
|
{data.error && (
|
||||||
|
<ItemBlock title="Errors">
|
||||||
|
<GenericTable items={data.error} status="error" />
|
||||||
|
</ItemBlock>
|
||||||
|
)}
|
||||||
|
</DetailSection>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default RouterPanel
|
||||||
@@ -1,50 +1,49 @@
|
|||||||
import { Box, CSS } from '@traefiklabs/faency'
|
import { Box, CSS } from '@traefiklabs/faency'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import { FiAlertCircle, FiAlertTriangle, FiCheckCircle, FiLoader } from 'react-icons/fi'
|
import { FiAlertCircle, FiAlertTriangle, FiCheckCircle } from 'react-icons/fi'
|
||||||
|
|
||||||
export const iconByStatus: { [key in Resource.Status]: ReactNode } = {
|
export type StatusType = 'info' | 'success' | 'warning' | 'error' | 'enabled' | 'disabled'
|
||||||
info: <FiAlertCircle color="currentColor" size={20} />,
|
|
||||||
success: <FiCheckCircle color="currentColor" size={20} />,
|
export const iconByStatus: { [key in StatusType]: ReactNode } = {
|
||||||
warning: <FiAlertCircle color="currentColor" size={20} />,
|
info: <FiAlertCircle color="white" size={20} />,
|
||||||
error: <FiAlertTriangle color="currentColor" size={20} />,
|
success: <FiCheckCircle color="white" size={20} />,
|
||||||
enabled: <FiCheckCircle color="currentColor" size={20} />,
|
warning: <FiAlertCircle color="white" size={20} />,
|
||||||
disabled: <FiAlertTriangle color="currentColor" size={20} />,
|
error: <FiAlertTriangle color="white" size={20} />,
|
||||||
loading: <FiLoader color="currentColor" size={20} />,
|
enabled: <FiCheckCircle color="white" size={20} />,
|
||||||
|
disabled: <FiAlertTriangle color="white" size={20} />,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Please notice: dark and light colors have the same values.
|
// Please notice: dark and light colors have the same values.
|
||||||
export const colorByStatus: { [key in Resource.Status]: string } = {
|
export const colorByStatus: { [key in StatusType]: string } = {
|
||||||
info: 'hsl(220, 67%, 51%)',
|
info: 'hsl(220, 67%, 51%)',
|
||||||
success: '#30A46C',
|
success: '#30A46C',
|
||||||
warning: 'hsl(24 94.0% 50.0%)',
|
warning: 'hsl(24 94.0% 50.0%)',
|
||||||
error: 'hsl(347, 100%, 60.0%)',
|
error: 'hsl(347, 100%, 60.0%)',
|
||||||
enabled: '#30A46C',
|
enabled: '#30A46C',
|
||||||
disabled: 'hsl(347, 100%, 60.0%)',
|
disabled: 'hsl(347, 100%, 60.0%)',
|
||||||
loading: 'hsla(0, 0%, 100%, 0.51)',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type StatusProps = {
|
type StatusProps = {
|
||||||
css?: CSS
|
css?: CSS
|
||||||
size?: number
|
size?: number
|
||||||
status: Resource.Status
|
status: StatusType
|
||||||
color?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Status({ css = {}, size = 20, status, color = 'white' }: StatusProps) {
|
export default function Status({ css = {}, size = 20, status }: StatusProps) {
|
||||||
const Icon = ({ size }: { size: number }) => {
|
const Icon = ({ size }: { size: number }) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'info':
|
case 'info':
|
||||||
return <FiAlertCircle color={color} size={size} />
|
return <FiAlertCircle color="white" size={size} />
|
||||||
case 'success':
|
case 'success':
|
||||||
return <FiCheckCircle color={color} size={size} />
|
return <FiCheckCircle color="white" size={size} />
|
||||||
case 'warning':
|
case 'warning':
|
||||||
return <FiAlertCircle color={color} size={size} />
|
return <FiAlertCircle color="white" size={size} />
|
||||||
case 'error':
|
case 'error':
|
||||||
return <FiAlertTriangle color={color} size={size} />
|
return <FiAlertTriangle color="white" size={size} />
|
||||||
case 'enabled':
|
case 'enabled':
|
||||||
return <FiCheckCircle color={color} size={size} />
|
return <FiCheckCircle color="white" size={size} />
|
||||||
case 'disabled':
|
case 'disabled':
|
||||||
return <FiAlertTriangle color={color} size={size} />
|
return <FiAlertTriangle color="white" size={size} />
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
77
webui/src/components/resources/TlsPanel.tsx
Normal file
77
webui/src/components/resources/TlsPanel.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { Badge, Box, Flex, Text } from '@traefiklabs/faency'
|
||||||
|
import { FiShield } from 'react-icons/fi'
|
||||||
|
|
||||||
|
import { BooleanState, DetailSection, EmptyPlaceholder, ItemBlock } from './DetailSections'
|
||||||
|
|
||||||
|
import { EmptyIcon } from 'components/icons/EmptyIcon'
|
||||||
|
import { RouterDetailType } from 'hooks/use-resource-detail'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: RouterDetailType
|
||||||
|
}
|
||||||
|
|
||||||
|
const TlsPanel = ({ data }: Props) => (
|
||||||
|
<DetailSection icon={<FiShield size={20} />} title="TLS">
|
||||||
|
{data.tls ? (
|
||||||
|
<Flex css={{ flexDirection: 'column' }}>
|
||||||
|
<ItemBlock title="TLS">
|
||||||
|
<BooleanState enabled />
|
||||||
|
</ItemBlock>
|
||||||
|
{data.tls.options && (
|
||||||
|
<ItemBlock title="Options">
|
||||||
|
<Text css={{ overflowWrap: 'break-word' }}>{data.tls.options}</Text>
|
||||||
|
</ItemBlock>
|
||||||
|
)}
|
||||||
|
<ItemBlock title="PassThrough">
|
||||||
|
<BooleanState enabled={!!data.tls.passthrough} />
|
||||||
|
</ItemBlock>
|
||||||
|
{data.tls.certResolver && (
|
||||||
|
<ItemBlock title="Certificate Resolver">
|
||||||
|
<Text css={{ overflowWrap: 'break-word' }}>{data.tls.certResolver}</Text>
|
||||||
|
</ItemBlock>
|
||||||
|
)}
|
||||||
|
{data.tls.domains && (
|
||||||
|
<ItemBlock title="Domains">
|
||||||
|
<Flex css={{ flexDirection: 'column' }}>
|
||||||
|
{data.tls.domains?.map((domain) => (
|
||||||
|
<Flex key={domain.main} css={{ flexWrap: 'wrap' }}>
|
||||||
|
<a href={`//${domain.main}`}>
|
||||||
|
<Badge variant="blue" css={{ mr: '$2', mb: '$2', color: '$primary', borderColor: '$primary' }}>
|
||||||
|
{domain.main}
|
||||||
|
</Badge>
|
||||||
|
</a>
|
||||||
|
{domain.sans?.map((sub) => (
|
||||||
|
<a key={sub} href={`//${sub}`}>
|
||||||
|
<Badge css={{ mr: '$2', mb: '$2' }}>{sub}</Badge>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</ItemBlock>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<Flex direction="column" align="center" justify="center" css={{ flexGrow: 1, textAlign: 'center' }}>
|
||||||
|
<Box
|
||||||
|
css={{
|
||||||
|
width: 88,
|
||||||
|
svg: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EmptyIcon />
|
||||||
|
</Box>
|
||||||
|
<EmptyPlaceholder css={{ mt: '$3' }}>
|
||||||
|
There is no
|
||||||
|
<br />
|
||||||
|
TLS configured
|
||||||
|
</EmptyPlaceholder>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</DetailSection>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default TlsPanel
|
||||||
@@ -5,7 +5,7 @@ import { Doughnut } from 'react-chartjs-2'
|
|||||||
import { FaArrowRightLong } from 'react-icons/fa6'
|
import { FaArrowRightLong } from 'react-icons/fa6'
|
||||||
import { Link as RouterLink, useNavigate } from 'react-router-dom'
|
import { Link as RouterLink, useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
import Status, { colorByStatus } from './Status'
|
import Status, { colorByStatus, StatusType } from './Status'
|
||||||
|
|
||||||
import { capitalizeFirstLetter } from 'utils/string'
|
import { capitalizeFirstLetter } from 'utils/string'
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ export type DataType = {
|
|||||||
|
|
||||||
const getPercent = (total: number, value: number) => (total > 0 ? ((value * 100) / total).toFixed(0) : 0)
|
const getPercent = (total: number, value: number) => (total > 0 ? ((value * 100) / total).toFixed(0) : 0)
|
||||||
|
|
||||||
const STATS_ATTRIBUTES: { status: Resource.Status; label: string }[] = [
|
const STATS_ATTRIBUTES: { status: StatusType; label: string }[] = [
|
||||||
{
|
{
|
||||||
status: 'enabled',
|
status: 'enabled',
|
||||||
label: 'success',
|
label: 'success',
|
||||||
@@ -80,7 +80,7 @@ const CustomLegend = ({
|
|||||||
total,
|
total,
|
||||||
linkTo,
|
linkTo,
|
||||||
}: {
|
}: {
|
||||||
status: Resource.Status
|
status: StatusType
|
||||||
label: string
|
label: string
|
||||||
count: number
|
count: number
|
||||||
total: number
|
total: number
|
||||||
|
|||||||
@@ -1,24 +1,96 @@
|
|||||||
import { Flex } from '@traefiklabs/faency'
|
import { AriaTable, AriaTbody, AriaTd, AriaTh, AriaThead, AriaTr, Box, Flex, styled } from '@traefiklabs/faency'
|
||||||
import { orderBy } from 'lodash'
|
import { orderBy } from 'lodash'
|
||||||
import { useContext, useEffect, useMemo } from 'react'
|
import { useContext, useEffect, useMemo } from 'react'
|
||||||
import { useSearchParams } from 'react-router-dom'
|
import { useSearchParams } from 'react-router-dom'
|
||||||
|
|
||||||
import { SectionTitle } from './DetailsCard'
|
import { SectionHeader } from 'components/resources/DetailSections'
|
||||||
|
import SortableTh from 'components/tables/SortableTh'
|
||||||
import AriaTableSkeleton from 'components/tables/AriaTableSkeleton'
|
|
||||||
import PaginatedTable from 'components/tables/PaginatedTable'
|
|
||||||
import { ToastContext } from 'contexts/toasts'
|
import { ToastContext } from 'contexts/toasts'
|
||||||
|
import { MiddlewareDetailType, ServiceDetailType } from 'hooks/use-resource-detail'
|
||||||
import { makeRowRender } from 'pages/http/HttpRouters'
|
import { makeRowRender } from 'pages/http/HttpRouters'
|
||||||
|
|
||||||
type UsedByRoutersSectionProps = {
|
type UsedByRoutersSectionProps = {
|
||||||
data: Service.Details | Middleware.DetailsData
|
data: ServiceDetailType | MiddlewareDetailType
|
||||||
protocol?: string
|
protocol?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SkeletonContent = styled(Box, {
|
||||||
|
backgroundColor: '$slate5',
|
||||||
|
height: '14px',
|
||||||
|
minWidth: '50px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
margin: '8px',
|
||||||
|
})
|
||||||
|
|
||||||
export const UsedByRoutersSkeleton = () => (
|
export const UsedByRoutersSkeleton = () => (
|
||||||
<Flex gap={2} css={{ flexDirection: 'column', mt: '40px' }}>
|
<Flex css={{ flexDirection: 'column', mt: '40px' }}>
|
||||||
<SectionTitle title="Used by routers" />
|
<SectionHeader />
|
||||||
<AriaTableSkeleton columns={8} />
|
<AriaTable>
|
||||||
|
<AriaThead>
|
||||||
|
<AriaTr>
|
||||||
|
<AriaTh>
|
||||||
|
<SkeletonContent />
|
||||||
|
</AriaTh>
|
||||||
|
<AriaTh>
|
||||||
|
<SkeletonContent />
|
||||||
|
</AriaTh>
|
||||||
|
<AriaTh>
|
||||||
|
<SkeletonContent />
|
||||||
|
</AriaTh>
|
||||||
|
<AriaTh>
|
||||||
|
<SkeletonContent />
|
||||||
|
</AriaTh>
|
||||||
|
<AriaTh>
|
||||||
|
<SkeletonContent />
|
||||||
|
</AriaTh>
|
||||||
|
<AriaTh>
|
||||||
|
<SkeletonContent />
|
||||||
|
</AriaTh>
|
||||||
|
</AriaTr>
|
||||||
|
</AriaThead>
|
||||||
|
<AriaTbody>
|
||||||
|
<AriaTr css={{ pointerEvents: 'none' }}>
|
||||||
|
<AriaTd>
|
||||||
|
<SkeletonContent />
|
||||||
|
</AriaTd>
|
||||||
|
<AriaTd>
|
||||||
|
<SkeletonContent />
|
||||||
|
</AriaTd>
|
||||||
|
<AriaTd>
|
||||||
|
<SkeletonContent />
|
||||||
|
</AriaTd>
|
||||||
|
<AriaTd>
|
||||||
|
<SkeletonContent />
|
||||||
|
</AriaTd>
|
||||||
|
<AriaTd>
|
||||||
|
<SkeletonContent />
|
||||||
|
</AriaTd>
|
||||||
|
<AriaTd>
|
||||||
|
<SkeletonContent />
|
||||||
|
</AriaTd>
|
||||||
|
</AriaTr>
|
||||||
|
<AriaTr css={{ pointerEvents: 'none' }}>
|
||||||
|
<AriaTd>
|
||||||
|
<SkeletonContent />
|
||||||
|
</AriaTd>
|
||||||
|
<AriaTd>
|
||||||
|
<SkeletonContent />
|
||||||
|
</AriaTd>
|
||||||
|
<AriaTd>
|
||||||
|
<SkeletonContent />
|
||||||
|
</AriaTd>
|
||||||
|
<AriaTd>
|
||||||
|
<SkeletonContent />
|
||||||
|
</AriaTd>
|
||||||
|
<AriaTd>
|
||||||
|
<SkeletonContent />
|
||||||
|
</AriaTd>
|
||||||
|
<AriaTd>
|
||||||
|
<SkeletonContent />
|
||||||
|
</AriaTd>
|
||||||
|
</AriaTr>
|
||||||
|
</AriaTbody>
|
||||||
|
</AriaTable>
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -46,38 +118,29 @@ export const UsedByRoutersSection = ({ data, protocol = 'http' }: UsedByRoutersS
|
|||||||
)
|
)
|
||||||
}, [addToast, routersNotFound])
|
}, [addToast, routersNotFound])
|
||||||
|
|
||||||
const columns = useMemo((): Array<{
|
|
||||||
key: keyof Router.DetailsData
|
|
||||||
header: string
|
|
||||||
sortable?: boolean
|
|
||||||
width?: string
|
|
||||||
}> => {
|
|
||||||
return [
|
|
||||||
{ key: 'status', header: 'Status', sortable: true, width: '36px' },
|
|
||||||
...(protocol !== 'udp' ? [{ key: 'tls' as keyof Router.DetailsData, header: 'TLS', width: '24px' }] : []),
|
|
||||||
...(protocol !== 'udp' ? [{ key: 'rule' as keyof Router.DetailsData, header: 'Rule', sortable: true }] : []),
|
|
||||||
{ key: 'using', header: 'Entrypoints', sortable: true },
|
|
||||||
{ key: 'name', header: 'Name', sortable: true },
|
|
||||||
{ key: 'service', header: 'Service', sortable: true },
|
|
||||||
{ key: 'provider', header: 'Provider', sortable: true, width: '40px' },
|
|
||||||
{ key: 'priority', header: 'Priority', sortable: true },
|
|
||||||
]
|
|
||||||
}, [protocol])
|
|
||||||
|
|
||||||
if (!routersFound || routersFound.length <= 0) {
|
if (!routersFound || routersFound.length <= 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex gap={2} css={{ flexDirection: 'column' }}>
|
<Flex css={{ flexDirection: 'column', mt: '$5' }}>
|
||||||
<SectionTitle title="Used by routers" />
|
<SectionHeader title="Used by Routers" />
|
||||||
<PaginatedTable
|
|
||||||
data={routersFound}
|
<AriaTable data-testid="routers-table">
|
||||||
columns={columns}
|
<AriaThead>
|
||||||
itemsPerPage={10}
|
<AriaTr>
|
||||||
testId="routers-table"
|
<SortableTh label="Status" css={{ width: '40px' }} isSortable sortByValue="status" />
|
||||||
renderRow={renderRow}
|
{protocol !== 'udp' ? <SortableTh css={{ width: '40px' }} label="TLS" /> : null}
|
||||||
/>
|
{protocol !== 'udp' ? <SortableTh label="Rule" isSortable sortByValue="rule" /> : null}
|
||||||
|
<SortableTh label="Entrypoints" isSortable sortByValue="entryPoints" />
|
||||||
|
<SortableTh label="Name" isSortable sortByValue="name" />
|
||||||
|
<SortableTh label="Service" isSortable sortByValue="service" />
|
||||||
|
<SortableTh label="Provider" css={{ width: '40px' }} isSortable sortByValue="provider" />
|
||||||
|
<SortableTh label="Priority" isSortable sortByValue="priority" />
|
||||||
|
</AriaTr>
|
||||||
|
</AriaThead>
|
||||||
|
<AriaTbody>{routersFound.map(renderRow)}</AriaTbody>
|
||||||
|
</AriaTable>
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
import { Flex, H1, Skeleton, Text } from '@traefiklabs/faency'
|
|
||||||
import { useMemo } from 'react'
|
|
||||||
import { Helmet } from 'react-helmet-async'
|
|
||||||
|
|
||||||
import { DetailsCardSkeleton } from 'components/resources/DetailsCard'
|
|
||||||
import ResourceErrors, { ResourceErrorsSkeleton } from 'components/resources/ResourceErrors'
|
|
||||||
import RouterFlowDiagram, { RouterFlowDiagramSkeleton } from 'components/routers/RouterFlowDiagram'
|
|
||||||
import TlsSection from 'components/routers/TlsSection'
|
|
||||||
import { NotFound } from 'pages/NotFound'
|
|
||||||
|
|
||||||
type RouterDetailProps = {
|
|
||||||
data?: Resource.DetailsData
|
|
||||||
error?: Error | null
|
|
||||||
name: string
|
|
||||||
protocol: 'http' | 'tcp' | 'udp'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RouterDetail = ({ data, error, name, protocol }: RouterDetailProps) => {
|
|
||||||
const isUdp = useMemo(() => protocol === 'udp', [protocol])
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Helmet>
|
|
||||||
<title>{name} - Traefik Proxy</title>
|
|
||||||
</Helmet>
|
|
||||||
<Text data-testid="error-text">
|
|
||||||
Sorry, we could not fetch detail information for this Router right now. Please, try again later.
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Helmet>
|
|
||||||
<title>{name} - Traefik Proxy</title>
|
|
||||||
</Helmet>
|
|
||||||
<Skeleton css={{ height: '$7', width: '320px', mb: '$7' }} data-testid="skeleton" />
|
|
||||||
<Flex direction="column" gap={6}>
|
|
||||||
<RouterFlowDiagramSkeleton />
|
|
||||||
<ResourceErrorsSkeleton />
|
|
||||||
<DetailsCardSkeleton />
|
|
||||||
</Flex>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.name) {
|
|
||||||
return <NotFound />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Helmet>
|
|
||||||
<title>{data.name} - Traefik Proxy</title>
|
|
||||||
</Helmet>
|
|
||||||
<H1 css={{ mb: '$7' }}>{data.name}</H1>
|
|
||||||
<Flex direction="column" gap={6}>
|
|
||||||
<RouterFlowDiagram data={data} protocol={protocol} />
|
|
||||||
{data?.error && <ResourceErrors errors={data.error} />}
|
|
||||||
{!isUdp && <TlsSection data={data?.tls} />}
|
|
||||||
</Flex>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
import { Card, Flex, styled, Link, Tooltip, Box, Text, Skeleton } from '@traefiklabs/faency'
|
|
||||||
import { useMemo } from 'react'
|
|
||||||
import { FiArrowRight, FiGlobe, FiLayers, FiLogIn, FiZap } from 'react-icons/fi'
|
|
||||||
|
|
||||||
import CopyableText from 'components/CopyableText'
|
|
||||||
import ProviderIcon from 'components/icons/providers'
|
|
||||||
import { ProviderName } from 'components/resources/DetailItemComponents'
|
|
||||||
import DetailsCard, { SectionTitle } from 'components/resources/DetailsCard'
|
|
||||||
import { ResourceStatus } from 'components/resources/ResourceStatus'
|
|
||||||
import ScrollableCard from 'components/ScrollableCard'
|
|
||||||
import { useHrefWithReturnTo } from 'hooks/use-href-with-return-to'
|
|
||||||
import { useResourceDetail } from 'hooks/use-resource-detail'
|
|
||||||
|
|
||||||
const FlexContainer = styled(Flex, {
|
|
||||||
gap: '$3',
|
|
||||||
flexDirection: 'column !important',
|
|
||||||
alignItems: 'center !important',
|
|
||||||
flex: '1 1 0',
|
|
||||||
minWidth: '0',
|
|
||||||
maxWidth: '100%',
|
|
||||||
})
|
|
||||||
|
|
||||||
const ArrowSeparator = () => {
|
|
||||||
return (
|
|
||||||
<Flex css={{ color: '$textSubtle' }}>
|
|
||||||
<FiArrowRight size={20} />
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const LinkedNameAndStatus = ({ data }: { data: { status: Resource.Status; name: string; href?: string } }) => {
|
|
||||||
const hrefWithReturnTo = useHrefWithReturnTo(data?.href || '')
|
|
||||||
|
|
||||||
if (!data.href) {
|
|
||||||
return (
|
|
||||||
<Flex gap={2} css={{ minWidth: 0, flex: 1 }}>
|
|
||||||
<Tooltip content="Service not found">
|
|
||||||
<Box>
|
|
||||||
<ResourceStatus status={data.status} />
|
|
||||||
</Box>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Text
|
|
||||||
css={{
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
overflowWrap: 'anywhere',
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 0,
|
|
||||||
fontSize: '$4',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{data.name}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Flex gap={2} css={{ minWidth: 0, flex: 1 }}>
|
|
||||||
<ResourceStatus status={data.status} />
|
|
||||||
<Link
|
|
||||||
data-testid={data.href}
|
|
||||||
href={hrefWithReturnTo}
|
|
||||||
css={{
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
overflowWrap: 'anywhere',
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{data.name}
|
|
||||||
</Link>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type RouterFlowDiagramProps = {
|
|
||||||
data: Resource.DetailsData
|
|
||||||
protocol: 'http' | 'tcp' | 'udp'
|
|
||||||
}
|
|
||||||
|
|
||||||
const RouterFlowDiagram = ({ data, protocol }: RouterFlowDiagramProps) => {
|
|
||||||
const displayedEntrypoints = useMemo(() => {
|
|
||||||
return data?.entryPointsData?.map((point) => {
|
|
||||||
if (!point.message) {
|
|
||||||
return { key: point.name, val: point.address }
|
|
||||||
} else {
|
|
||||||
return { key: point.message, val: '' }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [data?.entryPointsData])
|
|
||||||
|
|
||||||
const routerDetailsItems = useMemo(
|
|
||||||
() =>
|
|
||||||
[
|
|
||||||
data.status && { key: 'Status', val: <ResourceStatus status={data.status} withLabel /> },
|
|
||||||
data.provider && {
|
|
||||||
key: 'Provider',
|
|
||||||
val: (
|
|
||||||
<>
|
|
||||||
<ProviderIcon name={data.provider} />
|
|
||||||
<ProviderName css={{ ml: '$2' }}>{data.provider}</ProviderName>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
data.priority && { key: 'Priority', val: data.priority },
|
|
||||||
data.rule && { key: 'Rule', val: <CopyableText css={{ lineHeight: 1.2 }} text={data.rule} /> },
|
|
||||||
].filter(Boolean) as { key: string; val: string | React.ReactElement }[],
|
|
||||||
[data.priority, data.provider, data.rule, data.status],
|
|
||||||
)
|
|
||||||
|
|
||||||
const serviceSlug = data.service?.includes('@')
|
|
||||||
? data.service
|
|
||||||
: `${data.service ?? 'unknown'}@${data.provider ?? 'unknown'}`
|
|
||||||
|
|
||||||
const { data: serviceData, error: serviceDataError } = useResourceDetail(serviceSlug ?? '', 'services')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex gap={2} data-testid="router-structure">
|
|
||||||
{!!data.using?.length && (
|
|
||||||
<>
|
|
||||||
<FlexContainer>
|
|
||||||
<SectionTitle icon={<FiLogIn size={20} />} title="Entrypoints" />
|
|
||||||
{displayedEntrypoints?.length ? (
|
|
||||||
<DetailsCard
|
|
||||||
css={{ width: '100%' }}
|
|
||||||
items={displayedEntrypoints}
|
|
||||||
keyColumns={1}
|
|
||||||
maxKeyWidth="70%"
|
|
||||||
scrollable
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<DiagramCardSkeleton />
|
|
||||||
)}
|
|
||||||
</FlexContainer>
|
|
||||||
|
|
||||||
<ArrowSeparator />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FlexContainer data-testid="router-details">
|
|
||||||
<SectionTitle icon={<FiGlobe size={20} />} title={`${protocol.toUpperCase()} Router`} />
|
|
||||||
<DetailsCard css={{ width: '100%' }} items={routerDetailsItems} keyColumns={1} scrollable />
|
|
||||||
</FlexContainer>
|
|
||||||
|
|
||||||
{data.hasValidMiddlewares && (
|
|
||||||
<>
|
|
||||||
<ArrowSeparator />
|
|
||||||
<FlexContainer>
|
|
||||||
<SectionTitle icon={<FiLayers size={20} />} title={`${protocol.toUpperCase()} Middlewares`} />
|
|
||||||
{data.middlewares ? (
|
|
||||||
<ScrollableCard>
|
|
||||||
<Flex direction="column" gap={3}>
|
|
||||||
{data.middlewares.map((mw, idx) => {
|
|
||||||
const data = {
|
|
||||||
name: mw.name,
|
|
||||||
status: mw.status,
|
|
||||||
href: `/${protocol}/middlewares/${mw.name}`,
|
|
||||||
}
|
|
||||||
return <LinkedNameAndStatus key={`mw-${idx}`} data={data} />
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
</ScrollableCard>
|
|
||||||
) : (
|
|
||||||
<DiagramCardSkeleton />
|
|
||||||
)}
|
|
||||||
</FlexContainer>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ArrowSeparator />
|
|
||||||
|
|
||||||
<FlexContainer>
|
|
||||||
<SectionTitle icon={<FiZap size={20} />} title="Service" />
|
|
||||||
<Card css={{ width: '100%' }}>
|
|
||||||
<LinkedNameAndStatus
|
|
||||||
data={{
|
|
||||||
name: data.service as string,
|
|
||||||
status: !serviceDataError ? (serviceData?.status ?? 'loading') : 'disabled',
|
|
||||||
href: !serviceDataError ? `/${protocol}/services/${serviceSlug}` : undefined,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</FlexContainer>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const DiagramCardSkeleton = () => {
|
|
||||||
return (
|
|
||||||
<Card css={{ width: '100%', height: 200, gap: '$3', display: 'flex', flexDirection: 'column' }}>
|
|
||||||
{[...Array(5)].map((_, idx) => (
|
|
||||||
<Skeleton key={`1-${idx}`} />
|
|
||||||
))}
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RouterFlowDiagramSkeleton = () => {
|
|
||||||
return (
|
|
||||||
<Flex gap={4}>
|
|
||||||
{[...Array(4)].map((_, index) => [
|
|
||||||
<FlexContainer key={`container-${index}`}>
|
|
||||||
<Skeleton css={{ width: 100 }} />
|
|
||||||
<DiagramCardSkeleton />
|
|
||||||
</FlexContainer>,
|
|
||||||
index < 3 && <ArrowSeparator key={`separator-${index}`} />,
|
|
||||||
])}
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RouterFlowDiagram
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { FiShield } from 'react-icons/fi'
|
|
||||||
|
|
||||||
const TlsIcon = ({ size = 20 }: { size?: number }) => {
|
|
||||||
return <FiShield color="rgb(48, 164, 108)" size={size} />
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TlsIcon
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import { Badge, Box, Card, Flex } from '@traefiklabs/faency'
|
|
||||||
import { useMemo } from 'react'
|
|
||||||
|
|
||||||
import TlsIcon from './TlsIcon'
|
|
||||||
|
|
||||||
import { EmptyIcon } from 'components/icons/EmptyIcon'
|
|
||||||
import { BooleanState, EmptyPlaceholder } from 'components/resources/DetailItemComponents'
|
|
||||||
import DetailsCard, { SectionTitle } from 'components/resources/DetailsCard'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
data?: Router.TLS
|
|
||||||
}
|
|
||||||
|
|
||||||
const TlsSection = ({ data }: Props) => {
|
|
||||||
const items = useMemo(() => {
|
|
||||||
if (data) {
|
|
||||||
return [
|
|
||||||
data?.options && { key: 'Options', val: data.options },
|
|
||||||
{ key: 'Passthrough', val: <BooleanState enabled={!!data.passthrough} /> },
|
|
||||||
data?.certResolver && { key: 'Certificate resolver', val: data.certResolver },
|
|
||||||
data?.domains && {
|
|
||||||
stackVertical: true,
|
|
||||||
forceNewRow: true,
|
|
||||||
key: 'Domains',
|
|
||||||
val: (
|
|
||||||
<Flex css={{ flexDirection: 'column' }}>
|
|
||||||
{data.domains?.map((domain) => (
|
|
||||||
<Flex key={domain.main} css={{ flexWrap: 'wrap' }}>
|
|
||||||
<a href={`//${domain.main}`}>
|
|
||||||
<Badge variant="blue" css={{ mr: '$2', mb: '$2', color: '$primary', borderColor: '$primary' }}>
|
|
||||||
{domain.main}
|
|
||||||
</Badge>
|
|
||||||
</a>
|
|
||||||
{domain.sans?.map((sub) => (
|
|
||||||
<a key={sub} href={`//${sub}`}>
|
|
||||||
<Badge css={{ mr: '$2', mb: '$2' }}>{sub}</Badge>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
].filter(Boolean) as { key: string; val: string | React.ReactElement }[]
|
|
||||||
}
|
|
||||||
}, [data])
|
|
||||||
return (
|
|
||||||
<Flex direction="column" gap={2}>
|
|
||||||
<SectionTitle icon={<TlsIcon />} title="TLS" />
|
|
||||||
{items?.length ? (
|
|
||||||
<DetailsCard items={items} />
|
|
||||||
) : (
|
|
||||||
<Card>
|
|
||||||
<Flex direction="column" align="center" justify="center" css={{ flexGrow: 1, textAlign: 'center', py: '$4' }}>
|
|
||||||
<Box
|
|
||||||
css={{
|
|
||||||
width: 56,
|
|
||||||
svg: {
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EmptyIcon />
|
|
||||||
</Box>
|
|
||||||
<EmptyPlaceholder css={{ mt: '$3' }}>
|
|
||||||
There is no
|
|
||||||
<br />
|
|
||||||
TLS configured
|
|
||||||
</EmptyPlaceholder>
|
|
||||||
</Flex>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TlsSection
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user