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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
5.12.6 (Jun 19, 2026)
- Updated snapshot validation to warn on configuration mismatches instead of blocking proxy initialization.

5.12.5 (May 29, 2026)
- Fixed vulnerabilities (8 Critical, 3 High, 5 Medium, 3 Low):
- C: CVE-2026-39830, CVE-2026-39831, CVE-2026-39832, CVE-2026-39833, CVE-2026-39834, CVE-2026-42508, CVE-2026-39821
Expand Down
2 changes: 1 addition & 1 deletion splitio/commitversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "db1e39d"
const CommitVersion = "aeef5f7"
30 changes: 16 additions & 14 deletions splitio/proxy/initialization.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
)

// Start initialize in proxy mode
func Start(logger logging.LoggerInterface, cfg *pconf.Main) error {

Check failure on line 42 in splitio/proxy/initialization.go

View check run for this annotation

SonarQube Pull Requests / SonarQube Code Analysis

Refactor this method to reduce its Cognitive Complexity from 20 to the 15 allowed.

[S3776] Cognitive Complexity of functions should not be too high See more on https://sonar.harness.io/project/issues?id=split-synchronizer&pullRequest=349&issues=097e0f1c-6246-4a47-8f29-47c8f6142ae7&open=097e0f1c-6246-4a47-8f29-47c8f6142ae7
clientKey, err := util.GetClientKey(cfg.Apikey)
if err != nil {
return common.NewInitError(fmt.Errorf("error parsing client key from provided apikey: %w", err), common.ExitInvalidApikey)
Expand All @@ -47,23 +47,25 @@

// Initialization of DB
var dbpath = persistent.BoltInMemoryMode
var snapshotValid = false
if snapFile := cfg.Initialization.Snapshot; snapFile != "" {
snap, err := snapshot.DecodeFromFile(snapFile)
if err != nil {
return fmt.Errorf("error parsing snapshot file: %w", err)
}

dbpath, err = snap.WriteDataToTmpFile()
if err != nil {
return fmt.Errorf("error writing temporary snapshot file: %w", err)
}

currentHash := util.HashAPIKey(cfg.Apikey + cfg.FlagSpecVersion + strings.Join(cfg.FlagSetsFilter, "::"))
if snap.Meta().Hash != strconv.Itoa(int(currentHash)) {
return common.NewInitError(errors.New("snapshot cfg (apikey, version, flagsets) does not match the provided one"), common.ExitErrorDB)
logger.Warning("snapshot cfg (apikey, version, flagsets) does not match the provided one. Ignoring snapshot and starting with empty storage.")
} else {
// Hash matches - use the snapshot
dbpath, err = snap.WriteDataToTmpFile()
if err != nil {
return fmt.Errorf("error writing temporary snapshot file: %w", err)
}
snapshotValid = true
logger.Debug("Database created from snapshot at", dbpath)
}

logger.Debug("Database created from snapshot at", dbpath)
}

dbInstance, err := persistent.NewBoltWrapper(dbpath, nil)
Expand All @@ -89,9 +91,9 @@
splitAPI := api.NewSplitAPI(cfg.Apikey, *advanced, logger, metadata)

// Proxy storages already implement the observable interface, so no need to wrap them
splitStorage := storage.NewProxySplitStorage(dbInstance, logger, flagsets.NewFlagSetFilter(cfg.FlagSetsFilter), cfg.Initialization.Snapshot != "")
ruleBasedStorage := storage.NewProxyRuleBasedSegmentsStorage(dbInstance, logger, cfg.Initialization.Snapshot != "")
segmentStorage := storage.NewProxySegmentStorage(dbInstance, logger, cfg.Initialization.Snapshot != "")
splitStorage := storage.NewProxySplitStorage(dbInstance, logger, flagsets.NewFlagSetFilter(cfg.FlagSetsFilter), snapshotValid)
ruleBasedStorage := storage.NewProxyRuleBasedSegmentsStorage(dbInstance, logger, snapshotValid)
segmentStorage := storage.NewProxySegmentStorage(dbInstance, logger, snapshotValid)
largeSegmentStorage := inmemory.NewLargeSegmentsStorage()

// Local telemetry
Expand Down Expand Up @@ -176,13 +178,13 @@
return common.NewInitError(fmt.Errorf("error instantiating sync manager: %w", err), common.ExitTaskInitialization)
}

// Try to start bg sync in BG with unlimited retries (when a snapshot is provided),
// Try to start bg sync in BG with unlimited retries (when a valid snapshot is provided),
// the passed function is invoked upon initialization completion
// If no snapshot is provided and init fails, `errUnrecoverable` is returned and application execution is aborted
// If no valid snapshot is provided and init fails, `errUnrecoverable` is returned and application execution is aborted
// health monitors are only started after successful init (otherwise they'll fail if the app doesn't sync correctly within the
/// specified refresh period)
before := time.Now()
err = startBGSync(syncManager, mstatus, cfg.Initialization.Snapshot != "", func() {
err = startBGSync(syncManager, mstatus, snapshotValid, func() {
logger.Info("Synchronizer tasks started")
appMonitor.Start()
servicesMonitor.Start()
Expand Down
184 changes: 184 additions & 0 deletions splitio/proxy/initialization_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
package proxy

import (
"fmt"
"os"
"strconv"
"strings"
"sync/atomic"
"testing"
"time"

"github.com/splitio/go-split-commons/v9/synchronizer"
"github.com/splitio/go-toolkit/v5/logging"
"github.com/splitio/split-synchronizer/v5/splitio/common/snapshot"
pconf "github.com/splitio/split-synchronizer/v5/splitio/proxy/conf"
"github.com/splitio/split-synchronizer/v5/splitio/util"
)

type syncManagerMock struct {
Expand Down Expand Up @@ -67,3 +75,179 @@ func TestSyncManagerInitializationRetriesWithSnapshot(t *testing.T) {
t.Error("there should be 2 executions")
}
}

// mockLogger captures warning messages for testing
type mockLogger struct {
logging.LoggerInterface
warnings []string
}

func (m *mockLogger) Warning(msg ...interface{}) {
m.warnings = append(m.warnings, fmt.Sprint(msg...))
}

func (m *mockLogger) Debug(msg ...interface{}) {}
func (m *mockLogger) Error(msg ...interface{}) {}
func (m *mockLogger) Info(msg ...interface{}) {}

func TestSnapshotHashMismatchLogsWarningAndIgnoresSnapshot(t *testing.T) {
// Create a temporary snapshot file with a specific hash
tmpDir, err := os.MkdirTemp("", "snapshot-test")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)

snapshotPath := tmpDir + "/test-snapshot.snap"

// Create a snapshot with hash "12345"
meta := snapshot.Metadata{
Version: 1,
Storage: 1,
Hash: "12345",
}
snap, err := snapshot.New(meta, []byte("test data"))
if err != nil {
t.Fatalf("failed to create snapshot: %v", err)
}

// Encode snapshot to bytes and write to file
encoded, err := snap.Encode()
if err != nil {
t.Fatalf("failed to encode snapshot: %v", err)
}

if err := os.WriteFile(snapshotPath, encoded, 0644); err != nil {
t.Fatalf("failed to write snapshot file: %v", err)
}

// Create a config with different apikey/version/flagsets that will produce a different hash
cfg := &pconf.Main{
Apikey: "test-apikey",
FlagSpecVersion: "1.1",
FlagSetsFilter: []string{"set1", "set2"},
Initialization: pconf.Initialization{
Snapshot: snapshotPath,
},
}

// Calculate what the hash would be with this config
currentHash := util.HashAPIKey(cfg.Apikey + cfg.FlagSpecVersion + strings.Join(cfg.FlagSetsFilter, "::"))
expectedHashStr := strconv.Itoa(int(currentHash))

// Verify that the hashes are indeed different
if meta.Hash == expectedHashStr {
t.Fatal("test setup error: hashes should be different for this test")
}

// Create a mock logger to capture warnings
mockLog := &mockLogger{
warnings: make([]string, 0),
}

// This simulates the code path in Start() that checks the hash
snap2, err := snapshot.DecodeFromFile(snapshotPath)
if err != nil {
t.Fatalf("failed to decode snapshot: %v", err)
}

// Simulate the new behavior: check hash and ignore if mismatch
var snapshotValid = false
currentHash2 := util.HashAPIKey(cfg.Apikey + cfg.FlagSpecVersion + strings.Join(cfg.FlagSetsFilter, "::"))
if snap2.Meta().Hash != strconv.Itoa(int(currentHash2)) {
mockLog.Warning("snapshot cfg (apikey, version, flagsets) does not match the provided one. Ignoring snapshot and starting with empty storage.")
} else {
snapshotValid = true
}

// Verify that a warning was logged
if len(mockLog.warnings) != 1 {
t.Errorf("expected 1 warning, got %d", len(mockLog.warnings))
}

expectedWarning := "snapshot cfg (apikey, version, flagsets) does not match the provided one. Ignoring snapshot and starting with empty storage."
if len(mockLog.warnings) > 0 && mockLog.warnings[0] != expectedWarning {
t.Errorf("expected warning '%s', got '%s'", expectedWarning, mockLog.warnings[0])
}

// Verify that snapshot is marked as invalid
if snapshotValid {
t.Error("snapshot should be marked as invalid when hash mismatches")
}
}

func TestSnapshotHashMatchNoWarningAndUsesSnapshot(t *testing.T) {
// Create a temporary snapshot file
tmpDir, err := os.MkdirTemp("", "snapshot-test")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)

snapshotPath := tmpDir + "/test-snapshot.snap"

// Create config
cfg := &pconf.Main{
Apikey: "test-apikey",
FlagSpecVersion: "1.1",
FlagSetsFilter: []string{"set1", "set2"},
Initialization: pconf.Initialization{
Snapshot: snapshotPath,
},
}

// Calculate the hash with the config
currentHash := util.HashAPIKey(cfg.Apikey + cfg.FlagSpecVersion + strings.Join(cfg.FlagSetsFilter, "::"))
hashStr := strconv.Itoa(int(currentHash))

// Create a snapshot with the SAME hash
meta := snapshot.Metadata{
Version: 1,
Storage: 1,
Hash: hashStr,
}
snap, err := snapshot.New(meta, []byte("test data"))
if err != nil {
t.Fatalf("failed to create snapshot: %v", err)
}

// Encode snapshot to bytes and write to file
encoded, err := snap.Encode()
if err != nil {
t.Fatalf("failed to encode snapshot: %v", err)
}

if err := os.WriteFile(snapshotPath, encoded, 0644); err != nil {
t.Fatalf("failed to write snapshot file: %v", err)
}

// Create a mock logger to capture warnings
mockLog := &mockLogger{
warnings: make([]string, 0),
}

// This simulates the code path in Start() that checks the hash
snap2, err := snapshot.DecodeFromFile(snapshotPath)
if err != nil {
t.Fatalf("failed to decode snapshot: %v", err)
}

// Simulate the new behavior: check hash and use if match
var snapshotValid = false
currentHash2 := util.HashAPIKey(cfg.Apikey + cfg.FlagSpecVersion + strings.Join(cfg.FlagSetsFilter, "::"))
if snap2.Meta().Hash != strconv.Itoa(int(currentHash2)) {
mockLog.Warning("snapshot cfg (apikey, version, flagsets) does not match the provided one. Ignoring snapshot and starting with empty storage.")
} else {
snapshotValid = true
}

// Verify that NO warning was logged
if len(mockLog.warnings) != 0 {
t.Errorf("expected 0 warnings, got %d: %v", len(mockLog.warnings), mockLog.warnings)
}

// Verify that snapshot is marked as valid
if !snapshotValid {
t.Error("snapshot should be marked as valid when hash matches")
}
}
2 changes: 1 addition & 1 deletion splitio/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
package splitio

// Version is the version of this Agent
const Version = "5.12.5"
const Version = "5.12.6"