diff --git a/bin/run-benchmarks.sh b/bin/run-benchmarks.sh new file mode 100755 index 00000000000..f2d0b0c73fe --- /dev/null +++ b/bin/run-benchmarks.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e + +echo "Running benchmarks" + + +go test -json -benchtime 30s -run='^$' -bench BenchmarkPurgeLapsedOAuthTokens github.com/TykTechnologies/tyk/gateway diff --git a/cli/linter/linter.go b/cli/linter/linter.go index e743fac595c..fe622dedd36 100644 --- a/cli/linter/linter.go +++ b/cli/linter/linter.go @@ -23,7 +23,7 @@ func Run(schm string, paths []string) (string, []string, error) { schemaLoader := schema.NewBytesLoader([]byte(schm)) var orig map[string]interface{} - f, err := os.Open(conf.OriginalPath) + f, err := os.Open(conf.Private.OriginalPath) if err != nil { return "", nil, err } @@ -46,11 +46,11 @@ func Run(schm string, paths []string) (string, []string, error) { } // ensure it's well formatted and the keys are all lowercase - if err := config.WriteConf(conf.OriginalPath, &conf); err != nil { + if err := config.WriteConf(conf.Private.OriginalPath, &conf); err != nil { return "", nil, err } - return conf.OriginalPath, resultWarns(result), nil + return conf.Private.OriginalPath, resultWarns(result), nil } type stringFormat func(string) bool diff --git a/config/config.go b/config/config.go index 135e2bf713a..f2472fe0ecd 100644 --- a/config/config.go +++ b/config/config.go @@ -557,12 +557,7 @@ func (pwl *PortsWhiteList) Decode(value string) error { // Config is the configuration object used by Tyk to set up various parameters. type Config struct { - // OriginalPath is the path to the config file that is read. If - // none was found, it's the path to the default config file that - // was written. - OriginalPath string `json:"-"` - - // Force your Gateway to work only on a specifc domain name. Can be overriden by API custom domain. + // Force your Gateway to work only on a specific domain name. Can be overridden by API custom domain. HostName string `json:"hostname"` // If your machine has mulitple network devices or IPs you can force the Gateway to use the IP address you want. @@ -1015,6 +1010,8 @@ type Config struct { // Skip TLS verification for JWT JWKs url validation JWTSSLInsecureSkipVerify bool `json:"jwt_ssl_insecure_skip_verify"` + + Private Private `json:"-"` } type TykError struct { @@ -1171,12 +1168,12 @@ func WriteDefault(path string, conf *Config) error { // An error will be returned only if any of the paths existed but was // not a valid config file. func Load(paths []string, conf *Config) error { - var r io.Reader + var r io.ReadCloser for _, path := range paths { f, err := os.Open(path) if err == nil { r = f - conf.OriginalPath = path + conf.Private.OriginalPath = path break } if os.IsNotExist(err) { diff --git a/config/config_test.go b/config/config_test.go index 72df3670aca..29f046b47da 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -113,7 +113,7 @@ func TestConfigFiles(t *testing.T) { if _, err := os.Stat(path2); err == nil { t.Fatalf("Load with no configs wrote too many default config files") } - if conf.OriginalPath != path1 { + if conf.Private.OriginalPath != path1 { t.Fatalf("OriginalPath was not set properly") } @@ -122,7 +122,7 @@ func TestConfigFiles(t *testing.T) { if err := Load(paths, conf); err != nil { t.Fatalf("Load with an existing config errored") } - if conf.OriginalPath != path1 { + if conf.Private.OriginalPath != path1 { t.Fatalf("OriginalPath was not set properly") } @@ -134,7 +134,7 @@ func TestConfigFiles(t *testing.T) { if _, err := os.Stat(path1); err == nil { t.Fatalf("Load with a config wrote a default config file") } - if conf.OriginalPath != path2 { + if conf.Private.OriginalPath != path2 { t.Fatalf("OriginalPath was not set properly") } diff --git a/config/private.go b/config/private.go new file mode 100644 index 00000000000..aad8a7cb474 --- /dev/null +++ b/config/private.go @@ -0,0 +1,22 @@ +package config + +import "time" + +// Private contains configurations which are private, adding it to be part of config without exposing to customers. +type Private struct { + // OAuthTokensPurgeInterval specifies the interval at which lapsed tokens get purged. + OAuthTokensPurgeInterval int `json:"-"` + // OriginalPath is the path to the config file that is read. If + // none was found, it's the path to the default config file that + // was written. + OriginalPath string `json:"-"` +} + +// GetOAuthTokensPurgeInterval returns purge interval for lapsed OAuth tokens. +func (p Private) GetOAuthTokensPurgeInterval() time.Duration { + if p.OAuthTokensPurgeInterval != 0 { + return time.Second * time.Duration(p.OAuthTokensPurgeInterval) + } + + return time.Hour +} diff --git a/config/private_test.go b/config/private_test.go new file mode 100644 index 00000000000..e055368df31 --- /dev/null +++ b/config/private_test.go @@ -0,0 +1,20 @@ +package config + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestPrivate_GetOAuthTokensPurgeInterval(t *testing.T) { + t.Run("default value", func(t *testing.T) { + p := Private{} + assert.Equal(t, time.Hour, p.GetOAuthTokensPurgeInterval()) + }) + + t.Run("custom value", func(t *testing.T) { + p := Private{OAuthTokensPurgeInterval: 5} + assert.Equal(t, time.Second*5, p.GetOAuthTokensPurgeInterval()) + }) +} diff --git a/config/util.go b/config/util.go new file mode 100644 index 00000000000..aeb8850095e --- /dev/null +++ b/config/util.go @@ -0,0 +1,24 @@ +package config + +import ( + "strconv" +) + +// HostAddrs returns a sanitized list of hosts to connect to. +func (config *StorageOptionsConf) HostAddrs() (addrs []string) { + if len(config.Addrs) != 0 { + addrs = config.Addrs + } else { + for h, p := range config.Hosts { + addr := h + ":" + p + addrs = append(addrs, addr) + } + } + + if len(addrs) == 0 && config.Port != 0 { + addr := config.Host + ":" + strconv.Itoa(config.Port) + addrs = append(addrs, addr) + } + + return addrs +} diff --git a/config/util_test.go b/config/util_test.go new file mode 100644 index 00000000000..acb61240ff1 --- /dev/null +++ b/config/util_test.go @@ -0,0 +1,63 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHostAddrs(t *testing.T) { + tests := []struct { + name string + config StorageOptionsConf + want []string + }{ + { + name: "empty", + config: StorageOptionsConf{}, + }, + { + name: "addrs", + config: StorageOptionsConf{ + Addrs: []string{"host1:1234", "host2:5678"}, + }, + want: []string{"host1:1234", "host2:5678"}, + }, + { + name: "hosts map", + config: StorageOptionsConf{ + Hosts: map[string]string{ + "host1": "1234", + "host2": "5678", + }, + }, + want: []string{"host1:1234", "host2:5678"}, + }, + { + name: "addrs and host maps", + config: StorageOptionsConf{ + Addrs: []string{"host1:1234", "host2:5678"}, + Hosts: map[string]string{ + "host3": "1234", + "host4": "5678", + }, + }, + want: []string{"host1:1234", "host2:5678"}, + }, + { + name: "host and port", + config: StorageOptionsConf{ + Host: "localhost", + Port: 6379, + }, + want: []string{"localhost:6379"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.config.HostAddrs() + assert.ElementsMatch(t, tt.want, got) + }) + } +} diff --git a/gateway/api_test.go b/gateway/api_test.go index a6a52bd616d..99844411986 100644 --- a/gateway/api_test.go +++ b/gateway/api_test.go @@ -2043,7 +2043,7 @@ func TestOrgKeyHandler_LastUpdated(t *testing.T) { }...) } -func TestPurgeOAuthClientTokens(t *testing.T) { +func TestPurgeOAuthClientTokensEndpoint(t *testing.T) { conf := func(globalConf *config.Config) { // set tokens to be expired after 1 second globalConf.OauthTokenExpire = 1 diff --git a/gateway/oauth_manager.go b/gateway/oauth_manager.go index 90a870c3246..9ac2d4da0eb 100644 --- a/gateway/oauth_manager.go +++ b/gateway/oauth_manager.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/base64" "encoding/json" - "errors" "fmt" "math" "net/http" @@ -13,18 +12,18 @@ import ( "sync" "time" + "strconv" + "github.com/TykTechnologies/tyk/request" "github.com/hashicorp/go-multierror" "github.com/lonelycode/osin" "github.com/sirupsen/logrus" "golang.org/x/crypto/bcrypt" - "strconv" - "github.com/TykTechnologies/tyk/internal/uuid" "github.com/TykTechnologies/tyk/headers" - tykerrors "github.com/TykTechnologies/tyk/internal/errors" + "github.com/TykTechnologies/tyk/internal/errors" "github.com/TykTechnologies/tyk/storage" "github.com/TykTechnologies/tyk/user" ) @@ -234,8 +233,8 @@ const ( refreshToken = "refresh_token" ) -//in compliance with https://tools.ietf.org/html/rfc7009#section-2.1 -//ToDo: set an authentication mechanism +// in compliance with https://tools.ietf.org/html/rfc7009#section-2.1 +// ToDo: set an authentication mechanism func (o *OAuthHandlers) HandleRevokeToken(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { @@ -1195,10 +1194,22 @@ func (gw *Gateway) purgeLapsedOAuthTokens() error { } redisCluster := &storage.RedisCluster{KeyPrefix: "", HashKeys: false, RedisController: gw.RedisController} + + ok, err := redisCluster.Lock("oauth-purge-lock", time.Minute) + if err != nil { + log.WithError(err).Error("error acquiring lock to purge oauth tokens") + return err + } + + if !ok { + log.Info("oauth tokens purge lock not acquired, purging in background") + return nil + } + keys, err := redisCluster.ScanKeys(oAuthClientTokensKeyPattern) if err != nil { - log.WithError(err).Debug("error while scanning for tokens") + log.WithError(err).Error("error while scanning for tokens") return err } @@ -1224,7 +1235,7 @@ func (gw *Gateway) purgeLapsedOAuthTokens() error { close(errs) combinedErr := &multierror.Error{ - ErrorFormat: tykerrors.Formatter, + ErrorFormat: errors.Formatter, } for err := range errs { diff --git a/gateway/oauth_manager_test.go b/gateway/oauth_manager_test.go index 69fee07a1a6..e5b94ae4c9e 100644 --- a/gateway/oauth_manager_test.go +++ b/gateway/oauth_manager_test.go @@ -6,7 +6,9 @@ package gateway import ( "bytes" + "context" "encoding/json" + "errors" "net/url" "path" "reflect" @@ -14,7 +16,10 @@ import ( "strings" "testing" + "github.com/go-redis/redis/v8" + "github.com/go-redis/redismock/v8" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/TykTechnologies/tyk/config" @@ -64,6 +69,10 @@ const keyRulesWithMetadata = `{ "meta_data": {"key": "meta", "foo": "keybar"} }` +var ( + dummyErr = errors.New("dummy") +) + func buildTestOAuthSpec(apiGens ...func(spec *APISpec)) *APISpec { return BuildAPI(func(spec *APISpec) { spec.APIID = "999999" @@ -1307,51 +1316,229 @@ func TestJSONToFormValues(t *testing.T) { }) } -func TestPurgeOAuthClientTokensEvent(t *testing.T) { +func assertTokensLen(t *testing.T, storageManager storage.Handler, storageKey string, expectedTokensLen int) { + t.Helper() + nowTs := time.Now().Unix() + startScore := strconv.FormatInt(nowTs, 10) + tokens, _, err := storageManager.GetSortedSetRange(storageKey, startScore, "+inf") + assert.NoError(t, err) + assert.Equal(t, expectedTokensLen, len(tokens)) +} + +func TestPurgeOAuthClientTokens(t *testing.T) { + t.Run("event", func(t *testing.T) { + conf := func(globalConf *config.Config) { + // set tokens to be expired after 1 second + globalConf.OauthTokenExpire = 1 + // cleanup tokens older than 1 seconds + globalConf.OauthTokenExpiredRetainPeriod = 1 + } + + ts := StartTest(conf) + defer ts.Close() + + spec := ts.LoadTestOAuthSpec() + + clientID1, clientID2 := uuid.New(), uuid.New() + + ts.createOAuthClientIDAndTokens(t, spec, clientID1) + ts.createOAuthClientIDAndTokens(t, spec, clientID2) + storageKey1, storageKey2 := fmt.Sprintf("%s%s", prefixClientTokens, clientID1), + fmt.Sprintf("%s%s", prefixClientTokens, clientID2) + + storageManager := ts.Gw.getGlobalStorageHandler(generateOAuthPrefix(spec.APIID), false) + storageManager.Connect() + + assertTokensLen(t, storageManager, storageKey1, 3) + assertTokensLen(t, storageManager, storageKey2, 3) + + time.Sleep(time.Second * 2) + + // emit event + + n := Notification{ + Command: OAuthPurgeLapsedTokens, + Gw: ts.Gw, + } + ts.Gw.MainNotifier.Notify(n) + + assertTokensLen(t, storageManager, storageKey1, 0) + assertTokensLen(t, storageManager, storageKey2, 0) + }) + + t.Run("background", func(t *testing.T) { + + conf := func(globalConf *config.Config) { + // set tokens to be expired after 1 second + globalConf.OauthTokenExpire = 1 + // cleanup tokens older than 2 seconds + globalConf.OauthTokenExpiredRetainPeriod = 1 + } + + ts := StartTest(conf) + defer ts.Close() + + spec := ts.LoadTestOAuthSpec() + + clientID1, clientID2 := uuid.New(), uuid.New() + + ts.createOAuthClientIDAndTokens(t, spec, clientID1) + ts.createOAuthClientIDAndTokens(t, spec, clientID2) + storageKey1, storageKey2 := fmt.Sprintf("%s%s", prefixClientTokens, clientID1), + fmt.Sprintf("%s%s", prefixClientTokens, clientID2) + + storageManager := ts.Gw.getGlobalStorageHandler(generateOAuthPrefix(spec.APIID), false) + storageManager.Connect() + + assertTokensLen(t, storageManager, storageKey1, 3) + assertTokensLen(t, storageManager, storageKey2, 3) + + time.Sleep(time.Second * 2) + + assertTokensLen(t, storageManager, storageKey1, 0) + assertTokensLen(t, storageManager, storageKey2, 0) + }) + + t.Run("errors", func(t *testing.T) { + t.Run("lock err", func(t *testing.T) { + gw := Gateway{} + gw.SetConfig(config.Config{ + OauthTokenExpiredRetainPeriod: 1, + }) + db, mock := redismock.NewClientMock() + redisController := storage.NewRedisController(context.Background()) + redisController.MockWith(db, true) + gw.RedisController = redisController + mock.ExpectSetNX("oauth-purge-lock", "1", time.Minute).SetErr(dummyErr) + err := gw.purgeLapsedOAuthTokens() + assert.ErrorIs(t, err, dummyErr) + }) + + t.Run("lock failure", func(t *testing.T) { + gw := Gateway{} + gw.SetConfig(config.Config{ + OauthTokenExpiredRetainPeriod: 1, + }) + db, mock := redismock.NewClientMock() + redisController := storage.NewRedisController(context.Background()) + redisController.MockWith(db, true) + gw.RedisController = redisController + mock.ExpectSetNX("oauth-purge-lock", "1", time.Minute).SetVal(false) + err := gw.purgeLapsedOAuthTokens() + assert.NoError(t, err) + }) + + t.Run("scan keys error", func(t *testing.T) { + gw := Gateway{} + gw.SetConfig(config.Config{ + OauthTokenExpiredRetainPeriod: 1, + }) + db, mock := redismock.NewClientMock() + redisController := storage.NewRedisController(context.Background()) + redisController.MockWith(db, true) + gw.RedisController = redisController + mock.ExpectSetNX("oauth-purge-lock", "1", time.Minute).SetVal(true) + mock.ExpectScan(0, oAuthClientTokensKeyPattern, 0).SetErr(dummyErr) + err := gw.purgeLapsedOAuthTokens() + assert.ErrorIs(t, err, dummyErr) + }) + }) +} + +func BenchmarkPurgeLapsedOAuthTokens(b *testing.B) { conf := func(globalConf *config.Config) { // set tokens to be expired after 1 second globalConf.OauthTokenExpire = 1 // cleanup tokens older than 2 seconds globalConf.OauthTokenExpiredRetainPeriod = 2 + + globalConf.Private.OAuthTokensPurgeInterval = 1 } ts := StartTest(conf) defer ts.Close() - assertTokensLen := func(t *testing.T, storageManager storage.Handler, storageKey string, expectedTokensLen int) { - nowTs := time.Now().Unix() - startScore := strconv.FormatInt(nowTs, 10) - tokens, _, err := storageManager.GetSortedSetRange(storageKey, startScore, "+inf") - assert.NoError(t, err) - assert.Equal(t, expectedTokensLen, len(tokens)) + const ( + apiCount = 10 + clientsCount = 50 + tokensCount = 1000 + ) + + gwConf := ts.Gw.GetConfig() + + cfg := gwConf.Storage + timeout := 5 * time.Second + + opts := &redis.UniversalOptions{ + Addrs: cfg.HostAddrs(), + Username: cfg.Username, + Password: cfg.Password, + DB: cfg.Database, + DialTimeout: timeout, + ReadTimeout: timeout, + WriteTimeout: timeout, + IdleTimeout: 240 * timeout, + PoolSize: 500, } - spec := ts.LoadTestOAuthSpec() + fillZSet := func(client redis.UniversalClient, key string, count int) { + ctx := context.Background() + now := time.Now() + nowTs := now.Unix() + var setMembers []*redis.Z + for k := 0; k < count; k++ { + setMembers = append(setMembers, &redis.Z{ + Score: float64(nowTs - int64(k)), + Member: fmt.Sprintf("dummy-value-%d", k), + }) + } - clientID1, clientID2 := uuid.New(), uuid.New() + // add 10 more tokens to be not expired + for k := 0; k < count/10; k++ { + setMembers = append(setMembers, &redis.Z{ + Score: float64(nowTs + int64((k)*1000)), + Member: fmt.Sprintf("dummy-value-%d", k), + }) + } + client.ZAdd(ctx, key, setMembers...) + } - ts.createOAuthClientIDAndTokens(t, spec, clientID1) - ts.createOAuthClientIDAndTokens(t, spec, clientID2) - storageKey1, storageKey2 := fmt.Sprintf("%s%s", prefixClientTokens, clientID1), - fmt.Sprintf("%s%s", prefixClientTokens, clientID2) + copyZSet := func(client redis.UniversalClient, src, dst string) { + ctx := context.Background() + client.ZRangeStore(ctx, dst, redis.ZRangeArgs{ + Key: src, + Start: "0", + Stop: "+inf", + }) + } - storageManager := ts.Gw.getGlobalStorageHandler(generateOAuthPrefix(spec.APIID), false) - storageManager.Connect() + setup := func(tb testing.TB, client redis.UniversalClient) { + tb.Helper() + now := time.Now() + for i := 0; i < apiCount; i++ { + for j := 0; j < clientsCount; j++ { + //now := time.Now() + dst := fmt.Sprintf("oauth-data.%doauth-client-tokens.%d", i, j) + copyZSet(client, "api", dst) + //tb.Logf("copy zet elapsed %f", time.Since(now).Seconds()) + } - assertTokensLen(t, storageManager, storageKey1, 3) - assertTokensLen(t, storageManager, storageKey2, 3) + } + tb.Logf("setup time elapsed %f", time.Since(now).Seconds()) + } - time.Sleep(time.Second * 3) + client := redis.NewClient(opts.Simple()) // no S1021 + now := time.Now() + fillZSet(client, "api", tokensCount) + b.Logf("fill zet elapsed %f", time.Since(now).Seconds()) - // emit event + b.ReportAllocs() - n := Notification{ - Command: OAuthPurgeLapsedTokens, - Gw: ts.Gw, + for i := 0; i < b.N; i++ { + b.StopTimer() + setup(b, client) + b.StartTimer() + require.NoError(b, ts.Gw.purgeLapsedOAuthTokens()) + b.StopTimer() } - ts.Gw.MainNotifier.Notify(n) - - assertTokensLen(t, storageManager, storageKey1, 0) - assertTokensLen(t, storageManager, storageKey2, 0) - } diff --git a/gateway/redis_signal_handle_config.go b/gateway/redis_signal_handle_config.go index 2a2240b3401..fa4608a5919 100644 --- a/gateway/redis_signal_handle_config.go +++ b/gateway/redis_signal_handle_config.go @@ -128,7 +128,7 @@ func sanitizeConfig(mc map[string]interface{}) map[string]interface{} { } func (gw *Gateway) getExistingConfig() (map[string]interface{}, error) { - f, err := os.Open(gw.GetConfig().OriginalPath) + f, err := os.Open(gw.GetConfig().Private.OriginalPath) if err != nil { return nil, err } diff --git a/gateway/server.go b/gateway/server.go index 72510cd04ba..17dbe4355e5 100644 --- a/gateway/server.go +++ b/gateway/server.go @@ -19,19 +19,20 @@ import ( "strconv" "strings" "sync" + "time" "github.com/TykTechnologies/tyk/internal/crypto" "github.com/TykTechnologies/tyk/internal/httputil" "sync/atomic" textTemplate "text/template" - "time" "github.com/TykTechnologies/again" "github.com/TykTechnologies/drl" gas "github.com/TykTechnologies/goautosocket" "github.com/TykTechnologies/gorpc" "github.com/TykTechnologies/goverify" + "github.com/TykTechnologies/tyk/internal/scheduler" logstashHook "github.com/bshuster-repo/logrus-logstash-hook" "github.com/evalphobia/logrus_sentry" graylogHook "github.com/gemnasium/logrus-graylog-hook" @@ -1551,8 +1552,10 @@ func writeProfiles() { } func (gw *Gateway) start() { + conf := gw.GetConfig() + // Set up a default org manager so we can traverse non-live paths - if !gw.GetConfig().SupressDefaultOrgStore { + if !conf.SupressDefaultOrgStore { mainLog.Debug("Initialising default org store") gw.DefaultOrgStore.Init(gw.getGlobalStorageHandler("orgkey.", false)) //DefaultQuotaStore.Init(getGlobalStorageHandler(CloudHandler, "orgkey.", false)) @@ -1560,11 +1563,16 @@ func (gw *Gateway) start() { } // Start listening for reload messages - if !gw.GetConfig().SuppressRedisSignalReload { + if !conf.SuppressRedisSignalReload { go gw.startPubSubLoop() } - conf := gw.GetConfig() + purgeInterval := conf.Private.GetOAuthTokensPurgeInterval() + purgeJob := scheduler.NewJob("purge-oauth-tokens", gw.purgeLapsedOAuthTokens, purgeInterval) + + oauthTokensPurger := scheduler.NewScheduler(log) + go oauthTokensPurger.Start(gw.ctx, purgeJob) + if slaveOptions := conf.SlaveOptions; slaveOptions.UseRPC { mainLog.Debug("Starting RPC reload listener") gw.RPCListener = RPCStorageHandler{ diff --git a/go.mod b/go.mod index c57865f6b7c..4cc762f8661 100644 --- a/go.mod +++ b/go.mod @@ -34,10 +34,11 @@ require ( github.com/frankban/quicktest v1.11.0 // indirect github.com/gemnasium/logrus-graylog-hook v2.0.7+incompatible github.com/getsentry/raven-go v0.2.0 // indirect - github.com/go-redis/redis/v8 v8.3.1 + github.com/go-redis/redis/v8 v8.11.5 + github.com/go-redis/redismock/v8 v8.11.5 github.com/gocraft/health v0.0.0-20170925182251-8675af27fef0 github.com/gofrs/uuid v3.3.0+incompatible - github.com/golang/protobuf v1.4.2 + github.com/golang/protobuf v1.5.2 github.com/google/btree v1.0.0 // indirect github.com/google/go-cmp v0.5.6 // indirect github.com/gorilla/mux v1.8.0 @@ -49,7 +50,6 @@ require ( github.com/hashicorp/go-msgpack v0.5.5 // indirect github.com/hashicorp/go-multierror v1.1.0 github.com/hashicorp/go-retryablehttp v0.6.7 // indirect - github.com/hashicorp/go-version v1.1.0 github.com/hashicorp/memberlist v0.1.6 // indirect github.com/hashicorp/serf v0.8.6 // indirect github.com/hashicorp/vault/api v1.0.5-0.20200717191844-f687267c8086 @@ -94,7 +94,6 @@ require ( golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e golang.org/x/net v0.0.0-20211209124913-491a49abca63 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20211013075003-97ac67df715c // indirect golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect google.golang.org/appengine v1.6.1 // indirect google.golang.org/grpc v1.29.1 diff --git a/go.sum b/go.sum index 0630727129d..20570574d9c 100644 --- a/go.sum +++ b/go.sum @@ -89,6 +89,9 @@ github.com/certifi/gocertifi v0.0.0-20190905060710-a5e0173ced67/go.mod h1:GJKEex github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= @@ -160,9 +163,12 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= -github.com/go-redis/redis/v8 v8.3.1 h1:jEPCgHQopfNaABun3NVN9pv2K7RjstY/7UJD6UEKFEY= -github.com/go-redis/redis/v8 v8.3.1/go.mod h1:a2xkpBM7NJUN5V5kiF46X5Ltx4WeXJ9757X/ScKUBdE= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/go-redis/redismock/v8 v8.11.5 h1:RJFIiua58hrBrSpXhnGX3on79AU3S271H4ZhRI1wyVo= +github.com/go-redis/redismock/v8 v8.11.5/go.mod h1:UaAU9dEe1C+eGr+FHV5prCWIt0hafyPWbGMEWE0UWdA= 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/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= @@ -203,6 +209,9 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= @@ -215,9 +224,11 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -271,7 +282,6 @@ github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0= github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -298,6 +308,7 @@ github.com/huandu/xstrings v1.2.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= @@ -427,17 +438,23 @@ github.com/nsf/jsondiff v0.0.0-20210303162244-6ea32392771e h1:S+/ptYdZtpK/MDstwC github.com/nsf/jsondiff v0.0.0-20210303162244-6ea32392771e/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.1 h1:jMU0WaQrP0a/YAEq8eJmJKjBoMs+pClEr1vDMlM/Do4= -github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.0.0 h1:CcuG/HvWNkkaqCUpJifQY8z7qEMBJya6aLPx6ftGyjQ= +github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.10.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs= -github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= +github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= @@ -543,7 +560,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tidwall/gjson v1.11.0 h1:C16pk7tQNiH6VlCrtIXL1w8GaOsi1X3W8KDkE1BuYd4= @@ -588,8 +604,6 @@ github.com/xenolf/lego v0.3.2-0.20170618175828-28ead50ff1ca/go.mod h1:fwiGnfsIjG github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/otel v0.13.0 h1:2isEnyzjjJZq6r2EKMsFj4TxiQiexsM04AVhwbR/oBA= -go.opentelemetry.io/otel v0.13.0/go.mod h1:dlSNewoRYikTkotEnxdmuBHgzT+k/idJSfDv/FxEnOY= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= @@ -648,6 +662,7 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -680,19 +695,20 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211013075003-97ac67df715c h1:taxlMj0D/1sOAuv/CbSD+MMDof2vbyPTqz5FNYKpXt8= -golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -722,6 +738,7 @@ golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a h1:CB3a9Nez8M13wwlr/E2YtwoU+qYHKfC+JrDa45RXXoQ= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -751,6 +768,9 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -780,6 +800,8 @@ gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -794,5 +816,3 @@ rsc.io/letsencrypt v0.0.2 h1:CWRvaqcmyyWMhhhGes73TvuIjf7O3Crq6F+Xid/cWNI= rsc.io/letsencrypt v0.0.2/go.mod h1:buyQKZ6IXrRnB7TdkHP0RyEybLx18HHyOSoTyoOLqNY= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= -sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k= diff --git a/internal/errors/errors.go b/internal/errors/errors.go index 4faa273d4ae..70bd562ca6a 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -1,6 +1,15 @@ package errors -import "strings" +import ( + "errors" + "strings" +) + +var ( + New = errors.New + Is = errors.Is + Unwrap = errors.Unwrap +) func Formatter(errs []error) string { var result strings.Builder diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go new file mode 100644 index 00000000000..e97aa23a92c --- /dev/null +++ b/internal/scheduler/scheduler.go @@ -0,0 +1,105 @@ +// Package scheduler provides a simple job scheduling utility with support +// for running periodic tasks and graceful shutdown. +package scheduler + +import ( + "context" + "errors" + "sync" + "time" + + "github.com/sirupsen/logrus" +) + +// Break is an error used to indicate the need to break the scheduler loop. +// It's an internal mechanism for stopping a job's execution within the scheduler. +var Break = errors.New("internal: break scheduler loop") + +// Job represents a task that can be scheduled. Each Job has a Name, a Run function +// that performs the task, and an Interval that determines how often the task should run. +type Job struct { + Name string + Run func() error + Interval time.Duration +} + +// NewJob creates and returns a new Job with the specified name, task function, and interval. +func NewJob(name string, run func() error, interval time.Duration) *Job { + return &Job{ + Name: name, + Run: run, + Interval: interval, + } +} + +// Scheduler is responsible for executing Jobs at specified intervals. +type Scheduler struct { + logger *logrus.Logger + + mustBreak bool + stop chan bool + stopOnce sync.Once +} + +// NewScheduler creates and returns a new Scheduler with the specified logger. +func NewScheduler(logger *logrus.Logger) *Scheduler { + return &Scheduler{ + logger: logger, + stop: make(chan bool), + } +} + +// Logger creates and returns a logrus Entry with the scheduler prefix. +func (s *Scheduler) Logger() *logrus.Entry { + return s.logger.WithField("prefix", "scheduler") +} + +// Start begins the execution of the provided Job within the context of the Scheduler. +// It schedules the Job's Run function to be called at its specified interval. The job +// can be stopped via context cancellation, calling Close, or when the job returns the +// Break error. +func (s *Scheduler) Start(ctx context.Context, job *Job) { + tick := time.NewTicker(job.Interval) + + defer func() { + tick.Stop() + }() + + for { + logger := s.Logger().WithField("name", job.Name) + + err := job.Run() + + switch { + case errors.Is(err, Break): + s.mustBreak = true + logger.Info("job scheduler stopping") + case err != nil: + logger.WithError(err).Errorf("job run error") + default: + logger.Info("job run successful") + } + + if s.mustBreak { + break + } + + select { + case <-s.stop: + return + case <-ctx.Done(): + s.Close() + return + case <-tick.C: + } + } +} + +// Close gracefully stops the execution of any running Jobs in the Scheduler. +// It is safe to call multiple times and is concurrent-safe. +func (s *Scheduler) Close() error { + s.stopOnce.Do(func() { + close(s.stop) + }) + return nil +} diff --git a/internal/scheduler/scheduler_test.go b/internal/scheduler/scheduler_test.go new file mode 100644 index 00000000000..a3c98bfb97f --- /dev/null +++ b/internal/scheduler/scheduler_test.go @@ -0,0 +1,76 @@ +package scheduler_test + +import ( + "context" + "io" + "testing" + "time" + + logrus "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/assert" + + "github.com/TykTechnologies/tyk/internal/scheduler" +) + +func TestScheduler_Break(t *testing.T) { + logger, _ := logrus.NewNullLogger() + + s := scheduler.NewScheduler(logger) + + assert.NotEmpty(t, s) + + job := scheduler.NewJob("test", func() error { + return scheduler.Break + }, 1) + + s.Start(context.Background(), job) + + assert.NotNil(t, s) +} + +func TestScheduler_Close(t *testing.T) { + logger, _ := logrus.NewNullLogger() + + s := scheduler.NewScheduler(logger) + defer s.Close() + + job := scheduler.NewJob("test", func() error { + return nil + }, 1) + + go s.Start(context.Background(), job) + + assert.NotNil(t, s) +} + +func TestScheduler_Job_Errors(t *testing.T) { + logger, _ := logrus.NewNullLogger() + + testcases := []struct { + name string + err error + }{ + {name: "no error", err: nil}, + {name: "error", err: io.EOF}, + {name: "cancelled", err: context.Canceled}, + {name: "break", err: scheduler.Break}, + } + + for _, tc := range testcases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + job := scheduler.NewJob("test", func() error { + return tc.err + }, 1) + + runner := scheduler.NewScheduler(logger) + go runner.Start(ctx, job) + + time.Sleep(time.Millisecond) + }) + } +} diff --git a/storage/redis_cluster.go b/storage/redis_cluster.go index e37c70599fa..d3763e471e5 100644 --- a/storage/redis_cluster.go +++ b/storage/redis_cluster.go @@ -96,22 +96,8 @@ func NewRedisClusterPool(isCache, isAnalytics bool, conf config.Config) redis.Un return client } -func getRedisAddrs(config config.StorageOptionsConf) (addrs []string) { - if len(config.Addrs) != 0 { - addrs = config.Addrs - } else { - for h, p := range config.Hosts { - addr := h + ":" + p - addrs = append(addrs, addr) - } - } - - if len(addrs) == 0 && config.Port != 0 { - addr := config.Host + ":" + strconv.Itoa(config.Port) - addrs = append(addrs, addr) - } - - return addrs +func getRedisAddrs(conf config.StorageOptionsConf) (addrs []string) { + return conf.HostAddrs() } func clusterConnectionIsOpen(cluster *RedisCluster) bool { @@ -375,6 +361,24 @@ func (r *RedisCluster) SetRawKey(keyName, session string, timeout int64) error { return nil } +// Lock implements a distributed lock in a cluster. +func (r *RedisCluster) Lock(key string, timeout time.Duration) (bool, error) { + if err := r.up(); err != nil { + return false, err + } + singleton, err := r.singleton() + if err != nil { + return false, err + } + + res := singleton.SetNX(r.RedisController.ctx, key, "1", timeout) + if err := res.Err(); err != nil { + log.WithError(err).Error("Error trying to set value") + return false, err + } + return res.Val(), nil +} + // Decrement will decrement a key in redis func (r *RedisCluster) Decrement(keyName string) { keyName = r.fixKey(keyName) diff --git a/storage/redis_cluster_test.go b/storage/redis_cluster_test.go index e41127b90e2..81e34622add 100644 --- a/storage/redis_cluster_test.go +++ b/storage/redis_cluster_test.go @@ -2,13 +2,16 @@ package storage import ( "context" + "errors" "testing" "time" "github.com/go-redis/redis/v8" - "github.com/TykTechnologies/tyk/config" + "github.com/go-redis/redismock/v8" "github.com/stretchr/testify/assert" + + "github.com/TykTechnologies/tyk/config" ) var rc RedisController @@ -200,3 +203,85 @@ func TestCheckIsOpen(t *testing.T) { assert.NoError(t, err) } + +func TestLock(t *testing.T) { + t.Run("redis down", func(t *testing.T) { + db, _ := redismock.NewClientMock() + redisCluster := &RedisCluster{ + RedisController: &RedisController{ + ctx: context.Background(), + singlePool: db, + }, + } + redisCluster.RedisController.redisUp.Store(false) + + ok, err := redisCluster.Lock("lock-key", time.Second) + assert.Error(t, err) + assert.False(t, ok) + }) + + t.Run("redis not configured", func(t *testing.T) { + redisCluster := &RedisCluster{ + RedisController: &RedisController{ + ctx: context.Background(), + }, + } + redisCluster.RedisController.redisUp.Store(true) + + ok, err := redisCluster.Lock("lock-key", time.Second) + assert.Contains(t, err.Error(), "Error trying to get singleton instance") + assert.False(t, ok) + }) + + t.Run("lock success", func(t *testing.T) { + db, mock := redismock.NewClientMock() + mock.ExpectSetNX("lock-key", "1", time.Second).SetVal(true) + + redisCluster := &RedisCluster{ + RedisController: &RedisController{ + ctx: context.Background(), + singlePool: db, + }, + } + redisCluster.RedisController.redisUp.Store(true) + + ok, err := redisCluster.Lock("lock-key", time.Second) + assert.NoError(t, err) + assert.True(t, ok) + }) + + t.Run("lock failure", func(t *testing.T) { + db, mock := redismock.NewClientMock() + mock.ExpectSetNX("lock-key", "1", time.Second).SetVal(false) + + redisCluster := &RedisCluster{ + RedisController: &RedisController{ + ctx: context.Background(), + singlePool: db, + }, + } + redisCluster.RedisController.redisUp.Store(true) + + ok, err := redisCluster.Lock("lock-key", time.Second) + assert.NoError(t, err) + assert.False(t, ok) + }) + + t.Run("lock error", func(t *testing.T) { + db, mock := redismock.NewClientMock() + dummyErr := errors.New("dummy") + mock.ExpectSetNX("lock-key", "1", time.Second).SetErr(dummyErr) + + redisCluster := &RedisCluster{ + RedisController: &RedisController{ + ctx: context.Background(), + singlePool: db, + }, + } + redisCluster.RedisController.redisUp.Store(true) + + ok, err := redisCluster.Lock("lock-key", time.Second) + assert.Equal(t, dummyErr, err) + assert.False(t, ok) + }) +} diff --git a/storage/redis_controller.go b/storage/redis_controller.go index 79b0f659a4c..ab57ca10d86 100644 --- a/storage/redis_controller.go +++ b/storage/redis_controller.go @@ -241,3 +241,11 @@ func (rc *RedisController) establishConnection(v *RedisCluster, conf config.Conf } return clusterConnectionIsOpen(v) } + +// MockWith is used to mock redis controller with a redis client. +func (rc *RedisController) MockWith(client redis.UniversalClient, redisUp bool) { + rc.singlePool = client + rc.singleAnalyticsPool = client + rc.singleAnalyticsPool = client + rc.redisUp.Store(redisUp) +}