diff --git a/splitio/commitversion.go b/splitio/commitversion.go index 1c2d3396..dfb3f8ed 100644 --- a/splitio/commitversion.go +++ b/splitio/commitversion.go @@ -5,4 +5,4 @@ This file is created automatically, please do not edit */ // CommitVersion is the version of the last commit previous to release -const CommitVersion = "765bffc" +const CommitVersion = "eb1a57b" diff --git a/splitio/producer/conf/sections.go b/splitio/producer/conf/sections.go index 0436ae5d..139225d5 100644 --- a/splitio/producer/conf/sections.go +++ b/splitio/producer/conf/sections.go @@ -17,7 +17,7 @@ type Main struct { Integrations conf.Integrations `json:"integrations" s-nested:"true"` Logging conf.Logging `json:"logging" s-nested:"true"` Healthcheck Healthcheck `json:"healthcheck" s-nested:"true"` - SpecVersion string + SpecVersion string `json:"specVersion" s-cli:"spec-version" s-def:"1.1" s-desc:"Spec version for flags"` } // BuildAdvancedConfig generates a commons-compatible advancedconfig with default + overriden parameters diff --git a/splitio/producer/initialization.go b/splitio/producer/initialization.go index 3cd5b5f8..9c238f5b 100644 --- a/splitio/producer/initialization.go +++ b/splitio/producer/initialization.go @@ -46,7 +46,6 @@ const ( // Start initialize the producer mode func Start(logger logging.LoggerInterface, cfg *conf.Main) error { // Getting initial config data - cfg.SpecVersion = "1.0" // @TODO Until is implemented advanced := cfg.BuildAdvancedConfig() advanced.AuthSpecVersion = cfg.SpecVersion advanced.FlagsSpecVersion = cfg.SpecVersion diff --git a/splitio/producer/initialization_test.go b/splitio/producer/initialization_test.go index d2e1221e..12554bca 100644 --- a/splitio/producer/initialization_test.go +++ b/splitio/producer/initialization_test.go @@ -126,7 +126,8 @@ func TestSanitizeRedisWithForcedCleanup(t *testing.T) { func TestSanitizeRedisWithRedisEqualApiKey(t *testing.T) { cfg := getDefaultConf() - cfg.Apikey = "djasghdhjasfganyr73dsah9" + cfg.Apikey = "983564etyrudhijfgknf9i08euh" + cfg.SpecVersion = "1.0" logger := logging.NewLogger(nil) @@ -139,9 +140,10 @@ func TestSanitizeRedisWithRedisEqualApiKey(t *testing.T) { if err != nil { t.Error("It should be nil") } + hash := util.HashAPIKey(cfg.Apikey + cfg.SpecVersion + strings.Join(cfg.FlagSetsFilter, "::")) redisClient.Set("SPLITIO.test1", "123", 0) - redisClient.Set("SPLITIO.hash", "3376912823", 0) + redisClient.Set("SPLITIO.hash", hash, 0) miscStorage := predis.NewMiscStorage(redisClient, logger) err = sanitizeRedis(cfg, miscStorage, logger) @@ -155,16 +157,18 @@ func TestSanitizeRedisWithRedisEqualApiKey(t *testing.T) { } val, _ = redisClient.Get("SPLITIO.hash") - if val != "3376912823" { + if val != strconv.FormatUint(uint64(hash), 10) { t.Error("Incorrect apikey hash set in redis after sanitization operation.") } + redisClient.Del("SPLITIO.hash") redisClient.Del("SPLITIO.test1") } func TestSanitizeRedisWithRedisDifferentApiKey(t *testing.T) { cfg := getDefaultConf() cfg.Apikey = "983564etyrudhijfgknf9i08euh" + cfg.SpecVersion = "1.0" logger := logging.NewLogger(nil) @@ -177,9 +181,12 @@ func TestSanitizeRedisWithRedisDifferentApiKey(t *testing.T) { if err != nil { t.Error("It should be nil") } + hash := util.HashAPIKey("djasghdhjasfganyr73dsah9" + cfg.SpecVersion + strings.Join(cfg.FlagSetsFilter, "::")) redisClient.Set("SPLITIO.test1", "123", 0) - redisClient.Set("SPLITIO.hash", "3376912823", 0) + redisClient.Set("SPLITIO.hash", "3216514561", 0) + + hash = util.HashAPIKey(cfg.Apikey + cfg.SpecVersion + strings.Join(cfg.FlagSetsFilter, "::")) miscStorage := predis.NewMiscStorage(redisClient, logger) err = sanitizeRedis(cfg, miscStorage, logger) @@ -193,20 +200,22 @@ func TestSanitizeRedisWithRedisDifferentApiKey(t *testing.T) { } val, _ = redisClient.Get("SPLITIO.hash") - if val != "1497926959" { - t.Error("Incorrect apikey hash set in redis after sanitization operation.") + if val != strconv.FormatUint(uint64(hash), 10) { + t.Error("Incorrect apikey hash set in redis after sanitization operation.", val) } + redisClient.Del("SPLITIO.hash") redisClient.Del("SPLITIO.test1") } func TestSanitizeRedisWithForcedCleanupByFlagSets(t *testing.T) { cfg := getDefaultConf() + cfg.SpecVersion = "1.0" cfg.Apikey = "983564etyrudhijfgknf9i08euh" cfg.Initialization.ForceFreshStartup = true cfg.FlagSetsFilter = []string{"flagset1", "flagset2"} - hash := util.HashAPIKey(cfg.Apikey + strings.Join(cfg.FlagSetsFilter, "::")) + hash := util.HashAPIKey(cfg.Apikey + cfg.SpecVersion + strings.Join(cfg.FlagSetsFilter, "::")) logger := logging.NewLogger(nil) diff --git a/splitio/proxy/caching/caching.go b/splitio/proxy/caching/caching.go index 89d153ec..7752ac2c 100644 --- a/splitio/proxy/caching/caching.go +++ b/splitio/proxy/caching/caching.go @@ -51,23 +51,25 @@ func MakeProxyCache() *gincache.Middleware { return gincache.New(&gincache.Options{ SuccessfulOnly: true, // we're not interested in caching non-200 responses Size: cacheSize, - KeyFactory: func(ctx *gin.Context) string { - - var encodingPrefix string - if strings.Contains(ctx.Request.Header.Get("Accept-Encoding"), "gzip") { - encodingPrefix = "gzip::" - } - - if strings.HasPrefix(ctx.Request.URL.Path, "/api/auth") || strings.HasPrefix(ctx.Request.URL.Path, "/api/v2/auth") { - // For auth requests, since we don't support streaming yet, we only need a single entry in the table, - // so we strip the query-string which contains the user-list - return encodingPrefix + ctx.Request.URL.Path - } - return encodingPrefix + ctx.Request.URL.Path + ctx.Request.URL.RawQuery - }, + KeyFactory: keyFactoryFN, // we make each request handler responsible for generating the surrogates. // this way we can use segment names as surrogates for mysegments & segment changes // with a lot less work SurrogateFactory: func(ctx *gin.Context) []string { return ctx.GetStringSlice(SurrogateContextKey) }, }) } + +func keyFactoryFN(ctx *gin.Context) string { + + var encodingPrefix string + if strings.Contains(ctx.Request.Header.Get("Accept-Encoding"), "gzip") { + encodingPrefix = "gzip::" + } + + if strings.HasPrefix(ctx.Request.URL.Path, "/api/auth") || strings.HasPrefix(ctx.Request.URL.Path, "/api/v2/auth") { + // For auth requests, since we don't support streaming yet, we only need a single entry in the table, + // so we strip the query-string which contains the user-list + return encodingPrefix + ctx.Request.URL.Path + } + return encodingPrefix + ctx.Request.URL.Path + ctx.Request.URL.RawQuery +} diff --git a/splitio/proxy/caching/caching_test.go b/splitio/proxy/caching/caching_test.go index 5d46574b..ee73138e 100644 --- a/splitio/proxy/caching/caching_test.go +++ b/splitio/proxy/caching/caching_test.go @@ -1,12 +1,26 @@ package caching import ( + "net/http" + "net/url" "testing" + "github.com/gin-gonic/gin" "github.com/splitio/go-split-commons/v5/dtos" "github.com/stretchr/testify/assert" ) +func TestCacheKeysDoNotOverlap(t *testing.T) { + + url1, _ := url.Parse("http://proxy.split.io/api/spitChanges?since=-1") + c1 := &gin.Context{Request: &http.Request{URL: url1}} + + url2, _ := url.Parse("http://proxy.split.io/api/spitChanges?s=1.1&since=-1") + c2 := &gin.Context{Request: &http.Request{URL: url2}} + + assert.NotEqual(t, keyFactoryFN(c1), keyFactoryFN(c2)) +} + func TestSegmentSurrogates(t *testing.T) { assert.Equal(t, segmentPrefix+"segment1", MakeSurrogateForSegmentChanges("segment1")) assert.NotEqual(t, MakeSurrogateForSegmentChanges("segment1"), MakeSurrogateForSegmentChanges("segment2")) diff --git a/splitio/proxy/conf/sections.go b/splitio/proxy/conf/sections.go index 3f7d796a..7c9d13e6 100644 --- a/splitio/proxy/conf/sections.go +++ b/splitio/proxy/conf/sections.go @@ -20,6 +20,7 @@ type Main struct { Logging conf.Logging `json:"logging" s-nested:"true"` Healthcheck Healthcheck `json:"healthcheck" s-nested:"true"` Observability Observability `json:"observability" s-nested:"true"` + SpecVersion string `json:"specVersion" s-cli:"spec-version" s-def:"1.1" s-desc:"Spec version for flags"` } // BuildAdvancedConfig generates a commons-compatible advancedconfig with default + overriden parameters diff --git a/splitio/proxy/controllers/sdk.go b/splitio/proxy/controllers/sdk.go index 5487d76d..86c40d10 100644 --- a/splitio/proxy/controllers/sdk.go +++ b/splitio/proxy/controllers/sdk.go @@ -9,7 +9,10 @@ import ( "github.com/gin-gonic/gin" "github.com/splitio/go-split-commons/v5/dtos" + "github.com/splitio/go-split-commons/v5/engine/grammar" + "github.com/splitio/go-split-commons/v5/engine/grammar/matchers" "github.com/splitio/go-split-commons/v5/service" + "github.com/splitio/go-split-commons/v5/service/api/specs" "github.com/splitio/go-toolkit/v5/logging" "golang.org/x/exp/slices" @@ -18,6 +21,10 @@ import ( "github.com/splitio/split-synchronizer/v5/splitio/proxy/storage" ) +const ( + labelUnsupportedMatcher = "targeting rule type unsupported by sdk" +) + // SdkServerController bundles all request handler for sdk-server apis type SdkServerController struct { logger logging.LoggerInterface @@ -25,6 +32,7 @@ type SdkServerController struct { proxySplitStorage storage.ProxySplitStorage proxySegmentStorage storage.ProxySegmentStorage fsmatcher flagsets.FlagSetMatcher + versionFilter specs.SplitVersionFilter } // NewSdkServerController instantiates a new sdk server controller @@ -42,6 +50,7 @@ func NewSdkServerController( proxySplitStorage: proxySplitStorage, proxySegmentStorage: proxySegmentStorage, fsmatcher: fsmatcher, + versionFilter: specs.NewSplitVersionFilter(), } } @@ -77,6 +86,13 @@ func (c *SdkServerController) SplitChanges(ctx *gin.Context) { ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } + + spec, _ := ctx.GetQuery("s") + if spec != specs.FLAG_V1_1 { + spec = specs.FLAG_V1_0 + } + splits.Splits = c.patchUnsupportedMatchers(splits.Splits, spec) + ctx.JSON(http.StatusOK, splits) ctx.Set(caching.SurrogateContextKey, []string{caching.SplitSurrogate}) ctx.Set(caching.StickyContextKey, true) @@ -143,3 +159,21 @@ func (c *SdkServerController) fetchSplitChangesSince(since int64, sets []string) fetchOptions := service.MakeFlagRequestParams().WithChangeNumber(since).WithFlagSetsFilter(strings.Join(sets, ",")) // at this point the sets have been sanitized & sorted return c.fetcher.Fetch(fetchOptions) } + +func (c *SdkServerController) patchUnsupportedMatchers(splits []dtos.SplitDTO, version string) []dtos.SplitDTO { + for si := range splits { + for ci := range splits[si].Conditions { + for mi := range splits[si].Conditions[ci].MatcherGroup.Matchers { + if c.versionFilter.ShouldFilter(splits[si].Conditions[ci].MatcherGroup.Matchers[mi].MatcherType, version) { + splits[si].Conditions[ci].ConditionType = grammar.ConditionTypeWhitelist + splits[si].Conditions[ci].MatcherGroup.Matchers[mi].MatcherType = matchers.MatcherTypeAllKeys + splits[si].Conditions[ci].MatcherGroup.Matchers[mi].String = nil + splits[si].Conditions[ci].Label = labelUnsupportedMatcher + splits[si].Conditions[ci].Partitions = []dtos.PartitionDTO{{Treatment: "control", Size: 100}} + } + } + } + } + + return splits +} diff --git a/splitio/proxy/controllers/sdk_test.go b/splitio/proxy/controllers/sdk_test.go index 3091061b..2f55552d 100644 --- a/splitio/proxy/controllers/sdk_test.go +++ b/splitio/proxy/controllers/sdk_test.go @@ -3,14 +3,17 @@ package controllers import ( "encoding/json" "errors" - "io/ioutil" + "io" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/splitio/go-split-commons/v5/dtos" + "github.com/splitio/go-split-commons/v5/engine/grammar" + "github.com/splitio/go-split-commons/v5/engine/grammar/matchers" "github.com/splitio/go-split-commons/v5/service" + "github.com/splitio/go-split-commons/v5/service/api/specs" "github.com/splitio/go-toolkit/v5/logging" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -52,7 +55,7 @@ func TestSplitChangesRecentSince(t *testing.T) { assert.Equal(t, 200, resp.Code) - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) assert.Nil(t, err) var s dtos.SplitChangesDTO @@ -103,7 +106,7 @@ func TestSplitChangesOlderSince(t *testing.T) { assert.Equal(t, 200, resp.Code) - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) assert.Nil(t, err) var s dtos.SplitChangesDTO @@ -192,7 +195,7 @@ func TestSplitChangesWithFlagSets(t *testing.T) { assert.Equal(t, 200, resp.Code) - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) assert.Nil(t, err) var s dtos.SplitChangesDTO @@ -239,7 +242,7 @@ func TestSplitChangesWithFlagSetsStrict(t *testing.T) { assert.Equal(t, 200, resp.Code) - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) assert.Nil(t, err) var s dtos.SplitChangesDTO @@ -252,6 +255,142 @@ func TestSplitChangesWithFlagSetsStrict(t *testing.T) { splitFetcher.AssertExpectations(t) } +func TestSplitChangesNewMatcherOldSpec(t *testing.T) { + gin.SetMode(gin.TestMode) + + var splitStorage psmocks.ProxySplitStorageMock + splitStorage.On("ChangesSince", int64(-1), []string(nil)). + Return(&dtos.SplitChangesDTO{ + Since: -1, + Till: 1, + Splits: []dtos.SplitDTO{ + { + Name: "s1", + Status: "ACTIVE", + Conditions: []dtos.ConditionDTO{ + { + MatcherGroup: dtos.MatcherGroupDTO{Matchers: []dtos.MatcherDTO{{MatcherType: matchers.MatcherTypeGreaterThanOrEqualToSemver}}}, + Partitions: []dtos.PartitionDTO{{Treatment: "on", Size: 100}}, + Label: "some label", + }, + }}, + }, + }, nil). + Once() + + var splitFetcher splitFetcherMock + + resp := httptest.NewRecorder() + ctx, router := gin.CreateTestContext(resp) + logger := logging.NewLogger(nil) + group := router.Group("/api") + controller := NewSdkServerController( + logger, + &splitFetcher, + &splitStorage, + nil, + flagsets.NewMatcher(false, nil), + ) + controller.Register(group) + + ctx.Request, _ = http.NewRequest(http.MethodGet, "/api/splitChanges?since=-1", nil) + ctx.Request.Header.Set("Authorization", "Bearer someApiKey") + ctx.Request.Header.Set("SplitSDKVersion", "go-1.1.1") + ctx.Request.Header.Set("SplitSDKMachineIp", "1.2.3.4") + ctx.Request.Header.Set("SplitSDKMachineName", "ip-1-2-3-4") + router.ServeHTTP(resp, ctx.Request) + + assert.Equal(t, 200, resp.Code) + + body, err := io.ReadAll(resp.Body) + assert.Nil(t, err) + + var s dtos.SplitChangesDTO + err = json.Unmarshal(body, &s) + assert.Nil(t, err) + assert.Equal(t, 1, len(s.Splits)) + assert.Equal(t, int64(-1), s.Since) + assert.Equal(t, int64(1), s.Till) + + cond := s.Splits[0].Conditions[0] + assert.Equal(t, grammar.ConditionTypeWhitelist, cond.ConditionType) + assert.Equal(t, matchers.MatcherTypeAllKeys, cond.MatcherGroup.Matchers[0].MatcherType) + assert.Equal(t, labelUnsupportedMatcher, cond.Label) + assert.Equal(t, []dtos.PartitionDTO{{Treatment: "control", Size: 100}}, cond.Partitions) + + splitStorage.AssertExpectations(t) + splitFetcher.AssertExpectations(t) +} + +func TestSplitChangesNewMatcherNewSpec(t *testing.T) { + gin.SetMode(gin.TestMode) + + var splitStorage psmocks.ProxySplitStorageMock + splitStorage.On("ChangesSince", int64(-1), []string(nil)). + Return(&dtos.SplitChangesDTO{ + Since: -1, + Till: 1, + Splits: []dtos.SplitDTO{ + { + Name: "s1", + Status: "ACTIVE", + Conditions: []dtos.ConditionDTO{ + { + MatcherGroup: dtos.MatcherGroupDTO{Matchers: []dtos.MatcherDTO{{MatcherType: matchers.MatcherTypeGreaterThanOrEqualToSemver}}}, + Partitions: []dtos.PartitionDTO{{Treatment: "on", Size: 100}}, + Label: "some label", + }, + }}, + }, + }, nil). + Once() + + var splitFetcher splitFetcherMock + + resp := httptest.NewRecorder() + ctx, router := gin.CreateTestContext(resp) + logger := logging.NewLogger(nil) + group := router.Group("/api") + controller := NewSdkServerController( + logger, + &splitFetcher, + &splitStorage, + nil, + flagsets.NewMatcher(false, nil), + ) + controller.Register(group) + + ctx.Request, _ = http.NewRequest(http.MethodGet, "/api/splitChanges?since=-1", nil) + ctx.Request.Header.Set("Authorization", "Bearer someApiKey") + ctx.Request.Header.Set("SplitSDKVersion", "go-1.1.1") + ctx.Request.Header.Set("SplitSDKMachineIp", "1.2.3.4") + ctx.Request.Header.Set("SplitSDKMachineName", "ip-1-2-3-4") + q := ctx.Request.URL.Query() + q.Add("s", specs.FLAG_V1_1) + ctx.Request.URL.RawQuery = q.Encode() + router.ServeHTTP(resp, ctx.Request) + + assert.Equal(t, 200, resp.Code) + + body, err := io.ReadAll(resp.Body) + assert.Nil(t, err) + + var s dtos.SplitChangesDTO + err = json.Unmarshal(body, &s) + assert.Nil(t, err) + assert.Equal(t, 1, len(s.Splits)) + assert.Equal(t, int64(-1), s.Since) + assert.Equal(t, int64(1), s.Till) + + cond := s.Splits[0].Conditions[0] + assert.Equal(t, matchers.MatcherTypeGreaterThanOrEqualToSemver, cond.MatcherGroup.Matchers[0].MatcherType) + assert.Equal(t, "some label", cond.Label) + assert.Equal(t, []dtos.PartitionDTO{{Treatment: "on", Size: 100}}, cond.Partitions) + + splitStorage.AssertExpectations(t) + splitFetcher.AssertExpectations(t) +} + func TestSegmentChanges(t *testing.T) { gin.SetMode(gin.TestMode) @@ -280,7 +419,7 @@ func TestSegmentChanges(t *testing.T) { assert.Equal(t, 200, resp.Code) - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) assert.Nil(t, err) var s dtos.SegmentChangesDTO @@ -353,7 +492,7 @@ func TestMySegments(t *testing.T) { router.ServeHTTP(resp, ctx.Request) assert.Equal(t, 200, resp.Code) - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) assert.Nil(t, err) var ms MSC diff --git a/splitio/proxy/initialization.go b/splitio/proxy/initialization.go index 80254925..9e0f8c06 100644 --- a/splitio/proxy/initialization.go +++ b/splitio/proxy/initialization.go @@ -72,6 +72,8 @@ func Start(logger logging.LoggerInterface, cfg *pconf.Main) error { // Getting initial config data advanced := cfg.BuildAdvancedConfig() advanced.FlagSetsFilter = cfg.FlagSetsFilter + advanced.AuthSpecVersion = cfg.SpecVersion + advanced.FlagsSpecVersion = cfg.SpecVersion metadata := util.GetMetadata(cfg.IPAddressEnabled, true) // FlagSetsFilter