Skip to content

Commit 6b74c73

Browse files
feat: Add serializable Redis configuration layer (#4300)
* feat: Add serializable Redis configuration layer for RetrieverConf This commit resolves issue #4023 by introducing a JSON/YAML serializable configuration option for Redis retrievers while maintaining full backward compatibility with existing code. Changes: - Added SerializableRedisOptions struct with JSON-serializable fields - All timeout values stored as milliseconds (int64) for serialization - Supports common Redis options: addr, password, username, db, timeouts, pool settings, TLS, and more - Added ToRedisOptions() method to convert to redis.Options - Updated RetrieverConf to support both old and new configurations: * RedisOptions (deprecated but still functional) * Redis (new serializable option) * New option takes priority when both are provided - Updated validation logic to accept either configuration - Updated retriever initialization to prioritize new configuration - Added comprehensive test coverage: * 13 new test cases for serialization, conversion, and validation * Tests for backward compatibility scenarios * All tests pass including race detector Benefits: - Enables Redis configuration via JSON/YAML files - Facilitates command-line configuration in cmd/cli package - Maintains 100% backward compatibility - No breaking changes to existing API * refactor: Rename SerializableRedisOptions to Options Simplifies the type name for better usability while keeping it in the retrieverconf package to avoid naming conflicts with redis.Options. * Revert "refactor: Rename SerializableRedisOptions to Options" This reverts commit 14f7e32. * refactor: Rename Redis field to RedisOptions in RetrieverConf Renamed the Redis field to RedisOptions to better reflect its purpose and maintain consistency. Updated mapstructure and koanf tags from "redis" to "redisOptions". Changes: - Renamed Redis field to RedisOptions in RetrieverConf struct - Updated tags: mapstructure:"redisOptions" and koanf:"redisOptions" - Removed deprecated RedisOptions *redis.Options field - Simplified validation logic to only check RedisOptions - Updated retriever initialization to use RedisOptions - Updated all test cases to use the renamed field - Removed unused redis imports * refactor: Update RedisOptions field tags to redisOptions Updated the mapstructure and koanf tags from "redis" to "redisOptions" to match the field name. * Remove unused Redis import from test file Removed unused import for 'github.com/redis/go-redis/v9'. * fix linting issue * format file for gci
1 parent 0564b06 commit 6b74c73

File tree

6 files changed

+597
-11
lines changed

6 files changed

+597
-11
lines changed

cmdhelpers/retrieverconf/init/retriever_init.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"time"
77

88
awsConf "github.com/aws/aws-sdk-go-v2/config"
9+
"github.com/redis/go-redis/v9"
910
"github.com/thomaspoignant/go-feature-flag/cmdhelpers/retrieverconf"
1011
"github.com/thomaspoignant/go-feature-flag/retriever"
1112
azblobretriever "github.com/thomaspoignant/go-feature-flag/retriever/azblobstorageretriever"
@@ -156,7 +157,11 @@ func createMongoDBRetriever(c *retrieverconf.RetrieverConf, _ time.Duration) (re
156157
}
157158

158159
func createRedisRetriever(c *retrieverconf.RetrieverConf, _ time.Duration) (retriever.Retriever, error) {
159-
return &redisretriever.Retriever{Options: c.RedisOptions, Prefix: c.RedisPrefix}, nil
160+
var options *redis.Options
161+
if c.RedisOptions != nil {
162+
options = c.RedisOptions.ToRedisOptions()
163+
}
164+
return &redisretriever.Retriever{Options: options, Prefix: c.RedisPrefix}, nil
160165
}
161166

162167
func createAzBlobStorageRetriever(

cmdhelpers/retrieverconf/init/retriever_init_test.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"time"
77

88
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
910
"github.com/thomaspoignant/go-feature-flag/cmdhelpers/retrieverconf"
1011
"github.com/thomaspoignant/go-feature-flag/retriever"
1112
"github.com/thomaspoignant/go-feature-flag/retriever/bitbucketretriever"
@@ -15,6 +16,7 @@ import (
1516
"github.com/thomaspoignant/go-feature-flag/retriever/gitlabretriever"
1617
"github.com/thomaspoignant/go-feature-flag/retriever/httpretriever"
1718
"github.com/thomaspoignant/go-feature-flag/retriever/postgresqlretriever"
19+
"github.com/thomaspoignant/go-feature-flag/retriever/redisretriever"
1820
"github.com/thomaspoignant/go-feature-flag/retriever/s3retrieverv2"
1921
)
2022

@@ -237,3 +239,125 @@ func Test_InitRetriever(t *testing.T) {
237239
})
238240
}
239241
}
242+
243+
func Test_InitRetriever_Redis(t *testing.T) {
244+
tests := []struct {
245+
name string
246+
conf *retrieverconf.RetrieverConf
247+
wantErr bool
248+
validateRetriever func(t *testing.T, r retriever.Retriever)
249+
}{
250+
{
251+
name: "Redis with RedisOptions",
252+
conf: &retrieverconf.RetrieverConf{
253+
Kind: retrieverconf.RedisRetriever,
254+
RedisOptions: &retrieverconf.SerializableRedisOptions{
255+
Addr: "localhost:6379",
256+
Password: "secret",
257+
DB: 1,
258+
},
259+
RedisPrefix: "test:",
260+
},
261+
wantErr: false,
262+
validateRetriever: func(t *testing.T, r retriever.Retriever) {
263+
redisRet, ok := r.(*redisretriever.Retriever)
264+
require.True(t, ok, "expected *redisretriever.Retriever")
265+
assert.Equal(t, "localhost:6379", redisRet.Options.Addr)
266+
assert.Equal(t, "secret", redisRet.Options.Password)
267+
assert.Equal(t, 1, redisRet.Options.DB)
268+
assert.Equal(t, "test:", redisRet.Prefix)
269+
},
270+
},
271+
{
272+
name: "Redis with SerializableRedisOptions with username",
273+
conf: &retrieverconf.RetrieverConf{
274+
Kind: retrieverconf.RedisRetriever,
275+
RedisOptions: &retrieverconf.SerializableRedisOptions{
276+
Addr: "redis.example.com:6380",
277+
Password: "newsecret",
278+
DB: 2,
279+
Username: "admin",
280+
},
281+
RedisPrefix: "flags:",
282+
},
283+
wantErr: false,
284+
validateRetriever: func(t *testing.T, r retriever.Retriever) {
285+
redisRet, ok := r.(*redisretriever.Retriever)
286+
require.True(t, ok, "expected *redisretriever.Retriever")
287+
assert.Equal(t, "redis.example.com:6380", redisRet.Options.Addr)
288+
assert.Equal(t, "newsecret", redisRet.Options.Password)
289+
assert.Equal(t, 2, redisRet.Options.DB)
290+
assert.Equal(t, "admin", redisRet.Options.Username)
291+
assert.Equal(t, "flags:", redisRet.Prefix)
292+
},
293+
},
294+
{
295+
name: "Redis with RedisOptions password",
296+
conf: &retrieverconf.RetrieverConf{
297+
Kind: retrieverconf.RedisRetriever,
298+
RedisOptions: &retrieverconf.SerializableRedisOptions{
299+
Addr: "new:6380",
300+
Password: "new-secret",
301+
},
302+
RedisPrefix: "test:",
303+
},
304+
wantErr: false,
305+
validateRetriever: func(t *testing.T, r retriever.Retriever) {
306+
redisRet, ok := r.(*redisretriever.Retriever)
307+
require.True(t, ok, "expected *redisretriever.Retriever")
308+
assert.Equal(t, "new:6380", redisRet.Options.Addr)
309+
assert.Equal(t, "new-secret", redisRet.Options.Password)
310+
assert.Equal(t, "test:", redisRet.Prefix)
311+
},
312+
},
313+
{
314+
name: "Redis with full SerializableRedisOptions",
315+
conf: &retrieverconf.RetrieverConf{
316+
Kind: retrieverconf.RedisRetriever,
317+
RedisOptions: &retrieverconf.SerializableRedisOptions{
318+
Addr: "redis:6379",
319+
Password: "pass",
320+
DB: 3,
321+
MaxRetries: 5,
322+
DialTimeoutMs: 10000,
323+
ReadTimeoutMs: 5000,
324+
PoolSize: 20,
325+
MinIdleConns: 5,
326+
ClientName: "test-client",
327+
ContextTimeoutEnabled: true,
328+
},
329+
},
330+
wantErr: false,
331+
validateRetriever: func(t *testing.T, r retriever.Retriever) {
332+
redisRet, ok := r.(*redisretriever.Retriever)
333+
require.True(t, ok, "expected *redisretriever.Retriever")
334+
assert.Equal(t, "redis:6379", redisRet.Options.Addr)
335+
assert.Equal(t, "pass", redisRet.Options.Password)
336+
assert.Equal(t, 3, redisRet.Options.DB)
337+
assert.Equal(t, 5, redisRet.Options.MaxRetries)
338+
assert.Equal(t, 10000*time.Millisecond, redisRet.Options.DialTimeout)
339+
assert.Equal(t, 5000*time.Millisecond, redisRet.Options.ReadTimeout)
340+
assert.Equal(t, 20, redisRet.Options.PoolSize)
341+
assert.Equal(t, 5, redisRet.Options.MinIdleConns)
342+
assert.Equal(t, "test-client", redisRet.Options.ClientName)
343+
assert.True(t, redisRet.Options.ContextTimeoutEnabled)
344+
},
345+
},
346+
}
347+
348+
for _, tt := range tests {
349+
t.Run(tt.name, func(t *testing.T) {
350+
got, err := InitRetriever(tt.conf)
351+
if tt.wantErr {
352+
assert.Error(t, err)
353+
} else {
354+
assert.NoError(t, err)
355+
assert.NotNil(t, got)
356+
assert.IsType(t, &redisretriever.Retriever{}, got)
357+
if tt.validateRetriever != nil {
358+
tt.validateRetriever(t, got)
359+
}
360+
}
361+
})
362+
}
363+
}

