diff --git a/.github/workflows/proxy-ci.yml b/.github/workflows/proxy-ci.yml index a18ae01..43260ef 100644 --- a/.github/workflows/proxy-ci.yml +++ b/.github/workflows/proxy-ci.yml @@ -13,6 +13,15 @@ jobs: test: runs-on: ubuntu-latest + services: + mongodb: + image: mongodb/mongodb-community-server + ports: + - 27017:27017 + dynamodb: + image: amazon/dynamodb-local + ports: + - 8000:8000 steps: - uses: actions/checkout@v4 - name: Setup Go @@ -32,6 +41,15 @@ jobs: coverage: runs-on: ubuntu-latest needs: test + services: + mongodb: + image: mongodb/mongodb-community-server + ports: + - 27017:27017 + dynamodb: + image: amazon/dynamodb-local + ports: + - 8000:8000 steps: - uses: actions/checkout@v4 with: diff --git a/.github/workflows/proxy-release.yml b/.github/workflows/proxy-release.yml index 2cb2dc7..19c5eea 100644 --- a/.github/workflows/proxy-release.yml +++ b/.github/workflows/proxy-release.yml @@ -12,6 +12,15 @@ permissions: jobs: test: runs-on: ubuntu-latest + services: + mongodb: + image: mongodb/mongodb-community-server + ports: + - 27017:27017 + dynamodb: + image: amazon/dynamodb-local + ports: + - 8000:8000 steps: - uses: actions/checkout@v4 - name: Setup Go diff --git a/Dockerfile b/Dockerfile index 4ba36b5..a565d23 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.21-alpine3.18 AS build +FROM golang:1.22.1-alpine3.19 AS build WORKDIR /go/src/configcat_proxy diff --git a/config/config.go b/config/config.go index c8a684e..947b178 100644 --- a/config/config.go +++ b/config/config.go @@ -160,7 +160,9 @@ type GlobalOfflineConfig struct { } type CacheConfig struct { - Redis RedisConfig + Redis RedisConfig + MongoDb MongoDbConfig `yaml:"mongodb"` + DynamoDb DynamoDbConfig `yaml:"dynamodb"` } type RedisConfig struct { @@ -172,6 +174,20 @@ type RedisConfig struct { Tls TlsConfig } +type MongoDbConfig struct { + Enabled bool `yaml:"enabled"` + Url string `yaml:"url"` + Database string `yaml:"database"` + Collection string `yaml:"collection"` + Tls TlsConfig +} + +type DynamoDbConfig struct { + Enabled bool `yaml:"enabled"` + Url string `yaml:"url"` + Table string `yaml:"table"` +} + type LocalConfig struct { FilePath string `yaml:"file_path"` Polling bool `yaml:"polling"` @@ -291,6 +307,11 @@ func (c *Config) setDefaults() { c.Cache.Redis.DB = 0 c.Cache.Redis.Addresses = []string{"localhost:6379"} + + c.Cache.MongoDb.Database = "configcat_proxy" + c.Cache.MongoDb.Collection = "cache" + + c.Cache.DynamoDb.Table = "configcat_proxy_cache" } func (c *Config) fixupDefaults() { @@ -366,6 +387,9 @@ func (c *Config) fixupTlsMinVersions(defVersion float64) { if _, ok := allowedTlsVersions[c.Cache.Redis.Tls.MinVersion]; !ok { c.Cache.Redis.Tls.MinVersion = defVersion } + if _, ok := allowedTlsVersions[c.Cache.MongoDb.Tls.MinVersion]; !ok { + c.Cache.MongoDb.Tls.MinVersion = defVersion + } } func (c *Config) compileOriginRegexes() error { @@ -421,6 +445,25 @@ func (k *KeepAliveConfig) ToParams() (keepalive.ServerParameters, bool) { return param, true } +func (c *CacheConfig) IsSet() bool { + return c.Redis.Enabled || c.MongoDb.Enabled || c.DynamoDb.Enabled +} + +func (t *TlsConfig) LoadTlsOptions() (*tls.Config, error) { + conf := &tls.Config{ + MinVersion: t.GetVersion(), + ServerName: t.ServerName, + } + for _, c := range t.Certificates { + if cert, err := tls.LoadX509KeyPair(c.Cert, c.Key); err == nil { + conf.Certificates = append(conf.Certificates, cert) + } else { + return nil, fmt.Errorf("failed to load certificate and key files: %s", err) + } + } + return conf, nil +} + func defaultConfigPath() (string, bool) { switch runtime.GOOS { case "windows": diff --git a/config/config_test.go b/config/config_test.go index f76eb90..33aa6e5 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -46,11 +46,18 @@ func TestConfig_Defaults(t *testing.T) { assert.Equal(t, 0, conf.Cache.Redis.DB) assert.Equal(t, "localhost:6379", conf.Cache.Redis.Addresses[0]) + assert.Equal(t, "configcat_proxy", conf.Cache.MongoDb.Database) + assert.Equal(t, "cache", conf.Cache.MongoDb.Collection) + + assert.Equal(t, "configcat_proxy_cache", conf.Cache.DynamoDb.Table) + assert.Equal(t, 1.2, conf.Tls.MinVersion) assert.Equal(t, 1.2, conf.Cache.Redis.Tls.MinVersion) + assert.Equal(t, 1.2, conf.Cache.MongoDb.Tls.MinVersion) assert.Equal(t, uint16(tls.VersionTLS12), conf.Tls.GetVersion()) assert.Equal(t, uint16(tls.VersionTLS12), conf.Cache.Redis.Tls.GetVersion()) + assert.Equal(t, uint16(tls.VersionTLS12), conf.Cache.MongoDb.Tls.GetVersion()) assert.Nil(t, conf.DefaultAttrs) } @@ -289,7 +296,7 @@ sdks: }) } -func TestCacheConfig_YAML(t *testing.T) { +func TestRedisConfig_YAML(t *testing.T) { utils.UseTempFile(` cache: redis: @@ -327,6 +334,58 @@ cache: }) } +func TestMongoDbConfig_YAML(t *testing.T) { + utils.UseTempFile(` +cache: + mongodb: + enabled: true + url: "url" + database: "db" + collection: "coll" + tls: + enabled: true + min_version: 1.1 + server_name: "serv" + certificates: + - cert: "./cert1" + key: "./key1" + - cert: "./cert2" + key: "./key2" +`, func(file string) { + conf, err := LoadConfigFromFileAndEnvironment(file) + require.NoError(t, err) + + assert.True(t, conf.Cache.MongoDb.Enabled) + assert.Equal(t, "url", conf.Cache.MongoDb.Url) + assert.Equal(t, "db", conf.Cache.MongoDb.Database) + assert.Equal(t, "coll", conf.Cache.MongoDb.Collection) + assert.True(t, conf.Cache.MongoDb.Tls.Enabled) + assert.Equal(t, tls.VersionTLS11, int(conf.Cache.MongoDb.Tls.GetVersion())) + assert.Equal(t, "serv", conf.Cache.MongoDb.Tls.ServerName) + assert.Equal(t, "./cert1", conf.Cache.MongoDb.Tls.Certificates[0].Cert) + assert.Equal(t, "./key1", conf.Cache.MongoDb.Tls.Certificates[0].Key) + assert.Equal(t, "./cert2", conf.Cache.MongoDb.Tls.Certificates[1].Cert) + assert.Equal(t, "./key2", conf.Cache.MongoDb.Tls.Certificates[1].Key) + }) +} + +func TestDynamoDbConfig_YAML(t *testing.T) { + utils.UseTempFile(` +cache: + dynamodb: + enabled: true + url: "url" + table: "db" +`, func(file string) { + conf, err := LoadConfigFromFileAndEnvironment(file) + require.NoError(t, err) + + assert.True(t, conf.Cache.DynamoDb.Enabled) + assert.Equal(t, "url", conf.Cache.DynamoDb.Url) + assert.Equal(t, "db", conf.Cache.DynamoDb.Table) + }) +} + func TestGlobalOfflineConfig_YAML(t *testing.T) { utils.UseTempFile(` offline: @@ -614,23 +673,108 @@ default_user_attributes: } func TestGrpcConfig_KeepAlive(t *testing.T) { - conf := KeepAliveConfig{MaxConnectionIdle: 1, MaxConnectionAge: 2, MaxConnectionAgeGrace: 3, Time: 4, Timeout: 5} - param, ok := conf.ToParams() - - assert.True(t, ok) - assert.Equal(t, 1*time.Second, param.MaxConnectionIdle) - assert.Equal(t, 2*time.Second, param.MaxConnectionAge) - assert.Equal(t, 3*time.Second, param.MaxConnectionAgeGrace) - assert.Equal(t, 4*time.Second, param.Time) - assert.Equal(t, 5*time.Second, param.Timeout) - - conf = KeepAliveConfig{MaxConnectionIdle: 1} - param, ok = conf.ToParams() - - assert.True(t, ok) - assert.Equal(t, 1*time.Second, param.MaxConnectionIdle) - assert.Equal(t, time.Duration(0), param.MaxConnectionAge) - assert.Equal(t, time.Duration(0), param.MaxConnectionAgeGrace) - assert.Equal(t, time.Duration(0), param.Time) - assert.Equal(t, time.Duration(0), param.Timeout) + t.Run("valid", func(t *testing.T) { + conf := KeepAliveConfig{MaxConnectionIdle: 1, MaxConnectionAge: 2, MaxConnectionAgeGrace: 3, Time: 4, Timeout: 5} + param, ok := conf.ToParams() + + assert.True(t, ok) + assert.Equal(t, 1*time.Second, param.MaxConnectionIdle) + assert.Equal(t, 2*time.Second, param.MaxConnectionAge) + assert.Equal(t, 3*time.Second, param.MaxConnectionAgeGrace) + assert.Equal(t, 4*time.Second, param.Time) + assert.Equal(t, 5*time.Second, param.Timeout) + + conf = KeepAliveConfig{MaxConnectionIdle: 1} + param, ok = conf.ToParams() + + assert.True(t, ok) + assert.Equal(t, 1*time.Second, param.MaxConnectionIdle) + assert.Equal(t, time.Duration(0), param.MaxConnectionAge) + assert.Equal(t, time.Duration(0), param.MaxConnectionAgeGrace) + assert.Equal(t, time.Duration(0), param.Time) + assert.Equal(t, time.Duration(0), param.Timeout) + }) + t.Run("empty", func(t *testing.T) { + conf := KeepAliveConfig{} + _, ok := conf.ToParams() + assert.False(t, ok) + }) +} + +func TestTlsConfig_LoadTlsOptions(t *testing.T) { + t.Run("valid", func(t *testing.T) { + utils.UseTempFile(` +-----BEGIN CERTIFICATE----- +MIICrzCCAZcCFDnpdKF+Pg1smjtIXrNdIgxGYEJfMA0GCSqGSIb3DQEBCwUAMBQx +EjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yMzAzMDEyMTA2NThaFw0yNDAyMjkyMTA2 +NThaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAOiTDTjfAPvJLDZ2mwNvu0pohSHPRzzfZRc16iVI6+ESl0Dwjdjl +yERFO/ts1GQnhE2ggykvoxH4zUy1OCnjTJ+Mm1ryjy4G5ZIILIF9MfFcyma5/5Xd +oOTcDr3ZDTAwFaabKYKisoVMHAJCphencgoyOToW5/HRHMKOEpTJOQWSyNduXYfY +nsWb3hx7WD9NajliW7/Jjbf7UnDtKY2VM2GZWT3ygIH/7SlBqyuXJNqyZXbqfbrP +6mdZQ5wvYsnSUU4kNMtZg/ns+0H5R7PFmRhIRM0nZvJZTO9oHREdm+e2nnZwHyJF +Z26LxE7Qr1bn8+PQSydyQIqeUdaSX2LuXqECAwEAATANBgkqhkiG9w0BAQsFAAOC +AQEAjRoOTe4W4OQ6YOo5kx5sMAozh0Rg6eifS0s8GuxKwfuBop8FEnM3wAfF6x3J +fsik9MmoM4L11HWjttb46UFq/rP3GsA3DLX8i1yBOES+iyCELd5Ss9q1jfr/Jqo3 +cAanE4yl3NNEZoDmMdSj2U11BneKSzHDR+l2hDF9wBifWGI9DQ1ItfA5I6MwnL+0 +J03vcwPSwme4bKC/avAT2oDD7jLGLA+kuhMqHvVq7nXRzs46xyFPBBv7fBxXjPPG +c89d0ISafKtZ9kIKaRrzu2HX+b0fzKr0vtHYDLtC1U5oU7GPB12eupERkmWYlhrw +hDL3X7kt3jEZFkzGV1XL1IJx/g== +-----END CERTIFICATE-----`, func(cert string) { + utils.UseTempFile(`-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDokw043wD7ySw2 +dpsDb7tKaIUhz0c832UXNeolSOvhEpdA8I3Y5chERTv7bNRkJ4RNoIMpL6MR+M1M +tTgp40yfjJta8o8uBuWSCCyBfTHxXMpmuf+V3aDk3A692Q0wMBWmmymCorKFTBwC +QqYXp3IKMjk6Fufx0RzCjhKUyTkFksjXbl2H2J7Fm94ce1g/TWo5Ylu/yY23+1Jw +7SmNlTNhmVk98oCB/+0pQasrlyTasmV26n26z+pnWUOcL2LJ0lFOJDTLWYP57PtB ++UezxZkYSETNJ2byWUzvaB0RHZvntp52cB8iRWdui8RO0K9W5/Pj0EsnckCKnlHW +kl9i7l6hAgMBAAECggEBAOMWiqeIH5a6BGCdiJhfZZmu2qd7k8xdOIDkVN7ZB/B5 +TZTMDUTGgLggfgPubKfqaeW+H7N8XxZyQEtw+wjzduKm0R6JjsJbW5cuQf6htr08 +ZCjP3j5/69TrBb3bjGQL32gRQwPaRsOe4A5Y84JPLivEhFoy+YEFNLbHMF905yeH +IaSeqeK0GNm0a/MU68pa1ODIc8B2zqo+f6I9qekezlDR7Or487FqnlLtNf0yvnLD +sbshzj5rzLdLYgA/RNZ4CkuGddxEYjnDB1IG0NX8m9MrHlsi7jqxa7pHt5oDrRsW +ZxBez6Q70dE29sdl5lnce3qjxweB2NK3Q6Cr2eyizwECgYEA/L/WzgY1yDMWzaCr +SRThg9NWO1EYbvz4uxt7rElfZ+NYAaT08E35Ooo9IeBzp3VoFA1PcNQnKB5pgczO +Mu5W/td5zpx1dzguBZAl4IpKkml08i06R7FxxTqtRM/P7Pna+RagtqAo3JZww3bd +ofIPH2OrobqlcFhOsLqKp5ocDNECgYEA65DJsImeBfW1aZ5ABgPr7NErSv2fKj1r +eGsgC5Za1ZiaG5LWkCpuezsvf6ma4EN3CMl5Fo617qaY6mnL2HlfVtFhHYSeLpna +9ZgqZ1zj2HkqiXOPEkb3d3cC61rXiMK97NpshrpzFx+uMCH8MMu9/CVJEHNKGgAq +6zZQ4LhjaNECgYEA3W4UeprmM2bO64d/iJ9Kk3traLw7c8EdCI+jYeVGOHXsfERQ +ctddKfRCapOBv4wUiry+hFLZm0RJmvYbEHPOs6WDiYd5QeFuMGGBTZ7ahjrtwd3t +2TGUQv6NHmQR/cNIHEG+u0DFi7whPp28vkybAx0HGMG0fyBekGZdY0iYmoECgYEA +3mVOlVYHk9ba1AEsrsErDuSXe/AgQa/E8+YnVek4jqnI7LlfyrHUppFFEcDdUFdB +XVFg+ZP4XXx5p+4EHrbP9NYuWsDm2lY1K2Livb0r+ybBqw0niPjpD6eTYQHdtOcu +ihvZFAWZPL6TJCwhvSvNjOziox5FWnDIFFKuXsqWR9ECgYAfiG1izToF+GX3yUPq +CU+ceTbM2uy3hVnQLvCnraN7hkF02Fa9ZwP6nmnsvhfdaIUP5WLm3A+qMWu/PL0i +F/dUCUF6M/DyihQUnOl+MD9Sg89ZHiftqXSY8jGR14uH4woStyUFHiFbtajmnqV7 +MK4Li/LGWcksyoF+hbPNXMFCIA== +-----END PRIVATE KEY----- +`, func(key string) { + conf := TlsConfig{ + MinVersion: 1.1, + ServerName: "server", + Certificates: []CertConfig{ + {Key: key, Cert: cert}, + }, + } + tlsConf, err := conf.LoadTlsOptions() + assert.NoError(t, err) + assert.Equal(t, uint16(tls.VersionTLS11), tlsConf.MinVersion) + assert.Equal(t, "server", tlsConf.ServerName) + assert.NotEmpty(t, tlsConf.Certificates) + }) + }) + }) + t.Run("invalid", func(t *testing.T) { + conf := TlsConfig{ + MinVersion: 1.1, + ServerName: "server", + Certificates: []CertConfig{ + {Key: "notexisting", Cert: "notexisting"}, + }, + } + tlsConf, err := conf.LoadTlsOptions() + assert.ErrorContains(t, err, "failed to load certificate and key files") + assert.Nil(t, tlsConf) + }) } diff --git a/config/env.go b/config/env.go index a027588..8f4b330 100644 --- a/config/env.go +++ b/config/env.go @@ -187,7 +187,13 @@ func (h *HttpProxyConfig) loadEnv(prefix string) error { func (c *CacheConfig) loadEnv(prefix string) error { prefix = concatPrefix(prefix, "CACHE") - return c.Redis.loadEnv(prefix) + if err := c.Redis.loadEnv(prefix); err != nil { + return err + } + if err := c.MongoDb.loadEnv(prefix); err != nil { + return err + } + return c.DynamoDb.loadEnv(prefix) } func (g *GlobalOfflineConfig) loadEnv(prefix string) error { @@ -246,6 +252,24 @@ func (r *RedisConfig) loadEnv(prefix string) error { return r.Tls.loadEnv(prefix) } +func (m *MongoDbConfig) loadEnv(prefix string) error { + prefix = concatPrefix(prefix, "MONGODB") + readEnvString(prefix, "URL", &m.Url) + readEnvString(prefix, "DATABASE", &m.Database) + readEnvString(prefix, "COLLECTION", &m.Collection) + if err := readEnv(prefix, "ENABLED", &m.Enabled, toBool); err != nil { + return err + } + return m.Tls.loadEnv(prefix) +} + +func (d *DynamoDbConfig) loadEnv(prefix string) error { + prefix = concatPrefix(prefix, "DYNAMODB") + readEnvString(prefix, "URL", &d.Url) + readEnvString(prefix, "TABLE", &d.Table) + return readEnv(prefix, "ENABLED", &d.Enabled, toBool) +} + func (s *SseConfig) loadEnv(prefix string) error { prefix = concatPrefix(prefix, "SSE") if err := readEnv(prefix, "ENABLED", &s.Enabled, toBool); err != nil { diff --git a/config/env_test.go b/config/env_test.go index 5b5eba5..11f31d8 100644 --- a/config/env_test.go +++ b/config/env_test.go @@ -80,7 +80,7 @@ func TestSDKConfig_ENV_Invalid(t *testing.T) { }) } -func TestCacheConfig_ENV(t *testing.T) { +func TestRedisConfig_ENV(t *testing.T) { t.Setenv("CONFIGCAT_CACHE_REDIS_ENABLED", "true") t.Setenv("CONFIGCAT_CACHE_REDIS_DB", "1") t.Setenv("CONFIGCAT_CACHE_REDIS_PASSWORD", "pass") @@ -109,6 +109,45 @@ func TestCacheConfig_ENV(t *testing.T) { assert.Equal(t, "./key2", conf.Cache.Redis.Tls.Certificates[1].Key) } +func TestMongoDbConfig_ENV(t *testing.T) { + t.Setenv("CONFIGCAT_CACHE_MONGODB_ENABLED", "true") + t.Setenv("CONFIGCAT_CACHE_MONGODB_URL", "url") + t.Setenv("CONFIGCAT_CACHE_MONGODB_DATABASE", "db") + t.Setenv("CONFIGCAT_CACHE_MONGODB_COLLECTION", "coll") + t.Setenv("CONFIGCAT_CACHE_MONGODB_TLS_ENABLED", "true") + t.Setenv("CONFIGCAT_CACHE_MONGODB_TLS_MIN_VERSION", "1.1") + t.Setenv("CONFIGCAT_CACHE_MONGODB_TLS_SERVER_NAME", "serv") + t.Setenv("CONFIGCAT_CACHE_MONGODB_TLS_CERTIFICATES", `[{"key":"./key1","cert":"./cert1"},{"key":"./key2","cert":"./cert2"}]`) + + conf, err := LoadConfigFromFileAndEnvironment("") + require.NoError(t, err) + + assert.True(t, conf.Cache.MongoDb.Enabled) + assert.Equal(t, "url", conf.Cache.MongoDb.Url) + assert.Equal(t, "db", conf.Cache.MongoDb.Database) + assert.Equal(t, "coll", conf.Cache.MongoDb.Collection) + assert.True(t, conf.Cache.MongoDb.Tls.Enabled) + assert.Equal(t, tls.VersionTLS11, int(conf.Cache.MongoDb.Tls.GetVersion())) + assert.Equal(t, "serv", conf.Cache.MongoDb.Tls.ServerName) + assert.Equal(t, "./cert1", conf.Cache.MongoDb.Tls.Certificates[0].Cert) + assert.Equal(t, "./key1", conf.Cache.MongoDb.Tls.Certificates[0].Key) + assert.Equal(t, "./cert2", conf.Cache.MongoDb.Tls.Certificates[1].Cert) + assert.Equal(t, "./key2", conf.Cache.MongoDb.Tls.Certificates[1].Key) +} + +func TestDynamoDbConfig_ENV(t *testing.T) { + t.Setenv("CONFIGCAT_CACHE_DYNAMODB_ENABLED", "true") + t.Setenv("CONFIGCAT_CACHE_DYNAMODB_URL", "url") + t.Setenv("CONFIGCAT_CACHE_DYNAMODB_TABLE", "db") + + conf, err := LoadConfigFromFileAndEnvironment("") + require.NoError(t, err) + + assert.True(t, conf.Cache.DynamoDb.Enabled) + assert.Equal(t, "url", conf.Cache.DynamoDb.Url) + assert.Equal(t, "db", conf.Cache.DynamoDb.Table) +} + func TestTlsConfig_ENV(t *testing.T) { t.Setenv("CONFIGCAT_TLS_ENABLED", "true") t.Setenv("CONFIGCAT_TLS_MIN_VERSION", "1.1") diff --git a/config/validate.go b/config/validate.go index 1c354c4..14f3e27 100644 --- a/config/validate.go +++ b/config/validate.go @@ -30,6 +30,9 @@ func (c *Config) Validate() error { if err := c.Cache.Redis.validate(); err != nil { return err } + if err := c.Cache.MongoDb.validate(); err != nil { + return err + } if err := c.GlobalOfflineConfig.validate(&c.Cache); err != nil { return err } @@ -65,6 +68,19 @@ func (r *RedisConfig) validate() error { return nil } +func (m *MongoDbConfig) validate() error { + if !m.Enabled { + return nil + } + if len(m.Url) == 0 { + return fmt.Errorf("mongodb: invalid connection uri") + } + if err := m.Tls.validate(); err != nil { + return err + } + return nil +} + func (o *OfflineConfig) validate(c *CacheConfig, sdkId string) error { if !o.Enabled { return nil @@ -80,7 +96,7 @@ func (o *OfflineConfig) validate(c *CacheConfig, sdkId string) error { return err } } - if o.UseCache && !c.Redis.Enabled { + if o.UseCache && !c.IsSet() { return fmt.Errorf("sdk-" + sdkId + ": offline mode enabled with cache, but no cache is configured") } if o.UseCache && o.CachePollInterval < 1 { diff --git a/config/validate_test.go b/config/validate_test.go index f770352..8d9fb55 100644 --- a/config/validate_test.go +++ b/config/validate_test.go @@ -64,6 +64,19 @@ func TestConfig_Validate(t *testing.T) { conf.setDefaults() require.ErrorContains(t, conf.Validate(), "offline: global offline mode enabled, but no cache is configured") }) + t.Run("mongo enabled without uri", func(t *testing.T) { + conf := Config{SDKs: map[string]*SDKConfig{"env1": {Key: "Key"}}, Cache: CacheConfig{MongoDb: MongoDbConfig{Enabled: true}}, Grpc: GrpcConfig{Port: 100}, Diag: DiagConfig{Port: 90}, Http: HttpConfig{Port: 80}} + require.ErrorContains(t, conf.Validate(), "mongodb: invalid connection uri") + }) + t.Run("mongodb invalid tls config", func(t *testing.T) { + conf := Config{SDKs: map[string]*SDKConfig{"env1": {Key: "Key"}}, Cache: CacheConfig{MongoDb: MongoDbConfig{Enabled: true, Url: "uri", Tls: TlsConfig{Enabled: true, Certificates: []CertConfig{{Key: "key"}}}}}} + conf.setDefaults() + require.ErrorContains(t, conf.Validate(), "tls: both TLS cert and key file required") + + conf = Config{SDKs: map[string]*SDKConfig{"env1": {Key: "Key"}}, Cache: CacheConfig{Redis: RedisConfig{Enabled: true, Addresses: []string{"localhost"}, Tls: TlsConfig{Enabled: true, Certificates: []CertConfig{{Cert: "cert"}}}}}} + conf.setDefaults() + require.ErrorContains(t, conf.Validate(), "tls: both TLS cert and key file required") + }) t.Run("redis enabled without addresses", func(t *testing.T) { conf := Config{SDKs: map[string]*SDKConfig{"env1": {Key: "Key"}}, Cache: CacheConfig{Redis: RedisConfig{Enabled: true}}, Grpc: GrpcConfig{Port: 100}, Diag: DiagConfig{Port: 90}, Http: HttpConfig{Port: 80}} require.ErrorContains(t, conf.Validate(), "redis: at least 1 server address required") diff --git a/diag/server.go b/diag/server.go index 51039ea..29fe63c 100644 --- a/diag/server.go +++ b/diag/server.go @@ -79,7 +79,7 @@ func (s *Server) Shutdown() { err := s.httpServer.Shutdown(ctx) if err != nil { - s.log.Errorf("shutdown error: %v", err) + s.log.Errorf("shutdown error: %s", err) } s.log.Reportf("server shutdown complete") } diff --git a/diag/server_test.go b/diag/server_test.go index 9f68a5b..a267958 100644 --- a/diag/server_test.go +++ b/diag/server_test.go @@ -19,7 +19,10 @@ func TestNewServer(t *testing.T) { Status: config.StatusConfig{Enabled: true}, Metrics: config.MetricsConfig{Enabled: true}, } - srv := NewServer(&conf, status.NewReporter(&config.Config{SDKs: map[string]*config.SDKConfig{"sdk": {Key: "key"}}}), metrics.NewReporter(), log.NewNullLogger(), errChan) + + reporter := status.NewEmptyReporter() + reporter.RegisterSdk("test", &config.SDKConfig{Key: "key"}) + srv := NewServer(&conf, reporter, metrics.NewReporter(), log.NewNullLogger(), errChan) srv.Listen() time.Sleep(500 * time.Millisecond) @@ -47,7 +50,9 @@ func TestNewServer_NotEnabled(t *testing.T) { Metrics: config.MetricsConfig{Enabled: false}, } - srv := NewServer(&conf, status.NewReporter(&config.Config{SDKs: map[string]*config.SDKConfig{"sdk": {Key: "key"}}}), metrics.NewReporter(), log.NewNullLogger(), errChan) + reporter := status.NewEmptyReporter() + reporter.RegisterSdk("test", &config.SDKConfig{Key: "key"}) + srv := NewServer(&conf, reporter, metrics.NewReporter(), log.NewNullLogger(), errChan) srv.Listen() time.Sleep(500 * time.Millisecond) diff --git a/diag/status/mware_test.go b/diag/status/mware_test.go index 6a04200..9a402da 100644 --- a/diag/status/mware_test.go +++ b/diag/status/mware_test.go @@ -10,7 +10,8 @@ import ( func TestInterceptSdk(t *testing.T) { t.Run("ok", func(t *testing.T) { - reporter := NewReporter(&config.Config{SDKs: map[string]*config.SDKConfig{"test": {}}}).(*reporter) + reporter := NewEmptyReporter().(*reporter) + reporter.RegisterSdk("test", &config.SDKConfig{Key: "key"}) repSrv := httptest.NewServer(reporter.HttpHandler()) h := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(http.StatusOK) @@ -33,7 +34,8 @@ func TestInterceptSdk(t *testing.T) { assert.Equal(t, 0, len(stat.Cache.Records)) }) t.Run("not modified", func(t *testing.T) { - reporter := NewReporter(&config.Config{SDKs: map[string]*config.SDKConfig{"test": {}}}).(*reporter) + reporter := NewEmptyReporter().(*reporter) + reporter.RegisterSdk("test", &config.SDKConfig{Key: "key"}) repSrv := httptest.NewServer(reporter.HttpHandler()) h := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(http.StatusNotModified) @@ -56,7 +58,8 @@ func TestInterceptSdk(t *testing.T) { assert.Equal(t, 0, len(stat.Cache.Records)) }) t.Run("error", func(t *testing.T) { - reporter := NewReporter(&config.Config{SDKs: map[string]*config.SDKConfig{"test": {}}}).(*reporter) + reporter := NewEmptyReporter().(*reporter) + reporter.RegisterSdk("test", &config.SDKConfig{Key: "key"}) repSrv := httptest.NewServer(reporter.HttpHandler()) h := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(http.StatusBadRequest) diff --git a/diag/status/status.go b/diag/status/status.go index 94ac664..3cd6730 100644 --- a/diag/status/status.go +++ b/diag/status/status.go @@ -34,6 +34,7 @@ const maxRecordCount = 5 const maxLastErrorsMeaningDegraded = 2 type Reporter interface { + RegisterSdk(sdkId string, conf *config.SDKConfig) ReportOk(component string, message string) ReportError(component string, message string) GetStatus() Status @@ -74,14 +75,14 @@ type reporter struct { records map[string][]record mu sync.RWMutex status Status - conf *config.Config + conf *config.CacheConfig } -func NewNullReporter() Reporter { - return &reporter{records: make(map[string][]record), conf: &config.Config{SDKs: map[string]*config.SDKConfig{}}} +func NewEmptyReporter() Reporter { + return NewReporter(&config.CacheConfig{}) } -func NewReporter(conf *config.Config) Reporter { +func NewReporter(conf *config.CacheConfig) Reporter { r := &reporter{ conf: conf, records: make(map[string][]record), @@ -90,44 +91,54 @@ func NewReporter(conf *config.Config) Reporter { Cache: CacheStatus{ Status: Initializing, }, + SDKs: map[string]*SdkStatus{}, }, } - r.status.SDKs = make(map[string]*SdkStatus, len(conf.SDKs)) - for key, sdk := range conf.SDKs { - status := &SdkStatus{ - Mode: Online, - SdkKey: utils.Obfuscate(sdk.Key, 5), - Source: SdkSourceStatus{ - Type: RemoteSrc, - Status: Initializing, - }, - } - r.status.SDKs[key] = status - if sdk.Offline.Enabled { - status.Mode = Offline - if sdk.Offline.Local.FilePath != "" { - status.Source.Type = FileSrc - r.status.Cache.Status = NA - } else { - status.Source.Type = CacheSrc - } - } - if !conf.Cache.Redis.Enabled { + return r +} + +func (r *reporter) RegisterSdk(sdkId string, conf *config.SDKConfig) { + r.mu.Lock() + defer r.mu.Unlock() + + status := &SdkStatus{ + Mode: Online, + SdkKey: utils.Obfuscate(conf.Key, 5), + Source: SdkSourceStatus{ + Type: RemoteSrc, + Status: Initializing, + }, + } + r.status.SDKs[sdkId] = status + if conf.Offline.Enabled { + status.Mode = Offline + if conf.Offline.Local.FilePath != "" { + status.Source.Type = FileSrc r.status.Cache.Status = NA - if status.Source.Type == CacheSrc { - r.ReportError(key, "cache offline source enabled without a configured cache") - } + } else { + status.Source.Type = CacheSrc + } + } + if !r.conf.IsSet() { + r.status.Cache.Status = NA + if status.Source.Type == CacheSrc { + r.appendRecord(sdkId, "cache offline source enabled without a configured cache", true) } } - return r } func (r *reporter) ReportOk(component string, message string) { - r.appendRecord(component, "[ok] "+message, false) + r.mu.Lock() + defer r.mu.Unlock() + + r.appendRecord(component, message, false) } func (r *reporter) ReportError(component string, message string) { - r.appendRecord(component, "[error] "+message, true) + r.mu.Lock() + defer r.mu.Unlock() + + r.appendRecord(component, message, true) } func (r *reporter) HttpHandler() http.HandlerFunc { @@ -169,8 +180,11 @@ func (r *reporter) checkStatus(records []record) ([]string, HealthStatus) { } func (r *reporter) appendRecord(component string, message string, isError bool) { - r.mu.Lock() - defer r.mu.Unlock() + if isError { + message = "[error] " + message + } else { + message = "[ok] " + message + } recs, ok := r.records[component] if !ok { @@ -194,14 +208,12 @@ func (r *reporter) appendRecord(component string, message string, isError bool) allSdksDown := true hasDegradedSdk := false - for key := range r.conf.SDKs { - if sdk, ok := r.status.SDKs[key]; ok { - if sdk.Source.Status != Down { - allSdksDown = false - } - if sdk.Source.Status != Healthy { - hasDegradedSdk = true - } + for _, sdk := range r.status.SDKs { + if sdk.Source.Status != Down { + allSdksDown = false + } + if sdk.Source.Status != Healthy { + hasDegradedSdk = true } } if !hasDegradedSdk && !allSdksDown { diff --git a/diag/status/status_test.go b/diag/status/status_test.go index 2da64ac..eb396ee 100644 --- a/diag/status/status_test.go +++ b/diag/status/status_test.go @@ -12,7 +12,8 @@ import ( func TestReporter_Online(t *testing.T) { t.Run("ok", func(t *testing.T) { - reporter := NewReporter(&config.Config{SDKs: map[string]*config.SDKConfig{"t": {}}}) + reporter := NewEmptyReporter() + reporter.RegisterSdk("t", &config.SDKConfig{}) srv := httptest.NewServer(reporter.HttpHandler()) stat := readStatus(srv.URL) @@ -35,9 +36,9 @@ func TestReporter_Online(t *testing.T) { assert.Equal(t, NA, stat.Cache.Status) assert.Equal(t, 0, len(stat.Cache.Records)) }) - t.Run("down after 1 error, then ok, then degraded", func(t *testing.T) { - reporter := NewReporter(&config.Config{SDKs: map[string]*config.SDKConfig{"t": {}}}) + reporter := NewEmptyReporter() + reporter.RegisterSdk("t", &config.SDKConfig{}) srv := httptest.NewServer(reporter.HttpHandler()) reporter.ReportError("t", "") stat := readStatus(srv.URL) @@ -74,7 +75,8 @@ func TestReporter_Online(t *testing.T) { assert.Equal(t, 0, len(stat.Cache.Records)) }) t.Run("max 5 records", func(t *testing.T) { - reporter := NewReporter(&config.Config{SDKs: map[string]*config.SDKConfig{"t": {}}}) + reporter := NewEmptyReporter() + reporter.RegisterSdk("t", &config.SDKConfig{}) srv := httptest.NewServer(reporter.HttpHandler()) reporter.ReportOk("t", "m1") reporter.ReportOk("t", "m2") @@ -93,9 +95,48 @@ func TestReporter_Online(t *testing.T) { }) } +func TestReporter_Report_NonExisting(t *testing.T) { + reporter := NewEmptyReporter() + srv := httptest.NewServer(reporter.HttpHandler()) + + reporter.ReportOk("t1", "") + reporter.ReportError("t1", "") + stat := readStatus(srv.URL) + + assert.Equal(t, Initializing, stat.Status) + assert.Empty(t, stat.SDKs) + assert.Equal(t, Initializing, stat.Cache.Status) + assert.Equal(t, 0, len(stat.Cache.Records)) + + reporter.RegisterSdk("t2", &config.SDKConfig{}) + reporter.ReportOk("t1", "") + reporter.ReportError("t1", "") + reporter.ReportOk("t2", "") + stat = readStatus(srv.URL) + + assert.Equal(t, Healthy, stat.Status) + assert.Equal(t, Healthy, stat.SDKs["t2"].Source.Status) + assert.Equal(t, Online, stat.SDKs["t2"].Mode) + assert.Equal(t, 1, len(stat.SDKs["t2"].Source.Records)) + assert.Equal(t, RemoteSrc, stat.SDKs["t2"].Source.Type) + assert.Equal(t, NA, stat.Cache.Status) + assert.Equal(t, 0, len(stat.Cache.Records)) +} + +func TestReporter_Key_Obfuscation(t *testing.T) { + reporter := NewEmptyReporter() + srv := httptest.NewServer(reporter.HttpHandler()) + + reporter.RegisterSdk("t", &config.SDKConfig{Key: "XxPbCKmzIUGORk4vsufpzw/iC_KABprDEueeQs3yovVnQ"}) + stat := readStatus(srv.URL) + + assert.Equal(t, "****************************************ovVnQ", stat.SDKs["t"].SdkKey) +} + func TestReporter_Offline(t *testing.T) { t.Run("file", func(t *testing.T) { - reporter := NewReporter(&config.Config{SDKs: map[string]*config.SDKConfig{"t": {Offline: config.OfflineConfig{Enabled: true, Local: config.LocalConfig{FilePath: "test"}}}}}) + reporter := NewEmptyReporter() + reporter.RegisterSdk("t", &config.SDKConfig{Offline: config.OfflineConfig{Enabled: true, Local: config.LocalConfig{FilePath: "test"}}}) srv := httptest.NewServer(reporter.HttpHandler()) reporter.ReportOk("t", "") stat := readStatus(srv.URL) @@ -109,7 +150,8 @@ func TestReporter_Offline(t *testing.T) { assert.Equal(t, 0, len(stat.Cache.Records)) }) t.Run("cache invalid", func(t *testing.T) { - reporter := NewReporter(&config.Config{SDKs: map[string]*config.SDKConfig{"t": {Offline: config.OfflineConfig{Enabled: true, UseCache: true}}}}) + reporter := NewEmptyReporter() + reporter.RegisterSdk("t", &config.SDKConfig{Offline: config.OfflineConfig{Enabled: true, UseCache: true}}) srv := httptest.NewServer(reporter.HttpHandler()) stat := readStatus(srv.URL) @@ -122,7 +164,8 @@ func TestReporter_Offline(t *testing.T) { assert.Equal(t, 0, len(stat.Cache.Records)) }) t.Run("cache err", func(t *testing.T) { - reporter := NewReporter(&config.Config{SDKs: map[string]*config.SDKConfig{"t": {Offline: config.OfflineConfig{Enabled: true, UseCache: true}}}, Cache: config.CacheConfig{Redis: config.RedisConfig{Enabled: true}}}) + reporter := NewReporter(&config.CacheConfig{Redis: config.RedisConfig{Enabled: true}}) + reporter.RegisterSdk("t", &config.SDKConfig{Offline: config.OfflineConfig{Enabled: true, UseCache: true}}) srv := httptest.NewServer(reporter.HttpHandler()) reporter.ReportError("t", "") reporter.ReportError("t", "") @@ -135,7 +178,8 @@ func TestReporter_Offline(t *testing.T) { assert.Equal(t, CacheSrc, stat.SDKs["t"].Source.Type) }) t.Run("cache valid", func(t *testing.T) { - reporter := NewReporter(&config.Config{SDKs: map[string]*config.SDKConfig{"t": {Offline: config.OfflineConfig{Enabled: true, UseCache: true}}}, Cache: config.CacheConfig{Redis: config.RedisConfig{Enabled: true}}}) + reporter := NewReporter(&config.CacheConfig{Redis: config.RedisConfig{Enabled: true}}) + reporter.RegisterSdk("t", &config.SDKConfig{Offline: config.OfflineConfig{Enabled: true, UseCache: true}}) srv := httptest.NewServer(reporter.HttpHandler()) reporter.ReportOk("t", "") reporter.ReportOk(Cache, "") @@ -153,7 +197,8 @@ func TestReporter_Offline(t *testing.T) { func TestReporter_Degraded_Calc(t *testing.T) { t.Run("1 record first, 1 error", func(t *testing.T) { - reporter := NewReporter(&config.Config{SDKs: map[string]*config.SDKConfig{"t": {}}}).(*reporter) + reporter := NewEmptyReporter().(*reporter) + reporter.RegisterSdk("t", &config.SDKConfig{}) reporter.ReportError("t", "") stat := reporter.GetStatus() @@ -161,7 +206,8 @@ func TestReporter_Degraded_Calc(t *testing.T) { assert.Equal(t, Down, stat.SDKs["t"].Source.Status) }) t.Run("2 records, 1 error then 1 ok", func(t *testing.T) { - reporter := NewReporter(&config.Config{SDKs: map[string]*config.SDKConfig{"t": {}}}).(*reporter) + reporter := NewEmptyReporter().(*reporter) + reporter.RegisterSdk("t", &config.SDKConfig{}) reporter.ReportError("t", "") reporter.ReportOk("t", "") stat := reporter.GetStatus() @@ -170,7 +216,8 @@ func TestReporter_Degraded_Calc(t *testing.T) { assert.Equal(t, Healthy, stat.SDKs["t"].Source.Status) }) t.Run("2 records, 1 ok then 1 error", func(t *testing.T) { - reporter := NewReporter(&config.Config{SDKs: map[string]*config.SDKConfig{"t": {}}}).(*reporter) + reporter := NewEmptyReporter().(*reporter) + reporter.RegisterSdk("t", &config.SDKConfig{}) reporter.ReportOk("t", "") reporter.ReportError("t", "") stat := reporter.GetStatus() @@ -179,7 +226,8 @@ func TestReporter_Degraded_Calc(t *testing.T) { assert.Equal(t, Healthy, stat.SDKs["t"].Source.Status) }) t.Run("3 records, 1 ok then 2 errors", func(t *testing.T) { - reporter := NewReporter(&config.Config{SDKs: map[string]*config.SDKConfig{"t": {}}}).(*reporter) + reporter := NewEmptyReporter().(*reporter) + reporter.RegisterSdk("t", &config.SDKConfig{}) reporter.ReportOk("t", "") reporter.ReportError("t", "") reporter.ReportError("t", "") @@ -189,7 +237,8 @@ func TestReporter_Degraded_Calc(t *testing.T) { assert.Equal(t, Degraded, stat.SDKs["t"].Source.Status) }) t.Run("3 records, 1 ok then 1 error then 1 ok", func(t *testing.T) { - reporter := NewReporter(&config.Config{SDKs: map[string]*config.SDKConfig{"t": {}}}).(*reporter) + reporter := NewEmptyReporter().(*reporter) + reporter.RegisterSdk("t", &config.SDKConfig{}) reporter.ReportOk("t", "") reporter.ReportError("t", "") reporter.ReportOk("t", "") @@ -199,7 +248,8 @@ func TestReporter_Degraded_Calc(t *testing.T) { assert.Equal(t, Healthy, stat.SDKs["t"].Source.Status) }) t.Run("3 records, 1 error then 1 ok then 1 error", func(t *testing.T) { - reporter := NewReporter(&config.Config{SDKs: map[string]*config.SDKConfig{"t": {}}}).(*reporter) + reporter := NewEmptyReporter().(*reporter) + reporter.RegisterSdk("t", &config.SDKConfig{}) reporter.ReportError("t", "") reporter.ReportOk("t", "") reporter.ReportError("t", "") @@ -209,7 +259,9 @@ func TestReporter_Degraded_Calc(t *testing.T) { assert.Equal(t, Healthy, stat.SDKs["t"].Source.Status) }) t.Run("2 envs 1 down", func(t *testing.T) { - reporter := NewReporter(&config.Config{SDKs: map[string]*config.SDKConfig{"t1": {}, "t2": {}}}).(*reporter) + reporter := NewEmptyReporter().(*reporter) + reporter.RegisterSdk("t1", &config.SDKConfig{}) + reporter.RegisterSdk("t2", &config.SDKConfig{}) reporter.ReportError("t1", "") reporter.ReportOk("t2", "") stat := reporter.GetStatus() @@ -219,7 +271,9 @@ func TestReporter_Degraded_Calc(t *testing.T) { assert.Equal(t, Healthy, stat.SDKs["t2"].Source.Status) }) t.Run("2 envs 1 degraded", func(t *testing.T) { - reporter := NewReporter(&config.Config{SDKs: map[string]*config.SDKConfig{"t1": {}, "t2": {}}}).(*reporter) + reporter := NewEmptyReporter().(*reporter) + reporter.RegisterSdk("t1", &config.SDKConfig{}) + reporter.RegisterSdk("t2", &config.SDKConfig{}) reporter.ReportError("t1", "") reporter.ReportOk("t1", "") reporter.ReportError("t1", "") @@ -234,9 +288,9 @@ func TestReporter_Degraded_Calc(t *testing.T) { } func TestNewNullReporter(t *testing.T) { - rep := NewNullReporter().(*reporter) + rep := NewEmptyReporter().(*reporter) assert.Empty(t, rep.records) - assert.Empty(t, rep.conf.SDKs) + assert.Empty(t, rep.GetStatus().SDKs) } func readStatus(url string) Status { diff --git a/docker-compose.yml b/docker-compose.yml index 96c9bd0..4127bf3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,11 +34,19 @@ services: - CONFIGCAT_SDK2_BASE_URL=https://test-cdn-global.configcat.com - CONFIGCAT_SDK2_POLL_INTERVAL=300 - CONFIGCAT_SDK2_WEBHOOK_SIGNING_KEY=configcat_whsk_a8w2b38Ofhs0rzXbZhNCvPTeTeUxmerTBy9PzMCX6+E= + - CONFIGCAT_SDK2_LOG_LEVEL=error # - CONFIGCAT_SDK2_OFFLINE_ENABLED=true # - CONFIGCAT_SDK2_OFFLINE_USE_CACHE=true - - CONFIGCAT_SDK2_LOG_LEVEL=error - CONFIGCAT_CACHE_REDIS_ENABLED=true - CONFIGCAT_CACHE_REDIS_ADDRESSES=["redis:6379"] +# - CONFIGCAT_CACHE_MONGODB_ENABLED=true +# - CONFIGCAT_CACHE_MONGODB_URL=mongodb://mongodb:27017 +# - CONFIGCAT_CACHE_DYNAMODB_ENABLED=true +# - CONFIGCAT_CACHE_DYNAMODB_URL=http://dynamodb:8000 +# - AWS_ACCESS_KEY_ID=key +# - AWS_SECRET_ACCESS_KEY=secret +# - AWS_SESSION_TOKEN=session +# - AWS_DEFAULT_REGION=us-east-1 - CONFIGCAT_TLS_ENABLED=true - CONFIGCAT_TLS_CERTIFICATES=[{"key":"./cert/localhost.key","cert":"./cert/localhost.crt"}] - CONFIGCAT_HTTP_PROXY_URL=http://squid:3128 @@ -84,6 +92,16 @@ services: ports: - "6379:6379" + mongodb: + image: mongodb/mongodb-community-server + ports: + - "27017:27017" + + dynamodb: + image: amazon/dynamodb-local + ports: + - "8000:8000" + squid: image: ubuntu/squid ports: diff --git a/go.mod b/go.mod index a84b251..a09a6fa 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,10 @@ module github.com/configcat/configcat-proxy go 1.21 require ( - github.com/alicebob/miniredis/v2 v2.31.1 + github.com/alicebob/miniredis/v2 v2.32.1 + github.com/aws/aws-sdk-go-v2 v1.26.0 + github.com/aws/aws-sdk-go-v2/config v1.27.9 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.31.0 github.com/cespare/xxhash/v2 v2.2.0 github.com/configcat/go-sdk/v9 v9.0.3 github.com/fsnotify/fsnotify v1.7.0 @@ -11,6 +14,7 @@ require ( github.com/prometheus/client_golang v1.19.0 github.com/redis/go-redis/v9 v9.5.1 github.com/stretchr/testify v1.9.0 + go.mongodb.org/mongo-driver v1.14.0 google.golang.org/grpc v1.62.1 google.golang.org/protobuf v1.33.0 gopkg.in/yaml.v3 v3.0.1 @@ -18,18 +22,40 @@ require ( require ( github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.9 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.0 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.20.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.28.5 // indirect + github.com/aws/smithy-go v1.20.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/snappy v0.0.1 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/klauspost/compress v1.13.6 // indirect + github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect - github.com/yuin/gopher-lua v1.1.0 // indirect - golang.org/x/net v0.20.0 // indirect - golang.org/x/sys v0.16.0 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + github.com/yuin/gopher-lua v1.1.1 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect ) diff --git a/go.sum b/go.sum index 3b4aa98..a120359 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,37 @@ -github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= -github.com/alicebob/miniredis/v2 v2.31.1 h1:7XAt0uUg3DtwEKW5ZAGa+K7FZV2DdKQo5K/6TTnfX8Y= -github.com/alicebob/miniredis/v2 v2.31.1/go.mod h1:UB/T2Uztp7MlFSDakaX1sTXUv5CASoprx0wulRT6HBg= +github.com/alicebob/miniredis/v2 v2.32.1 h1:Bz7CciDnYSaa0mX5xODh6GUITRSx+cVhjNoOR4JssBo= +github.com/alicebob/miniredis/v2 v2.32.1/go.mod h1:AqkLNAfUm0K07J28hnAyyQKf/x0YkCY/g5DCtuL01Mw= +github.com/aws/aws-sdk-go-v2 v1.26.0 h1:/Ce4OCiM3EkpW7Y+xUnfAFpchU78K7/Ug01sZni9PgA= +github.com/aws/aws-sdk-go-v2 v1.26.0/go.mod h1:35hUlJVYd+M++iLI3ALmVwMOyRYMmRqUXpTtRGW+K9I= +github.com/aws/aws-sdk-go-v2/config v1.27.9 h1:gRx/NwpNEFSk+yQlgmk1bmxxvQ5TyJ76CWXs9XScTqg= +github.com/aws/aws-sdk-go-v2/config v1.27.9/go.mod h1:dK1FQfpwpql83kbD873E9vz4FyAxuJtR22wzoXn3qq0= +github.com/aws/aws-sdk-go-v2/credentials v1.17.9 h1:N8s0/7yW+h8qR8WaRlPQeJ6czVMNQVNtNdUqf6cItao= +github.com/aws/aws-sdk-go-v2/credentials v1.17.9/go.mod h1:446YhIdmSV0Jf/SLafGZalQo+xr2iw7/fzXGDPTU1yQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.0 h1:af5YzcLf80tv4Em4jWVD75lpnOHSBkPUZxZfGkrI3HI= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.0/go.mod h1:nQ3how7DMnFMWiU1SpECohgC82fpn4cKZ875NDMmwtA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.4 h1:0ScVK/4qZ8CIW0k8jOeFVsyS/sAiXpYxRBLolMkuLQM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.4/go.mod h1:84KyjNZdHC6QZW08nfHI6yZgPd+qRgaWcYsyLUo3QY8= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.4 h1:sHmMWWX5E7guWEFQ9SVo6A3S4xpPrWnd77a6y4WM6PU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.4/go.mod h1:WjpDrhWisWOIoS9n3nk67A3Ll1vfULJ9Kq6h29HTD48= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.31.0 h1:LtsNRZ6+ZYIbJcPiLHcefXeWkw2DZT9iJyXJJQvhvXw= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.31.0/go.mod h1:ua1eYOCxAAT0PUY3LAi9bUFuKJHC/iAksBLqR1Et7aU= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 h1:EyBZibRTVAs6ECHZOw5/wlylS9OcTzwyjeQMudmREjE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1/go.mod h1:JKpmtYhhPs7D97NL/ltqz7yCkERFW5dOlHyVl66ZYF8= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.5 h1:4vkDuYdXXD2xLgWmNalqH3q4u/d1XnaBMBXdVdZXVp0= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.5/go.mod h1:Ko/RW/qUJyM1rdTzZa74uhE2I0t0VXH0ob/MLcc+q+w= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.6 h1:b+E7zIUHMmcB4Dckjpkapoy47W6C9QBv/zoUP+Hn8Kc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.6/go.mod h1:S2fNV0rxrP78NhPbCZeQgY8H9jdDMeGtwcfZIRxzBqU= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.3 h1:mnbuWHOcM70/OFUlZZ5rcdfA8PflGXXiefU/O+1S3+8= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.3/go.mod h1:5HFu51Elk+4oRBZVxmHrSds5jFXmFj8C3w7DVF2gnrs= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.3 h1:uLq0BKatTmDzWa/Nu4WO0M1AaQDaPpwTKAeByEc6WFM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.3/go.mod h1:b+qdhjnxj8GSR6t5YfphOffeoQSQ1KmpoVVuBn+PWxs= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.5 h1:J/PpTf/hllOjx8Xu9DMflff3FajfLxqM5+tepvVXmxg= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.5/go.mod h1:0ih0Z83YDH/QeQ6Ori2yGE2XvWYv/Xm+cZc01LC6oK0= +github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw= +github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -18,6 +47,7 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/configcat/go-sdk/v9 v9.0.3 h1:kK2ClAO02GGsmtm+2HnC5eaqnGTEOw2iyS39MZnUnr8= github.com/configcat/go-sdk/v9 v9.0.3/go.mod h1:LA9GtJxbY8tQAs/LO4VQMPBNmlFqWMdWg707/hvrpmg= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= @@ -26,19 +56,28 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= @@ -53,17 +92,56 @@ github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLB github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE= -github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= +go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM= google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= @@ -76,5 +154,8 @@ google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHh gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/grpc/flag_service.go b/grpc/flag_service.go index acc1486..b5fc1e9 100644 --- a/grpc/flag_service.go +++ b/grpc/flag_service.go @@ -19,15 +19,15 @@ type flagService struct { proto.UnimplementedFlagServiceServer streamServer stream.Server log log.Logger - sdkClients map[string]sdk.Client + sdkRegistrar sdk.Registrar closed chan struct{} } -func newFlagService(sdkClients map[string]sdk.Client, metrics metrics.Reporter, log log.Logger) *flagService { +func newFlagService(sdkRegistrar sdk.Registrar, metrics metrics.Reporter, log log.Logger) *flagService { return &flagService{ - streamServer: stream.NewServer(sdkClients, metrics, log, "grpc"), + streamServer: stream.NewServer(sdkRegistrar, metrics, log, "grpc"), log: log, - sdkClients: sdkClients, + sdkRegistrar: sdkRegistrar, closed: make(chan struct{}), } } @@ -99,10 +99,10 @@ func (s *flagService) EvalFlag(_ context.Context, req *proto.EvalRequest) (*prot if err != nil { return nil, err } - value, err := sdkClient.Eval(req.GetKey(), user) - if err != nil { + value := sdkClient.Eval(req.GetKey(), user) + if value.Error != nil { var errKeyNotFound configcat.ErrKeyNotFound - if errors.As(err, &errKeyNotFound) { + if errors.As(value.Error, &errKeyNotFound) { return nil, status.Error(codes.NotFound, "feature flag or setting with key '"+req.GetKey()+"' not found") } else { return nil, status.Error(codes.Unknown, "the request failed; please check the logs for more details") @@ -132,8 +132,8 @@ func (s *flagService) GetKeys(_ context.Context, req *proto.KeysRequest) (*proto return nil, status.Error(codes.InvalidArgument, "sdk id parameter missing") } - sdkClient, ok := s.sdkClients[req.GetSdkId()] - if !ok { + sdkClient := s.sdkRegistrar.GetSdkOrNil(req.GetSdkId()) + if sdkClient == nil { return nil, status.Error(codes.InvalidArgument, "sdk not found for identifier: '"+req.GetSdkId()+"'") } if !sdkClient.IsInValidState() { @@ -149,8 +149,8 @@ func (s *flagService) Refresh(_ context.Context, req *proto.RefreshRequest) (*em return nil, status.Error(codes.InvalidArgument, "sdk id parameter missing") } - sdkClient, ok := s.sdkClients[req.GetSdkId()] - if !ok { + sdkClient := s.sdkRegistrar.GetSdkOrNil(req.GetSdkId()) + if sdkClient == nil { return nil, status.Error(codes.InvalidArgument, "sdk not found for identifier: '"+req.GetSdkId()+"'") } @@ -219,8 +219,8 @@ func (s *flagService) parseEvalRequest(req *proto.EvalRequest, user *model.UserA *user = getUserAttrs(req.GetUser()) } - sdkClient, ok := s.sdkClients[req.GetSdkId()] - if !ok { + sdkClient := s.sdkRegistrar.GetSdkOrNil(req.GetSdkId()) + if sdkClient == nil { return nil, status.Error(codes.InvalidArgument, "sdk not found for identifier: '"+req.GetSdkId()+"'") } if !sdkClient.IsInValidState() { diff --git a/grpc/flag_service_test.go b/grpc/flag_service_test.go index 842aa6a..e0d761a 100644 --- a/grpc/flag_service_test.go +++ b/grpc/flag_service_test.go @@ -7,7 +7,6 @@ import ( "github.com/configcat/configcat-proxy/internal/testutils" "github.com/configcat/configcat-proxy/internal/utils" "github.com/configcat/configcat-proxy/log" - "github.com/configcat/configcat-proxy/sdk" "github.com/configcat/go-sdk/v9/configcattest" "github.com/stretchr/testify/assert" "google.golang.org/grpc" @@ -30,10 +29,9 @@ func TestGrpc_EvalFlagStream(t *testing.T) { sdkSrv := httptest.NewServer(&h) defer sdkSrv.Close() - ctx := testutils.NewTestSdkContext(&config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key, PollInterval: 1}, nil) - sdkClient := sdk.NewClient(ctx, log.NewNullLogger()) - defer sdkClient.Close() - flagSrv := newFlagService(map[string]sdk.Client{"test": sdkClient}, nil, log.NewNullLogger()) + reg := testutils.NewTestRegistrar(&config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key, PollInterval: 1}, nil) + defer reg.Close() + flagSrv := newFlagService(reg, nil, log.NewNullLogger()) lis := bufconn.Listen(1024 * 1024) @@ -98,10 +96,9 @@ func TestGrpc_EvalAllFlagsStream(t *testing.T) { sdkSrv := httptest.NewServer(&h) defer sdkSrv.Close() - ctx := testutils.NewTestSdkContext(&config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key, PollInterval: 1}, nil) - sdkClient := sdk.NewClient(ctx, log.NewNullLogger()) - defer sdkClient.Close() - flagSrv := newFlagService(map[string]sdk.Client{"test": sdkClient}, nil, log.NewNullLogger()) + reg := testutils.NewTestRegistrar(&config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key, PollInterval: 1}, nil) + defer reg.Close() + flagSrv := newFlagService(reg, nil, log.NewNullLogger()) lis := bufconn.Listen(1024 * 1024) @@ -174,10 +171,9 @@ func TestGrpc_EvalFlag(t *testing.T) { sdkSrv := httptest.NewServer(&h) defer sdkSrv.Close() - ctx := testutils.NewTestSdkContext(&config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key, PollInterval: 1}, nil) - sdkClient := sdk.NewClient(ctx, log.NewNullLogger()) - defer sdkClient.Close() - flagSrv := newFlagService(map[string]sdk.Client{"test": sdkClient}, nil, log.NewNullLogger()) + reg := testutils.NewTestRegistrar(&config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key, PollInterval: 1}, nil) + defer reg.Close() + flagSrv := newFlagService(reg, nil, log.NewNullLogger()) lis := bufconn.Listen(1024 * 1024) @@ -226,10 +222,9 @@ func TestGrpc_EvalFlag(t *testing.T) { } func TestGrpc_SDK_InvalidState(t *testing.T) { - ctx := testutils.NewTestSdkContext(&config.SDKConfig{BaseUrl: "http://localhost", Key: configcattest.RandomSDKKey()}, nil) - sdkClient := sdk.NewClient(ctx, log.NewNullLogger()) - defer sdkClient.Close() - flagSrv := newFlagService(map[string]sdk.Client{"test": sdkClient}, nil, log.NewNullLogger()) + reg := testutils.NewTestRegistrar(&config.SDKConfig{BaseUrl: "http://localhost", Key: configcattest.RandomSDKKey()}, nil) + defer reg.Close() + flagSrv := newFlagService(reg, nil, log.NewNullLogger()) lis := bufconn.Listen(1024 * 1024) @@ -287,10 +282,9 @@ func TestGrpc_Invalid_SdkKey(t *testing.T) { }) sdkSrv := httptest.NewServer(&h) defer sdkSrv.Close() - ctx := testutils.NewTestSdkContext(&config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key}, nil) - sdkClient := sdk.NewClient(ctx, log.NewNullLogger()) - defer sdkClient.Close() - flagSrv := newFlagService(map[string]sdk.Client{"test": sdkClient}, nil, log.NewNullLogger()) + reg := testutils.NewTestRegistrar(&config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key}, nil) + defer reg.Close() + flagSrv := newFlagService(reg, nil, log.NewNullLogger()) lis := bufconn.Listen(1024 * 1024) srv := grpc.NewServer() defer srv.GracefulStop() @@ -348,10 +342,9 @@ func TestGrpc_Invalid_FlagKey(t *testing.T) { }) sdkSrv := httptest.NewServer(&h) defer sdkSrv.Close() - ctx := testutils.NewTestSdkContext(&config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key}, nil) - sdkClient := sdk.NewClient(ctx, log.NewNullLogger()) - defer sdkClient.Close() - flagSrv := newFlagService(map[string]sdk.Client{"test": sdkClient}, nil, log.NewNullLogger()) + reg := testutils.NewTestRegistrar(&config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key}, nil) + defer reg.Close() + flagSrv := newFlagService(reg, nil, log.NewNullLogger()) lis := bufconn.Listen(1024 * 1024) srv := grpc.NewServer() defer srv.GracefulStop() @@ -398,10 +391,9 @@ func TestGrpc_EvalAllFlags(t *testing.T) { sdkSrv := httptest.NewServer(&h) defer sdkSrv.Close() - ctx := testutils.NewTestSdkContext(&config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key, PollInterval: 1}, nil) - sdkClient := sdk.NewClient(ctx, log.NewNullLogger()) - defer sdkClient.Close() - flagSrv := newFlagService(map[string]sdk.Client{"test": sdkClient}, nil, log.NewNullLogger()) + reg := testutils.NewTestRegistrar(&config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key, PollInterval: 1}, nil) + defer reg.Close() + flagSrv := newFlagService(reg, nil, log.NewNullLogger()) lis := bufconn.Listen(1024 * 1024) @@ -471,10 +463,9 @@ func TestGrpc_GetKeys(t *testing.T) { sdkSrv := httptest.NewServer(&h) defer sdkSrv.Close() - ctx := testutils.NewTestSdkContext(&config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key, PollInterval: 1}, nil) - sdkClient := sdk.NewClient(ctx, log.NewNullLogger()) - defer sdkClient.Close() - flagSrv := newFlagService(map[string]sdk.Client{"test": sdkClient}, nil, log.NewNullLogger()) + reg := testutils.NewTestRegistrar(&config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key, PollInterval: 1}, nil) + defer reg.Close() + flagSrv := newFlagService(reg, nil, log.NewNullLogger()) lis := bufconn.Listen(1024 * 1024) diff --git a/grpc/mware.go b/grpc/mware.go index 0a69e90..f07e013 100644 --- a/grpc/mware.go +++ b/grpc/mware.go @@ -14,7 +14,7 @@ import ( func DebugLogUnaryInterceptor(log log.Logger) grpc.UnaryServerInterceptor { return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { - if isHealthCheck(info.FullMethod) { + if shouldIgnore(info.FullMethod) { return handler(ctx, req) } @@ -44,7 +44,7 @@ func DebugLogUnaryInterceptor(log log.Logger) grpc.UnaryServerInterceptor { func DebugLogStreamInterceptor(log log.Logger) grpc.StreamServerInterceptor { return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { - if isHealthCheck(info.FullMethod) { + if shouldIgnore(info.FullMethod) { return handler(srv, ss) } @@ -72,8 +72,8 @@ func DebugLogStreamInterceptor(log log.Logger) grpc.StreamServerInterceptor { } } -func isHealthCheck(method string) bool { - if strings.Contains(method, "grpc.health") { +func shouldIgnore(method string) bool { + if strings.Contains(method, "grpc.health") || strings.Contains(method, "grpc.reflection") { return true } return false diff --git a/grpc/mware_test.go b/grpc/mware_test.go index a92d616..b28bcb7 100644 --- a/grpc/mware_test.go +++ b/grpc/mware_test.go @@ -32,7 +32,7 @@ func TestDebug_UnaryInterceptor(t *testing.T) { outLog := out.String() assert.Contains(t, outLog, "[debug] rpc starting test-method [peer: 127.0.0.1/32]") - assert.Contains(t, outLog, "[debug] request finished test-method [peer: 127.0.0.1/32] [test-agent] [code: OK] [duration: 0ms]") + assert.Contains(t, outLog, "[debug] request finished test-method [peer: 127.0.0.1/32] [test-agent] [code: OK] [duration: ") } func TestDebug_StreamInterceptor(t *testing.T) { @@ -55,12 +55,13 @@ func TestDebug_StreamInterceptor(t *testing.T) { outLog := out.String() assert.Contains(t, outLog, "[debug] rpc starting test-method [peer: 127.0.0.1/32] [test-agent]") - assert.Contains(t, outLog, "[debug] request finished test-method [peer: 127.0.0.1/32] [test-agent] [code: OK] [duration: 0ms]") + assert.Contains(t, outLog, "[debug] request finished test-method [peer: 127.0.0.1/32] [test-agent] [code: OK] [duration: ") } -func TestIsHealthCheck(t *testing.T) { - assert.False(t, isHealthCheck("/configcat.FlagService/EvalFlag")) - assert.True(t, isHealthCheck("/grpc.health.v1.Health/Check")) +func TestIgnoreServiceNames(t *testing.T) { + assert.False(t, shouldIgnore("/configcat.FlagService/EvalFlag")) + assert.True(t, shouldIgnore("/grpc.health.v1.Health/Check")) + assert.True(t, shouldIgnore("/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo")) } type MockStreamServer struct { diff --git a/grpc/server.go b/grpc/server.go index 08dc2d8..b0f919b 100644 --- a/grpc/server.go +++ b/grpc/server.go @@ -1,7 +1,6 @@ package grpc import ( - "crypto/tls" "fmt" "github.com/configcat/configcat-proxy/config" "github.com/configcat/configcat-proxy/diag/metrics" @@ -33,20 +32,14 @@ type Server struct { errorChannel chan error } -func NewServer(sdkClients map[string]sdk.Client, metricsReporter metrics.Reporter, statusReporter status.Reporter, conf *config.Config, logger log.Logger, errorChan chan error) (*Server, error) { +func NewServer(sdkRegistrar sdk.Registrar, metricsReporter metrics.Reporter, statusReporter status.Reporter, conf *config.Config, logger log.Logger, errorChan chan error) (*Server, error) { grpcLog := logger.WithLevel(conf.Grpc.Log.GetLevel()).WithPrefix("grpc") opts := make([]grpc.ServerOption, 0) if conf.Tls.Enabled { - t := &tls.Config{ - MinVersion: conf.Tls.GetVersion(), - } - for _, c := range conf.Tls.Certificates { - if cert, err := tls.LoadX509KeyPair(c.Cert, c.Key); err == nil { - t.Certificates = append(t.Certificates, cert) - } else { - grpcLog.Errorf("failed to load the certificate and key pair: %s", err) - return nil, err - } + t, err := conf.Tls.LoadTlsOptions() + if err != nil { + grpcLog.Errorf("failed to configure TLS for the gRPC server: %s", err) + return nil, err } opts = append(opts, grpc.Creds(credentials.NewTLS(t))) grpcLog.Reportf("using TLS version: %.1f", conf.Tls.MinVersion) @@ -71,7 +64,7 @@ func NewServer(sdkClients map[string]sdk.Client, metricsReporter metrics.Reporte opts = append(opts, grpc.KeepaliveParams(params)) } - flagService := newFlagService(sdkClients, metricsReporter, grpcLog) + flagService := newFlagService(sdkRegistrar, metricsReporter, grpcLog) grpcServer := grpc.NewServer(opts...) proto.RegisterFlagServiceServer(grpcServer, flagService) diff --git a/grpc/server_test.go b/grpc/server_test.go index ad81d07..6863c65 100644 --- a/grpc/server_test.go +++ b/grpc/server_test.go @@ -7,7 +7,6 @@ import ( "github.com/configcat/configcat-proxy/internal/testutils" "github.com/configcat/configcat-proxy/internal/utils" "github.com/configcat/configcat-proxy/log" - "github.com/configcat/configcat-proxy/sdk" "github.com/configcat/go-sdk/v9/configcattest" "github.com/stretchr/testify/assert" "net/http/httptest" @@ -30,12 +29,12 @@ func TestNewServer(t *testing.T) { sdkSrv := httptest.NewServer(&h) defer sdkSrv.Close() - ctx := testutils.NewTestSdkContext(&config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key, PollInterval: 1}, nil) - sdkClient := sdk.NewClient(ctx, log.NewNullLogger()) - conf := config.Config{Grpc: config.GrpcConfig{Port: 5061, HealthCheckEnabled: true, ServerReflectionEnabled: true, KeepAlive: config.KeepAliveConfig{Timeout: 10}}, SDKs: map[string]*config.SDKConfig{key: ctx.SDKConf}} - defer sdkClient.Close() + sdkConf := config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key, PollInterval: 1} + reg := testutils.NewTestRegistrar(&sdkConf, nil) + conf := config.Config{Grpc: config.GrpcConfig{Port: 5061, HealthCheckEnabled: true, ServerReflectionEnabled: true, KeepAlive: config.KeepAliveConfig{Timeout: 10}}, SDKs: map[string]*config.SDKConfig{key: &sdkConf}} + defer reg.Close() - srv, _ := NewServer(map[string]sdk.Client{"test": sdkClient}, metrics.NewReporter(), status.NewReporter(&conf), &conf, log.NewDebugLogger(), errChan) + srv, _ := NewServer(reg, metrics.NewReporter(), status.NewReporter(&conf.Cache), &conf, log.NewDebugLogger(), errChan) wg := sync.WaitGroup{} wg.Add(1) @@ -116,12 +115,12 @@ MK4Li/LGWcksyoF+hbPNXMFCIA== sdkSrv := httptest.NewServer(&h) defer sdkSrv.Close() - ctx := testutils.NewTestSdkContext(&config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key, PollInterval: 1}, nil) - sdkClient := sdk.NewClient(ctx, log.NewNullLogger()) - conf := config.Config{Grpc: config.GrpcConfig{Port: 5062}, Tls: tlsConf, SDKs: map[string]*config.SDKConfig{key: ctx.SDKConf}} - defer sdkClient.Close() + sdkConf := config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key, PollInterval: 1} + reg := testutils.NewTestRegistrar(&sdkConf, nil) + conf := config.Config{Grpc: config.GrpcConfig{Port: 5062}, Tls: tlsConf, SDKs: map[string]*config.SDKConfig{key: &sdkConf}} + defer reg.Close() - srv, _ := NewServer(map[string]sdk.Client{"test": sdkClient}, nil, status.NewReporter(&conf), &conf, log.NewNullLogger(), errChan) + srv, _ := NewServer(reg, nil, status.NewReporter(&conf.Cache), &conf, log.NewNullLogger(), errChan) wg := sync.WaitGroup{} wg.Add(1) @@ -157,12 +156,12 @@ func TestNewServer_TLS_Missing_Cert(t *testing.T) { sdkSrv := httptest.NewServer(&h) defer sdkSrv.Close() - ctx := testutils.NewTestSdkContext(&config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key, PollInterval: 1}, nil) - sdkClient := sdk.NewClient(ctx, log.NewNullLogger()) - conf := config.Config{Grpc: config.GrpcConfig{Port: 5063}, Tls: tlsConf, SDKs: map[string]*config.SDKConfig{key: ctx.SDKConf}} - defer sdkClient.Close() + sdkConf := config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key, PollInterval: 1} + reg := testutils.NewTestRegistrar(&sdkConf, nil) + conf := config.Config{Grpc: config.GrpcConfig{Port: 5063}, Tls: tlsConf, SDKs: map[string]*config.SDKConfig{key: &sdkConf}} + defer reg.Close() - _, err := NewServer(map[string]sdk.Client{"test": sdkClient}, nil, status.NewReporter(&conf), &conf, log.NewNullLogger(), errChan) + _, err := NewServer(reg, nil, status.NewReporter(&conf.Cache), &conf, log.NewNullLogger(), errChan) assert.Error(t, err) } diff --git a/internal/testutils/utils.go b/internal/testutils/utils.go index f8a71e2..84d9792 100644 --- a/internal/testutils/utils.go +++ b/internal/testutils/utils.go @@ -14,7 +14,23 @@ import ( "testing" ) -func NewTestSdkClient(t *testing.T) (map[string]sdk.Client, *configcattest.Handler, string) { +func NewTestRegistrar(conf *config.SDKConfig, cache configcat.ConfigCache) sdk.Registrar { + return NewTestRegistrarWithStatusReporter(conf, cache, status.NewEmptyReporter()) +} + +func NewTestRegistrarWithStatusReporter(conf *config.SDKConfig, cache configcat.ConfigCache, reporter status.Reporter) sdk.Registrar { + ctx := NewTestSdkContext(conf, cache) + reg := sdk.NewRegistrar(&config.Config{ + SDKs: map[string]*config.SDKConfig{"test": conf}, + }, ctx.MetricsReporter, reporter, cache, log.NewNullLogger()) + return reg +} + +func NewTestRegistrarT(t *testing.T) (sdk.Registrar, *configcattest.Handler, string) { + return NewTestRegistrarTWithStatusReporter(t, status.NewEmptyReporter()) +} + +func NewTestRegistrarTWithStatusReporter(t *testing.T, reporter status.Reporter) (sdk.Registrar, *configcattest.Handler, string) { key := configcattest.RandomSDKKey() var h configcattest.Handler _ = h.SetFlags(key, map[string]*configcattest.Flag{ @@ -31,28 +47,38 @@ func NewTestSdkClient(t *testing.T) (map[string]sdk.Client, *configcattest.Handl }, }) srv := httptest.NewServer(&h) - opts := config.SDKConfig{BaseUrl: srv.URL, Key: key} - ctx := NewTestSdkContext(&opts, &config.CacheConfig{}) - client := sdk.NewClient(ctx, log.NewNullLogger()) + reg := NewTestRegistrarWithStatusReporter(&config.SDKConfig{BaseUrl: srv.URL, Key: key}, nil, reporter) t.Cleanup(func() { srv.Close() - client.Close() + reg.Close() }) - return map[string]sdk.Client{"test": client}, &h, key + return reg, &h, key } -func NewTestSdkContext(conf *config.SDKConfig, cacheConf *config.CacheConfig) *sdk.Context { - if cacheConf == nil { - cacheConf = &config.CacheConfig{} - } +func NewTestRegistrarTWithErrorServer(t *testing.T) sdk.Registrar { + key := configcattest.RandomSDKKey() + var h configcattest.Handler + srv := httptest.NewServer(&h) + reg := NewTestRegistrarWithStatusReporter(&config.SDKConfig{BaseUrl: srv.URL, Key: key}, nil, status.NewEmptyReporter()) + t.Cleanup(func() { + srv.Close() + reg.Close() + }) + return reg +} + +func NewTestSdkClient(t *testing.T) (map[string]sdk.Client, *configcattest.Handler, string) { + reg, h, k := NewTestRegistrarT(t) + return reg.GetAll(), h, k +} + +func NewTestSdkContext(conf *config.SDKConfig, cache configcat.ConfigCache) *sdk.Context { return &sdk.Context{ - SDKConf: conf, - ProxyConf: &config.HttpProxyConfig{}, - CacheConf: cacheConf, - StatusReporter: status.NewNullReporter(), - MetricsReporter: nil, - EvalReporter: nil, - SdkId: "test", + SDKConf: conf, + ProxyConf: &config.HttpProxyConfig{}, + StatusReporter: status.NewEmptyReporter(), + SdkId: "test", + ExternalCache: cache, } } diff --git a/main.go b/main.go index a8cd892..09c5466 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "flag" "github.com/configcat/configcat-proxy/config" "github.com/configcat/configcat-proxy/diag" @@ -9,11 +10,13 @@ import ( "github.com/configcat/configcat-proxy/grpc" "github.com/configcat/configcat-proxy/log" "github.com/configcat/configcat-proxy/sdk" + "github.com/configcat/configcat-proxy/sdk/store/cache" "github.com/configcat/configcat-proxy/web" "os" "os/signal" "sync" "syscall" + "time" ) const ( @@ -53,7 +56,7 @@ func run(closeSignal chan os.Signal) int { // in the future we might implement an evaluation statistics reporter // var evalReporter statistics.Reporter - statusReporter := status.NewReporter(&conf) + statusReporter := status.NewReporter(&conf.Cache) var metricsReporter metrics.Reporter if conf.Diag.Metrics.Enabled { @@ -66,24 +69,23 @@ func run(closeSignal chan os.Signal) int { diagServer.Listen() } - sdkClients := make(map[string]sdk.Client) - for key, sdkConf := range conf.SDKs { - sdkClients[key] = sdk.NewClient(&sdk.Context{ - SDKConf: sdkConf, - EvalReporter: nil, - MetricsReporter: metricsReporter, - StatusReporter: statusReporter, - ProxyConf: &conf.HttpProxy, - CacheConf: &conf.Cache, - GlobalDefaultAttrs: conf.DefaultAttrs, - SdkId: key, - }, logger) + var externalCache cache.External + if conf.Cache.IsSet() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) // give 5 sec to spin up the cache connection + defer cancel() + + externalCache, err = cache.SetupExternalCache(ctx, &conf.Cache, logger) + if err != nil { + return exitFailure + } } + sdkRegistrar := sdk.NewRegistrar(&conf, metricsReporter, statusReporter, externalCache, logger) + var httpServer *web.Server var router *web.HttpRouter if conf.Http.Enabled { - router = web.NewRouter(sdkClients, metricsReporter, statusReporter, &conf.Http, logger) + router = web.NewRouter(sdkRegistrar, metricsReporter, statusReporter, &conf.Http, logger) httpServer, err = web.NewServer(router.Handler(), logger, &conf, errorChan) if err != nil { return exitFailure @@ -93,7 +95,7 @@ func run(closeSignal chan os.Signal) int { var grpcServer *grpc.Server if conf.Grpc.Enabled { - grpcServer, err = grpc.NewServer(sdkClients, metricsReporter, statusReporter, &conf, logger, errorChan) + grpcServer, err = grpc.NewServer(sdkRegistrar, metricsReporter, statusReporter, &conf, logger, errorChan) if err != nil { return exitFailure } @@ -103,14 +105,16 @@ func run(closeSignal chan os.Signal) int { for { select { case <-closeSignal: - for _, sdkClient := range sdkClients { - sdkClient.Close() - } + sdkRegistrar.Close() + if router != nil { router.Close() } shutDownCount := 0 + if externalCache != nil { + shutDownCount++ + } if httpServer != nil { shutDownCount++ } @@ -122,6 +126,12 @@ func run(closeSignal chan os.Signal) int { } wg := sync.WaitGroup{} wg.Add(shutDownCount) + if externalCache != nil { + go func() { + externalCache.Shutdown() + wg.Done() + }() + } if httpServer != nil { go func() { httpServer.Shutdown() diff --git a/main_test.go b/main_test.go index 0efd6a5..352e733 100644 --- a/main_test.go +++ b/main_test.go @@ -2,6 +2,7 @@ package main import ( "flag" + "github.com/alicebob/miniredis/v2" "github.com/stretchr/testify/assert" "io" "os" @@ -13,6 +14,7 @@ import ( func TestAppMain(t *testing.T) { resetFlags() + t.Setenv("CONFIGCAT_SDK1_BASE_URL", "https://test-cdn-global.configcat.com") t.Setenv("CONFIGCAT_SDKS", `{"sdk1":"XxPbCKmzIUGORk4vsufpzw/iC_KABprDEueeQs3yovVnQ"}`) t.Setenv("CONFIGCAT_HTTP_PORT", "5081") t.Setenv("CONFIGCAT_GRPC_PORT", "5082") @@ -35,6 +37,7 @@ func TestAppMain(t *testing.T) { func TestAppMain_Disabled_Everything(t *testing.T) { resetFlags() + t.Setenv("CONFIGCAT_SDK1_BASE_URL", "https://test-cdn-global.configcat.com") t.Setenv("CONFIGCAT_SDKS", `{"sdk1":"XxPbCKmzIUGORk4vsufpzw/iC_KABprDEueeQs3yovVnQ"}`) t.Setenv("CONFIGCAT_HTTP_ENABLED", "false") t.Setenv("CONFIGCAT_GRPC_ENABLED", "false") @@ -57,6 +60,7 @@ func TestAppMain_Disabled_Everything(t *testing.T) { func TestAppMain_GRPCOnly(t *testing.T) { resetFlags() + t.Setenv("CONFIGCAT_SDK1_BASE_URL", "https://test-cdn-global.configcat.com") t.Setenv("CONFIGCAT_SDKS", `{"sdk1":"XxPbCKmzIUGORk4vsufpzw/iC_KABprDEueeQs3yovVnQ"}`) t.Setenv("CONFIGCAT_GRPC_PORT", "5092") t.Setenv("CONFIGCAT_HTTP_ENABLED", "false") @@ -77,6 +81,32 @@ func TestAppMain_GRPCOnly(t *testing.T) { assert.Equal(t, 0, exitCode) } +func TestAppMain_Cache(t *testing.T) { + resetFlags() + s := miniredis.RunT(t) + t.Setenv("CONFIGCAT_SDK1_BASE_URL", "https://test-cdn-global.configcat.com") + t.Setenv("CONFIGCAT_SDKS", `{"sdk1":"XxPbCKmzIUGORk4vsufpzw/iC_KABprDEueeQs3yovVnQ"}`) + t.Setenv("CONFIGCAT_HTTP_PORT", "5101") + t.Setenv("CONFIGCAT_GRPC_PORT", "5102") + t.Setenv("CONFIGCAT_DIAG_PORT", "5103") + t.Setenv("CONFIGCAT_CACHE_REDIS_ENABLED", "true") + t.Setenv("CONFIGCAT_CACHE_REDIS_ADDRESSES", "[\""+s.Addr()+"\"]") + + var exitCode int + closeSignal := make(chan os.Signal, 1) + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + exitCode = run(closeSignal) + wg.Done() + }() + time.Sleep(2 * time.Second) + closeSignal <- syscall.SIGTERM + wg.Wait() + + assert.Equal(t, 0, exitCode) +} + func TestAppMain_Invalid_Conf(t *testing.T) { resetFlags() var exitCode int diff --git a/model/eval.go b/model/eval.go index e075a4a..be818e5 100644 --- a/model/eval.go +++ b/model/eval.go @@ -7,6 +7,7 @@ import ( type EvalData struct { Value interface{} VariationId string + Error error User configcat.User } diff --git a/model/usr_attrs_test.go b/model/user_attrs_test.go similarity index 100% rename from model/usr_attrs_test.go rename to model/user_attrs_test.go diff --git a/sdk/sdk.go b/sdk/sdk.go index 20ea17d..25613be 100644 --- a/sdk/sdk.go +++ b/sdk/sdk.go @@ -10,7 +10,6 @@ import ( "github.com/configcat/configcat-proxy/sdk/statistics" "github.com/configcat/configcat-proxy/sdk/store" "github.com/configcat/configcat-proxy/sdk/store/cache" - "github.com/configcat/configcat-proxy/sdk/store/cache/redis" "github.com/configcat/configcat-proxy/sdk/store/file" "github.com/configcat/go-sdk/v9" "github.com/configcat/go-sdk/v9/configcatcache" @@ -26,7 +25,7 @@ const ( ) type Client interface { - Eval(key string, user model.UserAttrs) (model.EvalData, error) + Eval(key string, user model.UserAttrs) model.EvalData EvalAll(user model.UserAttrs) map[string]model.EvalData Keys() []string GetCachedJson() *store.EntryWithEtag @@ -44,11 +43,11 @@ type Context struct { SdkId string SDKConf *config.SDKConfig ProxyConf *config.HttpProxyConfig - CacheConf *config.CacheConfig GlobalDefaultAttrs model.UserAttrs MetricsReporter metrics.Reporter StatusReporter status.Reporter EvalReporter statistics.Reporter + ExternalCache configcat.ConfigCache } type client struct { @@ -67,18 +66,20 @@ type client struct { func NewClient(sdkCtx *Context, log log.Logger) Client { sdkLog := log.WithLevel(sdkCtx.SDKConf.Log.GetLevel()).WithPrefix("sdk-" + sdkCtx.SdkId) + sdkCtx.StatusReporter.RegisterSdk(sdkCtx.SdkId, sdkCtx.SDKConf) + offline := sdkCtx.SDKConf.Offline.Enabled key := sdkCtx.SDKConf.Key var storage configcat.ConfigCache if offline && sdkCtx.SDKConf.Offline.Local.FilePath != "" { key = validEmptySdkKey storage = file.NewFileStore(sdkCtx.SdkId, &sdkCtx.SDKConf.Offline.Local, sdkCtx.StatusReporter, log.WithLevel(sdkCtx.SDKConf.Offline.Log.GetLevel())) - } else if offline && sdkCtx.SDKConf.Offline.UseCache && sdkCtx.CacheConf.Redis.Enabled { - cacheKey := configcatcache.ProduceCacheKey(key, configcatcache.ConfigJSONName, configcatcache.ConfigJSONCacheVersion) - cacheStore := cache.NewCacheStore(redis.NewRedisStore(&sdkCtx.CacheConf.Redis), sdkCtx.StatusReporter) + } else if offline && sdkCtx.SDKConf.Offline.UseCache && sdkCtx.ExternalCache != nil { + cacheKey := configcatcache.ProduceCacheKey(sdkCtx.SDKConf.Key, configcatcache.ConfigJSONName, configcatcache.ConfigJSONCacheVersion) + cacheStore := cache.NewCacheStore(sdkCtx.ExternalCache, sdkCtx.StatusReporter) storage = cache.NewNotifyingCacheStore(sdkCtx.SdkId, cacheKey, cacheStore, &sdkCtx.SDKConf.Offline, sdkCtx.StatusReporter, log.WithLevel(sdkCtx.SDKConf.Offline.Log.GetLevel())) - } else if !offline && sdkCtx.CacheConf.Redis.Enabled { - storage = cache.NewCacheStore(redis.NewRedisStore(&sdkCtx.CacheConf.Redis), sdkCtx.StatusReporter) + } else if !offline && sdkCtx.ExternalCache != nil { + storage = cache.NewCacheStore(sdkCtx.ExternalCache, sdkCtx.StatusReporter) } else { storage = store.NewInMemoryStorage() } @@ -184,10 +185,10 @@ func (c *client) signal() { } } -func (c *client) Eval(key string, user model.UserAttrs) (model.EvalData, error) { +func (c *client) Eval(key string, user model.UserAttrs) model.EvalData { mergedUser := model.MergeUserAttrs(c.defaultAttrs, user) details := c.configCatClient.Snapshot(mergedUser).GetValueDetails(key) - return model.EvalData{Value: details.Value, VariationId: details.Data.VariationID, User: details.Data.User}, details.Data.Error + return model.EvalData{Value: details.Value, VariationId: details.Data.VariationID, User: details.Data.User, Error: details.Data.Error} } func (c *client) EvalAll(user model.UserAttrs) map[string]model.EvalData { @@ -195,7 +196,7 @@ func (c *client) EvalAll(user model.UserAttrs) map[string]model.EvalData { allDetails := c.configCatClient.Snapshot(mergedUser).GetAllValueDetails() result := make(map[string]model.EvalData, len(allDetails)) for _, details := range allDetails { - result[details.Data.Key] = model.EvalData{Value: details.Value, VariationId: details.Data.VariationID, User: details.Data.User} + result[details.Data.Key] = model.EvalData{Value: details.Value, VariationId: details.Data.VariationID, User: details.Data.User, Error: details.Data.Error} } return result } @@ -254,10 +255,10 @@ func (c *client) IsInValidState() bool { } func (c *client) Close() { - c.ctxCancel() - if closable, ok := c.cache.(store.ClosableStore); ok { - closable.Close() + if notifier, ok := c.cache.(store.Notifier); ok { + notifier.Close() } + c.ctxCancel() c.configCatClient.Close() c.log.Reportf("shutdown complete") } diff --git a/sdk/sdk_registrar.go b/sdk/sdk_registrar.go new file mode 100644 index 0000000..c2272b1 --- /dev/null +++ b/sdk/sdk_registrar.go @@ -0,0 +1,49 @@ +package sdk + +import ( + "github.com/configcat/configcat-proxy/config" + "github.com/configcat/configcat-proxy/diag/metrics" + "github.com/configcat/configcat-proxy/diag/status" + "github.com/configcat/configcat-proxy/log" + configcat "github.com/configcat/go-sdk/v9" +) + +type Registrar interface { + GetSdkOrNil(sdkId string) Client + GetAll() map[string]Client + Close() +} + +type registrar struct { + sdkClients map[string]Client +} + +func NewRegistrar(conf *config.Config, metricsReporter metrics.Reporter, statusReporter status.Reporter, externalCache configcat.ConfigCache, log log.Logger) Registrar { + sdkClients := make(map[string]Client, len(conf.SDKs)) + for key, sdkConf := range conf.SDKs { + sdkClients[key] = NewClient(&Context{ + SDKConf: sdkConf, + MetricsReporter: metricsReporter, + StatusReporter: statusReporter, + ProxyConf: &conf.HttpProxy, + GlobalDefaultAttrs: conf.DefaultAttrs, + SdkId: key, + ExternalCache: externalCache, + }, log) + } + return ®istrar{sdkClients: sdkClients} +} + +func (r *registrar) GetSdkOrNil(sdkId string) Client { + return r.sdkClients[sdkId] +} + +func (r *registrar) GetAll() map[string]Client { + return r.sdkClients +} + +func (r *registrar) Close() { + for _, sdkClient := range r.sdkClients { + sdkClient.Close() + } +} diff --git a/sdk/sdk_registrar_test.go b/sdk/sdk_registrar_test.go new file mode 100644 index 0000000..934904f --- /dev/null +++ b/sdk/sdk_registrar_test.go @@ -0,0 +1,41 @@ +package sdk + +import ( + "github.com/configcat/configcat-proxy/config" + "github.com/configcat/configcat-proxy/diag/status" + "github.com/configcat/configcat-proxy/internal/utils" + "github.com/configcat/configcat-proxy/log" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestRegistrar_GetSdkOrNil(t *testing.T) { + reg := NewRegistrar(&config.Config{ + SDKs: map[string]*config.SDKConfig{"test": {Key: "key"}}, + }, nil, status.NewEmptyReporter(), nil, log.NewNullLogger()) + defer reg.Close() + + assert.NotNil(t, reg.GetSdkOrNil("test")) +} + +func TestRegistrar_All(t *testing.T) { + reg := NewRegistrar(&config.Config{ + SDKs: map[string]*config.SDKConfig{"test1": {Key: "key1"}, "test2": {Key: "key2"}}, + }, nil, status.NewEmptyReporter(), nil, log.NewNullLogger()) + defer reg.Close() + + assert.Equal(t, 2, len(reg.GetAll())) +} + +func TestClient_Close(t *testing.T) { + reg := NewRegistrar(&config.Config{ + SDKs: map[string]*config.SDKConfig{"test": {Key: "key"}}, + }, nil, status.NewEmptyReporter(), nil, log.NewNullLogger()) + + c := reg.GetSdkOrNil("test").(*client) + reg.Close() + utils.WithTimeout(1*time.Second, func() { + <-c.ctx.Done() + }) +} diff --git a/sdk/sdk_test.go b/sdk/sdk_test.go index 564db85..c3b3c4f 100644 --- a/sdk/sdk_test.go +++ b/sdk/sdk_test.go @@ -1,6 +1,7 @@ package sdk import ( + "context" "crypto/sha1" "fmt" "github.com/alicebob/miniredis/v2" @@ -10,6 +11,8 @@ import ( "github.com/configcat/configcat-proxy/log" "github.com/configcat/configcat-proxy/model" "github.com/configcat/configcat-proxy/sdk/statistics" + "github.com/configcat/configcat-proxy/sdk/store/cache" + configcat "github.com/configcat/go-sdk/v9" "github.com/configcat/go-sdk/v9/configcatcache" "github.com/configcat/go-sdk/v9/configcattest" "github.com/stretchr/testify/assert" @@ -33,9 +36,9 @@ func TestSdk_Signal(t *testing.T) { client := NewClient(ctx, log.NewNullLogger()) defer client.Close() sub := client.SubConfigChanged("id") - data, err := client.Eval("flag", nil) + data := client.Eval("flag", nil) j := client.GetCachedJson() - assert.NoError(t, err) + assert.NoError(t, data.Error) assert.True(t, data.Value.(bool)) assert.Equal(t, `{"f":{"flag":{"a":"","i":"v_flag","v":{"b":true,"s":null,"i":null,"d":null},"t":0,"r":[],"p":null}},"s":null,"p":null}`, string(j.ConfigJson)) assert.Equal(t, fmt.Sprintf("%x", sha1.Sum(j.ConfigJson)), j.ETag) @@ -48,9 +51,9 @@ func TestSdk_Signal(t *testing.T) { utils.WithTimeout(2*time.Second, func() { <-sub }) - data, err = client.Eval("flag", nil) + data = client.Eval("flag", nil) j = client.GetCachedJson() - assert.NoError(t, err) + assert.NoError(t, data.Error) assert.False(t, data.Value.(bool)) assert.Equal(t, `{"f":{"flag":{"a":"","i":"v_flag","v":{"b":false,"s":null,"i":null,"d":null},"t":0,"r":[],"p":null}},"s":null,"p":null}`, string(j.ConfigJson)) assert.Equal(t, fmt.Sprintf("%x", sha1.Sum(j.ConfigJson)), j.ETag) @@ -107,9 +110,9 @@ func TestSdk_Signal_Refresh(t *testing.T) { client := NewClient(ctx, log.NewNullLogger()) defer client.Close() sub := client.SubConfigChanged("id") - data, err := client.Eval("flag", nil) + data := client.Eval("flag", nil) j := client.GetCachedJson() - assert.NoError(t, err) + assert.NoError(t, data.Error) assert.True(t, data.Value.(bool)) assert.Equal(t, `{"f":{"flag":{"a":"","i":"v_flag","v":{"b":true,"s":null,"i":null,"d":null},"t":0,"r":[],"p":null}},"s":null,"p":null}`, string(j.ConfigJson)) assert.Equal(t, fmt.Sprintf("%x", sha1.Sum(j.ConfigJson)), j.ETag) @@ -123,10 +126,11 @@ func TestSdk_Signal_Refresh(t *testing.T) { utils.WithTimeout(2*time.Second, func() { <-sub }) - data, err = client.Eval("flag", nil) + data = client.Eval("flag", nil) j = client.GetCachedJson() - assert.NoError(t, err) + assert.NoError(t, data.Error) assert.False(t, data.Value.(bool)) + assert.Nil(t, data.Error) assert.Equal(t, `{"f":{"flag":{"a":"","i":"v_flag","v":{"b":false,"s":null,"i":null,"d":null},"t":0,"r":[],"p":null}},"s":null,"p":null}`, string(j.ConfigJson)) assert.Equal(t, fmt.Sprintf("%x", sha1.Sum(j.ConfigJson)), j.ETag) } @@ -140,10 +144,11 @@ func TestSdk_BadConfig(t *testing.T) { ctx := newTestSdkContext(&config.SDKConfig{BaseUrl: srv.URL, Key: key, Log: config.LogConfig{Level: "debug"}}, nil) client := NewClient(ctx, log.NewDebugLogger()) defer client.Close() - data, err := client.Eval("flag", nil) + data := client.Eval("flag", nil) j := client.GetCachedJson() - assert.Error(t, err) + assert.Error(t, data.Error) assert.Nil(t, data.Value) + assert.NotNil(t, data.Error) assert.Equal(t, `{"f":null,"s":null,"p":null}`, string(j.ConfigJson)) assert.Equal(t, utils.GenerateEtag(j.ConfigJson), j.ETag) } @@ -158,13 +163,14 @@ func TestSdk_BadConfig_WithCache(t *testing.T) { cacheKey := configcatcache.ProduceCacheKey(key, configcatcache.ConfigJSONName, configcatcache.ConfigJSONCacheVersion) cacheEntry := configcatcache.CacheSegmentsToBytes(time.Now(), "etag", []byte(`{"f":{"flag":{"a":"","i":"v_flag","v":{"b":true},"t":0}}}`)) err := s.Set(cacheKey, string(cacheEntry)) + assert.NoError(t, err) - ctx := newTestSdkContext(&config.SDKConfig{BaseUrl: srv.URL, Key: key, Log: config.LogConfig{Level: "debug"}}, &config.CacheConfig{Redis: config.RedisConfig{Enabled: true, Addresses: []string{s.Addr()}}}) + ctx := newTestSdkContext(&config.SDKConfig{BaseUrl: srv.URL, Key: key, Log: config.LogConfig{Level: "debug"}}, newRedisCache(s.Addr())) client := NewClient(ctx, log.NewDebugLogger()) defer client.Close() - data, err := client.Eval("flag", nil) + data := client.Eval("flag", nil) j := client.GetCachedJson() - assert.NoError(t, err) + assert.NoError(t, data.Error) assert.True(t, data.Value.(bool)) assert.Equal(t, `{"f":{"flag":{"a":"","i":"v_flag","v":{"b":true},"t":0}}}`, string(j.ConfigJson)) assert.Equal(t, "etag", j.ETag) @@ -176,9 +182,9 @@ func TestSdk_Signal_Offline_File_Watch(t *testing.T) { client := NewClient(ctx, log.NewNullLogger()) defer client.Close() sub := client.SubConfigChanged("id") - data, err := client.Eval("flag", nil) + data := client.Eval("flag", nil) j := client.GetCachedJson() - assert.NoError(t, err) + assert.NoError(t, data.Error) assert.True(t, data.Value.(bool)) assert.Equal(t, `{"f":{"flag":{"a":"","i":"v_flag","v":{"b":true,"s":null,"i":null,"d":null},"t":0,"r":null,"p":null}},"s":null,"p":null}`, string(j.ConfigJson)) assert.Equal(t, fmt.Sprintf("W/\"%s\"", utils.FastHashHex(j.ConfigJson)), j.ETag) @@ -187,9 +193,9 @@ func TestSdk_Signal_Offline_File_Watch(t *testing.T) { utils.WithTimeout(2*time.Second, func() { <-sub }) - data, err = client.Eval("flag", nil) + data = client.Eval("flag", nil) j = client.GetCachedJson() - assert.NoError(t, err) + assert.NoError(t, data.Error) assert.False(t, data.Value.(bool)) assert.Equal(t, `{"f":{"flag":{"a":"","i":"v_flag","v":{"b":false,"s":null,"i":null,"d":null},"t":0,"r":null,"p":null}},"s":null,"p":null}`, string(j.ConfigJson)) assert.Equal(t, utils.GenerateEtag(j.ConfigJson), j.ETag) @@ -202,9 +208,9 @@ func TestSdk_Signal_Offline_Poll_Watch(t *testing.T) { client := NewClient(ctx, log.NewNullLogger()) defer client.Close() sub := client.SubConfigChanged("id") - data, err := client.Eval("flag", nil) + data := client.Eval("flag", nil) j := client.GetCachedJson() - assert.NoError(t, err) + assert.NoError(t, data.Error) assert.True(t, data.Value.(bool)) assert.Equal(t, `{"f":{"flag":{"a":"","i":"v_flag","v":{"b":true,"s":null,"i":null,"d":null},"t":0,"r":null,"p":null}},"s":null,"p":null}`, string(j.ConfigJson)) assert.Equal(t, fmt.Sprintf("W/\"%s\"", utils.FastHashHex(j.ConfigJson)), j.ETag) @@ -213,9 +219,9 @@ func TestSdk_Signal_Offline_Poll_Watch(t *testing.T) { utils.WithTimeout(2*time.Second, func() { <-sub }) - data, err = client.Eval("flag", nil) + data = client.Eval("flag", nil) j = client.GetCachedJson() - assert.NoError(t, err) + assert.NoError(t, data.Error) assert.False(t, data.Value.(bool)) assert.Equal(t, `{"f":{"flag":{"a":"","i":"v_flag","v":{"b":false,"s":null,"i":null,"d":null},"t":0,"r":null,"p":null}},"s":null,"p":null}`, string(j.ConfigJson)) assert.Equal(t, utils.GenerateEtag(j.ConfigJson), j.ETag) @@ -232,13 +238,13 @@ func TestSdk_Signal_Offline_Redis_Watch(t *testing.T) { ctx := newTestSdkContext(&config.SDKConfig{ Key: sdkKey, Offline: config.OfflineConfig{Enabled: true, UseCache: true, CachePollInterval: 1}, - }, &config.CacheConfig{Redis: config.RedisConfig{Enabled: true, Addresses: []string{s.Addr()}}}) + }, newRedisCache(s.Addr())) client := NewClient(ctx, log.NewNullLogger()) defer client.Close() sub := client.SubConfigChanged("id") - data, err := client.Eval("flag", nil) + data := client.Eval("flag", nil) j := client.GetCachedJson() - assert.NoError(t, err) + assert.NoError(t, data.Error) assert.True(t, data.Value.(bool)) assert.Equal(t, `{"f":{"flag":{"a":"","i":"v_flag","v":{"b":true,"s":null,"i":null,"d":null},"t":0,"r":null,"p":null}},"s":null,"p":null}`, string(j.ConfigJson)) assert.Equal(t, "etag", j.ETag) @@ -248,9 +254,9 @@ func TestSdk_Signal_Offline_Redis_Watch(t *testing.T) { utils.WithTimeout(2*time.Second, func() { <-sub }) - data, err = client.Eval("flag", nil) + data = client.Eval("flag", nil) j = client.GetCachedJson() - assert.NoError(t, err) + assert.NoError(t, data.Error) assert.False(t, data.Value.(bool)) assert.Equal(t, `{"f":{"flag":{"a":"","i":"v_flag","v":{"b":false,"s":null,"i":null,"d":null},"t":0,"r":null,"p":null}},"s":null,"p":null}`, string(j.ConfigJson)) assert.Equal(t, "etag2", j.ETag) @@ -298,6 +304,8 @@ func TestSdk_EvalAll(t *testing.T) { assert.Equal(t, 2, len(details)) assert.Equal(t, "v1", details["flag1"].Value) assert.Equal(t, "v2", details["flag2"].Value) + assert.Nil(t, details["flag1"].Error) + assert.Nil(t, details["flag2"].Error) } func TestSdk_Keys(t *testing.T) { @@ -341,7 +349,7 @@ func TestSdk_EvalStatsReporter(t *testing.T) { client := NewClient(ctx, log.NewNullLogger()) defer client.Close() - _, _ = client.Eval("flag1", model.UserAttrs{"e": "h"}) + _ = client.Eval("flag1", model.UserAttrs{"e": "h"}) var event *statistics.EvalEvent utils.WithTimeout(2*time.Second, func() { @@ -366,7 +374,7 @@ func TestSdk_DefaultAttrs(t *testing.T) { client := NewClient(ctx, log.NewNullLogger()) defer client.Close() - evalData, _ := client.Eval("flag1", model.UserAttrs{"e": "h"}) + evalData := client.Eval("flag1", model.UserAttrs{"e": "h"}) assert.Equal(t, model.UserAttrs{"a": "g", "c": "d", "e": "h"}, evalData.User.(model.UserAttrs)) } @@ -417,28 +425,43 @@ func TestSdk_IsInValidState_False(t *testing.T) { func TestSdk_IsInValidState_EmptyCache_False(t *testing.T) { r := miniredis.RunT(t) - ctx := newTestSdkContext(&config.SDKConfig{BaseUrl: "https://localhost", Key: configcattest.RandomSDKKey()}, &config.CacheConfig{Redis: config.RedisConfig{Enabled: true, Addresses: []string{r.Addr()}}}) + ctx := newTestSdkContext(&config.SDKConfig{BaseUrl: "https://localhost", Key: configcattest.RandomSDKKey()}, newRedisCache(r.Addr())) client := NewClient(ctx, log.NewDebugLogger()) defer client.Close() assert.False(t, client.IsInValidState()) } -func newTestSdkContext(conf *config.SDKConfig, cacheConf *config.CacheConfig) *Context { - if cacheConf == nil { - cacheConf = &config.CacheConfig{} - } +func TestSdk_StatusReporter(t *testing.T) { + reporter := status.NewEmptyReporter() + ctx := newTestSdkContextWithReporter(&config.SDKConfig{BaseUrl: "https://localhost", Key: configcattest.RandomSDKKey()}, nil, reporter) + client := NewClient(ctx, log.NewDebugLogger()) + defer client.Close() + + assert.NotEmpty(t, reporter.GetStatus().SDKs) +} + +func newTestSdkContext(conf *config.SDKConfig, externalCache configcat.ConfigCache) *Context { + return newTestSdkContextWithReporter(conf, externalCache, status.NewEmptyReporter()) +} + +func newTestSdkContextWithReporter(conf *config.SDKConfig, externalCache configcat.ConfigCache, reporter status.Reporter) *Context { return &Context{ SDKConf: conf, ProxyConf: &config.HttpProxyConfig{}, - CacheConf: cacheConf, - StatusReporter: status.NewNullReporter(), + StatusReporter: reporter, MetricsReporter: nil, EvalReporter: nil, SdkId: "test", + ExternalCache: externalCache, } } +func newRedisCache(addr string) configcat.ConfigCache { + c, _ := cache.SetupExternalCache(context.Background(), &config.CacheConfig{Redis: config.RedisConfig{Enabled: true, Addresses: []string{addr}}}, log.NewNullLogger()) + return c +} + type TestReporter struct { events chan *statistics.EvalEvent } diff --git a/sdk/store/cache/cache.go b/sdk/store/cache/cache.go index eee6c43..9de71c8 100644 --- a/sdk/store/cache/cache.go +++ b/sdk/store/cache/cache.go @@ -2,12 +2,24 @@ package cache import ( "context" + "github.com/configcat/configcat-proxy/config" "github.com/configcat/configcat-proxy/diag/status" + "github.com/configcat/configcat-proxy/log" "github.com/configcat/configcat-proxy/sdk/store" configcat "github.com/configcat/go-sdk/v9" "github.com/configcat/go-sdk/v9/configcatcache" ) +const ( + keyName = "key" + payloadName = "payload" +) + +type External interface { + configcat.ConfigCache + Shutdown() +} + type cacheStore struct { store.EntryStore @@ -15,6 +27,30 @@ type cacheStore struct { actualCache configcat.ConfigCache } +func SetupExternalCache(ctx context.Context, conf *config.CacheConfig, log log.Logger) (External, error) { + cacheLog := log.WithPrefix("cache") + if conf.Redis.Enabled { + redis, err := newRedis(&conf.Redis, cacheLog) + if err != nil { + return nil, err + } + return redis, nil + } else if conf.MongoDb.Enabled { + mongoDb, err := newMongoDb(ctx, &conf.MongoDb, cacheLog) + if err != nil { + return nil, err + } + return mongoDb, nil + } else if conf.DynamoDb.Enabled { + dynamoDb, err := newDynamoDb(ctx, &conf.DynamoDb, cacheLog) + if err != nil { + return nil, err + } + return dynamoDb, nil + } + return nil, nil +} + func NewCacheStore(actualCache configcat.ConfigCache, reporter status.Reporter) store.CacheEntryStore { return &cacheStore{ EntryStore: store.NewEntryStore(), @@ -48,9 +84,3 @@ func (c *cacheStore) Set(ctx context.Context, key string, value []byte) error { c.reporter.ReportOk(status.Cache, "cache write succeeded") return nil } - -func (c *cacheStore) Close() { - if closable, ok := c.actualCache.(store.ClosableStore); ok { - closable.Close() - } -} diff --git a/sdk/store/cache/cache_notify.go b/sdk/store/cache/cache_notify.go index 73059b4..a6e88c3 100644 --- a/sdk/store/cache/cache_notify.go +++ b/sdk/store/cache/cache_notify.go @@ -25,9 +25,7 @@ type notifyingCacheStore struct { cacheKey string } -var _ store.NotifyingStore = ¬ifyingCacheStore{} - -func NewNotifyingCacheStore(sdkId string, cacheKey string, cache store.CacheEntryStore, conf *config.OfflineConfig, reporter status.Reporter, log log.Logger) configcat.ConfigCache { +func NewNotifyingCacheStore(sdkId string, cacheKey string, cache store.CacheEntryStore, conf *config.OfflineConfig, reporter status.Reporter, log log.Logger) store.NotifyingStore { nrLogger := log.WithPrefix("cache-poll") n := ¬ifyingCacheStore{ CacheEntryStore: cache, @@ -60,8 +58,8 @@ func (n *notifyingCacheStore) run() { func (n *notifyingCacheStore) reload() bool { data, err := n.CacheEntryStore.Get(n.ctx, n.cacheKey) if err != nil { - n.log.Errorf("failed to read from redis: %s", err) - n.reporter.ReportError(n.sdkId, "failed to read from redis") + n.log.Errorf("failed to read from cache: %s", err) + n.reporter.ReportError(n.sdkId, "failed to read from cache") return false } fetchTime, eTag, configJson, err := configcatcache.CacheSegmentsFromBytes(data) @@ -74,12 +72,12 @@ func (n *notifyingCacheStore) reload() bool { n.reporter.ReportOk(n.sdkId, "config from cache not modified") return false } - n.log.Debugf("new JSON received from redis, reloading") + n.log.Debugf("new JSON received from cache, reloading") var root configcat.ConfigJson if err = json.Unmarshal(configJson, &root); err != nil { - n.log.Errorf("failed to parse JSON from redis: %s", err) - n.reporter.ReportError(n.sdkId, "failed to parse JSON from redis") + n.log.Errorf("failed to parse JSON from cache: %s", err) + n.reporter.ReportError(n.sdkId, "failed to parse JSON from cache") return false } ser, _ := json.Marshal(root) // Re-serialize to enforce the JSON schema @@ -100,8 +98,5 @@ func (n *notifyingCacheStore) Close() { n.Notifier.Close() n.ctxCancel() n.poller.Stop() - if closable, ok := n.CacheEntryStore.(store.ClosableStore); ok { - closable.Close() - } n.log.Reportf("shutdown complete") } diff --git a/sdk/store/cache/cache_notify_test.go b/sdk/store/cache/cache_notify_test.go index ab1d639..8559d60 100644 --- a/sdk/store/cache/cache_notify_test.go +++ b/sdk/store/cache/cache_notify_test.go @@ -7,9 +7,10 @@ import ( "github.com/configcat/configcat-proxy/diag/status" "github.com/configcat/configcat-proxy/internal/utils" "github.com/configcat/configcat-proxy/log" - "github.com/configcat/configcat-proxy/sdk/store/cache/redis" "github.com/configcat/go-sdk/v9/configcatcache" "github.com/stretchr/testify/assert" + "net/http" + "sync" "testing" "time" ) @@ -18,10 +19,12 @@ func TestRedisNotify(t *testing.T) { sdkKey := "key" cacheKey := configcatcache.ProduceCacheKey(sdkKey, configcatcache.ConfigJSONName, configcatcache.ConfigJSONCacheVersion) s := miniredis.RunT(t) - r := NewCacheStore(redis.NewRedisStore(&config.RedisConfig{Addresses: []string{s.Addr()}}), status.NewNullReporter()) - srv := NewNotifyingCacheStore("test", cacheKey, r, &config.OfflineConfig{CachePollInterval: 1}, status.NewNullReporter(), log.NewNullLogger()).(*notifyingCacheStore) + red, err := newRedis(&config.RedisConfig{Addresses: []string{s.Addr()}}, log.NewNullLogger()) + assert.NoError(t, err) + r := NewCacheStore(red, status.NewEmptyReporter()) + srv := NewNotifyingCacheStore("test", cacheKey, r, &config.OfflineConfig{CachePollInterval: 1}, status.NewEmptyReporter(), log.NewNullLogger()).(*notifyingCacheStore) cacheEntry := configcatcache.CacheSegmentsToBytes(time.Now(), "etag", []byte(`{"f":{"flag":{"v":{"b":true}}},"p":null}`)) - err := s.Set(cacheKey, string(cacheEntry)) + err = s.Set(cacheKey, string(cacheEntry)) assert.NoError(t, err) utils.WithTimeout(2*time.Second, func() { <-srv.Modified() @@ -43,8 +46,10 @@ func TestRedisNotify_Initial(t *testing.T) { cacheEntry := configcatcache.CacheSegmentsToBytes(time.Now(), "etag", []byte(`{"f":{"flag":{"v":{"b":true}}},"p":null}`)) err := s.Set(cacheKey, string(cacheEntry)) assert.NoError(t, err) - r := NewCacheStore(redis.NewRedisStore(&config.RedisConfig{Addresses: []string{s.Addr()}}), status.NewNullReporter()) - srv := NewNotifyingCacheStore("test", cacheKey, r, &config.OfflineConfig{CachePollInterval: 1}, status.NewNullReporter(), log.NewNullLogger()) + red, err := newRedis(&config.RedisConfig{Addresses: []string{s.Addr()}}, log.NewNullLogger()) + assert.NoError(t, err) + r := NewCacheStore(red, status.NewEmptyReporter()) + srv := NewNotifyingCacheStore("test", cacheKey, r, &config.OfflineConfig{CachePollInterval: 1}, status.NewEmptyReporter(), log.NewNullLogger()) s.CheckGet(t, cacheKey, string(cacheEntry)) res, err := srv.Get(context.Background(), "") _, _, j, _ := configcatcache.CacheSegmentsFromBytes(res) @@ -60,8 +65,10 @@ func TestRedisNotify_Notify(t *testing.T) { cacheEntry := configcatcache.CacheSegmentsToBytes(time.Now(), "etag", []byte(`{"f":{"flag":{"v":{"b":false}}},"p":null}`)) err := s.Set(cacheKey, string(cacheEntry)) assert.NoError(t, err) - r := NewCacheStore(redis.NewRedisStore(&config.RedisConfig{Addresses: []string{s.Addr()}}), status.NewNullReporter()) - srv := NewNotifyingCacheStore("test", cacheKey, r, &config.OfflineConfig{CachePollInterval: 1}, status.NewNullReporter(), log.NewNullLogger()).(*notifyingCacheStore) + red, err := newRedis(&config.RedisConfig{Addresses: []string{s.Addr()}}, log.NewNullLogger()) + assert.NoError(t, err) + r := NewCacheStore(red, status.NewEmptyReporter()) + srv := NewNotifyingCacheStore("test", cacheKey, r, &config.OfflineConfig{CachePollInterval: 1}, status.NewEmptyReporter(), log.NewNullLogger()).(*notifyingCacheStore) s.CheckGet(t, cacheKey, string(cacheEntry)) res, err := srv.Get(context.Background(), "") _, _, j, _ := configcatcache.CacheSegmentsFromBytes(res) @@ -87,8 +94,10 @@ func TestRedisNotify_BadJson(t *testing.T) { cacheKey := configcatcache.ProduceCacheKey(sdkKey, configcatcache.ConfigJSONName, configcatcache.ConfigJSONCacheVersion) err := s.Set(cacheKey, `{"f":{"flag":{"v":{"b":false}}},"p":null}`) assert.NoError(t, err) - r := NewCacheStore(redis.NewRedisStore(&config.RedisConfig{Addresses: []string{s.Addr()}}), status.NewNullReporter()) - srv := NewNotifyingCacheStore("test", cacheKey, r, &config.OfflineConfig{CachePollInterval: 1}, status.NewNullReporter(), log.NewNullLogger()) + red, err := newRedis(&config.RedisConfig{Addresses: []string{s.Addr()}}, log.NewNullLogger()) + assert.NoError(t, err) + r := NewCacheStore(red, status.NewEmptyReporter()) + srv := NewNotifyingCacheStore("test", cacheKey, r, &config.OfflineConfig{CachePollInterval: 1}, status.NewEmptyReporter(), log.NewNullLogger()) res, err := srv.Get(context.Background(), "") _, _, j, _ := configcatcache.CacheSegmentsFromBytes(res) assert.NoError(t, err) @@ -96,14 +105,16 @@ func TestRedisNotify_BadJson(t *testing.T) { assert.Equal(t, `{"f":null,"s":null,"p":null}`, string(r.LoadEntry().ConfigJson)) } -func TestRedisNotify_MalformedJson(t *testing.T) { +func TestRedisNotify_MalformedCacheEntry(t *testing.T) { sdkKey := "key" s := miniredis.RunT(t) cacheKey := configcatcache.ProduceCacheKey(sdkKey, configcatcache.ConfigJSONName, configcatcache.ConfigJSONCacheVersion) err := s.Set(cacheKey, `{"k":{"flag`) assert.NoError(t, err) - r := NewCacheStore(redis.NewRedisStore(&config.RedisConfig{Addresses: []string{s.Addr()}}), status.NewNullReporter()) - srv := NewNotifyingCacheStore("test", cacheKey, r, &config.OfflineConfig{CachePollInterval: 1}, status.NewNullReporter(), log.NewNullLogger()) + red, err := newRedis(&config.RedisConfig{Addresses: []string{s.Addr()}}, log.NewNullLogger()) + assert.NoError(t, err) + r := NewCacheStore(red, status.NewEmptyReporter()) + srv := NewNotifyingCacheStore("test", cacheKey, r, &config.OfflineConfig{CachePollInterval: 1}, status.NewEmptyReporter(), log.NewNullLogger()) res, err := srv.Get(context.Background(), "") _, _, j, _ := configcatcache.CacheSegmentsFromBytes(res) assert.NoError(t, err) @@ -111,9 +122,62 @@ func TestRedisNotify_MalformedJson(t *testing.T) { assert.Equal(t, `{"f":null,"s":null,"p":null}`, string(r.LoadEntry().ConfigJson)) } +func TestRedisNotify_MalformedJson(t *testing.T) { + sdkKey := "key" + s := miniredis.RunT(t) + cacheKey := configcatcache.ProduceCacheKey(sdkKey, configcatcache.ConfigJSONName, configcatcache.ConfigJSONCacheVersion) + cacheEntry := configcatcache.CacheSegmentsToBytes(time.Now(), "etag", []byte(`{"k":{"flag`)) + err := s.Set(cacheKey, string(cacheEntry)) + assert.NoError(t, err) + red, err := newRedis(&config.RedisConfig{Addresses: []string{s.Addr()}}, log.NewNullLogger()) + assert.NoError(t, err) + r := NewCacheStore(red, status.NewEmptyReporter()) + srv := NewNotifyingCacheStore("test", cacheKey, r, &config.OfflineConfig{CachePollInterval: 1}, status.NewEmptyReporter(), log.NewNullLogger()) + res, err := srv.Get(context.Background(), "") + _, _, j, _ := configcatcache.CacheSegmentsFromBytes(res) + assert.NoError(t, err) + assert.Equal(t, `{"f":null,"s":null,"p":null}`, string(j)) + assert.Equal(t, `{"f":null,"s":null,"p":null}`, string(r.LoadEntry().ConfigJson)) +} + +func TestRedisNotify_Reporter(t *testing.T) { + sdkKey := "key" + s := miniredis.RunT(t) + cacheKey := configcatcache.ProduceCacheKey(sdkKey, configcatcache.ConfigJSONName, configcatcache.ConfigJSONCacheVersion) + cacheEntry := configcatcache.CacheSegmentsToBytes(time.Now(), "etag", []byte(`{"f":{"flag":{"v":{"b":true}}},"p":null}`)) + err := s.Set(cacheKey, string(cacheEntry)) + assert.NoError(t, err) + reporter := &testReporter{} + red, err := newRedis(&config.RedisConfig{Addresses: []string{s.Addr()}}, log.NewNullLogger()) + assert.NoError(t, err) + r := NewCacheStore(red, reporter) + srv := NewNotifyingCacheStore(sdkKey, cacheKey, r, &config.OfflineConfig{CachePollInterval: 1}, reporter, log.NewNullLogger()).(*notifyingCacheStore) + + rec := reporter.Records() + assert.Contains(t, rec[len(rec)-1], "reload from cache succeeded") + + assert.Equal(t, "etag", srv.LoadEntry().ETag) + assert.False(t, srv.reload()) + assert.Equal(t, "etag", srv.LoadEntry().ETag) + + rec = reporter.Records() + assert.Contains(t, rec[len(rec)-1], "config from cache not modified") + + cacheEntry = configcatcache.CacheSegmentsToBytes(time.Now(), "etag2", []byte(`{"f":{"flag":{"v":`)) + err = s.Set(cacheKey, string(cacheEntry)) + assert.NoError(t, err) + assert.False(t, srv.reload()) + assert.Equal(t, "etag", srv.LoadEntry().ETag) + + rec = reporter.Records() + assert.Contains(t, rec[len(rec)-1], "failed to parse JSON from cache") +} + func TestRedisNotify_Unavailable(t *testing.T) { - r := NewCacheStore(redis.NewRedisStore(&config.RedisConfig{Addresses: []string{"nonexisting"}}), status.NewNullReporter()) - srv := NewNotifyingCacheStore("test", "", r, &config.OfflineConfig{CachePollInterval: 1}, status.NewNullReporter(), log.NewNullLogger()) + red, err := newRedis(&config.RedisConfig{Addresses: []string{"nonexisting"}}, log.NewNullLogger()) + assert.NoError(t, err) + r := NewCacheStore(red, status.NewEmptyReporter()) + srv := NewNotifyingCacheStore("test", "", r, &config.OfflineConfig{CachePollInterval: 1}, status.NewEmptyReporter(), log.NewNullLogger()) res, err := srv.Get(context.Background(), "") _, _, j, _ := configcatcache.CacheSegmentsFromBytes(res) assert.NoError(t, err) @@ -123,8 +187,10 @@ func TestRedisNotify_Unavailable(t *testing.T) { func TestRedisNotify_Close(t *testing.T) { s := miniredis.RunT(t) - r := NewCacheStore(redis.NewRedisStore(&config.RedisConfig{Addresses: []string{s.Addr()}}), status.NewNullReporter()) - srv := NewNotifyingCacheStore("test", "", r, &config.OfflineConfig{CachePollInterval: 1}, status.NewNullReporter(), log.NewNullLogger()).(*notifyingCacheStore) + red, err := newRedis(&config.RedisConfig{Addresses: []string{s.Addr()}}, log.NewNullLogger()) + assert.NoError(t, err) + r := NewCacheStore(red, status.NewEmptyReporter()) + srv := NewNotifyingCacheStore("test", "", r, &config.OfflineConfig{CachePollInterval: 1}, status.NewEmptyReporter(), log.NewNullLogger()).(*notifyingCacheStore) go func() { srv.Close() }() @@ -135,3 +201,42 @@ func TestRedisNotify_Close(t *testing.T) { } }) } + +type testReporter struct { + records []string + + mu sync.RWMutex +} + +func (r *testReporter) RegisterSdk(_ string, _ *config.SDKConfig) { + // do nothing +} + +func (r *testReporter) ReportOk(component string, message string) { + r.mu.Lock() + defer r.mu.Unlock() + + r.records = append(r.records, component+"[ok] "+message) +} + +func (r *testReporter) ReportError(component string, message string) { + r.mu.Lock() + defer r.mu.Unlock() + + r.records = append(r.records, component+"[error] "+message) +} + +func (r *testReporter) Records() []string { + r.mu.RLock() + defer r.mu.RUnlock() + + return r.records +} + +func (r *testReporter) HttpHandler() http.HandlerFunc { + return nil +} + +func (r *testReporter) GetStatus() status.Status { + return status.Status{} +} diff --git a/sdk/store/cache/cache_test.go b/sdk/store/cache/cache_test.go index 8595b79..85d9ff7 100644 --- a/sdk/store/cache/cache_test.go +++ b/sdk/store/cache/cache_test.go @@ -2,7 +2,10 @@ package cache import ( "context" + "github.com/alicebob/miniredis/v2" + "github.com/configcat/configcat-proxy/config" "github.com/configcat/configcat-proxy/diag/status" + "github.com/configcat/configcat-proxy/log" "github.com/configcat/go-sdk/v9/configcatcache" "github.com/stretchr/testify/assert" "testing" @@ -10,7 +13,7 @@ import ( ) func TestCacheStore(t *testing.T) { - store := NewCacheStore(&testCache{}, status.NewNullReporter()).(*cacheStore) + store := NewCacheStore(&testCache{}, status.NewEmptyReporter()).(*cacheStore) err := store.Set(context.Background(), "key", configcatcache.CacheSegmentsToBytes(time.Now(), "etag", []byte(`test`))) assert.NoError(t, err) @@ -22,6 +25,52 @@ func TestCacheStore(t *testing.T) { assert.Equal(t, `test`, string(store.LoadEntry().ConfigJson)) } +func TestSetupExternalCache(t *testing.T) { + t.Run("redis", func(t *testing.T) { + s := miniredis.RunT(t) + store, err := SetupExternalCache(context.Background(), &config.CacheConfig{Redis: config.RedisConfig{Addresses: []string{s.Addr()}, Enabled: true}}, log.NewNullLogger()) + defer store.Shutdown() + assert.NoError(t, err) + assert.IsType(t, &redisStore{}, store) + }) + t.Run("mongodb", func(t *testing.T) { + store, err := SetupExternalCache(context.Background(), &config.CacheConfig{MongoDb: config.MongoDbConfig{ + Enabled: true, + Url: "mongodb://localhost:27017", + Database: "test_db", + Collection: "coll", + }}, log.NewNullLogger()) + defer store.Shutdown() + assert.NoError(t, err) + assert.IsType(t, &mongoDbStore{}, store) + }) + t.Run("dynamodb", func(t *testing.T) { + store, err := SetupExternalCache(context.Background(), &config.CacheConfig{DynamoDb: config.DynamoDbConfig{ + Enabled: true, + Table: tableName, + Url: endpoint, + }}, log.NewNullLogger()) + defer store.Shutdown() + assert.NoError(t, err) + assert.IsType(t, &dynamoDbStore{}, store) + }) + t.Run("only one selected", func(t *testing.T) { + s := miniredis.RunT(t) + store, err := SetupExternalCache(context.Background(), &config.CacheConfig{ + Redis: config.RedisConfig{Addresses: []string{s.Addr()}, Enabled: true}, + MongoDb: config.MongoDbConfig{ + Enabled: true, + Url: "mongodb://localhost:27017", + Database: "test_db", + Collection: "coll", + }, + }, log.NewNullLogger()) + defer store.Shutdown() + assert.NoError(t, err) + assert.IsType(t, &redisStore{}, store) + }) +} + type testCache struct { v []byte } diff --git a/sdk/store/cache/dynamodb.go b/sdk/store/cache/dynamodb.go new file mode 100644 index 0000000..a008fc1 --- /dev/null +++ b/sdk/store/cache/dynamodb.go @@ -0,0 +1,75 @@ +package cache + +import ( + "context" + "fmt" + "github.com/aws/aws-sdk-go-v2/aws" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/configcat/configcat-proxy/config" + "github.com/configcat/configcat-proxy/log" +) + +type dynamoDbStore struct { + dynamoDb *dynamodb.Client + table *string + log log.Logger +} + +func newDynamoDb(ctx context.Context, conf *config.DynamoDbConfig, log log.Logger) (External, error) { + dynamoLog := log.WithPrefix("dynamodb") + awsCtx, err := awsconfig.LoadDefaultConfig(ctx) + if err != nil { + dynamoLog.Errorf("couldn't read aws config for DynamoDB: %s", err) + return nil, err + } + var opts []func(*dynamodb.Options) + if conf.Url != "" { + opts = append(opts, func(options *dynamodb.Options) { + options.BaseEndpoint = aws.String(conf.Url) + }) + } + log.Reportf("using DynamoDB for cache storage") + return &dynamoDbStore{ + dynamoDb: dynamodb.NewFromConfig(awsCtx, opts...), + table: aws.String(conf.Table), + }, nil +} + +func (d *dynamoDbStore) Get(ctx context.Context, key string) ([]byte, error) { + res, err := d.dynamoDb.GetItem(ctx, &dynamodb.GetItemInput{ + TableName: d.table, + Key: map[string]types.AttributeValue{ + keyName: &types.AttributeValueMemberS{Value: key}, + }, + ConsistentRead: aws.Bool(true), + }) + if err != nil { + return nil, err + } + if payload, ok := res.Item[payloadName]; ok { + switch v := payload.(type) { + case *types.AttributeValueMemberB: + return v.Value, nil + default: + return nil, fmt.Errorf("invalid item under key '%s'", key) + } + } + return nil, fmt.Errorf("cache item not found for key '%s'", key) +} + +func (d *dynamoDbStore) Set(ctx context.Context, key string, value []byte) error { + _, err := d.dynamoDb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: d.table, + Item: map[string]types.AttributeValue{ + keyName: &types.AttributeValueMemberS{Value: key}, + payloadName: &types.AttributeValueMemberB{Value: value}, + }, + }) + return err +} + +func (d *dynamoDbStore) Shutdown() { + // nothing to do +} diff --git a/sdk/store/cache/dynamodb_test.go b/sdk/store/cache/dynamodb_test.go new file mode 100644 index 0000000..c080d8f --- /dev/null +++ b/sdk/store/cache/dynamodb_test.go @@ -0,0 +1,142 @@ +package cache + +import ( + "context" + "fmt" + "github.com/aws/aws-sdk-go-v2/aws" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/configcat/configcat-proxy/config" + "github.com/configcat/configcat-proxy/log" + "github.com/configcat/go-sdk/v9/configcatcache" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +const ( + tableName = "test-table" + endpoint = "http://localhost:8000" +) + +func TestDynamoDbStore(t *testing.T) { + t.Setenv("AWS_ACCESS_KEY_ID", "key") + t.Setenv("AWS_SECRET_ACCESS_KEY", "secret") + t.Setenv("AWS_SESSION_TOKEN", "session") + t.Setenv("AWS_DEFAULT_REGION", "us-east-1") + assert.NoError(t, createTableIfNotExist()) + + t.Run("ok", func(t *testing.T) { + store, err := newDynamoDb(context.Background(), &config.DynamoDbConfig{ + Enabled: true, + Table: tableName, + Url: endpoint, + }, log.NewNullLogger()) + assert.NoError(t, err) + defer store.Shutdown() + + cacheEntry := configcatcache.CacheSegmentsToBytes(time.Now(), "etag", []byte(`test`)) + + err = store.Set(context.Background(), "k1", cacheEntry) + assert.NoError(t, err) + + res, err := store.Get(context.Background(), "k1") + assert.NoError(t, err) + assert.Equal(t, cacheEntry, res) + + cacheEntry = configcatcache.CacheSegmentsToBytes(time.Now(), "etag", []byte(`test2`)) + + err = store.Set(context.Background(), "k1", cacheEntry) + assert.NoError(t, err) + + res, err = store.Get(context.Background(), "k1") + assert.NoError(t, err) + assert.Equal(t, cacheEntry, res) + }) + + t.Run("empty", func(t *testing.T) { + store, err := newDynamoDb(context.Background(), &config.DynamoDbConfig{ + Enabled: true, + Table: tableName, + Url: endpoint, + }, log.NewNullLogger()) + assert.NoError(t, err) + defer store.Shutdown() + + _, err = store.Get(context.Background(), "k2") + assert.Error(t, err) + }) + + t.Run("no-table", func(t *testing.T) { + store, err := newDynamoDb(context.Background(), &config.DynamoDbConfig{ + Enabled: true, + Table: "nonexisting", + Url: endpoint, + }, log.NewNullLogger()) + assert.NoError(t, err) + defer store.Shutdown() + + _, err = store.Get(context.Background(), "k3") + assert.Error(t, err) + }) +} + +func createTableIfNotExist() error { + awsCtx, err := awsconfig.LoadDefaultConfig(context.Background()) + if err != nil { + return err + } + var opts []func(*dynamodb.Options) + opts = append(opts, func(options *dynamodb.Options) { + options.BaseEndpoint = aws.String(endpoint) + }) + + client := dynamodb.NewFromConfig(awsCtx, opts...) + + _, err = client.DescribeTable(context.Background(), &dynamodb.DescribeTableInput{ + TableName: aws.String(tableName), + }) + if err == nil { + return nil + } + _, err = client.CreateTable(context.Background(), &dynamodb.CreateTableInput{ + TableName: aws.String(tableName), + AttributeDefinitions: []types.AttributeDefinition{ + { + AttributeName: aws.String(keyName), + AttributeType: types.ScalarAttributeTypeS, + }, + }, + KeySchema: []types.KeySchemaElement{ + { + AttributeName: aws.String(keyName), + KeyType: types.KeyTypeHash, + }, + }, + ProvisionedThroughput: &types.ProvisionedThroughput{ + ReadCapacityUnits: aws.Int64(1), + WriteCapacityUnits: aws.Int64(1), + }, + }) + if err != nil { + return err + } + + timeout := time.After(5 * time.Second) + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + for { + select { + case <-timeout: + return fmt.Errorf("table creation timed out") + case <-ticker.C: + res, err := client.DescribeTable(context.Background(), &dynamodb.DescribeTableInput{ + TableName: aws.String(tableName), + }) + if err == nil && res.Table.TableStatus == types.TableStatusActive { + return nil + } + } + } +} diff --git a/sdk/store/cache/mongodb.go b/sdk/store/cache/mongodb.go new file mode 100644 index 0000000..4f0ec1e --- /dev/null +++ b/sdk/store/cache/mongodb.go @@ -0,0 +1,76 @@ +package cache + +import ( + "context" + "github.com/configcat/configcat-proxy/config" + "github.com/configcat/configcat-proxy/log" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "time" +) + +type mongoDbStore struct { + mongoDb *mongo.Client + collection *mongo.Collection + log log.Logger +} + +type entry struct { + Key string + Payload []byte +} + +func newMongoDb(ctx context.Context, conf *config.MongoDbConfig, log log.Logger) (External, error) { + opts := options.Client().ApplyURI(conf.Url) + if conf.Tls.Enabled { + t, err := conf.Tls.LoadTlsOptions() + if err != nil { + log.Errorf("failed to configure TLS for MongoDB: %s", err) + return nil, err + } + opts.SetTLSConfig(t) + } + client, err := mongo.Connect(ctx, opts) + if err != nil { + log.Errorf("couldn't connect to MongoDB: %s", err) + return nil, err + } + collection := client.Database(conf.Database).Collection(conf.Collection) + _, err = collection.Indexes().CreateOne(ctx, mongo.IndexModel{ + Keys: bson.M{keyName: 1}, + Options: options.Index().SetUnique(true), + }) + if err != nil { + log.Errorf("couldn't create the 'key' index in the '%s' MongoDB collection: %s", conf.Collection, err) + return nil, err + } + log.Reportf("using MongoDB for cache storage") + return &mongoDbStore{ + mongoDb: client, + collection: collection, + log: log, + }, nil +} + +func (m *mongoDbStore) Get(ctx context.Context, key string) ([]byte, error) { + var result entry + err := m.collection.FindOne(ctx, bson.M{keyName: key}).Decode(&result) + return result.Payload, err +} + +func (m *mongoDbStore) Set(ctx context.Context, key string, value []byte) error { + _, err := m.collection.ReplaceOne(ctx, bson.M{keyName: key}, entry{Key: key, Payload: value}, options.Replace().SetUpsert(true)) + return err +} + +func (m *mongoDbStore) Shutdown() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + err := m.mongoDb.Disconnect(ctx) + if err != nil { + m.log.Errorf("shutdown error: %s", err) + } + m.log.Reportf("shutdown complete") +} diff --git a/sdk/store/cache/mongodb_test.go b/sdk/store/cache/mongodb_test.go new file mode 100644 index 0000000..fa6e3d0 --- /dev/null +++ b/sdk/store/cache/mongodb_test.go @@ -0,0 +1,95 @@ +package cache + +import ( + "context" + "github.com/configcat/configcat-proxy/config" + "github.com/configcat/configcat-proxy/log" + "github.com/configcat/go-sdk/v9/configcatcache" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestMongoDbStore(t *testing.T) { + store, err := newMongoDb(context.Background(), &config.MongoDbConfig{ + Enabled: true, + Url: "mongodb://localhost:27017", + Database: "test_db", + Collection: "coll", + }, log.NewNullLogger()) + defer store.Shutdown() + + cacheEntry := configcatcache.CacheSegmentsToBytes(time.Now(), "etag", []byte(`test`)) + + err = store.Set(context.Background(), "k1", cacheEntry) + assert.NoError(t, err) + + res, err := store.Get(context.Background(), "k1") + assert.NoError(t, err) + assert.Equal(t, cacheEntry, res) + + cacheEntry = configcatcache.CacheSegmentsToBytes(time.Now(), "etag", []byte(`test2`)) + + err = store.Set(context.Background(), "k1", cacheEntry) + assert.NoError(t, err) + + res, err = store.Get(context.Background(), "k1") + assert.NoError(t, err) + assert.Equal(t, cacheEntry, res) +} + +func TestMongoDbStore_Empty(t *testing.T) { + store, err := newMongoDb(context.Background(), &config.MongoDbConfig{ + Enabled: true, + Url: "mongodb://localhost:27017", + Database: "test_db", + Collection: "coll", + }, log.NewNullLogger()) + defer store.Shutdown() + + _, err = store.Get(context.Background(), "k2") + assert.Error(t, err) +} + +func TestMongoDbStore_Invalid(t *testing.T) { + _, err := newMongoDb(context.Background(), &config.MongoDbConfig{ + Enabled: true, + Url: "invalid", + Database: "test_db", + Collection: "coll", + }, log.NewNullLogger()) + + assert.Error(t, err) +} + +func TestMongoDbStore_TLS_Invalid(t *testing.T) { + store, err := newMongoDb(context.Background(), &config.MongoDbConfig{ + Enabled: true, + Url: "mongodb://localhost:27017", + Database: "test_db", + Collection: "coll", + Tls: config.TlsConfig{ + Enabled: true, + MinVersion: 1.1, + Certificates: []config.CertConfig{ + {Key: "nonexisting", Cert: "nonexisting"}, + }, + }, + }, log.NewNullLogger()) + assert.ErrorContains(t, err, "failed to load certificate and key files") + assert.Nil(t, store) +} + +func TestMongoDbStore_Connect_Fails(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + store, err := newMongoDb(ctx, &config.MongoDbConfig{ + Enabled: true, + Url: "mongodb://localhost:27016", + Database: "test_db", + Collection: "coll", + }, log.NewNullLogger()) + assert.ErrorContains(t, err, "context deadline exceeded") + assert.Nil(t, store) +} diff --git a/sdk/store/cache/redis/redis.go b/sdk/store/cache/redis.go similarity index 57% rename from sdk/store/cache/redis/redis.go rename to sdk/store/cache/redis.go index a59dfce..768752f 100644 --- a/sdk/store/cache/redis/redis.go +++ b/sdk/store/cache/redis.go @@ -1,18 +1,18 @@ -package redis +package cache import ( "context" - "crypto/tls" "github.com/configcat/configcat-proxy/config" - configcat "github.com/configcat/go-sdk/v9" + "github.com/configcat/configcat-proxy/log" "github.com/redis/go-redis/v9" ) type redisStore struct { redisDb redis.UniversalClient + log log.Logger } -func NewRedisStore(conf *config.RedisConfig) configcat.ConfigCache { +func newRedis(conf *config.RedisConfig, log log.Logger) (External, error) { opts := &redis.UniversalOptions{ Addrs: conf.Addresses, Password: conf.Password, @@ -22,20 +22,18 @@ func NewRedisStore(conf *config.RedisConfig) configcat.ConfigCache { opts.Username = conf.User } if conf.Tls.Enabled { - t := &tls.Config{ - MinVersion: conf.Tls.GetVersion(), - ServerName: conf.Tls.ServerName, - } - for _, c := range conf.Tls.Certificates { - if cert, err := tls.LoadX509KeyPair(c.Cert, c.Key); err == nil { - t.Certificates = append(t.Certificates, cert) - } + t, err := conf.Tls.LoadTlsOptions() + if err != nil { + log.Errorf("failed to configure TLS for Redis: %s", err) + return nil, err } opts.TLSConfig = t } + log.Reportf("using Redis for cache storage") return &redisStore{ redisDb: redis.NewUniversalClient(opts), - } + log: log, + }, nil } func (r *redisStore) Get(ctx context.Context, key string) ([]byte, error) { @@ -46,6 +44,10 @@ func (r *redisStore) Set(ctx context.Context, key string, value []byte) error { return r.redisDb.Set(ctx, key, value, 0).Err() } -func (r *redisStore) Close() { - _ = r.redisDb.Close() +func (r *redisStore) Shutdown() { + err := r.redisDb.Close() + if err != nil { + r.log.Errorf("shutdown error: %s", err) + } + r.log.Reportf("shutdown complete") } diff --git a/sdk/store/cache/redis/redis_test.go b/sdk/store/cache/redis/redis_test.go deleted file mode 100644 index fe0e630..0000000 --- a/sdk/store/cache/redis/redis_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package redis - -import ( - "context" - "github.com/alicebob/miniredis/v2" - "github.com/configcat/configcat-proxy/config" - "github.com/configcat/go-sdk/v9/configcatcache" - "github.com/stretchr/testify/assert" - "testing" - "time" -) - -func TestRedisStorage(t *testing.T) { - s := miniredis.RunT(t) - srv := NewRedisStore(&config.RedisConfig{Addresses: []string{s.Addr()}}).(*redisStore) - defer srv.Close() - - cacheEntry := configcatcache.CacheSegmentsToBytes(time.Now(), "etag", []byte(`test`)) - err := srv.Set(context.Background(), "key", cacheEntry) - assert.NoError(t, err) - s.CheckGet(t, "key", string(cacheEntry)) - res, err := srv.Get(context.Background(), "key") - assert.NoError(t, err) - _, _, j, err := configcatcache.CacheSegmentsFromBytes(res) - assert.NoError(t, err) - assert.Equal(t, `test`, string(j)) -} - -func TestRedisStorage_Unavailable(t *testing.T) { - srv := NewRedisStore(&config.RedisConfig{Addresses: []string{"nonexisting"}}).(*redisStore) - defer srv.Close() - - cacheEntry := configcatcache.CacheSegmentsToBytes(time.Now(), "etag", []byte(`test`)) - err := srv.Set(context.Background(), "", cacheEntry) - assert.Error(t, err) - _, err = srv.Get(context.Background(), "") - assert.Error(t, err) -} diff --git a/sdk/store/cache/redis_test.go b/sdk/store/cache/redis_test.go new file mode 100644 index 0000000..82c364d --- /dev/null +++ b/sdk/store/cache/redis_test.go @@ -0,0 +1,126 @@ +package cache + +import ( + "context" + "github.com/alicebob/miniredis/v2" + "github.com/configcat/configcat-proxy/config" + "github.com/configcat/configcat-proxy/internal/utils" + "github.com/configcat/configcat-proxy/log" + "github.com/configcat/go-sdk/v9/configcatcache" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestRedisStorage(t *testing.T) { + s := miniredis.RunT(t) + store, err := newRedis(&config.RedisConfig{Addresses: []string{s.Addr()}}, log.NewNullLogger()) + assert.NoError(t, err) + srv := store.(*redisStore) + defer srv.Shutdown() + + cacheEntry := configcatcache.CacheSegmentsToBytes(time.Now(), "etag", []byte(`test`)) + err = srv.Set(context.Background(), "key", cacheEntry) + assert.NoError(t, err) + s.CheckGet(t, "key", string(cacheEntry)) + res, err := srv.Get(context.Background(), "key") + assert.NoError(t, err) + _, _, j, err := configcatcache.CacheSegmentsFromBytes(res) + assert.NoError(t, err) + assert.Equal(t, `test`, string(j)) +} + +func TestRedisStorage_Unavailable(t *testing.T) { + store, err := newRedis(&config.RedisConfig{Addresses: []string{"nonexisting"}}, log.NewNullLogger()) + assert.NoError(t, err) + srv := store.(*redisStore) + defer srv.Shutdown() + + cacheEntry := configcatcache.CacheSegmentsToBytes(time.Now(), "etag", []byte(`test`)) + err = srv.Set(context.Background(), "", cacheEntry) + assert.Error(t, err) + _, err = srv.Get(context.Background(), "") + assert.Error(t, err) +} + +func TestRedisStorage_TLS(t *testing.T) { + t.Run("valid", func(t *testing.T) { + utils.UseTempFile(` +-----BEGIN CERTIFICATE----- +MIICrzCCAZcCFDnpdKF+Pg1smjtIXrNdIgxGYEJfMA0GCSqGSIb3DQEBCwUAMBQx +EjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yMzAzMDEyMTA2NThaFw0yNDAyMjkyMTA2 +NThaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAOiTDTjfAPvJLDZ2mwNvu0pohSHPRzzfZRc16iVI6+ESl0Dwjdjl +yERFO/ts1GQnhE2ggykvoxH4zUy1OCnjTJ+Mm1ryjy4G5ZIILIF9MfFcyma5/5Xd +oOTcDr3ZDTAwFaabKYKisoVMHAJCphencgoyOToW5/HRHMKOEpTJOQWSyNduXYfY +nsWb3hx7WD9NajliW7/Jjbf7UnDtKY2VM2GZWT3ygIH/7SlBqyuXJNqyZXbqfbrP +6mdZQ5wvYsnSUU4kNMtZg/ns+0H5R7PFmRhIRM0nZvJZTO9oHREdm+e2nnZwHyJF +Z26LxE7Qr1bn8+PQSydyQIqeUdaSX2LuXqECAwEAATANBgkqhkiG9w0BAQsFAAOC +AQEAjRoOTe4W4OQ6YOo5kx5sMAozh0Rg6eifS0s8GuxKwfuBop8FEnM3wAfF6x3J +fsik9MmoM4L11HWjttb46UFq/rP3GsA3DLX8i1yBOES+iyCELd5Ss9q1jfr/Jqo3 +cAanE4yl3NNEZoDmMdSj2U11BneKSzHDR+l2hDF9wBifWGI9DQ1ItfA5I6MwnL+0 +J03vcwPSwme4bKC/avAT2oDD7jLGLA+kuhMqHvVq7nXRzs46xyFPBBv7fBxXjPPG +c89d0ISafKtZ9kIKaRrzu2HX+b0fzKr0vtHYDLtC1U5oU7GPB12eupERkmWYlhrw +hDL3X7kt3jEZFkzGV1XL1IJx/g== +-----END CERTIFICATE-----`, func(cert string) { + utils.UseTempFile(`-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDokw043wD7ySw2 +dpsDb7tKaIUhz0c832UXNeolSOvhEpdA8I3Y5chERTv7bNRkJ4RNoIMpL6MR+M1M +tTgp40yfjJta8o8uBuWSCCyBfTHxXMpmuf+V3aDk3A692Q0wMBWmmymCorKFTBwC +QqYXp3IKMjk6Fufx0RzCjhKUyTkFksjXbl2H2J7Fm94ce1g/TWo5Ylu/yY23+1Jw +7SmNlTNhmVk98oCB/+0pQasrlyTasmV26n26z+pnWUOcL2LJ0lFOJDTLWYP57PtB ++UezxZkYSETNJ2byWUzvaB0RHZvntp52cB8iRWdui8RO0K9W5/Pj0EsnckCKnlHW +kl9i7l6hAgMBAAECggEBAOMWiqeIH5a6BGCdiJhfZZmu2qd7k8xdOIDkVN7ZB/B5 +TZTMDUTGgLggfgPubKfqaeW+H7N8XxZyQEtw+wjzduKm0R6JjsJbW5cuQf6htr08 +ZCjP3j5/69TrBb3bjGQL32gRQwPaRsOe4A5Y84JPLivEhFoy+YEFNLbHMF905yeH +IaSeqeK0GNm0a/MU68pa1ODIc8B2zqo+f6I9qekezlDR7Or487FqnlLtNf0yvnLD +sbshzj5rzLdLYgA/RNZ4CkuGddxEYjnDB1IG0NX8m9MrHlsi7jqxa7pHt5oDrRsW +ZxBez6Q70dE29sdl5lnce3qjxweB2NK3Q6Cr2eyizwECgYEA/L/WzgY1yDMWzaCr +SRThg9NWO1EYbvz4uxt7rElfZ+NYAaT08E35Ooo9IeBzp3VoFA1PcNQnKB5pgczO +Mu5W/td5zpx1dzguBZAl4IpKkml08i06R7FxxTqtRM/P7Pna+RagtqAo3JZww3bd +ofIPH2OrobqlcFhOsLqKp5ocDNECgYEA65DJsImeBfW1aZ5ABgPr7NErSv2fKj1r +eGsgC5Za1ZiaG5LWkCpuezsvf6ma4EN3CMl5Fo617qaY6mnL2HlfVtFhHYSeLpna +9ZgqZ1zj2HkqiXOPEkb3d3cC61rXiMK97NpshrpzFx+uMCH8MMu9/CVJEHNKGgAq +6zZQ4LhjaNECgYEA3W4UeprmM2bO64d/iJ9Kk3traLw7c8EdCI+jYeVGOHXsfERQ +ctddKfRCapOBv4wUiry+hFLZm0RJmvYbEHPOs6WDiYd5QeFuMGGBTZ7ahjrtwd3t +2TGUQv6NHmQR/cNIHEG+u0DFi7whPp28vkybAx0HGMG0fyBekGZdY0iYmoECgYEA +3mVOlVYHk9ba1AEsrsErDuSXe/AgQa/E8+YnVek4jqnI7LlfyrHUppFFEcDdUFdB +XVFg+ZP4XXx5p+4EHrbP9NYuWsDm2lY1K2Livb0r+ybBqw0niPjpD6eTYQHdtOcu +ihvZFAWZPL6TJCwhvSvNjOziox5FWnDIFFKuXsqWR9ECgYAfiG1izToF+GX3yUPq +CU+ceTbM2uy3hVnQLvCnraN7hkF02Fa9ZwP6nmnsvhfdaIUP5WLm3A+qMWu/PL0i +F/dUCUF6M/DyihQUnOl+MD9Sg89ZHiftqXSY8jGR14uH4woStyUFHiFbtajmnqV7 +MK4Li/LGWcksyoF+hbPNXMFCIA== +-----END PRIVATE KEY----- +`, func(key string) { + s := miniredis.RunT(t) + store, err := newRedis(&config.RedisConfig{ + Addresses: []string{s.Addr()}, + Tls: config.TlsConfig{ + Enabled: true, + MinVersion: 1.1, + Certificates: []config.CertConfig{ + {Key: key, Cert: cert}, + }, + }, + }, log.NewNullLogger()) + assert.NoError(t, err) + assert.NotNil(t, store) + }) + }) + }) + t.Run("invalid", func(t *testing.T) { + s := miniredis.RunT(t) + store, err := newRedis(&config.RedisConfig{ + Addresses: []string{s.Addr()}, + Tls: config.TlsConfig{ + Enabled: true, + MinVersion: 1.1, + Certificates: []config.CertConfig{ + {Key: "nonexisting", Cert: "nonexisting"}, + }, + }, + }, log.NewNullLogger()) + assert.ErrorContains(t, err, "failed to load certificate and key files") + assert.Nil(t, store) + }) +} diff --git a/sdk/store/file/file.go b/sdk/store/file/file.go index 0b69204..9a8303e 100644 --- a/sdk/store/file/file.go +++ b/sdk/store/file/file.go @@ -36,9 +36,7 @@ type fileStore struct { sdkId string } -var _ store.NotifyingStore = &fileStore{} - -func NewFileStore(sdkId string, conf *config.LocalConfig, reporter status.Reporter, log log.Logger) configcat.ConfigCache { +func NewFileStore(sdkId string, conf *config.LocalConfig, reporter status.Reporter, log log.Logger) store.NotifyingStore { fileLogger := log.WithPrefix("file-store") var watch watcher var err error diff --git a/sdk/store/file/file_test.go b/sdk/store/file/file_test.go index 667d351..d55bc01 100644 --- a/sdk/store/file/file_test.go +++ b/sdk/store/file/file_test.go @@ -14,7 +14,7 @@ import ( func TestFileStore_Existing(t *testing.T) { utils.UseTempFile("", func(path string) { - str := NewFileStore("test", &config.LocalConfig{FilePath: path}, status.NewNullReporter(), log.NewNullLogger()).(*fileStore) + str := NewFileStore("test", &config.LocalConfig{FilePath: path}, status.NewEmptyReporter(), log.NewNullLogger()).(*fileStore) utils.WriteIntoFile(path, `{"f":{"flag":{"v":{"b":true}}},"p":null}`) utils.WithTimeout(2*time.Second, func() { <-str.Modified() @@ -31,7 +31,7 @@ func TestFileStore_Existing(t *testing.T) { func TestFileStore_Existing_Initial(t *testing.T) { utils.UseTempFile(`{"f":{"flag":{"v":{"b":false}}},"p":null}`, func(path string) { - str := NewFileStore("test", &config.LocalConfig{FilePath: path}, status.NewNullReporter(), log.NewNullLogger()).(*fileStore) + str := NewFileStore("test", &config.LocalConfig{FilePath: path}, status.NewEmptyReporter(), log.NewNullLogger()).(*fileStore) res, err := str.Get(context.Background(), "") assert.NoError(t, err) _, _, j, _ := configcatcache.CacheSegmentsFromBytes(res) @@ -42,7 +42,7 @@ func TestFileStore_Existing_Initial(t *testing.T) { func TestFileStore_Existing_Initial_Gets_MalformedJson(t *testing.T) { utils.UseTempFile(`{"f":{"flag":{"v":{"b":false}}},"p":null}`, func(path string) { - str := NewFileStore("test", &config.LocalConfig{FilePath: path}, status.NewNullReporter(), log.NewNullLogger()).(*fileStore) + str := NewFileStore("test", &config.LocalConfig{FilePath: path}, status.NewEmptyReporter(), log.NewNullLogger()).(*fileStore) res, err := str.Get(context.Background(), "") assert.NoError(t, err) _, _, j, _ := configcatcache.CacheSegmentsFromBytes(res) @@ -60,7 +60,7 @@ func TestFileStore_Existing_Initial_Gets_MalformedJson(t *testing.T) { func TestFileStore_Existing_Initial_Notify(t *testing.T) { utils.UseTempFile(`{"f":{"flag":{"v":{"b":false}}},"p":null}`, func(path string) { - str := NewFileStore("test", &config.LocalConfig{FilePath: path}, status.NewNullReporter(), log.NewNullLogger()).(*fileStore) + str := NewFileStore("test", &config.LocalConfig{FilePath: path}, status.NewEmptyReporter(), log.NewNullLogger()).(*fileStore) res, err := str.Get(context.Background(), "") assert.NoError(t, err) _, _, j, _ := configcatcache.CacheSegmentsFromBytes(res) @@ -80,7 +80,7 @@ func TestFileStore_Existing_Initial_Notify(t *testing.T) { func TestFileStore_Existing_Initial_Gets_BadJson(t *testing.T) { utils.UseTempFile(`{"f":{"flag":{"v":{"b":false}}},"p":null}`, func(path string) { - str := NewFileStore("test", &config.LocalConfig{FilePath: path}, status.NewNullReporter(), log.NewNullLogger()).(*fileStore) + str := NewFileStore("test", &config.LocalConfig{FilePath: path}, status.NewEmptyReporter(), log.NewNullLogger()).(*fileStore) res, err := str.Get(context.Background(), "") assert.NoError(t, err) _, _, j, _ := configcatcache.CacheSegmentsFromBytes(res) @@ -98,7 +98,7 @@ func TestFileStore_Existing_Initial_Gets_BadJson(t *testing.T) { func TestFileStore_Existing_Initial_BadJson(t *testing.T) { utils.UseTempFile(`{"k":{"flag":{"v":{"b":false}}},"p":null}`, func(path string) { - str := NewFileStore("test", &config.LocalConfig{FilePath: path}, status.NewNullReporter(), log.NewNullLogger()).(*fileStore) + str := NewFileStore("test", &config.LocalConfig{FilePath: path}, status.NewEmptyReporter(), log.NewNullLogger()).(*fileStore) res, err := str.Get(context.Background(), "") assert.NoError(t, err) _, _, j, _ := configcatcache.CacheSegmentsFromBytes(res) @@ -109,7 +109,7 @@ func TestFileStore_Existing_Initial_BadJson(t *testing.T) { func TestFileStore_Existing_Initial_MalformedJson(t *testing.T) { utils.UseTempFile(`{"k":{"flag`, func(path string) { - str := NewFileStore("test", &config.LocalConfig{FilePath: path}, status.NewNullReporter(), log.NewNullLogger()).(*fileStore) + str := NewFileStore("test", &config.LocalConfig{FilePath: path}, status.NewEmptyReporter(), log.NewNullLogger()).(*fileStore) res, err := str.Get(context.Background(), "") assert.NoError(t, err) _, _, j, _ := configcatcache.CacheSegmentsFromBytes(res) @@ -120,7 +120,7 @@ func TestFileStore_Existing_Initial_MalformedJson(t *testing.T) { func TestFileStore_Stop(t *testing.T) { utils.UseTempFile("", func(path string) { - str := NewFileStore("test", &config.LocalConfig{FilePath: path}, status.NewNullReporter(), log.NewNullLogger()).(*fileStore) + str := NewFileStore("test", &config.LocalConfig{FilePath: path}, status.NewEmptyReporter(), log.NewNullLogger()).(*fileStore) go func() { str.Close() }() @@ -139,7 +139,7 @@ func TestFileStore_Stop(t *testing.T) { } func TestFileStore_NonExisting(t *testing.T) { - str := NewFileStore("test", &config.LocalConfig{FilePath: "nonexisting"}, status.NewNullReporter(), log.NewNullLogger()).(*fileStore) + str := NewFileStore("test", &config.LocalConfig{FilePath: "nonexisting"}, status.NewEmptyReporter(), log.NewNullLogger()).(*fileStore) defer str.Close() res, err := str.Get(context.Background(), "") diff --git a/sdk/store/store.go b/sdk/store/store.go index 6823237..fe92932 100644 --- a/sdk/store/store.go +++ b/sdk/store/store.go @@ -12,21 +12,16 @@ type CacheEntryStore interface { configcat.ConfigCache } -type ClosableStore interface { - Close() -} - type NotifyingStore interface { - EntryStore + CacheEntryStore Notifier - configcat.ConfigCache } type inMemoryStore struct { EntryStore } -func NewInMemoryStorage() configcat.ConfigCache { +func NewInMemoryStorage() CacheEntryStore { return &inMemoryStore{EntryStore: NewEntryStore()} } diff --git a/sdk/user_agent.go b/sdk/user_agent.go index 88f4ba6..40adf63 100644 --- a/sdk/user_agent.go +++ b/sdk/user_agent.go @@ -4,7 +4,7 @@ import ( "net/http" ) -const proxyVersion = "0.4.2" +const proxyVersion = "0.5.0" type userAgentInterceptor struct { http.RoundTripper diff --git a/stream/benchmark_test.go b/stream/benchmark_test.go index bbb6ee5..0ccc623 100644 --- a/stream/benchmark_test.go +++ b/stream/benchmark_test.go @@ -6,7 +6,6 @@ import ( "github.com/configcat/configcat-proxy/internal/testutils" "github.com/configcat/configcat-proxy/log" "github.com/configcat/configcat-proxy/model" - "github.com/configcat/configcat-proxy/sdk" "github.com/configcat/go-sdk/v9/configcattest" "net/http/httptest" "strconv" @@ -24,11 +23,10 @@ func BenchmarkStream(b *testing.B) { srv := httptest.NewServer(&h) defer srv.Close() - ctx := testutils.NewTestSdkContext(&config.SDKConfig{BaseUrl: srv.URL, Key: key}, nil) - client := sdk.NewClient(ctx, log.NewNullLogger()) - defer client.Close() + reg := testutils.NewTestRegistrar(&config.SDKConfig{BaseUrl: srv.URL, Key: key}, nil) + defer reg.Close() - strServer := NewServer(map[string]sdk.Client{"test": client}, nil, log.NewNullLogger(), "test").(*server) + strServer := NewServer(reg, nil, log.NewNullLogger(), "test").(*server) defer strServer.Close() b.ResetTimer() diff --git a/stream/channel.go b/stream/channel.go index 5f1e119..30f0da4 100644 --- a/stream/channel.go +++ b/stream/channel.go @@ -42,7 +42,7 @@ func createChannel(established *connEstablished, sdkClient sdk.Client) channel { } return &allFlagsChannel{connectionHolder: connectionHolder{user: established.user}, lastPayload: payloads} } else { - val, _ := sdkClient.Eval(established.key, established.user) + val := sdkClient.Eval(established.key, established.user) payload := model.PayloadFromEvalData(&val) return &singleFlagChannel{connectionHolder: connectionHolder{user: established.user}, lastPayload: &payload} } @@ -58,8 +58,8 @@ func (af *allFlagsChannel) LastPayload() interface{} { func (sf *singleFlagChannel) Notify(sdkClient sdk.Client, key string) int { sent := 0 - val, err := sdkClient.Eval(key, sf.user) - if err != nil { + val := sdkClient.Eval(key, sf.user) + if val.Error != nil { return 0 } if sf.lastPayload == nil || val.Value != sf.lastPayload.Value { diff --git a/stream/load_test.go b/stream/load_test.go index e4fa105..eef9920 100644 --- a/stream/load_test.go +++ b/stream/load_test.go @@ -6,7 +6,6 @@ import ( "github.com/configcat/configcat-proxy/internal/utils" "github.com/configcat/configcat-proxy/log" "github.com/configcat/configcat-proxy/model" - "github.com/configcat/configcat-proxy/sdk" "github.com/configcat/go-sdk/v9/configcattest" "github.com/stretchr/testify/assert" "net/http/httptest" @@ -28,11 +27,10 @@ func TestStreamServer_Load(t *testing.T) { srv := httptest.NewServer(&h) defer srv.Close() - ctx := testutils.NewTestSdkContext(&config.SDKConfig{BaseUrl: srv.URL, Key: key}, nil) - client := sdk.NewClient(ctx, log.NewNullLogger()) - defer client.Close() + reg := testutils.NewTestRegistrar(&config.SDKConfig{BaseUrl: srv.URL, Key: key}, nil) + defer reg.Close() - strServer := NewServer(map[string]sdk.Client{"test": client}, nil, log.NewNullLogger(), "test").(*server) + strServer := NewServer(reg, nil, log.NewNullLogger(), "test").(*server) defer strServer.Close() t.Run("init", func(t *testing.T) { @@ -49,7 +47,7 @@ func TestStreamServer_Load(t *testing.T) { flags["flag"+strconv.Itoa(i)] = &configcattest.Flag{Default: true} } _ = h.SetFlags(key, flags) - _ = client.Refresh() + _ = reg.GetSdkOrNil("test").Refresh() assert.Equal(t, connCount, len(strServer.GetStreamOrNil("test").(*stream).channels[AllFlagsDiscriminator][0].(*allFlagsChannel).connections)) t.Run("check refresh", func(t *testing.T) { checkConnections(t, strServer) diff --git a/stream/server.go b/stream/server.go index fca0ae5..ceece1b 100644 --- a/stream/server.go +++ b/stream/server.go @@ -16,10 +16,10 @@ type server struct { log log.Logger } -func NewServer(sdkClients map[string]sdk.Client, metrics metrics.Reporter, log log.Logger, serverType string) Server { +func NewServer(sdkRegistrar sdk.Registrar, metrics metrics.Reporter, log log.Logger, serverType string) Server { strLog := log.WithPrefix("stream-server") streams := make(map[string]Stream) - for id, sdkClient := range sdkClients { + for id, sdkClient := range sdkRegistrar.GetAll() { streams[id] = NewStream(id, sdkClient, metrics, strLog, serverType) } return &server{ diff --git a/stream/server_test.go b/stream/server_test.go index 7a7e038..c86b93f 100644 --- a/stream/server_test.go +++ b/stream/server_test.go @@ -8,8 +8,8 @@ import ( ) func TestServer_GetStreamOrNil(t *testing.T) { - clients, _, _ := testutils.NewTestSdkClient(t) - srv := NewServer(clients, nil, log.NewNullLogger(), "test").(*server) + reg, _, _ := testutils.NewTestRegistrarT(t) + srv := NewServer(reg, nil, log.NewNullLogger(), "test").(*server) str := srv.GetStreamOrNil("test") assert.NotNil(t, str) diff --git a/web/api/api.go b/web/api/api.go index 90d102e..379d5b9 100644 --- a/web/api/api.go +++ b/web/api/api.go @@ -20,17 +20,17 @@ type keysResponse struct { } type Server struct { - sdkClients map[string]sdk.Client - config *config.ApiConfig - logger log.Logger + sdkRegistrar sdk.Registrar + config *config.ApiConfig + logger log.Logger } -func NewServer(sdkClients map[string]sdk.Client, config *config.ApiConfig, log log.Logger) *Server { +func NewServer(sdkRegistrar sdk.Registrar, config *config.ApiConfig, log log.Logger) *Server { cdnLogger := log.WithPrefix("api") return &Server{ - sdkClients: sdkClients, - config: config, - logger: cdnLogger, + sdkRegistrar: sdkRegistrar, + config: config, + logger: cdnLogger, } } @@ -41,10 +41,10 @@ func (s *Server) Eval(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), code) return } - eval, err := sdkClient.Eval(evalReq.Key, evalReq.User) - if err != nil { + eval := sdkClient.Eval(evalReq.Key, evalReq.User) + if eval.Error != nil { var errKeyNotFound configcat.ErrKeyNotFound - if errors.As(err, &errKeyNotFound) { + if errors.As(eval.Error, &errKeyNotFound) { http.Error(w, "feature flag or setting with key '"+evalReq.Key+"' not found", http.StatusBadRequest) } else { http.Error(w, "the request failed; please check the logs for more details", http.StatusInternalServerError) @@ -137,8 +137,8 @@ func (s *Server) getSDKClient(ctx context.Context) (sdk.Client, error, int) { if sdkId == "" { return nil, fmt.Errorf("'sdkId' path parameter must be set"), http.StatusNotFound } - sdkClient, ok := s.sdkClients[sdkId] - if !ok { + sdkClient := s.sdkRegistrar.GetSdkOrNil(sdkId) + if sdkClient == nil { return nil, fmt.Errorf("invalid SDK identifier: '%s'", sdkId), http.StatusNotFound } if !sdkClient.IsInValidState() { diff --git a/web/api/api_test.go b/web/api/api_test.go index 50f577b..749219b 100644 --- a/web/api/api_test.go +++ b/web/api/api_test.go @@ -5,7 +5,6 @@ import ( "github.com/configcat/configcat-proxy/internal/testutils" "github.com/configcat/configcat-proxy/internal/utils" "github.com/configcat/configcat-proxy/log" - "github.com/configcat/configcat-proxy/sdk" "github.com/configcat/go-sdk/v9/configcattest" "github.com/stretchr/testify/assert" "net/http" @@ -237,15 +236,7 @@ func TestAPI_Keys(t *testing.T) { } func TestAPI_Refresh(t *testing.T) { - key := configcattest.RandomSDKKey() - var h configcattest.Handler - _ = h.SetFlags(key, map[string]*configcattest.Flag{ - "flag": { - Default: true, - }, - }) - - srv := newServerWithHandler(t, &h, key, config.ApiConfig{Enabled: true}) + srv, h, key := newServerWithHandler(t, config.ApiConfig{Enabled: true}) res := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"key":"flag"}`)) @@ -322,16 +313,14 @@ func TestAPI_WrongSdkId(t *testing.T) { } func TestAPI_WrongSDKState(t *testing.T) { - opts := config.SDKConfig{BaseUrl: "http://localhost", Key: configcattest.RandomSDKKey()} - ctx := testutils.NewTestSdkContext(&opts, &config.CacheConfig{}) - client := sdk.NewClient(ctx, log.NewNullLogger()) - defer client.Close() + reg := testutils.NewTestRegistrar(&config.SDKConfig{BaseUrl: "http://localhost", Key: configcattest.RandomSDKKey()}, nil) + defer reg.Close() t.Run("Eval", func(t *testing.T) { res := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"key":"flag"}`)) - srv := NewServer(map[string]sdk.Client{"test": client}, &config.ApiConfig{Enabled: true}, log.NewNullLogger()) + srv := NewServer(reg, &config.ApiConfig{Enabled: true}, log.NewNullLogger()) testutils.AddSdkIdContextParam(req) srv.Eval(res, req) @@ -342,7 +331,7 @@ func TestAPI_WrongSDKState(t *testing.T) { res := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"key":"flag"}`)) - srv := NewServer(map[string]sdk.Client{"test": client}, &config.ApiConfig{Enabled: true}, log.NewNullLogger()) + srv := NewServer(reg, &config.ApiConfig{Enabled: true}, log.NewNullLogger()) testutils.AddSdkIdContextParam(req) srv.EvalAll(res, req) @@ -353,7 +342,7 @@ func TestAPI_WrongSDKState(t *testing.T) { res := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/", http.NoBody) - srv := NewServer(map[string]sdk.Client{"test": client}, &config.ApiConfig{Enabled: true}, log.NewNullLogger()) + srv := NewServer(reg, &config.ApiConfig{Enabled: true}, log.NewNullLogger()) testutils.AddSdkIdContextParam(req) srv.Keys(res, req) @@ -363,44 +352,24 @@ func TestAPI_WrongSDKState(t *testing.T) { } func newServer(t *testing.T, conf config.ApiConfig) *Server { - client, _, _ := testutils.NewTestSdkClient(t) - return NewServer(client, &conf, log.NewNullLogger()) + reg, _, _ := testutils.NewTestRegistrarT(t) + return NewServer(reg, &conf, log.NewNullLogger()) } -func newServerWithHandler(t *testing.T, h *configcattest.Handler, key string, conf config.ApiConfig) *Server { - _ = h.SetFlags(key, map[string]*configcattest.Flag{ - "flag": { - Default: true, - }, - }) - srv := httptest.NewServer(h) - ctx := testutils.NewTestSdkContext(&config.SDKConfig{BaseUrl: srv.URL, Key: key}, nil) - client := sdk.NewClient(ctx, log.NewNullLogger()) - t.Cleanup(func() { - srv.Close() - client.Close() - }) - return NewServer(map[string]sdk.Client{"test": client}, &conf, log.NewNullLogger()) +func newServerWithHandler(t *testing.T, conf config.ApiConfig) (*Server, *configcattest.Handler, string) { + reg, h, k := testutils.NewTestRegistrarT(t) + return NewServer(reg, &conf, log.NewNullLogger()), h, k } func newErrorServer(t *testing.T, conf config.ApiConfig) *Server { - key := configcattest.RandomSDKKey() - var h configcattest.Handler - srv := httptest.NewServer(&h) - ctx := testutils.NewTestSdkContext(&config.SDKConfig{BaseUrl: srv.URL, Key: key}, nil) - client := sdk.NewClient(ctx, log.NewNullLogger()) - t.Cleanup(func() { - srv.Close() - client.Close() - }) - return NewServer(map[string]sdk.Client{"test": client}, &conf, log.NewNullLogger()) + reg := testutils.NewTestRegistrarTWithErrorServer(t) + return NewServer(reg, &conf, log.NewNullLogger()) } func newOfflineServer(t *testing.T, path string, conf config.ApiConfig) *Server { - ctx := testutils.NewTestSdkContext(&config.SDKConfig{Key: "local", Offline: config.OfflineConfig{Enabled: true, Local: config.LocalConfig{FilePath: path, Polling: true, PollInterval: 30}}}, nil) - client := sdk.NewClient(ctx, log.NewNullLogger()) + reg := testutils.NewTestRegistrar(&config.SDKConfig{Key: "local", Offline: config.OfflineConfig{Enabled: true, Local: config.LocalConfig{FilePath: path, Polling: true, PollInterval: 30}}}, nil) t.Cleanup(func() { - client.Close() + reg.Close() }) - return NewServer(map[string]sdk.Client{"test": client}, &conf, log.NewNullLogger()) + return NewServer(reg, &conf, log.NewNullLogger()) } diff --git a/web/cdnproxy/cdnproxy.go b/web/cdnproxy/cdnproxy.go index 0d421e7..5acdb9f 100644 --- a/web/cdnproxy/cdnproxy.go +++ b/web/cdnproxy/cdnproxy.go @@ -11,17 +11,17 @@ import ( ) type Server struct { - sdkClients map[string]sdk.Client - config *config.CdnProxyConfig - logger log.Logger + sdkRegistrar sdk.Registrar + config *config.CdnProxyConfig + logger log.Logger } -func NewServer(sdkClients map[string]sdk.Client, config *config.CdnProxyConfig, log log.Logger) *Server { +func NewServer(sdkRegistrar sdk.Registrar, config *config.CdnProxyConfig, log log.Logger) *Server { cdnLogger := log.WithPrefix("cdn-proxy") return &Server{ - sdkClients: sdkClients, - config: config, - logger: cdnLogger, + sdkRegistrar: sdkRegistrar, + config: config, + logger: cdnLogger, } } @@ -51,8 +51,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *Server) getSDKClient(ctx context.Context) (sdk.Client, error, int) { vars := httprouter.ParamsFromContext(ctx) sdkId := vars.ByName("sdkId") - sdkClient, ok := s.sdkClients[sdkId] - if !ok { + sdkClient := s.sdkRegistrar.GetSdkOrNil(sdkId) + if sdkClient == nil { return nil, fmt.Errorf("invalid SDK identifier: '%s'", sdkId), http.StatusNotFound } if !sdkClient.IsInValidState() { diff --git a/web/cdnproxy/cdnproxy_test.go b/web/cdnproxy/cdnproxy_test.go index 7f6c66d..ba29075 100644 --- a/web/cdnproxy/cdnproxy_test.go +++ b/web/cdnproxy/cdnproxy_test.go @@ -5,7 +5,6 @@ import ( "github.com/configcat/configcat-proxy/internal/testutils" "github.com/configcat/configcat-proxy/internal/utils" "github.com/configcat/configcat-proxy/log" - "github.com/configcat/configcat-proxy/sdk" "github.com/configcat/go-sdk/v9/configcattest" "github.com/stretchr/testify/assert" "net/http" @@ -133,7 +132,7 @@ func TestProxy_Get(t *testing.T) { Default: false, }, }) - _ = srv.sdkClients["test"].Refresh() + _ = srv.sdkRegistrar.GetSdkOrNil("test").Refresh() res = httptest.NewRecorder() req = &http.Request{Method: http.MethodGet, Header: map[string][]string{}} @@ -166,15 +165,13 @@ func TestProxy_Get(t *testing.T) { assert.Equal(t, http.StatusNotFound, res.Code) }) t.Run("SDK invalid state", func(t *testing.T) { - opts := config.SDKConfig{BaseUrl: "http://localhost", Key: configcattest.RandomSDKKey()} - ctx := testutils.NewTestSdkContext(&opts, &config.CacheConfig{}) - client := sdk.NewClient(ctx, log.NewNullLogger()) - defer client.Close() + reg := testutils.NewTestRegistrar(&config.SDKConfig{BaseUrl: "http://localhost", Key: configcattest.RandomSDKKey()}, nil) + defer reg.Close() res := httptest.NewRecorder() req := &http.Request{Method: http.MethodGet} - srv := NewServer(map[string]sdk.Client{"test": client}, &config.CdnProxyConfig{Enabled: true}, log.NewNullLogger()) + srv := NewServer(reg, &config.CdnProxyConfig{Enabled: true}, log.NewNullLogger()) testutils.AddSdkIdContextParam(req) srv.ServeHTTP(res, req) @@ -184,33 +181,24 @@ func TestProxy_Get(t *testing.T) { } func newServer(t *testing.T, proxyConfig config.CdnProxyConfig) *Server { - client, _, _ := testutils.NewTestSdkClient(t) - return NewServer(client, &proxyConfig, log.NewNullLogger()) + reg, _, _ := testutils.NewTestRegistrarT(t) + return NewServer(reg, &proxyConfig, log.NewNullLogger()) } func newServerWithHandler(t *testing.T, proxyConfig config.CdnProxyConfig) (*Server, *configcattest.Handler, string) { - client, h, k := testutils.NewTestSdkClient(t) - return NewServer(client, &proxyConfig, log.NewNullLogger()), h, k + reg, h, k := testutils.NewTestRegistrarT(t) + return NewServer(reg, &proxyConfig, log.NewNullLogger()), h, k } func newErrorServer(t *testing.T, proxyConfig config.CdnProxyConfig) *Server { - key := configcattest.RandomSDKKey() - var h configcattest.Handler - srv := httptest.NewServer(&h) - ctx := testutils.NewTestSdkContext(&config.SDKConfig{BaseUrl: srv.URL, Key: key}, nil) - client := sdk.NewClient(ctx, log.NewNullLogger()) - t.Cleanup(func() { - srv.Close() - client.Close() - }) - return NewServer(map[string]sdk.Client{"test": client}, &proxyConfig, log.NewNullLogger()) + reg := testutils.NewTestRegistrarTWithErrorServer(t) + return NewServer(reg, &proxyConfig, log.NewNullLogger()) } func newOfflineServer(t *testing.T, path string, proxyConfig config.CdnProxyConfig) *Server { - ctx := testutils.NewTestSdkContext(&config.SDKConfig{Key: "local", Offline: config.OfflineConfig{Enabled: true, Local: config.LocalConfig{FilePath: path}}}, nil) - client := sdk.NewClient(ctx, log.NewNullLogger()) + reg := testutils.NewTestRegistrar(&config.SDKConfig{Key: "local", Offline: config.OfflineConfig{Enabled: true, Local: config.LocalConfig{FilePath: path}}}, nil) t.Cleanup(func() { - client.Close() + reg.Close() }) - return NewServer(map[string]sdk.Client{"test": client}, &proxyConfig, log.NewNullLogger()) + return NewServer(reg, &proxyConfig, log.NewNullLogger()) } diff --git a/web/router.go b/web/router.go index eeca6c3..ab50a0b 100644 --- a/web/router.go +++ b/web/router.go @@ -25,7 +25,7 @@ type HttpRouter struct { metrics metrics.Reporter } -func NewRouter(sdkClients map[string]sdk.Client, metrics metrics.Reporter, reporter status.Reporter, conf *config.HttpConfig, log log.Logger) *HttpRouter { +func NewRouter(sdkRegistrar sdk.Registrar, metrics metrics.Reporter, reporter status.Reporter, conf *config.HttpConfig, log log.Logger) *HttpRouter { httpLog := log.WithLevel(conf.Log.GetLevel()).WithPrefix("http") r := &HttpRouter{ @@ -37,16 +37,16 @@ func NewRouter(sdkClients map[string]sdk.Client, metrics metrics.Reporter, repor metrics: metrics, } if conf.Sse.Enabled { - r.setupSSERoutes(&conf.Sse, sdkClients, httpLog) + r.setupSSERoutes(&conf.Sse, sdkRegistrar, httpLog) } if conf.Webhook.Enabled { - r.setupWebhookRoutes(&conf.Webhook, sdkClients, httpLog) + r.setupWebhookRoutes(&conf.Webhook, sdkRegistrar, httpLog) } if conf.CdnProxy.Enabled { - r.setupCDNProxyRoutes(&conf.CdnProxy, sdkClients, httpLog) + r.setupCDNProxyRoutes(&conf.CdnProxy, sdkRegistrar, httpLog) } if conf.Api.Enabled { - r.setupAPIRoutes(&conf.Api, sdkClients, httpLog) + r.setupAPIRoutes(&conf.Api, sdkRegistrar, httpLog) } if conf.Status.Enabled { r.setupStatusRoutes(reporter, httpLog) @@ -64,8 +64,8 @@ func (s *HttpRouter) Close() { } } -func (s *HttpRouter) setupSSERoutes(conf *config.SseConfig, sdkClients map[string]sdk.Client, l log.Logger) { - s.sseServer = sse.NewServer(sdkClients, s.metrics, conf, l) +func (s *HttpRouter) setupSSERoutes(conf *config.SseConfig, sdkRegistrar sdk.Registrar, l log.Logger) { + s.sseServer = sse.NewServer(sdkRegistrar, s.metrics, conf, l) endpoints := []endpoint{ {path: "/sse/:sdkId/eval/:data", handler: http.HandlerFunc(s.sseServer.SingleFlag), method: http.MethodGet}, {path: "/sse/:sdkId/eval-all/:data", handler: http.HandlerFunc(s.sseServer.AllFlags), method: http.MethodGet}, @@ -88,8 +88,8 @@ func (s *HttpRouter) setupSSERoutes(conf *config.SseConfig, sdkClients map[strin l.Reportf("SSE enabled, accepting requests on path: /sse/:sdkId/*") } -func (s *HttpRouter) setupWebhookRoutes(conf *config.WebhookConfig, sdkClients map[string]sdk.Client, l log.Logger) { - s.webhookServer = webhook.NewServer(sdkClients, l) +func (s *HttpRouter) setupWebhookRoutes(conf *config.WebhookConfig, sdkRegistrar sdk.Registrar, l log.Logger) { + s.webhookServer = webhook.NewServer(sdkRegistrar, l) path := "/hook/:sdkId" handler := http.HandlerFunc(s.webhookServer.ServeHTTP) if conf.Auth.User != "" && conf.Auth.Password != "" { @@ -109,8 +109,8 @@ func (s *HttpRouter) setupWebhookRoutes(conf *config.WebhookConfig, sdkClients m l.Reportf("webhook enabled, accepting requests on path: %s", path) } -func (s *HttpRouter) setupCDNProxyRoutes(conf *config.CdnProxyConfig, sdkClients map[string]sdk.Client, l log.Logger) { - s.cdnProxyServer = cdnproxy.NewServer(sdkClients, conf, l) +func (s *HttpRouter) setupCDNProxyRoutes(conf *config.CdnProxyConfig, sdkRegistrar sdk.Registrar, l log.Logger) { + s.cdnProxyServer = cdnproxy.NewServer(sdkRegistrar, conf, l) path := "/configuration-files/configcat-proxy/:sdkId/config_v6.json" handler := mware.AutoOptions(mware.GZip(s.cdnProxyServer.ServeHTTP)) if len(conf.Headers) > 0 { @@ -147,8 +147,8 @@ type endpoint struct { path string } -func (s *HttpRouter) setupAPIRoutes(conf *config.ApiConfig, sdkClients map[string]sdk.Client, l log.Logger) { - s.apiServer = api.NewServer(sdkClients, conf, l) +func (s *HttpRouter) setupAPIRoutes(conf *config.ApiConfig, sdkRegistrar sdk.Registrar, l log.Logger) { + s.apiServer = api.NewServer(sdkRegistrar, conf, l) endpoints := []endpoint{ {path: "/api/:sdkId/eval", handler: mware.GZip(s.apiServer.Eval), method: http.MethodPost}, {path: "/api/:sdkId/eval-all", handler: mware.GZip(s.apiServer.EvalAll), method: http.MethodPost}, diff --git a/web/router_api_test.go b/web/router_api_test.go index 5fcfbe3..7622aee 100644 --- a/web/router_api_test.go +++ b/web/router_api_test.go @@ -425,6 +425,6 @@ func TestAPI_Refresh_Headers(t *testing.T) { } func newAPIRouter(t *testing.T, conf config.ApiConfig) *HttpRouter { - client, _, _ := testutils.NewTestSdkClient(t) - return NewRouter(client, nil, status.NewNullReporter(), &config.HttpConfig{Api: conf}, log.NewNullLogger()) + reg, _, _ := testutils.NewTestRegistrarT(t) + return NewRouter(reg, nil, status.NewEmptyReporter(), &config.HttpConfig{Api: conf}, log.NewNullLogger()) } diff --git a/web/router_cdnproxy_test.go b/web/router_cdnproxy_test.go index 6cf96c7..0168fe1 100644 --- a/web/router_cdnproxy_test.go +++ b/web/router_cdnproxy_test.go @@ -132,6 +132,6 @@ func TestCDNProxy_Get_Body_GZip(t *testing.T) { } func newCDNProxyRouter(t *testing.T, conf config.CdnProxyConfig) *HttpRouter { - client, _, _ := testutils.NewTestSdkClient(t) - return NewRouter(client, nil, status.NewNullReporter(), &config.HttpConfig{CdnProxy: conf}, log.NewNullLogger()) + reg, _, _ := testutils.NewTestRegistrarT(t) + return NewRouter(reg, nil, status.NewEmptyReporter(), &config.HttpConfig{CdnProxy: conf}, log.NewNullLogger()) } diff --git a/web/router_sse_test.go b/web/router_sse_test.go index a2fd6f5..ba58a6c 100644 --- a/web/router_sse_test.go +++ b/web/router_sse_test.go @@ -184,6 +184,6 @@ func TestSSE_EvalAllFlags_Not_Allowed_Methods(t *testing.T) { } func newSSERouter(t *testing.T, conf config.SseConfig) *HttpRouter { - client, _, _ := testutils.NewTestSdkClient(t) - return NewRouter(client, nil, status.NewNullReporter(), &config.HttpConfig{Sse: conf}, log.NewNullLogger()) + reg, _, _ := testutils.NewTestRegistrarT(t) + return NewRouter(reg, nil, status.NewEmptyReporter(), &config.HttpConfig{Sse: conf}, log.NewNullLogger()) } diff --git a/web/router_status_test.go b/web/router_status_test.go index ae05389..e4276bb 100644 --- a/web/router_status_test.go +++ b/web/router_status_test.go @@ -8,8 +8,6 @@ import ( "github.com/configcat/configcat-proxy/internal/testutils" "github.com/configcat/configcat-proxy/internal/utils" "github.com/configcat/configcat-proxy/log" - "github.com/configcat/configcat-proxy/sdk" - "github.com/configcat/go-sdk/v9/configcattest" "github.com/stretchr/testify/assert" "io" "net/http" @@ -74,26 +72,11 @@ func TestStatus_Not_Allowed_Methods(t *testing.T) { } func newStatusRouter(t *testing.T) *HttpRouter { - key := configcattest.RandomSDKKey() - var h configcattest.Handler - _ = h.SetFlags(key, map[string]*configcattest.Flag{ - "flag": { - Default: true, - }, - }) - srv := httptest.NewServer(&h) - opts := config.SDKConfig{BaseUrl: srv.URL, Key: key} - ctx := testutils.NewTestSdkContext(&opts, nil) - conf := config.Config{SDKs: map[string]*config.SDKConfig{"test": &opts}} - reporter := status.NewReporter(&conf) - ctx.StatusReporter = reporter - client := sdk.NewClient(ctx, log.NewNullLogger()) + reporter := status.NewEmptyReporter() + reg, _, _ := testutils.NewTestRegistrarTWithStatusReporter(t, reporter) + client := reg.GetSdkOrNil("test") utils.WithTimeout(2*time.Second, func() { <-client.Ready() }) - t.Cleanup(func() { - srv.Close() - client.Close() - }) - return NewRouter(map[string]sdk.Client{"test": client}, nil, reporter, &config.HttpConfig{Status: config.StatusConfig{Enabled: true}}, log.NewNullLogger()) + return NewRouter(reg, nil, reporter, &config.HttpConfig{Status: config.StatusConfig{Enabled: true}}, log.NewNullLogger()) } diff --git a/web/router_webhook_test.go b/web/router_webhook_test.go index e97e201..3a5fd5b 100644 --- a/web/router_webhook_test.go +++ b/web/router_webhook_test.go @@ -99,6 +99,6 @@ func TestWebhook_NotAllowed(t *testing.T) { } func newWebhookRouter(t *testing.T, conf config.WebhookConfig) *HttpRouter { - clients, _, _ := testutils.NewTestSdkClient(t) - return NewRouter(clients, nil, status.NewNullReporter(), &config.HttpConfig{Webhook: conf}, log.NewNullLogger()) + reg, _, _ := testutils.NewTestRegistrarT(t) + return NewRouter(reg, nil, status.NewEmptyReporter(), &config.HttpConfig{Webhook: conf}, log.NewNullLogger()) } diff --git a/web/server.go b/web/server.go index 7994911..53d943c 100644 --- a/web/server.go +++ b/web/server.go @@ -2,7 +2,6 @@ package web import ( "context" - "crypto/tls" "errors" "fmt" "github.com/configcat/configcat-proxy/config" @@ -26,16 +25,10 @@ func NewServer(handler http.Handler, log log.Logger, conf *config.Config, errorC Handler: handler, } if conf.Tls.Enabled { - t := &tls.Config{ - MinVersion: conf.Tls.GetVersion(), - } - for _, c := range conf.Tls.Certificates { - if cert, err := tls.LoadX509KeyPair(c.Cert, c.Key); err == nil { - t.Certificates = append(t.Certificates, cert) - } else { - httpLog.Errorf("failed to load the certificate and key pair: %s", err) - return nil, err - } + t, err := conf.Tls.LoadTlsOptions() + if err != nil { + httpLog.Errorf("failed to configure TLS for the HTTP server: %s", err) + return nil, err } httpServer.TLSConfig = t httpLog.Reportf("using TLS version: %.1f", conf.Tls.MinVersion) @@ -74,7 +67,7 @@ func (s *Server) Shutdown() { err := s.httpServer.Shutdown(ctx) if err != nil { - s.log.Errorf("shutdown error: %v", err) + s.log.Errorf("shutdown error: %s", err) } s.log.Reportf("server shutdown complete") } diff --git a/web/sse/sse.go b/web/sse/sse.go index b53950f..ea45286 100644 --- a/web/sse/sse.go +++ b/web/sse/sse.go @@ -22,10 +22,10 @@ type Server struct { stop chan struct{} } -func NewServer(sdkClients map[string]sdk.Client, metrics metrics.Reporter, conf *config.SseConfig, logger log.Logger) *Server { +func NewServer(sdkRegistrar sdk.Registrar, metrics metrics.Reporter, conf *config.SseConfig, logger log.Logger) *Server { sseLog := logger.WithLevel(conf.Log.GetLevel()).WithPrefix("sse") return &Server{ - streamServer: stream.NewServer(sdkClients, metrics, sseLog, "sse"), + streamServer: stream.NewServer(sdkRegistrar, metrics, sseLog, "sse"), logger: sseLog, config: conf, stop: make(chan struct{}), diff --git a/web/sse/sse_test.go b/web/sse/sse_test.go index 30e5dea..70faa3e 100644 --- a/web/sse/sse_test.go +++ b/web/sse/sse_test.go @@ -6,7 +6,6 @@ import ( "github.com/configcat/configcat-proxy/config" "github.com/configcat/configcat-proxy/internal/testutils" "github.com/configcat/configcat-proxy/log" - "github.com/configcat/configcat-proxy/sdk" "github.com/configcat/go-sdk/v9/configcattest" "github.com/julienschmidt/httprouter" "github.com/stretchr/testify/assert" @@ -80,14 +79,12 @@ func TestSSE_NonExisting_Flag(t *testing.T) { } func TestSSE_SDK_InvalidState(t *testing.T) { - opts := config.SDKConfig{BaseUrl: "http://localhost", Key: configcattest.RandomSDKKey()} - sdkCtx := testutils.NewTestSdkContext(&opts, &config.CacheConfig{}) - client := sdk.NewClient(sdkCtx, log.NewNullLogger()) - defer client.Close() + reg := testutils.NewTestRegistrar(&config.SDKConfig{BaseUrl: "http://localhost", Key: configcattest.RandomSDKKey()}, nil) + defer reg.Close() req := httptest.NewRequest(http.MethodGet, "/", nil) - srv := NewServer(map[string]sdk.Client{"test": client}, nil, &config.SseConfig{Enabled: true}, log.NewNullLogger()) + srv := NewServer(reg, nil, &config.SseConfig{Enabled: true}, log.NewNullLogger()) ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() @@ -216,6 +213,6 @@ func TestSSE_Get_All_User_Invalid(t *testing.T) { } func newServer(t *testing.T, conf *config.SseConfig) *Server { - client, _, _ := testutils.NewTestSdkClient(t) - return NewServer(client, nil, conf, log.NewNullLogger()) + reg, _, _ := testutils.NewTestRegistrarT(t) + return NewServer(reg, nil, conf, log.NewNullLogger()) } diff --git a/web/webhook/webhook.go b/web/webhook/webhook.go index 8bf2bc6..bef7c24 100644 --- a/web/webhook/webhook.go +++ b/web/webhook/webhook.go @@ -19,15 +19,15 @@ const idHeader = "X-ConfigCat-Webhook-ID" const timestampHeader = "X-ConfigCat-Webhook-Timestamp" type Server struct { - sdkClients map[string]sdk.Client - logger log.Logger + sdkRegistrar sdk.Registrar + logger log.Logger } -func NewServer(sdkClients map[string]sdk.Client, log log.Logger) *Server { +func NewServer(sdkRegistrar sdk.Registrar, log log.Logger) *Server { whLogger := log.WithPrefix("webhook") return &Server{ - sdkClients: sdkClients, - logger: whLogger, + sdkRegistrar: sdkRegistrar, + logger: whLogger, } } @@ -38,8 +38,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, "'sdkId' path parameter must be set", http.StatusBadRequest) return } - sdkClient, ok := s.sdkClients[sdkId] - if !ok { + sdkClient := s.sdkRegistrar.GetSdkOrNil(sdkId) + if sdkClient == nil { http.Error(w, "SDK not found for identifier: '"+sdkId+"'", http.StatusNotFound) return } diff --git a/web/webhook/webhook_test.go b/web/webhook/webhook_test.go index 213afe0..73e9061 100644 --- a/web/webhook/webhook_test.go +++ b/web/webhook/webhook_test.go @@ -21,11 +21,8 @@ import ( ) func TestWebhook_Signature_Bad(t *testing.T) { - key := configcattest.RandomSDKKey() - var h = &configcattest.Handler{} - _ = h.SetFlags(key, map[string]*configcattest.Flag{"flag": {Default: true}}) - clients := newClient(t, h, key, "test-key", 300) - srv := NewServer(clients, log.NewNullLogger()) + reg, _, _ := newRegistrar(t, "test-key", 300) + srv := NewServer(reg, log.NewNullLogger()) t.Run("headers missing", func(t *testing.T) { res := httptest.NewRecorder() @@ -58,11 +55,8 @@ func TestWebhook_Signature_Bad(t *testing.T) { func TestWebhook_Signature_Ok(t *testing.T) { t.Run("signature OK GET", func(t *testing.T) { - key := configcattest.RandomSDKKey() - var h = &configcattest.Handler{} - _ = h.SetFlags(key, map[string]*configcattest.Flag{"flag": {Default: true}}) - clients := newClient(t, h, key, "test-key", 300) - srv := NewServer(clients, log.NewNullLogger()) + reg, h, key := newRegistrar(t, "test-key", 300) + srv := NewServer(reg, log.NewNullLogger()) res := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/", nil) @@ -76,9 +70,9 @@ func TestWebhook_Signature_Ok(t *testing.T) { req.Header.Set("X-ConfigCat-Webhook-ID", id) req.Header.Set("X-ConfigCat-Webhook-Timestamp", timestamp) testutils.AddSdkIdContextParam(req) - sub := clients["test"].SubConfigChanged("hook1") + sub := reg.GetSdkOrNil("test").SubConfigChanged("hook1") utils.WithTimeout(2*time.Second, func() { - <-clients["test"].Ready() + <-reg.GetSdkOrNil("test").Ready() }) // wait for the SDK to do the initialization _ = h.SetFlags(key, map[string]*configcattest.Flag{"flag": {Default: false}}) srv.ServeHTTP(res, req) @@ -88,11 +82,8 @@ func TestWebhook_Signature_Ok(t *testing.T) { assert.Equal(t, http.StatusOK, res.Code) }) t.Run("signature OK POST", func(t *testing.T) { - key := configcattest.RandomSDKKey() - var h = &configcattest.Handler{} - _ = h.SetFlags(key, map[string]*configcattest.Flag{"flag": {Default: true}}) - clients := newClient(t, h, key, "test-key", 300) - srv := NewServer(clients, log.NewNullLogger()) + reg, h, key := newRegistrar(t, "test-key", 300) + srv := NewServer(reg, log.NewNullLogger()) id := "1" timestamp := strconv.FormatInt(time.Now().Unix(), 10) @@ -107,9 +98,9 @@ func TestWebhook_Signature_Ok(t *testing.T) { req.Header.Set("X-ConfigCat-Webhook-ID", id) req.Header.Set("X-ConfigCat-Webhook-Timestamp", timestamp) testutils.AddSdkIdContextParam(req) - sub := clients["test"].SubConfigChanged("hook1") + sub := reg.GetSdkOrNil("test").SubConfigChanged("hook1") utils.WithTimeout(2*time.Second, func() { - <-clients["test"].Ready() + <-reg.GetSdkOrNil("test").Ready() }) // wait for the SDK to do the initialization _ = h.SetFlags(key, map[string]*configcattest.Flag{"flag": {Default: false}}) srv.ServeHTTP(res, req) @@ -121,11 +112,8 @@ func TestWebhook_Signature_Ok(t *testing.T) { } func TestWebhook_Signature_Replay_Reject(t *testing.T) { - key := configcattest.RandomSDKKey() - var h = &configcattest.Handler{} - _ = h.SetFlags(key, map[string]*configcattest.Flag{"flag": {Default: true}}) - clients := newClient(t, h, key, "test-key", 1) - srv := NewServer(clients, log.NewNullLogger()) + reg, _, _ := newRegistrar(t, "test-key", 1) + srv := NewServer(reg, log.NewNullLogger()) id := "1" timestamp := strconv.FormatInt(time.Now().Unix(), 10) @@ -145,14 +133,15 @@ func TestWebhook_Signature_Replay_Reject(t *testing.T) { assert.Equal(t, http.StatusBadRequest, res.Code) } -func newClient(t *testing.T, h *configcattest.Handler, key string, signingKey string, validFor int) map[string]sdk.Client { +func newRegistrar(t *testing.T, signingKey string, validFor int) (sdk.Registrar, *configcattest.Handler, string) { + key := configcattest.RandomSDKKey() + var h = &configcattest.Handler{} + _ = h.SetFlags(key, map[string]*configcattest.Flag{"flag": {Default: true}}) srv := httptest.NewServer(h) - sdkConf := &config.SDKConfig{BaseUrl: srv.URL, Key: key, WebhookSigningKey: signingKey, WebhookSignatureValidFor: validFor} - ctx := testutils.NewTestSdkContext(sdkConf, nil) - client := sdk.NewClient(ctx, log.NewNullLogger()) + reg := testutils.NewTestRegistrar(&config.SDKConfig{BaseUrl: srv.URL, Key: key, WebhookSigningKey: signingKey, WebhookSignatureValidFor: validFor}, nil) t.Cleanup(func() { srv.Close() - client.Close() + reg.Close() }) - return map[string]sdk.Client{"test": client} + return reg, h, key }