Repository: traefik/traefik (language: go)

Issue:
KV Provider support Redis in cluster mode
### Welcome!

- [X] Yes, I've searched similar issues on [GitHub](https://github.com/traefik/traefik/issues) and didn't find any.
- [X] Yes, I've searched similar issues on the [Traefik community forum](https://community.traefik.io) and didn't find any.

### What did you expect to see?

Now traefik does not support the cluster mode of redis. I tried to search and found that there was an issue (https://github.com/traefik/traefik/issues/9120) last year, it was because the dependent library did not support it.

I just saw that this dependent library already supports the redis cluster mode last month.  Can you follow up on this change? The redis cluster mode is very important.

https://github.com/kvtools/redis/blob/main/redis.go
https://github.com/kvtools/redis/commit/2acbf1027ac48c0a5b732a60a8d686e751f35d86



Discussion / hints:

Fix patch:
diff --git a/docs/content/providers/redis.md b/docs/content/providers/redis.md
index bd61e103c7..3095314236 100644
--- a/docs/content/providers/redis.md
+++ b/docs/content/providers/redis.md
@@ -229,3 +229,166 @@ providers:
 ```bash tab="CLI"
 --providers.redis.tls.insecureSkipVerify=true
 ```
+
+### `sentinel`
+
+_Optional_
+
+Defines the Sentinel configuration used to interact with Redis Sentinel.
+
+#### `masterName`
+
+_Required_
+
+`masterName` is the name of the Sentinel master.
+
+```yaml tab="File (YAML)"
+providers:
+  redis:
+    sentinel:
+      masterName: my-master
+```
+
+```toml tab="File (TOML)"
+[providers.redis.sentinel]
+  masterName = "my-master"
+```
+
+```bash tab="CLI"
+--providers.redis.sentinel.masterName=my-master
+```
+
+#### `username`
+
+_Optional_
+
+`username` is the username for Sentinel authentication.
+
+```yaml tab="File (YAML)"
+providers:
+  redis:
+    sentinel:
+      username: user
+```
+
+```toml tab="File (TOML)"
+[providers.redis.sentinel]
+  username = "user"
+```
+
+```bash tab="CLI"
+--providers.redis.sentinel.username=user
+```
+
+#### `password`
+
+_Optional_
+
+`password` is the password for Sentinel authentication.
+
+```yaml tab="File (YAML)"
+providers:
+  redis:
+    sentinel:
+      password: password
+```
+
+```toml tab="File (TOML)"
+[providers.redis.sentinel]
+  password = "password"
+```
+
+```bash tab="CLI"
+--providers.redis.sentinel.password=password
+```
+
+#### `latencyStrategy`
+
+_Optional, Default=false_
+
+`latencyStrategy` defines whether to route commands to the closest master or replica nodes 
+(mutually exclusive with RandomStrategy and ReplicaStrategy).
+
+```yaml tab="File (YAML)"
+providers:
+  redis:
+    sentinel:
+      latencyStrategy: true
+```
+
+```toml tab="File (TOML)"
+[providers.redis.sentinel]
+latencyStrategy = true
+```
+
+```bash tab="CLI"
+--providers.redis.sentinel.latencyStrategy=true
+```
+
+#### `randomStrategy`
+
+_Optional, Default=false_
+
+`randomStrategy` defines whether to route commands randomly to master or replica nodes 
+(mutually exclusive with LatencyStrategy and ReplicaStrategy).
+
+```yaml tab="File (YAML)"
+providers:
+  redis:
+    sentinel:
+      randomStrategy: true
+```
+
+```toml tab="File (TOML)"
+[providers.redis.sentinel]
+randomStrategy = true
+```
+
+```bash tab="CLI"
+--providers.redis.sentinel.randomStrategy=true
+```
+
+#### `replicaStrategy`
+
+_Optional, Default=false_
+
+`replicaStrategy` Defines whether to route all commands to replica nodes 
+(mutually exclusive with LatencyStrategy and RandomStrategy).
+
+```yaml tab="File (YAML)"
+providers:
+  redis:
+    sentinel:
+      replicaStrategy: true
+```
+
+```toml tab="File (TOML)"
+[providers.redis.sentinel]
+replicaStrategy = true
+```
+
+```bash tab="CLI"
+--providers.redis.sentinel.replicaStrategy=true
+```
+
+#### `useDisconnectedReplicas`
+
+_Optional, Default=false_
+
+`useDisconnectedReplicas` defines whether to use replicas disconnected with master when cannot get connected replicas.
+
+```yaml tab="File (YAML)"
+providers:
+  redis:
+    sentinel:
+      useDisconnectedReplicas: true
+```
+
+```toml tab="File (TOML)"
+[providers.redis.sentinel]
+useDisconnectedReplicas = true
+```
+
+```bash tab="CLI"
+--providers.redis.sentinel.useDisconnectedReplicas=true
+```
diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md
index fbc3eb3898..a0415e1b42 100644
--- a/docs/content/reference/static-configuration/cli-ref.md
+++ b/docs/content/reference/static-configuration/cli-ref.md
@@ -906,6 +906,27 @@ Password for authentication.
 `--providers.redis.rootkey`:  
 Root key used for KV store. (Default: ```traefik```)
 
+`--providers.redis.sentinel.latencystrategy`:  
+Defines whether to route commands to the closest master or replica nodes (mutually exclusive with RandomStrategy and ReplicaStrategy). (Default: ```false```)
+
+`--providers.redis.sentinel.mastername`:  
+Name of the master.
+
+`--providers.redis.sentinel.password`:  
+Password for Sentinel authentication.
+
+`--providers.redis.sentinel.randomstrategy`:  
+Defines whether to route commands randomly to master or replica nodes (mutually exclusive with LatencyStrategy and ReplicaStrategy). (Default: ```false```)
+
+`--providers.redis.sentinel.replicastrategy`:  
+Defines whether to route all commands to replica nodes (mutually exclusive with LatencyStrategy and RandomStrategy). (Default: ```false```)
+
+`--providers.redis.sentinel.usedisconnectedreplicas`:  
+Use replicas disconnected with master when cannot get connected replicas. (Default: ```false```)
+
+`--providers.redis.sentinel.username`:  
+Username for Sentinel authentication.
+
 `--providers.redis.tls.ca`:  
 TLS CA
 
diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md
index 9f59741ce7..a7b3ace47c 100644
--- a/docs/content/reference/static-configuration/env-ref.md
+++ b/docs/content/reference/static-configuration/env-ref.md
@@ -906,6 +906,27 @@ Password for authentication.
 `TRAEFIK_PROVIDERS_REDIS_ROOTKEY`:  
 Root key used for KV store. (Default: ```traefik```)
 
+`TRAEFIK_PROVIDERS_REDIS_SENTINEL_LATENCYSTRATEGY`:  
+Defines whether to route commands to the closest master or replica nodes (mutually exclusive with RandomStrategy and ReplicaStrategy). (Default: ```false```)
+
+`TRAEFIK_PROVIDERS_REDIS_SENTINEL_MASTERNAME`:  
+Name of the master.
+
+`TRAEFIK_PROVIDERS_REDIS_SENTINEL_PASSWORD`:  
+Password for Sentinel authentication.
+
+`TRAEFIK_PROVIDERS_REDIS_SENTINEL_RANDOMSTRATEGY`:  
+Defines whether to route commands randomly to master or replica nodes (mutually exclusive with LatencyStrategy and ReplicaStrategy). (Default: ```false```)
+
+`TRAEFIK_PROVIDERS_REDIS_SENTINEL_REPLICASTRATEGY`:  
+Defines whether to route all commands to replica nodes (mutually exclusive with LatencyStrategy and RandomStrategy). (Default: ```false```)
+
+`TRAEFIK_PROVIDERS_REDIS_SENTINEL_USEDISCONNECTEDREPLICAS`:  
+Use replicas disconnected with master when cannot get connected replicas. (Default: ```false```)
+
+`TRAEFIK_PROVIDERS_REDIS_SENTINEL_USERNAME`:  
+Username for Sentinel authentication.
+
 `TRAEFIK_PROVIDERS_REDIS_TLS_CA`:  
 TLS CA
 
diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml
index c33fbf3091..42dda1fbf7 100644
--- a/docs/content/reference/static-configuration/file.toml
+++ b/docs/content/reference/static-configuration/file.toml
@@ -247,6 +247,14 @@
       cert = "foobar"
       key = "foobar"
       insecureSkipVerify = true
+    [providers.redis.sentinel]
+      masterName = "foobar"
+      username = "foobar"
+      password = "foobar"
+      latencyStrategy = true
+      randomStrategy = true
+      replicaStrategy = true
+      useDisconnectedReplicas = true
   [providers.http]
     endpoint = "foobar"
     pollInterval = "42s"
diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml
index 67cf113983..1497f57e54 100644
--- a/docs/content/reference/static-configuration/file.yaml
+++ b/docs/content/reference/static-configuration/file.yaml
@@ -275,6 +275,14 @@ providers:
       cert: foobar
       key: foobar
       insecureSkipVerify: true
+    sentinel:
+      masterName: foobar
+      username: foobar
+      password: foobar
+      latencyStrategy: true
+      randomStrategy: true
+      replicaStrategy: true
+      useDisconnectedReplicas: true
   http:
     endpoint: foobar
     pollInterval: 42s
diff --git a/go.mod b/go.mod
index 22eb59b22d..62ac8306b0 100644
--- a/go.mod
+++ b/go.mod
@@ -37,7 +37,7 @@ require (
 	github.com/klauspost/compress v1.17.1
 	github.com/kvtools/consul v1.0.2
 	github.com/kvtools/etcdv3 v1.0.2
-	github.com/kvtools/redis v1.0.2
+	github.com/kvtools/redis v1.1.0
 	github.com/kvtools/valkeyrie v1.0.0
 	github.com/kvtools/zookeeper v1.0.2
 	github.com/mailgun/ttlmap v0.0.0-20170619185759-c1c17f74874f
@@ -189,7 +189,6 @@ require (
 	github.com/go-openapi/jsonpointer v0.19.5 // indirect
 	github.com/go-openapi/jsonreference v0.20.0 // indirect
 	github.com/go-openapi/swag v0.19.14 // indirect
-	github.com/go-redis/redis/v8 v8.11.5 // indirect
 	github.com/go-resty/resty/v2 v2.7.0 // indirect
 	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
 	github.com/go-zookeeper/zk v1.0.3 // indirect
@@ -278,6 +277,7 @@ require (
 	github.com/nrdcg/nodion v0.1.0 // indirect
 	github.com/nrdcg/porkbun v0.2.0 // indirect
 	github.com/nzdjb/go-metaname v1.0.0 // indirect
+	github.com/onsi/ginkgo v1.16.5 // indirect
 	github.com/onsi/ginkgo/v2 v2.9.5 // indirect
 	github.com/opencontainers/go-digest v1.0.0 // indirect
 	github.com/opencontainers/image-spec v1.0.2 // indirect
@@ -294,6 +294,7 @@ require (
 	github.com/prometheus/procfs v0.8.0 // indirect
 	github.com/quic-go/qpack v0.4.0 // indirect
 	github.com/quic-go/qtls-go1-20 v0.3.4 // indirect
+	github.com/redis/go-redis/v9 v9.2.1 // indirect
 	github.com/sacloud/api-client-go v0.2.8 // indirect
 	github.com/sacloud/go-http v0.1.6 // indirect
 	github.com/sacloud/iaas-api-go v1.11.1 // indirect
diff --git a/go.sum b/go.sum
index b4a954782c..ea218e4294 100644
--- a/go.sum
+++ b/go.sum
@@ -287,6 +287,10 @@ github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8
 github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
 github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
 github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
+github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
+github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
+github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
+github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
 github.com/buger/goterm v1.0.0 h1:ZB6uUlY8+sjJyFGzz2WpRqX2XYPeXVgtZAOJMwOsTWM=
 github.com/buger/goterm v1.0.0/go.mod h1:16STi3LquiscTIHA8SXUNKEa/Cnu4ZHBH8NsCaWgso0=
 github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
@@ -741,8 +745,6 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
 github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
 github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
 github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
-github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
-github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
 github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
 github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
 github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
@@ -1169,8 +1171,8 @@ github.com/kvtools/consul v1.0.2 h1:ltPgs4Ld09Xaa7zrOJ/TewBYKAsr11/LRFpErdkb8AA=
 github.com/kvtools/consul v1.0.2/go.mod h1:bFnzfGJ5ZIRRXCBGBmwhJlLdEWOlrjOcS1WjyAQzaJA=
 github.com/kvtools/etcdv3 v1.0.2 h1:EB0mAtzqe1folE7m7Q6wnCXcGwaOmrYmsVmF3hNsTKI=
 github.com/kvtools/etcdv3 v1.0.2/go.mod h1:Xr6DbwqjuCEcXAIWmXxw0DX+N5BhuvablXgN90XeqMM=
-github.com/kvtools/redis v1.0.2 h1:D3GjGGtssJF2w8mniWtIxcT/YX9YnRc4jNCm0hrVygQ=
-github.com/kvtools/redis v1.0.2/go.mod h1:wuUNwwKOHi2TYxDxj1sGF74Jdg0jywydnatXtnOR3hA=
+github.com/kvtools/redis v1.1.0 h1:nXRAyh2nsaWiJyrX449/qHMc3SvGUqRqRXcrA/MplEo=
+github.com/kvtools/redis v1.1.0/go.mod h1:cqg3esJOIYMQ1qy5LVIbPZz9kuiBBcFREP2N5b9+Dn0=
 github.com/kvtools/valkeyrie v1.0.0 h1:LAITop2wPoYCMitR24GZZsW0b57hmI+ePD18VRTtOf0=
 github.com/kvtools/valkeyrie v1.0.0/go.mod h1:bDi/OdhJCSbGPMsCgUQl881yuEweKCSItAtTBI+ZjpU=
 github.com/kvtools/zookeeper v1.0.2 h1:uK0CzQa+mtKGxDDH+DeqXo2HC1Kx4hWXZ7pX/zS4aTo=
@@ -1586,6 +1588,8 @@ github.com/rancher/go-rancher-metadata v0.0.0-20200311180630-7f4c936a06ac/go.mod
 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 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg=
+github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
 github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052 h1:Qp27Idfgi6ACvFQat5+VJvlYToylpM/hcyLBI3WaKPA=
 github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052/go.mod h1:uvX/8buq8uVeiZiFht+0lqSLBHF+uGV8BrTv8W/SIwk=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
diff --git a/integration/fixtures/redis/sentinel.toml b/integration/fixtures/redis/sentinel.toml
new file mode 100644
index 0000000000..3d5a59ec4e
--- /dev/null
+++ b/integration/fixtures/redis/sentinel.toml
@@ -0,0 +1,19 @@
+[global]
+  checkNewVersion = false
+  sendAnonymousUsage = false
+
+[log]
+  level = "DEBUG"
+
+[entryPoints.web]
+  address = ":8000"
+
+[api]
+  insecure = true
+
+[providers.redis]
+  rootKey = "traefik"
+  endpoints = ["{{ .RedisAddress }}"]
+
+[providers.redis.sentinel]
+  masterName = "mymaster"
diff --git a/integration/resources/compose/config/sentinel_template.conf b/integration/resources/compose/config/sentinel_template.conf
new file mode 100644
index 0000000000..c9f5acf6d9
--- /dev/null
+++ b/integration/resources/compose/config/sentinel_template.conf
@@ -0,0 +1,5 @@
+port {{ .SentinelPort }}
+dir "/tmp"
+sentinel resolve-hostnames yes
+sentinel monitor mymaster master 6380 2
+sentinel deny-scripts-reconfig yes
diff --git a/integration/resources/compose/redis_sentinel.yml b/integration/resources/compose/redis_sentinel.yml
new file mode 100644
index 0000000000..261e694e57
--- /dev/null
+++ b/integration/resources/compose/redis_sentinel.yml
@@ -0,0 +1,61 @@
+version: "3.8"
+services:
+  master:
+    image: redis
+    container_name: redis-master
+    command: redis-server --port 6380
+    ports:
+      - 6380:6380
+    healthcheck:
+      test: redis-cli -p 6380 ping
+  node1:
+    image: redis
+    container_name: redis-node-1
+    ports:
+      - 6381:6381
+    command:  redis-server --port 6381 --slaveof redis-master 6380
+    healthcheck:
+      test: redis-cli -p 6381 ping
+  node2:
+    image: redis
+    container_name: redis-node-2
+    ports:
+      - 6382:6382
+    command: redis-server --port 6382 --slaveof redis-master 6380
+    healthcheck:
+      test: redis-cli -p 6382 ping
+  sentinel1:
+    image: redis
+    container_name: redis-sentinel-1
+    ports:
+      - 26379:26379
+    command: redis-sentinel /usr/local/etc/redis/conf/sentinel1.conf
+    healthcheck:
+      test: redis-cli -p 26379 ping
+    volumes:
+      - ./resources/compose/config:/usr/local/etc/redis/conf
+  sentinel2:
+    image: redis
+    container_name: redis-sentinel-2
+    ports:
+      - 36379:26379
+    command: redis-sentinel /usr/local/etc/redis/conf/sentinel2.conf
+    healthcheck:
+      test: redis-cli -p 36379 ping
+    volumes:
+      - ./resources/compose/config:/usr/local/etc/redis/conf
+  sentinel3:
+    image: redis
+    container_name: redis-sentinel-3
+    ports:
+      - 46379:26379
+    command: redis-sentinel /usr/local/etc/redis/conf/sentinel3.conf
+    healthcheck:
+      test: redis-cli -p 46379 ping
+    volumes:
+      - ./resources/compose/config:/usr/local/etc/redis/conf
+
+networks:
+  default:
+    name: traefik-test-network
+    external: true
diff --git a/pkg/provider/kv/redis/redis.go b/pkg/provider/kv/redis/redis.go
index 23f432bb1e..3005c8f2de 100644
--- a/pkg/provider/kv/redis/redis.go
+++ b/pkg/provider/kv/redis/redis.go
@@ -2,6 +2,7 @@ package redis
 
 import (
 	"context"
+	"errors"
 	"fmt"
 
 	"github.com/kvtools/redis"
@@ -20,6 +21,20 @@ type Provider struct {
 	Username string           `description:"Username for authentication." json:"username,omitempty" toml:"username,omitempty" yaml:"username,omitempty" loggable:"false"`
 	Password string           `description:"Password for authentication." json:"password,omitempty" toml:"password,omitempty" yaml:"password,omitempty" loggable:"false"`
 	DB       int              `description:"Database to be selected after connecting to the server." json:"db,omitempty" toml:"db,omitempty" yaml:"db,omitempty"`
+	Sentinel *Sentinel        `description:"Enable Sentinel support." json:"sentinel,omitempty" toml:"sentinel,omitempty" yaml:"sentinel,omitempty"`
+}
+
+// Sentinel holds the Redis Sentinel configuration.
+type Sentinel struct {
+	MasterName string `description:"Name of the master." json:"masterName,omitempty" toml:"masterName,omitempty" yaml:"masterName,omitempty" export:"true"`
+	Username   string `description:"Username for Sentinel authentication." json:"username,omitempty" toml:"username,omitempty" yaml:"username,omitempty" export:"true"`
+	Password   string `description:"Password for Sentinel authentication." json:"password,omitempty" toml:"password,omitempty" yaml:"password,omitempty" export:"true"`
+
+	LatencyStrategy bool `description:"Defines whether to route commands to the closest master or replica nodes (mutually exclusive with RandomStrategy and ReplicaStrategy)." json:"latencyStrategy,omitempty" toml:"latencyStrategy,omitempty" yaml:"latencyStrategy,omitempty" export:"true"`
+	RandomStrategy  bool `description:"Defines whether to route commands randomly to master or replica nodes (mutually exclusive with LatencyStrategy and ReplicaStrategy)." json:"randomStrategy,omitempty" toml:"randomStrategy,omitempty" yaml:"randomStrategy,omitempty" export:"true"`
+	ReplicaStrategy bool `description:"Defines whether to route all commands to replica nodes (mutually exclusive with LatencyStrategy and RandomStrategy)." json:"replicaStrategy,omitempty" toml:"replicaStrategy,omitempty" yaml:"replicaStrategy,omitempty" export:"true"`
+
+	UseDisconnectedReplicas bool `description:"Use replicas disconnected with master when cannot get connected replicas." json:"useDisconnectedReplicas,omitempty" toml:"useDisconnectedReplicas,omitempty" yaml:"useDisconnectedReplicas,omitempty" export:"true"`
 }
 
 // SetDefaults sets the default values.
@@ -44,5 +59,26 @@ func (p *Provider) Init() error {
 		}
 	}
 
+	if p.Sentinel != nil {
+		switch {
+		case p.Sentinel.LatencyStrategy && !(p.Sentinel.RandomStrategy || p.Sentinel.ReplicaStrategy):
+		case p.Sentinel.RandomStrategy && !(p.Sentinel.LatencyStrategy || p.Sentinel.ReplicaStrategy):
+		case p.Sentinel.ReplicaStrategy && !(p.Sentinel.RandomStrategy || p.Sentinel.LatencyStrategy):
+			return errors.New("latencyStrategy, randomStrategy and replicaStrategy options are mutually exclusive, please use only one of those options")
+		}
+
+		clusterClient := p.Sentinel.LatencyStrategy || p.Sentinel.RandomStrategy
+		config.Sentinel = &redis.Sentinel{
+			MasterName:              p.Sentinel.MasterName,
+			Username:                p.Sentinel.Username,
+			Password:                p.Sentinel.Password,
+			ClusterClient:           clusterClient,
+			RouteByLatency:          p.Sentinel.LatencyStrategy,
+			RouteRandomly:           p.Sentinel.RandomStrategy,
+			ReplicaOnly:             p.Sentinel.ReplicaStrategy,
+			UseDisconnectedReplicas: p.Sentinel.UseDisconnectedReplicas,
+		}
+	}
+
 	return p.Provider.Init(redis.StoreName, "redis", config)
 }


Test patch:
diff --git a/integration/redis_test.go b/integration/redis_test.go
index b642691c4b..6407a3a359 100644
--- a/integration/redis_test.go
+++ b/integration/redis_test.go
@@ -4,12 +4,18 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
+	"errors"
+	"fmt"
+	"io/fs"
 	"net"
 	"net/http"
 	"os"
 	"path/filepath"
+	"strings"
+	"text/template"
 	"time"
 
+	"github.com/fatih/structs"
 	"github.com/go-check/check"
 	"github.com/kvtools/redis"
 	"github.com/kvtools/valkeyrie"
@@ -23,24 +29,36 @@ import (
 // Redis test suites.
 type RedisSuite struct {
 	BaseSuite
-	kvClient  store.Store
-	redisAddr string
+	kvClient       store.Store
+	redisEndpoints []string
+}
+
+func (s *RedisSuite) TearDownSuite(c *check.C) {
+	s.composeDown(c)
+
+	for _, filename := range []string{"sentinel1.conf", "sentinel2.conf", "sentinel3.conf"} {
+		err := os.Remove(filepath.Join(".", "resources", "compose", "config", filename))
+		if err != nil && !errors.Is(err, fs.ErrNotExist) {
+			c.Fatal("unable to clean configuration file for sentinel: ", err)
+		}
+	}
 }
 
 func (s *RedisSuite) setupStore(c *check.C) {
 	s.createComposeProject(c, "redis")
 	s.composeUp(c)
 
-	s.redisAddr = net.JoinHostPort(s.getComposeServiceIP(c, "redis"), "6379")
+	s.redisEndpoints = []string{}
+	s.redisEndpoints = append(s.redisEndpoints, net.JoinHostPort(s.getComposeServiceIP(c, "redis"), "6379"))
 
 	kv, err := valkeyrie.NewStore(
 		context.Background(),
 		redis.StoreName,
-		[]string{s.redisAddr},
+		s.redisEndpoints,
 		&redis.Config{},
 	)
 	if err != nil {
-		c.Fatal("Cannot create store redis")
+		c.Fatal("Cannot create store redis: ", err)
 	}
 	s.kvClient = kv
 
@@ -52,7 +70,173 @@ func (s *RedisSuite) setupStore(c *check.C) {
 func (s *RedisSuite) TestSimpleConfiguration(c *check.C) {
 	s.setupStore(c)
 
-	file := s.adaptFile(c, "fixtures/redis/simple.toml", struct{ RedisAddress string }{s.redisAddr})
+	file := s.adaptFile(c, "fixtures/redis/simple.toml", struct{ RedisAddress string }{
+		RedisAddress: strings.Join(s.redisEndpoints, ","),
+	})
+	defer os.Remove(file)
+
+	data := map[string]string{
+		"traefik/http/routers/Router0/entryPoints/0": "web",
+		"traefik/http/routers/Router0/middlewares/0": "compressor",
+		"traefik/http/routers/Router0/middlewares/1": "striper",
+		"traefik/http/routers/Router0/service":       "simplesvc",
+		"traefik/http/routers/Router0/rule":          "Host(`kv1.localhost`)",
+		"traefik/http/routers/Router0/priority":      "42",
+		"traefik/http/routers/Router0/tls":           "true",
+
+		"traefik/http/routers/Router1/rule":                 "Host(`kv2.localhost`)",
+		"traefik/http/routers/Router1/priority":             "42",
+		"traefik/http/routers/Router1/tls/domains/0/main":   "aaa.localhost",
+		"traefik/http/routers/Router1/tls/domains/0/sans/0": "aaa.aaa.localhost",
+		"traefik/http/routers/Router1/tls/domains/0/sans/1": "bbb.aaa.localhost",
+		"traefik/http/routers/Router1/tls/domains/1/main":   "bbb.localhost",
+		"traefik/http/routers/Router1/tls/domains/1/sans/0": "aaa.bbb.localhost",
+		"traefik/http/routers/Router1/tls/domains/1/sans/1": "bbb.bbb.localhost",
+		"traefik/http/routers/Router1/entryPoints/0":        "web",
+		"traefik/http/routers/Router1/service":              "simplesvc",
+
+		"traefik/http/services/simplesvc/loadBalancer/servers/0/url": "http://10.0.1.1:8888",
+		"traefik/http/services/simplesvc/loadBalancer/servers/1/url": "http://10.0.1.1:8889",
+
+		"traefik/http/services/srvcA/loadBalancer/servers/0/url": "http://10.0.1.2:8888",
+		"traefik/http/services/srvcA/loadBalancer/servers/1/url": "http://10.0.1.2:8889",
+
+		"traefik/http/services/srvcB/loadBalancer/servers/0/url": "http://10.0.1.3:8888",
+		"traefik/http/services/srvcB/loadBalancer/servers/1/url": "http://10.0.1.3:8889",
+
+		"traefik/http/services/mirror/mirroring/service":           "simplesvc",
+		"traefik/http/services/mirror/mirroring/mirrors/0/name":    "srvcA",
+		"traefik/http/services/mirror/mirroring/mirrors/0/percent": "42",
+		"traefik/http/services/mirror/mirroring/mirrors/1/name":    "srvcB",
+		"traefik/http/services/mirror/mirroring/mirrors/1/percent": "42",
+
+		"traefik/http/services/Service03/weighted/services/0/name":   "srvcA",
+		"traefik/http/services/Service03/weighted/services/0/weight": "42",
+		"traefik/http/services/Service03/weighted/services/1/name":   "srvcB",
+		"traefik/http/services/Service03/weighted/services/1/weight": "42",
+
+		"traefik/http/middlewares/compressor/compress":            "true",
+		"traefik/http/middlewares/striper/stripPrefix/prefixes/0": "foo",
+		"traefik/http/middlewares/striper/stripPrefix/prefixes/1": "bar",
+		"traefik/http/middlewares/striper/stripPrefix/forceSlash": "true",
+	}
+
+	for k, v := range data {
+		err := s.kvClient.Put(context.Background(), k, []byte(v), nil)
+		c.Assert(err, checker.IsNil)
+	}
+
+	cmd, display := s.traefikCmd(withConfigFile(file))
+	defer display(c)
+	err := cmd.Start()
+	c.Assert(err, checker.IsNil)
+	defer s.killCmd(cmd)
+
+	// wait for traefik
+	err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 2*time.Second,
+		try.BodyContains(`"striper@redis":`, `"compressor@redis":`, `"srvcA@redis":`, `"srvcB@redis":`),
+	)
+	c.Assert(err, checker.IsNil)
+
+	resp, err := http.Get("http://127.0.0.1:8080/api/rawdata")
+	c.Assert(err, checker.IsNil)
+
+	var obtained api.RunTimeRepresentation
+	err = json.NewDecoder(resp.Body).Decode(&obtained)
+	c.Assert(err, checker.IsNil)
+	got, err := json.MarshalIndent(obtained, "", "  ")
+	c.Assert(err, checker.IsNil)
+
+	expectedJSON := filepath.FromSlash("testdata/rawdata-redis.json")
+
+	if *updateExpected {
+		err = os.WriteFile(expectedJSON, got, 0o666)
+		c.Assert(err, checker.IsNil)
+	}
+
+	expected, err := os.ReadFile(expectedJSON)
+	c.Assert(err, checker.IsNil)
+
+	if !bytes.Equal(expected, got) {
+		diff := difflib.UnifiedDiff{
+			FromFile: "Expected",
+			A:        difflib.SplitLines(string(expected)),
+			ToFile:   "Got",
+			B:        difflib.SplitLines(string(got)),
+			Context:  3,
+		}
+
+		text, err := difflib.GetUnifiedDiffString(diff)
+		c.Assert(err, checker.IsNil)
+		c.Error(text)
+	}
+}
+
+func (s *RedisSuite) setupSentinelStore(c *check.C) {
+	s.setupSentinelConfiguration(c, []string{"26379", "36379", "46379"})
+
+	s.createComposeProject(c, "redis_sentinel")
+	s.composeUp(c)
+
+	s.redisEndpoints = []string{
+		net.JoinHostPort(s.getComposeServiceIP(c, "sentinel1"), "26379"),
+		net.JoinHostPort(s.getComposeServiceIP(c, "sentinel2"), "36379"),
+		net.JoinHostPort(s.getComposeServiceIP(c, "sentinel3"), "46379"),
+	}
+
+	kv, err := valkeyrie.NewStore(
+		context.Background(),
+		redis.StoreName,
+		s.redisEndpoints,
+		&redis.Config{
+			Sentinel: &redis.Sentinel{
+				MasterName: "mymaster",
+			},
+		},
+	)
+	if err != nil {
+		c.Fatal("Cannot create store redis sentinel")
+	}
+	s.kvClient = kv
+
+	// wait for redis
+	err = try.Do(60*time.Second, try.KVExists(kv, "test"))
+	c.Assert(err, checker.IsNil)
+}
+
+func (s *RedisSuite) setupSentinelConfiguration(c *check.C, ports []string) {
+	for i, port := range ports {
+		templateValue := struct{ SentinelPort string }{SentinelPort: port}
+
+		// Load file
+		templateFile := "resources/compose/config/sentinel_template.conf"
+		tmpl, err := template.ParseFiles(templateFile)
+		c.Assert(err, checker.IsNil)
+
+		folder, prefix := filepath.Split(templateFile)
+
+		fileName := fmt.Sprintf("%s/sentinel%d.conf", folder, i+1)
+		tmpFile, err := os.Create(fileName)
+		c.Assert(err, checker.IsNil)
+		defer tmpFile.Close()
+
+		model := structs.Map(templateValue)
+		model["SelfFilename"] = tmpFile.Name()
+
+		err = tmpl.ExecuteTemplate(tmpFile, prefix, model)
+		c.Assert(err, checker.IsNil)
+
+		err = tmpFile.Sync()
+		c.Assert(err, checker.IsNil)
+	}
+}
+
+func (s *RedisSuite) TestSentinelConfiguration(c *check.C) {
+	s.setupSentinelStore(c)
+
+	file := s.adaptFile(c, "fixtures/redis/sentinel.toml", struct{ RedisAddress string }{
+		RedisAddress: strings.Join(s.redisEndpoints, `","`),
+	})
 	defer os.Remove(file)
 
 	data := map[string]string{