Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ Below is a comprehensive list of available configuration properties.
| sdkKeys | OPTIMIZELY_SDKKEYS | Comma delimited list of SDK keys used to initialize on startup |
| cmab | OPTIMIZELY_CMAB | Complete JSON configuration for CMAB. Format: see example below |
| cmab.cache | OPTIMIZELY_CMAB_CACHE | JSON configuration for just the CMAB cache section. Format: see example below |
| cmab.predictionEndpoint | OPTIMIZELY_CMAB_PREDICTIONENDPOINT | URL template for CMAB prediction API with %s placeholder for experimentId. Default: https://prediction.cmab.optimizely.com/predict/%s |
| cmab.retryConfig | OPTIMIZELY_CMAB_RETRYCONFIG | JSON configuration for just the CMAB retry settings. Format: see example below |
| server.allowedHosts | OPTIMIZELY_SERVER_ALLOWEDHOSTS | List of allowed request host values. Requests whose host value does not match either the configured server.host, or one of these, will be rejected with a 404 response. To match all subdomains, you can use a leading dot (for example `.example.com` matches `my.example.com`, `hello.world.example.com`, etc.). You can use the value `.` to disable allowed host checking, allowing requests with any host. Request host is determined in the following priority order: 1. X-Forwarded-Host header value, 2. Forwarded header host= directive value, 3. Host property of request (see Host under https://pkg.go.dev/net/http#Request). Note: don't include port in these hosts values - port is stripped from the request host before comparing against these. |
| server.batchRequests.maxConcurrency | OPTIMIZELY_SERVER_BATCHREQUESTS_MAXCONCURRENCY | Number of requests running in parallel. Default: 10 |
Expand All @@ -150,6 +151,7 @@ Below is a comprehensive list of available configuration properties.
```json
{
"requestTimeout": "5s",
"predictionEndpoint": "https://prediction.cmab.optimizely.com/predict/%s",
"cache": {
"type": "memory",
"size": 2000,
Expand Down
55 changes: 17 additions & 38 deletions cmd/optimizely/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import (
"github.com/optimizely/agent/pkg/optimizely"
"github.com/optimizely/agent/pkg/routers"
"github.com/optimizely/agent/pkg/server"
_ "github.com/optimizely/agent/plugins/cmabcache/all" // Initiate the loading of the cmabCache plugins
_ "github.com/optimizely/agent/plugins/interceptors/all" // Initiate the loading of the userprofileservice plugins
_ "github.com/optimizely/agent/plugins/odpcache/all" // Initiate the loading of the odpCache plugins
_ "github.com/optimizely/agent/plugins/userprofileservice/all" // Initiate the loading of the interceptor plugins
Expand Down Expand Up @@ -109,81 +110,59 @@ func loadConfig(v *viper.Viper) *config.AgentConfig {
}

// Handle CMAB configuration using the same approach as UserProfileService
// Check for complete CMAB configuration first
if cmab := v.GetStringMap("cmab"); len(cmab) > 0 {
// Check for complete CMAB configuration first (now under client.cmab)
if cmab := v.GetStringMap("client.cmab"); len(cmab) > 0 {
if timeout, ok := cmab["requestTimeout"].(string); ok {
if duration, err := time.ParseDuration(timeout); err == nil {
conf.CMAB.RequestTimeout = duration
conf.Client.CMAB.RequestTimeout = duration
}
}
if cache, ok := cmab["cache"].(map[string]interface{}); ok {
if cacheType, ok := cache["type"].(string); ok {
conf.CMAB.Cache.Type = cacheType
}
if cacheSize, ok := cache["size"].(float64); ok {
conf.CMAB.Cache.Size = int(cacheSize)
}
if cacheTTL, ok := cache["ttl"].(string); ok {
if duration, err := time.ParseDuration(cacheTTL); err == nil {
conf.CMAB.Cache.TTL = duration
}
}
conf.Client.CMAB.Cache = cache
}
if retryConfig, ok := cmab["retryConfig"].(map[string]interface{}); ok {
if maxRetries, ok := retryConfig["maxRetries"].(float64); ok {
conf.CMAB.RetryConfig.MaxRetries = int(maxRetries)
conf.Client.CMAB.RetryConfig.MaxRetries = int(maxRetries)
}
if initialBackoff, ok := retryConfig["initialBackoff"].(string); ok {
if duration, err := time.ParseDuration(initialBackoff); err == nil {
conf.CMAB.RetryConfig.InitialBackoff = duration
conf.Client.CMAB.RetryConfig.InitialBackoff = duration
}
}
if maxBackoff, ok := retryConfig["maxBackoff"].(string); ok {
if duration, err := time.ParseDuration(maxBackoff); err == nil {
conf.CMAB.RetryConfig.MaxBackoff = duration
conf.Client.CMAB.RetryConfig.MaxBackoff = duration
}
}
if backoffMultiplier, ok := retryConfig["backoffMultiplier"].(float64); ok {
conf.CMAB.RetryConfig.BackoffMultiplier = backoffMultiplier
conf.Client.CMAB.RetryConfig.BackoffMultiplier = backoffMultiplier
}
}
}

// Check for individual map sections
if cmabCache := v.GetStringMap("cmab.cache"); len(cmabCache) > 0 {
if cacheType, ok := cmabCache["type"].(string); ok {
conf.CMAB.Cache.Type = cacheType
}
if cacheSize, ok := cmabCache["size"].(int); ok {
conf.CMAB.Cache.Size = cacheSize
} else if cacheSize, ok := cmabCache["size"].(float64); ok {
conf.CMAB.Cache.Size = int(cacheSize)
}
if cacheTTL, ok := cmabCache["ttl"].(string); ok {
if duration, err := time.ParseDuration(cacheTTL); err == nil {
conf.CMAB.Cache.TTL = duration
}
}
if cmabCache := v.GetStringMap("client.cmab.cache"); len(cmabCache) > 0 {
conf.Client.CMAB.Cache = cmabCache
}

if cmabRetryConfig := v.GetStringMap("cmab.retryConfig"); len(cmabRetryConfig) > 0 {
if cmabRetryConfig := v.GetStringMap("client.cmab.retryConfig"); len(cmabRetryConfig) > 0 {
if maxRetries, ok := cmabRetryConfig["maxRetries"].(int); ok {
conf.CMAB.RetryConfig.MaxRetries = maxRetries
conf.Client.CMAB.RetryConfig.MaxRetries = maxRetries
} else if maxRetries, ok := cmabRetryConfig["maxRetries"].(float64); ok {
conf.CMAB.RetryConfig.MaxRetries = int(maxRetries)
conf.Client.CMAB.RetryConfig.MaxRetries = int(maxRetries)
}
if initialBackoff, ok := cmabRetryConfig["initialBackoff"].(string); ok {
if duration, err := time.ParseDuration(initialBackoff); err == nil {
conf.CMAB.RetryConfig.InitialBackoff = duration
conf.Client.CMAB.RetryConfig.InitialBackoff = duration
}
}
if maxBackoff, ok := cmabRetryConfig["maxBackoff"].(string); ok {
if duration, err := time.ParseDuration(maxBackoff); err == nil {
conf.CMAB.RetryConfig.MaxBackoff = duration
conf.Client.CMAB.RetryConfig.MaxBackoff = duration
}
}
if backoffMultiplier, ok := cmabRetryConfig["backoffMultiplier"].(float64); ok {
conf.CMAB.RetryConfig.BackoffMultiplier = backoffMultiplier
conf.Client.CMAB.RetryConfig.BackoffMultiplier = backoffMultiplier
}
}

Expand Down
156 changes: 113 additions & 43 deletions cmd/optimizely/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,20 @@ func assertCMAB(t *testing.T, cmab config.CMABConfig) {
// Base assertions
assert.Equal(t, 15*time.Second, cmab.RequestTimeout)

// Check cache configuration
// Check cache configuration (now a map[string]interface{})
cache := cmab.Cache
assert.Equal(t, "redis", cache.Type)
assert.Equal(t, 2000, cache.Size)
assert.Equal(t, 45*time.Minute, cache.TTL)
assert.NotNil(t, cache)
assert.Equal(t, "redis", cache["default"])

// Check services configuration
if services, ok := cache["services"].(map[string]interface{}); ok {
if redisConfig, ok := services["redis"].(map[string]interface{}); ok {
// Redis config should have host, database, and timeout fields
assert.NotNil(t, redisConfig["host"])
assert.NotNil(t, redisConfig["database"])
assert.NotNil(t, redisConfig["timeout"])
}
}

// Check retry configuration
retry := cmab.RetryConfig
Expand All @@ -204,12 +213,17 @@ func assertCMAB(t *testing.T, cmab config.CMABConfig) {
}

func TestCMABEnvDebug(t *testing.T) {
_ = os.Setenv("OPTIMIZELY_CMAB", `{
_ = os.Setenv("OPTIMIZELY_CLIENT_CMAB", `{
"requestTimeout": "15s",
"cache": {
"type": "redis",
"size": 2000,
"ttl": "45m"
"default": "redis",
"services": {
"redis": {
"host": "localhost:6379",
"database": 0,
"timeout": "45m"
}
}
},
"retryConfig": {
"maxRetries": 5,
Expand All @@ -231,40 +245,87 @@ func TestCMABEnvDebug(t *testing.T) {

// Debug: Print the parsed config
fmt.Println("Parsed CMAB config from JSON env var:")
fmt.Printf(" RequestTimeout: %v\n", conf.CMAB.RequestTimeout)
fmt.Printf(" Cache: %+v\n", conf.CMAB.Cache)
fmt.Printf(" RetryConfig: %+v\n", conf.CMAB.RetryConfig)
fmt.Printf(" RequestTimeout: %v\n", conf.Client.CMAB.RequestTimeout)
fmt.Printf(" Cache: %+v\n", conf.Client.CMAB.Cache)
fmt.Printf(" RetryConfig: %+v\n", conf.Client.CMAB.RetryConfig)

// Call assertCMAB
assertCMAB(t, conf.CMAB)
assertCMAB(t, conf.Client.CMAB)
}

func TestCMABPartialConfig(t *testing.T) {
// Clean any existing environment variables
os.Unsetenv("OPTIMIZELY_CMAB")
os.Unsetenv("OPTIMIZELY_CMAB_CACHE")
os.Unsetenv("OPTIMIZELY_CMAB_RETRYCONFIG")
os.Unsetenv("OPTIMIZELY_CLIENT_CMAB")
os.Unsetenv("OPTIMIZELY_CLIENT_CMAB_CACHE")
os.Unsetenv("OPTIMIZELY_CLIENT_CMAB_RETRYCONFIG")

// Set partial configuration through CMAB_CACHE and CMAB_RETRYCONFIG
_ = os.Setenv("OPTIMIZELY_CMAB_CACHE", `{"type": "redis", "size": 3000}`)
_ = os.Setenv("OPTIMIZELY_CMAB_RETRYCONFIG", `{"maxRetries": 10}`)
// Note: Cache is now a service-based map config
_ = os.Setenv("OPTIMIZELY_CLIENT_CMAB_CACHE", `{"default": "redis", "services": {"redis": {"host": "localhost:6379", "database": 0}}}`)
_ = os.Setenv("OPTIMIZELY_CLIENT_CMAB_RETRYCONFIG", `{"maxRetries": 10}`)

// Load config
v := viper.New()
assert.NoError(t, initConfig(v))
conf := loadConfig(v)

// Cache assertions
assert.Equal(t, "redis", conf.CMAB.Cache.Type)
assert.Equal(t, 3000, conf.CMAB.Cache.Size)
// Cache assertions (cache is now map[string]interface{})
assert.NotNil(t, conf.Client.CMAB.Cache)
if defaultCache, ok := conf.Client.CMAB.Cache["default"].(string); ok {
assert.Equal(t, "redis", defaultCache)
}

// RetryConfig assertions
assert.Equal(t, 10, conf.CMAB.RetryConfig.MaxRetries)
assert.Equal(t, 10, conf.Client.CMAB.RetryConfig.MaxRetries)

// Clean up
os.Unsetenv("OPTIMIZELY_CMAB")
os.Unsetenv("OPTIMIZELY_CMAB_CACHE")
os.Unsetenv("OPTIMIZELY_CMAB_RETRYCONFIG")
os.Unsetenv("OPTIMIZELY_CLIENT_CMAB")
os.Unsetenv("OPTIMIZELY_CLIENT_CMAB_CACHE")
os.Unsetenv("OPTIMIZELY_CLIENT_CMAB_RETRYCONFIG")
}

func TestCMABRetryConfigAllFields(t *testing.T) {
// Clean any existing environment variables
os.Unsetenv("OPTIMIZELY_CLIENT_CMAB")
os.Unsetenv("OPTIMIZELY_CLIENT_CMAB_CACHE")
os.Unsetenv("OPTIMIZELY_CLIENT_CMAB_RETRYCONFIG")

// Set all retry config fields via CMAB_RETRYCONFIG to cover lines 154-165
_ = os.Setenv("OPTIMIZELY_CLIENT_CMAB_RETRYCONFIG", `{
"maxRetries": 5,
"initialBackoff": "500ms",
"maxBackoff": "45s",
"backoffMultiplier": 2.5
}`)

defer func() {
os.Unsetenv("OPTIMIZELY_CLIENT_CMAB_RETRYCONFIG")
}()

v := viper.New()
assert.NoError(t, initConfig(v))
conf := loadConfig(v)

// Verify all retry config fields were parsed correctly
assert.Equal(t, 5, conf.Client.CMAB.RetryConfig.MaxRetries)
assert.Equal(t, 500*time.Millisecond, conf.Client.CMAB.RetryConfig.InitialBackoff)
assert.Equal(t, 45*time.Second, conf.Client.CMAB.RetryConfig.MaxBackoff)
assert.Equal(t, 2.5, conf.Client.CMAB.RetryConfig.BackoffMultiplier)
}

func TestCMABRetryConfigIntMaxRetries(t *testing.T) {
// Test the int type path for maxRetries (line 150) by using viper's Set method
// which will preserve the int type instead of converting to float64
v := viper.New()
assert.NoError(t, initConfig(v))

// Set via viper directly to ensure it's an int, not float64
v.Set("client.cmab.retryConfig.maxRetries", 7)

conf := loadConfig(v)

// Verify maxRetries was parsed as int
assert.Equal(t, 7, conf.Client.CMAB.RetryConfig.MaxRetries)
}

func TestViperYaml(t *testing.T) {
Expand Down Expand Up @@ -481,12 +542,17 @@ func TestViperEnv(t *testing.T) {
_ = os.Setenv("OPTIMIZELY_WEBHOOK_PROJECTS_20000_SDKKEYS", "xxx,yyy,zzz")
_ = os.Setenv("OPTIMIZELY_WEBHOOK_PROJECTS_20000_SKIPSIGNATURECHECK", "false")

_ = os.Setenv("OPTIMIZELY_CMAB", `{
_ = os.Setenv("OPTIMIZELY_CLIENT_CMAB", `{
"requestTimeout": "15s",
"cache": {
"type": "redis",
"size": 2000,
"ttl": "45m"
"default": "redis",
"services": {
"redis": {
"host": "localhost:6379",
"database": 0,
"timeout": "45m"
}
}
},
"retryConfig": {
"maxRetries": 5,
Expand All @@ -511,7 +577,7 @@ func TestViperEnv(t *testing.T) {
assertAPI(t, actual.API)
//assertWebhook(t, actual.Webhook) // Maps don't appear to be supported
assertRuntime(t, actual.Runtime)
assertCMAB(t, actual.CMAB)
assertCMAB(t, actual.Client.CMAB)
}

func TestLoggingWithIncludeSdkKey(t *testing.T) {
Expand Down Expand Up @@ -615,28 +681,32 @@ func Test_initTracing(t *testing.T) {

func TestCMABComplexJSON(t *testing.T) {
// Clean any existing environment variables for CMAB
os.Unsetenv("OPTIMIZELY_CMAB_CACHE_TYPE")
os.Unsetenv("OPTIMIZELY_CMAB_CACHE_SIZE")
os.Unsetenv("OPTIMIZELY_CMAB_CACHE_TTL")
os.Unsetenv("OPTIMIZELY_CMAB_CACHE_REDIS_HOST")
os.Unsetenv("OPTIMIZELY_CMAB_CACHE_REDIS_PASSWORD")
os.Unsetenv("OPTIMIZELY_CMAB_CACHE_REDIS_DATABASE")
os.Unsetenv("OPTIMIZELY_CLIENT_CMAB_CACHE_TYPE")
os.Unsetenv("OPTIMIZELY_CLIENT_CMAB_CACHE_SIZE")
os.Unsetenv("OPTIMIZELY_CLIENT_CMAB_CACHE_TTL")
os.Unsetenv("OPTIMIZELY_CLIENT_CMAB_CACHE_REDIS_HOST")
os.Unsetenv("OPTIMIZELY_CLIENT_CMAB_CACHE_REDIS_PASSWORD")
os.Unsetenv("OPTIMIZELY_CLIENT_CMAB_CACHE_REDIS_DATABASE")

// Set complex JSON environment variable for CMAB cache
_ = os.Setenv("OPTIMIZELY_CMAB_CACHE", `{"type":"redis","size":5000,"ttl":"3h"}`)
// Set complex JSON environment variable for CMAB cache (using new service-based format)
_ = os.Setenv("OPTIMIZELY_CLIENT_CMAB_CACHE", `{"default":"redis","services":{"redis":{"host":"localhost:6379","database":0,"timeout":"3h"}}}`)

defer func() {
// Clean up
os.Unsetenv("OPTIMIZELY_CMAB_CACHE")
os.Unsetenv("OPTIMIZELY_CLIENT_CMAB_CACHE")
}()

v := viper.New()
assert.NoError(t, initConfig(v))
actual := loadConfig(v)

// Test cache settings from JSON environment variable
cache := actual.CMAB.Cache
assert.Equal(t, "redis", cache.Type)
assert.Equal(t, 5000, cache.Size)
assert.Equal(t, 3*time.Hour, cache.TTL)
// Test cache settings from JSON environment variable (cache is now map[string]interface{})
cache := actual.Client.CMAB.Cache
assert.NotNil(t, cache)
if defaultCache, ok := cache["default"].(string); ok {
assert.Equal(t, "redis", defaultCache)
}
if services, ok := cache["services"].(map[string]interface{}); ok {
assert.NotNil(t, services["redis"])
}
}
Loading