diff --git a/configs/mcp-gateway.yaml b/configs/mcp-gateway.yaml index eb6db847..c107a9b8 100644 --- a/configs/mcp-gateway.yaml +++ b/configs/mcp-gateway.yaml @@ -90,6 +90,8 @@ notifier: master_name: "${NOTIFIER_REDIS_MASTER_NAME:}" # MasterName is the sentinel master name. username: "${NOTIFIER_REDIS_USERNAME:default}" password: "${NOTIFIER_REDIS_PASSWORD:}" + sentinel_username: "${NOTIFIER_REDIS_SENTINEL_USERNAME:}" + sentinel_password: "${NOTIFIER_REDIS_SENTINEL_PASSWORD:}" db: ${NOTIFIER_REDIS_DB:0} topic: "${NOTIFIER_REDIS_TOPIC:mcp-gateway:reload}" @@ -102,6 +104,8 @@ session: master_name: "${SESSION_REDIS_MASTER_NAME:}" # MasterName is the sentinel master name. username: "${SESSION_REDIS_USERNAME:default}" password: "${SESSION_REDIS_PASSWORD:}" + sentinel_username: "${SESSION_REDIS_SENTINEL_USERNAME:}" + sentinel_password: "${SESSION_REDIS_SENTINEL_PASSWORD:}" db: ${SESSION_REDIS_DB:0} topic: "${SESSION_REDIS_TOPIC:mcp-gateway:session}" prefix: "${SESSION_REDIS_PREFIX:session}" @@ -119,6 +123,8 @@ auth: master_name: "${OAUTH2_REDIS_MASTER_NAME:}" # MasterName is the sentinel master name. username: "${OAUTH2_REDIS_USERNAME:default}" password: "${OAUTH2_REDIS_PASSWORD:}" + sentinel_username: "${OAUTH2_REDIS_SENTINEL_USERNAME:}" + sentinel_password: "${OAUTH2_REDIS_SENTINEL_PASSWORD:}" db: ${OAUTH2_REDIS_DB:0} cors: allowOrigins: diff --git a/internal/auth/storage/factory.go b/internal/auth/storage/factory.go index ff70ca03..64ffa4cb 100644 --- a/internal/auth/storage/factory.go +++ b/internal/auth/storage/factory.go @@ -15,7 +15,7 @@ func NewStore(logger *zap.Logger, cfg *config.OAuth2StorageConfig) (Store, error case "memory": return NewMemoryStorage(), nil case "redis": - return NewRedisStorage(cfg.Redis.ClusterType, cfg.Redis.Addr, cfg.Redis.MasterName, cfg.Redis.Username, cfg.Redis.Password, cfg.Redis.DB) + return NewRedisStorage(cfg.Redis.ClusterType, cfg.Redis.Addr, cfg.Redis.MasterName, cfg.Redis.Username, cfg.Redis.Password, cfg.Redis.SentinelUsername, cfg.Redis.SentinelPassword, cfg.Redis.DB) default: return nil, fmt.Errorf("unsupported auth storage type: %s", cfg.Type) } diff --git a/internal/auth/storage/redis.go b/internal/auth/storage/redis.go index 4399d69f..acc4e80a 100644 --- a/internal/auth/storage/redis.go +++ b/internal/auth/storage/redis.go @@ -19,7 +19,7 @@ type RedisStorage struct { } // NewRedisStorage creates a new Redis storage instance -func NewRedisStorage(clusterType, addr, masterName string, username, password string, db int) (*RedisStorage, error) { +func NewRedisStorage(clusterType, addr, masterName string, username, password, sentinelUsername, sentinelPassword string, db int) (*RedisStorage, error) { addrs := utils.SplitByMultipleDelimiters(addr, ";", ",") redisOptions := &redis.UniversalOptions{ Addrs: addrs, @@ -28,6 +28,8 @@ func NewRedisStorage(clusterType, addr, masterName string, username, password st } if clusterType == cnst.RedisClusterTypeSentinel { redisOptions.MasterName = masterName + redisOptions.SentinelUsername = sentinelUsername + redisOptions.SentinelPassword = sentinelPassword } if clusterType != cnst.RedisClusterTypeCluster { // can not set db in cluster mode diff --git a/internal/auth/storage/redis_test.go b/internal/auth/storage/redis_test.go index b7120f3c..865fdf75 100644 --- a/internal/auth/storage/redis_test.go +++ b/internal/auth/storage/redis_test.go @@ -17,7 +17,7 @@ func newTestRedisStorage(t *testing.T) (*RedisStorage, *miniredis.Miniredis) { if err != nil { t.Fatalf("failed to start miniredis: %v", err) } - s, err := NewRedisStorage(cnst.RedisClusterTypeSingle, mr.Addr(), "", "", "", 0) + s, err := NewRedisStorage(cnst.RedisClusterTypeSingle, mr.Addr(), "", "", "", "", "", 0) if err != nil { mr.Close() t.Fatalf("failed to create RedisStorage: %v", err) @@ -101,7 +101,7 @@ func TestRedisStorage_Token_Flow(t *testing.T) { func TestNewRedisStorage_ConnectionError(t *testing.T) { // invalid address should fail to ping - s, err := NewRedisStorage(cnst.RedisClusterTypeSingle, "127.0.0.1:0", "", "", "", 0) + s, err := NewRedisStorage(cnst.RedisClusterTypeSingle, "127.0.0.1:0", "", "", "", "", "", 0) assert.Nil(t, s) assert.Error(t, err) } diff --git a/internal/common/config/config.go b/internal/common/config/config.go index c7cefdc7..7c2e4a17 100644 --- a/internal/common/config/config.go +++ b/internal/common/config/config.go @@ -80,15 +80,17 @@ type ( // SessionRedisConfig represents the Redis configuration for session storage SessionRedisConfig struct { - ClusterType string `yaml:"cluster_type"` // "single", "cluster" or "sentinel" - Addr string `yaml:"addr"` // multiple addresses separated by ;. - MasterName string `yaml:"master_name"` // MasterName is the sentinel master name. - Username string `yaml:"username"` - Password string `yaml:"password"` - DB int `yaml:"db"` - Topic string `yaml:"topic"` - Prefix string `yaml:"prefix"` - TTL time.Duration `yaml:"ttl"` // TTL for session data in Redis + ClusterType string `yaml:"cluster_type"` // "single", "cluster" or "sentinel" + Addr string `yaml:"addr"` // multiple addresses separated by ;. + MasterName string `yaml:"master_name"` // MasterName is the sentinel master name. + Username string `yaml:"username"` + Password string `yaml:"password"` + SentinelUsername string `yaml:"sentinel_username"` + SentinelPassword string `yaml:"sentinel_password"` + DB int `yaml:"db"` + Topic string `yaml:"topic"` + Prefix string `yaml:"prefix"` + TTL time.Duration `yaml:"ttl"` // TTL for session data in Redis } // LoggerConfig represents the logger configuration @@ -123,12 +125,14 @@ type ( Redis OAuth2RedisConfig `yaml:"redis"` } OAuth2RedisConfig struct { - ClusterType string `yaml:"cluster_type"` // "single", "cluster" or "sentinel" - Addr string `yaml:"addr"` - MasterName string `yaml:"master_name"` // MasterName is the sentinel master name. - Username string `yaml:"username"` - Password string `yaml:"password"` - DB int `yaml:"db"` + ClusterType string `yaml:"cluster_type"` // "single", "cluster" or "sentinel" + Addr string `yaml:"addr"` + MasterName string `yaml:"master_name"` // MasterName is the sentinel master name. + Username string `yaml:"username"` + Password string `yaml:"password"` + SentinelUsername string `yaml:"sentinel_username"` + SentinelPassword string `yaml:"sentinel_password"` + DB int `yaml:"db"` } // GoogleOAuthConfig defines Google OAuth configuration diff --git a/internal/common/config/config_test.go b/internal/common/config/config_test.go index 321db14c..c3393770 100644 --- a/internal/common/config/config_test.go +++ b/internal/common/config/config_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestResolveEnv(t *testing.T) { @@ -76,3 +77,45 @@ tool_access: assert.NoError(t, err) assert.Equal(t, []string{"127.0.0.1/32", "localhost", "::1/128"}, []string(cfg.ToolAccess.InternalNetwork.Allowlist)) } + +func TestLoadConfig_MCPGateway_RedisSentinelCredentials(t *testing.T) { + tmp := t.TempDir() + old, _ := os.Getwd() + t.Cleanup(func() { _ = os.Chdir(old) }) + _ = os.Chdir(tmp) + + t.Setenv("NOTIFIER_REDIS_SENTINEL_USERNAME", "sentinel-notifier") + t.Setenv("NOTIFIER_REDIS_SENTINEL_PASSWORD", "notifier-pass") + t.Setenv("SESSION_REDIS_SENTINEL_USERNAME", "sentinel-session") + t.Setenv("SESSION_REDIS_SENTINEL_PASSWORD", "session-pass") + t.Setenv("OAUTH2_REDIS_SENTINEL_USERNAME", "sentinel-oauth") + t.Setenv("OAUTH2_REDIS_SENTINEL_PASSWORD", "oauth-pass") + + yaml := ` +notifier: + redis: + sentinel_username: "${NOTIFIER_REDIS_SENTINEL_USERNAME:}" + sentinel_password: "${NOTIFIER_REDIS_SENTINEL_PASSWORD:}" +session: + redis: + sentinel_username: "${SESSION_REDIS_SENTINEL_USERNAME:}" + sentinel_password: "${SESSION_REDIS_SENTINEL_PASSWORD:}" +auth: + oauth2: + storage: + redis: + sentinel_username: "${OAUTH2_REDIS_SENTINEL_USERNAME:}" + sentinel_password: "${OAUTH2_REDIS_SENTINEL_PASSWORD:}" +` + file := filepath.Join(tmp, "mcp-gateway.yaml") + require.NoError(t, os.WriteFile(file, []byte(yaml), 0o644)) + + cfg, _, err := LoadConfig[MCPGatewayConfig]("mcp-gateway.yaml") + require.NoError(t, err) + assert.Equal(t, "sentinel-notifier", cfg.Notifier.Redis.SentinelUsername) + assert.Equal(t, "notifier-pass", cfg.Notifier.Redis.SentinelPassword) + assert.Equal(t, "sentinel-session", cfg.Session.Redis.SentinelUsername) + assert.Equal(t, "session-pass", cfg.Session.Redis.SentinelPassword) + assert.Equal(t, "sentinel-oauth", cfg.Auth.OAuth2.Storage.Redis.SentinelUsername) + assert.Equal(t, "oauth-pass", cfg.Auth.OAuth2.Storage.Redis.SentinelPassword) +} diff --git a/internal/common/config/notifier.go b/internal/common/config/notifier.go index beee9a64..866809b0 100644 --- a/internal/common/config/notifier.go +++ b/internal/common/config/notifier.go @@ -24,13 +24,15 @@ type ( // RedisConfig represents the configuration for Redis-based notifier RedisConfig struct { - ClusterType string `yaml:"cluster_type"` // "single", "cluster" or "sentinel" - Addr string `yaml:"addr"` - MasterName string `yaml:"master_name"` // MasterName is the sentinel master name. - Username string `yaml:"username"` - Password string `yaml:"password"` - DB int `yaml:"db"` - Topic string `yaml:"topic"` + ClusterType string `yaml:"cluster_type"` // "single", "cluster" or "sentinel" + Addr string `yaml:"addr"` + MasterName string `yaml:"master_name"` // MasterName is the sentinel master name. + Username string `yaml:"username"` + Password string `yaml:"password"` + SentinelUsername string `yaml:"sentinel_username"` + SentinelPassword string `yaml:"sentinel_password"` + DB int `yaml:"db"` + Topic string `yaml:"topic"` } ) diff --git a/internal/mcp/session/redis.go b/internal/mcp/session/redis.go index 74aa5132..44544154 100644 --- a/internal/mcp/session/redis.go +++ b/internal/mcp/session/redis.go @@ -41,6 +41,8 @@ func NewRedisStore(ctx context.Context, logger *zap.Logger, cfg config.SessionRe } if cfg.ClusterType == cnst.RedisClusterTypeSentinel { redisOptions.MasterName = cfg.MasterName + redisOptions.SentinelUsername = cfg.SentinelUsername + redisOptions.SentinelPassword = cfg.SentinelPassword } if cfg.ClusterType != cnst.RedisClusterTypeCluster { // can not set db in cluster mode diff --git a/internal/mcp/storage/notifier/factory.go b/internal/mcp/storage/notifier/factory.go index 0742da8e..c84c31e2 100644 --- a/internal/mcp/storage/notifier/factory.go +++ b/internal/mcp/storage/notifier/factory.go @@ -36,7 +36,7 @@ func NewNotifier(ctx context.Context, logger *zap.Logger, cfg *config.NotifierCo case TypeAPI: return NewAPINotifier(logger, cfg.API.Port, role, cfg.API.TargetURL), nil case TypeRedis: - return NewRedisNotifier(logger, cfg.Redis.ClusterType, cfg.Redis.Addr, cfg.Redis.MasterName, cfg.Redis.Username, cfg.Redis.Password, cfg.Redis.DB, cfg.Redis.Topic, role) + return NewRedisNotifier(logger, cfg.Redis.ClusterType, cfg.Redis.Addr, cfg.Redis.MasterName, cfg.Redis.Username, cfg.Redis.Password, cfg.Redis.SentinelUsername, cfg.Redis.SentinelPassword, cfg.Redis.DB, cfg.Redis.Topic, role) case TypeComposite: notifiers := make([]Notifier, 0) // Add signal notifier @@ -47,7 +47,7 @@ func NewNotifier(ctx context.Context, logger *zap.Logger, cfg *config.NotifierCo notifiers = append(notifiers, apiNotifier) // Add Redis notifier if configured if cfg.Redis.Addr != "" { - redisNotifier, err := NewRedisNotifier(logger, cfg.Redis.ClusterType, cfg.Redis.Addr, cfg.Redis.MasterName, cfg.Redis.Username, cfg.Redis.Password, cfg.Redis.DB, cfg.Redis.Topic, role) + redisNotifier, err := NewRedisNotifier(logger, cfg.Redis.ClusterType, cfg.Redis.Addr, cfg.Redis.MasterName, cfg.Redis.Username, cfg.Redis.Password, cfg.Redis.SentinelUsername, cfg.Redis.SentinelPassword, cfg.Redis.DB, cfg.Redis.Topic, role) if err != nil { return nil, err } diff --git a/internal/mcp/storage/notifier/redis.go b/internal/mcp/storage/notifier/redis.go index 32ebdd97..e228f525 100644 --- a/internal/mcp/storage/notifier/redis.go +++ b/internal/mcp/storage/notifier/redis.go @@ -25,7 +25,7 @@ type RedisNotifier struct { } // NewRedisNotifier creates a new Redis-based notifier -func NewRedisNotifier(logger *zap.Logger, clusterType, addr, masterName, username, password string, db int, streamName string, role config.NotifierRole) (*RedisNotifier, error) { +func NewRedisNotifier(logger *zap.Logger, clusterType, addr, masterName, username, password, sentinelUsername, sentinelPassword string, db int, streamName string, role config.NotifierRole) (*RedisNotifier, error) { addrs := utils.SplitByMultipleDelimiters(addr, ";", ",") redisOptions := &redis.UniversalOptions{ Addrs: addrs, @@ -34,6 +34,8 @@ func NewRedisNotifier(logger *zap.Logger, clusterType, addr, masterName, usernam } if clusterType == cnst.RedisClusterTypeSentinel { redisOptions.MasterName = masterName + redisOptions.SentinelUsername = sentinelUsername + redisOptions.SentinelPassword = sentinelPassword } if clusterType != cnst.RedisClusterTypeCluster { // can not set db in cluster mode diff --git a/internal/mcp/storage/notifier/redis_test.go b/internal/mcp/storage/notifier/redis_test.go index 0e5a6302..154f4112 100644 --- a/internal/mcp/storage/notifier/redis_test.go +++ b/internal/mcp/storage/notifier/redis_test.go @@ -36,7 +36,7 @@ func TestRedisNotifier_WatchAndNotify(t *testing.T) { logger := zap.NewNop() stream := "unla:mcp:updates" - recv, err := NewRedisNotifier(logger, cnst.RedisClusterTypeSingle, mr.Addr(), "", "", "", 0, stream, config.RoleReceiver) + recv, err := NewRedisNotifier(logger, cnst.RedisClusterTypeSingle, mr.Addr(), "", "", "", "", "", 0, stream, config.RoleReceiver) assert.NoError(t, err) assert.NotNil(t, recv) @@ -48,7 +48,7 @@ func TestRedisNotifier_WatchAndNotify(t *testing.T) { assert.NotNil(t, ch) // Create a sender and push an update - send, err := NewRedisNotifier(logger, cnst.RedisClusterTypeSingle, mr.Addr(), "", "", "", 0, stream, config.RoleSender) + send, err := NewRedisNotifier(logger, cnst.RedisClusterTypeSingle, mr.Addr(), "", "", "", "", "", 0, stream, config.RoleSender) assert.NoError(t, err) cfg := &config.MCPConfig{Name: "cfg-1"} assert.NoError(t, send.NotifyUpdate(context.Background(), cfg)) @@ -80,7 +80,7 @@ func TestRedisNotifier_Watch_NotReceiver(t *testing.T) { } defer mr.Close() - n, err := NewRedisNotifier(zap.NewNop(), cnst.RedisClusterTypeSingle, mr.Addr(), "", "", "", 0, "stream", config.RoleSender) + n, err := NewRedisNotifier(zap.NewNop(), cnst.RedisClusterTypeSingle, mr.Addr(), "", "", "", "", "", 0, "stream", config.RoleSender) assert.NoError(t, err) ch, werr := n.Watch(context.Background()) assert.Nil(t, ch) @@ -94,7 +94,7 @@ func TestRedisNotifier_NotifyUpdate_NotSender(t *testing.T) { } defer mr.Close() - n, err := NewRedisNotifier(zap.NewNop(), cnst.RedisClusterTypeSingle, mr.Addr(), "", "", "", 0, "stream", config.RoleReceiver) + n, err := NewRedisNotifier(zap.NewNop(), cnst.RedisClusterTypeSingle, mr.Addr(), "", "", "", "", "", 0, "stream", config.RoleReceiver) assert.NoError(t, err) err = n.NotifyUpdate(context.Background(), &config.MCPConfig{Name: "x"}) assert.ErrorIs(t, err, cnst.ErrNotSender) @@ -102,7 +102,7 @@ func TestRedisNotifier_NotifyUpdate_NotSender(t *testing.T) { func TestNewRedisNotifier_ConnectionError(t *testing.T) { // invalid address should cause ping failure - n, err := NewRedisNotifier(zap.NewNop(), cnst.RedisClusterTypeSingle, "127.0.0.1:0", "", "", "", 0, "stream", config.RoleBoth) + n, err := NewRedisNotifier(zap.NewNop(), cnst.RedisClusterTypeSingle, "127.0.0.1:0", "", "", "", "", "", 0, "stream", config.RoleBoth) assert.Nil(t, n) assert.Error(t, err) }