diff --git a/docs/content/providers/redis.md b/docs/content/providers/redis.md index bd61e103c..309531423 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 fbc3eb389..a0415e1b4 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 9f59741ce..a7b3ace47 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 c33fbf309..42dda1fbf 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 67cf11398..1497f57e5 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 22eb59b22..62ac8306b 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 b4a954782..ea218e429 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 000000000..3d5a59ec4 --- /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/redis_test.go b/integration/redis_test.go index b642691c4..6407a3a35 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{ diff --git a/integration/resources/compose/config/sentinel_template.conf b/integration/resources/compose/config/sentinel_template.conf new file mode 100644 index 000000000..c9f5acf6d --- /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 000000000..261e694e5 --- /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 23f432bb1..3005c8f2d 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) }