Redis Sentinel SingeFlight: support of Redis master node username and password (#2039)

* Add support for Redis Username and Password configuration

Introduced Redis master authentication parameters (username and password) to the Redis Sentinel setup. This enhances compatibility with Redis environments that require authentication for both sentinel and master nodes.

* Add support for protected Redis Sentinel configuration and related unit tests
This commit is contained in:
Alexandr Hacicheant
2025-04-01 08:34:13 +03:00
committed by GitHub
parent ebb5ac698b
commit ab1775afee
8 changed files with 142 additions and 6 deletions
+14
View File
@@ -34,6 +34,9 @@ jobs:
REDIS_SENTINEL_TEST_MASTER_NAME: redis-1
REDIS_SENTINEL_TEST_PASSWORD: sekret
PROTECTED_REDIS_TEST_ENDPOINT: localhost:6380
PROTECTED_REDIS_TEST_USERNAME: default
REDIS_SENTINEL_TEST_PROTECTED_ENDPOINT: localhost:26380
REDIS_SENTINEL_TEST_PROTECTED_MASTER_NAME: protectedredis-1
ATHENS_PROTECTED_REDIS_PASSWORD: AthensPass1
GA_PULL_REQUEST: ${{github.event.number}}
runs-on: ubuntu-latest
@@ -69,6 +72,17 @@ jobs:
env:
REDIS_PORT_NUMBER: 6380
REDIS_PASSWORD: AthensPass1
redis-sentinel-protected-redis:
image: bitnami/redis-sentinel
env:
REDIS_MASTER_HOST: protectedredis
REDIS_MASTER_PORT_NUMBER: 6380
REDIS_MASTER_SET: protectedredis-1
REDIS_SENTINEL_PASSWORD: sekret
REDIS_SENTINEL_QUORUM: "1"
REDIS_SENTINEL_PORT_NUMBER: 26380
ports:
- 26380:26380
steps:
- uses: actions/checkout@v4
- name: Set up Go
+2
View File
@@ -166,6 +166,8 @@ func getSingleFlight(l *log.Logger, c *config.Config, s storage.Backend, checker
c.SingleFlight.RedisSentinel.Endpoints,
c.SingleFlight.RedisSentinel.MasterName,
c.SingleFlight.RedisSentinel.SentinelPassword,
c.SingleFlight.RedisSentinel.RedisUsername,
c.SingleFlight.RedisSentinel.RedisPassword,
checker,
c.SingleFlight.RedisSentinel.LockConfig,
)
+6
View File
@@ -369,6 +369,12 @@ ShutdownTimeout = 60
# Env override: ATHENS_REDIS_SENTINEL_PASSWORD
SentinelPassword = "sekret"
# The Redis master authorization parameters
# Env override: ATHENS_REDIS_USERNAME
RedisUsername = ""
# Env override: ATHENS_REDIS_PASSWORD
RedisPassword = ""
[SingleFlight.RedisSentinel.LockConfig]
# TTL for the lock in seconds. Defaults to 900 seconds (15 minutes).
# Env override: ATHENS_REDIS_LOCK_TTL
+2
View File
@@ -179,6 +179,8 @@ func defaultConfig() *Config {
Endpoints: []string{"127.0.0.1:26379"},
MasterName: "redis-1",
SentinelPassword: "sekret",
RedisUsername: "",
RedisPassword: "",
LockConfig: DefaultRedisLockConfig(),
},
GCP: DefaultGCPConfig(),
+3 -1
View File
@@ -375,7 +375,7 @@ func getEnvMap(config *Config) map[string]string {
if singleFlight.Redis != nil {
envVars["ATHENS_SINGLE_FLIGHT_TYPE"] = "redis"
envVars["ATHENS_REDIS_ENDPOINT"] = singleFlight.Redis.Endpoint
envVars["ATHENS_REDIS_PASSWORD"] = singleFlight.Redis.Endpoint
envVars["ATHENS_REDIS_PASSWORD"] = singleFlight.Redis.Password
if singleFlight.Redis.LockConfig != nil {
envVars["ATHENS_REDIS_LOCK_TTL"] = strconv.Itoa(singleFlight.Redis.LockConfig.TTL)
envVars["ATHENS_REDIS_LOCK_TIMEOUT"] = strconv.Itoa(singleFlight.Redis.LockConfig.Timeout)
@@ -386,6 +386,8 @@ func getEnvMap(config *Config) map[string]string {
envVars["ATHENS_REDIS_SENTINEL_ENDPOINTS"] = strings.Join(singleFlight.RedisSentinel.Endpoints, ",")
envVars["ATHENS_REDIS_SENTINEL_MASTER_NAME"] = singleFlight.RedisSentinel.MasterName
envVars["ATHENS_REDIS_SENTINEL_PASSWORD"] = singleFlight.RedisSentinel.SentinelPassword
envVars["ATHENS_REDIS_USERNAME"] = singleFlight.RedisSentinel.RedisUsername
envVars["ATHENS_REDIS_PASSWORD"] = singleFlight.RedisSentinel.RedisPassword
if singleFlight.RedisSentinel.LockConfig != nil {
envVars["ATHENS_REDIS_LOCK_TTL"] = strconv.Itoa(singleFlight.RedisSentinel.LockConfig.TTL)
envVars["ATHENS_REDIS_LOCK_TIMEOUT"] = strconv.Itoa(singleFlight.RedisSentinel.LockConfig.Timeout)
+2
View File
@@ -31,6 +31,8 @@ type RedisSentinel struct {
Endpoints []string `envconfig:"ATHENS_REDIS_SENTINEL_ENDPOINTS"`
MasterName string `envconfig:"ATHENS_REDIS_SENTINEL_MASTER_NAME"`
SentinelPassword string `envconfig:"ATHENS_REDIS_SENTINEL_PASSWORD"`
RedisUsername string `envconfig:"ATHENS_REDIS_USERNAME"`
RedisPassword string `envconfig:"ATHENS_REDIS_PASSWORD"`
LockConfig *RedisLockConfig
}
+4 -2
View File
@@ -11,7 +11,7 @@ import (
// WithRedisSentinelLock returns a distributed singleflight
// with a redis cluster that utilizes sentinel for quorum and failover.
func WithRedisSentinelLock(l RedisLogger, endpoints []string, master, password string, checker storage.Checker, lockConfig *config.RedisLockConfig) (Wrapper, error) {
func WithRedisSentinelLock(l RedisLogger, endpoints []string, master, sentinelPassword, redisUsername, redisPassword string, checker storage.Checker, lockConfig *config.RedisLockConfig) (Wrapper, error) {
redis.SetLogger(l)
const op errors.Op = "stash.WithRedisSentinelLock"
@@ -23,7 +23,9 @@ func WithRedisSentinelLock(l RedisLogger, endpoints []string, master, password s
client := redis.NewFailoverClient(&redis.FailoverOptions{
MasterName: master,
SentinelAddrs: endpoints,
SentinelPassword: password,
SentinelPassword: sentinelPassword,
Username: redisUsername,
Password: redisPassword,
})
_, err := client.Ping(context.Background()).Result()
if err != nil {
+109 -3
View File
@@ -18,8 +18,8 @@ import (
func TestWithRedisSentinelLock(t *testing.T) {
endpoint := os.Getenv("REDIS_SENTINEL_TEST_ENDPOINT")
masterName := os.Getenv("REDIS_SENTINEL_TEST_MASTER_NAME")
password := os.Getenv("REDIS_SENTINEL_TEST_PASSWORD")
if len(endpoint) == 0 || len(masterName) == 0 || len(password) == 0 {
sentinelPassword := os.Getenv("REDIS_SENTINEL_TEST_PASSWORD")
if len(endpoint) == 0 || len(masterName) == 0 || len(sentinelPassword) == 0 {
t.SkipNow()
}
strg, err := mem.NewStorage()
@@ -29,7 +29,7 @@ func TestWithRedisSentinelLock(t *testing.T) {
ms := &mockRedisStasher{strg: strg}
l := &testingRedisLogger{t: t}
wrapper, err := WithRedisSentinelLock(l, []string{endpoint}, masterName, password, storage.WithChecker(strg), config.DefaultRedisLockConfig())
wrapper, err := WithRedisSentinelLock(l, []string{endpoint}, masterName, sentinelPassword, "", "", storage.WithChecker(strg), config.DefaultRedisLockConfig())
if err != nil {
t.Fatal(err)
}
@@ -50,3 +50,109 @@ func TestWithRedisSentinelLock(t *testing.T) {
t.Fatal(err)
}
}
// TestWithRedisSentinelLockWithRedisPassword verifies WithRedisSentinelLock working with
// password protected redis sentinel and redis master nodes
func TestWithRedisSentinelLockWithRedisPassword(t *testing.T) {
endpoint := os.Getenv("REDIS_SENTINEL_TEST_PROTECTED_ENDPOINT")
masterName := os.Getenv("REDIS_SENTINEL_TEST_PROTECTED_MASTER_NAME")
sentinelPassword := os.Getenv("REDIS_SENTINEL_TEST_PASSWORD")
redisPassword := os.Getenv("ATHENS_PROTECTED_REDIS_PASSWORD")
if len(endpoint) == 0 || len(masterName) == 0 {
t.SkipNow()
}
strg, err := mem.NewStorage()
if err != nil {
t.Fatal(err)
}
ms := &mockRedisStasher{strg: strg}
l := &testingRedisLogger{t: t}
wrapper, err := WithRedisSentinelLock(l, []string{endpoint}, masterName, sentinelPassword, "", redisPassword, storage.WithChecker(strg), config.DefaultRedisLockConfig())
if err != nil {
t.Fatal(err)
}
s := wrapper(ms)
var eg errgroup.Group
for i := 0; i < 5; i++ {
eg.Go(func() error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
_, err := s.Stash(ctx, "mod", "ver")
return err
})
}
err = eg.Wait()
if err != nil {
t.Fatal(err)
}
}
// TestWithRedisSentinelLockWithPassword verifies WithRedisSentinelLock working with
// username & password protected master node under the redis sentinel
func TestWithRedisSentinelLockWithUsernameAndPassword(t *testing.T) {
endpoint := os.Getenv("REDIS_SENTINEL_TEST_PROTECTED_ENDPOINT")
masterName := os.Getenv("REDIS_SENTINEL_TEST_PROTECTED_MASTER_NAME")
sentinelPassword := os.Getenv("REDIS_SENTINEL_TEST_PASSWORD")
redisPassword := os.Getenv("ATHENS_PROTECTED_REDIS_PASSWORD")
redisUsername := os.Getenv("PROTECTED_REDIS_TEST_USERNAME")
if len(endpoint) == 0 || len(masterName) == 0 {
t.SkipNow()
}
strg, err := mem.NewStorage()
if err != nil {
t.Fatal(err)
}
ms := &mockRedisStasher{strg: strg}
l := &testingRedisLogger{t: t}
wrapper, err := WithRedisSentinelLock(l, []string{endpoint}, masterName, sentinelPassword, redisUsername, redisPassword, storage.WithChecker(strg), config.DefaultRedisLockConfig())
if err != nil {
t.Fatal(err)
}
s := wrapper(ms)
var eg errgroup.Group
for i := 0; i < 5; i++ {
eg.Go(func() error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
_, err := s.Stash(ctx, "mod", "ver")
return err
})
}
err = eg.Wait()
if err != nil {
t.Fatal(err)
}
}
// TestWithRedisSentinelLockWithWrongPassword verifies the WithRedisSentinelLock fails
// with the correct error when trying to connect with wrong passwords
func TestWithRedisSentinelLockWithWrongRedisPassword(t *testing.T) {
endpoint := os.Getenv("REDIS_SENTINEL_TEST_PROTECTED_ENDPOINT")
masterName := os.Getenv("REDIS_SENTINEL_TEST_PROTECTED_MASTER_NAME")
sentinelPassword := os.Getenv("REDIS_SENTINEL_TEST_PASSWORD")
redisPassword := os.Getenv("ATHENS_PROTECTED_REDIS_PASSWORD")
if len(endpoint) == 0 || len(masterName) == 0 {
t.SkipNow()
}
strg, err := mem.NewStorage()
if err != nil {
t.Fatal(err)
}
l := &testingRedisLogger{t: t}
// Test with wrong sentinel password
_, err = WithRedisSentinelLock(l, []string{endpoint}, masterName, "wrong-sentinel-password", "", redisPassword, storage.WithChecker(strg), config.DefaultRedisLockConfig())
if err == nil {
t.Fatal("Expected Connection Error for wrong sentinel password")
}
// Test with wrong redis password
_, err = WithRedisSentinelLock(l, []string{endpoint}, masterName, sentinelPassword, "", "wrong-redis-password", storage.WithChecker(strg), config.DefaultRedisLockConfig())
if err == nil {
t.Fatal("Expected Connection Error for wrong redis password")
}
}