cmdhelpers/retrieverconf/retriever_conf.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"net/http"
66
"time"
77

8-
"github.com/redis/go-redis/v9"
98
"github.com/thomaspoignant/go-feature-flag/cmdhelpers/err"
109
)
1110

@@ -52,14 +51,17 @@ type RetrieverConf struct {
5251

5352
// Columns is used by
5453
// - the postgresql retriever (it allows to use custom column names)
55-
Columns map[string]string `mapstructure:"columns" koanf:"columns"`
56-
Database string `mapstructure:"database" koanf:"database"`
57-
Collection string `mapstructure:"collection" koanf:"collection"`
58-
RedisOptions *redis.Options `mapstructure:"redisOptions" koanf:"redisOptions"`
59-
RedisPrefix string `mapstructure:"redisPrefix" koanf:"redisPrefix"`
60-
AccountName string `mapstructure:"accountName" koanf:"accountname"`
61-
AccountKey string `mapstructure:"accountKey" koanf:"accountkey"`
62-
Container string `mapstructure:"container" koanf:"container"`
54+
Columns map[string]string `mapstructure:"columns" koanf:"columns"`
55+
Database string `mapstructure:"database" koanf:"database"`
56+
Collection string `mapstructure:"collection" koanf:"collection"`
57+
58+
// RedisOptions is the serializable redis configuration that can be used in JSON/YAML files
59+
RedisOptions *SerializableRedisOptions `mapstructure:"redisOptions" koanf:"redisOptions"`
60+
61+
RedisPrefix string `mapstructure:"redisPrefix" koanf:"redisPrefix"`
62+
AccountName string `mapstructure:"accountName" koanf:"accountname"`
63+
AccountKey string `mapstructure:"accountKey" koanf:"accountkey"`
64+
Container string `mapstructure:"container" koanf:"container"`
6365
}
6466

