diff --git a/featureflags/cache/cache.go b/featureflags/cache/cache.go new file mode 100644 index 00000000..b2a09960 --- /dev/null +++ b/featureflags/cache/cache.go @@ -0,0 +1,43 @@ +package cache + +import ( + "time" + + "github.com/rudderlabs/rudder-go-kit/featureflags/provider" +) + +// CacheEntry represents a cached item with its value and last updated timestamp +type CacheEntry struct { + Value map[string]*provider.FeatureValue + LastUpdated time.Time +} + +// CacheGetResult extends CacheEntry with staleness information +type CacheGetResult struct { + *CacheEntry + IsStale bool +} + +// Cache defines the interface for cache operations +type Cache interface { + Get(key string) (*CacheGetResult, bool) + Set(key string, value map[string]*provider.FeatureValue) (time.Time, error) + Delete(key string) error + Clear() error + IsEnabled() bool +} + +// CacheError represents cache-specific errors +type CacheError struct { + Message string +} + +func (e *CacheError) Error() string { + return e.Message +} + +// CacheConfig holds configuration for the cache +type CacheConfig struct { + Enabled bool + TTLInSeconds int64 +} diff --git a/featureflags/cache/cache_test.go b/featureflags/cache/cache_test.go new file mode 100644 index 00000000..73283f80 --- /dev/null +++ b/featureflags/cache/cache_test.go @@ -0,0 +1,70 @@ +package cache_test + +import ( + "testing" + "time" + + "github.com/rudderlabs/rudder-go-kit/featureflags/cache" + "github.com/rudderlabs/rudder-go-kit/featureflags/provider" +) + +func TestMemoryCache(t *testing.T) { + config := cache.CacheConfig{ + Enabled: true, + TTLInSeconds: 1, + } + + c := cache.NewMemoryCache(config) + + testValue := map[string]*provider.FeatureValue{ + "key1": { + Value: "value1", + }, + } + + // Test Set and Get + setTime, err := c.Set("key1", testValue) + if err != nil { + t.Errorf("Failed to set cache: %v", err) + } + + result, ok := c.Get("key1") + if !ok { + t.Error("Expected cache hit, got miss") + } + if result.LastUpdated != setTime { + t.Error("LastUpdated time mismatch") + } + + // Test staleness + time.Sleep(2 * time.Second) + result, ok = c.Get("key1") + if !ok { + t.Error("Expected stale cache entry") + } + + // Test Delete + err = c.Delete("key1") + if err != nil { + t.Errorf("Failed to delete cache: %v", err) + } + + result, ok = c.Get("key1") + if ok { + t.Error("Expected cache miss after delete") + } + + // Test Clear + c.Set("key1", testValue) + c.Set("key2", testValue) + + err = c.Clear() + if err != nil { + t.Errorf("Failed to clear cache: %v", err) + } + + result, ok = c.Get("key1") + if ok { + t.Error("Expected cache miss after clear") + } +} diff --git a/featureflags/cache/memory.go b/featureflags/cache/memory.go new file mode 100644 index 00000000..e3b76280 --- /dev/null +++ b/featureflags/cache/memory.go @@ -0,0 +1,91 @@ +package cache + +import ( + "sync" + "time" + + "github.com/rudderlabs/rudder-go-kit/featureflags/provider" +) + +type MemoryCache struct { + entries map[string]CacheEntry + config CacheConfig + mu sync.RWMutex +} + +func NewMemoryCache(config CacheConfig) *MemoryCache { + return &MemoryCache{ + entries: make(map[string]CacheEntry), + config: config, + } +} + +func (c *MemoryCache) Get(key string) (*CacheGetResult, bool) { + if !c.IsEnabled() { + return nil, false + } + + c.mu.RLock() + defer c.mu.RUnlock() + + entry, exists := c.entries[key] + if !exists { + return nil, false + } + + isStale := false + if c.config.TTLInSeconds > 0 { + expirationTime := entry.LastUpdated.Add(time.Duration(c.config.TTLInSeconds) * time.Second) + isStale = time.Now().After(expirationTime) + } + + return &CacheGetResult{ + CacheEntry: &entry, + IsStale: isStale, + }, true +} + +func (c *MemoryCache) Set(key string, value map[string]*provider.FeatureValue) (time.Time, error) { + if !c.IsEnabled() { + return time.Time{}, &CacheError{Message: "Cache is disabled"} + } + + c.mu.Lock() + defer c.mu.Unlock() + + now := time.Now() + c.entries[key] = CacheEntry{ + Value: value, + LastUpdated: now, + } + + return now, nil +} + +func (c *MemoryCache) Delete(key string) error { + if !c.IsEnabled() { + return &CacheError{Message: "Cache is disabled"} + } + + c.mu.Lock() + defer c.mu.Unlock() + + delete(c.entries, key) + return nil +} + +func (c *MemoryCache) Clear() error { + if !c.IsEnabled() { + return &CacheError{Message: "Cache is disabled"} + } + + c.mu.Lock() + defer c.mu.Unlock() + + c.entries = make(map[string]CacheEntry) + return nil +} + +func (c *MemoryCache) IsEnabled() bool { + return c.config.Enabled +} \ No newline at end of file diff --git a/featureflags/client.go b/featureflags/client.go new file mode 100644 index 00000000..a7c267db --- /dev/null +++ b/featureflags/client.go @@ -0,0 +1,161 @@ +package featureflags + +import ( + "fmt" + "os" + "sync" + + "github.com/rudderlabs/rudder-go-kit/featureflags/cache" + "github.com/rudderlabs/rudder-go-kit/featureflags/provider" +) + +// client represents the feature flag client interface +type client interface { + IsFeatureEnabled(workspaceID string, feature string) (bool, error) + IsFeatureEnabledLatest(workspaceID string, feature string) (bool, error) + GetFeatureValue(workspaceID string, feature string) (provider.FeatureValue, error) + GetFeatureValueLatest(workspaceID string, feature string) (provider.FeatureValue, error) + SetDefaultTraits(traits map[string]string) +} + +var ( + ffclient client + clientOnce sync.Once +) + +// getFeatureFlagClient returns the singleton feature flag client instance +func getFeatureFlagClient() client { + clientOnce.Do(func() { + // read the api key from env vars and create the default cache config + apiKey := os.Getenv("FLAGSMITH_SERVER_SIDE_ENVIRONMENT_KEY") + if apiKey == "" { + panic("FLAGSMITH_SERVER_SIDE_ENVIRONMENT_KEY is not set") + } + defaultCacheConfig := cache.CacheConfig{ + Enabled: true, + TTLInSeconds: 60, + } + + // create the provider + provider, err := provider.NewProvider(provider.ProviderConfig{ + Type: "flagsmith", + ApiKey: apiKey, + }) + if err != nil { + panic(err) + } + ffclient = &clientImpl{ + provider: provider, + cache: cache.NewMemoryCache(defaultCacheConfig), + } + }) + return ffclient +} + +type clientImpl struct { + provider provider.Provider + cache cache.Cache + defaultTraits map[string]string +} + +// IsFeatureEnabled checks if a feature is enabled for a workspace +// Note: Result may be stale if returned from cache. Use IsFeatureEnabledLatest if stale values are not acceptable. +func (c *clientImpl) IsFeatureEnabled(workspaceID string, feature string) (bool, error) { + ff, err := c.getAllFeatures(workspaceID, c.defaultTraits, false) + if err != nil { + return false, err + } + featureval, ok := ff[feature] + if !ok { + return false, newFeatureError(fmt.Sprintf("feature %s does not exist", feature)) + } + return featureval.Enabled, nil +} + +// IsFeatureEnabledLatest checks if a feature is enabled for a workspace, bypassing the cache +// Note: This method always fetches fresh values from the provider(bypassing the cache), which may impact performance. +func (c *clientImpl) IsFeatureEnabledLatest(workspaceID string, feature string) (bool, error) { + ff, err := c.getAllFeatures(workspaceID, c.defaultTraits, true) + if err != nil { + return false, err + } + return ff[feature].Enabled, nil +} + +// GetFeatureValue gets the value of a feature for a workspace +// Note: Result may be stale if returned from cache. Use GetFeatureValueLatest if stale values are not acceptable. +func (c *clientImpl) GetFeatureValue(workspaceID string, feature string) (provider.FeatureValue, error) { + ff, err := c.getAllFeatures(workspaceID, c.defaultTraits, false) + if err != nil { + return provider.FeatureValue{}, err + } + // create a copy of the feature value and return it + featureval := *ff[feature] + return featureval, nil +} + +// GetFeatureValueLatest gets the value of a feature for a workspace, bypassing the cache +// Note: This method always fetches fresh values from the provider(bypassing the cache), which may impact performance. +func (c *clientImpl) GetFeatureValueLatest(workspaceID string, feature string) (provider.FeatureValue, error) { + ff, err := c.getAllFeatures(workspaceID, c.defaultTraits, true) + if err != nil { + return provider.FeatureValue{}, err + } + // create a copy of the feature value and return it + featureval := *ff[feature] + return featureval, nil +} + +// SetDefaultTraits sets the default traits for the feature flag client +// These traits will always be used when fetching feature flags +// This function is only meant to be called once in the application lifecycle. +func (c *clientImpl) SetDefaultTraits(traits map[string]string) { + c.defaultTraits = traits +} + +func (c *clientImpl) getAllFeatures(workspaceID string, traits map[string]string, skipCache bool) (map[string]*provider.FeatureValue, error) { + if !skipCache && c.cache.IsEnabled() { + if val, ok := c.cache.Get(workspaceID); ok { + if val.IsStale { + // refresh the feature flags asynchronously if the cache is stale + go c.refreshFeatureFlags(workspaceID) + } + return val.Value, nil + } + } + + // Fetch from provider, if cache is disabled or cache miss + ff, err := c.provider.GetFeatureFlags(provider.ProviderParams{ + WorkspaceID: workspaceID, + Traits: traits, + }) + if err != nil { + return nil, err + } + + // Cache the fetched feature flags if cache is enabled + if c.cache.IsEnabled() { + if _, err := c.cache.Set(workspaceID, ff); err != nil { + return nil, err + } + } + return ff, nil +} + +func (c *clientImpl) refreshFeatureFlags(workspaceID string) error { + // fetch the feature flags from the provider + ff, err := c.provider.GetFeatureFlags(provider.ProviderParams{ + WorkspaceID: workspaceID, + Traits: c.defaultTraits, + }) + if err != nil { + return err + } + + // set the feature flags in the cache + if _, err := c.cache.Set(workspaceID, ff); err != nil { + return err + } + + return nil +} diff --git a/featureflags/client_test.go b/featureflags/client_test.go new file mode 100644 index 00000000..107cbec1 --- /dev/null +++ b/featureflags/client_test.go @@ -0,0 +1 @@ +package featureflags_test diff --git a/featureflags/errors.go b/featureflags/errors.go new file mode 100644 index 00000000..b7917701 --- /dev/null +++ b/featureflags/errors.go @@ -0,0 +1,14 @@ +package featureflags + +// featureError represents an error related to feature flag operations +type featureError struct { + Message string +} + +func (e *featureError) Error() string { + return e.Message +} + +func newFeatureError(message string) *featureError { + return &featureError{Message: message} +} diff --git a/featureflags/example/example.go b/featureflags/example/example.go new file mode 100644 index 00000000..c0432c2b --- /dev/null +++ b/featureflags/example/example.go @@ -0,0 +1,18 @@ +package main + +import ( + "fmt" + "log" + + "github.com/rudderlabs/rudder-go-kit/featureflags" +) + +func main() { + featureflags.SetDefaultTraits(map[string]string{"tier": "ENTERPRISE_V1"}) + isenabled, err := featureflags.IsFeatureEnabled("entTest", "enterpriseonlyfeature") + if err != nil { + log.Fatal(err) + } + fmt.Println(isenabled) + +} diff --git a/featureflags/featureflags.go b/featureflags/featureflags.go new file mode 100644 index 00000000..37d57a81 --- /dev/null +++ b/featureflags/featureflags.go @@ -0,0 +1,49 @@ +package featureflags + +import "github.com/rudderlabs/rudder-go-kit/featureflags/provider" + +func IsFeatureEnabled(workspaceID string, feature string) (bool, error) { + ff, err := getFeatureFlagClient().IsFeatureEnabled(workspaceID, feature) + if err != nil { + return false, err + } + return ff, nil +} + +// IsFeatureEnabledLatest checks if a feature is enabled for a workspace, bypassing the cache +// Note: This method always fetches fresh values from the provider(bypassing the cache), which may impact performance. +func IsFeatureEnabledLatest(workspaceID string, feature string) (bool, error) { + ff, err := getFeatureFlagClient().IsFeatureEnabledLatest(workspaceID, feature) + if err != nil { + return false, err + } + return ff, nil +} + +// GetFeatureValue gets the value of a feature for a workspace +// Note: Result may be stale if returned from cache. Use GetFeatureValueLatest if stale values are not acceptable. +func GetFeatureValue(workspaceID string, feature string) (provider.FeatureValue, error) { + ff, err := getFeatureFlagClient().GetFeatureValue(workspaceID, feature) + if err != nil { + return provider.FeatureValue{}, err + } + // create a copy of the feature value and return it + return ff, nil +} + +// GetFeatureValueLatest gets the value of a feature for a workspace, bypassing the cache +// Note: This method always fetches fresh values from the provider(bypassing the cache), which may impact performance. +func GetFeatureValueLatest(workspaceID string, feature string) (provider.FeatureValue, error) { + ff, err := getFeatureFlagClient().GetFeatureValueLatest(workspaceID, feature) + if err != nil { + return provider.FeatureValue{}, err + } + return ff, nil +} + +// SetDefaultTraits sets the default traits for the feature flag client +// These traits will always be used when fetching feature flags +// This function is only meant to be called once in the application lifecycle. +func SetDefaultTraits(traits map[string]string) { + getFeatureFlagClient().SetDefaultTraits(traits) +} diff --git a/featureflags/provider/flagsmith.go b/featureflags/provider/flagsmith.go new file mode 100644 index 00000000..8d7d3362 --- /dev/null +++ b/featureflags/provider/flagsmith.go @@ -0,0 +1,70 @@ +package provider + +import ( + "context" + "time" + + flagsmith "github.com/Flagsmith/flagsmith-go-client/v3" +) + +// FlagsmithProvider implements the Provider interface using Flagsmith +type FlagsmithProvider struct { + client *flagsmith.Client +} + +// NewFlagsmithProvider creates a new Flagsmith provider instance +func NewFlagsmithProvider(config ProviderConfig) (*FlagsmithProvider, error) { + + client := flagsmith.NewClient(config.ApiKey, + flagsmith.WithRequestTimeout(time.Duration(config.TimeoutInSeconds)*time.Second), + flagsmith.WithRetries(config.RetryAttempts, time.Duration(config.RetryWaitTimeInSeconds)*time.Second), + ) + if client == nil { + return nil, ErrProviderInit + } + + return &FlagsmithProvider{ + client: client, + }, nil +} + +// GetFeatureFlags implements Provider.GetFeatureFlags +func (p *FlagsmithProvider) GetFeatureFlags(params ProviderParams) (map[string]*FeatureValue, error) { + // Create traits map + traits := make([]*flagsmith.Trait, 0) + for k, v := range params.Traits { + traits = append(traits, &flagsmith.Trait{ + TraitKey: k, + TraitValue: v, + }) + } + + // Get flags for identity with traits + flags, err := p.client.GetIdentityFlags(context.Background(), params.WorkspaceID, traits) + if err != nil { + return nil, err + } + + now := time.Now() + result := make(map[string]*FeatureValue) + + // Convert flags to our format + for _, flag := range flags.AllFlags() { + featureValue := FeatureValue{ + Name: flag.FeatureName, + Enabled: flag.Enabled, + Value: flag.Value, + LastUpdatedAt: &now, + IsStale: false, + } + + result[flag.FeatureName] = &featureValue + } + + return result, nil +} + +// Name implements Provider.Name +func (p *FlagsmithProvider) Name() string { + return "flagsmith" +} diff --git a/featureflags/provider/provider.go b/featureflags/provider/provider.go new file mode 100644 index 00000000..c979ef21 --- /dev/null +++ b/featureflags/provider/provider.go @@ -0,0 +1,51 @@ +package provider + +import ( + "errors" + "time" +) + +var ErrProviderInit = errors.New("failed to initialize provider") + +// ProviderParams represents parameters for feature flag provider +type ProviderParams struct { + WorkspaceID string + Traits map[string]string +} + +// FeatureValue represents a feature flag value +type FeatureValue struct { + Name string + Enabled bool + Value interface{} + LastUpdatedAt *time.Time + IsStale bool + Error error +} + +// Provider defines the interface for feature flag providers +type Provider interface { + // GetFeatureFlags retrieves all feature flags for given parameters + GetFeatureFlags(params ProviderParams) (map[string]*FeatureValue, error) + + // Name returns the provider name + Name() string +} + +type ProviderConfig struct { + Type string + ApiKey string + TimeoutInSeconds int + RetryAttempts int + RetryWaitTimeInSeconds int +} + +func NewProvider(config ProviderConfig) (Provider, error) { + switch config.Type { + case "flagsmith": + return NewFlagsmithProvider(config) + default: + return nil, ErrProviderInit + } +} + diff --git a/featureflags/provider/provider_test.go b/featureflags/provider/provider_test.go new file mode 100644 index 00000000..c46edcb4 --- /dev/null +++ b/featureflags/provider/provider_test.go @@ -0,0 +1,51 @@ +package provider_test + +import ( + "testing" + + "github.com/rudderlabs/rudder-go-kit/featureflags/provider" + "github.com/stretchr/testify/assert" +) + +func TestFlagsmithProvider(t *testing.T) { + p, err := provider.NewFlagsmithProvider(provider.ProviderConfig{ + ApiKey: "test-api-key", + }) + assert.NoError(t, err) + assert.NotNil(t, p) + + t.Run("Name", func(t *testing.T) { + assert.Equal(t, "flagsmith", p.Name()) + }) + + t.Run("GetFeatureFlags", func(t *testing.T) { + params := provider.ProviderParams{ + WorkspaceID: "test-identity", + Traits: map[string]string{ + "email": "test@example.com", + "age": "25", + }, + } + + flags, err := p.GetFeatureFlags(params) + assert.NoError(t, err) + assert.IsType(t, map[string]provider.FeatureValue{}, flags) + + for _, flag := range flags { + assert.NotEmpty(t, flag.Name) + assert.NotNil(t, flag.LastUpdatedAt) + // Value and Enabled fields will depend on your Flagsmith configuration + } + }) + + t.Run("GetFeatureFlags_NoTraits", func(t *testing.T) { + params := provider.ProviderParams{ + WorkspaceID: "test-identity", + Traits: map[string]string{}, + } + + flags, err := p.GetFeatureFlags(params) + assert.NoError(t, err) + assert.IsType(t, map[string]provider.FeatureValue{}, flags) + }) +} diff --git a/go.mod b/go.mod index db870696..5087ddd4 100644 --- a/go.mod +++ b/go.mod @@ -63,6 +63,12 @@ require ( gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) +require ( + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/go-resty/resty/v2 v2.14.0 // indirect + github.com/itlightning/dateparse v0.2.0 // indirect +) + require ( cloud.google.com/go v0.115.1 // indirect cloud.google.com/go/auth v0.9.5 // indirect @@ -74,6 +80,7 @@ require ( github.com/Azure/azure-pipeline-go v0.2.3 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/DATA-DOG/go-sqlmock v1.5.2 + github.com/Flagsmith/flagsmith-go-client/v3 v3.7.0 github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/actgardner/gogen-avro/v10 v10.2.1 // indirect diff --git a/go.sum b/go.sum index 038ac78e..2edba478 100644 --- a/go.sum +++ b/go.sum @@ -47,6 +47,8 @@ github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7Oputl github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DataDog/zstd v1.5.6 h1:LbEglqepa/ipmmQJUDnSsfvA8e8IStVcGaFWDuxvGOY= github.com/DataDog/zstd v1.5.6/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/Flagsmith/flagsmith-go-client/v3 v3.7.0 h1:3uQbIaLZIxuh8b3MzYAQmsHaCDUUQbqwVwUTdbHdq4E= +github.com/Flagsmith/flagsmith-go-client/v3 v3.7.0/go.mod h1:bbGU66hX7Lx0fVIr56jF7dR4wlF+kIqTyKSTIRmCX1s= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -91,6 +93,8 @@ 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/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= @@ -207,6 +211,8 @@ github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8w github.com/go-redis/redis/v8 v8.4.2/go.mod h1:A1tbYoHSa1fXwN+//ljcCYYJeLmVrwL9hbQN45Jdy0M= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/go-resty/resty/v2 v2.14.0 h1:/rhkzsAqGQkozwfKS5aFAbb6TyKd3zyFRWcdRXLPCAU= +github.com/go-resty/resty/v2 v2.14.0/go.mod h1:IW6mekUOsElt9C7oWr0XRt9BNSD6D5rr9mhk6NjmNHg= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= @@ -306,6 +312,8 @@ github.com/in-toto/in-toto-golang v0.5.0 h1:hb8bgwr0M2hGdDsLjkJ3ZqJ8JFLL/tgYdAxF github.com/in-toto/in-toto-golang v0.5.0/go.mod h1:/Rq0IZHLV7Ku5gielPT4wPHJfH1GdHMCq8+WPxw8/BE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/itlightning/dateparse v0.2.0 h1:eOYLGZORnHweKdTZGOVjDXHhOwMQTNdP4g6+ErgPyeg= +github.com/itlightning/dateparse v0.2.0/go.mod h1:W2PH6/Sq+PuJJ6JUgx2nau+ew1KLGXwoGP1A240x204= 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= @@ -466,8 +474,8 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc h1:zAsgcP8MhzAbhMnB1QQ2O7ZhWYVGYSR2iVcjzQuPV+o= github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc/go.mod h1:S8xSOnV3CgpNrWd0GQ/OoQfMtlg2uPRSuTzcSGrzwK8= github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= @@ -634,7 +642,11 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -647,6 +659,9 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -668,7 +683,11 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -681,6 +700,9 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -709,15 +731,24 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 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/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -730,6 +761,9 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= @@ -744,6 +778,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=