diff --git a/CHANGES.txt b/CHANGES.txt index 62a00d18..d0c35e62 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,11 @@ +5.11.0 (Nov 12, 2025) +- Split Proxy: + - Added support for rule-based segment. These segments determine membership at runtime by evaluating their configured rules against the user attributes provided to the SDK. +- Split-Sync: + - Added support for rule-based segment. These segments determine membership at runtime by evaluating their configured rules against the user attributes provided to the SDK. +- Fixed vulnerabilities: + - Updated golang image to 1.24.9 + 5.10.4 (Oct 3, 2025) - Fixed vulnerabilities: - Updated golang image to 1.24.7 diff --git a/docker/Dockerfile.proxy b/docker/Dockerfile.proxy index d26d3b92..024ee397 100644 --- a/docker/Dockerfile.proxy +++ b/docker/Dockerfile.proxy @@ -1,5 +1,5 @@ # Build stage -FROM golang:1.24.7-bookworm AS builder +FROM golang:1.24.9-bookworm AS builder ARG EXTRA_BUILD_ARGS ARG FIPS_MODE diff --git a/docker/Dockerfile.synchronizer b/docker/Dockerfile.synchronizer index 0d065513..5d60edff 100644 --- a/docker/Dockerfile.synchronizer +++ b/docker/Dockerfile.synchronizer @@ -1,5 +1,5 @@ # Build stage -FROM golang:1.24.7-bookworm AS builder +FROM golang:1.24.9-bookworm AS builder ARG EXTRA_BUILD_ARGS ARG FIPS_MODE diff --git a/go.mod b/go.mod index 05b0939b..8c2d34de 100644 --- a/go.mod +++ b/go.mod @@ -8,9 +8,9 @@ require ( github.com/gin-gonic/gin v1.10.1 github.com/google/uuid v1.3.0 github.com/splitio/gincache v1.0.1 - github.com/splitio/go-split-commons/v6 v6.1.0 - github.com/splitio/go-toolkit/v5 v5.4.0 - github.com/stretchr/testify v1.10.0 + github.com/splitio/go-split-commons/v8 v8.0.0 + github.com/splitio/go-toolkit/v5 v5.4.1 + github.com/stretchr/testify v1.11.1 go.etcd.io/bbolt v1.3.6 golang.org/x/exp v0.0.0-20231006140011-7918f672742d ) diff --git a/go.sum b/go.sum index 1b002341..0827631c 100644 --- a/go.sum +++ b/go.sum @@ -74,10 +74,10 @@ github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUA github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/splitio/gincache v1.0.1 h1:dLYdANY/BqH4KcUMCe/LluLyV5WtuE/LEdQWRE06IXU= github.com/splitio/gincache v1.0.1/go.mod h1:CcgJDSM9Af75kyBH0724v55URVwMBuSj5x1eCWIOECY= -github.com/splitio/go-split-commons/v6 v6.1.0 h1:k3mwr12DF6gbEaV8XXU/tSAQlPkIEuzIgTEneYhGg2I= -github.com/splitio/go-split-commons/v6 v6.1.0/go.mod h1:D/XIY/9Hmfk9ivWsRsJVp439kEdmHbzUi3PKzQQDOXY= -github.com/splitio/go-toolkit/v5 v5.4.0 h1:g5WFpRhQomnXCmvfsNOWV4s5AuUrWIZ+amM68G8NBKM= -github.com/splitio/go-toolkit/v5 v5.4.0/go.mod h1:xYhUvV1gga9/1029Wbp5pjnR6Cy8nvBpjw99wAbsMko= +github.com/splitio/go-split-commons/v8 v8.0.0 h1:wLk5eT6WU2LfxtaWG3ZHlTbNMGWP2eYsZTb1o+tFpkI= +github.com/splitio/go-split-commons/v8 v8.0.0/go.mod h1:vgRGPn0s4RC9/zp1nIn4KeeIEj/K3iXE2fxYQbCk/WI= +github.com/splitio/go-toolkit/v5 v5.4.1 h1:srTyvDBJZMUcJ/KiiQDMyjCuELVgTBh2TGRVn0sOXEE= +github.com/splitio/go-toolkit/v5 v5.4.1/go.mod h1:SifzysrOVDbzMcOE8zjX02+FG5az4FrR3Us/i5SeStw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -87,8 +87,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twmb/murmur3 v1.1.6 h1:mqrRot1BRxm+Yct+vavLMou2/iJt0tNVTTC0QoIjaZg= diff --git a/splitio/admin/admin.go b/splitio/admin/admin.go index 3d0da679..b08f1eaa 100644 --- a/splitio/admin/admin.go +++ b/splitio/admin/admin.go @@ -41,6 +41,7 @@ type Options struct { FullConfig interface{} FlagSpecVersion string LargeSegmentVersion string + Hash string } type AdminServer struct { @@ -96,7 +97,7 @@ func NewServer(options *Options) (*AdminServer, error) { observabilityController.Register(admin) if options.Snapshotter != nil { - snapshotController := controllers.NewSnapshotController(options.Logger, options.Snapshotter) + snapshotController := controllers.NewSnapshotController(options.Logger, options.Snapshotter, options.Hash) snapshotController.Register(admin) } diff --git a/splitio/admin/common/config.go b/splitio/admin/common/config.go index 275f8ed3..63cb4376 100644 --- a/splitio/admin/common/config.go +++ b/splitio/admin/common/config.go @@ -1,14 +1,28 @@ package common -import "github.com/splitio/go-split-commons/v6/storage" +import ( + "github.com/splitio/go-split-commons/v8/engine/grammar/constants" + "github.com/splitio/go-split-commons/v8/storage" +) + +var ProducerFeatureFlagsRules = []string{constants.MatcherTypeAllKeys, constants.MatcherTypeInSegment, constants.MatcherTypeWhitelist, constants.MatcherTypeEqualTo, constants.MatcherTypeGreaterThanOrEqualTo, constants.MatcherTypeLessThanOrEqualTo, constants.MatcherTypeBetween, + constants.MatcherTypeEqualToSet, constants.MatcherTypePartOfSet, constants.MatcherTypeContainsAllOfSet, constants.MatcherTypeContainsAnyOfSet, constants.MatcherTypeStartsWith, constants.MatcherTypeEndsWith, constants.MatcherTypeContainsString, constants.MatcherTypeInSplitTreatment, + constants.MatcherTypeEqualToBoolean, constants.MatcherTypeMatchesString, constants.MatcherEqualToSemver, constants.MatcherTypeGreaterThanOrEqualToSemver, constants.MatcherTypeLessThanOrEqualToSemver, constants.MatcherTypeBetweenSemver, constants.MatcherTypeInListSemver, + constants.MatcherTypeInRuleBasedSegment} + +var ProducerRuleBasedSegmentRules = []string{constants.MatcherTypeAllKeys, constants.MatcherTypeInSegment, constants.MatcherTypeWhitelist, constants.MatcherTypeEqualTo, constants.MatcherTypeGreaterThanOrEqualTo, constants.MatcherTypeLessThanOrEqualTo, constants.MatcherTypeBetween, + constants.MatcherTypeEqualToSet, constants.MatcherTypePartOfSet, constants.MatcherTypeContainsAllOfSet, constants.MatcherTypeContainsAnyOfSet, constants.MatcherTypeStartsWith, constants.MatcherTypeEndsWith, constants.MatcherTypeContainsString, + constants.MatcherTypeEqualToBoolean, constants.MatcherTypeMatchesString, constants.MatcherEqualToSemver, constants.MatcherTypeGreaterThanOrEqualToSemver, constants.MatcherTypeLessThanOrEqualToSemver, constants.MatcherTypeBetweenSemver, constants.MatcherTypeInListSemver, + constants.MatcherTypeInRuleBasedSegment} // Storages wraps storages in one struct type Storages struct { - SplitStorage storage.SplitStorage - SegmentStorage storage.SegmentStorage - LocalTelemetryStorage storage.TelemetryRuntimeConsumer - EventStorage storage.EventMultiSdkConsumer - ImpressionStorage storage.ImpressionMultiSdkConsumer - UniqueKeysStorage storage.UniqueKeysMultiSdkConsumer - LargeSegmentStorage storage.LargeSegmentsStorage + SplitStorage storage.SplitStorage + SegmentStorage storage.SegmentStorage + LocalTelemetryStorage storage.TelemetryRuntimeConsumer + EventStorage storage.EventMultiSdkConsumer + ImpressionStorage storage.ImpressionMultiSdkConsumer + UniqueKeysStorage storage.UniqueKeysMultiSdkConsumer + LargeSegmentStorage storage.LargeSegmentsStorage + RuleBasedSegmentsStorage storage.RuleBasedSegmentsStorage } diff --git a/splitio/admin/controllers/dashboard.go b/splitio/admin/controllers/dashboard.go index 49a40bbe..a51ee444 100644 --- a/splitio/admin/controllers/dashboard.go +++ b/splitio/admin/controllers/dashboard.go @@ -150,6 +150,7 @@ func (c *DashboardController) gatherStats() *dashboard.GlobalStats { FeatureFlags: bundleSplitInfo(c.storages.SplitStorage), Segments: bundleSegmentInfo(c.storages.SplitStorage, c.storages.SegmentStorage), LargeSegments: bundleLargeSegmentInfo(c.storages.SplitStorage, c.storages.LargeSegmentStorage), + RuleBasedSegments: bundleRuleBasedInfo(c.storages.SplitStorage, c.storages.RuleBasedSegmentsStorage), Latencies: bundleProxyLatencies(c.storages.LocalTelemetryStorage), BackendLatencies: bundleLocalSyncLatencies(c.storages.LocalTelemetryStorage), ImpressionsQueueSize: getImpressionSize(c.storages.ImpressionStorage), diff --git a/splitio/admin/controllers/helpers.go b/splitio/admin/controllers/helpers.go index 6454ab9f..7565aed7 100644 --- a/splitio/admin/controllers/helpers.go +++ b/splitio/admin/controllers/helpers.go @@ -5,8 +5,8 @@ import ( "strings" "time" - "github.com/splitio/go-split-commons/v6/storage" - "github.com/splitio/go-split-commons/v6/telemetry" + "github.com/splitio/go-split-commons/v8/storage" + "github.com/splitio/go-split-commons/v8/telemetry" "github.com/splitio/split-synchronizer/v5/splitio/admin/views/dashboard" "github.com/splitio/split-synchronizer/v5/splitio/producer/evcalc" @@ -108,6 +108,45 @@ func bundleSegmentInfo(splitStorage storage.SplitStorage, segmentStorage storage return summaries } +func bundleRuleBasedInfo(splitStorage storage.SplitStorage, ruleBasedSegmentStorage storage.RuleBasedSegmentStorageConsumer) []dashboard.RuleBasedSegmentSummary { + names := splitStorage.RuleBasedSegmentNames() + summaries := make([]dashboard.RuleBasedSegmentSummary, 0, names.Size()) + + for _, name := range names.List() { + strName, ok := name.(string) + if !ok { + continue + } + + ruleBased, err := ruleBasedSegmentStorage.GetRuleBasedSegmentByName(strName) + if err != nil { + continue + } + + excluededSegments := make([]dashboard.ExcludedSegments, 0, len(ruleBased.Excluded.Segments)) + for _, excludedSegment := range ruleBased.Excluded.Segments { + excluededSegments = append(excluededSegments, dashboard.ExcludedSegments{ + Name: excludedSegment.Name, + Type: excludedSegment.Type, + }) + } + + if ruleBased.Excluded.Keys == nil { + ruleBased.Excluded.Keys = make([]string, 0) + } + + summaries = append(summaries, dashboard.RuleBasedSegmentSummary{ + Name: ruleBased.Name, + Active: ruleBased.Status == "ACTIVE", + ExcludedKeys: ruleBased.Excluded.Keys, + ExcludedSegments: excluededSegments, + LastModified: time.Unix(0, ruleBased.ChangeNumber*int64(time.Millisecond)).UTC().Format(time.UnixDate), + ChangeNumber: ruleBased.ChangeNumber, + }) + } + return summaries +} + func bundleSegmentKeysInfo(name string, segmentStorage storage.SegmentStorageConsumer) []dashboard.SegmentKeySummary { keys := segmentStorage.Keys(name) diff --git a/splitio/admin/controllers/helpers_test.go b/splitio/admin/controllers/helpers_test.go new file mode 100644 index 00000000..7889dfcb --- /dev/null +++ b/splitio/admin/controllers/helpers_test.go @@ -0,0 +1,29 @@ +package controllers + +import ( + "testing" + + "github.com/splitio/split-synchronizer/v5/splitio/admin/views/dashboard" + + "github.com/splitio/go-split-commons/v8/dtos" + "github.com/splitio/go-split-commons/v8/storage/mocks" + "github.com/splitio/go-toolkit/v5/datastructures/set" + + "github.com/stretchr/testify/assert" +) + +func TestBundleRBInfo(t *testing.T) { + split := &mocks.SplitStorageMock{} + split.On("RuleBasedSegmentNames").Return(set.NewSet("rb1", "rb2"), nil).Once() + rb := &mocks.MockRuleBasedSegmentStorage{} + rb.On("GetRuleBasedSegmentByName", "rb1").Return(&dtos.RuleBasedSegmentDTO{Name: "rb1", ChangeNumber: 1, Status: "ACTIVE", Excluded: dtos.ExcludedDTO{Keys: []string{"one"}}}, nil).Once() + rb.On("GetRuleBasedSegmentByName", "rb2").Return(&dtos.RuleBasedSegmentDTO{Name: "rb2", ChangeNumber: 2, Status: "ARCHIVED"}, nil).Once() + result := bundleRuleBasedInfo(split, rb) + assert.Len(t, result, 2) + assert.ElementsMatch(t, result, []dashboard.RuleBasedSegmentSummary{ + {Name: "rb1", ChangeNumber: 1, Active: true, ExcludedKeys: []string{"one"}, ExcludedSegments: []dashboard.ExcludedSegments{}, LastModified: "Thu Jan 1 00:00:00 UTC 1970"}, + {Name: "rb2", ChangeNumber: 2, Active: false, ExcludedKeys: []string{}, ExcludedSegments: []dashboard.ExcludedSegments{}, LastModified: "Thu Jan 1 00:00:00 UTC 1970"}, + }) + split.AssertExpectations(t) + rb.AssertExpectations(t) +} diff --git a/splitio/admin/controllers/observability_test.go b/splitio/admin/controllers/observability_test.go index e5f3e305..c92d57c5 100644 --- a/splitio/admin/controllers/observability_test.go +++ b/splitio/admin/controllers/observability_test.go @@ -7,14 +7,16 @@ import ( "net/http/httptest" "testing" - "github.com/gin-gonic/gin" - "github.com/splitio/go-split-commons/v6/dtos" - "github.com/splitio/go-split-commons/v6/storage/mocks" - "github.com/splitio/go-toolkit/v5/datastructures/set" - "github.com/splitio/go-toolkit/v5/logging" adminCommon "github.com/splitio/split-synchronizer/v5/splitio/admin/common" "github.com/splitio/split-synchronizer/v5/splitio/provisional/observability" "github.com/splitio/split-synchronizer/v5/splitio/proxy/storage" + + "github.com/splitio/go-split-commons/v8/dtos" + "github.com/splitio/go-split-commons/v8/storage/mocks" + "github.com/splitio/go-toolkit/v5/datastructures/set" + "github.com/splitio/go-toolkit/v5/logging" + + "github.com/gin-gonic/gin" ) func TestSyncObservabilityEndpoint(t *testing.T) { diff --git a/splitio/admin/controllers/snapshot.go b/splitio/admin/controllers/snapshot.go index 2bc933af..b5d451a8 100644 --- a/splitio/admin/controllers/snapshot.go +++ b/splitio/admin/controllers/snapshot.go @@ -16,11 +16,12 @@ import ( type SnapshotController struct { logger logging.LoggerInterface db storage.Snapshotter + hash string } // NewSnapshotController constructs a new snapshot controller -func NewSnapshotController(logger logging.LoggerInterface, db storage.Snapshotter) *SnapshotController { - return &SnapshotController{logger: logger, db: db} +func NewSnapshotController(logger logging.LoggerInterface, db storage.Snapshotter, hash string) *SnapshotController { + return &SnapshotController{logger: logger, db: db, hash: hash} } // Register mounts the endpoints int he provided router @@ -38,7 +39,7 @@ func (c *SnapshotController) downloadSnapshot(ctx *gin.Context) { return } - s, err := snapshot.New(snapshot.Metadata{Version: 1, Storage: snapshot.StorageBoltDB}, b) + s, err := snapshot.New(snapshot.Metadata{Version: 1, Storage: snapshot.StorageBoltDB, Hash: c.hash}, b) if err != nil { c.logger.Error("error building snapshot: ", err) ctx.JSON(http.StatusInternalServerError, gin.H{"error": "error building snapshot"}) diff --git a/splitio/admin/controllers/snapshot_test.go b/splitio/admin/controllers/snapshot_test.go index c966d9d4..fc5a9e0b 100644 --- a/splitio/admin/controllers/snapshot_test.go +++ b/splitio/admin/controllers/snapshot_test.go @@ -2,40 +2,34 @@ package controllers import ( "bytes" - "io/ioutil" + "io" "net/http" "net/http/httptest" "testing" - "github.com/gin-gonic/gin" - "github.com/splitio/go-toolkit/v5/logging" "github.com/splitio/split-synchronizer/v5/splitio/common/snapshot" "github.com/splitio/split-synchronizer/v5/splitio/proxy/storage/persistent" + + "github.com/splitio/go-toolkit/v5/logging" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" ) func TestDownloadProxySnapshot(t *testing.T) { // Read DB snapshot for test path := "../../../test/snapshot/proxy.snapshot" snap, err := snapshot.DecodeFromFile(path) - if err != nil { - t.Error(err) - return - } + assert.Nil(t, err) tmpDataFile, err := snap.WriteDataToTmpFile() - if err != nil { - t.Error(err) - return - } + assert.Nil(t, err) // loading snapshot from disk dbInstance, err := persistent.NewBoltWrapper(tmpDataFile, nil) - if err != nil { - t.Error(err) - return - } + assert.Nil(t, err) - ctrl := NewSnapshotController(logging.NewLogger(nil), dbInstance) + ctrl := NewSnapshotController(logging.NewLogger(nil), dbInstance, "123456") resp := httptest.NewRecorder() ctx, router := gin.CreateTestContext(resp) @@ -44,35 +38,19 @@ func TestDownloadProxySnapshot(t *testing.T) { ctx.Request, _ = http.NewRequest(http.MethodGet, "/snapshot", nil) router.ServeHTTP(resp, ctx.Request) - responseBody, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Error(err) - return - } + responseBody, err := io.ReadAll(resp.Body) + assert.Nil(t, err) snapRes, err := snapshot.Decode(responseBody) - if err != nil { - t.Error(err) - return - } - - if snapRes.Meta().Version != 1 { - t.Error("Invalid Metadata version") - } + assert.Nil(t, err) - if snapRes.Meta().Storage != 1 { - t.Error("Invalid Metadata storage") - } + assert.Equal(t, uint64(1), snapRes.Meta().Version) + assert.Equal(t, uint64(1), snapRes.Meta().Storage) + assert.Equal(t, "123456", snapRes.Meta().Hash) dat, err := snap.Data() - if err != nil { - t.Error(err) - } + assert.Nil(t, err) resData, err := snapRes.Data() - if err != nil { - t.Error(err) - } - if bytes.Compare(dat, resData) != 0 { - t.Error("loaded snapshot is different to downloaded") - } + assert.Nil(t, err) + assert.Equal(t, 0, bytes.Compare(dat, resData)) } diff --git a/splitio/admin/views/dashboard/datainspector.go b/splitio/admin/views/dashboard/datainspector.go index f4f63577..3af58c63 100644 --- a/splitio/admin/views/dashboard/datainspector.go +++ b/splitio/admin/views/dashboard/datainspector.go @@ -38,6 +38,19 @@ const dataInspector = ` {{end}} +
  • + + +  Rule-based Segments + +