6567
// IsValid validate the configuration of the retriever
@@ -155,6 +157,9 @@ func (c *RetrieverConf) validateRedisRetriever() error {
155157
if c.RedisOptions == nil {
156158
return err.NewRetrieverConfError("redisOptions", string(c.Kind))
157159
}
160+
if c.RedisOptions.Addr == "" {
161+
return err.NewRetrieverConfError("redisOptions.addr", string(c.Kind))
162+
}
158163
return nil
159164
}
160165

cmdhelpers/retrieverconf/retriever_conf_test.go

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,14 +254,71 @@ func TestRetrieverConf_IsValid(t *testing.T) {
254254
errValue: "invalid retriever: no \"uri\" property found for kind \"mongodb\"",
255255
},
256256
{
257-
name: "kind redis without options",
257+
name: "kind redis without options (old RedisOptions)",
258258
fields: retrieverconf.RetrieverConf{
259259
Kind: "redis",
260260
RedisPrefix: "xxx",
261261
},
262262
wantErr: true,
263263
errValue: "invalid retriever: no \"redisOptions\" property found for kind \"redis\"",
264264
},
265+
{
266+
name: "kind redis with RedisOptions",
267+
fields: retrieverconf.RetrieverConf{
268+
Kind: "redis",
269+
RedisOptions: &retrieverconf.SerializableRedisOptions{
270+
Addr: "localhost:6379",
271+
},
272+
RedisPrefix: "xxx",
273+
},
274+
wantErr: false,
275+
},
276+
{
277+
name: "kind redis with SerializableRedisOptions",
278+
fields: retrieverconf.RetrieverConf{
279+
Kind: "redis",
280+
RedisOptions: &retrieverconf.SerializableRedisOptions{
281+
Addr: "localhost:6379",
282+
},
283+
RedisPrefix: "xxx",
284+
},
285+
wantErr: false,
286+
},
287+
{
288+
name: "kind redis with RedisOptions",
289+
fields: retrieverconf.RetrieverConf{
290+
Kind: "redis",
291+
RedisOptions: &retrieverconf.SerializableRedisOptions{
292+
Addr: "new:6379",
293+
},
294+
RedisPrefix: "xxx",
295+
},
296+
wantErr: false,
297+
},
298+
{
299+
name: "kind redis with RedisOptions but empty Addr",
300+
fields: retrieverconf.RetrieverConf{
301+
Kind: "redis",
302+
RedisOptions: &retrieverconf.SerializableRedisOptions{
303+
Addr: "",
304+
},
305+
RedisPrefix: "xxx",
306+
},
307+
wantErr: true,
308+
errValue: "invalid retriever: no \"redisOptions.addr\" property found for kind \"redis\"",
309+
},
310+
{
311+
name: "kind redis with RedisOptions but empty Addr (2)",
312+
fields: retrieverconf.RetrieverConf{
313+
Kind: "redis",
314+
RedisOptions: &retrieverconf.SerializableRedisOptions{
315+
Addr: "",
316+
},
317+
RedisPrefix: "xxx",
318+
},
319+
wantErr: true,
320+
errValue: "invalid retriever: no \"redisOptions.addr\" property found for kind \"redis\"",
321+
},
265322
{
266323
name: "kind mongoDB without Collection",
267324
fields: retrieverconf.RetrieverConf{

0 commit comments

Comments
 (0)