From 94e54c39a9f1fb775e7c81eed86e3892e5d602a6 Mon Sep 17 00:00:00 2001 From: Jamie Sinn Date: Mon, 3 Jun 2024 12:08:39 -0400 Subject: [PATCH 1/9] SDKConfig Event --- testing_helpers.go | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 testing_helpers.go diff --git a/testing_helpers.go b/testing_helpers.go new file mode 100644 index 00000000..e69de29b From c6cb4e7c9e64aed50fa23da24001270008d99ebd Mon Sep 17 00:00:00 2001 From: Jamie Sinn Date: Mon, 3 Jun 2024 13:22:58 -0400 Subject: [PATCH 2/9] SSE - Squash --- .github/workflows/test_examples.yml | 4 +- api/model_event.go | 27 +- api/model_sse.go | 10 + bucketing/event_queue.go | 3 +- bucketing/model_config_body.go | 1 + client.go | 66 ++- client_native_bucketing.go | 4 + client_test.go | 100 ++-- configmanager.go | 223 +++++++-- configmanager_test.go | 120 ++++- configuration.go | 35 +- event_manager.go | 44 +- event_manager_test.go | 1 + example/local/main.go | 10 +- example/openfeature/main.go | 14 +- go.mod | 1 + go.sum | 11 + go.work.sum | 657 +++++++++++++++++++++++++ openfeature_provider.go | 6 +- ssemanager.go | 188 +++++++ ssemanager_test.go | 26 + testdata/fixture_small_config_sse.json | 162 ++++++ testing_helpers_test.go | 25 +- 23 files changed, 1543 insertions(+), 195 deletions(-) create mode 100644 api/model_sse.go create mode 100644 go.work.sum create mode 100644 ssemanager.go create mode 100644 ssemanager_test.go create mode 100644 testdata/fixture_small_config_sse.json diff --git a/.github/workflows/test_examples.yml b/.github/workflows/test_examples.yml index b2c54460..efc2b6f0 100644 --- a/.github/workflows/test_examples.yml +++ b/.github/workflows/test_examples.yml @@ -1,5 +1,5 @@ name: Test Examples - +## For anyone looking to change this (Internal to DevCycle) - the project is here: https://app.devcycle.com/o/org_U9F8YMaTChTEndWw/p/git-hub-actions-integration-tests/features/6642210af1c941418857b237 on: pull_request: branches: [ main ] @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest env: DEVCYCLE_SERVER_SDK_KEY: ${{ secrets.DEVCYCLE_SERVER_SDK_KEY }} - DEVCYCLE_VARIABLE_KEY: test-boolean-variable + DEVCYCLE_VARIABLE_KEY: go-example-tests steps: - uses: actions/checkout@v4 diff --git a/api/model_event.go b/api/model_event.go index bfc73a86..320fc83d 100644 --- a/api/model_event.go +++ b/api/model_event.go @@ -1,11 +1,3 @@ -/* - * DevCycle Bucketing API - * - * Documents the DevCycle Bucketing API which provides and API interface to User Bucketing and for generated SDKs. - * - * API version: 1.0.0 - * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) - */ package api import ( @@ -21,6 +13,25 @@ const ( EventType_CustomEvent = "customEvent" ) +type ClientEvent struct { + EventType ClientEventType `json:"eventType"` + EventData interface{} `json:"eventData"` + Status string `json:"status"` + Error error `json:"error"` +} + +type ClientEventType string + +const ( + ClientEventType_Initialized ClientEventType = "initialized" + ClientEventType_Error ClientEventType = "error" + ClientEventType_ConfigUpdated ClientEventType = "configUpdated" + ClientEventType_RealtimeUpdates ClientEventType = "realtimeUpdates" + ClientEventType_InternalSSEFailure ClientEventType = "internalSSEFailure" + ClientEventType_InternalNewConfigAvailable ClientEventType = "internalNewConfigAvailable" + ClientEventType_InternalSSEConnected ClientEventType = "internalSSEConnected" +) + type Event struct { Type_ string `json:"type"` Target string `json:"target,omitempty"` diff --git a/api/model_sse.go b/api/model_sse.go new file mode 100644 index 00000000..700c5ff7 --- /dev/null +++ b/api/model_sse.go @@ -0,0 +1,10 @@ +package api + +type MinimalConfig struct { + SSE *SSEHost `json:"sse,omitempty"` +} + +type SSEHost struct { + Hostname string `json:"hostname,omitempty"` + Path string `json:"path,omitempty"` +} diff --git a/bucketing/event_queue.go b/bucketing/event_queue.go index 8791d37c..c576dd0c 100644 --- a/bucketing/event_queue.go +++ b/bucketing/event_queue.go @@ -262,7 +262,6 @@ func (eq *EventQueue) FlushEventQueue(clientUUID, configEtag, rayId, lastModifie records = append(records, eq.userEventQueue.BuildBatchRecords()...) eq.aggEventQueue = make(AggregateEventQueue) eq.userEventQueue = make(UserEventQueue) - eq.userEventQueueCount = 0 for _, record := range records { var payload *api.FlushPayload @@ -286,7 +285,7 @@ func (eq *EventQueue) FlushEventQueue(clientUUID, configEtag, rayId, lastModifie } eq.pendingPayloads[payload.PayloadId] = *payload } - + eq.userEventQueueCount = 0 eq.updateFailedPayloads() eq.eventsFlushed.Add(int32(len(eq.pendingPayloads))) diff --git a/bucketing/model_config_body.go b/bucketing/model_config_body.go index 280f9cf5..8b5db1e6 100644 --- a/bucketing/model_config_body.go +++ b/bucketing/model_config_body.go @@ -33,6 +33,7 @@ type configBody struct { etag string rayId string lastModified string + SSE api.SSEHost `json:"sse,omitempty"` variableIdMap map[string]*Variable variableKeyMap map[string]*Variable variableIdToFeatureMap map[string]*ConfigFeature diff --git a/client.go b/client.go index cdc0ad0e..fb264518 100644 --- a/client.go +++ b/client.go @@ -52,8 +52,8 @@ type Client struct { localBucketing LocalBucketing platformData *PlatformData // Set to true when the client has been initialized, regardless of whether the config has loaded successfully. - isInitialized bool - internalOnInitializedChannel chan bool + isInitialized bool + internalClientEventChannel chan api.ClientEvent } type LocalBucketing interface { @@ -84,8 +84,11 @@ func NewClient(sdkKey string, options *Options) (*Client, error) { util.Errorf("%v", err) return nil, err } + if options == nil { + return nil, errors.New("missing options! Call NewClient with valid options") + } if !sdkKeyIsValid(sdkKey) { - return nil, fmt.Errorf("Invalid sdk key. Call NewClient with a valid sdk key.") + return nil, fmt.Errorf("invalid sdk key %s. Call NewClient with a valid sdk key", sdkKey) } options.CheckDefaults() cfg := NewConfiguration(options) @@ -99,6 +102,7 @@ func NewClient(sdkKey string, options *Options) (*Client, error) { } else { c.platformData = GeneratePlatformData() } + c.internalClientEventChannel = make(chan api.ClientEvent, 1) if c.DevCycleOptions.Logger != nil { util.SetLogger(c.DevCycleOptions.Logger) @@ -106,8 +110,6 @@ func NewClient(sdkKey string, options *Options) (*Client, error) { if c.IsLocalBucketing() { util.Infof("Using Native Bucketing") - c.internalOnInitializedChannel = make(chan bool, 1) - err := c.setLBClient(sdkKey, options) if err != nil { return c, fmt.Errorf("Error setting up local bucketing: %w", err) @@ -119,29 +121,27 @@ func NewClient(sdkKey string, options *Options) (*Client, error) { return c, fmt.Errorf("Error initializing event queue: %w", err) } - c.configManager = NewEnvironmentConfigManager(sdkKey, c.localBucketing, c.eventQueue, options, c.cfg) - - c.configManager.StartPolling(options.ConfigPollingIntervalMS) + c.configManager, err = NewEnvironmentConfigManager(sdkKey, c.localBucketing, c.eventQueue, options, c.cfg) - if c.DevCycleOptions.OnInitializedChannel != nil { - // TODO: Pass this error back via a channel internally + if err != nil { + return nil, fmt.Errorf("Error initializing config manager: %w", err) + } + if c.DevCycleOptions.ClientEventHandler != nil { go func() { _ = c.configManager.initialFetch() c.handleInitialization() }() } else { - err := c.configManager.initialFetch() + err = c.configManager.initialFetch() c.handleInitialization() - return c, err - } - } else { - util.Infof("Using Cloud Bucketing") - if c.DevCycleOptions.OnInitializedChannel != nil { - go func() { - c.DevCycleOptions.OnInitializedChannel <- true - }() + if err != nil { + return c, err + } } + return c, err } + + c.handleInitialization() return c, nil } @@ -150,18 +150,26 @@ func (c *Client) IsLocalBucketing() bool { } func (c *Client) handleInitialization() { - c.isInitialized = true - + bucketingInitMessage := "Using cloud bucketing with hostname: " + c.DevCycleOptions.BucketingAPIURI if c.IsLocalBucketing() { - util.Infof("Client initialized with local bucketing %v", c.localBucketing.GetUUID()) + bucketingInitMessage = fmt.Sprintf("Client initialized with local bucketing %v", c.localBucketing.GetUUID()) } - if c.DevCycleOptions.OnInitializedChannel != nil { + initEvent := api.ClientEvent{ + EventType: api.ClientEventType_Initialized, + EventData: bucketingInitMessage, + Status: "success", + Error: nil, + } + c.internalClientEventChannel <- initEvent + c.isInitialized = true + + if c.DevCycleOptions.ClientEventHandler != nil { go func() { - c.DevCycleOptions.OnInitializedChannel <- true + c.DevCycleOptions.ClientEventHandler <- initEvent }() - } - c.internalOnInitializedChannel <- true + util.Infof(bucketingInitMessage) + } func (c *Client) generateBucketedConfig(user User) (config *BucketedUserConfig, err error) { @@ -508,7 +516,11 @@ func (c *Client) Close() (err error) { if !c.isInitialized { util.Infof("Awaiting client initialization before closing") - <-c.internalOnInitializedChannel + for event := range c.internalClientEventChannel { + if event.EventType == api.ClientEventType_Initialized { + break + } + } } if c.eventQueue != nil { diff --git a/client_native_bucketing.go b/client_native_bucketing.go index 1bef67a5..15329398 100644 --- a/client_native_bucketing.go +++ b/client_native_bucketing.go @@ -37,6 +37,10 @@ type NativeLocalBucketing struct { clientUUID string } +func (n *NativeLocalBucketing) GetUUID() string { + return n.clientUUID +} + func NewNativeLocalBucketing(sdkKey string, platformData *api.PlatformData, options *Options) (*NativeLocalBucketing, error) { clientUUID := uuid.New().String() diff --git a/client_test.go b/client_test.go index 66859974..7574a950 100644 --- a/client_test.go +++ b/client_test.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/devcyclehq/go-server-sdk/v2/api" "github.com/devcyclehq/go-server-sdk/v2/util" + "github.com/jarcoal/httpmock" "github.com/stretchr/testify/require" "io" "log" @@ -14,8 +15,6 @@ import ( "sync/atomic" "testing" "time" - - "github.com/jarcoal/httpmock" ) func TestClient_AllFeatures_Local(t *testing.T) { @@ -77,6 +76,7 @@ func TestClient_AllVariablesLocal_WithSpecialCharacters(t *testing.T) { func TestClient_VariableCloud(t *testing.T) { sdkKey := generateTestSDKKey() + httpBucketingAPIMock() c, err := NewClient(sdkKey, &Options{EnableCloudBucketing: true, ConfigPollingIntervalMS: 10 * time.Second}) fatalErr(t, err) @@ -91,6 +91,7 @@ func TestClient_VariableCloud(t *testing.T) { } func TestClient_VariableLocalNumber(t *testing.T) { + sdkKey := generateTestSDKKey() httpCustomConfigMock(sdkKey, 200, test_large_config) @@ -323,10 +324,11 @@ func TestClient_TrackLocal_QueueEventBeforeConfig(t *testing.T) { func TestProduction_Local(t *testing.T) { environmentKey := os.Getenv("DEVCYCLE_SERVER_SDK_KEY") - user := User{UserId: "test"} if environmentKey == "" { t.Skip("DEVCYCLE_SERVER_SDK_KEY not set. Not using production tests.") } + user := User{UserId: "test"} + dvcOptions := Options{ EnableEdgeDB: false, EnableCloudBucketing: false, @@ -349,61 +351,75 @@ func TestProduction_Local(t *testing.T) { } } -func TestClient_Validate_OnInitializedChannel_EnableCloudBucketing_Options(t *testing.T) { - sdkKey, _ := httpConfigMock(200) - - onInitialized := make(chan bool) +func TestClient_CloudBucketingHandler(t *testing.T) { - // Try each of the combos to make sure they all act as expected and don't hang - dvcOptions := Options{OnInitializedChannel: onInitialized, EnableCloudBucketing: true} - c, err := NewClient(sdkKey, &dvcOptions) + sdkKey := generateTestSDKKey() + httpBucketingAPIMock() + clientEventHandler := make(chan api.ClientEvent, 10) + c, err := NewClient(sdkKey, &Options{EnableCloudBucketing: true, ClientEventHandler: clientEventHandler}) fatalErr(t, err) - val := <-onInitialized - if !val { - t.Fatal("Expected true from onInitialized channel") - } + init := <-clientEventHandler - if c.isInitialized { - // isInitialized is only relevant when using Local Bucketing - t.Fatal("Expected isInitialized to be false") + if init.EventType != api.ClientEventType_Initialized { + t.Fatal("Expected initialized event") } - - dvcOptions = Options{OnInitializedChannel: onInitialized, EnableCloudBucketing: false} - c, err = NewClient(sdkKey, &dvcOptions) - fatalErr(t, err) - val = <-onInitialized - if !val { - t.Fatal("Expected true from onInitialized channel") + if !c.isInitialized { + t.Fatal("Expected client to be initialized") } +} + +func TestClient_LocalBucketingHandler(t *testing.T) { + sdkKey, _ := httpConfigMock(200) + clientEventHandler := make(chan api.ClientEvent, 10) + c, err := NewClient(sdkKey, &Options{ClientEventHandler: clientEventHandler}) + fatalErr(t, err) + event1 := <-clientEventHandler + event2 := <-clientEventHandler + switch event1.EventType { + case api.ClientEventType_Initialized: + if event2.EventType != api.ClientEventType_ConfigUpdated { + t.Fatal("Expected config updated event and initialized events") + } + case api.ClientEventType_ConfigUpdated: + if event2.EventType != api.ClientEventType_Initialized { + t.Fatal("Expected initialized and config updated events") + } + } if !c.isInitialized { - t.Fatal("Expected isInitialized to be true") + t.Fatal("Expected client to be initialized") } - if !c.hasConfig() { - t.Fatal("Expected config to be loaded") + t.Fatal("Expected client to have config") } +} - dvcOptions = Options{OnInitializedChannel: nil, EnableCloudBucketing: true} - c, err = NewClient(sdkKey, &dvcOptions) - fatalErr(t, err) +func TestClient_ConfigUpdatedEvent(t *testing.T) { + responder := func(req *http.Request) (*http.Response, error) { + reqBody, err := io.ReadAll(req.Body) + if err != nil { + return nil, err + } + if !strings.Contains(string(reqBody), api.EventType_SDKConfig) { + return nil, fmt.Errorf("Expected config updated event") + } - if c.isInitialized { - // isInitialized is only relevant when using Local Bucketing - t.Fatal("Expected isInitialized to be false") + return httpmock.NewStringResponse(201, `{}`), nil } - - dvcOptions = Options{OnInitializedChannel: nil, EnableCloudBucketing: false} - c, err = NewClient(sdkKey, &dvcOptions) + httpmock.RegisterResponder("POST", "https://config-updated.devcycle.com/v1/events/batch", responder) + sdkKey, _ := httpConfigMock(200) + c, err := NewClient(sdkKey, &Options{EventsAPIURI: "https://config-updated.devcycle.com"}) fatalErr(t, err) - if !c.isInitialized { - t.Fatal("Expected isInitialized to be true") + t.Fatal("Expected client to be initialized") } - if !c.hasConfig() { - t.Fatal("Expected config to be loaded") + t.Fatal("Expected client to have config") } + _ = c.FlushEvents() + require.Eventually(t, func() bool { + return httpmock.GetCallCountInfo()["POST https://config-updated.devcycle.com/v1/events/batch"] >= 1 + }, 1*time.Second, 100*time.Millisecond) } func TestClient_ConfigUpdatedEvent(t *testing.T) { sdkKey, _ := httpConfigMock(200) @@ -470,9 +486,9 @@ func TestClient_ConfigUpdatedEvent_VariableEval(t *testing.T) { func BenchmarkClient_VariableSerial(b *testing.B) { util.SetLogger(util.DiscardLogger{}) + sdkKey := generateTestSDKKey() httpCustomConfigMock(sdkKey, 200, test_large_config) - httpEventsApiMock() if benchmarkDisableLogs { log.SetOutput(io.Discard) @@ -515,9 +531,9 @@ func BenchmarkClient_VariableSerial(b *testing.B) { func BenchmarkClient_VariableParallel(b *testing.B) { util.SetLogger(util.DiscardLogger{}) + sdkKey := generateTestSDKKey() httpCustomConfigMock(sdkKey, 200, test_large_config) - httpEventsApiMock() if benchmarkDisableLogs { log.SetOutput(io.Discard) diff --git a/configmanager.go b/configmanager.go index 8efb2215..925970c4 100644 --- a/configmanager.go +++ b/configmanager.go @@ -4,8 +4,10 @@ import ( "context" "encoding/json" "fmt" + "github.com/devcyclehq/go-server-sdk/v2/api" "io" "net/http" + "sync" "time" "github.com/devcyclehq/go-server-sdk/v2/util" @@ -22,15 +24,26 @@ type ConfigReceiver interface { } type EnvironmentConfigManager struct { - sdkKey string - localBucketing ConfigReceiver - firstLoad bool - context context.Context - stopPolling context.CancelFunc - httpClient *http.Client - cfg *HTTPConfiguration - eventManager *EventManager - ticker *time.Ticker + sdkKey string + minimalConfig *api.MinimalConfig + localBucketing ConfigReceiver + firstLoad bool + context context.Context + shutdown context.CancelFunc + pollingManager *configPollingManager + httpClient *http.Client + cfg *HTTPConfiguration + sseManager *SSEManager + options *Options + InternalClientEvents chan api.ClientEvent + eventManager *EventManager + pollingMutex sync.Mutex +} + +type configPollingManager struct { + context context.Context + ticker *time.Ticker + stopPolling context.CancelFunc } func NewEnvironmentConfigManager( @@ -39,38 +52,139 @@ func NewEnvironmentConfigManager( manager *EventManager, options *Options, cfg *HTTPConfiguration, -) (e *EnvironmentConfigManager) { - configManager := &EnvironmentConfigManager{ +) (configManager *EnvironmentConfigManager, err error) { + configManager = &EnvironmentConfigManager{ + options: options, sdkKey: sdkKey, localBucketing: localBucketing, cfg: cfg, - httpClient: &http.Client{ - // Set an explicit timeout so that we don't wait forever on a request - // Use the configurable timeout because fetching the first config can block SDK initialization. - Timeout: options.RequestTimeout, - }, - eventManager: manager, - firstLoad: true, + httpClient: cfg.HTTPClient, + firstLoad: true, + } + configManager.InternalClientEvents = make(chan api.ClientEvent, 100) + + configManager.context, configManager.shutdown = context.WithCancel(context.Background()) + configManager.eventManager = manager + + if options.EnableBetaRealtimeUpdates { + sseManager, err := newSSEManager(configManager, options, cfg) + if err != nil { + return nil, err + } + configManager.sseManager = sseManager + go configManager.ssePollingManager() + } else { + configManager.StartPolling(options.ConfigPollingIntervalMS) } + return configManager, err +} - configManager.context, configManager.stopPolling = context.WithCancel(context.Background()) +func (e *EnvironmentConfigManager) ssePollingManager() { + for { + select { + case <-e.context.Done(): + util.Warnf("Stopping SSE polling.") + return + case event := <-e.InternalClientEvents: + switch event.EventType { + case api.ClientEventType_InternalNewConfigAvailable: + minimumLastUpdated := event.EventData.(time.Time) + if e.GetLastModified() != "" { + currentLastModified, err := time.Parse(time.RFC1123, e.GetLastModified()) + if err != nil { + util.Warnf("Error parsing last modified time: %s\n", err) + e.InternalClientEvents <- api.ClientEvent{ + EventType: api.ClientEventType_Error, + EventData: "Error parsing last modified time: " + err.Error(), + Status: "error", + Error: err, + } + } + if currentLastModified.After(minimumLastUpdated) { + // Skip fetching config if the current config is newer than the minimumLastUpdated + continue + } + } + + err := e.fetchConfig(CONFIG_RETRIES, minimumLastUpdated) + if err != nil { + util.Warnf("Error fetching config: %s\n", err) + e.InternalClientEvents <- api.ClientEvent{ + EventType: api.ClientEventType_Error, + EventData: "Error fetching config: " + err.Error(), + Status: "error", + Error: err, + } + } + + case api.ClientEventType_InternalSSEFailure: + // Re-enable polling until a valid config is fetched, and then re-initialize SSE. + e.sseManager.StopSSE() + e.StartPolling(e.options.ConfigPollingIntervalMS) + + case api.ClientEventType_InternalSSEConnected: + e.StartPolling(time.Minute * 10) + + case api.ClientEventType_ConfigUpdated: + eventData := event.EventData.(map[string]string) + + if url, ok := eventData["sseUrl"]; ok && e.options.EnableBetaRealtimeUpdates && e.sseManager != nil { + // Reconnect SSE + if e.sseManager.url != url || !e.sseManager.Connected.Load() { + err := e.StartSSE(url) + if err != nil { + e.InternalClientEvents <- api.ClientEvent{ + EventType: api.ClientEventType_Error, + EventData: "Error starting SSE after config update: " + err.Error(), + Status: "error", + Error: err, + } + } + } + } + } + } + } +} + +func (e *EnvironmentConfigManager) StartSSE(url string) error { + if !e.options.EnableBetaRealtimeUpdates { + return fmt.Errorf("realtime updates are disabled. Cannot start SSE") + } + return e.sseManager.StartSSEOverride(url) +} - return configManager +func (e *EnvironmentConfigManager) StopPolling() { + if e.pollingManager != nil { + e.pollingManager.stopPolling() + } } -func (e *EnvironmentConfigManager) StartPolling( - interval time.Duration, -) { - e.ticker = time.NewTicker(interval) +func (e *EnvironmentConfigManager) StartPolling(interval time.Duration) { + e.pollingMutex.Lock() + defer e.pollingMutex.Unlock() + if e.pollingManager != nil { + e.pollingManager.stopPolling() + } + pollingManager := &configPollingManager{ + context: nil, + ticker: time.NewTicker(interval), + stopPolling: nil, + } + pollingManager.context, pollingManager.stopPolling = context.WithCancel(e.context) + e.pollingManager = pollingManager go func() { for { + if e.pollingManager == nil { + return + } select { case <-e.context.Done(): util.Warnf("Stopping config polling.") - e.ticker.Stop() + e.pollingManager.ticker.Stop() return - case <-e.ticker.C: + case <-e.pollingManager.ticker.C: err := e.fetchConfig(CONFIG_RETRIES) if err != nil { util.Warnf("Error fetching config: %s\n", err) @@ -81,10 +195,11 @@ func (e *EnvironmentConfigManager) StartPolling( } func (e *EnvironmentConfigManager) initialFetch() error { + return e.fetchConfig(CONFIG_RETRIES) } -func (e *EnvironmentConfigManager) fetchConfig(numRetriesRemaining int) (err error) { +func (e *EnvironmentConfigManager) fetchConfig(numRetriesRemaining int, minimumLastModified ...time.Time) (err error) { defer func() { if r := recover(); r != nil { // get the stack trace and potentially log it here @@ -99,14 +214,18 @@ func (e *EnvironmentConfigManager) fetchConfig(numRetriesRemaining int) (err err etag := e.localBucketing.GetETag() lastModified := e.localBucketing.GetLastModified() - + if len(minimumLastModified) > 0 { + lastModified = minimumLastModified[0].Format(time.RFC1123) + } if etag != "" { req.Header.Set("If-None-Match", etag) } if lastModified != "" { req.Header.Set("If-Modified-Since", lastModified) } + resp, err := e.httpClient.Do(req) + if err != nil { if numRetriesRemaining > 0 { util.Warnf("Retrying config fetch %d more times. Error: %s", numRetriesRemaining, err) @@ -115,6 +234,7 @@ func (e *EnvironmentConfigManager) fetchConfig(numRetriesRemaining int) (err err return err } defer resp.Body.Close() + switch statusCode := resp.StatusCode; { case statusCode == http.StatusOK: resp.Request = req @@ -122,7 +242,7 @@ func (e *EnvironmentConfigManager) fetchConfig(numRetriesRemaining int) (err err case statusCode == http.StatusNotModified: return nil case statusCode == http.StatusForbidden: - e.stopPolling() + e.StopPolling() return fmt.Errorf("invalid SDK key. Aborting config polling") case statusCode >= 500: // Retryable Errors. Continue polling. @@ -183,11 +303,46 @@ func (e *EnvironmentConfigManager) setConfigFromResponse(response *http.Response } func (e *EnvironmentConfigManager) setConfig(config []byte, eTag, rayId, lastModified string) error { + configUpdatedEvent := api.ClientEvent{ + EventType: api.ClientEventType_ConfigUpdated, + EventData: map[string]string{ + "rayId": rayId, + "eTag": eTag, + "lastModified": lastModified, + "sseUrl": "", + }, + Status: "success", + Error: nil, + } + defer func() { + go func() { + e.InternalClientEvents <- configUpdatedEvent + if e.options.ClientEventHandler != nil { + e.options.ClientEventHandler <- configUpdatedEvent + } + }() + }() err := e.localBucketing.StoreConfig(config, eTag, rayId, lastModified) if err != nil { + configUpdatedEvent.EventType = api.ClientEventType_Error + configUpdatedEvent.Status = "error" + configUpdatedEvent.Error = err return err } + err = json.Unmarshal(e.GetRawConfig(), &e.minimalConfig) + if err != nil { + configUpdatedEvent.EventType = api.ClientEventType_Error + configUpdatedEvent.Status = "error" + configUpdatedEvent.Error = err + return err + } + if e.minimalConfig != nil && e.minimalConfig.SSE != nil { + sseUrl := fmt.Sprintf("%s%s", e.minimalConfig.SSE.Hostname, e.minimalConfig.SSE.Path) + if e.sseManager != nil && e.sseManager.url != sseUrl { + configUpdatedEvent.EventData.(map[string]string)["sseUrl"] = sseUrl + } + } return nil } @@ -214,5 +369,13 @@ func (e *EnvironmentConfigManager) GetLastModified() string { } func (e *EnvironmentConfigManager) Close() { - e.stopPolling() + e.shutdown() + e.pollingMutex.Lock() + defer e.pollingMutex.Unlock() + if e.pollingManager != nil { + e.pollingManager.stopPolling() + } + if e.sseManager != nil { + e.sseManager.Close() + } } diff --git a/configmanager_test.go b/configmanager_test.go index ac8d2a98..7e589ac0 100644 --- a/configmanager_test.go +++ b/configmanager_test.go @@ -2,10 +2,12 @@ package devcycle import ( "fmt" + "github.com/devcyclehq/go-server-sdk/v2/api" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/require" "net/http" "testing" - - "github.com/jarcoal/httpmock" + "time" ) type recordingConfigReceiver struct { @@ -13,13 +15,15 @@ type recordingConfigReceiver struct { etag string rayId string lastModified string + config []byte } -func (r *recordingConfigReceiver) StoreConfig(_ []byte, etag, rayId, lastModified string) error { +func (r *recordingConfigReceiver) StoreConfig(c []byte, etag, rayId, lastModified string) error { r.configureCount++ r.etag = etag r.rayId = rayId r.lastModified = lastModified + r.config = c return nil } @@ -36,7 +40,7 @@ func (r *recordingConfigReceiver) GetRayId() string { } func (r *recordingConfigReceiver) GetRawConfig() []byte { - return nil + return r.config } func (r *recordingConfigReceiver) GetLastModified() string { @@ -44,11 +48,14 @@ func (r *recordingConfigReceiver) GetLastModified() string { } func TestEnvironmentConfigManager_fetchConfig_success(t *testing.T) { - sdkKey, _ := httpConfigMock(200) + sdkKey, _ := httpConfigMock(200) localBucketing := &recordingConfigReceiver{} - manager := NewEnvironmentConfigManager(sdkKey, localBucketing, nil, test_options, NewConfiguration(test_options)) + testOptionsWithHandler := *test_options + testOptionsWithHandler.ClientEventHandler = make(chan api.ClientEvent, 10) + manager, _ := NewEnvironmentConfigManager(sdkKey, localBucketing, nil, &testOptionsWithHandler, NewConfiguration(&testOptionsWithHandler)) + defer manager.Close() err := manager.initialFetch() if err != nil { t.Fatal(err) @@ -63,6 +70,40 @@ func TestEnvironmentConfigManager_fetchConfig_success(t *testing.T) { if manager.GetETag() != "TESTING" { t.Fatal("cm.configEtag != TESTING") } + event1 := <-testOptionsWithHandler.ClientEventHandler + if event1.Status != "success" { + fmt.Println(event1) + t.Fatal("event1.Status != success") + } +} + +func TestEnvironmentConfigManager_fetchConfig_success_sse(t *testing.T) { + + sdkKey, _ := httpSSEConfigMock(200) + httpSSEConnectionMock() + + localBucketing := &recordingConfigReceiver{} + + manager, _ := NewEnvironmentConfigManager(sdkKey, localBucketing, nil, test_options_sse, NewConfiguration(test_options_sse)) + defer manager.Close() + err := manager.initialFetch() + fatalErr(t, err) + if localBucketing.configureCount != 1 { + t.Fatal("localBucketing.configureCount != 1") + } + if !manager.HasConfig() { + t.Fatal("cm.hasConfig != true") + } + if manager.GetETag() != "TESTING" { + t.Fatal("cm.configEtag != TESTING") + } + if manager.sseManager == nil { + t.Fatal("cm.sseManager == nil") + } + require.Eventually(t, func() bool { + return manager.sseManager.Connected.Load() + }, 3*time.Second, 10*time.Millisecond) + } func TestEnvironmentConfigManager_fetchConfig_retries500(t *testing.T) { @@ -75,12 +116,10 @@ func TestEnvironmentConfigManager_fetchConfig_retries500(t *testing.T) { ) localBucketing := &recordingConfigReceiver{} - manager := NewEnvironmentConfigManager(sdkKey, localBucketing, nil, test_options, NewConfiguration(test_options)) - + manager, _ := NewEnvironmentConfigManager(sdkKey, localBucketing, nil, test_options, NewConfiguration(test_options)) + defer manager.Close() err := manager.initialFetch() - if err != nil { - t.Fatal(err) - } + fatalErr(t, err) if !manager.HasConfig() { t.Fatal("cm.hasConfig != true") } @@ -90,19 +129,20 @@ func TestEnvironmentConfigManager_fetchConfig_retries500(t *testing.T) { if manager.GetLastModified() != "LAST-MODIFIED" { t.Fatal("cm.lastModified != LAST-MODIFIED") } + } func TestEnvironmentConfigManager_fetchConfig_retries_errors(t *testing.T) { - sdkKey := generateTestSDKKey() - connectionErrorResponse := httpmock.NewErrorResponder(fmt.Errorf("connection error")) + connectionErrorResponse := httpmock.NewErrorResponder(fmt.Errorf("connection error")) + sdkKey := generateTestSDKKey() httpmock.RegisterResponder("GET", "https://config-cdn.devcycle.com/config/v1/server/"+sdkKey+".json", errorResponseChain(sdkKey, connectionErrorResponse, CONFIG_RETRIES), ) localBucketing := &recordingConfigReceiver{} - manager := NewEnvironmentConfigManager(sdkKey, localBucketing, nil, test_options, NewConfiguration(test_options)) - + manager, _ := NewEnvironmentConfigManager(sdkKey, localBucketing, nil, test_options, NewConfiguration(test_options)) + defer manager.Close() err := manager.initialFetch() if err != nil { t.Fatal(err) @@ -118,22 +158,68 @@ func TestEnvironmentConfigManager_fetchConfig_retries_errors(t *testing.T) { } } -func TestEnvironmentConfigManager_fetchConfig_returns_errors(t *testing.T) { +func TestEnvironmentConfigManager_fetchConfig_retries_errors_sse(t *testing.T) { sdkKey := generateTestSDKKey() + httpSSEConnectionMock() connectionErrorResponse := httpmock.NewErrorResponder(fmt.Errorf("connection error")) + httpmock.RegisterResponder("GET", "https://config-cdn.devcycle.com/config/v1/server/"+sdkKey+".json", + errorResponseChain(sdkKey, connectionErrorResponse, CONFIG_RETRIES, httpSSEConfigMock), + ) + + localBucketing := &recordingConfigReceiver{} + manager, _ := NewEnvironmentConfigManager(sdkKey, localBucketing, nil, test_options_sse, NewConfiguration(test_options_sse)) + defer manager.Close() + err := manager.initialFetch() + fatalErr(t, err) + + if !manager.HasConfig() { + t.Fatal("cm.hasConfig != true") + } +} + +func TestEnvironmentConfigManager_fetchConfig_returns_errors(t *testing.T) { + + sdkKey := generateTestSDKKey() + connectionErrorResponse := httpmock.NewErrorResponder(fmt.Errorf("connection error")) + + httpmock.RegisterResponder("GET", "https://config-cdn.devcycle.com/config/v1/server/"+sdkKey+".json", + errorResponseChain(sdkKey, connectionErrorResponse, CONFIG_RETRIES+1), + ) + localBucketing := &recordingConfigReceiver{} + manager, _ := NewEnvironmentConfigManager(sdkKey, localBucketing, nil, test_options, NewConfiguration(test_options)) + defer manager.Close() + err := manager.initialFetch() + if err == nil { + t.Fatal("expected error but got nil") + } +} + +func TestEnvironmentConfigManager_fetchConfig_returns_errors_sse(t *testing.T) { + + connectionErrorResponse := httpmock.NewErrorResponder(fmt.Errorf("connection error")) + sdkKey := generateTestSDKKey() httpmock.RegisterResponder("GET", "https://config-cdn.devcycle.com/config/v1/server/"+sdkKey+".json", errorResponseChain(sdkKey, connectionErrorResponse, CONFIG_RETRIES+1), ) localBucketing := &recordingConfigReceiver{} - manager := NewEnvironmentConfigManager(sdkKey, localBucketing, nil, test_options, NewConfiguration(test_options)) + manager, _ := NewEnvironmentConfigManager(sdkKey, localBucketing, nil, test_options_sse, NewConfiguration(test_options_sse)) + defer manager.Close() err := manager.initialFetch() if err == nil { t.Fatal("expected error but got nil") } + + if manager.HasConfig() { + t.Fatal("manager.hasConfig == true") + } + if manager.sseManager.Started { + t.Fatal("manager.sseManager.Started == true") + } + } func errorResponseChain(sdkKey string, errorResponse httpmock.Responder, count int, configMock ...func(respcode int, sdkKeys ...string) (string, httpmock.Responder)) httpmock.Responder { diff --git a/configuration.go b/configuration.go index 3702692f..9b176ae2 100644 --- a/configuration.go +++ b/configuration.go @@ -14,38 +14,6 @@ import ( type EventQueueOptions = api.EventQueueOptions -type contextKey string - -func (c contextKey) String() string { - return "auth " + string(c) -} - -var ( - // ContextOAuth2 takes a oauth2.TokenSource as authentication for the request. - ContextOAuth2 = contextKey("token") - - // ContextBasicAuth takes BasicAuth as authentication for the request. - ContextBasicAuth = contextKey("basic") - - // ContextAccessToken takes a string oauth2 access token as authentication for the request. - ContextAccessToken = contextKey("accesstoken") - - // ContextAPIKey takes an APIKey as authentication for the request - ContextAPIKey = contextKey("apikey") -) - -// BasicAuth provides basic http authentication to a request passed via context using ContextBasicAuth -type BasicAuth struct { - UserName string `json:"userName,omitempty"` - Password string `json:"password,omitempty"` -} - -// APIKey provides API key based authentication to a request passed via context using ContextAPIKey -type APIKey struct { - Key string - Prefix string -} - type AdvancedOptions struct { OverridePlatformData *api.PlatformData } @@ -58,11 +26,12 @@ type Options struct { RequestTimeout time.Duration `json:"requestTimeout,omitempty"` DisableAutomaticEventLogging bool `json:"disableAutomaticEventLogging,omitempty"` DisableCustomEventLogging bool `json:"disableCustomEventLogging,omitempty"` + EnableBetaRealtimeUpdates bool `json:"enableRealtimeUpdates,omitempty"` MaxEventQueueSize int `json:"maxEventsPerFlush,omitempty"` FlushEventQueueSize int `json:"minEventsPerFlush,omitempty"` ConfigCDNURI string EventsAPIURI string - OnInitializedChannel chan bool + ClientEventHandler chan api.ClientEvent BucketingAPIURI string Logger util.Logger AdvancedOptions diff --git a/event_manager.go b/event_manager.go index e53f53cc..4f08fbb3 100644 --- a/event_manager.go +++ b/event_manager.go @@ -24,6 +24,7 @@ type InternalEventQueue interface { UserQueueLength() (int, error) GetUUID() string Metrics() (int32, int32, int32) + GetUUID() string } // EventManager is responsible for flushing the event queue and reporting events to the server. @@ -34,6 +35,7 @@ type EventManager struct { sdkKey string options *Options cfg *HTTPConfiguration + httpClient *http.Client closed bool flushStop chan bool forceFlush chan bool @@ -47,15 +49,15 @@ type FlushResult struct { func NewEventManager(options *Options, localBucketing InternalEventQueue, cfg *HTTPConfiguration, sdkKey string) (eventQueue *EventManager, err error) { e := &EventManager{ - flushMutex: &sync.Mutex{}, + flushMutex: &sync.Mutex{}, + options: options, + internalQueue: localBucketing, + cfg: cfg, + sdkKey: sdkKey, + flushStop: make(chan bool, 1), + forceFlush: make(chan bool, 1), + httpClient: cfg.HTTPClient, } - e.options = options - e.internalQueue = localBucketing - e.cfg = cfg - e.sdkKey = sdkKey - - e.flushStop = make(chan bool, 1) - e.forceFlush = make(chan bool, 1) // Disable automatic flushing of events if all sources of events are disabled // DisableAutomaticEventLogging is passed into the WASM to disable events @@ -112,6 +114,30 @@ func (e *EventManager) QueueEvent(user User, event Event) error { return err } +func (e *EventManager) QueueSDKConfigEvent(req http.Request, resp http.Response) error { + uuid := e.GetUUID() + user := api.User{UserId: uuid} + + event := api.Event{ + Type_: api.EventType_SDKConfig, + UserId: uuid, + Target: req.RequestURI, + Value: -1, + MetaData: map[string]interface{}{ + "clientUUID": uuid, + "reqEtag": req.Header.Get("If-None-Match"), + "reqLastModified": req.Header.Get("If-Modified-Since"), + "resEtag": resp.Header.Get("Etag"), + "resLastModified": resp.Header.Get("Last-Modified"), + "resRayId": resp.Header.Get("Cf-Ray"), + "resStatus": resp.StatusCode, + "errMsg": resp.Status, + }, + } + // We don't actually care about this failing or succeeding. It's best effort to send the event. + return e.QueueEvent(user, event) +} + func (e *EventManager) QueueVariableDefaultedEvent(variableKey string, defaultReason string) error { return e.internalQueue.QueueVariableDefaulted(variableKey, defaultReason) } @@ -196,7 +222,7 @@ func (e *EventManager) flushEventPayload( req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") - resp, err = e.cfg.HTTPClient.Do(req) + resp, err = e.httpClient.Do(req) if err != nil { util.Errorf("Failed to make request to events api: %s", err) diff --git a/event_manager_test.go b/event_manager_test.go index 9eb277f0..2fdb9d1c 100644 --- a/event_manager_test.go +++ b/event_manager_test.go @@ -25,6 +25,7 @@ func TestEventManager_QueueEvent(t *testing.T) { } func TestEventManager_QueueEvent_100_DropEvent(t *testing.T) { + sdkKey, _ := httpConfigMock(200) c, err := NewClient(sdkKey, &Options{MaxEventQueueSize: 100, FlushEventQueueSize: 10}) diff --git a/example/local/main.go b/example/local/main.go index 52842626..740aa37f 100644 --- a/example/local/main.go +++ b/example/local/main.go @@ -21,13 +21,9 @@ func main() { user := devcycle.User{UserId: "test"} dvcOptions := devcycle.Options{ - EnableEdgeDB: false, - EnableCloudBucketing: false, - EventFlushIntervalMS: 0, - ConfigPollingIntervalMS: 10 * time.Second, - RequestTimeout: 10 * time.Second, - DisableAutomaticEventLogging: false, - DisableCustomEventLogging: false, + EventFlushIntervalMS: 0, + ConfigPollingIntervalMS: 10 * time.Second, + RequestTimeout: 10 * time.Second, } client, err := devcycle.NewClient(sdkKey, &dvcOptions) diff --git a/example/openfeature/main.go b/example/openfeature/main.go index 9d68d7b1..31e79d10 100644 --- a/example/openfeature/main.go +++ b/example/openfeature/main.go @@ -52,18 +52,16 @@ func main() { } log.Printf("Variable results: %#v", value) - // Checking a boolean variable flag - booleanVariable := "test-boolean-variable" - if featureEnabled, err := client.BooleanValue(context.Background(), booleanVariable, false, evalCtx); err != nil { + // Checking a string variable flag + stringVariable := "go-example-tests" + if exampleValue, err := client.StringValue(context.Background(), stringVariable, "DEFAULT", evalCtx); err != nil { log.Printf("Error retrieving feature flag: %v", err) - } else if featureEnabled { - log.Printf("%v = true, feature is enabled", booleanVariable) - } else { - log.Printf("%v = false, feature is disabled", booleanVariable) + } else if exampleValue != "DEFAULT" { + log.Printf("%v = %s, feature is enabled", stringVariable, exampleValue) } // Retrieving a string variable along with the resolution details - details, err := client.StringValueDetails(context.Background(), "doesnt-exist", "default", evalCtx) + details, err := client.StringValueDetails(context.Background(), "doesnt-exist", "DEFAULT", evalCtx) if err != nil { log.Fatal(err) diff --git a/go.mod b/go.mod index 0ed6664b..91f3f7f1 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/go-playground/validator/v10 v10.18.0 github.com/google/uuid v1.3.0 github.com/jarcoal/httpmock v1.2.0 + github.com/launchdarkly/eventsource v1.7.1 github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2 github.com/open-feature/go-sdk v1.8.0 github.com/stretchr/testify v1.8.4 diff --git a/go.sum b/go.sum index 66c32e8f..eb7b1cae 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,6 @@ github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 h1:SKI1/fuSdodxmNNyVBR8d7X/HuLnRpvvFO0AgyQk764= github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U= +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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= @@ -23,6 +24,10 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/launchdarkly/eventsource v1.7.1 h1:StoRQeiPyrcQIXjlQ7b5jWMzHW4p+GGczN2r2oBhujg= +github.com/launchdarkly/eventsource v1.7.1/go.mod h1:LHxSeb4OnqznNZxCSXbFghxS/CjIQfzHovNoAqbO/Wk= +github.com/launchdarkly/go-test-helpers/v2 v2.2.0 h1:L3kGILP/6ewikhzhdNkHy1b5y4zs50LueWenVF0sBbs= +github.com/launchdarkly/go-test-helpers/v2 v2.2.0/go.mod h1:L7+th5govYp5oKU9iN7To5PgznBuIjBPn+ejqKR0avw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2 h1:JAEbJn3j/FrhdWA9jW8B5ajsLIjeuEHLi8xE4fk997o= @@ -32,6 +37,9 @@ github.com/open-feature/go-sdk v1.8.0 h1:jRkP7zeSGC3pSYn/s3EzJSpO9Q6CVP8BOnmvBZY github.com/open-feature/go-sdk v1.8.0/go.mod h1:hpKxVZIJ0b+GpnI8imSJf9nFTcmTb0wWJZTgAS/3giw= 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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/twmb/murmur3 v1.1.7 h1:ULWBiM04n/XoN3YMSJ6Z2pHDFLf+MeIVQU71ZPrvbWg= @@ -49,5 +57,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 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.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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/go.work.sum b/go.work.sum new file mode 100644 index 00000000..ee967679 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,657 @@ +cloud.google.com/go v0.57.0 h1:EpMNVUorLiZIELdMZbCYX/ByTFCdoYopYAGxaGVz9ms= +cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys= +cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= +cloud.google.com/go/accessapproval v1.6.0 h1:x0cEHro/JFPd7eS4BlEWNTMecIj2HdXjOVB5BtvwER0= +cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= +cloud.google.com/go/accesscontextmanager v1.7.0 h1:MG60JgnEoawHJrbWw0jGdv6HLNSf6gQvYRiXpuzqgEA= +cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ= +cloud.google.com/go/aiplatform v1.37.0 h1:zTw+suCVchgZyO+k847wjzdVjWmrAuehxdvcZvJwfGg= +cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw= +cloud.google.com/go/analytics v0.19.0 h1:LqAo3tAh2FU9+w/r7vc3hBjU23Kv7GhO/PDIW7kIYgM= +cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE= +cloud.google.com/go/apigateway v1.5.0 h1:ZI9mVO7x3E9RK/BURm2p1aw9YTBSCQe3klmyP1WxWEg= +cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= +cloud.google.com/go/apigeeconnect v1.5.0 h1:sWOmgDyAsi1AZ48XRHcATC0tsi9SkPT7DA/+VCfkaeA= +cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= +cloud.google.com/go/apigeeregistry v0.6.0 h1:E43RdhhCxdlV+I161gUY2rI4eOaMzHTA5kNkvRsFXvc= +cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc= +cloud.google.com/go/apikeys v0.6.0 h1:B9CdHFZTFjVti89tmyXXrO+7vSNo2jvZuHG8zD5trdQ= +cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8= +cloud.google.com/go/appengine v1.7.1 h1:aBGDKmRIaRRoWJ2tAoN0oVSHoWLhtO9aj/NvUyP4aYs= +cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E= +cloud.google.com/go/area120 v0.7.1 h1:ugckkFh4XkHJMPhTIx0CyvdoBxmOpMe8rNs4Ok8GAag= +cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= +cloud.google.com/go/artifactregistry v1.13.0 h1:o1Q80vqEB6Qp8WLEH3b8FBLNUCrGQ4k5RFj0sn/sgO8= +cloud.google.com/go/artifactregistry v1.13.0/go.mod h1:uy/LNfoOIivepGhooAUpL1i30Hgee3Cu0l4VTWHUC08= +cloud.google.com/go/asset v1.13.0 h1:YAsssO08BqZ6mncbb6FPlj9h6ACS7bJQUOlzciSfbNk= +cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw= +cloud.google.com/go/assuredworkloads v1.10.0 h1:VLGnVFta+N4WM+ASHbhc14ZOItOabDLH1MSoDv+Xuag= +cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= +cloud.google.com/go/automl v1.12.0 h1:50VugllC+U4IGl3tDNcZaWvApHBTrn/TvyHDJ0wM+Uw= +cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= +cloud.google.com/go/baremetalsolution v0.5.0 h1:2AipdYXL0VxMboelTTw8c1UJ7gYu35LZYUbuRv9Q28s= +cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= +cloud.google.com/go/batch v0.7.0 h1:YbMt0E6BtqeD5FvSv1d56jbVsWEzlGm55lYte+M6Mzs= +cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= +cloud.google.com/go/beyondcorp v0.5.0 h1:UkY2BTZkEUAVrgqnSdOJ4p3y9ZRBPEe1LkjgC8Bj/Pc= +cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU= +cloud.google.com/go/bigquery v1.50.0 h1:RscMV6LbnAmhAzD893Lv9nXXy2WCaJmbxYPWDLbGqNQ= +cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= +cloud.google.com/go/billing v1.13.0 h1:JYj28UYF5w6VBAh0gQYlgHJ/OD1oA+JgW29YZQU+UHM= +cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc= +cloud.google.com/go/binaryauthorization v1.5.0 h1:d3pMDBCCNivxt5a4eaV7FwL7cSH0H7RrEnFrTb1QKWs= +cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= +cloud.google.com/go/certificatemanager v1.6.0 h1:5C5UWeSt8Jkgp7OWn2rCkLmYurar/vIWIoSQ2+LaTOc= +cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= +cloud.google.com/go/channel v1.12.0 h1:GpcQY5UJKeOekYgsX3QXbzzAc/kRGtBq43fTmyKe6Uw= +cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU= +cloud.google.com/go/cloudbuild v1.9.0 h1:GHQCjV4WlPPVU/j3Rlpc8vNIDwThhd1U9qSY/NPZdko= +cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= +cloud.google.com/go/clouddms v1.5.0 h1:E7v4TpDGUyEm1C/4KIrpVSOCTm0P6vWdHT0I4mostRA= +cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= +cloud.google.com/go/cloudtasks v1.10.0 h1:uK5k6abf4yligFgYFnG0ni8msai/dSv6mDmiBulU0hU= +cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs= +cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY= +cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/contactcenterinsights v1.6.0 h1:jXIpfcH/VYSE1SYcPzO0n1VVb+sAamiLOgCw45JbOQk= +cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= +cloud.google.com/go/container v1.15.0 h1:NKlY/wCDapfVZlbVVaeuu2UZZED5Dy1z4Zx1KhEzm8c= +cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA= +cloud.google.com/go/containeranalysis v0.9.0 h1:EQ4FFxNaEAg8PqQCO7bVQfWz9NVwZCUKaM1b3ycfx3U= +cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= +cloud.google.com/go/datacatalog v1.13.0 h1:4H5IJiyUE0X6ShQBqgFFZvGGcrwGVndTwUSLP4c52gw= +cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8= +cloud.google.com/go/dataflow v0.8.0 h1:eYyD9o/8Nm6EttsKZaEGD84xC17bNgSKCu0ZxwqUbpg= +cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= +cloud.google.com/go/dataform v0.7.0 h1:Dyk+fufup1FR6cbHjFpMuP4SfPiF3LI3JtoIIALoq48= +cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE= +cloud.google.com/go/datafusion v1.6.0 h1:sZjRnS3TWkGsu1LjYPFD/fHeMLZNXDK6PDHi2s2s/bk= +cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= +cloud.google.com/go/datalabeling v0.7.0 h1:ch4qA2yvddGRUrlfwrNJCr79qLqhS9QBwofPHfFlDIk= +cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= +cloud.google.com/go/dataplex v1.6.0 h1:RvoZ5T7gySwm1CHzAw7yY1QwwqaGswunmqEssPxU/AM= +cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= +cloud.google.com/go/dataproc v1.12.0 h1:W47qHL3W4BPkAIbk4SWmIERwsWBaNnWm0P2sdx3YgGU= +cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= +cloud.google.com/go/dataqna v0.7.0 h1:yFzi/YU4YAdjyo7pXkBE2FeHbgz5OQQBVDdbErEHmVQ= +cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= +cloud.google.com/go/datastore v1.11.0 h1:iF6I/HaLs3Ado8uRKMvZRvF/ZLkWaWE9i8AiHzbC774= +cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= +cloud.google.com/go/datastream v1.7.0 h1:BBCBTnWMDwwEzQQmipUXxATa7Cm7CA/gKjKcR2w35T0= +cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= +cloud.google.com/go/deploy v1.8.0 h1:otshdKEbmsi1ELYeCKNYppwV0UH5xD05drSdBm7ouTk= +cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= +cloud.google.com/go/dialogflow v1.32.0 h1:uVlKKzp6G/VtSW0E7IH1Y5o0H48/UOCmqksG2riYCwQ= +cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE= +cloud.google.com/go/dlp v1.9.0 h1:1JoJqezlgu6NWCroBxr4rOZnwNFILXr4cB9dMaSKO4A= +cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= +cloud.google.com/go/documentai v1.18.0 h1:KM3Xh0QQyyEdC8Gs2vhZfU+rt6OCPF0dwVwxKgLmWfI= +cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs= +cloud.google.com/go/domains v0.8.0 h1:2ti/o9tlWL4N+wIuWUNH+LbfgpwxPr8J1sv9RHA4bYQ= +cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= +cloud.google.com/go/edgecontainer v1.0.0 h1:O0YVE5v+O0Q/ODXYsQHmHb+sYM8KNjGZw2pjX2Ws41c= +cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY= +cloud.google.com/go/errorreporting v0.3.0 h1:kj1XEWMu8P0qlLhm3FwcaFsUvXChV/OraZwA70trRR0= +cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= +cloud.google.com/go/essentialcontacts v1.5.0 h1:gIzEhCoOT7bi+6QZqZIzX1Erj4SswMPIteNvYVlu+pM= +cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= +cloud.google.com/go/eventarc v1.11.0 h1:fsJmNeqvqtk74FsaVDU6cH79lyZNCYP8Rrv7EhaB/PU= +cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY= +cloud.google.com/go/filestore v1.6.0 h1:ckTEXN5towyTMu4q0uQ1Mde/JwTHur0gXs8oaIZnKfw= +cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg= +cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA= +cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= +cloud.google.com/go/functions v1.13.0 h1:pPDqtsXG2g9HeOQLoquLbmvmb82Y4Ezdo1GXuotFoWg= +cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c= +cloud.google.com/go/gaming v1.9.0 h1:7vEhFnZmd931Mo7sZ6pJy7uQPDxF7m7v8xtBheG08tc= +cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= +cloud.google.com/go/gkebackup v0.4.0 h1:za3QZvw6ujR0uyqkhomKKKNoXDyqYGPJies3voUK8DA= +cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= +cloud.google.com/go/gkeconnect v0.7.0 h1:gXYKciHS/Lgq0GJ5Kc9SzPA35NGc3yqu6SkjonpEr2Q= +cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= +cloud.google.com/go/gkehub v0.12.0 h1:TqCSPsEBQ6oZSJgEYZ3XT8x2gUadbvfwI32YB0kuHCs= +cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw= +cloud.google.com/go/gkemulticloud v0.5.0 h1:8I84Q4vl02rJRsFiinBxl7WCozfdLlUVBQuSrqr9Wtk= +cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= +cloud.google.com/go/gsuiteaddons v1.5.0 h1:1mvhXqJzV0Vg5Fa95QwckljODJJfDFXV4pn+iL50zzA= +cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= +cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k= +cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= +cloud.google.com/go/iap v1.7.1 h1:PxVHFuMxmSZyfntKXHXhd8bo82WJ+LcATenq7HLdVnU= +cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74= +cloud.google.com/go/ids v1.3.0 h1:fodnCDtOXuMmS8LTC2y3h8t24U8F3eKWfhi+3LY6Qf0= +cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= +cloud.google.com/go/iot v1.6.0 h1:39W5BFSarRNZfVG0eXI5LYux+OVQT8GkgpHCnrZL2vM= +cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE= +cloud.google.com/go/kms v1.10.1 h1:7hm1bRqGCA1GBRQUrp831TwJ9TWhP+tvLuP497CQS2g= +cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= +cloud.google.com/go/language v1.9.0 h1:7Ulo2mDk9huBoBi8zCE3ONOoBrL6UXfAI71CLQ9GEIM= +cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= +cloud.google.com/go/lifesciences v0.8.0 h1:uWrMjWTsGjLZpCTWEAzYvyXj+7fhiZST45u9AgasasI= +cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= +cloud.google.com/go/logging v1.7.0 h1:CJYxlNNNNAMkHp9em/YEXcfJg+rPDg7YfwoRpMU+t5I= +cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= +cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM= +cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= +cloud.google.com/go/managedidentities v1.5.0 h1:ZRQ4k21/jAhrHBVKl/AY7SjgzeJwG1iZa+mJ82P+VNg= +cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= +cloud.google.com/go/maps v0.7.0 h1:mv9YaczD4oZBZkM5XJl6fXQ984IkJNHPwkc8MUsdkBo= +cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= +cloud.google.com/go/mediatranslation v0.7.0 h1:anPxH+/WWt8Yc3EdoEJhPMBRF7EhIdz426A+tuoA0OU= +cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= +cloud.google.com/go/memcache v1.9.0 h1:8/VEmWCpnETCrBwS3z4MhT+tIdKgR1Z4Tr2tvYH32rg= +cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= +cloud.google.com/go/metastore v1.10.0 h1:QCFhZVe2289KDBQ7WxaHV2rAmPrmRAdLC6gbjUd3HPo= +cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= +cloud.google.com/go/monitoring v1.13.0 h1:2qsrgXGVoRXpP7otZ14eE1I568zAa92sJSDPyOJvwjM= +cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= +cloud.google.com/go/networkconnectivity v1.11.0 h1:ZD6b4Pk1jEtp/cx9nx0ZYcL3BKqDa+KixNDZ6Bjs1B8= +cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM= +cloud.google.com/go/networkmanagement v1.6.0 h1:8KWEUNGcpSX9WwZXq7FtciuNGPdPdPN/ruDm769yAEM= +cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= +cloud.google.com/go/networksecurity v0.8.0 h1:sOc42Ig1K2LiKlzG71GUVloeSJ0J3mffEBYmvu+P0eo= +cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU= +cloud.google.com/go/notebooks v1.8.0 h1:Kg2K3K7CbSXYJHZ1aGQpf1xi5x2GUvQWf2sFVuiZh8M= +cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= +cloud.google.com/go/optimization v1.3.1 h1:dj8O4VOJRB4CUwZXdmwNViH1OtI0WtWL867/lnYH248= +cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= +cloud.google.com/go/orchestration v1.6.0 h1:Vw+CEXo8M/FZ1rb4EjcLv0gJqqw89b7+g+C/EmniTb8= +cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= +cloud.google.com/go/orgpolicy v1.10.0 h1:XDriMWug7sd0kYT1QKofRpRHzjad0bK8Q8uA9q+XrU4= +cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= +cloud.google.com/go/osconfig v1.11.0 h1:PkSQx4OHit5xz2bNyr11KGcaFccL5oqglFPdTboyqwQ= +cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= +cloud.google.com/go/oslogin v1.9.0 h1:whP7vhpmc+ufZa90eVpkfbgzJRK/Xomjz+XCD4aGwWw= +cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= +cloud.google.com/go/phishingprotection v0.7.0 h1:l6tDkT7qAEV49MNEJkEJTB6vOO/onbSOcNtAT09HPuA= +cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= +cloud.google.com/go/policytroubleshooter v1.6.0 h1:yKAGC4p9O61ttZUswaq9GAn1SZnEzTd0vUYXD7ZBT7Y= +cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc= +cloud.google.com/go/privatecatalog v0.8.0 h1:EPEJ1DpEGXLDnmc7mnCAqFmkwUJbIsaLAiLHVOkkwtc= +cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs= +cloud.google.com/go/pubsub v1.4.0 h1:76oR7VBOkL7ivoIrFKyW0k7YDCRelrlxktIzQiIUGgg= +cloud.google.com/go/pubsub v1.30.0 h1:vCge8m7aUKBJYOgrZp7EsNDf6QMd2CAlXZqWTn3yq6s= +cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= +cloud.google.com/go/pubsublite v1.7.0 h1:cb9fsrtpINtETHiJ3ECeaVzrfIVhcGjhhJEjybHXHao= +cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM= +cloud.google.com/go/recaptchaenterprise/v2 v2.7.0 h1:6iOCujSNJ0YS7oNymI64hXsjGq60T4FK1zdLugxbzvU= +cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c= +cloud.google.com/go/recommendationengine v0.7.0 h1:VibRFCwWXrFebEWKHfZAt2kta6pS7Tlimsnms0fjv7k= +cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= +cloud.google.com/go/recommender v1.9.0 h1:ZnFRY5R6zOVk2IDS1Jbv5Bw+DExCI5rFumsTnMXiu/A= +cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= +cloud.google.com/go/redis v1.11.0 h1:JoAd3SkeDt3rLFAAxEvw6wV4t+8y4ZzfZcZmddqphQ8= +cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= +cloud.google.com/go/resourcemanager v1.7.0 h1:NRM0p+RJkaQF9Ee9JMnUV9BQ2QBIOq/v8M+Pbv/wmCs= +cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI= +cloud.google.com/go/resourcesettings v1.5.0 h1:8Dua37kQt27CCWHm4h/Q1XqCF6ByD7Ouu49xg95qJzI= +cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= +cloud.google.com/go/retail v1.12.0 h1:1Dda2OpFNzIb4qWgFZjYlpP7sxX3aLeypKG6A3H4Yys= +cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= +cloud.google.com/go/run v0.9.0 h1:ydJQo+k+MShYnBfhaRHSZYeD/SQKZzZLAROyfpeD9zw= +cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg= +cloud.google.com/go/scheduler v1.9.0 h1:NpQAHtx3sulByTLe2dMwWmah8PWgeoieFPpJpArwFV0= +cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= +cloud.google.com/go/secretmanager v1.10.0 h1:pu03bha7ukxF8otyPKTFdDz+rr9sE3YauS5PliDXK60= +cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= +cloud.google.com/go/security v1.13.0 h1:PYvDxopRQBfYAXKAuDpFCKBvDOWPWzp9k/H5nB3ud3o= +cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0= +cloud.google.com/go/securitycenter v1.19.0 h1:AF3c2s3awNTMoBtMX3oCUoOMmGlYxGOeuXSYHNBkf14= +cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag= +cloud.google.com/go/servicecontrol v1.11.1 h1:d0uV7Qegtfaa7Z2ClDzr9HJmnbJW7jn0WhZ7wOX6hLE= +cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk= +cloud.google.com/go/servicedirectory v1.9.0 h1:SJwk0XX2e26o25ObYUORXx6torSFiYgsGkWSkZgkoSU= +cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= +cloud.google.com/go/servicemanagement v1.8.0 h1:fopAQI/IAzlxnVeiKn/8WiV6zKndjFkvi+gzu+NjywY= +cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4= +cloud.google.com/go/serviceusage v1.6.0 h1:rXyq+0+RSIm3HFypctp7WoXxIA563rn206CfMWdqXX4= +cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA= +cloud.google.com/go/shell v1.6.0 h1:wT0Uw7ib7+AgZST9eCDygwTJn4+bHMDtZo5fh7kGWDU= +cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= +cloud.google.com/go/spanner v1.45.0 h1:7VdjZ8zj4sHbDw55atp5dfY6kn1j9sam9DRNpPQhqR4= +cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= +cloud.google.com/go/speech v1.15.0 h1:JEVoWGNnTF128kNty7T4aG4eqv2z86yiMJPT9Zjp+iw= +cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI= +cloud.google.com/go/storagetransfer v1.8.0 h1:5T+PM+3ECU3EY2y9Brv0Sf3oka8pKmsCfpQ07+91G9o= +cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= +cloud.google.com/go/talent v1.5.0 h1:nI9sVZPjMKiO2q3Uu0KhTDVov3Xrlpt63fghP9XjyEM= +cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= +cloud.google.com/go/texttospeech v1.6.0 h1:H4g1ULStsbVtalbZGktyzXzw6jP26RjVGYx9RaYjBzc= +cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= +cloud.google.com/go/tpu v1.5.0 h1:/34T6CbSi+kTv5E19Q9zbU/ix8IviInZpzwz3rsFE+A= +cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= +cloud.google.com/go/trace v1.9.0 h1:olxC0QHC59zgJVALtgqfD9tGk0lfeCP5/AGXL3Px/no= +cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= +cloud.google.com/go/translate v1.7.0 h1:GvLP4oQ4uPdChBmBaUSa/SaZxCdyWELtlAaKzpHsXdA= +cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= +cloud.google.com/go/video v1.15.0 h1:upIbnGI0ZgACm58HPjAeBMleW3sl5cT84AbYQ8PWOgM= +cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= +cloud.google.com/go/videointelligence v1.10.0 h1:Uh5BdoET8XXqXX2uXIahGb+wTKbLkGH7s4GXR58RrG8= +cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= +cloud.google.com/go/vision/v2 v2.7.0 h1:8C8RXUJoflCI4yVdqhTy9tRyygSHmp60aP363z23HKg= +cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0= +cloud.google.com/go/vmmigration v1.6.0 h1:Azs5WKtfOC8pxvkyrDvt7J0/4DYBch0cVbuFfCCFt5k= +cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY= +cloud.google.com/go/vmwareengine v0.3.0 h1:b0NBu7S294l0gmtrT0nOJneMYgZapr5x9tVWvgDoVEM= +cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY= +cloud.google.com/go/vpcaccess v1.6.0 h1:FOe6CuiQD3BhHJWt7E8QlbBcaIzVRddupwJlp7eqmn4= +cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= +cloud.google.com/go/webrisk v1.8.0 h1:IY+L2+UwxcVm2zayMAtBhZleecdIFLiC+QJMzgb0kT0= +cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= +cloud.google.com/go/websecurityscanner v1.5.0 h1:AHC1xmaNMOZtNqxI9Rmm87IJEyPaRkOxeI0gpAacXGk= +cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= +cloud.google.com/go/workflows v1.10.0 h1:FfGp9w0cYnaKZJhUOMqCOJCYT/WlvYBfTQhFWV3sRKI= +cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= +github.com/99designs/gqlgen v0.16.0 h1:7Qc4Ll3mfN3doAyUWOgtGLcBGu+KDgK48HdkBGLZVFs= +github.com/99designs/gqlgen v0.16.0/go.mod h1:nbeSjFkqphIqpZsYe1ULVz0yfH8hjpJdJIQoX/e0G2I= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= +github.com/DataDog/zstd v1.3.5 h1:DtpNbljikUepEPD16hD4LvIcmhnhdLTiW/5pHgbmp14= +github.com/DataDog/zstd v1.3.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Shopify/sarama v1.22.0 h1:rtiODsvY4jW6nUV6n3K+0gx/8WlAwVt+Ixt6RIvpYyo= +github.com/Shopify/sarama v1.22.0/go.mod h1:lm3THZ8reqBDBQKQyb5HB3sY1lKp3grEbQ81aWSgPp4= +github.com/agnivade/levenshtein v1.1.0 h1:n6qGwyHG61v3ABce1rPVZklEYRT8NFpCMrpZdBUbYGM= +github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af h1:wVe6/Ea46ZMeNkQjjBW6xcqyQA/j5e0D6GytH95g0gQ= +github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/armon/go-metrics v0.3.0 h1:B7AQgHi8QSEi4uHu7Sbsga+IJDU+CENgjxoo81vDUqU= +github.com/armon/go-metrics v0.3.0/go.mod h1:zXjbSimjXTd7vOpY8B0/2LpvNvDoXBuplAD+gJD3GYs= +github.com/aws/aws-sdk-go v1.34.28 h1:sscPpn/Ns3i0F4HPEWAVcwdIRaZZCuL7llJ2/60yPIk= +github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= +github.com/aws/aws-sdk-go-v2 v1.0.0 h1:ncEVPoHArsG+HjoDe/3ex/TG1CbLwMQ4eaWj0UGdyTo= +github.com/aws/aws-sdk-go-v2 v1.0.0/go.mod h1:smfAbmpW+tcRVuNUjo3MOArSZmW72t62rkCzc2i0TWM= +github.com/aws/aws-sdk-go-v2/config v1.0.0 h1:x6vSFAwqAvhYPeSu60f0ZUlGHo3PKKmwDOTL8aMXtv4= +github.com/aws/aws-sdk-go-v2/config v1.0.0/go.mod h1:WysE/OpUgE37tjtmtJd8GXgT8s1euilE5XtUkRNUQ1w= +github.com/aws/aws-sdk-go-v2/credentials v1.0.0 h1:0M7netgZ8gCV4v7z1km+Fbl7j6KQYyZL7SS0/l5Jn/4= +github.com/aws/aws-sdk-go-v2/credentials v1.0.0/go.mod h1:/SvsiqBf509hG4Bddigr3NB12MIpfHhZapyBurJe8aY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.0 h1:lO7fH5n7Q1dKcDBpuTmwJylD1bOQiRig8LI6TD9yVQk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.0/go.mod h1:wpMHDCXvOXZxGCRSidyepa8uJHY4vaBGfY2/+oKU/Bc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.0 h1:IAutMPSrynpvKOpHG6HyWHmh1xmxWAmYOK84NrQVqVQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.0/go.mod h1:3jExOmpbjgPnz2FJaMOfbSk1heTkZ66aD3yNtVhnjvI= +github.com/aws/aws-sdk-go-v2/service/sqs v1.0.0 h1:k+iXUEMp688JqUcxb4/bzt7xgJX4TLqahrwgWA/qO6E= +github.com/aws/aws-sdk-go-v2/service/sqs v1.0.0/go.mod h1:w5BclCU8ptTbagzXS/fHBr+vAyXUjggg/72qDIURKMk= +github.com/aws/aws-sdk-go-v2/service/sts v1.0.0 h1:6XCgxNfE4L/Fnq+InhVNd16DKc6Ue1f3dJl3IwwJRUQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.0.0/go.mod h1:5f+cELGATgill5Pu3/vK3Ebuigstc+qYEHW5MvGWZO4= +github.com/aws/smithy-go v1.11.0 h1:nOfSDwiiH232f90OuevPnAEQO5ZqH+xnn8uGVsvBCw4= +github.com/aws/smithy-go v1.11.0/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= +github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d h1:pVrfxiGfwelyab6n21ZBkbkmbevaf+WvMIiR7sr97hw= +github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= +github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= +github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= +github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f h1:WBZRG4aNOuI15bLRrCgN8fCq8E5Xuty6jGbmSNEvSsU= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= +github.com/confluentinc/confluent-kafka-go v1.4.0 h1:GCEMecax8zLZsCVn1cea7Y1uR/lRCdCDednpkc0NLsY= +github.com/confluentinc/confluent-kafka-go v1.4.0/go.mod h1:u2zNLny2xq+5rWeTQjFHbDzzNuba4P1vo31r9r4uAdg= +github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= +github.com/cucumber/gherkin-go/v19 v19.0.3 h1:mMSKu1077ffLbTJULUfM5HPokgeBcIGboyeNUof1MdE= +github.com/cucumber/gherkin-go/v19 v19.0.3/go.mod h1:jY/NP6jUtRSArQQJ5h1FXOUgk5fZK24qtE7vKi776Vw= +github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI= +github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0= +github.com/cucumber/godog v0.13.0 h1:KvX9kNWmAJwp882HmObGOyBbNUP5SXQ+SDLNajsuV7A= +github.com/cucumber/godog v0.13.0/go.mod h1:FX3rzIDybWABU4kuIXLZ/qtqEe1Ac5RdXmqvACJOces= +github.com/cucumber/messages-go/v16 v16.0.1 h1:fvkpwsLgnIm0qugftrw2YwNlio+ABe2Iu94Ap8GMYIY= +github.com/cucumber/messages-go/v16 v16.0.1/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g= +github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI= +github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s= +github.com/denisenkom/go-mssqldb v0.11.0 h1:9rHa233rhdOyrz2GcP9NM+gi2psgJZ4GWDpL/7ND8HI= +github.com/denisenkom/go-mssqldb v0.11.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415 h1:q1oJaUPdmpDm/VyXosjgPgr6wS7c5iV2p0PwJD73bUI= +github.com/eapache/go-resiliency v1.1.0 h1:1NtRmCAqadE2FN4ZcN6g90TP3uk8cg9rn9eNK2197aU= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/elastic/go-elasticsearch/v6 v6.8.5 h1:U2HtkBseC1FNBmDr0TR2tKltL6FxoY+niDAlj5M8TK8= +github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI= +github.com/elastic/go-elasticsearch/v7 v7.17.1 h1:49mHcHx7lpCL8cW1aioEwSEVKQF3s+Igi4Ye/QTWwmk= +github.com/elastic/go-elasticsearch/v7 v7.17.1/go.mod h1:OJ4wdbtDNk5g503kvlHLyErCgQwwzmDtaFC4XyOxXA4= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633 h1:H2pdYOb3KQ1/YsqVWoWNLQO+fusocsw354rqGTZtAgw= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.4 h1:rEvIZUSZ3fx39WIi3JkQqQBitGwpELBIYWeBVh6wn+E= +github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f h1:7T++XKzy4xg7PKy+bM+Sa9/oe1OC88yz2hXQUISoXfA= +github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= +github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= +github.com/envoyproxy/protoc-gen-validate v0.10.1 h1:c0g45+xCJhdgFGw7a5QAfdS4byAbud7miNWJ1WwEVf8= +github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= +github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/flynn/go-docopt v0.0.0-20140912013429-f6dd2ebbb31e h1:Ss/B3/5wWRh8+emnK0++g5zQzwDTi30W10pKxKc4JXI= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90 h1:WXb3TSNmHp2vHoCroCIB1foO/yQ36swABL8aOVeDpgg= +github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk= +github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/garyburd/redigo v1.6.3 h1:HCeeRluvAgMusMomi1+6Y5dmFOdYV/JzoRrrbFlkGIc= +github.com/garyburd/redigo v1.6.3/go.mod h1:rTb6epsqigu3kYKBnaF028A7Tf/Aw5s0cqA47doKKqw= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= +github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-chi/chi v1.5.0 h1:2ZcJZozJ+rj6BA0c19ffBUGXEKAT/aOLOtQjD46vBRA= +github.com/go-chi/chi v1.5.0/go.mod h1:REp24E+25iKvxgeTfHmdUoL5x15kBiDBlnIl5bCwe2k= +github.com/go-chi/chi/v5 v5.0.0 h1:DBPx88FjZJH3FsICfDAfIfnb7XxKIYVGG6lOPlhENAg= +github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= +github.com/go-pg/pg/v10 v10.0.0 h1:2XP/r9XdRfiC+LKWrIwqi2qqc+bhvW7/UpUVnwkT7wk= +github.com/go-pg/pg/v10 v10.0.0/go.mod h1:XHU1AkQW534GFuUdSiQ46+Xw6Ah+9+b8DlT4YwhiXL8= +github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU= +github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= +github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-redis/redis/v7 v7.1.0 h1:I4C4a8UGbFejiVjtYVTRVOiMIJ5pm5Yru6ibvDX/OS0= +github.com/go-redis/redis/v7 v7.1.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= +github.com/go-redis/redis/v8 v8.0.0 h1:PC0VsF9sFFd2sko5bu30aEFc8F1TKl6n65o0b8FnCIE= +github.com/go-redis/redis/v8 v8.0.0/go.mod h1:isLoQT/NFSP7V67lyvM9GmdvLdyZ7pEhsXvvyQtnQTo= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/gocql/gocql v0.0.0-20220224095938-0eacd3183625 h1:6ImvI6U901e1ezn/8u2z3bh1DZIvMOia0yTSBxhy4Ao= +github.com/gocql/gocql v0.0.0-20220224095938-0eacd3183625/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8= +github.com/gofiber/fiber/v2 v2.24.0 h1:18rpLoQMJBVlLtX/PwgHj3hIxPSeWfN1YeDJ2lEnzjU= +github.com/gofiber/fiber/v2 v2.24.0/go.mod h1:MR1usVH3JHYRyQwMe2eZXRSZHRX38fkV+A7CPB+DlDQ= +github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI= +github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= +github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/gorilla/mux v1.7.1 h1:Dw4jY2nghMMRsh1ol8dv1axHkDwMQK2DHerMNJsIpJU= +github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0= +github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/hashicorp/consul/api v1.0.0 h1:9GEZpB5zZ2Vm+rQ0t0JL/Ey2iaXbmIN1evA/LUU4do4= +github.com/hashicorp/consul/api v1.0.0/go.mod h1:mbFwfRxOTDHZpT3iUsMAFcLNoVm6Xbe1xZ6KiSm8FY0= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs= +github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-memdb v1.3.4 h1:XSL3NR682X/cVk2IeV0d70N4DZ9ljI885xAEU8IoK3c= +github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM= +github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/memberlist v0.1.6 h1:ouPxvwKYaNZe+eTcHxYP0EblPduVLvIPycul+vv8his= +github.com/hashicorp/memberlist v0.1.6/go.mod h1:5VDNHjqFMgEcclnwmkCnC99IPwxBmIsxwY8qn+Nl0H4= +github.com/hashicorp/serf v0.8.6 h1:w2ZEHuK1297elT/WbZjUojVzpZA3BuPUusa9vdXXTjc= +github.com/hashicorp/serf v0.8.6/go.mod h1:P/AVgr4UHsUYqVHG1y9eFhz8S35pqhGhLZaDpfGKIMo= +github.com/hashicorp/vault/api v1.1.0 h1:QcxC7FuqEl0sZaIjcXB/kNEeBa0DH5z57qbWBvZwLC4= +github.com/hashicorp/vault/api v1.1.0/go.mod h1:R3Umvhlxi2TN7Ex2hzOowyeNb+SfbVWI973N+ctaFMk= +github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267 h1:e1ok06zGrWJW91rzRroyl5nRNqraaBe4d5hiKcVZuHM= +github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267/go.mod h1:WX57W2PwkrOPQ6rVQk+dy5/htHIaB4aBM70EwKThu10= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI= +github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v1.10.1 h1:DzdIHIjG1AxGwoEEqS+mGsURyjt4enSmqzACXvVzOT8= +github.com/jackc/pgconn v1.10.1/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns= +github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgtype v1.9.0 h1:/SH1RxEtltvJgsDqp3TbiTFApD3mey3iygpuEGeuBXk= +github.com/jackc/pgtype v1.9.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.14.0 h1:TgdrmgnM7VY72EuSQzBbBd4JA1RLqJolrw9nQVZABVc= +github.com/jackc/pgx/v4 v4.14.0/go.mod h1:jT3ibf/A0ZVCp89rtCIN0zCJxcE74ypROmHEZYsG/j8= +github.com/jinzhu/gorm v1.9.10 h1:HvrsqdhCW78xpJF67g1hMxS6eCToo9PZH4LDB8WKPac= +github.com/jinzhu/gorm v1.9.10/go.mod h1:Kh6hTsSGffh4ui079FHrR5Gg+5D0hgihqDcsDN2BBJY= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.3 h1:PlHq1bSCSZL9K0wUhbm2pGLoTWs2GwVhsP6emvGV/ZI= +github.com/jinzhu/now v1.1.3/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +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/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5 h1:PJr+ZMXIecYc1Ey2zucXdR73SMBtgjPgwa31099IMv0= +github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U= +github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= +github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= +github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= +github.com/labstack/echo/v4 v4.2.0 h1:jkCSsjXmBmapVXF6U4BrSz/cgofWM0CU3Q74wQvXkIc= +github.com/labstack/echo/v4 v4.2.0/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg= +github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o= +github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0= +github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/maxatome/go-testdeep v1.11.0/go.mod h1:011SgQ6efzZYAen6fDn4BqQ+lUR72ysdyKe7Dyogw70= +github.com/miekg/dns v1.1.25 h1:dFwPR6SfLtrSwgDcIq2bcU/gVutB4sNApq2HBdqcakg= +github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo= +github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo/v2 v2.0.0 h1:CcuG/HvWNkkaqCUpJifQY8z7qEMBJya6aLPx6ftGyjQ= +github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI= +github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE= +github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a h1:9ZKAASQSHhDYGoxY8uLVpewe1GDZ2vu2Tr/vTdVAkFQ= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef h1:UD99BBEz19F21KhOFHLNAI6KodDWUvXaPr4Oqu8yMV8= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/segmentio/kafka-go v0.4.29 h1:4ujULpikzHG0HqKhjumDghFjy/0RRCSl/7lbriwQAH0= +github.com/segmentio/kafka-go v0.4.29/go.mod h1:m1lXeqJtIFYZayv0shM/tjrAFljvWLTprxBHd+3PnaU= +github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/tidwall/btree v1.1.0 h1:5P+9WU8ui5uhmcg3SoPyTwoI0mVyZ1nps7YQzTZFkYM= +github.com/tidwall/btree v1.1.0/go.mod h1:TzIRzen6yHbibdSfK6t8QimqbUnoxUSrZfeW7Uob0q4= +github.com/tidwall/buntdb v1.2.0 h1:8KOzf5Gg97DoCMSOgcwZjnM0FfROtq0fcZkPW54oGKU= +github.com/tidwall/buntdb v1.2.0/go.mod h1:XLza/dhlwzO6dc5o/KWor4kfZSt3BP8QV+77ZMKfI58= +github.com/tidwall/gjson v1.12.1 h1:ikuZsLdhr8Ws0IdROXUS1Gi4v9Z4pGqpX/CvJkxvfpo= +github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg= +github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8= +github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ= +github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE= +github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= +github.com/twitchtv/twirp v8.1.1+incompatible h1:s5WnVKMhC4Xz1jOfNAqTg85iguOWAvsrCJoPiezlLFA= +github.com/twitchtv/twirp v8.1.1+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= +github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4= +github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= +github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/vektah/gqlparser/v2 v2.2.0 h1:bAc3slekAAJW6sZTi07aGq0OrfaCjj4jxARAaC7g2EM= +github.com/vektah/gqlparser/v2 v2.2.0/go.mod h1:i3mQIGIrbK2PD1RrCeMTlVbkF2FJ6WkU1KJlJlC+3F4= +github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94= +github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ= +github.com/vmihailenco/msgpack/v5 v5.3.4 h1:qMKAwOV+meBw2Y8k9cVwAy7qErtYCwBzZ2ellBfvnqc= +github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= +github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +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.0.2 h1:akYIkZ28e6A96dkWNJQu3nmCzH3YfwMPQExUYDaRv7w= +github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= +github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc= +github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= +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.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= +github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.mongodb.org/mongo-driver v1.7.5 h1:ny3p0reEpgsR2cfA5cjgwFZg3Cv/ofFh/8jbhGtz9VI= +go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= +go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/otel v0.11.0 h1:IN2tzQa9Gc4ZVKnTaMbPVcHjvzOdg5n9QfnmlqiET7E= +go.opentelemetry.io/otel v0.11.0/go.mod h1:G8UCk+KooF2HLkgo8RHX9epABH/aRGYET7gQOqBVdB0= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= +golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b h1:Qh4dB5D/WpoUUp3lSod7qgoyEHbDGPUWjIbnqdqqe1k= +google.golang.org/api v0.29.0 h1:BaiDisFir8O4IJxvAabCGGkQ6yCJegNQqSVoYUNAnbk= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/jinzhu/gorm.v1 v1.9.1 h1:63D1Sk0C0mhCbK930D0PkD3nKT8wLxz6lLPh5V6D2hM= +gopkg.in/jinzhu/gorm.v1 v1.9.1/go.mod h1:56JJPUzbikvTVnoyP1nppSkbJ2L8sunqTBDY2fDrmFg= +gopkg.in/olivere/elastic.v3 v3.0.75 h1:u3B8p1VlHF3yNLVOlhIWFT3F1ICcHfM5V6FFJe6pPSo= +gopkg.in/olivere/elastic.v3 v3.0.75/go.mod h1:yDEuSnrM51Pc8dM5ov7U8aI/ToR3PG0llA8aRv2qmw0= +gopkg.in/olivere/elastic.v5 v5.0.84 h1:acF/tRSg5geZpE3rqLglkS79CQMIMzOpWZE7hRXIkjs= +gopkg.in/olivere/elastic.v5 v5.0.84/go.mod h1:LXF6q9XNBxpMqrcgax95C6xyARXWbbCXUrtTxrNrxJI= +gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gorm.io/driver/mysql v1.0.1 h1:omJoilUzyrAp0xNoio88lGJCroGdIOen9hq2A/+3ifw= +gorm.io/driver/mysql v1.0.1/go.mod h1:KtqSthtg55lFp3S5kUXqlGaelnWpKitn4k1xZTnoiPw= +gorm.io/driver/postgres v1.0.0 h1:Yh4jyFQ0a7F+JPU0Gtiam/eKmpT/XFc1FKxotGqc6FM= +gorm.io/driver/postgres v1.0.0/go.mod h1:wtMFcOzmuA5QigNsgEIb7O5lhvH1tHAF1RbWmLWV4to= +gorm.io/driver/sqlserver v1.0.4 h1:V15fszi0XAo7fbx3/cF50ngshDSN4QT0MXpWTylyPTY= +gorm.io/driver/sqlserver v1.0.4/go.mod h1:ciEo5btfITTBCj9BkoUVDvgQbUdLWQNqdFY5OGuGnRg= +gorm.io/gorm v1.20.6 h1:qa7tC1WcU+DBI/ZKMxvXy1FcrlGsvxlaKufHrT2qQ08= +gorm.io/gorm v1.20.6/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.17.0 h1:H9d/lw+VkZKEVIUc8F3wgiQ+FUXTTr21M87jXLU7yqM= +k8s.io/api v0.17.0/go.mod h1:npsyOePkeP0CPwyGfXDHxvypiYMJxBWAMpQxCaJ4ZxI= +k8s.io/apimachinery v0.17.0 h1:xRBnuie9rXcPxUkDizUsGvPf1cnlZCFu210op7J7LJo= +k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/client-go v0.17.0 h1:8QOGvUGdqDMFrm9sD6IUFl256BcffynGoe80sxgTEDg= +k8s.io/client-go v0.17.0/go.mod h1:TYgR6EUHs6k45hb6KWjVD6jFZvJV4gHDikv/It0xz+k= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +mellium.im/sasl v0.2.1 h1:nspKSRg7/SyO0cRGY71OkfHab8tf9kCts6a6oTDut0w= +mellium.im/sasl v0.2.1/go.mod h1:ROaEDLQNuf9vjKqE1SrAfnsobm2YKXT1gnN1uDp1PjQ= +rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= +sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/openfeature_provider.go b/openfeature_provider.go index a79c781c..f12eb386 100644 --- a/openfeature_provider.go +++ b/openfeature_provider.go @@ -320,7 +320,7 @@ func createUserFromEvaluationContext(evalCtx openfeature.FlattenedContext) (User if targetingKey, ok := evalCtx[openfeature.TargetingKey].(string); ok { userId = targetingKey } else { - return DVCUser{}, errors.New("targetingKey must be a string") + return User{}, errors.New("targetingKey must be a string") } } if userId == "" { @@ -329,13 +329,13 @@ func createUserFromEvaluationContext(evalCtx openfeature.FlattenedContext) (User if userIdValue, ok := evalCtx[DEVCYCLE_USER_ID_KEY].(string); ok { userId = userIdValue } else { - return DVCUser{}, errors.New("userId must be a string") + return User{}, errors.New("userId must be a string") } } } if userId == "" { - return DVCUser{}, errors.New("targetingKey or userId must be provided") + return User{}, errors.New("targetingKey or userId must be provided") } user := User{ UserId: userId, diff --git a/ssemanager.go b/ssemanager.go new file mode 100644 index 00000000..7d56edb0 --- /dev/null +++ b/ssemanager.go @@ -0,0 +1,188 @@ +package devcycle + +import ( + "context" + "encoding/json" + "fmt" + "github.com/devcyclehq/go-server-sdk/v2/api" + "github.com/devcyclehq/go-server-sdk/v2/util" + "github.com/launchdarkly/eventsource" + "sync/atomic" + "time" +) + +type SSEManager struct { + configManager *EnvironmentConfigManager + options *Options + stream *eventsource.Stream + eventChannel chan eventsource.Event + url string + errorHandler eventsource.StreamErrorHandler + context context.Context + stopEventHandler context.CancelFunc + cfg *HTTPConfiguration + Started bool + Connected atomic.Bool +} + +type sseEvent struct { + Id string `json:"id"` + Timestamp float64 `json:"timestamp"` + Channel string `json:"channel"` + Data string `json:"data"` + Name string `json:"name"` +} +type sseMessage struct { + Etag string `json:"etag,omitempty"` + LastModified float64 `json:"lastModified,omitempty"` + Type_ string `json:"type,omitempty"` +} + +func (m *sseMessage) LastModifiedDuration() time.Duration { + return time.Duration(m.LastModified) * time.Millisecond +} + +func newSSEManager(configManager *EnvironmentConfigManager, options *Options, cfg *HTTPConfiguration) (*SSEManager, error) { + if options == nil { + return nil, fmt.Errorf("SSE - Options cannot be nil") + } + sseManager := &SSEManager{ + configManager: configManager, + options: options, + errorHandler: func(err error) eventsource.StreamErrorHandlerResult { + util.Debugf("SSE - Error: %v\n", err) + return eventsource.StreamErrorHandlerResult{ + CloseNow: false, + } + }, + cfg: cfg, + } + sseManager.Connected.Store(false) + + sseManager.context, sseManager.stopEventHandler = context.WithCancel(context.Background()) + + return sseManager, nil +} + +func (m *SSEManager) connectSSE(url string) (err error) { + // A stream is mutex locked - so we need to make sure we close it before we open a new one + // This is to prevent multiple streams from being opened, and to prevent race conditions on accessing/reading from + // the event stream + if m.stream != nil { + m.stream.Close() + } + sseClientEvent := api.ClientEvent{ + EventType: api.ClientEventType_InternalSSEConnected, + EventData: "Connected to SSE stream: " + url, + Status: "success", + Error: nil, + } + + defer func() { + m.configManager.InternalClientEvents <- sseClientEvent + }() + sse, err := eventsource.SubscribeWithURL(url, + eventsource.StreamOptionReadTimeout(m.options.RequestTimeout), + eventsource.StreamOptionCanRetryFirstConnection(m.options.RequestTimeout), + eventsource.StreamOptionErrorHandler(m.errorHandler), + eventsource.StreamOptionUseBackoff(m.options.RequestTimeout), + eventsource.StreamOptionUseJitter(0.25), + eventsource.StreamOptionHTTPClient(m.cfg.HTTPClient)) + if err != nil { + sseClientEvent.EventType = api.ClientEventType_InternalSSEFailure + sseClientEvent.Status = "failure" + sseClientEvent.Error = err + sseClientEvent.EventData = "Error connecting to SSE stream: " + url + return + } + m.Connected.Store(true) + m.stream = sse + m.eventChannel = m.stream.Events + m.Started = sseClientEvent.Error == nil + go m.receiveSSEMessages() + return +} + +func (m *SSEManager) parseMessage(rawMessage []byte) (message sseMessage, err error) { + event := sseEvent{} + err = json.Unmarshal(rawMessage, &event) + if err != nil { + return + } + + err = json.Unmarshal([]byte(event.Data), &message) + return +} + +func (m *SSEManager) receiveSSEMessages() { + for { + // If the stream is killed/stopped - we should stop polling + if m.stream == nil || m.context.Err() != nil { + m.Connected.Store(false) + m.configManager.InternalClientEvents <- api.ClientEvent{ + EventType: api.ClientEventType_InternalSSEFailure, + EventData: "SSE stream has been stopped", + Status: "failure", + Error: m.context.Err(), + } + return + } + err := func() error { + select { + case <-m.context.Done(): + m.Connected.Store(false) + return fmt.Errorf("SSE - Stopping SSE polling") + case event, ok := <-m.eventChannel: + if !ok { + return nil + } + + if m.options.ClientEventHandler != nil { + go func() { + m.options.ClientEventHandler <- api.ClientEvent{ + EventType: api.ClientEventType_RealtimeUpdates, + EventData: event, + Status: "info", + Error: nil, + } + }() + } + message, err := m.parseMessage([]byte(event.Data())) + if err != nil { + util.Debugf("SSE - Error unmarshalling message: %v\n", err) + return nil + } + if message.Type_ == "refetchConfig" || message.Type_ == "" { + util.Debugf("SSE - Received refetchConfig message: %v\n", message) + m.configManager.InternalClientEvents <- api.ClientEvent{ + EventType: api.ClientEventType_InternalNewConfigAvailable, + EventData: time.UnixMilli(int64(message.LastModified)), + Status: "", + Error: nil, + } + } + } + return nil + }() + if err != nil { + return + } + } +} + +func (m *SSEManager) StartSSEOverride(url string) error { + m.url = url + return m.connectSSE(url) +} + +func (m *SSEManager) StopSSE() { + if m.stream != nil { + m.stream.Close() + // Close wraps `close` and is safe to call in threads - this also just explicitly sets the stream to nil + m.stream = nil + } +} + +func (m *SSEManager) Close() { + m.stopEventHandler() +} diff --git a/ssemanager_test.go b/ssemanager_test.go new file mode 100644 index 00000000..2eb6f346 --- /dev/null +++ b/ssemanager_test.go @@ -0,0 +1,26 @@ +package devcycle + +import ( + "testing" +) + +const ( + test_sseFullData = "{\"id\":\"yATzPE/mOzgY:0\",\"timestamp\":1712853334259,\"channel\":\"dvc_server_4fedfbd7a1aef0848768c8fad8f4536ca57e0ba0_v1\",\"data\":\"{\\\"etag\\\":\\\"\\\\\\\"714bc6a9acb038971923289ee6ce665b\\\\\\\"\\\",\\\"lastModified\\\":1712853333000}\",\"name\":\"change\"}" +) + +func TestSSEManager_ParseMessage(t *testing.T) { + m := &SSEManager{} + message, err := m.parseMessage([]byte(test_sseFullData)) + if err != nil { + t.Fatal(err) + } + if message.Etag != "\"714bc6a9acb038971923289ee6ce665b\"" { + t.Fatal("message.Etag != \"714bc6a9acb038971923289ee6ce665b\"") + } + if message.LastModified != 1712853333000 { + t.Fatal("message.LastModified != 1712853333000") + } + if message.Type_ != "" { + t.Fatal("message.Type_ != \"\"") + } +} diff --git a/testdata/fixture_small_config_sse.json b/testdata/fixture_small_config_sse.json new file mode 100644 index 00000000..6a7190cf --- /dev/null +++ b/testdata/fixture_small_config_sse.json @@ -0,0 +1,162 @@ +{ + "project": { + "settings": { + "edgeDB": { + "enabled": false + }, + "optIn": { + "enabled": true, + "title": "Beta Feature Access", + "description": "Get early access to new features below", + "imageURL": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR68cgQT_BTgnhWTdfjUXSN8zM9Vpxgq82dhw&usqp=CAU", + "colors": { + "primary": "#0042f9", + "secondary": "#facc15" + } + } + }, + "a0_organization": "org_NszUFyWBFy7cr95J", + "_id": "6216420c2ea68943c8833c09", + "key": "default" + }, + "environment": { + "_id": "6216420c2ea68943c8833c0b", + "key": "development" + }, + "features": [ + { + "_id": "6216422850294da359385e8b", + "key": "test", + "type": "release", + "variations": [ + { + "variables": [ + { + "_var": "6216422850294da359385e8d", + "value": true + }, + { + "_var": "64de2b2486d4b575121589db", + "value": 123 + }, + { + "_var": "64de2b9486d4b275121589d1", + "value": 4.56 + }, + { + "_var": "64de2b2486d4b575121589dc", + "value": "on" + }, + { + "_var": "64de88bcc99ba02630f3df80", + "value": { + "message": "a" + } + } + ], + "name": "Variation On", + "key": "variation-on", + "_id": "6216422850294da359385e8f" + }, + { + "variables": [ + { + "_var": "6216422850294da359385e8d", + "value": false + }, + { + "_var": "64de2b2486d4b575121589db", + "value": 0 + }, + { + "_var": "64de2b9486d4b275121589d1", + "value": 7.89 + }, + { + "_var": "64de2b2486d4b575121589dc", + "value": "off" + }, + { + "_var": "64de88bcc99ba02630f3df80", + "value": { + "message": "b" + } + } + ], + "name": "Variation Off", + "key": "variation-off", + "_id": "6216422850294da359385e90" + } + ], + "configuration": { + "_id": "621642332ea68943c8833c4a", + "targets": [ + { + "distribution": [ + { + "percentage": 0.5, + "_variation": "6216422850294da359385e8f" + }, + { + "percentage": 0.5, + "_variation": "6216422850294da359385e90" + } + ], + "_audience": { + "_id": "621642332ea68943c8833c4b", + "filters": { + "operator": "and", + "filters": [ + { + "values": [], + "type": "all", + "filters": [] + } + ] + } + }, + "_id": "621642332ea68943c8833c4d" + } + ], + "forcedUsers": {} + } + } + ], + "variables": [ + { + "_id": "6216422850294da359385e8d", + "key": "test", + "type": "Boolean" + }, + { + "_id": "64de2b2486d4b575121589db", + "key": "test-number-variable", + "type": "Number" + }, + { + "_id": "64de2b9486d4b275121589d1", + "key": "test-float-variable", + "type": "Number" + }, + { + "_id": "64de2b2486d4b575121589dc", + "key": "test-string-variable", + "type": "String" + }, + { + "_id": "64de88bcc99ba02630f3df80", + "key": "test-json-variable", + "type": "JSON" + } + ], + "variableHashes": { + "test": 2447239932, + "test-number-variable": 3332991395, + "test-string-variable": 957171234, + "test-json-variable": 2814889459 + }, + "sse": { + "hostname": "https://sse.devcycle.com", + "path": "/v1/sse" + } +} \ No newline at end of file diff --git a/testing_helpers_test.go b/testing_helpers_test.go index 7106bca6..7aca094e 100644 --- a/testing_helpers_test.go +++ b/testing_helpers_test.go @@ -26,19 +26,19 @@ var ( test_large_config string test_large_config_variable = "v-key-25" - test_options = &Options{ + //go:embed testdata/fixture_small_config_sse.json + test_small_config_sse string + test_options = &Options{ // use defaults that will be set by the CheckDefaults EventFlushIntervalMS: time.Second * 30, ConfigPollingIntervalMS: time.Second * 10, } test_options_sse = &Options{ // use defaults that will be set by the CheckDefaults - EventFlushIntervalMS: time.Second * 30, - ConfigPollingIntervalMS: time.Second * 10, + EventFlushIntervalMS: time.Second * 30, + ConfigPollingIntervalMS: time.Second * 10, + EnableBetaRealtimeUpdates: true, } - benchmarkEnableEvents bool - benchmarkEnableConfigUpdates bool - benchmarkDisableLogs bool ) func TestMain(t *testing.M) { @@ -52,6 +52,7 @@ func TestMain(t *testing.M) { log.SetFlags(log.LstdFlags | log.Lshortfile) // Remove newlines in configs test_config = strings.ReplaceAll(test_config, "\n", "") + test_small_config_sse = strings.ReplaceAll(test_small_config_sse, "\n", "") test_config_special_characters_var = strings.ReplaceAll(test_config_special_characters_var, "\n", "") test_large_config = strings.ReplaceAll(test_large_config, "\n", "") @@ -99,6 +100,16 @@ func httpCustomConfigMock(sdkKey string, respcode int, config string) httpmock.R return responder } +func httpSSEConfigMock(respCode int, sdkKeys ...string) (sdkKey string, responder httpmock.Responder) { + if len(sdkKeys) == 0 { + sdkKey = generateTestSDKKey() + } else { + sdkKey = sdkKeys[0] + } + responder = httpCustomConfigMock(sdkKey, respCode, test_small_config_sse) + return +} + func sseResponseBody() string { timestamp := strconv.FormatInt(time.Now().Add(time.Second*-2).UnixMilli(), 10) return `{ @@ -134,4 +145,4 @@ func fatalErr(t *testing.T, err error) { if err != nil { t.Fatal(err) } -} +} \ No newline at end of file From f36ef79a9acf14fe9a08ea5bfb896cbc0f7545b6 Mon Sep 17 00:00:00 2001 From: Jamie Sinn Date: Mon, 3 Jun 2024 13:38:26 -0400 Subject: [PATCH 3/9] Cleanup rebase --- client_native_bucketing.go | 4 ---- client_test.go | 2 +- testing_helpers.go | 0 testing_helpers_test.go | 5 ++++- 4 files changed, 5 insertions(+), 6 deletions(-) delete mode 100644 testing_helpers.go diff --git a/client_native_bucketing.go b/client_native_bucketing.go index 15329398..61f1b645 100644 --- a/client_native_bucketing.go +++ b/client_native_bucketing.go @@ -57,10 +57,6 @@ func NewNativeLocalBucketing(sdkKey string, platformData *api.PlatformData, opti }, err } -func (n *NativeLocalBucketing) GetUUID() string { - return n.clientUUID -} - func (n *NativeLocalBucketing) StoreConfig(configJSON []byte, eTag, rayId, lastModified string) error { err := bucketing.SetConfig(configJSON, n.sdkKey, eTag, rayId, lastModified, n.eventQueue) if err != nil { diff --git a/client_test.go b/client_test.go index 7574a950..159f33dc 100644 --- a/client_test.go +++ b/client_test.go @@ -421,7 +421,7 @@ func TestClient_ConfigUpdatedEvent(t *testing.T) { return httpmock.GetCallCountInfo()["POST https://config-updated.devcycle.com/v1/events/batch"] >= 1 }, 1*time.Second, 100*time.Millisecond) } -func TestClient_ConfigUpdatedEvent(t *testing.T) { +func TestClient_ConfigUpdatedEvent_Detail(t *testing.T) { sdkKey, _ := httpConfigMock(200) responder := func(req *http.Request) (*http.Response, error) { reqBody, err := io.ReadAll(req.Body) diff --git a/testing_helpers.go b/testing_helpers.go deleted file mode 100644 index e69de29b..00000000 diff --git a/testing_helpers_test.go b/testing_helpers_test.go index 7aca094e..564e879b 100644 --- a/testing_helpers_test.go +++ b/testing_helpers_test.go @@ -39,6 +39,9 @@ var ( ConfigPollingIntervalMS: time.Second * 10, EnableBetaRealtimeUpdates: true, } + benchmarkEnableConfigUpdates bool + benchmarkEnableEvents bool + benchmarkDisableLogs bool ) func TestMain(t *testing.M) { @@ -145,4 +148,4 @@ func fatalErr(t *testing.T, err error) { if err != nil { t.Fatal(err) } -} \ No newline at end of file +} From 88e6b2838e4f424dbdb2c5f53e30dbbc33c662ef Mon Sep 17 00:00:00 2001 From: Jamie Sinn Date: Fri, 7 Jun 2024 12:57:36 -0400 Subject: [PATCH 4/9] fix rebase --- event_manager.go | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/event_manager.go b/event_manager.go index 4f08fbb3..4f6320d3 100644 --- a/event_manager.go +++ b/event_manager.go @@ -24,7 +24,6 @@ type InternalEventQueue interface { UserQueueLength() (int, error) GetUUID() string Metrics() (int32, int32, int32) - GetUUID() string } // EventManager is responsible for flushing the event queue and reporting events to the server. @@ -114,30 +113,6 @@ func (e *EventManager) QueueEvent(user User, event Event) error { return err } -func (e *EventManager) QueueSDKConfigEvent(req http.Request, resp http.Response) error { - uuid := e.GetUUID() - user := api.User{UserId: uuid} - - event := api.Event{ - Type_: api.EventType_SDKConfig, - UserId: uuid, - Target: req.RequestURI, - Value: -1, - MetaData: map[string]interface{}{ - "clientUUID": uuid, - "reqEtag": req.Header.Get("If-None-Match"), - "reqLastModified": req.Header.Get("If-Modified-Since"), - "resEtag": resp.Header.Get("Etag"), - "resLastModified": resp.Header.Get("Last-Modified"), - "resRayId": resp.Header.Get("Cf-Ray"), - "resStatus": resp.StatusCode, - "errMsg": resp.Status, - }, - } - // We don't actually care about this failing or succeeding. It's best effort to send the event. - return e.QueueEvent(user, event) -} - func (e *EventManager) QueueVariableDefaultedEvent(variableKey string, defaultReason string) error { return e.internalQueue.QueueVariableDefaulted(variableKey, defaultReason) } From 8bd7750c7b99cd28dfaeb2cdec783db0535c640d Mon Sep 17 00:00:00 2001 From: Jamie Sinn Date: Fri, 7 Jun 2024 13:01:16 -0400 Subject: [PATCH 5/9] add flush interval to configupdated base test --- client_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client_test.go b/client_test.go index 159f33dc..a98442ea 100644 --- a/client_test.go +++ b/client_test.go @@ -408,7 +408,7 @@ func TestClient_ConfigUpdatedEvent(t *testing.T) { } httpmock.RegisterResponder("POST", "https://config-updated.devcycle.com/v1/events/batch", responder) sdkKey, _ := httpConfigMock(200) - c, err := NewClient(sdkKey, &Options{EventsAPIURI: "https://config-updated.devcycle.com"}) + c, err := NewClient(sdkKey, &Options{EventsAPIURI: "https://config-updated.devcycle.com", EventFlushIntervalMS: 500 * time.Millisecond}) fatalErr(t, err) if !c.isInitialized { t.Fatal("Expected client to be initialized") @@ -416,7 +416,6 @@ func TestClient_ConfigUpdatedEvent(t *testing.T) { if !c.hasConfig() { t.Fatal("Expected client to have config") } - _ = c.FlushEvents() require.Eventually(t, func() bool { return httpmock.GetCallCountInfo()["POST https://config-updated.devcycle.com/v1/events/batch"] >= 1 }, 1*time.Second, 100*time.Millisecond) From c3b778905cab39aae29534f199f28028c67a423e Mon Sep 17 00:00:00 2001 From: Jamie Sinn Date: Tue, 11 Jun 2024 10:39:01 -0400 Subject: [PATCH 6/9] Add retry logic for last modified header when passed for SSE --- configmanager.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/configmanager.go b/configmanager.go index 925970c4..f61561ec 100644 --- a/configmanager.go +++ b/configmanager.go @@ -233,6 +233,11 @@ func (e *EnvironmentConfigManager) fetchConfig(numRetriesRemaining int, minimumL } return err } + + if lmHeader, parseError := time.Parse(time.RFC1123, resp.Header.Get("Last-Modified")); parseError == nil && len(minimumLastModified) > 0 && lmHeader.Before(minimumLastModified[0]) { + return e.fetchConfig(numRetriesRemaining-1, minimumLastModified[0]) + } + defer resp.Body.Close() switch statusCode := resp.StatusCode; { From bacea40ca12c15d132e513948598dee7ac5e0809 Mon Sep 17 00:00:00 2001 From: Jamie Sinn Date: Tue, 11 Jun 2024 11:25:33 -0400 Subject: [PATCH 7/9] Multi-line the one liner --- configmanager.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/configmanager.go b/configmanager.go index f61561ec..77fd41bc 100644 --- a/configmanager.go +++ b/configmanager.go @@ -233,9 +233,13 @@ func (e *EnvironmentConfigManager) fetchConfig(numRetriesRemaining int, minimumL } return err } - - if lmHeader, parseError := time.Parse(time.RFC1123, resp.Header.Get("Last-Modified")); parseError == nil && len(minimumLastModified) > 0 && lmHeader.Before(minimumLastModified[0]) { - return e.fetchConfig(numRetriesRemaining-1, minimumLastModified[0]) + lastModifiedHeaderTS, err := time.Parse(time.RFC1123, resp.Header.Get("Last-Modified")) + if err != nil { + util.Warnf("Error parsing Last-Modified header: %s\n", err) + return e.fetchConfig(numRetriesRemaining-1, minimumLastModified...) + } + if len(minimumLastModified) > 0 && lastModifiedHeaderTS.Before(minimumLastModified[0]) { + return e.fetchConfig(numRetriesRemaining-1, minimumLastModified...) } defer resp.Body.Close() From 3db8d5532db966fbd8c83b416a42648e91fa0a4a Mon Sep 17 00:00:00 2001 From: Jamie Sinn Date: Tue, 11 Jun 2024 11:44:46 -0400 Subject: [PATCH 8/9] Only parse if not an empty string --- configmanager.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/configmanager.go b/configmanager.go index 77fd41bc..37bf1972 100644 --- a/configmanager.go +++ b/configmanager.go @@ -233,13 +233,14 @@ func (e *EnvironmentConfigManager) fetchConfig(numRetriesRemaining int, minimumL } return err } - lastModifiedHeaderTS, err := time.Parse(time.RFC1123, resp.Header.Get("Last-Modified")) - if err != nil { - util.Warnf("Error parsing Last-Modified header: %s\n", err) - return e.fetchConfig(numRetriesRemaining-1, minimumLastModified...) - } - if len(minimumLastModified) > 0 && lastModifiedHeaderTS.Before(minimumLastModified[0]) { - return e.fetchConfig(numRetriesRemaining-1, minimumLastModified...) + lastModifiedHeader := resp.Header.Get("Last-Modified") + if lastModifiedHeader != "" { + lastModifiedHeaderTS, parseError := time.Parse(time.RFC1123, lastModifiedHeader) + if parseError == nil { + if len(minimumLastModified) > 0 && lastModifiedHeaderTS.Before(minimumLastModified[0]) { + return e.fetchConfig(numRetriesRemaining-1, minimumLastModified...) + } + } } defer resp.Body.Close() From 63f08b48f9f2121d87fc1438a8e21c7d9ca6365b Mon Sep 17 00:00:00 2001 From: Jamie Sinn Date: Tue, 11 Jun 2024 15:40:35 -0400 Subject: [PATCH 9/9] And check retries --- configmanager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configmanager.go b/configmanager.go index 37bf1972..fecbf1ad 100644 --- a/configmanager.go +++ b/configmanager.go @@ -237,7 +237,7 @@ func (e *EnvironmentConfigManager) fetchConfig(numRetriesRemaining int, minimumL if lastModifiedHeader != "" { lastModifiedHeaderTS, parseError := time.Parse(time.RFC1123, lastModifiedHeader) if parseError == nil { - if len(minimumLastModified) > 0 && lastModifiedHeaderTS.Before(minimumLastModified[0]) { + if len(minimumLastModified) > 0 && lastModifiedHeaderTS.Before(minimumLastModified[0]) && numRetriesRemaining > 0 { return e.fetchConfig(numRetriesRemaining-1, minimumLastModified...) } }