Add Redis rate limiter
This commit is contained in:
parent
c166a41c99
commit
550d96ea67
26 changed files with 2268 additions and 69 deletions
|
@ -496,3 +496,718 @@ http:
|
||||||
[http.middlewares.test-ratelimit.rateLimit.sourceCriterion]
|
[http.middlewares.test-ratelimit.rateLimit.sourceCriterion]
|
||||||
requestHost = true
|
requestHost = true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `redis`
|
||||||
|
|
||||||
|
Enables distributed rate limit using `redis` to store the tokens.
|
||||||
|
If not set, Traefik's in-memory storage is used by default.
|
||||||
|
|
||||||
|
#### `redis.endpoints`
|
||||||
|
|
||||||
|
_Required, Default="127.0.0.1:6379"_
|
||||||
|
|
||||||
|
Defines how to connect to the Redis server.
|
||||||
|
|
||||||
|
```yaml tab="Docker & Swarm"
|
||||||
|
labels:
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.endpoints=127.0.0.1:6379"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Kubernetes"
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: test-ratelimit
|
||||||
|
spec:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
endpoints:
|
||||||
|
- "127.0.0.1:6379"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Consul Catalog"
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.endpoints=127.0.0.1:6379"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
test-ratelimit:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
endpoints:
|
||||||
|
- "127.0.0.1:6379"
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[http.middlewares]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit.redis]
|
||||||
|
endpoints = ["127.0.0.1:6379"]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `redis.username`
|
||||||
|
|
||||||
|
_Optional, Default=""_
|
||||||
|
|
||||||
|
Defines the username used to authenticate with the Redis server.
|
||||||
|
|
||||||
|
```yaml tab="Docker & Swarm"
|
||||||
|
labels:
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.username=user"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Kubernetes"
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: test-ratelimit
|
||||||
|
spec:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
secret: mysecret
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: mysecret
|
||||||
|
namespace: default
|
||||||
|
|
||||||
|
data:
|
||||||
|
username: dXNlcm5hbWU=
|
||||||
|
password: cGFzc3dvcmQ=
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Consul Catalog"
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.username=user"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
test-ratelimit:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
username: user
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[http.middlewares]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit.redis]
|
||||||
|
username = "user"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `redis.password`
|
||||||
|
|
||||||
|
_Optional, Default=""_
|
||||||
|
|
||||||
|
Defines the password to authenticate against the Redis server.
|
||||||
|
|
||||||
|
```yaml tab="Docker & Swarm"
|
||||||
|
labels:
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.password=password"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Kubernetes"
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: test-ratelimit
|
||||||
|
spec:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
secret: mysecret
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: mysecret
|
||||||
|
namespace: default
|
||||||
|
|
||||||
|
data:
|
||||||
|
username: dXNlcm5hbWU=
|
||||||
|
password: cGFzc3dvcmQ=
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Consul Catalog"
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.password=password"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
test-ratelimit:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
password: password
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[http.middlewares]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit.redis]
|
||||||
|
password = "password"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `redis.db`
|
||||||
|
|
||||||
|
_Optional, Default=0_
|
||||||
|
|
||||||
|
Defines the database to select after connecting to the Redis.
|
||||||
|
|
||||||
|
```yaml tab="Docker & Swarm"
|
||||||
|
labels:
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.db=0"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Kubernetes"
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: test-ratelimit
|
||||||
|
spec:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
db: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Consul Catalog"
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.db=0"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
test-ratelimit:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
db: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[http.middlewares]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit.redis]
|
||||||
|
db = 0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `redis.tls`
|
||||||
|
|
||||||
|
Same as this [config](https://doc.traefik.io/traefik/providers/redis/#tls)
|
||||||
|
|
||||||
|
_Optional_
|
||||||
|
|
||||||
|
Defines the TLS configuration used for the secure connection to Redis.
|
||||||
|
|
||||||
|
##### `redis.tls.ca`
|
||||||
|
|
||||||
|
_Optional_
|
||||||
|
|
||||||
|
`ca` is the path to the certificate authority used for the secure connection to Redis,
|
||||||
|
it defaults to the system bundle.
|
||||||
|
|
||||||
|
```yaml tab="Docker & Swarm"
|
||||||
|
labels:
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.tls.ca=path/to/ca.crt"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Kubernetes"
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: test-ratelimit
|
||||||
|
spec:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
tls:
|
||||||
|
caSecret: mycasercret
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: mycasercret
|
||||||
|
namespace: default
|
||||||
|
|
||||||
|
data:
|
||||||
|
# Must contain a certificate under either a `tls.ca` or a `ca.crt` key.
|
||||||
|
tls.ca: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Consul Catalog"
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.tls.ca=path/to/ca.crt"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
tls:
|
||||||
|
ca: path/to/ca.crt
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.redis.tls]
|
||||||
|
ca = "path/to/ca.crt"
|
||||||
|
```
|
||||||
|
|
||||||
|
##### `redis.tls.cert`
|
||||||
|
|
||||||
|
_Optional_
|
||||||
|
|
||||||
|
`cert` is the path to the public certificate used for the secure connection to Redis.
|
||||||
|
When this option is set, the `key` option is required.
|
||||||
|
|
||||||
|
```yaml tab="Docker & Swarm"
|
||||||
|
labels:
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.tls.cert=path/to/foo.cert"
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.tls.key=path/to/foo.key"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Kubernetes"
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: test-ratelimit
|
||||||
|
spec:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
tls:
|
||||||
|
certSecret: mytlscert
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: mytlscert
|
||||||
|
namespace: default
|
||||||
|
|
||||||
|
data:
|
||||||
|
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
|
||||||
|
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Consul Catalog"
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.tls.cert=path/to/foo.cert"
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.tls.key=path/to/foo.key"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
test-ratelimit:
|
||||||
|
rateLimit:
|
||||||
|
redis:
|
||||||
|
tls:
|
||||||
|
cert: path/to/foo.cert
|
||||||
|
key: path/to/foo.key
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[http.middlewares]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit.redis]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit.redis.tls]
|
||||||
|
cert = "path/to/foo.cert"
|
||||||
|
key = "path/to/foo.key"
|
||||||
|
```
|
||||||
|
|
||||||
|
##### `redis.tls.key`
|
||||||
|
|
||||||
|
_Optional_
|
||||||
|
|
||||||
|
`key` is the path to the private key used for the secure connection to Redis.
|
||||||
|
When this option is set, the `cert` option is required.
|
||||||
|
|
||||||
|
```yaml tab="Docker & Swarm"
|
||||||
|
labels:
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.tls.cert=path/to/foo.cert"
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.tls.key=path/to/foo.key"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Kubernetes"
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: test-ratelimit
|
||||||
|
spec:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
tls:
|
||||||
|
certSecret: mytlscert
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: mytlscert
|
||||||
|
namespace: default
|
||||||
|
|
||||||
|
data:
|
||||||
|
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
|
||||||
|
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Consul Catalog"
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.tls.cert=path/to/foo.cert"
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.tls.key=path/to/foo.key"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
test-ratelimit:
|
||||||
|
rateLimit:
|
||||||
|
redis:
|
||||||
|
tls:
|
||||||
|
cert: path/to/foo.cert
|
||||||
|
key: path/to/foo.key
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[http.middlewares]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit.redis]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit.redis.tls]
|
||||||
|
cert = "path/to/foo.cert"
|
||||||
|
key = "path/to/foo.key"
|
||||||
|
```
|
||||||
|
|
||||||
|
##### `redis.tls.insecureSkipVerify`
|
||||||
|
|
||||||
|
_Optional, Default=false_
|
||||||
|
|
||||||
|
If `insecureSkipVerify` is `true`, the TLS connection to Redis accepts any certificate presented by the server regardless of the hostnames it covers.
|
||||||
|
|
||||||
|
```yaml tab="Docker & Swarm"
|
||||||
|
labels:
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.tls.insecureSkipVerify=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Kubernetes"
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: test-ratelimit
|
||||||
|
spec:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
tls:
|
||||||
|
insecureSkipVerify: true
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Consul Catalog"
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.tls.insecureSkipVerify=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
test-ratelimit:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
tls:
|
||||||
|
insecureSkipVerify: true
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[http.middlewares]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit.redis]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit.redis.tls]
|
||||||
|
insecureSkipVerify = true
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `redis.poolSize`
|
||||||
|
|
||||||
|
_Optional, Default=0_
|
||||||
|
|
||||||
|
Defines the base number of socket connections.
|
||||||
|
|
||||||
|
If there are not enough connections in the pool, new connections will be allocated beyond `redis.poolSize`.
|
||||||
|
You can limit this using `redis.maxActiveConns`.
|
||||||
|
|
||||||
|
Zero means 10 connections per every available CPU as reported by runtime.GOMAXPROCS.
|
||||||
|
|
||||||
|
```yaml tab="Docker & Swarm"
|
||||||
|
labels:
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.poolSize=42"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Kubernetes"
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: test-ratelimit
|
||||||
|
spec:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
poolSize: 42
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Consul Catalog"
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.poolSize=42"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
test-ratelimit:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
poolSize: 42
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[http.middlewares]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit.redis]
|
||||||
|
poolSize = 42
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `redis.minIdleConns`
|
||||||
|
|
||||||
|
_Optional, Default=0_
|
||||||
|
|
||||||
|
Defines the minimum number of idle connections, which is useful when establishing new connections is slow.
|
||||||
|
Zero means that idle connections are not closed.
|
||||||
|
|
||||||
|
```yaml tab="Docker & Swarm"
|
||||||
|
labels:
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.minIdleConns=42"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Kubernetes"
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: test-ratelimit
|
||||||
|
spec:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
minIdleConns: 42
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Consul Catalog"
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.minIdleConns=42"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
test-ratelimit:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
minIdleConns: 42
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[http.middlewares]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit.redis]
|
||||||
|
minIdleConns = 42
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `redis.maxActiveConns`
|
||||||
|
|
||||||
|
_Optional, Default=0_
|
||||||
|
|
||||||
|
Defines the maximum number of connections the pool can allocate at a given time.
|
||||||
|
Zero means no limit.
|
||||||
|
|
||||||
|
```yaml tab="Docker & Swarm"
|
||||||
|
labels:
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.maxActiveConns=42"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Kubernetes"
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: test-ratelimit
|
||||||
|
spec:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
maxActiveConns: 42
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Consul Catalog"
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.maxActiveConns=42"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
test-ratelimit:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
maxActiveConns: 42
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[http.middlewares]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit.redis]
|
||||||
|
maxActiveConns = 42
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `redis.readTimeout`
|
||||||
|
|
||||||
|
_Optional, Default=3s_
|
||||||
|
|
||||||
|
Defines the timeout for socket reads.
|
||||||
|
If reached, commands will fail with a timeout instead of blocking.
|
||||||
|
Zero means no timeout.
|
||||||
|
|
||||||
|
```yaml tab="Docker & Swarm"
|
||||||
|
labels:
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.readTimeout=42s"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Kubernetes"
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: test-ratelimit
|
||||||
|
spec:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
readTimeout: 42s
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Consul Catalog"
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.readTimeout=42s"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
test-ratelimit:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
readTimeout: 42s
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[http.middlewares]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit.redis]
|
||||||
|
readTimeout = "42s"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `redis.writeTimeout`
|
||||||
|
|
||||||
|
_Optional, Default=3s_
|
||||||
|
|
||||||
|
Defines the timeout for socket writes.
|
||||||
|
If reached, commands will fail with a timeout instead of blocking.
|
||||||
|
Zero means no timeout.
|
||||||
|
|
||||||
|
```yaml tab="Docker & Swarm"
|
||||||
|
labels:
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.writeTimeout=42s"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Kubernetes"
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: test-ratelimit
|
||||||
|
spec:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
writeTimeout: 42s
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Consul Catalog"
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.writeTimeout=42s"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
test-ratelimit:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
writeTimeout: 42s
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[http.middlewares]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit.redis]
|
||||||
|
writeTimeout = "42s"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `redis.dialTimeout`
|
||||||
|
|
||||||
|
_Optional, Default=5s_
|
||||||
|
|
||||||
|
Defines the dial timeout for establishing new connections.
|
||||||
|
Zero means no timeout.
|
||||||
|
|
||||||
|
```yaml tab="Docker & Swarm"
|
||||||
|
labels:
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.dialTimeout=42s"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Kubernetes"
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: test-ratelimit
|
||||||
|
spec:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
dialTimeout: 42s
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Consul Catalog"
|
||||||
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.dialTimeout=42s"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
test-ratelimit:
|
||||||
|
rateLimit:
|
||||||
|
# ...
|
||||||
|
redis:
|
||||||
|
dialTimeout: 42s
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[http.middlewares]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit]
|
||||||
|
[http.middlewares.test-ratelimit.rateLimit.redis]
|
||||||
|
dialTimeout = "42s"
|
||||||
|
```
|
||||||
|
|
|
@ -132,6 +132,20 @@
|
||||||
- "traefik.http.middlewares.middleware18.ratelimit.average=42"
|
- "traefik.http.middlewares.middleware18.ratelimit.average=42"
|
||||||
- "traefik.http.middlewares.middleware18.ratelimit.burst=42"
|
- "traefik.http.middlewares.middleware18.ratelimit.burst=42"
|
||||||
- "traefik.http.middlewares.middleware18.ratelimit.period=42s"
|
- "traefik.http.middlewares.middleware18.ratelimit.period=42s"
|
||||||
|
- "traefik.http.middlewares.middleware18.ratelimit.redis.db=42"
|
||||||
|
- "traefik.http.middlewares.middleware18.ratelimit.redis.dialtimeout=42s"
|
||||||
|
- "traefik.http.middlewares.middleware18.ratelimit.redis.endpoints=foobar, foobar"
|
||||||
|
- "traefik.http.middlewares.middleware18.ratelimit.redis.maxactiveconns=42"
|
||||||
|
- "traefik.http.middlewares.middleware18.ratelimit.redis.minidleconns=42"
|
||||||
|
- "traefik.http.middlewares.middleware18.ratelimit.redis.password=foobar"
|
||||||
|
- "traefik.http.middlewares.middleware18.ratelimit.redis.poolsize=42"
|
||||||
|
- "traefik.http.middlewares.middleware18.ratelimit.redis.readtimeout=42s"
|
||||||
|
- "traefik.http.middlewares.middleware18.ratelimit.redis.tls.ca=foobar"
|
||||||
|
- "traefik.http.middlewares.middleware18.ratelimit.redis.tls.cert=foobar"
|
||||||
|
- "traefik.http.middlewares.middleware18.ratelimit.redis.tls.insecureskipverify=true"
|
||||||
|
- "traefik.http.middlewares.middleware18.ratelimit.redis.tls.key=foobar"
|
||||||
|
- "traefik.http.middlewares.middleware18.ratelimit.redis.username=foobar"
|
||||||
|
- "traefik.http.middlewares.middleware18.ratelimit.redis.writetimeout=42s"
|
||||||
- "traefik.http.middlewares.middleware18.ratelimit.sourcecriterion.ipstrategy.depth=42"
|
- "traefik.http.middlewares.middleware18.ratelimit.sourcecriterion.ipstrategy.depth=42"
|
||||||
- "traefik.http.middlewares.middleware18.ratelimit.sourcecriterion.ipstrategy.excludedips=foobar, foobar"
|
- "traefik.http.middlewares.middleware18.ratelimit.sourcecriterion.ipstrategy.excludedips=foobar, foobar"
|
||||||
- "traefik.http.middlewares.middleware18.ratelimit.sourcecriterion.ipstrategy.ipv6subnet=42"
|
- "traefik.http.middlewares.middleware18.ratelimit.sourcecriterion.ipstrategy.ipv6subnet=42"
|
||||||
|
|
|
@ -311,6 +311,22 @@
|
||||||
depth = 42
|
depth = 42
|
||||||
excludedIPs = ["foobar", "foobar"]
|
excludedIPs = ["foobar", "foobar"]
|
||||||
ipv6Subnet = 42
|
ipv6Subnet = 42
|
||||||
|
[http.middlewares.Middleware18.rateLimit.redis]
|
||||||
|
endpoints = ["foobar", "foobar"]
|
||||||
|
username = "foobar"
|
||||||
|
password = "foobar"
|
||||||
|
db = 42
|
||||||
|
poolSize = 42
|
||||||
|
minIdleConns = 42
|
||||||
|
maxActiveConns = 42
|
||||||
|
readTimeout = "42s"
|
||||||
|
writeTimeout = "42s"
|
||||||
|
dialTimeout = "42s"
|
||||||
|
[http.middlewares.Middleware18.rateLimit.redis.tls]
|
||||||
|
ca = "foobar"
|
||||||
|
cert = "foobar"
|
||||||
|
key = "foobar"
|
||||||
|
insecureSkipVerify = true
|
||||||
[http.middlewares.Middleware19]
|
[http.middlewares.Middleware19]
|
||||||
[http.middlewares.Middleware19.redirectRegex]
|
[http.middlewares.Middleware19.redirectRegex]
|
||||||
regex = "foobar"
|
regex = "foobar"
|
||||||
|
|
|
@ -360,6 +360,24 @@ http:
|
||||||
ipv6Subnet: 42
|
ipv6Subnet: 42
|
||||||
requestHeaderName: foobar
|
requestHeaderName: foobar
|
||||||
requestHost: true
|
requestHost: true
|
||||||
|
redis:
|
||||||
|
endpoints:
|
||||||
|
- foobar
|
||||||
|
- foobar
|
||||||
|
tls:
|
||||||
|
ca: foobar
|
||||||
|
cert: foobar
|
||||||
|
key: foobar
|
||||||
|
insecureSkipVerify: true
|
||||||
|
username: foobar
|
||||||
|
password: foobar
|
||||||
|
db: 42
|
||||||
|
poolSize: 42
|
||||||
|
minIdleConns: 42
|
||||||
|
maxActiveConns: 42
|
||||||
|
readTimeout: 42s
|
||||||
|
writeTimeout: 42s
|
||||||
|
dialTimeout: 42s
|
||||||
Middleware19:
|
Middleware19:
|
||||||
redirectRegex:
|
redirectRegex:
|
||||||
regex: foobar
|
regex: foobar
|
||||||
|
|
|
@ -1790,6 +1790,90 @@ spec:
|
||||||
Period, in combination with Average, defines the actual maximum rate, such as:
|
Period, in combination with Average, defines the actual maximum rate, such as:
|
||||||
r = Average / Period. It defaults to a second.
|
r = Average / Period. It defaults to a second.
|
||||||
x-kubernetes-int-or-string: true
|
x-kubernetes-int-or-string: true
|
||||||
|
redis:
|
||||||
|
description: Redis hold the configs of Redis as bucket in rate
|
||||||
|
limiter.
|
||||||
|
properties:
|
||||||
|
db:
|
||||||
|
description: DB defines the Redis database that will be selected
|
||||||
|
after connecting to the server.
|
||||||
|
type: integer
|
||||||
|
dialTimeout:
|
||||||
|
anyOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
description: |-
|
||||||
|
DialTimeout sets the timeout for establishing new connections.
|
||||||
|
Default value is 5 seconds.
|
||||||
|
pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$
|
||||||
|
x-kubernetes-int-or-string: true
|
||||||
|
endpoints:
|
||||||
|
description: |-
|
||||||
|
Endpoints contains either a single address or a seed list of host:port addresses.
|
||||||
|
Default value is ["localhost:6379"].
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
maxActiveConns:
|
||||||
|
description: |-
|
||||||
|
MaxActiveConns defines the maximum number of connections allocated by the pool at a given time.
|
||||||
|
Default value is 0, meaning there is no limit.
|
||||||
|
type: integer
|
||||||
|
minIdleConns:
|
||||||
|
description: |-
|
||||||
|
MinIdleConns defines the minimum number of idle connections.
|
||||||
|
Default value is 0, and idle connections are not closed by default.
|
||||||
|
type: integer
|
||||||
|
poolSize:
|
||||||
|
description: |-
|
||||||
|
PoolSize defines the initial number of socket connections.
|
||||||
|
If the pool runs out of available connections, additional ones will be created beyond PoolSize.
|
||||||
|
This can be limited using MaxActiveConns.
|
||||||
|
// Default value is 0, meaning 10 connections per every available CPU as reported by runtime.GOMAXPROCS.
|
||||||
|
type: integer
|
||||||
|
readTimeout:
|
||||||
|
anyOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
description: |-
|
||||||
|
ReadTimeout defines the timeout for socket read operations.
|
||||||
|
Default value is 3 seconds.
|
||||||
|
pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$
|
||||||
|
x-kubernetes-int-or-string: true
|
||||||
|
secret:
|
||||||
|
description: Secret defines the name of the referenced Kubernetes
|
||||||
|
Secret containing Redis credentials.
|
||||||
|
type: string
|
||||||
|
tls:
|
||||||
|
description: |-
|
||||||
|
TLS defines TLS-specific configurations, including the CA, certificate, and key,
|
||||||
|
which can be provided as a file path or file content.
|
||||||
|
properties:
|
||||||
|
caSecret:
|
||||||
|
description: |-
|
||||||
|
CASecret is the name of the referenced Kubernetes Secret containing the CA to validate the server certificate.
|
||||||
|
The CA certificate is extracted from key `tls.ca` or `ca.crt`.
|
||||||
|
type: string
|
||||||
|
certSecret:
|
||||||
|
description: |-
|
||||||
|
CertSecret is the name of the referenced Kubernetes Secret containing the client certificate.
|
||||||
|
The client certificate is extracted from the keys `tls.crt` and `tls.key`.
|
||||||
|
type: string
|
||||||
|
insecureSkipVerify:
|
||||||
|
description: InsecureSkipVerify defines whether the server
|
||||||
|
certificates should be validated.
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
|
writeTimeout:
|
||||||
|
anyOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
description: |-
|
||||||
|
WriteTimeout defines the timeout for socket write operations.
|
||||||
|
Default value is 3 seconds.
|
||||||
|
pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$
|
||||||
|
x-kubernetes-int-or-string: true
|
||||||
|
type: object
|
||||||
sourceCriterion:
|
sourceCriterion:
|
||||||
description: |-
|
description: |-
|
||||||
SourceCriterion defines what criterion is used to group requests as originating from a common source.
|
SourceCriterion defines what criterion is used to group requests as originating from a common source.
|
||||||
|
|
|
@ -153,6 +153,21 @@ THIS FILE MUST NOT BE EDITED BY HAND
|
||||||
| `traefik/http/middlewares/Middleware18/rateLimit/average` | `42` |
|
| `traefik/http/middlewares/Middleware18/rateLimit/average` | `42` |
|
||||||
| `traefik/http/middlewares/Middleware18/rateLimit/burst` | `42` |
|
| `traefik/http/middlewares/Middleware18/rateLimit/burst` | `42` |
|
||||||
| `traefik/http/middlewares/Middleware18/rateLimit/period` | `42s` |
|
| `traefik/http/middlewares/Middleware18/rateLimit/period` | `42s` |
|
||||||
|
| `traefik/http/middlewares/Middleware18/rateLimit/redis/db` | `42` |
|
||||||
|
| `traefik/http/middlewares/Middleware18/rateLimit/redis/dialTimeout` | `42s` |
|
||||||
|
| `traefik/http/middlewares/Middleware18/rateLimit/redis/endpoints/0` | `foobar` |
|
||||||
|
| `traefik/http/middlewares/Middleware18/rateLimit/redis/endpoints/1` | `foobar` |
|
||||||
|
| `traefik/http/middlewares/Middleware18/rateLimit/redis/maxActiveConns` | `42` |
|
||||||
|
| `traefik/http/middlewares/Middleware18/rateLimit/redis/minIdleConns` | `42` |
|
||||||
|
| `traefik/http/middlewares/Middleware18/rateLimit/redis/password` | `foobar` |
|
||||||
|
| `traefik/http/middlewares/Middleware18/rateLimit/redis/poolSize` | `42` |
|
||||||
|
| `traefik/http/middlewares/Middleware18/rateLimit/redis/readTimeout` | `42s` |
|
||||||
|
| `traefik/http/middlewares/Middleware18/rateLimit/redis/tls/ca` | `foobar` |
|
||||||
|
| `traefik/http/middlewares/Middleware18/rateLimit/redis/tls/cert` | `foobar` |
|
||||||
|
| `traefik/http/middlewares/Middleware18/rateLimit/redis/tls/insecureSkipVerify` | `true` |
|
||||||
|
| `traefik/http/middlewares/Middleware18/rateLimit/redis/tls/key` | `foobar` |
|
||||||
|
| `traefik/http/middlewares/Middleware18/rateLimit/redis/username` | `foobar` |
|
||||||
|
| `traefik/http/middlewares/Middleware18/rateLimit/redis/writeTimeout` | `42s` |
|
||||||
| `traefik/http/middlewares/Middleware18/rateLimit/sourceCriterion/ipStrategy/depth` | `42` |
|
| `traefik/http/middlewares/Middleware18/rateLimit/sourceCriterion/ipStrategy/depth` | `42` |
|
||||||
| `traefik/http/middlewares/Middleware18/rateLimit/sourceCriterion/ipStrategy/excludedIPs/0` | `foobar` |
|
| `traefik/http/middlewares/Middleware18/rateLimit/sourceCriterion/ipStrategy/excludedIPs/0` | `foobar` |
|
||||||
| `traefik/http/middlewares/Middleware18/rateLimit/sourceCriterion/ipStrategy/excludedIPs/1` | `foobar` |
|
| `traefik/http/middlewares/Middleware18/rateLimit/sourceCriterion/ipStrategy/excludedIPs/1` | `foobar` |
|
||||||
|
|
|
@ -1027,6 +1027,90 @@ spec:
|
||||||
Period, in combination with Average, defines the actual maximum rate, such as:
|
Period, in combination with Average, defines the actual maximum rate, such as:
|
||||||
r = Average / Period. It defaults to a second.
|
r = Average / Period. It defaults to a second.
|
||||||
x-kubernetes-int-or-string: true
|
x-kubernetes-int-or-string: true
|
||||||
|
redis:
|
||||||
|
description: Redis hold the configs of Redis as bucket in rate
|
||||||
|
limiter.
|
||||||
|
properties:
|
||||||
|
db:
|
||||||
|
description: DB defines the Redis database that will be selected
|
||||||
|
after connecting to the server.
|
||||||
|
type: integer
|
||||||
|
dialTimeout:
|
||||||
|
anyOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
description: |-
|
||||||
|
DialTimeout sets the timeout for establishing new connections.
|
||||||
|
Default value is 5 seconds.
|
||||||
|
pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$
|
||||||
|
x-kubernetes-int-or-string: true
|
||||||
|
endpoints:
|
||||||
|
description: |-
|
||||||
|
Endpoints contains either a single address or a seed list of host:port addresses.
|
||||||
|
Default value is ["localhost:6379"].
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
maxActiveConns:
|
||||||
|
description: |-
|
||||||
|
MaxActiveConns defines the maximum number of connections allocated by the pool at a given time.
|
||||||
|
Default value is 0, meaning there is no limit.
|
||||||
|
type: integer
|
||||||
|
minIdleConns:
|
||||||
|
description: |-
|
||||||
|
MinIdleConns defines the minimum number of idle connections.
|
||||||
|
Default value is 0, and idle connections are not closed by default.
|
||||||
|
type: integer
|
||||||
|
poolSize:
|
||||||
|
description: |-
|
||||||
|
PoolSize defines the initial number of socket connections.
|
||||||
|
If the pool runs out of available connections, additional ones will be created beyond PoolSize.
|
||||||
|
This can be limited using MaxActiveConns.
|
||||||
|
// Default value is 0, meaning 10 connections per every available CPU as reported by runtime.GOMAXPROCS.
|
||||||
|
type: integer
|
||||||
|
readTimeout:
|
||||||
|
anyOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
description: |-
|
||||||
|
ReadTimeout defines the timeout for socket read operations.
|
||||||
|
Default value is 3 seconds.
|
||||||
|
pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$
|
||||||
|
x-kubernetes-int-or-string: true
|
||||||
|
secret:
|
||||||
|
description: Secret defines the name of the referenced Kubernetes
|
||||||
|
Secret containing Redis credentials.
|
||||||
|
type: string
|
||||||
|
tls:
|
||||||
|
description: |-
|
||||||
|
TLS defines TLS-specific configurations, including the CA, certificate, and key,
|
||||||
|
which can be provided as a file path or file content.
|
||||||
|
properties:
|
||||||
|
caSecret:
|
||||||
|
description: |-
|
||||||
|
CASecret is the name of the referenced Kubernetes Secret containing the CA to validate the server certificate.
|
||||||
|
The CA certificate is extracted from key `tls.ca` or `ca.crt`.
|
||||||
|
type: string
|
||||||
|
certSecret:
|
||||||
|
description: |-
|
||||||
|
CertSecret is the name of the referenced Kubernetes Secret containing the client certificate.
|
||||||
|
The client certificate is extracted from the keys `tls.crt` and `tls.key`.
|
||||||
|
type: string
|
||||||
|
insecureSkipVerify:
|
||||||
|
description: InsecureSkipVerify defines whether the server
|
||||||
|
certificates should be validated.
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
|
writeTimeout:
|
||||||
|
anyOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
description: |-
|
||||||
|
WriteTimeout defines the timeout for socket write operations.
|
||||||
|
Default value is 3 seconds.
|
||||||
|
pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$
|
||||||
|
x-kubernetes-int-or-string: true
|
||||||
|
type: object
|
||||||
sourceCriterion:
|
sourceCriterion:
|
||||||
description: |-
|
description: |-
|
||||||
SourceCriterion defines what criterion is used to group requests as originating from a common source.
|
SourceCriterion defines what criterion is used to group requests as originating from a common source.
|
||||||
|
|
3
go.mod
3
go.mod
|
@ -50,6 +50,7 @@ require (
|
||||||
github.com/prometheus/client_golang v1.19.1
|
github.com/prometheus/client_golang v1.19.1
|
||||||
github.com/prometheus/client_model v0.6.1
|
github.com/prometheus/client_model v0.6.1
|
||||||
github.com/quic-go/quic-go v0.48.2
|
github.com/quic-go/quic-go v0.48.2
|
||||||
|
github.com/redis/go-redis/v9 v9.7.1
|
||||||
github.com/rs/zerolog v1.33.0
|
github.com/rs/zerolog v1.33.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/spiffe/go-spiffe/v2 v2.4.0
|
github.com/spiffe/go-spiffe/v2 v2.4.0
|
||||||
|
@ -70,6 +71,7 @@ require (
|
||||||
github.com/valyala/fasthttp v1.58.0
|
github.com/valyala/fasthttp v1.58.0
|
||||||
github.com/vulcand/oxy/v2 v2.0.0
|
github.com/vulcand/oxy/v2 v2.0.0
|
||||||
github.com/vulcand/predicate v1.2.0
|
github.com/vulcand/predicate v1.2.0
|
||||||
|
github.com/yuin/gopher-lua v1.1.1
|
||||||
go.opentelemetry.io/collector/pdata v1.10.0
|
go.opentelemetry.io/collector/pdata v1.10.0
|
||||||
go.opentelemetry.io/contrib/bridges/otellogrus v0.7.0
|
go.opentelemetry.io/contrib/bridges/otellogrus v0.7.0
|
||||||
go.opentelemetry.io/contrib/propagators/autoprop v0.53.0
|
go.opentelemetry.io/contrib/propagators/autoprop v0.53.0
|
||||||
|
@ -301,7 +303,6 @@ require (
|
||||||
github.com/prometheus/common v0.55.0 // indirect
|
github.com/prometheus/common v0.55.0 // indirect
|
||||||
github.com/prometheus/procfs v0.15.1 // indirect
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
github.com/redis/go-redis/v9 v9.6.1 // indirect
|
|
||||||
github.com/regfish/regfish-dnsapi-go v0.1.1 // indirect
|
github.com/regfish/regfish-dnsapi-go v0.1.1 // indirect
|
||||||
github.com/rs/cors v1.7.0 // indirect
|
github.com/rs/cors v1.7.0 // indirect
|
||||||
github.com/sacloud/api-client-go v0.2.10 // indirect
|
github.com/sacloud/api-client-go v0.2.10 // indirect
|
||||||
|
|
6
go.sum
6
go.sum
|
@ -1030,8 +1030,8 @@ github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KW
|
||||||
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
|
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=
|
github.com/redis/go-redis/v9 v9.7.1 h1:4LhKRCIduqXqtvCUlaq9c8bdHOkICjDMrr1+Zb3osAc=
|
||||||
github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
|
github.com/redis/go-redis/v9 v9.7.1/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
||||||
github.com/regfish/regfish-dnsapi-go v0.1.1 h1:TJFtbePHkd47q5GZwYl1h3DIYXmoxdLjW/SBsPtB5IE=
|
github.com/regfish/regfish-dnsapi-go v0.1.1 h1:TJFtbePHkd47q5GZwYl1h3DIYXmoxdLjW/SBsPtB5IE=
|
||||||
github.com/regfish/regfish-dnsapi-go v0.1.1/go.mod h1:ubIgXSfqarSnl3XHSn8hIFwFF3h0yrq0ZiWD93Y2VjY=
|
github.com/regfish/regfish-dnsapi-go v0.1.1/go.mod h1:ubIgXSfqarSnl3XHSn8hIFwFF3h0yrq0ZiWD93Y2VjY=
|
||||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
|
@ -1256,6 +1256,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||||
|
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||||
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
|
github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
|
||||||
|
|
|
@ -1790,6 +1790,90 @@ spec:
|
||||||
Period, in combination with Average, defines the actual maximum rate, such as:
|
Period, in combination with Average, defines the actual maximum rate, such as:
|
||||||
r = Average / Period. It defaults to a second.
|
r = Average / Period. It defaults to a second.
|
||||||
x-kubernetes-int-or-string: true
|
x-kubernetes-int-or-string: true
|
||||||
|
redis:
|
||||||
|
description: Redis hold the configs of Redis as bucket in rate
|
||||||
|
limiter.
|
||||||
|
properties:
|
||||||
|
db:
|
||||||
|
description: DB defines the Redis database that will be selected
|
||||||
|
after connecting to the server.
|
||||||
|
type: integer
|
||||||
|
dialTimeout:
|
||||||
|
anyOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
description: |-
|
||||||
|
DialTimeout sets the timeout for establishing new connections.
|
||||||
|
Default value is 5 seconds.
|
||||||
|
pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$
|
||||||
|
x-kubernetes-int-or-string: true
|
||||||
|
endpoints:
|
||||||
|
description: |-
|
||||||
|
Endpoints contains either a single address or a seed list of host:port addresses.
|
||||||
|
Default value is ["localhost:6379"].
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
maxActiveConns:
|
||||||
|
description: |-
|
||||||
|
MaxActiveConns defines the maximum number of connections allocated by the pool at a given time.
|
||||||
|
Default value is 0, meaning there is no limit.
|
||||||
|
type: integer
|
||||||
|
minIdleConns:
|
||||||
|
description: |-
|
||||||
|
MinIdleConns defines the minimum number of idle connections.
|
||||||
|
Default value is 0, and idle connections are not closed by default.
|
||||||
|
type: integer
|
||||||
|
poolSize:
|
||||||
|
description: |-
|
||||||
|
PoolSize defines the initial number of socket connections.
|
||||||
|
If the pool runs out of available connections, additional ones will be created beyond PoolSize.
|
||||||
|
This can be limited using MaxActiveConns.
|
||||||
|
// Default value is 0, meaning 10 connections per every available CPU as reported by runtime.GOMAXPROCS.
|
||||||
|
type: integer
|
||||||
|
readTimeout:
|
||||||
|
anyOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
description: |-
|
||||||
|
ReadTimeout defines the timeout for socket read operations.
|
||||||
|
Default value is 3 seconds.
|
||||||
|
pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$
|
||||||
|
x-kubernetes-int-or-string: true
|
||||||
|
secret:
|
||||||
|
description: Secret defines the name of the referenced Kubernetes
|
||||||
|
Secret containing Redis credentials.
|
||||||
|
type: string
|
||||||
|
tls:
|
||||||
|
description: |-
|
||||||
|
TLS defines TLS-specific configurations, including the CA, certificate, and key,
|
||||||
|
which can be provided as a file path or file content.
|
||||||
|
properties:
|
||||||
|
caSecret:
|
||||||
|
description: |-
|
||||||
|
CASecret is the name of the referenced Kubernetes Secret containing the CA to validate the server certificate.
|
||||||
|
The CA certificate is extracted from key `tls.ca` or `ca.crt`.
|
||||||
|
type: string
|
||||||
|
certSecret:
|
||||||
|
description: |-
|
||||||
|
CertSecret is the name of the referenced Kubernetes Secret containing the client certificate.
|
||||||
|
The client certificate is extracted from the keys `tls.crt` and `tls.key`.
|
||||||
|
type: string
|
||||||
|
insecureSkipVerify:
|
||||||
|
description: InsecureSkipVerify defines whether the server
|
||||||
|
certificates should be validated.
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
|
writeTimeout:
|
||||||
|
anyOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
description: |-
|
||||||
|
WriteTimeout defines the timeout for socket write operations.
|
||||||
|
Default value is 3 seconds.
|
||||||
|
pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$
|
||||||
|
x-kubernetes-int-or-string: true
|
||||||
|
type: object
|
||||||
sourceCriterion:
|
sourceCriterion:
|
||||||
description: |-
|
description: |-
|
||||||
SourceCriterion defines what criterion is used to group requests as originating from a common source.
|
SourceCriterion defines what criterion is used to group requests as originating from a common source.
|
||||||
|
|
39
integration/fixtures/ratelimit/simple_redis.toml
Normal file
39
integration/fixtures/ratelimit/simple_redis.toml
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
[global]
|
||||||
|
checkNewVersion = false
|
||||||
|
sendAnonymousUsage = false
|
||||||
|
|
||||||
|
[api]
|
||||||
|
insecure = true
|
||||||
|
|
||||||
|
[log]
|
||||||
|
level = "DEBUG"
|
||||||
|
noColor = true
|
||||||
|
|
||||||
|
[entryPoints]
|
||||||
|
[entryPoints.web]
|
||||||
|
address = ":8081"
|
||||||
|
|
||||||
|
[providers.file]
|
||||||
|
filename = "{{ .SelfFilename }}"
|
||||||
|
|
||||||
|
## dynamic configuration ##
|
||||||
|
|
||||||
|
[http.routers]
|
||||||
|
[http.routers.router1]
|
||||||
|
service = "service1"
|
||||||
|
middlewares = [ "ratelimit" ]
|
||||||
|
rule = "Path(`/`)"
|
||||||
|
|
||||||
|
[http.middlewares]
|
||||||
|
[http.middlewares.ratelimit.rateLimit]
|
||||||
|
average = 100
|
||||||
|
burst = 1
|
||||||
|
[http.middlewares.ratelimit.rateLimit.redis]
|
||||||
|
endpoints = ["{{ .RedisEndpoint }}"]
|
||||||
|
|
||||||
|
[http.services]
|
||||||
|
[http.services.service1]
|
||||||
|
[http.services.service1.loadBalancer]
|
||||||
|
passHostHeader = true
|
||||||
|
[[http.services.service1.loadBalancer.servers]]
|
||||||
|
url = "http://{{.Server1}}:80"
|
|
@ -1,6 +1,7 @@
|
||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -13,6 +14,7 @@ import (
|
||||||
type RateLimitSuite struct {
|
type RateLimitSuite struct {
|
||||||
BaseSuite
|
BaseSuite
|
||||||
ServerIP string
|
ServerIP string
|
||||||
|
RedisEndpoint string
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRateLimitSuite(t *testing.T) {
|
func TestRateLimitSuite(t *testing.T) {
|
||||||
|
@ -26,6 +28,7 @@ func (s *RateLimitSuite) SetupSuite() {
|
||||||
s.composeUp()
|
s.composeUp()
|
||||||
|
|
||||||
s.ServerIP = s.getComposeServiceIP("whoami1")
|
s.ServerIP = s.getComposeServiceIP("whoami1")
|
||||||
|
s.RedisEndpoint = net.JoinHostPort(s.getComposeServiceIP("redis"), "6379")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *RateLimitSuite) TearDownSuite() {
|
func (s *RateLimitSuite) TearDownSuite() {
|
||||||
|
@ -58,3 +61,34 @@ func (s *RateLimitSuite) TestSimpleConfiguration() {
|
||||||
s.T().Fatalf("requests throughput was too fast wrt to rate limiting: 100 requests in %v", elapsed)
|
s.T().Fatalf("requests throughput was too fast wrt to rate limiting: 100 requests in %v", elapsed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *RateLimitSuite) TestRedisRateLimitSimpleConfiguration() {
|
||||||
|
file := s.adaptFile("fixtures/ratelimit/simple_redis.toml", struct {
|
||||||
|
Server1 string
|
||||||
|
RedisEndpoint string
|
||||||
|
}{
|
||||||
|
Server1: s.ServerIP,
|
||||||
|
RedisEndpoint: s.RedisEndpoint,
|
||||||
|
})
|
||||||
|
|
||||||
|
s.traefikCmd(withConfigFile(file))
|
||||||
|
|
||||||
|
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("ratelimit", "redis"))
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
count := 0
|
||||||
|
for {
|
||||||
|
err = try.GetRequest("http://127.0.0.1:8081/", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK))
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
count++
|
||||||
|
if count > 100 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stop := time.Now()
|
||||||
|
elapsed := stop.Sub(start)
|
||||||
|
if elapsed < time.Second*99/100 {
|
||||||
|
s.T().Fatalf("requests throughput was too fast wrt to rate limiting: 100 requests in %v", elapsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,3 +2,10 @@ version: "3.8"
|
||||||
services:
|
services:
|
||||||
whoami1:
|
whoami1:
|
||||||
image: traefik/whoami
|
image: traefik/whoami
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:5.0
|
||||||
|
command:
|
||||||
|
- redis-server
|
||||||
|
- --port
|
||||||
|
- 6379
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
ptypes "github.com/traefik/paerser/types"
|
ptypes "github.com/traefik/paerser/types"
|
||||||
"github.com/traefik/traefik/v3/pkg/ip"
|
"github.com/traefik/traefik/v3/pkg/ip"
|
||||||
|
"github.com/traefik/traefik/v3/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ForwardAuthDefaultMaxBodySize is the ForwardAuth.MaxBodySize option default value.
|
// ForwardAuthDefaultMaxBodySize is the ForwardAuth.MaxBodySize option default value.
|
||||||
|
@ -566,6 +567,10 @@ type RateLimit struct {
|
||||||
// If several strategies are defined at the same time, an error will be raised.
|
// 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 *SourceCriterion `json:"sourceCriterion,omitempty" toml:"sourceCriterion,omitempty" yaml:"sourceCriterion,omitempty" export:"true"`
|
SourceCriterion *SourceCriterion `json:"sourceCriterion,omitempty" toml:"sourceCriterion,omitempty" yaml:"sourceCriterion,omitempty" export:"true"`
|
||||||
|
|
||||||
|
// Redis stores the configuration for using Redis as a bucket in the rate-limiting algorithm.
|
||||||
|
// If not specified, Traefik will default to an in-memory bucket for the algorithm.
|
||||||
|
Redis *Redis `json:"redis,omitempty" toml:"redis,omitempty" yaml:"redis,omitempty" export:"true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDefaults sets the default values on a RateLimit.
|
// SetDefaults sets the default values on a RateLimit.
|
||||||
|
@ -576,6 +581,58 @@ func (r *RateLimit) SetDefaults() {
|
||||||
|
|
||||||
// +k8s:deepcopy-gen=true
|
// +k8s:deepcopy-gen=true
|
||||||
|
|
||||||
|
// Redis holds the Redis configuration.
|
||||||
|
type Redis struct {
|
||||||
|
// Endpoints contains either a single address or a seed list of host:port addresses.
|
||||||
|
// Default value is ["localhost:6379"].
|
||||||
|
Endpoints []string `json:"endpoints,omitempty" toml:"endpoints,omitempty" yaml:"endpoints,omitempty"`
|
||||||
|
// TLS defines TLS-specific configurations, including the CA, certificate, and key,
|
||||||
|
// which can be provided as a file path or file content.
|
||||||
|
TLS *types.ClientTLS `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"`
|
||||||
|
// Username defines the username to connect to the Redis server.
|
||||||
|
Username string `json:"username,omitempty" toml:"username,omitempty" yaml:"username,omitempty" loggable:"false"`
|
||||||
|
// Password defines the password to connect to the Redis server.
|
||||||
|
Password string `json:"password,omitempty" toml:"password,omitempty" yaml:"password,omitempty" loggable:"false"`
|
||||||
|
// DB defines the Redis database that will be selected after connecting to the server.
|
||||||
|
DB int `json:"db,omitempty" toml:"db,omitempty" yaml:"db,omitempty"`
|
||||||
|
// PoolSize defines the initial number of socket connections.
|
||||||
|
// If the pool runs out of available connections, additional ones will be created beyond PoolSize.
|
||||||
|
// This can be limited using MaxActiveConns.
|
||||||
|
// Default value is 0, meaning 10 connections per every available CPU as reported by runtime.GOMAXPROCS.
|
||||||
|
PoolSize int `json:"poolSize,omitempty" toml:"poolSize,omitempty" yaml:"poolSize,omitempty" export:"true"`
|
||||||
|
// MinIdleConns defines the minimum number of idle connections.
|
||||||
|
// Default value is 0, and idle connections are not closed by default.
|
||||||
|
MinIdleConns int `json:"minIdleConns,omitempty" toml:"minIdleConns,omitempty" yaml:"minIdleConns,omitempty" export:"true"`
|
||||||
|
// MaxActiveConns defines the maximum number of connections allocated by the pool at a given time.
|
||||||
|
// Default value is 0, meaning there is no limit.
|
||||||
|
MaxActiveConns int `json:"maxActiveConns,omitempty" toml:"maxActiveConns,omitempty" yaml:"maxActiveConns,omitempty" export:"true"`
|
||||||
|
// ReadTimeout defines the timeout for socket read operations.
|
||||||
|
// Default value is 3 seconds.
|
||||||
|
ReadTimeout *ptypes.Duration `json:"readTimeout,omitempty" toml:"readTimeout,omitempty" yaml:"readTimeout,omitempty" export:"true"`
|
||||||
|
// WriteTimeout defines the timeout for socket write operations.
|
||||||
|
// Default value is 3 seconds.
|
||||||
|
WriteTimeout *ptypes.Duration `json:"writeTimeout,omitempty" toml:"writeTimeout,omitempty" yaml:"writeTimeout,omitempty" export:"true"`
|
||||||
|
// DialTimeout sets the timeout for establishing new connections.
|
||||||
|
// Default value is 5 seconds.
|
||||||
|
DialTimeout *ptypes.Duration `json:"dialTimeout,omitempty" toml:"dialTimeout,omitempty" yaml:"dialTimeout,omitempty" export:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaults sets the default values on a RateLimit.
|
||||||
|
func (r *Redis) SetDefaults() {
|
||||||
|
r.Endpoints = []string{"localhost:6379"}
|
||||||
|
|
||||||
|
defaultReadTimeout := ptypes.Duration(3 * time.Second)
|
||||||
|
r.ReadTimeout = &defaultReadTimeout
|
||||||
|
|
||||||
|
defaultWriteTimeout := ptypes.Duration(3 * time.Second)
|
||||||
|
r.WriteTimeout = &defaultWriteTimeout
|
||||||
|
|
||||||
|
defaultDialTimeout := ptypes.Duration(5 * time.Second)
|
||||||
|
r.DialTimeout = &defaultDialTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
// +k8s:deepcopy-gen=true
|
||||||
|
|
||||||
// RedirectRegex holds the redirect regex middleware configuration.
|
// RedirectRegex holds the redirect regex middleware configuration.
|
||||||
// This middleware redirects a request using regex matching and replacement.
|
// This middleware redirects a request using regex matching and replacement.
|
||||||
// More info: https://doc.traefik.io/traefik/v3.3/middlewares/http/redirectregex/#regex
|
// More info: https://doc.traefik.io/traefik/v3.3/middlewares/http/redirectregex/#regex
|
||||||
|
|
|
@ -30,6 +30,7 @@ THE SOFTWARE.
|
||||||
package dynamic
|
package dynamic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
paersertypes "github.com/traefik/paerser/types"
|
||||||
tls "github.com/traefik/traefik/v3/pkg/tls"
|
tls "github.com/traefik/traefik/v3/pkg/tls"
|
||||||
types "github.com/traefik/traefik/v3/pkg/types"
|
types "github.com/traefik/traefik/v3/pkg/types"
|
||||||
)
|
)
|
||||||
|
@ -1094,6 +1095,11 @@ func (in *RateLimit) DeepCopyInto(out *RateLimit) {
|
||||||
*out = new(SourceCriterion)
|
*out = new(SourceCriterion)
|
||||||
(*in).DeepCopyInto(*out)
|
(*in).DeepCopyInto(*out)
|
||||||
}
|
}
|
||||||
|
if in.Redis != nil {
|
||||||
|
in, out := &in.Redis, &out.Redis
|
||||||
|
*out = new(Redis)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1139,6 +1145,47 @@ func (in *RedirectScheme) DeepCopy() *RedirectScheme {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *Redis) DeepCopyInto(out *Redis) {
|
||||||
|
*out = *in
|
||||||
|
if in.Endpoints != nil {
|
||||||
|
in, out := &in.Endpoints, &out.Endpoints
|
||||||
|
*out = make([]string, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
|
if in.TLS != nil {
|
||||||
|
in, out := &in.TLS, &out.TLS
|
||||||
|
*out = new(types.ClientTLS)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.ReadTimeout != nil {
|
||||||
|
in, out := &in.ReadTimeout, &out.ReadTimeout
|
||||||
|
*out = new(paersertypes.Duration)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.WriteTimeout != nil {
|
||||||
|
in, out := &in.WriteTimeout, &out.WriteTimeout
|
||||||
|
*out = new(paersertypes.Duration)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.DialTimeout != nil {
|
||||||
|
in, out := &in.DialTimeout, &out.DialTimeout
|
||||||
|
*out = new(paersertypes.Duration)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Redis.
|
||||||
|
func (in *Redis) DeepCopy() *Redis {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(Redis)
|
||||||
|
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 *ReplacePath) DeepCopyInto(out *ReplacePath) {
|
func (in *ReplacePath) DeepCopyInto(out *ReplacePath) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
|
72
pkg/middlewares/ratelimiter/in_memory_limiter.go
Normal file
72
pkg/middlewares/ratelimiter/in_memory_limiter.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
package ratelimiter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mailgun/ttlmap"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
)
|
||||||
|
|
||||||
|
type inMemoryRateLimiter struct {
|
||||||
|
rate rate.Limit // reqs/s
|
||||||
|
burst int64
|
||||||
|
// maxDelay is the maximum duration we're willing to wait for a bucket reservation to become effective, in nanoseconds.
|
||||||
|
// For now it is somewhat arbitrarily set to 1/(2*rate).
|
||||||
|
maxDelay time.Duration
|
||||||
|
// Each rate limiter for a given source is stored in the buckets ttlmap.
|
||||||
|
// To keep this ttlmap constrained in size,
|
||||||
|
// each ratelimiter is "garbage collected" when it is considered expired.
|
||||||
|
// It is considered expired after it hasn't been used for ttl seconds.
|
||||||
|
ttl int
|
||||||
|
buckets *ttlmap.TtlMap // actual buckets, keyed by source.
|
||||||
|
|
||||||
|
logger *zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func newInMemoryRateLimiter(rate rate.Limit, burst int64, maxDelay time.Duration, ttl int, logger *zerolog.Logger) (*inMemoryRateLimiter, error) {
|
||||||
|
buckets, err := ttlmap.NewConcurrent(maxSources)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating ttlmap: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &inMemoryRateLimiter{
|
||||||
|
rate: rate,
|
||||||
|
burst: burst,
|
||||||
|
maxDelay: maxDelay,
|
||||||
|
ttl: ttl,
|
||||||
|
logger: logger,
|
||||||
|
buckets: buckets,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *inMemoryRateLimiter) Allow(_ context.Context, source string) (*time.Duration, error) {
|
||||||
|
// Get bucket which contains limiter information.
|
||||||
|
var bucket *rate.Limiter
|
||||||
|
if rlSource, exists := i.buckets.Get(source); exists {
|
||||||
|
bucket = rlSource.(*rate.Limiter)
|
||||||
|
} else {
|
||||||
|
bucket = rate.NewLimiter(i.rate, int(i.burst))
|
||||||
|
}
|
||||||
|
|
||||||
|
// We Set even in the case where the source already exists,
|
||||||
|
// because we want to update the expiryTime everytime we get the source,
|
||||||
|
// as the expiryTime is supposed to reflect the activity (or lack thereof) on that source.
|
||||||
|
if err := i.buckets.Set(source, bucket, i.ttl); err != nil {
|
||||||
|
return nil, fmt.Errorf("setting buckets: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res := bucket.Reserve()
|
||||||
|
if !res.OK() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
delay := res.Delay()
|
||||||
|
if delay > i.maxDelay {
|
||||||
|
res.Cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &delay, nil
|
||||||
|
}
|
66
pkg/middlewares/ratelimiter/lua.go
Normal file
66
pkg/middlewares/ratelimiter/lua.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
package ratelimiter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Rediser interface {
|
||||||
|
Eval(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd
|
||||||
|
EvalSha(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd
|
||||||
|
ScriptExists(ctx context.Context, hashes ...string) *redis.BoolSliceCmd
|
||||||
|
ScriptLoad(ctx context.Context, script string) *redis.StringCmd
|
||||||
|
Del(ctx context.Context, keys ...string) *redis.IntCmd
|
||||||
|
|
||||||
|
EvalRO(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd
|
||||||
|
EvalShaRO(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:dupword
|
||||||
|
var AllowTokenBucketRaw = `
|
||||||
|
local key = KEYS[1]
|
||||||
|
local limit, burst, ttl, t, max_delay = tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3]), tonumber(ARGV[4]),
|
||||||
|
tonumber(ARGV[5])
|
||||||
|
|
||||||
|
local bucket = {
|
||||||
|
limit = limit,
|
||||||
|
burst = burst,
|
||||||
|
tokens = 0,
|
||||||
|
last = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
local rl_source = redis.call('hgetall', key)
|
||||||
|
|
||||||
|
if table.maxn(rl_source) == 4 then
|
||||||
|
-- Get bucket state from redis
|
||||||
|
bucket.last = tonumber(rl_source[2])
|
||||||
|
bucket.tokens = tonumber(rl_source[4])
|
||||||
|
end
|
||||||
|
|
||||||
|
local last = bucket.last
|
||||||
|
if t < last then
|
||||||
|
last = t
|
||||||
|
end
|
||||||
|
|
||||||
|
local elapsed = t - last
|
||||||
|
local delta = bucket.limit * elapsed
|
||||||
|
local tokens = bucket.tokens + delta
|
||||||
|
tokens = math.min(tokens, bucket.burst)
|
||||||
|
tokens = tokens - 1
|
||||||
|
|
||||||
|
local wait_duration = 0
|
||||||
|
if tokens < 0 then
|
||||||
|
wait_duration = (tokens * -1) / bucket.limit
|
||||||
|
if wait_duration > max_delay then
|
||||||
|
tokens = tokens + 1
|
||||||
|
tokens = math.min(tokens, burst)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
redis.call('hset', key, 'last', t, 'tokens', tokens)
|
||||||
|
redis.call('expire', key, ttl)
|
||||||
|
|
||||||
|
return {tostring(true), tostring(wait_duration),tostring(tokens)}`
|
||||||
|
|
||||||
|
var AllowTokenBucketScript = redis.NewScript(AllowTokenBucketRaw)
|
79
pkg/middlewares/ratelimiter/rate_limiter.go
Normal file → Executable file
79
pkg/middlewares/ratelimiter/rate_limiter.go
Normal file → Executable file
|
@ -8,7 +8,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mailgun/ttlmap"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||||
"github.com/traefik/traefik/v3/pkg/middlewares"
|
"github.com/traefik/traefik/v3/pkg/middlewares"
|
||||||
|
@ -23,24 +23,23 @@ const (
|
||||||
maxSources = 65536
|
maxSources = 65536
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type limiter interface {
|
||||||
|
Allow(ctx context.Context, token string) (*time.Duration, error)
|
||||||
|
}
|
||||||
|
|
||||||
// rateLimiter implements rate limiting and traffic shaping with a set of token buckets;
|
// rateLimiter implements rate limiting and traffic shaping with a set of token buckets;
|
||||||
// one for each traffic source. The same parameters are applied to all the buckets.
|
// one for each traffic source. The same parameters are applied to all the buckets.
|
||||||
type rateLimiter struct {
|
type rateLimiter struct {
|
||||||
name string
|
name string
|
||||||
rate rate.Limit // reqs/s
|
rate rate.Limit // reqs/s
|
||||||
burst int64
|
|
||||||
// maxDelay is the maximum duration we're willing to wait for a bucket reservation to become effective, in nanoseconds.
|
// maxDelay is the maximum duration we're willing to wait for a bucket reservation to become effective, in nanoseconds.
|
||||||
// For now it is somewhat arbitrarily set to 1/(2*rate).
|
// For now it is somewhat arbitrarily set to 1/(2*rate).
|
||||||
maxDelay time.Duration
|
maxDelay time.Duration
|
||||||
// each rate limiter for a given source is stored in the buckets ttlmap.
|
|
||||||
// To keep this ttlmap constrained in size,
|
|
||||||
// each ratelimiter is "garbage collected" when it is considered expired.
|
|
||||||
// It is considered expired after it hasn't been used for ttl seconds.
|
|
||||||
ttl int
|
|
||||||
sourceMatcher utils.SourceExtractor
|
sourceMatcher utils.SourceExtractor
|
||||||
next http.Handler
|
next http.Handler
|
||||||
|
logger *zerolog.Logger
|
||||||
|
|
||||||
buckets *ttlmap.TtlMap // actual buckets, keyed by source.
|
limiter limiter
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a rate limiter middleware.
|
// New returns a rate limiter middleware.
|
||||||
|
@ -60,12 +59,7 @@ func New(ctx context.Context, next http.Handler, config dynamic.RateLimit, name
|
||||||
|
|
||||||
sourceMatcher, err := middlewares.GetSourceExtractor(ctxLog, config.SourceCriterion)
|
sourceMatcher, err := middlewares.GetSourceExtractor(ctxLog, config.SourceCriterion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("getting source extractor: %w", err)
|
||||||
}
|
|
||||||
|
|
||||||
buckets, err := ttlmap.NewConcurrent(maxSources)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
burst := config.Burst
|
burst := config.Burst
|
||||||
|
@ -109,16 +103,27 @@ func New(ctx context.Context, next http.Handler, config dynamic.RateLimit, name
|
||||||
} else if rtl > 0 {
|
} else if rtl > 0 {
|
||||||
ttl += int(1 / rtl)
|
ttl += int(1 / rtl)
|
||||||
}
|
}
|
||||||
|
var limiter limiter
|
||||||
|
if config.Redis != nil {
|
||||||
|
limiter, err = newRedisLimiter(ctx, rate.Limit(rtl), burst, maxDelay, ttl, config, logger)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating redis limiter: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
limiter, err = newInMemoryRateLimiter(rate.Limit(rtl), burst, maxDelay, ttl, logger)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating in-memory limiter: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &rateLimiter{
|
return &rateLimiter{
|
||||||
|
logger: logger,
|
||||||
name: name,
|
name: name,
|
||||||
rate: rate.Limit(rtl),
|
rate: rate.Limit(rtl),
|
||||||
burst: burst,
|
|
||||||
maxDelay: maxDelay,
|
maxDelay: maxDelay,
|
||||||
next: next,
|
next: next,
|
||||||
sourceMatcher: sourceMatcher,
|
sourceMatcher: sourceMatcher,
|
||||||
buckets: buckets,
|
limiter: limiter,
|
||||||
ttl: ttl,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,38 +146,34 @@ func (rl *rateLimiter) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
logger.Info().Msgf("ignoring token bucket amount > 1: %d", amount)
|
logger.Info().Msgf("ignoring token bucket amount > 1: %d", amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
var bucket *rate.Limiter
|
delay, err := rl.limiter.Allow(ctx, source)
|
||||||
if rlSource, exists := rl.buckets.Get(source); exists {
|
if err != nil {
|
||||||
bucket = rlSource.(*rate.Limiter)
|
rl.logger.Error().Err(err).Msg("Could not insert/update bucket")
|
||||||
} else {
|
observability.SetStatusErrorf(ctx, "Could not insert/update bucket")
|
||||||
bucket = rate.NewLimiter(rl.rate, int(rl.burst))
|
http.Error(rw, "Could not insert/update bucket", http.StatusInternalServerError)
|
||||||
}
|
|
||||||
|
|
||||||
// We Set even in the case where the source already exists,
|
|
||||||
// because we want to update the expiryTime every time we get the source,
|
|
||||||
// as the expiryTime is supposed to reflect the activity (or lack thereof) on that source.
|
|
||||||
if err := rl.buckets.Set(source, bucket, rl.ttl); err != nil {
|
|
||||||
logger.Error().Err(err).Msg("Could not insert/update bucket")
|
|
||||||
observability.SetStatusErrorf(req.Context(), "Could not insert/update bucket")
|
|
||||||
http.Error(rw, "could not insert/update bucket", http.StatusInternalServerError)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res := bucket.Reserve()
|
if delay == nil {
|
||||||
if !res.OK() {
|
observability.SetStatusErrorf(ctx, "No bursty traffic allowed")
|
||||||
observability.SetStatusErrorf(req.Context(), "No bursty traffic allowed")
|
|
||||||
http.Error(rw, "No bursty traffic allowed", http.StatusTooManyRequests)
|
http.Error(rw, "No bursty traffic allowed", http.StatusTooManyRequests)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
delay := res.Delay()
|
if *delay > rl.maxDelay {
|
||||||
if delay > rl.maxDelay {
|
rl.serveDelayError(ctx, rw, *delay)
|
||||||
res.Cancel()
|
|
||||||
rl.serveDelayError(ctx, rw, delay)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(delay)
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
observability.SetStatusErrorf(ctx, "Context canceled")
|
||||||
|
http.Error(rw, "context canceled", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
|
||||||
|
case <-time.After(*delay):
|
||||||
|
}
|
||||||
|
|
||||||
rl.next.ServeHTTP(rw, req)
|
rl.next.ServeHTTP(rw, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,19 +2,25 @@ package ratelimiter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mailgun/ttlmap"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"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/v3/pkg/config/dynamic"
|
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||||
"github.com/traefik/traefik/v3/pkg/testhelpers"
|
"github.com/traefik/traefik/v3/pkg/testhelpers"
|
||||||
"github.com/vulcand/oxy/v2/utils"
|
"github.com/vulcand/oxy/v2/utils"
|
||||||
|
lua "github.com/yuin/gopher-lua"
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -84,7 +90,17 @@ func TestNewRateLimiter(t *testing.T) {
|
||||||
RequestHeaderName: "Foo",
|
RequestHeaderName: "Foo",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedError: "iPStrategy and RequestHeaderName are mutually exclusive",
|
expectedError: "getting source extractor: iPStrategy and RequestHeaderName are mutually exclusive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Use Redis",
|
||||||
|
config: dynamic.RateLimit{
|
||||||
|
Average: 200,
|
||||||
|
Burst: 10,
|
||||||
|
Redis: &dynamic.Redis{
|
||||||
|
Endpoints: []string{"localhost:6379"},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,7 +154,7 @@ func TestNewRateLimiter(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRateLimit(t *testing.T) {
|
func TestInMemoryRateLimit(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
config dynamic.RateLimit
|
config dynamic.RateLimit
|
||||||
|
@ -326,15 +342,357 @@ func TestRateLimit(t *testing.T) {
|
||||||
minCount := computeMinCount(wantCount)
|
minCount := computeMinCount(wantCount)
|
||||||
|
|
||||||
if reqCount < minCount {
|
if reqCount < minCount {
|
||||||
t.Fatalf("rate was slower than expected: %d requests (wanted > %d) in %v", reqCount, minCount, elapsed)
|
t.Fatalf("rate was slower than expected: %d requests (wanted > %d) (dropped %d) in %v", reqCount, minCount, dropped, elapsed)
|
||||||
}
|
}
|
||||||
if reqCount > maxCount {
|
if reqCount > maxCount {
|
||||||
t.Fatalf("rate was faster than expected: %d requests (wanted < %d) in %v", reqCount, maxCount, elapsed)
|
t.Fatalf("rate was faster than expected: %d requests (wanted < %d) (dropped %d) in %v", reqCount, maxCount, dropped, elapsed)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRedisRateLimit(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
config dynamic.RateLimit
|
||||||
|
loadDuration time.Duration
|
||||||
|
incomingLoad int // in reqs/s
|
||||||
|
burst int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "Average is respected",
|
||||||
|
config: dynamic.RateLimit{
|
||||||
|
Average: 100,
|
||||||
|
Burst: 1,
|
||||||
|
},
|
||||||
|
loadDuration: 2 * time.Second,
|
||||||
|
incomingLoad: 400,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "burst allowed, no bursty traffic",
|
||||||
|
config: dynamic.RateLimit{
|
||||||
|
Average: 100,
|
||||||
|
Burst: 100,
|
||||||
|
},
|
||||||
|
loadDuration: 2 * time.Second,
|
||||||
|
incomingLoad: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "burst allowed, initial burst, under capacity",
|
||||||
|
config: dynamic.RateLimit{
|
||||||
|
Average: 100,
|
||||||
|
Burst: 100,
|
||||||
|
},
|
||||||
|
loadDuration: 2 * time.Second,
|
||||||
|
incomingLoad: 200,
|
||||||
|
burst: 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "burst allowed, initial burst, over capacity",
|
||||||
|
config: dynamic.RateLimit{
|
||||||
|
Average: 100,
|
||||||
|
Burst: 100,
|
||||||
|
},
|
||||||
|
loadDuration: 2 * time.Second,
|
||||||
|
incomingLoad: 200,
|
||||||
|
burst: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "burst over average, initial burst, over capacity",
|
||||||
|
config: dynamic.RateLimit{
|
||||||
|
Average: 100,
|
||||||
|
Burst: 200,
|
||||||
|
},
|
||||||
|
loadDuration: 2 * time.Second,
|
||||||
|
incomingLoad: 200,
|
||||||
|
burst: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "lower than 1/s",
|
||||||
|
config: dynamic.RateLimit{
|
||||||
|
// Bug on gopher-lua on parsing the string to number "5e-07" => 0.0000005
|
||||||
|
// See https://github.com/yuin/gopher-lua/issues/491
|
||||||
|
// Average: 5,
|
||||||
|
Average: 1,
|
||||||
|
Period: ptypes.Duration(10 * time.Second),
|
||||||
|
},
|
||||||
|
loadDuration: 2 * time.Second,
|
||||||
|
incomingLoad: 100,
|
||||||
|
burst: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "lower than 1/s, longer",
|
||||||
|
config: dynamic.RateLimit{
|
||||||
|
// Bug on gopher-lua on parsing the string to number "5e-07" => 0.0000005
|
||||||
|
// See https://github.com/yuin/gopher-lua/issues/491
|
||||||
|
// Average: 5,
|
||||||
|
Average: 1,
|
||||||
|
Period: ptypes.Duration(10 * time.Second),
|
||||||
|
},
|
||||||
|
loadDuration: time.Minute,
|
||||||
|
incomingLoad: 100,
|
||||||
|
burst: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "lower than 1/s, longer, harsher",
|
||||||
|
config: dynamic.RateLimit{
|
||||||
|
Average: 1,
|
||||||
|
Period: ptypes.Duration(time.Minute),
|
||||||
|
},
|
||||||
|
loadDuration: time.Minute,
|
||||||
|
incomingLoad: 100,
|
||||||
|
burst: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "period below 1 second",
|
||||||
|
config: dynamic.RateLimit{
|
||||||
|
Average: 50,
|
||||||
|
Period: ptypes.Duration(500 * time.Millisecond),
|
||||||
|
},
|
||||||
|
loadDuration: 2 * time.Second,
|
||||||
|
incomingLoad: 300,
|
||||||
|
burst: 0,
|
||||||
|
},
|
||||||
|
// TODO Try to disambiguate when it fails if it is because of too high a load.
|
||||||
|
// {
|
||||||
|
// desc: "Zero average ==> no rate limiting",
|
||||||
|
// config: dynamic.RateLimit{
|
||||||
|
// Average: 0,
|
||||||
|
// Burst: 1,
|
||||||
|
// },
|
||||||
|
// incomingLoad: 1000,
|
||||||
|
// loadDuration: time.Second,
|
||||||
|
// },
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
randPort := rand.Int()
|
||||||
|
if test.loadDuration >= time.Minute && testing.Short() {
|
||||||
|
t.Skip("skipping test in short mode.")
|
||||||
|
}
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
reqCount := 0
|
||||||
|
dropped := 0
|
||||||
|
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
reqCount++
|
||||||
|
})
|
||||||
|
test.config.Redis = &dynamic.Redis{
|
||||||
|
Endpoints: []string{"localhost:6379"},
|
||||||
|
}
|
||||||
|
h, err := New(context.Background(), next, test.config, "rate-limiter")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
l := h.(*rateLimiter)
|
||||||
|
|
||||||
|
limiter := l.limiter.(*redisLimiter)
|
||||||
|
limiter.client = newMockRedisClient(limiter.ttl)
|
||||||
|
|
||||||
|
h = l
|
||||||
|
|
||||||
|
loadPeriod := time.Duration(1e9 / test.incomingLoad)
|
||||||
|
start := time.Now()
|
||||||
|
end := start.Add(test.loadDuration)
|
||||||
|
ticker := time.NewTicker(loadPeriod)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
if time.Now().After(end) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil)
|
||||||
|
req.RemoteAddr = "127.0.0." + strconv.Itoa(randPort) + ":" + strconv.Itoa(randPort)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.ServeHTTP(w, req)
|
||||||
|
if w.Result().StatusCode != http.StatusOK {
|
||||||
|
dropped++
|
||||||
|
}
|
||||||
|
if test.burst > 0 && reqCount < test.burst {
|
||||||
|
// if a burst is defined we first hammer the server with test.burst requests as fast as possible
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
<-ticker.C
|
||||||
|
}
|
||||||
|
stop := time.Now()
|
||||||
|
elapsed := stop.Sub(start)
|
||||||
|
|
||||||
|
burst := test.config.Burst
|
||||||
|
if burst < 1 {
|
||||||
|
// actual default value
|
||||||
|
burst = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
period := time.Duration(test.config.Period)
|
||||||
|
if period == 0 {
|
||||||
|
period = time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.config.Average == 0 {
|
||||||
|
if reqCount < 75*test.incomingLoad/100 {
|
||||||
|
t.Fatalf("we (arbitrarily) expect at least 75%% of the requests to go through with no rate limiting, and yet only %d/%d went through", reqCount, test.incomingLoad)
|
||||||
|
}
|
||||||
|
if dropped != 0 {
|
||||||
|
t.Fatalf("no request should have been dropped if rate limiting is disabled, and yet %d were", dropped)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that even when there is no bursty traffic,
|
||||||
|
// we take into account the configured burst,
|
||||||
|
// because it also helps absorbing non-bursty traffic.
|
||||||
|
rate := float64(test.config.Average) / float64(period)
|
||||||
|
|
||||||
|
wantCount := int(int64(rate*float64(test.loadDuration)) + burst)
|
||||||
|
|
||||||
|
// Allow for a 2% leeway
|
||||||
|
maxCount := wantCount * 102 / 100
|
||||||
|
|
||||||
|
// With very high CPU loads,
|
||||||
|
// we can expect some extra delay in addition to the rate limiting we already do,
|
||||||
|
// so we allow for some extra leeway there.
|
||||||
|
// Feel free to adjust wrt to the load on e.g. the CI.
|
||||||
|
minCount := computeMinCount(wantCount)
|
||||||
|
|
||||||
|
if reqCount < minCount {
|
||||||
|
t.Fatalf("rate was slower than expected: %d requests (wanted > %d) (dropped %d) in %v", reqCount, minCount, dropped, elapsed)
|
||||||
|
}
|
||||||
|
if reqCount > maxCount {
|
||||||
|
t.Fatalf("rate was faster than expected: %d requests (wanted < %d) (dropped %d) in %v", reqCount, maxCount, dropped, elapsed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockRedisClient struct {
|
||||||
|
ttl int
|
||||||
|
keys *ttlmap.TtlMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockRedisClient(ttl int) Rediser {
|
||||||
|
buckets, _ := ttlmap.NewConcurrent(65536)
|
||||||
|
return &mockRedisClient{
|
||||||
|
ttl: ttl,
|
||||||
|
keys: buckets,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRedisClient) EvalSha(ctx context.Context, _ string, keys []string, args ...interface{}) *redis.Cmd {
|
||||||
|
state := lua.NewState()
|
||||||
|
defer state.Close()
|
||||||
|
|
||||||
|
tableKeys := state.NewTable()
|
||||||
|
for _, key := range keys {
|
||||||
|
tableKeys.Append(lua.LString(key))
|
||||||
|
}
|
||||||
|
state.SetGlobal("KEYS", tableKeys)
|
||||||
|
|
||||||
|
tableArgv := state.NewTable()
|
||||||
|
for _, arg := range args {
|
||||||
|
tableArgv.Append(lua.LString(fmt.Sprint(arg)))
|
||||||
|
}
|
||||||
|
state.SetGlobal("ARGV", tableArgv)
|
||||||
|
|
||||||
|
mod := state.SetFuncs(state.NewTable(), map[string]lua.LGFunction{
|
||||||
|
"call": func(state *lua.LState) int {
|
||||||
|
switch state.Get(1).String() {
|
||||||
|
case "hset":
|
||||||
|
key := state.Get(2).String()
|
||||||
|
keyLast := state.Get(3).String()
|
||||||
|
last := state.Get(4).String()
|
||||||
|
keyTokens := state.Get(5).String()
|
||||||
|
tokens := state.Get(6).String()
|
||||||
|
table := []string{keyLast, last, keyTokens, tokens}
|
||||||
|
_ = m.keys.Set(key, table, m.ttl)
|
||||||
|
case "hgetall":
|
||||||
|
key := state.Get(2).String()
|
||||||
|
value, ok := m.keys.Get(key)
|
||||||
|
table := state.NewTable()
|
||||||
|
if !ok {
|
||||||
|
state.Push(table)
|
||||||
|
} else {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case []string:
|
||||||
|
if len(v) != 4 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for i := range v {
|
||||||
|
table.Append(lua.LString(v[i]))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
fmt.Printf("Unknown type: %T\n", v)
|
||||||
|
}
|
||||||
|
state.Push(table)
|
||||||
|
}
|
||||||
|
case "expire":
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1
|
||||||
|
},
|
||||||
|
})
|
||||||
|
state.SetGlobal("redis", mod)
|
||||||
|
state.Push(mod)
|
||||||
|
|
||||||
|
cmd := redis.NewCmd(ctx)
|
||||||
|
if err := state.DoString(AllowTokenBucketRaw); err != nil {
|
||||||
|
cmd.SetErr(err)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
result := state.Get(2)
|
||||||
|
resultTable, ok := result.(*lua.LTable)
|
||||||
|
if !ok {
|
||||||
|
cmd.SetErr(errors.New("unexpected response type: " + result.String()))
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
var resultSlice []interface{}
|
||||||
|
resultTable.ForEach(func(_ lua.LValue, value lua.LValue) {
|
||||||
|
valueNbr, ok := value.(lua.LNumber)
|
||||||
|
if !ok {
|
||||||
|
valueStr, ok := value.(lua.LString)
|
||||||
|
if !ok {
|
||||||
|
cmd.SetErr(errors.New("unexpected response value type " + value.String()))
|
||||||
|
}
|
||||||
|
resultSlice = append(resultSlice, string(valueStr))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resultSlice = append(resultSlice, int64(valueNbr))
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd.SetVal(resultSlice)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRedisClient) Eval(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd {
|
||||||
|
return m.EvalSha(ctx, script, keys, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRedisClient) ScriptExists(ctx context.Context, hashes ...string) *redis.BoolSliceCmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRedisClient) ScriptLoad(ctx context.Context, script string) *redis.StringCmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRedisClient) Del(ctx context.Context, keys ...string) *redis.IntCmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRedisClient) EvalRO(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRedisClient) EvalShaRO(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func computeMinCount(wantCount int) int {
|
func computeMinCount(wantCount int) int {
|
||||||
if os.Getenv("CI") != "" {
|
if os.Getenv("CI") != "" {
|
||||||
return wantCount * 60 / 100
|
return wantCount * 60 / 100
|
||||||
|
|
118
pkg/middlewares/ratelimiter/redis_limiter.go
Normal file
118
pkg/middlewares/ratelimiter/redis_limiter.go
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
package ratelimiter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
ptypes "github.com/traefik/paerser/types"
|
||||||
|
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
)
|
||||||
|
|
||||||
|
const redisPrefix = "rate:"
|
||||||
|
|
||||||
|
type redisLimiter struct {
|
||||||
|
rate rate.Limit // reqs/s
|
||||||
|
burst int64
|
||||||
|
maxDelay time.Duration
|
||||||
|
period ptypes.Duration
|
||||||
|
logger *zerolog.Logger
|
||||||
|
ttl int
|
||||||
|
client Rediser
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRedisLimiter(ctx context.Context, rate rate.Limit, burst int64, maxDelay time.Duration, ttl int, config dynamic.RateLimit, logger *zerolog.Logger) (limiter, error) {
|
||||||
|
options := &redis.UniversalOptions{
|
||||||
|
Addrs: config.Redis.Endpoints,
|
||||||
|
Username: config.Redis.Username,
|
||||||
|
Password: config.Redis.Password,
|
||||||
|
DB: config.Redis.DB,
|
||||||
|
PoolSize: config.Redis.PoolSize,
|
||||||
|
MinIdleConns: config.Redis.MinIdleConns,
|
||||||
|
MaxActiveConns: config.Redis.MaxActiveConns,
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Redis.DialTimeout != nil && *config.Redis.DialTimeout > 0 {
|
||||||
|
options.DialTimeout = time.Duration(*config.Redis.DialTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Redis.ReadTimeout != nil {
|
||||||
|
if *config.Redis.ReadTimeout > 0 {
|
||||||
|
options.ReadTimeout = time.Duration(*config.Redis.ReadTimeout)
|
||||||
|
} else {
|
||||||
|
options.ReadTimeout = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Redis.WriteTimeout != nil {
|
||||||
|
if *config.Redis.ReadTimeout > 0 {
|
||||||
|
options.WriteTimeout = time.Duration(*config.Redis.WriteTimeout)
|
||||||
|
} else {
|
||||||
|
options.WriteTimeout = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Redis.TLS != nil {
|
||||||
|
var err error
|
||||||
|
options.TLSConfig, err = config.Redis.TLS.CreateTLSConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating TLS config: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &redisLimiter{
|
||||||
|
rate: rate,
|
||||||
|
burst: burst,
|
||||||
|
period: config.Period,
|
||||||
|
maxDelay: maxDelay,
|
||||||
|
logger: logger,
|
||||||
|
ttl: ttl,
|
||||||
|
client: redis.NewUniversalClient(options),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *redisLimiter) Allow(ctx context.Context, source string) (*time.Duration, error) {
|
||||||
|
ok, delay, err := r.evaluateScript(ctx, source)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("evaluating script: %w", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return delay, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *redisLimiter) evaluateScript(ctx context.Context, key string) (bool, *time.Duration, error) {
|
||||||
|
if r.rate == rate.Inf {
|
||||||
|
return true, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
params := []interface{}{
|
||||||
|
float64(r.rate / 1000000),
|
||||||
|
r.burst,
|
||||||
|
r.ttl,
|
||||||
|
time.Now().UnixMicro(),
|
||||||
|
r.maxDelay.Microseconds(),
|
||||||
|
}
|
||||||
|
v, err := AllowTokenBucketScript.Run(ctx, r.client, []string{redisPrefix + key}, params...).Result()
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, fmt.Errorf("running script: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
values := v.([]interface{})
|
||||||
|
ok, err := strconv.ParseBool(values[0].(string))
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, fmt.Errorf("parsing ok value from redis rate lua script: %w", err)
|
||||||
|
}
|
||||||
|
delay, err := strconv.ParseFloat(values[1].(string), 64)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, fmt.Errorf("parsing delay value from redis rate lua script: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
microDelay := time.Duration(delay * float64(time.Microsecond))
|
||||||
|
return ok, µDelay, nil
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ data:
|
||||||
users: |2
|
users: |2
|
||||||
dGVzdDokYXByMSRINnVza2trVyRJZ1hMUDZld1RyU3VCa1RycUU4d2ovCnRlc3QyOiRhcHIxJGQ5
|
dGVzdDokYXByMSRINnVza2trVyRJZ1hMUDZld1RyU3VCa1RycUU4d2ovCnRlc3QyOiRhcHIxJGQ5
|
||||||
aHI5SEJCJDRIeHdnVWlyM0hQNEVzZ2dQL1FObzAK
|
aHI5SEJCJDRIeHdnVWlyM0hQNEVzZ2dQL1FObzAK
|
||||||
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Secret
|
kind: Secret
|
||||||
|
|
82
pkg/provider/kubernetes/crd/fixtures/with_ratelimit.yml
Normal file
82
pkg/provider/kubernetes/crd/fixtures/with_ratelimit.yml
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: ratelimit
|
||||||
|
namespace: default
|
||||||
|
|
||||||
|
spec:
|
||||||
|
rateLimit:
|
||||||
|
period: 1m
|
||||||
|
average: 6
|
||||||
|
burst: 12
|
||||||
|
sourceCriterion:
|
||||||
|
ipStrategy:
|
||||||
|
excludedIPs:
|
||||||
|
- 127.0.0.1/32
|
||||||
|
- 192.168.1.7
|
||||||
|
|
||||||
|
redis:
|
||||||
|
secret: redissecret
|
||||||
|
endpoints:
|
||||||
|
- "127.0.0.1:6379"
|
||||||
|
tls:
|
||||||
|
certSecret: tlssecret
|
||||||
|
caSecret: casecret
|
||||||
|
db: 0
|
||||||
|
poolSize: 42
|
||||||
|
maxActiveConns: 42
|
||||||
|
readTimeout: 42s
|
||||||
|
writeTimeout: 42s
|
||||||
|
dialTimeout: 42s
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: redissecret
|
||||||
|
namespace: default
|
||||||
|
data:
|
||||||
|
username: dXNlcg== # username: user
|
||||||
|
password: cGFzc3dvcmQ= # password: password
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: casecret
|
||||||
|
namespace: default
|
||||||
|
|
||||||
|
data:
|
||||||
|
ca: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: tlssecret
|
||||||
|
namespace: default
|
||||||
|
|
||||||
|
data:
|
||||||
|
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
|
||||||
|
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: test2.route
|
||||||
|
namespace: default
|
||||||
|
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- web
|
||||||
|
|
||||||
|
routes:
|
||||||
|
- match: Host(`foo.com`) && PathPrefix(`/will-be-limited`)
|
||||||
|
priority: 12
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: whoami
|
||||||
|
port: 80
|
||||||
|
middlewares:
|
||||||
|
- name: ratelimit
|
|
@ -266,7 +266,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
rateLimit, err := createRateLimitMiddleware(middleware.Spec.RateLimit)
|
rateLimit, err := createRateLimitMiddleware(client, middleware.Namespace, middleware.Spec.RateLimit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error().Err(err).Msg("Error while reading rateLimit middleware")
|
logger.Error().Err(err).Msg("Error while reading rateLimit middleware")
|
||||||
continue
|
continue
|
||||||
|
@ -686,7 +686,7 @@ func createCompressMiddleware(compress *traefikv1alpha1.Compress) *dynamic.Compr
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func createRateLimitMiddleware(rateLimit *traefikv1alpha1.RateLimit) (*dynamic.RateLimit, error) {
|
func createRateLimitMiddleware(client Client, namespace string, rateLimit *traefikv1alpha1.RateLimit) (*dynamic.RateLimit, error) {
|
||||||
if rateLimit == nil {
|
if rateLimit == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
@ -713,9 +713,97 @@ func createRateLimitMiddleware(rateLimit *traefikv1alpha1.RateLimit) (*dynamic.R
|
||||||
rl.SourceCriterion = rateLimit.SourceCriterion
|
rl.SourceCriterion = rateLimit.SourceCriterion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rateLimit.Redis != nil {
|
||||||
|
rl.Redis = &dynamic.Redis{
|
||||||
|
DB: rateLimit.Redis.DB,
|
||||||
|
PoolSize: rateLimit.Redis.PoolSize,
|
||||||
|
MinIdleConns: rateLimit.Redis.MinIdleConns,
|
||||||
|
MaxActiveConns: rateLimit.Redis.MaxActiveConns,
|
||||||
|
}
|
||||||
|
rl.Redis.SetDefaults()
|
||||||
|
|
||||||
|
if len(rateLimit.Redis.Endpoints) > 0 {
|
||||||
|
rl.Redis.Endpoints = rateLimit.Redis.Endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
if rateLimit.Redis.TLS != nil {
|
||||||
|
rl.Redis.TLS = &types.ClientTLS{
|
||||||
|
InsecureSkipVerify: rateLimit.Redis.TLS.InsecureSkipVerify,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rateLimit.Redis.TLS.CASecret) > 0 {
|
||||||
|
caSecret, err := loadCASecret(namespace, rateLimit.Redis.TLS.CASecret, client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load auth ca secret: %w", err)
|
||||||
|
}
|
||||||
|
rl.Redis.TLS.CA = caSecret
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rateLimit.Redis.TLS.CertSecret) > 0 {
|
||||||
|
authSecretCert, authSecretKey, err := loadAuthTLSSecret(namespace, rateLimit.Redis.TLS.CertSecret, client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load auth secret: %w", err)
|
||||||
|
}
|
||||||
|
rl.Redis.TLS.Cert = authSecretCert
|
||||||
|
rl.Redis.TLS.Key = authSecretKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rateLimit.Redis.DialTimeout != nil {
|
||||||
|
err := rl.Redis.DialTimeout.Set(rateLimit.Redis.DialTimeout.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rateLimit.Redis.ReadTimeout != nil {
|
||||||
|
err := rl.Redis.ReadTimeout.Set(rateLimit.Redis.ReadTimeout.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rateLimit.Redis.WriteTimeout != nil {
|
||||||
|
err := rl.Redis.WriteTimeout.Set(rateLimit.Redis.WriteTimeout.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rateLimit.Redis.Secret != "" {
|
||||||
|
var err error
|
||||||
|
rl.Redis.Username, rl.Redis.Password, err = loadRedisCredentials(namespace, rateLimit.Redis.Secret, client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return rl, nil
|
return rl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadRedisCredentials(namespace, secretName string, k8sClient Client) (string, string, error) {
|
||||||
|
secret, exists, err := k8sClient.GetSecret(namespace, secretName)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("failed to fetch secret '%s/%s': %w", namespace, secretName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return "", "", fmt.Errorf("secret '%s/%s' not found", namespace, secretName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if secret == nil {
|
||||||
|
return "", "", fmt.Errorf("data for secret '%s/%s' must not be nil", namespace, secretName)
|
||||||
|
}
|
||||||
|
|
||||||
|
username, usernameExists := secret.Data["username"]
|
||||||
|
password, passwordExists := secret.Data["password"]
|
||||||
|
if !usernameExists || !passwordExists {
|
||||||
|
return "", "", fmt.Errorf("secret '%s/%s' must contain both username and password keys", secret.Namespace, secret.Name)
|
||||||
|
}
|
||||||
|
return string(username), string(password), nil
|
||||||
|
}
|
||||||
|
|
||||||
func createRetryMiddleware(retry *traefikv1alpha1.Retry) (*dynamic.Retry, error) {
|
func createRetryMiddleware(retry *traefikv1alpha1.Retry) (*dynamic.Retry, error) {
|
||||||
if retry == nil {
|
if retry == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|
|
@ -1835,6 +1835,84 @@ func TestLoadIngressRoutes(t *testing.T) {
|
||||||
TLS: &dynamic.TLSConfiguration{},
|
TLS: &dynamic.TLSConfiguration{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "Simple Ingress Route with middleware ratelimit",
|
||||||
|
allowCrossNamespace: true,
|
||||||
|
paths: []string{"services.yml", "with_ratelimit.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{},
|
||||||
|
ServersTransports: map[string]*dynamic.TCPServersTransport{},
|
||||||
|
},
|
||||||
|
HTTP: &dynamic.HTTPConfiguration{
|
||||||
|
Routers: map[string]*dynamic.Router{
|
||||||
|
"default-test2-route-3c9bf014491ebdba74f7": {
|
||||||
|
EntryPoints: []string{"web"},
|
||||||
|
Service: "default-test2-route-3c9bf014491ebdba74f7",
|
||||||
|
Rule: "Host(`foo.com`) && PathPrefix(`/will-be-limited`)",
|
||||||
|
Priority: 12,
|
||||||
|
Middlewares: []string{"default-ratelimit"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Middlewares: map[string]*dynamic.Middleware{
|
||||||
|
"default-ratelimit": {
|
||||||
|
RateLimit: &dynamic.RateLimit{
|
||||||
|
Average: 6,
|
||||||
|
Burst: 12,
|
||||||
|
Period: ptypes.Duration(60 * time.Second),
|
||||||
|
SourceCriterion: &dynamic.SourceCriterion{
|
||||||
|
IPStrategy: &dynamic.IPStrategy{
|
||||||
|
ExcludedIPs: []string{"127.0.0.1/32", "192.168.1.7"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Redis: &dynamic.Redis{
|
||||||
|
Endpoints: []string{"127.0.0.1:6379"},
|
||||||
|
Username: "user",
|
||||||
|
Password: "password",
|
||||||
|
TLS: &types.ClientTLS{
|
||||||
|
CA: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----",
|
||||||
|
Cert: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----",
|
||||||
|
Key: "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----",
|
||||||
|
},
|
||||||
|
DB: 0,
|
||||||
|
PoolSize: 42,
|
||||||
|
MaxActiveConns: 42,
|
||||||
|
ReadTimeout: pointer(ptypes.Duration(42 * time.Second)),
|
||||||
|
WriteTimeout: pointer(ptypes.Duration(42 * time.Second)),
|
||||||
|
DialTimeout: pointer(ptypes.Duration(42 * time.Second)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Services: map[string]*dynamic.Service{
|
||||||
|
"default-test2-route-3c9bf014491ebdba74f7": {
|
||||||
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
|
Servers: []dynamic.Server{
|
||||||
|
{
|
||||||
|
URL: "http://10.10.0.1:80",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "http://10.10.0.2:80",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PassHostHeader: pointer(true),
|
||||||
|
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||||
|
FlushInterval: ptypes.Duration(100 * time.Millisecond),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ServersTransports: map[string]*dynamic.ServersTransport{},
|
||||||
|
},
|
||||||
|
TLS: &dynamic.TLSConfiguration{},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
desc: "Middlewares in ingress route config are normalized",
|
desc: "Middlewares in ingress route config are normalized",
|
||||||
allowCrossNamespace: true,
|
allowCrossNamespace: true,
|
||||||
|
|
|
@ -170,7 +170,7 @@ type ForwardAuth struct {
|
||||||
// If not set or empty then all request headers are passed.
|
// If not set or empty then all request headers are passed.
|
||||||
AuthRequestHeaders []string `json:"authRequestHeaders,omitempty"`
|
AuthRequestHeaders []string `json:"authRequestHeaders,omitempty"`
|
||||||
// TLS defines the configuration used to secure the connection to the authentication server.
|
// TLS defines the configuration used to secure the connection to the authentication server.
|
||||||
TLS *ClientTLS `json:"tls,omitempty"`
|
TLS *ClientTLSWithCAOptional `json:"tls,omitempty"`
|
||||||
// AddAuthCookiesToResponse defines the list of cookies to copy from the authentication server response to the response.
|
// AddAuthCookiesToResponse defines the list of cookies to copy from the authentication server response to the response.
|
||||||
AddAuthCookiesToResponse []string `json:"addAuthCookiesToResponse,omitempty"`
|
AddAuthCookiesToResponse []string `json:"addAuthCookiesToResponse,omitempty"`
|
||||||
// HeaderField defines a header field to store the authenticated user.
|
// HeaderField defines a header field to store the authenticated user.
|
||||||
|
@ -186,16 +186,12 @@ type ForwardAuth struct {
|
||||||
PreserveRequestMethod bool `json:"preserveRequestMethod,omitempty"`
|
PreserveRequestMethod bool `json:"preserveRequestMethod,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientTLS holds the client TLS configuration.
|
// +k8s:deepcopy-gen=true
|
||||||
type ClientTLS struct {
|
|
||||||
// CASecret is the name of the referenced Kubernetes Secret containing the CA to validate the server certificate.
|
// ClientTLSWithCAOptional holds the client TLS configuration.
|
||||||
// The CA certificate is extracted from key `tls.ca` or `ca.crt`.
|
// TODO: This has to be removed once the CAOptional option is removed.
|
||||||
CASecret string `json:"caSecret,omitempty"`
|
type ClientTLSWithCAOptional struct {
|
||||||
// CertSecret is the name of the referenced Kubernetes Secret containing the client certificate.
|
ClientTLS `json:",inline"`
|
||||||
// The client certificate is extracted from the keys `tls.crt` and `tls.key`.
|
|
||||||
CertSecret string `json:"certSecret,omitempty"`
|
|
||||||
// InsecureSkipVerify defines whether the server certificates should be validated.
|
|
||||||
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"`
|
|
||||||
|
|
||||||
// Deprecated: TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634).
|
// Deprecated: TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634).
|
||||||
CAOptional *bool `json:"caOptional,omitempty"`
|
CAOptional *bool `json:"caOptional,omitempty"`
|
||||||
|
@ -225,6 +221,65 @@ type RateLimit struct {
|
||||||
// If several strategies are defined at the same time, an error will be raised.
|
// 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 *dynamic.SourceCriterion `json:"sourceCriterion,omitempty"`
|
SourceCriterion *dynamic.SourceCriterion `json:"sourceCriterion,omitempty"`
|
||||||
|
// Redis hold the configs of Redis as bucket in rate limiter.
|
||||||
|
Redis *Redis `json:"redis,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +k8s:deepcopy-gen=true
|
||||||
|
|
||||||
|
// Redis contains the configuration for using Redis in middleware.
|
||||||
|
// In a Kubernetes setup, the username and password are stored in a Secret file within the same namespace as the middleware.
|
||||||
|
type Redis struct {
|
||||||
|
// Endpoints contains either a single address or a seed list of host:port addresses.
|
||||||
|
// Default value is ["localhost:6379"].
|
||||||
|
Endpoints []string `json:"endpoints,omitempty"`
|
||||||
|
// TLS defines TLS-specific configurations, including the CA, certificate, and key,
|
||||||
|
// which can be provided as a file path or file content.
|
||||||
|
TLS *ClientTLS `json:"tls,omitempty"`
|
||||||
|
// Secret defines the name of the referenced Kubernetes Secret containing Redis credentials.
|
||||||
|
Secret string `json:"secret,omitempty"`
|
||||||
|
// DB defines the Redis database that will be selected after connecting to the server.
|
||||||
|
DB int `json:"db,omitempty"`
|
||||||
|
// PoolSize defines the initial number of socket connections.
|
||||||
|
// If the pool runs out of available connections, additional ones will be created beyond PoolSize.
|
||||||
|
// This can be limited using MaxActiveConns.
|
||||||
|
// // Default value is 0, meaning 10 connections per every available CPU as reported by runtime.GOMAXPROCS.
|
||||||
|
PoolSize int `json:"poolSize,omitempty"`
|
||||||
|
// MinIdleConns defines the minimum number of idle connections.
|
||||||
|
// Default value is 0, and idle connections are not closed by default.
|
||||||
|
MinIdleConns int `json:"minIdleConns,omitempty"`
|
||||||
|
// MaxActiveConns defines the maximum number of connections allocated by the pool at a given time.
|
||||||
|
// Default value is 0, meaning there is no limit.
|
||||||
|
MaxActiveConns int `json:"maxActiveConns,omitempty"`
|
||||||
|
// ReadTimeout defines the timeout for socket read operations.
|
||||||
|
// Default value is 3 seconds.
|
||||||
|
// +kubebuilder:validation:Pattern="^([0-9]+(ns|us|µs|ms|s|m|h)?)+$"
|
||||||
|
// +kubebuilder:validation:XIntOrString
|
||||||
|
ReadTimeout *intstr.IntOrString `json:"readTimeout,omitempty"`
|
||||||
|
// WriteTimeout defines the timeout for socket write operations.
|
||||||
|
// Default value is 3 seconds.
|
||||||
|
// +kubebuilder:validation:Pattern="^([0-9]+(ns|us|µs|ms|s|m|h)?)+$"
|
||||||
|
// +kubebuilder:validation:XIntOrString
|
||||||
|
WriteTimeout *intstr.IntOrString `json:"writeTimeout,omitempty"`
|
||||||
|
// DialTimeout sets the timeout for establishing new connections.
|
||||||
|
// Default value is 5 seconds.
|
||||||
|
// +kubebuilder:validation:Pattern="^([0-9]+(ns|us|µs|ms|s|m|h)?)+$"
|
||||||
|
// +kubebuilder:validation:XIntOrString
|
||||||
|
DialTimeout *intstr.IntOrString `json:"dialTimeout,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +k8s:deepcopy-gen=true
|
||||||
|
|
||||||
|
// ClientTLS holds the client TLS configuration.
|
||||||
|
type ClientTLS struct {
|
||||||
|
// CASecret is the name of the referenced Kubernetes Secret containing the CA to validate the server certificate.
|
||||||
|
// The CA certificate is extracted from key `tls.ca` or `ca.crt`.
|
||||||
|
CASecret string `json:"caSecret,omitempty"`
|
||||||
|
// CertSecret is the name of the referenced Kubernetes Secret containing the client certificate.
|
||||||
|
// The client certificate is extracted from the keys `tls.crt` and `tls.key`.
|
||||||
|
CertSecret string `json:"certSecret,omitempty"`
|
||||||
|
// InsecureSkipVerify defines whether the server certificates should be validated.
|
||||||
|
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// +k8s:deepcopy-gen=true
|
// +k8s:deepcopy-gen=true
|
||||||
|
|
|
@ -146,11 +146,6 @@ func (in *ClientAuth) DeepCopy() *ClientAuth {
|
||||||
// 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 *ClientTLS) DeepCopyInto(out *ClientTLS) {
|
func (in *ClientTLS) DeepCopyInto(out *ClientTLS) {
|
||||||
*out = *in
|
*out = *in
|
||||||
if in.CAOptional != nil {
|
|
||||||
in, out := &in.CAOptional, &out.CAOptional
|
|
||||||
*out = new(bool)
|
|
||||||
**out = **in
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,6 +159,28 @@ func (in *ClientTLS) DeepCopy() *ClientTLS {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *ClientTLSWithCAOptional) DeepCopyInto(out *ClientTLSWithCAOptional) {
|
||||||
|
*out = *in
|
||||||
|
out.ClientTLS = in.ClientTLS
|
||||||
|
if in.CAOptional != nil {
|
||||||
|
in, out := &in.CAOptional, &out.CAOptional
|
||||||
|
*out = new(bool)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientTLSWithCAOptional.
|
||||||
|
func (in *ClientTLSWithCAOptional) DeepCopy() *ClientTLSWithCAOptional {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(ClientTLSWithCAOptional)
|
||||||
|
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
|
||||||
|
@ -265,7 +282,7 @@ func (in *ForwardAuth) DeepCopyInto(out *ForwardAuth) {
|
||||||
}
|
}
|
||||||
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(ClientTLSWithCAOptional)
|
||||||
(*in).DeepCopyInto(*out)
|
(*in).DeepCopyInto(*out)
|
||||||
}
|
}
|
||||||
if in.AddAuthCookiesToResponse != nil {
|
if in.AddAuthCookiesToResponse != nil {
|
||||||
|
@ -1053,6 +1070,11 @@ func (in *RateLimit) DeepCopyInto(out *RateLimit) {
|
||||||
*out = new(dynamic.SourceCriterion)
|
*out = new(dynamic.SourceCriterion)
|
||||||
(*in).DeepCopyInto(*out)
|
(*in).DeepCopyInto(*out)
|
||||||
}
|
}
|
||||||
|
if in.Redis != nil {
|
||||||
|
in, out := &in.Redis, &out.Redis
|
||||||
|
*out = new(Redis)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1066,6 +1088,47 @@ func (in *RateLimit) DeepCopy() *RateLimit {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *Redis) DeepCopyInto(out *Redis) {
|
||||||
|
*out = *in
|
||||||
|
if in.Endpoints != nil {
|
||||||
|
in, out := &in.Endpoints, &out.Endpoints
|
||||||
|
*out = make([]string, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
|
if in.TLS != nil {
|
||||||
|
in, out := &in.TLS, &out.TLS
|
||||||
|
*out = new(ClientTLS)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.ReadTimeout != nil {
|
||||||
|
in, out := &in.ReadTimeout, &out.ReadTimeout
|
||||||
|
*out = new(intstr.IntOrString)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.WriteTimeout != nil {
|
||||||
|
in, out := &in.WriteTimeout, &out.WriteTimeout
|
||||||
|
*out = new(intstr.IntOrString)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.DialTimeout != nil {
|
||||||
|
in, out := &in.DialTimeout, &out.DialTimeout
|
||||||
|
*out = new(intstr.IntOrString)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Redis.
|
||||||
|
func (in *Redis) DeepCopy() *Redis {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(Redis)
|
||||||
|
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 *ResponseForwarding) DeepCopyInto(out *ResponseForwarding) {
|
func (in *ResponseForwarding) DeepCopyInto(out *ResponseForwarding) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue