Merge branch v2.5 into master

This commit is contained in:
kevinpollet 2021-11-08 22:41:43 +01:00
commit ce47f200d5
No known key found for this signature in database
GPG key ID: 0C9A5DDD1B292453
70 changed files with 834 additions and 500 deletions

View file

@ -7,7 +7,7 @@ on:
env: env:
GO_VERSION: 1.17 GO_VERSION: 1.17
GOLANGCI_LINT_VERSION: v1.42.1 GOLANGCI_LINT_VERSION: v1.43.0
MISSSPELL_VERSION: v0.3.4 MISSSPELL_VERSION: v0.3.4
PRE_TARGET: "" PRE_TARGET: ""

View file

@ -96,6 +96,10 @@
"godox", # Too strict "godox", # Too strict
"forcetypeassert", # Too strict "forcetypeassert", # Too strict
"tagliatelle", # Not compatible with current tags. "tagliatelle", # Not compatible with current tags.
"varnamelen", # not relevant
"nilnil", # not relevant
"ireturn", # not relevant
"contextcheck", # too many false-positive
] ]
[issues] [issues]

View file

@ -25,7 +25,7 @@ global_job_config:
- export "PATH=${GOPATH}/bin:${PATH}" - export "PATH=${GOPATH}/bin:${PATH}"
- mkdir -vp "${SEMAPHORE_GIT_DIR}" "${GOPATH}/bin" - mkdir -vp "${SEMAPHORE_GIT_DIR}" "${GOPATH}/bin"
- export GOPROXY=https://proxy.golang.org,direct - export GOPROXY=https://proxy.golang.org,direct
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b "${GOPATH}/bin" v1.42.1 - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b "${GOPATH}/bin" v1.43.0
- curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | bash -s -- -b "${GOPATH}/bin" - curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | bash -s -- -b "${GOPATH}/bin"
- checkout - checkout
- cache restore traefik-$(checksum go.sum) - cache restore traefik-$(checksum go.sum)

View file

@ -1,3 +1,33 @@
## [v2.5.4](https://github.com/traefik/traefik/tree/v2.5.4) (2021-11-08)
[All Commits](https://github.com/traefik/traefik/compare/v2.5.3...v2.5.4)
**Bug fixes:**
- **[acme]** Update go-acme/lego to v4.5.0 ([#8481](https://github.com/traefik/traefik/pull/8481) by [ldez](https://github.com/ldez))
- **[k8s/crd,k8s]** fix: add missing RequireAnyClientCert value to TLSOption CRD ([#8464](https://github.com/traefik/traefik/pull/8464) by [kevinpollet](https://github.com/kevinpollet))
- **[k8s/crd,k8s]** fix: normalize middleware names in ingress route config ([#8484](https://github.com/traefik/traefik/pull/8484) by [aaronraff](https://github.com/aaronraff))
- **[middleware,provider,tls]** fix: do not require a TLS client cert when InsecureSkipVerify is false ([#8525](https://github.com/traefik/traefik/pull/8525) by [kevinpollet](https://github.com/kevinpollet))
- **[middleware,tls]** fix: use host's root CA set if ClientTLS ca is not defined ([#8545](https://github.com/traefik/traefik/pull/8545) by [kevinpollet](https://github.com/kevinpollet))
- **[middleware]** fix: forward request Host to errors middleware service ([#8460](https://github.com/traefik/traefik/pull/8460) by [kevinpollet](https://github.com/kevinpollet))
- **[middleware]** fix: use EscapedPath as header value when RawPath is empty ([#8251](https://github.com/traefik/traefik/pull/8251) by [dtomcej](https://github.com/dtomcej))
- **[tcp,udp]** fix: TCP/UDP wrr when all servers have a weight set to 0 ([#8553](https://github.com/traefik/traefik/pull/8553) by [tomMoulard](https://github.com/tomMoulard))
- **[webui]** fix: bug parsing weighted service provider name ([#8522](https://github.com/traefik/traefik/pull/8522) by [cocoanton](https://github.com/cocoanton))
**Documentation:**
- **[acme]** docs: remove quotes in certificatesresolvers CLI examples ([#8544](https://github.com/traefik/traefik/pull/8544) by [rdxmb](https://github.com/rdxmb))
- **[k8s/ingress,k8s]** docs: clarify usage for cross provider references in Kubernetes ingress annotations ([#8536](https://github.com/traefik/traefik/pull/8536) by [rtribotte](https://github.com/rtribotte))
- **[k8s/ingress]** docs: networking.k8s.io/v1beta1 to networking.k8s.io/v1 ([#8523](https://github.com/traefik/traefik/pull/8523) by [pmareke](https://github.com/pmareke))
- **[k8s]** docs: replace links to French translation of k8s docs with English ones ([#8457](https://github.com/traefik/traefik/pull/8457) by [FoseFx](https://github.com/FoseFx))
- **[k8s]** docs: remove non-working kind config in IngressRouteTCP/UDP examples ([#8538](https://github.com/traefik/traefik/pull/8538) by [kevinpollet](https://github.com/kevinpollet))
- **[kv]** docs: fix typo in KV providers documentation ([#8477](https://github.com/traefik/traefik/pull/8477) by [rondoe](https://github.com/rondoe))
- **[metrics]** docs: fix typo in addRoutersLabels option title ([#8561](https://github.com/traefik/traefik/pull/8561) by [kevinpollet](https://github.com/kevinpollet))
- **[middleware]** fix: sourceCriterion documentation for InFlightReq and RateLimit middlewares ([#8524](https://github.com/traefik/traefik/pull/8524) by [pmareke](https://github.com/pmareke))
- **[middleware]** Mention escaping escape characters in YAML for regex usage ([#8496](https://github.com/traefik/traefik/pull/8496) by [JackMorganNZ](https://github.com/JackMorganNZ))
- **[rules]** docs: add named groups details to Regexp Syntax section ([#8559](https://github.com/traefik/traefik/pull/8559) by [kerrsmith](https://github.com/kerrsmith))
- **[tracing]** docs: reword tracing config descriptions to be consistent ([#8473](https://github.com/traefik/traefik/pull/8473) by [kevinpollet](https://github.com/kevinpollet))
- docs: remove link to microbadger.com ([#8555](https://github.com/traefik/traefik/pull/8555) by [CrispyBaguette](https://github.com/CrispyBaguette))
- docs: remove http scheme urls in documentation ([#8507](https://github.com/traefik/traefik/pull/8507) by [tomMoulard](https://github.com/tomMoulard))
- docs: update traefik image version ([#8533](https://github.com/traefik/traefik/pull/8533) by [tomMoulard](https://github.com/tomMoulard))
## [v2.5.3](https://github.com/traefik/traefik/tree/v2.5.3) (2021-09-20) ## [v2.5.3](https://github.com/traefik/traefik/tree/v2.5.3) (2021-09-20)
[All Commits](https://github.com/traefik/traefik/compare/v2.5.2...v2.5.3) [All Commits](https://github.com/traefik/traefik/compare/v2.5.2...v2.5.3)

View file

@ -63,7 +63,7 @@ _(But if you'd rather configure some of your routes manually, Traefik supports t
- Keeps access logs (JSON, CLF) - Keeps access logs (JSON, CLF)
- Fast - Fast
- Exposes a Rest API - Exposes a Rest API
- Packaged as a single binary file (made with :heart: with go) and available as a [tiny](https://microbadger.com/images/traefik) [official](https://hub.docker.com/r/_/traefik/) docker image - Packaged as a single binary file (made with :heart: with go) and available as an [official](https://hub.docker.com/r/_/traefik/) docker image
## Supported Backends ## Supported Backends

View file

@ -14,7 +14,7 @@ RUN mkdir -p /usr/local/bin \
| tar -xzC /usr/local/bin --transform 's#^.+/##x' | tar -xzC /usr/local/bin --transform 's#^.+/##x'
# Download golangci-lint binary to bin folder in $GOPATH # Download golangci-lint binary to bin folder in $GOPATH
RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b $GOPATH/bin v1.42.1 RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b $GOPATH/bin v1.43.0
# Download misspell binary to bin folder in $GOPATH # Download misspell binary to bin folder in $GOPATH
RUN curl -sfL https://raw.githubusercontent.com/client9/misspell/master/install-misspell.sh | bash -s -- -b $GOPATH/bin v0.3.4 RUN curl -sfL https://raw.githubusercontent.com/client9/misspell/master/install-misspell.sh | bash -s -- -b $GOPATH/bin v0.3.4

View file

@ -24,7 +24,7 @@ For more details, go to the [Docker provider documentation](../providers/docker.
!!! tip !!! tip
* Prefer a fixed version than the latest that could be an unexpected version. * Prefer a fixed version than the latest that could be an unexpected version.
ex: `traefik:v2.1.4` ex: `traefik:v2.5`
* Docker images are based from the [Alpine Linux Official image](https://hub.docker.com/_/alpine). * Docker images are based from the [Alpine Linux Official image](https://hub.docker.com/_/alpine).
* Any orchestrator using docker images can fetch the official Traefik docker image. * Any orchestrator using docker images can fetch the official Traefik docker image.
@ -101,13 +101,13 @@ helm install traefik traefik/traefik
This HelmChart does not expose the Traefik dashboard by default, for security concerns. This HelmChart does not expose the Traefik dashboard by default, for security concerns.
Thus, there are multiple ways to expose the dashboard. Thus, there are multiple ways to expose the dashboard.
For instance, the dashboard access could be achieved through a port-forward : For instance, the dashboard access could be achieved through a port-forward:
```shell ```shell
kubectl port-forward $(kubectl get pods --selector "app.kubernetes.io/name=traefik" --output=name) 9000:9000 kubectl port-forward $(kubectl get pods --selector "app.kubernetes.io/name=traefik" --output=name) 9000:9000
``` ```
Accessible with the url: http://127.0.0.1:9000/dashboard/ It can then be reached at: `http://127.0.0.1:9000/dashboard/`
Another way would be to apply your own configuration, for instance, Another way would be to apply your own configuration, for instance,
by defining and applying an IngressRoute CRD (`kubectl apply -f dashboard.yaml`): by defining and applying an IngressRoute CRD (`kubectl apply -f dashboard.yaml`):

View file

@ -36,7 +36,7 @@ Start your `reverse-proxy` with the following command:
docker-compose up -d reverse-proxy docker-compose up -d reverse-proxy
``` ```
You can open a browser and go to <http://localhost:8080/api/rawdata> to see Traefik's API rawdata (we'll go back there once we have launched a service in step 2). You can open a browser and go to `http://localhost:8080/api/rawdata` to see Traefik's API rawdata (we'll go back there once we have launched a service in step 2).
## Traefik Detects New Services and Creates the Route for You ## Traefik Detects New Services and Creates the Route for You
@ -61,7 +61,7 @@ Start the `whoami` service with the following command:
docker-compose up -d whoami docker-compose up -d whoami
``` ```
Go back to your browser (<http://localhost:8080/api/rawdata>) and see that Traefik has automatically detected the new container and updated its own configuration. Go back to your browser (`http://localhost:8080/api/rawdata`) and see that Traefik has automatically detected the new container and updated its own configuration.
When Traefik detects new services, it creates the corresponding routes so you can call them ... _let's see!_ (Here, we're using curl) When Traefik detects new services, it creates the corresponding routes so you can call them ... _let's see!_ (Here, we're using curl)
@ -85,7 +85,7 @@ Run more instances of your `whoami` service with the following command:
docker-compose up -d --scale whoami=2 docker-compose up -d --scale whoami=2
``` ```
Go back to your browser (<http://localhost:8080/api/rawdata>) and see that Traefik has automatically detected the new instance of the container. Go back to your browser (`http://localhost:8080/api/rawdata`) and see that Traefik has automatically detected the new instance of the container.
Finally, see that Traefik load-balances between the two instances of your service by running the following command twice: Finally, see that Traefik load-balances between the two instances of your service by running the following command twice:

View file

@ -560,7 +560,7 @@ certificatesResolvers:
```bash tab="CLI" ```bash tab="CLI"
# ... # ...
--certificatesresolvers.myresolver.acme.preferredChain="ISRG Root X1" --certificatesresolvers.myresolver.acme.preferredChain=ISRG Root X1
# ... # ...
``` ```
@ -588,7 +588,7 @@ certificatesResolvers:
```bash tab="CLI" ```bash tab="CLI"
# ... # ...
--certificatesresolvers.myresolver.acme.keyType="RSA4096" --certificatesresolvers.myresolver.acme.keyType=RSA4096
# ... # ...
``` ```

View file

@ -353,7 +353,8 @@ The `tls` option is the TLS configuration from Traefik to the authentication ser
#### `tls.ca` #### `tls.ca`
Certificate Authority used for the secured connection to the authentication server. Certificate Authority used for the secured connection to the authentication server,
defaults to the system bundle.
```yaml tab="Docker" ```yaml tab="Docker"
labels: labels:

View file

@ -115,7 +115,7 @@ http:
### `sourceCriterion` ### `sourceCriterion`
The `sourceCriterion` option defines what criterion is used to group requests as originating from a common source. The `sourceCriterion` option defines what criterion is used to group requests as originating from a common source.
The precedence order is `ipStrategy`, then `requestHeaderName`, then `requestHost`. If several strategies are defined at the same time, an error will be raised.
If none are set, the default is to use the `requestHost`. If none are set, the default is to use the `requestHost`.
#### `sourceCriterion.ipStrategy` #### `sourceCriterion.ipStrategy`

View file

@ -250,7 +250,7 @@ http:
### `sourceCriterion` ### `sourceCriterion`
The `sourceCriterion` option defines what criterion is used to group requests as originating from a common source. The `sourceCriterion` option defines what criterion is used to group requests as originating from a common source.
The precedence order is `ipStrategy`, then `requestHeaderName`, then `requestHost`. If several strategies are defined at the same time, an error will be raised.
If none are set, the default is to use the request's remote address field (as an `ipStrategy`). If none are set, the default is to use the request's remote address field (as an `ipStrategy`).
#### `sourceCriterion.ipStrategy` #### `sourceCriterion.ipStrategy`

View file

@ -179,7 +179,7 @@ To enable HTTPS, it is not sufficient anymore to only rely on a TLS section in t
#### Expose an Ingress on 80 and 443 #### Expose an Ingress on 80 and 443
Define the default TLS configuration on the HTTPS entry point. Define the default TLS configuration on the HTTPS entry point.
```yaml tab="Ingress" ```yaml tab="Ingress"
kind: Ingress kind: Ingress
@ -335,7 +335,7 @@ The file parser has been changed, since v2.3 the unknown options/fields in a dyn
### IngressClass ### IngressClass
In `v2.3`, the support of `IngressClass`, which is available since Kubernetes version `1.18`, has been introduced. In `v2.3`, the support of `IngressClass`, which is available since Kubernetes version `1.18`, has been introduced.
In order to be able to use this new resource the [Kubernetes RBAC](../reference/dynamic-configuration/kubernetes-crd.md#rbac) must be updated. In order to be able to use this new resource the [Kubernetes RBAC](../reference/dynamic-configuration/kubernetes-crd.md#rbac) must be updated.
## v2.3 to v2.4 ## v2.3 to v2.4
@ -350,7 +350,7 @@ It is therefore necessary to update [RBAC](../reference/dynamic-configuration/ku
In `v2.4.8`, we introduced a new check on domain names used in HTTP router rule `Host` and `HostRegexp` expressions, In `v2.4.8`, we introduced a new check on domain names used in HTTP router rule `Host` and `HostRegexp` expressions,
and in TCP router rule `HostSNI` expression. and in TCP router rule `HostSNI` expression.
This check ensures that provided domain names don't contain non-ASCII characters. This check ensures that provided domain names don't contain non-ASCII characters.
If not, an error is raised, and the associated router will be shown as invalid in the dashboard. If not, an error is raised, and the associated router will be shown as invalid in the dashboard.
This new behavior is intended to show what was failing silently previously and to help troubleshooting configuration issues. This new behavior is intended to show what was failing silently previously and to help troubleshooting configuration issues.
@ -380,8 +380,8 @@ To allow it, the `allowExternalNameServices` option should be set to `true`.
### Kubernetes CRD ### Kubernetes CRD
In `v2.5`, the [Traefik CRDs](../reference/dynamic-configuration/kubernetes-crd.md#definitions) have been updated to support the new API version `apiextensions.k8s.io/v1`. In `v2.5`, the [Traefik CRDs](../reference/dynamic-configuration/kubernetes-crd.md#definitions) have been updated to support the new API version `apiextensions.k8s.io/v1`.
As required by `apiextensions.k8s.io/v1`, we have included the OpenAPI validation schema. As required by `apiextensions.k8s.io/v1`, we have included the OpenAPI validation schema.
After deploying the new [Traefik CRDs](../reference/dynamic-configuration/kubernetes-crd.md#definitions), the resources will be validated only on creation or update. After deploying the new [Traefik CRDs](../reference/dynamic-configuration/kubernetes-crd.md#definitions), the resources will be validated only on creation or update.
@ -420,7 +420,7 @@ the legacy behavior related to the CommonName field can not be enabled at all an
### Errors middleware ### Errors middleware
In `v2.5.4`, when the errors service is configured with the [`PassHostHeader`](../routing/services/index.md#pass-host-header) option to `true` (default), In `v2.5.4`, when the errors service is configured with the [`PassHostHeader`](../routing/services/index.md#pass-host-header) option to `true` (default),
the forwarded Host header value is now set to the client request Host value and not `0.0.0.0`. the forwarded Host header value is now set to the client request Host value and not `0.0.0.0`.
Check out the [Errors middleware](../middlewares/http/errorpages.md#service) documentation for more details. Check out the [Errors middleware](../middlewares/http/errorpages.md#service) documentation for more details.

View file

@ -247,7 +247,7 @@ version: "3.7"
services: services:
traefik: traefik:
image: traefik:v2.2 image: traefik:v2.5
environment: environment:
- TZ=US/Alaska - TZ=US/Alaska
command: command:

View file

@ -59,7 +59,7 @@ metrics:
```bash tab="CLI" ```bash tab="CLI"
--metrics.datadog.addEntryPointsLabels=true --metrics.datadog.addEntryPointsLabels=true
``` ```
#### `AddRoutersLabels` #### `addRoutersLabels`
_Optional, Default=false_ _Optional, Default=false_

View file

@ -170,7 +170,7 @@ metrics:
--metrics.influxdb.addEntryPointsLabels=true --metrics.influxdb.addEntryPointsLabels=true
``` ```
#### `AddRoutersLabels` #### `addRoutersLabels`
_Optional, Default=false_ _Optional, Default=false_

View file

@ -64,7 +64,7 @@ metrics:
--metrics.prometheus.addEntryPointsLabels=true --metrics.prometheus.addEntryPointsLabels=true
``` ```
#### `AddRoutersLabels` #### `addRoutersLabels`
_Optional, Default=false_ _Optional, Default=false_

View file

@ -60,7 +60,7 @@ metrics:
--metrics.statsd.addEntryPointsLabels=true --metrics.statsd.addEntryPointsLabels=true
``` ```
#### `AddRoutersLabels` #### `addRoutersLabels`
_Optional, Default=false_ _Optional, Default=false_

View file

@ -368,7 +368,8 @@ Defines TLS options for Consul server endpoint.
_Optional_ _Optional_
`ca` is the path to the CA certificate used for Consul communication, defaults to the system bundle if not specified. Certificate Authority used for the secure connection to Consul,
defaults to the system bundle.
```yaml tab="File (YAML)" ```yaml tab="File (YAML)"
providers: providers:

View file

@ -106,7 +106,8 @@ _Optional_
#### `tls.ca` #### `tls.ca`
Certificate Authority used for the secure connection to Consul. Certificate Authority used for the secure connection to Consul,
defaults to the system bundle.
```yaml tab="File (YAML)" ```yaml tab="File (YAML)"
providers: providers:

View file

@ -615,7 +615,8 @@ _Optional_
#### `tls.ca` #### `tls.ca`
Certificate Authority used for the secure connection to Docker. Certificate Authority used for the secure connection to Docker,
defaults to the system bundle.
```yaml tab="File (YAML)" ```yaml tab="File (YAML)"
providers: providers:

View file

@ -106,7 +106,8 @@ _Optional_
#### `tls.ca` #### `tls.ca`
Certificate Authority used for the secure connection to etcd. Certificate Authority used for the secure connection to etcd,
defaults to the system bundle.
```yaml tab="File (YAML)" ```yaml tab="File (YAML)"
providers: providers:

View file

@ -78,7 +78,8 @@ _Optional_
#### `tls.ca` #### `tls.ca`
Certificate Authority used for the secure connection to the configured endpoint. Certificate Authority used for the secure connection to the configured endpoint,
defaults to the system bundle.
```yaml tab="File (YAML)" ```yaml tab="File (YAML)"
providers: providers:

View file

@ -62,7 +62,7 @@ Previous versions of Traefik used a [KV store](https://doc.traefik.io/traefik/v1
If you need Let's Encrypt with HA in a Kubernetes environment, we recommend using [Traefik Enterprise](https://traefik.io/traefik-enterprise/), which includes distributed Let's Encrypt as a supported feature. If you need Let's Encrypt with HA in a Kubernetes environment, we recommend using [Traefik Enterprise](https://traefik.io/traefik-enterprise/), which includes distributed Let's Encrypt as a supported feature.
If you want to keep using Traefik Proxy, high availability for Let's Encrypt can be achieved by using a Certificate Controller such as [Cert-Manager](https://docs.cert-manager.io/en/latest/index.html). If you want to keep using Traefik Proxy, high availability for Let's Encrypt can be achieved by using a Certificate Controller such as [Cert-Manager](https://cert-manager.io/docs/).
When using Cert-Manager to manage certificates, it creates secrets in your namespaces that can be referenced as TLS secrets in your [ingress objects](https://kubernetes.io/docs/concepts/services-networking/ingress/#tls). When using Cert-Manager to manage certificates, it creates secrets in your namespaces that can be referenced as TLS secrets in your [ingress objects](https://kubernetes.io/docs/concepts/services-networking/ingress/#tls).
When using the Traefik Kubernetes CRD Provider, unfortunately Cert-Manager cannot yet interface directly with the CRDs. When using the Traefik Kubernetes CRD Provider, unfortunately Cert-Manager cannot yet interface directly with the CRDs.
A workaround is to enable the [Kubernetes Ingress provider](./kubernetes-ingress.md) to allow Cert-Manager to create ingress objects to complete the challenges. A workaround is to enable the [Kubernetes Ingress provider](./kubernetes-ingress.md) to allow Cert-Manager to create ingress objects to complete the challenges.

View file

@ -36,10 +36,10 @@ and derives the corresponding dynamic configuration from it,
which in turn creates the resulting routers, services, handlers, etc. which in turn creates the resulting routers, services, handlers, etc.
```yaml tab="Ingress" ```yaml tab="Ingress"
apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
apiVersion: networking.k8s.io/v1beta1
metadata: metadata:
name: "foo" name: foo
namespace: production namespace: production
spec: spec:
@ -48,20 +48,26 @@ spec:
http: http:
paths: paths:
- path: /bar - path: /bar
pathType: Exact
backend: backend:
serviceName: service1 service:
servicePort: 80 name: service1
port:
number: 80
- path: /foo - path: /foo
pathType: Exact
backend: backend:
serviceName: service1 service:
servicePort: 80 name: service1
port:
number: 80
``` ```
```yaml tab="Ingress Kubernetes v1.19+" ```yaml tab="Ingress v1beta1 (deprecated)"
apiVersion: networking.k8s.io/v1beta1
kind: Ingress kind: Ingress
apiVersion: networking.k8s.io/v1
metadata: metadata:
name: "foo" name: foo
namespace: production namespace: production
spec: spec:
@ -70,19 +76,13 @@ spec:
http: http:
paths: paths:
- path: /bar - path: /bar
pathType: Exact
backend: backend:
service: serviceName: service1
name: service1 servicePort: 80
port:
number: 80
- path: /foo - path: /foo
pathType: Exact
backend: backend:
service: serviceName: service1
name: service1 servicePort: 80
port:
number: 80
``` ```
## LetsEncrypt Support with the Ingress Provider ## LetsEncrypt Support with the Ingress Provider
@ -104,7 +104,7 @@ If you need Let's Encrypt with high availability in a Kubernetes environment,
we recommend using [Traefik Enterprise](https://traefik.io/traefik-enterprise/) which includes distributed Let's Encrypt as a supported feature. we recommend using [Traefik Enterprise](https://traefik.io/traefik-enterprise/) which includes distributed Let's Encrypt as a supported feature.
If you want to keep using Traefik Proxy, If you want to keep using Traefik Proxy,
LetsEncrypt HA can be achieved by using a Certificate Controller such as [Cert-Manager](https://docs.cert-manager.io/en/latest/index.html). LetsEncrypt HA can be achieved by using a Certificate Controller such as [Cert-Manager](https://cert-manager.io/docs/).
When using Cert-Manager to manage certificates, When using Cert-Manager to manage certificates,
it creates secrets in your namespaces that can be referenced as TLS secrets in your [ingress objects](https://kubernetes.io/docs/concepts/services-networking/ingress/#tls). it creates secrets in your namespaces that can be referenced as TLS secrets in your [ingress objects](https://kubernetes.io/docs/concepts/services-networking/ingress/#tls).
@ -272,19 +272,19 @@ Otherwise, Ingresses missing the annotation, having an empty value, or the value
``` ```
```yaml tab="Ingress" ```yaml tab="Ingress"
apiVersion: "networking.k8s.io/v1beta1" apiVersion: networking.k8s.io/v1beta1
kind: "Ingress" kind: Ingress
metadata: metadata:
name: "example-ingress" name: example-ingress
spec: spec:
ingressClassName: "traefik-lb" ingressClassName: traefik-lb
rules: rules:
- host: "*.example.com" - host: "*.example.com"
http: http:
paths: paths:
- path: "/example" - path: /example
backend: backend:
serviceName: "example-service" serviceName: example-service
servicePort: 80 servicePort: 80
``` ```
@ -303,21 +303,21 @@ Otherwise, Ingresses missing the annotation, having an empty value, or the value
``` ```
```yaml tab="Ingress" ```yaml tab="Ingress"
apiVersion: "networking.k8s.io/v1" apiVersion: networking.k8s.io/v1
kind: "Ingress" kind: Ingress
metadata: metadata:
name: "example-ingress" name: example-ingress
spec: spec:
ingressClassName: "traefik-lb" ingressClassName: traefik-lb
rules: rules:
- host: "*.example.com" - host: "*.example.com"
http: http:
paths: paths:
- path: "/example" - path: /example
pathType: Exact pathType: Exact
backend: backend:
service: service:
name: "example-service" name: example-service
port: port:
number: 80 number: 80
``` ```

View file

@ -406,7 +406,8 @@ _Optional_
#### `tls.ca` #### `tls.ca`
Certificate Authority used for the secure connection to Marathon. Certificate Authority used for the secure connection to Marathon,
defaults to the system bundle.
```yaml tab="File (YAML)" ```yaml tab="File (YAML)"
providers: providers:

View file

@ -106,7 +106,8 @@ _Optional_
#### `tls.ca` #### `tls.ca`
Certificate Authority used for the secure connection to Redis. Certificate Authority used for the secure connection to Redis,
defaults to the system bundle.
```yaml tab="File (YAML)" ```yaml tab="File (YAML)"
providers: providers:

View file

@ -106,7 +106,8 @@ _Optional_
#### `tls.ca` #### `tls.ca`
Certificate Authority used for the secure connection to ZooKeeper. Certificate Authority used for the secure connection to ZooKeeper,
defaults to the system bundle.
```yaml tab="File (YAML)" ```yaml tab="File (YAML)"
providers: providers:

View file

@ -1,5 +1,5 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata: metadata:
name: traefik-ingress-controller name: traefik-ingress-controller
@ -48,8 +48,8 @@ rules:
- watch - watch
--- ---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata: metadata:
name: traefik-ingress-controller name: traefik-ingress-controller

View file

@ -45,8 +45,8 @@ rules:
- update - update
--- ---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata: metadata:
name: gateway-controller name: gateway-controller

View file

@ -400,7 +400,7 @@ spec:
info configuration. info configuration.
properties: properties:
issuer: issuer:
description: TLSCLientCertificateIssuerDNInfo holds the client description: TLSClientCertificateIssuerDNInfo holds the client
TLS certificate distinguished name info configuration. cf TLS certificate distinguished name info configuration. cf
https://tools.ietf.org/html/rfc3739 https://tools.ietf.org/html/rfc3739
properties: properties:
@ -428,7 +428,7 @@ spec:
serialNumber: serialNumber:
type: boolean type: boolean
subject: subject:
description: TLSCLientCertificateSubjectDNInfo holds the client description: TLSClientCertificateSubjectDNInfo holds the client
TLS certificate distinguished name info configuration. cf TLS certificate distinguished name info configuration. cf
https://tools.ietf.org/html/rfc3739 https://tools.ietf.org/html/rfc3739
properties: properties:

View file

@ -131,7 +131,6 @@ The Kubernetes Ingress Controller, The Custom Resource Way.
- tcpep - tcpep
routes: routes:
- match: HostSNI(`bar`) - match: HostSNI(`bar`)
kind: Rule
services: services:
- name: whoamitcp - name: whoamitcp
port: 8080 port: 8080
@ -147,8 +146,7 @@ The Kubernetes Ingress Controller, The Custom Resource Way.
entryPoints: entryPoints:
- udpep - udpep
routes: routes:
- kind: Rule - services:
services:
- name: whoamiudp - name: whoamiudp
port: 8080 port: 8080
``` ```
@ -1224,7 +1222,6 @@ Register the `IngressRouteTCP` [kind](../../reference/dynamic-configuration/kube
routes: routes:
- match: HostSNI(`*`) - match: HostSNI(`*`)
kind: Rule
services: services:
- name: external-svc - name: external-svc
port: 80 port: 80
@ -1254,7 +1251,6 @@ Register the `IngressRouteTCP` [kind](../../reference/dynamic-configuration/kube
routes: routes:
- match: HostSNI(`*`) - match: HostSNI(`*`)
kind: Rule
services: services:
- name: external-svc - name: external-svc
port: 80 port: 80

View file

@ -15,8 +15,8 @@ which in turn will create the resulting routers, services, handlers, etc.
```yaml tab="RBAC" ```yaml tab="RBAC"
--- ---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata: metadata:
name: traefik-ingress-controller name: traefik-ingress-controller
rules: rules:
@ -48,8 +48,8 @@ which in turn will create the resulting routers, services, handlers, etc.
- update - update
--- ---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata: metadata:
name: traefik-ingress-controller name: traefik-ingress-controller
roleRef: roleRef:
@ -63,8 +63,37 @@ which in turn will create the resulting routers, services, handlers, etc.
``` ```
```yaml tab="Ingress" ```yaml tab="Ingress"
apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata:
name: myingress
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
rules:
- host: example.com
http:
paths:
- path: /bar
pathType: Exact
backend:
service:
name: whoami
port:
number: 80
- path: /foo
pathType: Exact
backend:
service:
name: whoami
port:
number: 80
```
```yaml tab="Ingress v1beta1 (deprecated)"
apiVersion: networking.k8s.io/v1beta1 apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata: metadata:
name: myingress name: myingress
annotations: annotations:
@ -84,36 +113,7 @@ which in turn will create the resulting routers, services, handlers, etc.
serviceName: whoami serviceName: whoami
servicePort: 80 servicePort: 80
``` ```
```yaml tab="Ingress Kubernetes v1.19+"
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: myingress
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
rules:
- host: example.com
http:
paths:
- path: /bar
pathType: Exact
backend:
service:
name: whoami
port:
number: 80
- path: /foo
pathType: Exact
backend:
service:
name: whoami
port:
number: 80
```
```yaml tab="Traefik" ```yaml tab="Traefik"
apiVersion: v1 apiVersion: v1
kind: ServiceAccount kind: ServiceAccount
@ -121,8 +121,8 @@ which in turn will create the resulting routers, services, handlers, etc.
name: traefik-ingress-controller name: traefik-ingress-controller
--- ---
kind: Deployment
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment
metadata: metadata:
name: traefik name: traefik
labels: labels:
@ -166,8 +166,8 @@ which in turn will create the resulting routers, services, handlers, etc.
``` ```
```yaml tab="Whoami" ```yaml tab="Whoami"
kind: Deployment
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment
metadata: metadata:
name: whoami name: whoami
labels: labels:
@ -209,6 +209,11 @@ which in turn will create the resulting routers, services, handlers, etc.
## Annotations ## Annotations
!!! warning "Referencing resources in annotations"
In an annotation, when referencing a resource defined by another provider,
the [provider namespace syntax](../../providers/overview.md#provider-namespace) must be used.
#### On Ingress #### On Ingress
??? info "`traefik.ingress.kubernetes.io/router.entrypoints`" ??? info "`traefik.ingress.kubernetes.io/router.entrypoints`"
@ -224,7 +229,7 @@ which in turn will create the resulting routers, services, handlers, etc.
See [middlewares](../routers/index.md#middlewares) and [middlewares overview](../../middlewares/overview.md) for more information. See [middlewares](../routers/index.md#middlewares) and [middlewares overview](../../middlewares/overview.md) for more information.
```yaml ```yaml
traefik.ingress.kubernetes.io/router.middlewares: auth@file,prefix@kubernetescrd,cb@file traefik.ingress.kubernetes.io/router.middlewares: auth@file,default-prefix@kubernetescrd
``` ```
??? info "`traefik.ingress.kubernetes.io/router.priority`" ??? info "`traefik.ingress.kubernetes.io/router.priority`"
@ -237,7 +242,7 @@ which in turn will create the resulting routers, services, handlers, etc.
??? info "`traefik.ingress.kubernetes.io/router.pathmatcher`" ??? info "`traefik.ingress.kubernetes.io/router.pathmatcher`"
Overrides the default router rule type used for a path. Overrides the default router rule type used for a path.
Only path-related matcher name can be specified: `Path`, `PathPrefix`. Only path-related matcher name can be specified: `Path`, `PathPrefix`.
Default `PathPrefix` Default `PathPrefix`
@ -283,7 +288,7 @@ which in turn will create the resulting routers, services, handlers, etc.
See [options](../routers/index.md#options) for more information. See [options](../routers/index.md#options) for more information.
```yaml ```yaml
traefik.ingress.kubernetes.io/router.tls.options: foobar traefik.ingress.kubernetes.io/router.tls.options: foobar@file
``` ```
#### On Service #### On Service
@ -401,8 +406,8 @@ This way, any Ingress attached to this Entrypoint will have TLS termination by d
```yaml tab="RBAC" ```yaml tab="RBAC"
--- ---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata: metadata:
name: traefik-ingress-controller name: traefik-ingress-controller
rules: rules:
@ -434,8 +439,8 @@ This way, any Ingress attached to this Entrypoint will have TLS termination by d
- update - update
--- ---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata: metadata:
name: traefik-ingress-controller name: traefik-ingress-controller
roleRef: roleRef:
@ -449,8 +454,37 @@ This way, any Ingress attached to this Entrypoint will have TLS termination by d
``` ```
```yaml tab="Ingress" ```yaml tab="Ingress"
apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata:
name: myingress
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
spec:
rules:
- host: example.com
http:
paths:
- path: /bar
pathType: Exact
backend:
service:
name: whoami
port:
number: 80
- path: /foo
pathType: Exact
backend:
service:
name: whoami
port:
number: 80
```
```yaml tab="Ingress v1beta1 (deprecated)"
apiVersion: networking.k8s.io/v1beta1 apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata: metadata:
name: myingress name: myingress
annotations: annotations:
@ -470,36 +504,7 @@ This way, any Ingress attached to this Entrypoint will have TLS termination by d
serviceName: whoami serviceName: whoami
servicePort: 80 servicePort: 80
``` ```
```yaml tab="Ingress Kubernetes v1.19+"
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: myingress
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
spec:
rules:
- host: example.com
http:
paths:
- path: /bar
pathType: Exact
backend:
service:
name: whoami
port:
number: 80
- path: /foo
pathType: Exact
backend:
service:
name: whoami
port:
number: 80
```
```yaml tab="Traefik" ```yaml tab="Traefik"
apiVersion: v1 apiVersion: v1
kind: ServiceAccount kind: ServiceAccount
@ -507,8 +512,8 @@ This way, any Ingress attached to this Entrypoint will have TLS termination by d
name: traefik-ingress-controller name: traefik-ingress-controller
--- ---
kind: Deployment
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment
metadata: metadata:
name: traefik name: traefik
labels: labels:
@ -553,8 +558,8 @@ This way, any Ingress attached to this Entrypoint will have TLS termination by d
``` ```
```yaml tab="Whoami" ```yaml tab="Whoami"
kind: Deployment
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment
metadata: metadata:
name: whoami name: whoami
labels: labels:
@ -608,8 +613,8 @@ For more options, please refer to the available [annotations](#on-ingress).
```yaml tab="RBAC" ```yaml tab="RBAC"
--- ---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata: metadata:
name: traefik-ingress-controller name: traefik-ingress-controller
rules: rules:
@ -641,8 +646,8 @@ For more options, please refer to the available [annotations](#on-ingress).
- update - update
--- ---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata: metadata:
name: traefik-ingress-controller name: traefik-ingress-controller
roleRef: roleRef:
@ -656,8 +661,38 @@ For more options, please refer to the available [annotations](#on-ingress).
``` ```
```yaml tab="Ingress" ```yaml tab="Ingress"
apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata:
name: myingress
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: true
spec:
rules:
- host: example.com
http:
paths:
- path: /bar
pathType: Exact
backend:
service:
name: whoami
port:
number: 80
- path: /foo
pathType: Exact
backend:
service:
name: whoami
port:
number: 80
```
```yaml tab="Ingress v1beta1 (deprecated)"
apiVersion: networking.k8s.io/v1beta1 apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata: metadata:
name: myingress name: myingress
annotations: annotations:
@ -678,37 +713,7 @@ For more options, please refer to the available [annotations](#on-ingress).
serviceName: whoami serviceName: whoami
servicePort: 80 servicePort: 80
``` ```
```yaml tab="Ingress Kubernetes v1.19+"
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: myingress
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: true
spec:
rules:
- host: example.com
http:
paths:
- path: /bar
pathType: Exact
backend:
service:
name: whoami
port:
number: 80
- path: /foo
pathType: Exact
backend:
service:
name: whoami
port:
number: 80
```
```yaml tab="Traefik" ```yaml tab="Traefik"
apiVersion: v1 apiVersion: v1
kind: ServiceAccount kind: ServiceAccount
@ -716,8 +721,8 @@ For more options, please refer to the available [annotations](#on-ingress).
name: traefik-ingress-controller name: traefik-ingress-controller
--- ---
kind: Deployment
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment
metadata: metadata:
name: traefik name: traefik
labels: labels:
@ -761,8 +766,8 @@ For more options, please refer to the available [annotations](#on-ingress).
``` ```
```yaml tab="Whoami" ```yaml tab="Whoami"
kind: Deployment
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment
metadata: metadata:
name: whoami name: whoami
labels: labels:
@ -807,8 +812,34 @@ For more options, please refer to the available [annotations](#on-ingress).
??? example "Using a secret" ??? example "Using a secret"
```yaml tab="Ingress" ```yaml tab="Ingress"
apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata:
name: foo
namespace: production
spec:
rules:
- host: example.net
http:
paths:
- path: /bar
pathType: Exact
backend:
service:
name: service1
port:
number: 80
# Only selects which certificate(s) should be loaded from the secret, in order to terminate TLS.
# Doesn't enable TLS for that ingress (hence for the underlying router).
# Please see the TLS annotations on ingress made for that purpose.
tls:
- secretName: supersecret
```
```yaml tab="Ingress v1beta1 (deprecated)"
apiVersion: networking.k8s.io/v1beta1 apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata: metadata:
name: foo name: foo
namespace: production namespace: production
@ -829,32 +860,6 @@ For more options, please refer to the available [annotations](#on-ingress).
- secretName: supersecret - secretName: supersecret
``` ```
```yaml tab="Ingress Kubernetes v1.19+"
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: foo
namespace: production
spec:
rules:
- host: example.net
http:
paths:
- path: /bar
pathType: Exact
backend:
service:
name: service1
port:
number: 80
# Only selects which certificate(s) should be loaded from the secret, in order to terminate TLS.
# Doesn't enable TLS for that ingress (hence for the underlying router).
# Please see the TLS annotations on ingress made for that purpose.
tls:
- secretName: supersecret
```
```yaml tab="Secret" ```yaml tab="Secret"
apiVersion: v1 apiVersion: v1
kind: Secret kind: Secret
@ -900,18 +905,6 @@ and will connect via TLS automatically.
Ingresses can be created that look like the following: Ingresses can be created that look like the following:
```yaml tab="Ingress" ```yaml tab="Ingress"
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: cheese
spec:
defaultBackend:
serviceName: stilton
serverPort: 80
```
```yaml tab="Ingress Kubernetes v1.19+"
apiVersion: networking.k8s.io/v1 apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata: metadata:
@ -925,6 +918,18 @@ spec:
number: 80 number: 80
``` ```
```yaml tab="Ingress v1beta1 (deprecated)"
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: cheese
spec:
defaultBackend:
serviceName: stilton
serverPort: 80
```
This ingress follows the Global Default Backend property of ingresses. This ingress follows the Global Default Backend property of ingresses.
This will allow users to create a "default router" that will match all unmatched requests. This will allow users to create a "default router" that will match all unmatched requests.

View file

@ -251,6 +251,7 @@ The table below lists all the available matchers:
`HostRegexp` and `Path` accept an expression with zero or more groups enclosed by curly braces. `HostRegexp` and `Path` accept an expression with zero or more groups enclosed by curly braces.
Named groups can be like `{name:pattern}` that matches the given regexp pattern or like `{name}` that matches anything until the next dot. Named groups can be like `{name:pattern}` that matches the given regexp pattern or like `{name}` that matches anything until the next dot.
The group name (`name` is the above examples) is an arbitrary value.
Any pattern supported by [Go's regexp package](https://golang.org/pkg/regexp/) may be used (example: `{subdomain:[a-z]+}.{domain}.com`). Any pattern supported by [Go's regexp package](https://golang.org/pkg/regexp/) may be used (example: `{subdomain:[a-z]+}.{domain}.com`).
!!! info "Combining Matchers Using Operators and Parenthesis" !!! info "Combining Matchers Using Operators and Parenthesis"

View file

@ -842,7 +842,7 @@ spec:
info configuration. info configuration.
properties: properties:
issuer: issuer:
description: TLSCLientCertificateIssuerDNInfo holds the client description: TLSClientCertificateIssuerDNInfo holds the client
TLS certificate distinguished name info configuration. cf TLS certificate distinguished name info configuration. cf
https://tools.ietf.org/html/rfc3739 https://tools.ietf.org/html/rfc3739
properties: properties:
@ -870,7 +870,7 @@ spec:
serialNumber: serialNumber:
type: boolean type: boolean
subject: subject:
description: TLSCLientCertificateSubjectDNInfo holds the client description: TLSClientCertificateSubjectDNInfo holds the client
TLS certificate distinguished name info configuration. cf TLS certificate distinguished name info configuration. cf
https://tools.ietf.org/html/rfc3739 https://tools.ietf.org/html/rfc3739
properties: properties:

View file

@ -272,7 +272,7 @@ func TestDo_dynamicConfiguration(t *testing.T) {
}, },
ForwardAuth: &dynamic.ForwardAuth{ ForwardAuth: &dynamic.ForwardAuth{
Address: "127.0.0.1", Address: "127.0.0.1",
TLS: &dynamic.ClientTLS{ TLS: &types.ClientTLS{
CA: "ca.pem", CA: "ca.pem",
CAOptional: true, CAOptional: true,
Cert: "cert.pem", Cert: "cert.pem",
@ -314,7 +314,7 @@ func TestDo_dynamicConfiguration(t *testing.T) {
NotAfter: true, NotAfter: true,
NotBefore: true, NotBefore: true,
Sans: true, Sans: true,
Subject: &dynamic.TLSCLientCertificateSubjectDNInfo{ Subject: &dynamic.TLSClientCertificateSubjectDNInfo{
Country: true, Country: true,
Province: true, Province: true,
Locality: true, Locality: true,
@ -324,7 +324,7 @@ func TestDo_dynamicConfiguration(t *testing.T) {
SerialNumber: true, SerialNumber: true,
DomainComponent: true, DomainComponent: true,
}, },
Issuer: &dynamic.TLSCLientCertificateIssuerDNInfo{ Issuer: &dynamic.TLSClientCertificateIssuerDNInfo{
Country: true, Country: true,
Province: true, Province: true,
Locality: true, Locality: true,

View file

@ -1,14 +1,11 @@
package dynamic package dynamic
import ( import (
"crypto/tls"
"crypto/x509"
"fmt"
"os"
"time" "time"
ptypes "github.com/traefik/paerser/types" ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v2/pkg/ip" "github.com/traefik/traefik/v2/pkg/ip"
"github.com/traefik/traefik/v2/pkg/types"
) )
// +k8s:deepcopy-gen=true // +k8s:deepcopy-gen=true
@ -131,12 +128,12 @@ type ErrorPage struct {
// ForwardAuth holds the http forward authentication configuration. // ForwardAuth holds the http forward authentication configuration.
type ForwardAuth struct { type ForwardAuth struct {
Address string `json:"address,omitempty" toml:"address,omitempty" yaml:"address,omitempty"` Address string `json:"address,omitempty" toml:"address,omitempty" yaml:"address,omitempty"`
TLS *ClientTLS `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"` TLS *types.ClientTLS `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"`
TrustForwardHeader bool `json:"trustForwardHeader,omitempty" toml:"trustForwardHeader,omitempty" yaml:"trustForwardHeader,omitempty" export:"true"` TrustForwardHeader bool `json:"trustForwardHeader,omitempty" toml:"trustForwardHeader,omitempty" yaml:"trustForwardHeader,omitempty" export:"true"`
AuthResponseHeaders []string `json:"authResponseHeaders,omitempty" toml:"authResponseHeaders,omitempty" yaml:"authResponseHeaders,omitempty" export:"true"` AuthResponseHeaders []string `json:"authResponseHeaders,omitempty" toml:"authResponseHeaders,omitempty" yaml:"authResponseHeaders,omitempty" export:"true"`
AuthResponseHeadersRegex string `json:"authResponseHeadersRegex,omitempty" toml:"authResponseHeadersRegex,omitempty" yaml:"authResponseHeadersRegex,omitempty" export:"true"` AuthResponseHeadersRegex string `json:"authResponseHeadersRegex,omitempty" toml:"authResponseHeadersRegex,omitempty" yaml:"authResponseHeadersRegex,omitempty" export:"true"`
AuthRequestHeaders []string `json:"authRequestHeaders,omitempty" toml:"authRequestHeaders,omitempty" yaml:"authRequestHeaders,omitempty" export:"true"` AuthRequestHeaders []string `json:"authRequestHeaders,omitempty" toml:"authRequestHeaders,omitempty" yaml:"authRequestHeaders,omitempty" export:"true"`
} }
// +k8s:deepcopy-gen=true // +k8s:deepcopy-gen=true
@ -403,16 +400,16 @@ type TLSClientCertificateInfo struct {
NotAfter bool `json:"notAfter,omitempty" toml:"notAfter,omitempty" yaml:"notAfter,omitempty" export:"true"` NotAfter bool `json:"notAfter,omitempty" toml:"notAfter,omitempty" yaml:"notAfter,omitempty" export:"true"`
NotBefore bool `json:"notBefore,omitempty" toml:"notBefore,omitempty" yaml:"notBefore,omitempty" export:"true"` NotBefore bool `json:"notBefore,omitempty" toml:"notBefore,omitempty" yaml:"notBefore,omitempty" export:"true"`
Sans bool `json:"sans,omitempty" toml:"sans,omitempty" yaml:"sans,omitempty" export:"true"` Sans bool `json:"sans,omitempty" toml:"sans,omitempty" yaml:"sans,omitempty" export:"true"`
Subject *TLSCLientCertificateSubjectDNInfo `json:"subject,omitempty" toml:"subject,omitempty" yaml:"subject,omitempty" export:"true"` Subject *TLSClientCertificateSubjectDNInfo `json:"subject,omitempty" toml:"subject,omitempty" yaml:"subject,omitempty" export:"true"`
Issuer *TLSCLientCertificateIssuerDNInfo `json:"issuer,omitempty" toml:"issuer,omitempty" yaml:"issuer,omitempty" export:"true"` Issuer *TLSClientCertificateIssuerDNInfo `json:"issuer,omitempty" toml:"issuer,omitempty" yaml:"issuer,omitempty" export:"true"`
SerialNumber bool `json:"serialNumber,omitempty" toml:"serialNumber,omitempty" yaml:"serialNumber,omitempty" export:"true"` SerialNumber bool `json:"serialNumber,omitempty" toml:"serialNumber,omitempty" yaml:"serialNumber,omitempty" export:"true"`
} }
// +k8s:deepcopy-gen=true // +k8s:deepcopy-gen=true
// TLSCLientCertificateIssuerDNInfo holds the client TLS certificate distinguished name info configuration. // TLSClientCertificateIssuerDNInfo holds the client TLS certificate distinguished name info configuration.
// cf https://tools.ietf.org/html/rfc3739 // cf https://tools.ietf.org/html/rfc3739
type TLSCLientCertificateIssuerDNInfo struct { type TLSClientCertificateIssuerDNInfo struct {
Country bool `json:"country,omitempty" toml:"country,omitempty" yaml:"country,omitempty" export:"true"` Country bool `json:"country,omitempty" toml:"country,omitempty" yaml:"country,omitempty" export:"true"`
Province bool `json:"province,omitempty" toml:"province,omitempty" yaml:"province,omitempty" export:"true"` Province bool `json:"province,omitempty" toml:"province,omitempty" yaml:"province,omitempty" export:"true"`
Locality bool `json:"locality,omitempty" toml:"locality,omitempty" yaml:"locality,omitempty" export:"true"` Locality bool `json:"locality,omitempty" toml:"locality,omitempty" yaml:"locality,omitempty" export:"true"`
@ -424,9 +421,9 @@ type TLSCLientCertificateIssuerDNInfo struct {
// +k8s:deepcopy-gen=true // +k8s:deepcopy-gen=true
// TLSCLientCertificateSubjectDNInfo holds the client TLS certificate distinguished name info configuration. // TLSClientCertificateSubjectDNInfo holds the client TLS certificate distinguished name info configuration.
// cf https://tools.ietf.org/html/rfc3739 // cf https://tools.ietf.org/html/rfc3739
type TLSCLientCertificateSubjectDNInfo struct { type TLSClientCertificateSubjectDNInfo struct {
Country bool `json:"country,omitempty" toml:"country,omitempty" yaml:"country,omitempty" export:"true"` Country bool `json:"country,omitempty" toml:"country,omitempty" yaml:"country,omitempty" export:"true"`
Province bool `json:"province,omitempty" toml:"province,omitempty" yaml:"province,omitempty" export:"true"` Province bool `json:"province,omitempty" toml:"province,omitempty" yaml:"province,omitempty" export:"true"`
Locality bool `json:"locality,omitempty" toml:"locality,omitempty" yaml:"locality,omitempty" export:"true"` Locality bool `json:"locality,omitempty" toml:"locality,omitempty" yaml:"locality,omitempty" export:"true"`
@ -441,83 +438,3 @@ type TLSCLientCertificateSubjectDNInfo struct {
// Users holds a list of users. // Users holds a list of users.
type Users []string type Users []string
// +k8s:deepcopy-gen=true
// ClientTLS holds the TLS specific configurations as client
// CA, Cert and Key can be either path or file contents.
type ClientTLS struct {
CA string `json:"ca,omitempty" toml:"ca,omitempty" yaml:"ca,omitempty"`
CAOptional bool `json:"caOptional,omitempty" toml:"caOptional,omitempty" yaml:"caOptional,omitempty" export:"true"`
Cert string `json:"cert,omitempty" toml:"cert,omitempty" yaml:"cert,omitempty"`
Key string `json:"key,omitempty" toml:"key,omitempty" yaml:"key,omitempty"`
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty" toml:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty" export:"true"`
}
// CreateTLSConfig creates a TLS config from ClientTLS structures.
func (c *ClientTLS) CreateTLSConfig() (*tls.Config, error) {
if c == nil {
return nil, nil
}
var err error
caPool := x509.NewCertPool()
clientAuth := tls.NoClientCert
if c.CA != "" {
var ca []byte
if _, errCA := os.Stat(c.CA); errCA == nil {
ca, err = os.ReadFile(c.CA)
if err != nil {
return nil, fmt.Errorf("failed to read CA. %w", err)
}
} else {
ca = []byte(c.CA)
}
if !caPool.AppendCertsFromPEM(ca) {
return nil, fmt.Errorf("failed to parse CA")
}
if c.CAOptional {
clientAuth = tls.VerifyClientCertIfGiven
} else {
clientAuth = tls.RequireAndVerifyClientCert
}
}
cert := tls.Certificate{}
_, errKeyIsFile := os.Stat(c.Key)
if !c.InsecureSkipVerify && (len(c.Cert) == 0 || len(c.Key) == 0) {
return nil, fmt.Errorf("TLS Certificate or Key file must be set when TLS configuration is created")
}
if len(c.Cert) > 0 && len(c.Key) > 0 {
if _, errCertIsFile := os.Stat(c.Cert); errCertIsFile == nil {
if errKeyIsFile == nil {
cert, err = tls.LoadX509KeyPair(c.Cert, c.Key)
if err != nil {
return nil, fmt.Errorf("failed to load TLS keypair: %w", err)
}
} else {
return nil, fmt.Errorf("tls cert is a file, but tls key is not")
}
} else {
if errKeyIsFile != nil {
cert, err = tls.X509KeyPair([]byte(c.Cert), []byte(c.Key))
if err != nil {
return nil, fmt.Errorf("failed to load TLS keypair: %w", err)
}
} else {
return nil, fmt.Errorf("TLS key is a file, but tls cert is not")
}
}
}
return &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caPool,
InsecureSkipVerify: c.InsecureSkipVerify,
ClientAuth: clientAuth,
}, nil
}

View file

@ -124,22 +124,6 @@ func (in *CircuitBreaker) DeepCopy() *CircuitBreaker {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ClientTLS) DeepCopyInto(out *ClientTLS) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientTLS.
func (in *ClientTLS) DeepCopy() *ClientTLS {
if in == nil {
return nil
}
out := new(ClientTLS)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Compress) DeepCopyInto(out *Compress) { func (in *Compress) DeepCopyInto(out *Compress) {
*out = *in *out = *in
@ -306,7 +290,7 @@ func (in *ForwardAuth) DeepCopyInto(out *ForwardAuth) {
*out = *in *out = *in
if in.TLS != nil { if in.TLS != nil {
in, out := &in.TLS, &out.TLS in, out := &in.TLS, &out.TLS
*out = new(ClientTLS) *out = new(types.ClientTLS)
**out = **in **out = **in
} }
if in.AuthResponseHeaders != nil { if in.AuthResponseHeaders != nil {
@ -1535,49 +1519,17 @@ func (in *TCPWeightedRoundRobin) DeepCopy() *TCPWeightedRoundRobin {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TLSCLientCertificateIssuerDNInfo) DeepCopyInto(out *TLSCLientCertificateIssuerDNInfo) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSCLientCertificateIssuerDNInfo.
func (in *TLSCLientCertificateIssuerDNInfo) DeepCopy() *TLSCLientCertificateIssuerDNInfo {
if in == nil {
return nil
}
out := new(TLSCLientCertificateIssuerDNInfo)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TLSCLientCertificateSubjectDNInfo) DeepCopyInto(out *TLSCLientCertificateSubjectDNInfo) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSCLientCertificateSubjectDNInfo.
func (in *TLSCLientCertificateSubjectDNInfo) DeepCopy() *TLSCLientCertificateSubjectDNInfo {
if in == nil {
return nil
}
out := new(TLSCLientCertificateSubjectDNInfo)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TLSClientCertificateInfo) DeepCopyInto(out *TLSClientCertificateInfo) { func (in *TLSClientCertificateInfo) DeepCopyInto(out *TLSClientCertificateInfo) {
*out = *in *out = *in
if in.Subject != nil { if in.Subject != nil {
in, out := &in.Subject, &out.Subject in, out := &in.Subject, &out.Subject
*out = new(TLSCLientCertificateSubjectDNInfo) *out = new(TLSClientCertificateSubjectDNInfo)
**out = **in **out = **in
} }
if in.Issuer != nil { if in.Issuer != nil {
in, out := &in.Issuer, &out.Issuer in, out := &in.Issuer, &out.Issuer
*out = new(TLSCLientCertificateIssuerDNInfo) *out = new(TLSClientCertificateIssuerDNInfo)
**out = **in **out = **in
} }
return return
@ -1593,6 +1545,38 @@ func (in *TLSClientCertificateInfo) DeepCopy() *TLSClientCertificateInfo {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TLSClientCertificateIssuerDNInfo) DeepCopyInto(out *TLSClientCertificateIssuerDNInfo) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSClientCertificateIssuerDNInfo.
func (in *TLSClientCertificateIssuerDNInfo) DeepCopy() *TLSClientCertificateIssuerDNInfo {
if in == nil {
return nil
}
out := new(TLSClientCertificateIssuerDNInfo)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TLSClientCertificateSubjectDNInfo) DeepCopyInto(out *TLSClientCertificateSubjectDNInfo) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSClientCertificateSubjectDNInfo.
func (in *TLSClientCertificateSubjectDNInfo) DeepCopy() *TLSClientCertificateSubjectDNInfo {
if in == nil {
return nil
}
out := new(TLSClientCertificateSubjectDNInfo)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TLSConfiguration) DeepCopyInto(out *TLSConfiguration) { func (in *TLSConfiguration) DeepCopyInto(out *TLSConfiguration) {
*out = *in *out = *in

View file

@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
ptypes "github.com/traefik/paerser/types" ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/types"
) )
func TestDecodeConfiguration(t *testing.T) { func TestDecodeConfiguration(t *testing.T) {
@ -367,7 +368,7 @@ func TestDecodeConfiguration(t *testing.T) {
NotAfter: true, NotAfter: true,
NotBefore: true, NotBefore: true,
SerialNumber: true, SerialNumber: true,
Subject: &dynamic.TLSCLientCertificateSubjectDNInfo{ Subject: &dynamic.TLSClientCertificateSubjectDNInfo{
Country: true, Country: true,
Province: true, Province: true,
Locality: true, Locality: true,
@ -377,7 +378,7 @@ func TestDecodeConfiguration(t *testing.T) {
SerialNumber: true, SerialNumber: true,
DomainComponent: true, DomainComponent: true,
}, },
Issuer: &dynamic.TLSCLientCertificateIssuerDNInfo{ Issuer: &dynamic.TLSClientCertificateIssuerDNInfo{
Country: true, Country: true,
Province: true, Province: true,
Locality: true, Locality: true,
@ -505,7 +506,7 @@ func TestDecodeConfiguration(t *testing.T) {
"Middleware7": { "Middleware7": {
ForwardAuth: &dynamic.ForwardAuth{ ForwardAuth: &dynamic.ForwardAuth{
Address: "foobar", Address: "foobar",
TLS: &dynamic.ClientTLS{ TLS: &types.ClientTLS{
CA: "foobar", CA: "foobar",
CAOptional: true, CAOptional: true,
Cert: "foobar", Cert: "foobar",
@ -848,7 +849,7 @@ func TestEncodeConfiguration(t *testing.T) {
NotAfter: true, NotAfter: true,
NotBefore: true, NotBefore: true,
SerialNumber: true, SerialNumber: true,
Subject: &dynamic.TLSCLientCertificateSubjectDNInfo{ Subject: &dynamic.TLSClientCertificateSubjectDNInfo{
Country: true, Country: true,
Province: true, Province: true,
Locality: true, Locality: true,
@ -858,7 +859,7 @@ func TestEncodeConfiguration(t *testing.T) {
SerialNumber: true, SerialNumber: true,
DomainComponent: true, DomainComponent: true,
}, },
Issuer: &dynamic.TLSCLientCertificateIssuerDNInfo{ Issuer: &dynamic.TLSClientCertificateIssuerDNInfo{
Country: true, Country: true,
Province: true, Province: true,
Locality: true, Locality: true,
@ -993,7 +994,7 @@ func TestEncodeConfiguration(t *testing.T) {
"Middleware7": { "Middleware7": {
ForwardAuth: &dynamic.ForwardAuth{ ForwardAuth: &dynamic.ForwardAuth{
Address: "foobar", Address: "foobar",
TLS: &dynamic.ClientTLS{ TLS: &types.ClientTLS{
CA: "foobar", CA: "foobar",
CAOptional: true, CAOptional: true,
Cert: "foobar", Cert: "foobar",

View file

@ -2,7 +2,6 @@ package accesslog
import ( import (
"net/http" "net/http"
"os"
"testing" "testing"
"time" "time"
@ -84,7 +83,7 @@ func TestCommonLogFormatter_Format(t *testing.T) {
} }
// Set timezone to Etc/GMT+9 to have a constant behavior // Set timezone to Etc/GMT+9 to have a constant behavior
os.Setenv("TZ", "Etc/GMT+9") t.Setenv("TZ", "Etc/GMT+9")
for _, test := range testCases { for _, test := range testCases {
test := test test := test

View file

@ -72,9 +72,9 @@ func NewForward(ctx context.Context, next http.Handler, config dynamic.ForwardAu
} }
if config.TLS != nil { if config.TLS != nil {
tlsConfig, err := config.TLS.CreateTLSConfig() tlsConfig, err := config.TLS.CreateTLSConfig(ctx)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("unable to create client TLS configuration: %w", err)
} }
tr := http.DefaultTransport.(*http.Transport).Clone() tr := http.DefaultTransport.(*http.Transport).Clone()

View file

@ -48,7 +48,7 @@ type IssuerDistinguishedNameOptions struct {
StateOrProvinceName bool StateOrProvinceName bool
} }
func newIssuerDistinguishedNameOptions(info *dynamic.TLSCLientCertificateIssuerDNInfo) *IssuerDistinguishedNameOptions { func newIssuerDistinguishedNameOptions(info *dynamic.TLSClientCertificateIssuerDNInfo) *IssuerDistinguishedNameOptions {
if info == nil { if info == nil {
return nil return nil
} }
@ -78,7 +78,7 @@ type SubjectDistinguishedNameOptions struct {
StateOrProvinceName bool StateOrProvinceName bool
} }
func newSubjectDistinguishedNameOptions(info *dynamic.TLSCLientCertificateSubjectDNInfo) *SubjectDistinguishedNameOptions { func newSubjectDistinguishedNameOptions(info *dynamic.TLSClientCertificateSubjectDNInfo) *SubjectDistinguishedNameOptions {
if info == nil { if info == nil {
return nil return nil
} }

View file

@ -376,7 +376,7 @@ func TestPassTLSClientCert_certInfo(t *testing.T) {
desc: "No TLS, with subject info", desc: "No TLS, with subject info",
config: dynamic.PassTLSClientCert{ config: dynamic.PassTLSClientCert{
Info: &dynamic.TLSClientCertificateInfo{ Info: &dynamic.TLSClientCertificateInfo{
Subject: &dynamic.TLSCLientCertificateSubjectDNInfo{ Subject: &dynamic.TLSClientCertificateSubjectDNInfo{
CommonName: true, CommonName: true,
Organization: true, Organization: true,
OrganizationalUnit: true, OrganizationalUnit: true,
@ -393,7 +393,7 @@ func TestPassTLSClientCert_certInfo(t *testing.T) {
config: dynamic.PassTLSClientCert{ config: dynamic.PassTLSClientCert{
PEM: false, PEM: false,
Info: &dynamic.TLSClientCertificateInfo{ Info: &dynamic.TLSClientCertificateInfo{
Subject: &dynamic.TLSCLientCertificateSubjectDNInfo{}, Subject: &dynamic.TLSClientCertificateSubjectDNInfo{},
}, },
}, },
}, },
@ -406,7 +406,7 @@ func TestPassTLSClientCert_certInfo(t *testing.T) {
NotBefore: true, NotBefore: true,
Sans: true, Sans: true,
SerialNumber: true, SerialNumber: true,
Subject: &dynamic.TLSCLientCertificateSubjectDNInfo{ Subject: &dynamic.TLSClientCertificateSubjectDNInfo{
CommonName: true, CommonName: true,
Country: true, Country: true,
DomainComponent: true, DomainComponent: true,
@ -416,7 +416,7 @@ func TestPassTLSClientCert_certInfo(t *testing.T) {
Province: true, Province: true,
SerialNumber: true, SerialNumber: true,
}, },
Issuer: &dynamic.TLSCLientCertificateIssuerDNInfo{ Issuer: &dynamic.TLSClientCertificateIssuerDNInfo{
CommonName: true, CommonName: true,
Country: true, Country: true,
DomainComponent: true, DomainComponent: true,
@ -436,10 +436,10 @@ func TestPassTLSClientCert_certInfo(t *testing.T) {
Info: &dynamic.TLSClientCertificateInfo{ Info: &dynamic.TLSClientCertificateInfo{
NotAfter: true, NotAfter: true,
Sans: true, Sans: true,
Subject: &dynamic.TLSCLientCertificateSubjectDNInfo{ Subject: &dynamic.TLSClientCertificateSubjectDNInfo{
Organization: true, Organization: true,
}, },
Issuer: &dynamic.TLSCLientCertificateIssuerDNInfo{ Issuer: &dynamic.TLSClientCertificateIssuerDNInfo{
Country: true, Country: true,
}, },
}, },
@ -453,13 +453,13 @@ func TestPassTLSClientCert_certInfo(t *testing.T) {
Info: &dynamic.TLSClientCertificateInfo{ Info: &dynamic.TLSClientCertificateInfo{
NotAfter: true, NotAfter: true,
Sans: true, Sans: true,
Subject: &dynamic.TLSCLientCertificateSubjectDNInfo{ Subject: &dynamic.TLSClientCertificateSubjectDNInfo{
Organization: true, Organization: true,
// OrganizationalUnit is not set on this example certificate, // OrganizationalUnit is not set on this example certificate,
// so even though it's requested, it will be absent. // so even though it's requested, it will be absent.
OrganizationalUnit: true, OrganizationalUnit: true,
}, },
Issuer: &dynamic.TLSCLientCertificateIssuerDNInfo{ Issuer: &dynamic.TLSClientCertificateIssuerDNInfo{
Country: true, Country: true,
}, },
}, },
@ -475,7 +475,7 @@ func TestPassTLSClientCert_certInfo(t *testing.T) {
NotBefore: true, NotBefore: true,
Sans: true, Sans: true,
SerialNumber: true, SerialNumber: true,
Subject: &dynamic.TLSCLientCertificateSubjectDNInfo{ Subject: &dynamic.TLSClientCertificateSubjectDNInfo{
Country: true, Country: true,
Province: true, Province: true,
Locality: true, Locality: true,
@ -485,7 +485,7 @@ func TestPassTLSClientCert_certInfo(t *testing.T) {
SerialNumber: true, SerialNumber: true,
DomainComponent: true, DomainComponent: true,
}, },
Issuer: &dynamic.TLSCLientCertificateIssuerDNInfo{ Issuer: &dynamic.TLSClientCertificateIssuerDNInfo{
Country: true, Country: true,
Province: true, Province: true,
Locality: true, Locality: true,
@ -507,7 +507,7 @@ func TestPassTLSClientCert_certInfo(t *testing.T) {
NotBefore: true, NotBefore: true,
Sans: true, Sans: true,
SerialNumber: true, SerialNumber: true,
Subject: &dynamic.TLSCLientCertificateSubjectDNInfo{ Subject: &dynamic.TLSClientCertificateSubjectDNInfo{
Country: true, Country: true,
Province: true, Province: true,
Locality: true, Locality: true,
@ -517,7 +517,7 @@ func TestPassTLSClientCert_certInfo(t *testing.T) {
SerialNumber: true, SerialNumber: true,
DomainComponent: true, DomainComponent: true,
}, },
Issuer: &dynamic.TLSCLientCertificateIssuerDNInfo{ Issuer: &dynamic.TLSClientCertificateIssuerDNInfo{
Country: true, Country: true,
Province: true, Province: true,
Locality: true, Locality: true,

View file

@ -41,12 +41,12 @@ func (r *replacePath) GetTracingInformation() (string, ext.SpanKindEnum) {
} }
func (r *replacePath) ServeHTTP(rw http.ResponseWriter, req *http.Request) { func (r *replacePath) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if req.URL.RawPath == "" { currentPath := req.URL.RawPath
req.Header.Add(ReplacedPathHeader, req.URL.Path) if currentPath == "" {
} else { currentPath = req.URL.EscapedPath()
req.Header.Add(ReplacedPathHeader, req.URL.RawPath)
} }
req.Header.Add(ReplacedPathHeader, currentPath)
req.URL.RawPath = r.path req.URL.RawPath = r.path
var err error var err error

View file

@ -60,6 +60,16 @@ func TestReplacePath(t *testing.T) {
expectedRawPath: "/foo%2Fbar", expectedRawPath: "/foo%2Fbar",
expectedHeader: "/path", expectedHeader: "/path",
}, },
{
desc: "replacement with percent encoded backspace char",
path: "/path/%08bar",
config: dynamic.ReplacePath{
Path: "/path/%08bar",
},
expectedPath: "/path/\bbar",
expectedRawPath: "/path/%08bar",
expectedHeader: "/path/%08bar",
},
} }
for _, test := range testCases { for _, test := range testCases {

View file

@ -16,9 +16,7 @@ import (
"github.com/traefik/traefik/v2/pkg/tracing" "github.com/traefik/traefik/v2/pkg/tracing"
) )
const ( const typeName = "ReplacePathRegex"
typeName = "ReplacePathRegex"
)
// ReplacePathRegex is a middleware used to replace the path of a URL request with a regular expression. // ReplacePathRegex is a middleware used to replace the path of a URL request with a regular expression.
type replacePathRegex struct { type replacePathRegex struct {
@ -50,16 +48,13 @@ func (rp *replacePathRegex) GetTracingInformation() (string, ext.SpanKindEnum) {
} }
func (rp *replacePathRegex) ServeHTTP(rw http.ResponseWriter, req *http.Request) { func (rp *replacePathRegex) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
var currentPath string currentPath := req.URL.RawPath
if req.URL.RawPath == "" { if currentPath == "" {
currentPath = req.URL.Path currentPath = req.URL.EscapedPath()
} else {
currentPath = req.URL.RawPath
} }
if rp.regexp != nil && len(rp.replacement) > 0 && rp.regexp.MatchString(currentPath) { if rp.regexp != nil && len(rp.replacement) > 0 && rp.regexp.MatchString(currentPath) {
req.Header.Add(replacepath.ReplacedPathHeader, currentPath) req.Header.Add(replacepath.ReplacedPathHeader, currentPath)
req.URL.RawPath = rp.regexp.ReplaceAllString(currentPath, rp.replacement) req.URL.RawPath = rp.regexp.ReplaceAllString(currentPath, rp.replacement)
// as replacement can introduce escaped characters // as replacement can introduce escaped characters

View file

@ -106,6 +106,16 @@ func TestReplacePathRegex(t *testing.T) {
expectedPath: "/aaa/bbb", expectedPath: "/aaa/bbb",
expectedRawPath: "/aaa%2Fbbb", expectedRawPath: "/aaa%2Fbbb",
}, },
{
desc: "path with percent encoded backspace char",
path: "/foo/%08bar",
config: dynamic.ReplacePathRegex{
Replacement: "/$1",
Regex: `^/foo/(.*)`,
},
expectedPath: "/\bbar",
expectedRawPath: "/%08bar",
},
} }
for _, test := range testCases { for _, test := range testCases {

View file

@ -165,7 +165,7 @@ func (p *Provider) getClientOpts() ([]client.Opt, error) {
conf, err := p.TLS.CreateTLSConfig(ctx) conf, err := p.TLS.CreateTLSConfig(ctx)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("unable to create client TLS configuration: %w", err)
} }
hostURL, err := client.ParseHostURL(p.Endpoint) hostURL, err := client.ParseHostURL(p.Endpoint)

View file

@ -55,7 +55,7 @@ func (p *Provider) Init() error {
if p.TLS != nil { if p.TLS != nil {
tlsConfig, err := p.TLS.CreateTLSConfig(context.Background()) tlsConfig, err := p.TLS.CreateTLSConfig(context.Background())
if err != nil { if err != nil {
return fmt.Errorf("unable to create TLS configuration: %w", err) return fmt.Errorf("unable to create client TLS configuration: %w", err)
} }
p.httpClient.Transport = &http.Transport{ p.httpClient.Transport = &http.Transport{

View file

@ -0,0 +1,29 @@
apiVersion: traefik.containo.us/v1alpha1
kind: MiddlewareTCP
metadata:
name: multiple---hyphens
namespace: default
spec:
ipWhiteList:
sourceRange:
- 127.0.0.1/32
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRouteTCP
metadata:
name: test.route
namespace: default
spec:
entryPoints:
- foo
routes:
- match: HostSNI(`foo.com`)
services:
- name: whoamitcp
port: 8000
middlewares:
- name: multiple---hyphens

View file

@ -0,0 +1,31 @@
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: multiple---hyphens
namespace: default
spec:
stripPrefix:
prefixes:
- /tobestripped
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: test2.route
namespace: default
spec:
entryPoints:
- web
routes:
- match: Host(`foo.com`) && PathPrefix(`/tobestripped`)
priority: 12
kind: Rule
services:
- name: whoami
port: 80
middlewares:
- name: multiple---hyphens

View file

@ -25,6 +25,7 @@ import (
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1" "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1"
"github.com/traefik/traefik/v2/pkg/safe" "github.com/traefik/traefik/v2/pkg/safe"
"github.com/traefik/traefik/v2/pkg/tls" "github.com/traefik/traefik/v2/pkg/tls"
"github.com/traefik/traefik/v2/pkg/types"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
apiextensionv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensionv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
@ -483,7 +484,7 @@ func createForwardAuthMiddleware(k8sClient Client, namespace string, auth *v1alp
return forwardAuth, nil return forwardAuth, nil
} }
forwardAuth.TLS = &dynamic.ClientTLS{ forwardAuth.TLS = &types.ClientTLS{
CAOptional: auth.TLS.CAOptional, CAOptional: auth.TLS.CAOptional,
InsecureSkipVerify: auth.TLS.InsecureSkipVerify, InsecureSkipVerify: auth.TLS.InsecureSkipVerify,
} }

View file

@ -183,7 +183,7 @@ func (p *Provider) makeMiddlewareKeys(ctx context.Context, ingRouteNamespace str
ns = mi.Namespace ns = mi.Namespace
} }
mds = append(mds, makeID(ns, name)) mds = append(mds, provider.Normalize(makeID(ns, name)))
} }
return mds, nil return mds, nil

View file

@ -10,6 +10,7 @@ import (
"github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/log" "github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/provider"
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1" "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1"
"github.com/traefik/traefik/v2/pkg/tls" "github.com/traefik/traefik/v2/pkg/tls"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@ -162,7 +163,7 @@ func (p *Provider) makeMiddlewareTCPKeys(ctx context.Context, ingRouteTCPNamespa
ns = mi.Namespace ns = mi.Namespace
} }
mds = append(mds, makeID(ns, mi.Name)) mds = append(mds, provider.Normalize(makeID(ns, mi.Name)))
} }
return mds, nil return mds, nil

View file

@ -11,13 +11,14 @@ import (
auth "github.com/abbot/go-http-auth" auth "github.com/abbot/go-http-auth"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/traefik/paerser/types" ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/provider" "github.com/traefik/traefik/v2/pkg/provider"
crdfake "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/generated/clientset/versioned/fake" crdfake "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/generated/clientset/versioned/fake"
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1" "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1"
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/k8s" "github.com/traefik/traefik/v2/pkg/provider/kubernetes/k8s"
"github.com/traefik/traefik/v2/pkg/tls" "github.com/traefik/traefik/v2/pkg/tls"
"github.com/traefik/traefik/v2/pkg/types"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/intstr"
@ -151,6 +152,54 @@ func TestLoadIngressRouteTCPs(t *testing.T) {
TLS: &dynamic.TLSConfiguration{}, TLS: &dynamic.TLSConfiguration{},
}, },
}, },
{
desc: "Middlewares in ingress route config are normalized",
paths: []string{"tcp/services.yml", "tcp/with_middleware_multiple_hyphens.yml"},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{
"default-test.route-fdd3e9338e47a45efefc": {
EntryPoints: []string{"foo"},
Service: "default-test.route-fdd3e9338e47a45efefc",
Middlewares: []string{"default-multiple-hyphens"},
Rule: "HostSNI(`foo.com`)",
},
},
Middlewares: map[string]*dynamic.TCPMiddleware{
"default-multiple-hyphens": {
IPWhiteList: &dynamic.TCPIPWhiteList{
SourceRange: []string{"127.0.0.1/32"},
},
},
},
Services: map[string]*dynamic.TCPService{
"default-test.route-fdd3e9338e47a45efefc": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{
Address: "10.10.0.1:8000",
},
{
Address: "10.10.0.2:8000",
},
},
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{ {
desc: "Simple Ingress Route, with foo entrypoint and crossprovider middleware", desc: "Simple Ingress Route, with foo entrypoint and crossprovider middleware",
paths: []string{"tcp/services.yml", "tcp/with_middleware_crossprovider.yml"}, paths: []string{"tcp/services.yml", "tcp/with_middleware_crossprovider.yml"},
@ -1458,6 +1507,57 @@ func TestLoadIngressRoutes(t *testing.T) {
TLS: &dynamic.TLSConfiguration{}, TLS: &dynamic.TLSConfiguration{},
}, },
}, },
{
desc: "Middlewares in ingress route config are normalized",
AllowCrossNamespace: true,
paths: []string{"services.yml", "with_middleware_multiple_hyphens.yml"},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-test2-route-23c7f4c450289ee29016": {
EntryPoints: []string{"web"},
Service: "default-test2-route-23c7f4c450289ee29016",
Rule: "Host(`foo.com`) && PathPrefix(`/tobestripped`)",
Priority: 12,
Middlewares: []string{"default-multiple-hyphens"},
},
},
Middlewares: map[string]*dynamic.Middleware{
"default-multiple-hyphens": {
StripPrefix: &dynamic.StripPrefix{
Prefixes: []string{"/tobestripped"},
},
},
},
Services: map[string]*dynamic.Service{
"default-test2-route-23c7f4c450289ee29016": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: Bool(true),
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{ {
desc: "Simple Ingress Route with middleware crossprovider", desc: "Simple Ingress Route with middleware crossprovider",
AllowCrossNamespace: true, AllowCrossNamespace: true,
@ -3146,7 +3246,7 @@ func TestLoadIngressRoutes(t *testing.T) {
"default-forwardauth": { "default-forwardauth": {
ForwardAuth: &dynamic.ForwardAuth{ ForwardAuth: &dynamic.ForwardAuth{
Address: "test.com", Address: "test.com",
TLS: &dynamic.ClientTLS{ TLS: &types.ClientTLS{
CA: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----", CA: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----",
Cert: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----", Cert: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----",
Key: "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----", Key: "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----",
@ -3515,17 +3615,17 @@ func TestLoadIngressRoutes(t *testing.T) {
MaxIdleConnsPerHost: 42, MaxIdleConnsPerHost: 42,
DisableHTTP2: true, DisableHTTP2: true,
ForwardingTimeouts: &dynamic.ForwardingTimeouts{ ForwardingTimeouts: &dynamic.ForwardingTimeouts{
DialTimeout: types.Duration(42 * time.Second), DialTimeout: ptypes.Duration(42 * time.Second),
ResponseHeaderTimeout: types.Duration(42 * time.Second), ResponseHeaderTimeout: ptypes.Duration(42 * time.Second),
IdleConnTimeout: types.Duration(42 * time.Millisecond), IdleConnTimeout: ptypes.Duration(42 * time.Millisecond),
}, },
PeerCertURI: "foo://bar", PeerCertURI: "foo://bar",
}, },
"default-test": { "default-test": {
ServerName: "test", ServerName: "test",
ForwardingTimeouts: &dynamic.ForwardingTimeouts{ ForwardingTimeouts: &dynamic.ForwardingTimeouts{
DialTimeout: types.Duration(30 * time.Second), DialTimeout: ptypes.Duration(30 * time.Second),
IdleConnTimeout: types.Duration(90 * time.Second), IdleConnTimeout: ptypes.Duration(90 * time.Second),
}, },
}, },
}, },

View file

@ -14,7 +14,7 @@ import (
func MustParseYaml(content []byte) []runtime.Object { func MustParseYaml(content []byte) []runtime.Object {
acceptedK8sTypes := regexp.MustCompile(`^(Namespace|Deployment|Endpoints|Service|Ingress|IngressRoute|IngressRouteTCP|IngressRouteUDP|Middleware|MiddlewareTCP|Secret|TLSOption|TLSStore|TraefikService|IngressClass|ServersTransport|GatewayClass|Gateway|HTTPRoute|TCPRoute|TLSRoute)$`) acceptedK8sTypes := regexp.MustCompile(`^(Namespace|Deployment|Endpoints|Service|Ingress|IngressRoute|IngressRouteTCP|IngressRouteUDP|Middleware|MiddlewareTCP|Secret|TLSOption|TLSStore|TraefikService|IngressClass|ServersTransport|GatewayClass|Gateway|HTTPRoute|TCPRoute|TLSRoute)$`)
files := strings.Split(string(content), "---") files := strings.Split(string(content), "---\n")
retVal := make([]runtime.Object, 0, len(files)) retVal := make([]runtime.Object, 0, len(files))
for _, file := range files { for _, file := range files {
if file == "\n" || file == "" { if file == "\n" || file == "" {

View file

@ -170,7 +170,7 @@ func (p *Provider) createKVClient(ctx context.Context) (store.Store, error) {
var err error var err error
storeConfig.TLS, err = p.TLS.CreateTLSConfig(ctx) storeConfig.TLS, err = p.TLS.CreateTLSConfig(ctx)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("unable to create client TLS configuration: %w", err)
} }
} }

View file

@ -402,7 +402,7 @@ func Test_buildConfiguration(t *testing.T) {
"Middleware08": { "Middleware08": {
ForwardAuth: &dynamic.ForwardAuth{ ForwardAuth: &dynamic.ForwardAuth{
Address: "foobar", Address: "foobar",
TLS: &dynamic.ClientTLS{ TLS: &types.ClientTLS{
CA: "foobar", CA: "foobar",
CAOptional: true, CAOptional: true,
Cert: "foobar", Cert: "foobar",
@ -481,7 +481,7 @@ func Test_buildConfiguration(t *testing.T) {
NotAfter: true, NotAfter: true,
NotBefore: true, NotBefore: true,
Sans: true, Sans: true,
Subject: &dynamic.TLSCLientCertificateSubjectDNInfo{ Subject: &dynamic.TLSClientCertificateSubjectDNInfo{
Country: true, Country: true,
Province: true, Province: true,
Locality: true, Locality: true,
@ -491,7 +491,7 @@ func Test_buildConfiguration(t *testing.T) {
SerialNumber: true, SerialNumber: true,
DomainComponent: true, DomainComponent: true,
}, },
Issuer: &dynamic.TLSCLientCertificateIssuerDNInfo{ Issuer: &dynamic.TLSClientCertificateIssuerDNInfo{
Country: true, Country: true,
Province: true, Province: true,
Locality: true, Locality: true,

View file

@ -134,7 +134,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
} }
TLSConfig, err := p.TLS.CreateTLSConfig(ctx) TLSConfig, err := p.TLS.CreateTLSConfig(ctx)
if err != nil { if err != nil {
return err return fmt.Errorf("unable to create client TLS configuration: %w", err)
} }
confg.HTTPClient = &http.Client{ confg.HTTPClient = &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{

View file

@ -15,7 +15,7 @@ type server struct {
// WRRLoadBalancer is a naive RoundRobin load balancer for TCP services. // WRRLoadBalancer is a naive RoundRobin load balancer for TCP services.
type WRRLoadBalancer struct { type WRRLoadBalancer struct {
servers []server servers []server
lock sync.RWMutex lock sync.Mutex
currentWeight int currentWeight int
index int index int
} }
@ -29,16 +29,16 @@ func NewWRRLoadBalancer() *WRRLoadBalancer {
// ServeTCP forwards the connection to the right service. // ServeTCP forwards the connection to the right service.
func (b *WRRLoadBalancer) ServeTCP(conn WriteCloser) { func (b *WRRLoadBalancer) ServeTCP(conn WriteCloser) {
if len(b.servers) == 0 { b.lock.Lock()
log.WithoutContext().Error("no available server")
return
}
next, err := b.next() next, err := b.next()
b.lock.Unlock()
if err != nil { if err != nil {
log.WithoutContext().Errorf("Error during load balancing: %v", err) log.WithoutContext().Errorf("Error during load balancing: %v", err)
conn.Close() conn.Close()
return
} }
next.ServeTCP(conn) next.ServeTCP(conn)
} }
@ -50,6 +50,9 @@ func (b *WRRLoadBalancer) AddServer(serverHandler Handler) {
// AddWeightServer appends a server to the existing list with a weight. // AddWeightServer appends a server to the existing list with a weight.
func (b *WRRLoadBalancer) AddWeightServer(serverHandler Handler, weight *int) { func (b *WRRLoadBalancer) AddWeightServer(serverHandler Handler, weight *int) {
b.lock.Lock()
defer b.lock.Unlock()
w := 1 w := 1
if weight != nil { if weight != nil {
w = *weight w = *weight
@ -87,9 +90,6 @@ func gcd(a, b int) int {
} }
func (b *WRRLoadBalancer) next() (Handler, error) { func (b *WRRLoadBalancer) next() (Handler, error) {
b.lock.Lock()
defer b.lock.Unlock()
if len(b.servers) == 0 { if len(b.servers) == 0 {
return nil, fmt.Errorf("no servers in the pool") return nil, fmt.Errorf("no servers in the pool")
} }
@ -98,10 +98,14 @@ func (b *WRRLoadBalancer) next() (Handler, error) {
// it calculates the GCD and subtracts it on every iteration, what interleaves servers // it calculates the GCD and subtracts it on every iteration, what interleaves servers
// and allows us not to build an iterator every time we readjust weights // and allows us not to build an iterator every time we readjust weights
// GCD across all enabled servers
gcd := b.weightGcd()
// Maximum weight across all enabled servers // Maximum weight across all enabled servers
max := b.maxWeight() max := b.maxWeight()
if max == 0 {
return nil, fmt.Errorf("all servers have 0 weight")
}
// GCD across all enabled servers
gcd := b.weightGcd()
for { for {
b.index = (b.index + 1) % len(b.servers) b.index = (b.index + 1) % len(b.servers)
@ -109,9 +113,6 @@ func (b *WRRLoadBalancer) next() (Handler, error) {
b.currentWeight -= gcd b.currentWeight -= gcd
if b.currentWeight <= 0 { if b.currentWeight <= 0 {
b.currentWeight = max b.currentWeight = max
if b.currentWeight == 0 {
return nil, fmt.Errorf("all servers have 0 weight")
}
} }
} }
srv := b.servers[b.index] srv := b.servers[b.index]

View file

@ -10,7 +10,8 @@ import (
) )
type fakeConn struct { type fakeConn struct {
call map[string]int writeCall map[string]int
closeCall int
} }
func (f *fakeConn) Read(b []byte) (n int, err error) { func (f *fakeConn) Read(b []byte) (n int, err error) {
@ -18,12 +19,13 @@ func (f *fakeConn) Read(b []byte) (n int, err error) {
} }
func (f *fakeConn) Write(b []byte) (n int, err error) { func (f *fakeConn) Write(b []byte) (n int, err error) {
f.call[string(b)]++ f.writeCall[string(b)]++
return len(b), nil return len(b), nil
} }
func (f *fakeConn) Close() error { func (f *fakeConn) Close() error {
panic("implement me") f.closeCall++
return nil
} }
func (f *fakeConn) LocalAddr() net.Addr { func (f *fakeConn) LocalAddr() net.Addr {
@ -55,7 +57,8 @@ func TestLoadBalancing(t *testing.T) {
desc string desc string
serversWeight map[string]int serversWeight map[string]int
totalCall int totalCall int
expected map[string]int expectedWrite map[string]int
expectedClose int
}{ }{
{ {
desc: "RoundRobin", desc: "RoundRobin",
@ -64,7 +67,7 @@ func TestLoadBalancing(t *testing.T) {
"h2": 1, "h2": 1,
}, },
totalCall: 4, totalCall: 4,
expected: map[string]int{ expectedWrite: map[string]int{
"h1": 2, "h1": 2,
"h2": 2, "h2": 2,
}, },
@ -76,7 +79,7 @@ func TestLoadBalancing(t *testing.T) {
"h2": 1, "h2": 1,
}, },
totalCall: 4, totalCall: 4,
expected: map[string]int{ expectedWrite: map[string]int{
"h1": 3, "h1": 3,
"h2": 1, "h2": 1,
}, },
@ -88,22 +91,33 @@ func TestLoadBalancing(t *testing.T) {
"h2": 1, "h2": 1,
}, },
totalCall: 16, totalCall: 16,
expected: map[string]int{ expectedWrite: map[string]int{
"h1": 12, "h1": 12,
"h2": 4, "h2": 4,
}, },
}, },
{ {
desc: "WeighedRoundRobin with 0 weight server", desc: "WeighedRoundRobin with one 0 weight server",
serversWeight: map[string]int{ serversWeight: map[string]int{
"h1": 3, "h1": 3,
"h2": 0, "h2": 0,
}, },
totalCall: 16, totalCall: 16,
expected: map[string]int{ expectedWrite: map[string]int{
"h1": 16, "h1": 16,
}, },
}, },
{
desc: "WeighedRoundRobin with all servers with 0 weight",
serversWeight: map[string]int{
"h1": 0,
"h2": 0,
"h3": 0,
},
totalCall: 10,
expectedWrite: map[string]int{},
expectedClose: 10,
},
} }
for _, test := range testCases { for _, test := range testCases {
@ -120,12 +134,13 @@ func TestLoadBalancing(t *testing.T) {
}), &weight) }), &weight)
} }
conn := &fakeConn{call: make(map[string]int)} conn := &fakeConn{writeCall: make(map[string]int)}
for i := 0; i < test.totalCall; i++ { for i := 0; i < test.totalCall; i++ {
balancer.ServeTCP(conn) balancer.ServeTCP(conn)
} }
assert.Equal(t, test.expected, conn.call) assert.Equal(t, test.expectedWrite, conn.writeCall)
assert.Equal(t, test.expectedClose, conn.closeCall)
}) })
} }
} }

View file

@ -0,0 +1,13 @@
-----BEGIN CERTIFICATE-----
MIIB9jCCAV+gAwIBAgIQI3edJckNbicw4WIHs5Ws9TANBgkqhkiG9w0BAQsFADAS
MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw
MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
iQKBgQCb8oWyME1QRBoMLFei3M8TVKwfZfW74cVjtcugCBMTTOTCouEIgjjmiMv6
FdMio2uBcgeD9R3dOtjjnA7N+xjwZ4vIPqDlJRE3YbfpV9igVX3sXU7ssHTSH0vs
R0TuYJwGReIFUnu5QIjGwVorodF+CQ8dTnyXVLeQVU9kvjohHwIDAQABo0swSTAO
BgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIw
ADAUBgNVHREEDTALgglsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADgYEADqylUQ/4
lrxh4h8UUQ2wKATQ2kG2YvMGlaIhr2vPZo2QDBlmL2xzai7YXX3+JZyM15TNCamn
WtFR7WQIOHzKA1GkR9WkaXKmFbJjhGMSZVCG6ghhTjzB+stBYZXhBsdjCJbkZWBu
OeI73oivo0MdI+4iCYCo7TnoY4PZGObwcgI=
-----END CERTIFICATE-----

View file

@ -0,0 +1,16 @@
-----BEGIN PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAJvyhbIwTVBEGgws
V6LczxNUrB9l9bvhxWO1y6AIExNM5MKi4QiCOOaIy/oV0yKja4FyB4P1Hd062OOc
Ds37GPBni8g+oOUlETdht+lX2KBVfexdTuywdNIfS+xHRO5gnAZF4gVSe7lAiMbB
Wiuh0X4JDx1OfJdUt5BVT2S+OiEfAgMBAAECgYA9+PbghQl0aFvhko2RDybLi86K
+73X2DTVFx3AjvTlqp0OLCQ5eWabVqmYzKuHDGJgoqwR6Irhq80dRpsriCm0YNui
mMV35bbimOKz9FoCTKx0ZB6xsqrVoFhjVmX3DOD9Txe41H42ZxmccOKZndR/QaXz
VV+1W/Wbz2VawnkyYQJBAMvF6w2eOJRRoN8e7GM7b7uqkupJPp9axgFREoJZb16W
mqXUZnH4Cydzc5keG4yknQRHdgz6RrQxnvR7GyKHLfUCQQDD6qG9D5BX0+mNW6TG
PRwW/L2qWgnmg9lxtSSQat9ZOnBhw2OLPi0zTu4p70oSmU67/YJr50HEoJpRccZJ
mnJDAkBdBTtY2xpe8qhqUjZ80hweYi5wzwDMQ+bRoQ2+/U6usjdkbgJaEm4dE0H4
6tqOqHKZCnokUHfIOEKkvjHT4DulAkBAgiJNSTGi6aDOLa28pGR6YS/mRo1Z/HH9
kcJ/VuFB1Q8p8Zb2QzvI2CVtY2AFbbtSBPALrXKnVqZZSNgcZiFXAkEAvcLKaEXE
haGMGwq2BLADPHqAR3hdCJL3ikMJwWUsTkTjm973iEIEZfF5j57EzRI4bASm4Zq5
Zt3BcblLODQ//w==
-----END PRIVATE KEY-----

View file

@ -4,12 +4,15 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"errors"
"fmt" "fmt"
"os" "os"
"github.com/traefik/traefik/v2/pkg/log" "github.com/traefik/traefik/v2/pkg/log"
) )
// +k8s:deepcopy-gen=true
// ClientTLS holds TLS specific configurations as client // ClientTLS holds TLS specific configurations as client
// CA, Cert and Key can be either path or file contents. // CA, Cert and Key can be either path or file contents.
type ClientTLS struct { type ClientTLS struct {
@ -27,7 +30,9 @@ func (clientTLS *ClientTLS) CreateTLSConfig(ctx context.Context) (*tls.Config, e
return nil, nil return nil, nil
} }
caPool := x509.NewCertPool() // Not initialized, to rely on system bundle.
var caPool *x509.CertPool
clientAuth := tls.NoClientCert clientAuth := tls.NoClientCert
if clientTLS.CA != "" { if clientTLS.CA != "" {
var ca []byte var ca []byte
@ -41,8 +46,9 @@ func (clientTLS *ClientTLS) CreateTLSConfig(ctx context.Context) (*tls.Config, e
ca = []byte(clientTLS.CA) ca = []byte(clientTLS.CA)
} }
caPool = x509.NewCertPool()
if !caPool.AppendCertsFromPEM(ca) { if !caPool.AppendCertsFromPEM(ca) {
return nil, fmt.Errorf("failed to parse CA") return nil, errors.New("failed to parse CA")
} }
if clientTLS.CAOptional { if clientTLS.CAOptional {
@ -52,34 +58,24 @@ func (clientTLS *ClientTLS) CreateTLSConfig(ctx context.Context) (*tls.Config, e
} }
} }
if !clientTLS.InsecureSkipVerify && (len(clientTLS.Cert) == 0 || len(clientTLS.Key) == 0) { hasCert := len(clientTLS.Cert) > 0
return nil, fmt.Errorf("TLS Certificate or Key file must be set when TLS configuration is created") hasKey := len(clientTLS.Key) > 0
if hasCert != hasKey {
return nil, errors.New("both TLS cert and key must be defined")
} }
cert := tls.Certificate{} if !hasCert || !hasKey {
_, errKeyIsFile := os.Stat(clientTLS.Key) return &tls.Config{
RootCAs: caPool,
InsecureSkipVerify: clientTLS.InsecureSkipVerify,
ClientAuth: clientAuth,
}, nil
}
if len(clientTLS.Cert) > 0 && len(clientTLS.Key) > 0 { cert, err := loadKeyPair(clientTLS.Cert, clientTLS.Key)
var err error if err != nil {
if _, errCertIsFile := os.Stat(clientTLS.Cert); errCertIsFile == nil { return nil, err
if errKeyIsFile == nil {
cert, err = tls.LoadX509KeyPair(clientTLS.Cert, clientTLS.Key)
if err != nil {
return nil, fmt.Errorf("failed to load TLS keypair: %w", err)
}
} else {
return nil, fmt.Errorf("TLS cert is a file, but tls key is not")
}
} else {
if errKeyIsFile != nil {
cert, err = tls.X509KeyPair([]byte(clientTLS.Cert), []byte(clientTLS.Key))
if err != nil {
return nil, fmt.Errorf("failed to load TLS keypair: %w", err)
}
} else {
return nil, fmt.Errorf("TLS key is a file, but tls cert is not")
}
}
} }
return &tls.Config{ return &tls.Config{
@ -89,3 +85,27 @@ func (clientTLS *ClientTLS) CreateTLSConfig(ctx context.Context) (*tls.Config, e
ClientAuth: clientAuth, ClientAuth: clientAuth,
}, nil }, nil
} }
func loadKeyPair(cert, key string) (tls.Certificate, error) {
keyPair, err := tls.X509KeyPair([]byte(cert), []byte(key))
if err == nil {
return keyPair, nil
}
_, err = os.Stat(cert)
if err != nil {
return tls.Certificate{}, errors.New("cert file does not exist")
}
_, err = os.Stat(key)
if err != nil {
return tls.Certificate{}, errors.New("key file does not exist")
}
keyPair, err = tls.LoadX509KeyPair(cert, key)
if err != nil {
return tls.Certificate{}, err
}
return keyPair, nil
}

129
pkg/types/tls_test.go Normal file
View file

@ -0,0 +1,129 @@
package types
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// go run $GOROOT/src/crypto/tls/generate_cert.go --rsa-bits 1024 --host localhost --start-date "Jan 1 00:00:00 1970" --duration=1000000h
var cert = `-----BEGIN CERTIFICATE-----
MIIB9jCCAV+gAwIBAgIQI3edJckNbicw4WIHs5Ws9TANBgkqhkiG9w0BAQsFADAS
MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw
MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
iQKBgQCb8oWyME1QRBoMLFei3M8TVKwfZfW74cVjtcugCBMTTOTCouEIgjjmiMv6
FdMio2uBcgeD9R3dOtjjnA7N+xjwZ4vIPqDlJRE3YbfpV9igVX3sXU7ssHTSH0vs
R0TuYJwGReIFUnu5QIjGwVorodF+CQ8dTnyXVLeQVU9kvjohHwIDAQABo0swSTAO
BgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIw
ADAUBgNVHREEDTALgglsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADgYEADqylUQ/4
lrxh4h8UUQ2wKATQ2kG2YvMGlaIhr2vPZo2QDBlmL2xzai7YXX3+JZyM15TNCamn
WtFR7WQIOHzKA1GkR9WkaXKmFbJjhGMSZVCG6ghhTjzB+stBYZXhBsdjCJbkZWBu
OeI73oivo0MdI+4iCYCo7TnoY4PZGObwcgI=
-----END CERTIFICATE-----`
var key = `-----BEGIN PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAJvyhbIwTVBEGgws
V6LczxNUrB9l9bvhxWO1y6AIExNM5MKi4QiCOOaIy/oV0yKja4FyB4P1Hd062OOc
Ds37GPBni8g+oOUlETdht+lX2KBVfexdTuywdNIfS+xHRO5gnAZF4gVSe7lAiMbB
Wiuh0X4JDx1OfJdUt5BVT2S+OiEfAgMBAAECgYA9+PbghQl0aFvhko2RDybLi86K
+73X2DTVFx3AjvTlqp0OLCQ5eWabVqmYzKuHDGJgoqwR6Irhq80dRpsriCm0YNui
mMV35bbimOKz9FoCTKx0ZB6xsqrVoFhjVmX3DOD9Txe41H42ZxmccOKZndR/QaXz
VV+1W/Wbz2VawnkyYQJBAMvF6w2eOJRRoN8e7GM7b7uqkupJPp9axgFREoJZb16W
mqXUZnH4Cydzc5keG4yknQRHdgz6RrQxnvR7GyKHLfUCQQDD6qG9D5BX0+mNW6TG
PRwW/L2qWgnmg9lxtSSQat9ZOnBhw2OLPi0zTu4p70oSmU67/YJr50HEoJpRccZJ
mnJDAkBdBTtY2xpe8qhqUjZ80hweYi5wzwDMQ+bRoQ2+/U6usjdkbgJaEm4dE0H4
6tqOqHKZCnokUHfIOEKkvjHT4DulAkBAgiJNSTGi6aDOLa28pGR6YS/mRo1Z/HH9
kcJ/VuFB1Q8p8Zb2QzvI2CVtY2AFbbtSBPALrXKnVqZZSNgcZiFXAkEAvcLKaEXE
haGMGwq2BLADPHqAR3hdCJL3ikMJwWUsTkTjm973iEIEZfF5j57EzRI4bASm4Zq5
Zt3BcblLODQ//w==
-----END PRIVATE KEY-----`
func TestClientTLS_CreateTLSConfig(t *testing.T) {
tests := []struct {
desc string
clientTLS ClientTLS
wantCertLen int
wantCALen int
wantErr bool
}{
{
desc: "Configure CA",
clientTLS: ClientTLS{CA: cert},
wantCALen: 1,
wantErr: false,
},
{
desc: "Configure the client keyPair from strings",
clientTLS: ClientTLS{Cert: cert, Key: key},
wantCertLen: 1,
wantErr: false,
},
{
desc: "Configure the client keyPair from files",
clientTLS: ClientTLS{Cert: "fixtures/cert.pem", Key: "fixtures/key.pem"},
wantCertLen: 1,
wantErr: false,
},
{
desc: "Configure InsecureSkipVerify",
clientTLS: ClientTLS{InsecureSkipVerify: true},
wantErr: false,
},
{
desc: "Return an error if only the client cert is provided",
clientTLS: ClientTLS{Cert: cert},
wantErr: true,
},
{
desc: "Return an error if only the client key is provided",
clientTLS: ClientTLS{Key: key},
wantErr: true,
},
{
desc: "Return an error if only the client cert is of type file",
clientTLS: ClientTLS{Cert: "fixtures/cert.pem", Key: key},
wantErr: true,
},
{
desc: "Return an error if only the client key is of type file",
clientTLS: ClientTLS{Cert: cert, Key: "fixtures/key.pem"},
wantErr: true,
},
{
desc: "Return an error if the client cert does not exist",
clientTLS: ClientTLS{Cert: "fixtures/cert2.pem", Key: "fixtures/key.pem"},
wantErr: true,
},
{
desc: "Return an error if the client key does not exist",
clientTLS: ClientTLS{Cert: "fixtures/cert.pem", Key: "fixtures/key2.pem"},
wantErr: true,
},
}
for _, test := range tests {
test := test
t.Run(test.desc, func(t *testing.T) {
tlsConfig, err := test.clientTLS.CreateTLSConfig(context.Background())
if test.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Len(t, tlsConfig.Certificates, test.wantCertLen)
assert.Equal(t, test.clientTLS.InsecureSkipVerify, tlsConfig.InsecureSkipVerify)
if test.wantCALen > 0 {
assert.Len(t, tlsConfig.RootCAs.Subjects(), test.wantCALen)
return
}
assert.Nil(t, tlsConfig.RootCAs)
})
}
}

View file

@ -29,6 +29,22 @@ THE SOFTWARE.
package types package types
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ClientTLS) DeepCopyInto(out *ClientTLS) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientTLS.
func (in *ClientTLS) DeepCopy() *ClientTLS {
if in == nil {
return nil
}
out := new(ClientTLS)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Domain) DeepCopyInto(out *Domain) { func (in *Domain) DeepCopyInto(out *Domain) {
*out = *in *out = *in

View file

@ -15,7 +15,7 @@ type server struct {
// WRRLoadBalancer is a naive RoundRobin load balancer for UDP services. // WRRLoadBalancer is a naive RoundRobin load balancer for UDP services.
type WRRLoadBalancer struct { type WRRLoadBalancer struct {
servers []server servers []server
lock sync.RWMutex lock sync.Mutex
currentWeight int currentWeight int
index int index int
} }
@ -29,16 +29,16 @@ func NewWRRLoadBalancer() *WRRLoadBalancer {
// ServeUDP forwards the connection to the right service. // ServeUDP forwards the connection to the right service.
func (b *WRRLoadBalancer) ServeUDP(conn *Conn) { func (b *WRRLoadBalancer) ServeUDP(conn *Conn) {
if len(b.servers) == 0 { b.lock.Lock()
log.WithoutContext().Error("no available server")
return
}
next, err := b.next() next, err := b.next()
b.lock.Unlock()
if err != nil { if err != nil {
log.WithoutContext().Errorf("Error during load balancing: %v", err) log.WithoutContext().Errorf("Error during load balancing: %v", err)
conn.Close() conn.Close()
return
} }
next.ServeUDP(conn) next.ServeUDP(conn)
} }
@ -50,6 +50,9 @@ func (b *WRRLoadBalancer) AddServer(serverHandler Handler) {
// AddWeightedServer appends a handler to the existing list with a weight. // AddWeightedServer appends a handler to the existing list with a weight.
func (b *WRRLoadBalancer) AddWeightedServer(serverHandler Handler, weight *int) { func (b *WRRLoadBalancer) AddWeightedServer(serverHandler Handler, weight *int) {
b.lock.Lock()
defer b.lock.Unlock()
w := 1 w := 1
if weight != nil { if weight != nil {
w = *weight w = *weight
@ -87,9 +90,6 @@ func gcd(a, b int) int {
} }
func (b *WRRLoadBalancer) next() (Handler, error) { func (b *WRRLoadBalancer) next() (Handler, error) {
b.lock.Lock()
defer b.lock.Unlock()
if len(b.servers) == 0 { if len(b.servers) == 0 {
return nil, fmt.Errorf("no servers in the pool") return nil, fmt.Errorf("no servers in the pool")
} }
@ -98,10 +98,14 @@ func (b *WRRLoadBalancer) next() (Handler, error) {
// but is actually very simple it calculates the GCD and subtracts it on every iteration, // but is actually very simple it calculates the GCD and subtracts it on every iteration,
// what interleaves servers and allows us not to build an iterator every time we readjust weights. // what interleaves servers and allows us not to build an iterator every time we readjust weights.
// GCD across all enabled servers
gcd := b.weightGcd()
// Maximum weight across all enabled servers // Maximum weight across all enabled servers
max := b.maxWeight() max := b.maxWeight()
if max == 0 {
return nil, fmt.Errorf("all servers have 0 weight")
}
// GCD across all enabled servers
gcd := b.weightGcd()
for { for {
b.index = (b.index + 1) % len(b.servers) b.index = (b.index + 1) % len(b.servers)
@ -109,9 +113,6 @@ func (b *WRRLoadBalancer) next() (Handler, error) {
b.currentWeight -= gcd b.currentWeight -= gcd
if b.currentWeight <= 0 { if b.currentWeight <= 0 {
b.currentWeight = max b.currentWeight = max
if b.currentWeight == 0 {
return nil, fmt.Errorf("all servers have 0 weight")
}
} }
} }
srv := b.servers[b.index] srv := b.servers[b.index]

View file

@ -4,11 +4,11 @@ RepositoryName = "traefik"
OutputType = "file" OutputType = "file"
FileName = "traefik_changelog.md" FileName = "traefik_changelog.md"
# example new bugfix v2.5.3 # example new bugfix v2.5.4
CurrentRef = "v2.5" CurrentRef = "v2.5"
PreviousRef = "v2.5.2" PreviousRef = "v2.5.3"
BaseBranch = "v2.5" BaseBranch = "v2.5"
FutureCurrentRefName = "v2.5.3" FutureCurrentRefName = "v2.5.4"
ThresholdPreviousRef = 10 ThresholdPreviousRef = 10
ThresholdCurrentRef = 10 ThresholdCurrentRef = 10

View file

@ -12,10 +12,10 @@ then
# The shellcheck command are run in background, to have an overview of the linter (instead of a fail at first issue) # The shellcheck command are run in background, to have an overview of the linter (instead of a fail at first issue)
shellcheck "${script_to_check}" & shellcheck "${script_to_check}" &
done < <( # Search all the repository for sh and bash shebangs, excluding .js and .md files done < <( # Search all the repository for sh and bash shebangs, excluding .js and .md files
# the folders ".git" and "vendor" are also ignored # the folders ".git", "vendor" and "node_modules" are also ignored
grep -rI '#!/' "${script_dir}"/.. \ grep -rI '#!/' "${script_dir}"/.. \
| grep 'sh' | grep -v '.js' | grep -v '.md' \ | grep 'sh' | grep -v '.js' | grep -v '.md' \
| grep -v '.git/' | grep -v 'vendor/' \ | grep -v '.git/' | grep -v 'vendor/' | grep -v 'node_modules/' \
| cut -d':' -f1 | cut -d':' -f1
) )
wait # Wait for all background command to be completed wait # Wait for all background command to be completed

View file

@ -64,7 +64,7 @@ export default {
}, },
getProviderLogoPath (service) { getProviderLogoPath (service) {
const provider = this.getProvider(service) const provider = this.getProvider(service)
const name = provider.name.toLowerCase() const name = provider.toLowerCase()
if (name.includes('plugin-')) { if (name.includes('plugin-')) {
return 'statics/providers/plugin.svg' return 'statics/providers/plugin.svg'